Compare commits

...

373 commits

Author SHA1 Message Date
NoDRM
7379b45319 Remove future import from ion.py 2024-11-10 20:15:33 +01:00
NoDRM
bde82fd7ab Fix python2 support for ion.py 2024-11-10 16:10:29 +01:00
NoDRM
de3d91f5e5 Don't repack EPUB if nothing has changed 2024-11-10 15:21:09 +01:00
NoDRM
c5ee327a60 Add note about key import/export for K4PC in the help file (fixes #663) 2024-11-10 14:44:57 +01:00
NoDRM
501a1e6d31 Update obok readme to include wmic requirement (fixes #670) 2024-11-10 14:41:38 +01:00
NoDRM
815d86efe0 Update changelog 2024-11-10 14:36:27 +01:00
NoDRM
65646f4493 Fix CI 2024-11-10 14:25:35 +01:00
Josh Cotton
808dc7d29a
Fix Obok import in Calibre flatpak by using /sys/class/net/IFACE/address instead of ip (#586)
Fix #585.
Use /sys/class/net/IFACE/address for the MAC address instead of the ip
command.
2024-11-10 13:14:59 +00:00
precondition
2cd2792306 Obok.py/action.py: invoke _() only once 2024-11-10 13:11:28 +00:00
precondition
2e53d70e88 Catch FileNotFoundError due to undownloaded ebooks 2024-11-10 13:11:28 +00:00
Ben Combee
05fff5217b Fix crash using bare sha1 symbol
Use sha1 from hashlib, as it isn't imported globally, fixed crash trying to decrypt a eReader PDB file
2024-11-10 13:10:11 +00:00
Martin Rys
34c4c067e8 DeDRM ion: Correctly throw last exception if decrypt fails 2024-11-10 13:09:45 +00:00
Martin Rys
195ea69537 DeDRM ion: Clean out errorneous whitespace and UTF8 definition from python 2 times 2024-11-10 13:09:45 +00:00
NoDRM
3373d93874 Add binascii import, fixes FileOpen #514 2024-03-15 13:13:45 +01:00
NoDRM
bf2471e65b Update kfxdedrm as suggested in #440 2023-12-21 12:35:11 +01:00
NoDRM
5492dcdbf4 More FileOpen fixes 2023-12-21 11:57:39 +01:00
NoDRM
737d5e7f1e Bunch of updates for the FileOpen script 2023-12-03 10:45:09 +01:00
NoDRM
e4e5808894 Fix file lock issue in androidkindlekey.py 2023-12-03 10:42:41 +01:00
NoDRM
ef67dbd204 Fix more Py2/Py3 stuff 2023-08-06 15:49:52 +02:00
NoDRM
10b6caf9f5 Enable autorelease into 2nd repo 2023-08-03 21:53:16 +02:00
NoDRM
53996cf49c More Python2 fixes 2023-08-03 20:45:06 +02:00
NoDRM
d388ae72fd More Py2 fixes 2023-08-03 20:14:33 +02:00
NoDRM
bc089ee46d More Python2 bugfixes 2023-08-03 20:01:38 +02:00
NoDRM
e509b7d520 Fix python2 issues in kgenpids and kindlekey 2023-08-03 11:26:05 +02:00
NoDRM
e82d2b5c9c Fix PDF decryption for 256-bit AES with V=5 2023-08-02 18:13:42 +02:00
NoDRM
7f6dd84389 Fix PDF decryption of ancient 40-bit RC4 with R=2 2023-08-02 16:55:41 +02:00
NoDRM
b9bad26d4b Prepare release candidate v10.0.9 2023-08-02 07:39:35 +02:00
NoDRM
2a1413297e Add warning to the standalone code 2023-08-02 07:30:39 +02:00
NoDRM
815f880e34 Disable auto-prerelease again (#358) 2023-06-25 18:51:46 +02:00
NoDRM
9ae77c438f Update CI to create an automatic beta release 2023-06-25 18:21:20 +02:00
Satsuoni
abc5de018e Added several more scramble functions to Kindle decrypt 2023-06-25 16:38:55 +02:00
NoDRM
133e67fa03 Added fix for padding being correct on accident
Co-authored-by: Satsuoni <satsuoni@hotmail.com>
2023-06-25 16:27:31 +02:00
NoDRM
f86cff285b Fix python2 issues in Kindle and Nook code (#355) 2023-06-24 09:53:55 +02:00
NoDRM
a553a71f45 Fix font decryption with multiple IDs (#347) 2023-06-23 19:44:24 +02:00
NoDRM
740b46546f Try to add support for new K4PC
Co-authored-by: Andrew Innes <andrew.c12@gmail.com>
Co-authored-by: Satsuoni <satsuoni@hotmail.com>
2023-06-23 19:30:06 +02:00
NoDRM
fb8b003444 Support for Adobe's 'aes128-cbc-uncompressed' encryption (see #242) 2023-01-06 14:32:25 +01:00
NoDRM
3c12806f38 Fix issue with remaining data in encryption.xml 2023-01-06 14:29:56 +01:00
NoDRM
3151dbbd98 Try fixing a Python2 bug in the Obok plugin (#235) 2022-12-29 19:58:29 +01:00
NoDRM
08e7ac79ca Update CHANGELOG 2022-12-29 19:53:59 +01:00
NoDRM
a711954323 PDF: Ignore invalid objid in non-strict mode, fixes #233 2022-12-29 19:52:08 +01:00
NoDRM
a30405bebf Fix Python3 bug in stylexml2css.py, fixes #232 2022-12-23 10:44:45 +01:00
NoDRM
901a6c091d Fix exception in error logging in ineptpdf 2022-12-23 10:42:25 +01:00
NoDRM
e16748e854 Untested code for the Obok plugin to allow adding duplicate books.
See #148
2022-10-19 17:14:26 +02:00
NoDRM
06df18bea3 Strip whitespace from Kindle serials (#158) 2022-10-19 16:39:39 +02:00
NoDRM
06648eeb1c Add support for empty arrays (<>) in PDF objects. Fixes #183. 2022-10-17 17:13:41 +02:00
NoDRM
6c8051eded Update changelog 2022-09-10 11:57:35 +02:00
NoDRM
1cc245b103 Update README, fixes #136 2022-09-10 11:47:15 +02:00
NoDRM
eb45c71fd9 Cleanup 2022-09-10 11:44:55 +02:00
NoDRM
2d4c5d2c4b Fix key import sometimes generating corrupted keys.
Should fix #145, #134, #119, #116, #115, #109 and maybe others.
2022-09-10 11:42:59 +02:00
Roland W-H
21281baf21 fix 2 spelling errors in FAQs.md 2022-08-10 04:46:42 +00:00
NoDRM
88b0966961 Fix tons of PDF-related issues 2022-08-07 15:58:01 +02:00
NoDRM
52cf3faa59 Fix DeACSM import for PDF files 2022-08-07 09:31:49 +02:00
NoDRM
b12e567c5f Cleanup / SafeUnbuffered bugfix 2022-08-07 09:30:24 +02:00
NoDRM
ca6d30b2d9 More stuff I missed 2022-08-06 20:25:07 +02:00
NoDRM
dfa247bf88 Cleanup 2022-08-06 20:19:36 +02:00
NoDRM
a0bb84fbfc Move unicode_argv to its own file 2022-08-06 20:19:18 +02:00
NoDRM
410e086d08 Remove AlfCrypto libraries and perform everything in Python
The old AlfCrypto DLL, SO and DYLIB files are ancient,
I don't have the systems to recompile them all, they
cause issues on ARM Macs, and I doubt with all the Python
improvements over the last years that they have a significant
performance advantage. And even if that's the case, nobody is
importing hundreds of DRM books at the same time so it shouldn't
hurt if some decryptions might take a bit longer.
2022-08-06 20:13:19 +02:00
NoDRM
9276d77f63 Couple Python 2 fixes in (unsupported) standalone scripts 2022-08-06 20:10:51 +02:00
NoDRM
de23b5c221 Move SafeUnbuffered to own Python file 2022-08-06 20:09:30 +02:00
NoDRM
b404605878 Another Python2 Bugfix for Obok 2022-08-06 19:57:20 +02:00
NoDRM
1cc5d383cc Delete unused files 2022-08-06 19:56:18 +02:00
NoDRM
41df9ecda0 Fix PDF corruption in Calibre 4 (#104) 2022-08-06 15:29:45 +02:00
NoDRM
80cbaa4841 Fix ZIP attribute "external_attr" getting reset 2022-08-06 13:53:03 +02:00
NoDRM
9a11f480b5 Fix plugin crash with invalid ADE key 2022-08-03 19:49:20 +02:00
NoDRM
59839ae5c7 Fix Calibre 6 issue in Obok plugin 2022-08-03 17:16:42 +02:00
NoDRM
c15135b12f Fix RSA.import_key (fixes #101)
Apparently "import_key" only exists in newer versions (as an alias to
"importKey"). "importKey" works in all versions ...
2022-07-16 09:54:00 +02:00
NoDRM
077e8f5c2a Prepare release v10.0.3 2022-07-13 17:31:57 +02:00
NoDRM
fed8bb716b Add some Python2 compat code I forgot to add earlier 2022-07-13 17:31:57 +02:00
NoDRM
c12d214b59 Fix Obok plugin on Calibre 6 (#98) 2022-07-13 15:34:47 +02:00
Yuki Liu
012ff533ab fix the regular expression 2022-04-21 12:54:17 +00:00
NoDRM
dcbb377566 Fix Nook study key retrieval 2022-03-22 15:49:44 +01:00
NoDRM
76ce6d9c5c Fix Kindle for real 2022-03-20 14:32:22 +01:00
NoDRM
726d72217e Hopefully fix Kindle books 2022-03-20 08:09:00 +01:00
NoDRM
2d51005cf1 Fix print-replica Amazon books 2022-03-19 16:41:59 +01:00
NoDRM
7eb8f07a33 Bugfix for Nook PDFs? 2022-03-19 16:02:33 +01:00
NoDRM
e4fe032e47 Some untested Python2 Kindle bugfixes 2022-03-19 15:23:07 +01:00
NoDRM
bb170688ba (Hopefully) fix WineGetKeys for Kindle 2022-03-19 15:08:36 +01:00
NoDRM
b283777c0a Add back unpad to fix Python2 support 2022-03-19 10:14:45 +01:00
NoDRM
cf095a4171 Update plugin readme 2022-03-19 09:26:39 +01:00
NoDRM
263cc1d2cf Improve error message 2022-03-19 09:17:29 +01:00
NoDRM
a4689f6ac0 Make B&N plugin skip invalid hashes in Windows app 2022-03-18 17:45:07 +01:00
NoDRM
82a698edf6 Debugging for __version issue 2022-03-18 17:36:55 +01:00
NoDRM
227bda1ea6 Try to fix V3 PDF files 2022-03-18 17:29:19 +01:00
NoDRM
93ff0aac20 Update FAQs
Co-authored-by: ZolaLa <49111160+ZolaLa9@users.noreply.github.com>
2022-03-18 17:09:51 +01:00
Brose Johnstone
1f13ae0f78 Obok: Fix invalid UTF-8 causing UI to not open
For some reason, the title of a book on my device causes Obok to choke. Apparently it's not valid UTF-8.
This fixes that by ignoring decode errors.
2022-03-18 15:50:22 +00:00
a980e066a01
c5aebcca01 Add support for "hardened" Adobe DRM
What took the most time was not reverse-engineering
the scheme, but actually finding books using it...

Closes #20, #25, #45
2022-03-18 15:45:39 +00:00
a980e066a01
a1dd63ae5f Remove OpenSSL support; only support PyCryptodome
This allows us to clean up the code a lot.

On Windows, it isn't installed by default and
most of the time not be found at all.

On M1 Macs, the kernel will kill the process instead.

Closes #33.
2022-03-18 15:45:39 +00:00
NoDRM
f4634b5eab Update FAQ 2022-01-11 12:02:44 +01:00
NoDRM
034137962c Remove LCP references from Readme 2022-01-11 08:42:37 +01:00
NoDRM
2b46f61eae Add empty placeholder file for LCP 2022-01-11 07:57:02 +01:00
NoDRM
e54bb3f700 Fix IndexError in mobidedrm.py 2022-01-04 16:56:02 +01:00
NoDRM
5b3e3e420f Make plugin work in Calibre 6 (Qt 6) 2022-01-02 21:18:13 +01:00
NoDRM
f17b255159 Add "MemoryError" to FAQ 2022-01-02 19:13:37 +01:00
NoDRM
b2b55531d3 Fix FileNotFoundError during PassHash handling 2022-01-02 18:52:07 +01:00
NoDRM
b84cf9aeb8 Fix libcrypto DLL path search (see #13 and #14)
Co-authored-by: Adriano Caloiaro <code@adriano.fyi>
2022-01-02 17:29:27 +01:00
NoDRM
d5473f1db0 Try to fix B&N issues 2022-01-02 16:23:36 +01:00
NoDRM
a275d5d819 More work on standalone version, fix plugin 2022-01-01 14:11:39 +01:00
Aldo Bleeker
5ace15e912 Python 3 fixes 2021-12-29 12:18:06 +00:00
NoDRM
e0fcd99bcb Add passhash interface to CLI 2021-12-29 13:00:45 +01:00
NoDRM
b11aadcca6 Bugfixes in standalone code for Calibre < 5 / Python 2 2021-12-29 11:39:48 +01:00
NoDRM
dbf4b54026 Begin work on standalone version
Now the plugin ZIP file (DeDRM_plugin.zip) can be run with a normal
Python interpreter as if it were a Python file (try
`python3 DeDRM_plugin.zip --help`). This way I can begin building a
standalone version (that can run without Calibre) without having to
duplicate a ton of code.
2021-12-29 09:26:29 +01:00
NoDRM
9c40b3ce5a Cleanup 2021-12-29 09:14:35 +01:00
NoDRM
80f511ade9 Correct user pass padding, fix PDFStream export 2021-12-27 14:23:26 +01:00
NoDRM
c11db59150 Update Changelog 2021-12-27 10:53:40 +01:00
NoDRM
9c6f4ecc3b Fix broken key management 2021-12-27 10:45:36 +01:00
NoDRM
fbe9b5ea89 Ton of PDF DeDRM updates
- Support "Standard" and "Adobe.APS" encryptions
- Support decrypting with owner password instead of user password
- New function to return encryption filter name
- Support for V=5, R=5 and R=6 PDF files
- Support for AES256-encrypted PDF files
- Disable broken cross-reference streams in output
2021-12-27 10:45:12 +01:00
NoDRM
23a454205a Update watermark code 2021-12-27 10:39:41 +01:00
NoDRM
586609bb2c Remove ancient code to import keys from ancient plugins
There were a couple specific DRM removal plugins before the DeDRM plugin
was created. These are obsolete since a long time, there's no need to
still have the code to import their config.

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

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

Update main version to 7.0.2
2021-01-03 16:01:14 +00:00
Apprentice Harper
6cee615f26 Update ineptpdf.py
Fix handling of metadata
2021-01-03 15:35:17 +00:00
Apprentice Harper
c4581b4d72 Version to 7.0.1, ineptpdf fixes
ineptpdf should now decrypt at least some Adobe PDFs
2020-12-30 12:14:04 +00:00
Apprentice Harper
f6a568bcc1 Update ineptepub.py
Handle uncompressed elements (if any) in the zip file.
2020-12-27 12:16:11 +00:00
Apprentice Harper
bf6170e613
Merge pull request #1445 from ableeker/python3
Some more fixes for ePub
2020-12-26 16:02:12 +00:00
Apprentice Harper
afcd79c0cc
Merge pull request #1443 from jony0008/master
Update sv translation
2020-12-26 16:00:04 +00:00
Apprentice Harper
fdf0389936 MobiDeDRM fixes
Change handling of PIDs to cope with byte arrays or strings passed in. Also fixed handling of a very old default key format.
2020-12-26 15:58:42 +00:00
Aldo Bleeker
5599c1694b Some more fixes for ePub 2020-12-26 15:36:10 +01:00
Jony
dff90fae6f
Update sv translation 2020-12-25 12:47:14 +00:00
Apprentice Harper
d33f679eae
Merge pull request #1413 from ableeker/python3
Small fix to make Obok help link work.
2020-12-13 11:28:51 +00:00
Aldo Bleeker
225e74a334 Small fix to make Obok help work. 2020-12-09 17:34:24 +01:00
Apprentice Harper
13e9a14907
Merge pull request #1398 from xxyzz/config
return str from load_resource()
2020-12-04 12:52:42 +00:00
Apprentice Harper
92ea0a2f24
Merge pull request #1392 from penenkel/patch-1
Add conversion from bytearray to bytes so that pids are hashable
2020-12-04 12:51:26 +00:00
xxyzz
a1059650f6
return str from load_resource() 2020-12-03 19:02:09 +08:00
penenkel
a3cc221932
Revert changes to k4mobidedrm.py 2020-12-02 22:36:29 +01:00
penenkel
6732be1434
getPidList() now returns pids as bytes instead of bytearrays 2020-12-02 22:34:29 +01:00
penenkel
ad5cb056f0
Add conversion from bytearray to bytes so that pids are hashable 2020-11-30 23:25:01 +01:00
Apprentice Harper
d3c7388327
Merge pull request #1389 from ableeker/python3
Python 3 fixes for __init__.py
2020-11-29 16:35:46 +00:00
Aldo Bleeker
8e436ad920 Python 3 fixes fort correct version of __init__.py 2020-11-29 16:54:45 +01:00
Aldo Bleeker
ae806f734e Python 3 fixes for __init__.py 2020-11-29 13:39:04 +01:00
Apprentice Harper
ccfa454226 Merge branch 'Python2' - the DeDRM plugn version change 2020-11-29 10:47:09 +00:00
Apprentice Harper
6716db1f62 Derive calibre version tuple from __version__ string 2020-11-29 10:40:14 +00:00
Apprentice Harper
0e0d7d8b14 Don't rule out running from the command line 2020-11-28 16:25:54 +00:00
Apprentice Harper
981aadc497
Merge pull request #1380 from xxyzz/byte-string
Fix byte string error for KFX
2020-11-28 16:19:17 +00:00
Apprentice Harper
26eb5d676c Merge branch 'Python2' Bring across version number updates from 6.8.1 release 2020-11-28 16:18:09 +00:00
Apprentice Harper
464788a3f1 Update DeDRM version number to 6.8.1, and kindlekey to 2.8 2020-11-28 16:11:17 +00:00
Apprentice Harper
036f9007fd Merge branch 'Python2': Get the changes to fix Kindle key retrieval for Mac OS X Big Sur 2020-11-28 16:07:31 +00:00
Apprentice Harper
bdd1c2e474
Merge pull request #1383 from ableeker/python3
Python 3 fixes for Barnes&Noble
2020-11-28 15:47:22 +00:00
Apprentice Harper
54a58d05a5
Merge pull request #1382 from koumaza/koumaza/refine-github-actions-workflow
Refine GitHub Actions Workflow
2020-11-28 15:45:43 +00:00
Apprentice Harper
218539f131
Merge pull request #1381 from protochron/fix_big_sur_python_2
Fix loading libcrypto on OSX Big Sur
2020-11-28 15:44:42 +00:00
Aldo Bleeker
f9d9b6016f Python 3 fixes for Barnes&Noble 2020-11-28 14:49:27 +01:00
shanghai yakisoba chan!
131cea1215
Update Format.yaml: Change execution condition of workflow
Execute format workflow only if there is `!format` in the commit message.
2020-11-28 21:39:27 +09:00
shanghai yakisoba chan!
731eeac087
Refine gh-actions
* Update and rename Python_test.yml to Lint.yaml
* Create Format.yaml
2020-11-28 15:48:31 +09:00
Dan Norris
cdab22e59c
Fix loading libcrypto on OSX Big Sur
It looks like Big Sur removed `libcrypto.dylib` as a file on the
filesystem, so loading it using `ctypes.find_library` fails which breaks
Kindle decryption. Now to load a dylib you need to attempt to load it
directly and the operating system will load the dylib from the OS' cache
or fail.

This fixes the problem by explicitly setting the path to libcrypto to
`/usr/lib/libcrypto.dylib` if `ctypes.find_library` does not find the
file, loading the dylib and raising an exception if it fails at that
point.

See saltstack/salt#5778 for more detailed info.

Closes #1369.
2020-11-27 22:28:34 -05:00
xxyzz
b8b324956c
replace bord with ord and some other byte string fix
PyCryptodome's bord() in Python3 does nothing.
2020-11-28 11:22:27 +08:00
xxyzz
1955b34883
import ion correctly 2020-11-28 11:20:53 +08:00
Apprentice Harper
dbc5c2b4de
Merge pull request #1269 from keshavgbpecdelhi/patch-4
using the Kindle & prompt
2020-11-27 19:34:20 +00:00
Apprentice Harper
856fef55be
Merge pull request #1268 from keshavgbpecdelhi/patch-3
changing wil to will
2020-11-27 19:34:08 +00:00
Apprentice Harper
f2fa0426b7
Merge pull request #1267 from keshavgbpecdelhi/patch-2
prompt and will
2020-11-27 19:33:59 +00:00
Apprentice Harper
c3376cc492
Merge pull request #1266 from keshavgbpecdelhi/patch-1
"promt" doesn't make any sense
2020-11-27 19:33:46 +00:00
Apprentice Harper
dc72c368a5
Update ReadMe_Overview.txt 2020-11-27 19:32:25 +00:00
Apprentice Harper
77033e1602
Update FAQs.md
update with calibre 5 and new KFX info
2020-11-27 19:29:12 +00:00
Apprentice Harper
15cd372ad9
Update README.md
Update ReadMe for calibre 5 and new KFX DRM
2020-11-27 19:20:44 +00:00
Apprentice Harper
c52e4db3df Python 3 fix for old ereader PDB DRM removal 2020-11-27 15:51:33 +00:00
Apprentice Harper
45038cc77b Python 3 fix for epubtest.py that detects version of DRM used 2020-11-27 15:49:57 +00:00
Apprentice Harper
5ec9c98a0b Python 3 fixes for Android kindle key retrieval 2020-11-27 15:46:06 +00:00
xxyzz
66bab7bd7d
using byte string in kfxdedrm.py 2020-11-27 22:01:18 +08:00
Apprentice Harper
e0c7d7d382 Revert "PyCrypto requires RSA values to be long"
This reverts commit a1703e15d4.
2020-11-25 08:36:06 +00:00
Apprentice Harper
f12a4f3856 Revert to byte arrays for maps on PC, and so fix for Mac which still used byte arrays. Remove some unused code. 2020-11-23 14:22:48 +00:00
Apprentice Harper
87881659c4
Merge pull request #1362 from ivan-m/pycrypto_rsa_long
PyCrypto requires RSA values to be long not int (which is possible for small numbers)
2020-11-23 13:31:10 +00:00
Apprentice Harper
dbc7f26097
Merge pull request #1357 from task-hazy/python_3_cli_linux
Adjust wineutils to better call wine python
2020-11-23 13:28:29 +00:00
Apprentice Harper
c58e82d97f
Merge pull request #1354 from ableeker/python3
Python3 customisation dialog
2020-11-23 13:26:27 +00:00
Aldo Bleeker
74bcf33591 Python 3 fixes 2020-11-22 16:03:45 +01:00
Ivan Lazar Miljenovic
a1703e15d4 PyCrypto requires RSA values to be long
This is at least true for PyCrypto 2.6.1
2020-11-11 20:51:19 +08:00
Task Hazy
591448d1f5 Adjust wineutils to better call wine python
Separate out logic to find correct python executable, and change to not
do shell call with subprocess
2020-11-09 16:51:13 -07:00
Aldo Bleeker
a74f37c79e Minor Python 3 fix for Customize dialog 2020-11-07 13:43:58 +01:00
Aldo Bleeker
7f4e6698ef More Python 3 fixes for Customize plugin dialog 2020-11-06 23:49:18 +01:00
Apprentice Harper
e2e19fb50f
Merge pull request #1348 from fireattack/master
Convert all to bytes first before concat (fix for Windows routine)
2020-11-05 10:51:56 +00:00
fireattack
4a319a3522 Convert all to bytes first before concat 2020-11-02 02:09:52 -06:00
Apprentice Harper
f1ef1b8ecd
Merge pull request #1340 from ableeker/python3
Python 3 fixes config.py alfcrypto.py
2020-10-29 14:09:28 +00:00
Apprentice Harper
af0acf31a3
Merge pull request #1338 from ivan-m/wine_pythonpath
Set PYTHONPATH="" when running through wine
2020-10-29 14:06:53 +00:00
Aldo
6dd022e6a0 Python 3 fixes config.py alfcrypto.py 2020-10-28 18:54:33 +01:00
Ivan Lazar Miljenovic
ef59e112c1 Set PYTHONPATH="" when running through wine
Without this, it's possible for the Linux PYTHONPATH to leak through
and mixing up the PyCrypto libraries being called (or possibly
exceeding the allowed length of the PYTHONPATH in wine).
2020-10-27 13:34:16 +08:00
Apprentice Harper
019abecd05
Merge pull request #1333 from jpwhiting/fixwinreg
Fixwinreg - thanks, these all look useful and good.
2020-10-22 13:56:05 +01:00
Apprentice Harper
7b3bbbd008
Merge pull request #1331 from koumaza/koumaza/issue-template
Create Question Issue Template
2020-10-22 13:54:11 +01:00
Apprentice Harper
32968b1328
Merge pull request #1329 from koumaza/koumaza/readme-wiki-how-to-remove
Add link to Wiki Page `How to remove DRM` in README.md
2020-10-22 13:53:01 +01:00
Jeremy Whiting
e0ec691dd6 Fix another exception thrown when unable to find kindle keys. 2020-10-21 10:56:58 -06:00
Jeremy Whiting
0add3646d9 _winreg in python3 has been changed to winreg. Update imports. 2020-10-21 10:56:50 -06:00
shanghai yakisoba chan!
16024ee972
Update README.md
Change Wiki Link
2020-10-21 09:00:04 +09:00
shanghai yakisoba chan!
9cfe09e507
Create QUESTION.md 2020-10-21 02:26:26 +09:00
shanghai yakisoba chan!
4a58d6f7dc
Update README.md
Add Wiki Page Link
2020-10-21 01:29:35 +09:00
Apprentice Harper
c4c20eb07e
Merge pull request #1318 from task-hazy/kindle_fetch
Get working kindlekey.py on Python 3.8.6
2020-10-20 16:21:36 +01:00
Task Hazy
cc33f40ecc Get working kindlekey.py on Python 3.8.6 2020-10-16 12:07:34 -06:00
Apprentice Harper
939cdbb0c9 More fixes for Amazon books, fixing identity checks, started on Topaz. 2020-10-16 13:58:59 +01:00
Apprentice Harper
dc27c36761 test file type correctly 2020-10-16 13:22:19 +01:00
Apprentice Harper
7262264b95
Update README.md 2020-10-14 16:34:27 +01:00
Apprentice Harper
4b160132a5 Merge branch 'master' of https://github.com/apprenticeharper/DeDRM_tools 2020-10-14 16:33:14 +01:00
Apprentice Harper
85fb4ff729
Merge pull request #1297 from PetraOleum/patch-1
Update doc link for preferences code
2020-10-14 16:25:01 +01:00
Apprentice Harper
608bd400ee
Merge pull request #1296 from tartley/lint-fixes
Fix CI lint failures
2020-10-14 16:24:16 +01:00
Apprentice Harper
781268e17e More general changes, and get mobidedrm and kindlekey to work on Mac. 2020-10-14 16:23:49 +01:00
Petra Lamborn
41d3da12ec
Update doc link for preferences code
This should really be properly explained, but at least it's not a dead link now!
2020-10-09 22:25:01 +13:00
Jonathan Hartley
83139bc590 Remove unused fns in make_release.py 2020-10-08 14:37:04 -05:00
Apprentice Harper
e31752e334 Mostly Mac fixes. mobidedrm.py now works, and k4mobidedrm for at least some input. kindlekey.py should be working too. But lots more changes and testing to do. 2020-10-04 20:36:12 +01:00
Apprentice Harper
2eb31c8fb5
Merge pull request #1275 from jpwhiting/python3fixes
Python3fixes
2020-10-04 20:07:37 +01:00
Apprentice Harper
a3c7bad67e
Merge pull request #1265 from heindevries/master
Some changes in obok.py to make it work on windows
2020-10-04 20:04:32 +01:00
Jeremy Whiting
dca0cf7d00 Fix kgenpids string vs bytes usage for python3 for calibre 5.1.
In order to properly get pids etc. we need to pass bytes to MD5 and SHA1
instead of unicode strings. Also ord() is no longer needed since
data is bytes value gets int and we need chr() to get characters from
the mapping bytearrays.
2020-10-03 22:36:35 -06:00
Jeremy Whiting
62e0a69089 Fix launching help link from customization dialog.
To fix error with python3 when launching help link open files in binary
mode.
2020-10-03 22:36:35 -06:00
Jeremy Whiting
9df1563492 Use open instead of file() to export keys to file.
Fixes export of Kindle keys in calibre 5.0.1 here.
2020-10-03 22:36:27 -06:00
keshavgbpecdelhi
971db9ae71
using the Kindle & prompt
As I already said prompt is the right word so yeah...
and "you are use kindle" is making no sense so replacing it to make it meaningful i.e. "If you are using the Kindle for PC under Wine"
2020-10-01 00:16:55 +05:30
keshavgbpecdelhi
cf829db532
wil to will
typo
2020-10-01 00:05:46 +05:30
keshavgbpecdelhi
80c8bd2d24
prompt and will
Sorry but typos are typos 
"promt" should be written as "prompt"
and "wil" should be "will"
2020-10-01 00:01:32 +05:30
keshavgbpecdelhi
969599ce6b
"promt" doesn't make any sense
I think it may be a silly mistake or something because the other prompts are written well except this. Just to webpage will not look authentic by using a wrong spelling so writing the sentence like as follows :
Clicking this button will prompt you to enter a new name for the highlighted key in the list.
2020-09-30 23:12:26 +05:30
HdV
f55420bbf4 Merge branch 'master' of https://github.com/heindevries/DeDRM_tools
merging
2020-09-30 16:56:14 +02:00
HdV
7f758566d3 Changes to make obok work on win
_winreg renamed to winreg in python 3
os.popen3() replaced by subprocess.Popen()
2020-09-30 16:47:27 +02:00
Apprentice Harper
ff8d44492e Fix problem on Mac with byte arrays. 2020-09-30 13:25:32 +01:00
Apprentice Harper
21d4811bfe
Merge pull request #1255 from cclauss/patch-2
GitHub Action test on both Python 2 and Python 3
2020-09-30 11:45:50 +01:00
Christian Clauss
558efebbff
Update genbook.py 2020-09-28 01:03:30 +02:00
Christian Clauss
1eaee6a0a8
Old style exceptions are syntax errors in Python 3
Switch to new style exceptions which work on both Python 2 and Python 3.
2020-09-28 01:00:21 +02:00
Christian Clauss
3f644ddfd6
print() is a function in Python since 1/1/2020 2020-09-28 00:49:21 +02:00
Christian Clauss
08bdacf476
Fix Python syntax error: add a comma
Discovered by flake8 running in our GitHub Action
2020-09-28 00:39:57 +02:00
Christian Clauss
109261bdc0
GitHub Action test on both Python 2 and Python 3 2020-09-28 00:36:25 +02:00
Apprentice Harper
de50a02af9 More generic 3.0 changes, to be tested. 2020-09-27 11:54:49 +01:00
Apprentice Harper
6920f79a26
Merge pull request #1248 from kubik147/adobekey
Make adobekey.py work in Python 3
2020-09-27 10:11:37 +01:00
kubik147
2800f7cd80 Remove the u string prefixes 2020-09-27 00:57:53 +02:00
kubik147
61c5096da0 Make adobekey.py work in Python 3 2020-09-27 00:54:40 +02:00
Apprentice Harper
9118ce77ab
Merge pull request #1170 from Dr-Willy/master
Fix path in make_release.py
2020-09-26 21:19:48 +01:00
Apprentice Harper
c3aa1b62bb
Merge pull request #1241 from erikbrinkman/patch-1
Support ebook-convert
2020-09-26 21:19:17 +01:00
Apprentice Harper
afa4ac5716 Starting on Version 7.0 using the work done by others. Completely untested. I will be testing things, but I thought I'd get this base version up for others to give pull requests.
THIS IS ON THE MASTER BRANCH. The Master branch will be Python 3.0 from now on. While Python 2.7 support will not be deliberately broken, all efforts should now focus on Python 3.0 compatibility.

I can see a lot of work has been done. There's more to do. I've bumped the version number of everything I came across to the next major number for Python 3.0 compatibility indication.

Thanks everyone. I hope to update here at least once a week until we have a stable 7.0 release for calibre 5.0
2020-09-26 21:22:47 +01:00
Erik Brinkman
c516306858
Support ebook-convert
`ebook-convert`  converts ebooks without adding them to the calibre library, and so dedrm_tools fails to run and convert books that are processed in this way. Adding on_preprocess means that it will also run on any preprocessing allowing these tools to be used by the cli tools.

As far as I'm aware, there's nothing wrong with having this run in both instances, and it still seems to allow conversion in the "standard way".
2020-09-20 16:43:23 -04:00
Dr-Willy
e76bb408a3 Fix path in make_release.py 2020-07-20 21:07:20 +12:00
Apprentice Harper
4868a7460e Updates to FAQs and ReadMes 2020-06-18 08:03:20 +01:00
Apprentice Harper
0859f197fc Update init file, update versions in files, update comments in files 2020-06-18 07:42:41 +01:00
Apprentice Harper
da85d4ffac
Merge pull request #1095 from fondfire/patch-1
Create ignoblepdf.py
2020-06-17 16:04:41 +01:00
Apprentice Harper
6fd5535072
Merge pull request #1091 from vanicat/inetepub-python3
Inetepub python3
2020-06-17 15:57:27 +01:00
Apprentice Harper
885ef5e890
Merge pull request #1037 from apprenticesakuya/master
Finish .kinf2018 support and add KFX v2/v3 support
2020-06-17 15:56:37 +01:00
apprenticesakuya
22d2b37e04
Support KFX VoucherEnvelope versions 2 and 3 2020-06-16 01:19:15 +00:00
apprenticesakuya
837562db66
Support .kinf2018 on Mac 2020-06-11 17:26:36 +00:00
fondfire
3dcf3a5483
Create ignoblepdf.py
New Python 2 program to decrypt Barnes & Noble encrypted PDF files.
2020-05-15 22:08:30 -05:00
Rémi Vanicat
f7b4efc3e1 More handling of difference between python2 and python3
Place where python3 use bytes/int and python2 str/str
2020-05-08 18:09:27 +02:00
Rémi Vanicat
2fbf2c1c5f decoding from base64 in a portable way 2020-05-08 18:09:27 +02:00
Rémi Vanicat
3166273622 modernizing ineptepub.
decrypting as python2 work
failing with python3:
  File "ineptepub.py", line 424, in decryptBook
    bookkey = rsa.decrypt(bookkey.decode('base64'))
AttributeError: 'str' object has no attribute 'decode'
2020-05-08 18:09:27 +02:00
apprenticesakuya
ea916d85fc
Finish .kinf2018 support 2020-03-27 13:01:09 -07:00
Apprentice Harper
2bb73584f2 merge of translations 2020-02-17 12:07:35 +00:00
Apprentice Harper
8495ebe36d Merge branch 'master' of https://github.com/apprenticeharper/DeDRM_tools 2020-02-17 12:06:23 +00:00
Apprentice Harper
92bf51bc8f Remove stand-alone apps. Only support the two plugins. 2020-02-16 10:12:25 +00:00
Apprentice Harper
e15ff385ca
Merge pull request #989 from jony0008/master
New translation for obok plug-in: Swedish
2020-02-06 12:01:44 +00:00
Apprentice Harper
d48f4b86cf
Merge pull request #988 from ZolaLa9/Update-FAQs-for-Kindle-for-Mac-and-Catalina
Update FAQs.md for K4Mac and Catalina
2020-02-06 11:59:15 +00:00
Jony
2ef5c59ebe
New translation: Swedish
I have finished the Swedish translation. Please merge it.
2020-02-02 09:20:52 +01:00
ZolaLa
d2995539f0
Update FAQs.md 2020-02-01 05:22:56 +00:00
Apprentice Harper
ef3c7f261c
Merge pull request #859 from HansChua/linux_handling
Allow users to specify Kobo directory and add 'ip' command for linux
2020-01-30 12:08:45 +00:00
Apprentice Harper
778ce4782e
Merge branch 'master' into linux_handling 2020-01-30 12:06:38 +00:00
Apprentice Harper
69ac9b7399
Merge pull request #848 from sretlawd/kfx_dsn_fix
Allow decryption with DSN only.
2020-01-30 12:04:00 +00:00
Apprentice Harper
423dec0309
Merge pull request #847 from aplaice/linux_documentation
Improve documentation for using Kindle for PC with Linux in Wine
2020-01-30 12:03:08 +00:00
Apprentice Harper
582479c1f4
Merge pull request #845 from aplaice/fix_default_winepath
Fix automatic import of decryption keys on Linux with wine
2020-01-30 12:02:15 +00:00
Apprentice Harper
c1ece2f288
Merge pull request #850 from cclauss/modernize-Python-2-codes
Use print() function in both Python 2 and Python 3
2020-01-30 12:00:02 +00:00
Apprentice Harper
f5dd758b1b
Merge branch 'master' into modernize-Python-2-codes 2020-01-30 11:58:50 +00:00
Apprentice Harper
a107742191
Merge pull request #983 from cclauss/patch-1
GitHub Action: There is no requirements.txt
2020-01-30 11:57:21 +00:00
Christian Clauss
ce8538a2ca
Remove the unused rename_key() method 2020-01-23 13:24:45 +01:00
Apprentice Harper
2cf5960511
Merge pull request #863 from taroxd/patch-2
Fix typos - thanks.
2020-01-23 12:11:24 +00:00
Apprentice Harper
ef687eb057
Merge pull request #895 from adrw/patch-1
Update link - thanks.
2020-01-23 12:10:34 +00:00
Apprentice Harper
7d5352fdf3
Merge pull request #910 from corysolovewicz/patch-2
Update FAQs.md
2020-01-23 12:09:31 +00:00
Apprentice Harper
795f413ecb Allow Kindle serial numbers to have spaces, allowing copy/paste from Amazon web site (thanks to jakemarsden) 2020-01-23 12:14:19 +00:00
Christian Clauss
b35f777580
Focus only on legacy Python for now 2020-01-20 15:23:19 +01:00
Christian Clauss
0895aeb323
Undefined name: from ignoblekeygen import generate_key 2020-01-20 15:17:06 +01:00
Christian Clauss
eddbefcf91
Undefined name: strip(uuidnum) --> uuidnum.strip() 2020-01-20 15:11:14 +01:00
Christian Clauss
0955713cd6
Undefined name: errlog = '' 2020-01-20 14:58:03 +01:00
Christian Clauss
4e26b9d4e7
Undefined name: errlog = '' 2020-01-20 14:55:42 +01:00
Christian Clauss
8c08c67aa8
Undefined name: import zipfix 2020-01-20 14:47:04 +01:00
Christian Clauss
90335bb925
Undefined name: Define RegError 2020-01-20 14:41:29 +01:00
Christian Clauss
a10d9a617f
Undefined name: Error() --> ValueError() 2020-01-20 14:34:56 +01:00
Christian Clauss
7edebeef0d
import erdr2pml, ineptpdf, k4mobidedrm 2020-01-20 14:33:16 +01:00
Christian Clauss
e35b37c4f4
Undefined name: from .convert2xml import encodeNumber 2020-01-20 14:29:03 +01:00
Christian Clauss
1fd972ee17
Identity is not the same thing as equality in Python 2020-01-20 13:54:20 +01:00
Christian Clauss
616548a9a8
Undefined name: import traceback for line 70 2020-01-20 13:52:54 +01:00
Christian Clauss
e4c1a09d45
Undefined name: import traceback 2020-01-20 13:49:02 +01:00
Christian Clauss
89cf29cb78
flake8 . --builtins=_,I 2020-01-20 13:46:20 +01:00
Christian Clauss
c74f4b20d3
Undefined name: from datetime import datetime 2020-01-20 13:45:36 +01:00
Christian Clauss
ae703e523c
flake8 . --builtins=_ 2020-01-20 13:41:14 +01:00
Christian Clauss
48dac14218
builtins=_ 2020-01-20 13:39:27 +01:00
Christian Clauss
798a7f9c8e
GitHub Action: There is no requirements.txt 2020-01-20 13:35:10 +01:00
Apprentice Harper
43f80b767a
Merge pull request #957 from cclauss/patch-1
GitHub Actions: Lint and test our Python code
2020-01-20 12:32:28 +00:00
Apprentice Harper
e07bb6523b
Merge pull request #965 from jakemarsden/patch-1
Fix very minor typo in contrib README
2020-01-20 12:28:26 +00:00
Apprentice Harper
5d8dc595ce
Merge pull request #971 from cgaspar/master
Update lzma import to include calibre >= 4.6.0
2020-01-19 14:48:20 +00:00
Carson Gaspar
fc6f830088 Update lzma import to include calibre >= 4.6.0 2020-01-04 05:20:16 -08:00
Jake Marsden
ff51ee8227
Fix very minor typo in contrib README 2019-12-29 23:30:29 +13:00
Christian Clauss
952b7fa7c0
GitHub Actions: Lint and test our Python code 2019-12-17 08:51:25 +01:00
Cory Solovewicz
0e9e3cf7ca
Update FAQs.md
Update formatting: Wrap all filenames, file paths, and terminal commands in code quotes and cleaned up the file hashes by putting them in an unordered ist.
2019-10-05 12:03:59 -07:00
Andrew (Paradi) Alexander
57702b7d17
Update link 2019-09-05 12:04:40 -04:00
taroxd
666af55404
Update DeDRM_plugin_ReadMe.txt 2019-07-15 20:27:00 +08:00
taroxd
60f1865b53
Fix typo 2019-07-15 20:19:40 +08:00
snah
488cc540cd Allow users to specify Kobo directory and add 'ip' command for linux 2019-07-06 11:01:28 +08:00
cclauss
5bb6b58bc1 Use print() function in both Python 2 and Python 3
Legacy __print__ statements are syntax errors in Python 3 but __print()__ function works as expected in both Python 2 and Python 3.
2019-06-24 18:49:38 +02:00
Dan Walters
3f591ce66f Allow decryption with DSN only. 2019-06-14 14:20:56 -05:00
Adam Plaice
8bd53cd998 Improve documentation for using Kindle for PC with Linux in Wine
I've tested this on Ubuntu 18.04, with wine installed from the default
package repos (no PPAs) with Kindle for PC version 1.17.
2019-06-12 22:36:14 +02:00
Adam Plaice
4bd89fa4aa Fix automatic import of decryption keys on Linux with wine
By default, the wineprefix passed to WineGetKeys is "". Unfortunately,

    os.path.abspath(os.path.expanduser(os.path.expandvars("")))

returns the path to the working directory, which depends on the
directory from which calibre was invoked.  Hence under current
behaviour the wineprefix becomes that path, no longer being the empty
string.  This means that the `cmdline` that's run is always
`WINEPREFIX=/some/path/ wine python.exe [...]`, rather than `wine
python.exe [...]` even under default conditions, when the wineprefix
hasn't been changed.  Unless the user is improbably lucky and invokes
calibre from ~/.wine/ (the default wineprefix), this causes automatic
retrieval of the keys to always fail.

The bug was introduced in f2190a6755.

Checking for "" allows for correct behaviour in the default case,
while keeping the nice behaviour of expanding `~`.
2019-06-12 21:13:25 +02:00
Apprentice Harper
b71ed3887e
Update README.md
added three very FAQs
2019-05-18 18:42:56 +01:00
Apprentice Harper
d152586edc
Update FAQs.md 2019-04-22 15:03:09 +01:00
Apprentice Harper
aca8043174
Update FAQs.md
better pycrypto install instructions for Mac
2019-04-22 15:01:43 +01:00
Apprentice Harper
8165ad3ebb Fix silly version number error 2019-03-30 16:13:05 +00:00
170 changed files with 18077 additions and 12338 deletions

17
.gitattributes vendored
View file

@ -1,17 +0,0 @@
# Auto detect text files and perform LF normalization
* text=auto
# Custom for Visual Studio
*.cs diff=csharp
# Standard to msysgit
*.doc diff=astextplain
*.DOC diff=astextplain
*.docx diff=astextplain
*.DOCX diff=astextplain
*.dot diff=astextplain
*.DOT diff=astextplain
*.pdf diff=astextplain
*.PDF diff=astextplain
*.rtf diff=astextplain
*.RTF diff=astextplain

41
.github/ISSUE_TEMPLATE/QUESTION.yml vendored Normal file
View file

@ -0,0 +1,41 @@
name: Question
description: Questions for DeDRM Project
body:
- type: textarea
id: question
attributes:
label: Question / bug report
description: Please enter your question / your bug report.
- type: input
id: calibre-version
attributes:
label: Which version of Calibre are you running?
description: "Example: 6.23"
placeholder: "6.23"
validations:
required: true
- type: input
id: plugin-version
attributes:
label: Which version of the DeDRM plugin are you running?
description: "Example: v10.0.2"
placeholder: "v10.0.2"
validations:
required: true
- type: input
id: kindle-version
attributes:
label: If applicable, which version of the Kindle software are you running?
description: "Example: 1.24"
placeholder: "Leave empty if unrelated to Kindle books"
validations:
required: false
- type: textarea
id: log
attributes:
label: Log output
description: If applicable, please post your log output here - into the code block.
value: |
```log
Paste log output here.
```

52
.github/workflows/main.yml vendored Normal file
View file

@ -0,0 +1,52 @@
name: Package plugin
on:
push:
branches: [ master ]
jobs:
package:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Package
run: python3 make_release.py
- name: Upload
uses: actions/upload-artifact@v4
with:
name: plugin
path: |
DeDRM_tools_*.zip
DeDRM_tools.zip
- name: Prepare release
run: cp DeDRM_tools.zip DeDRM_alpha_${{ github.sha }}.zip
- uses: dev-drprasad/delete-older-releases@v0.2.1
with:
repo: noDRM/DeDRM_tools_autorelease
keep_latest: 0
delete_tags: true
env:
GITHUB_TOKEN: ${{ secrets.AUTORELEASE_KEY }}
- name: Auto-release
id: autorelease
uses: softprops/action-gh-release@v1
with:
tag_name: autorelease_${{ github.sha }}
repository: noDRM/DeDRM_tools_autorelease
token: ${{ secrets.AUTORELEASE_KEY }}
name: Automatic alpha release with latest changes
body: |
This release is automatically generated by Github for each commit.
This means, every time a change is made to the repo, a release with an untested copy of the plugin at that stage will be created. This will contain the most up-to-date code, but it's not tested at all and may be broken.
Last update based on Git commit [${{ github.sha }}](https://github.com/noDRM/DeDRM_tools/commit/${{ github.sha }}).
prerelease: true
draft: false
files: DeDRM_alpha_${{ github.sha }}.zip

99
.gitignore vendored
View file

@ -1,96 +1,9 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.pyc
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.cache
nosetests.xml
coverage.xml
# Translations
*.pot
# Django stuff:
*.log
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# =========================
# Operating System Files
# =========================
# OSX
# =========================
# Mac files
.DS_Store
.AppleDouble
.LSOverride
# Thumbnails
._*
# local test data
/user_data/
# Files that might appear on external disk
.Spotlight-V100
.Trashes
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
# Windows
# =========================
# Windows image file caches
Thumbs.db
ehthumbs.db
# Folder config file
Desktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msm
*.msp
# Windows shortcuts
*.lnk
# Cache
/DeDRM_plugin/__pycache__
/DeDRM_plugin/standalone/__pycache__

View file

@ -13,21 +13,21 @@ 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
- Figure out what format DeDRM wants your key in by looking in
[the code that handles that](src/prefs.py).
[the code that handles that](DeDRM_plugin/prefs.py).
- For Kindle eInk devices, DeDRM expects you to put a list of serial
numbers in the `serials` field: `"serials": ["012345689abcdef"]` or
`"serials": ["1111111111111111", "2222222222222222"]`.

113
CHANGELOG.md Normal file
View file

@ -0,0 +1,113 @@
# Changelog
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.~ (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.
- Issues with Obok key retrieval fixed (merged [apprenticeharper/DeDRM_tools#1691](https://github.com/apprenticeharper/DeDRM_tools/pull/1691) ).
- Issues with obfuscated Adobe fonts fixed (fixes [apprenticeharper/DeDRM_tools#1828](https://github.com/apprenticeharper/DeDRM_tools/issues/1828) ).
- Deobfuscate font files in EPUBs by default (can be disabled in the plugin settings).
- The standalone adobekey.py script now includes the account UUID in the key file name.
- When extracting the default key from an ADE install, include the account UUID in the key name.
- Adobe key management window size increased to account for longer key names due to the UUID.
- Verify that the decrypted book key has the correct format. This makes it way less likely for issue [apprenticeharper/DeDRM_tools#1862](https://github.com/apprenticeharper/DeDRM_tools/issues/1862) to cause trouble.
- If the Adobe owner UUID of a book being imported happens to be included in a particular key's name, try this key first before trying all the others. This completely fixes [apprenticeharper/DeDRM_tools#1862](https://github.com/apprenticeharper/DeDRM_tools/issues/1862), but only if the key name contains the correct UUID (not always the case, especially for keys imported with older versions of the plugin). It also makes DRM removal faster as the plugin no longer has to attempt all possible keys.
- Remove some additional DRM remnants in Amazon MOBI files (merged [apprenticeharper/DeDRM_tools#23](https://github.com/apprenticeharper/DeDRM_tools/pull/23) ).
- Just in case it's necessary, added a setting to the B&N key generation script to optionally allow the user to select the old key generation algorithm. Who knows, they might want to remove DRM from old books with the old key scheme.
- Add a more verbose error message when trying to remove DRM from a book with the new, not-yet-cracked version of the Adobe ADEPT DRM.
- Added back support for Python2 (Calibre 2.0+). Only tested with ADEPT (PDF & EPUB) and Readium LCP so far, please open an issue if there's errors with other book types.
- Begin work on removing some kinds of watermarks from files after DRM removal. This isn't tested a lot, and is disabled by default. You can enable it in the plugin settings.
- If you're using the [ACSM Input Plugin / DeACSM](https://www.mobileread.com/forums/showthread.php?t=341975), the encryption key will automatically be extracted from that plugin if necessary.
## 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 in v10.0.3 (2022-07-13):
- Fix issue where importing a key from Adobe Digital Editions would fail in Python2 (Calibre < 5) if there were non-ASCII characters in the username.
- 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.
- Fix IndexError when DeDRMing some Amazon eBooks.
- Add support for books with the new ADE3.0+ DRM by merging #48 by a980e066a01. Thanks a lot! (Also fixes #96 on MacOS)
- Remove OpenSSL support, now the plugin will always use the Python crypto libraries.
- Obok: Fix issues with invalid UTF-8 characters by merging #26 by baby-bell.
- ineptpdf: Fix broken V=3 key obfuscation algorithm.
- ineptpdf: (Hopefully) fix issues with some B&N PDF files.
- Fix broken Amazon K4PC key retrieval (fixes #38)
- Fix bug that corrupts output file for Print-Replica Amazon books (fixes #30).
- Fix Nook Study key retrieval code (partially fixes #50).
- Make the plugin work on Calibre 6 (Qt 6). (fixes #54 and #98) If you're running Calibre 6 and you notice any issues, please open a bug report.
## Fixes in v10.0.9 (RC for v10.1.0, 2023-08-02):
Note that versions v10.0.4(s), v10.0.5(s) and v10.0.6(s) were released by other people in various forks, so I have decided to make a larger version jump so there are no conflicting version numbers / different builds with the same version number.
This is v10.0.9, a release candidate for v10.1.0. I don't expect there to be major issues / bugs, but since a lot of code has changed in the last year I wanted to get some "extended testing" before this becomes v10.1.0.
- Fix a bug introduced with #48 that breaks DeDRM'ing on Calibre 4 (fixes #101).
- Fix some more Calibre-6 bugs in the Obok plugin (should fix #114).
- Fix a bug where invalid Adobe keys could cause the plugin to stop trying subsequent keys (partially fixes #109).
- Fix DRM removal sometimes resetting the ZIP's internal "external_attr" value on Calibre 5 and newer.
- Fix tons of PDF decryption issues (hopefully fixes #104 and other PDF-related issues).
- Small Python 2 / Calibre 4 bugfix for Obok.
- Removing ancient AlfCrypto machine code libraries, moving all encryption / decryption to Python code.
- General cleanup and removal of dead code.
- Fix a bug where ADE account keys weren't automatically imported from the DeACSM plugin when importing a PDF file.
- Re-enable Xrefs in exported PDF files since the file corruption bug is hopefully fixed. Please open bug reports if you encounter new issues with PDF files.
- Fix a bug that would sometimes cause corrupted keys to be added when adding them through the config dialog (fixes #145, #134, #119, #116, #115, #109).
- Update the README (fixes #136) to indicate that Apprentice Harper's version is no longer being updated.
- Fix a bug where PDFs with empty arrays (`<>`) in a PDF object failed to decrypt, fixes #183.
- Automatically strip whitespace from entered Amazon Kindle serial numbers, should fix #158.
- Obok: Add new setting option "Add new entry" for duplicate books to always add them to the Calibre database as a new book. Fixes #148.
- Obok: Fix where changing the Calibre UI language to some languages would cause the "duplicate book" setting to reset.
- Fix Python3 bug in stylexml2css.php script, fixes #232.
- PDF: Ignore invalid PDF objids unless the script is running in strict mode. Fixes some PDFs, apparently. Fixes #233.
- Bugfix: EPUBs with remaining content in the encryption.xml after decryption weren't written correctly.
- Support for Adobe's 'aes128-cbc-uncompressed' encryption method (fixes #242).
- Two bugfixes for Amazon DeDRM from Satuoni ( https://github.com/noDRM/DeDRM_tools/issues/315#issuecomment-1508305428 ) and andrewc12 ( https://github.com/andrewc12/DeDRM_tools/commit/d9233d61f00d4484235863969919059f4d0b2057 ) that might make the plugin work with newer versions.
- Fix font decryption not working with some books (fixes #347), thanks for the patch @bydioeds.
- Fix a couple unicode errors for Python2 in Kindle and Nook code.
## Fixes on master (not yet released):
- Fix a bug where decrypting a 40-bit RC4 pdf with R=2 didn't work.
- Fix a bug where decrypting a 256-bit AES pdf with V=5 didn't work.
- Fix bugs in kgenpids.py, alfcrypto.py, mobidedrm.py and kindlekey.py that caused it to fail on Python 2 (#380).
- Fix some bugs (Python 2 and Python 3) in erdr2pml.py (untested).
- Fix file lock bug in androidkindlekey.py on Windows with Calibre >= 7 (untested).
- A bunch of updates to the external FileOpen ineptpdf script, might fix #442 (untested).
- Fix exception handling on decrypt in ion.py (#662, thanks @C0rn3j).
- Fix SHA1 hash function for erdr2pml.py script (#608, thanks @unwiredben).
- Make Kobo DRM removal not fail when there are undownloaded ebooks (#384, thanks @precondition).
- Fix Obok import failing in Calibre flatpak due to missing ip command (#586 and #585, thanks @jcotton42).
- Don't re-pack EPUB if there's no DRM to remove and no postprocessing done (fixes #555).

View 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 plugins customization dialog, you will see a button with an icon that looks like a green plus sign (+). Clicking this button will open a new dialog for entering 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 dont 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 plugins 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 thats what you truly mean to do. Once gone, its permanently gone.</p>
<h3>Renaming Keys:</h3>
<p>On the right-hand side of the plugins 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 plugins customization dialog, you will see a button with an icon that looks like a computers 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 plugins 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>

View file

@ -38,7 +38,7 @@ li {margin-top: 0.5em}
<h3>Renaming Keys:</h3>
<p>On the right-hand side of the plugins customization dialog, you will see a button with an icon that looks like a sheet of paper. Clicking this button will promt you to enter a new name for the highlighted key in the list. Enter the new name for the encryption key and click the OK button to use the new name, or Cancel to revert to the old name..</p>
<p>On the right-hand side of the plugins 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>

View file

@ -36,7 +36,7 @@ li {margin-top: 0.5em}
<p>On the right-hand side of the plugins customization dialog, you will see a button with an icon that looks like a red "X". Clicking this button will delete the highlighted Kindle serial number from the list. You will be prompted once to be sure thats what you truly mean to do. Once gone, its permanently gone.</p>
<p>Once done creating/deleting serial numbers, click Close to exit the customization dialogue. Your changes wil only be saved permanently when you click OK in the main configuration dialog.</p>
<p>Once done creating/deleting serial numbers, click Close to exit the customization dialogue. Your changes will only be saved permanently when you click OK in the main configuration dialog.</p>
</body>

View file

@ -17,15 +17,21 @@ p {margin-top: 0}
<body>
<h1>DeDRM Plugin <span class="version">(v6.3.0)</span></h1>
<h1>DeDRM Plugin <span class="version">(v10.0.9 / v10.1.0 RC1)</span></h1>
<p>This plugin removes DRM from ebooks when they are imported into calibre. If you already have DRMed ebooks in your calibre library, you will need to remove them and import them again.</p>
<p>It is a forked version created by NoDRM, based on the original plugin by Apprentice Alf and Apprentice Harper.</p>
<h3>Installation</h3>
<p>You have obviously managed to install the plugin, as otherwise you wouldnt be reading this help file. However, you should also delete any older DeDRM plugins, as this DeDRM plugin replaces the five older plugins: Kindle and Mobipocket DeDRM (K4MobiDeDRM), Ignoble Epub DeDRM (ignobleepub), Inept Epub DeDRM (ineptepub), Inept PDF DeDRM (ineptepub) and eReader PDB 2 PML (eReaderPDB2PML).</p>
<p>You have obviously managed to install the plugin, as otherwise you wouldnt be reading this help file. However, you should also delete any older DRM removal plugins, as this DeDRM plugin replaces the five older plugins: Kindle and Mobipocket DeDRM (K4MobiDeDRM), Ignoble Epub DeDRM (ignobleepub), Inept Epub DeDRM (ineptepub), Inept PDF DeDRM (ineptepub) and eReader PDB 2 PML (eReaderPDB2PML).</p>
<p>This plugin (in versions v10.0.0 and above) will automatically replace the older 7.X and below versions from Apprentice Alf and Apprentice Harper.</p>
<h3>Configuration</h3>
<p>On Windows and Mac, the keys for ebooks downloaded for Kindle for Mac/PC and Adobe Digital Editions are automatically generated. If all your DRMed ebooks can be opened and read in Kindle for Mac/PC and/or Adobe Digital Editions on the same computer on which you are running calibre, you do not need to do any configuration of this plugin. On Linux, keys for Kindle for PC and Adobe Digital Editions need to be generated separately (see the Linux section below)</p>
<p>On Windows and Mac, the keys for ebooks downloaded for Kindle for Mac/PC and Adobe Digital Editions are automatically generated. If all your DRMed ebooks can be opened and read in Kindle for Mac/PC and/or Adobe Digital Editions on the same computer on which you are running calibre, you do not need to do any configuration of this plugin. On Linux, keys for Kindle for PC and Adobe Digital Editions need to be generated separately (see the Linux section below).</p>
<p>If you are using the <a href="https://www.mobileread.com/forums/showthread.php?t=341975">DeACSM / ACSM Input Plugin</a> for Calibre, the keys will also automatically be dumped for you.</p>
<p>If you have other DRMed ebooks, you will need to enter extra configuration information. The buttons in this dialog will open individual configuration dialogs that will allow you to enter the needed information, depending on the type and source of your DRMed eBooks. Additional help on the information required is available in each of the the dialogs.</p>
@ -35,18 +41,19 @@ p {margin-top: 0}
<h3>Troubleshooting:</h3>
<p >If you find that its not working for you , you can save a lot of time by trying to add the ebook to Calibre in debug mode. This will print out a lot of helpful info that can be copied into any online help requests.</p>
<p>If you find that its not working for you , you can save a lot of time by trying to add the ebook to Calibre in debug mode. This will print out a lot of helpful info that can be copied into any online help requests.</p>
<p>Open a command prompt (terminal window) and type "calibre-debug -g" (without the quotes). Calibre will launch, and you can can add the problem ebook the usual way. The debug info will be output to the original command prompt (terminal window). Copy the resulting output and paste it into the comment you make at my blog.</p>
<p><span class="bold">Note:</span> The Mac version of Calibre doesnt install the command line tools by default. If you go to the Preferences page and click on the miscellaneous button, youll find the option to install the command line tools.</p>
<h3>Credits:</h3>
<ul>
<li>NoDRM for a bunch of updates and maintenance since November 2021<s>, and the Readium LCP support</s></li>
<li>The Dark Reverser for the Mobipocket and eReader scripts</li>
<li>i♥cabbages for the Adobe Digital Editions scripts</li>
<li>Skindle aka Bart Simpson for the Amazon Kindle for PC script</li>
<li>CMBDTC for Amazon Topaz DRM removal script</li>
<li>some_updates, clarknova and Bart Simpson for Amazon Topaz conversion scripts</li>
<li>some_updates, clarknova and Bart Simpson for Amazon Topaz conversion scripts</li>
<li>DiapDealer for the first calibre plugin versions of the tools</li>
<li>some_updates, DiapDealer, Apprentice Alf and mdlnx for Amazon Kindle/Mobipocket tools</li>
<li>some_updates for the DeDRM all-in-one Python tool</li>
@ -55,7 +62,8 @@ p {margin-top: 0}
<li>And probably many more.</li>
</ul>
<h3> For additional help read the <a href="https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md">FAQs</a> at <a href="https://github.com/apprenticeharper/DeDRM_tools/">Apprentice Harperss GitHub repository</a>. You can ask questions in the comments section of the <a href="http://apprenticealf.wordpress.com/2012/09/10/drm-removal-tools-for-ebooks/">first post</a> at <a href="http://wordpress.com/apprenticealf/">Apprentice Alf's blog</a> or <a href="https://github.com/apprenticeharper/DeDRM_tools/issues">raise an issue</a>. </h3>
<h4>For additional help read the <a href="https://github.com/noDRM/DeDRM_tools/blob/master/FAQs.md">FAQs</a> at <a href="https://github.com/noDRM/DeDRM_tools">NoDRM's GitHub repository</a> (or the corresponding <a href="https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md">FAQs</a> at <a href="https://github.com/apprenticeharper/DeDRM_tools/">Apprentice Harperss GitHub repository</a>). You can <a href="https://github.com/noDRM/DeDRM_tools/issues">open issue reports</a> related to this fork at NoDRM's GitHub repository.</h4>
<h2>Linux Systems Only</h2>
<h3>Generating decryption keys for Adobe Digital Editions and Kindle for PC</h3>

View file

@ -22,6 +22,8 @@ li {margin-top: 0.5em}
<p>If you have upgraded from an earlier version of the plugin, any existing Kindle for Mac/PC keys will have been automatically imported, so you might not need to do any more configuration. In addition, on Windows and Mac, the default Kindle for Mac/PC key is added the first time the plugin is run. Continue reading for key generation and management instructions.</p>
<p>Note that for best results, you should run Calibre / this plugin on the same machine where Kindle 4 PC / Kindle 4 Mac is running. It is possible to export/import the keys to another machine, but this may not always work, particularly with the newer DRM versions.</p>
<h3>Creating New Keys:</h3>
<p>On the right-hand side of the plugins customization dialog, you will see a button with an icon that looks like a green plus sign (+). Clicking this button will open a new dialog prompting you to enter a key name for the default Kindle for Mac/PC key. </p>
@ -38,7 +40,7 @@ li {margin-top: 0.5em}
<h3>Renaming Keys:</h3>
<p>On the right-hand side of the plugins customization dialog, you will see a button with an icon that looks like a sheet of paper. Clicking this button will promt you to enter a new name for the highlighted key in the list. Enter the new name for the encryption key and click the OK button to use the new name, or Cancel to revert to the old name..</p>
<p>On the right-hand side of the plugins customization dialog, you will see a button with an icon that looks like a sheet of paper. Clicking this button will prompt you to enter a new name for the highlighted key in the list. Enter the new name for the encryption key and click the OK button to use the new name, or Cancel to revert to the old name..</p>
<h3>Exporting Keys:</h3>
@ -46,7 +48,7 @@ li {margin-top: 0.5em}
<h3>Linux Users: WINEPREFIX</h3>
<p>Under the list of keys, Linux users will see a text field labeled "WINEPREFIX". If you are use Kindle for PC under Wine, and your wine installation containing Kindle for PC isn't the default Wine installation, you may enter the full path to the correct Wine installation here. Leave blank if you are unsure.</p>
<p>Under the list of keys, Linux users will see a text field labeled "WINEPREFIX". If you are using the Kindle for PC under Wine, and your wine installation containing Kindle for PC isn't the default Wine installation, you may enter the full path to the correct Wine installation here. Leave blank if you are unsure.</p>
<h3>Importing Existing Keyfiles:</h3>

View file

@ -35,7 +35,7 @@ li {margin-top: 0.5em}
<p>On the right-hand side of the plugins customization dialog, you will see a button with an icon that looks like a red "X". Clicking this button will delete the highlighted Mobipocket PID from the list. You will be prompted once to be sure thats what you truly mean to do. Once gone, its permanently gone.</p>
<p>Once done creating/deleting PIDs, click Close to exit the customization dialogue. Your changes wil only be saved permanently when you click OK in the main configuration dialog.</p>
<p>Once done creating/deleting PIDs, click Close to exit the customization dialogue. Your changes will only be saved permanently when you click OK in the main configuration dialog.</p>
</body>

View 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 plugins customization dialog, you will see a button with an icon that looks like a green plus sign (+). Clicking this button will open a new dialog for entering a new passphrase.</p>
<p>Just enter your passphrase 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 plugins customization dialog, you will see a button with an icon that looks like a red "X". Clicking this button will delete the highlighted passphrase from the list. You will be prompted once to be sure thats what you truly mean to do. Once gone, its permanently gone.</p>
<p>Once done entering/deleting passphrases, click Close to exit the customization dialogue. Your changes will only be saved permanently when you click OK in the main configuration dialog.</p>
</body>
</html>

View file

@ -0,0 +1,42 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<title>Managing Readium LCP passphrases</title>
<style type="text/css">
span.version {font-size: 50%}
span.bold {font-weight: bold}
h3 {margin-bottom: 0}
p {margin-top: 0}
li {margin-top: 0.5em}
</style>
</head>
<body>
<h1>Managing Readium LCP passphrases</h1>
<p>Readium LCP is a relatively new eBook DRM. It's also known under the names "CARE DRM" or "TEA DRM". It does not rely on any accounts or key data that's difficult to acquire. All you need to open (or decrypt) LCP eBooks is the account passphrase given to you by the eBook provider - the very same passphrase you'd have to enter into your eBook reader device (once) to read LCP-encrypted books.</p>
<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>
<p>On the right-hand side of the plugins customization dialog, you will see a button with an icon that looks like a green plus sign (+). Clicking this button will open a new dialog for entering a new passphrase.</p>
<p>Just enter your passphrase as provided with the book, then click the OK button to save the passphrase. </p>
<p>Usually, passphrases are identical for all books bought with the same account. So if you buy multiple LCP-protected eBooks, they'll usually all have the same passphrase if they've all been bought at the same store with the same account. </p>
<h3>Deleting an LCP passphrase:</h3>
<p>On the right-hand side of the plugins customization dialog, you will see a button with an icon that looks like a red "X". Clicking this button will delete the highlighted passphrase from the list. You will be prompted once to be sure thats what you truly mean to do. Once gone, its permanently gone.</p>
<p>Once done entering/deleting passphrases, click Close to exit the customization dialogue. Your changes will only be saved permanently when you click OK in the main configuration dialog.</p>
</body>
</html>

View file

@ -0,0 +1,21 @@
#@@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"))
if "calibre" in sys.modules:
# Explicitly set the package identifier so we are allowed to import stuff ...
__package__ = "calibre_plugins.dedrm"
#@@CALIBRE_COMPAT_CODE_END@@

1059
DeDRM_plugin/__init__.py Normal file

File diff suppressed because it is too large Load diff

34
DeDRM_plugin/__main__.py Normal file
View file

@ -0,0 +1,34 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# __main__.py for DeDRM_plugin
# (CLI interface without Calibre)
# Copyright © 2021 NoDRM
"""
NOTE: This code is not functional (yet). I started working on it a while ago
to make a standalone version of the plugins that could work without Calibre,
too, but for now there's only a rough code structure and no working code yet.
Currently, to use these plugins, you will need to use Calibre. Hopwfully that'll
change in the future.
"""
__license__ = 'GPL v3'
__docformat__ = 'restructuredtext en'
# For revision history see CHANGELOG.md
"""
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
View file

@ -0,0 +1,12 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#@@CALIBRE_COMPAT_CODE@@
PLUGIN_NAME = "DeDRM"
__version__ = '10.0.9'
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'

View file

@ -1,32 +1,12 @@
#!/usr/bin/env python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from __future__ import with_statement
# adobekey.pyw, version 6.0
# Copyright © 2009-2010 i♥cabbages
# adobekey.pyw, version 7.4
# Copyright © 2009-2022 i♥cabbages, Apprentice Harper et al.
# Released under the terms of the GNU General Public Licence, version 3
# <http://www.gnu.org/licenses/>
# Modified 20102016 by several people
# Windows users: Before running this program, you must first install Python.
# We recommend ActiveState Python 2.7.X for Windows (x86) from
# http://www.activestate.com/activepython/downloads.
# You must also install PyCrypto from
# http://www.voidspace.org.uk/python/modules.shtml#pycrypto
# (make certain to install the version for Python 2.7).
# Then save this script file as adobekey.pyw and double-click on it to run it.
# It will create a file named adobekey_1.der in in the same directory as the script.
# This is your Adobe Digital Editions user key.
#
# Mac OS X users: Save this script file as adobekey.pyw. You can run this
# program from the command line (python adobekey.pyw) or by double-clicking
# it when it has been associated with PythonLauncher. It will create a file
# named adobekey_1.der in the same directory as the script.
# This is your Adobe Digital Editions user key.
# Revision history:
# 1 - Initial release, for Adobe Digital Editions 1.7
# 2 - Better algorithm for finding pLK; improved error handling
@ -48,32 +28,28 @@ from __future__ import with_statement
# 5.8 - Added getkey interface for Windows DeDRM application
# 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
# 7.4 - Remove OpenSSL support to only support PyCryptodome
"""
Retrieve Adobe ADEPT user key.
"""
__license__ = 'GPL v3'
__version__ = '6.0'
__version__ = '7.4'
import sys, os, struct, getopt
from base64 import b64decode
#@@CALIBRE_COMPAT_CODE@@
from .utilities import SafeUnbuffered
from .argv_utils import unicode_argv
# Wrap a stream so that output gets flushed immediately
# and also make sure that any unicode strings get
# encoded using "replace" before writing them.
class SafeUnbuffered:
def __init__(self, stream):
self.stream = stream
self.encoding = stream.encoding
if self.encoding == None:
self.encoding = "utf-8"
def write(self, data):
if isinstance(data,unicode):
data = data.encode(self.encoding,"replace")
self.stream.write(data)
self.stream.flush()
def __getattr__(self, attr):
return getattr(self.stream, attr)
try:
from calibre.constants import iswindows, isosx
@ -81,43 +57,6 @@ except:
iswindows = sys.platform.startswith('win')
isosx = sys.platform.startswith('darwin')
def unicode_argv():
if iswindows:
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
# strings.
# Versions 2.x of Python don't support Unicode in sys.argv on
# Windows, with the underlying Windows API instead replacing multi-byte
# characters with '?'. So use shell32.GetCommandLineArgvW to get sys.argv
# as a list of Unicode strings and encode them as utf-8
from ctypes import POINTER, byref, cdll, c_int, windll
from ctypes.wintypes import LPCWSTR, LPWSTR
GetCommandLineW = cdll.kernel32.GetCommandLineW
GetCommandLineW.argtypes = []
GetCommandLineW.restype = LPCWSTR
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
CommandLineToArgvW.restype = POINTER(LPWSTR)
cmd = GetCommandLineW()
argc = c_int(0)
argv = CommandLineToArgvW(cmd, byref(argc))
if argc.value > 0:
# Remove Python executable and commands if present
start = argc.value - len(sys.argv)
return [argv[i] for i in
xrange(start, argc.value)]
# if we don't have any arguments at all, just pass back script name
# this should never happen
return [u"adobekey.py"]
else:
argvencoding = sys.stdin.encoding
if argvencoding == None:
argvencoding = "utf-8"
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
class ADEPTError(Exception):
pass
@ -129,72 +68,23 @@ if iswindows:
c_long, c_ulong
from ctypes.wintypes import LPVOID, DWORD, BOOL
import _winreg as winreg
try:
import winreg
except ImportError:
import _winreg as winreg
def _load_crypto_libcrypto():
from ctypes.util import find_library
libcrypto = find_library('libeay32')
if libcrypto is None:
raise ADEPTError('libcrypto not found')
libcrypto = CDLL(libcrypto)
AES_MAXNR = 14
c_char_pp = POINTER(c_char_p)
c_int_p = POINTER(c_int)
class AES_KEY(Structure):
_fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))),
('rounds', c_int)]
AES_KEY_p = POINTER(AES_KEY)
try:
from Cryptodome.Cipher import AES
except ImportError:
from Crypto.Cipher import AES
def F(restype, name, argtypes):
func = getattr(libcrypto, name)
func.restype = restype
func.argtypes = argtypes
return func
AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key',
[c_char_p, c_int, AES_KEY_p])
AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',
[c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p,
c_int])
class AES(object):
def __init__(self, userkey):
self._blocksize = len(userkey)
if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) :
raise ADEPTError('AES improper key used')
key = self._key = AES_KEY()
rv = AES_set_decrypt_key(userkey, len(userkey) * 8, key)
if rv < 0:
raise ADEPTError('Failed to initialize AES key')
def decrypt(self, data):
out = create_string_buffer(len(data))
iv = ("\x00" * self._blocksize)
rv = AES_cbc_encrypt(data, out, len(data), self._key, iv, 0)
if rv == 0:
raise ADEPTError('AES decryption failed')
return out.raw
return AES
def _load_crypto_pycrypto():
from Crypto.Cipher import AES as _AES
class AES(object):
def __init__(self, key):
self._aes = _AES.new(key, _AES.MODE_CBC, '\x00'*16)
def decrypt(self, data):
return self._aes.decrypt(data)
return AES
def _load_crypto():
AES = None
for loader in (_load_crypto_pycrypto, _load_crypto_libcrypto):
try:
AES = loader()
break
except (ImportError, ADEPTError):
pass
return AES
AES = _load_crypto()
def unpad(data, padding=16):
if sys.version_info[0] == 2:
pad_len = ord(data[-1])
else:
pad_len = data[-1]
return data[:-pad_len]
DEVICE_KEY_PATH = r'Software\Adobe\Adept\Device'
PRIVATE_LICENCE_KEY_PATH = r'Software\Adobe\Adept\Activation'
@ -244,6 +134,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
@ -281,49 +192,54 @@ 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 = (
"\x53" # push %ebx
"\x31\xc0" # xor %eax,%eax
"\x0f\xa2" # cpuid
"\x8b\x44\x24\x08" # mov 0x8(%esp),%eax
"\x89\x18" # mov %ebx,0x0(%eax)
"\x89\x50\x04" # mov %edx,0x4(%eax)
"\x89\x48\x08" # mov %ecx,0x8(%eax)
"\x5b" # pop %ebx
"\xc3" # ret
b"\x53" # push %ebx
b"\x31\xc0" # xor %eax,%eax
b"\x0f\xa2" # cpuid
b"\x8b\x44\x24\x08" # mov 0x8(%esp),%eax
b"\x89\x18" # mov %ebx,0x0(%eax)
b"\x89\x50\x04" # mov %edx,0x4(%eax)
b"\x89\x48\x08" # mov %ecx,0x8(%eax)
b"\x5b" # pop %ebx
b"\xc3" # ret
)
CPUID1_INSNS = (
"\x53" # push %ebx
"\x31\xc0" # xor %eax,%eax
"\x40" # inc %eax
"\x0f\xa2" # cpuid
"\x5b" # pop %ebx
"\xc3" # ret
b"\x53" # push %ebx
b"\x31\xc0" # xor %eax,%eax
b"\x40" # inc %eax
b"\x0f\xa2" # cpuid
b"\x5b" # pop %ebx
b"\xc3" # ret
)
else:
CPUID0_INSNS = (
"\x49\x89\xd8" # mov %rbx,%r8
"\x49\x89\xc9" # mov %rcx,%r9
"\x48\x31\xc0" # xor %rax,%rax
"\x0f\xa2" # cpuid
"\x4c\x89\xc8" # mov %r9,%rax
"\x89\x18" # mov %ebx,0x0(%rax)
"\x89\x50\x04" # mov %edx,0x4(%rax)
"\x89\x48\x08" # mov %ecx,0x8(%rax)
"\x4c\x89\xc3" # mov %r8,%rbx
"\xc3" # retq
b"\x49\x89\xd8" # mov %rbx,%r8
b"\x49\x89\xc9" # mov %rcx,%r9
b"\x48\x31\xc0" # xor %rax,%rax
b"\x0f\xa2" # cpuid
b"\x4c\x89\xc8" # mov %r9,%rax
b"\x89\x18" # mov %ebx,0x0(%rax)
b"\x89\x50\x04" # mov %edx,0x4(%rax)
b"\x89\x48\x08" # mov %ecx,0x8(%rax)
b"\x4c\x89\xc3" # mov %r8,%rbx
b"\xc3" # retq
)
CPUID1_INSNS = (
"\x53" # push %rbx
"\x48\x31\xc0" # xor %rax,%rax
"\x48\xff\xc0" # inc %rax
"\x0f\xa2" # cpuid
"\x5b" # pop %rbx
"\xc3" # retq
b"\x53" # push %rbx
b"\x48\x31\xc0" # xor %rax,%rax
b"\x48\xff\xc0" # inc %rax
b"\x0f\xa2" # cpuid
b"\x5b" # pop %rbx
b"\xc3" # retq
)
def cpuid0():
@ -361,54 +277,76 @@ if iswindows:
CryptUnprotectData = CryptUnprotectData()
def adeptkeys():
if AES is None:
raise ADEPTError("PyCrypto or OpenSSL must be installed")
root = GetSystemDirectory().split('\\')[0] + '\\'
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
keys = []
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 xrange(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
for j in xrange(0, 16):
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 != 'privateLicenseKey':
continue
userkey = winreg.QueryValueEx(plkkey, 'value')[0]
userkey = userkey.decode('base64')
aes = AES(keykey)
userkey = aes.decrypt(userkey)
userkey = userkey[26:-ord(userkey[-1])]
#print "found key:",userkey.encode('hex')
keys.append(userkey)
if ktype == 'user':
# 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 = unpad(AES.new(keykey, AES.MODE_CBC, b'\x00'*16).decrypt(b64decode(userkey)))[26:]
# 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 u"Found {0:d} keys".format(len(keys))
return keys
print("Found {0:d} keys".format(len(keys)))
return keys, names
elif isosx:
@ -427,12 +365,12 @@ elif isosx:
cmdline = cmdline.encode(sys.getfilesystemencoding())
p2 = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
out1, out2 = p2.communicate()
reslst = out1.split('\n')
reslst = out1.split(b'\n')
cnt = len(reslst)
ActDatPath = "activation.dat"
for j in xrange(cnt):
ActDatPath = b"activation.dat"
for j in range(cnt):
resline = reslst[j]
pp = resline.find('activation.dat')
pp = resline.find(b'activation.dat')
if pp >= 0:
ActDatPath = resline
break
@ -441,6 +379,8 @@ 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.")
@ -448,56 +388,80 @@ elif isosx:
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
expr = '//%s/%s' % (adept('credentials'), adept('privateLicenseKey'))
userkey = tree.findtext(expr)
userkey = userkey.decode('base64')
exprUUID = '//%s/%s' % (adept('credentials'), adept('user'))
keyName = ""
try:
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]
return [userkey], [keyName]
else:
def adeptkeys():
raise ADEPTError("This script only supports Windows and Mac OS X.")
return []
return [], []
# interface for Python DeDRM
def getkey(outpath):
keys = adeptkeys()
keys, names = adeptkeys()
if len(keys) > 0:
if not os.path.isdir(outpath):
outfile = outpath
with file(outfile, 'wb') as keyfileout:
with open(outfile, 'wb') as keyfileout:
keyfileout.write(keys[0])
print u"Saved a key to {0}".format(outfile)
print("Saved a key to {0}".format(outfile))
else:
keycount = 0
name_index = 0
for key in keys:
while True:
keycount += 1
outfile = os.path.join(outpath,u"adobekey_{0:d}.der".format(keycount))
outfile = os.path.join(outpath,"adobekey{0:d}_uuid_{1}.der".format(keycount, names[name_index]))
if not os.path.exists(outfile):
break
with file(outfile, 'wb') as keyfileout:
with open(outfile, 'wb') as keyfileout:
keyfileout.write(key)
print u"Saved a key to {0}".format(outfile)
print("Saved a key to {0}".format(outfile))
name_index += 1
return True
return False
def usage(progname):
print u"Finds, decrypts and saves the default Adobe Adept encryption key(s)."
print u"Keys are saved to the current directory, or a specified output directory."
print u"If a file name is passed instead of a directory, only the first key is saved, in that file."
print u"Usage:"
print u" {0:s} [-h] [<outpath>]".format(progname)
print("Finds, decrypts and saves the default Adobe Adept encryption key(s).")
print("Keys are saved to the current directory, or a specified output directory.")
print("If a file name is passed instead of a directory, only the first key is saved, in that file.")
print("Usage:")
print(" {0:s} [-h] [<outpath>]".format(progname))
def cli_main():
sys.stdout=SafeUnbuffered(sys.stdout)
sys.stderr=SafeUnbuffered(sys.stderr)
argv=unicode_argv()
argv=unicode_argv("adobekey.py")
progname = os.path.basename(argv[0])
print u"{0} v{1}\nCopyright © 2009-2013 i♥cabbages and Apprentice Alf".format(progname,__version__)
print("{0} v{1}\nCopyright © 2009-2020 i♥cabbages, Apprentice Harper et al.".format(progname,__version__))
try:
opts, args = getopt.getopt(argv[1:], "h")
except getopt.GetoptError, err:
print u"Error in options or arguments: {0}".format(err.args[0])
except getopt.GetoptError as err:
print("Error in options or arguments: {0}".format(err.args[0]))
usage(progname)
sys.exit(2)
@ -522,76 +486,82 @@ def cli_main():
# make sure the outpath is the
outpath = os.path.realpath(os.path.normpath(outpath))
keys = adeptkeys()
keys, names = adeptkeys()
if len(keys) > 0:
if not os.path.isdir(outpath):
outfile = outpath
with file(outfile, 'wb') as keyfileout:
with open(outfile, 'wb') as keyfileout:
keyfileout.write(keys[0])
print u"Saved a key to {0}".format(outfile)
print("Saved a key to {0}".format(outfile))
else:
keycount = 0
name_index = 0
for key in keys:
while True:
keycount += 1
outfile = os.path.join(outpath,u"adobekey_{0:d}.der".format(keycount))
outfile = os.path.join(outpath,"adobekey{0:d}_uuid_{1}.der".format(keycount, names[name_index]))
if not os.path.exists(outfile):
break
with file(outfile, 'wb') as keyfileout:
with open(outfile, 'wb') as keyfileout:
keyfileout.write(key)
print u"Saved a key to {0}".format(outfile)
print("Saved a key to {0}".format(outfile))
name_index += 1
else:
print u"Could not retrieve Adobe Adept key."
print("Could not retrieve Adobe Adept key.")
return 0
def gui_main():
try:
import Tkinter
import Tkconstants
import tkMessageBox
import tkinter
import tkinter.constants
import tkinter.messagebox
import traceback
except:
return cli_main()
class ExceptionDialog(Tkinter.Frame):
class ExceptionDialog(tkinter.Frame):
def __init__(self, root, text):
Tkinter.Frame.__init__(self, root, border=5)
label = Tkinter.Label(self, text=u"Unexpected error:",
anchor=Tkconstants.W, justify=Tkconstants.LEFT)
label.pack(fill=Tkconstants.X, expand=0)
self.text = Tkinter.Text(self)
self.text.pack(fill=Tkconstants.BOTH, expand=1)
tkinter.Frame.__init__(self, root, border=5)
label = tkinter.Label(self, text="Unexpected error:",
anchor=tkinter.constants.W, justify=tkinter.constants.LEFT)
label.pack(fill=tkinter.constants.X, expand=0)
self.text = tkinter.Text(self)
self.text.pack(fill=tkinter.constants.BOTH, expand=1)
self.text.insert(Tkconstants.END, text)
self.text.insert(tkinter.constants.END, text)
argv=unicode_argv()
root = Tkinter.Tk()
argv=unicode_argv("adobekey.py")
root = tkinter.Tk()
root.withdraw()
progpath, progname = os.path.split(argv[0])
success = False
try:
keys = adeptkeys()
keys, names = adeptkeys()
print(keys)
print(names)
keycount = 0
name_index = 0
for key in keys:
while True:
keycount += 1
outfile = os.path.join(progpath,u"adobekey_{0:d}.der".format(keycount))
outfile = os.path.join(progpath,"adobekey{0:d}_uuid_{1}.der".format(keycount, names[name_index]))
if not os.path.exists(outfile):
break
with file(outfile, 'wb') as keyfileout:
with open(outfile, 'wb') as keyfileout:
keyfileout.write(key)
success = True
tkMessageBox.showinfo(progname, u"Key successfully retrieved to {0}".format(outfile))
except ADEPTError, e:
tkMessageBox.showerror(progname, u"Error: {0}".format(str(e)))
tkinter.messagebox.showinfo(progname, "Key successfully retrieved to {0}".format(outfile))
name_index += 1
except ADEPTError as e:
tkinter.messagebox.showerror(progname, "Error: {0}".format(str(e)))
except Exception:
root.wm_state('normal')
root.title(progname)
text = traceback.format_exc()
ExceptionDialog(root, text).pack(fill=Tkconstants.BOTH, expand=1)
ExceptionDialog(root, text).pack(fill=tkinter.constants.BOTH, expand=1)
root.mainloop()
if not success:
return 1

View 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 ImportError:
from Crypto.Cipher import AES
def unpad(data, padding=16):
if sys.version_info[0] == 2:
pad_len = ord(data[-1])
else:
pad_len = data[-1]
return data[:-pad_len]
PASS_HASH_SECRET = "9ca588496a1bc4394553d9e018d70b9e"
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.")

View 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']

View file

@ -1,4 +1,5 @@
#! /usr/bin/env python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Routines for doing AES CBC in one file
@ -13,6 +14,8 @@
CryptoPy Artisitic License Version 1.0
See the wonderful pure python package cryptopy-1.2.5
and read its LICENSE.txt for complete license details.
Adjusted for Python 3, September 2020
"""
class CryptoError(Exception):
@ -101,7 +104,7 @@ class BlockCipher:
numBlocks, numExtraBytes = divmod(len(self.bytesToDecrypt), self.blockSize)
if more == None: # no more calls to decrypt, should have all the data
if numExtraBytes != 0:
raise DecryptNotBlockAlignedError, 'Data not block aligned on decrypt'
raise DecryptNotBlockAlignedError('Data not block aligned on decrypt')
# hold back some bytes in case last decrypt has zero len
if (more != None) and (numExtraBytes == 0) and (numBlocks >0) :
@ -143,7 +146,7 @@ class padWithPadLen(Pad):
def removePad(self, paddedBinaryString, blockSize):
""" Remove padding from a binary string """
if not(0<len(paddedBinaryString)):
raise DecryptNotBlockAlignedError, 'Expected More Data'
raise DecryptNotBlockAlignedError('Expected More Data')
return paddedBinaryString[:-ord(paddedBinaryString[-1])]
class noPadding(Pad):
@ -173,8 +176,8 @@ class Rijndael(BlockCipher):
self.blockSize = blockSize # blockSize is in bytes
self.padding = padding # change default to noPadding() to get normal ECB behavior
assert( keySize%4==0 and NrTable[4].has_key(keySize/4)),'key size must be 16,20,24,29 or 32 bytes'
assert( blockSize%4==0 and NrTable.has_key(blockSize/4)), 'block size must be 16,20,24,29 or 32 bytes'
assert( keySize%4==0 and keySize/4 in NrTable[4]),'key size must be 16,20,24,29 or 32 bytes'
assert( blockSize%4==0 and blockSize/4 in NrTable), 'block size must be 16,20,24,29 or 32 bytes'
self.Nb = self.blockSize/4 # Nb is number of columns of 32 bit words
self.Nk = keySize/4 # Nk is the key length in 32-bit words
@ -451,7 +454,7 @@ class AES(Rijndael):
def __init__(self, key = None, padding = padWithPadLen(), keySize=16):
""" Initialize AES, keySize is in bytes """
if not (keySize == 16 or keySize == 24 or keySize == 32) :
raise BadKeySizeError, 'Illegal AES key size, must be 16, 24, or 32 bytes'
raise BadKeySizeError('Illegal AES key size, must be 16, 24, or 32 bytes')
Rijndael.__init__( self, key, padding=padding, keySize=keySize, blockSize=16 )

147
DeDRM_plugin/alfcrypto.py Normal file
View file

@ -0,0 +1,147 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# crypto library mainly by some_updates
# pbkdf2.py pbkdf2 code taken from pbkdf2.py
# pbkdf2.py Copyright © 2004 Matt Johnston <matt @ ucc asn au>
# pbkdf2.py Copyright © 2009 Daniel Holth <dholth@fastmail.fm>
# pbkdf2.py This code may be freely used and modified for any purpose.
import sys
import hmac
from struct import pack
import hashlib
import aescbc
class Pukall_Cipher(object):
def __init__(self):
self.key = None
def PC1(self, key, src, decryption=True):
sum1 = 0;
sum2 = 0;
keyXorVal = 0;
if len(key)!=16:
raise Exception("PC1: Bad key length")
wkey = []
for i in range(8):
if sys.version_info[0] == 2:
wkey.append(ord(key[i*2])<<8 | ord(key[i*2+1]))
else:
wkey.append(key[i*2]<<8 | key[i*2+1])
dst = bytearray(len(src))
for i in range(len(src)):
temp1 = 0;
byteXorVal = 0;
for j in range(8):
temp1 ^= wkey[j]
sum2 = (sum2+j)*20021 + sum1
sum1 = (temp1*346)&0xFFFF
sum2 = (sum2+sum1)&0xFFFF
temp1 = (temp1*20021+1)&0xFFFF
byteXorVal ^= temp1 ^ sum2
if sys.version_info[0] == 2:
curByte = ord(src[i])
else:
curByte = src[i]
if not decryption:
keyXorVal = curByte * 257;
curByte = ((curByte ^ (byteXorVal >> 8)) ^ byteXorVal) & 0xFF
if decryption:
keyXorVal = curByte * 257;
for j in range(8):
wkey[j] ^= keyXorVal;
if sys.version_info[0] == 2:
dst[i] = chr(curByte)
else:
dst[i] = curByte
return bytes(dst)
class Topaz_Cipher(object):
def __init__(self):
self._ctx = None
def ctx_init(self, key):
ctx1 = 0x0CAFFE19E
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]
return [ctx1,ctx2]
def decrypt(self, data, ctx=None):
if ctx == None:
ctx = self._ctx
ctx1 = ctx[0]
ctx2 = ctx[1]
plainText = ""
if isinstance(data, str):
data = data.encode('latin-1')
for dataByte in data:
m = (dataByte ^ ((ctx1 >> 3) &0xFF) ^ ((ctx2<<3) & 0xFF)) &0xFF
ctx2 = ctx1
ctx1 = (((ctx1 >> 2) * (ctx1 >> 7)) &0xFFFFFFFF) ^((m * m * 0x0F902007) &0xFFFFFFFF)
plainText += chr(m)
return plainText
class AES_CBC(object):
def __init__(self):
self._key = None
self._iv = None
self.aes = None
def set_decrypt_key(self, userkey, iv):
self._key = userkey
self._iv = iv
self.aes = aescbc.AES_CBC(userkey, aescbc.noPadding(), len(userkey))
def decrypt(self, data):
iv = self._iv
cleartext = self.aes.decrypt(iv + data)
return cleartext
class KeyIVGen(object):
# this only exists in openssl so we will use pure python implementation instead
# PKCS5_PBKDF2_HMAC_SHA1 = F(c_int, 'PKCS5_PBKDF2_HMAC_SHA1',
# [c_char_p, c_ulong, c_char_p, c_ulong, c_ulong, c_ulong, c_char_p])
def pbkdf2(self, passwd, salt, iter, keylen):
def xorbytes( a, b ):
if len(a) != len(b):
raise Exception("xorbytes(): lengths differ")
return bytes(bytearray([x ^ y for x, y in zip(a, b)]))
def prf( h, data ):
hm = h.copy()
hm.update( data )
return hm.digest()
def pbkdf2_F( h, salt, itercount, blocknum ):
U = prf( h, salt + pack('>i',blocknum ) )
T = U
for i in range(2, itercount+1):
U = prf( h, U )
T = xorbytes( T, U )
return T
sha = hashlib.sha1
digest_size = sha().digest_size
# l - number of output blocks to produce
l = keylen // digest_size
if keylen % digest_size != 0:
l += 1
h = hmac.new( passwd, None, sha )
T = b""
for i in range(1, l+1):
T += pbkdf2_F( h, salt, iter, i )
return T[0: keylen]

View file

@ -1,12 +1,8 @@
#!/usr/bin/env python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from __future__ import with_statement
# androidkindlekey.py
# Copyright © 2013-15 by Thom and Apprentice Harper
# Some portions Copyright © 2010-15 by some_updates and Apprentice Alf
#
# Copyright © 2010-22 by Thom, Apprentice Harper et al.
# Revision history:
# 1.0 - AmazonSecureStorage.xml decryption to serial number
@ -17,13 +13,15 @@ from __future__ import with_statement
# 1.3 - added in TkInter interface, output to a file
# 1.4 - Fix some problems identified by Aldo Bleeker
# 1.5 - Fix another problem identified by Aldo Bleeker
# 2.0 - Python 3 compatibility
# 2.1 - Remove OpenSSL support; only support PyCryptodome
"""
Retrieve Kindle for Android Serial Number.
"""
__license__ = 'GPL v3'
__version__ = '1.5'
__version__ = '2.1'
import os
import sys
@ -33,78 +31,37 @@ import tempfile
import zlib
import tarfile
from hashlib import md5
from cStringIO import StringIO
from io import BytesIO
from binascii import a2b_hex, b2a_hex
try:
from Cryptodome.Cipher import AES, DES
except ImportError:
from Crypto.Cipher import AES, DES
# Routines common to Mac and PC
# 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,unicode):
data = data.encode(self.encoding,"replace")
self.stream.write(data)
self.stream.flush()
def __getattr__(self, attr):
return getattr(self.stream, attr)
try:
from calibre.constants import iswindows, isosx
except:
iswindows = sys.platform.startswith('win')
isosx = sys.platform.startswith('darwin')
def unicode_argv():
if iswindows:
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
# strings.
# Versions 2.x of Python don't support Unicode in sys.argv on
# Windows, with the underlying Windows API instead replacing multi-byte
# characters with '?'. So use shell32.GetCommandLineArgvW to get sys.argv
# as a list of Unicode strings and encode them as utf-8
from ctypes import POINTER, byref, cdll, c_int, windll
from ctypes.wintypes import LPCWSTR, LPWSTR
GetCommandLineW = cdll.kernel32.GetCommandLineW
GetCommandLineW.argtypes = []
GetCommandLineW.restype = LPCWSTR
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
CommandLineToArgvW.restype = POINTER(LPWSTR)
cmd = GetCommandLineW()
argc = c_int(0)
argv = CommandLineToArgvW(cmd, byref(argc))
if argc.value > 0:
# Remove Python executable and commands if present
start = argc.value - len(sys.argv)
return [argv[i] for i in
xrange(start, argc.value)]
# if we don't have any arguments at all, just pass back script name
# this should never happen
return [u"kindlekey.py"]
else:
argvencoding = sys.stdin.encoding
if argvencoding == None:
argvencoding = "utf-8"
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
class DrmException(Exception):
pass
STORAGE = u"backup.ab"
STORAGE1 = u"AmazonSecureStorage.xml"
STORAGE2 = u"map_data_storage.db"
STORAGE = "backup.ab"
STORAGE1 = "AmazonSecureStorage.xml"
STORAGE2 = "map_data_storage.db"
def unpad(data, padding=16):
if sys.version_info[0] == 2:
pad_len = ord(data[-1])
else:
pad_len = data[-1]
return data[:-pad_len]
def pad(data, padding_len=16):
padding_data_len = padding_len - (len(data) % padding_len)
plaintext = data + chr(padding_data_len) * padding_data_len
return plaintext
class AndroidObfuscation(object):
'''AndroidObfuscation
@ -113,31 +70,23 @@ class AndroidObfuscation(object):
key = a2b_hex('0176e04c9408b1702d90be333fd53523')
def _get_cipher(self):
return AES.new(self.key, AES.MODE_ECB)
def encrypt(self, plaintext):
cipher = self._get_cipher()
padding = len(self.key) - len(plaintext) % len(self.key)
plaintext += chr(padding) * padding
return b2a_hex(cipher.encrypt(plaintext))
pt = pad(plaintext.encode('utf-8'), 16)
return b2a_hex(self._get_cipher().encrypt(pt))
def decrypt(self, ciphertext):
cipher = self._get_cipher()
plaintext = cipher.decrypt(a2b_hex(ciphertext))
return plaintext[:-ord(plaintext[-1])]
def _get_cipher(self):
try:
from Crypto.Cipher import AES
return AES.new(self.key)
except ImportError:
from aescbc import AES, noPadding
return AES(self.key, padding=noPadding())
ct = a2b_hex(ciphertext)
return unpad(self._get_cipher().decrypt(ct), 16)
class AndroidObfuscationV2(AndroidObfuscation):
'''AndroidObfuscationV2
'''
count = 503
password = 'Thomsun was here!'
password = b'Thomsun was here!'
def __init__(self, salt):
key = self.password + salt
@ -147,12 +96,7 @@ class AndroidObfuscationV2(AndroidObfuscation):
self.iv = key[8:16]
def _get_cipher(self):
try :
from Crypto.Cipher import DES
return DES.new(self.key, DES.MODE_CBC, self.iv)
except ImportError:
from python_des import Des, CBC
return Des(self.key, CBC, self.iv)
return DES.new(self.key, DES.MODE_CBC, self.iv)
def parse_preference(path):
''' parse android's shared preference xml '''
@ -199,6 +143,7 @@ def get_serials1(path=STORAGE1):
try:
tokens = set(get_value('kindle.account.tokens').split(','))
except:
sys.stderr.write('cannot get kindle account tokens\n')
return []
serials = []
@ -218,17 +163,16 @@ def get_serials2(path=STORAGE2):
import sqlite3
connection = sqlite3.connect(path)
cursor = connection.cursor()
cursor.execute('''select userdata_value from userdata where userdata_key like '%/%token.device.deviceserialname%' ''')
userdata_keys = cursor.fetchall()
cursor.execute('''select device_data_value from device_data where device_data_key like '%serial.number%' ''')
device_data_keys = cursor.fetchall()
dsns = []
for userdata_row in userdata_keys:
for device_data_row in device_data_keys:
try:
if userdata_row and userdata_row[0]:
userdata_utf8 = userdata_row[0].encode('utf8')
if len(userdata_utf8) > 0:
dsns.append(userdata_utf8)
if device_data_row and device_data_row[0]:
if len(device_data_row[0]) > 0:
dsns.append(device_data_row[0])
except:
print "Error getting one of the device serial name keys"
print("Error getting one of the device serial name keys")
traceback.print_exc()
pass
dsns = list(set(dsns))
@ -239,22 +183,27 @@ def get_serials2(path=STORAGE2):
for userdata_row in userdata_keys:
try:
if userdata_row and userdata_row[0]:
userdata_utf8 = userdata_row[0].encode('utf8')
if len(userdata_utf8) > 0:
tokens.append(userdata_utf8)
if len(userdata_row[0]) > 0:
if ',' in userdata_row[0]:
splits = userdata_row[0].split(',')
for split in splits:
tokens.append(split)
tokens.append(userdata_row[0])
except:
print "Error getting one of the account token keys"
print("Error getting one of the account token keys")
traceback.print_exc()
pass
tokens = list(set(tokens))
serials = []
for x in dsns:
serials.append(x)
for y in tokens:
serials.append('%s%s' % (x, y))
for y in tokens:
serials.append(y)
serials.append(y)
serials.append(x+y)
connection.close()
return serials
def get_serials(path=STORAGE):
@ -276,8 +225,8 @@ def get_serials(path=STORAGE):
try :
read = open(path, 'rb')
head = read.read(24)
if head[:14] == 'ANDROID BACKUP':
output = StringIO(zlib.decompress(read.read()))
if head[:14] == b'ANDROID BACKUP':
output = BytesIO(zlib.decompress(read.read()))
except Exception:
pass
finally:
@ -312,7 +261,7 @@ __all__ = [ 'get_serials', 'getkey']
def getkey(outfile, inpath):
keys = get_serials(inpath)
if len(keys) > 0:
with file(outfile, 'w') as keyfileout:
with open(outfile, 'w') as keyfileout:
for key in keys:
keyfileout.write(key)
keyfileout.write("\n")
@ -321,27 +270,25 @@ def getkey(outfile, inpath):
def usage(progname):
print u"Decrypts the serial number(s) of Kindle For Android from Android backup or file"
print u"Get backup.ab file using adb backup com.amazon.kindle for Android 4.0+."
print u"Otherwise extract AmazonSecureStorage.xml from /data/data/com.amazon.kindle/shared_prefs/AmazonSecureStorage.xml"
print u"Or map_data_storage.db from /data/data/com.amazon.kindle/databases/map_data_storage.db"
print u""
print u"Usage:"
print u" {0:s} [-h] [-b <backup.ab>] [<outfile.k4a>]".format(progname)
print("Decrypts the serial number(s) of Kindle For Android from Android backup or file")
print("Get backup.ab file using adb backup com.amazon.kindle for Android 4.0+.")
print("Otherwise extract AmazonSecureStorage.xml from /data/data/com.amazon.kindle/shared_prefs/AmazonSecureStorage.xml")
print("Or map_data_storage.db from /data/data/com.amazon.kindle/databases/map_data_storage.db")
print("")
print("Usage:")
print(" {0:s} [-h] [-b <backup.ab>] [<outfile.k4a>]".format(progname))
def cli_main():
sys.stdout=SafeUnbuffered(sys.stdout)
sys.stderr=SafeUnbuffered(sys.stderr)
argv=unicode_argv()
argv=sys.argv
progname = os.path.basename(argv[0])
print u"{0} v{1}\nCopyright © 2010-2015 Thom, some_updates, Apprentice Alf and Apprentice Harper".format(progname,__version__)
print("{0} v{1}\nCopyright © 2010-2020 Thom, Apprentice Harper et al.".format(progname,__version__))
try:
opts, args = getopt.getopt(argv[1:], "hb:")
except getopt.GetoptError, err:
except getopt.GetoptError as err:
usage(progname)
print u"\nError in options or arguments: {0}".format(err.args[0])
print("\nError in options or arguments: {0}".format(err.args[0]))
return 2
inpath = ""
@ -373,92 +320,92 @@ def cli_main():
if not os.path.isfile(inpath):
usage(progname)
print u"\n{0:s} file not found".format(inpath)
print("\n{0:s} file not found".format(inpath))
return 2
if getkey(outfile, inpath):
print u"\nSaved Kindle for Android key to {0}".format(outfile)
print("\nSaved Kindle for Android key to {0}".format(outfile))
else:
print u"\nCould not retrieve Kindle for Android key."
print("\nCould not retrieve Kindle for Android key.")
return 0
def gui_main():
try:
import Tkinter
import Tkconstants
import tkMessageBox
import tkFileDialog
import tkinter
import tkinter.constants
import tkinter.messagebox
import tkinter.filedialog
except:
print "Tkinter not installed"
return cli_main()
print("tkinter not installed")
return 0
class DecryptionDialog(Tkinter.Frame):
class DecryptionDialog(tkinter.Frame):
def __init__(self, root):
Tkinter.Frame.__init__(self, root, border=5)
self.status = Tkinter.Label(self, text=u"Select backup.ab file")
self.status.pack(fill=Tkconstants.X, expand=1)
body = Tkinter.Frame(self)
body.pack(fill=Tkconstants.X, expand=1)
sticky = Tkconstants.E + Tkconstants.W
tkinter.Frame.__init__(self, root, border=5)
self.status = tkinter.Label(self, text="Select backup.ab file")
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=u"Backup file").grid(row=0, column=0)
self.keypath = Tkinter.Entry(body, width=40)
tkinter.Label(body, text="Backup file").grid(row=0, column=0)
self.keypath = tkinter.Entry(body, width=40)
self.keypath.grid(row=0, column=1, sticky=sticky)
self.keypath.insert(2, u"backup.ab")
button = Tkinter.Button(body, text=u"...", command=self.get_keypath)
self.keypath.insert(2, "backup.ab")
button = tkinter.Button(body, text="...", command=self.get_keypath)
button.grid(row=0, column=2)
buttons = Tkinter.Frame(self)
buttons = tkinter.Frame(self)
buttons.pack()
button2 = Tkinter.Button(
buttons, text=u"Extract", width=10, command=self.generate)
button2.pack(side=Tkconstants.LEFT)
Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT)
button3 = Tkinter.Button(
buttons, text=u"Quit", width=10, command=self.quit)
button3.pack(side=Tkconstants.RIGHT)
button2 = tkinter.Button(
buttons, text="Extract", width=10, command=self.generate)
button2.pack(side=tkinter.constants.LEFT)
tkinter.Frame(buttons, width=10).pack(side=tkinter.constants.LEFT)
button3 = tkinter.Button(
buttons, text="Quit", width=10, command=self.quit)
button3.pack(side=tkinter.constants.RIGHT)
def get_keypath(self):
keypath = tkFileDialog.askopenfilename(
parent=None, title=u"Select backup.ab file",
defaultextension=u".ab",
keypath = tkinter.filedialog.askopenfilename(
parent=None, title="Select backup.ab file",
defaultextension=".ab",
filetypes=[('adb backup com.amazon.kindle', '.ab'),
('All Files', '.*')])
if keypath:
keypath = os.path.normpath(keypath)
self.keypath.delete(0, Tkconstants.END)
self.keypath.delete(0, tkinter.constants.END)
self.keypath.insert(0, keypath)
return
def generate(self):
inpath = self.keypath.get()
self.status['text'] = u"Getting key..."
self.status['text'] = "Getting key..."
try:
keys = get_serials(inpath)
keycount = 0
for key in keys:
while True:
keycount += 1
outfile = os.path.join(progpath,u"kindlekey{0:d}.k4a".format(keycount))
outfile = os.path.join(progpath,"kindlekey{0:d}.k4a".format(keycount))
if not os.path.exists(outfile):
break
with file(outfile, 'w') as keyfileout:
with open(outfile, 'w') as keyfileout:
keyfileout.write(key)
success = True
tkMessageBox.showinfo(progname, u"Key successfully retrieved to {0}".format(outfile))
except Exception, e:
self.status['text'] = u"Error: {0}".format(e.args[0])
tkinter.messagebox.showinfo(progname, "Key successfully retrieved to {0}".format(outfile))
except Exception as e:
self.status['text'] = "Error: {0}".format(e.args[0])
return
self.status['text'] = u"Select backup.ab file"
self.status['text'] = "Select backup.ab file"
argv=unicode_argv()
argv=sys.argv()
progpath, progname = os.path.split(argv[0])
root = Tkinter.Tk()
root.title(u"Kindle for Android Key Extraction v.{0}".format(__version__))
root = tkinter.Tk()
root.title("Kindle for Android Key Extraction v.{0}".format(__version__))
root.resizable(True, False)
root.minsize(300, 0)
DecryptionDialog(root).pack(fill=Tkconstants.X, expand=1)
DecryptionDialog(root).pack(fill=tkinter.constants.X, expand=1)
root.mainloop()
return 0

View file

@ -0,0 +1,48 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
# get sys.argv arguments and encode them into utf-8
def unicode_argv(default_name):
try:
from calibre.constants import iswindows
except:
iswindows = sys.platform.startswith('win')
if iswindows:
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
# strings.
# Versions 2.x of Python don't support Unicode in sys.argv on
# Windows, with the underlying Windows API instead replacing multi-byte
# characters with '?'.
from ctypes import POINTER, byref, cdll, c_int, windll
from ctypes.wintypes import LPCWSTR, LPWSTR
GetCommandLineW = cdll.kernel32.GetCommandLineW
GetCommandLineW.argtypes = []
GetCommandLineW.restype = LPCWSTR
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
CommandLineToArgvW.restype = POINTER(LPWSTR)
cmd = GetCommandLineW()
argc = c_int(0)
argv = CommandLineToArgvW(cmd, byref(argc))
if argc.value > 0:
# Remove Python executable and commands if present
start = argc.value - len(sys.argv)
return [argv[i] for i in
range(start, argc.value)]
# if we don't have any arguments at all, just pass back script name
# this should never happen
return [ default_name ]
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]

1549
DeDRM_plugin/config.py Executable file

File diff suppressed because it is too large Load diff

View file

@ -1,24 +1,20 @@
#! /usr/bin/python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
# For use with Topaz Scripts Version 2.6
class Unbuffered:
def __init__(self, stream):
self.stream = stream
def write(self, data):
self.stream.write(data)
self.stream.flush()
def __getattr__(self, attr):
return getattr(self.stream, attr)
# For use with Topaz Scripts Version 2.6
# Python 3, September 2020
#@@CALIBRE_COMPAT_CODE@@
from .utilities import SafeUnbuffered
import sys
sys.stdout=Unbuffered(sys.stdout)
import csv
import os
import getopt
from struct import pack
from struct import unpack
from struct import pack, unpack
class TpzDRMError(Exception):
pass
@ -46,7 +42,7 @@ def readEncodedNumber(file):
c = file.read(1)
if (len(c) == 0):
return None
data = ord(c)
data = c[0]
datax = (datax <<7) + (data & 0x7F)
data = datax
@ -106,7 +102,7 @@ def readString(file):
def convert(i):
result = ''
val = encodeNumber(i)
for j in xrange(len(val)):
for j in range(len(val)):
c = ord(val[j:j+1])
result += '%02x' % c
return result
@ -120,10 +116,10 @@ class Dictionary(object):
def __init__(self, dictFile):
self.filename = dictFile
self.size = 0
self.fo = file(dictFile,'rb')
self.fo = open(dictFile,'rb')
self.stable = []
self.size = readEncodedNumber(self.fo)
for i in xrange(self.size):
for i in range(self.size):
self.stable.append(self.escapestr(readString(self.fo)))
self.pos = 0
@ -139,7 +135,7 @@ class Dictionary(object):
self.pos = val
return self.stable[self.pos]
else:
print "Error - %d outside of string table limits" % val
print("Error - %d outside of string table limits" % val)
raise TpzDRMError('outside of string table limits')
# sys.exit(-1)
@ -150,8 +146,8 @@ class Dictionary(object):
return self.pos
def dumpDict(self):
for i in xrange(self.size):
print "%d %s %s" % (i, convert(i), self.stable[i])
for i in range(self.size):
print("%d %s %s" % (i, convert(i), self.stable[i]))
return
# parses the xml snippets that are represented by each page*.dat file.
@ -160,7 +156,7 @@ class Dictionary(object):
class PageParser(object):
def __init__(self, filename, dict, debug, flat_xml):
self.fo = file(filename,'rb')
self.fo = open(filename,'rb')
self.id = os.path.basename(filename).replace('.dat','')
self.dict = dict
self.debug = debug
@ -178,232 +174,232 @@ class PageParser(object):
# tag : (number of arguments, argument type, subtags present, special case of subtags presents when escaped)
token_tags = {
'x' : (1, 'scalar_number', 0, 0),
'y' : (1, 'scalar_number', 0, 0),
'h' : (1, 'scalar_number', 0, 0),
'w' : (1, 'scalar_number', 0, 0),
'firstWord' : (1, 'scalar_number', 0, 0),
'lastWord' : (1, 'scalar_number', 0, 0),
'rootID' : (1, 'scalar_number', 0, 0),
'stemID' : (1, 'scalar_number', 0, 0),
'type' : (1, 'scalar_text', 0, 0),
b'x' : (1, 'scalar_number', 0, 0),
b'y' : (1, 'scalar_number', 0, 0),
b'h' : (1, 'scalar_number', 0, 0),
b'w' : (1, 'scalar_number', 0, 0),
b'firstWord' : (1, 'scalar_number', 0, 0),
b'lastWord' : (1, 'scalar_number', 0, 0),
b'rootID' : (1, 'scalar_number', 0, 0),
b'stemID' : (1, 'scalar_number', 0, 0),
b'type' : (1, 'scalar_text', 0, 0),
'info' : (0, 'number', 1, 0),
b'info' : (0, 'number', 1, 0),
'info.word' : (0, 'number', 1, 1),
'info.word.ocrText' : (1, 'text', 0, 0),
'info.word.firstGlyph' : (1, 'raw', 0, 0),
'info.word.lastGlyph' : (1, 'raw', 0, 0),
'info.word.bl' : (1, 'raw', 0, 0),
'info.word.link_id' : (1, 'number', 0, 0),
b'info.word' : (0, 'number', 1, 1),
b'info.word.ocrText' : (1, 'text', 0, 0),
b'info.word.firstGlyph' : (1, 'raw', 0, 0),
b'info.word.lastGlyph' : (1, 'raw', 0, 0),
b'info.word.bl' : (1, 'raw', 0, 0),
b'info.word.link_id' : (1, 'number', 0, 0),
'glyph' : (0, 'number', 1, 1),
'glyph.x' : (1, 'number', 0, 0),
'glyph.y' : (1, 'number', 0, 0),
'glyph.glyphID' : (1, 'number', 0, 0),
b'glyph' : (0, 'number', 1, 1),
b'glyph.x' : (1, 'number', 0, 0),
b'glyph.y' : (1, 'number', 0, 0),
b'glyph.glyphID' : (1, 'number', 0, 0),
'dehyphen' : (0, 'number', 1, 1),
'dehyphen.rootID' : (1, 'number', 0, 0),
'dehyphen.stemID' : (1, 'number', 0, 0),
'dehyphen.stemPage' : (1, 'number', 0, 0),
'dehyphen.sh' : (1, 'number', 0, 0),
b'dehyphen' : (0, 'number', 1, 1),
b'dehyphen.rootID' : (1, 'number', 0, 0),
b'dehyphen.stemID' : (1, 'number', 0, 0),
b'dehyphen.stemPage' : (1, 'number', 0, 0),
b'dehyphen.sh' : (1, 'number', 0, 0),
'links' : (0, 'number', 1, 1),
'links.page' : (1, 'number', 0, 0),
'links.rel' : (1, 'number', 0, 0),
'links.row' : (1, 'number', 0, 0),
'links.title' : (1, 'text', 0, 0),
'links.href' : (1, 'text', 0, 0),
'links.type' : (1, 'text', 0, 0),
'links.id' : (1, 'number', 0, 0),
b'links' : (0, 'number', 1, 1),
b'links.page' : (1, 'number', 0, 0),
b'links.rel' : (1, 'number', 0, 0),
b'links.row' : (1, 'number', 0, 0),
b'links.title' : (1, 'text', 0, 0),
b'links.href' : (1, 'text', 0, 0),
b'links.type' : (1, 'text', 0, 0),
b'links.id' : (1, 'number', 0, 0),
'paraCont' : (0, 'number', 1, 1),
'paraCont.rootID' : (1, 'number', 0, 0),
'paraCont.stemID' : (1, 'number', 0, 0),
'paraCont.stemPage' : (1, 'number', 0, 0),
b'paraCont' : (0, 'number', 1, 1),
b'paraCont.rootID' : (1, 'number', 0, 0),
b'paraCont.stemID' : (1, 'number', 0, 0),
b'paraCont.stemPage' : (1, 'number', 0, 0),
'paraStems' : (0, 'number', 1, 1),
'paraStems.stemID' : (1, 'number', 0, 0),
b'paraStems' : (0, 'number', 1, 1),
b'paraStems.stemID' : (1, 'number', 0, 0),
'wordStems' : (0, 'number', 1, 1),
'wordStems.stemID' : (1, 'number', 0, 0),
b'wordStems' : (0, 'number', 1, 1),
b'wordStems.stemID' : (1, 'number', 0, 0),
'empty' : (1, 'snippets', 1, 0),
b'empty' : (1, 'snippets', 1, 0),
'page' : (1, 'snippets', 1, 0),
'page.class' : (1, 'scalar_text', 0, 0),
'page.pageid' : (1, 'scalar_text', 0, 0),
'page.pagelabel' : (1, 'scalar_text', 0, 0),
'page.type' : (1, 'scalar_text', 0, 0),
'page.h' : (1, 'scalar_number', 0, 0),
'page.w' : (1, 'scalar_number', 0, 0),
'page.startID' : (1, 'scalar_number', 0, 0),
b'page' : (1, 'snippets', 1, 0),
b'page.class' : (1, 'scalar_text', 0, 0),
b'page.pageid' : (1, 'scalar_text', 0, 0),
b'page.pagelabel' : (1, 'scalar_text', 0, 0),
b'page.type' : (1, 'scalar_text', 0, 0),
b'page.h' : (1, 'scalar_number', 0, 0),
b'page.w' : (1, 'scalar_number', 0, 0),
b'page.startID' : (1, 'scalar_number', 0, 0),
'group' : (1, 'snippets', 1, 0),
'group.class' : (1, 'scalar_text', 0, 0),
'group.type' : (1, 'scalar_text', 0, 0),
'group._tag' : (1, 'scalar_text', 0, 0),
'group.orientation': (1, 'scalar_text', 0, 0),
b'group' : (1, 'snippets', 1, 0),
b'group.class' : (1, 'scalar_text', 0, 0),
b'group.type' : (1, 'scalar_text', 0, 0),
b'group._tag' : (1, 'scalar_text', 0, 0),
b'group.orientation': (1, 'scalar_text', 0, 0),
'region' : (1, 'snippets', 1, 0),
'region.class' : (1, 'scalar_text', 0, 0),
'region.type' : (1, 'scalar_text', 0, 0),
'region.x' : (1, 'scalar_number', 0, 0),
'region.y' : (1, 'scalar_number', 0, 0),
'region.h' : (1, 'scalar_number', 0, 0),
'region.w' : (1, 'scalar_number', 0, 0),
'region.orientation' : (1, 'scalar_text', 0, 0),
b'region' : (1, 'snippets', 1, 0),
b'region.class' : (1, 'scalar_text', 0, 0),
b'region.type' : (1, 'scalar_text', 0, 0),
b'region.x' : (1, 'scalar_number', 0, 0),
b'region.y' : (1, 'scalar_number', 0, 0),
b'region.h' : (1, 'scalar_number', 0, 0),
b'region.w' : (1, 'scalar_number', 0, 0),
b'region.orientation' : (1, 'scalar_text', 0, 0),
'empty_text_region' : (1, 'snippets', 1, 0),
b'empty_text_region' : (1, 'snippets', 1, 0),
'img' : (1, 'snippets', 1, 0),
'img.x' : (1, 'scalar_number', 0, 0),
'img.y' : (1, 'scalar_number', 0, 0),
'img.h' : (1, 'scalar_number', 0, 0),
'img.w' : (1, 'scalar_number', 0, 0),
'img.src' : (1, 'scalar_number', 0, 0),
'img.color_src' : (1, 'scalar_number', 0, 0),
'img.gridSize' : (1, 'scalar_number', 0, 0),
'img.gridBottomCenter' : (1, 'scalar_number', 0, 0),
'img.gridTopCenter' : (1, 'scalar_number', 0, 0),
'img.gridBeginCenter' : (1, 'scalar_number', 0, 0),
'img.gridEndCenter' : (1, 'scalar_number', 0, 0),
'img.image_type' : (1, 'scalar_number', 0, 0),
b'img' : (1, 'snippets', 1, 0),
b'img.x' : (1, 'scalar_number', 0, 0),
b'img.y' : (1, 'scalar_number', 0, 0),
b'img.h' : (1, 'scalar_number', 0, 0),
b'img.w' : (1, 'scalar_number', 0, 0),
b'img.src' : (1, 'scalar_number', 0, 0),
b'img.color_src' : (1, 'scalar_number', 0, 0),
b'img.gridSize' : (1, 'scalar_number', 0, 0),
b'img.gridBottomCenter' : (1, 'scalar_number', 0, 0),
b'img.gridTopCenter' : (1, 'scalar_number', 0, 0),
b'img.gridBeginCenter' : (1, 'scalar_number', 0, 0),
b'img.gridEndCenter' : (1, 'scalar_number', 0, 0),
b'img.image_type' : (1, 'scalar_number', 0, 0),
'paragraph' : (1, 'snippets', 1, 0),
'paragraph.class' : (1, 'scalar_text', 0, 0),
'paragraph.firstWord' : (1, 'scalar_number', 0, 0),
'paragraph.lastWord' : (1, 'scalar_number', 0, 0),
'paragraph.lastWord' : (1, 'scalar_number', 0, 0),
'paragraph.gridSize' : (1, 'scalar_number', 0, 0),
'paragraph.gridBottomCenter' : (1, 'scalar_number', 0, 0),
'paragraph.gridTopCenter' : (1, 'scalar_number', 0, 0),
'paragraph.gridBeginCenter' : (1, 'scalar_number', 0, 0),
'paragraph.gridEndCenter' : (1, 'scalar_number', 0, 0),
b'paragraph' : (1, 'snippets', 1, 0),
b'paragraph.class' : (1, 'scalar_text', 0, 0),
b'paragraph.firstWord' : (1, 'scalar_number', 0, 0),
b'paragraph.lastWord' : (1, 'scalar_number', 0, 0),
b'paragraph.lastWord' : (1, 'scalar_number', 0, 0),
b'paragraph.gridSize' : (1, 'scalar_number', 0, 0),
b'paragraph.gridBottomCenter' : (1, 'scalar_number', 0, 0),
b'paragraph.gridTopCenter' : (1, 'scalar_number', 0, 0),
b'paragraph.gridBeginCenter' : (1, 'scalar_number', 0, 0),
b'paragraph.gridEndCenter' : (1, 'scalar_number', 0, 0),
'word_semantic' : (1, 'snippets', 1, 1),
'word_semantic.type' : (1, 'scalar_text', 0, 0),
'word_semantic.class' : (1, 'scalar_text', 0, 0),
'word_semantic.firstWord' : (1, 'scalar_number', 0, 0),
'word_semantic.lastWord' : (1, 'scalar_number', 0, 0),
'word_semantic.gridBottomCenter' : (1, 'scalar_number', 0, 0),
'word_semantic.gridTopCenter' : (1, 'scalar_number', 0, 0),
'word_semantic.gridBeginCenter' : (1, 'scalar_number', 0, 0),
'word_semantic.gridEndCenter' : (1, 'scalar_number', 0, 0),
b'word_semantic' : (1, 'snippets', 1, 1),
b'word_semantic.type' : (1, 'scalar_text', 0, 0),
b'word_semantic.class' : (1, 'scalar_text', 0, 0),
b'word_semantic.firstWord' : (1, 'scalar_number', 0, 0),
b'word_semantic.lastWord' : (1, 'scalar_number', 0, 0),
b'word_semantic.gridBottomCenter' : (1, 'scalar_number', 0, 0),
b'word_semantic.gridTopCenter' : (1, 'scalar_number', 0, 0),
b'word_semantic.gridBeginCenter' : (1, 'scalar_number', 0, 0),
b'word_semantic.gridEndCenter' : (1, 'scalar_number', 0, 0),
'word' : (1, 'snippets', 1, 0),
'word.type' : (1, 'scalar_text', 0, 0),
'word.class' : (1, 'scalar_text', 0, 0),
'word.firstGlyph' : (1, 'scalar_number', 0, 0),
'word.lastGlyph' : (1, 'scalar_number', 0, 0),
b'word' : (1, 'snippets', 1, 0),
b'word.type' : (1, 'scalar_text', 0, 0),
b'word.class' : (1, 'scalar_text', 0, 0),
b'word.firstGlyph' : (1, 'scalar_number', 0, 0),
b'word.lastGlyph' : (1, 'scalar_number', 0, 0),
'_span' : (1, 'snippets', 1, 0),
'_span.class' : (1, 'scalar_text', 0, 0),
'_span.firstWord' : (1, 'scalar_number', 0, 0),
'_span.lastWord' : (1, 'scalar_number', 0, 0),
'_span.gridSize' : (1, 'scalar_number', 0, 0),
'_span.gridBottomCenter' : (1, 'scalar_number', 0, 0),
'_span.gridTopCenter' : (1, 'scalar_number', 0, 0),
'_span.gridBeginCenter' : (1, 'scalar_number', 0, 0),
'_span.gridEndCenter' : (1, 'scalar_number', 0, 0),
b'_span' : (1, 'snippets', 1, 0),
b'_span.class' : (1, 'scalar_text', 0, 0),
b'_span.firstWord' : (1, 'scalar_number', 0, 0),
b'_span.lastWord' : (1, 'scalar_number', 0, 0),
b'_span.gridSize' : (1, 'scalar_number', 0, 0),
b'_span.gridBottomCenter' : (1, 'scalar_number', 0, 0),
b'_span.gridTopCenter' : (1, 'scalar_number', 0, 0),
b'_span.gridBeginCenter' : (1, 'scalar_number', 0, 0),
b'_span.gridEndCenter' : (1, 'scalar_number', 0, 0),
'span' : (1, 'snippets', 1, 0),
'span.firstWord' : (1, 'scalar_number', 0, 0),
'span.lastWord' : (1, 'scalar_number', 0, 0),
'span.gridSize' : (1, 'scalar_number', 0, 0),
'span.gridBottomCenter' : (1, 'scalar_number', 0, 0),
'span.gridTopCenter' : (1, 'scalar_number', 0, 0),
'span.gridBeginCenter' : (1, 'scalar_number', 0, 0),
'span.gridEndCenter' : (1, 'scalar_number', 0, 0),
b'span' : (1, 'snippets', 1, 0),
b'span.firstWord' : (1, 'scalar_number', 0, 0),
b'span.lastWord' : (1, 'scalar_number', 0, 0),
b'span.gridSize' : (1, 'scalar_number', 0, 0),
b'span.gridBottomCenter' : (1, 'scalar_number', 0, 0),
b'span.gridTopCenter' : (1, 'scalar_number', 0, 0),
b'span.gridBeginCenter' : (1, 'scalar_number', 0, 0),
b'span.gridEndCenter' : (1, 'scalar_number', 0, 0),
'extratokens' : (1, 'snippets', 1, 0),
'extratokens.class' : (1, 'scalar_text', 0, 0),
'extratokens.type' : (1, 'scalar_text', 0, 0),
'extratokens.firstGlyph' : (1, 'scalar_number', 0, 0),
'extratokens.lastGlyph' : (1, 'scalar_number', 0, 0),
'extratokens.gridSize' : (1, 'scalar_number', 0, 0),
'extratokens.gridBottomCenter' : (1, 'scalar_number', 0, 0),
'extratokens.gridTopCenter' : (1, 'scalar_number', 0, 0),
'extratokens.gridBeginCenter' : (1, 'scalar_number', 0, 0),
'extratokens.gridEndCenter' : (1, 'scalar_number', 0, 0),
b'extratokens' : (1, 'snippets', 1, 0),
b'extratokens.class' : (1, 'scalar_text', 0, 0),
b'extratokens.type' : (1, 'scalar_text', 0, 0),
b'extratokens.firstGlyph' : (1, 'scalar_number', 0, 0),
b'extratokens.lastGlyph' : (1, 'scalar_number', 0, 0),
b'extratokens.gridSize' : (1, 'scalar_number', 0, 0),
b'extratokens.gridBottomCenter' : (1, 'scalar_number', 0, 0),
b'extratokens.gridTopCenter' : (1, 'scalar_number', 0, 0),
b'extratokens.gridBeginCenter' : (1, 'scalar_number', 0, 0),
b'extratokens.gridEndCenter' : (1, 'scalar_number', 0, 0),
'glyph.h' : (1, 'number', 0, 0),
'glyph.w' : (1, 'number', 0, 0),
'glyph.use' : (1, 'number', 0, 0),
'glyph.vtx' : (1, 'number', 0, 1),
'glyph.len' : (1, 'number', 0, 1),
'glyph.dpi' : (1, 'number', 0, 0),
'vtx' : (0, 'number', 1, 1),
'vtx.x' : (1, 'number', 0, 0),
'vtx.y' : (1, 'number', 0, 0),
'len' : (0, 'number', 1, 1),
'len.n' : (1, 'number', 0, 0),
b'glyph.h' : (1, 'number', 0, 0),
b'glyph.w' : (1, 'number', 0, 0),
b'glyph.use' : (1, 'number', 0, 0),
b'glyph.vtx' : (1, 'number', 0, 1),
b'glyph.len' : (1, 'number', 0, 1),
b'glyph.dpi' : (1, 'number', 0, 0),
b'vtx' : (0, 'number', 1, 1),
b'vtx.x' : (1, 'number', 0, 0),
b'vtx.y' : (1, 'number', 0, 0),
b'len' : (0, 'number', 1, 1),
b'len.n' : (1, 'number', 0, 0),
'book' : (1, 'snippets', 1, 0),
'version' : (1, 'snippets', 1, 0),
'version.FlowEdit_1_id' : (1, 'scalar_text', 0, 0),
'version.FlowEdit_1_version' : (1, 'scalar_text', 0, 0),
'version.Schema_id' : (1, 'scalar_text', 0, 0),
'version.Schema_version' : (1, 'scalar_text', 0, 0),
'version.Topaz_version' : (1, 'scalar_text', 0, 0),
'version.WordDetailEdit_1_id' : (1, 'scalar_text', 0, 0),
'version.WordDetailEdit_1_version' : (1, 'scalar_text', 0, 0),
'version.ZoneEdit_1_id' : (1, 'scalar_text', 0, 0),
'version.ZoneEdit_1_version' : (1, 'scalar_text', 0, 0),
'version.chapterheaders' : (1, 'scalar_text', 0, 0),
'version.creation_date' : (1, 'scalar_text', 0, 0),
'version.header_footer' : (1, 'scalar_text', 0, 0),
'version.init_from_ocr' : (1, 'scalar_text', 0, 0),
'version.letter_insertion' : (1, 'scalar_text', 0, 0),
'version.xmlinj_convert' : (1, 'scalar_text', 0, 0),
'version.xmlinj_reflow' : (1, 'scalar_text', 0, 0),
'version.xmlinj_transform' : (1, 'scalar_text', 0, 0),
'version.findlists' : (1, 'scalar_text', 0, 0),
'version.page_num' : (1, 'scalar_text', 0, 0),
'version.page_type' : (1, 'scalar_text', 0, 0),
'version.bad_text' : (1, 'scalar_text', 0, 0),
'version.glyph_mismatch' : (1, 'scalar_text', 0, 0),
'version.margins' : (1, 'scalar_text', 0, 0),
'version.staggered_lines' : (1, 'scalar_text', 0, 0),
'version.paragraph_continuation' : (1, 'scalar_text', 0, 0),
'version.toc' : (1, 'scalar_text', 0, 0),
b'book' : (1, 'snippets', 1, 0),
b'version' : (1, 'snippets', 1, 0),
b'version.FlowEdit_1_id' : (1, 'scalar_text', 0, 0),
b'version.FlowEdit_1_version' : (1, 'scalar_text', 0, 0),
b'version.Schema_id' : (1, 'scalar_text', 0, 0),
b'version.Schema_version' : (1, 'scalar_text', 0, 0),
b'version.Topaz_version' : (1, 'scalar_text', 0, 0),
b'version.WordDetailEdit_1_id' : (1, 'scalar_text', 0, 0),
b'version.WordDetailEdit_1_version' : (1, 'scalar_text', 0, 0),
b'version.ZoneEdit_1_id' : (1, 'scalar_text', 0, 0),
b'version.ZoneEdit_1_version' : (1, 'scalar_text', 0, 0),
b'version.chapterheaders' : (1, 'scalar_text', 0, 0),
b'version.creation_date' : (1, 'scalar_text', 0, 0),
b'version.header_footer' : (1, 'scalar_text', 0, 0),
b'version.init_from_ocr' : (1, 'scalar_text', 0, 0),
b'version.letter_insertion' : (1, 'scalar_text', 0, 0),
b'version.xmlinj_convert' : (1, 'scalar_text', 0, 0),
b'version.xmlinj_reflow' : (1, 'scalar_text', 0, 0),
b'version.xmlinj_transform' : (1, 'scalar_text', 0, 0),
b'version.findlists' : (1, 'scalar_text', 0, 0),
b'version.page_num' : (1, 'scalar_text', 0, 0),
b'version.page_type' : (1, 'scalar_text', 0, 0),
b'version.bad_text' : (1, 'scalar_text', 0, 0),
b'version.glyph_mismatch' : (1, 'scalar_text', 0, 0),
b'version.margins' : (1, 'scalar_text', 0, 0),
b'version.staggered_lines' : (1, 'scalar_text', 0, 0),
b'version.paragraph_continuation' : (1, 'scalar_text', 0, 0),
b'version.toc' : (1, 'scalar_text', 0, 0),
'stylesheet' : (1, 'snippets', 1, 0),
'style' : (1, 'snippets', 1, 0),
'style._tag' : (1, 'scalar_text', 0, 0),
'style.type' : (1, 'scalar_text', 0, 0),
'style._after_type' : (1, 'scalar_text', 0, 0),
'style._parent_type' : (1, 'scalar_text', 0, 0),
'style._after_parent_type' : (1, 'scalar_text', 0, 0),
'style.class' : (1, 'scalar_text', 0, 0),
'style._after_class' : (1, 'scalar_text', 0, 0),
'rule' : (1, 'snippets', 1, 0),
'rule.attr' : (1, 'scalar_text', 0, 0),
'rule.value' : (1, 'scalar_text', 0, 0),
b'stylesheet' : (1, 'snippets', 1, 0),
b'style' : (1, 'snippets', 1, 0),
b'style._tag' : (1, 'scalar_text', 0, 0),
b'style.type' : (1, 'scalar_text', 0, 0),
b'style._after_type' : (1, 'scalar_text', 0, 0),
b'style._parent_type' : (1, 'scalar_text', 0, 0),
b'style._after_parent_type' : (1, 'scalar_text', 0, 0),
b'style.class' : (1, 'scalar_text', 0, 0),
b'style._after_class' : (1, 'scalar_text', 0, 0),
b'rule' : (1, 'snippets', 1, 0),
b'rule.attr' : (1, 'scalar_text', 0, 0),
b'rule.value' : (1, 'scalar_text', 0, 0),
'original' : (0, 'number', 1, 1),
'original.pnum' : (1, 'number', 0, 0),
'original.pid' : (1, 'text', 0, 0),
'pages' : (0, 'number', 1, 1),
'pages.ref' : (1, 'number', 0, 0),
'pages.id' : (1, 'number', 0, 0),
'startID' : (0, 'number', 1, 1),
'startID.page' : (1, 'number', 0, 0),
'startID.id' : (1, 'number', 0, 0),
'median_d' : (1, 'number', 0, 0),
'median_h' : (1, 'number', 0, 0),
'median_firsty' : (1, 'number', 0, 0),
'median_lasty' : (1, 'number', 0, 0),
b'original' : (0, 'number', 1, 1),
b'original.pnum' : (1, 'number', 0, 0),
b'original.pid' : (1, 'text', 0, 0),
b'pages' : (0, 'number', 1, 1),
b'pages.ref' : (1, 'number', 0, 0),
b'pages.id' : (1, 'number', 0, 0),
b'startID' : (0, 'number', 1, 1),
b'startID.page' : (1, 'number', 0, 0),
b'startID.id' : (1, 'number', 0, 0),
'num_footers_maybe' : (1, 'number', 0, 0),
'num_footers_yes' : (1, 'number', 0, 0),
'num_headers_maybe' : (1, 'number', 0, 0),
'num_headers_yes' : (1, 'number', 0, 0),
b'median_d' : (1, 'number', 0, 0),
b'median_h' : (1, 'number', 0, 0),
b'median_firsty' : (1, 'number', 0, 0),
b'median_lasty' : (1, 'number', 0, 0),
'tracking' : (1, 'number', 0, 0),
'src' : (1, 'text', 0, 0),
b'num_footers_maybe' : (1, 'number', 0, 0),
b'num_footers_yes' : (1, 'number', 0, 0),
b'num_headers_maybe' : (1, 'number', 0, 0),
b'num_headers_yes' : (1, 'number', 0, 0),
b'tracking' : (1, 'number', 0, 0),
b'src' : (1, 'text', 0, 0),
}
@ -419,8 +415,8 @@ class PageParser(object):
def get_tagpath(self, i):
cnt = len(self.tagpath)
if i < cnt : result = self.tagpath[i]
for j in xrange(i+1, cnt) :
result += '.' + self.tagpath[j]
for j in range(i+1, cnt) :
result += b'.' + self.tagpath[j]
return result
@ -457,7 +453,7 @@ class PageParser(object):
elif (argtype == 'snippets') :
result = arg
else :
print "Error Unknown argtype %s" % argtype
print("Error Unknown argtype %s" % argtype)
sys.exit(-2)
return result
@ -469,9 +465,9 @@ class PageParser(object):
known_token = False
self.tag_push(token)
if self.debug : print 'Processing: ', self.get_tagpath(0)
if self.debug : print('Processing: ', self.get_tagpath(0))
cnt = self.tagpath_len()
for j in xrange(cnt):
for j in range(cnt):
tkn = self.get_tagpath(j)
if tkn in self.token_tags :
num_args = self.token_tags[tkn][0]
@ -495,8 +491,8 @@ class PageParser(object):
if (subtags == 1):
ntags = readEncodedNumber(self.fo)
if self.debug : print 'subtags: ' + token + ' has ' + str(ntags)
for j in xrange(ntags):
if self.debug : print('subtags: ', token , ' has ' , str(ntags))
for j in range(ntags):
val = readEncodedNumber(self.fo)
subtagres.append(self.procToken(self.dict.lookup(val)))
@ -510,7 +506,7 @@ class PageParser(object):
argres = self.decodeCMD(arg,argtype)
else :
# num_arg scalar arguments
for i in xrange(num_args):
for i in range(num_args):
argres.append(self.formatArg(readEncodedNumber(self.fo), argtype))
# build the return tag
@ -529,7 +525,7 @@ class PageParser(object):
else:
result = []
if (self.debug or self.first_unknown):
print 'Unknown Token:', token
print('Unknown Token:', token)
self.first_unknown = False
self.tag_pop()
return result
@ -544,9 +540,9 @@ class PageParser(object):
result = 'Set of '+ str(cnt) + ' xml snippets. The overall structure \n'
result += 'of the document is indicated by snippet number sets at the\n'
result += 'end of each snippet. \n'
print result
for i in xrange(cnt):
if self.debug: print 'Snippet:',str(i)
print(result)
for i in range(cnt):
if self.debug: print('Snippet:',str(i))
snippet = []
snippet.append(i)
val = readEncodedNumber(self.fo)
@ -564,12 +560,12 @@ class PageParser(object):
adj = readEncodedNumber(self.fo)
mode = mode >> 1
x = []
for i in xrange(cnt):
for i in range(cnt):
x.append(readEncodedNumber(self.fo) - adj)
for i in xrange(mode):
for j in xrange(1, cnt):
for i in range(mode):
for j in range(1, cnt):
x[j] = x[j] + x[j - 1]
for i in xrange(cnt):
for i in range(cnt):
result.append(self.formatArg(x[i],argtype))
return result
@ -588,10 +584,10 @@ class PageParser(object):
cnt = readEncodedNumber(self.fo)
mode = readEncodedNumber(self.fo)
if self.debug : print 'Loop for', cnt, 'with mode', mode, ': '
if self.debug : print('Loop for', cnt, 'with mode', mode, ': ')
return self.doLoop76Mode(argtype, cnt, mode)
if self.dbug: print "Unknown command", cmd
if self.dbug: print("Unknown command", cmd)
result = []
return result
@ -603,7 +599,7 @@ class PageParser(object):
subtagList = tag[1]
argtype = tag[2]
argList = tag[3]
nname = prefix + '.' + name
nname = prefix + b'.' + name
nsubtaglist = []
for j in subtagList:
nsubtaglist.append(self.updateName(j,prefix))
@ -652,34 +648,34 @@ class PageParser(object):
subtagList = node[1]
argtype = node[2]
argList = node[3]
fullpathname = name.split('.')
fullpathname = name.split(b'.')
nodename = fullpathname.pop()
ilvl = len(fullpathname)
indent = ' ' * (3 * ilvl)
indent = b' ' * (3 * ilvl)
rlst = []
rlst.append(indent + '<' + nodename + '>')
rlst.append(indent + b'<' + nodename + b'>')
if len(argList) > 0:
alst = []
for j in argList:
if (argtype == 'text') or (argtype == 'scalar_text') :
alst.append(j + '|')
if (argtype == b'text') or (argtype == b'scalar_text') :
alst.append(j + b'|')
else :
alst.append(str(j) + ',')
argres = "".join(alst)
alst.append(str(j).encode('utf-8') + b',')
argres = b"".join(alst)
argres = argres[0:-1]
if argtype == 'snippets' :
rlst.append('snippets:' + argres)
if argtype == b'snippets' :
rlst.append(b'snippets:' + argres)
else :
rlst.append(argres)
if len(subtagList) > 0 :
rlst.append('\n')
rlst.append(b'\n')
for j in subtagList:
if len(j) > 0 :
rlst.append(self.formatTag(j))
rlst.append(indent + '</' + nodename + '>\n')
rlst.append(indent + b'</' + nodename + b'>\n')
else:
rlst.append('</' + nodename + '>\n')
return "".join(rlst)
rlst.append(b'</' + nodename + b'>\n')
return b"".join(rlst)
# flatten tag
@ -694,20 +690,20 @@ class PageParser(object):
alst = []
for j in argList:
if (argtype == 'text') or (argtype == 'scalar_text') :
alst.append(j + '|')
alst.append(j + b'|')
else :
alst.append(str(j) + '|')
argres = "".join(alst)
alst.append(str(j).encode('utf-8') + b'|')
argres = b"".join(alst)
argres = argres[0:-1]
if argtype == 'snippets' :
rlst.append('.snippets=' + argres)
if argtype == b'snippets' :
rlst.append(b'.snippets=' + argres)
else :
rlst.append('=' + argres)
rlst.append('\n')
rlst.append(b'=' + argres)
rlst.append(b'\n')
for j in subtagList:
if len(j) > 0 :
rlst.append(self.flattenTag(j))
return "".join(rlst)
return b"".join(rlst)
# reduce create xml output
@ -719,8 +715,8 @@ class PageParser(object):
rlst.append(self.flattenTag(j))
else:
rlst.append(self.formatTag(j))
result = "".join(rlst)
if self.debug : print result
result = b"".join(rlst)
if self.debug : print(result)
return result
@ -737,16 +733,16 @@ class PageParser(object):
# peek at the first bytes to see what type of file it is
magic = self.fo.read(9)
if (magic[0:1] == 'p') and (magic[2:9] == 'marker_'):
first_token = 'info'
elif (magic[0:1] == 'p') and (magic[2:9] == '__PAGE_'):
if (magic[0:1] == b'p') and (magic[2:9] == b'marker_'):
first_token = b'info'
elif (magic[0:1] == b'p') and (magic[2:9] == b'__PAGE_'):
skip = self.fo.read(2)
first_token = 'info'
elif (magic[0:1] == 'p') and (magic[2:8] == '_PAGE_'):
first_token = 'info'
elif (magic[0:1] == 'g') and (magic[2:9] == '__GLYPH'):
first_token = b'info'
elif (magic[0:1] == b'p') and (magic[2:8] == b'_PAGE_'):
first_token = b'info'
elif (magic[0:1] == b'g') and (magic[2:9] == b'__GLYPH'):
skip = self.fo.read(3)
first_token = 'info'
first_token = b'info'
else :
# other0.dat file
first_token = None
@ -768,26 +764,26 @@ class PageParser(object):
break
if (v == 0x72):
self.doLoop72('number')
self.doLoop72(b'number')
elif (v > 0) and (v < self.dict.getSize()) :
tag = self.procToken(self.dict.lookup(v))
if len(tag) > 0 :
self.doc.append(tag)
else:
if self.debug:
print "Main Loop: Unknown value: %x" % v
print("Main Loop: Unknown value: %x" % v)
if (v == 0):
if (self.peek(1) == 0x5f):
skip = self.fo.read(1)
first_token = 'info'
first_token = b'info'
# now do snippet injection
if len(self.snippetList) > 0 :
if self.debug : print 'Injecting Snippets:'
if self.debug : print('Injecting Snippets:')
snippet = self.injectSnippets(self.snippetList[0])
snipno = snippet[0]
tag_add = snippet[1]
if self.debug : print self.formatTag(tag_add)
if self.debug : print(self.formatTag(tag_add))
if len(tag_add) > 0:
self.doc.append(tag_add)
@ -799,41 +795,43 @@ class PageParser(object):
def fromData(dict, fname):
flat_xml = True
debug = False
debug = True
pp = PageParser(fname, dict, debug, flat_xml)
xmlpage = pp.process()
return xmlpage
def getXML(dict, fname):
flat_xml = False
debug = False
debug = True
pp = PageParser(fname, dict, debug, flat_xml)
xmlpage = pp.process()
return xmlpage
def usage():
print 'Usage: '
print ' convert2xml.py dict0000.dat infile.dat '
print ' '
print ' Options:'
print ' -h print this usage help message '
print ' -d turn on debug output to check for potential errors '
print ' --flat-xml output the flattened xml page description only '
print ' '
print ' This program will attempt to convert a page*.dat file or '
print ' glyphs*.dat file, using the dict0000.dat file, to its xml description. '
print ' '
print ' Use "cmbtc_dump.py" first to unencrypt, uncompress, and dump '
print ' the *.dat files from a Topaz format e-book.'
print('Usage: ')
print(' convert2xml.py dict0000.dat infile.dat ')
print(' ')
print(' Options:')
print(' -h print this usage help message ')
print(' -d turn on debug output to check for potential errors ')
print(' --flat-xml output the flattened xml page description only ')
print(' ')
print(' This program will attempt to convert a page*.dat file or ')
print(' glyphs*.dat file, using the dict0000.dat file, to its xml description. ')
print(' ')
print(' Use "cmbtc_dump.py" first to unencrypt, uncompress, and dump ')
print(' the *.dat files from a Topaz format e-book.')
#
# Main
#
def main(argv):
sys.stdout=SafeUnbuffered(sys.stdout)
sys.stderr=SafeUnbuffered(sys.stderr)
dictFile = ""
pageFile = ""
debug = False
debug = True
flat_xml = False
printOutput = False
if len(argv) == 0:
@ -843,10 +841,10 @@ def main(argv):
try:
opts, args = getopt.getopt(argv[1:], "hd", ["flat-xml"])
except getopt.GetoptError, err:
except getopt.GetoptError as err:
# print help information and exit:
print str(err) # will print something like "option -a not recognized"
print(str(err)) # will print something like "option -a not recognized"
usage()
sys.exit(2)
@ -875,7 +873,7 @@ def main(argv):
xmlpage = pp.process()
if printOutput:
print xmlpage
print(xmlpage)
return 0
return xmlpage

View file

@ -0,0 +1,330 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# epubfontdecrypt.py
# Copyright © 2021-2023 by noDRM
# Released under the terms of the GNU General Public Licence, version 3
# <http://www.gnu.org/licenses/>
# Revision history:
# 1 - Initial release
# 2 - Bugfix for multiple book IDs, reported at #347
"""
Decrypts / deobfuscates font files in EPUB files
"""
from __future__ import print_function
__license__ = 'GPL v3'
__version__ = "2"
import os
import traceback
import zlib
import zipfile
from zipfile import ZipInfo, ZipFile, ZIP_STORED, ZIP_DEFLATED
from zeroedzipinfo import ZeroedZipInfo
from contextlib import closing
from lxml import etree
import itertools
import hashlib
import binascii
class Decryptor(object):
def __init__(self, obfuscationkeyIETF, obfuscationkeyAdobe, encryption):
enc = lambda tag: '{%s}%s' % ('http://www.w3.org/2001/04/xmlenc#', tag)
dsig = lambda tag: '{%s}%s' % ('http://www.w3.org/2000/09/xmldsig#', tag)
self.obfuscation_key_Adobe = obfuscationkeyAdobe
self.obfuscation_key_IETF = obfuscationkeyIETF
self._encryption = etree.fromstring(encryption)
# This loops through all entries in the "encryption.xml" file
# to figure out which files need to be decrypted.
self._obfuscatedIETF = obfuscatedIETF = set()
self._obfuscatedAdobe = obfuscatedAdobe = set()
self._other = other = set()
self._json_elements_to_remove = json_elements_to_remove = set()
self._has_remaining_xml = False
expr = './%s/%s/%s' % (enc('EncryptedData'), enc('CipherData'),
enc('CipherReference'))
for elem in self._encryption.findall(expr):
path = elem.get('URI', None)
encryption_type_url = (elem.getparent().getparent().find("./%s" % (enc('EncryptionMethod'))).get('Algorithm', None))
if path is not None:
if encryption_type_url == "http://www.idpf.org/2008/embedding":
# Font files obfuscated with the IETF algorithm
path = path.encode('utf-8')
obfuscatedIETF.add(path)
if (self.obfuscation_key_IETF is None):
self._has_remaining_xml = True
else:
json_elements_to_remove.add(elem.getparent().getparent())
elif encryption_type_url == "http://ns.adobe.com/pdf/enc#RC":
# Font files obfuscated with the Adobe algorithm.
path = path.encode('utf-8')
obfuscatedAdobe.add(path)
if (self.obfuscation_key_Adobe is None):
self._has_remaining_xml = True
else:
json_elements_to_remove.add(elem.getparent().getparent())
else:
path = path.encode('utf-8')
other.add(path)
self._has_remaining_xml = True
# Other unsupported type.
for elem in json_elements_to_remove:
elem.getparent().remove(elem)
def check_if_remaining(self):
return self._has_remaining_xml
def get_xml(self):
return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + etree.tostring(self._encryption, encoding="utf-8", pretty_print=True, xml_declaration=False).decode("utf-8")
def decompress(self, bytes):
dc = zlib.decompressobj(-15)
try:
decompressed_bytes = dc.decompress(bytes)
ex = dc.decompress(b'Z') + dc.flush()
if ex:
decompressed_bytes = decompressed_bytes + ex
except:
# possibly not compressed by zip - just return bytes
return bytes, False
return decompressed_bytes , True
def decrypt(self, path, data):
if path.encode('utf-8') in self._obfuscatedIETF and self.obfuscation_key_IETF is not None:
# de-obfuscate according to the IETF standard
data, was_decomp = self.decompress(data)
if len(data) <= 1040:
# de-obfuscate whole file
out = self.deobfuscate_single_data(self.obfuscation_key_IETF, data)
else:
out = self.deobfuscate_single_data(self.obfuscation_key_IETF, data[:1040]) + data[1040:]
if (not was_decomp):
out, was_decomp = self.decompress(out)
return out
elif path.encode('utf-8') in self._obfuscatedAdobe and self.obfuscation_key_Adobe is not None:
# de-obfuscate according to the Adobe standard
data, was_decomp = self.decompress(data)
if len(data) <= 1024:
# de-obfuscate whole file
out = self.deobfuscate_single_data(self.obfuscation_key_Adobe, data)
else:
out = self.deobfuscate_single_data(self.obfuscation_key_Adobe, data[:1024]) + data[1024:]
if (not was_decomp):
out, was_decomp = self.decompress(out)
return out
else:
# Not encrypted or obfuscated
return data
def deobfuscate_single_data(self, key, data):
try:
msg = bytes([c^k for c,k in zip(data, itertools.cycle(key))])
except TypeError:
# Python 2
msg = ''.join(chr(ord(c)^ord(k)) for c,k in itertools.izip(data, itertools.cycle(key)))
return msg
def decryptFontsBook(inpath, outpath):
with closing(ZipFile(open(inpath, 'rb'))) as inf:
namelist = inf.namelist()
if 'META-INF/encryption.xml' not in namelist:
return 1
# Font key handling:
font_master_key = None
adobe_master_encryption_key = None
contNS = lambda tag: '{%s}%s' % ('urn:oasis:names:tc:opendocument:xmlns:container', tag)
path = None
try:
container = etree.fromstring(inf.read("META-INF/container.xml"))
rootfiles = container.find(contNS("rootfiles")).findall(contNS("rootfile"))
for rootfile in rootfiles:
path = rootfile.get("full-path", None)
if (path is not None):
break
except:
pass
# If path is None, we didn't find an OPF, so we probably don't have a font key.
# If path is set, it's the path to the main content OPF file.
if (path is None):
print("FontDecrypt: No OPF for font obfuscation found")
return 1
else:
packageNS = lambda tag: '{%s}%s' % ('http://www.idpf.org/2007/opf', tag)
metadataDCNS = lambda tag: '{%s}%s' % ('http://purl.org/dc/elements/1.1/', tag)
try:
container = etree.fromstring(inf.read(path))
except:
container = []
## IETF font key algorithm:
print("FontDecrypt: Checking {0} for IETF font obfuscation keys ... ".format(path), end='')
secret_key_name = None
try:
secret_key_name = container.get("unique-identifier")
except:
pass
try:
identify_elements = container.find(packageNS("metadata")).findall(metadataDCNS("identifier"))
for element in identify_elements:
if (secret_key_name is None or secret_key_name == element.get("id")):
font_master_key = element.text
except:
pass
if (font_master_key is not None):
if (secret_key_name is None):
print("found '%s'" % (font_master_key))
else:
print("found '%s' (%s)" % (font_master_key, secret_key_name))
# Trim / remove forbidden characters from the key, then hash it:
font_master_key = font_master_key.replace(' ', '')
font_master_key = font_master_key.replace('\t', '')
font_master_key = font_master_key.replace('\r', '')
font_master_key = font_master_key.replace('\n', '')
font_master_key = font_master_key.encode('utf-8')
font_master_key = hashlib.sha1(font_master_key).digest()
else:
print("not found")
## Adobe font key algorithm
print("FontDecrypt: Checking {0} for Adobe font obfuscation keys ... ".format(path), end='')
try:
metadata = container.find(packageNS("metadata"))
identifiers = metadata.findall(metadataDCNS("identifier"))
uid = None
uidMalformed = False
for identifier in identifiers:
if identifier.get(packageNS("scheme")) == "UUID":
if identifier.text[:9] == "urn:uuid:":
uid = identifier.text[9:]
else:
uid = identifier.text
break
if identifier.text[:9] == "urn:uuid:":
uid = identifier.text[9:]
break
if uid is not None:
uid = uid.replace(chr(0x20),'').replace(chr(0x09),'')
uid = uid.replace(chr(0x0D),'').replace(chr(0x0A),'').replace('-','')
if len(uid) < 16:
uidMalformed = True
if not all(c in "0123456789abcdefABCDEF" for c in uid):
uidMalformed = True
if not uidMalformed:
print("found '{0}'".format(uid))
uid = uid + uid
adobe_master_encryption_key = binascii.unhexlify(uid[:32])
if adobe_master_encryption_key is None:
print("not found")
except:
print("exception")
pass
# Begin decrypting.
try:
encryption = inf.read('META-INF/encryption.xml')
decryptor = Decryptor(font_master_key, adobe_master_encryption_key, encryption)
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf:
# Mimetype needs to be the first entry, so remove it from the list
# whereever it is, then add it at the beginning.
namelist.remove("mimetype")
for path in (["mimetype"] + namelist):
data = inf.read(path)
zi = ZipInfo(path)
zi.compress_type=ZIP_DEFLATED
if path == "mimetype":
# mimetype must not be compressed
zi.compress_type = ZIP_STORED
elif path == "META-INF/encryption.xml":
# Check if there's still other entries not related to fonts
if (decryptor.check_if_remaining()):
data = decryptor.get_xml()
print("FontDecrypt: There's remaining entries in encryption.xml, adding file ...")
else:
# No remaining entries, no need for that file.
continue
try:
# get the file info, including time-stamp
oldzi = inf.getinfo(path)
# copy across useful fields
zi.date_time = oldzi.date_time
zi.comment = oldzi.comment
zi.extra = oldzi.extra
zi.internal_attr = oldzi.internal_attr
# external attributes are dependent on the create system, so copy both.
zi.external_attr = oldzi.external_attr
zi.volume = oldzi.volume
zi.create_system = oldzi.create_system
zi.create_version = oldzi.create_version
if any(ord(c) >= 128 for c in path) or any(ord(c) >= 128 for c in zi.comment):
# If the file name or the comment contains any non-ASCII char, set the UTF8-flag
zi.flag_bits |= 0x800
except:
pass
# Python 3 has a bug where the external_attr is reset to `0o600 << 16`
# if it's NULL, so we need a workaround:
if zi.external_attr == 0:
zi = ZeroedZipInfo(zi)
if path == "mimetype":
outf.writestr(zi, inf.read('mimetype'))
elif path == "META-INF/encryption.xml":
outf.writestr(zi, data)
else:
outf.writestr(zi, decryptor.decrypt(path, data))
except:
print("FontDecrypt: Could not decrypt fonts in {0:s} because of an exception:\n{1:s}".format(os.path.basename(inpath), traceback.format_exc()))
traceback.print_exc()
return 2
return 0

View file

@ -1,4 +1,5 @@
#!/usr/bin/python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# This is a python script. You need a Python interpreter to run it.
# For example, ActiveState Python, which exists for windows.
@ -10,6 +11,8 @@
# Changelog epubtest
# 1.00 - Cut to epubtest.py, testing ePub files only by Apprentice Alf
# 1.01 - Added routine for use by Windows DeDRM
# 2.00 - Python 3, September 2020
# 2.01 - Add new Adobe DRM, add Readium LCP
#
# Written in 2011 by Paul Durrant
# Released with unlicense. See http://unlicense.org/
@ -44,78 +47,21 @@
# It's still polite to give attribution if you do reuse this code.
#
from __future__ import with_statement
__version__ = '2.0'
__version__ = '1.01'
#@@CALIBRE_COMPAT_CODE@@
import sys, struct, os
import sys, struct, os, traceback
import zlib
import zipfile
import xml.etree.ElementTree as etree
from .argv_utils import unicode_argv
NSMAP = {'adept': 'http://ns.adobe.com/adept',
'enc': 'http://www.w3.org/2001/04/xmlenc#'}
# 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,unicode):
data = data.encode(self.encoding,"replace")
self.stream.write(data)
self.stream.flush()
def __getattr__(self, attr):
return getattr(self.stream, attr)
from .utilities import SafeUnbuffered
try:
from calibre.constants import iswindows, isosx
except:
iswindows = sys.platform.startswith('win')
isosx = sys.platform.startswith('darwin')
def unicode_argv():
if iswindows:
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
# strings.
# Versions 2.x of Python don't support Unicode in sys.argv on
# Windows, with the underlying Windows API instead replacing multi-byte
# characters with '?'. So use shell32.GetCommandLineArgvW to get sys.argv
# as a list of Unicode strings and encode them as utf-8
from ctypes import POINTER, byref, cdll, c_int, windll
from ctypes.wintypes import LPCWSTR, LPWSTR
GetCommandLineW = cdll.kernel32.GetCommandLineW
GetCommandLineW.argtypes = []
GetCommandLineW.restype = LPCWSTR
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
CommandLineToArgvW.restype = POINTER(LPWSTR)
cmd = GetCommandLineW()
argc = c_int(0)
argv = CommandLineToArgvW(cmd, byref(argc))
if argc.value > 0:
# Remove Python executable and commands if present
start = argc.value - len(sys.argv)
return [argv[i] for i in
xrange(start, argc.value)]
# if we don't have any arguments at all, just pass back script name
# this should never happen
return [u"epubtest.py"]
else:
argvencoding = sys.stdin.encoding
if argvencoding == None:
argvencoding = "utf-8"
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
_FILENAME_LEN_OFFSET = 26
_EXTRA_LEN_OFFSET = 28
@ -169,37 +115,55 @@ def getfiledata(file, zi):
return data
def encryption(infile):
# returns encryption: one of Unencrypted, Adobe, B&N and Unknown
encryption = "Unknown"
# 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] == "PK":
foundrights = False
foundencryption = False
if bookdata[0:0+2] == b"PK":
inzip = zipfile.ZipFile(infile,'r')
namelist = set(inzip.namelist())
if 'META-INF/rights.xml' not in namelist or 'META-INF/encryption.xml' not in namelist:
if (
'META-INF/encryption.xml' in namelist and
'META-INF/license.lcpl' in namelist and
b"EncryptedContentKey" in inzip.read("META-INF/encryption.xml")):
encryption = "Readium LCP"
elif 'META-INF/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"
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"
elif len(bookkey) == 64:
encryption = "B&N"
else:
encryption = "Unknown (key len " + str(len(bookkey)) + ")"
except:
encryption = "Unknown"
except:
traceback.print_exc()
return encryption
def main():
argv=unicode_argv()
print encryption(argv[1])
argv=unicode_argv("epubtest.py")
if len(argv) < 2:
print("Give an ePub file as a parameter.")
else:
print(encryption(argv[1]))
return 0
if __name__ == "__main__":

View file

@ -0,0 +1,344 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# epubwatermark.py
# Copyright © 2021 NoDRM
# Revision history:
# 1.0 - Initial version
# Released under the terms of the GNU General Public Licence, version 3
# <http://www.gnu.org/licenses/>
"""
Removes various watermarks from EPUB files
"""
import traceback
from zipfile import ZipInfo, ZipFile, ZIP_STORED, ZIP_DEFLATED
from zeroedzipinfo import ZeroedZipInfo
from contextlib import closing
from lxml import etree
import re
# Runs a RegEx over all HTML/XHTML files to remove watermakrs.
def removeHTMLwatermarks(object, path_to_ebook):
try:
inf = ZipFile(open(path_to_ebook, 'rb'))
namelist = inf.namelist()
modded_names = []
modded_contents = []
count_adept = 0
count_pocketbook = 0
count_lemonink_invisible = 0
count_lemonink_visible = 0
lemonink_trackingID = None
for file in namelist:
if not (file.endswith('.html') or file.endswith('.xhtml') or file.endswith('.xml')):
continue
try:
file_str = inf.read(file).decode("utf-8")
str_new = file_str
# Remove Adobe ADEPT watermarks
# Match optional newline at the beginning, then a "meta" tag with name = "Adept.expected.resource" or "Adept.resource"
# and either a "value" or a "content" element with an Adobe UUID
pre_remove = str_new
str_new = re.sub(r'((\r\n|\r|\n)\s*)?\<meta\s+name=\"(Adept\.resource|Adept\.expected\.resource)\"\s+(content|value)=\"urn:uuid:[0-9a-fA-F\-]+\"\s*\/>', '', str_new)
str_new = re.sub(r'((\r\n|\r|\n)\s*)?\<meta\s+(content|value)=\"urn:uuid:[0-9a-fA-F\-]+\"\s+name=\"(Adept\.resource|Adept\.expected\.resource)\"\s*\/>', '', str_new)
if (str_new != pre_remove):
count_adept += 1
# Remove 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:
pre_remove = str_new
unique_id = re.search(r'<body[^>]+class="[^"]*(t0x[0-9a-fA-F]{25})[^"]*"[^>]*>', str_new)
if (unique_id):
lemonink_trackingID = unique_id.groups()[0]
count_lemonink_invisible += 1
str_new = re.sub(lemonink_trackingID, '', str_new)
pre_remove = str_new
pm = r'(<body[^>]+class="[^"]*"[^>]*>)'
pm += r'\<div style\=\'padding\:0\;border\:0\;text\-indent\:0\;line\-height\:normal\;margin\:0 1cm 0.5cm 1cm\;[^\']*text\-decoration\:none\;[^\']*background\:none\;[^\']*\'\>(.*?)</div>'
pm += r'\<div style\=\'padding\:0\;border\:0\;text\-indent\:0\;line\-height\:normal\;margin\:0 1cm 0.5cm 1cm\;[^\']*text\-decoration\:none\;[^\']*background\:none\;[^\']*\'\>(.*?)</div>'
str_new = re.sub(pm, r'\1', str_new)
if (str_new != pre_remove):
count_lemonink_visible += 1
else:
break
except:
traceback.print_exc()
continue
if (file_str == str_new):
continue
modded_names.append(file)
modded_contents.append(str_new)
if len(modded_names) == 0:
# No file modified, return original
return path_to_ebook
if len(modded_names) != len(modded_contents):
# Something went terribly wrong, return original
print("Watermark: Error during watermark removal")
return path_to_ebook
# Re-package with modified files:
namelist.remove("mimetype")
try:
output = object.temporary_file(".epub").name
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
with closing(ZipFile(open(output, 'wb'), 'w', **kwds)) as outf:
for path in (["mimetype"] + namelist):
data = inf.read(path)
try:
modded_index = None
modded_index = modded_names.index(path)
except:
pass
if modded_index is not None:
# Found modified file - replace contents
data = modded_contents[modded_index]
zi = ZipInfo(path)
oldzi = inf.getinfo(path)
try:
zi.compress_type = oldzi.compress_type
if path == "mimetype":
zi.compress_type = ZIP_STORED
zi.date_time = oldzi.date_time
zi.comment = oldzi.comment
zi.extra = oldzi.extra
zi.internal_attr = oldzi.internal_attr
zi.external_attr = oldzi.external_attr
zi.volume = oldzi.volume
zi.create_system = oldzi.create_system
zi.create_version = oldzi.create_version
if any(ord(c) >= 128 for c in path) or any(ord(c) >= 128 for c in zi.comment):
# If the file name or the comment contains any non-ASCII char, set the UTF8-flag
zi.flag_bits |= 0x800
except:
pass
# Python 3 has a bug where the external_attr is reset to `0o600 << 16`
# if it's NULL, so we need a workaround:
if zi.external_attr == 0:
zi = ZeroedZipInfo(zi)
outf.writestr(zi, data)
except:
traceback.print_exc()
return path_to_ebook
if (count_adept > 0):
print("Watermark: Successfully stripped {0} ADEPT watermark(s) from ebook.".format(count_adept))
if (count_lemonink_invisible > 0 or count_lemonink_visible > 0):
print("Watermark: Successfully stripped {0} visible and {1} invisible LemonInk watermark(s) (\"{2}\") from ebook."
.format(count_lemonink_visible, count_lemonink_invisible, lemonink_trackingID))
if (count_pocketbook > 0):
print("Watermark: Successfully stripped {0} Pocketbook watermark(s) from ebook.".format(count_pocketbook))
return output
except:
traceback.print_exc()
return path_to_ebook
# Finds the main OPF file, then uses RegEx to remove watermarks
def removeOPFwatermarks(object, path_to_ebook):
contNS = lambda tag: '{%s}%s' % ('urn:oasis:names:tc:opendocument:xmlns:container', tag)
opf_path = None
try:
inf = ZipFile(open(path_to_ebook, 'rb'))
container = etree.fromstring(inf.read("META-INF/container.xml"))
rootfiles = container.find(contNS("rootfiles")).findall(contNS("rootfile"))
for rootfile in rootfiles:
opf_path = rootfile.get("full-path", None)
if (opf_path is not None):
break
except:
traceback.print_exc()
return path_to_ebook
# If path is None, we didn't find an OPF, so we probably don't have a font key.
# If path is set, it's the path to the main content OPF file.
if (opf_path is None):
# No OPF found - no watermark
return path_to_ebook
else:
try:
container_str = inf.read(opf_path).decode("utf-8")
container_str_new = container_str
had_amazon = False
had_elibri = False
# Remove Amazon hex watermarks
# Match optional newline at the beginning, then spaces, then a "meta" tag with name = "Watermark" or "Watermark_(hex)" and a "content" element.
# This regex also matches DuMont watermarks with meta name="watermark", with the case-insensitive match on the "w" in watermark.
pre_remove = container_str_new
container_str_new = re.sub(r'((\r\n|\r|\n)\s*)?\<meta\s+name=\"[Ww]atermark(_\(hex\))?\"\s+content=\"[0-9a-fA-F]+\"\s*\/>', '', container_str_new)
container_str_new = re.sub(r'((\r\n|\r|\n)\s*)?\<meta\s+content=\"[0-9a-fA-F]+\"\s+name=\"[Ww]atermark(_\(hex\))?\"\s*\/>', '', container_str_new)
if pre_remove != container_str_new:
had_amazon = True
# Remove elibri / lemonink watermark
# Lemonink replaces all "id" fields in the opf with "idX_Y", with X being the watermark and Y being a number for that particular ID.
# This regex replaces all "idX_Y" IDs with "id_Y", removing the watermark IDs.
pre_remove = container_str_new
container_str_new = re.sub(r'((\r\n|\r|\n)\s*)?\<\!\-\-\s*Wygenerowane przez elibri dla zamówienia numer [0-9a-fA-F]+\s*\-\-\>', '', container_str_new)
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
except:
traceback.print_exc()
return path_to_ebook
if (container_str == container_str_new):
# container didn't change - no watermark
return path_to_ebook
# Re-package without watermark
namelist = inf.namelist()
namelist.remove("mimetype")
try:
output = object.temporary_file(".epub").name
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
with closing(ZipFile(open(output, 'wb'), 'w', **kwds)) as outf:
for path in (["mimetype"] + namelist):
data = inf.read(path)
if path == opf_path:
# Found OPF, replacing ...
data = container_str_new
zi = ZipInfo(path)
oldzi = inf.getinfo(path)
try:
zi.compress_type = oldzi.compress_type
if path == "mimetype":
zi.compress_type = ZIP_STORED
zi.date_time = oldzi.date_time
zi.comment = oldzi.comment
zi.extra = oldzi.extra
zi.internal_attr = oldzi.internal_attr
zi.external_attr = oldzi.external_attr
zi.volume = oldzi.volume
zi.create_system = oldzi.create_system
zi.create_version = oldzi.create_version
if any(ord(c) >= 128 for c in path) or any(ord(c) >= 128 for c in zi.comment):
# If the file name or the comment contains any non-ASCII char, set the UTF8-flag
zi.flag_bits |= 0x800
except:
pass
# Python 3 has a bug where the external_attr is reset to `0o600 << 16`
# if it's NULL, so we need a workaround:
if zi.external_attr == 0:
zi = ZeroedZipInfo(zi)
outf.writestr(zi, data)
except:
traceback.print_exc()
return path_to_ebook
if had_elibri:
print("Watermark: Successfully stripped eLibri watermark from OPF file.")
if had_amazon:
print("Watermark: Successfully stripped Amazon watermark from OPF file.")
return output
def removeCDPwatermark(object, path_to_ebook):
# "META-INF/cdp.info" is a watermark file used by some Tolino vendors.
# We don't want that in our eBooks, so lets remove that file.
try:
infile = ZipFile(open(path_to_ebook, 'rb'))
namelist = infile.namelist()
if 'META-INF/cdp.info' not in namelist:
return path_to_ebook
namelist.remove("mimetype")
namelist.remove("META-INF/cdp.info")
output = object.temporary_file(".epub").name
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
with closing(ZipFile(open(output, 'wb'), 'w', **kwds)) as outf:
for path in (["mimetype"] + namelist):
data = infile.read(path)
zi = ZipInfo(path)
oldzi = infile.getinfo(path)
try:
zi.compress_type = oldzi.compress_type
if path == "mimetype":
zi.compress_type = ZIP_STORED
zi.date_time = oldzi.date_time
zi.comment = oldzi.comment
zi.extra = oldzi.extra
zi.internal_attr = oldzi.internal_attr
zi.external_attr = oldzi.external_attr
zi.volume = oldzi.volume
zi.create_system = oldzi.create_system
zi.create_version = oldzi.create_version
if any(ord(c) >= 128 for c in path) or any(ord(c) >= 128 for c in zi.comment):
# If the file name or the comment contains any non-ASCII char, set the UTF8-flag
zi.flag_bits |= 0x800
except:
pass
# Python 3 has a bug where the external_attr is reset to `0o600 << 16`
# if it's NULL, so we need a workaround:
if zi.external_attr == 0:
zi = ZeroedZipInfo(zi)
outf.writestr(zi, data)
print("Watermark: Successfully removed cdp.info watermark")
return output
except:
traceback.print_exc()
return path_to_ebook

279
dedrm_src/erdr2pml.py → DeDRM_plugin/erdr2pml.py Normal file → Executable file
View file

@ -1,13 +1,9 @@
#!/usr/bin/env python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# erdr2pml.py
# Copyright © 2008 The Dark Reverser
# Copyright © 2008-2022 The Dark Reverser, Apprentice Harper, noDRM et al.
#
# Modified 20082012 by some_updates, DiapDealer and Apprentice Alf
# This is a python script. You need a Python interpreter to run it.
# For example, ActiveState Python, which exists for windows.
# Changelog
#
# Based on ereader2html version 0.08 plus some later small fixes
@ -67,127 +63,29 @@
# - Ignore sidebars for dictionaries (different format?)
# 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.
# 1.02 - Remove OpenSSL support; only use PyCryptodome
__version__='0.23'
__version__='1.02'
import sys, re
import struct, binascii, getopt, zlib, os, os.path, urllib, tempfile, traceback
import struct, binascii, getopt, zlib, os, os.path, urllib, tempfile, traceback, hashlib
if 'calibre' in sys.modules:
inCalibre = True
else:
inCalibre = False
try:
from Cryptodome.Cipher import DES
except ImportError:
from Crypto.Cipher import DES
# 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,unicode):
data = data.encode(self.encoding,"replace")
self.stream.write(data)
self.stream.flush()
def __getattr__(self, attr):
return getattr(self.stream, attr)
#@@CALIBRE_COMPAT_CODE@@
from .utilities import SafeUnbuffered
from .argv_utils import unicode_argv
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
xrange(start, argc.value)]
# if we don't have any arguments at all, just pass back script name
# this should never happen
return [u"mobidedrm.py"]
else:
argvencoding = sys.stdin.encoding
if argvencoding == None:
argvencoding = "utf-8"
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
Des = None
if iswindows:
# first try with pycrypto
if inCalibre:
from calibre_plugins.dedrm import pycrypto_des
else:
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
Des = openssl_des.load_libcrypto()
else:
# first try with openssl
if inCalibre:
from calibre_plugins.dedrm import openssl_des
else:
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
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
Des = python_des.Des
# Import Psyco if available
try:
# http://psyco.sourceforge.net
import psyco
psyco.full()
except ImportError:
pass
try:
from hashlib import sha1
except ImportError:
# older Python release
import sha
sha1 = lambda s: sha.new(s)
import cgi
import logging
@ -200,17 +98,17 @@ class Sectionizer(object):
bkType = "Book"
def __init__(self, filename, ident):
self.contents = file(filename, 'rb').read()
self.contents = open(filename, 'rb').read()
self.header = self.contents[0:72]
self.num_sections, = struct.unpack('>H', self.contents[76:78])
# Dictionary or normal content (TODO: Not hard-coded)
if self.header[0x3C:0x3C+8] != ident:
if self.header[0x3C:0x3C+8] == "PDctPPrs":
if self.header[0x3C:0x3C+8] == b"PDctPPrs":
self.bkType = "Dict"
else:
raise ValueError('Invalid file format')
self.sections = []
for i in xrange(self.num_sections):
for i in range(self.num_sections):
offset, a1,a2,a3,a4 = struct.unpack('>LBBBB', self.contents[78+i*8:78+i*8+8])
flags, val = a1, a2<<16|a3<<8|a4
self.sections.append( (offset, flags, val) )
@ -229,29 +127,35 @@ class Sectionizer(object):
# and with some (heavily edited) code from Paul Durrant's kindlenamer.py
def sanitizeFileName(name):
# substitute filename unfriendly characters
name = name.replace(u"<",u"[").replace(u">",u"]").replace(u" : ",u" ").replace(u": ",u" ").replace(u":",u"").replace(u"/",u"_").replace(u"\\",u"_").replace(u"|",u"_").replace(u"\"",u"\'")
name = name.replace("<","[").replace(">","]").replace(" : "," ").replace(": "," ").replace(":","").replace("/","_").replace("\\","_").replace("|","_").replace("\"","\'")
# delete control characters
name = u"".join(char for char in name if ord(char)>=32)
name = "".join(char for char in name if ord(char)>=32)
# white space to single space, delete leading and trailing while space
name = re.sub(ur"\s", u" ", name).strip()
name = re.sub(r"\s", " ", name).strip()
# remove leading dots
while len(name)>0 and name[0] == u".":
while len(name)>0 and name[0] == ".":
name = name[1:]
# remove trailing dots (Windows doesn't like them)
if name.endswith(u'.'):
if name.endswith("."):
name = name[:-1]
return name
def fixKey(key):
def fixByte(b):
if sys.version_info[0] == 2:
b = ord(b)
return b ^ ((b ^ (b<<1) ^ (b<<2) ^ (b<<3) ^ (b<<4) ^ (b<<5) ^ (b<<6) ^ (b<<7) ^ 0x80) & 0x80)
return "".join([chr(fixByte(ord(a))) for a in key])
return bytes(bytearray([fixByte(a) for a in key]))
def deXOR(text, sp, table):
r=''
r=b''
j = sp
for i in xrange(len(text)):
r += chr(ord(table[j]) ^ ord(text[i]))
for i in range(len(text)):
if sys.version_info[0] == 2:
r += chr(ord(table[j]) ^ ord(text[i]))
else:
r += bytes(bytearray([table[j] ^ text[i]]))
j = j + 1
if j == len(table):
j = 0
@ -268,19 +172,19 @@ class EreaderProcessor(object):
raise ValueError('incorrect eReader version %d (error 1)' % version)
data = self.section_reader(1)
self.data = data
des = Des(fixKey(data[0:8]))
des = DES.new(fixKey(data[0:8]), DES.MODE_ECB)
cookie_shuf, cookie_size = struct.unpack('>LL', des.decrypt(data[-8:]))
if cookie_shuf < 3 or cookie_shuf > 0x14 or cookie_size < 0xf0 or cookie_size > 0x200:
raise ValueError('incorrect eReader version (error 2)')
input = des.decrypt(data[-cookie_size:])
def unshuff(data, shuf):
r = [''] * len(data)
r = [0] * len(data)
j = 0
for i in xrange(len(data)):
for i in range(len(data)):
j = (j + shuf) % len(data)
r[j] = data[i]
assert len("".join(r)) == len(data)
return "".join(r)
assert len(bytes(r)) == len(data)
return bytes(r)
r = unshuff(input[0:-8], cookie_shuf)
drm_sub_version = struct.unpack('>H', r[0:2])[0]
@ -330,9 +234,9 @@ class EreaderProcessor(object):
self.flags = struct.unpack('>L', r[4:8])[0]
reqd_flags = (1<<9) | (1<<7) | (1<<10)
if (self.flags & reqd_flags) != reqd_flags:
print "Flags: 0x%X" % self.flags
print("Flags: 0x%X" % self.flags)
raise ValueError('incompatible eReader file')
des = Des(fixKey(user_key))
des = DES.new(fixKey(user_key), DES.MODE_ECB)
if version == 259:
if drm_sub_version != 7:
raise ValueError('incorrect eReader version %d (error 3)' % drm_sub_version)
@ -351,7 +255,7 @@ class EreaderProcessor(object):
encrypted_key = r[172:172+8]
encrypted_key_sha = r[56:56+20]
self.content_key = des.decrypt(encrypted_key)
if sha1(self.content_key).digest() != encrypted_key_sha:
if hashlib.sha1(self.content_key).digest() != encrypted_key_sha:
raise ValueError('Incorrect Name and/or Credit Card')
def getNumImages(self):
@ -359,9 +263,9 @@ class EreaderProcessor(object):
def getImage(self, i):
sect = self.section_reader(self.first_image_page + i)
name = sect[4:4+32].strip('\0')
name = sect[4:4+32].strip(b'\0')
data = sect[62:]
return sanitizeFileName(unicode(name,'windows-1252')), data
return sanitizeFileName(name.decode('windows-1252')), data
# def getChapterNamePMLOffsetData(self):
@ -408,9 +312,9 @@ class EreaderProcessor(object):
# return bkinfo
def getText(self):
des = Des(fixKey(self.content_key))
r = ''
for i in xrange(self.num_text_pages):
des = DES.new(fixKey(self.content_key), DES.MODE_ECB)
r = b''
for i in range(self.num_text_pages):
logging.debug('get page %d', i)
r += zlib.decompress(des.decrypt(self.section_reader(1 + i)))
@ -421,8 +325,8 @@ class EreaderProcessor(object):
sect = self.section_reader(self.first_footnote_page)
fnote_ids = deXOR(sect, 0, self.xortable)
# the remaining records of the footnote sections need to be decoded with the content_key and zlib inflated
des = Des(fixKey(self.content_key))
for i in xrange(1,self.num_footnote_pages):
des = DES.new(fixKey(self.content_key), DES.MODE_ECB)
for i in range(1,self.num_footnote_pages):
logging.debug('get footnotepage %d', i)
id_len = ord(fnote_ids[2])
id = fnote_ids[3:3+id_len]
@ -445,8 +349,8 @@ class EreaderProcessor(object):
sect = self.section_reader(self.first_sidebar_page)
sbar_ids = deXOR(sect, 0, self.xortable)
# the remaining records of the sidebar sections need to be decoded with the content_key and zlib inflated
des = Des(fixKey(self.content_key))
for i in xrange(1,self.num_sidebar_pages):
des = DES.new(fixKey(self.content_key), DES.MODE_ECB)
for i in range(1,self.num_sidebar_pages):
id_len = ord(sbar_ids[2])
id = sbar_ids[3:3+id_len]
smarker = '<sidebar id="%s">\n' % id
@ -460,9 +364,8 @@ class EreaderProcessor(object):
def cleanPML(pml):
# Convert special characters to proper PML code. High ASCII start at (\x80, \a128) and go up to (\xff, \a255)
pml2 = pml
for k in xrange(128,256):
badChar = chr(k)
pml2 = pml2.replace(badChar, '\\a%03d' % k)
for k in range(128,256):
pml2 = pml2.replace(bytes([k]), b'\\a%03d' % k)
return pml2
def decryptBook(infile, outpath, make_pmlz, user_key):
@ -471,35 +374,35 @@ def decryptBook(infile, outpath, make_pmlz, user_key):
# outpath is actually pmlz name
pmlzname = outpath
outdir = tempfile.mkdtemp()
imagedirpath = os.path.join(outdir,u"images")
imagedirpath = os.path.join(outdir,"images")
else:
pmlzname = None
outdir = outpath
imagedirpath = os.path.join(outdir,bookname + u"_img")
imagedirpath = os.path.join(outdir,bookname + "_img")
try:
if not os.path.exists(outdir):
os.makedirs(outdir)
print u"Decoding File"
sect = Sectionizer(infile, 'PNRdPPrs')
print("Decoding File")
sect =Sectionizer(infile, b'PNRdPPrs')
er = EreaderProcessor(sect, user_key)
if er.getNumImages() > 0:
print u"Extracting images"
print("Extracting images")
if not os.path.exists(imagedirpath):
os.makedirs(imagedirpath)
for i in xrange(er.getNumImages()):
for i in range(er.getNumImages()):
name, contents = er.getImage(i)
file(os.path.join(imagedirpath, name), 'wb').write(contents)
open(os.path.join(imagedirpath, name), 'wb').write(contents)
print u"Extracting pml"
print("Extracting pml")
pml_string = er.getText()
pmlfilename = bookname + ".pml"
file(os.path.join(outdir, pmlfilename),'wb').write(cleanPML(pml_string))
open(os.path.join(outdir, pmlfilename),'wb').write(cleanPML(pml_string))
if pmlzname is not None:
import zipfile
import shutil
print u"Creating PMLZ file {0}".format(os.path.basename(pmlzname))
print("Creating PMLZ file {0}".format(os.path.basename(pmlzname)))
myZipFile = zipfile.ZipFile(pmlzname,'w',zipfile.ZIP_STORED, False)
list = os.listdir(outdir)
for filename in list:
@ -518,48 +421,48 @@ def decryptBook(infile, outpath, make_pmlz, user_key):
myZipFile.close()
# remove temporary directory
shutil.rmtree(outdir, True)
print u"Output is {0}".format(pmlzname)
else :
print u"Output is in {0}".format(outdir)
print "done"
except ValueError, e:
print u"Error: {0}".format(e)
print("Output is {0}".format(pmlzname))
else:
print("Output is in {0}".format(outdir))
print("done")
except ValueError as e:
print("Error: {0}".format(e))
traceback.print_exc()
return 1
return 0
def usage():
print u"Converts DRMed eReader books to PML Source"
print u"Usage:"
print u" erdr2pml [options] infile.pdb [outpath] \"your name\" credit_card_number"
print u" "
print u"Options: "
print u" -h prints this message"
print u" -p create PMLZ instead of source folder"
print u" --make-pmlz create PMLZ instead of source folder"
print u" "
print u"Note:"
print u" if outpath is ommitted, creates source in 'infile_Source' folder"
print u" if outpath is ommitted and pmlz option, creates PMLZ 'infile.pmlz'"
print u" if source folder created, images are in infile_img folder"
print u" if pmlz file created, images are in images folder"
print u" It's enough to enter the last 8 digits of the credit card number"
print("Converts DRMed eReader books to PML Source")
print("Usage:")
print(" erdr2pml [options] infile.pdb [outpath] \"your name\" credit_card_number")
print(" ")
print("Options: ")
print(" -h prints this message")
print(" -p create PMLZ instead of source folder")
print(" --make-pmlz create PMLZ instead of source folder")
print(" ")
print("Note:")
print(" if outpath is ommitted, creates source in 'infile_Source' folder")
print(" if outpath is ommitted and pmlz option, creates PMLZ 'infile.pmlz'")
print(" if source folder created, images are in infile_img folder")
print(" if pmlz file created, images are in images folder")
print(" It's enough to enter the last 8 digits of the credit card number")
return
def getuser_key(name,cc):
newname = "".join(c for c in name.lower() if c >= 'a' and c <= 'z' or c >= '0' and c <= '9')
cc = cc.replace(" ","")
return struct.pack('>LL', binascii.crc32(newname) & 0xffffffff,binascii.crc32(cc[-8:])& 0xffffffff)
return struct.pack('>LL', binascii.crc32(bytes(newname.encode('utf-8'))) & 0xffffffff, binascii.crc32(bytes(cc[-8:].encode('utf-8'))) & 0xffffffff)
def cli_main():
print u"eRdr2Pml v{0}. Copyright © 20092012 The Dark Reverser et al.".format(__version__)
print("eRdr2Pml v{0}. Copyright © 20092020 The Dark Reverser et al.".format(__version__))
argv=unicode_argv()
argv=unicode_argv("erdr2pml.py")
try:
opts, args = getopt.getopt(argv[1:], "hp", ["make-pmlz"])
except getopt.GetoptError, err:
print err.args[0]
except getopt.GetoptError as err:
print(err.args[0])
usage()
return 1
make_pmlz = False
@ -579,13 +482,13 @@ def cli_main():
if len(args)==3:
infile, name, cc = args
if make_pmlz:
outpath = os.path.splitext(infile)[0] + u".pmlz"
outpath = os.path.splitext(infile)[0] + ".pmlz"
else:
outpath = os.path.splitext(infile)[0] + u"_Source"
outpath = os.path.splitext(infile)[0] + "_Source"
elif len(args)==4:
infile, outpath, name, cc = args
print getuser_key(name,cc).encode('hex')
print(binascii.b2a_hex(getuser_key(name,cc)))
return decryptBook(infile, outpath, make_pmlz, getuser_key(name,cc))

View file

@ -7,6 +7,7 @@ import csv
import os
import math
import getopt
import functools
from struct import pack
from struct import unpack
@ -15,14 +16,14 @@ class DocParser(object):
def __init__(self, flatxml, classlst, fileid, bookDir, gdict, fixedimage):
self.id = os.path.basename(fileid).replace('.dat','')
self.svgcount = 0
self.docList = flatxml.split('\n')
self.docList = flatxml.split(b'\n')
self.docSize = len(self.docList)
self.classList = {}
self.bookDir = bookDir
self.gdict = gdict
tmpList = classlst.split('\n')
for pclass in tmpList:
if pclass != '':
if pclass != b'':
# remove the leading period from the css name
cname = pclass[1:]
self.classList[cname] = True
@ -57,9 +58,9 @@ class DocParser(object):
imgfile = os.path.join(imgDir,imgname)
# get glyph information
gxList = self.getData('info.glyph.x',0,-1)
gyList = self.getData('info.glyph.y',0,-1)
gidList = self.getData('info.glyph.glyphID',0,-1)
gxList = self.getData(b'info.glyph.x',0,-1)
gyList = self.getData(b'info.glyph.y',0,-1)
gidList = self.getData(b'info.glyph.glyphID',0,-1)
gids = []
maxws = []
@ -94,7 +95,7 @@ class DocParser(object):
# change the origin to minx, miny and calc max height and width
maxw = maxws[0] + xs[0] - minx
maxh = maxhs[0] + ys[0] - miny
for j in xrange(0, len(xs)):
for j in range(0, len(xs)):
xs[j] = xs[j] - minx
ys[j] = ys[j] - miny
maxw = max( maxw, (maxws[j] + xs[j]) )
@ -106,10 +107,10 @@ class DocParser(object):
ifile.write('<!DOCTYPE svg PUBLIC "-//W3C/DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n')
ifile.write('<svg width="%dpx" height="%dpx" viewBox="0 0 %d %d" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">\n' % (math.floor(maxw/10), math.floor(maxh/10), maxw, maxh))
ifile.write('<defs>\n')
for j in xrange(0,len(gdefs)):
for j in range(0,len(gdefs)):
ifile.write(gdefs[j])
ifile.write('</defs>\n')
for j in xrange(0,len(gids)):
for j in range(0,len(gids)):
ifile.write('<use xlink:href="#gl%d" x="%d" y="%d" />\n' % (gids[j], xs[j], ys[j]))
ifile.write('</svg>')
ifile.close()
@ -122,11 +123,11 @@ class DocParser(object):
def lineinDoc(self, pos) :
if (pos >= 0) and (pos < self.docSize) :
item = self.docList[pos]
if item.find('=') >= 0:
(name, argres) = item.split('=',1)
if item.find(b'=') >= 0:
(name, argres) = item.split(b'=',1)
else :
name = item
argres = ''
argres = b''
return name, argres
@ -138,13 +139,15 @@ class DocParser(object):
else:
end = min(self.docSize, end)
foundat = -1
for j in xrange(pos, end):
for j in range(pos, end):
item = self.docList[j]
if item.find('=') >= 0:
(name, argres) = item.split('=',1)
if item.find(b'=') >= 0:
(name, argres) = item.split(b'=',1)
else :
name = item
argres = ''
if (isinstance(tagpath,str)):
tagpath = tagpath.encode('utf-8')
if name.endswith(tagpath) :
result = argres
foundat = j
@ -170,7 +173,7 @@ class DocParser(object):
argres=[]
(foundat, argt) = self.findinDoc(tagpath, pos, end)
if (argt != None) and (len(argt) > 0) :
argList = argt.split('|')
argList = argt.split(b'|')
argres = [ int(strval) for strval in argList]
return argres
@ -191,21 +194,21 @@ class DocParser(object):
# also some class names have spaces in them so need to convert to dashes
if nclass != None :
nclass = nclass.replace(' ','-')
classres = ''
nclass = nclass.replace(b' ',b'-')
classres = b''
nclass = nclass.lower()
nclass = 'cl-' + nclass
baseclass = ''
nclass = b'cl-' + nclass
baseclass = b''
# graphic is the base class for captions
if nclass.find('cl-cap-') >=0 :
classres = 'graphic' + ' '
if nclass.find(b'cl-cap-') >=0 :
classres = b'graphic' + b' '
else :
# strip to find baseclass
p = nclass.find('_')
p = nclass.find(b'_')
if p > 0 :
baseclass = nclass[0:p]
if baseclass in self.classList:
classres += baseclass + ' '
classres += baseclass + b' '
classres += nclass
nclass = classres
return nclass
@ -225,11 +228,11 @@ class DocParser(object):
return -1
result = []
(pos, pagetype) = self.findinDoc('page.type',0,-1)
(pos, pagetype) = self.findinDoc(b'page.type',0,-1)
groupList = self.posinDoc('page.group')
groupregionList = self.posinDoc('page.group.region')
pageregionList = self.posinDoc('page.region')
groupList = self.posinDoc(b'page.group')
groupregionList = self.posinDoc(b'page.group.region')
pageregionList = self.posinDoc(b'page.region')
# integrate into one list
for j in groupList:
result.append(('grpbeg',j))
@ -237,7 +240,7 @@ class DocParser(object):
result.append(('gregion',j))
for j in pageregionList:
result.append(('pregion',j))
result.sort(compare)
result.sort(key=functools.cmp_to_key(compare))
# insert group end and page end indicators
inGroup = False
@ -267,39 +270,39 @@ class DocParser(object):
result = []
# paragraph
(pos, pclass) = self.findinDoc('paragraph.class',start,end)
(pos, pclass) = self.findinDoc(b'paragraph.class',start,end)
pclass = self.getClass(pclass)
# if paragraph uses extratokens (extra glyphs) then make it fixed
(pos, extraglyphs) = self.findinDoc('paragraph.extratokens',start,end)
(pos, extraglyphs) = self.findinDoc(b'paragraph.extratokens',start,end)
# build up a description of the paragraph in result and return it
# first check for the basic - all words paragraph
(pos, sfirst) = self.findinDoc('paragraph.firstWord',start,end)
(pos, slast) = self.findinDoc('paragraph.lastWord',start,end)
(pos, sfirst) = self.findinDoc(b'paragraph.firstWord',start,end)
(pos, slast) = self.findinDoc(b'paragraph.lastWord',start,end)
if (sfirst != None) and (slast != None) :
first = int(sfirst)
last = int(slast)
makeImage = (regtype == 'vertical') or (regtype == 'table')
makeImage = (regtype == b'vertical') or (regtype == b'table')
makeImage = makeImage or (extraglyphs != None)
if self.fixedimage:
makeImage = makeImage or (regtype == 'fixed')
makeImage = makeImage or (regtype == b'fixed')
if (pclass != None):
makeImage = makeImage or (pclass.find('.inverted') >= 0)
makeImage = makeImage or (pclass.find(b'.inverted') >= 0)
if self.fixedimage :
makeImage = makeImage or (pclass.find('cl-f-') >= 0)
makeImage = makeImage or (pclass.find(b'cl-f-') >= 0)
# before creating an image make sure glyph info exists
gidList = self.getData('info.glyph.glyphID',0,-1)
gidList = self.getData(b'info.glyph.glyphID',0,-1)
makeImage = makeImage & (len(gidList) > 0)
if not makeImage :
# standard all word paragraph
for wordnum in xrange(first, last):
for wordnum in range(first, last):
result.append(('ocr', wordnum))
return pclass, result
@ -307,8 +310,8 @@ class DocParser(object):
# translate first and last word into first and last glyphs
# and generate inline image and include it
glyphList = []
firstglyphList = self.getData('word.firstGlyph',0,-1)
gidList = self.getData('info.glyph.glyphID',0,-1)
firstglyphList = self.getData(b'word.firstGlyph',0,-1)
gidList = self.getData(b'info.glyph.glyphID',0,-1)
firstGlyph = firstglyphList[first]
if last < len(firstglyphList):
lastGlyph = firstglyphList[last]
@ -319,17 +322,17 @@ class DocParser(object):
# by reverting to text based paragraph
if firstGlyph >= lastGlyph:
# revert to standard text based paragraph
for wordnum in xrange(first, last):
for wordnum in range(first, last):
result.append(('ocr', wordnum))
return pclass, result
for glyphnum in xrange(firstGlyph, lastGlyph):
for glyphnum in range(firstGlyph, lastGlyph):
glyphList.append(glyphnum)
# include any extratokens if they exist
(pos, sfg) = self.findinDoc('extratokens.firstGlyph',start,end)
(pos, slg) = self.findinDoc('extratokens.lastGlyph',start,end)
(pos, sfg) = self.findinDoc(b'extratokens.firstGlyph',start,end)
(pos, slg) = self.findinDoc(b'extratokens.lastGlyph',start,end)
if (sfg != None) and (slg != None):
for glyphnum in xrange(int(sfg), int(slg)):
for glyphnum in range(int(sfg), int(slg)):
glyphList.append(glyphnum)
num = self.svgcount
self.glyphs_to_image(glyphList)
@ -368,50 +371,50 @@ class DocParser(object):
(name, argres) = self.lineinDoc(line)
if name.endswith('span.firstWord') :
if name.endswith(b'span.firstWord') :
sp_first = int(argres)
elif name.endswith('span.lastWord') :
elif name.endswith(b'span.lastWord') :
sp_last = int(argres)
elif name.endswith('word.firstGlyph') :
elif name.endswith(b'word.firstGlyph') :
gl_first = int(argres)
elif name.endswith('word.lastGlyph') :
elif name.endswith(b'word.lastGlyph') :
gl_last = int(argres)
elif name.endswith('word_semantic.firstWord'):
elif name.endswith(b'word_semantic.firstWord'):
ws_first = int(argres)
elif name.endswith('word_semantic.lastWord'):
elif name.endswith(b'word_semantic.lastWord'):
ws_last = int(argres)
elif name.endswith('word.class'):
elif name.endswith(b'word.class'):
# we only handle spaceafter word class
try:
(cname, space) = argres.split('-',1)
if space == '' : space = '0'
if (cname == 'spaceafter') and (int(space) > 0) :
(cname, space) = argres.split(b'-',1)
if space == b'' : space = b'0'
if (cname == b'spaceafter') and (int(space) > 0) :
word_class = 'sa'
except:
pass
elif name.endswith('word.img.src'):
elif name.endswith(b'word.img.src'):
result.append(('img' + word_class, int(argres)))
word_class = ''
elif name.endswith('region.img.src'):
elif name.endswith(b'region.img.src'):
result.append(('img' + word_class, int(argres)))
if (sp_first != -1) and (sp_last != -1):
for wordnum in xrange(sp_first, sp_last):
for wordnum in range(sp_first, sp_last):
result.append(('ocr', wordnum))
sp_first = -1
sp_last = -1
if (gl_first != -1) and (gl_last != -1):
glyphList = []
for glyphnum in xrange(gl_first, gl_last):
for glyphnum in range(gl_first, gl_last):
glyphList.append(glyphnum)
num = self.svgcount
self.glyphs_to_image(glyphList)
@ -421,7 +424,7 @@ class DocParser(object):
gl_last = -1
if (ws_first != -1) and (ws_last != -1):
for wordnum in xrange(ws_first, ws_last):
for wordnum in range(ws_first, ws_last):
result.append(('ocr', wordnum))
ws_first = -1
ws_last = -1
@ -437,7 +440,7 @@ class DocParser(object):
classres = ''
if pclass :
classres = ' class="' + pclass + '"'
classres = ' class="' + pclass.decode('utf-8') + '"'
br_lb = (regtype == 'fixed') or (regtype == 'chapterheading') or (regtype == 'vertical')
@ -453,7 +456,7 @@ class DocParser(object):
cnt = len(pdesc)
for j in xrange( 0, cnt) :
for j in range( 0, cnt) :
(wtype, num) = pdesc[j]
@ -470,6 +473,8 @@ class DocParser(object):
if (link > 0):
linktype = self.link_type[link-1]
title = self.link_title[link-1]
if isinstance(title, bytes):
title = title.decode('utf-8')
if (title == "") or (parares.rfind(title) < 0):
title=parares[lstart:]
if linktype == 'external' :
@ -482,33 +487,34 @@ class DocParser(object):
else :
# just link to the current page
linkhtml = '<a href="#' + self.id + '">'
linkhtml += title + '</a>'
linkhtml += title
linkhtml += '</a>'
pos = parares.rfind(title)
if pos >= 0:
parares = parares[0:pos] + linkhtml + parares[pos+len(title):]
else :
parares += linkhtml
lstart = len(parares)
if word == '_link_' : word = ''
if word == b'_link_' : word = b''
elif (link < 0) :
if word == '_link_' : word = ''
if word == b'_link_' : word = b''
if word == '_lb_':
if word == b'_lb_':
if ((num-1) in self.dehyphen_rootid ) or handle_links:
word = ''
word = b''
sep = ''
elif br_lb :
word = '<br />\n'
word = b'<br />\n'
sep = ''
else :
word = '\n'
word = b'\n'
sep = ''
if num in self.dehyphen_rootid :
word = word[0:-1]
sep = ''
parares += word + sep
parares += word.decode('utf-8') + sep
elif wtype == 'img' :
sep = ''
@ -522,7 +528,9 @@ class DocParser(object):
elif wtype == 'svg' :
sep = ''
parares += '<img src="img/' + self.id + '_%04d.svg" alt="" />' % num
parares += '<img src="img/'
parares += self.id
parares += '_%04d.svg" alt="" />' % num
parares += sep
if len(sep) > 0 : parares = parares[0:-1]
@ -540,12 +548,12 @@ class DocParser(object):
lstart = 0
cnt = len(pdesc)
for j in xrange( 0, cnt) :
for j in range( 0, cnt) :
(wtype, num) = pdesc[j]
if wtype == 'ocr' :
word = self.ocrtext[num]
word = self.ocrtext[num].decode('utf-8')
sep = ' '
if handle_links:
@ -553,7 +561,7 @@ class DocParser(object):
if (link > 0):
linktype = self.link_type[link-1]
title = self.link_title[link-1]
title = title.rstrip('. ')
title = title.rstrip(b'. ').decode('utf-8')
alt_title = parares[lstart:]
alt_title = alt_title.strip()
# now strip off the actual printed page number
@ -607,38 +615,38 @@ class DocParser(object):
hlst = []
# get the ocr text
(pos, argres) = self.findinDoc('info.word.ocrText',0,-1)
if argres : self.ocrtext = argres.split('|')
(pos, argres) = self.findinDoc(b'info.word.ocrText',0,-1)
if argres : self.ocrtext = argres.split(b'|')
# get information to dehyphenate the text
self.dehyphen_rootid = self.getData('info.dehyphen.rootID',0,-1)
self.dehyphen_rootid = self.getData(b'info.dehyphen.rootID',0,-1)
# determine if first paragraph is continued from previous page
(pos, self.parastems_stemid) = self.findinDoc('info.paraStems.stemID',0,-1)
(pos, self.parastems_stemid) = self.findinDoc(b'info.paraStems.stemID',0,-1)
first_para_continued = (self.parastems_stemid != None)
# determine if last paragraph is continued onto the next page
(pos, self.paracont_stemid) = self.findinDoc('info.paraCont.stemID',0,-1)
(pos, self.paracont_stemid) = self.findinDoc(b'info.paraCont.stemID',0,-1)
last_para_continued = (self.paracont_stemid != None)
# collect link ids
self.link_id = self.getData('info.word.link_id',0,-1)
self.link_id = self.getData(b'info.word.link_id',0,-1)
# collect link destination page numbers
self.link_page = self.getData('info.links.page',0,-1)
self.link_page = self.getData(b'info.links.page',0,-1)
# collect link types (container versus external)
(pos, argres) = self.findinDoc('info.links.type',0,-1)
if argres : self.link_type = argres.split('|')
(pos, argres) = self.findinDoc(b'info.links.type',0,-1)
if argres : self.link_type = argres.split(b'|')
# collect link destinations
(pos, argres) = self.findinDoc('info.links.href',0,-1)
if argres : self.link_href = argres.split('|')
(pos, argres) = self.findinDoc(b'info.links.href',0,-1)
if argres : self.link_href = argres.split(b'|')
# collect link titles
(pos, argres) = self.findinDoc('info.links.title',0,-1)
(pos, argres) = self.findinDoc(b'info.links.title',0,-1)
if argres :
self.link_title = argres.split('|')
self.link_title = argres.split(b'|')
else:
self.link_title.append('')
@ -653,7 +661,7 @@ class DocParser(object):
# process each region on the page and convert what you can to html
for j in xrange(regcnt):
for j in range(regcnt):
(etype, start) = pageDesc[j]
(ntype, end) = pageDesc[j+1]
@ -662,51 +670,51 @@ class DocParser(object):
# set anchor for link target on this page
if not anchorSet and not first_para_continued:
hlst.append('<div style="visibility: hidden; height: 0; width: 0;" id="')
hlst.append(self.id + '" title="pagetype_' + pagetype + '"></div>\n')
hlst.append(self.id + '" title="pagetype_' + pagetype.decode('utf-8') + '"></div>\n')
anchorSet = True
# handle groups of graphics with text captions
if (etype == 'grpbeg'):
(pos, grptype) = self.findinDoc('group.type', start, end)
if (etype == b'grpbeg'):
(pos, grptype) = self.findinDoc(b'group.type', start, end)
if grptype != None:
if grptype == 'graphic':
gcstr = ' class="' + grptype + '"'
if grptype == b'graphic':
gcstr = ' class="' + grptype.decode('utf-8') + '"'
hlst.append('<div' + gcstr + '>')
inGroup = True
elif (etype == 'grpend'):
elif (etype == b'grpend'):
if inGroup:
hlst.append('</div>\n')
inGroup = False
else:
(pos, regtype) = self.findinDoc('region.type',start,end)
(pos, regtype) = self.findinDoc(b'region.type',start,end)
if regtype == 'graphic' :
(pos, simgsrc) = self.findinDoc('img.src',start,end)
if regtype == b'graphic' :
(pos, simgsrc) = self.findinDoc(b'img.src',start,end)
if simgsrc:
if inGroup:
hlst.append('<img src="img/img%04d.jpg" alt="" />' % int(simgsrc))
else:
hlst.append('<div class="graphic"><img src="img/img%04d.jpg" alt="" /></div>' % int(simgsrc))
elif regtype == 'chapterheading' :
elif regtype == b'chapterheading' :
(pclass, pdesc) = self.getParaDescription(start,end, regtype)
if not breakSet:
hlst.append('<div style="page-break-after: always;">&nbsp;</div>\n')
breakSet = True
tag = 'h1'
if pclass and (len(pclass) >= 7):
if pclass[3:7] == 'ch1-' : tag = 'h1'
if pclass[3:7] == 'ch2-' : tag = 'h2'
if pclass[3:7] == 'ch3-' : tag = 'h3'
hlst.append('<' + tag + ' class="' + pclass + '">')
if pclass[3:7] == b'ch1-' : tag = 'h1'
if pclass[3:7] == b'ch2-' : tag = 'h2'
if pclass[3:7] == b'ch3-' : tag = 'h3'
hlst.append('<' + tag + ' class="' + pclass.decode('utf-8') + '">')
else:
hlst.append('<' + tag + '>')
hlst.append(self.buildParagraph(pclass, pdesc, 'middle', regtype))
hlst.append('</' + tag + '>')
elif (regtype == 'text') or (regtype == 'fixed') or (regtype == 'insert') or (regtype == 'listitem'):
elif (regtype == b'text') or (regtype == b'fixed') or (regtype == b'insert') or (regtype == b'listitem'):
ptype = 'full'
# check to see if this is a continution from the previous page
if first_para_continued :
@ -715,16 +723,16 @@ class DocParser(object):
(pclass, pdesc) = self.getParaDescription(start,end, regtype)
if pclass and (len(pclass) >= 6) and (ptype == 'full'):
tag = 'p'
if pclass[3:6] == 'h1-' : tag = 'h4'
if pclass[3:6] == 'h2-' : tag = 'h5'
if pclass[3:6] == 'h3-' : tag = 'h6'
hlst.append('<' + tag + ' class="' + pclass + '">')
if pclass[3:6] == b'h1-' : tag = 'h4'
if pclass[3:6] == b'h2-' : tag = 'h5'
if pclass[3:6] == b'h3-' : tag = 'h6'
hlst.append('<' + tag + ' class="' + pclass.decode('utf-8') + '">')
hlst.append(self.buildParagraph(pclass, pdesc, 'middle', regtype))
hlst.append('</' + tag + '>')
else :
hlst.append(self.buildParagraph(pclass, pdesc, ptype, regtype))
elif (regtype == 'tocentry') :
elif (regtype == b'tocentry') :
ptype = 'full'
if first_para_continued :
ptype = 'end'
@ -733,7 +741,7 @@ class DocParser(object):
tocinfo += self.buildTOCEntry(pdesc)
hlst.append(self.buildParagraph(pclass, pdesc, ptype, regtype))
elif (regtype == 'vertical') or (regtype == 'table') :
elif (regtype == b'vertical') or (regtype == b'table') :
ptype = 'full'
if inGroup:
ptype = 'middle'
@ -744,19 +752,19 @@ class DocParser(object):
hlst.append(self.buildParagraph(pclass, pdesc, ptype, regtype))
elif (regtype == 'synth_fcvr.center'):
(pos, simgsrc) = self.findinDoc('img.src',start,end)
elif (regtype == b'synth_fcvr.center'):
(pos, simgsrc) = self.findinDoc(b'img.src',start,end)
if simgsrc:
hlst.append('<div class="graphic"><img src="img/img%04d.jpg" alt="" /></div>' % int(simgsrc))
else :
print ' Making region type', regtype,
(pos, temp) = self.findinDoc('paragraph',start,end)
(pos2, temp) = self.findinDoc('span',start,end)
print(' Making region type', regtype, end=' ')
(pos, temp) = self.findinDoc(b'paragraph',start,end)
(pos2, temp) = self.findinDoc(b'span',start,end)
if pos != -1 or pos2 != -1:
print ' a "text" region'
print(' a "text" region')
orig_regtype = regtype
regtype = 'fixed'
regtype = b'fixed'
ptype = 'full'
# check to see if this is a continution from the previous page
if first_para_continued :
@ -764,23 +772,23 @@ class DocParser(object):
first_para_continued = False
(pclass, pdesc) = self.getParaDescription(start,end, regtype)
if not pclass:
if orig_regtype.endswith('.right') : pclass = 'cl-right'
elif orig_regtype.endswith('.center') : pclass = 'cl-center'
elif orig_regtype.endswith('.left') : pclass = 'cl-left'
elif orig_regtype.endswith('.justify') : pclass = 'cl-justify'
if orig_regtype.endswith(b'.right') : pclass = b'cl-right'
elif orig_regtype.endswith(b'.center') : pclass = b'cl-center'
elif orig_regtype.endswith(b'.left') : pclass = b'cl-left'
elif orig_regtype.endswith(b'.justify') : pclass = b'cl-justify'
if pclass and (ptype == 'full') and (len(pclass) >= 6):
tag = 'p'
if pclass[3:6] == 'h1-' : tag = 'h4'
if pclass[3:6] == 'h2-' : tag = 'h5'
if pclass[3:6] == 'h3-' : tag = 'h6'
hlst.append('<' + tag + ' class="' + pclass + '">')
if pclass[3:6] == b'h1-' : tag = 'h4'
if pclass[3:6] == b'h2-' : tag = 'h5'
if pclass[3:6] == b'h3-' : tag = 'h6'
hlst.append('<' + tag + ' class="' + pclass.decode('utf-8') + '">')
hlst.append(self.buildParagraph(pclass, pdesc, 'middle', regtype))
hlst.append('</' + tag + '>')
else :
hlst.append(self.buildParagraph(pclass, pdesc, ptype, regtype))
else :
print ' a "graphic" region'
(pos, simgsrc) = self.findinDoc('img.src',start,end)
print(' a "graphic" region')
(pos, simgsrc) = self.findinDoc(b'img.src',start,end)
if simgsrc:
hlst.append('<div class="graphic"><img src="img/img%04d.jpg" alt="" /></div>' % int(simgsrc))

View file

@ -12,7 +12,7 @@ from struct import unpack
class PParser(object):
def __init__(self, gd, flatxml, meta_array):
self.gd = gd
self.flatdoc = flatxml.split('\n')
self.flatdoc = flatxml.split(b'\n')
self.docSize = len(self.flatdoc)
self.temp = []
@ -58,11 +58,11 @@ class PParser(object):
def lineinDoc(self, pos) :
if (pos >= 0) and (pos < self.docSize) :
item = self.flatdoc[pos]
if item.find('=') >= 0:
(name, argres) = item.split('=',1)
if item.find(b'=') >= 0:
(name, argres) = item.split(b'=',1)
else :
name = item
argres = ''
argres = b''
return name, argres
# find tag in doc if within pos to end inclusive
@ -73,13 +73,15 @@ class PParser(object):
else:
end = min(self.docSize, end)
foundat = -1
for j in xrange(pos, end):
for j in range(pos, end):
item = self.flatdoc[j]
if item.find('=') >= 0:
(name, argres) = item.split('=',1)
if item.find(b'=') >= 0:
(name, argres) = item.split(b'=',1)
else :
name = item
argres = ''
argres = b''
if (isinstance(tagpath,str)):
tagpath = tagpath.encode('utf-8')
if name.endswith(tagpath) :
result = argres
foundat = j
@ -101,11 +103,11 @@ class PParser(object):
def getData(self, path):
result = None
cnt = len(self.flatdoc)
for j in xrange(cnt):
for j in range(cnt):
item = self.flatdoc[j]
if item.find('=') >= 0:
(name, argt) = item.split('=')
argres = argt.split('|')
if item.find(b'=') >= 0:
(name, argt) = item.split(b'=')
argres = argt.split(b'|')
else:
name = item
argres = []
@ -113,22 +115,24 @@ class PParser(object):
result = argres
break
if (len(argres) > 0) :
for j in xrange(0,len(argres)):
for j in range(0,len(argres)):
argres[j] = int(argres[j])
return result
def getDataatPos(self, path, pos):
result = None
item = self.flatdoc[pos]
if item.find('=') >= 0:
(name, argt) = item.split('=')
argres = argt.split('|')
if item.find(b'=') >= 0:
(name, argt) = item.split(b'=')
argres = argt.split(b'|')
else:
name = item
argres = []
if (len(argres) > 0) :
for j in xrange(0,len(argres)):
for j in range(0,len(argres)):
argres[j] = int(argres[j])
if (isinstance(path,str)):
path = path.encode('utf-8')
if (name.endswith(path)):
result = argres
return result
@ -136,20 +140,22 @@ class PParser(object):
def getDataTemp(self, path):
result = None
cnt = len(self.temp)
for j in xrange(cnt):
for j in range(cnt):
item = self.temp[j]
if item.find('=') >= 0:
(name, argt) = item.split('=')
argres = argt.split('|')
if item.find(b'=') >= 0:
(name, argt) = item.split(b'=')
argres = argt.split(b'|')
else:
name = item
argres = []
if (isinstance(path,str)):
path = path.encode('utf-8')
if (name.endswith(path)):
result = argres
self.temp.pop(j)
break
if (len(argres) > 0) :
for j in xrange(0,len(argres)):
for j in range(0,len(argres)):
argres[j] = int(argres[j])
return result
@ -220,15 +226,15 @@ def convert2SVG(gdict, flat_xml, pageid, previd, nextid, svgDir, raw, meta_array
if (pp.gid != None):
mlst.append('<defs>\n')
gdefs = pp.getGlyphs()
for j in xrange(0,len(gdefs)):
for j in range(0,len(gdefs)):
mlst.append(gdefs[j])
mlst.append('</defs>\n')
img = pp.getImages()
if (img != None):
for j in xrange(0,len(img)):
for j in range(0,len(img)):
mlst.append(img[j])
if (pp.gid != None):
for j in xrange(0,len(pp.gid)):
for j in range(0,len(pp.gid)):
mlst.append('<use xlink:href="#gl%d" x="%d" y="%d" />\n' % (pp.gid[j], pp.gx[j], pp.gy[j]))
if (img == None or len(img) == 0) and (pp.gid == None or len(pp.gid) == 0):
xpos = "%d" % (pp.pw // 3)

View file

@ -1,43 +1,31 @@
#! /usr/bin/python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
# Python 3 for calibre 5.0
from __future__ import print_function
class Unbuffered:
def __init__(self, stream):
self.stream = stream
def write(self, data):
self.stream.write(data)
self.stream.flush()
def __getattr__(self, attr):
return getattr(self.stream, attr)
#@@CALIBRE_COMPAT_CODE@@
from .utilities import SafeUnbuffered
import sys
sys.stdout=Unbuffered(sys.stdout)
import csv
import os
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
@ -84,13 +72,13 @@ def readString(file):
def getMetaArray(metaFile):
# parse the meta file
result = {}
fo = file(metaFile,'rb')
fo = open(metaFile,'rb')
size = readEncodedNumber(fo)
for i in xrange(size):
for i in range(size):
tag = readString(fo)
value = readString(fo)
result[tag] = value
# print tag, value
# print(tag, value)
fo.close()
return result
@ -100,24 +88,24 @@ class Dictionary(object):
def __init__(self, dictFile):
self.filename = dictFile
self.size = 0
self.fo = file(dictFile,'rb')
self.fo = open(dictFile,'rb')
self.stable = []
self.size = readEncodedNumber(self.fo)
for i in xrange(self.size):
for i in range(self.size):
self.stable.append(self.escapestr(readString(self.fo)))
self.pos = 0
def escapestr(self, str):
str = str.replace('&','&amp;')
str = str.replace('<','&lt;')
str = str.replace('>','&gt;')
str = str.replace('=','&#61;')
str = str.replace(b'&',b'&amp;')
str = str.replace(b'<',b'&lt;')
str = str.replace(b'>',b'&gt;')
str = str.replace(b'=',b'&#61;')
return str
def lookup(self,val):
if ((val >= 0) and (val < self.size)) :
self.pos = val
return self.stable[self.pos]
else:
print "Error: %d outside of string table limits" % val
print("Error: %d outside of string table limits" % val)
raise TpzDRMError('outside or string table limits')
# sys.exit(-1)
def getSize(self):
@ -128,7 +116,7 @@ class Dictionary(object):
class PageDimParser(object):
def __init__(self, flatxml):
self.flatdoc = flatxml.split('\n')
self.flatdoc = flatxml.split(b'\n')
# find tag if within pos to end inclusive
def findinDoc(self, tagpath, pos, end) :
result = None
@ -139,10 +127,10 @@ class PageDimParser(object):
else:
end = min(cnt,end)
foundat = -1
for j in xrange(pos, end):
for j in range(pos, end):
item = docList[j]
if item.find('=') >= 0:
(name, argres) = item.split('=')
if item.find(b'=') >= 0:
(name, argres) = item.split(b'=')
else :
name = item
argres = ''
@ -152,8 +140,8 @@ class PageDimParser(object):
break
return foundat, result
def process(self):
(pos, sph) = self.findinDoc('page.h',0,-1)
(pos, spw) = self.findinDoc('page.w',0,-1)
(pos, sph) = self.findinDoc(b'page.h',0,-1)
(pos, spw) = self.findinDoc(b'page.w',0,-1)
if (sph == None): sph = '-1'
if (spw == None): spw = '-1'
return sph, spw
@ -166,21 +154,21 @@ def getPageDim(flatxml):
class GParser(object):
def __init__(self, flatxml):
self.flatdoc = flatxml.split('\n')
self.flatdoc = flatxml.split(b'\n')
self.dpi = 1440
self.gh = self.getData('info.glyph.h')
self.gw = self.getData('info.glyph.w')
self.guse = self.getData('info.glyph.use')
self.gh = self.getData(b'info.glyph.h')
self.gw = self.getData(b'info.glyph.w')
self.guse = self.getData(b'info.glyph.use')
if self.guse :
self.count = len(self.guse)
else :
self.count = 0
self.gvtx = self.getData('info.glyph.vtx')
self.glen = self.getData('info.glyph.len')
self.gdpi = self.getData('info.glyph.dpi')
self.vx = self.getData('info.vtx.x')
self.vy = self.getData('info.vtx.y')
self.vlen = self.getData('info.len.n')
self.gvtx = self.getData(b'info.glyph.vtx')
self.glen = self.getData(b'info.glyph.len')
self.gdpi = self.getData(b'info.glyph.dpi')
self.vx = self.getData(b'info.vtx.x')
self.vy = self.getData(b'info.vtx.y')
self.vlen = self.getData(b'info.len.n')
if self.vlen :
self.glen.append(len(self.vlen))
elif self.glen:
@ -192,11 +180,11 @@ class GParser(object):
def getData(self, path):
result = None
cnt = len(self.flatdoc)
for j in xrange(cnt):
for j in range(cnt):
item = self.flatdoc[j]
if item.find('=') >= 0:
(name, argt) = item.split('=')
argres = argt.split('|')
if item.find(b'=') >= 0:
(name, argt) = item.split(b'=')
argres = argt.split(b'|')
else:
name = item
argres = []
@ -204,7 +192,7 @@ class GParser(object):
result = argres
break
if (len(argres) > 0) :
for j in xrange(0,len(argres)):
for j in range(0,len(argres)):
argres[j] = int(argres[j])
return result
def getGlyphDim(self, gly):
@ -220,7 +208,7 @@ class GParser(object):
tx = self.vx[self.gvtx[gly]:self.gvtx[gly+1]]
ty = self.vy[self.gvtx[gly]:self.gvtx[gly+1]]
p = 0
for k in xrange(self.glen[gly], self.glen[gly+1]):
for k in range(self.glen[gly], self.glen[gly+1]):
if (p == 0):
zx = tx[0:self.vlen[k]+1]
zy = ty[0:self.vlen[k]+1]
@ -268,32 +256,32 @@ class GlyphDict(object):
def generateBook(bookDir, raw, fixedimage):
# sanity check Topaz file extraction
if not os.path.exists(bookDir) :
print "Can not find directory with unencrypted book"
print("Can not find directory with unencrypted book")
return 1
dictFile = os.path.join(bookDir,'dict0000.dat')
if not os.path.exists(dictFile) :
print "Can not find dict0000.dat file"
print("Can not find dict0000.dat file")
return 1
pageDir = os.path.join(bookDir,'page')
if not os.path.exists(pageDir) :
print "Can not find page directory in unencrypted book"
print("Can not find page directory in unencrypted book")
return 1
imgDir = os.path.join(bookDir,'img')
if not os.path.exists(imgDir) :
print "Can not find image directory in unencrypted book"
print("Can not find image directory in unencrypted book")
return 1
glyphsDir = os.path.join(bookDir,'glyphs')
if not os.path.exists(glyphsDir) :
print "Can not find glyphs directory in unencrypted book"
print("Can not find glyphs directory in unencrypted book")
return 1
metaFile = os.path.join(bookDir,'metadata0000.dat')
if not os.path.exists(metaFile) :
print "Can not find metadata0000.dat in unencrypted book"
print("Can not find metadata0000.dat in unencrypted book")
return 1
svgDir = os.path.join(bookDir,'svg')
@ -307,10 +295,10 @@ def generateBook(bookDir, raw, fixedimage):
otherFile = os.path.join(bookDir,'other0000.dat')
if not os.path.exists(otherFile) :
print "Can not find other0000.dat in unencrypted book"
print("Can not find other0000.dat in unencrypted book")
return 1
print "Updating to color images if available"
print("Updating to color images if available")
spath = os.path.join(bookDir,'color_img')
dpath = os.path.join(bookDir,'img')
filenames = os.listdir(spath)
@ -319,24 +307,24 @@ def generateBook(bookDir, raw, fixedimage):
imgname = filename.replace('color','img')
sfile = os.path.join(spath,filename)
dfile = os.path.join(dpath,imgname)
imgdata = file(sfile,'rb').read()
file(dfile,'wb').write(imgdata)
imgdata = open(sfile,'rb').read()
open(dfile,'wb').write(imgdata)
print "Creating cover.jpg"
print("Creating cover.jpg")
isCover = False
cpath = os.path.join(bookDir,'img')
cpath = os.path.join(cpath,'img0000.jpg')
if os.path.isfile(cpath):
cover = file(cpath, 'rb').read()
cover = open(cpath, 'rb').read()
cpath = os.path.join(bookDir,'cover.jpg')
file(cpath, 'wb').write(cover)
open(cpath, 'wb').write(cover)
isCover = True
print 'Processing Dictionary'
print('Processing Dictionary')
dict = Dictionary(dictFile)
print 'Processing Meta Data and creating OPF'
print('Processing Meta Data and creating OPF')
meta_array = getMetaArray(metaFile)
# replace special chars in title and authors like & < >
@ -358,9 +346,9 @@ def generateBook(bookDir, raw, fixedimage):
mlst.append('<meta name="' + key + '" content="' + meta_array[key] + '" />\n')
metastr = "".join(mlst)
mlst = None
file(xname, 'wb').write(metastr)
open(xname, 'wb').write(metastr)
print 'Processing StyleSheet'
print('Processing StyleSheet')
# get some scaling info from metadata to use while processing styles
# and first page info
@ -421,12 +409,12 @@ def generateBook(bookDir, raw, fixedimage):
# now get the css info
cssstr , classlst = stylexml2css.convert2CSS(flat_xml, fontsize, ph, pw)
file(xname, 'wb').write(cssstr)
open(xname, 'w').write(cssstr)
if buildXML:
xname = os.path.join(xmlDir, 'other0000.xml')
file(xname, 'wb').write(convert2xml.getXML(dict, otherFile))
open(xname, 'wb').write(convert2xml.getXML(dict, otherFile))
print 'Processing Glyphs'
print('Processing Glyphs')
gd = GlyphDict()
filenames = os.listdir(glyphsDir)
filenames = sorted(filenames)
@ -440,16 +428,16 @@ def generateBook(bookDir, raw, fixedimage):
counter = 0
for filename in filenames:
# print ' ', filename
print '.',
print('.', end=' ')
fname = os.path.join(glyphsDir,filename)
flat_xml = convert2xml.fromData(dict, fname)
if buildXML:
xname = os.path.join(xmlDir, filename.replace('.dat','.xml'))
file(xname, 'wb').write(convert2xml.getXML(dict, fname))
open(xname, 'wb').write(convert2xml.getXML(dict, fname))
gp = GParser(flat_xml)
for i in xrange(0, gp.count):
for i in range(0, gp.count):
path = gp.getPath(i)
maxh, maxw = gp.getGlyphDim(i)
fullpath = '<path id="gl%d" d="%s" fill="black" /><!-- width=%d height=%d -->\n' % (counter * 256 + i, path, maxw, maxh)
@ -459,7 +447,7 @@ def generateBook(bookDir, raw, fixedimage):
glyfile.write('</defs>\n')
glyfile.write('</svg>\n')
glyfile.close()
print " "
print(" ")
# start up the html
@ -481,7 +469,7 @@ def generateBook(bookDir, raw, fixedimage):
hlst.append('<link href="style.css" rel="stylesheet" type="text/css" />\n')
hlst.append('</head>\n<body>\n')
print 'Processing Pages'
print('Processing Pages')
# Books are at 1440 DPI. This is rendering at twice that size for
# readability when rendering to the screen.
scaledpi = 1440.0
@ -495,7 +483,7 @@ def generateBook(bookDir, raw, fixedimage):
for filename in filenames:
# print ' ', filename
print ".",
print(".", end=' ')
fname = os.path.join(pageDir,filename)
flat_xml = convert2xml.fromData(dict, fname)
@ -504,7 +492,7 @@ def generateBook(bookDir, raw, fixedimage):
if buildXML:
xname = os.path.join(xmlDir, filename.replace('.dat','.xml'))
file(xname, 'wb').write(convert2xml.getXML(dict, fname))
open(xname, 'wb').write(convert2xml.getXML(dict, fname))
# first get the html
pagehtml, tocinfo = flatxml2html.convert2HTML(flat_xml, classlst, fname, bookDir, gd, fixedimage)
@ -515,10 +503,10 @@ def generateBook(bookDir, raw, fixedimage):
hlst.append('</body>\n</html>\n')
htmlstr = "".join(hlst)
hlst = None
file(os.path.join(bookDir, htmlFileName), 'wb').write(htmlstr)
open(os.path.join(bookDir, htmlFileName), 'w').write(htmlstr)
print " "
print 'Extracting Table of Contents from Amazon OCR'
print(" ")
print('Extracting Table of Contents from Amazon OCR')
# first create a table of contents file for the svg images
tlst = []
@ -550,7 +538,7 @@ def generateBook(bookDir, raw, fixedimage):
toclst = tocentries.split('\n')
toclst.pop()
for entry in toclst:
print entry
print(entry)
title, pagenum = entry.split('|')
id = pageidnums[int(pagenum)]
if (raw):
@ -561,7 +549,7 @@ def generateBook(bookDir, raw, fixedimage):
tlst.append('</body>\n')
tlst.append('</html>\n')
tochtml = "".join(tlst)
file(os.path.join(svgDir, 'toc.xhtml'), 'wb').write(tochtml)
open(os.path.join(svgDir, 'toc.xhtml'), 'w').write(tochtml)
# now create index_svg.xhtml that points to all required files
@ -580,7 +568,7 @@ def generateBook(bookDir, raw, fixedimage):
slst.append('</head>\n')
slst.append('<body>\n')
print "Building svg images of each book page"
print("Building svg images of each book page")
slst.append('<h2>List of Pages</h2>\n')
slst.append('<div>\n')
idlst = sorted(pageIDMap.keys())
@ -593,12 +581,12 @@ def generateBook(bookDir, raw, fixedimage):
nextid = idlst[j+1]
else:
nextid = None
print '.',
print('.', end=' ')
pagelst = pageIDMap[pageid]
flst = []
for page in pagelst:
flst.append(xmllst[page])
flat_svg = "".join(flst)
flat_svg = b"".join(flst)
flst=None
svgxml = flatxml2svg.convert2SVG(gd, flat_svg, pageid, previd, nextid, svgDir, raw, meta_array, scaledpi)
if (raw) :
@ -616,9 +604,9 @@ def generateBook(bookDir, raw, fixedimage):
slst.append('</body>\n</html>\n')
svgindex = "".join(slst)
slst = None
file(os.path.join(bookDir, 'index_svg.xhtml'), 'wb').write(svgindex)
open(os.path.join(bookDir, 'index_svg.xhtml'), 'w').write(svgindex)
print " "
print(" ")
# build the opf file
opfname = os.path.join(bookDir, 'book.opf')
@ -627,16 +615,16 @@ def generateBook(bookDir, raw, fixedimage):
olst.append('<package xmlns="http://www.idpf.org/2007/opf" unique-identifier="guid_id">\n')
# adding metadata
olst.append(' <metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf">\n')
if 'GUID' in meta_array:
olst.append(' <dc:identifier opf:scheme="GUID" id="guid_id">' + meta_array['GUID'] + '</dc:identifier>\n')
if 'ASIN' in meta_array:
olst.append(' <dc:identifier opf:scheme="ASIN">' + meta_array['ASIN'] + '</dc:identifier>\n')
if 'oASIN' in meta_array:
olst.append(' <dc:identifier opf:scheme="oASIN">' + meta_array['oASIN'] + '</dc:identifier>\n')
olst.append(' <dc:title>' + meta_array['Title'] + '</dc:title>\n')
olst.append(' <dc:creator opf:role="aut">' + meta_array['Authors'] + '</dc:creator>\n')
if b'GUID' in meta_array:
olst.append(' <dc:identifier opf:scheme="GUID" id="guid_id">' + meta_array[b'GUID'].decode('utf-8') + '</dc:identifier>\n')
if b'ASIN' in meta_array:
olst.append(' <dc:identifier opf:scheme="ASIN">' + meta_array[b'ASIN'].decode('utf-8') + '</dc:identifier>\n')
if b'oASIN' in meta_array:
olst.append(' <dc:identifier opf:scheme="oASIN">' + meta_array[b'oASIN'].decode('utf-8') + '</dc:identifier>\n')
olst.append(' <dc:title>' + meta_array[b'Title'].decode('utf-8') + '</dc:title>\n')
olst.append(' <dc:creator opf:role="aut">' + meta_array[b'Authors'].decode('utf-8') + '</dc:creator>\n')
olst.append(' <dc:language>en</dc:language>\n')
olst.append(' <dc:date>' + meta_array['UpdateTime'] + '</dc:date>\n')
olst.append(' <dc:date>' + meta_array[b'UpdateTime'].decode('utf-8') + '</dc:date>\n')
if isCover:
olst.append(' <meta name="cover" content="bookcover"/>\n')
olst.append(' </metadata>\n')
@ -665,25 +653,27 @@ def generateBook(bookDir, raw, fixedimage):
olst.append('</package>\n')
opfstr = "".join(olst)
olst = None
file(opfname, 'wb').write(opfstr)
open(opfname, 'w').write(opfstr)
print 'Processing Complete'
print('Processing Complete')
return 0
def usage():
print "genbook.py generates a book from the extract Topaz Files"
print "Usage:"
print " genbook.py [-r] [-h [--fixed-image] <bookDir> "
print " "
print "Options:"
print " -h : help - print this usage message"
print " -r : generate raw svg files (not wrapped in xhtml)"
print " --fixed-image : genearate any Fixed Area as an svg image in the html"
print " "
print("genbook.py generates a book from the extract Topaz Files")
print("Usage:")
print(" genbook.py [-r] [-h [--fixed-image] <bookDir> ")
print(" ")
print("Options:")
print(" -h : help - print this usage message")
print(" -r : generate raw svg files (not wrapped in xhtml)")
print(" --fixed-image : genearate any Fixed Area as an svg image in the html")
print(" ")
def main(argv):
sys.stdout=SafeUnbuffered(sys.stdout)
sys.stderr=SafeUnbuffered(sys.stderr)
bookDir = ''
if len(argv) == 0:
argv = sys.argv
@ -691,8 +681,8 @@ def main(argv):
try:
opts, args = getopt.getopt(argv[1:], "rh:",["fixed-image"])
except getopt.GetoptError, err:
print str(err)
except getopt.GetoptError as err:
print(str(err))
usage()
return 1

View file

@ -0,0 +1,68 @@
'''
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 ImportError:
from Crypto.Cipher import AES
import hashlib
from lxml import etree
def unpad(data, padding=16):
if sys.version_info[0] == 2:
pad_len = ord(data[-1])
else:
pad_len = data[-1]
return data[:-pad_len]
PASS_HASH_SECRET = "9ca588496a1bc4394553d9e018d70b9e"
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"):
try:
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")))
except:
pass
return hashes
if __name__ == "__main__":
print("No standalone version available.")

View file

@ -0,0 +1,187 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# ignoblekeyGenPassHash.py
# Copyright © 2009-2022 i♥cabbages, Apprentice Harper et al.
# Released under the terms of the GNU General Public Licence, version 3
# <http://www.gnu.org/licenses/>
# Windows users: Before running this program, you must first install Python.
# We recommend ActiveState Python 2.7.X for Windows (x86) from
# http://www.activestate.com/activepython/downloads.
# You must also install PyCrypto from
# http://www.voidspace.org.uk/python/modules.shtml#pycrypto
# (make certain to install the version for Python 2.7).
# Then save this script file as ignoblekeygen.pyw and double-click on it to run it.
#
# Mac OS X users: Save this script file as ignoblekeygen.pyw. You can run this
# program from the command line (python ignoblekeygen.pyw) or by double-clicking
# it when it has been associated with PythonLauncher.
# Revision history:
# 1 - Initial release
# 2 - Add OS X support by using OpenSSL when available (taken/modified from ineptepub v5)
# 2.1 - Allow Windows versions of libcrypto to be found
# 2.2 - On Windows try PyCrypto first and then OpenSSL next
# 2.3 - Modify interface to allow use of import
# 2.4 - Improvements to UI and now works in plugins
# 2.5 - Additional improvement for unicode and plugin support
# 2.6 - moved unicode_argv call inside main for Windows DeDRM compatibility
# 2.7 - Work if TkInter is missing
# 2.8 - Fix bug in stand-alone use (import tkFileDialog)
# 3.0 - Added Python 3 compatibility for calibre 5.0
# 3.1 - Remove OpenSSL support, only PyCryptodome is supported now
"""
Generate Barnes & Noble EPUB user key from name and credit card number.
"""
__license__ = 'GPL v3'
__version__ = "3.1"
import sys
import os
import hashlib
import base64
#@@CALIBRE_COMPAT_CODE@@
try:
from Cryptodome.Cipher import AES
except ImportError:
from Crypto.Cipher import AES
from .utilities import SafeUnbuffered
from .argv_utils import unicode_argv
class IGNOBLEError(Exception):
pass
def normalize_name(name):
return ''.join(x for x in name.lower() if x != ' ')
def generate_key(name, ccn):
# remove spaces and case from name and CC numbers.
name = normalize_name(name)
ccn = normalize_name(ccn)
if type(name)==str:
name = name.encode('utf-8')
if type(ccn)==str:
ccn = ccn.encode('utf-8')
name = name + b'\x00'
ccn = ccn + b'\x00'
name_sha = hashlib.sha1(name).digest()[:16]
ccn_sha = hashlib.sha1(ccn).digest()[:16]
both_sha = hashlib.sha1(name + ccn).digest()
crypt = AES.new(ccn_sha, AES.MODE_CBC, name_sha).encrypt(both_sha + (b'\x0c' * 0x0c))
userkey = hashlib.sha1(crypt).digest()
return base64.b64encode(userkey)
def cli_main():
sys.stdout=SafeUnbuffered(sys.stdout)
sys.stderr=SafeUnbuffered(sys.stderr)
argv=unicode_argv("ignoblekeyGenPassHash.py")
progname = os.path.basename(argv[0])
if len(argv) != 4:
print("usage: {0} <Name> <CC#> <keyfileout.b64>".format(progname))
return 1
name, ccn, keypath = argv[1:]
userkey = generate_key(name, ccn)
open(keypath,'wb').write(userkey)
return 0
def gui_main():
try:
import tkinter
import tkinter.constants
import tkinter.messagebox
import tkinter.filedialog
import traceback
except:
return cli_main()
class DecryptionDialog(tkinter.Frame):
def __init__(self, root):
tkinter.Frame.__init__(self, root, border=5)
self.status = tkinter.Label(self, text="Enter parameters")
self.status.pack(fill=tkinter.constants.X, expand=1)
body = tkinter.Frame(self)
body.pack(fill=tkinter.constants.X, expand=1)
sticky = tkinter.constants.E + tkinter.constants.W
body.grid_columnconfigure(1, weight=2)
tkinter.Label(body, text="Account Name").grid(row=0)
self.name = tkinter.Entry(body, width=40)
self.name.grid(row=0, column=1, sticky=sticky)
tkinter.Label(body, text="CC#").grid(row=1)
self.ccn = tkinter.Entry(body, width=40)
self.ccn.grid(row=1, column=1, sticky=sticky)
tkinter.Label(body, text="Output file").grid(row=2)
self.keypath = tkinter.Entry(body, width=40)
self.keypath.grid(row=2, column=1, sticky=sticky)
self.keypath.insert(2, "bnepubkey.b64")
button = tkinter.Button(body, text="...", command=self.get_keypath)
button.grid(row=2, column=2)
buttons = tkinter.Frame(self)
buttons.pack()
botton = tkinter.Button(
buttons, text="Generate", width=10, command=self.generate)
botton.pack(side=tkinter.constants.LEFT)
tkinter.Frame(buttons, width=10).pack(side=tkinter.constants.LEFT)
button = tkinter.Button(
buttons, text="Quit", width=10, command=self.quit)
button.pack(side=tkinter.constants.RIGHT)
def get_keypath(self):
keypath = tkinter.filedialog.asksaveasfilename(
parent=None, title="Select B&N ePub key file to produce",
defaultextension=".b64",
filetypes=[('base64-encoded files', '.b64'),
('All Files', '.*')])
if keypath:
keypath = os.path.normpath(keypath)
self.keypath.delete(0, tkinter.constants.END)
self.keypath.insert(0, keypath)
return
def generate(self):
name = self.name.get()
ccn = self.ccn.get()
keypath = self.keypath.get()
if not name:
self.status['text'] = "Name not specified"
return
if not ccn:
self.status['text'] = "Credit card number not specified"
return
if not keypath:
self.status['text'] = "Output keyfile path not specified"
return
self.status['text'] = "Generating..."
try:
userkey = generate_key(name, ccn)
except Exception as e:
self.status['text'] = "Error: (0}".format(e.args[0])
return
open(keypath,'wb').write(userkey)
self.status['text'] = "Keyfile successfully generated"
root = tkinter.Tk()
root.title("Barnes & Noble ePub Keyfile Generator 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())

View file

@ -1,10 +1,8 @@
#!/usr/bin/env python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from __future__ import with_statement
# ignoblekey.py
# Copyright © 2015 Apprentice Alf and Apprentice Harper
# ignoblekeyNookStudy.py
# Copyright © 2015-2020 Apprentice Alf, Apprentice Harper et al.
# Based on kindlekey.py, Copyright © 2010-2013 by some_updates and Apprentice Alf
@ -14,13 +12,14 @@ from __future__ import with_statement
# Revision history:
# 1.0 - Initial release
# 1.1 - remove duplicates and return last key as single key
# 2.0 - Python 3 for calibre 5.0
"""
Get Barnes & Noble EPUB user key from nook Studio log file
"""
__license__ = 'GPL v3'
__version__ = "1.1"
__version__ = "2.0"
import sys
import os
@ -28,66 +27,16 @@ import hashlib
import getopt
import re
# 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,unicode):
data = data.encode(self.encoding,"replace")
self.stream.write(data)
self.stream.flush()
def __getattr__(self, attr):
return getattr(self.stream, attr)
#@@CALIBRE_COMPAT_CODE@@
from .utilities import SafeUnbuffered
try:
from calibre.constants import iswindows, isosx
from calibre.constants import iswindows
except:
iswindows = sys.platform.startswith('win')
isosx = sys.platform.startswith('darwin')
def unicode_argv():
if iswindows:
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
# strings.
# Versions 2.x of Python don't support Unicode in sys.argv on
# Windows, with the underlying Windows API instead replacing multi-byte
# characters with '?'. So use shell32.GetCommandLineArgvW to get sys.argv
# as a list of Unicode strings and encode them as utf-8
from ctypes import POINTER, byref, cdll, c_int, windll
from ctypes.wintypes import LPCWSTR, LPWSTR
GetCommandLineW = cdll.kernel32.GetCommandLineW
GetCommandLineW.argtypes = []
GetCommandLineW.restype = LPCWSTR
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
CommandLineToArgvW.restype = POINTER(LPWSTR)
cmd = GetCommandLineW()
argc = c_int(0)
argv = CommandLineToArgvW(cmd, byref(argc))
if argc.value > 0:
# Remove Python executable and commands if present
start = argc.value - len(sys.argv)
return [argv[i] for i in
xrange(start, argc.value)]
# if we don't have any arguments at all, just pass back script name
# this should never happen
return [u"ignoblekey.py"]
else:
argvencoding = sys.stdin.encoding
if argvencoding == None:
argvencoding = "utf-8"
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
from .argv_utils import unicode_argv
class DrmException(Exception):
pass
@ -97,22 +46,36 @@ def getNookLogFiles():
logFiles = []
found = False
if iswindows:
import _winreg as winreg
try:
import winreg
except ImportError:
import _winreg as winreg
# some 64 bit machines do not have the proper registry key for some reason
# or the python interface to the 32 vs 64 bit registry is broken
paths = set()
if 'LOCALAPPDATA' in os.environ.keys():
# Python 2.x does not return unicode env. Use Python 3.x
path = winreg.ExpandEnvironmentStrings(u"%LOCALAPPDATA%")
if sys.version_info[0] == 2:
path = winreg.ExpandEnvironmentStrings(u"%LOCALAPPDATA%")
else:
path = winreg.ExpandEnvironmentStrings("%LOCALAPPDATA%")
if os.path.isdir(path):
paths.add(path)
if 'USERPROFILE' in os.environ.keys():
# Python 2.x does not return unicode env. Use Python 3.x
path = winreg.ExpandEnvironmentStrings(u"%USERPROFILE%")+u"\\AppData\\Local"
if sys.version_info[0] == 2:
path = winreg.ExpandEnvironmentStrings(u"%USERPROFILE%")+u"\\AppData\\Local"
else:
path = winreg.ExpandEnvironmentStrings("%USERPROFILE%")+"\\AppData\\Local"
if os.path.isdir(path):
paths.add(path)
path = winreg.ExpandEnvironmentStrings(u"%USERPROFILE%")+u"\\AppData\\Roaming"
if sys.version_info[0] == 2:
path = winreg.ExpandEnvironmentStrings(u"%USERPROFILE%")+u"\\AppData\\Roaming"
else:
path = winreg.ExpandEnvironmentStrings("%USERPROFILE%")+"\\AppData\\Roaming"
if os.path.isdir(path):
paths.add(path)
# User Shell Folders show take precedent over Shell Folders if present
@ -150,7 +113,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, file=sys.stderr)
logFiles.append(logpath)
else:
home = os.getenv('HOME')
@ -158,26 +121,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
@ -198,7 +161,7 @@ def nookkeys(files = []):
for file in files:
fileKeys = getKeysFromLog(file)
if fileKeys:
print u"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))
@ -209,42 +172,42 @@ def getkey(outpath, files=[]):
if len(keys) > 0:
if not os.path.isdir(outpath):
outfile = outpath
with file(outfile, 'w') as keyfileout:
with open(outfile, 'w') as keyfileout:
keyfileout.write(keys[-1])
print u"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:
while True:
keycount += 1
outfile = os.path.join(outpath,u"nookkey{0:d}.b64".format(keycount))
outfile = os.path.join(outpath,"nookkey{0:d}.b64".format(keycount))
if not os.path.exists(outfile):
break
with file(outfile, 'w') as keyfileout:
with open(outfile, 'w') as keyfileout:
keyfileout.write(key)
print u"Saved a key to {0}".format(outfile)
print("Saved a key to {0}".format(outfile), file=sys.stderr)
return True
return False
def usage(progname):
print u"Finds the nook Study encryption keys."
print u"Keys are saved to the current directory, or a specified output directory."
print u"If a file name is passed instead of a directory, only the first key is saved, in that file."
print u"Usage:"
print u" {0:s} [-h] [-k <logFile>] [<outpath>]".format(progname)
print("Finds the nook Study encryption keys.")
print("Keys are saved to the current directory, or a specified output directory.")
print("If a file name is passed instead of a directory, only the first key is saved, in that file.")
print("Usage:")
print(" {0:s} [-h] [-k <logFile>] [<outpath>]".format(progname))
def cli_main():
sys.stdout=SafeUnbuffered(sys.stdout)
sys.stderr=SafeUnbuffered(sys.stderr)
argv=unicode_argv()
argv=unicode_argv("ignoblekeyNookStudy.py")
progname = os.path.basename(argv[0])
print u"{0} v{1}\nCopyright © 2015 Apprentice Alf".format(progname,__version__)
print("{0} v{1}\nCopyright © 2015 Apprentice Alf".format(progname,__version__))
try:
opts, args = getopt.getopt(argv[1:], "hk:")
except getopt.GetoptError, err:
print u"Error in options or arguments: {0}".format(err.args[0])
except getopt.GetoptError as err:
print("Error in options or arguments: {0}".format(err.args[0]))
usage(progname)
sys.exit(2)
@ -273,33 +236,33 @@ def cli_main():
outpath = os.path.realpath(os.path.normpath(outpath))
if not getkey(outpath, files):
print u"Could not retrieve nook Study key."
print("Could not retrieve nook Study key.")
return 0
def gui_main():
try:
import Tkinter
import Tkconstants
import tkMessageBox
import tkinter
import tkinter.constants
import tkinter.messagebox
import traceback
except:
return cli_main()
class ExceptionDialog(Tkinter.Frame):
class ExceptionDialog(tkinter.Frame):
def __init__(self, root, text):
Tkinter.Frame.__init__(self, root, border=5)
label = Tkinter.Label(self, text=u"Unexpected error:",
anchor=Tkconstants.W, justify=Tkconstants.LEFT)
label.pack(fill=Tkconstants.X, expand=0)
self.text = Tkinter.Text(self)
self.text.pack(fill=Tkconstants.BOTH, expand=1)
tkinter.Frame.__init__(self, root, border=5)
label = tkinter.Label(self, text="Unexpected error:",
anchor=tkinter.constants.W, justify=tkinter.constants.LEFT)
label.pack(fill=tkinter.constants.X, expand=0)
self.text = tkinter.Text(self)
self.text.pack(fill=tkinter.constants.BOTH, expand=1)
self.text.insert(Tkconstants.END, text)
self.text.insert(tkinter.constants.END, text)
argv=unicode_argv()
root = Tkinter.Tk()
argv=unicode_argv("ignoblekeyNookStudy.py")
root = tkinter.Tk()
root.withdraw()
progpath, progname = os.path.split(argv[0])
success = False
@ -307,24 +270,24 @@ def gui_main():
keys = nookkeys()
keycount = 0
for key in keys:
print key
print(key)
while True:
keycount += 1
outfile = os.path.join(progpath,u"nookkey{0:d}.b64".format(keycount))
outfile = os.path.join(progpath,"nookkey{0:d}.b64".format(keycount))
if not os.path.exists(outfile):
break
with file(outfile, 'w') as keyfileout:
with open(outfile, 'w') as keyfileout:
keyfileout.write(key)
success = True
tkMessageBox.showinfo(progname, u"Key successfully retrieved to {0}".format(outfile))
except DrmException, e:
tkMessageBox.showerror(progname, u"Error: {0}".format(str(e)))
tkinter.messagebox.showinfo(progname, "Key successfully retrieved to {0}".format(outfile))
except DrmException as e:
tkinter.messagebox.showerror(progname, "Error: {0}".format(str(e)))
except Exception:
root.wm_state('normal')
root.title(progname)
text = traceback.format_exc()
ExceptionDialog(root, text).pack(fill=Tkconstants.BOTH, expand=1)
ExceptionDialog(root, text).pack(fill=tkinter.constants.BOTH, expand=1)
root.mainloop()
if not success:
return 1

View file

@ -0,0 +1,78 @@
# 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
import traceback
try:
from Cryptodome.Cipher import AES
except:
from Crypto.Cipher import AES
import hashlib
from lxml import etree
def unpad(data, padding=16):
if sys.version_info[0] == 2:
pad_len = ord(data[-1])
else:
pad_len = data[-1]
return data[:-pad_len]
NOOK_DATA_FOLDER = "%LOCALAPPDATA%\\Packages\\BarnesNoble.Nook_ahnzqzva31enc\\LocalState"
PASS_HASH_SECRET = "9ca588496a1bc4394553d9e018d70b9e"
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"):
try:
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:]), 16)
decrypted_hashes.append((base64.b64encode(cc_hash).decode("ascii")))
if print_result:
print("Nook ccHash is %s" % (base64.b64encode(cc_hash).decode("ascii")))
except:
traceback.print_exc()
return decrypted_hashes
if __name__ == "__main__":
dump_keys(True)

467
DeDRM_plugin/ineptepub.py Normal file
View file

@ -0,0 +1,467 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# ineptepub.py
# Copyright © 2009-2022 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 - Rename to INEPT, fix exit code
# 5 - Version bump to avoid (?) confusion;
# Improve OS X support by using OpenSSL when available
# 5.1 - Improve OpenSSL error checking
# 5.2 - Fix ctypes error causing segfaults on some systems
# 5.3 - add support for OpenSSL on Windows, fix bug with some versions of libcrypto 0.9.8 prior to path level o
# 5.4 - add support for encoding to 'utf-8' when building up list of files to decrypt from encryption.xml
# 5.5 - On Windows try PyCrypto first, OpenSSL next
# 5.6 - Modify interface to allow use with import
# 5.7 - Fix for potential problem with PyCrypto
# 5.8 - Revised to allow use in calibre plugins to eliminate need for duplicate code
# 5.9 - Fixed to retain zip file metadata (e.g. file modification date)
# 6.0 - moved unicode_argv call inside main for Windows DeDRM compatibility
# 6.1 - Work if TkInter is missing
# 6.2 - Handle UTF-8 file names inside an ePub, fix by Jose Luis
# 6.3 - Add additional check on DER file sanity
# 6.4 - Remove erroneous check on DER file sanity
# 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
# 7.2 - Only support PyCryptodome; clean up the code
# 8.0 - Add support for "hardened" Adobe DRM (RMSDK >= 10)
"""
Decrypt Adobe Digital Editions encrypted ePub books.
"""
__license__ = 'GPL v3'
__version__ = "8.0"
import sys
import os
import traceback
import base64
import zlib
import zipfile
from zipfile import ZipInfo, ZipFile, ZIP_STORED, ZIP_DEFLATED
from zeroedzipinfo import ZeroedZipInfo
from contextlib import closing
from lxml import etree
from uuid import UUID
import hashlib
try:
from Cryptodome.Cipher import AES, PKCS1_v1_5
from Cryptodome.PublicKey import RSA
except ImportError:
from Crypto.Cipher import AES, PKCS1_v1_5
from Crypto.PublicKey import RSA
def unpad(data, padding=16):
if sys.version_info[0] == 2:
pad_len = ord(data[-1])
else:
pad_len = data[-1]
return data[:-pad_len]
#@@CALIBRE_COMPAT_CODE@@
from .utilities import SafeUnbuffered
from .argv_utils import unicode_argv
class ADEPTError(Exception):
pass
class ADEPTNewVersionError(Exception):
pass
META_NAMES = ('mimetype', 'META-INF/rights.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.new(bookkey, AES.MODE_CBC, b'\x00'*16)
self._encryption = etree.fromstring(encryption)
self._encrypted = encrypted = set()
self._encryptedForceNoDecomp = encryptedForceNoDecomp = set()
self._otherData = otherData = set()
self._json_elements_to_remove = json_elements_to_remove = set()
self._has_remaining_xml = False
expr = './%s/%s/%s' % (enc('EncryptedData'), enc('CipherData'),
enc('CipherReference'))
for elem in self._encryption.findall(expr):
path = elem.get('URI', None)
encryption_type_url = (elem.getparent().getparent().find("./%s" % (enc('EncryptionMethod'))).get('Algorithm', None))
if path is not None:
if (encryption_type_url == "http://www.w3.org/2001/04/xmlenc#aes128-cbc"):
# Adobe
path = path.encode('utf-8')
encrypted.add(path)
json_elements_to_remove.add(elem.getparent().getparent())
elif (encryption_type_url == "http://ns.adobe.com/adept/xmlenc#aes128-cbc-uncompressed"):
# Adobe uncompressed, for stuff like video files
path = path.encode('utf-8')
encryptedForceNoDecomp.add(path)
json_elements_to_remove.add(elem.getparent().getparent())
else:
path = path.encode('utf-8')
otherData.add(path)
self._has_remaining_xml = True
for elem in json_elements_to_remove:
elem.getparent().remove(elem)
def check_if_remaining(self):
return self._has_remaining_xml
def get_xml(self):
return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + etree.tostring(self._encryption, encoding="utf-8", pretty_print=True, xml_declaration=False).decode("utf-8")
def decompress(self, bytes):
dc = zlib.decompressobj(-15)
try:
decompressed_bytes = dc.decompress(bytes)
ex = dc.decompress(b'Z') + dc.flush()
if ex:
decompressed_bytes = decompressed_bytes + ex
except:
# possibly not compressed by zip - just return bytes
return bytes
return decompressed_bytes
def decrypt(self, path, data):
if path.encode('utf-8') in self._encrypted or path.encode('utf-8') in self._encryptedForceNoDecomp:
data = self._aes.decrypt(data)[16:]
if type(data[-1]) != int:
place = ord(data[-1])
else:
place = data[-1]
data = data[:-place]
if not path.encode('utf-8') in self._encryptedForceNoDecomp:
data = self.decompress(data)
return data
# check file to make check whether it's probably an Adobe Adept encrypted ePub
def adeptBook(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) 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.
def adeptGetUserUUID(inpath):
with closing(ZipFile(open(inpath, 'rb'))) as inf:
try:
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
expr = './/%s' % (adept('user'),)
user_uuid = ''.join(rights.findtext(expr))
if user_uuid[:9] != "urn:uuid:":
return None
return user_uuid[9:]
except:
return None
def removeHardening(rights, keytype, keydata):
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
textGetter = lambda name: ''.join(rights.findtext('.//%s' % (adept(name),)))
# Gather what we need, and generate the IV
resourceuuid = UUID(textGetter("resource"))
deviceuuid = UUID(textGetter("device"))
fullfillmentuuid = UUID(textGetter("fulfillment")[:36])
kekiv = UUID(int=resourceuuid.int ^ deviceuuid.int ^ fullfillmentuuid.int).bytes
# Derive kek from just "keytype"
rem = int(keytype, 10) % 16
H = hashlib.sha256(keytype.encode("ascii")).digest()
kek = H[2*rem : 16 + rem] + H[rem : 2*rem]
return unpad(AES.new(kek, AES.MODE_CBC, kekiv).decrypt(keydata), 16) # PKCS#7
def decryptBook(userkey, inpath, outpath):
with closing(ZipFile(open(inpath, 'rb'))) as inf:
namelist = inf.namelist()
if 'META-INF/rights.xml' not in namelist or \
'META-INF/encryption.xml' not in namelist:
print("{0:s} is DRM-free.".format(os.path.basename(inpath)))
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'),)
bookkeyelem = rights.find(expr)
bookkey = bookkeyelem.text
keytype = bookkeyelem.attrib.get('keyType', '0')
if len(bookkey) >= 172 and int(keytype, 10) > 2:
print("{0:s} is a secure Adobe Adept ePub with hardening.".format(os.path.basename(inpath)))
elif 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
if len(bookkey) != 64:
# Normal or "hardened" Adobe ADEPT
rsakey = RSA.importKey(userkey) # parses the ASN1 structure
bookkey = base64.b64decode(bookkey)
if int(keytype, 10) > 2:
bookkey = removeHardening(rights, keytype, bookkey)
try:
bookkey = PKCS1_v1_5.new(rsakey).decrypt(bookkey, None) # automatically unpads
except ValueError:
bookkey = None
if bookkey is None:
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]
bookkey = base64.b64decode(bookkey)
bookkey = unpad(AES.new(key, AES.MODE_CBC, b'\x00'*16).decrypt(bookkey), 16) # PKCS#7
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)
with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf:
for path in (["mimetype"] + namelist):
data = inf.read(path)
zi = ZipInfo(path)
zi.compress_type=ZIP_DEFLATED
if path == "mimetype":
zi.compress_type = ZIP_STORED
elif path == "META-INF/encryption.xml":
# Check if there's still something in there
if (decryptor.check_if_remaining()):
data = decryptor.get_xml()
print("Adding encryption.xml for the remaining embedded files.")
# We removed DRM, but there's still stuff like obfuscated fonts.
else:
continue
try:
# get the file info, including time-stamp
oldzi = inf.getinfo(path)
# 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.volume = oldzi.volume
zi.create_system = oldzi.create_system
zi.create_version = oldzi.create_version
if any(ord(c) >= 128 for c in path) or any(ord(c) >= 128 for c in zi.comment):
# If the file name or the comment contains any non-ASCII char, set the UTF8-flag
zi.flag_bits |= 0x800
except:
pass
# Python 3 has a bug where the external_attr is reset to `0o600 << 16`
# if it's NULL, so we need a workaround:
if zi.external_attr == 0:
zi = ZeroedZipInfo(zi)
if path == "META-INF/encryption.xml":
outf.writestr(zi, data)
else:
outf.writestr(zi, decryptor.decrypt(path, data))
except:
print("Could not decrypt {0:s} because of an exception:\n{1:s}".format(os.path.basename(inpath), traceback.format_exc()))
return 2
return 0
def cli_main():
sys.stdout=SafeUnbuffered(sys.stdout)
sys.stderr=SafeUnbuffered(sys.stderr)
argv=unicode_argv("ineptepub.py")
progname = os.path.basename(argv[0])
if len(argv) != 4:
print("usage: {0} <keyfile.der> <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("adeptkey.der"):
self.keypath.insert(0, "adeptkey.der")
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 Adobe Adept \'.der\' key file",
defaultextension=".der",
filetypes=[('Adobe Adept DER-encoded files', '.der'),
('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 ADEPT-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'] = "There was an error decrypting the file."
root = tkinter.Tk()
root.title("Adobe Adept 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())

1537
dedrm_src/ineptpdf.py → DeDRM_plugin/ineptpdf.py Normal file → Executable file

File diff suppressed because it is too large Load diff

View file

@ -1,9 +1,20 @@
#!/usr/bin/env python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Pascal implementation by lulzkabulz. Python translation by apprenticenaomi. DeDRM integration by anon.
# BinaryIon.pas + DrmIon.pas + IonSymbols.pas
from __future__ import with_statement
"""ion.py: Decrypt Kindle KFX files.
Revision history:
Pascal implementation by lulzkabulz.
BinaryIon.pas + DrmIon.pas + IonSymbols.pas
1.0 - Python translation by apprenticenaomi.
1.1 - DeDRM integration by anon.
1.2 - Added pylzma import fallback
1.3 - Fixed lzma support for calibre 4.6+
2.0 - VoucherEnvelope v2/v3 support by apprenticesakuya.
3.0 - Added Python 3 compatibility for calibre 5.0
Copyright © 2013-2020 Apprentice Harper et al.
"""
import collections
import hashlib
@ -12,29 +23,42 @@ import os
import os.path
import struct
from io import BytesIO
__license__ = 'GPL v3'
__version__ = '3.0'
#@@CALIBRE_COMPAT_CODE@@
try:
from cStringIO import StringIO
from Cryptodome.Cipher import AES
from Cryptodome.Util.py3compat import bchr
except ImportError:
from StringIO import StringIO
from Crypto.Cipher import AES
from Crypto.Util.py3compat import bchr, bord
from Crypto.Cipher import AES
from Crypto.Util.py3compat import bchr
try:
# lzma library from calibre 2.35.0 or later
import lzma.lzma1 as calibre_lzma
# lzma library from calibre 4.6.0 or later
import calibre_lzma.lzma1 as calibre_lzma
except ImportError:
calibre_lzma = None
# lzma library from calibre 2.35.0 or later
try:
import lzma
import lzma.lzma1 as calibre_lzma
except ImportError:
# Need pip backports.lzma on Python <3.3
calibre_lzma = None
try:
from backports import lzma
import lzma
except ImportError:
# Windows-friendly choice: pylzma wheels
import pylzma as lzma
# Need pip backports.lzma on Python <3.3
try:
from backports import lzma
except ImportError:
# Windows-friendly choice: pylzma wheels
import pylzma as lzma
from .kfxtables import *
TID_NULL = 0
TID_BOOLEAN = 1
@ -71,7 +95,7 @@ LEN_IS_VAR_LEN = 0xE
LEN_IS_NULL = 0xF
VERSION_MARKER = b"\x01\x00\xEA"
VERSION_MARKER = [b"\x01", b"\x00", b"\xEA"]
# asserts must always raise exceptions for proper functioning
@ -329,7 +353,7 @@ class BinaryIonParser(object):
b = self.stream.read(1)
if len(b) < 1:
return -1
b = bord(b)
b = ord(b)
result = b >> 4
ln = b & 0xF
@ -354,13 +378,13 @@ class BinaryIonParser(object):
return result
def readvarint(self):
b = bord(self.read())
b = ord(self.read())
negative = ((b & 0x40) != 0)
result = (b & 0x3F)
i = 0
while (b & 0x80) == 0 and i < 4:
b = bord(self.read())
b = ord(self.read())
result = (result << 7) | (b & 0x7F)
i += 1
@ -371,12 +395,12 @@ class BinaryIonParser(object):
return result
def readvaruint(self):
b = bord(self.read())
b = ord(self.read())
result = (b & 0x7F)
i = 0
while (b & 0x80) == 0 and i < 4:
b = bord(self.read())
b = ord(self.read())
result = (result << 7) | (b & 0x7F)
i += 1
@ -396,7 +420,7 @@ class BinaryIonParser(object):
_assert(self.localremaining <= 8, "Decimal overflow")
signed = False
b = [bord(x) for x in self.read(self.localremaining)]
b = [ord(x) for x in self.read(self.localremaining)]
if (b[0] & 0x80) != 0:
b[0] = b[0] & 0x7F
signed = True
@ -494,6 +518,8 @@ class BinaryIonParser(object):
if table is not None:
self.symbols.import_(table, min(maxid, len(table.symnames)))
if len(table.symnames) < maxid:
self.symbols.importunknown(name + "-unknown", maxid - len(table.symnames))
else:
self.symbols.importunknown(name, maxid)
@ -561,7 +587,7 @@ class BinaryIonParser(object):
_assert(self.valuelen <= 4, "int too long: %d" % self.valuelen)
v = 0
for i in range(self.valuelen - 1, -1, -1):
v = (v | (bord(self.read()) << (i * 8)))
v = (v | (ord(self.read()) << (i * 8)))
if self.valuetid == TID_NEGINT:
self.value = -v
@ -631,7 +657,7 @@ class BinaryIonParser(object):
result = ""
for i in b:
result += ("%02x " % bord(i))
result += ("%02x " % ord(i))
if len(result) > 0:
result = result[:-1]
@ -714,7 +740,11 @@ SYM_NAMES = [ 'com.amazon.drm.Envelope@1.0',
'com.amazon.drm.EnvelopeMetadata@2.0',
'com.amazon.drm.EncryptedPage@2.0',
'com.amazon.drm.PlainText@2.0', 'compression_algorithm',
'com.amazon.drm.Compressed@1.0', 'priority', 'refines']
'com.amazon.drm.Compressed@1.0', 'page_index_table',
] + ['com.amazon.drm.VoucherEnvelope@%d.0' % n
for n in list(range(2, 29)) + [
9708, 1031, 2069, 9041, 3646,
6052, 9479, 9888, 4648, 5683]]
def addprottable(ion):
ion.addtocatalog("ProtectedData", 1, SYM_NAMES)
@ -729,15 +759,546 @@ def pkcs7pad(msg, blocklen):
def pkcs7unpad(msg, blocklen):
_assert(len(msg) % blocklen == 0)
paddinglen = bord(msg[-1])
paddinglen = msg[-1]
_assert(paddinglen > 0 and paddinglen <= blocklen, "Incorrect padding - Wrong key")
_assert(msg[-paddinglen:] == bchr(paddinglen) * paddinglen, "Incorrect padding - Wrong key")
return msg[:-paddinglen]
# every VoucherEnvelope version has a corresponding "word" and magic number, used in obfuscating the shared secret
# 4-digit versions use their own obfuscation/scramble. It does not seem to depend on the "word" and number
OBFUSCATION_TABLE = {
"V1": (0x00, None),
"V2": (0x05, b'Antidisestablishmentarianism'),
"V3": (0x08, b'Floccinaucinihilipilification'),
"V4": (0x07, b'>\x14\x0c\x12\x10-\x13&\x18U\x1d\x05Rlt\x03!\x19\x1b\x13\x04]Y\x19,\t\x1b'),
"V5": (0x06, b'~\x18~\x16J\\\x18\x10\x05\x0b\x07\t\x0cZ\r|\x1c\x15\x1d\x11>,\x1b\x0e\x03"4\x1b\x01'),
"V6": (0x09, b'3h\x055\x03[^>\x19\x1c\x08\x1b\rtm4\x02Rp\x0c\x16B\n'),
"V7": (0x05, b'\x10\x1bJ\x18\nh!\x10"\x03>Z\'\r\x01]W\x06\x1c\x1e?\x0f\x13'),
"V8": (0x09, b"K\x0c6\x1d\x1a\x17pO}Rk\x1d'w1^\x1f$\x1c{C\x02Q\x06\x1d`"),
"V9": (0x05, b'X.\x0eW\x1c*K\x12\x12\t\n\n\x17Wx\x01\x02Yf\x0f\x18\x1bVXPi\x01'),
"V10": (0x07, b'z3\n\x039\x12\x13`\x06=v;\x02MTK\x1e%}L\x1c\x1f\x15\x0c\x11\x02\x0c\n8\x17p'),
"V11": (0x05, b'L=\nhVm\x07go\n6\x14\x06\x16L\r\x02\x0b\x0c\x1b\x04#p\t'),
"V12": (0x06, b';n\x1d\rl\x13\x1c\x13\x16p\x14\x07U\x0c\x1f\x19w\x16\x16\x1d5T'),
"V13": (0x07, b'I\x05\t\x08\x03r)\x01$N\x0fr3n\x0b062D\x0f\x13'),
"V14": (0x05, b"\x03\x02\x1c9\x19\x15\x15q\x1057\x08\x16\x0cF\x1b.Fw\x01\x12\x03\x13\x02\x17S'hk6"),
"V15": (0x0A, b'&,4B\x1dcI\x0bU\x03I\x07\x04\x1c\t\x05c\x07%ws\x0cj\t\x1a\x08\x0f'),
"V16": (0x0A, b'\x06\x18`h;b><\x06PqR\x02Zc\x034\n\x16\x1e\x18\x06#e'),
"V17": (0x07, b'y\r\x12\x08fw.[\x02\t\n\x13\x11\x0c\x11b\x1e8L\x10(\x13<Jx6c\x0f'),
"V18": (0x07, b'I\x0b\x0e;\x19\x1aIa\x10s\x19g\\\x1b\x11!\x18yf\x0f\t\x1d7[bSp\x03'),
"V19": (0x05, b'\n6>)N\x02\x188\x016s\x13\x14\x1b\x16jeN\n\x146\x04\x18\x1c\x0c\x19\x1f,\x02]'),
"V20": (0x08, b'_\r\x01\x12]\\\x14*\x17i\x14\r\t!\x1e;~hZ\x12jK\x17\x1e*1'),
"V21": (0x07, b'e\x1d\x19|\ty\x1di|N\x13\x0e\x04\x1bj<h\x13\x15k\x12\x08=\x1f\x16~\x13l'),
"V22": (0x08, b'?\x17yi$k7Pc\tEo\x0c\x07\x07\t\x1f,*i\x12\x0cI0\x10I\x1a?2\x04'),
"V23": (0x08, b'\x16+db\x13\x04\x18\rc%\x14\x17\x0f\x13F\x0c[\t9\x1ay\x01\x1eH'),
"V24": (0x06, b'|6\\\x1a\r\x10\nP\x07\x0fu\x1f\t;\rr`uv\\~55\x11]N'),
"V25": (0x09, b'\x07\x14w\x1e;^y\x01:\x08\x07\x1fr\tU#j\x16\x12\x1eB\x04\x16=\x06fZ\x07\x02\x06'),
"V26": (0x06, b'\x03IL\x1e"K\x1f\x0f\x1fp0\x01`X\x02z0`\x03\x0eN\x07'),
"V27": (0x07, b'Xk\x10y\x02\x18\x10\x17\x1d,\x0e\x05e\x10\x15"e\x0fh(\x06s\x1c\x08I\x0c\x1b\x0e'),
"V28": (0x0A, b'6P\x1bs\x0f\x06V.\x1cM\x14\x02\n\x1b\x07{P0:\x18zaU\x05'),
"V9708": (0x05, b'\x1diIm\x08a\x17\x1e!am\x1d\x1aQ.\x16!\x06*\x04\x11\t\x06\x04?'),
"V1031": (0x08, b'Antidisestablishmentarianism'),
"V2069": (0x07, b'Floccinaucinihilipilification'),
"V9041": (0x06, b'>\x14\x0c\x12\x10-\x13&\x18U\x1d\x05Rlt\x03!\x19\x1b\x13\x04]Y\x19,\t\x1b'),
"V3646": (0x09, b'~\x18~\x16J\\\x18\x10\x05\x0b\x07\t\x0cZ\r|\x1c\x15\x1d\x11>,\x1b\x0e\x03"4\x1b\x01'),
"V6052": (0x05, b'3h\x055\x03[^>\x19\x1c\x08\x1b\rtm4\x02Rp\x0c\x16B\n'),
"V9479": (0x09, b'\x10\x1bJ\x18\nh!\x10"\x03>Z\'\r\x01]W\x06\x1c\x1e?\x0f\x13'),
"V9888": (0x05, b"K\x0c6\x1d\x1a\x17pO}Rk\x1d'w1^\x1f$\x1c{C\x02Q\x06\x1d`"),
"V4648": (0x07, b'X.\x0eW\x1c*K\x12\x12\t\n\n\x17Wx\x01\x02Yf\x0f\x18\x1bVXPi\x01'),
"V5683": (0x05, b'z3\n\x039\x12\x13`\x06=v;\x02MTK\x1e%}L\x1c\x1f\x15\x0c\x11\x02\x0c\n8\x17p'),
}
#common str: "PIDv3AESAES/CBC/PKCS5PaddingHmacSHA256"
class workspace(object):
def __init__(self,initial_list):
self.work=initial_list
def shuffle(self,shuflist):
ll=len(shuflist)
rt=[]
for i in range(ll):
rt.append(self.work[shuflist[i]])
self.work=rt
def sbox(self,table,matrix,skplist=[]): #table is list of 4-byte integers
offset=0
nwork=list(self.work)
wo=0
toff=0
while offset<0x6000:
uv5=table[toff+nwork[wo+0]]
uv1=table[toff+nwork[wo+1]+0x100]
uv2=table[toff+nwork[wo+2]+0x200]
uv3=table[toff+nwork[wo+3]+0x300]
moff=0
if 0 in skplist:
moff+=0x400
else:
nib1=matrix[moff+offset+(uv1>>0x1c)|( (uv5>>0x18)&0xf0)]
moff+=0x100
nib2=matrix[moff+offset+(uv3>>0x1c)|( (uv2>>0x18)&0xf0)]
moff+=0x100
nib3=matrix[moff+offset+((uv1>>0x18)&0xf) |( (uv5>>0x14)&0xf0)]
moff+=0x100
nib4=matrix[moff+offset+((uv3>>0x18)&0xf) |( (uv2>>0x14)&0xf0)]
moff+=0x100
rnib1=matrix[moff+offset+nib1*0x10+nib2]
moff+=0x100
rnib2=matrix[moff+offset+nib3*0x10+nib4]
moff+=0x100
nwork[wo+0]=rnib1*0x10+rnib2
if 1 in skplist:
moff+=0x400
else:
nib1=matrix[moff+offset+((uv1>>0x14)&0xf)|( (uv5>>0x10)&0xf0)]
moff+=0x100
nib2=matrix[moff+offset+((uv3>>0x14)&0xf)|( (uv2>>0x10)&0xf0)]
moff+=0x100
nib3=matrix[moff+offset+((uv1>>0x10)&0xf) |( (uv5>>0xc)&0xf0)]
moff+=0x100
nib4=matrix[moff+offset+((uv3>>0x10)&0xf) |( (uv2>>0xc)&0xf0)]
moff+=0x100
rnib1=matrix[moff+offset+nib1*0x10+nib2]
moff+=0x100
rnib2=matrix[moff+offset+nib3*0x10+nib4]
moff+=0x100
nwork[wo+1]=rnib1*0x10+rnib2
if 2 in skplist:
moff+=0x400
else:
nib1=matrix[moff+offset+((uv1>>0xc)&0xf)|( (uv5>>0x8)&0xf0)]
moff+=0x100
nib2=matrix[moff+offset+((uv3>>0xc)&0xf)|( (uv2>>0x8)&0xf0)]
moff+=0x100
nib3=matrix[moff+offset+((uv1>>0x8)&0xf) |( (uv5>>0x4)&0xf0)]
moff+=0x100
nib4=matrix[moff+offset+((uv3>>0x8)&0xf) |( (uv2>>0x4)&0xf0)]
moff+=0x100
rnib1=matrix[moff+offset+nib1*0x10+nib2]
moff+=0x100
rnib2=matrix[moff+offset+nib3*0x10+nib4]
moff+=0x100
nwork[wo+2]=rnib1*0x10+rnib2
if 3 in skplist:
moff+=0x400
else:
nib1=matrix[moff+offset+((uv1>>0x4)&0xf)|( (uv5)&0xf0)]
moff+=0x100
nib2=matrix[moff+offset+((uv3>>0x4)&0xf)|( (uv2)&0xf0)]
moff+=0x100
nib3=matrix[moff+offset+((uv1)&0xf)|( (uv5<<4)&0xf0) ]
moff+=0x100
nib4=matrix[moff+offset+((uv3)&0xf)|( (uv2<<4)&0xf0) ]
moff+=0x100
##############
rnib1=matrix[moff+offset+nib1*0x10+nib2]
moff+=0x100
rnib2=matrix[moff+offset+nib3*0x10+nib4]
moff+=0x100
nwork[wo+3]=rnib1*0x10+rnib2
offset = offset + 0x1800
wo+=4
toff+=0x400
self.work=nwork
def lookup(self,ltable):
for a in range(len(self.work)):
self.work[a]=ltable[a]
def exlookup(self,ltable):
lookoffs=0
for a in range(len(self.work)):
self.work[a]=ltable[self.work[a]+lookoffs]
lookoffs+=0x100
def mask(self, chunk):
out=[]
for a in range(len(chunk)):
self.work[a]=self.work[a]^chunk[a]
out.append(self.work[a])
return out
def process_V9708(st):
#e9c457a7dae6aa24365e7ef219b934b17ed58ee7d5329343fc3aea7860ed51f9a73de14351c9
ws=workspace([0x11]*16)
repl=[0,5,10,15,4,9,14,3,8,13,2,7,12,1,6,11]
remln=len(st)
sto=0
out=[]
while(remln>0):
ws.shuffle(repl)
ws.sbox(d0x6a06ea70,d0x6a0dab50)
ws.sbox(d0x6a073a70,d0x6a0dab50)
ws.shuffle(repl)
ws.exlookup(d0x6a072a70)
dat=ws.mask(st[sto:sto+16])
out+=dat
sto+=16
remln-=16;
return bytes(out)
def process_V1031(st):
#d53efea7fdd0fda3e1e0ebbae87cad0e8f5ef413c471c3ae81f39222a9ec8b8ed582e045918c
ws=workspace([0x06,0x18,0x60,0x68,0x3b,0x62,0x3e,0x3c,0x06,0x50,0x71,0x52,0x02,0x5a,0x63,0x03])
repl=[0,5,10,15,4,9,14,3,8,13,2,7,12,1,6,11]
remln=len(st)
sto=0
out=[]
while(remln>0):
ws.shuffle(repl)
ws.sbox(d0x6a0797c0,d0x6a0dab50,[3])
ws.sbox(d0x6a07e7c0,d0x6a0dab50,[3])
ws.shuffle(repl)
ws.sbox(d0x6a0797c0,d0x6a0dab50,[3])
ws.sbox(d0x6a07e7c0,d0x6a0dab50,[3])
ws.exlookup(d0x6a07d7c0)
dat=ws.mask(st[sto:sto+16])
out+=dat
sto+=16
remln-=16
#break
return bytes(out)
def process_V2069(st):
#8e6196d754a304c9354e91b5d79f07b048026d31c7373a8691e513f2c802c706742731caa858
ws=workspace([0x79,0x0d,0x12,0x08,0x66,0x77,0x2e,0x5b,0x02,0x09,0x0a,0x13,0x11,0x0c,0x11,0x62])
repl=[0,5,10,15,4,9,14,3,8,13,2,7,12,1,6,11]
remln=len(st)
sto=0
out=[]
while(remln>0):
ws.sbox(d0x6a084498,d0x6a0dab50,[2])
ws.shuffle(repl)
ws.sbox(d0x6a089498,d0x6a0dab50,[2])
ws.sbox(d0x6a089498,d0x6a0dab50,[2])
ws.sbox(d0x6a084498,d0x6a0dab50,[2])
ws.shuffle(repl)
ws.exlookup(d0x6a088498)
dat=ws.mask(st[sto:sto+16])
out+=dat
sto+=16
remln-=16
return bytes(out)
def process_V9041(st):
#11f7db074b24e560dfa6fae3252b383c3b936e51f6ded570dc936cb1da9f4fc4a97ec686e7d8
ws=workspace([0x49,0x0b,0x0e,0x3b,0x19,0x1a,0x49,0x61,0x10,0x73,0x19,0x67,0x5c,0x1b,0x11,0x21])
repl=[0,5,10,15,4,9,14,3,8,13,2,7,12,1,6,11]
remln=len(st)
sto=0
out=[]
while(remln>0):
ws.sbox(d0x6a094170,d0x6a0dab50,[1])
ws.shuffle(repl)
ws.shuffle(repl)
ws.sbox(d0x6a08f170,d0x6a0dab50,[1])
ws.sbox(d0x6a08f170,d0x6a0dab50,[1])
ws.sbox(d0x6a094170,d0x6a0dab50,[1])
ws.exlookup(d0x6a093170)
dat=ws.mask(st[sto:sto+16])
out+=dat
sto+=16
remln-=16
#break
return bytes(out)
def process_V3646(st):
#d468aa362b44479282291983243b38197c4b4aa24c2c58e62c76ec4b81e08556ca0c54301664
ws=workspace([0x0a,0x36,0x3e,0x29,0x4e,0x02,0x18,0x38,0x01,0x36,0x73,0x13,0x14,0x1b,0x16,0x6a])
repl=[0,5,10,15,4,9,14,3,8,13,2,7,12,1,6,11]
remln=len(st)
sto=0
out=[]
while(remln>0):
ws.shuffle(repl)
ws.sbox(d0x6a099e48,d0x6a0dab50,[2,3])
ws.sbox(d0x6a09ee48,d0x6a0dab50,[2,3])
ws.sbox(d0x6a09ee48,d0x6a0dab50,[2,3])
ws.shuffle(repl)
ws.sbox(d0x6a099e48,d0x6a0dab50,[2,3])
ws.sbox(d0x6a099e48,d0x6a0dab50,[2,3])
ws.shuffle(repl)
ws.sbox(d0x6a09ee48,d0x6a0dab50,[2,3])
ws.exlookup(d0x6a09de48)
dat=ws.mask(st[sto:sto+16])
out+=dat
sto+=16
remln-=16
return bytes(out)
def process_V6052(st):
#d683c8c4e4f46ae45812196f37e218eabce0fae08994f25fabb01d3e569b8bf3866b99d36f57
ws=workspace([0x5f,0x0d,0x01,0x12,0x5d,0x5c,0x14,0x2a,0x17,0x69,0x14,0x0d,0x09,0x21,0x1e,0x3b])
repl=[0,5,10,15,4,9,14,3,8,13,2,7,12,1,6,11]
remln=len(st)
sto=0
out=[]
while(remln>0):
ws.shuffle(repl)
ws.sbox(d0x6a0a4b20,d0x6a0dab50,[1,3])
ws.shuffle(repl)
ws.sbox(d0x6a0a4b20,d0x6a0dab50,[1,3])
ws.sbox(d0x6a0a9b20,d0x6a0dab50,[1,3])
ws.shuffle(repl)
ws.sbox(d0x6a0a9b20,d0x6a0dab50,[1,3])
ws.sbox(d0x6a0a9b20,d0x6a0dab50,[1,3])
ws.sbox(d0x6a0a4b20,d0x6a0dab50,[1,3])
ws.exlookup(d0x6a0a8b20)
dat=ws.mask(st[sto:sto+16])
out+=dat
sto+=16
remln-=16
return bytes(out)
def process_V9479(st):
#925635db434bccd3f4791eb87b89d2dfc7c93be06e794744eb9de58e6d721e696980680ab551
ws=workspace([0x65,0x1d,0x19,0x7c,0x09,0x79,0x1d,0x69,0x7c,0x4e,0x13,0x0e,0x04,0x1b,0x6a,0x3c ])
repl=[0,5,10,15,4,9,14,3,8,13,2,7,12,1,6,11]
remln=len(st)
sto=0
out=[]
while(remln>0):
ws.sbox(d0x6a0af7f8,d0x6a0dab50,[1,2,3])
ws.sbox(d0x6a0af7f8,d0x6a0dab50,[1,2,3])
ws.sbox(d0x6a0b47f8,d0x6a0dab50,[1,2,3])
ws.sbox(d0x6a0af7f8,d0x6a0dab50,[1,2,3])
ws.shuffle(repl)
ws.sbox(d0x6a0b47f8,d0x6a0dab50,[1,2,3])
ws.shuffle(repl)
ws.shuffle(repl)
ws.sbox(d0x6a0b47f8,d0x6a0dab50,[1,2,3])
ws.exlookup(d0x6a0b37f8)
dat=ws.mask(st[sto:sto+16])
out+=dat
sto+=16
remln-=16
return bytes(out)
def process_V9888(st):
#54c470723f8c105ba0186b6319050869de673ce31a5ec15d4439921d4cd05c5e860cb2a41fea
ws=workspace([0x3f,0x17,0x79,0x69,0x24,0x6b,0x37,0x50,0x63,0x09,0x45,0x6f,0x0c,0x07,0x07,0x09])
repl=[0,5,10,15,4,9,14,3,8,13,2,7,12,1,6,11]
remln=len(st)
sto=0
out=[]
while(remln>0):
ws.sbox(d0x6a0ba4d0,d0x6a0dab50,[1,2])
ws.sbox(d0x6a0bf4d0,d0x6a0dab50,[1,2])
ws.sbox(d0x6a0bf4d0,d0x6a0dab50,[1,2])
ws.sbox(d0x6a0ba4d0,d0x6a0dab50,[1,2])
ws.shuffle(repl)
ws.shuffle(repl)
ws.shuffle(repl)
ws.sbox(d0x6a0bf4d0,d0x6a0dab50,[1,2])
ws.sbox(d0x6a0ba4d0,d0x6a0dab50,[1,2])
ws.exlookup(d0x6a0be4d0)
dat=ws.mask(st[sto:sto+16])
out+=dat
sto+=16
remln-=16
return bytes(out)
def process_V4648(st):
#705bd4cd8b61d4596ef4ca40774d68e71f1f846c6e94bd23fd26e5c127e0beaa650a50171f1b
ws=workspace([0x16,0x2b,0x64,0x62,0x13,0x04,0x18,0x0d,0x63,0x25,0x14,0x17,0x0f,0x13,0x46,0x0c])
repl=[0,5,10,15,4,9,14,3,8,13,2,7,12,1,6,11]
remln=len(st)
sto=0
out=[]
while(remln>0):
ws.sbox(d0x6a0ca1a8,d0x6a0dab50,[1,3])
ws.shuffle(repl)
ws.sbox(d0x6a0ca1a8,d0x6a0dab50,[1,3])
ws.sbox(d0x6a0c51a8,d0x6a0dab50,[1,3])
ws.sbox(d0x6a0ca1a8,d0x6a0dab50,[1,3])
ws.sbox(d0x6a0c51a8,d0x6a0dab50,[1,3])
ws.sbox(d0x6a0c51a8,d0x6a0dab50,[1,3])
ws.shuffle(repl)
ws.shuffle(repl)
ws.exlookup(d0x6a0c91a8)
dat=ws.mask(st[sto:sto+16])
out+=dat
sto+=16
remln-=16
return bytes(out)
def process_V5683(st):
#1f5af733423e5104afb9d5594e682ecf839a776257f33747c9beee671c57ab3f84943f69d8fd
ws=workspace([0x7c,0x36,0x5c,0x1a,0x0d,0x10,0x0a,0x50,0x07,0x0f,0x75,0x1f,0x09,0x3b,0x0d,0x72])
repl=[0,5,10,15,4,9,14,3,8,13,2,7,12,1,6,11]
remln=len(st)
sto=0
out=[]
while(remln>0):
ws.sbox(d0x6a0d4e80,d0x6a0dab50,[])
ws.shuffle(repl)
ws.sbox(d0x6a0cfe80,d0x6a0dab50,[])
ws.sbox(d0x6a0d4e80,d0x6a0dab50,[])
ws.sbox(d0x6a0cfe80,d0x6a0dab50,[])
ws.sbox(d0x6a0d4e80,d0x6a0dab50,[])
ws.shuffle(repl)
ws.sbox(d0x6a0cfe80,d0x6a0dab50,[])
ws.shuffle(repl)
ws.exlookup(d0x6a0d3e80)
dat=ws.mask(st[sto:sto+16])
out+=dat
sto+=16
remln-=16
return bytes(out)
# def a2hex(arr):
# ax=[]
# ha="0123456789abcdef"
# for a in arr:
# if a<0: a=256+a
# ax.append(ha[(a>>4)]+ha[a%16])
# return "".join(ax)
#
# def memhex(adr,sz):
# emu=EmulatorHelper(currentProgram)
# arr=emu.readMemory(getAddress(adr),sz)
# return a2hex(arr)
#
# obfuscate shared secret according to the VoucherEnvelope version
def obfuscate(secret, version):
if version == 1: # v1 does not use obfuscation
return secret
magic, word = OBFUSCATION_TABLE["V%d" % version]
# extend secret so that its length is divisible by the magic number
if len(secret) % magic != 0:
secret = secret + b'\x00' * (magic - len(secret) % magic)
secret = bytearray(secret)
obfuscated = bytearray(len(secret))
wordhash = bytearray(hashlib.sha256(word).digest())
# shuffle secret and xor it with the first half of the word hash
for i in range(0, len(secret)):
index = i // (len(secret) // magic) + magic * (i % (len(secret) // magic))
obfuscated[index] = secret[i] ^ wordhash[index % 16]
return obfuscated
# scramble() and obfuscate2() from https://github.com/andrewc12/DeDRM_tools/commit/d9233d61f00d4484235863969919059f4d0b2057
def scramble(st,magic):
ret=bytearray(len(st))
padlen=len(st)
for counter in range(len(st)):
ivar2=(padlen//2)-2*(counter%magic)+magic+counter-1
ret[ivar2%padlen]=st[counter]
return ret
def obfuscate2(secret, version):
if version == 1: # v1 does not use obfuscation
return secret
magic, word = OBFUSCATION_TABLE["V%d" % version]
# extend secret so that its length is divisible by the magic number
if len(secret) % magic != 0:
secret = secret + b'\x00' * (magic - len(secret) % magic)
obfuscated = bytearray(len(secret))
wordhash = bytearray(hashlib.sha256(word).digest()[16:])
#print(wordhash.hex())
shuffled = bytearray(scramble(secret,magic))
for i in range(0, len(secret)):
obfuscated[i] = shuffled[i] ^ wordhash[i % 16]
return obfuscated
# scramble3() and obfuscate3() from https://github.com/Satsuoni/DeDRM_tools/commit/da6b6a0c911b6d45fe1b13042b690daebc1cc22f
def scramble3(st,magic):
ret=bytearray(len(st))
padlen=len(st)
divs = padlen // magic
cntr = 0
iVar6 = 0
offset = 0
if (0 < ((magic - 1) + divs)):
while True:
if (offset & 1) == 0 :
uVar4 = divs - 1
if offset < divs:
iVar3 = 0
uVar4 = offset
else:
iVar3 = (offset - divs) + 1
if uVar4>=0:
iVar5 = uVar4 * magic
index = ((padlen - 1) - cntr)
while True:
if (magic <= iVar3): break
ret[index] = st[iVar3 + iVar5]
iVar3 = iVar3 + 1
cntr = cntr + 1
uVar4 = uVar4 - 1
iVar5 = iVar5 - magic
index -= 1
if uVar4<=-1: break
else:
if (offset < magic):
iVar3 = 0
else :
iVar3 = (offset - magic) + 1
if (iVar3 < divs):
uVar4 = offset
if (magic <= offset):
uVar4 = magic - 1
index = ((padlen - 1) - cntr)
iVar5 = iVar3 * magic
while True:
if (uVar4 < 0) : break
iVar3 += 1
ret[index] = st[uVar4 + iVar5]
uVar4 -= 1
index=index-1
iVar5 = iVar5 + magic;
cntr += 1;
if iVar3>=divs: break
offset = offset + 1
if offset >= ((magic - 1) + divs) :break
return ret
#not sure if the third variant is used anywhere, but it is in Kindle, so I tried to add it
def obfuscate3(secret, version):
if version == 1: # v1 does not use obfuscation
return secret
magic, word = OBFUSCATION_TABLE["V%d" % version]
# extend secret so that its length is divisible by the magic number
if len(secret) % magic != 0:
secret = secret + b'\x00' * (magic - len(secret) % magic)
#secret = bytearray(secret)
obfuscated = bytearray(len(secret))
wordhash = bytearray(hashlib.sha256(word).digest())
#print(wordhash.hex())
shuffled=bytearray(scramble3(secret,magic))
#print(shuffled)
# shuffle secret and xor it with the first half of the word hash
for i in range(0, len(secret)):
obfuscated[i] = shuffled[i] ^ wordhash[i % 16]
return obfuscated
class DrmIonVoucher(object):
envelope = None
version = None
voucher = None
drmkey = None
license_type = "Unknown"
@ -753,7 +1314,13 @@ class DrmIonVoucher(object):
secretkey = b""
def __init__(self, voucherenv, dsn, secret):
self.dsn,self.secret = dsn,secret
self.dsn, self.secret = dsn, secret
if isinstance(dsn, str):
self.dsn = dsn.encode('ASCII')
if isinstance(secret, str):
self.secret = secret.encode('ASCII')
self.lockparams = []
@ -761,29 +1328,46 @@ class DrmIonVoucher(object):
addprottable(self.envelope)
def decryptvoucher(self):
shared = "PIDv3" + self.encalgorithm + self.enctransformation + self.hashalgorithm
shared = ("PIDv3" + self.encalgorithm + self.enctransformation + self.hashalgorithm).encode('ASCII')
self.lockparams.sort()
for param in self.lockparams:
if param == "ACCOUNT_SECRET":
shared += param + self.secret
shared += param.encode('ASCII') + self.secret
elif param == "CLIENT_ID":
shared += param + self.dsn
shared += param.encode('ASCII') + self.dsn
else:
_assert(False, "Unknown lock parameter: %s" % param)
sharedsecret = shared.encode("UTF-8")
key = hmac.new(sharedsecret, sharedsecret[:5], digestmod=hashlib.sha256).digest()
aes = AES.new(key[:32], AES.MODE_CBC, self.cipheriv[:16])
b = aes.decrypt(self.ciphertext)
b = pkcs7unpad(b, 16)
# i know that version maps to scramble pretty much 1 to 1, but there was precendent where they changed it, so...
sharedsecrets = [obfuscate(shared, self.version),obfuscate2(shared, self.version),obfuscate3(shared, self.version),
process_V9708(shared), process_V1031(shared), process_V2069(shared), process_V9041(shared),
process_V3646(shared), process_V6052(shared), process_V9479(shared), process_V9888(shared),
process_V4648(shared), process_V5683(shared)]
self.drmkey = BinaryIonParser(StringIO(b))
addprottable(self.drmkey)
decrypted=False
lastexception = None # type: Exception | None
for sharedsecret in sharedsecrets:
key = hmac.new(sharedsecret, b"PIDv3", digestmod=hashlib.sha256).digest()
aes = AES.new(key[:32], AES.MODE_CBC, self.cipheriv[:16])
try:
b = aes.decrypt(self.ciphertext)
b = pkcs7unpad(b, 16)
self.drmkey = BinaryIonParser(BytesIO(b))
addprottable(self.drmkey)
_assert(self.drmkey.hasnext() and self.drmkey.next() == TID_LIST and self.drmkey.gettypename() == "com.amazon.drm.KeySet@1.0",
"Expected KeySet, got %s" % self.drmkey.gettypename())
_assert(self.drmkey.hasnext() and self.drmkey.next() == TID_LIST and self.drmkey.gettypename() == "com.amazon.drm.KeySet@1.0",
"Expected KeySet, got %s" % self.drmkey.gettypename())
decrypted=True
print("Decryption succeeded")
break
except Exception as ex:
lastexception = ex
print("Decryption failed, trying next fallback ")
if not decrypted:
raise lastexception
self.drmkey.stepin()
while self.drmkey.hasnext():
@ -809,15 +1393,16 @@ class DrmIonVoucher(object):
def parse(self):
self.envelope.reset()
_assert(self.envelope.hasnext(), "Envelope is empty")
_assert(self.envelope.next() == TID_STRUCT and self.envelope.gettypename() == "com.amazon.drm.VoucherEnvelope@1.0",
_assert(self.envelope.next() == TID_STRUCT and str.startswith(self.envelope.gettypename(), "com.amazon.drm.VoucherEnvelope@"),
"Unknown type encountered in envelope, expected VoucherEnvelope")
self.version = int(self.envelope.gettypename().split('@')[1][:-2])
self.envelope.stepin()
while self.envelope.hasnext():
self.envelope.next()
field = self.envelope.getfieldname()
if field == "voucher":
self.voucher = BinaryIonParser(StringIO(self.envelope.lobvalue()))
self.voucher = BinaryIonParser(BytesIO(self.envelope.lobvalue()))
addprottable(self.voucher)
continue
elif field != "strategy":
@ -938,6 +1523,7 @@ class DrmIon(object):
elif self.ion.gettypename() in ["com.amazon.drm.EncryptedPage@1.0", "com.amazon.drm.EncryptedPage@2.0"]:
decompress = False
decrypt = True
ct = None
civ = None
self.ion.stepin()
@ -951,7 +1537,23 @@ class DrmIon(object):
civ = self.ion.lobvalue()
if ct is not None and civ is not None:
self.processpage(ct, civ, outpages, decompress)
self.processpage(ct, civ, outpages, decompress, decrypt)
self.ion.stepout()
elif self.ion.gettypename() in ["com.amazon.drm.PlainText@1.0", "com.amazon.drm.PlainText@2.0"]:
decompress = False
decrypt = False
plaintext = None
self.ion.stepin()
while self.ion.hasnext():
self.ion.next()
if self.ion.gettypename() == "com.amazon.drm.Compressed@1.0":
decompress = True
if self.ion.getfieldname() == "data":
plaintext = self.ion.lobvalue()
if plaintext is not None:
self.processpage(plaintext, None, outpages, decompress, decrypt)
self.ion.stepout()
self.ion.stepout()
@ -962,15 +1564,18 @@ class DrmIon(object):
def print_(self, lst):
self.ion.print_(lst)
def processpage(self, ct, civ, outpages, decompress):
aes = AES.new(self.key[:16], AES.MODE_CBC, civ[:16])
msg = pkcs7unpad(aes.decrypt(ct), 16)
def processpage(self, ct, civ, outpages, decompress, decrypt):
if decrypt:
aes = AES.new(self.key[:16], AES.MODE_CBC, civ[:16])
msg = pkcs7unpad(aes.decrypt(ct), 16)
else:
msg = ct
if not decompress:
outpages.write(msg)
return
_assert(msg[0] == b"\x00", "LZMA UseFilter not supported")
_assert(msg[0] == 0, "LZMA UseFilter not supported")
if calibre_lzma is not None:
with calibre_lzma.decompress(msg[1:], bufsize=0x1000000) as f:

View file

@ -1,13 +1,11 @@
#!/usr/bin/env python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from __future__ import with_statement
# k4mobidedrm.py
# Copyright © 2008-2019 by Apprentice Harper et al.
# Copyright © 2008-2020 by Apprentice Harper et al.
__license__ = 'GPL v3'
__version__ = '5.7'
__version__ = '6.0'
# Engine to remove drm from Kindle and Mobipocket ebooks
# for personal use for archiving and converting your ebooks
@ -62,6 +60,8 @@ __version__ = '5.7'
# 5.5 - Added GPL v3 licence explicitly.
# 5.6 - Invoke KFXZipBook to handle zipped KFX files
# 5.7 - Revamp cleanup_name
# 6.0 - Added Python 3 compatibility for calibre 5.0
import sys, os, re
import csv
@ -69,87 +69,29 @@ import getopt
import re
import traceback
import time
import htmlentitydefs
try:
import html.entities as htmlentitydefs
except:
import htmlentitydefs
import json
#@@CALIBRE_COMPAT_CODE@@
class DrmException(Exception):
pass
if 'calibre' in sys.modules:
inCalibre = True
else:
inCalibre = False
import mobidedrm
import topazextract
import kgenpids
import androidkindlekey
import kfxdedrm
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
from .utilities import SafeUnbuffered
# 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,unicode):
data = data.encode(self.encoding,"replace")
self.stream.write(data)
self.stream.flush()
def __getattr__(self, attr):
return getattr(self.stream, attr)
from .argv_utils import unicode_argv
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
xrange(start, argc.value)]
# if we don't have any arguments at all, just pass back script name
# this should never happen
return [u"mobidedrm.py"]
else:
argvencoding = sys.stdin.encoding
if argvencoding == None:
argvencoding = "utf-8"
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
# cleanup unicode filenames
# borrowed from calibre from calibre/src/calibre/__init__.py
@ -159,68 +101,71 @@ def unicode_argv():
# and some improvements suggested by jhaisley
def cleanup_name(name):
# substitute filename unfriendly characters
name = name.replace(u"<",u"[").replace(u">",u"]").replace(u" : ",u" ").replace(u": ",u" ").replace(u":",u"").replace(u"/",u"_").replace(u"\\",u"_").replace(u"|",u"_").replace(u"\"",u"\'").replace(u"*",u"_").replace(u"?",u"")
name = name.replace("<","[").replace(">","]").replace(" : "," ").replace(": "," ").replace(":","").replace("/","_").replace("\\","_").replace("|","_").replace("\"","\'").replace("*","_").replace("?","")
# white space to single space, delete leading and trailing while space
name = re.sub(ur"\s", u" ", name).strip()
name = re.sub(r"\s", " ", name).strip()
# delete control characters
name = u"".join(char for char in name if ord(char)>=32)
name = "".join(char for char in name if ord(char)>=32)
# delete non-ascii characters
name = u"".join(char for char in name if ord(char)<=126)
name = "".join(char for char in name if ord(char)<=126)
# remove leading dots
while len(name)>0 and name[0] == u".":
while len(name)>0 and name[0] == ".":
name = name[1:]
# remove trailing dots (Windows doesn't like them)
while name.endswith(u'.'):
while name.endswith("."):
name = name[:-1]
if len(name)==0:
name=u"DecryptedBook"
name="DecryptedBook"
return name
# must be passed unicode
def unescape(text):
def fixup(m):
text = m.group(0)
if text[:2] == u"&#":
if text[:2] == "&#":
# character reference
try:
if text[:3] == u"&#x":
return unichr(int(text[3:-1], 16))
if text[:3] == "&#x":
return chr(int(text[3:-1], 16))
else:
return unichr(int(text[2:-1]))
return chr(int(text[2:-1]))
except ValueError:
pass
else:
# named entity
try:
text = unichr(htmlentitydefs.name2codepoint[text[1:-1]])
text = chr(htmlentitydefs.name2codepoint[text[1:-1]])
except KeyError:
pass
return text # leave as is
return re.sub(u"&#?\w+;", fixup, text)
return re.sub("&#?\\w+;", fixup, text)
def GetDecryptedBook(infile, kDatabases, androidFiles, serials, pids, starttime = time.time()):
# handle the obvious cases at the beginning
if not os.path.isfile(infile):
raise DrmException(u"Input file does not exist.")
raise DrmException("Input file does not exist.")
mobi = True
magic8 = open(infile,'rb').read(8)
if magic8 == '\xeaDRMION\xee':
raise DrmException(u"The .kfx DRMION file cannot be decrypted by itself. A .kfx-zip archive containing a DRM voucher is required.")
if magic8 == b'\xeaDRMION\xee':
raise DrmException("The .kfx DRMION file cannot be decrypted by itself. A .kfx-zip archive containing a DRM voucher is required.")
magic3 = magic8[:3]
if magic3 == 'TPZ':
if magic3 == b'TPZ':
mobi = False
if magic8[:4] == 'PK\x03\x04':
if magic8[:4] == b'PK\x03\x04':
mb = kfxdedrm.KFXZipBook(infile)
elif mobi:
mb = mobidedrm.MobiBook(infile)
else:
mb = topazextract.TopazBook(infile)
bookname = unescape(mb.getBookTitle())
print u"Decrypting {1} ebook: {0}".format(bookname, mb.getBookType())
try:
bookname = unescape(mb.getBookTitle())
print("Decrypting {1} ebook: {0}".format(bookname, mb.getBookType()))
except:
print("Decrypting {0} ebook.".format(mb.getBookType()))
# copy list of pids
totalpids = list(pids)
@ -232,16 +177,16 @@ def GetDecryptedBook(infile, kDatabases, androidFiles, serials, pids, starttime
totalpids.extend(kgenpids.getPidList(md1, md2, serials, kDatabases))
# remove any duplicates
totalpids = list(set(totalpids))
print u"Found {1:d} keys to try after {0:.1f} seconds".format(time.time()-starttime, len(totalpids))
print("Found {1:d} keys to try after {0:.1f} seconds".format(time.time()-starttime, len(totalpids)))
#print totalpids
try:
mb.processBook(totalpids)
except:
mb.cleanup
mb.cleanup()
raise
print u"Decryption succeeded after {0:.1f} seconds".format(time.time()-starttime)
print("Decryption succeeded after {0:.1f} seconds".format(time.time()-starttime))
return mb
@ -255,16 +200,16 @@ def decryptBook(infile, outdir, kDatabaseFiles, androidFiles, serials, pids):
with open(dbfile, 'r') as keyfilein:
kindleDatabase = json.loads(keyfilein.read())
kDatabases.append([dbfile,kindleDatabase])
except Exception, e:
print u"Error getting database from file {0:s}: {1:s}".format(dbfile,e)
except Exception as e:
print("Error getting database from file {0:s}: {1:s}".format(dbfile,e))
traceback.print_exc()
try:
book = GetDecryptedBook(infile, kDatabases, androidFiles, serials, pids, starttime)
except Exception, e:
print u"Error decrypting book after {1:.1f} seconds: {0}".format(e.args[0],time.time()-starttime)
except Exception as e:
print("Error decrypting book after {1:.1f} seconds: {0}".format(e.args[0],time.time()-starttime))
traceback.print_exc()
return 1
@ -272,10 +217,10 @@ def decryptBook(infile, outdir, kDatabaseFiles, androidFiles, serials, pids):
orig_fn_root = os.path.splitext(os.path.basename(infile))[0]
if (
re.match('^B[A-Z0-9]{9}(_EBOK|_EBSP|_sample)?$', orig_fn_root) or
re.match('^{0-9A-F-}{36}$', orig_fn_root)
re.match('^[0-9A-F-]{36}$', orig_fn_root)
): # Kindle for PC / Mac / Android / Fire / iOS
clean_title = cleanup_name(book.getBookTitle())
outfilename = u'{}_{}'.format(orig_fn_root, clean_title)
outfilename = "{}_{}".format(orig_fn_root, clean_title)
else: # E Ink Kindle, which already uses a reasonable name
outfilename = orig_fn_root
@ -283,16 +228,16 @@ def decryptBook(infile, outdir, kDatabaseFiles, androidFiles, serials, pids):
if len(outfilename)>150:
outfilename = outfilename[:99]+"--"+outfilename[-49:]
outfilename = outfilename+u"_nodrm"
outfilename = outfilename+"_nodrm"
outfile = os.path.join(outdir, outfilename + book.getBookExtension())
book.getFile(outfile)
print u"Saved decrypted book {1:s} after {0:.1f} seconds".format(time.time()-starttime, outfilename)
print("Saved decrypted book {1:s} after {0:.1f} seconds".format(time.time()-starttime, outfilename))
if book.getBookType()==u"Topaz":
zipname = os.path.join(outdir, outfilename + u"_SVG.zip")
if book.getBookType()=="Topaz":
zipname = os.path.join(outdir, outfilename + "_SVG.zip")
book.getSVGZip(zipname)
print u"Saved SVG ZIP Archive for {1:s} after {0:.1f} seconds".format(time.time()-starttime, outfilename)
print("Saved SVG ZIP Archive for {1:s} after {0:.1f} seconds".format(time.time()-starttime, outfilename))
# remove internal temporary directory of Topaz pieces
book.cleanup()
@ -300,22 +245,22 @@ def decryptBook(infile, outdir, kDatabaseFiles, androidFiles, serials, pids):
def usage(progname):
print u"Removes DRM protection from Mobipocket, Amazon KF8, Amazon Print Replica and Amazon Topaz ebooks"
print u"Usage:"
print u" {0} [-k <kindle.k4i>] [-p <comma separated PIDs>] [-s <comma separated Kindle serial numbers>] [ -a <AmazonSecureStorage.xml|backup.ab> ] <infile> <outdir>".format(progname)
print("Removes DRM protection from Mobipocket, Amazon KF8, Amazon Print Replica and Amazon Topaz ebooks")
print("Usage:")
print(" {0} [-k <kindle.k4i>] [-p <comma separated PIDs>] [-s <comma separated Kindle serial numbers>] [ -a <AmazonSecureStorage.xml|backup.ab> ] <infile> <outdir>".format(progname))
#
# Main
#
def cli_main():
argv=unicode_argv()
argv=unicode_argv("k4mobidedrm.py")
progname = os.path.basename(argv[0])
print u"K4MobiDeDrm v{0}.\nCopyright © 2008-2017 Apprentice Harper et al.".format(__version__)
print("K4MobiDeDrm v{0}.\nCopyright © 2008-2020 Apprentice Harper et al.".format(__version__))
try:
opts, args = getopt.getopt(argv[1:], "k:p:s:a:")
except getopt.GetoptError, err:
print u"Error in options or arguments: {0}".format(err.args[0])
opts, args = getopt.getopt(argv[1:], "k:p:s:a:h")
except getopt.GetoptError as err:
print("Error in options or arguments: {0}".format(err.args[0]))
usage(progname)
sys.exit(2)
if len(args)<2:
@ -330,6 +275,9 @@ def cli_main():
pids = []
for o, a in opts:
if o == "-h":
usage(progname)
sys.exit(0)
if o == "-k":
if a == None :
raise DrmException("Invalid parameter for -k")
@ -337,7 +285,7 @@ def cli_main():
if o == "-p":
if a == None :
raise DrmException("Invalid parameter for -p")
pids = a.split(',')
pids = a.encode('utf-8').split(b',')
if o == "-s":
if a == None :
raise DrmException("Invalid parameter for -s")
@ -347,9 +295,6 @@ def cli_main():
raise DrmException("Invalid parameter for -a")
androidFiles.append(a)
# try with built in Kindle Info files if not on Linux
k4 = not sys.platform.startswith('linux')
return decryptBook(infile, outdir, kDatabaseFiles, androidFiles, serials, pids)

View file

@ -1,27 +1,30 @@
#!/usr/bin/env python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from __future__ import with_statement
# Engine to remove drm from Kindle KFX ebooks
import os
# 2.0 - Python 3 for calibre 5.0
# 2.1 - Some fixes for debugging
# 2.1.1 - Whitespace!
import os, sys
import shutil
import traceback
import zipfile
try:
from cStringIO import StringIO
except ImportError:
from StringIO import StringIO
from io import BytesIO
#@@CALIBRE_COMPAT_CODE@@
from ion import DrmIon, DrmIonVoucher
try:
from calibre_plugins.dedrm import ion
except ImportError:
import ion
__license__ = 'GPL v3'
__version__ = '1.0'
__version__ = '2.0'
class KFXZipBook:
@ -38,58 +41,64 @@ class KFXZipBook:
for filename in zf.namelist():
with zf.open(filename) as fh:
data = fh.read(8)
if data != '\xeaDRMION\xee':
if data != b'\xeaDRMION\xee':
continue
data += fh.read()
if self.voucher is None:
self.decrypt_voucher(totalpids)
print u'Decrypting KFX DRMION: {0}'.format(filename)
outfile = StringIO()
ion.DrmIon(StringIO(data[8:-8]), lambda name: self.voucher).parse(outfile)
print("Decrypting KFX DRMION: {0}".format(filename))
outfile = BytesIO()
DrmIon(BytesIO(data[8:-8]), lambda name: self.voucher).parse(outfile)
self.decrypted[filename] = outfile.getvalue()
if not self.decrypted:
print(u'The .kfx-zip archive does not contain an encrypted DRMION file')
print("The .kfx-zip archive does not contain an encrypted DRMION file")
def decrypt_voucher(self, totalpids):
with zipfile.ZipFile(self.infile, 'r') as zf:
for info in zf.infolist():
with zf.open(info.filename) as fh:
data = fh.read(4)
if data != '\xe0\x01\x00\xea':
if data != b'\xe0\x01\x00\xea':
continue
data += fh.read()
if 'ProtectedData' in data:
if b'ProtectedData' in data:
break # found DRM voucher
else:
raise Exception(u'The .kfx-zip archive contains an encrypted DRMION file without a DRM voucher')
raise Exception("The .kfx-zip archive contains an encrypted DRMION file without a DRM voucher")
print u'Decrypting KFX DRM voucher: {0}'.format(info.filename)
print("Decrypting KFX DRM voucher: {0}".format(info.filename))
for pid in [''] + totalpids:
for dsn_len,secret_len in [(0,0), (16,0), (16,40), (32,40), (40,40)]:
# Belt and braces. PIDs should be unicode strings, but just in case...
if isinstance(pid, bytes):
pid = pid.decode('ascii')
for dsn_len,secret_len in [(0,0), (16,0), (16,40), (32,0), (32,40), (40,0), (40,40)]:
if len(pid) == dsn_len + secret_len:
break # split pid into DSN and account secret
else:
continue
try:
voucher = ion.DrmIonVoucher(StringIO(data), pid[:dsn_len], pid[dsn_len:])
voucher = DrmIonVoucher(BytesIO(data), pid[:dsn_len], pid[dsn_len:])
voucher.parse()
voucher.decryptvoucher()
break
except:
traceback.print_exc()
pass
else:
raise Exception(u'Failed to decrypt KFX DRM voucher with any key')
raise Exception("Failed to decrypt KFX DRM voucher with any key")
print u'KFX DRM voucher successfully decrypted'
print("KFX DRM voucher successfully decrypted")
license_type = voucher.getlicensetype()
if license_type != "Purchase":
raise Exception((u'This book is licensed as {0}. '
'These tools are intended for use on purchased books.').format(license_type))
#raise Exception(("This book is licensed as {0}. "
# 'These tools are intended for use on purchased books.').format(license_type))
print("Warning: This book is licensed as {0}. "
"These tools are intended for use on purchased books. Continuing ...".format(license_type))
self.voucher = voucher

5771
DeDRM_plugin/kfxtables.py Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,18 +1,18 @@
#!/usr/bin/env python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from __future__ import with_statement
# kgenpids.py
# Copyright © 2008-2017 Apprentice Harper et al.
# Copyright © 2008-2020 Apprentice Harper et al.
__license__ = 'GPL v3'
__version__ = '2.1'
__version__ = '3.0'
# Revision history:
# 2.0 - Fix for non-ascii Windows user names
# 2.1 - Actual fix for non-ascii WIndows user names.
# x.x - Return information needed for KFX decryption
# 2.2 - Return information needed for KFX decryption
# 3.0 - Python 3 for calibre 5.0
import sys
import os, csv
@ -30,9 +30,9 @@ global charMap3
global charMap4
charMap1 = 'n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M'
charMap3 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
charMap4 = 'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789'
charMap1 = b'n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M'
charMap3 = b'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
charMap4 = b'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789'
# crypto digestroutines
import hashlib
@ -49,14 +49,21 @@ def SHA1(message):
# Encode the bytes in data with the characters in map
# data and map should be byte arrays
def encode(data, map):
result = ''
result = b''
for char in data:
value = ord(char)
if sys.version_info[0] == 2:
value = ord(char)
else:
value = char
Q = (value ^ 0x80) // len(map)
R = value % len(map)
result += map[Q]
result += map[R]
result += bytes(bytearray([map[Q]]))
result += bytes(bytearray([map[R]]))
return result
# Hash the bytes in data and then encode the digest with the characters in map
@ -83,8 +90,11 @@ def decode(data,map):
def getTwoBitsFromBitField(bitField,offset):
byteNumber = offset // 4
bitPosition = 6 - 2*(offset % 4)
return ord(bitField[byteNumber]) >> bitPosition & 3
if sys.version_info[0] == 2:
return ord(bitField[byteNumber]) >> bitPosition & 3
else:
return bitField[byteNumber] >> bitPosition & 3
# Returns the six bits at offset from a bit field
def getSixBitsFromBitField(bitField,offset):
offset *= 3
@ -94,9 +104,10 @@ def getSixBitsFromBitField(bitField,offset):
# 8 bits to six bits encoding from hash to generate PID string
def encodePID(hash):
global charMap3
PID = ''
PID = b''
for position in range (0,8):
PID += charMap3[getSixBitsFromBitField(hash,position)]
PID += bytes(bytearray([charMap3[getSixBitsFromBitField(hash,position)]]))
return PID
# Encryption table used to generate the device PID
@ -117,7 +128,7 @@ def generatePidEncryptionTable() :
def generatePidSeed(table,dsn) :
value = 0
for counter in range (0,4) :
index = (ord(dsn[counter]) ^ value) &0xFF
index = (dsn[counter] ^ value) & 0xFF
value = (value >> 8) ^ table[index]
return value
@ -125,15 +136,15 @@ def generatePidSeed(table,dsn) :
def generateDevicePID(table,dsn,nbRoll):
global charMap4
seed = generatePidSeed(table,dsn)
pidAscii = ''
pidAscii = b''
pid = [(seed >>24) &0xFF,(seed >> 16) &0xff,(seed >> 8) &0xFF,(seed) & 0xFF,(seed>>24) & 0xFF,(seed >> 16) &0xff,(seed >> 8) &0xFF,(seed) & 0xFF]
index = 0
for counter in range (0,nbRoll):
pid[index] = pid[index] ^ ord(dsn[counter])
pid[index] = pid[index] ^ dsn[counter]
index = (index+1) %8
for counter in range (0,8):
index = ((((pid[counter] >>5) & 3) ^ pid[counter]) & 0x1f) + (pid[counter] >> 7)
pidAscii += charMap4[index]
pidAscii += bytes(bytearray([charMap4[index]]))
return pidAscii
def crc32(s):
@ -149,7 +160,7 @@ def checksumPid(s):
for i in (0,1):
b = crc & 0xff
pos = (b // l) ^ (b % l)
res += charMap4[pos%l]
res += bytes(bytearray([charMap4[pos%l]]))
crc >>= 8
return res
@ -159,28 +170,35 @@ def pidFromSerial(s, l):
global charMap4
crc = crc32(s)
arr1 = [0]*l
for i in xrange(len(s)):
arr1[i%l] ^= ord(s[i])
for i in range(len(s)):
if sys.version_info[0] == 2:
arr1[i%l] ^= ord(s[i])
else:
arr1[i%l] ^= s[i]
crc_bytes = [crc >> 24 & 0xff, crc >> 16 & 0xff, crc >> 8 & 0xff, crc & 0xff]
for i in xrange(l):
for i in range(l):
arr1[i] ^= crc_bytes[i&3]
pid = ""
for i in xrange(l):
pid = b""
for i in range(l):
b = arr1[i] & 0xff
pid+=charMap4[(b >> 7) + ((b >> 5 & 3) ^ (b & 0x1f))]
pid += bytes(bytearray([charMap4[(b >> 7) + ((b >> 5 & 3) ^ (b & 0x1f))]]))
return pid
# Parse the EXTH header records and use the Kindle serial number to calculate the book pid.
def getKindlePids(rec209, token, serialnum):
if isinstance(serialnum,str):
serialnum = serialnum.encode('utf-8')
if sys.version_info[0] == 2:
if isinstance(serialnum,unicode):
serialnum = serialnum.encode('utf-8')
if rec209 is None:
return [serialnum]
pids=[]
if isinstance(serialnum,unicode):
serialnum = serialnum.encode('utf-8')
# Compute book PID
pidHash = SHA1(serialnum+rec209+token)
bookPID = encodePID(pidHash)
@ -188,7 +206,7 @@ def getKindlePids(rec209, token, serialnum):
pids.append(bookPID)
# compute fixed pid for old pre 2.5 firmware update pid as well
kindlePID = pidFromSerial(serialnum, 7) + "*"
kindlePID = pidFromSerial(serialnum, 7) + b"*"
kindlePID = checksumPid(kindlePID)
pids.append(kindlePID)
@ -205,52 +223,52 @@ def getK4Pids(rec209, token, kindleDatabase):
try:
# Get the kindle account token, if present
kindleAccountToken = (kindleDatabase[1])['kindle.account.tokens'].decode('hex')
kindleAccountToken = bytearray.fromhex((kindleDatabase[1])['kindle.account.tokens'])
except KeyError:
kindleAccountToken=""
kindleAccountToken = b''
pass
try:
# Get the DSN token, if present
DSN = (kindleDatabase[1])['DSN'].decode('hex')
print u"Got DSN key from database {0}".format(kindleDatabase[0])
DSN = bytearray.fromhex((kindleDatabase[1])['DSN'])
print("Got DSN key from database {0}".format(kindleDatabase[0]))
except KeyError:
# See if we have the info to generate the DSN
try:
# Get the Mazama Random number
MazamaRandomNumber = (kindleDatabase[1])['MazamaRandomNumber'].decode('hex')
#print u"Got MazamaRandomNumber from database {0}".format(kindleDatabase[0])
MazamaRandomNumber = bytearray.fromhex((kindleDatabase[1])['MazamaRandomNumber'])
#print "Got MazamaRandomNumber from database {0}".format(kindleDatabase[0])
try:
# Get the SerialNumber token, if present
IDString = (kindleDatabase[1])['SerialNumber'].decode('hex')
print u"Got SerialNumber from database {0}".format(kindleDatabase[0])
IDString = bytearray.fromhex((kindleDatabase[1])['SerialNumber'])
print("Got SerialNumber from database {0}".format(kindleDatabase[0]))
except KeyError:
# Get the IDString we added
IDString = (kindleDatabase[1])['IDString'].decode('hex')
IDString = bytearray.fromhex((kindleDatabase[1])['IDString'])
try:
# Get the UsernameHash token, if present
encodedUsername = (kindleDatabase[1])['UsernameHash'].decode('hex')
print u"Got UsernameHash from database {0}".format(kindleDatabase[0])
encodedUsername = bytearray.fromhex((kindleDatabase[1])['UsernameHash'])
print("Got UsernameHash from database {0}".format(kindleDatabase[0]))
except KeyError:
# Get the UserName we added
UserName = (kindleDatabase[1])['UserName'].decode('hex')
UserName = bytearray.fromhex((kindleDatabase[1])['UserName'])
# encode it
encodedUsername = encodeHash(UserName,charMap1)
#print u"encodedUsername",encodedUsername.encode('hex')
#print "encodedUsername",encodedUsername.encode('hex')
except KeyError:
print u"Keys not found in the database {0}.".format(kindleDatabase[0])
print("Keys not found in the database {0}.".format(kindleDatabase[0]))
return pids
# Get the ID string used
encodedIDString = encodeHash(IDString,charMap1)
#print u"encodedIDString",encodedIDString.encode('hex')
#print "encodedIDString",encodedIDString.encode('hex')
# concat, hash and encode to calculate the DSN
DSN = encode(SHA1(MazamaRandomNumber+encodedIDString+encodedUsername),charMap1)
#print u"DSN",DSN.encode('hex')
#print "DSN",DSN.encode('hex')
pass
if rec209 is None:
@ -295,16 +313,16 @@ def getPidList(md1, md2, serials=[], kDatabases=[]):
for kDatabase in kDatabases:
try:
pidlst.extend(getK4Pids(md1, md2, kDatabase))
except Exception, e:
print u"Error getting PIDs from database {0}: {1}".format(kDatabase[0],e.args[0])
pidlst.extend(map(bytes,getK4Pids(md1, md2, kDatabase)))
except Exception as e:
print("Error getting PIDs from database {0}: {1}".format(kDatabase[0],e.args[0]))
traceback.print_exc()
for serialnum in serials:
try:
pidlst.extend(getKindlePids(md1, md2, serialnum))
except Exception, e:
print u"Error getting PIDs from serial number {0}: {1}".format(serialnum ,e.args[0])
pidlst.extend(map(bytes,getKindlePids(md1, md2, serialnum)))
except Exception as e:
print("Error getting PIDs from serial number {0}: {1}".format(serialnum ,e.args[0]))
traceback.print_exc()
return pidlst

1049
DeDRM_plugin/kindlekey.py Normal file

File diff suppressed because it is too large Load diff

92
DeDRM_plugin/kindlepid.py Normal file
View file

@ -0,0 +1,92 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Mobipocket PID calculator v0.4 for Amazon Kindle.
# Copyright (c) 2007, 2009 Igor Skochinsky <skochinsky@mail.ru>
# History:
# 0.1 Initial release
# 0.2 Added support for generating PID for iPhone (thanks to mbp)
# 0.3 changed to autoflush stdout, fixed return code usage
# 0.3 updated for unicode
# 0.4 Added support for serial numbers starting with '9', fixed unicode bugs.
# 0.5 moved unicode_argv call inside main for Windows DeDRM compatibility
# 1.0 Python 3 for calibre 5.0
import sys
import binascii
#@@CALIBRE_COMPAT_CODE@@
from .utilities import SafeUnbuffered
from .argv_utils import unicode_argv
letters = b'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789'
def crc32(s):
return (~binascii.crc32(s,-1))&0xFFFFFFFF
def checksumPid(s):
crc = crc32(s)
crc = crc ^ (crc >> 16)
res = s
l = len(letters)
for i in (0,1):
b = crc & 0xff
pos = (b // l) ^ (b % l)
res += bytes(bytearray([letters[pos%l]]))
crc >>= 8
return res
def pidFromSerial(s, l):
crc = crc32(s)
arr1 = [0]*l
for i in range(len(s)):
if sys.version_info[0] == 2:
arr1[i%l] ^= ord(s[i])
else:
arr1[i%l] ^= s[i]
crc_bytes = [crc >> 24 & 0xff, crc >> 16 & 0xff, crc >> 8 & 0xff, crc & 0xff]
for i in range(l):
arr1[i] ^= crc_bytes[i&3]
pid = b""
for i in range(l):
b = arr1[i] & 0xff
pid+=bytes(bytearray([letters[(b >> 7) + ((b >> 5 & 3) ^ (b & 0x1f))]]))
return pid
def cli_main():
print("Mobipocket PID calculator for Amazon Kindle. Copyright © 2007, 2009 Igor Skochinsky")
argv=unicode_argv("kindlepid.py")
if len(argv)==2:
serial = argv[1]
else:
print("Usage: kindlepid.py <Kindle Serial Number>/<iPhone/iPod Touch UDID>")
return 1
if len(serial)==16:
if serial.startswith("B") or serial.startswith("9"):
print("Kindle serial number detected")
else:
print("Warning: unrecognized serial number. Please recheck input.")
return 1
pid = pidFromSerial(serial.encode("utf-8"),7)+'*'
print("Mobipocket PID for Kindle serial#{0} is {1}".format(serial,checksumPid(pid)))
return 0
elif len(serial)==40:
print("iPhone serial number (UDID) detected")
pid = pidFromSerial(serial.encode("utf-8"),8)
print("Mobipocket PID for iPhone serial#{0} is {1}".format(serial,checksumPid(pid)))
return 0
print("Warning: unrecognized serial number. Please recheck input.")
return 1
if __name__ == "__main__":
sys.stdout=SafeUnbuffered(sys.stdout)
sys.stderr=SafeUnbuffered(sys.stderr)
sys.exit(cli_main())

70
DeDRM_plugin/lcpdedrm.py Normal file
View 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")

309
dedrm_src/mobidedrm.py → DeDRM_plugin/mobidedrm.py Normal file → Executable file
View file

@ -1,12 +1,13 @@
#!/usr/bin/env python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# mobidedrm.py
# Copyright © 2008 The Dark Reverser
# Portions © 20082017 Apprentice Harper et al.
# Portions © 20082020 Apprentice Harper et al.
from __future__ import print_function
__license__ = 'GPL v3'
__version__ = u"0.42"
__version__ = "1.1"
# This is a python script. You need a Python interpreter to run it.
# For example, ActiveState Python, which exists for windows.
@ -72,73 +73,21 @@ __version__ = u"0.42"
# 0.40 - moved unicode_argv call inside main for Windows DeDRM compatibility
# 0.41 - Fixed potential unicode problem in command line calls
# 0.42 - Added GPL v3 licence. updated/removed some print statements
# 1.0 - Python 3 compatibility for calibre 5.0
# 1.1 - Speed Python PC1 implementation up a little bit
import sys
import os
import struct
import binascii
try:
from alfcrypto import Pukall_Cipher
except:
print u"AlfCrypto not found. Using python PC1 implementation."
# Wrap a stream so that output gets flushed immediately
# and also make sure that any unicode strings get
# encoded using "replace" before writing them.
class SafeUnbuffered:
def __init__(self, stream):
self.stream = stream
self.encoding = stream.encoding
if self.encoding == None:
self.encoding = "utf-8"
def write(self, data):
if isinstance(data,unicode):
data = data.encode(self.encoding,"replace")
self.stream.write(data)
self.stream.flush()
def __getattr__(self, attr):
return getattr(self.stream, attr)
iswindows = sys.platform.startswith('win')
isosx = sys.platform.startswith('darwin')
def unicode_argv():
if iswindows:
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
# strings.
# Versions 2.x of Python don't support Unicode in sys.argv on
# Windows, with the underlying Windows API instead replacing multi-byte
# characters with '?'.
from ctypes import POINTER, byref, cdll, c_int, windll
from ctypes.wintypes import LPCWSTR, LPWSTR
#@@CALIBRE_COMPAT_CODE@@
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
xrange(start, argc.value)]
# if we don't have any arguments at all, just pass back script name
# this should never happen
return [u"mobidedrm.py"]
else:
argvencoding = sys.stdin.encoding
if argvencoding == None:
argvencoding = 'utf-8'
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
from .alfcrypto import Pukall_Cipher
from .utilities import SafeUnbuffered
from .argv_utils import unicode_argv
class DrmException(Exception):
@ -154,62 +103,42 @@ def PC1(key, src, decryption=True):
# if we can get it from alfcrypto, use that
try:
return Pukall_Cipher().PC1(key,src,decryption)
except NameError:
pass
except TypeError:
pass
except:
raise
# use slow python version, since Pukall_Cipher didn't load
sum1 = 0;
sum2 = 0;
keyXorVal = 0;
if len(key)!=16:
DrmException (u"PC1: Bad key length")
wkey = []
for i in xrange(8):
wkey.append(ord(key[i*2])<<8 | ord(key[i*2+1]))
dst = ""
for i in xrange(len(src)):
temp1 = 0;
byteXorVal = 0;
for j in xrange(8):
temp1 ^= wkey[j]
sum2 = (sum2+j)*20021 + sum1
sum1 = (temp1*346)&0xFFFF
sum2 = (sum2+sum1)&0xFFFF
temp1 = (temp1*20021+1)&0xFFFF
byteXorVal ^= temp1 ^ sum2
curByte = ord(src[i])
if not decryption:
keyXorVal = curByte * 257;
curByte = ((curByte ^ (byteXorVal >> 8)) ^ byteXorVal) & 0xFF
if decryption:
keyXorVal = curByte * 257;
for j in xrange(8):
wkey[j] ^= keyXorVal;
dst+=chr(curByte)
return dst
letters = b'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789'
def crc32(s):
return (~binascii.crc32(s,-1))&0xFFFFFFFF
def checksumPid(s):
letters = 'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789'
crc = (~binascii.crc32(s,-1))&0xFFFFFFFF
s = s.encode()
crc = crc32(s)
crc = crc ^ (crc >> 16)
res = s
l = len(letters)
for i in (0,1):
b = crc & 0xff
pos = (b // l) ^ (b % l)
res += letters[pos%l]
res += bytes(bytearray([letters[pos%l]]))
crc >>= 8
return res
return res.decode()
# expects bytearray
def getSizeOfTrailingDataEntries(ptr, size, flags):
def getSizeOfTrailingDataEntry(ptr, size):
bitpos, result = 0, 0
if size <= 0:
return result
while True:
v = ord(ptr[size-1])
if sys.version_info[0] == 2:
v = ord(ptr[size-1])
else:
v = ptr[size-1]
result |= (v & 0x7F) << bitpos
bitpos += 7
size -= 1
@ -225,7 +154,10 @@ def getSizeOfTrailingDataEntries(ptr, size, flags):
# if multibyte data is included in the encryped data, we'll
# have already cleared this flag.
if flags & 1:
num += (ord(ptr[size - num - 1]) & 0x3) + 1
if sys.version_info[0] == 2:
num += (ord(ptr[size - num - 1]) & 0x3) + 1
else:
num += (ptr[size - num - 1] & 0x3) + 1
return num
@ -244,26 +176,21 @@ class MobiBook:
pass
def __init__(self, infile):
print u"MobiDeDrm v{0:s}.\nCopyright © 2008-2017 The Dark Reverser, Apprentice Harper et al.".format(__version__)
try:
from alfcrypto import Pukall_Cipher
except:
print u"AlfCrypto not found. Using python PC1 implementation."
print("MobiDeDrm v{0:s}.\nCopyright © 2008-2022 The Dark Reverser, Apprentice Harper et al.".format(__version__))
# initial sanity check on file
self.data_file = file(infile, 'rb').read()
self.data_file = open(infile, 'rb').read()
self.mobi_data = ''
self.header = self.data_file[0:78]
if self.header[0x3C:0x3C+8] != 'BOOKMOBI' and self.header[0x3C:0x3C+8] != 'TEXtREAd':
raise DrmException(u"Invalid file format")
if self.header[0x3C:0x3C+8] != b'BOOKMOBI' and self.header[0x3C:0x3C+8] != b'TEXtREAd':
raise DrmException("Invalid file format")
self.magic = self.header[0x3C:0x3C+8]
self.crypto_type = -1
# build up section offset and flag info
self.num_sections, = struct.unpack('>H', self.header[76:78])
self.sections = []
for i in xrange(self.num_sections):
for i in range(self.num_sections):
offset, a1,a2,a3,a4 = struct.unpack('>LBBBB', self.data_file[78+i*8:78+i*8+8])
flags, val = a1, a2<<16|a3<<8|a4
self.sections.append( (offset, flags, val) )
@ -281,17 +208,17 @@ class MobiBook:
self.mobi_codepage = 1252
self.mobi_version = -1
if self.magic == 'TEXtREAd':
print u"PalmDoc format book detected."
if self.magic == b'TEXtREAd':
print("PalmDoc format book detected.")
return
self.mobi_length, = struct.unpack('>L',self.sect[0x14:0x18])
self.mobi_codepage, = struct.unpack('>L',self.sect[0x1c:0x20])
self.mobi_version, = struct.unpack('>L',self.sect[0x68:0x6C])
#print u"MOBI header version {0:d}, header length {1:d}".format(self.mobi_version, self.mobi_length)
#print "MOBI header version {0:d}, header length {1:d}".format(self.mobi_version, self.mobi_length)
if (self.mobi_length >= 0xE4) and (self.mobi_version >= 5):
self.extra_data_flags, = struct.unpack('>H', self.sect[0xF2:0xF4])
#print u"Extra Data Flags: {0:d}".format(self.extra_data_flags)
#print "Extra Data Flags: {0:d}".format(self.extra_data_flags)
if (self.compression != 17480):
# multibyte utf8 data is included in the encryption for PalmDoc compression
# so clear that byte so that we leave it to be decrypted.
@ -300,36 +227,46 @@ class MobiBook:
# if exth region exists parse it for metadata array
try:
exth_flag, = struct.unpack('>L', self.sect[0x80:0x84])
exth = ''
exth = b''
if exth_flag & 0x40:
exth = self.sect[16 + self.mobi_length:]
if (len(exth) >= 12) and (exth[:4] == 'EXTH'):
if (len(exth) >= 12) and (exth[:4] == b'EXTH'):
nitems, = struct.unpack('>I', exth[8:12])
pos = 12
for i in xrange(nitems):
for i in range(nitems):
type, size = struct.unpack('>II', exth[pos: pos + 8])
content = exth[pos + 8: pos + size]
self.meta_array[type] = content
# reset the text to speech flag and clipping limit, if present
if type == 401 and size == 9:
# set clipping limit to 100%
self.patchSection(0, '\144', 16 + self.mobi_length + pos + 8)
self.patchSection(0, b'\144', 16 + self.mobi_length + pos + 8)
elif type == 404 and size == 9:
# make sure text to speech is enabled
self.patchSection(0, '\0', 16 + self.mobi_length + pos + 8)
self.patchSection(0, b'\0', 16 + self.mobi_length + pos + 8)
elif type == 405 and size == 9:
# remove rented book flag
self.patchSection(0, b'\0', 16 + self.mobi_length + pos + 8)
elif type == 406 and size == 16:
# remove rental due date
self.patchSection(0, b'\0'*8, 16 + self.mobi_length + pos + 8)
elif type == 208:
# remove watermark (atv:kin: stuff)
self.patchSection(0, b'\0'*(size-8), 16 + self.mobi_length + pos + 8)
# print type, size, content, content.encode('hex')
pos += size
except:
pass
except Exception as e:
print("Cannot set meta_array: Error: {:s}".format(e.args[0]))
#returns unicode
def getBookTitle(self):
codec_map = {
1252 : 'windows-1252',
65001 : 'utf-8',
}
title = ''
title = b''
codec = 'windows-1252'
if self.magic == 'BOOKMOBI':
if self.magic == b'BOOKMOBI':
if 503 in self.meta_array:
title = self.meta_array[503]
else:
@ -338,29 +275,31 @@ class MobiBook:
title = self.sect[toff:tend]
if self.mobi_codepage in codec_map.keys():
codec = codec_map[self.mobi_codepage]
if title == '':
if title == b'':
title = self.header[:32]
title = title.split('\0')[0]
return unicode(title, codec)
title = title.split(b'\0')[0]
return title.decode(codec)
def getPIDMetaInfo(self):
rec209 = ''
token = ''
rec209 = b''
token = b''
if 209 in self.meta_array:
rec209 = self.meta_array[209]
data = rec209
# The 209 data comes in five byte groups. Interpret the last four bytes
# of each group as a big endian unsigned integer to get a key value
# if that key exists in the meta_array, append its contents to the token
for i in xrange(0,len(data),5):
for i in range(0,len(data),5):
val, = struct.unpack('>I',data[i+1:i+5])
sval = self.meta_array.get(val,'')
sval = self.meta_array.get(val,b'')
token += sval
return rec209, token
# new must be byte array
def patch(self, off, new):
self.data_file = self.data_file[:off] + new + self.data_file[off+len(new):]
# new must be byte array
def patchSection(self, section, new, in_off = 0):
if (section + 1 == self.num_sections):
endoff = len(self.data_file)
@ -370,15 +309,19 @@ class MobiBook:
assert off + in_off + len(new) <= endoff
self.patch(off + in_off, new)
# pids in pidlist must be unicode, returned key is byte array, pid is unicode
def parseDRM(self, data, count, pidlist):
found_key = None
keyvec1 = '\x72\x38\x33\xB0\xB4\xF2\xE3\xCA\xDF\x09\x01\xD6\xE2\xE0\x3F\x96'
keyvec1 = b'\x72\x38\x33\xB0\xB4\xF2\xE3\xCA\xDF\x09\x01\xD6\xE2\xE0\x3F\x96'
for pid in pidlist:
bigpid = pid.ljust(16,'\0')
bigpid = pid.encode('utf-8').ljust(16,b'\0')
temp_key = PC1(keyvec1, bigpid, False)
temp_key_sum = sum(map(ord,temp_key)) & 0xff
if sys.version_info[0] == 2:
temp_key_sum = sum(map(ord,temp_key)) & 0xff
else:
temp_key_sum = sum(temp_key) & 0xff
found_key = None
for i in xrange(count):
for i in range(count):
verification, size, type, cksum, cookie = struct.unpack('>LLLBxxx32s', data[i*0x30:i*0x30+0x30])
if cksum == temp_key_sum:
cookie = PC1(temp_key, cookie)
@ -392,8 +335,12 @@ class MobiBook:
# Then try the default encoding that doesn't require a PID
pid = '00000000'
temp_key = keyvec1
temp_key_sum = sum(map(ord,temp_key)) & 0xff
for i in xrange(count):
if sys.version_info[0] == 2:
temp_key_sum = sum(map(ord,temp_key)) & 0xff
else:
temp_key_sum = sum(temp_key) & 0xff
for i in range(count):
verification, size, type, cksum, cookie = struct.unpack('>LLLBxxx32s', data[i*0x30:i*0x30+0x30])
if cksum == temp_key_sum:
cookie = PC1(temp_key, cookie)
@ -404,56 +351,63 @@ class MobiBook:
return [found_key,pid]
def getFile(self, outpath):
file(outpath,'wb').write(self.mobi_data)
open(outpath,'wb').write(self.mobi_data)
def getBookType(self):
if self.print_replica:
return u"Print Replica"
return "Print Replica"
if self.mobi_version >= 8:
return u"Kindle Format 8"
return "Kindle Format 8"
if self.mobi_version >= 0:
return u"Mobipocket {0:d}".format(self.mobi_version)
return u"PalmDoc"
return "Mobipocket {0:d}".format(self.mobi_version)
return "PalmDoc"
def getBookExtension(self):
if self.print_replica:
return u".azw4"
return ".azw4"
if self.mobi_version >= 8:
return u".azw3"
return u".mobi"
return ".azw3"
return ".mobi"
# pids in pidlist may be unicode or bytearrays or bytes
def processBook(self, pidlist):
crypto_type, = struct.unpack('>H', self.sect[0xC:0xC+2])
print u"Crypto Type is: {0:d}".format(crypto_type)
print("Crypto Type is: {0:d}".format(crypto_type))
self.crypto_type = crypto_type
if crypto_type == 0:
print u"This book is not encrypted."
print("This book is not encrypted.")
# we must still check for Print Replica
self.print_replica = (self.loadSection(1)[0:4] == '%MOP')
self.print_replica = (self.loadSection(1)[0:4] == b'%MOP')
self.mobi_data = self.data_file
return
if crypto_type != 2 and crypto_type != 1:
raise DrmException(u"Cannot decode unknown Mobipocket encryption type {0:d}".format(crypto_type))
raise DrmException("Cannot decode unknown Mobipocket encryption type {0:d}".format(crypto_type))
if 406 in self.meta_array:
data406 = self.meta_array[406]
val406, = struct.unpack('>Q',data406)
if val406 != 0:
raise DrmException(u"Cannot decode library or rented ebooks.")
print("Warning: This is a library or rented ebook ({0}). Continuing ...".format(val406))
#raise DrmException("Cannot decode library or rented ebooks.")
goodpids = []
# print("DEBUG ==== pidlist = ", pidlist)
for pid in pidlist:
if isinstance(pid,(bytearray,bytes)):
pid = pid.decode('utf-8')
if len(pid)==10:
if checksumPid(pid[0:-2]) != pid:
print u"Warning: PID {0} has incorrect checksum, should have been {1}".format(pid,checksumPid(pid[0:-2]))
print("Warning: PID {0} has incorrect checksum, should have been {1}".format(pid,checksumPid(pid[0:-2])))
goodpids.append(pid[0:-2])
elif len(pid)==8:
goodpids.append(pid)
else:
print u"Warning: PID {0} has wrong number of digits".format(pid)
print("Warning: PID {0} has wrong number of digits".format(pid))
# print("======= DEBUG good pids = ", goodpids)
if self.crypto_type == 1:
t1_keyvec = 'QDCVEPMU675RUBSZ'
if self.magic == 'TEXtREAd':
t1_keyvec = b'QDCVEPMU675RUBSZ'
if self.magic == b'TEXtREAd':
bookkey_data = self.sect[0x0E:0x0E+16]
elif self.mobi_version < 0:
bookkey_data = self.sect[0x90:0x90+16]
@ -465,74 +419,75 @@ class MobiBook:
# calculate the keys
drm_ptr, drm_count, drm_size, drm_flags = struct.unpack('>LLLL', self.sect[0xA8:0xA8+16])
if drm_count == 0:
raise DrmException(u"Encryption not initialised. Must be opened with Mobipocket Reader first.")
raise DrmException("Encryption not initialised. Must be opened with Mobipocket Reader first.")
found_key, pid = self.parseDRM(self.sect[drm_ptr:drm_ptr+drm_size], drm_count, goodpids)
if not found_key:
raise DrmException(u"No key found in {0:d} keys tried.".format(len(goodpids)))
raise DrmException("No key found in {0:d} PIDs tried.".format(len(goodpids)))
# kill the drm keys
self.patchSection(0, '\0' * drm_size, drm_ptr)
self.patchSection(0, b'\0' * drm_size, drm_ptr)
# kill the drm pointers
self.patchSection(0, '\xff' * 4 + '\0' * 12, 0xA8)
self.patchSection(0, b'\xff' * 4 + b'\0' * 12, 0xA8)
if pid=='00000000':
print u"File has default encryption, no specific key needed."
print("File has default encryption, no specific key needed.")
else:
print u"File is encoded with PID {0}.".format(checksumPid(pid))
print("File is encoded with PID {0}.".format(checksumPid(pid)))
# clear the crypto type
self.patchSection(0, "\0" * 2, 0xC)
self.patchSection(0, b'\0' * 2, 0xC)
# decrypt sections
print u"Decrypting. Please wait . . .",
print("Decrypting. Please wait . . .", end=' ')
mobidataList = []
mobidataList.append(self.data_file[:self.sections[1][0]])
for i in xrange(1, self.records+1):
for i in range(1, self.records+1):
data = self.loadSection(i)
extra_size = getSizeOfTrailingDataEntries(data, len(data), self.extra_data_flags)
if i%100 == 0:
print u".",
print(".", end=' ')
# print "record %d, extra_size %d" %(i,extra_size)
decoded_data = PC1(found_key, data[0:len(data) - extra_size])
if i==1:
self.print_replica = (decoded_data[0:4] == '%MOP')
self.print_replica = (decoded_data[0:4] == b'%MOP')
mobidataList.append(decoded_data)
if extra_size > 0:
mobidataList.append(data[-extra_size:])
if self.num_sections > self.records+1:
mobidataList.append(self.data_file[self.sections[self.records+1][0]:])
self.mobi_data = "".join(mobidataList)
print u"done"
self.mobi_data = b''.join(mobidataList)
print("done")
return
# pids in pidlist must be unicode
def getUnencryptedBook(infile,pidlist):
if not os.path.isfile(infile):
raise DrmException(u"Input File Not Found.")
raise DrmException("Input File Not Found.")
book = MobiBook(infile)
book.processBook(pidlist)
return book.mobi_data
def cli_main():
argv=unicode_argv()
argv=unicode_argv("mobidedrm.py")
progname = os.path.basename(argv[0])
if len(argv)<3 or len(argv)>4:
print u"MobiDeDrm v{0:s}.\nCopyright © 2008-2017 The Dark Reverser, Apprentice Harper et al.".format(__version__)
print u"Removes protection from Kindle/Mobipocket, Kindle/KF8 and Kindle/Print Replica ebooks"
print u"Usage:"
print u" {0} <infile> <outfile> [<Comma separated list of PIDs to try>]".format(progname)
print("MobiDeDrm v{0:s}.\nCopyright © 2008-2020 The Dark Reverser, Apprentice Harper et al.".format(__version__))
print("Removes protection from Kindle/Mobipocket, Kindle/KF8 and Kindle/Print Replica ebooks")
print("Usage:")
print(" {0} <infile> <outfile> [<Comma separated list of PIDs to try>]".format(progname))
return 1
else:
infile = argv[1]
outfile = argv[2]
if len(argv) is 4:
if len(argv) == 4:
pidlist = argv[3].split(',')
else:
pidlist = []
try:
stripped_file = getUnencryptedBook(infile, pidlist)
file(outfile, 'wb').write(stripped_file)
except DrmException, e:
print u"MobiDeDRM v{0} Error: {1:s}".format(__version__,e.args[0])
open(outfile, 'wb').write(stripped_file)
except DrmException as e:
print("MobiDeDRM v{0} Error: {1:s}".format(__version__,e.args[0]))
return 1
return 0

106
DeDRM_plugin/prefs.py Executable file
View file

@ -0,0 +1,106 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
__license__ = 'GPL v3'
# Standard Python modules.
import os, sys
import traceback
#@@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=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
self.dedrmprefs.defaults['deobfuscate_fonts'] = True
self.dedrmprefs.defaults['remove_watermarks'] = False
self.dedrmprefs.defaults['bandnkeys'] = {}
self.dedrmprefs.defaults['adeptkeys'] = {}
self.dedrmprefs.defaults['ereaderkeys'] = {}
self.dedrmprefs.defaults['kindlekeys'] = {}
self.dedrmprefs.defaults['androidkeys'] = {}
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'] = ""
# initialise
# we must actually set the prefs that are dictionaries and lists
# to empty dictionaries and lists, otherwise we are unable to add to them
# as then it just adds to the (memory only) dedrmprefs.defaults versions!
if self.dedrmprefs['bandnkeys'] == {}:
self.dedrmprefs['bandnkeys'] = {}
if self.dedrmprefs['adeptkeys'] == {}:
self.dedrmprefs['adeptkeys'] = {}
if self.dedrmprefs['ereaderkeys'] == {}:
self.dedrmprefs['ereaderkeys'] = {}
if self.dedrmprefs['kindlekeys'] == {}:
self.dedrmprefs['kindlekeys'] = {}
if self.dedrmprefs['androidkeys'] == {}:
self.dedrmprefs['androidkeys'] = {}
if self.dedrmprefs['pids'] == []:
self.dedrmprefs['pids'] = []
if self.dedrmprefs['serials'] == []:
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:
return self.dedrmprefs[kind]
return self.dedrmprefs
def set(self, kind, value):
self.dedrmprefs[kind] = value
def writeprefs(self,value = True):
self.dedrmprefs['configured'] = value
def addnamedvaluetoprefs(self, prefkind, keyname, keyvalue):
try:
if keyvalue not in self.dedrmprefs[prefkind].values():
# ensure that the keyname is unique
# by adding a number (starting with 2) to the name if it is not
namecount = 1
newname = keyname
while newname in self.dedrmprefs[prefkind]:
namecount += 1
newname = "{0:s}_{1:d}".format(keyname,namecount)
# add to the preferences
self.dedrmprefs[prefkind][newname] = keyvalue
return (True, newname)
except:
traceback.print_exc()
pass
return (False, keyname)
def addvaluetoprefs(self, prefkind, prefsvalue):
# ensure the keyvalue isn't already in the preferences
try:
if prefsvalue not in self.dedrmprefs[prefkind]:
self.dedrmprefs[prefkind].append(prefsvalue)
return True
except:
traceback.print_exc()
return False

View file

@ -1,18 +1,23 @@
#!/usr/bin/env python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
import sys
import os
#@@CALIBRE_COMPAT_CODE@@
import re
import traceback
import ineptepub
import ignobleepub
import epubtest
import zipfix
import ineptpdf
import erdr2pml
import k4mobidedrm
import traceback
def decryptepub(infile, outdir, rscpath):
errlog = ''
@ -23,7 +28,7 @@ def decryptepub(infile, outdir, rscpath):
zippath = os.path.join(bpath,name + '_temp.zip')
rv = zipfix.repairBook(infile, zippath)
if rv != 0:
print "Error while trying to fix epub"
print("Error while trying to fix epub")
return rv
# determine a good name for the output file
@ -43,14 +48,14 @@ def decryptepub(infile, outdir, rscpath):
try:
rv = ineptepub.decryptBook(userkey, zippath, outfile)
if rv == 0:
print "Decrypted Adobe ePub with key file {0}".format(filename)
print("Decrypted Adobe ePub with key file {0}".format(filename))
break
except Exception, e:
except Exception as e:
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)
@ -61,25 +66,25 @@ 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)
print("Decrypted B&N ePub with key file {0}".format(filename))
break
except Exception, e:
except Exception as e:
errlog += traceback.format_exc()
errlog += str(e)
rv = 1
else:
encryption = epubtest.encryption(zippath)
if encryption == "Unencrypted":
print "{0} is not DRMed.".format(name)
print("{0} is not DRMed.".format(name))
rv = 0
else:
print "{0} has an unknown encryption.".format(name)
print("{0} has an unknown encryption.".format(name))
os.remove(zippath)
if rv != 0:
print errlog
print(errlog)
return rv
@ -103,34 +108,35 @@ def decryptpdf(infile, outdir, rscpath):
rv = ineptpdf.decryptBook(userkey, infile, outfile)
if rv == 0:
break
except Exception, e:
except Exception as e:
errlog += traceback.format_exc()
errlog += str(e)
rv = 1
if rv != 0:
print errlog
print(errlog)
return rv
def decryptpdb(infile, outdir, rscpath):
errlog = ''
outname = os.path.splitext(os.path.basename(infile))[0] + ".pmlz"
outpath = os.path.join(outdir, outname)
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:
try:
name, cc8 = i.split(':')
except ValueError:
print ' Error parsing user supplied social drm data.'
print(' Error parsing user supplied social drm data.')
return 1
try:
rv = erdr2pml.decryptBook(infile, outpath, True, erdr2pml.getuser_key(name, cc8))
except Exception, e:
except Exception as e:
errlog += traceback.format_exc()
errlog += str(e)
rv = 1
@ -141,11 +147,12 @@ def decryptpdb(infile, outdir, rscpath):
def decryptk4mobi(infile, outdir, rscpath):
errlog = ''
rv = 1
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 != '':
@ -153,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 != '':
@ -190,7 +197,7 @@ def decryptk4mobi(infile, outdir, rscpath):
androidFiles.append(dpath)
try:
rv = k4mobidedrm.decryptBook(infile, outdir, kDatabaseFiles, androidFiles, serialnums, pidnums)
except Exception, e:
except Exception as e:
errlog += traceback.format_exc()
errlog += str(e)
rv = 1

View file

@ -0,0 +1,290 @@
#!/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
"""
NOTE: This code is not functional (yet). I started working on it a while ago
to make a standalone version of the plugins that could work without Calibre,
too, but for now there's only a rough code structure and no working code yet.
Currently, to use these plugins, you will need to use Calibre. Hopwfully that'll
change in the future.
"""
OPT_SHORT_TO_LONG = [
["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)

View file

@ -0,0 +1,151 @@
#!/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
"""
NOTE: This code is not functional (yet). I started working on it a while ago
to make a standalone version of the plugins that could work without Calibre,
too, but for now there's only a rough code structure and no working code yet.
Currently, to use these plugins, you will need to use Calibre. Hopwfully that'll
change in the future.
"""
#@@CALIBRE_COMPAT_CODE@@
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()

View file

@ -0,0 +1,133 @@
#!/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
"""
NOTE: This code is not functional (yet). I started working on it a while ago
to make a standalone version of the plugins that could work without Calibre,
too, but for now there's only a rough code structure and no working code yet.
Currently, to use these plugins, you will need to use Calibre. Hopwfully that'll
change in the future.
"""
#@@CALIBRE_COMPAT_CODE@@
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)

View file

@ -0,0 +1,220 @@
#!/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
"""
NOTE: This code is not functional (yet). I started working on it a while ago
to make a standalone version of the plugins that could work without Calibre,
too, but for now there's only a rough code structure and no working code yet.
Currently, to use these plugins, you will need to use Calibre. Hopwfully that'll
change in the future.
"""
#@@CALIBRE_COMPAT_CODE@@
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)

View file

@ -2,6 +2,7 @@
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
# For use with Topaz Scripts Version 2.6
import csv
import sys
import os
@ -14,36 +15,36 @@ debug = False
class DocParser(object):
def __init__(self, flatxml, fontsize, ph, pw):
self.flatdoc = flatxml.split('\n')
self.flatdoc = flatxml.split(b'\n')
self.fontsize = int(fontsize)
self.ph = int(ph) * 1.0
self.pw = int(pw) * 1.0
stags = {
'paragraph' : 'p',
'graphic' : '.graphic'
b'paragraph' : 'p',
b'graphic' : '.graphic'
}
attr_val_map = {
'hang' : 'text-indent: ',
'indent' : 'text-indent: ',
'line-space' : 'line-height: ',
'margin-bottom' : 'margin-bottom: ',
'margin-left' : 'margin-left: ',
'margin-right' : 'margin-right: ',
'margin-top' : 'margin-top: ',
'space-after' : 'padding-bottom: ',
b'hang' : 'text-indent: ',
b'indent' : 'text-indent: ',
b'line-space' : 'line-height: ',
b'margin-bottom' : 'margin-bottom: ',
b'margin-left' : 'margin-left: ',
b'margin-right' : 'margin-right: ',
b'margin-top' : 'margin-top: ',
b'space-after' : 'padding-bottom: ',
}
attr_str_map = {
'align-center' : 'text-align: center; margin-left: auto; margin-right: auto;',
'align-left' : 'text-align: left;',
'align-right' : 'text-align: right;',
'align-justify' : 'text-align: justify;',
'display-inline' : 'display: inline;',
'pos-left' : 'text-align: left;',
'pos-right' : 'text-align: right;',
'pos-center' : 'text-align: center; margin-left: auto; margin-right: auto;',
b'align-center' : 'text-align: center; margin-left: auto; margin-right: auto;',
b'align-left' : 'text-align: left;',
b'align-right' : 'text-align: right;',
b'align-justify' : 'text-align: justify;',
b'display-inline' : 'display: inline;',
b'pos-left' : 'text-align: left;',
b'pos-right' : 'text-align: right;',
b'pos-center' : 'text-align: center; margin-left: auto; margin-right: auto;',
}
@ -57,13 +58,15 @@ class DocParser(object):
else:
end = min(cnt,end)
foundat = -1
for j in xrange(pos, end):
for j in range(pos, end):
item = docList[j]
if item.find('=') >= 0:
(name, argres) = item.split('=',1)
if item.find(b'=') >= 0:
(name, argres) = item.split(b'=',1)
else :
name = item
argres = ''
argres = b''
if (isinstance(tagpath,str)):
tagpath = tagpath.encode('utf-8')
if name.endswith(tagpath) :
result = argres
foundat = j
@ -75,7 +78,7 @@ class DocParser(object):
def posinDoc(self, tagpath):
startpos = []
pos = 0
res = ""
res = b""
while res != None :
(foundpos, res) = self.findinDoc(tagpath, pos, -1)
if res != None :
@ -86,11 +89,11 @@ class DocParser(object):
# returns a vector of integers for the tagpath
def getData(self, tagpath, pos, end, clean=False):
if clean:
digits_only = re.compile(r'''([0-9]+)''')
digits_only = re.compile(rb'''([0-9]+)''')
argres=[]
(foundat, argt) = self.findinDoc(tagpath, pos, end)
if (argt != None) and (len(argt) > 0) :
argList = argt.split('|')
argList = argt.split(b'|')
for strval in argList:
if clean:
m = re.search(digits_only, strval)
@ -108,81 +111,84 @@ class DocParser(object):
csspage += '.cl-justify { text-align: justify; }\n'
# generate a list of each <style> starting point in the stylesheet
styleList= self.posinDoc('book.stylesheet.style')
styleList= self.posinDoc(b'book.stylesheet.style')
stylecnt = len(styleList)
styleList.append(-1)
# process each style converting what you can
if debug: print ' ', 'Processing styles.'
for j in xrange(stylecnt):
if debug: print ' ', 'Processing style %d' %(j)
if debug: print(' ', 'Processing styles.')
for j in range(stylecnt):
if debug: print(' ', 'Processing style %d' %(j))
start = styleList[j]
end = styleList[j+1]
(pos, tag) = self.findinDoc('style._tag',start,end)
(pos, tag) = self.findinDoc(b'style._tag',start,end)
if tag == None :
(pos, tag) = self.findinDoc('style.type',start,end)
(pos, tag) = self.findinDoc(b'style.type',start,end)
# Is this something we know how to convert to css
if tag in self.stags :
# get the style class
(pos, sclass) = self.findinDoc('style.class',start,end)
(pos, sclass) = self.findinDoc(b'style.class',start,end)
if sclass != None:
sclass = sclass.replace(' ','-')
sclass = '.cl-' + sclass.lower()
sclass = sclass.replace(b' ',b'-')
sclass = b'.cl-' + sclass.lower()
else :
sclass = ''
sclass = b''
if debug: print 'sclass', sclass
if debug: print('sclass', sclass)
# check for any "after class" specifiers
(pos, aftclass) = self.findinDoc('style._after_class',start,end)
(pos, aftclass) = self.findinDoc(b'style._after_class',start,end)
if aftclass != None:
aftclass = aftclass.replace(' ','-')
aftclass = '.cl-' + aftclass.lower()
aftclass = aftclass.replace(b' ',b'-')
aftclass = b'.cl-' + aftclass.lower()
else :
aftclass = ''
aftclass = b''
if debug: print 'aftclass', aftclass
if debug: print('aftclass', aftclass)
cssargs = {}
while True :
(pos1, attr) = self.findinDoc('style.rule.attr', start, end)
(pos2, val) = self.findinDoc('style.rule.value', start, end)
(pos1, attr) = self.findinDoc(b'style.rule.attr', start, end)
(pos2, val) = self.findinDoc(b'style.rule.value', start, end)
if debug: print 'attr', attr
if debug: print 'val', val
if debug: print('attr', attr)
if debug: print('val', val)
if attr == None : break
if (attr == 'display') or (attr == 'pos') or (attr == 'align'):
if (attr == b'display') or (attr == b'pos') or (attr == b'align'):
# handle text based attributess
attr = attr + '-' + val
attr = attr + b'-' + val
if attr in self.attr_str_map :
cssargs[attr] = (self.attr_str_map[attr], '')
cssargs[attr] = (self.attr_str_map[attr], b'')
else :
# handle value based attributes
if attr in self.attr_val_map :
name = self.attr_val_map[attr]
if attr in ('margin-bottom', 'margin-top', 'space-after') :
if attr in (b'margin-bottom', b'margin-top', b'space-after') :
scale = self.ph
elif attr in ('margin-right', 'indent', 'margin-left', 'hang') :
elif attr in (b'margin-right', b'indent', b'margin-left', b'hang') :
scale = self.pw
elif attr == 'line-space':
elif attr == b'line-space':
scale = self.fontsize * 2.0
else:
print("Scale not defined!")
scale = 1.0
if val == "":
if not val:
val = 0
if not ((attr == 'hang') and (int(val) == 0)):
if not ((attr == b'hang') and (int(val) == 0)):
try:
f = float(val)
except:
print "Warning: unrecognised val, ignoring"
print("Warning: unrecognised val, ignoring")
val = 0
pv = float(val)/scale
cssargs[attr] = (self.attr_val_map[attr], pv)
@ -194,35 +200,35 @@ class DocParser(object):
if aftclass != "" : keep = False
if keep :
if debug: print 'keeping style'
if debug: print('keeping style')
# make sure line-space does not go below 100% or above 300% since
# it can be wacky in some styles
if 'line-space' in cssargs:
seg = cssargs['line-space'][0]
val = cssargs['line-space'][1]
if b'line-space' in cssargs:
seg = cssargs[b'line-space'][0]
val = cssargs[b'line-space'][1]
if val < 1.0: val = 1.0
if val > 3.0: val = 3.0
del cssargs['line-space']
cssargs['line-space'] = (self.attr_val_map['line-space'], val)
del cssargs[b'line-space']
cssargs[b'line-space'] = (self.attr_val_map[b'line-space'], val)
# handle modifications for css style hanging indents
if 'hang' in cssargs:
hseg = cssargs['hang'][0]
hval = cssargs['hang'][1]
del cssargs['hang']
cssargs['hang'] = (self.attr_val_map['hang'], -hval)
if b'hang' in cssargs:
hseg = cssargs[b'hang'][0]
hval = cssargs[b'hang'][1]
del cssargs[b'hang']
cssargs[b'hang'] = (self.attr_val_map[b'hang'], -hval)
mval = 0
mseg = 'margin-left: '
mval = hval
if 'margin-left' in cssargs:
mseg = cssargs['margin-left'][0]
mval = cssargs['margin-left'][1]
if b'margin-left' in cssargs:
mseg = cssargs[b'margin-left'][0]
mval = cssargs[b'margin-left'][1]
if mval < 0: mval = 0
mval = hval + mval
cssargs['margin-left'] = (mseg, mval)
if 'indent' in cssargs:
del cssargs['indent']
cssargs[b'margin-left'] = (mseg, mval)
if b'indent' in cssargs:
del cssargs[b'indent']
cssline = sclass + ' { '
for key in iter(cssargs):
@ -266,15 +272,15 @@ class DocParser(object):
def convert2CSS(flatxml, fontsize, ph, pw):
print ' ', 'Using font size:',fontsize
print ' ', 'Using page height:', ph
print ' ', 'Using page width:', pw
print(' ', 'Using font size:',fontsize)
print(' ', 'Using page height:', ph)
print(' ', 'Using page width:', pw)
# create a document parser
dp = DocParser(flatxml, fontsize, ph, pw)
if debug: print ' ', 'Created DocParser.'
if debug: print(' ', 'Created DocParser.')
csspage = dp.process()
if debug: print ' ', 'Processed DocParser.'
if debug: print(' ', 'Processed DocParser.')
return csspage

View file

@ -1,87 +1,39 @@
#!/usr/bin/env python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from __future__ import print_function
# topazextract.py
# Mostly written by some_updates based on code from many others
# Changelog
# 4.9 - moved unicode_argv call inside main for Windows DeDRM compatibility
# 5.0 - Fixed potential unicode problem with command line interface
# 6.0 - Added Python 3 compatibility for calibre 5.0
__version__ = '5.0'
__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
from alfcrypto import Topaz_Cipher
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,unicode):
data = data.encode(self.encoding,"replace")
self.stream.write(data)
self.stream.flush()
def __getattr__(self, attr):
return getattr(self.stream, attr)
from .alfcrypto import Topaz_Cipher
from .utilities import SafeUnbuffered
iswindows = sys.platform.startswith('win')
isosx = sys.platform.startswith('darwin')
from .argv_utils import unicode_argv
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
xrange(start, argc.value)]
# if we don't have any arguments at all, just pass back script name
# this should never happen
return [u"mobidedrm.py"]
else:
argvencoding = sys.stdin.encoding
if argvencoding == None:
argvencoding = 'utf-8'
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.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):
@ -91,7 +43,7 @@ class DrmException(Exception):
# recursive zip creation support routine
def zipUpDir(myzip, tdir, localname):
currentdir = tdir
if localname != u"":
if localname != "":
currentdir = os.path.join(currentdir,localname)
list = os.listdir(currentdir)
for file in list:
@ -166,22 +118,24 @@ 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] != 'PID' or fields[5] != 'pid' :
raise DrmException(u"Didn't find PID magic numbers in record")
if fields[0] != b'PID' or fields[5] != b'pid' :
raise DrmException("Didn't find PID magic numbers in record")
elif fields[1] != 8 or fields[3] != 8 :
raise DrmException(u"Record didn't contain correct length fields")
raise DrmException("Record didn't contain correct length fields")
elif fields[2] != PID :
raise DrmException(u"Record didn't contain PID")
raise DrmException("Record didn't contain PID")
return fields[4]
# Decrypt all dkey records (contain the book PID)
def decryptDkeyRecords(data,PID):
nbKeyRecords = ord(data[0])
nbKeyRecords = data[0]
records = []
data = data[1:]
for i in range (0,nbKeyRecords):
length = ord(data[0])
length = data[0]
try:
key = decryptDkeyRecord(data[1:length+1],PID)
records.append(key)
@ -189,13 +143,13 @@ def decryptDkeyRecords(data,PID):
pass
data = data[1+length:]
if len(records) == 0:
raise DrmException(u"BookKey Not Found")
raise DrmException("BookKey Not Found")
return records
class TopazBook:
def __init__(self, filename):
self.fo = file(filename, 'rb')
self.fo = open(filename, 'rb')
self.outdir = tempfile.mkdtemp()
# self.outdir = 'rawdat'
self.bookPayloadOffset = 0
@ -203,8 +157,8 @@ class TopazBook:
self.bookMetadata = {}
self.bookKey = None
magic = unpack('4s',self.fo.read(4))[0]
if magic != 'TPZ0':
raise DrmException(u"Parse Error : Invalid Header, not a Topaz file")
if magic != b'TPZ0':
raise DrmException("Parse Error : Invalid Header, not a Topaz file")
self.parseTopazHeaders()
self.parseMetadata()
@ -213,7 +167,7 @@ class TopazBook:
# Read and return the data of one header record at the current book file position
# [[offset,decompressedLength,compressedLength],...]
nbValues = bookReadEncodedNumber(self.fo)
if debug: print "%d records in header " % nbValues,
if debug: print("%d records in header " % nbValues, end=' ')
values = []
for i in range (0,nbValues):
values.append([bookReadEncodedNumber(self.fo),bookReadEncodedNumber(self.fo),bookReadEncodedNumber(self.fo)])
@ -222,50 +176,50 @@ class TopazBook:
# Read and parse one header record at the current book file position and return the associated data
# [[offset,decompressedLength,compressedLength],...]
if ord(self.fo.read(1)) != 0x63:
raise DrmException(u"Parse Error : Invalid Header")
raise DrmException("Parse Error : Invalid Header")
tag = bookReadString(self.fo)
record = bookReadHeaderRecordData()
return [tag,record]
nbRecords = bookReadEncodedNumber(self.fo)
if debug: print "Headers: %d" % nbRecords
if debug: print("Headers: %d" % nbRecords)
for i in range (0,nbRecords):
result = parseTopazHeaderRecord()
if debug: print result[0], ": ", result[1]
if debug: print(result[0], ": ", result[1])
self.bookHeaderRecords[result[0]] = result[1]
if ord(self.fo.read(1)) != 0x64 :
raise DrmException(u"Parse Error : Invalid Header")
raise DrmException("Parse Error : Invalid Header")
self.bookPayloadOffset = self.fo.tell()
def parseMetadata(self):
# Parse the metadata record from the book payload and return a list of [key,values]
self.fo.seek(self.bookPayloadOffset + self.bookHeaderRecords['metadata'][0][0])
self.fo.seek(self.bookPayloadOffset + self.bookHeaderRecords[b'metadata'][0][0])
tag = bookReadString(self.fo)
if tag != 'metadata' :
raise DrmException(u"Parse Error : Record Names Don't Match")
if tag != b'metadata' :
raise DrmException("Parse Error : Record Names Don't Match")
flags = ord(self.fo.read(1))
nbRecords = ord(self.fo.read(1))
if debug: print "Metadata Records: %d" % nbRecords
if debug: print("Metadata Records: %d" % nbRecords)
for i in range (0,nbRecords) :
keyval = bookReadString(self.fo)
content = bookReadString(self.fo)
if debug: print keyval
if debug: print content
if debug: print(keyval)
if debug: print(content)
self.bookMetadata[keyval] = content
return self.bookMetadata
def getPIDMetaInfo(self):
keysRecord = self.bookMetadata.get('keys','')
keysRecordRecord = ''
if keysRecord != '':
keylst = keysRecord.split(',')
keysRecord = self.bookMetadata.get(b'keys',b'')
keysRecordRecord = b''
if keysRecord != b'':
keylst = keysRecord.split(b',')
for keyval in keylst:
keysRecordRecord += self.bookMetadata.get(keyval,'')
keysRecordRecord += self.bookMetadata.get(keyval,b'')
return keysRecord, keysRecordRecord
def getBookTitle(self):
title = ''
if 'Title' in self.bookMetadata:
title = self.bookMetadata['Title']
title = b''
if b'Title' in self.bookMetadata:
title = self.bookMetadata[b'Title']
return title.decode('utf-8')
def setBookKey(self, key):
@ -309,6 +263,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
@ -317,21 +273,18 @@ class TopazBook:
raw = 0
fixedimage=True
try:
keydata = self.getBookPayloadRecord('dkey', 0)
except DrmException, e:
print u"no dkey record found, book may not be encrypted"
print u"attempting to extrct files without a book key"
keydata = self.getBookPayloadRecord(b'dkey', 0)
except DrmException as e:
print("no dkey record found, book may not be encrypted")
print("attempting to extract files without a book key")
self.createBookDirectory()
self.extractFiles()
print u"Successfully Extracted Topaz contents"
if inCalibre:
from calibre_plugins.dedrm import genbook
else:
import genbook
print("Successfully Extracted Topaz contents")
import genbook
rv = genbook.generateBook(self.outdir, raw, fixedimage)
if rv == 0:
print u"Book Successfully generated."
print("Book Successfully generated.")
return rv
# try each pid to decode the file
@ -339,33 +292,32 @@ class TopazBook:
for pid in pidlst:
# use 8 digit pids here
pid = pid[0:8]
print u"Trying: {0}".format(pid)
if isinstance(pid, str):
pid = pid.encode('latin-1')
print("Trying: {0}".format(pid))
bookKeys = []
data = keydata
try:
bookKeys+=decryptDkeyRecords(data,pid)
except DrmException, e:
except DrmException as e:
pass
else:
bookKey = bookKeys[0]
print u"Book Key Found! ({0})".format(bookKey.encode('hex'))
print("Book Key Found! ({0})".format(bookKey.hex()))
break
if not bookKey:
raise DrmException(u"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 u"Successfully Extracted Topaz contents"
if inCalibre:
from calibre_plugins.dedrm import genbook
else:
import genbook
self.extractFiles()
print("Successfully Extracted Topaz contents")
import genbook
rv = genbook.generateBook(self.outdir, raw, fixedimage)
if rv == 0:
print u"Book Successfully generated"
print("Book Successfully generated")
return rv
def createBookDirectory(self):
@ -373,16 +325,16 @@ class TopazBook:
# create output directory structure
if not os.path.exists(outdir):
os.makedirs(outdir)
destdir = os.path.join(outdir,u"img")
destdir = os.path.join(outdir,"img")
if not os.path.exists(destdir):
os.makedirs(destdir)
destdir = os.path.join(outdir,u"color_img")
destdir = os.path.join(outdir,"color_img")
if not os.path.exists(destdir):
os.makedirs(destdir)
destdir = os.path.join(outdir,u"page")
destdir = os.path.join(outdir,"page")
if not os.path.exists(destdir):
os.makedirs(destdir)
destdir = os.path.join(outdir,u"glyphs")
destdir = os.path.join(outdir,"glyphs")
if not os.path.exists(destdir):
os.makedirs(destdir)
@ -390,50 +342,52 @@ class TopazBook:
outdir = self.outdir
for headerRecord in self.bookHeaderRecords:
name = headerRecord
if name != 'dkey':
ext = u".dat"
if name == 'img': ext = u".jpg"
if name == 'color' : ext = u".jpg"
print u"Processing Section: {0}\n. . .".format(name),
if name != b'dkey':
ext = ".dat"
if name == b'img': ext = ".jpg"
if name == b'color' : ext = ".jpg"
print("Processing Section: {0}\n. . .".format(name.decode('utf-8')), end=' ')
for index in range (0,len(self.bookHeaderRecords[name])) :
fname = u"{0}{1:04d}{2}".format(name,index,ext)
fname = "{0}{1:04d}{2}".format(name.decode('utf-8'),index,ext)
destdir = outdir
if name == 'img':
destdir = os.path.join(outdir,u"img")
if name == 'color':
destdir = os.path.join(outdir,u"color_img")
if name == 'page':
destdir = os.path.join(outdir,u"page")
if name == 'glyphs':
destdir = os.path.join(outdir,u"glyphs")
if name == b'img':
destdir = os.path.join(outdir,"img")
if name == b'color':
destdir = os.path.join(outdir,"color_img")
if name == b'page':
destdir = os.path.join(outdir,"page")
if name == b'glyphs':
destdir = os.path.join(outdir,"glyphs")
outputFile = os.path.join(destdir,fname)
print u".",
print(".", end=' ')
record = self.getBookPayloadRecord(name,index)
if record != '':
file(outputFile, 'wb').write(record)
print u" "
if isinstance(record, str):
record=bytes(record, 'latin-1')
if record != b'':
open(outputFile, 'wb').write(record)
print(" ")
def getFile(self, zipname):
htmlzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
htmlzip.write(os.path.join(self.outdir,u"book.html"),u"book.html")
htmlzip.write(os.path.join(self.outdir,u"book.opf"),u"book.opf")
if os.path.isfile(os.path.join(self.outdir,u"cover.jpg")):
htmlzip.write(os.path.join(self.outdir,u"cover.jpg"),u"cover.jpg")
htmlzip.write(os.path.join(self.outdir,u"style.css"),u"style.css")
zipUpDir(htmlzip, self.outdir, u"img")
htmlzip.write(os.path.join(self.outdir,"book.html"),"book.html")
htmlzip.write(os.path.join(self.outdir,"book.opf"),"book.opf")
if os.path.isfile(os.path.join(self.outdir,"cover.jpg")):
htmlzip.write(os.path.join(self.outdir,"cover.jpg"),"cover.jpg")
htmlzip.write(os.path.join(self.outdir,"style.css"),"style.css")
zipUpDir(htmlzip, self.outdir, "img")
htmlzip.close()
def getBookType(self):
return u"Topaz"
return "Topaz"
def getBookExtension(self):
return u".htmlz"
return ".htmlz"
def getSVGZip(self, zipname):
svgzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
svgzip.write(os.path.join(self.outdir,u"index_svg.xhtml"),u"index_svg.xhtml")
zipUpDir(svgzip, self.outdir, u"svg")
zipUpDir(svgzip, self.outdir, u"img")
svgzip.write(os.path.join(self.outdir,"index_svg.xhtml"),"index_svg.xhtml")
zipUpDir(svgzip, self.outdir, "svg")
zipUpDir(svgzip, self.outdir, "img")
svgzip.close()
def cleanup(self):
@ -441,20 +395,20 @@ class TopazBook:
shutil.rmtree(self.outdir, True)
def usage(progname):
print u"Removes DRM protection from Topaz ebooks and extracts the contents"
print u"Usage:"
print u" {0} [-k <kindle.k4i>] [-p <comma separated PIDs>] [-s <comma separated Kindle serial numbers>] <infile> <outdir>".format(progname)
print("Removes DRM protection from Topaz ebooks and extracts the contents")
print("Usage:")
print(" {0} [-k <kindle.k4i>] [-p <comma separated PIDs>] [-s <comma separated Kindle serial numbers>] <infile> <outdir>".format(progname))
# Main
def cli_main():
argv=unicode_argv()
argv=unicode_argv("topazextract.py")
progname = os.path.basename(argv[0])
print u"TopazExtract v{0}.".format(__version__)
print("TopazExtract v{0}.".format(__version__))
try:
opts, args = getopt.getopt(argv[1:], "k:p:s:x")
except getopt.GetoptError, err:
print u"Error in options or arguments: {0}".format(err.args[0])
except getopt.GetoptError as err:
print("Error in options or arguments: {0}".format(err.args[0]))
usage(progname)
return 1
if len(args)<2:
@ -464,11 +418,11 @@ def cli_main():
infile = args[0]
outdir = args[1]
if not os.path.isfile(infile):
print u"Input File {0} Does Not Exist.".format(infile)
print("Input File {0} Does Not Exist.".format(infile))
return 1
if not os.path.exists(outdir):
print u"Output Directory {0} Does Not Exist.".format(outdir)
print("Output Directory {0} Does Not Exist.".format(outdir))
return 1
kDatabaseFiles = []
@ -493,27 +447,27 @@ def cli_main():
tb = TopazBook(infile)
title = tb.getBookTitle()
print u"Processing Book: {0}".format(title)
print("Processing Book: {0}".format(title))
md1, md2 = tb.getPIDMetaInfo()
pids.extend(kgenpids.getPidList(md1, md2, serials, kDatabaseFiles))
try:
print u"Decrypting Book"
print("Decrypting Book")
tb.processBook(pids)
print u" Creating HTML ZIP Archive"
zipname = os.path.join(outdir, bookname + u"_nodrm.htmlz")
print(" Creating HTML ZIP Archive")
zipname = os.path.join(outdir, bookname + "_nodrm.htmlz")
tb.getFile(zipname)
print u" Creating SVG ZIP Archive"
zipname = os.path.join(outdir, bookname + u"_SVG.zip")
print(" Creating SVG ZIP Archive")
zipname = os.path.join(outdir, bookname + "_SVG.zip")
tb.getSVGZip(zipname)
# removing internal temporary directory of pieces
tb.cleanup()
except DrmException, e:
print u"Decryption failed\n{0}".format(traceback.format_exc())
except DrmException as e:
print("Decryption failed\n{0}".format(traceback.format_exc()))
try:
tb.cleanup()
@ -521,8 +475,8 @@ def cli_main():
pass
return 1
except Exception, e:
print u"Decryption failed\m{0}".format(traceback.format_exc())
except Exception as e:
print("Decryption failed\n{0}".format(traceback.format_exc()))
try:
tb.cleanup()
except:

49
DeDRM_plugin/utilities.py Normal file
View file

@ -0,0 +1,49 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#@@CALIBRE_COMPAT_CODE@@
import sys
__license__ = 'GPL v3'
def uStrCmp (s1, s2, caseless=False):
import unicodedata as ud
if sys.version_info[0] == 2:
str1 = s1 if isinstance(s1, unicode) else unicode(s1)
str2 = s2 if isinstance(s2, unicode) else unicode(s2)
else:
str1 = s1 if isinstance(s1, str) else str(s1)
str2 = s2 if isinstance(s2, str) else str(s2)
if caseless:
return ud.normalize('NFC', str1.lower()) == ud.normalize('NFC', str2.lower())
else:
return ud.normalize('NFC', str1) == ud.normalize('NFC', str2)
# Wrap a stream so that output gets flushed immediately
# and also make sure that any unicode strings get safely
# encoded using "replace" before writing them.
class SafeUnbuffered:
def __init__(self, stream):
self.stream = stream
self.encoding = stream.encoding
if self.encoding == None:
self.encoding = "utf-8"
def write(self, data):
if isinstance(data,str) or isinstance(data,unicode):
# str for Python3, unicode for Python2
data = data.encode(self.encoding,"replace")
try:
buffer = getattr(self.stream, 'buffer', self.stream)
# self.stream.buffer for Python3, self.stream for Python2
buffer.write(data)
buffer.flush()
except:
# We can do nothing if a write fails
raise
def __getattr__(self, attr):
return getattr(self.stream, attr)

117
DeDRM_plugin/wineutils.py Normal file
View file

@ -0,0 +1,117 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
__license__ = 'GPL v3'
# Standard Python modules.
import os, sys, re, hashlib, traceback
#@@CALIBRE_COMPAT_CODE@@
from __init__ import PLUGIN_NAME, PLUGIN_VERSION
class NoWinePython3Exception(Exception):
pass
class WinePythonCLI:
py3_test = "import sys; sys.exit(0 if (sys.version_info.major==3) else 1)"
def __init__(self, wineprefix=""):
import subprocess
if wineprefix != "":
wineprefix = os.path.abspath(os.path.expanduser(os.path.expandvars(wineprefix)))
if wineprefix != "" and os.path.exists(wineprefix):
self.wineprefix = wineprefix
else:
self.wineprefix = None
candidate_execs = [
["wine", "py.exe", "-3"],
["wine", "python3.exe"],
["wine", "python.exe"],
["wine", "C:\\Python27\\python.exe"], # Should likely be removed
]
for e in candidate_execs:
self.python_exec = e
try:
self.check_call(["-c", self.py3_test])
print("{0} v{1}: Python3 exec found as {2}".format(
PLUGIN_NAME, PLUGIN_VERSION, " ".join(self.python_exec)
))
return None
except subprocess.CalledProcessError as e:
if e.returncode == 1:
print("{0} v{1}: {2} is not python3".format(
PLUGIN_NAME, PLUGIN_VERSION, " ".join(self.python_exec)
))
elif e.returncode == 53:
print("{0} v{1}: {2} does not exist".format(
PLUGIN_NAME, PLUGIN_VERSION, " ".join(self.python_exec)
))
raise NoWinePython3Exception("Could not find python3 executable on specified wine prefix")
def check_call(self, cli_args):
import subprocess
env_dict = os.environ
env_dict["PYTHONPATH"] = ""
if self.wineprefix is not None:
env_dict["WINEPREFIX"] = self.wineprefix
subprocess.check_call(self.python_exec + cli_args, env=env_dict,
stdin=None, stdout=sys.stdout,
stderr=subprocess.STDOUT, close_fds=False,
bufsize=1)
def WineGetKeys(scriptpath, extension, wineprefix=""):
if extension == ".k4i":
import json
try:
pyexec = WinePythonCLI(wineprefix)
except NoWinePython3Exception:
print('{0} v{1}: Unable to find python3 executable in WINEPREFIX="{2}"'.format(PLUGIN_NAME, PLUGIN_VERSION, wineprefix))
return [], []
basepath, script = os.path.split(scriptpath)
print("{0} v{1}: Running {2} under Wine".format(PLUGIN_NAME, PLUGIN_VERSION, script))
outdirpath = os.path.join(basepath, "winekeysdir")
if not os.path.exists(outdirpath):
os.makedirs(outdirpath)
if wineprefix != "":
wineprefix = os.path.abspath(os.path.expanduser(os.path.expandvars(wineprefix)))
try:
result = pyexec.check_call([scriptpath, outdirpath])
except Exception as e:
print("{0} v{1}: Wine subprocess call error: {2}".format(PLUGIN_NAME, PLUGIN_VERSION, e.args[0]))
# try finding winekeys anyway, even if above code errored
winekeys = []
winekey_names = []
# get any files with extension in the output dir
files = [f for f in os.listdir(outdirpath) if f.endswith(extension)]
for filename in files:
try:
fpath = os.path.join(outdirpath, filename)
with open(fpath, 'rb') as keyfile:
if extension == ".k4i":
new_key_value = json.loads(keyfile.read())
else:
new_key_value = keyfile.read()
winekeys.append(new_key_value)
winekey_names.append(filename)
except:
print("{0} v{1}: Error loading file {2}".format(PLUGIN_NAME, PLUGIN_VERSION, filename))
traceback.print_exc()
os.remove(fpath)
print("{0} v{1}: Found and decrypted {2} {3}".format(PLUGIN_NAME, PLUGIN_VERSION, len(winekeys), "key file" if len(winekeys) == 1 else "key files"))
return winekeys, winekey_names

View file

@ -0,0 +1,30 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Python 3's "zipfile" has an annoying bug where the `external_attr` field
of a ZIP file cannot be set to 0. However, if the original DRMed ZIP has
that set to 0 then we want the DRM-free ZIP to have that as 0, too.
See https://github.com/python/cpython/issues/87713
We cannot just set the "external_attr" to 0 as the code to save the ZIP
resets that variable.
So, here's a class that inherits from ZipInfo and ensures that EVERY
read access to that variable will return a 0 ...
"""
import zipfile
class ZeroedZipInfo(zipfile.ZipInfo):
def __init__(self, zinfo):
for k in self.__slots__:
if hasattr(zinfo, k):
setattr(self, k, getattr(zinfo, k))
def __getattribute__(self, name):
if name == "external_attr":
return 0
return object.__getattribute__(self, name)

View file

@ -1,11 +1,16 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Read and write ZIP files.
"""
import struct, os, time, sys, shutil
import binascii, cStringIO, stat
import binascii, stat
import io
import re
from io import BytesIO
try:
import zlib # We may need its compression method
crc32 = zlib.crc32
@ -45,8 +50,8 @@ ZIP_DEFLATED = 8
# The "end of central directory" structure, magic number, size, and indices
# (section V.I in the format document)
structEndArchive = "<4s4H2LH"
stringEndArchive = "PK\005\006"
structEndArchive = b"<4s4H2LH"
stringEndArchive = b"PK\005\006"
sizeEndCentDir = struct.calcsize(structEndArchive)
_ECD_SIGNATURE = 0
@ -64,8 +69,8 @@ _ECD_LOCATION = 9
# The "central directory" structure, magic number, size, and indices
# of entries in the structure (section V.F in the format document)
structCentralDir = "<4s4B4HL2L5H2L"
stringCentralDir = "PK\001\002"
structCentralDir = b"<4s4B4HL2L5H2L"
stringCentralDir = b"PK\001\002"
sizeCentralDir = struct.calcsize(structCentralDir)
# indexes of entries in the central directory structure
@ -91,8 +96,8 @@ _CD_LOCAL_HEADER_OFFSET = 18
# The "local file header" structure, magic number, size, and indices
# (section V.A in the format document)
structFileHeader = "<4s2B4HL2L2H"
stringFileHeader = "PK\003\004"
structFileHeader = b"<4s2B4HL2L2H"
stringFileHeader = b"PK\003\004"
sizeFileHeader = struct.calcsize(structFileHeader)
_FH_SIGNATURE = 0
@ -109,14 +114,14 @@ _FH_FILENAME_LENGTH = 10
_FH_EXTRA_FIELD_LENGTH = 11
# The "Zip64 end of central directory locator" structure, magic number, and size
structEndArchive64Locator = "<4sLQL"
stringEndArchive64Locator = "PK\x06\x07"
structEndArchive64Locator = b"<4sLQL"
stringEndArchive64Locator = b"PK\x06\x07"
sizeEndCentDir64Locator = struct.calcsize(structEndArchive64Locator)
# The "Zip64 end of central directory" record, magic number, size, and indices
# (section V.G in the format document)
structEndArchive64 = "<4sQ2H2L4Q"
stringEndArchive64 = "PK\x06\x06"
structEndArchive64 = b"<4sQ2H2L4Q"
stringEndArchive64 = b"PK\x06\x06"
sizeEndCentDir64 = struct.calcsize(structEndArchive64)
_CD64_SIGNATURE = 0
@ -204,6 +209,7 @@ def _EndRecData(fpin):
fpin.seek(-sizeEndCentDir, 2)
except IOError:
return None
data = fpin.read()
if data[0:4] == stringEndArchive and data[-2:] == "\000\000":
# the signature is correct and there's no comment, unpack structure
@ -275,21 +281,21 @@ class ZipInfo (object):
# Terminate the file name at the first null byte. Null bytes in file
# names are used as tricks by viruses in archives.
null_byte = filename.find(chr(0))
null_byte = filename.find(b"\0")
if null_byte >= 0:
filename = filename[0:null_byte]
# This is used to ensure paths in generated ZIP files always use
# forward slashes as the directory separator, as required by the
# ZIP format specification.
if os.sep != "/" and os.sep in filename:
filename = filename.replace(os.sep, "/")
if os.sep != "/" and os.sep.encode('utf-8') in filename:
filename = filename.replace(os.sep.encode('utf-8'), b"/")
self.filename = filename # Normalized file name
self.date_time = date_time # year, month, day, hour, min, sec
# Standard values:
self.compress_type = ZIP_STORED # Type of compression for the file
self.comment = "" # Comment for each file
self.extra = "" # ZIP extra data
self.comment = b"" # Comment for each file
self.extra = b"" # ZIP extra data
if sys.platform == 'win32':
self.create_system = 0 # System which created ZIP archive
else:
@ -343,23 +349,13 @@ class ZipInfo (object):
return header + filename + extra
def _encodeFilenameFlags(self):
if isinstance(self.filename, unicode):
if isinstance(self.filename, bytes):
return self.filename, self.flag_bits
else:
try:
return self.filename.encode('ascii'), self.flag_bits
except UnicodeEncodeError:
return self.filename.encode('utf-8'), self.flag_bits | 0x800
else:
return self.filename, self.flag_bits
def _decodeFilename(self):
if self.flag_bits & 0x800:
try:
#print "decoding filename",self.filename
return self.filename.decode('utf-8')
except:
return self.filename
else:
return self.filename
def _decodeExtra(self):
# Try to decode the extra field.
@ -377,20 +373,20 @@ class ZipInfo (object):
elif ln == 0:
counts = ()
else:
raise RuntimeError, "Corrupt extra field %s"%(ln,)
raise RuntimeError("Corrupt extra field %s"%(ln,))
idx = 0
# ZIP64 extension (large files and/or large archives)
if self.file_size in (0xffffffffffffffffL, 0xffffffffL):
if self.file_size in (0xffffffffffffffff, 0xffffffff):
self.file_size = counts[idx]
idx += 1
if self.compress_size == 0xFFFFFFFFL:
if self.compress_size == 0xFFFFFFFF:
self.compress_size = counts[idx]
idx += 1
if self.header_offset == 0xffffffffL:
if self.header_offset == 0xffffffff:
old = self.header_offset
self.header_offset = counts[idx]
idx+=1
@ -398,6 +394,19 @@ class ZipInfo (object):
extra = extra[ln+4:]
class ZeroedZipInfo(ZipInfo):
def __init__(self, zinfo):
for k in self.__slots__:
if hasattr(zinfo, k):
setattr(self, k, getattr(zinfo, k))
def __getattribute__(self, name):
if name == "external_attr":
return 0
return object.__getattribute__(self, name)
class _ZipDecrypter:
"""Class to handle decryption of files stored within a ZIP archive.
@ -481,9 +490,9 @@ class ZipExtFile(io.BufferedIOBase):
if self._compress_type == ZIP_DEFLATED:
self._decompressor = zlib.decompressobj(-15)
self._unconsumed = ''
self._unconsumed = b''
self._readbuffer = ''
self._readbuffer = b''
self._offset = 0
self._universal = 'U' in mode
@ -514,10 +523,10 @@ class ZipExtFile(io.BufferedIOBase):
if not self._universal:
return io.BufferedIOBase.readline(self, limit)
line = ''
line = b''
while limit < 0 or len(line) < limit:
readahead = self.peek(2)
if readahead == '':
if readahead == b'':
return line
#
@ -564,7 +573,7 @@ class ZipExtFile(io.BufferedIOBase):
If the argument is omitted, None, or negative, data is read and returned until EOF is reached..
"""
buf = ''
buf = b''
while n < 0 or n is None or n > len(buf):
data = self.read1(n)
if len(data) == 0:
@ -594,7 +603,7 @@ class ZipExtFile(io.BufferedIOBase):
self._compress_left -= len(data)
if data and self._decrypter is not None:
data = ''.join(map(self._decrypter, data))
data = b''.join(map(self._decrypter, data))
if self._compress_type == ZIP_STORED:
self._readbuffer = self._readbuffer[self._offset:] + data
@ -651,10 +660,10 @@ class ZipFile:
pass
elif compression == ZIP_DEFLATED:
if not zlib:
raise RuntimeError,\
"Compression requires the (missing) zlib module"
raise RuntimeError(
"Compression requires the (missing) zlib module")
else:
raise RuntimeError, "That compression method is not supported"
raise RuntimeError("That compression method is not supported")
self._allowZip64 = allowZip64
self._didModify = False
@ -664,10 +673,11 @@ class ZipFile:
self.compression = compression # Method of compression
self.mode = key = mode.replace('b', '')[0]
self.pwd = None
self.comment = ''
self.comment = b''
# Check if we were passed a file-like object
if isinstance(file, basestring):
# "str" is python3, "unicode" is python2
if isinstance(file, str) or isinstance(file, unicode):
self._filePassed = 0
self.filename = file
modeDict = {'r' : 'rb', 'w': 'wb', 'a' : 'r+b'}
@ -699,7 +709,7 @@ class ZipFile:
if not self._filePassed:
self.fp.close()
self.fp = None
raise RuntimeError, 'Mode must be "r", "w" or "a"'
raise RuntimeError('Mode must be "r", "w" or "a"')
def __enter__(self):
return self
@ -723,9 +733,9 @@ class ZipFile:
fp = self.fp
endrec = _EndRecData(fp)
if not endrec:
raise BadZipfile, "File is not a zip file"
raise BadZipfile("File is not a zip file")
if self.debug > 1:
print endrec
print(endrec)
size_cd = endrec[_ECD_SIZE] # bytes in central directory
offset_cd = endrec[_ECD_OFFSET] # offset of central directory
self.comment = endrec[_ECD_COMMENT] # archive comment
@ -738,20 +748,20 @@ class ZipFile:
if self.debug > 2:
inferred = concat + offset_cd
print "given, inferred, offset", offset_cd, inferred, concat
print("given, inferred, offset", offset_cd, inferred, concat)
# self.start_dir: Position of start of central directory
self.start_dir = offset_cd + concat
fp.seek(self.start_dir, 0)
data = fp.read(size_cd)
fp = cStringIO.StringIO(data)
fp = BytesIO(data)
total = 0
while total < size_cd:
centdir = fp.read(sizeCentralDir)
if centdir[0:4] != stringCentralDir:
raise BadZipfile, "Bad magic number for central directory"
raise BadZipfile("Bad magic number for central directory")
centdir = struct.unpack(structCentralDir, centdir)
if self.debug > 2:
print centdir
print(centdir)
filename = fp.read(centdir[_CD_FILENAME_LENGTH])
# Create ZipInfo instance to store file information
x = ZipInfo(filename)
@ -769,7 +779,6 @@ class ZipFile:
x._decodeExtra()
x.header_offset = x.header_offset + concat
x.filename = x._decodeFilename()
self.filelist.append(x)
self.NameToInfo[x.filename] = x
@ -779,7 +788,7 @@ class ZipFile:
+ centdir[_CD_COMMENT_LENGTH])
if self.debug > 2:
print "total", total
print("total", total)
def namelist(self):
@ -796,10 +805,10 @@ class ZipFile:
def printdir(self):
"""Print a table of contents for the zip file."""
print "%-46s %19s %12s" % ("File Name", "Modified ", "Size")
print("%-46s %19s %12s" % ("File Name", "Modified ", "Size"))
for zinfo in self.filelist:
date = "%d-%02d-%02d %02d:%02d:%02d" % zinfo.date_time[:6]
print "%-46s %s %12d" % (zinfo.filename, date, zinfo.file_size)
print("%-46s %s %12d" % (zinfo.filename, date, zinfo.file_size))
def testzip(self):
"""Read all the files and check the CRC."""
@ -833,11 +842,11 @@ class ZipFile:
def open(self, name, mode="r", pwd=None):
"""Return file-like object for 'name'."""
if mode not in ("r", "U", "rU"):
raise RuntimeError, 'open() requires mode "r", "U", or "rU"'
if mode not in ("r", "", "rU"):
raise RuntimeError('open() requires mode "r", "", or "rU"')
if not self.fp:
raise RuntimeError, \
"Attempt to read ZIP archive that was already closed"
raise RuntimeError(
"Attempt to read ZIP archive that was already closed")
# Only open a new file for instances where we were not
# given a file object in the constructor
@ -859,7 +868,7 @@ class ZipFile:
# Skip the file header:
fheader = zef_file.read(sizeFileHeader)
if fheader[0:4] != stringFileHeader:
raise BadZipfile, "Bad magic number for file header"
raise BadZipfile("Bad magic number for file header")
fheader = struct.unpack(structFileHeader, fheader)
fname = zef_file.read(fheader[_FH_FILENAME_LENGTH])
@ -867,9 +876,9 @@ class ZipFile:
zef_file.read(fheader[_FH_EXTRA_FIELD_LENGTH])
if fname != zinfo.orig_filename:
raise BadZipfile, \
raise BadZipfile(
'File name in directory "%s" and header "%s" differ.' % (
zinfo.orig_filename, fname)
zinfo.orig_filename, fname))
# check for encrypted flag & handle password
is_encrypted = zinfo.flag_bits & 0x1
@ -878,8 +887,8 @@ class ZipFile:
if not pwd:
pwd = self.pwd
if not pwd:
raise RuntimeError, "File %s is encrypted, " \
"password required for extraction" % name
raise RuntimeError("File %s is encrypted, " \
"password required for extraction" % name)
zd = _ZipDecrypter(pwd)
# The first 12 bytes in the cypher stream is an encryption header
@ -956,7 +965,7 @@ class ZipFile:
return targetpath
source = self.open(member, pwd=pwd)
target = file(targetpath, "wb")
target = open(targetpath, "wb")
shutil.copyfileobj(source, target)
source.close()
target.close()
@ -967,18 +976,18 @@ class ZipFile:
"""Check for errors before writing a file to the archive."""
if zinfo.filename in self.NameToInfo:
if self.debug: # Warning for duplicate names
print "Duplicate name:", zinfo.filename
print("Duplicate name:", zinfo.filename)
if self.mode not in ("w", "a"):
raise RuntimeError, 'write() requires mode "w" or "a"'
raise RuntimeError('write() requires mode "w" or "a"')
if not self.fp:
raise RuntimeError, \
"Attempt to write ZIP archive that was already closed"
raise RuntimeError(
"Attempt to write ZIP archive that was already closed")
if zinfo.compress_type == ZIP_DEFLATED and not zlib:
raise RuntimeError, \
"Compression requires the (missing) zlib module"
raise RuntimeError(
"Compression requires the (missing) zlib module")
if zinfo.compress_type not in (ZIP_STORED, ZIP_DEFLATED):
raise RuntimeError, \
"That compression method is not supported"
raise RuntimeError(
"That compression method is not supported")
if zinfo.file_size > ZIP64_LIMIT:
if not self._allowZip64:
raise LargeZipFile("Filesize would require ZIP64 extensions")
@ -1006,7 +1015,7 @@ class ZipFile:
if isdir:
arcname += '/'
zinfo = ZipInfo(arcname, date_time)
zinfo.external_attr = (st[0] & 0xFFFF) << 16L # Unix attributes
zinfo.external_attr = (st[0] & 0xFFFF) << 16 # Unix attributes
if compress_type is None:
zinfo.compress_type = self.compression
else:
@ -1076,7 +1085,7 @@ class ZipFile:
date_time=time.localtime(time.time())[:6])
zinfo.compress_type = self.compression
zinfo.external_attr = 0600 << 16
zinfo.external_attr = 0x0600 << 16
else:
zinfo = zinfo_or_arcname
@ -1141,7 +1150,7 @@ class ZipFile:
if zinfo.header_offset > ZIP64_LIMIT:
extra.append(zinfo.header_offset)
header_offset = 0xffffffffL
header_offset = 0xffffffff
else:
header_offset = zinfo.header_offset
@ -1169,14 +1178,14 @@ class ZipFile:
0, zinfo.internal_attr, zinfo.external_attr,
header_offset)
except DeprecationWarning:
print >>sys.stderr, (structCentralDir,
print(structCentralDir,
stringCentralDir, create_version,
zinfo.create_system, extract_version, zinfo.reserved,
zinfo.flag_bits, zinfo.compress_type, dostime, dosdate,
zinfo.CRC, compress_size, file_size,
len(zinfo.filename), len(extra_data), len(zinfo.comment),
0, zinfo.internal_attr, zinfo.external_attr,
header_offset)
header_offset, sys.stderr)
raise
self.fp.write(centdir)
self.fp.write(filename)
@ -1250,10 +1259,10 @@ class PyZipFile(ZipFile):
else:
basename = name
if self.debug:
print "Adding package in", pathname, "as", basename
print("Adding package in", pathname, "as", basename)
fname, arcname = self._get_codename(initname[0:-3], basename)
if self.debug:
print "Adding", arcname
print("Adding", arcname)
self.write(fname, arcname)
dirlist = os.listdir(pathname)
dirlist.remove("__init__.py")
@ -1269,12 +1278,12 @@ class PyZipFile(ZipFile):
fname, arcname = self._get_codename(path[0:-3],
basename)
if self.debug:
print "Adding", arcname
print("Adding", arcname)
self.write(fname, arcname)
else:
# This is NOT a package directory, add its files at top level
if self.debug:
print "Adding files from directory", pathname
print("Adding files from directory", pathname)
for filename in os.listdir(pathname):
path = os.path.join(pathname, filename)
root, ext = os.path.splitext(filename)
@ -1282,15 +1291,15 @@ class PyZipFile(ZipFile):
fname, arcname = self._get_codename(path[0:-3],
basename)
if self.debug:
print "Adding", arcname
print("Adding", arcname)
self.write(fname, arcname)
else:
if pathname[-3:] != ".py":
raise RuntimeError, \
'Files added with writepy() must end with ".py"'
raise RuntimeError(
'Files added with writepy() must end with ".py"')
fname, arcname = self._get_codename(pathname[0:-3], basename)
if self.debug:
print "Adding file", arcname
print("Adding file", arcname)
self.write(fname, arcname)
def _get_codename(self, pathname, basename):
@ -1310,11 +1319,11 @@ class PyZipFile(ZipFile):
os.stat(file_pyc).st_mtime < os.stat(file_py).st_mtime:
import py_compile
if self.debug:
print "Compiling", file_py
print("Compiling", file_py)
try:
py_compile.compile(file_py, file_pyc, None, True)
except py_compile.PyCompileError,err:
print err.msg
except py_compile.PyCompileError as err:
print(err.msg)
fname = file_pyc
else:
fname = file_pyc
@ -1337,12 +1346,12 @@ def main(args = None):
args = sys.argv[1:]
if not args or args[0] not in ('-l', '-c', '-e', '-t'):
print USAGE
print(USAGE)
sys.exit(1)
if args[0] == '-l':
if len(args) != 2:
print USAGE
print(USAGE)
sys.exit(1)
zf = ZipFile(args[1], 'r')
zf.printdir()
@ -1350,15 +1359,15 @@ def main(args = None):
elif args[0] == '-t':
if len(args) != 2:
print USAGE
print(USAGE)
sys.exit(1)
zf = ZipFile(args[1], 'r')
zf.testzip()
print "Done testing"
print("Done testing")
elif args[0] == '-e':
if len(args) != 3:
print USAGE
print(USAGE)
sys.exit(1)
zf = ZipFile(args[1], 'r')
@ -1378,7 +1387,7 @@ def main(args = None):
elif args[0] == '-c':
if len(args) < 3:
print USAGE
print(USAGE)
sys.exit(1)
def addToZip(zf, path, zippath):

View file

@ -1,8 +1,8 @@
#!/usr/bin/env python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# zipfix.py, version 1.1
# Copyright © 2010-2013 by some_updates, DiapDealer and Apprentice Alf
# zipfix.py
# Copyright © 2010-2020 by Apprentice Harper et al.
# Released under the terms of the GNU General Public Licence, version 3
# <http://www.gnu.org/licenses/>
@ -10,19 +10,23 @@
# Revision history:
# 1.0 - Initial release
# 1.1 - Updated to handle zip file metadata correctly
# 2.0 - Python 3 for calibre 5.0
"""
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
import zipfilerugged
import os
import os.path
from zipfilerugged import ZipInfo, ZeroedZipInfo
import getopt
from struct import unpack
@ -33,12 +37,6 @@ _FILENAME_OFFSET = 30
_MAX_SIZE = 64 * 1024
_MIMETYPE = 'application/epub+zip'
class ZipInfo(zipfilerugged.ZipInfo):
def __init__(self, *args, **kwargs):
if 'compress_type' in kwargs:
compress_type = kwargs.pop('compress_type')
super(ZipInfo, self).__init__(*args, **kwargs)
self.compress_type = compress_type
class fixZip:
def __init__(self, zinput, zoutput):
@ -48,7 +46,7 @@ class fixZip:
self.inzip = zipfilerugged.ZipFile(zinput,'r')
self.outzip = zipfilerugged.ZipFile(zoutput,'w')
# open the input zip for reading only as a raw file
self.bzf = file(zinput,'rb')
self.bzf = open(zinput,'rb')
def getlocalname(self, zi):
local_header_offset = zi.header_offset
@ -61,21 +59,21 @@ class fixZip:
def uncompress(self, cmpdata):
dc = zlib.decompressobj(-15)
data = ''
data = b''
while len(cmpdata) > 0:
if len(cmpdata) > _MAX_SIZE :
newdata = cmpdata[0:_MAX_SIZE]
cmpdata = cmpdata[_MAX_SIZE:]
else:
newdata = cmpdata
cmpdata = ''
cmpdata = b''
newdata = dc.decompress(newdata)
unprocessed = dc.unconsumed_tail
if len(unprocessed) == 0:
newdata += dc.flush()
data += newdata
cmpdata += unprocessed
unprocessed = ''
unprocessed = b''
return data
def getfiledata(self, zi):
@ -114,11 +112,12 @@ class fixZip:
# if epub write mimetype file first, with no compression
if self.ztype == 'epub':
# first get a ZipInfo with current time and no compression
mimeinfo = ZipInfo('mimetype',compress_type=zipfilerugged.ZIP_STORED)
mimeinfo = ZipInfo(b'mimetype')
mimeinfo.compress_type = zipfilerugged.ZIP_STORED
mimeinfo.internal_attr = 1 # text file
try:
# if the mimetype is present, get its info, including time-stamp
oldmimeinfo = self.inzip.getinfo('mimetype')
oldmimeinfo = self.inzip.getinfo(b'mimetype')
# copy across useful fields
mimeinfo.date_time = oldmimeinfo.date_time
mimeinfo.comment = oldmimeinfo.comment
@ -126,13 +125,21 @@ class fixZip:
mimeinfo.internal_attr = oldmimeinfo.internal_attr
mimeinfo.external_attr = oldmimeinfo.external_attr
mimeinfo.create_system = oldmimeinfo.create_system
mimeinfo.create_version = oldmimeinfo.create_version
mimeinfo.volume = oldmimeinfo.volume
except:
pass
self.outzip.writestr(mimeinfo, _MIMETYPE)
# Python 3 has a bug where the external_attr is reset to `0o600 << 16`
# if it's NULL, so we need a workaround:
if mimeinfo.external_attr == 0:
mimeinfo = ZeroedZipInfo(mimeinfo)
self.outzip.writestr(mimeinfo, _MIMETYPE.encode('ascii'))
# write the rest of the files
for zinfo in self.inzip.infolist():
if zinfo.filename != "mimetype" or self.ztype != 'epub':
if zinfo.filename != b"mimetype" or self.ztype != 'epub':
data = None
try:
data = self.inzip.read(zinfo.filename)
@ -142,12 +149,23 @@ class fixZip:
zinfo.filename = local_name
# create new ZipInfo with only the useful attributes from the old info
nzinfo = ZipInfo(zinfo.filename, zinfo.date_time, compress_type=zinfo.compress_type)
nzinfo = ZipInfo(zinfo.filename)
nzinfo.date_time = zinfo.date_time
nzinfo.compress_type = zinfo.compress_type
nzinfo.comment=zinfo.comment
nzinfo.extra=zinfo.extra
nzinfo.internal_attr=zinfo.internal_attr
nzinfo.external_attr=zinfo.external_attr
nzinfo.create_system=zinfo.create_system
nzinfo.create_version = zinfo.create_version
nzinfo.volume = zinfo.volume
nzinfo.flag_bits = zinfo.flag_bits & 0x800 # preserve UTF-8 flag
# Python 3 has a bug where the external_attr is reset to `0o600 << 16`
# if it's NULL, so we need a workaround:
if nzinfo.external_attr == 0:
nzinfo = ZeroedZipInfo(nzinfo)
self.outzip.writestr(nzinfo,data)
self.bzf.close()
@ -156,22 +174,22 @@ class fixZip:
def usage():
print """usage: zipfix.py inputzip outputzip
print("""usage: zipfix.py inputzip outputzip
inputzip is the source zipfile to fix
outputzip is the fixed zip archive
"""
""")
def repairBook(infile, outfile):
if not os.path.exists(infile):
print "Error: Input Zip File does not exist"
print("Error: Input Zip File does not exist")
return 1
try:
fr = fixZip(infile, outfile)
fr.fix()
return 0
except Exception, e:
print "Error Occurred ", e
except Exception as e:
print("Error Occurred ", e)
return 2

36
DeDRM_plugin_ReadMe.txt Normal file
View file

@ -0,0 +1,36 @@
DeDRM_plugin.zip
================
This plugin will remove the DRM from:
- Kindle ebooks (files from Kindle for Mac/PC and eInk Kindles).
- Adobe Digital Editions ePubs (including Kobo and Google ePubs downloaded to ADE)
- Adobe Digital Editions PDFs
For limitations and work-arounds, see the FAQ at https://github.com/noDRM/DeDRM_tools/blob/master/FAQs.md (or the FAQ in Apprentice Harper's original repository at https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md)
Installation
------------
Open calibre's Preferences dialog. Click on the "Plugins" button. Next, click on the button, "Load plugin from file". Navigate to the unzipped DeDRM_tools folder, find the file "DeDRM_plugin.zip". Click to select the file and select "Open". Click "Yes" in the "Are you sure?" dialog box. Click the "OK" button in the "Success" dialog box.
Customization
-------------
For Kindle ebooks from an E-Ink based Kindle (e.g. Voyage), or books downloaded from the Amazon web site 'for transfer via USB' to an E-Ink base Kindle, you must enter the Kindle's serial number in the customisation dialog.
When you have finished entering your configuration information, you must click the OK button to save it. If you click the Cancel button, all your changes in all the configuration dialogs will be lost.
Troubleshooting
---------------
If you find that the DeDRM plugin is not working for you (imported ebooks still have DRM - that is, they won't convert or open in the calibre ebook viewer), you should make a log of the import process by deleting the DRMed ebook from calibre and then adding the ebook to calibre when it's running in debug mode. This will generate a lot of helpful debugging info that can be copied into any online help requests. Here's how to do it:
- Remove the DRMed book from calibre.
- Click the Preferences drop-down menu and choose 'Restart in debug mode'.
- Once calibre has re-started, import the problem ebook.
- Now close calibre.
A log will appear that you can copy and paste into a GitHub issue report at https://github.com/noDRM/DeDRM_tools/issues. Please also include information about the eBook file.
If you're using Apprentice Harper's original version, you can also comment at Apprentice Alf's blog, http://apprenticealf.wordpress.com/ or open an issue at Apprentice Harper's repository, https://github.com/apprenticeharper/DeDRM_tools/issues.

166
FAQs.md
View file

@ -9,118 +9,99 @@ DRM ("Digital Rights Management") is a way of using encryption to tie the books
When your ebooks have DRM you are unable to convert the ebook from one format to another (e.g. Kindle KF8 to Kobo ePub), so you are restricted in the range of ebook stores you can use. DRM also allows publishers to restrict what you can do with the ebook you've bought, e.g. preventing the use of text-to-speech software. Longer term, you can never be sure that you'll be able to come back and re-read your ebooks if they have DRM, even if you save back-up copies.
## So how can I remove DRM from my ebooks?
Just download and use these tools, that's all! Uh, almost. There are a few, uh, provisos, a, a couple of quid pro quos.
Just download and use these tools, that's all! Uh, almost. There are a few, uh, provisos, a couple of quid pro quos.
* The tools don't work on all ebooks. For example, they don't work on any ebooks from Apple's iBooks store.
* You must own the ebook - the tools won't work on library ebooks or rented ebooks or books from a friend.
* You must not use these tools to give your ebooks to a hundred of your closest friends. Or to a million strangers. Authors need to sell books to be able to write more books. Don't be mean to the authors.
* Do NOT use Adobe Digital Editions 3.0 or later to download your ePubs. ADE 3.0 and later might use a new encryption scheme that the tools can't handle. While major ebook stores aren't using the new scheme yet, using ADE 2.0.1 will ensure that your ebooks are downloaded using the old scheme. Once a book has been downloaded with the new scheme, it's IMPOSSIBLE to re-download using the old scheme (without buying it again).
* Do NOT use Kindle for PC/Mac version 1.25 or later. The tools don't current work with those versions.
But otherwise, if your ebook is from Amazon, Kobo, Barnes & Noble or any of the ebook stores selling ebooks compatible with Adobe Digital Editions 2.0.1, you should be able to remove the DRM that's been applied to your ebooks.
### Recent Changes to Kindle for PC/Kindle for Mac
Starting with version 1.19, Kindle for PC/Mac uses Amazon's new KFX format which isn't quite as good a source for conversion to ePub as the older KF8 (& MOBI) formats. There are two options to get the older formats. Either stick with version 1.17 or earlier, or modify the executable by changing a file name (PC) or disabling a component of the application (Mac).
### A Recent Change to Kindle for PC/Kindle for Mac
Starting with version 1.19, Kindle for PC/Mac uses Amazon's new KFX format which isn't quite as good a source fro conversion to ePub as the older KF8 (& MOBI) formats. There are two options to get the older formats. Either stick with version 1.17 or earlier, or modify the executable by changing a file name. Note that with Kindle for PC/Mac 1.25 and later, there is no current solution even for FKX. You must use 1.24 or earlier.
Version 1.17 of Kindle is are no longer available directly from Amazon, so you will need to search for the proper file name and find it on a third party site. The name is "KindleForPC-installer-1.17.44170.exe" for PC and "KindleForMac-44182.dmg" for Mac.
Version 1.17 of Kindle is no longer available directly from Amazon, so you will need to search for the proper file name and find it on a third party site. The name is `KindleForPC-installer-1.17.44170.exe` for PC and `KindleForMac-44182.dmg` for Mac. (Note that this is a 32-bit application on the Mac, so will not work on Catalina and newer versions of macOS.)
Verify the one of the following cryptographic hash values, using software of your choice, before installing the downloaded file in order to avoid viruses. If the hash does not match, delete the downloaded file and try again from another site.
Kindle for PC:
MD-5: 53F793B562F4823721AA47D7DE099869
SHA-1: 73C404D719F0DD8D4AE1C2C96612B095D6C86255
SHA-256: 14E0F0053F1276C0C7C446892DC170344F707FBFE99B695176 2C120144163200
Kindle for Mac:
MD-5: E7E36D5369E1F3CF1D28E5D9115DF15F
SHA-1: 7AB9A86B954CB23D622BD79E3257F8E2182D791C
SHA-256: 28DC21246A9C7CDEDD2D6F0F4082E6BF7EF9DB9CE9D485548E 8A9E1D19EAE2AC.
You will need to go to the preferences and uncheck the auto update checkbox. Then download and install 1.17 over the top of the 1.19 installation. You'll also need to delete the KFX folders from your My Kindle Content folder.
#### Kindle for PC `KindleForPC-installer-1.17.44170.exe`:
* MD-5: 53F793B562F4823721AA47D7DE099869
* SHA-1: 73C404D719F0DD8D4AE1C2C96612B095D6C86255
* SHA-256: 14E0F0053F1276C0C7C446892DC170344F707FBFE99B6951762C120144163200
A other possible solution is to use 1.19 or later, but disable KFX by renaming or disabling a necessary component of the application. This may or may not work on versions after 1.20. In a command window, enter the following commands when Kindle for PC/Mac is not running:
#### Kindle for Mac `KindleForMac-44182.dmg`:
* MD-5: E7E36D5369E1F3CF1D28E5D9115DF15F
* SHA-1: 7AB9A86B954CB23D622BD79E3257F8E2182D791C
* SHA-256: 28DC21246A9C7CDEDD2D6F0F4082E6BF7EF9DB9CE9D485548E8A9E1D19EAE2AC
You will need to go to the preferences and uncheck the auto update checkbox. Then download and install 1.17 over the top of the newer installation. You'll also need to delete the KFX folders from your My Kindle Content folder. You may also need to take further action to prevent an auto update. The simplest way is to find the 'updates' folder and replace it with a file. See [this thread] (http://www.mobileread.com/forums/showthread.php?t=283371) at MobileRead for a Script to do this on a PC. On a Mac you can find the folder at ~/Library/Application Support/Kindle/. Make the 'updates' folder read-only, or delete it and save a blank text file called 'updates' in its place.
Another possible solution is to use 1.19 or later, but disable KFX by renaming or disabling a necessary component of the application. This may or may not work on versions after 1.25. In a command window, enter the following commands when Kindle for PC/Mac is not running:
#### Windows
ren %localappdata%\Amazon\Kindle\application\renderer-test.exe renderer-test.xxx
`ren %localappdata%\Amazon\Kindle\application\renderer-test.exe renderer-test.xxx`
PC Note: The renderer-test program may be in a different location in some Kindle for PC installations. If the rename command fails look in other folders, such as C:\Program Files\Amazon\Kindle.
PC Note: The renderer-test program may be in a different location in some Kindle for PC installations. If the rename command fails look in other folders, such as `C:\Program Files\Amazon\Kindle`.
#### Macintosh
chmod -x /Applications/Kindle.app/Contents/MacOS/renderer-test
`chmod -x /Applications/Kindle.app/Contents/MacOS/renderer-test`
Mac Note: If the chmod command fails with a permission error try again using sudo before chmod - sudo chmod [...]
Mac Note: If the chmod command fails with a permission error try again using `sudo` before `chmod` - `sudo chmod` [...]. This only works on Kindle for Mac 1.19 thru 1.31, it does NOT work with 1.32 or newer.
After restarting the Kindle program any books previously downloaded in KFX format will no longer open. You will need to remove them from your device and re-download them. All future downloads will use the older Kindle formats instead of KFX although they will continue to be placed in one individual subdirectory per book.
After restarting the Kindle program any books previously downloaded in KFX format will no longer open. You will need to remove them from your device and re-download them. All future downloads will use the older Kindle formats instead of KFX although they will continue to be placed in one individual subdirectory per book. Note that books should be downloaded by right-click and 'Download', not by just opening the book. Recent (1.25+) versions of Kindle for Mac/PC may convert KF8 files to a new format that is not supported by these tools when the book is opened for reading.
#### Decrypting KFX
Thanks to work by several people, the tools can now decrypt KFX format ebooks from Kindle for PC. In addition to the DeDRM plugin, calibre users will also need to install jhowell's KFX Input plugin which is available through the standard plugin menu in calibre, or directly from [his plugin thread](https://www.mobileread.com/forums/showthread.php?t=291290) on Mobileread. Not that KFX decryption does not work for Kindle for Mac 1.25 and later.
Thanks to work by several people, the tools can now decrypt KFX format ebooks from Kindle for Mac/PC. In addition to the DeDRM plugin, calibre users will also need to install jhowell's KFX Input plugin which is available through the standard plugin menu in calibre, or directly from [his plugin thread](https://www.mobileread.com/forums/showthread.php?t=291290) on Mobileread.
It's quite possible that Amazon will update their KFX DeDRM to prevent DRM removal from KFX books again. So Remove DRM as soon as possible!
#### Thanks
Thanks to jhowell for his investigations into KFX format and the KFX Input plugin. Some of these instructions are from [his thread on the subject](https://www.mobileread.com/forums/showthread.php?t=283371) at MobileRead.
## Where can I get the latest version of these free DRM removal tools?
Right here at github. Just go to the [releases page](https://github.com/apprenticeharper/DeDRM_tools/releases) and download the latest zip archive of the tools, named DeDRM\_tools\_X.X.X.zip, where X.X.X is the version number. You do not need to download the source code archive.
Right here at github. Just go to the [releases page](https://github.com/noDRM/DeDRM_tools/releases) and download the latest zip archive of the tools, named `DeDRM\_tools\_X.X.X.zip`, where X.X.X is the version number. You do not need to download the source code archive. This will get you the forked version by noDRM. If you want to download the original version by Apprentice Harper, go to [this page](https://github.com/apprenticeharper/DeDRM_tools/releases) instead.
## I've downloaded the tools archive. Now what?
First, unzip the archive. You should now have a DeDRM folder containing several other folders and a ReadMe\_First.txt file. Please read the ReadMe\_First file! That will explain what the folders are, and you'll be able to work out which of the tools you need.
First, unzip the archive. You should now have a DeDRM folder containing several files, including a `ReadMe_Overview.txt` file. Please read the `ReadMe_Overview.txt` file! That will explain what the files are, and you'll be able to work out which of the tools you need.
## That's a big complicated ReadMe file! Isn't there a quick guide?
Install calibre. Install the DeDRM\_plugin in calibre. Install the Obok\_plugin in calibre. Restart calibre. In the DeDRM_plugin customisation dialog add in any E-Ink Kindle serial numbers and your B&N account email address and password. Remember that the plugin only tries to remove DRM when ebooks are imported.
Install calibre. Install the DeDRM\_plugin in calibre. Install the Obok\_plugin in calibre. Restart calibre. In the DeDRM_plugin customisation dialog add in any E-Ink Kindle serial numbers. Remember that the plugin only tries to remove DRM when ebooks are imported.
# Installing the Tools
## The calibre plugin
### I am trying to install the calibre plugin, but calibre says "ERROR: Unhandled exception: InvalidPlugin: The plugin in u[path]DeDRM\_tools\_6.5.3.zip is invalid. It does not contain a top-level \_\_init\_\_.py file"
You are trying to add the tools archive (e.g. DeDRM\_tools\_6.5.3.zip) instead of the plugin. The tools archive is not the plugin. It is a collection of DRM removal tools which includes the plugin. You must unzip the archive, and install the calibre plugin (DeDRM\_plugin.zip) from a folder called “DeDRM\_calibre_plugin” in the unzipped archive.
### I am trying to install the calibre plugin, but calibre says "ERROR: Unhandled exception: InvalidPlugin: The plugin in '[path]DeDRM\_tools\_X.X.X.zip' is invalid. It does not contain a top-level \_\_init\_\_.py file"
You are trying to add the tools archive (e.g. `DeDRM_tools_10.0.2.zip`) instead of the plugin. The tools archive is not the plugin. It is a collection of DRM removal tools which includes the plugin. You must unzip the archive, and install the calibre plugin `DeDRM_plugin.zip` from inside the unzipped archive.
### Ive unzipped the tools archive, but I cant find the calibre plugin when I try to add them to calibre. I use Windows.
You should select the zip file that is in the “DeDRM\_calibre\_plugin” folder, not any files inside the plugins zip archive. Make sure you are selecting from the folder that you created when you unzipped the tools archive and not selecting a file inside the still-zipped tools archive.
You should select the zip file that is in the `DeDRM_calibre_plugin` folder, not any files inside the plugins zip archive. Make sure you are selecting from the folder that you created when you unzipped the tools archive and not selecting a file inside the still-zipped tools archive.
(The problem is that Windows will allow apps to browse inside zip archives without needing to unzip them first. If there are zip archives inside the main zip archives, Windows will show them as unzipped as well. So what happens is people will unzip the DeDRM\_tools\_X.X.X.zip to a folder, but when using calibre they will actually navigate to the still zipped file by mistake and cannot tell they have done so because they do not have file extensions showing. So to the unwary Windows user, it appears that the zip archive was unzipped and that everything inside it was unzipped as well so there is no way to install the plugins.
(The problem is that Windows will allow apps to browse inside zip archives without needing to unzip them first. If there are zip archives inside the main zip archives, Windows will show them as unzipped as well. So what happens is people will unzip the `DeDRM_tools_X.X.X.zip` to a folder, but when using calibre they will actually navigate to the still zipped file by mistake and cannot tell they have done so because they do not have file extensions showing. So to the unwary Windows user, it appears that the zip archive was unzipped and that everything inside it was unzipped as well so there is no way to install the plugins.
We strongly recommend renaming the DeDRM\_tools\_X.X.X.zip archive (after extracting its contents) to DeDRM\_tools\_X.X.X_archive.zip. If you do that, you are less likely to navigate to the wrong location from inside calibre.)
## The Windows Application
### I've installed ActiveState Python and PyCrypto, but the Windows application won't run. What have I done wrong?
Nothing. There's a bug in the some older ActiveState Python Windows installers that puts the Tcl code in the wrong place. See [this comment of mine at ActiveState community](https://community.activestate.com/node/19090). Just move the Tcl code to the correct place manually and the Windows app should run.
## The Macintosh Application
### I can't open the Macintosh Application. Some message about it not being signed or something.
Try right-clicking and select open. That might give you the option to open it anyway. Otherwise you'll need to change your security settings to allow unsigned applications to run. You can probably change these back after running it for the first time.
### I can't open the Macintosh Application at all. I get 'The aplication "DeDRM" can't be opened'
Some unzip applications do not respect the execution bit setting. Try unzipping the main tools archive using the built-in Mac unzip utility.
Alternatively, sometimes the execution bit isn't set correctly in the archive. If you put the extracted DeDRM application in your Applications folder, you can set the executable bit on the 'droplet' file from the terminal using the command chmod +x /Applications/DeDRM.app/Contents/MacOS/droplet
### I can't open the Macintosh Application at all. I get 'spawn_via_launchd() failed, errno=111'
There seems to be a bug in Apple's launch services. Try using the free [Maintenance utility](https://www.titanium-software.fr/en/maintenance.html) from Titanium Software to clear the launch cache and database.
### The application opens, but always gives an error in the log 'ImportError: No module named Crypto.Cipher'
Some version of MacOS don't include PyCrpto. Your should be able to install it by using this command in the Terminal app: python -m pip pycrypto
We strongly recommend renaming the `DeDRM_tools_X.X.X.zip` archive (after extracting its contents) to `DeDRM_tools_X.X.X_archive.zip`. If you do that, you are less likely to navigate to the wrong location from inside calibre.)
# Using the Tools
## I cant get the tools to work on my rented or library ebooks.
The tools are not designed to remove DRM from rented or library ebooks.
## I've unzipped the tools, but what are all the different files, and how do I use them?
Read the ReadMe_First.txt file and then the ReadMe files included in the tools folder(s) you're interested in. That's what they're for.
Read the `ReadMe_Overview.txt` file and then the ReadMe files for the tools you're interested in. That's what they're for.
## I have installed the calibre plugin, but my books still have DRM. When I try to view or convert my books, calibre says they have DRM.
DRM only gets removed when an ebook is imported into calibre. Also, if the book is already in calibre, by default calibre will discard the newly imported file. You can change this in calibre's Adding books preferences page (Automerge..../Overwrite....), so that newly imported files overwrite existing ebook formats. Then just re-import your books and the DRM-free versions will overwrite the DRMed versions while retaining your books' metadata.
## I have installed the calibre plugin or I am trying to use one of the other tools, but I dont know where my ebooks are stored.
## I have installed the calibre plugin, but I dont know where my ebooks are stored.
Your ebooks are stored on your computer or on your ebook reader. You need to find them to be able to remove the DRM. If they are on your reader, you should be able to locate them easily. On your computer its not so obvious. Here are the default locations.
### Macintosh
Navigating from your home folder,
Kindle for Mac ebooks are in either Library/Application Support/Kindle/My Kindle Content or Documents/My Kindle Content or Library/Containers/com.amazon.Kindle/Data/Library/Application Support/Kindle/My Kindle Content, depending on your version of Kindle for Mac.
Kindle for Mac ebooks are in either `Library/Application Support/Kindle/My Kindle Content` or `Documents/My Kindle Content` or `Library/Containers/com.amazon.Kindle/Data/Library/Application Support/Kindle/My Kindle Content`, depending on your version of Kindle for Mac.
Adobe Digital Editions ebooks are in Documents/Digital Editions
Adobe Digital Editions ebooks are in `Documents/Digital Editions`
### Windows
Navigating from your "Documents" folder ("My Documents" folder, pre-Windows 7)
Navigating from your `Documents` folder (`My Documents` folder, pre-Windows 7)
Kindle for PC ebooks are in My Kindle Content
Kindle for PC ebooks are in `My Kindle Content`
Adobe Digital Editions ebooks are in My Digital Editions
Adobe Digital Editions ebooks are in `My Digital Editions`
## I have installed the calibre plugin, and the book is not already in calibre, but the DRM does not get removed.
@ -134,71 +115,58 @@ If this book is from an eInk Kindle (e.g. Paperwhite), you must enter the serial
If this book is from Kindle for Mac or Kindle for PC, you must have the Kindle Software installed on the same computer and user account as your copy of calibre.
If this book is from Kindle for Mac you must be using version 1.24 or below, even if you have the Input plugin installed.
If the book is from Kindle for PC or Kindle for Mac and you think you are doing everything right, and you are getting this message, it is possible that the files containing the encryption key arent quite in the format the tools expect. To try to fix this:
1. Deregister Kindle for PC(Mac) from your Amazon account.
1. Uninstall Kindle for PC(Mac)
1. Delete the Kindle for PC(Mac) preferences
* PC: Delete the directory [home folder]\AppData\Local\Amazon (it might be hidden) and [home folder]\My Documents\My Kindle Content
* Mac: Delete the directory [home folder]/Library/Application Support/Kindle/and/or [home folder]/Library/Containers/com.amazon.Kindle/Data/Library/Application Support/Kindle/ (one or both may be present and should be deleted)
1. Reinstall Kindle for PC(Mac) version 1.17 or earlier (see above for download links).
1. Re-register Kindle for PC(Mac) with your Amazon account
1. Deregister Kindle for PC/Mac from your Amazon account.
1. Uninstall Kindle for PC/Mac
1. Delete the Kindle for PC/Mac preferences
* PC: Delete the directory `[home folder]\AppData\Local\Amazon` (it might be hidden) and `[home folder]\My Documents\My Kindle Content`
* Mac: Delete the directory `[home folder]/Library/Application Support/Kindle/` and/or `[home folder]/Library/Containers/com.amazon.Kindle/Data/Library/Application Support/Kindle/` (one or both may be present and should be deleted)
1. Reinstall Kindle for PC/Mac version 1.17 or earlier (see above for download links).
1. Re-register Kindle for PC/Mac with your Amazon account
1. Download the ebook again. Do not use the files you have downloaded previously.
## Some of my books had their DRM removed, but some still say that they have DRM and will not convert.
There are several possible reasons why only some books get their DRM removed.
* You still dont have the DRM removal tools working correctly, but some of your books didnt have DRM in the first place.
* Kindle only: It is a Topaz format book and contains some coding that the tools do not understand. You will need to get a log of the DeDRM attempt, and then create a [new issue at Apprentice Harper's github repository](https://github.com/apprenticeharper/DeDRM_tools/issues/), attaching the book and the log, so that the tools can be updated.
If you are still having problems with particular books, you will need to create a log of the DRM removal attempt for one of the problem books, and post that in a comment at Apprentice Alf's blog or in a new issue at Apprentice Harper's github repository.
If you are still having problems with particular books, you will need to create a log of the DRM removal attempt for one of the problem books. If you're using NoDRM's fork, open [a new issue](https://github.com/noDRM/DeDRM_tools/issues) in the GitHub repo. If you're using Apprentice Harper's version, post that logfile in a new issue at [Apprentice Harper's github repository](https://github.com/apprenticeharper/DeDRM_tools/issues).
## My Kindle book has imported and the DRM has been removed, but all the pictures are gone.
Most likely, this is a book downloaded from Amazon directly to an eInk Kindle (e.g. Paperwhite). Unfortunately, the pictures are probably in a .azw6 file that the tools don't understand. You must download the book manually from Amazon's web site "For transfer via USB" to your Kindle. When you download the eBook in this manner, Amazon will package the pictures in the with text in a single file that the tools will be able to import successfully.
Most likely, this is a book downloaded from Amazon directly to an eInk Kindle (e.g. Paperwhite). Unfortunately, the pictures are probably in a `.azw6` file that the tools don't understand. You must download the book manually from Amazon's web site "For transfer via USB" to your Kindle. When you download the eBook in this manner, Amazon will package the pictures in the with text in a single file that the tools will be able to import successfully.
## My Kindle book has imported, but it's showing up as an AZW4 format. Conversions take a long time and/or are very poor.
You have found a Print Replica Kindle ebook. This is a PDF in a Kindle wrapper. Now the DRM has been removed, you can extract the PDF from the wrapper using the KindleUnpack plugin. Conversion of PDFs rarely gives good results.
## The tools can't see an ebook that was downloaded directly to my eInk kindle, although it's definitely there, and I can read it on the Kindle. I can't even try to import it.
Mostly likely, this is a book downloaded from Amazon directly to one of the newer eInk Kindles (e.g. Paperwhite). Unfortunately, it is probably in a new multi-file KFX format that the tools don't understand. You must download the book manually from Amazon's web site "For transfer via USB" to your Kindle. When you download the ebook in this manner, Amazon will send a single KF8-format file that the tools will be able to import successfully.
## Do the tools work on books from Kobo?
If you use the Kobo desktop application for Mac or PC, install the obok plugin. This will import and remove the DRM from your Kobo books, and is the easiest method for Kobo ebooks.
## I registered Adobe Digital Editions 3.0 or later with an Adobe ID before downloading, but my epub or PDF still has DRM.
Adobe introduced a new DRM scheme with ADE 3.0 and later. Install ADE 2.0.1 and register with the same Adobe ID. If you can't open your book in ADE 2.01, then you have a book with the new DRM scheme. These tools can't help. You can avoid the new DRM scheme by always downloading your ebooks with ADE 2.0.1. Some retailers will require ADE 3.0 or later, in which case you won't be able to download with ADE 2.0.1.
## The DRM wasn't removed and the log says "Failed to decrypt with error: Cannot decode library or rented ebooks." What now?
You're trying to remove the DRM from an ebook that's only on loan to you. No help will be given to remove DRM from such ebooks. If you think that you have received this message for a book you own, please create an issue at github, or comment at the blog.
If you use the Kobo desktop application for Mac or PC, install the Obok plugin. This will import and remove the DRM from your Kobo books, and is the easiest method for Kobo ebooks.
## I cannot solve my problem with the DeDRM plugin, and now I need to post a log. How do I do that?
Remove the DRMed book from calibre. Click the Preferences drop-down menu and choose 'Restart in debug mode'. Once calibre has re-started, import the problem ebook. Now close calibre. A log will appear that you can copy and paste into a comment at Apprentice Alf's blog, or into a new issue at Apprentice Harper's github repository.
## I cannot solve my problem with the Macintosh DeDRM application, and now I need to post a log. How do I do that?
The Macintosh DeDRM application creates a log file on your desktop every time it is run. After unsuccessfully removing DRM from one ebook, copy the contents of the log file (it is a simple text file) and paste it into your comment at Apprentice Alf's blog or in a new issue at Apprentice Harper's github repository.
## I cannot solve my problem with the Windows DeDRM application, and now I need to post a log. How do I do that?
The Windows DeDRM application creates a log file in your home directory (C:\Users\[username]) every time it is run. After unsuccessfully removing DRM from one ebook, copy the contents of the log file (it is a simple text file) and paste it into your comment at Apprentice Alf's blog or in a new issue at Apprentice Harper's github repository.
Remove the DRMed book from calibre. Click the Preferences drop-down menu and choose 'Restart in debug mode'. Once calibre has re-started, import the problem ebook. Now close calibre. A log will appear that you can copy and paste into [a new issue](https://github.com/noDRM/DeDRM_tools/issues) in NoDRMs GitHub repo. If you're using Apprentice Harpers version, post that logfile in a new issue at [Apprentice Harper's GitHub repository](https://github.com/apprenticeharper/DeDRM_tools/issues).
## Is there a way to use the DeDRM plugin for Calibre from the command line?
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?
The tools only remove the DRM. No attempt is made to remove any personally identifying information.
That question cannot be answered for sure. While it is easy to check if a book has DRM or not, it is very difficult to verify if all (traces of) personal information have been removed from a book. The tools attempt to remove watermarks when they are detected (optionally, there's an option in the plugin settings to enable that), but that will not be the case for all watermarks.
## What do some of my Kindle ebooks import as HTMLZ format in calibre?
## Why do some of my Kindle ebooks import as HTMLZ format in calibre?
Most Amazon Kindle ebooks are Mobipocket format ebooks, or the new KF8 format. However, some are in a format known as Topaz. The Topaz format is only used by Amazon. A Topaz ebook is a collections of glyphs and their positions on each page tagged with some additional information from that page including OCRed text (Optical Character Recognition generated Text) to allow searching, and some additional layout information. Each page of a Topaz ebook is effectively a description of an image of that page. To convert a Topaz ebook to another format is not easy as there is not a one-to-one mapping between glyphs and characters/fonts. To account for this, two different formats are generated by the DRM removal software. The first is an html description built from the OCRtext and images stored in the Topaz file (HTMLZ). This format is easily reflowed but may suffer from typical OCRtext errors including typos, garbled text, missing italics, missing bolds, etc. The second format uses the glyph and position information to create an accurate scalable vector graphics (SVG) image of each page of the book that can be viewed in web browsers that support svg images (Safari, Firefox 4 or later, etc). Additional conversion software can be used to convert these SVG images to an image only PDF file. The DeDRM calibre plugin only imports the HTMLZ versions of the Topaz ebook. The html version can be manually cleaned up and spell checked and then converted using Sigil/calibre to epubs, mobi ebooks, and etc.
## 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 tools linked from this blog remove DRM from PDF, ePub, kePub (Kobo), eReader, Kindle (Mobipocket, KF8, Print Replica and Topaz) format ebooks using Adobe Adept, Barnes & Noble, Amazon, Kobo and eReader DRM schemes.
The Calibre plugin removes DRM from PDF, ePub, kePub (Kobo), eReader, Kindle (Mobipocket, KF8, Print Replica and Topaz) format ebooks using Adobe Adept, Barnes & Noble, 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 books owner to use the encryption key information already stored someplace on their computer or device to decrypt the ebook in the same manner the official ebook reading software uses.
@ -214,22 +182,26 @@ 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. Apples 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 GitHub repository no longer contains the code needed to remove DRM from LCP-encrypted books. For more information please read [this bug report](https://github.com/noDRM/DeDRM_tools/issues/18).
## Ive got the tools archive and Ive read all the FAQs but I still cant install the tools and/or the DRM removal doesnt work
* Read the ReadMe_First.txt file in the top level of the tools archive
* Read the ReadMe file in the folder of the tools you want to use.
* If you still cant remove the DRM, ask in the comments section of Apprentice Alf's blog or create a new issue at Apprentice Harper's github repository, reporting the error as precisely as you can, what platform you use, what tool you have tried, what errors you get, and what versions you are using. If the problem happens when running one of the tools, post a log (see previous questions on how to do this).
* Read the `ReadMe_Overview.txt` file in the top level of the tools archive
* Read the ReadMe file for the tool you want to use.
* If you still cant remove the DRM, create a new [GitHub issue](https://github.com/noDRM/DeDRM_tools/issues). If you are using Apprentice Harper's original version and not this fork, you can also create a new issue at Apprentice Harper's github repository. If you do report an issue in any of the GitHub repositories, please report the error as precisely as you can. Include what platform you use, what tool you have tried, what errors you get, and what versions you are using. If the problem happens when running one of the tools, post a log (see previous questions on how to do this).
## Who wrote these scripts?
The authors tend to identify themselves only by pseudonyms:
* The Adobe Adept and Barnes & Noble scripts were created by i♥cabbages
* The Adobe Adept support for ADE3.0+ DRM was added by a980e066a01
* ~The Readium LCP support for this plugin was created by NoDRM~ (removed due to a DMCA takedown, see [#18](https://github.com/noDRM/DeDRM_tools/issues/18) )
* The 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
* The Amazon Topaz DRM removal script was created by CMBDTC
* The Amazon Topaz format conversion was created by some_updates, clarknova, and Bart Simpson
* The DeDRM all-in-one AppleScript application was created by Apprentice Alf
* The DeDRM all-in-one Python application was created by some_updates
* The DeDRM all-in-one calibre plugin was created by Apprentice Alf
* The support for .kinf2018 key files and KFX 2&3 was by Apprentice Sakuya
* The Scuolabooks tool was created by Hex
* The Microsoft code was created by drs
* The Apple DRM removal tool was created by Brahms

View file

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

View file

@ -1,18 +1,19 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
_license__ = 'GPL v3'
__docformat__ = 'restructuredtext en'
import codecs
import os, traceback, zipfile
try:
from PyQt5.Qt import QToolButton, QUrl
except ImportError:
from PyQt4.Qt import QToolButton, QUrl
from calibre.gui2 import open_url, question_dialog
from calibre.gui2.actions import InterfaceAction
from calibre.utils.config import config_dir
@ -24,7 +25,7 @@ from calibre.ebooks.metadata.meta import get_metadata
from calibre_plugins.obok_dedrm.dialogs import (SelectionDialog, DecryptAddProgressDialog,
AddEpubFormatsProgressDialog, ResultsSummaryDialog)
from calibre_plugins.obok_dedrm.config import plugin_prefs as cfg
from calibre_plugins.obok_dedrm.__init__ import (PLUGIN_NAME, PLUGIN_SAFE_NAME,
from calibre_plugins.obok_dedrm.__init__ import (PLUGIN_NAME, PLUGIN_SAFE_NAME,
PLUGIN_VERSION, PLUGIN_DESCRIPTION, HELPFILE_NAME)
from calibre_plugins.obok_dedrm.utilities import (
get_icon, set_plugin_icon_resources, showErrorDlg, format_plural,
@ -53,7 +54,7 @@ class InterfacePluginAction(InterfaceAction):
def genesis(self):
icon_resources = self.load_resources(PLUGIN_ICONS)
set_plugin_icon_resources(PLUGIN_NAME, icon_resources)
self.qaction.setIcon(get_icon(PLUGIN_ICONS[0]))
self.qaction.triggered.connect(self.launchObok)
self.gui.keyboard.finalize()
@ -93,7 +94,7 @@ class InterfacePluginAction(InterfaceAction):
debug_print("Exception getting device path. Probably not an E-Ink Kobo device")
# Get the Kobo Library object (obok v3.01)
self.library = KoboLibrary(tmpserials, device_path)
self.library = KoboLibrary(tmpserials, device_path, cfg['kobo_directory'])
debug_print ("got kobodir %s" % self.library.kobodir)
if (self.library.kobodir == ''):
# linux and no device connected, but could be extended
@ -106,10 +107,10 @@ class InterfacePluginAction(InterfaceAction):
# Get a list of Kobo titles
books = self.build_book_list()
if len(books) < 1:
msg = _('<p>No books found in Kobo Library\nAre you sure it\'s installed\configured\synchronized?')
msg = _('<p>No books found in Kobo Library\nAre you sure it\'s installed/configured/synchronized?')
showErrorDlg(msg, None)
return
# Check to see if a key can be retrieved using the legacy obok method.
legacy_key = legacy_obok().get_legacy_cookie_id
if legacy_key is not None:
@ -154,7 +155,7 @@ class InterfacePluginAction(InterfaceAction):
# Close Kobo Library object
self.library.close()
# If we have decrypted books to work with, feed the list of decrypted books details
# If we have decrypted books to work with, feed the list of decrypted books details
# and the callback function (self.add_new_books) to the ProgressDialog dispatcher.
if len(self.books_to_add):
d = DecryptAddProgressDialog(self.gui, self.books_to_add, self.add_new_books, self.db, 'calibre',
@ -196,7 +197,7 @@ class InterfacePluginAction(InterfaceAction):
# We will write the help file out every time, in case the user upgrades the plugin zip
# and there is a newer help file contained within it.
file_path = os.path.join(config_dir, 'plugins', HELPFILE_NAME)
file_data = self.load_resources(HELPFILE_NAME)[HELPFILE_NAME]
file_data = self.load_resources(HELPFILE_NAME)[HELPFILE_NAME].decode('utf-8')
with open(file_path,'w') as f:
f.write(file_data)
return file_path
@ -212,7 +213,7 @@ class InterfacePluginAction(InterfaceAction):
def get_decrypted_kobo_books(self, book):
'''
This method is a call-back function used by DecryptAddProgressDialog in dialogs.py to decrypt Kobo books
:param book: A KoboBook object that is to be decrypted.
'''
print (_('{0} - Decrypting {1}').format(PLUGIN_NAME + ' v' + PLUGIN_VERSION, book.title))
@ -233,10 +234,13 @@ class InterfacePluginAction(InterfaceAction):
'''
This method is a call-back function used by DecryptAddProgressDialog in dialogs.py to add books to calibre
(It's set up to handle multiple books, but will only be fed books one at a time by DecryptAddProgressDialog)
:param books_to_add: List of calibre bookmaps (created in get_decrypted_kobo_books)
'''
added = self.db.add_books(books_to_add, add_duplicates=False, run_hooks=False)
cfg_add_duplicates = (cfg['finding_homes_for_formats'] == 'Add new entry')
added = self.db.add_books(books_to_add, add_duplicates=cfg_add_duplicates, run_hooks=False)
if len(added[0]):
# Record the id(s) that got added
for id in added[0]:
@ -253,7 +257,7 @@ class InterfacePluginAction(InterfaceAction):
def add_epub_format(self, book_id, mi, path):
'''
This method is a call-back function used by AddEpubFormatsProgressDialog in dialogs.py
:param book_id: calibre ID of the book to add the encrypted epub to.
:param mi: calibre metadata object
:param path: path to the decrypted epub (temp file)
@ -281,7 +285,7 @@ class InterfacePluginAction(InterfaceAction):
self.formats_to_add.append((home_id, mi, tmp_file))
else:
self.no_home_for_book.append(mi)
# If we found homes for decrypted epubs in existing calibre entries, feed the list of decrypted book
# If we found homes for decrypted epubs in existing calibre entries, feed the list of decrypted book
# details and the callback function (self.add_epub_format) to the ProgressDialog dispatcher.
if self.formats_to_add:
d = AddEpubFormatsProgressDialog(self.gui, self.formats_to_add, self.add_epub_format)
@ -306,10 +310,10 @@ class InterfacePluginAction(InterfaceAction):
sd = ResultsSummaryDialog(self.gui, caption, msg, log)
sd.exec_()
return
def ask_about_inserting_epubs(self):
'''
Build question dialog with details about kobo books
Build question dialog with details about kobo books
that couldn't be added to calibre as new books.
'''
''' Terisa: Improve the message
@ -327,13 +331,13 @@ class InterfacePluginAction(InterfaceAction):
msg = _('<p><b>{0}</b> -- not added because of {1} in your library.<br /><br />').format(self.duplicate_book_list[0][0].title, self.duplicate_book_list[0][2])
msg += _('Would you like to try and add the EPUB format to an available calibre duplicate?<br /><br />')
msg += _('NOTE: no pre-existing EPUB will be overwritten.')
return question_dialog(self.gui, caption, msg, det_msg)
def find_a_home(self, ids):
'''
Find the ID of the first EPUB-Free duplicate available
:param ids: List of calibre IDs that might serve as a home.
'''
for id in ids:
@ -370,11 +374,14 @@ class InterfacePluginAction(InterfaceAction):
result['success'] = False
result['fileobj'] = None
zin = zipfile.ZipFile(book.filename, 'r')
try:
zin = zipfile.ZipFile(book.filename, 'r')
except FileNotFoundError:
print (_('{0} - File "{1}" not found. Make sure the eBook has been properly downloaded in the Kobo app.').format(PLUGIN_NAME, book.filename))
return result
#print ('Kobo library filename: {0}'.format(book.filename))
for userkey in self.userkeys:
print (_('Trying key: '), userkey.encode('hex_codec'))
check = True
print (_('Trying key: '), codecs.encode(userkey, 'hex'))
try:
fileout = PersistentTemporaryFile('.epub', dir=self.tdir)
#print ('Temp file: {0}'.format(fileout.name))
@ -395,8 +402,7 @@ class InterfacePluginAction(InterfaceAction):
file = book.encryptedfiles[filename]
contents = file.decrypt(userkey, contents)
# Parse failures mean the key is probably wrong.
if check:
check = not file.check(contents)
file.check(contents)
zout.writestr(filename, contents)
zout.close()
zin.close()
@ -455,7 +461,7 @@ class InterfacePluginAction(InterfaceAction):
if cancelled_count > 0:
log += _('<p><b>Book imports cancelled by user:</b> {}</p>\n').format(cancelled_count)
return (msg, log)
log += _('<p><b>New EPUB formats inserted in existing calibre books:</b> {0}</p>\n').format(len(self.successful_format_adds))
log += _('<p><b>New EPUB formats inserted in existing calibre books:</b> {0}</p>\n').format(len(self.successful_format_adds))
if self.successful_format_adds:
log += '<ul>\n'
for id, mi in self.successful_format_adds:
@ -474,7 +480,7 @@ class InterfacePluginAction(InterfaceAction):
log += _('<p><b>Format imports cancelled by user:</b> {}</p>\n').format(cancelled_count)
return (msg, log)
else:
# Single book ... don't get fancy.
if self.ids_of_new_books:
title = self.ids_of_new_books[0][1].title
@ -494,4 +500,4 @@ class InterfacePluginAction(InterfaceAction):
reason = _('of unknown reasons. Gosh I\'m embarrassed!')
msg = _('<p>{0} not added because {1}').format(title, reason)
return (msg, log)

View file

@ -1,20 +1,14 @@
#!/usr/bin/env python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2012, David Forrester <davidfor@internode.on.net>'
__docformat__ = 'restructuredtext en'
import os, time, re, sys
try:
from PyQt5.Qt import (Qt, QIcon, QPixmap, QLabel, QDialog, QHBoxLayout, QProgressBar,
QTableWidgetItem, QFont, QLineEdit, QComboBox,
QVBoxLayout, QDialogButtonBox, QStyledItemDelegate, QDateTime,
QRegExpValidator, QRegExp, QDate, QDateEdit)
except ImportError:
from PyQt4.Qt import (Qt, QIcon, QPixmap, QLabel, QDialog, QHBoxLayout, QProgressBar,
from datetime import datetime
from PyQt5.Qt import (Qt, QIcon, QPixmap, QLabel, QDialog, QHBoxLayout, QProgressBar,
QTableWidgetItem, QFont, QLineEdit, QComboBox,
QVBoxLayout, QDialogButtonBox, QStyledItemDelegate, QDateTime,
QRegExpValidator, QRegExp, QDate, QDateEdit)
@ -271,13 +265,13 @@ class ReadOnlyTableWidgetItem(QTableWidgetItem):
def __init__(self, text):
if text is None:
text = ''
QTableWidgetItem.__init__(self, text, QTableWidgetItem.UserType)
QTableWidgetItem.__init__(self, text, QTableWidgetItem.ItemType.UserType)
self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)
class RatingTableWidgetItem(QTableWidgetItem):
def __init__(self, rating, is_read_only=False):
QTableWidgetItem.__init__(self, '', QTableWidgetItem.UserType)
QTableWidgetItem.__init__(self, '', QTableWidgetItem.ItemType.UserType)
self.setData(Qt.DisplayRole, rating)
if is_read_only:
self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)
@ -290,11 +284,11 @@ class DateTableWidgetItem(QTableWidgetItem):
if date_read is None or date_read == UNDEFINED_DATE and default_to_today:
date_read = now()
if is_read_only:
QTableWidgetItem.__init__(self, format_date(date_read, fmt), QTableWidgetItem.UserType)
QTableWidgetItem.__init__(self, format_date(date_read, fmt), QTableWidgetItem.ItemType.UserType)
self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)
self.setData(Qt.DisplayRole, QDateTime(date_read))
else:
QTableWidgetItem.__init__(self, '', QTableWidgetItem.UserType)
QTableWidgetItem.__init__(self, '', QTableWidgetItem.ItemType.UserType)
self.setData(Qt.DisplayRole, QDateTime(date_read))
from calibre.gui2.library.delegates import DateDelegate as _DateDelegate
@ -426,7 +420,7 @@ class KeyValueComboBox(QComboBox):
def selected_key(self):
for key, value in self.values.iteritems():
if value == unicode(self.currentText()).strip():
if value == self.currentText().strip():
return key
@ -449,7 +443,7 @@ class KeyComboBox(QComboBox):
def selected_key(self):
for key, value in self.values.iteritems():
if key == unicode(self.currentText()).strip():
if key == self.currentText().strip():
return key

View file

@ -1,16 +1,10 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import (unicode_literals, division, absolute_import,
print_function)
try:
from PyQt5.Qt import (Qt, QGroupBox, QListWidget, QLineEdit, QDialogButtonBox, QWidget, QLabel, QDialog, QVBoxLayout, QAbstractItemView, QIcon, QHBoxLayout, QComboBox, QListWidgetItem)
except ImportError:
from PyQt4.Qt import (Qt, QGroupBox, QListWidget, QLineEdit, QDialogButtonBox, QWidget, QLabel, QDialog, QVBoxLayout, QAbstractItemView, QIcon, QHBoxLayout, QComboBox, QListWidgetItem)
try:
from PyQt5 import Qt as QtGui
except ImportError:
from PyQt4 import QtGui
from PyQt5.Qt import (Qt, QGroupBox, QListWidget, QLineEdit, QDialogButtonBox, QWidget, QLabel, QDialog, QVBoxLayout, QAbstractItemView, QIcon, QHBoxLayout, QComboBox, QListWidgetItem, QFileDialog)
from PyQt5 import Qt as QtGui
from calibre.gui2 import (error_dialog, question_dialog, info_dialog, open_url)
from calibre.utils.config import JSONConfig, config_dir
@ -18,6 +12,7 @@ from calibre.utils.config import JSONConfig, config_dir
plugin_prefs = JSONConfig('plugins/obok_dedrm_prefs')
plugin_prefs.defaults['finding_homes_for_formats'] = 'Ask'
plugin_prefs.defaults['kobo_serials'] = []
plugin_prefs.defaults['kobo_directory'] = u''
from calibre_plugins.obok_dedrm.__init__ import PLUGIN_NAME, PLUGIN_VERSION
from calibre_plugins.obok_dedrm.utilities import (debug_print)
@ -37,31 +32,69 @@ class ConfigWidget(QWidget):
# copy of preferences
self.tmpserials = plugin_prefs['kobo_serials']
self.kobodirectory = plugin_prefs['kobo_directory']
combo_label = QLabel(_('When should Obok try to insert EPUBs into existing calibre entries?'), self)
layout.addWidget(combo_label)
self.find_homes = QComboBox()
self.find_homes.setToolTip(_('<p>Default behavior when duplicates are detected. None of the choices will cause calibre ebooks to be overwritten'))
layout.addWidget(self.find_homes)
self.find_homes.addItems([_('Ask'), _('Always'), _('Never')])
self.find_homes.addItems([_('Ask'), _('Always'), _('Never'), _('Add new entry')])
index = self.find_homes.findText(plugin_prefs['finding_homes_for_formats'])
if index == -1:
index = self.find_homes.findText(_(plugin_prefs['finding_homes_for_formats']))
self.find_homes.setCurrentIndex(index)
self.serials_button = QtGui.QPushButton(self)
self.serials_button.setToolTip(_(u"Click to manage Kobo serial numbers for Kobo ebooks"))
self.serials_button.setText(u"Kobo devices serials")
self.serials_button.setToolTip(_("Click to manage Kobo serial numbers for Kobo ebooks"))
self.serials_button.setText("Kobo devices serials")
self.serials_button.clicked.connect(self.edit_serials)
layout.addWidget(self.serials_button)
self.kobo_directory_button = QtGui.QPushButton(self)
self.kobo_directory_button.setToolTip(_("Click to specify the Kobo directory"))
self.kobo_directory_button.setText("Kobo directory")
self.kobo_directory_button.clicked.connect(self.edit_kobo_directory)
layout.addWidget(self.kobo_directory_button)
def edit_serials(self):
d = ManageKeysDialog(self,u"Kobo device serial numbers",self.tmpserials, AddSerialDialog)
d = ManageKeysDialog(self,"Kobo device serial number",self.tmpserials, AddSerialDialog)
d.exec_()
def edit_kobo_directory(self):
tmpkobodirectory = QFileDialog.getExistingDirectory(self, "Select Kobo directory", self.kobodirectory or "/home", QFileDialog.Option.ShowDirsOnly)
if tmpkobodirectory != u"" and tmpkobodirectory is not None:
self.kobodirectory = tmpkobodirectory
def save_settings(self):
plugin_prefs['finding_homes_for_formats'] = unicode(self.find_homes.currentText())
# Make sure the config file string is *always* english.
find_homes = None
if self.find_homes.currentText() == _('Ask'):
find_homes = 'Ask'
elif self.find_homes.currentText() == _('Always'):
find_homes = 'Always'
elif self.find_homes.currentText() == _('Never'):
find_homes = 'Never'
elif self.find_homes.currentText() == _('Add new entry'):
find_homes = 'Add new entry'
if find_homes is None:
# Fallback
find_homes = self.find_homes.currentText()
plugin_prefs['finding_homes_for_formats'] = find_homes
plugin_prefs['kobo_serials'] = self.tmpserials
plugin_prefs['kobo_directory'] = self.kobodirectory
@ -74,7 +107,7 @@ class ManageKeysDialog(QDialog):
self.plugin_keys = plugin_keys
self.create_key = create_key
self.keyfile_ext = keyfile_ext
self.json_file = (keyfile_ext == u"k4i")
self.json_file = (keyfile_ext == "k4i")
self.setWindowTitle("{0} {1}: Manage {2}s".format(PLUGIN_NAME, PLUGIN_VERSION, self.key_type_name))
@ -82,13 +115,13 @@ class ManageKeysDialog(QDialog):
layout = QVBoxLayout(self)
self.setLayout(layout)
keys_group_box = QGroupBox(_(u"{0}s".format(self.key_type_name)), self)
keys_group_box = QGroupBox(_("{0}s".format(self.key_type_name)), self)
layout.addWidget(keys_group_box)
keys_group_box_layout = QHBoxLayout()
keys_group_box.setLayout(keys_group_box_layout)
self.listy = QListWidget(self)
self.listy.setToolTip(u"{0}s that will be used to decrypt ebooks".format(self.key_type_name))
self.listy.setToolTip("{0}s that will be used to decrypt ebooks".format(self.key_type_name))
self.listy.setSelectionMode(QAbstractItemView.SingleSelection)
self.populate_list()
keys_group_box_layout.addWidget(self.listy)
@ -97,17 +130,23 @@ class ManageKeysDialog(QDialog):
keys_group_box_layout.addLayout(button_layout)
self._add_key_button = QtGui.QToolButton(self)
self._add_key_button.setIcon(QIcon(I('plus.png')))
self._add_key_button.setToolTip(u"Create new {0}".format(self.key_type_name))
self._add_key_button.setToolTip("Create new {0}".format(self.key_type_name))
self._add_key_button.clicked.connect(self.add_key)
button_layout.addWidget(self._add_key_button)
self._delete_key_button = QtGui.QToolButton(self)
self._delete_key_button.setToolTip(_(u"Delete highlighted key"))
self._delete_key_button.setToolTip(_("Delete highlighted key"))
self._delete_key_button.setIcon(QIcon(I('list_remove.png')))
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)
@ -138,40 +177,18 @@ class ManageKeysDialog(QDialog):
new_key_value = d.key_value
if new_key_value in self.plugin_keys:
info_dialog(None, "{0} {1}: Duplicate {2}".format(PLUGIN_NAME, PLUGIN_VERSION,self.key_type_name),
u"This {0} is already in the list of {0}s has not been added.".format(self.key_type_name), show=True)
"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()
def rename_key(self):
if not self.listy.currentItem():
errmsg = u"No {0} selected to rename. Highlight a keyfile first.".format(self.key_type_name)
r = error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION),
_(errmsg), show=True, show_copy_button=False)
return
d = RenameKeyDialog(self)
d.exec_()
if d.result() != d.Accepted:
# rename cancelled or moot.
return
keyname = unicode(self.listy.currentItem().text())
if not question_dialog(self, "{0} {1}: Confirm Rename".format(PLUGIN_NAME, PLUGIN_VERSION), u"Do you really want to rename the {2} named <strong>{0}</strong> to <strong>{1}</strong>?".format(keyname,d.key_name,self.key_type_name), show_copy_button=False, default_yes=False):
return
self.plugin_keys[d.key_name] = self.plugin_keys[keyname]
del self.plugin_keys[keyname]
self.listy.clear()
self.populate_list()
def delete_key(self):
if not self.listy.currentItem():
return
keyname = unicode(self.listy.currentItem().text())
if not question_dialog(self, "{0} {1}: Confirm Delete".format(PLUGIN_NAME, PLUGIN_VERSION), u"Do you really want to delete the {1} <strong>{0}</strong>?".format(keyname, self.key_type_name), show_copy_button=False, default_yes=False):
keyname = self.listy.currentItem().text()
if not question_dialog(self, "{0} {1}: Confirm Delete".format(PLUGIN_NAME, PLUGIN_VERSION), "Do you really want to delete the {1} <strong>{0}</strong>?".format(keyname, self.key_type_name), show_copy_button=False, default_yes=False):
return
self.plugin_keys.remove(keyname)
@ -182,7 +199,7 @@ class AddSerialDialog(QDialog):
def __init__(self, parent=None,):
QDialog.__init__(self, parent)
self.parent = parent
self.setWindowTitle(u"{0} {1}: Add New eInk Kobo Serial Number".format(PLUGIN_NAME, PLUGIN_VERSION))
self.setWindowTitle("{0} {1}: Add New eInk Kobo Serial Number".format(PLUGIN_NAME, PLUGIN_VERSION))
layout = QVBoxLayout(self)
self.setLayout(layout)
@ -193,9 +210,9 @@ class AddSerialDialog(QDialog):
key_group = QHBoxLayout()
data_group_box_layout.addLayout(key_group)
key_group.addWidget(QLabel(u"EInk Kobo Serial Number:", self))
key_group.addWidget(QLabel("EInk Kobo Serial Number:", self))
self.key_ledit = QLineEdit("", self)
self.key_ledit.setToolTip(u"Enter an eInk Kobo serial number. EInk Kobo serial numbers are 13 characters long and usually start with a 'N'. Kobo Serial Numbers are case-sensitive, so be sure to enter the upper and lower case letters unchanged.")
self.key_ledit.setToolTip("Enter an eInk Kobo serial number. EInk Kobo serial numbers are 13 characters long and usually start with a 'N'. Kobo Serial Numbers are case-sensitive, so be sure to enter the upper and lower case letters unchanged.")
key_group.addWidget(self.key_ledit)
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
@ -207,17 +224,17 @@ class AddSerialDialog(QDialog):
@property
def key_name(self):
return unicode(self.key_ledit.text()).strip()
return self.key_ledit.text().strip()
@property
def key_value(self):
return unicode(self.key_ledit.text()).strip()
return self.key_ledit.text().strip()
def accept(self):
if len(self.key_name) == 0 or self.key_name.isspace():
errmsg = u"Please enter an eInk Kindle Serial Number or click Cancel in the dialog."
errmsg = "Please enter an eInk Kobo Serial Number or click Cancel in the dialog."
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
if len(self.key_name) != 13:
errmsg = u"EInk Kobo Serial Numbers must be 13 characters long. This is {0:d} characters long.".format(len(self.key_name))
errmsg = "EInk Kobo Serial Numbers must be 13 characters long. This is {0:d} characters long.".format(len(self.key_name))
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
QDialog.accept(self)

View file

@ -409,7 +409,7 @@ class ReadOnlyTableWidgetItem(QTableWidgetItem):
def __init__(self, text):
if text is None:
text = ''
QTableWidgetItem.__init__(self, text, QTableWidgetItem.UserType)
QTableWidgetItem.__init__(self, text, QTableWidgetItem.ItemType.UserType)
self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)
class AuthorTableWidgetItem(ReadOnlyTableWidgetItem):
@ -448,7 +448,7 @@ class IconWidgetItem(ReadOnlyTableWidgetItem):
class NumericTableWidgetItem(QTableWidgetItem):
def __init__(self, number, is_read_only=False):
QTableWidgetItem.__init__(self, '', QTableWidgetItem.UserType)
QTableWidgetItem.__init__(self, '', QTableWidgetItem.ItemType.UserType)
self.setData(Qt.DisplayRole, number)
if is_read_only:
self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)

View file

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View file

@ -37,14 +37,17 @@ class legacy_obok(object):
def __oldcookiedeviceid(self):
'''Optionally attempt to get a device id using the old cookie method.
Must have _winreg installed on Windows machines for successful key retrieval.'''
Must have winreg installed on Windows machines for successful key retrieval.'''
wsuid = ''
pwsdid = ''
try:
if sys.platform.startswith('win'):
import _winreg
regkey_browser = _winreg.OpenKey(_winreg.HKEY_CURRENT_USER, 'Software\\Kobo\\Kobo Desktop Edition\\Browser')
cookies = _winreg.QueryValueEx(regkey_browser, 'cookies')
try:
import winreg
except ImportError:
import _winreg as winreg
regkey_browser = winreg.OpenKey(winreg.HKEY_CURRENT_USER, 'Software\\Kobo\\Kobo Desktop Edition\\Browser')
cookies = winreg.QueryValueEx(regkey_browser, 'cookies')
bytearrays = cookies[0]
elif sys.platform.startswith('darwin'):
prefs = os.path.join(os.environ['HOME'], 'Library/Preferences/com.kobo.Kobo Desktop Edition.plist')

View file

@ -1,6 +1,22 @@
#!/usr/bin/env python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Version 10.0.3 July 2022
# Fix Calibre 6
#
# Version 10.0.1 February 2022
# Remove OpenSSL support to only support PyCryptodome; clean up the code.
#
# Version 10.0.0 November 2021
# Merge https://github.com/apprenticeharper/DeDRM_tools/pull/1691 to fix
# key fetch issues on some machines.
#
# Version 4.1.0 February 2021
# Add detection for Kobo directory location on Linux
#
# Version 4.0.0 September 2020
# Python 3.0
#
# Version 3.2.5 December 2016
# Improve detection of good text decryption.
#
@ -150,9 +166,10 @@
# after all.
#
"""Manage all Kobo books, either encrypted or DRM-free."""
from __future__ import print_function
__version__ = '3.2.4'
__about__ = u"Obok v{0}\nCopyright © 2012-2016 Physisticated et al.".format(__version__)
__version__ = '10.0.9'
__about__ = "Obok v{0}\nCopyright © 2012-2023 Physisticated et al.".format(__version__)
import sys
import os
@ -169,13 +186,27 @@ import shutil
import argparse
import tempfile
try:
from Cryptodome.Cipher import AES
except ImportError:
from Crypto.Cipher import AES
def unpad(data, padding=16):
if sys.version_info[0] == 2:
pad_len = ord(data[-1])
else:
pad_len = data[-1]
return data[:-pad_len]
can_parse_xml = True
try:
from xml.etree import ElementTree as ET
# print u"using xml.etree for xml parsing"
# print "using xml.etree for xml parsing"
except ImportError:
can_parse_xml = False
# print u"Cannot find xml.etree, disabling extraction of serial numbers"
# print "Cannot find xml.etree, disabling extraction of serial numbers"
# List of all known hash keys
KOBO_HASH_KEYS = ['88b3a2e13', 'XzUhGYdFp', 'NoCanLook','QJhwzAtXL']
@ -183,88 +214,6 @@ KOBO_HASH_KEYS = ['88b3a2e13', 'XzUhGYdFp', 'NoCanLook','QJhwzAtXL']
class ENCRYPTIONError(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 sys.platform.startswith('win'):
libcrypto = find_library('libeay32')
else:
libcrypto = find_library('crypto')
if libcrypto is None:
raise ENCRYPTIONError('libcrypto not found')
libcrypto = CDLL(libcrypto)
AES_MAXNR = 14
c_char_pp = POINTER(c_char_p)
c_int_p = POINTER(c_int)
class AES_KEY(Structure):
_fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))),
('rounds', c_int)]
AES_KEY_p = POINTER(AES_KEY)
def F(restype, name, argtypes):
func = getattr(libcrypto, name)
func.restype = restype
func.argtypes = argtypes
return func
AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key',
[c_char_p, c_int, AES_KEY_p])
AES_ecb_encrypt = F(None, 'AES_ecb_encrypt',
[c_char_p, c_char_p, AES_KEY_p, c_int])
class AES(object):
def __init__(self, userkey):
self._blocksize = len(userkey)
if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) :
raise ENCRYPTIONError(_('AES improper key used'))
return
key = self._key = AES_KEY()
rv = AES_set_decrypt_key(userkey, len(userkey) * 8, key)
if rv < 0:
raise ENCRYPTIONError(_('Failed to initialize AES key'))
def decrypt(self, data):
clear = ''
for i in range(0, len(data), 16):
out = create_string_buffer(16)
rv = AES_ecb_encrypt(data[i:i+16], out, self._key, 0)
if rv == 0:
raise ENCRYPTIONError(_('AES decryption failed'))
clear += out.raw
return clear
return AES
def _load_crypto_pycrypto():
from Crypto.Cipher import AES as _AES
class AES(object):
def __init__(self, key):
self._aes = _AES.new(key, _AES.MODE_ECB)
def decrypt(self, data):
return self._aes.decrypt(data)
return AES
def _load_crypto():
AES = None
cryptolist = (_load_crypto_pycrypto, _load_crypto_libcrypto)
for loader in cryptolist:
try:
AES = loader()
break
except (ImportError, ENCRYPTIONError):
pass
return AES
AES = _load_crypto()
# Wrap a stream so that output gets flushed immediately
# and also make sure that any unicode strings get
# encoded using "replace" before writing them.
@ -275,10 +224,17 @@ class SafeUnbuffered:
if self.encoding == None:
self.encoding = "utf-8"
def write(self, data):
if isinstance(data,unicode):
if isinstance(data,str) or isinstance(data,unicode):
# str for Python3, unicode for Python2
data = data.encode(self.encoding,"replace")
self.stream.write(data)
self.stream.flush()
try:
buffer = getattr(self.stream, 'buffer', self.stream)
# self.stream.buffer for Python3, self.stream for Python2
buffer.write(data)
buffer.flush()
except:
# We can do nothing if a write fails
raise
def __getattr__(self, attr):
return getattr(self.stream, attr)
@ -290,8 +246,8 @@ class KoboLibrary(object):
written by the Kobo Desktop Edition application, including the list
of books, their titles, and the user's encryption key(s)."""
def __init__ (self, serials = [], device_path = None):
print __about__
def __init__ (self, serials = [], device_path = None, desktopkobodir = u""):
print(__about__)
self.kobodir = u""
kobodb = u""
@ -308,9 +264,9 @@ class KoboLibrary(object):
# step 1. check whether this looks like a real device
if (device_path):
# we got a device path
self.kobodir = os.path.join(device_path, u".kobo")
self.kobodir = os.path.join(device_path, ".kobo")
# devices use KoboReader.sqlite
kobodb = os.path.join(self.kobodir, u"KoboReader.sqlite")
kobodb = os.path.join(self.kobodir, "KoboReader.sqlite")
if (not(os.path.isfile(kobodb))):
# device path seems to be wrong, unset it
device_path = u""
@ -322,67 +278,104 @@ class KoboLibrary(object):
if (len(serials) == 0):
# we got a device path but no saved serial
# try to get the serial from the device
# print u"get_device_settings - device_path = {0}".format(device_path)
# print "get_device_settings - device_path = {0}".format(device_path)
# get serial from device_path/.adobe-digital-editions/device.xml
if can_parse_xml:
devicexml = os.path.join(device_path, '.adobe-digital-editions', 'device.xml')
# print u"trying to load {0}".format(devicexml)
# print "trying to load {0}".format(devicexml)
if (os.path.exists(devicexml)):
# print u"trying to parse {0}".format(devicexml)
# print "trying to parse {0}".format(devicexml)
xmltree = ET.parse(devicexml)
for node in xmltree.iter():
if "deviceSerial" in node.tag:
serial = node.text
# print u"found serial {0}".format(serial)
# print "found serial {0}".format(serial)
serials.append(serial)
break
else:
# print u"cannot get serials from device."
# print "cannot get serials from device."
device_path = u""
self.kobodir = u""
kobodb = u""
if (self.kobodir == u""):
# step 4. we haven't found a device with serials, so try desktop apps
if sys.platform.startswith('win'):
import _winreg as winreg
if sys.getwindowsversion().major > 5:
if 'LOCALAPPDATA' in os.environ.keys():
# Python 2.x does not return unicode env. Use Python 3.x
self.kobodir = winreg.ExpandEnvironmentStrings(u"%LOCALAPPDATA%")
if (self.kobodir == u""):
if 'USERPROFILE' in os.environ.keys():
# Python 2.x does not return unicode env. Use Python 3.x
self.kobodir = os.path.join(winreg.ExpandEnvironmentStrings(u"%USERPROFILE%"), u"Local Settings", u"Application Data")
self.kobodir = os.path.join(self.kobodir, u"Kobo", u"Kobo Desktop Edition")
elif sys.platform.startswith('darwin'):
self.kobodir = os.path.join(os.environ['HOME'], u"Library", u"Application Support", u"Kobo", u"Kobo Desktop Edition")
#elif linux_path != None:
# Probably Linux, let's get the wine prefix and path to Kobo.
# self.kobodir = os.path.join(linux_path, u"Local Settings", u"Application Data", u"Kobo", u"Kobo Desktop Edition")
if desktopkobodir != u'':
self.kobodir = desktopkobodir
if (self.kobodir == u""):
if sys.platform.startswith('win'):
try:
import winreg
except ImportError:
import _winreg as winreg
if sys.getwindowsversion().major > 5:
if 'LOCALAPPDATA' in os.environ.keys():
# Python 2.x does not return unicode env. Use Python 3.x
if sys.version_info[0] == 2:
self.kobodir = winreg.ExpandEnvironmentStrings(u"%LOCALAPPDATA%")
else:
self.kobodir = winreg.ExpandEnvironmentStrings("%LOCALAPPDATA%")
if (self.kobodir == u""):
if 'USERPROFILE' in os.environ.keys():
# Python 2.x does not return unicode env. Use Python 3.x
if sys.version_info[0] == 2:
self.kobodir = os.path.join(winreg.ExpandEnvironmentStrings(u"%USERPROFILE%"), "Local Settings", "Application Data")
else:
self.kobodir = os.path.join(winreg.ExpandEnvironmentStrings("%USERPROFILE%"), "Local Settings", "Application Data")
self.kobodir = os.path.join(self.kobodir, "Kobo", "Kobo Desktop Edition")
elif sys.platform.startswith('darwin'):
self.kobodir = os.path.join(os.environ['HOME'], "Library", "Application Support", "Kobo", "Kobo Desktop Edition")
elif sys.platform.startswith('linux'):
#sets ~/.config/calibre as the location to store the kobodir location info file and creates this directory if necessary
kobodir_cache_dir = os.path.join(os.environ['HOME'], ".config", "calibre")
if not os.path.isdir(kobodir_cache_dir):
os.mkdir(kobodir_cache_dir)
#appends the name of the file we're storing the kobodir location info to the above path
kobodir_cache_file = str(kobodir_cache_dir) + "/" + "kobo location"
"""if the above file does not exist, recursively searches from the root
of the filesystem until kobodir is found and stores the location of kobodir
in that file so this loop can be skipped in the future"""
original_stdout = sys.stdout
if not os.path.isfile(kobodir_cache_file):
for root, dirs, files in os.walk('/'):
for file in files:
if file == 'Kobo.sqlite':
kobo_linux_path = str(root)
with open(kobodir_cache_file, 'w') as f:
sys.stdout = f
print(kobo_linux_path, end='')
sys.stdout = original_stdout
f = open(kobodir_cache_file, 'r' )
self.kobodir = f.read()
# desktop versions use Kobo.sqlite
kobodb = os.path.join(self.kobodir, u"Kobo.sqlite")
kobodb = os.path.join(self.kobodir, "Kobo.sqlite")
# check for existence of file
if (not(os.path.isfile(kobodb))):
# give up here, we haven't found anything useful
self.kobodir = u""
kobodb = u""
if (self.kobodir != u""):
self.bookdir = os.path.join(self.kobodir, u"kepub")
self.bookdir = os.path.join(self.kobodir, "kepub")
# make a copy of the database in a temporary file
# so we can ensure it's not using WAL logging which sqlite3 can't do.
self.newdb = tempfile.NamedTemporaryFile(mode='wb', delete=False)
print self.newdb.name
print(self.newdb.name)
olddb = open(kobodb, 'rb')
self.newdb.write(olddb.read(18))
self.newdb.write('\x01\x01')
self.newdb.write(b'\x01\x01')
olddb.read(2)
self.newdb.write(olddb.read())
olddb.close()
self.newdb.close()
self.__sqlite = sqlite3.connect(self.newdb.name)
self.__sqlite.text_factory = lambda b: b.decode("utf-8", errors="ignore")
self.__cursor = self.__sqlite.cursor()
self._userkeys = []
self._books = []
@ -430,27 +423,49 @@ class KoboLibrary(object):
def __bookfile (self, volumeid):
"""The filename needed to open a given book."""
return os.path.join(self.kobodir, u"kepub", volumeid)
return os.path.join(self.kobodir, "kepub", volumeid)
def __getmacaddrs (self):
"""The list of all MAC addresses on this machine."""
macaddrs = []
if sys.platform.startswith('win'):
c = re.compile('\s(' + '[0-9a-f]{2}-' * 5 + '[0-9a-f]{2})(\s|$)', re.IGNORECASE)
(p_in, p_out, p_err) = os.popen3('ipconfig /all')
for line in p_out:
m = c.search(line)
if m:
macaddrs.append(re.sub("-", ":", m.group(1)).upper())
c = re.compile('\s?(' + '[0-9a-f]{2}[:\-]' * 5 + '[0-9a-f]{2})(\s|$)', re.IGNORECASE)
try:
output = subprocess.Popen('ipconfig /all', shell=True, stdout=subprocess.PIPE, text=True).stdout
for line in output:
m = c.search(line)
if m:
macaddrs.append(re.sub("-", ":", m.group(1)).upper())
except:
output = subprocess.Popen('wmic nic where PhysicalAdapter=True get MACAddress', shell=True, stdout=subprocess.PIPE, text=True).stdout
for line in output:
m = c.search(line)
if m:
macaddrs.append(re.sub("-", ":", m.group(1)).upper())
elif sys.platform.startswith('darwin'):
c = re.compile('\s(' + '[0-9a-f]{2}:' * 5 + '[0-9a-f]{2})(\s|$)', re.IGNORECASE)
output = subprocess.check_output('/sbin/ifconfig -a', shell=True)
output = subprocess.check_output('/sbin/ifconfig -a', shell=True, encoding='utf-8')
matches = c.findall(output)
for m in matches:
# print u"m:{0}".format(m[0])
# print "m:{0}".format(m[0])
macaddrs.append(m[0].upper())
elif sys.platform.startswith('linux'):
for interface in os.listdir('/sys/class/net'):
with open('/sys/class/net/' + interface + '/address', 'r') as f:
mac = f.read().strip().upper()
# some interfaces, like Tailscale's VPN interface, do not have a MAC address
if mac != '':
macaddrs.append(mac)
else:
# probably linux, let's try ipconfig under wine
# final fallback
# let's try ip
c = re.compile('\s(' + '[0-9a-f]{2}:' * 5 + '[0-9a-f]{2})(\s|$)', re.IGNORECASE)
for line in os.popen('ip -br link'):
m = c.search(line)
if m:
macaddrs.append(m.group(1).upper())
# let's try ipconfig under wine
c = re.compile('\s(' + '[0-9a-f]{2}-' * 5 + '[0-9a-f]{2})(\s|$)', re.IGNORECASE)
for line in os.popen('ipconfig /all'):
m = c.search(line)
@ -475,14 +490,14 @@ class KoboLibrary(object):
pass
row = cursor.fetchone()
return userids
def __getuserkeys (self, macaddr):
userids = self.__getuserids()
userkeys = []
for hash in KOBO_HASH_KEYS:
deviceid = hashlib.sha256(hash + macaddr).hexdigest()
deviceid = hashlib.sha256((hash + macaddr).encode('ascii')).hexdigest()
for userid in userids:
userkey = hashlib.sha256(deviceid + userid).hexdigest()
userkey = hashlib.sha256((deviceid + userid).encode('ascii')).hexdigest()
userkeys.append(binascii.a2b_hex(userkey[32:]))
return userkeys
@ -543,7 +558,7 @@ class KoboBook(object):
# Convert relative URIs
href = item.attrib['href']
if not c.match(href):
href = string.join((basedir, href), '')
href = ''.join((basedir, href))
# Update books we've found from the DB.
if href in self._encryptedfiles:
@ -573,11 +588,9 @@ class KoboFile(object):
file page key. The caller must determine if the decrypted
data is correct."""
# The userkey decrypts the page key (self.key)
keyenc = AES(userkey)
decryptedkey = keyenc.decrypt(self.key)
# The decrypted page key decrypts the content
pageenc = AES(decryptedkey)
return self.__removeaespadding(pageenc.decrypt(contents))
decryptedkey = AES.new(userkey, AES.MODE_ECB).decrypt(self.key)
# The decrypted page key decrypts the content. Padding is PKCS#7
return unpad(AES.new(decryptedkey, AES.MODE_ECB).decrypt(contents), 16)
def check (self, contents):
"""
@ -591,93 +604,76 @@ class KoboFile(object):
# assume utf-8 with no BOM
textoffset = 0
stride = 1
print u"Checking text:{0}:".format(contents[:10])
print("Checking text:{0}:".format(contents[:10]))
# check for byte order mark
if contents[:3]=="\xef\xbb\xbf":
if contents[:3]==b"\xef\xbb\xbf":
# seems to be utf-8 with BOM
print u"Could be utf-8 with BOM"
print("Could be utf-8 with BOM")
textoffset = 3
elif contents[:2]=="\xfe\xff":
elif contents[:2]==b"\xfe\xff":
# seems to be utf-16BE
print u"Could be utf-16BE"
print("Could be utf-16BE")
textoffset = 3
stride = 2
elif contents[:2]=="\xff\xfe":
elif contents[:2]==b"\xff\xfe":
# seems to be utf-16LE
print u"Could be utf-16LE"
print("Could be utf-16LE")
textoffset = 2
stride = 2
else:
print u"Perhaps utf-8 without BOM"
print("Perhaps utf-8 without BOM")
# now check that the first few characters are in the ASCII range
for i in xrange(textoffset,textoffset+5*stride,stride):
if ord(contents[i])<32 or ord(contents[i])>127:
for i in range(textoffset,textoffset+5*stride,stride):
if contents[i]<32 or contents[i]>127:
# Non-ascii, so decryption probably failed
print u"Bad character at {0}, value {1}".format(i,ord(contents[i]))
print("Bad character at {0}, value {1}".format(i,contents[i]))
raise ValueError
print u"Seems to be good text"
print("Seems to be good text")
return True
if contents[:5]=="<?xml" or contents[:8]=="\xef\xbb\xbf<?xml":
if contents[:5]==b"<?xml" or contents[:8]==b"\xef\xbb\xbf<?xml":
# utf-8
return True
elif contents[:14]=="\xfe\xff\x00<\x00?\x00x\x00m\x00l":
elif contents[:14]==b"\xfe\xff\x00<\x00?\x00x\x00m\x00l":
# utf-16BE
return True
elif contents[:14]=="\xff\xfe<\x00?\x00x\x00m\x00l\x00":
elif contents[:14]==b"\xff\xfe<\x00?\x00x\x00m\x00l\x00":
# utf-16LE
return True
elif contents[:9]=="<!DOCTYPE" or contents[:12]=="\xef\xbb\xbf<!DOCTYPE":
elif contents[:9]==b"<!DOCTYPE" or contents[:12]==b"\xef\xbb\xbf<!DOCTYPE":
# utf-8 of weird <!DOCTYPE start
return True
elif contents[:22]=="\xfe\xff\x00<\x00!\x00D\x00O\x00C\x00T\x00Y\x00P\x00E":
elif contents[:22]==b"\xfe\xff\x00<\x00!\x00D\x00O\x00C\x00T\x00Y\x00P\x00E":
# utf-16BE of weird <!DOCTYPE start
return True
elif contents[:22]=="\xff\xfe<\x00!\x00D\x00O\x00C\x00T\x00Y\x00P\x00E\x00":
elif contents[:22]==b"\xff\xfe<\x00!\x00D\x00O\x00C\x00T\x00Y\x00P\x00E\x00":
# utf-16LE of weird <!DOCTYPE start
return True
else:
print u"Bad XML: {0}".format(contents[:8])
print("Bad XML: {0}".format(contents[:8]))
raise ValueError
elif self.mimetype == 'image/jpeg':
if contents[:3] == '\xff\xd8\xff':
if contents[:3] == b'\xff\xd8\xff':
return True
else:
print u"Bad JPEG: {0}".format(contents[:3].encode('hex'))
print("Bad JPEG: {0}".format(contents[:3].hex()))
raise ValueError()
return False
def __removeaespadding (self, contents):
"""
Remove the trailing padding, using what appears to be the CMS
algorithm from RFC 5652 6.3"""
lastchar = binascii.b2a_hex(contents[-1:])
strlen = int(lastchar, 16)
padding = strlen
if strlen == 1:
return contents[:-1]
if strlen < 16:
for i in range(strlen):
testchar = binascii.b2a_hex(contents[-strlen:-(strlen-1)])
if testchar != lastchar:
padding = 0
if padding > 0:
contents = contents[:-padding]
return contents
def decrypt_book(book, lib):
print u"Converting {0}".format(book.title)
print("Converting {0}".format(book.title))
zin = zipfile.ZipFile(book.filename, "r")
# make filename out of Unicode alphanumeric and whitespace equivalents from title
outname = u"{0}.epub".format(re.sub('[^\s\w]', '_', book.title, 0, re.UNICODE))
outname = "{0}.epub".format(re.sub('[^\s\w]', '_', book.title, 0, re.UNICODE))
if (book.type == 'drm-free'):
print u"DRM-free book, conversion is not needed"
print("DRM-free book, conversion is not needed")
shutil.copyfile(book.filename, outname)
print u"Book saved as {0}".format(os.path.join(os.getcwd(), outname))
print("Book saved as {0}".format(os.path.join(os.getcwd(), outname)))
return 0
result = 1
for userkey in lib.userkeys:
print u"Trying key: {0}".format(userkey.encode('hex_codec'))
print("Trying key: {0}".format(userkey.hex()))
try:
zout = zipfile.ZipFile(outname, "w", zipfile.ZIP_DEFLATED)
for filename in zin.namelist():
@ -689,12 +685,12 @@ def decrypt_book(book, lib):
file.check(contents)
zout.writestr(filename, contents)
zout.close()
print u"Decryption succeeded."
print u"Book saved as {0}".format(os.path.join(os.getcwd(), outname))
print("Decryption succeeded.")
print("Book saved as {0}".format(os.path.join(os.getcwd(), outname)))
result = 0
break
except ValueError:
print u"Decryption failed."
print("Decryption failed.")
zout.close()
os.remove(outname)
zin.close()
@ -703,7 +699,7 @@ def decrypt_book(book, lib):
def cli_main():
description = __about__
epilog = u"Parsing of arguments failed."
epilog = "Parsing of arguments failed."
parser = argparse.ArgumentParser(prog=sys.argv[0], description=description, epilog=epilog)
parser.add_argument('--devicedir', default='/media/KOBOeReader', help="directory of connected Kobo device")
parser.add_argument('--all', action='store_true', help="flag for converting all books on device")
@ -719,25 +715,25 @@ def cli_main():
books = lib.books
else:
for i, book in enumerate(lib.books):
print u"{0}: {1}".format(i + 1, book.title)
print u"Or 'all'"
print("{0}: {1}".format(i + 1, book.title))
print("Or 'all'")
choice = raw_input(u"Convert book number... ")
if choice == u'all':
choice = input("Convert book number... ")
if choice == "all":
books = list(lib.books)
else:
try:
num = int(choice)
books = [lib.books[num - 1]]
except (ValueError, IndexError):
print u"Invalid choice. Exiting..."
exit()
print("Invalid choice. Exiting...")
sys.exit()
results = [decrypt_book(book, lib) for book in books]
lib.close()
overall_result = all(result != 0 for result in results)
if overall_result != 0:
print u"Could not decrypt book with any of the keys found."
print("Could not decrypt book with any of the keys found.")
return overall_result

View file

@ -8,7 +8,7 @@
<body>
<h1>Obok DeDRM Plugin</h1>
<h3>(version 3.1.3)</h3>
<h3>(version 10.0.9 / 10.1.0 RC1)</h3>
<h3>Installation:</h3>
@ -22,7 +22,7 @@
<h3>Troubleshooting:</h3>
<p >If you find that its not working for you , you can save a lot of time by using the plugin with Calibre in debug mode. This will print out a lot of helpful info that can be copied into any online help requests.</p>
<p >If you find that its not working for you, you can save a lot of time by using the plugin with Calibre in debug mode. This will print out a lot of helpful info that can be copied into any online help requests.</p>
<p>Open a command prompt (terminal window) and type "calibre-debug -g" (without the quotes). Calibre will launch, and you can use the plugin the usual way. The debug info will be output to the original command prompt (terminal window). Copy the resulting output and paste it into the comment you make at Apprentice Alf's blog.</p>

Binary file not shown.

View file

@ -0,0 +1,364 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2014-11-17 12:51+0100\n"
"PO-Revision-Date: 2021-01-19 12:20+0100\n"
"Language: sv\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"Last-Translator: \n"
"Language-Team: \n"
"X-Generator: Poedit 2.4.2\n"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:80
msgid ""
"<p>No books found in Kobo Library\n"
"Are you sure it's installed\\configured\\synchronized?"
msgstr ""
"<p>Inga böcker hittades i Kobo-bibliotek\n"
"Är du säker på att den är installerad\\konfigurerad\\synkroniserad?"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:87
msgid "Legacy key found: "
msgstr "Äldre nyckel hittades: "
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:93
msgid "Trouble retrieving keys with newer obok method."
msgstr "Problem med att hämta nycklar med nyare obok-metod."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:97
msgid "Found {0} possible keys to try."
msgstr "Hittade {0} möjliga nycklar att försöka med."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:99
msgid "<p>No userkeys found to decrypt books with. No point in proceeding."
msgstr ""
"<p>Inga användarnycklar hittades för att dekryptera böcker med. Det är ingen "
"idé att fortsätta."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:115
msgid "{} - Decryption canceled by user."
msgstr "{} - Dekryptering avbröts av användaren."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:135
msgid "{} - \"Add books\" canceled by user."
msgstr "{} - \"Lägg till böcker\" avbröts av användaren."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:137
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:156
msgid "{} - wrapping up results."
msgstr "{} - samlar in resultat."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:153
msgid "{} - User opted not to try to insert EPUB formats"
msgstr "{} - Användaren valde att inte försöka infoga EPUB-format"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:188
msgid "{0} - Decrypting {1}"
msgstr "{0} - Dekrypterar {1}"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:197
msgid "{0} - Couldn't decrypt {1}"
msgstr "{0} - Kunde inte dekryptera {1}"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:198
msgid "decryption errors"
msgstr "dekrypteringsfel"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:213
msgid "{0} - Added {1}"
msgstr "{0} - Lade till {1}"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:218
msgid "{0} - {1} already exists. Will try to add format later."
msgstr "{0} - {1} finns redan. Kommer att försöka lägga till format senare."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:219
msgid "duplicate detected"
msgstr "dubblett upptäcktes"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:233
msgid "{0} - Successfully added EPUB format to existing {1}"
msgstr "{0} - EPUB-formatet har lagts till i befintliga {1}"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:236
msgid ""
"{0} - Error adding EPUB format to existing {1}. This really shouldn't happen."
msgstr ""
"{0} - Fel vid tilläggning av EPUB-formatet till befintliga {1}. Detta borde "
"verkligen inte hända."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:259
msgid "{} - \"Insert formats\" canceled by user."
msgstr "{} - \"Infoga format\" avbröts av användaren."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:291
msgid ""
"<p><b>{0}</b> EPUB{2} successfully added to library.<br /><br /><b>{1}</b> "
msgstr "<p><b>{0}</b> EPUB{2} har lagts till i bibliotek.<br /><br /><b>{1}</b> "
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:292
msgid ""
"not added because books with the same title/author were detected.<br /><br /"
">Would you like to try and add the EPUB format{0}"
msgstr ""
"lades inte till eftersom böcker med samma titel/författare upptäcktes.<br/"
"><br />Vill du försöka lägga till EPUB-formatet{0}"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:293
msgid ""
" to those existing entries?<br /><br />NOTE: no pre-existing EPUBs will be "
"overwritten."
msgstr ""
" till dessa befintliga poster?<br /><br />OBS: inga befintliga EPUB:er kommer "
"att skrivas över."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:295
msgid ""
"{0} -- not added because of {1} in your library.\n"
"\n"
msgstr ""
"{0} -- lades inte till på grund av {1} i ditt bibliotek.\n"
"\n"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:297
msgid "<p><b>{0}</b> -- not added because of {1} in your library.<br /><br />"
msgstr ""
"<p><b>{0}</b> -- lades inte till på grund av {1} i ditt bibliotek.<br /><br />"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:298
msgid ""
"Would you like to try and add the EPUB format to an available calibre "
"duplicate?<br /><br />"
msgstr ""
"Vill du försöka lägga till EPUB-formatet till en tillgänglig calibre-dubblett?"
"<br /><br />"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:299
msgid "NOTE: no pre-existing EPUB will be overwritten."
msgstr "OBS: ingen befintlig EPUB kommer att skrivas över."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:346
msgid "Trying key: "
msgstr "Försöker med nyckel: "
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:378
msgid "Decryption failed, trying next key."
msgstr "Dekryptering misslyckades, försöker med nästa nyckel."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:382
msgid "Unknown Error decrypting, trying next key.."
msgstr "Okänt fel vid dekryptering, försöker med nästa nyckel.."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:395
msgid ""
"<p>All selected Kobo books added as new calibre books or inserted into "
"existing calibre ebooks.<br /><br />No issues."
msgstr ""
"<p>Alla valda Kobo-böcker har lagts till som nya calibre-böcker eller infogats "
"i befintliga calibre-e-böcker.<br /><br />Inga problem."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:399
msgid "<p>{0} successfully added."
msgstr "<p>{0} har lagts till."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:403
msgid ""
"<p>Not all selected Kobo books made it into calibre.<br /><br />View report "
"for details."
msgstr ""
"<p>Inte alla valda Kobo-böcker lades till i calibre.<br /><br />Visa rapport "
"för detaljer."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:404
msgid "<p><b>Total attempted:</b> {}</p>\n"
msgstr "<p><b>Försök totalt:</b> {}</p>\n"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:405
msgid "<p><b>Decryption errors:</b> {}</p>\n"
msgstr "<p><b>Dekrypteringsfel:</b> {}</p>\n"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:411
msgid "<p><b>New Books created:</b> {}</p>\n"
msgstr "<p><b>Nya böcker skapade:</b> {}</p>\n"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:418
msgid "<p><b>Duplicates that weren't added:</b> {}</p>\n"
msgstr "<p><b>Dubbletter som inte har lagts till:</b> {}</p>\n"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:426
msgid "<p><b>Book imports cancelled by user:</b> {}</p>\n"
msgstr "<p><b>Bokimport avbröts av användaren:</b> {}</p>\n"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:428
msgid "<p><b>New EPUB formats inserted in existing calibre books:</b> {0}</p>\n"
msgstr ""
"<p><b>Nya EPUB-format infogade i befintliga calibre-böcker:</b> {0}</p>\n"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:434
msgid ""
"<p><b>EPUB formats NOT inserted into existing calibre books:</b> {}<br />\n"
msgstr ""
"<p><b>EPUB-format som INTE infogats i befintliga calibre-böcker:</b> {}<br />\n"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:435
msgid ""
"(Either because the user <i>chose</i> not to insert them, or because all "
"duplicates already had an EPUB format)"
msgstr ""
"(Antingen för att användaren <i>valde</i> att inte infoga dem, eller för att "
"alla dubbletter redan hade ett EPUB-format)"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:444
msgid "<p><b>Format imports cancelled by user:</b> {}</p>\n"
msgstr "<p><b>Format-importen avbröts av användaren:</b> {}</p>\n"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:458
msgid "Unknown Book Title"
msgstr "Okänd boktitel"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:460
msgid "it couldn't be decrypted."
msgstr "den kunde inte dekrypteras."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:462
msgid ""
"user CHOSE not to insert the new EPUB format, or all existing calibre entries "
"HAD an EPUB format already."
msgstr ""
"användaren VALDE att inte infoga det nya EPUB-formatet, eller alla befintliga "
"calibre-poster hade redan ett EPUB-format."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:464
msgid "of unknown reasons. Gosh I'm embarrassed!"
msgstr "av okända skäl. Jag skäms!"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:465
msgid "<p>{0} not added because {1}"
msgstr "<p>{0} lades inte till på grund av {1}"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\common_utils.py:226
msgid "Help"
msgstr "Hjälp"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\common_utils.py:235
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\utilities.py:214
msgid "Restart required"
msgstr "Kräver omstart"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\common_utils.py:236
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\utilities.py:215
msgid ""
"Title image not found - you must restart Calibre before using this plugin!"
msgstr ""
"Titelbild hittades inte - du måste starta om calibre innan du använder denna "
"insticksmodul!"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\common_utils.py:322
msgid "Undefined"
msgstr "Odefinierad"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:30
msgid "When should Obok try to insert EPUBs into existing calibre entries?"
msgstr "När ska Obok försöka infoga EPUB:er i befintliga calibre-poster?"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:33
msgid ""
"<p>Default behavior when duplicates are detected. None of the choices will "
"cause calibre ebooks to be overwritten"
msgstr ""
"<p>Standardbeteende när dubbletter upptäcks. Inget av alternativen kommer att "
"orsaka att calibre-e-böcker skrivs över"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:35
msgid "Ask"
msgstr "Fråga"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:35
msgid "Always"
msgstr "Alltid"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:35
msgid "Never"
msgstr "Aldrig"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:60
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\utilities.py:150
msgid " v"
msgstr " v"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:65
msgid "Obok DeDRM"
msgstr "Obok DeDRM"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:68
msgid "<a href=\"http://www.foo.com/\">Help</a>"
msgstr "<a href=\"http://www.foo.com/\">Hjälp</a>"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:89
msgid "Select All"
msgstr "Välj alla"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:90
msgid "Select all books to add them to the calibre library."
msgstr "Välj alla böcker för att lägga till dem i calibre-biblioteket."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:92
msgid "All with DRM"
msgstr "Alla med DRM"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:93
msgid "Select all books with DRM."
msgstr "Välj alla böcker med DRM."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:95
msgid "All DRM free"
msgstr "Alla utan DRM"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:96
msgid "Select all books without DRM."
msgstr "Välj alla böcker utan DRM."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:146
msgid "Title"
msgstr "Titel"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:146
msgid "Author"
msgstr "Författare"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:146
msgid "Series"
msgstr "Serier"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:369
msgid "Copy to clipboard"
msgstr "Kopiera till urklipp"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:397
msgid "View Report"
msgstr "Visa rapport"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\__init__.py:21
msgid "Removes DRM from Kobo kepubs and adds them to the library."
msgstr "Tar bort DRM från Kobo-kepubs och lägger till dem i biblioteket."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\obok\obok.py:162
msgid "AES improper key used"
msgstr "Felaktig AES-nyckel används"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\obok\obok.py:167
msgid "Failed to initialize AES key"
msgstr "Misslyckades med att initiera AES-nyckel"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\obok\obok.py:175
msgid "AES decryption failed"
msgstr "AES-dekryptering misslyckades"

View file

@ -1,27 +1,27 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__docformat__ = 'restructuredtext en'
import os, struct, time
from StringIO import StringIO
try:
from StringIO import StringIO
except ImportError:
from io import StringIO
from traceback import print_exc
try:
from PyQt5.Qt import (Qt, QDialog, QPixmap, QIcon, QLabel, QHBoxLayout, QFont, QTableWidgetItem)
except ImportError:
from PyQt4.Qt import (Qt, QDialog, QPixmap, QIcon, QLabel, QHBoxLayout, QFont, QTableWidgetItem)
from PyQt5.Qt import (Qt, QDialog, QPixmap, QIcon, QLabel, QHBoxLayout, QFont, QTableWidgetItem)
from calibre.utils.config import config_dir
from calibre.constants import iswindows, DEBUG
from calibre import prints
from calibre.gui2 import (error_dialog, gprefs)
from calibre.gui2.actions import menu_action_unique_name
from calibre_plugins.obok_dedrm.__init__ import (PLUGIN_NAME,
from calibre_plugins.obok_dedrm.__init__ import (PLUGIN_NAME,
PLUGIN_SAFE_NAME, PLUGIN_VERSION, PLUGIN_DESCRIPTION)
plugin_ID = None
@ -39,7 +39,7 @@ else:
def convert_qvariant(x):
vt = x.type()
if vt == x.String:
return unicode(x.toString())
return x.toString()
if vt == x.List:
return [convert_qvariant(i) for i in x.toList()]
return x.toPyObject()
@ -62,7 +62,7 @@ except NameError:
def format_plural(number, possessive=False):
'''
Cosmetic ditty to provide the proper string formatting variable to handle singular/plural situations
:param: number: variable that represents the count/len of something
'''
if not possessive:
@ -141,7 +141,7 @@ def showErrorDlg(errmsg, parent, trcbk=False):
'''
if trcbk:
error= ''
f=StringIO()
f=StringIO()
print_exc(file=f)
error_mess = f.getvalue().splitlines()
for line in error_mess:
@ -224,5 +224,5 @@ class ReadOnlyTableWidgetItem(QTableWidgetItem):
def __init__(self, text):
if text is None:
text = ''
QTableWidgetItem.__init__(self, text, QTableWidgetItem.UserType)
QTableWidgetItem.__init__(self, text, QTableWidgetItem.ItemType.UserType)
self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)

View file

@ -68,12 +68,20 @@ class SafeUnbuffered:
if self.encoding == None:
self.encoding = "utf-8"
def write(self, data):
if isinstance(data,unicode):
if isinstance(data,str) or isinstance(data,unicode):
# str for Python3, unicode for Python2
data = data.encode(self.encoding,"replace")
self.stream.write(data)
self.stream.flush()
try:
buffer = getattr(self.stream, 'buffer', self.stream)
# self.stream.buffer for Python3, self.stream for Python2
buffer.write(data)
buffer.flush()
except:
# We can do nothing if a write fails
raise
def __getattr__(self, attr):
return getattr(self.stream, attr)
try:
from calibre.constants import iswindows, isosx
@ -129,7 +137,10 @@ if iswindows:
c_long, c_ulong
from ctypes.wintypes import LPVOID, DWORD, BOOL
import _winreg as winreg
try:
import winreg
except ImportError:
import _winreg as winreg
def _load_crypto_libcrypto():
from ctypes.util import find_library

View file

@ -4,7 +4,7 @@
from __future__ import with_statement
# ignoblekey.py
# Copyright © 2015 Apprentice Alf and Apprentice Harper
# Copyright © 2015-2020 Apprentice Harper et al.
# Based on kindlekey.py, Copyright © 2010-2013 by some_updates and Apprentice Alf
@ -14,13 +14,14 @@ from __future__ import with_statement
# Revision history:
# 1.0 - Initial release
# 1.1 - remove duplicates and return last key as single key
# 2.0 - Python 3
"""
Get Barnes & Noble EPUB user key from nook Studio log file
"""
__license__ = 'GPL v3'
__version__ = "1.1"
__version__ = "2.0"
import sys
import os
@ -38,12 +39,20 @@ class SafeUnbuffered:
if self.encoding == None:
self.encoding = "utf-8"
def write(self, data):
if isinstance(data,unicode):
if isinstance(data,str) or isinstance(data,unicode):
# str for Python3, unicode for Python2
data = data.encode(self.encoding,"replace")
self.stream.write(data)
self.stream.flush()
try:
buffer = getattr(self.stream, 'buffer', self.stream)
# self.stream.buffer for Python3, self.stream for Python2
buffer.write(data)
buffer.flush()
except:
# We can do nothing if a write fails
raise
def __getattr__(self, attr):
return getattr(self.stream, attr)
try:
from calibre.constants import iswindows, isosx
@ -97,7 +106,10 @@ def getNookLogFiles():
logFiles = []
found = False
if iswindows:
import _winreg as winreg
try:
import winreg
except ImportError:
import _winreg as winreg
# some 64 bit machines do not have the proper registry key for some reason
# or the python interface to the 32 vs 64 bit registry is broken
@ -318,7 +330,7 @@ def gui_main():
keyfileout.write(key)
success = True
tkMessageBox.showinfo(progname, u"Key successfully retrieved to {0}".format(outfile))
except DrmException, e:
except DrmException as e:
tkMessageBox.showerror(progname, u"Error: {0}".format(str(e)))
except Exception:
root.wm_state('normal')

View file

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

View file

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

View file

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

View file

@ -51,12 +51,20 @@ class SafeUnbuffered:
if self.encoding == None:
self.encoding = "utf-8"
def write(self, data):
if isinstance(data,unicode):
if isinstance(data,str) or isinstance(data,unicode):
# str for Python3, unicode for Python2
data = data.encode(self.encoding,"replace")
self.stream.write(data)
self.stream.flush()
try:
buffer = getattr(self.stream, 'buffer', self.stream)
# self.stream.buffer for Python3, self.stream for Python2
buffer.write(data)
buffer.flush()
except:
# We can do nothing if a write fails
raise
def __getattr__(self, attr):
return getattr(self.stream, attr)
try:
from calibre.constants import iswindows, isosx
@ -177,7 +185,10 @@ if iswindows:
create_unicode_buffer, create_string_buffer, CFUNCTYPE, addressof, \
string_at, Structure, c_void_p, cast
import _winreg as winreg
try:
import winreg
except ImportError:
import _winreg as winreg
MAX_PATH = 255
kernel32 = windll.kernel32
advapi32 = windll.advapi32
@ -289,7 +300,7 @@ if iswindows:
numBlocks, numExtraBytes = divmod(len(self.bytesToDecrypt), self.blockSize)
if more == None: # no more calls to decrypt, should have all the data
if numExtraBytes != 0:
raise DecryptNotBlockAlignedError, 'Data not block aligned on decrypt'
raise DecryptNotBlockAlignedError('Data not block aligned on decrypt')
# hold back some bytes in case last decrypt has zero len
if (more != None) and (numExtraBytes == 0) and (numBlocks >0) :
@ -331,7 +342,7 @@ if iswindows:
def removePad(self, paddedBinaryString, blockSize):
""" Remove padding from a binary string """
if not(0<len(paddedBinaryString)):
raise DecryptNotBlockAlignedError, 'Expected More Data'
raise DecryptNotBlockAlignedError('Expected More Data')
return paddedBinaryString[:-ord(paddedBinaryString[-1])]
class noPadding(Pad):
@ -361,8 +372,8 @@ if iswindows:
self.blockSize = blockSize # blockSize is in bytes
self.padding = padding # change default to noPadding() to get normal ECB behavior
assert( keySize%4==0 and NrTable[4].has_key(keySize/4)),'key size must be 16,20,24,29 or 32 bytes'
assert( blockSize%4==0 and NrTable.has_key(blockSize/4)), 'block size must be 16,20,24,29 or 32 bytes'
assert( keySize%4==0 and keySize/4 in NrTable[4]),'key size must be 16,20,24,29 or 32 bytes'
assert( blockSize%4==0 and blockSize/4 in NrTable), 'block size must be 16,20,24,29 or 32 bytes'
self.Nb = self.blockSize/4 # Nb is number of columns of 32 bit words
self.Nk = keySize/4 # Nk is the key length in 32-bit words
@ -639,7 +650,7 @@ if iswindows:
def __init__(self, key = None, padding = padWithPadLen(), keySize=16):
""" Initialize AES, keySize is in bytes """
if not (keySize == 16 or keySize == 24 or keySize == 32) :
raise BadKeySizeError, 'Illegal AES key size, must be 16, 24, or 32 bytes'
raise BadKeySizeError('Illegal AES key size, must be 16, 24, or 32 bytes')
Rijndael.__init__( self, key, padding=padding, keySize=keySize, blockSize=16 )
@ -779,10 +790,11 @@ if iswindows:
# [c_char_p, c_ulong, c_char_p, c_ulong, c_ulong, c_ulong, c_char_p])
def pbkdf2(self, passwd, salt, iter, keylen):
def xorstr( a, b ):
def xorbytes( a, b ):
if len(a) != len(b):
raise Exception("xorstr(): lengths differ")
return ''.join((chr(ord(x)^ord(y)) for x, y in zip(a, b)))
raise Exception("xorbytes(): lengths differ")
return bytes([x ^ y for x, y in zip(a, b)])
def prf( h, data ):
hm = h.copy()
@ -794,13 +806,13 @@ if iswindows:
T = U
for i in range(2, itercount+1):
U = prf( h, U )
T = xorstr( T, U )
T = xorbytes( T, U )
return T
sha = hashlib.sha1
digest_size = sha().digest_size
# l - number of output blocks to produce
l = keylen / digest_size
l = keylen // digest_size
if keylen % digest_size != 0:
l += 1
h = hmac.new( passwd, None, sha )

Some files were not shown because too many files have changed in this diff Show more