Compare commits

...

177 Commits

Author SHA1 Message Date
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
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
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
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
76 changed files with 5065 additions and 3504 deletions

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

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

View File

@@ -1,35 +0,0 @@
name: Python_tests
on: [push, pull_request]
jobs:
Python_tests:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [macos-latest]
python-version: [2.7] # , 3.8]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install flake8 pytest # -r requirements.txt
#- name: Check formatting with black
# if: matrix.python-version == '3.8'
# run: |
# pip install black
# black --check .
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --builtins=_,I --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --builtins=_,I --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
#- name: Test with pytest
# run: pytest
#- name: Run doctests with pytest
# run: pytest --doctest-modules

19
.github/workflows/main.yml vendored Normal file
View File

@@ -0,0 +1,19 @@
name: Package plugin
on:
push:
branches: [ master ]
jobs:
package:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Package
run: python3 make_release.py 10.0.0
- name: Upload
uses: actions/upload-artifact@v2
with:
name: plugin
path: |
DeDRM_tools_*.zip

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
.DS_Store

View File

@@ -27,7 +27,7 @@ platforms.
#### Enter your keys #### Enter your keys
- Figure out what format DeDRM wants your key in by looking in - Figure out what format DeDRM wants your key in by looking in
[the code that handles that](dedrm_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 - For Kindle eInk devices, DeDRM expects you to put a list of serial
numbers in the `serials` field: `"serials": ["012345689abcdef"]` or numbers in the `serials` field: `"serials": ["012345689abcdef"]` or
`"serials": ["1111111111111111", "2222222222222222"]`. `"serials": ["1111111111111111", "2222222222222222"]`.

23
CHANGELOG.md Normal file
View File

@@ -0,0 +1,23 @@
# Changelog
List of changes since the fork of Apprentice Harper's repository:
- CI testing / linting removed as that always failed anyways. The CI now "just" packages the plugin.
- Support for the Readium LCP DRM (also known as "CARE DRM" or "TEA DRM"). This supports EPUB and PDF files. It does not yet support Readium LCPDF/LPF/LCPA/LCPAU/LCPDI files, as I don't have access to any of these. If you have an LCP-protected file in one of these formats that this plugin does not work with, please open [an issue](https://github.com/noDRM/DeDRM_tools/issues) and attach the file to the report.
- Add new Github issue report form which forces the user to include stuff like their Calibre version to hopefully increase the quality of bug reports.
- Issues with PDF files in Calibre 5 should be fixed (merged [apprenticeharper/DeDRM_tools#1689](https://github.com/apprenticeharper/DeDRM_tools/pull/1689) ).
- Fixed tons of issues with the B&N PDF DRM removal script ignoblepdf.py. It looks like that has never been tested since the move to Python3. I have integrated the B&N-specific code into ineptpdf.py, the original ignoblepdf.py is now unused. Fairly untested as I don't have any PDFs with B&N DRM.
- Issues with Obok key retrieval fixed (merged [apprenticeharper/DeDRM_tools#1691](https://github.com/apprenticeharper/DeDRM_tools/pull/1691) ).
- Issues with obfuscated Adobe fonts fixed (fixes [apprenticeharper/DeDRM_tools#1828](https://github.com/apprenticeharper/DeDRM_tools/issues/1828) ).
- Deobfuscate font files in EPUBs by default (can be disabled in the plugin settings).
- The standalone adobekey.py script now includes the account UUID in the key file name.
- When extracting the default key from an ADE install, include the account UUID in the key name.
- Adobe key management window size increased to account for longer key names due to the UUID.
- Verify that the decrypted book key has the correct format. This makes it way less likely for issue [apprenticeharper/DeDRM_tools#1862](https://github.com/apprenticeharper/DeDRM_tools/issues/1862) to cause trouble.
- If the Adobe owner UUID of a book being imported happens to be included in a particular key's name, try this key first before trying all the others. This completely fixes [apprenticeharper/DeDRM_tools#1862](https://github.com/apprenticeharper/DeDRM_tools/issues/1862), but only if the key name contains the correct UUID (not always the case, especially for keys imported with older versions of the plugin). It also makes DRM removal faster as the plugin no longer has to attempt all possible keys.
- Remove some additional DRM remnants in Amazon MOBI files (merged [apprenticeharper/DeDRM_tools#23](https://github.com/apprenticeharper/DeDRM_tools/pull/23) ).
- Just in case it's necessary, added a setting to the B&N key generation script to optionally allow the user to select the old key generation algorithm. Who knows, they might want to remove DRM from old books with the old key scheme.
- Add a more verbose error message when trying to remove DRM from a book with the new, not-yet-cracked version of the Adobe ADEPT DRM.
- Added back support for Python2 (Calibre 2.0+). Only tested with ADEPT (PDF & EPUB) and Readium LCP so far, please open an issue if there's errors with other book types.
- Begin work on removing some kinds of watermarks from files after DRM removal. This isn't tested a lot, and is disabled by default. You can enable it in the plugin settings.
- If you're using the [ACSM Input Plugin / DeACSM](https://www.mobileread.com/forums/showthread.php?t=341975), the encryption key will automatically be extracted from that plugin if necessary.

View File

@@ -38,7 +38,7 @@ li {margin-top: 0.5em}
<h3>Renaming Keys:</h3> <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> <h3>Exporting Keys:</h3>

View File

@@ -46,7 +46,7 @@ li {margin-top: 0.5em}
<h3>Renaming Keys:</h3> <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> <h3>Exporting Keys:</h3>
@@ -56,7 +56,7 @@ li {margin-top: 0.5em}
<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>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 wil only be saved permanently when you click OK in the main configuration dialog.</p> <p>Once done creating/deleting/renaming/importing decryption keys, click Close to exit the customization dialogue. Your changes will only be saved permanently when you click OK in the main configuration dialog.</p>
<h3>NOOK Study</h3> <h3>NOOK Study</h3>
<p>Books downloaded through NOOK Study may or may not use the key found using the above method. If a book is not decrypted successfully with any of the keys, the plugin will attempt to recover keys from the NOOK Study log file and use them.</p> <p>Books downloaded through NOOK Study may or may not use the key found using the above method. If a book is not decrypted successfully with any of the keys, the plugin will attempt to recover keys from the NOOK Study log file and use them.</p>

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>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> </body>

View File

@@ -17,15 +17,19 @@ p {margin-top: 0}
<body> <body>
<h1>DeDRM Plugin <span class="version">(v6.7.0)</span></h1> <h1>DeDRM Plugin <span class="version">(v10.0.0)</span></h1>
<p>This plugin removes DRM from ebooks when they are imported into calibre. If you already have DRMed ebooks in your calibre library, you will need to remove them and import them again.</p> <p>This plugin removes DRM from ebooks when they are imported into calibre. If you already have DRMed ebooks in your calibre library, you will need to remove them and import them again.</p>
<p>It is a forked version created by NoDRM, based on the original plugin by Apprentice Alf and Apprentice Harper.</p>
<h3>Installation</h3> <h3>Installation</h3>
<p>You have obviously managed to install the plugin, as otherwise you wouldnt be reading this help file. However, you should also delete any older DRM removal plugins, as this DeDRM plugin replaces the five older plugins: Kindle and Mobipocket DeDRM (K4MobiDeDRM), Ignoble Epub DeDRM (ignobleepub), Inept Epub DeDRM (ineptepub), Inept PDF DeDRM (ineptepub) and eReader PDB 2 PML (eReaderPDB2PML).</p> <p>You have obviously managed to install the plugin, as otherwise you wouldnt be reading this help file. However, you should also delete any older DRM removal plugins, as this DeDRM plugin replaces the five older plugins: Kindle and Mobipocket DeDRM (K4MobiDeDRM), Ignoble Epub DeDRM (ignobleepub), Inept Epub DeDRM (ineptepub), Inept PDF DeDRM (ineptepub) and eReader PDB 2 PML (eReaderPDB2PML).</p>
<h3>Configuration</h3> <h3>Configuration</h3>
<p>On Windows and Mac, the keys for ebooks downloaded for Kindle for Mac/PC and Adobe Digital Editions are automatically generated. If all your DRMed ebooks can be opened and read in Kindle for Mac/PC and/or Adobe Digital Editions on the same computer on which you are running calibre, you do not need to do any configuration of this plugin. On Linux, keys for Kindle for PC and Adobe Digital Editions need to be generated separately (see the Linux section below)</p> <p>On Windows and Mac, the keys for ebooks downloaded for Kindle for Mac/PC and Adobe Digital Editions are automatically generated. If all your DRMed ebooks can be opened and read in Kindle for Mac/PC and/or Adobe Digital Editions on the same computer on which you are running calibre, you do not need to do any configuration of this plugin. On Linux, keys for Kindle for PC and Adobe Digital Editions need to be generated separately (see the Linux section below).</p>
<p>If you are using the <a href="https://www.mobileread.com/forums/showthread.php?t=341975">DeACSM / ACSM Input Plugin</a> for Calibre, the keys will also automatically be dumped for you.</p>
<p>If you have other DRMed ebooks, you will need to enter extra configuration information. The buttons in this dialog will open individual configuration dialogs that will allow you to enter the needed information, depending on the type and source of your DRMed eBooks. Additional help on the information required is available in each of the the dialogs.</p> <p>If you have other DRMed ebooks, you will need to enter extra configuration information. The buttons in this dialog will open individual configuration dialogs that will allow you to enter the needed information, depending on the type and source of your DRMed eBooks. Additional help on the information required is available in each of the the dialogs.</p>
@@ -42,11 +46,12 @@ p {margin-top: 0}
<h3>Credits:</h3> <h3>Credits:</h3>
<ul> <ul>
<li>NoDRM for a bunch of updates and the Readium LCP support</li>
<li>The Dark Reverser for the Mobipocket and eReader scripts</li> <li>The Dark Reverser for the Mobipocket and eReader scripts</li>
<li>i♥cabbages for the Adobe Digital Editions scripts</li> <li>i♥cabbages for the Adobe Digital Editions scripts</li>
<li>Skindle aka Bart Simpson for the Amazon Kindle for PC script</li> <li>Skindle aka Bart Simpson for the Amazon Kindle for PC script</li>
<li>CMBDTC for Amazon Topaz DRM removal script</li> <li>CMBDTC for Amazon Topaz DRM removal script</li>
<li>some_updates, clarknova and Bart Simpson for Amazon Topaz conversion scripts</li> <li>some_updates, clarknova and Bart Simpson for Amazon Topaz conversion scripts</li>
<li>DiapDealer for the first calibre plugin versions of the tools</li> <li>DiapDealer for the first calibre plugin versions of the tools</li>
<li>some_updates, DiapDealer, Apprentice Alf and mdlnx for Amazon Kindle/Mobipocket tools</li> <li>some_updates, DiapDealer, Apprentice Alf and mdlnx for Amazon Kindle/Mobipocket tools</li>
<li>some_updates for the DeDRM all-in-one Python tool</li> <li>some_updates for the DeDRM all-in-one Python tool</li>
@@ -55,7 +60,8 @@ p {margin-top: 0}
<li>And probably many more.</li> <li>And probably many more.</li>
</ul> </ul>
<h3> For additional help read the <a href="https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md">FAQs</a> at <a href="https://github.com/apprenticeharper/DeDRM_tools/">Apprentice Harperss GitHub repository</a>. You can ask questions in the comments section of the <a href="http://apprenticealf.wordpress.com/2012/09/10/drm-removal-tools-for-ebooks/">first post</a> at <a href="http://wordpress.com/apprenticealf/">Apprentice Alf's blog</a> or <a href="https://github.com/apprenticeharper/DeDRM_tools/issues">raise an issue</a>. </h3> <h3>For additional help read the <a href="https://github.com/noDRM/DeDRM_tools/blob/master/FAQs.md">FAQs</a> at <a href="https://github.com/noDRM/DeDRM_tools">NoDRM's GitHub repository</a> (or the corresponding <a href="https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md">FAQs</a> at <a href="https://github.com/apprenticeharper/DeDRM_tools/">Apprentice Harperss GitHub repository</a>). You can <a href="https://github.com/noDRM/DeDRM_tools/issues">open issue reports</a>related to this fork at NoDRM's GitHub repository.</h3>
<h2>Linux Systems Only</h2> <h2>Linux Systems Only</h2>
<h3>Generating decryption keys for Adobe Digital Editions and Kindle for PC</h3> <h3>Generating decryption keys for Adobe Digital Editions and Kindle for PC</h3>

View File

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

View File

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

View File

@@ -1,13 +1,12 @@
#!/usr/bin/env python #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import with_statement
# __init__.py for DeDRM_plugin # __init__.py for DeDRM_plugin
# Copyright © 2008-2020 Apprentice Harper et al. # Copyright © 2008-2020 Apprentice Harper et al.
# Copyright © 2021 NoDRM
__license__ = 'GPL v3' __license__ = 'GPL v3'
__version__ = '6.8.1' __version__ = '10.0.0'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
@@ -71,23 +70,31 @@ __docformat__ = 'restructuredtext en'
# 6.6.3 - More cleanup of kindle book names and start of support for .kinf2018 # 6.6.3 - More cleanup of kindle book names and start of support for .kinf2018
# 6.7.0 - Handle new library in calibre. # 6.7.0 - Handle new library in calibre.
# 6.8.0 - Full support for .kinf2018 and new KFX encryption (Kindle for PC/Mac 2.5+) # 6.8.0 - Full support for .kinf2018 and new KFX encryption (Kindle for PC/Mac 2.5+)
# 6.8.1 - Kindle key fix for Mac OS X Big Syr # 6.8.1 - Kindle key fix for Mac OS X Big Sur
# 7.0.0 - Switched to Python 3 for calibre 5.0. Thanks to all who contributed
# 7.0.1 - More Python 3 changes. Adobe PDF decryption should now work in some cases
# 7.0.2 - More Python 3 changes. Adobe PDF decryption should now work on PC too.
# 7.0.3 - More Python 3 changes. Integer division in ineptpdf.py
# 7.1.0 - Full release for calibre 5.x
# 7.2.0 - Update for latest KFX changes, and Python 3 Obok fixes.
# 7.2.1 - Whitespace!
# 10.0.0 - First forked version by NoDRM. See CHANGELOG.md for details.
""" """
Decrypt DRMed ebooks. Decrypt DRMed ebooks.
""" """
PLUGIN_NAME = u"DeDRM" PLUGIN_NAME = "DeDRM"
PLUGIN_VERSION_TUPLE = (6, 8, 0) PLUGIN_VERSION_TUPLE = tuple([int(x) for x in __version__.split(".")])
PLUGIN_VERSION = u".".join([unicode(str(x)) for x in PLUGIN_VERSION_TUPLE]) PLUGIN_VERSION = ".".join([str(x)for x in PLUGIN_VERSION_TUPLE])
# Include an html helpfile in the plugin's zipfile with the following name. # Include an html helpfile in the plugin's zipfile with the following name.
RESOURCE_NAME = PLUGIN_NAME + '_Help.htm' RESOURCE_NAME = PLUGIN_NAME + '_Help.htm'
import sys, os, re import codecs
import sys, os
import time import time
import zipfile
import traceback import traceback
from zipfile import ZipFile
class DeDRMError(Exception): class DeDRMError(Exception):
pass pass
@@ -108,26 +115,31 @@ class SafeUnbuffered:
if self.encoding == None: if self.encoding == None:
self.encoding = "utf-8" self.encoding = "utf-8"
def write(self, data): def write(self, data):
if isinstance(data,unicode): if isinstance(data,str) or isinstance(data,unicode):
# str for Python3, unicode for Python2
data = data.encode(self.encoding,"replace") data = data.encode(self.encoding,"replace")
try: try:
self.stream.write(data) buffer = getattr(self.stream, 'buffer', self.stream)
self.stream.flush() # self.stream.buffer for Python3, self.stream for Python2
buffer.write(data)
buffer.flush()
except: except:
# We can do nothing if a write fails # We can do nothing if a write fails
pass raise
def __getattr__(self, attr): def __getattr__(self, attr):
return getattr(self.stream, attr) return getattr(self.stream, attr)
class DeDRM(FileTypePlugin): class DeDRM(FileTypePlugin):
name = PLUGIN_NAME name = PLUGIN_NAME
description = u"Removes DRM from Amazon Kindle, Adobe Adept (including Kobo), Barnes & Noble, Mobipocket and eReader ebooks. Credit given to i♥cabbages and The Dark Reverser for the original stand-alone scripts." description = "Removes DRM from Amazon Kindle, Adobe Adept (including Kobo), Readium LCP, Barnes & Noble, Mobipocket and eReader ebooks. Credit given to i♥cabbages and The Dark Reverser for the original stand-alone scripts."
supported_platforms = ['linux', 'osx', 'windows'] supported_platforms = ['linux', 'osx', 'windows']
author = u"Apprentice Alf, Aprentice Harper, The Dark Reverser and i♥cabbages" author = "Apprentice Alf, Apprentice Harper, NoDRM, The Dark Reverser and i♥cabbages"
version = PLUGIN_VERSION_TUPLE version = PLUGIN_VERSION_TUPLE
minimum_calibre_version = (1, 0, 0) # Compiled python libraries cannot be imported in earlier versions. #minimum_calibre_version = (5, 0, 0) # Python 3.
minimum_calibre_version = (2, 0, 0) # Needs Calibre 1.0 minimum. 1.X untested.
file_types = set(['epub','pdf','pdb','prc','mobi','pobi','azw','azw1','azw3','azw4','azw8','tpz','kfx','kfx-zip']) file_types = set(['epub','pdf','pdb','prc','mobi','pobi','azw','azw1','azw3','azw4','azw8','tpz','kfx','kfx-zip'])
on_import = True on_import = True
on_preprocess = True
priority = 600 priority = 600
@@ -143,30 +155,31 @@ class DeDRM(FileTypePlugin):
The extraction only happens once per version of the plugin The extraction only happens once per version of the plugin
Also perform upgrade of preferences once per version Also perform upgrade of preferences once per version
""" """
try: try:
self.pluginsdir = os.path.join(config_dir,u"plugins") self.pluginsdir = os.path.join(config_dir,"plugins")
if not os.path.exists(self.pluginsdir): if not os.path.exists(self.pluginsdir):
os.mkdir(self.pluginsdir) os.mkdir(self.pluginsdir)
self.maindir = os.path.join(self.pluginsdir,u"DeDRM") self.maindir = os.path.join(self.pluginsdir,"DeDRM")
if not os.path.exists(self.maindir): if not os.path.exists(self.maindir):
os.mkdir(self.maindir) os.mkdir(self.maindir)
self.helpdir = os.path.join(self.maindir,u"help") self.helpdir = os.path.join(self.maindir,"help")
if not os.path.exists(self.helpdir): if not os.path.exists(self.helpdir):
os.mkdir(self.helpdir) os.mkdir(self.helpdir)
self.alfdir = os.path.join(self.maindir,u"libraryfiles") self.alfdir = os.path.join(self.maindir,"libraryfiles")
if not os.path.exists(self.alfdir): if not os.path.exists(self.alfdir):
os.mkdir(self.alfdir) os.mkdir(self.alfdir)
# only continue if we've never run this version of the plugin before # only continue if we've never run this version of the plugin before
self.verdir = os.path.join(self.maindir,PLUGIN_VERSION) self.verdir = os.path.join(self.maindir,PLUGIN_VERSION)
if not os.path.exists(self.verdir): if not os.path.exists(self.verdir):
if iswindows: if iswindows:
names = [u"alfcrypto.dll",u"alfcrypto64.dll"] names = ["alfcrypto.dll","alfcrypto64.dll"]
elif isosx: elif isosx:
names = [u"libalfcrypto.dylib"] names = ["libalfcrypto.dylib"]
else: else:
names = [u"libalfcrypto32.so",u"libalfcrypto64.so",u"kindlekey.py",u"adobekey.py",u"subasyncio.py"] names = ["libalfcrypto32.so","libalfcrypto64.so","kindlekey.py","adobekey.py","subasyncio.py"]
lib_dict = self.load_resources(names) lib_dict = self.load_resources(names)
print u"{0} v{1}: Copying needed library files from plugin's zip".format(PLUGIN_NAME, PLUGIN_VERSION) print("{0} v{1}: Copying needed library files from plugin's zip".format(PLUGIN_NAME, PLUGIN_VERSION))
for entry, data in lib_dict.items(): for entry, data in lib_dict.items():
file_path = os.path.join(self.alfdir, entry) file_path = os.path.join(self.alfdir, entry)
@@ -178,7 +191,7 @@ class DeDRM(FileTypePlugin):
try: try:
open(file_path,'wb').write(data) open(file_path,'wb').write(data)
except: except:
print u"{0} v{1}: Exception when copying needed library files".format(PLUGIN_NAME, PLUGIN_VERSION) print("{0} v{1}: Exception when copying needed library files".format(PLUGIN_NAME, PLUGIN_VERSION))
traceback.print_exc() traceback.print_exc()
pass pass
@@ -188,47 +201,117 @@ class DeDRM(FileTypePlugin):
# mark that this version has been initialized # mark that this version has been initialized
os.mkdir(self.verdir) os.mkdir(self.verdir)
except Exception, e: except Exception as e:
traceback.print_exc() traceback.print_exc()
raise raise
def postProcessEPUB(self, path_to_ebook):
# This is called after the DRM is removed (or if no DRM was present)
# It does stuff like de-obfuscating fonts (by calling checkFonts)
# or removing watermarks.
try:
import calibre_plugins.dedrm.prefs as prefs
dedrmprefs = prefs.DeDRM_Prefs()
if dedrmprefs["deobfuscate_fonts"] is True:
# Deobfuscate fonts
path_to_ebook = self.checkFonts(path_to_ebook) or path_to_ebook
if dedrmprefs["remove_watermarks"] is True:
import calibre_plugins.dedrm.epubwatermark as watermark
# Remove Tolino's CDP watermark file
path_to_ebook = watermark.removeCDPwatermark(self, path_to_ebook) or path_to_ebook
# Remove watermarks (currently just Amazon) from the OPF file
path_to_ebook = watermark.removeOPFwatermarks(self, path_to_ebook) or path_to_ebook
# Remove watermarks (currently just Adobe's resource ID) from all HTML and XHTML files
path_to_ebook = watermark.removeHTMLwatermarks(self, path_to_ebook) or path_to_ebook
return path_to_ebook
except:
print("Error while checking settings")
return path_to_ebook
def checkFonts(self, path_to_ebook):
# This is called after the normal DRM removal is done.
# It checks if there's fonts that need to be deobfuscated
try:
import calibre_plugins.dedrm.epubfontdecrypt as epubfontdecrypt
output = self.temporary_file(".epub").name
ret = epubfontdecrypt.decryptFontsBook(path_to_ebook, output)
if (ret == 0):
return output
elif (ret == 1):
return path_to_ebook
else:
print("{0} v{1}: Error during font deobfuscation".format(PLUGIN_NAME, PLUGIN_VERSION))
raise DeDRMError("Font deobfuscation failed")
except:
print("{0} v{1}: Error during font deobfuscation".format(PLUGIN_NAME, PLUGIN_VERSION))
traceback.print_exc()
return path_to_ebook
def ePubDecrypt(self,path_to_ebook): def ePubDecrypt(self,path_to_ebook):
# Create a TemporaryPersistent file to work with. # Create a TemporaryPersistent file to work with.
# Check original epub archive for zip errors. # Check original epub archive for zip errors.
import calibre_plugins.dedrm.zipfix import calibre_plugins.dedrm.zipfix as zipfix
inf = self.temporary_file(u".epub") inf = self.temporary_file(".epub")
try: try:
print u"{0} v{1}: Verifying zip archive integrity".format(PLUGIN_NAME, PLUGIN_VERSION) print("{0} v{1}: Verifying zip archive integrity".format(PLUGIN_NAME, PLUGIN_VERSION))
fr = zipfix.fixZip(path_to_ebook, inf.name) fr = zipfix.fixZip(path_to_ebook, inf.name)
fr.fix() fr.fix()
except Exception, e: except Exception as e:
print u"{0} v{1}: Error \'{2}\' when checking zip archive".format(PLUGIN_NAME, PLUGIN_VERSION, e.args[0]) print("{0} v{1}: Error \'{2}\' when checking zip archive".format(PLUGIN_NAME, PLUGIN_VERSION, e.args[0]))
raise Exception(e) raise
# import the decryption keys # import the decryption keys
import calibre_plugins.dedrm.prefs as prefs import calibre_plugins.dedrm.prefs as prefs
dedrmprefs = prefs.DeDRM_Prefs() dedrmprefs = prefs.DeDRM_Prefs()
# import the LCP handler
import calibre_plugins.dedrm.lcpdedrm as lcpdedrm
if (lcpdedrm.isLCPbook(path_to_ebook)):
try:
retval = lcpdedrm.decryptLCPbook(path_to_ebook, dedrmprefs['lcp_passphrases'], self)
except:
print("Looks like that didn't work:")
raise
return self.postProcessEPUB(retval)
# Not an LCP book, do the normal EPUB (Adobe) handling.
# import the Barnes & Noble ePub handler # import the Barnes & Noble ePub handler
import calibre_plugins.dedrm.ignobleepub as ignobleepub import calibre_plugins.dedrm.ignobleepub as ignobleepub
#check the book #check the book
if ignobleepub.ignobleBook(inf.name): if ignobleepub.ignobleBook(inf.name):
print u"{0} v{1}: “{2}” is a secure Barnes & Noble ePub".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook)) print("{0} v{1}: “{2}” is a secure Barnes & Noble ePub".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook)))
# Attempt to decrypt epub with each encryption key (generated or provided). # Attempt to decrypt epub with each encryption key (generated or provided).
for keyname, userkey in dedrmprefs['bandnkeys'].items(): for keyname, userkey in dedrmprefs['bandnkeys'].items():
keyname_masked = u"".join((u'X' if (x.isdigit()) else x) for x in keyname) keyname_masked = "".join(("X" if (x.isdigit()) else x) for x in keyname)
print u"{0} v{1}: Trying Encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname_masked) print("{0} v{1}: Trying Encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname_masked))
of = self.temporary_file(u".epub") of = self.temporary_file(".epub")
# Give the user key, ebook and TemporaryPersistent file to the decryption function. # Give the user key, ebook and TemporaryPersistent file to the decryption function.
try: try:
result = ignobleepub.decryptBook(userkey, inf.name, of.name) result = ignobleepub.decryptBook(userkey, inf.name, of.name)
except: except:
print u"{0} v{1}: Exception when trying to decrypt after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime) print("{0} v{1}: Exception when trying to decrypt after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
traceback.print_exc() traceback.print_exc()
result = 1 result = 1
@@ -237,12 +320,12 @@ class DeDRM(FileTypePlugin):
if result == 0: if result == 0:
# Decryption was successful. # Decryption was successful.
# Return the modified PersistentTemporary file to calibre. # Return the modified PersistentTemporary file to calibre.
return of.name return self.postProcessEPUB(of.name)
print u"{0} v{1}: Failed to decrypt with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname_masked,time.time()-self.starttime) print("{0} v{1}: Failed to decrypt with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname_masked,time.time()-self.starttime))
# perhaps we should see if we can get a key from a log file # perhaps we should see if we can get a key from a log file
print u"{0} v{1}: Looking for new NOOK Study Keys after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime) print("{0} v{1}: Looking for new NOOK Study Keys after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
# get the default NOOK Study keys # get the default NOOK Study keys
defaultkeys = [] defaultkeys = []
@@ -253,13 +336,13 @@ class DeDRM(FileTypePlugin):
defaultkeys = nookkeys() defaultkeys = nookkeys()
else: # linux else: # linux
from wineutils import WineGetKeys from .wineutils import WineGetKeys
scriptpath = os.path.join(self.alfdir,u"ignoblekey.py") scriptpath = os.path.join(self.alfdir,"ignoblekey.py")
defaultkeys = WineGetKeys(scriptpath, u".b64",dedrmprefs['adobewineprefix']) defaultkeys = WineGetKeys(scriptpath, ".b64",dedrmprefs['adobewineprefix'])
except: except:
print u"{0} v{1}: Exception when getting default NOOK Study Key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime) print("{0} v{1}: Exception when getting default NOOK Study Key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
traceback.print_exc() traceback.print_exc()
newkeys = [] newkeys = []
@@ -270,15 +353,15 @@ class DeDRM(FileTypePlugin):
if len(newkeys) > 0: if len(newkeys) > 0:
try: try:
for i,userkey in enumerate(newkeys): for i,userkey in enumerate(newkeys):
print u"{0} v{1}: Trying a new default key".format(PLUGIN_NAME, PLUGIN_VERSION) print("{0} v{1}: Trying a new default key".format(PLUGIN_NAME, PLUGIN_VERSION))
of = self.temporary_file(u".epub") of = self.temporary_file(".epub")
# Give the user key, ebook and TemporaryPersistent file to the decryption function. # Give the user key, ebook and TemporaryPersistent file to the decryption function.
try: try:
result = ignobleepub.decryptBook(userkey, inf.name, of.name) result = ignobleepub.decryptBook(userkey, inf.name, of.name)
except: except:
print u"{0} v{1}: Exception when trying to decrypt after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime) print("{0} v{1}: Exception when trying to decrypt after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
traceback.print_exc() traceback.print_exc()
result = 1 result = 1
@@ -287,59 +370,100 @@ class DeDRM(FileTypePlugin):
if result == 0: if result == 0:
# Decryption was a success # Decryption was a success
# Store the new successful key in the defaults # Store the new successful key in the defaults
print u"{0} v{1}: Saving a new default key".format(PLUGIN_NAME, PLUGIN_VERSION) print("{0} v{1}: Saving a new default key".format(PLUGIN_NAME, PLUGIN_VERSION))
try: try:
dedrmprefs.addnamedvaluetoprefs('bandnkeys','nook_Study_key',keyvalue) dedrmprefs.addnamedvaluetoprefs('bandnkeys','nook_Study_key',keyvalue)
dedrmprefs.writeprefs() dedrmprefs.writeprefs()
print u"{0} v{1}: Saved a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime) print("{0} v{1}: Saved a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
except: except:
print u"{0} v{1}: Exception saving a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime) print("{0} v{1}: Exception saving a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
traceback.print_exc() traceback.print_exc()
# Return the modified PersistentTemporary file to calibre. # Return the modified PersistentTemporary file to calibre.
return of.name return self.postProcessEPUB(of.name)
print u"{0} v{1}: Failed to decrypt with new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime) print("{0} v{1}: Failed to decrypt with new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
except Exception, e: except Exception as e:
pass pass
print u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime) print("{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)) raise DeDRMError("{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
# import the Adobe Adept ePub handler # import the Adobe Adept ePub handler
import calibre_plugins.dedrm.ineptepub as ineptepub import calibre_plugins.dedrm.ineptepub as ineptepub
if ineptepub.adeptBook(inf.name): if ineptepub.adeptBook(inf.name):
print u"{0} v{1}: {2} is a secure Adobe Adept ePub".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook)) book_uuid = None
try:
# This tries to figure out which Adobe account UUID the book is licensed for.
# If we know that we can directly use the correct key instead of having to
# try them all.
book_uuid = ineptepub.adeptGetUserUUID(inf.name)
except:
pass
if book_uuid is None:
print("{0} v{1}: {2} is a secure Adobe Adept ePub".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook)))
else:
print("{0} v{1}: {2} is a secure Adobe Adept ePub for UUID {3}".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook), book_uuid))
if book_uuid is not None:
# Check if we have a key with that UUID in its name:
for keyname, userkeyhex in dedrmprefs['adeptkeys'].items():
if not book_uuid.lower() in keyname.lower():
continue
# Found matching key
userkey = codecs.decode(userkeyhex, 'hex')
print("{0} v{1}: Trying UUID-matched encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname))
of = self.temporary_file(".epub")
try:
result = ineptepub.decryptBook(userkey, inf.name, of.name)
of.close()
if result == 0:
print("{0} v{1}: Decrypted with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname,time.time()-self.starttime))
return self.postProcessEPUB(of.name)
except ineptepub.ADEPTNewVersionError:
print("{0} v{1}: Book uses unsupported (too new) Adobe DRM.".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
return self.postProcessEPUB(path_to_ebook)
except:
print("{0} v{1}: Exception when decrypting after {2:.1f} seconds - trying other keys".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
traceback.print_exc()
# Attempt to decrypt epub with each encryption key (generated or provided). # Attempt to decrypt epub with each encryption key (generated or provided).
for keyname, userkeyhex in dedrmprefs['adeptkeys'].items(): for keyname, userkeyhex in dedrmprefs['adeptkeys'].items():
userkey = userkeyhex.decode('hex') userkey = codecs.decode(userkeyhex, 'hex')
print u"{0} v{1}: Trying Encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname) print("{0} v{1}: Trying Encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname))
of = self.temporary_file(u".epub") of = self.temporary_file(".epub")
# Give the user key, ebook and TemporaryPersistent file to the decryption function. # Give the user key, ebook and TemporaryPersistent file to the decryption function.
try: try:
result = ineptepub.decryptBook(userkey, inf.name, of.name) result = ineptepub.decryptBook(userkey, inf.name, of.name)
except ineptepub.ADEPTNewVersionError:
print("{0} v{1}: Book uses unsupported (too new) Adobe DRM.".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
return self.postProcessEPUB(path_to_ebook)
except: except:
print u"{0} v{1}: Exception when decrypting after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime) print("{0} v{1}: Exception when decrypting after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
traceback.print_exc() traceback.print_exc()
result = 1 result = 1
try: try:
of.close() of.close()
except: except:
print u"{0} v{1}: Exception closing temporary file after {2:.1f} seconds. Ignored.".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime) print("{0} v{1}: Exception closing temporary file after {2:.1f} seconds. Ignored.".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
if result == 0: if result == 0:
# Decryption was successful. # Decryption was successful.
# Return the modified PersistentTemporary file to calibre. # Return the modified PersistentTemporary file to calibre.
print u"{0} v{1}: Decrypted with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname,time.time()-self.starttime) print("{0} v{1}: Decrypted with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname,time.time()-self.starttime))
return of.name return self.postProcessEPUB(of.name)
print u"{0} v{1}: Failed to decrypt with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname,time.time()-self.starttime) print("{0} v{1}: Failed to decrypt with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname,time.time()-self.starttime))
# perhaps we need to get a new default ADE key # perhaps we need to get a new default ADE key
print u"{0} v{1}: Looking for new default Adobe Digital Editions Keys after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime) print("{0} v{1}: Looking for new default Adobe Digital Editions Keys after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
# get the default Adobe keys # get the default Adobe keys
defaultkeys = [] defaultkeys = []
@@ -348,35 +472,57 @@ class DeDRM(FileTypePlugin):
if iswindows or isosx: if iswindows or isosx:
from calibre_plugins.dedrm.adobekey import adeptkeys from calibre_plugins.dedrm.adobekey import adeptkeys
defaultkeys = adeptkeys() defaultkeys, defaultnames = adeptkeys()
else: # linux else: # linux
from wineutils import WineGetKeys from .wineutils import WineGetKeys
scriptpath = os.path.join(self.alfdir,u"adobekey.py") scriptpath = os.path.join(self.alfdir,"adobekey.py")
defaultkeys = WineGetKeys(scriptpath, u".der",dedrmprefs['adobewineprefix']) defaultkeys, defaultnames = WineGetKeys(scriptpath, ".der",dedrmprefs['adobewineprefix'])
self.default_key = defaultkeys[0] try:
self.default_key = defaultkeys[0]
except:
print("{0} v{1}: No ADE key found".format(PLUGIN_NAME, PLUGIN_VERSION))
except: except:
print u"{0} v{1}: Exception when getting default Adobe Key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime) print("{0} v{1}: Exception when getting default Adobe Key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
traceback.print_exc() traceback.print_exc()
self.default_key = u"" self.default_key = ""
newkeys = [] newkeys = []
newnames = []
idx = 0
for keyvalue in defaultkeys: for keyvalue in defaultkeys:
if keyvalue.encode('hex') not in dedrmprefs['adeptkeys'].values(): if codecs.encode(keyvalue, 'hex').decode('ascii') not in dedrmprefs['adeptkeys'].values():
newkeys.append(keyvalue) newkeys.append(keyvalue)
newnames.append("default_ade_key_uuid_" + defaultnames[idx])
idx += 1
# Check for DeACSM keys:
try:
from calibre_plugins.dedrm.config import checkForDeACSMkeys
newkey, newname = checkForDeACSMkeys()
if newkey is not None:
if codecs.encode(newkey, 'hex').decode('ascii') not in dedrmprefs['adeptkeys'].values():
print("{0} v{1}: Found new key '{2}' in DeACSM plugin".format(PLUGIN_NAME, PLUGIN_VERSION, newname))
newkeys.append(newkey)
newnames.append(newname)
except:
traceback.print_exc()
pass
if len(newkeys) > 0: if len(newkeys) > 0:
try: try:
for i,userkey in enumerate(newkeys): for i,userkey in enumerate(newkeys):
print u"{0} v{1}: Trying a new default key".format(PLUGIN_NAME, PLUGIN_VERSION) print("{0} v{1}: Trying a new default key".format(PLUGIN_NAME, PLUGIN_VERSION))
of = self.temporary_file(u".epub") of = self.temporary_file(".epub")
# Give the user key, ebook and TemporaryPersistent file to the decryption function. # Give the user key, ebook and TemporaryPersistent file to the decryption function.
try: try:
result = ineptepub.decryptBook(userkey, inf.name, of.name) result = ineptepub.decryptBook(userkey, inf.name, of.name)
except: except:
print u"{0} v{1}: Exception when decrypting after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime) print("{0} v{1}: Exception when decrypting after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
traceback.print_exc() traceback.print_exc()
result = 1 result = 1
@@ -385,50 +531,106 @@ class DeDRM(FileTypePlugin):
if result == 0: if result == 0:
# Decryption was a success # Decryption was a success
# Store the new successful key in the defaults # Store the new successful key in the defaults
print u"{0} v{1}: Saving a new default key".format(PLUGIN_NAME, PLUGIN_VERSION) print("{0} v{1}: Saving a new default key".format(PLUGIN_NAME, PLUGIN_VERSION))
try: try:
dedrmprefs.addnamedvaluetoprefs('adeptkeys','default_key',keyvalue.encode('hex')) dedrmprefs.addnamedvaluetoprefs('adeptkeys', newnames[i], codecs.encode(userkey, 'hex').decode('ascii'))
dedrmprefs.writeprefs() dedrmprefs.writeprefs()
print u"{0} v{1}: Saved a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime) print("{0} v{1}: Saved a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
except: except:
print u"{0} v{1}: Exception when saving a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime) print("{0} v{1}: Exception when saving a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
traceback.print_exc() traceback.print_exc()
print u"{0} v{1}: Decrypted with new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime) print("{0} v{1}: Decrypted with new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
# Return the modified PersistentTemporary file to calibre. # Return the modified PersistentTemporary file to calibre.
return of.name return self.postProcessEPUB(of.name)
print u"{0} v{1}: Failed to decrypt with new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime) print("{0} v{1}: Failed to decrypt with new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
except Exception, e: except Exception as e:
print u"{0} v{1}: Unexpected Exception trying a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime) print("{0} v{1}: Unexpected Exception trying a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
traceback.print_exc() traceback.print_exc()
pass pass
# Something went wrong with decryption. # Something went wrong with decryption.
print u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime) print("{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)) raise DeDRMError("{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
# Not a Barnes & Noble nor an Adobe Adept # Not a Barnes & Noble nor an Adobe Adept
# Import the fixed epub. # Probably a DRM-free EPUB, but we should still check for fonts.
print u"{0} v{1}: “{2}” is neither an Adobe Adept nor a Barnes & Noble encrypted ePub".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook)) print("{0} v{1}: “{2}” is neither an Adobe Adept nor a Barnes & Noble encrypted ePub".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook)))
raise DeDRMError(u"{0} v{1}: Couldn't decrypt after {2:.1f} seconds. DRM free perhaps?".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)) return self.postProcessEPUB(inf.name)
#raise DeDRMError("{0} v{1}: Couldn't decrypt after {2:.1f} seconds. DRM free perhaps?".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
def PDFDecrypt(self,path_to_ebook): def PDFDecrypt(self,path_to_ebook):
import calibre_plugins.dedrm.prefs as prefs import calibre_plugins.dedrm.prefs as prefs
import calibre_plugins.dedrm.ineptpdf import calibre_plugins.dedrm.ineptpdf as ineptpdf
import calibre_plugins.dedrm.lcpdedrm as lcpdedrm
dedrmprefs = prefs.DeDRM_Prefs() dedrmprefs = prefs.DeDRM_Prefs()
# Attempt to decrypt epub with each encryption key (generated or provided).
print u"{0} v{1}: {2} is a PDF ebook".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook)) if (lcpdedrm.isLCPbook(path_to_ebook)):
try:
retval = lcpdedrm.decryptLCPbook(path_to_ebook, dedrmprefs['lcp_passphrases'], self)
except:
print("Looks like that didn't work:")
raise
return retval
# Not an LCP book, do the normal Adobe handling.
book_uuid = None
try:
# Try to figure out which Adobe account this book is licensed for.
book_uuid = ineptpdf.adeptGetUserUUID(path_to_ebook)
except:
pass
if book_uuid is None:
print("{0} v{1}: {2} is a PDF ebook".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook)))
else:
print("{0} v{1}: {2} is a PDF ebook for UUID {3}".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook), book_uuid))
if book_uuid is not None:
# Check if we have a key for that UUID
for keyname, userkeyhex in dedrmprefs['adeptkeys'].items():
if not book_uuid.lower() in keyname.lower():
continue
# Found matching key
userkey = codecs.decode(userkeyhex, 'hex')
print("{0} v{1}: Trying UUID-matched encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname))
of = self.temporary_file(".pdf")
try:
result = ineptpdf.decryptBook(userkey, path_to_ebook, of.name)
of.close()
if result == 0:
print("{0} v{1}: Decrypted with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname,time.time()-self.starttime))
return of.name
except ineptpdf.ADEPTNewVersionError:
print("{0} v{1}: Book uses unsupported (too new) Adobe DRM.".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
return path_to_ebook
except:
print("{0} v{1}: Exception when decrypting after {2:.1f} seconds - trying other keys".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
traceback.print_exc()
# If we end up here, we didn't find a key with a matching UUID, so lets just try all of them.
# Attempt to decrypt PDF with each encryption key (generated or provided).
for keyname, userkeyhex in dedrmprefs['adeptkeys'].items(): for keyname, userkeyhex in dedrmprefs['adeptkeys'].items():
userkey = userkeyhex.decode('hex') userkey = codecs.decode(userkeyhex,'hex')
print u"{0} v{1}: Trying Encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname) print("{0} v{1}: Trying encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname))
of = self.temporary_file(u".pdf") of = self.temporary_file(".pdf")
# Give the user key, ebook and TemporaryPersistent file to the decryption function. # Give the user key, ebook and TemporaryPersistent file to the decryption function.
try: try:
result = ineptpdf.decryptBook(userkey, path_to_ebook, of.name) result = ineptpdf.decryptBook(userkey, path_to_ebook, of.name)
except ineptpdf.ADEPTNewVersionError:
print("{0} v{1}: Book uses unsupported (too new) Adobe DRM.".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
return path_to_ebook
except: except:
print u"{0} v{1}: Exception when decrypting after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime) print("{0} v{1}: Exception when decrypting after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
traceback.print_exc() traceback.print_exc()
result = 1 result = 1
@@ -437,12 +639,13 @@ class DeDRM(FileTypePlugin):
if result == 0: if result == 0:
# Decryption was successful. # Decryption was successful.
# Return the modified PersistentTemporary file to calibre. # Return the modified PersistentTemporary file to calibre.
print("{0} v{1}: Decrypted with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname,time.time()-self.starttime))
return of.name return of.name
print u"{0} v{1}: Failed to decrypt with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname,time.time()-self.starttime) print("{0} v{1}: Failed to decrypt with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname,time.time()-self.starttime))
# perhaps we need to get a new default ADE key # perhaps we need to get a new default ADE key
print u"{0} v{1}: Looking for new default Adobe Digital Editions Keys after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime) print("{0} v{1}: Looking for new default Adobe Digital Editions Keys after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
# get the default Adobe keys # get the default Adobe keys
defaultkeys = [] defaultkeys = []
@@ -451,35 +654,56 @@ class DeDRM(FileTypePlugin):
if iswindows or isosx: if iswindows or isosx:
from calibre_plugins.dedrm.adobekey import adeptkeys from calibre_plugins.dedrm.adobekey import adeptkeys
defaultkeys = adeptkeys() defaultkeys, defaultnames = adeptkeys()
else: # linux else: # linux
from wineutils import WineGetKeys from .wineutils import WineGetKeys
scriptpath = os.path.join(self.alfdir,u"adobekey.py") scriptpath = os.path.join(self.alfdir,"adobekey.py")
defaultkeys = WineGetKeys(scriptpath, u".der",dedrmprefs['adobewineprefix']) defaultkeys, defaultnames = WineGetKeys(scriptpath, ".der",dedrmprefs['adobewineprefix'])
self.default_key = defaultkeys[0] try:
self.default_key = defaultkeys[0]
except:
print("{0} v{1}: No ADE key found".format(PLUGIN_NAME, PLUGIN_VERSION))
except: except:
print u"{0} v{1}: Exception when getting default Adobe Key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime) print("{0} v{1}: Exception when getting default Adobe Key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
traceback.print_exc() traceback.print_exc()
self.default_key = u"" self.default_key = ""
newkeys = [] newkeys = []
newnames = []
idx = 0
for keyvalue in defaultkeys: for keyvalue in defaultkeys:
if keyvalue.encode('hex') not in dedrmprefs['adeptkeys'].values(): if codecs.encode(keyvalue,'hex') not in dedrmprefs['adeptkeys'].values():
newkeys.append(keyvalue) newkeys.append(keyvalue)
newnames.append("default_ade_key_uuid_" + defaultnames[idx])
idx += 1
# Check for DeACSM keys:
try:
from calibre_plugins.dedrm.config import checkForDeACSMkeys
newkey, newname = checkForDeACSMkeys()
if newkey is not None:
if codecs.encode(newkey, 'hex').decode('ascii') not in dedrmprefs['adeptkeys'].values():
print("{0} v{1}: Found new key '{2}' in DeACSM plugin".format(PLUGIN_NAME, PLUGIN_VERSION, newname))
newkeys.append(keyvalue)
newnames.append(newname)
except:
pass
if len(newkeys) > 0: if len(newkeys) > 0:
try: try:
for i,userkey in enumerate(newkeys): for i,userkey in enumerate(newkeys):
print u"{0} v{1}: Trying a new default key".format(PLUGIN_NAME, PLUGIN_VERSION) print("{0} v{1}: Trying a new default key".format(PLUGIN_NAME, PLUGIN_VERSION))
of = self.temporary_file(u".pdf") of = self.temporary_file(".pdf")
# Give the user key, ebook and TemporaryPersistent file to the decryption function. # Give the user key, ebook and TemporaryPersistent file to the decryption function.
try: try:
result = ineptpdf.decryptBook(userkey, path_to_ebook, of.name) result = ineptpdf.decryptBook(userkey, path_to_ebook, of.name)
except: except:
print u"{0} v{1}: Exception when decrypting after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime) print("{0} v{1}: Exception when decrypting after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
traceback.print_exc() traceback.print_exc()
result = 1 result = 1
@@ -488,24 +712,54 @@ class DeDRM(FileTypePlugin):
if result == 0: if result == 0:
# Decryption was a success # Decryption was a success
# Store the new successful key in the defaults # Store the new successful key in the defaults
print u"{0} v{1}: Saving a new default key".format(PLUGIN_NAME, PLUGIN_VERSION) print("{0} v{1}: Saving a new default key".format(PLUGIN_NAME, PLUGIN_VERSION))
try: try:
dedrmprefs.addnamedvaluetoprefs('adeptkeys','default_key',keyvalue.encode('hex')) dedrmprefs.addnamedvaluetoprefs('adeptkeys', newnames[i], codecs.encode(userkey,'hex').decode('ascii'))
dedrmprefs.writeprefs() dedrmprefs.writeprefs()
print u"{0} v{1}: Saved a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime) print("{0} v{1}: Saved a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
except: except:
print u"{0} v{1}: Exception when saving a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime) print("{0} v{1}: Exception when saving a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
traceback.print_exc() traceback.print_exc()
# Return the modified PersistentTemporary file to calibre. # Return the modified PersistentTemporary file to calibre.
return of.name return of.name
print u"{0} v{1}: Failed to decrypt with new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime) print("{0} v{1}: Failed to decrypt with new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
except Exception, e: except Exception as e:
pass pass
# Unable to decrypt the PDF with any of the existing keys. Is it a B&N PDF?
# Attempt to decrypt PDF with each encryption key (generated or provided).
for keyname, userkey in dedrmprefs['bandnkeys'].items():
keyname_masked = "".join(("X" if (x.isdigit()) else x) for x in keyname)
print("{0} v{1}: Trying Encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname_masked))
of = self.temporary_file(".pdf")
# Give the user key, ebook and TemporaryPersistent file to the decryption function.
try:
result = ineptpdf.decryptBook(userkey, path_to_ebook, of.name, False)
except ineptpdf.ADEPTNewVersionError:
print("{0} v{1}: Book uses unsupported (too new) Adobe DRM.".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
return path_to_ebook
except:
print("{0} v{1}: Exception when decrypting after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
traceback.print_exc()
result = 1
of.close()
if result == 0:
# Decryption was successful.
# Return the modified PersistentTemporary file to calibre.
print("{0} v{1}: Decrypted with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname,time.time()-self.starttime))
return of.name
print("{0} v{1}: Failed to decrypt with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname,time.time()-self.starttime))
# Something went wrong with decryption. # Something went wrong with decryption.
print u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime) print("{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)) raise DeDRMError("{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
def KindleMobiDecrypt(self,path_to_ebook): def KindleMobiDecrypt(self,path_to_ebook):
@@ -527,16 +781,16 @@ class DeDRM(FileTypePlugin):
serials.extend(android_serials_list) serials.extend(android_serials_list)
#print serials #print serials
androidFiles = [] androidFiles = []
kindleDatabases = dedrmprefs['kindlekeys'].items() kindleDatabases = list(dedrmprefs['kindlekeys'].items())
try: try:
book = k4mobidedrm.GetDecryptedBook(path_to_ebook,kindleDatabases,androidFiles,serials,pids,self.starttime) book = k4mobidedrm.GetDecryptedBook(path_to_ebook,kindleDatabases,androidFiles,serials,pids,self.starttime)
except Exception, e: except Exception as e:
decoded = False decoded = False
# perhaps we need to get a new default Kindle for Mac/PC key # perhaps we need to get a new default Kindle for Mac/PC key
defaultkeys = [] defaultkeys = []
print u"{0} v{1}: Failed to decrypt with error: {2}".format(PLUGIN_NAME, PLUGIN_VERSION,e.args[0]) print("{0} v{1}: Failed to decrypt with error: {2}".format(PLUGIN_NAME, PLUGIN_VERSION,e.args[0]))
print u"{0} v{1}: Looking for new default Kindle Key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime) print("{0} v{1}: Looking for new default Kindle Key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
try: try:
if iswindows or isosx: if iswindows or isosx:
@@ -544,36 +798,36 @@ class DeDRM(FileTypePlugin):
defaultkeys = kindlekeys() defaultkeys = kindlekeys()
else: # linux else: # linux
from wineutils import WineGetKeys from .wineutils import WineGetKeys
scriptpath = os.path.join(self.alfdir,u"kindlekey.py") scriptpath = os.path.join(self.alfdir,"kindlekey.py")
defaultkeys = WineGetKeys(scriptpath, u".k4i",dedrmprefs['kindlewineprefix']) defaultkeys = WineGetKeys(scriptpath, ".k4i",dedrmprefs['kindlewineprefix'])
except: except:
print u"{0} v{1}: Exception when getting default Kindle Key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime) print("{0} v{1}: Exception when getting default Kindle Key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
traceback.print_exc() traceback.print_exc()
pass pass
newkeys = {} newkeys = {}
for i,keyvalue in enumerate(defaultkeys): for i,keyvalue in enumerate(defaultkeys):
keyname = u"default_key_{0:d}".format(i+1) keyname = "default_key_{0:d}".format(i+1)
if keyvalue not in dedrmprefs['kindlekeys'].values(): if keyvalue not in dedrmprefs['kindlekeys'].values():
newkeys[keyname] = keyvalue newkeys[keyname] = keyvalue
if len(newkeys) > 0: if len(newkeys) > 0:
print u"{0} v{1}: Found {2} new {3}".format(PLUGIN_NAME, PLUGIN_VERSION, len(newkeys), u"key" if len(newkeys)==1 else u"keys") print("{0} v{1}: Found {2} new {3}".format(PLUGIN_NAME, PLUGIN_VERSION, len(newkeys), "key" if len(newkeys)==1 else "keys"))
try: try:
book = k4mobidedrm.GetDecryptedBook(path_to_ebook,newkeys.items(),[],[],[],self.starttime) book = k4mobidedrm.GetDecryptedBook(path_to_ebook,list(newkeys.items()),[],[],[],self.starttime)
decoded = True decoded = True
# store the new successful keys in the defaults # store the new successful keys in the defaults
print u"{0} v{1}: Saving {2} new {3}".format(PLUGIN_NAME, PLUGIN_VERSION, len(newkeys), u"key" if len(newkeys)==1 else u"keys") print("{0} v{1}: Saving {2} new {3}".format(PLUGIN_NAME, PLUGIN_VERSION, len(newkeys), "key" if len(newkeys)==1 else "keys"))
for keyvalue in newkeys.values(): for keyvalue in newkeys.values():
dedrmprefs.addnamedvaluetoprefs('kindlekeys','default_key',keyvalue) dedrmprefs.addnamedvaluetoprefs('kindlekeys','default_key',keyvalue)
dedrmprefs.writeprefs() dedrmprefs.writeprefs()
except Exception, e: except Exception as e:
pass pass
if not decoded: if not decoded:
#if you reached here then no luck raise and exception #if you reached here then no luck raise and exception
print u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime) print("{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime)) raise DeDRMError("{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
of = self.temporary_file(book.getBookExtension()) of = self.temporary_file(book.getBookExtension())
book.getFile(of.name) book.getFile(of.name)
@@ -590,25 +844,25 @@ class DeDRM(FileTypePlugin):
dedrmprefs = prefs.DeDRM_Prefs() dedrmprefs = prefs.DeDRM_Prefs()
# Attempt to decrypt epub with each encryption key (generated or provided). # Attempt to decrypt epub with each encryption key (generated or provided).
for keyname, userkey in dedrmprefs['ereaderkeys'].items(): for keyname, userkey in dedrmprefs['ereaderkeys'].items():
keyname_masked = u"".join((u'X' if (x.isdigit()) else x) for x in keyname) keyname_masked = "".join(("X" if (x.isdigit()) else x) for x in keyname)
print u"{0} v{1}: Trying Encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname_masked) print("{0} v{1}: Trying Encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname_masked))
of = self.temporary_file(u".pmlz") of = self.temporary_file(".pmlz")
# Give the userkey, ebook and TemporaryPersistent file to the decryption function. # Give the userkey, ebook and TemporaryPersistent file to the decryption function.
result = erdr2pml.decryptBook(path_to_ebook, of.name, True, userkey.decode('hex')) result = erdr2pml.decryptBook(path_to_ebook, of.name, True, codecs.decode(userkey,'hex'))
of.close() of.close()
# Decryption was successful return the modified PersistentTemporary # Decryption was successful return the modified PersistentTemporary
# file to Calibre's import process. # file to Calibre's import process.
if result == 0: if result == 0:
print u"{0} v{1}: Successfully decrypted with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname_masked,time.time()-self.starttime) print("{0} v{1}: Successfully decrypted with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname_masked,time.time()-self.starttime))
return of.name return of.name
print u"{0} v{1}: Failed to decrypt with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname_masked,time.time()-self.starttime) print("{0} v{1}: Failed to decrypt with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname_masked,time.time()-self.starttime))
print u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime) print("{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
raise DeDRMError(u"{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime)) raise DeDRMError("{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
def run(self, path_to_ebook): def run(self, path_to_ebook):
@@ -617,7 +871,7 @@ class DeDRM(FileTypePlugin):
sys.stdout=SafeUnbuffered(sys.stdout) sys.stdout=SafeUnbuffered(sys.stdout)
sys.stderr=SafeUnbuffered(sys.stderr) sys.stderr=SafeUnbuffered(sys.stderr)
print u"{0} v{1}: Trying to decrypt {2}".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook)) print("{0} v{1}: Trying to decrypt {2}".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook)))
self.starttime = time.time() self.starttime = time.time()
booktype = os.path.splitext(path_to_ebook)[1].lower()[1:] booktype = os.path.splitext(path_to_ebook)[1].lower()[1:]
@@ -636,9 +890,9 @@ class DeDRM(FileTypePlugin):
# Adobe Adept or B&N ePub # Adobe Adept or B&N ePub
decrypted_ebook = self.ePubDecrypt(path_to_ebook) decrypted_ebook = self.ePubDecrypt(path_to_ebook)
else: else:
print u"Unknown booktype {0}. Passing back to calibre unchanged".format(booktype) print("Unknown booktype {0}. Passing back to calibre unchanged".format(booktype))
return path_to_ebook return path_to_ebook
print u"{0} v{1}: Finished after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime) print("{0} v{1}: Finished after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
return decrypted_ebook return decrypted_ebook
def is_customizable(self): def is_customizable(self):

View File

@@ -1,12 +1,12 @@
import sys import sys
import Tkinter import tkinter
import Tkconstants import tkinter.constants
class ActivityBar(Tkinter.Frame): class ActivityBar(tkinter.Frame):
def __init__(self, master, length=300, height=20, barwidth=15, interval=50, bg='white', fillcolor='orchid1',\ def __init__(self, master, length=300, height=20, barwidth=15, interval=50, bg='white', fillcolor='orchid1',\
bd=2, relief=Tkconstants.GROOVE, *args, **kw): bd=2, relief=tkinter.constants.GROOVE, *args, **kw):
Tkinter.Frame.__init__(self, master, bg=bg, width=length, height=height, *args, **kw) tkinter.Frame.__init__(self, master, bg=bg, width=length, height=height, *args, **kw)
self._master = master self._master = master
self._interval = interval self._interval = interval
self._maximum = length self._maximum = length
@@ -20,7 +20,7 @@ class ActivityBar(Tkinter.Frame):
stopx = self._maximum stopx = self._maximum
# self._canv = Tkinter.Canvas(self, bg=self['bg'], width=self['width'], height=self['height'],\ # self._canv = Tkinter.Canvas(self, bg=self['bg'], width=self['width'], height=self['height'],\
# highlightthickness=0, relief='flat', bd=0) # highlightthickness=0, relief='flat', bd=0)
self._canv = Tkinter.Canvas(self, bg=self['bg'], width=self['width'], height=self['height'],\ self._canv = tkinter.Canvas(self, bg=self['bg'], width=self['width'], height=self['height'],\
highlightthickness=0, relief=relief, bd=bd) highlightthickness=0, relief=relief, bd=bd)
self._canv.pack(fill='both', expand=1) self._canv.pack(fill='both', expand=1)
self._rect = self._canv.create_rectangle(0, 0, self._canv.winfo_reqwidth(), self._canv.winfo_reqheight(), fill=fillcolor, width=0) self._rect = self._canv.create_rectangle(0, 0, self._canv.winfo_reqwidth(), self._canv.winfo_reqheight(), fill=fillcolor, width=0)

View File

@@ -1,32 +1,12 @@
#!/usr/bin/env python #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import with_statement
# adobekey.pyw, version 6.0 # adobekey.pyw, version 6.0
# Copyright © 2009-2010 i♥cabbages # Copyright © 2009-2020 i♥cabbages, Apprentice Harper et al.
# Released under the terms of the GNU General Public Licence, version 3 # Released under the terms of the GNU General Public Licence, version 3
# <http://www.gnu.org/licenses/> # <http://www.gnu.org/licenses/>
# 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: # Revision history:
# 1 - Initial release, for Adobe Digital Editions 1.7 # 1 - Initial release, for Adobe Digital Editions 1.7
# 2 - Better algorithm for finding pLK; improved error handling # 2 - Better algorithm for finding pLK; improved error handling
@@ -48,16 +28,18 @@ from __future__ import with_statement
# 5.8 - Added getkey interface for Windows DeDRM application # 5.8 - Added getkey interface for Windows DeDRM application
# 5.9 - moved unicode_argv call inside main for Windows DeDRM compatibility # 5.9 - moved unicode_argv call inside main for Windows DeDRM compatibility
# 6.0 - Work if TkInter is missing # 6.0 - Work if TkInter is missing
# 7.0 - Python 3 for calibre 5
""" """
Retrieve Adobe ADEPT user key. Retrieve Adobe ADEPT user key.
""" """
from __future__ import print_function
__license__ = 'GPL v3' __license__ = 'GPL v3'
__version__ = '6.0' __version__ = '7.0'
import sys, os, struct, getopt import sys, os, struct, getopt
from base64 import b64decode
# Wrap a stream so that output gets flushed immediately # Wrap a stream so that output gets flushed immediately
# and also make sure that any unicode strings get # and also make sure that any unicode strings get
@@ -69,10 +51,17 @@ class SafeUnbuffered:
if self.encoding == None: if self.encoding == None:
self.encoding = "utf-8" self.encoding = "utf-8"
def write(self, data): def write(self, data):
if isinstance(data,unicode): if isinstance(data,str) or isinstance(data,unicode):
# str for Python3, unicode for Python2
data = data.encode(self.encoding,"replace") data = data.encode(self.encoding,"replace")
self.stream.write(data) try:
self.stream.flush() buffer = getattr(self.stream, 'buffer', self.stream)
# self.stream.buffer for Python3, self.stream for Python2
buffer.write(data)
buffer.flush()
except:
# We can do nothing if a write fails
raise
def __getattr__(self, attr): def __getattr__(self, attr):
return getattr(self.stream, attr) return getattr(self.stream, attr)
@@ -110,15 +99,13 @@ def unicode_argv():
# Remove Python executable and commands if present # Remove Python executable and commands if present
start = argc.value - len(sys.argv) start = argc.value - len(sys.argv)
return [argv[i] for i in return [argv[i] for i in
xrange(start, argc.value)] range(start, argc.value)]
# if we don't have any arguments at all, just pass back script name # if we don't have any arguments at all, just pass back script name
# this should never happen # this should never happen
return [u"adobekey.py"] return ["adobekey.py"]
else: else:
argvencoding = sys.stdin.encoding argvencoding = sys.stdin.encoding or "utf-8"
if argvencoding == None: return [arg if (isinstance(arg, str) or isinstance(arg,unicode)) else str(arg, argvencoding) for arg in sys.argv]
argvencoding = "utf-8"
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
class ADEPTError(Exception): class ADEPTError(Exception):
pass pass
@@ -130,11 +117,16 @@ if iswindows:
c_long, c_ulong c_long, c_ulong
from ctypes.wintypes import LPVOID, DWORD, BOOL from ctypes.wintypes import LPVOID, DWORD, BOOL
import _winreg as winreg try:
import winreg
except ImportError:
import _winreg as winreg
def _load_crypto_libcrypto(): def _load_crypto_libcrypto():
from ctypes.util import find_library from ctypes.util import find_library
libcrypto = find_library('libeay32') libcrypto = find_library('libcrypto-1_1')
if libcrypto is None:
libcrypto = find_library('libeay32')
if libcrypto is None: if libcrypto is None:
raise ADEPTError('libcrypto not found') raise ADEPTError('libcrypto not found')
libcrypto = CDLL(libcrypto) libcrypto = CDLL(libcrypto)
@@ -168,7 +160,7 @@ if iswindows:
raise ADEPTError('Failed to initialize AES key') raise ADEPTError('Failed to initialize AES key')
def decrypt(self, data): def decrypt(self, data):
out = create_string_buffer(len(data)) out = create_string_buffer(len(data))
iv = ("\x00" * self._blocksize) iv = (b"\x00" * self._blocksize)
rv = AES_cbc_encrypt(data, out, len(data), self._key, iv, 0) rv = AES_cbc_encrypt(data, out, len(data), self._key, iv, 0)
if rv == 0: if rv == 0:
raise ADEPTError('AES decryption failed') raise ADEPTError('AES decryption failed')
@@ -179,7 +171,7 @@ if iswindows:
from Crypto.Cipher import AES as _AES from Crypto.Cipher import AES as _AES
class AES(object): class AES(object):
def __init__(self, key): def __init__(self, key):
self._aes = _AES.new(key, _AES.MODE_CBC, '\x00'*16) self._aes = _AES.new(key, _AES.MODE_CBC, b'\x00'*16)
def decrypt(self, data): def decrypt(self, data):
return self._aes.decrypt(data) return self._aes.decrypt(data)
return AES return AES
@@ -287,44 +279,44 @@ if iswindows:
if struct.calcsize("P") == 4: if struct.calcsize("P") == 4:
CPUID0_INSNS = ( CPUID0_INSNS = (
"\x53" # push %ebx b"\x53" # push %ebx
"\x31\xc0" # xor %eax,%eax b"\x31\xc0" # xor %eax,%eax
"\x0f\xa2" # cpuid b"\x0f\xa2" # cpuid
"\x8b\x44\x24\x08" # mov 0x8(%esp),%eax b"\x8b\x44\x24\x08" # mov 0x8(%esp),%eax
"\x89\x18" # mov %ebx,0x0(%eax) b"\x89\x18" # mov %ebx,0x0(%eax)
"\x89\x50\x04" # mov %edx,0x4(%eax) b"\x89\x50\x04" # mov %edx,0x4(%eax)
"\x89\x48\x08" # mov %ecx,0x8(%eax) b"\x89\x48\x08" # mov %ecx,0x8(%eax)
"\x5b" # pop %ebx b"\x5b" # pop %ebx
"\xc3" # ret b"\xc3" # ret
) )
CPUID1_INSNS = ( CPUID1_INSNS = (
"\x53" # push %ebx b"\x53" # push %ebx
"\x31\xc0" # xor %eax,%eax b"\x31\xc0" # xor %eax,%eax
"\x40" # inc %eax b"\x40" # inc %eax
"\x0f\xa2" # cpuid b"\x0f\xa2" # cpuid
"\x5b" # pop %ebx b"\x5b" # pop %ebx
"\xc3" # ret b"\xc3" # ret
) )
else: else:
CPUID0_INSNS = ( CPUID0_INSNS = (
"\x49\x89\xd8" # mov %rbx,%r8 b"\x49\x89\xd8" # mov %rbx,%r8
"\x49\x89\xc9" # mov %rcx,%r9 b"\x49\x89\xc9" # mov %rcx,%r9
"\x48\x31\xc0" # xor %rax,%rax b"\x48\x31\xc0" # xor %rax,%rax
"\x0f\xa2" # cpuid b"\x0f\xa2" # cpuid
"\x4c\x89\xc8" # mov %r9,%rax b"\x4c\x89\xc8" # mov %r9,%rax
"\x89\x18" # mov %ebx,0x0(%rax) b"\x89\x18" # mov %ebx,0x0(%rax)
"\x89\x50\x04" # mov %edx,0x4(%rax) b"\x89\x50\x04" # mov %edx,0x4(%rax)
"\x89\x48\x08" # mov %ecx,0x8(%rax) b"\x89\x48\x08" # mov %ecx,0x8(%rax)
"\x4c\x89\xc3" # mov %r8,%rbx b"\x4c\x89\xc3" # mov %r8,%rbx
"\xc3" # retq b"\xc3" # retq
) )
CPUID1_INSNS = ( CPUID1_INSNS = (
"\x53" # push %rbx b"\x53" # push %rbx
"\x48\x31\xc0" # xor %rax,%rax b"\x48\x31\xc0" # xor %rax,%rax
"\x48\xff\xc0" # inc %rax b"\x48\xff\xc0" # inc %rax
"\x0f\xa2" # cpuid b"\x0f\xa2" # cpuid
"\x5b" # pop %rbx b"\x5b" # pop %rbx
"\xc3" # retq b"\xc3" # retq
) )
def cpuid0(): def cpuid0():
@@ -379,11 +371,12 @@ if iswindows:
keykey = CryptUnprotectData(device, entropy) keykey = CryptUnprotectData(device, entropy)
userkey = None userkey = None
keys = [] keys = []
names = []
try: try:
plkroot = winreg.OpenKey(cuser, PRIVATE_LICENCE_KEY_PATH) plkroot = winreg.OpenKey(cuser, PRIVATE_LICENCE_KEY_PATH)
except WindowsError: except WindowsError:
raise ADEPTError("Could not locate ADE activation") raise ADEPTError("Could not locate ADE activation")
for i in xrange(0, 16): for i in range(0, 16):
try: try:
plkparent = winreg.OpenKey(plkroot, "%04d" % (i,)) plkparent = winreg.OpenKey(plkroot, "%04d" % (i,))
except WindowsError: except WindowsError:
@@ -391,25 +384,29 @@ if iswindows:
ktype = winreg.QueryValueEx(plkparent, None)[0] ktype = winreg.QueryValueEx(plkparent, None)[0]
if ktype != 'credentials': if ktype != 'credentials':
continue continue
for j in xrange(0, 16): uuid_name = "Unknown"
for j in range(0, 16):
try: try:
plkkey = winreg.OpenKey(plkparent, "%04d" % (j,)) plkkey = winreg.OpenKey(plkparent, "%04d" % (j,))
except WindowsError: except WindowsError:
break break
ktype = winreg.QueryValueEx(plkkey, None)[0] ktype = winreg.QueryValueEx(plkkey, None)[0]
if ktype == 'user':
uuid_name = winreg.QueryValueEx(plkkey, 'value')[0]
if ktype != 'privateLicenseKey': if ktype != 'privateLicenseKey':
continue continue
userkey = winreg.QueryValueEx(plkkey, 'value')[0] userkey = winreg.QueryValueEx(plkkey, 'value')[0]
userkey = userkey.decode('base64') userkey = b64decode(userkey)
aes = AES(keykey) aes = AES(keykey)
userkey = aes.decrypt(userkey) userkey = aes.decrypt(userkey)
userkey = userkey[26:-ord(userkey[-1])] userkey = userkey[26:-ord(userkey[-1:])]
#print "found key:",userkey.encode('hex') # print ("found " + uuid_name + " key: " + str(userkey))
keys.append(userkey) keys.append(userkey)
names.append(uuid_name[9:])
if len(keys) == 0: if len(keys) == 0:
raise ADEPTError('Could not locate privateLicenseKey') raise ADEPTError('Could not locate privateLicenseKey')
print(u"Found {0:d} keys".format(len(keys))) print("Found {0:d} keys".format(len(keys)))
return keys return keys, names
elif isosx: elif isosx:
@@ -428,12 +425,12 @@ elif isosx:
cmdline = cmdline.encode(sys.getfilesystemencoding()) cmdline = cmdline.encode(sys.getfilesystemencoding())
p2 = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False) p2 = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
out1, out2 = p2.communicate() out1, out2 = p2.communicate()
reslst = out1.split('\n') reslst = out1.split(b'\n')
cnt = len(reslst) cnt = len(reslst)
ActDatPath = "activation.dat" ActDatPath = b"activation.dat"
for j in xrange(cnt): for j in range(cnt):
resline = reslst[j] resline = reslst[j]
pp = resline.find('activation.dat') pp = resline.find(b'activation.dat')
if pp >= 0: if pp >= 0:
ActDatPath = resline ActDatPath = resline
break break
@@ -448,57 +445,65 @@ elif isosx:
tree = etree.parse(actpath) tree = etree.parse(actpath)
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag) adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
expr = '//%s/%s' % (adept('credentials'), adept('privateLicenseKey')) expr = '//%s/%s' % (adept('credentials'), adept('privateLicenseKey'))
exprUUID = '//%s/%s' % (adept('credentials'), adept('user'))
userkey = tree.findtext(expr) userkey = tree.findtext(expr)
userkey = userkey.decode('base64') userUUID = "Unknown"
try:
userUUID = tree.findtext(exprUUID)
except:
pass
userkey = b64decode(userkey)
userkey = userkey[26:] userkey = userkey[26:]
return [userkey] return [userkey], [userUUID[9:]]
else: else:
def adeptkeys(): def adeptkeys():
raise ADEPTError("This script only supports Windows and Mac OS X.") raise ADEPTError("This script only supports Windows and Mac OS X.")
return [] return [], []
# interface for Python DeDRM # interface for Python DeDRM
def getkey(outpath): def getkey(outpath):
keys = adeptkeys() keys, names = adeptkeys()
if len(keys) > 0: if len(keys) > 0:
if not os.path.isdir(outpath): if not os.path.isdir(outpath):
outfile = outpath outfile = outpath
with file(outfile, 'wb') as keyfileout: with open(outfile, 'wb') as keyfileout:
keyfileout.write(keys[0]) keyfileout.write(keys[0])
print(u"Saved a key to {0}".format(outfile)) print("Saved a key to {0}".format(outfile))
else: else:
keycount = 0 keycount = 0
name_index = 0
for key in keys: for key in keys:
while True: while True:
keycount += 1 keycount += 1
outfile = os.path.join(outpath,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): if not os.path.exists(outfile):
break break
with file(outfile, 'wb') as keyfileout: with open(outfile, 'wb') as keyfileout:
keyfileout.write(key) 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 True
return False return False
def usage(progname): def usage(progname):
print(u"Finds, decrypts and saves the default Adobe Adept encryption key(s).") print("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("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("If a file name is passed instead of a directory, only the first key is saved, in that file.")
print(u"Usage:") print("Usage:")
print(u" {0:s} [-h] [<outpath>]".format(progname)) print(" {0:s} [-h] [<outpath>]".format(progname))
def cli_main(): def cli_main():
sys.stdout=SafeUnbuffered(sys.stdout) sys.stdout=SafeUnbuffered(sys.stdout)
sys.stderr=SafeUnbuffered(sys.stderr) sys.stderr=SafeUnbuffered(sys.stderr)
argv=unicode_argv() argv=unicode_argv()
progname = os.path.basename(argv[0]) 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: try:
opts, args = getopt.getopt(argv[1:], "h") opts, args = getopt.getopt(argv[1:], "h")
except getopt.GetoptError, err: except getopt.GetoptError as err:
print(u"Error in options or arguments: {0}".format(err.args[0])) print("Error in options or arguments: {0}".format(err.args[0]))
usage(progname) usage(progname)
sys.exit(2) sys.exit(2)
@@ -523,76 +528,82 @@ def cli_main():
# make sure the outpath is the # make sure the outpath is the
outpath = os.path.realpath(os.path.normpath(outpath)) outpath = os.path.realpath(os.path.normpath(outpath))
keys = adeptkeys() keys, names = adeptkeys()
if len(keys) > 0: if len(keys) > 0:
if not os.path.isdir(outpath): if not os.path.isdir(outpath):
outfile = outpath outfile = outpath
with file(outfile, 'wb') as keyfileout: with open(outfile, 'wb') as keyfileout:
keyfileout.write(keys[0]) keyfileout.write(keys[0])
print(u"Saved a key to {0}".format(outfile)) print("Saved a key to {0}".format(outfile))
else: else:
keycount = 0 keycount = 0
name_index = 0
for key in keys: for key in keys:
while True: while True:
keycount += 1 keycount += 1
outfile = os.path.join(outpath,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): if not os.path.exists(outfile):
break break
with file(outfile, 'wb') as keyfileout: with open(outfile, 'wb') as keyfileout:
keyfileout.write(key) keyfileout.write(key)
print(u"Saved a key to {0}".format(outfile)) print("Saved a key to {0}".format(outfile))
name_index += 1
else: else:
print(u"Could not retrieve Adobe Adept key.") print("Could not retrieve Adobe Adept key.")
return 0 return 0
def gui_main(): def gui_main():
try: try:
import Tkinter import tkinter
import Tkconstants import tkinter.constants
import tkMessageBox import tkinter.messagebox
import traceback import traceback
except: except:
return cli_main() return cli_main()
class ExceptionDialog(Tkinter.Frame): class ExceptionDialog(tkinter.Frame):
def __init__(self, root, text): def __init__(self, root, text):
Tkinter.Frame.__init__(self, root, border=5) tkinter.Frame.__init__(self, root, border=5)
label = Tkinter.Label(self, text=u"Unexpected error:", label = tkinter.Label(self, text="Unexpected error:",
anchor=Tkconstants.W, justify=Tkconstants.LEFT) anchor=tkinter.constants.W, justify=tkinter.constants.LEFT)
label.pack(fill=Tkconstants.X, expand=0) label.pack(fill=tkinter.constants.X, expand=0)
self.text = Tkinter.Text(self) self.text = tkinter.Text(self)
self.text.pack(fill=Tkconstants.BOTH, expand=1) 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() argv=unicode_argv()
root = Tkinter.Tk() root = tkinter.Tk()
root.withdraw() root.withdraw()
progpath, progname = os.path.split(argv[0]) progpath, progname = os.path.split(argv[0])
success = False success = False
try: try:
keys = adeptkeys() keys, names = adeptkeys()
print(keys)
print(names)
keycount = 0 keycount = 0
name_index = 0
for key in keys: for key in keys:
while True: while True:
keycount += 1 keycount += 1
outfile = os.path.join(progpath,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): if not os.path.exists(outfile):
break break
with file(outfile, 'wb') as keyfileout: with open(outfile, 'wb') as keyfileout:
keyfileout.write(key) keyfileout.write(key)
success = True success = True
tkMessageBox.showinfo(progname, u"Key successfully retrieved to {0}".format(outfile)) tkinter.messagebox.showinfo(progname, "Key successfully retrieved to {0}".format(outfile))
except ADEPTError, e: name_index += 1
tkMessageBox.showerror(progname, u"Error: {0}".format(str(e))) except ADEPTError as e:
tkinter.messagebox.showerror(progname, "Error: {0}".format(str(e)))
except Exception: except Exception:
root.wm_state('normal') root.wm_state('normal')
root.title(progname) root.title(progname)
text = traceback.format_exc() 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() root.mainloop()
if not success: if not success:
return 1 return 1

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

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# crypto library mainly by some_updates # crypto library mainly by some_updates
@@ -8,7 +8,6 @@
# pbkdf2.py Copyright © 2009 Daniel Holth <dholth@fastmail.fm> # pbkdf2.py Copyright © 2009 Daniel Holth <dholth@fastmail.fm>
# pbkdf2.py This code may be freely used and modified for any purpose. # pbkdf2.py This code may be freely used and modified for any purpose.
from __future__ import print_function
import sys, os import sys, os
import hmac import hmac
from struct import pack from struct import pack
@@ -159,7 +158,7 @@ def _load_libalfcrypto():
topazCryptoDecrypt(ctx, data, out, len(data)) topazCryptoDecrypt(ctx, data, out, len(data))
return out.raw return out.raw
print(u"Using Library AlfCrypto DLL/DYLIB/SO") print("Using Library AlfCrypto DLL/DYLIB/SO")
return (AES_CBC, Pukall_Cipher, Topaz_Cipher) return (AES_CBC, Pukall_Cipher, Topaz_Cipher)
@@ -178,13 +177,13 @@ def _load_python_alfcrypto():
if len(key)!=16: if len(key)!=16:
raise Exception('Pukall_Cipher: Bad key length.') raise Exception('Pukall_Cipher: Bad key length.')
wkey = [] wkey = []
for i in xrange(8): for i in range(8):
wkey.append(ord(key[i*2])<<8 | ord(key[i*2+1])) wkey.append(ord(key[i*2])<<8 | ord(key[i*2+1]))
dst = "" dst = ""
for i in xrange(len(src)): for i in range(len(src)):
temp1 = 0; temp1 = 0;
byteXorVal = 0; byteXorVal = 0;
for j in xrange(8): for j in range(8):
temp1 ^= wkey[j] temp1 ^= wkey[j]
sum2 = (sum2+j)*20021 + sum1 sum2 = (sum2+j)*20021 + sum1
sum1 = (temp1*346)&0xFFFF sum1 = (temp1*346)&0xFFFF
@@ -197,7 +196,7 @@ def _load_python_alfcrypto():
curByte = ((curByte ^ (byteXorVal >> 8)) ^ byteXorVal) & 0xFF curByte = ((curByte ^ (byteXorVal >> 8)) ^ byteXorVal) & 0xFF
if decryption: if decryption:
keyXorVal = curByte * 257; keyXorVal = curByte * 257;
for j in xrange(8): for j in range(8):
wkey[j] ^= keyXorVal; wkey[j] ^= keyXorVal;
dst+=chr(curByte) dst+=chr(curByte)
return dst return dst
@@ -245,7 +244,7 @@ def _load_python_alfcrypto():
cleartext = self.aes.decrypt(iv + data) cleartext = self.aes.decrypt(iv + data)
return cleartext return cleartext
print(u"Using Library AlfCrypto Python") print("Using Library AlfCrypto Python")
return (AES_CBC, Pukall_Cipher, Topaz_Cipher) return (AES_CBC, Pukall_Cipher, Topaz_Cipher)
@@ -269,10 +268,10 @@ class KeyIVGen(object):
# [c_char_p, c_ulong, c_char_p, c_ulong, c_ulong, c_ulong, c_char_p]) # [c_char_p, c_ulong, c_char_p, c_ulong, c_ulong, c_ulong, c_char_p])
def pbkdf2(self, passwd, salt, iter, keylen): def pbkdf2(self, passwd, salt, iter, keylen):
def xorstr( a, b ): def xorbytes( a, b ):
if len(a) != len(b): if len(a) != len(b):
raise Exception("xorstr(): lengths differ") raise Exception("xorbytes(): lengths differ")
return ''.join((chr(ord(x)^ord(y)) for x, y in zip(a, b))) return bytes([x ^ y for x, y in zip(a, b)])
def prf( h, data ): def prf( h, data ):
hm = h.copy() hm = h.copy()
@@ -284,17 +283,17 @@ class KeyIVGen(object):
T = U T = U
for i in range(2, itercount+1): for i in range(2, itercount+1):
U = prf( h, U ) U = prf( h, U )
T = xorstr( T, U ) T = xorbytes( T, U )
return T return T
sha = hashlib.sha1 sha = hashlib.sha1
digest_size = sha().digest_size digest_size = sha().digest_size
# l - number of output blocks to produce # l - number of output blocks to produce
l = keylen / digest_size l = keylen // digest_size
if keylen % digest_size != 0: if keylen % digest_size != 0:
l += 1 l += 1
h = hmac.new( passwd, None, sha ) h = hmac.new( passwd, None, sha )
T = "" T = b""
for i in range(1, l+1): for i in range(1, l+1):
T += pbkdf2_F( h, salt, iter, i ) T += pbkdf2_F( h, salt, iter, i )
return T[0: keylen] return T[0: keylen]

185
DeDRM_plugin/androidkindlekey.py Normal file → Executable file
View File

@@ -1,12 +1,8 @@
#!/usr/bin/env python #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import with_statement
# androidkindlekey.py # androidkindlekey.py
# Copyright © 2013-15 by Thom and Apprentice Harper # Copyright © 2010-20 by Thom, Apprentice Harper et al.
# Some portions Copyright © 2010-15 by some_updates and Apprentice Alf
#
# Revision history: # Revision history:
# 1.0 - AmazonSecureStorage.xml decryption to serial number # 1.0 - AmazonSecureStorage.xml decryption to serial number
@@ -17,14 +13,14 @@ from __future__ import with_statement
# 1.3 - added in TkInter interface, output to a file # 1.3 - added in TkInter interface, output to a file
# 1.4 - Fix some problems identified by Aldo Bleeker # 1.4 - Fix some problems identified by Aldo Bleeker
# 1.5 - Fix another problem identified by Aldo Bleeker # 1.5 - Fix another problem identified by Aldo Bleeker
# 2.0 - Python 3 compatibility
""" """
Retrieve Kindle for Android Serial Number. Retrieve Kindle for Android Serial Number.
""" """
from __future__ import print_function
__license__ = 'GPL v3' __license__ = 'GPL v3'
__version__ = '1.5' __version__ = '2.0'
import os import os
import sys import sys
@@ -34,7 +30,7 @@ import tempfile
import zlib import zlib
import tarfile import tarfile
from hashlib import md5 from hashlib import md5
from cStringIO import StringIO from io import BytesIO
from binascii import a2b_hex, b2a_hex from binascii import a2b_hex, b2a_hex
# Routines common to Mac and PC # Routines common to Mac and PC
@@ -49,10 +45,17 @@ class SafeUnbuffered:
if self.encoding == None: if self.encoding == None:
self.encoding = "utf-8" self.encoding = "utf-8"
def write(self, data): def write(self, data):
if isinstance(data,unicode): if isinstance(data,str) or isinstance(data,unicode):
# str for Python3, unicode for Python2
data = data.encode(self.encoding,"replace") data = data.encode(self.encoding,"replace")
self.stream.write(data) try:
self.stream.flush() buffer = getattr(self.stream, 'buffer', self.stream)
# self.stream.buffer for Python3, self.stream for Python2
buffer.write(data)
buffer.flush()
except:
# We can do nothing if a write fails
raise
def __getattr__(self, attr): def __getattr__(self, attr):
return getattr(self.stream, attr) return getattr(self.stream, attr)
@@ -90,22 +93,20 @@ def unicode_argv():
# Remove Python executable and commands if present # Remove Python executable and commands if present
start = argc.value - len(sys.argv) start = argc.value - len(sys.argv)
return [argv[i] for i in return [argv[i] for i in
xrange(start, argc.value)] range(start, argc.value)]
# if we don't have any arguments at all, just pass back script name # if we don't have any arguments at all, just pass back script name
# this should never happen # this should never happen
return [u"kindlekey.py"] return ["kindlekey.py"]
else: else:
argvencoding = sys.stdin.encoding argvencoding = sys.stdin.encoding or "utf-8"
if argvencoding == None: return [arg if (isinstance(arg, str) or isinstance(arg,unicode)) else str(arg, argvencoding) for arg in sys.argv]
argvencoding = "utf-8"
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
class DrmException(Exception): class DrmException(Exception):
pass pass
STORAGE = u"backup.ab" STORAGE = "backup.ab"
STORAGE1 = u"AmazonSecureStorage.xml" STORAGE1 = "AmazonSecureStorage.xml"
STORAGE2 = u"map_data_storage.db" STORAGE2 = "map_data_storage.db"
class AndroidObfuscation(object): class AndroidObfuscation(object):
'''AndroidObfuscation '''AndroidObfuscation
@@ -118,7 +119,7 @@ class AndroidObfuscation(object):
cipher = self._get_cipher() cipher = self._get_cipher()
padding = len(self.key) - len(plaintext) % len(self.key) padding = len(self.key) - len(plaintext) % len(self.key)
plaintext += chr(padding) * padding plaintext += chr(padding) * padding
return b2a_hex(cipher.encrypt(plaintext)) return b2a_hex(cipher.encrypt(plaintext.encode('utf-8')))
def decrypt(self, ciphertext): def decrypt(self, ciphertext):
cipher = self._get_cipher() cipher = self._get_cipher()
@@ -138,7 +139,7 @@ class AndroidObfuscationV2(AndroidObfuscation):
''' '''
count = 503 count = 503
password = 'Thomsun was here!' password = b'Thomsun was here!'
def __init__(self, salt): def __init__(self, salt):
key = self.password + salt key = self.password + salt
@@ -200,6 +201,7 @@ def get_serials1(path=STORAGE1):
try: try:
tokens = set(get_value('kindle.account.tokens').split(',')) tokens = set(get_value('kindle.account.tokens').split(','))
except: except:
sys.stderr.write('cannot get kindle account tokens\n')
return [] return []
serials = [] serials = []
@@ -219,15 +221,14 @@ def get_serials2(path=STORAGE2):
import sqlite3 import sqlite3
connection = sqlite3.connect(path) connection = sqlite3.connect(path)
cursor = connection.cursor() cursor = connection.cursor()
cursor.execute('''select userdata_value from userdata where userdata_key like '%/%token.device.deviceserialname%' ''') cursor.execute('''select device_data_value from device_data where device_data_key like '%serial.number%' ''')
userdata_keys = cursor.fetchall() device_data_keys = cursor.fetchall()
dsns = [] dsns = []
for userdata_row in userdata_keys: for device_data_row in device_data_keys:
try: try:
if userdata_row and userdata_row[0]: if device_data_row and device_data_row[0]:
userdata_utf8 = userdata_row[0].encode('utf8') if len(device_data_row[0]) > 0:
if len(userdata_utf8) > 0: dsns.append(device_data_row[0])
dsns.append(userdata_utf8)
except: except:
print("Error getting one of the device serial name keys") print("Error getting one of the device serial name keys")
traceback.print_exc() traceback.print_exc()
@@ -240,9 +241,12 @@ def get_serials2(path=STORAGE2):
for userdata_row in userdata_keys: for userdata_row in userdata_keys:
try: try:
if userdata_row and userdata_row[0]: if userdata_row and userdata_row[0]:
userdata_utf8 = userdata_row[0].encode('utf8') if len(userdata_row[0]) > 0:
if len(userdata_utf8) > 0: if ',' in userdata_row[0]:
tokens.append(userdata_utf8) splits = userdata_row[0].split(',')
for split in splits:
tokens.append(split)
tokens.append(userdata_row[0])
except: except:
print("Error getting one of the account token keys") print("Error getting one of the account token keys")
traceback.print_exc() traceback.print_exc()
@@ -253,9 +257,8 @@ def get_serials2(path=STORAGE2):
for x in dsns: for x in dsns:
serials.append(x) serials.append(x)
for y in tokens: for y in tokens:
serials.append('%s%s' % (x, y)) serials.append(y)
for y in tokens: serials.append(x+y)
serials.append(y)
return serials return serials
def get_serials(path=STORAGE): def get_serials(path=STORAGE):
@@ -277,8 +280,8 @@ def get_serials(path=STORAGE):
try : try :
read = open(path, 'rb') read = open(path, 'rb')
head = read.read(24) head = read.read(24)
if head[:14] == 'ANDROID BACKUP': if head[:14] == b'ANDROID BACKUP':
output = StringIO(zlib.decompress(read.read())) output = BytesIO(zlib.decompress(read.read()))
except Exception: except Exception:
pass pass
finally: finally:
@@ -313,7 +316,7 @@ __all__ = [ 'get_serials', 'getkey']
def getkey(outfile, inpath): def getkey(outfile, inpath):
keys = get_serials(inpath) keys = get_serials(inpath)
if len(keys) > 0: if len(keys) > 0:
with file(outfile, 'w') as keyfileout: with open(outfile, 'w') as keyfileout:
for key in keys: for key in keys:
keyfileout.write(key) keyfileout.write(key)
keyfileout.write("\n") keyfileout.write("\n")
@@ -322,13 +325,13 @@ def getkey(outfile, inpath):
def usage(progname): def usage(progname):
print(u"Decrypts the serial number(s) of Kindle For Android from Android backup or file") print("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("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("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("Or map_data_storage.db from /data/data/com.amazon.kindle/databases/map_data_storage.db")
print(u"") print("")
print(u"Usage:") print("Usage:")
print(u" {0:s} [-h] [-b <backup.ab>] [<outfile.k4a>]".format(progname)) print(" {0:s} [-h] [-b <backup.ab>] [<outfile.k4a>]".format(progname))
def cli_main(): def cli_main():
@@ -336,13 +339,13 @@ def cli_main():
sys.stderr=SafeUnbuffered(sys.stderr) sys.stderr=SafeUnbuffered(sys.stderr)
argv=unicode_argv() argv=unicode_argv()
progname = os.path.basename(argv[0]) 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: try:
opts, args = getopt.getopt(argv[1:], "hb:") opts, args = getopt.getopt(argv[1:], "hb:")
except getopt.GetoptError, err: except getopt.GetoptError as err:
usage(progname) 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 return 2
inpath = "" inpath = ""
@@ -374,92 +377,92 @@ def cli_main():
if not os.path.isfile(inpath): if not os.path.isfile(inpath):
usage(progname) usage(progname)
print(u"\n{0:s} file not found".format(inpath)) print("\n{0:s} file not found".format(inpath))
return 2 return 2
if getkey(outfile, inpath): 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: else:
print(u"\nCould not retrieve Kindle for Android key.") print("\nCould not retrieve Kindle for Android key.")
return 0 return 0
def gui_main(): def gui_main():
try: try:
import Tkinter import tkinter
import Tkconstants import tkinter.constants
import tkMessageBox import tkinter.messagebox
import tkFileDialog import tkinter.filedialog
except: except:
print("Tkinter not installed") print("tkinter not installed")
return cli_main() return 0
class DecryptionDialog(Tkinter.Frame): class DecryptionDialog(tkinter.Frame):
def __init__(self, root): def __init__(self, root):
Tkinter.Frame.__init__(self, root, border=5) tkinter.Frame.__init__(self, root, border=5)
self.status = Tkinter.Label(self, text=u"Select backup.ab file") self.status = tkinter.Label(self, text="Select backup.ab file")
self.status.pack(fill=Tkconstants.X, expand=1) self.status.pack(fill=tkinter.constants.X, expand=1)
body = Tkinter.Frame(self) body = tkinter.Frame(self)
body.pack(fill=Tkconstants.X, expand=1) body.pack(fill=tkinter.constants.X, expand=1)
sticky = Tkconstants.E + Tkconstants.W sticky = tkinter.constants.E + tkinter.constants.W
body.grid_columnconfigure(1, weight=2) body.grid_columnconfigure(1, weight=2)
Tkinter.Label(body, text=u"Backup file").grid(row=0, column=0) tkinter.Label(body, text="Backup file").grid(row=0, column=0)
self.keypath = Tkinter.Entry(body, width=40) self.keypath = tkinter.Entry(body, width=40)
self.keypath.grid(row=0, column=1, sticky=sticky) self.keypath.grid(row=0, column=1, sticky=sticky)
self.keypath.insert(2, u"backup.ab") self.keypath.insert(2, "backup.ab")
button = Tkinter.Button(body, text=u"...", command=self.get_keypath) button = tkinter.Button(body, text="...", command=self.get_keypath)
button.grid(row=0, column=2) button.grid(row=0, column=2)
buttons = Tkinter.Frame(self) buttons = tkinter.Frame(self)
buttons.pack() buttons.pack()
button2 = Tkinter.Button( button2 = tkinter.Button(
buttons, text=u"Extract", width=10, command=self.generate) buttons, text="Extract", width=10, command=self.generate)
button2.pack(side=Tkconstants.LEFT) button2.pack(side=tkinter.constants.LEFT)
Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT) tkinter.Frame(buttons, width=10).pack(side=tkinter.constants.LEFT)
button3 = Tkinter.Button( button3 = tkinter.Button(
buttons, text=u"Quit", width=10, command=self.quit) buttons, text="Quit", width=10, command=self.quit)
button3.pack(side=Tkconstants.RIGHT) button3.pack(side=tkinter.constants.RIGHT)
def get_keypath(self): def get_keypath(self):
keypath = tkFileDialog.askopenfilename( keypath = tkinter.filedialog.askopenfilename(
parent=None, title=u"Select backup.ab file", parent=None, title="Select backup.ab file",
defaultextension=u".ab", defaultextension=".ab",
filetypes=[('adb backup com.amazon.kindle', '.ab'), filetypes=[('adb backup com.amazon.kindle', '.ab'),
('All Files', '.*')]) ('All Files', '.*')])
if keypath: if keypath:
keypath = os.path.normpath(keypath) keypath = os.path.normpath(keypath)
self.keypath.delete(0, Tkconstants.END) self.keypath.delete(0, tkinter.constants.END)
self.keypath.insert(0, keypath) self.keypath.insert(0, keypath)
return return
def generate(self): def generate(self):
inpath = self.keypath.get() inpath = self.keypath.get()
self.status['text'] = u"Getting key..." self.status['text'] = "Getting key..."
try: try:
keys = get_serials(inpath) keys = get_serials(inpath)
keycount = 0 keycount = 0
for key in keys: for key in keys:
while True: while True:
keycount += 1 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): if not os.path.exists(outfile):
break break
with file(outfile, 'w') as keyfileout: with open(outfile, 'w') as keyfileout:
keyfileout.write(key) keyfileout.write(key)
success = True success = True
tkMessageBox.showinfo(progname, u"Key successfully retrieved to {0}".format(outfile)) tkinter.messagebox.showinfo(progname, "Key successfully retrieved to {0}".format(outfile))
except Exception, e: except Exception as e:
self.status['text'] = u"Error: {0}".format(e.args[0]) self.status['text'] = "Error: {0}".format(e.args[0])
return return
self.status['text'] = u"Select backup.ab file" self.status['text'] = "Select backup.ab file"
argv=unicode_argv() argv=unicode_argv()
progpath, progname = os.path.split(argv[0]) progpath, progname = os.path.split(argv[0])
root = Tkinter.Tk() root = tkinter.Tk()
root.title(u"Kindle for Android Key Extraction v.{0}".format(__version__)) root.title("Kindle for Android Key Extraction v.{0}".format(__version__))
root.resizable(True, False) root.resizable(True, False)
root.minsize(300, 0) root.minsize(300, 0)
DecryptionDialog(root).pack(fill=Tkconstants.X, expand=1) DecryptionDialog(root).pack(fill=tkinter.constants.X, expand=1)
root.mainloop() root.mainloop()
return 0 return 0

View File

@@ -1,9 +1,10 @@
#!/usr/bin/env python #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import sys, os import sys, os
import locale import locale
import codecs import codecs
import importlib
# get sys.argv arguments and encode them into utf-8 # get sys.argv arguments and encode them into utf-8
def unicode_argv(): def unicode_argv():
@@ -34,15 +35,13 @@ def unicode_argv():
# Remove Python executable and commands if present # Remove Python executable and commands if present
start = argc.value - len(sys.argv) start = argc.value - len(sys.argv)
return [argv[i] for i in return [argv[i] for i in
xrange(start, argc.value)] range(start, argc.value)]
# if we don't have any arguments at all, just pass back script name # if we don't have any arguments at all, just pass back script name
# this should never happen # this should never happen
return [u"DeDRM.py"] return ["DeDRM.py"]
else: else:
argvencoding = sys.stdin.encoding argvencoding = sys.stdin.encoding or "utf-8"
if argvencoding == None: return [arg if (isinstance(arg, str) or isinstance(arg,unicode)) else str(arg, argvencoding) for arg in sys.argv]
argvencoding = "utf-8"
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
def add_cp65001_codec(): def add_cp65001_codec():
@@ -59,7 +58,7 @@ def set_utf8_default_encoding():
return return
# Regenerate setdefaultencoding. # Regenerate setdefaultencoding.
reload(sys) importlib.reload(sys)
sys.setdefaultencoding('utf-8') sys.setdefaultencoding('utf-8')
for attr in dir(locale): for attr in dir(locale):

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab # vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
@@ -29,6 +29,8 @@
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE. # DEALINGS IN THE SOFTWARE.
# Adjusted for Python 3, September 2020
""" """
AskFolder(...) -- Ask the user to select a folder Windows specific AskFolder(...) -- Ask the user to select a folder Windows specific
""" """
@@ -164,15 +166,15 @@ def AskFolder(
def BrowseCallback(hwnd, uMsg, lParam, lpData): def BrowseCallback(hwnd, uMsg, lParam, lpData):
if uMsg == BFFM_INITIALIZED: if uMsg == BFFM_INITIALIZED:
if actionButtonLabel: if actionButtonLabel:
label = unicode(actionButtonLabel, errors='replace') label = str(actionButtonLabel, errors='replace')
user32.SendMessageW(hwnd, BFFM_SETOKTEXT, 0, label) user32.SendMessageW(hwnd, BFFM_SETOKTEXT, 0, label)
if cancelButtonLabel: if cancelButtonLabel:
label = unicode(cancelButtonLabel, errors='replace') label = str(cancelButtonLabel, errors='replace')
cancelButton = user32.GetDlgItem(hwnd, IDCANCEL) cancelButton = user32.GetDlgItem(hwnd, IDCANCEL)
if cancelButton: if cancelButton:
user32.SetWindowTextW(cancelButton, label) user32.SetWindowTextW(cancelButton, label)
if windowTitle: if windowTitle:
title = unicode(windowTitle, erros='replace') title = str(windowTitle, errors='replace')
user32.SetWindowTextW(hwnd, title) user32.SetWindowTextW(hwnd, title)
if defaultLocation: if defaultLocation:
user32.SendMessageW(hwnd, BFFM_SETSELECTIONW, 1, defaultLocation.replace('/', '\\')) user32.SendMessageW(hwnd, BFFM_SETSELECTIONW, 1, defaultLocation.replace('/', '\\'))
@@ -200,7 +202,7 @@ def AskFolder(
if not pidl: if not pidl:
result = None result = None
else: else:
path = LPCWSTR(u" " * (MAX_PATH+1)) path = LPCWSTR(" " * (MAX_PATH+1))
shell32.SHGetPathFromIDListW(pidl, path) shell32.SHGetPathFromIDListW(pidl, path)
ole32.CoTaskMemFree(pidl) ole32.CoTaskMemFree(pidl)
result = path.value result = path.value

589
DeDRM_plugin/config.py Normal file → Executable file

File diff suppressed because it is too large Load Diff

View File

@@ -1,20 +1,35 @@
#! /usr/bin/python #!/usr/bin/env python3
# -*- coding: utf-8 -*-
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab # vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
# For use with Topaz Scripts Version 2.6
from __future__ import print_function # For use with Topaz Scripts Version 2.6
class Unbuffered: # Python 3, September 2020
# 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): def __init__(self, stream):
self.stream = stream self.stream = stream
self.encoding = stream.encoding
if self.encoding == None:
self.encoding = "utf-8"
def write(self, data): def write(self, data):
self.stream.write(data) if isinstance(data,str) or isinstance(data,unicode):
self.stream.flush() # 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): def __getattr__(self, attr):
return getattr(self.stream, attr) return getattr(self.stream, attr)
import sys import sys
sys.stdout=Unbuffered(sys.stdout)
import csv import csv
import os import os
import getopt import getopt
@@ -47,7 +62,7 @@ def readEncodedNumber(file):
c = file.read(1) c = file.read(1)
if (len(c) == 0): if (len(c) == 0):
return None return None
data = ord(c) data = c[0]
datax = (datax <<7) + (data & 0x7F) datax = (datax <<7) + (data & 0x7F)
data = datax data = datax
@@ -107,7 +122,7 @@ def readString(file):
def convert(i): def convert(i):
result = '' result = ''
val = encodeNumber(i) val = encodeNumber(i)
for j in xrange(len(val)): for j in range(len(val)):
c = ord(val[j:j+1]) c = ord(val[j:j+1])
result += '%02x' % c result += '%02x' % c
return result return result
@@ -121,10 +136,10 @@ class Dictionary(object):
def __init__(self, dictFile): def __init__(self, dictFile):
self.filename = dictFile self.filename = dictFile
self.size = 0 self.size = 0
self.fo = file(dictFile,'rb') self.fo = open(dictFile,'rb')
self.stable = [] self.stable = []
self.size = readEncodedNumber(self.fo) 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.stable.append(self.escapestr(readString(self.fo)))
self.pos = 0 self.pos = 0
@@ -151,7 +166,7 @@ class Dictionary(object):
return self.pos return self.pos
def dumpDict(self): def dumpDict(self):
for i in xrange(self.size): for i in range(self.size):
print("%d %s %s" % (i, convert(i), self.stable[i])) print("%d %s %s" % (i, convert(i), self.stable[i]))
return return
@@ -161,7 +176,7 @@ class Dictionary(object):
class PageParser(object): class PageParser(object):
def __init__(self, filename, dict, debug, flat_xml): 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.id = os.path.basename(filename).replace('.dat','')
self.dict = dict self.dict = dict
self.debug = debug self.debug = debug
@@ -179,232 +194,232 @@ class PageParser(object):
# tag : (number of arguments, argument type, subtags present, special case of subtags presents when escaped) # tag : (number of arguments, argument type, subtags present, special case of subtags presents when escaped)
token_tags = { token_tags = {
'x' : (1, 'scalar_number', 0, 0), b'x' : (1, 'scalar_number', 0, 0),
'y' : (1, 'scalar_number', 0, 0), b'y' : (1, 'scalar_number', 0, 0),
'h' : (1, 'scalar_number', 0, 0), b'h' : (1, 'scalar_number', 0, 0),
'w' : (1, 'scalar_number', 0, 0), b'w' : (1, 'scalar_number', 0, 0),
'firstWord' : (1, 'scalar_number', 0, 0), b'firstWord' : (1, 'scalar_number', 0, 0),
'lastWord' : (1, 'scalar_number', 0, 0), b'lastWord' : (1, 'scalar_number', 0, 0),
'rootID' : (1, 'scalar_number', 0, 0), b'rootID' : (1, 'scalar_number', 0, 0),
'stemID' : (1, 'scalar_number', 0, 0), b'stemID' : (1, 'scalar_number', 0, 0),
'type' : (1, 'scalar_text', 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), b'info.word' : (0, 'number', 1, 1),
'info.word.ocrText' : (1, 'text', 0, 0), b'info.word.ocrText' : (1, 'text', 0, 0),
'info.word.firstGlyph' : (1, 'raw', 0, 0), b'info.word.firstGlyph' : (1, 'raw', 0, 0),
'info.word.lastGlyph' : (1, 'raw', 0, 0), b'info.word.lastGlyph' : (1, 'raw', 0, 0),
'info.word.bl' : (1, 'raw', 0, 0), b'info.word.bl' : (1, 'raw', 0, 0),
'info.word.link_id' : (1, 'number', 0, 0), b'info.word.link_id' : (1, 'number', 0, 0),
'glyph' : (0, 'number', 1, 1), b'glyph' : (0, 'number', 1, 1),
'glyph.x' : (1, 'number', 0, 0), b'glyph.x' : (1, 'number', 0, 0),
'glyph.y' : (1, 'number', 0, 0), b'glyph.y' : (1, 'number', 0, 0),
'glyph.glyphID' : (1, 'number', 0, 0), b'glyph.glyphID' : (1, 'number', 0, 0),
'dehyphen' : (0, 'number', 1, 1), b'dehyphen' : (0, 'number', 1, 1),
'dehyphen.rootID' : (1, 'number', 0, 0), b'dehyphen.rootID' : (1, 'number', 0, 0),
'dehyphen.stemID' : (1, 'number', 0, 0), b'dehyphen.stemID' : (1, 'number', 0, 0),
'dehyphen.stemPage' : (1, 'number', 0, 0), b'dehyphen.stemPage' : (1, 'number', 0, 0),
'dehyphen.sh' : (1, 'number', 0, 0), b'dehyphen.sh' : (1, 'number', 0, 0),
'links' : (0, 'number', 1, 1), b'links' : (0, 'number', 1, 1),
'links.page' : (1, 'number', 0, 0), b'links.page' : (1, 'number', 0, 0),
'links.rel' : (1, 'number', 0, 0), b'links.rel' : (1, 'number', 0, 0),
'links.row' : (1, 'number', 0, 0), b'links.row' : (1, 'number', 0, 0),
'links.title' : (1, 'text', 0, 0), b'links.title' : (1, 'text', 0, 0),
'links.href' : (1, 'text', 0, 0), b'links.href' : (1, 'text', 0, 0),
'links.type' : (1, 'text', 0, 0), b'links.type' : (1, 'text', 0, 0),
'links.id' : (1, 'number', 0, 0), b'links.id' : (1, 'number', 0, 0),
'paraCont' : (0, 'number', 1, 1), b'paraCont' : (0, 'number', 1, 1),
'paraCont.rootID' : (1, 'number', 0, 0), b'paraCont.rootID' : (1, 'number', 0, 0),
'paraCont.stemID' : (1, 'number', 0, 0), b'paraCont.stemID' : (1, 'number', 0, 0),
'paraCont.stemPage' : (1, 'number', 0, 0), b'paraCont.stemPage' : (1, 'number', 0, 0),
'paraStems' : (0, 'number', 1, 1), b'paraStems' : (0, 'number', 1, 1),
'paraStems.stemID' : (1, 'number', 0, 0), b'paraStems.stemID' : (1, 'number', 0, 0),
'wordStems' : (0, 'number', 1, 1), b'wordStems' : (0, 'number', 1, 1),
'wordStems.stemID' : (1, 'number', 0, 0), b'wordStems.stemID' : (1, 'number', 0, 0),
'empty' : (1, 'snippets', 1, 0), b'empty' : (1, 'snippets', 1, 0),
'page' : (1, 'snippets', 1, 0), b'page' : (1, 'snippets', 1, 0),
'page.class' : (1, 'scalar_text', 0, 0), b'page.class' : (1, 'scalar_text', 0, 0),
'page.pageid' : (1, 'scalar_text', 0, 0), b'page.pageid' : (1, 'scalar_text', 0, 0),
'page.pagelabel' : (1, 'scalar_text', 0, 0), b'page.pagelabel' : (1, 'scalar_text', 0, 0),
'page.type' : (1, 'scalar_text', 0, 0), b'page.type' : (1, 'scalar_text', 0, 0),
'page.h' : (1, 'scalar_number', 0, 0), b'page.h' : (1, 'scalar_number', 0, 0),
'page.w' : (1, 'scalar_number', 0, 0), b'page.w' : (1, 'scalar_number', 0, 0),
'page.startID' : (1, 'scalar_number', 0, 0), b'page.startID' : (1, 'scalar_number', 0, 0),
'group' : (1, 'snippets', 1, 0), b'group' : (1, 'snippets', 1, 0),
'group.class' : (1, 'scalar_text', 0, 0), b'group.class' : (1, 'scalar_text', 0, 0),
'group.type' : (1, 'scalar_text', 0, 0), b'group.type' : (1, 'scalar_text', 0, 0),
'group._tag' : (1, 'scalar_text', 0, 0), b'group._tag' : (1, 'scalar_text', 0, 0),
'group.orientation': (1, 'scalar_text', 0, 0), b'group.orientation': (1, 'scalar_text', 0, 0),
'region' : (1, 'snippets', 1, 0), b'region' : (1, 'snippets', 1, 0),
'region.class' : (1, 'scalar_text', 0, 0), b'region.class' : (1, 'scalar_text', 0, 0),
'region.type' : (1, 'scalar_text', 0, 0), b'region.type' : (1, 'scalar_text', 0, 0),
'region.x' : (1, 'scalar_number', 0, 0), b'region.x' : (1, 'scalar_number', 0, 0),
'region.y' : (1, 'scalar_number', 0, 0), b'region.y' : (1, 'scalar_number', 0, 0),
'region.h' : (1, 'scalar_number', 0, 0), b'region.h' : (1, 'scalar_number', 0, 0),
'region.w' : (1, 'scalar_number', 0, 0), b'region.w' : (1, 'scalar_number', 0, 0),
'region.orientation' : (1, 'scalar_text', 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), b'img' : (1, 'snippets', 1, 0),
'img.x' : (1, 'scalar_number', 0, 0), b'img.x' : (1, 'scalar_number', 0, 0),
'img.y' : (1, 'scalar_number', 0, 0), b'img.y' : (1, 'scalar_number', 0, 0),
'img.h' : (1, 'scalar_number', 0, 0), b'img.h' : (1, 'scalar_number', 0, 0),
'img.w' : (1, 'scalar_number', 0, 0), b'img.w' : (1, 'scalar_number', 0, 0),
'img.src' : (1, 'scalar_number', 0, 0), b'img.src' : (1, 'scalar_number', 0, 0),
'img.color_src' : (1, 'scalar_number', 0, 0), b'img.color_src' : (1, 'scalar_number', 0, 0),
'img.gridSize' : (1, 'scalar_number', 0, 0), b'img.gridSize' : (1, 'scalar_number', 0, 0),
'img.gridBottomCenter' : (1, 'scalar_number', 0, 0), b'img.gridBottomCenter' : (1, 'scalar_number', 0, 0),
'img.gridTopCenter' : (1, 'scalar_number', 0, 0), b'img.gridTopCenter' : (1, 'scalar_number', 0, 0),
'img.gridBeginCenter' : (1, 'scalar_number', 0, 0), b'img.gridBeginCenter' : (1, 'scalar_number', 0, 0),
'img.gridEndCenter' : (1, 'scalar_number', 0, 0), b'img.gridEndCenter' : (1, 'scalar_number', 0, 0),
'img.image_type' : (1, 'scalar_number', 0, 0), b'img.image_type' : (1, 'scalar_number', 0, 0),
'paragraph' : (1, 'snippets', 1, 0), b'paragraph' : (1, 'snippets', 1, 0),
'paragraph.class' : (1, 'scalar_text', 0, 0), b'paragraph.class' : (1, 'scalar_text', 0, 0),
'paragraph.firstWord' : (1, 'scalar_number', 0, 0), b'paragraph.firstWord' : (1, 'scalar_number', 0, 0),
'paragraph.lastWord' : (1, 'scalar_number', 0, 0), b'paragraph.lastWord' : (1, 'scalar_number', 0, 0),
'paragraph.lastWord' : (1, 'scalar_number', 0, 0), b'paragraph.lastWord' : (1, 'scalar_number', 0, 0),
'paragraph.gridSize' : (1, 'scalar_number', 0, 0), b'paragraph.gridSize' : (1, 'scalar_number', 0, 0),
'paragraph.gridBottomCenter' : (1, 'scalar_number', 0, 0), b'paragraph.gridBottomCenter' : (1, 'scalar_number', 0, 0),
'paragraph.gridTopCenter' : (1, 'scalar_number', 0, 0), b'paragraph.gridTopCenter' : (1, 'scalar_number', 0, 0),
'paragraph.gridBeginCenter' : (1, 'scalar_number', 0, 0), b'paragraph.gridBeginCenter' : (1, 'scalar_number', 0, 0),
'paragraph.gridEndCenter' : (1, 'scalar_number', 0, 0), b'paragraph.gridEndCenter' : (1, 'scalar_number', 0, 0),
'word_semantic' : (1, 'snippets', 1, 1), b'word_semantic' : (1, 'snippets', 1, 1),
'word_semantic.type' : (1, 'scalar_text', 0, 0), b'word_semantic.type' : (1, 'scalar_text', 0, 0),
'word_semantic.class' : (1, 'scalar_text', 0, 0), b'word_semantic.class' : (1, 'scalar_text', 0, 0),
'word_semantic.firstWord' : (1, 'scalar_number', 0, 0), b'word_semantic.firstWord' : (1, 'scalar_number', 0, 0),
'word_semantic.lastWord' : (1, 'scalar_number', 0, 0), b'word_semantic.lastWord' : (1, 'scalar_number', 0, 0),
'word_semantic.gridBottomCenter' : (1, 'scalar_number', 0, 0), b'word_semantic.gridBottomCenter' : (1, 'scalar_number', 0, 0),
'word_semantic.gridTopCenter' : (1, 'scalar_number', 0, 0), b'word_semantic.gridTopCenter' : (1, 'scalar_number', 0, 0),
'word_semantic.gridBeginCenter' : (1, 'scalar_number', 0, 0), b'word_semantic.gridBeginCenter' : (1, 'scalar_number', 0, 0),
'word_semantic.gridEndCenter' : (1, 'scalar_number', 0, 0), b'word_semantic.gridEndCenter' : (1, 'scalar_number', 0, 0),
'word' : (1, 'snippets', 1, 0), b'word' : (1, 'snippets', 1, 0),
'word.type' : (1, 'scalar_text', 0, 0), b'word.type' : (1, 'scalar_text', 0, 0),
'word.class' : (1, 'scalar_text', 0, 0), b'word.class' : (1, 'scalar_text', 0, 0),
'word.firstGlyph' : (1, 'scalar_number', 0, 0), b'word.firstGlyph' : (1, 'scalar_number', 0, 0),
'word.lastGlyph' : (1, 'scalar_number', 0, 0), b'word.lastGlyph' : (1, 'scalar_number', 0, 0),
'_span' : (1, 'snippets', 1, 0), b'_span' : (1, 'snippets', 1, 0),
'_span.class' : (1, 'scalar_text', 0, 0), b'_span.class' : (1, 'scalar_text', 0, 0),
'_span.firstWord' : (1, 'scalar_number', 0, 0), b'_span.firstWord' : (1, 'scalar_number', 0, 0),
'_span.lastWord' : (1, 'scalar_number', 0, 0), b'_span.lastWord' : (1, 'scalar_number', 0, 0),
'_span.gridSize' : (1, 'scalar_number', 0, 0), b'_span.gridSize' : (1, 'scalar_number', 0, 0),
'_span.gridBottomCenter' : (1, 'scalar_number', 0, 0), b'_span.gridBottomCenter' : (1, 'scalar_number', 0, 0),
'_span.gridTopCenter' : (1, 'scalar_number', 0, 0), b'_span.gridTopCenter' : (1, 'scalar_number', 0, 0),
'_span.gridBeginCenter' : (1, 'scalar_number', 0, 0), b'_span.gridBeginCenter' : (1, 'scalar_number', 0, 0),
'_span.gridEndCenter' : (1, 'scalar_number', 0, 0), b'_span.gridEndCenter' : (1, 'scalar_number', 0, 0),
'span' : (1, 'snippets', 1, 0), b'span' : (1, 'snippets', 1, 0),
'span.firstWord' : (1, 'scalar_number', 0, 0), b'span.firstWord' : (1, 'scalar_number', 0, 0),
'span.lastWord' : (1, 'scalar_number', 0, 0), b'span.lastWord' : (1, 'scalar_number', 0, 0),
'span.gridSize' : (1, 'scalar_number', 0, 0), b'span.gridSize' : (1, 'scalar_number', 0, 0),
'span.gridBottomCenter' : (1, 'scalar_number', 0, 0), b'span.gridBottomCenter' : (1, 'scalar_number', 0, 0),
'span.gridTopCenter' : (1, 'scalar_number', 0, 0), b'span.gridTopCenter' : (1, 'scalar_number', 0, 0),
'span.gridBeginCenter' : (1, 'scalar_number', 0, 0), b'span.gridBeginCenter' : (1, 'scalar_number', 0, 0),
'span.gridEndCenter' : (1, 'scalar_number', 0, 0), b'span.gridEndCenter' : (1, 'scalar_number', 0, 0),
'extratokens' : (1, 'snippets', 1, 0), b'extratokens' : (1, 'snippets', 1, 0),
'extratokens.class' : (1, 'scalar_text', 0, 0), b'extratokens.class' : (1, 'scalar_text', 0, 0),
'extratokens.type' : (1, 'scalar_text', 0, 0), b'extratokens.type' : (1, 'scalar_text', 0, 0),
'extratokens.firstGlyph' : (1, 'scalar_number', 0, 0), b'extratokens.firstGlyph' : (1, 'scalar_number', 0, 0),
'extratokens.lastGlyph' : (1, 'scalar_number', 0, 0), b'extratokens.lastGlyph' : (1, 'scalar_number', 0, 0),
'extratokens.gridSize' : (1, 'scalar_number', 0, 0), b'extratokens.gridSize' : (1, 'scalar_number', 0, 0),
'extratokens.gridBottomCenter' : (1, 'scalar_number', 0, 0), b'extratokens.gridBottomCenter' : (1, 'scalar_number', 0, 0),
'extratokens.gridTopCenter' : (1, 'scalar_number', 0, 0), b'extratokens.gridTopCenter' : (1, 'scalar_number', 0, 0),
'extratokens.gridBeginCenter' : (1, 'scalar_number', 0, 0), b'extratokens.gridBeginCenter' : (1, 'scalar_number', 0, 0),
'extratokens.gridEndCenter' : (1, 'scalar_number', 0, 0), b'extratokens.gridEndCenter' : (1, 'scalar_number', 0, 0),
'glyph.h' : (1, 'number', 0, 0), b'glyph.h' : (1, 'number', 0, 0),
'glyph.w' : (1, 'number', 0, 0), b'glyph.w' : (1, 'number', 0, 0),
'glyph.use' : (1, 'number', 0, 0), b'glyph.use' : (1, 'number', 0, 0),
'glyph.vtx' : (1, 'number', 0, 1), b'glyph.vtx' : (1, 'number', 0, 1),
'glyph.len' : (1, 'number', 0, 1), b'glyph.len' : (1, 'number', 0, 1),
'glyph.dpi' : (1, 'number', 0, 0), b'glyph.dpi' : (1, 'number', 0, 0),
'vtx' : (0, 'number', 1, 1), b'vtx' : (0, 'number', 1, 1),
'vtx.x' : (1, 'number', 0, 0), b'vtx.x' : (1, 'number', 0, 0),
'vtx.y' : (1, 'number', 0, 0), b'vtx.y' : (1, 'number', 0, 0),
'len' : (0, 'number', 1, 1), b'len' : (0, 'number', 1, 1),
'len.n' : (1, 'number', 0, 0), b'len.n' : (1, 'number', 0, 0),
'book' : (1, 'snippets', 1, 0), b'book' : (1, 'snippets', 1, 0),
'version' : (1, 'snippets', 1, 0), b'version' : (1, 'snippets', 1, 0),
'version.FlowEdit_1_id' : (1, 'scalar_text', 0, 0), b'version.FlowEdit_1_id' : (1, 'scalar_text', 0, 0),
'version.FlowEdit_1_version' : (1, 'scalar_text', 0, 0), b'version.FlowEdit_1_version' : (1, 'scalar_text', 0, 0),
'version.Schema_id' : (1, 'scalar_text', 0, 0), b'version.Schema_id' : (1, 'scalar_text', 0, 0),
'version.Schema_version' : (1, 'scalar_text', 0, 0), b'version.Schema_version' : (1, 'scalar_text', 0, 0),
'version.Topaz_version' : (1, 'scalar_text', 0, 0), b'version.Topaz_version' : (1, 'scalar_text', 0, 0),
'version.WordDetailEdit_1_id' : (1, 'scalar_text', 0, 0), b'version.WordDetailEdit_1_id' : (1, 'scalar_text', 0, 0),
'version.WordDetailEdit_1_version' : (1, 'scalar_text', 0, 0), b'version.WordDetailEdit_1_version' : (1, 'scalar_text', 0, 0),
'version.ZoneEdit_1_id' : (1, 'scalar_text', 0, 0), b'version.ZoneEdit_1_id' : (1, 'scalar_text', 0, 0),
'version.ZoneEdit_1_version' : (1, 'scalar_text', 0, 0), b'version.ZoneEdit_1_version' : (1, 'scalar_text', 0, 0),
'version.chapterheaders' : (1, 'scalar_text', 0, 0), b'version.chapterheaders' : (1, 'scalar_text', 0, 0),
'version.creation_date' : (1, 'scalar_text', 0, 0), b'version.creation_date' : (1, 'scalar_text', 0, 0),
'version.header_footer' : (1, 'scalar_text', 0, 0), b'version.header_footer' : (1, 'scalar_text', 0, 0),
'version.init_from_ocr' : (1, 'scalar_text', 0, 0), b'version.init_from_ocr' : (1, 'scalar_text', 0, 0),
'version.letter_insertion' : (1, 'scalar_text', 0, 0), b'version.letter_insertion' : (1, 'scalar_text', 0, 0),
'version.xmlinj_convert' : (1, 'scalar_text', 0, 0), b'version.xmlinj_convert' : (1, 'scalar_text', 0, 0),
'version.xmlinj_reflow' : (1, 'scalar_text', 0, 0), b'version.xmlinj_reflow' : (1, 'scalar_text', 0, 0),
'version.xmlinj_transform' : (1, 'scalar_text', 0, 0), b'version.xmlinj_transform' : (1, 'scalar_text', 0, 0),
'version.findlists' : (1, 'scalar_text', 0, 0), b'version.findlists' : (1, 'scalar_text', 0, 0),
'version.page_num' : (1, 'scalar_text', 0, 0), b'version.page_num' : (1, 'scalar_text', 0, 0),
'version.page_type' : (1, 'scalar_text', 0, 0), b'version.page_type' : (1, 'scalar_text', 0, 0),
'version.bad_text' : (1, 'scalar_text', 0, 0), b'version.bad_text' : (1, 'scalar_text', 0, 0),
'version.glyph_mismatch' : (1, 'scalar_text', 0, 0), b'version.glyph_mismatch' : (1, 'scalar_text', 0, 0),
'version.margins' : (1, 'scalar_text', 0, 0), b'version.margins' : (1, 'scalar_text', 0, 0),
'version.staggered_lines' : (1, 'scalar_text', 0, 0), b'version.staggered_lines' : (1, 'scalar_text', 0, 0),
'version.paragraph_continuation' : (1, 'scalar_text', 0, 0), b'version.paragraph_continuation' : (1, 'scalar_text', 0, 0),
'version.toc' : (1, 'scalar_text', 0, 0), b'version.toc' : (1, 'scalar_text', 0, 0),
'stylesheet' : (1, 'snippets', 1, 0), b'stylesheet' : (1, 'snippets', 1, 0),
'style' : (1, 'snippets', 1, 0), b'style' : (1, 'snippets', 1, 0),
'style._tag' : (1, 'scalar_text', 0, 0), b'style._tag' : (1, 'scalar_text', 0, 0),
'style.type' : (1, 'scalar_text', 0, 0), b'style.type' : (1, 'scalar_text', 0, 0),
'style._after_type' : (1, 'scalar_text', 0, 0), b'style._after_type' : (1, 'scalar_text', 0, 0),
'style._parent_type' : (1, 'scalar_text', 0, 0), b'style._parent_type' : (1, 'scalar_text', 0, 0),
'style._after_parent_type' : (1, 'scalar_text', 0, 0), b'style._after_parent_type' : (1, 'scalar_text', 0, 0),
'style.class' : (1, 'scalar_text', 0, 0), b'style.class' : (1, 'scalar_text', 0, 0),
'style._after_class' : (1, 'scalar_text', 0, 0), b'style._after_class' : (1, 'scalar_text', 0, 0),
'rule' : (1, 'snippets', 1, 0), b'rule' : (1, 'snippets', 1, 0),
'rule.attr' : (1, 'scalar_text', 0, 0), b'rule.attr' : (1, 'scalar_text', 0, 0),
'rule.value' : (1, 'scalar_text', 0, 0), b'rule.value' : (1, 'scalar_text', 0, 0),
'original' : (0, 'number', 1, 1), b'original' : (0, 'number', 1, 1),
'original.pnum' : (1, 'number', 0, 0), b'original.pnum' : (1, 'number', 0, 0),
'original.pid' : (1, 'text', 0, 0), b'original.pid' : (1, 'text', 0, 0),
'pages' : (0, 'number', 1, 1), b'pages' : (0, 'number', 1, 1),
'pages.ref' : (1, 'number', 0, 0), b'pages.ref' : (1, 'number', 0, 0),
'pages.id' : (1, 'number', 0, 0), b'pages.id' : (1, 'number', 0, 0),
'startID' : (0, 'number', 1, 1), b'startID' : (0, 'number', 1, 1),
'startID.page' : (1, 'number', 0, 0), b'startID.page' : (1, 'number', 0, 0),
'startID.id' : (1, 'number', 0, 0), b'startID.id' : (1, 'number', 0, 0),
'median_d' : (1, 'number', 0, 0), b'median_d' : (1, 'number', 0, 0),
'median_h' : (1, 'number', 0, 0), b'median_h' : (1, 'number', 0, 0),
'median_firsty' : (1, 'number', 0, 0), b'median_firsty' : (1, 'number', 0, 0),
'median_lasty' : (1, 'number', 0, 0), b'median_lasty' : (1, 'number', 0, 0),
'num_footers_maybe' : (1, 'number', 0, 0), b'num_footers_maybe' : (1, 'number', 0, 0),
'num_footers_yes' : (1, 'number', 0, 0), b'num_footers_yes' : (1, 'number', 0, 0),
'num_headers_maybe' : (1, 'number', 0, 0), b'num_headers_maybe' : (1, 'number', 0, 0),
'num_headers_yes' : (1, 'number', 0, 0), b'num_headers_yes' : (1, 'number', 0, 0),
'tracking' : (1, 'number', 0, 0), b'tracking' : (1, 'number', 0, 0),
'src' : (1, 'text', 0, 0), b'src' : (1, 'text', 0, 0),
} }
@@ -420,8 +435,8 @@ class PageParser(object):
def get_tagpath(self, i): def get_tagpath(self, i):
cnt = len(self.tagpath) cnt = len(self.tagpath)
if i < cnt : result = self.tagpath[i] if i < cnt : result = self.tagpath[i]
for j in xrange(i+1, cnt) : for j in range(i+1, cnt) :
result += '.' + self.tagpath[j] result += b'.' + self.tagpath[j]
return result return result
@@ -472,7 +487,7 @@ class PageParser(object):
if self.debug : print('Processing: ', self.get_tagpath(0)) if self.debug : print('Processing: ', self.get_tagpath(0))
cnt = self.tagpath_len() cnt = self.tagpath_len()
for j in xrange(cnt): for j in range(cnt):
tkn = self.get_tagpath(j) tkn = self.get_tagpath(j)
if tkn in self.token_tags : if tkn in self.token_tags :
num_args = self.token_tags[tkn][0] num_args = self.token_tags[tkn][0]
@@ -496,8 +511,8 @@ class PageParser(object):
if (subtags == 1): if (subtags == 1):
ntags = readEncodedNumber(self.fo) ntags = readEncodedNumber(self.fo)
if self.debug : print('subtags: ' + token + ' has ' + str(ntags)) if self.debug : print('subtags: ', token , ' has ' , str(ntags))
for j in xrange(ntags): for j in range(ntags):
val = readEncodedNumber(self.fo) val = readEncodedNumber(self.fo)
subtagres.append(self.procToken(self.dict.lookup(val))) subtagres.append(self.procToken(self.dict.lookup(val)))
@@ -511,7 +526,7 @@ class PageParser(object):
argres = self.decodeCMD(arg,argtype) argres = self.decodeCMD(arg,argtype)
else : else :
# num_arg scalar arguments # num_arg scalar arguments
for i in xrange(num_args): for i in range(num_args):
argres.append(self.formatArg(readEncodedNumber(self.fo), argtype)) argres.append(self.formatArg(readEncodedNumber(self.fo), argtype))
# build the return tag # build the return tag
@@ -546,7 +561,7 @@ class PageParser(object):
result += 'of the document is indicated by snippet number sets at the\n' result += 'of the document is indicated by snippet number sets at the\n'
result += 'end of each snippet. \n' result += 'end of each snippet. \n'
print(result) print(result)
for i in xrange(cnt): for i in range(cnt):
if self.debug: print('Snippet:',str(i)) if self.debug: print('Snippet:',str(i))
snippet = [] snippet = []
snippet.append(i) snippet.append(i)
@@ -565,12 +580,12 @@ class PageParser(object):
adj = readEncodedNumber(self.fo) adj = readEncodedNumber(self.fo)
mode = mode >> 1 mode = mode >> 1
x = [] x = []
for i in xrange(cnt): for i in range(cnt):
x.append(readEncodedNumber(self.fo) - adj) x.append(readEncodedNumber(self.fo) - adj)
for i in xrange(mode): for i in range(mode):
for j in xrange(1, cnt): for j in range(1, cnt):
x[j] = x[j] + x[j - 1] x[j] = x[j] + x[j - 1]
for i in xrange(cnt): for i in range(cnt):
result.append(self.formatArg(x[i],argtype)) result.append(self.formatArg(x[i],argtype))
return result return result
@@ -604,7 +619,7 @@ class PageParser(object):
subtagList = tag[1] subtagList = tag[1]
argtype = tag[2] argtype = tag[2]
argList = tag[3] argList = tag[3]
nname = prefix + '.' + name nname = prefix + b'.' + name
nsubtaglist = [] nsubtaglist = []
for j in subtagList: for j in subtagList:
nsubtaglist.append(self.updateName(j,prefix)) nsubtaglist.append(self.updateName(j,prefix))
@@ -653,34 +668,34 @@ class PageParser(object):
subtagList = node[1] subtagList = node[1]
argtype = node[2] argtype = node[2]
argList = node[3] argList = node[3]
fullpathname = name.split('.') fullpathname = name.split(b'.')
nodename = fullpathname.pop() nodename = fullpathname.pop()
ilvl = len(fullpathname) ilvl = len(fullpathname)
indent = ' ' * (3 * ilvl) indent = b' ' * (3 * ilvl)
rlst = [] rlst = []
rlst.append(indent + '<' + nodename + '>') rlst.append(indent + b'<' + nodename + b'>')
if len(argList) > 0: if len(argList) > 0:
alst = [] alst = []
for j in argList: for j in argList:
if (argtype == 'text') or (argtype == 'scalar_text') : if (argtype == b'text') or (argtype == b'scalar_text') :
alst.append(j + '|') alst.append(j + b'|')
else : else :
alst.append(str(j) + ',') alst.append(str(j).encode('utf-8') + b',')
argres = "".join(alst) argres = b"".join(alst)
argres = argres[0:-1] argres = argres[0:-1]
if argtype == 'snippets' : if argtype == b'snippets' :
rlst.append('snippets:' + argres) rlst.append(b'snippets:' + argres)
else : else :
rlst.append(argres) rlst.append(argres)
if len(subtagList) > 0 : if len(subtagList) > 0 :
rlst.append('\n') rlst.append(b'\n')
for j in subtagList: for j in subtagList:
if len(j) > 0 : if len(j) > 0 :
rlst.append(self.formatTag(j)) rlst.append(self.formatTag(j))
rlst.append(indent + '</' + nodename + '>\n') rlst.append(indent + b'</' + nodename + b'>\n')
else: else:
rlst.append('</' + nodename + '>\n') rlst.append(b'</' + nodename + b'>\n')
return "".join(rlst) return b"".join(rlst)
# flatten tag # flatten tag
@@ -695,20 +710,20 @@ class PageParser(object):
alst = [] alst = []
for j in argList: for j in argList:
if (argtype == 'text') or (argtype == 'scalar_text') : if (argtype == 'text') or (argtype == 'scalar_text') :
alst.append(j + '|') alst.append(j + b'|')
else : else :
alst.append(str(j) + '|') alst.append(str(j).encode('utf-8') + b'|')
argres = "".join(alst) argres = b"".join(alst)
argres = argres[0:-1] argres = argres[0:-1]
if argtype == 'snippets' : if argtype == b'snippets' :
rlst.append('.snippets=' + argres) rlst.append(b'.snippets=' + argres)
else : else :
rlst.append('=' + argres) rlst.append(b'=' + argres)
rlst.append('\n') rlst.append(b'\n')
for j in subtagList: for j in subtagList:
if len(j) > 0 : if len(j) > 0 :
rlst.append(self.flattenTag(j)) rlst.append(self.flattenTag(j))
return "".join(rlst) return b"".join(rlst)
# reduce create xml output # reduce create xml output
@@ -720,7 +735,7 @@ class PageParser(object):
rlst.append(self.flattenTag(j)) rlst.append(self.flattenTag(j))
else: else:
rlst.append(self.formatTag(j)) rlst.append(self.formatTag(j))
result = "".join(rlst) result = b"".join(rlst)
if self.debug : print(result) if self.debug : print(result)
return result return result
@@ -738,16 +753,16 @@ class PageParser(object):
# peek at the first bytes to see what type of file it is # peek at the first bytes to see what type of file it is
magic = self.fo.read(9) magic = self.fo.read(9)
if (magic[0:1] == 'p') and (magic[2:9] == 'marker_'): if (magic[0:1] == b'p') and (magic[2:9] == b'marker_'):
first_token = 'info' first_token = b'info'
elif (magic[0:1] == 'p') and (magic[2:9] == '__PAGE_'): elif (magic[0:1] == b'p') and (magic[2:9] == b'__PAGE_'):
skip = self.fo.read(2) skip = self.fo.read(2)
first_token = 'info' first_token = b'info'
elif (magic[0:1] == 'p') and (magic[2:8] == '_PAGE_'): elif (magic[0:1] == b'p') and (magic[2:8] == b'_PAGE_'):
first_token = 'info' first_token = b'info'
elif (magic[0:1] == 'g') and (magic[2:9] == '__GLYPH'): elif (magic[0:1] == b'g') and (magic[2:9] == b'__GLYPH'):
skip = self.fo.read(3) skip = self.fo.read(3)
first_token = 'info' first_token = b'info'
else : else :
# other0.dat file # other0.dat file
first_token = None first_token = None
@@ -769,7 +784,7 @@ class PageParser(object):
break break
if (v == 0x72): if (v == 0x72):
self.doLoop72('number') self.doLoop72(b'number')
elif (v > 0) and (v < self.dict.getSize()) : elif (v > 0) and (v < self.dict.getSize()) :
tag = self.procToken(self.dict.lookup(v)) tag = self.procToken(self.dict.lookup(v))
if len(tag) > 0 : if len(tag) > 0 :
@@ -780,7 +795,7 @@ class PageParser(object):
if (v == 0): if (v == 0):
if (self.peek(1) == 0x5f): if (self.peek(1) == 0x5f):
skip = self.fo.read(1) skip = self.fo.read(1)
first_token = 'info' first_token = b'info'
# now do snippet injection # now do snippet injection
if len(self.snippetList) > 0 : if len(self.snippetList) > 0 :
@@ -800,14 +815,14 @@ class PageParser(object):
def fromData(dict, fname): def fromData(dict, fname):
flat_xml = True flat_xml = True
debug = False debug = True
pp = PageParser(fname, dict, debug, flat_xml) pp = PageParser(fname, dict, debug, flat_xml)
xmlpage = pp.process() xmlpage = pp.process()
return xmlpage return xmlpage
def getXML(dict, fname): def getXML(dict, fname):
flat_xml = False flat_xml = False
debug = False debug = True
pp = PageParser(fname, dict, debug, flat_xml) pp = PageParser(fname, dict, debug, flat_xml)
xmlpage = pp.process() xmlpage = pp.process()
return xmlpage return xmlpage
@@ -832,9 +847,11 @@ def usage():
# #
def main(argv): def main(argv):
sys.stdout=SafeUnbuffered(sys.stdout)
sys.stderr=SafeUnbuffered(sys.stderr)
dictFile = "" dictFile = ""
pageFile = "" pageFile = ""
debug = False debug = True
flat_xml = False flat_xml = False
printOutput = False printOutput = False
if len(argv) == 0: if len(argv) == 0:
@@ -844,7 +861,7 @@ def main(argv):
try: try:
opts, args = getopt.getopt(argv[1:], "hd", ["flat-xml"]) opts, args = getopt.getopt(argv[1:], "hd", ["flat-xml"])
except getopt.GetoptError, err: except getopt.GetoptError as err:
# print help information and exit: # 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"

View File

@@ -1,46 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# base64.py, version 1.0
# Copyright © 2010 Apprentice Alf
# Released under the terms of the GNU General Public Licence, version 3 or
# later. <http://www.gnu.org/licenses/>
# Revision history:
# 1 - Initial release. To allow Applescript to do base64 encoding
"""
Provide base64 encoding.
"""
from __future__ import with_statement
from __future__ import print_function
__license__ = 'GPL v3'
import sys
import os
import base64
def usage(progname):
print("Applies base64 encoding to the supplied file, sending to standard output")
print("Usage:")
print(" %s <infile>" % progname)
def cli_main(argv=sys.argv):
progname = os.path.basename(argv[0])
if len(argv)<2:
usage(progname)
sys.exit(2)
keypath = argv[1]
with open(keypath, 'rb') as f:
keyder = f.read()
print(keyder.encode('base64'))
return 0
if __name__ == '__main__':
sys.exit(cli_main())

View File

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

View File

@@ -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. # This is a python script. You need a Python interpreter to run it.
# For example, ActiveState Python, which exists for windows. # For example, ActiveState Python, which exists for windows.
@@ -10,6 +11,8 @@
# Changelog epubtest # Changelog epubtest
# 1.00 - Cut to epubtest.py, testing ePub files only by Apprentice Alf # 1.00 - Cut to epubtest.py, testing ePub files only by Apprentice Alf
# 1.01 - Added routine for use by Windows DeDRM # 1.01 - Added routine for use by Windows DeDRM
# 2.00 - Python 3, September 2020
# 2.01 - Add new Adobe DRM, add Readium LCP
# #
# Written in 2011 by Paul Durrant # Written in 2011 by Paul Durrant
# Released with unlicense. See http://unlicense.org/ # Released with unlicense. See http://unlicense.org/
@@ -44,10 +47,7 @@
# It's still polite to give attribution if you do reuse this code. # It's still polite to give attribution if you do reuse this code.
# #
from __future__ import with_statement __version__ = '2.0'
from __future__ import print_function
__version__ = '1.01'
import sys, struct, os, traceback import sys, struct, os, traceback
import zlib import zlib
@@ -67,10 +67,17 @@ class SafeUnbuffered:
if self.encoding == None: if self.encoding == None:
self.encoding = "utf-8" self.encoding = "utf-8"
def write(self, data): def write(self, data):
if isinstance(data,unicode): if isinstance(data,str) or isinstance(data,unicode):
# str for Python3, unicode for Python2
data = data.encode(self.encoding,"replace") data = data.encode(self.encoding,"replace")
self.stream.write(data) try:
self.stream.flush() buffer = getattr(self.stream, 'buffer', self.stream)
# self.stream.buffer for Python3, self.stream for Python2
buffer.write(data)
buffer.flush()
except:
# We can do nothing if a write fails
raise
def __getattr__(self, attr): def __getattr__(self, attr):
return getattr(self.stream, attr) return getattr(self.stream, attr)
@@ -108,15 +115,13 @@ def unicode_argv():
# Remove Python executable and commands if present # Remove Python executable and commands if present
start = argc.value - len(sys.argv) start = argc.value - len(sys.argv)
return [argv[i] for i in return [argv[i] for i in
xrange(start, argc.value)] range(start, argc.value)]
# if we don't have any arguments at all, just pass back script name # if we don't have any arguments at all, just pass back script name
# this should never happen # this should never happen
return [u"epubtest.py"] return ["epubtest.py"]
else: else:
argvencoding = sys.stdin.encoding argvencoding = sys.stdin.encoding or "utf-8"
if argvencoding == None: return [arg if (isinstance(arg, str) or isinstance(arg,unicode)) else str(arg, argvencoding) for arg in sys.argv]
argvencoding = "utf-8"
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
_FILENAME_LEN_OFFSET = 26 _FILENAME_LEN_OFFSET = 26
_EXTRA_LEN_OFFSET = 28 _EXTRA_LEN_OFFSET = 28
@@ -171,17 +176,23 @@ def getfiledata(file, zi):
def encryption(infile): def encryption(infile):
# returns encryption: one of Unencrypted, Adobe, B&N and Unknown # returns encryption: one of Unencrypted, Adobe, B&N and Unknown
encryption = "Unknown" encryption = "Error When Checking."
try: try:
with open(infile,'rb') as infileobject: with open(infile,'rb') as infileobject:
bookdata = infileobject.read(58) bookdata = infileobject.read(58)
# Check for Zip # Check for Zip
if bookdata[0:0+2] == "PK": if bookdata[0:0+2] == b"PK":
foundrights = False foundrights = False
foundencryption = False foundencryption = False
inzip = zipfile.ZipFile(infile,'r') inzip = zipfile.ZipFile(infile,'r')
namelist = set(inzip.namelist()) namelist = set(inzip.namelist())
if 'META-INF/rights.xml' not in namelist or 'META-INF/encryption.xml' not in namelist: if (
'META-INF/encryption.xml' in namelist and
'META-INF/license.lcpl' in namelist and
b"EncryptedContentKey" in inzip.read("META-INF/encryption.xml")):
encryption = "Readium LCP"
elif 'META-INF/rights.xml' not in namelist or 'META-INF/encryption.xml' not in namelist:
encryption = "Unencrypted" encryption = "Unencrypted"
else: else:
rights = etree.fromstring(inzip.read('META-INF/rights.xml')) rights = etree.fromstring(inzip.read('META-INF/rights.xml'))
@@ -189,7 +200,9 @@ def encryption(infile):
expr = './/%s' % (adept('encryptedKey'),) expr = './/%s' % (adept('encryptedKey'),)
bookkey = ''.join(rights.findtext(expr)) bookkey = ''.join(rights.findtext(expr))
if len(bookkey) == 172: if len(bookkey) == 172:
encryption = "Adobe" encryption = "Adobe (old)"
if len(bookkey) == 192:
encryption = "Adobe (new)"
elif len(bookkey) == 64: elif len(bookkey) == 64:
encryption = "B&N" encryption = "B&N"
else: else:
@@ -200,7 +213,10 @@ def encryption(infile):
def main(): def main():
argv=unicode_argv() argv=unicode_argv()
print(encryption(argv[1])) if len(argv) < 2:
print("Give an ePub file as a parameter.")
else:
print(encryption(argv[1]))
return 0 return 0
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -0,0 +1,244 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# epubwatermark.py
# Copyright © 2021 NoDRM
# Revision history:
# 1.0 - Initial version
# Released under the terms of the GNU General Public Licence, version 3
# <http://www.gnu.org/licenses/>
"""
Removes various watermarks from EPUB files
"""
import traceback
from zipfile import ZipInfo, ZipFile, ZIP_STORED, ZIP_DEFLATED
from contextlib import closing
from lxml import etree
import re
# Runs a RegEx over all HTML/XHTML files to remove watermakrs.
def removeHTMLwatermarks(object, path_to_ebook):
try:
inf = ZipFile(open(path_to_ebook, 'rb'))
namelist = inf.namelist()
modded_names = []
modded_contents = []
for file in namelist:
if not (file.endswith('.html') or file.endswith('.xhtml') or file.endswith('.xml')):
continue
try:
file_str = inf.read(file).decode("utf-8")
str_new = file_str
# Remove Adobe ADEPT watermarks
# Match optional newline at the beginning, then a "meta" tag with name = "Adept.expected.resource" or "Adept.resource"
# and either a "value" or a "content" element with an Adobe UUID
str_new = re.sub(r'((\r\n|\r|\n)\s*)?\<meta\s+name=\"(Adept\.resource|Adept\.expected\.resource)\"\s+(content|value)=\"urn:uuid:[0-9a-fA-F\-]+\"\s*\/>', '', str_new)
str_new = re.sub(r'((\r\n|\r|\n)\s*)?\<meta\s+(content|value)=\"urn:uuid:[0-9a-fA-F\-]+\"\s+name=\"(Adept\.resource|Adept\.expected\.resource)\"\s*\/>', '', str_new)
except:
traceback.print_exc()
continue
if (file_str == str_new):
continue
modded_names.append(file)
modded_contents.append(str_new)
if len(modded_names) == 0:
# No file modified, return original
return path_to_ebook
if len(modded_names) != len(modded_contents):
# Something went terribly wrong, return original
print("Watermark: Error during ADEPT watermark removal")
return path_to_ebook
# Re-package with modified files:
namelist.remove("mimetype")
try:
output = object.temporary_file(".epub").name
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
with closing(ZipFile(open(output, 'wb'), 'w', **kwds)) as outf:
for path in (["mimetype"] + namelist):
data = inf.read(path)
try:
modded_index = None
modded_index = modded_names.index(path)
except:
pass
if modded_index is not None:
# Found modified file - replace contents
data = modded_contents[modded_index]
zi = ZipInfo(path)
oldzi = inf.getinfo(path)
try:
zi.compress_type = oldzi.compress_type
if path == "mimetype":
zi.compress_type = ZIP_STORED
zi.date_time = oldzi.date_time
zi.comment = oldzi.comment
zi.extra = oldzi.extra
zi.internal_attr = oldzi.internal_attr
zi.external_attr = oldzi.external_attr
zi.create_system = oldzi.create_system
if any(ord(c) >= 128 for c in path) or any(ord(c) >= 128 for c in zi.comment):
# If the file name or the comment contains any non-ASCII char, set the UTF8-flag
zi.flag_bits |= 0x800
except:
pass
outf.writestr(zi, data)
except:
traceback.print_exc()
return path_to_ebook
except:
traceback.print_exc()
return path_to_ebook
print("Watermark: Successfully stripped {0} ADEPT watermark(s) from ebook.".format(len(modded_names)))
return output
# Finds the main OPF file, then uses RegEx to remove watermarks
def removeOPFwatermarks(object, path_to_ebook):
contNS = lambda tag: '{%s}%s' % ('urn:oasis:names:tc:opendocument:xmlns:container', tag)
opf_path = None
try:
inf = ZipFile(open(path_to_ebook, 'rb'))
container = etree.fromstring(inf.read("META-INF/container.xml"))
rootfiles = container.find(contNS("rootfiles")).findall(contNS("rootfile"))
for rootfile in rootfiles:
opf_path = rootfile.get("full-path", None)
if (opf_path is not None):
break
except:
traceback.print_exc()
return path_to_ebook
# If path is None, we didn't find an OPF, so we probably don't have a font key.
# If path is set, it's the path to the main content OPF file.
if (opf_path is None):
# No OPF found - no watermark
return path_to_ebook
else:
try:
container_str = inf.read(opf_path).decode("utf-8")
container_str_new = container_str
# Remove Amazon hex watermarks
# Match optional newline at the beginning, then spaces, then a "meta" tag with name = "Watermark" or "Watermark_(hex)" and a "content" element.
container_str_new = re.sub(r'((\r\n|\r|\n)\s*)?\<meta\s+name=\"Watermark(_\(hex\))?\"\s+content=\"[0-9a-fA-F]+\"\s*\/>', '', container_str_new)
container_str_new = re.sub(r'((\r\n|\r|\n)\s*)?\<meta\s+content=\"[0-9a-fA-F]+\"\s+name=\"Watermark(_\(hex\))?\"\s*\/>', '', container_str_new)
except:
traceback.print_exc()
return path_to_ebook
if (container_str == container_str_new):
# container didn't change - no watermark
return path_to_ebook
# Re-package without watermark
namelist = inf.namelist()
namelist.remove("mimetype")
try:
output = object.temporary_file(".epub").name
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
with closing(ZipFile(open(output, 'wb'), 'w', **kwds)) as outf:
for path in (["mimetype"] + namelist):
data = inf.read(path)
if path == opf_path:
# Found OPF, replacing ...
data = container_str_new
zi = ZipInfo(path)
oldzi = inf.getinfo(path)
try:
zi.compress_type = oldzi.compress_type
if path == "mimetype":
zi.compress_type = ZIP_STORED
zi.date_time = oldzi.date_time
zi.comment = oldzi.comment
zi.extra = oldzi.extra
zi.internal_attr = oldzi.internal_attr
zi.external_attr = oldzi.external_attr
zi.create_system = oldzi.create_system
if any(ord(c) >= 128 for c in path) or any(ord(c) >= 128 for c in zi.comment):
# If the file name or the comment contains any non-ASCII char, set the UTF8-flag
zi.flag_bits |= 0x800
except:
pass
outf.writestr(zi, data)
except:
traceback.print_exc()
return path_to_ebook
print("Watermark: Successfully stripped Amazon watermark from OPF file.")
return output
def removeCDPwatermark(object, path_to_ebook):
# "META-INF/cdp.info" is a watermark file used by some Tolino vendors.
# We don't want that in our eBooks, so lets remove that file.
try:
infile = ZipFile(open(path_to_ebook, 'rb'))
namelist = infile.namelist()
if 'META-INF/cdp.info' not in namelist:
return path_to_ebook
namelist.remove("mimetype")
namelist.remove("META-INF/cdp.info")
output = object.temporary_file(".epub").name
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
with closing(ZipFile(open(output, 'wb'), 'w', **kwds)) as outf:
for path in (["mimetype"] + namelist):
data = infile.read(path)
zi = ZipInfo(path)
oldzi = infile.getinfo(path)
try:
zi.compress_type = oldzi.compress_type
if path == "mimetype":
zi.compress_type = ZIP_STORED
zi.date_time = oldzi.date_time
zi.comment = oldzi.comment
zi.extra = oldzi.extra
zi.internal_attr = oldzi.internal_attr
zi.external_attr = oldzi.external_attr
zi.create_system = oldzi.create_system
if any(ord(c) >= 128 for c in path) or any(ord(c) >= 128 for c in zi.comment):
# If the file name or the comment contains any non-ASCII char, set the UTF8-flag
zi.flag_bits |= 0x800
except:
pass
outf.writestr(zi, data)
print("Watermark: Successfully removed cdp.info watermark")
return output
except:
traceback.print_exc()
return path_to_ebook

157
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 -*- # -*- coding: utf-8 -*-
# erdr2pml.py # erdr2pml.py
# Copyright © 2008 The Dark Reverser # Copyright © 2008-2020 The Dark Reverser, Apprentice Harper 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 # Changelog
# #
# Based on ereader2html version 0.08 plus some later small fixes # Based on ereader2html version 0.08 plus some later small fixes
@@ -67,8 +63,9 @@
# - Ignore sidebars for dictionaries (different format?) # - Ignore sidebars for dictionaries (different format?)
# 0.22 - Unicode and plugin support, different image folders for PMLZ and source # 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 # 0.23 - moved unicode_argv call inside main for Windows DeDRM compatibility
# 1.00 - Added Python 3 compatibility for calibre 5.0
__version__='0.23' __version__='1.00'
import sys, re import sys, re
import struct, binascii, getopt, zlib, os, os.path, urllib, tempfile, traceback import struct, binascii, getopt, zlib, os, os.path, urllib, tempfile, traceback
@@ -88,10 +85,17 @@ class SafeUnbuffered:
if self.encoding == None: if self.encoding == None:
self.encoding = "utf-8" self.encoding = "utf-8"
def write(self, data): def write(self, data):
if isinstance(data,unicode): if isinstance(data,str) or isinstance(data,unicode):
# str for Python3, unicode for Python2
data = data.encode(self.encoding,"replace") data = data.encode(self.encoding,"replace")
self.stream.write(data) try:
self.stream.flush() buffer = getattr(self.stream, 'buffer', self.stream)
# self.stream.buffer for Python3, self.stream for Python2
buffer.write(data)
buffer.flush()
except:
# We can do nothing if a write fails
raise
def __getattr__(self, attr): def __getattr__(self, attr):
return getattr(self.stream, attr) return getattr(self.stream, attr)
@@ -126,15 +130,13 @@ def unicode_argv():
# Remove Python executable and commands if present # Remove Python executable and commands if present
start = argc.value - len(sys.argv) start = argc.value - len(sys.argv)
return [argv[i] for i in return [argv[i] for i in
xrange(start, argc.value)] range(start, argc.value)]
# if we don't have any arguments at all, just pass back script name # if we don't have any arguments at all, just pass back script name
# this should never happen # this should never happen
return [u"mobidedrm.py"] return ["mobidedrm.py"]
else: else:
argvencoding = sys.stdin.encoding argvencoding = sys.stdin.encoding or "utf-8"
if argvencoding == None: return [arg if (isinstance(arg, str) or isinstance(arg,unicode)) else str(arg, argvencoding) for arg in sys.argv]
argvencoding = "utf-8"
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
Des = None Des = None
if iswindows: if iswindows:
@@ -200,17 +202,17 @@ class Sectionizer(object):
bkType = "Book" bkType = "Book"
def __init__(self, filename, ident): def __init__(self, filename, ident):
self.contents = file(filename, 'rb').read() self.contents = open(filename, 'rb').read()
self.header = self.contents[0:72] self.header = self.contents[0:72]
self.num_sections, = struct.unpack('>H', self.contents[76:78]) self.num_sections, = struct.unpack('>H', self.contents[76:78])
# Dictionary or normal content (TODO: Not hard-coded) # Dictionary or normal content (TODO: Not hard-coded)
if self.header[0x3C:0x3C+8] != ident: 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" self.bkType = "Dict"
else: else:
raise ValueError('Invalid file format') raise ValueError('Invalid file format')
self.sections = [] 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]) 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 flags, val = a1, a2<<16|a3<<8|a4
self.sections.append( (offset, flags, val) ) self.sections.append( (offset, flags, val) )
@@ -229,28 +231,28 @@ class Sectionizer(object):
# and with some (heavily edited) code from Paul Durrant's kindlenamer.py # and with some (heavily edited) code from Paul Durrant's kindlenamer.py
def sanitizeFileName(name): def sanitizeFileName(name):
# substitute filename unfriendly characters # 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 # 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 # 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 # remove leading dots
while len(name)>0 and name[0] == u".": while len(name)>0 and name[0] == ".":
name = name[1:] name = name[1:]
# remove trailing dots (Windows doesn't like them) # remove trailing dots (Windows doesn't like them)
if name.endswith(u'.'): if name.endswith("."):
name = name[:-1] name = name[:-1]
return name return name
def fixKey(key): def fixKey(key):
def fixByte(b): def fixByte(b):
return b ^ ((b ^ (b<<1) ^ (b<<2) ^ (b<<3) ^ (b<<4) ^ (b<<5) ^ (b<<6) ^ (b<<7) ^ 0x80) & 0x80) return b ^ ((b ^ (b<<1) ^ (b<<2) ^ (b<<3) ^ (b<<4) ^ (b<<5) ^ (b<<6) ^ (b<<7) ^ 0x80) & 0x80)
return "".join([chr(fixByte(ord(a))) for a in key]) return bytes([fixByte(a) for a in key])
def deXOR(text, sp, table): def deXOR(text, sp, table):
r='' r=''
j = sp j = sp
for i in xrange(len(text)): for i in range(len(text)):
r += chr(ord(table[j]) ^ ord(text[i])) r += chr(ord(table[j]) ^ ord(text[i]))
j = j + 1 j = j + 1
if j == len(table): if j == len(table):
@@ -274,13 +276,13 @@ class EreaderProcessor(object):
raise ValueError('incorrect eReader version (error 2)') raise ValueError('incorrect eReader version (error 2)')
input = des.decrypt(data[-cookie_size:]) input = des.decrypt(data[-cookie_size:])
def unshuff(data, shuf): def unshuff(data, shuf):
r = [''] * len(data) r = [0] * len(data)
j = 0 j = 0
for i in xrange(len(data)): for i in range(len(data)):
j = (j + shuf) % len(data) j = (j + shuf) % len(data)
r[j] = data[i] r[j] = data[i]
assert len("".join(r)) == len(data) assert len(bytes(r)) == len(data)
return "".join(r) return bytes(r)
r = unshuff(input[0:-8], cookie_shuf) r = unshuff(input[0:-8], cookie_shuf)
drm_sub_version = struct.unpack('>H', r[0:2])[0] drm_sub_version = struct.unpack('>H', r[0:2])[0]
@@ -330,7 +332,7 @@ class EreaderProcessor(object):
self.flags = struct.unpack('>L', r[4:8])[0] self.flags = struct.unpack('>L', r[4:8])[0]
reqd_flags = (1<<9) | (1<<7) | (1<<10) reqd_flags = (1<<9) | (1<<7) | (1<<10)
if (self.flags & reqd_flags) != reqd_flags: if (self.flags & reqd_flags) != reqd_flags:
print "Flags: 0x%X" % self.flags print("Flags: 0x%X" % self.flags)
raise ValueError('incompatible eReader file') raise ValueError('incompatible eReader file')
des = Des(fixKey(user_key)) des = Des(fixKey(user_key))
if version == 259: if version == 259:
@@ -359,9 +361,9 @@ class EreaderProcessor(object):
def getImage(self, i): def getImage(self, i):
sect = self.section_reader(self.first_image_page + 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:] data = sect[62:]
return sanitizeFileName(unicode(name,'windows-1252')), data return sanitizeFileName(name.decode('windows-1252')), data
# def getChapterNamePMLOffsetData(self): # def getChapterNamePMLOffsetData(self):
@@ -409,8 +411,8 @@ class EreaderProcessor(object):
def getText(self): def getText(self):
des = Des(fixKey(self.content_key)) des = Des(fixKey(self.content_key))
r = '' r = b''
for i in xrange(self.num_text_pages): for i in range(self.num_text_pages):
logging.debug('get page %d', i) logging.debug('get page %d', i)
r += zlib.decompress(des.decrypt(self.section_reader(1 + i))) r += zlib.decompress(des.decrypt(self.section_reader(1 + i)))
@@ -422,7 +424,7 @@ class EreaderProcessor(object):
fnote_ids = deXOR(sect, 0, self.xortable) fnote_ids = deXOR(sect, 0, self.xortable)
# the remaining records of the footnote sections need to be decoded with the content_key and zlib inflated # the remaining records of the footnote sections need to be decoded with the content_key and zlib inflated
des = Des(fixKey(self.content_key)) des = Des(fixKey(self.content_key))
for i in xrange(1,self.num_footnote_pages): for i in range(1,self.num_footnote_pages):
logging.debug('get footnotepage %d', i) logging.debug('get footnotepage %d', i)
id_len = ord(fnote_ids[2]) id_len = ord(fnote_ids[2])
id = fnote_ids[3:3+id_len] id = fnote_ids[3:3+id_len]
@@ -446,7 +448,7 @@ class EreaderProcessor(object):
sbar_ids = deXOR(sect, 0, self.xortable) sbar_ids = deXOR(sect, 0, self.xortable)
# the remaining records of the sidebar sections need to be decoded with the content_key and zlib inflated # the remaining records of the sidebar sections need to be decoded with the content_key and zlib inflated
des = Des(fixKey(self.content_key)) des = Des(fixKey(self.content_key))
for i in xrange(1,self.num_sidebar_pages): for i in range(1,self.num_sidebar_pages):
id_len = ord(sbar_ids[2]) id_len = ord(sbar_ids[2])
id = sbar_ids[3:3+id_len] id = sbar_ids[3:3+id_len]
smarker = '<sidebar id="%s">\n' % id smarker = '<sidebar id="%s">\n' % id
@@ -460,9 +462,8 @@ class EreaderProcessor(object):
def cleanPML(pml): def cleanPML(pml):
# Convert special characters to proper PML code. High ASCII start at (\x80, \a128) and go up to (\xff, \a255) # Convert special characters to proper PML code. High ASCII start at (\x80, \a128) and go up to (\xff, \a255)
pml2 = pml pml2 = pml
for k in xrange(128,256): for k in range(128,256):
badChar = chr(k) pml2 = pml2.replace(bytes([k]), b'\\a%03d' % k)
pml2 = pml2.replace(badChar, '\\a%03d' % k)
return pml2 return pml2
def decryptBook(infile, outpath, make_pmlz, user_key): def decryptBook(infile, outpath, make_pmlz, user_key):
@@ -471,35 +472,35 @@ def decryptBook(infile, outpath, make_pmlz, user_key):
# outpath is actually pmlz name # outpath is actually pmlz name
pmlzname = outpath pmlzname = outpath
outdir = tempfile.mkdtemp() outdir = tempfile.mkdtemp()
imagedirpath = os.path.join(outdir,u"images") imagedirpath = os.path.join(outdir,"images")
else: else:
pmlzname = None pmlzname = None
outdir = outpath outdir = outpath
imagedirpath = os.path.join(outdir,bookname + u"_img") imagedirpath = os.path.join(outdir,bookname + "_img")
try: try:
if not os.path.exists(outdir): if not os.path.exists(outdir):
os.makedirs(outdir) os.makedirs(outdir)
print u"Decoding File" print("Decoding File")
sect = Sectionizer(infile, 'PNRdPPrs') sect =Sectionizer(infile, b'PNRdPPrs')
er = EreaderProcessor(sect, user_key) er = EreaderProcessor(sect, user_key)
if er.getNumImages() > 0: if er.getNumImages() > 0:
print u"Extracting images" print("Extracting images")
if not os.path.exists(imagedirpath): if not os.path.exists(imagedirpath):
os.makedirs(imagedirpath) os.makedirs(imagedirpath)
for i in xrange(er.getNumImages()): for i in range(er.getNumImages()):
name, contents = er.getImage(i) 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() pml_string = er.getText()
pmlfilename = bookname + ".pml" 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: if pmlzname is not None:
import zipfile import zipfile
import shutil 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) myZipFile = zipfile.ZipFile(pmlzname,'w',zipfile.ZIP_STORED, False)
list = os.listdir(outdir) list = os.listdir(outdir)
for filename in list: for filename in list:
@@ -518,48 +519,48 @@ def decryptBook(infile, outpath, make_pmlz, user_key):
myZipFile.close() myZipFile.close()
# remove temporary directory # remove temporary directory
shutil.rmtree(outdir, True) shutil.rmtree(outdir, True)
print u"Output is {0}".format(pmlzname) print("Output is {0}".format(pmlzname))
else : else:
print u"Output is in {0}".format(outdir) print("Output is in {0}".format(outdir))
print "done" print("done")
except ValueError, e: except ValueError as e:
print u"Error: {0}".format(e) print("Error: {0}".format(e))
traceback.print_exc() traceback.print_exc()
return 1 return 1
return 0 return 0
def usage(): def usage():
print u"Converts DRMed eReader books to PML Source" print("Converts DRMed eReader books to PML Source")
print u"Usage:" print("Usage:")
print u" erdr2pml [options] infile.pdb [outpath] \"your name\" credit_card_number" print(" erdr2pml [options] infile.pdb [outpath] \"your name\" credit_card_number")
print u" " print(" ")
print u"Options: " print("Options: ")
print u" -h prints this message" print(" -h prints this message")
print u" -p create PMLZ instead of source folder" print(" -p create PMLZ instead of source folder")
print u" --make-pmlz create PMLZ instead of source folder" print(" --make-pmlz create PMLZ instead of source folder")
print u" " print(" ")
print u"Note:" print("Note:")
print u" if outpath is ommitted, creates source in 'infile_Source' folder" print(" if outpath is ommitted, creates source in 'infile_Source' folder")
print u" if outpath is ommitted and pmlz option, creates PMLZ 'infile.pmlz'" print(" if outpath is ommitted and pmlz option, creates PMLZ 'infile.pmlz'")
print u" if source folder created, images are in infile_img folder" print(" if source folder created, images are in infile_img folder")
print u" if pmlz file created, images are in images folder" print(" 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(" It's enough to enter the last 8 digits of the credit card number")
return return
def getuser_key(name,cc): 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') newname = "".join(c for c in name.lower() if c >= 'a' and c <= 'z' or c >= '0' and c <= '9')
cc = cc.replace(" ","") 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(): 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()
try: try:
opts, args = getopt.getopt(argv[1:], "hp", ["make-pmlz"]) opts, args = getopt.getopt(argv[1:], "hp", ["make-pmlz"])
except getopt.GetoptError, err: except getopt.GetoptError as err:
print err.args[0] print(err.args[0])
usage() usage()
return 1 return 1
make_pmlz = False make_pmlz = False
@@ -579,13 +580,13 @@ def cli_main():
if len(args)==3: if len(args)==3:
infile, name, cc = args infile, name, cc = args
if make_pmlz: if make_pmlz:
outpath = os.path.splitext(infile)[0] + u".pmlz" outpath = os.path.splitext(infile)[0] + ".pmlz"
else: else:
outpath = os.path.splitext(infile)[0] + u"_Source" outpath = os.path.splitext(infile)[0] + "_Source"
elif len(args)==4: elif len(args)==4:
infile, outpath, name, cc = args 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)) return decryptBook(infile, outpath, make_pmlz, getuser_key(name,cc))

View File

@@ -2,12 +2,12 @@
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab # vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
# For use with Topaz Scripts Version 2.6 # For use with Topaz Scripts Version 2.6
from __future__ import print_function
import sys import sys
import csv import csv
import os import os
import math import math
import getopt import getopt
import functools
from struct import pack from struct import pack
from struct import unpack from struct import unpack
@@ -16,14 +16,14 @@ class DocParser(object):
def __init__(self, flatxml, classlst, fileid, bookDir, gdict, fixedimage): def __init__(self, flatxml, classlst, fileid, bookDir, gdict, fixedimage):
self.id = os.path.basename(fileid).replace('.dat','') self.id = os.path.basename(fileid).replace('.dat','')
self.svgcount = 0 self.svgcount = 0
self.docList = flatxml.split('\n') self.docList = flatxml.split(b'\n')
self.docSize = len(self.docList) self.docSize = len(self.docList)
self.classList = {} self.classList = {}
self.bookDir = bookDir self.bookDir = bookDir
self.gdict = gdict self.gdict = gdict
tmpList = classlst.split('\n') tmpList = classlst.split('\n')
for pclass in tmpList: for pclass in tmpList:
if pclass != '': if pclass != b'':
# remove the leading period from the css name # remove the leading period from the css name
cname = pclass[1:] cname = pclass[1:]
self.classList[cname] = True self.classList[cname] = True
@@ -58,9 +58,9 @@ class DocParser(object):
imgfile = os.path.join(imgDir,imgname) imgfile = os.path.join(imgDir,imgname)
# get glyph information # get glyph information
gxList = self.getData('info.glyph.x',0,-1) gxList = self.getData(b'info.glyph.x',0,-1)
gyList = self.getData('info.glyph.y',0,-1) gyList = self.getData(b'info.glyph.y',0,-1)
gidList = self.getData('info.glyph.glyphID',0,-1) gidList = self.getData(b'info.glyph.glyphID',0,-1)
gids = [] gids = []
maxws = [] maxws = []
@@ -95,7 +95,7 @@ class DocParser(object):
# change the origin to minx, miny and calc max height and width # change the origin to minx, miny and calc max height and width
maxw = maxws[0] + xs[0] - minx maxw = maxws[0] + xs[0] - minx
maxh = maxhs[0] + ys[0] - miny 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 xs[j] = xs[j] - minx
ys[j] = ys[j] - miny ys[j] = ys[j] - miny
maxw = max( maxw, (maxws[j] + xs[j]) ) maxw = max( maxw, (maxws[j] + xs[j]) )
@@ -107,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('<!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('<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') ifile.write('<defs>\n')
for j in xrange(0,len(gdefs)): for j in range(0,len(gdefs)):
ifile.write(gdefs[j]) ifile.write(gdefs[j])
ifile.write('</defs>\n') 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('<use xlink:href="#gl%d" x="%d" y="%d" />\n' % (gids[j], xs[j], ys[j]))
ifile.write('</svg>') ifile.write('</svg>')
ifile.close() ifile.close()
@@ -123,11 +123,11 @@ class DocParser(object):
def lineinDoc(self, pos) : def lineinDoc(self, pos) :
if (pos >= 0) and (pos < self.docSize) : if (pos >= 0) and (pos < self.docSize) :
item = self.docList[pos] item = self.docList[pos]
if item.find('=') >= 0: if item.find(b'=') >= 0:
(name, argres) = item.split('=',1) (name, argres) = item.split(b'=',1)
else : else :
name = item name = item
argres = '' argres = b''
return name, argres return name, argres
@@ -139,13 +139,15 @@ class DocParser(object):
else: else:
end = min(self.docSize, end) end = min(self.docSize, end)
foundat = -1 foundat = -1
for j in xrange(pos, end): for j in range(pos, end):
item = self.docList[j] item = self.docList[j]
if item.find('=') >= 0: if item.find(b'=') >= 0:
(name, argres) = item.split('=',1) (name, argres) = item.split(b'=',1)
else : else :
name = item name = item
argres = '' argres = ''
if (isinstance(tagpath,str)):
tagpath = tagpath.encode('utf-8')
if name.endswith(tagpath) : if name.endswith(tagpath) :
result = argres result = argres
foundat = j foundat = j
@@ -171,7 +173,7 @@ class DocParser(object):
argres=[] argres=[]
(foundat, argt) = self.findinDoc(tagpath, pos, end) (foundat, argt) = self.findinDoc(tagpath, pos, end)
if (argt != None) and (len(argt) > 0) : if (argt != None) and (len(argt) > 0) :
argList = argt.split('|') argList = argt.split(b'|')
argres = [ int(strval) for strval in argList] argres = [ int(strval) for strval in argList]
return argres return argres
@@ -192,21 +194,21 @@ class DocParser(object):
# also some class names have spaces in them so need to convert to dashes # also some class names have spaces in them so need to convert to dashes
if nclass != None : if nclass != None :
nclass = nclass.replace(' ','-') nclass = nclass.replace(b' ',b'-')
classres = '' classres = b''
nclass = nclass.lower() nclass = nclass.lower()
nclass = 'cl-' + nclass nclass = b'cl-' + nclass
baseclass = '' baseclass = b''
# graphic is the base class for captions # graphic is the base class for captions
if nclass.find('cl-cap-') >=0 : if nclass.find(b'cl-cap-') >=0 :
classres = 'graphic' + ' ' classres = b'graphic' + b' '
else : else :
# strip to find baseclass # strip to find baseclass
p = nclass.find('_') p = nclass.find(b'_')
if p > 0 : if p > 0 :
baseclass = nclass[0:p] baseclass = nclass[0:p]
if baseclass in self.classList: if baseclass in self.classList:
classres += baseclass + ' ' classres += baseclass + b' '
classres += nclass classres += nclass
nclass = classres nclass = classres
return nclass return nclass
@@ -226,11 +228,11 @@ class DocParser(object):
return -1 return -1
result = [] result = []
(pos, pagetype) = self.findinDoc('page.type',0,-1) (pos, pagetype) = self.findinDoc(b'page.type',0,-1)
groupList = self.posinDoc('page.group') groupList = self.posinDoc(b'page.group')
groupregionList = self.posinDoc('page.group.region') groupregionList = self.posinDoc(b'page.group.region')
pageregionList = self.posinDoc('page.region') pageregionList = self.posinDoc(b'page.region')
# integrate into one list # integrate into one list
for j in groupList: for j in groupList:
result.append(('grpbeg',j)) result.append(('grpbeg',j))
@@ -238,7 +240,7 @@ class DocParser(object):
result.append(('gregion',j)) result.append(('gregion',j))
for j in pageregionList: for j in pageregionList:
result.append(('pregion',j)) result.append(('pregion',j))
result.sort(compare) result.sort(key=functools.cmp_to_key(compare))
# insert group end and page end indicators # insert group end and page end indicators
inGroup = False inGroup = False
@@ -268,39 +270,39 @@ class DocParser(object):
result = [] result = []
# paragraph # paragraph
(pos, pclass) = self.findinDoc('paragraph.class',start,end) (pos, pclass) = self.findinDoc(b'paragraph.class',start,end)
pclass = self.getClass(pclass) pclass = self.getClass(pclass)
# if paragraph uses extratokens (extra glyphs) then make it fixed # 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 # build up a description of the paragraph in result and return it
# first check for the basic - all words paragraph # first check for the basic - all words paragraph
(pos, sfirst) = self.findinDoc('paragraph.firstWord',start,end) (pos, sfirst) = self.findinDoc(b'paragraph.firstWord',start,end)
(pos, slast) = self.findinDoc('paragraph.lastWord',start,end) (pos, slast) = self.findinDoc(b'paragraph.lastWord',start,end)
if (sfirst != None) and (slast != None) : if (sfirst != None) and (slast != None) :
first = int(sfirst) first = int(sfirst)
last = int(slast) last = int(slast)
makeImage = (regtype == 'vertical') or (regtype == 'table') makeImage = (regtype == b'vertical') or (regtype == b'table')
makeImage = makeImage or (extraglyphs != None) makeImage = makeImage or (extraglyphs != None)
if self.fixedimage: if self.fixedimage:
makeImage = makeImage or (regtype == 'fixed') makeImage = makeImage or (regtype == b'fixed')
if (pclass != None): if (pclass != None):
makeImage = makeImage or (pclass.find('.inverted') >= 0) makeImage = makeImage or (pclass.find(b'.inverted') >= 0)
if self.fixedimage : 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 # 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) makeImage = makeImage & (len(gidList) > 0)
if not makeImage : if not makeImage :
# standard all word paragraph # standard all word paragraph
for wordnum in xrange(first, last): for wordnum in range(first, last):
result.append(('ocr', wordnum)) result.append(('ocr', wordnum))
return pclass, result return pclass, result
@@ -308,8 +310,8 @@ class DocParser(object):
# translate first and last word into first and last glyphs # translate first and last word into first and last glyphs
# and generate inline image and include it # and generate inline image and include it
glyphList = [] glyphList = []
firstglyphList = self.getData('word.firstGlyph',0,-1) firstglyphList = self.getData(b'word.firstGlyph',0,-1)
gidList = self.getData('info.glyph.glyphID',0,-1) gidList = self.getData(b'info.glyph.glyphID',0,-1)
firstGlyph = firstglyphList[first] firstGlyph = firstglyphList[first]
if last < len(firstglyphList): if last < len(firstglyphList):
lastGlyph = firstglyphList[last] lastGlyph = firstglyphList[last]
@@ -320,17 +322,17 @@ class DocParser(object):
# by reverting to text based paragraph # by reverting to text based paragraph
if firstGlyph >= lastGlyph: if firstGlyph >= lastGlyph:
# revert to standard text based paragraph # revert to standard text based paragraph
for wordnum in xrange(first, last): for wordnum in range(first, last):
result.append(('ocr', wordnum)) result.append(('ocr', wordnum))
return pclass, result return pclass, result
for glyphnum in xrange(firstGlyph, lastGlyph): for glyphnum in range(firstGlyph, lastGlyph):
glyphList.append(glyphnum) glyphList.append(glyphnum)
# include any extratokens if they exist # include any extratokens if they exist
(pos, sfg) = self.findinDoc('extratokens.firstGlyph',start,end) (pos, sfg) = self.findinDoc(b'extratokens.firstGlyph',start,end)
(pos, slg) = self.findinDoc('extratokens.lastGlyph',start,end) (pos, slg) = self.findinDoc(b'extratokens.lastGlyph',start,end)
if (sfg != None) and (slg != None): 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) glyphList.append(glyphnum)
num = self.svgcount num = self.svgcount
self.glyphs_to_image(glyphList) self.glyphs_to_image(glyphList)
@@ -369,50 +371,50 @@ class DocParser(object):
(name, argres) = self.lineinDoc(line) (name, argres) = self.lineinDoc(line)
if name.endswith('span.firstWord') : if name.endswith(b'span.firstWord') :
sp_first = int(argres) sp_first = int(argres)
elif name.endswith('span.lastWord') : elif name.endswith(b'span.lastWord') :
sp_last = int(argres) sp_last = int(argres)
elif name.endswith('word.firstGlyph') : elif name.endswith(b'word.firstGlyph') :
gl_first = int(argres) gl_first = int(argres)
elif name.endswith('word.lastGlyph') : elif name.endswith(b'word.lastGlyph') :
gl_last = int(argres) gl_last = int(argres)
elif name.endswith('word_semantic.firstWord'): elif name.endswith(b'word_semantic.firstWord'):
ws_first = int(argres) ws_first = int(argres)
elif name.endswith('word_semantic.lastWord'): elif name.endswith(b'word_semantic.lastWord'):
ws_last = int(argres) ws_last = int(argres)
elif name.endswith('word.class'): elif name.endswith(b'word.class'):
# we only handle spaceafter word class # we only handle spaceafter word class
try: try:
(cname, space) = argres.split('-',1) (cname, space) = argres.split(b'-',1)
if space == '' : space = '0' if space == b'' : space = b'0'
if (cname == 'spaceafter') and (int(space) > 0) : if (cname == b'spaceafter') and (int(space) > 0) :
word_class = 'sa' word_class = 'sa'
except: except:
pass pass
elif name.endswith('word.img.src'): elif name.endswith(b'word.img.src'):
result.append(('img' + word_class, int(argres))) result.append(('img' + word_class, int(argres)))
word_class = '' word_class = ''
elif name.endswith('region.img.src'): elif name.endswith(b'region.img.src'):
result.append(('img' + word_class, int(argres))) result.append(('img' + word_class, int(argres)))
if (sp_first != -1) and (sp_last != -1): 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)) result.append(('ocr', wordnum))
sp_first = -1 sp_first = -1
sp_last = -1 sp_last = -1
if (gl_first != -1) and (gl_last != -1): if (gl_first != -1) and (gl_last != -1):
glyphList = [] glyphList = []
for glyphnum in xrange(gl_first, gl_last): for glyphnum in range(gl_first, gl_last):
glyphList.append(glyphnum) glyphList.append(glyphnum)
num = self.svgcount num = self.svgcount
self.glyphs_to_image(glyphList) self.glyphs_to_image(glyphList)
@@ -422,7 +424,7 @@ class DocParser(object):
gl_last = -1 gl_last = -1
if (ws_first != -1) and (ws_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)) result.append(('ocr', wordnum))
ws_first = -1 ws_first = -1
ws_last = -1 ws_last = -1
@@ -438,7 +440,7 @@ class DocParser(object):
classres = '' classres = ''
if pclass : if pclass :
classres = ' class="' + pclass + '"' classres = ' class="' + pclass.decode('utf-8') + '"'
br_lb = (regtype == 'fixed') or (regtype == 'chapterheading') or (regtype == 'vertical') br_lb = (regtype == 'fixed') or (regtype == 'chapterheading') or (regtype == 'vertical')
@@ -454,7 +456,7 @@ class DocParser(object):
cnt = len(pdesc) cnt = len(pdesc)
for j in xrange( 0, cnt) : for j in range( 0, cnt) :
(wtype, num) = pdesc[j] (wtype, num) = pdesc[j]
@@ -471,8 +473,8 @@ class DocParser(object):
if (link > 0): if (link > 0):
linktype = self.link_type[link-1] linktype = self.link_type[link-1]
title = self.link_title[link-1] title = self.link_title[link-1]
if (title == "") or (parares.rfind(title) < 0): if (title == b"") or (parares.rfind(title.decode('utf-8')) < 0):
title=parares[lstart:] title=parares[lstart:].encode('utf-8')
if linktype == 'external' : if linktype == 'external' :
linkhref = self.link_href[link-1] linkhref = self.link_href[link-1]
linkhtml = '<a href="%s">' % linkhref linkhtml = '<a href="%s">' % linkhref
@@ -483,33 +485,34 @@ class DocParser(object):
else : else :
# just link to the current page # just link to the current page
linkhtml = '<a href="#' + self.id + '">' linkhtml = '<a href="#' + self.id + '">'
linkhtml += title + '</a>' linkhtml += title.decode('utf-8')
pos = parares.rfind(title) linkhtml += '</a>'
pos = parares.rfind(title.decode('utf-8'))
if pos >= 0: if pos >= 0:
parares = parares[0:pos] + linkhtml + parares[pos+len(title):] parares = parares[0:pos] + linkhtml + parares[pos+len(title):]
else : else :
parares += linkhtml parares += linkhtml
lstart = len(parares) lstart = len(parares)
if word == '_link_' : word = '' if word == b'_link_' : word = b''
elif (link < 0) : 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: if ((num-1) in self.dehyphen_rootid ) or handle_links:
word = '' word = b''
sep = '' sep = ''
elif br_lb : elif br_lb :
word = '<br />\n' word = b'<br />\n'
sep = '' sep = ''
else : else :
word = '\n' word = b'\n'
sep = '' sep = ''
if num in self.dehyphen_rootid : if num in self.dehyphen_rootid :
word = word[0:-1] word = word[0:-1]
sep = '' sep = ''
parares += word + sep parares += word.decode('utf-8') + sep
elif wtype == 'img' : elif wtype == 'img' :
sep = '' sep = ''
@@ -523,7 +526,9 @@ class DocParser(object):
elif wtype == 'svg' : elif wtype == 'svg' :
sep = '' 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 parares += sep
if len(sep) > 0 : parares = parares[0:-1] if len(sep) > 0 : parares = parares[0:-1]
@@ -541,12 +546,12 @@ class DocParser(object):
lstart = 0 lstart = 0
cnt = len(pdesc) cnt = len(pdesc)
for j in xrange( 0, cnt) : for j in range( 0, cnt) :
(wtype, num) = pdesc[j] (wtype, num) = pdesc[j]
if wtype == 'ocr' : if wtype == 'ocr' :
word = self.ocrtext[num] word = self.ocrtext[num].decode('utf-8')
sep = ' ' sep = ' '
if handle_links: if handle_links:
@@ -554,7 +559,7 @@ class DocParser(object):
if (link > 0): if (link > 0):
linktype = self.link_type[link-1] linktype = self.link_type[link-1]
title = self.link_title[link-1] title = self.link_title[link-1]
title = title.rstrip('. ') title = title.rstrip(b'. ').decode('utf-8')
alt_title = parares[lstart:] alt_title = parares[lstart:]
alt_title = alt_title.strip() alt_title = alt_title.strip()
# now strip off the actual printed page number # now strip off the actual printed page number
@@ -608,38 +613,38 @@ class DocParser(object):
hlst = [] hlst = []
# get the ocr text # get the ocr text
(pos, argres) = self.findinDoc('info.word.ocrText',0,-1) (pos, argres) = self.findinDoc(b'info.word.ocrText',0,-1)
if argres : self.ocrtext = argres.split('|') if argres : self.ocrtext = argres.split(b'|')
# get information to dehyphenate the text # 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 # 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) first_para_continued = (self.parastems_stemid != None)
# determine if last paragraph is continued onto the next page # 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) last_para_continued = (self.paracont_stemid != None)
# collect link ids # 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 # 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) # collect link types (container versus external)
(pos, argres) = self.findinDoc('info.links.type',0,-1) (pos, argres) = self.findinDoc(b'info.links.type',0,-1)
if argres : self.link_type = argres.split('|') if argres : self.link_type = argres.split(b'|')
# collect link destinations # collect link destinations
(pos, argres) = self.findinDoc('info.links.href',0,-1) (pos, argres) = self.findinDoc(b'info.links.href',0,-1)
if argres : self.link_href = argres.split('|') if argres : self.link_href = argres.split(b'|')
# collect link titles # collect link titles
(pos, argres) = self.findinDoc('info.links.title',0,-1) (pos, argres) = self.findinDoc(b'info.links.title',0,-1)
if argres : if argres :
self.link_title = argres.split('|') self.link_title = argres.split(b'|')
else: else:
self.link_title.append('') self.link_title.append('')
@@ -654,7 +659,7 @@ class DocParser(object):
# process each region on the page and convert what you can to html # 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] (etype, start) = pageDesc[j]
(ntype, end) = pageDesc[j+1] (ntype, end) = pageDesc[j+1]
@@ -663,51 +668,51 @@ class DocParser(object):
# set anchor for link target on this page # set anchor for link target on this page
if not anchorSet and not first_para_continued: if not anchorSet and not first_para_continued:
hlst.append('<div style="visibility: hidden; height: 0; width: 0;" id="') 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 anchorSet = True
# handle groups of graphics with text captions # handle groups of graphics with text captions
if (etype == 'grpbeg'): if (etype == b'grpbeg'):
(pos, grptype) = self.findinDoc('group.type', start, end) (pos, grptype) = self.findinDoc(b'group.type', start, end)
if grptype != None: if grptype != None:
if grptype == 'graphic': if grptype == b'graphic':
gcstr = ' class="' + grptype + '"' gcstr = ' class="' + grptype.decode('utf-8') + '"'
hlst.append('<div' + gcstr + '>') hlst.append('<div' + gcstr + '>')
inGroup = True inGroup = True
elif (etype == 'grpend'): elif (etype == b'grpend'):
if inGroup: if inGroup:
hlst.append('</div>\n') hlst.append('</div>\n')
inGroup = False inGroup = False
else: else:
(pos, regtype) = self.findinDoc('region.type',start,end) (pos, regtype) = self.findinDoc(b'region.type',start,end)
if regtype == 'graphic' : if regtype == b'graphic' :
(pos, simgsrc) = self.findinDoc('img.src',start,end) (pos, simgsrc) = self.findinDoc(b'img.src',start,end)
if simgsrc: if simgsrc:
if inGroup: if inGroup:
hlst.append('<img src="img/img%04d.jpg" alt="" />' % int(simgsrc)) hlst.append('<img src="img/img%04d.jpg" alt="" />' % int(simgsrc))
else: else:
hlst.append('<div class="graphic"><img src="img/img%04d.jpg" alt="" /></div>' % int(simgsrc)) 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) (pclass, pdesc) = self.getParaDescription(start,end, regtype)
if not breakSet: if not breakSet:
hlst.append('<div style="page-break-after: always;">&nbsp;</div>\n') hlst.append('<div style="page-break-after: always;">&nbsp;</div>\n')
breakSet = True breakSet = True
tag = 'h1' tag = 'h1'
if pclass and (len(pclass) >= 7): if pclass and (len(pclass) >= 7):
if pclass[3:7] == 'ch1-' : tag = 'h1' if pclass[3:7] == b'ch1-' : tag = 'h1'
if pclass[3:7] == 'ch2-' : tag = 'h2' if pclass[3:7] == b'ch2-' : tag = 'h2'
if pclass[3:7] == 'ch3-' : tag = 'h3' if pclass[3:7] == b'ch3-' : tag = 'h3'
hlst.append('<' + tag + ' class="' + pclass + '">') hlst.append('<' + tag + ' class="' + pclass.decode('utf-8') + '">')
else: else:
hlst.append('<' + tag + '>') hlst.append('<' + tag + '>')
hlst.append(self.buildParagraph(pclass, pdesc, 'middle', regtype)) hlst.append(self.buildParagraph(pclass, pdesc, 'middle', regtype))
hlst.append('</' + tag + '>') 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' ptype = 'full'
# check to see if this is a continution from the previous page # check to see if this is a continution from the previous page
if first_para_continued : if first_para_continued :
@@ -716,16 +721,16 @@ class DocParser(object):
(pclass, pdesc) = self.getParaDescription(start,end, regtype) (pclass, pdesc) = self.getParaDescription(start,end, regtype)
if pclass and (len(pclass) >= 6) and (ptype == 'full'): if pclass and (len(pclass) >= 6) and (ptype == 'full'):
tag = 'p' tag = 'p'
if pclass[3:6] == 'h1-' : tag = 'h4' if pclass[3:6] == b'h1-' : tag = 'h4'
if pclass[3:6] == 'h2-' : tag = 'h5' if pclass[3:6] == b'h2-' : tag = 'h5'
if pclass[3:6] == 'h3-' : tag = 'h6' if pclass[3:6] == b'h3-' : tag = 'h6'
hlst.append('<' + tag + ' class="' + pclass + '">') hlst.append('<' + tag + ' class="' + pclass.decode('utf-8') + '">')
hlst.append(self.buildParagraph(pclass, pdesc, 'middle', regtype)) hlst.append(self.buildParagraph(pclass, pdesc, 'middle', regtype))
hlst.append('</' + tag + '>') hlst.append('</' + tag + '>')
else : else :
hlst.append(self.buildParagraph(pclass, pdesc, ptype, regtype)) hlst.append(self.buildParagraph(pclass, pdesc, ptype, regtype))
elif (regtype == 'tocentry') : elif (regtype == b'tocentry') :
ptype = 'full' ptype = 'full'
if first_para_continued : if first_para_continued :
ptype = 'end' ptype = 'end'
@@ -734,7 +739,7 @@ class DocParser(object):
tocinfo += self.buildTOCEntry(pdesc) tocinfo += self.buildTOCEntry(pdesc)
hlst.append(self.buildParagraph(pclass, pdesc, ptype, regtype)) hlst.append(self.buildParagraph(pclass, pdesc, ptype, regtype))
elif (regtype == 'vertical') or (regtype == 'table') : elif (regtype == b'vertical') or (regtype == b'table') :
ptype = 'full' ptype = 'full'
if inGroup: if inGroup:
ptype = 'middle' ptype = 'middle'
@@ -745,19 +750,19 @@ class DocParser(object):
hlst.append(self.buildParagraph(pclass, pdesc, ptype, regtype)) hlst.append(self.buildParagraph(pclass, pdesc, ptype, regtype))
elif (regtype == 'synth_fcvr.center'): elif (regtype == b'synth_fcvr.center'):
(pos, simgsrc) = self.findinDoc('img.src',start,end) (pos, simgsrc) = self.findinDoc(b'img.src',start,end)
if simgsrc: if simgsrc:
hlst.append('<div class="graphic"><img src="img/img%04d.jpg" alt="" /></div>' % int(simgsrc)) hlst.append('<div class="graphic"><img src="img/img%04d.jpg" alt="" /></div>' % int(simgsrc))
else : else :
print(' Making region type', regtype, end=' ') print(' Making region type', regtype, end=' ')
(pos, temp) = self.findinDoc('paragraph',start,end) (pos, temp) = self.findinDoc(b'paragraph',start,end)
(pos2, temp) = self.findinDoc('span',start,end) (pos2, temp) = self.findinDoc(b'span',start,end)
if pos != -1 or pos2 != -1: if pos != -1 or pos2 != -1:
print(' a "text" region') print(' a "text" region')
orig_regtype = regtype orig_regtype = regtype
regtype = 'fixed' regtype = b'fixed'
ptype = 'full' ptype = 'full'
# check to see if this is a continution from the previous page # check to see if this is a continution from the previous page
if first_para_continued : if first_para_continued :
@@ -765,23 +770,23 @@ class DocParser(object):
first_para_continued = False first_para_continued = False
(pclass, pdesc) = self.getParaDescription(start,end, regtype) (pclass, pdesc) = self.getParaDescription(start,end, regtype)
if not pclass: if not pclass:
if orig_regtype.endswith('.right') : pclass = 'cl-right' if orig_regtype.endswith(b'.right') : pclass = b'cl-right'
elif orig_regtype.endswith('.center') : pclass = 'cl-center' elif orig_regtype.endswith(b'.center') : pclass = b'cl-center'
elif orig_regtype.endswith('.left') : pclass = 'cl-left' elif orig_regtype.endswith(b'.left') : pclass = b'cl-left'
elif orig_regtype.endswith('.justify') : pclass = 'cl-justify' elif orig_regtype.endswith(b'.justify') : pclass = b'cl-justify'
if pclass and (ptype == 'full') and (len(pclass) >= 6): if pclass and (ptype == 'full') and (len(pclass) >= 6):
tag = 'p' tag = 'p'
if pclass[3:6] == 'h1-' : tag = 'h4' if pclass[3:6] == b'h1-' : tag = 'h4'
if pclass[3:6] == 'h2-' : tag = 'h5' if pclass[3:6] == b'h2-' : tag = 'h5'
if pclass[3:6] == 'h3-' : tag = 'h6' if pclass[3:6] == b'h3-' : tag = 'h6'
hlst.append('<' + tag + ' class="' + pclass + '">') hlst.append('<' + tag + ' class="' + pclass.decode('utf-8') + '">')
hlst.append(self.buildParagraph(pclass, pdesc, 'middle', regtype)) hlst.append(self.buildParagraph(pclass, pdesc, 'middle', regtype))
hlst.append('</' + tag + '>') hlst.append('</' + tag + '>')
else : else :
hlst.append(self.buildParagraph(pclass, pdesc, ptype, regtype)) hlst.append(self.buildParagraph(pclass, pdesc, ptype, regtype))
else : else :
print(' a "graphic" region') print(' a "graphic" region')
(pos, simgsrc) = self.findinDoc('img.src',start,end) (pos, simgsrc) = self.findinDoc(b'img.src',start,end)
if simgsrc: if simgsrc:
hlst.append('<div class="graphic"><img src="img/img%04d.jpg" alt="" /></div>' % int(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): class PParser(object):
def __init__(self, gd, flatxml, meta_array): def __init__(self, gd, flatxml, meta_array):
self.gd = gd self.gd = gd
self.flatdoc = flatxml.split('\n') self.flatdoc = flatxml.split(b'\n')
self.docSize = len(self.flatdoc) self.docSize = len(self.flatdoc)
self.temp = [] self.temp = []
@@ -58,11 +58,11 @@ class PParser(object):
def lineinDoc(self, pos) : def lineinDoc(self, pos) :
if (pos >= 0) and (pos < self.docSize) : if (pos >= 0) and (pos < self.docSize) :
item = self.flatdoc[pos] item = self.flatdoc[pos]
if item.find('=') >= 0: if item.find(b'=') >= 0:
(name, argres) = item.split('=',1) (name, argres) = item.split(b'=',1)
else : else :
name = item name = item
argres = '' argres = b''
return name, argres return name, argres
# find tag in doc if within pos to end inclusive # find tag in doc if within pos to end inclusive
@@ -73,13 +73,15 @@ class PParser(object):
else: else:
end = min(self.docSize, end) end = min(self.docSize, end)
foundat = -1 foundat = -1
for j in xrange(pos, end): for j in range(pos, end):
item = self.flatdoc[j] item = self.flatdoc[j]
if item.find('=') >= 0: if item.find(b'=') >= 0:
(name, argres) = item.split('=',1) (name, argres) = item.split(b'=',1)
else : else :
name = item name = item
argres = '' argres = b''
if (isinstance(tagpath,str)):
tagpath = tagpath.encode('utf-8')
if name.endswith(tagpath) : if name.endswith(tagpath) :
result = argres result = argres
foundat = j foundat = j
@@ -101,11 +103,11 @@ class PParser(object):
def getData(self, path): def getData(self, path):
result = None result = None
cnt = len(self.flatdoc) cnt = len(self.flatdoc)
for j in xrange(cnt): for j in range(cnt):
item = self.flatdoc[j] item = self.flatdoc[j]
if item.find('=') >= 0: if item.find(b'=') >= 0:
(name, argt) = item.split('=') (name, argt) = item.split(b'=')
argres = argt.split('|') argres = argt.split(b'|')
else: else:
name = item name = item
argres = [] argres = []
@@ -113,22 +115,24 @@ class PParser(object):
result = argres result = argres
break break
if (len(argres) > 0) : if (len(argres) > 0) :
for j in xrange(0,len(argres)): for j in range(0,len(argres)):
argres[j] = int(argres[j]) argres[j] = int(argres[j])
return result return result
def getDataatPos(self, path, pos): def getDataatPos(self, path, pos):
result = None result = None
item = self.flatdoc[pos] item = self.flatdoc[pos]
if item.find('=') >= 0: if item.find(b'=') >= 0:
(name, argt) = item.split('=') (name, argt) = item.split(b'=')
argres = argt.split('|') argres = argt.split(b'|')
else: else:
name = item name = item
argres = [] argres = []
if (len(argres) > 0) : if (len(argres) > 0) :
for j in xrange(0,len(argres)): for j in range(0,len(argres)):
argres[j] = int(argres[j]) argres[j] = int(argres[j])
if (isinstance(path,str)):
path = path.encode('utf-8')
if (name.endswith(path)): if (name.endswith(path)):
result = argres result = argres
return result return result
@@ -136,20 +140,22 @@ class PParser(object):
def getDataTemp(self, path): def getDataTemp(self, path):
result = None result = None
cnt = len(self.temp) cnt = len(self.temp)
for j in xrange(cnt): for j in range(cnt):
item = self.temp[j] item = self.temp[j]
if item.find('=') >= 0: if item.find(b'=') >= 0:
(name, argt) = item.split('=') (name, argt) = item.split(b'=')
argres = argt.split('|') argres = argt.split(b'|')
else: else:
name = item name = item
argres = [] argres = []
if (isinstance(path,str)):
path = path.encode('utf-8')
if (name.endswith(path)): if (name.endswith(path)):
result = argres result = argres
self.temp.pop(j) self.temp.pop(j)
break break
if (len(argres) > 0) : if (len(argres) > 0) :
for j in xrange(0,len(argres)): for j in range(0,len(argres)):
argres[j] = int(argres[j]) argres[j] = int(argres[j])
return result return result
@@ -220,15 +226,15 @@ def convert2SVG(gdict, flat_xml, pageid, previd, nextid, svgDir, raw, meta_array
if (pp.gid != None): if (pp.gid != None):
mlst.append('<defs>\n') mlst.append('<defs>\n')
gdefs = pp.getGlyphs() gdefs = pp.getGlyphs()
for j in xrange(0,len(gdefs)): for j in range(0,len(gdefs)):
mlst.append(gdefs[j]) mlst.append(gdefs[j])
mlst.append('</defs>\n') mlst.append('</defs>\n')
img = pp.getImages() img = pp.getImages()
if (img != None): if (img != None):
for j in xrange(0,len(img)): for j in range(0,len(img)):
mlst.append(img[j]) mlst.append(img[j])
if (pp.gid != None): 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])) 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): if (img == None or len(img) == 0) and (pp.gid == None or len(pp.gid) == 0):
xpos = "%d" % (pp.pw // 3) xpos = "%d" % (pp.pw // 3)

View File

@@ -1,21 +1,34 @@
#! /usr/bin/python #!/usr/bin/env python3
# -*- coding: utf-8 -*-
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab # vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
# Python 3 for calibre 5.0
from __future__ import print_function from __future__ import print_function
from .convert2xml import encodeNumber
class Unbuffered: # 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): def __init__(self, stream):
self.stream = stream self.stream = stream
self.encoding = stream.encoding
if self.encoding == None:
self.encoding = "utf-8"
def write(self, data): def write(self, data):
self.stream.write(data) if isinstance(data,str) or isinstance(data,unicode):
self.stream.flush() # 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): def __getattr__(self, attr):
return getattr(self.stream, attr) return getattr(self.stream, attr)
import sys import sys
sys.stdout=Unbuffered(sys.stdout)
import csv import csv
import os import os
import getopt import getopt
@@ -87,13 +100,13 @@ def readString(file):
def getMetaArray(metaFile): def getMetaArray(metaFile):
# parse the meta file # parse the meta file
result = {} result = {}
fo = file(metaFile,'rb') fo = open(metaFile,'rb')
size = readEncodedNumber(fo) size = readEncodedNumber(fo)
for i in xrange(size): for i in range(size):
tag = readString(fo) tag = readString(fo)
value = readString(fo) value = readString(fo)
result[tag] = value result[tag] = value
# print tag, value # print(tag, value)
fo.close() fo.close()
return result return result
@@ -103,17 +116,17 @@ class Dictionary(object):
def __init__(self, dictFile): def __init__(self, dictFile):
self.filename = dictFile self.filename = dictFile
self.size = 0 self.size = 0
self.fo = file(dictFile,'rb') self.fo = open(dictFile,'rb')
self.stable = [] self.stable = []
self.size = readEncodedNumber(self.fo) 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.stable.append(self.escapestr(readString(self.fo)))
self.pos = 0 self.pos = 0
def escapestr(self, str): def escapestr(self, str):
str = str.replace('&','&amp;') str = str.replace(b'&',b'&amp;')
str = str.replace('<','&lt;') str = str.replace(b'<',b'&lt;')
str = str.replace('>','&gt;') str = str.replace(b'>',b'&gt;')
str = str.replace('=','&#61;') str = str.replace(b'=',b'&#61;')
return str return str
def lookup(self,val): def lookup(self,val):
if ((val >= 0) and (val < self.size)) : if ((val >= 0) and (val < self.size)) :
@@ -131,7 +144,7 @@ class Dictionary(object):
class PageDimParser(object): class PageDimParser(object):
def __init__(self, flatxml): def __init__(self, flatxml):
self.flatdoc = flatxml.split('\n') self.flatdoc = flatxml.split(b'\n')
# find tag if within pos to end inclusive # find tag if within pos to end inclusive
def findinDoc(self, tagpath, pos, end) : def findinDoc(self, tagpath, pos, end) :
result = None result = None
@@ -142,10 +155,10 @@ class PageDimParser(object):
else: else:
end = min(cnt,end) end = min(cnt,end)
foundat = -1 foundat = -1
for j in xrange(pos, end): for j in range(pos, end):
item = docList[j] item = docList[j]
if item.find('=') >= 0: if item.find(b'=') >= 0:
(name, argres) = item.split('=') (name, argres) = item.split(b'=')
else : else :
name = item name = item
argres = '' argres = ''
@@ -155,8 +168,8 @@ class PageDimParser(object):
break break
return foundat, result return foundat, result
def process(self): def process(self):
(pos, sph) = self.findinDoc('page.h',0,-1) (pos, sph) = self.findinDoc(b'page.h',0,-1)
(pos, spw) = self.findinDoc('page.w',0,-1) (pos, spw) = self.findinDoc(b'page.w',0,-1)
if (sph == None): sph = '-1' if (sph == None): sph = '-1'
if (spw == None): spw = '-1' if (spw == None): spw = '-1'
return sph, spw return sph, spw
@@ -169,21 +182,21 @@ def getPageDim(flatxml):
class GParser(object): class GParser(object):
def __init__(self, flatxml): def __init__(self, flatxml):
self.flatdoc = flatxml.split('\n') self.flatdoc = flatxml.split(b'\n')
self.dpi = 1440 self.dpi = 1440
self.gh = self.getData('info.glyph.h') self.gh = self.getData(b'info.glyph.h')
self.gw = self.getData('info.glyph.w') self.gw = self.getData(b'info.glyph.w')
self.guse = self.getData('info.glyph.use') self.guse = self.getData(b'info.glyph.use')
if self.guse : if self.guse :
self.count = len(self.guse) self.count = len(self.guse)
else : else :
self.count = 0 self.count = 0
self.gvtx = self.getData('info.glyph.vtx') self.gvtx = self.getData(b'info.glyph.vtx')
self.glen = self.getData('info.glyph.len') self.glen = self.getData(b'info.glyph.len')
self.gdpi = self.getData('info.glyph.dpi') self.gdpi = self.getData(b'info.glyph.dpi')
self.vx = self.getData('info.vtx.x') self.vx = self.getData(b'info.vtx.x')
self.vy = self.getData('info.vtx.y') self.vy = self.getData(b'info.vtx.y')
self.vlen = self.getData('info.len.n') self.vlen = self.getData(b'info.len.n')
if self.vlen : if self.vlen :
self.glen.append(len(self.vlen)) self.glen.append(len(self.vlen))
elif self.glen: elif self.glen:
@@ -195,11 +208,11 @@ class GParser(object):
def getData(self, path): def getData(self, path):
result = None result = None
cnt = len(self.flatdoc) cnt = len(self.flatdoc)
for j in xrange(cnt): for j in range(cnt):
item = self.flatdoc[j] item = self.flatdoc[j]
if item.find('=') >= 0: if item.find(b'=') >= 0:
(name, argt) = item.split('=') (name, argt) = item.split(b'=')
argres = argt.split('|') argres = argt.split(b'|')
else: else:
name = item name = item
argres = [] argres = []
@@ -207,7 +220,7 @@ class GParser(object):
result = argres result = argres
break break
if (len(argres) > 0) : if (len(argres) > 0) :
for j in xrange(0,len(argres)): for j in range(0,len(argres)):
argres[j] = int(argres[j]) argres[j] = int(argres[j])
return result return result
def getGlyphDim(self, gly): def getGlyphDim(self, gly):
@@ -223,7 +236,7 @@ class GParser(object):
tx = self.vx[self.gvtx[gly]:self.gvtx[gly+1]] tx = self.vx[self.gvtx[gly]:self.gvtx[gly+1]]
ty = self.vy[self.gvtx[gly]:self.gvtx[gly+1]] ty = self.vy[self.gvtx[gly]:self.gvtx[gly+1]]
p = 0 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): if (p == 0):
zx = tx[0:self.vlen[k]+1] zx = tx[0:self.vlen[k]+1]
zy = ty[0:self.vlen[k]+1] zy = ty[0:self.vlen[k]+1]
@@ -322,17 +335,17 @@ def generateBook(bookDir, raw, fixedimage):
imgname = filename.replace('color','img') imgname = filename.replace('color','img')
sfile = os.path.join(spath,filename) sfile = os.path.join(spath,filename)
dfile = os.path.join(dpath,imgname) dfile = os.path.join(dpath,imgname)
imgdata = file(sfile,'rb').read() imgdata = open(sfile,'rb').read()
file(dfile,'wb').write(imgdata) open(dfile,'wb').write(imgdata)
print("Creating cover.jpg") print("Creating cover.jpg")
isCover = False isCover = False
cpath = os.path.join(bookDir,'img') cpath = os.path.join(bookDir,'img')
cpath = os.path.join(cpath,'img0000.jpg') cpath = os.path.join(cpath,'img0000.jpg')
if os.path.isfile(cpath): if os.path.isfile(cpath):
cover = file(cpath, 'rb').read() cover = open(cpath, 'rb').read()
cpath = os.path.join(bookDir,'cover.jpg') cpath = os.path.join(bookDir,'cover.jpg')
file(cpath, 'wb').write(cover) open(cpath, 'wb').write(cover)
isCover = True isCover = True
@@ -361,7 +374,7 @@ def generateBook(bookDir, raw, fixedimage):
mlst.append('<meta name="' + key + '" content="' + meta_array[key] + '" />\n') mlst.append('<meta name="' + key + '" content="' + meta_array[key] + '" />\n')
metastr = "".join(mlst) metastr = "".join(mlst)
mlst = None mlst = None
file(xname, 'wb').write(metastr) open(xname, 'wb').write(metastr)
print('Processing StyleSheet') print('Processing StyleSheet')
@@ -424,10 +437,10 @@ def generateBook(bookDir, raw, fixedimage):
# now get the css info # now get the css info
cssstr , classlst = stylexml2css.convert2CSS(flat_xml, fontsize, ph, pw) cssstr , classlst = stylexml2css.convert2CSS(flat_xml, fontsize, ph, pw)
file(xname, 'wb').write(cssstr) open(xname, 'w').write(cssstr)
if buildXML: if buildXML:
xname = os.path.join(xmlDir, 'other0000.xml') 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() gd = GlyphDict()
@@ -449,10 +462,10 @@ def generateBook(bookDir, raw, fixedimage):
if buildXML: if buildXML:
xname = os.path.join(xmlDir, filename.replace('.dat','.xml')) 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) gp = GParser(flat_xml)
for i in xrange(0, gp.count): for i in range(0, gp.count):
path = gp.getPath(i) path = gp.getPath(i)
maxh, maxw = gp.getGlyphDim(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) fullpath = '<path id="gl%d" d="%s" fill="black" /><!-- width=%d height=%d -->\n' % (counter * 256 + i, path, maxw, maxh)
@@ -507,7 +520,7 @@ def generateBook(bookDir, raw, fixedimage):
if buildXML: if buildXML:
xname = os.path.join(xmlDir, filename.replace('.dat','.xml')) 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 # first get the html
pagehtml, tocinfo = flatxml2html.convert2HTML(flat_xml, classlst, fname, bookDir, gd, fixedimage) pagehtml, tocinfo = flatxml2html.convert2HTML(flat_xml, classlst, fname, bookDir, gd, fixedimage)
@@ -518,7 +531,7 @@ def generateBook(bookDir, raw, fixedimage):
hlst.append('</body>\n</html>\n') hlst.append('</body>\n</html>\n')
htmlstr = "".join(hlst) htmlstr = "".join(hlst)
hlst = None hlst = None
file(os.path.join(bookDir, htmlFileName), 'wb').write(htmlstr) open(os.path.join(bookDir, htmlFileName), 'w').write(htmlstr)
print(" ") print(" ")
print('Extracting Table of Contents from Amazon OCR') print('Extracting Table of Contents from Amazon OCR')
@@ -564,7 +577,7 @@ def generateBook(bookDir, raw, fixedimage):
tlst.append('</body>\n') tlst.append('</body>\n')
tlst.append('</html>\n') tlst.append('</html>\n')
tochtml = "".join(tlst) 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 # now create index_svg.xhtml that points to all required files
@@ -601,7 +614,7 @@ def generateBook(bookDir, raw, fixedimage):
flst = [] flst = []
for page in pagelst: for page in pagelst:
flst.append(xmllst[page]) flst.append(xmllst[page])
flat_svg = "".join(flst) flat_svg = b"".join(flst)
flst=None flst=None
svgxml = flatxml2svg.convert2SVG(gd, flat_svg, pageid, previd, nextid, svgDir, raw, meta_array, scaledpi) svgxml = flatxml2svg.convert2SVG(gd, flat_svg, pageid, previd, nextid, svgDir, raw, meta_array, scaledpi)
if (raw) : if (raw) :
@@ -619,7 +632,7 @@ def generateBook(bookDir, raw, fixedimage):
slst.append('</body>\n</html>\n') slst.append('</body>\n</html>\n')
svgindex = "".join(slst) svgindex = "".join(slst)
slst = None 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(" ")
@@ -630,16 +643,16 @@ def generateBook(bookDir, raw, fixedimage):
olst.append('<package xmlns="http://www.idpf.org/2007/opf" unique-identifier="guid_id">\n') olst.append('<package xmlns="http://www.idpf.org/2007/opf" unique-identifier="guid_id">\n')
# adding metadata # adding metadata
olst.append(' <metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf">\n') 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: if b'GUID' in meta_array:
olst.append(' <dc:identifier opf:scheme="GUID" id="guid_id">' + meta_array['GUID'] + '</dc:identifier>\n') olst.append(' <dc:identifier opf:scheme="GUID" id="guid_id">' + meta_array[b'GUID'].decode('utf-8') + '</dc:identifier>\n')
if 'ASIN' in meta_array: if b'ASIN' in meta_array:
olst.append(' <dc:identifier opf:scheme="ASIN">' + meta_array['ASIN'] + '</dc:identifier>\n') olst.append(' <dc:identifier opf:scheme="ASIN">' + meta_array[b'ASIN'].decode('utf-8') + '</dc:identifier>\n')
if 'oASIN' in meta_array: if b'oASIN' in meta_array:
olst.append(' <dc:identifier opf:scheme="oASIN">' + meta_array['oASIN'] + '</dc:identifier>\n') olst.append(' <dc:identifier opf:scheme="oASIN">' + meta_array[b'oASIN'].decode('utf-8') + '</dc:identifier>\n')
olst.append(' <dc:title>' + meta_array['Title'] + '</dc:title>\n') olst.append(' <dc:title>' + meta_array[b'Title'].decode('utf-8') + '</dc:title>\n')
olst.append(' <dc:creator opf:role="aut">' + meta_array['Authors'] + '</dc:creator>\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: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: if isCover:
olst.append(' <meta name="cover" content="bookcover"/>\n') olst.append(' <meta name="cover" content="bookcover"/>\n')
olst.append(' </metadata>\n') olst.append(' </metadata>\n')
@@ -668,7 +681,7 @@ def generateBook(bookDir, raw, fixedimage):
olst.append('</package>\n') olst.append('</package>\n')
opfstr = "".join(olst) opfstr = "".join(olst)
olst = None olst = None
file(opfname, 'wb').write(opfstr) open(opfname, 'w').write(opfstr)
print('Processing Complete') print('Processing Complete')
@@ -687,6 +700,8 @@ def usage():
def main(argv): def main(argv):
sys.stdout=SafeUnbuffered(sys.stdout)
sys.stderr=SafeUnbuffered(sys.stderr)
bookDir = '' bookDir = ''
if len(argv) == 0: if len(argv) == 0:
argv = sys.argv argv = sys.argv
@@ -694,7 +709,7 @@ def main(argv):
try: try:
opts, args = getopt.getopt(argv[1:], "rh:",["fixed-image"]) opts, args = getopt.getopt(argv[1:], "rh:",["fixed-image"])
except getopt.GetoptError, err: except getopt.GetoptError as err:
print(str(err)) print(str(err))
usage() usage()
return 1 return 1

View File

@@ -1,27 +1,13 @@
#!/usr/bin/env python #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import with_statement # ignobleepub.py
# Copyright © 2009-2020 by i♥cabbages, Apprentice Harper et al.
# ignobleepub.pyw, version 4.1
# Copyright © 2009-2010 by i♥cabbages
# Released under the terms of the GNU General Public Licence, version 3 # Released under the terms of the GNU General Public Licence, version 3
# <http://www.gnu.org/licenses/> # <http://www.gnu.org/licenses/>
# Modified 20102013 by some_updates, DiapDealer and Apprentice Alf
# Modified 20152017 by Apprentice Harper
# Windows users: Before running this program, you must first install Python 2.6
# from <http://www.python.org/download/> and PyCrypto from
# <http://www.voidspace.org.uk/python/modules.shtml#pycrypto> (make sure to
# install the version for Python 2.6). Save this script file as
# ineptepub.pyw and double-click on it to run it.
# #
# Mac OS X users: Save this script file as ineptepub.pyw. You can run this
# program from the command line (pythonw ineptepub.pyw) or by double-clicking
# it when it has been associated with PythonLauncher.
# Revision history: # Revision history:
# 1 - Initial release # 1 - Initial release
# 2 - Added OS X support by using OpenSSL when available # 2 - Added OS X support by using OpenSSL when available
@@ -37,18 +23,19 @@ from __future__ import with_statement
# 3.9 - moved unicode_argv call inside main for Windows DeDRM compatibility # 3.9 - moved unicode_argv call inside main for Windows DeDRM compatibility
# 4.0 - Work if TkInter is missing # 4.0 - Work if TkInter is missing
# 4.1 - Import tkFileDialog, don't assume something else will import it. # 4.1 - Import tkFileDialog, don't assume something else will import it.
# 5.0 - Python 3 for calibre 5.0
""" """
Decrypt Barnes & Noble encrypted ePub books. Decrypt Barnes & Noble encrypted ePub books.
""" """
from __future__ import print_function
__license__ = 'GPL v3' __license__ = 'GPL v3'
__version__ = "4.1" __version__ = "5.0"
import sys import sys
import os import os
import traceback import traceback
import base64
import zlib import zlib
import zipfile import zipfile
from zipfile import ZipInfo, ZipFile, ZIP_STORED, ZIP_DEFLATED from zipfile import ZipInfo, ZipFile, ZIP_STORED, ZIP_DEFLATED
@@ -65,10 +52,17 @@ class SafeUnbuffered:
if self.encoding == None: if self.encoding == None:
self.encoding = "utf-8" self.encoding = "utf-8"
def write(self, data): def write(self, data):
if isinstance(data,unicode): if isinstance(data,str) or isinstance(data,unicode):
# str for Python3, unicode for Python2
data = data.encode(self.encoding,"replace") data = data.encode(self.encoding,"replace")
self.stream.write(data) try:
self.stream.flush() buffer = getattr(self.stream, 'buffer', self.stream)
# self.stream.buffer for Python3, self.stream for Python2
buffer.write(data)
buffer.flush()
except:
# We can do nothing if a write fails
raise
def __getattr__(self, attr): def __getattr__(self, attr):
return getattr(self.stream, attr) return getattr(self.stream, attr)
@@ -106,13 +100,11 @@ def unicode_argv():
# Remove Python executable and commands if present # Remove Python executable and commands if present
start = argc.value - len(sys.argv) start = argc.value - len(sys.argv)
return [argv[i] for i in return [argv[i] for i in
xrange(start, argc.value)] range(start, argc.value)]
return [u"ineptepub.py"] return ["ineptepub.py"]
else: else:
argvencoding = sys.stdin.encoding argvencoding = sys.stdin.encoding or "utf-8"
if argvencoding == None: return [arg if (isinstance(arg, str) or isinstance(arg,unicode)) else str(arg, argvencoding) for arg in sys.argv]
argvencoding = "utf-8"
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
class IGNOBLEError(Exception): class IGNOBLEError(Exception):
@@ -167,7 +159,7 @@ def _load_crypto_libcrypto():
def decrypt(self, data): def decrypt(self, data):
out = create_string_buffer(len(data)) out = create_string_buffer(len(data))
iv = ("\x00" * self._blocksize) iv = (b'\x00' * self._blocksize)
rv = AES_cbc_encrypt(data, out, len(data), self._key, iv, 0) rv = AES_cbc_encrypt(data, out, len(data), self._key, iv, 0)
if rv == 0: if rv == 0:
raise IGNOBLEError('AES decryption failed') raise IGNOBLEError('AES decryption failed')
@@ -180,7 +172,7 @@ def _load_crypto_pycrypto():
class AES(object): class AES(object):
def __init__(self, key): def __init__(self, key):
self._aes = _AES.new(key, _AES.MODE_CBC, '\x00'*16) self._aes = _AES.new(key, _AES.MODE_CBC, b'\x00'*16)
def decrypt(self, data): def decrypt(self, data):
return self._aes.decrypt(data) return self._aes.decrypt(data)
@@ -223,15 +215,15 @@ class Decryptor(object):
def decompress(self, bytes): def decompress(self, bytes):
dc = zlib.decompressobj(-15) dc = zlib.decompressobj(-15)
bytes = dc.decompress(bytes) bytes = dc.decompress(bytes)
ex = dc.decompress('Z') + dc.flush() ex = dc.decompress(b'Z') + dc.flush()
if ex: if ex:
bytes = bytes + ex bytes = bytes + ex
return bytes return bytes
def decrypt(self, path, data): def decrypt(self, path, data):
if path in self._encrypted: if bytes(path,'utf-8') in self._encrypted:
data = self._aes.decrypt(data)[16:] data = self._aes.decrypt(data)[16:]
data = data[:-ord(data[-1])] data = data[:-data[-1]]
data = self.decompress(data) data = self.decompress(data)
return data return data
@@ -256,14 +248,14 @@ def ignobleBook(inpath):
def decryptBook(keyb64, inpath, outpath): def decryptBook(keyb64, inpath, outpath):
if AES is None: if AES is None:
raise IGNOBLEError(u"PyCrypto or OpenSSL must be installed.") raise IGNOBLEError("PyCrypto or OpenSSL must be installed.")
key = keyb64.decode('base64')[:16] key = base64.b64decode(keyb64)[:16]
aes = AES(key) aes = AES(key)
with closing(ZipFile(open(inpath, 'rb'))) as inf: with closing(ZipFile(open(inpath, 'rb'))) as inf:
namelist = set(inf.namelist()) namelist = set(inf.namelist())
if 'META-INF/rights.xml' not in namelist or \ if 'META-INF/rights.xml' not in namelist or \
'META-INF/encryption.xml' not in namelist: 'META-INF/encryption.xml' not in namelist:
print(u"{0:s} is DRM-free.".format(os.path.basename(inpath))) print("{0:s} is DRM-free.".format(os.path.basename(inpath)))
return 1 return 1
for name in META_NAMES: for name in META_NAMES:
namelist.remove(name) namelist.remove(name)
@@ -273,10 +265,10 @@ def decryptBook(keyb64, inpath, outpath):
expr = './/%s' % (adept('encryptedKey'),) expr = './/%s' % (adept('encryptedKey'),)
bookkey = ''.join(rights.findtext(expr)) bookkey = ''.join(rights.findtext(expr))
if len(bookkey) != 64: if len(bookkey) != 64:
print(u"{0:s} is not a secure Barnes & Noble ePub.".format(os.path.basename(inpath))) print("{0:s} is not a secure Barnes & Noble ePub.".format(os.path.basename(inpath)))
return 1 return 1
bookkey = aes.decrypt(bookkey.decode('base64')) bookkey = aes.decrypt(base64.b64decode(bookkey))
bookkey = bookkey[:-ord(bookkey[-1])] bookkey = bookkey[:-bookkey[-1]]
encryption = inf.read('META-INF/encryption.xml') encryption = inf.read('META-INF/encryption.xml')
decryptor = Decryptor(bookkey[-16:], encryption) decryptor = Decryptor(bookkey[-16:], encryption)
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False) kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
@@ -316,7 +308,7 @@ def decryptBook(keyb64, inpath, outpath):
pass pass
outf.writestr(zi, decryptor.decrypt(path, data)) outf.writestr(zi, decryptor.decrypt(path, data))
except: except:
print(u"Could not decrypt {0:s} because of an exception:\n{1:s}".format(os.path.basename(inpath), traceback.format_exc())) print("Could not decrypt {0:s} because of an exception:\n{1:s}".format(os.path.basename(inpath), traceback.format_exc()))
return 2 return 2
return 0 return 0
@@ -327,90 +319,90 @@ def cli_main():
argv=unicode_argv() argv=unicode_argv()
progname = os.path.basename(argv[0]) progname = os.path.basename(argv[0])
if len(argv) != 4: if len(argv) != 4:
print(u"usage: {0} <keyfile.b64> <inbook.epub> <outbook.epub>".format(progname)) print("usage: {0} <keyfile.b64> <inbook.epub> <outbook.epub>".format(progname))
return 1 return 1
keypath, inpath, outpath = argv[1:] keypath, inpath, outpath = argv[1:]
userkey = open(keypath,'rb').read() userkey = open(keypath,'rb').read()
result = decryptBook(userkey, inpath, outpath) result = decryptBook(userkey, inpath, outpath)
if result == 0: if result == 0:
print(u"Successfully decrypted {0:s} as {1:s}".format(os.path.basename(inpath),os.path.basename(outpath))) print("Successfully decrypted {0:s} as {1:s}".format(os.path.basename(inpath),os.path.basename(outpath)))
return result return result
def gui_main(): def gui_main():
try: try:
import Tkinter import tkinter
import Tkconstants import tkinter.constants
import tkFileDialog import tkinter.filedialog
import tkMessageBox import tkinter.messagebox
import traceback import traceback
except: except:
return cli_main() return cli_main()
class DecryptionDialog(Tkinter.Frame): class DecryptionDialog(tkinter.Frame):
def __init__(self, root): def __init__(self, root):
Tkinter.Frame.__init__(self, root, border=5) tkinter.Frame.__init__(self, root, border=5)
self.status = Tkinter.Label(self, text=u"Select files for decryption") self.status = tkinter.Label(self, text="Select files for decryption")
self.status.pack(fill=Tkconstants.X, expand=1) self.status.pack(fill=tkinter.constants.X, expand=1)
body = Tkinter.Frame(self) body = tkinter.Frame(self)
body.pack(fill=Tkconstants.X, expand=1) body.pack(fill=tkinter.constants.X, expand=1)
sticky = Tkconstants.E + Tkconstants.W sticky = tkinter.constants.E + tkinter.constants.W
body.grid_columnconfigure(1, weight=2) body.grid_columnconfigure(1, weight=2)
Tkinter.Label(body, text=u"Key file").grid(row=0) tkinter.Label(body, text="Key file").grid(row=0)
self.keypath = Tkinter.Entry(body, width=30) self.keypath = tkinter.Entry(body, width=30)
self.keypath.grid(row=0, column=1, sticky=sticky) self.keypath.grid(row=0, column=1, sticky=sticky)
if os.path.exists(u"bnepubkey.b64"): if os.path.exists("bnepubkey.b64"):
self.keypath.insert(0, u"bnepubkey.b64") self.keypath.insert(0, "bnepubkey.b64")
button = Tkinter.Button(body, text=u"...", command=self.get_keypath) button = tkinter.Button(body, text="...", command=self.get_keypath)
button.grid(row=0, column=2) button.grid(row=0, column=2)
Tkinter.Label(body, text=u"Input file").grid(row=1) tkinter.Label(body, text="Input file").grid(row=1)
self.inpath = Tkinter.Entry(body, width=30) self.inpath = tkinter.Entry(body, width=30)
self.inpath.grid(row=1, column=1, sticky=sticky) self.inpath.grid(row=1, column=1, sticky=sticky)
button = Tkinter.Button(body, text=u"...", command=self.get_inpath) button = tkinter.Button(body, text="...", command=self.get_inpath)
button.grid(row=1, column=2) button.grid(row=1, column=2)
Tkinter.Label(body, text=u"Output file").grid(row=2) tkinter.Label(body, text="Output file").grid(row=2)
self.outpath = Tkinter.Entry(body, width=30) self.outpath = tkinter.Entry(body, width=30)
self.outpath.grid(row=2, column=1, sticky=sticky) self.outpath.grid(row=2, column=1, sticky=sticky)
button = Tkinter.Button(body, text=u"...", command=self.get_outpath) button = tkinter.Button(body, text="...", command=self.get_outpath)
button.grid(row=2, column=2) button.grid(row=2, column=2)
buttons = Tkinter.Frame(self) buttons = tkinter.Frame(self)
buttons.pack() buttons.pack()
botton = Tkinter.Button( botton = tkinter.Button(
buttons, text=u"Decrypt", width=10, command=self.decrypt) buttons, text="Decrypt", width=10, command=self.decrypt)
botton.pack(side=Tkconstants.LEFT) botton.pack(side=tkinter.constants.LEFT)
Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT) tkinter.Frame(buttons, width=10).pack(side=tkinter.constants.LEFT)
button = Tkinter.Button( button = tkinter.Button(
buttons, text=u"Quit", width=10, command=self.quit) buttons, text="Quit", width=10, command=self.quit)
button.pack(side=Tkconstants.RIGHT) button.pack(side=tkinter.constants.RIGHT)
def get_keypath(self): def get_keypath(self):
keypath = tkFileDialog.askopenfilename( keypath = tkinter.filedialog.askopenfilename(
parent=None, title=u"Select Barnes & Noble \'.b64\' key file", parent=None, title="Select Barnes & Noble \'.b64\' key file",
defaultextension=u".b64", defaultextension=".b64",
filetypes=[('base64-encoded files', '.b64'), filetypes=[('base64-encoded files', '.b64'),
('All Files', '.*')]) ('All Files', '.*')])
if keypath: if keypath:
keypath = os.path.normpath(keypath) keypath = os.path.normpath(keypath)
self.keypath.delete(0, Tkconstants.END) self.keypath.delete(0, tkinter.constants.END)
self.keypath.insert(0, keypath) self.keypath.insert(0, keypath)
return return
def get_inpath(self): def get_inpath(self):
inpath = tkFileDialog.askopenfilename( inpath = tkinter.filedialog.askopenfilename(
parent=None, title=u"Select B&N-encrypted ePub file to decrypt", parent=None, title="Select B&N-encrypted ePub file to decrypt",
defaultextension=u".epub", filetypes=[('ePub files', '.epub')]) defaultextension=".epub", filetypes=[('ePub files', '.epub')])
if inpath: if inpath:
inpath = os.path.normpath(inpath) inpath = os.path.normpath(inpath)
self.inpath.delete(0, Tkconstants.END) self.inpath.delete(0, tkinter.constants.END)
self.inpath.insert(0, inpath) self.inpath.insert(0, inpath)
return return
def get_outpath(self): def get_outpath(self):
outpath = tkFileDialog.asksaveasfilename( outpath = tkinter.filedialog.asksaveasfilename(
parent=None, title=u"Select unencrypted ePub file to produce", parent=None, title="Select unencrypted ePub file to produce",
defaultextension=u".epub", filetypes=[('ePub files', '.epub')]) defaultextension=".epub", filetypes=[('ePub files', '.epub')])
if outpath: if outpath:
outpath = os.path.normpath(outpath) outpath = os.path.normpath(outpath)
self.outpath.delete(0, Tkconstants.END) self.outpath.delete(0, tkinter.constants.END)
self.outpath.insert(0, outpath) self.outpath.insert(0, outpath)
return return
@@ -419,34 +411,34 @@ def gui_main():
inpath = self.inpath.get() inpath = self.inpath.get()
outpath = self.outpath.get() outpath = self.outpath.get()
if not keypath or not os.path.exists(keypath): if not keypath or not os.path.exists(keypath):
self.status['text'] = u"Specified key file does not exist" self.status['text'] = "Specified key file does not exist"
return return
if not inpath or not os.path.exists(inpath): if not inpath or not os.path.exists(inpath):
self.status['text'] = u"Specified input file does not exist" self.status['text'] = "Specified input file does not exist"
return return
if not outpath: if not outpath:
self.status['text'] = u"Output file not specified" self.status['text'] = "Output file not specified"
return return
if inpath == outpath: if inpath == outpath:
self.status['text'] = u"Must have different input and output files" self.status['text'] = "Must have different input and output files"
return return
userkey = open(keypath,'rb').read() userkey = open(keypath,'rb').read()
self.status['text'] = u"Decrypting..." self.status['text'] = "Decrypting..."
try: try:
decrypt_status = decryptBook(userkey, inpath, outpath) decrypt_status = decryptBook(userkey, inpath, outpath)
except Exception, e: except Exception as e:
self.status['text'] = u"Error: {0}".format(e.args[0]) self.status['text'] = "Error: {0}".format(e.args[0])
return return
if decrypt_status == 0: if decrypt_status == 0:
self.status['text'] = u"File successfully decrypted" self.status['text'] = "File successfully decrypted"
else: else:
self.status['text'] = u"The was an error decrypting the file." self.status['text'] = "The was an error decrypting the file."
root = Tkinter.Tk() root = tkinter.Tk()
root.title(u"Barnes & Noble ePub Decrypter v.{0}".format(__version__)) root.title("Barnes & Noble ePub Decrypter v.{0}".format(__version__))
root.resizable(True, False) root.resizable(True, False)
root.minsize(300, 0) root.minsize(300, 0)
DecryptionDialog(root).pack(fill=Tkconstants.X, expand=1) DecryptionDialog(root).pack(fill=tkinter.constants.X, expand=1)
root.mainloop() root.mainloop()
return 0 return 0

View File

@@ -1,10 +1,8 @@
#!/usr/bin/env python #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import with_statement
# ignoblekey.py # ignoblekey.py
# Copyright © 2015 Apprentice Alf and Apprentice Harper # Copyright © 2015-2020 Apprentice Alf, Apprentice Harper et al.
# Based on kindlekey.py, Copyright © 2010-2013 by some_updates and Apprentice Alf # Based on kindlekey.py, Copyright © 2010-2013 by some_updates and Apprentice Alf
@@ -14,14 +12,14 @@ from __future__ import with_statement
# Revision history: # Revision history:
# 1.0 - Initial release # 1.0 - Initial release
# 1.1 - remove duplicates and return last key as single key # 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 Get Barnes & Noble EPUB user key from nook Studio log file
""" """
from __future__ import print_function
__license__ = 'GPL v3' __license__ = 'GPL v3'
__version__ = "1.1" __version__ = "2.0"
import sys import sys
import os import os
@@ -39,10 +37,17 @@ class SafeUnbuffered:
if self.encoding == None: if self.encoding == None:
self.encoding = "utf-8" self.encoding = "utf-8"
def write(self, data): def write(self, data):
if isinstance(data,unicode): if isinstance(data,str) or isinstance(data,unicode):
# str for Python3, unicode for Python2
data = data.encode(self.encoding,"replace") data = data.encode(self.encoding,"replace")
self.stream.write(data) try:
self.stream.flush() buffer = getattr(self.stream, 'buffer', self.stream)
# self.stream.buffer for Python3, self.stream for Python2
buffer.write(data)
buffer.flush()
except:
# We can do nothing if a write fails
raise
def __getattr__(self, attr): def __getattr__(self, attr):
return getattr(self.stream, attr) return getattr(self.stream, attr)
@@ -80,15 +85,13 @@ def unicode_argv():
# Remove Python executable and commands if present # Remove Python executable and commands if present
start = argc.value - len(sys.argv) start = argc.value - len(sys.argv)
return [argv[i] for i in return [argv[i] for i in
xrange(start, argc.value)] range(start, argc.value)]
# if we don't have any arguments at all, just pass back script name # if we don't have any arguments at all, just pass back script name
# this should never happen # this should never happen
return [u"ignoblekey.py"] return ["ignoblekey.py"]
else: else:
argvencoding = sys.stdin.encoding argvencoding = sys.stdin.encoding or "utf-8"
if argvencoding == None: return [arg if (isinstance(arg, str) or isinstance(arg,unicode)) else str(arg, argvencoding) for arg in sys.argv]
argvencoding = "utf-8"
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
class DrmException(Exception): class DrmException(Exception):
pass pass
@@ -98,22 +101,25 @@ def getNookLogFiles():
logFiles = [] logFiles = []
found = False found = False
if iswindows: 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 # some 64 bit machines do not have the proper registry key for some reason
# or the python interface to the 32 vs 64 bit registry is broken # or the python interface to the 32 vs 64 bit registry is broken
paths = set() paths = set()
if 'LOCALAPPDATA' in os.environ.keys(): if 'LOCALAPPDATA' in os.environ.keys():
# Python 2.x does not return unicode env. Use Python 3.x # Python 2.x does not return unicode env. Use Python 3.x
path = winreg.ExpandEnvironmentStrings(u"%LOCALAPPDATA%") path = winreg.ExpandEnvironmentStrings("%LOCALAPPDATA%")
if os.path.isdir(path): if os.path.isdir(path):
paths.add(path) paths.add(path)
if 'USERPROFILE' in os.environ.keys(): if 'USERPROFILE' in os.environ.keys():
# Python 2.x does not return unicode env. Use Python 3.x # Python 2.x does not return unicode env. Use Python 3.x
path = winreg.ExpandEnvironmentStrings(u"%USERPROFILE%")+u"\\AppData\\Local" path = winreg.ExpandEnvironmentStrings("%USERPROFILE%")+"\\AppData\\Local"
if os.path.isdir(path): if os.path.isdir(path):
paths.add(path) paths.add(path)
path = winreg.ExpandEnvironmentStrings(u"%USERPROFILE%")+u"\\AppData\\Roaming" path = winreg.ExpandEnvironmentStrings("%USERPROFILE%")+"\\AppData\\Roaming"
if os.path.isdir(path): if os.path.isdir(path):
paths.add(path) paths.add(path)
# User Shell Folders show take precedent over Shell Folders if present # User Shell Folders show take precedent over Shell Folders if present
@@ -199,7 +205,7 @@ def nookkeys(files = []):
for file in files: for file in files:
fileKeys = getKeysFromLog(file) fileKeys = getKeysFromLog(file)
if fileKeys: 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)))
keys.extend(fileKeys) keys.extend(fileKeys)
return list(set(keys)) return list(set(keys))
@@ -210,29 +216,29 @@ def getkey(outpath, files=[]):
if len(keys) > 0: if len(keys) > 0:
if not os.path.isdir(outpath): if not os.path.isdir(outpath):
outfile = outpath outfile = outpath
with file(outfile, 'w') as keyfileout: with open(outfile, 'w') as keyfileout:
keyfileout.write(keys[-1]) keyfileout.write(keys[-1])
print(u"Saved a key to {0}".format(outfile)) print("Saved a key to {0}".format(outfile))
else: else:
keycount = 0 keycount = 0
for key in keys: for key in keys:
while True: while True:
keycount += 1 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): if not os.path.exists(outfile):
break break
with file(outfile, 'w') as keyfileout: with open(outfile, 'w') as keyfileout:
keyfileout.write(key) keyfileout.write(key)
print(u"Saved a key to {0}".format(outfile)) print("Saved a key to {0}".format(outfile))
return True return True
return False return False
def usage(progname): def usage(progname):
print(u"Finds the nook Study encryption keys.") print("Finds the nook Study encryption keys.")
print(u"Keys are saved to the current directory, or a specified output directory.") print("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("If a file name is passed instead of a directory, only the first key is saved, in that file.")
print(u"Usage:") print("Usage:")
print(u" {0:s} [-h] [-k <logFile>] [<outpath>]".format(progname)) print(" {0:s} [-h] [-k <logFile>] [<outpath>]".format(progname))
def cli_main(): def cli_main():
@@ -240,12 +246,12 @@ def cli_main():
sys.stderr=SafeUnbuffered(sys.stderr) sys.stderr=SafeUnbuffered(sys.stderr)
argv=unicode_argv() argv=unicode_argv()
progname = os.path.basename(argv[0]) 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: try:
opts, args = getopt.getopt(argv[1:], "hk:") opts, args = getopt.getopt(argv[1:], "hk:")
except getopt.GetoptError, err: except getopt.GetoptError as err:
print(u"Error in options or arguments: {0}".format(err.args[0])) print("Error in options or arguments: {0}".format(err.args[0]))
usage(progname) usage(progname)
sys.exit(2) sys.exit(2)
@@ -274,33 +280,33 @@ def cli_main():
outpath = os.path.realpath(os.path.normpath(outpath)) outpath = os.path.realpath(os.path.normpath(outpath))
if not getkey(outpath, files): if not getkey(outpath, files):
print(u"Could not retrieve nook Study key.") print("Could not retrieve nook Study key.")
return 0 return 0
def gui_main(): def gui_main():
try: try:
import Tkinter import tkinter
import Tkconstants import tkinter.constants
import tkMessageBox import tkinter.messagebox
import traceback import traceback
except: except:
return cli_main() return cli_main()
class ExceptionDialog(Tkinter.Frame): class ExceptionDialog(tkinter.Frame):
def __init__(self, root, text): def __init__(self, root, text):
Tkinter.Frame.__init__(self, root, border=5) tkinter.Frame.__init__(self, root, border=5)
label = Tkinter.Label(self, text=u"Unexpected error:", label = tkinter.Label(self, text="Unexpected error:",
anchor=Tkconstants.W, justify=Tkconstants.LEFT) anchor=tkinter.constants.W, justify=tkinter.constants.LEFT)
label.pack(fill=Tkconstants.X, expand=0) label.pack(fill=tkinter.constants.X, expand=0)
self.text = Tkinter.Text(self) self.text = tkinter.Text(self)
self.text.pack(fill=Tkconstants.BOTH, expand=1) 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() argv=unicode_argv()
root = Tkinter.Tk() root = tkinter.Tk()
root.withdraw() root.withdraw()
progpath, progname = os.path.split(argv[0]) progpath, progname = os.path.split(argv[0])
success = False success = False
@@ -311,21 +317,21 @@ def gui_main():
print(key) print(key)
while True: while True:
keycount += 1 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): if not os.path.exists(outfile):
break break
with file(outfile, 'w') as keyfileout: with open(outfile, 'w') as keyfileout:
keyfileout.write(key) keyfileout.write(key)
success = True success = True
tkMessageBox.showinfo(progname, u"Key successfully retrieved to {0}".format(outfile)) tkinter.messagebox.showinfo(progname, "Key successfully retrieved to {0}".format(outfile))
except DrmException, e: except DrmException as e:
tkMessageBox.showerror(progname, u"Error: {0}".format(str(e))) tkinter.messagebox.showerror(progname, "Error: {0}".format(str(e)))
except Exception: except Exception:
root.wm_state('normal') root.wm_state('normal')
root.title(progname) root.title(progname)
text = traceback.format_exc() 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() root.mainloop()
if not success: if not success:
return 1 return 1

View File

@@ -1,10 +1,8 @@
#!/usr/bin/env python #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import with_statement # ignoblekeyfetch.py
# Copyright © 2015-2020 Apprentice Harper et al.
# ignoblekeyfetch.pyw, version 1.1
# Copyright © 2015 Apprentice Harper
# Released under the terms of the GNU General Public Licence, version 3 # Released under the terms of the GNU General Public Licence, version 3
# <http://www.gnu.org/licenses/> # <http://www.gnu.org/licenses/>
@@ -24,14 +22,14 @@ from __future__ import with_statement
# Revision history: # Revision history:
# 1.0 - Initial version # 1.0 - Initial version
# 1.1 - Try second URL if first one fails # 1.1 - Try second URL if first one fails
# 2.0 - Python 3 for calibre 5.0
""" """
Fetch Barnes & Noble EPUB user key from B&N servers using email and password Fetch Barnes & Noble EPUB user key from B&N servers using email and password
""" """
from __future__ import print_function
__license__ = 'GPL v3' __license__ = 'GPL v3'
__version__ = "1.1" __version__ = "2.0"
import sys import sys
import os import os
@@ -46,10 +44,17 @@ class SafeUnbuffered:
if self.encoding == None: if self.encoding == None:
self.encoding = "utf-8" self.encoding = "utf-8"
def write(self, data): def write(self, data):
if isinstance(data,unicode): if isinstance(data,str) or isinstance(data,unicode):
# str for Python3, unicode for Python2
data = data.encode(self.encoding,"replace") data = data.encode(self.encoding,"replace")
self.stream.write(data) try:
self.stream.flush() buffer = getattr(self.stream, 'buffer', self.stream)
# self.stream.buffer for Python3, self.stream for Python2
buffer.write(data)
buffer.flush()
except:
# We can do nothing if a write fails
raise
def __getattr__(self, attr): def __getattr__(self, attr):
return getattr(self.stream, attr) return getattr(self.stream, attr)
@@ -87,15 +92,13 @@ def unicode_argv():
# Remove Python executable and commands if present # Remove Python executable and commands if present
start = argc.value - len(sys.argv) start = argc.value - len(sys.argv)
return [argv[i] for i in return [argv[i] for i in
xrange(start, argc.value)] range(start, argc.value)]
# if we don't have any arguments at all, just pass back script name # if we don't have any arguments at all, just pass back script name
# this should never happen # this should never happen
return [u"ignoblekeyfetch.py"] return ["ignoblekeyfetch.py"]
else: else:
argvencoding = sys.stdin.encoding argvencoding = sys.stdin.encoding or "utf-8"
if argvencoding == None: return [arg if (isinstance(arg, str) or isinstance(arg,unicode)) else str(arg, argvencoding) for arg in sys.argv]
argvencoding = "utf-8"
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
class IGNOBLEError(Exception): class IGNOBLEError(Exception):
@@ -103,26 +106,25 @@ class IGNOBLEError(Exception):
def fetch_key(email, password): def fetch_key(email, password):
# change email and password to utf-8 if unicode # change email and password to utf-8 if unicode
if type(email)==unicode: if type(email)==str:
email = email.encode('utf-8') email = email.encode('utf-8')
if type(password)==unicode: if type(password)==str:
password = password.encode('utf-8') password = password.encode('utf-8')
import random import random
random = "%030x" % random.randrange(16**30) random = "%030x" % random.randrange(16**30)
import urllib, urllib2, re import urllib.parse, urllib.request, re
# try the URL from nook for PC # try the URL from nook for PC
fetch_url = "https://cart4.barnesandnoble.com/services/service.aspx?Version=2&acctPassword=" fetch_url = "https://cart4.barnesandnoble.com/services/service.aspx?Version=2&acctPassword="
fetch_url += urllib.quote(password,'')+"&devID=PC_BN_2.5.6.9575_"+random+"&emailAddress=" fetch_url += urllib.parse.quote(password,'')+"&devID=PC_BN_2.5.6.9575_"+random+"&emailAddress="
fetch_url += urllib.quote(email,"")+"&outFormat=5&schema=1&service=1&stage=deviceHashB" fetch_url += urllib.parse.quote(email,"")+"&outFormat=5&schema=1&service=1&stage=deviceHashB"
#print fetch_url #print fetch_url
found = '' found = ''
try: try:
req = urllib2.Request(fetch_url) response = urllib.request.urlopen(fetch_url)
response = urllib2.urlopen(req)
the_page = response.read() the_page = response.read()
#print the_page #print the_page
found = re.search('ccHash>(.+?)</ccHash', the_page).group(1) found = re.search('ccHash>(.+?)</ccHash', the_page).group(1)
@@ -131,14 +133,13 @@ def fetch_key(email, password):
if len(found)!=28: if len(found)!=28:
# try the URL from android devices # try the URL from android devices
fetch_url = "https://cart4.barnesandnoble.com/services/service.aspx?Version=2&acctPassword=" fetch_url = "https://cart4.barnesandnoble.com/services/service.aspx?Version=2&acctPassword="
fetch_url += urllib.quote(password,'')+"&devID=hobbes_9.3.50818_"+random+"&emailAddress=" fetch_url += urllib.parse.quote(password,'')+"&devID=hobbes_9.3.50818_"+random+"&emailAddress="
fetch_url += urllib.quote(email,"")+"&outFormat=5&schema=1&service=1&stage=deviceHashB" fetch_url += urllib.parse.quote(email,"")+"&outFormat=5&schema=1&service=1&stage=deviceHashB"
#print fetch_url #print fetch_url
found = '' found = ''
try: try:
req = urllib2.Request(fetch_url) response = urllib.request.urlopen(fetch_url)
response = urllib2.urlopen(req)
the_page = response.read() the_page = response.read()
#print the_page #print the_page
found = re.search('ccHash>(.+?)</ccHash', the_page).group(1) found = re.search('ccHash>(.+?)</ccHash', the_page).group(1)
@@ -156,67 +157,67 @@ def cli_main():
argv=unicode_argv() argv=unicode_argv()
progname = os.path.basename(argv[0]) progname = os.path.basename(argv[0])
if len(argv) != 4: if len(argv) != 4:
print(u"usage: {0} <email> <password> <keyfileout.b64>".format(progname)) print("usage: {0} <email> <password> <keyfileout.b64>".format(progname))
return 1 return 1
email, password, keypath = argv[1:] email, password, keypath = argv[1:]
userkey = fetch_key(email, password) userkey = fetch_key(email, password)
if len(userkey) == 28: if len(userkey) == 28:
open(keypath,'wb').write(userkey) open(keypath,'wb').write(userkey)
return 0 return 0
print(u"Failed to fetch key.") print("Failed to fetch key.")
return 1 return 1
def gui_main(): def gui_main():
try: try:
import Tkinter import tkinter
import tkFileDialog import tkinter.filedialog
import Tkconstants import tkinter.constants
import tkMessageBox import tkinter.messagebox
import traceback import traceback
except: except:
return cli_main() return cli_main()
class DecryptionDialog(Tkinter.Frame): class DecryptionDialog(tkinter.Frame):
def __init__(self, root): def __init__(self, root):
Tkinter.Frame.__init__(self, root, border=5) tkinter.Frame.__init__(self, root, border=5)
self.status = Tkinter.Label(self, text=u"Enter parameters") self.status = tkinter.Label(self, text="Enter parameters")
self.status.pack(fill=Tkconstants.X, expand=1) self.status.pack(fill=tkinter.constants.X, expand=1)
body = Tkinter.Frame(self) body = tkinter.Frame(self)
body.pack(fill=Tkconstants.X, expand=1) body.pack(fill=tkinter.constants.X, expand=1)
sticky = Tkconstants.E + Tkconstants.W sticky = tkinter.constants.E + tkinter.constants.W
body.grid_columnconfigure(1, weight=2) body.grid_columnconfigure(1, weight=2)
Tkinter.Label(body, text=u"Account email address").grid(row=0) tkinter.Label(body, text="Account email address").grid(row=0)
self.name = Tkinter.Entry(body, width=40) self.name = tkinter.Entry(body, width=40)
self.name.grid(row=0, column=1, sticky=sticky) self.name.grid(row=0, column=1, sticky=sticky)
Tkinter.Label(body, text=u"Account password").grid(row=1) tkinter.Label(body, text="Account password").grid(row=1)
self.ccn = Tkinter.Entry(body, width=40) self.ccn = tkinter.Entry(body, width=40)
self.ccn.grid(row=1, column=1, sticky=sticky) self.ccn.grid(row=1, column=1, sticky=sticky)
Tkinter.Label(body, text=u"Output file").grid(row=2) tkinter.Label(body, text="Output file").grid(row=2)
self.keypath = Tkinter.Entry(body, width=40) self.keypath = tkinter.Entry(body, width=40)
self.keypath.grid(row=2, column=1, sticky=sticky) self.keypath.grid(row=2, column=1, sticky=sticky)
self.keypath.insert(2, u"bnepubkey.b64") self.keypath.insert(2, "bnepubkey.b64")
button = Tkinter.Button(body, text=u"...", command=self.get_keypath) button = tkinter.Button(body, text="...", command=self.get_keypath)
button.grid(row=2, column=2) button.grid(row=2, column=2)
buttons = Tkinter.Frame(self) buttons = tkinter.Frame(self)
buttons.pack() buttons.pack()
botton = Tkinter.Button( botton = tkinter.Button(
buttons, text=u"Fetch", width=10, command=self.generate) buttons, text="Fetch", width=10, command=self.generate)
botton.pack(side=Tkconstants.LEFT) botton.pack(side=tkinter.constants.LEFT)
Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT) tkinter.Frame(buttons, width=10).pack(side=tkinter.constants.LEFT)
button = Tkinter.Button( button = tkinter.Button(
buttons, text=u"Quit", width=10, command=self.quit) buttons, text="Quit", width=10, command=self.quit)
button.pack(side=Tkconstants.RIGHT) button.pack(side=tkinter.constants.RIGHT)
def get_keypath(self): def get_keypath(self):
keypath = tkFileDialog.asksaveasfilename( keypath = tkinter.filedialog.asksaveasfilename(
parent=None, title=u"Select B&N ePub key file to produce", parent=None, title="Select B&N ePub key file to produce",
defaultextension=u".b64", defaultextension=".b64",
filetypes=[('base64-encoded files', '.b64'), filetypes=[('base64-encoded files', '.b64'),
('All Files', '.*')]) ('All Files', '.*')])
if keypath: if keypath:
keypath = os.path.normpath(keypath) keypath = os.path.normpath(keypath)
self.keypath.delete(0, Tkconstants.END) self.keypath.delete(0, tkinter.constants.END)
self.keypath.insert(0, keypath) self.keypath.insert(0, keypath)
return return
@@ -225,31 +226,31 @@ def gui_main():
password = self.ccn.get() password = self.ccn.get()
keypath = self.keypath.get() keypath = self.keypath.get()
if not email: if not email:
self.status['text'] = u"Email address not given" self.status['text'] = "Email address not given"
return return
if not password: if not password:
self.status['text'] = u"Account password not given" self.status['text'] = "Account password not given"
return return
if not keypath: if not keypath:
self.status['text'] = u"Output keyfile path not set" self.status['text'] = "Output keyfile path not set"
return return
self.status['text'] = u"Fetching..." self.status['text'] = "Fetching..."
try: try:
userkey = fetch_key(email, password) userkey = fetch_key(email, password)
except Exception, e: except Exception as e:
self.status['text'] = u"Error: {0}".format(e.args[0]) self.status['text'] = "Error: {0}".format(e.args[0])
return return
if len(userkey) == 28: if len(userkey) == 28:
open(keypath,'wb').write(userkey) open(keypath,'wb').write(userkey)
self.status['text'] = u"Keyfile fetched successfully" self.status['text'] = "Keyfile fetched successfully"
else: else:
self.status['text'] = u"Keyfile fetch failed." self.status['text'] = "Keyfile fetch failed."
root = Tkinter.Tk() root = tkinter.Tk()
root.title(u"Barnes & Noble ePub Keyfile Fetch v.{0}".format(__version__)) root.title("Barnes & Noble ePub Keyfile Fetch v.{0}".format(__version__))
root.resizable(True, False) root.resizable(True, False)
root.minsize(300, 0) root.minsize(300, 0)
DecryptionDialog(root).pack(fill=Tkconstants.X, expand=1) DecryptionDialog(root).pack(fill=tkinter.constants.X, expand=1)
root.mainloop() root.mainloop()
return 0 return 0

View File

@@ -1,16 +1,12 @@
#!/usr/bin/env python #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import with_statement # ignoblekeygen.py
# Copyright © 2009-2020 i♥cabbages, Apprentice Harper et al.
# ignoblekeygen.pyw, version 2.5
# Copyright © 2009-2010 i♥cabbages
# Released under the terms of the GNU General Public Licence, version 3 # Released under the terms of the GNU General Public Licence, version 3
# <http://www.gnu.org/licenses/> # <http://www.gnu.org/licenses/>
# Modified 20102013 by some_updates, DiapDealer and Apprentice Alf
# Windows users: Before running this program, you must first install Python. # Windows users: Before running this program, you must first install Python.
# We recommend ActiveState Python 2.7.X for Windows (x86) from # We recommend ActiveState Python 2.7.X for Windows (x86) from
# http://www.activestate.com/activepython/downloads. # http://www.activestate.com/activepython/downloads.
@@ -34,18 +30,19 @@ from __future__ import with_statement
# 2.6 - moved unicode_argv call inside main for Windows DeDRM compatibility # 2.6 - moved unicode_argv call inside main for Windows DeDRM compatibility
# 2.7 - Work if TkInter is missing # 2.7 - Work if TkInter is missing
# 2.8 - Fix bug in stand-alone use (import tkFileDialog) # 2.8 - Fix bug in stand-alone use (import tkFileDialog)
# 3.0 - Added Python 3 compatibility for calibre 5.0
""" """
Generate Barnes & Noble EPUB user key from name and credit card number. Generate Barnes & Noble EPUB user key from name and credit card number.
""" """
from __future__ import print_function
__license__ = 'GPL v3' __license__ = 'GPL v3'
__version__ = "2.8" __version__ = "3.0"
import sys import sys
import os import os
import hashlib import hashlib
import base64
# Wrap a stream so that output gets flushed immediately # Wrap a stream so that output gets flushed immediately
# and also make sure that any unicode strings get # and also make sure that any unicode strings get
@@ -57,10 +54,17 @@ class SafeUnbuffered:
if self.encoding == None: if self.encoding == None:
self.encoding = "utf-8" self.encoding = "utf-8"
def write(self, data): def write(self, data):
if isinstance(data,unicode): if isinstance(data,str) or isinstance(data,unicode):
# str for Python3, unicode for Python2
data = data.encode(self.encoding,"replace") data = data.encode(self.encoding,"replace")
self.stream.write(data) try:
self.stream.flush() buffer = getattr(self.stream, 'buffer', self.stream)
# self.stream.buffer for Python3, self.stream for Python2
buffer.write(data)
buffer.flush()
except:
# We can do nothing if a write fails
raise
def __getattr__(self, attr): def __getattr__(self, attr):
return getattr(self.stream, attr) return getattr(self.stream, attr)
@@ -98,15 +102,13 @@ def unicode_argv():
# Remove Python executable and commands if present # Remove Python executable and commands if present
start = argc.value - len(sys.argv) start = argc.value - len(sys.argv)
return [argv[i] for i in return [argv[i] for i in
xrange(start, argc.value)] range(start, argc.value)]
# if we don't have any arguments at all, just pass back script name # if we don't have any arguments at all, just pass back script name
# this should never happen # this should never happen
return [u"ignoblekeygen.py"] return ["ignoblekeygen.py"]
else: else:
argvencoding = sys.stdin.encoding argvencoding = sys.stdin.encoding or "utf-8"
if argvencoding == None: return [arg if (isinstance(arg, str) or isinstance(arg,unicode)) else str(arg, argvencoding) for arg in sys.argv]
argvencoding = "utf-8"
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
class IGNOBLEError(Exception): class IGNOBLEError(Exception):
@@ -199,23 +201,24 @@ def normalize_name(name):
def generate_key(name, ccn): def generate_key(name, ccn):
# remove spaces and case from name and CC numbers. # remove spaces and case from name and CC numbers.
if type(name)==unicode: name = normalize_name(name)
ccn = normalize_name(ccn)
if type(name)==str:
name = name.encode('utf-8') name = name.encode('utf-8')
if type(ccn)==unicode: if type(ccn)==str:
ccn = ccn.encode('utf-8') ccn = ccn.encode('utf-8')
name = normalize_name(name) + '\x00' name = name + b'\x00'
ccn = normalize_name(ccn) + '\x00' ccn = ccn + b'\x00'
name_sha = hashlib.sha1(name).digest()[:16] name_sha = hashlib.sha1(name).digest()[:16]
ccn_sha = hashlib.sha1(ccn).digest()[:16] ccn_sha = hashlib.sha1(ccn).digest()[:16]
both_sha = hashlib.sha1(name + ccn).digest() both_sha = hashlib.sha1(name + ccn).digest()
aes = AES(ccn_sha, name_sha) aes = AES(ccn_sha, name_sha)
crypt = aes.encrypt(both_sha + ('\x0c' * 0x0c)) crypt = aes.encrypt(both_sha + (b'\x0c' * 0x0c))
userkey = hashlib.sha1(crypt).digest() userkey = hashlib.sha1(crypt).digest()
return userkey.encode('base64') return base64.b64encode(userkey)
def cli_main(): def cli_main():
@@ -229,7 +232,7 @@ def cli_main():
(progname,)) (progname,))
return 1 return 1
if len(argv) != 4: if len(argv) != 4:
print(u"usage: {0} <Name> <CC#> <keyfileout.b64>".format(progname)) print("usage: {0} <Name> <CC#> <keyfileout.b64>".format(progname))
return 1 return 1
name, ccn, keypath = argv[1:] name, ccn, keypath = argv[1:]
userkey = generate_key(name, ccn) userkey = generate_key(name, ccn)
@@ -239,54 +242,54 @@ def cli_main():
def gui_main(): def gui_main():
try: try:
import Tkinter import tkinter
import Tkconstants import tkinter.constants
import tkMessageBox import tkinter.messagebox
import tkFileDialog import tkinter.filedialog
import traceback import traceback
except: except:
return cli_main() return cli_main()
class DecryptionDialog(Tkinter.Frame): class DecryptionDialog(tkinter.Frame):
def __init__(self, root): def __init__(self, root):
Tkinter.Frame.__init__(self, root, border=5) tkinter.Frame.__init__(self, root, border=5)
self.status = Tkinter.Label(self, text=u"Enter parameters") self.status = tkinter.Label(self, text="Enter parameters")
self.status.pack(fill=Tkconstants.X, expand=1) self.status.pack(fill=tkinter.constants.X, expand=1)
body = Tkinter.Frame(self) body = tkinter.Frame(self)
body.pack(fill=Tkconstants.X, expand=1) body.pack(fill=tkinter.constants.X, expand=1)
sticky = Tkconstants.E + Tkconstants.W sticky = tkinter.constants.E + tkinter.constants.W
body.grid_columnconfigure(1, weight=2) body.grid_columnconfigure(1, weight=2)
Tkinter.Label(body, text=u"Account Name").grid(row=0) tkinter.Label(body, text="Account Name").grid(row=0)
self.name = Tkinter.Entry(body, width=40) self.name = tkinter.Entry(body, width=40)
self.name.grid(row=0, column=1, sticky=sticky) self.name.grid(row=0, column=1, sticky=sticky)
Tkinter.Label(body, text=u"CC#").grid(row=1) tkinter.Label(body, text="CC#").grid(row=1)
self.ccn = Tkinter.Entry(body, width=40) self.ccn = tkinter.Entry(body, width=40)
self.ccn.grid(row=1, column=1, sticky=sticky) self.ccn.grid(row=1, column=1, sticky=sticky)
Tkinter.Label(body, text=u"Output file").grid(row=2) tkinter.Label(body, text="Output file").grid(row=2)
self.keypath = Tkinter.Entry(body, width=40) self.keypath = tkinter.Entry(body, width=40)
self.keypath.grid(row=2, column=1, sticky=sticky) self.keypath.grid(row=2, column=1, sticky=sticky)
self.keypath.insert(2, u"bnepubkey.b64") self.keypath.insert(2, "bnepubkey.b64")
button = Tkinter.Button(body, text=u"...", command=self.get_keypath) button = tkinter.Button(body, text="...", command=self.get_keypath)
button.grid(row=2, column=2) button.grid(row=2, column=2)
buttons = Tkinter.Frame(self) buttons = tkinter.Frame(self)
buttons.pack() buttons.pack()
botton = Tkinter.Button( botton = tkinter.Button(
buttons, text=u"Generate", width=10, command=self.generate) buttons, text="Generate", width=10, command=self.generate)
botton.pack(side=Tkconstants.LEFT) botton.pack(side=tkinter.constants.LEFT)
Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT) tkinter.Frame(buttons, width=10).pack(side=tkinter.constants.LEFT)
button = Tkinter.Button( button = tkinter.Button(
buttons, text=u"Quit", width=10, command=self.quit) buttons, text="Quit", width=10, command=self.quit)
button.pack(side=Tkconstants.RIGHT) button.pack(side=tkinter.constants.RIGHT)
def get_keypath(self): def get_keypath(self):
keypath = tkFileDialog.asksaveasfilename( keypath = tkinter.filedialog.asksaveasfilename(
parent=None, title=u"Select B&N ePub key file to produce", parent=None, title="Select B&N ePub key file to produce",
defaultextension=u".b64", defaultextension=".b64",
filetypes=[('base64-encoded files', '.b64'), filetypes=[('base64-encoded files', '.b64'),
('All Files', '.*')]) ('All Files', '.*')])
if keypath: if keypath:
keypath = os.path.normpath(keypath) keypath = os.path.normpath(keypath)
self.keypath.delete(0, Tkconstants.END) self.keypath.delete(0, tkinter.constants.END)
self.keypath.insert(0, keypath) self.keypath.insert(0, keypath)
return return
@@ -295,35 +298,35 @@ def gui_main():
ccn = self.ccn.get() ccn = self.ccn.get()
keypath = self.keypath.get() keypath = self.keypath.get()
if not name: if not name:
self.status['text'] = u"Name not specified" self.status['text'] = "Name not specified"
return return
if not ccn: if not ccn:
self.status['text'] = u"Credit card number not specified" self.status['text'] = "Credit card number not specified"
return return
if not keypath: if not keypath:
self.status['text'] = u"Output keyfile path not specified" self.status['text'] = "Output keyfile path not specified"
return return
self.status['text'] = u"Generating..." self.status['text'] = "Generating..."
try: try:
userkey = generate_key(name, ccn) userkey = generate_key(name, ccn)
except Exception, e: except Exception as e:
self.status['text'] = u"Error: (0}".format(e.args[0]) self.status['text'] = "Error: (0}".format(e.args[0])
return return
open(keypath,'wb').write(userkey) open(keypath,'wb').write(userkey)
self.status['text'] = u"Keyfile successfully generated" self.status['text'] = "Keyfile successfully generated"
root = Tkinter.Tk() root = tkinter.Tk()
if AES is None: if AES is None:
root.withdraw() root.withdraw()
tkMessageBox.showerror( tkinter.messagebox.showerror(
"Ignoble EPUB Keyfile Generator", "Ignoble EPUB Keyfile Generator",
"This script requires OpenSSL or PyCrypto, which must be installed " "This script requires OpenSSL or PyCrypto, which must be installed "
"separately. Read the top-of-script comment for details.") "separately. Read the top-of-script comment for details.")
return 1 return 1
root.title(u"Barnes & Noble ePub Keyfile Generator v.{0}".format(__version__)) root.title("Barnes & Noble ePub Keyfile Generator v.{0}".format(__version__))
root.resizable(True, False) root.resizable(True, False)
root.minsize(300, 0) root.minsize(300, 0)
DecryptionDialog(root).pack(fill=Tkconstants.X, expand=1) DecryptionDialog(root).pack(fill=tkinter.constants.X, expand=1)
root.mainloop() root.mainloop()
return 0 return 0

File diff suppressed because it is too large Load Diff

View File

@@ -1,19 +1,12 @@
#!/usr/bin/env python #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import with_statement # ineptepub.py
from __future__ import absolute_import # Copyright © 2009-2020 by i♥cabbages, Apprentice Harper et al.
from __future__ import print_function
# ineptepub.pyw, version 6.6
# Copyright © 2009-2020 by Apprentice Harper et al.
# Released under the terms of the GNU General Public Licence, version 3 # Released under the terms of the GNU General Public Licence, version 3
# <http://www.gnu.org/licenses/> # <http://www.gnu.org/licenses/>
# Original script by i♥cabbages
# Modified 20102013 by some_updates, DiapDealer and Apprentice Alf
# Modified 20152020 by Apprentice Harper et al.
# Revision history: # Revision history:
# 1 - Initial release # 1 - Initial release
@@ -36,17 +29,16 @@ from __future__ import print_function
# 6.4 - Remove erroneous 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.5 - Completely remove erroneous check on DER file sanity
# 6.6 - Import tkFileDialog, don't assume something else will import it. # 6.6 - Import tkFileDialog, don't assume something else will import it.
# 6.7 - Add Python 3 compatibility while still working with Python 2.7 # 7.0 - Add Python 3 compatibility for calibre 5.0
""" """
Decrypt Adobe Digital Editions encrypted ePub books. Decrypt Adobe Digital Editions encrypted ePub books.
""" """
__license__ = 'GPL v3' __license__ = 'GPL v3'
__version__ = "6.7" __version__ = "7.0"
import six import codecs
from six.moves import range
import sys import sys
import os import os
import traceback import traceback
@@ -54,8 +46,7 @@ import zlib
import zipfile import zipfile
from zipfile import ZipInfo, ZipFile, ZIP_STORED, ZIP_DEFLATED from zipfile import ZipInfo, ZipFile, ZIP_STORED, ZIP_DEFLATED
from contextlib import closing from contextlib import closing
import xml.etree.ElementTree as etree from lxml import etree
import base64
# Wrap a stream so that output gets flushed immediately # Wrap a stream so that output gets flushed immediately
# and also make sure that any unicode strings get # and also make sure that any unicode strings get
@@ -67,10 +58,17 @@ class SafeUnbuffered:
if self.encoding == None: if self.encoding == None:
self.encoding = "utf-8" self.encoding = "utf-8"
def write(self, data): def write(self, data):
if isinstance(data,six.text_type): if isinstance(data,str) or isinstance(data,unicode):
# str for Python3, unicode for Python2
data = data.encode(self.encoding,"replace") data = data.encode(self.encoding,"replace")
self.stream.write(data) try:
self.stream.flush() buffer = getattr(self.stream, 'buffer', self.stream)
# self.stream.buffer for Python3, self.stream for Python2
buffer.write(data)
buffer.flush()
except:
# We can do nothing if a write fails
raise
def __getattr__(self, attr): def __getattr__(self, attr):
return getattr(self.stream, attr) return getattr(self.stream, attr)
@@ -109,17 +107,18 @@ def unicode_argv():
start = argc.value - len(sys.argv) start = argc.value - len(sys.argv)
return [argv[i] for i in return [argv[i] for i in
range(start, argc.value)] range(start, argc.value)]
return [u"ineptepub.py"] return ["ineptepub.py"]
else: else:
argvencoding = sys.stdin.encoding argvencoding = sys.stdin.encoding or "utf-8"
if argvencoding == None: return [arg if (isinstance(arg, str) or isinstance(arg,unicode)) else str(arg, argvencoding) for arg in sys.argv]
argvencoding = "utf-8"
return [arg if (type(arg) == six.text_type) else six.text_type(arg,argvencoding) for arg in sys.argv]
class ADEPTError(Exception): class ADEPTError(Exception):
pass pass
class ADEPTNewVersionError(Exception):
pass
def _load_crypto_libcrypto(): def _load_crypto_libcrypto():
from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_int, c_long, \ from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_int, c_long, \
Structure, c_ulong, create_string_buffer, cast Structure, c_ulong, create_string_buffer, cast
@@ -213,6 +212,7 @@ def _load_crypto_libcrypto():
def _load_crypto_pycrypto(): def _load_crypto_pycrypto():
from Crypto.Cipher import AES as _AES from Crypto.Cipher import AES as _AES
from Crypto.PublicKey import RSA as _RSA from Crypto.PublicKey import RSA as _RSA
from Crypto.Cipher import PKCS1_v1_5 as _PKCS1_v1_5
# ASN.1 parsing code from tlslite # ASN.1 parsing code from tlslite
class ASN1Error(Exception): class ASN1Error(Exception):
@@ -304,14 +304,14 @@ def _load_crypto_pycrypto():
class AES(object): class AES(object):
def __init__(self, key): def __init__(self, key):
self._aes = _AES.new(key, _AES.MODE_CBC, '\x00'*16) self._aes = _AES.new(key, _AES.MODE_CBC, b'\x00'*16)
def decrypt(self, data): def decrypt(self, data):
return self._aes.decrypt(data) return self._aes.decrypt(data)
class RSA(object): class RSA(object):
def __init__(self, der): def __init__(self, der):
key = ASN1Parser([ord(x) for x in der]) key = ASN1Parser([x for x in der])
key = [key.getChild(x).value for x in range(1, 4)] key = [key.getChild(x).value for x in range(1, 4)]
key = [self.bytesToNumber(v) for v in key] key = [self.bytesToNumber(v) for v in key]
self._rsa = _RSA.construct(key) self._rsa = _RSA.construct(key)
@@ -323,7 +323,7 @@ def _load_crypto_pycrypto():
return total return total
def decrypt(self, data): def decrypt(self, data):
return self._rsa.decrypt(data) return _PKCS1_v1_5.new(self._rsa).decrypt(data, 172)
return (AES, RSA) return (AES, RSA)
@@ -342,7 +342,7 @@ def _load_crypto():
AES, RSA = _load_crypto() AES, RSA = _load_crypto()
META_NAMES = ('mimetype', 'META-INF/rights.xml', 'META-INF/encryption.xml') META_NAMES = ('mimetype', 'META-INF/rights.xml')
NSMAP = {'adept': 'http://ns.adobe.com/adept', NSMAP = {'adept': 'http://ns.adobe.com/adept',
'enc': 'http://www.w3.org/2001/04/xmlenc#'} 'enc': 'http://www.w3.org/2001/04/xmlenc#'}
@@ -352,21 +352,47 @@ class Decryptor(object):
self._aes = AES(bookkey) self._aes = AES(bookkey)
encryption = etree.fromstring(encryption) encryption = etree.fromstring(encryption)
self._encrypted = encrypted = set() self._encrypted = encrypted = set()
self._otherData = otherData = set()
self._json_elements_to_remove = json_elements_to_remove = set()
self._has_remaining_xml = False
expr = './%s/%s/%s' % (enc('EncryptedData'), enc('CipherData'), expr = './%s/%s/%s' % (enc('EncryptedData'), enc('CipherData'),
enc('CipherReference')) enc('CipherReference'))
for elem in encryption.findall(expr): for elem in encryption.findall(expr):
path = elem.get('URI', None) path = elem.get('URI', None)
encryption_type_url = (elem.getparent().getparent().find("./%s" % (enc('EncryptionMethod'))).get('Algorithm', None))
if path is not None: if path is not None:
path = path.encode('utf-8') if (encryption_type_url == "http://www.w3.org/2001/04/xmlenc#aes128-cbc"):
encrypted.add(path) # Adobe
path = path.encode('utf-8')
encrypted.add(path)
json_elements_to_remove.add(elem.getparent().getparent())
else:
path = path.encode('utf-8')
otherData.add(path)
self._has_remaining_xml = True
for elem in json_elements_to_remove:
elem.getparent().remove(elem)
def check_if_remaining(self):
return self._has_remaining_xml
def get_xml(self):
return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + etree.tostring(self._encryption, encoding="utf-8", pretty_print=True, xml_declaration=False).decode("utf-8")
def decompress(self, bytes): def decompress(self, bytes):
dc = zlib.decompressobj(-15) dc = zlib.decompressobj(-15)
bytes = dc.decompress(bytes) try:
ex = dc.decompress(b'Z') + dc.flush() decompressed_bytes = dc.decompress(bytes)
if ex: ex = dc.decompress(b'Z') + dc.flush()
bytes = bytes + ex if ex:
return bytes decompressed_bytes = decompressed_bytes + ex
except:
# possibly not compressed by zip - just return bytes
return bytes
return decompressed_bytes
def decrypt(self, path, data): def decrypt(self, path, data):
if path.encode('utf-8') in self._encrypted: if path.encode('utf-8') in self._encrypted:
@@ -398,15 +424,51 @@ def adeptBook(inpath):
return True return True
return False return False
# Checks the license file and returns the UUID the book is licensed for.
# This is used so that the Calibre plugin can pick the correct decryption key
# first try without having to loop through all possible keys.
def adeptGetUserUUID(inpath):
with closing(ZipFile(open(inpath, 'rb'))) as inf:
try:
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
expr = './/%s' % (adept('user'),)
user_uuid = ''.join(rights.findtext(expr))
if user_uuid[:9] != "urn:uuid:":
return None
return user_uuid[9:]
except:
return None
def verify_book_key(bookkey):
if bookkey[-17] != '\x00' and bookkey[-17] != 0:
# Byte not null, invalid result
return False
if ((bookkey[0] != '\x02' and bookkey[0] != 2) and
((bookkey[0] != '\x00' and bookkey[0] != 0) or
(bookkey[1] != '\x02' and bookkey[1] != 2))):
# Key not starting with "00 02" or "02" -> error
return False
keylen = len(bookkey) - 17
for i in range(1, keylen):
if bookkey[i] == 0 or bookkey[i] == '\x00':
# Padding data contains a space - that's not allowed.
# Probably bad decryption.
return False
return True
def decryptBook(userkey, inpath, outpath): def decryptBook(userkey, inpath, outpath):
if AES is None: if AES is None:
raise ADEPTError(u"PyCrypto or OpenSSL must be installed.") raise ADEPTError("PyCrypto or OpenSSL must be installed.")
rsa = RSA(userkey) rsa = RSA(userkey)
with closing(ZipFile(open(inpath, 'rb'))) as inf: with closing(ZipFile(open(inpath, 'rb'))) as inf:
namelist = set(inf.namelist()) namelist = inf.namelist()
if 'META-INF/rights.xml' not in namelist or \ if 'META-INF/rights.xml' not in namelist or \
'META-INF/encryption.xml' not in namelist: 'META-INF/encryption.xml' not in namelist:
print(u"{0:s} is DRM-free.".format(os.path.basename(inpath))) print("{0:s} is DRM-free.".format(os.path.basename(inpath)))
return 1 return 1
for name in META_NAMES: for name in META_NAMES:
namelist.remove(name) namelist.remove(name)
@@ -415,40 +477,46 @@ def decryptBook(userkey, inpath, outpath):
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag) adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
expr = './/%s' % (adept('encryptedKey'),) expr = './/%s' % (adept('encryptedKey'),)
bookkey = ''.join(rights.findtext(expr)) bookkey = ''.join(rights.findtext(expr))
if len(bookkey) == 192:
print("{0:s} seems to be an Adobe ADEPT ePub with Adobe's new DRM".format(os.path.basename(inpath)))
print("This DRM cannot be removed yet. ")
print("Try getting your distributor to give you a new ACSM file, then open that in an old version of ADE (2.0).")
print("If your book distributor is not enforcing the new DRM yet, this will give you a copy with the old DRM.")
raise ADEPTNewVersionError("Book uses new ADEPT encryption")
if len(bookkey) != 172: if len(bookkey) != 172:
print(u"{0:s} is not a secure Adobe Adept ePub.".format(os.path.basename(inpath))) print("{0:s} is not a secure Adobe Adept ePub.".format(os.path.basename(inpath)))
return 1 return 1
bookkey = bookkey.encode('ascii') bookkey = rsa.decrypt(codecs.decode(bookkey.encode('ascii'), 'base64'))
bookkey = base64.b64decode(bookkey)
bookkey = rsa.decrypt(bookkey)
# Padded as per RSAES-PKCS1-v1_5 # Padded as per RSAES-PKCS1-v1_5
if bookkey[-17] != '\x00' and bookkey[-17] != 0: if len(bookkey) > 16:
print(u"Could not decrypt {0:s}. Wrong key".format(os.path.basename(inpath))) if verify_book_key(bookkey):
return 2 bookkey = bookkey[-16:]
else:
print("Could not decrypt {0:s}. Wrong key".format(os.path.basename(inpath)))
return 2
encryption = inf.read('META-INF/encryption.xml') encryption = inf.read('META-INF/encryption.xml')
decryptor = Decryptor(bookkey[-16:], encryption) decryptor = Decryptor(bookkey, encryption)
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False) kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf: with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf:
zi = ZipInfo('mimetype')
zi.compress_type=ZIP_STORED for path in (["mimetype"] + namelist):
try:
# if the mimetype is present, get its info, including time-stamp
oldzi = inf.getinfo('mimetype')
# copy across fields to be preserved
zi.date_time = oldzi.date_time
zi.comment = oldzi.comment
zi.extra = oldzi.extra
zi.internal_attr = oldzi.internal_attr
# external attributes are dependent on the create system, so copy both.
zi.external_attr = oldzi.external_attr
zi.create_system = oldzi.create_system
except:
pass
outf.writestr(zi, inf.read('mimetype'))
for path in namelist:
data = inf.read(path) data = inf.read(path)
zi = ZipInfo(path) zi = ZipInfo(path)
zi.compress_type=ZIP_DEFLATED zi.compress_type=ZIP_DEFLATED
if path == "mimetype":
zi.compress_type = ZIP_STORED
elif path == "META-INF/encryption.xml":
# Check if there's still something in there
if (decryptor.check_if_remaining()):
data = decryptor.get_xml()
print("Adding encryption.xml for the remaining embedded files.")
# We removed DRM, but there's still stuff like obfuscated fonts.
else:
continue
try: try:
# get the file info, including time-stamp # get the file info, including time-stamp
oldzi = inf.getinfo(path) oldzi = inf.getinfo(path)
@@ -460,11 +528,17 @@ def decryptBook(userkey, inpath, outpath):
# external attributes are dependent on the create system, so copy both. # external attributes are dependent on the create system, so copy both.
zi.external_attr = oldzi.external_attr zi.external_attr = oldzi.external_attr
zi.create_system = oldzi.create_system zi.create_system = oldzi.create_system
if any(ord(c) >= 128 for c in path) or any(ord(c) >= 128 for c in zi.comment):
# If the file name or the comment contains any non-ASCII char, set the UTF8-flag
zi.flag_bits |= 0x800
except: except:
pass pass
outf.writestr(zi, decryptor.decrypt(path, data)) if path == "META-INF/encryption.xml":
outf.writestr(zi, data)
else:
outf.writestr(zi, decryptor.decrypt(path, data))
except: except:
print(u"Could not decrypt {0:s} because of an exception:\n{1:s}".format(os.path.basename(inpath), traceback.format_exc())) print("Could not decrypt {0:s} because of an exception:\n{1:s}".format(os.path.basename(inpath), traceback.format_exc()))
return 2 return 2
return 0 return 0
@@ -475,90 +549,90 @@ def cli_main():
argv=unicode_argv() argv=unicode_argv()
progname = os.path.basename(argv[0]) progname = os.path.basename(argv[0])
if len(argv) != 4: if len(argv) != 4:
print(u"usage: {0} <keyfile.der> <inbook.epub> <outbook.epub>".format(progname)) print("usage: {0} <keyfile.der> <inbook.epub> <outbook.epub>".format(progname))
return 1 return 1
keypath, inpath, outpath = argv[1:] keypath, inpath, outpath = argv[1:]
userkey = open(keypath,'rb').read() userkey = open(keypath,'rb').read()
result = decryptBook(userkey, inpath, outpath) result = decryptBook(userkey, inpath, outpath)
if result == 0: if result == 0:
print(u"Successfully decrypted {0:s} as {1:s}".format(os.path.basename(inpath),os.path.basename(outpath))) print("Successfully decrypted {0:s} as {1:s}".format(os.path.basename(inpath),os.path.basename(outpath)))
return result return result
def gui_main(): def gui_main():
try: try:
import six.moves.tkinter import tkinter
import six.moves.tkinter_constants import tkinter.constants
import six.moves.tkinter_filedialog import tkinter.filedialog
import six.moves.tkinter_messagebox import tkinter.messagebox
import traceback import traceback
except: except:
return cli_main() return cli_main()
class DecryptionDialog(six.moves.tkinter.Frame): class DecryptionDialog(tkinter.Frame):
def __init__(self, root): def __init__(self, root):
six.moves.tkinter.Frame.__init__(self, root, border=5) tkinter.Frame.__init__(self, root, border=5)
self.status = six.moves.tkinter.Label(self, text=u"Select files for decryption") self.status = tkinter.Label(self, text="Select files for decryption")
self.status.pack(fill=six.moves.tkinter_constants.X, expand=1) self.status.pack(fill=tkinter.constants.X, expand=1)
body = six.moves.tkinter.Frame(self) body = tkinter.Frame(self)
body.pack(fill=six.moves.tkinter_constants.X, expand=1) body.pack(fill=tkinter.constants.X, expand=1)
sticky = six.moves.tkinter_constants.E + six.moves.tkinter_constants.W sticky = tkinter.constants.E + tkinter.constants.W
body.grid_columnconfigure(1, weight=2) body.grid_columnconfigure(1, weight=2)
six.moves.tkinter.Label(body, text=u"Key file").grid(row=0) tkinter.Label(body, text="Key file").grid(row=0)
self.keypath = six.moves.tkinter.Entry(body, width=30) self.keypath = tkinter.Entry(body, width=30)
self.keypath.grid(row=0, column=1, sticky=sticky) self.keypath.grid(row=0, column=1, sticky=sticky)
if os.path.exists(u"adeptkey.der"): if os.path.exists("adeptkey.der"):
self.keypath.insert(0, u"adeptkey.der") self.keypath.insert(0, "adeptkey.der")
button = six.moves.tkinter.Button(body, text=u"...", command=self.get_keypath) button = tkinter.Button(body, text="...", command=self.get_keypath)
button.grid(row=0, column=2) button.grid(row=0, column=2)
six.moves.tkinter.Label(body, text=u"Input file").grid(row=1) tkinter.Label(body, text="Input file").grid(row=1)
self.inpath = six.moves.tkinter.Entry(body, width=30) self.inpath = tkinter.Entry(body, width=30)
self.inpath.grid(row=1, column=1, sticky=sticky) self.inpath.grid(row=1, column=1, sticky=sticky)
button = six.moves.tkinter.Button(body, text=u"...", command=self.get_inpath) button = tkinter.Button(body, text="...", command=self.get_inpath)
button.grid(row=1, column=2) button.grid(row=1, column=2)
six.moves.tkinter.Label(body, text=u"Output file").grid(row=2) tkinter.Label(body, text="Output file").grid(row=2)
self.outpath = six.moves.tkinter.Entry(body, width=30) self.outpath = tkinter.Entry(body, width=30)
self.outpath.grid(row=2, column=1, sticky=sticky) self.outpath.grid(row=2, column=1, sticky=sticky)
button = six.moves.tkinter.Button(body, text=u"...", command=self.get_outpath) button = tkinter.Button(body, text="...", command=self.get_outpath)
button.grid(row=2, column=2) button.grid(row=2, column=2)
buttons = six.moves.tkinter.Frame(self) buttons = tkinter.Frame(self)
buttons.pack() buttons.pack()
botton = six.moves.tkinter.Button( botton = tkinter.Button(
buttons, text=u"Decrypt", width=10, command=self.decrypt) buttons, text="Decrypt", width=10, command=self.decrypt)
botton.pack(side=six.moves.tkinter_constants.LEFT) botton.pack(side=tkinter.constants.LEFT)
six.moves.tkinter.Frame(buttons, width=10).pack(side=six.moves.tkinter_constants.LEFT) tkinter.Frame(buttons, width=10).pack(side=tkinter.constants.LEFT)
button = six.moves.tkinter.Button( button = tkinter.Button(
buttons, text=u"Quit", width=10, command=self.quit) buttons, text="Quit", width=10, command=self.quit)
button.pack(side=six.moves.tkinter_constants.RIGHT) button.pack(side=tkinter.constants.RIGHT)
def get_keypath(self): def get_keypath(self):
keypath = six.moves.tkinter_filedialog.askopenfilename( keypath = tkinter.filedialog.askopenfilename(
parent=None, title=u"Select Adobe Adept \'.der\' key file", parent=None, title="Select Adobe Adept \'.der\' key file",
defaultextension=u".der", defaultextension=".der",
filetypes=[('Adobe Adept DER-encoded files', '.der'), filetypes=[('Adobe Adept DER-encoded files', '.der'),
('All Files', '.*')]) ('All Files', '.*')])
if keypath: if keypath:
keypath = os.path.normpath(keypath) keypath = os.path.normpath(keypath)
self.keypath.delete(0, six.moves.tkinter_constants.END) self.keypath.delete(0, tkinter.constants.END)
self.keypath.insert(0, keypath) self.keypath.insert(0, keypath)
return return
def get_inpath(self): def get_inpath(self):
inpath = six.moves.tkinter_filedialog.askopenfilename( inpath = tkinter.filedialog.askopenfilename(
parent=None, title=u"Select ADEPT-encrypted ePub file to decrypt", parent=None, title="Select ADEPT-encrypted ePub file to decrypt",
defaultextension=u".epub", filetypes=[('ePub files', '.epub')]) defaultextension=".epub", filetypes=[('ePub files', '.epub')])
if inpath: if inpath:
inpath = os.path.normpath(inpath) inpath = os.path.normpath(inpath)
self.inpath.delete(0, six.moves.tkinter_constants.END) self.inpath.delete(0, tkinter.constants.END)
self.inpath.insert(0, inpath) self.inpath.insert(0, inpath)
return return
def get_outpath(self): def get_outpath(self):
outpath = six.moves.tkinter_filedialog.asksaveasfilename( outpath = tkinter.filedialog.asksaveasfilename(
parent=None, title=u"Select unencrypted ePub file to produce", parent=None, title="Select unencrypted ePub file to produce",
defaultextension=u".epub", filetypes=[('ePub files', '.epub')]) defaultextension=".epub", filetypes=[('ePub files', '.epub')])
if outpath: if outpath:
outpath = os.path.normpath(outpath) outpath = os.path.normpath(outpath)
self.outpath.delete(0, six.moves.tkinter_constants.END) self.outpath.delete(0, tkinter.constants.END)
self.outpath.insert(0, outpath) self.outpath.insert(0, outpath)
return return
@@ -567,34 +641,34 @@ def gui_main():
inpath = self.inpath.get() inpath = self.inpath.get()
outpath = self.outpath.get() outpath = self.outpath.get()
if not keypath or not os.path.exists(keypath): if not keypath or not os.path.exists(keypath):
self.status['text'] = u"Specified key file does not exist" self.status['text'] = "Specified key file does not exist"
return return
if not inpath or not os.path.exists(inpath): if not inpath or not os.path.exists(inpath):
self.status['text'] = u"Specified input file does not exist" self.status['text'] = "Specified input file does not exist"
return return
if not outpath: if not outpath:
self.status['text'] = u"Output file not specified" self.status['text'] = "Output file not specified"
return return
if inpath == outpath: if inpath == outpath:
self.status['text'] = u"Must have different input and output files" self.status['text'] = "Must have different input and output files"
return return
userkey = open(keypath,'rb').read() userkey = open(keypath,'rb').read()
self.status['text'] = u"Decrypting..." self.status['text'] = "Decrypting..."
try: try:
decrypt_status = decryptBook(userkey, inpath, outpath) decrypt_status = decryptBook(userkey, inpath, outpath)
except Exception as e: except Exception as e:
self.status['text'] = u"Error: {0}".format(e.args[0]) self.status['text'] = "Error: {0}".format(e.args[0])
return return
if decrypt_status == 0: if decrypt_status == 0:
self.status['text'] = u"File successfully decrypted" self.status['text'] = "File successfully decrypted"
else: else:
self.status['text'] = u"The was an error decrypting the file." self.status['text'] = "There was an error decrypting the file."
root = six.moves.tkinter.Tk() root = tkinter.Tk()
root.title(u"Adobe Adept ePub Decrypter v.{0}".format(__version__)) root.title("Adobe Adept ePub Decrypter v.{0}".format(__version__))
root.resizable(True, False) root.resizable(True, False)
root.minsize(300, 0) root.minsize(300, 0)
DecryptionDialog(root).pack(fill=six.moves.tkinter_constants.X, expand=1) DecryptionDialog(root).pack(fill=tkinter.constants.X, expand=1)
root.mainloop() root.mainloop()
return 0 return 0

745
DeDRM_plugin/ineptpdf.py Normal file → Executable file

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,11 @@
#!/usr/bin/env python #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import with_statement
# ion.py # ion.py
# Copyright © 2013-2020 Apprentice Harper et al. # Copyright © 2013-2020 Apprentice Harper et al.
__license__ = 'GPL v3' __license__ = 'GPL v3'
__version__ = '2.0' __version__ = '3.0'
# Revision history: # Revision history:
# Pascal implementation by lulzkabulz. # Pascal implementation by lulzkabulz.
@@ -17,7 +15,7 @@ __version__ = '2.0'
# 1.2 - Added pylzma import fallback # 1.2 - Added pylzma import fallback
# 1.3 - Fixed lzma support for calibre 4.6+ # 1.3 - Fixed lzma support for calibre 4.6+
# 2.0 - VoucherEnvelope v2/v3 support by apprenticesakuya. # 2.0 - VoucherEnvelope v2/v3 support by apprenticesakuya.
# 3.0 - Added Python 3 compatibility for calibre 5.0
""" """
Decrypt Kindle KFX files. Decrypt Kindle KFX files.
@@ -30,13 +28,10 @@ import os
import os.path import os.path
import struct import struct
try: from io import BytesIO
from cStringIO import StringIO
except ImportError:
from StringIO import StringIO
from Crypto.Cipher import AES from Crypto.Cipher import AES
from Crypto.Util.py3compat import bchr, bord from Crypto.Util.py3compat import bchr
try: try:
# lzma library from calibre 4.6.0 or later # lzma library from calibre 4.6.0 or later
@@ -94,7 +89,7 @@ LEN_IS_VAR_LEN = 0xE
LEN_IS_NULL = 0xF 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 # asserts must always raise exceptions for proper functioning
@@ -352,7 +347,7 @@ class BinaryIonParser(object):
b = self.stream.read(1) b = self.stream.read(1)
if len(b) < 1: if len(b) < 1:
return -1 return -1
b = bord(b) b = ord(b)
result = b >> 4 result = b >> 4
ln = b & 0xF ln = b & 0xF
@@ -377,13 +372,13 @@ class BinaryIonParser(object):
return result return result
def readvarint(self): def readvarint(self):
b = bord(self.read()) b = ord(self.read())
negative = ((b & 0x40) != 0) negative = ((b & 0x40) != 0)
result = (b & 0x3F) result = (b & 0x3F)
i = 0 i = 0
while (b & 0x80) == 0 and i < 4: while (b & 0x80) == 0 and i < 4:
b = bord(self.read()) b = ord(self.read())
result = (result << 7) | (b & 0x7F) result = (result << 7) | (b & 0x7F)
i += 1 i += 1
@@ -394,12 +389,12 @@ class BinaryIonParser(object):
return result return result
def readvaruint(self): def readvaruint(self):
b = bord(self.read()) b = ord(self.read())
result = (b & 0x7F) result = (b & 0x7F)
i = 0 i = 0
while (b & 0x80) == 0 and i < 4: while (b & 0x80) == 0 and i < 4:
b = bord(self.read()) b = ord(self.read())
result = (result << 7) | (b & 0x7F) result = (result << 7) | (b & 0x7F)
i += 1 i += 1
@@ -419,7 +414,7 @@ class BinaryIonParser(object):
_assert(self.localremaining <= 8, "Decimal overflow") _assert(self.localremaining <= 8, "Decimal overflow")
signed = False 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: if (b[0] & 0x80) != 0:
b[0] = b[0] & 0x7F b[0] = b[0] & 0x7F
signed = True signed = True
@@ -517,6 +512,8 @@ class BinaryIonParser(object):
if table is not None: if table is not None:
self.symbols.import_(table, min(maxid, len(table.symnames))) self.symbols.import_(table, min(maxid, len(table.symnames)))
if len(table.symnames) < maxid:
self.symbols.importunknown(name + "-unknown", maxid - len(table.symnames))
else: else:
self.symbols.importunknown(name, maxid) self.symbols.importunknown(name, maxid)
@@ -584,7 +581,7 @@ class BinaryIonParser(object):
_assert(self.valuelen <= 4, "int too long: %d" % self.valuelen) _assert(self.valuelen <= 4, "int too long: %d" % self.valuelen)
v = 0 v = 0
for i in range(self.valuelen - 1, -1, -1): 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: if self.valuetid == TID_NEGINT:
self.value = -v self.value = -v
@@ -654,7 +651,7 @@ class BinaryIonParser(object):
result = "" result = ""
for i in b: for i in b:
result += ("%02x " % bord(i)) result += ("%02x " % ord(i))
if len(result) > 0: if len(result) > 0:
result = result[:-1] result = result[:-1]
@@ -738,7 +735,10 @@ SYM_NAMES = [ 'com.amazon.drm.Envelope@1.0',
'com.amazon.drm.EncryptedPage@2.0', 'com.amazon.drm.EncryptedPage@2.0',
'com.amazon.drm.PlainText@2.0', 'compression_algorithm', 'com.amazon.drm.PlainText@2.0', 'compression_algorithm',
'com.amazon.drm.Compressed@1.0', 'page_index_table', 'com.amazon.drm.Compressed@1.0', 'page_index_table',
'com.amazon.drm.VoucherEnvelope@2.0', 'com.amazon.drm.VoucherEnvelope@3.0' ] ] + ['com.amazon.drm.VoucherEnvelope@%d.0' % n
for n in list(range(2, 29)) + [
9708, 1031, 2069, 9041, 3646,
6052, 9479, 9888, 4648, 5683]]
def addprottable(ion): def addprottable(ion):
ion.addtocatalog("ProtectedData", 1, SYM_NAMES) ion.addtocatalog("ProtectedData", 1, SYM_NAMES)
@@ -753,7 +753,8 @@ def pkcs7pad(msg, blocklen):
def pkcs7unpad(msg, blocklen): def pkcs7unpad(msg, blocklen):
_assert(len(msg) % blocklen == 0) _assert(len(msg) % blocklen == 0)
paddinglen = bord(msg[-1]) paddinglen = msg[-1]
_assert(paddinglen > 0 and paddinglen <= blocklen, "Incorrect padding - Wrong key") _assert(paddinglen > 0 and paddinglen <= blocklen, "Incorrect padding - Wrong key")
_assert(msg[-paddinglen:] == bchr(paddinglen) * paddinglen, "Incorrect padding - Wrong key") _assert(msg[-paddinglen:] == bchr(paddinglen) * paddinglen, "Incorrect padding - Wrong key")
@@ -761,9 +762,45 @@ def pkcs7unpad(msg, blocklen):
# every VoucherEnvelope version has a corresponding "word" and magic number, used in obfuscating the shared secret # every VoucherEnvelope version has a corresponding "word" and magic number, used in obfuscating the shared secret
VOUCHER_VERSION_INFOS = { OBFUSCATION_TABLE = {
2: [b'Antidisestablishmentarianism', 5], "V1": (0x00, None),
3: [b'Floccinaucinihilipilification', 8] "V2": (0x05, b'Antidisestablishmentarianism'),
"V3": (0x08, b'Floccinaucinihilipilification'),
"V4": (0x07, b'>\x14\x0c\x12\x10-\x13&\x18U\x1d\x05Rlt\x03!\x19\x1b\x13\x04]Y\x19,\t\x1b'),
"V5": (0x06, b'~\x18~\x16J\\\x18\x10\x05\x0b\x07\t\x0cZ\r|\x1c\x15\x1d\x11>,\x1b\x0e\x03"4\x1b\x01'),
"V6": (0x09, b'3h\x055\x03[^>\x19\x1c\x08\x1b\rtm4\x02Rp\x0c\x16B\n'),
"V7": (0x05, b'\x10\x1bJ\x18\nh!\x10"\x03>Z\'\r\x01]W\x06\x1c\x1e?\x0f\x13'),
"V8": (0x09, b"K\x0c6\x1d\x1a\x17pO}Rk\x1d'w1^\x1f$\x1c{C\x02Q\x06\x1d`"),
"V9": (0x05, b'X.\x0eW\x1c*K\x12\x12\t\n\n\x17Wx\x01\x02Yf\x0f\x18\x1bVXPi\x01'),
"V10": (0x07, b'z3\n\x039\x12\x13`\x06=v,\x02MTK\x1e%}L\x1c\x1f\x15\x0c\x11\x02\x0c\n8\x17p'),
"V11": (0x05, b'L=\nhVm\x07go\n6\x14\x06\x16L\r\x02\x0b\x0c\x1b\x04#p\t'),
"V12": (0x06, b',n\x1d\rl\x13\x1c\x13\x16p\x14\x07U\x0c\x1f\x19w\x16\x16\x1d5T'),
"V13": (0x07, b'I\x05\t\x08\x03r)\x01$N\x0fr3n\x0b062D\x0f\x13'),
"V14": (0x05, b"\x03\x02\x1c9\x19\x15\x15q\x1057\x08\x16\x0cF\x1b.Fw\x01\x12\x03\x13\x02\x17S'hk6"),
"V15": (0x0A, b'&,4B\x1dcI\x0bU\x03I\x07\x04\x1c\t\x05c\x07%ws\x0cj\t\x1a\x08\x0f'),
"V16": (0x0A, b'\x06\x18`h,b><\x06PqR\x02Zc\x034\n\x16\x1e\x18\x06#e'),
"V17": (0x07, b'y\r\x12\x08fw.[\x02\t\n\x13\x11\x0c\x11b\x1e8L\x10(\x13<Jx6c\x0f'),
"V18": (0x07, b'I\x0b\x0e,\x19\x1aIa\x10s\x19g\\\x1b\x11!\x18yf\x0f\t\x1d7[bSp\x03'),
"V19": (0x05, b'\n6>)N\x02\x188\x016s\x13\x14\x1b\x16jeN\n\x146\x04\x18\x1c\x0c\x19\x1f,\x02]'),
"V20": (0x08, b'_\r\x01\x12]\\\x14*\x17i\x14\r\t!\x1e,~hZ\x12jK\x17\x1e*1'),
"V21": (0x07, b'e\x1d\x19|\ty\x1di|N\x13\x0e\x04\x1bj<h\x13\x15k\x12\x08=\x1f\x16~\x13l'),
"V22": (0x08, b'?\x17yi$k7Pc\tEo\x0c\x07\x07\t\x1f,*i\x12\x0cI0\x10I\x1a?2\x04'),
"V23": (0x08, b'\x16+db\x13\x04\x18\rc%\x14\x17\x0f\x13F\x0c[\t9\x1ay\x01\x1eH'),
"V24": (0x06, b'|6\\\x1a\r\x10\nP\x07\x0fu\x1f\t,\rr`uv\\~55\x11]N'),
"V25": (0x09, b'\x07\x14w\x1e,^y\x01:\x08\x07\x1fr\tU#j\x16\x12\x1eB\x04\x16=\x06fZ\x07\x02\x06'),
"V26": (0x06, b'\x03IL\x1e"K\x1f\x0f\x1fp0\x01`X\x02z0`\x03\x0eN\x07'),
"V27": (0x07, b'Xk\x10y\x02\x18\x10\x17\x1d,\x0e\x05e\x10\x15"e\x0fh(\x06s\x1c\x08I\x0c\x1b\x0e'),
"V28": (0x0A, b'6P\x1bs\x0f\x06V.\x1cM\x14\x02\n\x1b\x07{P0:\x18zaU\x05'),
"V9708": (0x05, b'\x1diIm\x08a\x17\x1e!am\x1d\x1aQ.\x16!\x06*\}x04\x11\t\x06\x04?'),
"V1031": (0x08, b'Antidisestablishmentarianism'),
"V2069": (0x07, b'Floccinaucinihilipilification'),
"V9041": (0x06, b'>\x14\x0c\x12\x10-\x13&\x18U\x1d\x05Rlt\x03!\x19\x1b\x13\x04]Y\x19,\t\x1b'),
"V3646": (0x09, b'~\x18~\x16J\\\x18\x10\x05\x0b\x07\t\x0cZ\r|\x1c\x15\x1d\x11>,\x1b\x0e\x03"4\x1b\x01'),
"V6052": (0x05, b'3h\x055\x03[^>\x19\x1c\x08\x1b\rtm4\x02Rp\x0c\x16B\n'),
"V9479": (0x09, b'\x10\x1bJ\x18\nh!\x10"\x03>Z\'\r\x01]W\x06\x1c\x1e?\x0f\x13'),
"V9888": (0x05, b"K\x0c6\x1d\x1a\x17pO}Rk\x1d'w1^\x1f$\x1c{C\x02Q\x06\x1d`"),
"V4648": (0x07, b'X.\x0eW\x1c*K\x12\x12\t\n\n\x17Wx\x01\x02Yf\x0f\x18\x1bVXPi\x01'),
"V5683": (0x05, b'z3\n\x039\x12\x13`\x06=v,\x02MTK\x1e%}L\x1c\x1f\x15\x0c\x11\x02\x0c\n8\x17p'),
} }
@@ -772,9 +809,7 @@ def obfuscate(secret, version):
if version == 1: # v1 does not use obfuscation if version == 1: # v1 does not use obfuscation
return secret return secret
params = VOUCHER_VERSION_INFOS[version] magic, word = OBFUSCATION_TABLE["V%d" % version]
word = params[0]
magic = params[1]
# extend secret so that its length is divisible by the magic number # extend secret so that its length is divisible by the magic number
if len(secret) % magic != 0: if len(secret) % magic != 0:
@@ -811,7 +846,13 @@ class DrmIonVoucher(object):
secretkey = b"" secretkey = b""
def __init__(self, voucherenv, dsn, secret): def __init__(self, voucherenv, dsn, secret):
self.dsn,self.secret = dsn,secret self.dsn, self.secret = dsn, secret
if isinstance(dsn, str):
self.dsn = dsn.encode('ASCII')
if isinstance(secret, str):
self.secret = secret.encode('ASCII')
self.lockparams = [] self.lockparams = []
@@ -819,25 +860,25 @@ class DrmIonVoucher(object):
addprottable(self.envelope) addprottable(self.envelope)
def decryptvoucher(self): def decryptvoucher(self):
shared = "PIDv3" + self.encalgorithm + self.enctransformation + self.hashalgorithm shared = ("PIDv3" + self.encalgorithm + self.enctransformation + self.hashalgorithm).encode('ASCII')
self.lockparams.sort() self.lockparams.sort()
for param in self.lockparams: for param in self.lockparams:
if param == "ACCOUNT_SECRET": if param == "ACCOUNT_SECRET":
shared += param + self.secret shared += param.encode('ASCII') + self.secret
elif param == "CLIENT_ID": elif param == "CLIENT_ID":
shared += param + self.dsn shared += param.encode('ASCII') + self.dsn
else: else:
_assert(False, "Unknown lock parameter: %s" % param) _assert(False, "Unknown lock parameter: %s" % param)
sharedsecret = obfuscate(shared.encode('ASCII'), self.version) sharedsecret = obfuscate(shared, self.version)
key = hmac.new(sharedsecret, "PIDv3", digestmod=hashlib.sha256).digest() key = hmac.new(sharedsecret, b"PIDv3", digestmod=hashlib.sha256).digest()
aes = AES.new(key[:32], AES.MODE_CBC, self.cipheriv[:16]) aes = AES.new(key[:32], AES.MODE_CBC, self.cipheriv[:16])
b = aes.decrypt(self.ciphertext) b = aes.decrypt(self.ciphertext)
b = pkcs7unpad(b, 16) b = pkcs7unpad(b, 16)
self.drmkey = BinaryIonParser(StringIO(b)) self.drmkey = BinaryIonParser(BytesIO(b))
addprottable(self.drmkey) addprottable(self.drmkey)
_assert(self.drmkey.hasnext() and self.drmkey.next() == TID_LIST and self.drmkey.gettypename() == "com.amazon.drm.KeySet@1.0", _assert(self.drmkey.hasnext() and self.drmkey.next() == TID_LIST and self.drmkey.gettypename() == "com.amazon.drm.KeySet@1.0",
@@ -876,7 +917,7 @@ class DrmIonVoucher(object):
self.envelope.next() self.envelope.next()
field = self.envelope.getfieldname() field = self.envelope.getfieldname()
if field == "voucher": if field == "voucher":
self.voucher = BinaryIonParser(StringIO(self.envelope.lobvalue())) self.voucher = BinaryIonParser(BytesIO(self.envelope.lobvalue()))
addprottable(self.voucher) addprottable(self.voucher)
continue continue
elif field != "strategy": elif field != "strategy":
@@ -997,6 +1038,7 @@ class DrmIon(object):
elif self.ion.gettypename() in ["com.amazon.drm.EncryptedPage@1.0", "com.amazon.drm.EncryptedPage@2.0"]: elif self.ion.gettypename() in ["com.amazon.drm.EncryptedPage@1.0", "com.amazon.drm.EncryptedPage@2.0"]:
decompress = False decompress = False
decrypt = True
ct = None ct = None
civ = None civ = None
self.ion.stepin() self.ion.stepin()
@@ -1010,7 +1052,23 @@ class DrmIon(object):
civ = self.ion.lobvalue() civ = self.ion.lobvalue()
if ct is not None and civ is not None: if ct is not None and civ is not None:
self.processpage(ct, civ, outpages, decompress) self.processpage(ct, civ, outpages, decompress, decrypt)
self.ion.stepout()
elif self.ion.gettypename() in ["com.amazon.drm.PlainText@1.0", "com.amazon.drm.PlainText@2.0"]:
decompress = False
decrypt = False
plaintext = None
self.ion.stepin()
while self.ion.hasnext():
self.ion.next()
if self.ion.gettypename() == "com.amazon.drm.Compressed@1.0":
decompress = True
if self.ion.getfieldname() == "data":
plaintext = self.ion.lobvalue()
if plaintext is not None:
self.processpage(plaintext, None, outpages, decompress, decrypt)
self.ion.stepout() self.ion.stepout()
self.ion.stepout() self.ion.stepout()
@@ -1021,15 +1079,18 @@ class DrmIon(object):
def print_(self, lst): def print_(self, lst):
self.ion.print_(lst) self.ion.print_(lst)
def processpage(self, ct, civ, outpages, decompress): def processpage(self, ct, civ, outpages, decompress, decrypt):
aes = AES.new(self.key[:16], AES.MODE_CBC, civ[:16]) if decrypt:
msg = pkcs7unpad(aes.decrypt(ct), 16) aes = AES.new(self.key[:16], AES.MODE_CBC, civ[:16])
msg = pkcs7unpad(aes.decrypt(ct), 16)
else:
msg = ct
if not decompress: if not decompress:
outpages.write(msg) outpages.write(msg)
return return
_assert(msg[0] == b"\x00", "LZMA UseFilter not supported") _assert(msg[0] == 0, "LZMA UseFilter not supported")
if calibre_lzma is not None: if calibre_lzma is not None:
with calibre_lzma.decompress(msg[1:], bufsize=0x1000000) as f: 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 -*- # -*- coding: utf-8 -*-
from __future__ import with_statement
# k4mobidedrm.py # k4mobidedrm.py
# Copyright © 2008-2019 by Apprentice Harper et al. # Copyright © 2008-2020 by Apprentice Harper et al.
__license__ = 'GPL v3' __license__ = 'GPL v3'
__version__ = '5.7' __version__ = '6.0'
# Engine to remove drm from Kindle and Mobipocket ebooks # Engine to remove drm from Kindle and Mobipocket ebooks
# for personal use for archiving and converting your 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.5 - Added GPL v3 licence explicitly.
# 5.6 - Invoke KFXZipBook to handle zipped KFX files # 5.6 - Invoke KFXZipBook to handle zipped KFX files
# 5.7 - Revamp cleanup_name # 5.7 - Revamp cleanup_name
# 6.0 - Added Python 3 compatibility for calibre 5.0
import sys, os, re import sys, os, re
import csv import csv
@@ -69,7 +69,7 @@ import getopt
import re import re
import traceback import traceback
import time import time
import htmlentitydefs import html.entities
import json import json
class DrmException(Exception): class DrmException(Exception):
@@ -103,10 +103,17 @@ class SafeUnbuffered:
if self.encoding == None: if self.encoding == None:
self.encoding = "utf-8" self.encoding = "utf-8"
def write(self, data): def write(self, data):
if isinstance(data,unicode): if isinstance(data,str) or isinstance(data,unicode):
# str for Python3, unicode for Python2
data = data.encode(self.encoding,"replace") data = data.encode(self.encoding,"replace")
self.stream.write(data) try:
self.stream.flush() buffer = getattr(self.stream, 'buffer', self.stream)
# self.stream.buffer for Python3, self.stream for Python2
buffer.write(data)
buffer.flush()
except:
# We can do nothing if a write fails
raise
def __getattr__(self, attr): def __getattr__(self, attr):
return getattr(self.stream, attr) return getattr(self.stream, attr)
@@ -141,15 +148,13 @@ def unicode_argv():
# Remove Python executable and commands if present # Remove Python executable and commands if present
start = argc.value - len(sys.argv) start = argc.value - len(sys.argv)
return [argv[i] for i in return [argv[i] for i in
xrange(start, argc.value)] range(start, argc.value)]
# if we don't have any arguments at all, just pass back script name # if we don't have any arguments at all, just pass back script name
# this should never happen # this should never happen
return [u"mobidedrm.py"] return ["mobidedrm.py"]
else: else:
argvencoding = sys.stdin.encoding argvencoding = sys.stdin.encoding or "utf-8"
if argvencoding == None: return [arg if (isinstance(arg, str) or isinstance(arg,unicode)) else str(arg, argvencoding) for arg in sys.argv]
argvencoding = "utf-8"
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
# cleanup unicode filenames # cleanup unicode filenames
# borrowed from calibre from calibre/src/calibre/__init__.py # borrowed from calibre from calibre/src/calibre/__init__.py
@@ -159,60 +164,60 @@ def unicode_argv():
# and some improvements suggested by jhaisley # and some improvements suggested by jhaisley
def cleanup_name(name): def cleanup_name(name):
# substitute filename unfriendly characters # 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 # 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 # 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 # 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 # remove leading dots
while len(name)>0 and name[0] == u".": while len(name)>0 and name[0] == ".":
name = name[1:] name = name[1:]
# remove trailing dots (Windows doesn't like them) # remove trailing dots (Windows doesn't like them)
while name.endswith(u'.'): while name.endswith("."):
name = name[:-1] name = name[:-1]
if len(name)==0: if len(name)==0:
name=u"DecryptedBook" name="DecryptedBook"
return name return name
# must be passed unicode # must be passed unicode
def unescape(text): def unescape(text):
def fixup(m): def fixup(m):
text = m.group(0) text = m.group(0)
if text[:2] == u"&#": if text[:2] == "&#":
# character reference # character reference
try: try:
if text[:3] == u"&#x": if text[:3] == "&#x":
return unichr(int(text[3:-1], 16)) return chr(int(text[3:-1], 16))
else: else:
return unichr(int(text[2:-1])) return chr(int(text[2:-1]))
except ValueError: except ValueError:
pass pass
else: else:
# named entity # named entity
try: try:
text = unichr(htmlentitydefs.name2codepoint[text[1:-1]]) text = chr(html.entities.name2codepoint[text[1:-1]])
except KeyError: except KeyError:
pass pass
return text # leave as is return text # leave as is
return re.sub(u"&#?\w+;", fixup, text) return re.sub("&#?\\w+;", fixup, text)
def GetDecryptedBook(infile, kDatabases, androidFiles, serials, pids, starttime = time.time()): def GetDecryptedBook(infile, kDatabases, androidFiles, serials, pids, starttime = time.time()):
# handle the obvious cases at the beginning # handle the obvious cases at the beginning
if not os.path.isfile(infile): if not os.path.isfile(infile):
raise DrmException(u"Input file does not exist.") raise DrmException("Input file does not exist.")
mobi = True mobi = True
magic8 = open(infile,'rb').read(8) magic8 = open(infile,'rb').read(8)
if magic8 == '\xeaDRMION\xee': if magic8 == b'\xeaDRMION\xee':
raise DrmException(u"The .kfx DRMION file cannot be decrypted by itself. A .kfx-zip archive containing a DRM voucher is required.") raise DrmException("The .kfx DRMION file cannot be decrypted by itself. A .kfx-zip archive containing a DRM voucher is required.")
magic3 = magic8[:3] magic3 = magic8[:3]
if magic3 == 'TPZ': if magic3 == b'TPZ':
mobi = False mobi = False
if magic8[:4] == 'PK\x03\x04': if magic8[:4] == b'PK\x03\x04':
mb = kfxdedrm.KFXZipBook(infile) mb = kfxdedrm.KFXZipBook(infile)
elif mobi: elif mobi:
mb = mobidedrm.MobiBook(infile) mb = mobidedrm.MobiBook(infile)
@@ -220,7 +225,7 @@ def GetDecryptedBook(infile, kDatabases, androidFiles, serials, pids, starttime
mb = topazextract.TopazBook(infile) mb = topazextract.TopazBook(infile)
bookname = unescape(mb.getBookTitle()) bookname = unescape(mb.getBookTitle())
print u"Decrypting {1} ebook: {0}".format(bookname, mb.getBookType()) print("Decrypting {1} ebook: {0}".format(bookname, mb.getBookType()))
# copy list of pids # copy list of pids
totalpids = list(pids) totalpids = list(pids)
@@ -232,7 +237,7 @@ def GetDecryptedBook(infile, kDatabases, androidFiles, serials, pids, starttime
totalpids.extend(kgenpids.getPidList(md1, md2, serials, kDatabases)) totalpids.extend(kgenpids.getPidList(md1, md2, serials, kDatabases))
# remove any duplicates # remove any duplicates
totalpids = list(set(totalpids)) 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 #print totalpids
try: try:
@@ -241,7 +246,7 @@ def GetDecryptedBook(infile, kDatabases, androidFiles, serials, pids, starttime
mb.cleanup mb.cleanup
raise 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 return mb
@@ -255,16 +260,16 @@ def decryptBook(infile, outdir, kDatabaseFiles, androidFiles, serials, pids):
with open(dbfile, 'r') as keyfilein: with open(dbfile, 'r') as keyfilein:
kindleDatabase = json.loads(keyfilein.read()) kindleDatabase = json.loads(keyfilein.read())
kDatabases.append([dbfile,kindleDatabase]) kDatabases.append([dbfile,kindleDatabase])
except Exception, e: except Exception as e:
print u"Error getting database from file {0:s}: {1:s}".format(dbfile,e) print("Error getting database from file {0:s}: {1:s}".format(dbfile,e))
traceback.print_exc() traceback.print_exc()
try: try:
book = GetDecryptedBook(infile, kDatabases, androidFiles, serials, pids, starttime) book = GetDecryptedBook(infile, kDatabases, androidFiles, serials, pids, starttime)
except Exception, e: except Exception as e:
print u"Error decrypting book after {1:.1f} seconds: {0}".format(e.args[0],time.time()-starttime) print("Error decrypting book after {1:.1f} seconds: {0}".format(e.args[0],time.time()-starttime))
traceback.print_exc() traceback.print_exc()
return 1 return 1
@@ -275,7 +280,7 @@ def decryptBook(infile, outdir, kDatabaseFiles, androidFiles, serials, pids):
re.match('^{0-9A-F-}{36}$', orig_fn_root) re.match('^{0-9A-F-}{36}$', orig_fn_root)
): # Kindle for PC / Mac / Android / Fire / iOS ): # Kindle for PC / Mac / Android / Fire / iOS
clean_title = cleanup_name(book.getBookTitle()) clean_title = cleanup_name(book.getBookTitle())
outfilename = u'{}_{}'.format(orig_fn_root, clean_title) outfilename = "{}_{}".format(orig_fn_root, clean_title)
else: # E Ink Kindle, which already uses a reasonable name else: # E Ink Kindle, which already uses a reasonable name
outfilename = orig_fn_root outfilename = orig_fn_root
@@ -283,16 +288,16 @@ def decryptBook(infile, outdir, kDatabaseFiles, androidFiles, serials, pids):
if len(outfilename)>150: if len(outfilename)>150:
outfilename = outfilename[:99]+"--"+outfilename[-49:] outfilename = outfilename[:99]+"--"+outfilename[-49:]
outfilename = outfilename+u"_nodrm" outfilename = outfilename+"_nodrm"
outfile = os.path.join(outdir, outfilename + book.getBookExtension()) outfile = os.path.join(outdir, outfilename + book.getBookExtension())
book.getFile(outfile) 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": if book.getBookType()=="Topaz":
zipname = os.path.join(outdir, outfilename + u"_SVG.zip") zipname = os.path.join(outdir, outfilename + "_SVG.zip")
book.getSVGZip(zipname) 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 # remove internal temporary directory of Topaz pieces
book.cleanup() book.cleanup()
@@ -300,9 +305,9 @@ def decryptBook(infile, outdir, kDatabaseFiles, androidFiles, serials, pids):
def usage(progname): def usage(progname):
print u"Removes DRM protection from Mobipocket, Amazon KF8, Amazon Print Replica and Amazon Topaz ebooks" print("Removes DRM protection from Mobipocket, Amazon KF8, Amazon Print Replica and Amazon Topaz ebooks")
print u"Usage:" print("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(" {0} [-k <kindle.k4i>] [-p <comma separated PIDs>] [-s <comma separated Kindle serial numbers>] [ -a <AmazonSecureStorage.xml|backup.ab> ] <infile> <outdir>".format(progname))
# #
# Main # Main
@@ -310,12 +315,12 @@ def usage(progname):
def cli_main(): def cli_main():
argv=unicode_argv() argv=unicode_argv()
progname = os.path.basename(argv[0]) 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: try:
opts, args = getopt.getopt(argv[1:], "k:p:s:a:") opts, args = getopt.getopt(argv[1:], "k:p:s:a:h")
except getopt.GetoptError, err: except getopt.GetoptError as err:
print u"Error in options or arguments: {0}".format(err.args[0]) print("Error in options or arguments: {0}".format(err.args[0]))
usage(progname) usage(progname)
sys.exit(2) sys.exit(2)
if len(args)<2: if len(args)<2:
@@ -330,6 +335,9 @@ def cli_main():
pids = [] pids = []
for o, a in opts: for o, a in opts:
if o == "-h":
usage(progname)
sys.exit(0)
if o == "-k": if o == "-k":
if a == None : if a == None :
raise DrmException("Invalid parameter for -k") raise DrmException("Invalid parameter for -k")
@@ -337,7 +345,7 @@ def cli_main():
if o == "-p": if o == "-p":
if a == None : if a == None :
raise DrmException("Invalid parameter for -p") raise DrmException("Invalid parameter for -p")
pids = a.split(',') pids = a.encode('utf-8').split(b',')
if o == "-s": if o == "-s":
if a == None : if a == None :
raise DrmException("Invalid parameter for -s") raise DrmException("Invalid parameter for -s")
@@ -347,9 +355,6 @@ def cli_main():
raise DrmException("Invalid parameter for -a") raise DrmException("Invalid parameter for -a")
androidFiles.append(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) return decryptBook(infile, outdir, kDatabaseFiles, androidFiles, serials, pids)

View File

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

View File

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

View File

@@ -1,13 +1,11 @@
#!/usr/bin/env python #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import with_statement
# kindlekey.py # kindlekey.py
# Copyright © 2008-2020 Apprentice Harper et al. # Copyright © 2008-2020 Apprentice Harper et al.
__license__ = 'GPL v3' __license__ = 'GPL v3'
__version__ = '2.8' __version__ = '3.0'
# Revision history: # Revision history:
# 1.0 - Kindle info file decryption, extracted from k4mobidedrm, etc. # 1.0 - Kindle info file decryption, extracted from k4mobidedrm, etc.
@@ -31,6 +29,7 @@ __version__ = '2.8'
# 2.6 - Start adding support for Kindle 1.25+ .kinf2018 file # 2.6 - Start adding support for Kindle 1.25+ .kinf2018 file
# 2.7 - Finish .kinf2018 support, PC & Mac by Apprentice Sakuya # 2.7 - Finish .kinf2018 support, PC & Mac by Apprentice Sakuya
# 2.8 - Fix for Mac OS X Big Sur # 2.8 - Fix for Mac OS X Big Sur
# 3.0 - Python 3 for calibre 5.0
""" """
@@ -38,9 +37,11 @@ Retrieve Kindle for PC/Mac user key.
""" """
import sys, os, re import sys, os, re
from struct import pack, unpack import codecs
from struct import pack, unpack, unpack_from
import json import json
import getopt import getopt
import traceback
try: try:
RegError RegError
@@ -60,10 +61,17 @@ class SafeUnbuffered:
if self.encoding == None: if self.encoding == None:
self.encoding = "utf-8" self.encoding = "utf-8"
def write(self, data): def write(self, data):
if isinstance(data,unicode): if isinstance(data,str) or isinstance(data,unicode):
# str for Python3, unicode for Python2
data = data.encode(self.encoding,"replace") data = data.encode(self.encoding,"replace")
self.stream.write(data) try:
self.stream.flush() buffer = getattr(self.stream, 'buffer', self.stream)
# self.stream.buffer for Python3, self.stream for Python2
buffer.write(data)
buffer.flush()
except:
# We can do nothing if a write fails
raise
def __getattr__(self, attr): def __getattr__(self, attr):
return getattr(self.stream, attr) return getattr(self.stream, attr)
@@ -101,15 +109,13 @@ def unicode_argv():
# Remove Python executable and commands if present # Remove Python executable and commands if present
start = argc.value - len(sys.argv) start = argc.value - len(sys.argv)
return [argv[i] for i in return [argv[i] for i in
xrange(start, argc.value)] range(start, argc.value)]
# if we don't have any arguments at all, just pass back script name # if we don't have any arguments at all, just pass back script name
# this should never happen # this should never happen
return [u"kindlekey.py"] return ["kindlekey.py"]
else: else:
argvencoding = sys.stdin.encoding argvencoding = sys.stdin.encoding or "utf-8"
if argvencoding == None: return [arg if (isinstance(arg, str) or isinstance(arg,unicode)) else str(arg, argvencoding) for arg in sys.argv]
argvencoding = "utf-8"
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
class DrmException(Exception): class DrmException(Exception):
pass pass
@@ -156,14 +162,15 @@ def primes(n):
return primeList return primeList
# Encode the bytes in data with the characters in map # Encode the bytes in data with the characters in map
# data and map should be byte arrays
def encode(data, map): def encode(data, map):
result = '' result = b''
for char in data: for char in data:
value = ord(char) value = char
Q = (value ^ 0x80) // len(map) Q = (value ^ 0x80) // len(map)
R = value % len(map) R = value % len(map)
result += map[Q] result += bytes([map[Q]])
result += map[R] result += bytes([map[R]])
return result return result
# Hash the bytes in data and then encode the digest with the characters in map # Hash the bytes in data and then encode the digest with the characters in map
@@ -172,7 +179,7 @@ def encodeHash(data,map):
# Decode the string in data with the characters in map. Returns the decoded bytes # Decode the string in data with the characters in map. Returns the decoded bytes
def decode(data,map): def decode(data,map):
result = '' result = b''
for i in range (0,len(data)-1,2): for i in range (0,len(data)-1,2):
high = map.find(data[i]) high = map.find(data[i])
low = map.find(data[i+1]) low = map.find(data[i+1])
@@ -188,7 +195,11 @@ if iswindows:
create_unicode_buffer, create_string_buffer, CFUNCTYPE, addressof, \ create_unicode_buffer, create_string_buffer, CFUNCTYPE, addressof, \
string_at, Structure, c_void_p, cast string_at, Structure, c_void_p, cast
import _winreg as winreg try:
import winreg
except ImportError:
import _winreg as winreg
MAX_PATH = 255 MAX_PATH = 255
kernel32 = windll.kernel32 kernel32 = windll.kernel32
advapi32 = windll.advapi32 advapi32 = windll.advapi32
@@ -232,20 +243,12 @@ if iswindows:
class DecryptNotBlockAlignedError(DecryptError): class DecryptNotBlockAlignedError(DecryptError):
""" Error in decryption processing """ """ Error in decryption processing """
def xorS(a,b):
""" XOR two strings """
assert len(a)==len(b)
x = []
for i in range(len(a)):
x.append( chr(ord(a[i])^ord(b[i])))
return ''.join(x)
def xor(a,b): def xor(a,b):
""" XOR two strings """ """ XOR two byte arrays, to lesser length """
x = [] x = []
for i in range(min(len(a),len(b))): for i in range(min(len(a),len(b))):
x.append( chr(ord(a[i])^ord(b[i]))) x.append( a[i] ^ b[i])
return ''.join(x) return bytes(x)
""" """
Base 'BlockCipher' and Pad classes for cipher instances. Base 'BlockCipher' and Pad classes for cipher instances.
@@ -264,10 +267,10 @@ if iswindows:
self.resetDecrypt() self.resetDecrypt()
def resetEncrypt(self): def resetEncrypt(self):
self.encryptBlockCount = 0 self.encryptBlockCount = 0
self.bytesToEncrypt = '' self.bytesToEncrypt = b''
def resetDecrypt(self): def resetDecrypt(self):
self.decryptBlockCount = 0 self.decryptBlockCount = 0
self.bytesToDecrypt = '' self.bytesToDecrypt = b''
def encrypt(self, plainText, more = None): def encrypt(self, plainText, more = None):
""" Encrypt a string and return a binary string """ """ Encrypt a string and return a binary string """
@@ -300,14 +303,14 @@ if iswindows:
numBlocks, numExtraBytes = divmod(len(self.bytesToDecrypt), self.blockSize) numBlocks, numExtraBytes = divmod(len(self.bytesToDecrypt), self.blockSize)
if more == None: # no more calls to decrypt, should have all the data if more == None: # no more calls to decrypt, should have all the data
if numExtraBytes != 0: if numExtraBytes != 0:
raise DecryptNotBlockAlignedError, 'Data not block aligned on decrypt' raise DecryptNotBlockAlignedError('Data not block aligned on decrypt')
# hold back some bytes in case last decrypt has zero len # hold back some bytes in case last decrypt has zero len
if (more != None) and (numExtraBytes == 0) and (numBlocks >0) : if (more != None) and (numExtraBytes == 0) and (numBlocks >0) :
numBlocks -= 1 numBlocks -= 1
numExtraBytes = self.blockSize numExtraBytes = self.blockSize
plainText = '' plainText = b''
for i in range(numBlocks): for i in range(numBlocks):
bStart = i*self.blockSize bStart = i*self.blockSize
ptBlock = self.decryptBlock(self.bytesToDecrypt[bStart : bStart+self.blockSize]) ptBlock = self.decryptBlock(self.bytesToDecrypt[bStart : bStart+self.blockSize])
@@ -342,7 +345,7 @@ if iswindows:
def removePad(self, paddedBinaryString, blockSize): def removePad(self, paddedBinaryString, blockSize):
""" Remove padding from a binary string """ """ Remove padding from a binary string """
if not(0<len(paddedBinaryString)): if not(0<len(paddedBinaryString)):
raise DecryptNotBlockAlignedError, 'Expected More Data' raise DecryptNotBlockAlignedError('Expected More Data')
return paddedBinaryString[:-ord(paddedBinaryString[-1])] return paddedBinaryString[:-ord(paddedBinaryString[-1])]
class noPadding(Pad): class noPadding(Pad):
@@ -372,11 +375,11 @@ if iswindows:
self.blockSize = blockSize # blockSize is in bytes self.blockSize = blockSize # blockSize is in bytes
self.padding = padding # change default to noPadding() to get normal ECB behavior self.padding = padding # change default to noPadding() to get normal ECB behavior
assert( keySize%4==0 and NrTable[4].has_key(keySize/4)),'key size must be 16,20,24,29 or 32 bytes' assert( keySize%4==0 and (keySize//4) in NrTable[4]),'key size must be 16,20,24,29 or 32 bytes'
assert( blockSize%4==0 and NrTable.has_key(blockSize/4)), 'block size must be 16,20,24,29 or 32 bytes' assert( blockSize%4==0 and (blockSize//4) in NrTable), 'block size must be 16,20,24,29 or 32 bytes'
self.Nb = self.blockSize/4 # Nb is number of columns of 32 bit words self.Nb = self.blockSize//4 # Nb is number of columns of 32 bit words
self.Nk = keySize/4 # Nk is the key length in 32-bit words self.Nk = keySize//4 # Nk is the key length in 32-bit words
self.Nr = NrTable[self.Nb][self.Nk] # The number of rounds (Nr) is a function of self.Nr = NrTable[self.Nb][self.Nk] # The number of rounds (Nr) is a function of
# the block (Nb) and key (Nk) sizes. # the block (Nb) and key (Nk) sizes.
if key != None: if key != None:
@@ -420,15 +423,15 @@ if iswindows:
def _toBlock(self, bs): def _toBlock(self, bs):
""" Convert binary string to array of bytes, state[col][row]""" """ Convert binary string to array of bytes, state[col][row]"""
assert ( len(bs) == 4*self.Nb ), 'Rijndarl blocks must be of size blockSize' assert ( len(bs) == 4*self.Nb ), 'Rijndarl blocks must be of size blockSize'
return [[ord(bs[4*i]),ord(bs[4*i+1]),ord(bs[4*i+2]),ord(bs[4*i+3])] for i in range(self.Nb)] return [[bs[4*i],bs[4*i+1],bs[4*i+2],bs[4*i+3]] for i in range(self.Nb)]
def _toBString(self, block): def _toBString(self, block):
""" Convert block (array of bytes) to binary string """ """ Convert block (array of bytes) to binary string """
l = [] l = []
for col in block: for col in block:
for rowElement in col: for rowElement in col:
l.append(chr(rowElement)) l.append(rowElement)
return ''.join(l) return bytes(l)
#------------------------------------- #-------------------------------------
""" Number of rounds Nr = NrTable[Nb][Nk] """ Number of rounds Nr = NrTable[Nb][Nk]
@@ -440,17 +443,16 @@ if iswindows:
7: {4:13, 5:13, 6:13, 7:13, 8:14}, 7: {4:13, 5:13, 6:13, 7:13, 8:14},
8: {4:14, 5:14, 6:14, 7:14, 8:14}} 8: {4:14, 5:14, 6:14, 7:14, 8:14}}
#------------------------------------- #-------------------------------------
def keyExpansion(algInstance, keyString): def keyExpansion(algInstance, keyArray):
""" Expand a string of size keySize into a larger array """ """ Expand a byte array of size keySize into a larger array """
Nk, Nb, Nr = algInstance.Nk, algInstance.Nb, algInstance.Nr # for readability Nk, Nb, Nr = algInstance.Nk, algInstance.Nb, algInstance.Nr # for readability
key = [ord(byte) for byte in keyString] # convert string to list w = [[keyArray[4*i],keyArray[4*i+1],keyArray[4*i+2],keyArray[4*i+3]] for i in range(Nk)]
w = [[key[4*i],key[4*i+1],key[4*i+2],key[4*i+3]] for i in range(Nk)]
for i in range(Nk,Nb*(Nr+1)): for i in range(Nk,Nb*(Nr+1)):
temp = w[i-1] # a four byte column temp = w[i-1] # a four byte column
if (i%Nk) == 0 : if (i%Nk) == 0 :
temp = temp[1:]+[temp[0]] # RotWord(temp) temp = temp[1:]+[temp[0]] # RotWord(temp)
temp = [ Sbox[byte] for byte in temp ] temp = [ Sbox[byte] for byte in temp ]
temp[0] ^= Rcon[i/Nk] temp[0] ^= Rcon[i//Nk]
elif Nk > 6 and i%Nk == 4 : elif Nk > 6 and i%Nk == 4 :
temp = [ Sbox[byte] for byte in temp ] # SubWord(temp) temp = [ Sbox[byte] for byte in temp ] # SubWord(temp)
w.append( [ w[i-Nk][byte]^temp[byte] for byte in range(4) ] ) w.append( [ w[i-Nk][byte]^temp[byte] for byte in range(4) ] )
@@ -650,7 +652,7 @@ if iswindows:
def __init__(self, key = None, padding = padWithPadLen(), keySize=16): def __init__(self, key = None, padding = padWithPadLen(), keySize=16):
""" Initialize AES, keySize is in bytes """ """ Initialize AES, keySize is in bytes """
if not (keySize == 16 or keySize == 24 or keySize == 32) : if not (keySize == 16 or keySize == 24 or keySize == 32) :
raise BadKeySizeError, 'Illegal AES key size, must be 16, 24, or 32 bytes' raise BadKeySizeError('Illegal AES key size, must be 16, 24, or 32 bytes')
Rijndael.__init__( self, key, padding=padding, keySize=keySize, blockSize=16 ) Rijndael.__init__( self, key, padding=padding, keySize=keySize, blockSize=16 )
@@ -742,7 +744,7 @@ if iswindows:
if self.decryptBlockCount == 0: # first call, process IV if self.decryptBlockCount == 0: # first call, process IV
if self.iv == None: # auto decrypt IV? if self.iv == None: # auto decrypt IV?
self.prior_CT_block = encryptedBlock self.prior_CT_block = encryptedBlock
return '' return b''
else: else:
assert(len(self.iv)==self.blockSize),"Bad IV size on CBC decryption" assert(len(self.iv)==self.blockSize),"Bad IV size on CBC decryption"
self.prior_CT_block = self.iv self.prior_CT_block = self.iv
@@ -790,10 +792,10 @@ if iswindows:
# [c_char_p, c_ulong, c_char_p, c_ulong, c_ulong, c_ulong, c_char_p]) # [c_char_p, c_ulong, c_char_p, c_ulong, c_ulong, c_ulong, c_char_p])
def pbkdf2(self, passwd, salt, iter, keylen): def pbkdf2(self, passwd, salt, iter, keylen):
def xorstr( a, b ): def xorbytes( a, b ):
if len(a) != len(b): if len(a) != len(b):
raise Exception("xorstr(): lengths differ") raise Exception("xorbytes(): lengths differ")
return ''.join((chr(ord(x)^ord(y)) for x, y in zip(a, b))) return bytes([x ^ y for x, y in zip(a, b)])
def prf( h, data ): def prf( h, data ):
hm = h.copy() hm = h.copy()
@@ -805,24 +807,24 @@ if iswindows:
T = U T = U
for i in range(2, itercount+1): for i in range(2, itercount+1):
U = prf( h, U ) U = prf( h, U )
T = xorstr( T, U ) T = xorbytes( T, U )
return T return T
sha = hashlib.sha1 sha = hashlib.sha1
digest_size = sha().digest_size digest_size = sha().digest_size
# l - number of output blocks to produce # l - number of output blocks to produce
l = keylen / digest_size l = keylen // digest_size
if keylen % digest_size != 0: if keylen % digest_size != 0:
l += 1 l += 1
h = hmac.new( passwd, None, sha ) h = hmac.new( passwd, None, sha )
T = "" T = b""
for i in range(1, l+1): for i in range(1, l+1):
T += pbkdf2_F( h, salt, iter, i ) T += pbkdf2_F( h, salt, iter, i )
return T[0: keylen] return T[0: keylen]
def UnprotectHeaderData(encryptedData): def UnprotectHeaderData(encryptedData):
passwdData = 'header_key_data' passwdData = b'header_key_data'
salt = 'HEADER.2011' salt = b'HEADER.2011'
iter = 0x80 iter = 0x80
keylen = 0x100 keylen = 0x100
key_iv = KeyIVGen().pbkdf2(passwdData, salt, iter, keylen) key_iv = KeyIVGen().pbkdf2(passwdData, salt, iter, keylen)
@@ -835,12 +837,12 @@ if iswindows:
# Various character maps used to decrypt kindle info values. # Various character maps used to decrypt kindle info values.
# Probably supposed to act as obfuscation # Probably supposed to act as obfuscation
charMap2 = "AaZzB0bYyCc1XxDdW2wEeVv3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_" charMap2 = b"AaZzB0bYyCc1XxDdW2wEeVv3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_"
charMap5 = "AzB0bYyCeVvaZ3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_c1XxDdW2wE" charMap5 = b"AzB0bYyCeVvaZ3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_c1XxDdW2wE"
# New maps in K4PC 1.9.0 # New maps in K4PC 1.9.0
testMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M" testMap1 = b"n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M"
testMap6 = "9YzAb0Cd1Ef2n5Pr6St7Uvh3Jk4M8WxG" testMap6 = b"9YzAb0Cd1Ef2n5Pr6St7Uvh3Jk4M8WxG"
testMap8 = "YvaZ3FfUm9Nn_c1XuG4yCAzB0beVg-TtHh5SsIiR6rJjQdW2wEq7KkPpL8lOoMxD" testMap8 = b"YvaZ3FfUm9Nn_c1XuG4yCAzB0beVg-TtHh5SsIiR6rJjQdW2wEq7KkPpL8lOoMxD"
# interface with Windows OS Routines # interface with Windows OS Routines
class DataBlob(Structure): class DataBlob(Structure):
@@ -904,12 +906,12 @@ if iswindows:
size.value = len(buffer) size.value = len(buffer)
# replace any non-ASCII values with 0xfffd # replace any non-ASCII values with 0xfffd
for i in xrange(0,len(buffer)): for i in range(0,len(buffer)):
if buffer[i]>u"\u007f": if buffer[i]>"\u007f":
#print u"swapping char "+str(i)+" ("+buffer[i]+")" #print "swapping char "+str(i)+" ("+buffer[i]+")"
buffer[i] = u"\ufffd" buffer[i] = "\ufffd"
# return utf-8 encoding of modified username # return utf-8 encoding of modified username
#print u"modified username:"+buffer.value #print "modified username:"+buffer.value
return buffer.value.encode('utf-8') return buffer.value.encode('utf-8')
return GetUserName return GetUserName
GetUserName = GetUserName() GetUserName = GetUserName()
@@ -928,19 +930,19 @@ if iswindows:
if not _CryptUnprotectData(byref(indata), None, byref(entropy), if not _CryptUnprotectData(byref(indata), None, byref(entropy),
None, None, flags, byref(outdata)): None, None, flags, byref(outdata)):
# raise DrmException("Failed to Unprotect Data") # raise DrmException("Failed to Unprotect Data")
return 'failed' return b'failed'
return string_at(outdata.pbData, outdata.cbData) return string_at(outdata.pbData, outdata.cbData)
return CryptUnprotectData return CryptUnprotectData
CryptUnprotectData = CryptUnprotectData() CryptUnprotectData = CryptUnprotectData()
# Returns Environmental Variables that contain unicode # Returns Environmental Variables that contain unicode
# name must be unicode string, not byte string.
def getEnvironmentVariable(name): def getEnvironmentVariable(name):
import ctypes import ctypes
name = unicode(name) # make sure string argument is unicode
n = ctypes.windll.kernel32.GetEnvironmentVariableW(name, None, 0) n = ctypes.windll.kernel32.GetEnvironmentVariableW(name, None, 0)
if n == 0: if n == 0:
return None return None
buf = ctypes.create_unicode_buffer(u'\0'*n) buf = ctypes.create_unicode_buffer("\0"*n)
ctypes.windll.kernel32.GetEnvironmentVariableW(name, buf, n) ctypes.windll.kernel32.GetEnvironmentVariableW(name, buf, n)
return buf.value return buf.value
@@ -952,7 +954,7 @@ if iswindows:
path = "" path = ""
if 'LOCALAPPDATA' in os.environ.keys(): if 'LOCALAPPDATA' in os.environ.keys():
# Python 2.x does not return unicode env. Use Python 3.x # Python 2.x does not return unicode env. Use Python 3.x
path = winreg.ExpandEnvironmentStrings(u"%LOCALAPPDATA%") path = winreg.ExpandEnvironmentStrings("%LOCALAPPDATA%")
# this is just another alternative. # this is just another alternative.
# path = getEnvironmentVariable('LOCALAPPDATA') # path = getEnvironmentVariable('LOCALAPPDATA')
if not os.path.isdir(path): if not os.path.isdir(path):
@@ -980,20 +982,20 @@ if iswindows:
print ('Could not find the folder in which to look for kinfoFiles.') print ('Could not find the folder in which to look for kinfoFiles.')
else: else:
# Probably not the best. To Fix (shouldn't ignore in encoding) or use utf-8 # Probably not the best. To Fix (shouldn't ignore in encoding) or use utf-8
print(u'searching for kinfoFiles in ' + path.encode('ascii', 'ignore')) print("searching for kinfoFiles in " + path)
# look for (K4PC 1.25.1 and later) .kinf2018 file # look for (K4PC 1.25.1 and later) .kinf2018 file
kinfopath = path +'\\Amazon\\Kindle\\storage\\.kinf2018' kinfopath = path +'\\Amazon\\Kindle\\storage\\.kinf2018'
if os.path.isfile(kinfopath): if os.path.isfile(kinfopath):
found = True found = True
print('Found K4PC 1.25+ kinf2018 file: ' + kinfopath.encode('ascii','ignore')) print('Found K4PC 1.25+ kinf2018 file: ' + kinfopath)
kInfoFiles.append(kinfopath) kInfoFiles.append(kinfopath)
# look for (K4PC 1.9.0 and later) .kinf2011 file # look for (K4PC 1.9.0 and later) .kinf2011 file
kinfopath = path +'\\Amazon\\Kindle\\storage\\.kinf2011' kinfopath = path +'\\Amazon\\Kindle\\storage\\.kinf2011'
if os.path.isfile(kinfopath): if os.path.isfile(kinfopath):
found = True found = True
print('Found K4PC 1.9+ kinf2011 file: ' + kinfopath.encode('ascii','ignore')) print('Found K4PC 1.9+ kinf2011 file: ' + kinfopath)
kInfoFiles.append(kinfopath) kInfoFiles.append(kinfopath)
# look for (K4PC 1.6.0 and later) rainier.2.1.1.kinf file # look for (K4PC 1.6.0 and later) rainier.2.1.1.kinf file
@@ -1026,29 +1028,31 @@ if iswindows:
# database of keynames and values # database of keynames and values
def getDBfromFile(kInfoFile): def getDBfromFile(kInfoFile):
names = [\ names = [\
'kindle.account.tokens',\ b'kindle.account.tokens',\
'kindle.cookie.item',\ b'kindle.cookie.item',\
'eulaVersionAccepted',\ b'eulaVersionAccepted',\
'login_date',\ b'login_date',\
'kindle.token.item',\ b'kindle.token.item',\
'login',\ b'login',\
'kindle.key.item',\ b'kindle.key.item',\
'kindle.name.info',\ b'kindle.name.info',\
'kindle.device.info',\ b'kindle.device.info',\
'MazamaRandomNumber',\ b'MazamaRandomNumber',\
'max_date',\ b'max_date',\
'SIGVERIF',\ b'SIGVERIF',\
'build_version',\ b'build_version',\
'SerialNumber',\ b'SerialNumber',\
'UsernameHash',\ b'UsernameHash',\
'kindle.directedid.info',\ b'kindle.directedid.info',\
'DSN',\ b'DSN',\
'kindle.accounttype.info',\ b'kindle.accounttype.info',\
'krx.flashcardsplugin.data.encryption_key',\ b'krx.flashcardsplugin.data.encryption_key',\
'krx.notebookexportplugin.data.encryption_key',\ b'krx.notebookexportplugin.data.encryption_key',\
'proxy.http.password',\ b'proxy.http.password',\
'proxy.http.username' b'proxy.http.username'
] ]
namehashmap = {encodeHash(n,testMap8):n for n in names}
# print(namehashmap)
DB = {} DB = {}
with open(kInfoFile, 'rb') as infoReader: with open(kInfoFile, 'rb') as infoReader:
data = infoReader.read() data = infoReader.read()
@@ -1056,7 +1060,7 @@ if iswindows:
# the .kinf file uses "/" to separate it into records # the .kinf file uses "/" to separate it into records
# so remove the trailing "/" to make it easy to use split # so remove the trailing "/" to make it easy to use split
data = data[:-1] data = data[:-1]
items = data.split('/') items = data.split(b'/')
# starts with an encoded and encrypted header blob # starts with an encoded and encrypted header blob
headerblob = items.pop(0) headerblob = items.pop(0)
@@ -1064,7 +1068,7 @@ if iswindows:
cleartext = UnprotectHeaderData(encryptedValue) cleartext = UnprotectHeaderData(encryptedValue)
#print "header cleartext:",cleartext #print "header cleartext:",cleartext
# now extract the pieces that form the added entropy # now extract the pieces that form the added entropy
pattern = re.compile(r'''\[Version:(\d+)\]\[Build:(\d+)\]\[Cksum:([^\]]+)\]\[Guid:([\{\}a-z0-9\-]+)\]''', re.IGNORECASE) pattern = re.compile(br'''\[Version:(\d+)\]\[Build:(\d+)\]\[Cksum:([^\]]+)\]\[Guid:([\{\}a-z0-9\-]+)\]''', re.IGNORECASE)
for m in re.finditer(pattern, cleartext): for m in re.finditer(pattern, cleartext):
version = int(m.group(1)) version = int(m.group(1))
build = m.group(2) build = m.group(2)
@@ -1073,8 +1077,8 @@ if iswindows:
if version == 5: # .kinf2011 if version == 5: # .kinf2011
added_entropy = build + guid added_entropy = build + guid
elif version == 6: # .kinf2018 elif version == 6: # .kinf2018
salt = str(0x6d8 * int(build)) + guid salt = str(0x6d8 * int(build)).encode('utf-8') + guid
sp = GetUserName() + '+@#$%+' + GetIDString() sp = GetUserName() + b'+@#$%+' + GetIDString().encode('utf-8')
passwd = encode(SHA256(sp), charMap5) passwd = encode(SHA256(sp), charMap5)
key = KeyIVGen().pbkdf2(passwd, salt, 10000, 0x400)[:32] # this is very slow key = KeyIVGen().pbkdf2(passwd, salt, 10000, 0x400)[:32] # this is very slow
@@ -1098,18 +1102,15 @@ if iswindows:
# read and store in rcnt records of data # read and store in rcnt records of data
# that make up the contents value # that make up the contents value
edlst = [] edlst = []
for i in xrange(rcnt): for i in range(rcnt):
item = items.pop(0) item = items.pop(0)
edlst.append(item) edlst.append(item)
# key names now use the new testMap8 encoding # key names now use the new testMap8 encoding
keyname = "unknown" if keyhash in namehashmap:
for name in names: keyname=namehashmap[keyhash]
if encodeHash(name,testMap8) == keyhash: #print "keyname found from hash:",keyname
keyname = name else:
#print "keyname found from hash:",keyname
break
if keyname == "unknown":
keyname = keyhash keyname = keyhash
#print "keyname not found, hash is:",keyname #print "keyname not found, hash is:",keyname
@@ -1126,7 +1127,7 @@ if iswindows:
# move first offsets chars to end to align for decode by testMap8 # move first offsets chars to end to align for decode by testMap8
# by moving noffset chars from the start of the # by moving noffset chars from the start of the
# string to the end of the string # string to the end of the string
encdata = "".join(edlst) encdata = b"".join(edlst)
#print "encrypted data:",encdata #print "encrypted data:",encdata
contlen = len(encdata) contlen = len(encdata)
noffset = contlen - primes(int(contlen/3))[-1] noffset = contlen - primes(int(contlen/3))[-1]
@@ -1165,11 +1166,11 @@ if iswindows:
if len(DB)>6: if len(DB)>6:
# store values used in decryption # store values used in decryption
DB['IDString'] = GetIDString() DB[b'IDString'] = GetIDString().encode('utf-8')
DB['UserName'] = GetUserName() DB[b'UserName'] = GetUserName()
print u"Decrypted key file using IDString '{0:s}' and UserName '{1:s}'".format(GetIDString(), GetUserName().encode('hex')) print("Decrypted key file using IDString '{0:s}' and UserName '{1:s}'".format(GetIDString(), GetUserName().decode('utf-8')))
else: else:
print u"Couldn't decrypt file." print("Couldn't decrypt file.")
DB = {} DB = {}
return DB return DB
elif isosx: elif isosx:
@@ -1188,8 +1189,7 @@ elif isosx:
try: try:
libcrypto = CDLL(libcrypto) libcrypto = CDLL(libcrypto)
except Exception as e: except Exception as e:
raise DrmException(u"libcrypto not found: " % e) raise DrmException("libcrypto not found: " % e)
# From OpenSSL's crypto aes header # From OpenSSL's crypto aes header
# #
@@ -1246,14 +1246,14 @@ elif isosx:
def set_decrypt_key(self, userkey, iv): def set_decrypt_key(self, userkey, iv):
self._blocksize = len(userkey) self._blocksize = len(userkey)
if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) : if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) :
raise DrmException(u"AES improper key used") raise DrmException("AES improper key used")
return return
keyctx = self._keyctx = AES_KEY() keyctx = self._keyctx = AES_KEY()
self._iv = iv self._iv = iv
self._userkey = userkey self._userkey = userkey
rv = AES_set_decrypt_key(userkey, len(userkey) * 8, keyctx) rv = AES_set_decrypt_key(userkey, len(userkey) * 8, keyctx)
if rv < 0: if rv < 0:
raise DrmException(u"Failed to initialize AES key") raise DrmException("Failed to initialize AES key")
def decrypt(self, data): def decrypt(self, data):
out = create_string_buffer(len(data)) out = create_string_buffer(len(data))
@@ -1261,7 +1261,7 @@ elif isosx:
keyctx = self._keyctx keyctx = self._keyctx
rv = AES_cbc_encrypt(data, out, len(data), keyctx, mutable_iv, 0) rv = AES_cbc_encrypt(data, out, len(data), keyctx, mutable_iv, 0)
if rv == 0: if rv == 0:
raise DrmException(u"AES decryption failed") raise DrmException("AES decryption failed")
return out.raw return out.raw
def keyivgen(self, passwd, salt, iter, keylen): def keyivgen(self, passwd, salt, iter, keylen):
@@ -1283,8 +1283,8 @@ elif isosx:
LibCrypto = _load_crypto() LibCrypto = _load_crypto()
# Various character maps used to decrypt books. Probably supposed to act as obfuscation # Various character maps used to decrypt books. Probably supposed to act as obfuscation
charMap1 = 'n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M' charMap1 = b'n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M'
charMap2 = 'ZB0bYyc1xDdW2wEV3Ff7KkPpL8UuGA4gz-Tme9Nn_tHh5SvXCsIiR6rJjQaqlOoM' charMap2 = b'ZB0bYyc1xDdW2wEV3Ff7KkPpL8UuGA4gz-Tme9Nn_tHh5SvXCsIiR6rJjQaqlOoM'
# For kinf approach of K4Mac 1.6.X or later # For kinf approach of K4Mac 1.6.X or later
# On K4PC charMap5 = 'AzB0bYyCeVvaZ3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_c1XxDdW2wE' # On K4PC charMap5 = 'AzB0bYyCeVvaZ3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_c1XxDdW2wE'
@@ -1292,7 +1292,7 @@ elif isosx:
charMap5 = charMap2 charMap5 = charMap2
# new in K4M 1.9.X # new in K4M 1.9.X
testMap8 = 'YvaZ3FfUm9Nn_c1XuG4yCAzB0beVg-TtHh5SsIiR6rJjQdW2wEq7KkPpL8lOoMxD' testMap8 = b'YvaZ3FfUm9Nn_c1XuG4yCAzB0beVg-TtHh5SsIiR6rJjQdW2wEq7KkPpL8lOoMxD'
# uses a sub process to get the Hard Drive Serial Number using ioreg # uses a sub process to get the Hard Drive Serial Number using ioreg
# returns serial numbers of all internal hard drive drives # returns serial numbers of all internal hard drive drives
@@ -1306,11 +1306,11 @@ elif isosx:
p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False) p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
out1, out2 = p.communicate() out1, out2 = p.communicate()
#print out1 #print out1
reslst = out1.split('\n') reslst = out1.split(b'\n')
cnt = len(reslst) cnt = len(reslst)
for j in xrange(cnt): for j in range(cnt):
resline = reslst[j] resline = reslst[j]
pp = resline.find('\"Serial Number\" = \"') pp = resline.find(b'\"Serial Number\" = \"')
if pp >= 0: if pp >= 0:
sernum = resline[pp+19:-1] sernum = resline[pp+19:-1]
sernums.append(sernum.strip()) sernums.append(sernum.strip())
@@ -1322,12 +1322,12 @@ elif isosx:
cmdline = cmdline.encode(sys.getfilesystemencoding()) cmdline = cmdline.encode(sys.getfilesystemencoding())
p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False) p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
out1, out2 = p.communicate() out1, out2 = p.communicate()
reslst = out1.split('\n') reslst = out1.split(b'\n')
cnt = len(reslst) cnt = len(reslst)
for j in xrange(cnt): for j in range(cnt):
resline = reslst[j] resline = reslst[j]
if resline.startswith('/dev'): if resline.startswith(b'/dev'):
(devpart, mpath) = resline.split(' on ')[:2] (devpart, mpath) = resline.split(b' on ')[:2]
dpart = devpart[5:] dpart = devpart[5:]
names.append(dpart) names.append(dpart)
return names return names
@@ -1343,11 +1343,11 @@ elif isosx:
p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False) p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
out1, out2 = p.communicate() out1, out2 = p.communicate()
#print out1 #print out1
reslst = out1.split('\n') reslst = out1.split(b'\n')
cnt = len(reslst) cnt = len(reslst)
for j in xrange(cnt): for j in range(cnt):
resline = reslst[j] resline = reslst[j]
pp = resline.find('\"UUID\" = \"') pp = resline.find(b'\"UUID\" = \"')
if pp >= 0: if pp >= 0:
uuidnum = resline[pp+10:-1] uuidnum = resline[pp+10:-1]
uuidnum = uuidnum.strip() uuidnum = uuidnum.strip()
@@ -1363,16 +1363,16 @@ elif isosx:
cmdline = cmdline.encode(sys.getfilesystemencoding()) cmdline = cmdline.encode(sys.getfilesystemencoding())
p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False) p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
out1, out2 = p.communicate() out1, out2 = p.communicate()
reslst = out1.split('\n') reslst = out1.split(b'\n')
cnt = len(reslst) cnt = len(reslst)
for j in xrange(cnt): for j in range(cnt):
resline = reslst[j] resline = reslst[j]
pp = resline.find('Ethernet Address: ') pp = resline.find(b'Ethernet Address: ')
if pp >= 0: if pp >= 0:
#print resline #print resline
macnum = resline[pp+18:] macnum = resline[pp+18:]
macnum = macnum.strip() macnum = macnum.strip()
maclst = macnum.split(':') maclst = macnum.split(b':')
n = len(maclst) n = len(maclst)
if n != 6: if n != 6:
continue continue
@@ -1380,7 +1380,7 @@ elif isosx:
# now munge it up the way Kindle app does # now munge it up the way Kindle app does
# by xoring it with 0xa5 and swapping elements 3 and 4 # by xoring it with 0xa5 and swapping elements 3 and 4
for i in range(6): for i in range(6):
maclst[i] = int('0x' + maclst[i], 0) maclst[i] = int(b'0x' + maclst[i], 0)
mlst = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00] mlst = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
mlst[5] = maclst[5] ^ 0xa5 mlst[5] = maclst[5] ^ 0xa5
mlst[4] = maclst[3] ^ 0xa5 mlst[4] = maclst[3] ^ 0xa5
@@ -1388,7 +1388,7 @@ elif isosx:
mlst[2] = maclst[2] ^ 0xa5 mlst[2] = maclst[2] ^ 0xa5
mlst[1] = maclst[1] ^ 0xa5 mlst[1] = maclst[1] ^ 0xa5
mlst[0] = maclst[0] ^ 0xa5 mlst[0] = maclst[0] ^ 0xa5
macnum = '%0.2x%0.2x%0.2x%0.2x%0.2x%0.2x' % (mlst[0], mlst[1], mlst[2], mlst[3], mlst[4], mlst[5]) macnum = b'%0.2x%0.2x%0.2x%0.2x%0.2x%0.2x' % (mlst[0], mlst[1], mlst[2], mlst[3], mlst[4], mlst[5])
#print 'munged mac', macnum #print 'munged mac', macnum
macnums.append(macnum) macnums.append(macnum)
return macnums return macnums
@@ -1398,7 +1398,7 @@ elif isosx:
def GetUserName(): def GetUserName():
username = os.getenv('USER') username = os.getenv('USER')
#print "Username:",username #print "Username:",username
return username return username.encode('utf-8')
def GetIDStrings(): def GetIDStrings():
# Return all possible ID Strings # Return all possible ID Strings
@@ -1407,7 +1407,7 @@ elif isosx:
strings.extend(GetVolumesSerialNumbers()) strings.extend(GetVolumesSerialNumbers())
strings.extend(GetDiskPartitionNames()) strings.extend(GetDiskPartitionNames())
strings.extend(GetDiskPartitionUUIDs()) strings.extend(GetDiskPartitionUUIDs())
strings.append('9999999999') strings.append(b'9999999999')
#print "ID Strings:\n",strings #print "ID Strings:\n",strings
return strings return strings
@@ -1415,8 +1415,8 @@ elif isosx:
# unprotect the new header blob in .kinf2011 # unprotect the new header blob in .kinf2011
# used in Kindle for Mac Version >= 1.9.0 # used in Kindle for Mac Version >= 1.9.0
def UnprotectHeaderData(encryptedData): def UnprotectHeaderData(encryptedData):
passwdData = 'header_key_data' passwdData = b'header_key_data'
salt = 'HEADER.2011' salt = b'HEADER.2011'
iter = 0x80 iter = 0x80
keylen = 0x100 keylen = 0x100
crp = LibCrypto() crp = LibCrypto()
@@ -1431,7 +1431,7 @@ elif isosx:
# implements an Pseudo Mac Version of Windows built-in Crypto routine # implements an Pseudo Mac Version of Windows built-in Crypto routine
class CryptUnprotectData(object): class CryptUnprotectData(object):
def __init__(self, entropy, IDString): def __init__(self, entropy, IDString):
sp = GetUserName() + '+@#$%+' + IDString sp = GetUserName() + b'+@#$%+' + IDString
passwdData = encode(SHA256(sp),charMap2) passwdData = encode(SHA256(sp),charMap2)
salt = entropy salt = entropy
self.crp = LibCrypto() self.crp = LibCrypto()
@@ -1510,59 +1510,79 @@ elif isosx:
# database of keynames and values # database of keynames and values
def getDBfromFile(kInfoFile): def getDBfromFile(kInfoFile):
names = [\ names = [\
'kindle.account.tokens',\ b'kindle.account.tokens',\
'kindle.cookie.item',\ b'kindle.cookie.item',\
'eulaVersionAccepted',\ b'eulaVersionAccepted',\
'login_date',\ b'login_date',\
'kindle.token.item',\ b'kindle.token.item',\
'login',\ b'login',\
'kindle.key.item',\ b'kindle.key.item',\
'kindle.name.info',\ b'kindle.name.info',\
'kindle.device.info',\ b'kindle.device.info',\
'MazamaRandomNumber',\ b'MazamaRandomNumber',\
'max_date',\ b'max_date',\
'SIGVERIF',\ b'SIGVERIF',\
'build_version',\ b'build_version',\
'SerialNumber',\ b'SerialNumber',\
'UsernameHash',\ b'UsernameHash',\
'kindle.directedid.info',\ b'kindle.directedid.info',\
'DSN' b'DSN'
b'kindle.accounttype.info',\
b'krx.flashcardsplugin.data.encryption_key',\
b'krx.notebookexportplugin.data.encryption_key',\
b'proxy.http.password',\
b'proxy.http.username'
] ]
with open(kInfoFile, 'rb') as infoReader: with open(kInfoFile, 'rb') as infoReader:
filedata = infoReader.read() filedata = infoReader.read()
data = filedata[:-1] data = filedata[:-1]
items = data.split('/') items = data.split(b'/')
IDStrings = GetIDStrings() IDStrings = GetIDStrings()
print ("trying username ", GetUserName(), " on file ", kInfoFile)
for IDString in IDStrings: for IDString in IDStrings:
#print "trying IDString:",IDString print ("trying IDString:",IDString)
try: try:
DB = {} DB = {}
items = data.split('/') items = data.split(b'/')
# the headerblob is the encrypted information needed to build the entropy string # the headerblob is the encrypted information needed to build the entropy string
headerblob = items.pop(0) headerblob = items.pop(0)
#print ("headerblob: ",headerblob)
encryptedValue = decode(headerblob, charMap1) encryptedValue = decode(headerblob, charMap1)
#print ("encryptedvalue: ",encryptedValue)
cleartext = UnprotectHeaderData(encryptedValue) cleartext = UnprotectHeaderData(encryptedValue)
#print ("cleartext: ",cleartext)
# now extract the pieces in the same way # now extract the pieces in the same way
pattern = re.compile(r'''\[Version:(\d+)\]\[Build:(\d+)\]\[Cksum:([^\]]+)\]\[Guid:([\{\}a-z0-9\-]+)\]''', re.IGNORECASE) pattern = re.compile(br'''\[Version:(\d+)\]\[Build:(\d+)\]\[Cksum:([^\]]+)\]\[Guid:([\{\}a-z0-9\-]+)\]''', re.IGNORECASE)
for m in re.finditer(pattern, cleartext): for m in re.finditer(pattern, cleartext):
version = int(m.group(1)) version = int(m.group(1))
build = m.group(2) build = m.group(2)
guid = m.group(4) guid = m.group(4)
#print ("version",version)
#print ("build",build)
#print ("guid",guid,"\n")
if version == 5: # .kinf2011: identical to K4PC, except the build number gets multiplied if version == 5: # .kinf2011: identical to K4PC, except the build number gets multiplied
entropy = str(0x2df * int(build)) + guid entropy = str(0x2df * int(build)).encode('utf-8') + guid
cud = CryptUnprotectData(entropy,IDString) cud = CryptUnprotectData(entropy,IDString)
#print ("entropy",entropy)
#print ("cud",cud)
elif version == 6: # .kinf2018: identical to K4PC elif version == 6: # .kinf2018: identical to K4PC
salt = str(0x6d8 * int(build)) + guid salt = str(0x6d8 * int(build)).encode('utf-8') + guid
sp = GetUserName() + '+@#$%+' + IDString sp = GetUserName() + b'+@#$%+' + IDString
passwd = encode(SHA256(sp), charMap5) passwd = encode(SHA256(sp), charMap5)
key = LibCrypto().keyivgen(passwd, salt, 10000, 0x400)[:32] key = LibCrypto().keyivgen(passwd, salt, 10000, 0x400)[:32]
# loop through the item records until all are processed #print ("salt",salt)
#print ("sp",sp)
#print ("passwd",passwd)
#print ("key",key)
# loop through the item records until all are processed
while len(items) > 0: while len(items) > 0:
# get the first item record # get the first item record
@@ -1571,7 +1591,7 @@ elif isosx:
# the first 32 chars of the first record of a group # the first 32 chars of the first record of a group
# is the MD5 hash of the key name encoded by charMap5 # is the MD5 hash of the key name encoded by charMap5
keyhash = item[0:32] keyhash = item[0:32]
keyname = 'unknown' keyname = b'unknown'
# unlike K4PC the keyhash is not used in generating entropy # unlike K4PC the keyhash is not used in generating entropy
# entropy = SHA1(keyhash) + added_entropy # entropy = SHA1(keyhash) + added_entropy
@@ -1587,16 +1607,16 @@ elif isosx:
# read and store in rcnt records of data # read and store in rcnt records of data
# that make up the contents value # that make up the contents value
edlst = [] edlst = []
for i in xrange(rcnt): for i in range(rcnt):
item = items.pop(0) item = items.pop(0)
edlst.append(item) edlst.append(item)
keyname = 'unknown' keyname = b'unknown'
for name in names: for name in names:
if encodeHash(name,testMap8) == keyhash: if encodeHash(name,testMap8) == keyhash:
keyname = name keyname = name
break break
if keyname == 'unknown': if keyname == b'unknown':
keyname = keyhash keyname = keyhash
# the testMap8 encoded contents data has had a length # the testMap8 encoded contents data has had a length
@@ -1610,7 +1630,7 @@ elif isosx:
# (in other words split 'about' 2/3rds of the way through) # (in other words split 'about' 2/3rds of the way through)
# move first offsets chars to end to align for decode by testMap8 # move first offsets chars to end to align for decode by testMap8
encdata = ''.join(edlst) encdata = b''.join(edlst)
contlen = len(encdata) contlen = len(encdata)
# now properly split and recombine # now properly split and recombine
@@ -1650,20 +1670,22 @@ elif isosx:
if len(DB)>6: if len(DB)>6:
break break
except:
except Exception:
print (traceback.format_exc())
pass pass
if len(DB)>6: if len(DB)>6:
# store values used in decryption # store values used in decryption
print u"Decrypted key file using IDString '{0:s}' and UserName '{1:s}'".format(IDString, GetUserName()) print("Decrypted key file using IDString '{0:s}' and UserName '{1:s}'".format(IDString.decode('utf-8'), GetUserName().decode('utf-8')))
DB['IDString'] = IDString DB[b'IDString'] = IDString
DB['UserName'] = GetUserName() DB[b'UserName'] = GetUserName()
else: else:
print u"Couldn't decrypt file." print("Couldn't decrypt file.")
DB = {} DB = {}
return DB return DB
else: else:
def getDBfromFile(kInfoFile): def getDBfromFile(kInfoFile):
raise DrmException(u"This script only runs under Windows or Mac OS X.") raise DrmException("This script only runs under Windows or Mac OS X.")
return {} return {}
def kindlekeys(files = []): def kindlekeys(files = []):
@@ -1674,9 +1696,11 @@ def kindlekeys(files = []):
key = getDBfromFile(file) key = getDBfromFile(file)
if key: if key:
# convert all values to hex, just in case. # convert all values to hex, just in case.
for keyname in key: n_key = {}
key[keyname]=key[keyname].encode('hex') for k,v in key.items():
keys.append(key) n_key[k.decode()]=codecs.encode(v, 'hex_codec').decode()
# key = {k.decode():v.decode() for k,v in key.items()}
keys.append(n_key)
return keys return keys
# interface for Python DeDRM # interface for Python DeDRM
@@ -1686,29 +1710,29 @@ def getkey(outpath, files=[]):
if len(keys) > 0: if len(keys) > 0:
if not os.path.isdir(outpath): if not os.path.isdir(outpath):
outfile = outpath outfile = outpath
with file(outfile, 'w') as keyfileout: with open(outfile, 'w') as keyfileout:
keyfileout.write(json.dumps(keys[0])) keyfileout.write(json.dumps(keys[0]))
print u"Saved a key to {0}".format(outfile) print("Saved a key to {0}".format(outfile))
else: else:
keycount = 0 keycount = 0
for key in keys: for key in keys:
while True: while True:
keycount += 1 keycount += 1
outfile = os.path.join(outpath,u"kindlekey{0:d}.k4i".format(keycount)) outfile = os.path.join(outpath,"kindlekey{0:d}.k4i".format(keycount))
if not os.path.exists(outfile): if not os.path.exists(outfile):
break break
with file(outfile, 'w') as keyfileout: with open(outfile, 'w') as keyfileout:
keyfileout.write(json.dumps(key)) keyfileout.write(json.dumps(key))
print u"Saved a key to {0}".format(outfile) print("Saved a key to {0}".format(outfile))
return True return True
return False return False
def usage(progname): def usage(progname):
print u"Finds, decrypts and saves the default Kindle For Mac/PC encryption keys." print("Finds, decrypts and saves the default Kindle For Mac/PC encryption keys.")
print u"Keys are saved to the current directory, or a specified output directory." print("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("If a file name is passed instead of a directory, only the first key is saved, in that file.")
print u"Usage:" print("Usage:")
print u" {0:s} [-h] [-k <kindle.info>] [<outpath>]".format(progname) print(" {0:s} [-h] [-k <kindle.info>] [<outpath>]".format(progname))
def cli_main(): def cli_main():
@@ -1716,12 +1740,12 @@ def cli_main():
sys.stderr=SafeUnbuffered(sys.stderr) sys.stderr=SafeUnbuffered(sys.stderr)
argv=unicode_argv() argv=unicode_argv()
progname = os.path.basename(argv[0]) progname = os.path.basename(argv[0])
print u"{0} v{1}\nCopyright © 2010-2016 by some_updates, Apprentice Alf and Apprentice Harper".format(progname,__version__) print("{0} v{1}\nCopyright © 2010-2020 by some_updates, Apprentice Harper et al.".format(progname,__version__))
try: try:
opts, args = getopt.getopt(argv[1:], "hk:") opts, args = getopt.getopt(argv[1:], "hk:")
except getopt.GetoptError, err: except getopt.GetoptError as err:
print u"Error in options or arguments: {0}".format(err.args[0]) print("Error in options or arguments: {0}".format(err.args[0]))
usage(progname) usage(progname)
sys.exit(2) sys.exit(2)
@@ -1750,33 +1774,33 @@ def cli_main():
outpath = os.path.realpath(os.path.normpath(outpath)) outpath = os.path.realpath(os.path.normpath(outpath))
if not getkey(outpath, files): if not getkey(outpath, files):
print u"Could not retrieve Kindle for Mac/PC key." print("Could not retrieve Kindle for Mac/PC key.")
return 0 return 0
def gui_main(): def gui_main():
try: try:
import Tkinter import tkinter
import Tkconstants import tkinter.constants
import tkMessageBox import tkinter.messagebox
import traceback import traceback
except: except:
return cli_main() return cli_main()
class ExceptionDialog(Tkinter.Frame): class ExceptionDialog(tkinter.Frame):
def __init__(self, root, text): def __init__(self, root, text):
Tkinter.Frame.__init__(self, root, border=5) tkinter.Frame.__init__(self, root, border=5)
label = Tkinter.Label(self, text=u"Unexpected error:", label = tkinter.Label(self, text="Unexpected error:",
anchor=Tkconstants.W, justify=Tkconstants.LEFT) anchor=tkinter.constants.W, justify=tkinter.constants.LEFT)
label.pack(fill=Tkconstants.X, expand=0) label.pack(fill=tkinter.constants.X, expand=0)
self.text = Tkinter.Text(self) self.text = tkinter.Text(self)
self.text.pack(fill=Tkconstants.BOTH, expand=1) 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() argv=unicode_argv()
root = Tkinter.Tk() root = tkinter.Tk()
root.withdraw() root.withdraw()
progpath, progname = os.path.split(argv[0]) progpath, progname = os.path.split(argv[0])
success = False success = False
@@ -1786,21 +1810,21 @@ def gui_main():
for key in keys: for key in keys:
while True: while True:
keycount += 1 keycount += 1
outfile = os.path.join(progpath,u"kindlekey{0:d}.k4i".format(keycount)) outfile = os.path.join(progpath,"kindlekey{0:d}.k4i".format(keycount))
if not os.path.exists(outfile): if not os.path.exists(outfile):
break break
with file(outfile, 'w') as keyfileout: with open(outfile, 'w') as keyfileout:
keyfileout.write(json.dumps(key)) keyfileout.write(json.dumps(key))
success = True success = True
tkMessageBox.showinfo(progname, u"Key successfully retrieved to {0}".format(outfile)) tkinter.messagebox.showinfo(progname, "Key successfully retrieved to {0}".format(outfile))
except DrmException, e: except DrmException as e:
tkMessageBox.showerror(progname, u"Error: {0}".format(str(e))) tkinter.messagebox.showerror(progname, "Error: {0}".format(str(e)))
except Exception: except Exception:
root.wm_state('normal') root.wm_state('normal')
root.title(progname) root.title(progname)
text = traceback.format_exc() 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() root.mainloop()
if not success: if not success:
return 1 return 1

View File

@@ -1,4 +1,4 @@
#!/usr/bin/python #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Mobipocket PID calculator v0.4 for Amazon Kindle. # Mobipocket PID calculator v0.4 for Amazon Kindle.
@@ -10,8 +10,9 @@
# 0.3 updated for unicode # 0.3 updated for unicode
# 0.4 Added support for serial numbers starting with '9', fixed unicode bugs. # 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 # 0.5 moved unicode_argv call inside main for Windows DeDRM compatibility
# 1.0 Python 3 for calibre 5.0
from __future__ import print_function
import sys import sys
import binascii import binascii
@@ -25,10 +26,17 @@ class SafeUnbuffered:
if self.encoding == None: if self.encoding == None:
self.encoding = "utf-8" self.encoding = "utf-8"
def write(self, data): def write(self, data):
if isinstance(data,unicode): if isinstance(data,str) or isinstance(data,unicode):
# str for Python3, unicode for Python2
data = data.encode(self.encoding,"replace") data = data.encode(self.encoding,"replace")
self.stream.write(data) try:
self.stream.flush() buffer = getattr(self.stream, 'buffer', self.stream)
# self.stream.buffer for Python3, self.stream for Python2
buffer.write(data)
buffer.flush()
except:
# We can do nothing if a write fails
raise
def __getattr__(self, attr): def __getattr__(self, attr):
return getattr(self.stream, attr) return getattr(self.stream, attr)
@@ -63,19 +71,13 @@ def unicode_argv():
# Remove Python executable and commands if present # Remove Python executable and commands if present
start = argc.value - len(sys.argv) start = argc.value - len(sys.argv)
return [argv[i] for i in return [argv[i] for i in
xrange(start, argc.value)] range(start, argc.value)]
# if we don't have any arguments at all, just pass back script name # if we don't have any arguments at all, just pass back script name
# this should never happen # this should never happen
return [u"kindlepid.py"] return ["kindlepid.py"]
else: else:
argvencoding = sys.stdin.encoding argvencoding = sys.stdin.encoding or "utf-8"
if argvencoding == None: return [arg if (isinstance(arg, str) or isinstance(arg,unicode)) else str(arg, argvencoding) for arg in sys.argv]
argvencoding = "utf-8"
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
if sys.hexversion >= 0x3000000:
print('This script is incompatible with Python 3.x. Please install Python 2.7.x.')
sys.exit(2)
letters = 'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789' letters = 'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789'
@@ -83,7 +85,7 @@ def crc32(s):
return (~binascii.crc32(s,-1))&0xFFFFFFFF return (~binascii.crc32(s,-1))&0xFFFFFFFF
def checksumPid(s): def checksumPid(s):
crc = crc32(s) crc = crc32(s.encode('ascii'))
crc = crc ^ (crc >> 16) crc = crc ^ (crc >> 16)
res = s res = s
l = len(letters) l = len(letters)
@@ -99,43 +101,43 @@ def pidFromSerial(s, l):
crc = crc32(s) crc = crc32(s)
arr1 = [0]*l arr1 = [0]*l
for i in xrange(len(s)): for i in range(len(s)):
arr1[i%l] ^= ord(s[i]) arr1[i%l] ^= s[i]
crc_bytes = [crc >> 24 & 0xff, crc >> 16 & 0xff, crc >> 8 & 0xff, crc & 0xff] crc_bytes = [crc >> 24 & 0xff, crc >> 16 & 0xff, crc >> 8 & 0xff, crc & 0xff]
for i in xrange(l): for i in range(l):
arr1[i] ^= crc_bytes[i&3] arr1[i] ^= crc_bytes[i&3]
pid = '' pid = ''
for i in xrange(l): for i in range(l):
b = arr1[i] & 0xff b = arr1[i] & 0xff
pid+=letters[(b >> 7) + ((b >> 5 & 3) ^ (b & 0x1f))] pid+=letters[(b >> 7) + ((b >> 5 & 3) ^ (b & 0x1f))]
return pid return pid
def cli_main(): def cli_main():
print(u"Mobipocket PID calculator for Amazon Kindle. Copyright © 2007, 2009 Igor Skochinsky") print("Mobipocket PID calculator for Amazon Kindle. Copyright © 2007, 2009 Igor Skochinsky")
argv=unicode_argv() argv=unicode_argv()
if len(argv)==2: if len(argv)==2:
serial = argv[1] serial = argv[1]
else: else:
print(u"Usage: kindlepid.py <Kindle Serial Number>/<iPhone/iPod Touch UDID>") print("Usage: kindlepid.py <Kindle Serial Number>/<iPhone/iPod Touch UDID>")
return 1 return 1
if len(serial)==16: if len(serial)==16:
if serial.startswith("B") or serial.startswith("9"): if serial.startswith("B") or serial.startswith("9"):
print(u"Kindle serial number detected") print("Kindle serial number detected")
else: else:
print(u"Warning: unrecognized serial number. Please recheck input.") print("Warning: unrecognized serial number. Please recheck input.")
return 1 return 1
pid = pidFromSerial(serial.encode("utf-8"),7)+'*' pid = pidFromSerial(serial.encode("utf-8"),7)+'*'
print(u"Mobipocket PID for Kindle serial#{0} is {1}".format(serial,checksumPid(pid))) print("Mobipocket PID for Kindle serial#{0} is {1}".format(serial,checksumPid(pid)))
return 0 return 0
elif len(serial)==40: elif len(serial)==40:
print(u"iPhone serial number (UDID) detected") print("iPhone serial number (UDID) detected")
pid = pidFromSerial(serial.encode("utf-8"),8) pid = pidFromSerial(serial.encode("utf-8"),8)
print(u"Mobipocket PID for iPhone serial#{0} is {1}".format(serial,checksumPid(pid))) print("Mobipocket PID for iPhone serial#{0} is {1}".format(serial,checksumPid(pid)))
return 0 return 0
print(u"Warning: unrecognized serial number. Please recheck input.") print("Warning: unrecognized serial number. Please recheck input.")
return 1 return 1

215
DeDRM_plugin/mobidedrm.py Normal file → Executable file
View File

@@ -1,13 +1,13 @@
#!/usr/bin/env python #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# mobidedrm.py # mobidedrm.py
# Copyright © 2008 The Dark Reverser # Copyright © 2008 The Dark Reverser
# Portions © 20082017 Apprentice Harper et al. # Portions © 20082020 Apprentice Harper et al.
from __future__ import print_function from __future__ import print_function
__license__ = 'GPL v3' __license__ = 'GPL v3'
__version__ = u"0.42" __version__ = "1.0"
# This is a python script. You need a Python interpreter to run it. # This is a python script. You need a Python interpreter to run it.
# For example, ActiveState Python, which exists for windows. # For example, ActiveState Python, which exists for windows.
@@ -73,6 +73,7 @@ __version__ = u"0.42"
# 0.40 - moved unicode_argv call inside main for Windows DeDRM compatibility # 0.40 - moved unicode_argv call inside main for Windows DeDRM compatibility
# 0.41 - Fixed potential unicode problem in command line calls # 0.41 - Fixed potential unicode problem in command line calls
# 0.42 - Added GPL v3 licence. updated/removed some print statements # 0.42 - Added GPL v3 licence. updated/removed some print statements
# 1.0 - Python 3 compatibility for calibre 5.0
import sys import sys
import os import os
@@ -81,7 +82,7 @@ import binascii
try: try:
from alfcrypto import Pukall_Cipher from alfcrypto import Pukall_Cipher
except: except:
print(u"AlfCrypto not found. Using python PC1 implementation.") print("AlfCrypto not found. Using python PC1 implementation.")
# Wrap a stream so that output gets flushed immediately # Wrap a stream so that output gets flushed immediately
# and also make sure that any unicode strings get # and also make sure that any unicode strings get
@@ -93,10 +94,17 @@ class SafeUnbuffered:
if self.encoding == None: if self.encoding == None:
self.encoding = "utf-8" self.encoding = "utf-8"
def write(self, data): def write(self, data):
if isinstance(data,unicode): if isinstance(data,str) or isinstance(data,unicode):
# str for Python3, unicode for Python2
data = data.encode(self.encoding,"replace") data = data.encode(self.encoding,"replace")
self.stream.write(data) try:
self.stream.flush() buffer = getattr(self.stream, 'buffer', self.stream)
# self.stream.buffer for Python3, self.stream for Python2
buffer.write(data)
buffer.flush()
except:
# We can do nothing if a write fails
raise
def __getattr__(self, attr): def __getattr__(self, attr):
return getattr(self.stream, attr) return getattr(self.stream, attr)
@@ -131,15 +139,13 @@ def unicode_argv():
# Remove Python executable and commands if present # Remove Python executable and commands if present
start = argc.value - len(sys.argv) start = argc.value - len(sys.argv)
return [argv[i] for i in return [argv[i] for i in
xrange(start, argc.value)] range(start, argc.value)]
# if we don't have any arguments at all, just pass back script name # if we don't have any arguments at all, just pass back script name
# this should never happen # this should never happen
return [u"mobidedrm.py"] return ["mobidedrm.py"]
else: else:
argvencoding = sys.stdin.encoding argvencoding = sys.stdin.encoding or "utf-8"
if argvencoding == None: return [arg if (isinstance(arg, str) or isinstance(arg,unicode)) else str(arg, argvencoding) for arg in sys.argv]
argvencoding = 'utf-8'
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
class DrmException(Exception): class DrmException(Exception):
@@ -165,35 +171,36 @@ def PC1(key, src, decryption=True):
sum2 = 0; sum2 = 0;
keyXorVal = 0; keyXorVal = 0;
if len(key)!=16: if len(key)!=16:
DrmException (u"PC1: Bad key length") DrmException ("PC1: Bad key length")
wkey = [] wkey = []
for i in xrange(8): for i in range(8):
wkey.append(ord(key[i*2])<<8 | ord(key[i*2+1])) wkey.append(key[i*2]<<8 | key[i*2+1])
dst = "" dst = b''
for i in xrange(len(src)): for i in range(len(src)):
temp1 = 0; temp1 = 0;
byteXorVal = 0; byteXorVal = 0;
for j in xrange(8): for j in range(8):
temp1 ^= wkey[j] temp1 ^= wkey[j]
sum2 = (sum2+j)*20021 + sum1 sum2 = (sum2+j)*20021 + sum1
sum1 = (temp1*346)&0xFFFF sum1 = (temp1*346)&0xFFFF
sum2 = (sum2+sum1)&0xFFFF sum2 = (sum2+sum1)&0xFFFF
temp1 = (temp1*20021+1)&0xFFFF temp1 = (temp1*20021+1)&0xFFFF
byteXorVal ^= temp1 ^ sum2 byteXorVal ^= temp1 ^ sum2
curByte = ord(src[i]) curByte = src[i]
if not decryption: if not decryption:
keyXorVal = curByte * 257; keyXorVal = curByte * 257;
curByte = ((curByte ^ (byteXorVal >> 8)) ^ byteXorVal) & 0xFF curByte = ((curByte ^ (byteXorVal >> 8)) ^ byteXorVal) & 0xFF
if decryption: if decryption:
keyXorVal = curByte * 257; keyXorVal = curByte * 257;
for j in xrange(8): for j in range(8):
wkey[j] ^= keyXorVal; wkey[j] ^= keyXorVal;
dst+=chr(curByte) dst+=bytes([curByte])
return dst return dst
# accepts unicode returns unicode
def checksumPid(s): def checksumPid(s):
letters = 'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789' letters = 'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789'
crc = (~binascii.crc32(s,-1))&0xFFFFFFFF crc = (~binascii.crc32(s.encode('utf-8'),-1))&0xFFFFFFFF
crc = crc ^ (crc >> 16) crc = crc ^ (crc >> 16)
res = s res = s
l = len(letters) l = len(letters)
@@ -204,13 +211,14 @@ def checksumPid(s):
crc >>= 8 crc >>= 8
return res return res
# expects bytearray
def getSizeOfTrailingDataEntries(ptr, size, flags): def getSizeOfTrailingDataEntries(ptr, size, flags):
def getSizeOfTrailingDataEntry(ptr, size): def getSizeOfTrailingDataEntry(ptr, size):
bitpos, result = 0, 0 bitpos, result = 0, 0
if size <= 0: if size <= 0:
return result return result
while True: while True:
v = ord(ptr[size-1]) v = ptr[size-1]
result |= (v & 0x7F) << bitpos result |= (v & 0x7F) << bitpos
bitpos += 7 bitpos += 7
size -= 1 size -= 1
@@ -226,7 +234,7 @@ def getSizeOfTrailingDataEntries(ptr, size, flags):
# if multibyte data is included in the encryped data, we'll # if multibyte data is included in the encryped data, we'll
# have already cleared this flag. # have already cleared this flag.
if flags & 1: if flags & 1:
num += (ord(ptr[size - num - 1]) & 0x3) + 1 num += (ptr[size - num - 1] & 0x3) + 1
return num return num
@@ -245,26 +253,26 @@ class MobiBook:
pass pass
def __init__(self, infile): def __init__(self, infile):
print(u"MobiDeDrm v{0:s}.\nCopyright © 2008-2017 The Dark Reverser, Apprentice Harper et al.".format(__version__)) print("MobiDeDrm v{0:s}.\nCopyright © 2008-2020 The Dark Reverser, Apprentice Harper et al.".format(__version__))
try: try:
from alfcrypto import Pukall_Cipher from alfcrypto import Pukall_Cipher
except: except:
print(u"AlfCrypto not found. Using python PC1 implementation.") print("AlfCrypto not found. Using python PC1 implementation.")
# initial sanity check on file # initial sanity check on file
self.data_file = file(infile, 'rb').read() self.data_file = open(infile, 'rb').read()
self.mobi_data = '' self.mobi_data = ''
self.header = self.data_file[0:78] self.header = self.data_file[0:78]
if self.header[0x3C:0x3C+8] != 'BOOKMOBI' and self.header[0x3C:0x3C+8] != 'TEXtREAd': if self.header[0x3C:0x3C+8] != b'BOOKMOBI' and self.header[0x3C:0x3C+8] != b'TEXtREAd':
raise DrmException(u"Invalid file format") raise DrmException("Invalid file format")
self.magic = self.header[0x3C:0x3C+8] self.magic = self.header[0x3C:0x3C+8]
self.crypto_type = -1 self.crypto_type = -1
# build up section offset and flag info # build up section offset and flag info
self.num_sections, = struct.unpack('>H', self.header[76:78]) self.num_sections, = struct.unpack('>H', self.header[76:78])
self.sections = [] 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]) 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 flags, val = a1, a2<<16|a3<<8|a4
self.sections.append( (offset, flags, val) ) self.sections.append( (offset, flags, val) )
@@ -282,17 +290,17 @@ class MobiBook:
self.mobi_codepage = 1252 self.mobi_codepage = 1252
self.mobi_version = -1 self.mobi_version = -1
if self.magic == 'TEXtREAd': if self.magic == b'TEXtREAd':
print(u"PalmDoc format book detected.") print("PalmDoc format book detected.")
return return
self.mobi_length, = struct.unpack('>L',self.sect[0x14:0x18]) self.mobi_length, = struct.unpack('>L',self.sect[0x14:0x18])
self.mobi_codepage, = struct.unpack('>L',self.sect[0x1c:0x20]) self.mobi_codepage, = struct.unpack('>L',self.sect[0x1c:0x20])
self.mobi_version, = struct.unpack('>L',self.sect[0x68:0x6C]) 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): if (self.mobi_length >= 0xE4) and (self.mobi_version >= 5):
self.extra_data_flags, = struct.unpack('>H', self.sect[0xF2:0xF4]) 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): if (self.compression != 17480):
# multibyte utf8 data is included in the encryption for PalmDoc compression # multibyte utf8 data is included in the encryption for PalmDoc compression
# so clear that byte so that we leave it to be decrypted. # so clear that byte so that we leave it to be decrypted.
@@ -301,36 +309,46 @@ class MobiBook:
# if exth region exists parse it for metadata array # if exth region exists parse it for metadata array
try: try:
exth_flag, = struct.unpack('>L', self.sect[0x80:0x84]) exth_flag, = struct.unpack('>L', self.sect[0x80:0x84])
exth = '' exth = b''
if exth_flag & 0x40: if exth_flag & 0x40:
exth = self.sect[16 + self.mobi_length:] 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]) nitems, = struct.unpack('>I', exth[8:12])
pos = 12 pos = 12
for i in xrange(nitems): for i in range(nitems):
type, size = struct.unpack('>II', exth[pos: pos + 8]) type, size = struct.unpack('>II', exth[pos: pos + 8])
content = exth[pos + 8: pos + size] content = exth[pos + 8: pos + size]
self.meta_array[type] = content self.meta_array[type] = content
# reset the text to speech flag and clipping limit, if present # reset the text to speech flag and clipping limit, if present
if type == 401 and size == 9: if type == 401 and size == 9:
# set clipping limit to 100% # 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: elif type == 404 and size == 9:
# make sure text to speech is enabled # 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') # print type, size, content, content.encode('hex')
pos += size pos += size
except: except Exception as e:
pass print("Cannot set meta_array: Error: {:s}".format(e.args[0]))
#returns unicode
def getBookTitle(self): def getBookTitle(self):
codec_map = { codec_map = {
1252 : 'windows-1252', 1252 : 'windows-1252',
65001 : 'utf-8', 65001 : 'utf-8',
} }
title = '' title = b''
codec = 'windows-1252' codec = 'windows-1252'
if self.magic == 'BOOKMOBI': if self.magic == b'BOOKMOBI':
if 503 in self.meta_array: if 503 in self.meta_array:
title = self.meta_array[503] title = self.meta_array[503]
else: else:
@@ -339,29 +357,31 @@ class MobiBook:
title = self.sect[toff:tend] title = self.sect[toff:tend]
if self.mobi_codepage in codec_map.keys(): if self.mobi_codepage in codec_map.keys():
codec = codec_map[self.mobi_codepage] codec = codec_map[self.mobi_codepage]
if title == '': if title == b'':
title = self.header[:32] title = self.header[:32]
title = title.split('\0')[0] title = title.split(b'\0')[0]
return unicode(title, codec) return title.decode(codec)
def getPIDMetaInfo(self): def getPIDMetaInfo(self):
rec209 = '' rec209 = b''
token = '' token = b''
if 209 in self.meta_array: if 209 in self.meta_array:
rec209 = self.meta_array[209] rec209 = self.meta_array[209]
data = rec209 data = rec209
# The 209 data comes in five byte groups. Interpret the last four bytes # 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 # 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 # 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]) val, = struct.unpack('>I',data[i+1:i+5])
sval = self.meta_array.get(val,'') sval = self.meta_array.get(val,b'')
token += sval token += sval
return rec209, token return rec209, token
# new must be byte array
def patch(self, off, new): def patch(self, off, new):
self.data_file = self.data_file[:off] + new + self.data_file[off+len(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): def patchSection(self, section, new, in_off = 0):
if (section + 1 == self.num_sections): if (section + 1 == self.num_sections):
endoff = len(self.data_file) endoff = len(self.data_file)
@@ -371,15 +391,16 @@ class MobiBook:
assert off + in_off + len(new) <= endoff assert off + in_off + len(new) <= endoff
self.patch(off + in_off, new) 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): def parseDRM(self, data, count, pidlist):
found_key = None 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: 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 = PC1(keyvec1, bigpid, False)
temp_key_sum = sum(map(ord,temp_key)) & 0xff temp_key_sum = sum(temp_key) & 0xff
found_key = None 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]) verification, size, type, cksum, cookie = struct.unpack('>LLLBxxx32s', data[i*0x30:i*0x30+0x30])
if cksum == temp_key_sum: if cksum == temp_key_sum:
cookie = PC1(temp_key, cookie) cookie = PC1(temp_key, cookie)
@@ -393,8 +414,8 @@ class MobiBook:
# Then try the default encoding that doesn't require a PID # Then try the default encoding that doesn't require a PID
pid = '00000000' pid = '00000000'
temp_key = keyvec1 temp_key = keyvec1
temp_key_sum = sum(map(ord,temp_key)) & 0xff temp_key_sum = sum(temp_key) & 0xff
for i in xrange(count): for i in range(count):
verification, size, type, cksum, cookie = struct.unpack('>LLLBxxx32s', data[i*0x30:i*0x30+0x30]) verification, size, type, cksum, cookie = struct.unpack('>LLLBxxx32s', data[i*0x30:i*0x30+0x30])
if cksum == temp_key_sum: if cksum == temp_key_sum:
cookie = PC1(temp_key, cookie) cookie = PC1(temp_key, cookie)
@@ -405,56 +426,63 @@ class MobiBook:
return [found_key,pid] return [found_key,pid]
def getFile(self, outpath): def getFile(self, outpath):
file(outpath,'wb').write(self.mobi_data) open(outpath,'wb').write(self.mobi_data)
def getBookType(self): def getBookType(self):
if self.print_replica: if self.print_replica:
return u"Print Replica" return "Print Replica"
if self.mobi_version >= 8: if self.mobi_version >= 8:
return u"Kindle Format 8" return "Kindle Format 8"
if self.mobi_version >= 0: if self.mobi_version >= 0:
return u"Mobipocket {0:d}".format(self.mobi_version) return "Mobipocket {0:d}".format(self.mobi_version)
return u"PalmDoc" return "PalmDoc"
def getBookExtension(self): def getBookExtension(self):
if self.print_replica: if self.print_replica:
return u".azw4" return ".azw4"
if self.mobi_version >= 8: if self.mobi_version >= 8:
return u".azw3" return ".azw3"
return u".mobi" return ".mobi"
# pids in pidlist may be unicode or bytearrays or bytes
def processBook(self, pidlist): def processBook(self, pidlist):
crypto_type, = struct.unpack('>H', self.sect[0xC:0xC+2]) 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 self.crypto_type = crypto_type
if crypto_type == 0: if crypto_type == 0:
print(u"This book is not encrypted.") print("This book is not encrypted.")
# we must still check for Print Replica # we must still check for Print Replica
self.print_replica = (self.loadSection(1)[0:4] == '%MOP') self.print_replica = (self.loadSection(1)[0:4] == '%MOP')
self.mobi_data = self.data_file self.mobi_data = self.data_file
return return
if crypto_type != 2 and crypto_type != 1: if crypto_type != 2 and crypto_type != 1:
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: if 406 in self.meta_array:
data406 = self.meta_array[406] data406 = self.meta_array[406]
val406, = struct.unpack('>Q',data406) val406, = struct.unpack('>Q',data406)
if val406 != 0: if val406 != 0:
raise DrmException(u"Cannot decode library or rented ebooks.") print("Warning: This is a library or rented ebook ({1}). Continuing ...".format(val406))
#raise DrmException("Cannot decode library or rented ebooks.")
goodpids = [] goodpids = []
# print("DEBUG ==== pidlist = ", pidlist)
for pid in pidlist: for pid in pidlist:
if isinstance(pid,(bytearray,bytes)):
pid = pid.decode('utf-8')
if len(pid)==10: if len(pid)==10:
if checksumPid(pid[0:-2]) != pid: 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]) goodpids.append(pid[0:-2])
elif len(pid)==8: elif len(pid)==8:
goodpids.append(pid) goodpids.append(pid)
else: 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: if self.crypto_type == 1:
t1_keyvec = 'QDCVEPMU675RUBSZ' t1_keyvec = b'QDCVEPMU675RUBSZ'
if self.magic == 'TEXtREAd': if self.magic == b'TEXtREAd':
bookkey_data = self.sect[0x0E:0x0E+16] bookkey_data = self.sect[0x0E:0x0E+16]
elif self.mobi_version < 0: elif self.mobi_version < 0:
bookkey_data = self.sect[0x90:0x90+16] bookkey_data = self.sect[0x90:0x90+16]
@@ -466,32 +494,32 @@ class MobiBook:
# calculate the keys # calculate the keys
drm_ptr, drm_count, drm_size, drm_flags = struct.unpack('>LLLL', self.sect[0xA8:0xA8+16]) drm_ptr, drm_count, drm_size, drm_flags = struct.unpack('>LLLL', self.sect[0xA8:0xA8+16])
if drm_count == 0: 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) found_key, pid = self.parseDRM(self.sect[drm_ptr:drm_ptr+drm_size], drm_count, goodpids)
if not found_key: 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 # 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 # 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': if pid=='00000000':
print(u"File has default encryption, no specific key needed.") print("File has default encryption, no specific key needed.")
else: 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 # clear the crypto type
self.patchSection(0, "\0" * 2, 0xC) self.patchSection(0, b'\0' * 2, 0xC)
# decrypt sections # decrypt sections
print(u"Decrypting. Please wait . . .", end=' ') print("Decrypting. Please wait . . .", end=' ')
mobidataList = [] mobidataList = []
mobidataList.append(self.data_file[:self.sections[1][0]]) 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) data = self.loadSection(i)
extra_size = getSizeOfTrailingDataEntries(data, len(data), self.extra_data_flags) extra_size = getSizeOfTrailingDataEntries(data, len(data), self.extra_data_flags)
if i%100 == 0: if i%100 == 0:
print(u".", end=' ') print(".", end=' ')
# print "record %d, extra_size %d" %(i,extra_size) # print "record %d, extra_size %d" %(i,extra_size)
decoded_data = PC1(found_key, data[0:len(data) - extra_size]) decoded_data = PC1(found_key, data[0:len(data) - extra_size])
if i==1: if i==1:
@@ -501,13 +529,14 @@ class MobiBook:
mobidataList.append(data[-extra_size:]) mobidataList.append(data[-extra_size:])
if self.num_sections > self.records+1: if self.num_sections > self.records+1:
mobidataList.append(self.data_file[self.sections[self.records+1][0]:]) mobidataList.append(self.data_file[self.sections[self.records+1][0]:])
self.mobi_data = "".join(mobidataList) self.mobi_data = b''.join(mobidataList)
print(u"done") print("done")
return return
# pids in pidlist must be unicode
def getUnencryptedBook(infile,pidlist): def getUnencryptedBook(infile,pidlist):
if not os.path.isfile(infile): if not os.path.isfile(infile):
raise DrmException(u"Input File Not Found.") raise DrmException("Input File Not Found.")
book = MobiBook(infile) book = MobiBook(infile)
book.processBook(pidlist) book.processBook(pidlist)
return book.mobi_data return book.mobi_data
@@ -517,10 +546,10 @@ def cli_main():
argv=unicode_argv() argv=unicode_argv()
progname = os.path.basename(argv[0]) progname = os.path.basename(argv[0])
if len(argv)<3 or len(argv)>4: if len(argv)<3 or len(argv)>4:
print(u"MobiDeDrm v{0:s}.\nCopyright © 2008-2017 The Dark Reverser, Apprentice Harper et al.".format(__version__)) print("MobiDeDrm v{0:s}.\nCopyright © 2008-2020 The Dark Reverser, Apprentice Harper et al.".format(__version__))
print(u"Removes protection from Kindle/Mobipocket, Kindle/KF8 and Kindle/Print Replica ebooks") print("Removes protection from Kindle/Mobipocket, Kindle/KF8 and Kindle/Print Replica ebooks")
print(u"Usage:") print("Usage:")
print(u" {0} <infile> <outfile> [<Comma separated list of PIDs to try>]".format(progname)) print(" {0} <infile> <outfile> [<Comma separated list of PIDs to try>]".format(progname))
return 1 return 1
else: else:
infile = argv[1] infile = argv[1]
@@ -531,9 +560,9 @@ def cli_main():
pidlist = [] pidlist = []
try: try:
stripped_file = getUnencryptedBook(infile, pidlist) stripped_file = getUnencryptedBook(infile, pidlist)
file(outfile, 'wb').write(stripped_file) open(outfile, 'wb').write(stripped_file)
except DrmException, e: except DrmException as e:
print(u"MobiDeDRM v{0} Error: {1:s}".format(__version__,e.args[0])) print("MobiDeDRM v{0} Error: {1:s}".format(__version__,e.args[0]))
return 1 return 1
return 0 return 0

View File

@@ -76,7 +76,7 @@ def load_libcrypto():
return ob.raw return ob.raw
def decrypt(self, data): def decrypt(self, data):
if not data: if not data:
return '' return b''
i = 0 i = 0
result = [] result = []
while i < len(data): while i < len(data):
@@ -84,6 +84,6 @@ def load_libcrypto():
processed_block = self.desdecrypt(block) processed_block = self.desdecrypt(block)
result.append(processed_block) result.append(processed_block)
i += 8 i += 8
return ''.join(result) return b''.join(result)
return DES return DES

61
DeDRM_plugin/prefs.py Normal file → Executable file
View File

@@ -1,12 +1,12 @@
#!/usr/bin/env python #!/usr/bin/env python3
# -*- coding: utf-8 -*-
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai # vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import with_statement
__license__ = 'GPL v3' __license__ = 'GPL v3'
# Standard Python modules. # Standard Python modules.
import os, sys, re, hashlib import os, sys, re, hashlib
import json import codecs, json
import traceback import traceback
from calibre.utils.config import dynamic, config_dir, JSONConfig from calibre.utils.config import dynamic, config_dir, JSONConfig
@@ -15,10 +15,12 @@ from calibre.constants import iswindows, isosx
class DeDRM_Prefs(): class DeDRM_Prefs():
def __init__(self): def __init__(self):
JSON_PATH = os.path.join(u"plugins", PLUGIN_NAME.strip().lower().replace(' ', '_') + '.json') JSON_PATH = os.path.join("plugins", PLUGIN_NAME.strip().lower().replace(' ', '_') + '.json')
self.dedrmprefs = JSONConfig(JSON_PATH) self.dedrmprefs = JSONConfig(JSON_PATH)
self.dedrmprefs.defaults['configured'] = False self.dedrmprefs.defaults['configured'] = False
self.dedrmprefs.defaults['deobfuscate_fonts'] = True
self.dedrmprefs.defaults['remove_watermarks'] = False
self.dedrmprefs.defaults['bandnkeys'] = {} self.dedrmprefs.defaults['bandnkeys'] = {}
self.dedrmprefs.defaults['adeptkeys'] = {} self.dedrmprefs.defaults['adeptkeys'] = {}
self.dedrmprefs.defaults['ereaderkeys'] = {} self.dedrmprefs.defaults['ereaderkeys'] = {}
@@ -26,6 +28,7 @@ class DeDRM_Prefs():
self.dedrmprefs.defaults['androidkeys'] = {} self.dedrmprefs.defaults['androidkeys'] = {}
self.dedrmprefs.defaults['pids'] = [] self.dedrmprefs.defaults['pids'] = []
self.dedrmprefs.defaults['serials'] = [] self.dedrmprefs.defaults['serials'] = []
self.dedrmprefs.defaults['lcp_passphrases'] = []
self.dedrmprefs.defaults['adobewineprefix'] = "" self.dedrmprefs.defaults['adobewineprefix'] = ""
self.dedrmprefs.defaults['kindlewineprefix'] = "" self.dedrmprefs.defaults['kindlewineprefix'] = ""
@@ -47,6 +50,8 @@ class DeDRM_Prefs():
self.dedrmprefs['pids'] = [] self.dedrmprefs['pids'] = []
if self.dedrmprefs['serials'] == []: if self.dedrmprefs['serials'] == []:
self.dedrmprefs['serials'] = [] self.dedrmprefs['serials'] = []
if self.dedrmprefs['lcp_passphrases'] == []:
self.dedrmprefs['lcp_passphrases'] = []
def __getitem__(self,kind = None): def __getitem__(self,kind = None):
if kind is not None: if kind is not None:
@@ -98,12 +103,12 @@ def convertprefs(always = False):
try: try:
name, ccn = keystring.split(',') name, ccn = keystring.split(',')
# Generate Barnes & Noble EPUB user key from name and credit card number. # Generate Barnes & Noble EPUB user key from name and credit card number.
keyname = u"{0}_{1}".format(name.strip(),ccn.strip()[-4:]) keyname = "{0}_{1}".format(name.strip(),ccn.strip()[-4:])
keyvalue = generate_key(name, ccn) keyvalue = generate_key(name, ccn)
userkeys.append([keyname,keyvalue]) userkeys.append([keyname,keyvalue])
except Exception, e: except Exception as e:
traceback.print_exc() traceback.print_exc()
print e.args[0] print(e.args[0])
pass pass
return userkeys return userkeys
@@ -115,12 +120,12 @@ def convertprefs(always = False):
try: try:
name, cc = keystring.split(',') name, cc = keystring.split(',')
# Generate eReader user key from name and credit card number. # Generate eReader user key from name and credit card number.
keyname = u"{0}_{1}".format(name.strip(),cc.strip()[-4:]) keyname = "{0}_{1}".format(name.strip(),cc.strip()[-4:])
keyvalue = getuser_key(name,cc).encode('hex') keyvalue = codecs.encode(getuser_key(name,cc),'hex')
userkeys.append([keyname,keyvalue]) userkeys.append([keyname,keyvalue])
except Exception, e: except Exception as e:
traceback.print_exc() traceback.print_exc()
print e.args[0] print(e.args[0])
pass pass
return userkeys return userkeys
@@ -146,7 +151,7 @@ def convertprefs(always = False):
key = os.path.splitext(filename)[0] key = os.path.splitext(filename)[0]
value = open(fpath, 'rb').read() value = open(fpath, 'rb').read()
if encoding is not None: if encoding is not None:
value = value.encode(encoding) value = codecs.encode(value,encoding)
userkeys.append([key,value]) userkeys.append([key,value])
except: except:
traceback.print_exc() traceback.print_exc()
@@ -161,15 +166,15 @@ def convertprefs(always = False):
return return
print u"{0} v{1}: Importing configuration data from old DeDRM plugins".format(PLUGIN_NAME, PLUGIN_VERSION) print("{0} v{1}: Importing configuration data from old DeDRM plugins".format(PLUGIN_NAME, PLUGIN_VERSION))
IGNOBLEPLUGINNAME = "Ignoble Epub DeDRM" IGNOBLEPLUGINNAME = "Ignoble Epub DeDRM"
EREADERPLUGINNAME = "eReader PDB 2 PML" EREADERPLUGINNAME = "eReader PDB 2 PML"
OLDKINDLEPLUGINNAME = "K4PC, K4Mac, Kindle Mobi and Topaz DeDRM" OLDKINDLEPLUGINNAME = "K4PC, K4Mac, Kindle Mobi and Topaz DeDRM"
# get prefs from older tools # get prefs from older tools
kindleprefs = JSONConfig(os.path.join(u"plugins", u"K4MobiDeDRM")) kindleprefs = JSONConfig(os.path.join("plugins", "K4MobiDeDRM"))
ignobleprefs = JSONConfig(os.path.join(u"plugins", u"ignoble_epub_dedrm")) ignobleprefs = JSONConfig(os.path.join("plugins", "ignoble_epub_dedrm"))
# Handle the old ignoble plugin's customization string by converting the # Handle the old ignoble plugin's customization string by converting the
# old string to stored keys... get that personal data out of plain sight. # old string to stored keys... get that personal data out of plain sight.
@@ -177,7 +182,7 @@ def convertprefs(always = False):
sc = config['plugin_customization'] sc = config['plugin_customization']
val = sc.pop(IGNOBLEPLUGINNAME, None) val = sc.pop(IGNOBLEPLUGINNAME, None)
if val is not None: if val is not None:
print u"{0} v{1}: Converting old Ignoble plugin configuration string.".format(PLUGIN_NAME, PLUGIN_VERSION) print("{0} v{1}: Converting old Ignoble plugin configuration string.".format(PLUGIN_NAME, PLUGIN_VERSION))
priorkeycount = len(dedrmprefs['bandnkeys']) priorkeycount = len(dedrmprefs['bandnkeys'])
userkeys = parseIgnobleString(str(val)) userkeys = parseIgnobleString(str(val))
for keypair in userkeys: for keypair in userkeys:
@@ -185,7 +190,7 @@ def convertprefs(always = False):
value = keypair[1] value = keypair[1]
dedrmprefs.addnamedvaluetoprefs('bandnkeys', name, value) dedrmprefs.addnamedvaluetoprefs('bandnkeys', name, value)
addedkeycount = len(dedrmprefs['bandnkeys'])-priorkeycount addedkeycount = len(dedrmprefs['bandnkeys'])-priorkeycount
print u"{0} v{1}: {2:d} Barnes and Noble {3} imported from old Ignoble plugin configuration string".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, u"key" if addedkeycount==1 else u"keys") print("{0} v{1}: {2:d} Barnes and Noble {3} imported from old Ignoble plugin configuration string".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, "key" if addedkeycount==1 else "keys"))
# Make the json write all the prefs to disk # Make the json write all the prefs to disk
dedrmprefs.writeprefs(False) dedrmprefs.writeprefs(False)
@@ -193,7 +198,7 @@ def convertprefs(always = False):
# old string to stored keys... get that personal data out of plain sight. # old string to stored keys... get that personal data out of plain sight.
val = sc.pop(EREADERPLUGINNAME, None) val = sc.pop(EREADERPLUGINNAME, None)
if val is not None: if val is not None:
print u"{0} v{1}: Converting old eReader plugin configuration string.".format(PLUGIN_NAME, PLUGIN_VERSION) print("{0} v{1}: Converting old eReader plugin configuration string.".format(PLUGIN_NAME, PLUGIN_VERSION))
priorkeycount = len(dedrmprefs['ereaderkeys']) priorkeycount = len(dedrmprefs['ereaderkeys'])
userkeys = parseeReaderString(str(val)) userkeys = parseeReaderString(str(val))
for keypair in userkeys: for keypair in userkeys:
@@ -201,14 +206,14 @@ def convertprefs(always = False):
value = keypair[1] value = keypair[1]
dedrmprefs.addnamedvaluetoprefs('ereaderkeys', name, value) dedrmprefs.addnamedvaluetoprefs('ereaderkeys', name, value)
addedkeycount = len(dedrmprefs['ereaderkeys'])-priorkeycount addedkeycount = len(dedrmprefs['ereaderkeys'])-priorkeycount
print u"{0} v{1}: {2:d} eReader {3} imported from old eReader plugin configuration string".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, u"key" if addedkeycount==1 else u"keys") print("{0} v{1}: {2:d} eReader {3} imported from old eReader plugin configuration string".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, "key" if addedkeycount==1 else "keys"))
# Make the json write all the prefs to disk # Make the json write all the prefs to disk
dedrmprefs.writeprefs(False) dedrmprefs.writeprefs(False)
# get old Kindle plugin configuration string # get old Kindle plugin configuration string
val = sc.pop(OLDKINDLEPLUGINNAME, None) val = sc.pop(OLDKINDLEPLUGINNAME, None)
if val is not None: if val is not None:
print u"{0} v{1}: Converting old Kindle plugin configuration string.".format(PLUGIN_NAME, PLUGIN_VERSION) print("{0} v{1}: Converting old Kindle plugin configuration string.".format(PLUGIN_NAME, PLUGIN_VERSION))
priorpidcount = len(dedrmprefs['pids']) priorpidcount = len(dedrmprefs['pids'])
priorserialcount = len(dedrmprefs['serials']) priorserialcount = len(dedrmprefs['serials'])
pids, serials = parseKindleString(val) pids, serials = parseKindleString(val)
@@ -218,7 +223,7 @@ def convertprefs(always = False):
dedrmprefs.addvaluetoprefs('serials',serial) dedrmprefs.addvaluetoprefs('serials',serial)
addedpidcount = len(dedrmprefs['pids']) - priorpidcount addedpidcount = len(dedrmprefs['pids']) - priorpidcount
addedserialcount = len(dedrmprefs['serials']) - priorserialcount addedserialcount = len(dedrmprefs['serials']) - priorserialcount
print u"{0} v{1}: {2:d} {3} and {4:d} {5} imported from old Kindle plugin configuration string.".format(PLUGIN_NAME, PLUGIN_VERSION, addedpidcount, u"PID" if addedpidcount==1 else u"PIDs", addedserialcount, u"serial number" if addedserialcount==1 else u"serial numbers") print("{0} v{1}: {2:d} {3} and {4:d} {5} imported from old Kindle plugin configuration string.".format(PLUGIN_NAME, PLUGIN_VERSION, addedpidcount, "PID" if addedpidcount==1 else "PIDs", addedserialcount, "serial number" if addedserialcount==1 else "serial numbers"))
# Make the json write all the prefs to disk # Make the json write all the prefs to disk
dedrmprefs.writeprefs(False) dedrmprefs.writeprefs(False)
@@ -234,7 +239,7 @@ def convertprefs(always = False):
dedrmprefs.addnamedvaluetoprefs('bandnkeys', name, value) dedrmprefs.addnamedvaluetoprefs('bandnkeys', name, value)
addedkeycount = len(dedrmprefs['bandnkeys'])-priorkeycount addedkeycount = len(dedrmprefs['bandnkeys'])-priorkeycount
if addedkeycount > 0: if addedkeycount > 0:
print u"{0} v{1}: {2:d} Barnes and Noble {3} imported from config folder.".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, u"key file" if addedkeycount==1 else u"key files") print("{0} v{1}: {2:d} Barnes and Noble {3} imported from config folder.".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, "key file" if addedkeycount==1 else "key files"))
# Make the json write all the prefs to disk # Make the json write all the prefs to disk
dedrmprefs.writeprefs(False) dedrmprefs.writeprefs(False)
@@ -247,7 +252,7 @@ def convertprefs(always = False):
dedrmprefs.addnamedvaluetoprefs('adeptkeys', name, value) dedrmprefs.addnamedvaluetoprefs('adeptkeys', name, value)
addedkeycount = len(dedrmprefs['adeptkeys'])-priorkeycount addedkeycount = len(dedrmprefs['adeptkeys'])-priorkeycount
if addedkeycount > 0: if addedkeycount > 0:
print u"{0} v{1}: {2:d} Adobe Adept {3} imported from config folder.".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, u"keyfile" if addedkeycount==1 else u"keyfiles") print("{0} v{1}: {2:d} Adobe Adept {3} imported from config folder.".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, "keyfile" if addedkeycount==1 else "keyfiles"))
# Make the json write all the prefs to disk # Make the json write all the prefs to disk
dedrmprefs.writeprefs(False) dedrmprefs.writeprefs(False)
@@ -260,7 +265,7 @@ def convertprefs(always = False):
addedkeycount = len(dedrmprefs['bandnkeys']) - priorkeycount addedkeycount = len(dedrmprefs['bandnkeys']) - priorkeycount
# no need to delete old prefs, since they contain no recoverable private data # no need to delete old prefs, since they contain no recoverable private data
if addedkeycount > 0: if addedkeycount > 0:
print u"{0} v{1}: {2:d} Barnes and Noble {3} imported from Ignoble plugin preferences.".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, u"key" if addedkeycount==1 else u"keys") print("{0} v{1}: {2:d} Barnes and Noble {3} imported from Ignoble plugin preferences.".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, "key" if addedkeycount==1 else "keys"))
# Make the json write all the prefs to disk # Make the json write all the prefs to disk
dedrmprefs.writeprefs(False) dedrmprefs.writeprefs(False)
@@ -277,19 +282,19 @@ def convertprefs(always = False):
dedrmprefs.addvaluetoprefs('serials',serial) dedrmprefs.addvaluetoprefs('serials',serial)
addedpidcount = len(dedrmprefs['pids']) - priorpidcount addedpidcount = len(dedrmprefs['pids']) - priorpidcount
if addedpidcount > 0: if addedpidcount > 0:
print u"{0} v{1}: {2:d} {3} imported from Kindle plugin preferences".format(PLUGIN_NAME, PLUGIN_VERSION, addedpidcount, u"PID" if addedpidcount==1 else u"PIDs") print("{0} v{1}: {2:d} {3} imported from Kindle plugin preferences".format(PLUGIN_NAME, PLUGIN_VERSION, addedpidcount, "PID" if addedpidcount==1 else "PIDs"))
addedserialcount = len(dedrmprefs['serials']) - priorserialcount addedserialcount = len(dedrmprefs['serials']) - priorserialcount
if addedserialcount > 0: if addedserialcount > 0:
print u"{0} v{1}: {2:d} {3} imported from Kindle plugin preferences".format(PLUGIN_NAME, PLUGIN_VERSION, addedserialcount, u"serial number" if addedserialcount==1 else u"serial numbers") print("{0} v{1}: {2:d} {3} imported from Kindle plugin preferences".format(PLUGIN_NAME, PLUGIN_VERSION, addedserialcount, "serial number" if addedserialcount==1 else "serial numbers"))
try: try:
if 'wineprefix' in kindleprefs and kindleprefs['wineprefix'] != "": if 'wineprefix' in kindleprefs and kindleprefs['wineprefix'] != "":
dedrmprefs.set('adobewineprefix',kindleprefs['wineprefix']) dedrmprefs.set('adobewineprefix',kindleprefs['wineprefix'])
dedrmprefs.set('kindlewineprefix',kindleprefs['wineprefix']) dedrmprefs.set('kindlewineprefix',kindleprefs['wineprefix'])
print u"{0} v{1}: WINEPREFIX (2) imported from Kindle plugin preferences".format(PLUGIN_NAME, PLUGIN_VERSION, kindleprefs['wineprefix']) print("{0} v{1}: WINEPREFIX (2) imported from Kindle plugin preferences".format(PLUGIN_NAME, PLUGIN_VERSION, kindleprefs['wineprefix']))
except: except:
traceback.print_exc() traceback.print_exc()
# Make the json write all the prefs to disk # Make the json write all the prefs to disk
dedrmprefs.writeprefs() dedrmprefs.writeprefs()
print u"{0} v{1}: Finished setting up configuration data.".format(PLUGIN_NAME, PLUGIN_VERSION) print("{0} v{1}: Finished setting up configuration data.".format(PLUGIN_NAME, PLUGIN_VERSION))

View File

@@ -1,19 +1,19 @@
#!/usr/bin/env python #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab # vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
from __future__ import print_function
import sys import sys
import os import os
import re import re
import ineptepub
import ignobleepub
import epubtest
import zipfix
import ineptpdf
import erdr2pml
import k4mobidedrm
import traceback import traceback
import calibre_plugins.dedrm.ineptepub
import calibre_plugins.dedrm.ignobleepub
import calibre_plugins.dedrm.epubtest
import calibre_plugins.dedrm.zipfix
import calibre_plugins.dedrm.ineptpdf
import calibre_plugins.dedrm.erdr2pml
import calibre_plugins.dedrm.k4mobidedrm
def decryptepub(infile, outdir, rscpath): def decryptepub(infile, outdir, rscpath):
errlog = '' errlog = ''
@@ -46,7 +46,7 @@ def decryptepub(infile, outdir, rscpath):
if rv == 0: if rv == 0:
print("Decrypted Adobe ePub with key file {0}".format(filename)) print("Decrypted Adobe ePub with key file {0}".format(filename))
break break
except Exception, e: except Exception as e:
errlog += traceback.format_exc() errlog += traceback.format_exc()
errlog += str(e) errlog += str(e)
rv = 1 rv = 1
@@ -66,7 +66,7 @@ def decryptepub(infile, outdir, rscpath):
if rv == 0: 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 break
except Exception, e: except Exception as e:
errlog += traceback.format_exc() errlog += traceback.format_exc()
errlog += str(e) errlog += str(e)
rv = 1 rv = 1
@@ -104,7 +104,7 @@ def decryptpdf(infile, outdir, rscpath):
rv = ineptpdf.decryptBook(userkey, infile, outfile) rv = ineptpdf.decryptBook(userkey, infile, outfile)
if rv == 0: if rv == 0:
break break
except Exception, e: except Exception as e:
errlog += traceback.format_exc() errlog += traceback.format_exc()
errlog += str(e) errlog += str(e)
rv = 1 rv = 1
@@ -132,7 +132,7 @@ def decryptpdb(infile, outdir, rscpath):
return 1 return 1
try: try:
rv = erdr2pml.decryptBook(infile, outpath, True, erdr2pml.getuser_key(name, cc8)) rv = erdr2pml.decryptBook(infile, outpath, True, erdr2pml.getuser_key(name, cc8))
except Exception, e: except Exception as e:
errlog += traceback.format_exc() errlog += traceback.format_exc()
errlog += str(e) errlog += str(e)
rv = 1 rv = 1
@@ -193,7 +193,7 @@ def decryptk4mobi(infile, outdir, rscpath):
androidFiles.append(dpath) androidFiles.append(dpath)
try: try:
rv = k4mobidedrm.decryptBook(infile, outdir, kDatabaseFiles, androidFiles, serialnums, pidnums) rv = k4mobidedrm.decryptBook(infile, outdir, kDatabaseFiles, androidFiles, serialnums, pidnums)
except Exception, e: except Exception as e:
errlog += traceback.format_exc() errlog += traceback.format_exc()
errlog += str(e) errlog += str(e)
rv = 1 rv = 1

View File

@@ -1,23 +1,23 @@
#!/usr/bin/env python #!/usr/bin/env python
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab # vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
import Tkinter import tkinter
import Tkconstants import tkinter.constants
# basic scrolled text widget # basic scrolled text widget
class ScrolledText(Tkinter.Text): class ScrolledText(tkinter.Text):
def __init__(self, master=None, **kw): def __init__(self, master=None, **kw):
self.frame = Tkinter.Frame(master) self.frame = tkinter.Frame(master)
self.vbar = Tkinter.Scrollbar(self.frame) self.vbar = tkinter.Scrollbar(self.frame)
self.vbar.pack(side=Tkconstants.RIGHT, fill=Tkconstants.Y) self.vbar.pack(side=tkinter.constants.RIGHT, fill=tkinter.constants.Y)
kw.update({'yscrollcommand': self.vbar.set}) kw.update({'yscrollcommand': self.vbar.set})
Tkinter.Text.__init__(self, self.frame, **kw) tkinter.Text.__init__(self, self.frame, **kw)
self.pack(side=Tkconstants.LEFT, fill=Tkconstants.BOTH, expand=True) self.pack(side=tkinter.constants.LEFT, fill=tkinter.constants.BOTH, expand=True)
self.vbar['command'] = self.yview self.vbar['command'] = self.yview
# Copy geometry methods of self.frame without overriding Text # Copy geometry methods of self.frame without overriding Text
# methods = hack! # methods = hack!
text_meths = vars(Tkinter.Text).keys() text_meths = list(vars(tkinter.Text).keys())
methods = vars(Tkinter.Pack).keys() + vars(Tkinter.Grid).keys() + vars(Tkinter.Place).keys() methods = list(vars(tkinter.Pack).keys()) + list(vars(tkinter.Grid).keys()) + list(vars(tkinter.Place).keys())
methods = set(methods).difference(text_meths) methods = set(methods).difference(text_meths)
for m in methods: for m in methods:
if m[0] != '_' and m != 'config' and m != 'configure': if m[0] != '_' and m != 'config' and m != 'configure':

View File

@@ -1,4 +1,5 @@
#!/usr/bin/env python #!/usr/bin/env python3
# -*- coding: utf-8 -*-
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab # vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
import sys import sys
@@ -19,7 +20,10 @@ class SimplePrefs(object):
self.file2key[filename] = key self.file2key[filename] = key
self.target = target + 'Prefs' self.target = target + 'Prefs'
if sys.platform.startswith('win'): if sys.platform.startswith('win'):
import _winreg as winreg try:
import winreg
except ImportError:
import _winreg as winreg
regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\") regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\")
path = winreg.QueryValueEx(regkey, 'Local AppData')[0] path = winreg.QueryValueEx(regkey, 'Local AppData')[0]
prefdir = path + os.sep + self.target prefdir = path + os.sep + self.target
@@ -46,7 +50,7 @@ class SimplePrefs(object):
try : try :
data = file(filepath,'rb').read() data = file(filepath,'rb').read()
self.prefs[key] = data self.prefs[key] = data
except Exception, e: except Exception as e:
pass pass
def getPreferences(self): def getPreferences(self):
@@ -71,7 +75,7 @@ class SimplePrefs(object):
else: else:
try: try:
file(filepath,'wb').write(data) file(filepath,'wb').write(data)
except Exception, e: except Exception as e:
pass pass
self.prefs = newprefs self.prefs = newprefs
return return

View File

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

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# topazextract.py # topazextract.py
@@ -7,9 +7,9 @@
# Changelog # Changelog
# 4.9 - moved unicode_argv call inside main for Windows DeDRM compatibility # 4.9 - moved unicode_argv call inside main for Windows DeDRM compatibility
# 5.0 - Fixed potential unicode problem with command line interface # 5.0 - Fixed potential unicode problem with command line interface
# 6.0 - Added Python 3 compatibility for calibre 5.0
from __future__ import print_function __version__ = '6.0'
__version__ = '5.0'
import sys import sys
import os, csv, getopt import os, csv, getopt
@@ -17,8 +17,14 @@ import zlib, zipfile, tempfile, shutil
import traceback import traceback
from struct import pack from struct import pack
from struct import unpack from struct import unpack
from alfcrypto import Topaz_Cipher try:
from calibre_plugins.dedrm.alfcrypto import Topaz_Cipher
except:
from alfcrypto import Topaz_Cipher
# 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: class SafeUnbuffered:
def __init__(self, stream): def __init__(self, stream):
self.stream = stream self.stream = stream
@@ -26,10 +32,17 @@ class SafeUnbuffered:
if self.encoding == None: if self.encoding == None:
self.encoding = "utf-8" self.encoding = "utf-8"
def write(self, data): def write(self, data):
if isinstance(data,unicode): if isinstance(data,str) or isinstance(data,unicode):
# str for Python3, unicode for Python2
data = data.encode(self.encoding,"replace") data = data.encode(self.encoding,"replace")
self.stream.write(data) try:
self.stream.flush() buffer = getattr(self.stream, 'buffer', self.stream)
# self.stream.buffer for Python3, self.stream for Python2
buffer.write(data)
buffer.flush()
except:
# We can do nothing if a write fails
raise
def __getattr__(self, attr): def __getattr__(self, attr):
return getattr(self.stream, attr) return getattr(self.stream, attr)
@@ -64,15 +77,13 @@ def unicode_argv():
# Remove Python executable and commands if present # Remove Python executable and commands if present
start = argc.value - len(sys.argv) start = argc.value - len(sys.argv)
return [argv[i] for i in return [argv[i] for i in
xrange(start, argc.value)] range(start, argc.value)]
# if we don't have any arguments at all, just pass back script name # if we don't have any arguments at all, just pass back script name
# this should never happen # this should never happen
return [u"mobidedrm.py"] return ["mobidedrm.py"]
else: else:
argvencoding = sys.stdin.encoding argvencoding = sys.stdin.encoding or "utf-8"
if argvencoding == None: return [arg if (isinstance(arg, str) or isinstance(arg,unicode)) else str(arg, argvencoding) for arg in sys.argv]
argvencoding = 'utf-8'
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
#global switch #global switch
debug = False debug = False
@@ -92,7 +103,7 @@ class DrmException(Exception):
# recursive zip creation support routine # recursive zip creation support routine
def zipUpDir(myzip, tdir, localname): def zipUpDir(myzip, tdir, localname):
currentdir = tdir currentdir = tdir
if localname != u"": if localname != "":
currentdir = os.path.join(currentdir,localname) currentdir = os.path.join(currentdir,localname)
list = os.listdir(currentdir) list = os.listdir(currentdir)
for file in list: for file in list:
@@ -168,21 +179,21 @@ def decryptRecord(data,PID):
def decryptDkeyRecord(data,PID): def decryptDkeyRecord(data,PID):
record = decryptRecord(data,PID) record = decryptRecord(data,PID)
fields = unpack('3sB8sB8s3s',record) fields = unpack('3sB8sB8s3s',record)
if fields[0] != 'PID' or fields[5] != 'pid' : if fields[0] != b'PID' or fields[5] != b'pid' :
raise DrmException(u"Didn't find PID magic numbers in record") raise DrmException("Didn't find PID magic numbers in record")
elif fields[1] != 8 or fields[3] != 8 : 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 : elif fields[2] != PID :
raise DrmException(u"Record didn't contain PID") raise DrmException("Record didn't contain PID")
return fields[4] return fields[4]
# Decrypt all dkey records (contain the book PID) # Decrypt all dkey records (contain the book PID)
def decryptDkeyRecords(data,PID): def decryptDkeyRecords(data,PID):
nbKeyRecords = ord(data[0]) nbKeyRecords = data[0]
records = [] records = []
data = data[1:] data = data[1:]
for i in range (0,nbKeyRecords): for i in range (0,nbKeyRecords):
length = ord(data[0]) length = data[0]
try: try:
key = decryptDkeyRecord(data[1:length+1],PID) key = decryptDkeyRecord(data[1:length+1],PID)
records.append(key) records.append(key)
@@ -190,13 +201,13 @@ def decryptDkeyRecords(data,PID):
pass pass
data = data[1+length:] data = data[1+length:]
if len(records) == 0: if len(records) == 0:
raise DrmException(u"BookKey Not Found") raise DrmException("BookKey Not Found")
return records return records
class TopazBook: class TopazBook:
def __init__(self, filename): def __init__(self, filename):
self.fo = file(filename, 'rb') self.fo = open(filename, 'rb')
self.outdir = tempfile.mkdtemp() self.outdir = tempfile.mkdtemp()
# self.outdir = 'rawdat' # self.outdir = 'rawdat'
self.bookPayloadOffset = 0 self.bookPayloadOffset = 0
@@ -204,8 +215,8 @@ class TopazBook:
self.bookMetadata = {} self.bookMetadata = {}
self.bookKey = None self.bookKey = None
magic = unpack('4s',self.fo.read(4))[0] magic = unpack('4s',self.fo.read(4))[0]
if magic != 'TPZ0': if magic != b'TPZ0':
raise DrmException(u"Parse Error : Invalid Header, not a Topaz file") raise DrmException("Parse Error : Invalid Header, not a Topaz file")
self.parseTopazHeaders() self.parseTopazHeaders()
self.parseMetadata() self.parseMetadata()
@@ -223,7 +234,7 @@ class TopazBook:
# Read and parse one header record at the current book file position and return the associated data # Read and parse one header record at the current book file position and return the associated data
# [[offset,decompressedLength,compressedLength],...] # [[offset,decompressedLength,compressedLength],...]
if ord(self.fo.read(1)) != 0x63: if ord(self.fo.read(1)) != 0x63:
raise DrmException(u"Parse Error : Invalid Header") raise DrmException("Parse Error : Invalid Header")
tag = bookReadString(self.fo) tag = bookReadString(self.fo)
record = bookReadHeaderRecordData() record = bookReadHeaderRecordData()
return [tag,record] return [tag,record]
@@ -234,15 +245,15 @@ class TopazBook:
if debug: print(result[0], ": ", result[1]) if debug: print(result[0], ": ", result[1])
self.bookHeaderRecords[result[0]] = result[1] self.bookHeaderRecords[result[0]] = result[1]
if ord(self.fo.read(1)) != 0x64 : 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() self.bookPayloadOffset = self.fo.tell()
def parseMetadata(self): def parseMetadata(self):
# Parse the metadata record from the book payload and return a list of [key,values] # 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) tag = bookReadString(self.fo)
if tag != 'metadata' : if tag != b'metadata' :
raise DrmException(u"Parse Error : Record Names Don't Match") raise DrmException("Parse Error : Record Names Don't Match")
flags = ord(self.fo.read(1)) flags = ord(self.fo.read(1))
nbRecords = 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)
@@ -255,18 +266,18 @@ class TopazBook:
return self.bookMetadata return self.bookMetadata
def getPIDMetaInfo(self): def getPIDMetaInfo(self):
keysRecord = self.bookMetadata.get('keys','') keysRecord = self.bookMetadata.get(b'keys',b'')
keysRecordRecord = '' keysRecordRecord = b''
if keysRecord != '': if keysRecord != b'':
keylst = keysRecord.split(',') keylst = keysRecord.split(b',')
for keyval in keylst: for keyval in keylst:
keysRecordRecord += self.bookMetadata.get(keyval,'') keysRecordRecord += self.bookMetadata.get(keyval,b'')
return keysRecord, keysRecordRecord return keysRecord, keysRecordRecord
def getBookTitle(self): def getBookTitle(self):
title = '' title = b''
if 'Title' in self.bookMetadata: if b'Title' in self.bookMetadata:
title = self.bookMetadata['Title'] title = self.bookMetadata[b'Title']
return title.decode('utf-8') return title.decode('utf-8')
def setBookKey(self, key): def setBookKey(self, key):
@@ -318,13 +329,13 @@ class TopazBook:
raw = 0 raw = 0
fixedimage=True fixedimage=True
try: try:
keydata = self.getBookPayloadRecord('dkey', 0) keydata = self.getBookPayloadRecord(b'dkey', 0)
except DrmException, e: except DrmException as e:
print(u"no dkey record found, book may not be encrypted") print("no dkey record found, book may not be encrypted")
print(u"attempting to extrct files without a book key") print("attempting to extrct files without a book key")
self.createBookDirectory() self.createBookDirectory()
self.extractFiles() self.extractFiles()
print(u"Successfully Extracted Topaz contents") print("Successfully Extracted Topaz contents")
if inCalibre: if inCalibre:
from calibre_plugins.dedrm import genbook from calibre_plugins.dedrm import genbook
else: else:
@@ -332,7 +343,7 @@ class TopazBook:
rv = genbook.generateBook(self.outdir, raw, fixedimage) rv = genbook.generateBook(self.outdir, raw, fixedimage)
if rv == 0: if rv == 0:
print(u"Book Successfully generated.") print("Book Successfully generated.")
return rv return rv
# try each pid to decode the file # try each pid to decode the file
@@ -340,25 +351,25 @@ class TopazBook:
for pid in pidlst: for pid in pidlst:
# use 8 digit pids here # use 8 digit pids here
pid = pid[0:8] pid = pid[0:8]
print(u"Trying: {0}".format(pid)) print("Trying: {0}".format(pid))
bookKeys = [] bookKeys = []
data = keydata data = keydata
try: try:
bookKeys+=decryptDkeyRecords(data,pid) bookKeys+=decryptDkeyRecords(data,pid)
except DrmException, e: except DrmException as e:
pass pass
else: else:
bookKey = bookKeys[0] bookKey = bookKeys[0]
print(u"Book Key Found! ({0})".format(bookKey.encode('hex'))) print("Book Key Found! ({0})".format(bookKey.hex()))
break break
if not bookKey: 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 Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(len(pidlst)))
self.setBookKey(bookKey) self.setBookKey(bookKey)
self.createBookDirectory() self.createBookDirectory()
self.extractFiles() self.extractFiles()
print(u"Successfully Extracted Topaz contents") print("Successfully Extracted Topaz contents")
if inCalibre: if inCalibre:
from calibre_plugins.dedrm import genbook from calibre_plugins.dedrm import genbook
else: else:
@@ -366,7 +377,7 @@ class TopazBook:
rv = genbook.generateBook(self.outdir, raw, fixedimage) rv = genbook.generateBook(self.outdir, raw, fixedimage)
if rv == 0: if rv == 0:
print(u"Book Successfully generated") print("Book Successfully generated")
return rv return rv
def createBookDirectory(self): def createBookDirectory(self):
@@ -374,16 +385,16 @@ class TopazBook:
# create output directory structure # create output directory structure
if not os.path.exists(outdir): if not os.path.exists(outdir):
os.makedirs(outdir) os.makedirs(outdir)
destdir = os.path.join(outdir,u"img") destdir = os.path.join(outdir,"img")
if not os.path.exists(destdir): if not os.path.exists(destdir):
os.makedirs(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): if not os.path.exists(destdir):
os.makedirs(destdir) os.makedirs(destdir)
destdir = os.path.join(outdir,u"page") destdir = os.path.join(outdir,"page")
if not os.path.exists(destdir): if not os.path.exists(destdir):
os.makedirs(destdir) os.makedirs(destdir)
destdir = os.path.join(outdir,u"glyphs") destdir = os.path.join(outdir,"glyphs")
if not os.path.exists(destdir): if not os.path.exists(destdir):
os.makedirs(destdir) os.makedirs(destdir)
@@ -391,50 +402,50 @@ class TopazBook:
outdir = self.outdir outdir = self.outdir
for headerRecord in self.bookHeaderRecords: for headerRecord in self.bookHeaderRecords:
name = headerRecord name = headerRecord
if name != 'dkey': if name != b'dkey':
ext = u".dat" ext = ".dat"
if name == 'img': ext = u".jpg" if name == b'img': ext = ".jpg"
if name == 'color' : ext = u".jpg" if name == b'color' : ext = ".jpg"
print(u"Processing Section: {0}\n. . .".format(name), end=' ') print("Processing Section: {0}\n. . .".format(name.decode('utf-8')), end=' ')
for index in range (0,len(self.bookHeaderRecords[name])) : 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 destdir = outdir
if name == 'img': if name == b'img':
destdir = os.path.join(outdir,u"img") destdir = os.path.join(outdir,"img")
if name == 'color': if name == b'color':
destdir = os.path.join(outdir,u"color_img") destdir = os.path.join(outdir,"color_img")
if name == 'page': if name == b'page':
destdir = os.path.join(outdir,u"page") destdir = os.path.join(outdir,"page")
if name == 'glyphs': if name == b'glyphs':
destdir = os.path.join(outdir,u"glyphs") destdir = os.path.join(outdir,"glyphs")
outputFile = os.path.join(destdir,fname) outputFile = os.path.join(destdir,fname)
print(u".", end=' ') print(".", end=' ')
record = self.getBookPayloadRecord(name,index) record = self.getBookPayloadRecord(name,index)
if record != '': if record != b'':
file(outputFile, 'wb').write(record) open(outputFile, 'wb').write(record)
print(u" ") print(" ")
def getFile(self, zipname): def getFile(self, zipname):
htmlzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False) 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,"book.html"),"book.html")
htmlzip.write(os.path.join(self.outdir,u"book.opf"),u"book.opf") htmlzip.write(os.path.join(self.outdir,"book.opf"),"book.opf")
if os.path.isfile(os.path.join(self.outdir,u"cover.jpg")): if os.path.isfile(os.path.join(self.outdir,"cover.jpg")):
htmlzip.write(os.path.join(self.outdir,u"cover.jpg"),u"cover.jpg") htmlzip.write(os.path.join(self.outdir,"cover.jpg"),"cover.jpg")
htmlzip.write(os.path.join(self.outdir,u"style.css"),u"style.css") htmlzip.write(os.path.join(self.outdir,"style.css"),"style.css")
zipUpDir(htmlzip, self.outdir, u"img") zipUpDir(htmlzip, self.outdir, "img")
htmlzip.close() htmlzip.close()
def getBookType(self): def getBookType(self):
return u"Topaz" return "Topaz"
def getBookExtension(self): def getBookExtension(self):
return u".htmlz" return ".htmlz"
def getSVGZip(self, zipname): def getSVGZip(self, zipname):
svgzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False) svgzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
svgzip.write(os.path.join(self.outdir,u"index_svg.xhtml"),u"index_svg.xhtml") svgzip.write(os.path.join(self.outdir,"index_svg.xhtml"),"index_svg.xhtml")
zipUpDir(svgzip, self.outdir, u"svg") zipUpDir(svgzip, self.outdir, "svg")
zipUpDir(svgzip, self.outdir, u"img") zipUpDir(svgzip, self.outdir, "img")
svgzip.close() svgzip.close()
def cleanup(self): def cleanup(self):
@@ -442,20 +453,20 @@ class TopazBook:
shutil.rmtree(self.outdir, True) shutil.rmtree(self.outdir, True)
def usage(progname): def usage(progname):
print(u"Removes DRM protection from Topaz ebooks and extracts the contents") print("Removes DRM protection from Topaz ebooks and extracts the contents")
print(u"Usage:") print("Usage:")
print(u" {0} [-k <kindle.k4i>] [-p <comma separated PIDs>] [-s <comma separated Kindle serial numbers>] <infile> <outdir>".format(progname)) print(" {0} [-k <kindle.k4i>] [-p <comma separated PIDs>] [-s <comma separated Kindle serial numbers>] <infile> <outdir>".format(progname))
# Main # Main
def cli_main(): def cli_main():
argv=unicode_argv() argv=unicode_argv()
progname = os.path.basename(argv[0]) progname = os.path.basename(argv[0])
print(u"TopazExtract v{0}.".format(__version__)) print("TopazExtract v{0}.".format(__version__))
try: try:
opts, args = getopt.getopt(argv[1:], "k:p:s:x") opts, args = getopt.getopt(argv[1:], "k:p:s:x")
except getopt.GetoptError, err: except getopt.GetoptError as err:
print(u"Error in options or arguments: {0}".format(err.args[0])) print("Error in options or arguments: {0}".format(err.args[0]))
usage(progname) usage(progname)
return 1 return 1
if len(args)<2: if len(args)<2:
@@ -465,11 +476,11 @@ def cli_main():
infile = args[0] infile = args[0]
outdir = args[1] outdir = args[1]
if not os.path.isfile(infile): 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 return 1
if not os.path.exists(outdir): 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 return 1
kDatabaseFiles = [] kDatabaseFiles = []
@@ -494,27 +505,27 @@ def cli_main():
tb = TopazBook(infile) tb = TopazBook(infile)
title = tb.getBookTitle() title = tb.getBookTitle()
print(u"Processing Book: {0}".format(title)) print("Processing Book: {0}".format(title))
md1, md2 = tb.getPIDMetaInfo() md1, md2 = tb.getPIDMetaInfo()
pids.extend(kgenpids.getPidList(md1, md2, serials, kDatabaseFiles)) pids.extend(kgenpids.getPidList(md1, md2, serials, kDatabaseFiles))
try: try:
print(u"Decrypting Book") print("Decrypting Book")
tb.processBook(pids) tb.processBook(pids)
print(u" Creating HTML ZIP Archive") print(" Creating HTML ZIP Archive")
zipname = os.path.join(outdir, bookname + u"_nodrm.htmlz") zipname = os.path.join(outdir, bookname + "_nodrm.htmlz")
tb.getFile(zipname) tb.getFile(zipname)
print(u" Creating SVG ZIP Archive") print(" Creating SVG ZIP Archive")
zipname = os.path.join(outdir, bookname + u"_SVG.zip") zipname = os.path.join(outdir, bookname + "_SVG.zip")
tb.getSVGZip(zipname) tb.getSVGZip(zipname)
# removing internal temporary directory of pieces # removing internal temporary directory of pieces
tb.cleanup() tb.cleanup()
except DrmException, e: except DrmException as e:
print(u"Decryption failed\n{0}".format(traceback.format_exc())) print("Decryption failed\n{0}".format(traceback.format_exc()))
try: try:
tb.cleanup() tb.cleanup()
@@ -522,8 +533,8 @@ def cli_main():
pass pass
return 1 return 1
except Exception, e: except Exception as e:
print(u"Decryption failed\m{0}".format(traceback.format_exc())) print("Decryption failed\n{0}".format(traceback.format_exc()))
try: try:
tb.cleanup() tb.cleanup()
except: except:

View File

@@ -1,9 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import with_statement from calibre_plugins.dedrm.ignoblekeygen import generate_key
from ignoblekeygen import generate_key
__license__ = 'GPL v3' __license__ = 'GPL v3'
@@ -21,8 +19,8 @@ DETAILED_MESSAGE = \
def uStrCmp (s1, s2, caseless=False): def uStrCmp (s1, s2, caseless=False):
import unicodedata as ud import unicodedata as ud
str1 = s1 if isinstance(s1, unicode) else unicode(s1) str1 = s1 if isinstance(s1, str) else str(s1)
str2 = s2 if isinstance(s2, unicode) else unicode(s2) str2 = s2 if isinstance(s2, str) else str(s2)
if caseless: if caseless:
return ud.normalize('NFC', str1.lower()) == ud.normalize('NFC', str2.lower()) return ud.normalize('NFC', str1.lower()) == ud.normalize('NFC', str2.lower())
else: else:

View File

@@ -1,76 +1,114 @@
#!/usr/bin/env python #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import with_statement
from __future__ import print_function
__license__ = 'GPL v3' __license__ = 'GPL v3'
# Standard Python modules. # Standard Python modules.
import os, sys, re, hashlib, traceback import os, sys, re, hashlib, traceback
from calibre_plugins.dedrm.__init__ import PLUGIN_NAME, PLUGIN_VERSION from calibre_plugins.dedrm.__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=""): def WineGetKeys(scriptpath, extension, wineprefix=""):
import subprocess
from subprocess import Popen, PIPE, STDOUT
import subasyncio if extension == ".k4i":
from subasyncio import Process
if extension == u".k4i":
import json import json
basepath, script = os.path.split(scriptpath) try:
print(u"{0} v{1}: Running {2} under Wine".format(PLUGIN_NAME, PLUGIN_VERSION, script)) pyexec = WinePythonCLI(wineprefix)
except NoWinePython3Exception:
print('{0} v{1}: Unable to find python3 executable in WINEPREFIX="{2}"'.format(PLUGIN_NAME, PLUGIN_VERSION, wineprefix))
return []
outdirpath = os.path.join(basepath, u"winekeysdir") 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): if not os.path.exists(outdirpath):
os.makedirs(outdirpath) os.makedirs(outdirpath)
if wineprefix != "": if wineprefix != "":
wineprefix = os.path.abspath(os.path.expanduser(os.path.expandvars(wineprefix))) wineprefix = os.path.abspath(os.path.expanduser(os.path.expandvars(wineprefix)))
if wineprefix != "" and os.path.exists(wineprefix):
cmdline = u"WINEPREFIX=\"{2}\" wine python.exe \"{0}\" \"{1}\"".format(scriptpath,outdirpath,wineprefix)
else:
cmdline = u"wine python.exe \"{0}\" \"{1}\"".format(scriptpath,outdirpath)
print(u"{0} v{1}: Command line: '{2}'".format(PLUGIN_NAME, PLUGIN_VERSION, cmdline))
try: try:
cmdline = cmdline.encode(sys.getfilesystemencoding()) result = pyexec.check_call([scriptpath, outdirpath])
p2 = Process(cmdline, shell=True, bufsize=1, stdin=None, stdout=sys.stdout, stderr=STDOUT, close_fds=False) except Exception as e:
result = p2.wait("wait") print("{0} v{1}: Wine subprocess call error: {2}".format(PLUGIN_NAME, PLUGIN_VERSION, e.args[0]))
except Exception, e:
print(u"{0} v{1}: Wine subprocess call error: {2}".format(PLUGIN_NAME, PLUGIN_VERSION, e.args[0]))
if wineprefix != "" and os.path.exists(wineprefix):
cmdline = u"WINEPREFIX=\"{2}\" wine C:\\Python27\\python.exe \"{0}\" \"{1}\"".format(scriptpath,outdirpath,wineprefix)
else:
cmdline = u"wine C:\\Python27\\python.exe \"{0}\" \"{1}\"".format(scriptpath,outdirpath)
print(u"{0} v{1}: Command line: “{2}".format(PLUGIN_NAME, PLUGIN_VERSION, cmdline))
try:
cmdline = cmdline.encode(sys.getfilesystemencoding())
p2 = Process(cmdline, shell=True, bufsize=1, stdin=None, stdout=sys.stdout, stderr=STDOUT, close_fds=False)
result = p2.wait("wait")
except Exception, e:
print(u"{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 # try finding winekeys anyway, even if above code errored
winekeys = [] winekeys = []
winekey_names = []
# get any files with extension in the output dir # get any files with extension in the output dir
files = [f for f in os.listdir(outdirpath) if f.endswith(extension)] files = [f for f in os.listdir(outdirpath) if f.endswith(extension)]
for filename in files: for filename in files:
try: try:
fpath = os.path.join(outdirpath, filename) fpath = os.path.join(outdirpath, filename)
with open(fpath, 'rb') as keyfile: with open(fpath, 'rb') as keyfile:
if extension == u".k4i": if extension == ".k4i":
new_key_value = json.loads(keyfile.read()) new_key_value = json.loads(keyfile.read())
else: else:
new_key_value = keyfile.read() new_key_value = keyfile.read()
winekeys.append(new_key_value) winekeys.append(new_key_value)
winekey_names.append(filename)
except: except:
print(u"{0} v{1}: Error loading file {2}".format(PLUGIN_NAME, PLUGIN_VERSION, filename)) print("{0} v{1}: Error loading file {2}".format(PLUGIN_NAME, PLUGIN_VERSION, filename))
traceback.print_exc() traceback.print_exc()
os.remove(fpath) os.remove(fpath)
print(u"{0} v{1}: Found and decrypted {2} {3}".format(PLUGIN_NAME, PLUGIN_VERSION, len(winekeys), u"key file" if len(winekeys) == 1 else u"key files")) print("{0} v{1}: Found and decrypted {2} {3}".format(PLUGIN_NAME, PLUGIN_VERSION, len(winekeys), "key file" if len(winekeys) == 1 else "key files"))
return winekeys return winekeys, winekey_names

186
DeDRM_plugin/zipfilerugged.py Normal file → Executable file
View File

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

View File

@@ -1,8 +1,8 @@
#!/usr/bin/env python #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# zipfix.py, version 1.1 # zipfix.py
# Copyright © 2010-2013 by some_updates, DiapDealer and Apprentice Alf # Copyright © 2010-2020 by Apprentice Harper et al.
# Released under the terms of the GNU General Public Licence, version 3 # Released under the terms of the GNU General Public Licence, version 3
# <http://www.gnu.org/licenses/> # <http://www.gnu.org/licenses/>
@@ -10,18 +10,22 @@
# Revision history: # Revision history:
# 1.0 - Initial release # 1.0 - Initial release
# 1.1 - Updated to handle zip file metadata correctly # 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). Re-write zip (or ePub) fixing problems with file names (and mimetype entry).
""" """
from __future__ import print_function
__license__ = 'GPL v3' __license__ = 'GPL v3'
__version__ = "1.1" __version__ = "1.1"
import sys import sys
import zlib import zlib
import zipfilerugged try:
import zipfilerugged
except:
import calibre_plugins.dedrm.zipfilerugged as zipfilerugged
import os import os
import os.path import os.path
import getopt import getopt
@@ -49,7 +53,7 @@ class fixZip:
self.inzip = zipfilerugged.ZipFile(zinput,'r') self.inzip = zipfilerugged.ZipFile(zinput,'r')
self.outzip = zipfilerugged.ZipFile(zoutput,'w') self.outzip = zipfilerugged.ZipFile(zoutput,'w')
# open the input zip for reading only as a raw file # 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): def getlocalname(self, zi):
local_header_offset = zi.header_offset local_header_offset = zi.header_offset
@@ -62,21 +66,21 @@ class fixZip:
def uncompress(self, cmpdata): def uncompress(self, cmpdata):
dc = zlib.decompressobj(-15) dc = zlib.decompressobj(-15)
data = '' data = b''
while len(cmpdata) > 0: while len(cmpdata) > 0:
if len(cmpdata) > _MAX_SIZE : if len(cmpdata) > _MAX_SIZE :
newdata = cmpdata[0:_MAX_SIZE] newdata = cmpdata[0:_MAX_SIZE]
cmpdata = cmpdata[_MAX_SIZE:] cmpdata = cmpdata[_MAX_SIZE:]
else: else:
newdata = cmpdata newdata = cmpdata
cmpdata = '' cmpdata = b''
newdata = dc.decompress(newdata) newdata = dc.decompress(newdata)
unprocessed = dc.unconsumed_tail unprocessed = dc.unconsumed_tail
if len(unprocessed) == 0: if len(unprocessed) == 0:
newdata += dc.flush() newdata += dc.flush()
data += newdata data += newdata
cmpdata += unprocessed cmpdata += unprocessed
unprocessed = '' unprocessed = b''
return data return data
def getfiledata(self, zi): def getfiledata(self, zi):
@@ -115,11 +119,11 @@ class fixZip:
# if epub write mimetype file first, with no compression # if epub write mimetype file first, with no compression
if self.ztype == 'epub': if self.ztype == 'epub':
# first get a ZipInfo with current time and no compression # first get a ZipInfo with current time and no compression
mimeinfo = ZipInfo('mimetype',compress_type=zipfilerugged.ZIP_STORED) mimeinfo = ZipInfo(b'mimetype',compress_type=zipfilerugged.ZIP_STORED)
mimeinfo.internal_attr = 1 # text file mimeinfo.internal_attr = 1 # text file
try: try:
# if the mimetype is present, get its info, including time-stamp # if the mimetype is present, get its info, including time-stamp
oldmimeinfo = self.inzip.getinfo('mimetype') oldmimeinfo = self.inzip.getinfo(b'mimetype')
# copy across useful fields # copy across useful fields
mimeinfo.date_time = oldmimeinfo.date_time mimeinfo.date_time = oldmimeinfo.date_time
mimeinfo.comment = oldmimeinfo.comment mimeinfo.comment = oldmimeinfo.comment
@@ -129,11 +133,11 @@ class fixZip:
mimeinfo.create_system = oldmimeinfo.create_system mimeinfo.create_system = oldmimeinfo.create_system
except: except:
pass pass
self.outzip.writestr(mimeinfo, _MIMETYPE) self.outzip.writestr(mimeinfo, _MIMETYPE.encode('ascii'))
# write the rest of the files # write the rest of the files
for zinfo in self.inzip.infolist(): 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 data = None
try: try:
data = self.inzip.read(zinfo.filename) data = self.inzip.read(zinfo.filename)
@@ -149,6 +153,7 @@ class fixZip:
nzinfo.internal_attr=zinfo.internal_attr nzinfo.internal_attr=zinfo.internal_attr
nzinfo.external_attr=zinfo.external_attr nzinfo.external_attr=zinfo.external_attr
nzinfo.create_system=zinfo.create_system nzinfo.create_system=zinfo.create_system
nzinfo.flag_bits = zinfo.flag_bits & 0x800 # preserve UTF-8 flag
self.outzip.writestr(nzinfo,data) self.outzip.writestr(nzinfo,data)
self.bzf.close() self.bzf.close()
@@ -171,7 +176,7 @@ def repairBook(infile, outfile):
fr = fixZip(infile, outfile) fr = fixZip(infile, outfile)
fr.fix() fr.fix()
return 0 return 0
except Exception, e: except Exception as e:
print("Error Occurred ", e) print("Error Occurred ", e)
return 2 return 2

View File

@@ -7,7 +7,7 @@ This plugin will remove the DRM from:
- Adobe Digital Editions (v2.0.1***) ePubs (including Kobo and Google ePubs downloaded to ADE) - Adobe Digital Editions (v2.0.1***) ePubs (including Kobo and Google ePubs downloaded to ADE)
- Adobe Digital Editions (v2.0.1) PDFs - Adobe Digital Editions (v2.0.1) PDFs
For limitations and work-arounds, see the FAQ at https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md For limitations and work-arounds, see the FAQ at https://github.com/noDRM/DeDRM_tools/blob/master/FAQs.md (or the FAQ in Apprentice Harper's original repository at https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md)
Installation Installation
@@ -31,4 +31,6 @@ If you find that the DeDRM plugin is not working for you (imported ebooks still
- Once calibre has re-started, import the problem ebook. - Once calibre has re-started, import the problem ebook.
- Now close calibre. - Now close calibre.
A log will appear that you can copy and paste into a comment at Apprentice Alf's blog, http://apprenticealf.wordpress.com/ or an issue at Apprentice Harper's repository, https://github.com/apprenticeharper/DeDRM_tools/issues . You should also give details of your computer, and how you obtained the ebook file. A log will appear that you can copy and paste into a GitHub issue report at https://github.com/noDRM/DeDRM_tools/issues. Please also include information about the eBook file.
If you're using Apprentice Harper's original version, you can also comment at Apprentice Alf's blog, http://apprenticealf.wordpress.com/ or open an issue at Apprentice Harper's repository, https://github.com/apprenticeharper/DeDRM_tools/issues.

43
FAQs.md
View File

@@ -18,23 +18,23 @@ Just download and use these tools, that's all! Uh, almost. There are a few, uh,
But otherwise, if your ebook is from Amazon, Kobo, Barnes & Noble or any of the ebook stores selling ebooks compatible with Adobe Digital Editions 2.0.1, you should be able to remove the DRM that's been applied to your ebooks. But otherwise, if your ebook is from Amazon, Kobo, Barnes & Noble or any of the ebook stores selling ebooks compatible with Adobe Digital Editions 2.0.1, you should be able to remove the DRM that's been applied to your ebooks.
### A Recent Change to Kindle for PC/Kindle for Mac ### Recent Changes to Kindle for PC/Kindle for Mac
Starting with version 1.19, Kindle for PC/Mac uses Amazon's new KFX format which isn't quite as good a source for conversion to ePub as the older KF8 (& MOBI) formats. There are two options to get the older formats. Either stick with version 1.17 or earlier, or modify the executable by changing a file name (PC) or disabling a component of the application (Mac). Starting with version 1.19, Kindle for PC/Mac uses Amazon's new KFX format which isn't quite as good a source for conversion to ePub as the older KF8 (& MOBI) formats. There are two options to get the older formats. Either stick with version 1.17 or earlier, or modify the executable by changing a file name (PC) or disabling a component of the application (Mac).
Version 1.17 of Kindle is no longer available directly from Amazon, so you will need to search for the proper file name and find it on a third party site. The name is `KindleForPC-installer-1.17.44170.exe` for PC and `KindleForMac-44182.dmg` for Mac. Version 1.17 of Kindle is no longer available directly from Amazon, so you will need to search for the proper file name and find it on a third party site. The name is `KindleForPC-installer-1.17.44170.exe` for PC and `KindleForMac-44182.dmg` for Mac. (Note that this is a 32-bit application on the Mac, so will not work on Catalina and newer versions of macOS.)
Verify the one of the following cryptographic hash values, using software of your choice, before installing the downloaded file in order to avoid viruses. If the hash does not match, delete the downloaded file and try again from another site. Verify the one of the following cryptographic hash values, using software of your choice, before installing the downloaded file in order to avoid viruses. If the hash does not match, delete the downloaded file and try again from another site.
#### Kindle for PC `KindleForPC-installer-1.17.44170.exe`: #### Kindle for PC `KindleForPC-installer-1.17.44170.exe`:
* MD-5: 53F793B562F4823721AA47D7DE099869 * MD-5: 53F793B562F4823721AA47D7DE099869
* SHA-1: 73C404D719F0DD8D4AE1C2C96612B095D6C86255 * SHA-1: 73C404D719F0DD8D4AE1C2C96612B095D6C86255
* SHA-256: 14E0F0053F1276C0C7C446892DC170344F707FBFE99B695176 2C120144163200 * SHA-256: 14E0F0053F1276C0C7C446892DC170344F707FBFE99B6951762C120144163200
#### Kindle for Mac `KindleForMac-44182.dmg`: #### Kindle for Mac `KindleForMac-44182.dmg`:
* MD-5: E7E36D5369E1F3CF1D28E5D9115DF15F * MD-5: E7E36D5369E1F3CF1D28E5D9115DF15F
* SHA-1: 7AB9A86B954CB23D622BD79E3257F8E2182D791C * SHA-1: 7AB9A86B954CB23D622BD79E3257F8E2182D791C
* SHA-256: 28DC21246A9C7CDEDD2D6F0F4082E6BF7EF9DB9CE9D485548E 8A9E1D19EAE2AC. * SHA-256: 28DC21246A9C7CDEDD2D6F0F4082E6BF7EF9DB9CE9D485548E8A9E1D19EAE2AC
You will need to go to the preferences and uncheck the auto update checkbox. Then download and install 1.17 over the top of the newer installation. You'll also need to delete the KFX folders from your My Kindle Content folder. You will need to go to the preferences and uncheck the auto update checkbox. Then download and install 1.17 over the top of the newer installation. You'll also need to delete the KFX folders from your My Kindle Content folder. You may also need to take further action to prevent an auto update. The simplest wayis to find the 'updates' folder and replace it with a file. See [this thread] (http://www.mobileread.com/forums/showthread.php?t=283371) at MobileRead for a Script to do this on a PC. On a Mac you can find the folder at ~/Library/Application Support/Kindle/ just delete the folder 'updates' and save a blank text file called 'updates' in its place.
Another possible solution is to use 1.19 or later, but disable KFX by renaming or disabling a necessary component of the application. This may or may not work on versions after 1.25. In a command window, enter the following commands when Kindle for PC/Mac is not running: Another possible solution is to use 1.19 or later, but disable KFX by renaming or disabling a necessary component of the application. This may or may not work on versions after 1.25. In a command window, enter the following commands when Kindle for PC/Mac is not running:
@@ -53,11 +53,13 @@ After restarting the Kindle program any books previously downloaded in KFX forma
#### Decrypting KFX #### Decrypting KFX
Thanks to work by several people, the tools can now decrypt KFX format ebooks from Kindle for Mac/PC. In addition to the DeDRM plugin, calibre users will also need to install jhowell's KFX Input plugin which is available through the standard plugin menu in calibre, or directly from [his plugin thread](https://www.mobileread.com/forums/showthread.php?t=291290) on Mobileread. Thanks to work by several people, the tools can now decrypt KFX format ebooks from Kindle for Mac/PC. In addition to the DeDRM plugin, calibre users will also need to install jhowell's KFX Input plugin which is available through the standard plugin menu in calibre, or directly from [his plugin thread](https://www.mobileread.com/forums/showthread.php?t=291290) on Mobileread.
It's quite possible that Amazon will update their KFX DeDRM to prevent DRM removal from KFX books again. So Remove DRM as soon as possible!
#### Thanks #### Thanks
Thanks to jhowell for his investigations into KFX format and the KFX Input plugin. Some of these instructions are from [his thread on the subject](https://www.mobileread.com/forums/showthread.php?t=283371) at MobileRead. Thanks to jhowell for his investigations into KFX format and the KFX Input plugin. Some of these instructions are from [his thread on the subject](https://www.mobileread.com/forums/showthread.php?t=283371) at MobileRead.
## Where can I get the latest version of these free DRM removal tools? ## Where can I get the latest version of these free DRM removal tools?
Right here at github. Just go to the [releases page](https://github.com/apprenticeharper/DeDRM_tools/releases) and download the latest zip archive of the tools, named `DeDRM\_tools\_X.X.X.zip`, where X.X.X is the version number. You do not need to download the source code archive. Right here at github. Just go to the [releases page](https://github.com/noDRM/DeDRM_tools/releases) and download the latest zip archive of the tools, named `DeDRM\_tools\_X.X.X.zip`, where X.X.X is the version number. You do not need to download the source code archive. This will get you the forked version by noDRM. If you want to download the original version by Apprentice Harper, go to [this page](https://github.com/noDRM/DeDRM_tools/releases) instead.
## I've downloaded the tools archive. Now what? ## I've downloaded the tools archive. Now what?
First, unzip the archive. You should now have a DeDRM folder containing several other folders and a `ReadMe_Overview.txt` file. Please read the `ReadMe_Overview.txt` file! That will explain what the folders are, and you'll be able to work out which of the tools you need. First, unzip the archive. You should now have a DeDRM folder containing several other folders and a `ReadMe_Overview.txt` file. Please read the `ReadMe_Overview.txt` file! That will explain what the folders are, and you'll be able to work out which of the tools you need.
@@ -67,8 +69,8 @@ Install calibre. Install the DeDRM\_plugin in calibre. Install the Obok\_plugin
# Installing the Tools # Installing the Tools
## The calibre plugin ## The calibre plugin
### I am trying to install the calibre plugin, but calibre says "ERROR: Unhandled exception: InvalidPlugin: The plugin in u[path]DeDRM\_tools\_6.5.3.zip is invalid. It does not contain a top-level \_\_init\_\_.py file" ### I am trying to install the calibre plugin, but calibre says "ERROR: Unhandled exception: InvalidPlugin: The plugin in u[path]DeDRM\_tools\_6.8.0.zip is invalid. It does not contain a top-level \_\_init\_\_.py file"
You are trying to add the tools archive (e.g. `DeDRM_tools_6.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. You are trying to add the tools archive (e.g. `DeDRM_tools_6.8.0.zip`) instead of the plugin. The tools archive is not the plugin. It is a collection of DRM removal tools which includes the plugin. You must unzip the archive, and install the calibre plugin `DeDRM_plugin.zip` from a folder called `DeDRM_calibre_plugin` in the unzipped archive.
### Ive unzipped the tools archive, but I cant find the calibre plugin when I try to add them to calibre. I use Windows. ### Ive unzipped the tools archive, but I cant find the calibre plugin when I try to add them to calibre. I use Windows.
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.
@@ -93,7 +95,7 @@ Your ebooks are stored on your computer or on your ebook reader. You need to fin
### Macintosh ### Macintosh
Navigating from your home folder, Navigating from your home folder,
Kindle for Mac ebooks are in either `Library/Application Support/Kindle/My Kindle Content` or `Documents/My Kindle Content or Library/Containers/com.amazon.Kindle/Data/Library/Application Support/Kindle/My Kindle Content`, depending on your version of Kindle for Mac. Kindle for Mac ebooks are in either `Library/Application Support/Kindle/My Kindle Content` or `Documents/My Kindle Content` or `Library/Containers/com.amazon.Kindle/Data/Library/Application Support/Kindle/My Kindle Content`, depending on your version of Kindle for Mac.
Adobe Digital Editions ebooks are in `Documents/Digital Editions` Adobe Digital Editions ebooks are in `Documents/Digital Editions`
@@ -118,21 +120,20 @@ If this book is from Kindle for Mac or Kindle for PC, you must have the Kindle S
If the book is from Kindle for PC or Kindle for Mac and you think you are doing everything right, and you are getting this message, it is possible that the files containing the encryption key arent quite in the format the tools expect. To try to fix this: If the book is from Kindle for PC or Kindle for Mac and you think you are doing everything right, and you are getting this message, it is possible that the files containing the encryption key arent quite in the format the tools expect. To try to fix this:
1. Deregister Kindle for PC(Mac) from your Amazon account. 1. Deregister Kindle for PC/Mac from your Amazon account.
1. Uninstall Kindle for PC(Mac) 1. Uninstall Kindle for PC/Mac
1. Delete the Kindle for PC(Mac) preferences 1. Delete the Kindle for PC/Mac preferences
* PC: Delete the directory `[home folder]\AppData\Local\Amazon` (it might be hidden) and `[home folder]\My Documents\My Kindle Content` * PC: Delete the directory `[home folder]\AppData\Local\Amazon` (it might be hidden) and `[home folder]\My Documents\My Kindle Content`
* Mac: Delete the directory `[home folder]/Library/Application Support/Kindle/` and/or `[home folder]/Library/Containers/com.amazon.Kindle/Data/Library/Application Support/Kindle/` (one or both may be present and should be deleted) * Mac: Delete the directory `[home folder]/Library/Application Support/Kindle/` and/or `[home folder]/Library/Containers/com.amazon.Kindle/Data/Library/Application Support/Kindle/` (one or both may be present and should be deleted)
1. Reinstall Kindle for PC(Mac) version 1.17 or earlier (see above for download links). 1. Reinstall Kindle for PC/Mac version 1.17 or earlier (see above for download links).
1. Re-register Kindle for PC(Mac) with your Amazon account 1. Re-register Kindle for PC/Mac with your Amazon account
1. Download the ebook again. Do not use the files you have downloaded previously. 1. Download the ebook again. Do not use the files you have downloaded previously.
## Some of my books had their DRM removed, but some still say that they have DRM and will not convert. ## Some of my books had their DRM removed, but some still say that they have DRM and will not convert.
There are several possible reasons why only some books get their DRM removed. There are several possible reasons why only some books get their DRM removed.
* You still dont have the DRM removal tools working correctly, but some of your books didnt have DRM in the first place. * You still dont have the DRM removal tools working correctly, but some of your books didnt have DRM in the first place.
* Kindle only: It is a Topaz format book and contains some coding that the tools do not understand. You will need to get a log of the DeDRM attempt, and then create a [new issue at Apprentice Harper's github repository](https://github.com/apprenticeharper/DeDRM_tools/issues/), attaching the book and the log, so that the tools can be updated.
If you are still having problems with particular books, you will need to create a log of the DRM removal attempt for one of the problem books, and post that in a comment at Apprentice Alf's blog or in a new issue at Apprentice Harper's github repository. If you are still having problems with particular books, you will need to create a log of the DRM removal attempt for one of the problem books. If you're using NoDRM's fork, open [a new issue](https://github.com/noDRM/DeDRM_tools/issues) in the GitHub repo. If you're using Apprentice Harpers version, post that logfile as a comment at Apprentice Alf's blog or in a new issue at [Apprentice Harper's github repository](https://github.com/apprenticeharper/DeDRM_tools/issues).
## My Kindle book has imported and the DRM has been removed, but all the pictures are gone. ## My Kindle book has imported and the DRM has been removed, but all the pictures are gone.
Most likely, this is a book downloaded from Amazon directly to an eInk Kindle (e.g. Paperwhite). Unfortunately, the pictures are probably in a `.azw6` file that the tools don't understand. You must download the book manually from Amazon's web site "For transfer via USB" to your Kindle. When you download the eBook in this manner, Amazon will package the pictures in the with text in a single file that the tools will be able to import successfully. Most likely, this is a book downloaded from Amazon directly to an eInk Kindle (e.g. Paperwhite). Unfortunately, the pictures are probably in a `.azw6` file that the tools don't understand. You must download the book manually from Amazon's web site "For transfer via USB" to your Kindle. When you download the eBook in this manner, Amazon will package the pictures in the with text in a single file that the tools will be able to import successfully.
@@ -146,9 +147,6 @@ If you use the Kobo desktop application for Mac or PC, install the Obok plugin.
## I registered Adobe Digital Editions 3.0 or later with an Adobe ID before downloading, but my epub or PDF still has DRM. ## I registered Adobe Digital Editions 3.0 or later with an Adobe ID before downloading, but my epub or PDF still has DRM.
Adobe introduced a new DRM scheme with ADE 3.0 and later. Install ADE 2.0.1 and register with the same Adobe ID. If you can't open your book in ADE 2.01, then you have a book with the new DRM scheme. These tools can't help. You can avoid the new DRM scheme by always downloading your ebooks with ADE 2.0.1. Some retailers will require ADE 3.0 or later, in which case you won't be able to download with ADE 2.0.1. Adobe introduced a new DRM scheme with ADE 3.0 and later. Install ADE 2.0.1 and register with the same Adobe ID. If you can't open your book in ADE 2.01, then you have a book with the new DRM scheme. These tools can't help. You can avoid the new DRM scheme by always downloading your ebooks with ADE 2.0.1. Some retailers will require ADE 3.0 or later, in which case you won't be able to download with ADE 2.0.1.
## The DRM wasn't removed and the log says "Failed to decrypt with error: Cannot decode library or rented ebooks." What now?
You're trying to remove the DRM from an ebook that's only on loan to you. No help will be given to remove DRM from such ebooks. If you think that you have received this message for a book you own, please create an issue at github, or comment at the blog.
## I cannot solve my problem with the DeDRM plugin, and now I need to post a log. How do I do that? ## I cannot solve my problem with the DeDRM plugin, and now I need to post a log. How do I do that?
Remove the DRMed book from calibre. Click the Preferences drop-down menu and choose 'Restart in debug mode'. Once calibre has re-started, import the problem ebook. Now close calibre. A log will appear that you can copy and paste into a comment at Apprentice Alf's blog, or into a new issue at Apprentice Harper's github repository. Remove the DRMed book from calibre. Click the Preferences drop-down menu and choose 'Restart in debug mode'. Once calibre has re-started, import the problem ebook. Now close calibre. A log will appear that you can copy and paste into a comment at Apprentice Alf's blog, or into a new issue at Apprentice Harper's github repository.
@@ -158,7 +156,7 @@ See the [Calibre command line interface (CLI) instructions](CALIBRE_CLI_INSTRUCT
# General Questions # General Questions
## Once the DRM has been removed, is there any trace of my personal identity left in the ebook? ## Once the DRM has been removed, is there any trace of my personal identity left in the ebook?
The tools only remove the DRM. No attempt is made to remove any personally identifying information. That question cannot be answered for sure. While it is easy to check if a book has DRM or not, it is very difficult to verify if all (traces of) personal information have been removed from a book. The tools attempt to remove watermarks when they are detected, but that will not be the case for all watermarks.
## Why do some of my Kindle ebooks import as HTMLZ format in calibre? ## Why do some of my Kindle ebooks import as HTMLZ format in calibre?
Most Amazon Kindle ebooks are Mobipocket format ebooks, or the new KF8 format. However, some are in a format known as Topaz. The Topaz format is only used by Amazon. A Topaz ebook is a collections of glyphs and their positions on each page tagged with some additional information from that page including OCRed text (Optical Character Recognition generated Text) to allow searching, and some additional layout information. Each page of a Topaz ebook is effectively a description of an image of that page. To convert a Topaz ebook to another format is not easy as there is not a one-to-one mapping between glyphs and characters/fonts. To account for this, two different formats are generated by the DRM removal software. The first is an html description built from the OCRtext and images stored in the Topaz file (HTMLZ). This format is easily reflowed but may suffer from typical OCRtext errors including typos, garbled text, missing italics, missing bolds, etc. The second format uses the glyph and position information to create an accurate scalable vector graphics (SVG) image of each page of the book that can be viewed in web browsers that support svg images (Safari, Firefox 4 or later, etc). Additional conversion software can be used to convert these SVG images to an image only PDF file. The DeDRM calibre plugin only imports the HTMLZ versions of the Topaz ebook. The html version can be manually cleaned up and spell checked and then converted using Sigil/calibre to epubs, mobi ebooks, and etc. Most Amazon Kindle ebooks are Mobipocket format ebooks, or the new KF8 format. However, some are in a format known as Topaz. The Topaz format is only used by Amazon. A Topaz ebook is a collections of glyphs and their positions on each page tagged with some additional information from that page including OCRed text (Optical Character Recognition generated Text) to allow searching, and some additional layout information. Each page of a Topaz ebook is effectively a description of an image of that page. To convert a Topaz ebook to another format is not easy as there is not a one-to-one mapping between glyphs and characters/fonts. To account for this, two different formats are generated by the DRM removal software. The first is an html description built from the OCRtext and images stored in the Topaz file (HTMLZ). This format is easily reflowed but may suffer from typical OCRtext errors including typos, garbled text, missing italics, missing bolds, etc. The second format uses the glyph and position information to create an accurate scalable vector graphics (SVG) image of each page of the book that can be viewed in web browsers that support svg images (Safari, Firefox 4 or later, etc). Additional conversion software can be used to convert these SVG images to an image only PDF file. The DeDRM calibre plugin only imports the HTMLZ versions of the Topaz ebook. The html version can be manually cleaned up and spell checked and then converted using Sigil/calibre to epubs, mobi ebooks, and etc.
@@ -169,7 +167,7 @@ All the DRM removal tools hosted here are almost entirely scripts of one kind or
There are some optional shared libraries (`*.dll`, `*.dylib`, and `*.so`) included for performance. The source for any compiled pieces are provided within `alfcrypto_src.zip`. If this is a concern either delete the binary files or manually rebuild them. There are some optional shared libraries (`*.dll`, `*.dylib`, and `*.so`) included for performance. The source for any compiled pieces are provided within `alfcrypto_src.zip`. If this is a concern either delete the binary files or manually rebuild them.
## What ebooks do these tools work on? ## What ebooks do these tools work on?
The tools linked from this blog remove DRM from PDF, ePub, kePub (Kobo), eReader, Kindle (Mobipocket, KF8, Print Replica and Topaz) format ebooks using Adobe Adept, Barnes & Noble, Amazon, Kobo and eReader DRM schemes. The Calibre plugin removes DRM from PDF, ePub, kePub (Kobo), eReader, Kindle (Mobipocket, KF8, Print Replica and Topaz) format ebooks using Adobe Adept, Barnes & Noble, Readium LCP, Amazon, Kobo and eReader DRM schemes.
Note these tools do NOT crack the DRM. They simply allow the books owner to use the encryption key information already stored someplace on their computer or device to decrypt the ebook in the same manner the official ebook reading software uses. Note these tools do NOT crack the DRM. They simply allow the books owner to use the encryption key information already stored someplace on their computer or device to decrypt the ebook in the same manner the official ebook reading software uses.
@@ -188,11 +186,12 @@ Apple regularly change the details of their DRM and so the tools in the main too
## Ive got the tools archive and Ive read all the FAQs but I still cant install the tools and/or the DRM removal doesnt work ## Ive got the tools archive and Ive read all the FAQs but I still cant install the tools and/or the DRM removal doesnt work
* Read the `ReadMe_Overview.txt` file in the top level of the tools archive * Read the `ReadMe_Overview.txt` file in the top level of the tools archive
* Read the ReadMe file for the tool you want to use. * Read the ReadMe file for the tool you want to use.
* If you still cant remove the DRM, ask in the comments section of Apprentice Alf's blog or create a new issue at Apprentice Harper's github repository, reporting the error as precisely as you can, what platform you use, what tool you have tried, what errors you get, and what versions you are using. If the problem happens when running one of the tools, post a log (see previous questions on how to do this). * If you still cant remove the DRM, create a new [GitHub issue](https://github.com/noDRM/DeDRM_tools/issues). If you are using Apprentice Harper's original version and not this fork, you can also ask in the comments section of Apprentice Alf's blog or create a new issue at Apprentice Harper's github repository. If you do report an issue in any of the GitHub repositories, please report the error as precisely as you can. Include what platform you use, what tool you have tried, what errors you get, and what versions you are using. If the problem happens when running one of the tools, post a log (see previous questions on how to do this).
## Who wrote these scripts? ## Who wrote these scripts?
The authors tend to identify themselves only by pseudonyms: The authors tend to identify themselves only by pseudonyms:
* The Adobe Adept and Barnes & Noble scripts were created by i♥cabbages * The Adobe Adept and Barnes & Noble scripts were created by i♥cabbages
* The Readium LCP support for this plugin was created by NoDRM
* The Amazon Mobipocket and eReader scripts were created by The Dark Reverser * The Amazon Mobipocket and eReader scripts were created by The Dark Reverser
* The Amazon K4PC DRM/format was further decoded by Bart Simpson aka Skindle * The Amazon K4PC DRM/format was further decoded by Bart Simpson aka Skindle
* The Amazon K4 Mobi tool was created by by some_updates, mdlnx and others * The Amazon K4 Mobi tool was created by by some_updates, mdlnx and others

View File

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

View File

@@ -1,11 +1,12 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai # 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' __docformat__ = 'restructuredtext en'
import codecs
import os, traceback, zipfile import os, traceback, zipfile
try: try:
@@ -106,7 +107,7 @@ class InterfacePluginAction(InterfaceAction):
# Get a list of Kobo titles # Get a list of Kobo titles
books = self.build_book_list() books = self.build_book_list()
if len(books) < 1: 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) showErrorDlg(msg, None)
return return
@@ -196,7 +197,7 @@ class InterfacePluginAction(InterfaceAction):
# We will write the help file out every time, in case the user upgrades the plugin zip # 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. # and there is a newer help file contained within it.
file_path = os.path.join(config_dir, 'plugins', HELPFILE_NAME) 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: with open(file_path,'w') as f:
f.write(file_data) f.write(file_data)
return file_path return file_path
@@ -373,8 +374,7 @@ class InterfacePluginAction(InterfaceAction):
zin = zipfile.ZipFile(book.filename, 'r') zin = zipfile.ZipFile(book.filename, 'r')
#print ('Kobo library filename: {0}'.format(book.filename)) #print ('Kobo library filename: {0}'.format(book.filename))
for userkey in self.userkeys: for userkey in self.userkeys:
print (_('Trying key: '), userkey.encode('hex_codec')) print (_('Trying key: '), codecs.encode(userkey, 'hex'))
check = True
try: try:
fileout = PersistentTemporaryFile('.epub', dir=self.tdir) fileout = PersistentTemporaryFile('.epub', dir=self.tdir)
#print ('Temp file: {0}'.format(fileout.name)) #print ('Temp file: {0}'.format(fileout.name))
@@ -395,8 +395,7 @@ class InterfacePluginAction(InterfaceAction):
file = book.encryptedfiles[filename] file = book.encryptedfiles[filename]
contents = file.decrypt(userkey, contents) contents = file.decrypt(userkey, contents)
# Parse failures mean the key is probably wrong. # Parse failures mean the key is probably wrong.
if check: file.check(contents)
check = not file.check(contents)
zout.writestr(filename, contents) zout.writestr(filename, contents)
zout.close() zout.close()
zin.close() zin.close()

View File

@@ -1,7 +1,6 @@
#!/usr/bin/env python #!/usr/bin/env python3
# -*- coding: utf-8 -*-
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai # 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'
__copyright__ = '2012, David Forrester <davidfor@internode.on.net>' __copyright__ = '2012, David Forrester <davidfor@internode.on.net>'
@@ -9,13 +8,7 @@ __docformat__ = 'restructuredtext en'
import os, time, re, sys import os, time, re, sys
from datetime import datetime from datetime import datetime
try: from PyQt5.Qt import (Qt, QIcon, QPixmap, QLabel, QDialog, QHBoxLayout, QProgressBar,
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,
QTableWidgetItem, QFont, QLineEdit, QComboBox, QTableWidgetItem, QFont, QLineEdit, QComboBox,
QVBoxLayout, QDialogButtonBox, QStyledItemDelegate, QDateTime, QVBoxLayout, QDialogButtonBox, QStyledItemDelegate, QDateTime,
QRegExpValidator, QRegExp, QDate, QDateEdit) QRegExpValidator, QRegExp, QDate, QDateEdit)
@@ -427,7 +420,7 @@ class KeyValueComboBox(QComboBox):
def selected_key(self): def selected_key(self):
for key, value in self.values.iteritems(): for key, value in self.values.iteritems():
if value == unicode(self.currentText()).strip(): if value == self.currentText().strip():
return key return key
@@ -450,7 +443,7 @@ class KeyComboBox(QComboBox):
def selected_key(self): def selected_key(self):
for key, value in self.values.iteritems(): for key, value in self.values.iteritems():
if key == unicode(self.currentText()).strip(): if key == self.currentText().strip():
return key 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 # 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, QFileDialog)
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
except ImportError:
from PyQt4.Qt import (Qt, QGroupBox, QListWidget, QLineEdit, QDialogButtonBox, QWidget, QLabel, QDialog, QVBoxLayout, QAbstractItemView, QIcon, QHBoxLayout, QComboBox, QListWidgetItem, QFileDialog)
try:
from PyQt5 import Qt as QtGui
except ImportError:
from PyQt4 import QtGui
from calibre.gui2 import (error_dialog, question_dialog, info_dialog, open_url) from calibre.gui2 import (error_dialog, question_dialog, info_dialog, open_url)
from calibre.utils.config import JSONConfig, config_dir from calibre.utils.config import JSONConfig, config_dir
@@ -50,32 +44,32 @@ class ConfigWidget(QWidget):
self.find_homes.setCurrentIndex(index) self.find_homes.setCurrentIndex(index)
self.serials_button = QtGui.QPushButton(self) self.serials_button = QtGui.QPushButton(self)
self.serials_button.setToolTip(_(u"Click to manage Kobo serial numbers for Kobo ebooks")) self.serials_button.setToolTip(_("Click to manage Kobo serial numbers for Kobo ebooks"))
self.serials_button.setText(u"Kobo devices serials") self.serials_button.setText("Kobo devices serials")
self.serials_button.clicked.connect(self.edit_serials) self.serials_button.clicked.connect(self.edit_serials)
layout.addWidget(self.serials_button) layout.addWidget(self.serials_button)
self.kobo_directory_button = QtGui.QPushButton(self) self.kobo_directory_button = QtGui.QPushButton(self)
self.kobo_directory_button.setToolTip(_(u"Click to specify the Kobo directory")) self.kobo_directory_button.setToolTip(_("Click to specify the Kobo directory"))
self.kobo_directory_button.setText(u"Kobo directory") self.kobo_directory_button.setText("Kobo directory")
self.kobo_directory_button.clicked.connect(self.edit_kobo_directory) self.kobo_directory_button.clicked.connect(self.edit_kobo_directory)
layout.addWidget(self.kobo_directory_button) layout.addWidget(self.kobo_directory_button)
def edit_serials(self): def edit_serials(self):
d = ManageKeysDialog(self,u"Kobo device serial number",self.tmpserials, AddSerialDialog) d = ManageKeysDialog(self,"Kobo device serial number",self.tmpserials, AddSerialDialog)
d.exec_() d.exec_()
def edit_kobo_directory(self): def edit_kobo_directory(self):
tmpkobodirectory = QFileDialog.getExistingDirectory(self, u"Select Kobo directory", self.kobodirectory or "/home", QFileDialog.ShowDirsOnly) tmpkobodirectory = QFileDialog.getExistingDirectory(self, "Select Kobo directory", self.kobodirectory or "/home", QFileDialog.ShowDirsOnly)
if tmpkobodirectory != u"" and tmpkobodirectory is not None: if tmpkobodirectory != u"" and tmpkobodirectory is not None:
self.kobodirectory = tmpkobodirectory self.kobodirectory = tmpkobodirectory
def save_settings(self): def save_settings(self):
plugin_prefs['finding_homes_for_formats'] = unicode(self.find_homes.currentText()) plugin_prefs['finding_homes_for_formats'] = self.find_homes.currentText()
plugin_prefs['kobo_serials'] = self.tmpserials plugin_prefs['kobo_serials'] = self.tmpserials
plugin_prefs['kobo_directory'] = self.kobodirectory plugin_prefs['kobo_directory'] = self.kobodirectory
@@ -91,7 +85,7 @@ class ManageKeysDialog(QDialog):
self.plugin_keys = plugin_keys self.plugin_keys = plugin_keys
self.create_key = create_key self.create_key = create_key
self.keyfile_ext = keyfile_ext 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)) self.setWindowTitle("{0} {1}: Manage {2}s".format(PLUGIN_NAME, PLUGIN_VERSION, self.key_type_name))
@@ -99,13 +93,13 @@ class ManageKeysDialog(QDialog):
layout = QVBoxLayout(self) layout = QVBoxLayout(self)
self.setLayout(layout) 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) layout.addWidget(keys_group_box)
keys_group_box_layout = QHBoxLayout() keys_group_box_layout = QHBoxLayout()
keys_group_box.setLayout(keys_group_box_layout) keys_group_box.setLayout(keys_group_box_layout)
self.listy = QListWidget(self) 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.listy.setSelectionMode(QAbstractItemView.SingleSelection)
self.populate_list() self.populate_list()
keys_group_box_layout.addWidget(self.listy) keys_group_box_layout.addWidget(self.listy)
@@ -114,12 +108,12 @@ class ManageKeysDialog(QDialog):
keys_group_box_layout.addLayout(button_layout) keys_group_box_layout.addLayout(button_layout)
self._add_key_button = QtGui.QToolButton(self) self._add_key_button = QtGui.QToolButton(self)
self._add_key_button.setIcon(QIcon(I('plus.png'))) 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) self._add_key_button.clicked.connect(self.add_key)
button_layout.addWidget(self._add_key_button) button_layout.addWidget(self._add_key_button)
self._delete_key_button = QtGui.QToolButton(self) 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.setIcon(QIcon(I('list_remove.png')))
self._delete_key_button.clicked.connect(self.delete_key) self._delete_key_button.clicked.connect(self.delete_key)
button_layout.addWidget(self._delete_key_button) button_layout.addWidget(self._delete_key_button)
@@ -155,7 +149,7 @@ class ManageKeysDialog(QDialog):
new_key_value = d.key_value new_key_value = d.key_value
if new_key_value in self.plugin_keys: if new_key_value in self.plugin_keys:
info_dialog(None, "{0} {1}: Duplicate {2}".format(PLUGIN_NAME, PLUGIN_VERSION,self.key_type_name), 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 return
self.plugin_keys.append(d.key_value) self.plugin_keys.append(d.key_value)
@@ -165,8 +159,8 @@ class ManageKeysDialog(QDialog):
def delete_key(self): def delete_key(self):
if not self.listy.currentItem(): if not self.listy.currentItem():
return return
keyname = unicode(self.listy.currentItem().text()) keyname = 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): 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 return
self.plugin_keys.remove(keyname) self.plugin_keys.remove(keyname)
@@ -177,7 +171,7 @@ class AddSerialDialog(QDialog):
def __init__(self, parent=None,): def __init__(self, parent=None,):
QDialog.__init__(self, parent) QDialog.__init__(self, parent)
self.parent = 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) layout = QVBoxLayout(self)
self.setLayout(layout) self.setLayout(layout)
@@ -188,9 +182,9 @@ class AddSerialDialog(QDialog):
key_group = QHBoxLayout() key_group = QHBoxLayout()
data_group_box_layout.addLayout(key_group) data_group_box_layout.addLayout(key_group)
key_group.addWidget(QLabel(u"EInk Kobo Serial Number:", self)) key_group.addWidget(QLabel("EInk Kobo Serial Number:", self))
self.key_ledit = QLineEdit("", 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) key_group.addWidget(self.key_ledit)
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
@@ -202,17 +196,17 @@ class AddSerialDialog(QDialog):
@property @property
def key_name(self): def key_name(self):
return unicode(self.key_ledit.text()).strip() return self.key_ledit.text().strip()
@property @property
def key_value(self): def key_value(self):
return unicode(self.key_ledit.text()).strip() return self.key_ledit.text().strip()
def accept(self): def accept(self):
if len(self.key_name) == 0 or self.key_name.isspace(): if len(self.key_name) == 0 or self.key_name.isspace():
errmsg = 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) return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
if len(self.key_name) != 13: if len(self.key_name) != 13:
errmsg = 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) return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
QDialog.accept(self) QDialog.accept(self)

View File

@@ -37,14 +37,17 @@ class legacy_obok(object):
def __oldcookiedeviceid(self): def __oldcookiedeviceid(self):
'''Optionally attempt to get a device id using the old cookie method. '''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 = '' wsuid = ''
pwsdid = '' pwsdid = ''
try: try:
if sys.platform.startswith('win'): if sys.platform.startswith('win'):
import _winreg try:
regkey_browser = _winreg.OpenKey(_winreg.HKEY_CURRENT_USER, 'Software\\Kobo\\Kobo Desktop Edition\\Browser') import winreg
cookies = _winreg.QueryValueEx(regkey_browser, 'cookies') 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] bytearrays = cookies[0]
elif sys.platform.startswith('darwin'): elif sys.platform.startswith('darwin'):
prefs = os.path.join(os.environ['HOME'], 'Library/Preferences/com.kobo.Kobo Desktop Edition.plist') prefs = os.path.join(os.environ['HOME'], 'Library/Preferences/com.kobo.Kobo Desktop Edition.plist')

View File

@@ -1,6 +1,16 @@
#!/usr/bin/env python #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Version 10.0.0 November 2021
# Merge https://github.com/apprenticeharper/DeDRM_tools/pull/1691 to fix
# key fetch issues on some machines.
#
# Version 4.1.0 February 2021
# Add detection for Kobo directory location on Linux
#
# Version 4.0.0 September 2020
# Python 3.0
#
# Version 3.2.5 December 2016 # Version 3.2.5 December 2016
# Improve detection of good text decryption. # Improve detection of good text decryption.
# #
@@ -152,8 +162,8 @@
"""Manage all Kobo books, either encrypted or DRM-free.""" """Manage all Kobo books, either encrypted or DRM-free."""
from __future__ import print_function from __future__ import print_function
__version__ = '3.2.4' __version__ = '4.0.0'
__about__ = u"Obok v{0}\nCopyright © 2012-2016 Physisticated et al.".format(__version__) __about__ = "Obok v{0}\nCopyright © 2012-2020 Physisticated et al.".format(__version__)
import sys import sys
import os import os
@@ -173,10 +183,10 @@ import tempfile
can_parse_xml = True can_parse_xml = True
try: try:
from xml.etree import ElementTree as ET from xml.etree import ElementTree as ET
# print u"using xml.etree for xml parsing" # print "using xml.etree for xml parsing"
except ImportError: except ImportError:
can_parse_xml = False 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 # List of all known hash keys
KOBO_HASH_KEYS = ['88b3a2e13', 'XzUhGYdFp', 'NoCanLook','QJhwzAtXL'] KOBO_HASH_KEYS = ['88b3a2e13', 'XzUhGYdFp', 'NoCanLook','QJhwzAtXL']
@@ -231,7 +241,7 @@ def _load_crypto_libcrypto():
raise ENCRYPTIONError(_('Failed to initialize AES key')) raise ENCRYPTIONError(_('Failed to initialize AES key'))
def decrypt(self, data): def decrypt(self, data):
clear = '' clear = b''
for i in range(0, len(data), 16): for i in range(0, len(data), 16):
out = create_string_buffer(16) out = create_string_buffer(16)
rv = AES_ecb_encrypt(data[i:i+16], out, self._key, 0) rv = AES_ecb_encrypt(data[i:i+16], out, self._key, 0)
@@ -276,10 +286,10 @@ class SafeUnbuffered:
if self.encoding == None: if self.encoding == None:
self.encoding = "utf-8" self.encoding = "utf-8"
def write(self, data): def write(self, data):
if isinstance(data,unicode): if isinstance(data,str):
data = data.encode(self.encoding,"replace") data = data.encode(self.encoding,"replace")
self.stream.write(data) self.stream.buffer.write(data)
self.stream.flush() self.stream.buffer.flush()
def __getattr__(self, attr): def __getattr__(self, attr):
return getattr(self.stream, attr) return getattr(self.stream, attr)
@@ -309,9 +319,9 @@ class KoboLibrary(object):
# step 1. check whether this looks like a real device # step 1. check whether this looks like a real device
if (device_path): if (device_path):
# we got a 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 # 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))): if (not(os.path.isfile(kobodb))):
# device path seems to be wrong, unset it # device path seems to be wrong, unset it
device_path = u"" device_path = u""
@@ -323,22 +333,22 @@ class KoboLibrary(object):
if (len(serials) == 0): if (len(serials) == 0):
# we got a device path but no saved serial # we got a device path but no saved serial
# try to get the serial from the device # 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 # get serial from device_path/.adobe-digital-editions/device.xml
if can_parse_xml: if can_parse_xml:
devicexml = os.path.join(device_path, '.adobe-digital-editions', 'device.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)): if (os.path.exists(devicexml)):
# print u"trying to parse {0}".format(devicexml) # print "trying to parse {0}".format(devicexml)
xmltree = ET.parse(devicexml) xmltree = ET.parse(devicexml)
for node in xmltree.iter(): for node in xmltree.iter():
if "deviceSerial" in node.tag: if "deviceSerial" in node.tag:
serial = node.text serial = node.text
# print u"found serial {0}".format(serial) # print "found serial {0}".format(serial)
serials.append(serial) serials.append(serial)
break break
else: else:
# print u"cannot get serials from device." # print "cannot get serials from device."
device_path = u"" device_path = u""
self.kobodir = u"" self.kobodir = u""
kobodb = u"" kobodb = u""
@@ -350,23 +360,50 @@ class KoboLibrary(object):
if (self.kobodir == u""): if (self.kobodir == u""):
if sys.platform.startswith('win'): if sys.platform.startswith('win'):
import _winreg as winreg try:
import winreg
except ImportError:
import _winreg as winreg
if sys.getwindowsversion().major > 5: if sys.getwindowsversion().major > 5:
if 'LOCALAPPDATA' in os.environ.keys(): if 'LOCALAPPDATA' in os.environ.keys():
# Python 2.x does not return unicode env. Use Python 3.x # Python 2.x does not return unicode env. Use Python 3.x
self.kobodir = winreg.ExpandEnvironmentStrings(u"%LOCALAPPDATA%") self.kobodir = winreg.ExpandEnvironmentStrings("%LOCALAPPDATA%")
if (self.kobodir == u""): if (self.kobodir == u""):
if 'USERPROFILE' in os.environ.keys(): if 'USERPROFILE' in os.environ.keys():
# Python 2.x does not return unicode env. Use Python 3.x # Python 2.x does not return unicode env. Use Python 3.x
self.kobodir = os.path.join(winreg.ExpandEnvironmentStrings(u"%USERPROFILE%"), u"Local Settings", u"Application Data") self.kobodir = os.path.join(winreg.ExpandEnvironmentStrings("%USERPROFILE%"), "Local Settings", "Application Data")
self.kobodir = os.path.join(self.kobodir, u"Kobo", u"Kobo Desktop Edition") self.kobodir = os.path.join(self.kobodir, "Kobo", "Kobo Desktop Edition")
elif sys.platform.startswith('darwin'): elif sys.platform.startswith('darwin'):
self.kobodir = os.path.join(os.environ['HOME'], u"Library", u"Application Support", u"Kobo", u"Kobo Desktop Edition") self.kobodir = os.path.join(os.environ['HOME'], "Library", "Application Support", "Kobo", "Kobo Desktop Edition")
#elif linux_path != None: elif sys.platform.startswith('linux'):
# Probably Linux, let's get the wine prefix and path to Kobo.
# self.kobodir = os.path.join(linux_path, u"Local Settings", u"Application Data", u"Kobo", u"Kobo Desktop Edition") #sets ~/.config/calibre as the location to store the kobodir location info file and creates this directory if necessary
kobodir_cache_dir = os.path.join(os.environ['HOME'], ".config", "calibre")
if not os.path.isdir(kobodir_cache_dir):
os.mkdir(kobodir_cache_dir)
#appends the name of the file we're storing the kobodir location info to the above path
kobodir_cache_file = str(kobodir_cache_dir) + "/" + "kobo location"
"""if the above file does not exist, recursively searches from the root
of the filesystem until kobodir is found and stores the location of kobodir
in that file so this loop can be skipped in the future"""
original_stdout = sys.stdout
if not os.path.isfile(kobodir_cache_file):
for root, dirs, files in os.walk('/'):
for file in files:
if file == 'Kobo.sqlite':
kobo_linux_path = str(root)
with open(kobodir_cache_file, 'w') as f:
sys.stdout = f
print(kobo_linux_path, end='')
sys.stdout = original_stdout
f = open(kobodir_cache_file, 'r' )
self.kobodir = f.read()
# desktop versions use Kobo.sqlite # desktop versions use Kobo.sqlite
kobodb = os.path.join(self.kobodir, u"Kobo.sqlite") kobodb = os.path.join(self.kobodir, "Kobo.sqlite")
# check for existence of file # check for existence of file
if (not(os.path.isfile(kobodb))): if (not(os.path.isfile(kobodb))):
# give up here, we haven't found anything useful # give up here, we haven't found anything useful
@@ -374,14 +411,14 @@ class KoboLibrary(object):
kobodb = u"" kobodb = u""
if (self.kobodir != 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 # 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. # so we can ensure it's not using WAL logging which sqlite3 can't do.
self.newdb = tempfile.NamedTemporaryFile(mode='wb', delete=False) self.newdb = tempfile.NamedTemporaryFile(mode='wb', delete=False)
print(self.newdb.name) print(self.newdb.name)
olddb = open(kobodb, 'rb') olddb = open(kobodb, 'rb')
self.newdb.write(olddb.read(18)) self.newdb.write(olddb.read(18))
self.newdb.write('\x01\x01') self.newdb.write(b'\x01\x01')
olddb.read(2) olddb.read(2)
self.newdb.write(olddb.read()) self.newdb.write(olddb.read())
olddb.close() olddb.close()
@@ -434,24 +471,31 @@ class KoboLibrary(object):
def __bookfile (self, volumeid): def __bookfile (self, volumeid):
"""The filename needed to open a given book.""" """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): def __getmacaddrs (self):
"""The list of all MAC addresses on this machine.""" """The list of all MAC addresses on this machine."""
macaddrs = [] macaddrs = []
if sys.platform.startswith('win'): if sys.platform.startswith('win'):
c = re.compile('\s(' + '[0-9a-f]{2}-' * 5 + '[0-9a-f]{2})(\s|$)', re.IGNORECASE) c = re.compile('\s?(' + '[0-9a-f]{2}[:\-]' * 5 + '[0-9a-f]{2})(\s|$)', re.IGNORECASE)
(p_in, p_out, p_err) = os.popen3('ipconfig /all') try:
for line in p_out: output = subprocess.Popen('ipconfig /all', shell=True, stdout=subprocess.PIPE, text=True).stdout
m = c.search(line) for line in output:
if m: m = c.search(line)
macaddrs.append(re.sub("-", ":", m.group(1)).upper()) if m:
macaddrs.append(re.sub("-", ":", m.group(1)).upper())
except:
output = subprocess.Popen('wmic nic where PhysicalAdapter=True get MACAddress', shell=True, stdout=subprocess.PIPE, text=True).stdout
for line in output:
m = c.search(line)
if m:
macaddrs.append(re.sub("-", ":", m.group(1)).upper())
elif sys.platform.startswith('darwin'): elif sys.platform.startswith('darwin'):
c = re.compile('\s(' + '[0-9a-f]{2}:' * 5 + '[0-9a-f]{2})(\s|$)', re.IGNORECASE) c = re.compile('\s(' + '[0-9a-f]{2}:' * 5 + '[0-9a-f]{2})(\s|$)', re.IGNORECASE)
output = subprocess.check_output('/sbin/ifconfig -a', shell=True) output = subprocess.check_output('/sbin/ifconfig -a', shell=True, encoding='utf-8')
matches = c.findall(output) matches = c.findall(output)
for m in matches: for m in matches:
# print u"m:{0}".format(m[0]) # print "m:{0}".format(m[0])
macaddrs.append(m[0].upper()) macaddrs.append(m[0].upper())
else: else:
# probably linux # probably linux
@@ -493,9 +537,9 @@ class KoboLibrary(object):
userids = self.__getuserids() userids = self.__getuserids()
userkeys = [] userkeys = []
for hash in KOBO_HASH_KEYS: for hash in KOBO_HASH_KEYS:
deviceid = hashlib.sha256(hash + macaddr).hexdigest() deviceid = hashlib.sha256((hash + macaddr).encode('ascii')).hexdigest()
for userid in userids: 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:])) userkeys.append(binascii.a2b_hex(userkey[32:]))
return userkeys return userkeys
@@ -556,7 +600,7 @@ class KoboBook(object):
# Convert relative URIs # Convert relative URIs
href = item.attrib['href'] href = item.attrib['href']
if not c.match(href): if not c.match(href):
href = string.join((basedir, href), '') href = ''.join((basedir, href))
# Update books we've found from the DB. # Update books we've found from the DB.
if href in self._encryptedfiles: if href in self._encryptedfiles:
@@ -604,59 +648,59 @@ class KoboFile(object):
# assume utf-8 with no BOM # assume utf-8 with no BOM
textoffset = 0 textoffset = 0
stride = 1 stride = 1
print(u"Checking text:{0}:".format(contents[:10])) print("Checking text:{0}:".format(contents[:10]))
# check for byte order mark # 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 # seems to be utf-8 with BOM
print(u"Could be utf-8 with BOM") print("Could be utf-8 with BOM")
textoffset = 3 textoffset = 3
elif contents[:2]=="\xfe\xff": elif contents[:2]==b"\xfe\xff":
# seems to be utf-16BE # seems to be utf-16BE
print(u"Could be utf-16BE") print("Could be utf-16BE")
textoffset = 3 textoffset = 3
stride = 2 stride = 2
elif contents[:2]=="\xff\xfe": elif contents[:2]==b"\xff\xfe":
# seems to be utf-16LE # seems to be utf-16LE
print(u"Could be utf-16LE") print("Could be utf-16LE")
textoffset = 2 textoffset = 2
stride = 2 stride = 2
else: 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 # now check that the first few characters are in the ASCII range
for i in xrange(textoffset,textoffset+5*stride,stride): for i in range(textoffset,textoffset+5*stride,stride):
if ord(contents[i])<32 or ord(contents[i])>127: if contents[i]<32 or contents[i]>127:
# Non-ascii, so decryption probably failed # 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 raise ValueError
print(u"Seems to be good text") print("Seems to be good text")
return True 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 # utf-8
return True 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 # utf-16BE
return True 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 # utf-16LE
return True 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 # utf-8 of weird <!DOCTYPE start
return True 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 # utf-16BE of weird <!DOCTYPE start
return True 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 # utf-16LE of weird <!DOCTYPE start
return True return True
else: else:
print(u"Bad XML: {0}".format(contents[:8])) print("Bad XML: {0}".format(contents[:8]))
raise ValueError raise ValueError
elif self.mimetype == 'image/jpeg': elif self.mimetype == 'image/jpeg':
if contents[:3] == '\xff\xd8\xff': if contents[:3] == b'\xff\xd8\xff':
return True return True
else: else:
print(u"Bad JPEG: {0}".format(contents[:3].encode('hex'))) print("Bad JPEG: {0}".format(contents[:3].hex()))
raise ValueError() raise ValueError()
return False return False
@@ -679,18 +723,18 @@ class KoboFile(object):
return contents return contents
def decrypt_book(book, lib): def decrypt_book(book, lib):
print(u"Converting {0}".format(book.title)) print("Converting {0}".format(book.title))
zin = zipfile.ZipFile(book.filename, "r") zin = zipfile.ZipFile(book.filename, "r")
# make filename out of Unicode alphanumeric and whitespace equivalents from title # 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'): 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) 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 return 0
result = 1 result = 1
for userkey in lib.userkeys: for userkey in lib.userkeys:
print(u"Trying key: {0}".format(userkey.encode('hex_codec'))) print("Trying key: {0}".format(userkey.hex()))
try: try:
zout = zipfile.ZipFile(outname, "w", zipfile.ZIP_DEFLATED) zout = zipfile.ZipFile(outname, "w", zipfile.ZIP_DEFLATED)
for filename in zin.namelist(): for filename in zin.namelist():
@@ -702,12 +746,12 @@ def decrypt_book(book, lib):
file.check(contents) file.check(contents)
zout.writestr(filename, contents) zout.writestr(filename, contents)
zout.close() zout.close()
print(u"Decryption succeeded.") print("Decryption succeeded.")
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)))
result = 0 result = 0
break break
except ValueError: except ValueError:
print(u"Decryption failed.") print("Decryption failed.")
zout.close() zout.close()
os.remove(outname) os.remove(outname)
zin.close() zin.close()
@@ -716,7 +760,7 @@ def decrypt_book(book, lib):
def cli_main(): def cli_main():
description = __about__ 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 = 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('--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") parser.add_argument('--all', action='store_true', help="flag for converting all books on device")
@@ -732,25 +776,25 @@ def cli_main():
books = lib.books books = lib.books
else: else:
for i, book in enumerate(lib.books): for i, book in enumerate(lib.books):
print(u"{0}: {1}".format(i + 1, book.title)) print("{0}: {1}".format(i + 1, book.title))
print(u"Or 'all'") print("Or 'all'")
choice = raw_input(u"Convert book number... ") choice = input("Convert book number... ")
if choice == u'all': if choice == "all":
books = list(lib.books) books = list(lib.books)
else: else:
try: try:
num = int(choice) num = int(choice)
books = [lib.books[num - 1]] books = [lib.books[num - 1]]
except (ValueError, IndexError): except (ValueError, IndexError):
print(u"Invalid choice. Exiting...") print("Invalid choice. Exiting...")
exit() exit()
results = [decrypt_book(book, lib) for book in books] results = [decrypt_book(book, lib) for book in books]
lib.close() lib.close()
overall_result = all(result != 0 for result in results) overall_result = all(result != 0 for result in results)
if overall_result != 0: 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 return overall_result

View File

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

Binary file not shown.

View File

@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: \n" "Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2014-11-17 12:51+0100\n" "POT-Creation-Date: 2014-11-17 12:51+0100\n"
"PO-Revision-Date: 2020-02-02 09:18+0100\n" "PO-Revision-Date: 2021-01-19 12:20+0100\n"
"Language: sv\n" "Language: sv\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
@@ -16,14 +16,14 @@ msgstr ""
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: \n" "Language-Team: \n"
"X-Generator: Poedit 2.2.4\n" "X-Generator: Poedit 2.4.2\n"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:80 #: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:80
msgid "" msgid ""
"<p>No books found in Kobo Library\n" "<p>No books found in Kobo Library\n"
"Are you sure it's installed\\configured\\synchronized?" "Are you sure it's installed\\configured\\synchronized?"
msgstr "" msgstr ""
"<p>Inga böcker finns i Kobo-bibliotek\n" "<p>Inga böcker hittades i Kobo-bibliotek\n"
"Är du säker på att den är installerad\\konfigurerad\\synkroniserad?" "Ä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 #: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:87
@@ -36,7 +36,7 @@ msgstr "Problem med att hämta nycklar med nyare obok-metod."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:97 #: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:97
msgid "Found {0} possible keys to try." msgid "Found {0} possible keys to try."
msgstr "Hittade {0} möjliga nycklar att pröva med." msgstr "Hittade {0} möjliga nycklar att försöka med."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:99 #: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:99
msgid "<p>No userkeys found to decrypt books with. No point in proceeding." msgid "<p>No userkeys found to decrypt books with. No point in proceeding."
@@ -46,7 +46,7 @@ msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:115 #: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:115
msgid "{} - Decryption canceled by user." msgid "{} - Decryption canceled by user."
msgstr "{} - Dekryptering avbryts av användaren." msgstr "{} - Dekryptering avbröts av användaren."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:135 #: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:135
msgid "{} - \"Add books\" canceled by user." msgid "{} - \"Add books\" canceled by user."
@@ -87,14 +87,14 @@ msgstr "dubblett upptäcktes"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:233 #: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:233
msgid "{0} - Successfully added EPUB format to existing {1}" msgid "{0} - Successfully added EPUB format to existing {1}"
msgstr "{0} - Lade till EPUB-format till befintliga {1}" msgstr "{0} - EPUB-formatet har lagts till i befintliga {1}"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:236 #: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:236
msgid "" msgid ""
"{0} - Error adding EPUB format to existing {1}. This really shouldn't happen." "{0} - Error adding EPUB format to existing {1}. This really shouldn't happen."
msgstr "" msgstr ""
"{0} - Fel vid tillägg av EPUB-format till befintligt {1}. Det här borde inte " "{0} - Fel vid tilläggning av EPUB-formatet till befintliga {1}. Detta borde "
"hända." "verkligen inte hända."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:259 #: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:259
msgid "{} - \"Insert formats\" canceled by user." msgid "{} - \"Insert formats\" canceled by user."
@@ -103,44 +103,44 @@ msgstr "{} - \"Infoga format\" avbröts av användaren."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:291 #: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:291
msgid "" msgid ""
"<p><b>{0}</b> EPUB{2} successfully added to library.<br /><br /><b>{1}</b> " "<p><b>{0}</b> EPUB{2} successfully added to library.<br /><br /><b>{1}</b> "
msgstr "<p><b>{0}</b> EPUB{2} lades till bibliotek.<br /><br /><b>{1}</b> " msgstr "<p><b>{0}</b> EPUB{2} har lagts till i bibliotek.<br /><br /><b>{1}</b> "
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:292 #: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:292
msgid "" msgid ""
"not added because books with the same title/author were detected.<br /><br /" "not added because books with the same title/author were detected.<br /><br /"
">Would you like to try and add the EPUB format{0}" ">Would you like to try and add the EPUB format{0}"
msgstr "" msgstr ""
"inte tillagd eftersom böcker med samma titel/författare upptäcktes.<br/><br /" "lades inte till eftersom böcker med samma titel/författare upptäcktes.<br/"
">Vill du försöka lägga till EPUB-formatet{0}" "><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 #: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:293
msgid "" msgid ""
" to those existing entries?<br /><br />NOTE: no pre-existing EPUBs will be " " to those existing entries?<br /><br />NOTE: no pre-existing EPUBs will be "
"overwritten." "overwritten."
msgstr "" msgstr ""
" till dessa befintliga poster?<br /><br />OBS: inga befintliga EPUB:er " " till dessa befintliga poster?<br /><br />OBS: inga befintliga EPUB:er kommer "
"kommer att skrivas över." "att skrivas över."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:295 #: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:295
msgid "" msgid ""
"{0} -- not added because of {1} in your library.\n" "{0} -- not added because of {1} in your library.\n"
"\n" "\n"
msgstr "" msgstr ""
"{0} -- inte tillagd på grund av {1} i ditt bibliotek.\n" "{0} -- lades inte till på grund av {1} i ditt bibliotek.\n"
"\n" "\n"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:297 #: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:297
msgid "<p><b>{0}</b> -- not added because of {1} in your library.<br /><br />" msgid "<p><b>{0}</b> -- not added because of {1} in your library.<br /><br />"
msgstr "" msgstr ""
"<p><b>{0}</b> -- inte tillagd på grund av {1} i ditt bibliotek.<br /><br />" "<p><b>{0}</b> -- lades inte till på grund av {1} i ditt bibliotek.<br /><br />"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:298 #: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:298
msgid "" msgid ""
"Would you like to try and add the EPUB format to an available calibre " "Would you like to try and add the EPUB format to an available calibre "
"duplicate?<br /><br />" "duplicate?<br /><br />"
msgstr "" msgstr ""
"Vill du försöka lägga till EPUB-formatet till en tillgänglig calibre-" "Vill du försöka lägga till EPUB-formatet till en tillgänglig calibre-dubblett?"
"dubblett?<br /><br />" "<br /><br />"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:299 #: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:299
msgid "NOTE: no pre-existing EPUB will be overwritten." msgid "NOTE: no pre-existing EPUB will be overwritten."
@@ -148,23 +148,23 @@ msgstr "OBS: ingen befintlig EPUB kommer att skrivas över."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:346 #: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:346
msgid "Trying key: " msgid "Trying key: "
msgstr "Prövar nyckel: " msgstr "Försöker med nyckel: "
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:378 #: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:378
msgid "Decryption failed, trying next key." msgid "Decryption failed, trying next key."
msgstr "Det gick inte att dekryptera, prövar nästa nyckel." msgstr "Dekryptering misslyckades, försöker med nästa nyckel."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:382 #: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:382
msgid "Unknown Error decrypting, trying next key.." msgid "Unknown Error decrypting, trying next key.."
msgstr "Okänt fel dekryptering, prövar nästa nyckel.." msgstr "Okänt fel vid dekryptering, försöker med nästa nyckel.."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:395 #: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:395
msgid "" msgid ""
"<p>All selected Kobo books added as new calibre books or inserted into " "<p>All selected Kobo books added as new calibre books or inserted into "
"existing calibre ebooks.<br /><br />No issues." "existing calibre ebooks.<br /><br />No issues."
msgstr "" msgstr ""
"<p>Alla valda Kobo-böcker läggs till som nya calibre-böcker eller infogas i " "<p>Alla valda Kobo-böcker har lagts till som nya calibre-böcker eller infogats "
"befintliga calibre-e-böcker.<br /><br />Inga problem." "i befintliga calibre-e-böcker.<br /><br />Inga problem."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:399 #: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:399
msgid "<p>{0} successfully added." msgid "<p>{0} successfully added."
@@ -192,15 +192,14 @@ msgstr "<p><b>Nya böcker skapade:</b> {}</p>\n"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:418 #: 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" msgid "<p><b>Duplicates that weren't added:</b> {}</p>\n"
msgstr "<p><b>Dubbletter som inte tillsattes:</b> {}</p>\n" msgstr "<p><b>Dubbletter som inte har lagts till:</b> {}</p>\n"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:426 #: 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" msgid "<p><b>Book imports cancelled by user:</b> {}</p>\n"
msgstr "<p><b>Bokimport avbröts av användaren:</b> {}</p>\n" msgstr "<p><b>Bokimport avbröts av användaren:</b> {}</p>\n"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:428 #: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:428
msgid "" msgid "<p><b>New EPUB formats inserted in existing calibre books:</b> {0}</p>\n"
"<p><b>New EPUB formats inserted in existing calibre books:</b> {0}</p>\n"
msgstr "" msgstr ""
"<p><b>Nya EPUB-format infogade i befintliga calibre-böcker:</b> {0}</p>\n" "<p><b>Nya EPUB-format infogade i befintliga calibre-böcker:</b> {0}</p>\n"
@@ -208,8 +207,7 @@ msgstr ""
msgid "" msgid ""
"<p><b>EPUB formats NOT inserted into existing calibre books:</b> {}<br />\n" "<p><b>EPUB formats NOT inserted into existing calibre books:</b> {}<br />\n"
msgstr "" msgstr ""
"<p><b>EPUB-format som INTE infogats i befintliga calibre-böcker:</b> {}<br /" "<p><b>EPUB-format som INTE infogats i befintliga calibre-böcker:</b> {}<br />\n"
">\n"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:435 #: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:435
msgid "" msgid ""
@@ -221,7 +219,7 @@ msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:444 #: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:444
msgid "<p><b>Format imports cancelled by user:</b> {}</p>\n" msgid "<p><b>Format imports cancelled by user:</b> {}</p>\n"
msgstr "<p><b>Format-import avbröts av användaren:</b> {}</p>\n" msgstr "<p><b>Format-importen avbröts av användaren:</b> {}</p>\n"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:458 #: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:458
msgid "Unknown Book Title" msgid "Unknown Book Title"
@@ -233,11 +231,11 @@ msgstr "den kunde inte dekrypteras."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:462 #: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:462
msgid "" msgid ""
"user CHOSE not to insert the new EPUB format, or all existing calibre " "user CHOSE not to insert the new EPUB format, or all existing calibre entries "
"entries HAD an EPUB format already." "HAD an EPUB format already."
msgstr "" msgstr ""
"användaren VALDE att inte infoga det nya EPUB-formatet, eller alla " "användaren VALDE att inte infoga det nya EPUB-formatet, eller alla befintliga "
"befintliga calibre-poster hade redan ett EPUB-format." "calibre-poster hade redan ett EPUB-format."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:464 #: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:464
msgid "of unknown reasons. Gosh I'm embarrassed!" msgid "of unknown reasons. Gosh I'm embarrassed!"
@@ -245,7 +243,7 @@ msgstr "av okända skäl. Jag skäms!"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:465 #: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:465
msgid "<p>{0} not added because {1}" msgid "<p>{0} not added because {1}"
msgstr "<p>{0} inte tillagd eftersom {1}" msgstr "<p>{0} lades inte till på grund av {1}"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\common_utils.py:226 #: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\common_utils.py:226
msgid "Help" msgid "Help"
@@ -254,31 +252,31 @@ msgstr "Hjälp"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\common_utils.py:235 #: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\common_utils.py:235
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\utilities.py:214 #: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\utilities.py:214
msgid "Restart required" msgid "Restart required"
msgstr "Omstart krävs" msgstr "Kräver omstart"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\common_utils.py:236 #: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\common_utils.py:236
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\utilities.py:215 #: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\utilities.py:215
msgid "" msgid ""
"Title image not found - you must restart Calibre before using this plugin!" "Title image not found - you must restart Calibre before using this plugin!"
msgstr "" msgstr ""
"Titelbild hittades inte - du måste starta calibre innan du använder denna " "Titelbild hittades inte - du måste starta om calibre innan du använder denna "
"insticksmodul!" "insticksmodul!"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\common_utils.py:322 #: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\common_utils.py:322
msgid "Undefined" msgid "Undefined"
msgstr "Obestämd" msgstr "Odefinierad"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:30 #: 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?" msgid "When should Obok try to insert EPUBs into existing calibre entries?"
msgstr "När ska Obok försöka infoga EPUB:er i befintliga calibre-böcker?" msgstr "När ska Obok försöka infoga EPUB:er i befintliga calibre-poster?"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:33 #: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:33
msgid "" msgid ""
"<p>Default behavior when duplicates are detected. None of the choices will " "<p>Default behavior when duplicates are detected. None of the choices will "
"cause calibre ebooks to be overwritten" "cause calibre ebooks to be overwritten"
msgstr "" msgstr ""
"<p>Standardbeteende när dubbletter upptäcks. Inget av alternativen kommer " "<p>Standardbeteende när dubbletter upptäcks. Inget av alternativen kommer att "
"att orsaka calibre-e-böcker att skrivas över" "orsaka att calibre-e-böcker skrivs över"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:35 #: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:35
msgid "Ask" msgid "Ask"
@@ -323,7 +321,7 @@ msgstr "Välj alla böcker med DRM."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:95 #: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:95
msgid "All DRM free" msgid "All DRM free"
msgstr "Alla DRM fria" msgstr "Alla utan DRM"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:96 #: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:96
msgid "Select all books without DRM." msgid "Select all books without DRM."
@@ -355,12 +353,12 @@ msgstr "Tar bort DRM från Kobo-kepubs och lägger till dem i biblioteket."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\obok\obok.py:162 #: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\obok\obok.py:162
msgid "AES improper key used" msgid "AES improper key used"
msgstr "AES felaktig nyckel används" msgstr "Felaktig AES-nyckel används"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\obok\obok.py:167 #: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\obok\obok.py:167
msgid "Failed to initialize AES key" msgid "Failed to initialize AES key"
msgstr "Det gick inte att initiera AES-nyckel" msgstr "Misslyckades med att initiera AES-nyckel"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\obok\obok.py:175 #: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\obok\obok.py:175
msgid "AES decryption failed" msgid "AES decryption failed"
msgstr "AES dekryptering misslyckades" msgstr "AES-dekryptering misslyckades"

View File

@@ -1,19 +1,19 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai # 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' __docformat__ = 'restructuredtext en'
import os, struct, time import os, struct, time
from StringIO import StringIO try:
from StringIO import StringIO
except ImportError:
from io import StringIO
from traceback import print_exc from traceback import print_exc
try: from PyQt5.Qt import (Qt, QDialog, QPixmap, QIcon, QLabel, QHBoxLayout, QFont, QTableWidgetItem)
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 calibre.utils.config import config_dir from calibre.utils.config import config_dir
from calibre.constants import iswindows, DEBUG from calibre.constants import iswindows, DEBUG
@@ -39,7 +39,7 @@ else:
def convert_qvariant(x): def convert_qvariant(x):
vt = x.type() vt = x.type()
if vt == x.String: if vt == x.String:
return unicode(x.toString()) return x.toString()
if vt == x.List: if vt == x.List:
return [convert_qvariant(i) for i in x.toList()] return [convert_qvariant(i) for i in x.toList()]
return x.toPyObject() return x.toPyObject()

View File

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

View File

@@ -4,7 +4,7 @@
from __future__ import with_statement from __future__ import with_statement
# ignoblekey.py # 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 # Based on kindlekey.py, Copyright © 2010-2013 by some_updates and Apprentice Alf
@@ -14,13 +14,14 @@ from __future__ import with_statement
# Revision history: # Revision history:
# 1.0 - Initial release # 1.0 - Initial release
# 1.1 - remove duplicates and return last key as single key # 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 Get Barnes & Noble EPUB user key from nook Studio log file
""" """
__license__ = 'GPL v3' __license__ = 'GPL v3'
__version__ = "1.1" __version__ = "2.0"
import sys import sys
import os import os
@@ -97,7 +98,10 @@ def getNookLogFiles():
logFiles = [] logFiles = []
found = False found = False
if iswindows: 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 # some 64 bit machines do not have the proper registry key for some reason
# or the python interface to the 32 vs 64 bit registry is broken # or the python interface to the 32 vs 64 bit registry is broken
@@ -318,7 +322,7 @@ def gui_main():
keyfileout.write(key) keyfileout.write(key)
success = True success = True
tkMessageBox.showinfo(progname, u"Key successfully retrieved to {0}".format(outfile)) 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))) tkMessageBox.showerror(progname, u"Error: {0}".format(str(e)))
except Exception: except Exception:
root.wm_state('normal') root.wm_state('normal')

View File

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

View File

@@ -153,7 +153,7 @@
from __future__ import print_function from __future__ import print_function
__version__ = '3.2.4' __version__ = '3.2.4'
__about__ = u"Obok v{0}\nCopyright © 2012-2016 Physisticated et al.".format(__version__) __about__ = "Obok v{0}\nCopyright © 2012-2016 Physisticated et al.".format(__version__)
import sys import sys
import os import os
@@ -173,10 +173,10 @@ import tempfile
can_parse_xml = True can_parse_xml = True
try: try:
from xml.etree import ElementTree as ET from xml.etree import ElementTree as ET
# print u"using xml.etree for xml parsing" # print "using xml.etree for xml parsing"
except ImportError: except ImportError:
can_parse_xml = False 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 # List of all known hash keys
KOBO_HASH_KEYS = ['88b3a2e13', 'XzUhGYdFp', 'NoCanLook','QJhwzAtXL'] KOBO_HASH_KEYS = ['88b3a2e13', 'XzUhGYdFp', 'NoCanLook','QJhwzAtXL']
@@ -309,9 +309,9 @@ class KoboLibrary(object):
# step 1. check whether this looks like a real device # step 1. check whether this looks like a real device
if (device_path): if (device_path):
# we got a 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 # 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))): if (not(os.path.isfile(kobodb))):
# device path seems to be wrong, unset it # device path seems to be wrong, unset it
device_path = u"" device_path = u""
@@ -323,22 +323,22 @@ class KoboLibrary(object):
if (len(serials) == 0): if (len(serials) == 0):
# we got a device path but no saved serial # we got a device path but no saved serial
# try to get the serial from the device # 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 # get serial from device_path/.adobe-digital-editions/device.xml
if can_parse_xml: if can_parse_xml:
devicexml = os.path.join(device_path, '.adobe-digital-editions', 'device.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)): if (os.path.exists(devicexml)):
# print u"trying to parse {0}".format(devicexml) # print "trying to parse {0}".format(devicexml)
xmltree = ET.parse(devicexml) xmltree = ET.parse(devicexml)
for node in xmltree.iter(): for node in xmltree.iter():
if "deviceSerial" in node.tag: if "deviceSerial" in node.tag:
serial = node.text serial = node.text
# print u"found serial {0}".format(serial) # print "found serial {0}".format(serial)
serials.append(serial) serials.append(serial)
break break
else: else:
# print u"cannot get serials from device." # print "cannot get serials from device."
device_path = u"" device_path = u""
self.kobodir = u"" self.kobodir = u""
kobodb = u"" kobodb = u""
@@ -346,23 +346,26 @@ class KoboLibrary(object):
if (self.kobodir == u""): if (self.kobodir == u""):
# step 4. we haven't found a device with serials, so try desktop apps # step 4. we haven't found a device with serials, so try desktop apps
if sys.platform.startswith('win'): if sys.platform.startswith('win'):
import _winreg as winreg try:
import winreg
except ImportError:
import _winreg as winreg
if sys.getwindowsversion().major > 5: if sys.getwindowsversion().major > 5:
if 'LOCALAPPDATA' in os.environ.keys(): if 'LOCALAPPDATA' in os.environ.keys():
# Python 2.x does not return unicode env. Use Python 3.x # Python 2.x does not return unicode env. Use Python 3.x
self.kobodir = winreg.ExpandEnvironmentStrings(u"%LOCALAPPDATA%") self.kobodir = winreg.ExpandEnvironmentStrings("%LOCALAPPDATA%")
if (self.kobodir == u""): if (self.kobodir == u""):
if 'USERPROFILE' in os.environ.keys(): if 'USERPROFILE' in os.environ.keys():
# Python 2.x does not return unicode env. Use Python 3.x # Python 2.x does not return unicode env. Use Python 3.x
self.kobodir = os.path.join(winreg.ExpandEnvironmentStrings(u"%USERPROFILE%"), u"Local Settings", u"Application Data") self.kobodir = os.path.join(winreg.ExpandEnvironmentStrings("%USERPROFILE%"), "Local Settings", "Application Data")
self.kobodir = os.path.join(self.kobodir, u"Kobo", u"Kobo Desktop Edition") self.kobodir = os.path.join(self.kobodir, "Kobo", "Kobo Desktop Edition")
elif sys.platform.startswith('darwin'): elif sys.platform.startswith('darwin'):
self.kobodir = os.path.join(os.environ['HOME'], u"Library", u"Application Support", u"Kobo", u"Kobo Desktop Edition") self.kobodir = os.path.join(os.environ['HOME'], "Library", "Application Support", "Kobo", "Kobo Desktop Edition")
#elif linux_path != None: #elif linux_path != None:
# Probably Linux, let's get the wine prefix and path to Kobo. # 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") # self.kobodir = os.path.join(linux_path, "Local Settings", "Application Data", "Kobo", "Kobo Desktop Edition")
# desktop versions use Kobo.sqlite # 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 # check for existence of file
if (not(os.path.isfile(kobodb))): if (not(os.path.isfile(kobodb))):
# give up here, we haven't found anything useful # give up here, we haven't found anything useful
@@ -371,7 +374,7 @@ class KoboLibrary(object):
if (self.kobodir != 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 # 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. # so we can ensure it's not using WAL logging which sqlite3 can't do.
self.newdb = tempfile.NamedTemporaryFile(mode='wb', delete=False) self.newdb = tempfile.NamedTemporaryFile(mode='wb', delete=False)
@@ -431,7 +434,7 @@ class KoboLibrary(object):
def __bookfile (self, volumeid): def __bookfile (self, volumeid):
"""The filename needed to open a given book.""" """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): def __getmacaddrs (self):
"""The list of all MAC addresses on this machine.""" """The list of all MAC addresses on this machine."""
@@ -448,7 +451,7 @@ class KoboLibrary(object):
output = subprocess.check_output('/sbin/ifconfig -a', shell=True) output = subprocess.check_output('/sbin/ifconfig -a', shell=True)
matches = c.findall(output) matches = c.findall(output)
for m in matches: for m in matches:
# print u"m:{0}".format(m[0]) # print "m:{0}".format(m[0])
macaddrs.append(m[0].upper()) macaddrs.append(m[0].upper())
elif sys.platform.startswith('linux'): elif sys.platform.startswith('linux'):
p_out = subprocess.check_output("ip -br link show | awk '{print $3}'", shell=True) p_out = subprocess.check_output("ip -br link show | awk '{print $3}'", shell=True)
@@ -596,32 +599,32 @@ class KoboFile(object):
# assume utf-8 with no BOM # assume utf-8 with no BOM
textoffset = 0 textoffset = 0
stride = 1 stride = 1
print(u"Checking text:{0}:".format(contents[:10])) print("Checking text:{0}:".format(contents[:10]))
# check for byte order mark # check for byte order mark
if contents[:3]=="\xef\xbb\xbf": if contents[:3]=="\xef\xbb\xbf":
# seems to be utf-8 with BOM # seems to be utf-8 with BOM
print(u"Could be utf-8 with BOM") print("Could be utf-8 with BOM")
textoffset = 3 textoffset = 3
elif contents[:2]=="\xfe\xff": elif contents[:2]=="\xfe\xff":
# seems to be utf-16BE # seems to be utf-16BE
print(u"Could be utf-16BE") print("Could be utf-16BE")
textoffset = 3 textoffset = 3
stride = 2 stride = 2
elif contents[:2]=="\xff\xfe": elif contents[:2]=="\xff\xfe":
# seems to be utf-16LE # seems to be utf-16LE
print(u"Could be utf-16LE") print("Could be utf-16LE")
textoffset = 2 textoffset = 2
stride = 2 stride = 2
else: 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 # now check that the first few characters are in the ASCII range
for i in xrange(textoffset,textoffset+5*stride,stride): for i in xrange(textoffset,textoffset+5*stride,stride):
if ord(contents[i])<32 or ord(contents[i])>127: if ord(contents[i])<32 or ord(contents[i])>127:
# Non-ascii, so decryption probably failed # 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,ord(contents[i])))
raise ValueError raise ValueError
print(u"Seems to be good text") print("Seems to be good text")
return True return True
if contents[:5]=="<?xml" or contents[:8]=="\xef\xbb\xbf<?xml": if contents[:5]=="<?xml" or contents[:8]=="\xef\xbb\xbf<?xml":
# utf-8 # utf-8
@@ -642,13 +645,13 @@ class KoboFile(object):
# utf-16LE of weird <!DOCTYPE start # utf-16LE of weird <!DOCTYPE start
return True return True
else: else:
print(u"Bad XML: {0}".format(contents[:8])) print("Bad XML: {0}".format(contents[:8]))
raise ValueError raise ValueError
elif self.mimetype == 'image/jpeg': elif self.mimetype == 'image/jpeg':
if contents[:3] == '\xff\xd8\xff': if contents[:3] == '\xff\xd8\xff':
return True return True
else: else:
print(u"Bad JPEG: {0}".format(contents[:3].encode('hex'))) print("Bad JPEG: {0}".format(contents[:3].encode('hex')))
raise ValueError() raise ValueError()
return False return False
@@ -671,18 +674,18 @@ class KoboFile(object):
return contents return contents
def decrypt_book(book, lib): def decrypt_book(book, lib):
print(u"Converting {0}".format(book.title)) print("Converting {0}".format(book.title))
zin = zipfile.ZipFile(book.filename, "r") zin = zipfile.ZipFile(book.filename, "r")
# make filename out of Unicode alphanumeric and whitespace equivalents from title # 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'): 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) 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 return 0
result = 1 result = 1
for userkey in lib.userkeys: for userkey in lib.userkeys:
print(u"Trying key: {0}".format(userkey.encode('hex_codec'))) print("Trying key: {0}".format(userkey.encode('hex_codec')))
try: try:
zout = zipfile.ZipFile(outname, "w", zipfile.ZIP_DEFLATED) zout = zipfile.ZipFile(outname, "w", zipfile.ZIP_DEFLATED)
for filename in zin.namelist(): for filename in zin.namelist():
@@ -694,12 +697,12 @@ def decrypt_book(book, lib):
file.check(contents) file.check(contents)
zout.writestr(filename, contents) zout.writestr(filename, contents)
zout.close() zout.close()
print(u"Decryption succeeded.") print("Decryption succeeded.")
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)))
result = 0 result = 0
break break
except ValueError: except ValueError:
print(u"Decryption failed.") print("Decryption failed.")
zout.close() zout.close()
os.remove(outname) os.remove(outname)
zin.close() zin.close()
@@ -708,7 +711,7 @@ def decrypt_book(book, lib):
def cli_main(): def cli_main():
description = __about__ 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 = 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('--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") parser.add_argument('--all', action='store_true', help="flag for converting all books on device")
@@ -724,25 +727,25 @@ def cli_main():
books = lib.books books = lib.books
else: else:
for i, book in enumerate(lib.books): for i, book in enumerate(lib.books):
print(u"{0}: {1}".format(i + 1, book.title)) print("{0}: {1}".format(i + 1, book.title))
print(u"Or 'all'") print("Or 'all'")
choice = raw_input(u"Convert book number... ") choice = raw_input("Convert book number... ")
if choice == u'all': if choice == "all":
books = list(lib.books) books = list(lib.books)
else: else:
try: try:
num = int(choice) num = int(choice)
books = [lib.books[num - 1]] books = [lib.books[num - 1]]
except (ValueError, IndexError): except (ValueError, IndexError):
print(u"Invalid choice. Exiting...") print("Invalid choice. Exiting...")
exit() exit()
results = [decrypt_book(book, lib) for book in books] results = [decrypt_book(book, lib) for book in books]
lib.close() lib.close()
overall_result = all(result != 0 for result in results) overall_result = all(result != 0 for result in results)
if overall_result != 0: 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 return overall_result

View File

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

View File

@@ -1,18 +1,30 @@
# DeDRM_tools # DeDRM_tools
DeDRM tools for ebooks DeDRM tools for ebooks
This is a repository of all the scripts and other tools for removing DRM from ebooks that I could find, committed in date order as best as I could manage. (Except for the Requiem tools for Apple's iBooks, and Convert LIT for Microsoft's .lit ebooks.) This is a fork of Apprentice Harper's version of the DeDRM tools. I've added some of the PRs that still haven't been merged, as well as added some more features / bugfixes myself.
Mostly it tracks the tools released by Apprentice Alf, athough it also includes the individual tools and their histories from before Alf had a blog. Take a look at [the CHANGELOG](https://github.com/noDRM/DeDRM_tools/blob/master/CHANGELOG.md) to see a list of changes since the last version by Apprentice Harper (v7.2.1). This plugin will start with version v10.0.0 so there won't be conflicting / duplicate version numbers when Apprentice Harper's version is updated again.
Users should download the latest zip archive. The v10.0.0 versions of this plugin should both work with Calibre 5.x (Python 3) as well as Calibre 4.x and lower (Python 2). If you encounter issues with this plugin in Calibre 4.x or lower, please open a bug report.
Developers might be interested in forking the repository, as it contains unzipped versions of those tools that are zipped to make the changes over time easier to follow.
# Original README from Apprentice Harper
This is a repository that tracks all the scripts and other tools for removing DRM from ebooks that I could find, committed in date order as best as I could manage. (Except for the Requiem tools for Apple's iBooks, and Convert LIT for Microsoft's .lit ebooks.) This includes the tools from a time before Apprentice Alf had a blog, and continues through to when Apprentice Harper (with help) took over maintenance of the tools.
The individual scripts are now released as two plugins for calibre: DeDRM and Obok.
The DeDRM plugin handles books that use Amazon DRM, Adobe Digital Editions DRM (version 1), Barnes & Noble DRM, and some historical formats.
The Obok plugin handles Kobo DRM.
Users with calibre 5.x or later should use release 7.2.0 or later of the tools.
Users with calibe 4.x or earlier should use release 6.8.x of the tools.
For the latest Amazon KFX format, users of the calibre plugin should also install the KFX Input plugin from the standard calibre plugin menu. It's also available from the MobileRead thread here: https://www.mobileread.com/forums/showthread.php?t=291290 For the latest Amazon KFX format, users of the calibre plugin should also install the KFX Input plugin from the standard calibre plugin menu. It's also available from the MobileRead thread here: https://www.mobileread.com/forums/showthread.php?t=291290
Note that Amazon changes the DRM for KFX files frequently. What works for KFX today might not work tomorrow.
I welcome contributions from others to improve these tools, from expanding the range of books handled, improving key retrieval, to just general bug fixes, speed improvements and UI enhancements. I welcome contributions from others to improve these tools, from expanding the range of books handled, improving key retrieval, to just general bug fixes, speed improvements and UI enhancements.
I urge people to read the FAQs. But to cover the most common: Use ADE 2.0.1 to be sure not to get the new DRM scheme that these tools can't handle. Do remember to unzip the downloaded archive to get the plugin. You can't load the whole archive into calibre. I urge people to read the FAQs. But to cover the most common: Use ADE 2.0.1 to be sure not to get the new DRM scheme that these tools can't handle. Do remember to unzip the downloaded archive to get the plugin (beta versions may be just the plugin don't unzip that). You can't load the whole tools archive into calibre.
My special thanks to all those developers who have done the hard work of reverse engineering to provide the initial tools. My special thanks to all those developers who have done the hard work of reverse engineering to provide the initial tools.

View File

@@ -26,6 +26,8 @@ The tools are provided in the form of plugins for calibre. Calibre is an open so
DeDRM plugin for calibre (Mac OS X, Windows) DeDRM plugin for calibre (Mac OS X, Windows)
------------------------------------------------------- -------------------------------------------------------
calibe 5.x and later are now written in Python 3, and plugins must also use Python 3. If you have calibre 5, you must use version 7.x or later of the plugins. For calibre 4.x and earlier, use version 6.8.x of the plugins.
The DeDRM plugin for calibre removes DRM from your Kindle and Adobe DRM ebooks when they are imported to calibre. Just install the DeDRM plugin (DeDRM_plugin.zip), following the instructions and configuration directions provided in the ReadMe file and the help links in the plugin's configuration dialogs. The DeDRM plugin for calibre removes DRM from your Kindle and Adobe DRM ebooks when they are imported to calibre. Just install the DeDRM plugin (DeDRM_plugin.zip), following the instructions and configuration directions provided in the ReadMe file and the help links in the plugin's configuration dialogs.
Once installed and configured, you can simply add a DRM book to calibre and a DRM-free version will be imported into the calibre database. Note that DRM removal only occurs on IMPORT not on CONVERSION or at any other time. If you have already imported DRMed books you'll need to remove the books from calibre and re-import them. Once installed and configured, you can simply add a DRM book to calibre and a DRM-free version will be imported into the calibre database. Note that DRM removal only occurs on IMPORT not on CONVERSION or at any other time. If you have already imported DRMed books you'll need to remove the books from calibre and re-import them.
@@ -53,6 +55,7 @@ It may be possible to use the plugins on a Linux system, but no support is given
Credits Credits
------- -------
The original inept and ignoble scripts were by i♥cabbages The original inept and ignoble scripts were by i♥cabbages
The original Readium LCP DRM removal by NoDRM
The original mobidedrm and erdr2pml scripts were by The Dark Reverser The original mobidedrm and erdr2pml scripts were by The Dark Reverser
The original topaz DRM removal script was by CMBDTC The original topaz DRM removal script was by CMBDTC
The original topaz format conversion scripts were by some_updates, clarknova and Bart Simpson The original topaz format conversion scripts were by some_updates, clarknova and Bart Simpson

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env python #!/usr/bin/env python3
# code: utf-8 # -*- coding: utf-8 -*-
''' '''
A wrapper script to generate zip files for GitHub releases. A wrapper script to generate zip files for GitHub releases.
@@ -13,25 +13,12 @@ import os
import shutil import shutil
DEDRM_SRC_DIR = 'DeDRM_Plugin' DEDRM_SRC_DIR = 'DeDRM_plugin'
DEDRM_README= 'DeDRM_Plugin_ReadMe.txt' DEDRM_README= 'DeDRM_plugin_ReadMe.txt'
OBOK_SRC_DIR = 'Obok_plugin' OBOK_SRC_DIR = 'Obok_plugin'
OBOK_README = 'Obok_plugin_ReadMe.txt' OBOK_README = 'obok_plugin_ReadMe.txt'
RELEASE_DIR = 'release' RELEASE_DIR = 'release'
def make_calibre_plugin():
shutil.make_archive(core_dir, 'zip', core_dir)
shutil.rmtree(core_dir)
def make_obok_plugin():
obok_plugin_dir = os.path.join(SHELLS_BASE, 'Obok_calibre_plugin')
core_dir = os.path.join(obok_plugin_dir, 'obok_plugin')
shutil.copytree(OBOK_SRC_DIR, core_dir)
shutil.make_archive(core_dir, 'zip')
shutil.rmtree(core_dir)
def make_release(version): def make_release(version):
try: try: