mirror of
https://github.com/noDRM/DeDRM_tools.git
synced 2026-03-20 13:08:55 +00:00
Compare commits
36 Commits
v10.0.1
...
f4634b5eab
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4634b5eab | ||
|
|
034137962c | ||
|
|
2b46f61eae | ||
|
|
e54bb3f700 | ||
|
|
5b3e3e420f | ||
|
|
f17b255159 | ||
|
|
b2b55531d3 | ||
|
|
b84cf9aeb8 | ||
|
|
d5473f1db0 | ||
|
|
a275d5d819 | ||
|
|
5ace15e912 | ||
|
|
e0fcd99bcb | ||
|
|
b11aadcca6 | ||
|
|
dbf4b54026 | ||
|
|
9c40b3ce5a | ||
|
|
80f511ade9 | ||
|
|
c11db59150 | ||
|
|
9c6f4ecc3b | ||
|
|
fbe9b5ea89 | ||
|
|
23a454205a | ||
|
|
586609bb2c | ||
|
|
96cf14f3ec | ||
|
|
1958989487 | ||
|
|
8986855a47 | ||
|
|
620c90b695 | ||
|
|
3b9c201421 | ||
|
|
db71d35b40 | ||
|
|
a16e66a023 | ||
|
|
3eb4eab18d | ||
|
|
cdd6402b9a | ||
|
|
78ac98fc1b | ||
|
|
d05594dcbc | ||
|
|
09a34cf7d9 | ||
|
|
ca6ec8f6d0 | ||
|
|
e9a6e80e5a | ||
|
|
33437073d6 |
3
.github/workflows/main.yml
vendored
3
.github/workflows/main.yml
vendored
@@ -10,10 +10,11 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Package
|
||||
run: python3 make_release.py 10.0.1
|
||||
run: python3 make_release.py
|
||||
- name: Upload
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: plugin
|
||||
path: |
|
||||
DeDRM_tools_*.zip
|
||||
DeDRM_tools.zip
|
||||
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -1 +1,9 @@
|
||||
# Mac files
|
||||
.DS_Store
|
||||
|
||||
# local test data
|
||||
/user_data/
|
||||
|
||||
# Cache
|
||||
/DeDRM_plugin/__pycache__
|
||||
/DeDRM_plugin/standalone/__pycache__
|
||||
@@ -13,16 +13,16 @@ platforms.
|
||||
|
||||
#### Install plugins
|
||||
- Download the DeDRM `.zip` archive from DeDRM_tools'
|
||||
[latest release](https://github.com/apprenticeharper/DeDRM_tools/releases/latest).
|
||||
[latest release](https://github.com/noDRM/DeDRM_tools/releases/latest).
|
||||
Then unzip it.
|
||||
- Add the DeDRM plugin to Calibre:
|
||||
```
|
||||
cd *the unzipped DeDRM_tools folder*
|
||||
calibre-customize --add DeDRM_calibre_plugin/DeDRM_plugin.zip
|
||||
calibre-customize --add DeDRM_plugin.zip
|
||||
```
|
||||
- Add the Obok plugin:
|
||||
```
|
||||
calibre-customize --add Obok_calibre_plugin/obok_plugin.zip
|
||||
calibre-customize --add Obok_plugin.zip
|
||||
```
|
||||
|
||||
#### Enter your keys
|
||||
|
||||
41
CHANGELOG.md
41
CHANGELOG.md
@@ -2,8 +2,10 @@
|
||||
|
||||
List of changes since the fork of Apprentice Harper's repository:
|
||||
|
||||
## Fixes in v10.0.0 (2021-11-17):
|
||||
|
||||
- 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.
|
||||
- ~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.~ (removed due to a DMCA request, see #18 )
|
||||
- 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.
|
||||
@@ -20,4 +22,39 @@ List of changes since the fork of Apprentice Harper's repository:
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
## Fixes in v10.0.1 (2021-11-19):
|
||||
|
||||
- Hotfix update to fix broken EPUB DRM removal due to a typo.
|
||||
|
||||
## Fixes in v10.0.2 (2021-11-29):
|
||||
|
||||
- Fix Kindle for Mac key retrieval (merged [apprenticeharper/DeDRM_tools#1936](https://github.com/apprenticeharper/DeDRM_tools/pull/1936) ), fixing #1.
|
||||
- Fix Adobe key retrieval in case the username has been changed (merged [apprenticeharper/DeDRM_tools#1946](https://github.com/apprenticeharper/DeDRM_tools/pull/1946) ). This should fix the error "failed to decrypt user key key".
|
||||
- Fix small issue with elibri watermark removal.
|
||||
- Adobe key name will now contain account email.
|
||||
|
||||
## Fixes on master (not yet released):
|
||||
|
||||
- Fix issue where importing a key from Adobe Digital Editions would fail in Python2 (Calibre < 5) if there were non-ASCII characters in the username.
|
||||
- Add code to support importing multiple decryption keys from ADE.
|
||||
- Improve epubtest.py to also detect Kobo & Apple DRM.
|
||||
- ~Small updates to the LCP DRM error messages.~ (removed due to a DMCA request, see #18 ).
|
||||
- Merge ignobleepub into ineptepub so there's no duplicate code.
|
||||
- Support extracting the B&N / Nook key from the NOOK Microsoft Store application (based on [this script](https://github.com/noDRM/DeDRM_tools/discussions/9) by fesiwi).
|
||||
- Support extracting the B&N / Nook key from a data dump of the NOOK Android application.
|
||||
- Support adding an existing PassHash / B&N key base64 string without having to write it to a file first.
|
||||
- Support extracting PassHash keys from Adobe Digital Editions.
|
||||
- Fix a bug that might have stopped the eReader PDB DRM removal from working (untested, I don't have any PDB books)
|
||||
- Fix a bug where the watermark removal code wouldn't run for DRM-free files.
|
||||
- ineptpdf: Add code to plugin to support "Standard" (tested) and "Adobe.APS" (untested) encrypted PDFs using the ineptpdf implementation (PDF passwords can be entered in the plugin settings)
|
||||
- ineptpdf: Support for decrypting PDF with owner password instead of user password.
|
||||
- ineptpdf: Add function to return Filter name.
|
||||
- ineptpdf: Support for V=5, R=5 and R=6 PDF files, and for AES256-encrypted PDFs.
|
||||
- ineptpdf: Disable cross-reference streams in the output file. This may make PDFs slightly larger, but the current code for cross-reference streams seems to be buggy and sometimes creates corrupted PDFs.
|
||||
- Drop support for importing key data from the ancient, pre "DeDRM" Calibre plugins ("Ignoble Epub DeDRM", "eReader PDB 2 PML" and "K4MobiDeDRM"). These are from 2011, I doubt anyone still has these installed, I can't even find a working link for these to test them. If you still have encryption keys in one of these plugins, you will need to update to DeDRM v10.0.2 or older (to convert the keys) before updating to DeDRM v10.0.3 or newer.
|
||||
- Some Python3 bugfixes for Amazon books (merged #10 by ableeker).
|
||||
- Fix a bug where extracting an Adobe key from ADE on Linux through Wine did fail when using the OpenSSL backend (instead of PyCrypto). See #13 and #14 for details, thanks acaloiaro for the bugfix.
|
||||
- Make the plugin work on Calibre 6 (Qt 6). If you're running the Calibre 6 beta and you notice any issues, please open a bug report.
|
||||
- Fix IndexError when DeDRMing some Amazon eBooks.
|
||||
|
||||
68
DeDRM_plugin/DeDRM_ADE PassHash Key_Help.htm
Normal file
68
DeDRM_plugin/DeDRM_ADE PassHash Key_Help.htm
Normal file
@@ -0,0 +1,68 @@
|
||||
<!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 Adobe PassHash (B&N) Keys</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 Adobe PassHash Keys</h1>
|
||||
|
||||
<p>Adobe PassHash is a variant of the Adobe DRM which is used by retailers like Barnes and Noble. Instead of using certificates and device-based authorization, this uses a username and password combination. In B&&Ns implementation however, the user never gets access to these credentials, just to the credential hash.</p>
|
||||
|
||||
<h3>Changes at Barnes & Noble</h3>
|
||||
|
||||
<p>Since 2014, Barnes & Noble is no longer using the default Adobe key generation algorithm, which used to be the full name as "username" and the full credit card number as "password" for the PassHash algorithm.
|
||||
Instead, they started generating a random key on their server and send that to the reading application during login. This means that the old method to decrypt these books will no longer work. </p>
|
||||
|
||||
<p>There used to be a way to use the Android app's API to simulate a login to the Barnes and Noble servers, but that API has been shut down a while ago, too, and so far nobody has reverse-engineered the new one.</p>
|
||||
|
||||
<h3>Importing PassHash / B&N keys</h3>
|
||||
|
||||
<p>On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a green plus sign (+). Clicking this button will open a new dialog for entering the necessary data to generate a new key.</p>
|
||||
<p>Currently, the only known ways to access the key are the following:</p>
|
||||
<ul>
|
||||
<li>B&N: The encryption key can be extracted from the NOOK reading application available in the Microsoft store, or from the old "Nook Study" application. To do that, click on the "Extract key from Nook Windows application" option.</li>
|
||||
<li>B&N: The encryption key can also be extracted from a data backup of the NOOK Android application. To do that, you'll need to have a rooted Android device, a hacked / modified Nook APK file, or an Android emulator to be able to access the app data. If you have that, click on "Extract key from Nook Android application" and follow the instructions.</li>
|
||||
<li>B&N: The NOOK Android application supports / accepts user-added CA certificates, so you can set up something like mitmproxy on your computer, tunnel your phone's traffic through that, and extract the ccHash key data from the server response. You can then add that hash through the "Base64-encoded PassHash key string" option.</li>
|
||||
<li>If you already have a copy of the Nook ccHash key string (or, more general, the PassHash key string) in base64 encoding, you can either click on "Import existing keyfiles" if it's a file in b64 format, or you click on the "Base64-encoded PassHash key string" option while adding a new PassHash key.</li>
|
||||
<li>For retailers other than B&N that are using the PassHash algorihm as intended, you can click on "Adobe PassHash username & password" to enter your credentials while adding a key. This is the same algorihm as the original credit card number based key generation for B&N.</li>
|
||||
<li>Windows only: If you've successfully opened a PassHash-encrypted book in Adobe Digital Editions by entering username and password, you can dump the stored credentials from ADE.</li>
|
||||
</ul>
|
||||
|
||||
|
||||
<p>After you've selected a key retrieval method from the settings, the dialog may change and request some additional information depending on the key retrieval method. Enter that, then click the OK button to create and store the generated key. Or Cancel if you don’t want to create a key.</p>
|
||||
<p>New keys are checked against the current list of keys before being added, and duplicates are discarded.</p>
|
||||
|
||||
<h3>Deleting Keys:</h3>
|
||||
|
||||
<p>On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a red "X". Clicking this button will delete the highlighted key in the list. You will be prompted once to be sure that’s what you truly mean to do. Once gone, it’s permanently gone.</p>
|
||||
|
||||
<h3>Renaming Keys:</h3>
|
||||
|
||||
<p>On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a sheet of paper. Clicking this button will prompt you to enter a new name for the highlighted key in the list. Enter the new name for the encryption key and click the OK button to use the new name, or Cancel to revert to the old name..</p>
|
||||
|
||||
<h3>Exporting Keys:</h3>
|
||||
|
||||
<p>On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a computer’s hard-drive. Use this button to export the highlighted key to a file (with a ‘.b64’ file name extension). Used for backup purposes or to migrate key data to other computers/calibre installations. The dialog will prompt you for a place to save the file.</p>
|
||||
|
||||
<h3>Importing Existing Keyfiles:</h3>
|
||||
|
||||
<p>At the bottom-left of the plugin’s customization dialog, you will see a button labeled "Import Existing Keyfiles". Use this button to import existing ‘.b64’ key files. Key files might come from being exported from this or older plugins, or may have been generated using the original i♥cabbages script, or you may have made it by following the instructions above.</p>
|
||||
|
||||
<p>Once done creating/deleting/renaming/importing decryption keys, click Close to exit the customization dialogue. Your changes will only be saved permanently when you click OK in the main configuration dialog.</p>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,68 +0,0 @@
|
||||
<!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 Barnes and Noble Keys</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 Barnes and Noble Keys</h1>
|
||||
|
||||
|
||||
<p>If you have upgraded from an earlier version of the plugin, any existing Barnes and Noble keys will have been automatically imported, so you might not need to do any more configuration. Continue reading for key generation and management instructions.</p>
|
||||
|
||||
<h3>Changes at Barnes & Noble</h3>
|
||||
|
||||
<p>In mid-2014, Barnes & Noble changed the way they generated encryption keys. Instead of deriving the key from the user's name and credit card number, they started generating a random key themselves, sending that key through to devices when they connected to the Barnes & Noble servers. This means that most users will now find that no combination of their name and CC# will work in decrypting their recently downloaded ebooks.</p>
|
||||
|
||||
<p>Someone commenting at Apprentice Alf's blog detailed a way to retrieve a new account key using the account's email address and password. This method has now been incorporated into the plugin.
|
||||
|
||||
<h3>Creating New Keys:</h3>
|
||||
|
||||
<p>On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a green plus sign (+). Clicking this button will open a new dialog for entering the necessary data to generate a new key.</p>
|
||||
<ul>
|
||||
<li><span class="bold">Unique Key Name:</span> this is a unique name you choose to help you identify the key. This name will show in the list of configured keys. Choose something that will help you remember the data (account email address) it was created with.</li>
|
||||
<li><span class="bold">B&N/nook account email address:</span> This is the default email address for your Barnes and Noble/nook account. This email will not be stored anywhere on your computer or in calibre. It will only be used to fetch the account key that from the B&N server, and it is that key that will be stored in the preferences.</li>
|
||||
<li><span class="bold">B&N/nook account password:</span> this is the password for your Barnes and Noble/nook account. As with the email address, this will not be stored anywhere on your computer or in calibre. It will only be used to fetch the key from the B&N server.</li>
|
||||
</ul>
|
||||
|
||||
<p>Click the OK button to create and store the generated key. Or Cancel if you don’t want to create a key.</p>
|
||||
<p>New keys are checked against the current list of keys before being added, and duplicates are discarded.</p>
|
||||
|
||||
<h3>Deleting Keys:</h3>
|
||||
|
||||
<p>On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a red "X". Clicking this button will delete the highlighted key in the list. You will be prompted once to be sure that’s what you truly mean to do. Once gone, it’s permanently gone.</p>
|
||||
|
||||
<h3>Renaming Keys:</h3>
|
||||
|
||||
<p>On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a sheet of paper. Clicking this button will prompt you to enter a new name for the highlighted key in the list. Enter the new name for the encryption key and click the OK button to use the new name, or Cancel to revert to the old name..</p>
|
||||
|
||||
<h3>Exporting Keys:</h3>
|
||||
|
||||
<p>On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a computer’s hard-drive. Use this button to export the highlighted key to a file (with a ‘.b64’ file name extension). Used for backup purposes or to migrate key data to other computers/calibre installations. The dialog will prompt you for a place to save the file.</p>
|
||||
|
||||
<h3>Importing Existing Keyfiles:</h3>
|
||||
|
||||
<p>At the bottom-left of the plugin’s customization dialog, you will see a button labeled "Import Existing Keyfiles". Use this button to import existing ‘.b64’ key files. Key files might come from being exported from this or older plugins, or may have been generated using the original i♥cabbages script, or you may have made it by following the instructions above.</p>
|
||||
|
||||
<p>Once done creating/deleting/renaming/importing decryption keys, click Close to exit the customization dialogue. Your changes will only be saved permanently when you click OK in the main configuration dialog.</p>
|
||||
|
||||
<h3>NOOK Study</h3>
|
||||
<p>Books downloaded through NOOK Study may or may not use the key found using the above method. If a book is not decrypted successfully with any of the keys, the plugin will attempt to recover keys from the NOOK Study log file and use them.</p>
|
||||
|
||||
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -17,7 +17,7 @@ p {margin-top: 0}
|
||||
|
||||
<body>
|
||||
|
||||
<h1>DeDRM Plugin <span class="version">(v10.0.0)</span></h1>
|
||||
<h1>DeDRM Plugin <span class="version">(v10.0.2)</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>
|
||||
|
||||
@@ -46,7 +46,7 @@ p {margin-top: 0}
|
||||
|
||||
<h3>Credits:</h3>
|
||||
<ul>
|
||||
<li>NoDRM for a bunch of updates and the Readium LCP support</li>
|
||||
<li>NoDRM for a bunch of updates and maintenance since November 2021, and the Readium LCP support</li>
|
||||
<li>The Dark Reverser for the Mobipocket and eReader scripts</li>
|
||||
<li>i♥cabbages for the Adobe Digital Editions scripts</li>
|
||||
<li>Skindle aka Bart Simpson for the Amazon Kindle for PC script</li>
|
||||
|
||||
39
DeDRM_plugin/DeDRM_PDF passphrase_Help.htm
Normal file
39
DeDRM_plugin/DeDRM_PDF passphrase_Help.htm
Normal file
@@ -0,0 +1,39 @@
|
||||
<!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 PDF passwords</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 PDF passwords</h1>
|
||||
|
||||
<p>PDF files can be protected with a password / passphrase that will be required to open the PDF file. Enter your passphrases in the plugin settings to have the plugin automatically remove this encryption / restriction from PDF files you import. </p>
|
||||
|
||||
|
||||
<h3>Entering a passphrase:</h3>
|
||||
|
||||
<p>On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a green plus sign (+). Clicking this button will open a new dialog for entering a new passphrase.</p>
|
||||
|
||||
<p>Just enter your passphrase for the PDF file, then click the OK button to save the passphrase. </p>
|
||||
|
||||
<h3>Deleting a passphrase:</h3>
|
||||
|
||||
<p>On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a red "X". Clicking this button will delete the highlighted passphrase from the list. You will be prompted once to be sure that’s what you truly mean to do. Once gone, it’s permanently gone.</p>
|
||||
|
||||
<p>Once done entering/deleting passphrases, click Close to exit the customization dialogue. Your changes will only be saved permanently when you click OK in the main configuration dialog.</p>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -21,6 +21,7 @@ li {margin-top: 0.5em}
|
||||
|
||||
<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>
|
||||
|
||||
<p>This plugin no longer supports removing the Readium LCP DRM due to a DMCA takedown request issued by Readium. Please read the <a href="https://github.com/github/dmca/blob/master/2022/01/2022-01-04-readium.md">takedown notice</a> or <a href="https://github.com/noDRM/DeDRM_tools/issues/18">this bug report</a> for more information.</p>
|
||||
|
||||
<h3>Entering an LCP passphrase:</h3>
|
||||
|
||||
|
||||
20
DeDRM_plugin/__calibre_compat_code.py
Normal file
20
DeDRM_plugin/__calibre_compat_code.py
Normal file
@@ -0,0 +1,20 @@
|
||||
|
||||
#@@CALIBRE_COMPAT_CODE_START@@
|
||||
import sys, os
|
||||
|
||||
# Explicitly allow importing everything ...
|
||||
if os.path.dirname(os.path.dirname(os.path.abspath(__file__))) not in sys.path:
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
if os.path.dirname(os.path.abspath(__file__)) not in sys.path:
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
# Bugfix for Calibre < 5:
|
||||
if "calibre" in sys.modules and sys.version_info[0] == 2:
|
||||
from calibre.utils.config import config_dir
|
||||
if os.path.join(config_dir, "plugins", "DeDRM.zip") not in sys.path:
|
||||
sys.path.insert(0, os.path.join(config_dir, "plugins", "DeDRM.zip"))
|
||||
|
||||
# Explicitly set the package identifier so we are allowed to import stuff ...
|
||||
#__package__ = "DeDRM_plugin"
|
||||
|
||||
#@@CALIBRE_COMPAT_CODE_END@@
|
||||
@@ -1,12 +1,13 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
# __init__.py for DeDRM_plugin
|
||||
# Copyright © 2008-2020 Apprentice Harper et al.
|
||||
# Copyright © 2021 NoDRM
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = '10.0.1'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
|
||||
@@ -80,30 +81,41 @@ __docformat__ = 'restructuredtext en'
|
||||
# 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.
|
||||
# 10.0.2 - Fix Kindle for Mac & update Adobe key retrieval
|
||||
|
||||
"""
|
||||
Decrypt DRMed ebooks.
|
||||
"""
|
||||
|
||||
PLUGIN_NAME = "DeDRM"
|
||||
PLUGIN_VERSION_TUPLE = tuple([int(x) for x in __version__.split(".")])
|
||||
PLUGIN_VERSION = ".".join([str(x)for x in PLUGIN_VERSION_TUPLE])
|
||||
# Include an html helpfile in the plugin's zipfile with the following name.
|
||||
RESOURCE_NAME = PLUGIN_NAME + '_Help.htm'
|
||||
|
||||
import codecs
|
||||
import sys, os
|
||||
import time
|
||||
import traceback
|
||||
|
||||
#@@CALIBRE_COMPAT_CODE@@
|
||||
|
||||
import __version
|
||||
|
||||
class DeDRMError(Exception):
|
||||
pass
|
||||
|
||||
from calibre.customize import FileTypePlugin
|
||||
from calibre.constants import iswindows, isosx
|
||||
from calibre.gui2 import is_ok_to_use_qt
|
||||
from calibre.utils.config import config_dir
|
||||
try:
|
||||
from calibre.customize import FileTypePlugin
|
||||
except:
|
||||
# Allow import without Calibre.
|
||||
class FileTypePlugin:
|
||||
pass
|
||||
|
||||
try:
|
||||
from calibre.constants import iswindows, isosx
|
||||
except:
|
||||
iswindows = sys.platform.startswith('win')
|
||||
isosx = sys.platform.startswith('darwin')
|
||||
|
||||
try:
|
||||
from calibre.utils.config import config_dir
|
||||
except:
|
||||
config_dir = ""
|
||||
|
||||
|
||||
# Wrap a stream so that output gets flushed immediately
|
||||
@@ -130,6 +142,10 @@ class SafeUnbuffered:
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
PLUGIN_NAME = __version.PLUGIN_NAME
|
||||
PLUGIN_VERSION = __version.PLUGIN_VERSION
|
||||
PLUGIN_VERSION_TUPLE = __version.PLUGIN_VERSION_TUPLE
|
||||
|
||||
class DeDRM(FileTypePlugin):
|
||||
name = PLUGIN_NAME
|
||||
description = "Removes DRM from Amazon Kindle, Adobe Adept (including Kobo), Readium LCP, Barnes & Noble, Mobipocket and eReader ebooks. Credit given to i♥cabbages and The Dark Reverser for the original stand-alone scripts."
|
||||
@@ -144,6 +160,10 @@ class DeDRM(FileTypePlugin):
|
||||
priority = 600
|
||||
|
||||
|
||||
def cli_main(self, data):
|
||||
from .standalone import main
|
||||
main(data)
|
||||
|
||||
def initialize(self):
|
||||
"""
|
||||
Dynamic modules can't be imported/loaded from a zipfile.
|
||||
@@ -196,10 +216,6 @@ class DeDRM(FileTypePlugin):
|
||||
traceback.print_exc()
|
||||
pass
|
||||
|
||||
# convert old preferences, if necessary.
|
||||
from calibre_plugins.dedrm.prefs import convertprefs
|
||||
convertprefs()
|
||||
|
||||
# mark that this version has been initialized
|
||||
os.mkdir(self.verdir)
|
||||
except Exception as e:
|
||||
@@ -214,7 +230,7 @@ class DeDRM(FileTypePlugin):
|
||||
postProcessStart = time.time()
|
||||
|
||||
try:
|
||||
import calibre_plugins.dedrm.prefs as prefs
|
||||
import prefs
|
||||
dedrmprefs = prefs.DeDRM_Prefs()
|
||||
|
||||
if dedrmprefs["deobfuscate_fonts"] is True:
|
||||
@@ -222,7 +238,7 @@ class DeDRM(FileTypePlugin):
|
||||
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
|
||||
import epubwatermark as watermark
|
||||
|
||||
# Remove Tolino's CDP watermark file
|
||||
path_to_ebook = watermark.removeCDPwatermark(self, path_to_ebook) or path_to_ebook
|
||||
@@ -230,12 +246,13 @@ class DeDRM(FileTypePlugin):
|
||||
# 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
|
||||
# Remove watermarks (Adobe, Pocketbook 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))
|
||||
|
||||
|
||||
|
||||
postProcessEnd = time.time()
|
||||
print("{0} v{1}: Post-processing took {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, postProcessEnd-postProcessStart))
|
||||
|
||||
return path_to_ebook
|
||||
|
||||
@@ -248,7 +265,7 @@ class DeDRM(FileTypePlugin):
|
||||
# It checks if there's fonts that need to be deobfuscated
|
||||
|
||||
try:
|
||||
import calibre_plugins.dedrm.epubfontdecrypt as epubfontdecrypt
|
||||
import epubfontdecrypt
|
||||
|
||||
output = self.temporary_file(".epub").name
|
||||
ret = epubfontdecrypt.decryptFontsBook(path_to_ebook, output)
|
||||
@@ -269,7 +286,7 @@ class DeDRM(FileTypePlugin):
|
||||
def ePubDecrypt(self,path_to_ebook):
|
||||
# Create a TemporaryPersistent file to work with.
|
||||
# Check original epub archive for zip errors.
|
||||
import calibre_plugins.dedrm.zipfix as zipfix
|
||||
import zipfix
|
||||
|
||||
inf = self.temporary_file(".epub")
|
||||
try:
|
||||
@@ -281,12 +298,12 @@ class DeDRM(FileTypePlugin):
|
||||
raise
|
||||
|
||||
# import the decryption keys
|
||||
import calibre_plugins.dedrm.prefs as prefs
|
||||
import prefs
|
||||
dedrmprefs = prefs.DeDRM_Prefs()
|
||||
|
||||
|
||||
# import the LCP handler
|
||||
import calibre_plugins.dedrm.lcpdedrm as lcpdedrm
|
||||
import lcpdedrm
|
||||
|
||||
if (lcpdedrm.isLCPbook(path_to_ebook)):
|
||||
try:
|
||||
@@ -300,290 +317,331 @@ class DeDRM(FileTypePlugin):
|
||||
|
||||
# Not an LCP book, do the normal EPUB (Adobe) handling.
|
||||
|
||||
# import the Barnes & Noble ePub handler
|
||||
import calibre_plugins.dedrm.ignobleepub as ignobleepub
|
||||
|
||||
|
||||
#check the book
|
||||
if ignobleepub.ignobleBook(inf.name):
|
||||
print("{0} v{1}: “{2}” is a secure Barnes & Noble ePub".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook)))
|
||||
|
||||
# Attempt to decrypt epub with each encryption key (generated or provided).
|
||||
for keyname, userkey in dedrmprefs['bandnkeys'].items():
|
||||
keyname_masked = "".join(("X" if (x.isdigit()) else x) for x in keyname)
|
||||
print("{0} v{1}: Trying Encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname_masked))
|
||||
of = self.temporary_file(".epub")
|
||||
|
||||
# Give the user key, ebook and TemporaryPersistent file to the decryption function.
|
||||
try:
|
||||
result = ignobleepub.decryptBook(userkey, inf.name, of.name)
|
||||
except:
|
||||
print("{0} v{1}: Exception when trying to decrypt after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||
traceback.print_exc()
|
||||
result = 1
|
||||
|
||||
of.close()
|
||||
|
||||
if result == 0:
|
||||
# Decryption was successful.
|
||||
# Return the modified PersistentTemporary file to calibre.
|
||||
return 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))
|
||||
|
||||
# perhaps we should see if we can get a key from a log file
|
||||
print("{0} v{1}: Looking for new NOOK Study Keys after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||
|
||||
# get the default NOOK Study keys
|
||||
defaultkeys = []
|
||||
|
||||
try:
|
||||
if iswindows or isosx:
|
||||
from calibre_plugins.dedrm.ignoblekey import nookkeys
|
||||
|
||||
defaultkeys = nookkeys()
|
||||
else: # linux
|
||||
from .wineutils import WineGetKeys
|
||||
|
||||
scriptpath = os.path.join(self.alfdir,"ignoblekey.py")
|
||||
defaultkeys = WineGetKeys(scriptpath, ".b64",dedrmprefs['adobewineprefix'])
|
||||
|
||||
except:
|
||||
print("{0} v{1}: Exception when getting default NOOK Study Key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||
traceback.print_exc()
|
||||
|
||||
newkeys = []
|
||||
for keyvalue in defaultkeys:
|
||||
if keyvalue not in dedrmprefs['bandnkeys'].values():
|
||||
newkeys.append(keyvalue)
|
||||
|
||||
if len(newkeys) > 0:
|
||||
try:
|
||||
for i,userkey in enumerate(newkeys):
|
||||
print("{0} v{1}: Trying a new default key".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||
|
||||
of = self.temporary_file(".epub")
|
||||
|
||||
# Give the user key, ebook and TemporaryPersistent file to the decryption function.
|
||||
try:
|
||||
result = ignobleepub.decryptBook(userkey, inf.name, of.name)
|
||||
except:
|
||||
print("{0} v{1}: Exception when trying to decrypt after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||
traceback.print_exc()
|
||||
result = 1
|
||||
|
||||
of.close()
|
||||
|
||||
if result == 0:
|
||||
# Decryption was a success
|
||||
# Store the new successful key in the defaults
|
||||
print("{0} v{1}: Saving a new default key".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||
try:
|
||||
dedrmprefs.addnamedvaluetoprefs('bandnkeys','nook_Study_key',keyvalue)
|
||||
dedrmprefs.writeprefs()
|
||||
print("{0} v{1}: Saved a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
|
||||
except:
|
||||
print("{0} v{1}: Exception saving a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||
traceback.print_exc()
|
||||
# Return the modified PersistentTemporary file to calibre.
|
||||
return self.postProcessEPUB(of.name)
|
||||
|
||||
print("{0} v{1}: Failed to decrypt with new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
print("{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
|
||||
raise DeDRMError("{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||
|
||||
# import the Adobe Adept ePub handler
|
||||
import calibre_plugins.dedrm.ineptepub as ineptepub
|
||||
# import the Adobe ePub handler
|
||||
import ineptepub
|
||||
|
||||
if ineptepub.adeptBook(inf.name):
|
||||
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 ineptepub.isPassHashBook(inf.name):
|
||||
# This is an Adobe PassHash / B&N encrypted eBook
|
||||
print("{0} v{1}: “{2}” is a secure PassHash-protected (B&N) ePub".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook)))
|
||||
|
||||
|
||||
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))
|
||||
# Attempt to decrypt epub with each encryption key (generated or provided).
|
||||
for keyname, userkey in dedrmprefs['bandnkeys'].items():
|
||||
print("{0} v{1}: Trying Encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname))
|
||||
of = self.temporary_file(".epub")
|
||||
try:
|
||||
|
||||
# Give the user key, ebook and TemporaryPersistent file to the decryption function.
|
||||
try:
|
||||
result = ineptepub.decryptBook(userkey, inf.name, of.name)
|
||||
except:
|
||||
print("{0} v{1}: Exception when trying to decrypt after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||
traceback.print_exc()
|
||||
result = 1
|
||||
|
||||
of.close()
|
||||
|
||||
if result == 0:
|
||||
# Decryption was successful.
|
||||
# Return the modified PersistentTemporary file to calibre.
|
||||
return 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))
|
||||
|
||||
# perhaps we should see if we can get a key from a log file
|
||||
print("{0} v{1}: Looking for new NOOK Keys after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||
|
||||
# get the default NOOK keys
|
||||
defaultkeys = []
|
||||
|
||||
###### Add keys from the NOOK Study application (ignoblekeyNookStudy.py)
|
||||
|
||||
try:
|
||||
defaultkeys_study = []
|
||||
if iswindows or isosx:
|
||||
from ignoblekeyNookStudy import nookkeys
|
||||
|
||||
defaultkeys_study = nookkeys()
|
||||
else: # linux
|
||||
from wineutils import WineGetKeys
|
||||
|
||||
scriptpath = os.path.join(self.alfdir,"ignoblekeyNookStudy.py")
|
||||
defaultkeys_study = WineGetKeys(scriptpath, ".b64",dedrmprefs['adobewineprefix'])
|
||||
|
||||
except:
|
||||
print("{0} v{1}: Exception when getting default NOOK Study Key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
###### Add keys from the NOOK Microsoft Store application (ignoblekeyNookStudy.py)
|
||||
|
||||
try:
|
||||
defaultkeys_store = []
|
||||
if iswindows:
|
||||
# That's a Windows store app, it won't run on Linux or MacOS anyways.
|
||||
# No need to waste time running Wine.
|
||||
from ignoblekeyWindowsStore import dump_keys as dump_nook_keys
|
||||
defaultkeys_store = dump_nook_keys(False)
|
||||
|
||||
except:
|
||||
print("{0} v{1}: Exception when getting default NOOK Microsoft App keys after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||
traceback.print_exc()
|
||||
|
||||
###### Add keys from Adobe PassHash ADE activation data (adobekey_get_passhash.py)
|
||||
|
||||
try:
|
||||
defaultkeys_ade = []
|
||||
if iswindows:
|
||||
# Right now this is only implemented for Windows. MacOS support still needs to be added.
|
||||
from adobekey_get_passhash import passhash_keys, ADEPTError
|
||||
try:
|
||||
defaultkeys_ade, names = passhash_keys()
|
||||
except ADEPTError:
|
||||
defaultkeys_ade = []
|
||||
if isosx:
|
||||
print("{0} v{1}: Dumping ADE PassHash data is not yet supported on MacOS.".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||
defaultkeys_ade = []
|
||||
except:
|
||||
print("{0} v{1}: Exception when getting PassHashes from ADE after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
###### Check if one of the new keys decrypts the book:
|
||||
|
||||
newkeys = []
|
||||
for keyvalue in defaultkeys_study:
|
||||
if keyvalue not in dedrmprefs['bandnkeys'].values() and keyvalue not in newkeys:
|
||||
newkeys.append(keyvalue)
|
||||
|
||||
if iswindows:
|
||||
for keyvalue in defaultkeys_store:
|
||||
if keyvalue not in dedrmprefs['bandnkeys'].values() and keyvalue not in newkeys:
|
||||
newkeys.append(keyvalue)
|
||||
|
||||
for keyvalue in defaultkeys_ade:
|
||||
if keyvalue not in dedrmprefs['bandnkeys'].values() and keyvalue not in newkeys:
|
||||
newkeys.append(keyvalue)
|
||||
|
||||
if len(newkeys) > 0:
|
||||
try:
|
||||
for i,userkey in enumerate(newkeys):
|
||||
|
||||
if len(userkey) == 0:
|
||||
print("{0} v{1}: Skipping empty key.".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||
continue
|
||||
|
||||
print("{0} v{1}: Trying a new default key".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||
|
||||
of = self.temporary_file(".epub")
|
||||
|
||||
# Give the user key, ebook and TemporaryPersistent file to the decryption function.
|
||||
try:
|
||||
result = ineptepub.decryptBook(userkey, inf.name, of.name)
|
||||
except:
|
||||
print("{0} v{1}: Exception when trying to decrypt after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||
traceback.print_exc()
|
||||
result = 1
|
||||
|
||||
of.close()
|
||||
|
||||
if result == 0:
|
||||
# Decryption was a success
|
||||
# Store the new successful key in the defaults
|
||||
print("{0} v{1}: Saving a new default key".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||
try:
|
||||
if userkey in defaultkeys_ade:
|
||||
dedrmprefs.addnamedvaluetoprefs('bandnkeys','ade_passhash_'+str(int(time.time())),keyvalue)
|
||||
else:
|
||||
dedrmprefs.addnamedvaluetoprefs('bandnkeys','nook_key_'+str(int(time.time())),keyvalue)
|
||||
dedrmprefs.writeprefs()
|
||||
print("{0} v{1}: Saved a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
|
||||
except:
|
||||
print("{0} v{1}: Exception saving a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||
traceback.print_exc()
|
||||
# Return the modified PersistentTemporary file to calibre.
|
||||
return 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))
|
||||
return inf.name
|
||||
|
||||
except:
|
||||
pass
|
||||
|
||||
# Looks like we were unable to decrypt the book ...
|
||||
return inf.name
|
||||
|
||||
else:
|
||||
# This is a "normal" Adobe eBook.
|
||||
|
||||
book_uuid = None
|
||||
try:
|
||||
# This tries to figure out which Adobe account UUID the book is licensed for.
|
||||
# If we know that we can directly use the correct key instead of having to
|
||||
# try them all.
|
||||
book_uuid = ineptepub.adeptGetUserUUID(inf.name)
|
||||
except:
|
||||
pass
|
||||
|
||||
if book_uuid is None:
|
||||
print("{0} v{1}: {2} is a secure Adobe Adept ePub".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook)))
|
||||
else:
|
||||
print("{0} v{1}: {2} is a secure Adobe Adept ePub for UUID {3}".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook), book_uuid))
|
||||
|
||||
|
||||
if book_uuid is not None:
|
||||
# Check if we have a key with that UUID in its name:
|
||||
for keyname, userkeyhex in dedrmprefs['adeptkeys'].items():
|
||||
if not book_uuid.lower() in keyname.lower():
|
||||
continue
|
||||
|
||||
# Found matching key
|
||||
userkey = codecs.decode(userkeyhex, 'hex')
|
||||
print("{0} v{1}: Trying UUID-matched encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname))
|
||||
of = self.temporary_file(".epub")
|
||||
try:
|
||||
result = ineptepub.decryptBook(userkey, inf.name, of.name)
|
||||
of.close()
|
||||
if result == 0:
|
||||
print("{0} v{1}: Decrypted with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname,time.time()-self.starttime))
|
||||
return self.postProcessEPUB(of.name)
|
||||
except ineptepub.ADEPTNewVersionError:
|
||||
print("{0} v{1}: Book uses unsupported (too new) Adobe DRM.".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||
return self.postProcessEPUB(path_to_ebook)
|
||||
|
||||
except:
|
||||
print("{0} v{1}: Exception when decrypting after {2:.1f} seconds - trying other keys".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
# Attempt to decrypt epub with each encryption key (generated or provided).
|
||||
for keyname, userkeyhex in dedrmprefs['adeptkeys'].items():
|
||||
userkey = codecs.decode(userkeyhex, 'hex')
|
||||
print("{0} v{1}: Trying Encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname))
|
||||
of = self.temporary_file(".epub")
|
||||
|
||||
# Give the user key, ebook and TemporaryPersistent file to the decryption function.
|
||||
try:
|
||||
result = ineptepub.decryptBook(userkey, inf.name, of.name)
|
||||
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))
|
||||
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
|
||||
|
||||
|
||||
# Attempt to decrypt epub with each encryption key (generated or provided).
|
||||
for keyname, userkeyhex in dedrmprefs['adeptkeys'].items():
|
||||
userkey = codecs.decode(userkeyhex, 'hex')
|
||||
print("{0} v{1}: Trying Encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname))
|
||||
of = self.temporary_file(".epub")
|
||||
|
||||
# Give the user key, ebook and TemporaryPersistent file to the decryption function.
|
||||
try:
|
||||
result = ineptepub.decryptBook(userkey, inf.name, of.name)
|
||||
except ineptepub.ADEPTNewVersionError:
|
||||
print("{0} v{1}: Book uses unsupported (too new) Adobe DRM.".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||
return self.postProcessEPUB(path_to_ebook)
|
||||
except:
|
||||
print("{0} v{1}: Exception when decrypting after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||
traceback.print_exc()
|
||||
result = 1
|
||||
|
||||
try:
|
||||
of.close()
|
||||
except:
|
||||
print("{0} v{1}: Exception closing temporary file after {2:.1f} seconds. Ignored.".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||
|
||||
if result == 0:
|
||||
# Decryption was successful.
|
||||
# Return the modified PersistentTemporary file to calibre.
|
||||
print("{0} v{1}: Decrypted with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname,time.time()-self.starttime))
|
||||
return 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))
|
||||
|
||||
# perhaps we need to get a new default ADE key
|
||||
print("{0} v{1}: Looking for new default Adobe Digital Editions Keys after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||
|
||||
# get the default Adobe keys
|
||||
defaultkeys = []
|
||||
|
||||
try:
|
||||
if iswindows or isosx:
|
||||
from calibre_plugins.dedrm.adobekey import adeptkeys
|
||||
|
||||
defaultkeys, defaultnames = adeptkeys()
|
||||
else: # linux
|
||||
from .wineutils import WineGetKeys
|
||||
|
||||
scriptpath = os.path.join(self.alfdir,"adobekey.py")
|
||||
defaultkeys, defaultnames = WineGetKeys(scriptpath, ".der",dedrmprefs['adobewineprefix'])
|
||||
|
||||
try:
|
||||
self.default_key = defaultkeys[0]
|
||||
except:
|
||||
print("{0} v{1}: No ADE key found".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||
except:
|
||||
print("{0} v{1}: Exception when getting default Adobe Key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||
traceback.print_exc()
|
||||
self.default_key = ""
|
||||
|
||||
newkeys = []
|
||||
newnames = []
|
||||
idx = 0
|
||||
for keyvalue in defaultkeys:
|
||||
if codecs.encode(keyvalue, 'hex').decode('ascii') not in dedrmprefs['adeptkeys'].values():
|
||||
newkeys.append(keyvalue)
|
||||
newnames.append("default_ade_key_uuid_" + defaultnames[idx])
|
||||
idx += 1
|
||||
|
||||
# Check for DeACSM keys:
|
||||
try:
|
||||
from calibre_plugins.dedrm.config import checkForDeACSMkeys
|
||||
|
||||
newkey, newname = checkForDeACSMkeys()
|
||||
|
||||
if newkey is not None:
|
||||
if codecs.encode(newkey, 'hex').decode('ascii') not in dedrmprefs['adeptkeys'].values():
|
||||
print("{0} v{1}: Found new key '{2}' in DeACSM plugin".format(PLUGIN_NAME, PLUGIN_VERSION, newname))
|
||||
newkeys.append(newkey)
|
||||
newnames.append(newname)
|
||||
except:
|
||||
traceback.print_exc()
|
||||
pass
|
||||
|
||||
if len(newkeys) > 0:
|
||||
try:
|
||||
for i,userkey in enumerate(newkeys):
|
||||
print("{0} v{1}: Trying a new default key".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||
of = self.temporary_file(".epub")
|
||||
|
||||
# Give the user key, ebook and TemporaryPersistent file to the decryption function.
|
||||
try:
|
||||
result = ineptepub.decryptBook(userkey, inf.name, of.name)
|
||||
except:
|
||||
print("{0} v{1}: Exception when decrypting after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||
traceback.print_exc()
|
||||
result = 1
|
||||
|
||||
try:
|
||||
of.close()
|
||||
except:
|
||||
print("{0} v{1}: Exception closing temporary file after {2:.1f} seconds. Ignored.".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||
|
||||
if result == 0:
|
||||
# Decryption was a success
|
||||
# Store the new successful key in the defaults
|
||||
print("{0} v{1}: Saving a new default key".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||
try:
|
||||
dedrmprefs.addnamedvaluetoprefs('adeptkeys', newnames[i], codecs.encode(userkey, 'hex').decode('ascii'))
|
||||
dedrmprefs.writeprefs()
|
||||
print("{0} v{1}: Saved a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
|
||||
except:
|
||||
print("{0} v{1}: Exception when saving a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||
traceback.print_exc()
|
||||
print("{0} v{1}: Decrypted with new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
|
||||
# Return the modified PersistentTemporary file to calibre.
|
||||
return self.postProcessEPUB(of.name)
|
||||
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 self.postProcessEPUB(of.name)
|
||||
|
||||
print("{0} v{1}: Failed to decrypt with new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
|
||||
except Exception as e:
|
||||
print("{0} v{1}: Unexpected Exception trying a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, 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))
|
||||
|
||||
# perhaps we need to get a new default ADE key
|
||||
print("{0} v{1}: Looking for new default Adobe Digital Editions Keys after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||
|
||||
# get the default Adobe keys
|
||||
defaultkeys = []
|
||||
|
||||
try:
|
||||
if iswindows or isosx:
|
||||
from adobekey import adeptkeys
|
||||
|
||||
defaultkeys, defaultnames = adeptkeys()
|
||||
else: # linux
|
||||
from wineutils import WineGetKeys
|
||||
|
||||
scriptpath = os.path.join(self.alfdir,"adobekey.py")
|
||||
defaultkeys, defaultnames = WineGetKeys(scriptpath, ".der",dedrmprefs['adobewineprefix'])
|
||||
|
||||
except:
|
||||
print("{0} v{1}: Exception when getting default Adobe Key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||
traceback.print_exc()
|
||||
|
||||
newkeys = []
|
||||
newnames = []
|
||||
idx = 0
|
||||
for keyvalue in defaultkeys:
|
||||
if codecs.encode(keyvalue, 'hex').decode('ascii') not in dedrmprefs['adeptkeys'].values():
|
||||
newkeys.append(keyvalue)
|
||||
newnames.append("default_ade_key_uuid_" + defaultnames[idx])
|
||||
idx += 1
|
||||
|
||||
# Check for DeACSM keys:
|
||||
try:
|
||||
from 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
|
||||
|
||||
# Something went wrong with decryption.
|
||||
print("{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
|
||||
raise DeDRMError("{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
|
||||
if len(newkeys) > 0:
|
||||
try:
|
||||
for i,userkey in enumerate(newkeys):
|
||||
print("{0} v{1}: Trying a new default key".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||
of = self.temporary_file(".epub")
|
||||
|
||||
# Give the user key, ebook and TemporaryPersistent file to the decryption function.
|
||||
try:
|
||||
result = ineptepub.decryptBook(userkey, inf.name, of.name)
|
||||
except:
|
||||
print("{0} v{1}: Exception when decrypting after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||
traceback.print_exc()
|
||||
result = 1
|
||||
|
||||
of.close()
|
||||
|
||||
if result == 0:
|
||||
# Decryption was a success
|
||||
# Store the new successful key in the defaults
|
||||
print("{0} v{1}: Saving a new default key".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||
try:
|
||||
dedrmprefs.addnamedvaluetoprefs('adeptkeys', newnames[i], codecs.encode(userkey, 'hex').decode('ascii'))
|
||||
dedrmprefs.writeprefs()
|
||||
print("{0} v{1}: Saved a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
|
||||
except:
|
||||
print("{0} v{1}: Exception when saving a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||
traceback.print_exc()
|
||||
print("{0} v{1}: Decrypted with new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
|
||||
# Return the modified PersistentTemporary file to calibre.
|
||||
return self.postProcessEPUB(of.name)
|
||||
|
||||
print("{0} v{1}: Failed to decrypt with new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
|
||||
except Exception as e:
|
||||
print("{0} v{1}: Unexpected Exception trying a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||
traceback.print_exc()
|
||||
pass
|
||||
|
||||
# Something went wrong with decryption.
|
||||
print("{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at noDRM's repository: https://github.com/noDRM/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 noDRM's repository: https://github.com/noDRM/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
|
||||
|
||||
|
||||
|
||||
# Not a Barnes & Noble nor an Adobe Adept
|
||||
# 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)))
|
||||
return self.postProcessEPUB(inf.name)
|
||||
#raise DeDRMError("{0} v{1}: Couldn't decrypt after {2:.1f} seconds. DRM free perhaps?".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
|
||||
|
||||
def PDFDecrypt(self,path_to_ebook):
|
||||
import calibre_plugins.dedrm.prefs as prefs
|
||||
import calibre_plugins.dedrm.ineptpdf as ineptpdf
|
||||
import calibre_plugins.dedrm.lcpdedrm as lcpdedrm
|
||||
|
||||
def PDFIneptDecrypt(self, path_to_ebook):
|
||||
# Sub function to prevent PDFDecrypt from becoming too large ...
|
||||
import prefs
|
||||
import ineptpdf
|
||||
dedrmprefs = prefs.DeDRM_Prefs()
|
||||
|
||||
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.
|
||||
@@ -591,12 +649,8 @@ class DeDRM(FileTypePlugin):
|
||||
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:
|
||||
if book_uuid is not None:
|
||||
print("{0} v{1}: {2} is a PDF ebook (EBX) for UUID {3}".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook), book_uuid))
|
||||
# Check if we have a key for that UUID
|
||||
for keyname, userkeyhex in dedrmprefs['adeptkeys'].items():
|
||||
if not book_uuid.lower() in keyname.lower():
|
||||
@@ -659,23 +713,18 @@ class DeDRM(FileTypePlugin):
|
||||
|
||||
try:
|
||||
if iswindows or isosx:
|
||||
from calibre_plugins.dedrm.adobekey import adeptkeys
|
||||
from adobekey import adeptkeys
|
||||
|
||||
defaultkeys, defaultnames = adeptkeys()
|
||||
else: # linux
|
||||
from .wineutils import WineGetKeys
|
||||
from wineutils import WineGetKeys
|
||||
|
||||
scriptpath = os.path.join(self.alfdir,"adobekey.py")
|
||||
defaultkeys, defaultnames = WineGetKeys(scriptpath, ".der",dedrmprefs['adobewineprefix'])
|
||||
|
||||
try:
|
||||
self.default_key = defaultkeys[0]
|
||||
except:
|
||||
print("{0} v{1}: No ADE key found".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||
except:
|
||||
print("{0} v{1}: Exception when getting default Adobe Key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||
traceback.print_exc()
|
||||
self.default_key = ""
|
||||
|
||||
newkeys = []
|
||||
newnames = []
|
||||
@@ -688,7 +737,7 @@ class DeDRM(FileTypePlugin):
|
||||
|
||||
# Check for DeACSM keys:
|
||||
try:
|
||||
from calibre_plugins.dedrm.config import checkForDeACSMkeys
|
||||
from config import checkForDeACSMkeys
|
||||
|
||||
newkey, newname = checkForDeACSMkeys()
|
||||
|
||||
@@ -738,8 +787,7 @@ class DeDRM(FileTypePlugin):
|
||||
# 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))
|
||||
print("{0} v{1}: Trying Encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname))
|
||||
of = self.temporary_file(".pdf")
|
||||
|
||||
# Give the user key, ebook and TemporaryPersistent file to the decryption function.
|
||||
@@ -763,10 +811,90 @@ class DeDRM(FileTypePlugin):
|
||||
|
||||
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))
|
||||
|
||||
def PDFStandardDecrypt(self, path_to_ebook):
|
||||
# Sub function to prevent PDFDecrypt from becoming too large ...
|
||||
import prefs
|
||||
import ineptpdf
|
||||
dedrmprefs = prefs.DeDRM_Prefs()
|
||||
|
||||
# Something went wrong with decryption.
|
||||
print("{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
|
||||
raise DeDRMError("{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||
# Attempt to decrypt PDF with each encryption key (generated or provided).
|
||||
i = -1
|
||||
for userpassword in [""] + dedrmprefs['adobe_pdf_passphrases']:
|
||||
# Try the empty password, too.
|
||||
i = i + 1
|
||||
userpassword = bytearray(userpassword, "utf-8")
|
||||
if i == 0:
|
||||
print("{0} v{1}: Trying empty password ... ".format(PLUGIN_NAME, PLUGIN_VERSION), end="")
|
||||
else:
|
||||
print("{0} v{1}: Trying password {2} ... ".format(PLUGIN_NAME, PLUGIN_VERSION, i), end="")
|
||||
of = self.temporary_file(".pdf")
|
||||
|
||||
# Give the user password, ebook and TemporaryPersistent file to the decryption function.
|
||||
msg = False
|
||||
try:
|
||||
result = ineptpdf.decryptBook(userpassword, path_to_ebook, of.name)
|
||||
print("done")
|
||||
msg = True
|
||||
except ineptpdf.ADEPTInvalidPasswordError:
|
||||
print("invalid password".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||
msg = True
|
||||
result = 1
|
||||
except:
|
||||
print("exception\n{0} v{1}: Exception when decrypting after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||
msg = True
|
||||
traceback.print_exc()
|
||||
result = 1
|
||||
if not msg:
|
||||
print("error\n{0} v{1}: Failed to decrypt after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
|
||||
|
||||
of.close()
|
||||
|
||||
if result == 0:
|
||||
# Decryption was successful.
|
||||
# Return the modified PersistentTemporary file to calibre.
|
||||
print("{0} v{1}: Successfully decrypted with password {3} after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime, i))
|
||||
return of.name
|
||||
|
||||
print("{0} v{1}: Didn't manage to decrypt PDF. Make sure the correct password is entered in the settings.".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||
|
||||
|
||||
|
||||
def PDFDecrypt(self,path_to_ebook):
|
||||
import prefs
|
||||
import ineptpdf
|
||||
import lcpdedrm
|
||||
dedrmprefs = prefs.DeDRM_Prefs()
|
||||
|
||||
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.
|
||||
|
||||
pdf_encryption = ineptpdf.getPDFencryptionType(path_to_ebook)
|
||||
if pdf_encryption is None:
|
||||
print("{0} v{1}: {2} is an unencrypted PDF file - returning as is.".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook)))
|
||||
return path_to_ebook
|
||||
|
||||
print("{0} v{1}: {2} is a PDF ebook with encryption {3}".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook), pdf_encryption))
|
||||
|
||||
if pdf_encryption == "EBX_HANDLER":
|
||||
# Adobe eBook / ADEPT (normal or B&N)
|
||||
return self.PDFIneptDecrypt(path_to_ebook)
|
||||
elif pdf_encryption == "Standard" or pdf_encryption == "Adobe.APS":
|
||||
return self.PDFStandardDecrypt(path_to_ebook)
|
||||
elif pdf_encryption == "FOPN_fLock" or pdf_encryption == "FOPN_foweb":
|
||||
print("{0} v{1}: FileOpen encryption '{2}' is unsupported.".format(PLUGIN_NAME, PLUGIN_VERSION, pdf_encryption))
|
||||
print("{0} v{1}: Try the standalone script from the 'Tetrachroma_FileOpen_ineptpdf' folder in the Github repo.".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||
return path_to_ebook
|
||||
else:
|
||||
print("{0} v{1}: Encryption '{2}' is unsupported.".format(PLUGIN_NAME, PLUGIN_VERSION, pdf_encryption))
|
||||
return path_to_ebook
|
||||
|
||||
|
||||
def KindleMobiDecrypt(self,path_to_ebook):
|
||||
@@ -777,8 +905,8 @@ class DeDRM(FileTypePlugin):
|
||||
# Had to move this import here so the custom libs can be
|
||||
# extracted to the appropriate places beforehand these routines
|
||||
# look for them.
|
||||
import calibre_plugins.dedrm.prefs as prefs
|
||||
import calibre_plugins.dedrm.k4mobidedrm
|
||||
import prefs
|
||||
import k4mobidedrm
|
||||
|
||||
dedrmprefs = prefs.DeDRM_Prefs()
|
||||
pids = dedrmprefs['pids']
|
||||
@@ -801,11 +929,11 @@ class DeDRM(FileTypePlugin):
|
||||
|
||||
try:
|
||||
if iswindows or isosx:
|
||||
from calibre_plugins.dedrm.kindlekey import kindlekeys
|
||||
from kindlekey import kindlekeys
|
||||
|
||||
defaultkeys = kindlekeys()
|
||||
else: # linux
|
||||
from .wineutils import WineGetKeys
|
||||
from wineutils import WineGetKeys
|
||||
|
||||
scriptpath = os.path.join(self.alfdir,"kindlekey.py")
|
||||
defaultkeys = WineGetKeys(scriptpath, ".k4i",dedrmprefs['kindlewineprefix'])
|
||||
@@ -833,8 +961,8 @@ class DeDRM(FileTypePlugin):
|
||||
pass
|
||||
if not decoded:
|
||||
#if you reached here then no luck raise and exception
|
||||
print("{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
|
||||
raise DeDRMError("{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
|
||||
print("{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at noDRM's repository: https://github.com/noDRM/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 noDRM's repository: https://github.com/noDRM/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
|
||||
|
||||
of = self.temporary_file(book.getBookExtension())
|
||||
book.getFile(of.name)
|
||||
@@ -845,14 +973,13 @@ class DeDRM(FileTypePlugin):
|
||||
|
||||
def eReaderDecrypt(self,path_to_ebook):
|
||||
|
||||
import calibre_plugins.dedrm.prefs as prefs
|
||||
import calibre_plugins.dedrm.erdr2pml
|
||||
import prefs
|
||||
import erdr2pml
|
||||
|
||||
dedrmprefs = prefs.DeDRM_Prefs()
|
||||
# Attempt to decrypt epub with each encryption key (generated or provided).
|
||||
for keyname, userkey in dedrmprefs['ereaderkeys'].items():
|
||||
keyname_masked = "".join(("X" if (x.isdigit()) else x) for x in keyname)
|
||||
print("{0} v{1}: Trying Encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname_masked))
|
||||
print("{0} v{1}: Trying Encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname))
|
||||
of = self.temporary_file(".pmlz")
|
||||
|
||||
# Give the userkey, ebook and TemporaryPersistent file to the decryption function.
|
||||
@@ -863,13 +990,13 @@ class DeDRM(FileTypePlugin):
|
||||
# Decryption was successful return the modified PersistentTemporary
|
||||
# file to Calibre's import process.
|
||||
if result == 0:
|
||||
print("{0} v{1}: Successfully decrypted with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname_masked,time.time()-self.starttime))
|
||||
print("{0} v{1}: Successfully 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_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,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))
|
||||
print("{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at noDRM's repository: https://github.com/noDRM/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 noDRM's repository: https://github.com/noDRM/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||
|
||||
|
||||
def run(self, path_to_ebook):
|
||||
@@ -890,11 +1017,11 @@ class DeDRM(FileTypePlugin):
|
||||
decrypted_ebook = self.eReaderDecrypt(path_to_ebook)
|
||||
pass
|
||||
elif booktype == 'pdf':
|
||||
# Adobe Adept PDF (hopefully)
|
||||
# Adobe PDF (hopefully) or LCP PDF
|
||||
decrypted_ebook = self.PDFDecrypt(path_to_ebook)
|
||||
pass
|
||||
elif booktype == 'epub':
|
||||
# Adobe Adept or B&N ePub
|
||||
# Adobe Adept, PassHash (B&N) or LCP ePub
|
||||
decrypted_ebook = self.ePubDecrypt(path_to_ebook)
|
||||
else:
|
||||
print("Unknown booktype {0}. Passing back to calibre unchanged".format(booktype))
|
||||
@@ -907,7 +1034,7 @@ class DeDRM(FileTypePlugin):
|
||||
return True
|
||||
|
||||
def config_widget(self):
|
||||
import calibre_plugins.dedrm.config as config
|
||||
import config
|
||||
return config.ConfigWidget(self.plugin_path, self.alfdir)
|
||||
|
||||
def save_settings(self, config_widget):
|
||||
|
||||
23
DeDRM_plugin/__main__.py
Normal file
23
DeDRM_plugin/__main__.py
Normal file
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# __main__.py for DeDRM_plugin
|
||||
# (CLI interface without Calibre)
|
||||
# Copyright © 2021 NoDRM
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
# For revision history see __init__.py
|
||||
|
||||
"""
|
||||
Run DeDRM plugin without Calibre.
|
||||
"""
|
||||
|
||||
# Import __init__.py from the standalone folder so we can have all the
|
||||
# standalone / non-Calibre code in that subfolder.
|
||||
|
||||
import standalone.__init__ as mdata
|
||||
import sys
|
||||
|
||||
mdata.main(sys.argv)
|
||||
12
DeDRM_plugin/__version.py
Normal file
12
DeDRM_plugin/__version.py
Normal file
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
#@@CALIBRE_COMPAT_CODE@@
|
||||
|
||||
PLUGIN_NAME = "DeDRM"
|
||||
__version__ = '10.0.2'
|
||||
|
||||
PLUGIN_VERSION_TUPLE = tuple([int(x) for x in __version__.split(".")])
|
||||
PLUGIN_VERSION = ".".join([str(x)for x in PLUGIN_VERSION_TUPLE])
|
||||
# Include an html helpfile in the plugin's zipfile with the following name.
|
||||
RESOURCE_NAME = PLUGIN_NAME + '_Help.htm'
|
||||
@@ -1,3 +1,5 @@
|
||||
# I think this file is unused?
|
||||
|
||||
import sys
|
||||
import tkinter
|
||||
import tkinter.constants
|
||||
@@ -1,6 +1,9 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
|
||||
|
||||
# I think this file is unused?
|
||||
|
||||
|
||||
import tkinter
|
||||
import tkinter.constants
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# adobekey.pyw, version 6.0
|
||||
# Copyright © 2009-2020 i♥cabbages, Apprentice Harper et al.
|
||||
# adobekey.pyw, version 7.1
|
||||
# Copyright © 2009-2021 i♥cabbages, Apprentice Harper et al.
|
||||
|
||||
# Released under the terms of the GNU General Public Licence, version 3
|
||||
# <http://www.gnu.org/licenses/>
|
||||
@@ -29,13 +29,16 @@
|
||||
# 5.9 - moved unicode_argv call inside main for Windows DeDRM compatibility
|
||||
# 6.0 - Work if TkInter is missing
|
||||
# 7.0 - Python 3 for calibre 5
|
||||
# 7.1 - Fix "failed to decrypt user key key" error (read username from registry)
|
||||
# 7.2 - Fix decryption error on Python2 if there's unicode in the username
|
||||
# 7.3 - Fix OpenSSL in Wine
|
||||
|
||||
"""
|
||||
Retrieve Adobe ADEPT user key.
|
||||
"""
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = '7.0'
|
||||
__version__ = '7.3'
|
||||
|
||||
import sys, os, struct, getopt
|
||||
from base64 import b64decode
|
||||
@@ -122,11 +125,27 @@ if iswindows:
|
||||
except ImportError:
|
||||
import _winreg as winreg
|
||||
|
||||
def get_fake_windows_libcrypto_path():
|
||||
# There seems to be a bug in Wine where a `find_library('libcrypto-1_1')`
|
||||
# will not return the path to the libcrypto-1_1.dll file.
|
||||
# So if we're on Windows, and we didn't find the libcrypto the normal way,
|
||||
# lets try a hack-y workaround. It's already over anyways at this
|
||||
# point, can't really make it worse.
|
||||
import sys, os
|
||||
for p in sys.path:
|
||||
if os.path.isfile(os.path.join(p, "libcrypto-1_1.dll")):
|
||||
return os.path.join(p, "libcrypto-1_1.dll")
|
||||
if os.path.isfile(os.path.join(p, "libeay32.dll")):
|
||||
return os.path.join(p, "libeay32.dll")
|
||||
return None
|
||||
|
||||
def _load_crypto_libcrypto():
|
||||
from ctypes.util import find_library
|
||||
libcrypto = find_library('libcrypto-1_1')
|
||||
if libcrypto is None:
|
||||
libcrypto = find_library('libeay32')
|
||||
if libcrypto is None:
|
||||
libcrypto = get_fake_windows_libcrypto_path()
|
||||
if libcrypto is None:
|
||||
raise ADEPTError('libcrypto not found')
|
||||
libcrypto = CDLL(libcrypto)
|
||||
@@ -168,7 +187,10 @@ if iswindows:
|
||||
return AES
|
||||
|
||||
def _load_crypto_pycrypto():
|
||||
from Crypto.Cipher import AES as _AES
|
||||
try:
|
||||
from Crypto.Cipher import AES as _AES
|
||||
except (ImportError, ModuleNotFoundError):
|
||||
from Cryptodome.Cipher import AES as _AES
|
||||
class AES(object):
|
||||
def __init__(self, key):
|
||||
self._aes = _AES.new(key, _AES.MODE_CBC, b'\x00'*16)
|
||||
@@ -182,7 +204,7 @@ if iswindows:
|
||||
try:
|
||||
AES = loader()
|
||||
break
|
||||
except (ImportError, ADEPTError):
|
||||
except (ImportError, ModuleNotFoundError, ADEPTError):
|
||||
pass
|
||||
return AES
|
||||
|
||||
@@ -237,6 +259,27 @@ if iswindows:
|
||||
return GetUserName
|
||||
GetUserName = GetUserName()
|
||||
|
||||
def GetUserName2():
|
||||
try:
|
||||
from winreg import OpenKey, QueryValueEx, HKEY_CURRENT_USER
|
||||
except ImportError:
|
||||
# We're on Python 2
|
||||
try:
|
||||
# The default _winreg on Python2 isn't unicode-safe.
|
||||
# Check if we have winreg_unicode, a unicode-safe alternative.
|
||||
# Without winreg_unicode, this will fail with Unicode chars in the username.
|
||||
from adobekey_winreg_unicode import OpenKey, QueryValueEx, HKEY_CURRENT_USER
|
||||
except:
|
||||
from _winreg import OpenKey, QueryValueEx, HKEY_CURRENT_USER
|
||||
|
||||
try:
|
||||
DEVICE_KEY_PATH = r'Software\Adobe\Adept\Device'
|
||||
regkey = OpenKey(HKEY_CURRENT_USER, DEVICE_KEY_PATH)
|
||||
userREG = QueryValueEx(regkey, 'username')[0].encode('utf-16-le')[::2]
|
||||
return userREG
|
||||
except:
|
||||
return None
|
||||
|
||||
PAGE_EXECUTE_READWRITE = 0x40
|
||||
MEM_COMMIT = 0x1000
|
||||
MEM_RESERVE = 0x2000
|
||||
@@ -274,8 +317,13 @@ if iswindows:
|
||||
|
||||
def __del__(self):
|
||||
if self._buf is not None:
|
||||
VirtualFree(self._buf)
|
||||
self._buf = None
|
||||
try:
|
||||
VirtualFree(self._buf)
|
||||
self._buf = None
|
||||
except TypeError:
|
||||
# Apparently this sometimes gets cleared on application exit
|
||||
# Causes a useless exception in the log, so let's just catch and ignore that.
|
||||
pass
|
||||
|
||||
if struct.calcsize("P") == 4:
|
||||
CPUID0_INSNS = (
|
||||
@@ -360,13 +408,15 @@ if iswindows:
|
||||
serial = GetVolumeSerialNumber(root)
|
||||
vendor = cpuid0()
|
||||
signature = struct.pack('>I', cpuid1())[1:]
|
||||
user = GetUserName()
|
||||
user = GetUserName2()
|
||||
if user is None:
|
||||
user = GetUserName()
|
||||
entropy = struct.pack('>I12s3s13s', serial, vendor, signature, user)
|
||||
cuser = winreg.HKEY_CURRENT_USER
|
||||
try:
|
||||
regkey = winreg.OpenKey(cuser, DEVICE_KEY_PATH)
|
||||
device = winreg.QueryValueEx(regkey, 'key')[0]
|
||||
except WindowsError:
|
||||
except WindowsError, FileNotFoundError:
|
||||
raise ADEPTError("Adobe Digital Editions not activated")
|
||||
keykey = CryptUnprotectData(device, entropy)
|
||||
userkey = None
|
||||
@@ -374,35 +424,55 @@ if iswindows:
|
||||
names = []
|
||||
try:
|
||||
plkroot = winreg.OpenKey(cuser, PRIVATE_LICENCE_KEY_PATH)
|
||||
except WindowsError:
|
||||
except WindowsError, FileNotFoundError:
|
||||
raise ADEPTError("Could not locate ADE activation")
|
||||
for i in range(0, 16):
|
||||
|
||||
i = -1
|
||||
while True:
|
||||
i = i + 1 # start with 0
|
||||
try:
|
||||
plkparent = winreg.OpenKey(plkroot, "%04d" % (i,))
|
||||
except WindowsError:
|
||||
except:
|
||||
# No more keys
|
||||
break
|
||||
|
||||
ktype = winreg.QueryValueEx(plkparent, None)[0]
|
||||
if ktype != 'credentials':
|
||||
continue
|
||||
uuid_name = "Unknown"
|
||||
uuid_name = ""
|
||||
for j in range(0, 16):
|
||||
try:
|
||||
plkkey = winreg.OpenKey(plkparent, "%04d" % (j,))
|
||||
except WindowsError:
|
||||
except WindowsError, FileNotFoundError:
|
||||
break
|
||||
ktype = winreg.QueryValueEx(plkkey, None)[0]
|
||||
if ktype == 'user':
|
||||
uuid_name = winreg.QueryValueEx(plkkey, 'value')[0]
|
||||
if ktype != 'privateLicenseKey':
|
||||
continue
|
||||
userkey = winreg.QueryValueEx(plkkey, 'value')[0]
|
||||
userkey = b64decode(userkey)
|
||||
aes = AES(keykey)
|
||||
userkey = aes.decrypt(userkey)
|
||||
userkey = userkey[26:-ord(userkey[-1:])]
|
||||
# print ("found " + uuid_name + " key: " + str(userkey))
|
||||
keys.append(userkey)
|
||||
names.append(uuid_name[9:])
|
||||
# Add Adobe UUID to key name
|
||||
uuid_name = uuid_name + winreg.QueryValueEx(plkkey, 'value')[0][9:] + "_"
|
||||
if ktype == 'username':
|
||||
# Add account type & email to key name, if present
|
||||
try:
|
||||
uuid_name = uuid_name + winreg.QueryValueEx(plkkey, 'method')[0] + "_"
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
uuid_name = uuid_name + winreg.QueryValueEx(plkkey, 'value')[0] + "_"
|
||||
except:
|
||||
pass
|
||||
if ktype == 'privateLicenseKey':
|
||||
userkey = winreg.QueryValueEx(plkkey, 'value')[0]
|
||||
userkey = b64decode(userkey)
|
||||
aes = AES(keykey)
|
||||
userkey = aes.decrypt(userkey)
|
||||
userkey = userkey[26:-ord(userkey[-1:])]
|
||||
# print ("found " + uuid_name + " key: " + str(userkey))
|
||||
keys.append(userkey)
|
||||
|
||||
if uuid_name == "":
|
||||
names.append("Unknown")
|
||||
else:
|
||||
names.append(uuid_name[:-1])
|
||||
|
||||
if len(keys) == 0:
|
||||
raise ADEPTError('Could not locate privateLicenseKey')
|
||||
print("Found {0:d} keys".format(len(keys)))
|
||||
@@ -439,22 +509,40 @@ elif isosx:
|
||||
return None
|
||||
|
||||
def adeptkeys():
|
||||
# TODO: All the code to support extracting multiple activation keys
|
||||
# TODO: seems to be Windows-only currently, still needs to be added for Mac.
|
||||
actpath = findActivationDat()
|
||||
if actpath is None:
|
||||
raise ADEPTError("Could not find ADE activation.dat file.")
|
||||
tree = etree.parse(actpath)
|
||||
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
||||
expr = '//%s/%s' % (adept('credentials'), adept('privateLicenseKey'))
|
||||
exprUUID = '//%s/%s' % (adept('credentials'), adept('user'))
|
||||
userkey = tree.findtext(expr)
|
||||
userUUID = "Unknown"
|
||||
|
||||
exprUUID = '//%s/%s' % (adept('credentials'), adept('user'))
|
||||
keyName = ""
|
||||
try:
|
||||
userUUID = tree.findtext(exprUUID)
|
||||
keyName = tree.findtext(exprUUID)[9:] + "_"
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
exprMail = '//%s/%s' % (adept('credentials'), adept('username'))
|
||||
keyName = keyName + tree.find(exprMail).attrib["method"] + "_"
|
||||
keyName = keyName + tree.findtext(exprMail) + "_"
|
||||
except:
|
||||
pass
|
||||
|
||||
if keyName == "":
|
||||
keyName = "Unknown"
|
||||
else:
|
||||
keyName = keyName[:-1]
|
||||
|
||||
|
||||
|
||||
userkey = b64decode(userkey)
|
||||
userkey = userkey[26:]
|
||||
return [userkey], [userUUID[9:]]
|
||||
return [userkey], [keyName]
|
||||
|
||||
else:
|
||||
def adeptkeys():
|
||||
|
||||
179
DeDRM_plugin/adobekey_get_passhash.py
Normal file
179
DeDRM_plugin/adobekey_get_passhash.py
Normal file
@@ -0,0 +1,179 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# adobekey_get_passhash.py, version 1
|
||||
# based on adobekey.pyw, version 7.2
|
||||
# Copyright © 2009-2021 i♥cabbages, Apprentice Harper et al.
|
||||
# Copyright © 2021 noDRM
|
||||
|
||||
# Released under the terms of the GNU General Public Licence, version 3
|
||||
# <http://www.gnu.org/licenses/>
|
||||
|
||||
# Revision history:
|
||||
# 1 - Initial release
|
||||
|
||||
"""
|
||||
Retrieve Adobe ADEPT user passhash keys
|
||||
"""
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = '1'
|
||||
|
||||
import sys, os, time
|
||||
import base64, hashlib
|
||||
try:
|
||||
from Cryptodome.Cipher import AES
|
||||
except:
|
||||
from Crypto.Cipher import AES
|
||||
|
||||
PASS_HASH_SECRET = "9ca588496a1bc4394553d9e018d70b9e"
|
||||
|
||||
def unpad(data):
|
||||
|
||||
if sys.version_info[0] == 2:
|
||||
pad_len = ord(data[-1])
|
||||
else:
|
||||
pad_len = data[-1]
|
||||
|
||||
return data[:-pad_len]
|
||||
|
||||
|
||||
try:
|
||||
from calibre.constants import iswindows, isosx
|
||||
except:
|
||||
iswindows = sys.platform.startswith('win')
|
||||
isosx = sys.platform.startswith('darwin')
|
||||
|
||||
|
||||
class ADEPTError(Exception):
|
||||
pass
|
||||
|
||||
def decrypt_passhash(passhash, fp):
|
||||
|
||||
serial_number = base64.b64decode(fp).hex()
|
||||
|
||||
hash_key = hashlib.sha1(bytearray.fromhex(serial_number + PASS_HASH_SECRET)).digest()[:16]
|
||||
|
||||
encrypted_cc_hash = base64.b64decode(passhash)
|
||||
cc_hash = unpad(AES.new(hash_key, AES.MODE_CBC, encrypted_cc_hash[:16]).decrypt(encrypted_cc_hash[16:]))
|
||||
return base64.b64encode(cc_hash).decode("ascii")
|
||||
|
||||
|
||||
if iswindows:
|
||||
try:
|
||||
import winreg
|
||||
except ImportError:
|
||||
import _winreg as winreg
|
||||
|
||||
PRIVATE_LICENCE_KEY_PATH = r'Software\Adobe\Adept\Activation'
|
||||
|
||||
def passhash_keys():
|
||||
cuser = winreg.HKEY_CURRENT_USER
|
||||
keys = []
|
||||
names = []
|
||||
try:
|
||||
plkroot = winreg.OpenKey(cuser, PRIVATE_LICENCE_KEY_PATH)
|
||||
except WindowsError:
|
||||
raise ADEPTError("Could not locate ADE activation")
|
||||
except FileNotFoundError:
|
||||
raise ADEPTError("Could not locate ADE activation")
|
||||
|
||||
idx = 1
|
||||
|
||||
fp = None
|
||||
|
||||
i = -1
|
||||
while True:
|
||||
i = i + 1 # start with 0
|
||||
try:
|
||||
plkparent = winreg.OpenKey(plkroot, "%04d" % (i,))
|
||||
except:
|
||||
# No more keys
|
||||
break
|
||||
|
||||
ktype = winreg.QueryValueEx(plkparent, None)[0]
|
||||
|
||||
if ktype == "activationToken":
|
||||
# find fingerprint for hash decryption
|
||||
j = -1
|
||||
while True:
|
||||
j = j + 1 # start with 0
|
||||
try:
|
||||
plkkey = winreg.OpenKey(plkparent, "%04d" % (j,))
|
||||
except WindowsError:
|
||||
break
|
||||
except FileNotFoundError:
|
||||
break
|
||||
ktype = winreg.QueryValueEx(plkkey, None)[0]
|
||||
if ktype == 'fingerprint':
|
||||
fp = winreg.QueryValueEx(plkkey, 'value')[0]
|
||||
#print("Found fingerprint: " + fp)
|
||||
|
||||
|
||||
# Note: There can be multiple lists, with multiple entries each.
|
||||
if ktype == 'passHashList':
|
||||
|
||||
# Find operator (used in key name)
|
||||
j = -1
|
||||
lastOperator = "Unknown"
|
||||
while True:
|
||||
j = j + 1 # start with 0
|
||||
try:
|
||||
plkkey = winreg.OpenKey(plkparent, "%04d" % (j,))
|
||||
except WindowsError:
|
||||
break
|
||||
except FileNotFoundError:
|
||||
break
|
||||
ktype = winreg.QueryValueEx(plkkey, None)[0]
|
||||
if ktype == 'operatorURL':
|
||||
operatorURL = winreg.QueryValueEx(plkkey, 'value')[0]
|
||||
try:
|
||||
lastOperator = operatorURL.split('//')[1].split('/')[0]
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
# Find hashes
|
||||
j = -1
|
||||
while True:
|
||||
j = j + 1 # start with 0
|
||||
try:
|
||||
plkkey = winreg.OpenKey(plkparent, "%04d" % (j,))
|
||||
except WindowsError:
|
||||
break
|
||||
except FileNotFoundError:
|
||||
break
|
||||
ktype = winreg.QueryValueEx(plkkey, None)[0]
|
||||
|
||||
if ktype == "passHash":
|
||||
passhash_encrypted = winreg.QueryValueEx(plkkey, 'value')[0]
|
||||
names.append("ADE_key_" + lastOperator + "_" + str(int(time.time())) + "_" + str(idx))
|
||||
idx = idx + 1
|
||||
keys.append(passhash_encrypted)
|
||||
|
||||
if fp is None:
|
||||
#print("Didn't find fingerprint for decryption ...")
|
||||
return [], []
|
||||
|
||||
print("Found {0:d} passhashes".format(len(keys)), file=sys.stderr)
|
||||
|
||||
keys_decrypted = []
|
||||
|
||||
for key in keys:
|
||||
decrypted = decrypt_passhash(key, fp)
|
||||
#print("Input key: " + key)
|
||||
#print("Output key: " + decrypted)
|
||||
keys_decrypted.append(decrypted)
|
||||
|
||||
return keys_decrypted, names
|
||||
|
||||
|
||||
else:
|
||||
def passhash_keys():
|
||||
raise ADEPTError("This script only supports Windows.")
|
||||
#TODO: Add MacOS support by parsing the activation.xml file.
|
||||
return [], []
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("This is a python calibre plugin. It can't be directly executed.")
|
||||
271
DeDRM_plugin/adobekey_winreg_unicode.py
Normal file
271
DeDRM_plugin/adobekey_winreg_unicode.py
Normal file
@@ -0,0 +1,271 @@
|
||||
# This is based on https://github.com/DanielStutzbach/winreg_unicode
|
||||
# The original _winreg in Python2 doesn't support unicode.
|
||||
# This causes issues if there's unicode chars in the username needed to decrypt the key.
|
||||
|
||||
'''
|
||||
Copyright 2010 Stutzbach Enterprises, LLC (daniel@stutzbachenterprises.com)
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following
|
||||
disclaimer in the documentation and/or other materials provided
|
||||
with the distribution.
|
||||
3. The name of the author may not be used to endorse or promote
|
||||
products derived from this software without specific prior written
|
||||
permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
|
||||
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
|
||||
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
||||
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
|
||||
IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
'''
|
||||
|
||||
import ctypes, ctypes.wintypes
|
||||
|
||||
ERROR_SUCCESS = 0
|
||||
ERROR_MORE_DATA = 234
|
||||
|
||||
KEY_READ = 0x20019
|
||||
|
||||
REG_NONE = 0
|
||||
REG_SZ = 1
|
||||
REG_EXPAND_SZ = 2
|
||||
REG_BINARY = 3
|
||||
REG_DWORD = 4
|
||||
REG_DWORD_BIG_ENDIAN = 5
|
||||
REG_DWORD_LITTLE_ENDIAN = 4
|
||||
REG_LINK = 6
|
||||
REG_MULTI_SZ = 7
|
||||
REG_RESOURCE_LIST = 8
|
||||
REG_FULL_RESOURCE_DESCRIPTOR = 9
|
||||
REG_RESOURCE_REQUIREMENTS_LIST = 10
|
||||
|
||||
c_HKEY = ctypes.c_void_p
|
||||
DWORD = ctypes.wintypes.DWORD
|
||||
BYTE = ctypes.wintypes.BYTE
|
||||
LPDWORD = ctypes.POINTER(DWORD)
|
||||
LPBYTE = ctypes.POINTER(BYTE)
|
||||
|
||||
advapi32 = ctypes.windll.advapi32
|
||||
|
||||
class FILETIME(ctypes.Structure):
|
||||
_fields_ = [("dwLowDateTime", DWORD),
|
||||
("dwHighDateTime", DWORD)]
|
||||
|
||||
RegCloseKey = advapi32.RegCloseKey
|
||||
RegCloseKey.restype = ctypes.c_long
|
||||
RegCloseKey.argtypes = [c_HKEY]
|
||||
|
||||
RegOpenKeyEx = advapi32.RegOpenKeyExW
|
||||
RegOpenKeyEx.restype = ctypes.c_long
|
||||
RegOpenKeyEx.argtypes = [c_HKEY, ctypes.c_wchar_p, ctypes.c_ulong,
|
||||
ctypes.c_ulong, ctypes.POINTER(c_HKEY)]
|
||||
|
||||
RegQueryInfoKey = advapi32.RegQueryInfoKeyW
|
||||
RegQueryInfoKey.restype = ctypes.c_long
|
||||
RegQueryInfoKey.argtypes = [c_HKEY, ctypes.c_wchar_p, LPDWORD, LPDWORD,
|
||||
LPDWORD, LPDWORD, LPDWORD, LPDWORD,
|
||||
LPDWORD, LPDWORD, LPDWORD,
|
||||
ctypes.POINTER(FILETIME)]
|
||||
|
||||
RegEnumValue = advapi32.RegEnumValueW
|
||||
RegEnumValue.restype = ctypes.c_long
|
||||
RegEnumValue.argtypes = [c_HKEY, DWORD, ctypes.c_wchar_p, LPDWORD,
|
||||
LPDWORD, LPDWORD, LPBYTE, LPDWORD]
|
||||
|
||||
RegEnumKeyEx = advapi32.RegEnumKeyExW
|
||||
RegEnumKeyEx.restype = ctypes.c_long
|
||||
RegEnumKeyEx.argtypes = [c_HKEY, DWORD, ctypes.c_wchar_p, LPDWORD,
|
||||
LPDWORD, ctypes.c_wchar_p, LPDWORD,
|
||||
ctypes.POINTER(FILETIME)]
|
||||
|
||||
RegQueryValueEx = advapi32.RegQueryValueExW
|
||||
RegQueryValueEx.restype = ctypes.c_long
|
||||
RegQueryValueEx.argtypes = [c_HKEY, ctypes.c_wchar_p, LPDWORD, LPDWORD,
|
||||
LPBYTE, LPDWORD]
|
||||
|
||||
def check_code(code):
|
||||
if code == ERROR_SUCCESS:
|
||||
return
|
||||
raise ctypes.WinError(2)
|
||||
|
||||
class HKEY(object):
|
||||
def __init__(self):
|
||||
self.hkey = c_HKEY()
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type=None, exc_val=None, exc_tb=None):
|
||||
self.Close()
|
||||
return False
|
||||
|
||||
def Detach(self):
|
||||
rv = self.cast(self.hkey, self.c_ulong).value
|
||||
self.hkey = c_HKEY()
|
||||
return rv
|
||||
|
||||
def __nonzero__(self):
|
||||
return bool(self.hkey)
|
||||
|
||||
def Close(self):
|
||||
if not self.hkey:
|
||||
return
|
||||
if RegCloseKey is None or check_code is None or c_HKEY is None:
|
||||
return # globals become None during exit
|
||||
rc = RegCloseKey(self.hkey)
|
||||
self.hkey = c_HKEY()
|
||||
check_code(rc)
|
||||
|
||||
def __del__(self):
|
||||
self.Close()
|
||||
|
||||
class RootHKEY(ctypes.Structure):
|
||||
def __init__(self, value):
|
||||
self.hkey = c_HKEY(value)
|
||||
|
||||
def Close(self):
|
||||
pass
|
||||
|
||||
HKEY_CLASSES_ROOT = RootHKEY(0x80000000)
|
||||
HKEY_CURRENT_USER = RootHKEY(0x80000001)
|
||||
HKEY_LOCAL_MACHINE = RootHKEY(0x80000002)
|
||||
HKEY_USERS = RootHKEY(0x80000003)
|
||||
HKEY_PERFORMANCE_DATA = RootHKEY(0x80000004)
|
||||
HKEY_CURRENT_CONFIG = RootHKEY(0x80000005)
|
||||
HKEY_DYN_DATA = RootHKEY(0x80000006)
|
||||
|
||||
def OpenKey(key, sub_key):
|
||||
new_key = HKEY()
|
||||
rc = RegOpenKeyEx(key.hkey, sub_key, 0, KEY_READ,
|
||||
ctypes.cast(ctypes.byref(new_key.hkey),
|
||||
ctypes.POINTER(c_HKEY)))
|
||||
check_code(rc)
|
||||
return new_key
|
||||
|
||||
def QueryInfoKey(key):
|
||||
null = LPDWORD()
|
||||
num_sub_keys = DWORD()
|
||||
num_values = DWORD()
|
||||
ft = FILETIME()
|
||||
rc = RegQueryInfoKey(key.hkey, ctypes.c_wchar_p(), null, null,
|
||||
ctypes.byref(num_sub_keys), null, null,
|
||||
ctypes.byref(num_values), null, null, null,
|
||||
ctypes.byref(ft))
|
||||
check_code(rc)
|
||||
return (num_sub_keys.value, num_values.value,
|
||||
ft.dwLowDateTime | (ft.dwHighDateTime << 32))
|
||||
|
||||
def EnumValue(key, index):
|
||||
null = LPDWORD()
|
||||
value_size = DWORD()
|
||||
data_size = DWORD()
|
||||
rc = RegQueryInfoKey(key.hkey, ctypes.c_wchar_p(), null, null, null,
|
||||
null, null, null,
|
||||
ctypes.byref(value_size), ctypes.byref(data_size),
|
||||
null, ctypes.POINTER(FILETIME)())
|
||||
check_code(rc)
|
||||
value_size.value += 1
|
||||
data_size.value += 1
|
||||
|
||||
value = ctypes.create_unicode_buffer(value_size.value)
|
||||
|
||||
while True:
|
||||
data = ctypes.create_string_buffer(data_size.value)
|
||||
|
||||
tmp_value_size = DWORD(value_size.value)
|
||||
tmp_data_size = DWORD(data_size.value)
|
||||
typ = DWORD()
|
||||
rc = RegEnumValue(key.hkey, index,
|
||||
ctypes.cast(value, ctypes.c_wchar_p),
|
||||
ctypes.byref(tmp_value_size), null,
|
||||
ctypes.byref(typ),
|
||||
ctypes.cast(data, LPBYTE),
|
||||
ctypes.byref(tmp_data_size))
|
||||
|
||||
if rc != ERROR_MORE_DATA:
|
||||
break
|
||||
|
||||
data_size.value *= 2
|
||||
|
||||
check_code(rc)
|
||||
return (value.value, Reg2Py(data, tmp_data_size.value, typ.value),
|
||||
typ.value)
|
||||
|
||||
def split_multi_sz(data, size):
|
||||
if size == 0:
|
||||
return []
|
||||
Q = size
|
||||
P = 0
|
||||
rv = []
|
||||
while P < Q and data[P].value != u'\0':
|
||||
rv.append[P]
|
||||
while P < Q and data[P].value != u'\0':
|
||||
P += 1
|
||||
P += 1
|
||||
rv.append(size)
|
||||
return [ctypes.wstring_at(ctypes.pointer(data[rv[i]]),
|
||||
rv[i+1] - rv[i]).rstrip(u'\x00')
|
||||
for i in range(len(rv)-1)]
|
||||
|
||||
def Reg2Py(data, size, typ):
|
||||
if typ == REG_DWORD:
|
||||
if size == 0:
|
||||
return 0
|
||||
return ctypes.cast(data, ctypes.POINTER(ctypes.c_int)).contents.value
|
||||
elif typ == REG_SZ or typ == REG_EXPAND_SZ:
|
||||
return ctypes.wstring_at(data, size // 2).rstrip(u'\x00')
|
||||
elif typ == REG_MULTI_SZ:
|
||||
return split_multi_sz(ctypes.cast(data, ctypes.c_wchar_p), size // 2)
|
||||
else:
|
||||
if size == 0:
|
||||
return None
|
||||
return ctypes.string_at(data, size)
|
||||
|
||||
def EnumKey(key, index):
|
||||
tmpbuf = ctypes.create_unicode_buffer(257)
|
||||
length = DWORD(257)
|
||||
rc = RegEnumKeyEx(key.hkey, index,
|
||||
ctypes.cast(tmpbuf, ctypes.c_wchar_p),
|
||||
ctypes.byref(length),
|
||||
LPDWORD(), ctypes.c_wchar_p(), LPDWORD(),
|
||||
ctypes.POINTER(FILETIME)())
|
||||
check_code(rc)
|
||||
return ctypes.wstring_at(tmpbuf, length.value).rstrip(u'\x00')
|
||||
|
||||
def QueryValueEx(key, value_name):
|
||||
size = 256
|
||||
typ = DWORD()
|
||||
while True:
|
||||
tmp_size = DWORD(size)
|
||||
buf = ctypes.create_string_buffer(size)
|
||||
rc = RegQueryValueEx(key.hkey, value_name, LPDWORD(),
|
||||
ctypes.byref(typ),
|
||||
ctypes.cast(buf, LPBYTE), ctypes.byref(tmp_size))
|
||||
if rc != ERROR_MORE_DATA:
|
||||
break
|
||||
|
||||
size *= 2
|
||||
check_code(rc)
|
||||
return (Reg2Py(buf, tmp_size.value, typ.value), typ.value)
|
||||
|
||||
__all__ = ['OpenKey', 'QueryInfoKey', 'EnumValue', 'EnumKey', 'QueryValueEx',
|
||||
'HKEY_CLASSES_ROOT', 'HKEY_CURRENT_USER', 'HKEY_LOCAL_MACHINE',
|
||||
'HKEY_USERS', 'HKEY_PERFORMANCE_DATA', 'HKEY_CURRENT_CONFIG',
|
||||
'HKEY_DYN_DATA', 'REG_NONE', 'REG_SZ', 'REG_EXPAND_SZ',
|
||||
'REG_BINARY', 'REG_DWORD', 'REG_DWORD_BIG_ENDIAN',
|
||||
'REG_DWORD_LITTLE_ENDIAN', 'REG_LINK', 'REG_MULTI_SZ',
|
||||
'REG_RESOURCE_LIST', 'REG_FULL_RESOURCE_DESCRIPTOR',
|
||||
'REG_RESOURCE_REQUIREMENTS_LIST']
|
||||
@@ -207,8 +207,9 @@ def _load_python_alfcrypto():
|
||||
|
||||
def ctx_init(self, key):
|
||||
ctx1 = 0x0CAFFE19E
|
||||
for keyChar in key:
|
||||
keyByte = ord(keyChar)
|
||||
if isinstance(key, str):
|
||||
key = key.encode('latin-1')
|
||||
for keyByte in key:
|
||||
ctx2 = ctx1
|
||||
ctx1 = ((((ctx1 >>2) * (ctx1 >>7))&0xFFFFFFFF) ^ (keyByte * keyByte * 0x0F902007)& 0xFFFFFFFF )
|
||||
self._ctx = [ctx1, ctx2]
|
||||
@@ -220,8 +221,9 @@ def _load_python_alfcrypto():
|
||||
ctx1 = ctx[0]
|
||||
ctx2 = ctx[1]
|
||||
plainText = ""
|
||||
for dataChar in data:
|
||||
dataByte = ord(dataChar)
|
||||
if isinstance(data, str):
|
||||
data = data.encode('latin-1')
|
||||
for dataByte in data:
|
||||
m = (dataByte ^ ((ctx1 >> 3) &0xFF) ^ ((ctx2<<3) & 0xFF)) &0xFF
|
||||
ctx2 = ctx1
|
||||
ctx1 = (((ctx1 >> 2) * (ctx1 >> 7)) &0xFFFFFFFF) ^((m * m * 0x0F902007) &0xFFFFFFFF)
|
||||
|
||||
@@ -6,33 +6,42 @@ __license__ = 'GPL v3'
|
||||
# Python 3, September 2020
|
||||
|
||||
# Standard Python modules.
|
||||
import os, traceback, json, codecs
|
||||
import sys, os, traceback, json, codecs, base64
|
||||
|
||||
from PyQt5.Qt import (Qt, QWidget, QHBoxLayout, QVBoxLayout, QLabel, QLineEdit,
|
||||
QGroupBox, QPushButton, QListWidget, QListWidgetItem, QCheckBox,
|
||||
QAbstractItemView, QIcon, QDialog, QDialogButtonBox, QUrl,
|
||||
QCheckBox)
|
||||
QCheckBox, QComboBox)
|
||||
|
||||
from PyQt5 import Qt as QtGui
|
||||
from zipfile import ZipFile
|
||||
|
||||
|
||||
#@@CALIBRE_COMPAT_CODE@@
|
||||
|
||||
|
||||
# calibre modules and constants.
|
||||
from calibre.gui2 import (error_dialog, question_dialog, info_dialog, open_url,
|
||||
choose_dir, choose_files, choose_save_file)
|
||||
from calibre.utils.config import dynamic, config_dir, JSONConfig
|
||||
from calibre.constants import iswindows, isosx
|
||||
|
||||
# modules from this plugin's zipfile.
|
||||
from calibre_plugins.dedrm.__init__ import PLUGIN_NAME, PLUGIN_VERSION
|
||||
from calibre_plugins.dedrm.__init__ import RESOURCE_NAME as help_file_name
|
||||
from calibre_plugins.dedrm.utilities import uStrCmp
|
||||
|
||||
import calibre_plugins.dedrm.prefs as prefs
|
||||
import calibre_plugins.dedrm.androidkindlekey as androidkindlekey
|
||||
from __init__ import PLUGIN_NAME, PLUGIN_VERSION
|
||||
from __version import RESOURCE_NAME as help_file_name
|
||||
from utilities import uStrCmp
|
||||
|
||||
import prefs
|
||||
import androidkindlekey
|
||||
|
||||
def checkForDeACSMkeys():
|
||||
try:
|
||||
from calibre_plugins.deacsm.libadobeAccount import exportAccountEncryptionKeyDER, getAccountUUID
|
||||
except:
|
||||
# Looks like DeACSM is not installed.
|
||||
return None, None
|
||||
|
||||
try:
|
||||
from calibre.ptempfile import TemporaryFile
|
||||
|
||||
|
||||
@@ -85,6 +94,7 @@ class ConfigWidget(QWidget):
|
||||
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'])
|
||||
self.tempdedrmprefs['adobe_pdf_passphrases'] = list(self.dedrmprefs['adobe_pdf_passphrases'])
|
||||
|
||||
# Start Qt Gui dialog layout
|
||||
layout = QVBoxLayout(self)
|
||||
@@ -108,8 +118,8 @@ class ConfigWidget(QWidget):
|
||||
button_layout = QVBoxLayout()
|
||||
keys_group_box_layout.addLayout(button_layout)
|
||||
self.bandn_button = QtGui.QPushButton(self)
|
||||
self.bandn_button.setToolTip(_("Click to manage keys for Barnes and Noble ebooks"))
|
||||
self.bandn_button.setText("Barnes and Noble ebooks")
|
||||
self.bandn_button.setToolTip(_("Click to manage keys for ADE books with PassHash algorithm. <br/>Commonly used by Barnes and Noble"))
|
||||
self.bandn_button.setText("ADE PassHash (B&&N) ebooks")
|
||||
self.bandn_button.clicked.connect(self.bandn_keys)
|
||||
self.kindle_android_button = QtGui.QPushButton(self)
|
||||
self.kindle_android_button.setToolTip(_("Click to manage keys for Kindle for Android ebooks"))
|
||||
@@ -117,7 +127,7 @@ class ConfigWidget(QWidget):
|
||||
self.kindle_android_button.clicked.connect(self.kindle_android)
|
||||
self.kindle_serial_button = QtGui.QPushButton(self)
|
||||
self.kindle_serial_button.setToolTip(_("Click to manage eInk Kindle serial numbers for Kindle ebooks"))
|
||||
self.kindle_serial_button.setText("eInk Kindle ebooks")
|
||||
self.kindle_serial_button.setText("Kindle eInk ebooks")
|
||||
self.kindle_serial_button.clicked.connect(self.kindle_serials)
|
||||
self.kindle_key_button = QtGui.QPushButton(self)
|
||||
self.kindle_key_button.setToolTip(_("Click to manage keys for Kindle for Mac/PC ebooks"))
|
||||
@@ -139,14 +149,23 @@ class ConfigWidget(QWidget):
|
||||
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)
|
||||
self.pdf_keys_button = QtGui.QPushButton(self)
|
||||
self.pdf_keys_button.setToolTip(_("Click to manage PDF file passphrases"))
|
||||
self.pdf_keys_button.setText("Adobe PDF passwords")
|
||||
self.pdf_keys_button.clicked.connect(self.pdf_passphrases)
|
||||
|
||||
button_layout.addWidget(self.kindle_serial_button)
|
||||
button_layout.addWidget(self.kindle_android_button)
|
||||
button_layout.addWidget(self.kindle_key_button)
|
||||
button_layout.addSpacing(15)
|
||||
button_layout.addWidget(self.adept_button)
|
||||
button_layout.addWidget(self.bandn_button)
|
||||
button_layout.addWidget(self.pdf_keys_button)
|
||||
button_layout.addSpacing(15)
|
||||
button_layout.addWidget(self.mobi_button)
|
||||
button_layout.addWidget(self.ereader_button)
|
||||
button_layout.addWidget(self.adept_button)
|
||||
button_layout.addWidget(self.kindle_key_button)
|
||||
button_layout.addWidget(self.lcp_button)
|
||||
|
||||
|
||||
self.chkFontObfuscation = QtGui.QCheckBox(_("Deobfuscate EPUB fonts"))
|
||||
self.chkFontObfuscation.setToolTip("Deobfuscates fonts in EPUB files after DRM removal")
|
||||
@@ -191,7 +210,7 @@ class ConfigWidget(QWidget):
|
||||
d.exec_()
|
||||
|
||||
def bandn_keys(self):
|
||||
d = ManageKeysDialog(self,"Barnes and Noble Key",self.tempdedrmprefs['bandnkeys'], AddBandNKeyDialog, 'b64')
|
||||
d = ManageKeysDialog(self,"ADE PassHash Key",self.tempdedrmprefs['bandnkeys'], AddBandNKeyDialog, 'b64')
|
||||
d.exec_()
|
||||
|
||||
def ereader_keys(self):
|
||||
@@ -202,6 +221,10 @@ class ConfigWidget(QWidget):
|
||||
d = ManageKeysDialog(self,"Readium LCP passphrase",self.tempdedrmprefs['lcp_passphrases'], AddLCPKeyDialog)
|
||||
d.exec_()
|
||||
|
||||
def pdf_passphrases(self):
|
||||
d = ManageKeysDialog(self,"PDF passphrase",self.tempdedrmprefs['adobe_pdf_passphrases'], AddPDFPassDialog)
|
||||
d.exec_()
|
||||
|
||||
def help_link_activated(self, url):
|
||||
def get_help_file_resource():
|
||||
# Copy the HTML helpfile to the plugin directory each time the
|
||||
@@ -227,6 +250,7 @@ class ConfigWidget(QWidget):
|
||||
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.set('adobe_pdf_passphrases', self.tempdedrmprefs['adobe_pdf_passphrases'])
|
||||
self.dedrmprefs.writeprefs()
|
||||
|
||||
def load_resource(self, name):
|
||||
@@ -303,7 +327,13 @@ class ManageKeysDialog(QDialog):
|
||||
self.export_key_button.setIcon(QIcon(I('save.png')))
|
||||
self.export_key_button.clicked.connect(self.export_key)
|
||||
button_layout.addWidget(self.export_key_button)
|
||||
spacerItem = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
|
||||
try:
|
||||
# QT 6
|
||||
spacerItem = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Policy.Minimum, QtGui.QSizePolicy.Policy.Expanding)
|
||||
except AttributeError:
|
||||
# QT 5
|
||||
spacerItem = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
|
||||
|
||||
button_layout.addItem(spacerItem)
|
||||
|
||||
if self.wineprefix is not None:
|
||||
@@ -356,21 +386,62 @@ class ManageKeysDialog(QDialog):
|
||||
if d.result() != d.Accepted:
|
||||
# New key generation cancelled.
|
||||
return
|
||||
new_key_value = d.key_value
|
||||
if type(self.plugin_keys) == dict:
|
||||
if new_key_value in self.plugin_keys.values():
|
||||
old_key_name = [name for name, value in self.plugin_keys.items() if value == new_key_value][0]
|
||||
info_dialog(None, "{0} {1}: Duplicate {2}".format(PLUGIN_NAME, PLUGIN_VERSION,self.key_type_name),
|
||||
"The new {1} is the same as the existing {1} named <strong>{0}</strong> and has not been added.".format(old_key_name,self.key_type_name), show=True)
|
||||
return
|
||||
self.plugin_keys[d.key_name] = new_key_value
|
||||
else:
|
||||
if new_key_value in self.plugin_keys:
|
||||
info_dialog(None, "{0} {1}: Duplicate {2}".format(PLUGIN_NAME, PLUGIN_VERSION,self.key_type_name),
|
||||
"This {0} is already in the list of {0}s has not been added.".format(self.key_type_name), show=True)
|
||||
return
|
||||
|
||||
self.plugin_keys.append(d.key_value)
|
||||
if hasattr(d, "k_key_list") and d.k_key_list is not None:
|
||||
# importing multiple keys
|
||||
idx = -1
|
||||
dup_key_count = 0
|
||||
added_key_count = 0
|
||||
|
||||
while True:
|
||||
idx = idx + 1
|
||||
try:
|
||||
new_key_value = d.k_key_list[idx]
|
||||
except:
|
||||
break
|
||||
|
||||
if type(self.plugin_keys) == dict:
|
||||
if new_key_value in self.plugin_keys.values():
|
||||
dup_key_count = dup_key_count + 1
|
||||
continue
|
||||
self.plugin_keys[d.k_name_list[idx]] = new_key_value
|
||||
added_key_count = added_key_count + 1
|
||||
else:
|
||||
if new_key_value in self.plugin_keys:
|
||||
dup_key_count = dup_key_count + 1
|
||||
continue
|
||||
self.plugin_keys.append(new_key_value)
|
||||
added_key_count = added_key_count + 1
|
||||
|
||||
if (added_key_count > 0 or dup_key_count > 0):
|
||||
if (added_key_count == 0):
|
||||
info_dialog(None, "{0} {1}: Adding {2}".format(PLUGIN_NAME, PLUGIN_VERSION,self.key_type_name),
|
||||
"Skipped adding {0} duplicate / existing keys.".format(dup_key_count), show=True, show_copy_button=False)
|
||||
elif (dup_key_count == 0):
|
||||
info_dialog(None, "{0} {1}: Adding {2}".format(PLUGIN_NAME, PLUGIN_VERSION,self.key_type_name),
|
||||
"Added {0} new keys.".format(added_key_count), show=True, show_copy_button=False)
|
||||
else:
|
||||
info_dialog(None, "{0} {1}: Adding {2}".format(PLUGIN_NAME, PLUGIN_VERSION,self.key_type_name),
|
||||
"Added {0} new keys, skipped adding {1} existing keys.".format(added_key_count, dup_key_count), show=True, show_copy_button=False)
|
||||
|
||||
else:
|
||||
# Import single key
|
||||
new_key_value = d.key_value
|
||||
if type(self.plugin_keys) == dict:
|
||||
if new_key_value in self.plugin_keys.values():
|
||||
old_key_name = [name for name, value in self.plugin_keys.items() if value == new_key_value][0]
|
||||
info_dialog(None, "{0} {1}: Duplicate {2}".format(PLUGIN_NAME, PLUGIN_VERSION,self.key_type_name),
|
||||
"The new {1} is the same as the existing {1} named <strong>{0}</strong> and has not been added.".format(old_key_name,self.key_type_name), show=True)
|
||||
return
|
||||
self.plugin_keys[d.key_name] = new_key_value
|
||||
else:
|
||||
if new_key_value in self.plugin_keys:
|
||||
info_dialog(None, "{0} {1}: Duplicate {2}".format(PLUGIN_NAME, PLUGIN_VERSION,self.key_type_name),
|
||||
"This {0} is already in the list of {0}s has not been added.".format(self.key_type_name), show=True)
|
||||
return
|
||||
|
||||
self.plugin_keys.append(d.key_value)
|
||||
|
||||
self.listy.clear()
|
||||
self.populate_list()
|
||||
|
||||
@@ -561,89 +632,210 @@ class RenameKeyDialog(QDialog):
|
||||
|
||||
|
||||
class AddBandNKeyDialog(QDialog):
|
||||
def __init__(self, parent=None,):
|
||||
QDialog.__init__(self, parent)
|
||||
self.parent = parent
|
||||
self.setWindowTitle("{0} {1}: Create New Barnes & Noble Key".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||
layout = QVBoxLayout(self)
|
||||
self.setLayout(layout)
|
||||
|
||||
data_group_box = QGroupBox("", self)
|
||||
layout.addWidget(data_group_box)
|
||||
data_group_box_layout = QVBoxLayout()
|
||||
data_group_box.setLayout(data_group_box_layout)
|
||||
def update_form(self, idx):
|
||||
self.cbType.hide()
|
||||
|
||||
key_group = QHBoxLayout()
|
||||
data_group_box_layout.addLayout(key_group)
|
||||
key_group.addWidget(QLabel("Unique Key Name:", self))
|
||||
if idx == 1:
|
||||
self.add_fields_for_passhash()
|
||||
elif idx == 2:
|
||||
self.add_fields_for_b64_passhash()
|
||||
elif idx == 3:
|
||||
self.add_fields_for_ade_passhash()
|
||||
elif idx == 4:
|
||||
self.add_fields_for_windows_nook()
|
||||
elif idx == 5:
|
||||
self.add_fields_for_android_nook()
|
||||
|
||||
|
||||
|
||||
def add_fields_for_ade_passhash(self):
|
||||
|
||||
self.ade_extr_group_box = QGroupBox("", self)
|
||||
ade_extr_group_box_layout = QVBoxLayout()
|
||||
self.ade_extr_group_box.setLayout(ade_extr_group_box_layout)
|
||||
|
||||
self.layout.addWidget(self.ade_extr_group_box)
|
||||
|
||||
ade_extr_group_box_layout.addWidget(QLabel("Click \"OK\" to try and dump PassHash data \nfrom Adobe Digital Editions. This works if\nyou've opened your PassHash books in ADE before.", self))
|
||||
|
||||
self.button_box.hide()
|
||||
|
||||
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||||
self.button_box.accepted.connect(self.accept_ade_dump_passhash)
|
||||
self.button_box.rejected.connect(self.reject)
|
||||
self.layout.addWidget(self.button_box)
|
||||
|
||||
self.resize(self.sizeHint())
|
||||
|
||||
|
||||
def add_fields_for_android_nook(self):
|
||||
|
||||
self.andr_nook_group_box = QGroupBox("", self)
|
||||
andr_nook_group_box_layout = QVBoxLayout()
|
||||
self.andr_nook_group_box.setLayout(andr_nook_group_box_layout)
|
||||
|
||||
self.layout.addWidget(self.andr_nook_group_box)
|
||||
|
||||
ph_key_name_group = QHBoxLayout()
|
||||
andr_nook_group_box_layout.addLayout(ph_key_name_group)
|
||||
ph_key_name_group.addWidget(QLabel("Unique Key Name:", self))
|
||||
self.key_ledit = QLineEdit("", self)
|
||||
self.key_ledit.setToolTip(_("<p>Enter an identifying name for this new key.</p>"))
|
||||
ph_key_name_group.addWidget(self.key_ledit)
|
||||
|
||||
andr_nook_group_box_layout.addWidget(QLabel("Hidden in the Android application data is a " +
|
||||
"folder\nnamed '.adobe-digital-editions'. Please enter\nthe full path to that folder.", self))
|
||||
|
||||
ph_path_group = QHBoxLayout()
|
||||
andr_nook_group_box_layout.addLayout(ph_path_group)
|
||||
ph_path_group.addWidget(QLabel("Path:", self))
|
||||
self.cc_ledit = QLineEdit("", self)
|
||||
self.cc_ledit.setToolTip(_("<p>Enter path to .adobe-digital-editions folder.</p>"))
|
||||
ph_path_group.addWidget(self.cc_ledit)
|
||||
|
||||
self.button_box.hide()
|
||||
|
||||
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||||
self.button_box.accepted.connect(self.accept_android_nook)
|
||||
self.button_box.rejected.connect(self.reject)
|
||||
self.layout.addWidget(self.button_box)
|
||||
|
||||
self.resize(self.sizeHint())
|
||||
|
||||
def add_fields_for_windows_nook(self):
|
||||
|
||||
self.win_nook_group_box = QGroupBox("", self)
|
||||
win_nook_group_box_layout = QVBoxLayout()
|
||||
self.win_nook_group_box.setLayout(win_nook_group_box_layout)
|
||||
|
||||
self.layout.addWidget(self.win_nook_group_box)
|
||||
|
||||
ph_key_name_group = QHBoxLayout()
|
||||
win_nook_group_box_layout.addLayout(ph_key_name_group)
|
||||
ph_key_name_group.addWidget(QLabel("Unique Key Name:", self))
|
||||
self.key_ledit = QLineEdit("", self)
|
||||
self.key_ledit.setToolTip(_("<p>Enter an identifying name for this new key.</p>"))
|
||||
ph_key_name_group.addWidget(self.key_ledit)
|
||||
|
||||
self.button_box.hide()
|
||||
|
||||
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||||
self.button_box.accepted.connect(self.accept_win_nook)
|
||||
self.button_box.rejected.connect(self.reject)
|
||||
self.layout.addWidget(self.button_box)
|
||||
|
||||
self.resize(self.sizeHint())
|
||||
|
||||
def add_fields_for_b64_passhash(self):
|
||||
|
||||
self.passhash_group_box = QGroupBox("", self)
|
||||
passhash_group_box_layout = QVBoxLayout()
|
||||
self.passhash_group_box.setLayout(passhash_group_box_layout)
|
||||
|
||||
self.layout.addWidget(self.passhash_group_box)
|
||||
|
||||
ph_key_name_group = QHBoxLayout()
|
||||
passhash_group_box_layout.addLayout(ph_key_name_group)
|
||||
ph_key_name_group.addWidget(QLabel("Unique Key Name:", self))
|
||||
self.key_ledit = QLineEdit("", self)
|
||||
self.key_ledit.setToolTip(_("<p>Enter an identifying name for this new key.</p>" +
|
||||
"<p>It should be something that will help you remember " +
|
||||
"what personal information was used to create it."))
|
||||
key_group.addWidget(self.key_ledit)
|
||||
ph_key_name_group.addWidget(self.key_ledit)
|
||||
|
||||
name_group = QHBoxLayout()
|
||||
data_group_box_layout.addLayout(name_group)
|
||||
name_group.addWidget(QLabel("B&N/nook account email address:", self))
|
||||
self.name_ledit = QLineEdit("", self)
|
||||
self.name_ledit.setToolTip(_("<p>Enter your email address as it appears in your B&N " +
|
||||
"account.</p>" +
|
||||
"<p>It will only be used to generate this " +
|
||||
"key and won\'t be stored anywhere " +
|
||||
"in calibre or on your computer.</p>" +
|
||||
"<p>eg: apprenticeharper@gmail.com</p>"))
|
||||
name_group.addWidget(self.name_ledit)
|
||||
name_disclaimer_label = QLabel(_("(Will not be saved in configuration data)"), self)
|
||||
name_disclaimer_label.setAlignment(Qt.AlignHCenter)
|
||||
data_group_box_layout.addWidget(name_disclaimer_label)
|
||||
|
||||
ccn_group = QHBoxLayout()
|
||||
data_group_box_layout.addLayout(ccn_group)
|
||||
ccn_group.addWidget(QLabel("B&N/nook account password:", self))
|
||||
ph_name_group = QHBoxLayout()
|
||||
passhash_group_box_layout.addLayout(ph_name_group)
|
||||
ph_name_group.addWidget(QLabel("Base64 key string:", self))
|
||||
self.cc_ledit = QLineEdit("", self)
|
||||
self.cc_ledit.setToolTip(_("<p>Enter the password " +
|
||||
"for your B&N account.</p>" +
|
||||
"<p>The password will only be used to generate this " +
|
||||
"key and won\'t be stored anywhere in " +
|
||||
"calibre or on your computer."))
|
||||
ccn_group.addWidget(self.cc_ledit)
|
||||
ccn_disclaimer_label = QLabel(_('(Will not be saved in configuration data)'), self)
|
||||
ccn_disclaimer_label.setAlignment(Qt.AlignHCenter)
|
||||
data_group_box_layout.addWidget(ccn_disclaimer_label)
|
||||
layout.addSpacing(10)
|
||||
|
||||
self.chkOldAlgo = QCheckBox(_("Try to use the old algorithm"))
|
||||
self.chkOldAlgo.setToolTip(_("Leave this off if you're unsure."))
|
||||
data_group_box_layout.addWidget(self.chkOldAlgo)
|
||||
layout.addSpacing(10)
|
||||
|
||||
key_group = QHBoxLayout()
|
||||
data_group_box_layout.addLayout(key_group)
|
||||
key_group.addWidget(QLabel("Retrieved key:", self))
|
||||
self.key_display = QLabel("", self)
|
||||
self.key_display.setToolTip(_("Click the Retrieve Key button to fetch your B&N encryption key from the B&N servers"))
|
||||
key_group.addWidget(self.key_display)
|
||||
self.retrieve_button = QtGui.QPushButton(self)
|
||||
self.retrieve_button.setToolTip(_("Click to retrieve your B&N encryption key from the B&N servers"))
|
||||
self.retrieve_button.setText("Retrieve Key")
|
||||
self.retrieve_button.clicked.connect(self.retrieve_key)
|
||||
key_group.addWidget(self.retrieve_button)
|
||||
layout.addSpacing(10)
|
||||
self.cc_ledit.setToolTip(_("<p>Enter the Base64 key string</p>"))
|
||||
ph_name_group.addWidget(self.cc_ledit)
|
||||
|
||||
self.button_box.hide()
|
||||
|
||||
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||||
self.button_box.accepted.connect(self.accept)
|
||||
self.button_box.accepted.connect(self.accept_b64_passhash)
|
||||
self.button_box.rejected.connect(self.reject)
|
||||
layout.addWidget(self.button_box)
|
||||
self.layout.addWidget(self.button_box)
|
||||
|
||||
self.resize(self.sizeHint())
|
||||
|
||||
|
||||
def add_fields_for_passhash(self):
|
||||
|
||||
self.passhash_group_box = QGroupBox("", self)
|
||||
passhash_group_box_layout = QVBoxLayout()
|
||||
self.passhash_group_box.setLayout(passhash_group_box_layout)
|
||||
|
||||
self.layout.addWidget(self.passhash_group_box)
|
||||
|
||||
ph_key_name_group = QHBoxLayout()
|
||||
passhash_group_box_layout.addLayout(ph_key_name_group)
|
||||
ph_key_name_group.addWidget(QLabel("Unique Key Name:", self))
|
||||
self.key_ledit = QLineEdit("", self)
|
||||
self.key_ledit.setToolTip(_("<p>Enter an identifying name for this new key.</p>" +
|
||||
"<p>It should be something that will help you remember " +
|
||||
"what personal information was used to create it."))
|
||||
ph_key_name_group.addWidget(self.key_ledit)
|
||||
|
||||
ph_name_group = QHBoxLayout()
|
||||
passhash_group_box_layout.addLayout(ph_name_group)
|
||||
ph_name_group.addWidget(QLabel("Username:", self))
|
||||
self.name_ledit = QLineEdit("", self)
|
||||
self.name_ledit.setToolTip(_("<p>Enter the PassHash username</p>"))
|
||||
ph_name_group.addWidget(self.name_ledit)
|
||||
|
||||
ph_pass_group = QHBoxLayout()
|
||||
passhash_group_box_layout.addLayout(ph_pass_group)
|
||||
ph_pass_group.addWidget(QLabel("Password:", self))
|
||||
self.cc_ledit = QLineEdit("", self)
|
||||
self.cc_ledit.setToolTip(_("<p>Enter the PassHash password</p>"))
|
||||
ph_pass_group.addWidget(self.cc_ledit)
|
||||
|
||||
self.button_box.hide()
|
||||
|
||||
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||||
self.button_box.accepted.connect(self.accept_passhash)
|
||||
self.button_box.rejected.connect(self.reject)
|
||||
self.layout.addWidget(self.button_box)
|
||||
|
||||
self.resize(self.sizeHint())
|
||||
|
||||
|
||||
|
||||
def __init__(self, parent=None,):
|
||||
QDialog.__init__(self, parent)
|
||||
self.parent = parent
|
||||
self.setWindowTitle("{0} {1}: Create New PassHash (B&N) Key".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||
self.layout = QVBoxLayout(self)
|
||||
self.setLayout(self.layout)
|
||||
|
||||
self.cbType = QComboBox()
|
||||
self.cbType.addItem("--- Select key type ---")
|
||||
self.cbType.addItem("Adobe PassHash username & password")
|
||||
self.cbType.addItem("Base64-encoded PassHash key string")
|
||||
self.cbType.addItem("Extract passhashes from Adobe Digital Editions")
|
||||
self.cbType.addItem("Extract key from Nook Windows application")
|
||||
self.cbType.addItem("Extract key from Nook Android application")
|
||||
self.cbType.currentIndexChanged.connect(lambda: self.update_form(self.cbType.currentIndex()))
|
||||
self.layout.addWidget(self.cbType)
|
||||
|
||||
self.button_box = QDialogButtonBox(QDialogButtonBox.Cancel)
|
||||
self.button_box.rejected.connect(self.reject)
|
||||
self.layout.addWidget(self.button_box)
|
||||
|
||||
self.resize(self.sizeHint())
|
||||
|
||||
@property
|
||||
def key_name(self):
|
||||
return str(self.key_ledit.text()).strip()
|
||||
try:
|
||||
return str(self.key_ledit.text()).strip()
|
||||
except:
|
||||
return self.result_data_name
|
||||
|
||||
@property
|
||||
def key_value(self):
|
||||
return str(self.key_display.text()).strip()
|
||||
return self.result_data
|
||||
|
||||
@property
|
||||
def user_name(self):
|
||||
@@ -653,40 +845,194 @@ class AddBandNKeyDialog(QDialog):
|
||||
def cc_number(self):
|
||||
return str(self.cc_ledit.text()).strip()
|
||||
|
||||
def retrieve_key(self):
|
||||
|
||||
if self.chkOldAlgo.isChecked():
|
||||
# old method, try to generate
|
||||
from calibre_plugins.dedrm.ignoblekeygen import generate_key as generate_bandn_key
|
||||
generated_key = generate_bandn_key(self.user_name, self.cc_number)
|
||||
if generated_key == "":
|
||||
errmsg = "Could not generate key."
|
||||
error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
|
||||
else:
|
||||
self.key_display.setText(generated_key.decode("latin-1"))
|
||||
@property
|
||||
def k_name_list(self):
|
||||
# If the plugin supports returning multiple keys, return a list of names.
|
||||
if self.k_full_name_list is not None and self.k_full_key_list is not None:
|
||||
return self.k_full_name_list
|
||||
return None
|
||||
|
||||
@property
|
||||
def k_key_list(self):
|
||||
# If the plugin supports returning multiple keys, return a list of keys.
|
||||
if self.k_full_name_list is not None and self.k_full_key_list is not None:
|
||||
return self.k_full_key_list
|
||||
return None
|
||||
|
||||
|
||||
|
||||
def accept_android_nook(self):
|
||||
|
||||
if len(self.key_name) < 4:
|
||||
errmsg = "Key name must be at <i>least</i> 4 characters long!"
|
||||
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
|
||||
|
||||
path_to_ade_data = self.cc_number
|
||||
|
||||
if (os.path.isfile(os.path.join(path_to_ade_data, ".adobe-digital-editions", "activation.xml"))):
|
||||
path_to_ade_data = os.path.join(path_to_ade_data, ".adobe-digital-editions")
|
||||
elif (os.path.isfile(os.path.join(path_to_ade_data, "activation.xml"))):
|
||||
pass
|
||||
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)
|
||||
errmsg = "This isn't the correct path, or the data is invalid."
|
||||
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
|
||||
|
||||
def accept(self):
|
||||
from ignoblekeyAndroid import dump_keys
|
||||
store_result = dump_keys(path_to_ade_data)
|
||||
|
||||
if len(store_result) == 0:
|
||||
errmsg = "Failed to extract keys. Is this the correct folder?"
|
||||
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
|
||||
|
||||
if len(store_result) == 1:
|
||||
# Found exactly one key. Store it with that name.
|
||||
self.result_data = store_result[0]
|
||||
QDialog.accept(self)
|
||||
return
|
||||
|
||||
# Found multiple keys
|
||||
keys = []
|
||||
names = []
|
||||
idx = 1
|
||||
for key in store_result:
|
||||
keys.append(key)
|
||||
names.append(self.key_name + "_" + str(idx))
|
||||
idx = idx + 1
|
||||
|
||||
self.k_full_name_list = names
|
||||
self.k_full_key_list = keys
|
||||
QDialog.accept(self)
|
||||
return
|
||||
|
||||
|
||||
def accept_ade_dump_passhash(self):
|
||||
|
||||
try:
|
||||
from adobekey_get_passhash import passhash_keys
|
||||
keys, names = passhash_keys()
|
||||
except:
|
||||
errmsg = "Failed to grab PassHash keys from ADE."
|
||||
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
|
||||
|
||||
# Take the first new key we found.
|
||||
|
||||
idx = -1
|
||||
new_keys = []
|
||||
new_names = []
|
||||
for key in keys:
|
||||
idx = idx + 1
|
||||
if key in self.parent.plugin_keys.values():
|
||||
continue
|
||||
|
||||
new_keys.append(key)
|
||||
new_names.append(names[idx])
|
||||
|
||||
if len(new_keys) == 0:
|
||||
# Okay, we didn't find anything. How do we get rid of the window?
|
||||
errmsg = "Didn't find any PassHash keys in ADE."
|
||||
error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
|
||||
QDialog.reject(self)
|
||||
return
|
||||
|
||||
# Add new keys to list.
|
||||
self.k_full_name_list = new_names
|
||||
self.k_full_key_list = new_keys
|
||||
QDialog.accept(self)
|
||||
return
|
||||
|
||||
|
||||
|
||||
def accept_win_nook(self):
|
||||
|
||||
if len(self.key_name) < 4:
|
||||
errmsg = "Key name must be at <i>least</i> 4 characters long!"
|
||||
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
|
||||
|
||||
try:
|
||||
from ignoblekeyWindowsStore import dump_keys
|
||||
store_result = dump_keys(False)
|
||||
except:
|
||||
errmsg = "Failed to import from Nook Microsoft Store app."
|
||||
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
|
||||
|
||||
try:
|
||||
# Try the Nook Study app
|
||||
from ignoblekeyNookStudy import nookkeys
|
||||
study_result = nookkeys()
|
||||
except:
|
||||
errmsg = "Failed to import from Nook Study app."
|
||||
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
|
||||
|
||||
# Add all found keys to a list
|
||||
keys = []
|
||||
names = []
|
||||
idx = 1
|
||||
for key in store_result:
|
||||
keys.append(key)
|
||||
names.append(self.key_name + "_nookStore_" + str(idx))
|
||||
idx = idx + 1
|
||||
idx = 1
|
||||
for key in study_result:
|
||||
keys.append(key)
|
||||
names.append(self.key_name + "_nookStudy_" + str(idx))
|
||||
idx = idx + 1
|
||||
|
||||
if len(keys) > 0:
|
||||
self.k_full_name_list = names
|
||||
self.k_full_key_list = keys
|
||||
QDialog.accept(self)
|
||||
return
|
||||
|
||||
|
||||
# Okay, we didn't find anything.
|
||||
errmsg = "Didn't find any Nook keys in the Windows app."
|
||||
error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
|
||||
QDialog.reject(self)
|
||||
|
||||
|
||||
def accept_b64_passhash(self):
|
||||
if len(self.key_name) == 0 or len(self.cc_number) == 0 or self.key_name.isspace() or self.cc_number.isspace():
|
||||
errmsg = "All fields are required!"
|
||||
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
|
||||
|
||||
if len(self.key_name) < 4:
|
||||
errmsg = "Key name must be at <i>least</i> 4 characters long!"
|
||||
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
|
||||
|
||||
try:
|
||||
x = base64.b64decode(self.cc_number)
|
||||
except:
|
||||
errmsg = "Key data is no valid base64 string!"
|
||||
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
|
||||
|
||||
|
||||
self.result_data = self.cc_number
|
||||
QDialog.accept(self)
|
||||
|
||||
def accept_passhash(self):
|
||||
if len(self.key_name) == 0 or len(self.user_name) == 0 or len(self.cc_number) == 0 or self.key_name.isspace() or self.user_name.isspace() or self.cc_number.isspace():
|
||||
errmsg = "All fields are required!"
|
||||
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
|
||||
if len(self.key_name) < 4:
|
||||
errmsg = "Key name must be at <i>least</i> 4 characters long!"
|
||||
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
|
||||
if len(self.key_value) == 0:
|
||||
self.retrieve_key()
|
||||
if len(self.key_value) == 0:
|
||||
return
|
||||
|
||||
try:
|
||||
from ignoblekeyGenPassHash import generate_key
|
||||
self.result_data = generate_key(self.user_name, self.cc_number)
|
||||
except:
|
||||
errmsg = "Key generation failed."
|
||||
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
|
||||
|
||||
if len(self.result_data) == 0:
|
||||
errmsg = "Key generation failed."
|
||||
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
|
||||
|
||||
QDialog.accept(self)
|
||||
|
||||
|
||||
|
||||
class AddEReaderDialog(QDialog):
|
||||
def __init__(self, parent=None,):
|
||||
QDialog.__init__(self, parent)
|
||||
@@ -741,7 +1087,7 @@ class AddEReaderDialog(QDialog):
|
||||
|
||||
@property
|
||||
def key_value(self):
|
||||
from calibre_plugins.dedrm.erdr2pml import getuser_key as generate_ereader_key
|
||||
from erdr2pml import getuser_key as generate_ereader_key
|
||||
return codecs.encode(generate_ereader_key(self.user_name, self.cc_number),'hex')
|
||||
|
||||
@property
|
||||
@@ -766,92 +1112,89 @@ class AddEReaderDialog(QDialog):
|
||||
QDialog.accept(self)
|
||||
|
||||
|
||||
class AddAdeptDialog(QDialog):
|
||||
class AddAdeptDialog():
|
||||
# We don't actually need to show a dialog, but the wrapper class is expecting a QDialog here.
|
||||
# Emulate enough methods and parameters so that that works ...
|
||||
|
||||
def exec_(self):
|
||||
return
|
||||
|
||||
def result(self):
|
||||
return True
|
||||
|
||||
@property
|
||||
def Accepted(self):
|
||||
return True
|
||||
|
||||
def __init__(self, parent=None,):
|
||||
QDialog.__init__(self, parent)
|
||||
|
||||
self.parent = parent
|
||||
self.setWindowTitle("{0} {1}: Getting Default Adobe Digital Editions Key".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||
layout = QVBoxLayout(self)
|
||||
self.setLayout(layout)
|
||||
self.new_keys = []
|
||||
self.new_names = []
|
||||
|
||||
try:
|
||||
if iswindows or isosx:
|
||||
from calibre_plugins.dedrm.adobekey import adeptkeys
|
||||
from adobekey import adeptkeys
|
||||
|
||||
defaultkeys, defaultnames = adeptkeys()
|
||||
else: # linux
|
||||
from .wineutils import WineGetKeys
|
||||
from wineutils import WineGetKeys
|
||||
|
||||
scriptpath = os.path.join(parent.parent.alfdir,"adobekey.py")
|
||||
defaultkeys, defaultnames = WineGetKeys(scriptpath, ".der",parent.getwineprefix())
|
||||
|
||||
self.default_key = defaultkeys[0]
|
||||
self.default_name_A = defaultnames[0]
|
||||
|
||||
for key in self.parent.plugin_keys.values():
|
||||
if key == codecs.encode(self.default_key,'hex').decode("utf-8"):
|
||||
# We already have the ADE key imported into the plugin.
|
||||
# Set it back to "" as if we had not found anything,
|
||||
# so the next code path searches more places for potential keys.
|
||||
print("Found key '{0}' in ADE - already present, skipping.".format(self.default_name_A))
|
||||
self.default_key = ""
|
||||
break
|
||||
if sys.version_info[0] < 3:
|
||||
# Python2
|
||||
import itertools
|
||||
zip_function = itertools.izip
|
||||
else:
|
||||
# Python3
|
||||
zip_function = zip
|
||||
|
||||
for key, name in zip_function(defaultkeys, defaultnames):
|
||||
if codecs.encode(key,'hex').decode("latin-1") in self.parent.plugin_keys.values():
|
||||
print("Found key '{0}' in ADE - already present, skipping.".format(name))
|
||||
else:
|
||||
self.new_keys.append(key)
|
||||
self.new_names.append(name)
|
||||
except:
|
||||
self.default_key = ""
|
||||
|
||||
self.foundInPlugin = False
|
||||
print("Exception while checking for ADE keys")
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
if len(self.default_key) == 0:
|
||||
# No (new) key found in ADE. Check the DeACSM calibre plugin instead.
|
||||
# Check for keys in the DeACSM plugin
|
||||
try:
|
||||
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 codecs.encode(key,'hex').decode("latin-1") in self.parent.plugin_keys.values():
|
||||
print("Found key '{0}' in DeACSM - already present, skipping.".format(name))
|
||||
else:
|
||||
# Found new key, add that.
|
||||
self.new_keys.append(key)
|
||||
self.new_names.append(name)
|
||||
except:
|
||||
print("Exception while checking for DeACSM keys")
|
||||
traceback.print_exc()
|
||||
|
||||
# Just in case ADE and DeACSM are activated with the same account,
|
||||
# check the new_keys list for duplicates and remove them, if they exist.
|
||||
|
||||
new_keys_2 = []
|
||||
new_names_2 = []
|
||||
i = 0
|
||||
while True:
|
||||
if i >= len(self.new_keys):
|
||||
break
|
||||
if not self.new_keys[i] in new_keys_2:
|
||||
new_keys_2.append(self.new_keys[i])
|
||||
new_names_2.append(self.new_names[i])
|
||||
i = i + 1
|
||||
|
||||
self.k_full_key_list = new_keys_2
|
||||
self.k_full_name_list = new_names_2
|
||||
|
||||
if len(self.default_key)>0:
|
||||
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||||
|
||||
data_group_box = QGroupBox("", self)
|
||||
layout.addWidget(data_group_box)
|
||||
data_group_box_layout = QVBoxLayout()
|
||||
data_group_box.setLayout(data_group_box_layout)
|
||||
|
||||
key_group = QHBoxLayout()
|
||||
data_group_box_layout.addLayout(key_group)
|
||||
key_group.addWidget(QLabel("Unique Key Name:", self))
|
||||
self.key_ledit = QLineEdit(self.default_name_A, self)
|
||||
self.key_ledit.setToolTip("<p>Enter an identifying name for the current Adobe key. Note that it's recommended to leave the UUID (the random-looking digits and letters) as it is.")
|
||||
key_group.addWidget(self.key_ledit)
|
||||
|
||||
self.button_box.accepted.connect(self.accept)
|
||||
else:
|
||||
# No new key found - neither in ADE nor in the DeACSM plugin
|
||||
|
||||
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok)
|
||||
|
||||
default_key_error = QLabel("No new ADE key could be found. Either ADE is not installed, or the key is already present in the plugin.", self)
|
||||
default_key_error.setAlignment(Qt.AlignHCenter)
|
||||
layout.addWidget(default_key_error)
|
||||
# if no default, bot buttons do the same
|
||||
self.button_box.accepted.connect(self.reject)
|
||||
|
||||
self.button_box.rejected.connect(self.reject)
|
||||
layout.addWidget(self.button_box)
|
||||
|
||||
self.resize(self.sizeHint())
|
||||
|
||||
@property
|
||||
def key_name(self):
|
||||
@@ -859,17 +1202,22 @@ class AddAdeptDialog(QDialog):
|
||||
|
||||
@property
|
||||
def key_value(self):
|
||||
return codecs.encode(self.default_key,'hex').decode("utf-8")
|
||||
return codecs.encode(self.new_keys[0],'hex').decode("utf-8")
|
||||
|
||||
|
||||
@property
|
||||
def k_name_list(self):
|
||||
# If the plugin supports returning multiple keys, return a list of names.
|
||||
if self.k_full_name_list is not None and self.k_full_key_list is not None:
|
||||
return self.k_full_name_list
|
||||
return None
|
||||
|
||||
def accept(self):
|
||||
if len(self.key_name) == 0 or self.key_name.isspace():
|
||||
errmsg = "All fields are required!"
|
||||
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
|
||||
if len(self.key_name) < 4:
|
||||
errmsg = "Key name must be at <i>least</i> 4 characters long!"
|
||||
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
|
||||
QDialog.accept(self)
|
||||
@property
|
||||
def k_key_list(self):
|
||||
# If the plugin supports returning multiple keys, return a list of keys.
|
||||
if self.k_full_name_list is not None and self.k_full_key_list is not None:
|
||||
return self.k_full_key_list
|
||||
return None
|
||||
|
||||
|
||||
class AddKindleDialog(QDialog):
|
||||
@@ -882,11 +1230,11 @@ class AddKindleDialog(QDialog):
|
||||
|
||||
try:
|
||||
if iswindows or isosx:
|
||||
from calibre_plugins.dedrm.kindlekey import kindlekeys
|
||||
from kindlekey import kindlekeys
|
||||
|
||||
defaultkeys = kindlekeys()
|
||||
else: # linux
|
||||
from .wineutils import WineGetKeys
|
||||
from wineutils import WineGetKeys
|
||||
|
||||
scriptpath = os.path.join(parent.parent.alfdir,"kindlekey.py")
|
||||
defaultkeys = WineGetKeys(scriptpath, ".k4i",parent.getwineprefix())
|
||||
@@ -1157,3 +1505,44 @@ class AddLCPKeyDialog(QDialog):
|
||||
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)
|
||||
|
||||
class AddPDFPassDialog(QDialog):
|
||||
def __init__(self, parent=None,):
|
||||
QDialog.__init__(self, parent)
|
||||
self.parent = parent
|
||||
self.setWindowTitle("{0} {1}: Add new PDF 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("PDF password:", self))
|
||||
self.key_ledit = QLineEdit("", self)
|
||||
self.key_ledit.setToolTip("Enter the PDF file password.")
|
||||
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 a PDF password 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)
|
||||
|
||||
@@ -175,15 +175,13 @@ def getfiledata(file, zi):
|
||||
return data
|
||||
|
||||
def encryption(infile):
|
||||
# returns encryption: one of Unencrypted, Adobe, B&N and Unknown
|
||||
encryption = "Error When Checking."
|
||||
# Supports Adobe (old & new), B&N, Kobo, Apple, Readium LCP.
|
||||
encryption = "Error"
|
||||
try:
|
||||
with open(infile,'rb') as infileobject:
|
||||
bookdata = infileobject.read(58)
|
||||
# Check for Zip
|
||||
if bookdata[0:0+2] == b"PK":
|
||||
foundrights = False
|
||||
foundencryption = False
|
||||
inzip = zipfile.ZipFile(infile,'r')
|
||||
namelist = set(inzip.namelist())
|
||||
if (
|
||||
@@ -191,21 +189,32 @@ def encryption(infile):
|
||||
'META-INF/license.lcpl' in namelist and
|
||||
b"EncryptedContentKey" in inzip.read("META-INF/encryption.xml")):
|
||||
encryption = "Readium LCP"
|
||||
|
||||
elif 'META-INF/sinf.xml' in namelist and b"fairplay" in inzip.read("META-INF/sinf.xml"):
|
||||
# Untested, just found this info on Google
|
||||
encryption = "Apple"
|
||||
|
||||
elif 'META-INF/rights.xml' in namelist and b"<kdrm>" in inzip.read("META-INF/rights.xml"):
|
||||
# Untested, just found this info on Google
|
||||
encryption = "Kobo"
|
||||
|
||||
elif 'META-INF/rights.xml' not in namelist or 'META-INF/encryption.xml' not in namelist:
|
||||
encryption = "Unencrypted"
|
||||
else:
|
||||
rights = etree.fromstring(inzip.read('META-INF/rights.xml'))
|
||||
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
||||
expr = './/%s' % (adept('encryptedKey'),)
|
||||
bookkey = ''.join(rights.findtext(expr))
|
||||
if len(bookkey) == 172:
|
||||
encryption = "Adobe (old)"
|
||||
if len(bookkey) == 192:
|
||||
encryption = "Adobe (new)"
|
||||
elif len(bookkey) == 64:
|
||||
encryption = "B&N"
|
||||
else:
|
||||
try:
|
||||
rights = etree.fromstring(inzip.read('META-INF/rights.xml'))
|
||||
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
||||
expr = './/%s' % (adept('encryptedKey'),)
|
||||
bookkey = ''.join(rights.findtext(expr))
|
||||
if len(bookkey) == 172:
|
||||
encryption = "Adobe (old)"
|
||||
if len(bookkey) == 192:
|
||||
encryption = "Adobe (new)"
|
||||
elif len(bookkey) == 64:
|
||||
encryption = "B&N"
|
||||
else:
|
||||
encryption = "Unknown (key len " + str(len(bookkey)) + ")"
|
||||
except:
|
||||
encryption = "Unknown"
|
||||
except:
|
||||
traceback.print_exc()
|
||||
|
||||
@@ -30,7 +30,7 @@ def removeHTMLwatermarks(object, path_to_ebook):
|
||||
modded_contents = []
|
||||
|
||||
count_adept = 0
|
||||
|
||||
count_pocketbook = 0
|
||||
count_lemonink_invisible = 0
|
||||
count_lemonink_visible = 0
|
||||
lemonink_trackingID = None
|
||||
@@ -53,6 +53,14 @@ def removeHTMLwatermarks(object, path_to_ebook):
|
||||
if (str_new != pre_remove):
|
||||
count_adept += 1
|
||||
|
||||
# Remove Pocketbook watermarks
|
||||
pre_remove = str_new
|
||||
str_new = re.sub(r'\<div style\=\"padding\:0\;border\:0\;text\-indent\:0\;line\-height\:normal\;margin\:0 1cm 0.5cm 1cm\;[^\"]*opacity:0.0\;[^\"]*text\-decoration\:none\;[^\"]*background\:none\;[^\"]*\"\>(.*?)\<\/div\>', '', str_new)
|
||||
|
||||
if (str_new != pre_remove):
|
||||
count_pocketbook += 1
|
||||
|
||||
|
||||
# Remove eLibri / LemonInk watermark
|
||||
# Run this in a loop, as it is possible a file has been watermarked twice ...
|
||||
while True:
|
||||
@@ -143,6 +151,9 @@ def removeHTMLwatermarks(object, path_to_ebook):
|
||||
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))
|
||||
|
||||
if (count_pocketbook > 0):
|
||||
print("Watermark: Successfully stripped {0} Pocketbook watermark(s) from ebook.".format(count_pocketbook))
|
||||
|
||||
return output
|
||||
|
||||
@@ -198,7 +209,9 @@ def removeOPFwatermarks(object, path_to_ebook):
|
||||
# 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:
|
||||
# To prevent this Regex from applying to books without that watermark, only do that if the watermark above was found.
|
||||
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
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# erdr2pml.py
|
||||
# Copyright © 2008-2020 The Dark Reverser, Apprentice Harper et al.
|
||||
# Copyright © 2008-2021 The Dark Reverser, Apprentice Harper, noDRM et al.
|
||||
#
|
||||
# Changelog
|
||||
#
|
||||
@@ -64,16 +64,14 @@
|
||||
# 0.22 - Unicode and plugin support, different image folders for PMLZ and source
|
||||
# 0.23 - moved unicode_argv call inside main for Windows DeDRM compatibility
|
||||
# 1.00 - Added Python 3 compatibility for calibre 5.0
|
||||
# 1.01 - Bugfixes for standalone version.
|
||||
|
||||
__version__='1.00'
|
||||
|
||||
import sys, re
|
||||
import struct, binascii, getopt, zlib, os, os.path, urllib, tempfile, traceback
|
||||
|
||||
if 'calibre' in sys.modules:
|
||||
inCalibre = True
|
||||
else:
|
||||
inCalibre = False
|
||||
#@@CALIBRE_COMPAT_CODE@@
|
||||
|
||||
# Wrap a stream so that output gets flushed immediately
|
||||
# and also make sure that any unicode strings get
|
||||
@@ -141,40 +139,25 @@ def unicode_argv():
|
||||
Des = None
|
||||
if iswindows:
|
||||
# first try with pycrypto
|
||||
if inCalibre:
|
||||
from calibre_plugins.dedrm import pycrypto_des
|
||||
else:
|
||||
import pycrypto_des
|
||||
import pycrypto_des
|
||||
Des = pycrypto_des.load_pycrypto()
|
||||
if Des == None:
|
||||
# they try with openssl
|
||||
if inCalibre:
|
||||
from calibre_plugins.dedrm import openssl_des
|
||||
else:
|
||||
import openssl_des
|
||||
import openssl_des
|
||||
Des = openssl_des.load_libcrypto()
|
||||
else:
|
||||
# first try with openssl
|
||||
if inCalibre:
|
||||
from calibre_plugins.dedrm import openssl_des
|
||||
else:
|
||||
import openssl_des
|
||||
import openssl_des
|
||||
Des = openssl_des.load_libcrypto()
|
||||
if Des == None:
|
||||
# then try with pycrypto
|
||||
if inCalibre:
|
||||
from calibre_plugins.dedrm import pycrypto_des
|
||||
else:
|
||||
import pycrypto_des
|
||||
import pycrypto_des
|
||||
Des = pycrypto_des.load_pycrypto()
|
||||
|
||||
# if that did not work then use pure python implementation
|
||||
# of DES and try to speed it up with Psycho
|
||||
if Des == None:
|
||||
if inCalibre:
|
||||
from calibre_plugins.dedrm import python_des
|
||||
else:
|
||||
import python_des
|
||||
import python_des
|
||||
Des = python_des.Des
|
||||
# Import Psyco if available
|
||||
try:
|
||||
|
||||
@@ -473,8 +473,10 @@ class DocParser(object):
|
||||
if (link > 0):
|
||||
linktype = self.link_type[link-1]
|
||||
title = self.link_title[link-1]
|
||||
if (title == b"") or (parares.rfind(title.decode('utf-8')) < 0):
|
||||
title=parares[lstart:].encode('utf-8')
|
||||
if isinstance(title, bytes):
|
||||
title = title.decode('utf-8')
|
||||
if (title == "") or (parares.rfind(title) < 0):
|
||||
title=parares[lstart:]
|
||||
if linktype == 'external' :
|
||||
linkhref = self.link_href[link-1]
|
||||
linkhtml = '<a href="%s">' % linkhref
|
||||
@@ -485,9 +487,9 @@ class DocParser(object):
|
||||
else :
|
||||
# just link to the current page
|
||||
linkhtml = '<a href="#' + self.id + '">'
|
||||
linkhtml += title.decode('utf-8')
|
||||
linkhtml += title
|
||||
linkhtml += '</a>'
|
||||
pos = parares.rfind(title.decode('utf-8'))
|
||||
pos = parares.rfind(title)
|
||||
if pos >= 0:
|
||||
parares = parares[0:pos] + linkhtml + parares[pos+len(title):]
|
||||
else :
|
||||
|
||||
@@ -35,25 +35,17 @@ import getopt
|
||||
from struct import pack
|
||||
from struct import unpack
|
||||
|
||||
#@@CALIBRE_COMPAT_CODE@@
|
||||
|
||||
|
||||
class TpzDRMError(Exception):
|
||||
pass
|
||||
|
||||
# local support routines
|
||||
if 'calibre' in sys.modules:
|
||||
inCalibre = True
|
||||
else:
|
||||
inCalibre = False
|
||||
|
||||
if inCalibre :
|
||||
from calibre_plugins.dedrm import convert2xml
|
||||
from calibre_plugins.dedrm import flatxml2html
|
||||
from calibre_plugins.dedrm import flatxml2svg
|
||||
from calibre_plugins.dedrm import stylexml2css
|
||||
else :
|
||||
import convert2xml
|
||||
import flatxml2html
|
||||
import flatxml2svg
|
||||
import stylexml2css
|
||||
import convert2xml
|
||||
import flatxml2html
|
||||
import flatxml2svg
|
||||
import stylexml2css
|
||||
|
||||
# global switch
|
||||
buildXML = False
|
||||
|
||||
@@ -1,448 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# ignobleepub.py
|
||||
# Copyright © 2009-2020 by i♥cabbages, Apprentice Harper et al.
|
||||
|
||||
# Released under the terms of the GNU General Public Licence, version 3
|
||||
# <http://www.gnu.org/licenses/>
|
||||
|
||||
#
|
||||
# Revision history:
|
||||
# 1 - Initial release
|
||||
# 2 - Added OS X support by using OpenSSL when available
|
||||
# 3 - screen out improper key lengths to prevent segfaults on Linux
|
||||
# 3.1 - Allow Windows versions of libcrypto to be found
|
||||
# 3.2 - add support for encoding to 'utf-8' when building up list of files to decrypt from encryption.xml
|
||||
# 3.3 - On Windows try PyCrypto first, OpenSSL next
|
||||
# 3.4 - Modify interface to allow use with import
|
||||
# 3.5 - Fix for potential problem with PyCrypto
|
||||
# 3.6 - Revised to allow use in calibre plugins to eliminate need for duplicate code
|
||||
# 3.7 - Tweaked to match ineptepub more closely
|
||||
# 3.8 - Fixed to retain zip file metadata (e.g. file modification date)
|
||||
# 3.9 - moved unicode_argv call inside main for Windows DeDRM compatibility
|
||||
# 4.0 - Work if TkInter is missing
|
||||
# 4.1 - Import tkFileDialog, don't assume something else will import it.
|
||||
# 5.0 - Python 3 for calibre 5.0
|
||||
|
||||
"""
|
||||
Decrypt Barnes & Noble encrypted ePub books.
|
||||
"""
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = "5.0"
|
||||
|
||||
import sys
|
||||
import os
|
||||
import traceback
|
||||
import base64
|
||||
import zlib
|
||||
import zipfile
|
||||
from zipfile import ZipInfo, ZipFile, ZIP_STORED, ZIP_DEFLATED
|
||||
from contextlib import closing
|
||||
import xml.etree.ElementTree as etree
|
||||
|
||||
# Wrap a stream so that output gets flushed immediately
|
||||
# and also make sure that any unicode strings get
|
||||
# encoded using "replace" before writing them.
|
||||
class SafeUnbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
self.encoding = stream.encoding
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
def write(self, data):
|
||||
if isinstance(data,str) or isinstance(data,unicode):
|
||||
# str for Python3, unicode for Python2
|
||||
data = data.encode(self.encoding,"replace")
|
||||
try:
|
||||
buffer = getattr(self.stream, 'buffer', self.stream)
|
||||
# self.stream.buffer for Python3, self.stream for Python2
|
||||
buffer.write(data)
|
||||
buffer.flush()
|
||||
except:
|
||||
# We can do nothing if a write fails
|
||||
raise
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
try:
|
||||
from calibre.constants import iswindows, isosx
|
||||
except:
|
||||
iswindows = sys.platform.startswith('win')
|
||||
isosx = sys.platform.startswith('darwin')
|
||||
|
||||
def unicode_argv():
|
||||
if iswindows:
|
||||
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
||||
# strings.
|
||||
|
||||
# Versions 2.x of Python don't support Unicode in sys.argv on
|
||||
# Windows, with the underlying Windows API instead replacing multi-byte
|
||||
# characters with '?'.
|
||||
|
||||
|
||||
from ctypes import POINTER, byref, cdll, c_int, windll
|
||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
||||
|
||||
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
||||
GetCommandLineW.argtypes = []
|
||||
GetCommandLineW.restype = LPCWSTR
|
||||
|
||||
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
||||
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
||||
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
||||
|
||||
cmd = GetCommandLineW()
|
||||
argc = c_int(0)
|
||||
argv = CommandLineToArgvW(cmd, byref(argc))
|
||||
if argc.value > 0:
|
||||
# Remove Python executable and commands if present
|
||||
start = argc.value - len(sys.argv)
|
||||
return [argv[i] for i in
|
||||
range(start, argc.value)]
|
||||
return ["ineptepub.py"]
|
||||
else:
|
||||
argvencoding = sys.stdin.encoding or "utf-8"
|
||||
return [arg if (isinstance(arg, str) or isinstance(arg,unicode)) else str(arg, argvencoding) for arg in sys.argv]
|
||||
|
||||
|
||||
class IGNOBLEError(Exception):
|
||||
pass
|
||||
|
||||
def _load_crypto_libcrypto():
|
||||
from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_int, c_long, \
|
||||
Structure, c_ulong, create_string_buffer, cast
|
||||
from ctypes.util import find_library
|
||||
|
||||
if iswindows:
|
||||
libcrypto = find_library('libeay32')
|
||||
else:
|
||||
libcrypto = find_library('crypto')
|
||||
|
||||
if libcrypto is None:
|
||||
raise IGNOBLEError('libcrypto not found')
|
||||
libcrypto = CDLL(libcrypto)
|
||||
|
||||
AES_MAXNR = 14
|
||||
|
||||
c_char_pp = POINTER(c_char_p)
|
||||
c_int_p = POINTER(c_int)
|
||||
|
||||
class AES_KEY(Structure):
|
||||
_fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))),
|
||||
('rounds', c_int)]
|
||||
AES_KEY_p = POINTER(AES_KEY)
|
||||
|
||||
def F(restype, name, argtypes):
|
||||
func = getattr(libcrypto, name)
|
||||
func.restype = restype
|
||||
func.argtypes = argtypes
|
||||
return func
|
||||
|
||||
AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key',
|
||||
[c_char_p, c_int, AES_KEY_p])
|
||||
AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',
|
||||
[c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p,
|
||||
c_int])
|
||||
|
||||
class AES(object):
|
||||
def __init__(self, userkey):
|
||||
self._blocksize = len(userkey)
|
||||
if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) :
|
||||
raise IGNOBLEError('AES improper key used')
|
||||
return
|
||||
key = self._key = AES_KEY()
|
||||
rv = AES_set_decrypt_key(userkey, len(userkey) * 8, key)
|
||||
if rv < 0:
|
||||
raise IGNOBLEError('Failed to initialize AES key')
|
||||
|
||||
def decrypt(self, data):
|
||||
out = create_string_buffer(len(data))
|
||||
iv = (b'\x00' * self._blocksize)
|
||||
rv = AES_cbc_encrypt(data, out, len(data), self._key, iv, 0)
|
||||
if rv == 0:
|
||||
raise IGNOBLEError('AES decryption failed')
|
||||
return out.raw
|
||||
|
||||
return AES
|
||||
|
||||
def _load_crypto_pycrypto():
|
||||
from Crypto.Cipher import AES as _AES
|
||||
|
||||
class AES(object):
|
||||
def __init__(self, key):
|
||||
self._aes = _AES.new(key, _AES.MODE_CBC, b'\x00'*16)
|
||||
|
||||
def decrypt(self, data):
|
||||
return self._aes.decrypt(data)
|
||||
|
||||
return AES
|
||||
|
||||
def _load_crypto():
|
||||
AES = None
|
||||
cryptolist = (_load_crypto_libcrypto, _load_crypto_pycrypto)
|
||||
if sys.platform.startswith('win'):
|
||||
cryptolist = (_load_crypto_pycrypto, _load_crypto_libcrypto)
|
||||
for loader in cryptolist:
|
||||
try:
|
||||
AES = loader()
|
||||
break
|
||||
except (ImportError, IGNOBLEError):
|
||||
pass
|
||||
return AES
|
||||
|
||||
AES = _load_crypto()
|
||||
|
||||
META_NAMES = ('mimetype', 'META-INF/rights.xml', 'META-INF/encryption.xml')
|
||||
NSMAP = {'adept': 'http://ns.adobe.com/adept',
|
||||
'enc': 'http://www.w3.org/2001/04/xmlenc#'}
|
||||
|
||||
class Decryptor(object):
|
||||
def __init__(self, bookkey, encryption):
|
||||
enc = lambda tag: '{%s}%s' % (NSMAP['enc'], tag)
|
||||
self._aes = AES(bookkey)
|
||||
encryption = etree.fromstring(encryption)
|
||||
self._encrypted = encrypted = set()
|
||||
expr = './%s/%s/%s' % (enc('EncryptedData'), enc('CipherData'),
|
||||
enc('CipherReference'))
|
||||
for elem in encryption.findall(expr):
|
||||
path = elem.get('URI', None)
|
||||
if path is not None:
|
||||
path = path.encode('utf-8')
|
||||
encrypted.add(path)
|
||||
|
||||
def decompress(self, bytes):
|
||||
dc = zlib.decompressobj(-15)
|
||||
bytes = dc.decompress(bytes)
|
||||
ex = dc.decompress(b'Z') + dc.flush()
|
||||
if ex:
|
||||
bytes = bytes + ex
|
||||
return bytes
|
||||
|
||||
def decrypt(self, path, data):
|
||||
if bytes(path,'utf-8') in self._encrypted:
|
||||
data = self._aes.decrypt(data)[16:]
|
||||
data = data[:-data[-1]]
|
||||
data = self.decompress(data)
|
||||
return data
|
||||
|
||||
# check file to make check whether it's probably an Adobe Adept encrypted ePub
|
||||
def ignobleBook(inpath):
|
||||
with closing(ZipFile(open(inpath, 'rb'))) as inf:
|
||||
namelist = set(inf.namelist())
|
||||
if 'META-INF/rights.xml' not in namelist or \
|
||||
'META-INF/encryption.xml' not in namelist:
|
||||
return False
|
||||
try:
|
||||
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
|
||||
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
||||
expr = './/%s' % (adept('encryptedKey'),)
|
||||
bookkey = ''.join(rights.findtext(expr))
|
||||
if len(bookkey) == 64:
|
||||
return True
|
||||
except:
|
||||
# if we couldn't check, assume it is
|
||||
return True
|
||||
return False
|
||||
|
||||
def decryptBook(keyb64, inpath, outpath):
|
||||
if AES is None:
|
||||
raise IGNOBLEError("PyCrypto or OpenSSL must be installed.")
|
||||
key = base64.b64decode(keyb64)[:16]
|
||||
aes = AES(key)
|
||||
with closing(ZipFile(open(inpath, 'rb'))) as inf:
|
||||
namelist = set(inf.namelist())
|
||||
if 'META-INF/rights.xml' not in namelist or \
|
||||
'META-INF/encryption.xml' not in namelist:
|
||||
print("{0:s} is DRM-free.".format(os.path.basename(inpath)))
|
||||
return 1
|
||||
for name in META_NAMES:
|
||||
namelist.remove(name)
|
||||
try:
|
||||
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
|
||||
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
||||
expr = './/%s' % (adept('encryptedKey'),)
|
||||
bookkey = ''.join(rights.findtext(expr))
|
||||
if len(bookkey) != 64:
|
||||
print("{0:s} is not a secure Barnes & Noble ePub.".format(os.path.basename(inpath)))
|
||||
return 1
|
||||
bookkey = aes.decrypt(base64.b64decode(bookkey))
|
||||
bookkey = bookkey[:-bookkey[-1]]
|
||||
encryption = inf.read('META-INF/encryption.xml')
|
||||
decryptor = Decryptor(bookkey[-16:], encryption)
|
||||
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
|
||||
with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf:
|
||||
zi = ZipInfo('mimetype')
|
||||
zi.compress_type=ZIP_STORED
|
||||
try:
|
||||
# if the mimetype is present, get its info, including time-stamp
|
||||
oldzi = inf.getinfo('mimetype')
|
||||
# copy across fields to be preserved
|
||||
zi.date_time = oldzi.date_time
|
||||
zi.comment = oldzi.comment
|
||||
zi.extra = oldzi.extra
|
||||
zi.internal_attr = oldzi.internal_attr
|
||||
# external attributes are dependent on the create system, so copy both.
|
||||
zi.external_attr = oldzi.external_attr
|
||||
zi.create_system = oldzi.create_system
|
||||
except:
|
||||
pass
|
||||
outf.writestr(zi, inf.read('mimetype'))
|
||||
for path in namelist:
|
||||
data = inf.read(path)
|
||||
zi = ZipInfo(path)
|
||||
zi.compress_type=ZIP_DEFLATED
|
||||
try:
|
||||
# get the file info, including time-stamp
|
||||
oldzi = inf.getinfo(path)
|
||||
# copy across useful fields
|
||||
zi.date_time = oldzi.date_time
|
||||
zi.comment = oldzi.comment
|
||||
zi.extra = oldzi.extra
|
||||
zi.internal_attr = oldzi.internal_attr
|
||||
# external attributes are dependent on the create system, so copy both.
|
||||
zi.external_attr = oldzi.external_attr
|
||||
zi.create_system = oldzi.create_system
|
||||
except:
|
||||
pass
|
||||
outf.writestr(zi, decryptor.decrypt(path, data))
|
||||
except:
|
||||
print("Could not decrypt {0:s} because of an exception:\n{1:s}".format(os.path.basename(inpath), traceback.format_exc()))
|
||||
return 2
|
||||
return 0
|
||||
|
||||
|
||||
def cli_main():
|
||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
argv=unicode_argv()
|
||||
progname = os.path.basename(argv[0])
|
||||
if len(argv) != 4:
|
||||
print("usage: {0} <keyfile.b64> <inbook.epub> <outbook.epub>".format(progname))
|
||||
return 1
|
||||
keypath, inpath, outpath = argv[1:]
|
||||
userkey = open(keypath,'rb').read()
|
||||
result = decryptBook(userkey, inpath, outpath)
|
||||
if result == 0:
|
||||
print("Successfully decrypted {0:s} as {1:s}".format(os.path.basename(inpath),os.path.basename(outpath)))
|
||||
return result
|
||||
|
||||
def gui_main():
|
||||
try:
|
||||
import tkinter
|
||||
import tkinter.constants
|
||||
import tkinter.filedialog
|
||||
import tkinter.messagebox
|
||||
import traceback
|
||||
except:
|
||||
return cli_main()
|
||||
|
||||
class DecryptionDialog(tkinter.Frame):
|
||||
def __init__(self, root):
|
||||
tkinter.Frame.__init__(self, root, border=5)
|
||||
self.status = tkinter.Label(self, text="Select files for decryption")
|
||||
self.status.pack(fill=tkinter.constants.X, expand=1)
|
||||
body = tkinter.Frame(self)
|
||||
body.pack(fill=tkinter.constants.X, expand=1)
|
||||
sticky = tkinter.constants.E + tkinter.constants.W
|
||||
body.grid_columnconfigure(1, weight=2)
|
||||
tkinter.Label(body, text="Key file").grid(row=0)
|
||||
self.keypath = tkinter.Entry(body, width=30)
|
||||
self.keypath.grid(row=0, column=1, sticky=sticky)
|
||||
if os.path.exists("bnepubkey.b64"):
|
||||
self.keypath.insert(0, "bnepubkey.b64")
|
||||
button = tkinter.Button(body, text="...", command=self.get_keypath)
|
||||
button.grid(row=0, column=2)
|
||||
tkinter.Label(body, text="Input file").grid(row=1)
|
||||
self.inpath = tkinter.Entry(body, width=30)
|
||||
self.inpath.grid(row=1, column=1, sticky=sticky)
|
||||
button = tkinter.Button(body, text="...", command=self.get_inpath)
|
||||
button.grid(row=1, column=2)
|
||||
tkinter.Label(body, text="Output file").grid(row=2)
|
||||
self.outpath = tkinter.Entry(body, width=30)
|
||||
self.outpath.grid(row=2, column=1, sticky=sticky)
|
||||
button = tkinter.Button(body, text="...", command=self.get_outpath)
|
||||
button.grid(row=2, column=2)
|
||||
buttons = tkinter.Frame(self)
|
||||
buttons.pack()
|
||||
botton = tkinter.Button(
|
||||
buttons, text="Decrypt", width=10, command=self.decrypt)
|
||||
botton.pack(side=tkinter.constants.LEFT)
|
||||
tkinter.Frame(buttons, width=10).pack(side=tkinter.constants.LEFT)
|
||||
button = tkinter.Button(
|
||||
buttons, text="Quit", width=10, command=self.quit)
|
||||
button.pack(side=tkinter.constants.RIGHT)
|
||||
|
||||
def get_keypath(self):
|
||||
keypath = tkinter.filedialog.askopenfilename(
|
||||
parent=None, title="Select Barnes & Noble \'.b64\' key file",
|
||||
defaultextension=".b64",
|
||||
filetypes=[('base64-encoded files', '.b64'),
|
||||
('All Files', '.*')])
|
||||
if keypath:
|
||||
keypath = os.path.normpath(keypath)
|
||||
self.keypath.delete(0, tkinter.constants.END)
|
||||
self.keypath.insert(0, keypath)
|
||||
return
|
||||
|
||||
def get_inpath(self):
|
||||
inpath = tkinter.filedialog.askopenfilename(
|
||||
parent=None, title="Select B&N-encrypted ePub file to decrypt",
|
||||
defaultextension=".epub", filetypes=[('ePub files', '.epub')])
|
||||
if inpath:
|
||||
inpath = os.path.normpath(inpath)
|
||||
self.inpath.delete(0, tkinter.constants.END)
|
||||
self.inpath.insert(0, inpath)
|
||||
return
|
||||
|
||||
def get_outpath(self):
|
||||
outpath = tkinter.filedialog.asksaveasfilename(
|
||||
parent=None, title="Select unencrypted ePub file to produce",
|
||||
defaultextension=".epub", filetypes=[('ePub files', '.epub')])
|
||||
if outpath:
|
||||
outpath = os.path.normpath(outpath)
|
||||
self.outpath.delete(0, tkinter.constants.END)
|
||||
self.outpath.insert(0, outpath)
|
||||
return
|
||||
|
||||
def decrypt(self):
|
||||
keypath = self.keypath.get()
|
||||
inpath = self.inpath.get()
|
||||
outpath = self.outpath.get()
|
||||
if not keypath or not os.path.exists(keypath):
|
||||
self.status['text'] = "Specified key file does not exist"
|
||||
return
|
||||
if not inpath or not os.path.exists(inpath):
|
||||
self.status['text'] = "Specified input file does not exist"
|
||||
return
|
||||
if not outpath:
|
||||
self.status['text'] = "Output file not specified"
|
||||
return
|
||||
if inpath == outpath:
|
||||
self.status['text'] = "Must have different input and output files"
|
||||
return
|
||||
userkey = open(keypath,'rb').read()
|
||||
self.status['text'] = "Decrypting..."
|
||||
try:
|
||||
decrypt_status = decryptBook(userkey, inpath, outpath)
|
||||
except Exception as e:
|
||||
self.status['text'] = "Error: {0}".format(e.args[0])
|
||||
return
|
||||
if decrypt_status == 0:
|
||||
self.status['text'] = "File successfully decrypted"
|
||||
else:
|
||||
self.status['text'] = "The was an error decrypting the file."
|
||||
|
||||
root = tkinter.Tk()
|
||||
root.title("Barnes & Noble ePub Decrypter v.{0}".format(__version__))
|
||||
root.resizable(True, False)
|
||||
root.minsize(300, 0)
|
||||
DecryptionDialog(root).pack(fill=tkinter.constants.X, expand=1)
|
||||
root.mainloop()
|
||||
return 0
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) > 1:
|
||||
sys.exit(cli_main())
|
||||
sys.exit(gui_main())
|
||||
65
DeDRM_plugin/ignoblekeyAndroid.py
Normal file
65
DeDRM_plugin/ignoblekeyAndroid.py
Normal file
@@ -0,0 +1,65 @@
|
||||
'''
|
||||
Extracts the user's ccHash from an .adobe-digital-editions folder
|
||||
typically included in the Nook Android app's data folder.
|
||||
|
||||
Based on ignoblekeyWindowsStore.py, updated for Android by noDRM.
|
||||
'''
|
||||
|
||||
import sys
|
||||
import os
|
||||
import base64
|
||||
try:
|
||||
from Cryptodome.Cipher import AES
|
||||
except:
|
||||
from Crypto.Cipher import AES
|
||||
import hashlib
|
||||
from lxml import etree
|
||||
|
||||
|
||||
PASS_HASH_SECRET = "9ca588496a1bc4394553d9e018d70b9e"
|
||||
|
||||
def unpad(data):
|
||||
|
||||
if sys.version_info[0] == 2:
|
||||
pad_len = ord(data[-1])
|
||||
else:
|
||||
pad_len = data[-1]
|
||||
|
||||
return data[:-pad_len]
|
||||
|
||||
def dump_keys(path_to_adobe_folder):
|
||||
|
||||
activation_path = os.path.join(path_to_adobe_folder, "activation.xml")
|
||||
device_path = os.path.join(path_to_adobe_folder, "device.xml")
|
||||
|
||||
if not os.path.isfile(activation_path):
|
||||
print("Nook activation file is missing: %s\n" % activation_path)
|
||||
return []
|
||||
if not os.path.isfile(device_path):
|
||||
print("Nook device file is missing: %s\n" % device_path)
|
||||
return []
|
||||
|
||||
# Load files:
|
||||
activation_xml = etree.parse(activation_path)
|
||||
device_xml = etree.parse(device_path)
|
||||
|
||||
# Get fingerprint:
|
||||
device_fingerprint = device_xml.findall(".//{http://ns.adobe.com/adept}fingerprint")[0].text
|
||||
device_fingerprint = base64.b64decode(device_fingerprint).hex()
|
||||
|
||||
hash_key = hashlib.sha1(bytearray.fromhex(device_fingerprint + PASS_HASH_SECRET)).digest()[:16]
|
||||
|
||||
hashes = []
|
||||
|
||||
for pass_hash in activation_xml.findall(".//{http://ns.adobe.com/adept}passHash"):
|
||||
encrypted_cc_hash = base64.b64decode(pass_hash.text)
|
||||
cc_hash = unpad(AES.new(hash_key, AES.MODE_CBC, encrypted_cc_hash[:16]).decrypt(encrypted_cc_hash[16:]))
|
||||
hashes.append(base64.b64encode(cc_hash).decode("ascii"))
|
||||
#print("Nook ccHash is %s" % (base64.b64encode(cc_hash).decode("ascii")))
|
||||
|
||||
return hashes
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("No standalone version available.")
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# ignoblekeygen.py
|
||||
# ignoblekeyGenPassHash.py
|
||||
# Copyright © 2009-2020 i♥cabbages, Apprentice Harper et al.
|
||||
|
||||
# Released under the terms of the GNU General Public Licence, version 3
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# ignoblekey.py
|
||||
# ignoblekeyNookStudy.py
|
||||
# Copyright © 2015-2020 Apprentice Alf, Apprentice Harper et al.
|
||||
|
||||
# Based on kindlekey.py, Copyright © 2010-2013 by some_updates and Apprentice Alf
|
||||
@@ -157,7 +157,7 @@ def getNookLogFiles():
|
||||
logpath = path +'\\Barnes & Noble\\NOOKstudy\\logs\\BNClientLog.txt'
|
||||
if os.path.isfile(logpath):
|
||||
found = True
|
||||
print('Found nookStudy log file: ' + logpath.encode('ascii','ignore'))
|
||||
print('Found nookStudy log file: ' + logpath.encode('ascii','ignore'), file=sys.stderr)
|
||||
logFiles.append(logpath)
|
||||
else:
|
||||
home = os.getenv('HOME')
|
||||
@@ -165,26 +165,26 @@ def getNookLogFiles():
|
||||
testpath = home + '/Library/Application Support/Barnes & Noble/DesktopReader/logs/BNClientLog.txt'
|
||||
if os.path.isfile(testpath):
|
||||
logFiles.append(testpath)
|
||||
print('Found nookStudy log file: ' + testpath)
|
||||
print('Found nookStudy log file: ' + testpath, file=sys.stderr)
|
||||
found = True
|
||||
testpath = home + '/Library/Application Support/Barnes & Noble/DesktopReader/indices/BNClientLog.txt'
|
||||
if os.path.isfile(testpath):
|
||||
logFiles.append(testpath)
|
||||
print('Found nookStudy log file: ' + testpath)
|
||||
print('Found nookStudy log file: ' + testpath, file=sys.stderr)
|
||||
found = True
|
||||
testpath = home + '/Library/Application Support/Barnes & Noble/BNDesktopReader/logs/BNClientLog.txt'
|
||||
if os.path.isfile(testpath):
|
||||
logFiles.append(testpath)
|
||||
print('Found nookStudy log file: ' + testpath)
|
||||
print('Found nookStudy log file: ' + testpath, file=sys.stderr)
|
||||
found = True
|
||||
testpath = home + '/Library/Application Support/Barnes & Noble/BNDesktopReader/indices/BNClientLog.txt'
|
||||
if os.path.isfile(testpath):
|
||||
logFiles.append(testpath)
|
||||
print('Found nookStudy log file: ' + testpath)
|
||||
print('Found nookStudy log file: ' + testpath, file=sys.stderr)
|
||||
found = True
|
||||
|
||||
if not found:
|
||||
print('No nook Study log files have been found.')
|
||||
print('No nook Study log files have been found.', file=sys.stderr)
|
||||
return logFiles
|
||||
|
||||
|
||||
@@ -205,7 +205,7 @@ def nookkeys(files = []):
|
||||
for file in files:
|
||||
fileKeys = getKeysFromLog(file)
|
||||
if fileKeys:
|
||||
print("Found {0} keys in the Nook Study log files".format(len(fileKeys)))
|
||||
print("Found {0} keys in the Nook Study log files".format(len(fileKeys)), file=sys.stderr)
|
||||
keys.extend(fileKeys)
|
||||
return list(set(keys))
|
||||
|
||||
@@ -218,7 +218,7 @@ def getkey(outpath, files=[]):
|
||||
outfile = outpath
|
||||
with open(outfile, 'w') as keyfileout:
|
||||
keyfileout.write(keys[-1])
|
||||
print("Saved a key to {0}".format(outfile))
|
||||
print("Saved a key to {0}".format(outfile), file=sys.stderr)
|
||||
else:
|
||||
keycount = 0
|
||||
for key in keys:
|
||||
@@ -229,7 +229,7 @@ def getkey(outpath, files=[]):
|
||||
break
|
||||
with open(outfile, 'w') as keyfileout:
|
||||
keyfileout.write(key)
|
||||
print("Saved a key to {0}".format(outfile))
|
||||
print("Saved a key to {0}".format(outfile), file=sys.stderr)
|
||||
return True
|
||||
return False
|
||||
|
||||
75
DeDRM_plugin/ignoblekeyWindowsStore.py
Normal file
75
DeDRM_plugin/ignoblekeyWindowsStore.py
Normal file
@@ -0,0 +1,75 @@
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
|
||||
'''
|
||||
Obtain the user's ccHash from the Barnes & Noble Nook Windows Store app.
|
||||
https://www.microsoft.com/en-us/p/nook-books-magazines-newspapers-comics/9wzdncrfj33h
|
||||
(Requires a recent Windows version in a supported region (US).)
|
||||
This procedure has been tested with Nook app version 1.11.0.4 under Windows 11.
|
||||
|
||||
Based on experimental standalone python script created by fesiwi at
|
||||
https://github.com/noDRM/DeDRM_tools/discussions/9
|
||||
'''
|
||||
|
||||
import sys, os
|
||||
import apsw
|
||||
import base64
|
||||
try:
|
||||
from Cryptodome.Cipher import AES
|
||||
except:
|
||||
from Crypto.Cipher import AES
|
||||
import hashlib
|
||||
from lxml import etree
|
||||
|
||||
|
||||
NOOK_DATA_FOLDER = "%LOCALAPPDATA%\\Packages\\BarnesNoble.Nook_ahnzqzva31enc\\LocalState"
|
||||
PASS_HASH_SECRET = "9ca588496a1bc4394553d9e018d70b9e"
|
||||
|
||||
def unpad(data):
|
||||
|
||||
if sys.version_info[0] == 2:
|
||||
pad_len = ord(data[-1])
|
||||
else:
|
||||
pad_len = data[-1]
|
||||
|
||||
return data[:-pad_len]
|
||||
|
||||
|
||||
def dump_keys(print_result=False):
|
||||
db_filename = os.path.expandvars(NOOK_DATA_FOLDER + "\\NookDB.db3")
|
||||
|
||||
|
||||
if not os.path.isfile(db_filename):
|
||||
print("Database file not found. Is the Nook Windows Store app installed?")
|
||||
return []
|
||||
|
||||
|
||||
# Python2 has no fetchone() so we have to use fetchall() and discard everything but the first result.
|
||||
# There should only be one result anyways.
|
||||
serial_number = apsw.Connection(db_filename).cursor().execute(
|
||||
"SELECT value FROM bn_internal_key_value_table WHERE key = 'serialNumber';").fetchall()[0][0]
|
||||
|
||||
|
||||
hash_key = hashlib.sha1(bytearray.fromhex(serial_number + PASS_HASH_SECRET)).digest()[:16]
|
||||
|
||||
activation_file_name = os.path.expandvars(NOOK_DATA_FOLDER + "\\settings\\activation.xml")
|
||||
|
||||
if not os.path.isfile(activation_file_name):
|
||||
print("Activation file not found. Are you logged in to your Nook account?")
|
||||
return []
|
||||
|
||||
|
||||
activation_xml = etree.parse(activation_file_name)
|
||||
|
||||
decrypted_hashes = []
|
||||
|
||||
for pass_hash in activation_xml.findall(".//{http://ns.adobe.com/adept}passHash"):
|
||||
encrypted_cc_hash = base64.b64decode(pass_hash.text)
|
||||
cc_hash = unpad(AES.new(hash_key, AES.MODE_CBC, encrypted_cc_hash[:16]).decrypt(encrypted_cc_hash[16:]))
|
||||
decrypted_hashes.append((base64.b64encode(cc_hash).decode("ascii")))
|
||||
if print_result:
|
||||
print("Nook ccHash is %s" % (base64.b64encode(cc_hash).decode("ascii")))
|
||||
|
||||
return decrypted_hashes
|
||||
|
||||
if __name__ == "__main__":
|
||||
dump_keys(True)
|
||||
@@ -25,7 +25,12 @@
|
||||
# 2.0 - Python 3 for calibre 5.0
|
||||
|
||||
"""
|
||||
Fetch Barnes & Noble EPUB user key from B&N servers using email and password
|
||||
Fetch Barnes & Noble EPUB user key from B&N servers using email and password.
|
||||
|
||||
NOTE: This script used to work in the past, but the server it uses is long gone.
|
||||
It can no longer be used to download keys from B&N servers, it is no longer
|
||||
supported by the Calibre plugin, and it will be removed in the future.
|
||||
|
||||
"""
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# ineptepub.py
|
||||
# Copyright © 2009-2020 by i♥cabbages, Apprentice Harper et al.
|
||||
# Copyright © 2009-2021 by i♥cabbages, Apprentice Harper et al.
|
||||
|
||||
# Released under the terms of the GNU General Public Licence, version 3
|
||||
# <http://www.gnu.org/licenses/>
|
||||
@@ -30,18 +30,19 @@
|
||||
# 6.5 - Completely remove erroneous check on DER file sanity
|
||||
# 6.6 - Import tkFileDialog, don't assume something else will import it.
|
||||
# 7.0 - Add Python 3 compatibility for calibre 5.0
|
||||
# 7.1 - Add ignoble support, dropping the dedicated ignobleepub.py script
|
||||
|
||||
"""
|
||||
Decrypt Adobe Digital Editions encrypted ePub books.
|
||||
"""
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = "7.0"
|
||||
__version__ = "7.1"
|
||||
|
||||
import codecs
|
||||
import sys
|
||||
import os
|
||||
import traceback
|
||||
import base64
|
||||
import zlib
|
||||
import zipfile
|
||||
from zipfile import ZipInfo, ZipFile, ZIP_STORED, ZIP_DEFLATED
|
||||
@@ -210,9 +211,14 @@ def _load_crypto_libcrypto():
|
||||
return (AES, RSA)
|
||||
|
||||
def _load_crypto_pycrypto():
|
||||
from Crypto.Cipher import AES as _AES
|
||||
from Crypto.PublicKey import RSA as _RSA
|
||||
from Crypto.Cipher import PKCS1_v1_5 as _PKCS1_v1_5
|
||||
try:
|
||||
from Cryptodome.Cipher import AES as _AES
|
||||
from Cryptodome.PublicKey import RSA as _RSA
|
||||
from Cryptodome.Cipher import PKCS1_v1_5 as _PKCS1_v1_5
|
||||
except:
|
||||
from Crypto.Cipher import AES as _AES
|
||||
from Crypto.PublicKey import RSA as _RSA
|
||||
from Crypto.Cipher import PKCS1_v1_5 as _PKCS1_v1_5
|
||||
|
||||
# ASN.1 parsing code from tlslite
|
||||
class ASN1Error(Exception):
|
||||
@@ -417,13 +423,32 @@ def adeptBook(inpath):
|
||||
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
||||
expr = './/%s' % (adept('encryptedKey'),)
|
||||
bookkey = ''.join(rights.findtext(expr))
|
||||
if len(bookkey) == 172:
|
||||
if len(bookkey) in [192, 172, 64]:
|
||||
return True
|
||||
except:
|
||||
# if we couldn't check, assume it is
|
||||
return True
|
||||
return False
|
||||
|
||||
def isPassHashBook(inpath):
|
||||
# If this is an Adobe book, check if it's a PassHash-encrypted book (B&N)
|
||||
with closing(ZipFile(open(inpath, 'rb'))) as inf:
|
||||
namelist = set(inf.namelist())
|
||||
if 'META-INF/rights.xml' not in namelist or \
|
||||
'META-INF/encryption.xml' not in namelist:
|
||||
return False
|
||||
try:
|
||||
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
|
||||
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
||||
expr = './/%s' % (adept('encryptedKey'),)
|
||||
bookkey = ''.join(rights.findtext(expr))
|
||||
if len(bookkey) == 64:
|
||||
return True
|
||||
except:
|
||||
pass
|
||||
|
||||
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.
|
||||
@@ -463,7 +488,7 @@ def verify_book_key(bookkey):
|
||||
def decryptBook(userkey, inpath, outpath):
|
||||
if AES is None:
|
||||
raise ADEPTError("PyCrypto or OpenSSL must be installed.")
|
||||
rsa = RSA(userkey)
|
||||
|
||||
with closing(ZipFile(open(inpath, 'rb'))) as inf:
|
||||
namelist = inf.namelist()
|
||||
if 'META-INF/rights.xml' not in namelist or \
|
||||
@@ -483,17 +508,43 @@ def decryptBook(userkey, inpath, outpath):
|
||||
print("Try getting your distributor to give you a new ACSM file, then open that in an old version of ADE (2.0).")
|
||||
print("If your book distributor is not enforcing the new DRM yet, this will give you a copy with the old DRM.")
|
||||
raise ADEPTNewVersionError("Book uses new ADEPT encryption")
|
||||
if len(bookkey) != 172:
|
||||
print("{0:s} is not a secure Adobe Adept ePub.".format(os.path.basename(inpath)))
|
||||
|
||||
if len(bookkey) == 172:
|
||||
print("{0:s} is a secure Adobe Adept ePub.".format(os.path.basename(inpath)))
|
||||
elif len(bookkey) == 64:
|
||||
print("{0:s} is a secure Adobe PassHash (B&N) ePub.".format(os.path.basename(inpath)))
|
||||
else:
|
||||
print("{0:s} is not an Adobe-protected ePub!".format(os.path.basename(inpath)))
|
||||
return 1
|
||||
bookkey = rsa.decrypt(codecs.decode(bookkey.encode('ascii'), 'base64'))
|
||||
# Padded as per RSAES-PKCS1-v1_5
|
||||
if len(bookkey) > 16:
|
||||
if verify_book_key(bookkey):
|
||||
bookkey = bookkey[-16:]
|
||||
|
||||
if len(bookkey) != 64:
|
||||
# Normal Adobe ADEPT
|
||||
rsa = RSA(userkey)
|
||||
bookkey = rsa.decrypt(base64.b64decode(bookkey.encode('ascii')))
|
||||
|
||||
# Verify key:
|
||||
if len(bookkey) > 16:
|
||||
# Padded as per RSAES-PKCS1-v1_5
|
||||
if verify_book_key(bookkey):
|
||||
bookkey = bookkey[-16:]
|
||||
else:
|
||||
print("Could not decrypt {0:s}. Wrong key".format(os.path.basename(inpath)))
|
||||
return 2
|
||||
else:
|
||||
# Adobe PassHash / B&N
|
||||
key = base64.b64decode(userkey)[:16]
|
||||
aes = AES(key)
|
||||
bookkey = aes.decrypt(base64.b64decode(bookkey))
|
||||
if type(bookkey[-1]) != int:
|
||||
pad = ord(bookkey[-1])
|
||||
else:
|
||||
print("Could not decrypt {0:s}. Wrong key".format(os.path.basename(inpath)))
|
||||
return 2
|
||||
pad = bookkey[-1]
|
||||
|
||||
bookkey = bookkey[:-pad]
|
||||
|
||||
if len(bookkey) > 16:
|
||||
bookkey = bookkey[-16:]
|
||||
|
||||
encryption = inf.read('META-INF/encryption.xml')
|
||||
decryptor = Decryptor(bookkey, encryption)
|
||||
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
# ineptpdf.py
|
||||
# Copyright © 2009-2020 by i♥cabbages, Apprentice Harper et al.
|
||||
# Copyright © 2021 by noDRM
|
||||
|
||||
# Released under the terms of the GNU General Public Licence, version 3
|
||||
# <http://www.gnu.org/licenses/>
|
||||
@@ -46,13 +47,14 @@
|
||||
# 8.0.5 - Do not process DRM-free documents
|
||||
# 8.0.6 - Replace use of float by Decimal for greater precision, and import tkFileDialog
|
||||
# 9.0.0 - Add Python 3 compatibility for calibre 5
|
||||
# 9.1.0 - Support for decrypting with owner password, support for V=5, R=5 and R=6 PDF files, support for AES256-encrypted PDFs.
|
||||
|
||||
"""
|
||||
Decrypts Adobe ADEPT-encrypted PDF files.
|
||||
"""
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = "9.0.0"
|
||||
__version__ = "9.1.0"
|
||||
|
||||
import codecs
|
||||
import sys
|
||||
@@ -65,6 +67,7 @@ from io import BytesIO
|
||||
from decimal import Decimal
|
||||
import itertools
|
||||
import xml.etree.ElementTree as etree
|
||||
import traceback
|
||||
|
||||
# Wrap a stream so that output gets flushed immediately
|
||||
# and also make sure that any unicode strings get
|
||||
@@ -131,6 +134,9 @@ def unicode_argv():
|
||||
class ADEPTError(Exception):
|
||||
pass
|
||||
|
||||
class ADEPTInvalidPasswordError(Exception):
|
||||
pass
|
||||
|
||||
class ADEPTNewVersionError(Exception):
|
||||
pass
|
||||
|
||||
@@ -184,6 +190,7 @@ def _load_crypto_libcrypto():
|
||||
|
||||
AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',[c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p,c_int])
|
||||
AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key',[c_char_p, c_int, AES_KEY_p])
|
||||
AES_set_encrypt_key = F(c_int, 'AES_set_encrypt_key',[c_char_p, c_int, AES_KEY_p])
|
||||
|
||||
RC4_set_key = F(None,'RC4_set_key',[RC4_KEY_p, c_int, c_char_p])
|
||||
RC4_crypt = F(None,'RC4',[RC4_KEY_p, c_int, c_char_p, c_char_p])
|
||||
@@ -236,7 +243,7 @@ def _load_crypto_libcrypto():
|
||||
class AES(object):
|
||||
MODE_CBC = 0
|
||||
@classmethod
|
||||
def new(cls, userkey, mode, iv):
|
||||
def new(cls, userkey, mode, iv, decrypt=True):
|
||||
self = AES()
|
||||
self._blocksize = len(userkey)
|
||||
# mode is ignored since CBCMODE is only thing supported/used so far
|
||||
@@ -246,7 +253,11 @@ def _load_crypto_libcrypto():
|
||||
return
|
||||
keyctx = self._keyctx = AES_KEY()
|
||||
self._iv = iv
|
||||
rv = AES_set_decrypt_key(userkey, len(userkey) * 8, keyctx)
|
||||
self._isDecrypt = decrypt
|
||||
if decrypt:
|
||||
rv = AES_set_decrypt_key(userkey, len(userkey) * 8, keyctx)
|
||||
else:
|
||||
rv = AES_set_encrypt_key(userkey, len(userkey) * 8, keyctx)
|
||||
if rv < 0:
|
||||
raise ADEPTError('Failed to initialize AES key')
|
||||
return self
|
||||
@@ -255,12 +266,23 @@ def _load_crypto_libcrypto():
|
||||
self._keyctx = None
|
||||
self._iv = 0
|
||||
self._mode = 0
|
||||
self._isDecrypt = None
|
||||
def decrypt(self, data):
|
||||
if not self._isDecrypt:
|
||||
raise ADEPTError("AES not ready for decryption")
|
||||
out = create_string_buffer(len(data))
|
||||
rv = AES_cbc_encrypt(data, out, len(data), self._keyctx, self._iv, 0)
|
||||
if rv == 0:
|
||||
raise ADEPTError('AES decryption failed')
|
||||
return out.raw
|
||||
def encrypt(self, data):
|
||||
if self._isDecrypt:
|
||||
raise ADEPTError("AES not ready for encryption")
|
||||
out = create_string_buffer(len(data))
|
||||
rv = AES_cbc_encrypt(data, out, len(data), self._keyctx, self._iv, 1)
|
||||
if rv == 0:
|
||||
raise ADEPTError('AES decryption failed')
|
||||
return out.raw
|
||||
|
||||
return (ARC4, RSA, AES)
|
||||
|
||||
@@ -373,14 +395,23 @@ def _load_crypto_pycrypto():
|
||||
class AES(object):
|
||||
MODE_CBC = _AES.MODE_CBC
|
||||
@classmethod
|
||||
def new(cls, userkey, mode, iv):
|
||||
def new(cls, userkey, mode, iv, decrypt=True):
|
||||
self = AES()
|
||||
self._aes = _AES.new(userkey, mode, iv)
|
||||
self._decrypt = decrypt
|
||||
return self
|
||||
def __init__(self):
|
||||
self._aes = None
|
||||
self._decrypt = None
|
||||
def decrypt(self, data):
|
||||
if not self._decrypt:
|
||||
raise ADEPTError("AES not ready for decrypt.")
|
||||
|
||||
return self._aes.decrypt(data)
|
||||
def encrypt(self, data):
|
||||
if self._decrypt:
|
||||
raise ADEPTError("AES not ready for encrypt.")
|
||||
return self._aes.encrypt(data)
|
||||
|
||||
class RSA(object):
|
||||
def __init__(self, der):
|
||||
@@ -422,7 +453,7 @@ ARC4, RSA, AES = _load_crypto()
|
||||
# 1 = only if present in input
|
||||
# 2 = always
|
||||
|
||||
GEN_XREF_STM = 1
|
||||
GEN_XREF_STM = 0
|
||||
|
||||
# This is the value for the current document
|
||||
gen_xref_stm = False # will be set in PDFSerializer
|
||||
@@ -1507,6 +1538,16 @@ class PDFDocument(object):
|
||||
|
||||
raise PDFEncryptionError('Unknown filter: param=%r' % param)
|
||||
|
||||
def initialize_and_return_filter(self):
|
||||
if not self.encryption:
|
||||
self.is_printable = self.is_modifiable = self.is_extractable = True
|
||||
self.ready = True
|
||||
return None
|
||||
|
||||
(docid, param) = self.encryption
|
||||
type = literal_name(param['Filter'])
|
||||
return type
|
||||
|
||||
def initialize_adobe_ps(self, password, docid, param):
|
||||
global KEYFILEPATH
|
||||
self.decrypt_key = self.genkey_adobe_ps(param)
|
||||
@@ -1549,30 +1590,185 @@ class PDFDocument(object):
|
||||
PASSWORD_PADDING = b'(\xbfN^Nu\x8aAd\x00NV\xff\xfa\x01\x08..' \
|
||||
b'\x00\xb6\xd0h>\x80/\x0c\xa9\xfedSiz'
|
||||
# experimental aes pw support
|
||||
def initialize_standard(self, password, docid, param):
|
||||
# copy from a global variable
|
||||
|
||||
def check_user_password(self, password, docid, param):
|
||||
V = int_value(param.get('V', 0))
|
||||
if V < 5:
|
||||
return self.check_user_password_V4(password, docid, param)
|
||||
else:
|
||||
return self.check_user_password_V5(password, param)
|
||||
|
||||
def check_owner_password(self, password, docid, param):
|
||||
V = int_value(param.get('V', 0))
|
||||
if V < 5:
|
||||
return self.check_owner_password_V4(password, docid, param)
|
||||
else:
|
||||
return self.check_owner_password_V5(password, param)
|
||||
|
||||
def check_user_password_V5(self, password, param):
|
||||
U = str_value(param['U'])
|
||||
userdata = U[:32]
|
||||
salt = U[32:32+8]
|
||||
# Truncate password:
|
||||
password = password[:min(127, len(password))]
|
||||
if self.hash_V5(password, salt, b"", param) == userdata:
|
||||
return True
|
||||
return None
|
||||
|
||||
def check_owner_password_V5(self, password, param):
|
||||
U = str_value(param['U'])
|
||||
O = str_value(param['O'])
|
||||
userdata = U[:48]
|
||||
ownerdata = O[:32]
|
||||
salt = O[32:32+8]
|
||||
# Truncate password:
|
||||
password = password[:min(127, len(password))]
|
||||
if self.hash_V5(password, salt, userdata, param) == ownerdata:
|
||||
return True
|
||||
return None
|
||||
|
||||
def recover_encryption_key_with_password(self, password, docid, param):
|
||||
# Truncate password:
|
||||
key_password = password[:min(127, len(password))]
|
||||
|
||||
if self.check_owner_password_V5(key_password, param):
|
||||
O = str_value(param['O'])
|
||||
U = str_value(param['U'])
|
||||
OE = str_value(param['OE'])
|
||||
key_salt = O[40:40+8]
|
||||
user_data = U[:48]
|
||||
encrypted_file_key = OE[:32]
|
||||
elif self.check_user_password_V5(key_password, param):
|
||||
U = str_value(param['U'])
|
||||
UE = str_value(param['UE'])
|
||||
key_salt = U[40:40+8]
|
||||
user_data = b""
|
||||
encrypted_file_key = UE[:32]
|
||||
else:
|
||||
raise Exception("Trying to recover key, but neither user nor owner pass is correct.")
|
||||
|
||||
intermediate_key = self.hash_V5(key_password, key_salt, user_data, param)
|
||||
|
||||
file_key = self.process_with_aes(intermediate_key, False, encrypted_file_key)
|
||||
|
||||
return file_key
|
||||
|
||||
|
||||
def process_with_aes(self, key: bytes, encrypt: bool, data: bytes, repetitions: int = 1, iv: bytes = None):
|
||||
if iv is None:
|
||||
keylen = len(key)
|
||||
iv = bytes([0x00]*keylen)
|
||||
|
||||
if not encrypt:
|
||||
plaintext = AES.new(key,AES.MODE_CBC,iv, True).decrypt(data)
|
||||
return plaintext
|
||||
else:
|
||||
aes = AES.new(key, AES.MODE_CBC, iv, False)
|
||||
new_data = bytes(data * repetitions)
|
||||
crypt = aes.encrypt(new_data)
|
||||
return crypt
|
||||
|
||||
|
||||
def hash_V5(self, password, salt, userdata, param):
|
||||
R = int_value(param['R'])
|
||||
K = SHA256(password + salt + userdata)
|
||||
if R < 6:
|
||||
return K
|
||||
elif R == 6:
|
||||
round_number = 0
|
||||
done = False
|
||||
while (not done):
|
||||
round_number = round_number + 1
|
||||
K1 = password + K + userdata
|
||||
if len(K1) < 32:
|
||||
raise Exception("K1 < 32 ...")
|
||||
#def process_with_aes(self, key: bytes, encrypt: bool, data: bytes, repetitions: int = 1, iv: bytes = None):
|
||||
E = self.process_with_aes(K[:16], True, K1, 64, K[16:32])
|
||||
|
||||
E_mod_3 = 0
|
||||
for i in range(16):
|
||||
E_mod_3 += E[i]
|
||||
E_mod_3 = E_mod_3 % 3
|
||||
|
||||
if E_mod_3 == 0:
|
||||
ctx = hashlib.sha256()
|
||||
ctx.update(E)
|
||||
K = ctx.digest()
|
||||
elif E_mod_3 == 1:
|
||||
ctx = hashlib.sha384()
|
||||
ctx.update(E)
|
||||
K = ctx.digest()
|
||||
else:
|
||||
ctx = hashlib.sha512()
|
||||
ctx.update(E)
|
||||
K = ctx.digest()
|
||||
|
||||
if round_number >= 64:
|
||||
ch = int.from_bytes(E[-1:], "big", signed=False)
|
||||
if ch <= round_number - 32:
|
||||
done = True
|
||||
|
||||
result = K[0:32]
|
||||
return result
|
||||
else:
|
||||
raise NotImplementedError("Revision > 6 not supported.")
|
||||
|
||||
|
||||
def check_owner_password_V4(self, password, docid, param):
|
||||
|
||||
# compute_O_rc4_key:
|
||||
V = int_value(param.get('V', 0))
|
||||
if V >= 5:
|
||||
raise Exception("compute_O_rc4_key not possible with V>= 5")
|
||||
|
||||
R = int_value(param.get('R', 0))
|
||||
|
||||
length = int_value(param.get('Length', 40)) # Key length (bits)
|
||||
password = (password+self.PASSWORD_PADDING)[:32]
|
||||
hash = hashlib.md5(password)
|
||||
if R >= 3:
|
||||
for _ in range(50):
|
||||
hash = hashlib.md5(hash.digest()[:length//8])
|
||||
hash = hash.digest()[:length//8]
|
||||
|
||||
# "hash" is the return value of compute_O_rc4_key
|
||||
|
||||
Odata = str_value(param.get('O'))
|
||||
# now call iterate_rc4 ...
|
||||
x = ARC4.new(hash).decrypt(Odata) # 4
|
||||
if R >= 3:
|
||||
for i in range(1,19+1):
|
||||
k = b''.join(bytes([c ^ i]) for c in hash )
|
||||
x = ARC4.new(k).decrypt(x)
|
||||
|
||||
|
||||
# "x" is now the padded user password.
|
||||
|
||||
# If we wanted to recover / extract the user password,
|
||||
# we'd need to trim off the padding string from the end.
|
||||
# As we just want to get access to the encryption key,
|
||||
# we can just hand the password into the check_user_password
|
||||
# as it is, as that function would be adding padding anyways.
|
||||
# This trick only works with V4 and lower.
|
||||
|
||||
enc_key = self.check_user_password(x, docid, param)
|
||||
if enc_key is not None:
|
||||
return enc_key
|
||||
|
||||
return False
|
||||
|
||||
|
||||
|
||||
|
||||
def check_user_password_V4(self, password, docid, param):
|
||||
|
||||
V = int_value(param.get('V', 0))
|
||||
if (V <=0 or V > 4):
|
||||
raise PDFEncryptionError('Unknown algorithm: param=%r' % param)
|
||||
length = int_value(param.get('Length', 40)) # Key length (bits)
|
||||
O = str_value(param['O'])
|
||||
R = int_value(param['R']) # Revision
|
||||
if 5 <= R:
|
||||
raise PDFEncryptionError('Unknown revision: %r' % R)
|
||||
U = str_value(param['U'])
|
||||
P = int_value(param['P'])
|
||||
try:
|
||||
EncMetadata = str_value(param['EncryptMetadata'])
|
||||
except:
|
||||
EncMetadata = b'True'
|
||||
self.is_printable = bool(P & 4)
|
||||
self.is_modifiable = bool(P & 8)
|
||||
self.is_extractable = bool(P & 16)
|
||||
self.is_annotationable = bool(P & 32)
|
||||
self.is_formsenabled = bool(P & 256)
|
||||
self.is_textextractable = bool(P & 512)
|
||||
self.is_assemblable = bool(P & 1024)
|
||||
self.is_formprintable = bool(P & 2048)
|
||||
|
||||
# Algorithm 3.2
|
||||
password = (password+self.PASSWORD_PADDING)[:32] # 1
|
||||
hash = hashlib.md5(password) # 2
|
||||
@@ -1580,9 +1776,13 @@ class PDFDocument(object):
|
||||
hash.update(struct.pack('<l', P)) # 4
|
||||
hash.update(docid[0]) # 5
|
||||
# aes special handling if metadata isn't encrypted
|
||||
if EncMetadata == ('False' or 'false'):
|
||||
try:
|
||||
EncMetadata = str_value(param['EncryptMetadata'])
|
||||
except:
|
||||
EncMetadata = b'True'
|
||||
if (EncMetadata == ('False' or 'false') or V < 4) and R >= 4:
|
||||
hash.update(codecs.decode(b'ffffffff','hex'))
|
||||
if 5 <= R:
|
||||
if R >= 3:
|
||||
# 8
|
||||
for _ in range(50):
|
||||
hash = hashlib.md5(hash.digest()[:length//8])
|
||||
@@ -1603,25 +1803,100 @@ class PDFDocument(object):
|
||||
is_authenticated = (u1 == U)
|
||||
else:
|
||||
is_authenticated = (u1[:16] == U[:16])
|
||||
if not is_authenticated:
|
||||
raise ADEPTError('Password is not correct.')
|
||||
self.decrypt_key = key
|
||||
|
||||
if is_authenticated:
|
||||
return key
|
||||
|
||||
return None
|
||||
|
||||
def initialize_standard(self, password, docid, param):
|
||||
|
||||
self.decrypt_key = None
|
||||
|
||||
|
||||
# copy from a global variable
|
||||
V = int_value(param.get('V', 0))
|
||||
if (V <=0 or V > 5):
|
||||
raise PDFEncryptionError('Unknown algorithm: %r' % V)
|
||||
R = int_value(param['R']) # Revision
|
||||
if R >= 7:
|
||||
raise PDFEncryptionError('Unknown revision: %r' % R)
|
||||
|
||||
# check owner pass:
|
||||
retval = self.check_owner_password(password, docid, param)
|
||||
if retval is True or retval is not None:
|
||||
#print("Owner pass is valid - " + str(retval))
|
||||
if retval is True:
|
||||
self.decrypt_key = self.recover_encryption_key_with_password(password, docid, param)
|
||||
else:
|
||||
self.decrypt_key = retval
|
||||
|
||||
if self.decrypt_key is None or self.decrypt_key is True or self.decrypt_key is False:
|
||||
# That's not the owner password. Check if it's the user password.
|
||||
retval = self.check_user_password(password, docid, param)
|
||||
if retval is True or retval is not None:
|
||||
#print("User pass is valid")
|
||||
if retval is True:
|
||||
self.decrypt_key = self.recover_encryption_key_with_password(password, docid, param)
|
||||
else:
|
||||
self.decrypt_key = retval
|
||||
|
||||
if self.decrypt_key is None or self.decrypt_key is True or self.decrypt_key is False:
|
||||
raise ADEPTInvalidPasswordError("Password invalid.")
|
||||
|
||||
|
||||
P = int_value(param['P'])
|
||||
|
||||
self.is_printable = bool(P & 4)
|
||||
self.is_modifiable = bool(P & 8)
|
||||
self.is_extractable = bool(P & 16)
|
||||
self.is_annotationable = bool(P & 32)
|
||||
self.is_formsenabled = bool(P & 256)
|
||||
self.is_textextractable = bool(P & 512)
|
||||
self.is_assemblable = bool(P & 1024)
|
||||
self.is_formprintable = bool(P & 2048)
|
||||
|
||||
|
||||
# genkey method
|
||||
if V == 1 or V == 2:
|
||||
if V == 1 or V == 2 or V == 4:
|
||||
self.genkey = self.genkey_v2
|
||||
elif V == 3:
|
||||
self.genkey = self.genkey_v3
|
||||
elif V == 4:
|
||||
self.genkey = self.genkey_v2
|
||||
#self.genkey = self.genkey_v3 if V == 3 else self.genkey_v2
|
||||
elif V >= 5:
|
||||
self.genkey = self.genkey_v5
|
||||
|
||||
set_decipher = False
|
||||
|
||||
if V >= 4:
|
||||
# Check if we need new genkey_v4 - only if we're using AES.
|
||||
try:
|
||||
for key in param['CF']:
|
||||
algo = str(param["CF"][key]["CFM"])
|
||||
if algo == "/AESV2":
|
||||
if V == 4:
|
||||
self.genkey = self.genkey_v4
|
||||
set_decipher = True
|
||||
self.decipher = self.decrypt_aes
|
||||
elif algo == "/AESV3":
|
||||
if V == 4:
|
||||
self.genkey = self.genkey_v4
|
||||
set_decipher = True
|
||||
self.decipher = self.decrypt_aes
|
||||
elif algo == "/V2":
|
||||
set_decipher = True
|
||||
self.decipher = self.decrypt_rc4
|
||||
except:
|
||||
pass
|
||||
|
||||
# rc4
|
||||
if V != 4:
|
||||
self.decipher = self.decipher_rc4 # XXX may be AES
|
||||
if V < 4:
|
||||
self.decipher = self.decrypt_rc4 # XXX may be AES
|
||||
# aes
|
||||
elif V == 4 and length == 128:
|
||||
self.decipher = self.decipher_aes
|
||||
elif V == 4 and length == 256:
|
||||
raise PDFNotImplementedError('AES256 encryption is currently unsupported')
|
||||
if not set_decipher:
|
||||
# This should usually already be set by now.
|
||||
# If it's not, assume that V4 and newer are using AES
|
||||
if V >= 4:
|
||||
self.decipher = self.decrypt_aes
|
||||
self.ready = True
|
||||
return
|
||||
|
||||
@@ -1776,17 +2051,11 @@ class PDFDocument(object):
|
||||
key = hash.digest()[:min(len(self.decrypt_key) + 5, 16)]
|
||||
return key
|
||||
|
||||
def decrypt_aes(self, objid, genno, data):
|
||||
key = self.genkey(objid, genno)
|
||||
ivector = data[:16]
|
||||
data = data[16:]
|
||||
plaintext = AES.new(key,AES.MODE_CBC,ivector).decrypt(data)
|
||||
# remove pkcs#5 aes padding
|
||||
cutter = -1 * plaintext[-1]
|
||||
plaintext = plaintext[:cutter]
|
||||
return plaintext
|
||||
def genkey_v5(self, objid, genno):
|
||||
# Looks like they stopped this useless obfuscation.
|
||||
return self.decrypt_key
|
||||
|
||||
def decrypt_aes256(self, objid, genno, data):
|
||||
def decrypt_aes(self, objid, genno, data):
|
||||
key = self.genkey(objid, genno)
|
||||
ivector = data[:16]
|
||||
data = data[16:]
|
||||
@@ -2292,7 +2561,7 @@ class PDFSerializer(object):
|
||||
### are no longer useful, as we have extracted all objects from
|
||||
### them. Therefore leave them out from the output.
|
||||
if obj.dic.get('Type') == LITERAL_OBJSTM and not gen_xref_stm:
|
||||
self.write('(deleted)')
|
||||
self.write(b'(deleted)')
|
||||
else:
|
||||
data = obj.get_decdata()
|
||||
self.serialize_object(obj.dic)
|
||||
@@ -2326,10 +2595,22 @@ def decryptBook(userkey, inpath, outpath, inept=True):
|
||||
serializer.dump(outf)
|
||||
except Exception as e:
|
||||
print("error writing pdf: {0}".format(e))
|
||||
traceback.print_exc()
|
||||
return 2
|
||||
return 0
|
||||
|
||||
|
||||
def getPDFencryptionType(inpath):
|
||||
if RSA is None:
|
||||
raise ADEPTError("PyCryptodome or OpenSSL must be installed.")
|
||||
with open(inpath, 'rb') as inf:
|
||||
doc = doc = PDFDocument()
|
||||
parser = PDFParser(doc, inf)
|
||||
filter = doc.initialize_and_return_filter()
|
||||
return filter
|
||||
|
||||
|
||||
|
||||
def cli_main():
|
||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
|
||||
@@ -72,26 +72,17 @@ import time
|
||||
import html.entities
|
||||
import json
|
||||
|
||||
#@@CALIBRE_COMPAT_CODE@@
|
||||
|
||||
|
||||
class DrmException(Exception):
|
||||
pass
|
||||
|
||||
if 'calibre' in sys.modules:
|
||||
inCalibre = True
|
||||
else:
|
||||
inCalibre = False
|
||||
|
||||
if inCalibre:
|
||||
from calibre_plugins.dedrm import mobidedrm
|
||||
from calibre_plugins.dedrm import topazextract
|
||||
from calibre_plugins.dedrm import kgenpids
|
||||
from calibre_plugins.dedrm import androidkindlekey
|
||||
from calibre_plugins.dedrm import kfxdedrm
|
||||
else:
|
||||
import mobidedrm
|
||||
import topazextract
|
||||
import kgenpids
|
||||
import androidkindlekey
|
||||
import kfxdedrm
|
||||
import mobidedrm
|
||||
import topazextract
|
||||
import kgenpids
|
||||
import androidkindlekey
|
||||
import kfxdedrm
|
||||
|
||||
# Wrap a stream so that output gets flushed immediately
|
||||
# and also make sure that any unicode strings get
|
||||
@@ -243,7 +234,7 @@ def GetDecryptedBook(infile, kDatabases, androidFiles, serials, pids, starttime
|
||||
try:
|
||||
mb.processBook(totalpids)
|
||||
except:
|
||||
mb.cleanup
|
||||
mb.cleanup()
|
||||
raise
|
||||
|
||||
print("Decryption succeeded after {0:.1f} seconds".format(time.time()-starttime))
|
||||
|
||||
@@ -8,16 +8,19 @@
|
||||
# 2.1.1 - Whitespace!
|
||||
|
||||
|
||||
import os
|
||||
import os, sys
|
||||
import shutil
|
||||
import traceback
|
||||
import zipfile
|
||||
|
||||
from io import BytesIO
|
||||
try:
|
||||
from ion import DrmIon, DrmIonVoucher
|
||||
except:
|
||||
from calibre_plugins.dedrm.ion import DrmIon, DrmIonVoucher
|
||||
|
||||
|
||||
#@@CALIBRE_COMPAT_CODE@@
|
||||
|
||||
|
||||
from ion import DrmIon, DrmIonVoucher
|
||||
|
||||
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
|
||||
@@ -1526,7 +1526,7 @@ elif isosx:
|
||||
b'SerialNumber',\
|
||||
b'UsernameHash',\
|
||||
b'kindle.directedid.info',\
|
||||
b'DSN'
|
||||
b'DSN',\
|
||||
b'kindle.accounttype.info',\
|
||||
b'krx.flashcardsplugin.data.encryption_key',\
|
||||
b'krx.notebookexportplugin.data.encryption_key',\
|
||||
|
||||
70
DeDRM_plugin/lcpdedrm.py
Normal file
70
DeDRM_plugin/lcpdedrm.py
Normal file
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# lcpdedrm.py
|
||||
# Copyright © 2021-2022 NoDRM
|
||||
|
||||
# Released under the terms of the GNU General Public Licence, version 3
|
||||
# <http://www.gnu.org/licenses/>
|
||||
|
||||
|
||||
# Revision history:
|
||||
# 1 - Initial release
|
||||
# 2 - LCP DRM code removed due to a DMCA takedown.
|
||||
|
||||
"""
|
||||
This file used to contain code to remove the Readium LCP DRM
|
||||
from eBooks. Unfortunately, Readium has issued a DMCA takedown
|
||||
request, so I was forced to remove that code:
|
||||
|
||||
https://github.com/github/dmca/blob/master/2022/01/2022-01-04-readium.md
|
||||
|
||||
This file now just returns an error message when asked to remove LCP DRM.
|
||||
For more information, see this issue:
|
||||
https://github.com/noDRM/DeDRM_tools/issues/18
|
||||
"""
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = "2"
|
||||
|
||||
import json
|
||||
from zipfile import ZipFile
|
||||
from contextlib import closing
|
||||
|
||||
|
||||
class LCPError(Exception):
|
||||
pass
|
||||
|
||||
# Check file to see if this is an LCP-protected file
|
||||
def isLCPbook(inpath):
|
||||
try:
|
||||
with closing(ZipFile(open(inpath, 'rb'))) as lcpbook:
|
||||
if ("META-INF/license.lcpl" not in lcpbook.namelist() or
|
||||
"META-INF/encryption.xml" not in lcpbook.namelist() or
|
||||
b"EncryptedContentKey" not in lcpbook.read("META-INF/encryption.xml")):
|
||||
return False
|
||||
|
||||
license = json.loads(lcpbook.read('META-INF/license.lcpl'))
|
||||
|
||||
if "id" in license and "encryption" in license and "profile" in license["encryption"]:
|
||||
return True
|
||||
|
||||
except:
|
||||
return False
|
||||
|
||||
return False
|
||||
|
||||
|
||||
# Takes a file and a list of passphrases
|
||||
def decryptLCPbook(inpath, passphrases, parent_object):
|
||||
|
||||
if not isLCPbook(inpath):
|
||||
raise LCPError("This is not an LCP-encrypted book")
|
||||
|
||||
print("LCP: LCP DRM removal no longer supported due to a DMCA takedown request.")
|
||||
print("LCP: The takedown request can be found here: ")
|
||||
print("LCP: https://github.com/github/dmca/blob/master/2022/01/2022-01-04-readium.md ")
|
||||
print("LCP: More information can be found in the Github repository: ")
|
||||
print("LCP: https://github.com/noDRM/DeDRM_tools/issues/18 ")
|
||||
|
||||
raise LCPError("LCP DRM removal no longer supported")
|
||||
@@ -461,7 +461,7 @@ class MobiBook:
|
||||
data406 = self.meta_array[406]
|
||||
val406, = struct.unpack('>Q',data406)
|
||||
if val406 != 0:
|
||||
print("Warning: This is a library or rented ebook ({1}). Continuing ...".format(val406))
|
||||
print("Warning: This is a library or rented ebook ({0}). Continuing ...".format(val406))
|
||||
#raise DrmException("Cannot decode library or rented ebooks.")
|
||||
|
||||
goodpids = []
|
||||
|
||||
@@ -5,17 +5,27 @@
|
||||
__license__ = 'GPL v3'
|
||||
|
||||
# Standard Python modules.
|
||||
import os, sys, re, hashlib
|
||||
import codecs, json
|
||||
import os, sys
|
||||
import traceback
|
||||
|
||||
from calibre.utils.config import dynamic, config_dir, JSONConfig
|
||||
from calibre_plugins.dedrm.__init__ import PLUGIN_NAME, PLUGIN_VERSION
|
||||
from calibre.constants import iswindows, isosx
|
||||
|
||||
#@@CALIBRE_COMPAT_CODE@@
|
||||
|
||||
|
||||
try:
|
||||
from calibre.utils.config import JSONConfig
|
||||
except:
|
||||
from standalone.jsonconfig import JSONConfig
|
||||
|
||||
from __init__ import PLUGIN_NAME
|
||||
|
||||
class DeDRM_Prefs():
|
||||
def __init__(self):
|
||||
JSON_PATH = os.path.join("plugins", PLUGIN_NAME.strip().lower().replace(' ', '_') + '.json')
|
||||
def __init__(self, json_path=None):
|
||||
if json_path is None:
|
||||
JSON_PATH = os.path.join("plugins", PLUGIN_NAME.strip().lower().replace(' ', '_') + '.json')
|
||||
else:
|
||||
JSON_PATH = json_path
|
||||
|
||||
self.dedrmprefs = JSONConfig(JSON_PATH)
|
||||
|
||||
self.dedrmprefs.defaults['configured'] = False
|
||||
@@ -29,6 +39,7 @@ class DeDRM_Prefs():
|
||||
self.dedrmprefs.defaults['pids'] = []
|
||||
self.dedrmprefs.defaults['serials'] = []
|
||||
self.dedrmprefs.defaults['lcp_passphrases'] = []
|
||||
self.dedrmprefs.defaults['adobe_pdf_passphrases'] = []
|
||||
self.dedrmprefs.defaults['adobewineprefix'] = ""
|
||||
self.dedrmprefs.defaults['kindlewineprefix'] = ""
|
||||
|
||||
@@ -52,6 +63,8 @@ class DeDRM_Prefs():
|
||||
self.dedrmprefs['serials'] = []
|
||||
if self.dedrmprefs['lcp_passphrases'] == []:
|
||||
self.dedrmprefs['lcp_passphrases'] = []
|
||||
if self.dedrmprefs['adobe_pdf_passphrases'] == []:
|
||||
self.dedrmprefs['adobe_pdf_passphrases'] = []
|
||||
|
||||
def __getitem__(self,kind = None):
|
||||
if kind is not None:
|
||||
@@ -91,210 +104,3 @@ class DeDRM_Prefs():
|
||||
except:
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
|
||||
def convertprefs(always = False):
|
||||
|
||||
def parseIgnobleString(keystuff):
|
||||
from calibre_plugins.dedrm.ignoblekeygen import generate_key
|
||||
userkeys = []
|
||||
ar = keystuff.split(':')
|
||||
for keystring in ar:
|
||||
try:
|
||||
name, ccn = keystring.split(',')
|
||||
# Generate Barnes & Noble EPUB user key from name and credit card number.
|
||||
keyname = "{0}_{1}".format(name.strip(),ccn.strip()[-4:])
|
||||
keyvalue = generate_key(name, ccn)
|
||||
userkeys.append([keyname,keyvalue])
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
print(e.args[0])
|
||||
pass
|
||||
return userkeys
|
||||
|
||||
def parseeReaderString(keystuff):
|
||||
from calibre_plugins.dedrm.erdr2pml import getuser_key
|
||||
userkeys = []
|
||||
ar = keystuff.split(':')
|
||||
for keystring in ar:
|
||||
try:
|
||||
name, cc = keystring.split(',')
|
||||
# Generate eReader user key from name and credit card number.
|
||||
keyname = "{0}_{1}".format(name.strip(),cc.strip()[-4:])
|
||||
keyvalue = codecs.encode(getuser_key(name,cc),'hex')
|
||||
userkeys.append([keyname,keyvalue])
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
print(e.args[0])
|
||||
pass
|
||||
return userkeys
|
||||
|
||||
def parseKindleString(keystuff):
|
||||
pids = []
|
||||
serials = []
|
||||
ar = keystuff.split(',')
|
||||
for keystring in ar:
|
||||
keystring = str(keystring).strip().replace(" ","")
|
||||
if len(keystring) == 10 or len(keystring) == 8 and keystring not in pids:
|
||||
pids.append(keystring)
|
||||
elif len(keystring) == 16 and keystring[0] == 'B' and keystring not in serials:
|
||||
serials.append(keystring)
|
||||
return (pids,serials)
|
||||
|
||||
def getConfigFiles(extension, encoding = None):
|
||||
# get any files with extension 'extension' in the config dir
|
||||
userkeys = []
|
||||
files = [f for f in os.listdir(config_dir) if f.endswith(extension)]
|
||||
for filename in files:
|
||||
try:
|
||||
fpath = os.path.join(config_dir, filename)
|
||||
key = os.path.splitext(filename)[0]
|
||||
value = open(fpath, 'rb').read()
|
||||
if encoding is not None:
|
||||
value = codecs.encode(value,encoding)
|
||||
userkeys.append([key,value])
|
||||
except:
|
||||
traceback.print_exc()
|
||||
pass
|
||||
return userkeys
|
||||
|
||||
dedrmprefs = DeDRM_Prefs()
|
||||
|
||||
if (not always) and dedrmprefs['configured']:
|
||||
# We've already converted old preferences,
|
||||
# and we're not being forced to do it again, so just return
|
||||
return
|
||||
|
||||
|
||||
print("{0} v{1}: Importing configuration data from old DeDRM plugins".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||
|
||||
IGNOBLEPLUGINNAME = "Ignoble Epub DeDRM"
|
||||
EREADERPLUGINNAME = "eReader PDB 2 PML"
|
||||
OLDKINDLEPLUGINNAME = "K4PC, K4Mac, Kindle Mobi and Topaz DeDRM"
|
||||
|
||||
# get prefs from older tools
|
||||
kindleprefs = JSONConfig(os.path.join("plugins", "K4MobiDeDRM"))
|
||||
ignobleprefs = JSONConfig(os.path.join("plugins", "ignoble_epub_dedrm"))
|
||||
|
||||
# Handle the old ignoble plugin's customization string by converting the
|
||||
# old string to stored keys... get that personal data out of plain sight.
|
||||
from calibre.customize.ui import config
|
||||
sc = config['plugin_customization']
|
||||
val = sc.pop(IGNOBLEPLUGINNAME, None)
|
||||
if val is not None:
|
||||
print("{0} v{1}: Converting old Ignoble plugin configuration string.".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||
priorkeycount = len(dedrmprefs['bandnkeys'])
|
||||
userkeys = parseIgnobleString(str(val))
|
||||
for keypair in userkeys:
|
||||
name = keypair[0]
|
||||
value = keypair[1]
|
||||
dedrmprefs.addnamedvaluetoprefs('bandnkeys', name, value)
|
||||
addedkeycount = len(dedrmprefs['bandnkeys'])-priorkeycount
|
||||
print("{0} v{1}: {2:d} Barnes and Noble {3} imported from old Ignoble plugin configuration string".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, "key" if addedkeycount==1 else "keys"))
|
||||
# Make the json write all the prefs to disk
|
||||
dedrmprefs.writeprefs(False)
|
||||
|
||||
# Handle the old eReader plugin's customization string by converting the
|
||||
# old string to stored keys... get that personal data out of plain sight.
|
||||
val = sc.pop(EREADERPLUGINNAME, None)
|
||||
if val is not None:
|
||||
print("{0} v{1}: Converting old eReader plugin configuration string.".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||
priorkeycount = len(dedrmprefs['ereaderkeys'])
|
||||
userkeys = parseeReaderString(str(val))
|
||||
for keypair in userkeys:
|
||||
name = keypair[0]
|
||||
value = keypair[1]
|
||||
dedrmprefs.addnamedvaluetoprefs('ereaderkeys', name, value)
|
||||
addedkeycount = len(dedrmprefs['ereaderkeys'])-priorkeycount
|
||||
print("{0} v{1}: {2:d} eReader {3} imported from old eReader plugin configuration string".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, "key" if addedkeycount==1 else "keys"))
|
||||
# Make the json write all the prefs to disk
|
||||
dedrmprefs.writeprefs(False)
|
||||
|
||||
# get old Kindle plugin configuration string
|
||||
val = sc.pop(OLDKINDLEPLUGINNAME, None)
|
||||
if val is not None:
|
||||
print("{0} v{1}: Converting old Kindle plugin configuration string.".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||
priorpidcount = len(dedrmprefs['pids'])
|
||||
priorserialcount = len(dedrmprefs['serials'])
|
||||
pids, serials = parseKindleString(val)
|
||||
for pid in pids:
|
||||
dedrmprefs.addvaluetoprefs('pids',pid)
|
||||
for serial in serials:
|
||||
dedrmprefs.addvaluetoprefs('serials',serial)
|
||||
addedpidcount = len(dedrmprefs['pids']) - priorpidcount
|
||||
addedserialcount = len(dedrmprefs['serials']) - priorserialcount
|
||||
print("{0} v{1}: {2:d} {3} and {4:d} {5} imported from old Kindle plugin configuration string.".format(PLUGIN_NAME, PLUGIN_VERSION, addedpidcount, "PID" if addedpidcount==1 else "PIDs", addedserialcount, "serial number" if addedserialcount==1 else "serial numbers"))
|
||||
# Make the json write all the prefs to disk
|
||||
dedrmprefs.writeprefs(False)
|
||||
|
||||
# copy the customisations back into calibre preferences, as we've now removed the nasty plaintext
|
||||
config['plugin_customization'] = sc
|
||||
|
||||
# get any .b64 files in the config dir
|
||||
priorkeycount = len(dedrmprefs['bandnkeys'])
|
||||
bandnfilekeys = getConfigFiles('.b64')
|
||||
for keypair in bandnfilekeys:
|
||||
name = keypair[0]
|
||||
value = keypair[1]
|
||||
dedrmprefs.addnamedvaluetoprefs('bandnkeys', name, value)
|
||||
addedkeycount = len(dedrmprefs['bandnkeys'])-priorkeycount
|
||||
if addedkeycount > 0:
|
||||
print("{0} v{1}: {2:d} Barnes and Noble {3} imported from config folder.".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, "key file" if addedkeycount==1 else "key files"))
|
||||
# Make the json write all the prefs to disk
|
||||
dedrmprefs.writeprefs(False)
|
||||
|
||||
# get any .der files in the config dir
|
||||
priorkeycount = len(dedrmprefs['adeptkeys'])
|
||||
adeptfilekeys = getConfigFiles('.der','hex')
|
||||
for keypair in adeptfilekeys:
|
||||
name = keypair[0]
|
||||
value = keypair[1]
|
||||
dedrmprefs.addnamedvaluetoprefs('adeptkeys', name, value)
|
||||
addedkeycount = len(dedrmprefs['adeptkeys'])-priorkeycount
|
||||
if addedkeycount > 0:
|
||||
print("{0} v{1}: {2:d} Adobe Adept {3} imported from config folder.".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, "keyfile" if addedkeycount==1 else "keyfiles"))
|
||||
# Make the json write all the prefs to disk
|
||||
dedrmprefs.writeprefs(False)
|
||||
|
||||
# get ignoble json prefs
|
||||
if 'keys' in ignobleprefs:
|
||||
priorkeycount = len(dedrmprefs['bandnkeys'])
|
||||
for name in ignobleprefs['keys']:
|
||||
value = ignobleprefs['keys'][name]
|
||||
dedrmprefs.addnamedvaluetoprefs('bandnkeys', name, value)
|
||||
addedkeycount = len(dedrmprefs['bandnkeys']) - priorkeycount
|
||||
# no need to delete old prefs, since they contain no recoverable private data
|
||||
if addedkeycount > 0:
|
||||
print("{0} v{1}: {2:d} Barnes and Noble {3} imported from Ignoble plugin preferences.".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, "key" if addedkeycount==1 else "keys"))
|
||||
# Make the json write all the prefs to disk
|
||||
dedrmprefs.writeprefs(False)
|
||||
|
||||
# get kindle json prefs
|
||||
priorpidcount = len(dedrmprefs['pids'])
|
||||
priorserialcount = len(dedrmprefs['serials'])
|
||||
if 'pids' in kindleprefs:
|
||||
pids, serials = parseKindleString(kindleprefs['pids'])
|
||||
for pid in pids:
|
||||
dedrmprefs.addvaluetoprefs('pids',pid)
|
||||
if 'serials' in kindleprefs:
|
||||
pids, serials = parseKindleString(kindleprefs['serials'])
|
||||
for serial in serials:
|
||||
dedrmprefs.addvaluetoprefs('serials',serial)
|
||||
addedpidcount = len(dedrmprefs['pids']) - priorpidcount
|
||||
if addedpidcount > 0:
|
||||
print("{0} v{1}: {2:d} {3} imported from Kindle plugin preferences".format(PLUGIN_NAME, PLUGIN_VERSION, addedpidcount, "PID" if addedpidcount==1 else "PIDs"))
|
||||
addedserialcount = len(dedrmprefs['serials']) - priorserialcount
|
||||
if addedserialcount > 0:
|
||||
print("{0} v{1}: {2:d} {3} imported from Kindle plugin preferences".format(PLUGIN_NAME, PLUGIN_VERSION, addedserialcount, "serial number" if addedserialcount==1 else "serial numbers"))
|
||||
try:
|
||||
if 'wineprefix' in kindleprefs and kindleprefs['wineprefix'] != "":
|
||||
dedrmprefs.set('adobewineprefix',kindleprefs['wineprefix'])
|
||||
dedrmprefs.set('kindlewineprefix',kindleprefs['wineprefix'])
|
||||
print("{0} v{1}: WINEPREFIX ‘(2)’ imported from Kindle plugin preferences".format(PLUGIN_NAME, PLUGIN_VERSION, kindleprefs['wineprefix']))
|
||||
except:
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
# Make the json write all the prefs to disk
|
||||
dedrmprefs.writeprefs()
|
||||
print("{0} v{1}: Finished setting up configuration data.".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||
|
||||
@@ -5,15 +5,19 @@
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
|
||||
#@@CALIBRE_COMPAT_CODE@@
|
||||
|
||||
|
||||
import re
|
||||
import traceback
|
||||
import calibre_plugins.dedrm.ineptepub
|
||||
import calibre_plugins.dedrm.ignobleepub
|
||||
import calibre_plugins.dedrm.epubtest
|
||||
import calibre_plugins.dedrm.zipfix
|
||||
import calibre_plugins.dedrm.ineptpdf
|
||||
import calibre_plugins.dedrm.erdr2pml
|
||||
import calibre_plugins.dedrm.k4mobidedrm
|
||||
import ineptepub
|
||||
import epubtest
|
||||
import zipfix
|
||||
import ineptpdf
|
||||
import erdr2pml
|
||||
import k4mobidedrm
|
||||
|
||||
def decryptepub(infile, outdir, rscpath):
|
||||
errlog = ''
|
||||
@@ -50,8 +54,8 @@ def decryptepub(infile, outdir, rscpath):
|
||||
errlog += traceback.format_exc()
|
||||
errlog += str(e)
|
||||
rv = 1
|
||||
# now try with ignoble epub
|
||||
elif ignobleepub.ignobleBook(zippath):
|
||||
|
||||
# now try with ignoble epub
|
||||
# try with any keyfiles (*.b64) in the rscpath
|
||||
files = os.listdir(rscpath)
|
||||
filefilter = re.compile("\.b64$", re.IGNORECASE)
|
||||
@@ -62,7 +66,7 @@ def decryptepub(infile, outdir, rscpath):
|
||||
userkey = open(keypath,'r').read()
|
||||
#print userkey
|
||||
try:
|
||||
rv = ignobleepub.decryptBook(userkey, zippath, outfile)
|
||||
rv = ineptepub.decryptBook(userkey, zippath, outfile)
|
||||
if rv == 0:
|
||||
print("Decrypted B&N ePub with key file {0}".format(filename))
|
||||
break
|
||||
@@ -121,7 +125,7 @@ def decryptpdb(infile, outdir, rscpath):
|
||||
rv = 1
|
||||
socialpath = os.path.join(rscpath,'sdrmlist.txt')
|
||||
if os.path.exists(socialpath):
|
||||
keydata = file(socialpath,'r').read()
|
||||
keydata = open(socialpath,'r').read()
|
||||
keydata = keydata.rstrip(os.linesep)
|
||||
ar = keydata.split(',')
|
||||
for i in ar:
|
||||
@@ -148,7 +152,7 @@ def decryptk4mobi(infile, outdir, rscpath):
|
||||
pidnums = []
|
||||
pidspath = os.path.join(rscpath,'pidlist.txt')
|
||||
if os.path.exists(pidspath):
|
||||
pidstr = file(pidspath,'r').read()
|
||||
pidstr = open(pidspath,'r').read()
|
||||
pidstr = pidstr.rstrip(os.linesep)
|
||||
pidstr = pidstr.strip()
|
||||
if pidstr != '':
|
||||
@@ -156,7 +160,7 @@ def decryptk4mobi(infile, outdir, rscpath):
|
||||
serialnums = []
|
||||
serialnumspath = os.path.join(rscpath,'seriallist.txt')
|
||||
if os.path.exists(serialnumspath):
|
||||
serialstr = file(serialnumspath,'r').read()
|
||||
serialstr = open(serialnumspath,'r').read()
|
||||
serialstr = serialstr.rstrip(os.linesep)
|
||||
serialstr = serialstr.strip()
|
||||
if serialstr != '':
|
||||
|
||||
278
DeDRM_plugin/standalone/__init__.py
Normal file
278
DeDRM_plugin/standalone/__init__.py
Normal file
@@ -0,0 +1,278 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# CLI interface for the DeDRM plugin (useable without Calibre, too)
|
||||
|
||||
from __future__ import absolute_import, print_function
|
||||
|
||||
# Copyright © 2021 NoDRM
|
||||
|
||||
OPT_SHORT_TO_LONG = [
|
||||
["c", "config"],
|
||||
["e", "extract"],
|
||||
["f", "force"],
|
||||
["h", "help"],
|
||||
["i", "import"],
|
||||
["o", "output"],
|
||||
["p", "password"],
|
||||
["q", "quiet"],
|
||||
["t", "test"],
|
||||
["u", "username"],
|
||||
["v", "verbose"],
|
||||
]
|
||||
|
||||
#@@CALIBRE_COMPAT_CODE@@
|
||||
|
||||
import os, sys
|
||||
|
||||
|
||||
global _additional_data
|
||||
global _additional_params
|
||||
global _function
|
||||
_additional_data = []
|
||||
_additional_params = []
|
||||
_function = None
|
||||
|
||||
global config_file_path
|
||||
config_file_path = "dedrm.json"
|
||||
|
||||
def print_fname(f, info):
|
||||
print(" " + f.ljust(15) + " " + info)
|
||||
|
||||
def print_opt(short, long, info):
|
||||
if short is None:
|
||||
short = " "
|
||||
else:
|
||||
short = " -" + short
|
||||
|
||||
if long is None:
|
||||
long = " "
|
||||
else:
|
||||
long = "--" + long.ljust(16)
|
||||
|
||||
print(short + " " + long + " " + info, file=sys.stderr)
|
||||
|
||||
def print_std_usage(name, param_string):
|
||||
print("Usage: ", file=sys.stderr)
|
||||
if "calibre" in sys.modules:
|
||||
print(" calibre-debug -r \"DeDRM\" -- "+name+" " + param_string, file=sys.stderr)
|
||||
else:
|
||||
print(" python3 DeDRM_plugin.zip "+name+" "+param_string, file=sys.stderr)
|
||||
|
||||
def print_err_header():
|
||||
from __init__ import PLUGIN_NAME, PLUGIN_VERSION # type: ignore
|
||||
|
||||
print(PLUGIN_NAME + " v" + PLUGIN_VERSION + " - DRM removal plugin by noDRM")
|
||||
print()
|
||||
|
||||
def print_help():
|
||||
from __version import PLUGIN_NAME, PLUGIN_VERSION
|
||||
print(PLUGIN_NAME + " v" + PLUGIN_VERSION + " - DRM removal plugin by noDRM")
|
||||
print("Based on DeDRM Calibre plugin by Apprentice Harper, Apprentice Alf and others.")
|
||||
print("See https://github.com/noDRM/DeDRM_tools for more information.")
|
||||
print()
|
||||
if "calibre" in sys.modules:
|
||||
print("This plugin can be run through Calibre - like you are doing right now - ")
|
||||
print("but it can also be executed with a standalone Python interpreter.")
|
||||
else:
|
||||
print("This plugin can either be imported into Calibre, or be executed directly")
|
||||
print("through Python like you are doing right now.")
|
||||
print()
|
||||
print("Available functions:")
|
||||
print_fname("passhash", "Manage Adobe PassHashes")
|
||||
print_fname("remove_drm", "Remove DRM from one or multiple books")
|
||||
print()
|
||||
|
||||
# TODO: All parameters that are global should be listed here.
|
||||
|
||||
def print_credits():
|
||||
from __version import PLUGIN_NAME, PLUGIN_VERSION
|
||||
print(PLUGIN_NAME + " v" + PLUGIN_VERSION + " - Calibre DRM removal plugin by noDRM")
|
||||
print("Based on DeDRM Calibre plugin by Apprentice Harper, Apprentice Alf and others.")
|
||||
print("See https://github.com/noDRM/DeDRM_tools for more information.")
|
||||
print()
|
||||
print("Credits:")
|
||||
print(" - noDRM for the current release of the DeDRM plugin")
|
||||
print(" - Apprentice Alf and Apprentice Harper for the previous versions of the DeDRM plugin")
|
||||
print(" - The Dark Reverser for the Mobipocket and eReader script")
|
||||
print(" - i ♥ cabbages for the Adobe Digital Editions scripts")
|
||||
print(" - Skindle aka Bart Simpson for the Amazon Kindle for PC script")
|
||||
print(" - CMBDTC for Amazon Topaz DRM removal script")
|
||||
print(" - some_updates, clarknova and Bart Simpson for Amazon Topaz conversion scripts")
|
||||
print(" - DiapDealer for the first calibre plugin versions of the tools")
|
||||
print(" - some_updates, DiapDealer, Apprentice Alf and mdlnx for Amazon Kindle/Mobipocket tools")
|
||||
print(" - some_updates for the DeDRM all-in-one Python tool")
|
||||
print(" - Apprentice Alf for the DeDRM all-in-one AppleScript tool")
|
||||
|
||||
|
||||
def handle_single_argument(arg, next):
|
||||
used_up = 0
|
||||
global _additional_params
|
||||
global config_file_path
|
||||
|
||||
if arg in ["--username", "--password", "--output", "--outputdir"]:
|
||||
used_up = 1
|
||||
_additional_params.append(arg)
|
||||
if next is None or len(next) == 0:
|
||||
print_err_header()
|
||||
print("Missing parameter for argument " + arg, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
else:
|
||||
_additional_params.append(next[0])
|
||||
|
||||
elif arg == "--config":
|
||||
if next is None or len(next) == 0:
|
||||
print_err_header()
|
||||
print("Missing parameter for argument " + arg, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
config_file_path = next[0]
|
||||
used_up = 1
|
||||
|
||||
elif arg in ["--help", "--credits", "--verbose", "--quiet", "--extract", "--import", "--overwrite", "--force"]:
|
||||
_additional_params.append(arg)
|
||||
|
||||
|
||||
else:
|
||||
print_err_header()
|
||||
print("Unknown argument: " + arg, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# Used up 0 additional arguments
|
||||
return used_up
|
||||
|
||||
|
||||
|
||||
def handle_data(data):
|
||||
global _function
|
||||
global _additional_data
|
||||
|
||||
if _function is None:
|
||||
_function = str(data)
|
||||
else:
|
||||
_additional_data.append(str(data))
|
||||
|
||||
def execute_action(action, filenames, params):
|
||||
print("Executing '{0}' on file(s) {1} with parameters {2}".format(action, str(filenames), str(params)), file=sys.stderr)
|
||||
|
||||
if action == "help":
|
||||
print_help()
|
||||
sys.exit(0)
|
||||
|
||||
elif action == "passhash":
|
||||
from standalone.passhash import perform_action
|
||||
perform_action(params, filenames)
|
||||
|
||||
elif action == "remove_drm":
|
||||
if not os.path.isfile(os.path.abspath(config_file_path)):
|
||||
print("Config file missing ...")
|
||||
|
||||
from standalone.remove_drm import perform_action
|
||||
perform_action(params, filenames)
|
||||
|
||||
elif action == "config":
|
||||
import prefs
|
||||
config = prefs.DeDRM_Prefs(os.path.abspath(config_file_path))
|
||||
print(config["adeptkeys"])
|
||||
|
||||
else:
|
||||
print("Command '"+action+"' is unknown.", file=sys.stderr)
|
||||
|
||||
|
||||
def main(argv):
|
||||
arguments = argv
|
||||
skip_opts = False
|
||||
|
||||
# First element is always the ZIP name, remove that.
|
||||
if not arguments[0].lower().endswith(".zip") and not "calibre" in sys.modules:
|
||||
print("Warning: File name does not end in .zip ...")
|
||||
print(arguments)
|
||||
arguments.pop(0)
|
||||
|
||||
while len(arguments) > 0:
|
||||
arg = arguments.pop(0)
|
||||
|
||||
if arg == "--":
|
||||
skip_opts = True
|
||||
continue
|
||||
|
||||
if not skip_opts:
|
||||
if arg.startswith("--"):
|
||||
# Give the current arg, plus all remaining ones.
|
||||
# Return the number of additional args we used.
|
||||
used = handle_single_argument(arg, arguments)
|
||||
for _ in range(used):
|
||||
# Function returns number of additional arguments that were
|
||||
# "used up" by that argument.
|
||||
# Remove that amount of arguments from the list.
|
||||
try:
|
||||
arguments.pop(0)
|
||||
except:
|
||||
pass
|
||||
continue
|
||||
elif arg.startswith("-"):
|
||||
single_args = list(arg[1:])
|
||||
# single_args is now a list of single chars, for when you call the program like "ls -alR"
|
||||
# with multiple single-letter options combined.
|
||||
while len(single_args) > 0:
|
||||
c = single_args.pop(0)
|
||||
|
||||
# See if we have a long name for that option.
|
||||
for wrapper in OPT_SHORT_TO_LONG:
|
||||
if wrapper[0] == c:
|
||||
c = "--" + wrapper[1]
|
||||
break
|
||||
else:
|
||||
c = "-" + c
|
||||
# c is now the long term (unless there is no long version, then it's the short version).
|
||||
|
||||
if len(single_args) > 0:
|
||||
# If we have more short arguments, the argument for this one must be None.
|
||||
handle_single_argument(c, None)
|
||||
used = 0
|
||||
else:
|
||||
# If not, then there might be parameters for this short argument.
|
||||
used = handle_single_argument(c, arguments)
|
||||
|
||||
for _ in range(used):
|
||||
# Function returns number of additional arguments that were
|
||||
# "used up" by that argument.
|
||||
# Remove that amount of arguments from the list.
|
||||
try:
|
||||
arguments.pop(0)
|
||||
except:
|
||||
pass
|
||||
|
||||
continue
|
||||
|
||||
handle_data(arg)
|
||||
|
||||
|
||||
if _function is None and "--credits" in _additional_params:
|
||||
print_credits()
|
||||
sys.exit(0)
|
||||
|
||||
if _function is None and "--help" in _additional_params:
|
||||
print_help()
|
||||
sys.exit(0)
|
||||
|
||||
if _function is None:
|
||||
print_help()
|
||||
sys.exit(1)
|
||||
|
||||
# Okay, now actually begin doing stuff.
|
||||
# This function gets told what to do and gets additional data (filenames).
|
||||
# It also receives additional parameters.
|
||||
# The rest of the code will be in different Python files.
|
||||
execute_action(_function.lower(), _additional_data, _additional_params)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# NOTE: This MUST not do anything else other than calling main()
|
||||
# All the code must be in main(), not in here.
|
||||
import sys
|
||||
main(sys.argv)
|
||||
140
DeDRM_plugin/standalone/jsonconfig.py
Normal file
140
DeDRM_plugin/standalone/jsonconfig.py
Normal file
@@ -0,0 +1,140 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# CLI interface for the DeDRM plugin (useable without Calibre, too)
|
||||
# Config implementation
|
||||
|
||||
from __future__ import absolute_import, print_function
|
||||
|
||||
# Taken from Calibre code - Copyright © 2008, Kovid Goyal kovid@kovidgoyal.net, GPLv3
|
||||
|
||||
#@@CALIBRE_COMPAT_CODE@@
|
||||
|
||||
import sys, os, codecs, json
|
||||
|
||||
config_dir = "/"
|
||||
CONFIG_DIR_MODE = 0o700
|
||||
iswindows = sys.platform.startswith('win')
|
||||
|
||||
|
||||
filesystem_encoding = sys.getfilesystemencoding()
|
||||
if filesystem_encoding is None:
|
||||
filesystem_encoding = 'utf-8'
|
||||
else:
|
||||
try:
|
||||
if codecs.lookup(filesystem_encoding).name == 'ascii':
|
||||
filesystem_encoding = 'utf-8'
|
||||
# On linux, unicode arguments to os file functions are coerced to an ascii
|
||||
# bytestring if sys.getfilesystemencoding() == 'ascii', which is
|
||||
# just plain dumb. This is fixed by the icu.py module which, when
|
||||
# imported changes ascii to utf-8
|
||||
except Exception:
|
||||
filesystem_encoding = 'utf-8'
|
||||
|
||||
|
||||
class JSONConfig(dict):
|
||||
|
||||
EXTENSION = '.json'
|
||||
|
||||
|
||||
def __init__(self, rel_path_to_cf_file, base_path=config_dir):
|
||||
dict.__init__(self)
|
||||
self.no_commit = False
|
||||
self.defaults = {}
|
||||
self.file_path = os.path.join(base_path,
|
||||
*(rel_path_to_cf_file.split('/')))
|
||||
self.file_path = os.path.abspath(self.file_path)
|
||||
if not self.file_path.endswith(self.EXTENSION):
|
||||
self.file_path += self.EXTENSION
|
||||
|
||||
self.refresh()
|
||||
|
||||
def mtime(self):
|
||||
try:
|
||||
return os.path.getmtime(self.file_path)
|
||||
except OSError:
|
||||
return 0
|
||||
|
||||
def touch(self):
|
||||
try:
|
||||
os.utime(self.file_path, None)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def decouple(self, prefix):
|
||||
self.file_path = os.path.join(os.path.dirname(self.file_path), prefix + os.path.basename(self.file_path))
|
||||
self.refresh()
|
||||
|
||||
def refresh(self, clear_current=True):
|
||||
d = {}
|
||||
if os.path.exists(self.file_path):
|
||||
with open(self.file_path, "rb") as f:
|
||||
raw = f.read()
|
||||
try:
|
||||
d = self.raw_to_object(raw) if raw.strip() else {}
|
||||
except SystemError:
|
||||
pass
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
d = {}
|
||||
if clear_current:
|
||||
self.clear()
|
||||
self.update(d)
|
||||
|
||||
def has_key(self, key):
|
||||
return dict.__contains__(self, key)
|
||||
|
||||
def set(self, key, val):
|
||||
self.__setitem__(key, val)
|
||||
|
||||
def __delitem__(self, key):
|
||||
try:
|
||||
dict.__delitem__(self, key)
|
||||
except KeyError:
|
||||
pass # ignore missing keys
|
||||
else:
|
||||
self.commit()
|
||||
|
||||
def commit(self):
|
||||
if self.no_commit:
|
||||
return
|
||||
if hasattr(self, 'file_path') and self.file_path:
|
||||
dpath = os.path.dirname(self.file_path)
|
||||
if not os.path.exists(dpath):
|
||||
os.makedirs(dpath, mode=CONFIG_DIR_MODE)
|
||||
with open(self.file_path, "w") as f:
|
||||
raw = self.to_raw()
|
||||
f.seek(0)
|
||||
f.truncate()
|
||||
f.write(raw)
|
||||
|
||||
def __enter__(self):
|
||||
self.no_commit = True
|
||||
|
||||
def __exit__(self, *args):
|
||||
self.no_commit = False
|
||||
self.commit()
|
||||
|
||||
def raw_to_object(self, raw):
|
||||
return json.loads(raw)
|
||||
|
||||
def to_raw(self):
|
||||
return json.dumps(self, ensure_ascii=False)
|
||||
|
||||
def __getitem__(self, key):
|
||||
try:
|
||||
return dict.__getitem__(self, key)
|
||||
except KeyError:
|
||||
return self.defaults[key]
|
||||
|
||||
def get(self, key, default=None):
|
||||
try:
|
||||
return dict.__getitem__(self, key)
|
||||
except KeyError:
|
||||
return self.defaults.get(key, default)
|
||||
|
||||
def __setitem__(self, key, val):
|
||||
dict.__setitem__(self, key, val)
|
||||
self.commit()
|
||||
122
DeDRM_plugin/standalone/passhash.py
Normal file
122
DeDRM_plugin/standalone/passhash.py
Normal file
@@ -0,0 +1,122 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# CLI interface for the DeDRM plugin (useable without Calibre, too)
|
||||
# Adobe PassHash implementation
|
||||
|
||||
from __future__ import absolute_import, print_function
|
||||
|
||||
# Copyright © 2021 NoDRM
|
||||
|
||||
#@@CALIBRE_COMPAT_CODE@@
|
||||
|
||||
import os, sys
|
||||
|
||||
from standalone.__init__ import print_opt, print_std_usage
|
||||
|
||||
iswindows = sys.platform.startswith('win')
|
||||
isosx = sys.platform.startswith('darwin')
|
||||
|
||||
def print_passhash_help():
|
||||
from __version import PLUGIN_NAME, PLUGIN_VERSION
|
||||
print(PLUGIN_NAME + " v" + PLUGIN_VERSION + " - Calibre DRM removal plugin by noDRM")
|
||||
print()
|
||||
print("passhash: Manage Adobe PassHashes")
|
||||
print()
|
||||
print_std_usage("passhash", "[ -u username -p password | -b base64str ] [ -i ] ")
|
||||
|
||||
print()
|
||||
print("Options: ")
|
||||
print_opt("u", "username", "Generate a PassHash with the given username")
|
||||
print_opt("p", "password", "Generate a PassHash with the given password")
|
||||
print_opt("e", "extract", "Display PassHashes found on this machine")
|
||||
print_opt("i", "import", "Import hashes into the JSON config file")
|
||||
|
||||
def perform_action(params, files):
|
||||
user = None
|
||||
pwd = None
|
||||
|
||||
if len(params) == 0:
|
||||
print_passhash_help()
|
||||
return 0
|
||||
|
||||
extract = False
|
||||
import_to_json = True
|
||||
|
||||
while len(params) > 0:
|
||||
p = params.pop(0)
|
||||
if p == "--username":
|
||||
user = params.pop(0)
|
||||
elif p == "--password":
|
||||
pwd = params.pop(0)
|
||||
elif p == "--extract":
|
||||
extract = True
|
||||
elif p == "--help":
|
||||
print_passhash_help()
|
||||
return 0
|
||||
elif p == "--import":
|
||||
import_to_json = True
|
||||
|
||||
if not extract and not import_to_json:
|
||||
if user is None:
|
||||
print("Missing parameter: --username", file=sys.stderr)
|
||||
if pwd is None:
|
||||
print("Missing parameter: --password", file=sys.stderr)
|
||||
if user is None or pwd is None:
|
||||
return 1
|
||||
|
||||
if user is None and pwd is not None:
|
||||
print("Parameter --password also requires --username", file=sys.stderr)
|
||||
return 1
|
||||
if user is not None and pwd is None:
|
||||
print("Parameter --username also requires --password", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
if user is not None and pwd is not None:
|
||||
from ignoblekeyGenPassHash import generate_key
|
||||
key = generate_key(user, pwd)
|
||||
if import_to_json:
|
||||
# TODO: Import the key to the JSON
|
||||
pass
|
||||
|
||||
print(key.decode("utf-8"))
|
||||
|
||||
if extract or import_to_json:
|
||||
if not iswindows and not isosx:
|
||||
print("Extracting PassHash keys not supported on Linux.", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
keys = []
|
||||
|
||||
from ignoblekeyNookStudy import nookkeys
|
||||
keys.extend(nookkeys())
|
||||
|
||||
if iswindows:
|
||||
from ignoblekeyWindowsStore import dump_keys
|
||||
keys.extend(dump_keys())
|
||||
|
||||
from adobekey_get_passhash import passhash_keys
|
||||
ade_keys, ade_names = passhash_keys()
|
||||
keys.extend(ade_keys)
|
||||
|
||||
# Trim duplicates
|
||||
newkeys = []
|
||||
for k in keys:
|
||||
if not k in newkeys:
|
||||
newkeys.append(k)
|
||||
|
||||
# Print all found keys
|
||||
for k in newkeys:
|
||||
if import_to_json:
|
||||
# TODO: Add keys to json
|
||||
pass
|
||||
|
||||
if extract:
|
||||
print(k)
|
||||
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("This code is not intended to be executed directly!", file=sys.stderr)
|
||||
209
DeDRM_plugin/standalone/remove_drm.py
Normal file
209
DeDRM_plugin/standalone/remove_drm.py
Normal file
@@ -0,0 +1,209 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# CLI interface for the DeDRM plugin (useable without Calibre, too)
|
||||
# DRM removal
|
||||
|
||||
from __future__ import absolute_import, print_function
|
||||
|
||||
# Copyright © 2021 NoDRM
|
||||
|
||||
#@@CALIBRE_COMPAT_CODE@@
|
||||
|
||||
import os, sys
|
||||
|
||||
from zipfile import ZipInfo, ZipFile, ZIP_STORED, ZIP_DEFLATED
|
||||
from contextlib import closing
|
||||
|
||||
from standalone.__init__ import print_opt, print_std_usage
|
||||
|
||||
iswindows = sys.platform.startswith('win')
|
||||
isosx = sys.platform.startswith('darwin')
|
||||
|
||||
def print_removedrm_help():
|
||||
from __init__ import PLUGIN_NAME, PLUGIN_VERSION
|
||||
print(PLUGIN_NAME + " v" + PLUGIN_VERSION + " - Calibre DRM removal plugin by noDRM")
|
||||
print()
|
||||
print("remove_drm: Remove DRM from one or multiple files")
|
||||
print()
|
||||
print_std_usage("remove_drm", "<filename> ... [ -o <filename> ] [ -f ]")
|
||||
|
||||
print()
|
||||
print("Options: ")
|
||||
print_opt(None, "outputdir", "Folder to export the file(s) to")
|
||||
print_opt("o", "output", "File name to export the file to")
|
||||
print_opt("f", "force", "Overwrite output file if it already exists")
|
||||
print_opt(None, "overwrite", "Replace DRMed file with DRM-free file (implies --force)")
|
||||
|
||||
|
||||
def determine_file_type(file):
|
||||
# Returns a file type:
|
||||
# "PDF", "PDB", "MOBI", "TPZ", "LCP", "ADEPT", "ADEPT-PassHash", "KFX-ZIP", "ZIP" or None
|
||||
|
||||
f = open(file, "rb")
|
||||
fdata = f.read(100)
|
||||
f.close()
|
||||
|
||||
if fdata.startswith(b"PK\x03\x04"):
|
||||
pass
|
||||
# Either LCP, Adobe, or Amazon
|
||||
elif fdata.startswith(b"%PDF"):
|
||||
return "PDF"
|
||||
elif fdata[0x3c:0x3c+8] == b"PNRdPPrs" or fdata[0x3c:0x3c+8] == b"PDctPPrs":
|
||||
return "PDB"
|
||||
elif fdata[0x3c:0x3c+8] == b"BOOKMOBI" or fdata[0x3c:0x3c+8] == b"TEXtREAd":
|
||||
return "MOBI"
|
||||
elif fdata.startswith(b"TPZ"):
|
||||
return "TPZ"
|
||||
else:
|
||||
return None
|
||||
# Unknown file type
|
||||
|
||||
|
||||
# If it's a ZIP, determine the type.
|
||||
|
||||
from lcpdedrm import isLCPbook
|
||||
if isLCPbook(file):
|
||||
return "LCP"
|
||||
|
||||
from ineptepub import adeptBook, isPassHashBook
|
||||
if adeptBook(file):
|
||||
if isPassHashBook(file):
|
||||
return "ADEPT-PassHash"
|
||||
else:
|
||||
return "ADEPT"
|
||||
|
||||
try:
|
||||
# Amazon / KFX-ZIP has a file that starts with b'\xeaDRMION\xee' in the ZIP.
|
||||
with closing(ZipFile(open(file, "rb"))) as book:
|
||||
for subfilename in book.namelist():
|
||||
with book.open(subfilename) as subfile:
|
||||
data = subfile.read(8)
|
||||
if data == b'\xeaDRMION\xee':
|
||||
return "KFX-ZIP"
|
||||
except:
|
||||
pass
|
||||
|
||||
return "ZIP"
|
||||
|
||||
|
||||
|
||||
|
||||
def dedrm_single_file(input_file, output_file):
|
||||
# When this runs, all the stupid file handling is done.
|
||||
# Just take the file at the absolute path "input_file"
|
||||
# and export it, DRM-free, to "output_file".
|
||||
|
||||
# Use a temp file as input_file and output_file
|
||||
# might be identical.
|
||||
|
||||
# The output directory might not exist yet.
|
||||
|
||||
print("File " + input_file + " to " + output_file)
|
||||
|
||||
# Okay, first check the file type and don't rely on the extension.
|
||||
try:
|
||||
ftype = determine_file_type(input_file)
|
||||
except:
|
||||
print("Can't determine file type for this file.")
|
||||
ftype = None
|
||||
|
||||
if ftype is None:
|
||||
return
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def perform_action(params, files):
|
||||
output = None
|
||||
outputdir = None
|
||||
force = False
|
||||
overwrite_original = False
|
||||
|
||||
|
||||
if len(files) == 0:
|
||||
print_removedrm_help()
|
||||
return 0
|
||||
|
||||
while len(params) > 0:
|
||||
p = params.pop(0)
|
||||
if p == "--output":
|
||||
output = params.pop(0)
|
||||
elif p == "--outputdir":
|
||||
outputdir = params.pop(0)
|
||||
elif p == "--force":
|
||||
force = True
|
||||
elif p == "--overwrite":
|
||||
overwrite_original = True
|
||||
force = True
|
||||
elif p == "--help":
|
||||
print_removedrm_help()
|
||||
return 0
|
||||
|
||||
if overwrite_original and (output is not None or outputdir is not None):
|
||||
print("Can't use --overwrite together with --output or --outputdir.", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
if output is not None and os.path.isfile(output) and not force:
|
||||
print("Output file already exists. Use --force to overwrite.", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
if output is not None and len(files) > 1:
|
||||
print("Cannot set output file name if there's multiple input files.", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
if outputdir is not None and output is not None and os.path.isabs(output):
|
||||
print("--output parameter is absolute path despite --outputdir being set.", file=sys.stderr)
|
||||
print("Remove --outputdir, or give a relative path to --output.", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
|
||||
for file in files:
|
||||
|
||||
file = os.path.abspath(file)
|
||||
|
||||
if not os.path.isfile(file):
|
||||
print("Skipping file " + file + " - not found.", file=sys.stderr)
|
||||
continue
|
||||
|
||||
if overwrite_original:
|
||||
output_filename = file
|
||||
else:
|
||||
if output is not None:
|
||||
# Due to the check above, we DO only have one file here.
|
||||
if outputdir is not None and not os.path.isabs(output):
|
||||
output_filename = os.path.join(outputdir, output)
|
||||
else:
|
||||
output_filename = os.path.abspath(output)
|
||||
else:
|
||||
if outputdir is None:
|
||||
outputdir = os.getcwd()
|
||||
output_filename = os.path.join(outputdir, os.path.basename(file))
|
||||
output_filename = os.path.abspath(output_filename)
|
||||
|
||||
if output_filename == file:
|
||||
# If we export to the import folder, add a suffix to the file name.
|
||||
fn, f_ext = os.path.splitext(output_filename)
|
||||
output_filename = fn + "_nodrm" + f_ext
|
||||
|
||||
|
||||
|
||||
if os.path.isfile(output_filename) and not force:
|
||||
print("Skipping file " + file + " because output file already exists (use --force).", file=sys.stderr)
|
||||
continue
|
||||
|
||||
|
||||
|
||||
dedrm_single_file(file, output_filename)
|
||||
|
||||
|
||||
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("This code is not intended to be executed directly!", file=sys.stderr)
|
||||
@@ -13,14 +13,16 @@ __version__ = '6.0'
|
||||
|
||||
import sys
|
||||
import os, csv, getopt
|
||||
|
||||
#@@CALIBRE_COMPAT_CODE@@
|
||||
|
||||
|
||||
import zlib, zipfile, tempfile, shutil
|
||||
import traceback
|
||||
from struct import pack
|
||||
from struct import unpack
|
||||
try:
|
||||
from calibre_plugins.dedrm.alfcrypto import Topaz_Cipher
|
||||
except:
|
||||
from alfcrypto import Topaz_Cipher
|
||||
|
||||
from alfcrypto import Topaz_Cipher
|
||||
|
||||
# Wrap a stream so that output gets flushed immediately
|
||||
# and also make sure that any unicode strings get
|
||||
@@ -88,12 +90,7 @@ def unicode_argv():
|
||||
#global switch
|
||||
debug = False
|
||||
|
||||
if 'calibre' in sys.modules:
|
||||
inCalibre = True
|
||||
from calibre_plugins.dedrm import kgenpids
|
||||
else:
|
||||
inCalibre = False
|
||||
import kgenpids
|
||||
import kgenpids
|
||||
|
||||
|
||||
class DrmException(Exception):
|
||||
@@ -178,6 +175,8 @@ def decryptRecord(data,PID):
|
||||
# Try to decrypt a dkey record (contains the bookPID)
|
||||
def decryptDkeyRecord(data,PID):
|
||||
record = decryptRecord(data,PID)
|
||||
if isinstance(record, str):
|
||||
record = record.encode('latin-1')
|
||||
fields = unpack('3sB8sB8s3s',record)
|
||||
if fields[0] != b'PID' or fields[5] != b'pid' :
|
||||
raise DrmException("Didn't find PID magic numbers in record")
|
||||
@@ -321,6 +320,8 @@ class TopazBook:
|
||||
raise DrmException("Error: Attempt to decrypt without bookKey")
|
||||
|
||||
if compressed:
|
||||
if isinstance(record, str):
|
||||
record = bytes(record, 'latin-1')
|
||||
record = zlib.decompress(record)
|
||||
|
||||
return record
|
||||
@@ -332,14 +333,11 @@ class TopazBook:
|
||||
keydata = self.getBookPayloadRecord(b'dkey', 0)
|
||||
except DrmException as e:
|
||||
print("no dkey record found, book may not be encrypted")
|
||||
print("attempting to extrct files without a book key")
|
||||
print("attempting to extract files without a book key")
|
||||
self.createBookDirectory()
|
||||
self.extractFiles()
|
||||
print("Successfully Extracted Topaz contents")
|
||||
if inCalibre:
|
||||
from calibre_plugins.dedrm import genbook
|
||||
else:
|
||||
import genbook
|
||||
import genbook
|
||||
|
||||
rv = genbook.generateBook(self.outdir, raw, fixedimage)
|
||||
if rv == 0:
|
||||
@@ -351,6 +349,8 @@ class TopazBook:
|
||||
for pid in pidlst:
|
||||
# use 8 digit pids here
|
||||
pid = pid[0:8]
|
||||
if isinstance(pid, str):
|
||||
pid = pid.encode('latin-1')
|
||||
print("Trying: {0}".format(pid))
|
||||
bookKeys = []
|
||||
data = keydata
|
||||
@@ -364,16 +364,13 @@ class TopazBook:
|
||||
break
|
||||
|
||||
if not bookKey:
|
||||
raise DrmException("No key found in {0:d} keys tried. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(len(pidlst)))
|
||||
raise DrmException("No key found in {0:d} keys tried. Read the FAQs at noDRM's repository: https://github.com/noDRM/DeDRM_tools/blob/master/FAQs.md".format(len(pidlst)))
|
||||
|
||||
self.setBookKey(bookKey)
|
||||
self.createBookDirectory()
|
||||
self.extractFiles()
|
||||
print("Successfully Extracted Topaz contents")
|
||||
if inCalibre:
|
||||
from calibre_plugins.dedrm import genbook
|
||||
else:
|
||||
import genbook
|
||||
import genbook
|
||||
|
||||
rv = genbook.generateBook(self.outdir, raw, fixedimage)
|
||||
if rv == 0:
|
||||
@@ -421,6 +418,8 @@ class TopazBook:
|
||||
outputFile = os.path.join(destdir,fname)
|
||||
print(".", end=' ')
|
||||
record = self.getBookPayloadRecord(name,index)
|
||||
if isinstance(record, str):
|
||||
record=bytes(record, 'latin-1')
|
||||
if record != b'':
|
||||
open(outputFile, 'wb').write(record)
|
||||
print(" ")
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from calibre_plugins.dedrm.ignoblekeygen import generate_key
|
||||
#@@CALIBRE_COMPAT_CODE@@
|
||||
|
||||
from ignoblekeyGenPassHash import generate_key
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
|
||||
|
||||
@@ -5,7 +5,10 @@ __license__ = 'GPL v3'
|
||||
|
||||
# Standard Python modules.
|
||||
import os, sys, re, hashlib, traceback
|
||||
from calibre_plugins.dedrm.__init__ import PLUGIN_NAME, PLUGIN_VERSION
|
||||
|
||||
#@@CALIBRE_COMPAT_CODE@@
|
||||
|
||||
from __init__ import PLUGIN_NAME, PLUGIN_VERSION
|
||||
|
||||
|
||||
class NoWinePython3Exception(Exception):
|
||||
|
||||
@@ -20,14 +20,12 @@ Re-write zip (or ePub) fixing problems with file names (and mimetype entry).
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = "1.1"
|
||||
|
||||
import sys
|
||||
import sys, os
|
||||
|
||||
#@@CALIBRE_COMPAT_CODE@@
|
||||
|
||||
import zlib
|
||||
try:
|
||||
import zipfilerugged
|
||||
except:
|
||||
import calibre_plugins.dedrm.zipfilerugged as zipfilerugged
|
||||
import os
|
||||
import os.path
|
||||
import zipfilerugged
|
||||
import getopt
|
||||
from struct import unpack
|
||||
|
||||
|
||||
18
FAQs.md
18
FAQs.md
@@ -153,21 +153,26 @@ Remove the DRMed book from calibre. Click the Preferences drop-down menu and cho
|
||||
## Is there a way to use the DeDRM plugin for Calibre from the command line?
|
||||
See the [Calibre command line interface (CLI) instructions](CALIBRE_CLI_INSTRUCTIONS.md).
|
||||
|
||||
## The plugin displays a "MemoryError" in its log file during DRM removal.
|
||||
A "MemoryError" usually occurs when you're using the 32-bit version of Calibre (which is limited in the amount of useable RAM). If you have a 64-bit installation of your operating system (on Windows, press Windows+Break, then make sure it says "64-bit Operating System" under "System type"), try downloading the 64-bit version of Calibre instead of the 32-bit version.
|
||||
|
||||
If the error still occurs, even with the 64-bit version, please open a bug report.
|
||||
|
||||
# General Questions
|
||||
|
||||
## Once the DRM has been removed, is there any trace of my personal identity left in the ebook?
|
||||
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.
|
||||
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 (optionally, there's an option in the plugin settings to enable that), but that will not be the case for all watermarks.
|
||||
|
||||
## Why do some of my Kindle ebooks import as HTMLZ format in calibre?
|
||||
Most Amazon Kindle ebooks are Mobipocket format ebooks, or the new KF8 format. However, some are in a format known as Topaz. The Topaz format is only used by Amazon. A Topaz ebook is a collections of glyphs and their positions on each page tagged with some additional information from that page including OCRed text (Optical Character Recognition generated Text) to allow searching, and some additional layout information. Each page of a Topaz ebook is effectively a description of an image of that page. To convert a Topaz ebook to another format is not easy as there is not a one-to-one mapping between glyphs and characters/fonts. To account for this, two different formats are generated by the DRM removal software. The first is an html description built from the OCRtext and images stored in the Topaz file (HTMLZ). This format is easily reflowed but may suffer from typical OCRtext errors including typos, garbled text, missing italics, missing bolds, etc. The second format uses the glyph and position information to create an accurate scalable vector graphics (SVG) image of each page of the book that can be viewed in web browsers that support svg images (Safari, Firefox 4 or later, etc). Additional conversion software can be used to convert these SVG images to an image only PDF file. The DeDRM calibre plugin only imports the HTMLZ versions of the Topaz ebook. The html version can be manually cleaned up and spell checked and then converted using Sigil/calibre to epubs, mobi ebooks, and etc.
|
||||
|
||||
## Are the tools open source? How can I be sure they are safe and not a trojan horse?
|
||||
All the DRM removal tools hosted here are almost entirely scripts of one kind or another: Python, Applescript or Windows Batch files. So they are inherently open source, and open to inspection by everyone who downloads them.
|
||||
All the DRM removal tools hosted here are almost entirely written in Python. So they are inherently open source, and open to inspection by everyone who downloads 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.
|
||||
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 (there's fallback code in the plugin that allows it to work without these, it will just be slower) or manually rebuild them from source.
|
||||
|
||||
## What ebooks do these tools work on?
|
||||
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.
|
||||
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, Amazon, Kobo and eReader DRM schemes. It used to remove Readium LCP DRM from ePub or PDF files in the past, but that functionality had to be removed due to a [DMCA takedown request](https://github.com/noDRM/DeDRM_tools/issues/18).
|
||||
|
||||
Note these tools do NOT ‘crack’ the DRM. They simply allow the book’s owner to use the encryption key information already stored someplace on their computer or device to decrypt the ebook in the same manner the official ebook reading software uses.
|
||||
|
||||
@@ -183,6 +188,9 @@ Amazon turned off backup for Kindle for Android, so the tools can no longer find
|
||||
## Why don't the tools work on books from the Apple iBooks Store?
|
||||
Apple regularly change the details of their DRM and so the tools in the main tools archive will not work with these ebooks. Apple’s Fairplay DRM scheme can be removed using Requiem if the appropriate version of iTunes can still be installed and used. See the post Apple and ebooks: iBookstore DRM and how to remove it at Apprentice Alf's blog for more details.
|
||||
|
||||
## Why don't the tools work with LCP-encrypted ebooks? / Error message about a "DMCA takedown"
|
||||
Support for LCP DRM removal was included in the past, but Readium (the company who developed that particular DRM) has decided to [open a DMCA takedown request](https://github.com/github/dmca/blob/master/2022/01/2022-01-04-readium.md) in January 2022. This means that for legal reasons, this GitHun repository no longer contains the code needed to remove DRM from LCP-encrypted books. For more information please read [this bug report](https://github.com/noDRM/DeDRM_tools/issues/18).
|
||||
|
||||
## I’ve got the tools archive and I’ve read all the FAQs but I still can’t install the tools and/or the DRM removal doesn’t work
|
||||
* Read the `ReadMe_Overview.txt` file in the top level of the tools archive
|
||||
* Read the ReadMe file for the tool you want to use.
|
||||
@@ -191,7 +199,7 @@ Apple regularly change the details of their DRM and so the tools in the main too
|
||||
## Who wrote these scripts?
|
||||
The authors tend to identify themselves only by pseudonyms:
|
||||
* The Adobe Adept and Barnes & Noble scripts were created by i♥cabbages
|
||||
* The Readium LCP support for this plugin was created by NoDRM
|
||||
* ~The Readium LCP support for this plugin was created by NoDRM~ (removed due to a DMCA takedown, see [#18](https://github.com/noDRM/DeDRM_tools/issues/18) )
|
||||
* The Amazon Mobipocket and eReader scripts were created by The Dark Reverser
|
||||
* The Amazon 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
|
||||
|
||||
@@ -118,7 +118,13 @@ class ManageKeysDialog(QDialog):
|
||||
self._delete_key_button.clicked.connect(self.delete_key)
|
||||
button_layout.addWidget(self._delete_key_button)
|
||||
|
||||
spacerItem = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
|
||||
try:
|
||||
# QT 6
|
||||
spacerItem = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Policy.Minimum, QtGui.QSizePolicy.Policy.Expanding)
|
||||
except AttributeError:
|
||||
# QT 5
|
||||
spacerItem = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
|
||||
|
||||
button_layout.addItem(spacerItem)
|
||||
|
||||
layout.addSpacing(5)
|
||||
|
||||
@@ -788,7 +788,7 @@ def cli_main():
|
||||
books = [lib.books[num - 1]]
|
||||
except (ValueError, IndexError):
|
||||
print("Invalid choice. Exiting...")
|
||||
exit()
|
||||
sys.exit()
|
||||
|
||||
results = [decrypt_book(book, lib) for book in books]
|
||||
lib.close()
|
||||
|
||||
@@ -739,7 +739,7 @@ def cli_main():
|
||||
books = [lib.books[num - 1]]
|
||||
except (ValueError, IndexError):
|
||||
print("Invalid choice. Exiting...")
|
||||
exit()
|
||||
sys.exit()
|
||||
|
||||
results = [decrypt_book(book, lib) for book in books]
|
||||
lib.close()
|
||||
|
||||
@@ -55,7 +55,7 @@ It may be possible to use the plugins on a Linux system, but no support is given
|
||||
Credits
|
||||
-------
|
||||
The original inept and ignoble scripts were by i♥cabbages
|
||||
The original Readium LCP DRM removal by NoDRM
|
||||
~The original Readium LCP DRM removal by NoDRM~ (removed due to a DMCA request)
|
||||
The original mobidedrm and erdr2pml scripts were by The Dark Reverser
|
||||
The original topaz DRM removal script was by CMBDTC
|
||||
The original topaz format conversion scripts were by some_updates, clarknova and Bart Simpson
|
||||
|
||||
@@ -14,19 +14,65 @@ import shutil
|
||||
|
||||
|
||||
DEDRM_SRC_DIR = 'DeDRM_plugin'
|
||||
DEDRM_SRC_TMP_DIR = 'DeDRM_plugin_temp'
|
||||
DEDRM_README= 'DeDRM_plugin_ReadMe.txt'
|
||||
OBOK_SRC_DIR = 'Obok_plugin'
|
||||
OBOK_README = 'obok_plugin_ReadMe.txt'
|
||||
RELEASE_DIR = 'release'
|
||||
|
||||
def patch_file(filepath):
|
||||
f = open(filepath, "rb")
|
||||
fn = open(filepath + ".tmp", "wb")
|
||||
patch = open(os.path.join(DEDRM_SRC_DIR, "__calibre_compat_code.py"), "rb")
|
||||
patchdata = patch.read()
|
||||
patch.close()
|
||||
|
||||
while True:
|
||||
line = f.readline()
|
||||
if len(line) == 0:
|
||||
break
|
||||
|
||||
if line.strip().startswith(b"#@@CALIBRE_COMPAT_CODE@@"):
|
||||
fn.write(patchdata)
|
||||
else:
|
||||
fn.write(line)
|
||||
|
||||
f.close()
|
||||
fn.close()
|
||||
shutil.move(filepath + ".tmp", filepath)
|
||||
|
||||
|
||||
|
||||
def make_release(version):
|
||||
try:
|
||||
shutil.rmtree(RELEASE_DIR)
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
shutil.rmtree(DEDRM_SRC_TMP_DIR)
|
||||
except:
|
||||
pass
|
||||
|
||||
os.mkdir(RELEASE_DIR)
|
||||
shutil.make_archive(DEDRM_SRC_DIR, 'zip', DEDRM_SRC_DIR)
|
||||
|
||||
# Copy folder
|
||||
shutil.copytree(DEDRM_SRC_DIR, DEDRM_SRC_TMP_DIR)
|
||||
|
||||
# Modify folder
|
||||
try:
|
||||
shutil.rmtree(os.path.join(os.path.abspath(DEDRM_SRC_TMP_DIR), "__pycache__"))
|
||||
except:
|
||||
pass
|
||||
|
||||
# Patch file to add compat code.
|
||||
for root, dirs, files in os.walk(DEDRM_SRC_TMP_DIR):
|
||||
for name in files:
|
||||
if name.endswith(".py"):
|
||||
patch_file(os.path.join(root, name))
|
||||
|
||||
|
||||
# Package
|
||||
shutil.make_archive(DEDRM_SRC_DIR, 'zip', DEDRM_SRC_TMP_DIR)
|
||||
shutil.make_archive(OBOK_SRC_DIR, 'zip', OBOK_SRC_DIR)
|
||||
shutil.move(DEDRM_SRC_DIR+'.zip', RELEASE_DIR)
|
||||
shutil.move(OBOK_SRC_DIR+'.zip', RELEASE_DIR)
|
||||
@@ -34,7 +80,13 @@ def make_release(version):
|
||||
shutil.copy(OBOK_README, RELEASE_DIR)
|
||||
shutil.copy("ReadMe_Overview.txt", RELEASE_DIR)
|
||||
|
||||
release_name = 'DeDRM_tools_{}'.format(version)
|
||||
# Remove temp folder:
|
||||
shutil.rmtree(DEDRM_SRC_TMP_DIR)
|
||||
|
||||
if version is not None:
|
||||
release_name = 'DeDRM_tools_{}'.format(version)
|
||||
else:
|
||||
release_name = 'DeDRM_tools'
|
||||
result = shutil.make_archive(release_name, 'zip', RELEASE_DIR)
|
||||
try:
|
||||
shutil.rmtree(RELEASE_DIR)
|
||||
@@ -48,6 +100,6 @@ if __name__ == '__main__':
|
||||
try:
|
||||
version = sys.argv[1]
|
||||
except IndexError:
|
||||
raise SystemExit('Usage: {} version'.format(__file__))
|
||||
version = None
|
||||
|
||||
print(make_release(version))
|
||||
|
||||
Reference in New Issue
Block a user