mirror of
https://github.com/noDRM/DeDRM_tools.git
synced 2026-03-21 13:28:56 +00:00
Compare commits
255 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d05594dcbc | ||
|
|
09a34cf7d9 | ||
|
|
ca6ec8f6d0 | ||
|
|
e9a6e80e5a | ||
|
|
33437073d6 | ||
|
|
2edde54c44 | ||
|
|
a44b50d1d8 | ||
|
|
05e0d0bedb | ||
|
|
1b391da815 | ||
|
|
1545d76803 | ||
|
|
d9353bdd93 | ||
|
|
5d10420422 | ||
|
|
f20bede242 | ||
|
|
39f8595139 | ||
|
|
9c41716e5e | ||
|
|
b4c0e33b8b | ||
|
|
90910ab106 | ||
|
|
88dd1350c0 | ||
|
|
40a8e4360b | ||
|
|
17ccc4d1b9 | ||
|
|
30425c1ec8 | ||
|
|
77dcc462aa | ||
|
|
be57bcca7d | ||
|
|
4a58f7017c | ||
|
|
eae512da8c | ||
|
|
7058fbeb98 | ||
|
|
8cd3523a17 | ||
|
|
cc17d9cc59 | ||
|
|
969fe52e13 | ||
|
|
95fc924d1a | ||
|
|
0313088c15 | ||
|
|
066e613cee | ||
|
|
14947cd10c | ||
|
|
0005bba3c3 | ||
|
|
8e10b090a2 | ||
|
|
007a8e8a15 | ||
|
|
73af5d355d | ||
|
|
45a1a64db5 | ||
|
|
bc1c3c2197 | ||
|
|
79cfddfbee | ||
|
|
aa41bba68c | ||
|
|
86a90117e5 | ||
|
|
874a6b8de9 | ||
|
|
01c654cb68 | ||
|
|
5bc28623cb | ||
|
|
c1d7fcbb7f | ||
|
|
45eefd6c80 | ||
|
|
33e37eb375 | ||
|
|
4229b8ff85 | ||
|
|
91e4645315 | ||
|
|
425d8af73e | ||
|
|
0ce86fa8db | ||
|
|
ecc7db09a9 | ||
|
|
d7ddc2ab93 | ||
|
|
fd51422a36 | ||
|
|
cb36ca1b0d | ||
|
|
76a47e0dd0 | ||
|
|
70a754fb46 | ||
|
|
ffd79d5fe4 | ||
|
|
21a7b13524 | ||
|
|
52bdbe95c9 | ||
|
|
495dda3809 | ||
|
|
52e83922c0 | ||
|
|
6cbc5285cb | ||
|
|
33b9630ca5 | ||
|
|
9346f86f73 | ||
|
|
8d2d6627cf | ||
|
|
6f198b247c | ||
|
|
9fb95eff41 | ||
|
|
0b2b81fd23 | ||
|
|
63aecc598f | ||
|
|
51c8be6baf | ||
|
|
7aab8a3711 | ||
|
|
2789cee331 | ||
|
|
823704cf36 | ||
|
|
a7974f0f14 | ||
|
|
ed412bee35 | ||
|
|
6cee615f26 | ||
|
|
c4581b4d72 | ||
|
|
f6a568bcc1 | ||
|
|
bf6170e613 | ||
|
|
afcd79c0cc | ||
|
|
fdf0389936 | ||
|
|
5599c1694b | ||
|
|
dff90fae6f | ||
|
|
d33f679eae | ||
|
|
225e74a334 | ||
|
|
13e9a14907 | ||
|
|
92ea0a2f24 | ||
|
|
a1059650f6 | ||
|
|
a3cc221932 | ||
|
|
6732be1434 | ||
|
|
ad5cb056f0 | ||
|
|
d3c7388327 | ||
|
|
8e436ad920 | ||
|
|
ae806f734e | ||
|
|
ccfa454226 | ||
|
|
6716db1f62 | ||
|
|
0e0d7d8b14 | ||
|
|
981aadc497 | ||
|
|
26eb5d676c | ||
|
|
464788a3f1 | ||
|
|
036f9007fd | ||
|
|
bdd1c2e474 | ||
|
|
54a58d05a5 | ||
|
|
218539f131 | ||
|
|
f9d9b6016f | ||
|
|
131cea1215 | ||
|
|
731eeac087 | ||
|
|
cdab22e59c | ||
|
|
b8b324956c | ||
|
|
1955b34883 | ||
|
|
dbc5c2b4de | ||
|
|
856fef55be | ||
|
|
f2fa0426b7 | ||
|
|
c3376cc492 | ||
|
|
dc72c368a5 | ||
|
|
77033e1602 | ||
|
|
15cd372ad9 | ||
|
|
c52e4db3df | ||
|
|
45038cc77b | ||
|
|
5ec9c98a0b | ||
|
|
66bab7bd7d | ||
|
|
e0c7d7d382 | ||
|
|
f12a4f3856 | ||
|
|
87881659c4 | ||
|
|
dbc7f26097 | ||
|
|
c58e82d97f | ||
|
|
74bcf33591 | ||
|
|
a1703e15d4 | ||
|
|
591448d1f5 | ||
|
|
a74f37c79e | ||
|
|
7f4e6698ef | ||
|
|
e2e19fb50f | ||
|
|
4a319a3522 | ||
|
|
f1ef1b8ecd | ||
|
|
af0acf31a3 | ||
|
|
6dd022e6a0 | ||
|
|
ef59e112c1 | ||
|
|
019abecd05 | ||
|
|
7b3bbbd008 | ||
|
|
32968b1328 | ||
|
|
e0ec691dd6 | ||
|
|
0add3646d9 | ||
|
|
16024ee972 | ||
|
|
9cfe09e507 | ||
|
|
4a58d6f7dc | ||
|
|
c4c20eb07e | ||
|
|
cc33f40ecc | ||
|
|
939cdbb0c9 | ||
|
|
dc27c36761 | ||
|
|
7262264b95 | ||
|
|
4b160132a5 | ||
|
|
85fb4ff729 | ||
|
|
608bd400ee | ||
|
|
781268e17e | ||
|
|
41d3da12ec | ||
|
|
83139bc590 | ||
|
|
e31752e334 | ||
|
|
2eb31c8fb5 | ||
|
|
a3c7bad67e | ||
|
|
dca0cf7d00 | ||
|
|
62e0a69089 | ||
|
|
9df1563492 | ||
|
|
971db9ae71 | ||
|
|
cf829db532 | ||
|
|
80c8bd2d24 | ||
|
|
969599ce6b | ||
|
|
f55420bbf4 | ||
|
|
7f758566d3 | ||
|
|
ff8d44492e | ||
|
|
21d4811bfe | ||
|
|
558efebbff | ||
|
|
1eaee6a0a8 | ||
|
|
3f644ddfd6 | ||
|
|
08bdacf476 | ||
|
|
109261bdc0 | ||
|
|
de50a02af9 | ||
|
|
6920f79a26 | ||
|
|
2800f7cd80 | ||
|
|
61c5096da0 | ||
|
|
9118ce77ab | ||
|
|
c3aa1b62bb | ||
|
|
afa4ac5716 | ||
|
|
c516306858 | ||
|
|
e76bb408a3 | ||
|
|
4868a7460e | ||
|
|
0859f197fc | ||
|
|
da85d4ffac | ||
|
|
6fd5535072 | ||
|
|
885ef5e890 | ||
|
|
22d2b37e04 | ||
|
|
837562db66 | ||
|
|
3dcf3a5483 | ||
|
|
f7b4efc3e1 | ||
|
|
2fbf2c1c5f | ||
|
|
3166273622 | ||
|
|
ea916d85fc | ||
|
|
2bb73584f2 | ||
|
|
8495ebe36d | ||
|
|
92bf51bc8f | ||
|
|
e15ff385ca | ||
|
|
d48f4b86cf | ||
|
|
2ef5c59ebe | ||
|
|
d2995539f0 | ||
|
|
ef3c7f261c | ||
|
|
778ce4782e | ||
|
|
69ac9b7399 | ||
|
|
423dec0309 | ||
|
|
582479c1f4 | ||
|
|
c1ece2f288 | ||
|
|
f5dd758b1b | ||
|
|
a107742191 | ||
|
|
ce8538a2ca | ||
|
|
2cf5960511 | ||
|
|
ef687eb057 | ||
|
|
7d5352fdf3 | ||
|
|
795f413ecb | ||
|
|
b35f777580 | ||
|
|
0895aeb323 | ||
|
|
eddbefcf91 | ||
|
|
0955713cd6 | ||
|
|
4e26b9d4e7 | ||
|
|
8c08c67aa8 | ||
|
|
90335bb925 | ||
|
|
a10d9a617f | ||
|
|
7edebeef0d | ||
|
|
e35b37c4f4 | ||
|
|
1fd972ee17 | ||
|
|
616548a9a8 | ||
|
|
e4c1a09d45 | ||
|
|
89cf29cb78 | ||
|
|
c74f4b20d3 | ||
|
|
ae703e523c | ||
|
|
48dac14218 | ||
|
|
798a7f9c8e | ||
|
|
43f80b767a | ||
|
|
e07bb6523b | ||
|
|
5d8dc595ce | ||
|
|
fc6f830088 | ||
|
|
ff51ee8227 | ||
|
|
952b7fa7c0 | ||
|
|
0e9e3cf7ca | ||
|
|
57702b7d17 | ||
|
|
666af55404 | ||
|
|
60f1865b53 | ||
|
|
488cc540cd | ||
|
|
5bb6b58bc1 | ||
|
|
3f591ce66f | ||
|
|
8bd53cd998 | ||
|
|
4bd89fa4aa | ||
|
|
b71ed3887e | ||
|
|
d152586edc | ||
|
|
aca8043174 | ||
|
|
8165ad3ebb |
17
.gitattributes
vendored
17
.gitattributes
vendored
@@ -1,17 +0,0 @@
|
|||||||
# Auto detect text files and perform LF normalization
|
|
||||||
* text=auto
|
|
||||||
|
|
||||||
# Custom for Visual Studio
|
|
||||||
*.cs diff=csharp
|
|
||||||
|
|
||||||
# Standard to msysgit
|
|
||||||
*.doc diff=astextplain
|
|
||||||
*.DOC diff=astextplain
|
|
||||||
*.docx diff=astextplain
|
|
||||||
*.DOCX diff=astextplain
|
|
||||||
*.dot diff=astextplain
|
|
||||||
*.DOT diff=astextplain
|
|
||||||
*.pdf diff=astextplain
|
|
||||||
*.PDF diff=astextplain
|
|
||||||
*.rtf diff=astextplain
|
|
||||||
*.RTF diff=astextplain
|
|
||||||
41
.github/ISSUE_TEMPLATE/QUESTION.yml
vendored
Normal file
41
.github/ISSUE_TEMPLATE/QUESTION.yml
vendored
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
name: Question
|
||||||
|
description: Questions for DeDRM Project
|
||||||
|
body:
|
||||||
|
- type: textarea
|
||||||
|
id: question
|
||||||
|
attributes:
|
||||||
|
label: Question / bug report
|
||||||
|
description: Please enter your question / your bug report.
|
||||||
|
- type: input
|
||||||
|
id: calibre-version
|
||||||
|
attributes:
|
||||||
|
label: Which version of Calibre are you running?
|
||||||
|
description: "Example: 5.32"
|
||||||
|
placeholder: "5.32"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: plugin-version
|
||||||
|
attributes:
|
||||||
|
label: Which version of the DeDRM plugin are you running?
|
||||||
|
description: "Example: v10.0.0"
|
||||||
|
placeholder: "v10.0.0"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: kindle-version
|
||||||
|
attributes:
|
||||||
|
label: If applicable, which version of the Kindle software are you running?
|
||||||
|
description: "Example: 1.24"
|
||||||
|
placeholder: "Leave empty if unrelated to Kindle books"
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: textarea
|
||||||
|
id: log
|
||||||
|
attributes:
|
||||||
|
label: Log output
|
||||||
|
description: If applicable, please post your log output here - into the code block.
|
||||||
|
value: |
|
||||||
|
```log
|
||||||
|
Paste log output here.
|
||||||
|
```
|
||||||
20
.github/workflows/main.yml
vendored
Normal file
20
.github/workflows/main.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
name: Package plugin
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
package:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Package
|
||||||
|
run: python3 make_release.py
|
||||||
|
- name: Upload
|
||||||
|
uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: plugin
|
||||||
|
path: |
|
||||||
|
DeDRM_tools_*.zip
|
||||||
|
DeDRM_tools.zip
|
||||||
95
.gitignore
vendored
95
.gitignore
vendored
@@ -1,96 +1 @@
|
|||||||
# Byte-compiled / optimized / DLL files
|
|
||||||
__pycache__/
|
|
||||||
*.pyc
|
|
||||||
|
|
||||||
# Distribution / packaging
|
|
||||||
.Python
|
|
||||||
env/
|
|
||||||
build/
|
|
||||||
develop-eggs/
|
|
||||||
dist/
|
|
||||||
downloads/
|
|
||||||
eggs/
|
|
||||||
lib64/
|
|
||||||
parts/
|
|
||||||
sdist/
|
|
||||||
var/
|
|
||||||
*.egg-info/
|
|
||||||
.installed.cfg
|
|
||||||
*.egg
|
|
||||||
|
|
||||||
# PyInstaller
|
|
||||||
# Usually these files are written by a python script from a template
|
|
||||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
||||||
*.manifest
|
|
||||||
*.spec
|
|
||||||
|
|
||||||
# Installer logs
|
|
||||||
pip-log.txt
|
|
||||||
pip-delete-this-directory.txt
|
|
||||||
|
|
||||||
# Unit test / coverage reports
|
|
||||||
htmlcov/
|
|
||||||
.tox/
|
|
||||||
.coverage
|
|
||||||
.cache
|
|
||||||
nosetests.xml
|
|
||||||
coverage.xml
|
|
||||||
|
|
||||||
# Translations
|
|
||||||
*.pot
|
|
||||||
|
|
||||||
# Django stuff:
|
|
||||||
*.log
|
|
||||||
|
|
||||||
# Sphinx documentation
|
|
||||||
docs/_build/
|
|
||||||
|
|
||||||
# PyBuilder
|
|
||||||
target/
|
|
||||||
|
|
||||||
# =========================
|
|
||||||
# Operating System Files
|
|
||||||
# =========================
|
|
||||||
|
|
||||||
# OSX
|
|
||||||
# =========================
|
|
||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.AppleDouble
|
|
||||||
.LSOverride
|
|
||||||
|
|
||||||
# Thumbnails
|
|
||||||
._*
|
|
||||||
|
|
||||||
# Files that might appear on external disk
|
|
||||||
.Spotlight-V100
|
|
||||||
.Trashes
|
|
||||||
|
|
||||||
# Directories potentially created on remote AFP share
|
|
||||||
.AppleDB
|
|
||||||
.AppleDesktop
|
|
||||||
Network Trash Folder
|
|
||||||
Temporary Items
|
|
||||||
.apdisk
|
|
||||||
|
|
||||||
# Windows
|
|
||||||
# =========================
|
|
||||||
|
|
||||||
# Windows image file caches
|
|
||||||
Thumbs.db
|
|
||||||
ehthumbs.db
|
|
||||||
|
|
||||||
# Folder config file
|
|
||||||
Desktop.ini
|
|
||||||
|
|
||||||
# Recycle Bin used on file shares
|
|
||||||
$RECYCLE.BIN/
|
|
||||||
|
|
||||||
# Windows Installer files
|
|
||||||
*.cab
|
|
||||||
*.msi
|
|
||||||
*.msm
|
|
||||||
*.msp
|
|
||||||
|
|
||||||
# Windows shortcuts
|
|
||||||
*.lnk
|
|
||||||
|
|||||||
@@ -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](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"]`.
|
||||||
|
|||||||
36
CHANGELOG.md
Normal file
36
CHANGELOG.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
List of changes since the fork of Apprentice Harper's repository:
|
||||||
|
|
||||||
|
## Fixes in v10.0.0 (2021-11-17):
|
||||||
|
|
||||||
|
- CI testing / linting removed as that always failed anyways. The CI now "just" packages the plugin.
|
||||||
|
- Support for the Readium LCP DRM (also known as "CARE DRM" or "TEA DRM"). This supports EPUB and PDF files. It does not yet support Readium LCPDF/LPF/LCPA/LCPAU/LCPDI files, as I don't have access to any of these. If you have an LCP-protected file in one of these formats that this plugin does not work with, please open [an issue](https://github.com/noDRM/DeDRM_tools/issues) and attach the file to the report.
|
||||||
|
- Add new Github issue report form which forces the user to include stuff like their Calibre version to hopefully increase the quality of bug reports.
|
||||||
|
- Issues with PDF files in Calibre 5 should be fixed (merged [apprenticeharper/DeDRM_tools#1689](https://github.com/apprenticeharper/DeDRM_tools/pull/1689) ).
|
||||||
|
- Fixed tons of issues with the B&N PDF DRM removal script ignoblepdf.py. It looks like that has never been tested since the move to Python3. I have integrated the B&N-specific code into ineptpdf.py, the original ignoblepdf.py is now unused. Fairly untested as I don't have any PDFs with B&N DRM.
|
||||||
|
- Issues with Obok key retrieval fixed (merged [apprenticeharper/DeDRM_tools#1691](https://github.com/apprenticeharper/DeDRM_tools/pull/1691) ).
|
||||||
|
- Issues with obfuscated Adobe fonts fixed (fixes [apprenticeharper/DeDRM_tools#1828](https://github.com/apprenticeharper/DeDRM_tools/issues/1828) ).
|
||||||
|
- Deobfuscate font files in EPUBs by default (can be disabled in the plugin settings).
|
||||||
|
- The standalone adobekey.py script now includes the account UUID in the key file name.
|
||||||
|
- When extracting the default key from an ADE install, include the account UUID in the key name.
|
||||||
|
- Adobe key management window size increased to account for longer key names due to the UUID.
|
||||||
|
- Verify that the decrypted book key has the correct format. This makes it way less likely for issue [apprenticeharper/DeDRM_tools#1862](https://github.com/apprenticeharper/DeDRM_tools/issues/1862) to cause trouble.
|
||||||
|
- If the Adobe owner UUID of a book being imported happens to be included in a particular key's name, try this key first before trying all the others. This completely fixes [apprenticeharper/DeDRM_tools#1862](https://github.com/apprenticeharper/DeDRM_tools/issues/1862), but only if the key name contains the correct UUID (not always the case, especially for keys imported with older versions of the plugin). It also makes DRM removal faster as the plugin no longer has to attempt all possible keys.
|
||||||
|
- Remove some additional DRM remnants in Amazon MOBI files (merged [apprenticeharper/DeDRM_tools#23](https://github.com/apprenticeharper/DeDRM_tools/pull/23) ).
|
||||||
|
- Just in case it's necessary, added a setting to the B&N key generation script to optionally allow the user to select the old key generation algorithm. Who knows, they might want to remove DRM from old books with the old key scheme.
|
||||||
|
- Add a more verbose error message when trying to remove DRM from a book with the new, not-yet-cracked version of the Adobe ADEPT DRM.
|
||||||
|
- Added back support for Python2 (Calibre 2.0+). Only tested with ADEPT (PDF & EPUB) and Readium LCP so far, please open an issue if there's errors with other book types.
|
||||||
|
- Begin work on removing some kinds of watermarks from files after DRM removal. This isn't tested a lot, and is disabled by default. You can enable it in the plugin settings.
|
||||||
|
- If you're using the [ACSM Input Plugin / DeACSM](https://www.mobileread.com/forums/showthread.php?t=341975), the encryption key will automatically be extracted from that plugin if necessary.
|
||||||
|
|
||||||
|
## Fixes in v10.0.1 (2021-11-19):
|
||||||
|
|
||||||
|
- Hotfix update to fix broken EPUB DRM removal due to a typo.
|
||||||
|
|
||||||
|
## Fixes in v10.0.2 (2021-11-29):
|
||||||
|
|
||||||
|
- Fix Kindle for Mac key retrieval (merged [apprenticeharper/DeDRM_tools#1936](https://github.com/apprenticeharper/DeDRM_tools/pull/1936) ), fixing #1.
|
||||||
|
- Fix Adobe key retrieval in case the username has been changed (merged [apprenticeharper/DeDRM_tools#1946](https://github.com/apprenticeharper/DeDRM_tools/pull/1946) ). This should fix the error "failed to decrypt user key key".
|
||||||
|
- Fix small issue with elibri watermark removal.
|
||||||
|
- Adobe key name will now contain account email.
|
||||||
@@ -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 plugin’s customization dialog, you will see a button with an icon that looks like a sheet of paper. Clicking this button will promt you to enter a new name for the highlighted key in the list. Enter the new name for the encryption key and click the OK button to use the new name, or Cancel to revert to the old name..</p>
|
<p>On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a sheet of paper. Clicking this button will prompt you to enter a new name for the highlighted key in the list. Enter the new name for the encryption key and click the OK button to use the new name, or Cancel to revert to the old name..</p>
|
||||||
|
|
||||||
<h3>Exporting Keys:</h3>
|
<h3>Exporting Keys:</h3>
|
||||||
|
|
||||||
@@ -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 plugin’s customization dialog, you will see a button with an icon that looks like a sheet of paper. Clicking this button will promt you to enter a new name for the highlighted key in the list. Enter the new name for the encryption key and click the OK button to use the new name, or Cancel to revert to the old name..</p>
|
<p>On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a sheet of paper. Clicking this button will prompt you to enter a new name for the highlighted key in the list. Enter the new name for the encryption key and click the OK button to use the new name, or Cancel to revert to the old name..</p>
|
||||||
|
|
||||||
<h3>Exporting Keys:</h3>
|
<h3>Exporting Keys:</h3>
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@ li {margin-top: 0.5em}
|
|||||||
|
|
||||||
<p>At the bottom-left of the plugin’s customization dialog, you will see a button labeled "Import Existing Keyfiles". Use this button to import existing ‘.b64’ key files. Key files might come from being exported from this or older plugins, or may have been generated using the original i♥cabbages script, or you may have made it by following the instructions above.</p>
|
<p>At the bottom-left of the plugin’s customization dialog, you will see a button labeled "Import Existing Keyfiles". Use this button to import existing ‘.b64’ key files. Key files might come from being exported from this or older plugins, or may have been generated using the original i♥cabbages script, or you may have made it by following the instructions above.</p>
|
||||||
|
|
||||||
<p>Once done creating/deleting/renaming/importing decryption keys, click Close to exit the customization dialogue. Your changes wil only be saved permanently when you click OK in the main configuration dialog.</p>
|
<p>Once done creating/deleting/renaming/importing decryption keys, click Close to exit the customization dialogue. Your changes will only be saved permanently when you click OK in the main configuration dialog.</p>
|
||||||
|
|
||||||
<h3>NOOK Study</h3>
|
<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>
|
||||||
@@ -36,7 +36,7 @@ li {margin-top: 0.5em}
|
|||||||
|
|
||||||
<p>On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a red "X". Clicking this button will delete the highlighted Kindle serial number from the list. You will be prompted once to be sure that’s what you truly mean to do. Once gone, it’s permanently gone.</p>
|
<p>On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a red "X". Clicking this button will delete the highlighted Kindle serial number from the list. You will be prompted once to be sure that’s what you truly mean to do. Once gone, it’s permanently gone.</p>
|
||||||
|
|
||||||
<p>Once done creating/deleting serial numbers, click Close to exit the customization dialogue. Your changes wil only be saved permanently when you click OK in the main configuration dialog.</p>
|
<p>Once done creating/deleting serial numbers, click Close to exit the customization dialogue. Your changes will only be saved permanently when you click OK in the main configuration dialog.</p>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
@@ -17,15 +17,19 @@ p {margin-top: 0}
|
|||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<h1>DeDRM Plugin <span class="version">(v6.3.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 wouldn’t be reading this help file. However, you should also delete any older DeDRM plugins, as this DeDRM plugin replaces the five older plugins: Kindle and Mobipocket DeDRM (K4MobiDeDRM), Ignoble Epub DeDRM (ignobleepub), Inept Epub DeDRM (ineptepub), Inept PDF DeDRM (ineptepub) and eReader PDB 2 PML (eReaderPDB2PML).</p>
|
<p>You have obviously managed to install the plugin, as otherwise you wouldn’t be reading this help file. However, you should also delete any older DRM removal plugins, as this DeDRM plugin replaces the five older plugins: Kindle and Mobipocket DeDRM (K4MobiDeDRM), Ignoble Epub DeDRM (ignobleepub), Inept Epub DeDRM (ineptepub), Inept PDF DeDRM (ineptepub) and eReader PDB 2 PML (eReaderPDB2PML).</p>
|
||||||
|
|
||||||
<h3>Configuration</h3>
|
<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 Harpers’s GitHub repository</a>. You can ask questions in the comments section of the <a href="http://apprenticealf.wordpress.com/2012/09/10/drm-removal-tools-for-ebooks/">first post</a> at <a href="http://wordpress.com/apprenticealf/">Apprentice Alf's blog</a> or <a href="https://github.com/apprenticeharper/DeDRM_tools/issues">raise an issue</a>. </h3>
|
<h3>For additional help read the <a href="https://github.com/noDRM/DeDRM_tools/blob/master/FAQs.md">FAQs</a> at <a href="https://github.com/noDRM/DeDRM_tools">NoDRM's GitHub repository</a> (or the corresponding <a href="https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md">FAQs</a> at <a href="https://github.com/apprenticeharper/DeDRM_tools/">Apprentice Harpers’s GitHub repository</a>). You can <a href="https://github.com/noDRM/DeDRM_tools/issues">open issue reports</a>related to this fork at NoDRM's GitHub repository.</h3>
|
||||||
|
|
||||||
|
|
||||||
<h2>Linux Systems Only</h2>
|
<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>
|
||||||
@@ -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 plugin’s customization dialog, you will see a button with an icon that looks like a sheet of paper. Clicking this button will promt you to enter a new name for the highlighted key in the list. Enter the new name for the encryption key and click the OK button to use the new name, or Cancel to revert to the old name..</p>
|
<p>On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a sheet of paper. Clicking this button will prompt you to enter a new name for the highlighted key in the list. Enter the new name for the encryption key and click the OK button to use the new name, or Cancel to revert to the old name..</p>
|
||||||
|
|
||||||
<h3>Exporting Keys:</h3>
|
<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>
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ li {margin-top: 0.5em}
|
|||||||
|
|
||||||
<p>On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a red "X". Clicking this button will delete the highlighted Mobipocket PID from the list. You will be prompted once to be sure that’s what you truly mean to do. Once gone, it’s permanently gone.</p>
|
<p>On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a red "X". Clicking this button will delete the highlighted Mobipocket PID from the list. You will be prompted once to be sure that’s what you truly mean to do. Once gone, it’s permanently gone.</p>
|
||||||
|
|
||||||
<p>Once done creating/deleting PIDs, click Close to exit the customization dialogue. Your changes wil only be saved permanently when you click OK in the main configuration dialog.</p>
|
<p>Once done creating/deleting PIDs, click Close to exit the customization dialogue. Your changes will only be saved permanently when you click OK in the main configuration dialog.</p>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
41
DeDRM_plugin/DeDRM_Readium LCP passphrase_Help.htm
Normal file
41
DeDRM_plugin/DeDRM_Readium LCP passphrase_Help.htm
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
|
||||||
|
"http://www.w3.org/TR/html4/strict.dtd">
|
||||||
|
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="content-type" content="text/html; charset=utf-8">
|
||||||
|
<title>Managing Readium LCP passphrases</title>
|
||||||
|
<style type="text/css">
|
||||||
|
span.version {font-size: 50%}
|
||||||
|
span.bold {font-weight: bold}
|
||||||
|
h3 {margin-bottom: 0}
|
||||||
|
p {margin-top: 0}
|
||||||
|
li {margin-top: 0.5em}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<h1>Managing Readium LCP passphrases</h1>
|
||||||
|
|
||||||
|
<p>Readium LCP is a relatively new eBook DRM. It's also known under the names "CARE DRM" or "TEA DRM". It does not rely on any accounts or key data that's difficult to acquire. All you need to open (or decrypt) LCP eBooks is the account passphrase given to you by the eBook provider - the very same passphrase you'd have to enter into your eBook reader device (once) to read LCP-encrypted books.</p>
|
||||||
|
|
||||||
|
|
||||||
|
<h3>Entering an LCP passphrase:</h3>
|
||||||
|
|
||||||
|
<p>On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a green plus sign (+). Clicking this button will open a new dialog for entering a new passphrase.</p>
|
||||||
|
|
||||||
|
<p>Just enter your passphrase as provided with the book, then click the OK button to save the passphrase. </p>
|
||||||
|
|
||||||
|
<p>Usually, passphrases are identical for all books bought with the same account. So if you buy multiple LCP-protected eBooks, they'll usually all have the same passphrase if they've all been bought at the same store with the same account. </p>
|
||||||
|
|
||||||
|
<h3>Deleting an LCP passphrase:</h3>
|
||||||
|
|
||||||
|
<p>On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a red "X". Clicking this button will delete the highlighted passphrase from the list. You will be prompted once to be sure that’s what you truly mean to do. Once gone, it’s permanently gone.</p>
|
||||||
|
|
||||||
|
<p>Once done entering/deleting passphrases, click Close to exit the customization dialogue. Your changes will only be saved permanently when you click OK in the main configuration dialog.</p>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
915
DeDRM_plugin/__init__.py
Normal file
915
DeDRM_plugin/__init__.py
Normal file
@@ -0,0 +1,915 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# __init__.py for DeDRM_plugin
|
||||||
|
# Copyright © 2008-2020 Apprentice Harper et al.
|
||||||
|
# Copyright © 2021 NoDRM
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__version__ = '10.0.2'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
|
||||||
|
# Released under the terms of the GNU General Public Licence, version 3
|
||||||
|
# <http://www.gnu.org/licenses/>
|
||||||
|
#
|
||||||
|
# All credit given to i♥cabbages and The Dark Reverser for the original standalone scripts.
|
||||||
|
# We had the much easier job of converting them to a calibre plugin.
|
||||||
|
#
|
||||||
|
# This plugin is meant to decrypt eReader PDBs, Adobe Adept ePubs, Barnes & Noble ePubs,
|
||||||
|
# Adobe Adept PDFs, Amazon Kindle and Mobipocket files without having
|
||||||
|
# to install any dependencies... other than having calibre installed, of course.
|
||||||
|
#
|
||||||
|
# Configuration:
|
||||||
|
# Check out the plugin's configuration settings by clicking the "Customize plugin"
|
||||||
|
# button when you have the "DeDRM" plugin highlighted (under Preferences->
|
||||||
|
# Plugins->File type plugins). Once you have the configuration dialog open, you'll
|
||||||
|
# see a Help link on the top right-hand side.
|
||||||
|
#
|
||||||
|
# Revision history:
|
||||||
|
# 6.0.0 - Initial release
|
||||||
|
# 6.0.1 - Bug Fixes for Windows App, Kindle for Mac and Windows Adobe Digital Editions
|
||||||
|
# 6.0.2 - Restored call to Wine to get Kindle for PC keys, added for ADE
|
||||||
|
# 6.0.3 - Fixes for Kindle for Mac and Windows non-ascii user names
|
||||||
|
# 6.0.4 - Fixes for stand-alone scripts and applications
|
||||||
|
# and pdb files in plugin and initial conversion of prefs.
|
||||||
|
# 6.0.5 - Fix a key issue
|
||||||
|
# 6.0.6 - Fix up an incorrect function call
|
||||||
|
# 6.0.7 - Error handling for incomplete PDF metadata
|
||||||
|
# 6.0.8 - Fixes a Wine key issue and topaz support
|
||||||
|
# 6.0.9 - Ported to work with newer versions of Calibre (moved to Qt5). Still supports older Qt4 versions.
|
||||||
|
# 6.1.0 - Fixed multiple books import problem and PDF import with no key problem
|
||||||
|
# 6.2.0 - Support for getting B&N key from nook Study log. Fix for UTF-8 filenames in Adobe ePubs.
|
||||||
|
# Fix for not copying needed files. Fix for getting default Adobe key for PDFs
|
||||||
|
# 6.2.1 - Fix for non-ascii Windows user names
|
||||||
|
# 6.2.2 - Added URL method for B&N/nook books
|
||||||
|
# 6.3.0 - Added in Kindle for Android serial number solution
|
||||||
|
# 6.3.1 - Version number bump for clarity
|
||||||
|
# 6.3.2 - Fixed Kindle for Android help file
|
||||||
|
# 6.3.3 - Bug fix for Kindle for PC support
|
||||||
|
# 6.3.4 - Fixes for Kindle for Android, Linux, and Kobo 3.17
|
||||||
|
# 6.3.5 - Fixes for Linux, and Kobo 3.19 and more logging
|
||||||
|
# 6.3.6 - Fixes for ADE ePub and PDF introduced in 6.3.5
|
||||||
|
# 6.4.0 - Updated for new Kindle for PC encryption
|
||||||
|
# 6.4.1 - Fix for some new tags in Topaz ebooks.
|
||||||
|
# 6.4.2 - Fix for more new tags in Topaz ebooks and very small Topaz ebooks
|
||||||
|
# 6.4.3 - Fix for error that only appears when not in debug mode
|
||||||
|
# Also includes fix for Macs with bonded ethernet ports
|
||||||
|
# 6.5.0 - Big update to Macintosh app
|
||||||
|
# Fix for some more 'new' tags in Topaz ebooks.
|
||||||
|
# Fix an error in wineutils.py
|
||||||
|
# 6.5.1 - Updated version number, added PDF check for DRM-free documents
|
||||||
|
# 6.5.2 - Another Topaz fix
|
||||||
|
# 6.5.3 - Warn about KFX files explicitly
|
||||||
|
# 6.5.4 - Mac App Fix, improve PDF decryption, handle latest tcl changes in ActivePython
|
||||||
|
# 6.5.5 - Finally a fix for the Windows non-ASCII user names.
|
||||||
|
# 6.6.0 - Add kfx and kfx-zip as supported file types (also invoke this plugin if the original
|
||||||
|
# imported format was azw8 since that may be converted to kfx)
|
||||||
|
# 6.6.1 - Thanks to wzyboy for a fix for stand-alone tools, and the new folder structure.
|
||||||
|
# 6.6.2 - revamp of folders to get Mac OS X app working. Updated to 64-bit app. Various fixes.
|
||||||
|
# 6.6.3 - More cleanup of kindle book names and start of support for .kinf2018
|
||||||
|
# 6.7.0 - Handle new library in calibre.
|
||||||
|
# 6.8.0 - Full support for .kinf2018 and new KFX encryption (Kindle for PC/Mac 2.5+)
|
||||||
|
# 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.
|
||||||
|
# 10.0.1 - Fixes a bug in the watermark code.
|
||||||
|
# 10.0.2 - Fix Kindle for Mac & update Adobe key retrieval
|
||||||
|
|
||||||
|
"""
|
||||||
|
Decrypt DRMed ebooks.
|
||||||
|
"""
|
||||||
|
|
||||||
|
PLUGIN_NAME = "DeDRM"
|
||||||
|
PLUGIN_VERSION_TUPLE = tuple([int(x) for x in __version__.split(".")])
|
||||||
|
PLUGIN_VERSION = ".".join([str(x)for x in PLUGIN_VERSION_TUPLE])
|
||||||
|
# Include an html helpfile in the plugin's zipfile with the following name.
|
||||||
|
RESOURCE_NAME = PLUGIN_NAME + '_Help.htm'
|
||||||
|
|
||||||
|
import codecs
|
||||||
|
import sys, os
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
|
||||||
|
class DeDRMError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
from calibre.customize import FileTypePlugin
|
||||||
|
from calibre.constants import iswindows, isosx
|
||||||
|
from calibre.gui2 import is_ok_to_use_qt
|
||||||
|
from calibre.utils.config import config_dir
|
||||||
|
|
||||||
|
|
||||||
|
# Wrap a stream so that output gets flushed immediately
|
||||||
|
# and also make sure that any unicode strings get safely
|
||||||
|
# encoded using "replace" before writing them.
|
||||||
|
class SafeUnbuffered:
|
||||||
|
def __init__(self, stream):
|
||||||
|
self.stream = stream
|
||||||
|
self.encoding = stream.encoding
|
||||||
|
if self.encoding == None:
|
||||||
|
self.encoding = "utf-8"
|
||||||
|
def write(self, data):
|
||||||
|
if isinstance(data,str) or isinstance(data,unicode):
|
||||||
|
# str for Python3, unicode for Python2
|
||||||
|
data = data.encode(self.encoding,"replace")
|
||||||
|
try:
|
||||||
|
buffer = getattr(self.stream, 'buffer', self.stream)
|
||||||
|
# self.stream.buffer for Python3, self.stream for Python2
|
||||||
|
buffer.write(data)
|
||||||
|
buffer.flush()
|
||||||
|
except:
|
||||||
|
# We can do nothing if a write fails
|
||||||
|
raise
|
||||||
|
def __getattr__(self, attr):
|
||||||
|
return getattr(self.stream, attr)
|
||||||
|
|
||||||
|
class DeDRM(FileTypePlugin):
|
||||||
|
name = PLUGIN_NAME
|
||||||
|
description = "Removes DRM from Amazon Kindle, Adobe Adept (including Kobo), Readium LCP, Barnes & Noble, Mobipocket and eReader ebooks. Credit given to i♥cabbages and The Dark Reverser for the original stand-alone scripts."
|
||||||
|
supported_platforms = ['linux', 'osx', 'windows']
|
||||||
|
author = "Apprentice Alf, Apprentice Harper, NoDRM, The Dark Reverser and i♥cabbages"
|
||||||
|
version = PLUGIN_VERSION_TUPLE
|
||||||
|
#minimum_calibre_version = (5, 0, 0) # Python 3.
|
||||||
|
minimum_calibre_version = (2, 0, 0) # Needs Calibre 1.0 minimum. 1.X untested.
|
||||||
|
file_types = set(['epub','pdf','pdb','prc','mobi','pobi','azw','azw1','azw3','azw4','azw8','tpz','kfx','kfx-zip'])
|
||||||
|
on_import = True
|
||||||
|
on_preprocess = True
|
||||||
|
priority = 600
|
||||||
|
|
||||||
|
|
||||||
|
def initialize(self):
|
||||||
|
"""
|
||||||
|
Dynamic modules can't be imported/loaded from a zipfile.
|
||||||
|
So this routine will extract the appropriate
|
||||||
|
library for the target OS and copy it to the 'alfcrypto' subdirectory of
|
||||||
|
calibre's configuration directory. That 'alfcrypto' directory is then
|
||||||
|
inserted into the syspath (as the very first entry) in the run function
|
||||||
|
so the CDLL stuff will work in the alfcrypto.py script.
|
||||||
|
|
||||||
|
The extraction only happens once per version of the plugin
|
||||||
|
Also perform upgrade of preferences once per version
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.pluginsdir = os.path.join(config_dir,"plugins")
|
||||||
|
if not os.path.exists(self.pluginsdir):
|
||||||
|
os.mkdir(self.pluginsdir)
|
||||||
|
self.maindir = os.path.join(self.pluginsdir,"DeDRM")
|
||||||
|
if not os.path.exists(self.maindir):
|
||||||
|
os.mkdir(self.maindir)
|
||||||
|
self.helpdir = os.path.join(self.maindir,"help")
|
||||||
|
if not os.path.exists(self.helpdir):
|
||||||
|
os.mkdir(self.helpdir)
|
||||||
|
self.alfdir = os.path.join(self.maindir,"libraryfiles")
|
||||||
|
if not os.path.exists(self.alfdir):
|
||||||
|
os.mkdir(self.alfdir)
|
||||||
|
# only continue if we've never run this version of the plugin before
|
||||||
|
self.verdir = os.path.join(self.maindir,PLUGIN_VERSION)
|
||||||
|
if not os.path.exists(self.verdir):
|
||||||
|
if iswindows:
|
||||||
|
names = ["alfcrypto.dll","alfcrypto64.dll"]
|
||||||
|
elif isosx:
|
||||||
|
names = ["libalfcrypto.dylib"]
|
||||||
|
else:
|
||||||
|
names = ["libalfcrypto32.so","libalfcrypto64.so","kindlekey.py","adobekey.py","subasyncio.py"]
|
||||||
|
lib_dict = self.load_resources(names)
|
||||||
|
print("{0} v{1}: Copying needed library files from plugin's zip".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||||
|
|
||||||
|
for entry, data in lib_dict.items():
|
||||||
|
file_path = os.path.join(self.alfdir, entry)
|
||||||
|
try:
|
||||||
|
os.remove(file_path)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
open(file_path,'wb').write(data)
|
||||||
|
except:
|
||||||
|
print("{0} v{1}: Exception when copying needed library files".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||||
|
traceback.print_exc()
|
||||||
|
pass
|
||||||
|
|
||||||
|
# convert old preferences, if necessary.
|
||||||
|
from calibre_plugins.dedrm.prefs import convertprefs
|
||||||
|
convertprefs()
|
||||||
|
|
||||||
|
# mark that this version has been initialized
|
||||||
|
os.mkdir(self.verdir)
|
||||||
|
except Exception as e:
|
||||||
|
traceback.print_exc()
|
||||||
|
raise
|
||||||
|
|
||||||
|
def postProcessEPUB(self, path_to_ebook):
|
||||||
|
# This is called after the DRM is removed (or if no DRM was present)
|
||||||
|
# It does stuff like de-obfuscating fonts (by calling checkFonts)
|
||||||
|
# or removing watermarks.
|
||||||
|
|
||||||
|
postProcessStart = time.time()
|
||||||
|
|
||||||
|
try:
|
||||||
|
import calibre_plugins.dedrm.prefs as prefs
|
||||||
|
dedrmprefs = prefs.DeDRM_Prefs()
|
||||||
|
|
||||||
|
if dedrmprefs["deobfuscate_fonts"] is True:
|
||||||
|
# Deobfuscate fonts
|
||||||
|
path_to_ebook = self.checkFonts(path_to_ebook) or path_to_ebook
|
||||||
|
|
||||||
|
if dedrmprefs["remove_watermarks"] is True:
|
||||||
|
import calibre_plugins.dedrm.epubwatermark as watermark
|
||||||
|
|
||||||
|
# Remove Tolino's CDP watermark file
|
||||||
|
path_to_ebook = watermark.removeCDPwatermark(self, path_to_ebook) or path_to_ebook
|
||||||
|
|
||||||
|
# Remove watermarks (Amazon or LemonInk) from the OPF file
|
||||||
|
path_to_ebook = watermark.removeOPFwatermarks(self, path_to_ebook) or path_to_ebook
|
||||||
|
|
||||||
|
# Remove watermarks (Adobe or LemonInk) from all HTML and XHTML files
|
||||||
|
path_to_ebook = watermark.removeHTMLwatermarks(self, path_to_ebook) or path_to_ebook
|
||||||
|
|
||||||
|
postProcessEnd = time.time()
|
||||||
|
print("{0} v{1}: Post-processing took {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, postProcessEnd-postProcessStart))
|
||||||
|
|
||||||
|
|
||||||
|
return path_to_ebook
|
||||||
|
|
||||||
|
except:
|
||||||
|
print("Error while checking settings")
|
||||||
|
return path_to_ebook
|
||||||
|
|
||||||
|
def checkFonts(self, path_to_ebook):
|
||||||
|
# This is called after the normal DRM removal is done.
|
||||||
|
# It checks if there's fonts that need to be deobfuscated
|
||||||
|
|
||||||
|
try:
|
||||||
|
import calibre_plugins.dedrm.epubfontdecrypt as epubfontdecrypt
|
||||||
|
|
||||||
|
output = self.temporary_file(".epub").name
|
||||||
|
ret = epubfontdecrypt.decryptFontsBook(path_to_ebook, output)
|
||||||
|
|
||||||
|
if (ret == 0):
|
||||||
|
return output
|
||||||
|
elif (ret == 1):
|
||||||
|
return path_to_ebook
|
||||||
|
else:
|
||||||
|
print("{0} v{1}: Error during font deobfuscation".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||||
|
raise DeDRMError("Font deobfuscation failed")
|
||||||
|
|
||||||
|
except:
|
||||||
|
print("{0} v{1}: Error during font deobfuscation".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||||
|
traceback.print_exc()
|
||||||
|
return path_to_ebook
|
||||||
|
|
||||||
|
def ePubDecrypt(self,path_to_ebook):
|
||||||
|
# Create a TemporaryPersistent file to work with.
|
||||||
|
# Check original epub archive for zip errors.
|
||||||
|
import calibre_plugins.dedrm.zipfix as zipfix
|
||||||
|
|
||||||
|
inf = self.temporary_file(".epub")
|
||||||
|
try:
|
||||||
|
print("{0} v{1}: Verifying zip archive integrity".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||||
|
fr = zipfix.fixZip(path_to_ebook, inf.name)
|
||||||
|
fr.fix()
|
||||||
|
except Exception as e:
|
||||||
|
print("{0} v{1}: Error \'{2}\' when checking zip archive".format(PLUGIN_NAME, PLUGIN_VERSION, e.args[0]))
|
||||||
|
raise
|
||||||
|
|
||||||
|
# import the decryption keys
|
||||||
|
import calibre_plugins.dedrm.prefs as prefs
|
||||||
|
dedrmprefs = prefs.DeDRM_Prefs()
|
||||||
|
|
||||||
|
|
||||||
|
# import the LCP handler
|
||||||
|
import calibre_plugins.dedrm.lcpdedrm as lcpdedrm
|
||||||
|
|
||||||
|
if (lcpdedrm.isLCPbook(path_to_ebook)):
|
||||||
|
try:
|
||||||
|
retval = lcpdedrm.decryptLCPbook(path_to_ebook, dedrmprefs['lcp_passphrases'], self)
|
||||||
|
except:
|
||||||
|
print("Looks like that didn't work:")
|
||||||
|
raise
|
||||||
|
|
||||||
|
return self.postProcessEPUB(retval)
|
||||||
|
|
||||||
|
|
||||||
|
# Not an LCP book, do the normal EPUB (Adobe) handling.
|
||||||
|
|
||||||
|
# import the Barnes & Noble ePub handler
|
||||||
|
import calibre_plugins.dedrm.ignobleepub as ignobleepub
|
||||||
|
|
||||||
|
|
||||||
|
#check the book
|
||||||
|
if ignobleepub.ignobleBook(inf.name):
|
||||||
|
print("{0} v{1}: “{2}” is a secure Barnes & Noble ePub".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook)))
|
||||||
|
|
||||||
|
# Attempt to decrypt epub with each encryption key (generated or provided).
|
||||||
|
for keyname, userkey in dedrmprefs['bandnkeys'].items():
|
||||||
|
keyname_masked = "".join(("X" if (x.isdigit()) else x) for x in keyname)
|
||||||
|
print("{0} v{1}: Trying Encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname_masked))
|
||||||
|
of = self.temporary_file(".epub")
|
||||||
|
|
||||||
|
# Give the user key, ebook and TemporaryPersistent file to the decryption function.
|
||||||
|
try:
|
||||||
|
result = ignobleepub.decryptBook(userkey, inf.name, of.name)
|
||||||
|
except:
|
||||||
|
print("{0} v{1}: Exception when trying to decrypt after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||||
|
traceback.print_exc()
|
||||||
|
result = 1
|
||||||
|
|
||||||
|
of.close()
|
||||||
|
|
||||||
|
if result == 0:
|
||||||
|
# Decryption was successful.
|
||||||
|
# Return the modified PersistentTemporary file to calibre.
|
||||||
|
return self.postProcessEPUB(of.name)
|
||||||
|
|
||||||
|
print("{0} v{1}: Failed to decrypt with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname_masked,time.time()-self.starttime))
|
||||||
|
|
||||||
|
# perhaps we should see if we can get a key from a log file
|
||||||
|
print("{0} v{1}: Looking for new NOOK Study Keys after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||||
|
|
||||||
|
# get the default NOOK Study keys
|
||||||
|
defaultkeys = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
if iswindows or isosx:
|
||||||
|
from calibre_plugins.dedrm.ignoblekey import nookkeys
|
||||||
|
|
||||||
|
defaultkeys = nookkeys()
|
||||||
|
else: # linux
|
||||||
|
from .wineutils import WineGetKeys
|
||||||
|
|
||||||
|
scriptpath = os.path.join(self.alfdir,"ignoblekey.py")
|
||||||
|
defaultkeys = WineGetKeys(scriptpath, ".b64",dedrmprefs['adobewineprefix'])
|
||||||
|
|
||||||
|
except:
|
||||||
|
print("{0} v{1}: Exception when getting default NOOK Study Key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
newkeys = []
|
||||||
|
for keyvalue in defaultkeys:
|
||||||
|
if keyvalue not in dedrmprefs['bandnkeys'].values():
|
||||||
|
newkeys.append(keyvalue)
|
||||||
|
|
||||||
|
if len(newkeys) > 0:
|
||||||
|
try:
|
||||||
|
for i,userkey in enumerate(newkeys):
|
||||||
|
print("{0} v{1}: Trying a new default key".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||||
|
|
||||||
|
of = self.temporary_file(".epub")
|
||||||
|
|
||||||
|
# Give the user key, ebook and TemporaryPersistent file to the decryption function.
|
||||||
|
try:
|
||||||
|
result = ignobleepub.decryptBook(userkey, inf.name, of.name)
|
||||||
|
except:
|
||||||
|
print("{0} v{1}: Exception when trying to decrypt after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||||
|
traceback.print_exc()
|
||||||
|
result = 1
|
||||||
|
|
||||||
|
of.close()
|
||||||
|
|
||||||
|
if result == 0:
|
||||||
|
# Decryption was a success
|
||||||
|
# Store the new successful key in the defaults
|
||||||
|
print("{0} v{1}: Saving a new default key".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||||
|
try:
|
||||||
|
dedrmprefs.addnamedvaluetoprefs('bandnkeys','nook_Study_key',keyvalue)
|
||||||
|
dedrmprefs.writeprefs()
|
||||||
|
print("{0} v{1}: Saved a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
|
||||||
|
except:
|
||||||
|
print("{0} v{1}: Exception saving a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||||
|
traceback.print_exc()
|
||||||
|
# Return the modified PersistentTemporary file to calibre.
|
||||||
|
return self.postProcessEPUB(of.name)
|
||||||
|
|
||||||
|
print("{0} v{1}: Failed to decrypt with new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
|
||||||
|
except Exception as e:
|
||||||
|
pass
|
||||||
|
|
||||||
|
print("{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
|
||||||
|
raise DeDRMError("{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||||
|
|
||||||
|
# import the Adobe Adept ePub handler
|
||||||
|
import calibre_plugins.dedrm.ineptepub as ineptepub
|
||||||
|
|
||||||
|
if ineptepub.adeptBook(inf.name):
|
||||||
|
book_uuid = None
|
||||||
|
try:
|
||||||
|
# This tries to figure out which Adobe account UUID the book is licensed for.
|
||||||
|
# If we know that we can directly use the correct key instead of having to
|
||||||
|
# try them all.
|
||||||
|
book_uuid = ineptepub.adeptGetUserUUID(inf.name)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if book_uuid is None:
|
||||||
|
print("{0} v{1}: {2} is a secure Adobe Adept ePub".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook)))
|
||||||
|
else:
|
||||||
|
print("{0} v{1}: {2} is a secure Adobe Adept ePub for UUID {3}".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook), book_uuid))
|
||||||
|
|
||||||
|
|
||||||
|
if book_uuid is not None:
|
||||||
|
# Check if we have a key with that UUID in its name:
|
||||||
|
for keyname, userkeyhex in dedrmprefs['adeptkeys'].items():
|
||||||
|
if not book_uuid.lower() in keyname.lower():
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Found matching key
|
||||||
|
userkey = codecs.decode(userkeyhex, 'hex')
|
||||||
|
print("{0} v{1}: Trying UUID-matched encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname))
|
||||||
|
of = self.temporary_file(".epub")
|
||||||
|
try:
|
||||||
|
result = ineptepub.decryptBook(userkey, inf.name, of.name)
|
||||||
|
of.close()
|
||||||
|
if result == 0:
|
||||||
|
print("{0} v{1}: Decrypted with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname,time.time()-self.starttime))
|
||||||
|
return self.postProcessEPUB(of.name)
|
||||||
|
except ineptepub.ADEPTNewVersionError:
|
||||||
|
print("{0} v{1}: Book uses unsupported (too new) Adobe DRM.".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||||
|
return self.postProcessEPUB(path_to_ebook)
|
||||||
|
|
||||||
|
except:
|
||||||
|
print("{0} v{1}: Exception when decrypting after {2:.1f} seconds - trying other keys".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
|
||||||
|
# Attempt to decrypt epub with each encryption key (generated or provided).
|
||||||
|
for keyname, userkeyhex in dedrmprefs['adeptkeys'].items():
|
||||||
|
userkey = codecs.decode(userkeyhex, 'hex')
|
||||||
|
print("{0} v{1}: Trying Encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname))
|
||||||
|
of = self.temporary_file(".epub")
|
||||||
|
|
||||||
|
# Give the user key, ebook and TemporaryPersistent file to the decryption function.
|
||||||
|
try:
|
||||||
|
result = ineptepub.decryptBook(userkey, inf.name, of.name)
|
||||||
|
except ineptepub.ADEPTNewVersionError:
|
||||||
|
print("{0} v{1}: Book uses unsupported (too new) Adobe DRM.".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||||
|
return self.postProcessEPUB(path_to_ebook)
|
||||||
|
except:
|
||||||
|
print("{0} v{1}: Exception when decrypting after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||||
|
traceback.print_exc()
|
||||||
|
result = 1
|
||||||
|
|
||||||
|
try:
|
||||||
|
of.close()
|
||||||
|
except:
|
||||||
|
print("{0} v{1}: Exception closing temporary file after {2:.1f} seconds. Ignored.".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||||
|
|
||||||
|
if result == 0:
|
||||||
|
# Decryption was successful.
|
||||||
|
# Return the modified PersistentTemporary file to calibre.
|
||||||
|
print("{0} v{1}: Decrypted with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname,time.time()-self.starttime))
|
||||||
|
return self.postProcessEPUB(of.name)
|
||||||
|
|
||||||
|
print("{0} v{1}: Failed to decrypt with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname,time.time()-self.starttime))
|
||||||
|
|
||||||
|
# perhaps we need to get a new default ADE key
|
||||||
|
print("{0} v{1}: Looking for new default Adobe Digital Editions Keys after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||||
|
|
||||||
|
# get the default Adobe keys
|
||||||
|
defaultkeys = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
if iswindows or isosx:
|
||||||
|
from calibre_plugins.dedrm.adobekey import adeptkeys
|
||||||
|
|
||||||
|
defaultkeys, defaultnames = adeptkeys()
|
||||||
|
else: # linux
|
||||||
|
from .wineutils import WineGetKeys
|
||||||
|
|
||||||
|
scriptpath = os.path.join(self.alfdir,"adobekey.py")
|
||||||
|
defaultkeys, defaultnames = WineGetKeys(scriptpath, ".der",dedrmprefs['adobewineprefix'])
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.default_key = defaultkeys[0]
|
||||||
|
except:
|
||||||
|
print("{0} v{1}: No ADE key found".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||||
|
except:
|
||||||
|
print("{0} v{1}: Exception when getting default Adobe Key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||||
|
traceback.print_exc()
|
||||||
|
self.default_key = ""
|
||||||
|
|
||||||
|
newkeys = []
|
||||||
|
newnames = []
|
||||||
|
idx = 0
|
||||||
|
for keyvalue in defaultkeys:
|
||||||
|
if codecs.encode(keyvalue, 'hex').decode('ascii') not in dedrmprefs['adeptkeys'].values():
|
||||||
|
newkeys.append(keyvalue)
|
||||||
|
newnames.append("default_ade_key_uuid_" + defaultnames[idx])
|
||||||
|
idx += 1
|
||||||
|
|
||||||
|
# Check for DeACSM keys:
|
||||||
|
try:
|
||||||
|
from calibre_plugins.dedrm.config import checkForDeACSMkeys
|
||||||
|
|
||||||
|
newkey, newname = checkForDeACSMkeys()
|
||||||
|
|
||||||
|
if newkey is not None:
|
||||||
|
if codecs.encode(newkey, 'hex').decode('ascii') not in dedrmprefs['adeptkeys'].values():
|
||||||
|
print("{0} v{1}: Found new key '{2}' in DeACSM plugin".format(PLUGIN_NAME, PLUGIN_VERSION, newname))
|
||||||
|
newkeys.append(newkey)
|
||||||
|
newnames.append(newname)
|
||||||
|
except:
|
||||||
|
traceback.print_exc()
|
||||||
|
pass
|
||||||
|
|
||||||
|
if len(newkeys) > 0:
|
||||||
|
try:
|
||||||
|
for i,userkey in enumerate(newkeys):
|
||||||
|
print("{0} v{1}: Trying a new default key".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||||
|
of = self.temporary_file(".epub")
|
||||||
|
|
||||||
|
# Give the user key, ebook and TemporaryPersistent file to the decryption function.
|
||||||
|
try:
|
||||||
|
result = ineptepub.decryptBook(userkey, inf.name, of.name)
|
||||||
|
except:
|
||||||
|
print("{0} v{1}: Exception when decrypting after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||||
|
traceback.print_exc()
|
||||||
|
result = 1
|
||||||
|
|
||||||
|
of.close()
|
||||||
|
|
||||||
|
if result == 0:
|
||||||
|
# Decryption was a success
|
||||||
|
# Store the new successful key in the defaults
|
||||||
|
print("{0} v{1}: Saving a new default key".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||||
|
try:
|
||||||
|
dedrmprefs.addnamedvaluetoprefs('adeptkeys', newnames[i], codecs.encode(userkey, 'hex').decode('ascii'))
|
||||||
|
dedrmprefs.writeprefs()
|
||||||
|
print("{0} v{1}: Saved a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
|
||||||
|
except:
|
||||||
|
print("{0} v{1}: Exception when saving a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||||
|
traceback.print_exc()
|
||||||
|
print("{0} v{1}: Decrypted with new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
|
||||||
|
# Return the modified PersistentTemporary file to calibre.
|
||||||
|
return self.postProcessEPUB(of.name)
|
||||||
|
|
||||||
|
print("{0} v{1}: Failed to decrypt with new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
|
||||||
|
except Exception as e:
|
||||||
|
print("{0} v{1}: Unexpected Exception trying a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||||
|
traceback.print_exc()
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Something went wrong with decryption.
|
||||||
|
print("{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at 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
|
||||||
|
# Probably a DRM-free EPUB, but we should still check for fonts.
|
||||||
|
print("{0} v{1}: “{2}” is neither an Adobe Adept nor a Barnes & Noble encrypted ePub".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook)))
|
||||||
|
return self.postProcessEPUB(inf.name)
|
||||||
|
#raise DeDRMError("{0} v{1}: Couldn't decrypt after {2:.1f} seconds. DRM free perhaps?".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
|
||||||
|
|
||||||
|
def PDFDecrypt(self,path_to_ebook):
|
||||||
|
import calibre_plugins.dedrm.prefs as prefs
|
||||||
|
import calibre_plugins.dedrm.ineptpdf as ineptpdf
|
||||||
|
import calibre_plugins.dedrm.lcpdedrm as lcpdedrm
|
||||||
|
dedrmprefs = prefs.DeDRM_Prefs()
|
||||||
|
|
||||||
|
if (lcpdedrm.isLCPbook(path_to_ebook)):
|
||||||
|
try:
|
||||||
|
retval = lcpdedrm.decryptLCPbook(path_to_ebook, dedrmprefs['lcp_passphrases'], self)
|
||||||
|
except:
|
||||||
|
print("Looks like that didn't work:")
|
||||||
|
raise
|
||||||
|
|
||||||
|
return retval
|
||||||
|
|
||||||
|
|
||||||
|
# Not an LCP book, do the normal Adobe handling.
|
||||||
|
|
||||||
|
book_uuid = None
|
||||||
|
try:
|
||||||
|
# Try to figure out which Adobe account this book is licensed for.
|
||||||
|
book_uuid = ineptpdf.adeptGetUserUUID(path_to_ebook)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if book_uuid is None:
|
||||||
|
print("{0} v{1}: {2} is a PDF ebook".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook)))
|
||||||
|
else:
|
||||||
|
print("{0} v{1}: {2} is a PDF ebook for UUID {3}".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook), book_uuid))
|
||||||
|
|
||||||
|
if book_uuid is not None:
|
||||||
|
# Check if we have a key for that UUID
|
||||||
|
for keyname, userkeyhex in dedrmprefs['adeptkeys'].items():
|
||||||
|
if not book_uuid.lower() in keyname.lower():
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Found matching key
|
||||||
|
userkey = codecs.decode(userkeyhex, 'hex')
|
||||||
|
print("{0} v{1}: Trying UUID-matched encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname))
|
||||||
|
of = self.temporary_file(".pdf")
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = ineptpdf.decryptBook(userkey, path_to_ebook, of.name)
|
||||||
|
of.close()
|
||||||
|
if result == 0:
|
||||||
|
print("{0} v{1}: Decrypted with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname,time.time()-self.starttime))
|
||||||
|
return of.name
|
||||||
|
|
||||||
|
except ineptpdf.ADEPTNewVersionError:
|
||||||
|
print("{0} v{1}: Book uses unsupported (too new) Adobe DRM.".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||||
|
return path_to_ebook
|
||||||
|
except:
|
||||||
|
print("{0} v{1}: Exception when decrypting after {2:.1f} seconds - trying other keys".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
|
||||||
|
# If we end up here, we didn't find a key with a matching UUID, so lets just try all of them.
|
||||||
|
|
||||||
|
# Attempt to decrypt PDF with each encryption key (generated or provided).
|
||||||
|
for keyname, userkeyhex in dedrmprefs['adeptkeys'].items():
|
||||||
|
userkey = codecs.decode(userkeyhex,'hex')
|
||||||
|
print("{0} v{1}: Trying encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname))
|
||||||
|
of = self.temporary_file(".pdf")
|
||||||
|
|
||||||
|
# Give the user key, ebook and TemporaryPersistent file to the decryption function.
|
||||||
|
try:
|
||||||
|
result = ineptpdf.decryptBook(userkey, path_to_ebook, of.name)
|
||||||
|
except ineptpdf.ADEPTNewVersionError:
|
||||||
|
print("{0} v{1}: Book uses unsupported (too new) Adobe DRM.".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||||
|
return path_to_ebook
|
||||||
|
except:
|
||||||
|
print("{0} v{1}: Exception when decrypting after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||||
|
traceback.print_exc()
|
||||||
|
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))
|
||||||
|
|
||||||
|
# perhaps we need to get a new default ADE key
|
||||||
|
print("{0} v{1}: Looking for new default Adobe Digital Editions Keys after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||||
|
|
||||||
|
# get the default Adobe keys
|
||||||
|
defaultkeys = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
if iswindows or isosx:
|
||||||
|
from calibre_plugins.dedrm.adobekey import adeptkeys
|
||||||
|
|
||||||
|
defaultkeys, defaultnames = adeptkeys()
|
||||||
|
else: # linux
|
||||||
|
from .wineutils import WineGetKeys
|
||||||
|
|
||||||
|
scriptpath = os.path.join(self.alfdir,"adobekey.py")
|
||||||
|
defaultkeys, defaultnames = WineGetKeys(scriptpath, ".der",dedrmprefs['adobewineprefix'])
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.default_key = defaultkeys[0]
|
||||||
|
except:
|
||||||
|
print("{0} v{1}: No ADE key found".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||||
|
except:
|
||||||
|
print("{0} v{1}: Exception when getting default Adobe Key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||||
|
traceback.print_exc()
|
||||||
|
self.default_key = ""
|
||||||
|
|
||||||
|
newkeys = []
|
||||||
|
newnames = []
|
||||||
|
idx = 0
|
||||||
|
for keyvalue in defaultkeys:
|
||||||
|
if codecs.encode(keyvalue,'hex') not in dedrmprefs['adeptkeys'].values():
|
||||||
|
newkeys.append(keyvalue)
|
||||||
|
newnames.append("default_ade_key_uuid_" + defaultnames[idx])
|
||||||
|
idx += 1
|
||||||
|
|
||||||
|
# Check for DeACSM keys:
|
||||||
|
try:
|
||||||
|
from calibre_plugins.dedrm.config import checkForDeACSMkeys
|
||||||
|
|
||||||
|
newkey, newname = checkForDeACSMkeys()
|
||||||
|
|
||||||
|
if newkey is not None:
|
||||||
|
if codecs.encode(newkey, 'hex').decode('ascii') not in dedrmprefs['adeptkeys'].values():
|
||||||
|
print("{0} v{1}: Found new key '{2}' in DeACSM plugin".format(PLUGIN_NAME, PLUGIN_VERSION, newname))
|
||||||
|
newkeys.append(keyvalue)
|
||||||
|
newnames.append(newname)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if len(newkeys) > 0:
|
||||||
|
try:
|
||||||
|
for i,userkey in enumerate(newkeys):
|
||||||
|
print("{0} v{1}: Trying a new default key".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||||
|
of = self.temporary_file(".pdf")
|
||||||
|
|
||||||
|
# Give the user key, ebook and TemporaryPersistent file to the decryption function.
|
||||||
|
try:
|
||||||
|
result = ineptpdf.decryptBook(userkey, path_to_ebook, of.name)
|
||||||
|
except:
|
||||||
|
print("{0} v{1}: Exception when decrypting after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||||
|
traceback.print_exc()
|
||||||
|
result = 1
|
||||||
|
|
||||||
|
of.close()
|
||||||
|
|
||||||
|
if result == 0:
|
||||||
|
# Decryption was a success
|
||||||
|
# Store the new successful key in the defaults
|
||||||
|
print("{0} v{1}: Saving a new default key".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||||
|
try:
|
||||||
|
dedrmprefs.addnamedvaluetoprefs('adeptkeys', newnames[i], codecs.encode(userkey,'hex').decode('ascii'))
|
||||||
|
dedrmprefs.writeprefs()
|
||||||
|
print("{0} v{1}: Saved a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
|
||||||
|
except:
|
||||||
|
print("{0} v{1}: Exception when saving a new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||||
|
traceback.print_exc()
|
||||||
|
# Return the modified PersistentTemporary file to calibre.
|
||||||
|
return of.name
|
||||||
|
|
||||||
|
print("{0} v{1}: Failed to decrypt with new default key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
|
||||||
|
except Exception as e:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# Unable to decrypt the PDF with any of the existing keys. Is it a B&N PDF?
|
||||||
|
# Attempt to decrypt PDF with each encryption key (generated or provided).
|
||||||
|
for keyname, userkey in dedrmprefs['bandnkeys'].items():
|
||||||
|
keyname_masked = "".join(("X" if (x.isdigit()) else x) for x in keyname)
|
||||||
|
print("{0} v{1}: Trying Encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname_masked))
|
||||||
|
of = self.temporary_file(".pdf")
|
||||||
|
|
||||||
|
# Give the user key, ebook and TemporaryPersistent file to the decryption function.
|
||||||
|
try:
|
||||||
|
result = ineptpdf.decryptBook(userkey, path_to_ebook, of.name, False)
|
||||||
|
except ineptpdf.ADEPTNewVersionError:
|
||||||
|
print("{0} v{1}: Book uses unsupported (too new) Adobe DRM.".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||||
|
return path_to_ebook
|
||||||
|
except:
|
||||||
|
print("{0} v{1}: Exception when decrypting after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||||
|
traceback.print_exc()
|
||||||
|
result = 1
|
||||||
|
|
||||||
|
of.close()
|
||||||
|
|
||||||
|
if result == 0:
|
||||||
|
# Decryption was successful.
|
||||||
|
# Return the modified PersistentTemporary file to calibre.
|
||||||
|
print("{0} v{1}: Decrypted with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname,time.time()-self.starttime))
|
||||||
|
return of.name
|
||||||
|
|
||||||
|
print("{0} v{1}: Failed to decrypt with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname,time.time()-self.starttime))
|
||||||
|
|
||||||
|
|
||||||
|
# Something went wrong with decryption.
|
||||||
|
print("{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
|
||||||
|
raise DeDRMError("{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||||
|
|
||||||
|
|
||||||
|
def KindleMobiDecrypt(self,path_to_ebook):
|
||||||
|
|
||||||
|
# add the alfcrypto directory to sys.path so alfcrypto.py
|
||||||
|
# will be able to locate the custom lib(s) for CDLL import.
|
||||||
|
sys.path.insert(0, self.alfdir)
|
||||||
|
# Had to move this import here so the custom libs can be
|
||||||
|
# extracted to the appropriate places beforehand these routines
|
||||||
|
# look for them.
|
||||||
|
import calibre_plugins.dedrm.prefs as prefs
|
||||||
|
import calibre_plugins.dedrm.k4mobidedrm
|
||||||
|
|
||||||
|
dedrmprefs = prefs.DeDRM_Prefs()
|
||||||
|
pids = dedrmprefs['pids']
|
||||||
|
serials = dedrmprefs['serials']
|
||||||
|
for android_serials_list in dedrmprefs['androidkeys'].values():
|
||||||
|
#print android_serials_list
|
||||||
|
serials.extend(android_serials_list)
|
||||||
|
#print serials
|
||||||
|
androidFiles = []
|
||||||
|
kindleDatabases = list(dedrmprefs['kindlekeys'].items())
|
||||||
|
|
||||||
|
try:
|
||||||
|
book = k4mobidedrm.GetDecryptedBook(path_to_ebook,kindleDatabases,androidFiles,serials,pids,self.starttime)
|
||||||
|
except Exception as e:
|
||||||
|
decoded = False
|
||||||
|
# perhaps we need to get a new default Kindle for Mac/PC key
|
||||||
|
defaultkeys = []
|
||||||
|
print("{0} v{1}: Failed to decrypt with error: {2}".format(PLUGIN_NAME, PLUGIN_VERSION,e.args[0]))
|
||||||
|
print("{0} v{1}: Looking for new default Kindle Key after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION, time.time()-self.starttime))
|
||||||
|
|
||||||
|
try:
|
||||||
|
if iswindows or isosx:
|
||||||
|
from calibre_plugins.dedrm.kindlekey import kindlekeys
|
||||||
|
|
||||||
|
defaultkeys = kindlekeys()
|
||||||
|
else: # linux
|
||||||
|
from .wineutils import WineGetKeys
|
||||||
|
|
||||||
|
scriptpath = os.path.join(self.alfdir,"kindlekey.py")
|
||||||
|
defaultkeys = WineGetKeys(scriptpath, ".k4i",dedrmprefs['kindlewineprefix'])
|
||||||
|
except:
|
||||||
|
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()
|
||||||
|
pass
|
||||||
|
|
||||||
|
newkeys = {}
|
||||||
|
for i,keyvalue in enumerate(defaultkeys):
|
||||||
|
keyname = "default_key_{0:d}".format(i+1)
|
||||||
|
if keyvalue not in dedrmprefs['kindlekeys'].values():
|
||||||
|
newkeys[keyname] = keyvalue
|
||||||
|
if len(newkeys) > 0:
|
||||||
|
print("{0} v{1}: Found {2} new {3}".format(PLUGIN_NAME, PLUGIN_VERSION, len(newkeys), "key" if len(newkeys)==1 else "keys"))
|
||||||
|
try:
|
||||||
|
book = k4mobidedrm.GetDecryptedBook(path_to_ebook,list(newkeys.items()),[],[],[],self.starttime)
|
||||||
|
decoded = True
|
||||||
|
# store the new successful keys in the defaults
|
||||||
|
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():
|
||||||
|
dedrmprefs.addnamedvaluetoprefs('kindlekeys','default_key',keyvalue)
|
||||||
|
dedrmprefs.writeprefs()
|
||||||
|
except Exception as e:
|
||||||
|
pass
|
||||||
|
if not decoded:
|
||||||
|
#if you reached here then no luck raise and exception
|
||||||
|
print("{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
|
||||||
|
raise DeDRMError("{0} v{1}: Ultimately failed to decrypt after {2:.1f} seconds. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
|
||||||
|
|
||||||
|
of = self.temporary_file(book.getBookExtension())
|
||||||
|
book.getFile(of.name)
|
||||||
|
of.close()
|
||||||
|
book.cleanup()
|
||||||
|
return of.name
|
||||||
|
|
||||||
|
|
||||||
|
def eReaderDecrypt(self,path_to_ebook):
|
||||||
|
|
||||||
|
import calibre_plugins.dedrm.prefs as prefs
|
||||||
|
import calibre_plugins.dedrm.erdr2pml
|
||||||
|
|
||||||
|
dedrmprefs = prefs.DeDRM_Prefs()
|
||||||
|
# Attempt to decrypt epub with each encryption key (generated or provided).
|
||||||
|
for keyname, userkey in dedrmprefs['ereaderkeys'].items():
|
||||||
|
keyname_masked = "".join(("X" if (x.isdigit()) else x) for x in keyname)
|
||||||
|
print("{0} v{1}: Trying Encryption key {2:s}".format(PLUGIN_NAME, PLUGIN_VERSION, keyname_masked))
|
||||||
|
of = self.temporary_file(".pmlz")
|
||||||
|
|
||||||
|
# Give the userkey, ebook and TemporaryPersistent file to the decryption function.
|
||||||
|
result = erdr2pml.decryptBook(path_to_ebook, of.name, True, codecs.decode(userkey,'hex'))
|
||||||
|
|
||||||
|
of.close()
|
||||||
|
|
||||||
|
# Decryption was successful return the modified PersistentTemporary
|
||||||
|
# file to Calibre's import process.
|
||||||
|
if result == 0:
|
||||||
|
print("{0} v{1}: Successfully decrypted with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname_masked,time.time()-self.starttime))
|
||||||
|
return of.name
|
||||||
|
|
||||||
|
print("{0} v{1}: Failed to decrypt with key {2:s} after {3:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,keyname_masked,time.time()-self.starttime))
|
||||||
|
|
||||||
|
print("{0} v{1}: 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):
|
||||||
|
|
||||||
|
# make sure any unicode output gets converted safely with 'replace'
|
||||||
|
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||||
|
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||||
|
|
||||||
|
print("{0} v{1}: Trying to decrypt {2}".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook)))
|
||||||
|
self.starttime = time.time()
|
||||||
|
|
||||||
|
booktype = os.path.splitext(path_to_ebook)[1].lower()[1:]
|
||||||
|
if booktype in ['prc','mobi','pobi','azw','azw1','azw3','azw4','tpz','kfx-zip']:
|
||||||
|
# Kindle/Mobipocket
|
||||||
|
decrypted_ebook = self.KindleMobiDecrypt(path_to_ebook)
|
||||||
|
elif booktype == 'pdb':
|
||||||
|
# eReader
|
||||||
|
decrypted_ebook = self.eReaderDecrypt(path_to_ebook)
|
||||||
|
pass
|
||||||
|
elif booktype == 'pdf':
|
||||||
|
# Adobe Adept PDF (hopefully)
|
||||||
|
decrypted_ebook = self.PDFDecrypt(path_to_ebook)
|
||||||
|
pass
|
||||||
|
elif booktype == 'epub':
|
||||||
|
# Adobe Adept or B&N ePub
|
||||||
|
decrypted_ebook = self.ePubDecrypt(path_to_ebook)
|
||||||
|
else:
|
||||||
|
print("Unknown booktype {0}. Passing back to calibre unchanged".format(booktype))
|
||||||
|
return path_to_ebook
|
||||||
|
print("{0} v{1}: Finished after {2:.1f} seconds".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
|
||||||
|
return decrypted_ebook
|
||||||
|
|
||||||
|
def is_customizable(self):
|
||||||
|
# return true to allow customization via the Plugin->Preferences.
|
||||||
|
return True
|
||||||
|
|
||||||
|
def config_widget(self):
|
||||||
|
import calibre_plugins.dedrm.config as config
|
||||||
|
return config.ConfigWidget(self.plugin_path, self.alfdir)
|
||||||
|
|
||||||
|
def save_settings(self, config_widget):
|
||||||
|
config_widget.save_settings()
|
||||||
@@ -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)
|
||||||
@@ -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 7.1
|
||||||
|
# Copyright © 2009-2021 i♥cabbages, Apprentice Harper et al.
|
||||||
# adobekey.pyw, version 6.0
|
|
||||||
# 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 2010–2016 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,15 +28,19 @@ 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
|
||||||
|
# 7.1 - Fix "failed to decrypt user key key" error (read username from registry)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Retrieve Adobe ADEPT user key.
|
Retrieve Adobe ADEPT user key.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__version__ = '6.0'
|
__version__ = '7.1'
|
||||||
|
|
||||||
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
|
||||||
@@ -68,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)
|
||||||
|
|
||||||
@@ -109,15 +100,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
|
||||||
@@ -129,11 +118,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)
|
||||||
@@ -167,7 +161,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')
|
||||||
@@ -178,7 +172,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
|
||||||
@@ -244,6 +238,20 @@ if iswindows:
|
|||||||
return GetUserName
|
return GetUserName
|
||||||
GetUserName = GetUserName()
|
GetUserName = GetUserName()
|
||||||
|
|
||||||
|
def GetUserName2():
|
||||||
|
try:
|
||||||
|
import winreg
|
||||||
|
except ImportError:
|
||||||
|
import _winreg as winreg
|
||||||
|
|
||||||
|
try:
|
||||||
|
DEVICE_KEY_PATH = r'Software\Adobe\Adept\Device'
|
||||||
|
regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, DEVICE_KEY_PATH)
|
||||||
|
userREG = winreg.QueryValueEx(regkey, 'username')[0].encode('utf-16-le')[::2]
|
||||||
|
return userREG
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
PAGE_EXECUTE_READWRITE = 0x40
|
PAGE_EXECUTE_READWRITE = 0x40
|
||||||
MEM_COMMIT = 0x1000
|
MEM_COMMIT = 0x1000
|
||||||
MEM_RESERVE = 0x2000
|
MEM_RESERVE = 0x2000
|
||||||
@@ -281,49 +289,54 @@ if iswindows:
|
|||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
if self._buf is not None:
|
if self._buf is not None:
|
||||||
VirtualFree(self._buf)
|
try:
|
||||||
self._buf = None
|
VirtualFree(self._buf)
|
||||||
|
self._buf = None
|
||||||
|
except TypeError:
|
||||||
|
# Apparently this sometimes gets cleared on application exit
|
||||||
|
# Causes a useless exception in the log, so let's just catch and ignore that.
|
||||||
|
pass
|
||||||
|
|
||||||
if struct.calcsize("P") == 4:
|
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():
|
||||||
@@ -367,7 +380,9 @@ if iswindows:
|
|||||||
serial = GetVolumeSerialNumber(root)
|
serial = GetVolumeSerialNumber(root)
|
||||||
vendor = cpuid0()
|
vendor = cpuid0()
|
||||||
signature = struct.pack('>I', cpuid1())[1:]
|
signature = struct.pack('>I', cpuid1())[1:]
|
||||||
user = GetUserName()
|
user = GetUserName2()
|
||||||
|
if user is None:
|
||||||
|
user = GetUserName()
|
||||||
entropy = struct.pack('>I12s3s13s', serial, vendor, signature, user)
|
entropy = struct.pack('>I12s3s13s', serial, vendor, signature, user)
|
||||||
cuser = winreg.HKEY_CURRENT_USER
|
cuser = winreg.HKEY_CURRENT_USER
|
||||||
try:
|
try:
|
||||||
@@ -378,11 +393,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:
|
||||||
@@ -390,25 +406,44 @@ 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 = ""
|
||||||
|
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 != 'privateLicenseKey':
|
if ktype == 'user':
|
||||||
continue
|
# Add Adobe UUID to key name
|
||||||
userkey = winreg.QueryValueEx(plkkey, 'value')[0]
|
uuid_name = uuid_name + winreg.QueryValueEx(plkkey, 'value')[0][9:] + "_"
|
||||||
userkey = userkey.decode('base64')
|
if ktype == 'username':
|
||||||
aes = AES(keykey)
|
# Add account type & email to key name, if present
|
||||||
userkey = aes.decrypt(userkey)
|
try:
|
||||||
userkey = userkey[26:-ord(userkey[-1])]
|
uuid_name = uuid_name + winreg.QueryValueEx(plkkey, 'method')[0] + "_"
|
||||||
#print "found key:",userkey.encode('hex')
|
except:
|
||||||
keys.append(userkey)
|
pass
|
||||||
|
try:
|
||||||
|
uuid_name = uuid_name + winreg.QueryValueEx(plkkey, 'value')[0] + "_"
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
if ktype == 'privateLicenseKey':
|
||||||
|
userkey = winreg.QueryValueEx(plkkey, 'value')[0]
|
||||||
|
userkey = b64decode(userkey)
|
||||||
|
aes = AES(keykey)
|
||||||
|
userkey = aes.decrypt(userkey)
|
||||||
|
userkey = userkey[26:-ord(userkey[-1:])]
|
||||||
|
# print ("found " + uuid_name + " key: " + str(userkey))
|
||||||
|
keys.append(userkey)
|
||||||
|
|
||||||
|
if uuid_name == "":
|
||||||
|
names.append("Unknown")
|
||||||
|
else:
|
||||||
|
names.append(uuid_name[:-1])
|
||||||
|
|
||||||
if len(keys) == 0:
|
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:
|
||||||
@@ -427,12 +462,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,56 +483,80 @@ elif isosx:
|
|||||||
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'))
|
||||||
userkey = tree.findtext(expr)
|
userkey = tree.findtext(expr)
|
||||||
userkey = userkey.decode('base64')
|
|
||||||
|
exprUUID = '//%s/%s' % (adept('credentials'), adept('user'))
|
||||||
|
keyName = ""
|
||||||
|
try:
|
||||||
|
keyName = tree.findtext(exprUUID)[9:] + "_"
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
exprMail = '//%s/%s' % (adept('credentials'), adept('username'))
|
||||||
|
keyName = keyName + tree.find(exprMail).attrib["method"] + "_"
|
||||||
|
keyName = keyName + tree.findtext(exprMail) + "_"
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if keyName == "":
|
||||||
|
keyName = "Unknown"
|
||||||
|
else:
|
||||||
|
keyName = keyName[:-1]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
userkey = b64decode(userkey)
|
||||||
userkey = userkey[26:]
|
userkey = userkey[26:]
|
||||||
return [userkey]
|
return [userkey], [keyName]
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
@@ -522,76 +581,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
|
||||||
@@ -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 )
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -158,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)
|
||||||
|
|
||||||
|
|
||||||
@@ -177,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
|
||||||
@@ -196,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
|
||||||
@@ -244,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)
|
||||||
|
|
||||||
|
|
||||||
@@ -268,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()
|
||||||
@@ -283,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]
|
||||||
190
dedrm_src/androidkindlekey.py → DeDRM_plugin/androidkindlekey.py
Normal file → Executable file
190
dedrm_src/androidkindlekey.py → DeDRM_plugin/androidkindlekey.py
Normal file → Executable 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,13 +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.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__version__ = '1.5'
|
__version__ = '2.0'
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
@@ -33,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
|
||||||
@@ -48,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)
|
||||||
|
|
||||||
@@ -89,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
|
||||||
@@ -117,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()
|
||||||
@@ -137,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
|
||||||
@@ -199,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 = []
|
||||||
@@ -218,17 +221,16 @@ 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()
|
||||||
pass
|
pass
|
||||||
dsns = list(set(dsns))
|
dsns = list(set(dsns))
|
||||||
@@ -239,22 +241,24 @@ 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()
|
||||||
pass
|
pass
|
||||||
tokens = list(set(tokens))
|
tokens = list(set(tokens))
|
||||||
|
|
||||||
serials = []
|
serials = []
|
||||||
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):
|
||||||
@@ -276,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:
|
||||||
@@ -312,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")
|
||||||
@@ -321,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():
|
||||||
@@ -335,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 = ""
|
||||||
@@ -373,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
|
||||||
|
|
||||||
@@ -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):
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -17,10 +17,10 @@
|
|||||||
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||||
# and/or sell copies of the Software, and to permit persons to whom the
|
# and/or sell copies of the Software, and to permit persons to whom the
|
||||||
# Software is furnished to do so, subject to the following conditions:
|
# Software is furnished to do so, subject to the following conditions:
|
||||||
#
|
#
|
||||||
# The above copyright notice and this permission notice shall be included in
|
# The above copyright notice and this permission notice shall be included in
|
||||||
# all copies or substantial portions of the Software.
|
# all copies or substantial portions of the Software.
|
||||||
#
|
#
|
||||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||||
@@ -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
|
||||||
610
dedrm_src/config.py → DeDRM_plugin/config.py
Normal file → Executable file
610
dedrm_src/config.py → DeDRM_plugin/config.py
Normal file → Executable file
File diff suppressed because it is too large
Load Diff
902
DeDRM_plugin/convert2xml.py
Normal file
902
DeDRM_plugin/convert2xml.py
Normal file
@@ -0,0 +1,902 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
|
||||||
|
|
||||||
|
# For use with Topaz Scripts Version 2.6
|
||||||
|
# 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):
|
||||||
|
self.stream = stream
|
||||||
|
self.encoding = stream.encoding
|
||||||
|
if self.encoding == None:
|
||||||
|
self.encoding = "utf-8"
|
||||||
|
def write(self, data):
|
||||||
|
if isinstance(data,str) or isinstance(data,unicode):
|
||||||
|
# str for Python3, unicode for Python2
|
||||||
|
data = data.encode(self.encoding,"replace")
|
||||||
|
try:
|
||||||
|
buffer = getattr(self.stream, 'buffer', self.stream)
|
||||||
|
# self.stream.buffer for Python3, self.stream for Python2
|
||||||
|
buffer.write(data)
|
||||||
|
buffer.flush()
|
||||||
|
except:
|
||||||
|
# We can do nothing if a write fails
|
||||||
|
raise
|
||||||
|
def __getattr__(self, attr):
|
||||||
|
return getattr(self.stream, attr)
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import csv
|
||||||
|
import os
|
||||||
|
import getopt
|
||||||
|
from struct import pack
|
||||||
|
from struct import unpack
|
||||||
|
|
||||||
|
class TpzDRMError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Get a 7 bit encoded number from string. The most
|
||||||
|
# significant byte comes first and has the high bit (8th) set
|
||||||
|
|
||||||
|
def readEncodedNumber(file):
|
||||||
|
flag = False
|
||||||
|
c = file.read(1)
|
||||||
|
if (len(c) == 0):
|
||||||
|
return None
|
||||||
|
data = ord(c)
|
||||||
|
|
||||||
|
if data == 0xFF:
|
||||||
|
flag = True
|
||||||
|
c = file.read(1)
|
||||||
|
if (len(c) == 0):
|
||||||
|
return None
|
||||||
|
data = ord(c)
|
||||||
|
|
||||||
|
if data >= 0x80:
|
||||||
|
datax = (data & 0x7F)
|
||||||
|
while data >= 0x80 :
|
||||||
|
c = file.read(1)
|
||||||
|
if (len(c) == 0):
|
||||||
|
return None
|
||||||
|
data = c[0]
|
||||||
|
datax = (datax <<7) + (data & 0x7F)
|
||||||
|
data = datax
|
||||||
|
|
||||||
|
if flag:
|
||||||
|
data = -data
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
# returns a binary string that encodes a number into 7 bits
|
||||||
|
# most significant byte first which has the high bit set
|
||||||
|
|
||||||
|
def encodeNumber(number):
|
||||||
|
result = ""
|
||||||
|
negative = False
|
||||||
|
flag = 0
|
||||||
|
|
||||||
|
if number < 0 :
|
||||||
|
number = -number + 1
|
||||||
|
negative = True
|
||||||
|
|
||||||
|
while True:
|
||||||
|
byte = number & 0x7F
|
||||||
|
number = number >> 7
|
||||||
|
byte += flag
|
||||||
|
result += chr(byte)
|
||||||
|
flag = 0x80
|
||||||
|
if number == 0 :
|
||||||
|
if (byte == 0xFF and negative == False) :
|
||||||
|
result += chr(0x80)
|
||||||
|
break
|
||||||
|
|
||||||
|
if negative:
|
||||||
|
result += chr(0xFF)
|
||||||
|
|
||||||
|
return result[::-1]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# create / read a length prefixed string from the file
|
||||||
|
|
||||||
|
def lengthPrefixString(data):
|
||||||
|
return encodeNumber(len(data))+data
|
||||||
|
|
||||||
|
def readString(file):
|
||||||
|
stringLength = readEncodedNumber(file)
|
||||||
|
if (stringLength == None):
|
||||||
|
return ""
|
||||||
|
sv = file.read(stringLength)
|
||||||
|
if (len(sv) != stringLength):
|
||||||
|
return ""
|
||||||
|
return unpack(str(stringLength)+"s",sv)[0]
|
||||||
|
|
||||||
|
|
||||||
|
# convert a binary string generated by encodeNumber (7 bit encoded number)
|
||||||
|
# to the value you would find inside the page*.dat files to be processed
|
||||||
|
|
||||||
|
def convert(i):
|
||||||
|
result = ''
|
||||||
|
val = encodeNumber(i)
|
||||||
|
for j in range(len(val)):
|
||||||
|
c = ord(val[j:j+1])
|
||||||
|
result += '%02x' % c
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# the complete string table used to store all book text content
|
||||||
|
# as well as the xml tokens and values that make sense out of it
|
||||||
|
|
||||||
|
class Dictionary(object):
|
||||||
|
def __init__(self, dictFile):
|
||||||
|
self.filename = dictFile
|
||||||
|
self.size = 0
|
||||||
|
self.fo = open(dictFile,'rb')
|
||||||
|
self.stable = []
|
||||||
|
self.size = readEncodedNumber(self.fo)
|
||||||
|
for i in range(self.size):
|
||||||
|
self.stable.append(self.escapestr(readString(self.fo)))
|
||||||
|
self.pos = 0
|
||||||
|
|
||||||
|
def escapestr(self, str):
|
||||||
|
str = str.replace('&','&')
|
||||||
|
str = str.replace('<','<')
|
||||||
|
str = str.replace('>','>')
|
||||||
|
str = str.replace('=','=')
|
||||||
|
return str
|
||||||
|
|
||||||
|
def lookup(self,val):
|
||||||
|
if ((val >= 0) and (val < self.size)) :
|
||||||
|
self.pos = val
|
||||||
|
return self.stable[self.pos]
|
||||||
|
else:
|
||||||
|
print("Error - %d outside of string table limits" % val)
|
||||||
|
raise TpzDRMError('outside of string table limits')
|
||||||
|
# sys.exit(-1)
|
||||||
|
|
||||||
|
def getSize(self):
|
||||||
|
return self.size
|
||||||
|
|
||||||
|
def getPos(self):
|
||||||
|
return self.pos
|
||||||
|
|
||||||
|
def dumpDict(self):
|
||||||
|
for i in range(self.size):
|
||||||
|
print("%d %s %s" % (i, convert(i), self.stable[i]))
|
||||||
|
return
|
||||||
|
|
||||||
|
# parses the xml snippets that are represented by each page*.dat file.
|
||||||
|
# also parses the other0.dat file - the main stylesheet
|
||||||
|
# and information used to inject the xml snippets into page*.dat files
|
||||||
|
|
||||||
|
class PageParser(object):
|
||||||
|
def __init__(self, filename, dict, debug, flat_xml):
|
||||||
|
self.fo = open(filename,'rb')
|
||||||
|
self.id = os.path.basename(filename).replace('.dat','')
|
||||||
|
self.dict = dict
|
||||||
|
self.debug = debug
|
||||||
|
self.first_unknown = True
|
||||||
|
self.flat_xml = flat_xml
|
||||||
|
self.tagpath = []
|
||||||
|
self.doc = []
|
||||||
|
self.snippetList = []
|
||||||
|
|
||||||
|
|
||||||
|
# hash table used to enable the decoding process
|
||||||
|
# This has all been developed by trial and error so it may still have omissions or
|
||||||
|
# contain errors
|
||||||
|
# Format:
|
||||||
|
# tag : (number of arguments, argument type, subtags present, special case of subtags presents when escaped)
|
||||||
|
|
||||||
|
token_tags = {
|
||||||
|
b'x' : (1, 'scalar_number', 0, 0),
|
||||||
|
b'y' : (1, 'scalar_number', 0, 0),
|
||||||
|
b'h' : (1, 'scalar_number', 0, 0),
|
||||||
|
b'w' : (1, 'scalar_number', 0, 0),
|
||||||
|
b'firstWord' : (1, 'scalar_number', 0, 0),
|
||||||
|
b'lastWord' : (1, 'scalar_number', 0, 0),
|
||||||
|
b'rootID' : (1, 'scalar_number', 0, 0),
|
||||||
|
b'stemID' : (1, 'scalar_number', 0, 0),
|
||||||
|
b'type' : (1, 'scalar_text', 0, 0),
|
||||||
|
|
||||||
|
b'info' : (0, 'number', 1, 0),
|
||||||
|
|
||||||
|
b'info.word' : (0, 'number', 1, 1),
|
||||||
|
b'info.word.ocrText' : (1, 'text', 0, 0),
|
||||||
|
b'info.word.firstGlyph' : (1, 'raw', 0, 0),
|
||||||
|
b'info.word.lastGlyph' : (1, 'raw', 0, 0),
|
||||||
|
b'info.word.bl' : (1, 'raw', 0, 0),
|
||||||
|
b'info.word.link_id' : (1, 'number', 0, 0),
|
||||||
|
|
||||||
|
b'glyph' : (0, 'number', 1, 1),
|
||||||
|
b'glyph.x' : (1, 'number', 0, 0),
|
||||||
|
b'glyph.y' : (1, 'number', 0, 0),
|
||||||
|
b'glyph.glyphID' : (1, 'number', 0, 0),
|
||||||
|
|
||||||
|
b'dehyphen' : (0, 'number', 1, 1),
|
||||||
|
b'dehyphen.rootID' : (1, 'number', 0, 0),
|
||||||
|
b'dehyphen.stemID' : (1, 'number', 0, 0),
|
||||||
|
b'dehyphen.stemPage' : (1, 'number', 0, 0),
|
||||||
|
b'dehyphen.sh' : (1, 'number', 0, 0),
|
||||||
|
|
||||||
|
b'links' : (0, 'number', 1, 1),
|
||||||
|
b'links.page' : (1, 'number', 0, 0),
|
||||||
|
b'links.rel' : (1, 'number', 0, 0),
|
||||||
|
b'links.row' : (1, 'number', 0, 0),
|
||||||
|
b'links.title' : (1, 'text', 0, 0),
|
||||||
|
b'links.href' : (1, 'text', 0, 0),
|
||||||
|
b'links.type' : (1, 'text', 0, 0),
|
||||||
|
b'links.id' : (1, 'number', 0, 0),
|
||||||
|
|
||||||
|
b'paraCont' : (0, 'number', 1, 1),
|
||||||
|
b'paraCont.rootID' : (1, 'number', 0, 0),
|
||||||
|
b'paraCont.stemID' : (1, 'number', 0, 0),
|
||||||
|
b'paraCont.stemPage' : (1, 'number', 0, 0),
|
||||||
|
|
||||||
|
b'paraStems' : (0, 'number', 1, 1),
|
||||||
|
b'paraStems.stemID' : (1, 'number', 0, 0),
|
||||||
|
|
||||||
|
b'wordStems' : (0, 'number', 1, 1),
|
||||||
|
b'wordStems.stemID' : (1, 'number', 0, 0),
|
||||||
|
|
||||||
|
b'empty' : (1, 'snippets', 1, 0),
|
||||||
|
|
||||||
|
b'page' : (1, 'snippets', 1, 0),
|
||||||
|
b'page.class' : (1, 'scalar_text', 0, 0),
|
||||||
|
b'page.pageid' : (1, 'scalar_text', 0, 0),
|
||||||
|
b'page.pagelabel' : (1, 'scalar_text', 0, 0),
|
||||||
|
b'page.type' : (1, 'scalar_text', 0, 0),
|
||||||
|
b'page.h' : (1, 'scalar_number', 0, 0),
|
||||||
|
b'page.w' : (1, 'scalar_number', 0, 0),
|
||||||
|
b'page.startID' : (1, 'scalar_number', 0, 0),
|
||||||
|
|
||||||
|
b'group' : (1, 'snippets', 1, 0),
|
||||||
|
b'group.class' : (1, 'scalar_text', 0, 0),
|
||||||
|
b'group.type' : (1, 'scalar_text', 0, 0),
|
||||||
|
b'group._tag' : (1, 'scalar_text', 0, 0),
|
||||||
|
b'group.orientation': (1, 'scalar_text', 0, 0),
|
||||||
|
|
||||||
|
b'region' : (1, 'snippets', 1, 0),
|
||||||
|
b'region.class' : (1, 'scalar_text', 0, 0),
|
||||||
|
b'region.type' : (1, 'scalar_text', 0, 0),
|
||||||
|
b'region.x' : (1, 'scalar_number', 0, 0),
|
||||||
|
b'region.y' : (1, 'scalar_number', 0, 0),
|
||||||
|
b'region.h' : (1, 'scalar_number', 0, 0),
|
||||||
|
b'region.w' : (1, 'scalar_number', 0, 0),
|
||||||
|
b'region.orientation' : (1, 'scalar_text', 0, 0),
|
||||||
|
|
||||||
|
b'empty_text_region' : (1, 'snippets', 1, 0),
|
||||||
|
|
||||||
|
b'img' : (1, 'snippets', 1, 0),
|
||||||
|
b'img.x' : (1, 'scalar_number', 0, 0),
|
||||||
|
b'img.y' : (1, 'scalar_number', 0, 0),
|
||||||
|
b'img.h' : (1, 'scalar_number', 0, 0),
|
||||||
|
b'img.w' : (1, 'scalar_number', 0, 0),
|
||||||
|
b'img.src' : (1, 'scalar_number', 0, 0),
|
||||||
|
b'img.color_src' : (1, 'scalar_number', 0, 0),
|
||||||
|
b'img.gridSize' : (1, 'scalar_number', 0, 0),
|
||||||
|
b'img.gridBottomCenter' : (1, 'scalar_number', 0, 0),
|
||||||
|
b'img.gridTopCenter' : (1, 'scalar_number', 0, 0),
|
||||||
|
b'img.gridBeginCenter' : (1, 'scalar_number', 0, 0),
|
||||||
|
b'img.gridEndCenter' : (1, 'scalar_number', 0, 0),
|
||||||
|
b'img.image_type' : (1, 'scalar_number', 0, 0),
|
||||||
|
|
||||||
|
b'paragraph' : (1, 'snippets', 1, 0),
|
||||||
|
b'paragraph.class' : (1, 'scalar_text', 0, 0),
|
||||||
|
b'paragraph.firstWord' : (1, 'scalar_number', 0, 0),
|
||||||
|
b'paragraph.lastWord' : (1, 'scalar_number', 0, 0),
|
||||||
|
b'paragraph.lastWord' : (1, 'scalar_number', 0, 0),
|
||||||
|
b'paragraph.gridSize' : (1, 'scalar_number', 0, 0),
|
||||||
|
b'paragraph.gridBottomCenter' : (1, 'scalar_number', 0, 0),
|
||||||
|
b'paragraph.gridTopCenter' : (1, 'scalar_number', 0, 0),
|
||||||
|
b'paragraph.gridBeginCenter' : (1, 'scalar_number', 0, 0),
|
||||||
|
b'paragraph.gridEndCenter' : (1, 'scalar_number', 0, 0),
|
||||||
|
|
||||||
|
|
||||||
|
b'word_semantic' : (1, 'snippets', 1, 1),
|
||||||
|
b'word_semantic.type' : (1, 'scalar_text', 0, 0),
|
||||||
|
b'word_semantic.class' : (1, 'scalar_text', 0, 0),
|
||||||
|
b'word_semantic.firstWord' : (1, 'scalar_number', 0, 0),
|
||||||
|
b'word_semantic.lastWord' : (1, 'scalar_number', 0, 0),
|
||||||
|
b'word_semantic.gridBottomCenter' : (1, 'scalar_number', 0, 0),
|
||||||
|
b'word_semantic.gridTopCenter' : (1, 'scalar_number', 0, 0),
|
||||||
|
b'word_semantic.gridBeginCenter' : (1, 'scalar_number', 0, 0),
|
||||||
|
b'word_semantic.gridEndCenter' : (1, 'scalar_number', 0, 0),
|
||||||
|
|
||||||
|
b'word' : (1, 'snippets', 1, 0),
|
||||||
|
b'word.type' : (1, 'scalar_text', 0, 0),
|
||||||
|
b'word.class' : (1, 'scalar_text', 0, 0),
|
||||||
|
b'word.firstGlyph' : (1, 'scalar_number', 0, 0),
|
||||||
|
b'word.lastGlyph' : (1, 'scalar_number', 0, 0),
|
||||||
|
|
||||||
|
b'_span' : (1, 'snippets', 1, 0),
|
||||||
|
b'_span.class' : (1, 'scalar_text', 0, 0),
|
||||||
|
b'_span.firstWord' : (1, 'scalar_number', 0, 0),
|
||||||
|
b'_span.lastWord' : (1, 'scalar_number', 0, 0),
|
||||||
|
b'_span.gridSize' : (1, 'scalar_number', 0, 0),
|
||||||
|
b'_span.gridBottomCenter' : (1, 'scalar_number', 0, 0),
|
||||||
|
b'_span.gridTopCenter' : (1, 'scalar_number', 0, 0),
|
||||||
|
b'_span.gridBeginCenter' : (1, 'scalar_number', 0, 0),
|
||||||
|
b'_span.gridEndCenter' : (1, 'scalar_number', 0, 0),
|
||||||
|
|
||||||
|
b'span' : (1, 'snippets', 1, 0),
|
||||||
|
b'span.firstWord' : (1, 'scalar_number', 0, 0),
|
||||||
|
b'span.lastWord' : (1, 'scalar_number', 0, 0),
|
||||||
|
b'span.gridSize' : (1, 'scalar_number', 0, 0),
|
||||||
|
b'span.gridBottomCenter' : (1, 'scalar_number', 0, 0),
|
||||||
|
b'span.gridTopCenter' : (1, 'scalar_number', 0, 0),
|
||||||
|
b'span.gridBeginCenter' : (1, 'scalar_number', 0, 0),
|
||||||
|
b'span.gridEndCenter' : (1, 'scalar_number', 0, 0),
|
||||||
|
|
||||||
|
b'extratokens' : (1, 'snippets', 1, 0),
|
||||||
|
b'extratokens.class' : (1, 'scalar_text', 0, 0),
|
||||||
|
b'extratokens.type' : (1, 'scalar_text', 0, 0),
|
||||||
|
b'extratokens.firstGlyph' : (1, 'scalar_number', 0, 0),
|
||||||
|
b'extratokens.lastGlyph' : (1, 'scalar_number', 0, 0),
|
||||||
|
b'extratokens.gridSize' : (1, 'scalar_number', 0, 0),
|
||||||
|
b'extratokens.gridBottomCenter' : (1, 'scalar_number', 0, 0),
|
||||||
|
b'extratokens.gridTopCenter' : (1, 'scalar_number', 0, 0),
|
||||||
|
b'extratokens.gridBeginCenter' : (1, 'scalar_number', 0, 0),
|
||||||
|
b'extratokens.gridEndCenter' : (1, 'scalar_number', 0, 0),
|
||||||
|
|
||||||
|
b'glyph.h' : (1, 'number', 0, 0),
|
||||||
|
b'glyph.w' : (1, 'number', 0, 0),
|
||||||
|
b'glyph.use' : (1, 'number', 0, 0),
|
||||||
|
b'glyph.vtx' : (1, 'number', 0, 1),
|
||||||
|
b'glyph.len' : (1, 'number', 0, 1),
|
||||||
|
b'glyph.dpi' : (1, 'number', 0, 0),
|
||||||
|
b'vtx' : (0, 'number', 1, 1),
|
||||||
|
b'vtx.x' : (1, 'number', 0, 0),
|
||||||
|
b'vtx.y' : (1, 'number', 0, 0),
|
||||||
|
b'len' : (0, 'number', 1, 1),
|
||||||
|
b'len.n' : (1, 'number', 0, 0),
|
||||||
|
|
||||||
|
b'book' : (1, 'snippets', 1, 0),
|
||||||
|
b'version' : (1, 'snippets', 1, 0),
|
||||||
|
b'version.FlowEdit_1_id' : (1, 'scalar_text', 0, 0),
|
||||||
|
b'version.FlowEdit_1_version' : (1, 'scalar_text', 0, 0),
|
||||||
|
b'version.Schema_id' : (1, 'scalar_text', 0, 0),
|
||||||
|
b'version.Schema_version' : (1, 'scalar_text', 0, 0),
|
||||||
|
b'version.Topaz_version' : (1, 'scalar_text', 0, 0),
|
||||||
|
b'version.WordDetailEdit_1_id' : (1, 'scalar_text', 0, 0),
|
||||||
|
b'version.WordDetailEdit_1_version' : (1, 'scalar_text', 0, 0),
|
||||||
|
b'version.ZoneEdit_1_id' : (1, 'scalar_text', 0, 0),
|
||||||
|
b'version.ZoneEdit_1_version' : (1, 'scalar_text', 0, 0),
|
||||||
|
b'version.chapterheaders' : (1, 'scalar_text', 0, 0),
|
||||||
|
b'version.creation_date' : (1, 'scalar_text', 0, 0),
|
||||||
|
b'version.header_footer' : (1, 'scalar_text', 0, 0),
|
||||||
|
b'version.init_from_ocr' : (1, 'scalar_text', 0, 0),
|
||||||
|
b'version.letter_insertion' : (1, 'scalar_text', 0, 0),
|
||||||
|
b'version.xmlinj_convert' : (1, 'scalar_text', 0, 0),
|
||||||
|
b'version.xmlinj_reflow' : (1, 'scalar_text', 0, 0),
|
||||||
|
b'version.xmlinj_transform' : (1, 'scalar_text', 0, 0),
|
||||||
|
b'version.findlists' : (1, 'scalar_text', 0, 0),
|
||||||
|
b'version.page_num' : (1, 'scalar_text', 0, 0),
|
||||||
|
b'version.page_type' : (1, 'scalar_text', 0, 0),
|
||||||
|
b'version.bad_text' : (1, 'scalar_text', 0, 0),
|
||||||
|
b'version.glyph_mismatch' : (1, 'scalar_text', 0, 0),
|
||||||
|
b'version.margins' : (1, 'scalar_text', 0, 0),
|
||||||
|
b'version.staggered_lines' : (1, 'scalar_text', 0, 0),
|
||||||
|
b'version.paragraph_continuation' : (1, 'scalar_text', 0, 0),
|
||||||
|
b'version.toc' : (1, 'scalar_text', 0, 0),
|
||||||
|
|
||||||
|
b'stylesheet' : (1, 'snippets', 1, 0),
|
||||||
|
b'style' : (1, 'snippets', 1, 0),
|
||||||
|
b'style._tag' : (1, 'scalar_text', 0, 0),
|
||||||
|
b'style.type' : (1, 'scalar_text', 0, 0),
|
||||||
|
b'style._after_type' : (1, 'scalar_text', 0, 0),
|
||||||
|
b'style._parent_type' : (1, 'scalar_text', 0, 0),
|
||||||
|
b'style._after_parent_type' : (1, 'scalar_text', 0, 0),
|
||||||
|
b'style.class' : (1, 'scalar_text', 0, 0),
|
||||||
|
b'style._after_class' : (1, 'scalar_text', 0, 0),
|
||||||
|
b'rule' : (1, 'snippets', 1, 0),
|
||||||
|
b'rule.attr' : (1, 'scalar_text', 0, 0),
|
||||||
|
b'rule.value' : (1, 'scalar_text', 0, 0),
|
||||||
|
|
||||||
|
b'original' : (0, 'number', 1, 1),
|
||||||
|
b'original.pnum' : (1, 'number', 0, 0),
|
||||||
|
b'original.pid' : (1, 'text', 0, 0),
|
||||||
|
b'pages' : (0, 'number', 1, 1),
|
||||||
|
b'pages.ref' : (1, 'number', 0, 0),
|
||||||
|
b'pages.id' : (1, 'number', 0, 0),
|
||||||
|
b'startID' : (0, 'number', 1, 1),
|
||||||
|
b'startID.page' : (1, 'number', 0, 0),
|
||||||
|
b'startID.id' : (1, 'number', 0, 0),
|
||||||
|
|
||||||
|
b'median_d' : (1, 'number', 0, 0),
|
||||||
|
b'median_h' : (1, 'number', 0, 0),
|
||||||
|
b'median_firsty' : (1, 'number', 0, 0),
|
||||||
|
b'median_lasty' : (1, 'number', 0, 0),
|
||||||
|
|
||||||
|
b'num_footers_maybe' : (1, 'number', 0, 0),
|
||||||
|
b'num_footers_yes' : (1, 'number', 0, 0),
|
||||||
|
b'num_headers_maybe' : (1, 'number', 0, 0),
|
||||||
|
b'num_headers_yes' : (1, 'number', 0, 0),
|
||||||
|
|
||||||
|
b'tracking' : (1, 'number', 0, 0),
|
||||||
|
b'src' : (1, 'text', 0, 0),
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# full tag path record keeping routines
|
||||||
|
def tag_push(self, token):
|
||||||
|
self.tagpath.append(token)
|
||||||
|
def tag_pop(self):
|
||||||
|
if len(self.tagpath) > 0 :
|
||||||
|
self.tagpath.pop()
|
||||||
|
def tagpath_len(self):
|
||||||
|
return len(self.tagpath)
|
||||||
|
def get_tagpath(self, i):
|
||||||
|
cnt = len(self.tagpath)
|
||||||
|
if i < cnt : result = self.tagpath[i]
|
||||||
|
for j in range(i+1, cnt) :
|
||||||
|
result += b'.' + self.tagpath[j]
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# list of absolute command byte values values that indicate
|
||||||
|
# various types of loop meachanisms typically used to generate vectors
|
||||||
|
|
||||||
|
cmd_list = (0x76, 0x76)
|
||||||
|
|
||||||
|
# peek at and return 1 byte that is ahead by i bytes
|
||||||
|
def peek(self, aheadi):
|
||||||
|
c = self.fo.read(aheadi)
|
||||||
|
if (len(c) == 0):
|
||||||
|
return None
|
||||||
|
self.fo.seek(-aheadi,1)
|
||||||
|
c = c[-1:]
|
||||||
|
return ord(c)
|
||||||
|
|
||||||
|
|
||||||
|
# get the next value from the file being processed
|
||||||
|
def getNext(self):
|
||||||
|
nbyte = self.peek(1);
|
||||||
|
if (nbyte == None):
|
||||||
|
return None
|
||||||
|
val = readEncodedNumber(self.fo)
|
||||||
|
return val
|
||||||
|
|
||||||
|
|
||||||
|
# format an arg by argtype
|
||||||
|
def formatArg(self, arg, argtype):
|
||||||
|
if (argtype == 'text') or (argtype == 'scalar_text') :
|
||||||
|
result = self.dict.lookup(arg)
|
||||||
|
elif (argtype == 'raw') or (argtype == 'number') or (argtype == 'scalar_number') :
|
||||||
|
result = arg
|
||||||
|
elif (argtype == 'snippets') :
|
||||||
|
result = arg
|
||||||
|
else :
|
||||||
|
print("Error Unknown argtype %s" % argtype)
|
||||||
|
sys.exit(-2)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# process the next tag token, recursively handling subtags,
|
||||||
|
# arguments, and commands
|
||||||
|
def procToken(self, token):
|
||||||
|
|
||||||
|
known_token = False
|
||||||
|
self.tag_push(token)
|
||||||
|
|
||||||
|
if self.debug : print('Processing: ', self.get_tagpath(0))
|
||||||
|
cnt = self.tagpath_len()
|
||||||
|
for j in range(cnt):
|
||||||
|
tkn = self.get_tagpath(j)
|
||||||
|
if tkn in self.token_tags :
|
||||||
|
num_args = self.token_tags[tkn][0]
|
||||||
|
argtype = self.token_tags[tkn][1]
|
||||||
|
subtags = self.token_tags[tkn][2]
|
||||||
|
splcase = self.token_tags[tkn][3]
|
||||||
|
ntags = -1
|
||||||
|
known_token = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if known_token :
|
||||||
|
|
||||||
|
# handle subtags if present
|
||||||
|
subtagres = []
|
||||||
|
if (splcase == 1):
|
||||||
|
# this type of tag uses of escape marker 0x74 indicate subtag count
|
||||||
|
if self.peek(1) == 0x74:
|
||||||
|
skip = readEncodedNumber(self.fo)
|
||||||
|
subtags = 1
|
||||||
|
num_args = 0
|
||||||
|
|
||||||
|
if (subtags == 1):
|
||||||
|
ntags = readEncodedNumber(self.fo)
|
||||||
|
if self.debug : print('subtags: ', token , ' has ' , str(ntags))
|
||||||
|
for j in range(ntags):
|
||||||
|
val = readEncodedNumber(self.fo)
|
||||||
|
subtagres.append(self.procToken(self.dict.lookup(val)))
|
||||||
|
|
||||||
|
# arguments can be scalars or vectors of text or numbers
|
||||||
|
argres = []
|
||||||
|
if num_args > 0 :
|
||||||
|
firstarg = self.peek(1)
|
||||||
|
if (firstarg in self.cmd_list) and (argtype != 'scalar_number') and (argtype != 'scalar_text'):
|
||||||
|
# single argument is a variable length vector of data
|
||||||
|
arg = readEncodedNumber(self.fo)
|
||||||
|
argres = self.decodeCMD(arg,argtype)
|
||||||
|
else :
|
||||||
|
# num_arg scalar arguments
|
||||||
|
for i in range(num_args):
|
||||||
|
argres.append(self.formatArg(readEncodedNumber(self.fo), argtype))
|
||||||
|
|
||||||
|
# build the return tag
|
||||||
|
result = []
|
||||||
|
tkn = self.get_tagpath(0)
|
||||||
|
result.append(tkn)
|
||||||
|
result.append(subtagres)
|
||||||
|
result.append(argtype)
|
||||||
|
result.append(argres)
|
||||||
|
self.tag_pop()
|
||||||
|
return result
|
||||||
|
|
||||||
|
# all tokens that need to be processed should be in the hash
|
||||||
|
# table if it may indicate a problem, either new token
|
||||||
|
# or an out of sync condition
|
||||||
|
else:
|
||||||
|
result = []
|
||||||
|
if (self.debug or self.first_unknown):
|
||||||
|
print('Unknown Token:', token)
|
||||||
|
self.first_unknown = False
|
||||||
|
self.tag_pop()
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# special loop used to process code snippets
|
||||||
|
# it is NEVER used to format arguments.
|
||||||
|
# builds the snippetList
|
||||||
|
def doLoop72(self, argtype):
|
||||||
|
cnt = readEncodedNumber(self.fo)
|
||||||
|
if self.debug :
|
||||||
|
result = 'Set of '+ str(cnt) + ' xml snippets. The overall structure \n'
|
||||||
|
result += 'of the document is indicated by snippet number sets at the\n'
|
||||||
|
result += 'end of each snippet. \n'
|
||||||
|
print(result)
|
||||||
|
for i in range(cnt):
|
||||||
|
if self.debug: print('Snippet:',str(i))
|
||||||
|
snippet = []
|
||||||
|
snippet.append(i)
|
||||||
|
val = readEncodedNumber(self.fo)
|
||||||
|
snippet.append(self.procToken(self.dict.lookup(val)))
|
||||||
|
self.snippetList.append(snippet)
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# general loop code gracisouly submitted by "skindle" - thank you!
|
||||||
|
def doLoop76Mode(self, argtype, cnt, mode):
|
||||||
|
result = []
|
||||||
|
adj = 0
|
||||||
|
if mode & 1:
|
||||||
|
adj = readEncodedNumber(self.fo)
|
||||||
|
mode = mode >> 1
|
||||||
|
x = []
|
||||||
|
for i in range(cnt):
|
||||||
|
x.append(readEncodedNumber(self.fo) - adj)
|
||||||
|
for i in range(mode):
|
||||||
|
for j in range(1, cnt):
|
||||||
|
x[j] = x[j] + x[j - 1]
|
||||||
|
for i in range(cnt):
|
||||||
|
result.append(self.formatArg(x[i],argtype))
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# dispatches loop commands bytes with various modes
|
||||||
|
# The 0x76 style loops are used to build vectors
|
||||||
|
|
||||||
|
# This was all derived by trial and error and
|
||||||
|
# new loop types may exist that are not handled here
|
||||||
|
# since they did not appear in the test cases
|
||||||
|
|
||||||
|
def decodeCMD(self, cmd, argtype):
|
||||||
|
if (cmd == 0x76):
|
||||||
|
|
||||||
|
# loop with cnt, and mode to control loop styles
|
||||||
|
cnt = readEncodedNumber(self.fo)
|
||||||
|
mode = readEncodedNumber(self.fo)
|
||||||
|
|
||||||
|
if self.debug : print('Loop for', cnt, 'with mode', mode, ': ')
|
||||||
|
return self.doLoop76Mode(argtype, cnt, mode)
|
||||||
|
|
||||||
|
if self.dbug: print("Unknown command", cmd)
|
||||||
|
result = []
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# add full tag path to injected snippets
|
||||||
|
def updateName(self, tag, prefix):
|
||||||
|
name = tag[0]
|
||||||
|
subtagList = tag[1]
|
||||||
|
argtype = tag[2]
|
||||||
|
argList = tag[3]
|
||||||
|
nname = prefix + b'.' + name
|
||||||
|
nsubtaglist = []
|
||||||
|
for j in subtagList:
|
||||||
|
nsubtaglist.append(self.updateName(j,prefix))
|
||||||
|
ntag = []
|
||||||
|
ntag.append(nname)
|
||||||
|
ntag.append(nsubtaglist)
|
||||||
|
ntag.append(argtype)
|
||||||
|
ntag.append(argList)
|
||||||
|
return ntag
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# perform depth first injection of specified snippets into this one
|
||||||
|
def injectSnippets(self, snippet):
|
||||||
|
snipno, tag = snippet
|
||||||
|
name = tag[0]
|
||||||
|
subtagList = tag[1]
|
||||||
|
argtype = tag[2]
|
||||||
|
argList = tag[3]
|
||||||
|
nsubtagList = []
|
||||||
|
if len(argList) > 0 :
|
||||||
|
for j in argList:
|
||||||
|
asnip = self.snippetList[j]
|
||||||
|
aso, atag = self.injectSnippets(asnip)
|
||||||
|
atag = self.updateName(atag, name)
|
||||||
|
nsubtagList.append(atag)
|
||||||
|
argtype='number'
|
||||||
|
argList=[]
|
||||||
|
if len(nsubtagList) > 0 :
|
||||||
|
subtagList.extend(nsubtagList)
|
||||||
|
tag = []
|
||||||
|
tag.append(name)
|
||||||
|
tag.append(subtagList)
|
||||||
|
tag.append(argtype)
|
||||||
|
tag.append(argList)
|
||||||
|
snippet = []
|
||||||
|
snippet.append(snipno)
|
||||||
|
snippet.append(tag)
|
||||||
|
return snippet
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# format the tag for output
|
||||||
|
def formatTag(self, node):
|
||||||
|
name = node[0]
|
||||||
|
subtagList = node[1]
|
||||||
|
argtype = node[2]
|
||||||
|
argList = node[3]
|
||||||
|
fullpathname = name.split(b'.')
|
||||||
|
nodename = fullpathname.pop()
|
||||||
|
ilvl = len(fullpathname)
|
||||||
|
indent = b' ' * (3 * ilvl)
|
||||||
|
rlst = []
|
||||||
|
rlst.append(indent + b'<' + nodename + b'>')
|
||||||
|
if len(argList) > 0:
|
||||||
|
alst = []
|
||||||
|
for j in argList:
|
||||||
|
if (argtype == b'text') or (argtype == b'scalar_text') :
|
||||||
|
alst.append(j + b'|')
|
||||||
|
else :
|
||||||
|
alst.append(str(j).encode('utf-8') + b',')
|
||||||
|
argres = b"".join(alst)
|
||||||
|
argres = argres[0:-1]
|
||||||
|
if argtype == b'snippets' :
|
||||||
|
rlst.append(b'snippets:' + argres)
|
||||||
|
else :
|
||||||
|
rlst.append(argres)
|
||||||
|
if len(subtagList) > 0 :
|
||||||
|
rlst.append(b'\n')
|
||||||
|
for j in subtagList:
|
||||||
|
if len(j) > 0 :
|
||||||
|
rlst.append(self.formatTag(j))
|
||||||
|
rlst.append(indent + b'</' + nodename + b'>\n')
|
||||||
|
else:
|
||||||
|
rlst.append(b'</' + nodename + b'>\n')
|
||||||
|
return b"".join(rlst)
|
||||||
|
|
||||||
|
|
||||||
|
# flatten tag
|
||||||
|
def flattenTag(self, node):
|
||||||
|
name = node[0]
|
||||||
|
subtagList = node[1]
|
||||||
|
argtype = node[2]
|
||||||
|
argList = node[3]
|
||||||
|
rlst = []
|
||||||
|
rlst.append(name)
|
||||||
|
if (len(argList) > 0):
|
||||||
|
alst = []
|
||||||
|
for j in argList:
|
||||||
|
if (argtype == 'text') or (argtype == 'scalar_text') :
|
||||||
|
alst.append(j + b'|')
|
||||||
|
else :
|
||||||
|
alst.append(str(j).encode('utf-8') + b'|')
|
||||||
|
argres = b"".join(alst)
|
||||||
|
argres = argres[0:-1]
|
||||||
|
if argtype == b'snippets' :
|
||||||
|
rlst.append(b'.snippets=' + argres)
|
||||||
|
else :
|
||||||
|
rlst.append(b'=' + argres)
|
||||||
|
rlst.append(b'\n')
|
||||||
|
for j in subtagList:
|
||||||
|
if len(j) > 0 :
|
||||||
|
rlst.append(self.flattenTag(j))
|
||||||
|
return b"".join(rlst)
|
||||||
|
|
||||||
|
|
||||||
|
# reduce create xml output
|
||||||
|
def formatDoc(self, flat_xml):
|
||||||
|
rlst = []
|
||||||
|
for j in self.doc :
|
||||||
|
if len(j) > 0:
|
||||||
|
if flat_xml:
|
||||||
|
rlst.append(self.flattenTag(j))
|
||||||
|
else:
|
||||||
|
rlst.append(self.formatTag(j))
|
||||||
|
result = b"".join(rlst)
|
||||||
|
if self.debug : print(result)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# main loop - parse the page.dat files
|
||||||
|
# to create structured document and snippets
|
||||||
|
|
||||||
|
# FIXME: value at end of magic appears to be a subtags count
|
||||||
|
# but for what? For now, inject an 'info" tag as it is in
|
||||||
|
# every dictionary and seems close to what is meant
|
||||||
|
# The alternative is to special case the last _ "0x5f" to mean something
|
||||||
|
|
||||||
|
def process(self):
|
||||||
|
|
||||||
|
# peek at the first bytes to see what type of file it is
|
||||||
|
magic = self.fo.read(9)
|
||||||
|
if (magic[0:1] == b'p') and (magic[2:9] == b'marker_'):
|
||||||
|
first_token = b'info'
|
||||||
|
elif (magic[0:1] == b'p') and (magic[2:9] == b'__PAGE_'):
|
||||||
|
skip = self.fo.read(2)
|
||||||
|
first_token = b'info'
|
||||||
|
elif (magic[0:1] == b'p') and (magic[2:8] == b'_PAGE_'):
|
||||||
|
first_token = b'info'
|
||||||
|
elif (magic[0:1] == b'g') and (magic[2:9] == b'__GLYPH'):
|
||||||
|
skip = self.fo.read(3)
|
||||||
|
first_token = b'info'
|
||||||
|
else :
|
||||||
|
# other0.dat file
|
||||||
|
first_token = None
|
||||||
|
self.fo.seek(-9,1)
|
||||||
|
|
||||||
|
|
||||||
|
# main loop to read and build the document tree
|
||||||
|
while True:
|
||||||
|
|
||||||
|
if first_token != None :
|
||||||
|
# use "inserted" first token 'info' for page and glyph files
|
||||||
|
tag = self.procToken(first_token)
|
||||||
|
if len(tag) > 0 :
|
||||||
|
self.doc.append(tag)
|
||||||
|
first_token = None
|
||||||
|
|
||||||
|
v = self.getNext()
|
||||||
|
if (v == None):
|
||||||
|
break
|
||||||
|
|
||||||
|
if (v == 0x72):
|
||||||
|
self.doLoop72(b'number')
|
||||||
|
elif (v > 0) and (v < self.dict.getSize()) :
|
||||||
|
tag = self.procToken(self.dict.lookup(v))
|
||||||
|
if len(tag) > 0 :
|
||||||
|
self.doc.append(tag)
|
||||||
|
else:
|
||||||
|
if self.debug:
|
||||||
|
print("Main Loop: Unknown value: %x" % v)
|
||||||
|
if (v == 0):
|
||||||
|
if (self.peek(1) == 0x5f):
|
||||||
|
skip = self.fo.read(1)
|
||||||
|
first_token = b'info'
|
||||||
|
|
||||||
|
# now do snippet injection
|
||||||
|
if len(self.snippetList) > 0 :
|
||||||
|
if self.debug : print('Injecting Snippets:')
|
||||||
|
snippet = self.injectSnippets(self.snippetList[0])
|
||||||
|
snipno = snippet[0]
|
||||||
|
tag_add = snippet[1]
|
||||||
|
if self.debug : print(self.formatTag(tag_add))
|
||||||
|
if len(tag_add) > 0:
|
||||||
|
self.doc.append(tag_add)
|
||||||
|
|
||||||
|
# handle generation of xml output
|
||||||
|
xmlpage = self.formatDoc(self.flat_xml)
|
||||||
|
|
||||||
|
return xmlpage
|
||||||
|
|
||||||
|
|
||||||
|
def fromData(dict, fname):
|
||||||
|
flat_xml = True
|
||||||
|
debug = True
|
||||||
|
pp = PageParser(fname, dict, debug, flat_xml)
|
||||||
|
xmlpage = pp.process()
|
||||||
|
return xmlpage
|
||||||
|
|
||||||
|
def getXML(dict, fname):
|
||||||
|
flat_xml = False
|
||||||
|
debug = True
|
||||||
|
pp = PageParser(fname, dict, debug, flat_xml)
|
||||||
|
xmlpage = pp.process()
|
||||||
|
return xmlpage
|
||||||
|
|
||||||
|
def usage():
|
||||||
|
print('Usage: ')
|
||||||
|
print(' convert2xml.py dict0000.dat infile.dat ')
|
||||||
|
print(' ')
|
||||||
|
print(' Options:')
|
||||||
|
print(' -h print this usage help message ')
|
||||||
|
print(' -d turn on debug output to check for potential errors ')
|
||||||
|
print(' --flat-xml output the flattened xml page description only ')
|
||||||
|
print(' ')
|
||||||
|
print(' This program will attempt to convert a page*.dat file or ')
|
||||||
|
print(' glyphs*.dat file, using the dict0000.dat file, to its xml description. ')
|
||||||
|
print(' ')
|
||||||
|
print(' Use "cmbtc_dump.py" first to unencrypt, uncompress, and dump ')
|
||||||
|
print(' the *.dat files from a Topaz format e-book.')
|
||||||
|
|
||||||
|
#
|
||||||
|
# Main
|
||||||
|
#
|
||||||
|
|
||||||
|
def main(argv):
|
||||||
|
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||||
|
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||||
|
dictFile = ""
|
||||||
|
pageFile = ""
|
||||||
|
debug = True
|
||||||
|
flat_xml = False
|
||||||
|
printOutput = False
|
||||||
|
if len(argv) == 0:
|
||||||
|
printOutput = True
|
||||||
|
argv = sys.argv
|
||||||
|
|
||||||
|
try:
|
||||||
|
opts, args = getopt.getopt(argv[1:], "hd", ["flat-xml"])
|
||||||
|
|
||||||
|
except getopt.GetoptError as err:
|
||||||
|
|
||||||
|
# print help information and exit:
|
||||||
|
print(str(err)) # will print something like "option -a not recognized"
|
||||||
|
usage()
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
if len(opts) == 0 and len(args) == 0 :
|
||||||
|
usage()
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
for o, a in opts:
|
||||||
|
if o =="-d":
|
||||||
|
debug=True
|
||||||
|
if o =="-h":
|
||||||
|
usage()
|
||||||
|
sys.exit(0)
|
||||||
|
if o =="--flat-xml":
|
||||||
|
flat_xml = True
|
||||||
|
|
||||||
|
dictFile, pageFile = args[0], args[1]
|
||||||
|
|
||||||
|
# read in the string table dictionary
|
||||||
|
dict = Dictionary(dictFile)
|
||||||
|
# dict.dumpDict()
|
||||||
|
|
||||||
|
# create a page parser
|
||||||
|
pp = PageParser(pageFile, dict, debug, flat_xml)
|
||||||
|
|
||||||
|
xmlpage = pp.process()
|
||||||
|
|
||||||
|
if printOutput:
|
||||||
|
print(xmlpage)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
return xmlpage
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main(''))
|
||||||
319
DeDRM_plugin/epubfontdecrypt.py
Normal file
319
DeDRM_plugin/epubfontdecrypt.py
Normal file
@@ -0,0 +1,319 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# epubfontdecrypt.py
|
||||||
|
# Copyright © 2021 by noDRM
|
||||||
|
|
||||||
|
# Released under the terms of the GNU General Public Licence, version 3
|
||||||
|
# <http://www.gnu.org/licenses/>
|
||||||
|
|
||||||
|
|
||||||
|
# Revision history:
|
||||||
|
# 1 - Initial release
|
||||||
|
|
||||||
|
"""
|
||||||
|
Decrypts / deobfuscates font files in EPUB files
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__version__ = "1"
|
||||||
|
|
||||||
|
import os
|
||||||
|
import traceback
|
||||||
|
import zlib
|
||||||
|
import zipfile
|
||||||
|
from zipfile import ZipInfo, ZipFile, ZIP_STORED, ZIP_DEFLATED
|
||||||
|
from contextlib import closing
|
||||||
|
from lxml import etree
|
||||||
|
import itertools
|
||||||
|
import hashlib
|
||||||
|
import binascii
|
||||||
|
|
||||||
|
|
||||||
|
class Decryptor(object):
|
||||||
|
def __init__(self, obfuscationkeyIETF, obfuscationkeyAdobe, encryption):
|
||||||
|
enc = lambda tag: '{%s}%s' % ('http://www.w3.org/2001/04/xmlenc#', tag)
|
||||||
|
dsig = lambda tag: '{%s}%s' % ('http://www.w3.org/2000/09/xmldsig#', tag)
|
||||||
|
self.obfuscation_key_Adobe = obfuscationkeyAdobe
|
||||||
|
self.obfuscation_key_IETF = obfuscationkeyIETF
|
||||||
|
|
||||||
|
self._encryption = etree.fromstring(encryption)
|
||||||
|
# This loops through all entries in the "encryption.xml" file
|
||||||
|
# to figure out which files need to be decrypted.
|
||||||
|
self._obfuscatedIETF = obfuscatedIETF = set()
|
||||||
|
self._obfuscatedAdobe = obfuscatedAdobe = set()
|
||||||
|
self._other = other = set()
|
||||||
|
|
||||||
|
self._json_elements_to_remove = json_elements_to_remove = set()
|
||||||
|
self._has_remaining_xml = False
|
||||||
|
expr = './%s/%s/%s' % (enc('EncryptedData'), enc('CipherData'),
|
||||||
|
enc('CipherReference'))
|
||||||
|
for elem in self._encryption.findall(expr):
|
||||||
|
path = elem.get('URI', None)
|
||||||
|
encryption_type_url = (elem.getparent().getparent().find("./%s" % (enc('EncryptionMethod'))).get('Algorithm', None))
|
||||||
|
if path is not None:
|
||||||
|
|
||||||
|
if encryption_type_url == "http://www.idpf.org/2008/embedding":
|
||||||
|
# Font files obfuscated with the IETF algorithm
|
||||||
|
path = path.encode('utf-8')
|
||||||
|
obfuscatedIETF.add(path)
|
||||||
|
if (self.obfuscation_key_IETF is None):
|
||||||
|
self._has_remaining_xml = True
|
||||||
|
else:
|
||||||
|
json_elements_to_remove.add(elem.getparent().getparent())
|
||||||
|
|
||||||
|
elif encryption_type_url == "http://ns.adobe.com/pdf/enc#RC":
|
||||||
|
# Font files obfuscated with the Adobe algorithm.
|
||||||
|
path = path.encode('utf-8')
|
||||||
|
obfuscatedAdobe.add(path)
|
||||||
|
if (self.obfuscation_key_Adobe is None):
|
||||||
|
self._has_remaining_xml = True
|
||||||
|
else:
|
||||||
|
json_elements_to_remove.add(elem.getparent().getparent())
|
||||||
|
|
||||||
|
else:
|
||||||
|
path = path.encode('utf-8')
|
||||||
|
other.add(path)
|
||||||
|
self._has_remaining_xml = True
|
||||||
|
# Other unsupported type.
|
||||||
|
|
||||||
|
for elem in json_elements_to_remove:
|
||||||
|
elem.getparent().remove(elem)
|
||||||
|
|
||||||
|
def check_if_remaining(self):
|
||||||
|
return self._has_remaining_xml
|
||||||
|
|
||||||
|
def get_xml(self):
|
||||||
|
return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + etree.tostring(self._encryption, encoding="utf-8", pretty_print=True, xml_declaration=False).decode("utf-8")
|
||||||
|
|
||||||
|
def decompress(self, bytes):
|
||||||
|
dc = zlib.decompressobj(-15)
|
||||||
|
try:
|
||||||
|
decompressed_bytes = dc.decompress(bytes)
|
||||||
|
ex = dc.decompress(b'Z') + dc.flush()
|
||||||
|
if ex:
|
||||||
|
decompressed_bytes = decompressed_bytes + ex
|
||||||
|
except:
|
||||||
|
# possibly not compressed by zip - just return bytes
|
||||||
|
return bytes, False
|
||||||
|
return decompressed_bytes , True
|
||||||
|
|
||||||
|
def decrypt(self, path, data):
|
||||||
|
if path.encode('utf-8') in self._obfuscatedIETF and self.obfuscation_key_IETF is not None:
|
||||||
|
# de-obfuscate according to the IETF standard
|
||||||
|
data, was_decomp = self.decompress(data)
|
||||||
|
|
||||||
|
if len(data) <= 1040:
|
||||||
|
# de-obfuscate whole file
|
||||||
|
out = self.deobfuscate_single_data(self.obfuscation_key_IETF, data)
|
||||||
|
else:
|
||||||
|
out = self.deobfuscate_single_data(self.obfuscation_key_IETF, data[:1040]) + data[1040:]
|
||||||
|
|
||||||
|
if (not was_decomp):
|
||||||
|
out, was_decomp = self.decompress(out)
|
||||||
|
return out
|
||||||
|
|
||||||
|
elif path.encode('utf-8') in self._obfuscatedAdobe and self.obfuscation_key_Adobe is not None:
|
||||||
|
# de-obfuscate according to the Adobe standard
|
||||||
|
data, was_decomp = self.decompress(data)
|
||||||
|
|
||||||
|
if len(data) <= 1024:
|
||||||
|
# de-obfuscate whole file
|
||||||
|
out = self.deobfuscate_single_data(self.obfuscation_key_Adobe, data)
|
||||||
|
else:
|
||||||
|
out = self.deobfuscate_single_data(self.obfuscation_key_Adobe, data[:1024]) + data[1024:]
|
||||||
|
|
||||||
|
if (not was_decomp):
|
||||||
|
out, was_decomp = self.decompress(out)
|
||||||
|
return out
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Not encrypted or obfuscated
|
||||||
|
return data
|
||||||
|
|
||||||
|
def deobfuscate_single_data(self, key, data):
|
||||||
|
try:
|
||||||
|
msg = bytes([c^k for c,k in zip(data, itertools.cycle(key))])
|
||||||
|
except TypeError:
|
||||||
|
# Python 2
|
||||||
|
msg = ''.join(chr(ord(c)^ord(k)) for c,k in itertools.izip(data, itertools.cycle(key)))
|
||||||
|
return msg
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def decryptFontsBook(inpath, outpath):
|
||||||
|
|
||||||
|
with closing(ZipFile(open(inpath, 'rb'))) as inf:
|
||||||
|
namelist = inf.namelist()
|
||||||
|
if 'META-INF/encryption.xml' not in namelist:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
# Font key handling:
|
||||||
|
|
||||||
|
font_master_key = None
|
||||||
|
adobe_master_encryption_key = None
|
||||||
|
|
||||||
|
contNS = lambda tag: '{%s}%s' % ('urn:oasis:names:tc:opendocument:xmlns:container', tag)
|
||||||
|
path = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
container = etree.fromstring(inf.read("META-INF/container.xml"))
|
||||||
|
rootfiles = container.find(contNS("rootfiles")).findall(contNS("rootfile"))
|
||||||
|
for rootfile in rootfiles:
|
||||||
|
path = rootfile.get("full-path", None)
|
||||||
|
if (path is not None):
|
||||||
|
break
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# If path is None, we didn't find an OPF, so we probably don't have a font key.
|
||||||
|
# If path is set, it's the path to the main content OPF file.
|
||||||
|
|
||||||
|
if (path is None):
|
||||||
|
print("FontDecrypt: No OPF for font obfuscation found")
|
||||||
|
return 1
|
||||||
|
else:
|
||||||
|
packageNS = lambda tag: '{%s}%s' % ('http://www.idpf.org/2007/opf', tag)
|
||||||
|
metadataDCNS = lambda tag: '{%s}%s' % ('http://purl.org/dc/elements/1.1/', tag)
|
||||||
|
|
||||||
|
try:
|
||||||
|
container = etree.fromstring(inf.read(path))
|
||||||
|
except:
|
||||||
|
container = []
|
||||||
|
|
||||||
|
## IETF font key algorithm:
|
||||||
|
print("FontDecrypt: Checking {0} for IETF font obfuscation keys ... ".format(path), end='')
|
||||||
|
secret_key_name = None
|
||||||
|
try:
|
||||||
|
secret_key_name = container.get("unique-identifier")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
identify_element = container.find(packageNS("metadata")).find(metadataDCNS("identifier"))
|
||||||
|
if (secret_key_name is None or secret_key_name == identify_element.get("id")):
|
||||||
|
font_master_key = identify_element.text
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if (font_master_key is not None):
|
||||||
|
if (secret_key_name is None):
|
||||||
|
print("found '%s'" % (font_master_key))
|
||||||
|
else:
|
||||||
|
print("found '%s' (%s)" % (font_master_key, secret_key_name))
|
||||||
|
|
||||||
|
# Trim / remove forbidden characters from the key, then hash it:
|
||||||
|
font_master_key = font_master_key.replace(' ', '')
|
||||||
|
font_master_key = font_master_key.replace('\t', '')
|
||||||
|
font_master_key = font_master_key.replace('\r', '')
|
||||||
|
font_master_key = font_master_key.replace('\n', '')
|
||||||
|
font_master_key = font_master_key.encode('utf-8')
|
||||||
|
font_master_key = hashlib.sha1(font_master_key).digest()
|
||||||
|
else:
|
||||||
|
print("not found")
|
||||||
|
|
||||||
|
## Adobe font key algorithm
|
||||||
|
print("FontDecrypt: Checking {0} for Adobe font obfuscation keys ... ".format(path), end='')
|
||||||
|
|
||||||
|
try:
|
||||||
|
metadata = container.find(packageNS("metadata"))
|
||||||
|
identifiers = metadata.findall(metadataDCNS("identifier"))
|
||||||
|
|
||||||
|
uid = None
|
||||||
|
uidMalformed = False
|
||||||
|
|
||||||
|
for identifier in identifiers:
|
||||||
|
if identifier.get(packageNS("scheme")) == "UUID":
|
||||||
|
if identifier.text[:9] == "urn:uuid:":
|
||||||
|
uid = identifier.text[9:]
|
||||||
|
else:
|
||||||
|
uid = identifier.text
|
||||||
|
break
|
||||||
|
if identifier.text[:9] == "urn:uuid:":
|
||||||
|
uid = identifier.text[9:]
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
if uid is not None:
|
||||||
|
uid = uid.replace(chr(0x20),'').replace(chr(0x09),'')
|
||||||
|
uid = uid.replace(chr(0x0D),'').replace(chr(0x0A),'').replace('-','')
|
||||||
|
|
||||||
|
if len(uid) < 16:
|
||||||
|
uidMalformed = True
|
||||||
|
if not all(c in "0123456789abcdefABCDEF" for c in uid):
|
||||||
|
uidMalformed = True
|
||||||
|
|
||||||
|
|
||||||
|
if not uidMalformed:
|
||||||
|
print("found '{0}'".format(uid))
|
||||||
|
uid = uid + uid
|
||||||
|
adobe_master_encryption_key = binascii.unhexlify(uid[:32])
|
||||||
|
|
||||||
|
if adobe_master_encryption_key is None:
|
||||||
|
print("not found")
|
||||||
|
|
||||||
|
except:
|
||||||
|
print("exception")
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Begin decrypting.
|
||||||
|
|
||||||
|
try:
|
||||||
|
encryption = inf.read('META-INF/encryption.xml')
|
||||||
|
decryptor = Decryptor(font_master_key, adobe_master_encryption_key, encryption)
|
||||||
|
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
|
||||||
|
with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf:
|
||||||
|
|
||||||
|
# Mimetype needs to be the first entry, so remove it from the list
|
||||||
|
# whereever it is, then add it at the beginning.
|
||||||
|
namelist.remove("mimetype")
|
||||||
|
|
||||||
|
for path in (["mimetype"] + namelist):
|
||||||
|
data = inf.read(path)
|
||||||
|
zi = ZipInfo(path)
|
||||||
|
zi.compress_type=ZIP_DEFLATED
|
||||||
|
|
||||||
|
if path == "mimetype":
|
||||||
|
# mimetype must not be compressed
|
||||||
|
zi.compress_type = ZIP_STORED
|
||||||
|
|
||||||
|
elif path == "META-INF/encryption.xml":
|
||||||
|
# Check if there's still other entries not related to fonts
|
||||||
|
if (decryptor.check_if_remaining()):
|
||||||
|
data = decryptor.get_xml()
|
||||||
|
print("FontDecrypt: There's remaining entries in encryption.xml, adding file ...")
|
||||||
|
else:
|
||||||
|
# No remaining entries, no need for that file.
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
# get the file info, including time-stamp
|
||||||
|
oldzi = inf.getinfo(path)
|
||||||
|
# copy across useful fields
|
||||||
|
zi.date_time = oldzi.date_time
|
||||||
|
zi.comment = oldzi.comment
|
||||||
|
zi.extra = oldzi.extra
|
||||||
|
zi.internal_attr = oldzi.internal_attr
|
||||||
|
# external attributes are dependent on the create system, so copy both.
|
||||||
|
zi.external_attr = oldzi.external_attr
|
||||||
|
zi.create_system = oldzi.create_system
|
||||||
|
if any(ord(c) >= 128 for c in path) or any(ord(c) >= 128 for c in zi.comment):
|
||||||
|
# If the file name or the comment contains any non-ASCII char, set the UTF8-flag
|
||||||
|
zi.flag_bits |= 0x800
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if path == "mimetype":
|
||||||
|
outf.writestr(zi, inf.read('mimetype'))
|
||||||
|
elif path == "META-INF/encryption.xml":
|
||||||
|
outf.writestr(zi, data)
|
||||||
|
else:
|
||||||
|
outf.writestr(zi, decryptor.decrypt(path, data))
|
||||||
|
except:
|
||||||
|
print("FontDecrypt: Could not decrypt fonts in {0:s} because of an exception:\n{1:s}".format(os.path.basename(inpath), traceback.format_exc()))
|
||||||
|
traceback.print_exc()
|
||||||
|
return 2
|
||||||
|
return 0
|
||||||
|
|
||||||
@@ -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,11 +47,9 @@
|
|||||||
# 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'
|
||||||
|
|
||||||
__version__ = '1.01'
|
import sys, struct, os, traceback
|
||||||
|
|
||||||
import sys, struct, os
|
|
||||||
import zlib
|
import zlib
|
||||||
import zipfile
|
import zipfile
|
||||||
import xml.etree.ElementTree as etree
|
import xml.etree.ElementTree as etree
|
||||||
@@ -66,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)
|
||||||
|
|
||||||
@@ -107,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
|
||||||
@@ -170,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'))
|
||||||
@@ -188,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:
|
||||||
@@ -199,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__":
|
||||||
307
DeDRM_plugin/epubwatermark.py
Normal file
307
DeDRM_plugin/epubwatermark.py
Normal file
@@ -0,0 +1,307 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# epubwatermark.py
|
||||||
|
# Copyright © 2021 NoDRM
|
||||||
|
|
||||||
|
# Revision history:
|
||||||
|
# 1.0 - Initial version
|
||||||
|
|
||||||
|
# Released under the terms of the GNU General Public Licence, version 3
|
||||||
|
# <http://www.gnu.org/licenses/>
|
||||||
|
|
||||||
|
"""
|
||||||
|
Removes various watermarks from EPUB files
|
||||||
|
"""
|
||||||
|
|
||||||
|
import traceback
|
||||||
|
from zipfile import ZipInfo, ZipFile, ZIP_STORED, ZIP_DEFLATED
|
||||||
|
from contextlib import closing
|
||||||
|
from lxml import etree
|
||||||
|
import re
|
||||||
|
|
||||||
|
# Runs a RegEx over all HTML/XHTML files to remove watermakrs.
|
||||||
|
def removeHTMLwatermarks(object, path_to_ebook):
|
||||||
|
try:
|
||||||
|
inf = ZipFile(open(path_to_ebook, 'rb'))
|
||||||
|
namelist = inf.namelist()
|
||||||
|
|
||||||
|
modded_names = []
|
||||||
|
modded_contents = []
|
||||||
|
|
||||||
|
count_adept = 0
|
||||||
|
|
||||||
|
count_lemonink_invisible = 0
|
||||||
|
count_lemonink_visible = 0
|
||||||
|
lemonink_trackingID = None
|
||||||
|
|
||||||
|
for file in namelist:
|
||||||
|
if not (file.endswith('.html') or file.endswith('.xhtml') or file.endswith('.xml')):
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
file_str = inf.read(file).decode("utf-8")
|
||||||
|
str_new = file_str
|
||||||
|
|
||||||
|
# Remove Adobe ADEPT watermarks
|
||||||
|
# Match optional newline at the beginning, then a "meta" tag with name = "Adept.expected.resource" or "Adept.resource"
|
||||||
|
# and either a "value" or a "content" element with an Adobe UUID
|
||||||
|
pre_remove = str_new
|
||||||
|
str_new = re.sub(r'((\r\n|\r|\n)\s*)?\<meta\s+name=\"(Adept\.resource|Adept\.expected\.resource)\"\s+(content|value)=\"urn:uuid:[0-9a-fA-F\-]+\"\s*\/>', '', str_new)
|
||||||
|
str_new = re.sub(r'((\r\n|\r|\n)\s*)?\<meta\s+(content|value)=\"urn:uuid:[0-9a-fA-F\-]+\"\s+name=\"(Adept\.resource|Adept\.expected\.resource)\"\s*\/>', '', str_new)
|
||||||
|
|
||||||
|
if (str_new != pre_remove):
|
||||||
|
count_adept += 1
|
||||||
|
|
||||||
|
# Remove eLibri / LemonInk watermark
|
||||||
|
# Run this in a loop, as it is possible a file has been watermarked twice ...
|
||||||
|
while True:
|
||||||
|
pre_remove = str_new
|
||||||
|
unique_id = re.search(r'<body[^>]+class="[^"]*(t0x[0-9a-fA-F]{25})[^"]*"[^>]*>', str_new)
|
||||||
|
if (unique_id):
|
||||||
|
lemonink_trackingID = unique_id.groups()[0]
|
||||||
|
count_lemonink_invisible += 1
|
||||||
|
str_new = re.sub(lemonink_trackingID, '', str_new)
|
||||||
|
pre_remove = str_new
|
||||||
|
pm = r'(<body[^>]+class="[^"]*"[^>]*>)'
|
||||||
|
pm += r'\<div style\=\'padding\:0\;border\:0\;text\-indent\:0\;line\-height\:normal\;margin\:0 1cm 0.5cm 1cm\;[^\']*text\-decoration\:none\;[^\']*background\:none\;[^\']*\'\>(.*?)</div>'
|
||||||
|
pm += r'\<div style\=\'padding\:0\;border\:0\;text\-indent\:0\;line\-height\:normal\;margin\:0 1cm 0.5cm 1cm\;[^\']*text\-decoration\:none\;[^\']*background\:none\;[^\']*\'\>(.*?)</div>'
|
||||||
|
str_new = re.sub(pm, r'\1', str_new)
|
||||||
|
|
||||||
|
if (str_new != pre_remove):
|
||||||
|
count_lemonink_visible += 1
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
except:
|
||||||
|
traceback.print_exc()
|
||||||
|
continue
|
||||||
|
|
||||||
|
if (file_str == str_new):
|
||||||
|
continue
|
||||||
|
|
||||||
|
modded_names.append(file)
|
||||||
|
modded_contents.append(str_new)
|
||||||
|
|
||||||
|
|
||||||
|
if len(modded_names) == 0:
|
||||||
|
# No file modified, return original
|
||||||
|
return path_to_ebook
|
||||||
|
|
||||||
|
if len(modded_names) != len(modded_contents):
|
||||||
|
# Something went terribly wrong, return original
|
||||||
|
print("Watermark: Error during watermark removal")
|
||||||
|
return path_to_ebook
|
||||||
|
|
||||||
|
# Re-package with modified files:
|
||||||
|
namelist.remove("mimetype")
|
||||||
|
|
||||||
|
try:
|
||||||
|
output = object.temporary_file(".epub").name
|
||||||
|
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
|
||||||
|
with closing(ZipFile(open(output, 'wb'), 'w', **kwds)) as outf:
|
||||||
|
for path in (["mimetype"] + namelist):
|
||||||
|
|
||||||
|
data = inf.read(path)
|
||||||
|
|
||||||
|
try:
|
||||||
|
modded_index = None
|
||||||
|
modded_index = modded_names.index(path)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if modded_index is not None:
|
||||||
|
# Found modified file - replace contents
|
||||||
|
data = modded_contents[modded_index]
|
||||||
|
|
||||||
|
zi = ZipInfo(path)
|
||||||
|
oldzi = inf.getinfo(path)
|
||||||
|
try:
|
||||||
|
zi.compress_type = oldzi.compress_type
|
||||||
|
if path == "mimetype":
|
||||||
|
zi.compress_type = ZIP_STORED
|
||||||
|
zi.date_time = oldzi.date_time
|
||||||
|
zi.comment = oldzi.comment
|
||||||
|
zi.extra = oldzi.extra
|
||||||
|
zi.internal_attr = oldzi.internal_attr
|
||||||
|
zi.external_attr = oldzi.external_attr
|
||||||
|
zi.create_system = oldzi.create_system
|
||||||
|
if any(ord(c) >= 128 for c in path) or any(ord(c) >= 128 for c in zi.comment):
|
||||||
|
# If the file name or the comment contains any non-ASCII char, set the UTF8-flag
|
||||||
|
zi.flag_bits |= 0x800
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
outf.writestr(zi, data)
|
||||||
|
except:
|
||||||
|
traceback.print_exc()
|
||||||
|
return path_to_ebook
|
||||||
|
|
||||||
|
if (count_adept > 0):
|
||||||
|
print("Watermark: Successfully stripped {0} ADEPT watermark(s) from ebook.".format(count_adept))
|
||||||
|
|
||||||
|
if (count_lemonink_invisible > 0 or count_lemonink_visible > 0):
|
||||||
|
print("Watermark: Successfully stripped {0} visible and {1} invisible LemonInk watermark(s) (\"{2}\") from ebook."
|
||||||
|
.format(count_lemonink_visible, count_lemonink_invisible, lemonink_trackingID))
|
||||||
|
|
||||||
|
return output
|
||||||
|
|
||||||
|
except:
|
||||||
|
traceback.print_exc()
|
||||||
|
return path_to_ebook
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Finds the main OPF file, then uses RegEx to remove watermarks
|
||||||
|
def removeOPFwatermarks(object, path_to_ebook):
|
||||||
|
contNS = lambda tag: '{%s}%s' % ('urn:oasis:names:tc:opendocument:xmlns:container', tag)
|
||||||
|
opf_path = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
inf = ZipFile(open(path_to_ebook, 'rb'))
|
||||||
|
container = etree.fromstring(inf.read("META-INF/container.xml"))
|
||||||
|
rootfiles = container.find(contNS("rootfiles")).findall(contNS("rootfile"))
|
||||||
|
for rootfile in rootfiles:
|
||||||
|
opf_path = rootfile.get("full-path", None)
|
||||||
|
if (opf_path is not None):
|
||||||
|
break
|
||||||
|
except:
|
||||||
|
traceback.print_exc()
|
||||||
|
return path_to_ebook
|
||||||
|
|
||||||
|
# If path is None, we didn't find an OPF, so we probably don't have a font key.
|
||||||
|
# If path is set, it's the path to the main content OPF file.
|
||||||
|
|
||||||
|
if (opf_path is None):
|
||||||
|
# No OPF found - no watermark
|
||||||
|
return path_to_ebook
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
container_str = inf.read(opf_path).decode("utf-8")
|
||||||
|
container_str_new = container_str
|
||||||
|
|
||||||
|
had_amazon = False
|
||||||
|
had_elibri = False
|
||||||
|
|
||||||
|
# Remove Amazon hex watermarks
|
||||||
|
# Match optional newline at the beginning, then spaces, then a "meta" tag with name = "Watermark" or "Watermark_(hex)" and a "content" element.
|
||||||
|
# This regex also matches DuMont watermarks with meta name="watermark", with the case-insensitive match on the "w" in watermark.
|
||||||
|
pre_remove = container_str_new
|
||||||
|
container_str_new = re.sub(r'((\r\n|\r|\n)\s*)?\<meta\s+name=\"[Ww]atermark(_\(hex\))?\"\s+content=\"[0-9a-fA-F]+\"\s*\/>', '', container_str_new)
|
||||||
|
container_str_new = re.sub(r'((\r\n|\r|\n)\s*)?\<meta\s+content=\"[0-9a-fA-F]+\"\s+name=\"[Ww]atermark(_\(hex\))?\"\s*\/>', '', container_str_new)
|
||||||
|
if pre_remove != container_str_new:
|
||||||
|
had_amazon = True
|
||||||
|
|
||||||
|
# Remove elibri / lemonink watermark
|
||||||
|
# Lemonink replaces all "id" fields in the opf with "idX_Y", with X being the watermark and Y being a number for that particular ID.
|
||||||
|
# This regex replaces all "idX_Y" IDs with "id_Y", removing the watermark IDs.
|
||||||
|
pre_remove = container_str_new
|
||||||
|
container_str_new = re.sub(r'((\r\n|\r|\n)\s*)?\<\!\-\-\s*Wygenerowane przez elibri dla zamówienia numer [0-9a-fA-F]+\s*\-\-\>', '', container_str_new)
|
||||||
|
if pre_remove != container_str_new:
|
||||||
|
# To prevent this Regex from applying to books without that watermark, only do that if the watermark above was found.
|
||||||
|
container_str_new = re.sub(r'\=\"id[0-9]+_([0-9]+)\"', r'="id_\1"', container_str_new)
|
||||||
|
if pre_remove != container_str_new:
|
||||||
|
had_elibri = True
|
||||||
|
|
||||||
|
except:
|
||||||
|
traceback.print_exc()
|
||||||
|
return path_to_ebook
|
||||||
|
|
||||||
|
if (container_str == container_str_new):
|
||||||
|
# container didn't change - no watermark
|
||||||
|
return path_to_ebook
|
||||||
|
|
||||||
|
# Re-package without watermark
|
||||||
|
namelist = inf.namelist()
|
||||||
|
namelist.remove("mimetype")
|
||||||
|
|
||||||
|
try:
|
||||||
|
output = object.temporary_file(".epub").name
|
||||||
|
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
|
||||||
|
with closing(ZipFile(open(output, 'wb'), 'w', **kwds)) as outf:
|
||||||
|
for path in (["mimetype"] + namelist):
|
||||||
|
|
||||||
|
data = inf.read(path)
|
||||||
|
if path == opf_path:
|
||||||
|
# Found OPF, replacing ...
|
||||||
|
data = container_str_new
|
||||||
|
|
||||||
|
zi = ZipInfo(path)
|
||||||
|
oldzi = inf.getinfo(path)
|
||||||
|
try:
|
||||||
|
zi.compress_type = oldzi.compress_type
|
||||||
|
if path == "mimetype":
|
||||||
|
zi.compress_type = ZIP_STORED
|
||||||
|
zi.date_time = oldzi.date_time
|
||||||
|
zi.comment = oldzi.comment
|
||||||
|
zi.extra = oldzi.extra
|
||||||
|
zi.internal_attr = oldzi.internal_attr
|
||||||
|
zi.external_attr = oldzi.external_attr
|
||||||
|
zi.create_system = oldzi.create_system
|
||||||
|
if any(ord(c) >= 128 for c in path) or any(ord(c) >= 128 for c in zi.comment):
|
||||||
|
# If the file name or the comment contains any non-ASCII char, set the UTF8-flag
|
||||||
|
zi.flag_bits |= 0x800
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
outf.writestr(zi, data)
|
||||||
|
except:
|
||||||
|
traceback.print_exc()
|
||||||
|
return path_to_ebook
|
||||||
|
|
||||||
|
if had_elibri:
|
||||||
|
print("Watermark: Successfully stripped eLibri watermark from OPF file.")
|
||||||
|
if had_amazon:
|
||||||
|
print("Watermark: Successfully stripped Amazon watermark from OPF file.")
|
||||||
|
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def removeCDPwatermark(object, path_to_ebook):
|
||||||
|
# "META-INF/cdp.info" is a watermark file used by some Tolino vendors.
|
||||||
|
# We don't want that in our eBooks, so lets remove that file.
|
||||||
|
try:
|
||||||
|
infile = ZipFile(open(path_to_ebook, 'rb'))
|
||||||
|
namelist = infile.namelist()
|
||||||
|
if 'META-INF/cdp.info' not in namelist:
|
||||||
|
return path_to_ebook
|
||||||
|
|
||||||
|
namelist.remove("mimetype")
|
||||||
|
namelist.remove("META-INF/cdp.info")
|
||||||
|
|
||||||
|
output = object.temporary_file(".epub").name
|
||||||
|
|
||||||
|
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
|
||||||
|
with closing(ZipFile(open(output, 'wb'), 'w', **kwds)) as outf:
|
||||||
|
for path in (["mimetype"] + namelist):
|
||||||
|
|
||||||
|
data = infile.read(path)
|
||||||
|
|
||||||
|
zi = ZipInfo(path)
|
||||||
|
oldzi = infile.getinfo(path)
|
||||||
|
try:
|
||||||
|
zi.compress_type = oldzi.compress_type
|
||||||
|
if path == "mimetype":
|
||||||
|
zi.compress_type = ZIP_STORED
|
||||||
|
zi.date_time = oldzi.date_time
|
||||||
|
zi.comment = oldzi.comment
|
||||||
|
zi.extra = oldzi.extra
|
||||||
|
zi.internal_attr = oldzi.internal_attr
|
||||||
|
zi.external_attr = oldzi.external_attr
|
||||||
|
zi.create_system = oldzi.create_system
|
||||||
|
if any(ord(c) >= 128 for c in path) or any(ord(c) >= 128 for c in zi.comment):
|
||||||
|
# If the file name or the comment contains any non-ASCII char, set the UTF8-flag
|
||||||
|
zi.flag_bits |= 0x800
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
outf.writestr(zi, data)
|
||||||
|
|
||||||
|
print("Watermark: Successfully removed cdp.info watermark")
|
||||||
|
return output
|
||||||
|
|
||||||
|
except:
|
||||||
|
traceback.print_exc()
|
||||||
|
return path_to_ebook
|
||||||
157
dedrm_src/erdr2pml.py → DeDRM_plugin/erdr2pml.py
Normal file → Executable file
157
dedrm_src/erdr2pml.py → DeDRM_plugin/erdr2pml.py
Normal file → Executable 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 2008–2012 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 © 2009–2012 The Dark Reverser et al.".format(__version__)
|
print("eRdr2Pml v{0}. Copyright © 2009–2020 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))
|
||||||
|
|
||||||
@@ -7,6 +7,7 @@ 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
|
||||||
|
|
||||||
@@ -15,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
|
||||||
@@ -57,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 = []
|
||||||
@@ -94,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]) )
|
||||||
@@ -106,10 +107,10 @@ class DocParser(object):
|
|||||||
ifile.write('<!DOCTYPE svg PUBLIC "-//W3C/DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n')
|
ifile.write('<!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()
|
||||||
@@ -122,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
|
||||||
|
|
||||||
|
|
||||||
@@ -138,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
|
||||||
@@ -170,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
|
||||||
|
|
||||||
@@ -191,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
|
||||||
@@ -225,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))
|
||||||
@@ -237,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
|
||||||
@@ -267,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
|
||||||
|
|
||||||
@@ -307,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]
|
||||||
@@ -319,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)
|
||||||
@@ -368,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)
|
||||||
@@ -421,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
|
||||||
@@ -437,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')
|
||||||
|
|
||||||
@@ -453,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]
|
||||||
|
|
||||||
@@ -470,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
|
||||||
@@ -482,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 = ''
|
||||||
@@ -522,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]
|
||||||
@@ -540,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:
|
||||||
@@ -553,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
|
||||||
@@ -607,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('')
|
||||||
|
|
||||||
@@ -653,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]
|
||||||
@@ -662,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;"> </div>\n')
|
hlst.append('<div style="page-break-after: always;"> </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 :
|
||||||
@@ -715,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'
|
||||||
@@ -733,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'
|
||||||
@@ -744,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,
|
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 :
|
||||||
@@ -764,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))
|
||||||
|
|
||||||
@@ -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)
|
||||||
@@ -1,18 +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
|
||||||
|
|
||||||
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
|
||||||
@@ -84,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
|
||||||
|
|
||||||
@@ -100,24 +116,24 @@ 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('&','&')
|
str = str.replace(b'&',b'&')
|
||||||
str = str.replace('<','<')
|
str = str.replace(b'<',b'<')
|
||||||
str = str.replace('>','>')
|
str = str.replace(b'>',b'>')
|
||||||
str = str.replace('=','=')
|
str = str.replace(b'=',b'=')
|
||||||
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)) :
|
||||||
self.pos = val
|
self.pos = val
|
||||||
return self.stable[self.pos]
|
return self.stable[self.pos]
|
||||||
else:
|
else:
|
||||||
print "Error: %d outside of string table limits" % val
|
print("Error: %d outside of string table limits" % val)
|
||||||
raise TpzDRMError('outside or string table limits')
|
raise TpzDRMError('outside or string table limits')
|
||||||
# sys.exit(-1)
|
# sys.exit(-1)
|
||||||
def getSize(self):
|
def getSize(self):
|
||||||
@@ -128,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
|
||||||
@@ -139,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 = ''
|
||||||
@@ -152,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
|
||||||
@@ -166,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:
|
||||||
@@ -192,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 = []
|
||||||
@@ -204,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):
|
||||||
@@ -220,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]
|
||||||
@@ -268,32 +284,32 @@ class GlyphDict(object):
|
|||||||
def generateBook(bookDir, raw, fixedimage):
|
def generateBook(bookDir, raw, fixedimage):
|
||||||
# sanity check Topaz file extraction
|
# sanity check Topaz file extraction
|
||||||
if not os.path.exists(bookDir) :
|
if not os.path.exists(bookDir) :
|
||||||
print "Can not find directory with unencrypted book"
|
print("Can not find directory with unencrypted book")
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
dictFile = os.path.join(bookDir,'dict0000.dat')
|
dictFile = os.path.join(bookDir,'dict0000.dat')
|
||||||
if not os.path.exists(dictFile) :
|
if not os.path.exists(dictFile) :
|
||||||
print "Can not find dict0000.dat file"
|
print("Can not find dict0000.dat file")
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
pageDir = os.path.join(bookDir,'page')
|
pageDir = os.path.join(bookDir,'page')
|
||||||
if not os.path.exists(pageDir) :
|
if not os.path.exists(pageDir) :
|
||||||
print "Can not find page directory in unencrypted book"
|
print("Can not find page directory in unencrypted book")
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
imgDir = os.path.join(bookDir,'img')
|
imgDir = os.path.join(bookDir,'img')
|
||||||
if not os.path.exists(imgDir) :
|
if not os.path.exists(imgDir) :
|
||||||
print "Can not find image directory in unencrypted book"
|
print("Can not find image directory in unencrypted book")
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
glyphsDir = os.path.join(bookDir,'glyphs')
|
glyphsDir = os.path.join(bookDir,'glyphs')
|
||||||
if not os.path.exists(glyphsDir) :
|
if not os.path.exists(glyphsDir) :
|
||||||
print "Can not find glyphs directory in unencrypted book"
|
print("Can not find glyphs directory in unencrypted book")
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
metaFile = os.path.join(bookDir,'metadata0000.dat')
|
metaFile = os.path.join(bookDir,'metadata0000.dat')
|
||||||
if not os.path.exists(metaFile) :
|
if not os.path.exists(metaFile) :
|
||||||
print "Can not find metadata0000.dat in unencrypted book"
|
print("Can not find metadata0000.dat in unencrypted book")
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
svgDir = os.path.join(bookDir,'svg')
|
svgDir = os.path.join(bookDir,'svg')
|
||||||
@@ -307,10 +323,10 @@ def generateBook(bookDir, raw, fixedimage):
|
|||||||
|
|
||||||
otherFile = os.path.join(bookDir,'other0000.dat')
|
otherFile = os.path.join(bookDir,'other0000.dat')
|
||||||
if not os.path.exists(otherFile) :
|
if not os.path.exists(otherFile) :
|
||||||
print "Can not find other0000.dat in unencrypted book"
|
print("Can not find other0000.dat in unencrypted book")
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
print "Updating to color images if available"
|
print("Updating to color images if available")
|
||||||
spath = os.path.join(bookDir,'color_img')
|
spath = os.path.join(bookDir,'color_img')
|
||||||
dpath = os.path.join(bookDir,'img')
|
dpath = os.path.join(bookDir,'img')
|
||||||
filenames = os.listdir(spath)
|
filenames = os.listdir(spath)
|
||||||
@@ -319,24 +335,24 @@ 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
|
||||||
|
|
||||||
|
|
||||||
print 'Processing Dictionary'
|
print('Processing Dictionary')
|
||||||
dict = Dictionary(dictFile)
|
dict = Dictionary(dictFile)
|
||||||
|
|
||||||
print 'Processing Meta Data and creating OPF'
|
print('Processing Meta Data and creating OPF')
|
||||||
meta_array = getMetaArray(metaFile)
|
meta_array = getMetaArray(metaFile)
|
||||||
|
|
||||||
# replace special chars in title and authors like & < >
|
# replace special chars in title and authors like & < >
|
||||||
@@ -358,9 +374,9 @@ 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')
|
||||||
|
|
||||||
# get some scaling info from metadata to use while processing styles
|
# get some scaling info from metadata to use while processing styles
|
||||||
# and first page info
|
# and first page info
|
||||||
@@ -421,12 +437,12 @@ 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()
|
||||||
filenames = os.listdir(glyphsDir)
|
filenames = os.listdir(glyphsDir)
|
||||||
filenames = sorted(filenames)
|
filenames = sorted(filenames)
|
||||||
@@ -440,16 +456,16 @@ def generateBook(bookDir, raw, fixedimage):
|
|||||||
counter = 0
|
counter = 0
|
||||||
for filename in filenames:
|
for filename in filenames:
|
||||||
# print ' ', filename
|
# print ' ', filename
|
||||||
print '.',
|
print('.', end=' ')
|
||||||
fname = os.path.join(glyphsDir,filename)
|
fname = os.path.join(glyphsDir,filename)
|
||||||
flat_xml = convert2xml.fromData(dict, fname)
|
flat_xml = convert2xml.fromData(dict, fname)
|
||||||
|
|
||||||
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)
|
||||||
@@ -459,7 +475,7 @@ def generateBook(bookDir, raw, fixedimage):
|
|||||||
glyfile.write('</defs>\n')
|
glyfile.write('</defs>\n')
|
||||||
glyfile.write('</svg>\n')
|
glyfile.write('</svg>\n')
|
||||||
glyfile.close()
|
glyfile.close()
|
||||||
print " "
|
print(" ")
|
||||||
|
|
||||||
|
|
||||||
# start up the html
|
# start up the html
|
||||||
@@ -481,7 +497,7 @@ def generateBook(bookDir, raw, fixedimage):
|
|||||||
hlst.append('<link href="style.css" rel="stylesheet" type="text/css" />\n')
|
hlst.append('<link href="style.css" rel="stylesheet" type="text/css" />\n')
|
||||||
hlst.append('</head>\n<body>\n')
|
hlst.append('</head>\n<body>\n')
|
||||||
|
|
||||||
print 'Processing Pages'
|
print('Processing Pages')
|
||||||
# Books are at 1440 DPI. This is rendering at twice that size for
|
# Books are at 1440 DPI. This is rendering at twice that size for
|
||||||
# readability when rendering to the screen.
|
# readability when rendering to the screen.
|
||||||
scaledpi = 1440.0
|
scaledpi = 1440.0
|
||||||
@@ -495,7 +511,7 @@ def generateBook(bookDir, raw, fixedimage):
|
|||||||
|
|
||||||
for filename in filenames:
|
for filename in filenames:
|
||||||
# print ' ', filename
|
# print ' ', filename
|
||||||
print ".",
|
print(".", end=' ')
|
||||||
fname = os.path.join(pageDir,filename)
|
fname = os.path.join(pageDir,filename)
|
||||||
flat_xml = convert2xml.fromData(dict, fname)
|
flat_xml = convert2xml.fromData(dict, fname)
|
||||||
|
|
||||||
@@ -504,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)
|
||||||
@@ -515,10 +531,10 @@ 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')
|
||||||
|
|
||||||
# first create a table of contents file for the svg images
|
# first create a table of contents file for the svg images
|
||||||
tlst = []
|
tlst = []
|
||||||
@@ -550,7 +566,7 @@ def generateBook(bookDir, raw, fixedimage):
|
|||||||
toclst = tocentries.split('\n')
|
toclst = tocentries.split('\n')
|
||||||
toclst.pop()
|
toclst.pop()
|
||||||
for entry in toclst:
|
for entry in toclst:
|
||||||
print entry
|
print(entry)
|
||||||
title, pagenum = entry.split('|')
|
title, pagenum = entry.split('|')
|
||||||
id = pageidnums[int(pagenum)]
|
id = pageidnums[int(pagenum)]
|
||||||
if (raw):
|
if (raw):
|
||||||
@@ -561,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
|
||||||
@@ -580,7 +596,7 @@ def generateBook(bookDir, raw, fixedimage):
|
|||||||
slst.append('</head>\n')
|
slst.append('</head>\n')
|
||||||
slst.append('<body>\n')
|
slst.append('<body>\n')
|
||||||
|
|
||||||
print "Building svg images of each book page"
|
print("Building svg images of each book page")
|
||||||
slst.append('<h2>List of Pages</h2>\n')
|
slst.append('<h2>List of Pages</h2>\n')
|
||||||
slst.append('<div>\n')
|
slst.append('<div>\n')
|
||||||
idlst = sorted(pageIDMap.keys())
|
idlst = sorted(pageIDMap.keys())
|
||||||
@@ -593,12 +609,12 @@ def generateBook(bookDir, raw, fixedimage):
|
|||||||
nextid = idlst[j+1]
|
nextid = idlst[j+1]
|
||||||
else:
|
else:
|
||||||
nextid = None
|
nextid = None
|
||||||
print '.',
|
print('.', end=' ')
|
||||||
pagelst = pageIDMap[pageid]
|
pagelst = pageIDMap[pageid]
|
||||||
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) :
|
||||||
@@ -616,9 +632,9 @@ 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(" ")
|
||||||
|
|
||||||
# build the opf file
|
# build the opf file
|
||||||
opfname = os.path.join(bookDir, 'book.opf')
|
opfname = os.path.join(bookDir, 'book.opf')
|
||||||
@@ -627,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')
|
||||||
@@ -665,25 +681,27 @@ 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')
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def usage():
|
def usage():
|
||||||
print "genbook.py generates a book from the extract Topaz Files"
|
print("genbook.py generates a book from the extract Topaz Files")
|
||||||
print "Usage:"
|
print("Usage:")
|
||||||
print " genbook.py [-r] [-h [--fixed-image] <bookDir> "
|
print(" genbook.py [-r] [-h [--fixed-image] <bookDir> ")
|
||||||
print " "
|
print(" ")
|
||||||
print "Options:"
|
print("Options:")
|
||||||
print " -h : help - print this usage message"
|
print(" -h : help - print this usage message")
|
||||||
print " -r : generate raw svg files (not wrapped in xhtml)"
|
print(" -r : generate raw svg files (not wrapped in xhtml)")
|
||||||
print " --fixed-image : genearate any Fixed Area as an svg image in the html"
|
print(" --fixed-image : genearate any Fixed Area as an svg image in the html")
|
||||||
print " "
|
print(" ")
|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
@@ -691,8 +709,8 @@ 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
|
||||||
|
|
||||||
@@ -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 2010–2013 by some_updates, DiapDealer and Apprentice Alf
|
|
||||||
# Modified 2015–2017 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,17 +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.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__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
|
||||||
@@ -64,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)
|
||||||
|
|
||||||
@@ -105,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):
|
||||||
@@ -166,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')
|
||||||
@@ -179,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)
|
||||||
@@ -222,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
|
||||||
|
|
||||||
@@ -255,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)
|
||||||
@@ -272,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)
|
||||||
@@ -315,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
|
||||||
|
|
||||||
@@ -326,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
|
||||||
|
|
||||||
@@ -418,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
|
||||||
|
|
||||||
@@ -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,13 +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
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__version__ = "1.1"
|
__version__ = "2.0"
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
@@ -38,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)
|
||||||
|
|
||||||
@@ -79,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
|
||||||
@@ -97,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
|
||||||
@@ -198,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))
|
||||||
|
|
||||||
@@ -209,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():
|
||||||
@@ -239,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)
|
||||||
|
|
||||||
@@ -273,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
|
||||||
@@ -307,24 +314,24 @@ def gui_main():
|
|||||||
keys = nookkeys()
|
keys = nookkeys()
|
||||||
keycount = 0
|
keycount = 0
|
||||||
for key in keys:
|
for key in keys:
|
||||||
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
|
||||||
@@ -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,13 +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
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__version__ = "1.1"
|
__version__ = "2.0"
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
@@ -45,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)
|
||||||
|
|
||||||
@@ -86,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):
|
||||||
@@ -102,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)
|
||||||
@@ -130,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)
|
||||||
@@ -155,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
|
||||||
|
|
||||||
@@ -224,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
|
||||||
|
|
||||||
@@ -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 2010–2013 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,17 +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.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__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
|
||||||
@@ -56,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)
|
||||||
|
|
||||||
@@ -97,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):
|
||||||
@@ -198,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():
|
||||||
@@ -223,12 +227,12 @@ def cli_main():
|
|||||||
argv=unicode_argv()
|
argv=unicode_argv()
|
||||||
progname = os.path.basename(argv[0])
|
progname = os.path.basename(argv[0])
|
||||||
if AES is None:
|
if AES is None:
|
||||||
print "%s: This script requires OpenSSL or PyCrypto, which must be installed " \
|
print("%s: 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." % \
|
||||||
(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)
|
||||||
@@ -238,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
|
||||||
|
|
||||||
@@ -294,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
|
||||||
|
|
||||||
2199
DeDRM_plugin/ignoblepdf.py
Normal file
2199
DeDRM_plugin/ignoblepdf.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,26 +1,12 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from __future__ import with_statement
|
# ineptepub.py
|
||||||
|
# Copyright © 2009-2020 by i♥cabbages, Apprentice Harper et al.
|
||||||
# ineptepub.pyw, version 6.6
|
|
||||||
# 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 2010–2013 by some_updates, DiapDealer and Apprentice Alf
|
|
||||||
# Modified 2015–2017 by Apprentice Harper
|
|
||||||
|
|
||||||
# Windows users: Before running this program, you must first install Python 2.7
|
|
||||||
# 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.7). 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
|
||||||
@@ -43,14 +29,16 @@ from __future__ import with_statement
|
|||||||
# 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.
|
||||||
|
# 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.6"
|
__version__ = "7.0"
|
||||||
|
|
||||||
|
import codecs
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import traceback
|
import traceback
|
||||||
@@ -58,7 +46,7 @@ import zlib
|
|||||||
import zipfile
|
import zipfile
|
||||||
from zipfile import ZipInfo, ZipFile, ZIP_STORED, ZIP_DEFLATED
|
from zipfile import ZipInfo, ZipFile, ZIP_STORED, ZIP_DEFLATED
|
||||||
from contextlib import closing
|
from contextlib import closing
|
||||||
import xml.etree.ElementTree as etree
|
from lxml import etree
|
||||||
|
|
||||||
# Wrap a stream so that output gets flushed immediately
|
# Wrap a stream so that output gets flushed immediately
|
||||||
# and also make sure that any unicode strings get
|
# and also make sure that any unicode strings get
|
||||||
@@ -70,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,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)
|
||||||
|
|
||||||
@@ -111,18 +106,19 @@ 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 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
|
||||||
@@ -205,7 +201,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 ADEPTError('AES decryption failed')
|
raise ADEPTError('AES decryption failed')
|
||||||
@@ -216,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):
|
||||||
@@ -307,26 +304,26 @@ 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 xrange(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)
|
||||||
|
|
||||||
def bytesToNumber(self, bytes):
|
def bytesToNumber(self, bytes):
|
||||||
total = 0L
|
total = 0
|
||||||
for byte in bytes:
|
for byte in bytes:
|
||||||
total = (total << 8) + byte
|
total = (total << 8) + byte
|
||||||
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)
|
||||||
|
|
||||||
@@ -345,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#'}
|
||||||
|
|
||||||
@@ -355,26 +352,56 @@ 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('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:
|
||||||
data = self._aes.decrypt(data)[16:]
|
data = self._aes.decrypt(data)[16:]
|
||||||
data = data[:-ord(data[-1])]
|
if type(data[-1]) != int:
|
||||||
|
place = ord(data[-1])
|
||||||
|
else:
|
||||||
|
place = data[-1]
|
||||||
|
data = data[:-place]
|
||||||
data = self.decompress(data)
|
data = self.decompress(data)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@@ -397,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)
|
||||||
@@ -414,38 +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 = rsa.decrypt(bookkey.decode('base64'))
|
bookkey = rsa.decrypt(codecs.decode(bookkey.encode('ascii'), 'base64'))
|
||||||
# Padded as per RSAES-PKCS1-v1_5
|
# Padded as per RSAES-PKCS1-v1_5
|
||||||
if bookkey[-17] != '\x00':
|
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)
|
||||||
@@ -457,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
|
||||||
|
|
||||||
@@ -472,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 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"adeptkey.der"):
|
if os.path.exists("adeptkey.der"):
|
||||||
self.keypath.insert(0, u"adeptkey.der")
|
self.keypath.insert(0, "adeptkey.der")
|
||||||
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 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, 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 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, 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
|
||||||
|
|
||||||
@@ -564,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, 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 = 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=Tkconstants.X, expand=1)
|
DecryptionDialog(root).pack(fill=tkinter.constants.X, expand=1)
|
||||||
root.mainloop()
|
root.mainloop()
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
761
dedrm_src/ineptpdf.py → DeDRM_plugin/ineptpdf.py
Normal file → Executable file
761
dedrm_src/ineptpdf.py → DeDRM_plugin/ineptpdf.py
Normal file → Executable file
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,25 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Pascal implementation by lulzkabulz. Python translation by apprenticenaomi. DeDRM integration by anon.
|
|
||||||
# BinaryIon.pas + DrmIon.pas + IonSymbols.pas
|
|
||||||
|
|
||||||
from __future__ import with_statement
|
# ion.py
|
||||||
|
# Copyright © 2013-2020 Apprentice Harper et al.
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__version__ = '3.0'
|
||||||
|
|
||||||
|
# Revision history:
|
||||||
|
# Pascal implementation by lulzkabulz.
|
||||||
|
# BinaryIon.pas + DrmIon.pas + IonSymbols.pas
|
||||||
|
# 1.0 - Python translation by apprenticenaomi.
|
||||||
|
# 1.1 - DeDRM integration by anon.
|
||||||
|
# 1.2 - Added pylzma import fallback
|
||||||
|
# 1.3 - Fixed lzma support for calibre 4.6+
|
||||||
|
# 2.0 - VoucherEnvelope v2/v3 support by apprenticesakuya.
|
||||||
|
# 3.0 - Added Python 3 compatibility for calibre 5.0
|
||||||
|
|
||||||
|
"""
|
||||||
|
Decrypt Kindle KFX files.
|
||||||
|
"""
|
||||||
|
|
||||||
import collections
|
import collections
|
||||||
import hashlib
|
import hashlib
|
||||||
@@ -12,28 +28,30 @@ 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 2.35.0 or later
|
# lzma library from calibre 4.6.0 or later
|
||||||
import lzma.lzma1 as calibre_lzma
|
import calibre_lzma.lzma1 as calibre_lzma
|
||||||
except ImportError:
|
except ImportError:
|
||||||
calibre_lzma = None
|
calibre_lzma = None
|
||||||
|
# lzma library from calibre 2.35.0 or later
|
||||||
try:
|
try:
|
||||||
import lzma
|
import lzma.lzma1 as calibre_lzma
|
||||||
except ImportError:
|
except ImportError:
|
||||||
# Need pip backports.lzma on Python <3.3
|
calibre_lzma = None
|
||||||
try:
|
try:
|
||||||
from backports import lzma
|
import lzma
|
||||||
except ImportError:
|
except ImportError:
|
||||||
# Windows-friendly choice: pylzma wheels
|
# Need pip backports.lzma on Python <3.3
|
||||||
import pylzma as lzma
|
try:
|
||||||
|
from backports import lzma
|
||||||
|
except ImportError:
|
||||||
|
# Windows-friendly choice: pylzma wheels
|
||||||
|
import pylzma as lzma
|
||||||
|
|
||||||
|
|
||||||
TID_NULL = 0
|
TID_NULL = 0
|
||||||
@@ -71,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
|
||||||
@@ -329,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
|
||||||
|
|
||||||
@@ -354,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
|
||||||
|
|
||||||
@@ -371,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
|
||||||
|
|
||||||
@@ -396,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
|
||||||
@@ -494,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)
|
||||||
|
|
||||||
@@ -561,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
|
||||||
@@ -631,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]
|
||||||
@@ -714,7 +734,11 @@ SYM_NAMES = [ 'com.amazon.drm.Envelope@1.0',
|
|||||||
'com.amazon.drm.EnvelopeMetadata@2.0',
|
'com.amazon.drm.EnvelopeMetadata@2.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', 'priority', 'refines']
|
'com.amazon.drm.Compressed@1.0', 'page_index_table',
|
||||||
|
] + ['com.amazon.drm.VoucherEnvelope@%d.0' % n
|
||||||
|
for n in list(range(2, 29)) + [
|
||||||
|
9708, 1031, 2069, 9041, 3646,
|
||||||
|
6052, 9479, 9888, 4648, 5683]]
|
||||||
|
|
||||||
def addprottable(ion):
|
def addprottable(ion):
|
||||||
ion.addtocatalog("ProtectedData", 1, SYM_NAMES)
|
ion.addtocatalog("ProtectedData", 1, SYM_NAMES)
|
||||||
@@ -729,15 +753,84 @@ 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")
|
||||||
|
|
||||||
return msg[:-paddinglen]
|
return msg[:-paddinglen]
|
||||||
|
|
||||||
|
|
||||||
|
# every VoucherEnvelope version has a corresponding "word" and magic number, used in obfuscating the shared secret
|
||||||
|
OBFUSCATION_TABLE = {
|
||||||
|
"V1": (0x00, None),
|
||||||
|
"V2": (0x05, b'Antidisestablishmentarianism'),
|
||||||
|
"V3": (0x08, b'Floccinaucinihilipilification'),
|
||||||
|
"V4": (0x07, b'>\x14\x0c\x12\x10-\x13&\x18U\x1d\x05Rlt\x03!\x19\x1b\x13\x04]Y\x19,\t\x1b'),
|
||||||
|
"V5": (0x06, b'~\x18~\x16J\\\x18\x10\x05\x0b\x07\t\x0cZ\r|\x1c\x15\x1d\x11>,\x1b\x0e\x03"4\x1b\x01'),
|
||||||
|
"V6": (0x09, b'3h\x055\x03[^>\x19\x1c\x08\x1b\rtm4\x02Rp\x0c\x16B\n'),
|
||||||
|
"V7": (0x05, b'\x10\x1bJ\x18\nh!\x10"\x03>Z\'\r\x01]W\x06\x1c\x1e?\x0f\x13'),
|
||||||
|
"V8": (0x09, b"K\x0c6\x1d\x1a\x17pO}Rk\x1d'w1^\x1f$\x1c{C\x02Q\x06\x1d`"),
|
||||||
|
"V9": (0x05, b'X.\x0eW\x1c*K\x12\x12\t\n\n\x17Wx\x01\x02Yf\x0f\x18\x1bVXPi\x01'),
|
||||||
|
"V10": (0x07, b'z3\n\x039\x12\x13`\x06=v,\x02MTK\x1e%}L\x1c\x1f\x15\x0c\x11\x02\x0c\n8\x17p'),
|
||||||
|
"V11": (0x05, b'L=\nhVm\x07go\n6\x14\x06\x16L\r\x02\x0b\x0c\x1b\x04#p\t'),
|
||||||
|
"V12": (0x06, b',n\x1d\rl\x13\x1c\x13\x16p\x14\x07U\x0c\x1f\x19w\x16\x16\x1d5T'),
|
||||||
|
"V13": (0x07, b'I\x05\t\x08\x03r)\x01$N\x0fr3n\x0b062D\x0f\x13'),
|
||||||
|
"V14": (0x05, b"\x03\x02\x1c9\x19\x15\x15q\x1057\x08\x16\x0cF\x1b.Fw\x01\x12\x03\x13\x02\x17S'hk6"),
|
||||||
|
"V15": (0x0A, b'&,4B\x1dcI\x0bU\x03I\x07\x04\x1c\t\x05c\x07%ws\x0cj\t\x1a\x08\x0f'),
|
||||||
|
"V16": (0x0A, b'\x06\x18`h,b><\x06PqR\x02Zc\x034\n\x16\x1e\x18\x06#e'),
|
||||||
|
"V17": (0x07, b'y\r\x12\x08fw.[\x02\t\n\x13\x11\x0c\x11b\x1e8L\x10(\x13<Jx6c\x0f'),
|
||||||
|
"V18": (0x07, b'I\x0b\x0e,\x19\x1aIa\x10s\x19g\\\x1b\x11!\x18yf\x0f\t\x1d7[bSp\x03'),
|
||||||
|
"V19": (0x05, b'\n6>)N\x02\x188\x016s\x13\x14\x1b\x16jeN\n\x146\x04\x18\x1c\x0c\x19\x1f,\x02]'),
|
||||||
|
"V20": (0x08, b'_\r\x01\x12]\\\x14*\x17i\x14\r\t!\x1e,~hZ\x12jK\x17\x1e*1'),
|
||||||
|
"V21": (0x07, b'e\x1d\x19|\ty\x1di|N\x13\x0e\x04\x1bj<h\x13\x15k\x12\x08=\x1f\x16~\x13l'),
|
||||||
|
"V22": (0x08, b'?\x17yi$k7Pc\tEo\x0c\x07\x07\t\x1f,*i\x12\x0cI0\x10I\x1a?2\x04'),
|
||||||
|
"V23": (0x08, b'\x16+db\x13\x04\x18\rc%\x14\x17\x0f\x13F\x0c[\t9\x1ay\x01\x1eH'),
|
||||||
|
"V24": (0x06, b'|6\\\x1a\r\x10\nP\x07\x0fu\x1f\t,\rr`uv\\~55\x11]N'),
|
||||||
|
"V25": (0x09, b'\x07\x14w\x1e,^y\x01:\x08\x07\x1fr\tU#j\x16\x12\x1eB\x04\x16=\x06fZ\x07\x02\x06'),
|
||||||
|
"V26": (0x06, b'\x03IL\x1e"K\x1f\x0f\x1fp0\x01`X\x02z0`\x03\x0eN\x07'),
|
||||||
|
"V27": (0x07, b'Xk\x10y\x02\x18\x10\x17\x1d,\x0e\x05e\x10\x15"e\x0fh(\x06s\x1c\x08I\x0c\x1b\x0e'),
|
||||||
|
"V28": (0x0A, b'6P\x1bs\x0f\x06V.\x1cM\x14\x02\n\x1b\x07{P0:\x18zaU\x05'),
|
||||||
|
"V9708": (0x05, b'\x1diIm\x08a\x17\x1e!am\x1d\x1aQ.\x16!\x06*\}x04\x11\t\x06\x04?'),
|
||||||
|
"V1031": (0x08, b'Antidisestablishmentarianism'),
|
||||||
|
"V2069": (0x07, b'Floccinaucinihilipilification'),
|
||||||
|
"V9041": (0x06, b'>\x14\x0c\x12\x10-\x13&\x18U\x1d\x05Rlt\x03!\x19\x1b\x13\x04]Y\x19,\t\x1b'),
|
||||||
|
"V3646": (0x09, b'~\x18~\x16J\\\x18\x10\x05\x0b\x07\t\x0cZ\r|\x1c\x15\x1d\x11>,\x1b\x0e\x03"4\x1b\x01'),
|
||||||
|
"V6052": (0x05, b'3h\x055\x03[^>\x19\x1c\x08\x1b\rtm4\x02Rp\x0c\x16B\n'),
|
||||||
|
"V9479": (0x09, b'\x10\x1bJ\x18\nh!\x10"\x03>Z\'\r\x01]W\x06\x1c\x1e?\x0f\x13'),
|
||||||
|
"V9888": (0x05, b"K\x0c6\x1d\x1a\x17pO}Rk\x1d'w1^\x1f$\x1c{C\x02Q\x06\x1d`"),
|
||||||
|
"V4648": (0x07, b'X.\x0eW\x1c*K\x12\x12\t\n\n\x17Wx\x01\x02Yf\x0f\x18\x1bVXPi\x01'),
|
||||||
|
"V5683": (0x05, b'z3\n\x039\x12\x13`\x06=v,\x02MTK\x1e%}L\x1c\x1f\x15\x0c\x11\x02\x0c\n8\x17p'),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# obfuscate shared secret according to the VoucherEnvelope version
|
||||||
|
def obfuscate(secret, version):
|
||||||
|
if version == 1: # v1 does not use obfuscation
|
||||||
|
return secret
|
||||||
|
|
||||||
|
magic, word = OBFUSCATION_TABLE["V%d" % version]
|
||||||
|
|
||||||
|
# extend secret so that its length is divisible by the magic number
|
||||||
|
if len(secret) % magic != 0:
|
||||||
|
secret = secret + b'\x00' * (magic - len(secret) % magic)
|
||||||
|
|
||||||
|
secret = bytearray(secret)
|
||||||
|
|
||||||
|
obfuscated = bytearray(len(secret))
|
||||||
|
wordhash = bytearray(hashlib.sha256(word).digest())
|
||||||
|
|
||||||
|
# shuffle secret and xor it with the first half of the word hash
|
||||||
|
for i in range(0, len(secret)):
|
||||||
|
index = i // (len(secret) // magic) + magic * (i % (len(secret) // magic))
|
||||||
|
obfuscated[index] = secret[i] ^ wordhash[index % 16]
|
||||||
|
|
||||||
|
return obfuscated
|
||||||
|
|
||||||
|
|
||||||
class DrmIonVoucher(object):
|
class DrmIonVoucher(object):
|
||||||
envelope = None
|
envelope = None
|
||||||
|
version = None
|
||||||
voucher = None
|
voucher = None
|
||||||
drmkey = None
|
drmkey = None
|
||||||
license_type = "Unknown"
|
license_type = "Unknown"
|
||||||
@@ -753,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 = []
|
||||||
|
|
||||||
@@ -761,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 = shared.encode("UTF-8")
|
sharedsecret = obfuscate(shared, self.version)
|
||||||
|
|
||||||
key = hmac.new(sharedsecret, sharedsecret[:5], 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",
|
||||||
@@ -809,15 +908,16 @@ class DrmIonVoucher(object):
|
|||||||
def parse(self):
|
def parse(self):
|
||||||
self.envelope.reset()
|
self.envelope.reset()
|
||||||
_assert(self.envelope.hasnext(), "Envelope is empty")
|
_assert(self.envelope.hasnext(), "Envelope is empty")
|
||||||
_assert(self.envelope.next() == TID_STRUCT and self.envelope.gettypename() == "com.amazon.drm.VoucherEnvelope@1.0",
|
_assert(self.envelope.next() == TID_STRUCT and str.startswith(self.envelope.gettypename(), "com.amazon.drm.VoucherEnvelope@"),
|
||||||
"Unknown type encountered in envelope, expected VoucherEnvelope")
|
"Unknown type encountered in envelope, expected VoucherEnvelope")
|
||||||
|
self.version = int(self.envelope.gettypename().split('@')[1][:-2])
|
||||||
|
|
||||||
self.envelope.stepin()
|
self.envelope.stepin()
|
||||||
while self.envelope.hasnext():
|
while self.envelope.hasnext():
|
||||||
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":
|
||||||
@@ -938,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()
|
||||||
@@ -951,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()
|
||||||
@@ -962,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:
|
||||||
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
@@ -1,27 +1,27 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from __future__ import with_statement
|
|
||||||
|
|
||||||
# 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:
|
||||||
@@ -38,58 +38,64 @@ 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:
|
||||||
for dsn_len,secret_len in [(0,0), (16,0), (16,40), (32,40), (40,40)]:
|
# Belt and braces. PIDs should be unicode strings, but just in case...
|
||||||
|
if isinstance(pid, bytes):
|
||||||
|
pid = pid.decode('ascii')
|
||||||
|
for dsn_len,secret_len in [(0,0), (16,0), (16,40), (32,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
|
||||||
else:
|
else:
|
||||||
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
|
||||||
|
|
||||||
@@ -1,18 +1,18 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from __future__ import with_statement
|
|
||||||
|
|
||||||
# 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
|
||||||
@@ -30,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
|
||||||
@@ -49,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
|
||||||
@@ -83,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):
|
||||||
@@ -94,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
|
||||||
@@ -117,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
|
||||||
|
|
||||||
@@ -125,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):
|
||||||
@@ -149,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
|
||||||
|
|
||||||
@@ -159,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)
|
||||||
@@ -188,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)
|
||||||
|
|
||||||
@@ -205,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:
|
||||||
@@ -295,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
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -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,6 +10,8 @@
|
|||||||
# 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
|
||||||
|
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import binascii
|
import binascii
|
||||||
@@ -24,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)
|
||||||
|
|
||||||
@@ -62,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'
|
||||||
|
|
||||||
@@ -82,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)
|
||||||
@@ -98,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
|
||||||
|
|
||||||
|
|
||||||
218
dedrm_src/mobidedrm.py → DeDRM_plugin/mobidedrm.py
Normal file → Executable file
218
dedrm_src/mobidedrm.py → DeDRM_plugin/mobidedrm.py
Normal file → Executable file
@@ -1,12 +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 © 2008–2017 Apprentice Harper et al.
|
# Portions © 2008–2020 Apprentice Harper et al.
|
||||||
|
|
||||||
|
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.
|
||||||
@@ -72,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
|
||||||
@@ -80,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
|
||||||
@@ -92,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)
|
||||||
|
|
||||||
@@ -130,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):
|
||||||
@@ -164,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)
|
||||||
@@ -203,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
|
||||||
@@ -225,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
|
||||||
|
|
||||||
|
|
||||||
@@ -244,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) )
|
||||||
@@ -281,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.
|
||||||
@@ -300,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:
|
||||||
@@ -338,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)
|
||||||
@@ -370,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)
|
||||||
@@ -392,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)
|
||||||
@@ -404,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]
|
||||||
@@ -465,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 . . .",
|
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".",
|
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:
|
||||||
@@ -500,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
|
||||||
@@ -516,23 +546,23 @@ 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]
|
||||||
outfile = argv[2]
|
outfile = argv[2]
|
||||||
if len(argv) is 4:
|
if len(argv) == 4:
|
||||||
pidlist = argv[3].split(',')
|
pidlist = argv[3].split(',')
|
||||||
else:
|
else:
|
||||||
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
|
||||||
|
|
||||||
@@ -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_src/prefs.py → DeDRM_plugin/prefs.py
Normal file → Executable file
61
dedrm_src/prefs.py → DeDRM_plugin/prefs.py
Normal file → Executable 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))
|
||||||
@@ -11,7 +11,7 @@ def load_pycrypto():
|
|||||||
class DES(object):
|
class DES(object):
|
||||||
def __init__(self, key):
|
def __init__(self, key):
|
||||||
if len(key) != 8 :
|
if len(key) != 8 :
|
||||||
raise Error('DES improper key used')
|
raise ValueError('DES improper key used')
|
||||||
self.key = key
|
self.key = key
|
||||||
self._des = _DES.new(key,_DES.MODE_ECB)
|
self._des = _DES.new(key,_DES.MODE_ECB)
|
||||||
def desdecrypt(self, data):
|
def desdecrypt(self, data):
|
||||||
@@ -1,18 +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
|
||||||
|
|
||||||
|
|
||||||
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 = ''
|
||||||
@@ -23,7 +24,7 @@ def decryptepub(infile, outdir, rscpath):
|
|||||||
zippath = os.path.join(bpath,name + '_temp.zip')
|
zippath = os.path.join(bpath,name + '_temp.zip')
|
||||||
rv = zipfix.repairBook(infile, zippath)
|
rv = zipfix.repairBook(infile, zippath)
|
||||||
if rv != 0:
|
if rv != 0:
|
||||||
print "Error while trying to fix epub"
|
print("Error while trying to fix epub")
|
||||||
return rv
|
return rv
|
||||||
|
|
||||||
# determine a good name for the output file
|
# determine a good name for the output file
|
||||||
@@ -43,9 +44,9 @@ def decryptepub(infile, outdir, rscpath):
|
|||||||
try:
|
try:
|
||||||
rv = ineptepub.decryptBook(userkey, zippath, outfile)
|
rv = ineptepub.decryptBook(userkey, zippath, outfile)
|
||||||
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
|
||||||
@@ -63,23 +64,23 @@ def decryptepub(infile, outdir, rscpath):
|
|||||||
try:
|
try:
|
||||||
rv = ignobleepub.decryptBook(userkey, zippath, outfile)
|
rv = ignobleepub.decryptBook(userkey, zippath, outfile)
|
||||||
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
|
||||||
else:
|
else:
|
||||||
encryption = epubtest.encryption(zippath)
|
encryption = epubtest.encryption(zippath)
|
||||||
if encryption == "Unencrypted":
|
if encryption == "Unencrypted":
|
||||||
print "{0} is not DRMed.".format(name)
|
print("{0} is not DRMed.".format(name))
|
||||||
rv = 0
|
rv = 0
|
||||||
else:
|
else:
|
||||||
print "{0} has an unknown encryption.".format(name)
|
print("{0} has an unknown encryption.".format(name))
|
||||||
|
|
||||||
os.remove(zippath)
|
os.remove(zippath)
|
||||||
if rv != 0:
|
if rv != 0:
|
||||||
print errlog
|
print(errlog)
|
||||||
return rv
|
return rv
|
||||||
|
|
||||||
|
|
||||||
@@ -103,17 +104,18 @@ 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
|
||||||
|
|
||||||
if rv != 0:
|
if rv != 0:
|
||||||
print errlog
|
print(errlog)
|
||||||
return rv
|
return rv
|
||||||
|
|
||||||
|
|
||||||
def decryptpdb(infile, outdir, rscpath):
|
def decryptpdb(infile, outdir, rscpath):
|
||||||
|
errlog = ''
|
||||||
outname = os.path.splitext(os.path.basename(infile))[0] + ".pmlz"
|
outname = os.path.splitext(os.path.basename(infile))[0] + ".pmlz"
|
||||||
outpath = os.path.join(outdir, outname)
|
outpath = os.path.join(outdir, outname)
|
||||||
rv = 1
|
rv = 1
|
||||||
@@ -126,11 +128,11 @@ def decryptpdb(infile, outdir, rscpath):
|
|||||||
try:
|
try:
|
||||||
name, cc8 = i.split(':')
|
name, cc8 = i.split(':')
|
||||||
except ValueError:
|
except ValueError:
|
||||||
print ' Error parsing user supplied social drm data.'
|
print(' Error parsing user supplied social drm data.')
|
||||||
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
|
||||||
@@ -141,6 +143,7 @@ def decryptpdb(infile, outdir, rscpath):
|
|||||||
|
|
||||||
|
|
||||||
def decryptk4mobi(infile, outdir, rscpath):
|
def decryptk4mobi(infile, outdir, rscpath):
|
||||||
|
errlog = ''
|
||||||
rv = 1
|
rv = 1
|
||||||
pidnums = []
|
pidnums = []
|
||||||
pidspath = os.path.join(rscpath,'pidlist.txt')
|
pidspath = os.path.join(rscpath,'pidlist.txt')
|
||||||
@@ -190,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
|
||||||
@@ -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':
|
||||||
@@ -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
|
||||||
@@ -2,6 +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
|
||||||
|
|
||||||
|
|
||||||
import csv
|
import csv
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
@@ -14,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;',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -57,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
|
||||||
@@ -75,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 :
|
||||||
@@ -86,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)
|
||||||
@@ -108,81 +111,84 @@ 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)
|
||||||
|
|
||||||
cssargs = {}
|
cssargs = {}
|
||||||
|
|
||||||
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:
|
||||||
print "Warning: unrecognised val, ignoring"
|
print("Warning: unrecognised val, ignoring")
|
||||||
val = 0
|
val = 0
|
||||||
pv = float(val)/scale
|
pv = float(val)/scale
|
||||||
cssargs[attr] = (self.attr_val_map[attr], pv)
|
cssargs[attr] = (self.attr_val_map[attr], pv)
|
||||||
@@ -194,35 +200,35 @@ class DocParser(object):
|
|||||||
if aftclass != "" : keep = False
|
if aftclass != "" : keep = False
|
||||||
|
|
||||||
if keep :
|
if keep :
|
||||||
if debug: print 'keeping style'
|
if debug: print('keeping style')
|
||||||
# make sure line-space does not go below 100% or above 300% since
|
# 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):
|
||||||
@@ -266,15 +272,15 @@ class DocParser(object):
|
|||||||
|
|
||||||
def convert2CSS(flatxml, fontsize, ph, pw):
|
def convert2CSS(flatxml, fontsize, ph, pw):
|
||||||
|
|
||||||
print ' ', 'Using font size:',fontsize
|
print(' ', 'Using font size:',fontsize)
|
||||||
print ' ', 'Using page height:', ph
|
print(' ', 'Using page height:', ph)
|
||||||
print ' ', 'Using page width:', pw
|
print(' ', 'Using page width:', pw)
|
||||||
|
|
||||||
# create a document parser
|
# create a document parser
|
||||||
dp = DocParser(flatxml, fontsize, ph, pw)
|
dp = DocParser(flatxml, fontsize, ph, pw)
|
||||||
if debug: print ' ', 'Created DocParser.'
|
if debug: print(' ', 'Created DocParser.')
|
||||||
csspage = dp.process()
|
csspage = dp.process()
|
||||||
if debug: print ' ', 'Processed DocParser.'
|
if debug: print(' ', 'Processed DocParser.')
|
||||||
return csspage
|
return csspage
|
||||||
|
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# topazextract.py
|
# topazextract.py
|
||||||
@@ -7,8 +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
|
||||||
|
|
||||||
__version__ = '5.0'
|
__version__ = '6.0'
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import os, csv, getopt
|
import os, csv, getopt
|
||||||
@@ -16,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
|
||||||
@@ -25,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)
|
||||||
|
|
||||||
@@ -63,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
|
||||||
@@ -91,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:
|
||||||
@@ -167,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)
|
||||||
@@ -189,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
|
||||||
@@ -203,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()
|
||||||
|
|
||||||
@@ -213,7 +225,7 @@ class TopazBook:
|
|||||||
# Read and return the data of one header record at the current book file position
|
# Read and return the data of one header record at the current book file position
|
||||||
# [[offset,decompressedLength,compressedLength],...]
|
# [[offset,decompressedLength,compressedLength],...]
|
||||||
nbValues = bookReadEncodedNumber(self.fo)
|
nbValues = bookReadEncodedNumber(self.fo)
|
||||||
if debug: print "%d records in header " % nbValues,
|
if debug: print("%d records in header " % nbValues, end=' ')
|
||||||
values = []
|
values = []
|
||||||
for i in range (0,nbValues):
|
for i in range (0,nbValues):
|
||||||
values.append([bookReadEncodedNumber(self.fo),bookReadEncodedNumber(self.fo),bookReadEncodedNumber(self.fo)])
|
values.append([bookReadEncodedNumber(self.fo),bookReadEncodedNumber(self.fo),bookReadEncodedNumber(self.fo)])
|
||||||
@@ -222,50 +234,50 @@ 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]
|
||||||
nbRecords = bookReadEncodedNumber(self.fo)
|
nbRecords = bookReadEncodedNumber(self.fo)
|
||||||
if debug: print "Headers: %d" % nbRecords
|
if debug: print("Headers: %d" % nbRecords)
|
||||||
for i in range (0,nbRecords):
|
for i in range (0,nbRecords):
|
||||||
result = parseTopazHeaderRecord()
|
result = parseTopazHeaderRecord()
|
||||||
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)
|
||||||
for i in range (0,nbRecords) :
|
for i in range (0,nbRecords) :
|
||||||
keyval = bookReadString(self.fo)
|
keyval = bookReadString(self.fo)
|
||||||
content = bookReadString(self.fo)
|
content = bookReadString(self.fo)
|
||||||
if debug: print keyval
|
if debug: print(keyval)
|
||||||
if debug: print content
|
if debug: print(content)
|
||||||
self.bookMetadata[keyval] = content
|
self.bookMetadata[keyval] = content
|
||||||
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):
|
||||||
@@ -317,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:
|
||||||
@@ -331,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
|
||||||
@@ -339,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:
|
||||||
@@ -365,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):
|
||||||
@@ -373,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)
|
||||||
|
|
||||||
@@ -390,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),
|
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".",
|
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):
|
||||||
@@ -441,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:
|
||||||
@@ -464,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 = []
|
||||||
@@ -493,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()
|
||||||
@@ -521,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:
|
||||||
@@ -1,7 +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
|
||||||
|
|
||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
|
|
||||||
@@ -19,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:
|
||||||
114
DeDRM_plugin/wineutils.py
Normal file
114
DeDRM_plugin/wineutils.py
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
|
||||||
|
# Standard Python modules.
|
||||||
|
import os, sys, re, hashlib, traceback
|
||||||
|
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=""):
|
||||||
|
|
||||||
|
if extension == ".k4i":
|
||||||
|
import json
|
||||||
|
|
||||||
|
try:
|
||||||
|
pyexec = WinePythonCLI(wineprefix)
|
||||||
|
except NoWinePython3Exception:
|
||||||
|
print('{0} v{1}: Unable to find python3 executable in WINEPREFIX="{2}"'.format(PLUGIN_NAME, PLUGIN_VERSION, wineprefix))
|
||||||
|
return []
|
||||||
|
|
||||||
|
basepath, script = os.path.split(scriptpath)
|
||||||
|
print("{0} v{1}: Running {2} under Wine".format(PLUGIN_NAME, PLUGIN_VERSION, script))
|
||||||
|
|
||||||
|
outdirpath = os.path.join(basepath, "winekeysdir")
|
||||||
|
if not os.path.exists(outdirpath):
|
||||||
|
os.makedirs(outdirpath)
|
||||||
|
|
||||||
|
if wineprefix != "":
|
||||||
|
wineprefix = os.path.abspath(os.path.expanduser(os.path.expandvars(wineprefix)))
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = pyexec.check_call([scriptpath, outdirpath])
|
||||||
|
except Exception as e:
|
||||||
|
print("{0} v{1}: Wine subprocess call error: {2}".format(PLUGIN_NAME, PLUGIN_VERSION, e.args[0]))
|
||||||
|
|
||||||
|
# try finding winekeys anyway, even if above code errored
|
||||||
|
winekeys = []
|
||||||
|
winekey_names = []
|
||||||
|
# get any files with extension in the output dir
|
||||||
|
files = [f for f in os.listdir(outdirpath) if f.endswith(extension)]
|
||||||
|
for filename in files:
|
||||||
|
try:
|
||||||
|
fpath = os.path.join(outdirpath, filename)
|
||||||
|
with open(fpath, 'rb') as keyfile:
|
||||||
|
if extension == ".k4i":
|
||||||
|
new_key_value = json.loads(keyfile.read())
|
||||||
|
else:
|
||||||
|
new_key_value = keyfile.read()
|
||||||
|
winekeys.append(new_key_value)
|
||||||
|
winekey_names.append(filename)
|
||||||
|
except:
|
||||||
|
print("{0} v{1}: Error loading file {2}".format(PLUGIN_NAME, PLUGIN_VERSION, filename))
|
||||||
|
traceback.print_exc()
|
||||||
|
os.remove(fpath)
|
||||||
|
print("{0} v{1}: Found and decrypted {2} {3}".format(PLUGIN_NAME, PLUGIN_VERSION, len(winekeys), "key file" if len(winekeys) == 1 else "key files"))
|
||||||
|
return winekeys, winekey_names
|
||||||
186
dedrm_src/zipfilerugged.py → DeDRM_plugin/zipfilerugged.py
Normal file → Executable file
186
dedrm_src/zipfilerugged.py → DeDRM_plugin/zipfilerugged.py
Normal file → Executable 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):
|
||||||
@@ -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,17 +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).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
__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
|
||||||
@@ -48,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
|
||||||
@@ -61,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):
|
||||||
@@ -114,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
|
||||||
@@ -128,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)
|
||||||
@@ -148,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()
|
||||||
@@ -156,22 +162,22 @@ class fixZip:
|
|||||||
|
|
||||||
|
|
||||||
def usage():
|
def usage():
|
||||||
print """usage: zipfix.py inputzip outputzip
|
print("""usage: zipfix.py inputzip outputzip
|
||||||
inputzip is the source zipfile to fix
|
inputzip is the source zipfile to fix
|
||||||
outputzip is the fixed zip archive
|
outputzip is the fixed zip archive
|
||||||
"""
|
""")
|
||||||
|
|
||||||
|
|
||||||
def repairBook(infile, outfile):
|
def repairBook(infile, outfile):
|
||||||
if not os.path.exists(infile):
|
if not os.path.exists(infile):
|
||||||
print "Error: Input Zip File does not exist"
|
print("Error: Input Zip File does not exist")
|
||||||
return 1
|
return 1
|
||||||
try:
|
try:
|
||||||
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
|
||||||
|
|
||||||
|
|
||||||
36
DeDRM_plugin_ReadMe.txt
Normal file
36
DeDRM_plugin_ReadMe.txt
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
DeDRM_plugin.zip
|
||||||
|
================
|
||||||
|
|
||||||
|
This plugin will remove the DRM from:
|
||||||
|
|
||||||
|
- Kindle ebooks (files from Kindle for Mac/PC and eInk Kindles).
|
||||||
|
- Adobe Digital Editions (v2.0.1***) ePubs (including Kobo and Google ePubs downloaded to ADE)
|
||||||
|
- Adobe Digital Editions (v2.0.1) PDFs
|
||||||
|
|
||||||
|
For limitations and work-arounds, see the FAQ at https://github.com/noDRM/DeDRM_tools/blob/master/FAQs.md (or the FAQ in Apprentice Harper's original repository at https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md)
|
||||||
|
|
||||||
|
|
||||||
|
Installation
|
||||||
|
------------
|
||||||
|
Open calibre's Preferences dialog. Click on the "Plugins" button. Next, click on the button, "Load plugin from file". Navigate to the unzipped DeDRM_tools folder, find the file "DeDRM_plugin.zip". Click to select the file and select "Open". Click "Yes" in the "Are you sure?" dialog box. Click the "OK" button in the "Success" dialog box.
|
||||||
|
|
||||||
|
|
||||||
|
Customization
|
||||||
|
-------------
|
||||||
|
For Kindle ebooks from an E-Ink based Kindle (e.g. Voyage), or books downloaded from the Amazon web site 'for transfer via USB' to an E-Ink base Kindle, you must enter the Kindle's serial number in the customisation dialog.
|
||||||
|
|
||||||
|
When you have finished entering your configuration information, you must click the OK button to save it. If you click the Cancel button, all your changes in all the configuration dialogs will be lost.
|
||||||
|
|
||||||
|
|
||||||
|
Troubleshooting
|
||||||
|
---------------
|
||||||
|
If you find that the DeDRM plugin is not working for you (imported ebooks still have DRM - that is, they won't convert or open in the calibre ebook viewer), you should make a log of the import process by deleting the DRMed ebook from calibre and then adding the ebook to calibre when it's running in debug mode. This will generate a lot of helpful debugging info that can be copied into any online help requests. Here's how to do it:
|
||||||
|
|
||||||
|
- Remove the DRMed book from calibre.
|
||||||
|
- Click the Preferences drop-down menu and choose 'Restart in debug mode'.
|
||||||
|
- Once calibre has re-started, import the problem ebook.
|
||||||
|
- Now close calibre.
|
||||||
|
|
||||||
|
A log will appear that you can copy and paste into a GitHub issue report at https://github.com/noDRM/DeDRM_tools/issues. Please also include information about the eBook file.
|
||||||
|
|
||||||
|
If you're using Apprentice Harper's original version, you can also comment at Apprentice Alf's blog, http://apprenticealf.wordpress.com/ or open an issue at Apprentice Harper's repository, https://github.com/apprenticeharper/DeDRM_tools/issues.
|
||||||
143
FAQs.md
143
FAQs.md
@@ -15,112 +15,96 @@ Just download and use these tools, that's all! Uh, almost. There are a few, uh,
|
|||||||
* You must own the ebook - the tools won't work on library ebooks or rented ebooks or books from a friend.
|
* You must own the ebook - the tools won't work on library ebooks or rented ebooks or books from a friend.
|
||||||
* You must not use these tools to give your ebooks to a hundred of your closest friends. Or to a million strangers. Authors need to sell books to be able to write more books. Don't be mean to the authors.
|
* You must not use these tools to give your ebooks to a hundred of your closest friends. Or to a million strangers. Authors need to sell books to be able to write more books. Don't be mean to the authors.
|
||||||
* Do NOT use Adobe Digital Editions 3.0 or later to download your ePubs. ADE 3.0 and later might use a new encryption scheme that the tools can't handle. While major ebook stores aren't using the new scheme yet, using ADE 2.0.1 will ensure that your ebooks are downloaded using the old scheme. Once a book has been downloaded with the new scheme, it's IMPOSSIBLE to re-download using the old scheme (without buying it again).
|
* Do NOT use Adobe Digital Editions 3.0 or later to download your ePubs. ADE 3.0 and later might use a new encryption scheme that the tools can't handle. While major ebook stores aren't using the new scheme yet, using ADE 2.0.1 will ensure that your ebooks are downloaded using the old scheme. Once a book has been downloaded with the new scheme, it's IMPOSSIBLE to re-download using the old scheme (without buying it again).
|
||||||
* Do NOT use Kindle for PC/Mac version 1.25 or later. The tools don't current work with those versions.
|
|
||||||
|
|
||||||
But otherwise, if your ebook is from Amazon, Kobo, Barnes & Noble or any of the ebook stores selling ebooks compatible with Adobe Digital Editions 2.0.1, you should be able to remove the DRM that's been applied to your ebooks.
|
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 fro conversion to ePub as the older KF8 (& MOBI) formats. There are two options to get the older formats. Either stick with version 1.17 or earlier, or modify the executable by changing a file name. Note that with Kindle for PC/Mac 1.25 and later, there is no current solution even for FKX. You must use 1.24 or earlier.
|
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 are no longer available directly from Amazon, so you will need to search for the proper file name and find it on a third party site. The name is "KindleForPC-installer-1.17.44170.exe" for PC and "KindleForMac-44182.dmg" for Mac.
|
Version 1.17 of Kindle is no longer available directly from Amazon, so you will need to search for the proper file name and find it on a third party site. The name is `KindleForPC-installer-1.17.44170.exe` for PC and `KindleForMac-44182.dmg` for Mac. (Note that this is a 32-bit application on the Mac, so will not work on Catalina and newer versions of macOS.)
|
||||||
Verify the one of the following cryptographic hash values, using software of your choice, before installing the downloaded file in order to avoid viruses. If the hash does not match, delete the downloaded file and try again from another site.
|
Verify the one of the following cryptographic hash values, using software of your choice, before installing the downloaded file in order to avoid viruses. If the hash does not match, delete the downloaded file and try again from another site.
|
||||||
Kindle for PC:
|
|
||||||
MD-5: 53F793B562F4823721AA47D7DE099869
|
|
||||||
SHA-1: 73C404D719F0DD8D4AE1C2C96612B095D6C86255
|
|
||||||
SHA-256: 14E0F0053F1276C0C7C446892DC170344F707FBFE99B695176 2C120144163200
|
|
||||||
Kindle for Mac:
|
|
||||||
MD-5: E7E36D5369E1F3CF1D28E5D9115DF15F
|
|
||||||
SHA-1: 7AB9A86B954CB23D622BD79E3257F8E2182D791C
|
|
||||||
SHA-256: 28DC21246A9C7CDEDD2D6F0F4082E6BF7EF9DB9CE9D485548E 8A9E1D19EAE2AC.
|
|
||||||
|
|
||||||
You will need to go to the preferences and uncheck the auto update checkbox. Then download and install 1.17 over the top of the 1.19 installation. You'll also need to delete the KFX folders from your My Kindle Content folder.
|
#### Kindle for PC `KindleForPC-installer-1.17.44170.exe`:
|
||||||
|
* MD-5: 53F793B562F4823721AA47D7DE099869
|
||||||
|
* SHA-1: 73C404D719F0DD8D4AE1C2C96612B095D6C86255
|
||||||
|
* SHA-256: 14E0F0053F1276C0C7C446892DC170344F707FBFE99B6951762C120144163200
|
||||||
|
|
||||||
A other possible solution is to use 1.19 or later, but disable KFX by renaming or disabling a necessary component of the application. This may or may not work on versions after 1.20. In a command window, enter the following commands when Kindle for PC/Mac is not running:
|
#### Kindle for Mac `KindleForMac-44182.dmg`:
|
||||||
|
* MD-5: E7E36D5369E1F3CF1D28E5D9115DF15F
|
||||||
|
* SHA-1: 7AB9A86B954CB23D622BD79E3257F8E2182D791C
|
||||||
|
* SHA-256: 28DC21246A9C7CDEDD2D6F0F4082E6BF7EF9DB9CE9D485548E8A9E1D19EAE2AC
|
||||||
|
|
||||||
|
You will need to go to the preferences and uncheck the auto update checkbox. Then download and install 1.17 over the top of the newer installation. You'll also need to delete the KFX folders from your My Kindle Content folder. You may also need to take further action to prevent an auto update. The simplest 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:
|
||||||
|
|
||||||
#### Windows
|
#### Windows
|
||||||
ren %localappdata%\Amazon\Kindle\application\renderer-test.exe renderer-test.xxx
|
`ren %localappdata%\Amazon\Kindle\application\renderer-test.exe renderer-test.xxx`
|
||||||
|
|
||||||
PC Note: The renderer-test program may be in a different location in some Kindle for PC installations. If the rename command fails look in other folders, such as C:\Program Files\Amazon\Kindle.
|
PC Note: The renderer-test program may be in a different location in some Kindle for PC installations. If the rename command fails look in other folders, such as `C:\Program Files\Amazon\Kindle`.
|
||||||
|
|
||||||
#### Macintosh
|
#### Macintosh
|
||||||
chmod -x /Applications/Kindle.app/Contents/MacOS/renderer-test
|
`chmod -x /Applications/Kindle.app/Contents/MacOS/renderer-test`
|
||||||
|
|
||||||
Mac Note: If the chmod command fails with a permission error try again using sudo before chmod - sudo chmod [...]
|
Mac Note: If the chmod command fails with a permission error try again using `sudo` before `chmod` - `sudo chmod` [...]
|
||||||
|
|
||||||
After restarting the Kindle program any books previously downloaded in KFX format will no longer open. You will need to remove them from your device and re-download them. All future downloads will use the older Kindle formats instead of KFX although they will continue to be placed in one individual subdirectory per book.
|
After restarting the Kindle program any books previously downloaded in KFX format will no longer open. You will need to remove them from your device and re-download them. All future downloads will use the older Kindle formats instead of KFX although they will continue to be placed in one individual subdirectory per book. Note that books soudl be downoad by right-click and 'Download', not by just opening the book. Recent (1.25+) versions of Kindle for Mac/PC may convert KF8 files to a new format that is not supported by these tools when the book is opened for reading.
|
||||||
|
|
||||||
#### Decrypting KFX
|
#### Decrypting KFX
|
||||||
Thanks to work by several people, the tools can now decrypt KFX format ebooks from Kindle for PC. In addition to the DeDRM plugin, calibre users will also need to install jhowell's KFX Input plugin which is available through the standard plugin menu in calibre, or directly from [his plugin thread](https://www.mobileread.com/forums/showthread.php?t=291290) on Mobileread. Not that KFX decryption does not work for Kindle for Mac 1.25 and later.
|
Thanks to work by several people, the tools can now decrypt KFX format ebooks from Kindle for Mac/PC. In addition to the DeDRM plugin, calibre users will also need to install jhowell's KFX Input plugin which is available through the standard plugin menu in calibre, or directly from [his plugin thread](https://www.mobileread.com/forums/showthread.php?t=291290) on Mobileread.
|
||||||
|
|
||||||
|
It's quite possible that Amazon will update their KFX DeDRM to prevent DRM removal from KFX books again. So Remove DRM as soon as possible!
|
||||||
|
|
||||||
#### Thanks
|
#### Thanks
|
||||||
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\_First.txt file. Please read the ReadMe\_First file! That will explain what the folders are, and you'll be able to work out which of the tools you need.
|
First, unzip the archive. You should now have a DeDRM folder containing several 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.
|
||||||
|
|
||||||
## That's a big complicated ReadMe file! Isn't there a quick guide?
|
## That's a big complicated ReadMe file! Isn't there a quick guide?
|
||||||
Install calibre. Install the DeDRM\_plugin in calibre. Install the Obok\_plugin in calibre. Restart calibre. In the DeDRM_plugin customisation dialog add in any E-Ink Kindle serial numbers and your B&N account email address and password. Remember that the plugin only tries to remove DRM when ebooks are imported.
|
Install calibre. Install the DeDRM\_plugin in calibre. Install the Obok\_plugin in calibre. Restart calibre. In the DeDRM_plugin customisation dialog add in any E-Ink Kindle serial numbers. Remember that the plugin only tries to remove DRM when ebooks are imported.
|
||||||
|
|
||||||
# Installing the Tools
|
# 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.
|
||||||
|
|
||||||
### I’ve unzipped the tools archive, but I can’t find the calibre plugin when I try to add them to calibre. I use Windows.
|
### I’ve unzipped the tools archive, but I can’t find the calibre plugin when I try to add them to calibre. I use Windows.
|
||||||
You should select the zip file that is in the “DeDRM\_calibre\_plugin” folder, not any files inside the plugin’s zip archive. Make sure you are selecting from the folder that you created when you unzipped the tools archive and not selecting a file inside the still-zipped tools archive.
|
You should select the zip file that is in the `DeDRM_calibre_plugin` folder, not any files inside the plugin’s zip archive. Make sure you are selecting from the folder that you created when you unzipped the tools archive and not selecting a file inside the still-zipped tools archive.
|
||||||
|
|
||||||
(The problem is that Windows will allow apps to browse inside zip archives without needing to unzip them first. If there are zip archives inside the main zip archives, Windows will show them as unzipped as well. So what happens is people will unzip the DeDRM\_tools\_X.X.X.zip to a folder, but when using calibre they will actually navigate to the still zipped file by mistake and cannot tell they have done so because they do not have file extensions showing. So to the unwary Windows user, it appears that the zip archive was unzipped and that everything inside it was unzipped as well so there is no way to install the plugins.
|
(The problem is that Windows will allow apps to browse inside zip archives without needing to unzip them first. If there are zip archives inside the main zip archives, Windows will show them as unzipped as well. So what happens is people will unzip the `DeDRM_tools_X.X.X.zip` to a folder, but when using calibre they will actually navigate to the still zipped file by mistake and cannot tell they have done so because they do not have file extensions showing. So to the unwary Windows user, it appears that the zip archive was unzipped and that everything inside it was unzipped as well so there is no way to install the plugins.
|
||||||
|
|
||||||
We strongly recommend renaming the DeDRM\_tools\_X.X.X.zip archive (after extracting its contents) to DeDRM\_tools\_X.X.X_archive.zip. If you do that, you are less likely to navigate to the wrong location from inside calibre.)
|
We strongly recommend renaming the `DeDRM_tools_X.X.X.zip` archive (after extracting its contents) to `DeDRM_tools_X.X.X_archive.zip`. If you do that, you are less likely to navigate to the wrong location from inside calibre.)
|
||||||
|
|
||||||
## The Windows Application
|
|
||||||
### I've installed ActiveState Python and PyCrypto, but the Windows application won't run. What have I done wrong?
|
|
||||||
Nothing. There's a bug in the some older ActiveState Python Windows installers that puts the Tcl code in the wrong place. See [this comment of mine at ActiveState community](https://community.activestate.com/node/19090). Just move the Tcl code to the correct place manually and the Windows app should run.
|
|
||||||
|
|
||||||
## The Macintosh Application
|
|
||||||
### I can't open the Macintosh Application. Some message about it not being signed or something.
|
|
||||||
Try right-clicking and select open. That might give you the option to open it anyway. Otherwise you'll need to change your security settings to allow unsigned applications to run. You can probably change these back after running it for the first time.
|
|
||||||
|
|
||||||
### I can't open the Macintosh Application at all. I get 'The aplication "DeDRM" can't be opened'
|
|
||||||
Some unzip applications do not respect the execution bit setting. Try unzipping the main tools archive using the built-in Mac unzip utility.
|
|
||||||
|
|
||||||
Alternatively, sometimes the execution bit isn't set correctly in the archive. If you put the extracted DeDRM application in your Applications folder, you can set the executable bit on the 'droplet' file from the terminal using the command chmod +x /Applications/DeDRM.app/Contents/MacOS/droplet
|
|
||||||
|
|
||||||
### I can't open the Macintosh Application at all. I get 'spawn_via_launchd() failed, errno=111'
|
|
||||||
There seems to be a bug in Apple's launch services. Try using the free [Maintenance utility](https://www.titanium-software.fr/en/maintenance.html) from Titanium Software to clear the launch cache and database.
|
|
||||||
|
|
||||||
### The application opens, but always gives an error in the log 'ImportError: No module named Crypto.Cipher'
|
|
||||||
Some version of MacOS don't include PyCrpto. Your should be able to install it by using this command in the Terminal app: python -m pip pycrypto
|
|
||||||
|
|
||||||
# Using the Tools
|
# Using the Tools
|
||||||
## I can’t get the tools to work on my rented or library ebooks.
|
## I can’t get the tools to work on my rented or library ebooks.
|
||||||
The tools are not designed to remove DRM from rented or library ebooks.
|
The tools are not designed to remove DRM from rented or library ebooks.
|
||||||
|
|
||||||
## I've unzipped the tools, but what are all the different files, and how do I use them?
|
## I've unzipped the tools, but what are all the different files, and how do I use them?
|
||||||
Read the ReadMe_First.txt file and then the ReadMe files included in the tools folder(s) you're interested in. That's what they're for.
|
Read the `ReadMe_Overview.txt` file and then the ReadMe files for the tools you're interested in. That's what they're for.
|
||||||
|
|
||||||
## I have installed the calibre plugin, but my books still have DRM. When I try to view or convert my books, calibre says they have DRM.
|
## I have installed the calibre plugin, but my books still have DRM. When I try to view or convert my books, calibre says they have DRM.
|
||||||
DRM only gets removed when an ebook is imported into calibre. Also, if the book is already in calibre, by default calibre will discard the newly imported file. You can change this in calibre's Adding books preferences page (Automerge..../Overwrite....), so that newly imported files overwrite existing ebook formats. Then just re-import your books and the DRM-free versions will overwrite the DRMed versions while retaining your books' metadata.
|
DRM only gets removed when an ebook is imported into calibre. Also, if the book is already in calibre, by default calibre will discard the newly imported file. You can change this in calibre's Adding books preferences page (Automerge..../Overwrite....), so that newly imported files overwrite existing ebook formats. Then just re-import your books and the DRM-free versions will overwrite the DRMed versions while retaining your books' metadata.
|
||||||
|
|
||||||
## I have installed the calibre plugin or I am trying to use one of the other tools, but I don’t know where my ebooks are stored.
|
## I have installed the calibre plugin, but I don’t know where my ebooks are stored.
|
||||||
Your ebooks are stored on your computer or on your ebook reader. You need to find them to be able to remove the DRM. If they are on your reader, you should be able to locate them easily. On your computer it’s not so obvious. Here are the default locations.
|
Your ebooks are stored on your computer or on your ebook reader. You need to find them to be able to remove the DRM. If they are on your reader, you should be able to locate them easily. On your computer it’s not so obvious. Here are the default locations.
|
||||||
|
|
||||||
### 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`
|
||||||
|
|
||||||
### Windows
|
### Windows
|
||||||
Navigating from your "Documents" folder ("My Documents" folder, pre-Windows 7)
|
Navigating from your `Documents` folder (`My Documents` folder, pre-Windows 7)
|
||||||
|
|
||||||
Kindle for PC ebooks are in My Kindle Content
|
Kindle for PC ebooks are in `My Kindle Content`
|
||||||
|
|
||||||
Adobe Digital Editions ebooks are in My Digital Editions
|
Adobe Digital Editions ebooks are in `My Digital Editions`
|
||||||
|
|
||||||
|
|
||||||
## I have installed the calibre plugin, and the book is not already in calibre, but the DRM does not get removed.
|
## I have installed the calibre plugin, and the book is not already in calibre, but the DRM does not get removed.
|
||||||
@@ -134,62 +118,47 @@ If this book is from an eInk Kindle (e.g. Paperwhite), you must enter the serial
|
|||||||
|
|
||||||
If this book is from Kindle for Mac or Kindle for PC, you must have the Kindle Software installed on the same computer and user account as your copy of calibre.
|
If this book is from Kindle for Mac or Kindle for PC, you must have the Kindle Software installed on the same computer and user account as your copy of calibre.
|
||||||
|
|
||||||
If this book is from Kindle for Mac you must be using version 1.24 or below, even if you have the Input plugin installed.
|
|
||||||
|
|
||||||
If the book is from Kindle for PC or Kindle for Mac and you think you are doing everything right, and you are getting this message, it is possible that the files containing the encryption key aren’t 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 aren’t quite in the format the tools expect. To try to fix this:
|
||||||
|
|
||||||
1. Deregister Kindle for PC(Mac) from your Amazon account.
|
1. 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 don’t have the DRM removal tools working correctly, but some of your books didn’t have DRM in the first place.
|
* You still don’t have the DRM removal tools working correctly, but some of your books didn’t have DRM in the first place.
|
||||||
* Kindle only: It is a Topaz format book and contains some coding that the tools do not understand. You will need to get a log of the DeDRM attempt, and then create a [new issue at Apprentice Harper's github repository](https://github.com/apprenticeharper/DeDRM_tools/issues/), attaching the book and the log, so that the tools can be updated.
|
|
||||||
|
|
||||||
If you are still having problems with particular books, you will need to create a log of the DRM removal attempt for one of the problem books, and post that in a comment at Apprentice Alf's blog or in a new issue at Apprentice Harper's github repository.
|
If you are still having problems with particular books, you will need to create a log of the DRM removal attempt for one of the problem books. If you're using NoDRM's fork, open [a new issue](https://github.com/noDRM/DeDRM_tools/issues) in the GitHub repo. If you're using Apprentice Harpers version, post that logfile as a comment at Apprentice Alf's blog or in a new issue at [Apprentice Harper's github repository](https://github.com/apprenticeharper/DeDRM_tools/issues).
|
||||||
|
|
||||||
## My Kindle book has imported and the DRM has been removed, but all the pictures are gone.
|
## My Kindle book has imported and the DRM has been removed, but all the pictures are gone.
|
||||||
Most likely, this is a book downloaded from Amazon directly to an eInk Kindle (e.g. Paperwhite). Unfortunately, the pictures are probably in a .azw6 file that the tools don't understand. You must download the book manually from Amazon's web site "For transfer via USB" to your Kindle. When you download the eBook in this manner, Amazon will package the pictures in the with text in a single file that the tools will be able to import successfully.
|
Most likely, this is a book downloaded from Amazon directly to an eInk Kindle (e.g. Paperwhite). Unfortunately, the pictures are probably in a `.azw6` file that the tools don't understand. You must download the book manually from Amazon's web site "For transfer via USB" to your Kindle. When you download the eBook in this manner, Amazon will package the pictures in the with text in a single file that the tools will be able to import successfully.
|
||||||
|
|
||||||
## My Kindle book has imported, but it's showing up as an AZW4 format. Conversions take a long time and/or are very poor.
|
## My Kindle book has imported, but it's showing up as an AZW4 format. Conversions take a long time and/or are very poor.
|
||||||
You have found a Print Replica Kindle ebook. This is a PDF in a Kindle wrapper. Now the DRM has been removed, you can extract the PDF from the wrapper using the KindleUnpack plugin. Conversion of PDFs rarely gives good results.
|
You have found a Print Replica Kindle ebook. This is a PDF in a Kindle wrapper. Now the DRM has been removed, you can extract the PDF from the wrapper using the KindleUnpack plugin. Conversion of PDFs rarely gives good results.
|
||||||
|
|
||||||
## The tools can't see an ebook that was downloaded directly to my eInk kindle, although it's definitely there, and I can read it on the Kindle. I can't even try to import it.
|
|
||||||
Mostly likely, this is a book downloaded from Amazon directly to one of the newer eInk Kindles (e.g. Paperwhite). Unfortunately, it is probably in a new multi-file KFX format that the tools don't understand. You must download the book manually from Amazon's web site "For transfer via USB" to your Kindle. When you download the ebook in this manner, Amazon will send a single KF8-format file that the tools will be able to import successfully.
|
|
||||||
|
|
||||||
## Do the tools work on books from Kobo?
|
## Do the tools work on books from Kobo?
|
||||||
If you use the Kobo desktop application for Mac or PC, install the obok plugin. This will import and remove the DRM from your Kobo books, and is the easiest method for Kobo ebooks.
|
If you use the Kobo desktop application for Mac or PC, install the Obok plugin. This will import and remove the DRM from your Kobo books, and is the easiest method for Kobo ebooks.
|
||||||
|
|
||||||
## I registered Adobe Digital Editions 3.0 or later with an Adobe ID before downloading, but my epub or PDF still has DRM.
|
## 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.
|
||||||
|
|
||||||
## I cannot solve my problem with the Macintosh DeDRM application, and now I need to ‘post a log’. How do I do that?
|
|
||||||
The Macintosh DeDRM application creates a log file on your desktop every time it is run. After unsuccessfully removing DRM from one ebook, copy the contents of the log file (it is a simple text file) and paste it into your comment at Apprentice Alf's blog or in a new issue at Apprentice Harper's github repository.
|
|
||||||
|
|
||||||
## I cannot solve my problem with the Windows DeDRM application, and now I need to ‘post a log’. How do I do that?
|
|
||||||
The Windows DeDRM application creates a log file in your home directory (C:\Users\[username]) every time it is run. After unsuccessfully removing DRM from one ebook, copy the contents of the log file (it is a simple text file) and paste it into your comment at Apprentice Alf's blog or in a new issue at Apprentice Harper's github repository.
|
|
||||||
|
|
||||||
## Is there a way to use the DeDRM plugin for Calibre from the command line?
|
## Is there a way to use the DeDRM plugin for Calibre from the command line?
|
||||||
See the [Calibre command line interface (CLI) instructions](CALIBRE_CLI_INSTRUCTIONS.md).
|
See the [Calibre command line interface (CLI) instructions](CALIBRE_CLI_INSTRUCTIONS.md).
|
||||||
|
|
||||||
# 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.
|
||||||
|
|
||||||
## What do some of my Kindle ebooks import as HTMLZ format in calibre?
|
## Why do some of my Kindle ebooks import as HTMLZ format in calibre?
|
||||||
Most Amazon Kindle ebooks are Mobipocket format ebooks, or the new KF8 format. However, some are in a format known as Topaz. The Topaz format is only used by Amazon. A Topaz ebook is a collections of glyphs and their positions on each page tagged with some additional information from that page including OCRed text (Optical Character Recognition generated Text) to allow searching, and some additional layout information. Each page of a Topaz ebook is effectively a description of an image of that page. To convert a Topaz ebook to another format is not easy as there is not a one-to-one mapping between glyphs and characters/fonts. To account for this, two different formats are generated by the DRM removal software. The first is an html description built from the OCRtext and images stored in the Topaz file (HTMLZ). This format is easily reflowed but may suffer from typical OCRtext errors including typos, garbled text, missing italics, missing bolds, etc. The second format uses the glyph and position information to create an accurate scalable vector graphics (SVG) image of each page of the book that can be viewed in web browsers that support svg images (Safari, Firefox 4 or later, etc). Additional conversion software can be used to convert these SVG images to an image only PDF file. The DeDRM calibre plugin only imports the HTMLZ versions of the Topaz ebook. The html version can be manually cleaned up and spell checked and then converted using Sigil/calibre to epubs, mobi ebooks, and etc.
|
Most Amazon Kindle ebooks are Mobipocket format ebooks, or the new KF8 format. However, some are in a format known as Topaz. The Topaz format is only used by Amazon. A Topaz ebook is a collections of glyphs and their positions on each page tagged with some additional information from that page including OCRed text (Optical Character Recognition generated Text) to allow searching, and some additional layout information. Each page of a Topaz ebook is effectively a description of an image of that page. To convert a Topaz ebook to another format is not easy as there is not a one-to-one mapping between glyphs and characters/fonts. To account for this, two different formats are generated by the DRM removal software. The first is an html description built from the OCRtext and images stored in the Topaz file (HTMLZ). This format is easily reflowed but may suffer from typical OCRtext errors including typos, garbled text, missing italics, missing bolds, etc. The second format uses the glyph and position information to create an accurate scalable vector graphics (SVG) image of each page of the book that can be viewed in web browsers that support svg images (Safari, Firefox 4 or later, etc). Additional conversion software can be used to convert these SVG images to an image only PDF file. The DeDRM calibre plugin only imports the HTMLZ versions of the Topaz ebook. The html version can be manually cleaned up and spell checked and then converted using Sigil/calibre to epubs, mobi ebooks, and etc.
|
||||||
|
|
||||||
## Are the tools open source? How can I be sure they are safe and not a trojan horse?
|
## Are the tools open source? How can I be sure they are safe and not a trojan horse?
|
||||||
@@ -198,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 book’s owner to use the encryption key information already stored someplace on their computer or device to decrypt the ebook in the same manner the official ebook reading software uses.
|
Note these tools do NOT ‘crack’ the DRM. They simply allow the book’s owner to use the encryption key information already stored someplace on their computer or device to decrypt the ebook in the same manner the official ebook reading software uses.
|
||||||
|
|
||||||
@@ -215,21 +184,21 @@ Amazon turned off backup for Kindle for Android, so the tools can no longer find
|
|||||||
Apple regularly change the details of their DRM and so the tools in the main tools archive will not work with these ebooks. Apple’s Fairplay DRM scheme can be removed using Requiem if the appropriate version of iTunes can still be installed and used. See the post Apple and ebooks: iBookstore DRM and how to remove it at Apprentice Alf's blog for more details.
|
Apple regularly change the details of their DRM and so the tools in the main tools archive will not work with these ebooks. Apple’s Fairplay DRM scheme can be removed using Requiem if the appropriate version of iTunes can still be installed and used. See the post Apple and ebooks: iBookstore DRM and how to remove it at Apprentice Alf's blog for more details.
|
||||||
|
|
||||||
## I’ve got the tools archive and I’ve read all the FAQs but I still can’t install the tools and/or the DRM removal doesn’t work
|
## I’ve got the tools archive and I’ve read all the FAQs but I still can’t install the tools and/or the DRM removal doesn’t work
|
||||||
* Read the ReadMe_First.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 in the folder of the tools you want to use.
|
* Read the ReadMe file for the tool you want to use.
|
||||||
* If you still can’t remove the DRM, ask in the comments section of Apprentice Alf's blog or create a new issue at Apprentice Harper's github repository, reporting the error as precisely as you can, what platform you use, what tool you have tried, what errors you get, and what versions you are using. If the problem happens when running one of the tools, post a log (see previous questions on how to do this).
|
* If you still can’t remove the DRM, create a new [GitHub issue](https://github.com/noDRM/DeDRM_tools/issues). If you are using Apprentice Harper's original version and not this fork, you can also ask in the comments section of Apprentice Alf's blog or create a new issue at Apprentice Harper's github repository. If you do report an issue in any of the GitHub repositories, please report the error as precisely as you can. Include what platform you use, what tool you have tried, what errors you get, and what versions you are using. If the problem happens when running one of the tools, post a log (see previous questions on how to do this).
|
||||||
|
|
||||||
## Who wrote these scripts?
|
## 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
|
||||||
* The Amazon Topaz DRM removal script was created by CMBDTC
|
* The Amazon Topaz DRM removal script was created by CMBDTC
|
||||||
* The Amazon Topaz format conversion was created by some_updates, clarknova, and Bart Simpson
|
* The Amazon Topaz format conversion was created by some_updates, clarknova, and Bart Simpson
|
||||||
* The DeDRM all-in-one AppleScript application was created by Apprentice Alf
|
|
||||||
* The DeDRM all-in-one Python application was created by some_updates
|
|
||||||
* The DeDRM all-in-one calibre plugin was created by Apprentice Alf
|
* The DeDRM all-in-one calibre plugin was created by Apprentice Alf
|
||||||
|
* The support for .kinf2018 key files and KFX 2&3 was by Apprentice Sakuya
|
||||||
* The Scuolabooks tool was created by Hex
|
* The Scuolabooks tool was created by Hex
|
||||||
* The Microsoft code was created by drs
|
* The Microsoft code was created by drs
|
||||||
* The Apple DRM removal tool was created by Brahms
|
* The Apple DRM removal tool was created by Brahms
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from __future__ import (unicode_literals, division, absolute_import,
|
|||||||
print_function)
|
print_function)
|
||||||
|
|
||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
|
__version__ = '10.0.0'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
#####################################################################
|
#####################################################################
|
||||||
@@ -19,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, 5, 4)
|
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'
|
||||||
@@ -1,18 +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 codecs
|
||||||
import os, traceback, zipfile
|
import os, traceback, zipfile
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from PyQt5.Qt import QToolButton, QUrl
|
from PyQt5.Qt import QToolButton, QUrl
|
||||||
except ImportError:
|
except ImportError:
|
||||||
from PyQt4.Qt import QToolButton, QUrl
|
from PyQt4.Qt import QToolButton, QUrl
|
||||||
|
|
||||||
from calibre.gui2 import open_url, question_dialog
|
from calibre.gui2 import open_url, question_dialog
|
||||||
from calibre.gui2.actions import InterfaceAction
|
from calibre.gui2.actions import InterfaceAction
|
||||||
from calibre.utils.config import config_dir
|
from calibre.utils.config import config_dir
|
||||||
@@ -24,7 +25,7 @@ from calibre.ebooks.metadata.meta import get_metadata
|
|||||||
from calibre_plugins.obok_dedrm.dialogs import (SelectionDialog, DecryptAddProgressDialog,
|
from calibre_plugins.obok_dedrm.dialogs import (SelectionDialog, DecryptAddProgressDialog,
|
||||||
AddEpubFormatsProgressDialog, ResultsSummaryDialog)
|
AddEpubFormatsProgressDialog, ResultsSummaryDialog)
|
||||||
from calibre_plugins.obok_dedrm.config import plugin_prefs as cfg
|
from calibre_plugins.obok_dedrm.config import plugin_prefs as cfg
|
||||||
from calibre_plugins.obok_dedrm.__init__ import (PLUGIN_NAME, PLUGIN_SAFE_NAME,
|
from calibre_plugins.obok_dedrm.__init__ import (PLUGIN_NAME, PLUGIN_SAFE_NAME,
|
||||||
PLUGIN_VERSION, PLUGIN_DESCRIPTION, HELPFILE_NAME)
|
PLUGIN_VERSION, PLUGIN_DESCRIPTION, HELPFILE_NAME)
|
||||||
from calibre_plugins.obok_dedrm.utilities import (
|
from calibre_plugins.obok_dedrm.utilities import (
|
||||||
get_icon, set_plugin_icon_resources, showErrorDlg, format_plural,
|
get_icon, set_plugin_icon_resources, showErrorDlg, format_plural,
|
||||||
@@ -53,7 +54,7 @@ class InterfacePluginAction(InterfaceAction):
|
|||||||
def genesis(self):
|
def genesis(self):
|
||||||
icon_resources = self.load_resources(PLUGIN_ICONS)
|
icon_resources = self.load_resources(PLUGIN_ICONS)
|
||||||
set_plugin_icon_resources(PLUGIN_NAME, icon_resources)
|
set_plugin_icon_resources(PLUGIN_NAME, icon_resources)
|
||||||
|
|
||||||
self.qaction.setIcon(get_icon(PLUGIN_ICONS[0]))
|
self.qaction.setIcon(get_icon(PLUGIN_ICONS[0]))
|
||||||
self.qaction.triggered.connect(self.launchObok)
|
self.qaction.triggered.connect(self.launchObok)
|
||||||
self.gui.keyboard.finalize()
|
self.gui.keyboard.finalize()
|
||||||
@@ -93,7 +94,7 @@ class InterfacePluginAction(InterfaceAction):
|
|||||||
debug_print("Exception getting device path. Probably not an E-Ink Kobo device")
|
debug_print("Exception getting device path. Probably not an E-Ink Kobo device")
|
||||||
|
|
||||||
# Get the Kobo Library object (obok v3.01)
|
# Get the Kobo Library object (obok v3.01)
|
||||||
self.library = KoboLibrary(tmpserials, device_path)
|
self.library = KoboLibrary(tmpserials, device_path, cfg['kobo_directory'])
|
||||||
debug_print ("got kobodir %s" % self.library.kobodir)
|
debug_print ("got kobodir %s" % self.library.kobodir)
|
||||||
if (self.library.kobodir == ''):
|
if (self.library.kobodir == ''):
|
||||||
# linux and no device connected, but could be extended
|
# linux and no device connected, but could be extended
|
||||||
@@ -106,10 +107,10 @@ 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
|
||||||
|
|
||||||
# Check to see if a key can be retrieved using the legacy obok method.
|
# Check to see if a key can be retrieved using the legacy obok method.
|
||||||
legacy_key = legacy_obok().get_legacy_cookie_id
|
legacy_key = legacy_obok().get_legacy_cookie_id
|
||||||
if legacy_key is not None:
|
if legacy_key is not None:
|
||||||
@@ -154,7 +155,7 @@ class InterfacePluginAction(InterfaceAction):
|
|||||||
# Close Kobo Library object
|
# Close Kobo Library object
|
||||||
self.library.close()
|
self.library.close()
|
||||||
|
|
||||||
# If we have decrypted books to work with, feed the list of decrypted books details
|
# If we have decrypted books to work with, feed the list of decrypted books details
|
||||||
# and the callback function (self.add_new_books) to the ProgressDialog dispatcher.
|
# and the callback function (self.add_new_books) to the ProgressDialog dispatcher.
|
||||||
if len(self.books_to_add):
|
if len(self.books_to_add):
|
||||||
d = DecryptAddProgressDialog(self.gui, self.books_to_add, self.add_new_books, self.db, 'calibre',
|
d = DecryptAddProgressDialog(self.gui, self.books_to_add, self.add_new_books, self.db, 'calibre',
|
||||||
@@ -196,7 +197,7 @@ class InterfacePluginAction(InterfaceAction):
|
|||||||
# We will write the help file out every time, in case the user upgrades the plugin zip
|
# 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
|
||||||
@@ -212,7 +213,7 @@ class InterfacePluginAction(InterfaceAction):
|
|||||||
def get_decrypted_kobo_books(self, book):
|
def get_decrypted_kobo_books(self, book):
|
||||||
'''
|
'''
|
||||||
This method is a call-back function used by DecryptAddProgressDialog in dialogs.py to decrypt Kobo books
|
This method is a call-back function used by DecryptAddProgressDialog in dialogs.py to decrypt Kobo books
|
||||||
|
|
||||||
:param book: A KoboBook object that is to be decrypted.
|
:param book: A KoboBook object that is to be decrypted.
|
||||||
'''
|
'''
|
||||||
print (_('{0} - Decrypting {1}').format(PLUGIN_NAME + ' v' + PLUGIN_VERSION, book.title))
|
print (_('{0} - Decrypting {1}').format(PLUGIN_NAME + ' v' + PLUGIN_VERSION, book.title))
|
||||||
@@ -233,7 +234,7 @@ class InterfacePluginAction(InterfaceAction):
|
|||||||
'''
|
'''
|
||||||
This method is a call-back function used by DecryptAddProgressDialog in dialogs.py to add books to calibre
|
This method is a call-back function used by DecryptAddProgressDialog in dialogs.py to add books to calibre
|
||||||
(It's set up to handle multiple books, but will only be fed books one at a time by DecryptAddProgressDialog)
|
(It's set up to handle multiple books, but will only be fed books one at a time by DecryptAddProgressDialog)
|
||||||
|
|
||||||
:param books_to_add: List of calibre bookmaps (created in get_decrypted_kobo_books)
|
:param books_to_add: List of calibre bookmaps (created in get_decrypted_kobo_books)
|
||||||
'''
|
'''
|
||||||
added = self.db.add_books(books_to_add, add_duplicates=False, run_hooks=False)
|
added = self.db.add_books(books_to_add, add_duplicates=False, run_hooks=False)
|
||||||
@@ -253,7 +254,7 @@ class InterfacePluginAction(InterfaceAction):
|
|||||||
def add_epub_format(self, book_id, mi, path):
|
def add_epub_format(self, book_id, mi, path):
|
||||||
'''
|
'''
|
||||||
This method is a call-back function used by AddEpubFormatsProgressDialog in dialogs.py
|
This method is a call-back function used by AddEpubFormatsProgressDialog in dialogs.py
|
||||||
|
|
||||||
:param book_id: calibre ID of the book to add the encrypted epub to.
|
:param book_id: calibre ID of the book to add the encrypted epub to.
|
||||||
:param mi: calibre metadata object
|
:param mi: calibre metadata object
|
||||||
:param path: path to the decrypted epub (temp file)
|
:param path: path to the decrypted epub (temp file)
|
||||||
@@ -281,7 +282,7 @@ class InterfacePluginAction(InterfaceAction):
|
|||||||
self.formats_to_add.append((home_id, mi, tmp_file))
|
self.formats_to_add.append((home_id, mi, tmp_file))
|
||||||
else:
|
else:
|
||||||
self.no_home_for_book.append(mi)
|
self.no_home_for_book.append(mi)
|
||||||
# If we found homes for decrypted epubs in existing calibre entries, feed the list of decrypted book
|
# If we found homes for decrypted epubs in existing calibre entries, feed the list of decrypted book
|
||||||
# details and the callback function (self.add_epub_format) to the ProgressDialog dispatcher.
|
# details and the callback function (self.add_epub_format) to the ProgressDialog dispatcher.
|
||||||
if self.formats_to_add:
|
if self.formats_to_add:
|
||||||
d = AddEpubFormatsProgressDialog(self.gui, self.formats_to_add, self.add_epub_format)
|
d = AddEpubFormatsProgressDialog(self.gui, self.formats_to_add, self.add_epub_format)
|
||||||
@@ -306,10 +307,10 @@ class InterfacePluginAction(InterfaceAction):
|
|||||||
sd = ResultsSummaryDialog(self.gui, caption, msg, log)
|
sd = ResultsSummaryDialog(self.gui, caption, msg, log)
|
||||||
sd.exec_()
|
sd.exec_()
|
||||||
return
|
return
|
||||||
|
|
||||||
def ask_about_inserting_epubs(self):
|
def ask_about_inserting_epubs(self):
|
||||||
'''
|
'''
|
||||||
Build question dialog with details about kobo books
|
Build question dialog with details about kobo books
|
||||||
that couldn't be added to calibre as new books.
|
that couldn't be added to calibre as new books.
|
||||||
'''
|
'''
|
||||||
''' Terisa: Improve the message
|
''' Terisa: Improve the message
|
||||||
@@ -327,13 +328,13 @@ class InterfacePluginAction(InterfaceAction):
|
|||||||
msg = _('<p><b>{0}</b> -- not added because of {1} in your library.<br /><br />').format(self.duplicate_book_list[0][0].title, self.duplicate_book_list[0][2])
|
msg = _('<p><b>{0}</b> -- not added because of {1} in your library.<br /><br />').format(self.duplicate_book_list[0][0].title, self.duplicate_book_list[0][2])
|
||||||
msg += _('Would you like to try and add the EPUB format to an available calibre duplicate?<br /><br />')
|
msg += _('Would you like to try and add the EPUB format to an available calibre duplicate?<br /><br />')
|
||||||
msg += _('NOTE: no pre-existing EPUB will be overwritten.')
|
msg += _('NOTE: no pre-existing EPUB will be overwritten.')
|
||||||
|
|
||||||
return question_dialog(self.gui, caption, msg, det_msg)
|
return question_dialog(self.gui, caption, msg, det_msg)
|
||||||
|
|
||||||
def find_a_home(self, ids):
|
def find_a_home(self, ids):
|
||||||
'''
|
'''
|
||||||
Find the ID of the first EPUB-Free duplicate available
|
Find the ID of the first EPUB-Free duplicate available
|
||||||
|
|
||||||
:param ids: List of calibre IDs that might serve as a home.
|
:param ids: List of calibre IDs that might serve as a home.
|
||||||
'''
|
'''
|
||||||
for id in ids:
|
for id in ids:
|
||||||
@@ -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()
|
||||||
@@ -455,7 +454,7 @@ class InterfacePluginAction(InterfaceAction):
|
|||||||
if cancelled_count > 0:
|
if cancelled_count > 0:
|
||||||
log += _('<p><b>Book imports cancelled by user:</b> {}</p>\n').format(cancelled_count)
|
log += _('<p><b>Book imports cancelled by user:</b> {}</p>\n').format(cancelled_count)
|
||||||
return (msg, log)
|
return (msg, log)
|
||||||
log += _('<p><b>New EPUB formats inserted in existing calibre books:</b> {0}</p>\n').format(len(self.successful_format_adds))
|
log += _('<p><b>New EPUB formats inserted in existing calibre books:</b> {0}</p>\n').format(len(self.successful_format_adds))
|
||||||
if self.successful_format_adds:
|
if self.successful_format_adds:
|
||||||
log += '<ul>\n'
|
log += '<ul>\n'
|
||||||
for id, mi in self.successful_format_adds:
|
for id, mi in self.successful_format_adds:
|
||||||
@@ -474,7 +473,7 @@ class InterfacePluginAction(InterfaceAction):
|
|||||||
log += _('<p><b>Format imports cancelled by user:</b> {}</p>\n').format(cancelled_count)
|
log += _('<p><b>Format imports cancelled by user:</b> {}</p>\n').format(cancelled_count)
|
||||||
return (msg, log)
|
return (msg, log)
|
||||||
else:
|
else:
|
||||||
|
|
||||||
# Single book ... don't get fancy.
|
# Single book ... don't get fancy.
|
||||||
if self.ids_of_new_books:
|
if self.ids_of_new_books:
|
||||||
title = self.ids_of_new_books[0][1].title
|
title = self.ids_of_new_books[0][1].title
|
||||||
@@ -494,4 +493,4 @@ class InterfacePluginAction(InterfaceAction):
|
|||||||
reason = _('of unknown reasons. Gosh I\'m embarrassed!')
|
reason = _('of unknown reasons. Gosh I\'m embarrassed!')
|
||||||
msg = _('<p>{0} not added because {1}').format(title, reason)
|
msg = _('<p>{0} not added because {1}').format(title, reason)
|
||||||
return (msg, log)
|
return (msg, log)
|
||||||
|
|
||||||
@@ -1,20 +1,14 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
# 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>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
import os, time, re, sys
|
import os, time, re, sys
|
||||||
try:
|
from datetime import datetime
|
||||||
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)
|
||||||
@@ -426,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
|
||||||
|
|
||||||
|
|
||||||
@@ -449,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
|
||||||
|
|
||||||
|
|
||||||
@@ -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)
|
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)
|
|
||||||
|
|
||||||
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
|
||||||
@@ -18,6 +12,7 @@ from calibre.utils.config import JSONConfig, config_dir
|
|||||||
plugin_prefs = JSONConfig('plugins/obok_dedrm_prefs')
|
plugin_prefs = JSONConfig('plugins/obok_dedrm_prefs')
|
||||||
plugin_prefs.defaults['finding_homes_for_formats'] = 'Ask'
|
plugin_prefs.defaults['finding_homes_for_formats'] = 'Ask'
|
||||||
plugin_prefs.defaults['kobo_serials'] = []
|
plugin_prefs.defaults['kobo_serials'] = []
|
||||||
|
plugin_prefs.defaults['kobo_directory'] = u''
|
||||||
|
|
||||||
from calibre_plugins.obok_dedrm.__init__ import PLUGIN_NAME, PLUGIN_VERSION
|
from calibre_plugins.obok_dedrm.__init__ import PLUGIN_NAME, PLUGIN_VERSION
|
||||||
from calibre_plugins.obok_dedrm.utilities import (debug_print)
|
from calibre_plugins.obok_dedrm.utilities import (debug_print)
|
||||||
@@ -37,6 +32,7 @@ class ConfigWidget(QWidget):
|
|||||||
|
|
||||||
# copy of preferences
|
# copy of preferences
|
||||||
self.tmpserials = plugin_prefs['kobo_serials']
|
self.tmpserials = plugin_prefs['kobo_serials']
|
||||||
|
self.kobodirectory = plugin_prefs['kobo_directory']
|
||||||
|
|
||||||
combo_label = QLabel(_('When should Obok try to insert EPUBs into existing calibre entries?'), self)
|
combo_label = QLabel(_('When should Obok try to insert EPUBs into existing calibre entries?'), self)
|
||||||
layout.addWidget(combo_label)
|
layout.addWidget(combo_label)
|
||||||
@@ -48,20 +44,35 @@ 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.setToolTip(_("Click to specify the Kobo directory"))
|
||||||
|
self.kobo_directory_button.setText("Kobo directory")
|
||||||
|
self.kobo_directory_button.clicked.connect(self.edit_kobo_directory)
|
||||||
|
layout.addWidget(self.kobo_directory_button)
|
||||||
|
|
||||||
|
|
||||||
def edit_serials(self):
|
def edit_serials(self):
|
||||||
d = ManageKeysDialog(self,u"Kobo device serial numbers",self.tmpserials, AddSerialDialog)
|
d = ManageKeysDialog(self,"Kobo device serial number",self.tmpserials, AddSerialDialog)
|
||||||
d.exec_()
|
d.exec_()
|
||||||
|
|
||||||
|
|
||||||
|
def edit_kobo_directory(self):
|
||||||
|
tmpkobodirectory = QFileDialog.getExistingDirectory(self, "Select Kobo directory", self.kobodirectory or "/home", QFileDialog.ShowDirsOnly)
|
||||||
|
|
||||||
|
if tmpkobodirectory != u"" and tmpkobodirectory is not None:
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -74,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))
|
||||||
|
|
||||||
@@ -82,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)
|
||||||
@@ -97,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)
|
||||||
@@ -138,40 +149,18 @@ 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)
|
||||||
self.listy.clear()
|
self.listy.clear()
|
||||||
self.populate_list()
|
self.populate_list()
|
||||||
|
|
||||||
def rename_key(self):
|
|
||||||
if not self.listy.currentItem():
|
|
||||||
errmsg = u"No {0} selected to rename. Highlight a keyfile first.".format(self.key_type_name)
|
|
||||||
r = error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION),
|
|
||||||
_(errmsg), show=True, show_copy_button=False)
|
|
||||||
return
|
|
||||||
|
|
||||||
d = RenameKeyDialog(self)
|
|
||||||
d.exec_()
|
|
||||||
|
|
||||||
if d.result() != d.Accepted:
|
|
||||||
# rename cancelled or moot.
|
|
||||||
return
|
|
||||||
keyname = unicode(self.listy.currentItem().text())
|
|
||||||
if not question_dialog(self, "{0} {1}: Confirm Rename".format(PLUGIN_NAME, PLUGIN_VERSION), u"Do you really want to rename the {2} named <strong>{0}</strong> to <strong>{1}</strong>?".format(keyname,d.key_name,self.key_type_name), show_copy_button=False, default_yes=False):
|
|
||||||
return
|
|
||||||
self.plugin_keys[d.key_name] = self.plugin_keys[keyname]
|
|
||||||
del self.plugin_keys[keyname]
|
|
||||||
|
|
||||||
self.listy.clear()
|
|
||||||
self.populate_list()
|
|
||||||
|
|
||||||
def delete_key(self):
|
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)
|
||||||
|
|
||||||
@@ -182,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)
|
||||||
|
|
||||||
@@ -193,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)
|
||||||
@@ -207,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)
|
||||||
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
@@ -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')
|
||||||
@@ -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.
|
||||||
#
|
#
|
||||||
@@ -150,9 +160,10 @@
|
|||||||
# after all.
|
# after all.
|
||||||
#
|
#
|
||||||
"""Manage all Kobo books, either encrypted or DRM-free."""
|
"""Manage all Kobo books, either encrypted or DRM-free."""
|
||||||
|
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
|
||||||
@@ -172,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']
|
||||||
@@ -230,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)
|
||||||
@@ -275,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)
|
||||||
|
|
||||||
@@ -290,8 +301,8 @@ class KoboLibrary(object):
|
|||||||
written by the Kobo Desktop Edition application, including the list
|
written by the Kobo Desktop Edition application, including the list
|
||||||
of books, their titles, and the user's encryption key(s)."""
|
of books, their titles, and the user's encryption key(s)."""
|
||||||
|
|
||||||
def __init__ (self, serials = [], device_path = None):
|
def __init__ (self, serials = [], device_path = None, desktopkobodir = u""):
|
||||||
print __about__
|
print(__about__)
|
||||||
self.kobodir = u""
|
self.kobodir = u""
|
||||||
kobodb = u""
|
kobodb = u""
|
||||||
|
|
||||||
@@ -308,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""
|
||||||
@@ -322,62 +333,92 @@ 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""
|
||||||
|
|
||||||
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 desktopkobodir != u'':
|
||||||
import _winreg as winreg
|
self.kobodir = desktopkobodir
|
||||||
if sys.getwindowsversion().major > 5:
|
|
||||||
if 'LOCALAPPDATA' in os.environ.keys():
|
if (self.kobodir == u""):
|
||||||
# Python 2.x does not return unicode env. Use Python 3.x
|
if sys.platform.startswith('win'):
|
||||||
self.kobodir = winreg.ExpandEnvironmentStrings(u"%LOCALAPPDATA%")
|
try:
|
||||||
if (self.kobodir == u""):
|
import winreg
|
||||||
if 'USERPROFILE' in os.environ.keys():
|
except ImportError:
|
||||||
# Python 2.x does not return unicode env. Use Python 3.x
|
import _winreg as winreg
|
||||||
self.kobodir = os.path.join(winreg.ExpandEnvironmentStrings(u"%USERPROFILE%"), u"Local Settings", u"Application Data")
|
if sys.getwindowsversion().major > 5:
|
||||||
self.kobodir = os.path.join(self.kobodir, u"Kobo", u"Kobo Desktop Edition")
|
if 'LOCALAPPDATA' in os.environ.keys():
|
||||||
elif sys.platform.startswith('darwin'):
|
# Python 2.x does not return unicode env. Use Python 3.x
|
||||||
self.kobodir = os.path.join(os.environ['HOME'], u"Library", u"Application Support", u"Kobo", u"Kobo Desktop Edition")
|
self.kobodir = winreg.ExpandEnvironmentStrings("%LOCALAPPDATA%")
|
||||||
#elif linux_path != None:
|
if (self.kobodir == u""):
|
||||||
# Probably Linux, let's get the wine prefix and path to Kobo.
|
if 'USERPROFILE' in os.environ.keys():
|
||||||
# self.kobodir = os.path.join(linux_path, u"Local Settings", u"Application Data", u"Kobo", u"Kobo Desktop Edition")
|
# Python 2.x does not return unicode env. Use Python 3.x
|
||||||
|
self.kobodir = os.path.join(winreg.ExpandEnvironmentStrings("%USERPROFILE%"), "Local Settings", "Application Data")
|
||||||
|
self.kobodir = os.path.join(self.kobodir, "Kobo", "Kobo Desktop Edition")
|
||||||
|
elif sys.platform.startswith('darwin'):
|
||||||
|
self.kobodir = os.path.join(os.environ['HOME'], "Library", "Application Support", "Kobo", "Kobo Desktop Edition")
|
||||||
|
elif sys.platform.startswith('linux'):
|
||||||
|
|
||||||
|
#sets ~/.config/calibre as the location to store the kobodir location info file and creates this directory if necessary
|
||||||
|
kobodir_cache_dir = os.path.join(os.environ['HOME'], ".config", "calibre")
|
||||||
|
if not os.path.isdir(kobodir_cache_dir):
|
||||||
|
os.mkdir(kobodir_cache_dir)
|
||||||
|
|
||||||
|
#appends the name of the file we're storing the kobodir location info to the above path
|
||||||
|
kobodir_cache_file = str(kobodir_cache_dir) + "/" + "kobo location"
|
||||||
|
|
||||||
|
"""if the above file does not exist, recursively searches from the root
|
||||||
|
of the filesystem until kobodir is found and stores the location of kobodir
|
||||||
|
in that file so this loop can be skipped in the future"""
|
||||||
|
original_stdout = sys.stdout
|
||||||
|
if not os.path.isfile(kobodir_cache_file):
|
||||||
|
for root, dirs, files in os.walk('/'):
|
||||||
|
for file in files:
|
||||||
|
if file == 'Kobo.sqlite':
|
||||||
|
kobo_linux_path = str(root)
|
||||||
|
with open(kobodir_cache_file, 'w') as f:
|
||||||
|
sys.stdout = f
|
||||||
|
print(kobo_linux_path, end='')
|
||||||
|
sys.stdout = original_stdout
|
||||||
|
|
||||||
|
f = open(kobodir_cache_file, 'r' )
|
||||||
|
self.kobodir = f.read()
|
||||||
|
|
||||||
# desktop versions use Kobo.sqlite
|
# 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
|
||||||
self.kobodir = u""
|
self.kobodir = u""
|
||||||
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()
|
||||||
@@ -430,27 +471,43 @@ 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, let's try ipconfig under wine
|
# probably linux
|
||||||
|
|
||||||
|
# let's try ip
|
||||||
|
c = re.compile('\s(' + '[0-9a-f]{2}:' * 5 + '[0-9a-f]{2})(\s|$)', re.IGNORECASE)
|
||||||
|
for line in os.popen('ip -br link'):
|
||||||
|
m = c.search(line)
|
||||||
|
if m:
|
||||||
|
macaddrs.append(m.group(1).upper())
|
||||||
|
|
||||||
|
# let's try ipconfig under wine
|
||||||
c = re.compile('\s(' + '[0-9a-f]{2}-' * 5 + '[0-9a-f]{2})(\s|$)', re.IGNORECASE)
|
c = re.compile('\s(' + '[0-9a-f]{2}-' * 5 + '[0-9a-f]{2})(\s|$)', re.IGNORECASE)
|
||||||
for line in os.popen('ipconfig /all'):
|
for line in os.popen('ipconfig /all'):
|
||||||
m = c.search(line)
|
m = c.search(line)
|
||||||
@@ -475,14 +532,14 @@ class KoboLibrary(object):
|
|||||||
pass
|
pass
|
||||||
row = cursor.fetchone()
|
row = cursor.fetchone()
|
||||||
return userids
|
return userids
|
||||||
|
|
||||||
def __getuserkeys (self, macaddr):
|
def __getuserkeys (self, macaddr):
|
||||||
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
|
||||||
|
|
||||||
@@ -543,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:
|
||||||
@@ -591,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
|
||||||
|
|
||||||
@@ -666,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():
|
||||||
@@ -689,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()
|
||||||
@@ -703,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")
|
||||||
@@ -719,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
|
||||||
|
|
||||||
|
|
||||||
@@ -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 it’s not working for you , you can save a lot of time by using the plugin with Calibre in debug mode. This will print out a lot of helpful info that can be copied into any online help requests.</p>
|
<p >If you find that it’s not working for you, you can save a lot of time by using the plugin with Calibre in debug mode. This will print out a lot of helpful info that can be copied into any online help requests.</p>
|
||||||
|
|
||||||
<p>Open a command prompt (terminal window) and type "calibre-debug -g" (without the quotes). Calibre will launch, and you can use the plugin the usual way. The debug info will be output to the original command prompt (terminal window). Copy the resulting output and paste it into the comment you make at Apprentice Alf's blog.</p>
|
<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>
|
||||||
|
|
||||||
BIN
Obok_plugin/translations/sv.mo
Normal file
BIN
Obok_plugin/translations/sv.mo
Normal file
Binary file not shown.
364
Obok_plugin/translations/sv.po
Normal file
364
Obok_plugin/translations/sv.po
Normal file
@@ -0,0 +1,364 @@
|
|||||||
|
# SOME DESCRIPTIVE TITLE.
|
||||||
|
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||||
|
# This file is distributed under the same license as the PACKAGE package.
|
||||||
|
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: \n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2014-11-17 12:51+0100\n"
|
||||||
|
"PO-Revision-Date: 2021-01-19 12:20+0100\n"
|
||||||
|
"Language: sv\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
|
"Last-Translator: \n"
|
||||||
|
"Language-Team: \n"
|
||||||
|
"X-Generator: Poedit 2.4.2\n"
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:80
|
||||||
|
msgid ""
|
||||||
|
"<p>No books found in Kobo Library\n"
|
||||||
|
"Are you sure it's installed\\configured\\synchronized?"
|
||||||
|
msgstr ""
|
||||||
|
"<p>Inga böcker hittades i Kobo-bibliotek\n"
|
||||||
|
"Är du säker på att den är installerad\\konfigurerad\\synkroniserad?"
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:87
|
||||||
|
msgid "Legacy key found: "
|
||||||
|
msgstr "Äldre nyckel hittades: "
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:93
|
||||||
|
msgid "Trouble retrieving keys with newer obok method."
|
||||||
|
msgstr "Problem med att hämta nycklar med nyare obok-metod."
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:97
|
||||||
|
msgid "Found {0} possible keys to try."
|
||||||
|
msgstr "Hittade {0} möjliga nycklar att försöka med."
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:99
|
||||||
|
msgid "<p>No userkeys found to decrypt books with. No point in proceeding."
|
||||||
|
msgstr ""
|
||||||
|
"<p>Inga användarnycklar hittades för att dekryptera böcker med. Det är ingen "
|
||||||
|
"idé att fortsätta."
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:115
|
||||||
|
msgid "{} - Decryption canceled by user."
|
||||||
|
msgstr "{} - Dekryptering avbröts av användaren."
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:135
|
||||||
|
msgid "{} - \"Add books\" canceled by user."
|
||||||
|
msgstr "{} - \"Lägg till böcker\" avbröts av användaren."
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:137
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:156
|
||||||
|
msgid "{} - wrapping up results."
|
||||||
|
msgstr "{} - samlar in resultat."
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:153
|
||||||
|
msgid "{} - User opted not to try to insert EPUB formats"
|
||||||
|
msgstr "{} - Användaren valde att inte försöka infoga EPUB-format"
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:188
|
||||||
|
msgid "{0} - Decrypting {1}"
|
||||||
|
msgstr "{0} - Dekrypterar {1}"
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:197
|
||||||
|
msgid "{0} - Couldn't decrypt {1}"
|
||||||
|
msgstr "{0} - Kunde inte dekryptera {1}"
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:198
|
||||||
|
msgid "decryption errors"
|
||||||
|
msgstr "dekrypteringsfel"
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:213
|
||||||
|
msgid "{0} - Added {1}"
|
||||||
|
msgstr "{0} - Lade till {1}"
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:218
|
||||||
|
msgid "{0} - {1} already exists. Will try to add format later."
|
||||||
|
msgstr "{0} - {1} finns redan. Kommer att försöka lägga till format senare."
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:219
|
||||||
|
msgid "duplicate detected"
|
||||||
|
msgstr "dubblett upptäcktes"
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:233
|
||||||
|
msgid "{0} - Successfully added EPUB format to existing {1}"
|
||||||
|
msgstr "{0} - EPUB-formatet har lagts till i befintliga {1}"
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:236
|
||||||
|
msgid ""
|
||||||
|
"{0} - Error adding EPUB format to existing {1}. This really shouldn't happen."
|
||||||
|
msgstr ""
|
||||||
|
"{0} - Fel vid tilläggning av EPUB-formatet till befintliga {1}. Detta borde "
|
||||||
|
"verkligen inte hända."
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:259
|
||||||
|
msgid "{} - \"Insert formats\" canceled by user."
|
||||||
|
msgstr "{} - \"Infoga format\" avbröts av användaren."
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:291
|
||||||
|
msgid ""
|
||||||
|
"<p><b>{0}</b> EPUB{2} successfully added to library.<br /><br /><b>{1}</b> "
|
||||||
|
msgstr "<p><b>{0}</b> EPUB{2} har lagts till i bibliotek.<br /><br /><b>{1}</b> "
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:292
|
||||||
|
msgid ""
|
||||||
|
"not added because books with the same title/author were detected.<br /><br /"
|
||||||
|
">Would you like to try and add the EPUB format{0}"
|
||||||
|
msgstr ""
|
||||||
|
"lades inte till eftersom böcker med samma titel/författare upptäcktes.<br/"
|
||||||
|
"><br />Vill du försöka lägga till EPUB-formatet{0}"
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:293
|
||||||
|
msgid ""
|
||||||
|
" to those existing entries?<br /><br />NOTE: no pre-existing EPUBs will be "
|
||||||
|
"overwritten."
|
||||||
|
msgstr ""
|
||||||
|
" till dessa befintliga poster?<br /><br />OBS: inga befintliga EPUB:er kommer "
|
||||||
|
"att skrivas över."
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:295
|
||||||
|
msgid ""
|
||||||
|
"{0} -- not added because of {1} in your library.\n"
|
||||||
|
"\n"
|
||||||
|
msgstr ""
|
||||||
|
"{0} -- lades inte till på grund av {1} i ditt bibliotek.\n"
|
||||||
|
"\n"
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:297
|
||||||
|
msgid "<p><b>{0}</b> -- not added because of {1} in your library.<br /><br />"
|
||||||
|
msgstr ""
|
||||||
|
"<p><b>{0}</b> -- lades inte till på grund av {1} i ditt bibliotek.<br /><br />"
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:298
|
||||||
|
msgid ""
|
||||||
|
"Would you like to try and add the EPUB format to an available calibre "
|
||||||
|
"duplicate?<br /><br />"
|
||||||
|
msgstr ""
|
||||||
|
"Vill du försöka lägga till EPUB-formatet till en tillgänglig calibre-dubblett?"
|
||||||
|
"<br /><br />"
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:299
|
||||||
|
msgid "NOTE: no pre-existing EPUB will be overwritten."
|
||||||
|
msgstr "OBS: ingen befintlig EPUB kommer att skrivas över."
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:346
|
||||||
|
msgid "Trying key: "
|
||||||
|
msgstr "Försöker med nyckel: "
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:378
|
||||||
|
msgid "Decryption failed, trying next key."
|
||||||
|
msgstr "Dekryptering misslyckades, försöker med nästa nyckel."
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:382
|
||||||
|
msgid "Unknown Error decrypting, trying next key.."
|
||||||
|
msgstr "Okänt fel vid dekryptering, försöker med nästa nyckel.."
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:395
|
||||||
|
msgid ""
|
||||||
|
"<p>All selected Kobo books added as new calibre books or inserted into "
|
||||||
|
"existing calibre ebooks.<br /><br />No issues."
|
||||||
|
msgstr ""
|
||||||
|
"<p>Alla valda Kobo-böcker har lagts till som nya calibre-böcker eller infogats "
|
||||||
|
"i befintliga calibre-e-böcker.<br /><br />Inga problem."
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:399
|
||||||
|
msgid "<p>{0} successfully added."
|
||||||
|
msgstr "<p>{0} har lagts till."
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:403
|
||||||
|
msgid ""
|
||||||
|
"<p>Not all selected Kobo books made it into calibre.<br /><br />View report "
|
||||||
|
"for details."
|
||||||
|
msgstr ""
|
||||||
|
"<p>Inte alla valda Kobo-böcker lades till i calibre.<br /><br />Visa rapport "
|
||||||
|
"för detaljer."
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:404
|
||||||
|
msgid "<p><b>Total attempted:</b> {}</p>\n"
|
||||||
|
msgstr "<p><b>Försök totalt:</b> {}</p>\n"
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:405
|
||||||
|
msgid "<p><b>Decryption errors:</b> {}</p>\n"
|
||||||
|
msgstr "<p><b>Dekrypteringsfel:</b> {}</p>\n"
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:411
|
||||||
|
msgid "<p><b>New Books created:</b> {}</p>\n"
|
||||||
|
msgstr "<p><b>Nya böcker skapade:</b> {}</p>\n"
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:418
|
||||||
|
msgid "<p><b>Duplicates that weren't added:</b> {}</p>\n"
|
||||||
|
msgstr "<p><b>Dubbletter som inte har lagts till:</b> {}</p>\n"
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:426
|
||||||
|
msgid "<p><b>Book imports cancelled by user:</b> {}</p>\n"
|
||||||
|
msgstr "<p><b>Bokimport avbröts av användaren:</b> {}</p>\n"
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:428
|
||||||
|
msgid "<p><b>New EPUB formats inserted in existing calibre books:</b> {0}</p>\n"
|
||||||
|
msgstr ""
|
||||||
|
"<p><b>Nya EPUB-format infogade i befintliga calibre-böcker:</b> {0}</p>\n"
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:434
|
||||||
|
msgid ""
|
||||||
|
"<p><b>EPUB formats NOT inserted into existing calibre books:</b> {}<br />\n"
|
||||||
|
msgstr ""
|
||||||
|
"<p><b>EPUB-format som INTE infogats i befintliga calibre-böcker:</b> {}<br />\n"
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:435
|
||||||
|
msgid ""
|
||||||
|
"(Either because the user <i>chose</i> not to insert them, or because all "
|
||||||
|
"duplicates already had an EPUB format)"
|
||||||
|
msgstr ""
|
||||||
|
"(Antingen för att användaren <i>valde</i> att inte infoga dem, eller för att "
|
||||||
|
"alla dubbletter redan hade ett EPUB-format)"
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:444
|
||||||
|
msgid "<p><b>Format imports cancelled by user:</b> {}</p>\n"
|
||||||
|
msgstr "<p><b>Format-importen avbröts av användaren:</b> {}</p>\n"
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:458
|
||||||
|
msgid "Unknown Book Title"
|
||||||
|
msgstr "Okänd boktitel"
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:460
|
||||||
|
msgid "it couldn't be decrypted."
|
||||||
|
msgstr "den kunde inte dekrypteras."
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:462
|
||||||
|
msgid ""
|
||||||
|
"user CHOSE not to insert the new EPUB format, or all existing calibre entries "
|
||||||
|
"HAD an EPUB format already."
|
||||||
|
msgstr ""
|
||||||
|
"användaren VALDE att inte infoga det nya EPUB-formatet, eller alla befintliga "
|
||||||
|
"calibre-poster hade redan ett EPUB-format."
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:464
|
||||||
|
msgid "of unknown reasons. Gosh I'm embarrassed!"
|
||||||
|
msgstr "av okända skäl. Jag skäms!"
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:465
|
||||||
|
msgid "<p>{0} not added because {1}"
|
||||||
|
msgstr "<p>{0} lades inte till på grund av {1}"
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\common_utils.py:226
|
||||||
|
msgid "Help"
|
||||||
|
msgstr "Hjälp"
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\common_utils.py:235
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\utilities.py:214
|
||||||
|
msgid "Restart required"
|
||||||
|
msgstr "Kräver omstart"
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\common_utils.py:236
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\utilities.py:215
|
||||||
|
msgid ""
|
||||||
|
"Title image not found - you must restart Calibre before using this plugin!"
|
||||||
|
msgstr ""
|
||||||
|
"Titelbild hittades inte - du måste starta om calibre innan du använder denna "
|
||||||
|
"insticksmodul!"
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\common_utils.py:322
|
||||||
|
msgid "Undefined"
|
||||||
|
msgstr "Odefinierad"
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:30
|
||||||
|
msgid "When should Obok try to insert EPUBs into existing calibre entries?"
|
||||||
|
msgstr "När ska Obok försöka infoga EPUB:er i befintliga calibre-poster?"
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:33
|
||||||
|
msgid ""
|
||||||
|
"<p>Default behavior when duplicates are detected. None of the choices will "
|
||||||
|
"cause calibre ebooks to be overwritten"
|
||||||
|
msgstr ""
|
||||||
|
"<p>Standardbeteende när dubbletter upptäcks. Inget av alternativen kommer att "
|
||||||
|
"orsaka att calibre-e-böcker skrivs över"
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:35
|
||||||
|
msgid "Ask"
|
||||||
|
msgstr "Fråga"
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:35
|
||||||
|
msgid "Always"
|
||||||
|
msgstr "Alltid"
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:35
|
||||||
|
msgid "Never"
|
||||||
|
msgstr "Aldrig"
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:60
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\utilities.py:150
|
||||||
|
msgid " v"
|
||||||
|
msgstr " v"
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:65
|
||||||
|
msgid "Obok DeDRM"
|
||||||
|
msgstr "Obok DeDRM"
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:68
|
||||||
|
msgid "<a href=\"http://www.foo.com/\">Help</a>"
|
||||||
|
msgstr "<a href=\"http://www.foo.com/\">Hjälp</a>"
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:89
|
||||||
|
msgid "Select All"
|
||||||
|
msgstr "Välj alla"
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:90
|
||||||
|
msgid "Select all books to add them to the calibre library."
|
||||||
|
msgstr "Välj alla böcker för att lägga till dem i calibre-biblioteket."
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:92
|
||||||
|
msgid "All with DRM"
|
||||||
|
msgstr "Alla med DRM"
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:93
|
||||||
|
msgid "Select all books with DRM."
|
||||||
|
msgstr "Välj alla böcker med DRM."
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:95
|
||||||
|
msgid "All DRM free"
|
||||||
|
msgstr "Alla utan DRM"
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:96
|
||||||
|
msgid "Select all books without DRM."
|
||||||
|
msgstr "Välj alla böcker utan DRM."
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:146
|
||||||
|
msgid "Title"
|
||||||
|
msgstr "Titel"
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:146
|
||||||
|
msgid "Author"
|
||||||
|
msgstr "Författare"
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:146
|
||||||
|
msgid "Series"
|
||||||
|
msgstr "Serier"
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:369
|
||||||
|
msgid "Copy to clipboard"
|
||||||
|
msgstr "Kopiera till urklipp"
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:397
|
||||||
|
msgid "View Report"
|
||||||
|
msgstr "Visa rapport"
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\__init__.py:21
|
||||||
|
msgid "Removes DRM from Kobo kepubs and adds them to the library."
|
||||||
|
msgstr "Tar bort DRM från Kobo-kepubs och lägger till dem i biblioteket."
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\obok\obok.py:162
|
||||||
|
msgid "AES improper key used"
|
||||||
|
msgstr "Felaktig AES-nyckel används"
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\obok\obok.py:167
|
||||||
|
msgid "Failed to initialize AES key"
|
||||||
|
msgstr "Misslyckades med att initiera AES-nyckel"
|
||||||
|
|
||||||
|
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\obok\obok.py:175
|
||||||
|
msgid "AES decryption failed"
|
||||||
|
msgstr "AES-dekryptering misslyckades"
|
||||||
@@ -1,27 +1,27 @@
|
|||||||
|
#!/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
|
||||||
from calibre import prints
|
from calibre import prints
|
||||||
from calibre.gui2 import (error_dialog, gprefs)
|
from calibre.gui2 import (error_dialog, gprefs)
|
||||||
from calibre.gui2.actions import menu_action_unique_name
|
from calibre.gui2.actions import menu_action_unique_name
|
||||||
|
|
||||||
from calibre_plugins.obok_dedrm.__init__ import (PLUGIN_NAME,
|
from calibre_plugins.obok_dedrm.__init__ import (PLUGIN_NAME,
|
||||||
PLUGIN_SAFE_NAME, PLUGIN_VERSION, PLUGIN_DESCRIPTION)
|
PLUGIN_SAFE_NAME, PLUGIN_VERSION, PLUGIN_DESCRIPTION)
|
||||||
|
|
||||||
plugin_ID = None
|
plugin_ID = None
|
||||||
@@ -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()
|
||||||
@@ -62,7 +62,7 @@ except NameError:
|
|||||||
def format_plural(number, possessive=False):
|
def format_plural(number, possessive=False):
|
||||||
'''
|
'''
|
||||||
Cosmetic ditty to provide the proper string formatting variable to handle singular/plural situations
|
Cosmetic ditty to provide the proper string formatting variable to handle singular/plural situations
|
||||||
|
|
||||||
:param: number: variable that represents the count/len of something
|
:param: number: variable that represents the count/len of something
|
||||||
'''
|
'''
|
||||||
if not possessive:
|
if not possessive:
|
||||||
@@ -141,7 +141,7 @@ def showErrorDlg(errmsg, parent, trcbk=False):
|
|||||||
'''
|
'''
|
||||||
if trcbk:
|
if trcbk:
|
||||||
error= ''
|
error= ''
|
||||||
f=StringIO()
|
f=StringIO()
|
||||||
print_exc(file=f)
|
print_exc(file=f)
|
||||||
error_mess = f.getvalue().splitlines()
|
error_mess = f.getvalue().splitlines()
|
||||||
for line in error_mess:
|
for line in error_mess:
|
||||||
@@ -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
|
||||||
@@ -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')
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user