tools v5.5
Plugins now include unaltered stand-alone scripts, so no longer need to keep separate copies.
This commit is contained in:
@@ -41,7 +41,7 @@ Mac OS X 10.5 and above: You do
|
||||
\i not
|
||||
\i0 need to install Python.\
|
||||
\
|
||||
Drag the DeDRM application from from tools_v5.4.1\\DeDRM_Applications\\Macintosh (the location of this ReadMe) to your Applications folder, or anywhere else you find convenient.\
|
||||
Drag the DeDRM application from from tools_v5.5\\DeDRM_Applications\\Macintosh (the location of this ReadMe) to your Applications folder, or anywhere else you find convenient.\
|
||||
\
|
||||
\
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ global BNKeyGenTool
|
||||
global BNePubTool
|
||||
global AdobeKeyGenTool
|
||||
global AdobeePubTool
|
||||
global ePubTestTool
|
||||
global AdobePDFTool
|
||||
global ZipFixTool
|
||||
global ProgressApp
|
||||
@@ -42,7 +43,7 @@ on writetolog(logstring)
|
||||
try
|
||||
set fileRef to open for access logFilePath with write permission
|
||||
write logstring & "
|
||||
" to fileRef starting at eof
|
||||
" to fileRef starting at eof as <20>class utf8<66>
|
||||
close access fileRef
|
||||
end try
|
||||
end writetolog
|
||||
@@ -68,11 +69,7 @@ on readtemp()
|
||||
try
|
||||
set fileRef to open for access tempfilepath
|
||||
if (get eof fileRef) > 0 then
|
||||
set tempContentsLines to (read fileRef from 1 using delimiter (character id 10))
|
||||
set oldTIDs to AppleScript's text item delimiters
|
||||
set AppleScript's text item delimiters to (character id 13)
|
||||
set tempContents to tempContentsLines as string
|
||||
set AppleScript's text item delimiters to oldTIDs
|
||||
set tempContents to read fileRef from 1 as <20>class utf8<66>
|
||||
end if
|
||||
close access fileRef
|
||||
end try
|
||||
@@ -155,6 +152,7 @@ on GetTools()
|
||||
set BNePubTool to POSIX path of (path to resource "ignobleepub.py")
|
||||
set AdobeKeyGenTool to POSIX path of (path to resource "ineptkey.py")
|
||||
set AdobeePubTool to POSIX path of (path to resource "ineptepub.py")
|
||||
set ePubTestTool to POSIX path of (path to resource "epubtest.py")
|
||||
set AdobePDFTool to POSIX path of (path to resource "ineptpdf.py")
|
||||
set ZipFixTool to POSIX path of (path to resource "zipfix.py")
|
||||
set ProgressApp to POSIX path of (path to resource "DeDRM Progress.app")
|
||||
@@ -208,6 +206,12 @@ on GetTools()
|
||||
return false
|
||||
end if
|
||||
end if
|
||||
if not fileexists(ePubTestTool) then
|
||||
set dialogresult to (display dialog "The ePub encryption test script (epubtesttool.py) is missing from this package. Get a fresh copy." buttons {"Quit", "Continue Anyway"} default button 1 with title "DeDRM" with icon stop)
|
||||
if button returned of dialogresult is "Quit" then
|
||||
return false
|
||||
end if
|
||||
end if
|
||||
if not folderexists(ProgressApp) then
|
||||
set dialogresult to (display dialog "The Progress dialog application (DeDRM Progress.app) is missing from this package. Get a fresh copy." buttons {"Quit", "Continue Anyway"} default button 1 with title "DeDRM" with icon stop)
|
||||
if button returned of dialogresult is "Quit" then
|
||||
@@ -278,7 +282,8 @@ on unlockmobifile(encryptedFile)
|
||||
writetolog("shellresult: " & shellresult & " " & ErrorText)
|
||||
try
|
||||
repeat
|
||||
if (not DecodingError) or (totalebooks > 1) or (offset of "No key found" in ErrorText) is 0 then
|
||||
if (totalebooks > 1) or (offset of "No key found" in shellresult) is 0 then
|
||||
--display dialog (totalebooks as text) & shellresult
|
||||
exit repeat
|
||||
end if
|
||||
-- ask for another PID as we're only doing one ebook
|
||||
@@ -311,8 +316,7 @@ on unlockmobifile(encryptedFile)
|
||||
end repeat
|
||||
if DecodingError then
|
||||
set ErrorCount to ErrorCount + 1
|
||||
set ErrorList to ErrorList & fileName & fileExtension & " couldn't be decoded:
|
||||
" & (ErrorText as text) & "
|
||||
set ErrorList to ErrorList & fileName & fileExtension & " couldn't be decrypted.
|
||||
"
|
||||
else if (offset of "not encrypted" in shellresult) > 0 then
|
||||
set WarningCount to WarningCount + 1
|
||||
@@ -512,34 +516,66 @@ on unlockepubfile(encryptedFile)
|
||||
set shellresult to "no keys"
|
||||
set ErrorText to ""
|
||||
|
||||
-- first we'll try the Barnes & Noble keys
|
||||
repeat with BNKey in bnKeys
|
||||
|
||||
set keyfilepath to third item of BNKey
|
||||
if length of keyfilepath > 0 then
|
||||
set shellcommand to python & (quoted form of BNePubTool) & " " & (quoted form of keyfilepath) & " " & (quoted form of fixedFilePath) & " " & (quoted form of unlockedFilePath)
|
||||
set shellcommand to shellcommand & " > " & quotedtemppath()
|
||||
--display dialog "shellcommand: " & shellcommand buttons {"OK"} default button 1 giving up after 10
|
||||
writetolog("shellcommand: " & shellcommand)
|
||||
cleartemp()
|
||||
set DecodingError to false
|
||||
set ErrorText to ""
|
||||
try
|
||||
do shell script shellcommand
|
||||
on error ErrorText
|
||||
set DecodingError to true
|
||||
end try
|
||||
set shellresult to readtemp()
|
||||
writetolog("shellresult: " & shellresult & " " & ErrorText)
|
||||
--display dialog shellresult
|
||||
if not DecodingError then
|
||||
set decoded to "YES"
|
||||
exit repeat
|
||||
end if
|
||||
-- get encryption type
|
||||
set TryBandNePub to true
|
||||
set TryAdobeePub to true
|
||||
set shellcommand to python & (quoted form of ePubTestTool) & " " & (quoted form of fixedFilePath)
|
||||
set shellcommand to shellcommand & " > " & quotedtemppath()
|
||||
--display dialog "shellcommand: " & shellcommand buttons {"OK"} default button 1 giving up after 10
|
||||
writetolog("shellcommand: " & shellcommand)
|
||||
cleartemp()
|
||||
set TestError to false
|
||||
set ErrorText to ""
|
||||
try
|
||||
do shell script shellcommand
|
||||
on error ErrorText
|
||||
set TestError to true
|
||||
end try
|
||||
set shellresult to readtemp()
|
||||
writetolog("shellresult: " & shellresult & " " & ErrorText)
|
||||
--display dialog shellresult
|
||||
if not TestError then
|
||||
if (offset of "B&N" in shellresult) > 0 then
|
||||
set TryAdobeePub to false
|
||||
else if (offset of "Adobe" in shellresult) > 0 then
|
||||
set TryBandNePub to false
|
||||
else if (offset of "Unencrypted" in shellresult) > 0 then
|
||||
set TryAdobeePub to false
|
||||
set TryBandNePub to false
|
||||
end if
|
||||
end repeat
|
||||
end if
|
||||
|
||||
if decoded is "NO" then
|
||||
|
||||
-- first we'll try the Barnes & Noble keys
|
||||
if TryBandNePub then
|
||||
repeat with BNKey in bnKeys
|
||||
|
||||
set keyfilepath to third item of BNKey
|
||||
if length of keyfilepath > 0 then
|
||||
set shellcommand to python & (quoted form of BNePubTool) & " " & (quoted form of keyfilepath) & " " & (quoted form of fixedFilePath) & " " & (quoted form of unlockedFilePath)
|
||||
set shellcommand to shellcommand & " > " & quotedtemppath()
|
||||
--display dialog "shellcommand: " & shellcommand buttons {"OK"} default button 1 giving up after 10
|
||||
writetolog("shellcommand: " & shellcommand)
|
||||
cleartemp()
|
||||
set DecodingError to false
|
||||
set ErrorText to ""
|
||||
try
|
||||
do shell script shellcommand
|
||||
on error ErrorText
|
||||
set DecodingError to true
|
||||
end try
|
||||
set shellresult to readtemp()
|
||||
writetolog("shellresult: " & shellresult & " " & ErrorText)
|
||||
--display dialog shellresult
|
||||
if not DecodingError then
|
||||
set decoded to "YES"
|
||||
exit repeat
|
||||
end if
|
||||
end if
|
||||
end repeat
|
||||
end if
|
||||
|
||||
if decoded is "NO" and TryAdobeePub then
|
||||
-- now try Adobe ePub
|
||||
repeat with AdeptKey in AdeptKeyList
|
||||
set shellcommand to python & (quoted form of AdobeePubTool) & " " & (quoted form of AdeptKey) & " " & (quoted form of fixedFilePath) & " " & (quoted form of unlockedFilePath)
|
||||
@@ -567,13 +603,13 @@ on unlockepubfile(encryptedFile)
|
||||
if decoded is "YES" then
|
||||
set CompletedCount to CompletedCount + 1
|
||||
set CompletedList to CompletedList & fileName & fileExtension & paraend
|
||||
else if not TryAdobeePub and not TryBandNePub then
|
||||
set WarningCount to WarningCount + 1
|
||||
set WarningList to (WarningList & fileName & " doesn't seem to be encrypted.
|
||||
")
|
||||
else if shellresult is "no keys" then
|
||||
set ErrorCount to ErrorCount + 1
|
||||
set ErrorList to (ErrorList & fileName & fileExtension & " couldn't be decoded: no keys.
|
||||
")
|
||||
else if (offset of "not an ADEPT EPUB" in shellresult) is not 0 then
|
||||
set WarningCount to WarningCount + 1
|
||||
set WarningList to (WarningList & fileName & " doesn't seem to be encrypted.
|
||||
")
|
||||
else
|
||||
set ErrorCount to ErrorCount + 1
|
||||
@@ -942,7 +978,7 @@ Enter any additional Kindle Serial Numbers one at a time:"
|
||||
if button returned of dialogresult is "Add" then
|
||||
set Serial to text returned of dialogresult
|
||||
set Seriallength to length of Serial
|
||||
if Seriallength is 16 and (first character of Serial) is "B" then
|
||||
if Seriallength is 16 and ((first character of Serial) is "B" or (first character of Serial) is "9") then
|
||||
set KindleSerialList to KindleSerialList & Serial
|
||||
set Serial to ""
|
||||
else
|
||||
@@ -1528,7 +1564,7 @@ For full information about the licence, please see http://unlicense.org/
|
||||
The application icon is adapted from the Authors Against DRM logo at
|
||||
http://readersbillofrights.info/AAD and is under the Creative Commons Attribution-ShareAlike licence.
|
||||
The included Python scripts are all free to use, but have a variety of licences. See the individual files for details.
|
||||
" with title "DeDRM 5.2 by Apprentice Alf" buttons {"Close", "Select Ebook<6F> ", "Configure<72> "} default button 1 with icon note
|
||||
" with title "DeDRM by Apprentice Alf" buttons {"Close", "Select Ebook<6F> ", "Configure<72> "} default button 1 with icon note
|
||||
ReadPrefs()
|
||||
clearlog()
|
||||
GetAdeptKey(false)
|
||||
|
||||
@@ -24,17 +24,17 @@
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>droplet</string>
|
||||
<key>CFBundleGetInfoString</key>
|
||||
<string>DeDRM 5.4.1. AppleScript written 2010–2012 by Apprentice Alf and others.</string>
|
||||
<string>DeDRM 5.5. AppleScript written 2010–2012 by Apprentice Alf and others.</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>DeDRM</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>DeDRM 5.4.1</string>
|
||||
<string>DeDRM 5.5</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>5.4.1</string>
|
||||
<string>5.5</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>dplt</string>
|
||||
<key>LSRequiresCarbon</key>
|
||||
@@ -50,7 +50,7 @@
|
||||
<key>positionOfDivider</key>
|
||||
<real>0</real>
|
||||
<key>savedFrame</key>
|
||||
<string>287 405 800 473 0 0 1440 878 </string>
|
||||
<string>1846 -16 800 473 1440 -180 1920 1080 </string>
|
||||
<key>selectedTabView</key>
|
||||
<string>event log</string>
|
||||
</dict>
|
||||
|
||||
Binary file not shown.
@@ -1,11 +1,18 @@
|
||||
#! /usr/bin/env python
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# crypto library mainly by some_updates
|
||||
|
||||
# pbkdf2.py pbkdf2 code taken from pbkdf2.py
|
||||
# pbkdf2.py Copyright © 2004 Matt Johnston <matt @ ucc asn au>
|
||||
# pbkdf2.py Copyright © 2009 Daniel Holth <dholth@fastmail.fm>
|
||||
# pbkdf2.py This code may be freely used and modified for any purpose.
|
||||
|
||||
import sys, os
|
||||
import hmac
|
||||
from struct import pack
|
||||
import hashlib
|
||||
|
||||
|
||||
# interface to needed routines libalfcrypto
|
||||
def _load_libalfcrypto():
|
||||
import ctypes
|
||||
@@ -26,8 +33,8 @@ def _load_libalfcrypto():
|
||||
name_of_lib = 'libalfcrypto32.so'
|
||||
else:
|
||||
name_of_lib = 'libalfcrypto64.so'
|
||||
|
||||
libalfcrypto = sys.path[0] + os.sep + name_of_lib
|
||||
|
||||
libalfcrypto = os.path.join(sys.path[0],name_of_lib)
|
||||
|
||||
if not os.path.isfile(libalfcrypto):
|
||||
raise Exception('libalfcrypto not found')
|
||||
@@ -55,7 +62,7 @@ def _load_libalfcrypto():
|
||||
#
|
||||
# int AES_set_decrypt_key(const unsigned char *userKey, const int bits, AES_KEY *key);
|
||||
#
|
||||
#
|
||||
#
|
||||
# void AES_cbc_encrypt(const unsigned char *in, unsigned char *out,
|
||||
# const unsigned long length, const AES_KEY *key,
|
||||
# unsigned char *ivec, const int enc);
|
||||
@@ -147,7 +154,7 @@ def _load_libalfcrypto():
|
||||
topazCryptoDecrypt(ctx, data, out, len(data))
|
||||
return out.raw
|
||||
|
||||
print "Using Library AlfCrypto DLL/DYLIB/SO"
|
||||
print u"Using Library AlfCrypto DLL/DYLIB/SO"
|
||||
return (AES_CBC, Pukall_Cipher, Topaz_Cipher)
|
||||
|
||||
|
||||
@@ -164,8 +171,7 @@ def _load_python_alfcrypto():
|
||||
sum2 = 0;
|
||||
keyXorVal = 0;
|
||||
if len(key)!=16:
|
||||
print "Bad key length!"
|
||||
return None
|
||||
raise Exception('Pukall_Cipher: Bad key length.')
|
||||
wkey = []
|
||||
for i in xrange(8):
|
||||
wkey.append(ord(key[i*2])<<8 | ord(key[i*2+1]))
|
||||
@@ -234,6 +240,7 @@ def _load_python_alfcrypto():
|
||||
cleartext = self.aes.decrypt(iv + data)
|
||||
return cleartext
|
||||
|
||||
print u"Using Library AlfCrypto Python"
|
||||
return (AES_CBC, Pukall_Cipher, Topaz_Cipher)
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from PyQt4.Qt import QWidget, QVBoxLayout, QLabel, QLineEdit
|
||||
|
||||
from calibre.utils.config import JSONConfig
|
||||
|
||||
@@ -230,6 +230,7 @@ class PageParser(object):
|
||||
'empty' : (1, 'snippets', 1, 0),
|
||||
|
||||
'page' : (1, 'snippets', 1, 0),
|
||||
'page.class' : (1, 'scalar_text', 0, 0),
|
||||
'page.pageid' : (1, 'scalar_text', 0, 0),
|
||||
'page.pagelabel' : (1, 'scalar_text', 0, 0),
|
||||
'page.type' : (1, 'scalar_text', 0, 0),
|
||||
@@ -238,11 +239,13 @@ class PageParser(object):
|
||||
'page.startID' : (1, 'scalar_number', 0, 0),
|
||||
|
||||
'group' : (1, 'snippets', 1, 0),
|
||||
'group.class' : (1, 'scalar_text', 0, 0),
|
||||
'group.type' : (1, 'scalar_text', 0, 0),
|
||||
'group._tag' : (1, 'scalar_text', 0, 0),
|
||||
'group.orientation': (1, 'scalar_text', 0, 0),
|
||||
|
||||
'region' : (1, 'snippets', 1, 0),
|
||||
'region.class' : (1, 'scalar_text', 0, 0),
|
||||
'region.type' : (1, 'scalar_text', 0, 0),
|
||||
'region.x' : (1, 'scalar_number', 0, 0),
|
||||
'region.y' : (1, 'scalar_number', 0, 0),
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# This is a python script. You need a Python interpreter to run it.
|
||||
# For example, ActiveState Python, which exists for windows.
|
||||
#
|
||||
# Changelog drmcheck
|
||||
# 1.00 - Initial version, with code from various other scripts
|
||||
# 1.01 - Moved authorship announcement to usage section.
|
||||
#
|
||||
# Changelog drmcheck
|
||||
# 1.00 - Cut to drmtest.py, testing ePub files only by Apprentice Alf
|
||||
#
|
||||
# Written in 2011 by Paul Durrant
|
||||
# Released with unlicense. See http://unlicense.org/
|
||||
#
|
||||
#############################################################################
|
||||
#
|
||||
# This is free and unencumbered software released into the public domain.
|
||||
#
|
||||
# Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||
# distribute this software, either in source code form or as a compiled
|
||||
# binary, for any purpose, commercial or non-commercial, and by any
|
||||
# means.
|
||||
#
|
||||
# In jurisdictions that recognize copyright laws, the author or authors
|
||||
# of this software dedicate any and all copyright interest in the
|
||||
# software to the public domain. We make this dedication for the benefit
|
||||
# of the public at large and to the detriment of our heirs and
|
||||
# successors. We intend this dedication to be an overt act of
|
||||
# relinquishment in perpetuity of all present and future rights to this
|
||||
# software under copyright law.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||
# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
# OTHER DEALINGS IN THE SOFTWARE.
|
||||
#
|
||||
#############################################################################
|
||||
#
|
||||
# It's still polite to give attribution if you do reuse this code.
|
||||
#
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
__version__ = '1.00'
|
||||
|
||||
import sys, struct, os
|
||||
import zlib
|
||||
import zipfile
|
||||
import xml.etree.ElementTree as etree
|
||||
|
||||
NSMAP = {'adept': 'http://ns.adobe.com/adept',
|
||||
'enc': 'http://www.w3.org/2001/04/xmlenc#'}
|
||||
|
||||
# Wrap a stream so that output gets flushed immediately
|
||||
# and also make sure that any unicode strings get
|
||||
# encoded using "replace" before writing them.
|
||||
class SafeUnbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
self.encoding = stream.encoding
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
def write(self, data):
|
||||
if isinstance(data,unicode):
|
||||
data = data.encode(self.encoding,"replace")
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
def unicode_argv():
|
||||
argvencoding = sys.stdin.encoding
|
||||
if argvencoding == None:
|
||||
argvencoding = "utf-8"
|
||||
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
|
||||
|
||||
_FILENAME_LEN_OFFSET = 26
|
||||
_EXTRA_LEN_OFFSET = 28
|
||||
_FILENAME_OFFSET = 30
|
||||
_MAX_SIZE = 64 * 1024
|
||||
|
||||
|
||||
def uncompress(cmpdata):
|
||||
dc = zlib.decompressobj(-15)
|
||||
data = ''
|
||||
while len(cmpdata) > 0:
|
||||
if len(cmpdata) > _MAX_SIZE :
|
||||
newdata = cmpdata[0:_MAX_SIZE]
|
||||
cmpdata = cmpdata[_MAX_SIZE:]
|
||||
else:
|
||||
newdata = cmpdata
|
||||
cmpdata = ''
|
||||
newdata = dc.decompress(newdata)
|
||||
unprocessed = dc.unconsumed_tail
|
||||
if len(unprocessed) == 0:
|
||||
newdata += dc.flush()
|
||||
data += newdata
|
||||
cmpdata += unprocessed
|
||||
unprocessed = ''
|
||||
return data
|
||||
|
||||
def getfiledata(file, zi):
|
||||
# get file name length and exta data length to find start of file data
|
||||
local_header_offset = zi.header_offset
|
||||
|
||||
file.seek(local_header_offset + _FILENAME_LEN_OFFSET)
|
||||
leninfo = file.read(2)
|
||||
local_name_length, = struct.unpack('<H', leninfo)
|
||||
|
||||
file.seek(local_header_offset + _EXTRA_LEN_OFFSET)
|
||||
exinfo = file.read(2)
|
||||
extra_field_length, = struct.unpack('<H', exinfo)
|
||||
|
||||
file.seek(local_header_offset + _FILENAME_OFFSET + local_name_length + extra_field_length)
|
||||
data = None
|
||||
|
||||
# if not compressed we are good to go
|
||||
if zi.compress_type == zipfile.ZIP_STORED:
|
||||
data = file.read(zi.file_size)
|
||||
|
||||
# if compressed we must decompress it using zlib
|
||||
if zi.compress_type == zipfile.ZIP_DEFLATED:
|
||||
cmpdata = file.read(zi.compress_size)
|
||||
data = uncompress(cmpdata)
|
||||
|
||||
return data
|
||||
|
||||
def main(argv=unicode_argv()):
|
||||
infile = argv[1]
|
||||
kind = "Unknown"
|
||||
encryption = "Unknown"
|
||||
with file(infile,'rb') as infileobject:
|
||||
bookdata = infileobject.read(58)
|
||||
# Check for Mobipocket/Kindle
|
||||
if bookdata[0:0+2] == "PK":
|
||||
if bookdata[30:30+28] == 'mimetypeapplication/epub+zip':
|
||||
kind = "ePub"
|
||||
else:
|
||||
kind = "ZIP"
|
||||
encryption = "Unencrypted"
|
||||
foundrights = False
|
||||
foundencryption = False
|
||||
inzip = zipfile.ZipFile(infile,'r')
|
||||
namelist = set(inzip.namelist())
|
||||
if 'META-INF/rights.xml' not in namelist or 'META-INF/encryption.xml' not in namelist:
|
||||
encryption = "Unencrypted"
|
||||
else:
|
||||
rights = etree.fromstring(inzip.read('META-INF/rights.xml'))
|
||||
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
||||
expr = './/%s' % (adept('encryptedKey'),)
|
||||
bookkey = ''.join(rights.findtext(expr))
|
||||
if len(bookkey) == 172:
|
||||
encryption = "Adobe"
|
||||
elif len(bookkey) == 64:
|
||||
encryption = "B&N"
|
||||
else:
|
||||
encryption = "Unknown"
|
||||
|
||||
print u"{0} {1}".format(encryption, kind)
|
||||
return 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
sys.exit(main())
|
||||
@@ -1,8 +1,11 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
|
||||
#
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# erdr2pml.py
|
||||
# Copyright © 2008 The Dark Reverser
|
||||
#
|
||||
# Modified 2008–2012 by some_updates, DiapDealer and Apprentice Alf
|
||||
|
||||
# This is a python script. You need a Python interpreter to run it.
|
||||
# For example, ActiveState Python, which exists for windows.
|
||||
# Changelog
|
||||
@@ -62,19 +65,11 @@
|
||||
# 0.21 - Support eReader (drm) version 11.
|
||||
# - Don't reject dictionary format.
|
||||
# - Ignore sidebars for dictionaries (different format?)
|
||||
# 0.22 - Unicode and plugin support, different image folders for PMLZ and source
|
||||
|
||||
__version__='0.21'
|
||||
__version__='0.22'
|
||||
|
||||
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
|
||||
import sys, re
|
||||
import struct, binascii, getopt, zlib, os, os.path, urllib, tempfile
|
||||
|
||||
if 'calibre' in sys.modules:
|
||||
@@ -82,8 +77,66 @@ if 'calibre' in sys.modules:
|
||||
else:
|
||||
inCalibre = False
|
||||
|
||||
# Wrap a stream so that output gets flushed immediately
|
||||
# and also make sure that any unicode strings get
|
||||
# encoded using "replace" before writing them.
|
||||
class SafeUnbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
self.encoding = stream.encoding
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
def write(self, data):
|
||||
if isinstance(data,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]
|
||||
|
||||
Des = None
|
||||
if sys.platform.startswith('win'):
|
||||
if iswindows:
|
||||
# first try with pycrypto
|
||||
if inCalibre:
|
||||
from calibre_plugins.erdrpdb2pml import pycrypto_des
|
||||
@@ -168,17 +221,30 @@ class Sectionizer(object):
|
||||
off = self.sections[section][0]
|
||||
return self.contents[off:end_off]
|
||||
|
||||
def sanitizeFileName(s):
|
||||
r = ''
|
||||
for c in s:
|
||||
if c in "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_.-":
|
||||
r += c
|
||||
return r
|
||||
# cleanup unicode filenames
|
||||
# borrowed from calibre from calibre/src/calibre/__init__.py
|
||||
# added in removal of control (<32) chars
|
||||
# and removal of . at start and end
|
||||
# and with some (heavily edited) code from Paul Durrant's kindlenamer.py
|
||||
def sanitizeFileName(name):
|
||||
# substitute filename unfriendly characters
|
||||
name = name.replace(u"<",u"[").replace(u">",u"]").replace(u" : ",u" – ").replace(u": ",u" – ").replace(u":",u"—").replace(u"/",u"_").replace(u"\\",u"_").replace(u"|",u"_").replace(u"\"",u"\'")
|
||||
# delete control characters
|
||||
name = u"".join(char for char in name if ord(char)>=32)
|
||||
# white space to single space, delete leading and trailing while space
|
||||
name = re.sub(ur"\s", u" ", name).strip()
|
||||
# remove leading dots
|
||||
while len(name)>0 and name[0] == u".":
|
||||
name = name[1:]
|
||||
# remove trailing dots (Windows doesn't like them)
|
||||
if name.endswith(u'.'):
|
||||
name = name[:-1]
|
||||
return name
|
||||
|
||||
def fixKey(key):
|
||||
def fixByte(b):
|
||||
return b ^ ((b ^ (b<<1) ^ (b<<2) ^ (b<<3) ^ (b<<4) ^ (b<<5) ^ (b<<6) ^ (b<<7) ^ 0x80) & 0x80)
|
||||
return "".join([chr(fixByte(ord(a))) for a in key])
|
||||
return "".join([chr(fixByte(ord(a))) for a in key])
|
||||
|
||||
def deXOR(text, sp, table):
|
||||
r=''
|
||||
@@ -191,7 +257,7 @@ def deXOR(text, sp, table):
|
||||
return r
|
||||
|
||||
class EreaderProcessor(object):
|
||||
def __init__(self, sect, username, creditcard):
|
||||
def __init__(self, sect, user_key):
|
||||
self.section_reader = sect.loadSection
|
||||
data = self.section_reader(0)
|
||||
version, = struct.unpack('>H', data[0:2])
|
||||
@@ -212,18 +278,10 @@ class EreaderProcessor(object):
|
||||
for i in xrange(len(data)):
|
||||
j = (j + shuf) % len(data)
|
||||
r[j] = data[i]
|
||||
assert len("".join(r)) == len(data)
|
||||
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]
|
||||
@@ -302,7 +360,7 @@ class EreaderProcessor(object):
|
||||
sect = self.section_reader(self.first_image_page + i)
|
||||
name = sect[4:4+32].strip('\0')
|
||||
data = sect[62:]
|
||||
return sanitizeFileName(name), data
|
||||
return sanitizeFileName(unicode(name,'windows-1252')), data
|
||||
|
||||
|
||||
# def getChapterNamePMLOffsetData(self):
|
||||
@@ -399,60 +457,53 @@ class EreaderProcessor(object):
|
||||
return r
|
||||
|
||||
def cleanPML(pml):
|
||||
# Convert special characters to proper PML code. High ASCII start at (\x80, \a128) and go up to (\xff, \a255)
|
||||
# Convert special characters to proper PML code. High ASCII start at (\x80, \a128) and go up to (\xff, \a255)
|
||||
pml2 = pml
|
||||
for k in xrange(128,256):
|
||||
badChar = chr(k)
|
||||
pml2 = pml2.replace(badChar, '\\a%03d' % k)
|
||||
return pml2
|
||||
|
||||
def convertEreaderToPml(infile, name, cc, outdir):
|
||||
if not os.path.exists(outdir):
|
||||
os.makedirs(outdir)
|
||||
def decryptBook(infile, outpath, make_pmlz, user_key):
|
||||
bookname = os.path.splitext(os.path.basename(infile))[0]
|
||||
print " Decoding File"
|
||||
sect = Sectionizer(infile, 'PNRdPPrs')
|
||||
er = EreaderProcessor(sect, name, cc)
|
||||
|
||||
if er.getNumImages() > 0:
|
||||
print " Extracting images"
|
||||
imagedir = bookname + '_img/'
|
||||
imagedirpath = os.path.join(outdir,imagedir)
|
||||
if not os.path.exists(imagedirpath):
|
||||
os.makedirs(imagedirpath)
|
||||
for i in xrange(er.getNumImages()):
|
||||
name, contents = er.getImage(i)
|
||||
file(os.path.join(imagedirpath, name), 'wb').write(contents)
|
||||
|
||||
print " Extracting pml"
|
||||
pml_string = er.getText()
|
||||
pmlfilename = bookname + ".pml"
|
||||
file(os.path.join(outdir, pmlfilename),'wb').write(cleanPML(pml_string))
|
||||
|
||||
# bkinfo = er.getBookInfo()
|
||||
# if bkinfo != '':
|
||||
# print " Extracting book meta information"
|
||||
# file(os.path.join(outdir, 'bookinfo.txt'),'wb').write(bkinfo)
|
||||
|
||||
|
||||
|
||||
def decryptBook(infile, outdir, name, cc, make_pmlz):
|
||||
if make_pmlz :
|
||||
# ignore specified outdir, use tempdir instead
|
||||
if make_pmlz:
|
||||
# outpath is actually pmlz name
|
||||
pmlzname = outpath
|
||||
outdir = tempfile.mkdtemp()
|
||||
imagedirpath = os.path.join(outdir,u"images")
|
||||
else:
|
||||
pmlzname = None
|
||||
outdir = outpath
|
||||
imagedirpath = os.path.join(outdir,bookname + u"_img")
|
||||
|
||||
try:
|
||||
print "Processing..."
|
||||
convertEreaderToPml(infile, name, cc, outdir)
|
||||
if make_pmlz :
|
||||
if not os.path.exists(outdir):
|
||||
os.makedirs(outdir)
|
||||
print u"Decoding File"
|
||||
sect = Sectionizer(infile, 'PNRdPPrs')
|
||||
er = EreaderProcessor(sect, user_key)
|
||||
|
||||
if er.getNumImages() > 0:
|
||||
print u"Extracting images"
|
||||
if not os.path.exists(imagedirpath):
|
||||
os.makedirs(imagedirpath)
|
||||
for i in xrange(er.getNumImages()):
|
||||
name, contents = er.getImage(i)
|
||||
file(os.path.join(imagedirpath, name), 'wb').write(contents)
|
||||
|
||||
print u"Extracting pml"
|
||||
pml_string = er.getText()
|
||||
pmlfilename = bookname + ".pml"
|
||||
file(os.path.join(outdir, pmlfilename),'wb').write(cleanPML(pml_string))
|
||||
if pmlzname is not None:
|
||||
import zipfile
|
||||
import shutil
|
||||
print " Creating PMLZ file"
|
||||
zipname = infile[:-4] + '.pmlz'
|
||||
myZipFile = zipfile.ZipFile(zipname,'w',zipfile.ZIP_STORED, False)
|
||||
print u"Creating PMLZ file {0}".format(os.path.basename(pmlzname))
|
||||
myZipFile = zipfile.ZipFile(pmlzname,'w',zipfile.ZIP_STORED, False)
|
||||
list = os.listdir(outdir)
|
||||
for file in list:
|
||||
localname = file
|
||||
filePath = os.path.join(outdir,file)
|
||||
for filename in list:
|
||||
localname = filename
|
||||
filePath = os.path.join(outdir,filename)
|
||||
if os.path.isfile(filePath):
|
||||
myZipFile.write(filePath, localname)
|
||||
elif os.path.isdir(filePath):
|
||||
@@ -466,36 +517,46 @@ def decryptBook(infile, outdir, name, cc, make_pmlz):
|
||||
myZipFile.close()
|
||||
# remove temporary directory
|
||||
shutil.rmtree(outdir, True)
|
||||
print 'output is %s' % zipname
|
||||
print u"Output is {0}".format(pmlzname)
|
||||
else :
|
||||
print 'output in %s' % outdir
|
||||
print u"Output is in {0}".format(outdir)
|
||||
print "done"
|
||||
except ValueError, e:
|
||||
print "Error: %s" % e
|
||||
print u"Error: {0}".format(e.args[0])
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
def usage():
|
||||
print "Converts DRMed eReader books to PML Source"
|
||||
print "Usage:"
|
||||
print " erdr2pml [options] infile.pdb [outdir] \"your name\" credit_card_number "
|
||||
print " "
|
||||
print "Options: "
|
||||
print " -h prints this message"
|
||||
print " --make-pmlz create PMLZ instead of using output directory"
|
||||
print " "
|
||||
print "Note:"
|
||||
print " if ommitted, outdir defaults based on 'infile.pdb'"
|
||||
print " It's enough to enter the last 8 digits of the credit card number"
|
||||
print u"Converts DRMed eReader books to PML Source"
|
||||
print u"Usage:"
|
||||
print u" erdr2pml [options] infile.pdb [outpath] \"your name\" credit_card_number"
|
||||
print u" "
|
||||
print u"Options: "
|
||||
print u" -h prints this message"
|
||||
print u" -p create PMLZ instead of source folder"
|
||||
print u" --make-pmlz create PMLZ instead of source folder"
|
||||
print u" "
|
||||
print u"Note:"
|
||||
print u" if outpath is ommitted, creates source in 'infile_Source' folder"
|
||||
print u" if outpath is ommitted and pmlz option, creates PMLZ 'infile.pmlz'"
|
||||
print u" if source folder created, images are in infile_img folder"
|
||||
print u" if pmlz file created, images are in images folder"
|
||||
print u" It's enough to enter the last 8 digits of the credit card number"
|
||||
return
|
||||
|
||||
def getuser_key(name,cc):
|
||||
newname = "".join(c for c in name.lower() if c >= 'a' and c <= 'z' or c >= '0' and c <= '9')
|
||||
cc = cc.replace(" ","")
|
||||
return struct.pack('>LL', binascii.crc32(newname) & 0xffffffff,binascii.crc32(cc[-8:])& 0xffffffff)
|
||||
|
||||
def cli_main(argv=unicode_argv()):
|
||||
print u"eRdr2Pml v{0}. Copyright © 2009–2012 The Dark Reverser et al.".format(__version__)
|
||||
|
||||
def main(argv=None):
|
||||
try:
|
||||
opts, args = getopt.getopt(sys.argv[1:], "h", ["make-pmlz"])
|
||||
opts, args = getopt.getopt(argv[1:], "hp", ["make-pmlz"])
|
||||
except getopt.GetoptError, err:
|
||||
print str(err)
|
||||
print err.args[0]
|
||||
usage()
|
||||
return 1
|
||||
make_pmlz = False
|
||||
@@ -503,24 +564,31 @@ def main(argv=None):
|
||||
if o == "-h":
|
||||
usage()
|
||||
return 0
|
||||
elif o == "-p":
|
||||
make_pmlz = True
|
||||
elif o == "--make-pmlz":
|
||||
make_pmlz = True
|
||||
|
||||
print "eRdr2Pml v%s. Copyright (c) 2009 The Dark Reverser" % __version__
|
||||
|
||||
if len(args)!=3 and len(args)!=4:
|
||||
usage()
|
||||
return 1
|
||||
|
||||
if len(args)==3:
|
||||
infile, name, cc = args[0], args[1], args[2]
|
||||
outdir = infile[:-4] + '_Source'
|
||||
infile, name, cc = args
|
||||
if make_pmlz:
|
||||
outpath = os.path.splitext(infile)[0] + u".pmlz"
|
||||
else:
|
||||
outpath = os.path.splitext(infile)[0] + u"_Source"
|
||||
elif len(args)==4:
|
||||
infile, outdir, name, cc = args[0], args[1], args[2], args[3]
|
||||
infile, outpath, name, cc = args
|
||||
|
||||
return decryptBook(infile, outdir, name, cc, make_pmlz)
|
||||
print getuser_key(name,cc).encode('hex')
|
||||
|
||||
return decryptBook(infile, outpath, make_pmlz, getuser_key(name,cc))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.stdout=Unbuffered(sys.stdout)
|
||||
sys.exit(main())
|
||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
sys.exit(cli_main())
|
||||
|
||||
|
||||
@@ -1,13 +1,25 @@
|
||||
#! /usr/bin/python
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
# ignobleepub.pyw, version 3.5
|
||||
# ignobleepub.pyw, version 3.6
|
||||
# Copyright © 2009-2010 by i♥cabbages
|
||||
|
||||
# To run this program install Python 2.6 from <http://www.python.org/download/>
|
||||
# and OpenSSL or 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.
|
||||
# Released under the terms of the GNU General Public Licence, version 3
|
||||
# <http://www.gnu.org/licenses/>
|
||||
|
||||
# Modified 2010–2012 by some_updates, DiapDealer and Apprentice Alf
|
||||
|
||||
# Windows users: Before running this program, you must first install Python 2.6
|
||||
# from <http://www.python.org/download/> and PyCrypto from
|
||||
# <http://www.voidspace.org.uk/python/modules.shtml#pycrypto> (make sure to
|
||||
# install the version for Python 2.6). Save this script file as
|
||||
# ineptepub.pyw and double-click on it to run it.
|
||||
#
|
||||
# Mac OS X users: Save this script file as ineptepub.pyw. You can run this
|
||||
# program from the command line (pythonw ineptepub.pyw) or by double-clicking
|
||||
# it when it has been associated with PythonLauncher.
|
||||
|
||||
# Revision history:
|
||||
# 1 - Initial release
|
||||
@@ -18,21 +30,83 @@ from __future__ import with_statement
|
||||
# 3.3 - On Windows try PyCrypto first and OpenSSL next
|
||||
# 3.4 - Modify interace to allow use with import
|
||||
# 3.5 - Fix for potential problem with PyCrypto
|
||||
# 3.6 - Revised to allow use in calibre plugins to eliminate need for duplicate code
|
||||
|
||||
"""
|
||||
Decrypt Barnes & Noble encrypted ePub books.
|
||||
"""
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = "3.6"
|
||||
|
||||
import sys
|
||||
import os
|
||||
import traceback
|
||||
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
|
||||
|
||||
# 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 '?'.
|
||||
|
||||
|
||||
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)]
|
||||
return [u"ineptepub.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
|
||||
@@ -42,10 +116,11 @@ def _load_crypto_libcrypto():
|
||||
Structure, c_ulong, create_string_buffer, cast
|
||||
from ctypes.util import find_library
|
||||
|
||||
if sys.platform.startswith('win'):
|
||||
if iswindows:
|
||||
libcrypto = find_library('libeay32')
|
||||
else:
|
||||
libcrypto = find_library('crypto')
|
||||
|
||||
if libcrypto is None:
|
||||
raise IGNOBLEError('libcrypto not found')
|
||||
libcrypto = CDLL(libcrypto)
|
||||
@@ -66,9 +141,6 @@ def _load_crypto_libcrypto():
|
||||
func.argtypes = argtypes
|
||||
return func
|
||||
|
||||
AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',
|
||||
[c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p,
|
||||
c_int])
|
||||
AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key',
|
||||
[c_char_p, c_int, AES_KEY_p])
|
||||
AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',
|
||||
@@ -123,13 +195,6 @@ def _load_crypto():
|
||||
|
||||
AES = _load_crypto()
|
||||
|
||||
|
||||
|
||||
"""
|
||||
Decrypt Barnes & Noble ADEPT encrypted EPUB books.
|
||||
"""
|
||||
|
||||
|
||||
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#'}
|
||||
@@ -144,7 +209,6 @@ class ZipInfo(zipfile.ZipInfo):
|
||||
class Decryptor(object):
|
||||
def __init__(self, bookkey, encryption):
|
||||
enc = lambda tag: '{%s}%s' % (NSMAP['enc'], tag)
|
||||
# self._aes = AES.new(bookkey, AES.MODE_CBC, '\x00'*16)
|
||||
self._aes = AES(bookkey)
|
||||
encryption = etree.fromstring(encryption)
|
||||
self._encrypted = encrypted = set()
|
||||
@@ -152,8 +216,8 @@ class Decryptor(object):
|
||||
enc('CipherReference'))
|
||||
for elem in encryption.findall(expr):
|
||||
path = elem.get('URI', None)
|
||||
path = path.encode('utf-8')
|
||||
if path is not None:
|
||||
path = path.encode('utf-8')
|
||||
encrypted.add(path)
|
||||
|
||||
def decompress(self, bytes):
|
||||
@@ -171,167 +235,186 @@ class Decryptor(object):
|
||||
data = self.decompress(data)
|
||||
return data
|
||||
|
||||
|
||||
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 decryptBook(keypath, inpath, outpath):
|
||||
with open(keypath, 'rb') as f:
|
||||
keyb64 = f.read()
|
||||
key = keyb64.decode('base64')[:16]
|
||||
# aes = AES.new(key, AES.MODE_CBC, '\x00'*16)
|
||||
aes = AES(key)
|
||||
|
||||
# check file to make check whether it's probably an Adobe Adept encrypted ePub
|
||||
def ignobleBook(inpath):
|
||||
with closing(ZipFile(open(inpath, 'rb'))) as inf:
|
||||
namelist = set(inf.namelist())
|
||||
if 'META-INF/rights.xml' not in namelist or \
|
||||
'META-INF/encryption.xml' not in namelist:
|
||||
raise IGNOBLEError('%s: not an B&N ADEPT EPUB' % (inpath,))
|
||||
return False
|
||||
try:
|
||||
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
|
||||
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
||||
expr = './/%s' % (adept('encryptedKey'),)
|
||||
bookkey = ''.join(rights.findtext(expr))
|
||||
if len(bookkey) == 64:
|
||||
return True
|
||||
except:
|
||||
# if we couldn't check, assume it is
|
||||
return True
|
||||
return False
|
||||
|
||||
# return error code and error message duple
|
||||
def decryptBook(keyb64, inpath, outpath):
|
||||
if AES is None:
|
||||
# 1 means don't try again
|
||||
return (1, u"PyCrypto or OpenSSL must be installed.")
|
||||
key = keyb64.decode('base64')[:16]
|
||||
aes = AES(key)
|
||||
with closing(ZipFile(open(inpath, 'rb'))) as inf:
|
||||
namelist = set(inf.namelist())
|
||||
if 'META-INF/rights.xml' not in namelist or \
|
||||
'META-INF/encryption.xml' not in namelist:
|
||||
return (1, u"Not a secure Barnes & Noble ePub.")
|
||||
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
|
||||
try:
|
||||
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
|
||||
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
||||
expr = './/%s' % (adept('encryptedKey'),)
|
||||
bookkey = ''.join(rights.findtext(expr))
|
||||
if len(bookkey) != 64:
|
||||
return (1, u"Not a secure Barnes & Noble ePub.")
|
||||
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))
|
||||
except Exception, e:
|
||||
return (2, u"{0}.".format(e.args[0]))
|
||||
return (0, u"Success")
|
||||
|
||||
|
||||
def cli_main(argv=sys.argv):
|
||||
def cli_main(argv=unicode_argv()):
|
||||
progname = os.path.basename(argv[0])
|
||||
if AES is None:
|
||||
print "%s: This script requires OpenSSL or PyCrypto, which must be installed " \
|
||||
"separately. Read the top-of-script comment for details." % \
|
||||
(progname,)
|
||||
return 1
|
||||
if len(argv) != 4:
|
||||
print "usage: %s KEYFILE INBOOK OUTBOOK" % (progname,)
|
||||
print u"usage: {0} <keyfile.der> <inbook.epub> <outbook.epub>".format(progname)
|
||||
return 1
|
||||
keypath, inpath, outpath = argv[1:]
|
||||
return decryptBook(keypath, inpath, outpath)
|
||||
|
||||
userkey = open(keypath,'rb').read()
|
||||
result = decryptBook(userkey, inpath, outpath)
|
||||
print result[1]
|
||||
return result[0]
|
||||
|
||||
def gui_main():
|
||||
import Tkinter
|
||||
import Tkconstants
|
||||
import tkFileDialog
|
||||
import traceback
|
||||
|
||||
class DecryptionDialog(Tkinter.Frame):
|
||||
def __init__(self, root):
|
||||
Tkinter.Frame.__init__(self, root, border=5)
|
||||
self.status = Tkinter.Label(self, text=u"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=u"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(u"bnepubkey.b64"):
|
||||
self.keypath.insert(0, u"bnepubkey.b64")
|
||||
button = Tkinter.Button(body, text=u"...", command=self.get_keypath)
|
||||
button.grid(row=0, column=2)
|
||||
Tkinter.Label(body, text=u"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=u"...", command=self.get_inpath)
|
||||
button.grid(row=1, column=2)
|
||||
Tkinter.Label(body, text=u"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=u"...", command=self.get_outpath)
|
||||
button.grid(row=2, column=2)
|
||||
buttons = Tkinter.Frame(self)
|
||||
buttons.pack()
|
||||
botton = Tkinter.Button(
|
||||
buttons, text=u"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=u"Quit", width=10, command=self.quit)
|
||||
button.pack(side=Tkconstants.RIGHT)
|
||||
|
||||
def get_keypath(self):
|
||||
keypath = tkFileDialog.askopenfilename(
|
||||
parent=None, title=u"Select Barnes & Noble \'.b64\' key file",
|
||||
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 get_inpath(self):
|
||||
inpath = tkFileDialog.askopenfilename(
|
||||
parent=None, title=u"Select B&N-encrypted ePub file to decrypt",
|
||||
defaultextension=u".epub", filetypes=[('ePub files', '.epub')])
|
||||
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=u"Select unencrypted ePub file to produce",
|
||||
defaultextension=u".epub", filetypes=[('ePub files', '.epub')])
|
||||
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'] = u"Specified key file does not exist"
|
||||
return
|
||||
if not inpath or not os.path.exists(inpath):
|
||||
self.status['text'] = u"Specified input file does not exist"
|
||||
return
|
||||
if not outpath:
|
||||
self.status['text'] = u"Output file not specified"
|
||||
return
|
||||
if inpath == outpath:
|
||||
self.status['text'] = u"Must have different input and output files"
|
||||
return
|
||||
userkey = open(keypath,'rb').read()
|
||||
self.status['text'] = u"Decrypting..."
|
||||
try:
|
||||
decrypt_status = decryptBook(userkey, inpath, outpath)
|
||||
except Exception, e:
|
||||
self.status['text'] = u"Error: {0}".format(e.args[0])
|
||||
return
|
||||
if decrypt_status[0] == 0:
|
||||
self.status['text'] = u"File successfully decrypted"
|
||||
else:
|
||||
self.status['text'] = decrypt_status[1]
|
||||
|
||||
root = Tkinter.Tk()
|
||||
if AES is None:
|
||||
root.withdraw()
|
||||
tkMessageBox.showerror(
|
||||
"Ignoble EPUB Decrypter",
|
||||
"This script requires OpenSSL or PyCrypto, which must be installed "
|
||||
"separately. Read the top-of-script comment for details.")
|
||||
return 1
|
||||
root.title('Ignoble EPUB Decrypter')
|
||||
root.title(u"Barnes & Noble ePub Decrypter 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.stdout=SafeUnbuffered(sys.stdout)
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
sys.exit(cli_main())
|
||||
sys.exit(gui_main())
|
||||
|
||||
@@ -1,13 +1,25 @@
|
||||
#! /usr/bin/python
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
# ignoblekeygen.pyw, version 2.4
|
||||
# ignoblekeygen.pyw, version 2.5
|
||||
# Copyright © 2009-2010 by i♥cabbages
|
||||
|
||||
# To run this program install Python 2.6 from <http://www.python.org/download/>
|
||||
# and OpenSSL or 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.
|
||||
# Released under the terms of the GNU General Public Licence, version 3
|
||||
# <http://www.gnu.org/licenses/>
|
||||
|
||||
# Modified 2010–2012 by some_updates, DiapDealer and Apprentice Alf
|
||||
|
||||
# Windows users: Before running this program, you must first install Python 2.6
|
||||
# from <http://www.python.org/download/> and PyCrypto from
|
||||
# <http://www.voidspace.org.uk/python/modules.shtml#pycrypto> (make sure to
|
||||
# install the version for Python 2.6). Save this script file as
|
||||
# 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 (pythonw ignoblekeygen.pyw) or by double-clicking
|
||||
# it when it has been associated with PythonLauncher.
|
||||
|
||||
# Revision history:
|
||||
# 1 - Initial release
|
||||
@@ -16,36 +28,92 @@ from __future__ import with_statement
|
||||
# 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
|
||||
|
||||
"""
|
||||
Generate Barnes & Noble EPUB user key from name and credit card number.
|
||||
"""
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = "2.5"
|
||||
|
||||
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)
|
||||
|
||||
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"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]
|
||||
|
||||
|
||||
# use openssl's libcrypt if it exists in place of pycrypto
|
||||
# code extracted from the Adobe Adept DRM removal code also by I HeartCabbages
|
||||
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 sys.platform.startswith('win'):
|
||||
if iswindows:
|
||||
libcrypto = find_library('libeay32')
|
||||
else:
|
||||
libcrypto = find_library('crypto')
|
||||
|
||||
if libcrypto is None:
|
||||
print 'libcrypto not found'
|
||||
raise IGNOBLEError('libcrypto not found')
|
||||
libcrypto = CDLL(libcrypto)
|
||||
|
||||
@@ -70,6 +138,7 @@ def _load_crypto_libcrypto():
|
||||
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)
|
||||
@@ -88,7 +157,6 @@ def _load_crypto_libcrypto():
|
||||
|
||||
return AES
|
||||
|
||||
|
||||
def _load_crypto_pycrypto():
|
||||
from Crypto.Cipher import AES as _AES
|
||||
|
||||
@@ -120,25 +188,28 @@ def normalize_name(name):
|
||||
return ''.join(x for x in name.lower() if x != ' ')
|
||||
|
||||
|
||||
def generate_keyfile(name, ccn, outpath):
|
||||
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()
|
||||
with open(outpath, 'wb') as f:
|
||||
f.write(userkey.encode('base64'))
|
||||
return userkey
|
||||
return userkey.encode('base64')
|
||||
|
||||
|
||||
|
||||
|
||||
def cli_main(argv=sys.argv):
|
||||
def cli_main(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 " \
|
||||
@@ -146,10 +217,11 @@ def cli_main(argv=sys.argv):
|
||||
(progname,)
|
||||
return 1
|
||||
if len(argv) != 4:
|
||||
print "usage: %s NAME CC# OUTFILE" % (progname,)
|
||||
print u"usage: {0} <Name> <CC#> <keyfileout.b64>".format(progname)
|
||||
return 1
|
||||
name, ccn, outpath = argv[1:]
|
||||
generate_keyfile(name, ccn, outpath)
|
||||
name, ccn, keypath = argv[1:]
|
||||
userkey = generate_key(name, ccn)
|
||||
open(keypath,'wb').write(userkey)
|
||||
return 0
|
||||
|
||||
|
||||
@@ -162,38 +234,38 @@ def gui_main():
|
||||
class DecryptionDialog(Tkinter.Frame):
|
||||
def __init__(self, root):
|
||||
Tkinter.Frame.__init__(self, root, border=5)
|
||||
self.status = Tkinter.Label(self, text='Enter parameters')
|
||||
self.status = 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='Account Name').grid(row=0)
|
||||
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='CC#').grid(row=1)
|
||||
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='Output file').grid(row=2)
|
||||
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, 'bnepubkey.b64')
|
||||
button = Tkinter.Button(body, text="...", command=self.get_keypath)
|
||||
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="Generate", width=10, command=self.generate)
|
||||
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="Quit", width=10, command=self.quit)
|
||||
buttons, text=u"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',
|
||||
parent=None, title=u"Select B&N ePub key file to produce",
|
||||
defaultextension=u".b64",
|
||||
filetypes=[('base64-encoded files', '.b64'),
|
||||
('All Files', '.*')])
|
||||
if keypath:
|
||||
@@ -201,27 +273,28 @@ def gui_main():
|
||||
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'
|
||||
self.status['text'] = u"Name not specified"
|
||||
return
|
||||
if not ccn:
|
||||
self.status['text'] = 'Credit card number not specified'
|
||||
self.status['text'] = u"Credit card number not specified"
|
||||
return
|
||||
if not keypath:
|
||||
self.status['text'] = 'Output keyfile path not specified'
|
||||
self.status['text'] = u"Output keyfile path not specified"
|
||||
return
|
||||
self.status['text'] = 'Generating...'
|
||||
self.status['text'] = u"Generating..."
|
||||
try:
|
||||
generate_keyfile(name, ccn, keypath)
|
||||
userkey = generate_key(name, ccn)
|
||||
except Exception, e:
|
||||
self.status['text'] = 'Error: ' + str(e)
|
||||
self.status['text'] = u"Error: (0}".format(e.args[0])
|
||||
return
|
||||
self.status['text'] = 'Keyfile successfully generated'
|
||||
open(keypath,'wb').write(userkey)
|
||||
self.status['text'] = u"Keyfile successfully generated"
|
||||
|
||||
root = Tkinter.Tk()
|
||||
if AES is None:
|
||||
@@ -231,7 +304,7 @@ def gui_main():
|
||||
"This script requires OpenSSL or PyCrypto, which must be installed "
|
||||
"separately. Read the top-of-script comment for details.")
|
||||
return 1
|
||||
root.title('Ignoble EPUB Keyfile Generator')
|
||||
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)
|
||||
@@ -240,5 +313,7 @@ def gui_main():
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) > 1:
|
||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
sys.exit(cli_main())
|
||||
sys.exit(gui_main())
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
# ineptepub.pyw, version 5.6
|
||||
# Copyright © 2009-2010 i♥cabbages
|
||||
# ineptepub.pyw, version 5.8
|
||||
# Copyright © 2009-2010 by i♥cabbages
|
||||
|
||||
# Released under the terms of the GNU General Public Licence, version 3 or
|
||||
# later. <http://www.gnu.org/licenses/>
|
||||
# Released under the terms of the GNU General Public Licence, version 3
|
||||
# <http://www.gnu.org/licenses/>
|
||||
|
||||
# Modified 2010–2012 by some_updates, DiapDealer and Apprentice Alf
|
||||
|
||||
# Windows users: Before running this program, you must first install Python 2.6
|
||||
# from <http://www.python.org/download/> and PyCrypto from
|
||||
@@ -31,24 +33,83 @@ from __future__ import with_statement
|
||||
# 5.5 - On Windows try PyCrypto first, OpenSSL next
|
||||
# 5.6 - Modify interface to allow use with import
|
||||
# 5.7 - Fix for potential problem with PyCrypto
|
||||
# 5.8 - Revised to allow use in calibre plugins to eliminate need for duplicate code
|
||||
|
||||
"""
|
||||
Decrypt Adobe ADEPT-encrypted EPUB books.
|
||||
Decrypt Adobe Digital Editions encrypted ePub books.
|
||||
"""
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = "5.8"
|
||||
|
||||
import sys
|
||||
import os
|
||||
import traceback
|
||||
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
|
||||
|
||||
# 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 '?'.
|
||||
|
||||
|
||||
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)]
|
||||
return [u"ineptepub.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
|
||||
@@ -58,7 +119,7 @@ def _load_crypto_libcrypto():
|
||||
Structure, c_ulong, create_string_buffer, cast
|
||||
from ctypes.util import find_library
|
||||
|
||||
if sys.platform.startswith('win'):
|
||||
if iswindows:
|
||||
libcrypto = find_library('libeay32')
|
||||
else:
|
||||
libcrypto = find_library('crypto')
|
||||
@@ -272,6 +333,7 @@ def _load_crypto():
|
||||
except (ImportError, ADEPTError):
|
||||
pass
|
||||
return (AES, RSA)
|
||||
|
||||
AES, RSA = _load_crypto()
|
||||
|
||||
META_NAMES = ('mimetype', 'META-INF/rights.xml', 'META-INF/encryption.xml')
|
||||
@@ -314,158 +376,181 @@ class Decryptor(object):
|
||||
data = self.decompress(data)
|
||||
return data
|
||||
|
||||
|
||||
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 decryptBook(keypath, inpath, outpath):
|
||||
with open(keypath, 'rb') as f:
|
||||
keyder = f.read()
|
||||
rsa = RSA(keyder)
|
||||
# check file to make check whether it's probably an Adobe Adept encrypted ePub
|
||||
def adeptBook(inpath):
|
||||
with closing(ZipFile(open(inpath, 'rb'))) as inf:
|
||||
namelist = set(inf.namelist())
|
||||
if 'META-INF/rights.xml' not in namelist or \
|
||||
'META-INF/encryption.xml' not in namelist:
|
||||
raise ADEPTError('%s: not an ADEPT EPUB' % (inpath,))
|
||||
return False
|
||||
try:
|
||||
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
|
||||
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
||||
expr = './/%s' % (adept('encryptedKey'),)
|
||||
bookkey = ''.join(rights.findtext(expr))
|
||||
if len(bookkey) == 172:
|
||||
return True
|
||||
except:
|
||||
# if we couldn't check, assume it is
|
||||
return True
|
||||
return False
|
||||
|
||||
def decryptBook(userkey, inpath, outpath):
|
||||
if AES is None:
|
||||
raise ADEPTError(u"PyCrypto or OpenSSL must be installed.")
|
||||
rsa = RSA(userkey)
|
||||
with closing(ZipFile(open(inpath, 'rb'))) as inf:
|
||||
namelist = set(inf.namelist())
|
||||
if 'META-INF/rights.xml' not in namelist or \
|
||||
'META-INF/encryption.xml' not in namelist:
|
||||
print u"{0:s} is DRM-free.".format(os.path.basename(inpath))
|
||||
return 1
|
||||
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))
|
||||
try:
|
||||
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
|
||||
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
||||
expr = './/%s' % (adept('encryptedKey'),)
|
||||
bookkey = ''.join(rights.findtext(expr))
|
||||
if len(bookkey) != 172:
|
||||
print u"{0:s} is not a secure Adobe Adept ePub.".format(os.path.basename(inpath))
|
||||
return 1
|
||||
bookkey = rsa.decrypt(bookkey.decode('base64'))
|
||||
# Padded as per RSAES-PKCS1-v1_5
|
||||
if bookkey[-17] != '\x00':
|
||||
print u"Could not decrypt {0:s}. Wrong key".format(os.path.basename(inpath))
|
||||
return 2
|
||||
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))
|
||||
except:
|
||||
print u"Could not decrypt {0:s} because of an exception:\n{1:s}".format(os.path.basename(inpath), traceback.format_exc())
|
||||
return 2
|
||||
return 0
|
||||
|
||||
|
||||
def cli_main(argv=sys.argv):
|
||||
def cli_main(argv=unicode_argv()):
|
||||
progname = os.path.basename(argv[0])
|
||||
if AES is None:
|
||||
print "%s: This script requires OpenSSL or PyCrypto, which must be" \
|
||||
" installed separately. Read the top-of-script comment for" \
|
||||
" details." % (progname,)
|
||||
return 1
|
||||
if len(argv) != 4:
|
||||
print "usage: %s KEYFILE INBOOK OUTBOOK" % (progname,)
|
||||
print u"usage: {0} <keyfile.der> <inbook.epub> <outbook.epub>".format(progname)
|
||||
return 1
|
||||
keypath, inpath, outpath = argv[1:]
|
||||
return decryptBook(keypath, inpath, outpath)
|
||||
|
||||
userkey = open(keypath,'rb').read()
|
||||
result = decryptBook(userkey, inpath, outpath)
|
||||
if result == 0:
|
||||
print u"Successfully decrypted {0:s} as {1:s}".format(os.path.basename(inpath),os.path.basename(outpath))
|
||||
return result
|
||||
|
||||
def gui_main():
|
||||
import Tkinter
|
||||
import Tkconstants
|
||||
import tkFileDialog
|
||||
import traceback
|
||||
|
||||
class DecryptionDialog(Tkinter.Frame):
|
||||
def __init__(self, root):
|
||||
Tkinter.Frame.__init__(self, root, border=5)
|
||||
self.status = Tkinter.Label(self, text=u"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=u"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(u"adeptkey.der"):
|
||||
self.keypath.insert(0, u"adeptkey.der")
|
||||
button = Tkinter.Button(body, text=u"...", command=self.get_keypath)
|
||||
button.grid(row=0, column=2)
|
||||
Tkinter.Label(body, text=u"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=u"...", command=self.get_inpath)
|
||||
button.grid(row=1, column=2)
|
||||
Tkinter.Label(body, text=u"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=u"...", command=self.get_outpath)
|
||||
button.grid(row=2, column=2)
|
||||
buttons = Tkinter.Frame(self)
|
||||
buttons.pack()
|
||||
botton = Tkinter.Button(
|
||||
buttons, text=u"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=u"Quit", width=10, command=self.quit)
|
||||
button.pack(side=Tkconstants.RIGHT)
|
||||
|
||||
def get_keypath(self):
|
||||
keypath = tkFileDialog.askopenfilename(
|
||||
parent=None, title=u"Select Adobe Adept \'.der\' key file",
|
||||
defaultextension=u".der",
|
||||
filetypes=[('Adobe Adept 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=u"Select ADEPT-encrypted ePub file to decrypt",
|
||||
defaultextension=u".epub", filetypes=[('ePub files', '.epub')])
|
||||
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=u"Select unencrypted ePub file to produce",
|
||||
defaultextension=u".epub", filetypes=[('ePub files', '.epub')])
|
||||
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'] = u"Specified key file does not exist"
|
||||
return
|
||||
if not inpath or not os.path.exists(inpath):
|
||||
self.status['text'] = u"Specified input file does not exist"
|
||||
return
|
||||
if not outpath:
|
||||
self.status['text'] = u"Output file not specified"
|
||||
return
|
||||
if inpath == outpath:
|
||||
self.status['text'] = u"Must have different input and output files"
|
||||
return
|
||||
userkey = open(keypath,'rb').read()
|
||||
self.status['text'] = u"Decrypting..."
|
||||
try:
|
||||
decrypt_status = decryptBook(userkey, inpath, outpath)
|
||||
except Exception, e:
|
||||
self.status['text'] = u"Error; {0}".format(e)
|
||||
return
|
||||
if decrypt_status == 0:
|
||||
self.status['text'] = u"File successfully decrypted"
|
||||
else:
|
||||
self.status['text'] = u"The was an error decrypting the file."
|
||||
|
||||
root = Tkinter.Tk()
|
||||
if AES is None:
|
||||
root.withdraw()
|
||||
tkMessageBox.showerror(
|
||||
"INEPT EPUB Decrypter",
|
||||
"This script requires OpenSSL or PyCrypto, which must be"
|
||||
" installed separately. Read the top-of-script comment for"
|
||||
" details.")
|
||||
return 1
|
||||
root.title('INEPT EPUB Decrypter')
|
||||
root.title(u"Adobe Adept ePub Decrypter v.{0}".format(__version__))
|
||||
root.resizable(True, False)
|
||||
root.minsize(300, 0)
|
||||
DecryptionDialog(root).pack(fill=Tkconstants.X, expand=1)
|
||||
@@ -474,5 +559,7 @@ def gui_main():
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) > 1:
|
||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
sys.exit(cli_main())
|
||||
sys.exit(gui_main())
|
||||
|
||||
@@ -6,8 +6,8 @@ from __future__ import with_statement
|
||||
# ineptkey.pyw, version 5.6
|
||||
# Copyright © 2009-2010 i♥cabbages
|
||||
|
||||
# Released under the terms of the GNU General Public Licence, version 3 or
|
||||
# later. <http://www.gnu.org/licenses/>
|
||||
# Released under the terms of the GNU General Public Licence, version 3
|
||||
# <http://www.gnu.org/licenses/>
|
||||
|
||||
# Windows users: Before running this program, you must first install Python 2.6
|
||||
# from <http://www.python.org/download/> and PyCrypto from
|
||||
@@ -37,7 +37,7 @@ from __future__ import with_statement
|
||||
# 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 - Revise to allow use in Plugins to eliminate need for duplicate code
|
||||
# 5.6 - Revised to allow use in Plugins to eliminate need for duplicate code
|
||||
|
||||
"""
|
||||
Retrieve Adobe ADEPT user key.
|
||||
@@ -49,12 +49,65 @@ import sys
|
||||
import os
|
||||
import struct
|
||||
|
||||
# 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 '?'.
|
||||
|
||||
|
||||
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)]
|
||||
return [u"ineptkey.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
|
||||
|
||||
@@ -80,13 +133,13 @@ if iswindows:
|
||||
_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',
|
||||
@@ -308,9 +361,9 @@ if iswindows:
|
||||
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")
|
||||
device = winreg.QueryValueEx(regkey, 'key')[0]
|
||||
keykey = CryptUnprotectData(device, entropy)
|
||||
userkey = None
|
||||
keys = []
|
||||
@@ -343,7 +396,7 @@ if iswindows:
|
||||
if len(keys) == 0:
|
||||
raise ADEPTError('Could not locate privateLicenseKey')
|
||||
return keys
|
||||
|
||||
|
||||
|
||||
elif isosx:
|
||||
import xml.etree.ElementTree as etree
|
||||
@@ -386,7 +439,7 @@ else:
|
||||
def retrieve_keys(keypath):
|
||||
raise ADEPTError("This script only supports Windows and Mac OS X.")
|
||||
return []
|
||||
|
||||
|
||||
def retrieve_key(keypath):
|
||||
keys = retrieve_keys()
|
||||
with open(keypath, 'wb') as f:
|
||||
@@ -397,22 +450,22 @@ def extractKeyfile(keypath):
|
||||
try:
|
||||
success = retrieve_key(keypath)
|
||||
except ADEPTError, e:
|
||||
print "Key generation Error: " + str(e)
|
||||
print u"Key generation Error: {0}".format(e.args[0])
|
||||
return 1
|
||||
except Exception, e:
|
||||
print "General Error: " + str(e)
|
||||
print "General Error: {0}".format(e.args[0])
|
||||
return 1
|
||||
if not success:
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
def cli_main(argv=sys.argv):
|
||||
def cli_main(argv=unicode_argv()):
|
||||
keypath = argv[1]
|
||||
return extractKeyfile(keypath)
|
||||
|
||||
|
||||
def main(argv=sys.argv):
|
||||
def gui_main(argv=unicode_argv()):
|
||||
import Tkinter
|
||||
import Tkconstants
|
||||
import tkMessageBox
|
||||
@@ -421,24 +474,24 @@ def main(argv=sys.argv):
|
||||
class ExceptionDialog(Tkinter.Frame):
|
||||
def __init__(self, root, text):
|
||||
Tkinter.Frame.__init__(self, root, border=5)
|
||||
label = Tkinter.Label(self, text="Unexpected error:",
|
||||
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)
|
||||
|
||||
|
||||
root = Tkinter.Tk()
|
||||
root.withdraw()
|
||||
progname = os.path.basename(argv[0])
|
||||
keypath = os.path.abspath("adeptkey.der")
|
||||
keypath, progname = os.path.split(argv[0])
|
||||
keypath = os.path.join(keypath, u"adeptkey.der")
|
||||
success = False
|
||||
try:
|
||||
success = retrieve_key(keypath)
|
||||
except ADEPTError, e:
|
||||
tkMessageBox.showerror("ADEPT Key", "Error: " + str(e))
|
||||
tkMessageBox.showerror(u"ADEPT Key", "Error: {0}".format(e.args[0]))
|
||||
except Exception:
|
||||
root.wm_state('normal')
|
||||
root.title('ADEPT Key')
|
||||
@@ -448,10 +501,12 @@ def main(argv=sys.argv):
|
||||
if not success:
|
||||
return 1
|
||||
tkMessageBox.showinfo(
|
||||
"ADEPT Key", "Key successfully retrieved to %s" % (keypath))
|
||||
u"ADEPT Key", u"Key successfully retrieved to {0}".format(keypath))
|
||||
return 0
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) > 1:
|
||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
sys.exit(cli_main())
|
||||
sys.exit(main())
|
||||
sys.exit(gui_main())
|
||||
|
||||
@@ -1,13 +1,25 @@
|
||||
#! /usr/bin/env python
|
||||
# ineptpdf.pyw, version 7.11
|
||||
#! /usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
# To run this program install Python 2.6 from http://www.python.org/download/
|
||||
# and OpenSSL (already installed on Mac OS X and Linux) OR
|
||||
# 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
|
||||
# ineptpdf.pyw and double-click on it to run it.
|
||||
# ineptpdf.pyw, version 7.11
|
||||
# Copyright © 2009-2010 by i♥cabbages
|
||||
|
||||
# Released under the terms of the GNU General Public Licence, version 3
|
||||
# <http://www.gnu.org/licenses/>
|
||||
|
||||
# Modified 2010–2012 by some_updates, DiapDealer and Apprentice Alf
|
||||
|
||||
# Windows users: Before running this program, you must first install Python 2.6
|
||||
# from <http://www.python.org/download/> and PyCrypto from
|
||||
# <http://www.voidspace.org.uk/python/modules.shtml#pycrypto> (make sure to
|
||||
# install the version for Python 2.6). Save this script file as
|
||||
# ineptepub.pyw and double-click on it to run it.
|
||||
#
|
||||
# Mac OS X users: Save this script file as ineptepub.pyw. You can run this
|
||||
# program from the command line (pythonw ineptepub.pyw) or by double-clicking
|
||||
# it when it has been associated with PythonLauncher.
|
||||
|
||||
# Revision history:
|
||||
# 1 - Initial release
|
||||
@@ -36,12 +48,14 @@ from __future__ import with_statement
|
||||
# 7.9 - Bug fix for some session key errors when len(bookkey) > length required
|
||||
# 7.10 - Various tweaks to fix minor problems.
|
||||
# 7.11 - More tweaks to fix minor problems.
|
||||
# 7.12 - Revised to allow use in calibre plugins to eliminate need for duplicate code
|
||||
|
||||
"""
|
||||
Decrypts Adobe ADEPT-encrypted PDF files.
|
||||
"""
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__version__ = "7.12"
|
||||
|
||||
import sys
|
||||
import os
|
||||
@@ -51,10 +65,63 @@ import struct
|
||||
import hashlib
|
||||
from itertools import chain, islice
|
||||
import xml.etree.ElementTree as etree
|
||||
import Tkinter
|
||||
import Tkconstants
|
||||
import tkFileDialog
|
||||
import tkMessageBox
|
||||
|
||||
# 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)]
|
||||
return [u"ineptepub.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
|
||||
@@ -1520,9 +1587,7 @@ class PDFDocument(object):
|
||||
|
||||
def initialize_ebx(self, password, docid, param):
|
||||
self.is_printable = self.is_modifiable = self.is_extractable = True
|
||||
with open(password, 'rb') as f:
|
||||
keyder = f.read()
|
||||
rsa = RSA(keyder)
|
||||
rsa = RSA(password)
|
||||
length = int_value(param.get('Length', 0)) / 8
|
||||
rights = str_value(param.get('ADEPT_LICENSE')).decode('base64')
|
||||
rights = zlib.decompress(rights, -15)
|
||||
@@ -1907,14 +1972,14 @@ class PDFObjStrmParser(PDFParser):
|
||||
### My own code, for which there is none else to blame
|
||||
|
||||
class PDFSerializer(object):
|
||||
def __init__(self, inf, keypath):
|
||||
def __init__(self, inf, userkey):
|
||||
global GEN_XREF_STM, gen_xref_stm
|
||||
gen_xref_stm = GEN_XREF_STM > 1
|
||||
self.version = inf.read(8)
|
||||
inf.seek(0)
|
||||
self.doc = doc = PDFDocument()
|
||||
parser = PDFParser(doc, inf)
|
||||
doc.initialize(keypath)
|
||||
doc.initialize(userkey)
|
||||
self.objids = objids = set()
|
||||
for xref in reversed(doc.xrefs):
|
||||
trailer = xref.trailer
|
||||
@@ -2097,142 +2162,144 @@ class PDFSerializer(object):
|
||||
self.write('endobj\n')
|
||||
|
||||
|
||||
class DecryptionDialog(Tkinter.Frame):
|
||||
def __init__(self, root):
|
||||
Tkinter.Frame.__init__(self, root, border=5)
|
||||
ltext='Select file for decryption\n'
|
||||
self.status = Tkinter.Label(self, text=ltext)
|
||||
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(os.path.realpath(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 PDF file to decrypt',
|
||||
defaultextension='.pdf', filetypes=[('PDF files', '.pdf'),
|
||||
('All files', '.*')])
|
||||
if inpath:
|
||||
inpath = os.path.normpath(os.path.realpath(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 PDF file to produce',
|
||||
defaultextension='.pdf', filetypes=[('PDF files', '.pdf'),
|
||||
('All files', '.*')])
|
||||
if outpath:
|
||||
outpath = os.path.normpath(os.path.realpath(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):
|
||||
# keyfile doesn't exist
|
||||
self.status['text'] = 'Specified Adept 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
|
||||
# patch for non-ascii characters
|
||||
argv = [sys.argv[0], keypath, inpath, outpath]
|
||||
self.status['text'] = 'Processing ...'
|
||||
try:
|
||||
cli_main(argv)
|
||||
except Exception, a:
|
||||
self.status['text'] = 'Error: ' + str(a)
|
||||
return
|
||||
self.status['text'] = 'File successfully decrypted.\n'+\
|
||||
'Close this window or decrypt another pdf file.'
|
||||
return
|
||||
|
||||
|
||||
def decryptBook(keypath, inpath, outpath):
|
||||
def decryptBook(userkey, inpath, outpath):
|
||||
if RSA is None:
|
||||
raise ADEPTError(u"PyCrypto or OpenSSL must be installed.")
|
||||
with open(inpath, 'rb') as inf:
|
||||
try:
|
||||
serializer = PDFSerializer(inf, keypath)
|
||||
serializer = PDFSerializer(inf, userkey)
|
||||
except:
|
||||
print "Error serializing pdf. Probably wrong key."
|
||||
return 1
|
||||
print u"Error serializing pdf {0}. Probably wrong key.".format(os.path.basename(inpath))
|
||||
return 2
|
||||
# hope this will fix the 'bad file descriptor' problem
|
||||
with open(outpath, 'wb') as outf:
|
||||
# help construct to make sure the method runs to the end
|
||||
# help construct to make sure the method runs to the end
|
||||
try:
|
||||
serializer.dump(outf)
|
||||
except:
|
||||
print "error writing pdf."
|
||||
return 1
|
||||
except Exception, e:
|
||||
print u"error writing pdf: {0}".format(e.args[0])
|
||||
return 2
|
||||
return 0
|
||||
|
||||
|
||||
def cli_main(argv=sys.argv):
|
||||
def cli_main(argv=unicode_argv()):
|
||||
progname = os.path.basename(argv[0])
|
||||
if RSA is None:
|
||||
print "%s: This script requires OpenSSL or PyCrypto, which must be installed " \
|
||||
"separately. Read the top-of-script comment for details." % \
|
||||
(progname,)
|
||||
return 1
|
||||
if len(argv) != 4:
|
||||
print "usage: %s KEYFILE INBOOK OUTBOOK" % (progname,)
|
||||
print u"usage: {0} <keyfile.der> <inbook.pdf> <outbook.pdf>".format(progname)
|
||||
return 1
|
||||
keypath, inpath, outpath = argv[1:]
|
||||
return decryptBook(keypath, inpath, outpath)
|
||||
userkey = open(keypath,'rb').read()
|
||||
result = decryptBook(userkey, inpath, outpath)
|
||||
if result == 0:
|
||||
print u"Successfully decrypted {0:s} as {1:s}".format(os.path.basename(inpath),os.path.basename(outpath))
|
||||
return result
|
||||
|
||||
|
||||
def gui_main():
|
||||
import Tkinter
|
||||
import Tkconstants
|
||||
import tkFileDialog
|
||||
import tkMessageBox
|
||||
|
||||
class DecryptionDialog(Tkinter.Frame):
|
||||
def __init__(self, root):
|
||||
Tkinter.Frame.__init__(self, root, border=5)
|
||||
self.status = Tkinter.Label(self, text=u"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=u"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(u"adeptkey.der"):
|
||||
self.keypath.insert(0, u"adeptkey.der")
|
||||
button = Tkinter.Button(body, text=u"...", command=self.get_keypath)
|
||||
button.grid(row=0, column=2)
|
||||
Tkinter.Label(body, text=u"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=u"...", command=self.get_inpath)
|
||||
button.grid(row=1, column=2)
|
||||
Tkinter.Label(body, text=u"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=u"...", command=self.get_outpath)
|
||||
button.grid(row=2, column=2)
|
||||
buttons = Tkinter.Frame(self)
|
||||
buttons.pack()
|
||||
botton = Tkinter.Button(
|
||||
buttons, text=u"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=u"Quit", width=10, command=self.quit)
|
||||
button.pack(side=Tkconstants.RIGHT)
|
||||
|
||||
def get_keypath(self):
|
||||
keypath = tkFileDialog.askopenfilename(
|
||||
parent=None, title=u"Select Adobe Adept \'.der\' key file",
|
||||
defaultextension=u".der",
|
||||
filetypes=[('Adobe Adept 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=u"Select ADEPT-encrypted PDF file to decrypt",
|
||||
defaultextension=u".pdf", filetypes=[('PDF files', '.pdf')])
|
||||
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=u"Select unencrypted PDF file to produce",
|
||||
defaultextension=u".pdf", filetypes=[('PDF files', '.pdf')])
|
||||
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'] = u"Specified key file does not exist"
|
||||
return
|
||||
if not inpath or not os.path.exists(inpath):
|
||||
self.status['text'] = u"Specified input file does not exist"
|
||||
return
|
||||
if not outpath:
|
||||
self.status['text'] = u"Output file not specified"
|
||||
return
|
||||
if inpath == outpath:
|
||||
self.status['text'] = u"Must have different input and output files"
|
||||
return
|
||||
userkey = open(keypath,'rb').read()
|
||||
self.status['text'] = u"Decrypting..."
|
||||
try:
|
||||
decrypt_status = decryptBook(userkey, inpath, outpath)
|
||||
except Exception, e:
|
||||
self.status['text'] = u"Error; {0}".format(e.args[0])
|
||||
return
|
||||
if decrypt_status == 0:
|
||||
self.status['text'] = u"File successfully decrypted"
|
||||
else:
|
||||
self.status['text'] = u"The was an error decrypting the file."
|
||||
|
||||
|
||||
root = Tkinter.Tk()
|
||||
if RSA is None:
|
||||
root.withdraw()
|
||||
@@ -2241,7 +2308,7 @@ def gui_main():
|
||||
"This script requires OpenSSL or PyCrypto, which must be installed "
|
||||
"separately. Read the top-of-script comment for details.")
|
||||
return 1
|
||||
root.title('INEPT PDF Decrypter')
|
||||
root.title(u"Adobe Adept PDF Decrypter v.{0}".format(__version__))
|
||||
root.resizable(True, False)
|
||||
root.minsize(370, 0)
|
||||
DecryptionDialog(root).pack(fill=Tkconstants.X, expand=1)
|
||||
@@ -2251,5 +2318,7 @@ def gui_main():
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) > 1:
|
||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
sys.exit(cli_main())
|
||||
sys.exit(gui_main())
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
# ignobleepub.pyw, version 3.6
|
||||
# Copyright © 2009-2012 by DiapDealer et al.
|
||||
|
||||
# engine to remove drm from Kindle for Mac and Kindle for PC books
|
||||
# for personal use for archiving and converting your ebooks
|
||||
|
||||
@@ -12,30 +16,51 @@ from __future__ import with_statement
|
||||
# be able to read OUR books on whatever device we want and to keep
|
||||
# readable for a long, long time
|
||||
|
||||
# This borrows very heavily from works by CMBDTC, IHeartCabbages, skindle,
|
||||
# This borrows very heavily from works by CMBDTC, IHeartCabbages, skindle,
|
||||
# unswindle, DarkReverser, ApprenticeAlf, DiapDealer, some_updates
|
||||
# and many many others
|
||||
# Special thanks to The Dark Reverser for MobiDeDrm and CMBDTC for cmbdtc_dump
|
||||
# from which this script borrows most unashamedly.
|
||||
|
||||
|
||||
__version__ = '4.4'
|
||||
# Changelog
|
||||
# 1.0 - Name change to k4mobidedrm. Adds Mac support, Adds plugin code
|
||||
# 1.1 - Adds support for additional kindle.info files
|
||||
# 1.2 - Better error handling for older Mobipocket
|
||||
# 1.3 - Don't try to decrypt Topaz books
|
||||
# 1.7 - Add support for Topaz books and Kindle serial numbers. Split code.
|
||||
# 1.9 - Tidy up after Topaz, minor exception changes
|
||||
# 2.1 - Topaz fix and filename sanitizing
|
||||
# 2.2 - Topaz Fix and minor Mac code fix
|
||||
# 2.3 - More Topaz fixes
|
||||
# 2.4 - K4PC/Mac key generation fix
|
||||
# 2.6 - Better handling of non-K4PC/Mac ebooks
|
||||
# 2.7 - Better trailing bytes handling in mobidedrm
|
||||
# 2.8 - Moved parsing of kindle.info files to mac & pc util files.
|
||||
# 3.1 - Updated for new calibre interface. Now __init__ in plugin.
|
||||
# 3.5 - Now support Kindle for PC/Mac 1.6
|
||||
# 3.6 - Even better trailing bytes handling in mobidedrm
|
||||
# 3.7 - Add support for Amazon Print Replica ebooks.
|
||||
# 3.8 - Improved Topaz support
|
||||
# 4.1 - Improved Topaz support and faster decryption with alfcrypto
|
||||
# 4.2 - Added support for Amazon's KF8 format ebooks
|
||||
# 4.4 - Linux calls to Wine added, and improved configuration dialog
|
||||
# 4.5 - Linux works again without Wine. Some Mac key file search changes
|
||||
# 4.6 - First attempt to handle unicode properly
|
||||
# 4.7 - Added timing reports, and changed search for Mac key files
|
||||
# 4.8 - Much better unicode handling, matching the updated inept and ignoble scripts
|
||||
# - Moved back into plugin, __init__ in plugin now only contains plugin code.
|
||||
|
||||
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)
|
||||
__version__ = '4.8'
|
||||
|
||||
import sys
|
||||
import os, csv, getopt
|
||||
import string
|
||||
|
||||
import sys, os, re
|
||||
import csv
|
||||
import getopt
|
||||
import re
|
||||
import traceback
|
||||
import time
|
||||
|
||||
buildXML = False
|
||||
import htmlentitydefs
|
||||
|
||||
class DrmException(Exception):
|
||||
pass
|
||||
@@ -54,161 +79,203 @@ else:
|
||||
import topazextract
|
||||
import kgenpids
|
||||
|
||||
# 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)
|
||||
|
||||
# cleanup bytestring filenames
|
||||
iswindows = sys.platform.startswith('win')
|
||||
isosx = sys.platform.startswith('darwin')
|
||||
|
||||
def unicode_argv():
|
||||
if iswindows:
|
||||
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
|
||||
# strings.
|
||||
|
||||
# Versions 2.x of Python don't support Unicode in sys.argv on
|
||||
# Windows, with the underlying Windows API instead replacing multi-byte
|
||||
# characters with '?'.
|
||||
|
||||
|
||||
from ctypes import POINTER, byref, cdll, c_int, windll
|
||||
from ctypes.wintypes import LPCWSTR, LPWSTR
|
||||
|
||||
GetCommandLineW = cdll.kernel32.GetCommandLineW
|
||||
GetCommandLineW.argtypes = []
|
||||
GetCommandLineW.restype = LPCWSTR
|
||||
|
||||
CommandLineToArgvW = windll.shell32.CommandLineToArgvW
|
||||
CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
|
||||
CommandLineToArgvW.restype = POINTER(LPWSTR)
|
||||
|
||||
cmd = GetCommandLineW()
|
||||
argc = c_int(0)
|
||||
argv = CommandLineToArgvW(cmd, byref(argc))
|
||||
if argc.value > 0:
|
||||
# Remove Python executable and commands if present
|
||||
start = argc.value - len(sys.argv)
|
||||
return [argv[i] for i in
|
||||
xrange(start, argc.value)]
|
||||
# if we don't have any arguments at all, just pass back script name
|
||||
# this should never happen
|
||||
return [u"mobidedrm.py"]
|
||||
else:
|
||||
argvencoding = sys.stdin.encoding
|
||||
if argvencoding == None:
|
||||
argvencoding = "utf-8"
|
||||
return [arg if (type(arg) == unicode) else unicode(arg,argvencoding) for arg in sys.argv]
|
||||
|
||||
# cleanup unicode filenames
|
||||
# borrowed from calibre from calibre/src/calibre/__init__.py
|
||||
# added in removal of non-printing chars
|
||||
# and removal of . at start
|
||||
# convert underscores to spaces (we're OK with spaces in file names)
|
||||
# added in removal of control (<32) chars
|
||||
# and removal of . at start and end
|
||||
# and with some (heavily edited) code from Paul Durrant's kindlenamer.py
|
||||
def cleanup_name(name):
|
||||
_filename_sanitize = re.compile(r'[\xae\0\\|\?\*<":>\+/]')
|
||||
substitute='_'
|
||||
one = ''.join(char for char in name if char in string.printable)
|
||||
one = _filename_sanitize.sub(substitute, one)
|
||||
one = re.sub(r'\s', ' ', one).strip()
|
||||
one = re.sub(r'^\.+$', '_', one)
|
||||
one = one.replace('..', substitute)
|
||||
# Windows doesn't like path components that end with a period
|
||||
if one.endswith('.'):
|
||||
one = one[:-1]+substitute
|
||||
# Mac and Unix don't like file names that begin with a full stop
|
||||
if len(one) > 0 and one[0] == '.':
|
||||
one = substitute+one[1:]
|
||||
one = one.replace('_',' ')
|
||||
return one
|
||||
|
||||
def decryptBook(infile, outdir, k4, kInfoFiles, serials, pids):
|
||||
global buildXML
|
||||
# substitute filename unfriendly characters
|
||||
name = name.replace(u"<",u"[").replace(u">",u"]").replace(u" : ",u" – ").replace(u": ",u" – ").replace(u":",u"—").replace(u"/",u"_").replace(u"\\",u"_").replace(u"|",u"_").replace(u"\"",u"\'")
|
||||
# delete control characters
|
||||
name = u"".join(char for char in name if ord(char)>=32)
|
||||
# white space to single space, delete leading and trailing while space
|
||||
name = re.sub(ur"\s", u" ", name).strip()
|
||||
# remove leading dots
|
||||
while len(name)>0 and name[0] == u".":
|
||||
name = name[1:]
|
||||
# remove trailing dots (Windows doesn't like them)
|
||||
if name.endswith(u'.'):
|
||||
name = name[:-1]
|
||||
return name
|
||||
|
||||
# must be passed unicode
|
||||
def unescape(text):
|
||||
def fixup(m):
|
||||
text = m.group(0)
|
||||
if text[:2] == u"&#":
|
||||
# character reference
|
||||
try:
|
||||
if text[:3] == u"&#x":
|
||||
return unichr(int(text[3:-1], 16))
|
||||
else:
|
||||
return unichr(int(text[2:-1]))
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
# named entity
|
||||
try:
|
||||
text = unichr(htmlentitydefs.name2codepoint[text[1:-1]])
|
||||
except KeyError:
|
||||
pass
|
||||
return text # leave as is
|
||||
return re.sub(u"&#?\w+;", fixup, text)
|
||||
|
||||
def GetDecryptedBook(infile, kInfoFiles, serials, pids, starttime = time.time()):
|
||||
# handle the obvious cases at the beginning
|
||||
if not os.path.isfile(infile):
|
||||
print >>sys.stderr, ('K4MobiDeDrm v%(__version__)s\n' % globals()) + "Error: Input file does not exist"
|
||||
return 1
|
||||
|
||||
starttime = time.time()
|
||||
print "Starting decryptBook routine."
|
||||
|
||||
raise DRMException (u"Input file does not exist.")
|
||||
|
||||
mobi = True
|
||||
magic3 = file(infile,'rb').read(3)
|
||||
if magic3 == 'TPZ':
|
||||
mobi = False
|
||||
|
||||
bookname = os.path.splitext(os.path.basename(infile))[0]
|
||||
|
||||
if mobi:
|
||||
mb = mobidedrm.MobiBook(infile)
|
||||
else:
|
||||
mb = topazextract.TopazBook(infile)
|
||||
|
||||
title = mb.getBookTitle()
|
||||
print "Processing Book: ", title
|
||||
filenametitle = cleanup_name(title)
|
||||
outfilename = cleanup_name(bookname)
|
||||
bookname = unescape(mb.getBookTitle())
|
||||
print u"Decrypting {1} ebook: {0}".format(bookname, mb.getBookType())
|
||||
|
||||
# generate 'sensible' filename, that will sort with the original name,
|
||||
# but is close to the name from the file.
|
||||
outlength = len(outfilename)
|
||||
comparelength = min(8,min(outlength,len(filenametitle)))
|
||||
copylength = min(max(outfilename.find(' '),8),len(outfilename))
|
||||
if outlength==0:
|
||||
outfilename = filenametitle
|
||||
elif comparelength > 0:
|
||||
if outfilename[:comparelength] == filenametitle[:comparelength]:
|
||||
outfilename = filenametitle
|
||||
else:
|
||||
outfilename = outfilename[:copylength] + " " + filenametitle
|
||||
# extend PID list with book-specific PIDs
|
||||
md1, md2 = mb.getPIDMetaInfo()
|
||||
pids.extend(kgenpids.getPidList(md1, md2, serials, kInfoFiles))
|
||||
print u"Found {1:d} keys to try after {0:.1f} seconds".format(time.time()-starttime, len(pids))
|
||||
|
||||
try:
|
||||
mb.processBook(pids)
|
||||
except:
|
||||
mb.cleanup
|
||||
raise
|
||||
|
||||
print u"Decryption succeeded after {0:.1f} seconds".format(time.time()-starttime)
|
||||
return mb
|
||||
|
||||
|
||||
# infile, outdir and kInfoFiles should be unicode strings
|
||||
def decryptBook(infile, outdir, kInfoFiles, serials, pids):
|
||||
starttime = time.time()
|
||||
print "Starting decryptBook routine."
|
||||
try:
|
||||
book = GetDecryptedBook(infile, kInfoFiles, serials, pids, starttime)
|
||||
except Exception, e:
|
||||
print u"Error decrypting book after {1:.1f} seconds: {0}".format(e.args[0],time.time()-starttime)
|
||||
return 1
|
||||
|
||||
# if we're saving to the same folder as the original, use file name_
|
||||
# if to a different folder, use book name
|
||||
if os.path.normcase(os.path.normpath(outdir)) == os.path.normcase(os.path.normpath(os.path.dirname(infile))):
|
||||
outfilename = os.path.splitext(os.path.basename(infile))[0]
|
||||
else:
|
||||
outfilename = cleanup_name(book.getBookTitle())
|
||||
|
||||
# avoid excessively long file names
|
||||
if len(outfilename)>150:
|
||||
outfilename = outfilename[:150]
|
||||
|
||||
# build pid list
|
||||
md1, md2 = mb.getPIDMetaInfo()
|
||||
pids.extend(kgenpids.getPidList(md1, md2, k4, serials, kInfoFiles))
|
||||
outfilename = outfilename+u"_nodrm"
|
||||
outfile = os.path.join(outdir, outfilename + book.getBookExtension())
|
||||
|
||||
print "Found {1:d} keys to try after {0:.1f} seconds".format(time.time()-starttime, len(pids))
|
||||
book.getFile(outfile)
|
||||
print u"Saved decrypted book {1:s} after {0:.1f} seconds".format(time.time()-starttime, outfilename)
|
||||
|
||||
|
||||
try:
|
||||
mb.processBook(pids)
|
||||
|
||||
except mobidedrm.DrmException, e:
|
||||
print >>sys.stderr, ('K4MobiDeDrm v%(__version__)s\n' % globals()) + "Error: " + str(e) + "\nDRM Removal Failed.\n"
|
||||
print "Failed to decrypted book after {0:.1f} seconds".format(time.time()-starttime)
|
||||
return 1
|
||||
except topazextract.TpzDRMError, e:
|
||||
print >>sys.stderr, ('K4MobiDeDrm v%(__version__)s\n' % globals()) + "Error: " + str(e) + "\nDRM Removal Failed.\n"
|
||||
print "Failed to decrypted book after {0:.1f} seconds".format(time.time()-starttime)
|
||||
return 1
|
||||
except Exception, e:
|
||||
print >>sys.stderr, ('K4MobiDeDrm v%(__version__)s\n' % globals()) + "Error: " + str(e) + "\nDRM Removal Failed.\n"
|
||||
print "Failed to decrypted book after {0:.1f} seconds".format(time.time()-starttime)
|
||||
return 1
|
||||
|
||||
print "Successfully decrypted book after {0:.1f} seconds".format(time.time()-starttime)
|
||||
|
||||
if mobi:
|
||||
if mb.getPrintReplica():
|
||||
outfile = os.path.join(outdir, outfilename + '_nodrm' + '.azw4')
|
||||
elif mb.getMobiVersion() >= 8:
|
||||
outfile = os.path.join(outdir, outfilename + '_nodrm' + '.azw3')
|
||||
else:
|
||||
outfile = os.path.join(outdir, outfilename + '_nodrm' + '.mobi')
|
||||
mb.getMobiFile(outfile)
|
||||
print "Saved decrypted book {1:s} after {0:.1f} seconds".format(time.time()-starttime, outfilename + '_nodrm')
|
||||
return 0
|
||||
|
||||
# topaz:
|
||||
print " Creating NoDRM HTMLZ Archive"
|
||||
zipname = os.path.join(outdir, outfilename + '_nodrm' + '.htmlz')
|
||||
mb.getHTMLZip(zipname)
|
||||
|
||||
print " Creating SVG ZIP Archive"
|
||||
zipname = os.path.join(outdir, outfilename + '_SVG' + '.zip')
|
||||
mb.getSVGZip(zipname)
|
||||
|
||||
if buildXML:
|
||||
print " Creating XML ZIP Archive"
|
||||
zipname = os.path.join(outdir, outfilename + '_XML' + '.zip')
|
||||
mb.getXMLZip(zipname)
|
||||
if book.getBookType()==u"Topaz":
|
||||
zipname = os.path.join(outdir, outfilename + u"_SVG.zip")
|
||||
book.getSVGZip(zipname)
|
||||
print u"Saved SVG ZIP Archive for {1:s} after {0:.1f} seconds".format(time.time()-starttime, outfilename)
|
||||
|
||||
# remove internal temporary directory of Topaz pieces
|
||||
mb.cleanup()
|
||||
print "Saved decrypted Topaz book parts after {0:.1f} seconds".format(time.time()-starttime)
|
||||
return 0
|
||||
book.cleanup()
|
||||
|
||||
|
||||
def usage(progname):
|
||||
print "Removes DRM protection from K4PC/M, Kindle, Mobi and Topaz ebooks"
|
||||
print "Usage:"
|
||||
print " %s [-k <kindle.info>] [-p <pidnums>] [-s <kindleSerialNumbers>] <infile> <outdir> " % progname
|
||||
print u"Removes DRM protection from Mobipocket, Amazon KF8, Amazon Print Replica and Amazon Topaz ebooks"
|
||||
print u"Usage:"
|
||||
print u" {0} [-k <kindle.info>] [-p <comma separated PIDs>] [-s <comma separated Kindle serial numbers>] <infile> <outdir>".format(progname)
|
||||
|
||||
#
|
||||
# Main
|
||||
#
|
||||
def main(argv=sys.argv):
|
||||
def cli_main(argv=unicode_argv()):
|
||||
progname = os.path.basename(argv[0])
|
||||
|
||||
k4 = False
|
||||
kInfoFiles = []
|
||||
serials = []
|
||||
pids = []
|
||||
|
||||
print ('K4MobiDeDrm v%(__version__)s '
|
||||
'provided by the work of many including DiapDealer, SomeUpdates, IHeartCabbages, CMBDTC, Skindle, DarkReverser, ApprenticeAlf, etc .' % globals())
|
||||
print u"K4MobiDeDrm v{0}.\nCopyright © 2008-2012 The Dark Reverser et al.".format(__version__)
|
||||
|
||||
try:
|
||||
opts, args = getopt.getopt(sys.argv[1:], "k:p:s:")
|
||||
except getopt.GetoptError, err:
|
||||
print str(err)
|
||||
print u"Error in options or arguments: {0}".format(err.args[0])
|
||||
usage(progname)
|
||||
sys.exit(2)
|
||||
if len(args)<2:
|
||||
usage(progname)
|
||||
sys.exit(2)
|
||||
|
||||
infile = args[0]
|
||||
outdir = args[1]
|
||||
kInfoFiles = []
|
||||
serials = []
|
||||
pids = []
|
||||
|
||||
for o, a in opts:
|
||||
if o == "-k":
|
||||
if a == None :
|
||||
@@ -223,16 +290,13 @@ def main(argv=sys.argv):
|
||||
raise DrmException("Invalid parameter for -s")
|
||||
serials = a.split(',')
|
||||
|
||||
# try with built in Kindle Info files
|
||||
k4 = True
|
||||
if sys.platform.startswith('linux'):
|
||||
k4 = False
|
||||
kInfoFiles = None
|
||||
infile = args[0]
|
||||
outdir = args[1]
|
||||
return decryptBook(infile, outdir, k4, kInfoFiles, serials, pids)
|
||||
# try with built in Kindle Info files if not on Linux
|
||||
k4 = not sys.platform.startswith('linux')
|
||||
|
||||
return decryptBook(infile, outdir, kInfoFiles, serials, pids)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.stdout=Unbuffered(sys.stdout)
|
||||
sys.exit(main())
|
||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
sys.exit(cli_main())
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# standlone set of Mac OSX specific routines needed for KindleBooks
|
||||
|
||||
from __future__ import with_statement
|
||||
@@ -22,7 +25,7 @@ def _load_crypto_libcrypto():
|
||||
|
||||
libcrypto = find_library('crypto')
|
||||
if libcrypto is None:
|
||||
raise DrmException('libcrypto not found')
|
||||
raise DrmException(u"libcrypto not found")
|
||||
libcrypto = CDLL(libcrypto)
|
||||
|
||||
# From OpenSSL's crypto aes header
|
||||
@@ -80,14 +83,14 @@ def _load_crypto_libcrypto():
|
||||
def set_decrypt_key(self, userkey, iv):
|
||||
self._blocksize = len(userkey)
|
||||
if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) :
|
||||
raise DrmException('AES improper key used')
|
||||
raise DrmException(u"AES improper key used")
|
||||
return
|
||||
keyctx = self._keyctx = AES_KEY()
|
||||
self._iv = iv
|
||||
self._userkey = userkey
|
||||
rv = AES_set_decrypt_key(userkey, len(userkey) * 8, keyctx)
|
||||
if rv < 0:
|
||||
raise DrmException('Failed to initialize AES key')
|
||||
raise DrmException(u"Failed to initialize AES key")
|
||||
|
||||
def decrypt(self, data):
|
||||
out = create_string_buffer(len(data))
|
||||
@@ -95,7 +98,7 @@ def _load_crypto_libcrypto():
|
||||
keyctx = self._keyctx
|
||||
rv = AES_cbc_encrypt(data, out, len(data), keyctx, mutable_iv, 0)
|
||||
if rv == 0:
|
||||
raise DrmException('AES decryption failed')
|
||||
raise DrmException(u"AES decryption failed")
|
||||
return out.raw
|
||||
|
||||
def keyivgen(self, passwd, salt, iter, keylen):
|
||||
@@ -139,20 +142,20 @@ def SHA256(message):
|
||||
return ctx.digest()
|
||||
|
||||
# Various character maps used to decrypt books. Probably supposed to act as obfuscation
|
||||
charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M"
|
||||
charMap2 = "ZB0bYyc1xDdW2wEV3Ff7KkPpL8UuGA4gz-Tme9Nn_tHh5SvXCsIiR6rJjQaqlOoM"
|
||||
charMap1 = 'n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M'
|
||||
charMap2 = 'ZB0bYyc1xDdW2wEV3Ff7KkPpL8UuGA4gz-Tme9Nn_tHh5SvXCsIiR6rJjQaqlOoM'
|
||||
|
||||
# For kinf approach of K4Mac 1.6.X or later
|
||||
# On K4PC charMap5 = "AzB0bYyCeVvaZ3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_c1XxDdW2wE"
|
||||
# On K4PC charMap5 = 'AzB0bYyCeVvaZ3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_c1XxDdW2wE'
|
||||
# For Mac they seem to re-use charMap2 here
|
||||
charMap5 = charMap2
|
||||
|
||||
# new in K4M 1.9.X
|
||||
testMap8 = "YvaZ3FfUm9Nn_c1XuG4yCAzB0beVg-TtHh5SsIiR6rJjQdW2wEq7KkPpL8lOoMxD"
|
||||
testMap8 = 'YvaZ3FfUm9Nn_c1XuG4yCAzB0beVg-TtHh5SsIiR6rJjQdW2wEq7KkPpL8lOoMxD'
|
||||
|
||||
|
||||
def encode(data, map):
|
||||
result = ""
|
||||
result = ''
|
||||
for char in data:
|
||||
value = ord(char)
|
||||
Q = (value ^ 0x80) // len(map)
|
||||
@@ -167,14 +170,14 @@ def encodeHash(data,map):
|
||||
|
||||
# Decode the string in data with the characters in map. Returns the decoded bytes
|
||||
def decode(data,map):
|
||||
result = ""
|
||||
result = ''
|
||||
for i in range (0,len(data)-1,2):
|
||||
high = map.find(data[i])
|
||||
low = map.find(data[i+1])
|
||||
if (high == -1) or (low == -1) :
|
||||
break
|
||||
value = (((high * len(map)) ^ 0x80) & 0xFF) + low
|
||||
result += pack("B",value)
|
||||
result += pack('B',value)
|
||||
return result
|
||||
|
||||
# For K4M 1.6.X and later
|
||||
@@ -200,7 +203,7 @@ def primes(n):
|
||||
|
||||
|
||||
# uses a sub process to get the Hard Drive Serial Number using ioreg
|
||||
# returns with the serial number of drive whose BSD Name is "disk0"
|
||||
# returns with the serial number of drive whose BSD Name is 'disk0'
|
||||
def GetVolumeSerialNumber():
|
||||
sernum = os.getenv('MYSERIALNUMBER')
|
||||
if sernum != None:
|
||||
@@ -216,11 +219,11 @@ def GetVolumeSerialNumber():
|
||||
foundIt = False
|
||||
for j in xrange(cnt):
|
||||
resline = reslst[j]
|
||||
pp = resline.find('"Serial Number" = "')
|
||||
pp = resline.find('\"Serial Number\" = \"')
|
||||
if pp >= 0:
|
||||
sernum = resline[pp+19:-1]
|
||||
sernum = sernum.strip()
|
||||
bb = resline.find('"BSD Name" = "')
|
||||
bb = resline.find('\"BSD Name\" = \"')
|
||||
if bb >= 0:
|
||||
bsdname = resline[bb+14:-1]
|
||||
bsdname = bsdname.strip()
|
||||
@@ -277,7 +280,7 @@ def GetDiskPartitionUUID(diskpart):
|
||||
nest += 1
|
||||
if resline.find('}') >= 0:
|
||||
nest -= 1
|
||||
pp = resline.find('"UUID" = "')
|
||||
pp = resline.find('\"UUID\" = \"')
|
||||
if pp >= 0:
|
||||
uuidnum = resline[pp+10:-1]
|
||||
uuidnum = uuidnum.strip()
|
||||
@@ -285,7 +288,7 @@ def GetDiskPartitionUUID(diskpart):
|
||||
if partnest == uuidnest and uuidnest > 0:
|
||||
foundIt = True
|
||||
break
|
||||
bb = resline.find('"BSD Name" = "')
|
||||
bb = resline.find('\"BSD Name\" = \"')
|
||||
if bb >= 0:
|
||||
bsdname = resline[bb+14:-1]
|
||||
bsdname = bsdname.strip()
|
||||
@@ -323,7 +326,7 @@ def GetMACAddressMunged():
|
||||
if pp >= 0:
|
||||
macnum = resline[pp+6:-1]
|
||||
macnum = macnum.strip()
|
||||
# print "original mac", macnum
|
||||
# print 'original mac', macnum
|
||||
# now munge it up the way Kindle app does
|
||||
# by xoring it with 0xa5 and swapping elements 3 and 4
|
||||
maclst = macnum.split(':')
|
||||
@@ -340,7 +343,7 @@ def GetMACAddressMunged():
|
||||
mlst[2] = maclst[2] ^ 0xa5
|
||||
mlst[1] = maclst[1] ^ 0xa5
|
||||
mlst[0] = maclst[0] ^ 0xa5
|
||||
macnum = "%0.2x%0.2x%0.2x%0.2x%0.2x%0.2x" % (mlst[0], mlst[1], mlst[2], mlst[3], mlst[4], mlst[5])
|
||||
macnum = '%0.2x%0.2x%0.2x%0.2x%0.2x%0.2x' % (mlst[0], mlst[1], mlst[2], mlst[3], mlst[4], mlst[5])
|
||||
foundIt = True
|
||||
break
|
||||
if not foundIt:
|
||||
@@ -367,6 +370,19 @@ def isNewInstall():
|
||||
return False
|
||||
|
||||
|
||||
class Memoize:
|
||||
"""Memoize(fn) - an instance which acts like fn but memoizes its arguments
|
||||
Will only work on functions with non-mutable arguments
|
||||
"""
|
||||
def __init__(self, fn):
|
||||
self.fn = fn
|
||||
self.memo = {}
|
||||
def __call__(self, *args):
|
||||
if not self.memo.has_key(args):
|
||||
self.memo[args] = self.fn(*args)
|
||||
return self.memo[args]
|
||||
|
||||
@Memoize
|
||||
def GetIDString():
|
||||
# K4Mac now has an extensive set of ids strings it uses
|
||||
# in encoding pids and in creating unique passwords
|
||||
@@ -530,7 +546,8 @@ def getKindleInfoFiles():
|
||||
# determine type of kindle info provided and return a
|
||||
# database of keynames and values
|
||||
def getDBfromFile(kInfoFile):
|
||||
names = ["kindle.account.tokens","kindle.cookie.item","eulaVersionAccepted","login_date","kindle.token.item","login","kindle.key.item","kindle.name.info","kindle.device.info", "MazamaRandomNumber", "max_date", "SIGVERIF"]
|
||||
|
||||
names = ['kindle.account.tokens','kindle.cookie.item','eulaVersionAccepted','login_date','kindle.token.item','login','kindle.key.item','kindle.name.info','kindle.device.info', 'MazamaRandomNumber', 'max_date', 'SIGVERIF']
|
||||
DB = {}
|
||||
cnt = 0
|
||||
infoReader = open(kInfoFile, 'r')
|
||||
@@ -545,12 +562,12 @@ def getDBfromFile(kInfoFile):
|
||||
for item in items:
|
||||
if item != '':
|
||||
keyhash, rawdata = item.split(':')
|
||||
keyname = "unknown"
|
||||
keyname = 'unknown'
|
||||
for name in names:
|
||||
if encodeHash(name,charMap2) == keyhash:
|
||||
keyname = name
|
||||
break
|
||||
if keyname == "unknown":
|
||||
if keyname == 'unknown':
|
||||
keyname = keyhash
|
||||
encryptedValue = decode(rawdata,charMap2)
|
||||
cleartext = cud.decrypt(encryptedValue)
|
||||
@@ -563,8 +580,8 @@ def getDBfromFile(kInfoFile):
|
||||
if hdr == '/':
|
||||
|
||||
# else newer style .kinf file used by K4Mac >= 1.6.0
|
||||
# the .kinf file uses "/" to separate it into records
|
||||
# so remove the trailing "/" to make it easy to use split
|
||||
# the .kinf file uses '/' to separate it into records
|
||||
# so remove the trailing '/' to make it easy to use split
|
||||
data = data[:-1]
|
||||
items = data.split('/')
|
||||
cud = CryptUnprotectDataV2()
|
||||
@@ -578,11 +595,11 @@ def getDBfromFile(kInfoFile):
|
||||
# the first 32 chars of the first record of a group
|
||||
# is the MD5 hash of the key name encoded by charMap5
|
||||
keyhash = item[0:32]
|
||||
keyname = "unknown"
|
||||
keyname = 'unknown'
|
||||
|
||||
# the raw keyhash string is also used to create entropy for the actual
|
||||
# CryptProtectData Blob that represents that keys contents
|
||||
# "entropy" not used for K4Mac only K4PC
|
||||
# 'entropy' not used for K4Mac only K4PC
|
||||
# entropy = SHA1(keyhash)
|
||||
|
||||
# the remainder of the first record when decoded with charMap5
|
||||
@@ -599,12 +616,12 @@ def getDBfromFile(kInfoFile):
|
||||
item = items.pop(0)
|
||||
edlst.append(item)
|
||||
|
||||
keyname = "unknown"
|
||||
keyname = 'unknown'
|
||||
for name in names:
|
||||
if encodeHash(name,charMap5) == keyhash:
|
||||
keyname = name
|
||||
break
|
||||
if keyname == "unknown":
|
||||
if keyname == 'unknown':
|
||||
keyname = keyhash
|
||||
|
||||
# the charMap5 encoded contents data has had a length
|
||||
@@ -615,10 +632,10 @@ def getDBfromFile(kInfoFile):
|
||||
|
||||
# The offset into the charMap5 encoded contents seems to be:
|
||||
# len(contents) - largest prime number less than or equal to int(len(content)/3)
|
||||
# (in other words split "about" 2/3rds of the way through)
|
||||
# (in other words split 'about' 2/3rds of the way through)
|
||||
|
||||
# move first offsets chars to end to align for decode by charMap5
|
||||
encdata = "".join(edlst)
|
||||
encdata = ''.join(edlst)
|
||||
contlen = len(encdata)
|
||||
|
||||
# now properly split and recombine
|
||||
@@ -667,7 +684,7 @@ def getDBfromFile(kInfoFile):
|
||||
# the first 32 chars of the first record of a group
|
||||
# is the MD5 hash of the key name encoded by charMap5
|
||||
keyhash = item[0:32]
|
||||
keyname = "unknown"
|
||||
keyname = 'unknown'
|
||||
|
||||
# unlike K4PC the keyhash is not used in generating entropy
|
||||
# entropy = SHA1(keyhash) + added_entropy
|
||||
@@ -687,12 +704,12 @@ def getDBfromFile(kInfoFile):
|
||||
item = items.pop(0)
|
||||
edlst.append(item)
|
||||
|
||||
keyname = "unknown"
|
||||
keyname = 'unknown'
|
||||
for name in names:
|
||||
if encodeHash(name,testMap8) == keyhash:
|
||||
keyname = name
|
||||
break
|
||||
if keyname == "unknown":
|
||||
if keyname == 'unknown':
|
||||
keyname = keyhash
|
||||
|
||||
# the testMap8 encoded contents data has had a length
|
||||
@@ -703,10 +720,10 @@ def getDBfromFile(kInfoFile):
|
||||
|
||||
# The offset into the testMap8 encoded contents seems to be:
|
||||
# len(contents) - largest prime number less than or equal to int(len(content)/3)
|
||||
# (in other words split "about" 2/3rds of the way through)
|
||||
# (in other words split 'about' 2/3rds of the way through)
|
||||
|
||||
# move first offsets chars to end to align for decode by testMap8
|
||||
encdata = "".join(edlst)
|
||||
encdata = ''.join(edlst)
|
||||
contlen = len(encdata)
|
||||
|
||||
# now properly split and recombine
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# K4PC Windows specific routines
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import with_statement
|
||||
import sys
|
||||
@@ -17,26 +18,24 @@ global charMap4
|
||||
|
||||
if 'calibre' in sys.modules:
|
||||
inCalibre = True
|
||||
else:
|
||||
inCalibre = False
|
||||
|
||||
if inCalibre:
|
||||
if sys.platform.startswith('win'):
|
||||
from calibre.constants import iswindows, isosx
|
||||
if iswindows:
|
||||
from calibre_plugins.k4mobidedrm.k4pcutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString
|
||||
|
||||
if sys.platform.startswith('darwin'):
|
||||
if isosx:
|
||||
from calibre_plugins.k4mobidedrm.k4mutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString
|
||||
else:
|
||||
if sys.platform.startswith('win'):
|
||||
inCalibre = False
|
||||
iswindows = sys.platform.startswith('win')
|
||||
isosx = sys.platform.startswith('darwin')
|
||||
if iswindows:
|
||||
from k4pcutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString
|
||||
|
||||
if sys.platform.startswith('darwin'):
|
||||
if isosx:
|
||||
from k4mutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString
|
||||
|
||||
|
||||
charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M"
|
||||
charMap3 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
||||
charMap4 = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789"
|
||||
charMap1 = 'n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M'
|
||||
charMap3 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
|
||||
charMap4 = 'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789'
|
||||
|
||||
# crypto digestroutines
|
||||
import hashlib
|
||||
@@ -54,7 +53,7 @@ def SHA1(message):
|
||||
|
||||
# Encode the bytes in data with the characters in map
|
||||
def encode(data, map):
|
||||
result = ""
|
||||
result = ''
|
||||
for char in data:
|
||||
value = ord(char)
|
||||
Q = (value ^ 0x80) // len(map)
|
||||
@@ -69,14 +68,14 @@ def encodeHash(data,map):
|
||||
|
||||
# Decode the string in data with the characters in map. Returns the decoded bytes
|
||||
def decode(data,map):
|
||||
result = ""
|
||||
result = ''
|
||||
for i in range (0,len(data)-1,2):
|
||||
high = map.find(data[i])
|
||||
low = map.find(data[i+1])
|
||||
if (high == -1) or (low == -1) :
|
||||
break
|
||||
value = (((high * len(map)) ^ 0x80) & 0xFF) + low
|
||||
result += pack("B",value)
|
||||
result += pack('B',value)
|
||||
return result
|
||||
|
||||
#
|
||||
@@ -98,7 +97,7 @@ def getSixBitsFromBitField(bitField,offset):
|
||||
# 8 bits to six bits encoding from hash to generate PID string
|
||||
def encodePID(hash):
|
||||
global charMap3
|
||||
PID = ""
|
||||
PID = ''
|
||||
for position in range (0,8):
|
||||
PID += charMap3[getSixBitsFromBitField(hash,position)]
|
||||
return PID
|
||||
@@ -129,7 +128,7 @@ def generatePidSeed(table,dsn) :
|
||||
def generateDevicePID(table,dsn,nbRoll):
|
||||
global charMap4
|
||||
seed = generatePidSeed(table,dsn)
|
||||
pidAscii = ""
|
||||
pidAscii = ''
|
||||
pid = [(seed >>24) &0xFF,(seed >> 16) &0xff,(seed >> 8) &0xFF,(seed) & 0xFF,(seed>>24) & 0xFF,(seed >> 16) &0xff,(seed >> 8) &0xFF,(seed) & 0xFF]
|
||||
index = 0
|
||||
for counter in range (0,nbRoll):
|
||||
@@ -176,28 +175,31 @@ def pidFromSerial(s, l):
|
||||
|
||||
|
||||
# Parse the EXTH header records and use the Kindle serial number to calculate the book pid.
|
||||
def getKindlePid(pidlst, rec209, token, serialnum):
|
||||
def getKindlePids(rec209, token, serialnum):
|
||||
pids=[]
|
||||
|
||||
# Compute book PID
|
||||
pidHash = SHA1(serialnum+rec209+token)
|
||||
bookPID = encodePID(pidHash)
|
||||
bookPID = checksumPid(bookPID)
|
||||
pidlst.append(bookPID)
|
||||
pids.append(bookPID)
|
||||
|
||||
# compute fixed pid for old pre 2.5 firmware update pid as well
|
||||
bookPID = pidFromSerial(serialnum, 7) + "*"
|
||||
bookPID = checksumPid(bookPID)
|
||||
pidlst.append(bookPID)
|
||||
kindlePID = pidFromSerial(serialnum, 7) + "*"
|
||||
kindlePID = checksumPid(kindlePID)
|
||||
pids.append(kindlePID)
|
||||
|
||||
return pidlst
|
||||
return pids
|
||||
|
||||
|
||||
# parse the Kindleinfo file to calculate the book pid.
|
||||
|
||||
keynames = ["kindle.account.tokens","kindle.cookie.item","eulaVersionAccepted","login_date","kindle.token.item","login","kindle.key.item","kindle.name.info","kindle.device.info", "MazamaRandomNumber"]
|
||||
keynames = ['kindle.account.tokens','kindle.cookie.item','eulaVersionAccepted','login_date','kindle.token.item','login','kindle.key.item','kindle.name.info','kindle.device.info', 'MazamaRandomNumber']
|
||||
|
||||
def getK4Pids(pidlst, rec209, token, kInfoFile):
|
||||
def getK4Pids(rec209, token, kInfoFile):
|
||||
global charMap1
|
||||
kindleDatabase = None
|
||||
pids = []
|
||||
try:
|
||||
kindleDatabase = getDBfromFile(kInfoFile)
|
||||
except Exception, message:
|
||||
@@ -206,17 +208,17 @@ def getK4Pids(pidlst, rec209, token, kInfoFile):
|
||||
pass
|
||||
|
||||
if kindleDatabase == None :
|
||||
return pidlst
|
||||
return pids
|
||||
|
||||
try:
|
||||
# Get the Mazama Random number
|
||||
MazamaRandomNumber = kindleDatabase["MazamaRandomNumber"]
|
||||
MazamaRandomNumber = kindleDatabase['MazamaRandomNumber']
|
||||
|
||||
# Get the kindle account token
|
||||
kindleAccountToken = kindleDatabase["kindle.account.tokens"]
|
||||
kindleAccountToken = kindleDatabase['kindle.account.tokens']
|
||||
except KeyError:
|
||||
print "Keys not found in " + kInfoFile
|
||||
return pidlst
|
||||
print u"Keys not found in {0}".format(os.path.basename(kInfoFile))
|
||||
return pids
|
||||
|
||||
# Get the ID string used
|
||||
encodedIDString = encodeHash(GetIDString(),charMap1)
|
||||
@@ -231,7 +233,7 @@ def getK4Pids(pidlst, rec209, token, kInfoFile):
|
||||
table = generatePidEncryptionTable()
|
||||
devicePID = generateDevicePID(table,DSN,4)
|
||||
devicePID = checksumPid(devicePID)
|
||||
pidlst.append(devicePID)
|
||||
pids.append(devicePID)
|
||||
|
||||
# Compute book PIDs
|
||||
|
||||
@@ -239,36 +241,38 @@ def getK4Pids(pidlst, rec209, token, kInfoFile):
|
||||
pidHash = SHA1(DSN+kindleAccountToken+rec209+token)
|
||||
bookPID = encodePID(pidHash)
|
||||
bookPID = checksumPid(bookPID)
|
||||
pidlst.append(bookPID)
|
||||
pids.append(bookPID)
|
||||
|
||||
# variant 1
|
||||
pidHash = SHA1(kindleAccountToken+rec209+token)
|
||||
bookPID = encodePID(pidHash)
|
||||
bookPID = checksumPid(bookPID)
|
||||
pidlst.append(bookPID)
|
||||
pids.append(bookPID)
|
||||
|
||||
# variant 2
|
||||
pidHash = SHA1(DSN+rec209+token)
|
||||
bookPID = encodePID(pidHash)
|
||||
bookPID = checksumPid(bookPID)
|
||||
pidlst.append(bookPID)
|
||||
pids.append(bookPID)
|
||||
|
||||
return pidlst
|
||||
return pids
|
||||
|
||||
def getPidList(md1, md2, k4 = True, serials=[], kInfoFiles=[]):
|
||||
def getPidList(md1, md2, serials=[], kInfoFiles=[]):
|
||||
pidlst = []
|
||||
if kInfoFiles is None:
|
||||
kInfoFiles = []
|
||||
if k4:
|
||||
if serials is None:
|
||||
serials = []
|
||||
if iswindows or isosx:
|
||||
kInfoFiles.extend(getKindleInfoFiles())
|
||||
for infoFile in kInfoFiles:
|
||||
try:
|
||||
pidlst = getK4Pids(pidlst, md1, md2, infoFile)
|
||||
except Exception, message:
|
||||
print("Error getting PIDs from " + infoFile + ": " + message)
|
||||
pidlst.extend(getK4Pids(md1, md2, infoFile))
|
||||
except Exception, e:
|
||||
print u"Error getting PIDs from {0}: {1}".format(os.path.basename(infoFile),e.args[0])
|
||||
for serialnum in serials:
|
||||
try:
|
||||
pidlst = getKindlePid(pidlst, md1, md2, serialnum)
|
||||
pidlst.extend(getKindlePids(md1, md2, serialnum))
|
||||
except Exception, message:
|
||||
print("Error getting PIDs from " + serialnum + ": " + message)
|
||||
print u"Error getting PIDs from serial number {0}: {1}".format(serialnum ,e.args[0])
|
||||
return pidlst
|
||||
|
||||
@@ -1,29 +1,80 @@
|
||||
#!/usr/bin/python
|
||||
# Mobipocket PID calculator v0.2 for Amazon Kindle.
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Mobipocket PID calculator v0.4 for Amazon Kindle.
|
||||
# Copyright (c) 2007, 2009 Igor Skochinsky <skochinsky@mail.ru>
|
||||
# History:
|
||||
# 0.1 Initial release
|
||||
# 0.2 Added support for generating PID for iPhone (thanks to mbp)
|
||||
# 0.3 changed to autoflush stdout, fixed return code usage
|
||||
class Unbuffered:
|
||||
# 0.3 updated for unicode
|
||||
|
||||
import sys
|
||||
import binascii
|
||||
|
||||
# Wrap a stream so that output gets flushed immediately
|
||||
# and also make sure that any unicode strings get
|
||||
# encoded using "replace" before writing them.
|
||||
class SafeUnbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
self.encoding = stream.encoding
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
def write(self, data):
|
||||
if isinstance(data,unicode):
|
||||
data = data.encode(self.encoding,"replace")
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
import sys
|
||||
sys.stdout=Unbuffered(sys.stdout)
|
||||
iswindows = sys.platform.startswith('win')
|
||||
isosx = sys.platform.startswith('darwin')
|
||||
|
||||
import binascii
|
||||
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]
|
||||
|
||||
if sys.hexversion >= 0x3000000:
|
||||
print "This script is incompatible with Python 3.x. Please install Python 2.6.x from python.org"
|
||||
print 'This script is incompatible with Python 3.x. Please install Python 2.7.x.'
|
||||
sys.exit(2)
|
||||
|
||||
letters = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789"
|
||||
letters = 'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789'
|
||||
|
||||
def crc32(s):
|
||||
return (~binascii.crc32(s,-1))&0xFFFFFFFF
|
||||
@@ -53,39 +104,39 @@ def pidFromSerial(s, l):
|
||||
for i in xrange(l):
|
||||
arr1[i] ^= crc_bytes[i&3]
|
||||
|
||||
pid = ""
|
||||
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"
|
||||
def cli_main(argv=unicode_argv()):
|
||||
print u"Mobipocket PID calculator for Amazon Kindle. Copyright © 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>"
|
||||
print u"Usage: kindlepid.py <Kindle Serial Number>/<iPhone/iPod Touch UDID>"
|
||||
return 1
|
||||
if len(serial)==16:
|
||||
if serial.startswith("B"):
|
||||
print "Kindle serial number detected"
|
||||
print u"Kindle serial number detected"
|
||||
else:
|
||||
print "Warning: unrecognized serial number. Please recheck input."
|
||||
print u"Warning: unrecognized serial number. Please recheck input."
|
||||
return 1
|
||||
pid = pidFromSerial(serial,7)+"*"
|
||||
print "Mobipocket PID for Kindle serial# "+serial+" is "+checksumPid(pid)
|
||||
pid = pidFromSerial(serial.encode("utf-8"),7)+'*'
|
||||
print u"Mobipocket PID for Kindle serial#{0} is {1} ".format(serial,checksumPid(pid))
|
||||
return 0
|
||||
elif len(serial)==40:
|
||||
print "iPhone serial number (UDID) detected"
|
||||
pid = pidFromSerial(serial,8)
|
||||
print "Mobipocket PID for iPhone serial# "+serial+" is "+checksumPid(pid)
|
||||
print u"iPhone serial number (UDID) detected"
|
||||
pid = pidFromSerial(serial.encode("utf-8"),8)
|
||||
print u"Mobipocket PID for iPhone serial#{0} is {1} ".format(serial,checksumPid(pid))
|
||||
return 0
|
||||
else:
|
||||
print "Warning: unrecognized serial number. Please recheck input."
|
||||
return 1
|
||||
return 0
|
||||
print u"Warning: unrecognized serial number. Please recheck input."
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
sys.exit(cli_main())
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
#!/usr/bin/python
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# mobidedrm.py, version 0.38
|
||||
# Copyright © 2008 The Dark Reverser
|
||||
#
|
||||
# Modified 2008–2012 by some_updates, DiapDealer and Apprentice Alf
|
||||
|
||||
# This is a python script. You need a Python interpreter to run it.
|
||||
# For example, ActiveState Python, which exists for windows.
|
||||
#
|
||||
@@ -59,26 +65,78 @@
|
||||
# 0.35 - add interface to get mobi_version
|
||||
# 0.36 - fixed problem with TEXtREAd and getBookTitle interface
|
||||
# 0.37 - Fixed double announcement for stand-alone operation
|
||||
# 0.38 - Unicode used wherever possible, cope with absent alfcrypto
|
||||
|
||||
|
||||
__version__ = '0.37'
|
||||
__version__ = u"0.38"
|
||||
|
||||
import sys
|
||||
import os
|
||||
import struct
|
||||
import binascii
|
||||
try:
|
||||
from alfcrypto import Pukall_Cipher
|
||||
except:
|
||||
print u"AlfCrypto not found. Using python PC1 implementation."
|
||||
|
||||
class Unbuffered:
|
||||
# Wrap a stream so that output gets flushed immediately
|
||||
# and also make sure that any unicode strings get
|
||||
# encoded using "replace" before writing them.
|
||||
class SafeUnbuffered:
|
||||
def __init__(self, stream):
|
||||
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)
|
||||
sys.stdout=Unbuffered(sys.stdout)
|
||||
|
||||
import os
|
||||
import struct
|
||||
import binascii
|
||||
from alfcrypto import Pukall_Cipher
|
||||
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]
|
||||
|
||||
|
||||
class DrmException(Exception):
|
||||
pass
|
||||
@@ -90,40 +148,45 @@ class DrmException(Exception):
|
||||
|
||||
# Implementation of Pukall Cipher 1
|
||||
def PC1(key, src, decryption=True):
|
||||
return Pukall_Cipher().PC1(key,src,decryption)
|
||||
# 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
|
||||
# if we can get it from alfcrypto, use that
|
||||
try:
|
||||
return Pukall_Cipher().PC1(key,src,decryption)
|
||||
except NameError:
|
||||
pass
|
||||
|
||||
# use slow python version, since Pukall_Cipher didn't load
|
||||
sum1 = 0;
|
||||
sum2 = 0;
|
||||
keyXorVal = 0;
|
||||
if len(key)!=16:
|
||||
DrmException (u"PC1: Bad key length")
|
||||
wkey = []
|
||||
for i in xrange(8):
|
||||
wkey.append(ord(key[i*2])<<8 | ord(key[i*2+1]))
|
||||
dst = ""
|
||||
for i in xrange(len(src)):
|
||||
temp1 = 0;
|
||||
byteXorVal = 0;
|
||||
for j in xrange(8):
|
||||
temp1 ^= wkey[j]
|
||||
sum2 = (sum2+j)*20021 + sum1
|
||||
sum1 = (temp1*346)&0xFFFF
|
||||
sum2 = (sum2+sum1)&0xFFFF
|
||||
temp1 = (temp1*20021+1)&0xFFFF
|
||||
byteXorVal ^= temp1 ^ sum2
|
||||
curByte = ord(src[i])
|
||||
if not decryption:
|
||||
keyXorVal = curByte * 257;
|
||||
curByte = ((curByte ^ (byteXorVal >> 8)) ^ byteXorVal) & 0xFF
|
||||
if decryption:
|
||||
keyXorVal = curByte * 257;
|
||||
for j in xrange(8):
|
||||
wkey[j] ^= keyXorVal;
|
||||
dst+=chr(curByte)
|
||||
return dst
|
||||
|
||||
def checksumPid(s):
|
||||
letters = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789"
|
||||
letters = 'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789'
|
||||
crc = (~binascii.crc32(s,-1))&0xFFFFFFFF
|
||||
crc = crc ^ (crc >> 16)
|
||||
res = s
|
||||
@@ -171,17 +234,24 @@ class MobiBook:
|
||||
off = self.sections[section][0]
|
||||
return self.data_file[off:endoff]
|
||||
|
||||
def __init__(self, infile, announce = True):
|
||||
if announce:
|
||||
print ('MobiDeDrm v%(__version__)s. '
|
||||
'Copyright 2008-2012 The Dark Reverser et al.' % globals())
|
||||
def cleanup(self):
|
||||
# to match function in Topaz book
|
||||
pass
|
||||
|
||||
def __init__(self, infile):
|
||||
print u"MobiDeDrm v{0:s}.\nCopyright © 2008-2012 The Dark Reverser et al.".format(__version__)
|
||||
|
||||
try:
|
||||
from alfcrypto import Pukall_Cipher
|
||||
except:
|
||||
print u"AlfCrypto not found. Using python PC1 implementation."
|
||||
|
||||
# initial sanity check on file
|
||||
self.data_file = file(infile, 'rb').read()
|
||||
self.mobi_data = ''
|
||||
self.header = self.data_file[0:78]
|
||||
if self.header[0x3C:0x3C+8] != 'BOOKMOBI' and self.header[0x3C:0x3C+8] != 'TEXtREAd':
|
||||
raise DrmException("invalid file format")
|
||||
raise DrmException(u"Invalid file format")
|
||||
self.magic = self.header[0x3C:0x3C+8]
|
||||
self.crypto_type = -1
|
||||
|
||||
@@ -199,7 +269,7 @@ class MobiBook:
|
||||
self.compression, = struct.unpack('>H', self.sect[0x0:0x0+2])
|
||||
|
||||
if self.magic == 'TEXtREAd':
|
||||
print "Book has format: ", self.magic
|
||||
print u"PalmDoc format book detected."
|
||||
self.extra_data_flags = 0
|
||||
self.mobi_length = 0
|
||||
self.mobi_codepage = 1252
|
||||
@@ -209,11 +279,11 @@ class MobiBook:
|
||||
self.mobi_length, = struct.unpack('>L',self.sect[0x14:0x18])
|
||||
self.mobi_codepage, = struct.unpack('>L',self.sect[0x1c:0x20])
|
||||
self.mobi_version, = struct.unpack('>L',self.sect[0x68:0x6C])
|
||||
print "MOBI header version = %d, length = %d" %(self.mobi_version, self.mobi_length)
|
||||
print u"MOBI header version {0:d}, header length {1:d}".format(self.mobi_version, self.mobi_length)
|
||||
self.extra_data_flags = 0
|
||||
if (self.mobi_length >= 0xE4) and (self.mobi_version >= 5):
|
||||
self.extra_data_flags, = struct.unpack('>H', self.sect[0xF2:0xF4])
|
||||
print "Extra Data Flags = %d" % self.extra_data_flags
|
||||
print u"Extra Data Flags: {0:d}".format(self.extra_data_flags)
|
||||
if (self.compression != 17480):
|
||||
# multibyte utf8 data is included in the encryption for PalmDoc compression
|
||||
# so clear that byte so that we leave it to be decrypted.
|
||||
@@ -223,10 +293,10 @@ class MobiBook:
|
||||
self.meta_array = {}
|
||||
try:
|
||||
exth_flag, = struct.unpack('>L', self.sect[0x80:0x84])
|
||||
exth = 'NONE'
|
||||
exth = ''
|
||||
if exth_flag & 0x40:
|
||||
exth = self.sect[16 + self.mobi_length:]
|
||||
if (len(exth) >= 4) and (exth[:4] == 'EXTH'):
|
||||
if (len(exth) >= 12) and (exth[:4] == 'EXTH'):
|
||||
nitems, = struct.unpack('>I', exth[8:12])
|
||||
pos = 12
|
||||
for i in xrange(nitems):
|
||||
@@ -236,10 +306,10 @@ class MobiBook:
|
||||
# reset the text to speech flag and clipping limit, if present
|
||||
if type == 401 and size == 9:
|
||||
# set clipping limit to 100%
|
||||
self.patchSection(0, "\144", 16 + self.mobi_length + pos + 8)
|
||||
self.patchSection(0, '\144', 16 + self.mobi_length + pos + 8)
|
||||
elif type == 404 and size == 9:
|
||||
# make sure text to speech is enabled
|
||||
self.patchSection(0, "\0", 16 + self.mobi_length + pos + 8)
|
||||
self.patchSection(0, '\0', 16 + self.mobi_length + pos + 8)
|
||||
# print type, size, content, content.encode('hex')
|
||||
pos += size
|
||||
except:
|
||||
@@ -265,8 +335,8 @@ class MobiBook:
|
||||
codec = codec_map[self.mobi_codepage]
|
||||
if title == '':
|
||||
title = self.header[:32]
|
||||
title = title.split("\0")[0]
|
||||
return unicode(title, codec).encode('utf-8')
|
||||
title = title.split('\0')[0]
|
||||
return unicode(title, codec)
|
||||
|
||||
def getPIDMetaInfo(self):
|
||||
rec209 = ''
|
||||
@@ -297,7 +367,7 @@ class MobiBook:
|
||||
|
||||
def parseDRM(self, data, count, pidlist):
|
||||
found_key = None
|
||||
keyvec1 = "\x72\x38\x33\xB0\xB4\xF2\xE3\xCA\xDF\x09\x01\xD6\xE2\xE0\x3F\x96"
|
||||
keyvec1 = '\x72\x38\x33\xB0\xB4\xF2\xE3\xCA\xDF\x09\x01\xD6\xE2\xE0\x3F\x96'
|
||||
for pid in pidlist:
|
||||
bigpid = pid.ljust(16,'\0')
|
||||
temp_key = PC1(keyvec1, bigpid, False)
|
||||
@@ -315,7 +385,7 @@ class MobiBook:
|
||||
break
|
||||
if not found_key:
|
||||
# Then try the default encoding that doesn't require a PID
|
||||
pid = "00000000"
|
||||
pid = '00000000'
|
||||
temp_key = keyvec1
|
||||
temp_key_sum = sum(map(ord,temp_key)) & 0xff
|
||||
for i in xrange(count):
|
||||
@@ -328,82 +398,90 @@ class MobiBook:
|
||||
break
|
||||
return [found_key,pid]
|
||||
|
||||
def getMobiFile(self, outpath):
|
||||
def getFile(self, outpath):
|
||||
file(outpath,'wb').write(self.mobi_data)
|
||||
|
||||
def getMobiVersion(self):
|
||||
return self.mobi_version
|
||||
def getBookType(self):
|
||||
if self.print_replica:
|
||||
return u"Print Replica"
|
||||
if self.mobi_version >= 8:
|
||||
return u"Kindle Format 8"
|
||||
return u"Mobipocket"
|
||||
|
||||
def getPrintReplica(self):
|
||||
return self.print_replica
|
||||
def getBookExtension(self):
|
||||
if self.print_replica:
|
||||
return u".azw4"
|
||||
if self.mobi_version >= 8:
|
||||
return u".azw3"
|
||||
return u".mobi"
|
||||
|
||||
def processBook(self, pidlist):
|
||||
crypto_type, = struct.unpack('>H', self.sect[0xC:0xC+2])
|
||||
print 'Crypto Type is: ', crypto_type
|
||||
print u"Crypto Type is: {0:d}".format(crypto_type)
|
||||
self.crypto_type = crypto_type
|
||||
if crypto_type == 0:
|
||||
print "This book is not encrypted."
|
||||
print u"This book is not encrypted."
|
||||
# we must still check for Print Replica
|
||||
self.print_replica = (self.loadSection(1)[0:4] == '%MOP')
|
||||
self.mobi_data = self.data_file
|
||||
return
|
||||
if crypto_type != 2 and crypto_type != 1:
|
||||
raise DrmException("Cannot decode unknown Mobipocket encryption type %d" % crypto_type)
|
||||
raise DrmException(u"Cannot decode unknown Mobipocket encryption type {0:d}".format(crypto_type))
|
||||
if 406 in self.meta_array:
|
||||
data406 = self.meta_array[406]
|
||||
val406, = struct.unpack('>Q',data406)
|
||||
if val406 != 0:
|
||||
raise DrmException("Cannot decode library or rented ebooks.")
|
||||
raise DrmException(u"Cannot decode library or rented ebooks.")
|
||||
|
||||
goodpids = []
|
||||
for pid in pidlist:
|
||||
if len(pid)==10:
|
||||
if checksumPid(pid[0:-2]) != pid:
|
||||
print "Warning: PID " + pid + " has incorrect checksum, should have been "+checksumPid(pid[0:-2])
|
||||
print u"Warning: PID {0} has incorrect checksum, should have been {1}".format(pid,checksumPid(pid[0:-2]))
|
||||
goodpids.append(pid[0:-2])
|
||||
elif len(pid)==8:
|
||||
goodpids.append(pid)
|
||||
|
||||
if self.crypto_type == 1:
|
||||
t1_keyvec = "QDCVEPMU675RUBSZ"
|
||||
t1_keyvec = 'QDCVEPMU675RUBSZ'
|
||||
if self.magic == 'TEXtREAd':
|
||||
bookkey_data = self.sect[0x0E:0x0E+16]
|
||||
elif self.mobi_version < 0:
|
||||
bookkey_data = self.sect[0x90:0x90+16]
|
||||
else:
|
||||
bookkey_data = self.sect[self.mobi_length+16:self.mobi_length+32]
|
||||
pid = "00000000"
|
||||
pid = '00000000'
|
||||
found_key = PC1(t1_keyvec, bookkey_data)
|
||||
else :
|
||||
# calculate the keys
|
||||
drm_ptr, drm_count, drm_size, drm_flags = struct.unpack('>LLLL', self.sect[0xA8:0xA8+16])
|
||||
if drm_count == 0:
|
||||
raise DrmException("Not yet initialised with PID. Must be opened with Mobipocket Reader first.")
|
||||
raise DrmException(u"Encryption not initialised. Must be opened with Mobipocket Reader first.")
|
||||
found_key, pid = self.parseDRM(self.sect[drm_ptr:drm_ptr+drm_size], drm_count, goodpids)
|
||||
if not found_key:
|
||||
raise DrmException("No key found in " + str(len(goodpids)) + " keys tried. Read the FAQs at Alf's blog. Only if none apply, report this failure for help.")
|
||||
raise DrmException(u"No key found in {0:d} keys tried. Read the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(len(goodpids)))
|
||||
# kill the drm keys
|
||||
self.patchSection(0, "\0" * drm_size, drm_ptr)
|
||||
self.patchSection(0, '\0' * drm_size, drm_ptr)
|
||||
# kill the drm pointers
|
||||
self.patchSection(0, "\xff" * 4 + "\0" * 12, 0xA8)
|
||||
self.patchSection(0, '\xff' * 4 + '\0' * 12, 0xA8)
|
||||
|
||||
if pid=="00000000":
|
||||
print "File has default encryption, no specific PID."
|
||||
if pid=='00000000':
|
||||
print u"File has default encryption, no specific key needed."
|
||||
else:
|
||||
print "File is encoded with PID "+checksumPid(pid)+"."
|
||||
print u"File is encoded with PID {0}.".format(checksumPid(pid))
|
||||
|
||||
# clear the crypto type
|
||||
self.patchSection(0, "\0" * 2, 0xC)
|
||||
|
||||
# decrypt sections
|
||||
print "Decrypting. Please wait . . .",
|
||||
print u"Decrypting. Please wait . . .",
|
||||
mobidataList = []
|
||||
mobidataList.append(self.data_file[:self.sections[1][0]])
|
||||
for i in xrange(1, self.records+1):
|
||||
data = self.loadSection(i)
|
||||
extra_size = getSizeOfTrailingDataEntries(data, len(data), self.extra_data_flags)
|
||||
if i%100 == 0:
|
||||
print ".",
|
||||
print u".",
|
||||
# print "record %d, extra_size %d" %(i,extra_size)
|
||||
decoded_data = PC1(found_key, data[0:len(data) - extra_size])
|
||||
if i==1:
|
||||
@@ -414,31 +492,24 @@ class MobiBook:
|
||||
if self.num_sections > self.records+1:
|
||||
mobidataList.append(self.data_file[self.sections[self.records+1][0]:])
|
||||
self.mobi_data = "".join(mobidataList)
|
||||
print "done"
|
||||
print u"done"
|
||||
return
|
||||
|
||||
def getUnencryptedBook(infile,pid,announce=True):
|
||||
def getUnencryptedBook(infile,pidlist):
|
||||
if not os.path.isfile(infile):
|
||||
raise DrmException('Input File Not Found')
|
||||
book = MobiBook(infile,announce)
|
||||
book.processBook([pid])
|
||||
return book.mobi_data
|
||||
|
||||
def getUnencryptedBookWithList(infile,pidlist,announce=True):
|
||||
if not os.path.isfile(infile):
|
||||
raise DrmException('Input File Not Found')
|
||||
book = MobiBook(infile, announce)
|
||||
raise DrmException(u"Input File Not Found.")
|
||||
book = MobiBook(infile)
|
||||
book.processBook(pidlist)
|
||||
return book.mobi_data
|
||||
|
||||
|
||||
def main(argv=sys.argv):
|
||||
print ('MobiDeDrm v%(__version__)s. '
|
||||
'Copyright 2008-2012 The Dark Reverser et al.' % globals())
|
||||
def cli_main(argv=unicode_argv()):
|
||||
progname = os.path.basename(argv[0])
|
||||
if len(argv)<3 or len(argv)>4:
|
||||
print "Removes protection from Kindle/Mobipocket, Kindle/KF8 and Kindle/Print Replica ebooks"
|
||||
print "Usage:"
|
||||
print " %s <infile> <outfile> [<Comma separated list of PIDs to try>]" % sys.argv[0]
|
||||
print u"MobiDeDrm v{0}.\nCopyright © 2008-2012 The Dark Reverser et al.".format(__version__)
|
||||
print u"Removes protection from Kindle/Mobipocket, Kindle/KF8 and Kindle/Print Replica ebooks"
|
||||
print u"Usage:"
|
||||
print u" {0} <infile> <outfile> [<Comma separated list of PIDs to try>]".format(os.path.basename(sys.argv[0]))
|
||||
return 1
|
||||
else:
|
||||
infile = argv[1]
|
||||
@@ -446,15 +517,17 @@ def main(argv=sys.argv):
|
||||
if len(argv) is 4:
|
||||
pidlist = argv[3].split(',')
|
||||
else:
|
||||
pidlist = {}
|
||||
pidlist = []
|
||||
try:
|
||||
stripped_file = getUnencryptedBookWithList(infile, pidlist, False)
|
||||
stripped_file = getUnencryptedBook(infile, pidlist)
|
||||
file(outfile, 'wb').write(stripped_file)
|
||||
except DrmException, e:
|
||||
print "Error: %s" % e
|
||||
print u"MobiDeDRM v{0} Error: {0:s}".format(__version__,e.args[0])
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
if __name__ == '__main__':
|
||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
sys.exit(cli_main())
|
||||
|
||||
@@ -1,43 +1,90 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
class Unbuffered:
|
||||
# topazextract.py, version ?
|
||||
# Mostly written by some_updates based on code from many others
|
||||
|
||||
__version__ = '4.8'
|
||||
|
||||
import sys
|
||||
import os, csv, getopt
|
||||
import zlib, zipfile, tempfile, shutil
|
||||
import traceback
|
||||
from struct import pack
|
||||
from struct import unpack
|
||||
from alfcrypto import Topaz_Cipher
|
||||
|
||||
class SafeUnbuffered:
|
||||
def __init__(self, stream):
|
||||
self.stream = stream
|
||||
self.encoding = stream.encoding
|
||||
if self.encoding == None:
|
||||
self.encoding = "utf-8"
|
||||
def write(self, data):
|
||||
if isinstance(data,unicode):
|
||||
data = data.encode(self.encoding,"replace")
|
||||
self.stream.write(data)
|
||||
self.stream.flush()
|
||||
def __getattr__(self, attr):
|
||||
return getattr(self.stream, attr)
|
||||
|
||||
import sys
|
||||
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]
|
||||
|
||||
if 'calibre' in sys.modules:
|
||||
inCalibre = True
|
||||
else:
|
||||
inCalibre = False
|
||||
|
||||
buildXML = False
|
||||
|
||||
import os, csv, getopt
|
||||
import zlib, zipfile, tempfile, shutil
|
||||
from struct import pack
|
||||
from struct import unpack
|
||||
from alfcrypto import Topaz_Cipher
|
||||
|
||||
class TpzDRMError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
# local support routines
|
||||
if inCalibre:
|
||||
from calibre_plugins.k4mobidedrm import kgenpids
|
||||
else:
|
||||
inCalibre = False
|
||||
import kgenpids
|
||||
|
||||
|
||||
class DrmException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
# recursive zip creation support routine
|
||||
def zipUpDir(myzip, tdir, localname):
|
||||
currentdir = tdir
|
||||
if localname != "":
|
||||
if localname != u"":
|
||||
currentdir = os.path.join(currentdir,localname)
|
||||
list = os.listdir(currentdir)
|
||||
for file in list:
|
||||
@@ -73,7 +120,7 @@ def bookReadEncodedNumber(fo):
|
||||
# Get a length prefixed string from file
|
||||
def bookReadString(fo):
|
||||
stringLength = bookReadEncodedNumber(fo)
|
||||
return unpack(str(stringLength)+"s",fo.read(stringLength))[0]
|
||||
return unpack(str(stringLength)+'s',fo.read(stringLength))[0]
|
||||
|
||||
#
|
||||
# crypto routines
|
||||
@@ -112,13 +159,13 @@ def decryptRecord(data,PID):
|
||||
# Try to decrypt a dkey record (contains the bookPID)
|
||||
def decryptDkeyRecord(data,PID):
|
||||
record = decryptRecord(data,PID)
|
||||
fields = unpack("3sB8sB8s3s",record)
|
||||
if fields[0] != "PID" or fields[5] != "pid" :
|
||||
raise TpzDRMError("Didn't find PID magic numbers in record")
|
||||
fields = unpack('3sB8sB8s3s',record)
|
||||
if fields[0] != 'PID' or fields[5] != 'pid' :
|
||||
raise DrmException(u"Didn't find PID magic numbers in record")
|
||||
elif fields[1] != 8 or fields[3] != 8 :
|
||||
raise TpzDRMError("Record didn't contain correct length fields")
|
||||
raise DrmException(u"Record didn't contain correct length fields")
|
||||
elif fields[2] != PID :
|
||||
raise TpzDRMError("Record didn't contain PID")
|
||||
raise DrmException(u"Record didn't contain PID")
|
||||
return fields[4]
|
||||
|
||||
# Decrypt all dkey records (contain the book PID)
|
||||
@@ -131,11 +178,11 @@ def decryptDkeyRecords(data,PID):
|
||||
try:
|
||||
key = decryptDkeyRecord(data[1:length+1],PID)
|
||||
records.append(key)
|
||||
except TpzDRMError:
|
||||
except DrmException:
|
||||
pass
|
||||
data = data[1+length:]
|
||||
if len(records) == 0:
|
||||
raise TpzDRMError("BookKey Not Found")
|
||||
raise DrmException(u"BookKey Not Found")
|
||||
return records
|
||||
|
||||
|
||||
@@ -148,9 +195,9 @@ class TopazBook:
|
||||
self.bookHeaderRecords = {}
|
||||
self.bookMetadata = {}
|
||||
self.bookKey = None
|
||||
magic = unpack("4s",self.fo.read(4))[0]
|
||||
magic = unpack('4s',self.fo.read(4))[0]
|
||||
if magic != 'TPZ0':
|
||||
raise TpzDRMError("Parse Error : Invalid Header, not a Topaz file")
|
||||
raise DrmException(u"Parse Error : Invalid Header, not a Topaz file")
|
||||
self.parseTopazHeaders()
|
||||
self.parseMetadata()
|
||||
|
||||
@@ -167,7 +214,7 @@ class TopazBook:
|
||||
# Read and parse one header record at the current book file position and return the associated data
|
||||
# [[offset,decompressedLength,compressedLength],...]
|
||||
if ord(self.fo.read(1)) != 0x63:
|
||||
raise TpzDRMError("Parse Error : Invalid Header")
|
||||
raise DrmException(u"Parse Error : Invalid Header")
|
||||
tag = bookReadString(self.fo)
|
||||
record = bookReadHeaderRecordData()
|
||||
return [tag,record]
|
||||
@@ -177,15 +224,15 @@ class TopazBook:
|
||||
# print result[0], result[1]
|
||||
self.bookHeaderRecords[result[0]] = result[1]
|
||||
if ord(self.fo.read(1)) != 0x64 :
|
||||
raise TpzDRMError("Parse Error : Invalid Header")
|
||||
raise DrmException(u"Parse Error : Invalid Header")
|
||||
self.bookPayloadOffset = self.fo.tell()
|
||||
|
||||
def parseMetadata(self):
|
||||
# Parse the metadata record from the book payload and return a list of [key,values]
|
||||
self.fo.seek(self.bookPayloadOffset + self.bookHeaderRecords["metadata"][0][0])
|
||||
self.fo.seek(self.bookPayloadOffset + self.bookHeaderRecords['metadata'][0][0])
|
||||
tag = bookReadString(self.fo)
|
||||
if tag != "metadata" :
|
||||
raise TpzDRMError("Parse Error : Record Names Don't Match")
|
||||
if tag != 'metadata' :
|
||||
raise DrmException(u"Parse Error : Record Names Don't Match")
|
||||
flags = ord(self.fo.read(1))
|
||||
nbRecords = ord(self.fo.read(1))
|
||||
# print nbRecords
|
||||
@@ -210,7 +257,7 @@ class TopazBook:
|
||||
title = ''
|
||||
if 'Title' in self.bookMetadata:
|
||||
title = self.bookMetadata['Title']
|
||||
return title
|
||||
return title.decode('utf-8')
|
||||
|
||||
def setBookKey(self, key):
|
||||
self.bookKey = key
|
||||
@@ -223,13 +270,13 @@ class TopazBook:
|
||||
try:
|
||||
recordOffset = self.bookHeaderRecords[name][index][0]
|
||||
except:
|
||||
raise TpzDRMError("Parse Error : Invalid Record, record not found")
|
||||
raise DrmException("Parse Error : Invalid Record, record not found")
|
||||
|
||||
self.fo.seek(self.bookPayloadOffset + recordOffset)
|
||||
|
||||
tag = bookReadString(self.fo)
|
||||
if tag != name :
|
||||
raise TpzDRMError("Parse Error : Invalid Record, record name doesn't match")
|
||||
raise DrmException("Parse Error : Invalid Record, record name doesn't match")
|
||||
|
||||
recordIndex = bookReadEncodedNumber(self.fo)
|
||||
if recordIndex < 0 :
|
||||
@@ -237,7 +284,7 @@ class TopazBook:
|
||||
recordIndex = -recordIndex -1
|
||||
|
||||
if recordIndex != index :
|
||||
raise TpzDRMError("Parse Error : Invalid Record, index doesn't match")
|
||||
raise DrmException("Parse Error : Invalid Record, index doesn't match")
|
||||
|
||||
if (self.bookHeaderRecords[name][index][2] > 0):
|
||||
compressed = True
|
||||
@@ -250,7 +297,7 @@ class TopazBook:
|
||||
ctx = topazCryptoInit(self.bookKey)
|
||||
record = topazCryptoDecrypt(record,ctx)
|
||||
else :
|
||||
raise TpzDRMError("Error: Attempt to decrypt without bookKey")
|
||||
raise DrmException("Error: Attempt to decrypt without bookKey")
|
||||
|
||||
if compressed:
|
||||
record = zlib.decompress(record)
|
||||
@@ -262,12 +309,12 @@ class TopazBook:
|
||||
fixedimage=True
|
||||
try:
|
||||
keydata = self.getBookPayloadRecord('dkey', 0)
|
||||
except TpzDRMError, e:
|
||||
print "no dkey record found, book may not be encrypted"
|
||||
print "attempting to extrct files without a book key"
|
||||
except DrmException, e:
|
||||
print u"no dkey record found, book may not be encrypted"
|
||||
print u"attempting to extrct files without a book key"
|
||||
self.createBookDirectory()
|
||||
self.extractFiles()
|
||||
print "Successfully Extracted Topaz contents"
|
||||
print u"Successfully Extracted Topaz contents"
|
||||
if inCalibre:
|
||||
from calibre_plugins.k4mobidedrm import genbook
|
||||
else:
|
||||
@@ -275,7 +322,7 @@ class TopazBook:
|
||||
|
||||
rv = genbook.generateBook(self.outdir, raw, fixedimage)
|
||||
if rv == 0:
|
||||
print "\nBook Successfully generated"
|
||||
print u"Book Successfully generated."
|
||||
return rv
|
||||
|
||||
# try each pid to decode the file
|
||||
@@ -283,25 +330,25 @@ class TopazBook:
|
||||
for pid in pidlst:
|
||||
# use 8 digit pids here
|
||||
pid = pid[0:8]
|
||||
print "\nTrying: ", pid
|
||||
print u"Trying: {0}".format(pid)
|
||||
bookKeys = []
|
||||
data = keydata
|
||||
try:
|
||||
bookKeys+=decryptDkeyRecords(data,pid)
|
||||
except TpzDRMError, e:
|
||||
except DrmException, e:
|
||||
pass
|
||||
else:
|
||||
bookKey = bookKeys[0]
|
||||
print "Book Key Found!"
|
||||
print u"Book Key Found! ({0})".format(bookKey.encode('hex'))
|
||||
break
|
||||
|
||||
if not bookKey:
|
||||
raise TpzDRMError("Topaz Book. No key found in " + str(len(pidlst)) + " keys tried. Read the FAQs at Alf's blog. Only if none apply, report this failure for help.")
|
||||
raise DrmException(u"No key found in {0:d} keys tried. Read the FAQs at Alf's blog: http://apprenticealf.wordpress.com/".format(len(pidlst)))
|
||||
|
||||
self.setBookKey(bookKey)
|
||||
self.createBookDirectory()
|
||||
self.extractFiles()
|
||||
print "Successfully Extracted Topaz contents"
|
||||
print u"Successfully Extracted Topaz contents"
|
||||
if inCalibre:
|
||||
from calibre_plugins.k4mobidedrm import genbook
|
||||
else:
|
||||
@@ -309,7 +356,7 @@ class TopazBook:
|
||||
|
||||
rv = genbook.generateBook(self.outdir, raw, fixedimage)
|
||||
if rv == 0:
|
||||
print "\nBook Successfully generated"
|
||||
print u"Book Successfully generated"
|
||||
return rv
|
||||
|
||||
def createBookDirectory(self):
|
||||
@@ -317,16 +364,16 @@ class TopazBook:
|
||||
# create output directory structure
|
||||
if not os.path.exists(outdir):
|
||||
os.makedirs(outdir)
|
||||
destdir = os.path.join(outdir,'img')
|
||||
destdir = os.path.join(outdir,u"img")
|
||||
if not os.path.exists(destdir):
|
||||
os.makedirs(destdir)
|
||||
destdir = os.path.join(outdir,'color_img')
|
||||
destdir = os.path.join(outdir,u"color_img")
|
||||
if not os.path.exists(destdir):
|
||||
os.makedirs(destdir)
|
||||
destdir = os.path.join(outdir,'page')
|
||||
destdir = os.path.join(outdir,u"page")
|
||||
if not os.path.exists(destdir):
|
||||
os.makedirs(destdir)
|
||||
destdir = os.path.join(outdir,'glyphs')
|
||||
destdir = os.path.join(outdir,u"glyphs")
|
||||
if not os.path.exists(destdir):
|
||||
os.makedirs(destdir)
|
||||
|
||||
@@ -334,149 +381,148 @@ class TopazBook:
|
||||
outdir = self.outdir
|
||||
for headerRecord in self.bookHeaderRecords:
|
||||
name = headerRecord
|
||||
if name != "dkey" :
|
||||
ext = '.dat'
|
||||
if name == 'img' : ext = '.jpg'
|
||||
if name == 'color' : ext = '.jpg'
|
||||
print "\nProcessing Section: %s " % name
|
||||
if name != 'dkey':
|
||||
ext = u".dat"
|
||||
if name == 'img': ext = u".jpg"
|
||||
if name == 'color' : ext = u".jpg"
|
||||
print u"Processing Section: {0}\n. . .".format(name),
|
||||
for index in range (0,len(self.bookHeaderRecords[name])) :
|
||||
fnum = "%04d" % index
|
||||
fname = name + fnum + ext
|
||||
fname = u"{0}{1:04d}{2}".format(name,index,ext)
|
||||
destdir = outdir
|
||||
if name == 'img':
|
||||
destdir = os.path.join(outdir,'img')
|
||||
destdir = os.path.join(outdir,u"img")
|
||||
if name == 'color':
|
||||
destdir = os.path.join(outdir,'color_img')
|
||||
destdir = os.path.join(outdir,u"color_img")
|
||||
if name == 'page':
|
||||
destdir = os.path.join(outdir,'page')
|
||||
destdir = os.path.join(outdir,u"page")
|
||||
if name == 'glyphs':
|
||||
destdir = os.path.join(outdir,'glyphs')
|
||||
destdir = os.path.join(outdir,u"glyphs")
|
||||
outputFile = os.path.join(destdir,fname)
|
||||
print ".",
|
||||
print u".",
|
||||
record = self.getBookPayloadRecord(name,index)
|
||||
if record != '':
|
||||
file(outputFile, 'wb').write(record)
|
||||
print " "
|
||||
print u" "
|
||||
|
||||
def getHTMLZip(self, zipname):
|
||||
def getFile(self, zipname):
|
||||
htmlzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||
htmlzip.write(os.path.join(self.outdir,'book.html'),'book.html')
|
||||
htmlzip.write(os.path.join(self.outdir,'book.opf'),'book.opf')
|
||||
if os.path.isfile(os.path.join(self.outdir,'cover.jpg')):
|
||||
htmlzip.write(os.path.join(self.outdir,'cover.jpg'),'cover.jpg')
|
||||
htmlzip.write(os.path.join(self.outdir,'style.css'),'style.css')
|
||||
zipUpDir(htmlzip, self.outdir, 'img')
|
||||
htmlzip.write(os.path.join(self.outdir,u"book.html"),u"book.html")
|
||||
htmlzip.write(os.path.join(self.outdir,u"book.opf"),u"book.opf")
|
||||
if os.path.isfile(os.path.join(self.outdir,u"cover.jpg")):
|
||||
htmlzip.write(os.path.join(self.outdir,u"cover.jpg"),u"cover.jpg")
|
||||
htmlzip.write(os.path.join(self.outdir,u"style.css"),u"style.css")
|
||||
zipUpDir(htmlzip, self.outdir, u"img")
|
||||
htmlzip.close()
|
||||
|
||||
def getBookType(self):
|
||||
return u"Topaz"
|
||||
|
||||
def getBookExtension(self):
|
||||
return u".htmlz"
|
||||
|
||||
def getSVGZip(self, zipname):
|
||||
svgzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||
svgzip.write(os.path.join(self.outdir,'index_svg.xhtml'),'index_svg.xhtml')
|
||||
zipUpDir(svgzip, self.outdir, 'svg')
|
||||
zipUpDir(svgzip, self.outdir, 'img')
|
||||
svgzip.write(os.path.join(self.outdir,u"index_svg.xhtml"),u"index_svg.xhtml")
|
||||
zipUpDir(svgzip, self.outdir, u"svg")
|
||||
zipUpDir(svgzip, self.outdir, u"img")
|
||||
svgzip.close()
|
||||
|
||||
def getXMLZip(self, zipname):
|
||||
xmlzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||
targetdir = os.path.join(self.outdir,'xml')
|
||||
zipUpDir(xmlzip, targetdir, '')
|
||||
zipUpDir(xmlzip, self.outdir, 'img')
|
||||
xmlzip.close()
|
||||
|
||||
def cleanup(self):
|
||||
if os.path.isdir(self.outdir):
|
||||
shutil.rmtree(self.outdir, True)
|
||||
|
||||
def usage(progname):
|
||||
print "Removes DRM protection from Topaz ebooks and extract the contents"
|
||||
print "Usage:"
|
||||
print " %s [-k <kindle.info>] [-p <pidnums>] [-s <kindleSerialNumbers>] <infile> <outdir> " % progname
|
||||
|
||||
print u"Removes DRM protection from Topaz ebooks and extracts the contents"
|
||||
print u"Usage:"
|
||||
print u" {0} [-k <kindle.info>] [-p <comma separated PIDs>] [-s <comma separated Kindle serial numbers>] <infile> <outdir>".format(progname)
|
||||
|
||||
# Main
|
||||
def main(argv=sys.argv):
|
||||
global buildXML
|
||||
def cli_main(argv=unicode_argv()):
|
||||
progname = os.path.basename(argv[0])
|
||||
k4 = False
|
||||
pids = []
|
||||
serials = []
|
||||
kInfoFiles = []
|
||||
print u"TopazExtract v{0}.".format(__version__)
|
||||
|
||||
try:
|
||||
opts, args = getopt.getopt(sys.argv[1:], "k:p:s:")
|
||||
opts, args = getopt.getopt(sys.argv[1:], "k:p:s:x")
|
||||
except getopt.GetoptError, err:
|
||||
print str(err)
|
||||
print u"Error in options or arguments: {0}".format(err.args[0])
|
||||
usage(progname)
|
||||
return 1
|
||||
if len(args)<2:
|
||||
usage(progname)
|
||||
return 1
|
||||
|
||||
for o, a in opts:
|
||||
if o == "-k":
|
||||
if a == None :
|
||||
print "Invalid parameter for -k"
|
||||
return 1
|
||||
kInfoFiles.append(a)
|
||||
if o == "-p":
|
||||
if a == None :
|
||||
print "Invalid parameter for -p"
|
||||
return 1
|
||||
pids = a.split(',')
|
||||
if o == "-s":
|
||||
if a == None :
|
||||
print "Invalid parameter for -s"
|
||||
return 1
|
||||
serials = a.split(',')
|
||||
k4 = True
|
||||
|
||||
infile = args[0]
|
||||
outdir = args[1]
|
||||
|
||||
if not os.path.isfile(infile):
|
||||
print "Input File Does Not Exist"
|
||||
print u"Input File {0} Does Not Exist.".format(infile)
|
||||
return 1
|
||||
|
||||
if not os.path.exists(outdir):
|
||||
print u"Output Directory {0} Does Not Exist.".format(outdir)
|
||||
return 1
|
||||
|
||||
kInfoFiles = []
|
||||
serials = []
|
||||
pids = []
|
||||
|
||||
for o, a in opts:
|
||||
if o == '-k':
|
||||
if a == None :
|
||||
raise DrmException("Invalid parameter for -k")
|
||||
kInfoFiles.append(a)
|
||||
if o == '-p':
|
||||
if a == None :
|
||||
raise DrmException("Invalid parameter for -p")
|
||||
pids = a.split(',')
|
||||
if o == '-s':
|
||||
if a == None :
|
||||
raise DrmException("Invalid parameter for -s")
|
||||
serials = [serial.replace(" ","") for serial in a.split(',')]
|
||||
|
||||
bookname = os.path.splitext(os.path.basename(infile))[0]
|
||||
|
||||
tb = TopazBook(infile)
|
||||
title = tb.getBookTitle()
|
||||
print "Processing Book: ", title
|
||||
keysRecord, keysRecordRecord = tb.getPIDMetaInfo()
|
||||
pids.extend(kgenpids.getPidList(keysRecord, keysRecordRecord, k4, serials, kInfoFiles))
|
||||
print u"Processing Book: {0}".format(title)
|
||||
md1, md2 = tb.getPIDMetaInfo()
|
||||
pids.extend(kgenpids.getPidList(md1, md2, serials, kInfoFiles))
|
||||
|
||||
try:
|
||||
print "Decrypting Book"
|
||||
print u"Decrypting Book"
|
||||
tb.processBook(pids)
|
||||
|
||||
print " Creating HTML ZIP Archive"
|
||||
zipname = os.path.join(outdir, bookname + '_nodrm' + '.htmlz')
|
||||
tb.getHTMLZip(zipname)
|
||||
print u" Creating HTML ZIP Archive"
|
||||
zipname = os.path.join(outdir, bookname + u"_nodrm.htmlz")
|
||||
tb.getFile(zipname)
|
||||
|
||||
print " Creating SVG ZIP Archive"
|
||||
zipname = os.path.join(outdir, bookname + '_SVG' + '.zip')
|
||||
print u" Creating SVG ZIP Archive"
|
||||
zipname = os.path.join(outdir, bookname + u"_SVG.zip")
|
||||
tb.getSVGZip(zipname)
|
||||
|
||||
if buildXML:
|
||||
print " Creating XML ZIP Archive"
|
||||
zipname = os.path.join(outdir, bookname + '_XML' + '.zip')
|
||||
tb.getXMLZip(zipname)
|
||||
|
||||
# removing internal temporary directory of pieces
|
||||
tb.cleanup()
|
||||
|
||||
except TpzDRMError, e:
|
||||
print str(e)
|
||||
# tb.cleanup()
|
||||
except DrmException, e:
|
||||
print u"Decryption failed\n{0}".format(traceback.format_exc())
|
||||
|
||||
try:
|
||||
tb.cleanup()
|
||||
except:
|
||||
pass
|
||||
return 1
|
||||
|
||||
except Exception, e:
|
||||
print str(e)
|
||||
# tb.cleanup
|
||||
print u"Decryption failed\m{0}".format(traceback.format_exc())
|
||||
try:
|
||||
tb.cleanup()
|
||||
except:
|
||||
pass
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.stdout=Unbuffered(sys.stdout)
|
||||
sys.exit(main())
|
||||
sys.stdout=SafeUnbuffered(sys.stdout)
|
||||
sys.stderr=SafeUnbuffered(sys.stderr)
|
||||
sys.exit(cli_main())
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import sys
|
||||
import zlib
|
||||
@@ -27,14 +28,10 @@ class fixZip:
|
||||
self.ztype = 'zip'
|
||||
if zinput.lower().find('.epub') >= 0 :
|
||||
self.ztype = 'epub'
|
||||
print "opening input"
|
||||
self.inzip = zipfilerugged.ZipFile(zinput,'r')
|
||||
print "opening outout"
|
||||
self.outzip = zipfilerugged.ZipFile(zoutput,'w')
|
||||
print "opening input as raw file"
|
||||
# open the input zip for reading only as a raw file
|
||||
self.bzf = file(zinput,'rb')
|
||||
print "finished initialising"
|
||||
|
||||
def getlocalname(self, zi):
|
||||
local_header_offset = zi.header_offset
|
||||
|
||||
Reference in New Issue
Block a user