Compare commits
449 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76a47e0dd0 | ||
|
|
70a754fb46 | ||
|
|
21a7b13524 | ||
|
|
52bdbe95c9 | ||
|
|
495dda3809 | ||
|
|
52e83922c0 | ||
|
|
6cbc5285cb | ||
|
|
33b9630ca5 | ||
|
|
9346f86f73 | ||
|
|
8d2d6627cf | ||
|
|
6f198b247c | ||
|
|
9fb95eff41 | ||
|
|
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 | ||
|
|
3d0aa17b2e | ||
|
|
b17b913839 | ||
|
|
d73cd15090 | ||
|
|
e4f44604d7 | ||
|
|
6ab4f633f1 | ||
|
|
feae07c502 | ||
|
|
c8aaabcbca | ||
|
|
588d06e846 | ||
|
|
ca4bab45ec | ||
|
|
3f21bd9f5a | ||
|
|
454286b08b | ||
|
|
63c8b28efd | ||
|
|
9b001bfaf3 | ||
|
|
a76adf0ee1 | ||
|
|
49b064efa4 | ||
|
|
8f23bf2b30 | ||
|
|
1a38cdaf21 | ||
|
|
c20089676d | ||
|
|
f688bee0aa | ||
|
|
114c4988c0 | ||
|
|
0b206e3fc5 | ||
|
|
ed306a8488 | ||
|
|
34b533363a | ||
|
|
b1d13f2b23 | ||
|
|
613450f84d | ||
|
|
af6e479af4 | ||
|
|
90e822f470 | ||
|
|
5c4eed8f1b | ||
|
|
e665c47075 | ||
|
|
d6374f7eab | ||
|
|
0055386f7b | ||
|
|
30eeeea618 | ||
|
|
749731fdd4 | ||
|
|
95247503f0 | ||
|
|
79b10f3dfb | ||
|
|
d617822610 | ||
|
|
421877574f | ||
|
|
6956117e28 | ||
|
|
dd09da7dd9 | ||
|
|
75acbe5536 | ||
|
|
6ee560e425 | ||
|
|
f54b0aef5c | ||
|
|
a6ceea1ed9 | ||
|
|
599f33171f | ||
|
|
12ce977d79 | ||
|
|
4f1e9fcf43 | ||
|
|
7b45d2128c | ||
|
|
4400d8d1d4 | ||
|
|
85e3db8f7c | ||
|
|
29338db228 | ||
|
|
608159d71b | ||
|
|
20e0850001 | ||
|
|
ffd7d41bcd | ||
|
|
75cad40804 | ||
|
|
18d6413467 | ||
|
|
7619ee4e0f | ||
|
|
20d445acb7 | ||
|
|
a390d7a207 | ||
|
|
f04f9eca04 | ||
|
|
fe3b2873de | ||
|
|
1ece09023c | ||
|
|
8d9f384492 | ||
|
|
c3fbb83dbc | ||
|
|
0f23eac3b8 | ||
|
|
bcdcb23d0d | ||
|
|
a252dd0da6 | ||
|
|
2042354788 | ||
|
|
9bea20c7ab | ||
|
|
415df655a1 | ||
|
|
346b3e312c | ||
|
|
f981a548a3 | ||
|
|
4084a49872 | ||
|
|
347ad3cc05 | ||
|
|
e4b702e241 | ||
|
|
691a3d6955 | ||
|
|
fa317dc1cf | ||
|
|
6f0c36b67a | ||
|
|
ceacdbbb1b | ||
|
|
ff03c68674 | ||
|
|
84d4e4e0c8 | ||
|
|
a553df50d7 | ||
|
|
0b244b5781 | ||
|
|
ab4597dfd7 | ||
|
|
82e9927ace | ||
|
|
528092c05d | ||
|
|
faa19cc19b | ||
|
|
e6592841b6 | ||
|
|
482ac15055 | ||
|
|
cb74bd8cef | ||
|
|
956f3034ad | ||
|
|
fca7eaab8e | ||
|
|
0df66bcfc0 | ||
|
|
20ab5b354d | ||
|
|
46ce2ce0ea | ||
|
|
ca59704dc4 | ||
|
|
17300283d0 | ||
|
|
92ce0396fe | ||
|
|
5eb3338423 | ||
|
|
d65dd1ab87 | ||
|
|
5d75018719 | ||
|
|
1c3a12425e | ||
|
|
6b4d621159 | ||
|
|
53c16c916b | ||
|
|
34231cc252 | ||
|
|
c2615c4d3b | ||
|
|
908ebc5c58 | ||
|
|
4d7556e919 | ||
|
|
6feeb352fc | ||
|
|
bc3676c1bc | ||
|
|
a4ebec359b | ||
|
|
10cffca6b4 | ||
|
|
24a8c80617 | ||
|
|
b2338b71c0 | ||
|
|
16733c3198 | ||
|
|
3a931dfc90 | ||
|
|
eaa7a1afed | ||
|
|
dc5261870f | ||
|
|
a2ba5005c9 | ||
|
|
24922999dc | ||
|
|
e2170b4260 | ||
|
|
054ddc894b | ||
|
|
8cd4be6fb0 | ||
|
|
d67e05cf04 | ||
|
|
a5197a6abb | ||
|
|
cfc13db6c5 | ||
|
|
8aa2157d55 | ||
|
|
81b08dcf05 | ||
|
|
ca42e028a7 | ||
|
|
10963f6011 | ||
|
|
72968d2124 | ||
|
|
3e95168972 | ||
|
|
ecf1d76d90 | ||
|
|
e59f0f346d | ||
|
|
b6046d3f4b | ||
|
|
a863a4856a | ||
|
|
a13d08c3bc | ||
|
|
9434751a72 | ||
|
|
fc156852a4 | ||
|
|
0c67fd43a2 | ||
|
|
00a5c4e1d1 | ||
|
|
4ea0d81144 | ||
|
|
b1cccf4b25 | ||
|
|
fe6074949b | ||
|
|
2db7ee8894 | ||
|
|
93d8758462 | ||
|
|
f97bc078db | ||
|
|
2e96db6cdc | ||
|
|
0d530c0c46 | ||
|
|
488924d443 | ||
|
|
07485be2c0 | ||
|
|
e5e269fbae | ||
|
|
d54dc38c2d | ||
|
|
3c322f3695 | ||
|
|
c112c28f58 | ||
|
|
b8606cd182 | ||
|
|
f2190a6755 | ||
|
|
33d8a63f61 | ||
|
|
91b22c18c4 | ||
|
|
3317dc7330 | ||
|
|
aa866938f5 | ||
|
|
aa822de138 | ||
|
|
f5e66d42a1 | ||
|
|
c16d767b00 | ||
|
|
9a8d5f74a6 | ||
|
|
6be1323817 | ||
|
|
9b77255212 | ||
|
|
46426a9eae | ||
|
|
45ad3cedec | ||
|
|
d140b7e2dc | ||
|
|
e729ae8904 | ||
|
|
0837482686 | ||
|
|
4c9aacd01e | ||
|
|
6b2672ff7c | ||
|
|
39c9d57b15 | ||
|
|
9c347ca42f | ||
|
|
032fcfa422 | ||
|
|
35aaf20c8d | ||
|
|
b146e4b864 | ||
|
|
27d8f08b54 | ||
|
|
6db762bc40 | ||
|
|
c7c34274e9 | ||
|
|
cf922b6ba1 | ||
|
|
9d9c879413 | ||
|
|
c4fc10395b | ||
|
|
a30aace99c | ||
|
|
b1feca321d | ||
|
|
74a4c894cb | ||
|
|
5a502dbce3 | ||
|
|
a399d3b7bd | ||
|
|
cd2d74601a | ||
|
|
51919284ca | ||
|
|
d586f74faa | ||
|
|
a2f044e672 | ||
|
|
20bc936e99 | ||
|
|
748bd2d471 | ||
|
|
490ee4e5d8 | ||
|
|
c23b903420 | ||
|
|
602ff30b3a | ||
|
|
c010e3f77a | ||
|
|
0c03820db7 | ||
|
|
9fda194391 | ||
|
|
b661a6cdc5 | ||
|
|
0dcd18d524 | ||
|
|
0028027f71 | ||
|
|
4b3618246b | ||
|
|
07ea87edf4 | ||
|
|
899fd419ae | ||
|
|
f3f02adc98 | ||
|
|
0812438b9d | ||
|
|
26d9f7bd20 | ||
|
|
2c95633fcd | ||
|
|
07e532f59c | ||
|
|
882edb6c69 | ||
|
|
93f02c625a | ||
|
|
e95ed1a8ed | ||
|
|
ba5927a20d | ||
|
|
297a9ddc66 | ||
|
|
4f34a9a196 | ||
|
|
529dd3f160 | ||
|
|
4163d5ccf4 | ||
|
|
867ac35b45 | ||
|
|
427137b0fe | ||
|
|
ac9cdb1e98 | ||
|
|
2bedd75005 | ||
|
|
8b632e309f | ||
|
|
bc968f8eca | ||
|
|
00ac669f76 | ||
|
|
694dfafd39 | ||
|
|
a7856f5c32 | ||
|
|
38eabe7612 | ||
|
|
9162698f89 | ||
|
|
506d97d5f0 | ||
|
|
a76ba56cd8 | ||
|
|
8e73edc012 | ||
|
|
c386ac6e6d | ||
|
|
5f0671db7f | ||
|
|
bf03edd18c | ||
|
|
d427f758f6 | ||
|
|
92dafd94b2 | ||
|
|
c5ed30d8c8 | ||
|
|
b7de1dcea5 | ||
|
|
ab9d585190 | ||
|
|
b92458c8c2 | ||
|
|
4f19f5ac11 | ||
|
|
f027848bff | ||
|
|
26a54dd3d6 | ||
|
|
6c70a073d9 | ||
|
|
37696e1495 | ||
|
|
446d45da6b | ||
|
|
a73fbbb484 | ||
|
|
086d25a441 | ||
|
|
63219d6054 | ||
|
|
f0d920c158 | ||
|
|
e9a7312759 |
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
|
||||
33
.github/ISSUE_TEMPLATE/QUESTION.md
vendored
Normal file
33
.github/ISSUE_TEMPLATE/QUESTION.md
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
---
|
||||
name: Question
|
||||
about: Questions for DeDRM Project
|
||||
title: "[QUESTION] Title"
|
||||
labels: Question
|
||||
---
|
||||
|
||||
## CheckList
|
||||
<!-- Check with `[x]` -->
|
||||
- [ ] `The Title` and The `Log Title` are setted correctly.
|
||||
- [ ] Clarified about `my environment`.
|
||||
- [ ] Code block is used for `the log`.
|
||||
<!-- If you don't know the version, please specify 'Unknown'. -->
|
||||
<!-- In case of markdown To use the code block, enclose it in ```. -->
|
||||
<!-- If you don't need Log, please delete the log section. -->
|
||||
|
||||
---
|
||||
|
||||
## Title
|
||||
<!-- content -->
|
||||
|
||||
## My Environment
|
||||
### Calibre: `Version`
|
||||
### Kindle: `Version`
|
||||
### DeDRM: `Version`
|
||||
|
||||
## Log
|
||||
<details><summary>Log Title</summary>
|
||||
|
||||
```log
|
||||
PUT YOUR LOG
|
||||
```
|
||||
</details>
|
||||
39
.github/workflows/Format.yaml
vendored
Normal file
39
.github/workflows/Format.yaml
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
name: Python code format
|
||||
on:
|
||||
push:
|
||||
branches: master
|
||||
jobs:
|
||||
Format:
|
||||
if: "contains(github.event.head_commit.message, '!format')"
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@main
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@main
|
||||
with:
|
||||
python-version: 3.x
|
||||
- uses: actions/cache@main
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-format
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-format
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install autopep8 pycodestyle
|
||||
- name: Format by autopep8 then Push
|
||||
env:
|
||||
GIT_EMAIL: github-actions[bot]@users.noreply.github.com
|
||||
GIT_ACTOR: github-actions[bot]
|
||||
run: |
|
||||
export HASH_SHORT=$(git rev-parse --short HEAD)
|
||||
git checkout -b format--${HASH_SHORT}
|
||||
git config --global user.email $GIT_EMAIL
|
||||
git config --global user.name $GIT_ACTOR
|
||||
python -m autopep8 --in-place --aggressive --aggressive --experimental -r ./
|
||||
git add -A
|
||||
git commit -m 'Format by autopep8' -m From: -m $(git rev-parse HEAD)
|
||||
git push --set-upstream origin format--${HASH_SHORT}
|
||||
26
.github/workflows/Lint.yaml
vendored
Normal file
26
.github/workflows/Lint.yaml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
name: Python code review
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
Test:
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@main
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@main
|
||||
with:
|
||||
python-version: 3.x
|
||||
- uses: actions/cache@main
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-lint
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-lint
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install flake8
|
||||
- name: Lint with flake8
|
||||
run: |
|
||||
python -m flake8 . --builtins=_,I --ignore=E501 --count --benchmark --show-source --statistics
|
||||
99
.gitignore
vendored
99
.gitignore
vendored
@@ -1,100 +1 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# 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
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# =========================
|
||||
# Operating System Files
|
||||
# =========================
|
||||
|
||||
# OSX
|
||||
# =========================
|
||||
|
||||
.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
|
||||
|
||||
@@ -1,341 +0,0 @@
|
||||
#! /usr/bin/python
|
||||
|
||||
# ineptepub.pyw, version 2
|
||||
|
||||
# To run this program 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.
|
||||
|
||||
# Revision history:
|
||||
# 1 - Initial release
|
||||
# 2 - Rename to INEPT, fix exit code
|
||||
|
||||
"""
|
||||
Decrypt Adobe ADEPT-encrypted EPUB books.
|
||||
"""
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
|
||||
import sys
|
||||
import os
|
||||
import zlib
|
||||
import zipfile
|
||||
from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED
|
||||
from contextlib import closing
|
||||
import xml.etree.ElementTree as etree
|
||||
import Tkinter
|
||||
import Tkconstants
|
||||
import tkFileDialog
|
||||
import tkMessageBox
|
||||
|
||||
try:
|
||||
from Crypto.Cipher import AES
|
||||
from Crypto.PublicKey import RSA
|
||||
except ImportError:
|
||||
AES = None
|
||||
RSA = None
|
||||
|
||||
META_NAMES = ('mimetype', 'META-INF/rights.xml', 'META-INF/encryption.xml')
|
||||
NSMAP = {'adept': 'http://ns.adobe.com/adept',
|
||||
'enc': 'http://www.w3.org/2001/04/xmlenc#'}
|
||||
|
||||
|
||||
# ASN.1 parsing code from tlslite
|
||||
|
||||
def bytesToNumber(bytes):
|
||||
total = 0L
|
||||
multiplier = 1L
|
||||
for count in range(len(bytes)-1, -1, -1):
|
||||
byte = bytes[count]
|
||||
total += multiplier * byte
|
||||
multiplier *= 256
|
||||
return total
|
||||
|
||||
class ASN1Error(Exception):
|
||||
pass
|
||||
|
||||
class ASN1Parser(object):
|
||||
class Parser(object):
|
||||
def __init__(self, bytes):
|
||||
self.bytes = bytes
|
||||
self.index = 0
|
||||
|
||||
def get(self, length):
|
||||
if self.index + length > len(self.bytes):
|
||||
raise ASN1Error("Error decoding ASN.1")
|
||||
x = 0
|
||||
for count in range(length):
|
||||
x <<= 8
|
||||
x |= self.bytes[self.index]
|
||||
self.index += 1
|
||||
return x
|
||||
|
||||
def getFixBytes(self, lengthBytes):
|
||||
bytes = self.bytes[self.index : self.index+lengthBytes]
|
||||
self.index += lengthBytes
|
||||
return bytes
|
||||
|
||||
def getVarBytes(self, lengthLength):
|
||||
lengthBytes = self.get(lengthLength)
|
||||
return self.getFixBytes(lengthBytes)
|
||||
|
||||
def getFixList(self, length, lengthList):
|
||||
l = [0] * lengthList
|
||||
for x in range(lengthList):
|
||||
l[x] = self.get(length)
|
||||
return l
|
||||
|
||||
def getVarList(self, length, lengthLength):
|
||||
lengthList = self.get(lengthLength)
|
||||
if lengthList % length != 0:
|
||||
raise ASN1Error("Error decoding ASN.1")
|
||||
lengthList = int(lengthList/length)
|
||||
l = [0] * lengthList
|
||||
for x in range(lengthList):
|
||||
l[x] = self.get(length)
|
||||
return l
|
||||
|
||||
def startLengthCheck(self, lengthLength):
|
||||
self.lengthCheck = self.get(lengthLength)
|
||||
self.indexCheck = self.index
|
||||
|
||||
def setLengthCheck(self, length):
|
||||
self.lengthCheck = length
|
||||
self.indexCheck = self.index
|
||||
|
||||
def stopLengthCheck(self):
|
||||
if (self.index - self.indexCheck) != self.lengthCheck:
|
||||
raise ASN1Error("Error decoding ASN.1")
|
||||
|
||||
def atLengthCheck(self):
|
||||
if (self.index - self.indexCheck) < self.lengthCheck:
|
||||
return False
|
||||
elif (self.index - self.indexCheck) == self.lengthCheck:
|
||||
return True
|
||||
else:
|
||||
raise ASN1Error("Error decoding ASN.1")
|
||||
|
||||
def __init__(self, bytes):
|
||||
p = self.Parser(bytes)
|
||||
p.get(1)
|
||||
self.length = self._getASN1Length(p)
|
||||
self.value = p.getFixBytes(self.length)
|
||||
|
||||
def getChild(self, which):
|
||||
p = self.Parser(self.value)
|
||||
for x in range(which+1):
|
||||
markIndex = p.index
|
||||
p.get(1)
|
||||
length = self._getASN1Length(p)
|
||||
p.getFixBytes(length)
|
||||
return ASN1Parser(p.bytes[markIndex:p.index])
|
||||
|
||||
def _getASN1Length(self, p):
|
||||
firstLength = p.get(1)
|
||||
if firstLength<=127:
|
||||
return firstLength
|
||||
else:
|
||||
lengthLength = firstLength & 0x7F
|
||||
return p.get(lengthLength)
|
||||
|
||||
|
||||
class ZipInfo(zipfile.ZipInfo):
|
||||
def __init__(self, *args, **kwargs):
|
||||
if 'compress_type' in kwargs:
|
||||
compress_type = kwargs.pop('compress_type')
|
||||
super(ZipInfo, self).__init__(*args, **kwargs)
|
||||
self.compress_type = compress_type
|
||||
|
||||
|
||||
class Decryptor(object):
|
||||
def __init__(self, bookkey, encryption):
|
||||
enc = lambda tag: '{%s}%s' % (NSMAP['enc'], tag)
|
||||
self._aes = AES.new(bookkey, AES.MODE_CBC)
|
||||
encryption = etree.fromstring(encryption)
|
||||
self._encrypted = encrypted = set()
|
||||
expr = './%s/%s/%s' % (enc('EncryptedData'), enc('CipherData'),
|
||||
enc('CipherReference'))
|
||||
for elem in encryption.findall(expr):
|
||||
path = elem.get('URI', None)
|
||||
if path is not None:
|
||||
encrypted.add(path)
|
||||
|
||||
def decompress(self, bytes):
|
||||
dc = zlib.decompressobj(-15)
|
||||
bytes = dc.decompress(bytes)
|
||||
ex = dc.decompress('Z') + dc.flush()
|
||||
if ex:
|
||||
bytes = bytes + ex
|
||||
return bytes
|
||||
|
||||
def decrypt(self, path, data):
|
||||
if path in self._encrypted:
|
||||
data = self._aes.decrypt(data)[16:]
|
||||
data = data[:-ord(data[-1])]
|
||||
data = self.decompress(data)
|
||||
return data
|
||||
|
||||
|
||||
class ADEPTError(Exception):
|
||||
pass
|
||||
|
||||
def cli_main(argv=sys.argv):
|
||||
progname = os.path.basename(argv[0])
|
||||
if AES is None:
|
||||
print "%s: This script requires PyCrypto, which must be installed " \
|
||||
"separately. Read the top-of-script comment for details." % \
|
||||
(progname,)
|
||||
return 1
|
||||
if len(argv) != 4:
|
||||
print "usage: %s KEYFILE INBOOK OUTBOOK" % (progname,)
|
||||
return 1
|
||||
keypath, inpath, outpath = argv[1:]
|
||||
with open(keypath, 'rb') as f:
|
||||
keyder = f.read()
|
||||
key = ASN1Parser([ord(x) for x in keyder])
|
||||
key = [bytesToNumber(key.getChild(x).value) for x in xrange(1, 4)]
|
||||
rsa = RSA.construct(key)
|
||||
with closing(ZipFile(open(inpath, 'rb'))) as inf:
|
||||
namelist = set(inf.namelist())
|
||||
if 'META-INF/rights.xml' not in namelist or \
|
||||
'META-INF/encryption.xml' not in namelist:
|
||||
raise ADEPTError('%s: not an ADEPT EPUB' % (inpath,))
|
||||
for name in META_NAMES:
|
||||
namelist.remove(name)
|
||||
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
|
||||
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
||||
expr = './/%s' % (adept('encryptedKey'),)
|
||||
bookkey = ''.join(rights.findtext(expr))
|
||||
bookkey = rsa.decrypt(bookkey.decode('base64'))
|
||||
# Padded as per RSAES-PKCS1-v1_5
|
||||
if bookkey[-17] != '\x00':
|
||||
raise ADEPTError('problem decrypting session key')
|
||||
encryption = inf.read('META-INF/encryption.xml')
|
||||
decryptor = Decryptor(bookkey[-16:], encryption)
|
||||
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
|
||||
with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf:
|
||||
zi = ZipInfo('mimetype', compress_type=ZIP_STORED)
|
||||
outf.writestr(zi, inf.read('mimetype'))
|
||||
for path in namelist:
|
||||
data = inf.read(path)
|
||||
outf.writestr(path, decryptor.decrypt(path, data))
|
||||
return 0
|
||||
|
||||
|
||||
class DecryptionDialog(Tkinter.Frame):
|
||||
def __init__(self, root):
|
||||
Tkinter.Frame.__init__(self, root, border=5)
|
||||
self.status = Tkinter.Label(self, text='Select files for decryption')
|
||||
self.status.pack(fill=Tkconstants.X, expand=1)
|
||||
body = Tkinter.Frame(self)
|
||||
body.pack(fill=Tkconstants.X, expand=1)
|
||||
sticky = Tkconstants.E + Tkconstants.W
|
||||
body.grid_columnconfigure(1, weight=2)
|
||||
Tkinter.Label(body, text='Key file').grid(row=0)
|
||||
self.keypath = Tkinter.Entry(body, width=30)
|
||||
self.keypath.grid(row=0, column=1, sticky=sticky)
|
||||
if os.path.exists('adeptkey.der'):
|
||||
self.keypath.insert(0, 'adeptkey.der')
|
||||
button = Tkinter.Button(body, text="...", command=self.get_keypath)
|
||||
button.grid(row=0, column=2)
|
||||
Tkinter.Label(body, text='Input file').grid(row=1)
|
||||
self.inpath = Tkinter.Entry(body, width=30)
|
||||
self.inpath.grid(row=1, column=1, sticky=sticky)
|
||||
button = Tkinter.Button(body, text="...", command=self.get_inpath)
|
||||
button.grid(row=1, column=2)
|
||||
Tkinter.Label(body, text='Output file').grid(row=2)
|
||||
self.outpath = Tkinter.Entry(body, width=30)
|
||||
self.outpath.grid(row=2, column=1, sticky=sticky)
|
||||
button = Tkinter.Button(body, text="...", command=self.get_outpath)
|
||||
button.grid(row=2, column=2)
|
||||
buttons = Tkinter.Frame(self)
|
||||
buttons.pack()
|
||||
botton = Tkinter.Button(
|
||||
buttons, text="Decrypt", width=10, command=self.decrypt)
|
||||
botton.pack(side=Tkconstants.LEFT)
|
||||
Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT)
|
||||
button = Tkinter.Button(
|
||||
buttons, text="Quit", width=10, command=self.quit)
|
||||
button.pack(side=Tkconstants.RIGHT)
|
||||
|
||||
def get_keypath(self):
|
||||
keypath = tkFileDialog.askopenfilename(
|
||||
parent=None, title='Select ADEPT key file',
|
||||
defaultextension='.der', filetypes=[('DER-encoded files', '.der'),
|
||||
('All Files', '.*')])
|
||||
if keypath:
|
||||
keypath = os.path.normpath(keypath)
|
||||
self.keypath.delete(0, Tkconstants.END)
|
||||
self.keypath.insert(0, keypath)
|
||||
return
|
||||
|
||||
def get_inpath(self):
|
||||
inpath = tkFileDialog.askopenfilename(
|
||||
parent=None, title='Select ADEPT-encrypted EPUB file to decrypt',
|
||||
defaultextension='.epub', filetypes=[('EPUB files', '.epub'),
|
||||
('All files', '.*')])
|
||||
if inpath:
|
||||
inpath = os.path.normpath(inpath)
|
||||
self.inpath.delete(0, Tkconstants.END)
|
||||
self.inpath.insert(0, inpath)
|
||||
return
|
||||
|
||||
def get_outpath(self):
|
||||
outpath = tkFileDialog.asksaveasfilename(
|
||||
parent=None, title='Select unencrypted EPUB file to produce',
|
||||
defaultextension='.epub', filetypes=[('EPUB files', '.epub'),
|
||||
('All files', '.*')])
|
||||
if outpath:
|
||||
outpath = os.path.normpath(outpath)
|
||||
self.outpath.delete(0, Tkconstants.END)
|
||||
self.outpath.insert(0, outpath)
|
||||
return
|
||||
|
||||
def decrypt(self):
|
||||
keypath = self.keypath.get()
|
||||
inpath = self.inpath.get()
|
||||
outpath = self.outpath.get()
|
||||
if not keypath or not os.path.exists(keypath):
|
||||
self.status['text'] = 'Specified key file does not exist'
|
||||
return
|
||||
if not inpath or not os.path.exists(inpath):
|
||||
self.status['text'] = 'Specified input file does not exist'
|
||||
return
|
||||
if not outpath:
|
||||
self.status['text'] = 'Output file not specified'
|
||||
return
|
||||
if inpath == outpath:
|
||||
self.status['text'] = 'Must have different input and output files'
|
||||
return
|
||||
argv = [sys.argv[0], keypath, inpath, outpath]
|
||||
self.status['text'] = 'Decrypting...'
|
||||
try:
|
||||
cli_main(argv)
|
||||
except Exception, e:
|
||||
self.status['text'] = 'Error: ' + str(e)
|
||||
return
|
||||
self.status['text'] = 'File successfully decrypted'
|
||||
|
||||
def gui_main():
|
||||
root = Tkinter.Tk()
|
||||
if AES is None:
|
||||
root.withdraw()
|
||||
tkMessageBox.showerror(
|
||||
"INEPT EPUB Decrypter",
|
||||
"This script requires PyCrypto, which must be installed "
|
||||
"separately. Read the top-of-script comment for details.")
|
||||
return 1
|
||||
root.title('INEPT EPUB Decrypter')
|
||||
root.resizable(True, False)
|
||||
root.minsize(300, 0)
|
||||
DecryptionDialog(root).pack(fill=Tkconstants.X, expand=1)
|
||||
root.mainloop()
|
||||
return 0
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) > 1:
|
||||
sys.exit(cli_main())
|
||||
sys.exit(gui_main())
|
||||
@@ -1,236 +0,0 @@
|
||||
#! /usr/bin/python
|
||||
|
||||
# ineptkey.pyw, version 4
|
||||
|
||||
# To run this program 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
|
||||
# ineptkey.pyw and double-click on it to run it. It will create a file named
|
||||
# adeptkey.der in the same directory. This is your ADEPT user key.
|
||||
|
||||
# Revision history:
|
||||
# 1 - Initial release, for Adobe Digital Editions 1.7
|
||||
# 2 - Better algorithm for finding pLK; improved error handling
|
||||
# 3 - Rename to INEPT
|
||||
# 4 - quick beta fix for ADE 1.7.3 - for older versions use ineptkey v3
|
||||
# or upgrade to ADE 1.7.3 (anon)
|
||||
|
||||
"""
|
||||
Retrieve Adobe ADEPT user key under Windows.
|
||||
"""
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
|
||||
import sys
|
||||
import os
|
||||
from struct import pack
|
||||
from ctypes import windll, c_char_p, c_wchar_p, c_uint, POINTER, byref, \
|
||||
create_unicode_buffer, create_string_buffer, CFUNCTYPE, addressof, \
|
||||
string_at, Structure, c_void_p, cast
|
||||
import _winreg as winreg
|
||||
import Tkinter
|
||||
import Tkconstants
|
||||
import tkMessageBox
|
||||
import traceback
|
||||
|
||||
try:
|
||||
from Crypto.Cipher import AES
|
||||
except ImportError:
|
||||
AES = None
|
||||
|
||||
|
||||
DEVICE_KEY = 'Software\\Adobe\\Adept\\Device'
|
||||
PRIVATE_LICENCE_KEY_KEY = 'Software\\Adobe\\Adept\\Activation\\%04d\\%04d'
|
||||
|
||||
MAX_PATH = 255
|
||||
|
||||
kernel32 = windll.kernel32
|
||||
advapi32 = windll.advapi32
|
||||
crypt32 = windll.crypt32
|
||||
|
||||
|
||||
class ADEPTError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def GetSystemDirectory():
|
||||
GetSystemDirectoryW = kernel32.GetSystemDirectoryW
|
||||
GetSystemDirectoryW.argtypes = [c_wchar_p, c_uint]
|
||||
GetSystemDirectoryW.restype = c_uint
|
||||
def GetSystemDirectory():
|
||||
buffer = create_unicode_buffer(MAX_PATH + 1)
|
||||
GetSystemDirectoryW(buffer, len(buffer))
|
||||
return buffer.value
|
||||
return GetSystemDirectory
|
||||
GetSystemDirectory = GetSystemDirectory()
|
||||
|
||||
|
||||
def GetVolumeSerialNumber():
|
||||
GetVolumeInformationW = kernel32.GetVolumeInformationW
|
||||
GetVolumeInformationW.argtypes = [c_wchar_p, c_wchar_p, c_uint,
|
||||
POINTER(c_uint), POINTER(c_uint),
|
||||
POINTER(c_uint), c_wchar_p, c_uint]
|
||||
GetVolumeInformationW.restype = c_uint
|
||||
def GetVolumeSerialNumber(path):
|
||||
vsn = c_uint(0)
|
||||
GetVolumeInformationW(path, None, 0, byref(vsn), None, None, None, 0)
|
||||
return vsn.value
|
||||
return GetVolumeSerialNumber
|
||||
GetVolumeSerialNumber = GetVolumeSerialNumber()
|
||||
|
||||
|
||||
def GetUserName():
|
||||
GetUserNameW = advapi32.GetUserNameW
|
||||
GetUserNameW.argtypes = [c_wchar_p, POINTER(c_uint)]
|
||||
GetUserNameW.restype = c_uint
|
||||
def GetUserName():
|
||||
buffer = create_unicode_buffer(32)
|
||||
size = c_uint(len(buffer))
|
||||
while not GetUserNameW(buffer, byref(size)):
|
||||
buffer = create_unicode_buffer(len(buffer) * 2)
|
||||
size.value = len(buffer)
|
||||
return buffer.value.encode('utf-16-le')[::2]
|
||||
return GetUserName
|
||||
GetUserName = GetUserName()
|
||||
|
||||
|
||||
CPUID0_INSNS = create_string_buffer("\x53\x31\xc0\x0f\xa2\x8b\x44\x24\x08\x89"
|
||||
"\x18\x89\x50\x04\x89\x48\x08\x5b\xc3")
|
||||
def cpuid0():
|
||||
buffer = create_string_buffer(12)
|
||||
cpuid0__ = CFUNCTYPE(c_char_p)(addressof(CPUID0_INSNS))
|
||||
def cpuid0():
|
||||
cpuid0__(buffer)
|
||||
return buffer.raw
|
||||
return cpuid0
|
||||
cpuid0 = cpuid0()
|
||||
|
||||
|
||||
CPUID1_INSNS = create_string_buffer("\x53\x31\xc0\x40\x0f\xa2\x5b\xc3")
|
||||
cpuid1 = CFUNCTYPE(c_uint)(addressof(CPUID1_INSNS))
|
||||
|
||||
|
||||
class DataBlob(Structure):
|
||||
_fields_ = [('cbData', c_uint),
|
||||
('pbData', c_void_p)]
|
||||
DataBlob_p = POINTER(DataBlob)
|
||||
|
||||
def CryptUnprotectData():
|
||||
_CryptUnprotectData = crypt32.CryptUnprotectData
|
||||
_CryptUnprotectData.argtypes = [DataBlob_p, c_wchar_p, DataBlob_p,
|
||||
c_void_p, c_void_p, c_uint, DataBlob_p]
|
||||
_CryptUnprotectData.restype = c_uint
|
||||
def CryptUnprotectData(indata, entropy):
|
||||
indatab = create_string_buffer(indata)
|
||||
indata = DataBlob(len(indata), cast(indatab, c_void_p))
|
||||
entropyb = create_string_buffer(entropy)
|
||||
entropy = DataBlob(len(entropy), cast(entropyb, c_void_p))
|
||||
outdata = DataBlob()
|
||||
if not _CryptUnprotectData(byref(indata), None, byref(entropy),
|
||||
None, None, 0, byref(outdata)):
|
||||
raise ADEPTError("Failed to decrypt user key key (sic)")
|
||||
return string_at(outdata.pbData, outdata.cbData)
|
||||
return CryptUnprotectData
|
||||
CryptUnprotectData = CryptUnprotectData()
|
||||
|
||||
|
||||
def retrieve_key(keypath):
|
||||
root = GetSystemDirectory().split('\\')[0] + '\\'
|
||||
serial = GetVolumeSerialNumber(root)
|
||||
vendor = cpuid0()
|
||||
signature = pack('>I', cpuid1())[1:]
|
||||
user = GetUserName()
|
||||
entropy = pack('>I12s3s13s', serial, vendor, signature, user)
|
||||
cuser = winreg.HKEY_CURRENT_USER
|
||||
try:
|
||||
regkey = winreg.OpenKey(cuser, DEVICE_KEY)
|
||||
except WindowsError:
|
||||
raise ADEPTError("Adobe Digital Editions not activated")
|
||||
device = winreg.QueryValueEx(regkey, 'key')[0]
|
||||
keykey = CryptUnprotectData(device, entropy)
|
||||
userkey = None
|
||||
pkcs = None
|
||||
for i in xrange(4, 16):
|
||||
for j in xrange(0, 16):
|
||||
plkkey = PRIVATE_LICENCE_KEY_KEY % (i, j)
|
||||
try:
|
||||
pkcs = winreg.OpenKey(cuser, plkkey)
|
||||
except WindowsError:
|
||||
break
|
||||
type = winreg.QueryValueEx(pkcs, None)[0]
|
||||
if type != 'pkcs12':
|
||||
continue
|
||||
pkcs = winreg.QueryValueEx(pkcs, 'value')[0]
|
||||
break
|
||||
if pkcs is not None:
|
||||
break
|
||||
|
||||
for i in xrange(4, 16):
|
||||
for j in xrange(0, 16):
|
||||
plkkey = PRIVATE_LICENCE_KEY_KEY % (i, j)
|
||||
try:
|
||||
regkey = winreg.OpenKey(cuser, plkkey)
|
||||
except WindowsError:
|
||||
break
|
||||
type = winreg.QueryValueEx(regkey, None)[0]
|
||||
if type != 'privateLicenseKey':
|
||||
continue
|
||||
userkey = winreg.QueryValueEx(regkey, 'value')[0]
|
||||
break
|
||||
if userkey is not None:
|
||||
break
|
||||
if pkcs is None:
|
||||
raise ADEPTError('Could not locate PKCS specification')
|
||||
if userkey is None:
|
||||
raise ADEPTError('Could not locate privateLicenseKey')
|
||||
pkcs = pkcs.decode('base64')
|
||||
print pkcs
|
||||
userkey = userkey.decode('base64')
|
||||
userkey = AES.new(keykey, AES.MODE_CBC).decrypt(userkey)
|
||||
userkey = userkey[26:-ord(userkey[-1])]
|
||||
with open(keypath, 'wb') as f:
|
||||
f.write(userkey)
|
||||
return
|
||||
|
||||
class ExceptionDialog(Tkinter.Frame):
|
||||
def __init__(self, root, text):
|
||||
Tkinter.Frame.__init__(self, root, border=5)
|
||||
label = Tkinter.Label(self, text="Unexpected error:",
|
||||
anchor=Tkconstants.W, justify=Tkconstants.LEFT)
|
||||
label.pack(fill=Tkconstants.X, expand=0)
|
||||
self.text = Tkinter.Text(self)
|
||||
self.text.pack(fill=Tkconstants.BOTH, expand=1)
|
||||
self.text.insert(Tkconstants.END, text)
|
||||
|
||||
|
||||
def main(argv=sys.argv):
|
||||
root = Tkinter.Tk()
|
||||
root.withdraw()
|
||||
progname = os.path.basename(argv[0])
|
||||
if AES is None:
|
||||
tkMessageBox.showerror(
|
||||
"ADEPT Key",
|
||||
"This script requires PyCrypto, which must be installed "
|
||||
"separately. Read the top-of-script comment for details.")
|
||||
return 1
|
||||
keypath = 'adeptkey.der'
|
||||
try:
|
||||
retrieve_key(keypath)
|
||||
except ADEPTError, e:
|
||||
tkMessageBox.showerror("ADEPT Key", "Error: " + str(e))
|
||||
return 1
|
||||
except Exception:
|
||||
root.wm_state('normal')
|
||||
root.title('ADEPT Key')
|
||||
text = traceback.format_exc()
|
||||
ExceptionDialog(root, text).pack(fill=Tkconstants.BOTH, expand=1)
|
||||
root.mainloop()
|
||||
return 1
|
||||
tkMessageBox.showinfo(
|
||||
"ADEPT Key", "Key successfully retrieved to %s" % (keypath))
|
||||
return 0
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
@@ -1,123 +0,0 @@
|
||||
#! /usr/bin/env python
|
||||
|
||||
# ineptkeymac.py, version 1
|
||||
|
||||
# This program runs on Mac OS X, version 10.6.2 and probably several other
|
||||
# versions. It uses Python 2.6, but it probably also runs on all versions
|
||||
# 2.x with x >= 5.
|
||||
|
||||
# This program extracts the private RSA key for your ADE account in a
|
||||
# standard binary form (DER format) in a file of your choosing. Its purpose
|
||||
# is to make a backup of that key so that your legally bought ADE encoded
|
||||
# ebooks can be salvaged in case they would no longer be supported by ADE
|
||||
# software. No other usages are intended.
|
||||
|
||||
# It has been tested with the key storage structure of ADE 1.7.1 and 1.7.2
|
||||
# and Sony Reader Library.
|
||||
|
||||
# This software does not contain any encryption code. Its only use of
|
||||
# external encryption software is the use of openssl for the conversion of
|
||||
# the private key from pem to der format. It doesn't use encryption or
|
||||
# decryption, however.
|
||||
|
||||
# You can run this program from the command line (python ineptkeymac.py
|
||||
# filename), or by doubleclicking when it has been associated with
|
||||
# Pythonlauncher. When no filename is given it will show a dialog to obtain one.
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
|
||||
import sys
|
||||
import os
|
||||
import xml.etree.ElementTree as etree
|
||||
from contextlib import closing
|
||||
import Tkinter
|
||||
import Tkconstants
|
||||
import tkFileDialog
|
||||
from tkMessageBox import showerror
|
||||
from subprocess import Popen, PIPE
|
||||
import textwrap
|
||||
|
||||
NS = 'http://ns.adobe.com/adept'
|
||||
ACTFILE = '~/Library/Application Support/Adobe/Digital Editions/activation.dat'
|
||||
HEADER = '-----BEGIN PRIVATE KEY-----\n'
|
||||
FOOTER = '\n-----END PRIVATE KEY-----\n'
|
||||
|
||||
Gui = False
|
||||
|
||||
def get_key():
|
||||
'''Returns the private key as a binary string (DER format)'''
|
||||
try:
|
||||
filename = os.path.expanduser(ACTFILE)
|
||||
tree = etree.parse(filename)
|
||||
xpath = '//{%s}credentials/{%s}privateLicenseKey' % (NS, NS)
|
||||
b64key = tree.findtext(xpath)
|
||||
pemkey = HEADER + textwrap.fill(b64key, 64) + FOOTER
|
||||
|
||||
cmd = ['openssl', 'rsa', '-outform', 'der']
|
||||
proc = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)
|
||||
stdout, stderr = proc.communicate(pemkey)
|
||||
|
||||
if proc.returncode != 0:
|
||||
error("openssl error: " + stderr)
|
||||
return None
|
||||
return stdout
|
||||
|
||||
except IOError:
|
||||
error("Can find keyfile. Maybe you should activate your Adobe ID.")
|
||||
sys.exit(1)
|
||||
|
||||
def store_key(key, keypath):
|
||||
'''Store the key in the file given as keypath. If no keypath is given a
|
||||
dialog will ask for one.'''
|
||||
|
||||
try:
|
||||
if keypath is None:
|
||||
keypath = get_keypath()
|
||||
if not keypath: # Cancelled
|
||||
return
|
||||
|
||||
with closing(open(keypath, 'wb')) as outf:
|
||||
outf.write(key)
|
||||
|
||||
except IOError, e:
|
||||
error("Can write keyfile: " + str(e))
|
||||
|
||||
def get_keypath():
|
||||
keypath = tkFileDialog.asksaveasfilename(
|
||||
parent = None, title = 'Select file to store ADEPT key',
|
||||
initialdir = os.path.expanduser('~/Desktop'),
|
||||
initialfile = 'adeptkey.der',
|
||||
defaultextension = '.der', filetypes = [('DER-encoded files', '.der'),
|
||||
('All Files', '.*')])
|
||||
if keypath:
|
||||
keypath = os.path.normpath(keypath)
|
||||
return keypath
|
||||
|
||||
def error(text):
|
||||
print text
|
||||
if Gui: showerror('Error!', text)
|
||||
|
||||
def gui_main():
|
||||
root = Tkinter.Tk()
|
||||
root.iconify()
|
||||
global Gui
|
||||
Gui = True
|
||||
store_key(get_key(), None)
|
||||
|
||||
return 0
|
||||
|
||||
def main(argv=sys.argv):
|
||||
progname = os.path.basename(argv[0])
|
||||
|
||||
if len(argv) == 1: # assume GUI if no argument given
|
||||
return gui_main()
|
||||
if len(argv) != 2:
|
||||
print "usage: %s KEYFILE" % (progname,)
|
||||
return 1
|
||||
|
||||
store_key(get_key(), argv[1])
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
@@ -1,235 +0,0 @@
|
||||
#! /usr/bin/python
|
||||
|
||||
# ignobleepub.pyw, version 1-rc2
|
||||
|
||||
# To run this program 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
|
||||
# ignobleepub.pyw and double-click on it to run it.
|
||||
|
||||
# Revision history:
|
||||
# 1 - Initial release
|
||||
|
||||
"""
|
||||
Decrypt Barnes & Noble ADEPT encrypted EPUB books.
|
||||
"""
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
|
||||
import sys
|
||||
import os
|
||||
import zlib
|
||||
import zipfile
|
||||
from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED
|
||||
from contextlib import closing
|
||||
import xml.etree.ElementTree as etree
|
||||
import Tkinter
|
||||
import Tkconstants
|
||||
import tkFileDialog
|
||||
import tkMessageBox
|
||||
|
||||
try:
|
||||
from Crypto.Cipher import AES
|
||||
except ImportError:
|
||||
AES = None
|
||||
|
||||
META_NAMES = ('mimetype', 'META-INF/rights.xml', 'META-INF/encryption.xml')
|
||||
NSMAP = {'adept': 'http://ns.adobe.com/adept',
|
||||
'enc': 'http://www.w3.org/2001/04/xmlenc#'}
|
||||
|
||||
class ZipInfo(zipfile.ZipInfo):
|
||||
def __init__(self, *args, **kwargs):
|
||||
if 'compress_type' in kwargs:
|
||||
compress_type = kwargs.pop('compress_type')
|
||||
super(ZipInfo, self).__init__(*args, **kwargs)
|
||||
self.compress_type = compress_type
|
||||
|
||||
class Decryptor(object):
|
||||
def __init__(self, bookkey, encryption):
|
||||
enc = lambda tag: '{%s}%s' % (NSMAP['enc'], tag)
|
||||
self._aes = AES.new(bookkey, AES.MODE_CBC)
|
||||
encryption = etree.fromstring(encryption)
|
||||
self._encrypted = encrypted = set()
|
||||
expr = './%s/%s/%s' % (enc('EncryptedData'), enc('CipherData'),
|
||||
enc('CipherReference'))
|
||||
for elem in encryption.findall(expr):
|
||||
path = elem.get('URI', None)
|
||||
if path is not None:
|
||||
encrypted.add(path)
|
||||
|
||||
def decompress(self, bytes):
|
||||
dc = zlib.decompressobj(-15)
|
||||
bytes = dc.decompress(bytes)
|
||||
ex = dc.decompress('Z') + dc.flush()
|
||||
if ex:
|
||||
bytes = bytes + ex
|
||||
return bytes
|
||||
|
||||
def decrypt(self, path, data):
|
||||
if path in self._encrypted:
|
||||
data = self._aes.decrypt(data)[16:]
|
||||
data = data[:-ord(data[-1])]
|
||||
data = self.decompress(data)
|
||||
return data
|
||||
|
||||
|
||||
class ADEPTError(Exception):
|
||||
pass
|
||||
|
||||
def cli_main(argv=sys.argv):
|
||||
progname = os.path.basename(argv[0])
|
||||
if AES is None:
|
||||
print "%s: This script requires PyCrypto, which must be installed " \
|
||||
"separately. Read the top-of-script comment for details." % \
|
||||
(progname,)
|
||||
return 1
|
||||
if len(argv) != 4:
|
||||
print "usage: %s KEYFILE INBOOK OUTBOOK" % (progname,)
|
||||
return 1
|
||||
keypath, inpath, outpath = argv[1:]
|
||||
with open(keypath, 'rb') as f:
|
||||
keyb64 = f.read()
|
||||
key = keyb64.decode('base64')[:16]
|
||||
aes = AES.new(key, AES.MODE_CBC)
|
||||
with closing(ZipFile(open(inpath, 'rb'))) as inf:
|
||||
namelist = set(inf.namelist())
|
||||
if 'META-INF/rights.xml' not in namelist or \
|
||||
'META-INF/encryption.xml' not in namelist:
|
||||
raise ADEPTError('%s: not an B&N ADEPT EPUB' % (inpath,))
|
||||
for name in META_NAMES:
|
||||
namelist.remove(name)
|
||||
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
|
||||
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
||||
expr = './/%s' % (adept('encryptedKey'),)
|
||||
bookkey = ''.join(rights.findtext(expr))
|
||||
bookkey = aes.decrypt(bookkey.decode('base64'))
|
||||
bookkey = bookkey[:-ord(bookkey[-1])]
|
||||
encryption = inf.read('META-INF/encryption.xml')
|
||||
decryptor = Decryptor(bookkey[-16:], encryption)
|
||||
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
|
||||
with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf:
|
||||
zi = ZipInfo('mimetype', compress_type=ZIP_STORED)
|
||||
outf.writestr(zi, inf.read('mimetype'))
|
||||
for path in namelist:
|
||||
data = inf.read(path)
|
||||
outf.writestr(path, decryptor.decrypt(path, data))
|
||||
return 0
|
||||
|
||||
|
||||
class DecryptionDialog(Tkinter.Frame):
|
||||
def __init__(self, root):
|
||||
Tkinter.Frame.__init__(self, root, border=5)
|
||||
self.status = Tkinter.Label(self, text='Select files for decryption')
|
||||
self.status.pack(fill=Tkconstants.X, expand=1)
|
||||
body = Tkinter.Frame(self)
|
||||
body.pack(fill=Tkconstants.X, expand=1)
|
||||
sticky = Tkconstants.E + Tkconstants.W
|
||||
body.grid_columnconfigure(1, weight=2)
|
||||
Tkinter.Label(body, text='Key file').grid(row=0)
|
||||
self.keypath = Tkinter.Entry(body, width=30)
|
||||
self.keypath.grid(row=0, column=1, sticky=sticky)
|
||||
if os.path.exists('bnepubkey.b64'):
|
||||
self.keypath.insert(0, 'bnepubkey.b64')
|
||||
button = Tkinter.Button(body, text="...", command=self.get_keypath)
|
||||
button.grid(row=0, column=2)
|
||||
Tkinter.Label(body, text='Input file').grid(row=1)
|
||||
self.inpath = Tkinter.Entry(body, width=30)
|
||||
self.inpath.grid(row=1, column=1, sticky=sticky)
|
||||
button = Tkinter.Button(body, text="...", command=self.get_inpath)
|
||||
button.grid(row=1, column=2)
|
||||
Tkinter.Label(body, text='Output file').grid(row=2)
|
||||
self.outpath = Tkinter.Entry(body, width=30)
|
||||
self.outpath.grid(row=2, column=1, sticky=sticky)
|
||||
button = Tkinter.Button(body, text="...", command=self.get_outpath)
|
||||
button.grid(row=2, column=2)
|
||||
buttons = Tkinter.Frame(self)
|
||||
buttons.pack()
|
||||
botton = Tkinter.Button(
|
||||
buttons, text="Decrypt", width=10, command=self.decrypt)
|
||||
botton.pack(side=Tkconstants.LEFT)
|
||||
Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT)
|
||||
button = Tkinter.Button(
|
||||
buttons, text="Quit", width=10, command=self.quit)
|
||||
button.pack(side=Tkconstants.RIGHT)
|
||||
|
||||
def get_keypath(self):
|
||||
keypath = tkFileDialog.askopenfilename(
|
||||
parent=None, title='Select B&N EPUB key file',
|
||||
defaultextension='.b64',
|
||||
filetypes=[('base64-encoded files', '.b64'),
|
||||
('All Files', '.*')])
|
||||
if keypath:
|
||||
keypath = os.path.normpath(keypath)
|
||||
self.keypath.delete(0, Tkconstants.END)
|
||||
self.keypath.insert(0, keypath)
|
||||
return
|
||||
|
||||
def get_inpath(self):
|
||||
inpath = tkFileDialog.askopenfilename(
|
||||
parent=None, title='Select B&N-encrypted EPUB file to decrypt',
|
||||
defaultextension='.epub', filetypes=[('EPUB files', '.epub'),
|
||||
('All files', '.*')])
|
||||
if inpath:
|
||||
inpath = os.path.normpath(inpath)
|
||||
self.inpath.delete(0, Tkconstants.END)
|
||||
self.inpath.insert(0, inpath)
|
||||
return
|
||||
|
||||
def get_outpath(self):
|
||||
outpath = tkFileDialog.asksaveasfilename(
|
||||
parent=None, title='Select unencrypted EPUB file to produce',
|
||||
defaultextension='.epub', filetypes=[('EPUB files', '.epub'),
|
||||
('All files', '.*')])
|
||||
if outpath:
|
||||
outpath = os.path.normpath(outpath)
|
||||
self.outpath.delete(0, Tkconstants.END)
|
||||
self.outpath.insert(0, outpath)
|
||||
return
|
||||
|
||||
def decrypt(self):
|
||||
keypath = self.keypath.get()
|
||||
inpath = self.inpath.get()
|
||||
outpath = self.outpath.get()
|
||||
if not keypath or not os.path.exists(keypath):
|
||||
self.status['text'] = 'Specified key file does not exist'
|
||||
return
|
||||
if not inpath or not os.path.exists(inpath):
|
||||
self.status['text'] = 'Specified input file does not exist'
|
||||
return
|
||||
if not outpath:
|
||||
self.status['text'] = 'Output file not specified'
|
||||
return
|
||||
if inpath == outpath:
|
||||
self.status['text'] = 'Must have different input and output files'
|
||||
return
|
||||
argv = [sys.argv[0], keypath, inpath, outpath]
|
||||
self.status['text'] = 'Decrypting...'
|
||||
try:
|
||||
cli_main(argv)
|
||||
except Exception, e:
|
||||
self.status['text'] = 'Error: ' + str(e)
|
||||
return
|
||||
self.status['text'] = 'File successfully decrypted'
|
||||
|
||||
def gui_main():
|
||||
root = Tkinter.Tk()
|
||||
if AES is None:
|
||||
root.withdraw()
|
||||
tkMessageBox.showerror(
|
||||
"Ignoble EPUB Decrypter",
|
||||
"This script requires PyCrypto, which must be installed "
|
||||
"separately. Read the top-of-script comment for details.")
|
||||
return 1
|
||||
root.title('Ignoble EPUB Decrypter')
|
||||
root.resizable(True, False)
|
||||
root.minsize(300, 0)
|
||||
DecryptionDialog(root).pack(fill=Tkconstants.X, expand=1)
|
||||
root.mainloop()
|
||||
return 0
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) > 1:
|
||||
sys.exit(cli_main())
|
||||
sys.exit(gui_main())
|
||||
@@ -1,112 +0,0 @@
|
||||
#! /usr/bin/python
|
||||
|
||||
# ignoblekey.pyw, version 2
|
||||
|
||||
# To run this program install Python 2.6 from <http://www.python.org/download/>
|
||||
# Save this script file as ignoblekey.pyw and double-click on it to run it.
|
||||
|
||||
# Revision history:
|
||||
# 1 - Initial release
|
||||
# 2 - Add some missing code
|
||||
|
||||
"""
|
||||
Retrieve B&N DesktopReader EPUB user AES key.
|
||||
"""
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
|
||||
import sys
|
||||
import os
|
||||
import binascii
|
||||
import glob
|
||||
import Tkinter
|
||||
import Tkconstants
|
||||
import tkMessageBox
|
||||
import traceback
|
||||
|
||||
BN_KEY_KEY = 'uhk00000000'
|
||||
BN_APPDATA_DIR = r'Barnes & Noble\DesktopReader'
|
||||
|
||||
class IgnobleError(Exception):
|
||||
pass
|
||||
|
||||
def retrieve_key(inpath, outpath):
|
||||
# The B&N DesktopReader 'ClientAPI' file is just a sqlite3 DB. Requiring
|
||||
# users to install sqlite3 and bindings seems like overkill for retrieving
|
||||
# one value, so we go in hot and dirty.
|
||||
with open(inpath, 'rb') as f:
|
||||
data = f.read()
|
||||
if BN_KEY_KEY not in data:
|
||||
raise IgnobleError('B&N user key not found; unexpected DB format?')
|
||||
index = data.rindex(BN_KEY_KEY) + len(BN_KEY_KEY) + 1
|
||||
data = data[index:index + 40]
|
||||
for i in xrange(20, len(data)):
|
||||
try:
|
||||
keyb64 = data[:i]
|
||||
if len(keyb64.decode('base64')) == 20:
|
||||
break
|
||||
except binascii.Error:
|
||||
pass
|
||||
else:
|
||||
raise IgnobleError('Problem decoding key; unexpected DB format?')
|
||||
with open(outpath, 'wb') as f:
|
||||
f.write(keyb64 + '\n')
|
||||
|
||||
def cli_main(argv=sys.argv):
|
||||
progname = os.path.basename(argv[0])
|
||||
args = argv[1:]
|
||||
if len(args) != 2:
|
||||
sys.stderr.write("USAGE: %s CLIENTDB KEYFILE" % (progname,))
|
||||
return 1
|
||||
inpath, outpath = args
|
||||
retrieve_key(inpath, outpath)
|
||||
return 0
|
||||
|
||||
def find_bnclientdb_path():
|
||||
appdata = os.environ['APPDATA']
|
||||
bndir = os.path.join(appdata, BN_APPDATA_DIR)
|
||||
if not os.path.isdir(bndir):
|
||||
raise IgnobleError('Could not locate B&N Reader installation')
|
||||
dbpath = glob.glob(os.path.join(bndir, 'ClientAPI_*.db'))
|
||||
if len(dbpath) == 0:
|
||||
raise IgnobleError('Problem locating B&N Reader DB')
|
||||
return sorted(dbpath)[-1]
|
||||
|
||||
class ExceptionDialog(Tkinter.Frame):
|
||||
def __init__(self, root, text):
|
||||
Tkinter.Frame.__init__(self, root, border=5)
|
||||
label = Tkinter.Label(self, text="Unexpected error:",
|
||||
anchor=Tkconstants.W, justify=Tkconstants.LEFT)
|
||||
label.pack(fill=Tkconstants.X, expand=0)
|
||||
self.text = Tkinter.Text(self)
|
||||
self.text.pack(fill=Tkconstants.BOTH, expand=1)
|
||||
self.text.insert(Tkconstants.END, text)
|
||||
|
||||
def gui_main(argv=sys.argv):
|
||||
root = Tkinter.Tk()
|
||||
root.withdraw()
|
||||
progname = os.path.basename(argv[0])
|
||||
keypath = 'bnepubkey.b64'
|
||||
try:
|
||||
dbpath = find_bnclientdb_path()
|
||||
retrieve_key(dbpath, keypath)
|
||||
except IgnobleError, e:
|
||||
tkMessageBox.showerror("Ignoble Key", "Error: " + str(e))
|
||||
return 1
|
||||
except Exception:
|
||||
root.wm_state('normal')
|
||||
root.title('Ignoble Key')
|
||||
text = traceback.format_exc()
|
||||
ExceptionDialog(root, text).pack(fill=Tkconstants.BOTH, expand=1)
|
||||
root.mainloop()
|
||||
return 1
|
||||
tkMessageBox.showinfo(
|
||||
"Ignoble Key", "Key successfully retrieved to %s" % (keypath))
|
||||
return 0
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) > 1:
|
||||
sys.exit(cli_main())
|
||||
sys.exit(gui_main())
|
||||
@@ -1,147 +0,0 @@
|
||||
#! /usr/bin/python
|
||||
|
||||
# ignoblekeygen.pyw, version 1
|
||||
|
||||
# To run this program 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
|
||||
# ignoblekeygen.pyw and double-click on it to run it.
|
||||
|
||||
# Revision history:
|
||||
# 1 - Initial release
|
||||
|
||||
"""
|
||||
Generate Barnes & Noble EPUB user key from name and credit card number.
|
||||
"""
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
|
||||
import sys
|
||||
import os
|
||||
import hashlib
|
||||
import Tkinter
|
||||
import Tkconstants
|
||||
import tkFileDialog
|
||||
import tkMessageBox
|
||||
|
||||
try:
|
||||
from Crypto.Cipher import AES
|
||||
except ImportError:
|
||||
AES = None
|
||||
|
||||
def normalize_name(name):
|
||||
return ''.join(x for x in name.lower() if x != ' ')
|
||||
|
||||
def generate_keyfile(name, ccn, outpath):
|
||||
name = normalize_name(name) + '\x00'
|
||||
ccn = ccn + '\x00'
|
||||
name_sha = hashlib.sha1(name).digest()[:16]
|
||||
ccn_sha = hashlib.sha1(ccn).digest()[:16]
|
||||
both_sha = hashlib.sha1(name + ccn).digest()
|
||||
aes = AES.new(ccn_sha, AES.MODE_CBC, name_sha)
|
||||
crypt = aes.encrypt(both_sha + ('\x0c' * 0x0c))
|
||||
userkey = hashlib.sha1(crypt).digest()
|
||||
with open(outpath, 'wb') as f:
|
||||
f.write(userkey.encode('base64'))
|
||||
return userkey
|
||||
|
||||
def cli_main(argv=sys.argv):
|
||||
progname = os.path.basename(argv[0])
|
||||
if AES is None:
|
||||
print "%s: This script requires PyCrypto, which must be installed " \
|
||||
"separately. Read the top-of-script comment for details." % \
|
||||
(progname,)
|
||||
return 1
|
||||
if len(argv) != 4:
|
||||
print "usage: %s NAME CC# OUTFILE" % (progname,)
|
||||
return 1
|
||||
name, ccn, outpath = argv[1:]
|
||||
generate_keyfile(name, ccn, outpath)
|
||||
return 0
|
||||
|
||||
class DecryptionDialog(Tkinter.Frame):
|
||||
def __init__(self, root):
|
||||
Tkinter.Frame.__init__(self, root, border=5)
|
||||
self.status = Tkinter.Label(self, text='Enter parameters')
|
||||
self.status.pack(fill=Tkconstants.X, expand=1)
|
||||
body = Tkinter.Frame(self)
|
||||
body.pack(fill=Tkconstants.X, expand=1)
|
||||
sticky = Tkconstants.E + Tkconstants.W
|
||||
body.grid_columnconfigure(1, weight=2)
|
||||
Tkinter.Label(body, text='Name').grid(row=1)
|
||||
self.name = Tkinter.Entry(body, width=30)
|
||||
self.name.grid(row=1, column=1, sticky=sticky)
|
||||
Tkinter.Label(body, text='CC#').grid(row=2)
|
||||
self.ccn = Tkinter.Entry(body, width=30)
|
||||
self.ccn.grid(row=2, column=1, sticky=sticky)
|
||||
Tkinter.Label(body, text='Output file').grid(row=0)
|
||||
self.keypath = Tkinter.Entry(body, width=30)
|
||||
self.keypath.grid(row=0, column=1, sticky=sticky)
|
||||
self.keypath.insert(0, 'bnepubkey.b64')
|
||||
button = Tkinter.Button(body, text="...", command=self.get_keypath)
|
||||
button.grid(row=0, column=2)
|
||||
buttons = Tkinter.Frame(self)
|
||||
buttons.pack()
|
||||
botton = Tkinter.Button(
|
||||
buttons, text="Generate", width=10, command=self.generate)
|
||||
botton.pack(side=Tkconstants.LEFT)
|
||||
Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT)
|
||||
button = Tkinter.Button(
|
||||
buttons, text="Quit", width=10, command=self.quit)
|
||||
button.pack(side=Tkconstants.RIGHT)
|
||||
|
||||
def get_keypath(self):
|
||||
keypath = tkFileDialog.asksaveasfilename(
|
||||
parent=None, title='Select B&N EPUB key file to produce',
|
||||
defaultextension='.b64',
|
||||
filetypes=[('base64-encoded files', '.b64'),
|
||||
('All Files', '.*')])
|
||||
if keypath:
|
||||
keypath = os.path.normpath(keypath)
|
||||
self.keypath.delete(0, Tkconstants.END)
|
||||
self.keypath.insert(0, keypath)
|
||||
return
|
||||
|
||||
def generate(self):
|
||||
name = self.name.get()
|
||||
ccn = self.ccn.get()
|
||||
keypath = self.keypath.get()
|
||||
if not name:
|
||||
self.status['text'] = 'Name not specified'
|
||||
return
|
||||
if not ccn:
|
||||
self.status['text'] = 'Credit card number not specified'
|
||||
return
|
||||
if not keypath:
|
||||
self.status['text'] = 'Output keyfile path not specified'
|
||||
return
|
||||
self.status['text'] = 'Generating...'
|
||||
try:
|
||||
generate_keyfile(name, ccn, keypath)
|
||||
except Exception, e:
|
||||
self.status['text'] = 'Error: ' + str(e)
|
||||
return
|
||||
self.status['text'] = 'Keyfile successfully generated'
|
||||
|
||||
def gui_main():
|
||||
root = Tkinter.Tk()
|
||||
if AES is None:
|
||||
root.withdraw()
|
||||
tkMessageBox.showerror(
|
||||
"Ignoble EPUB Keyfile Generator",
|
||||
"This script requires PyCrypto, which must be installed "
|
||||
"separately. Read the top-of-script comment for details.")
|
||||
return 1
|
||||
root.title('Ignoble EPUB Keyfile Generator')
|
||||
root.resizable(True, False)
|
||||
root.minsize(300, 0)
|
||||
DecryptionDialog(root).pack(fill=Tkconstants.X, expand=1)
|
||||
root.mainloop()
|
||||
return 0
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) > 1:
|
||||
sys.exit(cli_main())
|
||||
sys.exit(gui_main())
|
||||
47
CALIBRE_CLI_INSTRUCTIONS.md
Normal file
47
CALIBRE_CLI_INSTRUCTIONS.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# Using the DeDRM plugin with the Calibre command line interface
|
||||
|
||||
If you prefer the Calibre CLI instead of the GUI, follow this guide to
|
||||
install and use the DeDRM plugin.
|
||||
|
||||
This guide assumes you are on Linux, but it may very well work on other
|
||||
platforms.
|
||||
|
||||
## Step-by-step Tutorial
|
||||
|
||||
#### Install Calibre
|
||||
- Follow [Calibre's installation instructions](https://calibre-ebook.com/download_linux)
|
||||
|
||||
#### Install plugins
|
||||
- Download the DeDRM `.zip` archive from DeDRM_tools'
|
||||
[latest release](https://github.com/apprenticeharper/DeDRM_tools/releases/latest).
|
||||
Then unzip it.
|
||||
- Add the DeDRM plugin to Calibre:
|
||||
```
|
||||
cd *the unzipped DeDRM_tools folder*
|
||||
calibre-customize --add DeDRM_calibre_plugin/DeDRM_plugin.zip
|
||||
```
|
||||
- Add the Obok plugin:
|
||||
```
|
||||
calibre-customize --add Obok_calibre_plugin/obok_plugin.zip
|
||||
```
|
||||
|
||||
#### Enter your keys
|
||||
- Figure out what format DeDRM wants your key in by looking in
|
||||
[the code that handles that](DeDRM_plugin/prefs.py).
|
||||
- For Kindle eInk devices, DeDRM expects you to put a list of serial
|
||||
numbers in the `serials` field: `"serials": ["012345689abcdef"]` or
|
||||
`"serials": ["1111111111111111", "2222222222222222"]`.
|
||||
- Now add your keys to `$CALIBRE_CONFIG_DIRECTORY/plugins/dedrm.json`.
|
||||
|
||||
#### Import your books
|
||||
- Make a library folder
|
||||
```
|
||||
mkdir library
|
||||
```
|
||||
- Add your book(s) with this command:
|
||||
```
|
||||
calibredb add /path/to/book.format --with-library=library
|
||||
```
|
||||
|
||||
The DRM should be removed from your book, which you can find in the `library`
|
||||
folder.
|
||||
60
DeDRM_plugin/DeDRM_Adobe Digital Editions Key_Help.htm
Normal file
60
DeDRM_plugin/DeDRM_Adobe Digital Editions Key_Help.htm
Normal file
@@ -0,0 +1,60 @@
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
|
||||
"http://www.w3.org/TR/html4/strict.dtd">
|
||||
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8">
|
||||
<title>Managing Adobe Digital Editions Keys</title>
|
||||
<style type="text/css">
|
||||
span.version {font-size: 50%}
|
||||
span.bold {font-weight: bold}
|
||||
h3 {margin-bottom: 0}
|
||||
p {margin-top: 0}
|
||||
li {margin-top: 0.5em}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<h1>Managing Adobe Digital Editions Keys</h1>
|
||||
|
||||
|
||||
<p>If you have upgraded from an earlier version of the plugin, any existing Adobe Digital Editions keys will have been automatically imported, so you might not need to do any more configuration. In addition, on Windows and Mac, the default Adobe Digital Editions key is added the first time the plugin is run. Continue reading for key generation and management instructions.</p>
|
||||
|
||||
<h3>Creating New Keys:</h3>
|
||||
|
||||
<p>On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a green plus sign (+). Clicking this button will open a new dialog prompting you to enter a key name for the default Adobe Digital Editions key. </p>
|
||||
<ul>
|
||||
<li><span class="bold">Unique Key Name:</span> this is a unique name you choose to help you identify the key. This name will show in the list of configured keys.</li>
|
||||
</ul>
|
||||
|
||||
<p>Click the OK button to create and store the Adobe Digital Editions key for the current installation of Adobe Digital Editions. Or Cancel if you don’t want to create the key.</p>
|
||||
<p>New keys are checked against the current list of keys before being added, and duplicates are discarded.</p>
|
||||
|
||||
<h3>Deleting Keys:</h3>
|
||||
|
||||
<p>On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a red "X". Clicking this button will delete the highlighted key in the list. You will be prompted once to be sure that’s what you truly mean to do. Once gone, it’s permanently gone.</p>
|
||||
|
||||
<h3>Renaming Keys:</h3>
|
||||
|
||||
<p>On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a sheet of paper. Clicking this button will prompt you to enter a new name for the highlighted key in the list. Enter the new name for the encryption key and click the OK button to use the new name, or Cancel to revert to the old name..</p>
|
||||
|
||||
<h3>Exporting Keys:</h3>
|
||||
|
||||
<p>On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a computer’s hard-drive. Use this button to export the highlighted key to a file (with a ‘.der’ file name extension). Used for backup purposes or to migrate key data to other computers/calibre installations. The dialog will prompt you for a place to save the file.</p>
|
||||
|
||||
<h3>Linux Users: WINEPREFIX</h3>
|
||||
|
||||
<p>Under the list of keys, Linux users will see a text field labeled "WINEPREFIX". If you are use Adobe Digital Editions under Wine, and your wine installation containing Adobe Digital Editions 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>
|
||||
|
||||
<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 ‘.der’ key files. Key files might come from being exported from this or older plugins, or may have been generated using the adobekey.pyw script running under Wine on Linux systems.</p>
|
||||
|
||||
<p>Once done creating/deleting/renaming/importing decryption keys, click Close to exit the customization dialogue. Your changes will only be saved permanently when you click OK in the main configuration dialog.</p>
|
||||
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
68
DeDRM_plugin/DeDRM_Barnes and Noble Key_Help.htm
Normal file
68
DeDRM_plugin/DeDRM_Barnes and Noble Key_Help.htm
Normal file
@@ -0,0 +1,68 @@
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
|
||||
"http://www.w3.org/TR/html4/strict.dtd">
|
||||
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8">
|
||||
<title>Managing Barnes and Noble Keys</title>
|
||||
<style type="text/css">
|
||||
span.version {font-size: 50%}
|
||||
span.bold {font-weight: bold}
|
||||
h3 {margin-bottom: 0}
|
||||
p {margin-top: 0}
|
||||
li {margin-top: 0.5em}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<h1>Managing Barnes and Noble Keys</h1>
|
||||
|
||||
|
||||
<p>If you have upgraded from an earlier version of the plugin, any existing Barnes and Noble keys will have been automatically imported, so you might not need to do any more configuration. Continue reading for key generation and management instructions.</p>
|
||||
|
||||
<h3>Changes at Barnes & Noble</h3>
|
||||
|
||||
<p>In mid-2014, Barnes & Noble changed the way they generated encryption keys. Instead of deriving the key from the user's name and credit card number, they started generating a random key themselves, sending that key through to devices when they connected to the Barnes & Noble servers. This means that most users will now find that no combination of their name and CC# will work in decrypting their recently downloaded ebooks.</p>
|
||||
|
||||
<p>Someone commenting at Apprentice Alf's blog detailed a way to retrieve a new account key using the account's email address and password. This method has now been incorporated into the plugin.
|
||||
|
||||
<h3>Creating New Keys:</h3>
|
||||
|
||||
<p>On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a green plus sign (+). Clicking this button will open a new dialog for entering the necessary data to generate a new key.</p>
|
||||
<ul>
|
||||
<li><span class="bold">Unique Key Name:</span> this is a unique name you choose to help you identify the key. This name will show in the list of configured keys. Choose something that will help you remember the data (account email address) it was created with.</li>
|
||||
<li><span class="bold">B&N/nook account email address:</span> This is the default email address for your Barnes and Noble/nook account. This email will not be stored anywhere on your computer or in calibre. It will only be used to fetch the account key that from the B&N server, and it is that key that will be stored in the preferences.</li>
|
||||
<li><span class="bold">B&N/nook account password:</span> this is the password for your Barnes and Noble/nook account. As with the email address, this will not be stored anywhere on your computer or in calibre. It will only be used to fetch the key from the B&N server.</li>
|
||||
</ul>
|
||||
|
||||
<p>Click the OK button to create and store the generated key. Or Cancel if you don’t want to create a key.</p>
|
||||
<p>New keys are checked against the current list of keys before being added, and duplicates are discarded.</p>
|
||||
|
||||
<h3>Deleting Keys:</h3>
|
||||
|
||||
<p>On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a red "X". Clicking this button will delete the highlighted key in the list. You will be prompted once to be sure that’s what you truly mean to do. Once gone, it’s permanently gone.</p>
|
||||
|
||||
<h3>Renaming Keys:</h3>
|
||||
|
||||
<p>On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a sheet of paper. Clicking this button will prompt you to enter a new name for the highlighted key in the list. Enter the new name for the encryption key and click the OK button to use the new name, or Cancel to revert to the old name..</p>
|
||||
|
||||
<h3>Exporting Keys:</h3>
|
||||
|
||||
<p>On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a computer’s hard-drive. Use this button to export the highlighted key to a file (with a ‘.b64’ file name extension). Used for backup purposes or to migrate key data to other computers/calibre installations. The dialog will prompt you for a place to save the file.</p>
|
||||
|
||||
<h3>Importing Existing Keyfiles:</h3>
|
||||
|
||||
<p>At the bottom-left of the plugin’s customization dialog, you will see a button labeled "Import Existing Keyfiles". Use this button to import existing ‘.b64’ key files. Key files might come from being exported from this or older plugins, or may have been generated using the original i♥cabbages script, or you may have made it by following the instructions above.</p>
|
||||
|
||||
<p>Once done creating/deleting/renaming/importing decryption keys, click Close to exit the customization dialogue. Your changes will only be saved permanently when you click OK in the main configuration dialog.</p>
|
||||
|
||||
<h3>NOOK Study</h3>
|
||||
<p>Books downloaded through NOOK Study may or may not use the key found using the above method. If a book is not decrypted successfully with any of the keys, the plugin will attempt to recover keys from the NOOK Study log file and use them.</p>
|
||||
|
||||
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
43
DeDRM_plugin/DeDRM_EInk Kindle Serial Number_Help.htm
Normal file
43
DeDRM_plugin/DeDRM_EInk Kindle Serial Number_Help.htm
Normal file
@@ -0,0 +1,43 @@
|
||||
<!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 eInk Kindle serial numbers</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 eInk Kindle serial numbers</h1>
|
||||
|
||||
<p>If you have upgraded from an earlier version of the plugin, any existing eInk Kindle serial numbers will have been automatically imported, so you might not need to do any more configuration.</p>
|
||||
|
||||
<p>Please note that Kindle serial numbers are only valid keys for eInk Kindles like the Kindle Touch and PaperWhite. The Kindle Fire and Fire HD do not use their serial number for DRM and it is useless to enter those serial numbers.</p>
|
||||
|
||||
<h3>Creating New Kindle serial numbers:</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 Kindle serial number.</p>
|
||||
<ul>
|
||||
<li><span class="bold">Eink Kindle Serial Number:</span> this is the unique serial number of your device. It usually starts with a ‘B’ or a ‘9’ and is sixteen characters long. For a reference of where to find serial numbers and their ranges, please refer to this <a href="http://wiki.mobileread.com/wiki/Kindle_serial_numbers">mobileread wiki page.</a></li>
|
||||
</ul>
|
||||
|
||||
<p>Click the OK button to save the serial number. Or Cancel if you didn’t want to enter a serial number.</p>
|
||||
|
||||
<h3>Deleting Kindle serial numbers:</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 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 will only be saved permanently when you click OK in the main configuration dialog.</p>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
73
DeDRM_plugin/DeDRM_Help.htm
Normal file
73
DeDRM_plugin/DeDRM_Help.htm
Normal file
@@ -0,0 +1,73 @@
|
||||
<!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>DeDRM Plugin Configuration</title>
|
||||
<style type="text/css">
|
||||
span.version {font-size: 50%}
|
||||
span.bold {font-weight: bold}
|
||||
h3 {margin-bottom: 0}
|
||||
p {margin-top: 0}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<h1>DeDRM Plugin <span class="version">(v6.7.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>
|
||||
|
||||
<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 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>
|
||||
<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 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 used previous versions of the various DeDRM plugins on this machine, you may find that some of the configuration dialogs already contain the information you entered through those previous plugins.</p>
|
||||
|
||||
<p>When you have finished entering your configuration information, you <em>must</em> click the OK button to save it. If you click the Cancel button, all your changes in all the configuration dialogs will be lost.</p>
|
||||
|
||||
<h3>Troubleshooting:</h3>
|
||||
|
||||
<p >If you find that it’s not working for you , you can save a lot of time by trying to add the ebook to Calibre in debug mode. This will print out a lot of helpful info that can be copied into any online help requests.</p>
|
||||
|
||||
<p>Open a command prompt (terminal window) and type "calibre-debug -g" (without the quotes). Calibre will launch, and you can can add the problem ebook the usual way. The debug info will be output to the original command prompt (terminal window). Copy the resulting output and paste it into the comment you make at my blog.</p>
|
||||
<p><span class="bold">Note:</span> The Mac version of Calibre doesn’t install the command line tools by default. If you go to the ‘Preferences’ page and click on the miscellaneous button, you’ll find the option to install the command line tools.</p>
|
||||
|
||||
<h3>Credits:</h3>
|
||||
<ul>
|
||||
<li>The Dark Reverser for the Mobipocket and eReader scripts</li>
|
||||
<li>i♥cabbages for the Adobe Digital Editions scripts</li>
|
||||
<li>Skindle aka Bart Simpson for the Amazon Kindle for PC script</li>
|
||||
<li>CMBDTC for Amazon Topaz DRM removal script</li>
|
||||
<li>some_updates, clarknova and Bart Simpson for Amazon Topaz conversion scripts</li>
|
||||
<li>DiapDealer for the first calibre plugin versions of the tools</li>
|
||||
<li>some_updates, DiapDealer, Apprentice Alf and mdlnx for Amazon Kindle/Mobipocket tools</li>
|
||||
<li>some_updates for the DeDRM all-in-one Python tool</li>
|
||||
<li>Apprentice Alf for the DeDRM all-in-one AppleScript tool</li>
|
||||
<li>Apprentice Alf for the DeDRM all-in-one calibre plugin</li>
|
||||
<li>And probably many more.</li>
|
||||
</ul>
|
||||
|
||||
<h3> For additional help read the <a href="https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md">FAQs</a> at <a href="https://github.com/apprenticeharper/DeDRM_tools/">Apprentice 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>
|
||||
|
||||
<h2>Linux Systems Only</h2>
|
||||
<h3>Generating decryption keys for Adobe Digital Editions and Kindle for PC</h3>
|
||||
<p>If you install Kindle for PC and/or Adobe Digital Editions in Wine, you will be able to download DRMed ebooks to them under Wine. To be able to remove the DRM, you will need to generate key files and add them in the plugin's customisation dialogs.</p>
|
||||
|
||||
<p>To generate the key files you will need to install Python and PyCrypto under the same Wine setup as your Kindle for PC and/or Adobe Digital Editions installations. (Kindle for PC, Python and Pycrypto installation instructions in the ReadMe.)</p>
|
||||
|
||||
<p>Once everything's installed under Wine, you'll need to run the adobekey.pyw script (for Adobe Digital Editions) and kindlekey.pyw (For Kindle for PC) using the python installation in your Wine system. The scripts can be found in Other_Tools/Key_Retrieval_Scripts.</p>
|
||||
|
||||
<p>Each script will create a key file in the same folder as the script. Copy the key files to your Linux system and then load the key files using the Adobe Digital Editions ebooks dialog and the Kindle for Mac/PC ebooks dialog.</p>
|
||||
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
61
DeDRM_plugin/DeDRM_Kindle for Android Key_Help.htm
Normal file
61
DeDRM_plugin/DeDRM_Kindle for Android Key_Help.htm
Normal file
@@ -0,0 +1,61 @@
|
||||
<!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 Kindle for Android Keys</title>
|
||||
<style type="text/css">
|
||||
span.version {font-size: 50%}
|
||||
span.bold {font-weight: bold}
|
||||
h3 {margin-bottom: 0}
|
||||
p {margin-top: 0}
|
||||
li {margin-top: 0.5em}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<h1>Managing Kindle for Android Keys</h1>
|
||||
|
||||
<p>Amazon's Kindle for Android application uses an internal key equivalent to an eInk Kindle's serial number. Extracting that key is a little tricky, but worth it, as it then allows the DRM to be removed from any Kindle ebooks that have been downloaded to that Android device.</p>
|
||||
|
||||
<p>Please note that it is not currently known whether the same applies to the Kindle application on the Kindle Fire and Fire HD.</p>
|
||||
|
||||
<h3>Getting the Kindle for Android backup file</h3>
|
||||
|
||||
<p>Obtain and install adb (Android Debug Bridge) on your computer. Details of how to do this are beyond the scope of this help file, but there are plenty of on-line guides.</p>
|
||||
<p>Enable developer mode on your Android device. Again, look for an on-line guide for your device.</p>
|
||||
<p>Once you have adb installed and your device in developer mode, connect your device to your computer with a USB cable and then open up a command line (Terminal on Mac OS X and cmd.exe on Windows) and enter "adb backup com.amazon.kindle" (without the quotation marks!) and press return. A file "backup.ab" should be created in your home directory.
|
||||
|
||||
<h3>Adding a Kindle for Android Key</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 with two main controls.
|
||||
<ul>
|
||||
<li><span class="bold">Choose backup file:</span> click this button and you will be prompted to find the backup.ab file you created earlier. Once selected the file will be processed to extract the decryption key, and if successful the file name will be displayed to the right of the button.</li>
|
||||
<li><span class="bold">Unique Key Name:</span> this is a unique name you choose to help you identify the key. This name will show in the list of Kindle for Android keys. Enter a name that will help you remember which device this key came from.</li>
|
||||
</ul>
|
||||
|
||||
<p>Click the OK button to store the Kindle for Android key for the current list of Kindle for Android keys. Or click Cancel if you don’t want to store the key.</p>
|
||||
<p>New keys are checked against the current list of keys before being added, and duplicates are discarded.</p>
|
||||
|
||||
<h3>Deleting Keys:</h3>
|
||||
|
||||
<p>On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a red "X". Clicking this button will delete the highlighted key in the list. You will be prompted once to be sure that’s what you truly mean to do. Once gone, it’s permanently gone.</p>
|
||||
|
||||
<h3>Renaming Keys:</h3>
|
||||
|
||||
<p>On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a sheet of paper. Clicking this button will prompt you to enter a new name for the highlighted key in the list. Enter the new name for the key and click the OK button to use the new name, or Cancel to revert to the old name.</p>
|
||||
|
||||
<h3>Exporting Keys:</h3>
|
||||
|
||||
<p>On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a computer’s hard-drive. Use this button to export the highlighted key to a file (with a ‘.k4a' file name extension). Used for backup purposes or to migrate key data to other computers/calibre installations. The dialog will prompt you for a place to save the file.</p>
|
||||
|
||||
<h3>Importing Existing Keyfiles:</h3>
|
||||
|
||||
<p>At the bottom-left of the plugin’s customization dialog, you will see a button labeled "Import Existing Keyfiles". Use this button to import any ‘.k4a’ file you obtained by using the androidkindlekey.py script manually, or by exporting from another copy of calibre.</p>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
59
DeDRM_plugin/DeDRM_Kindle for Mac and PC Key_Help.htm
Normal file
59
DeDRM_plugin/DeDRM_Kindle for Mac and PC Key_Help.htm
Normal file
@@ -0,0 +1,59 @@
|
||||
<!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 Kindle for Mac/PC Keys</title>
|
||||
<style type="text/css">
|
||||
span.version {font-size: 50%}
|
||||
span.bold {font-weight: bold}
|
||||
h3 {margin-bottom: 0}
|
||||
p {margin-top: 0}
|
||||
li {margin-top: 0.5em}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<h1>Managing Kindle for Mac/PC Keys</h1>
|
||||
|
||||
|
||||
<p>If you have upgraded from an earlier version of the plugin, any existing Kindle for Mac/PC keys will have been automatically imported, so you might not need to do any more configuration. In addition, on Windows and Mac, the default Kindle for Mac/PC key is added the first time the plugin is run. Continue reading for key generation and management instructions.</p>
|
||||
|
||||
<h3>Creating New Keys:</h3>
|
||||
|
||||
<p>On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a green plus sign (+). Clicking this button will open a new dialog prompting you to enter a key name for the default Kindle for Mac/PC key. </p>
|
||||
<ul>
|
||||
<li><span class="bold">Unique Key Name:</span> this is a unique name you choose to help you identify the key. This name will show in the list of configured keys.</li>
|
||||
</ul>
|
||||
|
||||
<p>Click the OK button to create and store the Kindle for Mac/PC key for the current installation of Kindle for Mac/PC. Or Cancel if you don’t want to create the key.</p>
|
||||
<p>New keys are checked against the current list of keys before being added, and duplicates are discarded.</p>
|
||||
|
||||
<h3>Deleting Keys:</h3>
|
||||
|
||||
<p>On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a red "X". Clicking this button will delete the highlighted key in the list. You will be prompted once to be sure that’s what you truly mean to do. Once gone, it’s permanently gone.</p>
|
||||
|
||||
<h3>Renaming Keys:</h3>
|
||||
|
||||
<p>On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a sheet of paper. Clicking this button will prompt you to enter a new name for the highlighted key in the list. Enter the new name for the encryption key and click the OK button to use the new name, or Cancel to revert to the old name..</p>
|
||||
|
||||
<h3>Exporting Keys:</h3>
|
||||
|
||||
<p>On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a computer’s hard-drive. Use this button to export the highlighted key to a file (with a ‘.der’ file name extension). Used for backup purposes or to migrate key data to other computers/calibre installations. The dialog will prompt you for a place to save the file.</p>
|
||||
|
||||
<h3>Linux Users: WINEPREFIX</h3>
|
||||
|
||||
<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>
|
||||
|
||||
<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 ‘.k4i’ key files. Key files might come from being exported from this plugin, or may have been generated using the kindlekey.pyw script running under Wine on Linux systems.</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>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
42
DeDRM_plugin/DeDRM_Mobipocket PID_Help.htm
Normal file
42
DeDRM_plugin/DeDRM_Mobipocket PID_Help.htm
Normal file
@@ -0,0 +1,42 @@
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
|
||||
"http://www.w3.org/TR/html4/strict.dtd">
|
||||
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8">
|
||||
<title>Managing Mobipocket PIDs</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 Mobipocket PIDs</h1>
|
||||
|
||||
<p>If you have upgraded from an earlier version of the plugin, any existing Mobipocket PIDs will have been automatically imported, so you might not need to do any more configuration.</p>
|
||||
|
||||
|
||||
<h3>Creating New Mobipocket PIDs:</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 Mobipocket PID.</p>
|
||||
<ul>
|
||||
<li><span class="bold">PID:</span> this is a PID used to decrypt your Mobipocket ebooks. It is eight or ten characters long. Mobipocket PIDs are usualy displayed in the About screen of your Mobipocket device.</li>
|
||||
</ul>
|
||||
|
||||
<p>Click the OK button to save the PID. Or Cancel if you didn’t want to enter a PID.</p>
|
||||
|
||||
<h3>Deleting Mobipocket PIDs:</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 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>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
56
DeDRM_plugin/DeDRM_eReader Key_Help.htm
Normal file
56
DeDRM_plugin/DeDRM_eReader Key_Help.htm
Normal file
@@ -0,0 +1,56 @@
|
||||
<!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 eReader Keys</title>
|
||||
<style type="text/css">
|
||||
span.version {font-size: 50%}
|
||||
span.bold {font-weight: bold}
|
||||
h3 {margin-bottom: 0}
|
||||
p {margin-top: 0}
|
||||
li {margin-top: 0.5em}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<h1>Managing eReader Keys</h1>
|
||||
|
||||
<p>If you have upgraded from an earlier version of the plugin, any existing eReader (Fictionwise ‘.pdb’) keys will have been automatically imported, so you might not need to do any more configuration. Continue reading for key generation and management instructions.</p>
|
||||
|
||||
<h3>Creating New Keys:</h3>
|
||||
|
||||
<p>On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a green plus sign (+). Clicking this button will open a new dialog for entering the necessary data to generate a new key.</p>
|
||||
<ul>
|
||||
<li><span class="bold">Unique Key Name:</span> this is a unique name you choose to help you identify the key. This name will show in the list of configured keys. Choose something that will help you remember the data (name, cc#) it was created with.</li>
|
||||
<li><span class="bold">Your Name:</span> This is the name used by Fictionwise to generate your encryption key. Since Fictionwise has now closed down, you might not have easy access to this. It was often the name on the Credit Card used at Fictionwise.</li>
|
||||
<li><span class="bold">Credit Card#:</span> this is the default credit card number that was on file with Fictionwise at the time of download of the ebook to be de-DRMed. Just enter the last 8 digits of the number. As with the name, this number will not be stored anywhere on your computer or in calibre. It will only be used in the creation of the one-way hash/key that’s stored in the preferences.</li>
|
||||
</ul>
|
||||
|
||||
<p>Click the OK button to create and store the generated key. Or Cancel if you don’t want to create a key.</p>
|
||||
<p>New keys are checked against the current list of keys before being added, and duplicates are discarded.</p>
|
||||
|
||||
<h3>Deleting Keys:</h3>
|
||||
|
||||
<p>On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a red "X". Clicking this button will delete the highlighted key in the list. You will be prompted once to be sure that’s what you truly mean to do. Once gone, it’s permanently gone.</p>
|
||||
|
||||
<h3>Renaming Keys:</h3>
|
||||
|
||||
<p>On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a sheet of paper. Clicking this button will 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>
|
||||
|
||||
<h3>Exporting Keys:</h3>
|
||||
|
||||
<p>On the right-hand side of the plugin’s customization dialog, you will see a button with an icon that looks like a computer’s hard-drive. Use this button to export the highlighted key to a file (with a ‘.b63’ file name extension). Used for backup purposes or to migrate key data to other computers/calibre installations. The dialog will prompt you for a place to save the file.</p>
|
||||
|
||||
<h3>Importing Existing Keyfiles:</h3>
|
||||
|
||||
<p>At the bottom-left of the plugin’s customization dialog, you will see a button labeled "Import Existing Keyfiles". Use this button to import existing ‘.b63’ key files that have previously been exported.</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>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
658
DeDRM_plugin/__init__.py
Normal file
658
DeDRM_plugin/__init__.py
Normal file
@@ -0,0 +1,658 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# __init__.py for DeDRM_plugin
|
||||
# Copyright © 2008-2020 Apprentice Harper et al.
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = '7.1.0'
|
||||
__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
|
||||
|
||||
"""
|
||||
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, re
|
||||
import time
|
||||
import zipfile
|
||||
import traceback
|
||||
from zipfile import ZipFile
|
||||
|
||||
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):
|
||||
data = data.encode(self.encoding,"replace")
|
||||
try:
|
||||
self.stream.buffer.write(data)
|
||||
self.stream.buffer.flush()
|
||||
except:
|
||||
# We can do nothing if a write fails
|
||||
pass
|
||||
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), 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, Aprentice Harper, The Dark Reverser and i♥cabbages"
|
||||
version = PLUGIN_VERSION_TUPLE
|
||||
minimum_calibre_version = (5, 0, 0) # Python 3.
|
||||
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 ePubDecrypt(self,path_to_ebook):
|
||||
# Create a TemporaryPersistent file to work with.
|
||||
# Check original epub archive for zip errors.
|
||||
import calibre_plugins.dedrm.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 Exception(e)
|
||||
|
||||
# import the decryption keys
|
||||
import calibre_plugins.dedrm.prefs as prefs
|
||||
dedrmprefs = prefs.DeDRM_Prefs()
|
||||
|
||||
# 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 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 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):
|
||||
print("{0} v{1}: {2} is a secure Adobe Adept 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, 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:
|
||||
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 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 = adeptkeys()
|
||||
else: # linux
|
||||
from .wineutils import WineGetKeys
|
||||
|
||||
scriptpath = os.path.join(self.alfdir,"adobekey.py")
|
||||
defaultkeys = WineGetKeys(scriptpath, ".der",dedrmprefs['adobewineprefix'])
|
||||
|
||||
self.default_key = defaultkeys[0]
|
||||
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 = []
|
||||
for keyvalue in defaultkeys:
|
||||
if codecs.encode(keyvalue, 'hex').decode('ascii') not in dedrmprefs['adeptkeys'].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 = 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','default_key',codecs.encode(keyvalue, '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 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
|
||||
# Import the fixed epub.
|
||||
print("{0} v{1}: “{2}” is neither an Adobe Adept nor a Barnes & Noble encrypted ePub".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook)))
|
||||
raise DeDRMError("{0} v{1}: Couldn't decrypt after {2:.1f} seconds. DRM free perhaps?".format(PLUGIN_NAME, PLUGIN_VERSION,time.time()-self.starttime))
|
||||
|
||||
def PDFDecrypt(self,path_to_ebook):
|
||||
import calibre_plugins.dedrm.prefs as prefs
|
||||
import calibre_plugins.dedrm.ineptpdf
|
||||
|
||||
dedrmprefs = prefs.DeDRM_Prefs()
|
||||
# Attempt to decrypt epub with each encryption key (generated or provided).
|
||||
print("{0} v{1}: {2} is a PDF ebook".format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook)))
|
||||
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:
|
||||
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.
|
||||
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 = adeptkeys()
|
||||
else: # linux
|
||||
from .wineutils import WineGetKeys
|
||||
|
||||
scriptpath = os.path.join(self.alfdir,"adobekey.py")
|
||||
defaultkeys = WineGetKeys(scriptpath, ".der",dedrmprefs['adobewineprefix'])
|
||||
|
||||
self.default_key = defaultkeys[0]
|
||||
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 = []
|
||||
for keyvalue in defaultkeys:
|
||||
if codecs.encode(keyvalue,'hex') not in dedrmprefs['adeptkeys'].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(".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','default_key',codecs.encode(keyvalue,'hex'))
|
||||
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
|
||||
|
||||
# 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()
|
||||
75
DeDRM_plugin/activitybar.py
Normal file
75
DeDRM_plugin/activitybar.py
Normal file
@@ -0,0 +1,75 @@
|
||||
import sys
|
||||
import tkinter
|
||||
import tkinter.constants
|
||||
|
||||
class ActivityBar(tkinter.Frame):
|
||||
|
||||
def __init__(self, master, length=300, height=20, barwidth=15, interval=50, bg='white', fillcolor='orchid1',\
|
||||
bd=2, relief=tkinter.constants.GROOVE, *args, **kw):
|
||||
tkinter.Frame.__init__(self, master, bg=bg, width=length, height=height, *args, **kw)
|
||||
self._master = master
|
||||
self._interval = interval
|
||||
self._maximum = length
|
||||
self._startx = 0
|
||||
self._barwidth = barwidth
|
||||
self._bardiv = length / barwidth
|
||||
if self._bardiv < 10:
|
||||
self._bardiv = 10
|
||||
stopx = self._startx + self._barwidth
|
||||
if stopx > self._maximum:
|
||||
stopx = self._maximum
|
||||
# self._canv = Tkinter.Canvas(self, bg=self['bg'], width=self['width'], height=self['height'],\
|
||||
# highlightthickness=0, relief='flat', bd=0)
|
||||
self._canv = tkinter.Canvas(self, bg=self['bg'], width=self['width'], height=self['height'],\
|
||||
highlightthickness=0, relief=relief, bd=bd)
|
||||
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._set()
|
||||
self.bind('<Configure>', self._update_coords)
|
||||
self._running = False
|
||||
|
||||
def _update_coords(self, event):
|
||||
'''Updates the position of the rectangle inside the canvas when the size of
|
||||
the widget gets changed.'''
|
||||
# looks like we have to call update_idletasks() twice to make sure
|
||||
# to get the results we expect
|
||||
self._canv.update_idletasks()
|
||||
self._maximum = self._canv.winfo_width()
|
||||
self._startx = 0
|
||||
self._barwidth = self._maximum / self._bardiv
|
||||
if self._barwidth < 2:
|
||||
self._barwidth = 2
|
||||
stopx = self._startx + self._barwidth
|
||||
if stopx > self._maximum:
|
||||
stopx = self._maximum
|
||||
self._canv.coords(self._rect, 0, 0, stopx, self._canv.winfo_height())
|
||||
self._canv.update_idletasks()
|
||||
|
||||
def _set(self):
|
||||
if self._startx < 0:
|
||||
self._startx = 0
|
||||
if self._startx > self._maximum:
|
||||
self._startx = self._startx % self._maximum
|
||||
stopx = self._startx + self._barwidth
|
||||
if stopx > self._maximum:
|
||||
stopx = self._maximum
|
||||
self._canv.coords(self._rect, self._startx, 0, stopx, self._canv.winfo_height())
|
||||
self._canv.update_idletasks()
|
||||
|
||||
def start(self):
|
||||
self._running = True
|
||||
self.after(self._interval, self._step)
|
||||
|
||||
def stop(self):
|
||||
self._running = False
|
||||
self._set()
|
||||
|
||||
def _step(self):
|
||||
if self._running:
|
||||
stepsize = self._barwidth / 4
|
||||
if stepsize < 2:
|
||||
stepsize = 2
|
||||
self._startx += stepsize
|
||||
self._set()
|
||||
self.after(self._interval, self._step)
|
||||
585
DeDRM_plugin/adobekey.py
Normal file
585
DeDRM_plugin/adobekey.py
Normal file
@@ -0,0 +1,585 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# adobekey.pyw, version 6.0
|
||||
# Copyright © 2009-2020 i♥cabbages, Apprentice Harper et al.
|
||||
|
||||
# Released under the terms of the GNU General Public Licence, version 3
|
||||
# <http://www.gnu.org/licenses/>
|
||||
|
||||
# Revision history:
|
||||
# 1 - Initial release, for Adobe Digital Editions 1.7
|
||||
# 2 - Better algorithm for finding pLK; improved error handling
|
||||
# 3 - Rename to INEPT
|
||||
# 4 - Series of changes by joblack (and others?) --
|
||||
# 4.1 - quick beta fix for ADE 1.7.2 (anon)
|
||||
# 4.2 - added old 1.7.1 processing
|
||||
# 4.3 - better key search
|
||||
# 4.4 - Make it working on 64-bit Python
|
||||
# 5 - Clean up and improve 4.x changes;
|
||||
# Clean up and merge OS X support by unknown
|
||||
# 5.1 - add support for using OpenSSL on Windows in place of PyCrypto
|
||||
# 5.2 - added support for output of key to a particular file
|
||||
# 5.3 - On Windows try PyCrypto first, OpenSSL next
|
||||
# 5.4 - Modify interface to allow use of import
|
||||
# 5.5 - Fix for potential problem with PyCrypto
|
||||
# 5.6 - Revised to allow use in Plugins to eliminate need for duplicate code
|
||||
# 5.7 - Unicode support added, renamed adobekey from ineptkey
|
||||
# 5.8 - Added getkey interface for Windows DeDRM application
|
||||
# 5.9 - moved unicode_argv call inside main for Windows DeDRM compatibility
|
||||
# 6.0 - Work if TkInter is missing
|
||||
# 7.0 - Python 3 for calibre 5
|
||||
|
||||
"""
|
||||
Retrieve Adobe ADEPT user key.
|
||||
"""
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = '7.0'
|
||||
|
||||
import sys, os, struct, getopt
|
||||
from base64 import b64decode
|
||||
|
||||
|
||||
# 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):
|
||||
data = data.encode(self.encoding,"replace")
|
||||
self.stream.buffer.write(data)
|
||||
self.stream.buffer.flush()
|
||||
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
try:
|
||||
from calibre.constants import iswindows, isosx
|
||||
except:
|
||||
iswindows = sys.platform.startswith('win')
|
||||
isosx = sys.platform.startswith('darwin')
|
||||
|
||||
def unicode_argv():
|
||||
if iswindows:
|
||||
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
||||
# strings.
|
||||
|
||||
# Versions 2.x of Python don't support Unicode in sys.argv on
|
||||
# Windows, with the underlying Windows API instead replacing multi-byte
|
||||
# characters with '?'. So use shell32.GetCommandLineArgvW to get sys.argv
|
||||
# as a list of Unicode strings and encode them as utf-8
|
||||
|
||||
from ctypes import POINTER, byref, cdll, c_int, windll
|
||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
||||
|
||||
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
||||
GetCommandLineW.argtypes = []
|
||||
GetCommandLineW.restype = LPCWSTR
|
||||
|
||||
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
||||
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
||||
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
||||
|
||||
cmd = GetCommandLineW()
|
||||
argc = c_int(0)
|
||||
argv = CommandLineToArgvW(cmd, byref(argc))
|
||||
if argc.value > 0:
|
||||
# Remove Python executable and commands if present
|
||||
start = argc.value - len(sys.argv)
|
||||
return [argv[i] for i in
|
||||
range(start, argc.value)]
|
||||
# if we don't have any arguments at all, just pass back script name
|
||||
# this should never happen
|
||||
return ["adobekey.py"]
|
||||
else:
|
||||
argvencoding = sys.stdin.encoding or "utf-8"
|
||||
return [arg if isinstance(arg, str) else str(arg, argvencoding) for arg in sys.argv]
|
||||
|
||||
class ADEPTError(Exception):
|
||||
pass
|
||||
|
||||
if iswindows:
|
||||
from ctypes import windll, c_char_p, c_wchar_p, c_uint, POINTER, byref, \
|
||||
create_unicode_buffer, create_string_buffer, CFUNCTYPE, addressof, \
|
||||
string_at, Structure, c_void_p, cast, c_size_t, memmove, CDLL, c_int, \
|
||||
c_long, c_ulong
|
||||
|
||||
from ctypes.wintypes import LPVOID, DWORD, BOOL
|
||||
import winreg
|
||||
|
||||
def _load_crypto_libcrypto():
|
||||
from ctypes.util import find_library
|
||||
libcrypto = find_library('libeay32')
|
||||
if libcrypto is None:
|
||||
raise ADEPTError('libcrypto not found')
|
||||
libcrypto = CDLL(libcrypto)
|
||||
AES_MAXNR = 14
|
||||
c_char_pp = POINTER(c_char_p)
|
||||
c_int_p = POINTER(c_int)
|
||||
class AES_KEY(Structure):
|
||||
_fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))),
|
||||
('rounds', c_int)]
|
||||
AES_KEY_p = POINTER(AES_KEY)
|
||||
|
||||
def F(restype, name, argtypes):
|
||||
func = getattr(libcrypto, name)
|
||||
func.restype = restype
|
||||
func.argtypes = argtypes
|
||||
return func
|
||||
|
||||
AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key',
|
||||
[c_char_p, c_int, AES_KEY_p])
|
||||
AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',
|
||||
[c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p,
|
||||
c_int])
|
||||
class AES(object):
|
||||
def __init__(self, userkey):
|
||||
self._blocksize = len(userkey)
|
||||
if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) :
|
||||
raise ADEPTError('AES improper key used')
|
||||
key = self._key = AES_KEY()
|
||||
rv = AES_set_decrypt_key(userkey, len(userkey) * 8, key)
|
||||
if rv < 0:
|
||||
raise ADEPTError('Failed to initialize AES key')
|
||||
def decrypt(self, data):
|
||||
out = create_string_buffer(len(data))
|
||||
iv = (b"\x00" * self._blocksize)
|
||||
rv = AES_cbc_encrypt(data, out, len(data), self._key, iv, 0)
|
||||
if rv == 0:
|
||||
raise ADEPTError('AES decryption failed')
|
||||
return out.raw
|
||||
return AES
|
||||
|
||||
def _load_crypto_pycrypto():
|
||||
from Crypto.Cipher import AES as _AES
|
||||
class AES(object):
|
||||
def __init__(self, key):
|
||||
self._aes = _AES.new(key, _AES.MODE_CBC, b'\x00'*16)
|
||||
def decrypt(self, data):
|
||||
return self._aes.decrypt(data)
|
||||
return AES
|
||||
|
||||
def _load_crypto():
|
||||
AES = None
|
||||
for loader in (_load_crypto_pycrypto, _load_crypto_libcrypto):
|
||||
try:
|
||||
AES = loader()
|
||||
break
|
||||
except (ImportError, ADEPTError):
|
||||
pass
|
||||
return AES
|
||||
|
||||
AES = _load_crypto()
|
||||
|
||||
|
||||
DEVICE_KEY_PATH = r'Software\Adobe\Adept\Device'
|
||||
PRIVATE_LICENCE_KEY_PATH = r'Software\Adobe\Adept\Activation'
|
||||
|
||||
MAX_PATH = 255
|
||||
|
||||
kernel32 = windll.kernel32
|
||||
advapi32 = windll.advapi32
|
||||
crypt32 = windll.crypt32
|
||||
|
||||
def GetSystemDirectory():
|
||||
GetSystemDirectoryW = kernel32.GetSystemDirectoryW
|
||||
GetSystemDirectoryW.argtypes = [c_wchar_p, c_uint]
|
||||
GetSystemDirectoryW.restype = c_uint
|
||||
def GetSystemDirectory():
|
||||
buffer = create_unicode_buffer(MAX_PATH + 1)
|
||||
GetSystemDirectoryW(buffer, len(buffer))
|
||||
return buffer.value
|
||||
return GetSystemDirectory
|
||||
GetSystemDirectory = GetSystemDirectory()
|
||||
|
||||
def GetVolumeSerialNumber():
|
||||
GetVolumeInformationW = kernel32.GetVolumeInformationW
|
||||
GetVolumeInformationW.argtypes = [c_wchar_p, c_wchar_p, c_uint,
|
||||
POINTER(c_uint), POINTER(c_uint),
|
||||
POINTER(c_uint), c_wchar_p, c_uint]
|
||||
GetVolumeInformationW.restype = c_uint
|
||||
def GetVolumeSerialNumber(path):
|
||||
vsn = c_uint(0)
|
||||
GetVolumeInformationW(
|
||||
path, None, 0, byref(vsn), None, None, None, 0)
|
||||
return vsn.value
|
||||
return GetVolumeSerialNumber
|
||||
GetVolumeSerialNumber = GetVolumeSerialNumber()
|
||||
|
||||
def GetUserName():
|
||||
GetUserNameW = advapi32.GetUserNameW
|
||||
GetUserNameW.argtypes = [c_wchar_p, POINTER(c_uint)]
|
||||
GetUserNameW.restype = c_uint
|
||||
def GetUserName():
|
||||
buffer = create_unicode_buffer(32)
|
||||
size = c_uint(len(buffer))
|
||||
while not GetUserNameW(buffer, byref(size)):
|
||||
buffer = create_unicode_buffer(len(buffer) * 2)
|
||||
size.value = len(buffer)
|
||||
return buffer.value.encode('utf-16-le')[::2]
|
||||
return GetUserName
|
||||
GetUserName = GetUserName()
|
||||
|
||||
PAGE_EXECUTE_READWRITE = 0x40
|
||||
MEM_COMMIT = 0x1000
|
||||
MEM_RESERVE = 0x2000
|
||||
|
||||
def VirtualAlloc():
|
||||
_VirtualAlloc = kernel32.VirtualAlloc
|
||||
_VirtualAlloc.argtypes = [LPVOID, c_size_t, DWORD, DWORD]
|
||||
_VirtualAlloc.restype = LPVOID
|
||||
def VirtualAlloc(addr, size, alloctype=(MEM_COMMIT | MEM_RESERVE),
|
||||
protect=PAGE_EXECUTE_READWRITE):
|
||||
return _VirtualAlloc(addr, size, alloctype, protect)
|
||||
return VirtualAlloc
|
||||
VirtualAlloc = VirtualAlloc()
|
||||
|
||||
MEM_RELEASE = 0x8000
|
||||
|
||||
def VirtualFree():
|
||||
_VirtualFree = kernel32.VirtualFree
|
||||
_VirtualFree.argtypes = [LPVOID, c_size_t, DWORD]
|
||||
_VirtualFree.restype = BOOL
|
||||
def VirtualFree(addr, size=0, freetype=MEM_RELEASE):
|
||||
return _VirtualFree(addr, size, freetype)
|
||||
return VirtualFree
|
||||
VirtualFree = VirtualFree()
|
||||
|
||||
class NativeFunction(object):
|
||||
def __init__(self, restype, argtypes, insns):
|
||||
self._buf = buf = VirtualAlloc(None, len(insns))
|
||||
memmove(buf, insns, len(insns))
|
||||
ftype = CFUNCTYPE(restype, *argtypes)
|
||||
self._native = ftype(buf)
|
||||
|
||||
def __call__(self, *args):
|
||||
return self._native(*args)
|
||||
|
||||
def __del__(self):
|
||||
if self._buf is not None:
|
||||
VirtualFree(self._buf)
|
||||
self._buf = None
|
||||
|
||||
if struct.calcsize("P") == 4:
|
||||
CPUID0_INSNS = (
|
||||
b"\x53" # push %ebx
|
||||
b"\x31\xc0" # xor %eax,%eax
|
||||
b"\x0f\xa2" # cpuid
|
||||
b"\x8b\x44\x24\x08" # mov 0x8(%esp),%eax
|
||||
b"\x89\x18" # mov %ebx,0x0(%eax)
|
||||
b"\x89\x50\x04" # mov %edx,0x4(%eax)
|
||||
b"\x89\x48\x08" # mov %ecx,0x8(%eax)
|
||||
b"\x5b" # pop %ebx
|
||||
b"\xc3" # ret
|
||||
)
|
||||
CPUID1_INSNS = (
|
||||
b"\x53" # push %ebx
|
||||
b"\x31\xc0" # xor %eax,%eax
|
||||
b"\x40" # inc %eax
|
||||
b"\x0f\xa2" # cpuid
|
||||
b"\x5b" # pop %ebx
|
||||
b"\xc3" # ret
|
||||
)
|
||||
else:
|
||||
CPUID0_INSNS = (
|
||||
b"\x49\x89\xd8" # mov %rbx,%r8
|
||||
b"\x49\x89\xc9" # mov %rcx,%r9
|
||||
b"\x48\x31\xc0" # xor %rax,%rax
|
||||
b"\x0f\xa2" # cpuid
|
||||
b"\x4c\x89\xc8" # mov %r9,%rax
|
||||
b"\x89\x18" # mov %ebx,0x0(%rax)
|
||||
b"\x89\x50\x04" # mov %edx,0x4(%rax)
|
||||
b"\x89\x48\x08" # mov %ecx,0x8(%rax)
|
||||
b"\x4c\x89\xc3" # mov %r8,%rbx
|
||||
b"\xc3" # retq
|
||||
)
|
||||
CPUID1_INSNS = (
|
||||
b"\x53" # push %rbx
|
||||
b"\x48\x31\xc0" # xor %rax,%rax
|
||||
b"\x48\xff\xc0" # inc %rax
|
||||
b"\x0f\xa2" # cpuid
|
||||
b"\x5b" # pop %rbx
|
||||
b"\xc3" # retq
|
||||
)
|
||||
|
||||
def cpuid0():
|
||||
_cpuid0 = NativeFunction(None, [c_char_p], CPUID0_INSNS)
|
||||
buf = create_string_buffer(12)
|
||||
def cpuid0():
|
||||
_cpuid0(buf)
|
||||
return buf.raw
|
||||
return cpuid0
|
||||
cpuid0 = cpuid0()
|
||||
|
||||
cpuid1 = NativeFunction(c_uint, [], CPUID1_INSNS)
|
||||
|
||||
class DataBlob(Structure):
|
||||
_fields_ = [('cbData', c_uint),
|
||||
('pbData', c_void_p)]
|
||||
DataBlob_p = POINTER(DataBlob)
|
||||
|
||||
def CryptUnprotectData():
|
||||
_CryptUnprotectData = crypt32.CryptUnprotectData
|
||||
_CryptUnprotectData.argtypes = [DataBlob_p, c_wchar_p, DataBlob_p,
|
||||
c_void_p, c_void_p, c_uint, DataBlob_p]
|
||||
_CryptUnprotectData.restype = c_uint
|
||||
def CryptUnprotectData(indata, entropy):
|
||||
indatab = create_string_buffer(indata)
|
||||
indata = DataBlob(len(indata), cast(indatab, c_void_p))
|
||||
entropyb = create_string_buffer(entropy)
|
||||
entropy = DataBlob(len(entropy), cast(entropyb, c_void_p))
|
||||
outdata = DataBlob()
|
||||
if not _CryptUnprotectData(byref(indata), None, byref(entropy),
|
||||
None, None, 0, byref(outdata)):
|
||||
raise ADEPTError("Failed to decrypt user key key (sic)")
|
||||
return string_at(outdata.pbData, outdata.cbData)
|
||||
return CryptUnprotectData
|
||||
CryptUnprotectData = CryptUnprotectData()
|
||||
|
||||
def adeptkeys():
|
||||
if AES is None:
|
||||
raise ADEPTError("PyCrypto or OpenSSL must be installed")
|
||||
root = GetSystemDirectory().split('\\')[0] + '\\'
|
||||
serial = GetVolumeSerialNumber(root)
|
||||
vendor = cpuid0()
|
||||
signature = struct.pack('>I', cpuid1())[1:]
|
||||
user = GetUserName()
|
||||
entropy = struct.pack('>I12s3s13s', serial, vendor, signature, user)
|
||||
cuser = winreg.HKEY_CURRENT_USER
|
||||
try:
|
||||
regkey = winreg.OpenKey(cuser, DEVICE_KEY_PATH)
|
||||
device = winreg.QueryValueEx(regkey, 'key')[0]
|
||||
except WindowsError:
|
||||
raise ADEPTError("Adobe Digital Editions not activated")
|
||||
keykey = CryptUnprotectData(device, entropy)
|
||||
userkey = None
|
||||
keys = []
|
||||
try:
|
||||
plkroot = winreg.OpenKey(cuser, PRIVATE_LICENCE_KEY_PATH)
|
||||
except WindowsError:
|
||||
raise ADEPTError("Could not locate ADE activation")
|
||||
for i in range(0, 16):
|
||||
try:
|
||||
plkparent = winreg.OpenKey(plkroot, "%04d" % (i,))
|
||||
except WindowsError:
|
||||
break
|
||||
ktype = winreg.QueryValueEx(plkparent, None)[0]
|
||||
if ktype != 'credentials':
|
||||
continue
|
||||
for j in range(0, 16):
|
||||
try:
|
||||
plkkey = winreg.OpenKey(plkparent, "%04d" % (j,))
|
||||
except WindowsError:
|
||||
break
|
||||
ktype = winreg.QueryValueEx(plkkey, None)[0]
|
||||
if ktype != 'privateLicenseKey':
|
||||
continue
|
||||
userkey = winreg.QueryValueEx(plkkey, 'value')[0]
|
||||
userkey = b64decode(userkey)
|
||||
aes = AES(keykey)
|
||||
userkey = aes.decrypt(userkey)
|
||||
userkey = userkey[26:-ord(userkey[-1:])]
|
||||
#print "found key:",userkey.encode('hex')
|
||||
keys.append(userkey)
|
||||
if len(keys) == 0:
|
||||
raise ADEPTError('Could not locate privateLicenseKey')
|
||||
print("Found {0:d} keys".format(len(keys)))
|
||||
return keys
|
||||
|
||||
|
||||
elif isosx:
|
||||
import xml.etree.ElementTree as etree
|
||||
import subprocess
|
||||
|
||||
NSMAP = {'adept': 'http://ns.adobe.com/adept',
|
||||
'enc': 'http://www.w3.org/2001/04/xmlenc#'}
|
||||
|
||||
def findActivationDat():
|
||||
import warnings
|
||||
warnings.filterwarnings('ignore', category=FutureWarning)
|
||||
|
||||
home = os.getenv('HOME')
|
||||
cmdline = 'find "' + home + '/Library/Application Support/Adobe/Digital Editions" -name "activation.dat"'
|
||||
cmdline = cmdline.encode(sys.getfilesystemencoding())
|
||||
p2 = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
|
||||
out1, out2 = p2.communicate()
|
||||
reslst = out1.split(b'\n')
|
||||
cnt = len(reslst)
|
||||
ActDatPath = b"activation.dat"
|
||||
for j in range(cnt):
|
||||
resline = reslst[j]
|
||||
pp = resline.find(b'activation.dat')
|
||||
if pp >= 0:
|
||||
ActDatPath = resline
|
||||
break
|
||||
if os.path.exists(ActDatPath):
|
||||
return ActDatPath
|
||||
return None
|
||||
|
||||
def adeptkeys():
|
||||
actpath = findActivationDat()
|
||||
if actpath is None:
|
||||
raise ADEPTError("Could not find ADE activation.dat file.")
|
||||
tree = etree.parse(actpath)
|
||||
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
||||
expr = '//%s/%s' % (adept('credentials'), adept('privateLicenseKey'))
|
||||
userkey = tree.findtext(expr)
|
||||
userkey = b64decode(userkey)
|
||||
userkey = userkey[26:]
|
||||
return [userkey]
|
||||
|
||||
else:
|
||||
def adeptkeys():
|
||||
raise ADEPTError("This script only supports Windows and Mac OS X.")
|
||||
return []
|
||||
|
||||
# interface for Python DeDRM
|
||||
def getkey(outpath):
|
||||
keys = adeptkeys()
|
||||
if len(keys) > 0:
|
||||
if not os.path.isdir(outpath):
|
||||
outfile = outpath
|
||||
with open(outfile, 'wb') as keyfileout:
|
||||
keyfileout.write(keys[0])
|
||||
print("Saved a key to {0}".format(outfile))
|
||||
else:
|
||||
keycount = 0
|
||||
for key in keys:
|
||||
while True:
|
||||
keycount += 1
|
||||
outfile = os.path.join(outpath,"adobekey_{0:d}.der".format(keycount))
|
||||
if not os.path.exists(outfile):
|
||||
break
|
||||
with open(outfile, 'wb') as keyfileout:
|
||||
keyfileout.write(key)
|
||||
print("Saved a key to {0}".format(outfile))
|
||||
return True
|
||||
return False
|
||||
|
||||
def usage(progname):
|
||||
print("Finds, decrypts and saves the default Adobe Adept encryption key(s).")
|
||||
print("Keys are saved to the current directory, or a specified output directory.")
|
||||
print("If a file name is passed instead of a directory, only the first key is saved, in that file.")
|
||||
print("Usage:")
|
||||
print(" {0:s} [-h] [<outpath>]".format(progname))
|
||||
|
||||
def cli_main():
|
||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
argv=unicode_argv()
|
||||
progname = os.path.basename(argv[0])
|
||||
print("{0} v{1}\nCopyright © 2009-2020 i♥cabbages, Apprentice Harper et al.".format(progname,__version__))
|
||||
|
||||
try:
|
||||
opts, args = getopt.getopt(argv[1:], "h")
|
||||
except getopt.GetoptError as err:
|
||||
print("Error in options or arguments: {0}".format(err.args[0]))
|
||||
usage(progname)
|
||||
sys.exit(2)
|
||||
|
||||
for o, a in opts:
|
||||
if o == "-h":
|
||||
usage(progname)
|
||||
sys.exit(0)
|
||||
|
||||
if len(args) > 1:
|
||||
usage(progname)
|
||||
sys.exit(2)
|
||||
|
||||
if len(args) == 1:
|
||||
# save to the specified file or directory
|
||||
outpath = args[0]
|
||||
if not os.path.isabs(outpath):
|
||||
outpath = os.path.abspath(outpath)
|
||||
else:
|
||||
# save to the same directory as the script
|
||||
outpath = os.path.dirname(argv[0])
|
||||
|
||||
# make sure the outpath is the
|
||||
outpath = os.path.realpath(os.path.normpath(outpath))
|
||||
|
||||
keys = adeptkeys()
|
||||
if len(keys) > 0:
|
||||
if not os.path.isdir(outpath):
|
||||
outfile = outpath
|
||||
with open(outfile, 'wb') as keyfileout:
|
||||
keyfileout.write(keys[0])
|
||||
print("Saved a key to {0}".format(outfile))
|
||||
else:
|
||||
keycount = 0
|
||||
for key in keys:
|
||||
while True:
|
||||
keycount += 1
|
||||
outfile = os.path.join(outpath,"adobekey_{0:d}.der".format(keycount))
|
||||
if not os.path.exists(outfile):
|
||||
break
|
||||
with open(outfile, 'wb') as keyfileout:
|
||||
keyfileout.write(key)
|
||||
print("Saved a key to {0}".format(outfile))
|
||||
else:
|
||||
print("Could not retrieve Adobe Adept key.")
|
||||
return 0
|
||||
|
||||
|
||||
def gui_main():
|
||||
try:
|
||||
import tkinter
|
||||
import tkinter.constants
|
||||
import tkinter.messagebox
|
||||
import traceback
|
||||
except:
|
||||
return cli_main()
|
||||
|
||||
class ExceptionDialog(tkinter.Frame):
|
||||
def __init__(self, root, text):
|
||||
tkinter.Frame.__init__(self, root, border=5)
|
||||
label = tkinter.Label(self, text="Unexpected error:",
|
||||
anchor=tkinter.constants.W, justify=tkinter.constants.LEFT)
|
||||
label.pack(fill=tkinter.constants.X, expand=0)
|
||||
self.text = tkinter.Text(self)
|
||||
self.text.pack(fill=tkinter.constants.BOTH, expand=1)
|
||||
|
||||
self.text.insert(tkinter.constants.END, text)
|
||||
|
||||
|
||||
argv=unicode_argv()
|
||||
root = tkinter.Tk()
|
||||
root.withdraw()
|
||||
progpath, progname = os.path.split(argv[0])
|
||||
success = False
|
||||
try:
|
||||
keys = adeptkeys()
|
||||
keycount = 0
|
||||
for key in keys:
|
||||
while True:
|
||||
keycount += 1
|
||||
outfile = os.path.join(progpath,"adobekey_{0:d}.der".format(keycount))
|
||||
if not os.path.exists(outfile):
|
||||
break
|
||||
|
||||
with open(outfile, 'wb') as keyfileout:
|
||||
keyfileout.write(key)
|
||||
success = True
|
||||
tkinter.messagebox.showinfo(progname, "Key successfully retrieved to {0}".format(outfile))
|
||||
except ADEPTError as e:
|
||||
tkinter.messagebox.showerror(progname, "Error: {0}".format(str(e)))
|
||||
except Exception:
|
||||
root.wm_state('normal')
|
||||
root.title(progname)
|
||||
text = traceback.format_exc()
|
||||
ExceptionDialog(root, text).pack(fill=tkinter.constants.BOTH, expand=1)
|
||||
root.mainloop()
|
||||
if not success:
|
||||
return 1
|
||||
return 0
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) > 1:
|
||||
sys.exit(cli_main())
|
||||
sys.exit(gui_main())
|
||||
571
DeDRM_plugin/aescbc.py
Normal file
571
DeDRM_plugin/aescbc.py
Normal file
@@ -0,0 +1,571 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Routines for doing AES CBC in one file
|
||||
|
||||
Modified by some_updates to extract
|
||||
and combine only those parts needed for AES CBC
|
||||
into one simple to add python file
|
||||
|
||||
Original Version
|
||||
Copyright (c) 2002 by Paul A. Lambert
|
||||
Under:
|
||||
CryptoPy Artisitic License Version 1.0
|
||||
See the wonderful pure python package cryptopy-1.2.5
|
||||
and read its LICENSE.txt for complete license details.
|
||||
|
||||
Adjusted for Python 3, September 2020
|
||||
"""
|
||||
|
||||
class CryptoError(Exception):
|
||||
""" Base class for crypto exceptions """
|
||||
def __init__(self,errorMessage='Error!'):
|
||||
self.message = errorMessage
|
||||
def __str__(self):
|
||||
return self.message
|
||||
|
||||
class InitCryptoError(CryptoError):
|
||||
""" Crypto errors during algorithm initialization """
|
||||
class BadKeySizeError(InitCryptoError):
|
||||
""" Bad key size error """
|
||||
class EncryptError(CryptoError):
|
||||
""" Error in encryption processing """
|
||||
class DecryptError(CryptoError):
|
||||
""" Error in decryption processing """
|
||||
class DecryptNotBlockAlignedError(DecryptError):
|
||||
""" Error in decryption processing """
|
||||
|
||||
def xorS(a,b):
|
||||
""" XOR two strings """
|
||||
assert len(a)==len(b)
|
||||
x = []
|
||||
for i in range(len(a)):
|
||||
x.append( chr(ord(a[i])^ord(b[i])))
|
||||
return ''.join(x)
|
||||
|
||||
def xor(a,b):
|
||||
""" XOR two strings """
|
||||
x = []
|
||||
for i in range(min(len(a),len(b))):
|
||||
x.append( chr(ord(a[i])^ord(b[i])))
|
||||
return ''.join(x)
|
||||
|
||||
"""
|
||||
Base 'BlockCipher' and Pad classes for cipher instances.
|
||||
BlockCipher supports automatic padding and type conversion. The BlockCipher
|
||||
class was written to make the actual algorithm code more readable and
|
||||
not for performance.
|
||||
"""
|
||||
|
||||
class BlockCipher:
|
||||
""" Block ciphers """
|
||||
def __init__(self):
|
||||
self.reset()
|
||||
|
||||
def reset(self):
|
||||
self.resetEncrypt()
|
||||
self.resetDecrypt()
|
||||
def resetEncrypt(self):
|
||||
self.encryptBlockCount = 0
|
||||
self.bytesToEncrypt = ''
|
||||
def resetDecrypt(self):
|
||||
self.decryptBlockCount = 0
|
||||
self.bytesToDecrypt = ''
|
||||
|
||||
def encrypt(self, plainText, more = None):
|
||||
""" Encrypt a string and return a binary string """
|
||||
self.bytesToEncrypt += plainText # append plainText to any bytes from prior encrypt
|
||||
numBlocks, numExtraBytes = divmod(len(self.bytesToEncrypt), self.blockSize)
|
||||
cipherText = ''
|
||||
for i in range(numBlocks):
|
||||
bStart = i*self.blockSize
|
||||
ctBlock = self.encryptBlock(self.bytesToEncrypt[bStart:bStart+self.blockSize])
|
||||
self.encryptBlockCount += 1
|
||||
cipherText += ctBlock
|
||||
if numExtraBytes > 0: # save any bytes that are not block aligned
|
||||
self.bytesToEncrypt = self.bytesToEncrypt[-numExtraBytes:]
|
||||
else:
|
||||
self.bytesToEncrypt = ''
|
||||
|
||||
if more == None: # no more data expected from caller
|
||||
finalBytes = self.padding.addPad(self.bytesToEncrypt,self.blockSize)
|
||||
if len(finalBytes) > 0:
|
||||
ctBlock = self.encryptBlock(finalBytes)
|
||||
self.encryptBlockCount += 1
|
||||
cipherText += ctBlock
|
||||
self.resetEncrypt()
|
||||
return cipherText
|
||||
|
||||
def decrypt(self, cipherText, more = None):
|
||||
""" Decrypt a string and return a string """
|
||||
self.bytesToDecrypt += cipherText # append to any bytes from prior decrypt
|
||||
|
||||
numBlocks, numExtraBytes = divmod(len(self.bytesToDecrypt), self.blockSize)
|
||||
if more == None: # no more calls to decrypt, should have all the data
|
||||
if numExtraBytes != 0:
|
||||
raise DecryptNotBlockAlignedError('Data not block aligned on decrypt')
|
||||
|
||||
# hold back some bytes in case last decrypt has zero len
|
||||
if (more != None) and (numExtraBytes == 0) and (numBlocks >0) :
|
||||
numBlocks -= 1
|
||||
numExtraBytes = self.blockSize
|
||||
|
||||
plainText = ''
|
||||
for i in range(numBlocks):
|
||||
bStart = i*self.blockSize
|
||||
ptBlock = self.decryptBlock(self.bytesToDecrypt[bStart : bStart+self.blockSize])
|
||||
self.decryptBlockCount += 1
|
||||
plainText += ptBlock
|
||||
|
||||
if numExtraBytes > 0: # save any bytes that are not block aligned
|
||||
self.bytesToEncrypt = self.bytesToEncrypt[-numExtraBytes:]
|
||||
else:
|
||||
self.bytesToEncrypt = ''
|
||||
|
||||
if more == None: # last decrypt remove padding
|
||||
plainText = self.padding.removePad(plainText, self.blockSize)
|
||||
self.resetDecrypt()
|
||||
return plainText
|
||||
|
||||
|
||||
class Pad:
|
||||
def __init__(self):
|
||||
pass # eventually could put in calculation of min and max size extension
|
||||
|
||||
class padWithPadLen(Pad):
|
||||
""" Pad a binary string with the length of the padding """
|
||||
|
||||
def addPad(self, extraBytes, blockSize):
|
||||
""" Add padding to a binary string to make it an even multiple
|
||||
of the block size """
|
||||
blocks, numExtraBytes = divmod(len(extraBytes), blockSize)
|
||||
padLength = blockSize - numExtraBytes
|
||||
return extraBytes + padLength*chr(padLength)
|
||||
|
||||
def removePad(self, paddedBinaryString, blockSize):
|
||||
""" Remove padding from a binary string """
|
||||
if not(0<len(paddedBinaryString)):
|
||||
raise DecryptNotBlockAlignedError('Expected More Data')
|
||||
return paddedBinaryString[:-ord(paddedBinaryString[-1])]
|
||||
|
||||
class noPadding(Pad):
|
||||
""" No padding. Use this to get ECB behavior from encrypt/decrypt """
|
||||
|
||||
def addPad(self, extraBytes, blockSize):
|
||||
""" Add no padding """
|
||||
return extraBytes
|
||||
|
||||
def removePad(self, paddedBinaryString, blockSize):
|
||||
""" Remove no padding """
|
||||
return paddedBinaryString
|
||||
|
||||
"""
|
||||
Rijndael encryption algorithm
|
||||
This byte oriented implementation is intended to closely
|
||||
match FIPS specification for readability. It is not implemented
|
||||
for performance.
|
||||
"""
|
||||
|
||||
class Rijndael(BlockCipher):
|
||||
""" Rijndael encryption algorithm """
|
||||
def __init__(self, key = None, padding = padWithPadLen(), keySize=16, blockSize=16 ):
|
||||
self.name = 'RIJNDAEL'
|
||||
self.keySize = keySize
|
||||
self.strength = keySize*8
|
||||
self.blockSize = blockSize # blockSize is in bytes
|
||||
self.padding = padding # change default to noPadding() to get normal ECB behavior
|
||||
|
||||
assert( keySize%4==0 and keySize/4 in NrTable[4]),'key size must be 16,20,24,29 or 32 bytes'
|
||||
assert( blockSize%4==0 and blockSize/4 in NrTable), 'block size must be 16,20,24,29 or 32 bytes'
|
||||
|
||||
self.Nb = self.blockSize/4 # Nb is number of columns of 32 bit words
|
||||
self.Nk = keySize/4 # Nk is the key length in 32-bit words
|
||||
self.Nr = NrTable[self.Nb][self.Nk] # The number of rounds (Nr) is a function of
|
||||
# the block (Nb) and key (Nk) sizes.
|
||||
if key != None:
|
||||
self.setKey(key)
|
||||
|
||||
def setKey(self, key):
|
||||
""" Set a key and generate the expanded key """
|
||||
assert( len(key) == (self.Nk*4) ), 'Key length must be same as keySize parameter'
|
||||
self.__expandedKey = keyExpansion(self, key)
|
||||
self.reset() # BlockCipher.reset()
|
||||
|
||||
def encryptBlock(self, plainTextBlock):
|
||||
""" Encrypt a block, plainTextBlock must be a array of bytes [Nb by 4] """
|
||||
self.state = self._toBlock(plainTextBlock)
|
||||
AddRoundKey(self, self.__expandedKey[0:self.Nb])
|
||||
for round in range(1,self.Nr): #for round = 1 step 1 to Nr
|
||||
SubBytes(self)
|
||||
ShiftRows(self)
|
||||
MixColumns(self)
|
||||
AddRoundKey(self, self.__expandedKey[round*self.Nb:(round+1)*self.Nb])
|
||||
SubBytes(self)
|
||||
ShiftRows(self)
|
||||
AddRoundKey(self, self.__expandedKey[self.Nr*self.Nb:(self.Nr+1)*self.Nb])
|
||||
return self._toBString(self.state)
|
||||
|
||||
|
||||
def decryptBlock(self, encryptedBlock):
|
||||
""" decrypt a block (array of bytes) """
|
||||
self.state = self._toBlock(encryptedBlock)
|
||||
AddRoundKey(self, self.__expandedKey[self.Nr*self.Nb:(self.Nr+1)*self.Nb])
|
||||
for round in range(self.Nr-1,0,-1):
|
||||
InvShiftRows(self)
|
||||
InvSubBytes(self)
|
||||
AddRoundKey(self, self.__expandedKey[round*self.Nb:(round+1)*self.Nb])
|
||||
InvMixColumns(self)
|
||||
InvShiftRows(self)
|
||||
InvSubBytes(self)
|
||||
AddRoundKey(self, self.__expandedKey[0:self.Nb])
|
||||
return self._toBString(self.state)
|
||||
|
||||
def _toBlock(self, bs):
|
||||
""" Convert binary string to array of bytes, state[col][row]"""
|
||||
assert ( len(bs) == 4*self.Nb ), 'Rijndarl blocks must be of size blockSize'
|
||||
return [[ord(bs[4*i]),ord(bs[4*i+1]),ord(bs[4*i+2]),ord(bs[4*i+3])] for i in range(self.Nb)]
|
||||
|
||||
def _toBString(self, block):
|
||||
""" Convert block (array of bytes) to binary string """
|
||||
l = []
|
||||
for col in block:
|
||||
for rowElement in col:
|
||||
l.append(chr(rowElement))
|
||||
return ''.join(l)
|
||||
#-------------------------------------
|
||||
""" Number of rounds Nr = NrTable[Nb][Nk]
|
||||
|
||||
Nb Nk=4 Nk=5 Nk=6 Nk=7 Nk=8
|
||||
------------------------------------- """
|
||||
NrTable = {4: {4:10, 5:11, 6:12, 7:13, 8:14},
|
||||
5: {4:11, 5:11, 6:12, 7:13, 8:14},
|
||||
6: {4:12, 5:12, 6:12, 7:13, 8:14},
|
||||
7: {4:13, 5:13, 6:13, 7:13, 8:14},
|
||||
8: {4:14, 5:14, 6:14, 7:14, 8:14}}
|
||||
#-------------------------------------
|
||||
def keyExpansion(algInstance, keyString):
|
||||
""" Expand a string of size keySize into a larger array """
|
||||
Nk, Nb, Nr = algInstance.Nk, algInstance.Nb, algInstance.Nr # for readability
|
||||
key = [ord(byte) for byte in keyString] # convert string to list
|
||||
w = [[key[4*i],key[4*i+1],key[4*i+2],key[4*i+3]] for i in range(Nk)]
|
||||
for i in range(Nk,Nb*(Nr+1)):
|
||||
temp = w[i-1] # a four byte column
|
||||
if (i%Nk) == 0 :
|
||||
temp = temp[1:]+[temp[0]] # RotWord(temp)
|
||||
temp = [ Sbox[byte] for byte in temp ]
|
||||
temp[0] ^= Rcon[i/Nk]
|
||||
elif Nk > 6 and i%Nk == 4 :
|
||||
temp = [ Sbox[byte] for byte in temp ] # SubWord(temp)
|
||||
w.append( [ w[i-Nk][byte]^temp[byte] for byte in range(4) ] )
|
||||
return w
|
||||
|
||||
Rcon = (0,0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80,0x1b,0x36, # note extra '0' !!!
|
||||
0x6c,0xd8,0xab,0x4d,0x9a,0x2f,0x5e,0xbc,0x63,0xc6,
|
||||
0x97,0x35,0x6a,0xd4,0xb3,0x7d,0xfa,0xef,0xc5,0x91)
|
||||
|
||||
#-------------------------------------
|
||||
def AddRoundKey(algInstance, keyBlock):
|
||||
""" XOR the algorithm state with a block of key material """
|
||||
for column in range(algInstance.Nb):
|
||||
for row in range(4):
|
||||
algInstance.state[column][row] ^= keyBlock[column][row]
|
||||
#-------------------------------------
|
||||
|
||||
def SubBytes(algInstance):
|
||||
for column in range(algInstance.Nb):
|
||||
for row in range(4):
|
||||
algInstance.state[column][row] = Sbox[algInstance.state[column][row]]
|
||||
|
||||
def InvSubBytes(algInstance):
|
||||
for column in range(algInstance.Nb):
|
||||
for row in range(4):
|
||||
algInstance.state[column][row] = InvSbox[algInstance.state[column][row]]
|
||||
|
||||
Sbox = (0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5,
|
||||
0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76,
|
||||
0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0,
|
||||
0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0,
|
||||
0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc,
|
||||
0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15,
|
||||
0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a,
|
||||
0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75,
|
||||
0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0,
|
||||
0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84,
|
||||
0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b,
|
||||
0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf,
|
||||
0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85,
|
||||
0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8,
|
||||
0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5,
|
||||
0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2,
|
||||
0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17,
|
||||
0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73,
|
||||
0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88,
|
||||
0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb,
|
||||
0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c,
|
||||
0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79,
|
||||
0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9,
|
||||
0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08,
|
||||
0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6,
|
||||
0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a,
|
||||
0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e,
|
||||
0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e,
|
||||
0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94,
|
||||
0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf,
|
||||
0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68,
|
||||
0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16)
|
||||
|
||||
InvSbox = (0x52,0x09,0x6a,0xd5,0x30,0x36,0xa5,0x38,
|
||||
0xbf,0x40,0xa3,0x9e,0x81,0xf3,0xd7,0xfb,
|
||||
0x7c,0xe3,0x39,0x82,0x9b,0x2f,0xff,0x87,
|
||||
0x34,0x8e,0x43,0x44,0xc4,0xde,0xe9,0xcb,
|
||||
0x54,0x7b,0x94,0x32,0xa6,0xc2,0x23,0x3d,
|
||||
0xee,0x4c,0x95,0x0b,0x42,0xfa,0xc3,0x4e,
|
||||
0x08,0x2e,0xa1,0x66,0x28,0xd9,0x24,0xb2,
|
||||
0x76,0x5b,0xa2,0x49,0x6d,0x8b,0xd1,0x25,
|
||||
0x72,0xf8,0xf6,0x64,0x86,0x68,0x98,0x16,
|
||||
0xd4,0xa4,0x5c,0xcc,0x5d,0x65,0xb6,0x92,
|
||||
0x6c,0x70,0x48,0x50,0xfd,0xed,0xb9,0xda,
|
||||
0x5e,0x15,0x46,0x57,0xa7,0x8d,0x9d,0x84,
|
||||
0x90,0xd8,0xab,0x00,0x8c,0xbc,0xd3,0x0a,
|
||||
0xf7,0xe4,0x58,0x05,0xb8,0xb3,0x45,0x06,
|
||||
0xd0,0x2c,0x1e,0x8f,0xca,0x3f,0x0f,0x02,
|
||||
0xc1,0xaf,0xbd,0x03,0x01,0x13,0x8a,0x6b,
|
||||
0x3a,0x91,0x11,0x41,0x4f,0x67,0xdc,0xea,
|
||||
0x97,0xf2,0xcf,0xce,0xf0,0xb4,0xe6,0x73,
|
||||
0x96,0xac,0x74,0x22,0xe7,0xad,0x35,0x85,
|
||||
0xe2,0xf9,0x37,0xe8,0x1c,0x75,0xdf,0x6e,
|
||||
0x47,0xf1,0x1a,0x71,0x1d,0x29,0xc5,0x89,
|
||||
0x6f,0xb7,0x62,0x0e,0xaa,0x18,0xbe,0x1b,
|
||||
0xfc,0x56,0x3e,0x4b,0xc6,0xd2,0x79,0x20,
|
||||
0x9a,0xdb,0xc0,0xfe,0x78,0xcd,0x5a,0xf4,
|
||||
0x1f,0xdd,0xa8,0x33,0x88,0x07,0xc7,0x31,
|
||||
0xb1,0x12,0x10,0x59,0x27,0x80,0xec,0x5f,
|
||||
0x60,0x51,0x7f,0xa9,0x19,0xb5,0x4a,0x0d,
|
||||
0x2d,0xe5,0x7a,0x9f,0x93,0xc9,0x9c,0xef,
|
||||
0xa0,0xe0,0x3b,0x4d,0xae,0x2a,0xf5,0xb0,
|
||||
0xc8,0xeb,0xbb,0x3c,0x83,0x53,0x99,0x61,
|
||||
0x17,0x2b,0x04,0x7e,0xba,0x77,0xd6,0x26,
|
||||
0xe1,0x69,0x14,0x63,0x55,0x21,0x0c,0x7d)
|
||||
|
||||
#-------------------------------------
|
||||
""" For each block size (Nb), the ShiftRow operation shifts row i
|
||||
by the amount Ci. Note that row 0 is not shifted.
|
||||
Nb C1 C2 C3
|
||||
------------------- """
|
||||
shiftOffset = { 4 : ( 0, 1, 2, 3),
|
||||
5 : ( 0, 1, 2, 3),
|
||||
6 : ( 0, 1, 2, 3),
|
||||
7 : ( 0, 1, 2, 4),
|
||||
8 : ( 0, 1, 3, 4) }
|
||||
def ShiftRows(algInstance):
|
||||
tmp = [0]*algInstance.Nb # list of size Nb
|
||||
for r in range(1,4): # row 0 reamains unchanged and can be skipped
|
||||
for c in range(algInstance.Nb):
|
||||
tmp[c] = algInstance.state[(c+shiftOffset[algInstance.Nb][r]) % algInstance.Nb][r]
|
||||
for c in range(algInstance.Nb):
|
||||
algInstance.state[c][r] = tmp[c]
|
||||
def InvShiftRows(algInstance):
|
||||
tmp = [0]*algInstance.Nb # list of size Nb
|
||||
for r in range(1,4): # row 0 reamains unchanged and can be skipped
|
||||
for c in range(algInstance.Nb):
|
||||
tmp[c] = algInstance.state[(c+algInstance.Nb-shiftOffset[algInstance.Nb][r]) % algInstance.Nb][r]
|
||||
for c in range(algInstance.Nb):
|
||||
algInstance.state[c][r] = tmp[c]
|
||||
#-------------------------------------
|
||||
def MixColumns(a):
|
||||
Sprime = [0,0,0,0]
|
||||
for j in range(a.Nb): # for each column
|
||||
Sprime[0] = mul(2,a.state[j][0])^mul(3,a.state[j][1])^mul(1,a.state[j][2])^mul(1,a.state[j][3])
|
||||
Sprime[1] = mul(1,a.state[j][0])^mul(2,a.state[j][1])^mul(3,a.state[j][2])^mul(1,a.state[j][3])
|
||||
Sprime[2] = mul(1,a.state[j][0])^mul(1,a.state[j][1])^mul(2,a.state[j][2])^mul(3,a.state[j][3])
|
||||
Sprime[3] = mul(3,a.state[j][0])^mul(1,a.state[j][1])^mul(1,a.state[j][2])^mul(2,a.state[j][3])
|
||||
for i in range(4):
|
||||
a.state[j][i] = Sprime[i]
|
||||
|
||||
def InvMixColumns(a):
|
||||
""" Mix the four bytes of every column in a linear way
|
||||
This is the opposite operation of Mixcolumn """
|
||||
Sprime = [0,0,0,0]
|
||||
for j in range(a.Nb): # for each column
|
||||
Sprime[0] = mul(0x0E,a.state[j][0])^mul(0x0B,a.state[j][1])^mul(0x0D,a.state[j][2])^mul(0x09,a.state[j][3])
|
||||
Sprime[1] = mul(0x09,a.state[j][0])^mul(0x0E,a.state[j][1])^mul(0x0B,a.state[j][2])^mul(0x0D,a.state[j][3])
|
||||
Sprime[2] = mul(0x0D,a.state[j][0])^mul(0x09,a.state[j][1])^mul(0x0E,a.state[j][2])^mul(0x0B,a.state[j][3])
|
||||
Sprime[3] = mul(0x0B,a.state[j][0])^mul(0x0D,a.state[j][1])^mul(0x09,a.state[j][2])^mul(0x0E,a.state[j][3])
|
||||
for i in range(4):
|
||||
a.state[j][i] = Sprime[i]
|
||||
|
||||
#-------------------------------------
|
||||
def mul(a, b):
|
||||
""" Multiply two elements of GF(2^m)
|
||||
needed for MixColumn and InvMixColumn """
|
||||
if (a !=0 and b!=0):
|
||||
return Alogtable[(Logtable[a] + Logtable[b])%255]
|
||||
else:
|
||||
return 0
|
||||
|
||||
Logtable = ( 0, 0, 25, 1, 50, 2, 26, 198, 75, 199, 27, 104, 51, 238, 223, 3,
|
||||
100, 4, 224, 14, 52, 141, 129, 239, 76, 113, 8, 200, 248, 105, 28, 193,
|
||||
125, 194, 29, 181, 249, 185, 39, 106, 77, 228, 166, 114, 154, 201, 9, 120,
|
||||
101, 47, 138, 5, 33, 15, 225, 36, 18, 240, 130, 69, 53, 147, 218, 142,
|
||||
150, 143, 219, 189, 54, 208, 206, 148, 19, 92, 210, 241, 64, 70, 131, 56,
|
||||
102, 221, 253, 48, 191, 6, 139, 98, 179, 37, 226, 152, 34, 136, 145, 16,
|
||||
126, 110, 72, 195, 163, 182, 30, 66, 58, 107, 40, 84, 250, 133, 61, 186,
|
||||
43, 121, 10, 21, 155, 159, 94, 202, 78, 212, 172, 229, 243, 115, 167, 87,
|
||||
175, 88, 168, 80, 244, 234, 214, 116, 79, 174, 233, 213, 231, 230, 173, 232,
|
||||
44, 215, 117, 122, 235, 22, 11, 245, 89, 203, 95, 176, 156, 169, 81, 160,
|
||||
127, 12, 246, 111, 23, 196, 73, 236, 216, 67, 31, 45, 164, 118, 123, 183,
|
||||
204, 187, 62, 90, 251, 96, 177, 134, 59, 82, 161, 108, 170, 85, 41, 157,
|
||||
151, 178, 135, 144, 97, 190, 220, 252, 188, 149, 207, 205, 55, 63, 91, 209,
|
||||
83, 57, 132, 60, 65, 162, 109, 71, 20, 42, 158, 93, 86, 242, 211, 171,
|
||||
68, 17, 146, 217, 35, 32, 46, 137, 180, 124, 184, 38, 119, 153, 227, 165,
|
||||
103, 74, 237, 222, 197, 49, 254, 24, 13, 99, 140, 128, 192, 247, 112, 7)
|
||||
|
||||
Alogtable= ( 1, 3, 5, 15, 17, 51, 85, 255, 26, 46, 114, 150, 161, 248, 19, 53,
|
||||
95, 225, 56, 72, 216, 115, 149, 164, 247, 2, 6, 10, 30, 34, 102, 170,
|
||||
229, 52, 92, 228, 55, 89, 235, 38, 106, 190, 217, 112, 144, 171, 230, 49,
|
||||
83, 245, 4, 12, 20, 60, 68, 204, 79, 209, 104, 184, 211, 110, 178, 205,
|
||||
76, 212, 103, 169, 224, 59, 77, 215, 98, 166, 241, 8, 24, 40, 120, 136,
|
||||
131, 158, 185, 208, 107, 189, 220, 127, 129, 152, 179, 206, 73, 219, 118, 154,
|
||||
181, 196, 87, 249, 16, 48, 80, 240, 11, 29, 39, 105, 187, 214, 97, 163,
|
||||
254, 25, 43, 125, 135, 146, 173, 236, 47, 113, 147, 174, 233, 32, 96, 160,
|
||||
251, 22, 58, 78, 210, 109, 183, 194, 93, 231, 50, 86, 250, 21, 63, 65,
|
||||
195, 94, 226, 61, 71, 201, 64, 192, 91, 237, 44, 116, 156, 191, 218, 117,
|
||||
159, 186, 213, 100, 172, 239, 42, 126, 130, 157, 188, 223, 122, 142, 137, 128,
|
||||
155, 182, 193, 88, 232, 35, 101, 175, 234, 37, 111, 177, 200, 67, 197, 84,
|
||||
252, 31, 33, 99, 165, 244, 7, 9, 27, 45, 119, 153, 176, 203, 70, 202,
|
||||
69, 207, 74, 222, 121, 139, 134, 145, 168, 227, 62, 66, 198, 81, 243, 14,
|
||||
18, 54, 90, 238, 41, 123, 141, 140, 143, 138, 133, 148, 167, 242, 13, 23,
|
||||
57, 75, 221, 124, 132, 151, 162, 253, 28, 36, 108, 180, 199, 82, 246, 1)
|
||||
|
||||
|
||||
|
||||
|
||||
"""
|
||||
AES Encryption Algorithm
|
||||
The AES algorithm is just Rijndael algorithm restricted to the default
|
||||
blockSize of 128 bits.
|
||||
"""
|
||||
|
||||
class AES(Rijndael):
|
||||
""" The AES algorithm is the Rijndael block cipher restricted to block
|
||||
sizes of 128 bits and key sizes of 128, 192 or 256 bits
|
||||
"""
|
||||
def __init__(self, key = None, padding = padWithPadLen(), keySize=16):
|
||||
""" Initialize AES, keySize is in bytes """
|
||||
if not (keySize == 16 or keySize == 24 or keySize == 32) :
|
||||
raise BadKeySizeError('Illegal AES key size, must be 16, 24, or 32 bytes')
|
||||
|
||||
Rijndael.__init__( self, key, padding=padding, keySize=keySize, blockSize=16 )
|
||||
|
||||
self.name = 'AES'
|
||||
|
||||
|
||||
"""
|
||||
CBC mode of encryption for block ciphers.
|
||||
This algorithm mode wraps any BlockCipher to make a
|
||||
Cipher Block Chaining mode.
|
||||
"""
|
||||
from random import Random # should change to crypto.random!!!
|
||||
|
||||
|
||||
class CBC(BlockCipher):
|
||||
""" The CBC class wraps block ciphers to make cipher block chaining (CBC) mode
|
||||
algorithms. The initialization (IV) is automatic if set to None. Padding
|
||||
is also automatic based on the Pad class used to initialize the algorithm
|
||||
"""
|
||||
def __init__(self, blockCipherInstance, padding = padWithPadLen()):
|
||||
""" CBC algorithms are created by initializing with a BlockCipher instance """
|
||||
self.baseCipher = blockCipherInstance
|
||||
self.name = self.baseCipher.name + '_CBC'
|
||||
self.blockSize = self.baseCipher.blockSize
|
||||
self.keySize = self.baseCipher.keySize
|
||||
self.padding = padding
|
||||
self.baseCipher.padding = noPadding() # baseCipher should NOT pad!!
|
||||
self.r = Random() # for IV generation, currently uses
|
||||
# mediocre standard distro version <----------------
|
||||
import time
|
||||
newSeed = time.ctime()+str(self.r) # seed with instance location
|
||||
self.r.seed(newSeed) # to make unique
|
||||
self.reset()
|
||||
|
||||
def setKey(self, key):
|
||||
self.baseCipher.setKey(key)
|
||||
|
||||
# Overload to reset both CBC state and the wrapped baseCipher
|
||||
def resetEncrypt(self):
|
||||
BlockCipher.resetEncrypt(self) # reset CBC encrypt state (super class)
|
||||
self.baseCipher.resetEncrypt() # reset base cipher encrypt state
|
||||
|
||||
def resetDecrypt(self):
|
||||
BlockCipher.resetDecrypt(self) # reset CBC state (super class)
|
||||
self.baseCipher.resetDecrypt() # reset base cipher decrypt state
|
||||
|
||||
def encrypt(self, plainText, iv=None, more=None):
|
||||
""" CBC encryption - overloads baseCipher to allow optional explicit IV
|
||||
when iv=None, iv is auto generated!
|
||||
"""
|
||||
if self.encryptBlockCount == 0:
|
||||
self.iv = iv
|
||||
else:
|
||||
assert(iv==None), 'IV used only on first call to encrypt'
|
||||
|
||||
return BlockCipher.encrypt(self,plainText, more=more)
|
||||
|
||||
def decrypt(self, cipherText, iv=None, more=None):
|
||||
""" CBC decryption - overloads baseCipher to allow optional explicit IV
|
||||
when iv=None, iv is auto generated!
|
||||
"""
|
||||
if self.decryptBlockCount == 0:
|
||||
self.iv = iv
|
||||
else:
|
||||
assert(iv==None), 'IV used only on first call to decrypt'
|
||||
|
||||
return BlockCipher.decrypt(self, cipherText, more=more)
|
||||
|
||||
def encryptBlock(self, plainTextBlock):
|
||||
""" CBC block encryption, IV is set with 'encrypt' """
|
||||
auto_IV = ''
|
||||
if self.encryptBlockCount == 0:
|
||||
if self.iv == None:
|
||||
# generate IV and use
|
||||
self.iv = ''.join([chr(self.r.randrange(256)) for i in range(self.blockSize)])
|
||||
self.prior_encr_CT_block = self.iv
|
||||
auto_IV = self.prior_encr_CT_block # prepend IV if it's automatic
|
||||
else: # application provided IV
|
||||
assert(len(self.iv) == self.blockSize ),'IV must be same length as block'
|
||||
self.prior_encr_CT_block = self.iv
|
||||
""" encrypt the prior CT XORed with the PT """
|
||||
ct = self.baseCipher.encryptBlock( xor(self.prior_encr_CT_block, plainTextBlock) )
|
||||
self.prior_encr_CT_block = ct
|
||||
return auto_IV+ct
|
||||
|
||||
def decryptBlock(self, encryptedBlock):
|
||||
""" Decrypt a single block """
|
||||
|
||||
if self.decryptBlockCount == 0: # first call, process IV
|
||||
if self.iv == None: # auto decrypt IV?
|
||||
self.prior_CT_block = encryptedBlock
|
||||
return ''
|
||||
else:
|
||||
assert(len(self.iv)==self.blockSize),"Bad IV size on CBC decryption"
|
||||
self.prior_CT_block = self.iv
|
||||
|
||||
dct = self.baseCipher.decryptBlock(encryptedBlock)
|
||||
""" XOR the prior decrypted CT with the prior CT """
|
||||
dct_XOR_priorCT = xor( self.prior_CT_block, dct )
|
||||
|
||||
self.prior_CT_block = encryptedBlock
|
||||
|
||||
return dct_XOR_priorCT
|
||||
|
||||
|
||||
"""
|
||||
AES_CBC Encryption Algorithm
|
||||
"""
|
||||
|
||||
class AES_CBC(CBC):
|
||||
""" AES encryption in CBC feedback mode """
|
||||
def __init__(self, key=None, padding=padWithPadLen(), keySize=16):
|
||||
CBC.__init__( self, AES(key, noPadding(), keySize), padding)
|
||||
self.name = 'AES_CBC'
|
||||
BIN
DeDRM_plugin/alfcrypto.dll
Normal file
BIN
DeDRM_plugin/alfcrypto.dll
Normal file
Binary file not shown.
301
DeDRM_plugin/alfcrypto.py
Normal file
301
DeDRM_plugin/alfcrypto.py
Normal file
@@ -0,0 +1,301 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# crypto library mainly by some_updates
|
||||
|
||||
# pbkdf2.py pbkdf2 code taken from pbkdf2.py
|
||||
# pbkdf2.py Copyright © 2004 Matt Johnston <matt @ ucc asn au>
|
||||
# pbkdf2.py Copyright © 2009 Daniel Holth <dholth@fastmail.fm>
|
||||
# pbkdf2.py This code may be freely used and modified for any purpose.
|
||||
|
||||
import sys, os
|
||||
import hmac
|
||||
from struct import pack
|
||||
import hashlib
|
||||
|
||||
# interface to needed routines libalfcrypto
|
||||
def _load_libalfcrypto():
|
||||
import ctypes
|
||||
from ctypes import CDLL, byref, POINTER, c_void_p, c_char_p, c_int, c_long, \
|
||||
Structure, c_ulong, create_string_buffer, addressof, string_at, cast, sizeof
|
||||
|
||||
pointer_size = ctypes.sizeof(ctypes.c_voidp)
|
||||
name_of_lib = None
|
||||
if sys.platform.startswith('darwin'):
|
||||
name_of_lib = 'libalfcrypto.dylib'
|
||||
elif sys.platform.startswith('win'):
|
||||
if pointer_size == 4:
|
||||
name_of_lib = 'alfcrypto.dll'
|
||||
else:
|
||||
name_of_lib = 'alfcrypto64.dll'
|
||||
else:
|
||||
if pointer_size == 4:
|
||||
name_of_lib = 'libalfcrypto32.so'
|
||||
else:
|
||||
name_of_lib = 'libalfcrypto64.so'
|
||||
|
||||
# hard code to local location for libalfcrypto
|
||||
libalfcrypto = os.path.join(sys.path[0],name_of_lib)
|
||||
if not os.path.isfile(libalfcrypto):
|
||||
libalfcrypto = os.path.join(sys.path[0], 'lib', name_of_lib)
|
||||
if not os.path.isfile(libalfcrypto):
|
||||
libalfcrypto = os.path.join('.',name_of_lib)
|
||||
if not os.path.isfile(libalfcrypto):
|
||||
raise Exception('libalfcrypto not found at %s' % libalfcrypto)
|
||||
|
||||
libalfcrypto = CDLL(libalfcrypto)
|
||||
|
||||
c_char_pp = POINTER(c_char_p)
|
||||
c_int_p = POINTER(c_int)
|
||||
|
||||
|
||||
def F(restype, name, argtypes):
|
||||
func = getattr(libalfcrypto, name)
|
||||
func.restype = restype
|
||||
func.argtypes = argtypes
|
||||
return func
|
||||
|
||||
# aes cbc decryption
|
||||
#
|
||||
# struct aes_key_st {
|
||||
# unsigned long rd_key[4 *(AES_MAXNR + 1)];
|
||||
# int rounds;
|
||||
# };
|
||||
#
|
||||
# typedef struct aes_key_st AES_KEY;
|
||||
#
|
||||
# int AES_set_decrypt_key(const unsigned char *userKey, const int bits, AES_KEY *key);
|
||||
#
|
||||
#
|
||||
# void AES_cbc_encrypt(const unsigned char *in, unsigned char *out,
|
||||
# const unsigned long length, const AES_KEY *key,
|
||||
# unsigned char *ivec, const int enc);
|
||||
|
||||
AES_MAXNR = 14
|
||||
|
||||
class AES_KEY(Structure):
|
||||
_fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))), ('rounds', c_int)]
|
||||
|
||||
AES_KEY_p = POINTER(AES_KEY)
|
||||
AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',[c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p, c_int])
|
||||
AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key',[c_char_p, c_int, AES_KEY_p])
|
||||
|
||||
|
||||
|
||||
# Pukall 1 Cipher
|
||||
# unsigned char *PC1(const unsigned char *key, unsigned int klen, const unsigned char *src,
|
||||
# unsigned char *dest, unsigned int len, int decryption);
|
||||
|
||||
PC1 = F(c_char_p, 'PC1', [c_char_p, c_ulong, c_char_p, c_char_p, c_ulong, c_ulong])
|
||||
|
||||
# Topaz Encryption
|
||||
# typedef struct _TpzCtx {
|
||||
# unsigned int v[2];
|
||||
# } TpzCtx;
|
||||
#
|
||||
# void topazCryptoInit(TpzCtx *ctx, const unsigned char *key, int klen);
|
||||
# void topazCryptoDecrypt(const TpzCtx *ctx, const unsigned char *in, unsigned char *out, int len);
|
||||
|
||||
class TPZ_CTX(Structure):
|
||||
_fields_ = [('v', c_long * 2)]
|
||||
|
||||
TPZ_CTX_p = POINTER(TPZ_CTX)
|
||||
topazCryptoInit = F(None, 'topazCryptoInit', [TPZ_CTX_p, c_char_p, c_ulong])
|
||||
topazCryptoDecrypt = F(None, 'topazCryptoDecrypt', [TPZ_CTX_p, c_char_p, c_char_p, c_ulong])
|
||||
|
||||
|
||||
class AES_CBC(object):
|
||||
def __init__(self):
|
||||
self._blocksize = 0
|
||||
self._keyctx = None
|
||||
self._iv = 0
|
||||
|
||||
def set_decrypt_key(self, userkey, iv):
|
||||
self._blocksize = len(userkey)
|
||||
if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) :
|
||||
raise Exception('AES CBC improper key used')
|
||||
return
|
||||
keyctx = self._keyctx = AES_KEY()
|
||||
self._iv = iv
|
||||
rv = AES_set_decrypt_key(userkey, len(userkey) * 8, keyctx)
|
||||
if rv < 0:
|
||||
raise Exception('Failed to initialize AES CBC key')
|
||||
|
||||
def decrypt(self, data):
|
||||
out = create_string_buffer(len(data))
|
||||
mutable_iv = create_string_buffer(self._iv, len(self._iv))
|
||||
rv = AES_cbc_encrypt(data, out, len(data), self._keyctx, mutable_iv, 0)
|
||||
if rv == 0:
|
||||
raise Exception('AES CBC decryption failed')
|
||||
return out.raw
|
||||
|
||||
class Pukall_Cipher(object):
|
||||
def __init__(self):
|
||||
self.key = None
|
||||
|
||||
def PC1(self, key, src, decryption=True):
|
||||
self.key = key
|
||||
out = create_string_buffer(len(src))
|
||||
de = 0
|
||||
if decryption:
|
||||
de = 1
|
||||
rv = PC1(key, len(key), src, out, len(src), de)
|
||||
return out.raw
|
||||
|
||||
class Topaz_Cipher(object):
|
||||
def __init__(self):
|
||||
self._ctx = None
|
||||
|
||||
def ctx_init(self, key):
|
||||
tpz_ctx = self._ctx = TPZ_CTX()
|
||||
topazCryptoInit(tpz_ctx, key, len(key))
|
||||
return tpz_ctx
|
||||
|
||||
def decrypt(self, data, ctx=None):
|
||||
if ctx == None:
|
||||
ctx = self._ctx
|
||||
out = create_string_buffer(len(data))
|
||||
topazCryptoDecrypt(ctx, data, out, len(data))
|
||||
return out.raw
|
||||
|
||||
print("Using Library AlfCrypto DLL/DYLIB/SO")
|
||||
return (AES_CBC, Pukall_Cipher, Topaz_Cipher)
|
||||
|
||||
|
||||
def _load_python_alfcrypto():
|
||||
|
||||
import aescbc
|
||||
|
||||
class Pukall_Cipher(object):
|
||||
def __init__(self):
|
||||
self.key = None
|
||||
|
||||
def PC1(self, key, src, decryption=True):
|
||||
sum1 = 0;
|
||||
sum2 = 0;
|
||||
keyXorVal = 0;
|
||||
if len(key)!=16:
|
||||
raise Exception('Pukall_Cipher: Bad key length.')
|
||||
wkey = []
|
||||
for i in range(8):
|
||||
wkey.append(ord(key[i*2])<<8 | ord(key[i*2+1]))
|
||||
dst = ""
|
||||
for i in range(len(src)):
|
||||
temp1 = 0;
|
||||
byteXorVal = 0;
|
||||
for j in range(8):
|
||||
temp1 ^= wkey[j]
|
||||
sum2 = (sum2+j)*20021 + sum1
|
||||
sum1 = (temp1*346)&0xFFFF
|
||||
sum2 = (sum2+sum1)&0xFFFF
|
||||
temp1 = (temp1*20021+1)&0xFFFF
|
||||
byteXorVal ^= temp1 ^ sum2
|
||||
curByte = ord(src[i])
|
||||
if not decryption:
|
||||
keyXorVal = curByte * 257;
|
||||
curByte = ((curByte ^ (byteXorVal >> 8)) ^ byteXorVal) & 0xFF
|
||||
if decryption:
|
||||
keyXorVal = curByte * 257;
|
||||
for j in range(8):
|
||||
wkey[j] ^= keyXorVal;
|
||||
dst+=chr(curByte)
|
||||
return dst
|
||||
|
||||
class Topaz_Cipher(object):
|
||||
def __init__(self):
|
||||
self._ctx = None
|
||||
|
||||
def ctx_init(self, key):
|
||||
ctx1 = 0x0CAFFE19E
|
||||
for keyChar in key:
|
||||
keyByte = ord(keyChar)
|
||||
ctx2 = ctx1
|
||||
ctx1 = ((((ctx1 >>2) * (ctx1 >>7))&0xFFFFFFFF) ^ (keyByte * keyByte * 0x0F902007)& 0xFFFFFFFF )
|
||||
self._ctx = [ctx1, ctx2]
|
||||
return [ctx1,ctx2]
|
||||
|
||||
def decrypt(self, data, ctx=None):
|
||||
if ctx == None:
|
||||
ctx = self._ctx
|
||||
ctx1 = ctx[0]
|
||||
ctx2 = ctx[1]
|
||||
plainText = ""
|
||||
for dataChar in data:
|
||||
dataByte = ord(dataChar)
|
||||
m = (dataByte ^ ((ctx1 >> 3) &0xFF) ^ ((ctx2<<3) & 0xFF)) &0xFF
|
||||
ctx2 = ctx1
|
||||
ctx1 = (((ctx1 >> 2) * (ctx1 >> 7)) &0xFFFFFFFF) ^((m * m * 0x0F902007) &0xFFFFFFFF)
|
||||
plainText += chr(m)
|
||||
return plainText
|
||||
|
||||
class AES_CBC(object):
|
||||
def __init__(self):
|
||||
self._key = None
|
||||
self._iv = None
|
||||
self.aes = None
|
||||
|
||||
def set_decrypt_key(self, userkey, iv):
|
||||
self._key = userkey
|
||||
self._iv = iv
|
||||
self.aes = aescbc.AES_CBC(userkey, aescbc.noPadding(), len(userkey))
|
||||
|
||||
def decrypt(self, data):
|
||||
iv = self._iv
|
||||
cleartext = self.aes.decrypt(iv + data)
|
||||
return cleartext
|
||||
|
||||
print("Using Library AlfCrypto Python")
|
||||
return (AES_CBC, Pukall_Cipher, Topaz_Cipher)
|
||||
|
||||
|
||||
def _load_crypto():
|
||||
AES_CBC = Pukall_Cipher = Topaz_Cipher = None
|
||||
cryptolist = (_load_libalfcrypto, _load_python_alfcrypto)
|
||||
for loader in cryptolist:
|
||||
try:
|
||||
AES_CBC, Pukall_Cipher, Topaz_Cipher = loader()
|
||||
break
|
||||
except (ImportError, Exception):
|
||||
pass
|
||||
return AES_CBC, Pukall_Cipher, Topaz_Cipher
|
||||
|
||||
AES_CBC, Pukall_Cipher, Topaz_Cipher = _load_crypto()
|
||||
|
||||
|
||||
class KeyIVGen(object):
|
||||
# this only exists in openssl so we will use pure python implementation instead
|
||||
# PKCS5_PBKDF2_HMAC_SHA1 = F(c_int, 'PKCS5_PBKDF2_HMAC_SHA1',
|
||||
# [c_char_p, c_ulong, c_char_p, c_ulong, c_ulong, c_ulong, c_char_p])
|
||||
def pbkdf2(self, passwd, salt, iter, keylen):
|
||||
|
||||
def xorbytes( a, b ):
|
||||
if len(a) != len(b):
|
||||
raise Exception("xorbytes(): lengths differ")
|
||||
return bytes([x ^ y for x, y in zip(a, b)])
|
||||
|
||||
def prf( h, data ):
|
||||
hm = h.copy()
|
||||
hm.update( data )
|
||||
return hm.digest()
|
||||
|
||||
def pbkdf2_F( h, salt, itercount, blocknum ):
|
||||
U = prf( h, salt + pack('>i',blocknum ) )
|
||||
T = U
|
||||
for i in range(2, itercount+1):
|
||||
U = prf( h, U )
|
||||
T = xorbytes( T, U )
|
||||
return T
|
||||
|
||||
sha = hashlib.sha1
|
||||
digest_size = sha().digest_size
|
||||
# l - number of output blocks to produce
|
||||
l = keylen // digest_size
|
||||
if keylen % digest_size != 0:
|
||||
l += 1
|
||||
h = hmac.new( passwd, None, sha )
|
||||
T = b""
|
||||
for i in range(1, l+1):
|
||||
T += pbkdf2_F( h, salt, iter, i )
|
||||
return T[0: keylen]
|
||||
|
||||
|
||||
BIN
DeDRM_plugin/alfcrypto64.dll
Normal file
BIN
DeDRM_plugin/alfcrypto64.dll
Normal file
Binary file not shown.
BIN
DeDRM_plugin/alfcrypto_src.zip
Normal file
BIN
DeDRM_plugin/alfcrypto_src.zip
Normal file
Binary file not shown.
466
DeDRM_plugin/androidkindlekey.py
Executable file
466
DeDRM_plugin/androidkindlekey.py
Executable file
@@ -0,0 +1,466 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# androidkindlekey.py
|
||||
# Copyright © 2010-20 by Thom, Apprentice Harper et al.
|
||||
|
||||
# Revision history:
|
||||
# 1.0 - AmazonSecureStorage.xml decryption to serial number
|
||||
# 1.1 - map_data_storage.db decryption to serial number
|
||||
# 1.2 - Changed to be callable from AppleScript by returning only serial number
|
||||
# - and changed name to androidkindlekey.py
|
||||
# - and added in unicode command line support
|
||||
# 1.3 - added in TkInter interface, output to a file
|
||||
# 1.4 - Fix some problems identified by Aldo Bleeker
|
||||
# 1.5 - Fix another problem identified by Aldo Bleeker
|
||||
# 2.0 - Python 3 compatibility
|
||||
|
||||
"""
|
||||
Retrieve Kindle for Android Serial Number.
|
||||
"""
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = '2.0'
|
||||
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
import getopt
|
||||
import tempfile
|
||||
import zlib
|
||||
import tarfile
|
||||
from hashlib import md5
|
||||
from io import BytesIO
|
||||
from binascii import a2b_hex, b2a_hex
|
||||
|
||||
# Routines common to Mac and PC
|
||||
|
||||
# Wrap a stream so that output gets flushed immediately
|
||||
# and also make sure that any unicode strings get
|
||||
# encoded using "replace" before writing them.
|
||||
class SafeUnbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
self.encoding = stream.encoding
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
def write(self, data):
|
||||
if isinstance(data,str):
|
||||
data = data.encode(self.encoding,"replace")
|
||||
self.stream.buffer.write(data)
|
||||
self.stream.buffer.flush()
|
||||
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
try:
|
||||
from calibre.constants import iswindows, isosx
|
||||
except:
|
||||
iswindows = sys.platform.startswith('win')
|
||||
isosx = sys.platform.startswith('darwin')
|
||||
|
||||
def unicode_argv():
|
||||
if iswindows:
|
||||
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
||||
# strings.
|
||||
|
||||
# Versions 2.x of Python don't support Unicode in sys.argv on
|
||||
# Windows, with the underlying Windows API instead replacing multi-byte
|
||||
# characters with '?'. So use shell32.GetCommandLineArgvW to get sys.argv
|
||||
# as a list of Unicode strings and encode them as utf-8
|
||||
|
||||
from ctypes import POINTER, byref, cdll, c_int, windll
|
||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
||||
|
||||
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
||||
GetCommandLineW.argtypes = []
|
||||
GetCommandLineW.restype = LPCWSTR
|
||||
|
||||
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
||||
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
||||
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
||||
|
||||
cmd = GetCommandLineW()
|
||||
argc = c_int(0)
|
||||
argv = CommandLineToArgvW(cmd, byref(argc))
|
||||
if argc.value > 0:
|
||||
# Remove Python executable and commands if present
|
||||
start = argc.value - len(sys.argv)
|
||||
return [argv[i] for i in
|
||||
range(start, argc.value)]
|
||||
# if we don't have any arguments at all, just pass back script name
|
||||
# this should never happen
|
||||
return ["kindlekey.py"]
|
||||
else:
|
||||
argvencoding = sys.stdin.encoding or "utf-8"
|
||||
return [arg if isinstance(arg, str) else str(arg, argvencoding) for arg in sys.argv]
|
||||
|
||||
class DrmException(Exception):
|
||||
pass
|
||||
|
||||
STORAGE = "backup.ab"
|
||||
STORAGE1 = "AmazonSecureStorage.xml"
|
||||
STORAGE2 = "map_data_storage.db"
|
||||
|
||||
class AndroidObfuscation(object):
|
||||
'''AndroidObfuscation
|
||||
For the key, it's written in java, and run in android dalvikvm
|
||||
'''
|
||||
|
||||
key = a2b_hex('0176e04c9408b1702d90be333fd53523')
|
||||
|
||||
def encrypt(self, plaintext):
|
||||
cipher = self._get_cipher()
|
||||
padding = len(self.key) - len(plaintext) % len(self.key)
|
||||
plaintext += chr(padding) * padding
|
||||
return b2a_hex(cipher.encrypt(plaintext.encode('utf-8')))
|
||||
|
||||
def decrypt(self, ciphertext):
|
||||
cipher = self._get_cipher()
|
||||
plaintext = cipher.decrypt(a2b_hex(ciphertext))
|
||||
return plaintext[:-ord(plaintext[-1])]
|
||||
|
||||
def _get_cipher(self):
|
||||
try:
|
||||
from Crypto.Cipher import AES
|
||||
return AES.new(self.key)
|
||||
except ImportError:
|
||||
from aescbc import AES, noPadding
|
||||
return AES(self.key, padding=noPadding())
|
||||
|
||||
class AndroidObfuscationV2(AndroidObfuscation):
|
||||
'''AndroidObfuscationV2
|
||||
'''
|
||||
|
||||
count = 503
|
||||
password = b'Thomsun was here!'
|
||||
|
||||
def __init__(self, salt):
|
||||
key = self.password + salt
|
||||
for _ in range(self.count):
|
||||
key = md5(key).digest()
|
||||
self.key = key[:8]
|
||||
self.iv = key[8:16]
|
||||
|
||||
def _get_cipher(self):
|
||||
try :
|
||||
from Crypto.Cipher import DES
|
||||
return DES.new(self.key, DES.MODE_CBC, self.iv)
|
||||
except ImportError:
|
||||
from python_des import Des, CBC
|
||||
return Des(self.key, CBC, self.iv)
|
||||
|
||||
def parse_preference(path):
|
||||
''' parse android's shared preference xml '''
|
||||
storage = {}
|
||||
read = open(path)
|
||||
for line in read:
|
||||
line = line.strip()
|
||||
# <string name="key">value</string>
|
||||
if line.startswith('<string name="'):
|
||||
index = line.find('"', 14)
|
||||
key = line[14:index]
|
||||
value = line[index+2:-9]
|
||||
storage[key] = value
|
||||
read.close()
|
||||
return storage
|
||||
|
||||
def get_serials1(path=STORAGE1):
|
||||
''' get serials from android's shared preference xml '''
|
||||
|
||||
if not os.path.isfile(path):
|
||||
return []
|
||||
|
||||
storage = parse_preference(path)
|
||||
salt = storage.get('AmazonSaltKey')
|
||||
if salt and len(salt) == 16:
|
||||
obfuscation = AndroidObfuscationV2(a2b_hex(salt))
|
||||
else:
|
||||
obfuscation = AndroidObfuscation()
|
||||
|
||||
def get_value(key):
|
||||
encrypted_key = obfuscation.encrypt(key)
|
||||
encrypted_value = storage.get(encrypted_key)
|
||||
if encrypted_value:
|
||||
return obfuscation.decrypt(encrypted_value)
|
||||
return ''
|
||||
|
||||
# also see getK4Pids in kgenpids.py
|
||||
try:
|
||||
dsnid = get_value('DsnId')
|
||||
except:
|
||||
sys.stderr.write('cannot get DsnId\n')
|
||||
return []
|
||||
|
||||
try:
|
||||
tokens = set(get_value('kindle.account.tokens').split(','))
|
||||
except:
|
||||
sys.stderr.write('cannot get kindle account tokens\n')
|
||||
return []
|
||||
|
||||
serials = []
|
||||
if dsnid:
|
||||
serials.append(dsnid)
|
||||
for token in tokens:
|
||||
if token:
|
||||
serials.append('%s%s' % (dsnid, token))
|
||||
serials.append(token)
|
||||
return serials
|
||||
|
||||
def get_serials2(path=STORAGE2):
|
||||
''' get serials from android's sql database '''
|
||||
if not os.path.isfile(path):
|
||||
return []
|
||||
|
||||
import sqlite3
|
||||
connection = sqlite3.connect(path)
|
||||
cursor = connection.cursor()
|
||||
cursor.execute('''select device_data_value from device_data where device_data_key like '%serial.number%' ''')
|
||||
device_data_keys = cursor.fetchall()
|
||||
dsns = []
|
||||
for device_data_row in device_data_keys:
|
||||
try:
|
||||
if device_data_row and device_data_row[0]:
|
||||
if len(device_data_row[0]) > 0:
|
||||
dsns.append(device_data_row[0])
|
||||
except:
|
||||
print("Error getting one of the device serial name keys")
|
||||
traceback.print_exc()
|
||||
pass
|
||||
dsns = list(set(dsns))
|
||||
|
||||
cursor.execute('''select userdata_value from userdata where userdata_key like '%/%kindle.account.tokens%' ''')
|
||||
userdata_keys = cursor.fetchall()
|
||||
tokens = []
|
||||
for userdata_row in userdata_keys:
|
||||
try:
|
||||
if userdata_row and userdata_row[0]:
|
||||
if len(userdata_row[0]) > 0:
|
||||
if ',' in userdata_row[0]:
|
||||
splits = userdata_row[0].split(',')
|
||||
for split in splits:
|
||||
tokens.append(split)
|
||||
tokens.append(userdata_row[0])
|
||||
except:
|
||||
print("Error getting one of the account token keys")
|
||||
traceback.print_exc()
|
||||
pass
|
||||
tokens = list(set(tokens))
|
||||
|
||||
serials = []
|
||||
for x in dsns:
|
||||
serials.append(x)
|
||||
for y in tokens:
|
||||
serials.append(y)
|
||||
serials.append(x+y)
|
||||
return serials
|
||||
|
||||
def get_serials(path=STORAGE):
|
||||
'''get serials from files in from android backup.ab
|
||||
backup.ab can be get using adb command:
|
||||
shell> adb backup com.amazon.kindle
|
||||
or from individual files if they're passed.
|
||||
'''
|
||||
if not os.path.isfile(path):
|
||||
return []
|
||||
|
||||
basename = os.path.basename(path)
|
||||
if basename == STORAGE1:
|
||||
return get_serials1(path)
|
||||
elif basename == STORAGE2:
|
||||
return get_serials2(path)
|
||||
|
||||
output = None
|
||||
try :
|
||||
read = open(path, 'rb')
|
||||
head = read.read(24)
|
||||
if head[:14] == b'ANDROID BACKUP':
|
||||
output = BytesIO(zlib.decompress(read.read()))
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
read.close()
|
||||
|
||||
if not output:
|
||||
return []
|
||||
|
||||
serials = []
|
||||
tar = tarfile.open(fileobj=output)
|
||||
for member in tar.getmembers():
|
||||
if member.name.strip().endswith(STORAGE1):
|
||||
write = tempfile.NamedTemporaryFile(mode='wb', delete=False)
|
||||
write.write(tar.extractfile(member).read())
|
||||
write.close()
|
||||
write_path = os.path.abspath(write.name)
|
||||
serials.extend(get_serials1(write_path))
|
||||
os.remove(write_path)
|
||||
elif member.name.strip().endswith(STORAGE2):
|
||||
write = tempfile.NamedTemporaryFile(mode='wb', delete=False)
|
||||
write.write(tar.extractfile(member).read())
|
||||
write.close()
|
||||
write_path = os.path.abspath(write.name)
|
||||
serials.extend(get_serials2(write_path))
|
||||
os.remove(write_path)
|
||||
return list(set(serials))
|
||||
|
||||
__all__ = [ 'get_serials', 'getkey']
|
||||
|
||||
# procedure for CLI and GUI interfaces
|
||||
# returns single or multiple keys (one per line) in the specified file
|
||||
def getkey(outfile, inpath):
|
||||
keys = get_serials(inpath)
|
||||
if len(keys) > 0:
|
||||
with open(outfile, 'w') as keyfileout:
|
||||
for key in keys:
|
||||
keyfileout.write(key)
|
||||
keyfileout.write("\n")
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def usage(progname):
|
||||
print("Decrypts the serial number(s) of Kindle For Android from Android backup or file")
|
||||
print("Get backup.ab file using adb backup com.amazon.kindle for Android 4.0+.")
|
||||
print("Otherwise extract AmazonSecureStorage.xml from /data/data/com.amazon.kindle/shared_prefs/AmazonSecureStorage.xml")
|
||||
print("Or map_data_storage.db from /data/data/com.amazon.kindle/databases/map_data_storage.db")
|
||||
print("")
|
||||
print("Usage:")
|
||||
print(" {0:s} [-h] [-b <backup.ab>] [<outfile.k4a>]".format(progname))
|
||||
|
||||
|
||||
def cli_main():
|
||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
argv=unicode_argv()
|
||||
progname = os.path.basename(argv[0])
|
||||
print("{0} v{1}\nCopyright © 2010-2020 Thom, Apprentice Harper et al.".format(progname,__version__))
|
||||
|
||||
try:
|
||||
opts, args = getopt.getopt(argv[1:], "hb:")
|
||||
except getopt.GetoptError as err:
|
||||
usage(progname)
|
||||
print("\nError in options or arguments: {0}".format(err.args[0]))
|
||||
return 2
|
||||
|
||||
inpath = ""
|
||||
for o, a in opts:
|
||||
if o == "-h":
|
||||
usage(progname)
|
||||
return 0
|
||||
if o == "-b":
|
||||
inpath = a
|
||||
|
||||
if len(args) > 1:
|
||||
usage(progname)
|
||||
return 2
|
||||
|
||||
if len(args) == 1:
|
||||
# save to the specified file or directory
|
||||
outfile = args[0]
|
||||
if not os.path.isabs(outfile):
|
||||
outfile = os.path.join(os.path.dirname(argv[0]),outfile)
|
||||
outfile = os.path.abspath(outfile)
|
||||
if os.path.isdir(outfile):
|
||||
outfile = os.path.join(os.path.dirname(argv[0]),"androidkindlekey.k4a")
|
||||
else:
|
||||
# save to the same directory as the script
|
||||
outfile = os.path.join(os.path.dirname(argv[0]),"androidkindlekey.k4a")
|
||||
|
||||
# make sure the outpath is OK
|
||||
outfile = os.path.realpath(os.path.normpath(outfile))
|
||||
|
||||
if not os.path.isfile(inpath):
|
||||
usage(progname)
|
||||
print("\n{0:s} file not found".format(inpath))
|
||||
return 2
|
||||
|
||||
if getkey(outfile, inpath):
|
||||
print("\nSaved Kindle for Android key to {0}".format(outfile))
|
||||
else:
|
||||
print("\nCould not retrieve Kindle for Android key.")
|
||||
return 0
|
||||
|
||||
|
||||
def gui_main():
|
||||
try:
|
||||
import tkinter
|
||||
import tkinter.constants
|
||||
import tkinter.messagebox
|
||||
import tkinter.filedialog
|
||||
except:
|
||||
print("tkinter not installed")
|
||||
return 0
|
||||
|
||||
class DecryptionDialog(tkinter.Frame):
|
||||
def __init__(self, root):
|
||||
tkinter.Frame.__init__(self, root, border=5)
|
||||
self.status = tkinter.Label(self, text="Select backup.ab file")
|
||||
self.status.pack(fill=tkinter.constants.X, expand=1)
|
||||
body = tkinter.Frame(self)
|
||||
body.pack(fill=tkinter.constants.X, expand=1)
|
||||
sticky = tkinter.constants.E + tkinter.constants.W
|
||||
body.grid_columnconfigure(1, weight=2)
|
||||
tkinter.Label(body, text="Backup file").grid(row=0, column=0)
|
||||
self.keypath = tkinter.Entry(body, width=40)
|
||||
self.keypath.grid(row=0, column=1, sticky=sticky)
|
||||
self.keypath.insert(2, "backup.ab")
|
||||
button = tkinter.Button(body, text="...", command=self.get_keypath)
|
||||
button.grid(row=0, column=2)
|
||||
buttons = tkinter.Frame(self)
|
||||
buttons.pack()
|
||||
button2 = tkinter.Button(
|
||||
buttons, text="Extract", width=10, command=self.generate)
|
||||
button2.pack(side=tkinter.constants.LEFT)
|
||||
tkinter.Frame(buttons, width=10).pack(side=tkinter.constants.LEFT)
|
||||
button3 = tkinter.Button(
|
||||
buttons, text="Quit", width=10, command=self.quit)
|
||||
button3.pack(side=tkinter.constants.RIGHT)
|
||||
|
||||
def get_keypath(self):
|
||||
keypath = tkinter.filedialog.askopenfilename(
|
||||
parent=None, title="Select backup.ab file",
|
||||
defaultextension=".ab",
|
||||
filetypes=[('adb backup com.amazon.kindle', '.ab'),
|
||||
('All Files', '.*')])
|
||||
if keypath:
|
||||
keypath = os.path.normpath(keypath)
|
||||
self.keypath.delete(0, tkinter.constants.END)
|
||||
self.keypath.insert(0, keypath)
|
||||
return
|
||||
|
||||
def generate(self):
|
||||
inpath = self.keypath.get()
|
||||
self.status['text'] = "Getting key..."
|
||||
try:
|
||||
keys = get_serials(inpath)
|
||||
keycount = 0
|
||||
for key in keys:
|
||||
while True:
|
||||
keycount += 1
|
||||
outfile = os.path.join(progpath,"kindlekey{0:d}.k4a".format(keycount))
|
||||
if not os.path.exists(outfile):
|
||||
break
|
||||
|
||||
with open(outfile, 'w') as keyfileout:
|
||||
keyfileout.write(key)
|
||||
success = True
|
||||
tkinter.messagebox.showinfo(progname, "Key successfully retrieved to {0}".format(outfile))
|
||||
except Exception as e:
|
||||
self.status['text'] = "Error: {0}".format(e.args[0])
|
||||
return
|
||||
self.status['text'] = "Select backup.ab file"
|
||||
|
||||
argv=unicode_argv()
|
||||
progpath, progname = os.path.split(argv[0])
|
||||
root = tkinter.Tk()
|
||||
root.title("Kindle for Android Key Extraction v.{0}".format(__version__))
|
||||
root.resizable(True, False)
|
||||
root.minsize(300, 0)
|
||||
DecryptionDialog(root).pack(fill=tkinter.constants.X, expand=1)
|
||||
root.mainloop()
|
||||
return 0
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) > 1:
|
||||
sys.exit(cli_main())
|
||||
sys.exit(gui_main())
|
||||
87
DeDRM_plugin/argv_utils.py
Normal file
87
DeDRM_plugin/argv_utils.py
Normal file
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import sys, os
|
||||
import locale
|
||||
import codecs
|
||||
import importlib
|
||||
|
||||
# get sys.argv arguments and encode them into utf-8
|
||||
def unicode_argv():
|
||||
if sys.platform.startswith('win'):
|
||||
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
||||
# strings.
|
||||
|
||||
# Versions 2.x of Python don't support Unicode in sys.argv on
|
||||
# Windows, with the underlying Windows API instead replacing multi-byte
|
||||
# characters with '?'.
|
||||
|
||||
|
||||
from ctypes import POINTER, byref, cdll, c_int, windll
|
||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
||||
|
||||
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
||||
GetCommandLineW.argtypes = []
|
||||
GetCommandLineW.restype = LPCWSTR
|
||||
|
||||
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
||||
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
||||
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
||||
|
||||
cmd = GetCommandLineW()
|
||||
argc = c_int(0)
|
||||
argv = CommandLineToArgvW(cmd, byref(argc))
|
||||
if argc.value > 0:
|
||||
# Remove Python executable and commands if present
|
||||
start = argc.value - len(sys.argv)
|
||||
return [argv[i] for i in
|
||||
range(start, argc.value)]
|
||||
# if we don't have any arguments at all, just pass back script name
|
||||
# this should never happen
|
||||
return ["DeDRM.py"]
|
||||
else:
|
||||
argvencoding = sys.stdin.encoding or "utf-8"
|
||||
return [arg if isinstance(arg, str) else str(arg, argvencoding) for arg in sys.argv]
|
||||
|
||||
|
||||
def add_cp65001_codec():
|
||||
try:
|
||||
codecs.lookup('cp65001')
|
||||
except LookupError:
|
||||
codecs.register(
|
||||
lambda name: name == 'cp65001' and codecs.lookup('utf-8') or None)
|
||||
return
|
||||
|
||||
|
||||
def set_utf8_default_encoding():
|
||||
if sys.getdefaultencoding() == 'utf-8':
|
||||
return
|
||||
|
||||
# Regenerate setdefaultencoding.
|
||||
importlib.reload(sys)
|
||||
sys.setdefaultencoding('utf-8')
|
||||
|
||||
for attr in dir(locale):
|
||||
if attr[0:3] != 'LC_':
|
||||
continue
|
||||
aref = getattr(locale, attr)
|
||||
try:
|
||||
locale.setlocale(aref, '')
|
||||
except locale.Error:
|
||||
continue
|
||||
try:
|
||||
lang = locale.getlocale(aref)[0]
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
if lang:
|
||||
try:
|
||||
locale.setlocale(aref, (lang, 'UTF-8'))
|
||||
except locale.Error:
|
||||
os.environ[attr] = lang + '.UTF-8'
|
||||
try:
|
||||
locale.setlocale(locale.LC_ALL, '')
|
||||
except locale.Error:
|
||||
pass
|
||||
return
|
||||
|
||||
|
||||
213
DeDRM_plugin/askfolder_ed.py
Normal file
213
DeDRM_plugin/askfolder_ed.py
Normal file
@@ -0,0 +1,213 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
|
||||
|
||||
# to work around tk_chooseDirectory not properly returning unicode paths on Windows
|
||||
# need to use a dialog that can be hacked up to actually return full unicode paths
|
||||
# originally based on AskFolder from EasyDialogs for Windows but modified to fix it
|
||||
# to actually use unicode for path
|
||||
|
||||
# The original license for EasyDialogs is as follows
|
||||
#
|
||||
# Copyright (c) 2003-2005 Jimmy Retzlaff
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a
|
||||
# copy of this software and associated documentation files (the "Software"),
|
||||
# to deal in the Software without restriction, including without limitation
|
||||
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
# 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:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
# DEALINGS IN THE SOFTWARE.
|
||||
|
||||
# Adjusted for Python 3, September 2020
|
||||
|
||||
"""
|
||||
AskFolder(...) -- Ask the user to select a folder Windows specific
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
import ctypes
|
||||
from ctypes import POINTER, byref, cdll, c_int, windll
|
||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
||||
import ctypes.wintypes as wintypes
|
||||
|
||||
|
||||
__all__ = ['AskFolder']
|
||||
|
||||
# Load required Windows DLLs
|
||||
ole32 = ctypes.windll.ole32
|
||||
shell32 = ctypes.windll.shell32
|
||||
user32 = ctypes.windll.user32
|
||||
|
||||
|
||||
# Windows Constants
|
||||
BFFM_INITIALIZED = 1
|
||||
BFFM_SETOKTEXT = 1129
|
||||
BFFM_SETSELECTIONA = 1126
|
||||
BFFM_SETSELECTIONW = 1127
|
||||
BIF_EDITBOX = 16
|
||||
BS_DEFPUSHBUTTON = 1
|
||||
CB_ADDSTRING = 323
|
||||
CB_GETCURSEL = 327
|
||||
CB_SETCURSEL = 334
|
||||
CDM_SETCONTROLTEXT = 1128
|
||||
EM_GETLINECOUNT = 186
|
||||
EM_GETMARGINS = 212
|
||||
EM_POSFROMCHAR = 214
|
||||
EM_SETSEL = 177
|
||||
GWL_STYLE = -16
|
||||
IDC_STATIC = -1
|
||||
IDCANCEL = 2
|
||||
IDNO = 7
|
||||
IDOK = 1
|
||||
IDYES = 6
|
||||
MAX_PATH = 260
|
||||
OFN_ALLOWMULTISELECT = 512
|
||||
OFN_ENABLEHOOK = 32
|
||||
OFN_ENABLESIZING = 8388608
|
||||
OFN_ENABLETEMPLATEHANDLE = 128
|
||||
OFN_EXPLORER = 524288
|
||||
OFN_OVERWRITEPROMPT = 2
|
||||
OPENFILENAME_SIZE_VERSION_400 = 76
|
||||
PBM_GETPOS = 1032
|
||||
PBM_SETMARQUEE = 1034
|
||||
PBM_SETPOS = 1026
|
||||
PBM_SETRANGE = 1025
|
||||
PBM_SETRANGE32 = 1030
|
||||
PBS_MARQUEE = 8
|
||||
PM_REMOVE = 1
|
||||
SW_HIDE = 0
|
||||
SW_SHOW = 5
|
||||
SW_SHOWNORMAL = 1
|
||||
SWP_NOACTIVATE = 16
|
||||
SWP_NOMOVE = 2
|
||||
SWP_NOSIZE = 1
|
||||
SWP_NOZORDER = 4
|
||||
VER_PLATFORM_WIN32_NT = 2
|
||||
WM_COMMAND = 273
|
||||
WM_GETTEXT = 13
|
||||
WM_GETTEXTLENGTH = 14
|
||||
WM_INITDIALOG = 272
|
||||
WM_NOTIFY = 78
|
||||
|
||||
# Windows function prototypes
|
||||
BrowseCallbackProc = ctypes.WINFUNCTYPE(ctypes.c_int, wintypes.HWND, ctypes.c_uint, wintypes.LPARAM, wintypes.LPARAM)
|
||||
|
||||
# Windows types
|
||||
LPCTSTR = ctypes.c_char_p
|
||||
LPTSTR = ctypes.c_char_p
|
||||
LPVOID = ctypes.c_voidp
|
||||
TCHAR = ctypes.c_char
|
||||
|
||||
class BROWSEINFO(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("hwndOwner", wintypes.HWND),
|
||||
("pidlRoot", LPVOID),
|
||||
("pszDisplayName", LPTSTR),
|
||||
("lpszTitle", LPCTSTR),
|
||||
("ulFlags", ctypes.c_uint),
|
||||
("lpfn", BrowseCallbackProc),
|
||||
("lParam", wintypes.LPARAM),
|
||||
("iImage", ctypes.c_int)
|
||||
]
|
||||
|
||||
|
||||
# Utilities
|
||||
def CenterWindow(hwnd):
|
||||
desktopRect = GetWindowRect(user32.GetDesktopWindow())
|
||||
myRect = GetWindowRect(hwnd)
|
||||
x = width(desktopRect) // 2 - width(myRect) // 2
|
||||
y = height(desktopRect) // 2 - height(myRect) // 2
|
||||
user32.SetWindowPos(hwnd, 0,
|
||||
desktopRect.left + x,
|
||||
desktopRect.top + y,
|
||||
0, 0,
|
||||
SWP_NOACTIVATE | SWP_NOSIZE | SWP_NOZORDER
|
||||
)
|
||||
|
||||
|
||||
def GetWindowRect(hwnd):
|
||||
rect = wintypes.RECT()
|
||||
user32.GetWindowRect(hwnd, ctypes.byref(rect))
|
||||
return rect
|
||||
|
||||
def width(rect):
|
||||
return rect.right-rect.left
|
||||
|
||||
def height(rect):
|
||||
return rect.bottom-rect.top
|
||||
|
||||
|
||||
def AskFolder(
|
||||
message=None,
|
||||
version=None,
|
||||
defaultLocation=None,
|
||||
location=None,
|
||||
windowTitle=None,
|
||||
actionButtonLabel=None,
|
||||
cancelButtonLabel=None,
|
||||
multiple=None):
|
||||
"""Display a dialog asking the user for select a folder.
|
||||
modified to use unicode strings as much as possible
|
||||
returns unicode path
|
||||
"""
|
||||
|
||||
def BrowseCallback(hwnd, uMsg, lParam, lpData):
|
||||
if uMsg == BFFM_INITIALIZED:
|
||||
if actionButtonLabel:
|
||||
label = str(actionButtonLabel, errors='replace')
|
||||
user32.SendMessageW(hwnd, BFFM_SETOKTEXT, 0, label)
|
||||
if cancelButtonLabel:
|
||||
label = str(cancelButtonLabel, errors='replace')
|
||||
cancelButton = user32.GetDlgItem(hwnd, IDCANCEL)
|
||||
if cancelButton:
|
||||
user32.SetWindowTextW(cancelButton, label)
|
||||
if windowTitle:
|
||||
title = str(windowTitle, errors='replace')
|
||||
user32.SetWindowTextW(hwnd, title)
|
||||
if defaultLocation:
|
||||
user32.SendMessageW(hwnd, BFFM_SETSELECTIONW, 1, defaultLocation.replace('/', '\\'))
|
||||
if location:
|
||||
x, y = location
|
||||
desktopRect = wintypes.RECT()
|
||||
user32.GetWindowRect(0, ctypes.byref(desktopRect))
|
||||
user32.SetWindowPos(hwnd, 0,
|
||||
desktopRect.left + x,
|
||||
desktopRect.top + y, 0, 0,
|
||||
SWP_NOACTIVATE | SWP_NOSIZE | SWP_NOZORDER)
|
||||
else:
|
||||
CenterWindow(hwnd)
|
||||
return 0
|
||||
|
||||
# This next line is needed to prevent gc of the callback
|
||||
callback = BrowseCallbackProc(BrowseCallback)
|
||||
|
||||
browseInfo = BROWSEINFO()
|
||||
browseInfo.pszDisplayName = ctypes.c_char_p('\0' * (MAX_PATH+1))
|
||||
browseInfo.lpszTitle = message
|
||||
browseInfo.lpfn = callback
|
||||
|
||||
pidl = shell32.SHBrowseForFolder(ctypes.byref(browseInfo))
|
||||
if not pidl:
|
||||
result = None
|
||||
else:
|
||||
path = LPCWSTR(" " * (MAX_PATH+1))
|
||||
shell32.SHGetPathFromIDListW(pidl, path)
|
||||
ole32.CoTaskMemFree(pidl)
|
||||
result = path.value
|
||||
return result
|
||||
|
||||
|
||||
|
||||
|
||||
1015
DeDRM_plugin/config.py
Executable file
1015
DeDRM_plugin/config.py
Executable file
File diff suppressed because it is too large
Load Diff
896
DeDRM_plugin/convert2xml.py
Normal file
896
DeDRM_plugin/convert2xml.py
Normal file
@@ -0,0 +1,896 @@
|
||||
#!/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):
|
||||
data = data.encode(self.encoding,"replace")
|
||||
self.stream.buffer.write(data)
|
||||
self.stream.buffer.flush()
|
||||
|
||||
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(''))
|
||||
210
DeDRM_plugin/epubtest.py
Normal file
210
DeDRM_plugin/epubtest.py
Normal file
@@ -0,0 +1,210 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# This is a python script. You need a Python interpreter to run it.
|
||||
# For example, ActiveState Python, which exists for windows.
|
||||
#
|
||||
# Changelog drmcheck
|
||||
# 1.00 - Initial version, with code from various other scripts
|
||||
# 1.01 - Moved authorship announcement to usage section.
|
||||
#
|
||||
# Changelog epubtest
|
||||
# 1.00 - Cut to epubtest.py, testing ePub files only by Apprentice Alf
|
||||
# 1.01 - Added routine for use by Windows DeDRM
|
||||
# 2.00 - Python 3, September 2020
|
||||
#
|
||||
# Written in 2011 by Paul Durrant
|
||||
# Released with unlicense. See http://unlicense.org/
|
||||
#
|
||||
#############################################################################
|
||||
#
|
||||
# This is free and unencumbered software released into the public domain.
|
||||
#
|
||||
# Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||
# distribute this software, either in source code form or as a compiled
|
||||
# binary, for any purpose, commercial or non-commercial, and by any
|
||||
# means.
|
||||
#
|
||||
# In jurisdictions that recognize copyright laws, the author or authors
|
||||
# of this software dedicate any and all copyright interest in the
|
||||
# software to the public domain. We make this dedication for the benefit
|
||||
# of the public at large and to the detriment of our heirs and
|
||||
# successors. We intend this dedication to be an overt act of
|
||||
# relinquishment in perpetuity of all present and future rights to this
|
||||
# software under copyright law.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||
# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
# OTHER DEALINGS IN THE SOFTWARE.
|
||||
#
|
||||
#############################################################################
|
||||
#
|
||||
# It's still polite to give attribution if you do reuse this code.
|
||||
#
|
||||
|
||||
__version__ = '2.0'
|
||||
|
||||
import sys, struct, os, traceback
|
||||
import zlib
|
||||
import zipfile
|
||||
import xml.etree.ElementTree as etree
|
||||
|
||||
NSMAP = {'adept': 'http://ns.adobe.com/adept',
|
||||
'enc': 'http://www.w3.org/2001/04/xmlenc#'}
|
||||
|
||||
# Wrap a stream so that output gets flushed immediately
|
||||
# and also make sure that any unicode strings get
|
||||
# encoded using "replace" before writing them.
|
||||
class SafeUnbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
self.encoding = stream.encoding
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
def write(self, data):
|
||||
if isinstance(data, str):
|
||||
data = data.encode(self.encoding,"replace")
|
||||
self.stream.buffer.write(data)
|
||||
self.stream.buffer.flush()
|
||||
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
try:
|
||||
from calibre.constants import iswindows, isosx
|
||||
except:
|
||||
iswindows = sys.platform.startswith('win')
|
||||
isosx = sys.platform.startswith('darwin')
|
||||
|
||||
def unicode_argv():
|
||||
if iswindows:
|
||||
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
||||
# strings.
|
||||
|
||||
# Versions 2.x of Python don't support Unicode in sys.argv on
|
||||
# Windows, with the underlying Windows API instead replacing multi-byte
|
||||
# characters with '?'. So use shell32.GetCommandLineArgvW to get sys.argv
|
||||
# as a list of Unicode strings and encode them as utf-8
|
||||
|
||||
from ctypes import POINTER, byref, cdll, c_int, windll
|
||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
||||
|
||||
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
||||
GetCommandLineW.argtypes = []
|
||||
GetCommandLineW.restype = LPCWSTR
|
||||
|
||||
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
||||
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
||||
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
||||
|
||||
cmd = GetCommandLineW()
|
||||
argc = c_int(0)
|
||||
argv = CommandLineToArgvW(cmd, byref(argc))
|
||||
if argc.value > 0:
|
||||
# Remove Python executable and commands if present
|
||||
start = argc.value - len(sys.argv)
|
||||
return [argv[i] for i in
|
||||
range(start, argc.value)]
|
||||
# if we don't have any arguments at all, just pass back script name
|
||||
# this should never happen
|
||||
return ["epubtest.py"]
|
||||
else:
|
||||
argvencoding = sys.stdin.encoding or "utf-8"
|
||||
return [arg if isinstance(arg, str) else str(arg, argvencoding) for arg in sys.argv]
|
||||
|
||||
_FILENAME_LEN_OFFSET = 26
|
||||
_EXTRA_LEN_OFFSET = 28
|
||||
_FILENAME_OFFSET = 30
|
||||
_MAX_SIZE = 64 * 1024
|
||||
|
||||
|
||||
def uncompress(cmpdata):
|
||||
dc = zlib.decompressobj(-15)
|
||||
data = ''
|
||||
while len(cmpdata) > 0:
|
||||
if len(cmpdata) > _MAX_SIZE :
|
||||
newdata = cmpdata[0:_MAX_SIZE]
|
||||
cmpdata = cmpdata[_MAX_SIZE:]
|
||||
else:
|
||||
newdata = cmpdata
|
||||
cmpdata = ''
|
||||
newdata = dc.decompress(newdata)
|
||||
unprocessed = dc.unconsumed_tail
|
||||
if len(unprocessed) == 0:
|
||||
newdata += dc.flush()
|
||||
data += newdata
|
||||
cmpdata += unprocessed
|
||||
unprocessed = ''
|
||||
return data
|
||||
|
||||
def getfiledata(file, zi):
|
||||
# get file name length and exta data length to find start of file data
|
||||
local_header_offset = zi.header_offset
|
||||
|
||||
file.seek(local_header_offset + _FILENAME_LEN_OFFSET)
|
||||
leninfo = file.read(2)
|
||||
local_name_length, = struct.unpack('<H', leninfo)
|
||||
|
||||
file.seek(local_header_offset + _EXTRA_LEN_OFFSET)
|
||||
exinfo = file.read(2)
|
||||
extra_field_length, = struct.unpack('<H', exinfo)
|
||||
|
||||
file.seek(local_header_offset + _FILENAME_OFFSET + local_name_length + extra_field_length)
|
||||
data = None
|
||||
|
||||
# if not compressed we are good to go
|
||||
if zi.compress_type == zipfile.ZIP_STORED:
|
||||
data = file.read(zi.file_size)
|
||||
|
||||
# if compressed we must decompress it using zlib
|
||||
if zi.compress_type == zipfile.ZIP_DEFLATED:
|
||||
cmpdata = file.read(zi.compress_size)
|
||||
data = uncompress(cmpdata)
|
||||
|
||||
return data
|
||||
|
||||
def encryption(infile):
|
||||
# returns encryption: one of Unencrypted, Adobe, B&N and Unknown
|
||||
encryption = "Error When Checking."
|
||||
try:
|
||||
with open(infile,'rb') as infileobject:
|
||||
bookdata = infileobject.read(58)
|
||||
# Check for Zip
|
||||
if bookdata[0:0+2] == b"PK":
|
||||
foundrights = False
|
||||
foundencryption = False
|
||||
inzip = zipfile.ZipFile(infile,'r')
|
||||
namelist = set(inzip.namelist())
|
||||
if 'META-INF/rights.xml' not in namelist or 'META-INF/encryption.xml' not in namelist:
|
||||
encryption = "Unencrypted"
|
||||
else:
|
||||
rights = etree.fromstring(inzip.read('META-INF/rights.xml'))
|
||||
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
||||
expr = './/%s' % (adept('encryptedKey'),)
|
||||
bookkey = ''.join(rights.findtext(expr))
|
||||
if len(bookkey) == 172:
|
||||
encryption = "Adobe"
|
||||
elif len(bookkey) == 64:
|
||||
encryption = "B&N"
|
||||
else:
|
||||
encryption = "Unknown"
|
||||
except:
|
||||
traceback.print_exc()
|
||||
return encryption
|
||||
|
||||
def main():
|
||||
argv=unicode_argv()
|
||||
if len(argv) < 2:
|
||||
print("Give an ePub file as a parameter.")
|
||||
else:
|
||||
print(encryption(argv[1]))
|
||||
return 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
sys.exit(main())
|
||||
591
DeDRM_plugin/erdr2pml.py
Executable file
591
DeDRM_plugin/erdr2pml.py
Executable file
@@ -0,0 +1,591 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# erdr2pml.py
|
||||
# Copyright © 2008-2020 The Dark Reverser, Apprentice Harper et al.
|
||||
#
|
||||
# Changelog
|
||||
#
|
||||
# Based on ereader2html version 0.08 plus some later small fixes
|
||||
#
|
||||
# 0.01 - Initial version
|
||||
# 0.02 - Support more eReader files. Support bold text and links. Fix PML decoder parsing bug.
|
||||
# 0.03 - Fix incorrect variable usage at one place.
|
||||
# 0.03b - enhancement by DeBockle (version 259 support)
|
||||
# Custom version 0.03 - no change to eReader support, only usability changes
|
||||
# - start of pep-8 indentation (spaces not tab), fix trailing blanks
|
||||
# - version variable, only one place to change
|
||||
# - added main routine, now callable as a library/module,
|
||||
# means tools can add optional support for ereader2html
|
||||
# - outdir is no longer a mandatory parameter (defaults based on input name if missing)
|
||||
# - time taken output to stdout
|
||||
# - Psyco support - reduces runtime by a factor of (over) 3!
|
||||
# E.g. (~600Kb file) 90 secs down to 24 secs
|
||||
# - newstyle classes
|
||||
# - changed map call to list comprehension
|
||||
# may not work with python 2.3
|
||||
# without Psyco this reduces runtime to 90%
|
||||
# E.g. 90 secs down to 77 secs
|
||||
# Psyco with map calls takes longer, do not run with map in Psyco JIT!
|
||||
# - izip calls used instead of zip (if available), further reduction
|
||||
# in run time (factor of 4.5).
|
||||
# E.g. (~600Kb file) 90 secs down to 20 secs
|
||||
# - Python 2.6+ support, avoid DeprecationWarning with sha/sha1
|
||||
# 0.04 - Footnote support, PML output, correct charset in html, support more PML tags
|
||||
# - Feature change, dump out PML file
|
||||
# - Added supprt for footnote tags. NOTE footnote ids appear to be bad (not usable)
|
||||
# in some pdb files :-( due to the same id being used multiple times
|
||||
# - Added correct charset encoding (pml is based on cp1252)
|
||||
# - Added logging support.
|
||||
# 0.05 - Improved type 272 support for sidebars, links, chapters, metainfo, etc
|
||||
# 0.06 - Merge of 0.04 and 0.05. Improved HTML output
|
||||
# Placed images in subfolder, so that it's possible to just
|
||||
# drop the book.pml file onto DropBook to make an unencrypted
|
||||
# copy of the eReader file.
|
||||
# Using that with Calibre works a lot better than the HTML
|
||||
# conversion in this code.
|
||||
# 0.07 - Further Improved type 272 support for sidebars with all earlier fixes
|
||||
# 0.08 - fixed typos, removed extraneous things
|
||||
# 0.09 - fixed typos in first_pages to first_page to again support older formats
|
||||
# 0.10 - minor cleanups
|
||||
# 0.11 - fixups for using correct xml for footnotes and sidebars for use with Dropbook
|
||||
# 0.12 - Fix added to prevent lowercasing of image names when the pml code itself uses a different case in the link name.
|
||||
# 0.13 - change to unbuffered stdout for use with gui front ends
|
||||
# 0.14 - contributed enhancement to support --make-pmlz switch
|
||||
# 0.15 - enabled high-ascii to pml character encoding. DropBook now works on Mac.
|
||||
# 0.16 - convert to use openssl DES (very very fast) or pure python DES if openssl's libcrypto is not available
|
||||
# 0.17 - added support for pycrypto's DES as well
|
||||
# 0.18 - on Windows try PyCrypto first and OpenSSL next
|
||||
# 0.19 - Modify the interface to allow use of import
|
||||
# 0.20 - modify to allow use inside new interface for calibre plugins
|
||||
# 0.21 - Support eReader (drm) version 11.
|
||||
# - Don't reject dictionary format.
|
||||
# - Ignore sidebars for dictionaries (different format?)
|
||||
# 0.22 - Unicode and plugin support, different image folders for PMLZ and source
|
||||
# 0.23 - moved unicode_argv call inside main for Windows DeDRM compatibility
|
||||
# 1.00 - Added Python 3 compatibility for calibre 5.0
|
||||
|
||||
__version__='1.00'
|
||||
|
||||
import sys, re
|
||||
import struct, binascii, getopt, zlib, os, os.path, urllib, tempfile, traceback
|
||||
|
||||
if 'calibre' in sys.modules:
|
||||
inCalibre = True
|
||||
else:
|
||||
inCalibre = False
|
||||
|
||||
# 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):
|
||||
data = data.encode(self.encoding,"replace")
|
||||
self.stream.buffer.write(data)
|
||||
self.stream.buffer.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
iswindows = sys.platform.startswith('win')
|
||||
isosx = sys.platform.startswith('darwin')
|
||||
|
||||
def unicode_argv():
|
||||
if iswindows:
|
||||
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
||||
# strings.
|
||||
|
||||
# Versions 2.x of Python don't support Unicode in sys.argv on
|
||||
# Windows, with the underlying Windows API instead replacing multi-byte
|
||||
# characters with '?'.
|
||||
|
||||
|
||||
from ctypes import POINTER, byref, cdll, c_int, windll
|
||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
||||
|
||||
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
||||
GetCommandLineW.argtypes = []
|
||||
GetCommandLineW.restype = LPCWSTR
|
||||
|
||||
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
||||
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
||||
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
||||
|
||||
cmd = GetCommandLineW()
|
||||
argc = c_int(0)
|
||||
argv = CommandLineToArgvW(cmd, byref(argc))
|
||||
if argc.value > 0:
|
||||
# Remove Python executable and commands if present
|
||||
start = argc.value - len(sys.argv)
|
||||
return [argv[i] for i in
|
||||
range(start, argc.value)]
|
||||
# if we don't have any arguments at all, just pass back script name
|
||||
# this should never happen
|
||||
return ["mobidedrm.py"]
|
||||
else:
|
||||
argvencoding = sys.stdin.encoding or "utf-8"
|
||||
return [arg if isinstance(arg, str) else str(arg, argvencoding) for arg in sys.argv]
|
||||
|
||||
Des = None
|
||||
if iswindows:
|
||||
# first try with pycrypto
|
||||
if inCalibre:
|
||||
from calibre_plugins.dedrm import pycrypto_des
|
||||
else:
|
||||
import pycrypto_des
|
||||
Des = pycrypto_des.load_pycrypto()
|
||||
if Des == None:
|
||||
# they try with openssl
|
||||
if inCalibre:
|
||||
from calibre_plugins.dedrm import openssl_des
|
||||
else:
|
||||
import openssl_des
|
||||
Des = openssl_des.load_libcrypto()
|
||||
else:
|
||||
# first try with openssl
|
||||
if inCalibre:
|
||||
from calibre_plugins.dedrm import openssl_des
|
||||
else:
|
||||
import openssl_des
|
||||
Des = openssl_des.load_libcrypto()
|
||||
if Des == None:
|
||||
# then try with pycrypto
|
||||
if inCalibre:
|
||||
from calibre_plugins.dedrm import pycrypto_des
|
||||
else:
|
||||
import pycrypto_des
|
||||
Des = pycrypto_des.load_pycrypto()
|
||||
|
||||
# if that did not work then use pure python implementation
|
||||
# of DES and try to speed it up with Psycho
|
||||
if Des == None:
|
||||
if inCalibre:
|
||||
from calibre_plugins.dedrm import python_des
|
||||
else:
|
||||
import python_des
|
||||
Des = python_des.Des
|
||||
# Import Psyco if available
|
||||
try:
|
||||
# http://psyco.sourceforge.net
|
||||
import psyco
|
||||
psyco.full()
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
try:
|
||||
from hashlib import sha1
|
||||
except ImportError:
|
||||
# older Python release
|
||||
import sha
|
||||
sha1 = lambda s: sha.new(s)
|
||||
|
||||
import cgi
|
||||
import logging
|
||||
|
||||
logging.basicConfig()
|
||||
#logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
|
||||
class Sectionizer(object):
|
||||
bkType = "Book"
|
||||
|
||||
def __init__(self, filename, ident):
|
||||
self.contents = open(filename, 'rb').read()
|
||||
self.header = self.contents[0:72]
|
||||
self.num_sections, = struct.unpack('>H', self.contents[76:78])
|
||||
# Dictionary or normal content (TODO: Not hard-coded)
|
||||
if self.header[0x3C:0x3C+8] != ident:
|
||||
if self.header[0x3C:0x3C+8] == b"PDctPPrs":
|
||||
self.bkType = "Dict"
|
||||
else:
|
||||
raise ValueError('Invalid file format')
|
||||
self.sections = []
|
||||
for i in range(self.num_sections):
|
||||
offset, a1,a2,a3,a4 = struct.unpack('>LBBBB', self.contents[78+i*8:78+i*8+8])
|
||||
flags, val = a1, a2<<16|a3<<8|a4
|
||||
self.sections.append( (offset, flags, val) )
|
||||
def loadSection(self, section):
|
||||
if section + 1 == self.num_sections:
|
||||
end_off = len(self.contents)
|
||||
else:
|
||||
end_off = self.sections[section + 1][0]
|
||||
off = self.sections[section][0]
|
||||
return self.contents[off:end_off]
|
||||
|
||||
# cleanup unicode filenames
|
||||
# borrowed from calibre from calibre/src/calibre/__init__.py
|
||||
# added in removal of control (<32) chars
|
||||
# and removal of . at start and end
|
||||
# and with some (heavily edited) code from Paul Durrant's kindlenamer.py
|
||||
def sanitizeFileName(name):
|
||||
# substitute filename unfriendly characters
|
||||
name = name.replace("<","[").replace(">","]").replace(" : "," – ").replace(": "," – ").replace(":","—").replace("/","_").replace("\\","_").replace("|","_").replace("\"","\'")
|
||||
# delete control characters
|
||||
name = "".join(char for char in name if ord(char)>=32)
|
||||
# white space to single space, delete leading and trailing while space
|
||||
name = re.sub(r"\s", " ", name).strip()
|
||||
# remove leading dots
|
||||
while len(name)>0 and name[0] == ".":
|
||||
name = name[1:]
|
||||
# remove trailing dots (Windows doesn't like them)
|
||||
if name.endswith("."):
|
||||
name = name[:-1]
|
||||
return name
|
||||
|
||||
def fixKey(key):
|
||||
def fixByte(b):
|
||||
return b ^ ((b ^ (b<<1) ^ (b<<2) ^ (b<<3) ^ (b<<4) ^ (b<<5) ^ (b<<6) ^ (b<<7) ^ 0x80) & 0x80)
|
||||
return bytes([fixByte(a) for a in key])
|
||||
|
||||
def deXOR(text, sp, table):
|
||||
r=''
|
||||
j = sp
|
||||
for i in range(len(text)):
|
||||
r += chr(ord(table[j]) ^ ord(text[i]))
|
||||
j = j + 1
|
||||
if j == len(table):
|
||||
j = 0
|
||||
return r
|
||||
|
||||
class EreaderProcessor(object):
|
||||
def __init__(self, sect, user_key):
|
||||
self.section_reader = sect.loadSection
|
||||
data = self.section_reader(0)
|
||||
version, = struct.unpack('>H', data[0:2])
|
||||
self.version = version
|
||||
logging.info('eReader file format version %s', version)
|
||||
if version != 272 and version != 260 and version != 259:
|
||||
raise ValueError('incorrect eReader version %d (error 1)' % version)
|
||||
data = self.section_reader(1)
|
||||
self.data = data
|
||||
des = Des(fixKey(data[0:8]))
|
||||
cookie_shuf, cookie_size = struct.unpack('>LL', des.decrypt(data[-8:]))
|
||||
if cookie_shuf < 3 or cookie_shuf > 0x14 or cookie_size < 0xf0 or cookie_size > 0x200:
|
||||
raise ValueError('incorrect eReader version (error 2)')
|
||||
input = des.decrypt(data[-cookie_size:])
|
||||
def unshuff(data, shuf):
|
||||
r = [0] * len(data)
|
||||
j = 0
|
||||
for i in range(len(data)):
|
||||
j = (j + shuf) % len(data)
|
||||
r[j] = data[i]
|
||||
assert len(bytes(r)) == len(data)
|
||||
return bytes(r)
|
||||
r = unshuff(input[0:-8], cookie_shuf)
|
||||
|
||||
drm_sub_version = struct.unpack('>H', r[0:2])[0]
|
||||
self.num_text_pages = struct.unpack('>H', r[2:4])[0] - 1
|
||||
self.num_image_pages = struct.unpack('>H', r[26:26+2])[0]
|
||||
self.first_image_page = struct.unpack('>H', r[24:24+2])[0]
|
||||
# Default values
|
||||
self.num_footnote_pages = 0
|
||||
self.num_sidebar_pages = 0
|
||||
self.first_footnote_page = -1
|
||||
self.first_sidebar_page = -1
|
||||
if self.version == 272:
|
||||
self.num_footnote_pages = struct.unpack('>H', r[46:46+2])[0]
|
||||
self.first_footnote_page = struct.unpack('>H', r[44:44+2])[0]
|
||||
if (sect.bkType == "Book"):
|
||||
self.num_sidebar_pages = struct.unpack('>H', r[38:38+2])[0]
|
||||
self.first_sidebar_page = struct.unpack('>H', r[36:36+2])[0]
|
||||
# self.num_bookinfo_pages = struct.unpack('>H', r[34:34+2])[0]
|
||||
# self.first_bookinfo_page = struct.unpack('>H', r[32:32+2])[0]
|
||||
# self.num_chapter_pages = struct.unpack('>H', r[22:22+2])[0]
|
||||
# self.first_chapter_page = struct.unpack('>H', r[20:20+2])[0]
|
||||
# self.num_link_pages = struct.unpack('>H', r[30:30+2])[0]
|
||||
# self.first_link_page = struct.unpack('>H', r[28:28+2])[0]
|
||||
# self.num_xtextsize_pages = struct.unpack('>H', r[54:54+2])[0]
|
||||
# self.first_xtextsize_page = struct.unpack('>H', r[52:52+2])[0]
|
||||
|
||||
# **before** data record 1 was decrypted and unshuffled, it contained data
|
||||
# to create an XOR table and which is used to fix footnote record 0, link records, chapter records, etc
|
||||
self.xortable_offset = struct.unpack('>H', r[40:40+2])[0]
|
||||
self.xortable_size = struct.unpack('>H', r[42:42+2])[0]
|
||||
self.xortable = self.data[self.xortable_offset:self.xortable_offset + self.xortable_size]
|
||||
else:
|
||||
# Nothing needs to be done
|
||||
pass
|
||||
# self.num_bookinfo_pages = 0
|
||||
# self.num_chapter_pages = 0
|
||||
# self.num_link_pages = 0
|
||||
# self.num_xtextsize_pages = 0
|
||||
# self.first_bookinfo_page = -1
|
||||
# self.first_chapter_page = -1
|
||||
# self.first_link_page = -1
|
||||
# self.first_xtextsize_page = -1
|
||||
|
||||
logging.debug('self.num_text_pages %d', self.num_text_pages)
|
||||
logging.debug('self.num_footnote_pages %d, self.first_footnote_page %d', self.num_footnote_pages , self.first_footnote_page)
|
||||
logging.debug('self.num_sidebar_pages %d, self.first_sidebar_page %d', self.num_sidebar_pages , self.first_sidebar_page)
|
||||
self.flags = struct.unpack('>L', r[4:8])[0]
|
||||
reqd_flags = (1<<9) | (1<<7) | (1<<10)
|
||||
if (self.flags & reqd_flags) != reqd_flags:
|
||||
print("Flags: 0x%X" % self.flags)
|
||||
raise ValueError('incompatible eReader file')
|
||||
des = Des(fixKey(user_key))
|
||||
if version == 259:
|
||||
if drm_sub_version != 7:
|
||||
raise ValueError('incorrect eReader version %d (error 3)' % drm_sub_version)
|
||||
encrypted_key_sha = r[44:44+20]
|
||||
encrypted_key = r[64:64+8]
|
||||
elif version == 260:
|
||||
if drm_sub_version != 13 and drm_sub_version != 11:
|
||||
raise ValueError('incorrect eReader version %d (error 3)' % drm_sub_version)
|
||||
if drm_sub_version == 13:
|
||||
encrypted_key = r[44:44+8]
|
||||
encrypted_key_sha = r[52:52+20]
|
||||
else:
|
||||
encrypted_key = r[64:64+8]
|
||||
encrypted_key_sha = r[44:44+20]
|
||||
elif version == 272:
|
||||
encrypted_key = r[172:172+8]
|
||||
encrypted_key_sha = r[56:56+20]
|
||||
self.content_key = des.decrypt(encrypted_key)
|
||||
if sha1(self.content_key).digest() != encrypted_key_sha:
|
||||
raise ValueError('Incorrect Name and/or Credit Card')
|
||||
|
||||
def getNumImages(self):
|
||||
return self.num_image_pages
|
||||
|
||||
def getImage(self, i):
|
||||
sect = self.section_reader(self.first_image_page + i)
|
||||
name = sect[4:4+32].strip(b'\0')
|
||||
data = sect[62:]
|
||||
return sanitizeFileName(name.decode('windows-1252')), data
|
||||
|
||||
|
||||
# def getChapterNamePMLOffsetData(self):
|
||||
# cv = ''
|
||||
# if self.num_chapter_pages > 0:
|
||||
# for i in xrange(self.num_chapter_pages):
|
||||
# chaps = self.section_reader(self.first_chapter_page + i)
|
||||
# j = i % self.xortable_size
|
||||
# offname = deXOR(chaps, j, self.xortable)
|
||||
# offset = struct.unpack('>L', offname[0:4])[0]
|
||||
# name = offname[4:].strip('\0')
|
||||
# cv += '%d|%s\n' % (offset, name)
|
||||
# return cv
|
||||
|
||||
# def getLinkNamePMLOffsetData(self):
|
||||
# lv = ''
|
||||
# if self.num_link_pages > 0:
|
||||
# for i in xrange(self.num_link_pages):
|
||||
# links = self.section_reader(self.first_link_page + i)
|
||||
# j = i % self.xortable_size
|
||||
# offname = deXOR(links, j, self.xortable)
|
||||
# offset = struct.unpack('>L', offname[0:4])[0]
|
||||
# name = offname[4:].strip('\0')
|
||||
# lv += '%d|%s\n' % (offset, name)
|
||||
# return lv
|
||||
|
||||
# def getExpandedTextSizesData(self):
|
||||
# ts = ''
|
||||
# if self.num_xtextsize_pages > 0:
|
||||
# tsize = deXOR(self.section_reader(self.first_xtextsize_page), 0, self.xortable)
|
||||
# for i in xrange(self.num_text_pages):
|
||||
# xsize = struct.unpack('>H', tsize[0:2])[0]
|
||||
# ts += "%d\n" % xsize
|
||||
# tsize = tsize[2:]
|
||||
# return ts
|
||||
|
||||
# def getBookInfo(self):
|
||||
# bkinfo = ''
|
||||
# if self.num_bookinfo_pages > 0:
|
||||
# info = self.section_reader(self.first_bookinfo_page)
|
||||
# bkinfo = deXOR(info, 0, self.xortable)
|
||||
# bkinfo = bkinfo.replace('\0','|')
|
||||
# bkinfo += '\n'
|
||||
# return bkinfo
|
||||
|
||||
def getText(self):
|
||||
des = Des(fixKey(self.content_key))
|
||||
r = b''
|
||||
for i in range(self.num_text_pages):
|
||||
logging.debug('get page %d', i)
|
||||
r += zlib.decompress(des.decrypt(self.section_reader(1 + i)))
|
||||
|
||||
# now handle footnotes pages
|
||||
if self.num_footnote_pages > 0:
|
||||
r += '\n'
|
||||
# the record 0 of the footnote section must pass through the Xor Table to make it useful
|
||||
sect = self.section_reader(self.first_footnote_page)
|
||||
fnote_ids = deXOR(sect, 0, self.xortable)
|
||||
# the remaining records of the footnote sections need to be decoded with the content_key and zlib inflated
|
||||
des = Des(fixKey(self.content_key))
|
||||
for i in range(1,self.num_footnote_pages):
|
||||
logging.debug('get footnotepage %d', i)
|
||||
id_len = ord(fnote_ids[2])
|
||||
id = fnote_ids[3:3+id_len]
|
||||
fmarker = '<footnote id="%s">\n' % id
|
||||
fmarker += zlib.decompress(des.decrypt(self.section_reader(self.first_footnote_page + i)))
|
||||
fmarker += '\n</footnote>\n'
|
||||
r += fmarker
|
||||
fnote_ids = fnote_ids[id_len+4:]
|
||||
|
||||
# TODO: Handle dictionary index (?) pages - which are also marked as
|
||||
# sidebar_pages (?). For now dictionary sidebars are ignored
|
||||
# For dictionaries - record 0 is null terminated strings, followed by
|
||||
# blocks of around 62000 bytes and a final block. Not sure of the
|
||||
# encoding
|
||||
|
||||
# now handle sidebar pages
|
||||
if self.num_sidebar_pages > 0:
|
||||
r += '\n'
|
||||
# the record 0 of the sidebar section must pass through the Xor Table to make it useful
|
||||
sect = self.section_reader(self.first_sidebar_page)
|
||||
sbar_ids = deXOR(sect, 0, self.xortable)
|
||||
# the remaining records of the sidebar sections need to be decoded with the content_key and zlib inflated
|
||||
des = Des(fixKey(self.content_key))
|
||||
for i in range(1,self.num_sidebar_pages):
|
||||
id_len = ord(sbar_ids[2])
|
||||
id = sbar_ids[3:3+id_len]
|
||||
smarker = '<sidebar id="%s">\n' % id
|
||||
smarker += zlib.decompress(des.decrypt(self.section_reader(self.first_sidebar_page + i)))
|
||||
smarker += '\n</sidebar>\n'
|
||||
r += smarker
|
||||
sbar_ids = sbar_ids[id_len+4:]
|
||||
|
||||
return r
|
||||
|
||||
def cleanPML(pml):
|
||||
# Convert special characters to proper PML code. High ASCII start at (\x80, \a128) and go up to (\xff, \a255)
|
||||
pml2 = pml
|
||||
for k in range(128,256):
|
||||
pml2 = pml2.replace(bytes([k]), b'\\a%03d' % k)
|
||||
return pml2
|
||||
|
||||
def decryptBook(infile, outpath, make_pmlz, user_key):
|
||||
bookname = os.path.splitext(os.path.basename(infile))[0]
|
||||
if make_pmlz:
|
||||
# outpath is actually pmlz name
|
||||
pmlzname = outpath
|
||||
outdir = tempfile.mkdtemp()
|
||||
imagedirpath = os.path.join(outdir,"images")
|
||||
else:
|
||||
pmlzname = None
|
||||
outdir = outpath
|
||||
imagedirpath = os.path.join(outdir,bookname + "_img")
|
||||
|
||||
try:
|
||||
if not os.path.exists(outdir):
|
||||
os.makedirs(outdir)
|
||||
print("Decoding File")
|
||||
sect =Sectionizer(infile, b'PNRdPPrs')
|
||||
er = EreaderProcessor(sect, user_key)
|
||||
|
||||
if er.getNumImages() > 0:
|
||||
print("Extracting images")
|
||||
if not os.path.exists(imagedirpath):
|
||||
os.makedirs(imagedirpath)
|
||||
for i in range(er.getNumImages()):
|
||||
name, contents = er.getImage(i)
|
||||
open(os.path.join(imagedirpath, name), 'wb').write(contents)
|
||||
|
||||
print("Extracting pml")
|
||||
pml_string = er.getText()
|
||||
pmlfilename = bookname + ".pml"
|
||||
open(os.path.join(outdir, pmlfilename),'wb').write(cleanPML(pml_string))
|
||||
if pmlzname is not None:
|
||||
import zipfile
|
||||
import shutil
|
||||
print("Creating PMLZ file {0}".format(os.path.basename(pmlzname)))
|
||||
myZipFile = zipfile.ZipFile(pmlzname,'w',zipfile.ZIP_STORED, False)
|
||||
list = os.listdir(outdir)
|
||||
for filename in list:
|
||||
localname = filename
|
||||
filePath = os.path.join(outdir,filename)
|
||||
if os.path.isfile(filePath):
|
||||
myZipFile.write(filePath, localname)
|
||||
elif os.path.isdir(filePath):
|
||||
imageList = os.listdir(filePath)
|
||||
localimgdir = os.path.basename(filePath)
|
||||
for image in imageList:
|
||||
localname = os.path.join(localimgdir,image)
|
||||
imagePath = os.path.join(filePath,image)
|
||||
if os.path.isfile(imagePath):
|
||||
myZipFile.write(imagePath, localname)
|
||||
myZipFile.close()
|
||||
# remove temporary directory
|
||||
shutil.rmtree(outdir, True)
|
||||
print("Output is {0}".format(pmlzname))
|
||||
else:
|
||||
print("Output is in {0}".format(outdir))
|
||||
print("done")
|
||||
except ValueError as e:
|
||||
print("Error: {0}".format(e))
|
||||
traceback.print_exc()
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
def usage():
|
||||
print("Converts DRMed eReader books to PML Source")
|
||||
print("Usage:")
|
||||
print(" erdr2pml [options] infile.pdb [outpath] \"your name\" credit_card_number")
|
||||
print(" ")
|
||||
print("Options: ")
|
||||
print(" -h prints this message")
|
||||
print(" -p create PMLZ instead of source folder")
|
||||
print(" --make-pmlz create PMLZ instead of source folder")
|
||||
print(" ")
|
||||
print("Note:")
|
||||
print(" if outpath is ommitted, creates source in 'infile_Source' folder")
|
||||
print(" if outpath is ommitted and pmlz option, creates PMLZ 'infile.pmlz'")
|
||||
print(" if source folder created, images are in infile_img folder")
|
||||
print(" if pmlz file created, images are in images folder")
|
||||
print(" It's enough to enter the last 8 digits of the credit card number")
|
||||
return
|
||||
|
||||
def getuser_key(name,cc):
|
||||
newname = "".join(c for c in name.lower() if c >= 'a' and c <= 'z' or c >= '0' and c <= '9')
|
||||
cc = cc.replace(" ","")
|
||||
return struct.pack('>LL', binascii.crc32(bytes(newname.encode('utf-8'))) & 0xffffffff, binascii.crc32(bytes(cc[-8:].encode('utf-8'))) & 0xffffffff)
|
||||
|
||||
def cli_main():
|
||||
print("eRdr2Pml v{0}. Copyright © 2009–2020 The Dark Reverser et al.".format(__version__))
|
||||
|
||||
argv=unicode_argv()
|
||||
try:
|
||||
opts, args = getopt.getopt(argv[1:], "hp", ["make-pmlz"])
|
||||
except getopt.GetoptError as err:
|
||||
print(err.args[0])
|
||||
usage()
|
||||
return 1
|
||||
make_pmlz = False
|
||||
for o, a in opts:
|
||||
if o == "-h":
|
||||
usage()
|
||||
return 0
|
||||
elif o == "-p":
|
||||
make_pmlz = True
|
||||
elif o == "--make-pmlz":
|
||||
make_pmlz = True
|
||||
|
||||
if len(args)!=3 and len(args)!=4:
|
||||
usage()
|
||||
return 1
|
||||
|
||||
if len(args)==3:
|
||||
infile, name, cc = args
|
||||
if make_pmlz:
|
||||
outpath = os.path.splitext(infile)[0] + ".pmlz"
|
||||
else:
|
||||
outpath = os.path.splitext(infile)[0] + "_Source"
|
||||
elif len(args)==4:
|
||||
infile, outpath, name, cc = args
|
||||
|
||||
print(binascii.b2a_hex(getuser_key(name,cc)))
|
||||
|
||||
return decryptBook(infile, outpath, make_pmlz, getuser_key(name,cc))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
sys.exit(cli_main())
|
||||
|
||||
@@ -7,23 +7,23 @@ import csv
|
||||
import os
|
||||
import math
|
||||
import getopt
|
||||
import functools
|
||||
from struct import pack
|
||||
from struct import unpack
|
||||
|
||||
|
||||
class DocParser(object):
|
||||
def __init__(self, flatxml, classlst, fileid, bookDir, fixedimage):
|
||||
def __init__(self, flatxml, classlst, fileid, bookDir, gdict, fixedimage):
|
||||
self.id = os.path.basename(fileid).replace('.dat','')
|
||||
self.svgcount = 0
|
||||
self.docList = flatxml.split('\n')
|
||||
self.docList = flatxml.split(b'\n')
|
||||
self.docSize = len(self.docList)
|
||||
self.classList = {}
|
||||
self.bookDir = bookDir
|
||||
self.glyphPaths = { }
|
||||
self.numPaths = 0
|
||||
self.gdict = gdict
|
||||
tmpList = classlst.split('\n')
|
||||
for pclass in tmpList:
|
||||
if pclass != '':
|
||||
if pclass != b'':
|
||||
# remove the leading period from the css name
|
||||
cname = pclass[1:]
|
||||
self.classList[cname] = True
|
||||
@@ -41,9 +41,8 @@ class DocParser(object):
|
||||
|
||||
def getGlyph(self, gid):
|
||||
result = ''
|
||||
id='gl%d' % gid
|
||||
return self.glyphPaths[id]
|
||||
|
||||
id='id="gl%d"' % gid
|
||||
return self.gdict.lookup(id)
|
||||
|
||||
def glyphs_to_image(self, glyphList):
|
||||
|
||||
@@ -52,35 +51,16 @@ class DocParser(object):
|
||||
e = path.find(' ',b)
|
||||
return int(path[b:e])
|
||||
|
||||
def extractID(path, key):
|
||||
b = path.find(key) + len(key)
|
||||
e = path.find('"',b)
|
||||
return path[b:e]
|
||||
|
||||
|
||||
svgDir = os.path.join(self.bookDir,'svg')
|
||||
glyfile = os.path.join(svgDir,'glyphs.svg')
|
||||
|
||||
imgDir = os.path.join(self.bookDir,'img')
|
||||
imgname = self.id + '_%04d.svg' % self.svgcount
|
||||
imgfile = os.path.join(imgDir,imgname)
|
||||
|
||||
# build hashtable of glyph paths keyed by glyph id
|
||||
if self.numPaths == 0:
|
||||
gfile = open(glyfile, 'r')
|
||||
while True:
|
||||
path = gfile.readline()
|
||||
if (path == ''): break
|
||||
glyphid = extractID(path,'id="')
|
||||
self.glyphPaths[glyphid] = path
|
||||
self.numPaths += 1
|
||||
gfile.close()
|
||||
|
||||
|
||||
# get glyph information
|
||||
gxList = self.getData('info.glyph.x',0,-1)
|
||||
gyList = self.getData('info.glyph.y',0,-1)
|
||||
gidList = self.getData('info.glyph.glyphID',0,-1)
|
||||
gxList = self.getData(b'info.glyph.x',0,-1)
|
||||
gyList = self.getData(b'info.glyph.y',0,-1)
|
||||
gidList = self.getData(b'info.glyph.glyphID',0,-1)
|
||||
|
||||
gids = []
|
||||
maxws = []
|
||||
@@ -89,7 +69,7 @@ class DocParser(object):
|
||||
ys = []
|
||||
gdefs = []
|
||||
|
||||
# get path defintions, positions, dimensions for ecah glyph
|
||||
# get path defintions, positions, dimensions for each glyph
|
||||
# that makes up the image, and find min x and min y to reposition origin
|
||||
minx = -1
|
||||
miny = -1
|
||||
@@ -100,7 +80,7 @@ class DocParser(object):
|
||||
xs.append(gxList[j])
|
||||
if minx == -1: minx = gxList[j]
|
||||
else : minx = min(minx, gxList[j])
|
||||
|
||||
|
||||
ys.append(gyList[j])
|
||||
if miny == -1: miny = gyList[j]
|
||||
else : miny = min(miny, gyList[j])
|
||||
@@ -115,7 +95,7 @@ class DocParser(object):
|
||||
# change the origin to minx, miny and calc max height and width
|
||||
maxw = maxws[0] + xs[0] - minx
|
||||
maxh = maxhs[0] + ys[0] - miny
|
||||
for j in xrange(0, len(xs)):
|
||||
for j in range(0, len(xs)):
|
||||
xs[j] = xs[j] - minx
|
||||
ys[j] = ys[j] - miny
|
||||
maxw = max( maxw, (maxws[j] + xs[j]) )
|
||||
@@ -127,10 +107,10 @@ class DocParser(object):
|
||||
ifile.write('<!DOCTYPE svg PUBLIC "-//W3C/DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n')
|
||||
ifile.write('<svg width="%dpx" height="%dpx" viewBox="0 0 %d %d" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">\n' % (math.floor(maxw/10), math.floor(maxh/10), maxw, maxh))
|
||||
ifile.write('<defs>\n')
|
||||
for j in xrange(0,len(gdefs)):
|
||||
for j in range(0,len(gdefs)):
|
||||
ifile.write(gdefs[j])
|
||||
ifile.write('</defs>\n')
|
||||
for j in xrange(0,len(gids)):
|
||||
for j in range(0,len(gids)):
|
||||
ifile.write('<use xlink:href="#gl%d" x="%d" y="%d" />\n' % (gids[j], xs[j], ys[j]))
|
||||
ifile.write('</svg>')
|
||||
ifile.close()
|
||||
@@ -143,14 +123,14 @@ class DocParser(object):
|
||||
def lineinDoc(self, pos) :
|
||||
if (pos >= 0) and (pos < self.docSize) :
|
||||
item = self.docList[pos]
|
||||
if item.find('=') >= 0:
|
||||
(name, argres) = item.split('=',1)
|
||||
else :
|
||||
if item.find(b'=') >= 0:
|
||||
(name, argres) = item.split(b'=',1)
|
||||
else :
|
||||
name = item
|
||||
argres = ''
|
||||
argres = b''
|
||||
return name, argres
|
||||
|
||||
|
||||
|
||||
# find tag in doc if within pos to end inclusive
|
||||
def findinDoc(self, tagpath, pos, end) :
|
||||
result = None
|
||||
@@ -159,14 +139,16 @@ class DocParser(object):
|
||||
else:
|
||||
end = min(self.docSize, end)
|
||||
foundat = -1
|
||||
for j in xrange(pos, end):
|
||||
for j in range(pos, end):
|
||||
item = self.docList[j]
|
||||
if item.find('=') >= 0:
|
||||
(name, argres) = item.split('=',1)
|
||||
else :
|
||||
if item.find(b'=') >= 0:
|
||||
(name, argres) = item.split(b'=',1)
|
||||
else :
|
||||
name = item
|
||||
argres = ''
|
||||
if name.endswith(tagpath) :
|
||||
if (isinstance(tagpath,str)):
|
||||
tagpath = tagpath.encode('utf-8')
|
||||
if name.endswith(tagpath) :
|
||||
result = argres
|
||||
foundat = j
|
||||
break
|
||||
@@ -191,7 +173,7 @@ class DocParser(object):
|
||||
argres=[]
|
||||
(foundat, argt) = self.findinDoc(tagpath, pos, end)
|
||||
if (argt != None) and (len(argt) > 0) :
|
||||
argList = argt.split('|')
|
||||
argList = argt.split(b'|')
|
||||
argres = [ int(strval) for strval in argList]
|
||||
return argres
|
||||
|
||||
@@ -203,36 +185,36 @@ class DocParser(object):
|
||||
# class names are an issue given topaz may start them with numerals (not allowed),
|
||||
# use a mix of cases (which cause some browsers problems), and actually
|
||||
# attach numbers after "_reclustered*" to the end to deal classeses that inherit
|
||||
# from a base class (but then not actually provide all of these _reclustereed
|
||||
# from a base class (but then not actually provide all of these _reclustereed
|
||||
# classes in the stylesheet!
|
||||
|
||||
# so we clean this up by lowercasing, prepend 'cl-', and getting any baseclass
|
||||
# that exists in the stylesheet first, and then adding this specific class
|
||||
# after
|
||||
|
||||
|
||||
# also some class names have spaces in them so need to convert to dashes
|
||||
if nclass != None :
|
||||
nclass = nclass.replace(' ','-')
|
||||
classres = ''
|
||||
nclass = nclass.replace(b' ',b'-')
|
||||
classres = b''
|
||||
nclass = nclass.lower()
|
||||
nclass = 'cl-' + nclass
|
||||
baseclass = ''
|
||||
nclass = b'cl-' + nclass
|
||||
baseclass = b''
|
||||
# graphic is the base class for captions
|
||||
if nclass.find('cl-cap-') >=0 :
|
||||
classres = 'graphic' + ' '
|
||||
if nclass.find(b'cl-cap-') >=0 :
|
||||
classres = b'graphic' + b' '
|
||||
else :
|
||||
# strip to find baseclass
|
||||
p = nclass.find('_')
|
||||
p = nclass.find(b'_')
|
||||
if p > 0 :
|
||||
baseclass = nclass[0:p]
|
||||
if baseclass in self.classList:
|
||||
classres += baseclass + ' '
|
||||
classres += baseclass + b' '
|
||||
classres += nclass
|
||||
nclass = classres
|
||||
return nclass
|
||||
|
||||
|
||||
# develop a sorted description of the starting positions of
|
||||
# develop a sorted description of the starting positions of
|
||||
# groups and regions on the page, as well as the page type
|
||||
def PageDescription(self):
|
||||
|
||||
@@ -246,11 +228,11 @@ class DocParser(object):
|
||||
return -1
|
||||
|
||||
result = []
|
||||
(pos, pagetype) = self.findinDoc('page.type',0,-1)
|
||||
(pos, pagetype) = self.findinDoc(b'page.type',0,-1)
|
||||
|
||||
groupList = self.posinDoc('page.group')
|
||||
groupregionList = self.posinDoc('page.group.region')
|
||||
pageregionList = self.posinDoc('page.region')
|
||||
groupList = self.posinDoc(b'page.group')
|
||||
groupregionList = self.posinDoc(b'page.group.region')
|
||||
pageregionList = self.posinDoc(b'page.region')
|
||||
# integrate into one list
|
||||
for j in groupList:
|
||||
result.append(('grpbeg',j))
|
||||
@@ -258,7 +240,7 @@ class DocParser(object):
|
||||
result.append(('gregion',j))
|
||||
for j in pageregionList:
|
||||
result.append(('pregion',j))
|
||||
result.sort(compare)
|
||||
result.sort(key=functools.cmp_to_key(compare))
|
||||
|
||||
# insert group end and page end indicators
|
||||
inGroup = False
|
||||
@@ -288,30 +270,39 @@ class DocParser(object):
|
||||
result = []
|
||||
|
||||
# paragraph
|
||||
(pos, pclass) = self.findinDoc('paragraph.class',start,end)
|
||||
(pos, pclass) = self.findinDoc(b'paragraph.class',start,end)
|
||||
|
||||
pclass = self.getClass(pclass)
|
||||
|
||||
# if paragraph uses extratokens (extra glyphs) then make it fixed
|
||||
(pos, extraglyphs) = self.findinDoc(b'paragraph.extratokens',start,end)
|
||||
|
||||
# build up a description of the paragraph in result and return it
|
||||
# first check for the basic - all words paragraph
|
||||
(pos, sfirst) = self.findinDoc('paragraph.firstWord',start,end)
|
||||
(pos, slast) = self.findinDoc('paragraph.lastWord',start,end)
|
||||
(pos, sfirst) = self.findinDoc(b'paragraph.firstWord',start,end)
|
||||
(pos, slast) = self.findinDoc(b'paragraph.lastWord',start,end)
|
||||
if (sfirst != None) and (slast != None) :
|
||||
first = int(sfirst)
|
||||
last = int(slast)
|
||||
|
||||
makeImage = (regtype == 'vertical') or (regtype == 'table')
|
||||
if self.fixedimage:
|
||||
makeImage = makeImage or (regtype == 'fixed')
|
||||
|
||||
if (pclass != None):
|
||||
makeImage = makeImage or (pclass.find('.inverted') >= 0)
|
||||
makeImage = (regtype == b'vertical') or (regtype == b'table')
|
||||
makeImage = makeImage or (extraglyphs != None)
|
||||
if self.fixedimage:
|
||||
makeImage = makeImage or (regtype == b'fixed')
|
||||
|
||||
if (pclass != None):
|
||||
makeImage = makeImage or (pclass.find(b'.inverted') >= 0)
|
||||
if self.fixedimage :
|
||||
makeImage = makeImage or (pclass.find('cl-f-') >= 0)
|
||||
makeImage = makeImage or (pclass.find(b'cl-f-') >= 0)
|
||||
|
||||
# before creating an image make sure glyph info exists
|
||||
gidList = self.getData(b'info.glyph.glyphID',0,-1)
|
||||
|
||||
makeImage = makeImage & (len(gidList) > 0)
|
||||
|
||||
if not makeImage :
|
||||
# standard all word paragraph
|
||||
for wordnum in xrange(first, last):
|
||||
for wordnum in range(first, last):
|
||||
result.append(('ocr', wordnum))
|
||||
return pclass, result
|
||||
|
||||
@@ -319,20 +310,29 @@ class DocParser(object):
|
||||
# translate first and last word into first and last glyphs
|
||||
# and generate inline image and include it
|
||||
glyphList = []
|
||||
firstglyphList = self.getData('word.firstGlyph',0,-1)
|
||||
gidList = self.getData('info.glyph.glyphID',0,-1)
|
||||
firstglyphList = self.getData(b'word.firstGlyph',0,-1)
|
||||
gidList = self.getData(b'info.glyph.glyphID',0,-1)
|
||||
firstGlyph = firstglyphList[first]
|
||||
if last < len(firstglyphList):
|
||||
lastGlyph = firstglyphList[last]
|
||||
else :
|
||||
lastGlyph = len(gidList)
|
||||
for glyphnum in xrange(firstGlyph, lastGlyph):
|
||||
|
||||
# handle case of white sapce paragraphs with no actual glyphs in them
|
||||
# by reverting to text based paragraph
|
||||
if firstGlyph >= lastGlyph:
|
||||
# revert to standard text based paragraph
|
||||
for wordnum in range(first, last):
|
||||
result.append(('ocr', wordnum))
|
||||
return pclass, result
|
||||
|
||||
for glyphnum in range(firstGlyph, lastGlyph):
|
||||
glyphList.append(glyphnum)
|
||||
# include any extratokens if they exist
|
||||
(pos, sfg) = self.findinDoc('extratokens.firstGlyph',start,end)
|
||||
(pos, slg) = self.findinDoc('extratokens.lastGlyph',start,end)
|
||||
(pos, sfg) = self.findinDoc(b'extratokens.firstGlyph',start,end)
|
||||
(pos, slg) = self.findinDoc(b'extratokens.lastGlyph',start,end)
|
||||
if (sfg != None) and (slg != None):
|
||||
for glyphnum in xrange(int(sfg), int(slg)):
|
||||
for glyphnum in range(int(sfg), int(slg)):
|
||||
glyphList.append(glyphnum)
|
||||
num = self.svgcount
|
||||
self.glyphs_to_image(glyphList)
|
||||
@@ -340,10 +340,10 @@ class DocParser(object):
|
||||
result.append(('svg', num))
|
||||
return pclass, result
|
||||
|
||||
# this type of paragraph may be made up of multiple spans, inline
|
||||
# word monograms (images), and words with semantic meaning,
|
||||
# this type of paragraph may be made up of multiple spans, inline
|
||||
# word monograms (images), and words with semantic meaning,
|
||||
# plus glyphs used to form starting letter of first word
|
||||
|
||||
|
||||
# need to parse this type line by line
|
||||
line = start + 1
|
||||
word_class = ''
|
||||
@@ -352,7 +352,7 @@ class DocParser(object):
|
||||
if end == -1 :
|
||||
end = self.docSize
|
||||
|
||||
# seems some xml has last* coming before first* so we have to
|
||||
# seems some xml has last* coming before first* so we have to
|
||||
# handle any order
|
||||
sp_first = -1
|
||||
sp_last = -1
|
||||
@@ -365,50 +365,56 @@ class DocParser(object):
|
||||
|
||||
word_class = ''
|
||||
|
||||
word_semantic_type = ''
|
||||
|
||||
while (line < end) :
|
||||
|
||||
(name, argres) = self.lineinDoc(line)
|
||||
|
||||
if name.endswith('span.firstWord') :
|
||||
if name.endswith(b'span.firstWord') :
|
||||
sp_first = int(argres)
|
||||
|
||||
elif name.endswith('span.lastWord') :
|
||||
elif name.endswith(b'span.lastWord') :
|
||||
sp_last = int(argres)
|
||||
|
||||
elif name.endswith('word.firstGlyph') :
|
||||
elif name.endswith(b'word.firstGlyph') :
|
||||
gl_first = int(argres)
|
||||
|
||||
elif name.endswith('word.lastGlyph') :
|
||||
elif name.endswith(b'word.lastGlyph') :
|
||||
gl_last = int(argres)
|
||||
|
||||
elif name.endswith('word_semantic.firstWord'):
|
||||
elif name.endswith(b'word_semantic.firstWord'):
|
||||
ws_first = int(argres)
|
||||
|
||||
elif name.endswith('word_semantic.lastWord'):
|
||||
elif name.endswith(b'word_semantic.lastWord'):
|
||||
ws_last = int(argres)
|
||||
|
||||
elif name.endswith('word.class'):
|
||||
(cname, space) = argres.split('-',1)
|
||||
if space == '' : space = '0'
|
||||
if (cname == 'spaceafter') and (int(space) > 0) :
|
||||
word_class = 'sa'
|
||||
elif name.endswith(b'word.class'):
|
||||
# we only handle spaceafter word class
|
||||
try:
|
||||
(cname, space) = argres.split(b'-',1)
|
||||
if space == b'' : space = b'0'
|
||||
if (cname == b'spaceafter') and (int(space) > 0) :
|
||||
word_class = 'sa'
|
||||
except:
|
||||
pass
|
||||
|
||||
elif name.endswith('word.img.src'):
|
||||
elif name.endswith(b'word.img.src'):
|
||||
result.append(('img' + word_class, int(argres)))
|
||||
word_class = ''
|
||||
|
||||
elif name.endswith('region.img.src'):
|
||||
elif name.endswith(b'region.img.src'):
|
||||
result.append(('img' + word_class, int(argres)))
|
||||
|
||||
if (sp_first != -1) and (sp_last != -1):
|
||||
for wordnum in xrange(sp_first, sp_last):
|
||||
for wordnum in range(sp_first, sp_last):
|
||||
result.append(('ocr', wordnum))
|
||||
sp_first = -1
|
||||
sp_last = -1
|
||||
|
||||
if (gl_first != -1) and (gl_last != -1):
|
||||
glyphList = []
|
||||
for glyphnum in xrange(gl_first, gl_last):
|
||||
for glyphnum in range(gl_first, gl_last):
|
||||
glyphList.append(glyphnum)
|
||||
num = self.svgcount
|
||||
self.glyphs_to_image(glyphList)
|
||||
@@ -418,15 +424,15 @@ class DocParser(object):
|
||||
gl_last = -1
|
||||
|
||||
if (ws_first != -1) and (ws_last != -1):
|
||||
for wordnum in xrange(ws_first, ws_last):
|
||||
for wordnum in range(ws_first, ws_last):
|
||||
result.append(('ocr', wordnum))
|
||||
ws_first = -1
|
||||
ws_last = -1
|
||||
|
||||
|
||||
line += 1
|
||||
|
||||
return pclass, result
|
||||
|
||||
|
||||
|
||||
def buildParagraph(self, pclass, pdesc, type, regtype) :
|
||||
parares = ''
|
||||
@@ -434,12 +440,12 @@ class DocParser(object):
|
||||
|
||||
classres = ''
|
||||
if pclass :
|
||||
classres = ' class="' + pclass + '"'
|
||||
classres = ' class="' + pclass.decode('utf-8') + '"'
|
||||
|
||||
br_lb = (regtype == 'fixed') or (regtype == 'chapterheading') or (regtype == 'vertical')
|
||||
|
||||
handle_links = len(self.link_id) > 0
|
||||
|
||||
|
||||
if (type == 'full') or (type == 'begin') :
|
||||
parares += '<p' + classres + '>'
|
||||
|
||||
@@ -450,12 +456,16 @@ class DocParser(object):
|
||||
|
||||
cnt = len(pdesc)
|
||||
|
||||
for j in xrange( 0, cnt) :
|
||||
for j in range( 0, cnt) :
|
||||
|
||||
(wtype, num) = pdesc[j]
|
||||
|
||||
if wtype == 'ocr' :
|
||||
word = self.ocrtext[num]
|
||||
try:
|
||||
word = self.ocrtext[num]
|
||||
except:
|
||||
word = ""
|
||||
|
||||
sep = ' '
|
||||
|
||||
if handle_links:
|
||||
@@ -463,45 +473,46 @@ class DocParser(object):
|
||||
if (link > 0):
|
||||
linktype = self.link_type[link-1]
|
||||
title = self.link_title[link-1]
|
||||
if (title == "") or (parares.rfind(title) < 0):
|
||||
title=parares[lstart:]
|
||||
if (title == b"") or (parares.rfind(title.decode('utf-8')) < 0):
|
||||
title=parares[lstart:].encode('utf-8')
|
||||
if linktype == 'external' :
|
||||
linkhref = self.link_href[link-1]
|
||||
linkhtml = '<a href="%s">' % linkhref
|
||||
else :
|
||||
else :
|
||||
if len(self.link_page) >= link :
|
||||
ptarget = self.link_page[link-1] - 1
|
||||
linkhtml = '<a href="#page%04d">' % ptarget
|
||||
else :
|
||||
# just link to the current page
|
||||
linkhtml = '<a href="#' + self.id + '">'
|
||||
linkhtml += title + '</a>'
|
||||
pos = parares.rfind(title)
|
||||
linkhtml += title.decode('utf-8')
|
||||
linkhtml += '</a>'
|
||||
pos = parares.rfind(title.decode('utf-8'))
|
||||
if pos >= 0:
|
||||
parares = parares[0:pos] + linkhtml + parares[pos+len(title):]
|
||||
else :
|
||||
parares += linkhtml
|
||||
lstart = len(parares)
|
||||
if word == '_link_' : word = ''
|
||||
if word == b'_link_' : word = b''
|
||||
elif (link < 0) :
|
||||
if word == '_link_' : word = ''
|
||||
if word == b'_link_' : word = b''
|
||||
|
||||
if word == '_lb_':
|
||||
if word == b'_lb_':
|
||||
if ((num-1) in self.dehyphen_rootid ) or handle_links:
|
||||
word = ''
|
||||
word = b''
|
||||
sep = ''
|
||||
elif br_lb :
|
||||
word = '<br />\n'
|
||||
word = b'<br />\n'
|
||||
sep = ''
|
||||
else :
|
||||
word = '\n'
|
||||
word = b'\n'
|
||||
sep = ''
|
||||
|
||||
if num in self.dehyphen_rootid :
|
||||
word = word[0:-1]
|
||||
sep = ''
|
||||
|
||||
parares += word + sep
|
||||
parares += word.decode('utf-8') + sep
|
||||
|
||||
elif wtype == 'img' :
|
||||
sep = ''
|
||||
@@ -515,7 +526,9 @@ class DocParser(object):
|
||||
|
||||
elif wtype == 'svg' :
|
||||
sep = ''
|
||||
parares += '<img src="img/' + self.id + '_%04d.svg" alt="" />' % num
|
||||
parares += '<img src="img/'
|
||||
parares += self.id
|
||||
parares += '_%04d.svg" alt="" />' % num
|
||||
parares += sep
|
||||
|
||||
if len(sep) > 0 : parares = parares[0:-1]
|
||||
@@ -524,115 +537,182 @@ class DocParser(object):
|
||||
return parares
|
||||
|
||||
|
||||
|
||||
def buildTOCEntry(self, pdesc) :
|
||||
parares = ''
|
||||
sep =''
|
||||
tocentry = ''
|
||||
handle_links = len(self.link_id) > 0
|
||||
|
||||
lstart = 0
|
||||
|
||||
cnt = len(pdesc)
|
||||
for j in range( 0, cnt) :
|
||||
|
||||
(wtype, num) = pdesc[j]
|
||||
|
||||
if wtype == 'ocr' :
|
||||
word = self.ocrtext[num].decode('utf-8')
|
||||
sep = ' '
|
||||
|
||||
if handle_links:
|
||||
link = self.link_id[num]
|
||||
if (link > 0):
|
||||
linktype = self.link_type[link-1]
|
||||
title = self.link_title[link-1]
|
||||
title = title.rstrip(b'. ')
|
||||
alt_title = parares[lstart:]
|
||||
alt_title = alt_title.strip()
|
||||
# now strip off the actual printed page number
|
||||
alt_title = alt_title.rstrip('01234567890ivxldIVXLD-.')
|
||||
alt_title = alt_title.rstrip('. ')
|
||||
# skip over any external links - can't have them in a books toc
|
||||
if linktype == 'external' :
|
||||
title = ''
|
||||
alt_title = ''
|
||||
linkpage = ''
|
||||
else :
|
||||
if len(self.link_page) >= link :
|
||||
ptarget = self.link_page[link-1] - 1
|
||||
linkpage = '%04d' % ptarget
|
||||
else :
|
||||
# just link to the current page
|
||||
linkpage = self.id[4:]
|
||||
if len(alt_title) >= len(title):
|
||||
title = alt_title
|
||||
if title != '' and linkpage != '':
|
||||
tocentry += title + '|' + linkpage + '\n'
|
||||
lstart = len(parares)
|
||||
if word == '_link_' : word = ''
|
||||
elif (link < 0) :
|
||||
if word == '_link_' : word = ''
|
||||
|
||||
if word == '_lb_':
|
||||
word = ''
|
||||
sep = ''
|
||||
|
||||
if num in self.dehyphen_rootid :
|
||||
word = word[0:-1]
|
||||
sep = ''
|
||||
|
||||
parares += word + sep
|
||||
|
||||
else :
|
||||
continue
|
||||
|
||||
return tocentry
|
||||
|
||||
|
||||
|
||||
|
||||
# walk the document tree collecting the information needed
|
||||
# to build an html page using the ocrText
|
||||
|
||||
def process(self):
|
||||
|
||||
htmlpage = ''
|
||||
tocinfo = ''
|
||||
hlst = []
|
||||
|
||||
# get the ocr text
|
||||
(pos, argres) = self.findinDoc('info.word.ocrText',0,-1)
|
||||
if argres : self.ocrtext = argres.split('|')
|
||||
(pos, argres) = self.findinDoc(b'info.word.ocrText',0,-1)
|
||||
if argres : self.ocrtext = argres.split(b'|')
|
||||
|
||||
# get information to dehyphenate the text
|
||||
self.dehyphen_rootid = self.getData('info.dehyphen.rootID',0,-1)
|
||||
self.dehyphen_rootid = self.getData(b'info.dehyphen.rootID',0,-1)
|
||||
|
||||
# determine if first paragraph is continued from previous page
|
||||
(pos, self.parastems_stemid) = self.findinDoc('info.paraStems.stemID',0,-1)
|
||||
first_para_continued = (self.parastems_stemid != None)
|
||||
|
||||
(pos, self.parastems_stemid) = self.findinDoc(b'info.paraStems.stemID',0,-1)
|
||||
first_para_continued = (self.parastems_stemid != None)
|
||||
|
||||
# determine if last paragraph is continued onto the next page
|
||||
(pos, self.paracont_stemid) = self.findinDoc('info.paraCont.stemID',0,-1)
|
||||
(pos, self.paracont_stemid) = self.findinDoc(b'info.paraCont.stemID',0,-1)
|
||||
last_para_continued = (self.paracont_stemid != None)
|
||||
|
||||
# collect link ids
|
||||
self.link_id = self.getData('info.word.link_id',0,-1)
|
||||
self.link_id = self.getData(b'info.word.link_id',0,-1)
|
||||
|
||||
# collect link destination page numbers
|
||||
self.link_page = self.getData('info.links.page',0,-1)
|
||||
self.link_page = self.getData(b'info.links.page',0,-1)
|
||||
|
||||
# collect link types (container versus external)
|
||||
(pos, argres) = self.findinDoc('info.links.type',0,-1)
|
||||
if argres : self.link_type = argres.split('|')
|
||||
(pos, argres) = self.findinDoc(b'info.links.type',0,-1)
|
||||
if argres : self.link_type = argres.split(b'|')
|
||||
|
||||
# collect link destinations
|
||||
(pos, argres) = self.findinDoc('info.links.href',0,-1)
|
||||
if argres : self.link_href = argres.split('|')
|
||||
(pos, argres) = self.findinDoc(b'info.links.href',0,-1)
|
||||
if argres : self.link_href = argres.split(b'|')
|
||||
|
||||
# collect link titles
|
||||
(pos, argres) = self.findinDoc('info.links.title',0,-1)
|
||||
(pos, argres) = self.findinDoc(b'info.links.title',0,-1)
|
||||
if argres :
|
||||
self.link_title = argres.split('|')
|
||||
self.link_title = argres.split(b'|')
|
||||
else:
|
||||
self.link_title.append('')
|
||||
|
||||
# get a descriptions of the starting points of the regions
|
||||
# and groups on the page
|
||||
(pagetype, pageDesc) = self.PageDescription()
|
||||
(pagetype, pageDesc) = self.PageDescription()
|
||||
regcnt = len(pageDesc) - 1
|
||||
|
||||
anchorSet = False
|
||||
breakSet = False
|
||||
inGroup = False
|
||||
|
||||
|
||||
# process each region on the page and convert what you can to html
|
||||
|
||||
for j in xrange(regcnt):
|
||||
for j in range(regcnt):
|
||||
|
||||
(etype, start) = pageDesc[j]
|
||||
(ntype, end) = pageDesc[j+1]
|
||||
|
||||
|
||||
|
||||
# set anchor for link target on this page
|
||||
if not anchorSet and not first_para_continued:
|
||||
htmlpage += '<div style="visibility: hidden; height: 0; width: 0;" id="'
|
||||
htmlpage += self.id + '" title="pagetype_' + pagetype + '"></div>\n'
|
||||
hlst.append('<div style="visibility: hidden; height: 0; width: 0;" id="')
|
||||
hlst.append(self.id + '" title="pagetype_' + pagetype.decode('utf-8') + '"></div>\n')
|
||||
anchorSet = True
|
||||
|
||||
# handle groups of graphics with text captions
|
||||
if (etype == 'grpbeg'):
|
||||
(pos, grptype) = self.findinDoc('group.type', start, end)
|
||||
if (etype == b'grpbeg'):
|
||||
(pos, grptype) = self.findinDoc(b'group.type', start, end)
|
||||
if grptype != None:
|
||||
if grptype == 'graphic':
|
||||
gcstr = ' class="' + grptype + '"'
|
||||
htmlpage += '<div' + gcstr + '>'
|
||||
if grptype == b'graphic':
|
||||
gcstr = ' class="' + grptype.decode('utf-8') + '"'
|
||||
hlst.append('<div' + gcstr + '>')
|
||||
inGroup = True
|
||||
|
||||
elif (etype == 'grpend'):
|
||||
|
||||
elif (etype == b'grpend'):
|
||||
if inGroup:
|
||||
htmlpage += '</div>\n'
|
||||
hlst.append('</div>\n')
|
||||
inGroup = False
|
||||
|
||||
else:
|
||||
(pos, regtype) = self.findinDoc('region.type',start,end)
|
||||
(pos, regtype) = self.findinDoc(b'region.type',start,end)
|
||||
|
||||
if regtype == 'graphic' :
|
||||
(pos, simgsrc) = self.findinDoc('img.src',start,end)
|
||||
if regtype == b'graphic' :
|
||||
(pos, simgsrc) = self.findinDoc(b'img.src',start,end)
|
||||
if simgsrc:
|
||||
if inGroup:
|
||||
htmlpage += '<img src="img/img%04d.jpg" alt="" />' % int(simgsrc)
|
||||
hlst.append('<img src="img/img%04d.jpg" alt="" />' % int(simgsrc))
|
||||
else:
|
||||
htmlpage += '<div class="graphic"><img src="img/img%04d.jpg" alt="" /></div>' % int(simgsrc)
|
||||
|
||||
elif regtype == 'chapterheading' :
|
||||
hlst.append('<div class="graphic"><img src="img/img%04d.jpg" alt="" /></div>' % int(simgsrc))
|
||||
|
||||
elif regtype == b'chapterheading' :
|
||||
(pclass, pdesc) = self.getParaDescription(start,end, regtype)
|
||||
if not breakSet:
|
||||
htmlpage += '<div style="page-break-after: always;"> </div>\n'
|
||||
hlst.append('<div style="page-break-after: always;"> </div>\n')
|
||||
breakSet = True
|
||||
tag = 'h1'
|
||||
if pclass and (len(pclass) >= 7):
|
||||
if pclass[3:7] == 'ch1-' : tag = 'h1'
|
||||
if pclass[3:7] == 'ch2-' : tag = 'h2'
|
||||
if pclass[3:7] == 'ch3-' : tag = 'h3'
|
||||
htmlpage += '<' + tag + ' class="' + pclass + '">'
|
||||
if pclass[3:7] == b'ch1-' : tag = 'h1'
|
||||
if pclass[3:7] == b'ch2-' : tag = 'h2'
|
||||
if pclass[3:7] == b'ch3-' : tag = 'h3'
|
||||
hlst.append('<' + tag + ' class="' + pclass.decode('utf-8') + '">')
|
||||
else:
|
||||
htmlpage += '<' + tag + '>'
|
||||
htmlpage += self.buildParagraph(pclass, pdesc, 'middle', regtype)
|
||||
htmlpage += '</' + tag + '>'
|
||||
hlst.append('<' + tag + '>')
|
||||
hlst.append(self.buildParagraph(pclass, pdesc, 'middle', regtype))
|
||||
hlst.append('</' + tag + '>')
|
||||
|
||||
elif (regtype == 'text') or (regtype == 'fixed') or (regtype == 'insert') or (regtype == 'listitem'):
|
||||
elif (regtype == b'text') or (regtype == b'fixed') or (regtype == b'insert') or (regtype == b'listitem'):
|
||||
ptype = 'full'
|
||||
# check to see if this is a continution from the previous page
|
||||
if first_para_continued :
|
||||
@@ -641,25 +721,25 @@ class DocParser(object):
|
||||
(pclass, pdesc) = self.getParaDescription(start,end, regtype)
|
||||
if pclass and (len(pclass) >= 6) and (ptype == 'full'):
|
||||
tag = 'p'
|
||||
if pclass[3:6] == 'h1-' : tag = 'h4'
|
||||
if pclass[3:6] == 'h2-' : tag = 'h5'
|
||||
if pclass[3:6] == 'h3-' : tag = 'h6'
|
||||
htmlpage += '<' + tag + ' class="' + pclass + '">'
|
||||
htmlpage += self.buildParagraph(pclass, pdesc, 'middle', regtype)
|
||||
htmlpage += '</' + tag + '>'
|
||||
if pclass[3:6] == b'h1-' : tag = 'h4'
|
||||
if pclass[3:6] == b'h2-' : tag = 'h5'
|
||||
if pclass[3:6] == b'h3-' : tag = 'h6'
|
||||
hlst.append('<' + tag + ' class="' + pclass.decode('utf-8') + '">')
|
||||
hlst.append(self.buildParagraph(pclass, pdesc, 'middle', regtype))
|
||||
hlst.append('</' + tag + '>')
|
||||
else :
|
||||
htmlpage += self.buildParagraph(pclass, pdesc, ptype, regtype)
|
||||
hlst.append(self.buildParagraph(pclass, pdesc, ptype, regtype))
|
||||
|
||||
elif (regtype == 'tocentry') :
|
||||
elif (regtype == b'tocentry') :
|
||||
ptype = 'full'
|
||||
if first_para_continued :
|
||||
ptype = 'end'
|
||||
first_para_continued = False
|
||||
(pclass, pdesc) = self.getParaDescription(start,end, regtype)
|
||||
htmlpage += self.buildParagraph(pclass, pdesc, ptype, regtype)
|
||||
tocinfo += self.buildTOCEntry(pdesc)
|
||||
hlst.append(self.buildParagraph(pclass, pdesc, ptype, regtype))
|
||||
|
||||
|
||||
elif (regtype == 'vertical') or (regtype == 'table') :
|
||||
elif (regtype == b'vertical') or (regtype == b'table') :
|
||||
ptype = 'full'
|
||||
if inGroup:
|
||||
ptype = 'middle'
|
||||
@@ -667,22 +747,22 @@ class DocParser(object):
|
||||
ptype = 'end'
|
||||
first_para_continued = False
|
||||
(pclass, pdesc) = self.getParaDescription(start, end, regtype)
|
||||
htmlpage += self.buildParagraph(pclass, pdesc, ptype, regtype)
|
||||
hlst.append(self.buildParagraph(pclass, pdesc, ptype, regtype))
|
||||
|
||||
|
||||
elif (regtype == 'synth_fcvr.center'):
|
||||
(pos, simgsrc) = self.findinDoc('img.src',start,end)
|
||||
elif (regtype == b'synth_fcvr.center'):
|
||||
(pos, simgsrc) = self.findinDoc(b'img.src',start,end)
|
||||
if simgsrc:
|
||||
htmlpage += '<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 :
|
||||
print ' Making region type', regtype,
|
||||
(pos, temp) = self.findinDoc('paragraph',start,end)
|
||||
(pos2, temp) = self.findinDoc('span',start,end)
|
||||
print(' Making region type', regtype, end=' ')
|
||||
(pos, temp) = self.findinDoc(b'paragraph',start,end)
|
||||
(pos2, temp) = self.findinDoc(b'span',start,end)
|
||||
if pos != -1 or pos2 != -1:
|
||||
print ' a "text" region'
|
||||
print(' a "text" region')
|
||||
orig_regtype = regtype
|
||||
regtype = 'fixed'
|
||||
regtype = b'fixed'
|
||||
ptype = 'full'
|
||||
# check to see if this is a continution from the previous page
|
||||
if first_para_continued :
|
||||
@@ -690,41 +770,38 @@ class DocParser(object):
|
||||
first_para_continued = False
|
||||
(pclass, pdesc) = self.getParaDescription(start,end, regtype)
|
||||
if not pclass:
|
||||
if orig_regtype.endswith('.right') : pclass = 'cl-right'
|
||||
elif orig_regtype.endswith('.center') : pclass = 'cl-center'
|
||||
elif orig_regtype.endswith('.left') : pclass = 'cl-left'
|
||||
elif orig_regtype.endswith('.justify') : pclass = 'cl-justify'
|
||||
if orig_regtype.endswith(b'.right') : pclass = 'cl-right'
|
||||
elif orig_regtype.endswith(b'.center') : pclass = 'cl-center'
|
||||
elif orig_regtype.endswith(b'.left') : pclass = 'cl-left'
|
||||
elif orig_regtype.endswith(b'.justify') : pclass = 'cl-justify'
|
||||
if pclass and (ptype == 'full') and (len(pclass) >= 6):
|
||||
tag = 'p'
|
||||
if pclass[3:6] == 'h1-' : tag = 'h4'
|
||||
if pclass[3:6] == 'h2-' : tag = 'h5'
|
||||
if pclass[3:6] == 'h3-' : tag = 'h6'
|
||||
htmlpage += '<' + tag + ' class="' + pclass + '">'
|
||||
htmlpage += self.buildParagraph(pclass, pdesc, 'middle', regtype)
|
||||
htmlpage += '</' + tag + '>'
|
||||
if pclass[3:6] == b'h1-' : tag = 'h4'
|
||||
if pclass[3:6] == b'h2-' : tag = 'h5'
|
||||
if pclass[3:6] == b'h3-' : tag = 'h6'
|
||||
hlst.append('<' + tag + ' class="' + pclass.decode('utf-8') + '">')
|
||||
hlst.append(self.buildParagraph(pclass, pdesc, 'middle', regtype))
|
||||
hlst.append('</' + tag + '>')
|
||||
else :
|
||||
htmlpage += self.buildParagraph(pclass, pdesc, ptype, regtype)
|
||||
hlst.append(self.buildParagraph(pclass, pdesc, ptype, regtype))
|
||||
else :
|
||||
print ' a "graphic" region'
|
||||
(pos, simgsrc) = self.findinDoc('img.src',start,end)
|
||||
print(' a "graphic" region')
|
||||
(pos, simgsrc) = self.findinDoc(b'img.src',start,end)
|
||||
if simgsrc:
|
||||
htmlpage += '<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))
|
||||
|
||||
|
||||
htmlpage = "".join(hlst)
|
||||
if last_para_continued :
|
||||
if htmlpage[-4:] == '</p>':
|
||||
htmlpage = htmlpage[0:-4]
|
||||
last_para_continued = False
|
||||
|
||||
return htmlpage
|
||||
return htmlpage, tocinfo
|
||||
|
||||
|
||||
|
||||
def convert2HTML(flatxml, classlst, fileid, bookDir, fixedimage):
|
||||
|
||||
def convert2HTML(flatxml, classlst, fileid, bookDir, gdict, fixedimage):
|
||||
# create a document parser
|
||||
dp = DocParser(flatxml, classlst, fileid, bookDir, fixedimage)
|
||||
|
||||
htmlpage = dp.process()
|
||||
|
||||
return htmlpage
|
||||
dp = DocParser(flatxml, classlst, fileid, bookDir, gdict, fixedimage)
|
||||
htmlpage, tocinfo = dp.process()
|
||||
return htmlpage, tocinfo
|
||||
255
DeDRM_plugin/flatxml2svg.py
Normal file
255
DeDRM_plugin/flatxml2svg.py
Normal file
@@ -0,0 +1,255 @@
|
||||
#! /usr/bin/python
|
||||
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
|
||||
|
||||
import sys
|
||||
import csv
|
||||
import os
|
||||
import getopt
|
||||
from struct import pack
|
||||
from struct import unpack
|
||||
|
||||
|
||||
class PParser(object):
|
||||
def __init__(self, gd, flatxml, meta_array):
|
||||
self.gd = gd
|
||||
self.flatdoc = flatxml.split(b'\n')
|
||||
self.docSize = len(self.flatdoc)
|
||||
self.temp = []
|
||||
|
||||
self.ph = -1
|
||||
self.pw = -1
|
||||
startpos = self.posinDoc('page.h') or self.posinDoc('book.h')
|
||||
for p in startpos:
|
||||
(name, argres) = self.lineinDoc(p)
|
||||
self.ph = max(self.ph, int(argres))
|
||||
startpos = self.posinDoc('page.w') or self.posinDoc('book.w')
|
||||
for p in startpos:
|
||||
(name, argres) = self.lineinDoc(p)
|
||||
self.pw = max(self.pw, int(argres))
|
||||
|
||||
if self.ph <= 0:
|
||||
self.ph = int(meta_array.get('pageHeight', '11000'))
|
||||
if self.pw <= 0:
|
||||
self.pw = int(meta_array.get('pageWidth', '8500'))
|
||||
|
||||
res = []
|
||||
startpos = self.posinDoc('info.glyph.x')
|
||||
for p in startpos:
|
||||
argres = self.getDataatPos('info.glyph.x', p)
|
||||
res.extend(argres)
|
||||
self.gx = res
|
||||
|
||||
res = []
|
||||
startpos = self.posinDoc('info.glyph.y')
|
||||
for p in startpos:
|
||||
argres = self.getDataatPos('info.glyph.y', p)
|
||||
res.extend(argres)
|
||||
self.gy = res
|
||||
|
||||
res = []
|
||||
startpos = self.posinDoc('info.glyph.glyphID')
|
||||
for p in startpos:
|
||||
argres = self.getDataatPos('info.glyph.glyphID', p)
|
||||
res.extend(argres)
|
||||
self.gid = res
|
||||
|
||||
|
||||
# return tag at line pos in document
|
||||
def lineinDoc(self, pos) :
|
||||
if (pos >= 0) and (pos < self.docSize) :
|
||||
item = self.flatdoc[pos]
|
||||
if item.find(b'=') >= 0:
|
||||
(name, argres) = item.split(b'=',1)
|
||||
else :
|
||||
name = item
|
||||
argres = b''
|
||||
return name, argres
|
||||
|
||||
# find tag in doc if within pos to end inclusive
|
||||
def findinDoc(self, tagpath, pos, end) :
|
||||
result = None
|
||||
if end == -1 :
|
||||
end = self.docSize
|
||||
else:
|
||||
end = min(self.docSize, end)
|
||||
foundat = -1
|
||||
for j in range(pos, end):
|
||||
item = self.flatdoc[j]
|
||||
if item.find(b'=') >= 0:
|
||||
(name, argres) = item.split(b'=',1)
|
||||
else :
|
||||
name = item
|
||||
argres = b''
|
||||
if (isinstance(tagpath,str)):
|
||||
tagpath = tagpath.encode('utf-8')
|
||||
if name.endswith(tagpath) :
|
||||
result = argres
|
||||
foundat = j
|
||||
break
|
||||
return foundat, result
|
||||
|
||||
# return list of start positions for the tagpath
|
||||
def posinDoc(self, tagpath):
|
||||
startpos = []
|
||||
pos = 0
|
||||
res = ""
|
||||
while res != None :
|
||||
(foundpos, res) = self.findinDoc(tagpath, pos, -1)
|
||||
if res != None :
|
||||
startpos.append(foundpos)
|
||||
pos = foundpos + 1
|
||||
return startpos
|
||||
|
||||
def getData(self, path):
|
||||
result = None
|
||||
cnt = len(self.flatdoc)
|
||||
for j in range(cnt):
|
||||
item = self.flatdoc[j]
|
||||
if item.find(b'=') >= 0:
|
||||
(name, argt) = item.split(b'=')
|
||||
argres = argt.split(b'|')
|
||||
else:
|
||||
name = item
|
||||
argres = []
|
||||
if (name.endswith(path)):
|
||||
result = argres
|
||||
break
|
||||
if (len(argres) > 0) :
|
||||
for j in range(0,len(argres)):
|
||||
argres[j] = int(argres[j])
|
||||
return result
|
||||
|
||||
def getDataatPos(self, path, pos):
|
||||
result = None
|
||||
item = self.flatdoc[pos]
|
||||
if item.find(b'=') >= 0:
|
||||
(name, argt) = item.split(b'=')
|
||||
argres = argt.split(b'|')
|
||||
else:
|
||||
name = item
|
||||
argres = []
|
||||
if (len(argres) > 0) :
|
||||
for j in range(0,len(argres)):
|
||||
argres[j] = int(argres[j])
|
||||
if (isinstance(path,str)):
|
||||
path = path.encode('utf-8')
|
||||
if (name.endswith(path)):
|
||||
result = argres
|
||||
return result
|
||||
|
||||
def getDataTemp(self, path):
|
||||
result = None
|
||||
cnt = len(self.temp)
|
||||
for j in range(cnt):
|
||||
item = self.temp[j]
|
||||
if item.find(b'=') >= 0:
|
||||
(name, argt) = item.split(b'=')
|
||||
argres = argt.split(b'|')
|
||||
else:
|
||||
name = item
|
||||
argres = []
|
||||
if (isinstance(path,str)):
|
||||
path = path.encode('utf-8')
|
||||
if (name.endswith(path)):
|
||||
result = argres
|
||||
self.temp.pop(j)
|
||||
break
|
||||
if (len(argres) > 0) :
|
||||
for j in range(0,len(argres)):
|
||||
argres[j] = int(argres[j])
|
||||
return result
|
||||
|
||||
def getImages(self):
|
||||
result = []
|
||||
self.temp = self.flatdoc
|
||||
while (self.getDataTemp('img') != None):
|
||||
h = self.getDataTemp('img.h')[0]
|
||||
w = self.getDataTemp('img.w')[0]
|
||||
x = self.getDataTemp('img.x')[0]
|
||||
y = self.getDataTemp('img.y')[0]
|
||||
src = self.getDataTemp('img.src')[0]
|
||||
result.append('<image xlink:href="../img/img%04d.jpg" x="%d" y="%d" width="%d" height="%d" />\n' % (src, x, y, w, h))
|
||||
return result
|
||||
|
||||
def getGlyphs(self):
|
||||
result = []
|
||||
if (self.gid != None) and (len(self.gid) > 0):
|
||||
glyphs = []
|
||||
for j in set(self.gid):
|
||||
glyphs.append(j)
|
||||
glyphs.sort()
|
||||
for gid in glyphs:
|
||||
id='id="gl%d"' % gid
|
||||
path = self.gd.lookup(id)
|
||||
if path:
|
||||
result.append(id + ' ' + path)
|
||||
return result
|
||||
|
||||
|
||||
def convert2SVG(gdict, flat_xml, pageid, previd, nextid, svgDir, raw, meta_array, scaledpi):
|
||||
mlst = []
|
||||
pp = PParser(gdict, flat_xml, meta_array)
|
||||
mlst.append('<?xml version="1.0" standalone="no"?>\n')
|
||||
if (raw):
|
||||
mlst.append('<!DOCTYPE svg PUBLIC "-//W3C/DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n')
|
||||
mlst.append('<svg width="%fin" height="%fin" viewBox="0 0 %d %d" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">\n' % (pp.pw / scaledpi, pp.ph / scaledpi, pp.pw -1, pp.ph -1))
|
||||
mlst.append('<title>Page %d - %s by %s</title>\n' % (pageid, meta_array['Title'],meta_array['Authors']))
|
||||
else:
|
||||
mlst.append('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">\n')
|
||||
mlst.append('<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" ><head>\n')
|
||||
mlst.append('<title>Page %d - %s by %s</title>\n' % (pageid, meta_array['Title'],meta_array['Authors']))
|
||||
mlst.append('<script><![CDATA[\n')
|
||||
mlst.append('function gd(){var p=window.location.href.replace(/^.*\?dpi=(\d+).*$/i,"$1");return p;}\n')
|
||||
mlst.append('var dpi=%d;\n' % scaledpi)
|
||||
if (previd) :
|
||||
mlst.append('var prevpage="page%04d.xhtml";\n' % (previd))
|
||||
if (nextid) :
|
||||
mlst.append('var nextpage="page%04d.xhtml";\n' % (nextid))
|
||||
mlst.append('var pw=%d;var ph=%d;' % (pp.pw, pp.ph))
|
||||
mlst.append('function zoomin(){dpi=dpi*(0.8);setsize();}\n')
|
||||
mlst.append('function zoomout(){dpi=dpi*1.25;setsize();}\n')
|
||||
mlst.append('function setsize(){var svg=document.getElementById("svgimg");var prev=document.getElementById("prevsvg");var next=document.getElementById("nextsvg");var width=(pw/dpi)+"in";var height=(ph/dpi)+"in";svg.setAttribute("width",width);svg.setAttribute("height",height);prev.setAttribute("height",height);prev.setAttribute("width","50px");next.setAttribute("height",height);next.setAttribute("width","50px");}\n')
|
||||
mlst.append('function ppage(){window.location.href=prevpage+"?dpi="+Math.round(dpi);}\n')
|
||||
mlst.append('function npage(){window.location.href=nextpage+"?dpi="+Math.round(dpi);}\n')
|
||||
mlst.append('var gt=gd();if(gt>0){dpi=gt;}\n')
|
||||
mlst.append('window.onload=setsize;\n')
|
||||
mlst.append(']]></script>\n')
|
||||
mlst.append('</head>\n')
|
||||
mlst.append('<body onLoad="setsize();" style="background-color:#777;text-align:center;">\n')
|
||||
mlst.append('<div style="white-space:nowrap;">\n')
|
||||
if previd == None:
|
||||
mlst.append('<a href="javascript:ppage();"><svg id="prevsvg" viewBox="0 0 100 300" xmlns="http://www.w3.org/2000/svg" version="1.1" style="background-color:#777"></svg></a>\n')
|
||||
else:
|
||||
mlst.append('<a href="javascript:ppage();"><svg id="prevsvg" viewBox="0 0 100 300" xmlns="http://www.w3.org/2000/svg" version="1.1" style="background-color:#777"><polygon points="5,150,95,5,95,295" fill="#AAAAAA" /></svg></a>\n')
|
||||
|
||||
mlst.append('<a href="javascript:npage();"><svg id="svgimg" viewBox="0 0 %d %d" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" style="background-color:#FFF;border:1px solid black;">' % (pp.pw, pp.ph))
|
||||
if (pp.gid != None):
|
||||
mlst.append('<defs>\n')
|
||||
gdefs = pp.getGlyphs()
|
||||
for j in range(0,len(gdefs)):
|
||||
mlst.append(gdefs[j])
|
||||
mlst.append('</defs>\n')
|
||||
img = pp.getImages()
|
||||
if (img != None):
|
||||
for j in range(0,len(img)):
|
||||
mlst.append(img[j])
|
||||
if (pp.gid != None):
|
||||
for j in range(0,len(pp.gid)):
|
||||
mlst.append('<use xlink:href="#gl%d" x="%d" y="%d" />\n' % (pp.gid[j], pp.gx[j], pp.gy[j]))
|
||||
if (img == None or len(img) == 0) and (pp.gid == None or len(pp.gid) == 0):
|
||||
xpos = "%d" % (pp.pw // 3)
|
||||
ypos = "%d" % (pp.ph // 3)
|
||||
mlst.append('<text x="' + xpos + '" y="' + ypos + '" font-size="' + meta_array['fontSize'] + '" font-family="Helvetica" stroke="black">This page intentionally left blank.</text>\n')
|
||||
if (raw) :
|
||||
mlst.append('</svg>')
|
||||
else :
|
||||
mlst.append('</svg></a>\n')
|
||||
if nextid == None:
|
||||
mlst.append('<a href="javascript:npage();"><svg id="nextsvg" viewBox="0 0 100 300" xmlns="http://www.w3.org/2000/svg" version="1.1" style="background-color:#777"></svg></a>\n')
|
||||
else :
|
||||
mlst.append('<a href="javascript:npage();"><svg id="nextsvg" viewBox="0 0 100 300" xmlns="http://www.w3.org/2000/svg" version="1.1" style="background-color:#777"><polygon points="5,5,5,295,95,150" fill="#AAAAAA" /></svg></a>\n')
|
||||
mlst.append('</div>\n')
|
||||
mlst.append('<div><a href="javascript:zoomin();">zoom in</a> - <a href="javascript:zoomout();">zoom out</a></div>\n')
|
||||
mlst.append('</body>\n')
|
||||
mlst.append('</html>\n')
|
||||
return "".join(mlst)
|
||||
733
DeDRM_plugin/genbook.py
Normal file
733
DeDRM_plugin/genbook.py
Normal file
@@ -0,0 +1,733 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
|
||||
# Python 3 for calibre 5.0
|
||||
from __future__ import print_function
|
||||
|
||||
# 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):
|
||||
data = data.encode(self.encoding,"replace")
|
||||
self.stream.buffer.write(data)
|
||||
self.stream.buffer.flush()
|
||||
|
||||
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
|
||||
|
||||
# local support routines
|
||||
if 'calibre' in sys.modules:
|
||||
inCalibre = True
|
||||
else:
|
||||
inCalibre = False
|
||||
|
||||
if inCalibre :
|
||||
from calibre_plugins.dedrm import convert2xml
|
||||
from calibre_plugins.dedrm import flatxml2html
|
||||
from calibre_plugins.dedrm import flatxml2svg
|
||||
from calibre_plugins.dedrm import stylexml2css
|
||||
else :
|
||||
import convert2xml
|
||||
import flatxml2html
|
||||
import flatxml2svg
|
||||
import stylexml2css
|
||||
|
||||
# global switch
|
||||
buildXML = False
|
||||
|
||||
# Get a 7 bit encoded number from a file
|
||||
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 = ord(c)
|
||||
datax = (datax <<7) + (data & 0x7F)
|
||||
data = datax
|
||||
if flag:
|
||||
data = -data
|
||||
return data
|
||||
|
||||
# Get 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 None
|
||||
sv = file.read(stringLength)
|
||||
if (len(sv) != stringLength):
|
||||
return ""
|
||||
return unpack(str(stringLength)+"s",sv)[0]
|
||||
|
||||
def getMetaArray(metaFile):
|
||||
# parse the meta file
|
||||
result = {}
|
||||
fo = open(metaFile,'rb')
|
||||
size = readEncodedNumber(fo)
|
||||
for i in range(size):
|
||||
tag = readString(fo)
|
||||
value = readString(fo)
|
||||
result[tag] = value
|
||||
# print(tag, value)
|
||||
fo.close()
|
||||
return result
|
||||
|
||||
|
||||
# dictionary of all text strings by index value
|
||||
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(b'&',b'&')
|
||||
str = str.replace(b'<',b'<')
|
||||
str = str.replace(b'>',b'>')
|
||||
str = str.replace(b'=',b'=')
|
||||
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 or string table limits')
|
||||
# sys.exit(-1)
|
||||
def getSize(self):
|
||||
return self.size
|
||||
def getPos(self):
|
||||
return self.pos
|
||||
|
||||
|
||||
class PageDimParser(object):
|
||||
def __init__(self, flatxml):
|
||||
self.flatdoc = flatxml.split(b'\n')
|
||||
# find tag if within pos to end inclusive
|
||||
def findinDoc(self, tagpath, pos, end) :
|
||||
result = None
|
||||
docList = self.flatdoc
|
||||
cnt = len(docList)
|
||||
if end == -1 :
|
||||
end = cnt
|
||||
else:
|
||||
end = min(cnt,end)
|
||||
foundat = -1
|
||||
for j in range(pos, end):
|
||||
item = docList[j]
|
||||
if item.find(b'=') >= 0:
|
||||
(name, argres) = item.split(b'=')
|
||||
else :
|
||||
name = item
|
||||
argres = ''
|
||||
if name.endswith(tagpath) :
|
||||
result = argres
|
||||
foundat = j
|
||||
break
|
||||
return foundat, result
|
||||
def process(self):
|
||||
(pos, sph) = self.findinDoc(b'page.h',0,-1)
|
||||
(pos, spw) = self.findinDoc(b'page.w',0,-1)
|
||||
if (sph == None): sph = '-1'
|
||||
if (spw == None): spw = '-1'
|
||||
return sph, spw
|
||||
|
||||
def getPageDim(flatxml):
|
||||
# create a document parser
|
||||
dp = PageDimParser(flatxml)
|
||||
(ph, pw) = dp.process()
|
||||
return ph, pw
|
||||
|
||||
class GParser(object):
|
||||
def __init__(self, flatxml):
|
||||
self.flatdoc = flatxml.split(b'\n')
|
||||
self.dpi = 1440
|
||||
self.gh = self.getData(b'info.glyph.h')
|
||||
self.gw = self.getData(b'info.glyph.w')
|
||||
self.guse = self.getData(b'info.glyph.use')
|
||||
if self.guse :
|
||||
self.count = len(self.guse)
|
||||
else :
|
||||
self.count = 0
|
||||
self.gvtx = self.getData(b'info.glyph.vtx')
|
||||
self.glen = self.getData(b'info.glyph.len')
|
||||
self.gdpi = self.getData(b'info.glyph.dpi')
|
||||
self.vx = self.getData(b'info.vtx.x')
|
||||
self.vy = self.getData(b'info.vtx.y')
|
||||
self.vlen = self.getData(b'info.len.n')
|
||||
if self.vlen :
|
||||
self.glen.append(len(self.vlen))
|
||||
elif self.glen:
|
||||
self.glen.append(0)
|
||||
if self.vx :
|
||||
self.gvtx.append(len(self.vx))
|
||||
elif self.gvtx :
|
||||
self.gvtx.append(0)
|
||||
def getData(self, path):
|
||||
result = None
|
||||
cnt = len(self.flatdoc)
|
||||
for j in range(cnt):
|
||||
item = self.flatdoc[j]
|
||||
if item.find(b'=') >= 0:
|
||||
(name, argt) = item.split(b'=')
|
||||
argres = argt.split(b'|')
|
||||
else:
|
||||
name = item
|
||||
argres = []
|
||||
if (name == path):
|
||||
result = argres
|
||||
break
|
||||
if (len(argres) > 0) :
|
||||
for j in range(0,len(argres)):
|
||||
argres[j] = int(argres[j])
|
||||
return result
|
||||
def getGlyphDim(self, gly):
|
||||
if self.gdpi[gly] == 0:
|
||||
return 0, 0
|
||||
maxh = (self.gh[gly] * self.dpi) / self.gdpi[gly]
|
||||
maxw = (self.gw[gly] * self.dpi) / self.gdpi[gly]
|
||||
return maxh, maxw
|
||||
def getPath(self, gly):
|
||||
path = ''
|
||||
if (gly < 0) or (gly >= self.count):
|
||||
return path
|
||||
tx = self.vx[self.gvtx[gly]:self.gvtx[gly+1]]
|
||||
ty = self.vy[self.gvtx[gly]:self.gvtx[gly+1]]
|
||||
p = 0
|
||||
for k in range(self.glen[gly], self.glen[gly+1]):
|
||||
if (p == 0):
|
||||
zx = tx[0:self.vlen[k]+1]
|
||||
zy = ty[0:self.vlen[k]+1]
|
||||
else:
|
||||
zx = tx[self.vlen[k-1]+1:self.vlen[k]+1]
|
||||
zy = ty[self.vlen[k-1]+1:self.vlen[k]+1]
|
||||
p += 1
|
||||
j = 0
|
||||
while ( j < len(zx) ):
|
||||
if (j == 0):
|
||||
# Start Position.
|
||||
path += 'M %d %d ' % (zx[j] * self.dpi / self.gdpi[gly], zy[j] * self.dpi / self.gdpi[gly])
|
||||
elif (j <= len(zx)-3):
|
||||
# Cubic Bezier Curve
|
||||
path += 'C %d %d %d %d %d %d ' % (zx[j] * self.dpi / self.gdpi[gly], zy[j] * self.dpi / self.gdpi[gly], zx[j+1] * self.dpi / self.gdpi[gly], zy[j+1] * self.dpi / self.gdpi[gly], zx[j+2] * self.dpi / self.gdpi[gly], zy[j+2] * self.dpi / self.gdpi[gly])
|
||||
j += 2
|
||||
elif (j == len(zx)-2):
|
||||
# Cubic Bezier Curve to Start Position
|
||||
path += 'C %d %d %d %d %d %d ' % (zx[j] * self.dpi / self.gdpi[gly], zy[j] * self.dpi / self.gdpi[gly], zx[j+1] * self.dpi / self.gdpi[gly], zy[j+1] * self.dpi / self.gdpi[gly], zx[0] * self.dpi / self.gdpi[gly], zy[0] * self.dpi / self.gdpi[gly])
|
||||
j += 1
|
||||
elif (j == len(zx)-1):
|
||||
# Quadratic Bezier Curve to Start Position
|
||||
path += 'Q %d %d %d %d ' % (zx[j] * self.dpi / self.gdpi[gly], zy[j] * self.dpi / self.gdpi[gly], zx[0] * self.dpi / self.gdpi[gly], zy[0] * self.dpi / self.gdpi[gly])
|
||||
|
||||
j += 1
|
||||
path += 'z'
|
||||
return path
|
||||
|
||||
|
||||
|
||||
# dictionary of all text strings by index value
|
||||
class GlyphDict(object):
|
||||
def __init__(self):
|
||||
self.gdict = {}
|
||||
def lookup(self, id):
|
||||
# id='id="gl%d"' % val
|
||||
if id in self.gdict:
|
||||
return self.gdict[id]
|
||||
return None
|
||||
def addGlyph(self, val, path):
|
||||
id='id="gl%d"' % val
|
||||
self.gdict[id] = path
|
||||
|
||||
|
||||
def generateBook(bookDir, raw, fixedimage):
|
||||
# sanity check Topaz file extraction
|
||||
if not os.path.exists(bookDir) :
|
||||
print("Can not find directory with unencrypted book")
|
||||
return 1
|
||||
|
||||
dictFile = os.path.join(bookDir,'dict0000.dat')
|
||||
if not os.path.exists(dictFile) :
|
||||
print("Can not find dict0000.dat file")
|
||||
return 1
|
||||
|
||||
pageDir = os.path.join(bookDir,'page')
|
||||
if not os.path.exists(pageDir) :
|
||||
print("Can not find page directory in unencrypted book")
|
||||
return 1
|
||||
|
||||
imgDir = os.path.join(bookDir,'img')
|
||||
if not os.path.exists(imgDir) :
|
||||
print("Can not find image directory in unencrypted book")
|
||||
return 1
|
||||
|
||||
glyphsDir = os.path.join(bookDir,'glyphs')
|
||||
if not os.path.exists(glyphsDir) :
|
||||
print("Can not find glyphs directory in unencrypted book")
|
||||
return 1
|
||||
|
||||
metaFile = os.path.join(bookDir,'metadata0000.dat')
|
||||
if not os.path.exists(metaFile) :
|
||||
print("Can not find metadata0000.dat in unencrypted book")
|
||||
return 1
|
||||
|
||||
svgDir = os.path.join(bookDir,'svg')
|
||||
if not os.path.exists(svgDir) :
|
||||
os.makedirs(svgDir)
|
||||
|
||||
if buildXML:
|
||||
xmlDir = os.path.join(bookDir,'xml')
|
||||
if not os.path.exists(xmlDir) :
|
||||
os.makedirs(xmlDir)
|
||||
|
||||
otherFile = os.path.join(bookDir,'other0000.dat')
|
||||
if not os.path.exists(otherFile) :
|
||||
print("Can not find other0000.dat in unencrypted book")
|
||||
return 1
|
||||
|
||||
print("Updating to color images if available")
|
||||
spath = os.path.join(bookDir,'color_img')
|
||||
dpath = os.path.join(bookDir,'img')
|
||||
filenames = os.listdir(spath)
|
||||
filenames = sorted(filenames)
|
||||
for filename in filenames:
|
||||
imgname = filename.replace('color','img')
|
||||
sfile = os.path.join(spath,filename)
|
||||
dfile = os.path.join(dpath,imgname)
|
||||
imgdata = open(sfile,'rb').read()
|
||||
open(dfile,'wb').write(imgdata)
|
||||
|
||||
print("Creating cover.jpg")
|
||||
isCover = False
|
||||
cpath = os.path.join(bookDir,'img')
|
||||
cpath = os.path.join(cpath,'img0000.jpg')
|
||||
if os.path.isfile(cpath):
|
||||
cover = open(cpath, 'rb').read()
|
||||
cpath = os.path.join(bookDir,'cover.jpg')
|
||||
open(cpath, 'wb').write(cover)
|
||||
isCover = True
|
||||
|
||||
|
||||
print('Processing Dictionary')
|
||||
dict = Dictionary(dictFile)
|
||||
|
||||
print('Processing Meta Data and creating OPF')
|
||||
meta_array = getMetaArray(metaFile)
|
||||
|
||||
# replace special chars in title and authors like & < >
|
||||
title = meta_array.get('Title','No Title Provided')
|
||||
title = title.replace('&','&')
|
||||
title = title.replace('<','<')
|
||||
title = title.replace('>','>')
|
||||
meta_array['Title'] = title
|
||||
authors = meta_array.get('Authors','No Authors Provided')
|
||||
authors = authors.replace('&','&')
|
||||
authors = authors.replace('<','<')
|
||||
authors = authors.replace('>','>')
|
||||
meta_array['Authors'] = authors
|
||||
|
||||
if buildXML:
|
||||
xname = os.path.join(xmlDir, 'metadata.xml')
|
||||
mlst = []
|
||||
for key in meta_array:
|
||||
mlst.append('<meta name="' + key + '" content="' + meta_array[key] + '" />\n')
|
||||
metastr = "".join(mlst)
|
||||
mlst = None
|
||||
open(xname, 'wb').write(metastr)
|
||||
|
||||
print('Processing StyleSheet')
|
||||
|
||||
# get some scaling info from metadata to use while processing styles
|
||||
# and first page info
|
||||
|
||||
fontsize = '135'
|
||||
if 'fontSize' in meta_array:
|
||||
fontsize = meta_array['fontSize']
|
||||
|
||||
# also get the size of a normal text page
|
||||
# get the total number of pages unpacked as a safety check
|
||||
filenames = os.listdir(pageDir)
|
||||
numfiles = len(filenames)
|
||||
|
||||
spage = '1'
|
||||
if 'firstTextPage' in meta_array:
|
||||
spage = meta_array['firstTextPage']
|
||||
pnum = int(spage)
|
||||
if pnum >= numfiles or pnum < 0:
|
||||
# metadata is wrong so just select a page near the front
|
||||
# 10% of the book to get a normal text page
|
||||
pnum = int(0.10 * numfiles)
|
||||
# print "first normal text page is", spage
|
||||
|
||||
# get page height and width from first text page for use in stylesheet scaling
|
||||
pname = 'page%04d.dat' % (pnum - 1)
|
||||
fname = os.path.join(pageDir,pname)
|
||||
flat_xml = convert2xml.fromData(dict, fname)
|
||||
|
||||
(ph, pw) = getPageDim(flat_xml)
|
||||
if (ph == '-1') or (ph == '0') : ph = '11000'
|
||||
if (pw == '-1') or (pw == '0') : pw = '8500'
|
||||
meta_array['pageHeight'] = ph
|
||||
meta_array['pageWidth'] = pw
|
||||
if 'fontSize' not in meta_array.keys():
|
||||
meta_array['fontSize'] = fontsize
|
||||
|
||||
# process other.dat for css info and for map of page files to svg images
|
||||
# this map is needed because some pages actually are made up of multiple
|
||||
# pageXXXX.xml files
|
||||
xname = os.path.join(bookDir, 'style.css')
|
||||
flat_xml = convert2xml.fromData(dict, otherFile)
|
||||
|
||||
# extract info.original.pid to get original page information
|
||||
pageIDMap = {}
|
||||
pageidnums = stylexml2css.getpageIDMap(flat_xml)
|
||||
if len(pageidnums) == 0:
|
||||
filenames = os.listdir(pageDir)
|
||||
numfiles = len(filenames)
|
||||
for k in range(numfiles):
|
||||
pageidnums.append(k)
|
||||
# create a map from page ids to list of page file nums to process for that page
|
||||
for i in range(len(pageidnums)):
|
||||
id = pageidnums[i]
|
||||
if id in pageIDMap.keys():
|
||||
pageIDMap[id].append(i)
|
||||
else:
|
||||
pageIDMap[id] = [i]
|
||||
|
||||
# now get the css info
|
||||
cssstr , classlst = stylexml2css.convert2CSS(flat_xml, fontsize, ph, pw)
|
||||
open(xname, 'w').write(cssstr)
|
||||
if buildXML:
|
||||
xname = os.path.join(xmlDir, 'other0000.xml')
|
||||
open(xname, 'wb').write(convert2xml.getXML(dict, otherFile))
|
||||
|
||||
print('Processing Glyphs')
|
||||
gd = GlyphDict()
|
||||
filenames = os.listdir(glyphsDir)
|
||||
filenames = sorted(filenames)
|
||||
glyfname = os.path.join(svgDir,'glyphs.svg')
|
||||
glyfile = open(glyfname, 'w')
|
||||
glyfile.write('<?xml version="1.0" standalone="no"?>\n')
|
||||
glyfile.write('<!DOCTYPE svg PUBLIC "-//W3C/DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n')
|
||||
glyfile.write('<svg width="512" height="512" viewBox="0 0 511 511" xmlns="http://www.w3.org/2000/svg" version="1.1">\n')
|
||||
glyfile.write('<title>Glyphs for %s</title>\n' % meta_array['Title'])
|
||||
glyfile.write('<defs>\n')
|
||||
counter = 0
|
||||
for filename in filenames:
|
||||
# print ' ', filename
|
||||
print('.', end=' ')
|
||||
fname = os.path.join(glyphsDir,filename)
|
||||
flat_xml = convert2xml.fromData(dict, fname)
|
||||
|
||||
if buildXML:
|
||||
xname = os.path.join(xmlDir, filename.replace('.dat','.xml'))
|
||||
open(xname, 'wb').write(convert2xml.getXML(dict, fname))
|
||||
|
||||
gp = GParser(flat_xml)
|
||||
for i in range(0, gp.count):
|
||||
path = gp.getPath(i)
|
||||
maxh, maxw = gp.getGlyphDim(i)
|
||||
fullpath = '<path id="gl%d" d="%s" fill="black" /><!-- width=%d height=%d -->\n' % (counter * 256 + i, path, maxw, maxh)
|
||||
glyfile.write(fullpath)
|
||||
gd.addGlyph(counter * 256 + i, fullpath)
|
||||
counter += 1
|
||||
glyfile.write('</defs>\n')
|
||||
glyfile.write('</svg>\n')
|
||||
glyfile.close()
|
||||
print(" ")
|
||||
|
||||
|
||||
# start up the html
|
||||
# also build up tocentries while processing html
|
||||
htmlFileName = "book.html"
|
||||
hlst = []
|
||||
hlst.append('<?xml version="1.0" encoding="utf-8"?>\n')
|
||||
hlst.append('<!DOCTYPE HTML PUBLIC "-//W3C//DTD XHTML 1.1 Strict//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11-strict.dtd">\n')
|
||||
hlst.append('<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">\n')
|
||||
hlst.append('<head>\n')
|
||||
hlst.append('<meta http-equiv="content-type" content="text/html; charset=utf-8"/>\n')
|
||||
hlst.append('<title>' + meta_array['Title'] + ' by ' + meta_array['Authors'] + '</title>\n')
|
||||
hlst.append('<meta name="Author" content="' + meta_array['Authors'] + '" />\n')
|
||||
hlst.append('<meta name="Title" content="' + meta_array['Title'] + '" />\n')
|
||||
if 'ASIN' in meta_array:
|
||||
hlst.append('<meta name="ASIN" content="' + meta_array['ASIN'] + '" />\n')
|
||||
if 'GUID' in meta_array:
|
||||
hlst.append('<meta name="GUID" content="' + meta_array['GUID'] + '" />\n')
|
||||
hlst.append('<link href="style.css" rel="stylesheet" type="text/css" />\n')
|
||||
hlst.append('</head>\n<body>\n')
|
||||
|
||||
print('Processing Pages')
|
||||
# Books are at 1440 DPI. This is rendering at twice that size for
|
||||
# readability when rendering to the screen.
|
||||
scaledpi = 1440.0
|
||||
|
||||
filenames = os.listdir(pageDir)
|
||||
filenames = sorted(filenames)
|
||||
numfiles = len(filenames)
|
||||
|
||||
xmllst = []
|
||||
elst = []
|
||||
|
||||
for filename in filenames:
|
||||
# print ' ', filename
|
||||
print(".", end=' ')
|
||||
fname = os.path.join(pageDir,filename)
|
||||
flat_xml = convert2xml.fromData(dict, fname)
|
||||
|
||||
# keep flat_xml for later svg processing
|
||||
xmllst.append(flat_xml)
|
||||
|
||||
if buildXML:
|
||||
xname = os.path.join(xmlDir, filename.replace('.dat','.xml'))
|
||||
open(xname, 'wb').write(convert2xml.getXML(dict, fname))
|
||||
|
||||
# first get the html
|
||||
pagehtml, tocinfo = flatxml2html.convert2HTML(flat_xml, classlst, fname, bookDir, gd, fixedimage)
|
||||
elst.append(tocinfo)
|
||||
hlst.append(pagehtml)
|
||||
|
||||
# finish up the html string and output it
|
||||
hlst.append('</body>\n</html>\n')
|
||||
htmlstr = "".join(hlst)
|
||||
hlst = None
|
||||
open(os.path.join(bookDir, htmlFileName), 'w').write(htmlstr)
|
||||
|
||||
print(" ")
|
||||
print('Extracting Table of Contents from Amazon OCR')
|
||||
|
||||
# first create a table of contents file for the svg images
|
||||
tlst = []
|
||||
tlst.append('<?xml version="1.0" encoding="utf-8"?>\n')
|
||||
tlst.append('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">\n')
|
||||
tlst.append('<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" >')
|
||||
tlst.append('<head>\n')
|
||||
tlst.append('<title>' + meta_array['Title'] + '</title>\n')
|
||||
tlst.append('<meta name="Author" content="' + meta_array['Authors'] + '" />\n')
|
||||
tlst.append('<meta name="Title" content="' + meta_array['Title'] + '" />\n')
|
||||
if 'ASIN' in meta_array:
|
||||
tlst.append('<meta name="ASIN" content="' + meta_array['ASIN'] + '" />\n')
|
||||
if 'GUID' in meta_array:
|
||||
tlst.append('<meta name="GUID" content="' + meta_array['GUID'] + '" />\n')
|
||||
tlst.append('</head>\n')
|
||||
tlst.append('<body>\n')
|
||||
|
||||
tlst.append('<h2>Table of Contents</h2>\n')
|
||||
start = pageidnums[0]
|
||||
if (raw):
|
||||
startname = 'page%04d.svg' % start
|
||||
else:
|
||||
startname = 'page%04d.xhtml' % start
|
||||
|
||||
tlst.append('<h3><a href="' + startname + '">Start of Book</a></h3>\n')
|
||||
# build up a table of contents for the svg xhtml output
|
||||
tocentries = "".join(elst)
|
||||
elst = None
|
||||
toclst = tocentries.split('\n')
|
||||
toclst.pop()
|
||||
for entry in toclst:
|
||||
print(entry)
|
||||
title, pagenum = entry.split('|')
|
||||
id = pageidnums[int(pagenum)]
|
||||
if (raw):
|
||||
fname = 'page%04d.svg' % id
|
||||
else:
|
||||
fname = 'page%04d.xhtml' % id
|
||||
tlst.append('<h3><a href="'+ fname + '">' + title + '</a></h3>\n')
|
||||
tlst.append('</body>\n')
|
||||
tlst.append('</html>\n')
|
||||
tochtml = "".join(tlst)
|
||||
open(os.path.join(svgDir, 'toc.xhtml'), 'w').write(tochtml)
|
||||
|
||||
|
||||
# now create index_svg.xhtml that points to all required files
|
||||
slst = []
|
||||
slst.append('<?xml version="1.0" encoding="utf-8"?>\n')
|
||||
slst.append('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">\n')
|
||||
slst.append('<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" >')
|
||||
slst.append('<head>\n')
|
||||
slst.append('<title>' + meta_array['Title'] + '</title>\n')
|
||||
slst.append('<meta name="Author" content="' + meta_array['Authors'] + '" />\n')
|
||||
slst.append('<meta name="Title" content="' + meta_array['Title'] + '" />\n')
|
||||
if 'ASIN' in meta_array:
|
||||
slst.append('<meta name="ASIN" content="' + meta_array['ASIN'] + '" />\n')
|
||||
if 'GUID' in meta_array:
|
||||
slst.append('<meta name="GUID" content="' + meta_array['GUID'] + '" />\n')
|
||||
slst.append('</head>\n')
|
||||
slst.append('<body>\n')
|
||||
|
||||
print("Building svg images of each book page")
|
||||
slst.append('<h2>List of Pages</h2>\n')
|
||||
slst.append('<div>\n')
|
||||
idlst = sorted(pageIDMap.keys())
|
||||
numids = len(idlst)
|
||||
cnt = len(idlst)
|
||||
previd = None
|
||||
for j in range(cnt):
|
||||
pageid = idlst[j]
|
||||
if j < cnt - 1:
|
||||
nextid = idlst[j+1]
|
||||
else:
|
||||
nextid = None
|
||||
print('.', end=' ')
|
||||
pagelst = pageIDMap[pageid]
|
||||
flst = []
|
||||
for page in pagelst:
|
||||
flst.append(xmllst[page])
|
||||
flat_svg = b"".join(flst)
|
||||
flst=None
|
||||
svgxml = flatxml2svg.convert2SVG(gd, flat_svg, pageid, previd, nextid, svgDir, raw, meta_array, scaledpi)
|
||||
if (raw) :
|
||||
pfile = open(os.path.join(svgDir,'page%04d.svg' % pageid),'w')
|
||||
slst.append('<a href="svg/page%04d.svg">Page %d</a>\n' % (pageid, pageid))
|
||||
else :
|
||||
pfile = open(os.path.join(svgDir,'page%04d.xhtml' % pageid), 'w')
|
||||
slst.append('<a href="svg/page%04d.xhtml">Page %d</a>\n' % (pageid, pageid))
|
||||
previd = pageid
|
||||
pfile.write(svgxml)
|
||||
pfile.close()
|
||||
counter += 1
|
||||
slst.append('</div>\n')
|
||||
slst.append('<h2><a href="svg/toc.xhtml">Table of Contents</a></h2>\n')
|
||||
slst.append('</body>\n</html>\n')
|
||||
svgindex = "".join(slst)
|
||||
slst = None
|
||||
open(os.path.join(bookDir, 'index_svg.xhtml'), 'w').write(svgindex)
|
||||
|
||||
print(" ")
|
||||
|
||||
# build the opf file
|
||||
opfname = os.path.join(bookDir, 'book.opf')
|
||||
olst = []
|
||||
olst.append('<?xml version="1.0" encoding="utf-8"?>\n')
|
||||
olst.append('<package xmlns="http://www.idpf.org/2007/opf" unique-identifier="guid_id">\n')
|
||||
# adding metadata
|
||||
olst.append(' <metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf">\n')
|
||||
if b'GUID' in meta_array:
|
||||
olst.append(' <dc:identifier opf:scheme="GUID" id="guid_id">' + meta_array[b'GUID'].decode('utf-8') + '</dc:identifier>\n')
|
||||
if b'ASIN' in meta_array:
|
||||
olst.append(' <dc:identifier opf:scheme="ASIN">' + meta_array[b'ASIN'].decode('utf-8') + '</dc:identifier>\n')
|
||||
if b'oASIN' in meta_array:
|
||||
olst.append(' <dc:identifier opf:scheme="oASIN">' + meta_array[b'oASIN'].decode('utf-8') + '</dc:identifier>\n')
|
||||
olst.append(' <dc:title>' + meta_array[b'Title'].decode('utf-8') + '</dc:title>\n')
|
||||
olst.append(' <dc:creator opf:role="aut">' + meta_array[b'Authors'].decode('utf-8') + '</dc:creator>\n')
|
||||
olst.append(' <dc:language>en</dc:language>\n')
|
||||
olst.append(' <dc:date>' + meta_array[b'UpdateTime'].decode('utf-8') + '</dc:date>\n')
|
||||
if isCover:
|
||||
olst.append(' <meta name="cover" content="bookcover"/>\n')
|
||||
olst.append(' </metadata>\n')
|
||||
olst.append('<manifest>\n')
|
||||
olst.append(' <item id="book" href="book.html" media-type="application/xhtml+xml"/>\n')
|
||||
olst.append(' <item id="stylesheet" href="style.css" media-type="text/css"/>\n')
|
||||
# adding image files to manifest
|
||||
filenames = os.listdir(imgDir)
|
||||
filenames = sorted(filenames)
|
||||
for filename in filenames:
|
||||
imgname, imgext = os.path.splitext(filename)
|
||||
if imgext == '.jpg':
|
||||
imgext = 'jpeg'
|
||||
if imgext == '.svg':
|
||||
imgext = 'svg+xml'
|
||||
olst.append(' <item id="' + imgname + '" href="img/' + filename + '" media-type="image/' + imgext + '"/>\n')
|
||||
if isCover:
|
||||
olst.append(' <item id="bookcover" href="cover.jpg" media-type="image/jpeg" />\n')
|
||||
olst.append('</manifest>\n')
|
||||
# adding spine
|
||||
olst.append('<spine>\n <itemref idref="book" />\n</spine>\n')
|
||||
if isCover:
|
||||
olst.append(' <guide>\n')
|
||||
olst.append(' <reference href="cover.jpg" type="cover" title="Cover"/>\n')
|
||||
olst.append(' </guide>\n')
|
||||
olst.append('</package>\n')
|
||||
opfstr = "".join(olst)
|
||||
olst = None
|
||||
open(opfname, 'w').write(opfstr)
|
||||
|
||||
print('Processing Complete')
|
||||
|
||||
return 0
|
||||
|
||||
def usage():
|
||||
print("genbook.py generates a book from the extract Topaz Files")
|
||||
print("Usage:")
|
||||
print(" genbook.py [-r] [-h [--fixed-image] <bookDir> ")
|
||||
print(" ")
|
||||
print("Options:")
|
||||
print(" -h : help - print this usage message")
|
||||
print(" -r : generate raw svg files (not wrapped in xhtml)")
|
||||
print(" --fixed-image : genearate any Fixed Area as an svg image in the html")
|
||||
print(" ")
|
||||
|
||||
|
||||
def main(argv):
|
||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
bookDir = ''
|
||||
if len(argv) == 0:
|
||||
argv = sys.argv
|
||||
|
||||
try:
|
||||
opts, args = getopt.getopt(argv[1:], "rh:",["fixed-image"])
|
||||
|
||||
except getopt.GetoptError as err:
|
||||
print(str(err))
|
||||
usage()
|
||||
return 1
|
||||
|
||||
if len(opts) == 0 and len(args) == 0 :
|
||||
usage()
|
||||
return 1
|
||||
|
||||
raw = 0
|
||||
fixedimage = True
|
||||
for o, a in opts:
|
||||
if o =="-h":
|
||||
usage()
|
||||
return 0
|
||||
if o =="-r":
|
||||
raw = 1
|
||||
if o =="--fixed-image":
|
||||
fixedimage = True
|
||||
|
||||
bookDir = args[0]
|
||||
|
||||
rv = generateBook(bookDir, raw, fixedimage)
|
||||
return rv
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main(''))
|
||||
441
DeDRM_plugin/ignobleepub.py
Normal file
441
DeDRM_plugin/ignobleepub.py
Normal file
@@ -0,0 +1,441 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# ignobleepub.py
|
||||
# Copyright © 2009-2020 by i♥cabbages, Apprentice Harper et al.
|
||||
|
||||
# Released under the terms of the GNU General Public Licence, version 3
|
||||
# <http://www.gnu.org/licenses/>
|
||||
|
||||
#
|
||||
# Revision history:
|
||||
# 1 - Initial release
|
||||
# 2 - Added OS X support by using OpenSSL when available
|
||||
# 3 - screen out improper key lengths to prevent segfaults on Linux
|
||||
# 3.1 - Allow Windows versions of libcrypto to be found
|
||||
# 3.2 - add support for encoding to 'utf-8' when building up list of files to decrypt from encryption.xml
|
||||
# 3.3 - On Windows try PyCrypto first, OpenSSL next
|
||||
# 3.4 - Modify interface to allow use with import
|
||||
# 3.5 - Fix for potential problem with PyCrypto
|
||||
# 3.6 - Revised to allow use in calibre plugins to eliminate need for duplicate code
|
||||
# 3.7 - Tweaked to match ineptepub more closely
|
||||
# 3.8 - Fixed to retain zip file metadata (e.g. file modification date)
|
||||
# 3.9 - moved unicode_argv call inside main for Windows DeDRM compatibility
|
||||
# 4.0 - Work if TkInter is missing
|
||||
# 4.1 - Import tkFileDialog, don't assume something else will import it.
|
||||
# 5.0 - Python 3 for calibre 5.0
|
||||
|
||||
"""
|
||||
Decrypt Barnes & Noble encrypted ePub books.
|
||||
"""
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = "5.0"
|
||||
|
||||
import sys
|
||||
import os
|
||||
import traceback
|
||||
import base64
|
||||
import zlib
|
||||
import zipfile
|
||||
from zipfile import ZipInfo, ZipFile, ZIP_STORED, ZIP_DEFLATED
|
||||
from contextlib import closing
|
||||
import xml.etree.ElementTree as etree
|
||||
|
||||
# Wrap a stream so that output gets flushed immediately
|
||||
# and also make sure that any unicode strings get
|
||||
# encoded using "replace" before writing them.
|
||||
class SafeUnbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
self.encoding = stream.encoding
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
def write(self, data):
|
||||
if isinstance(data,str):
|
||||
data = data.encode(self.encoding,"replace")
|
||||
self.stream.buffer.write(data)
|
||||
self.stream.buffer.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
try:
|
||||
from calibre.constants import iswindows, isosx
|
||||
except:
|
||||
iswindows = sys.platform.startswith('win')
|
||||
isosx = sys.platform.startswith('darwin')
|
||||
|
||||
def unicode_argv():
|
||||
if iswindows:
|
||||
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
||||
# strings.
|
||||
|
||||
# Versions 2.x of Python don't support Unicode in sys.argv on
|
||||
# Windows, with the underlying Windows API instead replacing multi-byte
|
||||
# characters with '?'.
|
||||
|
||||
|
||||
from ctypes import POINTER, byref, cdll, c_int, windll
|
||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
||||
|
||||
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
||||
GetCommandLineW.argtypes = []
|
||||
GetCommandLineW.restype = LPCWSTR
|
||||
|
||||
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
||||
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
||||
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
||||
|
||||
cmd = GetCommandLineW()
|
||||
argc = c_int(0)
|
||||
argv = CommandLineToArgvW(cmd, byref(argc))
|
||||
if argc.value > 0:
|
||||
# Remove Python executable and commands if present
|
||||
start = argc.value - len(sys.argv)
|
||||
return [argv[i] for i in
|
||||
range(start, argc.value)]
|
||||
return ["ineptepub.py"]
|
||||
else:
|
||||
argvencoding = sys.stdin.encoding or "utf-8"
|
||||
return [arg if isinstance(arg, str) else str(arg, argvencoding) for arg in sys.argv]
|
||||
|
||||
|
||||
class IGNOBLEError(Exception):
|
||||
pass
|
||||
|
||||
def _load_crypto_libcrypto():
|
||||
from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_int, c_long, \
|
||||
Structure, c_ulong, create_string_buffer, cast
|
||||
from ctypes.util import find_library
|
||||
|
||||
if iswindows:
|
||||
libcrypto = find_library('libeay32')
|
||||
else:
|
||||
libcrypto = find_library('crypto')
|
||||
|
||||
if libcrypto is None:
|
||||
raise IGNOBLEError('libcrypto not found')
|
||||
libcrypto = CDLL(libcrypto)
|
||||
|
||||
AES_MAXNR = 14
|
||||
|
||||
c_char_pp = POINTER(c_char_p)
|
||||
c_int_p = POINTER(c_int)
|
||||
|
||||
class AES_KEY(Structure):
|
||||
_fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))),
|
||||
('rounds', c_int)]
|
||||
AES_KEY_p = POINTER(AES_KEY)
|
||||
|
||||
def F(restype, name, argtypes):
|
||||
func = getattr(libcrypto, name)
|
||||
func.restype = restype
|
||||
func.argtypes = argtypes
|
||||
return func
|
||||
|
||||
AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key',
|
||||
[c_char_p, c_int, AES_KEY_p])
|
||||
AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',
|
||||
[c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p,
|
||||
c_int])
|
||||
|
||||
class AES(object):
|
||||
def __init__(self, userkey):
|
||||
self._blocksize = len(userkey)
|
||||
if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) :
|
||||
raise IGNOBLEError('AES improper key used')
|
||||
return
|
||||
key = self._key = AES_KEY()
|
||||
rv = AES_set_decrypt_key(userkey, len(userkey) * 8, key)
|
||||
if rv < 0:
|
||||
raise IGNOBLEError('Failed to initialize AES key')
|
||||
|
||||
def decrypt(self, data):
|
||||
out = create_string_buffer(len(data))
|
||||
iv = (b'\x00' * self._blocksize)
|
||||
rv = AES_cbc_encrypt(data, out, len(data), self._key, iv, 0)
|
||||
if rv == 0:
|
||||
raise IGNOBLEError('AES decryption failed')
|
||||
return out.raw
|
||||
|
||||
return AES
|
||||
|
||||
def _load_crypto_pycrypto():
|
||||
from Crypto.Cipher import AES as _AES
|
||||
|
||||
class AES(object):
|
||||
def __init__(self, key):
|
||||
self._aes = _AES.new(key, _AES.MODE_CBC, b'\x00'*16)
|
||||
|
||||
def decrypt(self, data):
|
||||
return self._aes.decrypt(data)
|
||||
|
||||
return AES
|
||||
|
||||
def _load_crypto():
|
||||
AES = None
|
||||
cryptolist = (_load_crypto_libcrypto, _load_crypto_pycrypto)
|
||||
if sys.platform.startswith('win'):
|
||||
cryptolist = (_load_crypto_pycrypto, _load_crypto_libcrypto)
|
||||
for loader in cryptolist:
|
||||
try:
|
||||
AES = loader()
|
||||
break
|
||||
except (ImportError, IGNOBLEError):
|
||||
pass
|
||||
return AES
|
||||
|
||||
AES = _load_crypto()
|
||||
|
||||
META_NAMES = ('mimetype', 'META-INF/rights.xml', 'META-INF/encryption.xml')
|
||||
NSMAP = {'adept': 'http://ns.adobe.com/adept',
|
||||
'enc': 'http://www.w3.org/2001/04/xmlenc#'}
|
||||
|
||||
class Decryptor(object):
|
||||
def __init__(self, bookkey, encryption):
|
||||
enc = lambda tag: '{%s}%s' % (NSMAP['enc'], tag)
|
||||
self._aes = AES(bookkey)
|
||||
encryption = etree.fromstring(encryption)
|
||||
self._encrypted = encrypted = set()
|
||||
expr = './%s/%s/%s' % (enc('EncryptedData'), enc('CipherData'),
|
||||
enc('CipherReference'))
|
||||
for elem in encryption.findall(expr):
|
||||
path = elem.get('URI', None)
|
||||
if path is not None:
|
||||
path = path.encode('utf-8')
|
||||
encrypted.add(path)
|
||||
|
||||
def decompress(self, bytes):
|
||||
dc = zlib.decompressobj(-15)
|
||||
bytes = dc.decompress(bytes)
|
||||
ex = dc.decompress(b'Z') + dc.flush()
|
||||
if ex:
|
||||
bytes = bytes + ex
|
||||
return bytes
|
||||
|
||||
def decrypt(self, path, data):
|
||||
if bytes(path,'utf-8') in self._encrypted:
|
||||
data = self._aes.decrypt(data)[16:]
|
||||
data = data[:-data[-1]]
|
||||
data = self.decompress(data)
|
||||
return data
|
||||
|
||||
# check file to make check whether it's probably an Adobe Adept encrypted ePub
|
||||
def ignobleBook(inpath):
|
||||
with closing(ZipFile(open(inpath, 'rb'))) as inf:
|
||||
namelist = set(inf.namelist())
|
||||
if 'META-INF/rights.xml' not in namelist or \
|
||||
'META-INF/encryption.xml' not in namelist:
|
||||
return False
|
||||
try:
|
||||
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
|
||||
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
||||
expr = './/%s' % (adept('encryptedKey'),)
|
||||
bookkey = ''.join(rights.findtext(expr))
|
||||
if len(bookkey) == 64:
|
||||
return True
|
||||
except:
|
||||
# if we couldn't check, assume it is
|
||||
return True
|
||||
return False
|
||||
|
||||
def decryptBook(keyb64, inpath, outpath):
|
||||
if AES is None:
|
||||
raise IGNOBLEError("PyCrypto or OpenSSL must be installed.")
|
||||
key = base64.b64decode(keyb64)[:16]
|
||||
aes = AES(key)
|
||||
with closing(ZipFile(open(inpath, 'rb'))) as inf:
|
||||
namelist = set(inf.namelist())
|
||||
if 'META-INF/rights.xml' not in namelist or \
|
||||
'META-INF/encryption.xml' not in namelist:
|
||||
print("{0:s} is DRM-free.".format(os.path.basename(inpath)))
|
||||
return 1
|
||||
for name in META_NAMES:
|
||||
namelist.remove(name)
|
||||
try:
|
||||
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
|
||||
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
||||
expr = './/%s' % (adept('encryptedKey'),)
|
||||
bookkey = ''.join(rights.findtext(expr))
|
||||
if len(bookkey) != 64:
|
||||
print("{0:s} is not a secure Barnes & Noble ePub.".format(os.path.basename(inpath)))
|
||||
return 1
|
||||
bookkey = aes.decrypt(base64.b64decode(bookkey))
|
||||
bookkey = bookkey[:-bookkey[-1]]
|
||||
encryption = inf.read('META-INF/encryption.xml')
|
||||
decryptor = Decryptor(bookkey[-16:], encryption)
|
||||
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
|
||||
with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf:
|
||||
zi = ZipInfo('mimetype')
|
||||
zi.compress_type=ZIP_STORED
|
||||
try:
|
||||
# if the mimetype is present, get its info, including time-stamp
|
||||
oldzi = inf.getinfo('mimetype')
|
||||
# copy across fields to be preserved
|
||||
zi.date_time = oldzi.date_time
|
||||
zi.comment = oldzi.comment
|
||||
zi.extra = oldzi.extra
|
||||
zi.internal_attr = oldzi.internal_attr
|
||||
# external attributes are dependent on the create system, so copy both.
|
||||
zi.external_attr = oldzi.external_attr
|
||||
zi.create_system = oldzi.create_system
|
||||
except:
|
||||
pass
|
||||
outf.writestr(zi, inf.read('mimetype'))
|
||||
for path in namelist:
|
||||
data = inf.read(path)
|
||||
zi = ZipInfo(path)
|
||||
zi.compress_type=ZIP_DEFLATED
|
||||
try:
|
||||
# get the file info, including time-stamp
|
||||
oldzi = inf.getinfo(path)
|
||||
# copy across useful fields
|
||||
zi.date_time = oldzi.date_time
|
||||
zi.comment = oldzi.comment
|
||||
zi.extra = oldzi.extra
|
||||
zi.internal_attr = oldzi.internal_attr
|
||||
# external attributes are dependent on the create system, so copy both.
|
||||
zi.external_attr = oldzi.external_attr
|
||||
zi.create_system = oldzi.create_system
|
||||
except:
|
||||
pass
|
||||
outf.writestr(zi, decryptor.decrypt(path, data))
|
||||
except:
|
||||
print("Could not decrypt {0:s} because of an exception:\n{1:s}".format(os.path.basename(inpath), traceback.format_exc()))
|
||||
return 2
|
||||
return 0
|
||||
|
||||
|
||||
def cli_main():
|
||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
argv=unicode_argv()
|
||||
progname = os.path.basename(argv[0])
|
||||
if len(argv) != 4:
|
||||
print("usage: {0} <keyfile.b64> <inbook.epub> <outbook.epub>".format(progname))
|
||||
return 1
|
||||
keypath, inpath, outpath = argv[1:]
|
||||
userkey = open(keypath,'rb').read()
|
||||
result = decryptBook(userkey, inpath, outpath)
|
||||
if result == 0:
|
||||
print("Successfully decrypted {0:s} as {1:s}".format(os.path.basename(inpath),os.path.basename(outpath)))
|
||||
return result
|
||||
|
||||
def gui_main():
|
||||
try:
|
||||
import tkinter
|
||||
import tkinter.constants
|
||||
import tkinter.filedialog
|
||||
import tkinter.messagebox
|
||||
import traceback
|
||||
except:
|
||||
return cli_main()
|
||||
|
||||
class DecryptionDialog(tkinter.Frame):
|
||||
def __init__(self, root):
|
||||
tkinter.Frame.__init__(self, root, border=5)
|
||||
self.status = tkinter.Label(self, text="Select files for decryption")
|
||||
self.status.pack(fill=tkinter.constants.X, expand=1)
|
||||
body = tkinter.Frame(self)
|
||||
body.pack(fill=tkinter.constants.X, expand=1)
|
||||
sticky = tkinter.constants.E + tkinter.constants.W
|
||||
body.grid_columnconfigure(1, weight=2)
|
||||
tkinter.Label(body, text="Key file").grid(row=0)
|
||||
self.keypath = tkinter.Entry(body, width=30)
|
||||
self.keypath.grid(row=0, column=1, sticky=sticky)
|
||||
if os.path.exists("bnepubkey.b64"):
|
||||
self.keypath.insert(0, "bnepubkey.b64")
|
||||
button = tkinter.Button(body, text="...", command=self.get_keypath)
|
||||
button.grid(row=0, column=2)
|
||||
tkinter.Label(body, text="Input file").grid(row=1)
|
||||
self.inpath = tkinter.Entry(body, width=30)
|
||||
self.inpath.grid(row=1, column=1, sticky=sticky)
|
||||
button = tkinter.Button(body, text="...", command=self.get_inpath)
|
||||
button.grid(row=1, column=2)
|
||||
tkinter.Label(body, text="Output file").grid(row=2)
|
||||
self.outpath = tkinter.Entry(body, width=30)
|
||||
self.outpath.grid(row=2, column=1, sticky=sticky)
|
||||
button = tkinter.Button(body, text="...", command=self.get_outpath)
|
||||
button.grid(row=2, column=2)
|
||||
buttons = tkinter.Frame(self)
|
||||
buttons.pack()
|
||||
botton = tkinter.Button(
|
||||
buttons, text="Decrypt", width=10, command=self.decrypt)
|
||||
botton.pack(side=tkinter.constants.LEFT)
|
||||
tkinter.Frame(buttons, width=10).pack(side=tkinter.constants.LEFT)
|
||||
button = tkinter.Button(
|
||||
buttons, text="Quit", width=10, command=self.quit)
|
||||
button.pack(side=tkinter.constants.RIGHT)
|
||||
|
||||
def get_keypath(self):
|
||||
keypath = tkinter.filedialog.askopenfilename(
|
||||
parent=None, title="Select Barnes & Noble \'.b64\' key file",
|
||||
defaultextension=".b64",
|
||||
filetypes=[('base64-encoded files', '.b64'),
|
||||
('All Files', '.*')])
|
||||
if keypath:
|
||||
keypath = os.path.normpath(keypath)
|
||||
self.keypath.delete(0, tkinter.constants.END)
|
||||
self.keypath.insert(0, keypath)
|
||||
return
|
||||
|
||||
def get_inpath(self):
|
||||
inpath = tkinter.filedialog.askopenfilename(
|
||||
parent=None, title="Select B&N-encrypted ePub file to decrypt",
|
||||
defaultextension=".epub", filetypes=[('ePub files', '.epub')])
|
||||
if inpath:
|
||||
inpath = os.path.normpath(inpath)
|
||||
self.inpath.delete(0, tkinter.constants.END)
|
||||
self.inpath.insert(0, inpath)
|
||||
return
|
||||
|
||||
def get_outpath(self):
|
||||
outpath = tkinter.filedialog.asksaveasfilename(
|
||||
parent=None, title="Select unencrypted ePub file to produce",
|
||||
defaultextension=".epub", filetypes=[('ePub files', '.epub')])
|
||||
if outpath:
|
||||
outpath = os.path.normpath(outpath)
|
||||
self.outpath.delete(0, tkinter.constants.END)
|
||||
self.outpath.insert(0, outpath)
|
||||
return
|
||||
|
||||
def decrypt(self):
|
||||
keypath = self.keypath.get()
|
||||
inpath = self.inpath.get()
|
||||
outpath = self.outpath.get()
|
||||
if not keypath or not os.path.exists(keypath):
|
||||
self.status['text'] = "Specified key file does not exist"
|
||||
return
|
||||
if not inpath or not os.path.exists(inpath):
|
||||
self.status['text'] = "Specified input file does not exist"
|
||||
return
|
||||
if not outpath:
|
||||
self.status['text'] = "Output file not specified"
|
||||
return
|
||||
if inpath == outpath:
|
||||
self.status['text'] = "Must have different input and output files"
|
||||
return
|
||||
userkey = open(keypath,'rb').read()
|
||||
self.status['text'] = "Decrypting..."
|
||||
try:
|
||||
decrypt_status = decryptBook(userkey, inpath, outpath)
|
||||
except Exception as e:
|
||||
self.status['text'] = "Error: {0}".format(e.args[0])
|
||||
return
|
||||
if decrypt_status == 0:
|
||||
self.status['text'] = "File successfully decrypted"
|
||||
else:
|
||||
self.status['text'] = "The was an error decrypting the file."
|
||||
|
||||
root = tkinter.Tk()
|
||||
root.title("Barnes & Noble ePub Decrypter v.{0}".format(__version__))
|
||||
root.resizable(True, False)
|
||||
root.minsize(300, 0)
|
||||
DecryptionDialog(root).pack(fill=tkinter.constants.X, expand=1)
|
||||
root.mainloop()
|
||||
return 0
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) > 1:
|
||||
sys.exit(cli_main())
|
||||
sys.exit(gui_main())
|
||||
334
DeDRM_plugin/ignoblekey.py
Normal file
334
DeDRM_plugin/ignoblekey.py
Normal file
@@ -0,0 +1,334 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# ignoblekey.py
|
||||
# Copyright © 2015-2020 Apprentice Alf, Apprentice Harper et al.
|
||||
|
||||
# Based on kindlekey.py, Copyright © 2010-2013 by some_updates and Apprentice Alf
|
||||
|
||||
# Released under the terms of the GNU General Public Licence, version 3
|
||||
# <http://www.gnu.org/licenses/>
|
||||
|
||||
# Revision history:
|
||||
# 1.0 - Initial release
|
||||
# 1.1 - remove duplicates and return last key as single key
|
||||
# 2.0 - Python 3 for calibre 5.0
|
||||
|
||||
"""
|
||||
Get Barnes & Noble EPUB user key from nook Studio log file
|
||||
"""
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = "2.0"
|
||||
|
||||
import sys
|
||||
import os
|
||||
import hashlib
|
||||
import getopt
|
||||
import re
|
||||
|
||||
# Wrap a stream so that output gets flushed immediately
|
||||
# and also make sure that any unicode strings get
|
||||
# encoded using "replace" before writing them.
|
||||
class SafeUnbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
self.encoding = stream.encoding
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
def write(self, data):
|
||||
if isinstance(data, str):
|
||||
data = data.encode(self.encoding,"replace")
|
||||
self.stream.buffer.write(data)
|
||||
self.stream.buffer.flush()
|
||||
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
try:
|
||||
from calibre.constants import iswindows, isosx
|
||||
except:
|
||||
iswindows = sys.platform.startswith('win')
|
||||
isosx = sys.platform.startswith('darwin')
|
||||
|
||||
def unicode_argv():
|
||||
if iswindows:
|
||||
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
||||
# strings.
|
||||
|
||||
# Versions 2.x of Python don't support Unicode in sys.argv on
|
||||
# Windows, with the underlying Windows API instead replacing multi-byte
|
||||
# characters with '?'. So use shell32.GetCommandLineArgvW to get sys.argv
|
||||
# as a list of Unicode strings and encode them as utf-8
|
||||
|
||||
from ctypes import POINTER, byref, cdll, c_int, windll
|
||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
||||
|
||||
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
||||
GetCommandLineW.argtypes = []
|
||||
GetCommandLineW.restype = LPCWSTR
|
||||
|
||||
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
||||
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
||||
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
||||
|
||||
cmd = GetCommandLineW()
|
||||
argc = c_int(0)
|
||||
argv = CommandLineToArgvW(cmd, byref(argc))
|
||||
if argc.value > 0:
|
||||
# Remove Python executable and commands if present
|
||||
start = argc.value - len(sys.argv)
|
||||
return [argv[i] for i in
|
||||
range(start, argc.value)]
|
||||
# if we don't have any arguments at all, just pass back script name
|
||||
# this should never happen
|
||||
return ["ignoblekey.py"]
|
||||
else:
|
||||
argvencoding = sys.stdin.encoding or "utf-8"
|
||||
return [arg if isinstance(arg, str) else str(arg, argvencoding) for arg in sys.argv]
|
||||
|
||||
class DrmException(Exception):
|
||||
pass
|
||||
|
||||
# Locate all of the nookStudy/nook for PC/Mac log file and return as list
|
||||
def getNookLogFiles():
|
||||
logFiles = []
|
||||
found = False
|
||||
if iswindows:
|
||||
import winreg
|
||||
|
||||
# some 64 bit machines do not have the proper registry key for some reason
|
||||
# or the python interface to the 32 vs 64 bit registry is broken
|
||||
paths = set()
|
||||
if 'LOCALAPPDATA' in os.environ.keys():
|
||||
# Python 2.x does not return unicode env. Use Python 3.x
|
||||
path = winreg.ExpandEnvironmentStrings("%LOCALAPPDATA%")
|
||||
if os.path.isdir(path):
|
||||
paths.add(path)
|
||||
if 'USERPROFILE' in os.environ.keys():
|
||||
# Python 2.x does not return unicode env. Use Python 3.x
|
||||
path = winreg.ExpandEnvironmentStrings("%USERPROFILE%")+"\\AppData\\Local"
|
||||
if os.path.isdir(path):
|
||||
paths.add(path)
|
||||
path = winreg.ExpandEnvironmentStrings("%USERPROFILE%")+"\\AppData\\Roaming"
|
||||
if os.path.isdir(path):
|
||||
paths.add(path)
|
||||
# User Shell Folders show take precedent over Shell Folders if present
|
||||
try:
|
||||
regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders\\")
|
||||
path = winreg.QueryValueEx(regkey, 'Local AppData')[0]
|
||||
if os.path.isdir(path):
|
||||
paths.add(path)
|
||||
except WindowsError:
|
||||
pass
|
||||
try:
|
||||
regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders\\")
|
||||
path = winreg.QueryValueEx(regkey, 'AppData')[0]
|
||||
if os.path.isdir(path):
|
||||
paths.add(path)
|
||||
except WindowsError:
|
||||
pass
|
||||
try:
|
||||
regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\")
|
||||
path = winreg.QueryValueEx(regkey, 'Local AppData')[0]
|
||||
if os.path.isdir(path):
|
||||
paths.add(path)
|
||||
except WindowsError:
|
||||
pass
|
||||
try:
|
||||
regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\")
|
||||
path = winreg.QueryValueEx(regkey, 'AppData')[0]
|
||||
if os.path.isdir(path):
|
||||
paths.add(path)
|
||||
except WindowsError:
|
||||
pass
|
||||
|
||||
for path in paths:
|
||||
# look for nookStudy log file
|
||||
logpath = path +'\\Barnes & Noble\\NOOKstudy\\logs\\BNClientLog.txt'
|
||||
if os.path.isfile(logpath):
|
||||
found = True
|
||||
print('Found nookStudy log file: ' + logpath.encode('ascii','ignore'))
|
||||
logFiles.append(logpath)
|
||||
else:
|
||||
home = os.getenv('HOME')
|
||||
# check for BNClientLog.txt in various locations
|
||||
testpath = home + '/Library/Application Support/Barnes & Noble/DesktopReader/logs/BNClientLog.txt'
|
||||
if os.path.isfile(testpath):
|
||||
logFiles.append(testpath)
|
||||
print('Found nookStudy log file: ' + testpath)
|
||||
found = True
|
||||
testpath = home + '/Library/Application Support/Barnes & Noble/DesktopReader/indices/BNClientLog.txt'
|
||||
if os.path.isfile(testpath):
|
||||
logFiles.append(testpath)
|
||||
print('Found nookStudy log file: ' + testpath)
|
||||
found = True
|
||||
testpath = home + '/Library/Application Support/Barnes & Noble/BNDesktopReader/logs/BNClientLog.txt'
|
||||
if os.path.isfile(testpath):
|
||||
logFiles.append(testpath)
|
||||
print('Found nookStudy log file: ' + testpath)
|
||||
found = True
|
||||
testpath = home + '/Library/Application Support/Barnes & Noble/BNDesktopReader/indices/BNClientLog.txt'
|
||||
if os.path.isfile(testpath):
|
||||
logFiles.append(testpath)
|
||||
print('Found nookStudy log file: ' + testpath)
|
||||
found = True
|
||||
|
||||
if not found:
|
||||
print('No nook Study log files have been found.')
|
||||
return logFiles
|
||||
|
||||
|
||||
# Extract CCHash key(s) from log file
|
||||
def getKeysFromLog(kLogFile):
|
||||
keys = []
|
||||
regex = re.compile("ccHash: \"(.{28})\"");
|
||||
for line in open(kLogFile):
|
||||
for m in regex.findall(line):
|
||||
keys.append(m)
|
||||
return keys
|
||||
|
||||
# interface for calibre plugin
|
||||
def nookkeys(files = []):
|
||||
keys = []
|
||||
if files == []:
|
||||
files = getNookLogFiles()
|
||||
for file in files:
|
||||
fileKeys = getKeysFromLog(file)
|
||||
if fileKeys:
|
||||
print("Found {0} keys in the Nook Study log files".format(len(fileKeys)))
|
||||
keys.extend(fileKeys)
|
||||
return list(set(keys))
|
||||
|
||||
# interface for Python DeDRM
|
||||
# returns single key or multiple keys, depending on path or file passed in
|
||||
def getkey(outpath, files=[]):
|
||||
keys = nookkeys(files)
|
||||
if len(keys) > 0:
|
||||
if not os.path.isdir(outpath):
|
||||
outfile = outpath
|
||||
with open(outfile, 'w') as keyfileout:
|
||||
keyfileout.write(keys[-1])
|
||||
print("Saved a key to {0}".format(outfile))
|
||||
else:
|
||||
keycount = 0
|
||||
for key in keys:
|
||||
while True:
|
||||
keycount += 1
|
||||
outfile = os.path.join(outpath,"nookkey{0:d}.b64".format(keycount))
|
||||
if not os.path.exists(outfile):
|
||||
break
|
||||
with open(outfile, 'w') as keyfileout:
|
||||
keyfileout.write(key)
|
||||
print("Saved a key to {0}".format(outfile))
|
||||
return True
|
||||
return False
|
||||
|
||||
def usage(progname):
|
||||
print("Finds the nook Study encryption keys.")
|
||||
print("Keys are saved to the current directory, or a specified output directory.")
|
||||
print("If a file name is passed instead of a directory, only the first key is saved, in that file.")
|
||||
print("Usage:")
|
||||
print(" {0:s} [-h] [-k <logFile>] [<outpath>]".format(progname))
|
||||
|
||||
|
||||
def cli_main():
|
||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
argv=unicode_argv()
|
||||
progname = os.path.basename(argv[0])
|
||||
print("{0} v{1}\nCopyright © 2015 Apprentice Alf".format(progname,__version__))
|
||||
|
||||
try:
|
||||
opts, args = getopt.getopt(argv[1:], "hk:")
|
||||
except getopt.GetoptError as err:
|
||||
print("Error in options or arguments: {0}".format(err.args[0]))
|
||||
usage(progname)
|
||||
sys.exit(2)
|
||||
|
||||
files = []
|
||||
for o, a in opts:
|
||||
if o == "-h":
|
||||
usage(progname)
|
||||
sys.exit(0)
|
||||
if o == "-k":
|
||||
files = [a]
|
||||
|
||||
if len(args) > 1:
|
||||
usage(progname)
|
||||
sys.exit(2)
|
||||
|
||||
if len(args) == 1:
|
||||
# save to the specified file or directory
|
||||
outpath = args[0]
|
||||
if not os.path.isabs(outpath):
|
||||
outpath = os.path.abspath(outpath)
|
||||
else:
|
||||
# save to the same directory as the script
|
||||
outpath = os.path.dirname(argv[0])
|
||||
|
||||
# make sure the outpath is the
|
||||
outpath = os.path.realpath(os.path.normpath(outpath))
|
||||
|
||||
if not getkey(outpath, files):
|
||||
print("Could not retrieve nook Study key.")
|
||||
return 0
|
||||
|
||||
|
||||
def gui_main():
|
||||
try:
|
||||
import tkinter
|
||||
import tkinter.constants
|
||||
import tkinter.messagebox
|
||||
import traceback
|
||||
except:
|
||||
return cli_main()
|
||||
|
||||
class ExceptionDialog(tkinter.Frame):
|
||||
def __init__(self, root, text):
|
||||
tkinter.Frame.__init__(self, root, border=5)
|
||||
label = tkinter.Label(self, text="Unexpected error:",
|
||||
anchor=tkinter.constants.W, justify=tkinter.constants.LEFT)
|
||||
label.pack(fill=tkinter.constants.X, expand=0)
|
||||
self.text = tkinter.Text(self)
|
||||
self.text.pack(fill=tkinter.constants.BOTH, expand=1)
|
||||
|
||||
self.text.insert(tkinter.constants.END, text)
|
||||
|
||||
|
||||
argv=unicode_argv()
|
||||
root = tkinter.Tk()
|
||||
root.withdraw()
|
||||
progpath, progname = os.path.split(argv[0])
|
||||
success = False
|
||||
try:
|
||||
keys = nookkeys()
|
||||
keycount = 0
|
||||
for key in keys:
|
||||
print(key)
|
||||
while True:
|
||||
keycount += 1
|
||||
outfile = os.path.join(progpath,"nookkey{0:d}.b64".format(keycount))
|
||||
if not os.path.exists(outfile):
|
||||
break
|
||||
|
||||
with open(outfile, 'w') as keyfileout:
|
||||
keyfileout.write(key)
|
||||
success = True
|
||||
tkinter.messagebox.showinfo(progname, "Key successfully retrieved to {0}".format(outfile))
|
||||
except DrmException as e:
|
||||
tkinter.messagebox.showerror(progname, "Error: {0}".format(str(e)))
|
||||
except Exception:
|
||||
root.wm_state('normal')
|
||||
root.title(progname)
|
||||
text = traceback.format_exc()
|
||||
ExceptionDialog(root, text).pack(fill=tkinter.constants.BOTH, expand=1)
|
||||
root.mainloop()
|
||||
if not success:
|
||||
return 1
|
||||
return 0
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) > 1:
|
||||
sys.exit(cli_main())
|
||||
sys.exit(gui_main())
|
||||
254
DeDRM_plugin/ignoblekeyfetch.py
Normal file
254
DeDRM_plugin/ignoblekeyfetch.py
Normal file
@@ -0,0 +1,254 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# ignoblekeyfetch.py
|
||||
# Copyright © 2015-2020 Apprentice Harper et al.
|
||||
|
||||
# Released under the terms of the GNU General Public Licence, version 3
|
||||
# <http://www.gnu.org/licenses/>
|
||||
|
||||
# Based on discoveries by "Nobody You Know"
|
||||
# Code partly based on ignoblekeygen.py by several people.
|
||||
|
||||
# Windows users: Before running this program, you must first install Python.
|
||||
# We recommend ActiveState Python 2.7.X for Windows from
|
||||
# http://www.activestate.com/activepython/downloads.
|
||||
# Then save this script file as ignoblekeyfetch.pyw and double-click on it to run it.
|
||||
#
|
||||
# Mac OS X users: Save this script file as ignoblekeyfetch.pyw. You can run this
|
||||
# program from the command line (python ignoblekeyfetch.pyw) or by double-clicking
|
||||
# it when it has been associated with PythonLauncher.
|
||||
|
||||
# Revision history:
|
||||
# 1.0 - Initial version
|
||||
# 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
|
||||
"""
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = "2.0"
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# 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):
|
||||
data = data.encode(self.encoding,"replace")
|
||||
self.stream.buffer.write(data)
|
||||
self.stream.buffer.flush()
|
||||
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
try:
|
||||
from calibre.constants import iswindows, isosx
|
||||
except:
|
||||
iswindows = sys.platform.startswith('win')
|
||||
isosx = sys.platform.startswith('darwin')
|
||||
|
||||
def unicode_argv():
|
||||
if iswindows:
|
||||
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
||||
# strings.
|
||||
|
||||
# Versions 2.x of Python don't support Unicode in sys.argv on
|
||||
# Windows, with the underlying Windows API instead replacing multi-byte
|
||||
# characters with '?'. So use shell32.GetCommandLineArgvW to get sys.argv
|
||||
# as a list of Unicode strings and encode them as utf-8
|
||||
|
||||
from ctypes import POINTER, byref, cdll, c_int, windll
|
||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
||||
|
||||
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
||||
GetCommandLineW.argtypes = []
|
||||
GetCommandLineW.restype = LPCWSTR
|
||||
|
||||
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
||||
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
||||
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
||||
|
||||
cmd = GetCommandLineW()
|
||||
argc = c_int(0)
|
||||
argv = CommandLineToArgvW(cmd, byref(argc))
|
||||
if argc.value > 0:
|
||||
# Remove Python executable and commands if present
|
||||
start = argc.value - len(sys.argv)
|
||||
return [argv[i] for i in
|
||||
range(start, argc.value)]
|
||||
# if we don't have any arguments at all, just pass back script name
|
||||
# this should never happen
|
||||
return ["ignoblekeyfetch.py"]
|
||||
else:
|
||||
argvencoding = sys.stdin.encoding or "utf-8"
|
||||
return [arg if isinstance(arg, str) else str(arg, argvencoding) for arg in sys.argv]
|
||||
|
||||
|
||||
class IGNOBLEError(Exception):
|
||||
pass
|
||||
|
||||
def fetch_key(email, password):
|
||||
# change email and password to utf-8 if unicode
|
||||
if type(email)==str:
|
||||
email = email.encode('utf-8')
|
||||
if type(password)==str:
|
||||
password = password.encode('utf-8')
|
||||
|
||||
import random
|
||||
random = "%030x" % random.randrange(16**30)
|
||||
|
||||
import urllib.parse, urllib.request, re
|
||||
|
||||
# try the URL from nook for PC
|
||||
fetch_url = "https://cart4.barnesandnoble.com/services/service.aspx?Version=2&acctPassword="
|
||||
fetch_url += urllib.parse.quote(password,'')+"&devID=PC_BN_2.5.6.9575_"+random+"&emailAddress="
|
||||
fetch_url += urllib.parse.quote(email,"")+"&outFormat=5&schema=1&service=1&stage=deviceHashB"
|
||||
#print fetch_url
|
||||
|
||||
found = ''
|
||||
try:
|
||||
response = urllib.request.urlopen(fetch_url)
|
||||
the_page = response.read()
|
||||
#print the_page
|
||||
found = re.search('ccHash>(.+?)</ccHash', the_page).group(1)
|
||||
except:
|
||||
found = ''
|
||||
if len(found)!=28:
|
||||
# try the URL from android devices
|
||||
fetch_url = "https://cart4.barnesandnoble.com/services/service.aspx?Version=2&acctPassword="
|
||||
fetch_url += urllib.parse.quote(password,'')+"&devID=hobbes_9.3.50818_"+random+"&emailAddress="
|
||||
fetch_url += urllib.parse.quote(email,"")+"&outFormat=5&schema=1&service=1&stage=deviceHashB"
|
||||
#print fetch_url
|
||||
|
||||
found = ''
|
||||
try:
|
||||
response = urllib.request.urlopen(fetch_url)
|
||||
the_page = response.read()
|
||||
#print the_page
|
||||
found = re.search('ccHash>(.+?)</ccHash', the_page).group(1)
|
||||
except:
|
||||
found = ''
|
||||
|
||||
return found
|
||||
|
||||
|
||||
|
||||
|
||||
def cli_main():
|
||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
argv=unicode_argv()
|
||||
progname = os.path.basename(argv[0])
|
||||
if len(argv) != 4:
|
||||
print("usage: {0} <email> <password> <keyfileout.b64>".format(progname))
|
||||
return 1
|
||||
email, password, keypath = argv[1:]
|
||||
userkey = fetch_key(email, password)
|
||||
if len(userkey) == 28:
|
||||
open(keypath,'wb').write(userkey)
|
||||
return 0
|
||||
print("Failed to fetch key.")
|
||||
return 1
|
||||
|
||||
|
||||
def gui_main():
|
||||
try:
|
||||
import tkinter
|
||||
import tkinter.filedialog
|
||||
import tkinter.constants
|
||||
import tkinter.messagebox
|
||||
import traceback
|
||||
except:
|
||||
return cli_main()
|
||||
|
||||
class DecryptionDialog(tkinter.Frame):
|
||||
def __init__(self, root):
|
||||
tkinter.Frame.__init__(self, root, border=5)
|
||||
self.status = tkinter.Label(self, text="Enter parameters")
|
||||
self.status.pack(fill=tkinter.constants.X, expand=1)
|
||||
body = tkinter.Frame(self)
|
||||
body.pack(fill=tkinter.constants.X, expand=1)
|
||||
sticky = tkinter.constants.E + tkinter.constants.W
|
||||
body.grid_columnconfigure(1, weight=2)
|
||||
tkinter.Label(body, text="Account email address").grid(row=0)
|
||||
self.name = tkinter.Entry(body, width=40)
|
||||
self.name.grid(row=0, column=1, sticky=sticky)
|
||||
tkinter.Label(body, text="Account password").grid(row=1)
|
||||
self.ccn = tkinter.Entry(body, width=40)
|
||||
self.ccn.grid(row=1, column=1, sticky=sticky)
|
||||
tkinter.Label(body, text="Output file").grid(row=2)
|
||||
self.keypath = tkinter.Entry(body, width=40)
|
||||
self.keypath.grid(row=2, column=1, sticky=sticky)
|
||||
self.keypath.insert(2, "bnepubkey.b64")
|
||||
button = tkinter.Button(body, text="...", command=self.get_keypath)
|
||||
button.grid(row=2, column=2)
|
||||
buttons = tkinter.Frame(self)
|
||||
buttons.pack()
|
||||
botton = tkinter.Button(
|
||||
buttons, text="Fetch", width=10, command=self.generate)
|
||||
botton.pack(side=tkinter.constants.LEFT)
|
||||
tkinter.Frame(buttons, width=10).pack(side=tkinter.constants.LEFT)
|
||||
button = tkinter.Button(
|
||||
buttons, text="Quit", width=10, command=self.quit)
|
||||
button.pack(side=tkinter.constants.RIGHT)
|
||||
|
||||
def get_keypath(self):
|
||||
keypath = tkinter.filedialog.asksaveasfilename(
|
||||
parent=None, title="Select B&N ePub key file to produce",
|
||||
defaultextension=".b64",
|
||||
filetypes=[('base64-encoded files', '.b64'),
|
||||
('All Files', '.*')])
|
||||
if keypath:
|
||||
keypath = os.path.normpath(keypath)
|
||||
self.keypath.delete(0, tkinter.constants.END)
|
||||
self.keypath.insert(0, keypath)
|
||||
return
|
||||
|
||||
def generate(self):
|
||||
email = self.name.get()
|
||||
password = self.ccn.get()
|
||||
keypath = self.keypath.get()
|
||||
if not email:
|
||||
self.status['text'] = "Email address not given"
|
||||
return
|
||||
if not password:
|
||||
self.status['text'] = "Account password not given"
|
||||
return
|
||||
if not keypath:
|
||||
self.status['text'] = "Output keyfile path not set"
|
||||
return
|
||||
self.status['text'] = "Fetching..."
|
||||
try:
|
||||
userkey = fetch_key(email, password)
|
||||
except Exception as e:
|
||||
self.status['text'] = "Error: {0}".format(e.args[0])
|
||||
return
|
||||
if len(userkey) == 28:
|
||||
open(keypath,'wb').write(userkey)
|
||||
self.status['text'] = "Keyfile fetched successfully"
|
||||
else:
|
||||
self.status['text'] = "Keyfile fetch failed."
|
||||
|
||||
root = tkinter.Tk()
|
||||
root.title("Barnes & Noble ePub Keyfile Fetch v.{0}".format(__version__))
|
||||
root.resizable(True, False)
|
||||
root.minsize(300, 0)
|
||||
DecryptionDialog(root).pack(fill=tkinter.constants.X, expand=1)
|
||||
root.mainloop()
|
||||
return 0
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) > 1:
|
||||
sys.exit(cli_main())
|
||||
sys.exit(gui_main())
|
||||
330
DeDRM_plugin/ignoblekeygen.py
Normal file
330
DeDRM_plugin/ignoblekeygen.py
Normal file
@@ -0,0 +1,330 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# ignoblekeygen.py
|
||||
# Copyright © 2009-2020 i♥cabbages, Apprentice Harper et al.
|
||||
|
||||
# Released under the terms of the GNU General Public Licence, version 3
|
||||
# <http://www.gnu.org/licenses/>
|
||||
|
||||
# Windows users: Before running this program, you must first install Python.
|
||||
# We recommend ActiveState Python 2.7.X for Windows (x86) from
|
||||
# http://www.activestate.com/activepython/downloads.
|
||||
# You must also install PyCrypto from
|
||||
# http://www.voidspace.org.uk/python/modules.shtml#pycrypto
|
||||
# (make certain to install the version for Python 2.7).
|
||||
# Then save this script file as ignoblekeygen.pyw and double-click on it to run it.
|
||||
#
|
||||
# Mac OS X users: Save this script file as ignoblekeygen.pyw. You can run this
|
||||
# program from the command line (python ignoblekeygen.pyw) or by double-clicking
|
||||
# it when it has been associated with PythonLauncher.
|
||||
|
||||
# Revision history:
|
||||
# 1 - Initial release
|
||||
# 2 - Add OS X support by using OpenSSL when available (taken/modified from ineptepub v5)
|
||||
# 2.1 - Allow Windows versions of libcrypto to be found
|
||||
# 2.2 - On Windows try PyCrypto first and then OpenSSL next
|
||||
# 2.3 - Modify interface to allow use of import
|
||||
# 2.4 - Improvements to UI and now works in plugins
|
||||
# 2.5 - Additional improvement for unicode and plugin support
|
||||
# 2.6 - moved unicode_argv call inside main for Windows DeDRM compatibility
|
||||
# 2.7 - Work if TkInter is missing
|
||||
# 2.8 - Fix bug in stand-alone use (import tkFileDialog)
|
||||
# 3.0 - Added Python 3 compatibility for calibre 5.0
|
||||
|
||||
"""
|
||||
Generate Barnes & Noble EPUB user key from name and credit card number.
|
||||
"""
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = "3.0"
|
||||
|
||||
import sys
|
||||
import os
|
||||
import hashlib
|
||||
import base64
|
||||
|
||||
# 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):
|
||||
data = data.encode(self.encoding,"replace")
|
||||
self.stream.buffer.write(data)
|
||||
self.stream.buffer.flush()
|
||||
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
try:
|
||||
from calibre.constants import iswindows, isosx
|
||||
except:
|
||||
iswindows = sys.platform.startswith('win')
|
||||
isosx = sys.platform.startswith('darwin')
|
||||
|
||||
def unicode_argv():
|
||||
if iswindows:
|
||||
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
||||
# strings.
|
||||
|
||||
# Versions 2.x of Python don't support Unicode in sys.argv on
|
||||
# Windows, with the underlying Windows API instead replacing multi-byte
|
||||
# characters with '?'. So use shell32.GetCommandLineArgvW to get sys.argv
|
||||
# as a list of Unicode strings and encode them as utf-8
|
||||
|
||||
from ctypes import POINTER, byref, cdll, c_int, windll
|
||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
||||
|
||||
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
||||
GetCommandLineW.argtypes = []
|
||||
GetCommandLineW.restype = LPCWSTR
|
||||
|
||||
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
||||
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
||||
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
||||
|
||||
cmd = GetCommandLineW()
|
||||
argc = c_int(0)
|
||||
argv = CommandLineToArgvW(cmd, byref(argc))
|
||||
if argc.value > 0:
|
||||
# Remove Python executable and commands if present
|
||||
start = argc.value - len(sys.argv)
|
||||
return [argv[i] for i in
|
||||
range(start, argc.value)]
|
||||
# if we don't have any arguments at all, just pass back script name
|
||||
# this should never happen
|
||||
return ["ignoblekeygen.py"]
|
||||
else:
|
||||
argvencoding = sys.stdin.encoding or "utf-8"
|
||||
return [arg if isinstance(arg, str) else str(arg, argvencoding) for arg in sys.argv]
|
||||
|
||||
|
||||
class IGNOBLEError(Exception):
|
||||
pass
|
||||
|
||||
def _load_crypto_libcrypto():
|
||||
from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_int, c_long, \
|
||||
Structure, c_ulong, create_string_buffer, cast
|
||||
from ctypes.util import find_library
|
||||
|
||||
if iswindows:
|
||||
libcrypto = find_library('libeay32')
|
||||
else:
|
||||
libcrypto = find_library('crypto')
|
||||
|
||||
if libcrypto is None:
|
||||
raise IGNOBLEError('libcrypto not found')
|
||||
libcrypto = CDLL(libcrypto)
|
||||
|
||||
AES_MAXNR = 14
|
||||
|
||||
c_char_pp = POINTER(c_char_p)
|
||||
c_int_p = POINTER(c_int)
|
||||
|
||||
class AES_KEY(Structure):
|
||||
_fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))),
|
||||
('rounds', c_int)]
|
||||
AES_KEY_p = POINTER(AES_KEY)
|
||||
|
||||
def F(restype, name, argtypes):
|
||||
func = getattr(libcrypto, name)
|
||||
func.restype = restype
|
||||
func.argtypes = argtypes
|
||||
return func
|
||||
|
||||
AES_set_encrypt_key = F(c_int, 'AES_set_encrypt_key',
|
||||
[c_char_p, c_int, AES_KEY_p])
|
||||
AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',
|
||||
[c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p,
|
||||
c_int])
|
||||
|
||||
class AES(object):
|
||||
def __init__(self, userkey, iv):
|
||||
self._blocksize = len(userkey)
|
||||
self._iv = iv
|
||||
key = self._key = AES_KEY()
|
||||
rv = AES_set_encrypt_key(userkey, len(userkey) * 8, key)
|
||||
if rv < 0:
|
||||
raise IGNOBLEError('Failed to initialize AES Encrypt key')
|
||||
|
||||
def encrypt(self, data):
|
||||
out = create_string_buffer(len(data))
|
||||
rv = AES_cbc_encrypt(data, out, len(data), self._key, self._iv, 1)
|
||||
if rv == 0:
|
||||
raise IGNOBLEError('AES encryption failed')
|
||||
return out.raw
|
||||
|
||||
return AES
|
||||
|
||||
def _load_crypto_pycrypto():
|
||||
from Crypto.Cipher import AES as _AES
|
||||
|
||||
class AES(object):
|
||||
def __init__(self, key, iv):
|
||||
self._aes = _AES.new(key, _AES.MODE_CBC, iv)
|
||||
|
||||
def encrypt(self, data):
|
||||
return self._aes.encrypt(data)
|
||||
|
||||
return AES
|
||||
|
||||
def _load_crypto():
|
||||
AES = None
|
||||
cryptolist = (_load_crypto_libcrypto, _load_crypto_pycrypto)
|
||||
if sys.platform.startswith('win'):
|
||||
cryptolist = (_load_crypto_pycrypto, _load_crypto_libcrypto)
|
||||
for loader in cryptolist:
|
||||
try:
|
||||
AES = loader()
|
||||
break
|
||||
except (ImportError, IGNOBLEError):
|
||||
pass
|
||||
return AES
|
||||
|
||||
AES = _load_crypto()
|
||||
|
||||
def normalize_name(name):
|
||||
return ''.join(x for x in name.lower() if x != ' ')
|
||||
|
||||
|
||||
def generate_key(name, ccn):
|
||||
# remove spaces and case from name and CC numbers.
|
||||
name = normalize_name(name)
|
||||
ccn = normalize_name(ccn)
|
||||
|
||||
if type(name)==str:
|
||||
name = name.encode('utf-8')
|
||||
if type(ccn)==str:
|
||||
ccn = ccn.encode('utf-8')
|
||||
|
||||
name = name + b'\x00'
|
||||
ccn = ccn + b'\x00'
|
||||
|
||||
name_sha = hashlib.sha1(name).digest()[:16]
|
||||
ccn_sha = hashlib.sha1(ccn).digest()[:16]
|
||||
both_sha = hashlib.sha1(name + ccn).digest()
|
||||
aes = AES(ccn_sha, name_sha)
|
||||
crypt = aes.encrypt(both_sha + (b'\x0c' * 0x0c))
|
||||
userkey = hashlib.sha1(crypt).digest()
|
||||
return base64.b64encode(userkey)
|
||||
|
||||
|
||||
def cli_main():
|
||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
argv=unicode_argv()
|
||||
progname = os.path.basename(argv[0])
|
||||
if AES is None:
|
||||
print("%s: This script requires OpenSSL or PyCrypto, which must be installed " \
|
||||
"separately. Read the top-of-script comment for details." % \
|
||||
(progname,))
|
||||
return 1
|
||||
if len(argv) != 4:
|
||||
print("usage: {0} <Name> <CC#> <keyfileout.b64>".format(progname))
|
||||
return 1
|
||||
name, ccn, keypath = argv[1:]
|
||||
userkey = generate_key(name, ccn)
|
||||
open(keypath,'wb').write(userkey)
|
||||
return 0
|
||||
|
||||
|
||||
def gui_main():
|
||||
try:
|
||||
import tkinter
|
||||
import tkinter.constants
|
||||
import tkinter.messagebox
|
||||
import tkinter.filedialog
|
||||
import traceback
|
||||
except:
|
||||
return cli_main()
|
||||
|
||||
class DecryptionDialog(tkinter.Frame):
|
||||
def __init__(self, root):
|
||||
tkinter.Frame.__init__(self, root, border=5)
|
||||
self.status = tkinter.Label(self, text="Enter parameters")
|
||||
self.status.pack(fill=tkinter.constants.X, expand=1)
|
||||
body = tkinter.Frame(self)
|
||||
body.pack(fill=tkinter.constants.X, expand=1)
|
||||
sticky = tkinter.constants.E + tkinter.constants.W
|
||||
body.grid_columnconfigure(1, weight=2)
|
||||
tkinter.Label(body, text="Account Name").grid(row=0)
|
||||
self.name = tkinter.Entry(body, width=40)
|
||||
self.name.grid(row=0, column=1, sticky=sticky)
|
||||
tkinter.Label(body, text="CC#").grid(row=1)
|
||||
self.ccn = tkinter.Entry(body, width=40)
|
||||
self.ccn.grid(row=1, column=1, sticky=sticky)
|
||||
tkinter.Label(body, text="Output file").grid(row=2)
|
||||
self.keypath = tkinter.Entry(body, width=40)
|
||||
self.keypath.grid(row=2, column=1, sticky=sticky)
|
||||
self.keypath.insert(2, "bnepubkey.b64")
|
||||
button = tkinter.Button(body, text="...", command=self.get_keypath)
|
||||
button.grid(row=2, column=2)
|
||||
buttons = tkinter.Frame(self)
|
||||
buttons.pack()
|
||||
botton = tkinter.Button(
|
||||
buttons, text="Generate", width=10, command=self.generate)
|
||||
botton.pack(side=tkinter.constants.LEFT)
|
||||
tkinter.Frame(buttons, width=10).pack(side=tkinter.constants.LEFT)
|
||||
button = tkinter.Button(
|
||||
buttons, text="Quit", width=10, command=self.quit)
|
||||
button.pack(side=tkinter.constants.RIGHT)
|
||||
|
||||
def get_keypath(self):
|
||||
keypath = tkinter.filedialog.asksaveasfilename(
|
||||
parent=None, title="Select B&N ePub key file to produce",
|
||||
defaultextension=".b64",
|
||||
filetypes=[('base64-encoded files', '.b64'),
|
||||
('All Files', '.*')])
|
||||
if keypath:
|
||||
keypath = os.path.normpath(keypath)
|
||||
self.keypath.delete(0, tkinter.constants.END)
|
||||
self.keypath.insert(0, keypath)
|
||||
return
|
||||
|
||||
def generate(self):
|
||||
name = self.name.get()
|
||||
ccn = self.ccn.get()
|
||||
keypath = self.keypath.get()
|
||||
if not name:
|
||||
self.status['text'] = "Name not specified"
|
||||
return
|
||||
if not ccn:
|
||||
self.status['text'] = "Credit card number not specified"
|
||||
return
|
||||
if not keypath:
|
||||
self.status['text'] = "Output keyfile path not specified"
|
||||
return
|
||||
self.status['text'] = "Generating..."
|
||||
try:
|
||||
userkey = generate_key(name, ccn)
|
||||
except Exception as e:
|
||||
self.status['text'] = "Error: (0}".format(e.args[0])
|
||||
return
|
||||
open(keypath,'wb').write(userkey)
|
||||
self.status['text'] = "Keyfile successfully generated"
|
||||
|
||||
root = tkinter.Tk()
|
||||
if AES is None:
|
||||
root.withdraw()
|
||||
tkinter.messagebox.showerror(
|
||||
"Ignoble EPUB Keyfile Generator",
|
||||
"This script requires OpenSSL or PyCrypto, which must be installed "
|
||||
"separately. Read the top-of-script comment for details.")
|
||||
return 1
|
||||
root.title("Barnes & Noble ePub Keyfile Generator v.{0}".format(__version__))
|
||||
root.resizable(True, False)
|
||||
root.minsize(300, 0)
|
||||
DecryptionDialog(root).pack(fill=tkinter.constants.X, expand=1)
|
||||
root.mainloop()
|
||||
return 0
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) > 1:
|
||||
sys.exit(cli_main())
|
||||
sys.exit(gui_main())
|
||||
File diff suppressed because it is too large
Load Diff
600
DeDRM_plugin/ineptepub.py
Normal file
600
DeDRM_plugin/ineptepub.py
Normal file
@@ -0,0 +1,600 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# ineptepub.py
|
||||
# Copyright © 2009-2020 by i♥cabbages, Apprentice Harper et al.
|
||||
|
||||
# Released under the terms of the GNU General Public Licence, version 3
|
||||
# <http://www.gnu.org/licenses/>
|
||||
|
||||
|
||||
# Revision history:
|
||||
# 1 - Initial release
|
||||
# 2 - Rename to INEPT, fix exit code
|
||||
# 5 - Version bump to avoid (?) confusion;
|
||||
# Improve OS X support by using OpenSSL when available
|
||||
# 5.1 - Improve OpenSSL error checking
|
||||
# 5.2 - Fix ctypes error causing segfaults on some systems
|
||||
# 5.3 - add support for OpenSSL on Windows, fix bug with some versions of libcrypto 0.9.8 prior to path level o
|
||||
# 5.4 - add support for encoding to 'utf-8' when building up list of files to decrypt from encryption.xml
|
||||
# 5.5 - On Windows try PyCrypto first, OpenSSL next
|
||||
# 5.6 - Modify interface to allow use with import
|
||||
# 5.7 - Fix for potential problem with PyCrypto
|
||||
# 5.8 - Revised to allow use in calibre plugins to eliminate need for duplicate code
|
||||
# 5.9 - Fixed to retain zip file metadata (e.g. file modification date)
|
||||
# 6.0 - moved unicode_argv call inside main for Windows DeDRM compatibility
|
||||
# 6.1 - Work if TkInter is missing
|
||||
# 6.2 - Handle UTF-8 file names inside an ePub, fix by Jose Luis
|
||||
# 6.3 - Add additional check on DER file sanity
|
||||
# 6.4 - Remove erroneous check on DER file sanity
|
||||
# 6.5 - Completely remove erroneous check on DER file sanity
|
||||
# 6.6 - Import tkFileDialog, don't assume something else will import it.
|
||||
# 7.0 - Add Python 3 compatibility for calibre 5.0
|
||||
|
||||
"""
|
||||
Decrypt Adobe Digital Editions encrypted ePub books.
|
||||
"""
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = "7.0"
|
||||
|
||||
import codecs
|
||||
import sys
|
||||
import os
|
||||
import traceback
|
||||
import zlib
|
||||
import zipfile
|
||||
from zipfile import ZipInfo, ZipFile, ZIP_STORED, ZIP_DEFLATED
|
||||
from contextlib import closing
|
||||
import xml.etree.ElementTree as etree
|
||||
|
||||
# Wrap a stream so that output gets flushed immediately
|
||||
# and also make sure that any unicode strings get
|
||||
# encoded using "replace" before writing them.
|
||||
class SafeUnbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
self.encoding = stream.encoding
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
def write(self, data):
|
||||
if isinstance(data, str):
|
||||
data = data.encode(self.encoding,"replace")
|
||||
self.stream.buffer.write(data)
|
||||
self.stream.buffer.flush()
|
||||
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
try:
|
||||
from calibre.constants import iswindows, isosx
|
||||
except:
|
||||
iswindows = sys.platform.startswith('win')
|
||||
isosx = sys.platform.startswith('darwin')
|
||||
|
||||
def unicode_argv():
|
||||
if iswindows:
|
||||
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
||||
# strings.
|
||||
|
||||
# Versions 2.x of Python don't support Unicode in sys.argv on
|
||||
# Windows, with the underlying Windows API instead replacing multi-byte
|
||||
# characters with '?'.
|
||||
|
||||
|
||||
from ctypes import POINTER, byref, cdll, c_int, windll
|
||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
||||
|
||||
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
||||
GetCommandLineW.argtypes = []
|
||||
GetCommandLineW.restype = LPCWSTR
|
||||
|
||||
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
||||
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
||||
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
||||
|
||||
cmd = GetCommandLineW()
|
||||
argc = c_int(0)
|
||||
argv = CommandLineToArgvW(cmd, byref(argc))
|
||||
if argc.value > 0:
|
||||
# Remove Python executable and commands if present
|
||||
start = argc.value - len(sys.argv)
|
||||
return [argv[i] for i in
|
||||
range(start, argc.value)]
|
||||
return ["ineptepub.py"]
|
||||
else:
|
||||
argvencoding = sys.stdin.encoding or "utf-8"
|
||||
return [arg if isinstance(arg, str) else str(arg, argvencoding) for arg in sys.argv]
|
||||
|
||||
|
||||
class ADEPTError(Exception):
|
||||
pass
|
||||
|
||||
def _load_crypto_libcrypto():
|
||||
from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_int, c_long, \
|
||||
Structure, c_ulong, create_string_buffer, cast
|
||||
from ctypes.util import find_library
|
||||
|
||||
if iswindows:
|
||||
libcrypto = find_library('libeay32')
|
||||
else:
|
||||
libcrypto = find_library('crypto')
|
||||
|
||||
if libcrypto is None:
|
||||
raise ADEPTError('libcrypto not found')
|
||||
libcrypto = CDLL(libcrypto)
|
||||
|
||||
RSA_NO_PADDING = 3
|
||||
AES_MAXNR = 14
|
||||
|
||||
c_char_pp = POINTER(c_char_p)
|
||||
c_int_p = POINTER(c_int)
|
||||
|
||||
class RSA(Structure):
|
||||
pass
|
||||
RSA_p = POINTER(RSA)
|
||||
|
||||
class AES_KEY(Structure):
|
||||
_fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))),
|
||||
('rounds', c_int)]
|
||||
AES_KEY_p = POINTER(AES_KEY)
|
||||
|
||||
def F(restype, name, argtypes):
|
||||
func = getattr(libcrypto, name)
|
||||
func.restype = restype
|
||||
func.argtypes = argtypes
|
||||
return func
|
||||
|
||||
d2i_RSAPrivateKey = F(RSA_p, 'd2i_RSAPrivateKey',
|
||||
[RSA_p, c_char_pp, c_long])
|
||||
RSA_size = F(c_int, 'RSA_size', [RSA_p])
|
||||
RSA_private_decrypt = F(c_int, 'RSA_private_decrypt',
|
||||
[c_int, c_char_p, c_char_p, RSA_p, c_int])
|
||||
RSA_free = F(None, 'RSA_free', [RSA_p])
|
||||
AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key',
|
||||
[c_char_p, c_int, AES_KEY_p])
|
||||
AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',
|
||||
[c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p,
|
||||
c_int])
|
||||
|
||||
class RSA(object):
|
||||
def __init__(self, der):
|
||||
buf = create_string_buffer(der)
|
||||
pp = c_char_pp(cast(buf, c_char_p))
|
||||
rsa = self._rsa = d2i_RSAPrivateKey(None, pp, len(der))
|
||||
if rsa is None:
|
||||
raise ADEPTError('Error parsing ADEPT user key DER')
|
||||
|
||||
def decrypt(self, from_):
|
||||
rsa = self._rsa
|
||||
to = create_string_buffer(RSA_size(rsa))
|
||||
dlen = RSA_private_decrypt(len(from_), from_, to, rsa,
|
||||
RSA_NO_PADDING)
|
||||
if dlen < 0:
|
||||
raise ADEPTError('RSA decryption failed')
|
||||
return to[:dlen]
|
||||
|
||||
def __del__(self):
|
||||
if self._rsa is not None:
|
||||
RSA_free(self._rsa)
|
||||
self._rsa = None
|
||||
|
||||
class AES(object):
|
||||
def __init__(self, userkey):
|
||||
self._blocksize = len(userkey)
|
||||
if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) :
|
||||
raise ADEPTError('AES improper key used')
|
||||
return
|
||||
key = self._key = AES_KEY()
|
||||
rv = AES_set_decrypt_key(userkey, len(userkey) * 8, key)
|
||||
if rv < 0:
|
||||
raise ADEPTError('Failed to initialize AES key')
|
||||
|
||||
def decrypt(self, data):
|
||||
out = create_string_buffer(len(data))
|
||||
iv = (b"\x00" * self._blocksize)
|
||||
rv = AES_cbc_encrypt(data, out, len(data), self._key, iv, 0)
|
||||
if rv == 0:
|
||||
raise ADEPTError('AES decryption failed')
|
||||
return out.raw
|
||||
|
||||
return (AES, RSA)
|
||||
|
||||
def _load_crypto_pycrypto():
|
||||
from Crypto.Cipher import AES as _AES
|
||||
from Crypto.PublicKey import RSA as _RSA
|
||||
from Crypto.Cipher import PKCS1_v1_5 as _PKCS1_v1_5
|
||||
|
||||
# ASN.1 parsing code from tlslite
|
||||
class ASN1Error(Exception):
|
||||
pass
|
||||
|
||||
class ASN1Parser(object):
|
||||
class Parser(object):
|
||||
def __init__(self, bytes):
|
||||
self.bytes = bytes
|
||||
self.index = 0
|
||||
|
||||
def get(self, length):
|
||||
if self.index + length > len(self.bytes):
|
||||
raise ASN1Error("Error decoding ASN.1")
|
||||
x = 0
|
||||
for count in range(length):
|
||||
x <<= 8
|
||||
x |= self.bytes[self.index]
|
||||
self.index += 1
|
||||
return x
|
||||
|
||||
def getFixBytes(self, lengthBytes):
|
||||
bytes = self.bytes[self.index : self.index+lengthBytes]
|
||||
self.index += lengthBytes
|
||||
return bytes
|
||||
|
||||
def getVarBytes(self, lengthLength):
|
||||
lengthBytes = self.get(lengthLength)
|
||||
return self.getFixBytes(lengthBytes)
|
||||
|
||||
def getFixList(self, length, lengthList):
|
||||
l = [0] * lengthList
|
||||
for x in range(lengthList):
|
||||
l[x] = self.get(length)
|
||||
return l
|
||||
|
||||
def getVarList(self, length, lengthLength):
|
||||
lengthList = self.get(lengthLength)
|
||||
if lengthList % length != 0:
|
||||
raise ASN1Error("Error decoding ASN.1")
|
||||
lengthList = int(lengthList/length)
|
||||
l = [0] * lengthList
|
||||
for x in range(lengthList):
|
||||
l[x] = self.get(length)
|
||||
return l
|
||||
|
||||
def startLengthCheck(self, lengthLength):
|
||||
self.lengthCheck = self.get(lengthLength)
|
||||
self.indexCheck = self.index
|
||||
|
||||
def setLengthCheck(self, length):
|
||||
self.lengthCheck = length
|
||||
self.indexCheck = self.index
|
||||
|
||||
def stopLengthCheck(self):
|
||||
if (self.index - self.indexCheck) != self.lengthCheck:
|
||||
raise ASN1Error("Error decoding ASN.1")
|
||||
|
||||
def atLengthCheck(self):
|
||||
if (self.index - self.indexCheck) < self.lengthCheck:
|
||||
return False
|
||||
elif (self.index - self.indexCheck) == self.lengthCheck:
|
||||
return True
|
||||
else:
|
||||
raise ASN1Error("Error decoding ASN.1")
|
||||
|
||||
def __init__(self, bytes):
|
||||
p = self.Parser(bytes)
|
||||
p.get(1)
|
||||
self.length = self._getASN1Length(p)
|
||||
self.value = p.getFixBytes(self.length)
|
||||
|
||||
def getChild(self, which):
|
||||
p = self.Parser(self.value)
|
||||
for x in range(which+1):
|
||||
markIndex = p.index
|
||||
p.get(1)
|
||||
length = self._getASN1Length(p)
|
||||
p.getFixBytes(length)
|
||||
return ASN1Parser(p.bytes[markIndex:p.index])
|
||||
|
||||
def _getASN1Length(self, p):
|
||||
firstLength = p.get(1)
|
||||
if firstLength<=127:
|
||||
return firstLength
|
||||
else:
|
||||
lengthLength = firstLength & 0x7F
|
||||
return p.get(lengthLength)
|
||||
|
||||
class AES(object):
|
||||
def __init__(self, key):
|
||||
self._aes = _AES.new(key, _AES.MODE_CBC, b'\x00'*16)
|
||||
|
||||
def decrypt(self, data):
|
||||
return self._aes.decrypt(data)
|
||||
|
||||
class RSA(object):
|
||||
def __init__(self, der):
|
||||
key = ASN1Parser([x for x in der])
|
||||
key = [key.getChild(x).value for x in range(1, 4)]
|
||||
key = [self.bytesToNumber(v) for v in key]
|
||||
self._rsa = _RSA.construct(key)
|
||||
|
||||
def bytesToNumber(self, bytes):
|
||||
total = 0
|
||||
for byte in bytes:
|
||||
total = (total << 8) + byte
|
||||
return total
|
||||
|
||||
def decrypt(self, data):
|
||||
return _PKCS1_v1_5.new(self._rsa).decrypt(data, 172)
|
||||
|
||||
return (AES, RSA)
|
||||
|
||||
def _load_crypto():
|
||||
AES = RSA = None
|
||||
cryptolist = (_load_crypto_libcrypto, _load_crypto_pycrypto)
|
||||
if sys.platform.startswith('win'):
|
||||
cryptolist = (_load_crypto_pycrypto, _load_crypto_libcrypto)
|
||||
for loader in cryptolist:
|
||||
try:
|
||||
AES, RSA = loader()
|
||||
break
|
||||
except (ImportError, ADEPTError):
|
||||
pass
|
||||
return (AES, RSA)
|
||||
|
||||
AES, RSA = _load_crypto()
|
||||
|
||||
META_NAMES = ('mimetype', 'META-INF/rights.xml', 'META-INF/encryption.xml')
|
||||
NSMAP = {'adept': 'http://ns.adobe.com/adept',
|
||||
'enc': 'http://www.w3.org/2001/04/xmlenc#'}
|
||||
|
||||
class Decryptor(object):
|
||||
def __init__(self, bookkey, encryption):
|
||||
enc = lambda tag: '{%s}%s' % (NSMAP['enc'], tag)
|
||||
self._aes = AES(bookkey)
|
||||
encryption = etree.fromstring(encryption)
|
||||
self._encrypted = encrypted = set()
|
||||
expr = './%s/%s/%s' % (enc('EncryptedData'), enc('CipherData'),
|
||||
enc('CipherReference'))
|
||||
for elem in encryption.findall(expr):
|
||||
path = elem.get('URI', None)
|
||||
if path is not None:
|
||||
path = path.encode('utf-8')
|
||||
encrypted.add(path)
|
||||
|
||||
def decompress(self, bytes):
|
||||
dc = zlib.decompressobj(-15)
|
||||
try:
|
||||
decompressed_bytes = dc.decompress(bytes)
|
||||
ex = dc.decompress(b'Z') + dc.flush()
|
||||
if ex:
|
||||
decompressed_bytes = decompressed_bytes + ex
|
||||
except:
|
||||
# possibly not compressed by zip - just return bytes
|
||||
return bytes
|
||||
return decompressed_bytes
|
||||
|
||||
def decrypt(self, path, data):
|
||||
if path.encode('utf-8') in self._encrypted:
|
||||
data = self._aes.decrypt(data)[16:]
|
||||
if type(data[-1]) != int:
|
||||
place = ord(data[-1])
|
||||
else:
|
||||
place = data[-1]
|
||||
data = data[:-place]
|
||||
data = self.decompress(data)
|
||||
return data
|
||||
|
||||
# check file to make check whether it's probably an Adobe Adept encrypted ePub
|
||||
def adeptBook(inpath):
|
||||
with closing(ZipFile(open(inpath, 'rb'))) as inf:
|
||||
namelist = set(inf.namelist())
|
||||
if 'META-INF/rights.xml' not in namelist or \
|
||||
'META-INF/encryption.xml' not in namelist:
|
||||
return False
|
||||
try:
|
||||
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
|
||||
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
||||
expr = './/%s' % (adept('encryptedKey'),)
|
||||
bookkey = ''.join(rights.findtext(expr))
|
||||
if len(bookkey) == 172:
|
||||
return True
|
||||
except:
|
||||
# if we couldn't check, assume it is
|
||||
return True
|
||||
return False
|
||||
|
||||
def decryptBook(userkey, inpath, outpath):
|
||||
if AES is None:
|
||||
raise ADEPTError("PyCrypto or OpenSSL must be installed.")
|
||||
rsa = RSA(userkey)
|
||||
with closing(ZipFile(open(inpath, 'rb'))) as inf:
|
||||
namelist = set(inf.namelist())
|
||||
if 'META-INF/rights.xml' not in namelist or \
|
||||
'META-INF/encryption.xml' not in namelist:
|
||||
print("{0:s} is DRM-free.".format(os.path.basename(inpath)))
|
||||
return 1
|
||||
for name in META_NAMES:
|
||||
namelist.remove(name)
|
||||
try:
|
||||
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
|
||||
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
||||
expr = './/%s' % (adept('encryptedKey'),)
|
||||
bookkey = ''.join(rights.findtext(expr))
|
||||
if len(bookkey) != 172:
|
||||
print("{0:s} is not a secure Adobe Adept ePub.".format(os.path.basename(inpath)))
|
||||
return 1
|
||||
bookkey = rsa.decrypt(codecs.decode(bookkey.encode('ascii'), 'base64'))
|
||||
# Padded as per RSAES-PKCS1-v1_5
|
||||
if len(bookkey) > 16:
|
||||
if bookkey[-17] == '\x00' or bookkey[-17] == 0:
|
||||
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')
|
||||
decryptor = Decryptor(bookkey, encryption)
|
||||
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
|
||||
with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf:
|
||||
zi = ZipInfo('mimetype')
|
||||
zi.compress_type=ZIP_STORED
|
||||
try:
|
||||
# if the mimetype is present, get its info, including time-stamp
|
||||
oldzi = inf.getinfo('mimetype')
|
||||
# copy across fields to be preserved
|
||||
zi.date_time = oldzi.date_time
|
||||
zi.comment = oldzi.comment
|
||||
zi.extra = oldzi.extra
|
||||
zi.internal_attr = oldzi.internal_attr
|
||||
# external attributes are dependent on the create system, so copy both.
|
||||
zi.external_attr = oldzi.external_attr
|
||||
zi.create_system = oldzi.create_system
|
||||
except:
|
||||
pass
|
||||
outf.writestr(zi, inf.read('mimetype'))
|
||||
for path in namelist:
|
||||
data = inf.read(path)
|
||||
zi = ZipInfo(path)
|
||||
zi.compress_type=ZIP_DEFLATED
|
||||
try:
|
||||
# get the file info, including time-stamp
|
||||
oldzi = inf.getinfo(path)
|
||||
# copy across useful fields
|
||||
zi.date_time = oldzi.date_time
|
||||
zi.comment = oldzi.comment
|
||||
zi.extra = oldzi.extra
|
||||
zi.internal_attr = oldzi.internal_attr
|
||||
# external attributes are dependent on the create system, so copy both.
|
||||
zi.external_attr = oldzi.external_attr
|
||||
zi.create_system = oldzi.create_system
|
||||
except:
|
||||
pass
|
||||
outf.writestr(zi, decryptor.decrypt(path, data))
|
||||
except:
|
||||
print("Could not decrypt {0:s} because of an exception:\n{1:s}".format(os.path.basename(inpath), traceback.format_exc()))
|
||||
return 2
|
||||
return 0
|
||||
|
||||
|
||||
def cli_main():
|
||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
argv=unicode_argv()
|
||||
progname = os.path.basename(argv[0])
|
||||
if len(argv) != 4:
|
||||
print("usage: {0} <keyfile.der> <inbook.epub> <outbook.epub>".format(progname))
|
||||
return 1
|
||||
keypath, inpath, outpath = argv[1:]
|
||||
userkey = open(keypath,'rb').read()
|
||||
result = decryptBook(userkey, inpath, outpath)
|
||||
if result == 0:
|
||||
print("Successfully decrypted {0:s} as {1:s}".format(os.path.basename(inpath),os.path.basename(outpath)))
|
||||
return result
|
||||
|
||||
def gui_main():
|
||||
try:
|
||||
import tkinter
|
||||
import tkinter.constants
|
||||
import tkinter.filedialog
|
||||
import tkinter.messagebox
|
||||
import traceback
|
||||
except:
|
||||
return cli_main()
|
||||
|
||||
class DecryptionDialog(tkinter.Frame):
|
||||
def __init__(self, root):
|
||||
tkinter.Frame.__init__(self, root, border=5)
|
||||
self.status = tkinter.Label(self, text="Select files for decryption")
|
||||
self.status.pack(fill=tkinter.constants.X, expand=1)
|
||||
body = tkinter.Frame(self)
|
||||
body.pack(fill=tkinter.constants.X, expand=1)
|
||||
sticky = tkinter.constants.E + tkinter.constants.W
|
||||
body.grid_columnconfigure(1, weight=2)
|
||||
tkinter.Label(body, text="Key file").grid(row=0)
|
||||
self.keypath = tkinter.Entry(body, width=30)
|
||||
self.keypath.grid(row=0, column=1, sticky=sticky)
|
||||
if os.path.exists("adeptkey.der"):
|
||||
self.keypath.insert(0, "adeptkey.der")
|
||||
button = tkinter.Button(body, text="...", command=self.get_keypath)
|
||||
button.grid(row=0, column=2)
|
||||
tkinter.Label(body, text="Input file").grid(row=1)
|
||||
self.inpath = tkinter.Entry(body, width=30)
|
||||
self.inpath.grid(row=1, column=1, sticky=sticky)
|
||||
button = tkinter.Button(body, text="...", command=self.get_inpath)
|
||||
button.grid(row=1, column=2)
|
||||
tkinter.Label(body, text="Output file").grid(row=2)
|
||||
self.outpath = tkinter.Entry(body, width=30)
|
||||
self.outpath.grid(row=2, column=1, sticky=sticky)
|
||||
button = tkinter.Button(body, text="...", command=self.get_outpath)
|
||||
button.grid(row=2, column=2)
|
||||
buttons = tkinter.Frame(self)
|
||||
buttons.pack()
|
||||
botton = tkinter.Button(
|
||||
buttons, text="Decrypt", width=10, command=self.decrypt)
|
||||
botton.pack(side=tkinter.constants.LEFT)
|
||||
tkinter.Frame(buttons, width=10).pack(side=tkinter.constants.LEFT)
|
||||
button = tkinter.Button(
|
||||
buttons, text="Quit", width=10, command=self.quit)
|
||||
button.pack(side=tkinter.constants.RIGHT)
|
||||
|
||||
def get_keypath(self):
|
||||
keypath = tkinter.filedialog.askopenfilename(
|
||||
parent=None, title="Select Adobe Adept \'.der\' key file",
|
||||
defaultextension=".der",
|
||||
filetypes=[('Adobe Adept DER-encoded files', '.der'),
|
||||
('All Files', '.*')])
|
||||
if keypath:
|
||||
keypath = os.path.normpath(keypath)
|
||||
self.keypath.delete(0, tkinter.constants.END)
|
||||
self.keypath.insert(0, keypath)
|
||||
return
|
||||
|
||||
def get_inpath(self):
|
||||
inpath = tkinter.filedialog.askopenfilename(
|
||||
parent=None, title="Select ADEPT-encrypted ePub file to decrypt",
|
||||
defaultextension=".epub", filetypes=[('ePub files', '.epub')])
|
||||
if inpath:
|
||||
inpath = os.path.normpath(inpath)
|
||||
self.inpath.delete(0, tkinter.constants.END)
|
||||
self.inpath.insert(0, inpath)
|
||||
return
|
||||
|
||||
def get_outpath(self):
|
||||
outpath = tkinter.filedialog.asksaveasfilename(
|
||||
parent=None, title="Select unencrypted ePub file to produce",
|
||||
defaultextension=".epub", filetypes=[('ePub files', '.epub')])
|
||||
if outpath:
|
||||
outpath = os.path.normpath(outpath)
|
||||
self.outpath.delete(0, tkinter.constants.END)
|
||||
self.outpath.insert(0, outpath)
|
||||
return
|
||||
|
||||
def decrypt(self):
|
||||
keypath = self.keypath.get()
|
||||
inpath = self.inpath.get()
|
||||
outpath = self.outpath.get()
|
||||
if not keypath or not os.path.exists(keypath):
|
||||
self.status['text'] = "Specified key file does not exist"
|
||||
return
|
||||
if not inpath or not os.path.exists(inpath):
|
||||
self.status['text'] = "Specified input file does not exist"
|
||||
return
|
||||
if not outpath:
|
||||
self.status['text'] = "Output file not specified"
|
||||
return
|
||||
if inpath == outpath:
|
||||
self.status['text'] = "Must have different input and output files"
|
||||
return
|
||||
userkey = open(keypath,'rb').read()
|
||||
self.status['text'] = "Decrypting..."
|
||||
try:
|
||||
decrypt_status = decryptBook(userkey, inpath, outpath)
|
||||
except Exception as e:
|
||||
self.status['text'] = "Error: {0}".format(e.args[0])
|
||||
return
|
||||
if decrypt_status == 0:
|
||||
self.status['text'] = "File successfully decrypted"
|
||||
else:
|
||||
self.status['text'] = "There was an error decrypting the file."
|
||||
|
||||
root = tkinter.Tk()
|
||||
root.title("Adobe Adept ePub Decrypter v.{0}".format(__version__))
|
||||
root.resizable(True, False)
|
||||
root.minsize(300, 0)
|
||||
DecryptionDialog(root).pack(fill=tkinter.constants.X, expand=1)
|
||||
root.mainloop()
|
||||
return 0
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) > 1:
|
||||
sys.exit(cli_main())
|
||||
sys.exit(gui_main())
|
||||
2331
DeDRM_plugin/ineptpdf.py
Executable file
2331
DeDRM_plugin/ineptpdf.py
Executable file
File diff suppressed because it is too large
Load Diff
1040
DeDRM_plugin/ion.py
Normal file
1040
DeDRM_plugin/ion.py
Normal file
File diff suppressed because it is too large
Load Diff
358
DeDRM_plugin/k4mobidedrm.py
Normal file
358
DeDRM_plugin/k4mobidedrm.py
Normal file
@@ -0,0 +1,358 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# k4mobidedrm.py
|
||||
# Copyright © 2008-2020 by Apprentice Harper et al.
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = '6.0'
|
||||
|
||||
# Engine to remove drm from Kindle and Mobipocket ebooks
|
||||
# for personal use for archiving and converting your ebooks
|
||||
|
||||
# PLEASE DO NOT PIRATE EBOOKS!
|
||||
|
||||
# We want all authors and publishers, and ebook stores to live
|
||||
# long and prosperous lives but at the same time we just want to
|
||||
# be able to read OUR books on whatever device we want and to keep
|
||||
# readable for a long, long time
|
||||
|
||||
# This borrows very heavily from works by CMBDTC, IHeartCabbages, skindle,
|
||||
# unswindle, DarkReverser, ApprenticeAlf, and many many others
|
||||
|
||||
# Special thanks to The Dark Reverser for MobiDeDrm and CMBDTC for cmbdtc_dump
|
||||
# from which this script borrows most unashamedly.
|
||||
|
||||
# Changelog
|
||||
# 1.0 - Name change to k4mobidedrm. Adds Mac support, Adds plugin code
|
||||
# 1.1 - Adds support for additional kindle.info files
|
||||
# 1.2 - Better error handling for older Mobipocket
|
||||
# 1.3 - Don't try to decrypt Topaz books
|
||||
# 1.7 - Add support for Topaz books and Kindle serial numbers. Split code.
|
||||
# 1.9 - Tidy up after Topaz, minor exception changes
|
||||
# 2.1 - Topaz fix and filename sanitizing
|
||||
# 2.2 - Topaz Fix and minor Mac code fix
|
||||
# 2.3 - More Topaz fixes
|
||||
# 2.4 - K4PC/Mac key generation fix
|
||||
# 2.6 - Better handling of non-K4PC/Mac ebooks
|
||||
# 2.7 - Better trailing bytes handling in mobidedrm
|
||||
# 2.8 - Moved parsing of kindle.info files to mac & pc util files.
|
||||
# 3.1 - Updated for new calibre interface. Now __init__ in plugin.
|
||||
# 3.5 - Now support Kindle for PC/Mac 1.6
|
||||
# 3.6 - Even better trailing bytes handling in mobidedrm
|
||||
# 3.7 - Add support for Amazon Print Replica ebooks.
|
||||
# 3.8 - Improved Topaz support
|
||||
# 4.1 - Improved Topaz support and faster decryption with alfcrypto
|
||||
# 4.2 - Added support for Amazon's KF8 format ebooks
|
||||
# 4.4 - Linux calls to Wine added, and improved configuration dialog
|
||||
# 4.5 - Linux works again without Wine. Some Mac key file search changes
|
||||
# 4.6 - First attempt to handle unicode properly
|
||||
# 4.7 - Added timing reports, and changed search for Mac key files
|
||||
# 4.8 - Much better unicode handling, matching the updated inept and ignoble scripts
|
||||
# - Moved back into plugin, __init__ in plugin now only contains plugin code.
|
||||
# 4.9 - Missed some invalid characters in cleanup_name
|
||||
# 5.0 - Extraction of info from Kindle for PC/Mac moved into kindlekey.py
|
||||
# - tweaked GetDecryptedBook interface to leave passed parameters unchanged
|
||||
# 5.1 - moved unicode_argv call inside main for Windows DeDRM compatibility
|
||||
# 5.2 - Fixed error in command line processing of unicode arguments
|
||||
# 5.3 - Changed Android support to allow passing of backup .ab files
|
||||
# 5.4 - Recognise KFX files masquerading as azw, even if we can't decrypt them yet.
|
||||
# 5.5 - Added GPL v3 licence explicitly.
|
||||
# 5.6 - Invoke KFXZipBook to handle zipped KFX files
|
||||
# 5.7 - Revamp cleanup_name
|
||||
# 6.0 - Added Python 3 compatibility for calibre 5.0
|
||||
|
||||
|
||||
import sys, os, re
|
||||
import csv
|
||||
import getopt
|
||||
import re
|
||||
import traceback
|
||||
import time
|
||||
import html.entities
|
||||
import json
|
||||
|
||||
class DrmException(Exception):
|
||||
pass
|
||||
|
||||
if 'calibre' in sys.modules:
|
||||
inCalibre = True
|
||||
else:
|
||||
inCalibre = False
|
||||
|
||||
if inCalibre:
|
||||
from calibre_plugins.dedrm import mobidedrm
|
||||
from calibre_plugins.dedrm import topazextract
|
||||
from calibre_plugins.dedrm import kgenpids
|
||||
from calibre_plugins.dedrm import androidkindlekey
|
||||
from calibre_plugins.dedrm import kfxdedrm
|
||||
else:
|
||||
import mobidedrm
|
||||
import topazextract
|
||||
import kgenpids
|
||||
import androidkindlekey
|
||||
import kfxdedrm
|
||||
|
||||
# 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):
|
||||
data = data.encode(self.encoding,"replace")
|
||||
self.stream.buffer.write(data)
|
||||
self.stream.buffer.flush()
|
||||
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
iswindows = sys.platform.startswith('win')
|
||||
isosx = sys.platform.startswith('darwin')
|
||||
|
||||
def unicode_argv():
|
||||
if iswindows:
|
||||
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
||||
# strings.
|
||||
|
||||
# Versions 2.x of Python don't support Unicode in sys.argv on
|
||||
# Windows, with the underlying Windows API instead replacing multi-byte
|
||||
# characters with '?'.
|
||||
|
||||
|
||||
from ctypes import POINTER, byref, cdll, c_int, windll
|
||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
||||
|
||||
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
||||
GetCommandLineW.argtypes = []
|
||||
GetCommandLineW.restype = LPCWSTR
|
||||
|
||||
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
||||
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
||||
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
||||
|
||||
cmd = GetCommandLineW()
|
||||
argc = c_int(0)
|
||||
argv = CommandLineToArgvW(cmd, byref(argc))
|
||||
if argc.value > 0:
|
||||
# Remove Python executable and commands if present
|
||||
start = argc.value - len(sys.argv)
|
||||
return [argv[i] for i in
|
||||
range(start, argc.value)]
|
||||
# if we don't have any arguments at all, just pass back script name
|
||||
# this should never happen
|
||||
return ["mobidedrm.py"]
|
||||
else:
|
||||
argvencoding = sys.stdin.encoding or "utf-8"
|
||||
return [arg if isinstance(arg, str) else str(arg, argvencoding) for arg in sys.argv]
|
||||
|
||||
# cleanup unicode filenames
|
||||
# borrowed from calibre from calibre/src/calibre/__init__.py
|
||||
# added in removal of control (<32) chars
|
||||
# and removal of . at start and end
|
||||
# and with some (heavily edited) code from Paul Durrant's kindlenamer.py
|
||||
# and some improvements suggested by jhaisley
|
||||
def cleanup_name(name):
|
||||
# substitute filename unfriendly characters
|
||||
name = name.replace("<","[").replace(">","]").replace(" : "," – ").replace(": "," – ").replace(":","—").replace("/","_").replace("\\","_").replace("|","_").replace("\"","\'").replace("*","_").replace("?","")
|
||||
# white space to single space, delete leading and trailing while space
|
||||
name = re.sub(r"\s", " ", name).strip()
|
||||
# delete control characters
|
||||
name = "".join(char for char in name if ord(char)>=32)
|
||||
# delete non-ascii characters
|
||||
name = "".join(char for char in name if ord(char)<=126)
|
||||
# remove leading dots
|
||||
while len(name)>0 and name[0] == ".":
|
||||
name = name[1:]
|
||||
# remove trailing dots (Windows doesn't like them)
|
||||
while name.endswith("."):
|
||||
name = name[:-1]
|
||||
if len(name)==0:
|
||||
name="DecryptedBook"
|
||||
return name
|
||||
|
||||
# must be passed unicode
|
||||
def unescape(text):
|
||||
def fixup(m):
|
||||
text = m.group(0)
|
||||
if text[:2] == "&#":
|
||||
# character reference
|
||||
try:
|
||||
if text[:3] == "&#x":
|
||||
return chr(int(text[3:-1], 16))
|
||||
else:
|
||||
return chr(int(text[2:-1]))
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
# named entity
|
||||
try:
|
||||
text = chr(htmlentitydefs.name2codepoint[text[1:-1]])
|
||||
except KeyError:
|
||||
pass
|
||||
return text # leave as is
|
||||
return re.sub("&#?\\w+;", fixup, text)
|
||||
|
||||
def GetDecryptedBook(infile, kDatabases, androidFiles, serials, pids, starttime = time.time()):
|
||||
# handle the obvious cases at the beginning
|
||||
if not os.path.isfile(infile):
|
||||
raise DrmException("Input file does not exist.")
|
||||
|
||||
mobi = True
|
||||
magic8 = open(infile,'rb').read(8)
|
||||
if magic8 == b'\xeaDRMION\xee':
|
||||
raise DrmException("The .kfx DRMION file cannot be decrypted by itself. A .kfx-zip archive containing a DRM voucher is required.")
|
||||
|
||||
magic3 = magic8[:3]
|
||||
if magic3 == b'TPZ':
|
||||
mobi = False
|
||||
|
||||
if magic8[:4] == b'PK\x03\x04':
|
||||
mb = kfxdedrm.KFXZipBook(infile)
|
||||
elif mobi:
|
||||
mb = mobidedrm.MobiBook(infile)
|
||||
else:
|
||||
mb = topazextract.TopazBook(infile)
|
||||
|
||||
bookname = unescape(mb.getBookTitle())
|
||||
print("Decrypting {1} ebook: {0}".format(bookname, mb.getBookType()))
|
||||
|
||||
# copy list of pids
|
||||
totalpids = list(pids)
|
||||
# extend list of serials with serials from android databases
|
||||
for aFile in androidFiles:
|
||||
serials.extend(androidkindlekey.get_serials(aFile))
|
||||
# extend PID list with book-specific PIDs from seriala and kDatabases
|
||||
md1, md2 = mb.getPIDMetaInfo()
|
||||
totalpids.extend(kgenpids.getPidList(md1, md2, serials, kDatabases))
|
||||
# remove any duplicates
|
||||
totalpids = list(set(totalpids))
|
||||
print("Found {1:d} keys to try after {0:.1f} seconds".format(time.time()-starttime, len(totalpids)))
|
||||
#print totalpids
|
||||
|
||||
try:
|
||||
mb.processBook(totalpids)
|
||||
except:
|
||||
mb.cleanup
|
||||
raise
|
||||
|
||||
print("Decryption succeeded after {0:.1f} seconds".format(time.time()-starttime))
|
||||
return mb
|
||||
|
||||
|
||||
# kDatabaseFiles is a list of files created by kindlekey
|
||||
def decryptBook(infile, outdir, kDatabaseFiles, androidFiles, serials, pids):
|
||||
starttime = time.time()
|
||||
kDatabases = []
|
||||
for dbfile in kDatabaseFiles:
|
||||
kindleDatabase = {}
|
||||
try:
|
||||
with open(dbfile, 'r') as keyfilein:
|
||||
kindleDatabase = json.loads(keyfilein.read())
|
||||
kDatabases.append([dbfile,kindleDatabase])
|
||||
except Exception as e:
|
||||
print("Error getting database from file {0:s}: {1:s}".format(dbfile,e))
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
|
||||
try:
|
||||
book = GetDecryptedBook(infile, kDatabases, androidFiles, serials, pids, starttime)
|
||||
except Exception as e:
|
||||
print("Error decrypting book after {1:.1f} seconds: {0}".format(e.args[0],time.time()-starttime))
|
||||
traceback.print_exc()
|
||||
return 1
|
||||
|
||||
# Try to infer a reasonable name
|
||||
orig_fn_root = os.path.splitext(os.path.basename(infile))[0]
|
||||
if (
|
||||
re.match('^B[A-Z0-9]{9}(_EBOK|_EBSP|_sample)?$', orig_fn_root) or
|
||||
re.match('^{0-9A-F-}{36}$', orig_fn_root)
|
||||
): # Kindle for PC / Mac / Android / Fire / iOS
|
||||
clean_title = cleanup_name(book.getBookTitle())
|
||||
outfilename = "{}_{}".format(orig_fn_root, clean_title)
|
||||
else: # E Ink Kindle, which already uses a reasonable name
|
||||
outfilename = orig_fn_root
|
||||
|
||||
# avoid excessively long file names
|
||||
if len(outfilename)>150:
|
||||
outfilename = outfilename[:99]+"--"+outfilename[-49:]
|
||||
|
||||
outfilename = outfilename+"_nodrm"
|
||||
outfile = os.path.join(outdir, outfilename + book.getBookExtension())
|
||||
|
||||
book.getFile(outfile)
|
||||
print("Saved decrypted book {1:s} after {0:.1f} seconds".format(time.time()-starttime, outfilename))
|
||||
|
||||
if book.getBookType()=="Topaz":
|
||||
zipname = os.path.join(outdir, outfilename + "_SVG.zip")
|
||||
book.getSVGZip(zipname)
|
||||
print("Saved SVG ZIP Archive for {1:s} after {0:.1f} seconds".format(time.time()-starttime, outfilename))
|
||||
|
||||
# remove internal temporary directory of Topaz pieces
|
||||
book.cleanup()
|
||||
return 0
|
||||
|
||||
|
||||
def usage(progname):
|
||||
print("Removes DRM protection from Mobipocket, Amazon KF8, Amazon Print Replica and Amazon Topaz ebooks")
|
||||
print("Usage:")
|
||||
print(" {0} [-k <kindle.k4i>] [-p <comma separated PIDs>] [-s <comma separated Kindle serial numbers>] [ -a <AmazonSecureStorage.xml|backup.ab> ] <infile> <outdir>".format(progname))
|
||||
|
||||
#
|
||||
# Main
|
||||
#
|
||||
def cli_main():
|
||||
argv=unicode_argv()
|
||||
progname = os.path.basename(argv[0])
|
||||
print("K4MobiDeDrm v{0}.\nCopyright © 2008-2020 Apprentice Harper et al.".format(__version__))
|
||||
|
||||
try:
|
||||
opts, args = getopt.getopt(argv[1:], "k:p:s:a:h")
|
||||
except getopt.GetoptError as err:
|
||||
print("Error in options or arguments: {0}".format(err.args[0]))
|
||||
usage(progname)
|
||||
sys.exit(2)
|
||||
if len(args)<2:
|
||||
usage(progname)
|
||||
sys.exit(2)
|
||||
|
||||
infile = args[0]
|
||||
outdir = args[1]
|
||||
kDatabaseFiles = []
|
||||
androidFiles = []
|
||||
serials = []
|
||||
pids = []
|
||||
|
||||
for o, a in opts:
|
||||
if o == "-h":
|
||||
usage(progname)
|
||||
sys.exit(0)
|
||||
if o == "-k":
|
||||
if a == None :
|
||||
raise DrmException("Invalid parameter for -k")
|
||||
kDatabaseFiles.append(a)
|
||||
if o == "-p":
|
||||
if a == None :
|
||||
raise DrmException("Invalid parameter for -p")
|
||||
pids = a.encode('utf-8').split(b',')
|
||||
if o == "-s":
|
||||
if a == None :
|
||||
raise DrmException("Invalid parameter for -s")
|
||||
serials = a.split(',')
|
||||
if o == '-a':
|
||||
if a == None:
|
||||
raise DrmException("Invalid parameter for -a")
|
||||
androidFiles.append(a)
|
||||
|
||||
return decryptBook(infile, outdir, kDatabaseFiles, androidFiles, serials, pids)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
sys.exit(cli_main())
|
||||
112
DeDRM_plugin/kfxdedrm.py
Normal file
112
DeDRM_plugin/kfxdedrm.py
Normal file
@@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Engine to remove drm from Kindle KFX ebooks
|
||||
|
||||
# 2.0 - Python 3 for calibre 5.0
|
||||
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import zipfile
|
||||
|
||||
from io import BytesIO
|
||||
try:
|
||||
from ion import DrmIon, DrmIonVoucher
|
||||
except:
|
||||
from calibre_plugins.dedrm.ion import DrmIon, DrmIonVoucher
|
||||
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = '2.0'
|
||||
|
||||
|
||||
class KFXZipBook:
|
||||
def __init__(self, infile):
|
||||
self.infile = infile
|
||||
self.voucher = None
|
||||
self.decrypted = {}
|
||||
|
||||
def getPIDMetaInfo(self):
|
||||
return (None, None)
|
||||
|
||||
def processBook(self, totalpids):
|
||||
with zipfile.ZipFile(self.infile, 'r') as zf:
|
||||
for filename in zf.namelist():
|
||||
with zf.open(filename) as fh:
|
||||
data = fh.read(8)
|
||||
if data != b'\xeaDRMION\xee':
|
||||
continue
|
||||
data += fh.read()
|
||||
if self.voucher is None:
|
||||
self.decrypt_voucher(totalpids)
|
||||
print("Decrypting KFX DRMION: {0}".format(filename))
|
||||
outfile = BytesIO()
|
||||
DrmIon(BytesIO(data[8:-8]), lambda name: self.voucher).parse(outfile)
|
||||
self.decrypted[filename] = outfile.getvalue()
|
||||
|
||||
if not self.decrypted:
|
||||
print("The .kfx-zip archive does not contain an encrypted DRMION file")
|
||||
|
||||
def decrypt_voucher(self, totalpids):
|
||||
with zipfile.ZipFile(self.infile, 'r') as zf:
|
||||
for info in zf.infolist():
|
||||
with zf.open(info.filename) as fh:
|
||||
data = fh.read(4)
|
||||
if data != b'\xe0\x01\x00\xea':
|
||||
continue
|
||||
|
||||
data += fh.read()
|
||||
if b'ProtectedData' in data:
|
||||
break # found DRM voucher
|
||||
else:
|
||||
raise Exception("The .kfx-zip archive contains an encrypted DRMION file without a DRM voucher")
|
||||
|
||||
print("Decrypting KFX DRM voucher: {0}".format(info.filename))
|
||||
|
||||
for pid in [''] + totalpids:
|
||||
for dsn_len,secret_len in [(0,0), (16,0), (16,40), (32,40), (40,0), (40,40)]:
|
||||
if len(pid) == dsn_len + secret_len:
|
||||
break # split pid into DSN and account secret
|
||||
else:
|
||||
continue
|
||||
|
||||
try:
|
||||
voucher = DrmIonVoucher(BytesIO(data), pid[:dsn_len], pid[dsn_len:])
|
||||
voucher.parse()
|
||||
voucher.decryptvoucher()
|
||||
break
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
raise Exception("Failed to decrypt KFX DRM voucher with any key")
|
||||
|
||||
print("KFX DRM voucher successfully decrypted")
|
||||
|
||||
license_type = voucher.getlicensetype()
|
||||
if license_type != "Purchase":
|
||||
raise Exception(("This book is licensed as {0}. "
|
||||
'These tools are intended for use on purchased books.').format(license_type))
|
||||
|
||||
self.voucher = voucher
|
||||
|
||||
def getBookTitle(self):
|
||||
return os.path.splitext(os.path.split(self.infile)[1])[0]
|
||||
|
||||
def getBookExtension(self):
|
||||
return '.kfx-zip'
|
||||
|
||||
def getBookType(self):
|
||||
return 'KFX-ZIP'
|
||||
|
||||
def cleanup(self):
|
||||
pass
|
||||
|
||||
def getFile(self, outpath):
|
||||
if not self.decrypted:
|
||||
shutil.copyfile(self.infile, outpath)
|
||||
else:
|
||||
with zipfile.ZipFile(self.infile, 'r') as zif:
|
||||
with zipfile.ZipFile(outpath, 'w') as zof:
|
||||
for info in zif.infolist():
|
||||
zof.writestr(info, self.decrypted.get(info.filename, zif.read(info.filename)))
|
||||
311
DeDRM_plugin/kgenpids.py
Normal file
311
DeDRM_plugin/kgenpids.py
Normal file
@@ -0,0 +1,311 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# kgenpids.py
|
||||
# Copyright © 2008-2020 Apprentice Harper et al.
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = '3.0'
|
||||
|
||||
# Revision history:
|
||||
# 2.0 - Fix for non-ascii Windows user names
|
||||
# 2.1 - Actual fix for non-ascii WIndows user names.
|
||||
# 2.2 - Return information needed for KFX decryption
|
||||
# 3.0 - Python 3 for calibre 5.0
|
||||
|
||||
|
||||
import sys
|
||||
import os, csv
|
||||
import binascii
|
||||
import zlib
|
||||
import re
|
||||
from struct import pack, unpack, unpack_from
|
||||
import traceback
|
||||
|
||||
class DrmException(Exception):
|
||||
pass
|
||||
|
||||
global charMap1
|
||||
global charMap3
|
||||
global charMap4
|
||||
|
||||
|
||||
charMap1 = b'n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M'
|
||||
charMap3 = b'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
|
||||
charMap4 = b'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789'
|
||||
|
||||
# crypto digestroutines
|
||||
import hashlib
|
||||
|
||||
def MD5(message):
|
||||
ctx = hashlib.md5()
|
||||
ctx.update(message)
|
||||
return ctx.digest()
|
||||
|
||||
def SHA1(message):
|
||||
ctx = hashlib.sha1()
|
||||
ctx.update(message)
|
||||
return ctx.digest()
|
||||
|
||||
|
||||
# Encode the bytes in data with the characters in map
|
||||
# data and map should be byte arrays
|
||||
def encode(data, map):
|
||||
result = b''
|
||||
for char in data:
|
||||
value = char
|
||||
Q = (value ^ 0x80) // len(map)
|
||||
R = value % len(map)
|
||||
result += bytes([map[Q]])
|
||||
result += bytes([map[R]])
|
||||
return result
|
||||
|
||||
# Hash the bytes in data and then encode the digest with the characters in map
|
||||
def encodeHash(data,map):
|
||||
return encode(MD5(data),map)
|
||||
|
||||
# Decode the string in data with the characters in map. Returns the decoded bytes
|
||||
def decode(data,map):
|
||||
result = ''
|
||||
for i in range (0,len(data)-1,2):
|
||||
high = map.find(data[i])
|
||||
low = map.find(data[i+1])
|
||||
if (high == -1) or (low == -1) :
|
||||
break
|
||||
value = (((high * len(map)) ^ 0x80) & 0xFF) + low
|
||||
result += pack('B',value)
|
||||
return result
|
||||
|
||||
#
|
||||
# PID generation routines
|
||||
#
|
||||
|
||||
# Returns two bit at offset from a bit field
|
||||
def getTwoBitsFromBitField(bitField,offset):
|
||||
byteNumber = offset // 4
|
||||
bitPosition = 6 - 2*(offset % 4)
|
||||
return bitField[byteNumber] >> bitPosition & 3
|
||||
|
||||
# Returns the six bits at offset from a bit field
|
||||
def getSixBitsFromBitField(bitField,offset):
|
||||
offset *= 3
|
||||
value = (getTwoBitsFromBitField(bitField,offset) <<4) + (getTwoBitsFromBitField(bitField,offset+1) << 2) +getTwoBitsFromBitField(bitField,offset+2)
|
||||
return value
|
||||
|
||||
# 8 bits to six bits encoding from hash to generate PID string
|
||||
def encodePID(hash):
|
||||
global charMap3
|
||||
PID = b''
|
||||
for position in range (0,8):
|
||||
PID += bytes([charMap3[getSixBitsFromBitField(hash,position)]])
|
||||
return PID
|
||||
|
||||
# Encryption table used to generate the device PID
|
||||
def generatePidEncryptionTable() :
|
||||
table = []
|
||||
for counter1 in range (0,0x100):
|
||||
value = counter1
|
||||
for counter2 in range (0,8):
|
||||
if (value & 1 == 0) :
|
||||
value = value >> 1
|
||||
else :
|
||||
value = value >> 1
|
||||
value = value ^ 0xEDB88320
|
||||
table.append(value)
|
||||
return table
|
||||
|
||||
# Seed value used to generate the device PID
|
||||
def generatePidSeed(table,dsn) :
|
||||
value = 0
|
||||
for counter in range (0,4) :
|
||||
index = (dsn[counter] ^ value) & 0xFF
|
||||
value = (value >> 8) ^ table[index]
|
||||
return value
|
||||
|
||||
# Generate the device PID
|
||||
def generateDevicePID(table,dsn,nbRoll):
|
||||
global charMap4
|
||||
seed = generatePidSeed(table,dsn)
|
||||
pidAscii = b''
|
||||
pid = [(seed >>24) &0xFF,(seed >> 16) &0xff,(seed >> 8) &0xFF,(seed) & 0xFF,(seed>>24) & 0xFF,(seed >> 16) &0xff,(seed >> 8) &0xFF,(seed) & 0xFF]
|
||||
index = 0
|
||||
for counter in range (0,nbRoll):
|
||||
pid[index] = pid[index] ^ dsn[counter]
|
||||
index = (index+1) %8
|
||||
for counter in range (0,8):
|
||||
index = ((((pid[counter] >>5) & 3) ^ pid[counter]) & 0x1f) + (pid[counter] >> 7)
|
||||
pidAscii += bytes([charMap4[index]])
|
||||
return pidAscii
|
||||
|
||||
def crc32(s):
|
||||
return (~binascii.crc32(s,-1))&0xFFFFFFFF
|
||||
|
||||
# convert from 8 digit PID to 10 digit PID with checksum
|
||||
def checksumPid(s):
|
||||
global charMap4
|
||||
crc = crc32(s)
|
||||
crc = crc ^ (crc >> 16)
|
||||
res = s
|
||||
l = len(charMap4)
|
||||
for i in (0,1):
|
||||
b = crc & 0xff
|
||||
pos = (b // l) ^ (b % l)
|
||||
res += bytes([charMap4[pos%l]])
|
||||
crc >>= 8
|
||||
return res
|
||||
|
||||
|
||||
# old kindle serial number to fixed pid
|
||||
def pidFromSerial(s, l):
|
||||
global charMap4
|
||||
crc = crc32(s)
|
||||
arr1 = [0]*l
|
||||
for i in range(len(s)):
|
||||
arr1[i%l] ^= s[i]
|
||||
crc_bytes = [crc >> 24 & 0xff, crc >> 16 & 0xff, crc >> 8 & 0xff, crc & 0xff]
|
||||
for i in range(l):
|
||||
arr1[i] ^= crc_bytes[i&3]
|
||||
pid = b""
|
||||
for i in range(l):
|
||||
b = arr1[i] & 0xff
|
||||
pid += bytes([charMap4[(b >> 7) + ((b >> 5 & 3) ^ (b & 0x1f))]])
|
||||
return pid
|
||||
|
||||
|
||||
# Parse the EXTH header records and use the Kindle serial number to calculate the book pid.
|
||||
def getKindlePids(rec209, token, serialnum):
|
||||
if isinstance(serialnum,str):
|
||||
serialnum = serialnum.encode('utf-8')
|
||||
|
||||
if rec209 is None:
|
||||
return [serialnum]
|
||||
|
||||
pids=[]
|
||||
|
||||
# Compute book PID
|
||||
pidHash = SHA1(serialnum+rec209+token)
|
||||
bookPID = encodePID(pidHash)
|
||||
bookPID = checksumPid(bookPID)
|
||||
pids.append(bookPID)
|
||||
|
||||
# compute fixed pid for old pre 2.5 firmware update pid as well
|
||||
kindlePID = pidFromSerial(serialnum, 7) + b"*"
|
||||
kindlePID = checksumPid(kindlePID)
|
||||
pids.append(kindlePID)
|
||||
|
||||
return pids
|
||||
|
||||
|
||||
# parse the Kindleinfo file to calculate the book pid.
|
||||
|
||||
keynames = ['kindle.account.tokens','kindle.cookie.item','eulaVersionAccepted','login_date','kindle.token.item','login','kindle.key.item','kindle.name.info','kindle.device.info', 'MazamaRandomNumber']
|
||||
|
||||
def getK4Pids(rec209, token, kindleDatabase):
|
||||
global charMap1
|
||||
pids = []
|
||||
|
||||
try:
|
||||
# Get the kindle account token, if present
|
||||
kindleAccountToken = bytearray.fromhex((kindleDatabase[1])['kindle.account.tokens'])
|
||||
|
||||
except KeyError:
|
||||
kindleAccountToken=""
|
||||
pass
|
||||
|
||||
try:
|
||||
# Get the DSN token, if present
|
||||
DSN = bytearray.fromhex((kindleDatabase[1])['DSN'])
|
||||
print("Got DSN key from database {0}".format(kindleDatabase[0]))
|
||||
except KeyError:
|
||||
# See if we have the info to generate the DSN
|
||||
try:
|
||||
# Get the Mazama Random number
|
||||
MazamaRandomNumber = bytearray.fromhex((kindleDatabase[1])['MazamaRandomNumber'])
|
||||
#print "Got MazamaRandomNumber from database {0}".format(kindleDatabase[0])
|
||||
|
||||
try:
|
||||
# Get the SerialNumber token, if present
|
||||
IDString = bytearray.fromhex((kindleDatabase[1])['SerialNumber'])
|
||||
print("Got SerialNumber from database {0}".format(kindleDatabase[0]))
|
||||
except KeyError:
|
||||
# Get the IDString we added
|
||||
IDString = bytearray.fromhex((kindleDatabase[1])['IDString'])
|
||||
|
||||
try:
|
||||
# Get the UsernameHash token, if present
|
||||
encodedUsername = bytearray.fromhex((kindleDatabase[1])['UsernameHash'])
|
||||
print("Got UsernameHash from database {0}".format(kindleDatabase[0]))
|
||||
except KeyError:
|
||||
# Get the UserName we added
|
||||
UserName = bytearray.fromhex((kindleDatabase[1])['UserName'])
|
||||
# encode it
|
||||
encodedUsername = encodeHash(UserName,charMap1)
|
||||
#print "encodedUsername",encodedUsername.encode('hex')
|
||||
except KeyError:
|
||||
print("Keys not found in the database {0}.".format(kindleDatabase[0]))
|
||||
return pids
|
||||
|
||||
# Get the ID string used
|
||||
encodedIDString = encodeHash(IDString,charMap1)
|
||||
#print "encodedIDString",encodedIDString.encode('hex')
|
||||
|
||||
# concat, hash and encode to calculate the DSN
|
||||
DSN = encode(SHA1(MazamaRandomNumber+encodedIDString+encodedUsername),charMap1)
|
||||
#print "DSN",DSN.encode('hex')
|
||||
pass
|
||||
|
||||
if rec209 is None:
|
||||
pids.append(DSN+kindleAccountToken)
|
||||
return pids
|
||||
|
||||
# Compute the device PID (for which I can tell, is used for nothing).
|
||||
table = generatePidEncryptionTable()
|
||||
devicePID = generateDevicePID(table,DSN,4)
|
||||
devicePID = checksumPid(devicePID)
|
||||
pids.append(devicePID)
|
||||
|
||||
# Compute book PIDs
|
||||
|
||||
# book pid
|
||||
pidHash = SHA1(DSN+kindleAccountToken+rec209+token)
|
||||
bookPID = encodePID(pidHash)
|
||||
bookPID = checksumPid(bookPID)
|
||||
pids.append(bookPID)
|
||||
|
||||
# variant 1
|
||||
pidHash = SHA1(kindleAccountToken+rec209+token)
|
||||
bookPID = encodePID(pidHash)
|
||||
bookPID = checksumPid(bookPID)
|
||||
pids.append(bookPID)
|
||||
|
||||
# variant 2
|
||||
pidHash = SHA1(DSN+rec209+token)
|
||||
bookPID = encodePID(pidHash)
|
||||
bookPID = checksumPid(bookPID)
|
||||
pids.append(bookPID)
|
||||
|
||||
return pids
|
||||
|
||||
def getPidList(md1, md2, serials=[], kDatabases=[]):
|
||||
pidlst = []
|
||||
|
||||
if kDatabases is None:
|
||||
kDatabases = []
|
||||
if serials is None:
|
||||
serials = []
|
||||
|
||||
for kDatabase in kDatabases:
|
||||
try:
|
||||
pidlst.extend(map(bytes,getK4Pids(md1, md2, kDatabase)))
|
||||
except Exception as e:
|
||||
print("Error getting PIDs from database {0}: {1}".format(kDatabase[0],e.args[0]))
|
||||
traceback.print_exc()
|
||||
|
||||
for serialnum in serials:
|
||||
try:
|
||||
pidlst.extend(map(bytes,getKindlePids(md1, md2, serialnum)))
|
||||
except Exception as e:
|
||||
print("Error getting PIDs from serial number {0}: {1}".format(serialnum ,e.args[0]))
|
||||
traceback.print_exc()
|
||||
|
||||
return pidlst
|
||||
1826
DeDRM_plugin/kindlekey.py
Normal file
1826
DeDRM_plugin/kindlekey.py
Normal file
File diff suppressed because it is too large
Load Diff
141
DeDRM_plugin/kindlepid.py
Normal file
141
DeDRM_plugin/kindlepid.py
Normal file
@@ -0,0 +1,141 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Mobipocket PID calculator v0.4 for Amazon Kindle.
|
||||
# Copyright (c) 2007, 2009 Igor Skochinsky <skochinsky@mail.ru>
|
||||
# History:
|
||||
# 0.1 Initial release
|
||||
# 0.2 Added support for generating PID for iPhone (thanks to mbp)
|
||||
# 0.3 changed to autoflush stdout, fixed return code usage
|
||||
# 0.3 updated for unicode
|
||||
# 0.4 Added support for serial numbers starting with '9', fixed unicode bugs.
|
||||
# 0.5 moved unicode_argv call inside main for Windows DeDRM compatibility
|
||||
# 1.0 Python 3 for calibre 5.0
|
||||
|
||||
|
||||
import sys
|
||||
import binascii
|
||||
|
||||
# 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):
|
||||
data = data.encode(self.encoding,"replace")
|
||||
self.stream.buffer.write(data)
|
||||
self.stream.buffer.flush()
|
||||
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
iswindows = sys.platform.startswith('win')
|
||||
isosx = sys.platform.startswith('darwin')
|
||||
|
||||
def unicode_argv():
|
||||
if iswindows:
|
||||
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
||||
# strings.
|
||||
|
||||
# Versions 2.x of Python don't support Unicode in sys.argv on
|
||||
# Windows, with the underlying Windows API instead replacing multi-byte
|
||||
# characters with '?'.
|
||||
|
||||
|
||||
from ctypes import POINTER, byref, cdll, c_int, windll
|
||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
||||
|
||||
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
||||
GetCommandLineW.argtypes = []
|
||||
GetCommandLineW.restype = LPCWSTR
|
||||
|
||||
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
||||
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
||||
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
||||
|
||||
cmd = GetCommandLineW()
|
||||
argc = c_int(0)
|
||||
argv = CommandLineToArgvW(cmd, byref(argc))
|
||||
if argc.value > 0:
|
||||
# Remove Python executable and commands if present
|
||||
start = argc.value - len(sys.argv)
|
||||
return [argv[i] for i in
|
||||
range(start, argc.value)]
|
||||
# if we don't have any arguments at all, just pass back script name
|
||||
# this should never happen
|
||||
return ["kindlepid.py"]
|
||||
else:
|
||||
argvencoding = sys.stdin.encoding or "utf-8"
|
||||
return [arg if isinstance(arg, str) else str(arg, argvencoding) for arg in sys.argv]
|
||||
|
||||
letters = 'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789'
|
||||
|
||||
def crc32(s):
|
||||
return (~binascii.crc32(s,-1))&0xFFFFFFFF
|
||||
|
||||
def checksumPid(s):
|
||||
crc = crc32(s.encode('ascii'))
|
||||
crc = crc ^ (crc >> 16)
|
||||
res = s
|
||||
l = len(letters)
|
||||
for i in (0,1):
|
||||
b = crc & 0xff
|
||||
pos = (b // l) ^ (b % l)
|
||||
res += letters[pos%l]
|
||||
crc >>= 8
|
||||
|
||||
return res
|
||||
|
||||
def pidFromSerial(s, l):
|
||||
crc = crc32(s)
|
||||
|
||||
arr1 = [0]*l
|
||||
for i in range(len(s)):
|
||||
arr1[i%l] ^= s[i]
|
||||
|
||||
crc_bytes = [crc >> 24 & 0xff, crc >> 16 & 0xff, crc >> 8 & 0xff, crc & 0xff]
|
||||
for i in range(l):
|
||||
arr1[i] ^= crc_bytes[i&3]
|
||||
|
||||
pid = ''
|
||||
for i in range(l):
|
||||
b = arr1[i] & 0xff
|
||||
pid+=letters[(b >> 7) + ((b >> 5 & 3) ^ (b & 0x1f))]
|
||||
|
||||
return pid
|
||||
|
||||
def cli_main():
|
||||
print("Mobipocket PID calculator for Amazon Kindle. Copyright © 2007, 2009 Igor Skochinsky")
|
||||
argv=unicode_argv()
|
||||
if len(argv)==2:
|
||||
serial = argv[1]
|
||||
else:
|
||||
print("Usage: kindlepid.py <Kindle Serial Number>/<iPhone/iPod Touch UDID>")
|
||||
return 1
|
||||
if len(serial)==16:
|
||||
if serial.startswith("B") or serial.startswith("9"):
|
||||
print("Kindle serial number detected")
|
||||
else:
|
||||
print("Warning: unrecognized serial number. Please recheck input.")
|
||||
return 1
|
||||
pid = pidFromSerial(serial.encode("utf-8"),7)+'*'
|
||||
print("Mobipocket PID for Kindle serial#{0} is {1}".format(serial,checksumPid(pid)))
|
||||
return 0
|
||||
elif len(serial)==40:
|
||||
print("iPhone serial number (UDID) detected")
|
||||
pid = pidFromSerial(serial.encode("utf-8"),8)
|
||||
print("Mobipocket PID for iPhone serial#{0} is {1}".format(serial,checksumPid(pid)))
|
||||
return 0
|
||||
print("Warning: unrecognized serial number. Please recheck input.")
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
sys.exit(cli_main())
|
||||
BIN
DeDRM_plugin/libalfcrypto.dylib
Normal file
BIN
DeDRM_plugin/libalfcrypto.dylib
Normal file
Binary file not shown.
BIN
DeDRM_plugin/libalfcrypto32.so
Normal file
BIN
DeDRM_plugin/libalfcrypto32.so
Normal file
Binary file not shown.
BIN
DeDRM_plugin/libalfcrypto64.so
Normal file
BIN
DeDRM_plugin/libalfcrypto64.so
Normal file
Binary file not shown.
557
DeDRM_plugin/mobidedrm.py
Executable file
557
DeDRM_plugin/mobidedrm.py
Executable file
@@ -0,0 +1,557 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# mobidedrm.py
|
||||
# Copyright © 2008 The Dark Reverser
|
||||
# Portions © 2008–2020 Apprentice Harper et al.
|
||||
|
||||
from __future__ import print_function
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = "1.0"
|
||||
|
||||
# This is a python script. You need a Python interpreter to run it.
|
||||
# For example, ActiveState Python, which exists for windows.
|
||||
#
|
||||
# Changelog
|
||||
# 0.01 - Initial version
|
||||
# 0.02 - Huffdic compressed books were not properly decrypted
|
||||
# 0.03 - Wasn't checking MOBI header length
|
||||
# 0.04 - Wasn't sanity checking size of data record
|
||||
# 0.05 - It seems that the extra data flags take two bytes not four
|
||||
# 0.06 - And that low bit does mean something after all :-)
|
||||
# 0.07 - The extra data flags aren't present in MOBI header < 0xE8 in size
|
||||
# 0.08 - ...and also not in Mobi header version < 6
|
||||
# 0.09 - ...but they are there with Mobi header version 6, header size 0xE4!
|
||||
# 0.10 - Outputs unencrypted files as-is, so that when run as a Calibre
|
||||
# import filter it works when importing unencrypted files.
|
||||
# Also now handles encrypted files that don't need a specific PID.
|
||||
# 0.11 - use autoflushed stdout and proper return values
|
||||
# 0.12 - Fix for problems with metadata import as Calibre plugin, report errors
|
||||
# 0.13 - Formatting fixes: retabbed file, removed trailing whitespace
|
||||
# and extra blank lines, converted CR/LF pairs at ends of each line,
|
||||
# and other cosmetic fixes.
|
||||
# 0.14 - Working out when the extra data flags are present has been problematic
|
||||
# Versions 7 through 9 have tried to tweak the conditions, but have been
|
||||
# only partially successful. Closer examination of lots of sample
|
||||
# files reveals that a confusion has arisen because trailing data entries
|
||||
# are not encrypted, but it turns out that the multibyte entries
|
||||
# in utf8 file are encrypted. (Although neither kind gets compressed.)
|
||||
# This knowledge leads to a simplification of the test for the
|
||||
# trailing data byte flags - version 5 and higher AND header size >= 0xE4.
|
||||
# 0.15 - Now outputs 'heartbeat', and is also quicker for long files.
|
||||
# 0.16 - And reverts to 'done' not 'done.' at the end for unswindle compatibility.
|
||||
# 0.17 - added modifications to support its use as an imported python module
|
||||
# both inside calibre and also in other places (ie K4DeDRM tools)
|
||||
# 0.17a- disabled the standalone plugin feature since a plugin can not import
|
||||
# a plugin
|
||||
# 0.18 - It seems that multibyte entries aren't encrypted in a v7 file...
|
||||
# Removed the disabled Calibre plug-in code
|
||||
# Permit use of 8-digit PIDs
|
||||
# 0.19 - It seems that multibyte entries aren't encrypted in a v6 file either.
|
||||
# 0.20 - Correction: It seems that multibyte entries are encrypted in a v6 file.
|
||||
# 0.21 - Added support for multiple pids
|
||||
# 0.22 - revised structure to hold MobiBook as a class to allow an extended interface
|
||||
# 0.23 - fixed problem with older files with no EXTH section
|
||||
# 0.24 - add support for type 1 encryption and 'TEXtREAd' books as well
|
||||
# 0.25 - Fixed support for 'BOOKMOBI' type 1 encryption
|
||||
# 0.26 - Now enables Text-To-Speech flag and sets clipping limit to 100%
|
||||
# 0.27 - Correct pid metadata token generation to match that used by skindle (Thank You Bart!)
|
||||
# 0.28 - slight additional changes to metadata token generation (None -> '')
|
||||
# 0.29 - It seems that the ideas about when multibyte trailing characters were
|
||||
# included in the encryption were wrong. They are for DOC compressed
|
||||
# files, but they are not for HUFF/CDIC compress files!
|
||||
# 0.30 - Modified interface slightly to work better with new calibre plugin style
|
||||
# 0.31 - The multibyte encrytion info is true for version 7 files too.
|
||||
# 0.32 - Added support for "Print Replica" Kindle ebooks
|
||||
# 0.33 - Performance improvements for large files (concatenation)
|
||||
# 0.34 - Performance improvements in decryption (libalfcrypto)
|
||||
# 0.35 - add interface to get mobi_version
|
||||
# 0.36 - fixed problem with TEXtREAd and getBookTitle interface
|
||||
# 0.37 - Fixed double announcement for stand-alone operation
|
||||
# 0.38 - Unicode used wherever possible, cope with absent alfcrypto
|
||||
# 0.39 - Fixed problem with TEXtREAd and getBookType interface
|
||||
# 0.40 - moved unicode_argv call inside main for Windows DeDRM compatibility
|
||||
# 0.41 - Fixed potential unicode problem in command line calls
|
||||
# 0.42 - Added GPL v3 licence. updated/removed some print statements
|
||||
# 1.0 - Python 3 compatibility for calibre 5.0
|
||||
|
||||
import sys
|
||||
import os
|
||||
import struct
|
||||
import binascii
|
||||
try:
|
||||
from alfcrypto import Pukall_Cipher
|
||||
except:
|
||||
print("AlfCrypto not found. Using python PC1 implementation.")
|
||||
|
||||
# Wrap a stream so that output gets flushed immediately
|
||||
# and also make sure that any unicode strings get
|
||||
# encoded using "replace" before writing them.
|
||||
class SafeUnbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
self.encoding = stream.encoding
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
def write(self, data):
|
||||
if isinstance(data, str):
|
||||
data = data.encode(self.encoding,"replace")
|
||||
self.stream.buffer.write(data)
|
||||
self.stream.buffer.flush()
|
||||
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
iswindows = sys.platform.startswith('win')
|
||||
isosx = sys.platform.startswith('darwin')
|
||||
|
||||
def unicode_argv():
|
||||
if iswindows:
|
||||
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
||||
# strings.
|
||||
|
||||
# Versions 2.x of Python don't support Unicode in sys.argv on
|
||||
# Windows, with the underlying Windows API instead replacing multi-byte
|
||||
# characters with '?'.
|
||||
|
||||
|
||||
from ctypes import POINTER, byref, cdll, c_int, windll
|
||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
||||
|
||||
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
||||
GetCommandLineW.argtypes = []
|
||||
GetCommandLineW.restype = LPCWSTR
|
||||
|
||||
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
||||
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
||||
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
||||
|
||||
cmd = GetCommandLineW()
|
||||
argc = c_int(0)
|
||||
argv = CommandLineToArgvW(cmd, byref(argc))
|
||||
if argc.value > 0:
|
||||
# Remove Python executable and commands if present
|
||||
start = argc.value - len(sys.argv)
|
||||
return [argv[i] for i in
|
||||
range(start, argc.value)]
|
||||
# if we don't have any arguments at all, just pass back script name
|
||||
# this should never happen
|
||||
return ["mobidedrm.py"]
|
||||
else:
|
||||
argvencoding = sys.stdin.encoding or "utf-8"
|
||||
return [arg if isinstance(arg, str) else str(arg, argvencoding) for arg in sys.argv]
|
||||
|
||||
|
||||
class DrmException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
#
|
||||
# MobiBook Utility Routines
|
||||
#
|
||||
|
||||
# Implementation of Pukall Cipher 1
|
||||
def PC1(key, src, decryption=True):
|
||||
# if we can get it from alfcrypto, use that
|
||||
try:
|
||||
return Pukall_Cipher().PC1(key,src,decryption)
|
||||
except NameError:
|
||||
pass
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
# use slow python version, since Pukall_Cipher didn't load
|
||||
sum1 = 0;
|
||||
sum2 = 0;
|
||||
keyXorVal = 0;
|
||||
if len(key)!=16:
|
||||
DrmException ("PC1: Bad key length")
|
||||
wkey = []
|
||||
for i in range(8):
|
||||
wkey.append(key[i*2]<<8 | key[i*2+1])
|
||||
dst = b''
|
||||
for i in range(len(src)):
|
||||
temp1 = 0;
|
||||
byteXorVal = 0;
|
||||
for j in range(8):
|
||||
temp1 ^= wkey[j]
|
||||
sum2 = (sum2+j)*20021 + sum1
|
||||
sum1 = (temp1*346)&0xFFFF
|
||||
sum2 = (sum2+sum1)&0xFFFF
|
||||
temp1 = (temp1*20021+1)&0xFFFF
|
||||
byteXorVal ^= temp1 ^ sum2
|
||||
curByte = src[i]
|
||||
if not decryption:
|
||||
keyXorVal = curByte * 257;
|
||||
curByte = ((curByte ^ (byteXorVal >> 8)) ^ byteXorVal) & 0xFF
|
||||
if decryption:
|
||||
keyXorVal = curByte * 257;
|
||||
for j in range(8):
|
||||
wkey[j] ^= keyXorVal;
|
||||
dst+=bytes([curByte])
|
||||
return dst
|
||||
|
||||
# accepts unicode returns unicode
|
||||
def checksumPid(s):
|
||||
letters = 'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789'
|
||||
crc = (~binascii.crc32(s.encode('utf-8'),-1))&0xFFFFFFFF
|
||||
crc = crc ^ (crc >> 16)
|
||||
res = s
|
||||
l = len(letters)
|
||||
for i in (0,1):
|
||||
b = crc & 0xff
|
||||
pos = (b // l) ^ (b % l)
|
||||
res += letters[pos%l]
|
||||
crc >>= 8
|
||||
return res
|
||||
|
||||
# expects bytearray
|
||||
def getSizeOfTrailingDataEntries(ptr, size, flags):
|
||||
def getSizeOfTrailingDataEntry(ptr, size):
|
||||
bitpos, result = 0, 0
|
||||
if size <= 0:
|
||||
return result
|
||||
while True:
|
||||
v = ptr[size-1]
|
||||
result |= (v & 0x7F) << bitpos
|
||||
bitpos += 7
|
||||
size -= 1
|
||||
if (v & 0x80) != 0 or (bitpos >= 28) or (size == 0):
|
||||
return result
|
||||
num = 0
|
||||
testflags = flags >> 1
|
||||
while testflags:
|
||||
if testflags & 1:
|
||||
num += getSizeOfTrailingDataEntry(ptr, size - num)
|
||||
testflags >>= 1
|
||||
# Check the low bit to see if there's multibyte data present.
|
||||
# if multibyte data is included in the encryped data, we'll
|
||||
# have already cleared this flag.
|
||||
if flags & 1:
|
||||
num += (ptr[size - num - 1] & 0x3) + 1
|
||||
return num
|
||||
|
||||
|
||||
|
||||
class MobiBook:
|
||||
def loadSection(self, section):
|
||||
if (section + 1 == self.num_sections):
|
||||
endoff = len(self.data_file)
|
||||
else:
|
||||
endoff = self.sections[section + 1][0]
|
||||
off = self.sections[section][0]
|
||||
return self.data_file[off:endoff]
|
||||
|
||||
def cleanup(self):
|
||||
# to match function in Topaz book
|
||||
pass
|
||||
|
||||
def __init__(self, infile):
|
||||
print("MobiDeDrm v{0:s}.\nCopyright © 2008-2020 The Dark Reverser, Apprentice Harper et al.".format(__version__))
|
||||
|
||||
try:
|
||||
from alfcrypto import Pukall_Cipher
|
||||
except:
|
||||
print("AlfCrypto not found. Using python PC1 implementation.")
|
||||
|
||||
# initial sanity check on file
|
||||
self.data_file = open(infile, 'rb').read()
|
||||
self.mobi_data = ''
|
||||
self.header = self.data_file[0:78]
|
||||
if self.header[0x3C:0x3C+8] != b'BOOKMOBI' and self.header[0x3C:0x3C+8] != b'TEXtREAd':
|
||||
raise DrmException("Invalid file format")
|
||||
self.magic = self.header[0x3C:0x3C+8]
|
||||
self.crypto_type = -1
|
||||
|
||||
# build up section offset and flag info
|
||||
self.num_sections, = struct.unpack('>H', self.header[76:78])
|
||||
self.sections = []
|
||||
for i in range(self.num_sections):
|
||||
offset, a1,a2,a3,a4 = struct.unpack('>LBBBB', self.data_file[78+i*8:78+i*8+8])
|
||||
flags, val = a1, a2<<16|a3<<8|a4
|
||||
self.sections.append( (offset, flags, val) )
|
||||
|
||||
# parse information from section 0
|
||||
self.sect = self.loadSection(0)
|
||||
self.records, = struct.unpack('>H', self.sect[0x8:0x8+2])
|
||||
self.compression, = struct.unpack('>H', self.sect[0x0:0x0+2])
|
||||
|
||||
# det default values before PalmDoc test
|
||||
self.print_replica = False
|
||||
self.extra_data_flags = 0
|
||||
self.meta_array = {}
|
||||
self.mobi_length = 0
|
||||
self.mobi_codepage = 1252
|
||||
self.mobi_version = -1
|
||||
|
||||
if self.magic == b'TEXtREAd':
|
||||
print("PalmDoc format book detected.")
|
||||
return
|
||||
|
||||
self.mobi_length, = struct.unpack('>L',self.sect[0x14:0x18])
|
||||
self.mobi_codepage, = struct.unpack('>L',self.sect[0x1c:0x20])
|
||||
self.mobi_version, = struct.unpack('>L',self.sect[0x68:0x6C])
|
||||
#print "MOBI header version {0:d}, header length {1:d}".format(self.mobi_version, self.mobi_length)
|
||||
if (self.mobi_length >= 0xE4) and (self.mobi_version >= 5):
|
||||
self.extra_data_flags, = struct.unpack('>H', self.sect[0xF2:0xF4])
|
||||
#print "Extra Data Flags: {0:d}".format(self.extra_data_flags)
|
||||
if (self.compression != 17480):
|
||||
# multibyte utf8 data is included in the encryption for PalmDoc compression
|
||||
# so clear that byte so that we leave it to be decrypted.
|
||||
self.extra_data_flags &= 0xFFFE
|
||||
|
||||
# if exth region exists parse it for metadata array
|
||||
try:
|
||||
exth_flag, = struct.unpack('>L', self.sect[0x80:0x84])
|
||||
exth = b''
|
||||
if exth_flag & 0x40:
|
||||
exth = self.sect[16 + self.mobi_length:]
|
||||
if (len(exth) >= 12) and (exth[:4] == b'EXTH'):
|
||||
nitems, = struct.unpack('>I', exth[8:12])
|
||||
pos = 12
|
||||
for i in range(nitems):
|
||||
type, size = struct.unpack('>II', exth[pos: pos + 8])
|
||||
content = exth[pos + 8: pos + size]
|
||||
self.meta_array[type] = content
|
||||
# reset the text to speech flag and clipping limit, if present
|
||||
if type == 401 and size == 9:
|
||||
# set clipping limit to 100%
|
||||
self.patchSection(0, b'\144', 16 + self.mobi_length + pos + 8)
|
||||
elif type == 404 and size == 9:
|
||||
# make sure text to speech is enabled
|
||||
self.patchSection(0, b'\0', 16 + self.mobi_length + pos + 8)
|
||||
# print type, size, content, content.encode('hex')
|
||||
pos += size
|
||||
except Exception as e:
|
||||
print("Cannot set meta_array: Error: {:s}".format(e.args[0]))
|
||||
|
||||
#returns unicode
|
||||
def getBookTitle(self):
|
||||
codec_map = {
|
||||
1252 : 'windows-1252',
|
||||
65001 : 'utf-8',
|
||||
}
|
||||
title = b''
|
||||
codec = 'windows-1252'
|
||||
if self.magic == b'BOOKMOBI':
|
||||
if 503 in self.meta_array:
|
||||
title = self.meta_array[503]
|
||||
else:
|
||||
toff, tlen = struct.unpack('>II', self.sect[0x54:0x5c])
|
||||
tend = toff + tlen
|
||||
title = self.sect[toff:tend]
|
||||
if self.mobi_codepage in codec_map.keys():
|
||||
codec = codec_map[self.mobi_codepage]
|
||||
if title == b'':
|
||||
title = self.header[:32]
|
||||
title = title.split(b'\0')[0]
|
||||
return title.decode(codec)
|
||||
|
||||
def getPIDMetaInfo(self):
|
||||
rec209 = b''
|
||||
token = b''
|
||||
if 209 in self.meta_array:
|
||||
rec209 = self.meta_array[209]
|
||||
data = rec209
|
||||
# The 209 data comes in five byte groups. Interpret the last four bytes
|
||||
# of each group as a big endian unsigned integer to get a key value
|
||||
# if that key exists in the meta_array, append its contents to the token
|
||||
for i in range(0,len(data),5):
|
||||
val, = struct.unpack('>I',data[i+1:i+5])
|
||||
sval = self.meta_array.get(val,b'')
|
||||
token += sval
|
||||
return rec209, token
|
||||
|
||||
# new must be byte array
|
||||
def patch(self, off, new):
|
||||
self.data_file = self.data_file[:off] + new + self.data_file[off+len(new):]
|
||||
|
||||
# new must be byte array
|
||||
def patchSection(self, section, new, in_off = 0):
|
||||
if (section + 1 == self.num_sections):
|
||||
endoff = len(self.data_file)
|
||||
else:
|
||||
endoff = self.sections[section + 1][0]
|
||||
off = self.sections[section][0]
|
||||
assert off + in_off + len(new) <= endoff
|
||||
self.patch(off + in_off, new)
|
||||
|
||||
# pids in pidlist must be unicode, returned key is byte array, pid is unicode
|
||||
def parseDRM(self, data, count, pidlist):
|
||||
found_key = None
|
||||
keyvec1 = b'\x72\x38\x33\xB0\xB4\xF2\xE3\xCA\xDF\x09\x01\xD6\xE2\xE0\x3F\x96'
|
||||
for pid in pidlist:
|
||||
bigpid = pid.encode('utf-8').ljust(16,b'\0')
|
||||
temp_key = PC1(keyvec1, bigpid, False)
|
||||
temp_key_sum = sum(temp_key) & 0xff
|
||||
found_key = None
|
||||
for i in range(count):
|
||||
verification, size, type, cksum, cookie = struct.unpack('>LLLBxxx32s', data[i*0x30:i*0x30+0x30])
|
||||
if cksum == temp_key_sum:
|
||||
cookie = PC1(temp_key, cookie)
|
||||
ver,flags,finalkey,expiry,expiry2 = struct.unpack('>LL16sLL', cookie)
|
||||
if verification == ver and (flags & 0x1F) == 1:
|
||||
found_key = finalkey
|
||||
break
|
||||
if found_key != None:
|
||||
break
|
||||
if not found_key:
|
||||
# Then try the default encoding that doesn't require a PID
|
||||
pid = '00000000'
|
||||
temp_key = keyvec1
|
||||
temp_key_sum = sum(temp_key) & 0xff
|
||||
for i in range(count):
|
||||
verification, size, type, cksum, cookie = struct.unpack('>LLLBxxx32s', data[i*0x30:i*0x30+0x30])
|
||||
if cksum == temp_key_sum:
|
||||
cookie = PC1(temp_key, cookie)
|
||||
ver,flags,finalkey,expiry,expiry2 = struct.unpack('>LL16sLL', cookie)
|
||||
if verification == ver:
|
||||
found_key = finalkey
|
||||
break
|
||||
return [found_key,pid]
|
||||
|
||||
def getFile(self, outpath):
|
||||
open(outpath,'wb').write(self.mobi_data)
|
||||
|
||||
def getBookType(self):
|
||||
if self.print_replica:
|
||||
return "Print Replica"
|
||||
if self.mobi_version >= 8:
|
||||
return "Kindle Format 8"
|
||||
if self.mobi_version >= 0:
|
||||
return "Mobipocket {0:d}".format(self.mobi_version)
|
||||
return "PalmDoc"
|
||||
|
||||
def getBookExtension(self):
|
||||
if self.print_replica:
|
||||
return ".azw4"
|
||||
if self.mobi_version >= 8:
|
||||
return ".azw3"
|
||||
return ".mobi"
|
||||
|
||||
# pids in pidlist may be unicode or bytearrays or bytes
|
||||
def processBook(self, pidlist):
|
||||
crypto_type, = struct.unpack('>H', self.sect[0xC:0xC+2])
|
||||
print("Crypto Type is: {0:d}".format(crypto_type))
|
||||
self.crypto_type = crypto_type
|
||||
if crypto_type == 0:
|
||||
print("This book is not encrypted.")
|
||||
# we must still check for Print Replica
|
||||
self.print_replica = (self.loadSection(1)[0:4] == '%MOP')
|
||||
self.mobi_data = self.data_file
|
||||
return
|
||||
if crypto_type != 2 and crypto_type != 1:
|
||||
raise DrmException("Cannot decode unknown Mobipocket encryption type {0:d}".format(crypto_type))
|
||||
if 406 in self.meta_array:
|
||||
data406 = self.meta_array[406]
|
||||
val406, = struct.unpack('>Q',data406)
|
||||
if val406 != 0:
|
||||
raise DrmException("Cannot decode library or rented ebooks.")
|
||||
|
||||
goodpids = []
|
||||
# print("DEBUG ==== pidlist = ", pidlist)
|
||||
for pid in pidlist:
|
||||
if isinstance(pid,(bytearray,bytes)):
|
||||
pid = pid.decode('utf-8')
|
||||
if len(pid)==10:
|
||||
if checksumPid(pid[0:-2]) != pid:
|
||||
print("Warning: PID {0} has incorrect checksum, should have been {1}".format(pid,checksumPid(pid[0:-2])))
|
||||
goodpids.append(pid[0:-2])
|
||||
elif len(pid)==8:
|
||||
goodpids.append(pid)
|
||||
else:
|
||||
print("Warning: PID {0} has wrong number of digits".format(pid))
|
||||
|
||||
# print("======= DEBUG good pids = ", goodpids)
|
||||
|
||||
if self.crypto_type == 1:
|
||||
t1_keyvec = b'QDCVEPMU675RUBSZ'
|
||||
if self.magic == b'TEXtREAd':
|
||||
bookkey_data = self.sect[0x0E:0x0E+16]
|
||||
elif self.mobi_version < 0:
|
||||
bookkey_data = self.sect[0x90:0x90+16]
|
||||
else:
|
||||
bookkey_data = self.sect[self.mobi_length+16:self.mobi_length+32]
|
||||
pid = '00000000'
|
||||
found_key = PC1(t1_keyvec, bookkey_data)
|
||||
else :
|
||||
# calculate the keys
|
||||
drm_ptr, drm_count, drm_size, drm_flags = struct.unpack('>LLLL', self.sect[0xA8:0xA8+16])
|
||||
if drm_count == 0:
|
||||
raise DrmException("Encryption not initialised. Must be opened with Mobipocket Reader first.")
|
||||
found_key, pid = self.parseDRM(self.sect[drm_ptr:drm_ptr+drm_size], drm_count, goodpids)
|
||||
if not found_key:
|
||||
raise DrmException("No key found in {0:d} PIDs tried.".format(len(goodpids)))
|
||||
# kill the drm keys
|
||||
self.patchSection(0, b'\0' * drm_size, drm_ptr)
|
||||
# kill the drm pointers
|
||||
self.patchSection(0, b'\xff' * 4 + b'\0' * 12, 0xA8)
|
||||
|
||||
if pid=='00000000':
|
||||
print("File has default encryption, no specific key needed.")
|
||||
else:
|
||||
print("File is encoded with PID {0}.".format(checksumPid(pid)))
|
||||
|
||||
# clear the crypto type
|
||||
self.patchSection(0, b'\0' * 2, 0xC)
|
||||
|
||||
# decrypt sections
|
||||
print("Decrypting. Please wait . . .", end=' ')
|
||||
mobidataList = []
|
||||
mobidataList.append(self.data_file[:self.sections[1][0]])
|
||||
for i in range(1, self.records+1):
|
||||
data = self.loadSection(i)
|
||||
extra_size = getSizeOfTrailingDataEntries(data, len(data), self.extra_data_flags)
|
||||
if i%100 == 0:
|
||||
print(".", end=' ')
|
||||
# print "record %d, extra_size %d" %(i,extra_size)
|
||||
decoded_data = PC1(found_key, data[0:len(data) - extra_size])
|
||||
if i==1:
|
||||
self.print_replica = (decoded_data[0:4] == '%MOP')
|
||||
mobidataList.append(decoded_data)
|
||||
if extra_size > 0:
|
||||
mobidataList.append(data[-extra_size:])
|
||||
if self.num_sections > self.records+1:
|
||||
mobidataList.append(self.data_file[self.sections[self.records+1][0]:])
|
||||
self.mobi_data = b''.join(mobidataList)
|
||||
print("done")
|
||||
return
|
||||
|
||||
# pids in pidlist must be unicode
|
||||
def getUnencryptedBook(infile,pidlist):
|
||||
if not os.path.isfile(infile):
|
||||
raise DrmException("Input File Not Found.")
|
||||
book = MobiBook(infile)
|
||||
book.processBook(pidlist)
|
||||
return book.mobi_data
|
||||
|
||||
|
||||
def cli_main():
|
||||
argv=unicode_argv()
|
||||
progname = os.path.basename(argv[0])
|
||||
if len(argv)<3 or len(argv)>4:
|
||||
print("MobiDeDrm v{0:s}.\nCopyright © 2008-2020 The Dark Reverser, Apprentice Harper et al.".format(__version__))
|
||||
print("Removes protection from Kindle/Mobipocket, Kindle/KF8 and Kindle/Print Replica ebooks")
|
||||
print("Usage:")
|
||||
print(" {0} <infile> <outfile> [<Comma separated list of PIDs to try>]".format(progname))
|
||||
return 1
|
||||
else:
|
||||
infile = argv[1]
|
||||
outfile = argv[2]
|
||||
if len(argv) == 4:
|
||||
pidlist = argv[3].split(',')
|
||||
else:
|
||||
pidlist = []
|
||||
try:
|
||||
stripped_file = getUnencryptedBook(infile, pidlist)
|
||||
open(outfile, 'wb').write(stripped_file)
|
||||
except DrmException as e:
|
||||
print("MobiDeDRM v{0} Error: {1:s}".format(__version__,e.args[0]))
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
sys.exit(cli_main())
|
||||
89
DeDRM_plugin/openssl_des.py
Normal file
89
DeDRM_plugin/openssl_des.py
Normal file
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
|
||||
|
||||
# implement just enough of des from openssl to make erdr2pml.py happy
|
||||
|
||||
def load_libcrypto():
|
||||
from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_char, c_int, c_long, \
|
||||
Structure, c_ulong, create_string_buffer, cast
|
||||
from ctypes.util import find_library
|
||||
import sys
|
||||
|
||||
if sys.platform.startswith('win'):
|
||||
libcrypto = find_library('libeay32')
|
||||
else:
|
||||
libcrypto = find_library('crypto')
|
||||
|
||||
if libcrypto is None:
|
||||
return None
|
||||
|
||||
libcrypto = CDLL(libcrypto)
|
||||
|
||||
# typedef struct DES_ks
|
||||
# {
|
||||
# union
|
||||
# {
|
||||
# DES_cblock cblock;
|
||||
# /* make sure things are correct size on machines with
|
||||
# * 8 byte longs */
|
||||
# DES_LONG deslong[2];
|
||||
# } ks[16];
|
||||
# } DES_key_schedule;
|
||||
|
||||
# just create a big enough place to hold everything
|
||||
# it will have alignment of structure so we should be okay (16 byte aligned?)
|
||||
class DES_KEY_SCHEDULE(Structure):
|
||||
_fields_ = [('DES_cblock1', c_char * 16),
|
||||
('DES_cblock2', c_char * 16),
|
||||
('DES_cblock3', c_char * 16),
|
||||
('DES_cblock4', c_char * 16),
|
||||
('DES_cblock5', c_char * 16),
|
||||
('DES_cblock6', c_char * 16),
|
||||
('DES_cblock7', c_char * 16),
|
||||
('DES_cblock8', c_char * 16),
|
||||
('DES_cblock9', c_char * 16),
|
||||
('DES_cblock10', c_char * 16),
|
||||
('DES_cblock11', c_char * 16),
|
||||
('DES_cblock12', c_char * 16),
|
||||
('DES_cblock13', c_char * 16),
|
||||
('DES_cblock14', c_char * 16),
|
||||
('DES_cblock15', c_char * 16),
|
||||
('DES_cblock16', c_char * 16)]
|
||||
|
||||
DES_KEY_SCHEDULE_p = POINTER(DES_KEY_SCHEDULE)
|
||||
|
||||
def F(restype, name, argtypes):
|
||||
func = getattr(libcrypto, name)
|
||||
func.restype = restype
|
||||
func.argtypes = argtypes
|
||||
return func
|
||||
|
||||
DES_set_key = F(None, 'DES_set_key',[c_char_p, DES_KEY_SCHEDULE_p])
|
||||
DES_ecb_encrypt = F(None, 'DES_ecb_encrypt',[c_char_p, c_char_p, DES_KEY_SCHEDULE_p, c_int])
|
||||
|
||||
|
||||
class DES(object):
|
||||
def __init__(self, key):
|
||||
if len(key) != 8 :
|
||||
raise Exception('DES improper key used')
|
||||
return
|
||||
self.key = key
|
||||
self.keyschedule = DES_KEY_SCHEDULE()
|
||||
DES_set_key(self.key, self.keyschedule)
|
||||
def desdecrypt(self, data):
|
||||
ob = create_string_buffer(len(data))
|
||||
DES_ecb_encrypt(data, ob, self.keyschedule, 0)
|
||||
return ob.raw
|
||||
def decrypt(self, data):
|
||||
if not data:
|
||||
return b''
|
||||
i = 0
|
||||
result = []
|
||||
while i < len(data):
|
||||
block = data[i:i+8]
|
||||
processed_block = self.desdecrypt(block)
|
||||
result.append(processed_block)
|
||||
i += 8
|
||||
return b''.join(result)
|
||||
|
||||
return DES
|
||||
0
DeDRM_plugin/plugin-import-name-dedrm.txt
Normal file
0
DeDRM_plugin/plugin-import-name-dedrm.txt
Normal file
295
DeDRM_plugin/prefs.py
Executable file
295
DeDRM_plugin/prefs.py
Executable file
@@ -0,0 +1,295 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
|
||||
# Standard Python modules.
|
||||
import os, sys, re, hashlib
|
||||
import codecs, json
|
||||
import traceback
|
||||
|
||||
from calibre.utils.config import dynamic, config_dir, JSONConfig
|
||||
from calibre_plugins.dedrm.__init__ import PLUGIN_NAME, PLUGIN_VERSION
|
||||
from calibre.constants import iswindows, isosx
|
||||
|
||||
class DeDRM_Prefs():
|
||||
def __init__(self):
|
||||
JSON_PATH = os.path.join("plugins", PLUGIN_NAME.strip().lower().replace(' ', '_') + '.json')
|
||||
self.dedrmprefs = JSONConfig(JSON_PATH)
|
||||
|
||||
self.dedrmprefs.defaults['configured'] = False
|
||||
self.dedrmprefs.defaults['bandnkeys'] = {}
|
||||
self.dedrmprefs.defaults['adeptkeys'] = {}
|
||||
self.dedrmprefs.defaults['ereaderkeys'] = {}
|
||||
self.dedrmprefs.defaults['kindlekeys'] = {}
|
||||
self.dedrmprefs.defaults['androidkeys'] = {}
|
||||
self.dedrmprefs.defaults['pids'] = []
|
||||
self.dedrmprefs.defaults['serials'] = []
|
||||
self.dedrmprefs.defaults['adobewineprefix'] = ""
|
||||
self.dedrmprefs.defaults['kindlewineprefix'] = ""
|
||||
|
||||
# initialise
|
||||
# we must actually set the prefs that are dictionaries and lists
|
||||
# to empty dictionaries and lists, otherwise we are unable to add to them
|
||||
# as then it just adds to the (memory only) dedrmprefs.defaults versions!
|
||||
if self.dedrmprefs['bandnkeys'] == {}:
|
||||
self.dedrmprefs['bandnkeys'] = {}
|
||||
if self.dedrmprefs['adeptkeys'] == {}:
|
||||
self.dedrmprefs['adeptkeys'] = {}
|
||||
if self.dedrmprefs['ereaderkeys'] == {}:
|
||||
self.dedrmprefs['ereaderkeys'] = {}
|
||||
if self.dedrmprefs['kindlekeys'] == {}:
|
||||
self.dedrmprefs['kindlekeys'] = {}
|
||||
if self.dedrmprefs['androidkeys'] == {}:
|
||||
self.dedrmprefs['androidkeys'] = {}
|
||||
if self.dedrmprefs['pids'] == []:
|
||||
self.dedrmprefs['pids'] = []
|
||||
if self.dedrmprefs['serials'] == []:
|
||||
self.dedrmprefs['serials'] = []
|
||||
|
||||
def __getitem__(self,kind = None):
|
||||
if kind is not None:
|
||||
return self.dedrmprefs[kind]
|
||||
return self.dedrmprefs
|
||||
|
||||
def set(self, kind, value):
|
||||
self.dedrmprefs[kind] = value
|
||||
|
||||
def writeprefs(self,value = True):
|
||||
self.dedrmprefs['configured'] = value
|
||||
|
||||
def addnamedvaluetoprefs(self, prefkind, keyname, keyvalue):
|
||||
try:
|
||||
if keyvalue not in self.dedrmprefs[prefkind].values():
|
||||
# ensure that the keyname is unique
|
||||
# by adding a number (starting with 2) to the name if it is not
|
||||
namecount = 1
|
||||
newname = keyname
|
||||
while newname in self.dedrmprefs[prefkind]:
|
||||
namecount += 1
|
||||
newname = "{0:s}_{1:d}".format(keyname,namecount)
|
||||
# add to the preferences
|
||||
self.dedrmprefs[prefkind][newname] = keyvalue
|
||||
return (True, newname)
|
||||
except:
|
||||
traceback.print_exc()
|
||||
pass
|
||||
return (False, keyname)
|
||||
|
||||
def addvaluetoprefs(self, prefkind, prefsvalue):
|
||||
# ensure the keyvalue isn't already in the preferences
|
||||
try:
|
||||
if prefsvalue not in self.dedrmprefs[prefkind]:
|
||||
self.dedrmprefs[prefkind].append(prefsvalue)
|
||||
return True
|
||||
except:
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
|
||||
def convertprefs(always = False):
|
||||
|
||||
def parseIgnobleString(keystuff):
|
||||
from calibre_plugins.dedrm.ignoblekeygen import generate_key
|
||||
userkeys = []
|
||||
ar = keystuff.split(':')
|
||||
for keystring in ar:
|
||||
try:
|
||||
name, ccn = keystring.split(',')
|
||||
# Generate Barnes & Noble EPUB user key from name and credit card number.
|
||||
keyname = "{0}_{1}".format(name.strip(),ccn.strip()[-4:])
|
||||
keyvalue = generate_key(name, ccn)
|
||||
userkeys.append([keyname,keyvalue])
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
print(e.args[0])
|
||||
pass
|
||||
return userkeys
|
||||
|
||||
def parseeReaderString(keystuff):
|
||||
from calibre_plugins.dedrm.erdr2pml import getuser_key
|
||||
userkeys = []
|
||||
ar = keystuff.split(':')
|
||||
for keystring in ar:
|
||||
try:
|
||||
name, cc = keystring.split(',')
|
||||
# Generate eReader user key from name and credit card number.
|
||||
keyname = "{0}_{1}".format(name.strip(),cc.strip()[-4:])
|
||||
keyvalue = codecs.encode(getuser_key(name,cc),'hex')
|
||||
userkeys.append([keyname,keyvalue])
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
print(e.args[0])
|
||||
pass
|
||||
return userkeys
|
||||
|
||||
def parseKindleString(keystuff):
|
||||
pids = []
|
||||
serials = []
|
||||
ar = keystuff.split(',')
|
||||
for keystring in ar:
|
||||
keystring = str(keystring).strip().replace(" ","")
|
||||
if len(keystring) == 10 or len(keystring) == 8 and keystring not in pids:
|
||||
pids.append(keystring)
|
||||
elif len(keystring) == 16 and keystring[0] == 'B' and keystring not in serials:
|
||||
serials.append(keystring)
|
||||
return (pids,serials)
|
||||
|
||||
def getConfigFiles(extension, encoding = None):
|
||||
# get any files with extension 'extension' in the config dir
|
||||
userkeys = []
|
||||
files = [f for f in os.listdir(config_dir) if f.endswith(extension)]
|
||||
for filename in files:
|
||||
try:
|
||||
fpath = os.path.join(config_dir, filename)
|
||||
key = os.path.splitext(filename)[0]
|
||||
value = open(fpath, 'rb').read()
|
||||
if encoding is not None:
|
||||
value = codecs.encode(value,encoding)
|
||||
userkeys.append([key,value])
|
||||
except:
|
||||
traceback.print_exc()
|
||||
pass
|
||||
return userkeys
|
||||
|
||||
dedrmprefs = DeDRM_Prefs()
|
||||
|
||||
if (not always) and dedrmprefs['configured']:
|
||||
# We've already converted old preferences,
|
||||
# and we're not being forced to do it again, so just return
|
||||
return
|
||||
|
||||
|
||||
print("{0} v{1}: Importing configuration data from old DeDRM plugins".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||
|
||||
IGNOBLEPLUGINNAME = "Ignoble Epub DeDRM"
|
||||
EREADERPLUGINNAME = "eReader PDB 2 PML"
|
||||
OLDKINDLEPLUGINNAME = "K4PC, K4Mac, Kindle Mobi and Topaz DeDRM"
|
||||
|
||||
# get prefs from older tools
|
||||
kindleprefs = JSONConfig(os.path.join("plugins", "K4MobiDeDRM"))
|
||||
ignobleprefs = JSONConfig(os.path.join("plugins", "ignoble_epub_dedrm"))
|
||||
|
||||
# Handle the old ignoble plugin's customization string by converting the
|
||||
# old string to stored keys... get that personal data out of plain sight.
|
||||
from calibre.customize.ui import config
|
||||
sc = config['plugin_customization']
|
||||
val = sc.pop(IGNOBLEPLUGINNAME, None)
|
||||
if val is not None:
|
||||
print("{0} v{1}: Converting old Ignoble plugin configuration string.".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||
priorkeycount = len(dedrmprefs['bandnkeys'])
|
||||
userkeys = parseIgnobleString(str(val))
|
||||
for keypair in userkeys:
|
||||
name = keypair[0]
|
||||
value = keypair[1]
|
||||
dedrmprefs.addnamedvaluetoprefs('bandnkeys', name, value)
|
||||
addedkeycount = len(dedrmprefs['bandnkeys'])-priorkeycount
|
||||
print("{0} v{1}: {2:d} Barnes and Noble {3} imported from old Ignoble plugin configuration string".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, "key" if addedkeycount==1 else "keys"))
|
||||
# Make the json write all the prefs to disk
|
||||
dedrmprefs.writeprefs(False)
|
||||
|
||||
# Handle the old eReader plugin's customization string by converting the
|
||||
# old string to stored keys... get that personal data out of plain sight.
|
||||
val = sc.pop(EREADERPLUGINNAME, None)
|
||||
if val is not None:
|
||||
print("{0} v{1}: Converting old eReader plugin configuration string.".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||
priorkeycount = len(dedrmprefs['ereaderkeys'])
|
||||
userkeys = parseeReaderString(str(val))
|
||||
for keypair in userkeys:
|
||||
name = keypair[0]
|
||||
value = keypair[1]
|
||||
dedrmprefs.addnamedvaluetoprefs('ereaderkeys', name, value)
|
||||
addedkeycount = len(dedrmprefs['ereaderkeys'])-priorkeycount
|
||||
print("{0} v{1}: {2:d} eReader {3} imported from old eReader plugin configuration string".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, "key" if addedkeycount==1 else "keys"))
|
||||
# Make the json write all the prefs to disk
|
||||
dedrmprefs.writeprefs(False)
|
||||
|
||||
# get old Kindle plugin configuration string
|
||||
val = sc.pop(OLDKINDLEPLUGINNAME, None)
|
||||
if val is not None:
|
||||
print("{0} v{1}: Converting old Kindle plugin configuration string.".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||
priorpidcount = len(dedrmprefs['pids'])
|
||||
priorserialcount = len(dedrmprefs['serials'])
|
||||
pids, serials = parseKindleString(val)
|
||||
for pid in pids:
|
||||
dedrmprefs.addvaluetoprefs('pids',pid)
|
||||
for serial in serials:
|
||||
dedrmprefs.addvaluetoprefs('serials',serial)
|
||||
addedpidcount = len(dedrmprefs['pids']) - priorpidcount
|
||||
addedserialcount = len(dedrmprefs['serials']) - priorserialcount
|
||||
print("{0} v{1}: {2:d} {3} and {4:d} {5} imported from old Kindle plugin configuration string.".format(PLUGIN_NAME, PLUGIN_VERSION, addedpidcount, "PID" if addedpidcount==1 else "PIDs", addedserialcount, "serial number" if addedserialcount==1 else "serial numbers"))
|
||||
# Make the json write all the prefs to disk
|
||||
dedrmprefs.writeprefs(False)
|
||||
|
||||
# copy the customisations back into calibre preferences, as we've now removed the nasty plaintext
|
||||
config['plugin_customization'] = sc
|
||||
|
||||
# get any .b64 files in the config dir
|
||||
priorkeycount = len(dedrmprefs['bandnkeys'])
|
||||
bandnfilekeys = getConfigFiles('.b64')
|
||||
for keypair in bandnfilekeys:
|
||||
name = keypair[0]
|
||||
value = keypair[1]
|
||||
dedrmprefs.addnamedvaluetoprefs('bandnkeys', name, value)
|
||||
addedkeycount = len(dedrmprefs['bandnkeys'])-priorkeycount
|
||||
if addedkeycount > 0:
|
||||
print("{0} v{1}: {2:d} Barnes and Noble {3} imported from config folder.".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, "key file" if addedkeycount==1 else "key files"))
|
||||
# Make the json write all the prefs to disk
|
||||
dedrmprefs.writeprefs(False)
|
||||
|
||||
# get any .der files in the config dir
|
||||
priorkeycount = len(dedrmprefs['adeptkeys'])
|
||||
adeptfilekeys = getConfigFiles('.der','hex')
|
||||
for keypair in adeptfilekeys:
|
||||
name = keypair[0]
|
||||
value = keypair[1]
|
||||
dedrmprefs.addnamedvaluetoprefs('adeptkeys', name, value)
|
||||
addedkeycount = len(dedrmprefs['adeptkeys'])-priorkeycount
|
||||
if addedkeycount > 0:
|
||||
print("{0} v{1}: {2:d} Adobe Adept {3} imported from config folder.".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, "keyfile" if addedkeycount==1 else "keyfiles"))
|
||||
# Make the json write all the prefs to disk
|
||||
dedrmprefs.writeprefs(False)
|
||||
|
||||
# get ignoble json prefs
|
||||
if 'keys' in ignobleprefs:
|
||||
priorkeycount = len(dedrmprefs['bandnkeys'])
|
||||
for name in ignobleprefs['keys']:
|
||||
value = ignobleprefs['keys'][name]
|
||||
dedrmprefs.addnamedvaluetoprefs('bandnkeys', name, value)
|
||||
addedkeycount = len(dedrmprefs['bandnkeys']) - priorkeycount
|
||||
# no need to delete old prefs, since they contain no recoverable private data
|
||||
if addedkeycount > 0:
|
||||
print("{0} v{1}: {2:d} Barnes and Noble {3} imported from Ignoble plugin preferences.".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, "key" if addedkeycount==1 else "keys"))
|
||||
# Make the json write all the prefs to disk
|
||||
dedrmprefs.writeprefs(False)
|
||||
|
||||
# get kindle json prefs
|
||||
priorpidcount = len(dedrmprefs['pids'])
|
||||
priorserialcount = len(dedrmprefs['serials'])
|
||||
if 'pids' in kindleprefs:
|
||||
pids, serials = parseKindleString(kindleprefs['pids'])
|
||||
for pid in pids:
|
||||
dedrmprefs.addvaluetoprefs('pids',pid)
|
||||
if 'serials' in kindleprefs:
|
||||
pids, serials = parseKindleString(kindleprefs['serials'])
|
||||
for serial in serials:
|
||||
dedrmprefs.addvaluetoprefs('serials',serial)
|
||||
addedpidcount = len(dedrmprefs['pids']) - priorpidcount
|
||||
if addedpidcount > 0:
|
||||
print("{0} v{1}: {2:d} {3} imported from Kindle plugin preferences".format(PLUGIN_NAME, PLUGIN_VERSION, addedpidcount, "PID" if addedpidcount==1 else "PIDs"))
|
||||
addedserialcount = len(dedrmprefs['serials']) - priorserialcount
|
||||
if addedserialcount > 0:
|
||||
print("{0} v{1}: {2:d} {3} imported from Kindle plugin preferences".format(PLUGIN_NAME, PLUGIN_VERSION, addedserialcount, "serial number" if addedserialcount==1 else "serial numbers"))
|
||||
try:
|
||||
if 'wineprefix' in kindleprefs and kindleprefs['wineprefix'] != "":
|
||||
dedrmprefs.set('adobewineprefix',kindleprefs['wineprefix'])
|
||||
dedrmprefs.set('kindlewineprefix',kindleprefs['wineprefix'])
|
||||
print("{0} v{1}: WINEPREFIX ‘(2)’ imported from Kindle plugin preferences".format(PLUGIN_NAME, PLUGIN_VERSION, kindleprefs['wineprefix']))
|
||||
except:
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
# Make the json write all the prefs to disk
|
||||
dedrmprefs.writeprefs()
|
||||
print("{0} v{1}: Finished setting up configuration data.".format(PLUGIN_NAME, PLUGIN_VERSION))
|
||||
30
DeDRM_plugin/pycrypto_des.py
Normal file
30
DeDRM_plugin/pycrypto_des.py
Normal file
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
|
||||
|
||||
|
||||
def load_pycrypto():
|
||||
try :
|
||||
from Crypto.Cipher import DES as _DES
|
||||
except:
|
||||
return None
|
||||
|
||||
class DES(object):
|
||||
def __init__(self, key):
|
||||
if len(key) != 8 :
|
||||
raise ValueError('DES improper key used')
|
||||
self.key = key
|
||||
self._des = _DES.new(key,_DES.MODE_ECB)
|
||||
def desdecrypt(self, data):
|
||||
return self._des.decrypt(data)
|
||||
def decrypt(self, data):
|
||||
if not data:
|
||||
return ''
|
||||
i = 0
|
||||
result = []
|
||||
while i < len(data):
|
||||
block = data[i:i+8]
|
||||
processed_block = self.desdecrypt(block)
|
||||
result.append(processed_block)
|
||||
i += 8
|
||||
return ''.join(result)
|
||||
return DES
|
||||
220
DeDRM_plugin/python_des.py
Normal file
220
DeDRM_plugin/python_des.py
Normal file
@@ -0,0 +1,220 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
|
||||
import sys
|
||||
|
||||
ECB = 0
|
||||
CBC = 1
|
||||
class Des(object):
|
||||
__pc1 = [56, 48, 40, 32, 24, 16, 8, 0, 57, 49, 41, 33, 25, 17,
|
||||
9, 1, 58, 50, 42, 34, 26, 18, 10, 2, 59, 51, 43, 35,
|
||||
62, 54, 46, 38, 30, 22, 14, 6, 61, 53, 45, 37, 29, 21,
|
||||
13, 5, 60, 52, 44, 36, 28, 20, 12, 4, 27, 19, 11, 3]
|
||||
__left_rotations = [1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1]
|
||||
__pc2 = [13, 16, 10, 23, 0, 4,2, 27, 14, 5, 20, 9,
|
||||
22, 18, 11, 3, 25, 7, 15, 6, 26, 19, 12, 1,
|
||||
40, 51, 30, 36, 46, 54, 29, 39, 50, 44, 32, 47,
|
||||
43, 48, 38, 55, 33, 52, 45, 41, 49, 35, 28, 31]
|
||||
__ip = [57, 49, 41, 33, 25, 17, 9, 1, 59, 51, 43, 35, 27, 19, 11, 3,
|
||||
61, 53, 45, 37, 29, 21, 13, 5, 63, 55, 47, 39, 31, 23, 15, 7,
|
||||
56, 48, 40, 32, 24, 16, 8, 0, 58, 50, 42, 34, 26, 18, 10, 2,
|
||||
60, 52, 44, 36, 28, 20, 12, 4, 62, 54, 46, 38, 30, 22, 14, 6]
|
||||
__expansion_table = [31, 0, 1, 2, 3, 4, 3, 4, 5, 6, 7, 8,
|
||||
7, 8, 9, 10, 11, 12,11, 12, 13, 14, 15, 16,
|
||||
15, 16, 17, 18, 19, 20,19, 20, 21, 22, 23, 24,
|
||||
23, 24, 25, 26, 27, 28,27, 28, 29, 30, 31, 0]
|
||||
__sbox = [[14, 4, 13, 1, 2, 15, 11, 8, 3, 10, 6, 12, 5, 9, 0, 7,
|
||||
0, 15, 7, 4, 14, 2, 13, 1, 10, 6, 12, 11, 9, 5, 3, 8,
|
||||
4, 1, 14, 8, 13, 6, 2, 11, 15, 12, 9, 7, 3, 10, 5, 0,
|
||||
15, 12, 8, 2, 4, 9, 1, 7, 5, 11, 3, 14, 10, 0, 6, 13],
|
||||
[15, 1, 8, 14, 6, 11, 3, 4, 9, 7, 2, 13, 12, 0, 5, 10,
|
||||
3, 13, 4, 7, 15, 2, 8, 14, 12, 0, 1, 10, 6, 9, 11, 5,
|
||||
0, 14, 7, 11, 10, 4, 13, 1, 5, 8, 12, 6, 9, 3, 2, 15,
|
||||
13, 8, 10, 1, 3, 15, 4, 2, 11, 6, 7, 12, 0, 5, 14, 9],
|
||||
[10, 0, 9, 14, 6, 3, 15, 5, 1, 13, 12, 7, 11, 4, 2, 8,
|
||||
13, 7, 0, 9, 3, 4, 6, 10, 2, 8, 5, 14, 12, 11, 15, 1,
|
||||
13, 6, 4, 9, 8, 15, 3, 0, 11, 1, 2, 12, 5, 10, 14, 7,
|
||||
1, 10, 13, 0, 6, 9, 8, 7, 4, 15, 14, 3, 11, 5, 2, 12],
|
||||
[7, 13, 14, 3, 0, 6, 9, 10, 1, 2, 8, 5, 11, 12, 4, 15,
|
||||
13, 8, 11, 5, 6, 15, 0, 3, 4, 7, 2, 12, 1, 10, 14, 9,
|
||||
10, 6, 9, 0, 12, 11, 7, 13, 15, 1, 3, 14, 5, 2, 8, 4,
|
||||
3, 15, 0, 6, 10, 1, 13, 8, 9, 4, 5, 11, 12, 7, 2, 14],
|
||||
[2, 12, 4, 1, 7, 10, 11, 6, 8, 5, 3, 15, 13, 0, 14, 9,
|
||||
14, 11, 2, 12, 4, 7, 13, 1, 5, 0, 15, 10, 3, 9, 8, 6,
|
||||
4, 2, 1, 11, 10, 13, 7, 8, 15, 9, 12, 5, 6, 3, 0, 14,
|
||||
11, 8, 12, 7, 1, 14, 2, 13, 6, 15, 0, 9, 10, 4, 5, 3],
|
||||
[12, 1, 10, 15, 9, 2, 6, 8, 0, 13, 3, 4, 14, 7, 5, 11,
|
||||
10, 15, 4, 2, 7, 12, 9, 5, 6, 1, 13, 14, 0, 11, 3, 8,
|
||||
9, 14, 15, 5, 2, 8, 12, 3, 7, 0, 4, 10, 1, 13, 11, 6,
|
||||
4, 3, 2, 12, 9, 5, 15, 10, 11, 14, 1, 7, 6, 0, 8, 13],
|
||||
[4, 11, 2, 14, 15, 0, 8, 13, 3, 12, 9, 7, 5, 10, 6, 1,
|
||||
13, 0, 11, 7, 4, 9, 1, 10, 14, 3, 5, 12, 2, 15, 8, 6,
|
||||
1, 4, 11, 13, 12, 3, 7, 14, 10, 15, 6, 8, 0, 5, 9, 2,
|
||||
6, 11, 13, 8, 1, 4, 10, 7, 9, 5, 0, 15, 14, 2, 3, 12],
|
||||
[13, 2, 8, 4, 6, 15, 11, 1, 10, 9, 3, 14, 5, 0, 12, 7,
|
||||
1, 15, 13, 8, 10, 3, 7, 4, 12, 5, 6, 11, 0, 14, 9, 2,
|
||||
7, 11, 4, 1, 9, 12, 14, 2, 0, 6, 10, 13, 15, 3, 5, 8,
|
||||
2, 1, 14, 7, 4, 10, 8, 13, 15, 12, 9, 0, 3, 5, 6, 11],]
|
||||
__p = [15, 6, 19, 20, 28, 11,27, 16, 0, 14, 22, 25,
|
||||
4, 17, 30, 9, 1, 7,23,13, 31, 26, 2, 8,18, 12, 29, 5, 21, 10,3, 24]
|
||||
__fp = [39, 7, 47, 15, 55, 23, 63, 31,38, 6, 46, 14, 54, 22, 62, 30,
|
||||
37, 5, 45, 13, 53, 21, 61, 29,36, 4, 44, 12, 52, 20, 60, 28,
|
||||
35, 3, 43, 11, 51, 19, 59, 27,34, 2, 42, 10, 50, 18, 58, 26,
|
||||
33, 1, 41, 9, 49, 17, 57, 25,32, 0, 40, 8, 48, 16, 56, 24]
|
||||
# Type of crypting being done
|
||||
ENCRYPT = 0x00
|
||||
DECRYPT = 0x01
|
||||
def __init__(self, key, mode=ECB, IV=None):
|
||||
if len(key) != 8:
|
||||
raise ValueError("Invalid DES key size. Key must be exactly 8 bytes long.")
|
||||
self.block_size = 8
|
||||
self.key_size = 8
|
||||
self.__padding = ''
|
||||
self.setMode(mode)
|
||||
if IV:
|
||||
self.setIV(IV)
|
||||
self.L = []
|
||||
self.R = []
|
||||
self.Kn = [ [0] * 48 ] * 16 # 16 48-bit keys (K1 - K16)
|
||||
self.final = []
|
||||
self.setKey(key)
|
||||
def getKey(self):
|
||||
return self.__key
|
||||
def setKey(self, key):
|
||||
self.__key = key
|
||||
self.__create_sub_keys()
|
||||
def getMode(self):
|
||||
return self.__mode
|
||||
def setMode(self, mode):
|
||||
self.__mode = mode
|
||||
def getIV(self):
|
||||
return self.__iv
|
||||
def setIV(self, IV):
|
||||
if not IV or len(IV) != self.block_size:
|
||||
raise ValueError("Invalid Initial Value (IV), must be a multiple of " + str(self.block_size) + " bytes")
|
||||
self.__iv = IV
|
||||
def getPadding(self):
|
||||
return self.__padding
|
||||
def __String_to_BitList(self, data):
|
||||
l = len(data) * 8
|
||||
result = [0] * l
|
||||
pos = 0
|
||||
for c in data:
|
||||
i = 7
|
||||
ch = ord(c)
|
||||
while i >= 0:
|
||||
if ch & (1 << i) != 0:
|
||||
result[pos] = 1
|
||||
else:
|
||||
result[pos] = 0
|
||||
pos += 1
|
||||
i -= 1
|
||||
return result
|
||||
def __BitList_to_String(self, data):
|
||||
result = ''
|
||||
pos = 0
|
||||
c = 0
|
||||
while pos < len(data):
|
||||
c += data[pos] << (7 - (pos % 8))
|
||||
if (pos % 8) == 7:
|
||||
result += chr(c)
|
||||
c = 0
|
||||
pos += 1
|
||||
return result
|
||||
def __permutate(self, table, block):
|
||||
return [block[x] for x in table]
|
||||
def __create_sub_keys(self):
|
||||
key = self.__permutate(Des.__pc1, self.__String_to_BitList(self.getKey()))
|
||||
i = 0
|
||||
self.L = key[:28]
|
||||
self.R = key[28:]
|
||||
while i < 16:
|
||||
j = 0
|
||||
while j < Des.__left_rotations[i]:
|
||||
self.L.append(self.L[0])
|
||||
del self.L[0]
|
||||
self.R.append(self.R[0])
|
||||
del self.R[0]
|
||||
j += 1
|
||||
self.Kn[i] = self.__permutate(Des.__pc2, self.L + self.R)
|
||||
i += 1
|
||||
def __des_crypt(self, block, crypt_type):
|
||||
block = self.__permutate(Des.__ip, block)
|
||||
self.L = block[:32]
|
||||
self.R = block[32:]
|
||||
if crypt_type == Des.ENCRYPT:
|
||||
iteration = 0
|
||||
iteration_adjustment = 1
|
||||
else:
|
||||
iteration = 15
|
||||
iteration_adjustment = -1
|
||||
i = 0
|
||||
while i < 16:
|
||||
tempR = self.R[:]
|
||||
self.R = self.__permutate(Des.__expansion_table, self.R)
|
||||
self.R = [x ^ y for x,y in zip(self.R, self.Kn[iteration])]
|
||||
B = [self.R[:6], self.R[6:12], self.R[12:18], self.R[18:24], self.R[24:30], self.R[30:36], self.R[36:42], self.R[42:]]
|
||||
j = 0
|
||||
Bn = [0] * 32
|
||||
pos = 0
|
||||
while j < 8:
|
||||
m = (B[j][0] << 1) + B[j][5]
|
||||
n = (B[j][1] << 3) + (B[j][2] << 2) + (B[j][3] << 1) + B[j][4]
|
||||
v = Des.__sbox[j][(m << 4) + n]
|
||||
Bn[pos] = (v & 8) >> 3
|
||||
Bn[pos + 1] = (v & 4) >> 2
|
||||
Bn[pos + 2] = (v & 2) >> 1
|
||||
Bn[pos + 3] = v & 1
|
||||
pos += 4
|
||||
j += 1
|
||||
self.R = self.__permutate(Des.__p, Bn)
|
||||
self.R = [x ^ y for x, y in zip(self.R, self.L)]
|
||||
self.L = tempR
|
||||
i += 1
|
||||
iteration += iteration_adjustment
|
||||
self.final = self.__permutate(Des.__fp, self.R + self.L)
|
||||
return self.final
|
||||
def crypt(self, data, crypt_type):
|
||||
if not data:
|
||||
return ''
|
||||
if len(data) % self.block_size != 0:
|
||||
if crypt_type == Des.DECRYPT: # Decryption must work on 8 byte blocks
|
||||
raise ValueError("Invalid data length, data must be a multiple of " + str(self.block_size) + " bytes\n.")
|
||||
if not self.getPadding():
|
||||
raise ValueError("Invalid data length, data must be a multiple of " + str(self.block_size) + " bytes\n. Try setting the optional padding character")
|
||||
else:
|
||||
data += (self.block_size - (len(data) % self.block_size)) * self.getPadding()
|
||||
if self.getMode() == CBC:
|
||||
if self.getIV():
|
||||
iv = self.__String_to_BitList(self.getIV())
|
||||
else:
|
||||
raise ValueError("For CBC mode, you must supply the Initial Value (IV) for ciphering")
|
||||
i = 0
|
||||
dict = {}
|
||||
result = []
|
||||
while i < len(data):
|
||||
block = self.__String_to_BitList(data[i:i+8])
|
||||
if self.getMode() == CBC:
|
||||
if crypt_type == Des.ENCRYPT:
|
||||
block = [x ^ y for x, y in zip(block, iv)]
|
||||
processed_block = self.__des_crypt(block, crypt_type)
|
||||
if crypt_type == Des.DECRYPT:
|
||||
processed_block = [x ^ y for x, y in zip(processed_block, iv)]
|
||||
iv = block
|
||||
else:
|
||||
iv = processed_block
|
||||
else:
|
||||
processed_block = self.__des_crypt(block, crypt_type)
|
||||
result.append(self.__BitList_to_String(processed_block))
|
||||
i += 8
|
||||
if crypt_type == Des.DECRYPT and self.getPadding():
|
||||
s = result[-1]
|
||||
while s[-1] == self.getPadding():
|
||||
s = s[:-1]
|
||||
result[-1] = s
|
||||
return ''.join(result)
|
||||
def encrypt(self, data, pad=''):
|
||||
self.__padding = pad
|
||||
return self.crypt(data, Des.ENCRYPT)
|
||||
def decrypt(self, data, pad=''):
|
||||
self.__padding = pad
|
||||
return self.crypt(data, Des.DECRYPT)
|
||||
201
DeDRM_plugin/scriptinterface.py
Normal file
201
DeDRM_plugin/scriptinterface.py
Normal file
@@ -0,0 +1,201 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
|
||||
|
||||
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
import traceback
|
||||
import calibre_plugins.dedrm.ineptepub
|
||||
import calibre_plugins.dedrm.ignobleepub
|
||||
import calibre_plugins.dedrm.epubtest
|
||||
import calibre_plugins.dedrm.zipfix
|
||||
import calibre_plugins.dedrm.ineptpdf
|
||||
import calibre_plugins.dedrm.erdr2pml
|
||||
import calibre_plugins.dedrm.k4mobidedrm
|
||||
|
||||
def decryptepub(infile, outdir, rscpath):
|
||||
errlog = ''
|
||||
|
||||
# first fix the epub to make sure we do not get errors
|
||||
name, ext = os.path.splitext(os.path.basename(infile))
|
||||
bpath = os.path.dirname(infile)
|
||||
zippath = os.path.join(bpath,name + '_temp.zip')
|
||||
rv = zipfix.repairBook(infile, zippath)
|
||||
if rv != 0:
|
||||
print("Error while trying to fix epub")
|
||||
return rv
|
||||
|
||||
# determine a good name for the output file
|
||||
outfile = os.path.join(outdir, name + '_nodrm.epub')
|
||||
|
||||
rv = 1
|
||||
# first try with the Adobe adept epub
|
||||
if ineptepub.adeptBook(zippath):
|
||||
# try with any keyfiles (*.der) in the rscpath
|
||||
files = os.listdir(rscpath)
|
||||
filefilter = re.compile("\.der$", re.IGNORECASE)
|
||||
files = filter(filefilter.search, files)
|
||||
if files:
|
||||
for filename in files:
|
||||
keypath = os.path.join(rscpath, filename)
|
||||
userkey = open(keypath,'rb').read()
|
||||
try:
|
||||
rv = ineptepub.decryptBook(userkey, zippath, outfile)
|
||||
if rv == 0:
|
||||
print("Decrypted Adobe ePub with key file {0}".format(filename))
|
||||
break
|
||||
except Exception as e:
|
||||
errlog += traceback.format_exc()
|
||||
errlog += str(e)
|
||||
rv = 1
|
||||
# now try with ignoble epub
|
||||
elif ignobleepub.ignobleBook(zippath):
|
||||
# try with any keyfiles (*.b64) in the rscpath
|
||||
files = os.listdir(rscpath)
|
||||
filefilter = re.compile("\.b64$", re.IGNORECASE)
|
||||
files = filter(filefilter.search, files)
|
||||
if files:
|
||||
for filename in files:
|
||||
keypath = os.path.join(rscpath, filename)
|
||||
userkey = open(keypath,'r').read()
|
||||
#print userkey
|
||||
try:
|
||||
rv = ignobleepub.decryptBook(userkey, zippath, outfile)
|
||||
if rv == 0:
|
||||
print("Decrypted B&N ePub with key file {0}".format(filename))
|
||||
break
|
||||
except Exception as e:
|
||||
errlog += traceback.format_exc()
|
||||
errlog += str(e)
|
||||
rv = 1
|
||||
else:
|
||||
encryption = epubtest.encryption(zippath)
|
||||
if encryption == "Unencrypted":
|
||||
print("{0} is not DRMed.".format(name))
|
||||
rv = 0
|
||||
else:
|
||||
print("{0} has an unknown encryption.".format(name))
|
||||
|
||||
os.remove(zippath)
|
||||
if rv != 0:
|
||||
print(errlog)
|
||||
return rv
|
||||
|
||||
|
||||
def decryptpdf(infile, outdir, rscpath):
|
||||
errlog = ''
|
||||
rv = 1
|
||||
|
||||
# determine a good name for the output file
|
||||
name, ext = os.path.splitext(os.path.basename(infile))
|
||||
outfile = os.path.join(outdir, name + '_nodrm.pdf')
|
||||
|
||||
# try with any keyfiles (*.der) in the rscpath
|
||||
files = os.listdir(rscpath)
|
||||
filefilter = re.compile("\.der$", re.IGNORECASE)
|
||||
files = filter(filefilter.search, files)
|
||||
if files:
|
||||
for filename in files:
|
||||
keypath = os.path.join(rscpath, filename)
|
||||
userkey = open(keypath,'rb').read()
|
||||
try:
|
||||
rv = ineptpdf.decryptBook(userkey, infile, outfile)
|
||||
if rv == 0:
|
||||
break
|
||||
except Exception as e:
|
||||
errlog += traceback.format_exc()
|
||||
errlog += str(e)
|
||||
rv = 1
|
||||
|
||||
if rv != 0:
|
||||
print(errlog)
|
||||
return rv
|
||||
|
||||
|
||||
def decryptpdb(infile, outdir, rscpath):
|
||||
errlog = ''
|
||||
outname = os.path.splitext(os.path.basename(infile))[0] + ".pmlz"
|
||||
outpath = os.path.join(outdir, outname)
|
||||
rv = 1
|
||||
socialpath = os.path.join(rscpath,'sdrmlist.txt')
|
||||
if os.path.exists(socialpath):
|
||||
keydata = file(socialpath,'r').read()
|
||||
keydata = keydata.rstrip(os.linesep)
|
||||
ar = keydata.split(',')
|
||||
for i in ar:
|
||||
try:
|
||||
name, cc8 = i.split(':')
|
||||
except ValueError:
|
||||
print(' Error parsing user supplied social drm data.')
|
||||
return 1
|
||||
try:
|
||||
rv = erdr2pml.decryptBook(infile, outpath, True, erdr2pml.getuser_key(name, cc8))
|
||||
except Exception as e:
|
||||
errlog += traceback.format_exc()
|
||||
errlog += str(e)
|
||||
rv = 1
|
||||
|
||||
if rv == 0:
|
||||
break
|
||||
return rv
|
||||
|
||||
|
||||
def decryptk4mobi(infile, outdir, rscpath):
|
||||
errlog = ''
|
||||
rv = 1
|
||||
pidnums = []
|
||||
pidspath = os.path.join(rscpath,'pidlist.txt')
|
||||
if os.path.exists(pidspath):
|
||||
pidstr = file(pidspath,'r').read()
|
||||
pidstr = pidstr.rstrip(os.linesep)
|
||||
pidstr = pidstr.strip()
|
||||
if pidstr != '':
|
||||
pidnums = pidstr.split(',')
|
||||
serialnums = []
|
||||
serialnumspath = os.path.join(rscpath,'seriallist.txt')
|
||||
if os.path.exists(serialnumspath):
|
||||
serialstr = file(serialnumspath,'r').read()
|
||||
serialstr = serialstr.rstrip(os.linesep)
|
||||
serialstr = serialstr.strip()
|
||||
if serialstr != '':
|
||||
serialnums = serialstr.split(',')
|
||||
kDatabaseFiles = []
|
||||
files = os.listdir(rscpath)
|
||||
filefilter = re.compile("\.k4i$", re.IGNORECASE)
|
||||
files = filter(filefilter.search, files)
|
||||
if files:
|
||||
for filename in files:
|
||||
dpath = os.path.join(rscpath,filename)
|
||||
kDatabaseFiles.append(dpath)
|
||||
androidFiles = []
|
||||
files = os.listdir(rscpath)
|
||||
filefilter = re.compile("\.ab$", re.IGNORECASE)
|
||||
files = filter(filefilter.search, files)
|
||||
if files:
|
||||
for filename in files:
|
||||
dpath = os.path.join(rscpath,filename)
|
||||
androidFiles.append(dpath)
|
||||
files = os.listdir(rscpath)
|
||||
filefilter = re.compile("\.db$", re.IGNORECASE)
|
||||
files = filter(filefilter.search, files)
|
||||
if files:
|
||||
for filename in files:
|
||||
dpath = os.path.join(rscpath,filename)
|
||||
androidFiles.append(dpath)
|
||||
files = os.listdir(rscpath)
|
||||
filefilter = re.compile("\.xml$", re.IGNORECASE)
|
||||
files = filter(filefilter.search, files)
|
||||
if files:
|
||||
for filename in files:
|
||||
dpath = os.path.join(rscpath,filename)
|
||||
androidFiles.append(dpath)
|
||||
try:
|
||||
rv = k4mobidedrm.decryptBook(infile, outdir, kDatabaseFiles, androidFiles, serialnums, pidnums)
|
||||
except Exception as e:
|
||||
errlog += traceback.format_exc()
|
||||
errlog += str(e)
|
||||
rv = 1
|
||||
|
||||
return rv
|
||||
@@ -1,23 +1,23 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
|
||||
|
||||
import Tkinter
|
||||
import Tkconstants
|
||||
import tkinter
|
||||
import tkinter.constants
|
||||
|
||||
# basic scrolled text widget
|
||||
class ScrolledText(Tkinter.Text):
|
||||
class ScrolledText(tkinter.Text):
|
||||
def __init__(self, master=None, **kw):
|
||||
self.frame = Tkinter.Frame(master)
|
||||
self.vbar = Tkinter.Scrollbar(self.frame)
|
||||
self.vbar.pack(side=Tkconstants.RIGHT, fill=Tkconstants.Y)
|
||||
self.frame = tkinter.Frame(master)
|
||||
self.vbar = tkinter.Scrollbar(self.frame)
|
||||
self.vbar.pack(side=tkinter.constants.RIGHT, fill=tkinter.constants.Y)
|
||||
kw.update({'yscrollcommand': self.vbar.set})
|
||||
Tkinter.Text.__init__(self, self.frame, **kw)
|
||||
self.pack(side=Tkconstants.LEFT, fill=Tkconstants.BOTH, expand=True)
|
||||
tkinter.Text.__init__(self, self.frame, **kw)
|
||||
self.pack(side=tkinter.constants.LEFT, fill=tkinter.constants.BOTH, expand=True)
|
||||
self.vbar['command'] = self.yview
|
||||
# Copy geometry methods of self.frame without overriding Text
|
||||
# methods = hack!
|
||||
text_meths = vars(Tkinter.Text).keys()
|
||||
methods = vars(Tkinter.Pack).keys() + vars(Tkinter.Grid).keys() + vars(Tkinter.Place).keys()
|
||||
text_meths = list(vars(tkinter.Text).keys())
|
||||
methods = list(vars(tkinter.Pack).keys()) + list(vars(tkinter.Grid).keys()) + list(vars(tkinter.Place).keys())
|
||||
methods = set(methods).difference(text_meths)
|
||||
for m in methods:
|
||||
if m[0] != '_' and m != 'config' and m != 'configure':
|
||||
78
DeDRM_plugin/simpleprefs.py
Normal file
78
DeDRM_plugin/simpleprefs.py
Normal file
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
|
||||
|
||||
import sys
|
||||
import os, os.path
|
||||
import shutil
|
||||
|
||||
class SimplePrefsError(Exception):
|
||||
pass
|
||||
|
||||
class SimplePrefs(object):
|
||||
def __init__(self, target, description):
|
||||
self.prefs = {}
|
||||
self.key2file={}
|
||||
self.file2key={}
|
||||
for keyfilemap in description:
|
||||
[key, filename] = keyfilemap
|
||||
self.key2file[key] = filename
|
||||
self.file2key[filename] = key
|
||||
self.target = target + 'Prefs'
|
||||
if sys.platform.startswith('win'):
|
||||
import winreg
|
||||
regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\")
|
||||
path = winreg.QueryValueEx(regkey, 'Local AppData')[0]
|
||||
prefdir = path + os.sep + self.target
|
||||
elif sys.platform.startswith('darwin'):
|
||||
home = os.getenv('HOME')
|
||||
prefdir = os.path.join(home,'Library','Preferences','org.' + self.target)
|
||||
else:
|
||||
# linux and various flavors of unix
|
||||
home = os.getenv('HOME')
|
||||
prefdir = os.path.join(home,'.' + self.target)
|
||||
if not os.path.exists(prefdir):
|
||||
os.makedirs(prefdir)
|
||||
self.prefdir = prefdir
|
||||
self.prefs['dir'] = self.prefdir
|
||||
self._loadPreferences()
|
||||
|
||||
def _loadPreferences(self):
|
||||
filenames = os.listdir(self.prefdir)
|
||||
for filename in filenames:
|
||||
if filename in self.file2key:
|
||||
key = self.file2key[filename]
|
||||
filepath = os.path.join(self.prefdir,filename)
|
||||
if os.path.isfile(filepath):
|
||||
try :
|
||||
data = file(filepath,'rb').read()
|
||||
self.prefs[key] = data
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
def getPreferences(self):
|
||||
return self.prefs
|
||||
|
||||
def setPreferences(self, newprefs={}):
|
||||
if 'dir' not in newprefs:
|
||||
raise SimplePrefsError('Error: Attempt to Set Preferences in unspecified directory')
|
||||
if newprefs['dir'] != self.prefs['dir']:
|
||||
raise SimplePrefsError('Error: Attempt to Set Preferences in unspecified directory')
|
||||
for key in newprefs:
|
||||
if key != 'dir':
|
||||
if key in self.key2file:
|
||||
filename = self.key2file[key]
|
||||
filepath = os.path.join(self.prefdir,filename)
|
||||
data = newprefs[key]
|
||||
if data != None:
|
||||
data = str(data)
|
||||
if data == None or data == '':
|
||||
if os.path.exists(filepath):
|
||||
os.remove(filepath)
|
||||
else:
|
||||
try:
|
||||
file(filepath,'wb').write(data)
|
||||
except Exception as e:
|
||||
pass
|
||||
self.prefs = newprefs
|
||||
return
|
||||
@@ -2,49 +2,52 @@
|
||||
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
|
||||
# For use with Topaz Scripts Version 2.6
|
||||
|
||||
|
||||
import csv
|
||||
import sys
|
||||
import os
|
||||
import getopt
|
||||
import re
|
||||
from struct import pack
|
||||
from struct import unpack
|
||||
|
||||
debug = False
|
||||
|
||||
class DocParser(object):
|
||||
def __init__(self, flatxml, fontsize, ph, pw):
|
||||
self.flatdoc = flatxml.split('\n')
|
||||
self.flatdoc = flatxml.split(b'\n')
|
||||
self.fontsize = int(fontsize)
|
||||
self.ph = int(ph) * 1.0
|
||||
self.pw = int(pw) * 1.0
|
||||
|
||||
stags = {
|
||||
'paragraph' : 'p',
|
||||
'graphic' : '.graphic'
|
||||
b'paragraph' : 'p',
|
||||
b'graphic' : '.graphic'
|
||||
}
|
||||
|
||||
attr_val_map = {
|
||||
'hang' : 'text-indent: ',
|
||||
'indent' : 'text-indent: ',
|
||||
'line-space' : 'line-height: ',
|
||||
'margin-bottom' : 'margin-bottom: ',
|
||||
'margin-left' : 'margin-left: ',
|
||||
'margin-right' : 'margin-right: ',
|
||||
'margin-top' : 'margin-top: ',
|
||||
'space-after' : 'padding-bottom: ',
|
||||
b'hang' : 'text-indent: ',
|
||||
b'indent' : 'text-indent: ',
|
||||
b'line-space' : 'line-height: ',
|
||||
b'margin-bottom' : 'margin-bottom: ',
|
||||
b'margin-left' : 'margin-left: ',
|
||||
b'margin-right' : 'margin-right: ',
|
||||
b'margin-top' : 'margin-top: ',
|
||||
b'space-after' : 'padding-bottom: ',
|
||||
}
|
||||
|
||||
attr_str_map = {
|
||||
'align-center' : 'text-align: center; margin-left: auto; margin-right: auto;',
|
||||
'align-left' : 'text-align: left;',
|
||||
'align-right' : 'text-align: right;',
|
||||
'align-justify' : 'text-align: justify;',
|
||||
'display-inline' : 'display: inline;',
|
||||
'pos-left' : 'text-align: left;',
|
||||
'pos-right' : 'text-align: right;',
|
||||
'pos-center' : 'text-align: center; margin-left: auto; margin-right: auto;',
|
||||
b'align-center' : 'text-align: center; margin-left: auto; margin-right: auto;',
|
||||
b'align-left' : 'text-align: left;',
|
||||
b'align-right' : 'text-align: right;',
|
||||
b'align-justify' : 'text-align: justify;',
|
||||
b'display-inline' : 'display: inline;',
|
||||
b'pos-left' : 'text-align: left;',
|
||||
b'pos-right' : 'text-align: right;',
|
||||
b'pos-center' : 'text-align: center; margin-left: auto; margin-right: auto;',
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
# find tag if within pos to end inclusive
|
||||
def findinDoc(self, tagpath, pos, end) :
|
||||
result = None
|
||||
@@ -55,14 +58,16 @@ class DocParser(object):
|
||||
else:
|
||||
end = min(cnt,end)
|
||||
foundat = -1
|
||||
for j in xrange(pos, end):
|
||||
for j in range(pos, end):
|
||||
item = docList[j]
|
||||
if item.find('=') >= 0:
|
||||
(name, argres) = item.split('=',1)
|
||||
else :
|
||||
if item.find(b'=') >= 0:
|
||||
(name, argres) = item.split(b'=',1)
|
||||
else :
|
||||
name = item
|
||||
argres = ''
|
||||
if name.endswith(tagpath) :
|
||||
argres = b''
|
||||
if (isinstance(tagpath,str)):
|
||||
tagpath = tagpath.encode('utf-8')
|
||||
if name.endswith(tagpath) :
|
||||
result = argres
|
||||
foundat = j
|
||||
break
|
||||
@@ -73,7 +78,7 @@ class DocParser(object):
|
||||
def posinDoc(self, tagpath):
|
||||
startpos = []
|
||||
pos = 0
|
||||
res = ""
|
||||
res = b""
|
||||
while res != None :
|
||||
(foundpos, res) = self.findinDoc(tagpath, pos, -1)
|
||||
if res != None :
|
||||
@@ -81,6 +86,21 @@ class DocParser(object):
|
||||
pos = foundpos + 1
|
||||
return startpos
|
||||
|
||||
# returns a vector of integers for the tagpath
|
||||
def getData(self, tagpath, pos, end, clean=False):
|
||||
if clean:
|
||||
digits_only = re.compile(rb'''([0-9]+)''')
|
||||
argres=[]
|
||||
(foundat, argt) = self.findinDoc(tagpath, pos, end)
|
||||
if (argt != None) and (len(argt) > 0) :
|
||||
argList = argt.split(b'|')
|
||||
for strval in argList:
|
||||
if clean:
|
||||
m = re.search(digits_only, strval)
|
||||
if m != None:
|
||||
strval = m.group()
|
||||
argres.append(int(strval))
|
||||
return argres
|
||||
|
||||
def process(self):
|
||||
|
||||
@@ -91,65 +111,85 @@ class DocParser(object):
|
||||
csspage += '.cl-justify { text-align: justify; }\n'
|
||||
|
||||
# generate a list of each <style> starting point in the stylesheet
|
||||
styleList= self.posinDoc('book.stylesheet.style')
|
||||
styleList= self.posinDoc(b'book.stylesheet.style')
|
||||
stylecnt = len(styleList)
|
||||
styleList.append(-1)
|
||||
|
||||
# process each style converting what you can
|
||||
|
||||
for j in xrange(stylecnt):
|
||||
if debug: print(' ', 'Processing styles.')
|
||||
for j in range(stylecnt):
|
||||
if debug: print(' ', 'Processing style %d' %(j))
|
||||
start = styleList[j]
|
||||
end = styleList[j+1]
|
||||
|
||||
(pos, tag) = self.findinDoc('style._tag',start,end)
|
||||
(pos, tag) = self.findinDoc(b'style._tag',start,end)
|
||||
if tag == None :
|
||||
(pos, tag) = self.findinDoc('style.type',start,end)
|
||||
|
||||
(pos, tag) = self.findinDoc(b'style.type',start,end)
|
||||
|
||||
# Is this something we know how to convert to css
|
||||
if tag in self.stags :
|
||||
|
||||
# get the style class
|
||||
(pos, sclass) = self.findinDoc('style.class',start,end)
|
||||
(pos, sclass) = self.findinDoc(b'style.class',start,end)
|
||||
if sclass != None:
|
||||
sclass = sclass.replace(' ','-')
|
||||
sclass = '.cl-' + sclass.lower()
|
||||
else :
|
||||
sclass = ''
|
||||
sclass = sclass.replace(b' ',b'-')
|
||||
sclass = b'.cl-' + sclass.lower()
|
||||
else :
|
||||
sclass = b''
|
||||
|
||||
if debug: print('sclass', sclass)
|
||||
|
||||
# check for any "after class" specifiers
|
||||
(pos, aftclass) = self.findinDoc('style._after_class',start,end)
|
||||
(pos, aftclass) = self.findinDoc(b'style._after_class',start,end)
|
||||
if aftclass != None:
|
||||
aftclass = aftclass.replace(' ','-')
|
||||
aftclass = '.cl-' + aftclass.lower()
|
||||
else :
|
||||
aftclass = ''
|
||||
aftclass = aftclass.replace(b' ',b'-')
|
||||
aftclass = b'.cl-' + aftclass.lower()
|
||||
else :
|
||||
aftclass = b''
|
||||
|
||||
if debug: print('aftclass', aftclass)
|
||||
|
||||
cssargs = {}
|
||||
|
||||
while True :
|
||||
|
||||
(pos1, attr) = self.findinDoc('style.rule.attr', start, end)
|
||||
(pos2, val) = self.findinDoc('style.rule.value', start, end)
|
||||
(pos1, attr) = self.findinDoc(b'style.rule.attr', start, end)
|
||||
(pos2, val) = self.findinDoc(b'style.rule.value', start, end)
|
||||
|
||||
if debug: print('attr', attr)
|
||||
if debug: print('val', val)
|
||||
|
||||
if attr == None : break
|
||||
|
||||
if (attr == 'display') or (attr == 'pos') or (attr == 'align'):
|
||||
|
||||
if (attr == b'display') or (attr == b'pos') or (attr == b'align'):
|
||||
# handle text based attributess
|
||||
attr = attr + '-' + val
|
||||
attr = attr + b'-' + val
|
||||
if attr in self.attr_str_map :
|
||||
cssargs[attr] = (self.attr_str_map[attr], '')
|
||||
cssargs[attr] = (self.attr_str_map[attr], b'')
|
||||
else :
|
||||
# handle value based attributes
|
||||
if attr in self.attr_val_map :
|
||||
name = self.attr_val_map[attr]
|
||||
if attr in ('margin-bottom', 'margin-top', 'space-after') :
|
||||
if attr in (b'margin-bottom', b'margin-top', b'space-after') :
|
||||
scale = self.ph
|
||||
elif attr in ('margin-right', 'indent', 'margin-left', 'hang') :
|
||||
elif attr in (b'margin-right', b'indent', b'margin-left', b'hang') :
|
||||
scale = self.pw
|
||||
elif attr == 'line-space':
|
||||
elif attr == b'line-space':
|
||||
scale = self.fontsize * 2.0
|
||||
else:
|
||||
print("Scale not defined!")
|
||||
scale = 1.0
|
||||
|
||||
if not ((attr == 'hang') and (int(val) == 0)) :
|
||||
if val == "":
|
||||
val = 0
|
||||
|
||||
if not ((attr == b'hang') and (int(val) == 0)):
|
||||
try:
|
||||
f = float(val)
|
||||
except:
|
||||
print("Warning: unrecognised val, ignoring")
|
||||
val = 0
|
||||
pv = float(val)/scale
|
||||
cssargs[attr] = (self.attr_val_map[attr], pv)
|
||||
keep = True
|
||||
@@ -160,34 +200,35 @@ class DocParser(object):
|
||||
if aftclass != "" : keep = False
|
||||
|
||||
if keep :
|
||||
# make sure line-space does not go below 100% or above 300% since
|
||||
if debug: print('keeping style')
|
||||
# make sure line-space does not go below 100% or above 300% since
|
||||
# it can be wacky in some styles
|
||||
if 'line-space' in cssargs:
|
||||
seg = cssargs['line-space'][0]
|
||||
val = cssargs['line-space'][1]
|
||||
if b'line-space' in cssargs:
|
||||
seg = cssargs[b'line-space'][0]
|
||||
val = cssargs[b'line-space'][1]
|
||||
if val < 1.0: val = 1.0
|
||||
if val > 3.0: val = 3.0
|
||||
del cssargs['line-space']
|
||||
cssargs['line-space'] = (self.attr_val_map['line-space'], val)
|
||||
del cssargs[b'line-space']
|
||||
cssargs[b'line-space'] = (self.attr_val_map[b'line-space'], val)
|
||||
|
||||
|
||||
|
||||
# handle modifications for css style hanging indents
|
||||
if 'hang' in cssargs:
|
||||
hseg = cssargs['hang'][0]
|
||||
hval = cssargs['hang'][1]
|
||||
del cssargs['hang']
|
||||
cssargs['hang'] = (self.attr_val_map['hang'], -hval)
|
||||
if b'hang' in cssargs:
|
||||
hseg = cssargs[b'hang'][0]
|
||||
hval = cssargs[b'hang'][1]
|
||||
del cssargs[b'hang']
|
||||
cssargs[b'hang'] = (self.attr_val_map[b'hang'], -hval)
|
||||
mval = 0
|
||||
mseg = 'margin-left: '
|
||||
mval = hval
|
||||
if 'margin-left' in cssargs:
|
||||
mseg = cssargs['margin-left'][0]
|
||||
mval = cssargs['margin-left'][1]
|
||||
if b'margin-left' in cssargs:
|
||||
mseg = cssargs[b'margin-left'][0]
|
||||
mval = cssargs[b'margin-left'][1]
|
||||
if mval < 0: mval = 0
|
||||
mval = hval + mval
|
||||
cssargs['margin-left'] = (mseg, mval)
|
||||
if 'indent' in cssargs:
|
||||
del cssargs['indent']
|
||||
cssargs[b'margin-left'] = (mseg, mval)
|
||||
if b'indent' in cssargs:
|
||||
del cssargs[b'indent']
|
||||
|
||||
cssline = sclass + ' { '
|
||||
for key in iter(cssargs):
|
||||
@@ -203,7 +244,7 @@ class DocParser(object):
|
||||
|
||||
if sclass != '' :
|
||||
classlst += sclass + '\n'
|
||||
|
||||
|
||||
# handle special case of paragraph class used inside chapter heading
|
||||
# and non-chapter headings
|
||||
if sclass != '' :
|
||||
@@ -224,20 +265,26 @@ class DocParser(object):
|
||||
if cssline != ' { }':
|
||||
csspage += self.stags[tag] + cssline + '\n'
|
||||
|
||||
|
||||
|
||||
return csspage, classlst
|
||||
|
||||
|
||||
|
||||
def convert2CSS(flatxml, fontsize, ph, pw):
|
||||
|
||||
print ' ', 'Using font size:',fontsize
|
||||
print ' ', 'Using page height:', ph
|
||||
print ' ', 'Using page width:', pw
|
||||
print(' ', 'Using font size:',fontsize)
|
||||
print(' ', 'Using page height:', ph)
|
||||
print(' ', 'Using page width:', pw)
|
||||
|
||||
# create a document parser
|
||||
dp = DocParser(flatxml, fontsize, ph, pw)
|
||||
|
||||
if debug: print(' ', 'Created DocParser.')
|
||||
csspage = dp.process()
|
||||
|
||||
if debug: print(' ', 'Processed DocParser.')
|
||||
return csspage
|
||||
|
||||
|
||||
def getpageIDMap(flatxml):
|
||||
dp = DocParser(flatxml, 0, 0, 0)
|
||||
pageidnumbers = dp.getData('info.original.pid', 0, -1, True)
|
||||
return pageidnumbers
|
||||
@@ -52,7 +52,7 @@ class Process(object):
|
||||
self.__stdout_thread = threading.Thread(
|
||||
name="stdout-thread",
|
||||
target=self.__reader, args=(self.__collected_outdata,
|
||||
self.__process.stdout))
|
||||
self.__process.stdout))
|
||||
self.__stdout_thread.setDaemon(True)
|
||||
self.__stdout_thread.start()
|
||||
|
||||
@@ -60,7 +60,7 @@ class Process(object):
|
||||
self.__stderr_thread = threading.Thread(
|
||||
name="stderr-thread",
|
||||
target=self.__reader, args=(self.__collected_errdata,
|
||||
self.__process.stderr))
|
||||
self.__process.stderr))
|
||||
self.__stderr_thread.setDaemon(True)
|
||||
self.__stderr_thread.start()
|
||||
|
||||
@@ -146,4 +146,3 @@ class Process(object):
|
||||
self.__quit = True
|
||||
self.__inputsem.release()
|
||||
self.__lock.release()
|
||||
|
||||
544
DeDRM_plugin/topazextract.py
Normal file
544
DeDRM_plugin/topazextract.py
Normal file
@@ -0,0 +1,544 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# topazextract.py
|
||||
# Mostly written by some_updates based on code from many others
|
||||
|
||||
# Changelog
|
||||
# 4.9 - moved unicode_argv call inside main for Windows DeDRM compatibility
|
||||
# 5.0 - Fixed potential unicode problem with command line interface
|
||||
# 6.0 - Added Python 3 compatibility for calibre 5.0
|
||||
|
||||
__version__ = '6.0'
|
||||
|
||||
import sys
|
||||
import os, csv, getopt
|
||||
import zlib, zipfile, tempfile, shutil
|
||||
import traceback
|
||||
from struct import pack
|
||||
from struct import unpack
|
||||
try:
|
||||
from calibre_plugins.dedrm.alfcrypto import Topaz_Cipher
|
||||
except:
|
||||
from alfcrypto import Topaz_Cipher
|
||||
|
||||
# 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):
|
||||
data = data.encode(self.encoding,"replace")
|
||||
self.stream.buffer.write(data)
|
||||
self.stream.buffer.flush()
|
||||
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
iswindows = sys.platform.startswith('win')
|
||||
isosx = sys.platform.startswith('darwin')
|
||||
|
||||
def unicode_argv():
|
||||
if iswindows:
|
||||
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
||||
# strings.
|
||||
|
||||
# Versions 2.x of Python don't support Unicode in sys.argv on
|
||||
# Windows, with the underlying Windows API instead replacing multi-byte
|
||||
# characters with '?'.
|
||||
|
||||
|
||||
from ctypes import POINTER, byref, cdll, c_int, windll
|
||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
||||
|
||||
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
||||
GetCommandLineW.argtypes = []
|
||||
GetCommandLineW.restype = LPCWSTR
|
||||
|
||||
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
||||
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
||||
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
||||
|
||||
cmd = GetCommandLineW()
|
||||
argc = c_int(0)
|
||||
argv = CommandLineToArgvW(cmd, byref(argc))
|
||||
if argc.value > 0:
|
||||
# Remove Python executable and commands if present
|
||||
start = argc.value - len(sys.argv)
|
||||
return [argv[i] for i in
|
||||
range(start, argc.value)]
|
||||
# if we don't have any arguments at all, just pass back script name
|
||||
# this should never happen
|
||||
return ["mobidedrm.py"]
|
||||
else:
|
||||
argvencoding = sys.stdin.encoding or "utf-8"
|
||||
return [arg if isinstance(arg, str) else str(arg, argvencoding) for arg in sys.argv]
|
||||
|
||||
#global switch
|
||||
debug = False
|
||||
|
||||
if 'calibre' in sys.modules:
|
||||
inCalibre = True
|
||||
from calibre_plugins.dedrm import kgenpids
|
||||
else:
|
||||
inCalibre = False
|
||||
import kgenpids
|
||||
|
||||
|
||||
class DrmException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
# recursive zip creation support routine
|
||||
def zipUpDir(myzip, tdir, localname):
|
||||
currentdir = tdir
|
||||
if localname != "":
|
||||
currentdir = os.path.join(currentdir,localname)
|
||||
list = os.listdir(currentdir)
|
||||
for file in list:
|
||||
afilename = file
|
||||
localfilePath = os.path.join(localname, afilename)
|
||||
realfilePath = os.path.join(currentdir,file)
|
||||
if os.path.isfile(realfilePath):
|
||||
myzip.write(realfilePath, localfilePath)
|
||||
elif os.path.isdir(realfilePath):
|
||||
zipUpDir(myzip, tdir, localfilePath)
|
||||
|
||||
#
|
||||
# Utility routines
|
||||
#
|
||||
|
||||
# Get a 7 bit encoded number from file
|
||||
def bookReadEncodedNumber(fo):
|
||||
flag = False
|
||||
data = ord(fo.read(1))
|
||||
if data == 0xFF:
|
||||
flag = True
|
||||
data = ord(fo.read(1))
|
||||
if data >= 0x80:
|
||||
datax = (data & 0x7F)
|
||||
while data >= 0x80 :
|
||||
data = ord(fo.read(1))
|
||||
datax = (datax <<7) + (data & 0x7F)
|
||||
data = datax
|
||||
if flag:
|
||||
data = -data
|
||||
return data
|
||||
|
||||
# Get a length prefixed string from file
|
||||
def bookReadString(fo):
|
||||
stringLength = bookReadEncodedNumber(fo)
|
||||
return unpack(str(stringLength)+'s',fo.read(stringLength))[0]
|
||||
|
||||
#
|
||||
# crypto routines
|
||||
#
|
||||
|
||||
# Context initialisation for the Topaz Crypto
|
||||
def topazCryptoInit(key):
|
||||
return Topaz_Cipher().ctx_init(key)
|
||||
|
||||
# ctx1 = 0x0CAFFE19E
|
||||
# for keyChar in key:
|
||||
# keyByte = ord(keyChar)
|
||||
# ctx2 = ctx1
|
||||
# ctx1 = ((((ctx1 >>2) * (ctx1 >>7))&0xFFFFFFFF) ^ (keyByte * keyByte * 0x0F902007)& 0xFFFFFFFF )
|
||||
# return [ctx1,ctx2]
|
||||
|
||||
# decrypt data with the context prepared by topazCryptoInit()
|
||||
def topazCryptoDecrypt(data, ctx):
|
||||
return Topaz_Cipher().decrypt(data, ctx)
|
||||
# ctx1 = ctx[0]
|
||||
# ctx2 = ctx[1]
|
||||
# plainText = ""
|
||||
# for dataChar in data:
|
||||
# dataByte = ord(dataChar)
|
||||
# m = (dataByte ^ ((ctx1 >> 3) &0xFF) ^ ((ctx2<<3) & 0xFF)) &0xFF
|
||||
# ctx2 = ctx1
|
||||
# ctx1 = (((ctx1 >> 2) * (ctx1 >> 7)) &0xFFFFFFFF) ^((m * m * 0x0F902007) &0xFFFFFFFF)
|
||||
# plainText += chr(m)
|
||||
# return plainText
|
||||
|
||||
# Decrypt data with the PID
|
||||
def decryptRecord(data,PID):
|
||||
ctx = topazCryptoInit(PID)
|
||||
return topazCryptoDecrypt(data, ctx)
|
||||
|
||||
# Try to decrypt a dkey record (contains the bookPID)
|
||||
def decryptDkeyRecord(data,PID):
|
||||
record = decryptRecord(data,PID)
|
||||
fields = unpack('3sB8sB8s3s',record)
|
||||
if fields[0] != b'PID' or fields[5] != b'pid' :
|
||||
raise DrmException("Didn't find PID magic numbers in record")
|
||||
elif fields[1] != 8 or fields[3] != 8 :
|
||||
raise DrmException("Record didn't contain correct length fields")
|
||||
elif fields[2] != PID :
|
||||
raise DrmException("Record didn't contain PID")
|
||||
return fields[4]
|
||||
|
||||
# Decrypt all dkey records (contain the book PID)
|
||||
def decryptDkeyRecords(data,PID):
|
||||
nbKeyRecords = data[0]
|
||||
records = []
|
||||
data = data[1:]
|
||||
for i in range (0,nbKeyRecords):
|
||||
length = data[0]
|
||||
try:
|
||||
key = decryptDkeyRecord(data[1:length+1],PID)
|
||||
records.append(key)
|
||||
except DrmException:
|
||||
pass
|
||||
data = data[1+length:]
|
||||
if len(records) == 0:
|
||||
raise DrmException("BookKey Not Found")
|
||||
return records
|
||||
|
||||
|
||||
class TopazBook:
|
||||
def __init__(self, filename):
|
||||
self.fo = open(filename, 'rb')
|
||||
self.outdir = tempfile.mkdtemp()
|
||||
# self.outdir = 'rawdat'
|
||||
self.bookPayloadOffset = 0
|
||||
self.bookHeaderRecords = {}
|
||||
self.bookMetadata = {}
|
||||
self.bookKey = None
|
||||
magic = unpack('4s',self.fo.read(4))[0]
|
||||
if magic != b'TPZ0':
|
||||
raise DrmException("Parse Error : Invalid Header, not a Topaz file")
|
||||
self.parseTopazHeaders()
|
||||
self.parseMetadata()
|
||||
|
||||
def parseTopazHeaders(self):
|
||||
def bookReadHeaderRecordData():
|
||||
# Read and return the data of one header record at the current book file position
|
||||
# [[offset,decompressedLength,compressedLength],...]
|
||||
nbValues = bookReadEncodedNumber(self.fo)
|
||||
if debug: print("%d records in header " % nbValues, end=' ')
|
||||
values = []
|
||||
for i in range (0,nbValues):
|
||||
values.append([bookReadEncodedNumber(self.fo),bookReadEncodedNumber(self.fo),bookReadEncodedNumber(self.fo)])
|
||||
return values
|
||||
def parseTopazHeaderRecord():
|
||||
# Read and parse one header record at the current book file position and return the associated data
|
||||
# [[offset,decompressedLength,compressedLength],...]
|
||||
if ord(self.fo.read(1)) != 0x63:
|
||||
raise DrmException("Parse Error : Invalid Header")
|
||||
tag = bookReadString(self.fo)
|
||||
record = bookReadHeaderRecordData()
|
||||
return [tag,record]
|
||||
nbRecords = bookReadEncodedNumber(self.fo)
|
||||
if debug: print("Headers: %d" % nbRecords)
|
||||
for i in range (0,nbRecords):
|
||||
result = parseTopazHeaderRecord()
|
||||
if debug: print(result[0], ": ", result[1])
|
||||
self.bookHeaderRecords[result[0]] = result[1]
|
||||
if ord(self.fo.read(1)) != 0x64 :
|
||||
raise DrmException("Parse Error : Invalid Header")
|
||||
self.bookPayloadOffset = self.fo.tell()
|
||||
|
||||
def parseMetadata(self):
|
||||
# Parse the metadata record from the book payload and return a list of [key,values]
|
||||
self.fo.seek(self.bookPayloadOffset + self.bookHeaderRecords[b'metadata'][0][0])
|
||||
tag = bookReadString(self.fo)
|
||||
if tag != b'metadata' :
|
||||
raise DrmException("Parse Error : Record Names Don't Match")
|
||||
flags = ord(self.fo.read(1))
|
||||
nbRecords = ord(self.fo.read(1))
|
||||
if debug: print("Metadata Records: %d" % nbRecords)
|
||||
for i in range (0,nbRecords) :
|
||||
keyval = bookReadString(self.fo)
|
||||
content = bookReadString(self.fo)
|
||||
if debug: print(keyval)
|
||||
if debug: print(content)
|
||||
self.bookMetadata[keyval] = content
|
||||
return self.bookMetadata
|
||||
|
||||
def getPIDMetaInfo(self):
|
||||
keysRecord = self.bookMetadata.get(b'keys',b'')
|
||||
keysRecordRecord = b''
|
||||
if keysRecord != b'':
|
||||
keylst = keysRecord.split(b',')
|
||||
for keyval in keylst:
|
||||
keysRecordRecord += self.bookMetadata.get(keyval,b'')
|
||||
return keysRecord, keysRecordRecord
|
||||
|
||||
def getBookTitle(self):
|
||||
title = b''
|
||||
if b'Title' in self.bookMetadata:
|
||||
title = self.bookMetadata[b'Title']
|
||||
return title.decode('utf-8')
|
||||
|
||||
def setBookKey(self, key):
|
||||
self.bookKey = key
|
||||
|
||||
def getBookPayloadRecord(self, name, index):
|
||||
# Get a record in the book payload, given its name and index.
|
||||
# decrypted and decompressed if necessary
|
||||
encrypted = False
|
||||
compressed = False
|
||||
try:
|
||||
recordOffset = self.bookHeaderRecords[name][index][0]
|
||||
except:
|
||||
raise DrmException("Parse Error : Invalid Record, record not found")
|
||||
|
||||
self.fo.seek(self.bookPayloadOffset + recordOffset)
|
||||
|
||||
tag = bookReadString(self.fo)
|
||||
if tag != name :
|
||||
raise DrmException("Parse Error : Invalid Record, record name doesn't match")
|
||||
|
||||
recordIndex = bookReadEncodedNumber(self.fo)
|
||||
if recordIndex < 0 :
|
||||
encrypted = True
|
||||
recordIndex = -recordIndex -1
|
||||
|
||||
if recordIndex != index :
|
||||
raise DrmException("Parse Error : Invalid Record, index doesn't match")
|
||||
|
||||
if (self.bookHeaderRecords[name][index][2] > 0):
|
||||
compressed = True
|
||||
record = self.fo.read(self.bookHeaderRecords[name][index][2])
|
||||
else:
|
||||
record = self.fo.read(self.bookHeaderRecords[name][index][1])
|
||||
|
||||
if encrypted:
|
||||
if self.bookKey:
|
||||
ctx = topazCryptoInit(self.bookKey)
|
||||
record = topazCryptoDecrypt(record,ctx)
|
||||
else :
|
||||
raise DrmException("Error: Attempt to decrypt without bookKey")
|
||||
|
||||
if compressed:
|
||||
record = zlib.decompress(record)
|
||||
|
||||
return record
|
||||
|
||||
def processBook(self, pidlst):
|
||||
raw = 0
|
||||
fixedimage=True
|
||||
try:
|
||||
keydata = self.getBookPayloadRecord(b'dkey', 0)
|
||||
except DrmException as e:
|
||||
print("no dkey record found, book may not be encrypted")
|
||||
print("attempting to extrct files without a book key")
|
||||
self.createBookDirectory()
|
||||
self.extractFiles()
|
||||
print("Successfully Extracted Topaz contents")
|
||||
if inCalibre:
|
||||
from calibre_plugins.dedrm import genbook
|
||||
else:
|
||||
import genbook
|
||||
|
||||
rv = genbook.generateBook(self.outdir, raw, fixedimage)
|
||||
if rv == 0:
|
||||
print("Book Successfully generated.")
|
||||
return rv
|
||||
|
||||
# try each pid to decode the file
|
||||
bookKey = None
|
||||
for pid in pidlst:
|
||||
# use 8 digit pids here
|
||||
pid = pid[0:8]
|
||||
print("Trying: {0}".format(pid))
|
||||
bookKeys = []
|
||||
data = keydata
|
||||
try:
|
||||
bookKeys+=decryptDkeyRecords(data,pid)
|
||||
except DrmException as e:
|
||||
pass
|
||||
else:
|
||||
bookKey = bookKeys[0]
|
||||
print("Book Key Found! ({0})".format(bookKey.hex()))
|
||||
break
|
||||
|
||||
if not bookKey:
|
||||
raise DrmException("No key found in {0:d} keys tried. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(len(pidlst)))
|
||||
|
||||
self.setBookKey(bookKey)
|
||||
self.createBookDirectory()
|
||||
self.extractFiles()
|
||||
print("Successfully Extracted Topaz contents")
|
||||
if inCalibre:
|
||||
from calibre_plugins.dedrm import genbook
|
||||
else:
|
||||
import genbook
|
||||
|
||||
rv = genbook.generateBook(self.outdir, raw, fixedimage)
|
||||
if rv == 0:
|
||||
print("Book Successfully generated")
|
||||
return rv
|
||||
|
||||
def createBookDirectory(self):
|
||||
outdir = self.outdir
|
||||
# create output directory structure
|
||||
if not os.path.exists(outdir):
|
||||
os.makedirs(outdir)
|
||||
destdir = os.path.join(outdir,"img")
|
||||
if not os.path.exists(destdir):
|
||||
os.makedirs(destdir)
|
||||
destdir = os.path.join(outdir,"color_img")
|
||||
if not os.path.exists(destdir):
|
||||
os.makedirs(destdir)
|
||||
destdir = os.path.join(outdir,"page")
|
||||
if not os.path.exists(destdir):
|
||||
os.makedirs(destdir)
|
||||
destdir = os.path.join(outdir,"glyphs")
|
||||
if not os.path.exists(destdir):
|
||||
os.makedirs(destdir)
|
||||
|
||||
def extractFiles(self):
|
||||
outdir = self.outdir
|
||||
for headerRecord in self.bookHeaderRecords:
|
||||
name = headerRecord
|
||||
if name != b'dkey':
|
||||
ext = ".dat"
|
||||
if name == b'img': ext = ".jpg"
|
||||
if name == b'color' : ext = ".jpg"
|
||||
print("Processing Section: {0}\n. . .".format(name.decode('utf-8')), end=' ')
|
||||
for index in range (0,len(self.bookHeaderRecords[name])) :
|
||||
fname = "{0}{1:04d}{2}".format(name.decode('utf-8'),index,ext)
|
||||
destdir = outdir
|
||||
if name == b'img':
|
||||
destdir = os.path.join(outdir,"img")
|
||||
if name == b'color':
|
||||
destdir = os.path.join(outdir,"color_img")
|
||||
if name == b'page':
|
||||
destdir = os.path.join(outdir,"page")
|
||||
if name == b'glyphs':
|
||||
destdir = os.path.join(outdir,"glyphs")
|
||||
outputFile = os.path.join(destdir,fname)
|
||||
print(".", end=' ')
|
||||
record = self.getBookPayloadRecord(name,index)
|
||||
if record != b'':
|
||||
open(outputFile, 'wb').write(record)
|
||||
print(" ")
|
||||
|
||||
def getFile(self, zipname):
|
||||
htmlzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||
htmlzip.write(os.path.join(self.outdir,"book.html"),"book.html")
|
||||
htmlzip.write(os.path.join(self.outdir,"book.opf"),"book.opf")
|
||||
if os.path.isfile(os.path.join(self.outdir,"cover.jpg")):
|
||||
htmlzip.write(os.path.join(self.outdir,"cover.jpg"),"cover.jpg")
|
||||
htmlzip.write(os.path.join(self.outdir,"style.css"),"style.css")
|
||||
zipUpDir(htmlzip, self.outdir, "img")
|
||||
htmlzip.close()
|
||||
|
||||
def getBookType(self):
|
||||
return "Topaz"
|
||||
|
||||
def getBookExtension(self):
|
||||
return ".htmlz"
|
||||
|
||||
def getSVGZip(self, zipname):
|
||||
svgzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||
svgzip.write(os.path.join(self.outdir,"index_svg.xhtml"),"index_svg.xhtml")
|
||||
zipUpDir(svgzip, self.outdir, "svg")
|
||||
zipUpDir(svgzip, self.outdir, "img")
|
||||
svgzip.close()
|
||||
|
||||
def cleanup(self):
|
||||
if os.path.isdir(self.outdir):
|
||||
shutil.rmtree(self.outdir, True)
|
||||
|
||||
def usage(progname):
|
||||
print("Removes DRM protection from Topaz ebooks and extracts the contents")
|
||||
print("Usage:")
|
||||
print(" {0} [-k <kindle.k4i>] [-p <comma separated PIDs>] [-s <comma separated Kindle serial numbers>] <infile> <outdir>".format(progname))
|
||||
|
||||
# Main
|
||||
def cli_main():
|
||||
argv=unicode_argv()
|
||||
progname = os.path.basename(argv[0])
|
||||
print("TopazExtract v{0}.".format(__version__))
|
||||
|
||||
try:
|
||||
opts, args = getopt.getopt(argv[1:], "k:p:s:x")
|
||||
except getopt.GetoptError as err:
|
||||
print("Error in options or arguments: {0}".format(err.args[0]))
|
||||
usage(progname)
|
||||
return 1
|
||||
if len(args)<2:
|
||||
usage(progname)
|
||||
return 1
|
||||
|
||||
infile = args[0]
|
||||
outdir = args[1]
|
||||
if not os.path.isfile(infile):
|
||||
print("Input File {0} Does Not Exist.".format(infile))
|
||||
return 1
|
||||
|
||||
if not os.path.exists(outdir):
|
||||
print("Output Directory {0} Does Not Exist.".format(outdir))
|
||||
return 1
|
||||
|
||||
kDatabaseFiles = []
|
||||
serials = []
|
||||
pids = []
|
||||
|
||||
for o, a in opts:
|
||||
if o == '-k':
|
||||
if a == None :
|
||||
raise DrmException("Invalid parameter for -k")
|
||||
kDatabaseFiles.append(a)
|
||||
if o == '-p':
|
||||
if a == None :
|
||||
raise DrmException("Invalid parameter for -p")
|
||||
pids = a.split(',')
|
||||
if o == '-s':
|
||||
if a == None :
|
||||
raise DrmException("Invalid parameter for -s")
|
||||
serials = [serial.replace(" ","") for serial in a.split(',')]
|
||||
|
||||
bookname = os.path.splitext(os.path.basename(infile))[0]
|
||||
|
||||
tb = TopazBook(infile)
|
||||
title = tb.getBookTitle()
|
||||
print("Processing Book: {0}".format(title))
|
||||
md1, md2 = tb.getPIDMetaInfo()
|
||||
pids.extend(kgenpids.getPidList(md1, md2, serials, kDatabaseFiles))
|
||||
|
||||
try:
|
||||
print("Decrypting Book")
|
||||
tb.processBook(pids)
|
||||
|
||||
print(" Creating HTML ZIP Archive")
|
||||
zipname = os.path.join(outdir, bookname + "_nodrm.htmlz")
|
||||
tb.getFile(zipname)
|
||||
|
||||
print(" Creating SVG ZIP Archive")
|
||||
zipname = os.path.join(outdir, bookname + "_SVG.zip")
|
||||
tb.getSVGZip(zipname)
|
||||
|
||||
# removing internal temporary directory of pieces
|
||||
tb.cleanup()
|
||||
|
||||
except DrmException as e:
|
||||
print("Decryption failed\n{0}".format(traceback.format_exc()))
|
||||
|
||||
try:
|
||||
tb.cleanup()
|
||||
except:
|
||||
pass
|
||||
return 1
|
||||
|
||||
except Exception as e:
|
||||
print("Decryption failed\n{0}".format(traceback.format_exc()))
|
||||
try:
|
||||
tb.cleanup()
|
||||
except:
|
||||
pass
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
sys.exit(cli_main())
|
||||
39
DeDRM_plugin/utilities.py
Normal file
39
DeDRM_plugin/utilities.py
Normal file
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from calibre_plugins.dedrm.ignoblekeygen import generate_key
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
|
||||
DETAILED_MESSAGE = \
|
||||
'You have personal information stored in this plugin\'s customization '+ \
|
||||
'string from a previous version of this plugin.\n\n'+ \
|
||||
'This new version of the plugin can convert that info '+ \
|
||||
'into key data that the new plugin can then use (which doesn\'t '+ \
|
||||
'require personal information to be stored/displayed in an insecure '+ \
|
||||
'manner like the old plugin did).\n\nIf you choose NOT to migrate this data at this time '+ \
|
||||
'you will be prompted to save that personal data to a file elsewhere; and you\'ll have '+ \
|
||||
'to manually re-configure this plugin with your information.\n\nEither way... ' + \
|
||||
'this new version of the plugin will not be responsible for storing that personal '+ \
|
||||
'info in plain sight any longer.'
|
||||
|
||||
def uStrCmp (s1, s2, caseless=False):
|
||||
import unicodedata as ud
|
||||
str1 = s1 if isinstance(s1, str) else str(s1)
|
||||
str2 = s2 if isinstance(s2, str) else str(s2)
|
||||
if caseless:
|
||||
return ud.normalize('NFC', str1.lower()) == ud.normalize('NFC', str2.lower())
|
||||
else:
|
||||
return ud.normalize('NFC', str1) == ud.normalize('NFC', str2)
|
||||
|
||||
def parseCustString(keystuff):
|
||||
userkeys = []
|
||||
ar = keystuff.split(':')
|
||||
for i in ar:
|
||||
try:
|
||||
name, ccn = i.split(',')
|
||||
# Generate Barnes & Noble EPUB user key from name and credit card number.
|
||||
userkeys.append(generate_key(name, ccn))
|
||||
except:
|
||||
pass
|
||||
return userkeys
|
||||
112
DeDRM_plugin/wineutils.py
Normal file
112
DeDRM_plugin/wineutils.py
Normal file
@@ -0,0 +1,112 @@
|
||||
#!/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 = []
|
||||
# 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)
|
||||
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
|
||||
1394
DeDRM_plugin/zipfilerugged.py
Executable file
1394
DeDRM_plugin/zipfilerugged.py
Executable file
File diff suppressed because it is too large
Load Diff
193
DeDRM_plugin/zipfix.py
Normal file
193
DeDRM_plugin/zipfix.py
Normal file
@@ -0,0 +1,193 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# zipfix.py
|
||||
# Copyright © 2010-2020 by Apprentice Harper et al.
|
||||
|
||||
# Released under the terms of the GNU General Public Licence, version 3
|
||||
# <http://www.gnu.org/licenses/>
|
||||
|
||||
# Revision history:
|
||||
# 1.0 - Initial release
|
||||
# 1.1 - Updated to handle zip file metadata correctly
|
||||
# 2.0 - Python 3 for calibre 5.0
|
||||
|
||||
"""
|
||||
Re-write zip (or ePub) fixing problems with file names (and mimetype entry).
|
||||
"""
|
||||
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = "1.1"
|
||||
|
||||
import sys
|
||||
import zlib
|
||||
try:
|
||||
import zipfilerugged
|
||||
except:
|
||||
import calibre_plugins.dedrm.zipfilerugged as zipfilerugged
|
||||
import os
|
||||
import os.path
|
||||
import getopt
|
||||
from struct import unpack
|
||||
|
||||
|
||||
_FILENAME_LEN_OFFSET = 26
|
||||
_EXTRA_LEN_OFFSET = 28
|
||||
_FILENAME_OFFSET = 30
|
||||
_MAX_SIZE = 64 * 1024
|
||||
_MIMETYPE = 'application/epub+zip'
|
||||
|
||||
class ZipInfo(zipfilerugged.ZipInfo):
|
||||
def __init__(self, *args, **kwargs):
|
||||
if 'compress_type' in kwargs:
|
||||
compress_type = kwargs.pop('compress_type')
|
||||
super(ZipInfo, self).__init__(*args, **kwargs)
|
||||
self.compress_type = compress_type
|
||||
|
||||
class fixZip:
|
||||
def __init__(self, zinput, zoutput):
|
||||
self.ztype = 'zip'
|
||||
if zinput.lower().find('.epub') >= 0 :
|
||||
self.ztype = 'epub'
|
||||
self.inzip = zipfilerugged.ZipFile(zinput,'r')
|
||||
self.outzip = zipfilerugged.ZipFile(zoutput,'w')
|
||||
# open the input zip for reading only as a raw file
|
||||
self.bzf = open(zinput,'rb')
|
||||
|
||||
def getlocalname(self, zi):
|
||||
local_header_offset = zi.header_offset
|
||||
self.bzf.seek(local_header_offset + _FILENAME_LEN_OFFSET)
|
||||
leninfo = self.bzf.read(2)
|
||||
local_name_length, = unpack('<H', leninfo)
|
||||
self.bzf.seek(local_header_offset + _FILENAME_OFFSET)
|
||||
local_name = self.bzf.read(local_name_length)
|
||||
return local_name
|
||||
|
||||
def uncompress(self, cmpdata):
|
||||
dc = zlib.decompressobj(-15)
|
||||
data = b''
|
||||
while len(cmpdata) > 0:
|
||||
if len(cmpdata) > _MAX_SIZE :
|
||||
newdata = cmpdata[0:_MAX_SIZE]
|
||||
cmpdata = cmpdata[_MAX_SIZE:]
|
||||
else:
|
||||
newdata = cmpdata
|
||||
cmpdata = b''
|
||||
newdata = dc.decompress(newdata)
|
||||
unprocessed = dc.unconsumed_tail
|
||||
if len(unprocessed) == 0:
|
||||
newdata += dc.flush()
|
||||
data += newdata
|
||||
cmpdata += unprocessed
|
||||
unprocessed = b''
|
||||
return data
|
||||
|
||||
def getfiledata(self, zi):
|
||||
# get file name length and exta data length to find start of file data
|
||||
local_header_offset = zi.header_offset
|
||||
|
||||
self.bzf.seek(local_header_offset + _FILENAME_LEN_OFFSET)
|
||||
leninfo = self.bzf.read(2)
|
||||
local_name_length, = unpack('<H', leninfo)
|
||||
|
||||
self.bzf.seek(local_header_offset + _EXTRA_LEN_OFFSET)
|
||||
exinfo = self.bzf.read(2)
|
||||
extra_field_length, = unpack('<H', exinfo)
|
||||
|
||||
self.bzf.seek(local_header_offset + _FILENAME_OFFSET + local_name_length + extra_field_length)
|
||||
data = None
|
||||
|
||||
# if not compressed we are good to go
|
||||
if zi.compress_type == zipfilerugged.ZIP_STORED:
|
||||
data = self.bzf.read(zi.file_size)
|
||||
|
||||
# if compressed we must decompress it using zlib
|
||||
if zi.compress_type == zipfilerugged.ZIP_DEFLATED:
|
||||
cmpdata = self.bzf.read(zi.compress_size)
|
||||
data = self.uncompress(cmpdata)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
|
||||
def fix(self):
|
||||
# get the zipinfo for each member of the input archive
|
||||
# and copy member over to output archive
|
||||
# if problems exist with local vs central filename, fix them
|
||||
|
||||
# if epub write mimetype file first, with no compression
|
||||
if self.ztype == 'epub':
|
||||
# first get a ZipInfo with current time and no compression
|
||||
mimeinfo = ZipInfo(b'mimetype',compress_type=zipfilerugged.ZIP_STORED)
|
||||
mimeinfo.internal_attr = 1 # text file
|
||||
try:
|
||||
# if the mimetype is present, get its info, including time-stamp
|
||||
oldmimeinfo = self.inzip.getinfo(b'mimetype')
|
||||
# copy across useful fields
|
||||
mimeinfo.date_time = oldmimeinfo.date_time
|
||||
mimeinfo.comment = oldmimeinfo.comment
|
||||
mimeinfo.extra = oldmimeinfo.extra
|
||||
mimeinfo.internal_attr = oldmimeinfo.internal_attr
|
||||
mimeinfo.external_attr = oldmimeinfo.external_attr
|
||||
mimeinfo.create_system = oldmimeinfo.create_system
|
||||
except:
|
||||
pass
|
||||
self.outzip.writestr(mimeinfo, _MIMETYPE.encode('ascii'))
|
||||
|
||||
# write the rest of the files
|
||||
for zinfo in self.inzip.infolist():
|
||||
if zinfo.filename != b"mimetype" or self.ztype != 'epub':
|
||||
data = None
|
||||
try:
|
||||
data = self.inzip.read(zinfo.filename)
|
||||
except zipfilerugged.BadZipfile or zipfilerugged.error:
|
||||
local_name = self.getlocalname(zinfo)
|
||||
data = self.getfiledata(zinfo)
|
||||
zinfo.filename = local_name
|
||||
|
||||
# create new ZipInfo with only the useful attributes from the old info
|
||||
nzinfo = ZipInfo(zinfo.filename, zinfo.date_time, compress_type=zinfo.compress_type)
|
||||
nzinfo.comment=zinfo.comment
|
||||
nzinfo.extra=zinfo.extra
|
||||
nzinfo.internal_attr=zinfo.internal_attr
|
||||
nzinfo.external_attr=zinfo.external_attr
|
||||
nzinfo.create_system=zinfo.create_system
|
||||
self.outzip.writestr(nzinfo,data)
|
||||
|
||||
self.bzf.close()
|
||||
self.inzip.close()
|
||||
self.outzip.close()
|
||||
|
||||
|
||||
def usage():
|
||||
print("""usage: zipfix.py inputzip outputzip
|
||||
inputzip is the source zipfile to fix
|
||||
outputzip is the fixed zip archive
|
||||
""")
|
||||
|
||||
|
||||
def repairBook(infile, outfile):
|
||||
if not os.path.exists(infile):
|
||||
print("Error: Input Zip File does not exist")
|
||||
return 1
|
||||
try:
|
||||
fr = fixZip(infile, outfile)
|
||||
fr.fix()
|
||||
return 0
|
||||
except Exception as e:
|
||||
print("Error Occurred ", e)
|
||||
return 2
|
||||
|
||||
|
||||
def main(argv=sys.argv):
|
||||
if len(argv)!=3:
|
||||
usage()
|
||||
return 1
|
||||
infile = argv[1]
|
||||
outfile = argv[2]
|
||||
return repairBook(infile, outfile)
|
||||
|
||||
|
||||
if __name__ == '__main__' :
|
||||
sys.exit(main())
|
||||
34
DeDRM_plugin_ReadMe.txt
Normal file
34
DeDRM_plugin_ReadMe.txt
Normal file
@@ -0,0 +1,34 @@
|
||||
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/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 comment at Apprentice Alf's blog, http://apprenticealf.wordpress.com/ or an issue at Apprentice Harper's repository, https://github.com/apprenticeharper/DeDRM_tools/issues . You should also give details of your computer, and how you obtained the ebook file.
|
||||
207
FAQs.md
Normal file
207
FAQs.md
Normal file
@@ -0,0 +1,207 @@
|
||||
# Overview
|
||||
## What's this repository all about?
|
||||
Providing free open source tools to remove DRM from your ebooks.
|
||||
|
||||
## What's DRM?
|
||||
DRM ("Digital Rights Management") is a way of using encryption to tie the books you've bought to a specific device or to a particular piece of software.
|
||||
|
||||
## Why would I want to remove DRM from my ebooks?
|
||||
When your ebooks have DRM you are unable to convert the ebook from one format to another (e.g. Kindle KF8 to Kobo ePub), so you are restricted in the range of ebook stores you can use. DRM also allows publishers to restrict what you can do with the ebook you've bought, e.g. preventing the use of text-to-speech software. Longer term, you can never be sure that you'll be able to come back and re-read your ebooks if they have DRM, even if you save back-up copies.
|
||||
|
||||
## So how can I remove DRM from my ebooks?
|
||||
Just download and use these tools, that's all! Uh, almost. There are a few, uh, provisos, a, a couple of quid pro quos.
|
||||
|
||||
* The tools don't work on all ebooks. For example, they don't work on any ebooks from Apple's iBooks store.
|
||||
* You must own the ebook - the tools won't work on library ebooks or rented ebooks or books from a friend.
|
||||
* You must not use these tools to give your ebooks to a hundred of your closest friends. Or to a million strangers. Authors need to sell books to be able to write more books. Don't be mean to the authors.
|
||||
* Do NOT use Adobe Digital Editions 3.0 or later to download your ePubs. ADE 3.0 and later might use a new encryption scheme that the tools can't handle. While major ebook stores aren't using the new scheme yet, using ADE 2.0.1 will ensure that your ebooks are downloaded using the old scheme. Once a book has been downloaded with the new scheme, it's IMPOSSIBLE to re-download using the old scheme (without buying it again).
|
||||
|
||||
But otherwise, if your ebook is from Amazon, Kobo, Barnes & Noble or any of the ebook stores selling ebooks compatible with Adobe Digital Editions 2.0.1, you should be able to remove the DRM that's been applied to your ebooks.
|
||||
|
||||
### Recent Changes to Kindle for PC/Kindle for Mac
|
||||
Starting with version 1.19, Kindle for PC/Mac uses Amazon's new KFX format which isn't quite as good a source for conversion to ePub as the older KF8 (& MOBI) formats. There are two options to get the older formats. Either stick with version 1.17 or earlier, or modify the executable by changing a file name (PC) or disabling a component of the application (Mac).
|
||||
|
||||
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.
|
||||
Verify the one of the following cryptographic hash values, using software of your choice, before installing the downloaded file in order to avoid viruses. If the hash does not match, delete the downloaded file and try again from another site.
|
||||
|
||||
#### Kindle for PC `KindleForPC-installer-1.17.44170.exe`:
|
||||
* MD-5: 53F793B562F4823721AA47D7DE099869
|
||||
* SHA-1: 73C404D719F0DD8D4AE1C2C96612B095D6C86255
|
||||
* SHA-256: 14E0F0053F1276C0C7C446892DC170344F707FBFE99B695176 2C120144163200
|
||||
|
||||
#### Kindle for Mac `KindleForMac-44182.dmg`:
|
||||
* 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 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
|
||||
`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`.
|
||||
|
||||
#### Macintosh
|
||||
`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` [...]
|
||||
|
||||
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
|
||||
Thanks to work by several people, the tools can now decrypt KFX format ebooks from Kindle for Mac/PC version 1.26 or earlier (version later than 1.26 use a new encryption scheme for KFX files). In addition to the DeDRM plugin, calibre users will also need to install jhowell's KFX Input plugin which is available through the standard plugin menu in calibre, or directly from [his plugin thread](https://www.mobileread.com/forums/showthread.php?t=291290) on Mobileread.
|
||||
|
||||
#### Thanks
|
||||
Thanks to jhowell for his investigations into KFX format and the KFX Input plugin. Some of these instructions are from [his thread on the subject](https://www.mobileread.com/forums/showthread.php?t=283371) at MobileRead.
|
||||
|
||||
## Where can I get the latest version of these free DRM removal tools?
|
||||
Right here at github. Just go to the [releases page](https://github.com/apprenticeharper/DeDRM_tools/releases) and download the latest zip archive of the tools, named `DeDRM\_tools\_X.X.X.zip`, where X.X.X is the version number. You do not need to download the source code archive.
|
||||
|
||||
## I've downloaded the tools archive. Now what?
|
||||
First, unzip the archive. You should now have a DeDRM folder containing several other folders and a `ReadMe_Overview.txt` file. Please read the `ReadMe_Overview.txt` file! That will explain what the folders are, and you'll be able to work out which of the tools you need.
|
||||
|
||||
## 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. Remember that the plugin only tries to remove DRM when ebooks are imported.
|
||||
|
||||
# Installing the Tools
|
||||
## The calibre plugin
|
||||
### I am trying to install the calibre plugin, but calibre says "ERROR: Unhandled exception: InvalidPlugin: The plugin in u’[path]DeDRM\_tools\_6.5.3.zip’ is invalid. It does not contain a top-level \_\_init\_\_.py file"
|
||||
You are trying to add the tools archive (e.g. `DeDRM_tools_6.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.
|
||||
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.
|
||||
|
||||
We strongly recommend renaming the `DeDRM_tools_X.X.X.zip` archive (after extracting its contents) to `DeDRM_tools_X.X.X_archive.zip`. If you do that, you are less likely to navigate to the wrong location from inside calibre.)
|
||||
|
||||
# Using the Tools
|
||||
## I 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.
|
||||
|
||||
## I've unzipped the tools, but what are all the different files, and how do I use them?
|
||||
Read the `ReadMe_Overview.txt` file and then the ReadMe files for the tools you're interested in. That's what they're for.
|
||||
|
||||
## I have installed the calibre plugin, but my books still have DRM. When I try to view or convert my books, calibre says they have DRM.
|
||||
DRM only gets removed when an ebook is imported into calibre. Also, if the book is already in calibre, by default calibre will discard the newly imported file. You can change this in calibre's Adding books preferences page (Automerge..../Overwrite....), so that newly imported files overwrite existing ebook formats. Then just re-import your books and the DRM-free versions will overwrite the DRMed versions while retaining your books' metadata.
|
||||
|
||||
## I have installed the calibre plugin, 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.
|
||||
|
||||
### Macintosh
|
||||
Navigating from your home folder,
|
||||
|
||||
Kindle for Mac ebooks are in either `Library/Application Support/Kindle/My Kindle Content` or `Documents/My Kindle Content or Library/Containers/com.amazon.Kindle/Data/Library/Application Support/Kindle/My Kindle Content`, depending on your version of Kindle for Mac.
|
||||
|
||||
Adobe Digital Editions ebooks are in `Documents/Digital Editions`
|
||||
|
||||
### Windows
|
||||
Navigating from your `Documents` folder (`My Documents` folder, pre-Windows 7)
|
||||
|
||||
Kindle for PC ebooks are in `My Kindle Content`
|
||||
|
||||
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.
|
||||
You must use the exact file that is used by your ebook reading software or hardware. See the previous question on where to find your ebook files. Do not use an old copy you have that you can no longer read.
|
||||
If you cannot read the ebook on your current device or installed software, the tools will certainly not be able to remove the DRM. Download a fresh copy that does work with your current device or installed software.
|
||||
|
||||
## I have installed the calibre plugin, and the book is not already in calibre, but the DRM does not get removed. It is a Kindle book.
|
||||
If you are on Windows 8 and using the Windows 8 AppStore Kindle app, you must download and install the Kindle for PC application directly from the Amazon website. The tools do not work with the Windows 8 AppStore Kindle app.
|
||||
|
||||
If this book is from an eInk Kindle (e.g. Paperwhite), you must enter the serial number into the configuration dialog. The serial number is sixteen characters long, and is case-sensitive.
|
||||
|
||||
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 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. Uninstall Kindle for PC(Mac)
|
||||
1. Delete the Kindle for PC(Mac) preferences
|
||||
* PC: Delete the directory `[home folder]\AppData\Local\Amazon` (it might be hidden) and `[home folder]\My Documents\My Kindle Content`
|
||||
* Mac: Delete the directory `[home folder]/Library/Application Support/Kindle/` and/or `[home folder]/Library/Containers/com.amazon.Kindle/Data/Library/Application Support/Kindle/` (one or both may be present and should be deleted)
|
||||
1. Reinstall Kindle for PC(Mac) version 1.17 or earlier (see above for download links).
|
||||
1. Re-register Kindle for PC(Mac) with your Amazon account
|
||||
1. Download the ebook again. Do not use the files you have downloaded previously.
|
||||
|
||||
## Some of my books had their DRM removed, but some still say that they have DRM and will not convert.
|
||||
There are several possible reasons why only some books get their DRM removed.
|
||||
* You still 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.
|
||||
|
||||
## 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.
|
||||
|
||||
## 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.
|
||||
|
||||
## Do the tools work on books from Kobo?
|
||||
If you use the Kobo desktop application for Mac or PC, install the Obok plugin. This will import and remove the DRM from your Kobo books, and is the easiest method for Kobo ebooks.
|
||||
|
||||
## I registered Adobe Digital Editions 3.0 or later with an Adobe ID before downloading, but my epub or PDF still has DRM.
|
||||
Adobe introduced a new DRM scheme with ADE 3.0 and later. Install ADE 2.0.1 and register with the same Adobe ID. If you can't open your book in ADE 2.01, then you have a book with the new DRM scheme. These tools can't help. You can avoid the new DRM scheme by always downloading your ebooks with ADE 2.0.1. Some retailers will require ADE 3.0 or later, in which case you won't be able to download with ADE 2.0.1.
|
||||
|
||||
## The DRM wasn't removed and the log says "Failed to decrypt with error: Cannot decode library or rented ebooks." What now?
|
||||
You're trying to remove the DRM from an ebook that's only on loan to you. No help will be given to remove DRM from such ebooks. If you think that you have received this message for a book you own, please create an issue at github, or comment at the blog.
|
||||
|
||||
## 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.
|
||||
|
||||
## 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).
|
||||
|
||||
# General Questions
|
||||
|
||||
## Once the DRM has been removed, is there any trace of my personal identity left in the ebook?
|
||||
The tools only remove the DRM. No attempt is made to remove any personally identifying information.
|
||||
|
||||
## Why do some of my Kindle ebooks import as HTMLZ format in calibre?
|
||||
Most Amazon Kindle ebooks are Mobipocket format ebooks, or the new KF8 format. However, some are in a format known as Topaz. The Topaz format is only used by Amazon. A Topaz ebook is a collections of glyphs and their positions on each page tagged with some additional information from that page including OCRed text (Optical Character Recognition generated Text) to allow searching, and some additional layout information. Each page of a Topaz ebook is effectively a description of an image of that page. To convert a Topaz ebook to another format is not easy as there is not a one-to-one mapping between glyphs and characters/fonts. To account for this, two different formats are generated by the DRM removal software. The first is an html description built from the OCRtext and images stored in the Topaz file (HTMLZ). This format is easily reflowed but may suffer from typical OCRtext errors including typos, garbled text, missing italics, missing bolds, etc. The second format uses the glyph and position information to create an accurate scalable vector graphics (SVG) image of each page of the book that can be viewed in web browsers that support svg images (Safari, Firefox 4 or later, etc). Additional conversion software can be used to convert these SVG images to an image only PDF file. The DeDRM calibre plugin only imports the HTMLZ versions of the Topaz ebook. The html version can be manually cleaned up and spell checked and then converted using Sigil/calibre to epubs, mobi ebooks, and etc.
|
||||
|
||||
## Are the tools open source? How can I be sure they are safe and not a trojan horse?
|
||||
All the DRM removal tools hosted here are almost entirely scripts of one kind or another: Python, Applescript or Windows Batch files. So they are inherently open source, and open to inspection by everyone who downloads them.
|
||||
|
||||
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?
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
## Why don’t the tools work with Kindle Fire ebooks?
|
||||
Because no-one's found out how to remove the DRM from ebooks from Kindle Fire devices yet. The workaround is to install Kindle for PC or Kindle for Mac and use books from there instead.
|
||||
|
||||
## Why don't the tools work with Kindle for iOS ebooks?
|
||||
Amazon changed the way the key was generated for Kindle for iOS books, and the tools can no longer find the key. The workaround is to install Kindle for PC or Kindle for Mac and use books from there instead.
|
||||
|
||||
## Why don't the tools work with Kindle for Android ebooks?
|
||||
Amazon turned off backup for Kindle for Android, so the tools can no longer find the key. The workaround is to install Kindle for PC or Kindle for Mac and use books from there instead.
|
||||
|
||||
## Why don't the tools work on books from the Apple iBooks Store?
|
||||
Apple regularly change the details of their DRM and so the tools in the main tools archive will not work with these ebooks. Apple’s Fairplay DRM scheme can be removed using Requiem if the appropriate version of iTunes can still be installed and used. See the post Apple and ebooks: iBookstore DRM and how to remove it at Apprentice Alf's blog for more details.
|
||||
|
||||
## I’ve got the tools archive and I’ve read all the FAQs but I still can’t install the tools and/or the DRM removal doesn’t work
|
||||
* Read the `ReadMe_Overview.txt` file in the top level of the tools archive
|
||||
* Read the ReadMe file for the tool you want to use.
|
||||
* 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).
|
||||
|
||||
## Who wrote these scripts?
|
||||
The authors tend to identify themselves only by pseudonyms:
|
||||
* The Adobe Adept and Barnes & Noble scripts were created by i♥cabbages
|
||||
* The Amazon Mobipocket and eReader scripts were created by The Dark Reverser
|
||||
* The Amazon K4PC DRM/format was further decoded by Bart Simpson aka Skindle
|
||||
* The Amazon K4 Mobi tool was created by by some_updates, mdlnx and others
|
||||
* The Amazon Topaz DRM removal script was created by CMBDTC
|
||||
* The Amazon Topaz format conversion was created by some_updates, clarknova, and Bart Simpson
|
||||
* The DeDRM all-in-one calibre plugin was created by Apprentice Alf
|
||||
* The support for .kinf2018 key files and KFX 2&3 was by Apprentice Sakuya
|
||||
* The Scuolabooks tool was created by Hex
|
||||
* The Microsoft code was created by drs
|
||||
* The Apple DRM removal tool was created by Brahms
|
||||
|
||||
Since the original versions of the scripts and programs were released, various people have helped to maintain and improve them.
|
||||
@@ -1,140 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
|
||||
|
||||
import sys
|
||||
sys.path.append('lib')
|
||||
import os, os.path, urllib
|
||||
import subprocess
|
||||
from subprocess import Popen, PIPE, STDOUT
|
||||
import subasyncio
|
||||
from subasyncio import Process
|
||||
import Tkinter
|
||||
import Tkconstants
|
||||
import tkFileDialog
|
||||
import tkMessageBox
|
||||
from scrolltextwidget import ScrolledText
|
||||
|
||||
class MainDialog(Tkinter.Frame):
|
||||
def __init__(self, root):
|
||||
Tkinter.Frame.__init__(self, root, border=5)
|
||||
self.root = root
|
||||
self.interval = 2000
|
||||
self.p2 = None
|
||||
self.status = Tkinter.Label(self, text='Find your Kindle PID')
|
||||
self.status.pack(fill=Tkconstants.X, expand=1)
|
||||
body = Tkinter.Frame(self)
|
||||
body.pack(fill=Tkconstants.X, expand=1)
|
||||
sticky = Tkconstants.E + Tkconstants.W
|
||||
body.grid_columnconfigure(1, weight=2)
|
||||
|
||||
Tkinter.Label(body, text='Kindle Serial # or iPhone UDID').grid(row=1, sticky=Tkconstants.E)
|
||||
self.serialnum = Tkinter.StringVar()
|
||||
self.serialinfo = Tkinter.Entry(body, width=45, textvariable=self.serialnum)
|
||||
self.serialinfo.grid(row=1, column=1, sticky=sticky)
|
||||
|
||||
msg1 = 'Conversion Log \n\n'
|
||||
self.stext = ScrolledText(body, bd=5, relief=Tkconstants.RIDGE, height=15, width=60, wrap=Tkconstants.WORD)
|
||||
self.stext.grid(row=3, column=0, columnspan=2,sticky=sticky)
|
||||
self.stext.insert(Tkconstants.END,msg1)
|
||||
|
||||
buttons = Tkinter.Frame(self)
|
||||
buttons.pack()
|
||||
self.sbotton = Tkinter.Button(
|
||||
buttons, text="Start", width=10, command=self.convertit)
|
||||
self.sbotton.pack(side=Tkconstants.LEFT)
|
||||
|
||||
Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT)
|
||||
self.qbutton = Tkinter.Button(
|
||||
buttons, text="Quit", width=10, command=self.quitting)
|
||||
self.qbutton.pack(side=Tkconstants.RIGHT)
|
||||
|
||||
# read from subprocess pipe without blocking
|
||||
# invoked every interval via the widget "after"
|
||||
# option being used, so need to reset it for the next time
|
||||
def processPipe(self):
|
||||
poll = self.p2.wait('nowait')
|
||||
if poll != None:
|
||||
text = self.p2.readerr()
|
||||
text += self.p2.read()
|
||||
msg = text + '\n\n' + 'Kindle PID Successfully Determined\n'
|
||||
if poll != 0:
|
||||
msg = text + '\n\n' + 'Error: Kindle PID Failed\n'
|
||||
self.showCmdOutput(msg)
|
||||
self.p2 = None
|
||||
self.sbotton.configure(state='normal')
|
||||
return
|
||||
text = self.p2.readerr()
|
||||
text += self.p2.read()
|
||||
self.showCmdOutput(text)
|
||||
# make sure we get invoked again by event loop after interval
|
||||
self.stext.after(self.interval,self.processPipe)
|
||||
return
|
||||
|
||||
# post output from subprocess in scrolled text widget
|
||||
def showCmdOutput(self, msg):
|
||||
if msg and msg !='':
|
||||
msg = msg.encode('utf-8')
|
||||
self.stext.insert(Tkconstants.END,msg)
|
||||
self.stext.yview_pickplace(Tkconstants.END)
|
||||
return
|
||||
|
||||
# run as a subprocess via pipes and collect stdout
|
||||
def pidrdr(self, serial):
|
||||
# os.putenv('PYTHONUNBUFFERED', '1')
|
||||
cmdline = 'python ./lib/kindlepid.py "' + serial + '"'
|
||||
if sys.platform[0:3] == 'win':
|
||||
search_path = os.environ['PATH']
|
||||
search_path = search_path.lower()
|
||||
if search_path.find('python') >= 0:
|
||||
cmdline = 'python lib\kindlepid.py "' + serial + '"'
|
||||
else :
|
||||
cmdline = 'lib\kindlepid.py "' + serial + '"'
|
||||
cmdline = cmdline.encode(sys.getfilesystemencoding())
|
||||
p2 = Process(cmdline, shell=True, bufsize=1, stdin=None, stdout=PIPE, stderr=PIPE, close_fds=False)
|
||||
return p2
|
||||
|
||||
def quitting(self):
|
||||
# kill any still running subprocess
|
||||
if self.p2 != None:
|
||||
if (self.p2.wait('nowait') == None):
|
||||
self.p2.terminate()
|
||||
self.root.destroy()
|
||||
|
||||
# actually ready to run the subprocess and get its output
|
||||
def convertit(self):
|
||||
# now disable the button to prevent multiple launches
|
||||
self.sbotton.configure(state='disabled')
|
||||
serial = self.serialinfo.get()
|
||||
if not serial or serial == '':
|
||||
self.status['text'] = 'No Kindle Serial Number or iPhone UDID specified'
|
||||
self.sbotton.configure(state='normal')
|
||||
return
|
||||
|
||||
log = 'Command = "python kindlepid.py"\n'
|
||||
log += 'Serial = "' + serial + '"\n'
|
||||
log += '\n\n'
|
||||
log += 'Please Wait ...\n\n'
|
||||
log = log.encode('utf-8')
|
||||
self.stext.insert(Tkconstants.END,log)
|
||||
self.p2 = self.pidrdr(serial)
|
||||
|
||||
# python does not seem to allow you to create
|
||||
# your own eventloop which every other gui does - strange
|
||||
# so need to use the widget "after" command to force
|
||||
# event loop to run non-gui events every interval
|
||||
self.stext.after(self.interval,self.processPipe)
|
||||
return
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
root = Tkinter.Tk()
|
||||
root.title('Kindle and iPhone PID Calculator')
|
||||
root.resizable(True, False)
|
||||
root.minsize(300, 0)
|
||||
MainDialog(root).pack(fill=Tkconstants.X, expand=1)
|
||||
root.mainloop()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,168 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
|
||||
|
||||
import sys
|
||||
sys.path.append('lib')
|
||||
import os, os.path, urllib
|
||||
import subprocess
|
||||
from subprocess import Popen, PIPE, STDOUT
|
||||
import subasyncio
|
||||
from subasyncio import Process
|
||||
import Tkinter
|
||||
import Tkconstants
|
||||
import tkFileDialog
|
||||
import tkMessageBox
|
||||
from scrolltextwidget import ScrolledText
|
||||
|
||||
class MainDialog(Tkinter.Frame):
|
||||
def __init__(self, root):
|
||||
Tkinter.Frame.__init__(self, root, border=5)
|
||||
self.root = root
|
||||
self.interval = 2000
|
||||
self.p2 = None
|
||||
self.status = Tkinter.Label(self, text='Fix Encrypted Mobi eBooks so the Kindle can read them')
|
||||
self.status.pack(fill=Tkconstants.X, expand=1)
|
||||
body = Tkinter.Frame(self)
|
||||
body.pack(fill=Tkconstants.X, expand=1)
|
||||
sticky = Tkconstants.E + Tkconstants.W
|
||||
body.grid_columnconfigure(1, weight=2)
|
||||
|
||||
Tkinter.Label(body, text='Mobi eBook input file').grid(row=0, sticky=Tkconstants.E)
|
||||
self.mobipath = Tkinter.Entry(body, width=50)
|
||||
self.mobipath.grid(row=0, column=1, sticky=sticky)
|
||||
cwd = os.getcwdu()
|
||||
cwd = cwd.encode('utf-8')
|
||||
self.mobipath.insert(0, cwd)
|
||||
button = Tkinter.Button(body, text="...", command=self.get_mobipath)
|
||||
button.grid(row=0, column=2)
|
||||
|
||||
Tkinter.Label(body, text='10 Character PID').grid(row=1, sticky=Tkconstants.E)
|
||||
self.pidnum = Tkinter.StringVar()
|
||||
self.pidinfo = Tkinter.Entry(body, width=12, textvariable=self.pidnum)
|
||||
self.pidinfo.grid(row=1, column=1, sticky=sticky)
|
||||
|
||||
msg1 = 'Conversion Log \n\n'
|
||||
self.stext = ScrolledText(body, bd=5, relief=Tkconstants.RIDGE, height=15, width=60, wrap=Tkconstants.WORD)
|
||||
self.stext.grid(row=2, column=0, columnspan=2,sticky=sticky)
|
||||
self.stext.insert(Tkconstants.END,msg1)
|
||||
|
||||
buttons = Tkinter.Frame(self)
|
||||
buttons.pack()
|
||||
self.sbotton = Tkinter.Button(
|
||||
buttons, text="Start", width=10, command=self.convertit)
|
||||
self.sbotton.pack(side=Tkconstants.LEFT)
|
||||
|
||||
Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT)
|
||||
self.qbutton = Tkinter.Button(
|
||||
buttons, text="Quit", width=10, command=self.quitting)
|
||||
self.qbutton.pack(side=Tkconstants.RIGHT)
|
||||
|
||||
# read from subprocess pipe without blocking
|
||||
# invoked every interval via the widget "after"
|
||||
# option being used, so need to reset it for the next time
|
||||
def processPipe(self):
|
||||
poll = self.p2.wait('nowait')
|
||||
if poll != None:
|
||||
text = self.p2.readerr()
|
||||
text += self.p2.read()
|
||||
msg = text + '\n\n' + 'Fix for Kindle successful\n'
|
||||
if poll != 0:
|
||||
msg = text + '\n\n' + 'Error: Fix for Kindle Failed\n'
|
||||
self.showCmdOutput(msg)
|
||||
self.p2 = None
|
||||
self.sbotton.configure(state='normal')
|
||||
return
|
||||
text = self.p2.readerr()
|
||||
text += self.p2.read()
|
||||
self.showCmdOutput(text)
|
||||
# make sure we get invoked again by event loop after interval
|
||||
self.stext.after(self.interval,self.processPipe)
|
||||
return
|
||||
|
||||
# post output from subprocess in scrolled text widget
|
||||
def showCmdOutput(self, msg):
|
||||
if msg and msg !='':
|
||||
msg = msg.encode('utf-8')
|
||||
self.stext.insert(Tkconstants.END,msg)
|
||||
self.stext.yview_pickplace(Tkconstants.END)
|
||||
return
|
||||
|
||||
# run as a subprocess via pipes and collect stdout
|
||||
def krdr(self, infile, pidnum):
|
||||
# os.putenv('PYTHONUNBUFFERED', '1')
|
||||
cmdline = 'python ./lib/kindlefix.py "' + infile + '" "' + pidnum + '"'
|
||||
if sys.platform[0:3] == 'win':
|
||||
search_path = os.environ['PATH']
|
||||
search_path = search_path.lower()
|
||||
if search_path.find('python') >= 0:
|
||||
cmdline = 'python lib\kindlefix.py "' + infile + '" "' + pidnum + '"'
|
||||
else :
|
||||
cmdline = 'lib\kindlefix.py "' + infile + '" "' + pidnum + '"'
|
||||
|
||||
cmdline = cmdline.encode(sys.getfilesystemencoding())
|
||||
p2 = Process(cmdline, shell=True, bufsize=1, stdin=None, stdout=PIPE, stderr=PIPE, close_fds=False)
|
||||
return p2
|
||||
|
||||
|
||||
def get_mobipath(self):
|
||||
mobipath = tkFileDialog.askopenfilename(
|
||||
parent=None, title='Select Mobi eBook File',
|
||||
defaultextension='.prc', filetypes=[('Mobi eBook File', '.prc'), ('Mobi eBook File', '.mobi'),
|
||||
('All Files', '.*')])
|
||||
if mobipath:
|
||||
mobipath = os.path.normpath(mobipath)
|
||||
self.mobipath.delete(0, Tkconstants.END)
|
||||
self.mobipath.insert(0, mobipath)
|
||||
return
|
||||
|
||||
def quitting(self):
|
||||
# kill any still running subprocess
|
||||
if self.p2 != None:
|
||||
if (self.p2.wait('nowait') == None):
|
||||
self.p2.terminate()
|
||||
self.root.destroy()
|
||||
|
||||
# actually ready to run the subprocess and get its output
|
||||
def convertit(self):
|
||||
# now disable the button to prevent multiple launches
|
||||
self.sbotton.configure(state='disabled')
|
||||
mobipath = self.mobipath.get()
|
||||
pidnum = self.pidinfo.get()
|
||||
if not mobipath or not os.path.exists(mobipath):
|
||||
self.status['text'] = 'Specified Mobi eBook file does not exist'
|
||||
self.sbotton.configure(state='normal')
|
||||
return
|
||||
if not pidnum or pidnum == '':
|
||||
self.status['text'] = 'No PID specified'
|
||||
self.sbotton.configure(state='normal')
|
||||
return
|
||||
|
||||
log = 'Command = "python kindlefix.py"\n'
|
||||
log += 'Mobi Path = "'+ mobipath + '"\n'
|
||||
log += 'PID = "' + pidnum + '"\n'
|
||||
log += '\n\n'
|
||||
log += 'Please Wait ...\n\n'
|
||||
log = log.encode('utf-8')
|
||||
self.stext.insert(Tkconstants.END,log)
|
||||
self.p2 = self.krdr(mobipath, pidnum)
|
||||
|
||||
# python does not seem to allow you to create
|
||||
# your own eventloop which every other gui does - strange
|
||||
# so need to use the widget "after" command to force
|
||||
# event loop to run non-gui events every interval
|
||||
self.stext.after(self.interval,self.processPipe)
|
||||
return
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
root = Tkinter.Tk()
|
||||
root.title('Fix Encrypted Mobi eBooks to work with the Kindle')
|
||||
root.resizable(True, False)
|
||||
root.minsize(300, 0)
|
||||
MainDialog(root).pack(fill=Tkconstants.X, expand=1)
|
||||
root.mainloop()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,197 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
|
||||
|
||||
import sys
|
||||
sys.path.append('lib')
|
||||
import os, os.path, urllib
|
||||
import subprocess
|
||||
from subprocess import Popen, PIPE, STDOUT
|
||||
import subasyncio
|
||||
from subasyncio import Process
|
||||
import Tkinter
|
||||
import Tkconstants
|
||||
import tkFileDialog
|
||||
import tkMessageBox
|
||||
from scrolltextwidget import ScrolledText
|
||||
|
||||
class MainDialog(Tkinter.Frame):
|
||||
def __init__(self, root):
|
||||
Tkinter.Frame.__init__(self, root, border=5)
|
||||
self.root = root
|
||||
self.interval = 2000
|
||||
self.p2 = None
|
||||
self.status = Tkinter.Label(self, text='Remove Encryption from a Mobi eBook')
|
||||
self.status.pack(fill=Tkconstants.X, expand=1)
|
||||
body = Tkinter.Frame(self)
|
||||
body.pack(fill=Tkconstants.X, expand=1)
|
||||
sticky = Tkconstants.E + Tkconstants.W
|
||||
body.grid_columnconfigure(1, weight=2)
|
||||
|
||||
Tkinter.Label(body, text='Mobi eBook input file').grid(row=0, sticky=Tkconstants.E)
|
||||
self.mobipath = Tkinter.Entry(body, width=50)
|
||||
self.mobipath.grid(row=0, column=1, sticky=sticky)
|
||||
cwd = os.getcwdu()
|
||||
cwd = cwd.encode('utf-8')
|
||||
self.mobipath.insert(0, cwd)
|
||||
button = Tkinter.Button(body, text="...", command=self.get_mobipath)
|
||||
button.grid(row=0, column=2)
|
||||
|
||||
Tkinter.Label(body, text='Name for Unencrypted Output File').grid(row=1, sticky=Tkconstants.E)
|
||||
self.outpath = Tkinter.Entry(body, width=50)
|
||||
self.outpath.grid(row=1, column=1, sticky=sticky)
|
||||
self.outpath.insert(0, '')
|
||||
button = Tkinter.Button(body, text="...", command=self.get_outpath)
|
||||
button.grid(row=1, column=2)
|
||||
|
||||
Tkinter.Label(body, text='10 Character PID').grid(row=2, sticky=Tkconstants.E)
|
||||
self.pidnum = Tkinter.StringVar()
|
||||
self.pidinfo = Tkinter.Entry(body, width=12, textvariable=self.pidnum)
|
||||
self.pidinfo.grid(row=2, column=1, sticky=sticky)
|
||||
|
||||
msg1 = 'Conversion Log \n\n'
|
||||
self.stext = ScrolledText(body, bd=5, relief=Tkconstants.RIDGE, height=15, width=60, wrap=Tkconstants.WORD)
|
||||
self.stext.grid(row=3, column=0, columnspan=2,sticky=sticky)
|
||||
self.stext.insert(Tkconstants.END,msg1)
|
||||
|
||||
buttons = Tkinter.Frame(self)
|
||||
buttons.pack()
|
||||
self.sbotton = Tkinter.Button(
|
||||
buttons, text="Start", width=10, command=self.convertit)
|
||||
self.sbotton.pack(side=Tkconstants.LEFT)
|
||||
|
||||
Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT)
|
||||
self.qbutton = Tkinter.Button(
|
||||
buttons, text="Quit", width=10, command=self.quitting)
|
||||
self.qbutton.pack(side=Tkconstants.RIGHT)
|
||||
|
||||
# read from subprocess pipe without blocking
|
||||
# invoked every interval via the widget "after"
|
||||
# option being used, so need to reset it for the next time
|
||||
def processPipe(self):
|
||||
poll = self.p2.wait('nowait')
|
||||
if poll != None:
|
||||
text = self.p2.readerr()
|
||||
text += self.p2.read()
|
||||
msg = text + '\n\n' + 'Encryption successfully removed\n'
|
||||
if poll != 0:
|
||||
msg = text + '\n\n' + 'Error: Encryption Removal Failed\n'
|
||||
self.showCmdOutput(msg)
|
||||
self.p2 = None
|
||||
self.sbotton.configure(state='normal')
|
||||
return
|
||||
text = self.p2.readerr()
|
||||
text += self.p2.read()
|
||||
self.showCmdOutput(text)
|
||||
# make sure we get invoked again by event loop after interval
|
||||
self.stext.after(self.interval,self.processPipe)
|
||||
return
|
||||
|
||||
# post output from subprocess in scrolled text widget
|
||||
def showCmdOutput(self, msg):
|
||||
if msg and msg !='':
|
||||
msg = msg.encode('utf-8')
|
||||
self.stext.insert(Tkconstants.END,msg)
|
||||
self.stext.yview_pickplace(Tkconstants.END)
|
||||
return
|
||||
|
||||
# run as a subprocess via pipes and collect stdout
|
||||
def mobirdr(self, infile, outfile, pidnum):
|
||||
# os.putenv('PYTHONUNBUFFERED', '1')
|
||||
cmdline = 'python ./lib/mobidedrm.py "' + infile + '" "' + outfile + '" "' + pidnum + '"'
|
||||
if sys.platform[0:3] == 'win':
|
||||
search_path = os.environ['PATH']
|
||||
search_path = search_path.lower()
|
||||
if search_path.find('python') >= 0:
|
||||
cmdline = 'python lib\mobidedrm.py "' + infile + '" "' + outfile + '" "' + pidnum + '"'
|
||||
else :
|
||||
cmdline = 'lib\mobidedrm.py "' + infile + '" "' + outfile + '" "' + pidnum + '"'
|
||||
|
||||
cmdline = cmdline.encode(sys.getfilesystemencoding())
|
||||
p2 = Process(cmdline, shell=True, bufsize=1, stdin=None, stdout=PIPE, stderr=PIPE, close_fds=False)
|
||||
return p2
|
||||
|
||||
|
||||
def get_mobipath(self):
|
||||
mobipath = tkFileDialog.askopenfilename(
|
||||
parent=None, title='Select Mobi eBook File',
|
||||
defaultextension='.prc', filetypes=[('Mobi eBook File', '.prc'), ('Mobi eBook File', '.mobi'),
|
||||
('All Files', '.*')])
|
||||
if mobipath:
|
||||
mobipath = os.path.normpath(mobipath)
|
||||
self.mobipath.delete(0, Tkconstants.END)
|
||||
self.mobipath.insert(0, mobipath)
|
||||
return
|
||||
|
||||
def get_outpath(self):
|
||||
mobipath = self.mobipath.get()
|
||||
initname = os.path.basename(mobipath)
|
||||
p = initname.find('.')
|
||||
if p >= 0: initname = initname[0:p]
|
||||
initname += '_nodrm.mobi'
|
||||
outpath = tkFileDialog.asksaveasfilename(
|
||||
parent=None, title='Select Unencrypted Mobi File to produce',
|
||||
defaultextension='.mobi', initialfile=initname,
|
||||
filetypes=[('Mobi files', '.mobi'), ('All files', '.*')])
|
||||
if outpath:
|
||||
outpath = os.path.normpath(outpath)
|
||||
self.outpath.delete(0, Tkconstants.END)
|
||||
self.outpath.insert(0, outpath)
|
||||
return
|
||||
|
||||
def quitting(self):
|
||||
# kill any still running subprocess
|
||||
if self.p2 != None:
|
||||
if (self.p2.wait('nowait') == None):
|
||||
self.p2.terminate()
|
||||
self.root.destroy()
|
||||
|
||||
# actually ready to run the subprocess and get its output
|
||||
def convertit(self):
|
||||
# now disable the button to prevent multiple launches
|
||||
self.sbotton.configure(state='disabled')
|
||||
mobipath = self.mobipath.get()
|
||||
outpath = self.outpath.get()
|
||||
pidnum = self.pidinfo.get()
|
||||
if not mobipath or not os.path.exists(mobipath):
|
||||
self.status['text'] = 'Specified Mobi eBook file does not exist'
|
||||
self.sbotton.configure(state='normal')
|
||||
return
|
||||
if not outpath:
|
||||
self.status['text'] = 'No output file specified'
|
||||
self.sbotton.configure(state='normal')
|
||||
return
|
||||
if not pidnum or pidnum == '':
|
||||
self.status['text'] = 'No PID specified'
|
||||
self.sbotton.configure(state='normal')
|
||||
return
|
||||
|
||||
log = 'Command = "python mobidedrm.py"\n'
|
||||
log += 'Mobi Path = "'+ mobipath + '"\n'
|
||||
log += 'Output File = "' + outpath + '"\n'
|
||||
log += 'PID = "' + pidnum + '"\n'
|
||||
log += '\n\n'
|
||||
log += 'Please Wait ...\n\n'
|
||||
log = log.encode('utf-8')
|
||||
self.stext.insert(Tkconstants.END,log)
|
||||
self.p2 = self.mobirdr(mobipath, outpath, pidnum)
|
||||
|
||||
# python does not seem to allow you to create
|
||||
# your own eventloop which every other gui does - strange
|
||||
# so need to use the widget "after" command to force
|
||||
# event loop to run non-gui events every interval
|
||||
self.stext.after(self.interval,self.processPipe)
|
||||
return
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
root = Tkinter.Tk()
|
||||
root.title('Mobi eBook Encryption Removal')
|
||||
root.resizable(True, False)
|
||||
root.minsize(300, 0)
|
||||
MainDialog(root).pack(fill=Tkconstants.X, expand=1)
|
||||
root.mainloop()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,172 +0,0 @@
|
||||
class Unbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
def write(self, data):
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
import sys
|
||||
sys.stdout=Unbuffered(sys.stdout)
|
||||
|
||||
|
||||
import prc, struct
|
||||
from binascii import hexlify
|
||||
|
||||
def strByte(s,off=0):
|
||||
return struct.unpack(">B",s[off])[0];
|
||||
|
||||
def strSWord(s,off=0):
|
||||
return struct.unpack(">h",s[off:off+2])[0];
|
||||
|
||||
def strWord(s,off=0):
|
||||
return struct.unpack(">H",s[off:off+2])[0];
|
||||
|
||||
def strDWord(s,off=0):
|
||||
return struct.unpack(">L",s[off:off+4])[0];
|
||||
|
||||
def strPutDWord(s,off,i):
|
||||
return s[:off]+struct.pack(">L",i)+s[off+4:];
|
||||
|
||||
keyvec1 = "\x72\x38\x33\xB0\xB4\xF2\xE3\xCA\xDF\x09\x01\xD6\xE2\xE0\x3F\x96"
|
||||
|
||||
#implementation of Pukall Cipher 1
|
||||
def PC1(key, src, decryption=True):
|
||||
sum1 = 0;
|
||||
sum2 = 0;
|
||||
keyXorVal = 0;
|
||||
if len(key)!=16:
|
||||
print "Bad key length!"
|
||||
return None
|
||||
wkey = []
|
||||
for i in xrange(8):
|
||||
wkey.append(ord(key[i*2])<<8 | ord(key[i*2+1]))
|
||||
|
||||
dst = ""
|
||||
for i in xrange(len(src)):
|
||||
temp1 = 0;
|
||||
byteXorVal = 0;
|
||||
for j in xrange(8):
|
||||
temp1 ^= wkey[j]
|
||||
sum2 = (sum2+j)*20021 + sum1
|
||||
sum1 = (temp1*346)&0xFFFF
|
||||
sum2 = (sum2+sum1)&0xFFFF
|
||||
temp1 = (temp1*20021+1)&0xFFFF
|
||||
byteXorVal ^= temp1 ^ sum2
|
||||
|
||||
curByte = ord(src[i])
|
||||
if not decryption:
|
||||
keyXorVal = curByte * 257;
|
||||
curByte = ((curByte ^ (byteXorVal >> 8)) ^ byteXorVal) & 0xFF
|
||||
if decryption:
|
||||
keyXorVal = curByte * 257;
|
||||
for j in xrange(8):
|
||||
wkey[j] ^= keyXorVal;
|
||||
|
||||
dst+=chr(curByte)
|
||||
|
||||
return dst
|
||||
|
||||
def find_key(rec0, pid):
|
||||
off1 = strDWord(rec0, 0xA8)
|
||||
if off1==0xFFFFFFFF or off1==0:
|
||||
print "No DRM"
|
||||
return None
|
||||
size1 = strDWord(rec0, 0xB0)
|
||||
cnt = strDWord(rec0, 0xAC)
|
||||
flag = strDWord(rec0, 0xB4)
|
||||
|
||||
temp_key = PC1(keyvec1, pid.ljust(16,'\0'), False)
|
||||
cksum = 0
|
||||
#print pid, "->", hexlify(temp_key)
|
||||
for i in xrange(len(temp_key)):
|
||||
cksum += ord(temp_key[i])
|
||||
cksum &= 0xFF
|
||||
temp_key = temp_key.ljust(16,'\0')
|
||||
#print "pid cksum: %02X"%cksum
|
||||
|
||||
#print "Key records: %02X-%02X, count: %d, flag: %02X"%(off1, off1+size1, cnt, flag)
|
||||
iOff = off1
|
||||
drm_key = None
|
||||
for i in xrange(cnt):
|
||||
dwCheck = strDWord(rec0, iOff)
|
||||
dwSize = strDWord(rec0, iOff+4)
|
||||
dwType = strDWord(rec0, iOff+8)
|
||||
nCksum = strByte(rec0, iOff+0xC)
|
||||
#print "Key record %d: check=%08X, size=%d, type=%d, cksum=%02X"%(i, dwCheck, dwSize, dwType, nCksum)
|
||||
if nCksum==cksum:
|
||||
drmInfo = PC1(temp_key, rec0[iOff+0x10:iOff+0x30])
|
||||
dw0, dw4, dw18, dw1c = struct.unpack(">II16xII", drmInfo)
|
||||
#print "Decrypted drmInfo:", "%08X, %08X, %s, %08X, %08X"%(dw0, dw4, hexlify(drmInfo[0x8:0x18]), dw18, dw1c)
|
||||
#print "Decrypted drmInfo:", hexlify(drmInfo)
|
||||
if dw0==dwCheck:
|
||||
print "Found the matching record; setting the CustomDRM flag for Kindle"
|
||||
drmInfo = strPutDWord(drmInfo,4,(dw4|0x800))
|
||||
dw0, dw4, dw18, dw1c = struct.unpack(">II16xII", drmInfo)
|
||||
#print "Updated drmInfo:", "%08X, %08X, %s, %08X, %08X"%(dw0, dw4, hexlify(drmInfo[0x8:0x18]), dw18, dw1c)
|
||||
return rec0[:iOff+0x10] + PC1(temp_key, drmInfo, False) + rec0[:iOff+0x30]
|
||||
iOff += dwSize
|
||||
return None
|
||||
|
||||
def replaceext(filename, newext):
|
||||
nameparts = filename.split(".")
|
||||
if len(nameparts)>1:
|
||||
return (".".join(nameparts[:-1]))+newext
|
||||
else:
|
||||
return nameparts[0]+newext
|
||||
|
||||
def main(argv=sys.argv):
|
||||
print "The Kindleizer v0.2. Copyright (c) 2007 Igor Skochinsky"
|
||||
if len(sys.argv) != 3:
|
||||
print "Fixes encrypted Mobipocket books to be readable by Kindle"
|
||||
print "Usage: kindlefix.py file.mobi PID"
|
||||
return 1
|
||||
fname = sys.argv[1]
|
||||
pid = sys.argv[2]
|
||||
if len(pid)==10 and pid[-3]=='*':
|
||||
pid = pid[:-2]
|
||||
if len(pid)!=8 or pid[-1]!='*':
|
||||
print "PID is not valid! (should be in format AAAAAAA*DD)"
|
||||
return 3
|
||||
db = prc.File(fname)
|
||||
#print dir(db)
|
||||
if db.getDBInfo()["creator"]!='MOBI':
|
||||
print "Not a Mobi file!"
|
||||
return 1
|
||||
rec0 = db.getRecord(0)[0]
|
||||
enc = strSWord(rec0, 0xC)
|
||||
print "Encryption:", enc
|
||||
if enc!=2:
|
||||
print "Unknown encryption type"
|
||||
return 1
|
||||
|
||||
if len(rec0)<0x28 or rec0[0x10:0x14] != 'MOBI':
|
||||
print "bad file format"
|
||||
return 1
|
||||
print "Mobi publication type:", strDWord(rec0, 0x18)
|
||||
formatVer = strDWord(rec0, 0x24)
|
||||
print "Mobi format version:", formatVer
|
||||
last_rec = strWord(rec0, 8)
|
||||
dwE0 = 0
|
||||
if formatVer>=4:
|
||||
new_rec0 = find_key(rec0, pid)
|
||||
if new_rec0:
|
||||
db.setRecordIdx(0,new_rec0)
|
||||
else:
|
||||
print "PID doesn't match this file"
|
||||
return 2
|
||||
else:
|
||||
print "Wrong Mobi format version"
|
||||
return 1
|
||||
|
||||
outfname = replaceext(fname, ".azw")
|
||||
if outfname==fname:
|
||||
outfname = replaceext(fname, "_fixed.azw")
|
||||
db.save(outfname)
|
||||
print "Output written to "+outfname
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,97 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
# Mobipocket PID calculator v0.2 for Amazon Kindle.
|
||||
# Copyright (c) 2007, 2009 Igor Skochinsky <skochinsky@mail.ru>
|
||||
# History:
|
||||
# 0.1 Initial release
|
||||
# 0.2 Added support for generating PID for iPhone (thanks to mbp)
|
||||
# 0.3 changed to autoflush stdout, fixed return code usage
|
||||
class Unbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
def write(self, data):
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
import sys
|
||||
sys.stdout=Unbuffered(sys.stdout)
|
||||
|
||||
import binascii
|
||||
|
||||
if sys.hexversion >= 0x3000000:
|
||||
print "This script is incompatible with Python 3.x. Please install Python 2.6.x from python.org"
|
||||
sys.exit(2)
|
||||
|
||||
letters = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789"
|
||||
|
||||
def crc32(s):
|
||||
return (~binascii.crc32(s,-1))&0xFFFFFFFF
|
||||
|
||||
def checksumPid(s):
|
||||
crc = crc32(s)
|
||||
crc = crc ^ (crc >> 16)
|
||||
res = s
|
||||
l = len(letters)
|
||||
for i in (0,1):
|
||||
b = crc & 0xff
|
||||
pos = (b // l) ^ (b % l)
|
||||
res += letters[pos%l]
|
||||
crc >>= 8
|
||||
|
||||
return res
|
||||
|
||||
|
||||
def pidFromSerial(s, l):
|
||||
crc = crc32(s)
|
||||
|
||||
arr1 = [0]*l
|
||||
for i in xrange(len(s)):
|
||||
arr1[i%l] ^= ord(s[i])
|
||||
|
||||
crc_bytes = [crc >> 24 & 0xff, crc >> 16 & 0xff, crc >> 8 & 0xff, crc & 0xff]
|
||||
for i in xrange(l):
|
||||
arr1[i] ^= crc_bytes[i&3]
|
||||
|
||||
pid = ""
|
||||
for i in xrange(l):
|
||||
b = arr1[i] & 0xff
|
||||
pid+=letters[(b >> 7) + ((b >> 5 & 3) ^ (b & 0x1f))]
|
||||
|
||||
return pid
|
||||
|
||||
def main(argv=sys.argv):
|
||||
print "Mobipocket PID calculator for Amazon Kindle. Copyright (c) 2007, 2009 Igor Skochinsky"
|
||||
if len(sys.argv)==2:
|
||||
serial = sys.argv[1]
|
||||
else:
|
||||
print "Usage: kindlepid.py <Kindle Serial Number>/<iPhone/iPod Touch UDID>"
|
||||
return 1
|
||||
if len(serial)==16:
|
||||
if serial.startswith("B001"):
|
||||
print "Kindle 1 serial number detected"
|
||||
elif serial.startswith("B002"):
|
||||
print "Kindle 2 serial number detected"
|
||||
elif serial.startswith("B003"):
|
||||
print "Kindle 2 Global serial number detected"
|
||||
elif serial.startswith("B004"):
|
||||
print "Kindle DX serial number detected"
|
||||
else:
|
||||
print "Warning: unrecognized serial number. Please recheck input."
|
||||
return 1
|
||||
pid = pidFromSerial(serial,7)+"*"
|
||||
print "Mobipocked PID for Kindle serial# "+serial+" is "+checksumPid(pid)
|
||||
return 0
|
||||
elif len(serial)==40:
|
||||
print "iPhone serial number (UDID) detected"
|
||||
pid = pidFromSerial(serial,8)
|
||||
print "Mobipocked PID for iPhone serial# "+serial+" is "+checksumPid(pid)
|
||||
return 0
|
||||
else:
|
||||
print "Warning: unrecognized serial number. Please recheck input."
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,300 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# This is a python script. You need a Python interpreter to run it.
|
||||
# For example, ActiveState Python, which exists for windows.
|
||||
#
|
||||
# It can run standalone to convert files, or it can be installed as a
|
||||
# plugin for Calibre (http://calibre-ebook.com/about) so that
|
||||
# importing files with DRM 'Just Works'.
|
||||
#
|
||||
# To create a Calibre plugin, rename this file so that the filename
|
||||
# ends in '_plugin.py', put it into a ZIP file and import that Calibre
|
||||
# using its plugin configuration GUI.
|
||||
#
|
||||
# Changelog
|
||||
# 0.01 - Initial version
|
||||
# 0.02 - Huffdic compressed books were not properly decrypted
|
||||
# 0.03 - Wasn't checking MOBI header length
|
||||
# 0.04 - Wasn't sanity checking size of data record
|
||||
# 0.05 - It seems that the extra data flags take two bytes not four
|
||||
# 0.06 - And that low bit does mean something after all :-)
|
||||
# 0.07 - The extra data flags aren't present in MOBI header < 0xE8 in size
|
||||
# 0.08 - ...and also not in Mobi header version < 6
|
||||
# 0.09 - ...but they are there with Mobi header version 6, header size 0xE4!
|
||||
# 0.10 - Outputs unencrypted files as-is, so that when run as a Calibre
|
||||
# import filter it works when importing unencrypted files.
|
||||
# Also now handles encrypted files that don't need a specific PID.
|
||||
# 0.11 - use autoflushed stdout and proper return values
|
||||
# 0.12 - Fix for problems with metadata import as Calibre plugin, report errors
|
||||
# 0.13 - Formatting fixes: retabbed file, removed trailing whitespace
|
||||
# and extra blank lines, converted CR/LF pairs at ends of each line,
|
||||
# and other cosmetic fixes.
|
||||
# 0.14 - Working out when the extra data flags are present has been problematic
|
||||
# Versions 7 through 9 have tried to tweak the conditions, but have been
|
||||
# only partially successful. Closer examination of lots of sample
|
||||
# files reveals that a confusin has arisen because trailing data entries
|
||||
# are not encrypted, but it turns out that the multibyte entries
|
||||
# in utf8 file are encrypted. (Although neither kind gets compressed.)
|
||||
# This knowledge leads to a simplification of the test for the
|
||||
# trailing data byte flags - version 5 and higher AND header size >= 0xE4.
|
||||
|
||||
__version__ = '0.14'
|
||||
|
||||
import sys
|
||||
import struct
|
||||
import binascii
|
||||
|
||||
class Unbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
def write(self, data):
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
class DrmException(Exception):
|
||||
pass
|
||||
|
||||
# Implementation of Pukall Cipher 1
|
||||
def PC1(key, src, decryption=True):
|
||||
sum1 = 0;
|
||||
sum2 = 0;
|
||||
keyXorVal = 0;
|
||||
if len(key)!=16:
|
||||
print "Bad key length!"
|
||||
return None
|
||||
wkey = []
|
||||
for i in xrange(8):
|
||||
wkey.append(ord(key[i*2])<<8 | ord(key[i*2+1]))
|
||||
|
||||
dst = ""
|
||||
for i in xrange(len(src)):
|
||||
temp1 = 0;
|
||||
byteXorVal = 0;
|
||||
for j in xrange(8):
|
||||
temp1 ^= wkey[j]
|
||||
sum2 = (sum2+j)*20021 + sum1
|
||||
sum1 = (temp1*346)&0xFFFF
|
||||
sum2 = (sum2+sum1)&0xFFFF
|
||||
temp1 = (temp1*20021+1)&0xFFFF
|
||||
byteXorVal ^= temp1 ^ sum2
|
||||
curByte = ord(src[i])
|
||||
if not decryption:
|
||||
keyXorVal = curByte * 257;
|
||||
curByte = ((curByte ^ (byteXorVal >> 8)) ^ byteXorVal) & 0xFF
|
||||
if decryption:
|
||||
keyXorVal = curByte * 257;
|
||||
for j in xrange(8):
|
||||
wkey[j] ^= keyXorVal;
|
||||
dst+=chr(curByte)
|
||||
return dst
|
||||
|
||||
def checksumPid(s):
|
||||
letters = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789"
|
||||
crc = (~binascii.crc32(s,-1))&0xFFFFFFFF
|
||||
crc = crc ^ (crc >> 16)
|
||||
res = s
|
||||
l = len(letters)
|
||||
for i in (0,1):
|
||||
b = crc & 0xff
|
||||
pos = (b // l) ^ (b % l)
|
||||
res += letters[pos%l]
|
||||
crc >>= 8
|
||||
return res
|
||||
|
||||
def getSizeOfTrailingDataEntries(ptr, size, flags):
|
||||
def getSizeOfTrailingDataEntry(ptr, size):
|
||||
bitpos, result = 0, 0
|
||||
if size <= 0:
|
||||
return result
|
||||
while True:
|
||||
v = ord(ptr[size-1])
|
||||
result |= (v & 0x7F) << bitpos
|
||||
bitpos += 7
|
||||
size -= 1
|
||||
if (v & 0x80) != 0 or (bitpos >= 28) or (size == 0):
|
||||
return result
|
||||
num = 0
|
||||
testflags = flags >> 1
|
||||
while testflags:
|
||||
if testflags & 1:
|
||||
num += getSizeOfTrailingDataEntry(ptr, size - num)
|
||||
testflags >>= 1
|
||||
# Multibyte data, if present, is included in the encryption, so
|
||||
# we do not need to check the low bit.
|
||||
# if flags & 1:
|
||||
# num += (ord(ptr[size - num - 1]) & 0x3) + 1
|
||||
return num
|
||||
|
||||
class DrmStripper:
|
||||
def loadSection(self, section):
|
||||
if (section + 1 == self.num_sections):
|
||||
endoff = len(self.data_file)
|
||||
else:
|
||||
endoff = self.sections[section + 1][0]
|
||||
off = self.sections[section][0]
|
||||
return self.data_file[off:endoff]
|
||||
|
||||
def patch(self, off, new):
|
||||
self.data_file = self.data_file[:off] + new + self.data_file[off+len(new):]
|
||||
|
||||
def patchSection(self, section, new, in_off = 0):
|
||||
if (section + 1 == self.num_sections):
|
||||
endoff = len(self.data_file)
|
||||
else:
|
||||
endoff = self.sections[section + 1][0]
|
||||
off = self.sections[section][0]
|
||||
assert off + in_off + len(new) <= endoff
|
||||
self.patch(off + in_off, new)
|
||||
|
||||
def parseDRM(self, data, count, pid):
|
||||
pid = pid.ljust(16,'\0')
|
||||
keyvec1 = "\x72\x38\x33\xB0\xB4\xF2\xE3\xCA\xDF\x09\x01\xD6\xE2\xE0\x3F\x96"
|
||||
temp_key = PC1(keyvec1, pid, False)
|
||||
temp_key_sum = sum(map(ord,temp_key)) & 0xff
|
||||
found_key = None
|
||||
for i in xrange(count):
|
||||
verification, size, type, cksum, cookie = struct.unpack('>LLLBxxx32s', data[i*0x30:i*0x30+0x30])
|
||||
cookie = PC1(temp_key, cookie)
|
||||
ver,flags,finalkey,expiry,expiry2 = struct.unpack('>LL16sLL', cookie)
|
||||
if verification == ver and cksum == temp_key_sum and (flags & 0x1F) == 1:
|
||||
found_key = finalkey
|
||||
break
|
||||
if not found_key:
|
||||
# Then try the default encoding that doesn't require a PID
|
||||
temp_key = keyvec1
|
||||
temp_key_sum = sum(map(ord,temp_key)) & 0xff
|
||||
for i in xrange(count):
|
||||
verification, size, type, cksum, cookie = struct.unpack('>LLLBxxx32s', data[i*0x30:i*0x30+0x30])
|
||||
cookie = PC1(temp_key, cookie)
|
||||
ver,flags,finalkey,expiry,expiry2 = struct.unpack('>LL16sLL', cookie)
|
||||
if verification == ver and cksum == temp_key_sum:
|
||||
found_key = finalkey
|
||||
break
|
||||
return found_key
|
||||
|
||||
def __init__(self, data_file, pid):
|
||||
if checksumPid(pid[0:-2]) != pid:
|
||||
raise DrmException("invalid PID checksum")
|
||||
pid = pid[0:-2]
|
||||
|
||||
self.data_file = data_file
|
||||
header = data_file[0:72]
|
||||
if header[0x3C:0x3C+8] != 'BOOKMOBI':
|
||||
raise DrmException("invalid file format")
|
||||
self.num_sections, = struct.unpack('>H', data_file[76:78])
|
||||
|
||||
self.sections = []
|
||||
for i in xrange(self.num_sections):
|
||||
offset, a1,a2,a3,a4 = struct.unpack('>LBBBB', data_file[78+i*8:78+i*8+8])
|
||||
flags, val = a1, a2<<16|a3<<8|a4
|
||||
self.sections.append( (offset, flags, val) )
|
||||
|
||||
sect = self.loadSection(0)
|
||||
records, = struct.unpack('>H', sect[0x8:0x8+2])
|
||||
mobi_length, = struct.unpack('>L',sect[0x14:0x18])
|
||||
mobi_version, = struct.unpack('>L',sect[0x68:0x6C])
|
||||
extra_data_flags = 0
|
||||
print "MOBI header length = %d" %mobi_length
|
||||
print "MOBI header version = %d" %mobi_version
|
||||
if (mobi_length >= 0xE4) and (mobi_version >= 5):
|
||||
extra_data_flags, = struct.unpack('>H', sect[0xF2:0xF4])
|
||||
print "Extra Data Flags = %d" %extra_data_flags
|
||||
|
||||
crypto_type, = struct.unpack('>H', sect[0xC:0xC+2])
|
||||
if crypto_type == 0:
|
||||
print "This book is not encrypted."
|
||||
else:
|
||||
if crypto_type == 1:
|
||||
raise DrmException("cannot decode Mobipocket encryption type 1")
|
||||
if crypto_type != 2:
|
||||
raise DrmException("unknown encryption type: %d" % crypto_type)
|
||||
|
||||
# calculate the keys
|
||||
drm_ptr, drm_count, drm_size, drm_flags = struct.unpack('>LLLL', sect[0xA8:0xA8+16])
|
||||
if drm_count == 0:
|
||||
raise DrmException("no PIDs found in this file")
|
||||
found_key = self.parseDRM(sect[drm_ptr:drm_ptr+drm_size], drm_count, pid)
|
||||
if not found_key:
|
||||
raise DrmException("no key found. maybe the PID is incorrect")
|
||||
|
||||
# kill the drm keys
|
||||
self.patchSection(0, "\0" * drm_size, drm_ptr)
|
||||
# kill the drm pointers
|
||||
self.patchSection(0, "\xff" * 4 + "\0" * 12, 0xA8)
|
||||
# clear the crypto type
|
||||
self.patchSection(0, "\0" * 2, 0xC)
|
||||
|
||||
# decrypt sections
|
||||
print "Decrypting. Please wait...",
|
||||
for i in xrange(1, records+1):
|
||||
data = self.loadSection(i)
|
||||
extra_size = getSizeOfTrailingDataEntries(data, len(data), extra_data_flags)
|
||||
# print "record %d, extra_size %d" %(i,extra_size)
|
||||
self.patchSection(i, PC1(found_key, data[0:len(data) - extra_size]))
|
||||
print "done"
|
||||
|
||||
def getResult(self):
|
||||
return self.data_file
|
||||
|
||||
if not __name__ == "__main__":
|
||||
from calibre.customize import FileTypePlugin
|
||||
|
||||
class MobiDeDRM(FileTypePlugin):
|
||||
name = 'MobiDeDRM' # Name of the plugin
|
||||
description = 'Removes DRM from secure Mobi files'
|
||||
supported_platforms = ['linux', 'osx', 'windows'] # Platforms this plugin will run on
|
||||
author = 'The Dark Reverser' # The author of this plugin
|
||||
version = (0, 1, 4) # The version number of this plugin
|
||||
file_types = set(['prc','mobi','azw']) # The file types that this plugin will be applied to
|
||||
on_import = True # Run this plugin during the import
|
||||
|
||||
def run(self, path_to_ebook):
|
||||
from calibre.gui2 import is_ok_to_use_qt
|
||||
from PyQt4.Qt import QMessageBox
|
||||
PID = self.site_customization
|
||||
data_file = file(path_to_ebook, 'rb').read()
|
||||
ar = PID.split(',')
|
||||
for i in ar:
|
||||
try:
|
||||
unlocked_file = DrmStripper(data_file, i).getResult()
|
||||
except DrmException:
|
||||
# ignore the error
|
||||
pass
|
||||
else:
|
||||
of = self.temporary_file('.mobi')
|
||||
of.write(unlocked_file)
|
||||
of.close()
|
||||
return of.name
|
||||
if is_ok_to_use_qt():
|
||||
d = QMessageBox(QMessageBox.Warning, "MobiDeDRM Plugin", "Couldn't decode: %s\n\nImporting encrypted version." % path_to_ebook)
|
||||
d.show()
|
||||
d.raise_()
|
||||
d.exec_()
|
||||
return path_to_ebook
|
||||
|
||||
def customization_help(self, gui=False):
|
||||
return 'Enter PID (separate multiple PIDs with comma)'
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.stdout=Unbuffered(sys.stdout)
|
||||
print ('MobiDeDrm v%(__version__)s. '
|
||||
'Copyright 2008-2010 The Dark Reverser.' % globals())
|
||||
if len(sys.argv)<4:
|
||||
print "Removes protection from Mobipocket books"
|
||||
print "Usage:"
|
||||
print " %s <infile> <outfile> <PID>" % sys.argv[0]
|
||||
sys.exit(1)
|
||||
else:
|
||||
infile = sys.argv[1]
|
||||
outfile = sys.argv[2]
|
||||
pid = sys.argv[3]
|
||||
data_file = file(infile, 'rb').read()
|
||||
try:
|
||||
strippedFile = DrmStripper(data_file, pid)
|
||||
file(outfile, 'wb').write(strippedFile.getResult())
|
||||
except DrmException, e:
|
||||
print "Error: %s" % e
|
||||
sys.exit(1)
|
||||
sys.exit(0)
|
||||
@@ -1,189 +0,0 @@
|
||||
# This is a python script. You need a Python interpreter to run it.
|
||||
# For example, ActiveState Python, which exists for windows.
|
||||
#
|
||||
# Big Thanks to Igor SKOCHINSKY for providing me with all his information
|
||||
# and source code relating to the inner workings of this compression scheme.
|
||||
# Without it, I wouldn't be able to solve this as easily.
|
||||
#
|
||||
# Changelog
|
||||
# 0.01 - Initial version
|
||||
# 0.02 - Fix issue with size computing
|
||||
# 0.03 - Fix issue with some files
|
||||
# 0.04 - make stdout self flushing and fix return values
|
||||
|
||||
class Unbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
def write(self, data):
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
import sys
|
||||
sys.stdout=Unbuffered(sys.stdout)
|
||||
|
||||
|
||||
import struct
|
||||
|
||||
class BitReader:
|
||||
def __init__(self, data):
|
||||
self.data, self.pos, self.nbits = data + "\x00\x00\x00\x00", 0, len(data) * 8
|
||||
def peek(self, n):
|
||||
r, g = 0, 0
|
||||
while g < n:
|
||||
r, g = (r << 8) | ord(self.data[(self.pos+g)>>3]), g + 8 - ((self.pos+g) & 7)
|
||||
return (r >> (g - n)) & ((1 << n) - 1)
|
||||
def eat(self, n):
|
||||
self.pos += n
|
||||
return self.pos <= self.nbits
|
||||
def left(self):
|
||||
return self.nbits - self.pos
|
||||
|
||||
class HuffReader:
|
||||
def __init__(self, huffs):
|
||||
self.huffs = huffs
|
||||
h = huffs[0]
|
||||
if huffs[0][0:4] != 'HUFF' or huffs[0][4:8] != '\x00\x00\x00\x18':
|
||||
raise ValueError('invalid huff1 header')
|
||||
if huffs[1][0:4] != 'CDIC' or huffs[1][4:8] != '\x00\x00\x00\x10':
|
||||
raise ValueError('invalid huff2 header')
|
||||
self.entry_bits, = struct.unpack('>L', huffs[1][12:16])
|
||||
off1,off2 = struct.unpack('>LL', huffs[0][16:24])
|
||||
self.dict1 = struct.unpack('<256L', huffs[0][off1:off1+256*4])
|
||||
self.dict2 = struct.unpack('<64L', huffs[0][off2:off2+64*4])
|
||||
self.dicts = huffs[1:]
|
||||
self.r = ''
|
||||
|
||||
def _unpack(self, bits, depth = 0):
|
||||
if depth > 32:
|
||||
raise ValueError('corrupt file')
|
||||
while bits.left():
|
||||
dw = bits.peek(32)
|
||||
v = self.dict1[dw >> 24]
|
||||
codelen = v & 0x1F
|
||||
assert codelen != 0
|
||||
code = dw >> (32 - codelen)
|
||||
r = (v >> 8)
|
||||
if not (v & 0x80):
|
||||
while code < self.dict2[(codelen-1)*2]:
|
||||
codelen += 1
|
||||
code = dw >> (32 - codelen)
|
||||
r = self.dict2[(codelen-1)*2+1]
|
||||
r -= code
|
||||
assert codelen != 0
|
||||
if not bits.eat(codelen):
|
||||
return
|
||||
dicno = r >> self.entry_bits
|
||||
off1 = 16 + (r - (dicno << self.entry_bits)) * 2
|
||||
dic = self.dicts[dicno]
|
||||
off2 = 16 + ord(dic[off1]) * 256 + ord(dic[off1+1])
|
||||
blen = ord(dic[off2]) * 256 + ord(dic[off2+1])
|
||||
slice = dic[off2+2:off2+2+(blen&0x7fff)]
|
||||
if blen & 0x8000:
|
||||
self.r += slice
|
||||
else:
|
||||
self._unpack(BitReader(slice), depth + 1)
|
||||
|
||||
def unpack(self, data):
|
||||
self.r = ''
|
||||
self._unpack(BitReader(data))
|
||||
return self.r
|
||||
|
||||
class Sectionizer:
|
||||
def __init__(self, filename, ident):
|
||||
self.contents = file(filename, 'rb').read()
|
||||
self.header = self.contents[0:72]
|
||||
self.num_sections, = struct.unpack('>H', self.contents[76:78])
|
||||
if self.header[0x3C:0x3C+8] != ident:
|
||||
raise ValueError('Invalid file format')
|
||||
self.sections = []
|
||||
for i in xrange(self.num_sections):
|
||||
offset, a1,a2,a3,a4 = struct.unpack('>LBBBB', self.contents[78+i*8:78+i*8+8])
|
||||
flags, val = a1, a2<<16|a3<<8|a4
|
||||
self.sections.append( (offset, flags, val) )
|
||||
def loadSection(self, section):
|
||||
if section + 1 == self.num_sections:
|
||||
end_off = len(self.contents)
|
||||
else:
|
||||
end_off = self.sections[section + 1][0]
|
||||
off = self.sections[section][0]
|
||||
return self.contents[off:end_off]
|
||||
|
||||
|
||||
def getSizeOfTrailingDataEntry(ptr, size):
|
||||
bitpos, result = 0, 0
|
||||
while True:
|
||||
v = ord(ptr[size-1])
|
||||
result |= (v & 0x7F) << bitpos
|
||||
bitpos += 7
|
||||
size -= 1
|
||||
if (v & 0x80) != 0 or (bitpos >= 28) or (size == 0):
|
||||
return result
|
||||
|
||||
def getSizeOfTrailingDataEntries(ptr, size, flags):
|
||||
num = 0
|
||||
flags >>= 1
|
||||
while flags:
|
||||
if flags & 1:
|
||||
num += getSizeOfTrailingDataEntry(ptr, size - num)
|
||||
flags >>= 1
|
||||
return num
|
||||
|
||||
def unpackBook(input_file):
|
||||
sect = Sectionizer(input_file, 'BOOKMOBI')
|
||||
|
||||
header = sect.loadSection(0)
|
||||
|
||||
crypto_type, = struct.unpack('>H', header[0xC:0xC+2])
|
||||
if crypto_type != 0:
|
||||
raise ValueError('The book is encrypted. Run mobidedrm first')
|
||||
|
||||
if header[0:2] != 'DH':
|
||||
raise ValueError('invalid compression type')
|
||||
|
||||
extra_flags, = struct.unpack('>L', header[0xF0:0xF4])
|
||||
records, = struct.unpack('>H', header[0x8:0x8+2])
|
||||
|
||||
huffoff,huffnum = struct.unpack('>LL', header[0x70:0x78])
|
||||
huffs = [sect.loadSection(i) for i in xrange(huffoff, huffoff+huffnum)]
|
||||
huff = HuffReader(huffs)
|
||||
|
||||
def decompressSection(nr):
|
||||
data = sect.loadSection(nr)
|
||||
trail_size = getSizeOfTrailingDataEntries(data, len(data), extra_flags)
|
||||
return huff.unpack(data[0:len(data)-trail_size])
|
||||
|
||||
r = ''
|
||||
for i in xrange(1, records+1):
|
||||
r += decompressSection(i)
|
||||
return r
|
||||
|
||||
def main(argv=sys.argv):
|
||||
print "MobiHuff v0.03"
|
||||
print " Copyright (c) 2008 The Dark Reverser <dark.reverser@googlemail.com>"
|
||||
if len(sys.argv)!=3:
|
||||
print ""
|
||||
print "Description:"
|
||||
print " Unpacks the new mobipocket huffdic compression."
|
||||
print " This program works with unencrypted files only."
|
||||
print "Usage:"
|
||||
print " mobihuff.py infile.mobi outfile.html"
|
||||
return 1
|
||||
else:
|
||||
infile = sys.argv[1]
|
||||
outfile = sys.argv[2]
|
||||
try:
|
||||
print "Decompressing...",
|
||||
result = unpackBook(infile)
|
||||
file(outfile, 'wb').write(result)
|
||||
print "done"
|
||||
except ValueError, e:
|
||||
print
|
||||
print "Error: %s" % e
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,529 +0,0 @@
|
||||
#
|
||||
# $Id: prc.py,v 1.3 2001/12/27 08:48:02 rob Exp $
|
||||
#
|
||||
# Copyright 1998-2001 Rob Tillotson <rob@pyrite.org>
|
||||
# All Rights Reserved
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and
|
||||
# its documentation for any purpose and without fee or royalty is
|
||||
# hereby granted, provided that the above copyright notice appear in
|
||||
# all copies and that both the copyright notice and this permission
|
||||
# notice appear in supporting documentation or portions thereof,
|
||||
# including modifications, that you you make.
|
||||
#
|
||||
# THE AUTHOR ROB TILLOTSON DISCLAIMS ALL WARRANTIES WITH REGARD TO
|
||||
# THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
# AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
||||
# SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
|
||||
# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
|
||||
# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
||||
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE!
|
||||
#
|
||||
"""PRC/PDB file I/O in pure Python.
|
||||
|
||||
This module serves two purposes: one, it allows access to Palm OS(tm)
|
||||
database files on the desktop in pure Python without requiring
|
||||
pilot-link (hence, it may be useful for import/export utilities),
|
||||
and two, it caches the contents of the file in memory so it can
|
||||
be freely modified using an identical API to databases over a
|
||||
DLP connection.
|
||||
"""
|
||||
|
||||
__version__ = '$Id: prc.py,v 1.3 2001/12/27 08:48:02 rob Exp $'
|
||||
|
||||
__copyright__ = 'Copyright 1998-2001 Rob Tillotson <robt@debian.org>'
|
||||
|
||||
|
||||
# temporary hack until we get gettext support again
|
||||
def _(s): return s
|
||||
|
||||
#
|
||||
# DBInfo structure:
|
||||
#
|
||||
# int more
|
||||
# unsigned int flags
|
||||
# unsigned int miscflags
|
||||
# unsigned long type
|
||||
# unsigned long creator
|
||||
# unsigned int version
|
||||
# unsigned long modnum
|
||||
# time_t createDate, modifydate, backupdate
|
||||
# unsigned int index
|
||||
# char name[34]
|
||||
#
|
||||
#
|
||||
# DB Header:
|
||||
# 32 name
|
||||
# 2 flags
|
||||
# 2 version
|
||||
# 4 creation time
|
||||
# 4 modification time
|
||||
# 4 backup time
|
||||
# 4 modification number
|
||||
# 4 appinfo offset
|
||||
# 4 sortinfo offset
|
||||
# 4 type
|
||||
# 4 creator
|
||||
# 4 unique id seed (garbage?)
|
||||
# 4 next record list id (normally 0)
|
||||
# 2 num of records for this header
|
||||
# (maybe 2 more bytes)
|
||||
#
|
||||
# Resource entry header: (if low bit of attr = 1)
|
||||
# 4 type
|
||||
# 2 id
|
||||
# 4 offset
|
||||
#
|
||||
# record entry header: (if low bit of attr = 0)
|
||||
# 4 offset
|
||||
# 1 attributes
|
||||
# 3 unique id
|
||||
#
|
||||
# then 2 bytes of 0
|
||||
#
|
||||
# then appinfo then sortinfo
|
||||
#
|
||||
|
||||
import sys, os, stat, struct
|
||||
|
||||
PI_HDR_SIZE = 78
|
||||
PI_RESOURCE_ENT_SIZE = 10
|
||||
PI_RECORD_ENT_SIZE = 8
|
||||
|
||||
PILOT_TIME_DELTA = 2082844800L
|
||||
|
||||
flagResource = 0x0001
|
||||
flagReadOnly = 0x0002
|
||||
flagAppInfoDirty = 0x0004
|
||||
flagBackup = 0x0008
|
||||
flagOpen = 0x8000
|
||||
# 2.x
|
||||
flagNewer = 0x0010
|
||||
flagReset = 0x0020
|
||||
#
|
||||
flagExcludeFromSync = 0x0080
|
||||
|
||||
attrDeleted = 0x80
|
||||
attrDirty = 0x40
|
||||
attrBusy = 0x20
|
||||
attrSecret = 0x10
|
||||
attrArchived = 0x08
|
||||
|
||||
default_info = {
|
||||
'name': '',
|
||||
'type': 'DATA',
|
||||
'creator': ' ',
|
||||
'createDate': 0,
|
||||
'modifyDate': 0,
|
||||
'backupDate': 0,
|
||||
'modnum': 0,
|
||||
'version': 0,
|
||||
'flagReset': 0,
|
||||
'flagResource': 0,
|
||||
'flagNewer': 0,
|
||||
'flagExcludeFromSync': 0,
|
||||
'flagAppInfoDirty': 0,
|
||||
'flagReadOnly': 0,
|
||||
'flagBackup': 0,
|
||||
'flagOpen': 0,
|
||||
'more': 0,
|
||||
'index': 0
|
||||
}
|
||||
|
||||
def null_terminated(s):
|
||||
for x in range(0, len(s)):
|
||||
if s[x] == '\000': return s[:x]
|
||||
return s
|
||||
|
||||
def trim_null(s):
|
||||
return string.split(s, '\0')[0]
|
||||
|
||||
def pad_null(s, l):
|
||||
if len(s) > l - 1:
|
||||
s = s[:l-1]
|
||||
s = s + '\0'
|
||||
if len(s) < l: s = s + '\0' * (l - len(s))
|
||||
return s
|
||||
|
||||
#
|
||||
# new stuff
|
||||
|
||||
# Record object to be put in tree...
|
||||
class PRecord:
|
||||
def __init__(self, attr=0, id=0, category=0, raw=''):
|
||||
self.raw = raw
|
||||
self.id = id
|
||||
self.attr = attr
|
||||
self.category = category
|
||||
|
||||
# comparison and hashing are done by ID;
|
||||
# thus, the id value *may not be changed* once
|
||||
# the object is created.
|
||||
def __cmp__(self, obj):
|
||||
if type(obj) == type(0):
|
||||
return cmp(self.id, obj)
|
||||
else:
|
||||
return cmp(self.id, obj.id)
|
||||
|
||||
def __hash__(self):
|
||||
return self.id
|
||||
|
||||
class PResource:
|
||||
def __init__(self, typ=' ', id=0, raw=''):
|
||||
self.raw = raw
|
||||
self.id = id
|
||||
self.type = typ
|
||||
|
||||
def __cmp__(self, obj):
|
||||
if type(obj) == type(()):
|
||||
return cmp( (self.type, self.id), obj)
|
||||
else:
|
||||
return cmp( (self.type, self.id), (obj.type, obj.id) )
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.type, self.id))
|
||||
|
||||
|
||||
class PCache:
|
||||
def __init__(self):
|
||||
self.data = []
|
||||
self.appblock = ''
|
||||
self.sortblock = ''
|
||||
self.dirty = 0
|
||||
self.next = 0
|
||||
self.info = {}
|
||||
self.info.update(default_info)
|
||||
# if allow_zero_ids is 1, then this prc behaves appropriately
|
||||
# for a desktop database. That is, it never attempts to assign
|
||||
# an ID, and lets new records be inserted with an ID of zero.
|
||||
self.allow_zero_ids = 0
|
||||
|
||||
# pi-file API
|
||||
def getRecords(self): return len(self.data)
|
||||
def getAppBlock(self): return self.appblock and self.appblock or None
|
||||
def setAppBlock(self, raw):
|
||||
self.dirty = 1
|
||||
self.appblock = raw
|
||||
def getSortBlock(self): return self.sortblock and self.sortblock or None
|
||||
def setSortBlock(self, raw):
|
||||
self.dirty = 1
|
||||
self.appblock = raw
|
||||
def checkID(self, id): return id in self.data
|
||||
def getRecord(self, i):
|
||||
try: r = self.data[i]
|
||||
except: return None
|
||||
return r.raw, i, r.id, r.attr, r.category
|
||||
def getRecordByID(self, id):
|
||||
try:
|
||||
i = self.data.index(id)
|
||||
r = self.data[i]
|
||||
except: return None
|
||||
return r.raw, i, r.id, r.attr, r.category
|
||||
def getResource(self, i):
|
||||
try: r = self.data[i]
|
||||
except: return None
|
||||
return r.raw, r.type, r.id
|
||||
def getDBInfo(self): return self.info
|
||||
def setDBInfo(self, info):
|
||||
self.dirty = 1
|
||||
self.info = {}
|
||||
self.info.update(info)
|
||||
|
||||
def updateDBInfo(self, info):
|
||||
self.dirty = 1
|
||||
self.info.update(info)
|
||||
|
||||
def setRecord(self, attr, id, cat, data):
|
||||
if not self.allow_zero_ids and not id:
|
||||
if not len(self.data): id = 1
|
||||
else:
|
||||
xid = self.data[0].id + 1
|
||||
while xid in self.data: xid = xid + 1
|
||||
id = xid
|
||||
|
||||
r = PRecord(attr, id, cat, data)
|
||||
if id and id in self.data:
|
||||
self.data.remove(id)
|
||||
self.data.append(r)
|
||||
self.dirty = 1
|
||||
return id
|
||||
|
||||
def setRecordIdx(self, i, data):
|
||||
self.data[i].raw = data
|
||||
self.dirty = 1
|
||||
|
||||
def setResource(self, typ, id, data):
|
||||
if (typ, id) in self.data:
|
||||
self.data.remove((typ,id))
|
||||
r = PResource(typ, id, data)
|
||||
self.data.append(r)
|
||||
self.dirty = 1
|
||||
return id
|
||||
|
||||
def getNextRecord(self, cat):
|
||||
while self.next < len(self.data):
|
||||
r = self.data[self.next]
|
||||
i = self.next
|
||||
self.next = self.next + 1
|
||||
if r.category == cat:
|
||||
return r.raw, i, r.id, r.attr, r.category
|
||||
return ''
|
||||
|
||||
def getNextModRecord(self, cat=-1):
|
||||
while self.next < len(self.data):
|
||||
r = self.data[self.next]
|
||||
i = self.next
|
||||
self.next = self.next + 1
|
||||
if (r.attr & attrModified) and (cat < 0 or r.category == cat):
|
||||
return r.raw, i, r.id, r.attr, r.category
|
||||
|
||||
def getResourceByID(self, type, id):
|
||||
try: r = self.data[self.data.index((type,id))]
|
||||
except: return None
|
||||
return r.raw, r.type, r.id
|
||||
|
||||
def deleteRecord(self, id):
|
||||
if not id in self.data: return None
|
||||
self.data.remove(id)
|
||||
self.dirty = 1
|
||||
|
||||
def deleteRecords(self):
|
||||
self.data = []
|
||||
self.dirty = 1
|
||||
|
||||
def deleteResource(self, type, id):
|
||||
if not (type,id) in self.data: return None
|
||||
self.data.remove((type,id))
|
||||
self.dirty = 1
|
||||
|
||||
def deleteResources(self):
|
||||
self.data = []
|
||||
self.dirty = 1
|
||||
|
||||
def getRecordIDs(self, sort=0):
|
||||
m = map(lambda x: x.id, self.data)
|
||||
if sort: m.sort()
|
||||
return m
|
||||
|
||||
def moveCategory(self, frm, to):
|
||||
for r in self.data:
|
||||
if r.category == frm:
|
||||
r.category = to
|
||||
self.dirty = 1
|
||||
|
||||
def deleteCategory(self, cat):
|
||||
raise RuntimeError, _("unimplemented")
|
||||
|
||||
def purge(self):
|
||||
ndata = []
|
||||
# change to filter later
|
||||
for r in self.data:
|
||||
if (r.attr & attrDeleted):
|
||||
continue
|
||||
ndata.append(r)
|
||||
self.data = ndata
|
||||
self.dirty = 1
|
||||
|
||||
def resetNext(self):
|
||||
self.next = 0
|
||||
|
||||
def resetFlags(self):
|
||||
# special behavior for resources
|
||||
if not self.info.get('flagResource',0):
|
||||
# use map()
|
||||
for r in self.data:
|
||||
r.attr = r.attr & ~attrDirty
|
||||
self.dirty = 1
|
||||
|
||||
import pprint
|
||||
class File(PCache):
|
||||
def __init__(self, name=None, read=1, write=0, info={}):
|
||||
PCache.__init__(self)
|
||||
self.filename = name
|
||||
self.info.update(info)
|
||||
self.writeback = write
|
||||
self.isopen = 0
|
||||
|
||||
if read:
|
||||
self.load(name)
|
||||
self.isopen = 1
|
||||
|
||||
def close(self):
|
||||
if self.writeback and self.dirty:
|
||||
self.save(self.filename)
|
||||
self.isopen = 0
|
||||
|
||||
def __del__(self):
|
||||
if self.isopen: self.close()
|
||||
|
||||
def load(self, f):
|
||||
if type(f) == type(''): f = open(f, 'rb')
|
||||
|
||||
data = f.read()
|
||||
self.unpack(data)
|
||||
|
||||
def unpack(self, data):
|
||||
if len(data) < PI_HDR_SIZE: raise IOError, _("file too short")
|
||||
(name, flags, ver, ctime, mtime, btime, mnum, appinfo, sortinfo,
|
||||
typ, creator, uid, nextrec, numrec) \
|
||||
= struct.unpack('>32shhLLLlll4s4sllh', data[:PI_HDR_SIZE])
|
||||
|
||||
if nextrec or appinfo < 0 or sortinfo < 0 or numrec < 0:
|
||||
raise IOError, _("invalid database header")
|
||||
|
||||
self.info = {
|
||||
'name': null_terminated(name),
|
||||
'type': typ,
|
||||
'creator': creator,
|
||||
'createDate': ctime - PILOT_TIME_DELTA,
|
||||
'modifyDate': mtime - PILOT_TIME_DELTA,
|
||||
'backupDate': btime - PILOT_TIME_DELTA,
|
||||
'modnum': mnum,
|
||||
'version': ver,
|
||||
'flagReset': flags & flagReset,
|
||||
'flagResource': flags & flagResource,
|
||||
'flagNewer': flags & flagNewer,
|
||||
'flagExcludeFromSync': flags & flagExcludeFromSync,
|
||||
'flagAppInfoDirty': flags & flagAppInfoDirty,
|
||||
'flagReadOnly': flags & flagReadOnly,
|
||||
'flagBackup': flags & flagBackup,
|
||||
'flagOpen': flags & flagOpen,
|
||||
'more': 0,
|
||||
'index': 0
|
||||
}
|
||||
|
||||
rsrc = flags & flagResource
|
||||
if rsrc: s = PI_RESOURCE_ENT_SIZE
|
||||
else: s = PI_RECORD_ENT_SIZE
|
||||
|
||||
entries = []
|
||||
|
||||
pos = PI_HDR_SIZE
|
||||
for x in range(0,numrec):
|
||||
hstr = data[pos:pos+s]
|
||||
pos = pos + s
|
||||
if not hstr or len(hstr) < s:
|
||||
raise IOError, _("bad database header")
|
||||
|
||||
if rsrc:
|
||||
(typ, id, offset) = struct.unpack('>4shl', hstr)
|
||||
entries.append((offset, typ, id))
|
||||
else:
|
||||
(offset, auid) = struct.unpack('>ll', hstr)
|
||||
attr = (auid & 0xff000000) >> 24
|
||||
uid = auid & 0x00ffffff
|
||||
entries.append((offset, attr, uid))
|
||||
|
||||
offset = len(data)
|
||||
entries.reverse()
|
||||
for of, q, id in entries:
|
||||
size = offset - of
|
||||
if size < 0: raise IOError, _("bad pdb/prc record entry (size < 0)")
|
||||
d = data[of:offset]
|
||||
offset = of
|
||||
if len(d) != size: raise IOError, _("failed to read record")
|
||||
if rsrc:
|
||||
r = PResource(q, id, d)
|
||||
self.data.append(r)
|
||||
else:
|
||||
r = PRecord(q & 0xf0, id, q & 0x0f, d)
|
||||
self.data.append(r)
|
||||
self.data.reverse()
|
||||
|
||||
if sortinfo:
|
||||
sortinfo_size = offset - sortinfo
|
||||
offset = sortinfo
|
||||
else:
|
||||
sortinfo_size = 0
|
||||
|
||||
if appinfo:
|
||||
appinfo_size = offset - appinfo
|
||||
offset = appinfo
|
||||
else:
|
||||
appinfo_size = 0
|
||||
|
||||
if appinfo_size < 0 or sortinfo_size < 0:
|
||||
raise IOError, _("bad database header (appinfo or sortinfo size < 0)")
|
||||
|
||||
if appinfo_size:
|
||||
self.appblock = data[appinfo:appinfo+appinfo_size]
|
||||
if len(self.appblock) != appinfo_size:
|
||||
raise IOError, _("failed to read appinfo block")
|
||||
|
||||
if sortinfo_size:
|
||||
self.sortblock = data[sortinfo:sortinfo+sortinfo_size]
|
||||
if len(self.sortblock) != sortinfo_size:
|
||||
raise IOError, _("failed to read sortinfo block")
|
||||
|
||||
def save(self, f):
|
||||
"""Dump the cache to a file.
|
||||
"""
|
||||
if type(f) == type(''): f = open(f, 'wb')
|
||||
|
||||
# first, we need to precalculate the offsets.
|
||||
if self.info.get('flagResource'):
|
||||
entries_len = 10 * len(self.data)
|
||||
else: entries_len = 8 * len(self.data)
|
||||
|
||||
off = PI_HDR_SIZE + entries_len + 2
|
||||
if self.appblock:
|
||||
appinfo_offset = off
|
||||
off = off + len(self.appblock)
|
||||
else:
|
||||
appinfo_offset = 0
|
||||
if self.sortblock:
|
||||
sortinfo_offset = off
|
||||
off = off + len(self.sortblock)
|
||||
else:
|
||||
sortinfo_offset = 0
|
||||
|
||||
rec_offsets = []
|
||||
for x in self.data:
|
||||
rec_offsets.append(off)
|
||||
off = off + len(x.raw)
|
||||
|
||||
info = self.info
|
||||
flg = 0
|
||||
if info.get('flagResource',0): flg = flg | flagResource
|
||||
if info.get('flagReadOnly',0): flg = flg | flagReadOnly
|
||||
if info.get('flagAppInfoDirty',0): flg = flg | flagAppInfoDirty
|
||||
if info.get('flagBackup',0): flg = flg | flagBackup
|
||||
if info.get('flagOpen',0): flg = flg | flagOpen
|
||||
if info.get('flagNewer',0): flg = flg | flagNewer
|
||||
if info.get('flagReset',0): flg = flg | flagReset
|
||||
# excludefromsync doesn't actually get stored?
|
||||
hdr = struct.pack('>32shhLLLlll4s4sllh',
|
||||
pad_null(info.get('name',''), 32),
|
||||
flg,
|
||||
info.get('version',0),
|
||||
info.get('createDate',0L)+PILOT_TIME_DELTA,
|
||||
info.get('modifyDate',0L)+PILOT_TIME_DELTA,
|
||||
info.get('backupDate',0L)+PILOT_TIME_DELTA,
|
||||
info.get('modnum',0),
|
||||
appinfo_offset, # appinfo
|
||||
sortinfo_offset, # sortinfo
|
||||
info.get('type',' '),
|
||||
info.get('creator',' '),
|
||||
0, # uid???
|
||||
0, # nextrec???
|
||||
len(self.data))
|
||||
|
||||
f.write(hdr)
|
||||
|
||||
entries = []
|
||||
record_data = []
|
||||
rsrc = self.info.get('flagResource')
|
||||
for x, off in map(None, self.data, rec_offsets):
|
||||
if rsrc:
|
||||
record_data.append(x.raw)
|
||||
entries.append(struct.pack('>4shl', x.type, x.id, off))
|
||||
else:
|
||||
record_data.append(x.raw)
|
||||
a = ((x.attr | x.category) << 24) | x.id
|
||||
entries.append(struct.pack('>ll', off, a))
|
||||
|
||||
for x in entries: f.write(x)
|
||||
f.write('\0\0') # padding? dunno, it's always there.
|
||||
if self.appblock: f.write(self.appblock)
|
||||
if self.sortblock: f.write(self.sortblock)
|
||||
for x in record_data: f.write(x)
|
||||
@@ -1,38 +0,0 @@
|
||||
Kindle Mobipocket tools 0.2
|
||||
Copyright (c) 2007, 2009 Igor Skochinsky <skochinsky@mail.ru>
|
||||
|
||||
These scripts allow one to read legally purchased Secure Mobipocket books
|
||||
on Amazon Kindle or Kindle for iPhone.
|
||||
|
||||
* kindlepid.py
|
||||
This script generates Mobipocket PID from the Kindle serial number or iPhone/iPod Touch
|
||||
identifier (UDID). That PID can then be added at a Mobi retailer site and used for downloading
|
||||
books locked to the Kindle.
|
||||
|
||||
Example:
|
||||
|
||||
> kindlepid.py B001BAB012345678
|
||||
Mobipocket PID calculator for Amazon Kindle. Copyright (c) 2007, 2009 Igor Skochinsky
|
||||
Kindle 1 serial number detected
|
||||
Mobipocked PID for Kindle serial# B001BAB012345678 is V176CXM*FZ
|
||||
|
||||
* kindlefix.py
|
||||
This script adds a "CustomDRM" flag necessary for opening Secure
|
||||
Mobipocket books on Kindle. The book has to be enabled for Kindle's PID
|
||||
(generated by kindlepid.py). The "fixed" book is written with
|
||||
extension ".azw". That file can then be uploaded to Kindle for reading.
|
||||
|
||||
Example:
|
||||
> kindlefix.py MyBook.mobi V176CXM*FZ
|
||||
The Kindleizer v0.2. Copyright (c) 2007, 2009 Igor Skochinsky
|
||||
Encryption: 2
|
||||
Mobi publication type: 2
|
||||
Mobi format version: 4
|
||||
Found the matching record; setting the CustomDRM flag for Kindle
|
||||
Output written to MyBook.azw
|
||||
|
||||
* History
|
||||
2007-12-12 Initial release
|
||||
2009-03-10 Updated scripts to version 0.2
|
||||
kindlepid.py: Added support for generating PID for iPhone (thanks to mbp)
|
||||
kindlefix.py: Fixed corrupted metadata issue (thanks to Mark Peek)
|
||||
@@ -1,149 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
|
||||
|
||||
import os, sys
|
||||
import signal
|
||||
import threading
|
||||
import subprocess
|
||||
from subprocess import Popen, PIPE, STDOUT
|
||||
|
||||
# **heavily** chopped up and modfied version of asyncproc.py
|
||||
# to make it actually work on Windows as well as Mac/Linux
|
||||
# For the original see:
|
||||
# "http://www.lysator.liu.se/~bellman/download/"
|
||||
# author is "Thomas Bellman <bellman@lysator.liu.se>"
|
||||
# available under GPL version 3 or Later
|
||||
|
||||
# create an asynchronous subprocess whose output can be collected in
|
||||
# a non-blocking manner
|
||||
|
||||
# What a mess! Have to use threads just to get non-blocking io
|
||||
# in a cross-platform manner
|
||||
|
||||
# luckily all thread use is hidden within this class
|
||||
|
||||
class Process(object):
|
||||
def __init__(self, *params, **kwparams):
|
||||
if len(params) <= 3:
|
||||
kwparams.setdefault('stdin', subprocess.PIPE)
|
||||
if len(params) <= 4:
|
||||
kwparams.setdefault('stdout', subprocess.PIPE)
|
||||
if len(params) <= 5:
|
||||
kwparams.setdefault('stderr', subprocess.PIPE)
|
||||
self.__pending_input = []
|
||||
self.__collected_outdata = []
|
||||
self.__collected_errdata = []
|
||||
self.__exitstatus = None
|
||||
self.__lock = threading.Lock()
|
||||
self.__inputsem = threading.Semaphore(0)
|
||||
self.__quit = False
|
||||
|
||||
self.__process = subprocess.Popen(*params, **kwparams)
|
||||
|
||||
if self.__process.stdin:
|
||||
self.__stdin_thread = threading.Thread(
|
||||
name="stdin-thread",
|
||||
target=self.__feeder, args=(self.__pending_input,
|
||||
self.__process.stdin))
|
||||
self.__stdin_thread.setDaemon(True)
|
||||
self.__stdin_thread.start()
|
||||
|
||||
if self.__process.stdout:
|
||||
self.__stdout_thread = threading.Thread(
|
||||
name="stdout-thread",
|
||||
target=self.__reader, args=(self.__collected_outdata,
|
||||
self.__process.stdout))
|
||||
self.__stdout_thread.setDaemon(True)
|
||||
self.__stdout_thread.start()
|
||||
|
||||
if self.__process.stderr:
|
||||
self.__stderr_thread = threading.Thread(
|
||||
name="stderr-thread",
|
||||
target=self.__reader, args=(self.__collected_errdata,
|
||||
self.__process.stderr))
|
||||
self.__stderr_thread.setDaemon(True)
|
||||
self.__stderr_thread.start()
|
||||
|
||||
def pid(self):
|
||||
return self.__process.pid
|
||||
|
||||
def kill(self, signal):
|
||||
self.__process.send_signal(signal)
|
||||
|
||||
# check on subprocess (pass in 'nowait') to act like poll
|
||||
def wait(self, flag):
|
||||
if flag.lower() == 'nowait':
|
||||
rc = self.__process.poll()
|
||||
else:
|
||||
rc = self.__process.wait()
|
||||
if rc != None:
|
||||
if self.__process.stdin:
|
||||
self.closeinput()
|
||||
if self.__process.stdout:
|
||||
self.__stdout_thread.join()
|
||||
if self.__process.stderr:
|
||||
self.__stderr_thread.join()
|
||||
return self.__process.returncode
|
||||
|
||||
def terminate(self):
|
||||
if self.__process.stdin:
|
||||
self.closeinput()
|
||||
self.__process.terminate()
|
||||
|
||||
# thread gets data from subprocess stdout
|
||||
def __reader(self, collector, source):
|
||||
while True:
|
||||
data = os.read(source.fileno(), 65536)
|
||||
self.__lock.acquire()
|
||||
collector.append(data)
|
||||
self.__lock.release()
|
||||
if data == "":
|
||||
source.close()
|
||||
break
|
||||
return
|
||||
|
||||
# thread feeds data to subprocess stdin
|
||||
def __feeder(self, pending, drain):
|
||||
while True:
|
||||
self.__inputsem.acquire()
|
||||
self.__lock.acquire()
|
||||
if not pending and self.__quit:
|
||||
drain.close()
|
||||
self.__lock.release()
|
||||
break
|
||||
data = pending.pop(0)
|
||||
self.__lock.release()
|
||||
drain.write(data)
|
||||
|
||||
# non-blocking read of data from subprocess stdout
|
||||
def read(self):
|
||||
self.__lock.acquire()
|
||||
outdata = "".join(self.__collected_outdata)
|
||||
del self.__collected_outdata[:]
|
||||
self.__lock.release()
|
||||
return outdata
|
||||
|
||||
# non-blocking read of data from subprocess stderr
|
||||
def readerr(self):
|
||||
self.__lock.acquire()
|
||||
errdata = "".join(self.__collected_errdata)
|
||||
del self.__collected_errdata[:]
|
||||
self.__lock.release()
|
||||
return errdata
|
||||
|
||||
# non-blocking write to stdin of subprocess
|
||||
def write(self, data):
|
||||
if self.__process.stdin is None:
|
||||
raise ValueError("Writing to process with stdin not a pipe")
|
||||
self.__lock.acquire()
|
||||
self.__pending_input.append(data)
|
||||
self.__inputsem.release()
|
||||
self.__lock.release()
|
||||
|
||||
# close stdinput of subprocess
|
||||
def closeinput(self):
|
||||
self.__lock.acquire()
|
||||
self.__quit = True
|
||||
self.__inputsem.release()
|
||||
self.__lock.release()
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
|
||||
OBJS=skindle.o md5.o sha1.o b64.o skinutils.o cbuf.o mobi.o tpz.o
|
||||
|
||||
CC=gcc
|
||||
LD=gcc
|
||||
EXE=skindle
|
||||
EXTRALIBS=libz.a -lCrypt32 -lAdvapi32
|
||||
CFLAGS=-mno-cygwin
|
||||
|
||||
#use the following to strip your binary
|
||||
LDFLAGS=-s -mno-cygwin
|
||||
#LDFLAGS=-mno-cygwin
|
||||
|
||||
all: $(EXE)
|
||||
|
||||
%.o: %.c
|
||||
$(CC) -c $(CFLAGS) -g $(INC) $< -o $@
|
||||
|
||||
$(EXE): $(OBJS)
|
||||
$(LD) $(LDFLAGS) -o $@ -g $(OBJS) $(EXTRALIBS)
|
||||
|
||||
clean:
|
||||
-@rm *.o
|
||||
@@ -1,85 +0,0 @@
|
||||
|
||||
/*
|
||||
Copyright 2010 BartSimpson aka skindle
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Dependencies: zlib (included)
|
||||
* build on cygwin using make and the included make file
|
||||
* A fully functionaly windows executable is included
|
||||
*/
|
||||
|
||||
/*
|
||||
* MUST be run on the computer on which KindleForPC is installed
|
||||
* under the account that was used to purchase DRM'ed content.
|
||||
* Requires your kindle.info file which can be found in something like:
|
||||
* <User home>\...\Amazon\Kindle For PC\{AMAwzsaPaaZAzmZzZQzgZCAkZ3AjA_AY}
|
||||
* where ... varies by platform but is "Local Settings\Application Data" on XP
|
||||
* skindle will attempt to find this file automatically.
|
||||
*/
|
||||
|
||||
/*
|
||||
What: KindleForPC DRM removal utility to preserve your fair use rights!
|
||||
Why: Fair use is a well established doctrine, and I am no fan of vendor
|
||||
lockin.
|
||||
How: This utility implements the PID extraction, DRM key generation and
|
||||
decryption algorithms employed by the KindleForPC application. This
|
||||
is a stand alone app that does not require you to determine a PID on
|
||||
your own, and it does not need to run KindleForPC in order to extract
|
||||
any data from memory.
|
||||
|
||||
Shoutz: The DarkReverser - thanks for mobidedrm! The last part of this
|
||||
is just a C port of mobidedrm.
|
||||
labba and I<3cabbages for motivating me to do this the right way.
|
||||
You guys shouldn't need to spend all your time responding to all the
|
||||
changes Amazon is going to force you to make in unswindle each time
|
||||
the release a new version.
|
||||
CMBDTC - nice work on the topaz break!
|
||||
Lawrence Lessig - You are my hero. 'Nuff said.
|
||||
Cory Doctorow - A voice of reason in a sea of insanity
|
||||
Thumbs down: Disney, MPAA, RIAA - you guys suck. Making billions off
|
||||
of the exploitation of works out of copyright while vigourously
|
||||
pushing copyright extension to prevent others from doing the same
|
||||
is the height of hypocrasy.
|
||||
Congress - you guys suck too. Why you arrogant pricks think you
|
||||
are smarter than the founding fathers is beyond me.
|
||||
*/
|
||||
|
||||
Rationale:
|
||||
Need a tool to enable fair use of purchased ebook content.
|
||||
Need a tool that is not dependent on any particular version of
|
||||
KindleForPC and that does not need to run KindleForPC in order to
|
||||
extract a PID. The tool documents the structure of the kindle.info
|
||||
file and the data and algorthims that are used to derive per book
|
||||
PID values.
|
||||
|
||||
Installing:
|
||||
A compiled binary is included. Though it was built using cygwin, it
|
||||
should not require a cygwin installation in order to run it. To build
|
||||
from source, you will need cygwin with gcc and make.
|
||||
This has not been tested with Visual Studio, though you may be able to
|
||||
pile all the files into a project and add the Crypt32.lib, Advapi32 and
|
||||
zlib1 dependencies to build it.
|
||||
|
||||
usage: ./skindle [-d] [-v] -i <ebook file> -o <output file> [-k kindle.info file] [-p pid]
|
||||
-d optional, for topaz files only, produce a decompressed output file
|
||||
-i required name of the input mobi or topaz file
|
||||
-o required name of the output file to generate
|
||||
-k optional kindle.info path
|
||||
-v dump the contents of kindle.info
|
||||
-p additional PID values to attempt (can specifiy multiple times)
|
||||
|
||||
You only need to specify a kindle.info path if skindle can't find
|
||||
your kindle.info file automatically
|
||||
@@ -1,80 +0,0 @@
|
||||
/*********************************************************************\
|
||||
LICENCE: Copyright (c) 2001 Bob Trower, Trantor Standard Systems Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person
|
||||
obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute,
|
||||
sublicense, 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:
|
||||
|
||||
The above copyright notice and this permission notice shall
|
||||
be included in all copies or substantial portions of the
|
||||
Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
|
||||
KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
|
||||
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
|
||||
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
VERSION HISTORY:
|
||||
Bob Trower 08/04/01 -- Create Version 0.00.00B
|
||||
|
||||
\******************************************************************* */
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
/*
|
||||
** Translation Table as described in RFC1113
|
||||
*/
|
||||
static const char cb64[]="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
|
||||
/*
|
||||
** encodeblock
|
||||
**
|
||||
** encode 3 8-bit binary bytes as 4 '6-bit' characters
|
||||
*/
|
||||
void encodeblock(unsigned char in[3], unsigned char out[4], int len) {
|
||||
out[0] = cb64[ in[0] >> 2 ];
|
||||
out[1] = cb64[ ((in[0] & 0x03) << 4) | ((in[1] & 0xf0) >> 4) ];
|
||||
out[2] = (unsigned char) (len > 1 ? cb64[ ((in[1] & 0x0f) << 2) | ((in[2] & 0xc0) >> 6) ] : '=');
|
||||
out[3] = (unsigned char) (len > 2 ? cb64[ in[2] & 0x3f ] : '=');
|
||||
}
|
||||
|
||||
/*
|
||||
** encode
|
||||
**
|
||||
** base64 encode a stream adding padding and line breaks as per spec.
|
||||
*/
|
||||
unsigned int base64(unsigned char *inbuf, unsigned int len, unsigned char *outbuf) {
|
||||
unsigned char in[3], out[4];
|
||||
int c;
|
||||
unsigned int i = 0;
|
||||
unsigned int outlen = 0;
|
||||
while (i < len) {
|
||||
int n = 0;
|
||||
for(c = 0; c < 3; c++, i++) {
|
||||
if (i < len) {
|
||||
in[c] = inbuf[i];
|
||||
n++;
|
||||
}
|
||||
else {
|
||||
in[c] = 0;
|
||||
}
|
||||
}
|
||||
if (n) {
|
||||
encodeblock(in, out, n);
|
||||
for(c = 0; c < 4; c++) {
|
||||
outbuf[outlen++] = out[c];
|
||||
}
|
||||
}
|
||||
}
|
||||
return outlen;
|
||||
}
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
/*
|
||||
Copyright 2010 BartSimpson aka skindle
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "cbuf.h"
|
||||
|
||||
cbuf *b_new(unsigned int size) {
|
||||
cbuf *b = (cbuf*)calloc(sizeof(cbuf), 1);
|
||||
if (b) {
|
||||
b->buf = (unsigned char *)malloc(size);
|
||||
b->size = b->buf ? size : 0;
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
void b_free(cbuf *b) {
|
||||
if (b) {
|
||||
free(b->buf);
|
||||
free(b);
|
||||
}
|
||||
}
|
||||
|
||||
void b_add_byte(cbuf *b, unsigned char ch) {
|
||||
if (b == NULL) return;
|
||||
if (b->idx == b->size) {
|
||||
unsigned char *p = realloc(b->buf, b->size * 2);
|
||||
if (p) {
|
||||
b->buf = p;
|
||||
b->size = b->size * 2;
|
||||
}
|
||||
}
|
||||
if (b->idx < b->size) {
|
||||
b->buf[b->idx++] = ch;
|
||||
}
|
||||
}
|
||||
|
||||
void b_add_buf(cbuf *b, unsigned char *buf, unsigned int len) {
|
||||
if (b == NULL) return;
|
||||
unsigned int new_sz = b->idx + len;
|
||||
while (b->size < new_sz) {
|
||||
unsigned char *p = realloc(b->buf, b->size * 2);
|
||||
if (p) {
|
||||
b->buf = p;
|
||||
b->size = b->size * 2;
|
||||
}
|
||||
else break;
|
||||
}
|
||||
if ((b->idx + len) <= b->size) {
|
||||
memcpy(b->buf + b->idx, buf, len);
|
||||
b->idx += len;
|
||||
}
|
||||
}
|
||||
|
||||
void b_add_str(cbuf *b, const char *buf) {
|
||||
if (b == NULL) return;
|
||||
unsigned int len = strlen(buf);
|
||||
unsigned int new_sz = b->idx + len;
|
||||
while (b->size < new_sz) {
|
||||
unsigned char *p = realloc(b->buf, b->size * 2);
|
||||
if (p) {
|
||||
b->buf = p;
|
||||
b->size = b->size * 2;
|
||||
}
|
||||
else break;
|
||||
}
|
||||
if ((b->idx + len) <= b->size) {
|
||||
memcpy(b->buf + b->idx, buf, len);
|
||||
b->idx += len;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
/*
|
||||
Copyright 2010 BartSimpson aka skindle
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
#ifndef __CBUF_H
|
||||
#define __CBUF_H
|
||||
|
||||
typedef struct _cbuf {
|
||||
unsigned int size; //current size
|
||||
unsigned int idx; //current position
|
||||
unsigned char *buf;
|
||||
} cbuf;
|
||||
|
||||
cbuf *b_new(unsigned int size);
|
||||
void b_free(cbuf *b);
|
||||
void b_add_byte(cbuf *b, unsigned char ch);
|
||||
void b_add_buf(cbuf *b, unsigned char *buf, unsigned int len);
|
||||
void b_add_str(cbuf *b, const char *buf);
|
||||
|
||||
#endif
|
||||
Binary file not shown.
@@ -1,381 +0,0 @@
|
||||
/*
|
||||
Copyright (C) 1999, 2000, 2002 Aladdin Enterprises. All rights reserved.
|
||||
|
||||
This software is provided 'as-is', without any express or implied
|
||||
warranty. In no event will the authors be held liable for any damages
|
||||
arising from the use of this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any purpose,
|
||||
including commercial applications, and to alter it and redistribute it
|
||||
freely, subject to the following restrictions:
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must not
|
||||
claim that you wrote the original software. If you use this software
|
||||
in a product, an acknowledgment in the product documentation would be
|
||||
appreciated but is not required.
|
||||
2. Altered source versions must be plainly marked as such, and must not be
|
||||
misrepresented as being the original software.
|
||||
3. This notice may not be removed or altered from any source distribution.
|
||||
|
||||
L. Peter Deutsch
|
||||
ghost@aladdin.com
|
||||
|
||||
*/
|
||||
/* $Id: md5.c,v 1.6 2002/04/13 19:20:28 lpd Exp $ */
|
||||
/*
|
||||
Independent implementation of MD5 (RFC 1321).
|
||||
|
||||
This code implements the MD5 Algorithm defined in RFC 1321, whose
|
||||
text is available at
|
||||
http://www.ietf.org/rfc/rfc1321.txt
|
||||
The code is derived from the text of the RFC, including the test suite
|
||||
(section A.5) but excluding the rest of Appendix A. It does not include
|
||||
any code or documentation that is identified in the RFC as being
|
||||
copyrighted.
|
||||
|
||||
The original and principal author of md5.c is L. Peter Deutsch
|
||||
<ghost@aladdin.com>. Other authors are noted in the change history
|
||||
that follows (in reverse chronological order):
|
||||
|
||||
2002-04-13 lpd Clarified derivation from RFC 1321; now handles byte order
|
||||
either statically or dynamically; added missing #include <string.h>
|
||||
in library.
|
||||
2002-03-11 lpd Corrected argument list for main(), and added int return
|
||||
type, in test program and T value program.
|
||||
2002-02-21 lpd Added missing #include <stdio.h> in test program.
|
||||
2000-07-03 lpd Patched to eliminate warnings about "constant is
|
||||
unsigned in ANSI C, signed in traditional"; made test program
|
||||
self-checking.
|
||||
1999-11-04 lpd Edited comments slightly for automatic TOC extraction.
|
||||
1999-10-18 lpd Fixed typo in header comment (ansi2knr rather than md5).
|
||||
1999-05-03 lpd Original version.
|
||||
*/
|
||||
|
||||
#include "md5.h"
|
||||
#include <string.h>
|
||||
|
||||
#undef BYTE_ORDER /* 1 = big-endian, -1 = little-endian, 0 = unknown */
|
||||
#ifdef ARCH_IS_BIG_ENDIAN
|
||||
# define BYTE_ORDER (ARCH_IS_BIG_ENDIAN ? 1 : -1)
|
||||
#else
|
||||
# define BYTE_ORDER 0
|
||||
#endif
|
||||
|
||||
#define T_MASK ((md5_word_t)~0)
|
||||
#define T1 /* 0xd76aa478 */ (T_MASK ^ 0x28955b87)
|
||||
#define T2 /* 0xe8c7b756 */ (T_MASK ^ 0x173848a9)
|
||||
#define T3 0x242070db
|
||||
#define T4 /* 0xc1bdceee */ (T_MASK ^ 0x3e423111)
|
||||
#define T5 /* 0xf57c0faf */ (T_MASK ^ 0x0a83f050)
|
||||
#define T6 0x4787c62a
|
||||
#define T7 /* 0xa8304613 */ (T_MASK ^ 0x57cfb9ec)
|
||||
#define T8 /* 0xfd469501 */ (T_MASK ^ 0x02b96afe)
|
||||
#define T9 0x698098d8
|
||||
#define T10 /* 0x8b44f7af */ (T_MASK ^ 0x74bb0850)
|
||||
#define T11 /* 0xffff5bb1 */ (T_MASK ^ 0x0000a44e)
|
||||
#define T12 /* 0x895cd7be */ (T_MASK ^ 0x76a32841)
|
||||
#define T13 0x6b901122
|
||||
#define T14 /* 0xfd987193 */ (T_MASK ^ 0x02678e6c)
|
||||
#define T15 /* 0xa679438e */ (T_MASK ^ 0x5986bc71)
|
||||
#define T16 0x49b40821
|
||||
#define T17 /* 0xf61e2562 */ (T_MASK ^ 0x09e1da9d)
|
||||
#define T18 /* 0xc040b340 */ (T_MASK ^ 0x3fbf4cbf)
|
||||
#define T19 0x265e5a51
|
||||
#define T20 /* 0xe9b6c7aa */ (T_MASK ^ 0x16493855)
|
||||
#define T21 /* 0xd62f105d */ (T_MASK ^ 0x29d0efa2)
|
||||
#define T22 0x02441453
|
||||
#define T23 /* 0xd8a1e681 */ (T_MASK ^ 0x275e197e)
|
||||
#define T24 /* 0xe7d3fbc8 */ (T_MASK ^ 0x182c0437)
|
||||
#define T25 0x21e1cde6
|
||||
#define T26 /* 0xc33707d6 */ (T_MASK ^ 0x3cc8f829)
|
||||
#define T27 /* 0xf4d50d87 */ (T_MASK ^ 0x0b2af278)
|
||||
#define T28 0x455a14ed
|
||||
#define T29 /* 0xa9e3e905 */ (T_MASK ^ 0x561c16fa)
|
||||
#define T30 /* 0xfcefa3f8 */ (T_MASK ^ 0x03105c07)
|
||||
#define T31 0x676f02d9
|
||||
#define T32 /* 0x8d2a4c8a */ (T_MASK ^ 0x72d5b375)
|
||||
#define T33 /* 0xfffa3942 */ (T_MASK ^ 0x0005c6bd)
|
||||
#define T34 /* 0x8771f681 */ (T_MASK ^ 0x788e097e)
|
||||
#define T35 0x6d9d6122
|
||||
#define T36 /* 0xfde5380c */ (T_MASK ^ 0x021ac7f3)
|
||||
#define T37 /* 0xa4beea44 */ (T_MASK ^ 0x5b4115bb)
|
||||
#define T38 0x4bdecfa9
|
||||
#define T39 /* 0xf6bb4b60 */ (T_MASK ^ 0x0944b49f)
|
||||
#define T40 /* 0xbebfbc70 */ (T_MASK ^ 0x4140438f)
|
||||
#define T41 0x289b7ec6
|
||||
#define T42 /* 0xeaa127fa */ (T_MASK ^ 0x155ed805)
|
||||
#define T43 /* 0xd4ef3085 */ (T_MASK ^ 0x2b10cf7a)
|
||||
#define T44 0x04881d05
|
||||
#define T45 /* 0xd9d4d039 */ (T_MASK ^ 0x262b2fc6)
|
||||
#define T46 /* 0xe6db99e5 */ (T_MASK ^ 0x1924661a)
|
||||
#define T47 0x1fa27cf8
|
||||
#define T48 /* 0xc4ac5665 */ (T_MASK ^ 0x3b53a99a)
|
||||
#define T49 /* 0xf4292244 */ (T_MASK ^ 0x0bd6ddbb)
|
||||
#define T50 0x432aff97
|
||||
#define T51 /* 0xab9423a7 */ (T_MASK ^ 0x546bdc58)
|
||||
#define T52 /* 0xfc93a039 */ (T_MASK ^ 0x036c5fc6)
|
||||
#define T53 0x655b59c3
|
||||
#define T54 /* 0x8f0ccc92 */ (T_MASK ^ 0x70f3336d)
|
||||
#define T55 /* 0xffeff47d */ (T_MASK ^ 0x00100b82)
|
||||
#define T56 /* 0x85845dd1 */ (T_MASK ^ 0x7a7ba22e)
|
||||
#define T57 0x6fa87e4f
|
||||
#define T58 /* 0xfe2ce6e0 */ (T_MASK ^ 0x01d3191f)
|
||||
#define T59 /* 0xa3014314 */ (T_MASK ^ 0x5cfebceb)
|
||||
#define T60 0x4e0811a1
|
||||
#define T61 /* 0xf7537e82 */ (T_MASK ^ 0x08ac817d)
|
||||
#define T62 /* 0xbd3af235 */ (T_MASK ^ 0x42c50dca)
|
||||
#define T63 0x2ad7d2bb
|
||||
#define T64 /* 0xeb86d391 */ (T_MASK ^ 0x14792c6e)
|
||||
|
||||
|
||||
static void
|
||||
md5_process(md5_state_t *pms, const md5_byte_t *data /*[64]*/)
|
||||
{
|
||||
md5_word_t
|
||||
a = pms->abcd[0], b = pms->abcd[1],
|
||||
c = pms->abcd[2], d = pms->abcd[3];
|
||||
md5_word_t t;
|
||||
#if BYTE_ORDER > 0
|
||||
/* Define storage only for big-endian CPUs. */
|
||||
md5_word_t X[16];
|
||||
#else
|
||||
/* Define storage for little-endian or both types of CPUs. */
|
||||
md5_word_t xbuf[16];
|
||||
const md5_word_t *X;
|
||||
#endif
|
||||
|
||||
{
|
||||
#if BYTE_ORDER == 0
|
||||
/*
|
||||
* Determine dynamically whether this is a big-endian or
|
||||
* little-endian machine, since we can use a more efficient
|
||||
* algorithm on the latter.
|
||||
*/
|
||||
static const int w = 1;
|
||||
|
||||
if (*((const md5_byte_t *)&w)) /* dynamic little-endian */
|
||||
#endif
|
||||
#if BYTE_ORDER <= 0 /* little-endian */
|
||||
{
|
||||
/*
|
||||
* On little-endian machines, we can process properly aligned
|
||||
* data without copying it.
|
||||
*/
|
||||
if (!((data - (const md5_byte_t *)0) & 3)) {
|
||||
/* data are properly aligned */
|
||||
X = (const md5_word_t *)data;
|
||||
} else {
|
||||
/* not aligned */
|
||||
memcpy(xbuf, data, 64);
|
||||
X = xbuf;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#if BYTE_ORDER == 0
|
||||
else /* dynamic big-endian */
|
||||
#endif
|
||||
#if BYTE_ORDER >= 0 /* big-endian */
|
||||
{
|
||||
/*
|
||||
* On big-endian machines, we must arrange the bytes in the
|
||||
* right order.
|
||||
*/
|
||||
const md5_byte_t *xp = data;
|
||||
int i;
|
||||
|
||||
# if BYTE_ORDER == 0
|
||||
X = xbuf; /* (dynamic only) */
|
||||
# else
|
||||
# define xbuf X /* (static only) */
|
||||
# endif
|
||||
for (i = 0; i < 16; ++i, xp += 4)
|
||||
xbuf[i] = xp[0] + (xp[1] << 8) + (xp[2] << 16) + (xp[3] << 24);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#define ROTATE_LEFT(x, n) (((x) << (n)) | ((x) >> (32 - (n))))
|
||||
|
||||
/* Round 1. */
|
||||
/* Let [abcd k s i] denote the operation
|
||||
a = b + ((a + F(b,c,d) + X[k] + T[i]) <<< s). */
|
||||
#define F(x, y, z) (((x) & (y)) | (~(x) & (z)))
|
||||
#define SET(a, b, c, d, k, s, Ti)\
|
||||
t = a + F(b,c,d) + X[k] + Ti;\
|
||||
a = ROTATE_LEFT(t, s) + b
|
||||
/* Do the following 16 operations. */
|
||||
SET(a, b, c, d, 0, 7, T1);
|
||||
SET(d, a, b, c, 1, 12, T2);
|
||||
SET(c, d, a, b, 2, 17, T3);
|
||||
SET(b, c, d, a, 3, 22, T4);
|
||||
SET(a, b, c, d, 4, 7, T5);
|
||||
SET(d, a, b, c, 5, 12, T6);
|
||||
SET(c, d, a, b, 6, 17, T7);
|
||||
SET(b, c, d, a, 7, 22, T8);
|
||||
SET(a, b, c, d, 8, 7, T9);
|
||||
SET(d, a, b, c, 9, 12, T10);
|
||||
SET(c, d, a, b, 10, 17, T11);
|
||||
SET(b, c, d, a, 11, 22, T12);
|
||||
SET(a, b, c, d, 12, 7, T13);
|
||||
SET(d, a, b, c, 13, 12, T14);
|
||||
SET(c, d, a, b, 14, 17, T15);
|
||||
SET(b, c, d, a, 15, 22, T16);
|
||||
#undef SET
|
||||
|
||||
/* Round 2. */
|
||||
/* Let [abcd k s i] denote the operation
|
||||
a = b + ((a + G(b,c,d) + X[k] + T[i]) <<< s). */
|
||||
#define G(x, y, z) (((x) & (z)) | ((y) & ~(z)))
|
||||
#define SET(a, b, c, d, k, s, Ti)\
|
||||
t = a + G(b,c,d) + X[k] + Ti;\
|
||||
a = ROTATE_LEFT(t, s) + b
|
||||
/* Do the following 16 operations. */
|
||||
SET(a, b, c, d, 1, 5, T17);
|
||||
SET(d, a, b, c, 6, 9, T18);
|
||||
SET(c, d, a, b, 11, 14, T19);
|
||||
SET(b, c, d, a, 0, 20, T20);
|
||||
SET(a, b, c, d, 5, 5, T21);
|
||||
SET(d, a, b, c, 10, 9, T22);
|
||||
SET(c, d, a, b, 15, 14, T23);
|
||||
SET(b, c, d, a, 4, 20, T24);
|
||||
SET(a, b, c, d, 9, 5, T25);
|
||||
SET(d, a, b, c, 14, 9, T26);
|
||||
SET(c, d, a, b, 3, 14, T27);
|
||||
SET(b, c, d, a, 8, 20, T28);
|
||||
SET(a, b, c, d, 13, 5, T29);
|
||||
SET(d, a, b, c, 2, 9, T30);
|
||||
SET(c, d, a, b, 7, 14, T31);
|
||||
SET(b, c, d, a, 12, 20, T32);
|
||||
#undef SET
|
||||
|
||||
/* Round 3. */
|
||||
/* Let [abcd k s t] denote the operation
|
||||
a = b + ((a + H(b,c,d) + X[k] + T[i]) <<< s). */
|
||||
#define H(x, y, z) ((x) ^ (y) ^ (z))
|
||||
#define SET(a, b, c, d, k, s, Ti)\
|
||||
t = a + H(b,c,d) + X[k] + Ti;\
|
||||
a = ROTATE_LEFT(t, s) + b
|
||||
/* Do the following 16 operations. */
|
||||
SET(a, b, c, d, 5, 4, T33);
|
||||
SET(d, a, b, c, 8, 11, T34);
|
||||
SET(c, d, a, b, 11, 16, T35);
|
||||
SET(b, c, d, a, 14, 23, T36);
|
||||
SET(a, b, c, d, 1, 4, T37);
|
||||
SET(d, a, b, c, 4, 11, T38);
|
||||
SET(c, d, a, b, 7, 16, T39);
|
||||
SET(b, c, d, a, 10, 23, T40);
|
||||
SET(a, b, c, d, 13, 4, T41);
|
||||
SET(d, a, b, c, 0, 11, T42);
|
||||
SET(c, d, a, b, 3, 16, T43);
|
||||
SET(b, c, d, a, 6, 23, T44);
|
||||
SET(a, b, c, d, 9, 4, T45);
|
||||
SET(d, a, b, c, 12, 11, T46);
|
||||
SET(c, d, a, b, 15, 16, T47);
|
||||
SET(b, c, d, a, 2, 23, T48);
|
||||
#undef SET
|
||||
|
||||
/* Round 4. */
|
||||
/* Let [abcd k s t] denote the operation
|
||||
a = b + ((a + I(b,c,d) + X[k] + T[i]) <<< s). */
|
||||
#define I(x, y, z) ((y) ^ ((x) | ~(z)))
|
||||
#define SET(a, b, c, d, k, s, Ti)\
|
||||
t = a + I(b,c,d) + X[k] + Ti;\
|
||||
a = ROTATE_LEFT(t, s) + b
|
||||
/* Do the following 16 operations. */
|
||||
SET(a, b, c, d, 0, 6, T49);
|
||||
SET(d, a, b, c, 7, 10, T50);
|
||||
SET(c, d, a, b, 14, 15, T51);
|
||||
SET(b, c, d, a, 5, 21, T52);
|
||||
SET(a, b, c, d, 12, 6, T53);
|
||||
SET(d, a, b, c, 3, 10, T54);
|
||||
SET(c, d, a, b, 10, 15, T55);
|
||||
SET(b, c, d, a, 1, 21, T56);
|
||||
SET(a, b, c, d, 8, 6, T57);
|
||||
SET(d, a, b, c, 15, 10, T58);
|
||||
SET(c, d, a, b, 6, 15, T59);
|
||||
SET(b, c, d, a, 13, 21, T60);
|
||||
SET(a, b, c, d, 4, 6, T61);
|
||||
SET(d, a, b, c, 11, 10, T62);
|
||||
SET(c, d, a, b, 2, 15, T63);
|
||||
SET(b, c, d, a, 9, 21, T64);
|
||||
#undef SET
|
||||
|
||||
/* Then perform the following additions. (That is increment each
|
||||
of the four registers by the value it had before this block
|
||||
was started.) */
|
||||
pms->abcd[0] += a;
|
||||
pms->abcd[1] += b;
|
||||
pms->abcd[2] += c;
|
||||
pms->abcd[3] += d;
|
||||
}
|
||||
|
||||
void
|
||||
md5_init(md5_state_t *pms)
|
||||
{
|
||||
pms->count[0] = pms->count[1] = 0;
|
||||
pms->abcd[0] = 0x67452301;
|
||||
pms->abcd[1] = /*0xefcdab89*/ T_MASK ^ 0x10325476;
|
||||
pms->abcd[2] = /*0x98badcfe*/ T_MASK ^ 0x67452301;
|
||||
pms->abcd[3] = 0x10325476;
|
||||
}
|
||||
|
||||
void
|
||||
md5_append(md5_state_t *pms, const md5_byte_t *data, int nbytes)
|
||||
{
|
||||
const md5_byte_t *p = data;
|
||||
int left = nbytes;
|
||||
int offset = (pms->count[0] >> 3) & 63;
|
||||
md5_word_t nbits = (md5_word_t)(nbytes << 3);
|
||||
|
||||
if (nbytes <= 0)
|
||||
return;
|
||||
|
||||
/* Update the message length. */
|
||||
pms->count[1] += nbytes >> 29;
|
||||
pms->count[0] += nbits;
|
||||
if (pms->count[0] < nbits)
|
||||
pms->count[1]++;
|
||||
|
||||
/* Process an initial partial block. */
|
||||
if (offset) {
|
||||
int copy = (offset + nbytes > 64 ? 64 - offset : nbytes);
|
||||
|
||||
memcpy(pms->buf + offset, p, copy);
|
||||
if (offset + copy < 64)
|
||||
return;
|
||||
p += copy;
|
||||
left -= copy;
|
||||
md5_process(pms, pms->buf);
|
||||
}
|
||||
|
||||
/* Process full blocks. */
|
||||
for (; left >= 64; p += 64, left -= 64)
|
||||
md5_process(pms, p);
|
||||
|
||||
/* Process a final partial block. */
|
||||
if (left)
|
||||
memcpy(pms->buf, p, left);
|
||||
}
|
||||
|
||||
void
|
||||
md5_finish(md5_state_t *pms, md5_byte_t digest[16])
|
||||
{
|
||||
static const md5_byte_t pad[64] = {
|
||||
0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
|
||||
};
|
||||
md5_byte_t data[8];
|
||||
int i;
|
||||
|
||||
/* Save the length before padding. */
|
||||
for (i = 0; i < 8; ++i)
|
||||
data[i] = (md5_byte_t)(pms->count[i >> 2] >> ((i & 3) << 3));
|
||||
/* Pad to 56 bytes mod 64. */
|
||||
md5_append(pms, pad, ((55 - (pms->count[0] >> 3)) & 63) + 1);
|
||||
/* Append the length. */
|
||||
md5_append(pms, data, 8);
|
||||
for (i = 0; i < 16; ++i)
|
||||
digest[i] = (md5_byte_t)(pms->abcd[i >> 2] >> ((i & 3) << 3));
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
/*
|
||||
Copyright (C) 1999, 2002 Aladdin Enterprises. All rights reserved.
|
||||
|
||||
This software is provided 'as-is', without any express or implied
|
||||
warranty. In no event will the authors be held liable for any damages
|
||||
arising from the use of this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any purpose,
|
||||
including commercial applications, and to alter it and redistribute it
|
||||
freely, subject to the following restrictions:
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must not
|
||||
claim that you wrote the original software. If you use this software
|
||||
in a product, an acknowledgment in the product documentation would be
|
||||
appreciated but is not required.
|
||||
2. Altered source versions must be plainly marked as such, and must not be
|
||||
misrepresented as being the original software.
|
||||
3. This notice may not be removed or altered from any source distribution.
|
||||
|
||||
L. Peter Deutsch
|
||||
ghost@aladdin.com
|
||||
|
||||
*/
|
||||
/* $Id: md5.h,v 1.4 2002/04/13 19:20:28 lpd Exp $ */
|
||||
/*
|
||||
Independent implementation of MD5 (RFC 1321).
|
||||
|
||||
This code implements the MD5 Algorithm defined in RFC 1321, whose
|
||||
text is available at
|
||||
http://www.ietf.org/rfc/rfc1321.txt
|
||||
The code is derived from the text of the RFC, including the test suite
|
||||
(section A.5) but excluding the rest of Appendix A. It does not include
|
||||
any code or documentation that is identified in the RFC as being
|
||||
copyrighted.
|
||||
|
||||
The original and principal author of md5.h is L. Peter Deutsch
|
||||
<ghost@aladdin.com>. Other authors are noted in the change history
|
||||
that follows (in reverse chronological order):
|
||||
|
||||
2002-04-13 lpd Removed support for non-ANSI compilers; removed
|
||||
references to Ghostscript; clarified derivation from RFC 1321;
|
||||
now handles byte order either statically or dynamically.
|
||||
1999-11-04 lpd Edited comments slightly for automatic TOC extraction.
|
||||
1999-10-18 lpd Fixed typo in header comment (ansi2knr rather than md5);
|
||||
added conditionalization for C++ compilation from Martin
|
||||
Purschke <purschke@bnl.gov>.
|
||||
1999-05-03 lpd Original version.
|
||||
*/
|
||||
|
||||
#ifndef md5_INCLUDED
|
||||
# define md5_INCLUDED
|
||||
|
||||
/*
|
||||
* This package supports both compile-time and run-time determination of CPU
|
||||
* byte order. If ARCH_IS_BIG_ENDIAN is defined as 0, the code will be
|
||||
* compiled to run only on little-endian CPUs; if ARCH_IS_BIG_ENDIAN is
|
||||
* defined as non-zero, the code will be compiled to run only on big-endian
|
||||
* CPUs; if ARCH_IS_BIG_ENDIAN is not defined, the code will be compiled to
|
||||
* run on either big- or little-endian CPUs, but will run slightly less
|
||||
* efficiently on either one than if ARCH_IS_BIG_ENDIAN is defined.
|
||||
*/
|
||||
|
||||
typedef unsigned char md5_byte_t; /* 8-bit byte */
|
||||
typedef unsigned int md5_word_t; /* 32-bit word */
|
||||
|
||||
/* Define the state of the MD5 Algorithm. */
|
||||
typedef struct md5_state_s {
|
||||
md5_word_t count[2]; /* message length in bits, lsw first */
|
||||
md5_word_t abcd[4]; /* digest buffer */
|
||||
md5_byte_t buf[64]; /* accumulate block */
|
||||
} md5_state_t;
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C"
|
||||
{
|
||||
#endif
|
||||
|
||||
/* Initialize the algorithm. */
|
||||
void md5_init(md5_state_t *pms);
|
||||
|
||||
/* Append a string to the message. */
|
||||
void md5_append(md5_state_t *pms, const md5_byte_t *data, int nbytes);
|
||||
|
||||
/* Finish the message and return the digest. */
|
||||
void md5_finish(md5_state_t *pms, md5_byte_t digest[16]);
|
||||
|
||||
#ifdef __cplusplus
|
||||
} /* end extern "C" */
|
||||
#endif
|
||||
|
||||
#endif /* md5_INCLUDED */
|
||||
@@ -1,365 +0,0 @@
|
||||
/*
|
||||
Copyright 2010 BartSimpson aka skindle
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#include "mobi.h"
|
||||
|
||||
unsigned char *getExthData(MobiFile *book, unsigned int type, unsigned int *len) {
|
||||
unsigned int i;
|
||||
unsigned int exthRecords = bswap_l(book->exth->recordCount);
|
||||
ExthRecHeader *erh = book->exth->records;
|
||||
|
||||
*len = 0;
|
||||
|
||||
for (i = 0; i < exthRecords; i++) {
|
||||
unsigned int recType = bswap_l(erh->type);
|
||||
unsigned int recLen = bswap_l(erh->len);
|
||||
|
||||
if (recLen < 8) {
|
||||
fprintf(stderr, "Invalid exth record length %d\n", i);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (recType == type) {
|
||||
*len = recLen - 8;
|
||||
return (unsigned char*)(erh + 1);
|
||||
}
|
||||
erh = (ExthRecHeader*)(recLen + (char*)erh);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void enumExthRecords(ExthHeader *eh) {
|
||||
unsigned int exthRecords = bswap_l(eh->recordCount);
|
||||
unsigned int i;
|
||||
unsigned char *data;
|
||||
ExthRecHeader *erh = eh->records;
|
||||
|
||||
for (i = 0; i < exthRecords; i++) {
|
||||
unsigned int recType = bswap_l(erh->type);
|
||||
unsigned int recLen = bswap_l(erh->len);
|
||||
|
||||
fprintf(stderr, "%d: type - %d, len %d\n", i, recType, recLen);
|
||||
|
||||
if (recLen < 8) {
|
||||
fprintf(stderr, "Invalid exth record length %d\n", i);
|
||||
return;
|
||||
}
|
||||
|
||||
data = (unsigned char*)(erh + 1);
|
||||
switch (recType) {
|
||||
case 1: //drm_server_id
|
||||
fprintf(stderr, "drm_server_id: %s\n", data);
|
||||
break;
|
||||
case 2: //drm_commerce_id
|
||||
fprintf(stderr, "drm_commerce_id: %s\n", data);
|
||||
break;
|
||||
case 3: //drm_ebookbase_book_id
|
||||
fprintf(stderr, "drm_ebookbase_book_id: %s\n", data);
|
||||
break;
|
||||
case 100: //author
|
||||
fprintf(stderr, "author: %s\n", data);
|
||||
break;
|
||||
case 101: //publisher
|
||||
fprintf(stderr, "publisher: %s\n", data);
|
||||
break;
|
||||
case 106: //publishingdate
|
||||
fprintf(stderr, "publishingdate: %s\n", data);
|
||||
break;
|
||||
case 113: //asin
|
||||
fprintf(stderr, "asin: %s\n", data);
|
||||
break;
|
||||
case 208: //book unique drm key
|
||||
fprintf(stderr, "book drm key: %s\n", data);
|
||||
break;
|
||||
case 503: //updatedtitle
|
||||
fprintf(stderr, "updatedtitle: %s\n", data);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
erh = (ExthRecHeader*)(recLen + (char*)erh);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//implementation of Pukall Cipher 1
|
||||
unsigned char *PC1(unsigned char *key, unsigned int klen, unsigned char *src,
|
||||
unsigned char *dest, unsigned int len, int decryption) {
|
||||
unsigned int sum1 = 0;
|
||||
unsigned int sum2 = 0;
|
||||
unsigned int keyXorVal = 0;
|
||||
unsigned short wkey[8];
|
||||
unsigned int i;
|
||||
if (klen != 16) {
|
||||
fprintf(stderr, "Bad key length!\n");
|
||||
return NULL;
|
||||
}
|
||||
for (i = 0; i < 8; i++) {
|
||||
wkey[i] = (key[i * 2] << 8) | key[i * 2 + 1];
|
||||
}
|
||||
for (i = 0; i < len; i++) {
|
||||
unsigned int temp1 = 0;
|
||||
unsigned int byteXorVal = 0;
|
||||
unsigned int j, curByte;
|
||||
for (j = 0; j < 8; j++) {
|
||||
temp1 ^= wkey[j];
|
||||
sum2 = (sum2 + j) * 20021 + sum1;
|
||||
sum1 = (temp1 * 346) & 0xFFFF;
|
||||
sum2 = (sum2 + sum1) & 0xFFFF;
|
||||
temp1 = (temp1 * 20021 + 1) & 0xFFFF;
|
||||
byteXorVal ^= temp1 ^ sum2;
|
||||
}
|
||||
curByte = src[i];
|
||||
if (!decryption) {
|
||||
keyXorVal = curByte * 257;
|
||||
}
|
||||
curByte = ((curByte ^ (byteXorVal >> 8)) ^ byteXorVal) & 0xFF;
|
||||
if (decryption) {
|
||||
keyXorVal = curByte * 257;
|
||||
}
|
||||
for (j = 0; j < 8; j++) {
|
||||
wkey[j] ^= keyXorVal;
|
||||
}
|
||||
dest[i] = curByte;
|
||||
}
|
||||
return dest;
|
||||
}
|
||||
|
||||
unsigned int getSizeOfTrailingDataEntry(unsigned char *ptr, unsigned int size) {
|
||||
unsigned int bitpos = 0;
|
||||
unsigned int result = 0;
|
||||
if (size <= 0) {
|
||||
return result;
|
||||
}
|
||||
while (1) {
|
||||
unsigned int v = ptr[size - 1];
|
||||
result |= (v & 0x7F) << bitpos;
|
||||
bitpos += 7;
|
||||
size -= 1;
|
||||
if ((v & 0x80) != 0 || (bitpos >= 28) || (size == 0)) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsigned int getSizeOfTrailingDataEntries(unsigned char *ptr, unsigned int size, unsigned int flags) {
|
||||
unsigned int num = 0;
|
||||
unsigned int testflags = flags >> 1;
|
||||
while (testflags) {
|
||||
if (testflags & 1) {
|
||||
num += getSizeOfTrailingDataEntry(ptr, size - num);
|
||||
}
|
||||
testflags >>= 1;
|
||||
}
|
||||
if (flags & 1) {
|
||||
num += (ptr[size - num - 1] & 0x3) + 1;
|
||||
}
|
||||
return num;
|
||||
}
|
||||
|
||||
unsigned char *parseDRM(unsigned char *data, unsigned int count, unsigned char *pid, unsigned int pidlen) {
|
||||
unsigned int i;
|
||||
unsigned char temp_key_sum = 0;
|
||||
unsigned char *found_key = NULL;
|
||||
unsigned char *keyvec1 = "\x72\x38\x33\xB0\xB4\xF2\xE3\xCA\xDF\x09\x01\xD6\xE2\xE0\x3F\x96";
|
||||
unsigned char temp_key[16];
|
||||
|
||||
memset(temp_key, 0, 16);
|
||||
memcpy(temp_key, pid, 8);
|
||||
PC1(keyvec1, 16, temp_key, temp_key, 16, 0);
|
||||
|
||||
for (i = 0; i < 16; i++) {
|
||||
temp_key_sum += temp_key[i];
|
||||
}
|
||||
|
||||
for (i = 0; i < count; i++) {
|
||||
unsigned char kk[32];
|
||||
vstruct *v = (vstruct*)(data + i * 0x30);
|
||||
kstruct *k = (kstruct*)PC1(temp_key, 16, v->cookie, kk, 32, 1);
|
||||
|
||||
if (v->verification == k->ver && v->cksum[0] == temp_key_sum &&
|
||||
(bswap_l(k->flags) & 0x1F) == 1) {
|
||||
found_key = (unsigned char*)malloc(16);
|
||||
memcpy(found_key, k->finalkey, 16);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return found_key;
|
||||
}
|
||||
|
||||
void freeMobiFile(MobiFile *book) {
|
||||
free(book->hr);
|
||||
free(book->record0);
|
||||
free(book);
|
||||
}
|
||||
|
||||
MobiFile *parseMobiHeader(FILE *f) {
|
||||
unsigned int numRecs, i, magic;
|
||||
MobiFile *book = (MobiFile*)calloc(sizeof(MobiFile), 1);
|
||||
book->f = f;
|
||||
if (fread(&book->pdb, sizeof(PDB), 1, f) != 1) {
|
||||
fprintf(stderr, "Failed to read Palm headers\n");
|
||||
free(book);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
//do BOOKMOBI check
|
||||
if (book->pdb.type != 0x4b4f4f42 || book->pdb.creator != 0x49424f4d) {
|
||||
fprintf(stderr, "Invalid header type or creator\n");
|
||||
free(book);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
book->recs = bswap_s(book->pdb.numRecs);
|
||||
|
||||
book->hr = (HeaderRec*)malloc(book->recs * sizeof(HeaderRec));
|
||||
if (fread(book->hr, sizeof(HeaderRec), book->recs, f) != book->recs) {
|
||||
fprintf(stderr, "Failed read of header record\n");
|
||||
freeMobiFile(book);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
book->record0_offset = bswap_l(book->hr[0].offset);
|
||||
book->record0_size = bswap_l(book->hr[1].offset) - book->record0_offset;
|
||||
|
||||
if (fseek(f, book->record0_offset, SEEK_SET) == -1) {
|
||||
fprintf(stderr, "bad seek to header record offset\n");
|
||||
freeMobiFile(book);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
book->record0 = (unsigned char*)malloc(book->record0_size);
|
||||
|
||||
if (fread(book->record0, book->record0_size, 1, f) != 1) {
|
||||
fprintf(stderr, "bad read of record0\n");
|
||||
freeMobiFile(book);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
book->pdh = (PalmDocHeader*)(book->record0);
|
||||
if (bswap_s(book->pdh->encryptionType) != 2) {
|
||||
fprintf(stderr, "MOBI BOOK is not encrypted\n");
|
||||
freeMobiFile(book);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
book->textRecs = bswap_s(book->pdh->recordCount);
|
||||
|
||||
book->mobi = (MobiHeader*)(book->pdh + 1);
|
||||
if (book->mobi->id != 0x49424f4d) {
|
||||
fprintf(stderr, "MOBI header not found\n");
|
||||
freeMobiFile(book);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
book->mobiLen = bswap_l(book->mobi->hdrLen);
|
||||
book->extra_data_flags = 0;
|
||||
|
||||
if (book->mobiLen >= 0xe4) {
|
||||
book->extra_data_flags = bswap_s(book->mobi->extra_flags);
|
||||
}
|
||||
|
||||
if ((bswap_l(book->mobi->exthFlags) & 0x40) == 0) {
|
||||
fprintf(stderr, "Missing exth header\n");
|
||||
freeMobiFile(book);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
book->exth = (ExthHeader*)(book->mobiLen + (char*)(book->mobi));
|
||||
if (book->exth->id != 0x48545845) {
|
||||
fprintf(stderr, "EXTH header not found\n");
|
||||
freeMobiFile(book);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
//if you want a list of EXTH records, uncomment the following
|
||||
// enumExthRecords(exth);
|
||||
|
||||
book->drmCount = bswap_l(book->mobi->drmCount);
|
||||
|
||||
if (book->drmCount == 0) {
|
||||
fprintf(stderr, "no PIDs found in this file\n");
|
||||
freeMobiFile(book);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return book;
|
||||
}
|
||||
|
||||
int writeMobiOutputFile(MobiFile *book, FILE *out, unsigned char *key,
|
||||
unsigned int drmOffset, unsigned int drm_len) {
|
||||
int i;
|
||||
struct stat statbuf;
|
||||
|
||||
fstat(fileno(book->f), &statbuf);
|
||||
|
||||
// kill the drm keys
|
||||
memset(book->record0 + drmOffset, 0, drm_len);
|
||||
// kill the drm pointers
|
||||
book->mobi->drmOffset = 0xffffffff;
|
||||
book->mobi->drmCount = book->mobi->drmSize = book->mobi->drmFlags = 0;
|
||||
// clear the crypto type
|
||||
book->pdh->encryptionType = 0;
|
||||
|
||||
fwrite(&book->pdb, sizeof(PDB), 1, out);
|
||||
fwrite(book->hr, sizeof(HeaderRec), book->recs, out);
|
||||
fwrite("\x00\x00", 1, 2, out);
|
||||
fwrite(book->record0, book->record0_size, 1, out);
|
||||
|
||||
//need to zero out exth 209 data
|
||||
for (i = 1; i < book->recs; i++) {
|
||||
unsigned int offset = bswap_l(book->hr[i].offset);
|
||||
unsigned int len, extra_size = 0;
|
||||
unsigned char *rec;
|
||||
if (i == (book->recs - 1)) { //last record extends to end of file
|
||||
len = statbuf.st_size - offset;
|
||||
}
|
||||
else {
|
||||
len = bswap_l(book->hr[i + 1].offset) - offset;
|
||||
}
|
||||
//make sure we are at proper offset
|
||||
while (ftell(out) < offset) {
|
||||
fwrite("\x00", 1, 1, out);
|
||||
}
|
||||
rec = (unsigned char *)malloc(len);
|
||||
if (fseek(book->f, offset, SEEK_SET) != 0) {
|
||||
fprintf(stderr, "Failed record seek on input\n");
|
||||
freeMobiFile(book);
|
||||
free(rec);
|
||||
return 0;
|
||||
}
|
||||
if (fread(rec, len, 1, book->f) != 1) {
|
||||
fprintf(stderr, "Failed record read on input\n");
|
||||
freeMobiFile(book);
|
||||
free(rec);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (i <= book->textRecs) { //decrypt if necessary
|
||||
extra_size = getSizeOfTrailingDataEntries(rec, len, book->extra_data_flags);
|
||||
PC1(key, 16, rec, rec, len - extra_size, 1);
|
||||
}
|
||||
fwrite(rec, len, 1, out);
|
||||
free(rec);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
/*
|
||||
Copyright 2010 BartSimpson aka skindle
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
#ifndef __MOBI_H
|
||||
#define __MOBI_H
|
||||
|
||||
#include <stdio.h>
|
||||
#include "skinutils.h"
|
||||
|
||||
#pragma pack(2)
|
||||
typedef struct _PDB {
|
||||
char name[32];
|
||||
unsigned short attrib;
|
||||
unsigned short version;
|
||||
unsigned int created;
|
||||
unsigned int modified;
|
||||
unsigned int backup;
|
||||
unsigned int modNum;
|
||||
unsigned int appInfoID;
|
||||
unsigned int sortInfoID;
|
||||
unsigned int type;
|
||||
unsigned int creator;
|
||||
unsigned int uniqueIDseed;
|
||||
unsigned int nextRecordListID;
|
||||
unsigned short numRecs;
|
||||
} PDB;
|
||||
|
||||
typedef struct _HeaderRec {
|
||||
unsigned int offset;
|
||||
unsigned int attribId;
|
||||
} HeaderRec;
|
||||
|
||||
#define attrib(x) ((x)&0xFF)
|
||||
#define id(x) (bswap_l((x) & 0xFFFFFF00))
|
||||
|
||||
typedef struct _PalmDocHeader {
|
||||
unsigned short compression;
|
||||
unsigned short reserverd1;
|
||||
unsigned int textLength;
|
||||
unsigned short recordCount;
|
||||
unsigned short recordSize;
|
||||
unsigned short encryptionType;
|
||||
unsigned short reserved2;
|
||||
} PalmDocHeader;
|
||||
|
||||
|
||||
//checked lengths are 24, 116, 208, 228
|
||||
typedef struct _MobiHeader {
|
||||
unsigned int id;
|
||||
unsigned int hdrLen;
|
||||
unsigned int type;
|
||||
unsigned int encoding;
|
||||
unsigned int uniqueId;
|
||||
unsigned int generator;
|
||||
unsigned char reserved1[40];
|
||||
unsigned int firstNonBookIdx;
|
||||
unsigned int nameOffset;
|
||||
unsigned int nameLength;
|
||||
unsigned int language;
|
||||
unsigned int inputLang;
|
||||
unsigned int outputLang;
|
||||
unsigned int formatVersion;
|
||||
unsigned int firstImageIdx;
|
||||
unsigned char unknown1[16];
|
||||
unsigned int exthFlags;
|
||||
unsigned char unknown2[36];
|
||||
unsigned int drmOffset;
|
||||
unsigned int drmCount;
|
||||
unsigned int drmSize;
|
||||
unsigned int drmFlags;
|
||||
unsigned char unknown3[58];
|
||||
unsigned short extra_flags;
|
||||
} MobiHeader;
|
||||
|
||||
typedef struct _ExthRecHeader {
|
||||
unsigned int type;
|
||||
unsigned int len;
|
||||
} ExthRecHeader;
|
||||
|
||||
typedef struct _ExthHeader {
|
||||
unsigned int id;
|
||||
unsigned int hdrLen;
|
||||
unsigned int recordCount;
|
||||
ExthRecHeader records[1];
|
||||
} ExthHeader;
|
||||
|
||||
typedef struct _vstruct {
|
||||
unsigned int verification;
|
||||
unsigned int size;
|
||||
unsigned int type;
|
||||
unsigned char cksum[4];
|
||||
unsigned char cookie[32];
|
||||
} vstruct;
|
||||
|
||||
typedef struct _kstruct {
|
||||
unsigned int ver;
|
||||
unsigned int flags;
|
||||
unsigned char finalkey[16];
|
||||
unsigned int expiry;
|
||||
unsigned int expiry2;
|
||||
} kstruct;
|
||||
|
||||
typedef struct _MobiFile {
|
||||
FILE *f;
|
||||
PDB pdb;
|
||||
HeaderRec *hr;
|
||||
PalmDocHeader *pdh;
|
||||
MobiHeader *mobi;
|
||||
ExthHeader *exth;
|
||||
unsigned char *record0;
|
||||
unsigned int record0_offset;
|
||||
unsigned int record0_size;
|
||||
unsigned int mobiLen;
|
||||
unsigned int extra_data_flags;
|
||||
unsigned int recs;
|
||||
unsigned int drmCount;
|
||||
unsigned int textRecs;
|
||||
PidList *pids; //extra pids to try from command line
|
||||
} MobiFile;
|
||||
|
||||
unsigned char *getExthData(MobiFile *book, unsigned int type, unsigned int *len);
|
||||
void enumExthRecords(ExthHeader *eh);
|
||||
unsigned char *PC1(unsigned char *key, unsigned int klen, unsigned char *src,
|
||||
unsigned char *dest, unsigned int len, int decryption);
|
||||
unsigned int getSizeOfTrailingDataEntry(unsigned char *ptr, unsigned int size);
|
||||
unsigned int getSizeOfTrailingDataEntries(unsigned char *ptr, unsigned int size, unsigned int flags);
|
||||
unsigned char *parseDRM(unsigned char *data, unsigned int count, unsigned char *pid, unsigned int pidlen);
|
||||
|
||||
void freeMobiFile(MobiFile *book);
|
||||
MobiFile *parseMobiHeader(FILE *f);
|
||||
int writeMobiOutputFile(MobiFile *book, FILE *out, unsigned char *key,
|
||||
unsigned int drmOffset, unsigned int drm_len);
|
||||
|
||||
#endif
|
||||
@@ -1,234 +0,0 @@
|
||||
/*
|
||||
sha1.c: Implementation of SHA-1 Secure Hash Algorithm-1
|
||||
|
||||
Based upon: NIST FIPS180-1 Secure Hash Algorithm-1
|
||||
http://www.itl.nist.gov/fipspubs/fip180-1.htm
|
||||
|
||||
Non-official Japanese Translation by HIRATA Yasuyuki:
|
||||
http://yasu.asuka.net/translations/SHA-1.html
|
||||
|
||||
Copyright (C) 2002 vi@nwr.jp. All rights reserved.
|
||||
|
||||
This software is provided 'as-is', without any express or implied
|
||||
warranty. In no event will the authors be held liable for any damages
|
||||
arising from the use of this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any purpose,
|
||||
including commercial applications, and to alter it and redistribute it
|
||||
freely, subject to the following restrictions:
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must not
|
||||
claim that you wrote the original software. If you use this software
|
||||
in a product, an acknowledgement in the product documentation would be
|
||||
appreciated but is not required.
|
||||
2. Altered source versions must be plainly marked as such, and must not be
|
||||
misrepresented as beging the original software.
|
||||
3. This notice may not be removed or altered from any source distribution.
|
||||
|
||||
Note:
|
||||
The copyright notice above is copied from md5.h by L. Peter Deutsch
|
||||
<ghost@aladdin.com>. Thank him since I'm not a good speaker of English. :)
|
||||
*/
|
||||
#include <string.h>
|
||||
#include "sha1.h"
|
||||
|
||||
/*
|
||||
* Packing bytes to a word
|
||||
*
|
||||
* Should not assume p is aligned to word boundary
|
||||
*/
|
||||
static sha1_word_t packup(sha1_byte_t *p)
|
||||
{
|
||||
/* Portable, but slow */
|
||||
return p[0] << 24 | p[1] << 16 | p[2] << 8 | p[3] << 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Unpacking a word to bytes
|
||||
*
|
||||
* Should not assume p is aligned to word boundary
|
||||
*/
|
||||
static void unpackup(sha1_byte_t *p, sha1_word_t q)
|
||||
{
|
||||
p[0] = (q >> 24) & 0xff;
|
||||
p[1] = (q >> 16) & 0xff;
|
||||
p[2] = (q >> 8) & 0xff;
|
||||
p[3] = (q >> 0) & 0xff;
|
||||
}
|
||||
|
||||
/*
|
||||
* Processing a block
|
||||
*/
|
||||
static void sha1_update_now(sha1_state_s *pms, sha1_byte_t *bp)
|
||||
{
|
||||
sha1_word_t tmp, a, b, c, d, e, w[16+16];
|
||||
int i, s;
|
||||
|
||||
/* pack 64 bytes into 16 words */
|
||||
for(i = 0; i < 16; i++) {
|
||||
w[i] = packup(bp + i * sizeof(sha1_word_t));
|
||||
}
|
||||
memcpy(w + 16, w + 0, sizeof(sha1_word_t) * 16);
|
||||
|
||||
a = pms->sha1_h[0], b = pms->sha1_h[1], c = pms->sha1_h[2], d = pms->sha1_h[3], e = pms->sha1_h[4];
|
||||
|
||||
#define rot(x,n) (((x) << n) | ((x) >> (32-n)))
|
||||
#define f0(b, c, d) ((b&c)|(~b&d))
|
||||
#define f1(b, c, d) (b^c^d)
|
||||
#define f2(b, c, d) ((b&c)|(b&d)|(c&d))
|
||||
#define f3(b, c, d) (b^c^d)
|
||||
#define k0 0x5a827999
|
||||
#define k1 0x6ed9eba1
|
||||
#define k2 0x8f1bbcdc
|
||||
#define k3 0xca62c1d6
|
||||
|
||||
/* t=0-15 */
|
||||
s = 0;
|
||||
for(i = 0; i < 16; i++) {
|
||||
tmp = rot(a, 5) + f0(b, c, d) + e + w[s] + k0;
|
||||
e = d; d = c; c = rot(b, 30); b = a; a = tmp;
|
||||
s = (s + 1) % 16;
|
||||
}
|
||||
|
||||
/* t=16-19 */
|
||||
for(i = 16; i < 20; i++) {
|
||||
w[s] = rot(w[s+13] ^ w[s+8] ^ w[s+2] ^ w[s], 1);
|
||||
w[s+16] = w[s];
|
||||
tmp = rot(a, 5) + f0(b, c, d) + e + w[s] + k0;
|
||||
e = d; d = c; c = rot(b, 30); b = a; a = tmp;
|
||||
s = (s + 1) % 16;
|
||||
}
|
||||
|
||||
/* t=20-39 */
|
||||
for(i = 0; i < 20; i++) {
|
||||
w[s] = rot(w[s+13] ^ w[s+8] ^ w[s+2] ^ w[s], 1);
|
||||
w[s+16] = w[s];
|
||||
tmp = rot(a, 5) + f1(b, c, d) + e + w[s] + k1;
|
||||
e = d; d = c; c = rot(b, 30); b = a; a = tmp;
|
||||
s = (s + 1) % 16;
|
||||
}
|
||||
|
||||
/* t=40-59 */
|
||||
for(i = 0; i < 20; i++) {
|
||||
w[s] = rot(w[s+13] ^ w[s+8] ^ w[s+2] ^ w[s], 1);
|
||||
w[s+16] = w[s];
|
||||
tmp = rot(a, 5) + f2(b, c, d) + e + w[s] + k2;
|
||||
e = d; d = c; c = rot(b, 30); b = a; a = tmp;
|
||||
s = (s + 1) % 16;
|
||||
}
|
||||
|
||||
/* t=60-79 */
|
||||
for(i = 0; i < 20; i++) {
|
||||
w[s] = rot(w[s+13] ^ w[s+8] ^ w[s+2] ^ w[s], 1);
|
||||
w[s+16] = w[s];
|
||||
tmp = rot(a, 5) + f3(b, c, d) + e + w[s] + k3;
|
||||
e = d; d = c; c = rot(b, 30); b = a; a = tmp;
|
||||
s = (s + 1) % 16;
|
||||
}
|
||||
|
||||
pms->sha1_h[0] += a, pms->sha1_h[1] += b, pms->sha1_h[2] += c, pms->sha1_h[3] += d, pms->sha1_h[4] += e;
|
||||
}
|
||||
|
||||
/*
|
||||
* Increment sha1_size1, sha1_size2 field of sha1_state_s
|
||||
*/
|
||||
static void incr(sha1_state_s *pms, int v)
|
||||
{
|
||||
sha1_word_t q;
|
||||
|
||||
q = pms->sha1_size1 + v * BITS;
|
||||
if(q < pms->sha1_size1) {
|
||||
pms->sha1_size2++;
|
||||
}
|
||||
pms->sha1_size1 = q;
|
||||
}
|
||||
|
||||
/*
|
||||
* Initialize sha1_state_s as FIPS specifies
|
||||
*/
|
||||
void sha1_init(sha1_state_s *pms)
|
||||
{
|
||||
memset(pms, 0, sizeof(*pms));
|
||||
pms->sha1_h[0] = 0x67452301; /* Initialize H[0]-H[4] */
|
||||
pms->sha1_h[1] = 0xEFCDAB89;
|
||||
pms->sha1_h[2] = 0x98BADCFE;
|
||||
pms->sha1_h[3] = 0x10325476;
|
||||
pms->sha1_h[4] = 0xC3D2E1F0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Fill block and update output when needed
|
||||
*/
|
||||
void sha1_update(sha1_state_s *pms, sha1_byte_t *bufp, int length)
|
||||
{
|
||||
/* Is the buffer partially filled? */
|
||||
if(pms->sha1_count != 0) {
|
||||
if(pms->sha1_count + length >= (signed) sizeof(pms->sha1_buf)) { /* buffer is filled enough */
|
||||
int fil = sizeof(pms->sha1_buf) - pms->sha1_count; /* length to copy */
|
||||
|
||||
memcpy(pms->sha1_buf + pms->sha1_count, bufp, fil);
|
||||
sha1_update_now(pms, pms->sha1_buf);
|
||||
length -= fil;
|
||||
bufp += fil;
|
||||
pms->sha1_count = 0;
|
||||
incr(pms, fil);
|
||||
} else {
|
||||
memcpy(pms->sha1_buf + pms->sha1_count, bufp, length);
|
||||
pms->sha1_count += length;
|
||||
incr(pms, length);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/* Loop to update state */
|
||||
for(;;) {
|
||||
if(length < (signed) sizeof(pms->sha1_buf)) { /* Short to fill up the buffer */
|
||||
if(length) {
|
||||
memcpy(pms->sha1_buf, bufp, length);
|
||||
}
|
||||
pms->sha1_count = length;
|
||||
incr(pms, length);
|
||||
break;
|
||||
}
|
||||
sha1_update_now(pms, bufp);
|
||||
length -= sizeof(pms->sha1_buf);
|
||||
bufp += sizeof(pms->sha1_buf);
|
||||
incr(pms, sizeof(pms->sha1_buf));
|
||||
}
|
||||
}
|
||||
|
||||
void sha1_finish(sha1_state_s *pms, sha1_byte_t output[SHA1_OUTPUT_SIZE])
|
||||
{
|
||||
int i;
|
||||
sha1_byte_t buf[1];
|
||||
|
||||
/* fill a bit */
|
||||
buf[0] = 0x80;
|
||||
sha1_update(pms, buf, 1);
|
||||
|
||||
/* Decrement sha1_size1, sha1_size2 */
|
||||
if((pms->sha1_size1 -= BITS) == 0) {
|
||||
pms->sha1_size2--;
|
||||
}
|
||||
|
||||
/* fill zeros */
|
||||
if(pms->sha1_count > (signed) (sizeof(pms->sha1_buf) - 2 * sizeof(sha1_word_t))) {
|
||||
memset(pms->sha1_buf + pms->sha1_count, 0, sizeof(pms->sha1_buf) - pms->sha1_count);
|
||||
sha1_update_now(pms, pms->sha1_buf);
|
||||
pms->sha1_count = 0;
|
||||
}
|
||||
memset(pms->sha1_buf + pms->sha1_count, 0,
|
||||
sizeof(pms->sha1_buf) - pms->sha1_count - sizeof(sha1_word_t) * 2);
|
||||
|
||||
/* fill last length */
|
||||
unpackup(pms->sha1_buf + sizeof(pms->sha1_buf) - sizeof(sha1_word_t) * 2, pms->sha1_size2);
|
||||
unpackup(pms->sha1_buf + sizeof(pms->sha1_buf) - sizeof(sha1_word_t) * 1, pms->sha1_size1);
|
||||
|
||||
/* final update */
|
||||
sha1_update_now(pms, pms->sha1_buf);
|
||||
|
||||
/* move hash value to output byte array */
|
||||
for(i = 0; i < (signed) (sizeof(pms->sha1_h)/sizeof(sha1_word_t)); i++) {
|
||||
unpackup(output + i * sizeof(sha1_word_t), pms->sha1_h[i]);
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
/*
|
||||
sha1.h: Implementation of SHA-1 Secure Hash Algorithm-1
|
||||
|
||||
Based upon: NIST FIPS180-1 Secure Hash Algorithm-1
|
||||
http://www.itl.nist.gov/fipspubs/fip180-1.htm
|
||||
|
||||
Non-official Japanese Translation by HIRATA Yasuyuki:
|
||||
http://yasu.asuka.net/translations/SHA-1.html
|
||||
|
||||
Copyright (C) 2002 vi@nwr.jp. All rights reserved.
|
||||
|
||||
This software is provided 'as-is', without any express or implied
|
||||
warranty. In no event will the authors be held liable for any damages
|
||||
arising from the use of this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any purpose,
|
||||
including commercial applications, and to alter it and redistribute it
|
||||
freely, subject to the following restrictions:
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must not
|
||||
claim that you wrote the original software. If you use this software
|
||||
in a product, an acknowledgement in the product documentation would be
|
||||
appreciated but is not required.
|
||||
2. Altered source versions must be plainly marked as such, and must not be
|
||||
misrepresented as beging the original software.
|
||||
3. This notice may not be removed or altered from any source distribution.
|
||||
|
||||
Note:
|
||||
The copyright notice above is copied from md5.h by L. Petet Deutsch
|
||||
<ghost@aladdin.com>. Thank him since I'm not a good speaker of English. :)
|
||||
*/
|
||||
#ifndef SHA1_H
|
||||
#define SHA1_H
|
||||
|
||||
typedef unsigned int sha1_word_t; /* 32bits unsigned integer */
|
||||
typedef unsigned char sha1_byte_t; /* 8bits unsigned integer */
|
||||
#define BITS 8
|
||||
|
||||
/* Define the state of SHA-1 algorithm */
|
||||
typedef struct {
|
||||
sha1_byte_t sha1_buf[64]; /* 512 bits */
|
||||
int sha1_count; /* How many bytes are used */
|
||||
sha1_word_t sha1_size1; /* Length counter Lower Word */
|
||||
sha1_word_t sha1_size2; /* Length counter Upper Word */
|
||||
sha1_word_t sha1_h[5]; /* Hash output */
|
||||
} sha1_state_s;
|
||||
#define SHA1_OUTPUT_SIZE 20 /* in bytes */
|
||||
|
||||
/* External Functions */
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* Initialize SHA-1 algorithm */
|
||||
void sha1_init(sha1_state_s *pms);
|
||||
|
||||
/* Append a string to SHA-1 algorithm */
|
||||
void sha1_update(sha1_state_s *pms, sha1_byte_t *input_buffer, int length);
|
||||
|
||||
/* Finish the SHA-1 algorithm and return the hash */
|
||||
void sha1_finish(sha1_state_s *pms, sha1_byte_t output[SHA1_OUTPUT_SIZE]);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -1,461 +0,0 @@
|
||||
|
||||
/*
|
||||
Copyright 2010 BartSimpson aka skindle
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Dependencies: none
|
||||
* build on cygwin:
|
||||
* gcc -o skindle skindle.c md5.c sha1.c b64.c -lCrypt32
|
||||
* or gcc -o skindle skindle.c md5.c sha1.c b64.c -lCrypt32 -mno-cygwin
|
||||
* Under cygwin, you can just type make to build it.
|
||||
* The code should compile with Visual Studio, just add all the files to
|
||||
* a project and add the Crypt32.lib dependency and it should build as a
|
||||
* Win32 console app.
|
||||
*/
|
||||
|
||||
/*
|
||||
* MUST be run on the computer on which KindleForPC is installed
|
||||
* under the account that was used to purchase DRM'ed content.
|
||||
* Requires your kindle.info file which can be found in something like:
|
||||
* <User home>\...\Amazon\Kindle For PC\{AMAwzsaPaaZAzmZzZQzgZCAkZ3AjA_AY}
|
||||
* where ... varies by platform but is "Local Settings\Application Data" on XP
|
||||
*/
|
||||
|
||||
/*
|
||||
What: KindleForPC DRM removal utility to preserve your fair use rights!
|
||||
Why: Fair use is a well established doctrine, and I am no fan of vendor
|
||||
lockin.
|
||||
How: This utility implements the PID extraction, DRM key generation and
|
||||
decryption algorithms employed by the KindleForPC application. This
|
||||
is a stand alone app that does not require you to determine a PID on
|
||||
your own, and it does not need to run KindleForPC in order to extract
|
||||
any data from memory.
|
||||
|
||||
Shoutz: The DarkReverser - thanks for mobidedrm! The last part of this
|
||||
is just a C port of mobidedrm.
|
||||
labba and I<3cabbages for motivating me to do this the right way.
|
||||
You guys shouldn't need to spend all your time responding to all the
|
||||
changes Amazon is going to force you to make in unswindle each time
|
||||
the release a new version.
|
||||
Lawrence Lessig - You are my hero. 'Nuff said.
|
||||
Thumbs down: Disney, MPAA, RIAA - you guys suck. Making billions off
|
||||
of the exploitation of works out of copyright while vigourously
|
||||
pushing copyright extension to prevent others from doing the same
|
||||
is the height of hypocrasy.
|
||||
Congress - you guys suck too. Why you arrogant pricks think you
|
||||
are smarter than the founding fathers is beyond me.
|
||||
*/
|
||||
|
||||
#include <windows.h>
|
||||
#include <Wincrypt.h>
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#include "skinutils.h"
|
||||
#include "cbuf.h"
|
||||
#include "mobi.h"
|
||||
#include "tpz.h"
|
||||
|
||||
#include "zlib.h"
|
||||
|
||||
int processTopaz(FILE *in, char *outFile, int explode, PidList *extraPids) {
|
||||
//had to pile all these up here to please VS2009
|
||||
cbuf *tpzHeaders, *tpzBody;
|
||||
struct stat statbuf;
|
||||
FILE *out;
|
||||
unsigned int i;
|
||||
char *keysRecord, *keysRecordRecord;
|
||||
TopazFile *topaz;
|
||||
char *pid;
|
||||
|
||||
fstat(fileno(in), &statbuf);
|
||||
|
||||
topaz = parseTopazHeader(in);
|
||||
if (topaz == NULL) {
|
||||
fprintf(stderr, "Failed to parse topaz headers\n");
|
||||
return 0;
|
||||
}
|
||||
topaz->pids = extraPids;
|
||||
|
||||
tpzHeaders = b_new(topaz->bodyOffset);
|
||||
tpzBody = b_new(statbuf.st_size);
|
||||
|
||||
parseMetadata(topaz);
|
||||
|
||||
// dumpMap(bookMetadata);
|
||||
|
||||
keysRecord = getMetadata(topaz, "keys");
|
||||
if (keysRecord == NULL) {
|
||||
//fail
|
||||
}
|
||||
keysRecordRecord = getMetadata(topaz, keysRecord);
|
||||
if (keysRecordRecord == NULL) {
|
||||
//fail
|
||||
}
|
||||
|
||||
pid = getBookPid(keysRecord, strlen(keysRecord), keysRecordRecord, strlen(keysRecordRecord));
|
||||
|
||||
if (pid == NULL) {
|
||||
fprintf(stderr, "Failed to extract pid automatically\n");
|
||||
}
|
||||
else {
|
||||
char *title = getMetadata(topaz, "Title");
|
||||
fprintf(stderr, "PID for %s is: %s\n", title ? title : "UNK", pid);
|
||||
}
|
||||
|
||||
/*
|
||||
unique pid is computed as:
|
||||
base64(sha1(idArray . kindleToken . 209_data . 209_tokens))
|
||||
*/
|
||||
|
||||
//
|
||||
// Decrypt book key
|
||||
//
|
||||
|
||||
Payload *dkey = getBookPayloadRecord(topaz, "dkey", 0, 0);
|
||||
|
||||
if (dkey == NULL) {
|
||||
fprintf(stderr, "No dkey record found\n");
|
||||
freeTopazFile(topaz);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (pid) {
|
||||
topaz->bookKey = decryptDkeyRecords(dkey, pid);
|
||||
free(pid);
|
||||
}
|
||||
if (topaz->bookKey == NULL) {
|
||||
if (extraPids) {
|
||||
int p;
|
||||
freePayload(dkey);
|
||||
for (p = 0; p < extraPids->numPids; p++) {
|
||||
dkey = getBookPayloadRecord(topaz, "dkey", 0, 0);
|
||||
topaz->bookKey = decryptDkeyRecords(dkey, extraPids->pidList[p]);
|
||||
if (topaz->bookKey) break;
|
||||
}
|
||||
}
|
||||
if (topaz->bookKey == NULL) {
|
||||
fprintf(stderr, "No valid pids available, failed to find DRM key\n");
|
||||
freeTopazFile(topaz);
|
||||
freePayload(dkey);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
fprintf(stderr, "Found a DRM key!\n");
|
||||
for (i = 0; i < 8; i++) {
|
||||
fprintf(stderr, "%02x", topaz->bookKey[i]);
|
||||
}
|
||||
fprintf(stderr, "\n");
|
||||
|
||||
out = fopen(outFile, "wb");
|
||||
if (out == NULL) {
|
||||
fprintf(stderr, "Failed to open output file, quitting\n");
|
||||
freeTopazFile(topaz);
|
||||
freePayload(dkey);
|
||||
return 0;
|
||||
}
|
||||
|
||||
writeTopazOutputFile(topaz, out, tpzHeaders, tpzBody, explode);
|
||||
fwrite(tpzHeaders->buf, tpzHeaders->idx, 1, out);
|
||||
fwrite(tpzBody->buf, tpzBody->idx, 1, out);
|
||||
fclose(out);
|
||||
b_free(tpzHeaders);
|
||||
b_free(tpzBody);
|
||||
|
||||
freePayload(dkey);
|
||||
|
||||
freeTopazFile(topaz);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int processMobi(FILE *prc, char *outFile, PidList *extraPids) {
|
||||
//had to pile all these up here to please VS2009
|
||||
PDB header;
|
||||
cbuf *keyBuf;
|
||||
char *pid;
|
||||
FILE *out;
|
||||
unsigned int i, keyPtrLen;
|
||||
unsigned char *keyPtr;
|
||||
unsigned int drmOffset, drm_len;
|
||||
unsigned char *drm, *found_key = NULL;
|
||||
MobiFile *book;
|
||||
int result;
|
||||
|
||||
book = parseMobiHeader(prc);
|
||||
|
||||
if (book == NULL) {
|
||||
fprintf(stderr, "Failed to read mobi headers\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
book->pids = extraPids;
|
||||
keyPtr = getExthData(book, 209, &keyPtrLen);
|
||||
|
||||
keyBuf = b_new(128);
|
||||
if (keyPtr != NULL) {
|
||||
unsigned int idx;
|
||||
for (idx = 0; idx < keyPtrLen; idx += 5) {
|
||||
unsigned char *rec;
|
||||
unsigned int dlen;
|
||||
unsigned int rtype = bswap_l(*(unsigned int*)(keyPtr + idx + 1));
|
||||
rec = getExthData(book, rtype, &dlen);
|
||||
if (rec != NULL) {
|
||||
b_add_buf(keyBuf, rec, dlen);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pid = getBookPid(keyPtr, keyPtrLen, keyBuf->buf, keyBuf->idx);
|
||||
|
||||
b_free(keyBuf);
|
||||
|
||||
if (pid == NULL) {
|
||||
fprintf(stderr, "Failed to extract pid automatically\n");
|
||||
}
|
||||
else {
|
||||
fprintf(stderr, "PID for %s is: %s\n", book->pdb.name, pid);
|
||||
}
|
||||
|
||||
/*
|
||||
unique pid is computed as:
|
||||
base64(sha1(idArray . kindleToken . 209_data . 209_tokens))
|
||||
*/
|
||||
|
||||
drmOffset = bswap_l(book->mobi->drmOffset);
|
||||
|
||||
drm_len = bswap_l(book->mobi->drmSize);
|
||||
drm = book->record0 + drmOffset;
|
||||
|
||||
if (pid) {
|
||||
found_key = parseDRM(drm, book->drmCount, pid, 8);
|
||||
free(pid);
|
||||
}
|
||||
if (found_key == NULL) {
|
||||
if (extraPids) {
|
||||
int p;
|
||||
for (p = 0; p < extraPids->numPids; p++) {
|
||||
found_key = parseDRM(drm, book->drmCount, extraPids->pidList[p], 8);
|
||||
if (found_key) break;
|
||||
}
|
||||
}
|
||||
if (found_key == NULL) {
|
||||
fprintf(stderr, "No valid pids available, failed to find DRM key\n");
|
||||
freeMobiFile(book);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
fprintf(stderr, "Found a DRM key!\n");
|
||||
|
||||
out = fopen(outFile, "wb");
|
||||
if (out == NULL) {
|
||||
fprintf(stderr, "Failed to open output file, quitting\n");
|
||||
freeMobiFile(book);
|
||||
free(found_key);
|
||||
return 0;
|
||||
}
|
||||
|
||||
result = writeMobiOutputFile(book, out, found_key, drmOffset, drm_len);
|
||||
|
||||
fclose(out);
|
||||
if (result == 0) {
|
||||
_unlink(outFile);
|
||||
}
|
||||
freeMobiFile(book);
|
||||
free(found_key);
|
||||
return result;
|
||||
}
|
||||
|
||||
enum {
|
||||
FileTypeUnk,
|
||||
FileTypeMobi,
|
||||
FileTypeTopaz
|
||||
};
|
||||
|
||||
int getFileType(FILE *in) {
|
||||
PDB p;
|
||||
int type = FileTypeUnk;
|
||||
fseek(in, 0, SEEK_SET);
|
||||
fread(&p, sizeof(p), 1, in);
|
||||
if (p.type == 0x4b4f4f42 && p.creator == 0x49424f4d) {
|
||||
type = FileTypeMobi;
|
||||
}
|
||||
else if (strncmp(p.name, "TPZ0", 4) == 0) {
|
||||
type = FileTypeTopaz;
|
||||
}
|
||||
fseek(in, 0, SEEK_SET);
|
||||
return type;
|
||||
}
|
||||
|
||||
void usage() {
|
||||
fprintf(stderr, "usage: ./skindle [-d] [-v] -i <ebook file> -o <output file> [-k kindle.info file] [-p pid]\n");
|
||||
fprintf(stderr, " -d optional, for topaz files only, produce a decompressed output file\n");
|
||||
fprintf(stderr, " -i required name of the input mobi or topaz file\n");
|
||||
fprintf(stderr, " -o required name of the output file to generate\n");
|
||||
fprintf(stderr, " -k optional kindle.info path\n");
|
||||
fprintf(stderr, " -v dump the contents of kindle.info\n");
|
||||
fprintf(stderr, " -p additional PID values to attempt (can specifiy multiple times)\n");
|
||||
}
|
||||
|
||||
extern char *optarg;
|
||||
extern int optind;
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
//had to pile all these up here to please VS2009
|
||||
FILE *in;
|
||||
int type, explode = 0;
|
||||
int result = 0;
|
||||
int firstArg = 1;
|
||||
int opt;
|
||||
PidList *pids = NULL;
|
||||
char *infile = NULL, *outfile = NULL, *kinfo = NULL;
|
||||
int dump = 0;
|
||||
|
||||
while ((opt = getopt(argc, argv, "vdp:i:o:k:")) != -1) {
|
||||
switch (opt) {
|
||||
case 'v':
|
||||
dump = 1;
|
||||
break;
|
||||
case 'd':
|
||||
explode = 1;
|
||||
break;
|
||||
case 'p': {
|
||||
int l = strlen(optarg);
|
||||
if (l == 10) {
|
||||
if (!verifyPidChecksum(optarg)) {
|
||||
fprintf(stderr, "Invalid pid %s, skipping\n", optarg);
|
||||
break;
|
||||
}
|
||||
optarg[8] = 0;
|
||||
}
|
||||
else if (l != 8) {
|
||||
fprintf(stderr, "Invalid pid length for %s, skipping\n", optarg);
|
||||
break;
|
||||
}
|
||||
if (pids == NULL) {
|
||||
pids = (PidList*)malloc(sizeof(PidList));
|
||||
pids->numPids = 1;
|
||||
pids->pidList[0] = optarg;
|
||||
}
|
||||
else {
|
||||
pids = (PidList*)realloc(pids, sizeof(PidList) + pids->numPids * sizeof(unsigned char*));
|
||||
pids->pidList[pids->numPids++] = optarg;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'k':
|
||||
kinfo = optarg;
|
||||
break;
|
||||
case 'i':
|
||||
infile = optarg;
|
||||
break;
|
||||
case 'o':
|
||||
outfile = optarg;
|
||||
break;
|
||||
default: /* '?' */
|
||||
usage();
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (optind != argc) {
|
||||
fprintf(stderr, "Extra options ignored\n");
|
||||
}
|
||||
|
||||
if (!buildKindleMap(kinfo)) {
|
||||
fprintf(stderr, "buildKindleMap failed\n");
|
||||
usage();
|
||||
exit(1);
|
||||
}
|
||||
|
||||
//The following loop dumps the contents of your kindle.info file
|
||||
if (dump) {
|
||||
MapList *ml;
|
||||
// dumpKindleMap();
|
||||
fprintf(stderr, "\nDumping kindle.info contents:\n");
|
||||
for (ml = kindleMap; ml; ml = ml->next) {
|
||||
DATA_BLOB DataIn;
|
||||
DATA_BLOB DataOut;
|
||||
DataIn.pbData = mazamaDecode(ml->value, (int*)&DataIn.cbData);
|
||||
if (CryptUnprotectData(&DataIn, NULL, NULL, NULL, NULL, 1, &DataOut)) {
|
||||
fprintf(stderr, "%s ==> %s\n", ml->key, translateKindleKey(ml->key));
|
||||
fwrite(DataOut.pbData, DataOut.cbData, 1, stderr);
|
||||
fprintf(stderr, "\n\n");
|
||||
LocalFree(DataOut.pbData);
|
||||
}
|
||||
else {
|
||||
fprintf(stderr, "CryptUnprotectData failed\n");
|
||||
}
|
||||
free(DataIn.pbData);
|
||||
}
|
||||
}
|
||||
|
||||
if (infile == NULL && outfile == NULL) {
|
||||
//special case, user just wants to see kindle.info
|
||||
freeMap(kindleMap);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if (infile == NULL) {
|
||||
fprintf(stderr, "Missing input file name\n");
|
||||
usage();
|
||||
freeMap(kindleMap);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if (outfile == NULL) {
|
||||
fprintf(stderr, "Missing output file name\n");
|
||||
usage();
|
||||
freeMap(kindleMap);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
in = fopen(infile, "rb");
|
||||
if (in == NULL) {
|
||||
fprintf(stderr, "%s bad open, quitting\n", infile);
|
||||
freeMap(kindleMap);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
type = getFileType(in);
|
||||
if (type == FileTypeTopaz) {
|
||||
result = processTopaz(in, outfile, explode, pids);
|
||||
}
|
||||
else if (type == FileTypeMobi) {
|
||||
result = processMobi(in, outfile, pids);
|
||||
}
|
||||
else {
|
||||
fprintf(stderr, "%s file type unknown, quitting\n", infile);
|
||||
fclose(in);
|
||||
freeMap(kindleMap);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
fclose(in);
|
||||
if (result) {
|
||||
fprintf(stderr, "Success! Enjoy!\n");
|
||||
}
|
||||
else {
|
||||
fprintf(stderr, "An error occurred, unable to process input file!\n");
|
||||
}
|
||||
|
||||
freeMap(kindleMap);
|
||||
return 0;
|
||||
}
|
||||
Binary file not shown.
@@ -1,539 +0,0 @@
|
||||
/*
|
||||
Copyright 2010 BartSimpson aka skindle
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
#include <windows.h>
|
||||
#include <Wincrypt.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#include "skinutils.h"
|
||||
|
||||
/* The kindle.info file created when you install KindleForPC is a set
|
||||
* of key:value pairs delimited by '{'. The keys and values are encoded
|
||||
* in a variety of ways. Keys are the mazama64 encoded md5 hash of the
|
||||
* key name, while values are the mazama64 encoding of the blob returned
|
||||
* by the Windows CryptProtectData function. The use of CryptProtectData
|
||||
* is what locks things to a particular user/machine
|
||||
|
||||
* kindle.info layout
|
||||
|
||||
* Key:AbaZZ6z4a7ZxzLzkZcaqauZMZjZ_Ztz6 ("kindle.account.tokens")
|
||||
* Value: mazama64Encode(CryptProtectData(some sha1 hash))
|
||||
|
||||
* Key:AsAWa4ZJAQaCZ7A3zrZSaZavZMarZFAw ("kindle.cookie.item")
|
||||
* Value: mazama64Encode(CryptProtectData(base64(144 bytes of data)))
|
||||
|
||||
* Key:ZHatAla4a-zTzWA-AvaeAvZQzKZ-agAz ("eulaVersionAccepted")
|
||||
* Value: mazama64Encode(CryptProtectData(kindle version?))
|
||||
|
||||
* Key:ZiajZga7Z9zjZRz7AfZ-zRzUANZNZJzP ("login_date")
|
||||
* Value: mazama64Encode(CryptProtectData(registration date))
|
||||
|
||||
* Key:ZkzeAUA-Z2ZYA2Z_ayA_ahZEATaEAOaG ("kindle.token.item")
|
||||
* Value: mazama64Encode(CryptProtectData(multi-field crypto data))
|
||||
* {enc:xxx}{iv:xxx}{key:xxx}{name:xxx}{serial:xxx}
|
||||
* enc:base64(binary blob)
|
||||
* iv:base64(16 bytes)
|
||||
* key:base64(256 bytes)
|
||||
* name:base64("ADPTokenEncryptionKey")
|
||||
* serial:base64("1")
|
||||
|
||||
* Key:aVzrzRAFZ7aIzmASZOzVzIAGAKawzwaU ("login")
|
||||
* Value: mazama64Encode(CryptProtectData(your amazon email))
|
||||
|
||||
* Key:avalzbzkAcAPAQA5ApZgaOZPzQZzaiaO mazama64Encode(md5("MazamaRandomNumber"))
|
||||
* Value: mazama64Encode(CryptProtectData(mazama32Encode(32 bytes random data)))
|
||||
|
||||
* Key:zgACzqAjZ2zzAmAJa6ZFaZALaYAlZrz- ("kindle.key.item")
|
||||
* Value: mazama64Encode(CryptProtectData(RSA private key)) no password
|
||||
|
||||
* Key:zga-aIANZPzbzfZ1zHZWZcA4afZMZcA_ ("kindle.name.info")
|
||||
* Value: mazama64Encode(CryptProtectData(your name))
|
||||
|
||||
* Key:zlZ9afz1AfAVZjacaqa-ZHa1aIa_ajz7 ("kindle.device.info");
|
||||
* Value: mazama64Encode(CryptProtectData(the name of your kindle))
|
||||
*/
|
||||
|
||||
char *kindleKeys[] = {
|
||||
"AbaZZ6z4a7ZxzLzkZcaqauZMZjZ_Ztz6", "kindle.account.tokens",
|
||||
"AsAWa4ZJAQaCZ7A3zrZSaZavZMarZFAw", "kindle.cookie.item",
|
||||
"ZHatAla4a-zTzWA-AvaeAvZQzKZ-agAz", "eulaVersionAccepted",
|
||||
"ZiajZga7Z9zjZRz7AfZ-zRzUANZNZJzP", "login_date",
|
||||
"ZkzeAUA-Z2ZYA2Z_ayA_ahZEATaEAOaG", "kindle.token.item",
|
||||
"aVzrzRAFZ7aIzmASZOzVzIAGAKawzwaU", "login",
|
||||
"avalzbzkAcAPAQA5ApZgaOZPzQZzaiaO", "MazamaRandomNumber",
|
||||
"zgACzqAjZ2zzAmAJa6ZFaZALaYAlZrz-", "kindle.key.item",
|
||||
"zga-aIANZPzbzfZ1zHZWZcA4afZMZcA_", "kindle.name.info",
|
||||
"zlZ9afz1AfAVZjacaqa-ZHa1aIa_ajz7", "kindle.device.info"
|
||||
};
|
||||
|
||||
MapList *kindleMap;
|
||||
|
||||
unsigned short bswap_s(unsigned short s) {
|
||||
return (s >> 8) | (s << 8);
|
||||
}
|
||||
|
||||
unsigned int bswap_l(unsigned int s) {
|
||||
unsigned int u = bswap_s(s);
|
||||
unsigned int l = bswap_s(s >> 16);
|
||||
return (u << 16) | l;
|
||||
}
|
||||
|
||||
char *translateKindleKey(char *key) {
|
||||
int n = sizeof(kindleKeys) / sizeof(char*);
|
||||
int i;
|
||||
for (i = 0; i < n; i += 2) {
|
||||
if (strcmp(key, kindleKeys[i]) == 0) {
|
||||
return kindleKeys[i + 1];
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
MapList *findNode(MapList *map, char *key) {
|
||||
MapList *l;
|
||||
for (l = map; l; l = l->next) {
|
||||
if (strcmp(key, l->key) == 0) {
|
||||
return l;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
MapList *findKindleNode(char *key) {
|
||||
return findNode(kindleMap, key);
|
||||
}
|
||||
|
||||
char *getNodeValue(MapList *map, char *key) {
|
||||
MapList *l;
|
||||
for (l = map; l; l = l->next) {
|
||||
if (strcmp(key, l->key) == 0) {
|
||||
return l->value;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
char *getKindleValue(char *key) {
|
||||
return getNodeValue(kindleMap, key);
|
||||
}
|
||||
|
||||
MapList *addMapNode(MapList *map, char *key, char *value) {
|
||||
MapList *ml;
|
||||
ml = findNode(map, key);
|
||||
if (ml) {
|
||||
free(ml->value);
|
||||
ml->value = value;
|
||||
return map;
|
||||
}
|
||||
else {
|
||||
ml = (MapList*)malloc(sizeof(MapList));
|
||||
ml->key = key;
|
||||
ml->value = value;
|
||||
ml->next = map;
|
||||
return ml;
|
||||
}
|
||||
}
|
||||
|
||||
void dumpMap(MapList *m) {
|
||||
MapList *l;
|
||||
for (l = m; l; l = l->next) {
|
||||
fprintf(stderr, "%s:%s\n", l->key, l->value);
|
||||
}
|
||||
}
|
||||
|
||||
void freeMap(MapList *m) {
|
||||
MapList *n;
|
||||
while (m) {
|
||||
n = m;
|
||||
m = m->next;
|
||||
free(n->key);
|
||||
free(n->value);
|
||||
free(n);
|
||||
}
|
||||
}
|
||||
|
||||
void parseLine(char *line) {
|
||||
char *colon = strchr(line, ':');
|
||||
if (colon) {
|
||||
char *key, *value;
|
||||
int len = colon - line;
|
||||
key = (char*)malloc(len + 1);
|
||||
*colon++ = 0;
|
||||
strcpy(key, line);
|
||||
len = strlen(colon);
|
||||
value = (char*)malloc(len + 1);
|
||||
strcpy(value, colon);
|
||||
value[len] = 0;
|
||||
kindleMap = addMapNode(kindleMap, key, value);
|
||||
}
|
||||
}
|
||||
|
||||
void dumpKindleMap() {
|
||||
dumpMap(kindleMap);
|
||||
}
|
||||
|
||||
int buildKindleMap(char *infoFile) {
|
||||
int result = 0;
|
||||
struct stat statbuf;
|
||||
char ki[512];
|
||||
DWORD len = sizeof(ki);
|
||||
if (infoFile == NULL) {
|
||||
HKEY regkey;
|
||||
fprintf(stderr, "Attempting to locate kindle.info\n");
|
||||
if (RegOpenKey(HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\", ®key) != ERROR_SUCCESS) {
|
||||
fprintf(stderr, "Unable to locate kindle.info, please specify path on command line\n");
|
||||
return result;
|
||||
}
|
||||
|
||||
// if (RegGetValue(regkey, "Local AppData", NULL, NULL, ki, &len) != ERROR_SUCCESS) {
|
||||
if (RegQueryValueEx(regkey, "Local AppData", NULL, NULL, ki, &len) != ERROR_SUCCESS) {
|
||||
RegCloseKey(regkey);
|
||||
fprintf(stderr, "Unable to locate kindle.info, please specify path on command line\n");
|
||||
return result;
|
||||
}
|
||||
ki[len] = 0;
|
||||
strncat(ki, "\\Amazon\\Kindle For PC\\{AMAwzsaPaaZAzmZzZQzgZCAkZ3AjA_AY}\\kindle.info", sizeof(ki) - 1 - strlen(ki));
|
||||
infoFile = ki;
|
||||
fprintf(stderr, "Found kindle.info location\n");
|
||||
}
|
||||
if (stat(infoFile, &statbuf) == 0) {
|
||||
FILE *fd = fopen(infoFile, "rb");
|
||||
char *infoBuf = (char*)malloc(statbuf.st_size + 1);
|
||||
infoBuf[statbuf.st_size] = 0;
|
||||
if (fread(infoBuf, statbuf.st_size, 1, fd) == 1) {
|
||||
char *end = infoBuf + statbuf.st_size;
|
||||
char *b = infoBuf, *e;
|
||||
while (e = strchr(b, '{')) {
|
||||
*e = 0;
|
||||
if ((e - b) > 2) {
|
||||
parseLine(b);
|
||||
}
|
||||
e++;
|
||||
b = e;
|
||||
}
|
||||
if (b < end) {
|
||||
parseLine(b);
|
||||
}
|
||||
}
|
||||
else {
|
||||
fprintf(stderr, "short read on info file\n");
|
||||
}
|
||||
free(infoBuf);
|
||||
fclose(fd);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static unsigned int crc_table[256];
|
||||
|
||||
void png_crc_table_init() {
|
||||
unsigned int i;
|
||||
if (crc_table[255]) return;
|
||||
for (i = 0; i < 256; i++) {
|
||||
unsigned int n = i;
|
||||
unsigned int j;
|
||||
for (j = 0; j < 8; j++) {
|
||||
if (n & 1) {
|
||||
n = 0xEDB88320 ^ (n >> 1);
|
||||
}
|
||||
else {
|
||||
n >>= 1;
|
||||
}
|
||||
}
|
||||
crc_table[i] = n;
|
||||
}
|
||||
}
|
||||
|
||||
unsigned int do_crc(unsigned char *input, unsigned int len) {
|
||||
unsigned int crc = 0;
|
||||
unsigned int i;
|
||||
png_crc_table_init();
|
||||
for (i = 0; i < len; i++) {
|
||||
unsigned int v = (input[i] ^ crc) & 0xff;
|
||||
crc = crc_table[v] ^ (crc >> 8);
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
|
||||
char *decodeString = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789";
|
||||
|
||||
void doPngDecode(unsigned char *input, unsigned int len, unsigned char *output) {
|
||||
// unsigned int crc_table[256];
|
||||
unsigned int crc, i, x = 0;
|
||||
unsigned int *out = (unsigned int*)output;
|
||||
crc = bswap_l(do_crc(input, len));
|
||||
memset(output, 0, 8);
|
||||
for (i = 0; i < len; i++) {
|
||||
output[x++] ^= input[i];
|
||||
if (x == 8) x = 0;
|
||||
}
|
||||
out[0] ^= crc;
|
||||
out[1] ^= crc;
|
||||
for (i = 0; i < 8; i++) {
|
||||
unsigned char v = output[i];
|
||||
output[i] = decodeString[((((v >> 5) & 3) ^ v) & 0x1F) + (v >> 7)];
|
||||
}
|
||||
}
|
||||
|
||||
static char *string_32 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M";
|
||||
static char *string_64 = "AaZzB0bYyCc1XxDdW2wEeVv3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_";
|
||||
|
||||
char *mazamaEncode32(unsigned char *input, unsigned int len) {
|
||||
return mazamaEncode(input, len, 32);
|
||||
}
|
||||
|
||||
char *mazamaEncode64(unsigned char *input, unsigned int len) {
|
||||
return mazamaEncode(input, len, 64);
|
||||
}
|
||||
|
||||
char *mazamaEncode(unsigned char *input, unsigned int len, unsigned char choice) {
|
||||
unsigned int i;
|
||||
char *enc, *out;
|
||||
if (choice == 0x20) enc = string_32;
|
||||
else if (choice == 0x40) enc = string_64;
|
||||
else return NULL;
|
||||
out = (char*)malloc(len * 2 + 1);
|
||||
out[len * 2] = 0;
|
||||
for (i = 0; i < len; i++) {
|
||||
unsigned char v = input[i] + 128;
|
||||
unsigned char q = v / choice;
|
||||
unsigned char m = v % choice;
|
||||
out[i * 2] = enc[q];
|
||||
out[i * 2 + 1] = enc[m];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
unsigned char *mazamaDecode(char *input, int *outlen) {
|
||||
unsigned char *out;
|
||||
int len = strlen(input);
|
||||
char *dec = NULL;
|
||||
int i, choice = 0x20;
|
||||
*outlen = 0;
|
||||
for (i = 0; i < 8 && i < len; i++) {
|
||||
if (*input == string_32[i]) {
|
||||
dec = string_32;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (dec == NULL) {
|
||||
for (i = 0; i < 4 && i < len; i++) {
|
||||
if (*input == string_64[i]) {
|
||||
dec = string_64;
|
||||
choice = 0x40;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (dec == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
out = (unsigned char*)malloc(len / 2 + 1);
|
||||
out[len / 2] = 0;
|
||||
for (i = 0; i < len; i += 2) {
|
||||
int q, m, v;
|
||||
char *p = strchr(dec, input[i]);
|
||||
if (p == NULL) break;
|
||||
q = p - dec;
|
||||
p = strchr(dec, input[i + 1]);
|
||||
if (p == NULL) break;
|
||||
m = p - dec;
|
||||
v = (choice * q + m) - 128;
|
||||
out[(*outlen)++] = (unsigned char)v;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
#ifndef HEADER_MD5_H
|
||||
|
||||
void md5(unsigned char *in, int len, unsigned char *md) {
|
||||
MD5_CTX s;
|
||||
MD5_Init(&s);
|
||||
MD5_Update(&s, in, len);
|
||||
MD5_Final(md, &s);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifndef HEADER_SHA_H
|
||||
|
||||
void sha1(unsigned char *in, int len, unsigned char *md) {
|
||||
SHA_CTX s;
|
||||
SHA1_Init(&s);
|
||||
SHA1_Update(&s, in, len);
|
||||
SHA1_Final(md, &s);
|
||||
}
|
||||
#endif
|
||||
|
||||
char *getBookPid(unsigned char *keys, unsigned int klen, unsigned char *keysValue, unsigned int kvlen) {
|
||||
unsigned char *vsn, *username, *mrn_key, *kat_key, *pid;
|
||||
char drive[256];
|
||||
char name[256];
|
||||
DWORD nlen = sizeof(name);
|
||||
char *d;
|
||||
char volumeName[256];
|
||||
DWORD volumeSerialNumber;
|
||||
char fileSystemNameBuffer[256];
|
||||
char volumeID[32];
|
||||
unsigned char md5sum[MD5_DIGEST_LENGTH];
|
||||
unsigned char sha1sum[SHA_DIGEST_LENGTH];
|
||||
SHA_CTX sha1_ctx;
|
||||
char *mv;
|
||||
|
||||
if (GetUserName(name, &nlen) == 0) {
|
||||
fprintf(stderr, "GetUserName failed\n");
|
||||
return NULL;
|
||||
}
|
||||
fprintf(stderr, "Using UserName = \"%s\"\n", name);
|
||||
|
||||
d = getenv("SystemDrive");
|
||||
if (d) {
|
||||
strcpy(drive, d);
|
||||
strcat(drive, "\\");
|
||||
}
|
||||
else {
|
||||
strcpy(drive, "c:\\");
|
||||
}
|
||||
fprintf(stderr, "Using SystemDrive = \"%s\"\n", drive);
|
||||
if (GetVolumeInformation(drive, volumeName, sizeof(volumeName), &volumeSerialNumber,
|
||||
NULL, NULL, fileSystemNameBuffer, sizeof(fileSystemNameBuffer))) {
|
||||
sprintf(volumeID, "%u", volumeSerialNumber);
|
||||
}
|
||||
else {
|
||||
strcpy(volumeID, "9999999999");
|
||||
}
|
||||
fprintf(stderr, "Using VolumeSerialNumber = \"%s\"\n", volumeID);
|
||||
MD5(volumeID, strlen(volumeID), md5sum);
|
||||
vsn = mazamaEncode(md5sum, MD5_DIGEST_LENGTH, 0x20);
|
||||
|
||||
MD5(name, strlen(name), md5sum);
|
||||
username = mazamaEncode(md5sum, MD5_DIGEST_LENGTH, 0x20);
|
||||
|
||||
MD5("MazamaRandomNumber", 18, md5sum);
|
||||
mrn_key = mazamaEncode(md5sum, MD5_DIGEST_LENGTH, 0x40);
|
||||
|
||||
MD5("kindle.account.tokens", 21, md5sum);
|
||||
kat_key = mazamaEncode(md5sum, MD5_DIGEST_LENGTH, 0x40);
|
||||
|
||||
SHA1_Init(&sha1_ctx);
|
||||
|
||||
mv = getKindleValue(mrn_key);
|
||||
if (mv) {
|
||||
DATA_BLOB DataIn;
|
||||
DATA_BLOB DataOut;
|
||||
DataIn.pbData = mazamaDecode(mv, (int*)&DataIn.cbData);
|
||||
if (CryptUnprotectData(&DataIn, NULL, NULL, NULL, NULL, 1, &DataOut)) {
|
||||
char *devId = (char*)malloc(DataOut.cbData + 4 * MD5_DIGEST_LENGTH + 1);
|
||||
char *finalDevId;
|
||||
unsigned char pidbuf[10];
|
||||
|
||||
// fprintf(stderr, "CryptUnprotectData success\n");
|
||||
// fwrite(DataOut.pbData, DataOut.cbData, 1, stderr);
|
||||
// fprintf(stderr, "\n");
|
||||
|
||||
memcpy(devId, DataOut.pbData, DataOut.cbData);
|
||||
strcpy(devId + DataOut.cbData, vsn);
|
||||
strcat(devId + DataOut.cbData, username);
|
||||
|
||||
// fprintf(stderr, "Computing sha1 over %d bytes\n", DataOut.cbData + 4 * MD5_DIGEST_LENGTH);
|
||||
sha1(devId, DataOut.cbData + 4 * MD5_DIGEST_LENGTH, sha1sum);
|
||||
finalDevId = mazamaEncode(sha1sum, SHA_DIGEST_LENGTH, 0x20);
|
||||
// fprintf(stderr, "finalDevId: %s\n", finalDevId);
|
||||
|
||||
SHA1_Update(&sha1_ctx, finalDevId, strlen(finalDevId));
|
||||
|
||||
pidbuf[8] = 0;
|
||||
doPngDecode(finalDevId, 4, (unsigned char*)pidbuf);
|
||||
fprintf(stderr, "Device PID: %s\n", pidbuf);
|
||||
|
||||
LocalFree(DataOut.pbData);
|
||||
free(devId);
|
||||
free(finalDevId);
|
||||
}
|
||||
else {
|
||||
fprintf(stderr, "CryptUnprotectData failed, quitting\n");
|
||||
free(kat_key);
|
||||
free(mrn_key);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
free(DataIn.pbData);
|
||||
}
|
||||
else {
|
||||
fprintf(stderr, "Failed to find map node: %s\n", mrn_key);
|
||||
}
|
||||
|
||||
mv = getKindleValue(kat_key);
|
||||
if (mv) {
|
||||
DATA_BLOB DataIn;
|
||||
DATA_BLOB DataOut;
|
||||
DataIn.pbData = mazamaDecode(mv, (int*)&DataIn.cbData);
|
||||
if (CryptUnprotectData(&DataIn, NULL, NULL, NULL, NULL, 1, &DataOut)) {
|
||||
// fprintf(stderr, "CryptUnprotectData success\n");
|
||||
// fwrite(DataOut.pbData, DataOut.cbData, 1, stderr);
|
||||
// fprintf(stderr, "\n");
|
||||
|
||||
SHA1_Update(&sha1_ctx, DataOut.pbData, DataOut.cbData);
|
||||
|
||||
LocalFree(DataOut.pbData);
|
||||
}
|
||||
else {
|
||||
fprintf(stderr, "CryptUnprotectData failed, quitting\n");
|
||||
free(kat_key);
|
||||
free(mrn_key);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
free(DataIn.pbData);
|
||||
}
|
||||
else {
|
||||
fprintf(stderr, "Failed to find map node: %s\n", kat_key);
|
||||
}
|
||||
|
||||
SHA1_Update(&sha1_ctx, keys, klen);
|
||||
SHA1_Update(&sha1_ctx, keysValue, kvlen);
|
||||
SHA1_Final(sha1sum, &sha1_ctx);
|
||||
|
||||
pid = (char*)malloc(SHA_DIGEST_LENGTH * 2);
|
||||
base64(sha1sum, SHA_DIGEST_LENGTH, pid);
|
||||
|
||||
pid[8] = 0;
|
||||
|
||||
free(mrn_key);
|
||||
free(kat_key);
|
||||
free(vsn);
|
||||
free(username);
|
||||
|
||||
return pid;
|
||||
}
|
||||
|
||||
static char *letters = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789";
|
||||
|
||||
int verifyPidChecksum(char *pid) {
|
||||
int l = strlen(letters);
|
||||
unsigned int crc = ~do_crc(pid, 8);
|
||||
unsigned char b;
|
||||
crc = crc ^ (crc >> 16);
|
||||
b = crc & 0xff;
|
||||
if (pid[8] != letters[((b / l) ^ (b % l)) % l]) return 0;
|
||||
crc >>= 8;
|
||||
b = crc & 0xff;
|
||||
if (pid[9] != letters[((b / l) ^ (b % l)) % l]) return 0;
|
||||
return 1;
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
/*
|
||||
Copyright 2010 BartSimpson aka skindle
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
#ifndef __SKINUTILS_H
|
||||
#define __SKINUTILS_H
|
||||
|
||||
typedef struct _PidList {
|
||||
unsigned int numPids;
|
||||
char *pidList[1]; //extra pids to try from command line
|
||||
} PidList;
|
||||
|
||||
typedef struct _MapList {
|
||||
char *key;
|
||||
char *value;
|
||||
struct _MapList *next;
|
||||
} MapList;
|
||||
|
||||
extern MapList *kindleMap;
|
||||
|
||||
unsigned int base64(unsigned char *inbuf, unsigned int len, unsigned char *outbuf);
|
||||
|
||||
unsigned short bswap_s(unsigned short s);
|
||||
unsigned int bswap_l(unsigned int s);
|
||||
|
||||
char *translateKindleKey(char *key);
|
||||
MapList *findNode(MapList *map, char *key);
|
||||
MapList *findKindleNode(char *key);
|
||||
|
||||
//don't free the result of getNodeValue;
|
||||
char *getNodeValue(MapList *map, char *key);
|
||||
char *getKindleValue(char *key);
|
||||
|
||||
MapList *addMapNode(MapList *map, char *key, char *value);
|
||||
void dumpMap(MapList *m);
|
||||
|
||||
void freeMap(MapList *m);
|
||||
|
||||
int buildKindleMap(char *infoFile);
|
||||
void dumpKindleMap();
|
||||
|
||||
//void png_crc_table_init(unsigned int *crc_table);
|
||||
unsigned int do_crc(unsigned char *input, unsigned int len);
|
||||
void doPngDecode(unsigned char *input, unsigned int len, unsigned char *output);
|
||||
|
||||
char *mazamaEncode(unsigned char *input, unsigned int len, unsigned char choice);
|
||||
char *mazamaEncode32(unsigned char *input, unsigned int len);
|
||||
char *mazamaEncode64(unsigned char *input, unsigned int len);
|
||||
|
||||
unsigned char *mazamaDecode(char *input, int *outlen);
|
||||
|
||||
int verifyPidChecksum(char *pid);
|
||||
|
||||
//If you prefer to use openssl uncomment the following
|
||||
//#include <openssl/sha.h>
|
||||
//#include <openssl/md5.h>
|
||||
|
||||
#ifndef HEADER_MD5_H
|
||||
#include "md5.h"
|
||||
|
||||
#define MD5_DIGEST_LENGTH 16
|
||||
|
||||
#define MD5_CTX md5_state_t
|
||||
#define MD5_Init md5_init
|
||||
#define MD5_Update md5_append
|
||||
#define MD5_Final(x, y) md5_finish(y, x)
|
||||
#define MD5 md5
|
||||
|
||||
void md5(unsigned char *in, int len, unsigned char *md);
|
||||
#endif
|
||||
|
||||
#ifndef HEADER_SHA_H
|
||||
|
||||
#include "sha1.h"
|
||||
|
||||
#define SHA_DIGEST_LENGTH 20
|
||||
#define SHA_CTX sha1_state_s
|
||||
#define SHA1_Init sha1_init
|
||||
#define SHA1_Update sha1_update
|
||||
#define SHA1_Final(x, y) sha1_finish(y, x)
|
||||
#define SHA1 sha1
|
||||
|
||||
void sha1(unsigned char *in, int len, unsigned char *md);
|
||||
#endif
|
||||
|
||||
char *getBookPid(unsigned char *keys, unsigned int klen, unsigned char *keysValue, unsigned int kvlen);
|
||||
|
||||
#endif
|
||||
@@ -1,504 +0,0 @@
|
||||
/*
|
||||
Copyright 2010 BartSimpson aka skindle
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "skinutils.h"
|
||||
#include "cbuf.h"
|
||||
#include "tpz.h"
|
||||
#include "zlib.h"
|
||||
|
||||
//
|
||||
// Context initialisation for the Topaz Crypto
|
||||
//
|
||||
void topazCryptoInit(TpzCtx *ctx, unsigned char *key, int klen) {
|
||||
int i = 0;
|
||||
ctx->v[0] = 0x0CAFFE19E;
|
||||
|
||||
for (i = 0; i < klen; i++) {
|
||||
ctx->v[1] = ctx->v[0];
|
||||
ctx->v[0] = ((ctx->v[0] >> 2) * (ctx->v[0] >> 7)) ^
|
||||
(key[i] * key[i] * 0x0F902007);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// decrypt data with the context prepared by topazCryptoInit()
|
||||
//
|
||||
|
||||
void topazCryptoDecrypt(TpzCtx *ctx, unsigned char *in, unsigned char *out, int len) {
|
||||
unsigned int ctx1 = ctx->v[0];
|
||||
unsigned int ctx2 = ctx->v[1];
|
||||
int i;
|
||||
for (i = 0; i < len; i++) {
|
||||
unsigned char m = in[i] ^ (ctx1 >> 3) ^ (ctx2 << 3);
|
||||
ctx2 = ctx1;
|
||||
ctx1 = ((ctx1 >> 2) * (ctx1 >> 7)) ^ (m * m * 0x0F902007);
|
||||
out[i] = m;
|
||||
}
|
||||
}
|
||||
|
||||
int bookReadEncodedNumber(FILE *f) {
|
||||
int flag = 0;
|
||||
int data = fgetc(f);
|
||||
if (data == 0xFF) { //negative number flag
|
||||
flag = 1;
|
||||
data = fgetc(f);
|
||||
}
|
||||
if (data >= 0x80) {
|
||||
int datax = data & 0x7F;
|
||||
while (data >= 0x80) {
|
||||
data = fgetc(f);
|
||||
datax = (datax << 7) + (data & 0x7F);
|
||||
}
|
||||
data = datax;
|
||||
}
|
||||
|
||||
if (flag) {
|
||||
data = -data;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
//
|
||||
// Encode a number in 7 bit format
|
||||
//
|
||||
|
||||
int encodeNumber(int number, unsigned char *out) {
|
||||
unsigned char *b = out;
|
||||
unsigned char flag = 0;
|
||||
int len;
|
||||
int neg = number < 0;
|
||||
|
||||
if (neg) {
|
||||
number = -number + 1;
|
||||
}
|
||||
|
||||
do {
|
||||
*b++ = (number & 0x7F) | flag;
|
||||
number >>= 7;
|
||||
flag = 0x80;
|
||||
} while (number);
|
||||
|
||||
if (neg) {
|
||||
*b++ = 0xFF;
|
||||
}
|
||||
len = b - out;
|
||||
b--;
|
||||
while (out < b) {
|
||||
unsigned char v = *out;
|
||||
*out++ = *b;
|
||||
*b-- = v;
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
//
|
||||
// Get a length prefixed string from the file
|
||||
//
|
||||
|
||||
char *bookReadString(FILE *f) {
|
||||
int len = bookReadEncodedNumber(f);
|
||||
char *s = (char*)malloc(len + 1);
|
||||
s[len] = 0;
|
||||
if (fread(s, 1, len, f) != len) {
|
||||
fprintf(stderr, "String read failed at filepos %x\n", ftell(f));
|
||||
free(s);
|
||||
s = NULL;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
//
|
||||
// Read and return the data of one header record at the current book file position [[offset,compressedLength,decompressedLength],...]
|
||||
//
|
||||
|
||||
Record *bookReadHeaderRecordData(FILE *f) {
|
||||
int nbValues = bookReadEncodedNumber(f);
|
||||
Record *result = NULL;
|
||||
Record *tail = NULL;
|
||||
unsigned int i;
|
||||
if (nbValues == -1) {
|
||||
fprintf(stderr, "Parse Error : EOF encountered\n");
|
||||
return NULL;
|
||||
}
|
||||
for (i = 0; i < nbValues; i++) {
|
||||
Record *r = (Record*)malloc(sizeof(Record));
|
||||
r->offset = bookReadEncodedNumber(f);
|
||||
r->length = bookReadEncodedNumber(f);
|
||||
r->compressed = bookReadEncodedNumber(f);
|
||||
r->next = NULL;
|
||||
if (result == NULL) {
|
||||
result = r;
|
||||
}
|
||||
else {
|
||||
tail->next = r;
|
||||
}
|
||||
tail = r;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
//
|
||||
// Read and parse one header record at the current book file position and return the associated data [[offset,compressedLength,decompressedLength],...]
|
||||
//
|
||||
|
||||
void freeRecordList(Record *r) {
|
||||
Record *n;
|
||||
while (r) {
|
||||
n = r;
|
||||
r = r->next;
|
||||
free(n);
|
||||
}
|
||||
}
|
||||
|
||||
void freeHeaderList(HeaderRecord *r) {
|
||||
HeaderRecord *n;
|
||||
while (r) {
|
||||
free(r->tag);
|
||||
freeRecordList(r->rec);
|
||||
n = r;
|
||||
r = r->next;
|
||||
free(n);
|
||||
}
|
||||
}
|
||||
|
||||
void freeTopazFile(TopazFile *t) {
|
||||
freeHeaderList(t->hdrs);
|
||||
freeMap(t->metadata);
|
||||
free(t);
|
||||
}
|
||||
|
||||
HeaderRecord *parseTopazHeaderRecord(FILE *f) {
|
||||
char *tag;
|
||||
Record *record;
|
||||
if (fgetc(f) != 0x63) {
|
||||
fprintf(stderr, "Parse Error : Invalid Header at 0x%x\n", ftell(f) - 1);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
tag = bookReadString(f);
|
||||
record = bookReadHeaderRecordData(f);
|
||||
if (tag && record) {
|
||||
HeaderRecord *r = (HeaderRecord*)malloc(sizeof(Record));
|
||||
r->tag = tag;
|
||||
r->rec = record;
|
||||
r->next = NULL;
|
||||
return r;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
//
|
||||
// Parse the header of a Topaz file, get all the header records and the offset for the payload
|
||||
//
|
||||
|
||||
HeaderRecord *addRecord(HeaderRecord *head, HeaderRecord *r) {
|
||||
HeaderRecord *i;
|
||||
for (i = head; i; i = i->next) {
|
||||
if (i->next == NULL) {
|
||||
i->next = r;
|
||||
return head;
|
||||
}
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
TopazFile *parseTopazHeader(FILE *f) {
|
||||
unsigned int numRecs, i, magic;
|
||||
TopazFile *tpz;
|
||||
if (fread(&magic, sizeof(magic), 1, f) != 1) {
|
||||
fprintf(stderr, "Failed to read file magic\n");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (magic != 0x305a5054) {
|
||||
fprintf(stderr, "Parse Error : Invalid Header, not a Topaz file");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
numRecs = fgetc(f);
|
||||
|
||||
tpz = (TopazFile*)calloc(sizeof(TopazFile), 1);
|
||||
tpz->f = f;
|
||||
|
||||
for (i = 0; i < numRecs; i++) {
|
||||
HeaderRecord *result = parseTopazHeaderRecord(f);
|
||||
if (result == NULL) {
|
||||
break;
|
||||
}
|
||||
tpz->hdrs = addRecord(tpz->hdrs, result);
|
||||
}
|
||||
|
||||
if (fgetc(f) != 0x64) {
|
||||
fprintf(stderr, "Parse Error : Invalid Header end at pos 0x%x\n", ftell(f) - 1);
|
||||
//empty list
|
||||
freeTopazFile(tpz);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
tpz->bodyOffset = ftell(f);
|
||||
return tpz;
|
||||
}
|
||||
|
||||
HeaderRecord *findHeader(TopazFile *tpz, char *tag) {
|
||||
HeaderRecord *hr;
|
||||
for (hr = tpz->hdrs; hr; hr = hr->next) {
|
||||
if (strcmp(hr->tag, tag) == 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return hr;
|
||||
}
|
||||
|
||||
void freePayload(Payload *p) {
|
||||
free(p->blob);
|
||||
free(p);
|
||||
}
|
||||
|
||||
//
|
||||
//Get a record in the book payload, given its name and index. If necessary the record is decrypted. The record is not decompressed
|
||||
//
|
||||
Payload *getBookPayloadRecord(TopazFile *t, char *name, int index, int explode) {
|
||||
int encrypted = 0;
|
||||
int recordOffset, i, recordIndex;
|
||||
Record *r;
|
||||
int fileSize;
|
||||
char *tag;
|
||||
Payload *p;
|
||||
off_t fileOffset;
|
||||
HeaderRecord *hr = findHeader(t, name);
|
||||
|
||||
if (hr == NULL) {
|
||||
fprintf(stderr, "Parse Error : Invalid Record, record %s not found\n", name);
|
||||
return NULL;
|
||||
}
|
||||
r = hr->rec;
|
||||
for (i = 0; r && i < index; i++) {
|
||||
r = r->next;
|
||||
}
|
||||
if (r == NULL) {
|
||||
fprintf(stderr, "Parse Error : Invalid Record, record %s:%d not found\n", name, index);
|
||||
return NULL;
|
||||
}
|
||||
recordOffset = r->offset;
|
||||
|
||||
if (fseek(t->f, t->bodyOffset + recordOffset, SEEK_SET) == -1) {
|
||||
fprintf(stderr, "Parse Error : Invalid Record offset, record %s:%d\n", name, index);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
tag = bookReadString(t->f);
|
||||
if (strcmp(tag, name)) {
|
||||
fprintf(stderr, "Parse Error : Invalid Record offset, record %s:%d name doesn't match\n", name, index);
|
||||
return NULL;
|
||||
}
|
||||
recordIndex = bookReadEncodedNumber(t->f);
|
||||
|
||||
if (recordIndex < 0) {
|
||||
encrypted = 1;
|
||||
recordIndex = -recordIndex - 1;
|
||||
}
|
||||
|
||||
if (recordIndex != index) {
|
||||
fprintf(stderr, "Parse Error : Invalid Record offset, record %s:%d index doesn't match\n", name, index);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
fileSize = r->compressed ? r->compressed : r->length;
|
||||
p = (Payload*)malloc(sizeof(Payload));
|
||||
p->blob = (unsigned char*)malloc(fileSize);
|
||||
p->len = fileSize;
|
||||
p->name = name;
|
||||
p->index = index;
|
||||
fileOffset = ftell(t->f);
|
||||
if (fread(p->blob, fileSize, 1, t->f) != 1) {
|
||||
freePayload(p);
|
||||
fprintf(stderr, "Parse Error : Failed payload read of record %s:%d offset 0x%x:0x%x\n", name, index, fileOffset, fileSize);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (encrypted) {
|
||||
TpzCtx ctx;
|
||||
topazCryptoInit(&ctx, t->bookKey, 8);
|
||||
topazCryptoDecrypt(&ctx, p->blob, p->blob, p->len);
|
||||
}
|
||||
|
||||
if (r->compressed && explode) {
|
||||
unsigned char *db = (unsigned char *)malloc(r->length);
|
||||
uLongf dl = r->length;
|
||||
switch (uncompress(db, &dl, p->blob, p->len)) {
|
||||
case Z_OK:
|
||||
free(p->blob);
|
||||
p->blob = db;
|
||||
p->len = dl;
|
||||
break;
|
||||
case Z_MEM_ERROR:
|
||||
free(db);
|
||||
fprintf(stderr, "out of memory\n");
|
||||
break;
|
||||
case Z_BUF_ERROR:
|
||||
free(db);
|
||||
fprintf(stderr, "output buffer wasn't large enough!\n");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
//
|
||||
// Parse the metadata record from the book payload and return a list of [key,values]
|
||||
//
|
||||
|
||||
char *getMetadata(TopazFile *t, char *key) {
|
||||
return getNodeValue(t->metadata, key);
|
||||
}
|
||||
|
||||
void parseMetadata(TopazFile *t) {
|
||||
char *tag;
|
||||
int flags, nbRecords, i;
|
||||
HeaderRecord *hr = findHeader(t, "metadata");
|
||||
|
||||
fseek(t->f, t->bodyOffset + hr->rec->offset, SEEK_SET);
|
||||
tag = bookReadString(t->f);
|
||||
if (strcmp(tag, "metadata")) {
|
||||
//raise CMBDTCFatal("Parse Error : Record Names Don't Match")
|
||||
return;
|
||||
}
|
||||
|
||||
flags = fgetc(t->f);
|
||||
nbRecords = bookReadEncodedNumber(t->f);
|
||||
|
||||
for (i = 0; i < nbRecords; i++) {
|
||||
char *key = bookReadString(t->f);
|
||||
char *value = bookReadString(t->f);
|
||||
t->metadata = addMapNode(t->metadata, key, value);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Decrypt a payload record with the PID
|
||||
//
|
||||
|
||||
void decryptRecord(unsigned char *in, int len, unsigned char *out, unsigned char *PID) {
|
||||
TpzCtx ctx;
|
||||
topazCryptoInit(&ctx, PID, 8); //is this length correct
|
||||
topazCryptoDecrypt(&ctx, in, out, len);
|
||||
}
|
||||
|
||||
//
|
||||
// Try to decrypt a dkey record (contains the book PID)
|
||||
//
|
||||
unsigned char *decryptDkeyRecord(unsigned char *data, int len, unsigned char *PID) {
|
||||
decryptRecord(data, len, data, PID);
|
||||
//fields = unpack("3sB8sB8s3s",record);
|
||||
|
||||
if (strncmp(data, "PID", 3) || strncmp(data + 21, "pid", 3)) {
|
||||
fprintf(stderr, "Didn't find PID magic numbers in record\n");
|
||||
return NULL;
|
||||
}
|
||||
else if (data[3] != 8 || data[12] != 8) {
|
||||
fprintf(stderr, "Record didn't contain correct length fields\n");
|
||||
return NULL;
|
||||
}
|
||||
else if (strncmp(data + 4, PID, 8)) {
|
||||
fprintf(stderr, "Record didn't contain PID\n");
|
||||
return NULL;
|
||||
}
|
||||
return data + 13;
|
||||
}
|
||||
|
||||
//
|
||||
// Decrypt all the book's dkey records (contain the book PID)
|
||||
//
|
||||
|
||||
unsigned char *decryptDkeyRecords(Payload *data, unsigned char *PID) {
|
||||
int nbKeyRecords = data->blob[0]; //is this encoded number?
|
||||
int i, idx;
|
||||
idx = 1;
|
||||
unsigned char *key = NULL;
|
||||
// records = []
|
||||
for (i = 0; i < nbKeyRecords && idx < data->len; i++) {
|
||||
int length = data->blob[idx++];
|
||||
key = decryptDkeyRecord(data->blob + idx, length, PID);
|
||||
if (key) break; //???
|
||||
idx += length;
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
void bufEncodeInt(cbuf *b, int i) {
|
||||
unsigned char encoded[16];
|
||||
int len = encodeNumber(i, encoded);
|
||||
b_add_buf(b, encoded, len);
|
||||
}
|
||||
|
||||
void bufEncodeString(cbuf *b, char *s) {
|
||||
bufEncodeInt(b, strlen(s));
|
||||
b_add_str(b, s);
|
||||
}
|
||||
|
||||
void writeTopazOutputFile(TopazFile *t, FILE *out, cbuf *tpzHeaders,
|
||||
cbuf *tpzBody, int explode) {
|
||||
int i, numHdrs = 0;
|
||||
HeaderRecord *h;
|
||||
b_add_str(tpzHeaders, "TPZ0");
|
||||
for (h = t->hdrs; h; h = h->next) {
|
||||
if (strcmp(h->tag, "dkey")) {
|
||||
numHdrs++;
|
||||
}
|
||||
}
|
||||
bufEncodeInt(tpzHeaders, numHdrs);
|
||||
|
||||
b_add_byte(tpzBody, 0x40);
|
||||
|
||||
for (h = t->hdrs; h; h = h->next) {
|
||||
Record *r;
|
||||
int nr = 0, idx = 0;
|
||||
if (strcmp(h->tag, "dkey") == 0) continue;
|
||||
b_add_byte(tpzHeaders, 0x63);
|
||||
bufEncodeString(tpzHeaders, h->tag);
|
||||
for (r = h->rec; r; r = r->next) nr++;
|
||||
bufEncodeInt(tpzHeaders, nr);
|
||||
for (r = h->rec; r; r = r->next) {
|
||||
Payload *p;
|
||||
int b, e;
|
||||
bufEncodeInt(tpzHeaders, tpzBody->idx);
|
||||
bufEncodeString(tpzBody, h->tag);
|
||||
bufEncodeInt(tpzBody, idx);
|
||||
b = tpzBody->idx;
|
||||
p = getBookPayloadRecord(t, h->tag, idx++, explode);
|
||||
b_add_buf(tpzBody, p->blob, p->len);
|
||||
e = tpzBody->idx;
|
||||
|
||||
bufEncodeInt(tpzHeaders, r->length); //this is length of blob portion after decompression
|
||||
if (explode) {
|
||||
bufEncodeInt(tpzHeaders, 0); //this is the length in the file if compressed
|
||||
}
|
||||
else {
|
||||
bufEncodeInt(tpzHeaders, r->compressed); //this is the length in the file if compressed
|
||||
}
|
||||
|
||||
freePayload(p);
|
||||
}
|
||||
}
|
||||
|
||||
b_add_byte(tpzHeaders, 0x64);
|
||||
}
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
/*
|
||||
Copyright 2010 BartSimpson aka skindle
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
#ifndef __TPZ_H
|
||||
#define __TPZ_H
|
||||
|
||||
#include <stdio.h>
|
||||
#include "skinutils.h"
|
||||
|
||||
typedef struct _TpzCtx {
|
||||
unsigned int v[2];
|
||||
} TpzCtx;
|
||||
|
||||
void topazCryptoInit(TpzCtx *ctx, unsigned char *key, int klen);
|
||||
void topazCryptoDecrypt(TpzCtx *ctx, unsigned char *in, unsigned char *out, int len);
|
||||
int bookReadEncodedNumber(FILE *f);
|
||||
int encodeNumber(int number, unsigned char *out);
|
||||
char *bookReadString(FILE *f);
|
||||
|
||||
typedef struct _Payload {
|
||||
unsigned char *blob;
|
||||
unsigned int len;
|
||||
char *name;
|
||||
int index;
|
||||
} Payload;
|
||||
|
||||
typedef struct _Record {
|
||||
int offset;
|
||||
int length;
|
||||
int compressed;
|
||||
struct _Record *next;
|
||||
} Record;
|
||||
|
||||
typedef struct _HeaderRecord {
|
||||
char *tag;
|
||||
Record *rec;
|
||||
struct _HeaderRecord *next;
|
||||
} HeaderRecord;
|
||||
|
||||
typedef struct _TopazFile {
|
||||
FILE *f;
|
||||
HeaderRecord *hdrs;
|
||||
unsigned char *bookKey;
|
||||
unsigned int bodyOffset;
|
||||
MapList *metadata;
|
||||
PidList *pids; //extra pids to try from command line
|
||||
} TopazFile;
|
||||
|
||||
Record *bookReadHeaderRecordData(FILE *f);
|
||||
void freeRecordList(Record *r);
|
||||
void freeHeaderList(HeaderRecord *r);
|
||||
void freeTopazFile(TopazFile *t);
|
||||
HeaderRecord *parseTopazHeaderRecord(FILE *f);
|
||||
HeaderRecord *addRecord(HeaderRecord *head, HeaderRecord *r);
|
||||
TopazFile *parseTopazHeader(FILE *f);
|
||||
void freeTopazFile(TopazFile *tpz);
|
||||
HeaderRecord *findHeader(TopazFile *tpz, char *tag);
|
||||
void freePayload(Payload *p);
|
||||
Payload *getBookPayloadRecord(TopazFile *t, char *name, int index, int explode);
|
||||
char *getMetadata(TopazFile *t, char *key);
|
||||
void parseMetadata(TopazFile *t);
|
||||
void decryptRecord(unsigned char *in, int len, unsigned char *out, unsigned char *PID);
|
||||
unsigned char *decryptDkeyRecord(unsigned char *data, int len, unsigned char *PID);
|
||||
unsigned char *decryptDkeyRecords(Payload *data, unsigned char *PID);
|
||||
void writeTopazOutputFile(TopazFile *t, FILE *out, cbuf *tpzHeaders,
|
||||
cbuf *tpzBody, int explode);
|
||||
|
||||
|
||||
#endif
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user