Compare commits

...

228 Commits
v1.6 ... v6.6.1

Author SHA1 Message Date
Apprentice Harper
af6e479af4 Update version number to 6.6.1, with wzyboy's new folder structure. 2018-06-02 16:47:00 +01:00
Apprentice Harper
90e822f470 Merge pull request #502 from wzyboy/feature/reuse-code
Reuse code
2018-06-02 16:21:44 +01:00
Zhuoyun Wei
5c4eed8f1b Generate only one zip file, making the behaviours consistent 2018-05-07 06:27:05 -04:00
Zhuoyun Wei
e665c47075 A wrapper script to make releases 2018-05-07 05:55:38 -04:00
Zhuoyun Wei
d6374f7eab Adjust macOS app directory structure 2018-05-07 04:49:14 -04:00
Zhuoyun Wei
0055386f7b Remove redundant source files in macOS app 2018-05-07 04:45:36 -04:00
Zhuoyun Wei
30eeeea618 Move macOS app resources into contrib/macos/res/ 2018-05-07 04:44:33 -04:00
Zhuoyun Wei
749731fdd4 Move Windows-related stuff into contrib/windows/ 2018-05-07 04:40:43 -04:00
Zhuoyun Wei
95247503f0 Remove redundant files in Windows app 2018-05-07 04:38:49 -04:00
Zhuoyun Wei
79b10f3dfb Move calibre-related into contrib/calibre/ 2018-05-07 04:36:59 -04:00
Zhuoyun Wei
d617822610 Move core source files into src/ 2018-05-07 04:35:49 -04:00
Apprentice Harper
421877574f Merge pull request #332 from wxl/patch-1
removed Requiem website
2018-05-05 18:41:21 +01:00
Apprentice Harper
6956117e28 Merge pull request #490 from wzyboy/backports/infer-filename
Infer filenames consistently
2018-05-05 18:39:34 +01:00
Apprentice Harper
dd09da7dd9 Merge pull request #489 from wzyboy/backports/pylzma
Support pylzma as a fallback
2018-05-05 18:36:55 +01:00
Apprentice Harper
75acbe5536 Merge pull request #473 from cemeyer/kfxzip_efficiency
kfxdrm: Traipse through the kfx-zip more efficiently
2018-05-05 18:34:17 +01:00
Walter Lapchynski
6ee560e425 fixed spelling mistake 2018-04-19 15:56:35 -07:00
Zhuoyun Wei
f54b0aef5c Propagate changes 2018-04-18 05:23:12 -04:00
Zhuoyun Wei
a6ceea1ed9 Infer filenames consistently 2018-04-18 05:21:44 -04:00
Zhuoyun Wei
599f33171f Document LZMA support on Windows 2018-04-18 05:09:33 -04:00
Zhuoyun Wei
12ce977d79 Propagate changes 2018-04-18 04:58:45 -04:00
Zhuoyun Wei
4f1e9fcf43 Use pylzma as a fallback 2018-04-18 04:57:07 -04:00
Zhuoyun Wei
7b45d2128c Don't mask ImportError if dependencies are not met 2018-04-18 04:54:53 -04:00
Conrad Meyer
4400d8d1d4 kfxdrm: Traipse through the kfx-zip more efficiently
We only need to read the magic bytes headers to identify files of the
correct type.  Avoid slurping the entire contents out of the zip if it's
the wrong file.
2018-04-05 10:32:59 -07:00
Apprentice Harper
85e3db8f7c Minor tweaks for first attempt at KFX support - version numbers to 6.6.0, readmes, etc. Removed KFX archive. 2018-04-05 18:30:37 +01:00
Apprentice Harper
29338db228 Merge pull request #458 from tomthumb1997/KFX
Initial KFX support from tomthumb1997
2018-04-05 17:53:41 +01:00
tomthumb1997
608159d71b Create kfxdedrm.py 2018-03-12 20:35:52 -04:00
tomthumb1997
20e0850001 Create ion.py 2018-03-12 20:35:28 -04:00
tomthumb1997
ffd7d41bcd Update kgenpids.py 2018-03-12 20:34:58 -04:00
tomthumb1997
75cad40804 Update k4mobidedrm.py 2018-03-12 20:33:33 -04:00
tomthumb1997
18d6413467 Update __init__.py 2018-03-12 20:32:41 -04:00
Apprentice Harper
7619ee4e0f updated KFX instructions. 2018-01-20 17:04:50 +00:00
Apprentice Harper
a390d7a207 Merge branch 'master' of https://github.com/apprenticeharper/DeDRM_tools 2017-10-20 08:09:50 +01:00
Apprentice Harper
f04f9eca04 Updated version number, copied fix to all tools, updated READMEs and FAQs 2017-10-20 08:09:16 +01:00
Walter Lapchynski
fe3b2873de removed Requiem website
The hidden service serving the Requiem website is down and has probably [been down][1] for quite a while. 

[1]: http://forums.peerblock.com/read.php?13,13983,13983
2017-08-03 12:51:44 -07:00
Apprentice Harper
1ece09023c Added note about Mac application and error 111 2017-08-01 06:54:53 +01:00
Apprentice Harper
8d9f384492 Merge branch 'master' of https://github.com/apprenticeharper/DeDRM_tools 2017-07-04 07:07:04 +01:00
Apprentice Harper
c3fbb83dbc Finally, a proper fix for the accented user name problem 2017-07-04 07:05:51 +01:00
Apprentice Harper
0f23eac3b8 Merge pull request #306 from concavegit/master
forgot backslash in kindle key instructions
2017-06-29 07:03:36 +01:00
Kawin Nikomborirak
bcdcb23d0d forgot backslash in kindle key instructions 2017-06-28 19:25:39 +00:00
Apprentice Harper
a252dd0da6 Kindlekey fix for complex mac disk setups. 2017-06-27 17:22:23 +01:00
Apprentice Harper
2042354788 Update PDF to use Decimal instead of float to handle very precise numbers. Update for changes to ActiveState Python. Fix a few copyright dates. Update version to 6.5.4. Minor changes to obok script for stand-alone use. 2017-06-27 07:05:37 +01:00
Apprentice Harper
9bea20c7ab fix formatting 2017-06-22 07:01:09 +01:00
Apprentice Harper
415df655a1 Add disabling workaround to KFX section. 2017-06-22 06:59:36 +01:00
Apprentice Harper
346b3e312c KFX Decrypter archive in Pascal! 2017-06-20 06:55:25 +01:00
Apprentice Harper
f981a548a3 Merge pull request #300 from concavegit/master
Linux documentation update from concavegit
2017-06-20 06:46:53 +01:00
concavegit
4084a49872 fix step numbers 2017-06-18 21:04:16 -07:00
concavegit
347ad3cc05 Update kindle on linux documentation
- ActivePython for x86 is gone, but using regular python, at least for the kindle decryption, works.
- The new kindle for pc hangs during wine installation, but the one presented by `winetricks kindle` works.
- The decryption keys must be manually obtained.
- vcrun2008 stops any available version of Kindle from being able to install.

I have only tested these steps for kindle for PC on linux, so these changes may step on some toes. However, it does seem the linux documentation needs updating nonetheless.
2017-06-18 21:02:49 -07:00
Apprentice Harper
e4b702e241 Merge pull request #257 from agronauts/multi_decrypt
Add support to command line tool to decrypt all kobo books instead of having to choose books one by one.
2017-04-27 07:51:40 +01:00
Patrick Nicholls
691a3d6955 Added --all flag to sys.args 2017-04-24 21:48:53 +12:00
Apprentice Harper
fa317dc1cf KFX and other updates to the readmes 2017-04-20 08:03:56 +01:00
Patrick Nicholls
6f0c36b67a Implement decrypting of all books 2017-04-16 12:27:34 +12:00
Patrick Nicholls
ceacdbbb1b Refactor decrypt book & add 'all' option to CLI 2017-04-16 12:16:59 +12:00
Apprentice Harper
ff03c68674 Fix for problem starting Mac app due to missing folder 2017-03-23 06:28:24 +00:00
Apprentice Harper
84d4e4e0c8 Fixes for FAQs file concerning Kindle for PC/MAc 2017-03-10 06:59:33 +00:00
Apprentice Harper
a553df50d7 added more info about 1.19 2017-03-08 17:05:51 +00:00
Apprentice Harper
0b244b5781 Merge pull request #198 from dunesmopy/patch-1
Support multiple input Kindle files
2017-03-01 06:50:53 +00:00
dunesmopy
ab4597dfd7 Address review feedback
* Version number updated to 5.5 in the version variable.
  * allow a variable number of input parameters, either files or directories of files.
  * also look for .azw1, .azw3, .azw4, .prc, .mobi, and .pobi files in any specified directories.
2017-02-11 17:35:52 -08:00
Apprentice Harper
82e9927ace Merge pull request #200 from dunesmopy/patch-3
Update FAQs.md
2017-02-09 06:46:23 +00:00
Apprentice Harper
528092c05d Updates FAQs.md
Adjust description of path to ebook files to use current location in Windows 7 and later as default.
2017-02-09 06:43:41 +00:00
Apprentice Harper
faa19cc19b Merge pull request #199 from dunesmopy/patch-2
Update FAQs.md
2017-02-09 06:39:28 +00:00
dunesmopy
e6592841b6 Update FAQs.md
Document included compiled binaries.
2017-02-04 12:34:19 -08:00
dunesmopy
482ac15055 Update FAQs.md 2017-02-04 12:28:42 -08:00
dunesmopy
cb74bd8cef Support multiple input Kindle files
Sample bulk/batch usage:

    @echo off
    setlocal

    SET IN_DIR=%USERPROFILE%\My Documents\My Kindle Content
    SET DEST_DIR=%IN_DIR%_drmfree
    SET KEY_FILE=mykeyfile.k4i


    mkdir "%DEST_DIR%
    k4mobidedrm.py -k %KEY_FILE% "%IN_DIR%" "%DEST_DIR%

    echo done, see %DEST_DIR%
    cd /d "%DEST_DIR%"
    start .

    endlocal
2017-02-04 12:25:12 -08:00
Apprentice Harper
956f3034ad Explicitly warn about KFX files. Bump version number to 6.5.3 2017-01-12 07:24:42 +00:00
Apprentice Harper
fca7eaab8e Update FAQs.md
Added note about Kindle for PC/Mac update and links to version 1.1.7
2017-01-06 06:31:42 +00:00
Apprentice Harper
0df66bcfc0 Improve testing of decrypted text file. (And so decrypt badly formatted ePubs) 2016-12-21 06:33:34 +00:00
Apprentice Harper
20ab5b354d Remove incorrect Linux support for Kobo Desktop 2016-12-20 06:27:08 +00:00
Apprentice Harper
46ce2ce0ea Update for strange windows users on a network, and more xml verification fixes 2016-10-20 07:12:37 +01:00
Apprentice Harper
ca59704dc4 Updated the FAQs and the main ReadMe 2016-10-19 07:14:42 +01:00
Apprentice Harper
17300283d0 improve xml detection and handle strange windows network file systems better 2016-10-10 17:41:05 +01:00
Apprentice Harper
92ce0396fe Making sure files and versions are consistent 2016-10-07 17:32:13 +01:00
Apprentice Harper
5eb3338423 Update FAQs to point people to new test obok plugin 2016-10-07 07:13:43 +01:00
Apprentice Harper
d65dd1ab87 New obok fix that should work for stand-alone script and non-Windows machines. (Makes a tweaked copy of the database.) 2016-10-07 07:06:22 +01:00
Apprentice Harper
5d75018719 Topaz fix and updated FAQ reference to point to github 2016-09-29 07:01:19 +01:00
Apprentice Harper
1c3a12425e Merge branch 'master' of https://github.com/apprenticeharper/DeDRM_tools 2016-09-28 07:15:06 +01:00
Apprentice Harper
6b4d621159 Fix for Obok Plugin and Obok Desktop v4.0 2016-09-28 07:14:16 +01:00
Apprentice Harper
53c16c916b Note the new Kobo Desktop version
The new Kobo Desktop version breaks the obok plugin.
2016-09-13 06:53:16 +01:00
Apprentice Harper
34231cc252 Remove debugging dialog from Mac App. Update version to 6.5.1 2016-08-12 06:30:48 +01:00
Apprentice Harper
c2615c4d3b Change to ineptpdf.py, so that we throw an exception for DRM-free PDFs, rather than processing them. 2016-08-10 06:40:48 +01:00
Apprentice Harper
908ebc5c58 Update version number to 6.5.0 2016-08-05 17:34:52 +01:00
Apprentice Harper
4d7556e919 Fix for another unknown Topaz token. 2016-08-05 17:24:44 +01:00
Apprentice Harper
6feeb352fc Update to Mac app to make getting keys much more robust and make startup quicker. 2016-07-26 06:47:07 +01:00
Apprentice Harper
bc3676c1bc Changes to the ReadMes 2016-06-13 17:37:26 +01:00
Apprentice Harper
a4ebec359b Added Kindle for Android note 2016-06-09 06:28:26 +01:00
Apprentice Harper
10cffca6b4 Formatting and ActiveState Python
More escaping of characters, and added a Q&A about ActiveState Python on Windows. (The current installer has a bug.)
2016-06-07 18:20:05 +01:00
Apprentice Harper
24a8c80617 Improved the logging answers 2016-06-06 18:39:33 +01:00
Apprentice Harper
b2338b71c0 Some formatting fixes for the FAQs 2016-06-06 06:46:07 +01:00
Apprentice Harper
16733c3198 Added FAQs.md 2016-05-31 17:36:17 +01:00
Apprentice Harper
3a931dfc90 Fixes for B&N key generation and Macs with bonded ethernet ports 2016-04-25 17:49:06 +01:00
Apprentice Harper
eaa7a1afed Switch to notifications for Mac App. Fix problem with Android backup files being missing, 2016-04-25 06:39:20 +01:00
Apprentice Harper
dc5261870f Topaz fixes to Mac & Windows apps, and version number update 2016-04-18 17:39:17 +01:00
Apprentice Harper
a2ba5005c9 Another Topaz missing token fix. 2016-04-18 16:54:46 +01:00
Apprentice Harper
24922999dc Fix for Topaz books of no more than two text pages. 2016-04-14 17:35:48 +01:00
Apprentice Harper
e2170b4260 Fox for new tags in Topaz format ebooks. 2016-04-13 18:39:13 +01:00
Apprentice Harper
054ddc894b updated kindlekey version 2016-03-18 06:39:53 +00:00
Apprentice Harper
8cd4be6fb0 First try at a fix for the Kindle for PC encryption update 2016-03-13 12:00:57 +00:00
Apprentice Harper
d67e05cf04 Merge pull request #75 from directionless/errormessages
Error Messages - one fix, one adddition
2016-03-08 07:00:22 +00:00
seph
a5197a6abb Fix an error message, and add another 2016-03-04 22:27:57 -05:00
apprenticeharper
cfc13db6c5 Merge branch 'master' of https://github.com/apprenticeharper/DeDRM_tools 2016-01-15 06:34:15 +00:00
apprenticeharper
8aa2157d55 Completely remove erroneous check 2016-01-15 06:33:05 +00:00
apprenticeharper
81b08dcf05 Completely remove erroneous check 2016-01-15 06:30:54 +00:00
apprenticeharper
ca42e028a7 Regression bug fixes 2016-01-14 17:15:43 +00:00
apprenticeharper
10963f6011 Update to DeDRM to try to fix Linux python problem, and improve Adobe logging 2016-01-11 06:44:44 +00:00
apprenticeharper
72968d2124 Update of obok and change Scuolabook to a link 2016-01-11 06:43:22 +00:00
apprenticeharper
3e95168972 Merge fixes for new Kobo version and Linux support, update version number 2016-01-11 06:40:15 +00:00
apprenticeharper
ecf1d76d90 Update to Scuolabooks tool 2016-01-07 06:20:00 +00:00
Apprentice Harper
e59f0f346d Merge pull request #51 from norbusan/master
update obok.py with new hashes - support obok cmd line usage
2016-01-06 17:45:13 +00:00
Norbert Preining
b6046d3f4b move serial detection into obok.py, cater for cmd usage 2015-12-28 09:53:13 +09:00
Norbert Preining
a863a4856a update obok.py with new hashes and code from minmax 2015-12-26 09:13:16 +09:00
apprenticeharper
a13d08c3bc Fix for crash when Arc or Vox is connected. 2015-10-29 07:47:49 +00:00
apprenticeharper
9434751a72 updated version number and script copy for obok changes 2015-10-13 08:05:34 +01:00
Apprentice Harper
fc156852a4 Merge pull request #41 from norbusan/fixes
obok: make sure that file exists before opening the db
2015-10-13 07:36:54 +01:00
Norbert Preining
0c67fd43a2 obok: make sure that file exists before opening the db 2015-10-08 10:59:34 +09:00
Apprentice Harper
00a5c4e1d1 Updated obok plugin readme 2015-10-05 07:49:48 +01:00
apprenticeharper
4ea0d81144 Change to unicode strings to fix stand-alone character encoding problems 2015-09-30 07:39:29 +01:00
Apprentice Harper
b1cccf4b25 Merge pull request #39 from norbusan/obok_linux
improvements in the obok device handling
2015-09-21 07:40:54 +01:00
Norbert Preining
fe6074949b obok.py: first try device, and only if that fails fall back to Desktop progs 2015-09-18 09:10:06 +09:00
Norbert Preining
2db7ee8894 obok_plugin:action.py - get serial from device if possible 2015-09-18 08:58:01 +09:00
Norbert Preining
93d8758462 get device path from calibre, and allow device usage on all platforms 2015-09-16 23:01:29 +09:00
Norbert Preining
f97bc078db add support for linux via device serials and reading from device 2015-09-14 14:12:22 +09:00
apprenticeharper
2e96db6cdc More changes to the obok cli interface for character encodings 2015-09-08 07:52:06 +01:00
apprenticeharper
0d530c0c46 Add encryption fixes from other scripts (SafeUnbuffered). 2015-09-07 08:12:45 +01:00
apprenticeharper
488924d443 Fix for kobo users who haven't yet bought a book. 2015-09-07 07:52:23 +01:00
apprenticeharper
07485be2c0 Add transparency to the obok logo 2015-09-03 08:06:33 +01:00
apprenticeharper
e5e269fbae Fixes for android key extraction 2015-09-03 07:51:10 +01:00
apprenticeharper
d54dc38c2d Fix for Kobo Desktop 3.17 2015-09-03 07:49:09 +01:00
apprenticeharper
3c322f3695 Second and last step to fix capitalisation issue 2015-09-01 07:08:46 +01:00
apprenticeharper
c112c28f58 First step to fix capitalisation issue 2015-09-01 07:06:13 +01:00
apprenticeharper
b8606cd182 Merge pull request #32 from eli-schwartz/master
Linux: Allow using ~ when specifying a wineprefix.
2015-09-01 06:57:48 +01:00
Eli Schwartz
f2190a6755 Linux: Allow using ~ when specifying a wineprefix. 2015-08-12 17:39:50 -04:00
apprenticeharper
33d8a63f61 Fix for plugin bug introduced in 6.3.1 for Kindle for PC 2015-08-12 18:35:21 +01:00
apprenticeharper
91b22c18c4 Update kobo plugin to 3.1.3 with Portuguese and Arabic translations. Include .mo files in git. 2015-08-05 06:59:01 +01:00
apprenticeharper
3317dc7330 Fixed plugin help file and updated readmes 2015-08-04 07:18:33 +01:00
apprenticeharper
aa866938f5 Updated plugin zip with 6.3.1 files 2015-08-02 11:10:57 +01:00
apprenticeharper
aa822de138 New approach to Android backup files. Changed version number to 6.3.1 2015-08-02 11:09:35 +01:00
apprenticeharper
f5e66d42a1 Android backup handling approach improved and implemented in Windows and plugin. Mac work to follow. 2015-07-29 18:11:19 +01:00
apprenticeharper
c16d767b00 Merge branch 'nook_url_support' 2015-07-13 18:02:48 +01:00
Apprentice Harper
9a8d5f74a6 Improvements to nook Study key retrieval, and addition of retrieval of nook keys via the internet. 2015-07-11 13:50:36 +01:00
Apprentice Harper
6be1323817 Fixed name of Kindle for Android help file 2015-04-14 07:02:18 +01:00
Apprentice Harper
9b77255212 Starting to move ignoblekeyfetch into all tools. 2015-04-13 07:45:43 +01:00
Apprentice Harper
46426a9eae The compressed plugin so far 2015-04-10 18:14:36 +01:00
Apprentice Harper
45ad3cedec Added in fetching B&N key via URL instead of generating from name & CC# 2015-04-10 18:12:39 +01:00
Apprentice Harper
d140b7e2dc Merge of bugfix 6.2.1 into master 2015-03-26 07:31:45 +00:00
Apprentice Harper
e729ae8904 Fix for Kindle problem in Mac app and non-ascii username problem in Windows (plugin and app). 2015-03-25 07:26:33 +00:00
Apprentice Harper
0837482686 changed for android support - in progress 2015-03-24 07:04:06 +00:00
Apprentice Harper
4c9aacd01e Added help file for Kindle for Android keys to plugin. Copied updated files to Mac and Windows applications. 2015-03-18 20:37:54 +00:00
Apprentice Harper
6b2672ff7c Fixes for the plugin and Android keys (help still needs adding) 2015-03-18 19:12:01 +00:00
Apprentice Alf
39c9d57b15 Merge branch 'master' of https://github.com/apprenticeharper/DeDRM_tools 2015-03-17 18:02:23 +00:00
Apprentice Alf
9c347ca42f removed unused file 2015-03-17 17:49:57 +00:00
Apprentice Alf
032fcfa422 Partial update of plugin to use androidkindlekey.py. Still needs more testing/tweaking in the preferences. 2015-03-17 17:49:30 +00:00
Apprentice Alf
35aaf20c8d Update the Macintosh AppleScript to use the new androidkindlekey.py 2015-03-17 17:48:25 +00:00
Apprentice Alf
b146e4b864 Update androidkindlekey.py to work with tools 2015-03-17 07:07:12 +00:00
Apprentice Alf
27d8f08b54 android.py name change to androidkindlekey.py 2015-03-17 07:05:00 +00:00
apprenticeharper
6db762bc40 Create README.md 2015-03-13 17:22:40 +00:00
Apprentice Alf
c7c34274e9 Obok plugin 3.1.2 unzipped 2015-03-13 07:18:16 +00:00
Apprentice Alf
cf922b6ba1 obok 3.1.1 plugin unzipped 2015-03-13 07:16:59 +00:00
Apprentice Harper
9d9c879413 tools v6.2.0
Updated for B&N new scheme, added obok plugin, and many minor fixes,
2015-03-09 07:41:07 +00:00
Apprentice Alf
c4fc10395b tools v6.1.0 2015-03-07 21:23:33 +00:00
Apprentice Alf
a30aace99c tools v6.0.9
obok added to other tools
2015-03-07 21:18:50 +00:00
Apprentice Alf
b1feca321d tools v6.0.8 2015-03-07 21:10:52 +00:00
Apprentice Alf
74a4c894cb tools v6.0.7 2015-03-07 19:41:44 +00:00
Apprentice Alf
5a502dbce3 DeDRM plugin 6.0.6
(Only the plugin seems to have been updated for the 6.0.6 release.)
2015-03-07 19:37:26 +00:00
Apprentice Alf
a399d3b7bd tools v6.0.5 2015-03-07 19:29:43 +00:00
Apprentice Alf
cd2d74601a tools v6.0.4 2015-03-07 14:55:09 +00:00
Apprentice Alf
51919284ca tools v6.0.3 2015-03-07 14:47:45 +00:00
Apprentice Alf
d586f74faa tools v6.0.2 2015-03-07 14:45:12 +00:00
Apprentice Alf
a2f044e672 tools v6.0.1 2015-03-07 14:38:41 +00:00
Apprentice Alf
20bc936e99 tools v6.0.0
The first unified calibre plugin
2015-03-07 14:34:21 +00:00
Apprentice Alf
748bd2d471 tools v5.6.2 2015-03-07 14:29:24 +00:00
Apprentice Alf
490ee4e5d8 tools v5.6.1 2015-03-07 14:25:39 +00:00
Apprentice Alf
c23b903420 tools v5.6 2015-03-07 14:21:18 +00:00
Apprentice Alf
602ff30b3a tools v5.5.3 2015-03-07 14:02:17 +00:00
Apprentice Alf
c010e3f77a tools v5.5.2 2015-03-07 13:58:52 +00:00
Apprentice Alf
0c03820db7 tools v5.5.1 2015-03-07 13:55:45 +00:00
Apprentice Alf
9fda194391 tools v5.5
Plugins now include unaltered stand-alone scripts, so no longer need to keep separate copies.
2015-03-07 13:48:25 +00:00
Apprentice Alf
b661a6cdc5 tools v5.4.1 2015-03-07 13:28:34 +00:00
Apprentice Alf
0dcd18d524 tools v5.4 2015-03-07 13:14:37 +00:00
Apprentice Alf
0028027f71 Changed from DeDRM_WinApp to DeDRM_App 2015-03-07 13:05:48 +00:00
TetraChroma
4b3618246b ineptpdf 8.4.51 2015-03-06 18:02:17 +00:00
Apprentice Alf
07ea87edf4 tools v5.3.1 2015-03-06 18:00:34 +00:00
Apprentice Alf
899fd419ae tools v5.3 2015-03-06 17:57:20 +00:00
Apprentice Alf
f3f02adc98 tools v5.2 2015-03-06 17:43:57 +00:00
Apprentice Alf
0812438b9d nib changed from folder to file, so must delete first 2015-03-06 17:41:42 +00:00
Apprentice Alf
26d9f7bd20 ReadMe names changed 2015-03-06 17:30:07 +00:00
Apprentice Alf
2c95633fcd tools v5.1
alfcrypto added to DeDRM plugin
2015-03-06 17:15:59 +00:00
Apprentice Alf
07e532f59c tools v5.0
Introduction of alfcrypto library for speed
Reorganisation of archive plugins,apps,other
2015-03-06 07:43:33 +00:00
Apprentice Alf
882edb6c69 Re-arrange folders and files for 5.0 tools 2015-03-06 07:32:13 +00:00
Apprentice Alf
93f02c625a tools v4.8 2015-03-06 07:24:30 +00:00
Apprentice Alf
e95ed1a8ed tools v4.7 2015-03-06 07:18:01 +00:00
Apprentice Alf
ba5927a20d tools v4.6 2015-03-06 07:13:06 +00:00
Apprentice Alf
297a9ddc66 tools v4.5 2015-03-06 07:08:24 +00:00
Apprentice Alf
4f34a9a196 tools v4.0
New calibre plugin interface (0.7.55)
Dropped unswindle.pyw
Added Android patch
2015-03-06 06:59:36 +00:00
Apprentice Alf
529dd3f160 tools v3.8
version 2 - a minor change to one script.
2015-03-05 17:54:25 +00:00
Apprentice Alf
4163d5ccf4 tools v3.8 2015-03-05 17:48:25 +00:00
Apprentice Alf
867ac35b45 tools v3.7 2015-03-05 17:42:55 +00:00
Apprentice Alf
427137b0fe tools v3.6 2015-03-05 17:37:44 +00:00
Apprentice Alf
ac9cdb1e98 tools v3.5 2015-03-05 17:33:13 +00:00
Apprentice Alf
2bedd75005 tools v3.4 2015-03-05 17:22:23 +00:00
Apprentice Alf
8b632e309f tools v3.3 2015-03-05 07:42:05 +00:00
Apprentice Alf
bc968f8eca tools v3.2
First appearance of combined windows python app
2015-03-05 07:25:35 +00:00
Apprentice Alf
00ac669f76 tools v3.1 2015-03-05 07:11:14 +00:00
Apprentice Alf
694dfafd39 MobiDeDRM 0.23 2015-03-05 06:53:44 +00:00
Apprentice Alf
a7856f5c32 tools v3.0
First combined mobi/topaz kindle tool
2015-03-04 18:41:37 +00:00
Apprentice Alf
38eabe7612 tools v2.4 2015-03-04 18:19:08 +00:00
Apprentice Alf
9162698f89 tools v2.3
First appearance of DeDRM AppleScript application
2015-03-04 18:09:09 +00:00
Apprentice Alf
506d97d5f0 ineptpdf 7.6 2015-03-04 07:20:25 +00:00
TetraChroma
a76ba56cd8 ineptpdf 8.4.48 2015-03-04 07:18:37 +00:00
Apprentice Alf
8e73edc012 ineptpdf 7.5 2015-03-04 07:17:08 +00:00
Apprentice Alf
c386ac6e6d tools v2.2 2015-03-04 07:12:08 +00:00
Apprentice Alf
5f0671db7f tools v2.1
combined kindle/mobi plugin
2015-03-03 18:18:52 +00:00
Apprentice Alf
bf03edd18c tools v2.0
Most tools now have plugins
2015-03-03 18:02:10 +00:00
Apprentice Alf
d427f758f6 tools v1.8 2015-03-03 07:19:44 +00:00
TetraChroma
92dafd94b2 ineptpdf 8.2 fileopen 2015-03-03 07:18:58 +00:00
TetraChroma
c5ed30d8c8 Duplicate for split to fileopen version 2015-03-03 07:17:33 +00:00
Apprentice Alf
b7de1dcea5 ineptpdf 7.4 2015-03-03 07:13:37 +00:00
Anonymous
ab9d585190 ineptpdf 7.3 2015-03-03 07:11:36 +00:00
Anonymous
b92458c8c2 ineptpdf 7.2 2015-03-03 07:09:18 +00:00
Anonymous
4f19f5ac11 ineptpdf 7 2015-03-03 07:07:03 +00:00
Anonymous
f027848bff ineptpdf 6.1 2015-03-03 07:04:39 +00:00
Anonymous
26a54dd3d6 ineptpdf 6 2015-03-03 07:02:54 +00:00
Apprentice Alf
6c70a073d9 mobidedrm 0.15 2015-03-03 06:53:16 +00:00
Anonymous
37696e1495 ineptkey 4.4 2015-03-02 18:20:43 +00:00
Anonymous
446d45da6b ineptkey 4.3 2015-03-02 18:19:11 +00:00
Anonymous
a73fbbb484 ineptkey 4.2 2015-03-02 18:16:16 +00:00
Anonymous
086d25a441 ineptkey 4.1 2015-03-02 18:14:33 +00:00
Anonymous
63219d6054 ineptepub 4.1 2015-03-02 18:10:00 +00:00
Anonymous
f0d920c158 ineptepub v4 2015-03-02 18:08:10 +00:00
i♥cabbages
e9a7312759 ineptepub 3 2015-03-02 18:06:42 +00:00
209 changed files with 32017 additions and 18474 deletions

6
.gitignore vendored
View File

@@ -1,9 +1,6 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
# C extensions
*.so
*.pyc
# Distribution / packaging
.Python
@@ -40,7 +37,6 @@ nosetests.xml
coverage.xml
# Translations
*.mo
*.pot
# Django stuff:

View File

@@ -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())

View File

@@ -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())

View File

@@ -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())

View File

@@ -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())

View File

@@ -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())

View File

@@ -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())

229
FAQs.md Normal file
View File

@@ -0,0 +1,229 @@
# 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.
### A Recent Change to Kindle for PC/Kindle for Mac
Starting with version 1.19, Kindle for PC/Mac uses Amazon's new KFX format which these tools can't handle. There are two options to get the older formats that the tools can decrypt. Either stick with version 1.17 or earlier, or modify the executable by changing a file name.
Version 1.17 of Kindle is are no longer available directly from Amazon, so you will need to search for the proper file name and find it on a third party site. The name is "KindleForPC-installer-1.17.44170.exe" for PC and "KindleForMac-44182.dmg" for Mac.
Verify the one of the following cryptographic hash values, using software of your choice, before installing the downloaded file in order to avoid viruses. If the hash does not match, delete the downloaded file and try again from another site.
Kindle for PC:
MD-5: 53F793B562F4823721AA47D7DE099869
SHA-1: 73C404D719F0DD8D4AE1C2C96612B095D6C86255
SHA-256: 14E0F0053F1276C0C7C446892DC170344F707FBFE99B695176 2C120144163200
Kindle for Mac:
MD-5: E7E36D5369E1F3CF1D28E5D9115DF15F
SHA-1: 7AB9A86B954CB23D622BD79E3257F8E2182D791C
SHA-256: 28DC21246A9C7CDEDD2D6F0F4082E6BF7EF9DB9CE9D485548E 8A9E1D19EAE2AC.
You will need to go to the preferences and uncheck the auto update checkbox. Then download and install 1.17 over the top of the 1.19 installation. You'll also need to delete the KFX folders from your My Kindle Content folder.
A second possible solution is to use 1.19 or later, but disable KFX by renaming or disabling a necessary component of the application. This may or may not work on versions after 1.20. In a command window, enter the following commands when Kindle for PC/Mac is not running:
#### 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.
#### Another Note on KFX
It now possible, but not easy, to convert books from KFX to other formats in calibre by installing the optional KFX Input plugin. The lack of automatic DRM removal makes this process difficult so it is not recommended unless there is no other alternative, such as for Indic language books only available in KFX. There is a windows-only KFX DRM rmeoval program in the repository, but not yet integrated into the tools.
#### Thanks
Thanks to jhowell for his investigations into KFX format and workarounds. 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\_First.txt file. Please read the ReadMe\_First file! That will explain what the folders are, and you'll be able to work out which of the tools you need.
## That's a big complicated ReadMe file! Isn't there a quick guide?
Install calibre. Install the DeDRM\_plugin in calibre. Install the Obok\_plugin in calibre. Restart calibre. In the DeDRM_plugin customisation dialog add in any E-Ink Kindle serial numbers and your B&N account email address and password. Remember that the plugin only tries to remove DRM when ebooks are imported.
# Installing the Tools
## The calibre plugin
### I am trying to install the calibre plugin, but calibre says "ERROR: Unhandled exception: InvalidPlugin: The plugin in u[path]DeDRM\_tools\_6.5.3.zip is invalid. It does not contain a top-level \_\_init\_\_.py file"
You are trying to add the tools archive (e.g. DeDRM\_tools\_6.5.3.zip) instead of the plugin. The tools archive is not the plugin. It is a collection of DRM removal tools which includes the plugin. You must unzip the archive, and install the calibre plugin (DeDRM\_plugin.zip) from a folder called “DeDRM\_calibre_plugin” in the unzipped archive.
### Ive unzipped the tools archive, but I cant find the calibre plugin when I try to add them to calibre. I use Windows.
You should select the zip file that is in the “DeDRM\_calibre\_plugin” folder, not any files inside the plugins zip archive. Make sure you are selecting from the folder that you created when you unzipped the tools archive and not selecting a file inside the still-zipped tools archive.
(The problem is that Windows will allow apps to browse inside zip archives without needing to unzip them first. If there are zip archives inside the main zip archives, Windows will show them as unzipped as well. So what happens is people will unzip the DeDRM\_tools\_X.X.X.zip to a folder, but when using calibre they will actually navigate to the still zipped file by mistake and cannot tell they have done so because they do not have file extensions showing. So to the unwary Windows user, it appears that the zip archive was unzipped and that everything inside it was unzipped as well so there is no way to install the plugins.
We strongly recommend renaming the DeDRM\_tools\_X.X.X.zip archive (after extracting its contents) to DeDRM\_tools\_X.X.X_archive.zip. If you do that, you are less likely to navigate to the wrong location from inside calibre.)
## The Windows Application
### I've installed ActiveState Python and PyCrypto, but the Windows application won't run. What have I done wrong?
Nothing. There's a bug in the some older ActiveState Python Windows installers that puts the Tcl code in the wrong place. See [this comment of mine at ActiveState community](https://community.activestate.com/node/19090). Just move the Tcl code to the correct place manually and the Windows app should run.
## The Macintosh Application
### I can't open the Macintosh Application. Some message about it not being signed or something.
Try right-clicking and select open. That might give you the option to open it anyway. Otherwise you'll need to change your security settings to allow unsigned applications to run. You can probably change these back after running it for the first time.
### I can't open the Macintosh Application at all. I get 'The aplication "DeDRM" can't be opened'
Some unzip applications do not respect the execution bit setting. Try unzipping the main tools archive using the built-in Mac unzip utility.
### I can't open the Macintosh Application at all. I get 'spawn_via_launchd() failed, errno=111'
There seems to be a bug in Apple's launch services. Try using the free [Maintenance utility](https://www.titanium-software.fr/en/maintenance.html) from Titanium Software to clear the launch cache and database.
# Using the Tools
## I cant get the tools to work on my rented or library ebooks.
The tools are not designed to remove DRM from rented or library ebooks.
## I've unzipped the tools, but what are all the different files, and how do I use them?
Read the ReadMe_First.txt file and then the ReadMe files included in the tools folder(s) you're interested in. That's what they're for.
## I have installed the calibre plugin, but my books still have DRM. When I try to view or convert my books, calibre says they have DRM.
DRM only gets removed when an ebook is imported into calibre. Also, if the book is already in calibre, by default calibre will discard the newly imported file. You can change this in calibre's Adding books preferences page (Automerge..../Overwrite....), so that newly imported files overwrite existing ebook formats. Then just re-import your books and the DRM-free versions will overwrite the DRMed versions while retaining your books' metadata.
## I have installed the calibre plugin or I am trying to use one of the other tools, but I dont know where my ebooks are stored.
Your ebooks are stored on your computer or on your ebook reader. You need to find them to be able to remove the DRM. If they are on your reader, you should be able to locate them easily. On your computer its not so obvious. Here are the default locations.
### Macintosh
Navigating from your home folder,
Kindle for Mac ebooks are in either Library/Application Support/Kindle/My Kindle Content or Documents/My Kindle Content or Library/Containers/com.amazon.Kindle/Data/Library/Application Support/Kindle/My Kindle Content, depending on your version of Kindle for Mac.
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 this book is from Kindle for Mac or Kindle for PC, you must be using version 1.17 or below, see note at top of this file.
If the book is from Kindle for PC or Kindle for Mac and you think you are doing everything right, and you are getting this message, it is possible that the files containing the encryption key arent quite in the format the tools expect. To try to fix this:
1. Deregister Kindle for PC(Mac) from your Amazon account.
1. Uninstall Kindle for PC(Mac)
1. Delete the Kindle for PC(Mac) preferences
* PC: Delete the directory [home folder]\AppData\Local\Amazon (it might be hidden) and [home folder]\My Documents\My Kindle Content
* Mac: Delete the directory [home folder]/Library/Application Support/Kindle/and/or [home folder]/Library/Containers/com.amazon.Kindle/Data/Library/Application Support/Kindle/ (one or both may be present and should be deleted)
1. Reinstall Kindle for PC(Mac) version 1.17 or earlier (see above for download links).
1. Re-register Kindle for PC(Mac) with your Amazon account
1. Download the ebook again. Do not use the files you have downloaded previously.
## Some of my books had their DRM removed, but some still say that they have DRM and will not convert.
There are several possible reasons why only some books get their DRM removed.
* You still dont have the DRM removal tools working correctly, but some of your books didnt have DRM in the first place.
* Kindle only: It is a Topaz format book and contains some coding that the tools do not understand. You will need to get a log of the DeDRM attempt, and then create a [new issue at Apprentice Harper's github repository](https://github.com/apprenticeharper/DeDRM_tools/issues/), attaching the book and the log, so that the tools can be updated.
If you are still having problems with particular books, you will need to create a log of the DRM removal attempt for one of the problem books, and post that in a comment at Apprentice Alf's blog or in a new issue at Apprentice Harper's github repository.
## 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 eboook in this manner, Amazon will package the pictures in the with text in a single file that the tools will be able to import successfully.
## My Kindle book has imported, but it's showing up as an AZW4 format. Conversions take a long time and/or are very poor.
You have found a Print Replica Kindle ebook. This is a PDF in a Kindle wrapper. Now the DRM has been removed, you can extract the PDF from the wrapper using the KindleUnpack plugin. Conversion of PDFs rarely gives good results.
## The tools can't see an ebook that was downloaded directly to my eInk kindle, although it's definitely there, and I can read it on the Kindle. I can't even try to import it.
Mostly likely, this is a book downloaded from Amazon directly to one of the newer eInk Kindles (e.g. Paperwhite). Unfortunately, it is probably in a new multi-file KFX format that the tools don't understand. You must download the book manually from Amazon's web site "For transfer via USB" to your Kindle. When you download the ebook in this manner, Amazon will send a single KF8-format file that the tools will be able to import successfully.
## Do the tools work on books from Kobo?
If you use the Kobo desktop application for Mac or PC, install the obok plugin. This will import and remove the DRM from your Kobo books, and is the easiest method for Kobo ebooks.
## I registered Adobe Digital Editions 3.0 or later with an Adobe ID before downloading, but my epub or PDF still has DRM.
Adobe introduced a new DRM scheme with ADE 3.0 and later. Install ADE 2.0.1 and register with the same Adobe ID. If you can't open your book in ADE 2.01, then you have a book with the new DRM scheme. These tools can't help. You can avoid the new DRM scheme by always downloading your ebooks with ADE 2.0.1. Some retailers will require ADE 3.0 or later, in which case you won't be able to download with ADE 2.0.1.
## The DRM wasn't removed and the log says "Failed to decrypt with error: Cannot decode library or rented ebooks." What now?
You're trying to remove the DRM from an ebook that's only on loan to you. No help will be given to remove DRM from such ebooks. If you think that you have received this message for a book you own, please create an issue at github, or comment at the blog.
## I cannot solve my problem with the DeDRM plugin, and now I need to post a log. How do I do that?
Remove the DRMed book from calibre. Click the Preferences drop-down menu and choose 'Restart in debug mode'. Once calibre has re-started, import the problem ebook. Now close calibre. A log will appear that you can copy and paste into a comment at Apprentice Alf's blog, or into a new issue at Apprentice Harper's github repository.
## I cannot solve my problem with the Macintosh DeDRM application, and now I need to post a log. How do I do that?
The Macintosh DeDRM application creates a log file on your desktop every time it is run. After unsuccessfully removing DRM from one ebook, copy the contents of the log file (it is a simple text file) and paste it into your comment at Apprentice Alf's blog or in a new issue at Apprentice Harper's github repository.
## I cannot solve my problem with the Windows DeDRM application, and now I need to post a log. How do I do that?
The Windows DeDRM application creates a log file in your home directory (C:\Users\[username]) every time it is run. After unsuccessfully removing DRM from one ebook, copy the contents of the log file (it is a simple text file) and paste it into your comment at Apprentice Alf's blog or in a new issue at Apprentice Harper's github repository.
# 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.
## What 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 books owner to use the encryption key information already stored someplace on their computer or device to decrypt the ebook in the same manner the official ebook reading software uses.
## Why dont 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. Apples Fairplay DRM scheme can be removed using Requiem if the appropriate version of iTunes can still be installed and used. See the post Apple and ebooks: iBookstore DRM and how to remove it at Apprentice Alf's blog for more details.
## Ive got the tools archive and Ive read all the FAQs but I still cant install the tools and/or the DRM removal doesnt work
* Read the ReadMe_First.txt file in the top level of the tools archive
* Read the ReadMe file in the folder of the tools you want to use.
* If you still cant remove the DRM, ask in the comments section of Apprentice Alf's blog or create a new issue at Apprentice Harper's github repository, reporting the error as precisely as you can, what platform you use, what tool you have tried, what errors you get, and what versions you are using. If the problem happens when running one of the tools, post a log (see previous questions on how to do this).
## 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 AppleScript application was created by Apprentice Alf
* The DeDRM all-in-one Python application was created by some_updates
* The DeDRM all-in-one calibre plugins was created by Apprentice Alf
* 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.

View File

@@ -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())

View File

@@ -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())

View File

@@ -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())

View File

@@ -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())

View File

@@ -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())

View File

@@ -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)

View File

@@ -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())

View File

@@ -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)

View File

@@ -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)

View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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.

View File

@@ -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));
}

View File

@@ -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 */

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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]);
}
}

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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\\", &regkey) != 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;
}

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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

View File

@@ -1,332 +0,0 @@
/* zconf.h -- configuration of the zlib compression library
* Copyright (C) 1995-2005 Jean-loup Gailly.
* For conditions of distribution and use, see copyright notice in zlib.h
*/
/* @(#) $Id$ */
#ifndef ZCONF_H
#define ZCONF_H
/*
* If you *really* need a unique prefix for all types and library functions,
* compile with -DZ_PREFIX. The "standard" zlib should be compiled without it.
*/
#ifdef Z_PREFIX
# define deflateInit_ z_deflateInit_
# define deflate z_deflate
# define deflateEnd z_deflateEnd
# define inflateInit_ z_inflateInit_
# define inflate z_inflate
# define inflateEnd z_inflateEnd
# define deflateInit2_ z_deflateInit2_
# define deflateSetDictionary z_deflateSetDictionary
# define deflateCopy z_deflateCopy
# define deflateReset z_deflateReset
# define deflateParams z_deflateParams
# define deflateBound z_deflateBound
# define deflatePrime z_deflatePrime
# define inflateInit2_ z_inflateInit2_
# define inflateSetDictionary z_inflateSetDictionary
# define inflateSync z_inflateSync
# define inflateSyncPoint z_inflateSyncPoint
# define inflateCopy z_inflateCopy
# define inflateReset z_inflateReset
# define inflateBack z_inflateBack
# define inflateBackEnd z_inflateBackEnd
# define compress z_compress
# define compress2 z_compress2
# define compressBound z_compressBound
# define uncompress z_uncompress
# define adler32 z_adler32
# define crc32 z_crc32
# define get_crc_table z_get_crc_table
# define zError z_zError
# define alloc_func z_alloc_func
# define free_func z_free_func
# define in_func z_in_func
# define out_func z_out_func
# define Byte z_Byte
# define uInt z_uInt
# define uLong z_uLong
# define Bytef z_Bytef
# define charf z_charf
# define intf z_intf
# define uIntf z_uIntf
# define uLongf z_uLongf
# define voidpf z_voidpf
# define voidp z_voidp
#endif
#if defined(__MSDOS__) && !defined(MSDOS)
# define MSDOS
#endif
#if (defined(OS_2) || defined(__OS2__)) && !defined(OS2)
# define OS2
#endif
#if defined(_WINDOWS) && !defined(WINDOWS)
# define WINDOWS
#endif
#if defined(_WIN32) || defined(_WIN32_WCE) || defined(__WIN32__)
# ifndef WIN32
# define WIN32
# endif
#endif
#if (defined(MSDOS) || defined(OS2) || defined(WINDOWS)) && !defined(WIN32)
# if !defined(__GNUC__) && !defined(__FLAT__) && !defined(__386__)
# ifndef SYS16BIT
# define SYS16BIT
# endif
# endif
#endif
/*
* Compile with -DMAXSEG_64K if the alloc function cannot allocate more
* than 64k bytes at a time (needed on systems with 16-bit int).
*/
#ifdef SYS16BIT
# define MAXSEG_64K
#endif
#ifdef MSDOS
# define UNALIGNED_OK
#endif
#ifdef __STDC_VERSION__
# ifndef STDC
# define STDC
# endif
# if __STDC_VERSION__ >= 199901L
# ifndef STDC99
# define STDC99
# endif
# endif
#endif
#if !defined(STDC) && (defined(__STDC__) || defined(__cplusplus))
# define STDC
#endif
#if !defined(STDC) && (defined(__GNUC__) || defined(__BORLANDC__))
# define STDC
#endif
#if !defined(STDC) && (defined(MSDOS) || defined(WINDOWS) || defined(WIN32))
# define STDC
#endif
#if !defined(STDC) && (defined(OS2) || defined(__HOS_AIX__))
# define STDC
#endif
#if defined(__OS400__) && !defined(STDC) /* iSeries (formerly AS/400). */
# define STDC
#endif
#ifndef STDC
# ifndef const /* cannot use !defined(STDC) && !defined(const) on Mac */
# define const /* note: need a more gentle solution here */
# endif
#endif
/* Some Mac compilers merge all .h files incorrectly: */
#if defined(__MWERKS__)||defined(applec)||defined(THINK_C)||defined(__SC__)
# define NO_DUMMY_DECL
#endif
/* Maximum value for memLevel in deflateInit2 */
#ifndef MAX_MEM_LEVEL
# ifdef MAXSEG_64K
# define MAX_MEM_LEVEL 8
# else
# define MAX_MEM_LEVEL 9
# endif
#endif
/* Maximum value for windowBits in deflateInit2 and inflateInit2.
* WARNING: reducing MAX_WBITS makes minigzip unable to extract .gz files
* created by gzip. (Files created by minigzip can still be extracted by
* gzip.)
*/
#ifndef MAX_WBITS
# define MAX_WBITS 15 /* 32K LZ77 window */
#endif
/* The memory requirements for deflate are (in bytes):
(1 << (windowBits+2)) + (1 << (memLevel+9))
that is: 128K for windowBits=15 + 128K for memLevel = 8 (default values)
plus a few kilobytes for small objects. For example, if you want to reduce
the default memory requirements from 256K to 128K, compile with
make CFLAGS="-O -DMAX_WBITS=14 -DMAX_MEM_LEVEL=7"
Of course this will generally degrade compression (there's no free lunch).
The memory requirements for inflate are (in bytes) 1 << windowBits
that is, 32K for windowBits=15 (default value) plus a few kilobytes
for small objects.
*/
/* Type declarations */
#ifndef OF /* function prototypes */
# ifdef STDC
# define OF(args) args
# else
# define OF(args) ()
# endif
#endif
/* The following definitions for FAR are needed only for MSDOS mixed
* model programming (small or medium model with some far allocations).
* This was tested only with MSC; for other MSDOS compilers you may have
* to define NO_MEMCPY in zutil.h. If you don't need the mixed model,
* just define FAR to be empty.
*/
#ifdef SYS16BIT
# if defined(M_I86SM) || defined(M_I86MM)
/* MSC small or medium model */
# define SMALL_MEDIUM
# ifdef _MSC_VER
# define FAR _far
# else
# define FAR far
# endif
# endif
# if (defined(__SMALL__) || defined(__MEDIUM__))
/* Turbo C small or medium model */
# define SMALL_MEDIUM
# ifdef __BORLANDC__
# define FAR _far
# else
# define FAR far
# endif
# endif
#endif
#if defined(WINDOWS) || defined(WIN32)
/* If building or using zlib as a DLL, define ZLIB_DLL.
* This is not mandatory, but it offers a little performance increase.
*/
# ifdef ZLIB_DLL
# if defined(WIN32) && (!defined(__BORLANDC__) || (__BORLANDC__ >= 0x500))
# ifdef ZLIB_INTERNAL
# define ZEXTERN extern __declspec(dllexport)
# else
# define ZEXTERN extern __declspec(dllimport)
# endif
# endif
# endif /* ZLIB_DLL */
/* If building or using zlib with the WINAPI/WINAPIV calling convention,
* define ZLIB_WINAPI.
* Caution: the standard ZLIB1.DLL is NOT compiled using ZLIB_WINAPI.
*/
# ifdef ZLIB_WINAPI
# ifdef FAR
# undef FAR
# endif
# include <windows.h>
/* No need for _export, use ZLIB.DEF instead. */
/* For complete Windows compatibility, use WINAPI, not __stdcall. */
# define ZEXPORT WINAPI
# ifdef WIN32
# define ZEXPORTVA WINAPIV
# else
# define ZEXPORTVA FAR CDECL
# endif
# endif
#endif
#if defined (__BEOS__)
# ifdef ZLIB_DLL
# ifdef ZLIB_INTERNAL
# define ZEXPORT __declspec(dllexport)
# define ZEXPORTVA __declspec(dllexport)
# else
# define ZEXPORT __declspec(dllimport)
# define ZEXPORTVA __declspec(dllimport)
# endif
# endif
#endif
#ifndef ZEXTERN
# define ZEXTERN extern
#endif
#ifndef ZEXPORT
# define ZEXPORT
#endif
#ifndef ZEXPORTVA
# define ZEXPORTVA
#endif
#ifndef FAR
# define FAR
#endif
#if !defined(__MACTYPES__)
typedef unsigned char Byte; /* 8 bits */
#endif
typedef unsigned int uInt; /* 16 bits or more */
typedef unsigned long uLong; /* 32 bits or more */
#ifdef SMALL_MEDIUM
/* Borland C/C++ and some old MSC versions ignore FAR inside typedef */
# define Bytef Byte FAR
#else
typedef Byte FAR Bytef;
#endif
typedef char FAR charf;
typedef int FAR intf;
typedef uInt FAR uIntf;
typedef uLong FAR uLongf;
#ifdef STDC
typedef void const *voidpc;
typedef void FAR *voidpf;
typedef void *voidp;
#else
typedef Byte const *voidpc;
typedef Byte FAR *voidpf;
typedef Byte *voidp;
#endif
#if 0 /* HAVE_UNISTD_H -- this line is updated by ./configure */
# include <sys/types.h> /* for off_t */
# include <unistd.h> /* for SEEK_* and off_t */
# ifdef VMS
# include <unixio.h> /* for off_t */
# endif
# define z_off_t off_t
#endif
#ifndef SEEK_SET
# define SEEK_SET 0 /* Seek from beginning of file. */
# define SEEK_CUR 1 /* Seek from current position. */
# define SEEK_END 2 /* Set file pointer to EOF plus "offset" */
#endif
#ifndef z_off_t
# define z_off_t long
#endif
#if defined(__OS400__)
# define NO_vsnprintf
#endif
#if defined(__MVS__)
# define NO_vsnprintf
# ifdef FAR
# undef FAR
# endif
#endif
/* MVS linker does not support external names larger than 8 bytes */
#if defined(__MVS__)
# pragma map(deflateInit_,"DEIN")
# pragma map(deflateInit2_,"DEIN2")
# pragma map(deflateEnd,"DEEND")
# pragma map(deflateBound,"DEBND")
# pragma map(inflateInit_,"ININ")
# pragma map(inflateInit2_,"ININ2")
# pragma map(inflateEnd,"INEND")
# pragma map(inflateSync,"INSY")
# pragma map(inflateSetDictionary,"INSEDI")
# pragma map(compressBound,"CMBND")
# pragma map(inflate_table,"INTABL")
# pragma map(inflate_fast,"INFA")
# pragma map(inflate_copyright,"INCOPY")
#endif
#endif /* ZCONF_H */

File diff suppressed because it is too large Load Diff

View File

@@ -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)

View File

@@ -1,871 +0,0 @@
#! /usr/bin/python
# -*- coding: utf-8 -*-
# unswindle.pyw, version 6-rc1
# Copyright © 2009 i♥cabbages
# Released under the terms of the GNU General Public Licence, version 3 or
# later. <http://www.gnu.org/licenses/>
# To run this program install a 32-bit version of Python 2.6 from
# <http://www.python.org/download/>. Save this script file as unswindle.pyw.
# Find and save in the same directory a copy of mobidedrm.py. Double-click on
# unswindle.pyw. It will run Kindle For PC. Open the book you want to
# decrypt. Close Kindle For PC. A dialog will open allowing you to select the
# output file. And you're done!
# Revision history:
# 1 - Initial release
# 2 - Fixes to work properly on Windows versions >XP
# 3 - Fix minor bug in path extraction
# 4 - Fix error opening threads; detect Topaz books;
# detect unsupported versions of K4PC
# 5 - Work with new (20091222) version of K4PC
# 6 - Detect and just copy DRM-free books
"""
Decrypt Kindle For PC encrypted Mobipocket books.
"""
__license__ = 'GPL v3'
import sys
import os
import re
import tempfile
import shutil
import subprocess
import struct
import hashlib
import ctypes
from ctypes import *
from ctypes.wintypes import *
import binascii
import _winreg as winreg
import Tkinter
import Tkconstants
import tkMessageBox
import tkFileDialog
import traceback
#
# _extrawintypes.py
UBYTE = c_ubyte
ULONG_PTR = POINTER(ULONG)
PULONG = ULONG_PTR
PVOID = LPVOID
LPCTSTR = LPTSTR = c_wchar_p
LPBYTE = c_char_p
SIZE_T = c_uint
SIZE_T_p = POINTER(SIZE_T)
#
# _ntdll.py
NTSTATUS = DWORD
ntdll = windll.ntdll
class PROCESS_BASIC_INFORMATION(Structure):
_fields_ = [('Reserved1', PVOID),
('PebBaseAddress', PVOID),
('Reserved2', PVOID * 2),
('UniqueProcessId', ULONG_PTR),
('Reserved3', PVOID)]
# NTSTATUS WINAPI NtQueryInformationProcess(
# __in HANDLE ProcessHandle,
# __in PROCESSINFOCLASS ProcessInformationClass,
# __out PVOID ProcessInformation,
# __in ULONG ProcessInformationLength,
# __out_opt PULONG ReturnLength
# );
NtQueryInformationProcess = ntdll.NtQueryInformationProcess
NtQueryInformationProcess.argtypes = [HANDLE, DWORD, PVOID, ULONG, PULONG]
NtQueryInformationProcess.restype = NTSTATUS
#
# _kernel32.py
INFINITE = 0xffffffff
CREATE_UNICODE_ENVIRONMENT = 0x00000400
DEBUG_ONLY_THIS_PROCESS = 0x00000002
DEBUG_PROCESS = 0x00000001
THREAD_GET_CONTEXT = 0x0008
THREAD_QUERY_INFORMATION = 0x0040
THREAD_SET_CONTEXT = 0x0010
THREAD_SET_INFORMATION = 0x0020
EXCEPTION_BREAKPOINT = 0x80000003
EXCEPTION_SINGLE_STEP = 0x80000004
EXCEPTION_ACCESS_VIOLATION = 0xC0000005
DBG_CONTINUE = 0x00010002L
DBG_EXCEPTION_NOT_HANDLED = 0x80010001L
EXCEPTION_DEBUG_EVENT = 1
CREATE_THREAD_DEBUG_EVENT = 2
CREATE_PROCESS_DEBUG_EVENT = 3
EXIT_THREAD_DEBUG_EVENT = 4
EXIT_PROCESS_DEBUG_EVENT = 5
LOAD_DLL_DEBUG_EVENT = 6
UNLOAD_DLL_DEBUG_EVENT = 7
OUTPUT_DEBUG_STRING_EVENT = 8
RIP_EVENT = 9
class DataBlob(Structure):
_fields_ = [('cbData', c_uint),
('pbData', c_void_p)]
DataBlob_p = POINTER(DataBlob)
class SECURITY_ATTRIBUTES(Structure):
_fields_ = [('nLength', DWORD),
('lpSecurityDescriptor', LPVOID),
('bInheritHandle', BOOL)]
LPSECURITY_ATTRIBUTES = POINTER(SECURITY_ATTRIBUTES)
class STARTUPINFO(Structure):
_fields_ = [('cb', DWORD),
('lpReserved', LPTSTR),
('lpDesktop', LPTSTR),
('lpTitle', LPTSTR),
('dwX', DWORD),
('dwY', DWORD),
('dwXSize', DWORD),
('dwYSize', DWORD),
('dwXCountChars', DWORD),
('dwYCountChars', DWORD),
('dwFillAttribute', DWORD),
('dwFlags', DWORD),
('wShowWindow', WORD),
('cbReserved2', WORD),
('lpReserved2', LPBYTE),
('hStdInput', HANDLE),
('hStdOutput', HANDLE),
('hStdError', HANDLE)]
LPSTARTUPINFO = POINTER(STARTUPINFO)
class PROCESS_INFORMATION(Structure):
_fields_ = [('hProcess', HANDLE),
('hThread', HANDLE),
('dwProcessId', DWORD),
('dwThreadId', DWORD)]
LPPROCESS_INFORMATION = POINTER(PROCESS_INFORMATION)
EXCEPTION_MAXIMUM_PARAMETERS = 15
class EXCEPTION_RECORD(Structure):
pass
EXCEPTION_RECORD._fields_ = [
('ExceptionCode', DWORD),
('ExceptionFlags', DWORD),
('ExceptionRecord', POINTER(EXCEPTION_RECORD)),
('ExceptionAddress', LPVOID),
('NumberParameters', DWORD),
('ExceptionInformation', ULONG_PTR * EXCEPTION_MAXIMUM_PARAMETERS)]
class EXCEPTION_DEBUG_INFO(Structure):
_fields_ = [('ExceptionRecord', EXCEPTION_RECORD),
('dwFirstChance', DWORD)]
class CREATE_THREAD_DEBUG_INFO(Structure):
_fields_ = [('hThread', HANDLE),
('lpThreadLocalBase', LPVOID),
('lpStartAddress', LPVOID)]
class CREATE_PROCESS_DEBUG_INFO(Structure):
_fields_ = [('hFile', HANDLE),
('hProcess', HANDLE),
('hThread', HANDLE),
('dwDebugInfoFileOffset', DWORD),
('nDebugInfoSize', DWORD),
('lpThreadLocalBase', LPVOID),
('lpStartAddress', LPVOID),
('lpImageName', LPVOID),
('fUnicode', WORD)]
class EXIT_THREAD_DEBUG_INFO(Structure):
_fields_ = [('dwExitCode', DWORD)]
class EXIT_PROCESS_DEBUG_INFO(Structure):
_fields_ = [('dwExitCode', DWORD)]
class LOAD_DLL_DEBUG_INFO(Structure):
_fields_ = [('hFile', HANDLE),
('lpBaseOfDll', LPVOID),
('dwDebugInfoFileOffset', DWORD),
('nDebugInfoSize', DWORD),
('lpImageName', LPVOID),
('fUnicode', WORD)]
class UNLOAD_DLL_DEBUG_INFO(Structure):
_fields_ = [('lpBaseOfDll', LPVOID)]
class OUTPUT_DEBUG_STRING_INFO(Structure):
_fields_ = [('lpDebugStringData', LPSTR),
('fUnicode', WORD),
('nDebugStringLength', WORD)]
class RIP_INFO(Structure):
_fields_ = [('dwError', DWORD),
('dwType', DWORD)]
class _U(Union):
_fields_ = [('Exception', EXCEPTION_DEBUG_INFO),
('CreateThread', CREATE_THREAD_DEBUG_INFO),
('CreateProcessInfo', CREATE_PROCESS_DEBUG_INFO),
('ExitThread', EXIT_THREAD_DEBUG_INFO),
('ExitProcess', EXIT_PROCESS_DEBUG_INFO),
('LoadDll', LOAD_DLL_DEBUG_INFO),
('UnloadDll', UNLOAD_DLL_DEBUG_INFO),
('DebugString', OUTPUT_DEBUG_STRING_INFO),
('RipInfo', RIP_INFO)]
class DEBUG_EVENT(Structure):
_anonymous_ = ('u',)
_fields_ = [('dwDebugEventCode', DWORD),
('dwProcessId', DWORD),
('dwThreadId', DWORD),
('u', _U)]
LPDEBUG_EVENT = POINTER(DEBUG_EVENT)
CONTEXT_X86 = 0x00010000
CONTEXT_i386 = CONTEXT_X86
CONTEXT_i486 = CONTEXT_X86
CONTEXT_CONTROL = (CONTEXT_i386 | 0x0001) # SS:SP, CS:IP, FLAGS, BP
CONTEXT_INTEGER = (CONTEXT_i386 | 0x0002) # AX, BX, CX, DX, SI, DI
CONTEXT_SEGMENTS = (CONTEXT_i386 | 0x0004) # DS, ES, FS, GS
CONTEXT_FLOATING_POINT = (CONTEXT_i386 | 0x0008L) # 387 state
CONTEXT_DEBUG_REGISTERS = (CONTEXT_i386 | 0x0010L) # DB 0-3,6,7
CONTEXT_EXTENDED_REGISTERS = (CONTEXT_i386 | 0x0020L)
CONTEXT_FULL = (CONTEXT_CONTROL | CONTEXT_INTEGER | CONTEXT_SEGMENTS)
CONTEXT_ALL = (CONTEXT_CONTROL | CONTEXT_INTEGER | CONTEXT_SEGMENTS |
CONTEXT_FLOATING_POINT | CONTEXT_DEBUG_REGISTERS |
CONTEXT_EXTENDED_REGISTERS)
SIZE_OF_80387_REGISTERS = 80
class FLOATING_SAVE_AREA(Structure):
_fields_ = [('ControlWord', DWORD),
('StatusWord', DWORD),
('TagWord', DWORD),
('ErrorOffset', DWORD),
('ErrorSelector', DWORD),
('DataOffset', DWORD),
('DataSelector', DWORD),
('RegisterArea', BYTE * SIZE_OF_80387_REGISTERS),
('Cr0NpxState', DWORD)]
MAXIMUM_SUPPORTED_EXTENSION = 512
class CONTEXT(Structure):
_fields_ = [('ContextFlags', DWORD),
('Dr0', DWORD),
('Dr1', DWORD),
('Dr2', DWORD),
('Dr3', DWORD),
('Dr6', DWORD),
('Dr7', DWORD),
('FloatSave', FLOATING_SAVE_AREA),
('SegGs', DWORD),
('SegFs', DWORD),
('SegEs', DWORD),
('SegDs', DWORD),
('Edi', DWORD),
('Esi', DWORD),
('Ebx', DWORD),
('Edx', DWORD),
('Ecx', DWORD),
('Eax', DWORD),
('Ebp', DWORD),
('Eip', DWORD),
('SegCs', DWORD),
('EFlags', DWORD),
('Esp', DWORD),
('SegSs', DWORD),
('ExtendedRegisters', BYTE * MAXIMUM_SUPPORTED_EXTENSION)]
LPCONTEXT = POINTER(CONTEXT)
class LDT_ENTRY(Structure):
_fields_ = [('LimitLow', WORD),
('BaseLow', WORD),
('BaseMid', UBYTE),
('Flags1', UBYTE),
('Flags2', UBYTE),
('BaseHi', UBYTE)]
LPLDT_ENTRY = POINTER(LDT_ENTRY)
kernel32 = windll.kernel32
# BOOL WINAPI CloseHandle(
# __in HANDLE hObject
# );
CloseHandle = kernel32.CloseHandle
CloseHandle.argtypes = [HANDLE]
CloseHandle.restype = BOOL
# BOOL WINAPI CreateProcess(
# __in_opt LPCTSTR lpApplicationName,
# __inout_opt LPTSTR lpCommandLine,
# __in_opt LPSECURITY_ATTRIBUTES lpProcessAttributes,
# __in_opt LPSECURITY_ATTRIBUTES lpThreadAttributes,
# __in BOOL bInheritHandles,
# __in DWORD dwCreationFlags,
# __in_opt LPVOID lpEnvironment,
# __in_opt LPCTSTR lpCurrentDirectory,
# __in LPSTARTUPINFO lpStartupInfo,
# __out LPPROCESS_INFORMATION lpProcessInformation
# );
CreateProcess = kernel32.CreateProcessW
CreateProcess.argtypes = [LPCTSTR, LPTSTR, LPSECURITY_ATTRIBUTES,
LPSECURITY_ATTRIBUTES, BOOL, DWORD, LPVOID, LPCTSTR,
LPSTARTUPINFO, LPPROCESS_INFORMATION]
CreateProcess.restype = BOOL
# HANDLE WINAPI OpenThread(
# __in DWORD dwDesiredAccess,
# __in BOOL bInheritHandle,
# __in DWORD dwThreadId
# );
OpenThread = kernel32.OpenThread
OpenThread.argtypes = [DWORD, BOOL, DWORD]
OpenThread.restype = HANDLE
# BOOL WINAPI ContinueDebugEvent(
# __in DWORD dwProcessId,
# __in DWORD dwThreadId,
# __in DWORD dwContinueStatus
# );
ContinueDebugEvent = kernel32.ContinueDebugEvent
ContinueDebugEvent.argtypes = [DWORD, DWORD, DWORD]
ContinueDebugEvent.restype = BOOL
# BOOL WINAPI DebugActiveProcess(
# __in DWORD dwProcessId
# );
DebugActiveProcess = kernel32.DebugActiveProcess
DebugActiveProcess.argtypes = [DWORD]
DebugActiveProcess.restype = BOOL
# BOOL WINAPI GetThreadContext(
# __in HANDLE hThread,
# __inout LPCONTEXT lpContext
# );
GetThreadContext = kernel32.GetThreadContext
GetThreadContext.argtypes = [HANDLE, LPCONTEXT]
GetThreadContext.restype = BOOL
# BOOL WINAPI GetThreadSelectorEntry(
# __in HANDLE hThread,
# __in DWORD dwSelector,
# __out LPLDT_ENTRY lpSelectorEntry
# );
GetThreadSelectorEntry = kernel32.GetThreadSelectorEntry
GetThreadSelectorEntry.argtypes = [HANDLE, DWORD, LPLDT_ENTRY]
GetThreadSelectorEntry.restype = BOOL
# BOOL WINAPI ReadProcessMemory(
# __in HANDLE hProcess,
# __in LPCVOID lpBaseAddress,
# __out LPVOID lpBuffer,
# __in SIZE_T nSize,
# __out SIZE_T *lpNumberOfBytesRead
# );
ReadProcessMemory = kernel32.ReadProcessMemory
ReadProcessMemory.argtypes = [HANDLE, LPCVOID, LPVOID, SIZE_T, SIZE_T_p]
ReadProcessMemory.restype = BOOL
# BOOL WINAPI SetThreadContext(
# __in HANDLE hThread,
# __in const CONTEXT *lpContext
# );
SetThreadContext = kernel32.SetThreadContext
SetThreadContext.argtypes = [HANDLE, LPCONTEXT]
SetThreadContext.restype = BOOL
# BOOL WINAPI WaitForDebugEvent(
# __out LPDEBUG_EVENT lpDebugEvent,
# __in DWORD dwMilliseconds
# );
WaitForDebugEvent = kernel32.WaitForDebugEvent
WaitForDebugEvent.argtypes = [LPDEBUG_EVENT, DWORD]
WaitForDebugEvent.restype = BOOL
# BOOL WINAPI WriteProcessMemory(
# __in HANDLE hProcess,
# __in LPVOID lpBaseAddress,
# __in LPCVOID lpBuffer,
# __in SIZE_T nSize,
# __out SIZE_T *lpNumberOfBytesWritten
# );
WriteProcessMemory = kernel32.WriteProcessMemory
WriteProcessMemory.argtypes = [HANDLE, LPVOID, LPCVOID, SIZE_T, SIZE_T_p]
WriteProcessMemory.restype = BOOL
# BOOL WINAPI FlushInstructionCache(
# __in HANDLE hProcess,
# __in LPCVOID lpBaseAddress,
# __in SIZE_T dwSize
# );
FlushInstructionCache = kernel32.FlushInstructionCache
FlushInstructionCache.argtypes = [HANDLE, LPCVOID, SIZE_T]
FlushInstructionCache.restype = BOOL
#
# debugger.py
FLAG_TRACE_BIT = 0x100
class DebuggerError(Exception):
pass
class Debugger(object):
def __init__(self, process_info):
self.process_info = process_info
self.pid = process_info.dwProcessId
self.tid = process_info.dwThreadId
self.hprocess = process_info.hProcess
self.hthread = process_info.hThread
self._threads = {self.tid: self.hthread}
self._processes = {self.pid: self.hprocess}
self._bps = {}
self._inactive = {}
def read_process_memory(self, addr, size=None, type=str):
if issubclass(type, basestring):
buf = ctypes.create_string_buffer(size)
ref = buf
else:
size = ctypes.sizeof(type)
buf = type()
ref = byref(buf)
copied = SIZE_T(0)
rv = ReadProcessMemory(self.hprocess, addr, ref, size, byref(copied))
if not rv:
addr = getattr(addr, 'value', addr)
raise DebuggerError("could not read memory @ 0x%08x" % (addr,))
if copied.value != size:
raise DebuggerError("insufficient memory read")
if issubclass(type, basestring):
return buf.raw
return buf
def set_bp(self, addr, callback, bytev=None):
hprocess = self.hprocess
if bytev is None:
byte = self.read_process_memory(addr, type=ctypes.c_byte)
bytev = byte.value
else:
byte = ctypes.c_byte(0)
self._bps[addr] = (bytev, callback)
byte.value = 0xcc
copied = SIZE_T(0)
rv = WriteProcessMemory(hprocess, addr, byref(byte), 1, byref(copied))
if not rv:
addr = getattr(addr, 'value', addr)
raise DebuggerError("could not write memory @ 0x%08x" % (addr,))
if copied.value != 1:
raise DebuggerError("insufficient memory written")
rv = FlushInstructionCache(hprocess, None, 0)
if not rv:
raise DebuggerError("could not flush instruction cache")
return
def _restore_bps(self):
for addr, (bytev, callback) in self._inactive.items():
self.set_bp(addr, callback, bytev=bytev)
self._inactive.clear()
def _handle_bp(self, addr):
hprocess = self.hprocess
hthread = self.hthread
bytev, callback = self._inactive[addr] = self._bps.pop(addr)
byte = ctypes.c_byte(bytev)
copied = SIZE_T(0)
rv = WriteProcessMemory(hprocess, addr, byref(byte), 1, byref(copied))
if not rv:
raise DebuggerError("could not write memory")
if copied.value != 1:
raise DebuggerError("insufficient memory written")
rv = FlushInstructionCache(hprocess, None, 0)
if not rv:
raise DebuggerError("could not flush instruction cache")
context = CONTEXT(ContextFlags=CONTEXT_FULL)
rv = GetThreadContext(hthread, byref(context))
if not rv:
raise DebuggerError("could not get thread context")
context.Eip = addr
callback(self, context)
context.EFlags |= FLAG_TRACE_BIT
rv = SetThreadContext(hthread, byref(context))
if not rv:
raise DebuggerError("could not set thread context")
return
def _get_peb_address(self):
hthread = self.hthread
hprocess = self.hprocess
try:
pbi = PROCESS_BASIC_INFORMATION()
rv = NtQueryInformationProcess(hprocess, 0, byref(pbi),
sizeof(pbi), None)
if rv != 0:
raise DebuggerError("could not query process information")
return pbi.PebBaseAddress
except DebuggerError:
pass
try:
context = CONTEXT(ContextFlags=CONTEXT_FULL)
rv = GetThreadContext(hthread, byref(context))
if not rv:
raise DebuggerError("could not get thread context")
entry = LDT_ENTRY()
rv = GetThreadSelectorEntry(hthread, context.SegFs, byref(entry))
if not rv:
raise DebuggerError("could not get selector entry")
low, mid, high = entry.BaseLow, entry.BaseMid, entry.BaseHi
fsbase = low | (mid << 16) | (high << 24)
pebaddr = self.read_process_memory(fsbase + 0x30, type=c_voidp)
return pebaddr.value
except DebuggerError:
pass
return 0x7ffdf000
def get_base_address(self):
addr = self._get_peb_address() + (2 * 4)
baseaddr = self.read_process_memory(addr, type=c_voidp)
return baseaddr.value
def main_loop(self):
event = DEBUG_EVENT()
finished = False
while not finished:
rv = WaitForDebugEvent(byref(event), INFINITE)
if not rv:
raise DebuggerError("could not get debug event")
self.pid = pid = event.dwProcessId
self.tid = tid = event.dwThreadId
self.hprocess = self._processes.get(pid, None)
self.hthread = self._threads.get(tid, None)
status = DBG_CONTINUE
evid = event.dwDebugEventCode
if evid == EXCEPTION_DEBUG_EVENT:
first = event.Exception.dwFirstChance
record = event.Exception.ExceptionRecord
exid = record.ExceptionCode
flags = record.ExceptionFlags
addr = record.ExceptionAddress
if exid == EXCEPTION_BREAKPOINT:
if addr in self._bps:
self._handle_bp(addr)
elif exid == EXCEPTION_SINGLE_STEP:
self._restore_bps()
else:
status = DBG_EXCEPTION_NOT_HANDLED
elif evid == LOAD_DLL_DEBUG_EVENT:
hfile = event.LoadDll.hFile
if hfile is not None:
rv = CloseHandle(hfile)
if not rv:
raise DebuggerError("error closing file handle")
elif evid == CREATE_THREAD_DEBUG_EVENT:
info = event.CreateThread
self.hthread = info.hThread
self._threads[tid] = self.hthread
elif evid == EXIT_THREAD_DEBUG_EVENT:
hthread = self._threads.pop(tid, None)
if hthread is not None:
rv = CloseHandle(hthread)
if not rv:
raise DebuggerError("error closing thread handle")
elif evid == CREATE_PROCESS_DEBUG_EVENT:
info = event.CreateProcessInfo
self.hprocess = info.hProcess
self._processes[pid] = self.hprocess
elif evid == EXIT_PROCESS_DEBUG_EVENT:
hprocess = self._processes.pop(pid, None)
if hprocess is not None:
rv = CloseHandle(hprocess)
if not rv:
raise DebuggerError("error closing process handle")
if pid == self.process_info.dwProcessId:
finished = True
rv = ContinueDebugEvent(pid, tid, status)
if not rv:
raise DebuggerError("could not continue debug")
return True
#
# unswindle.py
KINDLE_REG_KEY = \
r'Software\Classes\Amazon.KindleForPC.content\shell\open\command'
class UnswindleError(Exception):
pass
class PC1KeyGrabber(object):
HOOKS = {
'b9f7e422094b8c8966a0e881e6358116e03e5b7b': {
0x004a719d: '_no_debugger_here',
0x005a795b: '_no_debugger_here',
0x0054f7e0: '_get_pc1_pid',
0x004f9c79: '_get_book_path',
},
'd5124ee20dab10e44b41a039363f6143725a5417': {
0x0041150d: '_i_like_wine',
0x004a681d: '_no_debugger_here',
0x005a438b: '_no_debugger_here',
0x0054c9e0: '_get_pc1_pid',
0x004f8ac9: '_get_book_path',
},
}
@classmethod
def supported_version(cls, hexdigest):
return (hexdigest in cls.HOOKS)
def _taddr(self, addr):
return (addr - 0x00400000) + self.baseaddr
def __init__(self, debugger, hexdigest):
self.book_path = None
self.book_pid = None
self.baseaddr = debugger.get_base_address()
hooks = self.HOOKS[hexdigest]
for addr, mname in hooks.items():
debugger.set_bp(self._taddr(addr), getattr(self, mname))
def _i_like_wine(self, debugger, context):
context.Eax = 1
return
def _no_debugger_here(self, debugger, context):
context.Eip += 2
context.Eax = 0
return
def _get_book_path(self, debugger, context):
addr = debugger.read_process_memory(context.Esp, type=ctypes.c_voidp)
try:
path = debugger.read_process_memory(addr, 4096)
except DebuggerError:
pgrest = 0x1000 - (addr.value & 0xfff)
path = debugger.read_process_memory(addr, pgrest)
path = path.decode('utf-16', 'ignore')
if u'\0' in path:
path = path[:path.index(u'\0')]
if path[-4:].lower() not in ('.prc', '.pdb', '.mobi'):
return
self.book_path = path
def _get_pc1_pid(self, debugger, context):
addr = context.Esp + ctypes.sizeof(ctypes.c_voidp)
addr = debugger.read_process_memory(addr, type=ctypes.c_char_p)
pid = debugger.read_process_memory(addr, 8)
pid = self._checksum_pid(pid)
print pid
self.book_pid = pid
def _checksum_pid(self, 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
class MobiParser(object):
def __init__(self, data):
self.data = data
header = data[0:72]
if header[0x3C:0x3C+8] != 'BOOKMOBI':
raise UnswindleError("invalid file format")
self.nsections = nsections = struct.unpack('>H', data[76:78])[0]
self.sections = sections = []
for i in xrange(nsections):
offset, a1, a2, a3, a4 = \
struct.unpack('>LBBBB', data[78+i*8:78+i*8+8])
flags, val = a1, ((a2 << 16) | (a3 << 8) | a4)
sections.append((offset, flags, val))
sect = self.load_section(0)
self.crypto_type = struct.unpack('>H', sect[0x0c:0x0c+2])[0]
def load_section(self, snum):
if (snum + 1) == self.nsections:
endoff = len(self.data)
else:
endoff = self.sections[snum + 1][0]
off = self.sections[snum][0]
return self.data[off:endoff]
class Unswindler(object):
def __init__(self):
self._exepath = self._get_exe_path()
self._hexdigest = self._get_hexdigest()
self._exedir = os.path.dirname(self._exepath)
self._mobidedrmpath = self._get_mobidedrm_path()
def _get_mobidedrm_path(self):
basedir = sys.modules[self.__module__].__file__
basedir = os.path.dirname(basedir)
for basename in ('mobidedrm', 'mobidedrm.py', 'mobidedrm.pyw'):
path = os.path.join(basedir, basename)
if os.path.isfile(path):
return path
raise UnswindleError("could not locate MobiDeDRM script")
def _get_exe_path(self):
path = None
for root in (winreg.HKEY_CURRENT_USER, winreg.HKEY_LOCAL_MACHINE):
try:
regkey = winreg.OpenKey(root, KINDLE_REG_KEY)
path = winreg.QueryValue(regkey, None)
break
except WindowsError:
pass
else:
raise UnswindleError("Kindle For PC installation not found")
if '"' in path:
path = re.search(r'"(.*?)"', path).group(1)
return path
def _get_hexdigest(self):
path = self._exepath
sha1 = hashlib.sha1()
with open(path, 'rb') as f:
data = f.read(4096)
while data:
sha1.update(data)
data = f.read(4096)
hexdigest = sha1.hexdigest()
if not PC1KeyGrabber.supported_version(hexdigest):
raise UnswindleError("Unsupported version of Kindle For PC")
return hexdigest
def _check_topaz(self, path):
with open(path, 'rb') as f:
magic = f.read(4)
if magic == 'TPZ0':
return True
return False
def _check_drm_free(self, path):
with open(path, 'rb') as f:
crypto = MobiParser(f.read()).crypto_type
return (crypto == 0)
def get_book(self):
creation_flags = (CREATE_UNICODE_ENVIRONMENT |
DEBUG_PROCESS |
DEBUG_ONLY_THIS_PROCESS)
startup_info = STARTUPINFO()
process_info = PROCESS_INFORMATION()
path = pid = None
try:
rv = CreateProcess(self._exepath, None, None, None, False,
creation_flags, None, self._exedir,
byref(startup_info), byref(process_info))
if not rv:
raise UnswindleError("failed to launch Kindle For PC")
debugger = Debugger(process_info)
grabber = PC1KeyGrabber(debugger, self._hexdigest)
debugger.main_loop()
path = grabber.book_path
pid = grabber.book_pid
finally:
if process_info.hThread is not None:
CloseHandle(process_info.hThread)
if process_info.hProcess is not None:
CloseHandle(process_info.hProcess)
if path is None:
raise UnswindleError("failed to determine book path")
if self._check_topaz(path):
raise UnswindleError("cannot decrypt Topaz format book")
return (path, pid)
def decrypt_book(self, inpath, outpath, pid):
if self._check_drm_free(inpath):
shutil.copy(inpath, outpath)
else:
self._mobidedrm(inpath, outpath, pid)
return
def _mobidedrm(self, inpath, outpath, pid):
# darkreverser didn't protect mobidedrm's script execution to allow
# importing, so we have to just run it in a subprocess
if pid is None:
raise UnswindleError("failed to determine book PID")
with tempfile.NamedTemporaryFile(delete=False) as tmpf:
tmppath = tmpf.name
args = [sys.executable, self._mobidedrmpath, inpath, tmppath, pid]
mobidedrm = subprocess.Popen(args, stderr=subprocess.STDOUT,
stdout=subprocess.PIPE,
universal_newlines=True)
output = mobidedrm.communicate()[0]
if not output.endswith("done\n"):
try:
os.remove(tmppath)
except OSError:
pass
raise UnswindleError("problem running MobiDeDRM:\n" + output)
shutil.move(tmppath, outpath)
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 gui_main(argv=sys.argv):
root = Tkinter.Tk()
root.withdraw()
progname = os.path.basename(argv[0])
try:
unswindler = Unswindler()
inpath, pid = unswindler.get_book()
outpath = tkFileDialog.asksaveasfilename(
parent=None, title='Select unencrypted Mobipocket file to produce',
defaultextension='.mobi', filetypes=[('MOBI files', '.mobi'),
('All files', '.*')])
if not outpath:
return 0
unswindler.decrypt_book(inpath, outpath, pid)
except UnswindleError, e:
tkMessageBox.showerror("Unswindle For PC", "Error: " + str(e))
return 1
except Exception:
root.wm_state('normal')
root.title('Unswindle For PC')
text = traceback.format_exc()
ExceptionDialog(root, text).pack(fill=Tkconstants.BOTH, expand=1)
root.mainloop()
return 1
def cli_main(argv=sys.argv):
progname = os.path.basename(argv[0])
args = argv[1:]
if len(args) != 1:
sys.stderr.write("usage: %s OUTFILE\n" % (progname,))
return 1
outpath = args[0]
unswindler = Unswindler()
inpath, pid = unswindler.get_book()
unswindler.decrypt_book(inpath, outpath, pid)
return 0
if __name__ == '__main__':
sys.exit(gui_main())

View File

@@ -1,12 +0,0 @@
Mobipocket Unlocker
How to get Drag&Drop decryption of DRM-encumbered Mobipocket eBook files.
You'll need the MobiDeDRM.py python script, as well as an installed version 2.4 or later of python. If you have Mac OS X Leopard (10.5) you already have a suitable version of python installed as part of Leopard.
Control-click the script and select "Show Package Contents" from the contextual menu. Copy the python script, which must be called "MobiDeDRM.py" into the Resources folder inside the Contents folder. (NB not into the Scripts folder - that's where the Applescript part is stored.)
Close the package, and you now have a drag&drop Mobipocket unlocker.
You can use the AppleScript ScriptEditor application to put your Mobipocket code into the script to save you having to enter it in the dialog all the time.

View File

@@ -1,249 +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!
import sys,struct,binascii
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
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
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:
raise DrmException("it seems that this book isn't encrypted")
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, 0, 9) # 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):
of = self.temporary_file('.mobi')
PID = self.site_customization
data_file = file(path_to_ebook, 'rb').read()
ar = PID.split(',')
for i in ar:
try:
file(of.name, 'wb').write(DrmStripper(data_file, i).getResult())
except DrmException:
# Hm, we should display an error dialog here.
# Dunno how though.
# Ignore the dirty hack behind the curtain.
# strexcept = 'echo exception: %s > /dev/tty' % e
# subprocess.call(strexcept,shell=True)
print i + ": not PID for book"
else:
return of.name
def customization_help(self, gui=False):
return 'Enter PID (separate multiple PIDs with comma)'
if __name__ == "__main__":
print "MobiDeDrm v0.09. Copyright (c) 2008 The Dark Reverser"
if len(sys.argv)<4:
print "Removes protection from Mobipocket books"
print "Usage:"
print " mobidedrm infile.mobi outfile.mobi PID"
else:
infile = sys.argv[1]
outfile = sys.argv[2]
pid = sys.argv[3]
data_file = file(infile, 'rb').read()
try:
file(outfile, 'wb').write(DrmStripper(data_file, pid).getResult())
except DrmException, e:
print "Error: %s" % e

View File

@@ -1,61 +0,0 @@
on unlockfile(encryptedFile, MobiDeDRMPath, encryptionKey)
set encryptedFilePath to POSIX path of file encryptedFile
-- display dialog "filepath " & encryptedFilePath buttons {"OK"} default button 1 giving up after 10
tell application "Finder" to <20>
set parent_folder to (container of file encryptedFile) as text
tell application "Finder" to set fileName to (name of file encryptedFile) as text
set unlockedFilePath to POSIX path of file (parent_folder & "Unlocked_" & fileName)
set shellcommand to "python '" & MobiDeDRMPath & "' '" & encryptedFilePath & "' '" & unlockedFilePath & "' '" & encryptionKey & "'"
-- display dialog "shellcommand: " & shellcommand buttons {"OK"} default button 1 giving up after 10
try
--with timeout of 5 seconds
-- display dialog "About to Unlock " & fileName buttons {"Unlock"} default button 1 giving up after 1
--end timeout
end try
set result to do shell script shellcommand
try
if (offset of "Error" in result) > 0 then
with timeout of 5 seconds
display dialog "Can't unlock file " & fileName & ".
" & result buttons ("OK") default button 1 giving up after 5
end timeout
end if
end try
end unlockfile
on unlockfolder(encryptedFolder, MobiDeDRMPath, encryptionKey)
tell application "Finder" to set encryptedFileList to (every file in folder encryptedFolder) whose (name extension is "prc") or (name extension is "mobi") or (name extension is "azw")
tell application "Finder" to set encryptedFolderList to (every folder in folder encryptedFolder)
repeat with this_item in encryptedFileList
unlockfile(this_item as text, MobiDeDRMPath, encryptionKey)
end repeat
repeat with this_item in encryptedFolderList
unlockfolder(this_item as text, MobiDeDRMPath, encryptionKey)
end repeat
end unlockfolder
on run
set MobiDeDRMPath to POSIX path of file ((path to me as text) & "Contents:Resources:MobiDeDRM.py")
set encryptedFolder to choose folder with prompt "Please choose the folder of encrypted Mobipocket files."
set encryptionKey to (display dialog "Enter Mobipocket key for encrypted Mobipocket files." default answer "X12QIL1M3D" buttons {"Cancel", "OK"} default button 2)
set encryptionKey to text returned of encryptionKey
unlockfolder(encryptedFolder, MobiDeDRMPath, encryptionKey)
end run
on open some_items
set MobiDeDRMPath to POSIX path of file ((path to me as text) & "Contents:Resources:MobiDeDRM.py")
set encryptionKey to (display dialog "Enter Mobipocket key for encrypted Mobipocket files." default answer "X12QIL1M3D" buttons {"Cancel", "OK"} default button 2)
set encryptionKey to text returned of encryptionKey
repeat with this_item in some_items
if (folder of (info for this_item) is true) then
unlockfolder(this_item as text, MobiDeDRMPath, encryptionKey)
else
tell application "Finder" to set item_extension to name extension of file this_item
if item_extension is "prc" or item_extension is "mobi" or item_extension is "azw" then
unlockfile(this_item as text, MobiDeDRMPath, encryptionKey)
end if
end if
end repeat
end open

View File

@@ -1,4 +0,0 @@
{\rtf1\ansi\ansicpg1252\cocoartf949\cocoasubrtf540
{\fonttbl}
{\colortbl;\red255\green255\blue255;}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 336 B

View File

@@ -1,50 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleAllowMixedLocalizations</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeExtensions</key>
<array>
<string>*</string>
</array>
<key>CFBundleTypeOSTypes</key>
<array>
<string>****</string>
</array>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
</dict>
</array>
<key>CFBundleExecutable</key>
<string>droplet</string>
<key>CFBundleIconFile</key>
<string>droplet</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>Mobipocket Unlocker</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleSignature</key>
<string>dplt</string>
<key>LSRequiresCarbon</key>
<true/>
<key>WindowState</key>
<dict>
<key>name</key>
<string>ScriptWindowState</string>
<key>positionOfDivider</key>
<real>627</real>
<key>savedFrame</key>
<string>53 78 661 691 0 0 1280 778 </string>
<key>selectedTabView</key>
<string>result</string>
</dict>
</dict>
</plist>

View File

@@ -1,59 +0,0 @@
on unlockfile(encryptedFile, eReaderDeDRMPath, encryptionNameKey, encryptionKey)
set encryptedFilePath to POSIX path of file encryptedFile
tell application "Finder" to <20>
set parent_folder to (container of file encryptedFile) as text
tell application "Finder" to set fileName to (name of file encryptedFile) as text
set unlockedFilePath to POSIX path of file (parent_folder & "Unlocked_" & fileName)
set shellcommand to "python \"" & eReaderDeDRMPath & "\" \"" & encryptedFilePath & "\" \"" & unlockedFilePath & "\" \"" & encryptionNameKey & "\" " & encryptionKey
try
--with timeout of 5 seconds
--display dialog "About to Unlock " & fileName buttons {"Unlock"} default button 1 giving up after 1
--end timeout
end try
set result to do shell script shellcommand
try
--with timeout of 5 seconds
--display dialog "Result" default answer result buttons ("OK") default button 1 --giving up after 2
--end timeout
end try
end unlockfile
on unlockfolder(encryptedFolder, eReaderDeDRMPath, encryptionNameKey, encryptionKey)
tell application "Finder" to set encryptedFileList to (every file in folder encryptedFolder) whose (name extension is "pdb")
tell application "Finder" to set encryptedFolderList to (every folder in folder encryptedFolder)
repeat with this_item in encryptedFileList
unlockfile(this_item as text, eReaderDeDRMPath, encryptionNameKey, encryptionKey)
end repeat
repeat with this_item in encryptedFolderList
unlockfolder(this_item as text, eReaderDeDRMPath, encryptionNameKey, encryptionKey)
end repeat
end unlockfolder
on run
set eReaderDeDRMPath to POSIX path of file ((path to me as text) & "Contents:Resources:eReaderDeDRM.py")
set encryptedFolder to choose folder with prompt "Please choose the folder of encrypted eReader files."
set encryptionNameKey to (display dialog "Enter Name key for encrypted eReader files." default answer "MR PAUL M P DURRANT" buttons {"Cancel", "OK"} default button 2)
set encryptionKey to (display dialog "Enter Number key for encrypted eReader files." default answer "89940827" buttons {"Cancel", "OK"} default button 2)
set encryptionNameKey to text returned of encryptionNameKey
set encryptionKey to text returned of encryptionKey
unlockfolder(encryptedFolder, eReaderDeDRMPath, encryptionNameKey, encryptionKey)
end run
on open some_items
set eReaderDeDRMPath to POSIX path of file ((path to me as text) & "Contents:Resources:eReaderDeDRM.py")
set encryptionNameKey to (display dialog "Enter Name key for encrypted eReader files." default answer "MR PAUL M P DURRANT" buttons {"Cancel", "OK"} default button 2)
set encryptionKey to (display dialog "Enter Number key for encrypted eReader files." default answer "89940827" buttons {"Cancel", "OK"} default button 2)
set encryptionNameKey to text returned of encryptionNameKey
set encryptionKey to text returned of encryptionKey
repeat with this_item in some_items
if (folder of (info for this_item) is true) then
unlockfolder(this_item as text, eReaderDeDRMPath, encryptionNameKey, encryptionKey)
else
tell application "Finder" to set item_extension to name extension of file this_item
if item_extension is "pdb" then
unlockfile(this_item as text, eReaderDeDRMPath, encryptionNameKey, encryptionKey)
end if
end if
end repeat
end open

View File

@@ -1,4 +0,0 @@
{\rtf1\ansi\ansicpg1252\cocoartf949\cocoasubrtf330
{\fonttbl}
{\colortbl;\red255\green255\blue255;}
}

View File

@@ -1,493 +0,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 - Support more eReader files. Support bold text and links. Fix PML decoder parsing bug.
# 0.03 - Fix incorrect variable usage at one place.
import struct, binascii, zlib, os, sha, sys, os.path
ECB = 0
CBC = 1
class Des:
__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 map(lambda x: block[x], 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 = map(lambda x, y: x ^ y, 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 = map(lambda x, y: x ^ y, 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 = map(lambda x, y: x ^ y, block, iv)
processed_block = self.__des_crypt(block, crypt_type)
if crypt_type == Des.DECRYPT:
processed_block = map(lambda x, y: x ^ y, 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)
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 sanitizeFileName(s):
r = ''
for c in s.lower():
if c in "abcdefghijklmnopqrstuvwxyz0123456789_.-":
r += c
return r
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 "".join([chr(fixByte(ord(a))) for a in key])
class EreaderProcessor:
def __init__(self, section_reader, username, creditcard):
self.section_reader = section_reader
data = section_reader(0)
version, = struct.unpack('>H', data[0:2])
if version != 272 and version != 260:
raise ValueError('incorrect eReader version %d (error 1)' % version)
data = 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 = [''] * len(data)
j = 0
for i in xrange(len(data)):
j = (j + shuf) % len(data)
r[j] = data[i]
assert len("".join(r)) == len(data)
return "".join(r)
r = unshuff(input[0:-8], cookie_shuf)
def fixUsername(s):
r = ''
for c in s.lower():
if (c >= 'a' and c <= 'z' or c >= '0' and c <= '9'):
r += c
return r
user_key = struct.pack('>LL', binascii.crc32(fixUsername(username)) & 0xffffffff, binascii.crc32(creditcard[-8:])& 0xffffffff)
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]
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 == 260:
if drm_sub_version != 13:
raise ValueError('incorrect eReader version %d (error 3)' % drm_sub_version)
encrypted_key = r[44:44+8]
encrypted_key_sha = r[52:52+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 sha.new(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('\0')
data = sect[62:]
return sanitizeFileName(name), data
def getText(self):
des = Des(fixKey(self.content_key))
r = ''
for i in xrange(self.num_text_pages):
r += zlib.decompress(des.decrypt(self.section_reader(1 + i)))
return r
class PmlConverter:
def __init__(self, s):
self.s = s
self.pos = 0
def nextOptAttr(self):
p = self.pos
if self.s[p:p+2] != '="':
return None
r = ''
p += 2
while self.s[p] != '"':
r += self.s[p]
p += 1
self.pos = p + 1
return r
def next(self):
p = self.pos
if p >= len(self.s):
return None
if self.s[p] != '\\':
res = self.s.find('\\', p)
if res == -1:
res = len(self.s)
self.pos = res
return self.s[p : res], None, None
c = self.s[p+1]
if c in 'pxcriuovtnsblBk-lI\\d':
self.pos = p + 2
return None, c, None
if c in 'TwmqQ':
self.pos = p + 2
return None, c, self.nextOptAttr()
if c == 'a':
self.pos = p + 5
return None, c, int(self.s[p+2:p+5])
if c == 'U':
self.pos = p + 6
return None, c, int(self.s[p+2:p+6], 16)
c = self.s[p+1:p+1+2]
if c in ('X0','X1','X2','X3','X4','Sp','Sb'):
self.pos = p + 3
return None, c, None
if c in ('C0','C1','C2','C3','C4','Fn','Sd'):
self.pos = p + 3
return None, c, self.nextOptAttr()
print "unknown escape code %s" % c
self.pos = p + 1
return None, None, None
def linkPrinter(link):
return '<a href="%s">' % link
html_tags = {
'c' : ('<p align="center">', '</p>'),
'r' : ('<p align="right">', '</p>'),
'i' : ('<i>', '</i>'),
'u' : ('<u>', '</u>'),
'b' : ('<strong>', '</strong>'),
'B' : ('<strong>', '</strong>'),
'o' : ('<strike>', '</strike>'),
'v' : ('<!-- ', ' -->'),
't' : ('', ''),
'Sb' : ('<sub>', '</sub>'),
'Sp' : ('<sup>', '</sup>'),
'X0' : ('<h1>', '</h1>'),
'X1' : ('<h2>', '</h2>'),
'X2' : ('<h3>', '</h3>'),
'X3' : ('<h4>', '</h4>'),
'X4' : ('<h5>', '</h5>'),
'l' : ('<font size="+2">', '</font>'),
'q' : (linkPrinter, '</a>'),
}
html_one_tags = {
'p' : '<br><br>'
}
pml_chars = {
160 : '&nbsp;',130 : '&#8212;',131: '&#402;',132: '&#8222;',
133: '&#8230;',134: '&#8224;',135: '&#8225;',138: '&#352;',
139: '&#8249;',140: '&#338;',145: '&#8216;',146: '&#8217;',
147: '&#8220;',148: '&#8221;',149: '&#8226;',150: '&#8211;',
151: '&#8212;',153: '&#8482;',154: '&#353;',155: '&#8250;',
156: '&#339;',159: '&#376;'
}
def process(self):
final = '<html><body>\n'
in_tags = []
def makeText(s):
s = s.replace('&', '&amp;')
#s = s.replace('"', '&quot;')
s = s.replace('<', '&lt;')
s = s.replace('>', '&gt;')
s = s.replace('\n', '<br>\n')
return s
while True:
r = self.next()
if not r:
break
text, cmd, attr = r
if text:
final += makeText(text)
if cmd:
def getTag(ti, end):
cmd, attr = ti
r = self.html_tags[cmd][end]
if type(r) != str:
r = r(attr)
return r
if cmd in self.html_tags:
pair = (cmd, attr)
if cmd not in [a for (a,b) in in_tags]:
final += getTag(pair, False)
in_tags.append(pair)
else:
j = len(in_tags)
while True:
j = j - 1
final += getTag(in_tags[j], True)
if in_tags[j][0] == cmd:
break
del in_tags[j]
while j < len(in_tags):
final += getTag(in_tags[j], False)
j = j + 1
if cmd in self.html_one_tags:
final += self.html_one_tags[cmd]
if cmd == 'm':
final += '<img src="%s">' % attr
if cmd == 'Q':
final += '<a name="%s"> </a>' % attr
if cmd == 'a':
final += self.pml_chars.get(attr, '&#%d;' % attr)
if cmd == 'U':
final += '&#%d;' % attr
final += '</body></html>\n'
while True:
s = final.replace('<br>\n<br>\n<br>\n', '<br>\n<br>\n')
if s == final:
break
final = s
return final
def convertEreaderToHtml(infile, name, cc, outdir):
if not os.path.exists(outdir):
os.makedirs(outdir)
sect = Sectionizer(infile, 'PNRdPPrs')
er = EreaderProcessor(sect.loadSection, name, cc)
for i in xrange(er.getNumImages()):
name, contents = er.getImage(i)
file(os.path.join(outdir, name), 'wb').write(contents)
pml = PmlConverter(er.getText())
file(os.path.join(outdir, 'book.html'),'wb').write(pml.process())
print "eReader2Html v0.03. Copyright (c) 2008 The Dark Reverser"
if len(sys.argv)!=5:
print "Converts eReader books to HTML"
print "Usage:"
print " ereader2html infile.pdb outdir \"your name\" credit_card_number "
print "Note:"
print " It's enough to enter the last 8 digits of the credit card number"
else:
infile, outdir, name, cc = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4]
try:
print "Processing...",
convertEreaderToHtml(infile, name, cc, outdir)
print "done"
except ValueError, e:
print "Error: %s" % e

Binary file not shown.

View File

@@ -0,0 +1,75 @@
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__docformat__ = 'restructuredtext en'
#####################################################################
# Plug-in base class
#####################################################################
from calibre.customize import InterfaceActionBase
try:
load_translations()
except NameError:
pass # load_translations() added in calibre 1.9
PLUGIN_NAME = 'Obok DeDRM'
PLUGIN_SAFE_NAME = PLUGIN_NAME.strip().lower().replace(' ', '_')
PLUGIN_DESCRIPTION = _('Removes DRM from Kobo kepubs and adds them to the library.')
PLUGIN_VERSION_TUPLE = (6, 5, 4)
PLUGIN_VERSION = '.'.join([str(x) for x in PLUGIN_VERSION_TUPLE])
HELPFILE_NAME = PLUGIN_SAFE_NAME + '_Help.htm'
PLUGIN_AUTHORS = 'Anon'
#####################################################################
class ObokDeDRMAction(InterfaceActionBase):
name = PLUGIN_NAME
description = PLUGIN_DESCRIPTION
supported_platforms = ['windows', 'osx', 'linux' ]
author = PLUGIN_AUTHORS
version = PLUGIN_VERSION_TUPLE
minimum_calibre_version = (1, 0, 0)
#: This field defines the GUI plugin class that contains all the code
#: that actually does something. Its format is module_path:class_name
#: The specified class must be defined in the specified module.
actual_plugin = 'calibre_plugins.'+PLUGIN_SAFE_NAME+'.action:InterfacePluginAction'
def is_customizable(self):
'''
This method must return True to enable customization via
Preferences->Plugins
'''
return True
def config_widget(self):
'''
Implement this method and :meth:`save_settings` in your plugin to
use a custom configuration dialog.
This method, if implemented, must return a QWidget. The widget can have
an optional method validate() that takes no arguments and is called
immediately after the user clicks OK. Changes are applied if and only
if the method returns True.
If for some reason you cannot perform the configuration at this time,
return a tuple of two strings (message, details), these will be
displayed as a warning dialog to the user and the process will be
aborted.
The base class implementation of this method raises NotImplementedError
so by default no user configuration is possible.
'''
if self.actual_plugin_:
from calibre_plugins.obok_dedrm.config import ConfigWidget
return ConfigWidget(self.actual_plugin_)
def save_settings(self, config_widget):
'''
Save the settings specified by the user with config_widget.
'''
config_widget.save_settings()

View File

@@ -0,0 +1,497 @@
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__docformat__ = 'restructuredtext en'
import os, traceback, zipfile
try:
from PyQt5.Qt import QToolButton, QUrl
except ImportError:
from PyQt4.Qt import QToolButton, QUrl
from calibre.gui2 import open_url, question_dialog
from calibre.gui2.actions import InterfaceAction
from calibre.utils.config import config_dir
from calibre.ptempfile import (PersistentTemporaryDirectory,
PersistentTemporaryFile, remove_dir)
from calibre.ebooks.metadata.meta import get_metadata
from calibre_plugins.obok_dedrm.dialogs import (SelectionDialog, DecryptAddProgressDialog,
AddEpubFormatsProgressDialog, ResultsSummaryDialog)
from calibre_plugins.obok_dedrm.config import plugin_prefs as cfg
from calibre_plugins.obok_dedrm.__init__ import (PLUGIN_NAME, PLUGIN_SAFE_NAME,
PLUGIN_VERSION, PLUGIN_DESCRIPTION, HELPFILE_NAME)
from calibre_plugins.obok_dedrm.utilities import (
get_icon, set_plugin_icon_resources, showErrorDlg, format_plural,
debug_print
)
from calibre_plugins.obok_dedrm.obok.obok import KoboLibrary
from calibre_plugins.obok_dedrm.obok.legacy_obok import legacy_obok
PLUGIN_ICONS = ['images/obok.png']
try:
debug_print("obok::action_err.py - loading translations")
load_translations()
except NameError:
debug_print("obok::action_err.py - exception when loading translations")
pass # load_translations() added in calibre 1.9
class InterfacePluginAction(InterfaceAction):
name = PLUGIN_NAME
action_spec = (PLUGIN_NAME, None,
_(PLUGIN_DESCRIPTION), None)
popup_type = QToolButton.InstantPopup
action_type = 'current'
def genesis(self):
icon_resources = self.load_resources(PLUGIN_ICONS)
set_plugin_icon_resources(PLUGIN_NAME, icon_resources)
self.qaction.setIcon(get_icon(PLUGIN_ICONS[0]))
self.qaction.triggered.connect(self.launchObok)
self.gui.keyboard.finalize()
def launchObok(self):
'''
Main processing/distribution method
'''
self.count = 0
self.books_to_add = []
self.formats_to_add = []
self.add_books_cancelled = False
self.decryption_errors = []
self.userkeys = []
self.duplicate_book_list = []
self.no_home_for_book = []
self.ids_of_new_books = []
self.successful_format_adds =[]
self.add_formats_cancelled = False
self.tdir = PersistentTemporaryDirectory('_obok', prefix='')
self.db = self.gui.current_db.new_api
self.current_idx = self.gui.library_view.currentIndex()
print ('Running {}'.format(PLUGIN_NAME + ' v' + PLUGIN_VERSION))
#
# search for connected device in case serials are saved
tmpserials = cfg['kobo_serials']
device_path = None
try:
device = self.parent().device_manager.connected_device
if (device):
device_path = device._main_prefix
debug_print("get_device_settings - device_path=", device_path)
else:
debug_print("didn't find device")
except:
debug_print("Exception getting device path. Probably not an E-Ink Kobo device")
# Get the Kobo Library object (obok v3.01)
self.library = KoboLibrary(tmpserials, device_path)
debug_print ("got kobodir %s" % self.library.kobodir)
if (self.library.kobodir == ''):
# linux and no device connected, but could be extended
# to the case where on Windows/Mac the prog is not installed
msg = _('<p>Could not find Kobo Library\n<p>Windows/Mac: do you have Kobo Desktop installed?\n<p>Windows/Mac/Linux: In case you have an Kobo eInk device, connect the device.')
showErrorDlg(msg, None)
return
# Get a list of Kobo titles
books = self.build_book_list()
if len(books) < 1:
msg = _('<p>No books found in Kobo Library\nAre you sure it\'s installed\configured\synchronized?')
showErrorDlg(msg, None)
return
# Check to see if a key can be retrieved using the legacy obok method.
legacy_key = legacy_obok().get_legacy_cookie_id
if legacy_key is not None:
print (_('Legacy key found: '), legacy_key.encode('hex_codec'))
self.userkeys.append(legacy_key)
# Add userkeys found through the normal obok method to the list to try.
try:
candidate_keys = self.library.userkeys
except:
print (_('Trouble retrieving keys with newer obok method.'))
traceback.print_exc()
else:
if len(candidate_keys):
self.userkeys.extend(candidate_keys)
print (_('Found {0} possible keys to try.').format(len(self.userkeys)))
if not len(self.userkeys):
msg = _('<p>No userkeys found to decrypt books with. No point in proceeding.')
showErrorDlg(msg, None)
return
# Launch the Dialog so the user can select titles.
dlg = SelectionDialog(self.gui, self, books)
if dlg.exec_():
books_to_import = dlg.getBooks()
self.count = len(books_to_import)
debug_print("InterfacePluginAction::launchObok - number of books to decrypt: %d" % self.count)
# Feed the titles, the callback function (self.get_decrypted_kobo_books)
# and the Kobo library object to the ProgressDialog dispatcher.
d = DecryptAddProgressDialog(self.gui, books_to_import, self.get_decrypted_kobo_books, self.library, 'kobo',
status_msg_type='Kobo books', action_type=('Decrypting', 'Decryption'))
# Canceled the decryption process; clean up and exit.
if d.wasCanceled():
print (_('{} - Decryption canceled by user.').format(PLUGIN_NAME + ' v' + PLUGIN_VERSION))
self.library.close()
remove_dir(self.tdir)
return
else:
# Canceled the selection process; clean up and exit.
self.library.close()
remove_dir(self.tdir)
return
# Close Kobo Library object
self.library.close()
# If we have decrypted books to work with, feed the list of decrypted books details
# and the callback function (self.add_new_books) to the ProgressDialog dispatcher.
if len(self.books_to_add):
d = DecryptAddProgressDialog(self.gui, self.books_to_add, self.add_new_books, self.db, 'calibre',
status_msg_type='new calibre books', action_type=('Adding','Addition'))
# Canceled the "add new books to calibre" process;
# show the results of what got added before cancellation.
if d.wasCanceled():
print (_('{} - "Add books" canceled by user.').format(PLUGIN_NAME + ' v' + PLUGIN_VERSION))
self.add_books_cancelled = True
print (_('{} - wrapping up results.').format(PLUGIN_NAME + ' v' + PLUGIN_VERSION))
self.wrap_up_results()
remove_dir(self.tdir)
return
# If books couldn't be added because of duplicate entries in calibre, ask
# if we should try to add the decrypted epubs to existing calibre library entries.
if len(self.duplicate_book_list):
if cfg['finding_homes_for_formats'] == 'Always':
self.process_epub_formats()
elif cfg['finding_homes_for_formats'] == 'Never':
self.no_home_for_book.extend([entry[0] for entry in self.duplicate_book_list])
else:
if self.ask_about_inserting_epubs():
# Find homes for the epub decrypted formats in existing calibre library entries.
self.process_epub_formats()
else:
print (_('{} - User opted not to try to insert EPUB formats').format(PLUGIN_NAME + ' v' + PLUGIN_VERSION))
self.no_home_for_book.extend([entry[0] for entry in self.duplicate_book_list])
print (_('{} - wrapping up results.').format(PLUGIN_NAME + ' v' + PLUGIN_VERSION))
self.wrap_up_results()
remove_dir(self.tdir)
return
def show_help(self):
'''
Extract on demand the help file resource
'''
def get_help_file_resource():
# We will write the help file out every time, in case the user upgrades the plugin zip
# and there is a newer help file contained within it.
file_path = os.path.join(config_dir, 'plugins', HELPFILE_NAME)
file_data = self.load_resources(HELPFILE_NAME)[HELPFILE_NAME]
with open(file_path,'w') as f:
f.write(file_data)
return file_path
url = 'file:///' + get_help_file_resource()
open_url(QUrl(url))
def build_book_list(self):
'''
Connect to Kobo db and get titles.
'''
return self.library.books
def get_decrypted_kobo_books(self, book):
'''
This method is a call-back function used by DecryptAddProgressDialog in dialogs.py to decrypt Kobo books
:param book: A KoboBook object that is to be decrypted.
'''
print (_('{0} - Decrypting {1}').format(PLUGIN_NAME + ' v' + PLUGIN_VERSION, book.title))
decrypted = self.decryptBook(book)
if decrypted['success']:
# Build a list of calibre "book maps" for calibre's add_book function.
mi = get_metadata(decrypted['fileobj'], 'epub')
bookmap = {'EPUB':decrypted['fileobj'].name}
self.books_to_add.append((mi, bookmap))
else:
# Book is probably still encrypted.
print (_('{0} - Couldn\'t decrypt {1}').format(PLUGIN_NAME + ' v' + PLUGIN_VERSION, book.title))
self.decryption_errors.append((book.title, _('decryption errors')))
return False
return True
def add_new_books(self, books_to_add):
'''
This method is a call-back function used by DecryptAddProgressDialog in dialogs.py to add books to calibre
(It's set up to handle multiple books, but will only be fed books one at a time by DecryptAddProgressDialog)
:param books_to_add: List of calibre bookmaps (created in get_decrypted_kobo_books)
'''
added = self.db.add_books(books_to_add, add_duplicates=False, run_hooks=False)
if len(added[0]):
# Record the id(s) that got added
for id in added[0]:
print (_('{0} - Added {1}').format(PLUGIN_NAME + ' v' + PLUGIN_VERSION, books_to_add[0][0].title))
self.ids_of_new_books.append((id, books_to_add[0][0]))
if len(added[1]):
# Build a list of details about the books that didn't get added because duplicate were detected.
for mi, map in added[1]:
print (_('{0} - {1} already exists. Will try to add format later.').format(PLUGIN_NAME + ' v' + PLUGIN_VERSION, mi.title))
self.duplicate_book_list.append((mi, map['EPUB'], _('duplicate detected')))
return False
return True
def add_epub_format(self, book_id, mi, path):
'''
This method is a call-back function used by AddEpubFormatsProgressDialog in dialogs.py
:param book_id: calibre ID of the book to add the encrypted epub to.
:param mi: calibre metadata object
:param path: path to the decrypted epub (temp file)
'''
if self.db.add_format(book_id, 'EPUB', path, replace=False, run_hooks=False):
self.successful_format_adds.append((book_id, mi))
print (_('{0} - Successfully added EPUB format to existing {1}').format(PLUGIN_NAME + ' v' + PLUGIN_VERSION, mi.title))
return True
# we really shouldn't get here.
print (_('{0} - Error adding EPUB format to existing {1}. This really shouldn\'t happen.').format(PLUGIN_NAME + ' v' + PLUGIN_VERSION, mi.title))
self.no_home_for_book.append(mi)
return False
def process_epub_formats(self):
'''
Ask the user if they want to try to find homes for those books that already had an entry in calibre
'''
for book in self.duplicate_book_list:
mi, tmp_file = book[0], book[1]
dup_ids = self.db.find_identical_books(mi)
home_id = self.find_a_home(dup_ids)
if home_id is not None:
# Found an epub-free duplicate to add the epub to.
# build a list for the add_epub_format method to use.
self.formats_to_add.append((home_id, mi, tmp_file))
else:
self.no_home_for_book.append(mi)
# If we found homes for decrypted epubs in existing calibre entries, feed the list of decrypted book
# details and the callback function (self.add_epub_format) to the ProgressDialog dispatcher.
if self.formats_to_add:
d = AddEpubFormatsProgressDialog(self.gui, self.formats_to_add, self.add_epub_format)
if d.wasCanceled():
print (_('{} - "Insert formats" canceled by user.').format(PLUGIN_NAME + ' v' + PLUGIN_VERSION))
self.add_formats_cancelled = True
return
#return
return
def wrap_up_results(self):
'''
Present the results
'''
caption = PLUGIN_NAME + ' v' + PLUGIN_VERSION
# Refresh the gui and highlight new entries/modified entries.
if len(self.ids_of_new_books) or len(self.successful_format_adds):
self.refresh_gui_lib()
msg, log = self.build_report()
sd = ResultsSummaryDialog(self.gui, caption, msg, log)
sd.exec_()
return
def ask_about_inserting_epubs(self):
'''
Build question dialog with details about kobo books
that couldn't be added to calibre as new books.
'''
''' Terisa: Improve the message
'''
caption = PLUGIN_NAME + ' v' + PLUGIN_VERSION
plural = format_plural(len(self.ids_of_new_books))
det_msg = ''
if self.count > 1:
msg = _('<p><b>{0}</b> EPUB{2} successfully added to library.<br /><br /><b>{1}</b> ').format(len(self.ids_of_new_books), len(self.duplicate_book_list), plural)
msg += _('not added because books with the same title/author were detected.<br /><br />Would you like to try and add the EPUB format{0}').format(plural)
msg += _(' to those existing entries?<br /><br />NOTE: no pre-existing EPUBs will be overwritten.')
for entry in self.duplicate_book_list:
det_msg += _('{0} -- not added because of {1} in your library.\n\n').format(entry[0].title, entry[2])
else:
msg = _('<p><b>{0}</b> -- not added because of {1} in your library.<br /><br />').format(self.duplicate_book_list[0][0].title, self.duplicate_book_list[0][2])
msg += _('Would you like to try and add the EPUB format to an available calibre duplicate?<br /><br />')
msg += _('NOTE: no pre-existing EPUB will be overwritten.')
return question_dialog(self.gui, caption, msg, det_msg)
def find_a_home(self, ids):
'''
Find the ID of the first EPUB-Free duplicate available
:param ids: List of calibre IDs that might serve as a home.
'''
for id in ids:
# Find the first entry that matches the incoming book that doesn't have an EPUB format.
if not self.db.has_format(id, 'EPUB'):
return id
break
return None
def refresh_gui_lib(self):
'''
Update the GUI; highlight the books that were added/modified
'''
if self.current_idx.isValid():
self.gui.library_view.model().current_changed(self.current_idx, self.current_idx)
new_entries = [id for id, mi in self.ids_of_new_books]
if new_entries:
self.gui.library_view.model().db.data.books_added(new_entries)
self.gui.library_view.model().books_added(len(new_entries))
new_entries.extend([id for id, mi in self.successful_format_adds])
self.gui.db_images.reset()
self.gui.tags_view.recount()
self.gui.library_view.model().set_highlight_only(True)
self.gui.library_view.select_rows(new_entries)
return
def decryptBook(self, book):
'''
Decrypt Kobo book
:param book: obok file object
'''
result = {}
result['success'] = False
result['fileobj'] = None
zin = zipfile.ZipFile(book.filename, 'r')
#print ('Kobo library filename: {0}'.format(book.filename))
for userkey in self.userkeys:
print (_('Trying key: '), userkey.encode('hex_codec'))
check = True
try:
fileout = PersistentTemporaryFile('.epub', dir=self.tdir)
#print ('Temp file: {0}'.format(fileout.name))
# modify the output file to be compressed by default
zout = zipfile.ZipFile(fileout.name, "w", zipfile.ZIP_DEFLATED)
# ensure that the mimetype file is the first written to the epub container
# and is stored with no compression
members = zin.namelist();
try:
members.remove('mimetype')
except Exception:
pass
zout.writestr('mimetype', 'application/epub+zip', zipfile.ZIP_STORED)
# end of mimetype mod
for filename in members:
contents = zin.read(filename)
if filename in book.encryptedfiles:
file = book.encryptedfiles[filename]
contents = file.decrypt(userkey, contents)
# Parse failures mean the key is probably wrong.
if check:
check = not file.check(contents)
zout.writestr(filename, contents)
zout.close()
zin.close()
result['success'] = True
result['fileobj'] = fileout
print ('Success!')
return result
except ValueError:
print (_('Decryption failed, trying next key.'))
zout.close()
continue
except Exception:
print (_('Unknown Error decrypting, trying next key..'))
zout.close()
continue
result['fileobj'] = book.filename
zin.close()
return result
def build_report(self):
log = ''
processed = len(self.ids_of_new_books) + len(self.successful_format_adds)
if processed == self.count:
if self.count > 1:
msg = _('<p>All selected Kobo books added as new calibre books or inserted into existing calibre ebooks.<br /><br />No issues.')
else:
# Single book ... don't get fancy.
title = self.ids_of_new_books[0][1].title if self.ids_of_new_books else self.successful_format_adds[0][1].title
msg = _('<p>{0} successfully added.').format(title)
return (msg, log)
else:
if self.count != 1:
msg = _('<p>Not all selected Kobo books made it into calibre.<br /><br />View report for details.')
log += _('<p><b>Total attempted:</b> {}</p>\n').format(self.count)
log += _('<p><b>Decryption errors:</b> {}</p>\n').format(len(self.decryption_errors))
if self.decryption_errors:
log += '<ul>\n'
for title, reason in self.decryption_errors:
log += '<li>{}</li>\n'.format(title)
log += '</ul>\n'
log += _('<p><b>New Books created:</b> {}</p>\n').format(len(self.ids_of_new_books))
if self.ids_of_new_books:
log += '<ul>\n'
for id, mi in self.ids_of_new_books:
log += '<li>{}</li>\n'.format(mi.title)
log += '</ul>\n'
if self.add_books_cancelled:
log += _('<p><b>Duplicates that weren\'t added:</b> {}</p>\n').format(len(self.duplicate_book_list))
if self.duplicate_book_list:
log += '<ul>\n'
for book in self.duplicate_book_list:
log += '<li>{}</li>\n'.format(book[0].title)
log += '</ul>\n'
cancelled_count = self.count - (len(self.decryption_errors) + len(self.ids_of_new_books) + len(self.duplicate_book_list))
if cancelled_count > 0:
log += _('<p><b>Book imports cancelled by user:</b> {}</p>\n').format(cancelled_count)
return (msg, log)
log += _('<p><b>New EPUB formats inserted in existing calibre books:</b> {0}</p>\n').format(len(self.successful_format_adds))
if self.successful_format_adds:
log += '<ul>\n'
for id, mi in self.successful_format_adds:
log += '<li>{}</li>\n'.format(mi.title)
log += '</ul>\n'
log += _('<p><b>EPUB formats NOT inserted into existing calibre books:</b> {}<br />\n').format(len(self.no_home_for_book))
log += _('(Either because the user <i>chose</i> not to insert them, or because all duplicates already had an EPUB format)')
if self.no_home_for_book:
log += '<ul>\n'
for mi in self.no_home_for_book:
log += '<li>{}</li>\n'.format(mi.title)
log += '</ul>\n'
if self.add_formats_cancelled:
cancelled_count = self.count - (len(self.decryption_errors) + len(self.ids_of_new_books) + len(self.successful_format_adds) + len(self.no_home_for_book))
if cancelled_count > 0:
log += _('<p><b>Format imports cancelled by user:</b> {}</p>\n').format(cancelled_count)
return (msg, log)
else:
# Single book ... don't get fancy.
if self.ids_of_new_books:
title = self.ids_of_new_books[0][1].title
elif self.successful_format_adds:
title = self.successful_format_adds[0][1].title
elif self.no_home_for_book:
title = self.no_home_for_book[0].title
elif self.decryption_errors:
title = self.decryption_errors[0][0]
else:
title = _('Unknown Book Title')
if self.decryption_errors:
reason = _('it couldn\'t be decrypted.')
elif self.no_home_for_book:
reason = _('user CHOSE not to insert the new EPUB format, or all existing calibre entries HAD an EPUB format already.')
else:
reason = _('of unknown reasons. Gosh I\'m embarrassed!')
msg = _('<p>{0} not added because {1}').format(title, reason)
return (msg, log)

View File

@@ -0,0 +1,589 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2012, David Forrester <davidfor@internode.on.net>'
__docformat__ = 'restructuredtext en'
import os, time, re, sys
try:
from PyQt5.Qt import (Qt, QIcon, QPixmap, QLabel, QDialog, QHBoxLayout, QProgressBar,
QTableWidgetItem, QFont, QLineEdit, QComboBox,
QVBoxLayout, QDialogButtonBox, QStyledItemDelegate, QDateTime,
QRegExpValidator, QRegExp, QDate, QDateEdit)
except ImportError:
from PyQt4.Qt import (Qt, QIcon, QPixmap, QLabel, QDialog, QHBoxLayout, QProgressBar,
QTableWidgetItem, QFont, QLineEdit, QComboBox,
QVBoxLayout, QDialogButtonBox, QStyledItemDelegate, QDateTime,
QRegExpValidator, QRegExp, QDate, QDateEdit)
from calibre.constants import iswindows, filesystem_encoding, DEBUG
from calibre.gui2 import gprefs, error_dialog, UNDEFINED_QDATETIME, Application
from calibre.gui2.actions import menu_action_unique_name
from calibre.gui2.keyboard import ShortcutConfig
from calibre.utils.config import config_dir, tweaks
from calibre.utils.date import now, format_date, qt_to_dt, UNDEFINED_DATE, as_local_time
from calibre import prints
# Global definition of our plugin name. Used for common functions that require this.
plugin_name = None
# Global definition of our plugin resources. Used to share between the xxxAction and xxxBase
# classes if you need any zip images to be displayed on the configuration dialog.
plugin_icon_resources = {}
BASE_TIME = None
def debug_print(*args):
global BASE_TIME
if BASE_TIME is None:
BASE_TIME = time.time()
if DEBUG:
prints('DEBUG: %6.1f'%(time.time()-BASE_TIME), *args)
try:
debug_print("obok::common_utils.py - loading translations")
load_translations()
except NameError:
debug_print("obok::common_utils.py - exception when loading translations")
pass # load_translations() added in calibre 1.9
def set_plugin_icon_resources(name, resources):
'''
Set our global store of plugin name and icon resources for sharing between
the InterfaceAction class which reads them and the ConfigWidget
if needed for use on the customization dialog for this plugin.
'''
global plugin_icon_resources, plugin_name
plugin_name = name
plugin_icon_resources = resources
def get_icon(icon_name):
'''
Retrieve a QIcon for the named image from the zip file if it exists,
or if not then from Calibre's image cache.
'''
if icon_name:
pixmap = get_pixmap(icon_name)
if pixmap is None:
# Look in Calibre's cache for the icon
return QIcon(I(icon_name))
else:
return QIcon(pixmap)
return QIcon()
def get_pixmap(icon_name):
'''
Retrieve a QPixmap for the named image
Any icons belonging to the plugin must be prefixed with 'images/'
'''
global plugin_icon_resources, plugin_name
if not icon_name.startswith('images/'):
# We know this is definitely not an icon belonging to this plugin
pixmap = QPixmap()
pixmap.load(I(icon_name))
return pixmap
# Check to see whether the icon exists as a Calibre resource
# This will enable skinning if the user stores icons within a folder like:
# ...\AppData\Roaming\calibre\resources\images\Plugin Name\
if plugin_name:
local_images_dir = get_local_images_dir(plugin_name)
local_image_path = os.path.join(local_images_dir, icon_name.replace('images/', ''))
if os.path.exists(local_image_path):
pixmap = QPixmap()
pixmap.load(local_image_path)
return pixmap
# As we did not find an icon elsewhere, look within our zip resources
if icon_name in plugin_icon_resources:
pixmap = QPixmap()
pixmap.loadFromData(plugin_icon_resources[icon_name])
return pixmap
return None
def get_local_images_dir(subfolder=None):
'''
Returns a path to the user's local resources/images folder
If a subfolder name parameter is specified, appends this to the path
'''
images_dir = os.path.join(config_dir, 'resources/images')
if subfolder:
images_dir = os.path.join(images_dir, subfolder)
if iswindows:
images_dir = os.path.normpath(images_dir)
return images_dir
def create_menu_item(ia, parent_menu, menu_text, image=None, tooltip=None,
shortcut=(), triggered=None, is_checked=None):
'''
Create a menu action with the specified criteria and action
Note that if no shortcut is specified, will not appear in Preferences->Keyboard
This method should only be used for actions which either have no shortcuts,
or register their menus only once. Use create_menu_action_unique for all else.
'''
if shortcut is not None:
if len(shortcut) == 0:
shortcut = ()
else:
shortcut = _(shortcut)
ac = ia.create_action(spec=(menu_text, None, tooltip, shortcut),
attr=menu_text)
if image:
ac.setIcon(get_icon(image))
if triggered is not None:
ac.triggered.connect(triggered)
if is_checked is not None:
ac.setCheckable(True)
if is_checked:
ac.setChecked(True)
parent_menu.addAction(ac)
return ac
def create_menu_action_unique(ia, parent_menu, menu_text, image=None, tooltip=None,
shortcut=None, triggered=None, is_checked=None, shortcut_name=None,
unique_name=None):
'''
Create a menu action with the specified criteria and action, using the new
InterfaceAction.create_menu_action() function which ensures that regardless of
whether a shortcut is specified it will appear in Preferences->Keyboard
'''
orig_shortcut = shortcut
kb = ia.gui.keyboard
if unique_name is None:
unique_name = menu_text
if not shortcut == False:
full_unique_name = menu_action_unique_name(ia, unique_name)
if full_unique_name in kb.shortcuts:
shortcut = False
else:
if shortcut is not None and not shortcut == False:
if len(shortcut) == 0:
shortcut = None
else:
shortcut = _(shortcut)
if shortcut_name is None:
shortcut_name = menu_text.replace('&','')
ac = ia.create_menu_action(parent_menu, unique_name, menu_text, icon=None, shortcut=shortcut,
description=tooltip, triggered=triggered, shortcut_name=shortcut_name)
if shortcut == False and not orig_shortcut == False:
if ac.calibre_shortcut_unique_name in ia.gui.keyboard.shortcuts:
kb.replace_action(ac.calibre_shortcut_unique_name, ac)
if image:
ac.setIcon(get_icon(image))
if is_checked is not None:
ac.setCheckable(True)
if is_checked:
ac.setChecked(True)
return ac
def get_library_uuid(db):
try:
library_uuid = db.library_id
except:
library_uuid = ''
return library_uuid
class ImageLabel(QLabel):
def __init__(self, parent, icon_name, size=16):
QLabel.__init__(self, parent)
pixmap = get_pixmap(icon_name)
self.setPixmap(pixmap)
self.setMaximumSize(size, size)
self.setScaledContents(True)
class ImageTitleLayout(QHBoxLayout):
'''
A reusable layout widget displaying an image followed by a title
'''
def __init__(self, parent, icon_name, title):
QHBoxLayout.__init__(self)
self.title_image_label = QLabel(parent)
self.update_title_icon(icon_name)
self.addWidget(self.title_image_label)
title_font = QFont()
title_font.setPointSize(16)
shelf_label = QLabel(title, parent)
shelf_label.setFont(title_font)
self.addWidget(shelf_label)
self.insertStretch(-1)
# Add hyperlink to a help file at the right. We will replace the correct name when it is clicked.
help_label = QLabel(('<a href="http://www.foo.com/">{0}</a>').format(_("Help")), parent)
help_label.setTextInteractionFlags(Qt.LinksAccessibleByMouse | Qt.LinksAccessibleByKeyboard)
help_label.setAlignment(Qt.AlignRight)
help_label.linkActivated.connect(parent.help_link_activated)
self.addWidget(help_label)
def update_title_icon(self, icon_name):
pixmap = get_pixmap(icon_name)
if pixmap is None:
error_dialog(self.parent(), _("Restart required"),
_("Title image not found - you must restart Calibre before using this plugin!"), show=True)
else:
self.title_image_label.setPixmap(pixmap)
self.title_image_label.setMaximumSize(32, 32)
self.title_image_label.setScaledContents(True)
class SizePersistedDialog(QDialog):
'''
This dialog is a base class for any dialogs that want their size/position
restored when they are next opened.
'''
def __init__(self, parent, unique_pref_name):
QDialog.__init__(self, parent)
self.unique_pref_name = unique_pref_name
self.geom = gprefs.get(unique_pref_name, None)
self.finished.connect(self.dialog_closing)
self.help_anchor = ''
def resize_dialog(self):
if self.geom is None:
self.resize(self.sizeHint())
else:
self.restoreGeometry(self.geom)
def dialog_closing(self, result):
geom = bytearray(self.saveGeometry())
gprefs[self.unique_pref_name] = geom
def help_link_activated(self, url):
self.plugin_action.show_help(anchor=self.help_anchor)
class ReadOnlyTableWidgetItem(QTableWidgetItem):
def __init__(self, text):
if text is None:
text = ''
QTableWidgetItem.__init__(self, text, QTableWidgetItem.UserType)
self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)
class RatingTableWidgetItem(QTableWidgetItem):
def __init__(self, rating, is_read_only=False):
QTableWidgetItem.__init__(self, '', QTableWidgetItem.UserType)
self.setData(Qt.DisplayRole, rating)
if is_read_only:
self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)
class DateTableWidgetItem(QTableWidgetItem):
def __init__(self, date_read, is_read_only=False, default_to_today=False, fmt=None):
# debug_print("DateTableWidgetItem:__init__ - date_read=", date_read)
if date_read is None or date_read == UNDEFINED_DATE and default_to_today:
date_read = now()
if is_read_only:
QTableWidgetItem.__init__(self, format_date(date_read, fmt), QTableWidgetItem.UserType)
self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)
self.setData(Qt.DisplayRole, QDateTime(date_read))
else:
QTableWidgetItem.__init__(self, '', QTableWidgetItem.UserType)
self.setData(Qt.DisplayRole, QDateTime(date_read))
from calibre.gui2.library.delegates import DateDelegate as _DateDelegate
class DateDelegate(_DateDelegate):
'''
Delegate for dates. Because this delegate stores the
format as an instance variable, a new instance must be created for each
column. This differs from all the other delegates.
'''
def __init__(self, parent, fmt='dd MMM yyyy', default_to_today=True):
_DateDelegate.__init__(self, parent)
self.format = fmt
self.default_to_today = default_to_today
# def displayText(self, val, locale):
# d = val.toDateTime()
# if d <= UNDEFINED_QDATETIME:
# return ''
# return format_date(qt_to_dt(d, as_utc=False), self.format)
def createEditor(self, parent, option, index):
qde = QStyledItemDelegate.createEditor(self, parent, option, index)
qde.setDisplayFormat(self.format)
qde.setMinimumDateTime(UNDEFINED_QDATETIME)
qde.setSpecialValueText(_('Undefined'))
qde.setCalendarPopup(True)
return qde
def setEditorData(self, editor, index):
val = index.model().data(index, Qt.DisplayRole).toDateTime()
if val is None or val == UNDEFINED_QDATETIME:
if self.default_to_today:
val = self.default_date
else:
val = UNDEFINED_QDATETIME
editor.setDateTime(val)
def setModelData(self, editor, model, index):
val = editor.dateTime()
if val <= UNDEFINED_QDATETIME:
model.setData(index, UNDEFINED_QDATETIME, Qt.EditRole)
else:
model.setData(index, QDateTime(val), Qt.EditRole)
class NoWheelComboBox(QComboBox):
def wheelEvent (self, event):
# Disable the mouse wheel on top of the combo box changing selection as plays havoc in a grid
event.ignore()
class CheckableTableWidgetItem(QTableWidgetItem):
def __init__(self, checked=False, is_tristate=False):
QTableWidgetItem.__init__(self, '')
self.setFlags(Qt.ItemFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled ))
if is_tristate:
self.setFlags(self.flags() | Qt.ItemIsTristate)
if checked:
self.setCheckState(Qt.Checked)
else:
if is_tristate and checked is None:
self.setCheckState(Qt.PartiallyChecked)
else:
self.setCheckState(Qt.Unchecked)
def get_boolean_value(self):
'''
Return a boolean value indicating whether checkbox is checked
If this is a tristate checkbox, a partially checked value is returned as None
'''
if self.checkState() == Qt.PartiallyChecked:
return None
else:
return self.checkState() == Qt.Checked
class TextIconWidgetItem(QTableWidgetItem):
def __init__(self, text, icon):
QTableWidgetItem.__init__(self, text)
if icon:
self.setIcon(icon)
class ReadOnlyTextIconWidgetItem(ReadOnlyTableWidgetItem):
def __init__(self, text, icon):
ReadOnlyTableWidgetItem.__init__(self, text)
if icon:
self.setIcon(icon)
class ReadOnlyLineEdit(QLineEdit):
def __init__(self, text, parent):
if text is None:
text = ''
QLineEdit.__init__(self, text, parent)
self.setEnabled(False)
class NumericLineEdit(QLineEdit):
'''
Allows a numeric value up to two decimal places, or an integer
'''
def __init__(self, *args):
QLineEdit.__init__(self, *args)
self.setValidator(QRegExpValidator(QRegExp(r'(^\d*\.[\d]{1,2}$)|(^[1-9]\d*[\.]$)'), self))
class KeyValueComboBox(QComboBox):
def __init__(self, parent, values, selected_key):
QComboBox.__init__(self, parent)
self.values = values
self.populate_combo(selected_key)
def populate_combo(self, selected_key):
self.clear()
selected_idx = idx = -1
for key, value in self.values.iteritems():
idx = idx + 1
self.addItem(value)
if key == selected_key:
selected_idx = idx
self.setCurrentIndex(selected_idx)
def selected_key(self):
for key, value in self.values.iteritems():
if value == unicode(self.currentText()).strip():
return key
class KeyComboBox(QComboBox):
def __init__(self, parent, values, selected_key):
QComboBox.__init__(self, parent)
self.values = values
self.populate_combo(selected_key)
def populate_combo(self, selected_key):
self.clear()
selected_idx = idx = -1
for key in sorted(self.values.keys()):
idx = idx + 1
self.addItem(key)
if key == selected_key:
selected_idx = idx
self.setCurrentIndex(selected_idx)
def selected_key(self):
for key, value in self.values.iteritems():
if key == unicode(self.currentText()).strip():
return key
class CustomColumnComboBox(QComboBox):
def __init__(self, parent, custom_columns={}, selected_column='', initial_items=['']):
QComboBox.__init__(self, parent)
self.populate_combo(custom_columns, selected_column, initial_items)
def populate_combo(self, custom_columns, selected_column, initial_items=['']):
self.clear()
self.column_names = list(initial_items)
if len(initial_items) > 0:
self.addItems(initial_items)
selected_idx = 0
for idx, value in enumerate(initial_items):
if value == selected_column:
selected_idx = idx
for key in sorted(custom_columns.keys()):
self.column_names.append(key)
self.addItem('%s (%s)'%(key, custom_columns[key]['name']))
if key == selected_column:
selected_idx = len(self.column_names) - 1
self.setCurrentIndex(selected_idx)
def get_selected_column(self):
return self.column_names[self.currentIndex()]
class KeyboardConfigDialog(SizePersistedDialog):
'''
This dialog is used to allow editing of keyboard shortcuts.
'''
def __init__(self, gui, group_name):
SizePersistedDialog.__init__(self, gui, 'Keyboard shortcut dialog')
self.gui = gui
self.setWindowTitle('Keyboard shortcuts')
layout = QVBoxLayout(self)
self.setLayout(layout)
self.keyboard_widget = ShortcutConfig(self)
layout.addWidget(self.keyboard_widget)
self.group_name = group_name
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
button_box.accepted.connect(self.commit)
button_box.rejected.connect(self.reject)
layout.addWidget(button_box)
# Cause our dialog size to be restored from prefs or created on first usage
self.resize_dialog()
self.initialize()
def initialize(self):
self.keyboard_widget.initialize(self.gui.keyboard)
self.keyboard_widget.highlight_group(self.group_name)
def commit(self):
self.keyboard_widget.commit()
self.accept()
class ProgressBar(QDialog):
def __init__(self, parent=None, max_items=100, window_title='Progress Bar',
label='Label goes here', on_top=False):
if on_top:
QDialog.__init__(self, parent=parent, flags=Qt.WindowStaysOnTopHint)
else:
QDialog.__init__(self, parent=parent)
self.application = Application
self.setWindowTitle(window_title)
self.l = QVBoxLayout(self)
self.setLayout(self.l)
self.label = QLabel(label)
self.label.setAlignment(Qt.AlignHCenter)
self.l.addWidget(self.label)
self.progressBar = QProgressBar(self)
self.progressBar.setRange(0, max_items)
self.progressBar.setValue(0)
self.l.addWidget(self.progressBar)
def increment(self):
self.progressBar.setValue(self.progressBar.value() + 1)
self.refresh()
def refresh(self):
self.application.processEvents()
def set_label(self, value):
self.label.setText(value)
self.refresh()
def set_maximum(self, value):
self.progressBar.setMaximum(value)
self.refresh()
def set_value(self, value):
self.progressBar.setValue(value)
self.refresh()
def convert_kobo_date(kobo_date):
from calibre.utils.date import utc_tz
try:
converted_date = datetime.strptime(kobo_date, "%Y-%m-%dT%H:%M:%S.%f")
converted_date = datetime.strptime(kobo_date[0:19], "%Y-%m-%dT%H:%M:%S")
converted_date = converted_date.replace(tzinfo=utc_tz)
# debug_print("convert_kobo_date - '%Y-%m-%dT%H:%M:%S.%f' - kobo_date={0}'".format(kobo_date))
except:
try:
converted_date = datetime.strptime(kobo_date, "%Y-%m-%dT%H:%M:%S%+00:00")
# debug_print("convert_kobo_date - '%Y-%m-%dT%H:%M:%S+00:00' - kobo_date=%s' - kobo_date={0}'".format(kobo_date))
except:
try:
converted_date = datetime.strptime(kobo_date.split('+')[0], "%Y-%m-%dT%H:%M:%S")
converted_date = converted_date.replace(tzinfo=utc_tz)
# debug_print("convert_kobo_date - '%Y-%m-%dT%H:%M:%S' - kobo_date={0}'".format(kobo_date))
except:
try:
converted_date = datetime.strptime(kobo_date.split('+')[0], "%Y-%m-%d")
converted_date = converted_date.replace(tzinfo=utc_tz)
# debug_print("convert_kobo_date - '%Y-%m-%d' - kobo_date={0}'".format(kobo_date))
except:
try:
from calibre.utils.date import parse_date
converted_date = parse_date(kobo_date, assume_utc=True)
# debug_print("convert_kobo_date - parse_date - kobo_date=%s' - kobo_date={0}'".format(kobo_date))
except:
# try:
# converted_date = time.gmtime(os.path.getctime(self.path))
# debug_print("convert_kobo_date - time.gmtime(os.path.getctime(self.path)) - kobo_date={0}'".format(kobo_date))
# except:
converted_date = time.gmtime()
debug_print("convert_kobo_date - time.gmtime() - kobo_date={0}'".format(kobo_date))
return converted_date

View File

@@ -0,0 +1,223 @@
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import (unicode_literals, division, absolute_import,
print_function)
try:
from PyQt5.Qt import (Qt, QGroupBox, QListWidget, QLineEdit, QDialogButtonBox, QWidget, QLabel, QDialog, QVBoxLayout, QAbstractItemView, QIcon, QHBoxLayout, QComboBox, QListWidgetItem)
except ImportError:
from PyQt4.Qt import (Qt, QGroupBox, QListWidget, QLineEdit, QDialogButtonBox, QWidget, QLabel, QDialog, QVBoxLayout, QAbstractItemView, QIcon, QHBoxLayout, QComboBox, QListWidgetItem)
try:
from PyQt5 import Qt as QtGui
except ImportError:
from PyQt4 import QtGui
from calibre.gui2 import (error_dialog, question_dialog, info_dialog, open_url)
from calibre.utils.config import JSONConfig, config_dir
plugin_prefs = JSONConfig('plugins/obok_dedrm_prefs')
plugin_prefs.defaults['finding_homes_for_formats'] = 'Ask'
plugin_prefs.defaults['kobo_serials'] = []
from calibre_plugins.obok_dedrm.__init__ import PLUGIN_NAME, PLUGIN_VERSION
from calibre_plugins.obok_dedrm.utilities import (debug_print)
try:
debug_print("obok::config.py - loading translations")
load_translations()
except NameError:
debug_print("obok::config.py - exception when loading translations")
pass # load_translations() added in calibre 1.9
class ConfigWidget(QWidget):
def __init__(self, plugin_action):
QWidget.__init__(self)
self.plugin_action = plugin_action
layout = QVBoxLayout(self)
self.setLayout(layout)
# copy of preferences
self.tmpserials = plugin_prefs['kobo_serials']
combo_label = QLabel(_('When should Obok try to insert EPUBs into existing calibre entries?'), self)
layout.addWidget(combo_label)
self.find_homes = QComboBox()
self.find_homes.setToolTip(_('<p>Default behavior when duplicates are detected. None of the choices will cause calibre ebooks to be overwritten'))
layout.addWidget(self.find_homes)
self.find_homes.addItems([_('Ask'), _('Always'), _('Never')])
index = self.find_homes.findText(plugin_prefs['finding_homes_for_formats'])
self.find_homes.setCurrentIndex(index)
self.serials_button = QtGui.QPushButton(self)
self.serials_button.setToolTip(_(u"Click to manage Kobo serial numbers for Kobo ebooks"))
self.serials_button.setText(u"Kobo devices serials")
self.serials_button.clicked.connect(self.edit_serials)
layout.addWidget(self.serials_button)
def edit_serials(self):
d = ManageKeysDialog(self,u"Kobo device serial numbers",self.tmpserials, AddSerialDialog)
d.exec_()
def save_settings(self):
plugin_prefs['finding_homes_for_formats'] = unicode(self.find_homes.currentText())
plugin_prefs['kobo_serials'] = self.tmpserials
class ManageKeysDialog(QDialog):
def __init__(self, parent, key_type_name, plugin_keys, create_key, keyfile_ext = u""):
QDialog.__init__(self,parent)
self.parent = parent
self.key_type_name = key_type_name
self.plugin_keys = plugin_keys
self.create_key = create_key
self.keyfile_ext = keyfile_ext
self.json_file = (keyfile_ext == u"k4i")
self.setWindowTitle("{0} {1}: Manage {2}s".format(PLUGIN_NAME, PLUGIN_VERSION, self.key_type_name))
# Start Qt Gui dialog layout
layout = QVBoxLayout(self)
self.setLayout(layout)
keys_group_box = QGroupBox(_(u"{0}s".format(self.key_type_name)), self)
layout.addWidget(keys_group_box)
keys_group_box_layout = QHBoxLayout()
keys_group_box.setLayout(keys_group_box_layout)
self.listy = QListWidget(self)
self.listy.setToolTip(u"{0}s that will be used to decrypt ebooks".format(self.key_type_name))
self.listy.setSelectionMode(QAbstractItemView.SingleSelection)
self.populate_list()
keys_group_box_layout.addWidget(self.listy)
button_layout = QVBoxLayout()
keys_group_box_layout.addLayout(button_layout)
self._add_key_button = QtGui.QToolButton(self)
self._add_key_button.setIcon(QIcon(I('plus.png')))
self._add_key_button.setToolTip(u"Create new {0}".format(self.key_type_name))
self._add_key_button.clicked.connect(self.add_key)
button_layout.addWidget(self._add_key_button)
self._delete_key_button = QtGui.QToolButton(self)
self._delete_key_button.setToolTip(_(u"Delete highlighted key"))
self._delete_key_button.setIcon(QIcon(I('list_remove.png')))
self._delete_key_button.clicked.connect(self.delete_key)
button_layout.addWidget(self._delete_key_button)
spacerItem = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
button_layout.addItem(spacerItem)
layout.addSpacing(5)
migrate_layout = QHBoxLayout()
layout.addLayout(migrate_layout)
migrate_layout.addStretch()
self.button_box = QDialogButtonBox(QDialogButtonBox.Close)
self.button_box.rejected.connect(self.close)
migrate_layout.addWidget(self.button_box)
self.resize(self.sizeHint())
def populate_list(self):
if type(self.plugin_keys) == dict:
for key in self.plugin_keys.keys():
self.listy.addItem(QListWidgetItem(key))
else:
for key in self.plugin_keys:
self.listy.addItem(QListWidgetItem(key))
def add_key(self):
d = self.create_key(self)
d.exec_()
if d.result() != d.Accepted:
# New key generation cancelled.
return
new_key_value = d.key_value
if new_key_value in self.plugin_keys:
info_dialog(None, "{0} {1}: Duplicate {2}".format(PLUGIN_NAME, PLUGIN_VERSION,self.key_type_name),
u"This {0} is already in the list of {0}s has not been added.".format(self.key_type_name), show=True)
return
self.plugin_keys.append(d.key_value)
self.listy.clear()
self.populate_list()
def rename_key(self):
if not self.listy.currentItem():
errmsg = u"No {0} selected to rename. Highlight a keyfile first.".format(self.key_type_name)
r = error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION),
_(errmsg), show=True, show_copy_button=False)
return
d = RenameKeyDialog(self)
d.exec_()
if d.result() != d.Accepted:
# rename cancelled or moot.
return
keyname = unicode(self.listy.currentItem().text())
if not question_dialog(self, "{0} {1}: Confirm Rename".format(PLUGIN_NAME, PLUGIN_VERSION), u"Do you really want to rename the {2} named <strong>{0}</strong> to <strong>{1}</strong>?".format(keyname,d.key_name,self.key_type_name), show_copy_button=False, default_yes=False):
return
self.plugin_keys[d.key_name] = self.plugin_keys[keyname]
del self.plugin_keys[keyname]
self.listy.clear()
self.populate_list()
def delete_key(self):
if not self.listy.currentItem():
return
keyname = unicode(self.listy.currentItem().text())
if not question_dialog(self, "{0} {1}: Confirm Delete".format(PLUGIN_NAME, PLUGIN_VERSION), u"Do you really want to delete the {1} <strong>{0}</strong>?".format(keyname, self.key_type_name), show_copy_button=False, default_yes=False):
return
self.plugin_keys.remove(keyname)
self.listy.clear()
self.populate_list()
class AddSerialDialog(QDialog):
def __init__(self, parent=None,):
QDialog.__init__(self, parent)
self.parent = parent
self.setWindowTitle(u"{0} {1}: Add New eInk Kobo Serial Number".format(PLUGIN_NAME, PLUGIN_VERSION))
layout = QVBoxLayout(self)
self.setLayout(layout)
data_group_box = QGroupBox(u"", self)
layout.addWidget(data_group_box)
data_group_box_layout = QVBoxLayout()
data_group_box.setLayout(data_group_box_layout)
key_group = QHBoxLayout()
data_group_box_layout.addLayout(key_group)
key_group.addWidget(QLabel(u"EInk Kobo Serial Number:", self))
self.key_ledit = QLineEdit("", self)
self.key_ledit.setToolTip(u"Enter an eInk Kobo serial number. EInk Kobo serial numbers are 13 characters long and usually start with a 'N'. Kobo Serial Numbers are case-sensitive, so be sure to enter the upper and lower case letters unchanged.")
key_group.addWidget(self.key_ledit)
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.button_box.accepted.connect(self.accept)
self.button_box.rejected.connect(self.reject)
layout.addWidget(self.button_box)
self.resize(self.sizeHint())
@property
def key_name(self):
return unicode(self.key_ledit.text()).strip()
@property
def key_value(self):
return unicode(self.key_ledit.text()).strip()
def accept(self):
if len(self.key_name) == 0 or self.key_name.isspace():
errmsg = u"Please enter an eInk Kindle Serial Number or click Cancel in the dialog."
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
if len(self.key_name) != 13:
errmsg = u"EInk Kobo Serial Numbers must be 13 characters long. This is {0:d} characters long.".format(len(self.key_name))
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
QDialog.accept(self)

View File

@@ -0,0 +1,455 @@
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__docformat__ = 'restructuredtext en'
TEXT_DRM_FREE = ' (*: drm - free)'
LAB_DRM_FREE = '* : drm - free'
try:
from PyQt5.Qt import (Qt, QVBoxLayout, QLabel, QApplication, QGroupBox,
QDialogButtonBox, QHBoxLayout, QTextBrowser, QProgressDialog,
QTimer, QSize, QDialog, QIcon, QTableWidget, QTableWidgetItem)
except ImportError:
from PyQt4.Qt import (Qt, QVBoxLayout, QLabel, QApplication, QGroupBox,
QDialogButtonBox, QHBoxLayout, QTextBrowser, QProgressDialog,
QTimer, QSize, QDialog, QIcon, QTableWidget, QTableWidgetItem)
try:
from PyQt5.QtWidgets import (QListWidget, QAbstractItemView)
except ImportError:
from PyQt4.QtGui import (QListWidget, QAbstractItemView)
from calibre.gui2 import gprefs, warning_dialog, error_dialog
from calibre.gui2.dialogs.message_box import MessageBox
#from calibre.ptempfile import remove_dir
from calibre_plugins.obok_dedrm.utilities import (SizePersistedDialog, ImageTitleLayout,
showErrorDlg, get_icon, convert_qvariant, debug_print
)
from calibre_plugins.obok_dedrm.__init__ import (PLUGIN_NAME,
PLUGIN_SAFE_NAME, PLUGIN_VERSION, PLUGIN_DESCRIPTION)
try:
debug_print("obok::dialogs.py - loading translations")
load_translations()
except NameError:
debug_print("obok::dialogs.py - exception when loading translations")
pass # load_translations() added in calibre 1.9
class SelectionDialog(SizePersistedDialog):
'''
Dialog to select the kobo books to decrypt
'''
def __init__(self, gui, interface_action, books):
'''
:param gui: Parent gui
:param interface_action: InterfaceActionObject (InterfacePluginAction class from action.py)
:param books: list of Kobo book
'''
self.books = books
self.gui = gui
self.interface_action = interface_action
self.books = books
SizePersistedDialog.__init__(self, gui, PLUGIN_NAME + 'plugin:selections dialog')
self.setWindowTitle(_(PLUGIN_NAME + ' v' + PLUGIN_VERSION))
self.setMinimumWidth(300)
self.setMinimumHeight(300)
layout = QVBoxLayout(self)
self.setLayout(layout)
title_layout = ImageTitleLayout(self, 'images/obok.png', _('Obok DeDRM'))
layout.addLayout(title_layout)
help_label = QLabel(_('<a href="http://www.foo.com/">Help</a>'), self)
help_label.setTextInteractionFlags(Qt.LinksAccessibleByMouse | Qt.LinksAccessibleByKeyboard)
help_label.setAlignment(Qt.AlignRight)
help_label.linkActivated.connect(self._help_link_activated)
title_layout.addWidget(help_label)
title_layout.setAlignment(Qt.AlignTop)
layout.addSpacing(5)
main_layout = QHBoxLayout()
layout.addLayout(main_layout)
# self.listy = QListWidget()
# self.listy.setSelectionMode(QAbstractItemView.ExtendedSelection)
# main_layout.addWidget(self.listy)
# self.listy.addItems(books)
self.books_table = BookListTableWidget(self)
main_layout.addWidget(self.books_table)
layout.addSpacing(10)
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
button_box.accepted.connect(self._ok_clicked)
button_box.rejected.connect(self.reject)
self.select_all_button = button_box.addButton(_("Select All"), QDialogButtonBox.ResetRole)
self.select_all_button.setToolTip(_("Select all books to add them to the calibre library."))
self.select_all_button.clicked.connect(self._select_all_clicked)
self.select_drm_button = button_box.addButton(_("All with DRM"), QDialogButtonBox.ResetRole)
self.select_drm_button.setToolTip(_("Select all books with DRM."))
self.select_drm_button.clicked.connect(self._select_drm_clicked)
self.select_free_button = button_box.addButton(_("All DRM free"), QDialogButtonBox.ResetRole)
self.select_free_button.setToolTip(_("Select all books without DRM."))
self.select_free_button.clicked.connect(self._select_free_clicked)
layout.addWidget(button_box)
# Cause our dialog size to be restored from prefs or created on first usage
self.resize_dialog()
self.books_table.populate_table(self.books)
def _select_all_clicked(self):
self.books_table.select_all()
def _select_drm_clicked(self):
self.books_table.select_drm(True)
def _select_free_clicked(self):
self.books_table.select_drm(False)
def _help_link_activated(self, url):
'''
:param url: Dummy url to pass to the show_help method of the InterfacePluginAction class
'''
self.interface_action.show_help()
def _ok_clicked(self):
'''
Build an index of the selected titles
'''
if len(self.books_table.selectedItems()):
self.accept()
else:
msg = 'You must make a selection!'
showErrorDlg(msg, self)
def getBooks(self):
'''
Method to return the selected books
'''
return self.books_table.get_books()
class BookListTableWidget(QTableWidget):
def __init__(self, parent):
QTableWidget.__init__(self, parent)
self.setSelectionBehavior(QAbstractItemView.SelectRows)
def populate_table(self, books):
self.clear()
self.setAlternatingRowColors(True)
self.setRowCount(len(books))
header_labels = ['DRM', _('Title'), _('Author'), _('Series'), 'book_id']
self.setColumnCount(len(header_labels))
self.setHorizontalHeaderLabels(header_labels)
self.verticalHeader().setDefaultSectionSize(24)
self.horizontalHeader().setStretchLastSection(True)
self.books = {}
for row, book in enumerate(books):
self.populate_table_row(row, book)
self.books[row] = book
self.setSortingEnabled(False)
self.resizeColumnsToContents()
self.setMinimumColumnWidth(1, 100)
self.setMinimumColumnWidth(2, 100)
self.setMinimumSize(300, 0)
if len(books) > 0:
self.selectRow(0)
self.hideColumn(4)
self.setSortingEnabled(True)
def setMinimumColumnWidth(self, col, minimum):
if self.columnWidth(col) < minimum:
self.setColumnWidth(col, minimum)
def populate_table_row(self, row, book):
if book.has_drm:
icon = get_icon('drm-locked.png')
val = 1
else:
icon = get_icon('drm-unlocked.png')
val = 0
status_cell = IconWidgetItem(None, icon, val)
status_cell.setData(Qt.UserRole, val)
self.setItem(row, 0, status_cell)
self.setItem(row, 1, ReadOnlyTableWidgetItem(book.title))
self.setItem(row, 2, AuthorTableWidgetItem(book.author, book.author))
self.setItem(row, 3, SeriesTableWidgetItem(book.series, book.series_index))
self.setItem(row, 4, NumericTableWidgetItem(row))
def get_books(self):
# debug_print("BookListTableWidget:get_books - self.books:", self.books)
books = []
if len(self.selectedItems()):
for row in range(self.rowCount()):
# debug_print("BookListTableWidget:get_books - row:", row)
if self.item(row, 0).isSelected():
book_num = convert_qvariant(self.item(row, 4).data(Qt.DisplayRole))
debug_print("BookListTableWidget:get_books - book_num:", book_num)
book = self.books[book_num]
debug_print("BookListTableWidget:get_books - book:", book.title)
books.append(book)
return books
def select_all(self):
self .selectAll()
def select_drm(self, has_drm):
self.clearSelection()
current_selection_mode = self.selectionMode()
self.setSelectionMode(QAbstractItemView.MultiSelection)
for row in range(self.rowCount()):
# debug_print("BookListTableWidget:select_drm - row:", row)
if convert_qvariant(self.item(row, 0).data(Qt.UserRole)) == 1:
# debug_print("BookListTableWidget:select_drm - has DRM:", row)
if has_drm:
self.selectRow(row)
else:
# debug_print("BookListTableWidget:select_drm - DRM free:", row)
if not has_drm:
self.selectRow(row)
self.setSelectionMode(current_selection_mode)
class DecryptAddProgressDialog(QProgressDialog):
'''
Use the QTimer singleShot method to dole out books one at
a time to the indicated callback function from action.py
'''
def __init__(self, gui, indices, callback_fn, db, db_type='calibre', status_msg_type='books', action_type=('Decrypting','Decryption')):
'''
:param gui: Parent gui
:param indices: List of Kobo books or list calibre book maps (indicated by param db_type)
:param callback_fn: the function from action.py that will do the heavy lifting (get_decrypted_kobo_books or add_new_books)
:param db: kobo database object or calibre database cache (indicated by param db_type)
:param db_type: string indicating what kind of database param db is
:param status_msg_type: string to indicate what the ProgressDialog is operating on (cosmetic only)
:param action_type: 2-Tuple of strings indicating what the ProgressDialog is doing to param status_msg_type (cosmetic only)
'''
self.total_count = len(indices)
QProgressDialog.__init__(self, '', 'Cancel', 0, self.total_count, gui)
self.setMinimumWidth(500)
self.indices, self.callback_fn, self.db, self.db_type = indices, callback_fn, db, db_type
self.action_type, self.status_msg_type = action_type, status_msg_type
self.gui = gui
self.setWindowTitle('{0} {1} {2}...'.format(self.action_type[0], self.total_count, self.status_msg_type))
self.i, self.successes, self.failures = 0, [], []
QTimer.singleShot(0, self.do_book_action)
self.exec_()
def do_book_action(self):
if self.wasCanceled():
return self.do_close()
if self.i >= self.total_count:
return self.do_close()
book = self.indices[self.i]
self.i += 1
# Get the title and build the caption and label text from the string parameters provided
if self.db_type == 'calibre':
dtitle = book[0].title
elif self.db_type == 'kobo':
dtitle = book.title
self.setWindowTitle('{0} {1} {2} ({3} {4} failures)...'.format(self.action_type[0], self.total_count,
self.status_msg_type, len(self.failures), self.action_type[1]))
self.setLabelText('{0}: {1}'.format(self.action_type[0], dtitle))
# If a calibre db, feed the calibre bookmap to action.py's add_new_books method
if self.db_type == 'calibre':
if self.callback_fn([book]):
self.successes.append(book)
else:
self.failures.append(book)
# If a kobo db, feed the index to the kobo book to action.py's get_decrypted_kobo_books method
elif self.db_type == 'kobo':
if self.callback_fn(book):
debug_print("DecryptAddProgressDialog::do_book_action - decrypted book: '%s'" % dtitle)
self.successes.append(book)
else:
debug_print("DecryptAddProgressDialog::do_book_action - book decryption failed: '%s'" % dtitle)
self.failures.append(book)
self.setValue(self.i)
# Lather, rinse, repeat.
QTimer.singleShot(0, self.do_book_action)
def do_close(self):
self.hide()
self.gui = None
class AddEpubFormatsProgressDialog(QProgressDialog):
'''
Use the QTimer singleShot method to dole out epub formats one at
a time to the indicated callback function from action.py
'''
def __init__(self, gui, entries, callback_fn, status_msg_type='formats', action_type=('Adding','Added')):
'''
:param gui: Parent gui
:param entries: List of 3-tuples [(target calibre id, calibre metadata object, path to epub file)]
:param callback_fn: the function from action.py that will do the heavy lifting (process_epub_formats)
:param status_msg_type: string to indicate what the ProgressDialog is operating on (cosmetic only)
:param action_type: 2-tuple of strings indicating what the ProgressDialog is doing to param status_msg_type (cosmetic only)
'''
self.total_count = len(entries)
QProgressDialog.__init__(self, '', 'Cancel', 0, self.total_count, gui)
self.setMinimumWidth(500)
self.entries, self.callback_fn = entries, callback_fn
self.action_type, self.status_msg_type = action_type, status_msg_type
self.gui = gui
self.setWindowTitle('{0} {1} {2}...'.format(self.action_type[0], self.total_count, self.status_msg_type))
self.i, self.successes, self.failures = 0, [], []
QTimer.singleShot(0, self.do_book_action)
self.exec_()
def do_book_action(self):
if self.wasCanceled():
return self.do_close()
if self.i >= self.total_count:
return self.do_close()
epub_format = self.entries[self.i]
self.i += 1
# assign the elements of the 3-tuple details to legible variables
book_id, mi, path = epub_format[0], epub_format[1], epub_format[2]
# Get the title and build the caption and label text from the string parameters provided
dtitle = mi.title
self.setWindowTitle('{0} {1} {2} ({3} {4} failures)...'.format(self.action_type[0], self.total_count,
self.status_msg_type, len(self.failures), self.action_type[1]))
self.setLabelText('{0}: {1}'.format(self.action_type[0], dtitle))
# Send the necessary elements to the process_epub_formats callback function (action.py)
# and record the results
if self.callback_fn(book_id, mi, path):
self.successes.append((book_id, mi, path))
else:
self.failures.append((book_id, mi, path))
self.setValue(self.i)
# Lather, rinse, repeat
QTimer.singleShot(0, self.do_book_action)
def do_close(self):
self.hide()
self.gui = None
class ViewLog(QDialog):
'''
Show a detailed summary of results as html.
'''
def __init__(self, title, html, parent=None):
'''
:param title: Caption for window title
:param html: HTML string log/report
'''
QDialog.__init__(self, parent)
self.l = l = QVBoxLayout()
self.setLayout(l)
self.tb = QTextBrowser(self)
QApplication.setOverrideCursor(Qt.WaitCursor)
# Rather than formatting the text in <pre> blocks like the calibre
# ViewLog does, instead just format it inside divs to keep style formatting
html = html.replace('\t','&nbsp;&nbsp;&nbsp;&nbsp;')#.replace('\n', '<br/>')
html = html.replace('> ','>&nbsp;')
self.tb.setHtml('<div>{0}</div>'.format(html))
QApplication.restoreOverrideCursor()
l.addWidget(self.tb)
self.bb = QDialogButtonBox(QDialogButtonBox.Ok)
self.bb.accepted.connect(self.accept)
self.bb.rejected.connect(self.reject)
self.copy_button = self.bb.addButton(_('Copy to clipboard'),
self.bb.ActionRole)
self.copy_button.setIcon(QIcon(I('edit-copy.png')))
self.copy_button.clicked.connect(self.copy_to_clipboard)
l.addWidget(self.bb)
self.setModal(False)
self.resize(QSize(700, 500))
self.setWindowTitle(title)
self.setWindowIcon(QIcon(I('dialog_information.png')))
self.show()
def copy_to_clipboard(self):
txt = self.tb.toPlainText()
QApplication.clipboard().setText(txt)
class ResultsSummaryDialog(MessageBox):
def __init__(self, parent, title, msg, log='', det_msg=''):
'''
:param log: An HTML log
:param title: The title for this popup
:param msg: The msg to display
:param det_msg: Detailed message
'''
MessageBox.__init__(self, MessageBox.INFO, title, msg,
det_msg=det_msg, show_copy_button=False,
parent=parent)
self.log = log
self.vlb = self.bb.addButton(_('View Report'), self.bb.ActionRole)
self.vlb.setIcon(QIcon(I('dialog_information.png')))
self.vlb.clicked.connect(self.show_log)
self.det_msg_toggle.setVisible(bool(det_msg))
self.vlb.setVisible(bool(log))
def show_log(self):
self.log_viewer = ViewLog(PLUGIN_NAME + ' v' + PLUGIN_VERSION, self.log,
parent=self)
class ReadOnlyTableWidgetItem(QTableWidgetItem):
def __init__(self, text):
if text is None:
text = ''
QTableWidgetItem.__init__(self, text, QTableWidgetItem.UserType)
self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)
class AuthorTableWidgetItem(ReadOnlyTableWidgetItem):
def __init__(self, text, sort_key):
ReadOnlyTableWidgetItem.__init__(self, text)
self.sort_key = sort_key
#Qt uses a simple < check for sorting items, override this to use the sortKey
def __lt__(self, other):
return self.sort_key < other.sort_key
class SeriesTableWidgetItem(ReadOnlyTableWidgetItem):
def __init__(self, series, series_index=None):
display = ''
if series:
if series_index:
from calibre.ebooks.metadata import fmt_sidx
display = '%s [%s]' % (series, fmt_sidx(series_index))
self.sortKey = '%s%04d' % (series, series_index)
else:
display = series
self.sortKey = series
ReadOnlyTableWidgetItem.__init__(self, display)
class IconWidgetItem(ReadOnlyTableWidgetItem):
def __init__(self, text, icon, sort_key):
ReadOnlyTableWidgetItem.__init__(self, text)
if icon:
self.setIcon(icon)
self.sort_key = sort_key
#Qt uses a simple < check for sorting items, override this to use the sortKey
def __lt__(self, other):
return self.sort_key < other.sort_key
class NumericTableWidgetItem(QTableWidgetItem):
def __init__(self, number, is_read_only=False):
QTableWidgetItem.__init__(self, '', QTableWidgetItem.UserType)
self.setData(Qt.DisplayRole, number)
if is_read_only:
self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -0,0 +1,4 @@
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
__license__ = 'GPL v3'
__docformat__ = 'restructuredtext en'

View File

@@ -0,0 +1,71 @@
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
__license__ = 'GPL v3'
__docformat__ = 'restructuredtext en'
import os, sys
import binascii, hashlib, re, string
class legacy_obok(object):
def __init__(self):
self._userkey = ''
@property
def get_legacy_cookie_id(self):
if self._userkey != '':
return self._userkey
self._userkey = self.__oldcookiedeviceid()
return self._userkey
def __bytearraytostring(self, bytearr):
wincheck = re.match('@ByteArray\\((.+)\\)', bytearr)
if wincheck:
return wincheck.group(1)
return bytearr
def plist_to_dictionary(self, filename):
from subprocess import Popen, PIPE
from plistlib import readPlistFromString
'Pipe the binary plist through plutil and parse the xml output'
with open(filename, 'rb') as f:
content = f.read()
args = ['plutil', '-convert', 'xml1', '-o', '-', '--', '-']
p = Popen(args, stdin=PIPE, stdout=PIPE)
p.stdin.write(content)
out, err = p.communicate()
return readPlistFromString(out)
def __oldcookiedeviceid(self):
'''Optionally attempt to get a device id using the old cookie method.
Must have _winreg installed on Windows machines for successful key retrieval.'''
wsuid = ''
pwsdid = ''
try:
if sys.platform.startswith('win'):
import _winreg
regkey_browser = _winreg.OpenKey(_winreg.HKEY_CURRENT_USER, 'Software\\Kobo\\Kobo Desktop Edition\\Browser')
cookies = _winreg.QueryValueEx(regkey_browser, 'cookies')
bytearrays = cookies[0]
elif sys.platform.startswith('darwin'):
prefs = os.path.join(os.environ['HOME'], 'Library/Preferences/com.kobo.Kobo Desktop Edition.plist')
cookies = self.plist_to_dictionary(prefs)
bytearrays = cookies['Browser.cookies']
for bytearr in bytearrays:
cookie = self.__bytearraytostring(bytearr)
wsuidcheck = re.match("^wsuid=([0-9a-f-]+)", cookie)
if(wsuidcheck):
wsuid = wsuidcheck.group(1)
pwsdidcheck = re.match('^pwsdid=([0-9a-f-]+)', cookie)
if (pwsdidcheck):
pwsdid = pwsdidcheck.group(1)
if (wsuid == '' or pwsdid == ''):
return None
preuserkey = string.join((pwsdid, wsuid), '')
userkey = hashlib.sha256(preuserkey).hexdigest()
return binascii.a2b_hex(userkey[32:])
except KeyError:
print ('No "cookies" key found in Kobo plist: no legacy user key found.')
return None
except:
print ('Error parsing Kobo plist: no legacy user key found.')
return None

View File

@@ -0,0 +1,747 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Version 3.2.5 December 2016
# Improve detection of good text decryption.
#
# Version 3.2.4 December 2016
# Remove incorrect support for Kobo Desktop under Wine
#
# Version 3.2.3 October 2016
# Fix for windows network user and more xml fixes
#
# Version 3.2.2 October 2016
# Change to the way the new database version is handled.
#
# Version 3.2.1 September 2016
# Update for v4.0 of Windows Desktop app.
#
# Version 3.2.0 January 2016
# Update for latest version of Windows Desktop app.
# Support Kobo devices in the command line version.
#
# Version 3.1.9 November 2015
# Handle Kobo Desktop under wine on Linux
#
# Version 3.1.8 November 2015
# Handle the case of Kobo Arc or Vox device (i.e. don't crash).
#
# Version 3.1.7 October 2015
# Handle the case of no device or database more gracefully.
#
# Version 3.1.6 September 2015
# Enable support for Kobo devices
# More character encoding fixes (unicode strings)
#
# Version 3.1.5 September 2015
# Removed requirement that a purchase has been made.
# Also add in character encoding fixes
#
# Version 3.1.4 September 2015
# Updated for version 3.17 of the Windows Desktop app.
#
# Version 3.1.3 August 2015
# Add translations for Portuguese and Arabic
#
# Version 3.1.2 January 2015
# Add coding, version number and version announcement
#
# Version 3.05 October 2014
# Identifies DRM-free books in the dialog
#
# Version 3.04 September 2014
# Handles DRM-free books as well (sometimes Kobo Library doesn't
# show download link for DRM-free books)
#
# Version 3.03 August 2014
# If PyCrypto is unavailable try to use libcrypto for AES_ECB.
#
# Version 3.02 August 2014
# Relax checking of application/xhtml+xml and image/jpeg content.
#
# Version 3.01 June 2014
# Check image/jpeg as well as application/xhtml+xml content. Fix typo
# in Windows ipconfig parsing.
#
# Version 3.0 June 2014
# Made portable for Mac and Windows, and the only module dependency
# not part of python core is PyCrypto. Major code cleanup/rewrite.
# No longer tries the first MAC address; tries them all if it detects
# the decryption failed.
#
# Updated September 2013 by Anon
# Version 2.02
# Incorporated minor fixes posted at Apprentice Alf's.
#
# Updates July 2012 by Michael Newton
# PWSD ID is no longer a MAC address, but should always
# be stored in the registry. Script now works with OS X
# and checks plist for values instead of registry. Must
# have biplist installed for OS X support.
#
# Original comments left below; note the "AUTOPSY" is inaccurate. See
# KoboLibrary.userkeys and KoboFile.decrypt()
#
##########################################################
# KOBO DRM CRACK BY #
# PHYSISTICATED #
##########################################################
# This app was made for Python 2.7 on Windows 32-bit
#
# This app needs pycrypto - get from here:
# http://www.voidspace.org.uk/python/modules.shtml
#
# Usage: obok.py
# Choose the book you want to decrypt
#
# Shouts to my krew - you know who you are - and one in
# particular who gave me a lot of help with this - thank
# you so much!
#
# Kopimi /K\
# Keep sharing, keep copying, but remember that nothing is
# for free - make sure you compensate your favorite
# authors - and cut out the middle man whenever possible
# ;) ;) ;)
#
# DRM AUTOPSY
# The Kobo DRM was incredibly easy to crack, but it took
# me months to get around to making this. Here's the
# basics of how it works:
# 1: Get MAC address of first NIC in ipconfig (sometimes
# stored in registry as pwsdid)
# 2: Get user ID (stored in tons of places, this gets it
# from HKEY_CURRENT_USER\Software\Kobo\Kobo Desktop
# Edition\Browser\cookies)
# 3: Concatenate and SHA256, take the second half - this
# is your master key
# 4: Open %LOCALAPPDATA%\Kobo Desktop Editions\Kobo.sqlite
# and dump content_keys
# 5: Unbase64 the keys, then decode these with the master
# key - these are your page keys
# 6: Unzip EPUB of your choice, decrypt each page with its
# page key, then zip back up again
#
# WHY USE THIS WHEN INEPT WORKS FINE? (adobe DRM stripper)
# Inept works very well, but authors on Kobo can choose
# what DRM they want to use - and some have chosen not to
# let people download them with Adobe Digital Editions -
# they would rather lock you into a single platform.
#
# With Obok, you can sync Kobo Desktop, decrypt all your
# ebooks, and then use them on whatever device you want
# - you bought them, you own them, you can do what you
# like with them.
#
# Obok is Kobo backwards, but it is also means "next to"
# in Polish.
# When you buy a real book, it is right next to you. You
# can read it at home, at work, on a train, you can lend
# it to a friend, you can scribble on it, and add your own
# explanations/translations.
#
# Obok gives you this power over your ebooks - no longer
# are you restricted to one device. This allows you to
# embed foreign fonts into your books, as older Kobo's
# can't display them properly. You can read your books
# on your phones, in different PC readers, and different
# ereader devices. You can share them with your friends
# too, if you like - you can do that with a real book
# after all.
#
"""Manage all Kobo books, either encrypted or DRM-free."""
__version__ = '3.2.4'
__about__ = u"Obok v{0}\nCopyright © 2012-2016 Physisticated et al.".format(__version__)
import sys
import os
import subprocess
import sqlite3
import base64
import binascii
import re
import zipfile
import hashlib
import xml.etree.ElementTree as ET
import string
import shutil
import argparse
import tempfile
can_parse_xml = True
try:
from xml.etree import ElementTree as ET
# print u"using xml.etree for xml parsing"
except ImportError:
can_parse_xml = False
# print u"Cannot find xml.etree, disabling extraction of serial numbers"
# List of all known hash keys
KOBO_HASH_KEYS = ['88b3a2e13', 'XzUhGYdFp', 'NoCanLook','QJhwzAtXL']
class ENCRYPTIONError(Exception):
pass
def _load_crypto_libcrypto():
from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_int, c_long, \
Structure, c_ulong, create_string_buffer, cast
from ctypes.util import find_library
if sys.platform.startswith('win'):
libcrypto = find_library('libeay32')
else:
libcrypto = find_library('crypto')
if libcrypto is None:
raise ENCRYPTIONError('libcrypto not found')
libcrypto = CDLL(libcrypto)
AES_MAXNR = 14
c_char_pp = POINTER(c_char_p)
c_int_p = POINTER(c_int)
class AES_KEY(Structure):
_fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))),
('rounds', c_int)]
AES_KEY_p = POINTER(AES_KEY)
def F(restype, name, argtypes):
func = getattr(libcrypto, name)
func.restype = restype
func.argtypes = argtypes
return func
AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key',
[c_char_p, c_int, AES_KEY_p])
AES_ecb_encrypt = F(None, 'AES_ecb_encrypt',
[c_char_p, c_char_p, AES_KEY_p, c_int])
class AES(object):
def __init__(self, userkey):
self._blocksize = len(userkey)
if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) :
raise ENCRYPTIONError(_('AES improper key used'))
return
key = self._key = AES_KEY()
rv = AES_set_decrypt_key(userkey, len(userkey) * 8, key)
if rv < 0:
raise ENCRYPTIONError(_('Failed to initialize AES key'))
def decrypt(self, data):
clear = ''
for i in range(0, len(data), 16):
out = create_string_buffer(16)
rv = AES_ecb_encrypt(data[i:i+16], out, self._key, 0)
if rv == 0:
raise ENCRYPTIONError(_('AES decryption failed'))
clear += out.raw
return clear
return AES
def _load_crypto_pycrypto():
from Crypto.Cipher import AES as _AES
class AES(object):
def __init__(self, key):
self._aes = _AES.new(key, _AES.MODE_ECB)
def decrypt(self, data):
return self._aes.decrypt(data)
return AES
def _load_crypto():
AES = None
cryptolist = (_load_crypto_pycrypto, _load_crypto_libcrypto)
for loader in cryptolist:
try:
AES = loader()
break
except (ImportError, ENCRYPTIONError):
pass
return AES
AES = _load_crypto()
# Wrap a stream so that output gets flushed immediately
# and also make sure that any unicode strings get
# encoded using "replace" before writing them.
class SafeUnbuffered:
def __init__(self, stream):
self.stream = stream
self.encoding = stream.encoding
if self.encoding == None:
self.encoding = "utf-8"
def write(self, data):
if isinstance(data,unicode):
data = data.encode(self.encoding,"replace")
self.stream.write(data)
self.stream.flush()
def __getattr__(self, attr):
return getattr(self.stream, attr)
class KoboLibrary(object):
"""The Kobo library.
This class represents all the information available from the data
written by the Kobo Desktop Edition application, including the list
of books, their titles, and the user's encryption key(s)."""
def __init__ (self, serials = [], device_path = None):
print __about__
self.kobodir = u""
kobodb = u""
# Order of checks
# 1. first check if a device_path has been passed in, and whether
# we can find the sqlite db in the respective place
# 2. if 1., and we got some serials passed in (from saved
# settings in calibre), just use it
# 3. if 1. worked, but we didn't get serials, try to parse them
# from the device, if this didn't work, unset everything
# 4. if by now we don't have kobodir set, give up on device and
# try to use the Desktop app.
# step 1. check whether this looks like a real device
if (device_path):
# we got a device path
self.kobodir = os.path.join(device_path, u".kobo")
# devices use KoboReader.sqlite
kobodb = os.path.join(self.kobodir, u"KoboReader.sqlite")
if (not(os.path.isfile(kobodb))):
# device path seems to be wrong, unset it
device_path = u""
self.kobodir = u""
kobodb = u""
if (self.kobodir):
# step 3. we found a device but didn't get serials, try to get them
if (len(serials) == 0):
# we got a device path but no saved serial
# try to get the serial from the device
# print u"get_device_settings - device_path = {0}".format(device_path)
# get serial from device_path/.adobe-digital-editions/device.xml
if can_parse_xml:
devicexml = os.path.join(device_path, '.adobe-digital-editions', 'device.xml')
# print u"trying to load {0}".format(devicexml)
if (os.path.exists(devicexml)):
# print u"trying to parse {0}".format(devicexml)
xmltree = ET.parse(devicexml)
for node in xmltree.iter():
if "deviceSerial" in node.tag:
serial = node.text
# print u"found serial {0}".format(serial)
serials.append(serial)
break
else:
# print u"cannot get serials from device."
device_path = u""
self.kobodir = u""
kobodb = u""
if (self.kobodir == u""):
# step 4. we haven't found a device with serials, so try desktop apps
if sys.platform.startswith('win'):
import _winreg as winreg
if sys.getwindowsversion().major > 5:
if 'LOCALAPPDATA' in os.environ.keys():
# Python 2.x does not return unicode env. Use Python 3.x
self.kobodir = winreg.ExpandEnvironmentStrings(u"%LOCALAPPDATA%")
if (self.kobodir == u""):
if 'USERPROFILE' in os.environ.keys():
# Python 2.x does not return unicode env. Use Python 3.x
self.kobodir = os.path.join(winreg.ExpandEnvironmentStrings(u"%USERPROFILE%"), u"Local Settings", u"Application Data")
self.kobodir = os.path.join(self.kobodir, u"Kobo", u"Kobo Desktop Edition")
elif sys.platform.startswith('darwin'):
self.kobodir = os.path.join(os.environ['HOME'], u"Library", u"Application Support", u"Kobo", u"Kobo Desktop Edition")
#elif linux_path != None:
# Probably Linux, let's get the wine prefix and path to Kobo.
# self.kobodir = os.path.join(linux_path, u"Local Settings", u"Application Data", u"Kobo", u"Kobo Desktop Edition")
# desktop versions use Kobo.sqlite
kobodb = os.path.join(self.kobodir, u"Kobo.sqlite")
# check for existence of file
if (not(os.path.isfile(kobodb))):
# give up here, we haven't found anything useful
self.kobodir = u""
kobodb = u""
if (self.kobodir != u""):
self.bookdir = os.path.join(self.kobodir, u"kepub")
# make a copy of the database in a temporary file
# so we can ensure it's not using WAL logging which sqlite3 can't do.
self.newdb = tempfile.NamedTemporaryFile(mode='wb', delete=False)
print self.newdb.name
olddb = open(kobodb, 'rb')
self.newdb.write(olddb.read(18))
self.newdb.write('\x01\x01')
olddb.read(2)
self.newdb.write(olddb.read())
olddb.close()
self.newdb.close()
self.__sqlite = sqlite3.connect(self.newdb.name)
self.__cursor = self.__sqlite.cursor()
self._userkeys = []
self._books = []
self._volumeID = []
self._serials = serials
def close (self):
"""Closes the database used by the library."""
self.__cursor.close()
self.__sqlite.close()
# delete the temporary copy of the database
os.remove(self.newdb.name)
@property
def userkeys (self):
"""The list of potential userkeys being used by this library.
Only one of these will be valid.
"""
if len(self._userkeys) != 0:
return self._userkeys
for macaddr in self.__getmacaddrs():
self._userkeys.extend(self.__getuserkeys(macaddr))
return self._userkeys
@property
def books (self):
"""The list of KoboBook objects in the library."""
if len(self._books) != 0:
return self._books
"""Drm-ed kepub"""
for row in self.__cursor.execute('SELECT DISTINCT volumeid, Title, Attribution, Series FROM content_keys, content WHERE contentid = volumeid'):
self._books.append(KoboBook(row[0], row[1], self.__bookfile(row[0]), 'kepub', self.__cursor, author=row[2], series=row[3]))
self._volumeID.append(row[0])
"""Drm-free"""
for f in os.listdir(self.bookdir):
if(f not in self._volumeID):
row = self.__cursor.execute("SELECT Title, Attribution, Series FROM content WHERE ContentID = '" + f + "'").fetchone()
if row is not None:
fTitle = row[0]
self._books.append(KoboBook(f, fTitle, self.__bookfile(f), 'drm-free', self.__cursor, author=row[1], series=row[2]))
self._volumeID.append(f)
"""Sort"""
self._books.sort(key=lambda x: x.title)
return self._books
def __bookfile (self, volumeid):
"""The filename needed to open a given book."""
return os.path.join(self.kobodir, u"kepub", volumeid)
def __getmacaddrs (self):
"""The list of all MAC addresses on this machine."""
macaddrs = []
if sys.platform.startswith('win'):
c = re.compile('\s(' + '[0-9a-f]{2}-' * 5 + '[0-9a-f]{2})(\s|$)', re.IGNORECASE)
(p_in, p_out, p_err) = os.popen3('ipconfig /all')
for line in p_out:
m = c.search(line)
if m:
macaddrs.append(re.sub("-", ":", m.group(1)).upper())
elif sys.platform.startswith('darwin'):
c = re.compile('\s(' + '[0-9a-f]{2}:' * 5 + '[0-9a-f]{2})(\s|$)', re.IGNORECASE)
output = subprocess.check_output('/sbin/ifconfig -a', shell=True)
matches = c.findall(output)
for m in matches:
# print u"m:{0}".format(m[0])
macaddrs.append(m[0].upper())
else:
# probably linux, let's try ipconfig under wine
c = re.compile('\s(' + '[0-9a-f]{2}-' * 5 + '[0-9a-f]{2})(\s|$)', re.IGNORECASE)
for line in os.popen('ipconfig /all'):
m = c.search(line)
if m:
macaddrs.append(re.sub("-", ":", m.group(1)).upper())
# extend the list of macaddrs in any case with the serials
# cannot hurt ;-)
macaddrs.extend(self._serials)
return macaddrs
def __getuserids (self):
userids = []
cursor = self.__cursor.execute('SELECT UserID FROM user')
row = cursor.fetchone()
while row is not None:
try:
userid = row[0]
userids.append(userid)
except:
pass
row = cursor.fetchone()
return userids
def __getuserkeys (self, macaddr):
userids = self.__getuserids()
userkeys = []
for hash in KOBO_HASH_KEYS:
deviceid = hashlib.sha256(hash + macaddr).hexdigest()
for userid in userids:
userkey = hashlib.sha256(deviceid + userid).hexdigest()
userkeys.append(binascii.a2b_hex(userkey[32:]))
return userkeys
class KoboBook(object):
"""A Kobo book.
A Kobo book contains a number of unencrypted and encrypted files.
This class provides a list of the encrypted files.
Each book has the following instance variables:
volumeid - a UUID which uniquely refers to the book in this library.
title - the human-readable book title.
filename - the complete path and filename of the book.
type - either kepub or drm-free"""
def __init__ (self, volumeid, title, filename, type, cursor, author=None, series=None):
self.volumeid = volumeid
self.title = title
self.author = author
self.series = series
self.series_index = None
self.filename = filename
self.type = type
self.__cursor = cursor
self._encryptedfiles = {}
@property
def encryptedfiles (self):
"""A dictionary of KoboFiles inside the book.
The dictionary keys are the relative pathnames, which are
the same as the pathnames inside the book 'zip' file."""
if (self.type == 'drm-free'):
return self._encryptedfiles
if len(self._encryptedfiles) != 0:
return self._encryptedfiles
# Read the list of encrypted files from the DB
for row in self.__cursor.execute('SELECT elementid,elementkey FROM content_keys,content WHERE volumeid = ? AND volumeid = contentid',(self.volumeid,)):
self._encryptedfiles[row[0]] = KoboFile(row[0], None, base64.b64decode(row[1]))
# Read the list of files from the kepub OPF manifest so that
# we can get their proper MIME type.
# NOTE: this requires that the OPF file is unencrypted!
zin = zipfile.ZipFile(self.filename, "r")
xmlns = {
'ocf': 'urn:oasis:names:tc:opendocument:xmlns:container',
'opf': 'http://www.idpf.org/2007/opf'
}
ocf = ET.fromstring(zin.read('META-INF/container.xml'))
opffile = ocf.find('.//ocf:rootfile', xmlns).attrib['full-path']
basedir = re.sub('[^/]+$', '', opffile)
opf = ET.fromstring(zin.read(opffile))
zin.close()
c = re.compile('/')
for item in opf.findall('.//opf:item', xmlns):
mimetype = item.attrib['media-type']
# Convert relative URIs
href = item.attrib['href']
if not c.match(href):
href = string.join((basedir, href), '')
# Update books we've found from the DB.
if href in self._encryptedfiles:
self._encryptedfiles[href].mimetype = mimetype
return self._encryptedfiles
@property
def has_drm (self):
return not self.type == 'drm-free'
class KoboFile(object):
"""An encrypted file in a KoboBook.
Each file has the following instance variables:
filename - the relative pathname inside the book zip file.
mimetype - the file's MIME type, e.g. 'image/jpeg'
key - the encrypted page key."""
def __init__ (self, filename, mimetype, key):
self.filename = filename
self.mimetype = mimetype
self.key = key
def decrypt (self, userkey, contents):
"""
Decrypt the contents using the provided user key and the
file page key. The caller must determine if the decrypted
data is correct."""
# The userkey decrypts the page key (self.key)
keyenc = AES(userkey)
decryptedkey = keyenc.decrypt(self.key)
# The decrypted page key decrypts the content
pageenc = AES(decryptedkey)
return self.__removeaespadding(pageenc.decrypt(contents))
def check (self, contents):
"""
If the contents uses some known MIME types, check if it
conforms to the type. Throw a ValueError exception if not.
If the contents uses an uncheckable MIME type, don't check
it and don't throw an exception.
Returns True if the content was checked, False if it was not
checked."""
if self.mimetype == 'application/xhtml+xml':
# assume utf-8 with no BOM
textoffset = 0
stride = 1
print u"Checking text:{0}:".format(contents[:10])
# check for byte order mark
if contents[:3]=="\xef\xbb\xbf":
# seems to be utf-8 with BOM
print u"Could be utf-8 with BOM"
textoffset = 3
elif contents[:2]=="\xfe\xff":
# seems to be utf-16BE
print u"Could be utf-16BE"
textoffset = 3
stride = 2
elif contents[:2]=="\xff\xfe":
# seems to be utf-16LE
print u"Could be utf-16LE"
textoffset = 2
stride = 2
else:
print u"Perhaps utf-8 without BOM"
# now check that the first few characters are in the ASCII range
for i in xrange(textoffset,textoffset+5*stride,stride):
if ord(contents[i])<32 or ord(contents[i])>127:
# Non-ascii, so decryption probably failed
print u"Bad character at {0}, value {1}".format(i,ord(contents[i]))
raise ValueError
print u"Seems to be good text"
return True
if contents[:5]=="<?xml" or contents[:8]=="\xef\xbb\xbf<?xml":
# utf-8
return True
elif contents[:14]=="\xfe\xff\x00<\x00?\x00x\x00m\x00l":
# utf-16BE
return True
elif contents[:14]=="\xff\xfe<\x00?\x00x\x00m\x00l\x00":
# utf-16LE
return True
elif contents[:9]=="<!DOCTYPE" or contents[:12]=="\xef\xbb\xbf<!DOCTYPE":
# utf-8 of weird <!DOCTYPE start
return True
elif contents[:22]=="\xfe\xff\x00<\x00!\x00D\x00O\x00C\x00T\x00Y\x00P\x00E":
# utf-16BE of weird <!DOCTYPE start
return True
elif contents[:22]=="\xff\xfe<\x00!\x00D\x00O\x00C\x00T\x00Y\x00P\x00E\x00":
# utf-16LE of weird <!DOCTYPE start
return True
else:
print u"Bad XML: {0}".format(contents[:8])
raise ValueError
elif self.mimetype == 'image/jpeg':
if contents[:3] == '\xff\xd8\xff':
return True
else:
print u"Bad JPEG: {0}".format(contents[:3].encode('hex'))
raise ValueError()
return False
def __removeaespadding (self, contents):
"""
Remove the trailing padding, using what appears to be the CMS
algorithm from RFC 5652 6.3"""
lastchar = binascii.b2a_hex(contents[-1:])
strlen = int(lastchar, 16)
padding = strlen
if strlen == 1:
return contents[:-1]
if strlen < 16:
for i in range(strlen):
testchar = binascii.b2a_hex(contents[-strlen:-(strlen-1)])
if testchar != lastchar:
padding = 0
if padding > 0:
contents = contents[:-padding]
return contents
def decrypt_book(book, lib):
print u"Converting {0}".format(book.title)
zin = zipfile.ZipFile(book.filename, "r")
# make filename out of Unicode alphanumeric and whitespace equivalents from title
outname = u"{0}.epub".format(re.sub('[^\s\w]', '_', book.title, 0, re.UNICODE))
if (book.type == 'drm-free'):
print u"DRM-free book, conversion is not needed"
shutil.copyfile(book.filename, outname)
print u"Book saved as {0}".format(os.path.join(os.getcwd(), outname))
return 0
result = 1
for userkey in lib.userkeys:
print u"Trying key: {0}".format(userkey.encode('hex_codec'))
try:
zout = zipfile.ZipFile(outname, "w", zipfile.ZIP_DEFLATED)
for filename in zin.namelist():
contents = zin.read(filename)
if filename in book.encryptedfiles:
file = book.encryptedfiles[filename]
contents = file.decrypt(userkey, contents)
# Parse failures mean the key is probably wrong.
file.check(contents)
zout.writestr(filename, contents)
zout.close()
print u"Decryption succeeded."
print u"Book saved as {0}".format(os.path.join(os.getcwd(), outname))
result = 0
break
except ValueError:
print u"Decryption failed."
zout.close()
os.remove(outname)
zin.close()
return result
def cli_main():
description = __about__
epilog = u"Parsing of arguments failed."
parser = argparse.ArgumentParser(prog=sys.argv[0], description=description, epilog=epilog)
parser.add_argument('--devicedir', default='/media/KOBOeReader', help="directory of connected Kobo device")
parser.add_argument('--all', action='store_true', help="flag for converting all books on device")
args = vars(parser.parse_args())
serials = []
devicedir = u""
if args['devicedir']:
devicedir = args['devicedir']
lib = KoboLibrary(serials, devicedir)
if args['all']:
books = lib.books
else:
for i, book in enumerate(lib.books):
print u"{0}: {1}".format(i + 1, book.title)
print u"Or 'all'"
choice = raw_input(u"Convert book number... ")
if choice == u'all':
books = list(lib.books)
else:
try:
num = int(choice)
books = [lib.books[num - 1]]
except (ValueError, IndexError):
print u"Invalid choice. Exiting..."
exit()
results = [decrypt_book(book, lib) for book in books]
lib.close()
overall_result = all(result != 0 for result in results)
if overall_result != 0:
print u"Could not decrypt book with any of the keys found."
return overall_result
if __name__ == '__main__':
sys.stdout=SafeUnbuffered(sys.stdout)
sys.stderr=SafeUnbuffered(sys.stderr)
sys.exit(cli_main())

View File

@@ -0,0 +1,31 @@
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<title>Obok DeDRM Plugin Configuration</title>
</head>
<body>
<h1>Obok DeDRM Plugin</h1>
<h3>(version 3.1.3)</h3>
<h3>Installation:</h3>
<p>The ususal method of Preferences -> Plugins -> Load plugin from file.</p>
<h3>Configuration:</h3>
<p>There is no configuration (other than to choose what menus to add obok to)</p>
<h3>Troubleshooting:</h3>
<p >If you find that its not working for you , you can save a lot of time by using the plugin with Calibre in debug mode. This will print out a lot of helpful info that can be copied into any online help requests.</p>
<p>Open a command prompt (terminal window) and type "calibre-debug -g" (without the quotes). Calibre will launch, and you can use the plugin the usual way. The debug info will be output to the original command prompt (terminal window). Copy the resulting output and paste it into the comment you make at Apprentice Alf's blog.</p>
</body>
</html>

Binary file not shown.

View File

@@ -0,0 +1,331 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2014-11-17 12:51+0100\n"
"PO-Revision-Date: 2015-05-31 22:44+1000\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Last-Translator: \n"
"Language-Team: \n"
"X-Generator: Poedit 1.8.1\n"
"Plural-Forms: nplurals=6; plural=(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 "
"&& n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5);\n"
"Language: ar\n"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:80
msgid ""
"<p>No books found in Kobo Library\n"
"Are you sure it's installed\\configured\\synchronized?"
msgstr ""
"<p>لا يوجد كتب بمكتبة كوبو الخاصة بكم\n"
"هل أنت متأكد أنها موجودة\\معدة بشكل سليم\\محملة بشكل كامل؟"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:87
msgid "Legacy key found: "
msgstr "تم العثور على مفتاح شفرة قديم"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:93
msgid "Trouble retrieving keys with newer obok method."
msgstr "هناك مشكلة فى تحميل مفاتيح الشفرة بطريقة أوبوك الجديدة."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:97
msgid "Found {0} possible keys to try."
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:99
msgid "<p>No userkeys found to decrypt books with. No point in proceeding."
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:115
msgid "{} - Decryption canceled by user."
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:135
msgid "{} - \"Add books\" canceled by user."
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:137
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:156
msgid "{} - wrapping up results."
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:153
msgid "{} - User opted not to try to insert EPUB formats"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:188
msgid "{0} - Decrypting {1}"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:197
msgid "{0} - Couldn't decrypt {1}"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:198
msgid "decryption errors"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:213
msgid "{0} - Added {1}"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:218
msgid "{0} - {1} already exists. Will try to add format later."
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:219
msgid "duplicate detected"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:233
msgid "{0} - Successfully added EPUB format to existing {1}"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:236
msgid ""
"{0} - Error adding EPUB format to existing {1}. This really shouldn't happen."
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:259
msgid "{} - \"Insert formats\" canceled by user."
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:291
msgid ""
"<p><b>{0}</b> EPUB{2} successfully added to library.<br /><br /><b>{1}</b> "
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:292
msgid ""
"not added because books with the same title/author were detected.<br /><br /"
">Would you like to try and add the EPUB format{0}"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:293
msgid ""
" to those existing entries?<br /><br />NOTE: no pre-existing EPUBs will be "
"overwritten."
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:295
msgid ""
"{0} -- not added because of {1} in your library.\n"
"\n"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:297
msgid "<p><b>{0}</b> -- not added because of {1} in your library.<br /><br />"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:298
msgid ""
"Would you like to try and add the EPUB format to an available calibre "
"duplicate?<br /><br />"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:299
msgid "NOTE: no pre-existing EPUB will be overwritten."
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:346
msgid "Trying key: "
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:378
msgid "Decryption failed, trying next key."
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:382
msgid "Unknown Error decrypting, trying next key.."
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:395
msgid ""
"<p>All selected Kobo books added as new calibre books or inserted into "
"existing calibre ebooks.<br /><br />No issues."
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:399
msgid "<p>{0} successfully added."
msgstr "<p>{0} تم إضافتهم بنجاح."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:403
msgid ""
"<p>Not all selected Kobo books made it into calibre.<br /><br />View report "
"for details."
msgstr ""
"<p>لم يتم نقل كل كتب كوبو إلى كاليبر.<br/><br/>شاهد التقرير لمزيد من التفاصيل"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:404
msgid "<p><b>Total attempted:</b> {}</p>\n"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:405
msgid "<p><b>Decryption errors:</b> {}</p>\n"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:411
msgid "<p><b>New Books created:</b> {}</p>\n"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:418
msgid "<p><b>Duplicates that weren't added:</b> {}</p>\n"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:426
msgid "<p><b>Book imports cancelled by user:</b> {}</p>\n"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:428
msgid ""
"<p><b>New EPUB formats inserted in existing calibre books:</b> {0}</p>\n"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:434
msgid ""
"<p><b>EPUB formats NOT inserted into existing calibre books:</b> {}<br />\n"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:435
msgid ""
"(Either because the user <i>chose</i> not to insert them, or because all "
"duplicates already had an EPUB format)"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:444
msgid "<p><b>Format imports cancelled by user:</b> {}</p>\n"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:458
msgid "Unknown Book Title"
msgstr "عنوان غير معروف"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:460
msgid "it couldn't be decrypted."
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:462
msgid ""
"user CHOSE not to insert the new EPUB format, or all existing calibre "
"entries HAD an EPUB format already."
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:464
msgid "of unknown reasons. Gosh I'm embarrassed!"
msgstr "معذرة، خطأ غير معروف! أشعر بالخجل من هذا!"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:465
msgid "<p>{0} not added because {1}"
msgstr "<p>لم يتم إضافة {0} بسبب {1}"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\common_utils.py:226
msgid "Help"
msgstr "مساعدة"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\common_utils.py:235
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\utilities.py:214
msgid "Restart required"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\common_utils.py:236
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\utilities.py:215
msgid ""
"Title image not found - you must restart Calibre before using this plugin!"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\common_utils.py:322
msgid "Undefined"
msgstr "غير معرف"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:30
msgid "When should Obok try to insert EPUBs into existing calibre entries?"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:33
msgid ""
"<p>Default behavior when duplicates are detected. None of the choices will "
"cause calibre ebooks to be overwritten"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:35
msgid "Ask"
msgstr "اسأل"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:35
msgid "Always"
msgstr "دائما"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:35
msgid "Never"
msgstr "أبدا"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:65
msgid "Obok DeDRM"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:89
msgid "Select All"
msgstr "اختر الكل"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:90
msgid "Select all books to add them to the calibre library."
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:92
msgid "All with DRM"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:93
msgid "Select all books with DRM."
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:95
msgid "All DRM free"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:96
msgid "Select all books without DRM."
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:146
msgid "Title"
msgstr "العنوان"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:146
msgid "Author"
msgstr "مؤلِّف"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:146
msgid "Series"
msgstr "السلسلة"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:369
msgid "Copy to clipboard"
msgstr "نسخ إلى الحافظة"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:397
msgid "View Report"
msgstr "مشاهدة التقرير"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\__init__.py:21
msgid "Removes DRM from Kobo kepubs and adds them to the library."
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\obok\obok.py:162
msgid "AES improper key used"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\obok\obok.py:167
msgid "Failed to initialize AES key"
msgstr "خطأ فى تحميل مفتاح AES"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\obok\obok.py:175
msgid "AES decryption failed"
msgstr "خطأ فى فك الحماية بطريقة AES"

Binary file not shown.

View File

@@ -0,0 +1,102 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: obok\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2014-10-19 10:28+0200\n"
"PO-Revision-Date: 2014-10-23 14:43+0100\n"
"Last-Translator: \n"
"Language-Team: friends of obok\n"
"Language: de\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 1.6.10\n"
#: common_utils.py:220
msgid "Help"
msgstr "Hilfe"
#: common_utils.py:229 utilities.py:207
msgid "Restart required"
msgstr "Neustart erforderlich"
#: common_utils.py:230 utilities.py:208
msgid ""
"Title image not found - you must restart Calibre before using this plugin!"
msgstr ""
"Das Abbild wurde nicht gefunden. - vor der Verwendung dieses Calibre Plugin "
"is ein Neustart erforderlich!"
#: common_utils.py:316
msgid "Undefined"
msgstr "Undefiniert"
#: config.py:25
msgid ""
"<p>Default behavior when duplicates are detected. None of the choices will "
"cause calibre ebooks to be overwritten"
msgstr ""
"<p>Standardverhalten, wenn Duplikate erkannt werden. Keine der "
"Entscheidungen werden ebooks verursachen das sie überschrieben werden."
#: dialogs.py:58
msgid "Obok DeDRM"
msgstr "Obok DeDRM"
#: dialogs.py:68
msgid "<a href=\"http://www.foo.com/\">Help</a>"
msgstr "<a href=\"http://www.foo.com/\">Hilfe</a>"
#: dialogs.py:82
msgid "Select All"
msgstr "Alles markieren"
#: dialogs.py:83
msgid "Select all books to add them to the calibre library."
msgstr "Wählen Sie alle Bücher, um sie zu Calibre Bibliothek hinzuzufügen."
#: dialogs.py:85
msgid "All with DRM"
msgstr "Alle mit DRM"
#: dialogs.py:86
msgid "Select all books with DRM."
msgstr "Wählen Sie alle Bücher mit DRM."
#: dialogs.py:88
msgid "All DRM free"
msgstr "Alle ohne DRM"
#: dialogs.py:89
msgid "Select all books without DRM."
msgstr "Wählen Sie alle Bücher ohne DRM."
#: dialogs.py:139
msgid "Title"
msgstr "Titel"
#: dialogs.py:139
msgid "Author"
msgstr "Autor"
#: dialogs.py:139
msgid "Series"
msgstr "Reihe"
#: dialogs.py:362
msgid "Copy to clipboard"
msgstr "In Zwischenablage kopieren"
#: dialogs.py:390
msgid "View Report"
msgstr "Bericht anzeigen"
#: __init__.py:24
msgid "Removes DRM from Kobo kepubs and adds them to the library."
msgstr "Entfernt DRM von Kobo kepubs und fügt sie zu Bibliothek hinzu."

View File

@@ -0,0 +1,335 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2014-11-17 12:51+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: 8bit\n"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:80
msgid ""
"<p>No books found in Kobo Library\n"
"Are you sure it's installed\\configured\\synchronized?"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:87
msgid "Legacy key found: "
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:93
msgid "Trouble retrieving keys with newer obok method."
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:97
msgid "Found {0} possible keys to try."
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:99
msgid "<p>No userkeys found to decrypt books with. No point in proceeding."
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:115
msgid "{} - Decryption canceled by user."
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:135
msgid "{} - \"Add books\" canceled by user."
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:137
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:156
msgid "{} - wrapping up results."
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:153
msgid "{} - User opted not to try to insert EPUB formats"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:188
msgid "{0} - Decrypting {1}"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:197
msgid "{0} - Couldn't decrypt {1}"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:198
msgid "decryption errors"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:213
msgid "{0} - Added {1}"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:218
msgid "{0} - {1} already exists. Will try to add format later."
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:219
msgid "duplicate detected"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:233
msgid "{0} - Successfully added EPUB format to existing {1}"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:236
msgid ""
"{0} - Error adding EPUB format to existing {1}. This really shouldn't happen."
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:259
msgid "{} - \"Insert formats\" canceled by user."
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:291
msgid ""
"<p><b>{0}</b> EPUB{2} successfully added to library.<br /><br /><b>{1}</b> "
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:292
msgid ""
"not added because books with the same title/author were detected.<br /><br /"
">Would you like to try and add the EPUB format{0}"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:293
msgid ""
" to those existing entries?<br /><br />NOTE: no pre-existing EPUBs will be "
"overwritten."
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:295
msgid ""
"{0} -- not added because of {1} in your library.\n"
"\n"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:297
msgid "<p><b>{0}</b> -- not added because of {1} in your library.<br /><br />"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:298
msgid ""
"Would you like to try and add the EPUB format to an available calibre "
"duplicate?<br /><br />"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:299
msgid "NOTE: no pre-existing EPUB will be overwritten."
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:346
msgid "Trying key: "
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:378
msgid "Decryption failed, trying next key."
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:382
msgid "Unknown Error decrypting, trying next key.."
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:395
msgid ""
"<p>All selected Kobo books added as new calibre books or inserted into "
"existing calibre ebooks.<br /><br />No issues."
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:399
msgid "<p>{0} successfully added."
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:403
msgid ""
"<p>Not all selected Kobo books made it into calibre.<br /><br />View report "
"for details."
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:404
msgid "<p><b>Total attempted:</b> {}</p>\n"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:405
msgid "<p><b>Decryption errors:</b> {}</p>\n"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:411
msgid "<p><b>New Books created:</b> {}</p>\n"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:418
msgid "<p><b>Duplicates that weren't added:</b> {}</p>\n"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:426
msgid "<p><b>Book imports cancelled by user:</b> {}</p>\n"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:428
msgid ""
"<p><b>New EPUB formats inserted in existing calibre books:</b> {0}</p>\n"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:434
msgid ""
"<p><b>EPUB formats NOT inserted into existing calibre books:</b> {}<br />\n"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:435
msgid ""
"(Either because the user <i>chose</i> not to insert them, or because all "
"duplicates already had an EPUB format)"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:444
msgid "<p><b>Format imports cancelled by user:</b> {}</p>\n"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:458
msgid "Unknown Book Title"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:460
msgid "it couldn't be decrypted."
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:462
msgid ""
"user CHOSE not to insert the new EPUB format, or all existing calibre "
"entries HAD an EPUB format already."
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:464
msgid "of unknown reasons. Gosh I'm embarrassed!"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:465
msgid "<p>{0} not added because {1}"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\common_utils.py:226
msgid "Help"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\common_utils.py:235
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\utilities.py:214
msgid "Restart required"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\common_utils.py:236
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\utilities.py:215
msgid ""
"Title image not found - you must restart Calibre before using this plugin!"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\common_utils.py:322
msgid "Undefined"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:30
msgid "When should Obok try to insert EPUBs into existing calibre entries?"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:33
msgid ""
"<p>Default behavior when duplicates are detected. None of the choices will "
"cause calibre ebooks to be overwritten"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:35
msgid "Ask"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:35
msgid "Always"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:35
msgid "Never"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:60
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\utilities.py:150
msgid " v"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:65
msgid "Obok DeDRM"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:68
msgid "<a href=\"http://www.foo.com/\">Help</a>"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:89
msgid "Select All"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:90
msgid "Select all books to add them to the calibre library."
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:92
msgid "All with DRM"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:93
msgid "Select all books with DRM."
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:95
msgid "All DRM free"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:96
msgid "Select all books without DRM."
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:146
msgid "Title"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:146
msgid "Author"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:146
msgid "Series"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:369
msgid "Copy to clipboard"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:397
msgid "View Report"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\__init__.py:21
msgid "Removes DRM from Kobo kepubs and adds them to the library."
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\obok\obok.py:162
msgid "AES improper key used"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\obok\obok.py:167
msgid "Failed to initialize AES key"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\obok\obok.py:175
msgid "AES decryption failed"
msgstr ""

Binary file not shown.

View File

@@ -0,0 +1,419 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: obok\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2014-11-17 12:51+0100\n"
"PO-Revision-Date: 2014-11-17 21:32+0100\n"
"Last-Translator: Friends of obok\n"
"Language-Team: friends of obok\n"
"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 1.6.10\n"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:80
msgid ""
"<p>No books found in Kobo Library\n"
"Are you sure it's installed\\configured\\synchronized?"
msgstr ""
"<p>No se han encontrado libros en la biblioteca de Kobo\n"
"¿Estás seguro que está instalada\\configurada\\sincronizada?"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:87
msgid "Legacy key found: "
msgstr "Clave antigua localizada:"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:93
msgid "Trouble retrieving keys with newer obok method."
msgstr "Problema al obtener las claves con el nuevo método obok"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:97
msgid "Found {0} possible keys to try."
msgstr "Localizadas {0} posibles claves que probar."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:99
msgid "<p>No userkeys found to decrypt books with. No point in proceeding."
msgstr ""
"<p>No se han encontrado claves de usuarios con las que desencriptar los "
"libros. No tiene sentido proceder."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:115
msgid "{} - Decryption canceled by user."
msgstr "{} - Desencriptación cancelada por el usuario"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:135
msgid "{} - \"Add books\" canceled by user."
msgstr "{} - \"Añadir libros\" cancelado por el usuario."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:137
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:156
msgid "{} - wrapping up results."
msgstr "{} - Preparando resultados."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:153
msgid "{} - User opted not to try to insert EPUB formats"
msgstr "{} - El usuario optó por no tratar de insertar los formatos EPUB."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:188
msgid "{0} - Decrypting {1}"
msgstr "{0} - Desencriptando {1}"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:197
msgid "{0} - Couldn't decrypt {1}"
msgstr "{0} - No se pudo desencriptar {1}"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:198
msgid "decryption errors"
msgstr "errores de desencriptación"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:213
msgid "{0} - Added {1}"
msgstr "{0} - Añadido {1}"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:218
msgid "{0} - {1} already exists. Will try to add format later."
msgstr "{0} - {1} ya existe. Se tratará de añadir el formato más tarde."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:219
msgid "duplicate detected"
msgstr "detectado un duplicado"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:233
msgid "{0} - Successfully added EPUB format to existing {1}"
msgstr "{0} - Formato EPUB añadido con éxito al {1} existente"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:236
msgid ""
"{0} - Error adding EPUB format to existing {1}. This really shouldn't happen."
msgstr ""
"{0} - Error al añadir el formato EPUB al existente {1}. Esto realmente no "
"debería ocurrir."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:259
msgid "{} - \"Insert formats\" canceled by user."
msgstr "{} - \"Insertar formatos\" cancelado por el usuario."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:291
msgid ""
"<p><b>{0}</b> EPUB{2} successfully added to library.<br /><br /><b>{1}</b> "
msgstr ""
"<p><b>{0}</b> EPUB({2}) añadido({2}) con éxito a la biblioteca.<br /><br /"
"><b>{1}</b> "
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:292
msgid ""
"not added because books with the same title/author were detected.<br /><br /"
">Would you like to try and add the EPUB format{0}"
msgstr ""
"no añadido({0}) porque se han detectado libros con el mismo título/autor."
"<br /><br />¿Deseas añadir el formato EPUB"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:293
msgid ""
" to those existing entries?<br /><br />NOTE: no pre-existing EPUBs will be "
"overwritten."
msgstr ""
" a las entradas existentes?<br /><br />NOTA: no se sobreescribirá ningún "
"EPUB que ya existiera."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:295
msgid ""
"{0} -- not added because of {1} in your library.\n"
"\n"
msgstr ""
"{0} -- no añadido porque {1} está en tu biblioteca.\n"
"\n"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:297
msgid "<p><b>{0}</b> -- not added because of {1} in your library.<br /><br />"
msgstr ""
"<p><b>{0}</b> -- no se ha añadido porque se ha {1}, que está en tu "
"biblioteca.<br /><br />"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:298
msgid ""
"Would you like to try and add the EPUB format to an available calibre "
"duplicate?<br /><br />"
msgstr ""
"¿Desearías añadir el formato EPUB al elemento que ya está disponible en "
"calibre?<br /><br />"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:299
msgid "NOTE: no pre-existing EPUB will be overwritten."
msgstr "NOTA: no se sobreescribirá ningún EPUB que ya existiera."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:346
msgid "Trying key: "
msgstr "Probando clave:"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:378
msgid "Decryption failed, trying next key."
msgstr "La desencriptación falló, probando la clave siguiente."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:382
msgid "Unknown Error decrypting, trying next key.."
msgstr "Error desconocido al desencriptar, probando siguiente clave..."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:395
msgid ""
"<p>All selected Kobo books added as new calibre books or inserted into "
"existing calibre ebooks.<br /><br />No issues."
msgstr ""
"<p>Todos los libros de Kobo seleccionados se han añadido a calibre como "
"nuevos libros o en libros ya existentes.<br /><br />Sin problemas."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:399
msgid "<p>{0} successfully added."
msgstr "<p><b>{0}</b> añadido con éxito."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:403
msgid ""
"<p>Not all selected Kobo books made it into calibre.<br /><br />View report "
"for details."
msgstr ""
"<p>No se han añadido a calibre todos los libros de Kobo seleccionados.<br /"
"><br />Comprueba el informe para obtener los detalles."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:404
msgid "<p><b>Total attempted:</b> {}</p>\n"
msgstr "<p><b>Intentados en total:</b> {}</p>\n"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:405
msgid "<p><b>Decryption errors:</b> {}</p>\n"
msgstr "<p><b>Errores de desencriptación:</b> {}</p>\n"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:411
msgid "<p><b>New Books created:</b> {}</p>\n"
msgstr "<p><b>Nuevos libros creados:</b> {}</p>\n"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:418
msgid "<p><b>Duplicates that weren't added:</b> {}</p>\n"
msgstr "<p><b>Duplicados que no se han añadido:</b> {}</p>\n"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:426
msgid "<p><b>Book imports cancelled by user:</b> {}</p>\n"
msgstr "<p><b>Importación de libros cancelada por el usuario:</b> {}</p>\n"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:428
msgid ""
"<p><b>New EPUB formats inserted in existing calibre books:</b> {0}</p>\n"
msgstr ""
"<p><b>Nuevos formatos EPUB insertados en libros existentes en calibre:</b> "
"{0}</p>\n"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:434
msgid ""
"<p><b>EPUB formats NOT inserted into existing calibre books:</b> {}<br />\n"
msgstr ""
"<p><b>Formatos EPUB NO insertados en libros de calibre existentes:</b> {}"
"<br />\n"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:435
msgid ""
"(Either because the user <i>chose</i> not to insert them, or because all "
"duplicates already had an EPUB format)"
msgstr ""
"(Bien porque el usuario <i>eligió</i> no insertarlos, o porque todos los "
"duplicados ya tenían un formato EPUB)"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:444
msgid "<p><b>Format imports cancelled by user:</b> {}</p>\n"
msgstr "<p><b>Importación de formatos cancelada por el usuario:</b> {}</p>\n"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:458
msgid "Unknown Book Title"
msgstr "Título de libro desconocido"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:460
msgid "it couldn't be decrypted."
msgstr "no se podía desencriptar."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:462
msgid ""
"user CHOSE not to insert the new EPUB format, or all existing calibre "
"entries HAD an EPUB format already."
msgstr ""
"el usuario ELIGIÓ no insertar el nuevo formato EPUB o todas las entradas de "
"calibre existentes ya TENÍAN un formato EPUB."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:464
msgid "of unknown reasons. Gosh I'm embarrassed!"
msgstr "por razones desconocidas. ¡Dios, qué vergüenza!"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:465
msgid "<p>{0} not added because {1}"
msgstr "<p><b>{0}</b> no se ha añadido porque {1}"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\common_utils.py:226
msgid "Help"
msgstr "Ayuda"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\common_utils.py:235
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\utilities.py:214
msgid "Restart required"
msgstr "Se necesita reiniciar"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\common_utils.py:236
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\utilities.py:215
msgid ""
"Title image not found - you must restart Calibre before using this plugin!"
msgstr ""
"Imagen del título no encontrada - ¡debes reiniciar Calibre antes de usar "
"este plugin!"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\common_utils.py:322
msgid "Undefined"
msgstr "Indefinido"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:30
msgid "When should Obok try to insert EPUBs into existing calibre entries?"
msgstr ""
"¿Cuándo debería Obok tratar de insertar EPUB en las entradas de calibre que "
"ya existen?"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:33
msgid ""
"<p>Default behavior when duplicates are detected. None of the choices will "
"cause calibre ebooks to be overwritten"
msgstr ""
"<p>Comportamiento por defecto cuando se detectan duplicados. Ninguna de las "
"opciones provocará que se sobreescriban los libros en calibre."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:35
msgid "Ask"
msgstr "Preguntar"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:35
msgid "Always"
msgstr "Siempre"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:35
msgid "Never"
msgstr "Nunca"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:60
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\utilities.py:150
msgid " v"
msgstr "v"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:65
msgid "Obok DeDRM"
msgstr "Obok DeDRM"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:68
msgid "<a href=\"http://www.foo.com/\">Help</a>"
msgstr "<a href=\"http://www.foo.com/\">Ayuda</a>"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:89
msgid "Select All"
msgstr "Seleccionar todo"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:90
msgid "Select all books to add them to the calibre library."
msgstr ""
"Seleccionar todos los libros para añadirlos a la biblioteca de calibre."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:92
msgid "All with DRM"
msgstr "Todos con DRM"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:93
msgid "Select all books with DRM."
msgstr "Seleccionar todos los libros con DRM."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:95
msgid "All DRM free"
msgstr "Todos sin DRM"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:96
msgid "Select all books without DRM."
msgstr "Seleccionar todos los libros sin DRM."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:146
msgid "Title"
msgstr "Título"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:146
msgid "Author"
msgstr "Autor"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:146
msgid "Series"
msgstr "Serie"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:369
msgid "Copy to clipboard"
msgstr "Copiar al portapapeles"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:397
msgid "View Report"
msgstr "Ver informe"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\__init__.py:21
msgid "Removes DRM from Kobo kepubs and adds them to the library."
msgstr "Elimina el DRM de kepubs de Kobo y los añade a la biblioteca."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\obok\obok.py:162
msgid "AES improper key used"
msgstr "Utilizada clave AES inapropiada"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\obok\obok.py:167
msgid "Failed to initialize AES key"
msgstr "Fallo al inicializar clave AES"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\obok\obok.py:175
msgid "AES decryption failed"
msgstr "Fallo de desencriptación AES"
#~ msgid ""
#~ "<p>No Kobo Library found\n"
#~ "Are you sure it's installed\\configured\\synchronized?"
#~ msgstr ""
#~ "<p>No se ha encontrado la biblioteca de Kobo\n"
#~ "¿Estás seguro que está instalada\\configurada\\sincronizada?"
#~ msgid "Decryption"
#~ msgstr "Desencriptación"
#~ msgid "Adding"
#~ msgstr "Añadiendo"
#~ msgid "Addition"
#~ msgstr "Adición"
#~ msgid "new calibre books"
#~ msgstr "nuevos libros de calibre"
#~ msgid " (*: drm - free)"
#~ msgstr "(*: sin drm)"
#~ msgid "* : drm - free"
#~ msgstr "*: sin drm"
#~ msgid "You must make a selection!"
#~ msgstr "¡Debes seleccionar algo!"
#~ msgid "Cancel"
#~ msgstr "Cancelar"
#~ msgid "{0} {1} {2} ({3} {4} failures)..."
#~ msgstr "{0} {1} {2} ({3} {4} fallos)..."
#~ msgid "Added"
#~ msgstr "Añadido"
#~ msgid "formats"
#~ msgstr "formatos"
#~ msgid "Yes"
#~ msgstr "Sí"
#~ msgid "No"
#~ msgstr "No"

Binary file not shown.

View File

@@ -0,0 +1,102 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: obok\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2014-10-19 10:28+0200\n"
"PO-Revision-Date: 2014-10-23 14:08+0100\n"
"Last-Translator: \n"
"Language-Team: friends of obok\n"
"Language: nl\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 1.6.10\n"
#: common_utils.py:220
msgid "Help"
msgstr "Help"
#: common_utils.py:229 utilities.py:207
msgid "Restart required"
msgstr "Opnieuw opstarten vereist"
#: common_utils.py:230 utilities.py:208
msgid ""
"Title image not found - you must restart Calibre before using this plugin!"
msgstr ""
"Afbeelding niet gevonden. - Calibre moet opnieuw opgestart worden voordat "
"deze plugin kan worden gebruikt!"
#: common_utils.py:316
msgid "Undefined"
msgstr "Niet gedefinieerd"
#: config.py:25
msgid ""
"<p>Default behavior when duplicates are detected. None of the choices will "
"cause calibre ebooks to be overwritten"
msgstr ""
"<p>Standaard gedrag wanneer er duplicaten worden geconstateerd. Geen van de "
"opties zal reeds bestaande ebooks in de Calibre bibliotheek overschrijven."
#: dialogs.py:58
msgid "Obok DeDRM"
msgstr "Obok DeDRM"
#: dialogs.py:68
msgid "<a href=\"http://www.foo.com/\">Help</a>"
msgstr "<a href=\"http://www.foo.com/\">Help</a>"
#: dialogs.py:82
msgid "Select All"
msgstr "Alles selecteren"
#: dialogs.py:83
msgid "Select all books to add them to the calibre library."
msgstr "Alle boeken selecteren om ze aan de Calibre bibliotheek toe te voegen."
#: dialogs.py:85
msgid "All with DRM"
msgstr "Alle met DRM"
#: dialogs.py:86
msgid "Select all books with DRM."
msgstr "Alle boeken met DRM selecteren."
#: dialogs.py:88
msgid "All DRM free"
msgstr "Alle zonder DRM"
#: dialogs.py:89
msgid "Select all books without DRM."
msgstr "Alle boeken zonder DRM selecteren."
#: dialogs.py:139
msgid "Title"
msgstr "Titel"
#: dialogs.py:139
msgid "Author"
msgstr "Auteur"
#: dialogs.py:139
msgid "Series"
msgstr "Reeks/serie"
#: dialogs.py:362
msgid "Copy to clipboard"
msgstr "Naar het Klembord kopiëren"
#: dialogs.py:390
msgid "View Report"
msgstr "Rapport weergeven"
#: __init__.py:24
msgid "Removes DRM from Kobo kepubs and adds them to the library."
msgstr "Verwijdert de DRM van Kobo kepubs en voegt ze toe aan de bibliotheek."

Binary file not shown.

View File

@@ -0,0 +1,361 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2014-11-17 12:51+0100\n"
"PO-Revision-Date: 2015-05-31 22:44+1000\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Last-Translator: \n"
"Language-Team: \n"
"X-Generator: Poedit 1.8.1\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"Language: pt\n"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:80
msgid ""
"<p>No books found in Kobo Library\n"
"Are you sure it's installed\\configured\\synchronized?"
msgstr ""
"<p>Não foram encontrados livros na livraria Kobo\n"
"Tem a certeza de que está instalado\\configured\\synchronized?"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:87
msgid "Legacy key found: "
msgstr "Chave de legado encontrada"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:93
msgid "Trouble retrieving keys with newer obok method."
msgstr "Problema na obtenção das chaves com o novo método obok."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:97
msgid "Found {0} possible keys to try."
msgstr "Encontradas {0} chaves possíveis para experimentar."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:99
msgid "<p>No userkeys found to decrypt books with. No point in proceeding."
msgstr ""
"Não foi encontrada nenhuma chave de usuário com a qual desencriptar os "
"livros. Não vale a pena continuar."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:115
msgid "{} - Decryption canceled by user."
msgstr "Desencriptação cancelada pelo utilizador."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:135
msgid "{} - \"Add books\" canceled by user."
msgstr "{} - \"Adição de livros\" cancelada pelo utilizador."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:137
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:156
msgid "{} - wrapping up results."
msgstr "{} - finalizando os resultados."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:153
msgid "{} - User opted not to try to insert EPUB formats"
msgstr "O utilizador optou por não tentar inserir formatos EPUB"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:188
msgid "{0} - Decrypting {1}"
msgstr "Desencriptando"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:197
msgid "{0} - Couldn't decrypt {1}"
msgstr "{0} - Não foi possível desencriptar {1}"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:198
msgid "decryption errors"
msgstr "erros na desencriptação"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:213
msgid "{0} - Added {1}"
msgstr "{0} - Adicionado {1}"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:218
msgid "{0} - {1} already exists. Will try to add format later."
msgstr "{0} - {1} já existe. a adição do formato irá ser tentada mais tarde."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:219
msgid "duplicate detected"
msgstr "detectados duplicados"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:233
msgid "{0} - Successfully added EPUB format to existing {1}"
msgstr "{0} - formato EPUB adicionado com sucesso ao existente {1}"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:236
msgid ""
"{0} - Error adding EPUB format to existing {1}. This really shouldn't happen."
msgstr ""
"{0} - Erro ao adicionar o formato EPUB ao existente {1}. Isto realmente não "
"deveria acontecer."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:259
msgid "{} - \"Insert formats\" canceled by user."
msgstr "{} - \"Inserção de formatos\" cancelada pelo utilizador."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:291
msgid ""
"<p><b>{0}</b> EPUB{2} successfully added to library.<br /><br /><b>{1}</b> "
msgstr ""
"<p><b>{0}</b> EPUB{2} adicionado com sucesso à biblioteca.<br /><br /><b>{1}"
"</b> "
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:292
msgid ""
"not added because books with the same title/author were detected.<br /><br /"
">Would you like to try and add the EPUB format{0}"
msgstr ""
"não adicionados porque foram detectados com o mesmo título/autor.<br /><br /"
">Gostaria de tentar e adicionar o formato EPUB{0}"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:293
msgid ""
" to those existing entries?<br /><br />NOTE: no pre-existing EPUBs will be "
"overwritten."
msgstr ""
" às entradas existentes?<br /><br />NOTA: EPUBs pré existentes não serão "
"reescritos."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:295
msgid ""
"{0} -- not added because of {1} in your library.\n"
"\n"
msgstr ""
"{0} -- não adicionado porque {1} na biblioteca.\n"
"\n"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:297
msgid "<p><b>{0}</b> -- not added because of {1} in your library.<br /><br />"
msgstr "<p><b>{0}</b> -- não adicionado porque {1} na biblioteca.<br /><br />"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:298
msgid ""
"Would you like to try and add the EPUB format to an available calibre "
"duplicate?<br /><br />"
msgstr ""
"Gostaria de tentar adicionar o formato EPUB a um duplicado já existente no "
"calibre?<br /><br />"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:299
msgid "NOTE: no pre-existing EPUB will be overwritten."
msgstr "NOTA: EPUBs pré existentes não serão reescritos."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:346
msgid "Trying key: "
msgstr "Experimentando a chave:"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:378
msgid "Decryption failed, trying next key."
msgstr "A desencriptação falhou, tentado a próxima chave."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:382
msgid "Unknown Error decrypting, trying next key.."
msgstr "Erro desconhecido na desencriptação, tentado a próxima chave."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:395
msgid ""
"<p>All selected Kobo books added as new calibre books or inserted into "
"existing calibre ebooks.<br /><br />No issues."
msgstr ""
"<p>Todos os livros Kobo selecionados foram adicionados como livros novos no "
"calibre ou inseridos em livros já existentes no calibre.<br /><br />Sem "
"problemas."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:399
msgid "<p>{0} successfully added."
msgstr "<p>{0} adicionados com sucesso."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:403
msgid ""
"<p>Not all selected Kobo books made it into calibre.<br /><br />View report "
"for details."
msgstr ""
"<p>Nem todos os livros Kobo selecionados seguiram para o calibre.<br /><br /"
">Veja o relatório para mais detalhes."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:404
msgid "<p><b>Total attempted:</b> {}</p>\n"
msgstr "<p><b>tentativas totais:</b> {}</p>\n"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:405
msgid "<p><b>Decryption errors:</b> {}</p>\n"
msgstr "<p><b>Erros de desencriptação:</b> {}</p>\n"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:411
msgid "<p><b>New Books created:</b> {}</p>\n"
msgstr "<p><b>Novos livros criados:</b> {}</p>\n"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:418
msgid "<p><b>Duplicates that weren't added:</b> {}</p>\n"
msgstr "<p><b>Duplicados não adicionados:</b> {}</p>\n"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:426
msgid "<p><b>Book imports cancelled by user:</b> {}</p>\n"
msgstr "<p><b>Importação de livros cancelada pelo utilizador:</b> {}</p>\n"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:428
msgid ""
"<p><b>New EPUB formats inserted in existing calibre books:</b> {0}</p>\n"
msgstr ""
"<p><b>Novos formatos EPUB inseridos em livros existentes no calibre:</b> {0}"
"</p>\n"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:434
msgid ""
"<p><b>EPUB formats NOT inserted into existing calibre books:</b> {}<br />\n"
msgstr ""
"<p><b>formatos EPUB NÃO inseridos em livros existentes no calibre:</b> {}"
"<br />\n"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:435
msgid ""
"(Either because the user <i>chose</i> not to insert them, or because all "
"duplicates already had an EPUB format)"
msgstr ""
"(Porque o utilizador <i>escolheu</i> não os inserir, ou porque todos os "
"duplicados já tinham um formato EPUB)"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:444
msgid "<p><b>Format imports cancelled by user:</b> {}</p>\n"
msgstr "<p><b>Importação do formato cancelada pelo utilizador:</b> {}</p>\n"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:458
msgid "Unknown Book Title"
msgstr "Título do livro desconhecido"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:460
msgid "it couldn't be decrypted."
msgstr "não pode ser desencriptado."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:462
msgid ""
"user CHOSE not to insert the new EPUB format, or all existing calibre "
"entries HAD an EPUB format already."
msgstr ""
"o utilizador ESCOLHEU não inserir o novo formato EPUB, ou todas as entradas "
"existentes no calibre já tinham um formato EPUB."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:464
msgid "of unknown reasons. Gosh I'm embarrassed!"
msgstr "de razões desconhecidas. Estou envergonhado!"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\action.py:465
msgid "<p>{0} not added because {1}"
msgstr "<p>{0} não adicionado porque {1}"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\common_utils.py:226
msgid "Help"
msgstr "Help"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\common_utils.py:235
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\utilities.py:214
msgid "Restart required"
msgstr "Reinicio requerido"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\common_utils.py:236
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\utilities.py:215
msgid ""
"Title image not found - you must restart Calibre before using this plugin!"
msgstr ""
"Imagem do título não encontrada - tem que reiniciar o Calibre antes de "
"utilizar este plugin!"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\common_utils.py:322
msgid "Undefined"
msgstr "Não definido"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:30
msgid "When should Obok try to insert EPUBs into existing calibre entries?"
msgstr ""
"Quando deve o Obok tentar inserir EPUBs em entradas já existentes no calibre?"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:33
msgid ""
"<p>Default behavior when duplicates are detected. None of the choices will "
"cause calibre ebooks to be overwritten"
msgstr ""
"<p>Comportamento por defeito quando são detetados duplicados. Nenhuma das "
"escolhas fará com que os livros existentes no calibre sejam reescritos"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:35
msgid "Ask"
msgstr "Pergunta"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:35
msgid "Always"
msgstr "Sempre"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\config.py:35
msgid "Never"
msgstr "Nunca"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:65
msgid "Obok DeDRM"
msgstr ""
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:89
msgid "Select All"
msgstr "Selecionar todos"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:90
msgid "Select all books to add them to the calibre library."
msgstr "Selecionar todos os livros para adicioná-los à biblioteca do calibre."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:92
msgid "All with DRM"
msgstr "Todos com DRM"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:93
msgid "Select all books with DRM."
msgstr "Selecionar todos os livros com DRM"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:95
msgid "All DRM free"
msgstr "Todos sem DRM"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:96
msgid "Select all books without DRM."
msgstr "Selecionar todos os livros sem DRM."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:146
msgid "Title"
msgstr "Título"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:146
msgid "Author"
msgstr "Autor"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:146
msgid "Series"
msgstr "Série"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:369
msgid "Copy to clipboard"
msgstr "Copiar para a área de transferência"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\dialogs.py:397
msgid "View Report"
msgstr "Ver relatório"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\__init__.py:21
msgid "Removes DRM from Kobo kepubs and adds them to the library."
msgstr "Remove o DRM dos kepubs Kobo e adiciona-os à biblioteca."
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\obok\obok.py:162
msgid "AES improper key used"
msgstr "AES chave imprópria usada"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\obok\obok.py:167
msgid "Failed to initialize AES key"
msgstr "Falha na inicialização da chave AES"
#: I:\Herramientas\PoeditPortable\App\Poedit\bin\obok_plugin-3.1.0_trad\obok\obok.py:175
msgid "AES decryption failed"
msgstr "A desencriptação da chave AES falhou"

View File

@@ -0,0 +1,228 @@
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__docformat__ = 'restructuredtext en'
import os, struct, time
from StringIO import StringIO
from traceback import print_exc
try:
from PyQt5.Qt import (Qt, QDialog, QPixmap, QIcon, QLabel, QHBoxLayout, QFont, QTableWidgetItem)
except ImportError:
from PyQt4.Qt import (Qt, QDialog, QPixmap, QIcon, QLabel, QHBoxLayout, QFont, QTableWidgetItem)
from calibre.utils.config import config_dir
from calibre.constants import iswindows, DEBUG
from calibre import prints
from calibre.gui2 import (error_dialog, gprefs)
from calibre.gui2.actions import menu_action_unique_name
from calibre_plugins.obok_dedrm.__init__ import (PLUGIN_NAME,
PLUGIN_SAFE_NAME, PLUGIN_VERSION, PLUGIN_DESCRIPTION)
plugin_ID = None
plugin_icon_resources = {}
try:
from calibre.gui2 import QVariant
del QVariant
except ImportError:
is_qt4 = False
convert_qvariant = lambda x: x
else:
is_qt4 = True
def convert_qvariant(x):
vt = x.type()
if vt == x.String:
return unicode(x.toString())
if vt == x.List:
return [convert_qvariant(i) for i in x.toList()]
return x.toPyObject()
BASE_TIME = None
def debug_print(*args):
global BASE_TIME
if BASE_TIME is None:
BASE_TIME = time.time()
if DEBUG:
prints('DEBUG: %6.1f'%(time.time()-BASE_TIME), *args)
try:
debug_print("obok::utilities.py - loading translations")
load_translations()
except NameError:
debug_print("obok::utilities.py - exception when loading translations")
pass # load_translations() added in calibre 1.9
def format_plural(number, possessive=False):
'''
Cosmetic ditty to provide the proper string formatting variable to handle singular/plural situations
:param: number: variable that represents the count/len of something
'''
if not possessive:
return '' if number == 1 else 's'
return '\'s' if number == 1 else 's\''
def set_plugin_icon_resources(name, resources):
'''
Set our global store of plugin name and icon resources for sharing between
the InterfaceAction class which reads them and the ConfigWidget
if needed for use on the customization dialog for this plugin.
'''
global plugin_icon_resources, plugin_ID
plugin_ID = name
plugin_icon_resources = resources
def get_icon(icon_name):
'''
Retrieve a QIcon for the named image from the zip file if it exists,
or if not then from Calibre's image cache.
'''
if icon_name:
pixmap = get_pixmap(icon_name)
if pixmap is None:
# Look in Calibre's cache for the icon
return QIcon(I(icon_name))
else:
return QIcon(pixmap)
return QIcon()
def get_pixmap(icon_name):
'''
Retrieve a QPixmap for the named image
Any icons belonging to the plugin must be prefixed with 'images/'
'''
if not icon_name.startswith('images/'):
# We know this is definitely not an icon belonging to this plugin
pixmap = QPixmap()
pixmap.load(I(icon_name))
return pixmap
# Check to see whether the icon exists as a Calibre resource
# This will enable skinning if the user stores icons within a folder like:
# ...\AppData\Roaming\calibre\resources\images\Plugin Name\
if plugin_ID:
local_images_dir = get_local_images_dir(plugin_ID)
local_image_path = os.path.join(local_images_dir, icon_name.replace('images/', ''))
if os.path.exists(local_image_path):
pixmap = QPixmap()
pixmap.load(local_image_path)
return pixmap
# As we did not find an icon elsewhere, look within our zip resources
if icon_name in plugin_icon_resources:
pixmap = QPixmap()
pixmap.loadFromData(plugin_icon_resources[icon_name])
return pixmap
return None
def get_local_images_dir(subfolder=None):
'''
Returns a path to the user's local resources/images folder
If a subfolder name parameter is specified, appends this to the path
'''
images_dir = os.path.join(config_dir, 'resources/images')
if subfolder:
images_dir = os.path.join(images_dir, subfolder)
if iswindows:
images_dir = os.path.normpath(images_dir)
return images_dir
def showErrorDlg(errmsg, parent, trcbk=False):
'''
Wrapper method for calibre's error_dialog
'''
if trcbk:
error= ''
f=StringIO()
print_exc(file=f)
error_mess = f.getvalue().splitlines()
for line in error_mess:
error = error + str(line) + '\n'
errmsg = errmsg + '\n\n' + error
return error_dialog(parent, _(PLUGIN_NAME + ' v' + PLUGIN_VERSION),
_(errmsg), show=True)
class SizePersistedDialog(QDialog):
'''
This dialog is a base class for any dialogs that want their size/position
restored when they are next opened.
'''
def __init__(self, parent, unique_pref_name):
QDialog.__init__(self, parent)
self.unique_pref_name = unique_pref_name
self.geom = gprefs.get(unique_pref_name, None)
self.finished.connect(self.dialog_closing)
def resize_dialog(self):
if self.geom is None:
self.resize(self.sizeHint())
else:
self.restoreGeometry(self.geom)
def dialog_closing(self, result):
geom = bytearray(self.saveGeometry())
gprefs[self.unique_pref_name] = geom
self.persist_custom_prefs()
def persist_custom_prefs(self):
'''
Invoked when the dialog is closing. Override this function to call
save_custom_pref() if you have a setting you want persisted that you can
retrieve in your __init__() using load_custom_pref() when next opened
'''
pass
def load_custom_pref(self, name, default=None):
return gprefs.get(self.unique_pref_name+':'+name, default)
def save_custom_pref(self, name, value):
gprefs[self.unique_pref_name+':'+name] = value
class ImageTitleLayout(QHBoxLayout):
'''
A reusable layout widget displaying an image followed by a title
'''
def __init__(self, parent, icon_name, title):
'''
:param parent: Parent gui
:param icon_name: Path to plugin image resource
:param title: String to be displayed beside the image
'''
QHBoxLayout.__init__(self)
self.title_image_label = QLabel(parent)
self.update_title_icon(icon_name)
self.addWidget(self.title_image_label)
title_font = QFont()
title_font.setPointSize(16)
shelf_label = QLabel(title, parent)
shelf_label.setFont(title_font)
self.addWidget(shelf_label)
self.insertStretch(-1)
def update_title_icon(self, icon_name):
pixmap = get_pixmap(icon_name)
if pixmap is None:
error_dialog(self.parent(), _('Restart required'),
_('Title image not found - you must restart Calibre before using this plugin!'), show=True)
else:
self.title_image_label.setPixmap(pixmap)
self.title_image_label.setMaximumSize(32, 32)
self.title_image_label.setScaledContents(True)
class ReadOnlyTableWidgetItem(QTableWidgetItem):
def __init__(self, text):
if text is None:
text = ''
QTableWidgetItem.__init__(self, text, QTableWidgetItem.UserType)
self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)

View File

@@ -0,0 +1,53 @@
obok_plugin.zip
================
This plugin will remove the DRM from Kobo ebooks download on Mac or Windows using the Kobo desktop application, or from Kobo ebooks on an attached E-Ink Kobo reader (but not a Kobo Arc or Kobo Vox). If both are available, ebooks will be read from the attached E-Ink Kobo reader. To import from the desktop application, unplug the Kobo reader.
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 and, in the folder "obok_calibre_plugin", find the file "obok_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
-------------
No customization is required, except choosing which menus will show the plugin. Altough the ability to enter a device serial number is given, this should not need to be filled in, as the serial number should be picked up automatically from the attached Kobo reader.
Using the plugin
----------------
Select the plugin's menu or icon from whichever part of the calibre interface you have chosen to have it. Follow the instructions in the dialog that appears.
Troubleshooting
---------------
If you find that it's 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 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:
On Windows, open a terminal/command window. (Start/Run… and then type 'cmd.exe' (without the 's) as the program to run).
On Macintosh, open the Terminal application (in your Utilities folder).
On Linux open a command window. Hopefully all Linux users know how to do this.
You should now have a text-based command-line window open.
Type in "calibre-debug -g" (without the "s but with the space before the -g) and press the return/enter key. Calibre will launch and run as normal, but with debugging information output to the terminal window.
Import the DRMed eBook into calibre in any of the the normal ways. (I usually drag&drop onto the calibre window.)
Debug information will be written to the terminal window.
Copy the output from the terminal window.
On Windows, you must use the window menu (little icon at left of window bar) to select all the text and then to copy it.
On Macintosh and Linux, just use the normal text select and copy commands.
Paste the information into a comment at my blog, http://apprenticealf.wordpress.com/ describing your problem.
Credits
-------
The original obok script was by Physisticated
The plugin conversion was done anonymously.
The Kobo reader support was added by norbusan
Additional improvements to the script and the plugin adaption by numerous anonymous people.

View File

@@ -0,0 +1,26 @@
// ==UserScript==
// @name BN-Dload
// @namespace http://www.mailinator.com/J-man
// @include https://mynook.barnesandnoble.com/library.html*
// @grant none
// @version 20121119
// ==/UserScript==
function doIt() {
if ($('#adl1').length == 0) {
$('[action$="deleteItem"]').each(function(index) {
if ($(this).parent().find('[action$="EDSDeliverItem.aspx"]').length == 0) {
var delid = $(this).find('input').attr('value');
$(this).after('<span class="vb2"></span><form id="adl' + index + '" action="https://edelivery.barnesandnoble.com/EDS/EDSDeliverItem.aspx" class="download"><input value="' + delid + '" type="hidden" name="delid"><input type="hidden" value="Browser" name="clienttype"><input type="hidden" value="browser" name="deviceinfo"><button class="download "name="download">Alternative Download</button></form>');
}
});
}
setTimeout (function() {
doIt();
}, 3000 );
}
doIt();

View File

@@ -0,0 +1,32 @@
INTRODUCTION
============
To obtain unencrypted content from the B&N, you have to download it directly from the website. Unrooted Nook devices will not let you save your content to your PC.
If the downloaded file is encrypted, install and configure the ignoble plugin in Calibre to decrypt that.
DOWNLOAD HIDDEN FILES FROM B&N
------------------------------
Some content is not downloadable from the B&N website, notably magazines. A Greasemonkey script (details below) modifies the myNook page of the Barnes and Noble website to show a download button for normally non-downloadable content. This will work until Barnes & Noble changes their website.
Prerequisites
-------------
1) Firefox: http://www.getfirefox.com
2) Greasemokey extension: https://addons.mozilla.org/nl/firefox/addon/greasemonkey/
One time installation
---------------------
1) Install Firefox if not already done so
2) Follow the above link to GreaseMonkey and click Add to Firefox
3) Restart Firefox
4) Go to http://userscripts.org/scripts/source/152985.user.js
5) A popup should appear, stating you are about to install a GreaseMonkey user script.
6) Click on install
Use
---
1) Log in into your B&N account
2) Go to MyNook
3) An “Alternative download” should appear next to normally non-downloadable content. Note that this will not work for content such as Nook applications, and some children books.

View File

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

View File

@@ -0,0 +1,336 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import with_statement
# ignoblekey.py
# Copyright © 2015 Apprentice Alf and Apprentice Harper
# 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
"""
Get Barnes & Noble EPUB user key from nook Studio log file
"""
__license__ = 'GPL v3'
__version__ = "1.1"
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,unicode):
data = data.encode(self.encoding,"replace")
self.stream.write(data)
self.stream.flush()
def __getattr__(self, attr):
return getattr(self.stream, attr)
try:
from calibre.constants import iswindows, isosx
except:
iswindows = sys.platform.startswith('win')
isosx = sys.platform.startswith('darwin')
def unicode_argv():
if iswindows:
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
# strings.
# Versions 2.x of Python don't support Unicode in sys.argv on
# Windows, with the underlying Windows API instead replacing multi-byte
# characters with '?'. So use shell32.GetCommandLineArgvW to get sys.argv
# as a list of Unicode strings and encode them as utf-8
from ctypes import POINTER, byref, cdll, c_int, windll
from ctypes.wintypes import LPCWSTR, LPWSTR
GetCommandLineW = cdll.kernel32.GetCommandLineW
GetCommandLineW.argtypes = []
GetCommandLineW.restype = LPCWSTR
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
CommandLineToArgvW.restype = POINTER(LPWSTR)
cmd = GetCommandLineW()
argc = c_int(0)
argv = CommandLineToArgvW(cmd, byref(argc))
if argc.value > 0:
# Remove Python executable and commands if present
start = argc.value - len(sys.argv)
return [argv[i] for i in
xrange(start, argc.value)]
# if we don't have any arguments at all, just pass back script name
# this should never happen
return [u"ignoblekey.py"]
else:
argvencoding = sys.stdin.encoding
if argvencoding == None:
argvencoding = "utf-8"
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
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 as winreg
# some 64 bit machines do not have the proper registry key for some reason
# or the python interface to the 32 vs 64 bit registry is broken
paths = set()
if 'LOCALAPPDATA' in os.environ.keys():
# Python 2.x does not return unicode env. Use Python 3.x
path = winreg.ExpandEnvironmentStrings(u"%LOCALAPPDATA%")
if os.path.isdir(path):
paths.add(path)
if 'USERPROFILE' in os.environ.keys():
# Python 2.x does not return unicode env. Use Python 3.x
path = winreg.ExpandEnvironmentStrings(u"%USERPROFILE%")+u"\\AppData\\Local"
if os.path.isdir(path):
paths.add(path)
path = winreg.ExpandEnvironmentStrings(u"%USERPROFILE%")+u"\\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 u"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 file(outfile, 'w') as keyfileout:
keyfileout.write(keys[-1])
print u"Saved a key to {0}".format(outfile)
else:
keycount = 0
for key in keys:
while True:
keycount += 1
outfile = os.path.join(outpath,u"nookkey{0:d}.b64".format(keycount))
if not os.path.exists(outfile):
break
with file(outfile, 'w') as keyfileout:
keyfileout.write(key)
print u"Saved a key to {0}".format(outfile)
return True
return False
def usage(progname):
print u"Finds the nook Study encryption keys."
print u"Keys are saved to the current directory, or a specified output directory."
print u"If a file name is passed instead of a directory, only the first key is saved, in that file."
print u"Usage:"
print u" {0:s} [-h] [-k <logFile>] [<outpath>]".format(progname)
def cli_main():
sys.stdout=SafeUnbuffered(sys.stdout)
sys.stderr=SafeUnbuffered(sys.stderr)
argv=unicode_argv()
progname = os.path.basename(argv[0])
print u"{0} v{1}\nCopyright © 2015 Apprentice Alf".format(progname,__version__)
try:
opts, args = getopt.getopt(argv[1:], "hk:")
except getopt.GetoptError, err:
print u"Error in options or arguments: {0}".format(err.args[0])
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 u"Could not retrieve nook Study key."
return 0
def gui_main():
try:
import Tkinter
import Tkconstants
import tkMessageBox
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=u"Unexpected error:",
anchor=Tkconstants.W, justify=Tkconstants.LEFT)
label.pack(fill=Tkconstants.X, expand=0)
self.text = Tkinter.Text(self)
self.text.pack(fill=Tkconstants.BOTH, expand=1)
self.text.insert(Tkconstants.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,u"nookkey{0:d}.b64".format(keycount))
if not os.path.exists(outfile):
break
with file(outfile, 'w') as keyfileout:
keyfileout.write(key)
success = True
tkMessageBox.showinfo(progname, u"Key successfully retrieved to {0}".format(outfile))
except DrmException, e:
tkMessageBox.showerror(progname, u"Error: {0}".format(str(e)))
except Exception:
root.wm_state('normal')
root.title(progname)
text = traceback.format_exc()
ExceptionDialog(root, text).pack(fill=Tkconstants.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())

View File

@@ -0,0 +1,258 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import with_statement
# ignoblekeyfetch.pyw, version 1.1
# Copyright © 2015 Apprentice Harper
# 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
"""
Fetch Barnes & Noble EPUB user key from B&N servers using email and password
"""
__license__ = 'GPL v3'
__version__ = "1.1"
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,unicode):
data = data.encode(self.encoding,"replace")
self.stream.write(data)
self.stream.flush()
def __getattr__(self, attr):
return getattr(self.stream, attr)
try:
from calibre.constants import iswindows, isosx
except:
iswindows = sys.platform.startswith('win')
isosx = sys.platform.startswith('darwin')
def unicode_argv():
if iswindows:
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
# strings.
# Versions 2.x of Python don't support Unicode in sys.argv on
# Windows, with the underlying Windows API instead replacing multi-byte
# characters with '?'. So use shell32.GetCommandLineArgvW to get sys.argv
# as a list of Unicode strings and encode them as utf-8
from ctypes import POINTER, byref, cdll, c_int, windll
from ctypes.wintypes import LPCWSTR, LPWSTR
GetCommandLineW = cdll.kernel32.GetCommandLineW
GetCommandLineW.argtypes = []
GetCommandLineW.restype = LPCWSTR
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
CommandLineToArgvW.restype = POINTER(LPWSTR)
cmd = GetCommandLineW()
argc = c_int(0)
argv = CommandLineToArgvW(cmd, byref(argc))
if argc.value > 0:
# Remove Python executable and commands if present
start = argc.value - len(sys.argv)
return [argv[i] for i in
xrange(start, argc.value)]
# if we don't have any arguments at all, just pass back script name
# this should never happen
return [u"ignoblekeyfetch.py"]
else:
argvencoding = sys.stdin.encoding
if argvencoding == None:
argvencoding = "utf-8"
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
class IGNOBLEError(Exception):
pass
def fetch_key(email, password):
# change email and password to utf-8 if unicode
if type(email)==unicode:
email = email.encode('utf-8')
if type(password)==unicode:
password = password.encode('utf-8')
import random
random = "%030x" % random.randrange(16**30)
import urllib, urllib2, re
# try the URL from nook for PC
fetch_url = "https://cart4.barnesandnoble.com/services/service.aspx?Version=2&acctPassword="
fetch_url += urllib.quote(password,'')+"&devID=PC_BN_2.5.6.9575_"+random+"&emailAddress="
fetch_url += urllib.quote(email,"")+"&outFormat=5&schema=1&service=1&stage=deviceHashB"
#print fetch_url
found = ''
try:
req = urllib2.Request(fetch_url)
response = urllib2.urlopen(req)
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.quote(password,'')+"&devID=hobbes_9.3.50818_"+random+"&emailAddress="
fetch_url += urllib.quote(email,"")+"&outFormat=5&schema=1&service=1&stage=deviceHashB"
#print fetch_url
found = ''
try:
req = urllib2.Request(fetch_url)
response = urllib2.urlopen(req)
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 u"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 u"Failed to fetch key."
return 1
def gui_main():
try:
import Tkinter
import tkFileDialog
import Tkconstants
import tkMessageBox
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=u"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=u"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=u"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=u"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, u"bnepubkey.b64")
button = Tkinter.Button(body, text=u"...", command=self.get_keypath)
button.grid(row=2, column=2)
buttons = Tkinter.Frame(self)
buttons.pack()
botton = Tkinter.Button(
buttons, text=u"Fetch", width=10, command=self.generate)
botton.pack(side=Tkconstants.LEFT)
Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT)
button = Tkinter.Button(
buttons, text=u"Quit", width=10, command=self.quit)
button.pack(side=Tkconstants.RIGHT)
def get_keypath(self):
keypath = tkFileDialog.asksaveasfilename(
parent=None, title=u"Select B&N ePub key file to produce",
defaultextension=u".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):
email = self.name.get()
password = self.ccn.get()
keypath = self.keypath.get()
if not email:
self.status['text'] = u"Email address not given"
return
if not password:
self.status['text'] = u"Account password not given"
return
if not keypath:
self.status['text'] = u"Output keyfile path not set"
return
self.status['text'] = u"Fetching..."
try:
userkey = fetch_key(email, password)
except Exception, e:
self.status['text'] = u"Error: {0}".format(e.args[0])
return
if len(userkey) == 28:
open(keypath,'wb').write(userkey)
self.status['text'] = u"Keyfile fetched successfully"
else:
self.status['text'] = u"Keyfile fetch failed."
root = Tkinter.Tk()
root.title(u"Barnes & Noble ePub Keyfile Fetch v.{0}".format(__version__))
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())

View File

@@ -0,0 +1,332 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import with_statement
# ignoblekeygen.pyw, version 2.5
# Copyright © 2009-2010 i♥cabbages
# Released under the terms of the GNU General Public Licence, version 3
# <http://www.gnu.org/licenses/>
# Modified 20102013 by some_updates, DiapDealer and Apprentice Alf
# Windows users: Before running this program, you must first install Python.
# 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)
"""
Generate Barnes & Noble EPUB user key from name and credit card number.
"""
__license__ = 'GPL v3'
__version__ = "2.8"
import sys
import os
import hashlib
# Wrap a stream so that output gets flushed immediately
# and also make sure that any unicode strings get
# encoded using "replace" before writing them.
class SafeUnbuffered:
def __init__(self, stream):
self.stream = stream
self.encoding = stream.encoding
if self.encoding == None:
self.encoding = "utf-8"
def write(self, data):
if isinstance(data,unicode):
data = data.encode(self.encoding,"replace")
self.stream.write(data)
self.stream.flush()
def __getattr__(self, attr):
return getattr(self.stream, attr)
try:
from calibre.constants import iswindows, isosx
except:
iswindows = sys.platform.startswith('win')
isosx = sys.platform.startswith('darwin')
def unicode_argv():
if iswindows:
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
# strings.
# Versions 2.x of Python don't support Unicode in sys.argv on
# Windows, with the underlying Windows API instead replacing multi-byte
# characters with '?'. So use shell32.GetCommandLineArgvW to get sys.argv
# as a list of Unicode strings and encode them as utf-8
from ctypes import POINTER, byref, cdll, c_int, windll
from ctypes.wintypes import LPCWSTR, LPWSTR
GetCommandLineW = cdll.kernel32.GetCommandLineW
GetCommandLineW.argtypes = []
GetCommandLineW.restype = LPCWSTR
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
CommandLineToArgvW.restype = POINTER(LPWSTR)
cmd = GetCommandLineW()
argc = c_int(0)
argv = CommandLineToArgvW(cmd, byref(argc))
if argc.value > 0:
# Remove Python executable and commands if present
start = argc.value - len(sys.argv)
return [argv[i] for i in
xrange(start, argc.value)]
# if we don't have any arguments at all, just pass back script name
# this should never happen
return [u"ignoblekeygen.py"]
else:
argvencoding = sys.stdin.encoding
if argvencoding == None:
argvencoding = "utf-8"
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
class 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.
if type(name)==unicode:
name = name.encode('utf-8')
if type(ccn)==unicode:
ccn = ccn.encode('utf-8')
name = normalize_name(name) + '\x00'
ccn = normalize_name(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(ccn_sha, name_sha)
crypt = aes.encrypt(both_sha + ('\x0c' * 0x0c))
userkey = hashlib.sha1(crypt).digest()
return userkey.encode('base64')
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 u"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 Tkconstants
import tkMessageBox
import tkFileDialog
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=u"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=u"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=u"CC#").grid(row=1)
self.ccn = Tkinter.Entry(body, width=40)
self.ccn.grid(row=1, column=1, sticky=sticky)
Tkinter.Label(body, text=u"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, u"bnepubkey.b64")
button = Tkinter.Button(body, text=u"...", command=self.get_keypath)
button.grid(row=2, column=2)
buttons = Tkinter.Frame(self)
buttons.pack()
botton = Tkinter.Button(
buttons, text=u"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=u"Quit", width=10, command=self.quit)
button.pack(side=Tkconstants.RIGHT)
def get_keypath(self):
keypath = tkFileDialog.asksaveasfilename(
parent=None, title=u"Select B&N ePub key file to produce",
defaultextension=u".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'] = u"Name not specified"
return
if not ccn:
self.status['text'] = u"Credit card number not specified"
return
if not keypath:
self.status['text'] = u"Output keyfile path not specified"
return
self.status['text'] = u"Generating..."
try:
userkey = generate_key(name, ccn)
except Exception, e:
self.status['text'] = u"Error: (0}".format(e.args[0])
return
open(keypath,'wb').write(userkey)
self.status['text'] = u"Keyfile successfully generated"
root = Tkinter.Tk()
if AES is None:
root.withdraw()
tkMessageBox.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(u"Barnes & Noble ePub Keyfile Generator v.{0}".format(__version__))
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())

View File

@@ -0,0 +1,468 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import with_statement
# androidkindlekey.py
# Copyright © 2013-15 by Thom and Apprentice Harper
# Some portions Copyright © 2010-15 by some_updates and Apprentice Alf
#
# 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
"""
Retrieve Kindle for Android Serial Number.
"""
__license__ = 'GPL v3'
__version__ = '1.5'
import os
import sys
import traceback
import getopt
import tempfile
import zlib
import tarfile
from hashlib import md5
from cStringIO import StringIO
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,unicode):
data = data.encode(self.encoding,"replace")
self.stream.write(data)
self.stream.flush()
def __getattr__(self, attr):
return getattr(self.stream, attr)
try:
from calibre.constants import iswindows, isosx
except:
iswindows = sys.platform.startswith('win')
isosx = sys.platform.startswith('darwin')
def unicode_argv():
if iswindows:
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
# strings.
# Versions 2.x of Python don't support Unicode in sys.argv on
# Windows, with the underlying Windows API instead replacing multi-byte
# characters with '?'. So use shell32.GetCommandLineArgvW to get sys.argv
# as a list of Unicode strings and encode them as utf-8
from ctypes import POINTER, byref, cdll, c_int, windll
from ctypes.wintypes import LPCWSTR, LPWSTR
GetCommandLineW = cdll.kernel32.GetCommandLineW
GetCommandLineW.argtypes = []
GetCommandLineW.restype = LPCWSTR
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
CommandLineToArgvW.restype = POINTER(LPWSTR)
cmd = GetCommandLineW()
argc = c_int(0)
argv = CommandLineToArgvW(cmd, byref(argc))
if argc.value > 0:
# Remove Python executable and commands if present
start = argc.value - len(sys.argv)
return [argv[i] for i in
xrange(start, argc.value)]
# if we don't have any arguments at all, just pass back script name
# this should never happen
return [u"kindlekey.py"]
else:
argvencoding = sys.stdin.encoding
if argvencoding == None:
argvencoding = "utf-8"
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
class DrmException(Exception):
pass
STORAGE = u"backup.ab"
STORAGE1 = u"AmazonSecureStorage.xml"
STORAGE2 = u"map_data_storage.db"
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))
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 = '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:
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 userdata_value from userdata where userdata_key like '%/%token.device.deviceserialname%' ''')
userdata_keys = cursor.fetchall()
dsns = []
for userdata_row in userdata_keys:
try:
if userdata_row and userdata_row[0]:
userdata_utf8 = userdata_row[0].encode('utf8')
if len(userdata_utf8) > 0:
dsns.append(userdata_utf8)
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]:
userdata_utf8 = userdata_row[0].encode('utf8')
if len(userdata_utf8) > 0:
tokens.append(userdata_utf8)
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('%s%s' % (x, y))
for y in tokens:
serials.append(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] == 'ANDROID BACKUP':
output = StringIO(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 file(outfile, 'w') as keyfileout:
for key in keys:
keyfileout.write(key)
keyfileout.write("\n")
return True
return False
def usage(progname):
print u"Decrypts the serial number(s) of Kindle For Android from Android backup or file"
print u"Get backup.ab file using adb backup com.amazon.kindle for Android 4.0+."
print u"Otherwise extract AmazonSecureStorage.xml from /data/data/com.amazon.kindle/shared_prefs/AmazonSecureStorage.xml"
print u"Or map_data_storage.db from /data/data/com.amazon.kindle/databases/map_data_storage.db"
print u""
print u"Usage:"
print u" {0:s} [-h] [-b <backup.ab>] [<outfile.k4a>]".format(progname)
def cli_main():
sys.stdout=SafeUnbuffered(sys.stdout)
sys.stderr=SafeUnbuffered(sys.stderr)
argv=unicode_argv()
progname = os.path.basename(argv[0])
print u"{0} v{1}\nCopyright © 2010-2015 Thom, some_updates, Apprentice Alf and Apprentice Harper".format(progname,__version__)
try:
opts, args = getopt.getopt(argv[1:], "hb:")
except getopt.GetoptError, err:
usage(progname)
print u"\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 u"\n{0:s} file not found".format(inpath)
return 2
if getkey(outfile, inpath):
print u"\nSaved Kindle for Android key to {0}".format(outfile)
else:
print u"\nCould not retrieve Kindle for Android key."
return 0
def gui_main():
try:
import Tkinter
import Tkconstants
import tkMessageBox
import tkFileDialog
except:
print "Tkinter not installed"
return cli_main()
class DecryptionDialog(Tkinter.Frame):
def __init__(self, root):
Tkinter.Frame.__init__(self, root, border=5)
self.status = Tkinter.Label(self, text=u"Select backup.ab file")
self.status.pack(fill=Tkconstants.X, expand=1)
body = Tkinter.Frame(self)
body.pack(fill=Tkconstants.X, expand=1)
sticky = Tkconstants.E + Tkconstants.W
body.grid_columnconfigure(1, weight=2)
Tkinter.Label(body, text=u"Backup file").grid(row=0, column=0)
self.keypath = Tkinter.Entry(body, width=40)
self.keypath.grid(row=0, column=1, sticky=sticky)
self.keypath.insert(2, u"backup.ab")
button = Tkinter.Button(body, text=u"...", command=self.get_keypath)
button.grid(row=0, column=2)
buttons = Tkinter.Frame(self)
buttons.pack()
button2 = Tkinter.Button(
buttons, text=u"Extract", width=10, command=self.generate)
button2.pack(side=Tkconstants.LEFT)
Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT)
button3 = Tkinter.Button(
buttons, text=u"Quit", width=10, command=self.quit)
button3.pack(side=Tkconstants.RIGHT)
def get_keypath(self):
keypath = tkFileDialog.askopenfilename(
parent=None, title=u"Select backup.ab file",
defaultextension=u".ab",
filetypes=[('adb backup com.amazon.kindle', '.ab'),
('All Files', '.*')])
if keypath:
keypath = os.path.normpath(keypath)
self.keypath.delete(0, Tkconstants.END)
self.keypath.insert(0, keypath)
return
def generate(self):
inpath = self.keypath.get()
self.status['text'] = u"Getting key..."
try:
keys = get_serials(inpath)
keycount = 0
for key in keys:
while True:
keycount += 1
outfile = os.path.join(progpath,u"kindlekey{0:d}.k4a".format(keycount))
if not os.path.exists(outfile):
break
with file(outfile, 'w') as keyfileout:
keyfileout.write(key)
success = True
tkMessageBox.showinfo(progname, u"Key successfully retrieved to {0}".format(outfile))
except Exception, e:
self.status['text'] = u"Error: {0}".format(e.args[0])
return
self.status['text'] = u"Select backup.ab file"
argv=unicode_argv()
progpath, progname = os.path.split(argv[0])
root = Tkinter.Tk()
root.title(u"Kindle for Android Key Extraction v.{0}".format(__version__))
root.resizable(True, False)
root.minsize(300, 0)
DecryptionDialog(root).pack(fill=Tkconstants.X, expand=1)
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

View File

@@ -0,0 +1,275 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import with_statement
# kindleforios4key.py
# Copyright © 2013 by Apprentice Alf
# Portions Copyright © 2007, 2009 Igor Skochinsky <skochinsky@mail.ru>
# Revision history:
# 1.0 - Generates fixed PID for Kindle for iOS 3.1.1 running on iOS 4.x
"""
Generate fixed PID for Kindle for iOS 3.1.1
"""
__license__ = 'GPL v3'
__version__ = '1.0'
import sys, os
import getopt
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,unicode):
data = data.encode(self.encoding,"replace")
self.stream.write(data)
self.stream.flush()
def __getattr__(self, attr):
return getattr(self.stream, attr)
iswindows = sys.platform.startswith('win')
isosx = sys.platform.startswith('darwin')
def unicode_argv():
if iswindows:
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
# strings.
# Versions 2.x of Python don't support Unicode in sys.argv on
# Windows, with the underlying Windows API instead replacing multi-byte
# characters with '?'.
from ctypes import POINTER, byref, cdll, c_int, windll
from ctypes.wintypes import LPCWSTR, LPWSTR
GetCommandLineW = cdll.kernel32.GetCommandLineW
GetCommandLineW.argtypes = []
GetCommandLineW.restype = LPCWSTR
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
CommandLineToArgvW.restype = POINTER(LPWSTR)
cmd = GetCommandLineW()
argc = c_int(0)
argv = CommandLineToArgvW(cmd, byref(argc))
if argc.value > 0:
# Remove Python executable and commands if present
start = argc.value - len(sys.argv)
return [argv[i] for i in
xrange(start, argc.value)]
# if we don't have any arguments at all, just pass back script name
# this should never happen
return [u"mobidedrm.py"]
else:
argvencoding = sys.stdin.encoding
if argvencoding == None:
argvencoding = "utf-8"
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
import hashlib
def SHA256(message):
ctx = hashlib.sha256()
ctx.update(message)
return ctx.digest()
def crc32(s):
return (~binascii.crc32(s,-1))&0xFFFFFFFF
letters = 'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789'
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 generatekeys(email, mac):
keys = []
email = email.encode('utf-8').lower()
mac = mac.encode('utf-8').lower()
cleanmac = "".join(c if (c in "0123456789abcdef") else "" for c in mac)
lowermac = cleanmac.lower()
#print lowermac
keyseed = lowermac + email.encode('utf-8')
#print keyseed
keysha256 = SHA256(keyseed)
keybase64 = keysha256.encode('base64')
#print keybase64
cleankeybase64 = "".join(c if (c in "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") else "0" for c in keybase64)
#print cleankeybase64
pseudoudid = cleankeybase64[:40]
#print pseudoudid
keys.append(pidFromSerial(pseudoudid.encode("utf-8"),8))
return keys
# interface for Python DeDRM
# returns single key or multiple keys, depending on path or file passed in
def getkey(email, mac, outpath):
keys = generatekeys(email,mac)
if len(keys) > 0:
if not os.path.isdir(outpath):
outfile = outpath
with file(outfile, 'w') as keyfileout:
keyfileout.write(keys[0])
print u"Saved a key to {0}".format(outfile)
else:
keycount = 0
for key in keys:
while True:
keycount += 1
outfile = os.path.join(outpath,u"kindleios{0:d}.pid".format(keycount))
if not os.path.exists(outfile):
break
with file(outfile, 'w') as keyfileout:
keyfileout.write(key)
print u"Saved a key to {0}".format(outfile)
return True
return False
def usage(progname):
print u"Generates the key for Kindle for iOS 3.1.1"
print u"Requires email address of Amazon acccount"
print u"And MAC address for iOS devices wifi"
print u"Outputs to a file or to stdout"
print u"Usage:"
print u" {0:s} [-h] <email address> <MAC address> [<outfile>]".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 u"{0} v{1}\nCopyright © 2013 Apprentice Alf".format(progname,__version__)
try:
opts, args = getopt.getopt(argv[1:], "h")
except getopt.GetoptError, err:
print u"Error in options or arguments: {0}".format(err.args[0])
usage(progname)
sys.exit(2)
for o, a in opts:
if o == "-h":
usage(progname)
sys.exit(0)
if len(args) < 2 or len(args) > 3:
usage(progname)
sys.exit(2)
if len(args) == 3:
# save to the specified file or folder
getkey(args[0],args[1],args[2])
else:
keys = generatekeys(args[0],args[1])
for key in keys:
print key
return 0
def gui_main():
try:
import Tkinter
import Tkconstants
import tkMessageBox
except:
print "Tkinter not installed"
return cli_main()
class DecryptionDialog(Tkinter.Frame):
def __init__(self, root):
Tkinter.Frame.__init__(self, root, border=5)
self.status = Tkinter.Label(self, text=u"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=u"Amazon email address").grid(row=0)
self.email = Tkinter.Entry(body, width=40)
self.email.grid(row=0, column=1, sticky=sticky)
Tkinter.Label(body, text=u"iOS MAC address").grid(row=1)
self.mac = Tkinter.Entry(body, width=40)
self.mac.grid(row=1, column=1, sticky=sticky)
buttons = Tkinter.Frame(self)
buttons.pack()
button = Tkinter.Button(
buttons, text=u"Generate", width=10, command=self.generate)
button.pack(side=Tkconstants.LEFT)
Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT)
button = Tkinter.Button(
buttons, text=u"Quit", width=10, command=self.quit)
button.pack(side=Tkconstants.RIGHT)
def generate(self):
email = self.email.get()
mac = self.mac.get()
if not email:
self.status['text'] = u"Email not specified"
return
if not mac:
self.status['text'] = u"MAC not specified"
return
self.status['text'] = u"Generating..."
try:
keys = generatekeys(email, mac)
except Exception, e:
self.status['text'] = u"Error: (0}".format(e.args[0])
return
self.status['text'] = ", ".join(key for key in keys)
root = Tkinter.Tk()
root.title(u"Kindle for iOS PID Generator v.{0}".format(__version__))
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())

View File

@@ -0,0 +1,50 @@
Of Historical Interest Only
===========================
It is now much simpler and easier to get a backup.ab file from your Android device and import that into the tools.
Comment at Apprentice Alf's Blog by cestmoicestmoi, 21st December, 2012.
========================================================================
just to share my experience in patching the kindle.apk
first thanks to all of you guys for this great site & tools, keep going
following instructions in latest readme.txt
successfully patched Android Kindle 3.7.0.108, Android 2.3.4 Xperia Arc unrooted;
key painful snags encountered in my case (+ had never coded java/built any apk! .apk = app on Android) (just sums up here the tip of the iceberg):
on phone :
to get the app kindle.apk (invisible in unrooted phones) :
use MyAppShare (free), but this required old Kindle.apk version (for patching) wont probably be in your phone anymore (latest today 3.8.1.11, with new updates almost everyday !)
anyway in any-case turnoff the Play-Store auto update
otherwise/then look for a 3.7.0.108 on the web (or try patch a newer one ?)
reminder: .apk are just containers = zip files… to check just add an extra .zip and unzip kindle.apk, but dont play with that… patching wouldnt work; however all names of kindle.apk can be completely freely changed : kindle2patchedJojo.apk, com.amazon.kindle.apk, etc.
on your PC (XP):
rather long & tricky expert process for any non-android-app maker
(basics is to use the cmd.exe very old DOS style command line with the tricky path and directories changes, forward and back slash etc etc !)
pb to obtain all the necessary .exe tools (then you call them as a command in cdm.exe)
to make things easier, put copy of everything in same folder anywhere:
kindle.apk, cmd.exe (from XP), apktool.jar+apktool.bat+aapt.exe (from http://code.google.com/p/android-apktool/), patch.exe (from http://gnuwin32.sourceforge.net/packages/patch.htm), keytool.exe+jarsign.exe(from http://www.oracle.com/technetwork/java/javase/downloads/ download JDK+JRE same recent version, in my case v6038; start with keytool then jarsign; keep these .exe in their java/…/bin files, make shortcuts, copy them in your “work” folder, then do the path thing in cmd.exe in just copying the paths in the properties of the shortcuts)
then, key points:
download/use apktool.jar v143 not v150 (version seen in calling from cdm.exe)
do not “apktool if…” (frameworks)
besides all cmd.exe commands given in the readme.txt (read both .txt the old and the newer, even if same name) are perfectly ok (except in keytool: a typo: -valkeidity is -validity; and -genkey preferably -genkeypair)
(for the tricky keytool then jarsign stuff better read for ex. https://www.owasp.org/index.php/Signing_jar_files_with_jarsigner)
(almost1) finally move your patched & signed new kindle.apk anywhere to phone sd card, and then from within android just like possibly for any .apk move to it with any file manager and click it, it installs by itself (mine didnt want to install over the non-patched kindle app, so I had to desinstall the latter 1st from within the settings, but its probably because my phone ram was full !)
(almost2) finally you get your famous PIDs at the bottom of the info in the kindle.apk: dont worry, in my test case (just 1 book yet), there were 11 PIDs, but 5 were redundant, and the last is a weird large neg number -obviously not a 10 max char. PID, so forget it-).
then thanks to Android 2.3.4 now you can screenshot (press power button then back button), then from my PC i got this screen pict with all these PIDs, which I even OCRised (stupidly without checking… so I mixed up some “O” and “0″ etc !),
(almost3) finally then I implemented these PIDs in deDRM v541, …and it didnt work, long error message, but, finally, at last, worked in deDRM v531 !
conclusion:
the idea and work behind this patch is brilliant… but I have the bad feeling that most of this/my rather crazy work above to do myself the whole patching thing was more for glory, like climbing some kind of Everest without any training, than anything else…
must precise that I have only an old mac-ppc + this Android phone; ppc has no kindle.app available*; ppc has no java 1.6 so no possible apktool etc. (nor eclipse Android plugin etc.) (possible with very old versions ?), luckily I had an old version of VPC(virtualpc)/XP (extremely slow!), so I could try all this java stuff at least in a normal XP window/virtual machine, on my mac-ppc; i didnt try any XP-Android link through USB or else, had only my usual Mac-Android wifi ftp (great direct dragn drop with cyberduck).
But when i see all these/my efforts described above for a non-specialist (like 1 week work vs. 1-2 hours for you apk developers !), I wonder if this patched apk shouldnt/couldnt be made directly available for most other average joes, no ? just like deDRM ? (or maybe i dont see the problems here ?);
besides, i didnt check, but i have the strong feeling that in using kindle directly on XP to buy my books, this great app deDRM would have found directly these PIDs or other keys automatically (as it does for me on my mac-ppc for some ebook purchased and downloaded directly from other vendors; i.e. “stupid me”… on XP, the deDRM app would have worked in 1 sec… vs. 1 week work above for patching Android, + the crazy procedure just to get these pids, screenshot + ocr + etc. ! well, it should be much faster now, of course !).
*reminder: there is a solution for a mac-ppc only guy who want to buy & read Kindle books for very cheap, and accept to read them online in a browser : little advertised “Kindle cloud reader” which used to work even with rather old Safari versions (didnt check recently if it still works)… (looks a lot like the Google play book reader) texts are very little protected/not encrypted there apparently (journalists even said at launch 2-3 years ago that Amazon was abandoning DRMs, just like Apple did with music a while ago) : texts can more or less easily be copied piece by piece/ by page with a few astute clicks (same for Google; but formatting is gone; and who want to recopy a 300 p. book page by page !); tried to reverse engineer a bit for fun this browser reader to see where and how the text was stored, etc. but (as mentioned I think by another in another comment) the text is downloaded only by pieces, not all at once (dont remember what if browser turned “offline” ?), and stored here and there, in caches… maybe there are tools for capturing auto these books sent to browsers ? didnt check ? (not for my mac-ppc at the time in any case; but possibly Windows).

View File

@@ -0,0 +1,50 @@
Kindle for Android
------------------
Kindle for Android uses a different scheme to generate its books specific PIDs than Kindle for PC, Kindle for iPhone/iPad, and standalone Kindles.
Unfortunately, K4Android uses an "account secrets" file that would only be available if the device were jail-broken and even then someone would have to figure out how to decode this secret information in order to reverse the process.
Instead of trying to calculate the correct PIDs for each book from this primary data, "Me" (a commenter who posted to the ApprenticeAlf site) came up with a wonderful idea to simply modify the Kindle 3 for Android application to store the PIDs it uses to decode each book in its "about activity" window. This list of PIDS can then be provided to MobiDeDRM.py, which in its latest incarnations allows a comma separated list of pids to be passed in, to successfully remove the DRM from that book. Effectively "Me" has created an "Unswindle" for the Kindle for Android 3 application!
Obviously, to use "Me"'s approach, requires an Android Developer's Certificate (to sign the modified application) and access to and knowledge of the developer tools, but does not require anything to be jail-broken.
This is a copy the detailed instructions supplied by "Me" to the ApprenticeAlf blog in the comments. The kindle3.patch described below is included in this folder in the tools:
From the ApprenticeAlf Comments:
"Me" writes:
A better solution seems to create a patched version of the Kindle apk which either logs or displays its PID. I created a patch to both log the pid list and show it in the Kindle application in the about activity screen. The pid list isnt available until the DRMed book has been opened (and the list seem to differ for different books).
To create the patched kindle apk a certificate must be created (http://developer.android.com/guide/publishing/app-signing.html#cert) and the apktool must be build from source (all subprojects) as long as version 1.4.2 isnt released (http://code.google.com/p/android-apktool/wiki/BuildApktool).
These are the steps to pull the original apk from the Android device, uninstall it, create a patched apk and install that (tested on a rooted device, but I think all steps should also work on non-rooted devices):
adb pull /data/app/com.amazon.kindle-1.apk kindle3.apk
adb uninstall com.amazon.kindle
apktool d kindle3.apk kindle3
cd kindle3
patch -p1 < ../kindle3.patch
cd ..
apktool b kindle3 kindle3_patched.apk
jarsigner -verbose -keystore kindle.keystore kindle3_patched.apk kindle
zipalign -v 4 kindle3_patched.apk kindle3_signed.apk
adb install kindle3_signed.apk
kindle3.patch (based on kindle version 3.0.1.70) is available on pastebin:
http://pastebin.com/LNpgkcpP
Have fun!
Comment by me — June 9, 2011 @ 9:01 pm | Reply
Hi me,
Wow! Great work!!!!
With your patch, you have created the equivalent of Unswindle for the Kindle for Android app and it does not even require jailbreaking!
Very nice work indeed!
Comment by some_updates — June 10, 2011 @ 4:28 am | Reply

View File

@@ -0,0 +1,100 @@
diff -ru kindle3_orig/smali/com/amazon/kcp/application/AndroidDeviceInformationProvider.smali kindle3/smali/com/amazon/kcp/application/AndroidDeviceInformationProvider.smali
--- kindle3_orig/smali/com/amazon/kcp/application/AndroidDeviceInformationProvider.smali
+++ kindle3/smali/com/amazon/kcp/application/AndroidDeviceInformationProvider.smali
@@ -11,6 +11,8 @@
.field private security:Lcom/mobipocket/android/library/reader/AndroidSecurity;
+.field private pidList:Ljava/lang/String;
+
# direct methods
.method public constructor <init>(Lcom/mobipocket/android/library/reader/AndroidSecurity;Lcom/amazon/kcp/application/AndroidDeviceType;)V
@@ -28,6 +30,10 @@
.line 26
iput-object p2, p0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->deviceType:Lcom/amazon/kcp/application/AndroidDeviceType;
+ const-string v0, "Open DRMed book to show PID list."
+
+ iput-object v0, p0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->pidList:Ljava/lang/String;
+
.line 27
new-instance v0, Ljava/lang/StringBuilder;
@@ -175,4 +181,26 @@
move-result-object v0
return-object v0
+.end method
+
+.method public getPidList()Ljava/lang/String;
+ .locals 1
+
+ .prologue
+ .line 15
+ iget-object v0, p0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->pidList:Ljava/lang/String;
+
+ return-object v0
+.end method
+
+.method public setPidList(Ljava/lang/String;)V
+ .locals 0
+ .parameter "value"
+
+ .prologue
+ .line 11
+ iput-object p1, p0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->pidList:Ljava/lang/String;
+
+ .line 12
+ return-void
.end method
diff -ru kindle3_orig/smali/com/amazon/kcp/application/IDeviceInformationProvider.smali kindle3/smali/com/amazon/kcp/application/IDeviceInformationProvider.smali
--- kindle3_orig/smali/com/amazon/kcp/application/IDeviceInformationProvider.smali
+++ kindle3/smali/com/amazon/kcp/application/IDeviceInformationProvider.smali
@@ -27,3 +27,9 @@
.method public abstract getPid()Ljava/lang/String;
.end method
+
+.method public abstract getPidList()Ljava/lang/String;
+.end method
+
+.method public abstract setPidList(Ljava/lang/String;)V
+.end method
\ No newline at end of file
diff -ru kindle3_orig/smali/com/amazon/kcp/info/AboutActivity.smali kindle3/smali/com/amazon/kcp/info/AboutActivity.smali
--- kindle3_orig/smali/com/amazon/kcp/info/AboutActivity.smali
+++ kindle3/smali/com/amazon/kcp/info/AboutActivity.smali
@@ -32,9 +32,11 @@
invoke-direct {v6, v1}, Ljava/util/ArrayList;-><init>(I)V
.line 36
- const v1, 0x7f0b0005
+ invoke-static {}, Lcom/amazon/kcp/application/DeviceInformationProviderFactory;->getProvider()Lcom/amazon/kcp/application/IDeviceInformationProvider;
- invoke-virtual {p0, v1}, Lcom/amazon/kcp/info/AboutActivity;->getString(I)Ljava/lang/String;
+ move-result-object v0
+
+ invoke-interface {v0}, Lcom/amazon/kcp/application/IDeviceInformationProvider;->getPidList()Ljava/lang/String;
move-result-object v1
diff -ru kindle3_orig/smali/com/amazon/system/security/Security.smali kindle3/smali/com/amazon/system/security/Security.smali
--- kindle3_orig/smali/com/amazon/system/security/Security.smali
+++ kindle3/smali/com/amazon/system/security/Security.smali
@@ -884,6 +884,15 @@
.line 332
:cond_1
+
+ const-string v1, "PID list"
+ invoke-static {}, Lcom/amazon/kcp/application/DeviceInformationProviderFactory;->getProvider()Lcom/amazon/kcp/application/IDeviceInformationProvider;
+ move-result-object v0
+ invoke-static {v7}, Ljava/util/Arrays;->toString([Ljava/lang/Object;)Ljava/lang/String;
+ move-result-object v2
+ invoke-interface {v0, v2}, Lcom/amazon/kcp/application/IDeviceInformationProvider;->setPidList(Ljava/lang/String;)V
+ invoke-static {v1, v2}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I
+
return-object v7
:cond_2

View File

@@ -0,0 +1,74 @@
Kindle for Android
------------------
Kindle for Android uses a different scheme to generate its books specific PIDs than Kindle for PC, Kindle for iPhone/iPad, and standalone Kindles.
Unfortunately, K4Android uses an "account secrets" file that would only be available if the device were jail-broken and even then someone would have to figure out how to decode this secret information in order to reverse the process.
Instead of trying to calculate the correct PIDs for each book from this primary data, "Me" (a commenter who posted to the ApprenticeAlf site) came up with a wonderful idea to simply modify the Kindle 3 for Android application to store the PIDs it uses to decode each book in its "about activity" window. This list of PIDS can then be provided to MobiDeDRM.py, which in its latest incarnations allows a comma separated list of pids to be passed in, to successfully remove the DRM from that book. Effectively "Me" has created an "Unswindle" for the Kindle for Android 3 application!
"Me"'s original patch was for Kindle for Android version 3.0.1.70. Now "Me II" has created a patch for Kindle for Android version 3.7.0.108 and new instructions, since some of the previous steps are no longer necessary.
From the ApprenticeAlf Comments:
"Me II" writes:
Since “Me”s old method for getting PIDs from Kindle for Android is outdated and no longer works with newer versions of the app, I decided Id take a stab at bringing it up to date. It took a little fiddling to get everything working, considering how much has changed since the last patch, but I managed to do it. The process is pretty much identical to “Me”s original instructions, with a few minor changes.
1) You dont need to build apktool from source. You can just grab the binaries from here for whatever OS youre running: http://code.google.com/p/android-apktool/
2) When you sign the rebuilt APK, use the following command instead of the one in the instructions:
jarsigner -verbose -sigalg MD5withRSA -digestalg SHA1 -keystore kindle.keystore kindle3_patched.apk kindle
3) It no longer logs the PIDs, only displays them within the app.
You can get the new patch, for version 3.7.0.108, here: http://pastebin.com/6FN2cTSN
And heres a screenshot of the updated menu: http://imgur.com/BbFVF (sorry for the Japanese, I was too lazy to change my phones language).
Subsequently, "s" wrote:
For others it would be useful to add the keystore generation command into the help file:
keytool -genkey -v -keystore kindle.keystore -alias kindle -keyalg RSA -keysize 2048 -validity 10000
As well as location of prcs on android being (with sdcard):
/mnt/sdcard/Android/data/com.amazon.kindle/files/
"s" also reported success with using the patch on version 3.7.1.8, although I recommend using the 3.7.0.108 version just in case.
"Me"'s original instructions, from the ApprenticeAlf Comments:
"Me" writes:
A better solution seems to create a patched version of the Kindle apk which either logs or displays its PID. I created a patch to both log the pid list and show it in the Kindle application in the about activity screen. The pid list isnt available until the DRMed book has been opened (and the list seem to differ for different books).
To create the patched kindle apk a certificate must be created (http://developer.android.com/guide/publishing/app-signing.html#cert) and the apktool must be build from source (all subprojects) as long as version 1.4.2 isnt released (http://code.google.com/p/android-apktool/wiki/BuildApktool).
These are the steps to pull the original apk from the Android device, uninstall it, create a patched apk and install that (tested on a rooted device, but I think all steps should also work on non-rooted devices):
adb pull /data/app/com.amazon.kindle-1.apk kindle3.apk
adb uninstall com.amazon.kindle
apktool d kindle3.apk kindle3
cd kindle3
patch -p1 < ../kindle3.patch
cd ..
apktool b kindle3 kindle3_patched.apk
jarsigner -verbose -keystore kindle.keystore kindle3_patched.apk kindle
zipalign -v 4 kindle3_patched.apk kindle3_signed.apk
adb install kindle3_signed.apk
kindle3.patch (based on kindle version 3.0.1.70) is available on pastebin:
http://pastebin.com/LNpgkcpP
Have fun!
Comment by me — June 9, 2011 @ 9:01 pm | Reply
Hi me,
Wow! Great work!!!!
With your patch, you have created the equivalent of Unswindle for the Kindle for Android app and it does not even require jailbreaking!
Very nice work indeed!
Comment by some_updates — June 10, 2011 @ 4:28 am | Reply

View File

@@ -0,0 +1,155 @@
diff -ru kindle3.7.0.108_orig/smali/com/amazon/kcp/application/AndroidDeviceInformationProvider.smali kindle3.7.0.108/smali/com/amazon/kcp/application/AndroidDeviceInformationProvider.smali
--- kindle3.7.0.108_orig/smali/com/amazon/kcp/application/AndroidDeviceInformationProvider.smali
+++ kindle3.7.0.108/smali/com/amazon/kcp/application/AndroidDeviceInformationProvider.smali
@@ -43,6 +43,8 @@
.field private security:Lcom/mobipocket/android/library/reader/AndroidSecurity;
+.field private pidList:Ljava/lang/String;
+
.field private totalMemory:J
@@ -78,6 +80,10 @@
.line 132
iput-object p2, p0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->deviceType:Lcom/amazon/kcp/application/AmazonDeviceType;
+
+ const-string v0, "Open DRMed book to show PID list."
+
+ iput-object v0, p0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->pidList:Ljava/lang/String;
.line 133
sget-object v0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->TAG:Ljava/lang/String;
@@ -1242,3 +1248,25 @@
return-wide v0
.end method
+
+.method public getPidList()Ljava/lang/String;
+ .locals 1
+
+ .prologue
+ .line 15
+ iget-object v0, p0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->pidList:Ljava/lang/String;
+
+ return-object v0
+.end method
+
+.method public setPidList(Ljava/lang/String;)V
+ .locals 0
+ .parameter "value"
+
+ .prologue
+ .line 11
+ iput-object p1, p0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->pidList:Ljava/lang/String;
+
+ .line 12
+ return-void
+.end method
\ No newline at end of file
diff -ru kindle3.7.0.108_orig/smali/com/amazon/kcp/application/IDeviceInformationProvider.smali kindle3.7.0.108/smali/com/amazon/kcp/application/IDeviceInformationProvider.smali
--- kindle3.7.0.108_orig/smali/com/amazon/kcp/application/IDeviceInformationProvider.smali
+++ kindle3.7.0.108/smali/com/amazon/kcp/application/IDeviceInformationProvider.smali
@@ -30,3 +30,9 @@
.method public abstract getPid()Ljava/lang/String;
.end method
+
+.method public abstract getPidList()Ljava/lang/String;
+.end method
+
+.method public abstract setPidList(Ljava/lang/String;)V
+.end method
\ No newline at end of file
diff -ru kindle3.7.0.108_orig/smali/com/amazon/kcp/info/AboutActivity.smali kindle3.7.0.108/smali/com/amazon/kcp/info/AboutActivity.smali
--- kindle3.7.0.108_orig/smali/com/amazon/kcp/info/AboutActivity.smali
+++ kindle3.7.0.108/smali/com/amazon/kcp/info/AboutActivity.smali
@@ -493,6 +493,57 @@
return-void
.end method
+.method private populatePIDList()V
+ .locals 7
+
+ .prologue
+ .line 313
+ invoke-static {}, Lcom/amazon/kcp/application/DeviceInformationProviderFactory;->getProvider()Lcom/amazon/kcp/application/IDeviceInformationProvider;
+
+ move-result-object v0
+
+ invoke-interface {v0}, Lcom/amazon/kcp/application/IDeviceInformationProvider;->getPidList()Ljava/lang/String;
+
+ move-result-object v1
+
+ .line 314
+ .local v1, PidList:Ljava/lang/String;
+ iget-object v3, p0, Lcom/amazon/kcp/info/AboutActivity;->groupItemList:Ljava/util/List;
+
+ new-instance v4, Lcom/amazon/kcp/info/AboutActivity$GroupItem;
+
+ const-string v5, "PID List"
+
+ const v6, 0x1
+
+ invoke-direct {v4, p0, v5, v6}, Lcom/amazon/kcp/info/AboutActivity$GroupItem;-><init>(Lcom/amazon/kcp/info/AboutActivity;Ljava/lang/String;Z)V
+
+ invoke-interface {v3, v4}, Ljava/util/List;->add(Ljava/lang/Object;)Z
+
+ .line 315
+ new-instance v2, Ljava/util/ArrayList;
+
+ invoke-direct {v2}, Ljava/util/ArrayList;-><init>()V
+
+ .line 316
+ .local v2, children:Ljava/util/List;,"Ljava/util/List<Lcom/amazon/kcp/info/AboutActivity$DetailItem;>;"
+ new-instance v3, Lcom/amazon/kcp/info/AboutActivity$DetailItem;
+
+ const-string v4, "PIDs"
+
+ invoke-direct {v3, p0, v4, v1}, Lcom/amazon/kcp/info/AboutActivity$DetailItem;-><init>(Lcom/amazon/kcp/info/AboutActivity;Ljava/lang/String;Ljava/lang/String;)V
+
+ invoke-interface {v2, v3}, Ljava/util/List;->add(Ljava/lang/Object;)Z
+
+ .line 317
+ iget-object v3, p0, Lcom/amazon/kcp/info/AboutActivity;->detailItemList:Ljava/util/List;
+
+ invoke-interface {v3, v2}, Ljava/util/List;->add(Ljava/lang/Object;)Z
+
+ .line 318
+ return-void
+.end method
+
.method private populateDisplayItems()V
.locals 1
@@ -539,6 +590,9 @@
invoke-direct {p0}, Lcom/amazon/kcp/info/AboutActivity;->populateDisplayInformation()V
.line 190
+ invoke-direct {p0}, Lcom/amazon/kcp/info/AboutActivity;->populatePIDList()V
+
+ .line 191
return-void
.line 172
diff -ru kindle3.7.0.108_orig/smali/com/amazon/system/security/Security.smali kindle3.7.0.108/smali/com/amazon/system/security/Security.smali
--- kindle3.7.0.108_orig/smali/com/amazon/system/security/Security.smali
+++ kindle3.7.0.108/smali/com/amazon/system/security/Security.smali
@@ -926,6 +926,16 @@
sget-object v0, Lcom/amazon/system/security/Security;->CUSTOM_PID_FOR_BUNDLED_DICTIONARY_DRM:Ljava/lang/String;
aput-object v0, v6, v8
+
+ invoke-static {}, Lcom/amazon/kcp/application/DeviceInformationProviderFactory;->getProvider()Lcom/amazon/kcp/application/IDeviceInformationProvider;
+
+ move-result-object v5
+
+ invoke-static {v6}, Ljava/util/Arrays;->toString([Ljava/lang/Object;)Ljava/lang/String;
+
+ move-result-object v2
+
+ invoke-interface {v5, v2}, Lcom/amazon/kcp/application/IDeviceInformationProvider;->setPidList(Ljava/lang/String;)V
.line 353
return-object v6

View File

@@ -0,0 +1,238 @@
Only in kindle4.0.2.1: build
diff -r -u10 kindle4.0.2.1_orig/smali/com/amazon/kcp/application/AndroidDeviceInformationProvider.smali kindle4.0.2.1/smali/com/amazon/kcp/application/AndroidDeviceInformationProvider.smali
--- kindle4.0.2.1_orig/smali/com/amazon/kcp/application/AndroidDeviceInformationProvider.smali 2013-05-22 18:39:03.000000000 -0500
+++ kindle4.0.2.1/smali/com/amazon/kcp/application/AndroidDeviceInformationProvider.smali 2013-05-23 16:54:53.000000000 -0500
@@ -36,20 +36,22 @@
.field private maxCpuSpeed:J
.field private maxMemory:J
.field private minCpuSpeed:J
.field private resources:Landroid/content/res/Resources;
.field private security:Lcom/mobipocket/android/library/reader/AndroidSecurity;
+.field private pidList:Ljava/lang/String;
+
.field private totalMemory:J
# direct methods
.method static constructor <clinit>()V
.locals 1
.prologue
.line 30
const-class v0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;
@@ -72,20 +74,24 @@
.prologue
.line 130
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
.line 131
iput-object p1, p0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->security:Lcom/mobipocket/android/library/reader/AndroidSecurity;
.line 132
iput-object p2, p0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->deviceType:Lcom/amazon/kcp/application/AmazonDeviceType;
+ const-string v0, "Open DRMed book to show PID list."
+
+ iput-object v0, p0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->pidList:Ljava/lang/String;
+
.line 133
sget-object v0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->TAG:Ljava/lang/String;
new-instance v0, Ljava/lang/StringBuilder;
invoke-direct {v0}, Ljava/lang/StringBuilder;-><init>()V
const-string v1, "Device Type is set to \""
invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
@@ -1235,10 +1241,33 @@
move-result-wide v0
iput-wide v0, p0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->totalMemory:J
.line 308
:cond_0
iget-wide v0, p0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->totalMemory:J
return-wide v0
.end method
+
+.method public getPidList()Ljava/lang/String;
+ .locals 1
+
+ .prologue
+ .line 15
+ iget-object v0, p0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->pidList:Ljava/lang/String;
+
+ return-object v0
+.end method
+
+.method public setPidList(Ljava/lang/String;)V
+ .locals 0
+ .parameter "value"
+
+ .prologue
+ .line 11
+ iput-object p1, p0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->pidList:Ljava/lang/String;
+
+ .line 12
+ return-void
+.end method
+
diff -r -u10 kindle4.0.2.1_orig/smali/com/amazon/kcp/application/IDeviceInformationProvider.smali kindle4.0.2.1/smali/com/amazon/kcp/application/IDeviceInformationProvider.smali
--- kindle4.0.2.1_orig/smali/com/amazon/kcp/application/IDeviceInformationProvider.smali 2013-05-22 18:39:03.000000000 -0500
+++ kindle4.0.2.1/smali/com/amazon/kcp/application/IDeviceInformationProvider.smali 2013-05-23 16:55:58.000000000 -0500
@@ -23,10 +23,16 @@
.end method
.method public abstract getDeviceTypeId()Ljava/lang/String;
.end method
.method public abstract getOsVersion()Ljava/lang/String;
.end method
.method public abstract getPid()Ljava/lang/String;
.end method
+
+.method public abstract getPidList()Ljava/lang/String;
+.end method
+
+.method public abstract setPidList(Ljava/lang/String;)V
+.end method
diff -r -u10 kindle4.0.2.1_orig/smali/com/amazon/kcp/info/AboutActivity.smali kindle4.0.2.1/smali/com/amazon/kcp/info/AboutActivity.smali
--- kindle4.0.2.1_orig/smali/com/amazon/kcp/info/AboutActivity.smali 2013-05-22 18:39:03.000000000 -0500
+++ kindle4.0.2.1/smali/com/amazon/kcp/info/AboutActivity.smali 2013-05-23 17:18:14.000000000 -0500
@@ -486,20 +486,71 @@
.end local v2 #screenDpi:Ljava/lang/String;
:cond_0
iget-object v5, p0, Lcom/amazon/kcp/info/AboutActivity;->detailItemList:Ljava/util/List;
invoke-interface {v5, v0}, Ljava/util/List;->add(Ljava/lang/Object;)Z
.line 317
return-void
.end method
+.method private populatePIDList()V
+ .locals 7
+
+ .prologue
+ .line 313
+ invoke-static {}, Lcom/amazon/kcp/application/DeviceInformationProviderFactory;->getProvider()Lcom/amazon/kcp/application/IDeviceInformationProvider;
+
+ move-result-object v0
+
+ invoke-interface {v0}, Lcom/amazon/kcp/application/IDeviceInformationProvider;->getPidList()Ljava/lang/String;
+
+ move-result-object v1
+
+ .line 314
+ .local v1, PidList:Ljava/lang/String;
+ iget-object v3, p0, Lcom/amazon/kcp/info/AboutActivity;->groupItemList:Ljava/util/List;
+
+ new-instance v4, Lcom/amazon/kcp/info/AboutActivity$GroupItem;
+
+ const-string v5, "PID List"
+
+ const v6, 0x1
+
+ invoke-direct {v4, p0, v5, v6}, Lcom/amazon/kcp/info/AboutActivity$GroupItem;-><init>(Lcom/amazon/kcp/info/AboutActivity;Ljava/lang/String;Z)V
+
+ invoke-interface {v3, v4}, Ljava/util/List;->add(Ljava/lang/Object;)Z
+
+ .line 315
+ new-instance v2, Ljava/util/ArrayList;
+
+ invoke-direct {v2}, Ljava/util/ArrayList;-><init>()V
+
+ .line 316
+ .local v2, children:Ljava/util/List;,"Ljava/util/List<Lcom/amazon/kcp/info/AboutActivity$DetailItem;>;"
+ new-instance v3, Lcom/amazon/kcp/info/AboutActivity$DetailItem;
+
+ const-string v4, "PIDs"
+
+ invoke-direct {v3, p0, v4, v1}, Lcom/amazon/kcp/info/AboutActivity$DetailItem;-><init>(Lcom/amazon/kcp/info/AboutActivity;Ljava/lang/String;Ljava/lang/String;)V
+
+ invoke-interface {v2, v3}, Ljava/util/List;->add(Ljava/lang/Object;)Z
+
+ .line 317
+ iget-object v3, p0, Lcom/amazon/kcp/info/AboutActivity;->detailItemList:Ljava/util/List;
+
+ invoke-interface {v3, v2}, Ljava/util/List;->add(Ljava/lang/Object;)Z
+
+ .line 318
+ return-void
+.end method
+
.method private populateDisplayItems()V
.locals 1
.prologue
.line 171
iget-object v0, p0, Lcom/amazon/kcp/info/AboutActivity;->groupItemList:Ljava/util/List;
if-nez v0, :cond_0
.line 173
@@ -531,20 +582,22 @@
.line 192
invoke-direct {p0}, Lcom/amazon/kcp/info/AboutActivity;->populateRamInformation()V
.line 193
invoke-direct {p0}, Lcom/amazon/kcp/info/AboutActivity;->populateStorageInformation()V
.line 194
invoke-direct {p0}, Lcom/amazon/kcp/info/AboutActivity;->populateDisplayInformation()V
+ invoke-direct {p0}, Lcom/amazon/kcp/info/AboutActivity;->populatePIDList()V
+
.line 195
return-void
.line 177
:cond_0
iget-object v0, p0, Lcom/amazon/kcp/info/AboutActivity;->groupItemList:Ljava/util/List;
invoke-interface {v0}, Ljava/util/List;->clear()V
goto :goto_0
diff -r -u10 kindle4.0.2.1_orig/smali/com/amazon/system/security/Security.smali kindle4.0.2.1/smali/com/amazon/system/security/Security.smali
--- kindle4.0.2.1_orig/smali/com/amazon/system/security/Security.smali 2013-05-22 18:39:04.000000000 -0500
+++ kindle4.0.2.1/smali/com/amazon/system/security/Security.smali 2013-05-23 17:19:05.000000000 -0500
@@ -920,20 +920,30 @@
.line 350
:cond_2
add-int/lit8 v8, v8, 0x1
.line 351
sget-object v0, Lcom/amazon/system/security/Security;->CUSTOM_PID_FOR_BUNDLED_DICTIONARY_DRM:Ljava/lang/String;
aput-object v0, v6, v8
+ invoke-static {}, Lcom/amazon/kcp/application/DeviceInformationProviderFactory;->getProvider()Lcom/amazon/kcp/application/IDeviceInformationProvider;
+
+ move-result-object v5
+
+ invoke-static {v6}, Ljava/util/Arrays;->toString([Ljava/lang/Object;)Ljava/lang/String;
+
+ move-result-object v2
+
+ invoke-interface {v5, v2}, Lcom/amazon/kcp/application/IDeviceInformationProvider;->setPidList(Ljava/lang/String;)V
+
.line 353
return-object v6
.end method
# virtual methods
.method public customDrmOnly()I
.locals 1
.prologue

View File

@@ -0,0 +1,11 @@
Notes from UE01 about this patch:
1. Revised the “Other_Tools/Kindle_for_Android_Patches/” for Kindle 4.8.1.10
2. Built Kindle 4.8.1.10 with the PID List added to the About activity
3. Uninstalled the Amazon/Play-store version and installed the patched version
4. Signed in to Amazon
5. Opened the book
6. Did Info > About > PID List and copied the PIDs to Calibres Plugins>File type > DeDRM > Mobipocket dialog
7. **Crucial** copied the PRC file to the PC (because the files checksum has changed since it was last copied)
8. In Calibre, Add Books (from a single directory)

View File

@@ -0,0 +1,157 @@
diff --git a/smali/com/amazon/kcp/application/AndroidDeviceInformationProvider.smali b/smali/com/amazon/kcp/application/AndroidDeviceInformationProvider.smali
index 8ea400e..3aefad2 100644
--- a/smali/com/amazon/kcp/application/AndroidDeviceInformationProvider.smali
+++ b/smali/com/amazon/kcp/application/AndroidDeviceInformationProvider.smali
@@ -41,6 +41,8 @@
.field private security:Lcom/mobipocket/android/library/reader/AndroidSecurity;
+.field private pidList:Ljava/lang/String;
+
.field private totalMemory:J
@@ -74,6 +76,10 @@
.line 133
iput-object p1, p0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->security:Lcom/mobipocket/android/library/reader/AndroidSecurity;
+ const-string v0, "Open DRMed book to show PID list."
+
+ iput-object v0, p0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->pidList:Ljava/lang/String;
+
.line 134
sget-object v0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->TAG:Ljava/lang/String;
@@ -1339,3 +1345,26 @@
return-wide v0
.end method
+
+.method public getPidList()Ljava/lang/String;
+ .locals 1
+
+ .prologue
+ .line 15
+ iget-object v0, p0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->pidList:Ljava/lang/String;
+
+ return-object v0
+.end method
+
+.method public setPidList(Ljava/lang/String;)V
+ .locals 0
+ .param p1, "value"
+
+ .prologue
+ .line 11
+ iput-object p1, p0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->pidList:Ljava/lang/String;
+
+ .line 12
+ return-void
+.end method
+
diff --git a/smali/com/amazon/kcp/application/IDeviceInformationProvider.smali b/smali/com/amazon/kcp/application/IDeviceInformationProvider.smali
index e4a3523..2269fab 100644
--- a/smali/com/amazon/kcp/application/IDeviceInformationProvider.smali
+++ b/smali/com/amazon/kcp/application/IDeviceInformationProvider.smali
@@ -30,3 +30,9 @@
.method public abstract getPid()Ljava/lang/String;
.end method
+
+.method public abstract getPidList()Ljava/lang/String;
+.end method
+
+.method public abstract setPidList(Ljava/lang/String;)V
+.end method
diff --git a/smali/com/amazon/kcp/info/AboutActivity.smali b/smali/com/amazon/kcp/info/AboutActivity.smali
index 5640e9e..e298341 100644
--- a/smali/com/amazon/kcp/info/AboutActivity.smali
+++ b/smali/com/amazon/kcp/info/AboutActivity.smali
@@ -493,6 +493,57 @@
return-void
.end method
+.method private populatePIDList()V
+ .locals 7
+
+ .prologue
+ .line 313
+ invoke-static {}, Lcom/amazon/kcp/application/DeviceInformationProviderFactory;->getProvider()Lcom/amazon/kcp/application/IDeviceInformationProvider;
+
+ move-result-object v0
+
+ invoke-interface {v0}, Lcom/amazon/kcp/application/IDeviceInformationProvider;->getPidList()Ljava/lang/String;
+
+ move-result-object v1
+
+ .line 314
+ .local v1, "PidList":Ljava/lang/String;
+ iget-object v3, p0, Lcom/amazon/kcp/info/AboutActivity;->groupItemList:Ljava/util/List;
+
+ new-instance v4, Lcom/amazon/kcp/info/AboutActivity$GroupItem;
+
+ const-string v5, "PID List"
+
+ const v6, 0x1
+
+ invoke-direct {v4, p0, v5, v6}, Lcom/amazon/kcp/info/AboutActivity$GroupItem;-><init>(Lcom/amazon/kcp/info/AboutActivity;Ljava/lang/String;Z)V
+
+ invoke-interface {v3, v4}, Ljava/util/List;->add(Ljava/lang/Object;)Z
+
+ .line 315
+ new-instance v2, Ljava/util/ArrayList;
+
+ invoke-direct {v2}, Ljava/util/ArrayList;-><init>()V
+
+ .line 316
+ .local v2, "children":Ljava/util/List;,"Ljava/util/List<Lcom/amazon/kcp/info/AboutActivity$DetailItem;>;"
+ new-instance v3, Lcom/amazon/kcp/info/AboutActivity$DetailItem;
+
+ const-string v4, "PIDs"
+
+ invoke-direct {v3, p0, v4, v1}, Lcom/amazon/kcp/info/AboutActivity$DetailItem;-><init>(Lcom/amazon/kcp/info/AboutActivity;Ljava/lang/String;Ljava/lang/String;)V
+
+ invoke-interface {v2, v3}, Ljava/util/List;->add(Ljava/lang/Object;)Z
+
+ .line 317
+ iget-object v3, p0, Lcom/amazon/kcp/info/AboutActivity;->detailItemList:Ljava/util/List;
+
+ invoke-interface {v3, v2}, Ljava/util/List;->add(Ljava/lang/Object;)Z
+
+ .line 318
+ return-void
+.end method
+
.method private populateDisplayItems()V
.locals 1
@@ -538,6 +589,8 @@
.line 173
invoke-direct {p0}, Lcom/amazon/kcp/info/AboutActivity;->populateDisplayInformation()V
+ invoke-direct {p0}, Lcom/amazon/kcp/info/AboutActivity;->populatePIDList()V
+
.line 174
return-void
diff --git a/smali/com/amazon/system/security/Security.smali b/smali/com/amazon/system/security/Security.smali
index 04ea997..e88fe08 100644
--- a/smali/com/amazon/system/security/Security.smali
+++ b/smali/com/amazon/system/security/Security.smali
@@ -940,6 +940,16 @@
aput-object v0, v6, v8
+ invoke-static {}, Lcom/amazon/kcp/application/DeviceInformationProviderFactory;->getProvider()Lcom/amazon/kcp/application/IDeviceInformationProvider;
+
+ move-result-object v5
+
+ invoke-static {v6}, Ljava/util/Arrays;->toString([Ljava/lang/Object;)Ljava/lang/String;
+
+ move-result-object v2
+
+ invoke-interface {v5, v2}, Lcom/amazon/kcp/application/IDeviceInformationProvider;->setPidList(Ljava/lang/String;)V
+
.line 347
return-object v6
.end method

747
Other_Tools/Kobo/obok.py Executable file
View File

@@ -0,0 +1,747 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Version 3.2.5 December 2016
# Improve detection of good text decryption.
#
# Version 3.2.4 December 2016
# Remove incorrect support for Kobo Desktop under Wine
#
# Version 3.2.3 October 2016
# Fix for windows network user and more xml fixes
#
# Version 3.2.2 October 2016
# Change to the way the new database version is handled.
#
# Version 3.2.1 September 2016
# Update for v4.0 of Windows Desktop app.
#
# Version 3.2.0 January 2016
# Update for latest version of Windows Desktop app.
# Support Kobo devices in the command line version.
#
# Version 3.1.9 November 2015
# Handle Kobo Desktop under wine on Linux
#
# Version 3.1.8 November 2015
# Handle the case of Kobo Arc or Vox device (i.e. don't crash).
#
# Version 3.1.7 October 2015
# Handle the case of no device or database more gracefully.
#
# Version 3.1.6 September 2015
# Enable support for Kobo devices
# More character encoding fixes (unicode strings)
#
# Version 3.1.5 September 2015
# Removed requirement that a purchase has been made.
# Also add in character encoding fixes
#
# Version 3.1.4 September 2015
# Updated for version 3.17 of the Windows Desktop app.
#
# Version 3.1.3 August 2015
# Add translations for Portuguese and Arabic
#
# Version 3.1.2 January 2015
# Add coding, version number and version announcement
#
# Version 3.05 October 2014
# Identifies DRM-free books in the dialog
#
# Version 3.04 September 2014
# Handles DRM-free books as well (sometimes Kobo Library doesn't
# show download link for DRM-free books)
#
# Version 3.03 August 2014
# If PyCrypto is unavailable try to use libcrypto for AES_ECB.
#
# Version 3.02 August 2014
# Relax checking of application/xhtml+xml and image/jpeg content.
#
# Version 3.01 June 2014
# Check image/jpeg as well as application/xhtml+xml content. Fix typo
# in Windows ipconfig parsing.
#
# Version 3.0 June 2014
# Made portable for Mac and Windows, and the only module dependency
# not part of python core is PyCrypto. Major code cleanup/rewrite.
# No longer tries the first MAC address; tries them all if it detects
# the decryption failed.
#
# Updated September 2013 by Anon
# Version 2.02
# Incorporated minor fixes posted at Apprentice Alf's.
#
# Updates July 2012 by Michael Newton
# PWSD ID is no longer a MAC address, but should always
# be stored in the registry. Script now works with OS X
# and checks plist for values instead of registry. Must
# have biplist installed for OS X support.
#
# Original comments left below; note the "AUTOPSY" is inaccurate. See
# KoboLibrary.userkeys and KoboFile.decrypt()
#
##########################################################
# KOBO DRM CRACK BY #
# PHYSISTICATED #
##########################################################
# This app was made for Python 2.7 on Windows 32-bit
#
# This app needs pycrypto - get from here:
# http://www.voidspace.org.uk/python/modules.shtml
#
# Usage: obok.py
# Choose the book you want to decrypt
#
# Shouts to my krew - you know who you are - and one in
# particular who gave me a lot of help with this - thank
# you so much!
#
# Kopimi /K\
# Keep sharing, keep copying, but remember that nothing is
# for free - make sure you compensate your favorite
# authors - and cut out the middle man whenever possible
# ;) ;) ;)
#
# DRM AUTOPSY
# The Kobo DRM was incredibly easy to crack, but it took
# me months to get around to making this. Here's the
# basics of how it works:
# 1: Get MAC address of first NIC in ipconfig (sometimes
# stored in registry as pwsdid)
# 2: Get user ID (stored in tons of places, this gets it
# from HKEY_CURRENT_USER\Software\Kobo\Kobo Desktop
# Edition\Browser\cookies)
# 3: Concatenate and SHA256, take the second half - this
# is your master key
# 4: Open %LOCALAPPDATA%\Kobo Desktop Editions\Kobo.sqlite
# and dump content_keys
# 5: Unbase64 the keys, then decode these with the master
# key - these are your page keys
# 6: Unzip EPUB of your choice, decrypt each page with its
# page key, then zip back up again
#
# WHY USE THIS WHEN INEPT WORKS FINE? (adobe DRM stripper)
# Inept works very well, but authors on Kobo can choose
# what DRM they want to use - and some have chosen not to
# let people download them with Adobe Digital Editions -
# they would rather lock you into a single platform.
#
# With Obok, you can sync Kobo Desktop, decrypt all your
# ebooks, and then use them on whatever device you want
# - you bought them, you own them, you can do what you
# like with them.
#
# Obok is Kobo backwards, but it is also means "next to"
# in Polish.
# When you buy a real book, it is right next to you. You
# can read it at home, at work, on a train, you can lend
# it to a friend, you can scribble on it, and add your own
# explanations/translations.
#
# Obok gives you this power over your ebooks - no longer
# are you restricted to one device. This allows you to
# embed foreign fonts into your books, as older Kobo's
# can't display them properly. You can read your books
# on your phones, in different PC readers, and different
# ereader devices. You can share them with your friends
# too, if you like - you can do that with a real book
# after all.
#
"""Manage all Kobo books, either encrypted or DRM-free."""
__version__ = '3.2.4'
__about__ = u"Obok v{0}\nCopyright © 2012-2016 Physisticated et al.".format(__version__)
import sys
import os
import subprocess
import sqlite3
import base64
import binascii
import re
import zipfile
import hashlib
import xml.etree.ElementTree as ET
import string
import shutil
import argparse
import tempfile
can_parse_xml = True
try:
from xml.etree import ElementTree as ET
# print u"using xml.etree for xml parsing"
except ImportError:
can_parse_xml = False
# print u"Cannot find xml.etree, disabling extraction of serial numbers"
# List of all known hash keys
KOBO_HASH_KEYS = ['88b3a2e13', 'XzUhGYdFp', 'NoCanLook','QJhwzAtXL']
class ENCRYPTIONError(Exception):
pass
def _load_crypto_libcrypto():
from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_int, c_long, \
Structure, c_ulong, create_string_buffer, cast
from ctypes.util import find_library
if sys.platform.startswith('win'):
libcrypto = find_library('libeay32')
else:
libcrypto = find_library('crypto')
if libcrypto is None:
raise ENCRYPTIONError('libcrypto not found')
libcrypto = CDLL(libcrypto)
AES_MAXNR = 14
c_char_pp = POINTER(c_char_p)
c_int_p = POINTER(c_int)
class AES_KEY(Structure):
_fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))),
('rounds', c_int)]
AES_KEY_p = POINTER(AES_KEY)
def F(restype, name, argtypes):
func = getattr(libcrypto, name)
func.restype = restype
func.argtypes = argtypes
return func
AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key',
[c_char_p, c_int, AES_KEY_p])
AES_ecb_encrypt = F(None, 'AES_ecb_encrypt',
[c_char_p, c_char_p, AES_KEY_p, c_int])
class AES(object):
def __init__(self, userkey):
self._blocksize = len(userkey)
if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) :
raise ENCRYPTIONError(_('AES improper key used'))
return
key = self._key = AES_KEY()
rv = AES_set_decrypt_key(userkey, len(userkey) * 8, key)
if rv < 0:
raise ENCRYPTIONError(_('Failed to initialize AES key'))
def decrypt(self, data):
clear = ''
for i in range(0, len(data), 16):
out = create_string_buffer(16)
rv = AES_ecb_encrypt(data[i:i+16], out, self._key, 0)
if rv == 0:
raise ENCRYPTIONError(_('AES decryption failed'))
clear += out.raw
return clear
return AES
def _load_crypto_pycrypto():
from Crypto.Cipher import AES as _AES
class AES(object):
def __init__(self, key):
self._aes = _AES.new(key, _AES.MODE_ECB)
def decrypt(self, data):
return self._aes.decrypt(data)
return AES
def _load_crypto():
AES = None
cryptolist = (_load_crypto_pycrypto, _load_crypto_libcrypto)
for loader in cryptolist:
try:
AES = loader()
break
except (ImportError, ENCRYPTIONError):
pass
return AES
AES = _load_crypto()
# Wrap a stream so that output gets flushed immediately
# and also make sure that any unicode strings get
# encoded using "replace" before writing them.
class SafeUnbuffered:
def __init__(self, stream):
self.stream = stream
self.encoding = stream.encoding
if self.encoding == None:
self.encoding = "utf-8"
def write(self, data):
if isinstance(data,unicode):
data = data.encode(self.encoding,"replace")
self.stream.write(data)
self.stream.flush()
def __getattr__(self, attr):
return getattr(self.stream, attr)
class KoboLibrary(object):
"""The Kobo library.
This class represents all the information available from the data
written by the Kobo Desktop Edition application, including the list
of books, their titles, and the user's encryption key(s)."""
def __init__ (self, serials = [], device_path = None):
print __about__
self.kobodir = u""
kobodb = u""
# Order of checks
# 1. first check if a device_path has been passed in, and whether
# we can find the sqlite db in the respective place
# 2. if 1., and we got some serials passed in (from saved
# settings in calibre), just use it
# 3. if 1. worked, but we didn't get serials, try to parse them
# from the device, if this didn't work, unset everything
# 4. if by now we don't have kobodir set, give up on device and
# try to use the Desktop app.
# step 1. check whether this looks like a real device
if (device_path):
# we got a device path
self.kobodir = os.path.join(device_path, u".kobo")
# devices use KoboReader.sqlite
kobodb = os.path.join(self.kobodir, u"KoboReader.sqlite")
if (not(os.path.isfile(kobodb))):
# device path seems to be wrong, unset it
device_path = u""
self.kobodir = u""
kobodb = u""
if (self.kobodir):
# step 3. we found a device but didn't get serials, try to get them
if (len(serials) == 0):
# we got a device path but no saved serial
# try to get the serial from the device
# print u"get_device_settings - device_path = {0}".format(device_path)
# get serial from device_path/.adobe-digital-editions/device.xml
if can_parse_xml:
devicexml = os.path.join(device_path, '.adobe-digital-editions', 'device.xml')
# print u"trying to load {0}".format(devicexml)
if (os.path.exists(devicexml)):
# print u"trying to parse {0}".format(devicexml)
xmltree = ET.parse(devicexml)
for node in xmltree.iter():
if "deviceSerial" in node.tag:
serial = node.text
# print u"found serial {0}".format(serial)
serials.append(serial)
break
else:
# print u"cannot get serials from device."
device_path = u""
self.kobodir = u""
kobodb = u""
if (self.kobodir == u""):
# step 4. we haven't found a device with serials, so try desktop apps
if sys.platform.startswith('win'):
import _winreg as winreg
if sys.getwindowsversion().major > 5:
if 'LOCALAPPDATA' in os.environ.keys():
# Python 2.x does not return unicode env. Use Python 3.x
self.kobodir = winreg.ExpandEnvironmentStrings(u"%LOCALAPPDATA%")
if (self.kobodir == u""):
if 'USERPROFILE' in os.environ.keys():
# Python 2.x does not return unicode env. Use Python 3.x
self.kobodir = os.path.join(winreg.ExpandEnvironmentStrings(u"%USERPROFILE%"), u"Local Settings", u"Application Data")
self.kobodir = os.path.join(self.kobodir, u"Kobo", u"Kobo Desktop Edition")
elif sys.platform.startswith('darwin'):
self.kobodir = os.path.join(os.environ['HOME'], u"Library", u"Application Support", u"Kobo", u"Kobo Desktop Edition")
#elif linux_path != None:
# Probably Linux, let's get the wine prefix and path to Kobo.
# self.kobodir = os.path.join(linux_path, u"Local Settings", u"Application Data", u"Kobo", u"Kobo Desktop Edition")
# desktop versions use Kobo.sqlite
kobodb = os.path.join(self.kobodir, u"Kobo.sqlite")
# check for existence of file
if (not(os.path.isfile(kobodb))):
# give up here, we haven't found anything useful
self.kobodir = u""
kobodb = u""
if (self.kobodir != u""):
self.bookdir = os.path.join(self.kobodir, u"kepub")
# make a copy of the database in a temporary file
# so we can ensure it's not using WAL logging which sqlite3 can't do.
self.newdb = tempfile.NamedTemporaryFile(mode='wb', delete=False)
print self.newdb.name
olddb = open(kobodb, 'rb')
self.newdb.write(olddb.read(18))
self.newdb.write('\x01\x01')
olddb.read(2)
self.newdb.write(olddb.read())
olddb.close()
self.newdb.close()
self.__sqlite = sqlite3.connect(self.newdb.name)
self.__cursor = self.__sqlite.cursor()
self._userkeys = []
self._books = []
self._volumeID = []
self._serials = serials
def close (self):
"""Closes the database used by the library."""
self.__cursor.close()
self.__sqlite.close()
# delete the temporary copy of the database
os.remove(self.newdb.name)
@property
def userkeys (self):
"""The list of potential userkeys being used by this library.
Only one of these will be valid.
"""
if len(self._userkeys) != 0:
return self._userkeys
for macaddr in self.__getmacaddrs():
self._userkeys.extend(self.__getuserkeys(macaddr))
return self._userkeys
@property
def books (self):
"""The list of KoboBook objects in the library."""
if len(self._books) != 0:
return self._books
"""Drm-ed kepub"""
for row in self.__cursor.execute('SELECT DISTINCT volumeid, Title, Attribution, Series FROM content_keys, content WHERE contentid = volumeid'):
self._books.append(KoboBook(row[0], row[1], self.__bookfile(row[0]), 'kepub', self.__cursor, author=row[2], series=row[3]))
self._volumeID.append(row[0])
"""Drm-free"""
for f in os.listdir(self.bookdir):
if(f not in self._volumeID):
row = self.__cursor.execute("SELECT Title, Attribution, Series FROM content WHERE ContentID = '" + f + "'").fetchone()
if row is not None:
fTitle = row[0]
self._books.append(KoboBook(f, fTitle, self.__bookfile(f), 'drm-free', self.__cursor, author=row[1], series=row[2]))
self._volumeID.append(f)
"""Sort"""
self._books.sort(key=lambda x: x.title)
return self._books
def __bookfile (self, volumeid):
"""The filename needed to open a given book."""
return os.path.join(self.kobodir, u"kepub", volumeid)
def __getmacaddrs (self):
"""The list of all MAC addresses on this machine."""
macaddrs = []
if sys.platform.startswith('win'):
c = re.compile('\s(' + '[0-9a-f]{2}-' * 5 + '[0-9a-f]{2})(\s|$)', re.IGNORECASE)
(p_in, p_out, p_err) = os.popen3('ipconfig /all')
for line in p_out:
m = c.search(line)
if m:
macaddrs.append(re.sub("-", ":", m.group(1)).upper())
elif sys.platform.startswith('darwin'):
c = re.compile('\s(' + '[0-9a-f]{2}:' * 5 + '[0-9a-f]{2})(\s|$)', re.IGNORECASE)
output = subprocess.check_output('/sbin/ifconfig -a', shell=True)
matches = c.findall(output)
for m in matches:
# print u"m:{0}".format(m[0])
macaddrs.append(m[0].upper())
else:
# probably linux, let's try ipconfig under wine
c = re.compile('\s(' + '[0-9a-f]{2}-' * 5 + '[0-9a-f]{2})(\s|$)', re.IGNORECASE)
for line in os.popen('ipconfig /all'):
m = c.search(line)
if m:
macaddrs.append(re.sub("-", ":", m.group(1)).upper())
# extend the list of macaddrs in any case with the serials
# cannot hurt ;-)
macaddrs.extend(self._serials)
return macaddrs
def __getuserids (self):
userids = []
cursor = self.__cursor.execute('SELECT UserID FROM user')
row = cursor.fetchone()
while row is not None:
try:
userid = row[0]
userids.append(userid)
except:
pass
row = cursor.fetchone()
return userids
def __getuserkeys (self, macaddr):
userids = self.__getuserids()
userkeys = []
for hash in KOBO_HASH_KEYS:
deviceid = hashlib.sha256(hash + macaddr).hexdigest()
for userid in userids:
userkey = hashlib.sha256(deviceid + userid).hexdigest()
userkeys.append(binascii.a2b_hex(userkey[32:]))
return userkeys
class KoboBook(object):
"""A Kobo book.
A Kobo book contains a number of unencrypted and encrypted files.
This class provides a list of the encrypted files.
Each book has the following instance variables:
volumeid - a UUID which uniquely refers to the book in this library.
title - the human-readable book title.
filename - the complete path and filename of the book.
type - either kepub or drm-free"""
def __init__ (self, volumeid, title, filename, type, cursor, author=None, series=None):
self.volumeid = volumeid
self.title = title
self.author = author
self.series = series
self.series_index = None
self.filename = filename
self.type = type
self.__cursor = cursor
self._encryptedfiles = {}
@property
def encryptedfiles (self):
"""A dictionary of KoboFiles inside the book.
The dictionary keys are the relative pathnames, which are
the same as the pathnames inside the book 'zip' file."""
if (self.type == 'drm-free'):
return self._encryptedfiles
if len(self._encryptedfiles) != 0:
return self._encryptedfiles
# Read the list of encrypted files from the DB
for row in self.__cursor.execute('SELECT elementid,elementkey FROM content_keys,content WHERE volumeid = ? AND volumeid = contentid',(self.volumeid,)):
self._encryptedfiles[row[0]] = KoboFile(row[0], None, base64.b64decode(row[1]))
# Read the list of files from the kepub OPF manifest so that
# we can get their proper MIME type.
# NOTE: this requires that the OPF file is unencrypted!
zin = zipfile.ZipFile(self.filename, "r")
xmlns = {
'ocf': 'urn:oasis:names:tc:opendocument:xmlns:container',
'opf': 'http://www.idpf.org/2007/opf'
}
ocf = ET.fromstring(zin.read('META-INF/container.xml'))
opffile = ocf.find('.//ocf:rootfile', xmlns).attrib['full-path']
basedir = re.sub('[^/]+$', '', opffile)
opf = ET.fromstring(zin.read(opffile))
zin.close()
c = re.compile('/')
for item in opf.findall('.//opf:item', xmlns):
mimetype = item.attrib['media-type']
# Convert relative URIs
href = item.attrib['href']
if not c.match(href):
href = string.join((basedir, href), '')
# Update books we've found from the DB.
if href in self._encryptedfiles:
self._encryptedfiles[href].mimetype = mimetype
return self._encryptedfiles
@property
def has_drm (self):
return not self.type == 'drm-free'
class KoboFile(object):
"""An encrypted file in a KoboBook.
Each file has the following instance variables:
filename - the relative pathname inside the book zip file.
mimetype - the file's MIME type, e.g. 'image/jpeg'
key - the encrypted page key."""
def __init__ (self, filename, mimetype, key):
self.filename = filename
self.mimetype = mimetype
self.key = key
def decrypt (self, userkey, contents):
"""
Decrypt the contents using the provided user key and the
file page key. The caller must determine if the decrypted
data is correct."""
# The userkey decrypts the page key (self.key)
keyenc = AES(userkey)
decryptedkey = keyenc.decrypt(self.key)
# The decrypted page key decrypts the content
pageenc = AES(decryptedkey)
return self.__removeaespadding(pageenc.decrypt(contents))
def check (self, contents):
"""
If the contents uses some known MIME types, check if it
conforms to the type. Throw a ValueError exception if not.
If the contents uses an uncheckable MIME type, don't check
it and don't throw an exception.
Returns True if the content was checked, False if it was not
checked."""
if self.mimetype == 'application/xhtml+xml':
# assume utf-8 with no BOM
textoffset = 0
stride = 1
print u"Checking text:{0}:".format(contents[:10])
# check for byte order mark
if contents[:3]=="\xef\xbb\xbf":
# seems to be utf-8 with BOM
print u"Could be utf-8 with BOM"
textoffset = 3
elif contents[:2]=="\xfe\xff":
# seems to be utf-16BE
print u"Could be utf-16BE"
textoffset = 3
stride = 2
elif contents[:2]=="\xff\xfe":
# seems to be utf-16LE
print u"Could be utf-16LE"
textoffset = 2
stride = 2
else:
print u"Perhaps utf-8 without BOM"
# now check that the first few characters are in the ASCII range
for i in xrange(textoffset,textoffset+5*stride,stride):
if ord(contents[i])<32 or ord(contents[i])>127:
# Non-ascii, so decryption probably failed
print u"Bad character at {0}, value {1}".format(i,ord(contents[i]))
raise ValueError
print u"Seems to be good text"
return True
if contents[:5]=="<?xml" or contents[:8]=="\xef\xbb\xbf<?xml":
# utf-8
return True
elif contents[:14]=="\xfe\xff\x00<\x00?\x00x\x00m\x00l":
# utf-16BE
return True
elif contents[:14]=="\xff\xfe<\x00?\x00x\x00m\x00l\x00":
# utf-16LE
return True
elif contents[:9]=="<!DOCTYPE" or contents[:12]=="\xef\xbb\xbf<!DOCTYPE":
# utf-8 of weird <!DOCTYPE start
return True
elif contents[:22]=="\xfe\xff\x00<\x00!\x00D\x00O\x00C\x00T\x00Y\x00P\x00E":
# utf-16BE of weird <!DOCTYPE start
return True
elif contents[:22]=="\xff\xfe<\x00!\x00D\x00O\x00C\x00T\x00Y\x00P\x00E\x00":
# utf-16LE of weird <!DOCTYPE start
return True
else:
print u"Bad XML: {0}".format(contents[:8])
raise ValueError
elif self.mimetype == 'image/jpeg':
if contents[:3] == '\xff\xd8\xff':
return True
else:
print u"Bad JPEG: {0}".format(contents[:3].encode('hex'))
raise ValueError()
return False
def __removeaespadding (self, contents):
"""
Remove the trailing padding, using what appears to be the CMS
algorithm from RFC 5652 6.3"""
lastchar = binascii.b2a_hex(contents[-1:])
strlen = int(lastchar, 16)
padding = strlen
if strlen == 1:
return contents[:-1]
if strlen < 16:
for i in range(strlen):
testchar = binascii.b2a_hex(contents[-strlen:-(strlen-1)])
if testchar != lastchar:
padding = 0
if padding > 0:
contents = contents[:-padding]
return contents
def decrypt_book(book, lib):
print u"Converting {0}".format(book.title)
zin = zipfile.ZipFile(book.filename, "r")
# make filename out of Unicode alphanumeric and whitespace equivalents from title
outname = u"{0}.epub".format(re.sub('[^\s\w]', '_', book.title, 0, re.UNICODE))
if (book.type == 'drm-free'):
print u"DRM-free book, conversion is not needed"
shutil.copyfile(book.filename, outname)
print u"Book saved as {0}".format(os.path.join(os.getcwd(), outname))
return 0
result = 1
for userkey in lib.userkeys:
print u"Trying key: {0}".format(userkey.encode('hex_codec'))
try:
zout = zipfile.ZipFile(outname, "w", zipfile.ZIP_DEFLATED)
for filename in zin.namelist():
contents = zin.read(filename)
if filename in book.encryptedfiles:
file = book.encryptedfiles[filename]
contents = file.decrypt(userkey, contents)
# Parse failures mean the key is probably wrong.
file.check(contents)
zout.writestr(filename, contents)
zout.close()
print u"Decryption succeeded."
print u"Book saved as {0}".format(os.path.join(os.getcwd(), outname))
result = 0
break
except ValueError:
print u"Decryption failed."
zout.close()
os.remove(outname)
zin.close()
return result
def cli_main():
description = __about__
epilog = u"Parsing of arguments failed."
parser = argparse.ArgumentParser(prog=sys.argv[0], description=description, epilog=epilog)
parser.add_argument('--devicedir', default='/media/KOBOeReader', help="directory of connected Kobo device")
parser.add_argument('--all', action='store_true', help="flag for converting all books on device")
args = vars(parser.parse_args())
serials = []
devicedir = u""
if args['devicedir']:
devicedir = args['devicedir']
lib = KoboLibrary(serials, devicedir)
if args['all']:
books = lib.books
else:
for i, book in enumerate(lib.books):
print u"{0}: {1}".format(i + 1, book.title)
print u"Or 'all'"
choice = raw_input(u"Convert book number... ")
if choice == u'all':
books = list(lib.books)
else:
try:
num = int(choice)
books = [lib.books[num - 1]]
except (ValueError, IndexError):
print u"Invalid choice. Exiting..."
exit()
results = [decrypt_book(book, lib) for book in books]
lib.close()
overall_result = all(result != 0 for result in results)
if overall_result != 0:
print u"Could not decrypt book with any of the keys found."
return overall_result
if __name__ == '__main__':
sys.stdout=SafeUnbuffered(sys.stdout)
sys.stderr=SafeUnbuffered(sys.stderr)
sys.exit(cli_main())

Binary file not shown.

View File

@@ -0,0 +1,8 @@
Rocket eBooks
=============
Rocket ebooks (.rb) are no longer sold.
This is the only archive of tools for decrypting Rocket ebooks that I have been able to find. It is included here without further comment or support.
— Alf.

View File

@@ -0,0 +1,4 @@
The latest Scuolabook tool can be found at Hex's own blog:
https://thisishex.wordpress.com/scuolabook-drm-remover/
Harper.

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