mirror of
https://github.com/noDRM/DeDRM_tools.git
synced 2026-03-20 21:08:57 +00:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba5927a20d | ||
|
|
297a9ddc66 | ||
|
|
4f34a9a196 | ||
|
|
529dd3f160 | ||
|
|
4163d5ccf4 | ||
|
|
867ac35b45 | ||
|
|
427137b0fe | ||
|
|
ac9cdb1e98 | ||
|
|
2bedd75005 | ||
|
|
8b632e309f | ||
|
|
bc968f8eca | ||
|
|
00ac669f76 | ||
|
|
694dfafd39 |
@@ -1,6 +1,6 @@
|
|||||||
From Apprentice Alf's Blog
|
From Apprentice Alf's Blog
|
||||||
|
|
||||||
Adobe Adept ePub and PDF, .epub, .pdf
|
Adobe Adept ePub, .epub
|
||||||
|
|
||||||
This directory includes modified versions of the I♥CABBAGES Adobe Adept inept scripts for epubs. These scripts have been modified to work with OpenSSL on Windows as well as Linux and Mac OS X. His original scripts can be found in the clearly labelled folder. If a Windows User has OpenSSL installed, these scripts will make use of it in place of PyCrypto.
|
This directory includes modified versions of the I♥CABBAGES Adobe Adept inept scripts for epubs. These scripts have been modified to work with OpenSSL on Windows as well as Linux and Mac OS X. His original scripts can be found in the clearly labelled folder. If a Windows User has OpenSSL installed, these scripts will make use of it in place of PyCrypto.
|
||||||
|
|
||||||
@@ -11,20 +11,8 @@ http://i-u2665-cabbages.blogspot.com/2009_02_01_archive.html
|
|||||||
|
|
||||||
There are two scripts:
|
There are two scripts:
|
||||||
|
|
||||||
The first is called ineptkey_v5.1.pyw. Simply double-click to launch it and it will create a key file that is needed later to actually remove the DRM. This script need only be run once unless you change your ADE account information.
|
The first is called ineptkey_vX.X.pyw. Simply double-click to launch it and it will create a key file that is needed later to actually remove the DRM. This script need only be run once unless you change your ADE account information.
|
||||||
|
|
||||||
The second is called in ineptepub_v5.3.pyw. Simply double-click to launch it. It will ask for your previously generated key file and the path to the book you want to remove the DRM from.
|
The second is called in ineptepub_vX.X.pyw. Simply double-click to launch it. It will ask for your previously generated key file and the path to the book you want to remove the DRM from.
|
||||||
|
|
||||||
Both of these scripts are gui python programs. Python 2.X (32 bit) is already installed in Mac OSX. We recommend ActiveState's Active Python Version 2.X (32 bit) for Windows users.
|
Both of these scripts are gui python programs. Python 2.X (32 bit) is already installed in Mac OSX. We recommend ActiveState's Active Python Version 2.X (32 bit) for Windows users.
|
||||||
|
|
||||||
The latest version of ineptpdf to use is version 8.4.42, which improves support for some PDF files.
|
|
||||||
|
|
||||||
ineptpdf version 8.4.42 can be found here:
|
|
||||||
|
|
||||||
http://pastebin.com/kuKMXXsC
|
|
||||||
|
|
||||||
It is not included in the tools archive.
|
|
||||||
|
|
||||||
If that link is down, please check out the following website for some of the latest releases of these tools:
|
|
||||||
|
|
||||||
http://ainept.freewebspace.com/
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
#! /usr/bin/python
|
#! /usr/bin/python
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# ineptepub.pyw, version 5.4
|
from __future__ import with_statement
|
||||||
|
|
||||||
|
# ineptepub.pyw, version 5.6
|
||||||
# Copyright © 2009-2010 i♥cabbages
|
# Copyright © 2009-2010 i♥cabbages
|
||||||
|
|
||||||
# Released under the terms of the GNU General Public Licence, version 3 or
|
# Released under the terms of the GNU General Public Licence, version 3 or
|
||||||
@@ -26,13 +28,12 @@
|
|||||||
# 5.2 - Fix ctypes error causing segfaults on some systems
|
# 5.2 - Fix ctypes error causing segfaults on some systems
|
||||||
# 5.3 - add support for OpenSSL on Windows, fix bug with some versions of libcrypto 0.9.8 prior to path level o
|
# 5.3 - add support for OpenSSL on Windows, fix bug with some versions of libcrypto 0.9.8 prior to path level o
|
||||||
# 5.4 - add support for encoding to 'utf-8' when building up list of files to decrypt from encryption.xml
|
# 5.4 - add support for encoding to 'utf-8' when building up list of files to decrypt from encryption.xml
|
||||||
|
# 5.5 - On Windows try PyCrypto first, OpenSSL next
|
||||||
|
# 5.6 - Modify interface to allow use with import
|
||||||
"""
|
"""
|
||||||
Decrypt Adobe ADEPT-encrypted EPUB books.
|
Decrypt Adobe ADEPT-encrypted EPUB books.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import with_statement
|
|
||||||
|
|
||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
@@ -259,7 +260,10 @@ def _load_crypto_pycrypto():
|
|||||||
|
|
||||||
def _load_crypto():
|
def _load_crypto():
|
||||||
AES = RSA = None
|
AES = RSA = None
|
||||||
for loader in (_load_crypto_libcrypto, _load_crypto_pycrypto):
|
cryptolist = (_load_crypto_libcrypto, _load_crypto_pycrypto)
|
||||||
|
if sys.platform.startswith('win'):
|
||||||
|
cryptolist = (_load_crypto_pycrypto, _load_crypto_libcrypto)
|
||||||
|
for loader in cryptolist:
|
||||||
try:
|
try:
|
||||||
AES, RSA = loader()
|
AES, RSA = loader()
|
||||||
break
|
break
|
||||||
@@ -308,45 +312,6 @@ class Decryptor(object):
|
|||||||
data = self.decompress(data)
|
data = self.decompress(data)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def cli_main(argv=sys.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,)
|
|
||||||
return 1
|
|
||||||
keypath, inpath, outpath = argv[1:]
|
|
||||||
with open(keypath, 'rb') as f:
|
|
||||||
keyder = f.read()
|
|
||||||
rsa = RSA(keyder)
|
|
||||||
with closing(ZipFile(open(inpath, 'rb'))) as inf:
|
|
||||||
namelist = set(inf.namelist())
|
|
||||||
if 'META-INF/rights.xml' not in namelist or \
|
|
||||||
'META-INF/encryption.xml' not in namelist:
|
|
||||||
raise ADEPTError('%s: not an ADEPT EPUB' % (inpath,))
|
|
||||||
for name in META_NAMES:
|
|
||||||
namelist.remove(name)
|
|
||||||
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
|
|
||||||
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
|
||||||
expr = './/%s' % (adept('encryptedKey'),)
|
|
||||||
bookkey = ''.join(rights.findtext(expr))
|
|
||||||
bookkey = rsa.decrypt(bookkey.decode('base64'))
|
|
||||||
# Padded as per RSAES-PKCS1-v1_5
|
|
||||||
if bookkey[-17] != '\x00':
|
|
||||||
raise ADEPTError('problem decrypting session key')
|
|
||||||
encryption = inf.read('META-INF/encryption.xml')
|
|
||||||
decryptor = Decryptor(bookkey[-16:], encryption)
|
|
||||||
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
|
|
||||||
with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf:
|
|
||||||
zi = ZipInfo('mimetype', compress_type=ZIP_STORED)
|
|
||||||
outf.writestr(zi, inf.read('mimetype'))
|
|
||||||
for path in namelist:
|
|
||||||
data = inf.read(path)
|
|
||||||
outf.writestr(path, decryptor.decrypt(path, data))
|
|
||||||
return 0
|
|
||||||
|
|
||||||
class DecryptionDialog(Tkinter.Frame):
|
class DecryptionDialog(Tkinter.Frame):
|
||||||
def __init__(self, root):
|
def __init__(self, root):
|
||||||
@@ -442,6 +407,52 @@ class DecryptionDialog(Tkinter.Frame):
|
|||||||
return
|
return
|
||||||
self.status['text'] = 'File successfully decrypted'
|
self.status['text'] = 'File successfully decrypted'
|
||||||
|
|
||||||
|
|
||||||
|
def decryptBook(keypath, inpath, outpath):
|
||||||
|
with open(keypath, 'rb') as f:
|
||||||
|
keyder = f.read()
|
||||||
|
rsa = RSA(keyder)
|
||||||
|
with closing(ZipFile(open(inpath, 'rb'))) as inf:
|
||||||
|
namelist = set(inf.namelist())
|
||||||
|
if 'META-INF/rights.xml' not in namelist or \
|
||||||
|
'META-INF/encryption.xml' not in namelist:
|
||||||
|
raise ADEPTError('%s: not an ADEPT EPUB' % (inpath,))
|
||||||
|
for name in META_NAMES:
|
||||||
|
namelist.remove(name)
|
||||||
|
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
|
||||||
|
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
||||||
|
expr = './/%s' % (adept('encryptedKey'),)
|
||||||
|
bookkey = ''.join(rights.findtext(expr))
|
||||||
|
bookkey = rsa.decrypt(bookkey.decode('base64'))
|
||||||
|
# Padded as per RSAES-PKCS1-v1_5
|
||||||
|
if bookkey[-17] != '\x00':
|
||||||
|
raise ADEPTError('problem decrypting session key')
|
||||||
|
encryption = inf.read('META-INF/encryption.xml')
|
||||||
|
decryptor = Decryptor(bookkey[-16:], encryption)
|
||||||
|
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
|
||||||
|
with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf:
|
||||||
|
zi = ZipInfo('mimetype', compress_type=ZIP_STORED)
|
||||||
|
outf.writestr(zi, inf.read('mimetype'))
|
||||||
|
for path in namelist:
|
||||||
|
data = inf.read(path)
|
||||||
|
outf.writestr(path, decryptor.decrypt(path, data))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cli_main(argv=sys.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,)
|
||||||
|
return 1
|
||||||
|
keypath, inpath, outpath = argv[1:]
|
||||||
|
return decryptBook(keypath, inpath, outpath)
|
||||||
|
|
||||||
|
|
||||||
def gui_main():
|
def gui_main():
|
||||||
root = Tkinter.Tk()
|
root = Tkinter.Tk()
|
||||||
if AES is None:
|
if AES is None:
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
#! /usr/bin/python
|
#! /usr/bin/python
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# ineptkey.pyw, version 5
|
from __future__ import with_statement
|
||||||
|
|
||||||
|
# ineptkey.pyw, version 5.4
|
||||||
# Copyright © 2009-2010 i♥cabbages
|
# Copyright © 2009-2010 i♥cabbages
|
||||||
|
|
||||||
# Released under the terms of the GNU General Public Licence, version 3 or
|
# Released under the terms of the GNU General Public Licence, version 3 or
|
||||||
@@ -32,13 +34,13 @@
|
|||||||
# Clean up and merge OS X support by unknown
|
# Clean up and merge OS X support by unknown
|
||||||
# 5.1 - add support for using OpenSSL on Windows in place of PyCrypto
|
# 5.1 - add support for using OpenSSL on Windows in place of PyCrypto
|
||||||
# 5.2 - added support for output of key to a particular file
|
# 5.2 - added support for output of key to a particular file
|
||||||
|
# 5.3 - On Windows try PyCrypto first, OpenSSL next
|
||||||
|
# 5.4 - Modify interface to allow use of import
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Retrieve Adobe ADEPT user key.
|
Retrieve Adobe ADEPT user key.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import with_statement
|
|
||||||
|
|
||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
@@ -115,7 +117,7 @@ if sys.platform.startswith('win'):
|
|||||||
|
|
||||||
def _load_crypto():
|
def _load_crypto():
|
||||||
AES = None
|
AES = None
|
||||||
for loader in (_load_crypto_libcrypto, _load_crypto_pycrypto):
|
for loader in (_load_crypto_pycrypto, _load_crypto_libcrypto):
|
||||||
try:
|
try:
|
||||||
AES = loader()
|
AES = loader()
|
||||||
break
|
break
|
||||||
@@ -414,10 +416,11 @@ class ExceptionDialog(Tkinter.Frame):
|
|||||||
label.pack(fill=Tkconstants.X, expand=0)
|
label.pack(fill=Tkconstants.X, expand=0)
|
||||||
self.text = Tkinter.Text(self)
|
self.text = Tkinter.Text(self)
|
||||||
self.text.pack(fill=Tkconstants.BOTH, expand=1)
|
self.text.pack(fill=Tkconstants.BOTH, expand=1)
|
||||||
|
|
||||||
self.text.insert(Tkconstants.END, text)
|
self.text.insert(Tkconstants.END, text)
|
||||||
|
|
||||||
def cli_main(argv=sys.argv):
|
|
||||||
keypath = argv[1]
|
def extractKeyfile(keypath):
|
||||||
try:
|
try:
|
||||||
success = retrieve_key(keypath)
|
success = retrieve_key(keypath)
|
||||||
except ADEPTError, e:
|
except ADEPTError, e:
|
||||||
@@ -430,6 +433,12 @@ def cli_main(argv=sys.argv):
|
|||||||
return 1
|
return 1
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cli_main(argv=sys.argv):
|
||||||
|
keypath = argv[1]
|
||||||
|
return extractKeyfile(keypath)
|
||||||
|
|
||||||
|
|
||||||
def main(argv=sys.argv):
|
def main(argv=sys.argv):
|
||||||
root = Tkinter.Tk()
|
root = Tkinter.Tk()
|
||||||
root.withdraw()
|
root.withdraw()
|
||||||
|
|||||||
18
Adobe_PDF_Tools/README_ineptpdf.txt
Normal file
18
Adobe_PDF_Tools/README_ineptpdf.txt
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
From Apprentice Alf's Blog
|
||||||
|
|
||||||
|
Adobe Adept PDF, .pdf
|
||||||
|
|
||||||
|
This directory includes modified versions of the I♥CABBAGES Adobe Adept inept scripts for pdfs. These scripts have been modified to work with OpenSSL on Windows as well as Linux and Mac OS X. If a Windows User has OpenSSL installed, these scripts will make use of it in place of PyCrypto.
|
||||||
|
|
||||||
|
The wonderful I♥CABBAGES has produced scripts that will remove the DRM from ePubs and PDFs encryped with Adobe’s DRM. These scripts require installation of the PyCrypto python package *or* the OpenSSL library on Windows. For Mac OS X and Linux boxes, these scripts use the already installed OpenSSL libcrypto so there is no additional requirements for these platforms.
|
||||||
|
|
||||||
|
For more info, see the author's blog:
|
||||||
|
http://i-u2665-cabbages.blogspot.com/2009_02_01_archive.html
|
||||||
|
|
||||||
|
There are two scripts:
|
||||||
|
|
||||||
|
The first is called ineptkey_vX.X.pyw. Simply double-click to launch it and it will create a key file that is needed later to actually remove the DRM. This script need only be run once unless you change your ADE account information.
|
||||||
|
|
||||||
|
The second is called in ineptpdf_vX.X.pyw. Simply double-click to launch it. It will ask for your previously generated key file and the path to the book you want to remove the DRM from.
|
||||||
|
|
||||||
|
Both of these scripts are gui python programs. Python 2.X (32 bit) is already installed in Mac OSX. We recommend ActiveState's Active Python Version 2.X (32 bit) for Windows users.
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
#! /usr/bin/python
|
#! /usr/bin/python
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# ineptkey.pyw, version 5
|
from __future__ import with_statement
|
||||||
|
|
||||||
|
# ineptkey.pyw, version 5.4
|
||||||
# Copyright © 2009-2010 i♥cabbages
|
# Copyright © 2009-2010 i♥cabbages
|
||||||
|
|
||||||
# Released under the terms of the GNU General Public Licence, version 3 or
|
# Released under the terms of the GNU General Public Licence, version 3 or
|
||||||
@@ -32,13 +34,13 @@
|
|||||||
# Clean up and merge OS X support by unknown
|
# Clean up and merge OS X support by unknown
|
||||||
# 5.1 - add support for using OpenSSL on Windows in place of PyCrypto
|
# 5.1 - add support for using OpenSSL on Windows in place of PyCrypto
|
||||||
# 5.2 - added support for output of key to a particular file
|
# 5.2 - added support for output of key to a particular file
|
||||||
|
# 5.3 - On Windows try PyCrypto first, OpenSSL next
|
||||||
|
# 5.4 - Modify interface to allow use of import
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Retrieve Adobe ADEPT user key.
|
Retrieve Adobe ADEPT user key.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import with_statement
|
|
||||||
|
|
||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
@@ -115,7 +117,7 @@ if sys.platform.startswith('win'):
|
|||||||
|
|
||||||
def _load_crypto():
|
def _load_crypto():
|
||||||
AES = None
|
AES = None
|
||||||
for loader in (_load_crypto_libcrypto, _load_crypto_pycrypto):
|
for loader in (_load_crypto_pycrypto, _load_crypto_libcrypto):
|
||||||
try:
|
try:
|
||||||
AES = loader()
|
AES = loader()
|
||||||
break
|
break
|
||||||
@@ -414,10 +416,11 @@ class ExceptionDialog(Tkinter.Frame):
|
|||||||
label.pack(fill=Tkconstants.X, expand=0)
|
label.pack(fill=Tkconstants.X, expand=0)
|
||||||
self.text = Tkinter.Text(self)
|
self.text = Tkinter.Text(self)
|
||||||
self.text.pack(fill=Tkconstants.BOTH, expand=1)
|
self.text.pack(fill=Tkconstants.BOTH, expand=1)
|
||||||
|
|
||||||
self.text.insert(Tkconstants.END, text)
|
self.text.insert(Tkconstants.END, text)
|
||||||
|
|
||||||
def cli_main(argv=sys.argv):
|
|
||||||
keypath = argv[1]
|
def extractKeyfile(keypath):
|
||||||
try:
|
try:
|
||||||
success = retrieve_key(keypath)
|
success = retrieve_key(keypath)
|
||||||
except ADEPTError, e:
|
except ADEPTError, e:
|
||||||
@@ -430,6 +433,12 @@ def cli_main(argv=sys.argv):
|
|||||||
return 1
|
return 1
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cli_main(argv=sys.argv):
|
||||||
|
keypath = argv[1]
|
||||||
|
return extractKeyfile(keypath)
|
||||||
|
|
||||||
|
|
||||||
def main(argv=sys.argv):
|
def main(argv=sys.argv):
|
||||||
root = Tkinter.Tk()
|
root = Tkinter.Tk()
|
||||||
root.withdraw()
|
root.withdraw()
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#! /usr/bin/env python
|
#! /usr/bin/env python
|
||||||
# ineptpdf.pyw, version 7.6
|
# ineptpdf.pyw, version 7.9
|
||||||
|
|
||||||
|
from __future__ import with_statement
|
||||||
|
|
||||||
# To run this program install Python 2.6 from http://www.python.org/download/
|
# 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
|
# and OpenSSL (already installed on Mac OS X and Linux) OR
|
||||||
@@ -29,13 +31,14 @@
|
|||||||
# implemented ARC4 interface to OpenSSL
|
# implemented ARC4 interface to OpenSSL
|
||||||
# fixed minor typos
|
# fixed minor typos
|
||||||
# 7.6 - backported AES and other fixes from version 8.4.48
|
# 7.6 - backported AES and other fixes from version 8.4.48
|
||||||
|
# 7.7 - On Windows try PyCrypto first and OpenSSL next
|
||||||
|
# 7.8 - Modify interface to allow use of import
|
||||||
|
# 7.9 - Bug fix for some session key errors when len(bookkey) > length required
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Decrypts Adobe ADEPT-encrypted PDF files.
|
Decrypts Adobe ADEPT-encrypted PDF files.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import with_statement
|
|
||||||
|
|
||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
@@ -154,6 +157,7 @@ def _load_crypto_libcrypto():
|
|||||||
return out.raw
|
return out.raw
|
||||||
|
|
||||||
class AES(object):
|
class AES(object):
|
||||||
|
MODE_CBC = 0
|
||||||
@classmethod
|
@classmethod
|
||||||
def new(cls, userkey, mode, iv):
|
def new(cls, userkey, mode, iv):
|
||||||
self = AES()
|
self = AES()
|
||||||
@@ -319,7 +323,10 @@ def _load_crypto_pycrypto():
|
|||||||
|
|
||||||
def _load_crypto():
|
def _load_crypto():
|
||||||
ARC4 = RSA = AES = None
|
ARC4 = RSA = AES = None
|
||||||
for loader in (_load_crypto_libcrypto, _load_crypto_pycrypto):
|
cryptolist = (_load_crypto_libcrypto, _load_crypto_pycrypto)
|
||||||
|
if sys.platform.startswith('win'):
|
||||||
|
cryptolist = (_load_crypto_pycrypto, _load_crypto_libcrypto)
|
||||||
|
for loader in cryptolist:
|
||||||
try:
|
try:
|
||||||
ARC4, RSA, AES = loader()
|
ARC4, RSA, AES = loader()
|
||||||
break
|
break
|
||||||
@@ -1526,16 +1533,30 @@ class PDFDocument(object):
|
|||||||
bookkey = bookkey[index:]
|
bookkey = bookkey[index:]
|
||||||
ebx_V = int_value(param.get('V', 4))
|
ebx_V = int_value(param.get('V', 4))
|
||||||
ebx_type = int_value(param.get('EBX_ENCRYPTIONTYPE', 6))
|
ebx_type = int_value(param.get('EBX_ENCRYPTIONTYPE', 6))
|
||||||
# added because of the booktype / decryption book session key error
|
# added because of improper booktype / decryption book session key errors
|
||||||
|
if length > 0:
|
||||||
|
if len(bookkey) == length:
|
||||||
if ebx_V == 3:
|
if ebx_V == 3:
|
||||||
V = 3
|
V = 3
|
||||||
elif ebx_V < 4 or ebx_type < 6:
|
else:
|
||||||
|
V = 2
|
||||||
|
elif len(bookkey) == length + 1:
|
||||||
V = ord(bookkey[0])
|
V = ord(bookkey[0])
|
||||||
bookkey = bookkey[1:]
|
bookkey = bookkey[1:]
|
||||||
|
else:
|
||||||
|
print "ebx_V is %d and ebx_type is %d" % (ebx_V, ebx_type)
|
||||||
|
print "length is %d and len(bookkey) is %d" % (length, len(bookkey))
|
||||||
|
print "bookkey[0] is %d" % ord(bookkey[0])
|
||||||
|
raise ADEPTError('error decrypting book session key - mismatched length')
|
||||||
|
else:
|
||||||
|
# proper length unknown try with whatever you have
|
||||||
|
print "ebx_V is %d and ebx_type is %d" % (ebx_V, ebx_type)
|
||||||
|
print "length is %d and len(bookkey) is %d" % (length, len(bookkey))
|
||||||
|
print "bookkey[0] is %d" % ord(bookkey[0])
|
||||||
|
if ebx_V == 3:
|
||||||
|
V = 3
|
||||||
else:
|
else:
|
||||||
V = 2
|
V = 2
|
||||||
if length and len(bookkey) != length:
|
|
||||||
raise ADEPTError('error decrypting book session key')
|
|
||||||
self.decrypt_key = bookkey
|
self.decrypt_key = bookkey
|
||||||
self.genkey = self.genkey_v3 if V == 3 else self.genkey_v2
|
self.genkey = self.genkey_v3 if V == 3 else self.genkey_v2
|
||||||
self.decipher = self.decrypt_rc4
|
self.decipher = self.decrypt_rc4
|
||||||
@@ -2072,25 +2093,6 @@ class PDFSerializer(object):
|
|||||||
self.write('\n')
|
self.write('\n')
|
||||||
self.write('endobj\n')
|
self.write('endobj\n')
|
||||||
|
|
||||||
def cli_main(argv=sys.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,)
|
|
||||||
return 1
|
|
||||||
keypath, inpath, outpath = argv[1:]
|
|
||||||
with open(inpath, 'rb') as inf:
|
|
||||||
serializer = PDFSerializer(inf, keypath)
|
|
||||||
# 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
|
|
||||||
serializer.dump(outf)
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
class DecryptionDialog(Tkinter.Frame):
|
class DecryptionDialog(Tkinter.Frame):
|
||||||
def __init__(self, root):
|
def __init__(self, root):
|
||||||
@@ -2194,6 +2196,31 @@ class DecryptionDialog(Tkinter.Frame):
|
|||||||
'Close this window or decrypt another pdf file.'
|
'Close this window or decrypt another pdf file.'
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def decryptBook(keypath, inpath, outpath):
|
||||||
|
with open(inpath, 'rb') as inf:
|
||||||
|
serializer = PDFSerializer(inf, keypath)
|
||||||
|
# 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
|
||||||
|
serializer.dump(outf)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cli_main(argv=sys.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,)
|
||||||
|
return 1
|
||||||
|
keypath, inpath, outpath = argv[1:]
|
||||||
|
return decryptBook(keypath, inpath, outpath)
|
||||||
|
|
||||||
|
|
||||||
def gui_main():
|
def gui_main():
|
||||||
root = Tkinter.Tk()
|
root = Tkinter.Tk()
|
||||||
if RSA is None:
|
if RSA is None:
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ Barnes and Noble EPUB ebooks use a form of Social DRM which requires information
|
|||||||
For more info, see the author's blog:
|
For more info, see the author's blog:
|
||||||
http://i-u2665-cabbages.blogspot.com/2009_12_01_archive.html
|
http://i-u2665-cabbages.blogspot.com/2009_12_01_archive.html
|
||||||
|
|
||||||
The original scripts by IHeartCabbages are available here as well. These scripts have been modified to allow the use of OpenSSL in place of PyCrypto to make them easier to run on Linux and Mac OS X.
|
The original scripts by IHeartCabbages are available here as well. These scripts have been modified to allow the use of OpenSSL in place of PyCrypto to make them easier to run on Linux and Mac OS X, as well as to fix some minor bugs/
|
||||||
|
|
||||||
There are 2 scripts:
|
There are 2 scripts:
|
||||||
|
|
||||||
The first is ignoblekeygen_v2.pyw. Double-click to launch it and provide the required information, and this program will generate a key file needed to remove the DRM from the books. This key file need only be generated once unless either you change your credit card number or your name on the credit card (or if you use a different credit card to purchase your book).
|
The first is ignoblekeygen_vX.X.pyw. Double-click to launch it and provide the required information, and this program will generate a key file needed to remove the DRM from the books. This key file need only be generated once unless either you change your credit card number or your name on the credit card (or if you use a different credit card to purchase your book).
|
||||||
|
|
||||||
The second is ignobleepub_v3.pyw. Double-click it and it will ask for your key file and the path to the book to remove the DRM from.
|
The second is ignobleepub_vX.X.pyw. Double-click it and it will ask for your key file and the path to the book to remove the DRM from.
|
||||||
|
|
||||||
All of these scripts are gui python programs. Python 2.X (32 bit) is already installed in Mac OSX. We recommend ActiveState's Active Python Version 2.X (32 bit) for Windows users.
|
All of these scripts are gui python programs. Python 2.X (32 bit) is already installed in Mac OSX. We recommend ActiveState's Active Python Version 2.X (32 bit) for Windows users.
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
#! /usr/bin/python
|
#! /usr/bin/python
|
||||||
|
|
||||||
# ignobleepub.pyw, version 3
|
from __future__ import with_statement
|
||||||
|
|
||||||
|
# ignobleepub.pyw, version 3.4
|
||||||
|
|
||||||
# To run this program install Python 2.6 from <http://www.python.org/download/>
|
# 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
|
# and OpenSSL or PyCrypto from http://www.voidspace.org.uk/python/modules.shtml#pycrypto
|
||||||
@@ -12,8 +14,10 @@
|
|||||||
# 2 - Added OS X support by using OpenSSL when available
|
# 2 - Added OS X support by using OpenSSL when available
|
||||||
# 3 - screen out improper key lengths to prevent segfaults on Linux
|
# 3 - screen out improper key lengths to prevent segfaults on Linux
|
||||||
# 3.1 - Allow Windows versions of libcrypto to be found
|
# 3.1 - Allow Windows versions of libcrypto to be found
|
||||||
|
# 3.2 - add support for encoding to 'utf-8' when building up list of files to cecrypt from encryption.xml
|
||||||
|
# 3.3 - On Windows try PyCrypto first and OpenSSL next
|
||||||
|
# 3.4 - Modify interace to allow use with import
|
||||||
|
|
||||||
from __future__ import with_statement
|
|
||||||
|
|
||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
|
|
||||||
@@ -105,15 +109,18 @@ def _load_crypto_pycrypto():
|
|||||||
|
|
||||||
def _load_crypto():
|
def _load_crypto():
|
||||||
AES = None
|
AES = None
|
||||||
for loader in (_load_crypto_libcrypto, _load_crypto_pycrypto):
|
cryptolist = (_load_crypto_libcrypto, _load_crypto_pycrypto)
|
||||||
|
if sys.platform.startswith('win'):
|
||||||
|
cryptolist = (_load_crypto_pycrypto, _load_crypto_libcrypto)
|
||||||
|
for loader in cryptolist:
|
||||||
try:
|
try:
|
||||||
AES = loader()
|
AES = loader()
|
||||||
break
|
break
|
||||||
except (ImportError, IGNOBLEError):
|
except (ImportError, IGNOBLEError):
|
||||||
pass
|
pass
|
||||||
return AES
|
return AES
|
||||||
AES = _load_crypto()
|
|
||||||
|
|
||||||
|
AES = _load_crypto()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -164,49 +171,6 @@ class Decryptor(object):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def cli_main(argv=sys.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,)
|
|
||||||
return 1
|
|
||||||
keypath, inpath, outpath = argv[1:]
|
|
||||||
with open(keypath, 'rb') as f:
|
|
||||||
keyb64 = f.read()
|
|
||||||
key = keyb64.decode('base64')[:16]
|
|
||||||
# aes = AES.new(key, AES.MODE_CBC)
|
|
||||||
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:
|
|
||||||
raise IGNOBLEError('%s: not an B&N ADEPT EPUB' % (inpath,))
|
|
||||||
for name in META_NAMES:
|
|
||||||
namelist.remove(name)
|
|
||||||
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
|
|
||||||
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
|
||||||
expr = './/%s' % (adept('encryptedKey'),)
|
|
||||||
bookkey = ''.join(rights.findtext(expr))
|
|
||||||
bookkey = aes.decrypt(bookkey.decode('base64'))
|
|
||||||
bookkey = bookkey[:-ord(bookkey[-1])]
|
|
||||||
encryption = inf.read('META-INF/encryption.xml')
|
|
||||||
decryptor = Decryptor(bookkey[-16:], encryption)
|
|
||||||
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
|
|
||||||
with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf:
|
|
||||||
zi = ZipInfo('mimetype', compress_type=ZIP_STORED)
|
|
||||||
outf.writestr(zi, inf.read('mimetype'))
|
|
||||||
for path in namelist:
|
|
||||||
data = inf.read(path)
|
|
||||||
outf.writestr(path, decryptor.decrypt(path, data))
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
class DecryptionDialog(Tkinter.Frame):
|
class DecryptionDialog(Tkinter.Frame):
|
||||||
def __init__(self, root):
|
def __init__(self, root):
|
||||||
Tkinter.Frame.__init__(self, root, border=5)
|
Tkinter.Frame.__init__(self, root, border=5)
|
||||||
@@ -302,6 +266,53 @@ class DecryptionDialog(Tkinter.Frame):
|
|||||||
return
|
return
|
||||||
self.status['text'] = 'File successfully decrypted'
|
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)
|
||||||
|
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:
|
||||||
|
raise IGNOBLEError('%s: not an B&N ADEPT EPUB' % (inpath,))
|
||||||
|
for name in META_NAMES:
|
||||||
|
namelist.remove(name)
|
||||||
|
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
|
||||||
|
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
||||||
|
expr = './/%s' % (adept('encryptedKey'),)
|
||||||
|
bookkey = ''.join(rights.findtext(expr))
|
||||||
|
bookkey = aes.decrypt(bookkey.decode('base64'))
|
||||||
|
bookkey = bookkey[:-ord(bookkey[-1])]
|
||||||
|
encryption = inf.read('META-INF/encryption.xml')
|
||||||
|
decryptor = Decryptor(bookkey[-16:], encryption)
|
||||||
|
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
|
||||||
|
with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf:
|
||||||
|
zi = ZipInfo('mimetype', compress_type=ZIP_STORED)
|
||||||
|
outf.writestr(zi, inf.read('mimetype'))
|
||||||
|
for path in namelist:
|
||||||
|
data = inf.read(path)
|
||||||
|
outf.writestr(path, decryptor.decrypt(path, data))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cli_main(argv=sys.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,)
|
||||||
|
return 1
|
||||||
|
keypath, inpath, outpath = argv[1:]
|
||||||
|
return decryptBook(keypath, inpath, outpath)
|
||||||
|
|
||||||
|
|
||||||
def gui_main():
|
def gui_main():
|
||||||
root = Tkinter.Tk()
|
root = Tkinter.Tk()
|
||||||
if AES is None:
|
if AES is None:
|
||||||
@@ -318,6 +329,7 @@ def gui_main():
|
|||||||
root.mainloop()
|
root.mainloop()
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
if len(sys.argv) > 1:
|
if len(sys.argv) > 1:
|
||||||
sys.exit(cli_main())
|
sys.exit(cli_main())
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
#! /usr/bin/python
|
#! /usr/bin/python
|
||||||
|
|
||||||
# ignoblekeygen.pyw, version 2
|
from __future__ import with_statement
|
||||||
|
|
||||||
|
# ignoblekeygen.pyw, version 2.3
|
||||||
|
|
||||||
# To run this program install Python 2.6 from <http://www.python.org/download/>
|
# 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
|
# and OpenSSL or PyCrypto from http://www.voidspace.org.uk/python/modules.shtml#pycrypto
|
||||||
@@ -11,13 +13,13 @@
|
|||||||
# 1 - Initial release
|
# 1 - Initial release
|
||||||
# 2 - Add OS X support by using OpenSSL when available (taken/modified from ineptepub v5)
|
# 2 - Add OS X support by using OpenSSL when available (taken/modified from ineptepub v5)
|
||||||
# 2.1 - Allow Windows versions of libcrypto to be found
|
# 2.1 - Allow Windows versions of libcrypto to be found
|
||||||
|
# 2.2 - On Windows try PyCrypto first and then OpenSSL next
|
||||||
|
# 2.3 - Modify interface to allow use of import
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Generate Barnes & Noble EPUB user key from name and credit card number.
|
Generate Barnes & Noble EPUB user key from name and credit card number.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import with_statement
|
|
||||||
|
|
||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
@@ -102,11 +104,12 @@ def _load_crypto_pycrypto():
|
|||||||
|
|
||||||
return AES
|
return AES
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _load_crypto():
|
def _load_crypto():
|
||||||
AES = None
|
AES = None
|
||||||
for loader in (_load_crypto_libcrypto, _load_crypto_pycrypto):
|
cryptolist = (_load_crypto_libcrypto, _load_crypto_pycrypto)
|
||||||
|
if sys.platform.startswith('win'):
|
||||||
|
cryptolist = (_load_crypto_pycrypto, _load_crypto_libcrypto)
|
||||||
|
for loader in cryptolist:
|
||||||
try:
|
try:
|
||||||
AES = loader()
|
AES = loader()
|
||||||
break
|
break
|
||||||
@@ -119,6 +122,7 @@ AES = _load_crypto()
|
|||||||
def normalize_name(name):
|
def normalize_name(name):
|
||||||
return ''.join(x for x in name.lower() if x != ' ')
|
return ''.join(x for x in name.lower() if x != ' ')
|
||||||
|
|
||||||
|
|
||||||
def generate_keyfile(name, ccn, outpath):
|
def generate_keyfile(name, ccn, outpath):
|
||||||
name = normalize_name(name) + '\x00'
|
name = normalize_name(name) + '\x00'
|
||||||
ccn = ccn + '\x00'
|
ccn = ccn + '\x00'
|
||||||
@@ -132,19 +136,6 @@ def generate_keyfile(name, ccn, outpath):
|
|||||||
f.write(userkey.encode('base64'))
|
f.write(userkey.encode('base64'))
|
||||||
return userkey
|
return userkey
|
||||||
|
|
||||||
def cli_main(argv=sys.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 NAME CC# OUTFILE" % (progname,)
|
|
||||||
return 1
|
|
||||||
name, ccn, outpath = argv[1:]
|
|
||||||
generate_keyfile(name, ccn, outpath)
|
|
||||||
return 0
|
|
||||||
|
|
||||||
class DecryptionDialog(Tkinter.Frame):
|
class DecryptionDialog(Tkinter.Frame):
|
||||||
def __init__(self, root):
|
def __init__(self, root):
|
||||||
@@ -210,6 +201,22 @@ class DecryptionDialog(Tkinter.Frame):
|
|||||||
return
|
return
|
||||||
self.status['text'] = 'Keyfile successfully generated'
|
self.status['text'] = 'Keyfile successfully generated'
|
||||||
|
|
||||||
|
|
||||||
|
def cli_main(argv=sys.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 NAME CC# OUTFILE" % (progname,)
|
||||||
|
return 1
|
||||||
|
name, ccn, outpath = argv[1:]
|
||||||
|
generate_keyfile(name, ccn, outpath)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def gui_main():
|
def gui_main():
|
||||||
root = Tkinter.Tk()
|
root = Tkinter.Tk()
|
||||||
if AES is None:
|
if AES is None:
|
||||||
|
|||||||
116
Calibre_Plugins/K4MobiDeDRM_plugin/__init__.py
Normal file
116
Calibre_Plugins/K4MobiDeDRM_plugin/__init__.py
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
from __future__ import with_statement
|
||||||
|
|
||||||
|
from calibre.customize import FileTypePlugin
|
||||||
|
from calibre.gui2 import is_ok_to_use_qt
|
||||||
|
# from calibre.ptempfile import PersistentTemporaryDirectory
|
||||||
|
|
||||||
|
from calibre_plugins.k4mobidedrm import kgenpids
|
||||||
|
from calibre_plugins.k4mobidedrm import topazextract
|
||||||
|
from calibre_plugins.k4mobidedrm import mobidedrm
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
class K4DeDRM(FileTypePlugin):
|
||||||
|
name = 'K4PC, K4Mac, Kindle Mobi and Topaz DeDRM' # Name of the plugin
|
||||||
|
description = 'Removes DRM from K4PC and Mac, Kindle Mobi and Topaz files. Provided by the work of many including DiapDealer, SomeUpdates, IHeartCabbages, CMBDTC, Skindle, DarkReverser, ApprenticeAlf, etc.'
|
||||||
|
supported_platforms = ['osx', 'windows', 'linux'] # Platforms this plugin will run on
|
||||||
|
author = 'DiapDealer, SomeUpdates' # The author of this plugin
|
||||||
|
version = (0, 3, 6) # The version number of this plugin
|
||||||
|
file_types = set(['prc','mobi','azw','azw1','tpz']) # The file types that this plugin will be applied to
|
||||||
|
on_import = True # Run this plugin during the import
|
||||||
|
priority = 210 # run this plugin before mobidedrm, k4pcdedrm, k4dedrm
|
||||||
|
minimum_calibre_version = (0, 7, 55)
|
||||||
|
|
||||||
|
def run(self, path_to_ebook):
|
||||||
|
|
||||||
|
k4 = True
|
||||||
|
if sys.platform.startswith('linux'):
|
||||||
|
k4 = False
|
||||||
|
pids = []
|
||||||
|
serials = []
|
||||||
|
kInfoFiles = []
|
||||||
|
|
||||||
|
# Get supplied list of PIDs to try from plugin customization.
|
||||||
|
customvalues = self.site_customization.split(',')
|
||||||
|
for customvalue in customvalues:
|
||||||
|
customvalue = str(customvalue)
|
||||||
|
customvalue = customvalue.strip()
|
||||||
|
if len(customvalue) == 10 or len(customvalue) == 8:
|
||||||
|
pids.append(customvalue)
|
||||||
|
else :
|
||||||
|
if len(customvalue) == 16 and customvalue[0] == 'B':
|
||||||
|
serials.append(customvalue)
|
||||||
|
else:
|
||||||
|
print "%s is not a valid Kindle serial number or PID." % str(customvalue)
|
||||||
|
|
||||||
|
# Load any kindle info files (*.info) included Calibre's config directory.
|
||||||
|
try:
|
||||||
|
# Find Calibre's configuration directory.
|
||||||
|
confpath = os.path.split(os.path.split(self.plugin_path)[0])[0]
|
||||||
|
print 'K4MobiDeDRM: Calibre configuration directory = %s' % confpath
|
||||||
|
files = os.listdir(confpath)
|
||||||
|
filefilter = re.compile("\.info$|\.kinf$", re.IGNORECASE)
|
||||||
|
files = filter(filefilter.search, files)
|
||||||
|
if files:
|
||||||
|
for filename in files:
|
||||||
|
fpath = os.path.join(confpath, filename)
|
||||||
|
kInfoFiles.append(fpath)
|
||||||
|
print 'K4MobiDeDRM: Kindle info/kinf file %s found in config folder.' % filename
|
||||||
|
except IOError:
|
||||||
|
print 'K4MobiDeDRM: Error reading kindle info/kinf files from config directory.'
|
||||||
|
pass
|
||||||
|
|
||||||
|
mobi = True
|
||||||
|
magic3 = file(path_to_ebook,'rb').read(3)
|
||||||
|
if magic3 == 'TPZ':
|
||||||
|
mobi = False
|
||||||
|
|
||||||
|
bookname = os.path.splitext(os.path.basename(path_to_ebook))[0]
|
||||||
|
|
||||||
|
if mobi:
|
||||||
|
mb = mobidedrm.MobiBook(path_to_ebook)
|
||||||
|
else:
|
||||||
|
mb = topazextract.TopazBook(path_to_ebook)
|
||||||
|
|
||||||
|
title = mb.getBookTitle()
|
||||||
|
md1, md2 = mb.getPIDMetaInfo()
|
||||||
|
pidlst = kgenpids.getPidList(md1, md2, k4, pids, serials, kInfoFiles)
|
||||||
|
|
||||||
|
try:
|
||||||
|
mb.processBook(pidlst)
|
||||||
|
|
||||||
|
except mobidedrm.DrmException:
|
||||||
|
#if you reached here then no luck raise and exception
|
||||||
|
if is_ok_to_use_qt():
|
||||||
|
from PyQt4.Qt import QMessageBox
|
||||||
|
d = QMessageBox(QMessageBox.Warning, "K4MobiDeDRM Plugin", "Error decoding: %s\n" % path_to_ebook)
|
||||||
|
d.show()
|
||||||
|
d.raise_()
|
||||||
|
d.exec_()
|
||||||
|
raise Exception("K4MobiDeDRM plugin could not decode the file")
|
||||||
|
except topazextract.TpzDRMError:
|
||||||
|
#if you reached here then no luck raise and exception
|
||||||
|
if is_ok_to_use_qt():
|
||||||
|
from PyQt4.Qt import QMessageBox
|
||||||
|
d = QMessageBox(QMessageBox.Warning, "K4MobiDeDRM Plugin", "Error decoding: %s\n" % path_to_ebook)
|
||||||
|
d.show()
|
||||||
|
d.raise_()
|
||||||
|
d.exec_()
|
||||||
|
raise Exception("K4MobiDeDRM plugin could not decode the file")
|
||||||
|
|
||||||
|
print "Success!"
|
||||||
|
if mobi:
|
||||||
|
of = self.temporary_file(bookname+'.mobi')
|
||||||
|
mb.getMobiFile(of.name)
|
||||||
|
else :
|
||||||
|
of = self.temporary_file(bookname+'.htmlz')
|
||||||
|
mb.getHTMLZip(of.name)
|
||||||
|
mb.cleanup()
|
||||||
|
return of.name
|
||||||
|
|
||||||
|
def customization_help(self, gui=False):
|
||||||
|
return 'Enter 10 character PIDs and/or Kindle serial numbers, use a comma (no spaces) to separate each PID or SerialNumber from the next.'
|
||||||
@@ -235,6 +235,7 @@ class PageParser(object):
|
|||||||
|
|
||||||
'group' : (1, 'snippets', 1, 0),
|
'group' : (1, 'snippets', 1, 0),
|
||||||
'group.type' : (1, 'scalar_text', 0, 0),
|
'group.type' : (1, 'scalar_text', 0, 0),
|
||||||
|
'group._tag' : (1, 'scalar_text', 0, 0),
|
||||||
|
|
||||||
'region' : (1, 'snippets', 1, 0),
|
'region' : (1, 'snippets', 1, 0),
|
||||||
'region.type' : (1, 'scalar_text', 0, 0),
|
'region.type' : (1, 'scalar_text', 0, 0),
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ class DocParser(object):
|
|||||||
ys = []
|
ys = []
|
||||||
gdefs = []
|
gdefs = []
|
||||||
|
|
||||||
# get path defintions, positions, dimensions for ecah glyph
|
# get path defintions, positions, dimensions for each glyph
|
||||||
# that makes up the image, and find min x and min y to reposition origin
|
# that makes up the image, and find min x and min y to reposition origin
|
||||||
minx = -1
|
minx = -1
|
||||||
miny = -1
|
miny = -1
|
||||||
@@ -305,6 +305,15 @@ class DocParser(object):
|
|||||||
lastGlyph = firstglyphList[last]
|
lastGlyph = firstglyphList[last]
|
||||||
else :
|
else :
|
||||||
lastGlyph = len(gidList)
|
lastGlyph = len(gidList)
|
||||||
|
|
||||||
|
# handle case of white sapce paragraphs with no actual glyphs in them
|
||||||
|
# by reverting to text based paragraph
|
||||||
|
if firstGlyph >= lastGlyph:
|
||||||
|
# revert to standard text based paragraph
|
||||||
|
for wordnum in xrange(first, last):
|
||||||
|
result.append(('ocr', wordnum))
|
||||||
|
return pclass, result
|
||||||
|
|
||||||
for glyphnum in xrange(firstGlyph, lastGlyph):
|
for glyphnum in xrange(firstGlyph, lastGlyph):
|
||||||
glyphList.append(glyphnum)
|
glyphList.append(glyphnum)
|
||||||
# include any extratokens if they exist
|
# include any extratokens if they exist
|
||||||
|
|||||||
@@ -21,6 +21,17 @@ from struct import unpack
|
|||||||
|
|
||||||
|
|
||||||
# local support routines
|
# local support routines
|
||||||
|
if 'calibre' in sys.modules:
|
||||||
|
inCalibre = True
|
||||||
|
else:
|
||||||
|
inCalibre = False
|
||||||
|
|
||||||
|
if inCalibre :
|
||||||
|
from calibre_plugins.k4mobidedrm import convert2xml
|
||||||
|
from calibre_plugins.k4mobidedrm import flatxml2html
|
||||||
|
from calibre_plugins.k4mobidedrm import flatxml2svg
|
||||||
|
from calibre_plugins.k4mobidedrm import stylexml2css
|
||||||
|
else :
|
||||||
import convert2xml
|
import convert2xml
|
||||||
import flatxml2html
|
import flatxml2html
|
||||||
import flatxml2svg
|
import flatxml2svg
|
||||||
@@ -192,6 +203,8 @@ class GParser(object):
|
|||||||
argres[j] = int(argres[j])
|
argres[j] = int(argres[j])
|
||||||
return result
|
return result
|
||||||
def getGlyphDim(self, gly):
|
def getGlyphDim(self, gly):
|
||||||
|
if self.gdpi[gly] == 0:
|
||||||
|
return 0, 0
|
||||||
maxh = (self.gh[gly] * self.dpi) / self.gdpi[gly]
|
maxh = (self.gh[gly] * self.dpi) / self.gdpi[gly]
|
||||||
maxw = (self.gw[gly] * self.dpi) / self.gdpi[gly]
|
maxw = (self.gw[gly] * self.dpi) / self.gdpi[gly]
|
||||||
return maxh, maxw
|
return maxh, maxw
|
||||||
@@ -320,6 +333,18 @@ def generateBook(bookDir, raw, fixedimage):
|
|||||||
print 'Processing Meta Data and creating OPF'
|
print 'Processing Meta Data and creating OPF'
|
||||||
meta_array = getMetaArray(metaFile)
|
meta_array = getMetaArray(metaFile)
|
||||||
|
|
||||||
|
# replace special chars in title and authors like & < >
|
||||||
|
title = meta_array.get('Title','No Title Provided')
|
||||||
|
title = title.replace('&','&')
|
||||||
|
title = title.replace('<','<')
|
||||||
|
title = title.replace('>','>')
|
||||||
|
meta_array['Title'] = title
|
||||||
|
authors = meta_array.get('Authors','No Authors Provided')
|
||||||
|
authors = authors.replace('&','&')
|
||||||
|
authors = authors.replace('<','<')
|
||||||
|
authors = authors.replace('>','>')
|
||||||
|
meta_array['Authors'] = authors
|
||||||
|
|
||||||
xname = os.path.join(xmlDir, 'metadata.xml')
|
xname = os.path.join(xmlDir, 'metadata.xml')
|
||||||
metastr = ''
|
metastr = ''
|
||||||
for key in meta_array:
|
for key in meta_array:
|
||||||
@@ -399,7 +424,9 @@ def generateBook(bookDir, raw, fixedimage):
|
|||||||
htmlstr += '<title>' + meta_array['Title'] + ' by ' + meta_array['Authors'] + '</title>\n'
|
htmlstr += '<title>' + meta_array['Title'] + ' by ' + meta_array['Authors'] + '</title>\n'
|
||||||
htmlstr += '<meta name="Author" content="' + meta_array['Authors'] + '" />\n'
|
htmlstr += '<meta name="Author" content="' + meta_array['Authors'] + '" />\n'
|
||||||
htmlstr += '<meta name="Title" content="' + meta_array['Title'] + '" />\n'
|
htmlstr += '<meta name="Title" content="' + meta_array['Title'] + '" />\n'
|
||||||
|
if 'ASIN' in meta_array:
|
||||||
htmlstr += '<meta name="ASIN" content="' + meta_array['ASIN'] + '" />\n'
|
htmlstr += '<meta name="ASIN" content="' + meta_array['ASIN'] + '" />\n'
|
||||||
|
if 'GUID' in meta_array:
|
||||||
htmlstr += '<meta name="GUID" content="' + meta_array['GUID'] + '" />\n'
|
htmlstr += '<meta name="GUID" content="' + meta_array['GUID'] + '" />\n'
|
||||||
htmlstr += '<link href="style.css" rel="stylesheet" type="text/css" />\n'
|
htmlstr += '<link href="style.css" rel="stylesheet" type="text/css" />\n'
|
||||||
htmlstr += '</head>\n<body>\n'
|
htmlstr += '</head>\n<body>\n'
|
||||||
@@ -416,7 +443,9 @@ def generateBook(bookDir, raw, fixedimage):
|
|||||||
svgindex += '<title>' + meta_array['Title'] + '</title>\n'
|
svgindex += '<title>' + meta_array['Title'] + '</title>\n'
|
||||||
svgindex += '<meta name="Author" content="' + meta_array['Authors'] + '" />\n'
|
svgindex += '<meta name="Author" content="' + meta_array['Authors'] + '" />\n'
|
||||||
svgindex += '<meta name="Title" content="' + meta_array['Title'] + '" />\n'
|
svgindex += '<meta name="Title" content="' + meta_array['Title'] + '" />\n'
|
||||||
|
if 'ASIN' in meta_array:
|
||||||
svgindex += '<meta name="ASIN" content="' + meta_array['ASIN'] + '" />\n'
|
svgindex += '<meta name="ASIN" content="' + meta_array['ASIN'] + '" />\n'
|
||||||
|
if 'GUID' in meta_array:
|
||||||
svgindex += '<meta name="GUID" content="' + meta_array['GUID'] + '" />\n'
|
svgindex += '<meta name="GUID" content="' + meta_array['GUID'] + '" />\n'
|
||||||
svgindex += '</head>\n'
|
svgindex += '</head>\n'
|
||||||
svgindex += '<body>\n'
|
svgindex += '<body>\n'
|
||||||
@@ -471,8 +500,11 @@ def generateBook(bookDir, raw, fixedimage):
|
|||||||
opfstr += '<package xmlns="http://www.idpf.org/2007/opf" unique-identifier="guid_id">\n'
|
opfstr += '<package xmlns="http://www.idpf.org/2007/opf" unique-identifier="guid_id">\n'
|
||||||
# adding metadata
|
# adding metadata
|
||||||
opfstr += ' <metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf">\n'
|
opfstr += ' <metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf">\n'
|
||||||
|
if 'GUID' in meta_array:
|
||||||
opfstr += ' <dc:identifier opf:scheme="GUID" id="guid_id">' + meta_array['GUID'] + '</dc:identifier>\n'
|
opfstr += ' <dc:identifier opf:scheme="GUID" id="guid_id">' + meta_array['GUID'] + '</dc:identifier>\n'
|
||||||
|
if 'ASIN' in meta_array:
|
||||||
opfstr += ' <dc:identifier opf:scheme="ASIN">' + meta_array['ASIN'] + '</dc:identifier>\n'
|
opfstr += ' <dc:identifier opf:scheme="ASIN">' + meta_array['ASIN'] + '</dc:identifier>\n'
|
||||||
|
if 'oASIN' in meta_array:
|
||||||
opfstr += ' <dc:identifier opf:scheme="oASIN">' + meta_array['oASIN'] + '</dc:identifier>\n'
|
opfstr += ' <dc:identifier opf:scheme="oASIN">' + meta_array['oASIN'] + '</dc:identifier>\n'
|
||||||
opfstr += ' <dc:title>' + meta_array['Title'] + '</dc:title>\n'
|
opfstr += ' <dc:title>' + meta_array['Title'] + '</dc:title>\n'
|
||||||
opfstr += ' <dc:creator opf:role="aut">' + meta_array['Authors'] + '</dc:creator>\n'
|
opfstr += ' <dc:creator opf:role="aut">' + meta_array['Authors'] + '</dc:creator>\n'
|
||||||
@@ -483,7 +515,7 @@ def generateBook(bookDir, raw, fixedimage):
|
|||||||
opfstr += ' </metadata>\n'
|
opfstr += ' </metadata>\n'
|
||||||
opfstr += '<manifest>\n'
|
opfstr += '<manifest>\n'
|
||||||
opfstr += ' <item id="book" href="book.html" media-type="application/xhtml+xml"/>\n'
|
opfstr += ' <item id="book" href="book.html" media-type="application/xhtml+xml"/>\n'
|
||||||
opfstr += ' <item id="stylesheet" href="style.css" media-type="text.css"/>\n'
|
opfstr += ' <item id="stylesheet" href="style.css" media-type="text/css"/>\n'
|
||||||
# adding image files to manifest
|
# adding image files to manifest
|
||||||
filenames = os.listdir(imgDir)
|
filenames = os.listdir(imgDir)
|
||||||
filenames = sorted(filenames)
|
filenames = sorted(filenames)
|
||||||
|
|||||||
199
Calibre_Plugins/K4MobiDeDRM_plugin/k4mobidedrm_orig.py
Normal file
199
Calibre_Plugins/K4MobiDeDRM_plugin/k4mobidedrm_orig.py
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
from __future__ import with_statement
|
||||||
|
|
||||||
|
# engine to remove drm from Kindle for Mac and Kindle for PC books
|
||||||
|
# for personal use for archiving and converting your ebooks
|
||||||
|
|
||||||
|
# PLEASE DO NOT PIRATE EBOOKS!
|
||||||
|
|
||||||
|
# We want all authors and publishers, and eBook stores to live
|
||||||
|
# long and prosperous lives but at the same time we just want to
|
||||||
|
# be able to read OUR books on whatever device we want and to keep
|
||||||
|
# readable for a long, long time
|
||||||
|
|
||||||
|
# This borrows very heavily from works by CMBDTC, IHeartCabbages, skindle,
|
||||||
|
# unswindle, DarkReverser, ApprenticeAlf, DiapDealer, some_updates
|
||||||
|
# and many many others
|
||||||
|
|
||||||
|
|
||||||
|
__version__ = '3.6'
|
||||||
|
|
||||||
|
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 os, csv, getopt
|
||||||
|
import string
|
||||||
|
import re
|
||||||
|
|
||||||
|
class DrmException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
if 'calibre' in sys.modules:
|
||||||
|
inCalibre = True
|
||||||
|
else:
|
||||||
|
inCalibre = False
|
||||||
|
|
||||||
|
if inCalibre:
|
||||||
|
from calibre_plugins.k4mobidedrm import mobidedrm
|
||||||
|
from calibre_plugins.k4mobidedrm import topazextract
|
||||||
|
from calibre_plugins.k4mobidedrm import kgenpids
|
||||||
|
else:
|
||||||
|
import mobidedrm
|
||||||
|
import topazextract
|
||||||
|
import kgenpids
|
||||||
|
|
||||||
|
|
||||||
|
# cleanup bytestring filenames
|
||||||
|
# borrowed from calibre from calibre/src/calibre/__init__.py
|
||||||
|
# added in removal of non-printing chars
|
||||||
|
# and removal of . at start
|
||||||
|
# convert spaces to underscores
|
||||||
|
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):
|
||||||
|
# handle the obvious cases at the beginning
|
||||||
|
if not os.path.isfile(infile):
|
||||||
|
print "Error: Input file does not exist"
|
||||||
|
return 1
|
||||||
|
|
||||||
|
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 = bookname
|
||||||
|
if len(bookname)>4 and len(filenametitle)>4 and bookname[:4] != filenametitle[:4]:
|
||||||
|
outfilename = outfilename + "_" + filenametitle
|
||||||
|
|
||||||
|
# build pid list
|
||||||
|
md1, md2 = mb.getPIDMetaInfo()
|
||||||
|
pidlst = kgenpids.getPidList(md1, md2, k4, pids, serials, kInfoFiles)
|
||||||
|
|
||||||
|
try:
|
||||||
|
mb.processBook(pidlst)
|
||||||
|
|
||||||
|
except mobidedrm.DrmException, e:
|
||||||
|
print "Error: " + str(e) + "\nDRM Removal Failed.\n"
|
||||||
|
return 1
|
||||||
|
except topazextract.TpzDRMError, e:
|
||||||
|
print "Error: " + str(e) + "\nDRM Removal Failed.\n"
|
||||||
|
return 1
|
||||||
|
except Exception, e:
|
||||||
|
print "Error: " + str(e) + "\nDRM Removal Failed.\n"
|
||||||
|
return 1
|
||||||
|
|
||||||
|
if mobi:
|
||||||
|
outfile = os.path.join(outdir, outfilename + '_nodrm' + '.mobi')
|
||||||
|
mb.getMobiFile(outfile)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# topaz:
|
||||||
|
print " Creating NoDRM HTMLZ Archive"
|
||||||
|
zipname = os.path.join(outdir, outfilename + '_nodrm' + '.htmlz')
|
||||||
|
mb.getHTMLZip(zipname)
|
||||||
|
|
||||||
|
print " Creating SVG HTMLZ Archive"
|
||||||
|
zipname = os.path.join(outdir, outfilename + '_SVG' + '.htmlz')
|
||||||
|
mb.getSVGZip(zipname)
|
||||||
|
|
||||||
|
print " Creating XML ZIP Archive"
|
||||||
|
zipname = os.path.join(outdir, outfilename + '_XML' + '.zip')
|
||||||
|
mb.getXMLZip(zipname)
|
||||||
|
|
||||||
|
# remove internal temporary directory of Topaz pieces
|
||||||
|
mb.cleanup()
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
#
|
||||||
|
# Main
|
||||||
|
#
|
||||||
|
def main(argv=sys.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 ' '
|
||||||
|
try:
|
||||||
|
opts, args = getopt.getopt(sys.argv[1:], "k:p:s:")
|
||||||
|
except getopt.GetoptError, err:
|
||||||
|
print str(err)
|
||||||
|
usage(progname)
|
||||||
|
sys.exit(2)
|
||||||
|
if len(args)<2:
|
||||||
|
usage(progname)
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
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 = 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)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.stdout=Unbuffered(sys.stdout)
|
||||||
|
sys.exit(main())
|
||||||
|
|
||||||
@@ -11,16 +11,28 @@ from struct import pack, unpack, unpack_from
|
|||||||
class DrmException(Exception):
|
class DrmException(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
global kindleDatabase
|
|
||||||
global charMap1
|
global charMap1
|
||||||
global charMap2
|
|
||||||
global charMap3
|
global charMap3
|
||||||
global charMap4
|
global charMap4
|
||||||
|
|
||||||
|
if 'calibre' in sys.modules:
|
||||||
|
inCalibre = True
|
||||||
|
else:
|
||||||
|
inCalibre = False
|
||||||
|
|
||||||
|
if inCalibre:
|
||||||
if sys.platform.startswith('win'):
|
if sys.platform.startswith('win'):
|
||||||
from k4pcutils import openKindleInfo, CryptUnprotectData, GetUserName, GetVolumeSerialNumber, charMap2
|
from calibre_plugins.k4mobidedrm.k4pcutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString
|
||||||
|
|
||||||
if sys.platform.startswith('darwin'):
|
if sys.platform.startswith('darwin'):
|
||||||
from k4mutils import openKindleInfo, CryptUnprotectData, GetUserName, GetVolumeSerialNumber, charMap2
|
from calibre_plugins.k4mobidedrm.k4mutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString
|
||||||
|
else:
|
||||||
|
if sys.platform.startswith('win'):
|
||||||
|
from k4pcutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString
|
||||||
|
|
||||||
|
if sys.platform.startswith('darwin'):
|
||||||
|
from k4mutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString
|
||||||
|
|
||||||
|
|
||||||
charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M"
|
charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M"
|
||||||
charMap3 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
charMap3 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
||||||
@@ -67,62 +79,6 @@ def decode(data,map):
|
|||||||
result += pack("B",value)
|
result += pack("B",value)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
# Parse the Kindle.info file and return the records as a list of key-values
|
|
||||||
def parseKindleInfo(kInfoFile):
|
|
||||||
DB = {}
|
|
||||||
infoReader = openKindleInfo(kInfoFile)
|
|
||||||
infoReader.read(1)
|
|
||||||
data = infoReader.read()
|
|
||||||
if sys.platform.startswith('win'):
|
|
||||||
items = data.split('{')
|
|
||||||
else :
|
|
||||||
items = data.split('[')
|
|
||||||
for item in items:
|
|
||||||
splito = item.split(':')
|
|
||||||
DB[splito[0]] =splito[1]
|
|
||||||
return DB
|
|
||||||
|
|
||||||
# Get a record from the Kindle.info file for the key "hashedKey" (already hashed and encoded). Return the decoded and decrypted record
|
|
||||||
def getKindleInfoValueForHash(hashedKey):
|
|
||||||
global kindleDatabase
|
|
||||||
global charMap1
|
|
||||||
global charMap2
|
|
||||||
encryptedValue = decode(kindleDatabase[hashedKey],charMap2)
|
|
||||||
if sys.platform.startswith('win'):
|
|
||||||
return CryptUnprotectData(encryptedValue,"")
|
|
||||||
else:
|
|
||||||
cleartext = CryptUnprotectData(encryptedValue)
|
|
||||||
return decode(cleartext, charMap1)
|
|
||||||
|
|
||||||
# Get a record from the Kindle.info file for the string in "key" (plaintext). Return the decoded and decrypted record
|
|
||||||
def getKindleInfoValueForKey(key):
|
|
||||||
global charMap2
|
|
||||||
return getKindleInfoValueForHash(encodeHash(key,charMap2))
|
|
||||||
|
|
||||||
# Find if the original string for a hashed/encoded string is known. If so return the original string othwise return an empty string.
|
|
||||||
def findNameForHash(hash):
|
|
||||||
global charMap2
|
|
||||||
names = ["kindle.account.tokens","kindle.cookie.item","eulaVersionAccepted","login_date","kindle.token.item","login","kindle.key.item","kindle.name.info","kindle.device.info", "MazamaRandomNumber"]
|
|
||||||
result = ""
|
|
||||||
for name in names:
|
|
||||||
if hash == encodeHash(name, charMap2):
|
|
||||||
result = name
|
|
||||||
break
|
|
||||||
return result
|
|
||||||
|
|
||||||
# Print all the records from the kindle.info file (option -i)
|
|
||||||
def printKindleInfo():
|
|
||||||
for record in kindleDatabase:
|
|
||||||
name = findNameForHash(record)
|
|
||||||
if name != "" :
|
|
||||||
print (name)
|
|
||||||
print ("--------------------------")
|
|
||||||
else :
|
|
||||||
print ("Unknown Record")
|
|
||||||
print getKindleInfoValueForHash(record)
|
|
||||||
print "\n"
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# PID generation routines
|
# PID generation routines
|
||||||
#
|
#
|
||||||
@@ -221,8 +177,6 @@ def pidFromSerial(s, l):
|
|||||||
|
|
||||||
# Parse the EXTH header records and use the Kindle serial number to calculate the book pid.
|
# Parse the EXTH header records and use the Kindle serial number to calculate the book pid.
|
||||||
def getKindlePid(pidlst, rec209, token, serialnum):
|
def getKindlePid(pidlst, rec209, token, serialnum):
|
||||||
|
|
||||||
if rec209 != None:
|
|
||||||
# Compute book PID
|
# Compute book PID
|
||||||
pidHash = SHA1(serialnum+rec209+token)
|
pidHash = SHA1(serialnum+rec209+token)
|
||||||
bookPID = encodePID(pidHash)
|
bookPID = encodePID(pidHash)
|
||||||
@@ -237,33 +191,41 @@ def getKindlePid(pidlst, rec209, token, serialnum):
|
|||||||
return pidlst
|
return pidlst
|
||||||
|
|
||||||
|
|
||||||
# Parse the EXTH header records and parse the Kindleinfo
|
# parse the Kindleinfo file to calculate the book pid.
|
||||||
# file to calculate the book pid.
|
|
||||||
|
|
||||||
def getK4Pids(pidlst, rec209, token, kInfoFile=None):
|
keynames = ["kindle.account.tokens","kindle.cookie.item","eulaVersionAccepted","login_date","kindle.token.item","login","kindle.key.item","kindle.name.info","kindle.device.info", "MazamaRandomNumber"]
|
||||||
global kindleDatabase
|
|
||||||
|
def getK4Pids(pidlst, rec209, token, kInfoFile):
|
||||||
global charMap1
|
global charMap1
|
||||||
kindleDatabase = None
|
kindleDatabase = None
|
||||||
try:
|
try:
|
||||||
kindleDatabase = parseKindleInfo(kInfoFile)
|
kindleDatabase = getDBfromFile(kInfoFile)
|
||||||
except Exception, message:
|
except Exception, message:
|
||||||
print(message)
|
print(message)
|
||||||
|
kindleDatabase = None
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if kindleDatabase == None :
|
if kindleDatabase == None :
|
||||||
return pidlst
|
return pidlst
|
||||||
|
|
||||||
|
try:
|
||||||
# Get the Mazama Random number
|
# Get the Mazama Random number
|
||||||
MazamaRandomNumber = getKindleInfoValueForKey("MazamaRandomNumber")
|
MazamaRandomNumber = kindleDatabase["MazamaRandomNumber"]
|
||||||
|
|
||||||
# Get the HDD serial
|
# Get the kindle account token
|
||||||
encodedSystemVolumeSerialNumber = encodeHash(GetVolumeSerialNumber(),charMap1)
|
kindleAccountToken = kindleDatabase["kindle.account.tokens"]
|
||||||
|
except KeyError:
|
||||||
|
print "Keys not found in " + kInfoFile
|
||||||
|
return pidlst
|
||||||
|
|
||||||
|
# Get the ID string used
|
||||||
|
encodedIDString = encodeHash(GetIDString(),charMap1)
|
||||||
|
|
||||||
# Get the current user name
|
# Get the current user name
|
||||||
encodedUsername = encodeHash(GetUserName(),charMap1)
|
encodedUsername = encodeHash(GetUserName(),charMap1)
|
||||||
|
|
||||||
# concat, hash and encode to calculate the DSN
|
# concat, hash and encode to calculate the DSN
|
||||||
DSN = encode(SHA1(MazamaRandomNumber+encodedSystemVolumeSerialNumber+encodedUsername),charMap1)
|
DSN = encode(SHA1(MazamaRandomNumber+encodedIDString+encodedUsername),charMap1)
|
||||||
|
|
||||||
# Compute the device PID (for which I can tell, is used for nothing).
|
# Compute the device PID (for which I can tell, is used for nothing).
|
||||||
table = generatePidEncryptionTable()
|
table = generatePidEncryptionTable()
|
||||||
@@ -271,13 +233,7 @@ def getK4Pids(pidlst, rec209, token, kInfoFile=None):
|
|||||||
devicePID = checksumPid(devicePID)
|
devicePID = checksumPid(devicePID)
|
||||||
pidlst.append(devicePID)
|
pidlst.append(devicePID)
|
||||||
|
|
||||||
# Compute book PID
|
# Compute book PIDs
|
||||||
if rec209 == None:
|
|
||||||
print "\nNo EXTH record type 209 - Perhaps not a K4 file?"
|
|
||||||
return pidlst
|
|
||||||
|
|
||||||
# Get the kindle account token
|
|
||||||
kindleAccountToken = getKindleInfoValueForKey("kindle.account.tokens")
|
|
||||||
|
|
||||||
# book pid
|
# book pid
|
||||||
pidHash = SHA1(DSN+kindleAccountToken+rec209+token)
|
pidHash = SHA1(DSN+kindleAccountToken+rec209+token)
|
||||||
@@ -301,8 +257,10 @@ def getK4Pids(pidlst, rec209, token, kInfoFile=None):
|
|||||||
|
|
||||||
def getPidList(md1, md2, k4, pids, serials, kInfoFiles):
|
def getPidList(md1, md2, k4, pids, serials, kInfoFiles):
|
||||||
pidlst = []
|
pidlst = []
|
||||||
|
if kInfoFiles is None:
|
||||||
|
kInfoFiles = []
|
||||||
if k4:
|
if k4:
|
||||||
pidlst = getK4Pids(pidlst, md1, md2)
|
kInfoFiles = getKindleInfoFiles(kInfoFiles)
|
||||||
for infoFile in kInfoFiles:
|
for infoFile in kInfoFiles:
|
||||||
pidlst = getK4Pids(pidlst, md1, md2, infoFile)
|
pidlst = getK4Pids(pidlst, md1, md2, infoFile)
|
||||||
for serialnum in serials:
|
for serialnum in serials:
|
||||||
|
|||||||
@@ -10,7 +10,12 @@ class Unbuffered:
|
|||||||
return getattr(self.stream, attr)
|
return getattr(self.stream, attr)
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
sys.stdout=Unbuffered(sys.stdout)
|
|
||||||
|
if 'calibre' in sys.modules:
|
||||||
|
inCalibre = True
|
||||||
|
else:
|
||||||
|
inCalibre = False
|
||||||
|
|
||||||
import os, csv, getopt
|
import os, csv, getopt
|
||||||
import zlib, zipfile, tempfile, shutil
|
import zlib, zipfile, tempfile, shutil
|
||||||
from struct import pack
|
from struct import pack
|
||||||
@@ -19,9 +24,31 @@ from struct import unpack
|
|||||||
class TpzDRMError(Exception):
|
class TpzDRMError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
# local support routines
|
# local support routines
|
||||||
|
if inCalibre:
|
||||||
|
from calibre_plugins.k4mobidedrm import kgenpids
|
||||||
|
from calibre_plugins.k4mobidedrm import genbook
|
||||||
|
else:
|
||||||
import kgenpids
|
import kgenpids
|
||||||
import genbook
|
import genbook
|
||||||
|
|
||||||
|
|
||||||
|
# recursive zip creation support routine
|
||||||
|
def zipUpDir(myzip, tdir, localname):
|
||||||
|
currentdir = tdir
|
||||||
|
if localname != "":
|
||||||
|
currentdir = os.path.join(currentdir,localname)
|
||||||
|
list = os.listdir(currentdir)
|
||||||
|
for file in list:
|
||||||
|
afilename = file
|
||||||
|
localfilePath = os.path.join(localname, afilename)
|
||||||
|
realfilePath = os.path.join(currentdir,file)
|
||||||
|
if os.path.isfile(realfilePath):
|
||||||
|
myzip.write(realfilePath, localfilePath)
|
||||||
|
elif os.path.isdir(realfilePath):
|
||||||
|
zipUpDir(myzip, tdir, localfilePath)
|
||||||
|
|
||||||
#
|
#
|
||||||
# Utility routines
|
# Utility routines
|
||||||
#
|
#
|
||||||
@@ -110,9 +137,9 @@ def decryptDkeyRecords(data,PID):
|
|||||||
|
|
||||||
|
|
||||||
class TopazBook:
|
class TopazBook:
|
||||||
def __init__(self, filename, outdir):
|
def __init__(self, filename):
|
||||||
self.fo = file(filename, 'rb')
|
self.fo = file(filename, 'rb')
|
||||||
self.outdir = outdir
|
self.outdir = tempfile.mkdtemp()
|
||||||
self.bookPayloadOffset = 0
|
self.bookPayloadOffset = 0
|
||||||
self.bookHeaderRecords = {}
|
self.bookHeaderRecords = {}
|
||||||
self.bookMetadata = {}
|
self.bookMetadata = {}
|
||||||
@@ -157,17 +184,22 @@ class TopazBook:
|
|||||||
raise TpzDRMError("Parse Error : Record Names Don't Match")
|
raise TpzDRMError("Parse Error : Record Names Don't Match")
|
||||||
flags = ord(self.fo.read(1))
|
flags = ord(self.fo.read(1))
|
||||||
nbRecords = ord(self.fo.read(1))
|
nbRecords = ord(self.fo.read(1))
|
||||||
|
# print nbRecords
|
||||||
for i in range (0,nbRecords) :
|
for i in range (0,nbRecords) :
|
||||||
record = [bookReadString(self.fo), bookReadString(self.fo)]
|
keyval = bookReadString(self.fo)
|
||||||
self.bookMetadata[record[0]] = record[1]
|
content = bookReadString(self.fo)
|
||||||
|
# print keyval
|
||||||
|
# print content
|
||||||
|
self.bookMetadata[keyval] = content
|
||||||
return self.bookMetadata
|
return self.bookMetadata
|
||||||
|
|
||||||
def getPIDMetaInfo(self):
|
def getPIDMetaInfo(self):
|
||||||
keysRecord = None
|
keysRecord = self.bookMetadata.get('keys','')
|
||||||
KeysRecordRecord = None
|
keysRecordRecord = ''
|
||||||
if 'keys' in self.bookMetadata:
|
if keysRecord != '':
|
||||||
keysRecord = self.bookMetadata['keys']
|
keylst = keysRecord.split(',')
|
||||||
keysRecordRecord = self.bookMetadata[keysRecord]
|
for keyval in keylst:
|
||||||
|
keysRecordRecord += self.bookMetadata.get(keyval,'')
|
||||||
return keysRecord, keysRecordRecord
|
return keysRecord, keysRecordRecord
|
||||||
|
|
||||||
def getBookTitle(self):
|
def getBookTitle(self):
|
||||||
@@ -312,21 +344,33 @@ class TopazBook:
|
|||||||
file(outputFile, 'wb').write(record)
|
file(outputFile, 'wb').write(record)
|
||||||
print " "
|
print " "
|
||||||
|
|
||||||
|
def getHTMLZip(self, zipname):
|
||||||
|
htmlzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||||
|
htmlzip.write(os.path.join(self.outdir,'book.html'),'book.html')
|
||||||
|
htmlzip.write(os.path.join(self.outdir,'book.opf'),'book.opf')
|
||||||
|
if os.path.isfile(os.path.join(self.outdir,'cover.jpg')):
|
||||||
|
htmlzip.write(os.path.join(self.outdir,'cover.jpg'),'cover.jpg')
|
||||||
|
htmlzip.write(os.path.join(self.outdir,'style.css'),'style.css')
|
||||||
|
zipUpDir(htmlzip, self.outdir, 'img')
|
||||||
|
htmlzip.close()
|
||||||
|
|
||||||
def zipUpDir(myzip, tempdir,localname):
|
def getSVGZip(self, zipname):
|
||||||
currentdir = tempdir
|
svgzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||||
if localname != "":
|
svgzip.write(os.path.join(self.outdir,'index_svg.xhtml'),'index_svg.xhtml')
|
||||||
currentdir = os.path.join(currentdir,localname)
|
zipUpDir(svgzip, self.outdir, 'svg')
|
||||||
list = os.listdir(currentdir)
|
zipUpDir(svgzip, self.outdir, 'img')
|
||||||
for file in list:
|
svgzip.close()
|
||||||
afilename = file
|
|
||||||
localfilePath = os.path.join(localname, afilename)
|
|
||||||
realfilePath = os.path.join(currentdir,file)
|
|
||||||
if os.path.isfile(realfilePath):
|
|
||||||
myzip.write(realfilePath, localfilePath)
|
|
||||||
elif os.path.isdir(realfilePath):
|
|
||||||
zipUpDir(myzip, tempdir, localfilePath)
|
|
||||||
|
|
||||||
|
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):
|
def usage(progname):
|
||||||
print "Removes DRM protection from Topaz ebooks and extract the contents"
|
print "Removes DRM protection from Topaz ebooks and extract the contents"
|
||||||
@@ -378,57 +422,46 @@ def main(argv=sys.argv):
|
|||||||
return 1
|
return 1
|
||||||
|
|
||||||
bookname = os.path.splitext(os.path.basename(infile))[0]
|
bookname = os.path.splitext(os.path.basename(infile))[0]
|
||||||
tempdir = tempfile.mkdtemp()
|
|
||||||
|
|
||||||
tb = TopazBook(infile, tempdir)
|
tb = TopazBook(infile)
|
||||||
title = tb.getBookTitle()
|
title = tb.getBookTitle()
|
||||||
print "Processing Book: ", title
|
print "Processing Book: ", title
|
||||||
keysRecord, keysRecordRecord = tb.getPIDMetaInfo()
|
keysRecord, keysRecordRecord = tb.getPIDMetaInfo()
|
||||||
pidlst = kgenpids.getPidList(keysRecord, keysRecordRecord, k4, pids, serials, kInfoFiles)
|
pidlst = kgenpids.getPidList(keysRecord, keysRecordRecord, k4, pids, serials, kInfoFiles)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
print "Decrypting Book"
|
||||||
tb.processBook(pidlst)
|
tb.processBook(pidlst)
|
||||||
except TpzDRMError, e:
|
|
||||||
print str(e)
|
|
||||||
print " Creating DeBug Full Zip Archive of Book"
|
|
||||||
zipname = os.path.join(outdir, bookname + '_debug' + '.zip')
|
|
||||||
myzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
|
||||||
zipUpDir(myzip, tempdir, '')
|
|
||||||
myzip.close()
|
|
||||||
return 1
|
|
||||||
|
|
||||||
print " Creating HTML ZIP Archive"
|
print " Creating HTML ZIP Archive"
|
||||||
zipname = os.path.join(outdir, bookname + '_nodrm' + '.zip')
|
zipname = os.path.join(outdir, bookname + '_nodrm' + '.htmlz')
|
||||||
myzip1 = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
tb.getHTMLZip(zipname)
|
||||||
myzip1.write(os.path.join(tempdir,'book.html'),'book.html')
|
|
||||||
myzip1.write(os.path.join(tempdir,'book.opf'),'book.opf')
|
|
||||||
if os.path.isfile(os.path.join(tempdir,'cover.jpg')):
|
|
||||||
myzip1.write(os.path.join(tempdir,'cover.jpg'),'cover.jpg')
|
|
||||||
myzip1.write(os.path.join(tempdir,'style.css'),'style.css')
|
|
||||||
zipUpDir(myzip1, tempdir, 'img')
|
|
||||||
myzip1.close()
|
|
||||||
|
|
||||||
print " Creating SVG ZIP Archive"
|
print " Creating SVG ZIP Archive"
|
||||||
zipname = os.path.join(outdir, bookname + '_SVG' + '.zip')
|
zipname = os.path.join(outdir, bookname + '_SVG' + '.htmlz')
|
||||||
myzip2 = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
tb.getSVGZip(zipname)
|
||||||
myzip2.write(os.path.join(tempdir,'index_svg.xhtml'),'index_svg.xhtml')
|
|
||||||
zipUpDir(myzip2, tempdir, 'svg')
|
|
||||||
zipUpDir(myzip2, tempdir, 'img')
|
|
||||||
myzip2.close()
|
|
||||||
|
|
||||||
print " Creating XML ZIP Archive"
|
print " Creating XML ZIP Archive"
|
||||||
zipname = os.path.join(outdir, bookname + '_XML' + '.zip')
|
zipname = os.path.join(outdir, bookname + '_XML' + '.zip')
|
||||||
myzip3 = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
tb.getXMLZip(zipname)
|
||||||
targetdir = os.path.join(tempdir,'xml')
|
|
||||||
zipUpDir(myzip3, targetdir, '')
|
|
||||||
zipUpDir(myzip3, tempdir, 'img')
|
|
||||||
myzip3.close()
|
|
||||||
|
|
||||||
shutil.rmtree(tempdir)
|
# removing internal temporary directory of pieces
|
||||||
|
tb.cleanup()
|
||||||
|
|
||||||
|
except TpzDRMError, e:
|
||||||
|
print str(e)
|
||||||
|
tb.cleanup()
|
||||||
|
return 1
|
||||||
|
|
||||||
|
except Exception, e:
|
||||||
|
print str(e)
|
||||||
|
tb.cleanup
|
||||||
|
return 1
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
sys.stdout=Unbuffered(sys.stdout)
|
||||||
sys.exit(main())
|
sys.exit(main())
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ This plugin is meant to decrypt Adobe Digital Edition PDFs that are protected wi
|
|||||||
|
|
||||||
I had the much easier job of converting them to a Calibre plugin.
|
I had the much easier job of converting them to a Calibre plugin.
|
||||||
|
|
||||||
Go to Calibre's Preferences page... click on the Plugins button. Use the file dialog button to select the plugin's zip file (ineptpdf_vXX_plugin.zip) and click the 'Add' button. you're done.
|
|
||||||
|
|
||||||
|
This plugin is meant to decrypt Adobe Digital Edition PDFs that are protected with Adobe's Adept encryption. It is meant to function without having to install any dependencies... other than having Calibre installed, of course. It will still work if you have Python, PyCrypto and/or OpenSSL already installed, but they aren't necessary.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Installation:
|
Installation:
|
||||||
|
|||||||
@@ -5,10 +5,11 @@ This plugin supersedes MobiDeDRM, K4DeDRM, and K4PCDeDRM and K4X plugins. If yo
|
|||||||
|
|
||||||
|
|
||||||
This plugin is meant to remove the DRM from .prc, .azw, .azw1, and .tpz ebooks. Calibre can then convert them to whatever format you desire. It is meant to function without having to install any dependencies except for Calibre being on your same machine and in the same account as your "Kindle for PC" or "Kindle for Mac" application if you are going to remove the DRM from those types of books.
|
This plugin is meant to remove the DRM from .prc, .azw, .azw1, and .tpz ebooks. Calibre can then convert them to whatever format you desire. It is meant to function without having to install any dependencies except for Calibre being on your same machine and in the same account as your "Kindle for PC" or "Kindle for Mac" application if you are going to remove the DRM from those types of books.
|
||||||
Go to Calibre's Preferences page... click on the Plugins button. Use the file dialog button to select the plugin's zip file (K4MobiDeDRM_vXX_plugin.zip) and click the 'Add' button. You're done.
|
|
||||||
|
|
||||||
|
|
||||||
Installation:
|
Installation:
|
||||||
Highlight the plugin (K4MobiDeDRM under the "File type plugins" category) and click the "Customize Plugin" button on Calibre's Preferences->Plugins page. Enter a comma separated list of your 10 digit PIDs. Include in this list (again separated by commas) any 16 digit serial numbers the standalone Kindles you may have (these typically begin "B0...") This is not needed if you only want to decode "Kindle for PC" or "Kindle for Mac" books.
|
|
||||||
Go to Calibre's Preferences page. Do **NOT** select "Get Plugins to enhance calibre" as this is reserved for official calibre plugins", instead select "Change calibre behavior". Under "Advanced" click on the on the Plugins button. Click on the "Load plugin from file" button at the bottom of the screen. Use the file dialog button to select the plugin's zip file (K4MobiDeDRM_vXX_plugin.zip) and click the "Add" (or it may say "Open" button. Then click on the "Yes" button in the warning dialog that appears. A Confirmation dialog appears that says the plugin has been installed.
|
Go to Calibre's Preferences page. Do **NOT** select "Get Plugins to enhance calibre" as this is reserved for official calibre plugins", instead select "Change calibre behavior". Under "Advanced" click on the on the Plugins button. Click on the "Load plugin from file" button at the bottom of the screen. Use the file dialog button to select the plugin's zip file (K4MobiDeDRM_vXX_plugin.zip) and click the "Add" (or it may say "Open" button. Then click on the "Yes" button in the warning dialog that appears. A Confirmation dialog appears that says the plugin has been installed.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ All credit given to The Dark Reverser for the original standalone script. I had
|
|||||||
All credit given to The Dark Reverser for the original standalone script. I had the much easier job of converting it to a Calibre plugin.
|
All credit given to The Dark Reverser for the original standalone script. I had the much easier job of converting it to a Calibre plugin.
|
||||||
|
|
||||||
|
|
||||||
Go to Calibre's Preferences page... click on the Plugins button. Use the file dialog button to select the plugin's zip file (eReaderPDB2PML_vXX_plugin.zip) and click the 'Add' button. You're done.
|
|
||||||
|
This plugin is meant to convert secure Ereader files (PDB) to unsecured PMLZ files. Calibre can then convert it to whatever format you desire. It is meant to function without having to install any dependencies... other than having Calibre installed, of course. I've included the psyco libraries (compiled for each platform) for speed. If your system can use them, great! Otherwise, they won't be used and things will just work slower.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Installation:
|
Installation:
|
||||||
|
|||||||
@@ -9,9 +9,11 @@ with Adobe's Adept encryption. It is meant to function without having to install
|
|||||||
I had the much easier job of converting them to a Calibre plugin.
|
I had the much easier job of converting them to a Calibre plugin.
|
||||||
|
|
||||||
|
|
||||||
Go to Calibre's Preferences page... click on the Plugins button. Use the file dialog button to select the plugin's zip file (ignobleepub_vXX_plugin.zip) and
|
|
||||||
This plugin is meant to decrypt Barnes & Noble Epubs that are protected
|
This plugin is meant to decrypt Barnes & Noble Epubs that are protected
|
||||||
|
|
||||||
|
with Adobe's Adept encryption. It is meant to function without having to install any dependencies... other than having Calibre installed, of course. It will still work if you have Python and PyCrypto already installed, but they aren't necessary.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Installation:
|
Installation:
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ This plugin is meant to decrypt Adobe Digital Edition Epubs that are protected w
|
|||||||
|
|
||||||
I had the much easier job of converting them to a Calibre plugin.
|
I had the much easier job of converting them to a Calibre plugin.
|
||||||
|
|
||||||
Go to Calibre's Preferences page... click on the Plugins button. Use the file dialog button to select the plugin's zip file (ineptepub_vXX_plugin.zip) and click the 'Add' button. you're done.
|
|
||||||
|
|
||||||
|
This plugin is meant to decrypt Adobe Digital Edition Epubs that are protected with Adobe's Adept encryption. It is meant to function without having to install any dependencies... other than having Calibre installed, of course. It will still work if you have Python and PyCrypto already installed, but they aren't necessary.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Installation:
|
Installation:
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
Installing openssl on Windows 64-bit (Windows 2000 and higher)
|
|
||||||
|
|
||||||
Win64 OpenSSL v0.9.8o (8Mb)
|
|
||||||
http://www.slproweb.com/download/Win64OpenSSL-0_9_8o.exe
|
|
||||||
(if you get an error message about missing Visual C++ redistributables... cancel the install and install the below support program from Microsoft, THEN install OpenSSL)
|
|
||||||
|
|
||||||
Visual C++ 2008 Redistributables (x64) (1.7Mb)
|
|
||||||
http://www.microsoft.com/downloads/details.aspx?familyid=bd2a6171-e2d6-4230-b809-9a8d7548c1b6
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Installing openssl on Windows 32-bit (Windows 2000 and higher)
|
|
||||||
|
|
||||||
Win32 OpenSSL v0.9.8o (8Mb)
|
|
||||||
http://www.slproweb.com/download/Win32OpenSSL-0_9_8o.exe
|
|
||||||
(if you get an error message about missing Visual C++ redistributables... cancel the install and install the below support program from Microsoft, THEN install OpenSSL)
|
|
||||||
|
|
||||||
Visual C++ 2008 Redistributables (1.7Mb)
|
|
||||||
http://www.microsoft.com/downloads/details.aspx?familyid=9B2DA534-3E03-4391-8A4D-074B9F2BC1BF
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Other versions of OpenSSL (and versions for Windows older than Windows 2000) can be found on the following website.
|
|
||||||
|
|
||||||
Shining Light Productions
|
|
||||||
http://www.slproweb.com/products/Win32OpenSSL.html
|
|
||||||
Binary file not shown.
@@ -31,10 +31,15 @@
|
|||||||
# 0.0.1 - Initial release
|
# 0.0.1 - Initial release
|
||||||
# 0.0.2 - updated to distinguish it from earlier non-openssl version
|
# 0.0.2 - updated to distinguish it from earlier non-openssl version
|
||||||
# 0.0.3 - removed added psyco code as it is not supported under Calibre's Python 2.7
|
# 0.0.3 - removed added psyco code as it is not supported under Calibre's Python 2.7
|
||||||
|
# 0.0.4 - minor typos fixed
|
||||||
|
# 0.0.5 - updated to the new calibre plugin interface
|
||||||
|
|
||||||
import sys, os
|
import sys, os
|
||||||
|
|
||||||
from calibre.customize import FileTypePlugin
|
from calibre.customize import FileTypePlugin
|
||||||
|
from calibre.ptempfile import PersistentTemporaryDirectory
|
||||||
|
from calibre.constants import iswindows, isosx
|
||||||
|
from calibre_plugins.erdrpdb2pml import erdr2pml
|
||||||
|
|
||||||
class eRdrDeDRM(FileTypePlugin):
|
class eRdrDeDRM(FileTypePlugin):
|
||||||
name = 'eReader PDB 2 PML' # Name of the plugin
|
name = 'eReader PDB 2 PML' # Name of the plugin
|
||||||
@@ -42,16 +47,14 @@ class eRdrDeDRM(FileTypePlugin):
|
|||||||
Credit given to The Dark Reverser for the original standalone script.'
|
Credit given to The Dark Reverser for the original standalone script.'
|
||||||
supported_platforms = ['linux', 'osx', 'windows'] # Platforms this plugin will run on
|
supported_platforms = ['linux', 'osx', 'windows'] # Platforms this plugin will run on
|
||||||
author = 'DiapDealer' # The author of this plugin
|
author = 'DiapDealer' # The author of this plugin
|
||||||
version = (0, 0, 3) # The version number of this plugin
|
version = (0, 0, 5) # The version number of this plugin
|
||||||
file_types = set(['pdb']) # The file types that this plugin will be applied to
|
file_types = set(['pdb']) # The file types that this plugin will be applied to
|
||||||
on_import = True # Run this plugin during the import
|
on_import = True # Run this plugin during the import
|
||||||
|
minimum_calibre_version = (0, 7, 55)
|
||||||
|
|
||||||
def run(self, path_to_ebook):
|
def run(self, path_to_ebook):
|
||||||
from calibre.ptempfile import PersistentTemporaryDirectory
|
|
||||||
from calibre.constants import iswindows, isosx
|
|
||||||
|
|
||||||
global bookname, erdr2pml
|
global bookname, erdr2pml
|
||||||
import erdr2pml
|
|
||||||
|
|
||||||
infile = path_to_ebook
|
infile = path_to_ebook
|
||||||
bookname = os.path.splitext(os.path.basename(infile))[0]
|
bookname = os.path.splitext(os.path.basename(infile))[0]
|
||||||
@@ -76,7 +79,6 @@ class eRdrDeDRM(FileTypePlugin):
|
|||||||
|
|
||||||
if pmlfilepath and pmlfilepath != 1:
|
if pmlfilepath and pmlfilepath != 1:
|
||||||
import zipfile
|
import zipfile
|
||||||
import shutil
|
|
||||||
print " Creating PMLZ file"
|
print " Creating PMLZ file"
|
||||||
myZipFile = zipfile.ZipFile(pmlzfile.name,'w',zipfile.ZIP_STORED, False)
|
myZipFile = zipfile.ZipFile(pmlzfile.name,'w',zipfile.ZIP_STORED, False)
|
||||||
list = os.listdir(outdir)
|
list = os.listdir(outdir)
|
||||||
@@ -56,32 +56,11 @@
|
|||||||
# 0.15 - enabled high-ascii to pml character encoding. DropBook now works on Mac.
|
# 0.15 - enabled high-ascii to pml character encoding. DropBook now works on Mac.
|
||||||
# 0.16 - convert to use openssl DES (very very fast) or pure python DES if openssl's libcrypto is not available
|
# 0.16 - convert to use openssl DES (very very fast) or pure python DES if openssl's libcrypto is not available
|
||||||
# 0.17 - added support for pycrypto's DES as well
|
# 0.17 - added support for pycrypto's DES as well
|
||||||
|
# 0.18 - on Windows try PyCrypto first and OpenSSL next
|
||||||
|
# 0.19 - Modify the interface to allow use of import
|
||||||
|
# 0.20 - modify to allow use inside new interface for calibre plugins
|
||||||
|
|
||||||
Des = None
|
__version__='0.20'
|
||||||
|
|
||||||
import openssl_des
|
|
||||||
Des = openssl_des.load_libcrypto()
|
|
||||||
|
|
||||||
# if that did not work then try pycrypto version of DES
|
|
||||||
if Des == None:
|
|
||||||
import pycrypto_des
|
|
||||||
Des = pycrypto_des.load_pycrypto()
|
|
||||||
|
|
||||||
# if that did not work then use pure python implementation
|
|
||||||
# of DES and try to speed it up with Psycho
|
|
||||||
if Des == None:
|
|
||||||
import python_des
|
|
||||||
Des = python_des.Des
|
|
||||||
# Import Psyco if available
|
|
||||||
try:
|
|
||||||
# http://psyco.sourceforge.net
|
|
||||||
import psyco
|
|
||||||
psyco.full()
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
__version__='0.17'
|
|
||||||
|
|
||||||
class Unbuffered:
|
class Unbuffered:
|
||||||
def __init__(self, stream):
|
def __init__(self, stream):
|
||||||
@@ -93,22 +72,73 @@ class Unbuffered:
|
|||||||
return getattr(self.stream, attr)
|
return getattr(self.stream, attr)
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
sys.stdout=Unbuffered(sys.stdout)
|
|
||||||
|
|
||||||
import struct, binascii, getopt, zlib, os, os.path, urllib, tempfile
|
import struct, binascii, getopt, zlib, os, os.path, urllib, tempfile
|
||||||
|
|
||||||
|
if 'calibre' in sys.modules:
|
||||||
|
inCalibre = True
|
||||||
|
else:
|
||||||
|
inCalibre = False
|
||||||
|
|
||||||
|
Des = None
|
||||||
|
if sys.platform.startswith('win'):
|
||||||
|
# first try with pycrypto
|
||||||
|
if inCalibre:
|
||||||
|
from calibre_plugins.erdrpdb2pml import pycrypto_des
|
||||||
|
else:
|
||||||
|
import pycrypto_des
|
||||||
|
Des = pycrypto_des.load_pycrypto()
|
||||||
|
if Des == None:
|
||||||
|
# they try with openssl
|
||||||
|
if inCalibre:
|
||||||
|
from calibre_plugins.erdrpdb2pml import openssl_des
|
||||||
|
else:
|
||||||
|
import openssl_des
|
||||||
|
Des = openssl_des.load_libcrypto()
|
||||||
|
else:
|
||||||
|
# first try with openssl
|
||||||
|
if inCalibre:
|
||||||
|
from calibre_plugins.erdrpdb2pml import openssl_des
|
||||||
|
else:
|
||||||
|
import openssl_des
|
||||||
|
Des = openssl_des.load_libcrypto()
|
||||||
|
if Des == None:
|
||||||
|
# then try with pycrypto
|
||||||
|
if inCalibre:
|
||||||
|
from calibre_plugins.erdrpdb2pml import pycrypto_des
|
||||||
|
else:
|
||||||
|
import pycrypto_des
|
||||||
|
Des = pycrypto_des.load_pycrypto()
|
||||||
|
|
||||||
|
# if that did not work then use pure python implementation
|
||||||
|
# of DES and try to speed it up with Psycho
|
||||||
|
if Des == None:
|
||||||
|
if inCalibre:
|
||||||
|
from calibre_plugins.erdrpdb2pml import python_des
|
||||||
|
else:
|
||||||
|
import python_des
|
||||||
|
Des = python_des.Des
|
||||||
|
# Import Psyco if available
|
||||||
|
try:
|
||||||
|
# http://psyco.sourceforge.net
|
||||||
|
import psyco
|
||||||
|
psyco.full()
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from hashlib import sha1
|
from hashlib import sha1
|
||||||
except ImportError:
|
except ImportError:
|
||||||
# older Python release
|
# older Python release
|
||||||
import sha
|
import sha
|
||||||
sha1 = lambda s: sha.new(s)
|
sha1 = lambda s: sha.new(s)
|
||||||
|
|
||||||
import cgi
|
import cgi
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logging.basicConfig()
|
logging.basicConfig()
|
||||||
#logging.basicConfig(level=logging.DEBUG)
|
#logging.basicConfig(level=logging.DEBUG)
|
||||||
|
|
||||||
|
|
||||||
class Sectionizer(object):
|
class Sectionizer(object):
|
||||||
def __init__(self, filename, ident):
|
def __init__(self, filename, ident):
|
||||||
self.contents = file(filename, 'rb').read()
|
self.contents = file(filename, 'rb').read()
|
||||||
@@ -356,7 +386,7 @@ def cleanPML(pml):
|
|||||||
def convertEreaderToPml(infile, name, cc, outdir):
|
def convertEreaderToPml(infile, name, cc, outdir):
|
||||||
if not os.path.exists(outdir):
|
if not os.path.exists(outdir):
|
||||||
os.makedirs(outdir)
|
os.makedirs(outdir)
|
||||||
|
bookname = os.path.splitext(os.path.basename(infile))[0]
|
||||||
print " Decoding File"
|
print " Decoding File"
|
||||||
sect = Sectionizer(infile, 'PNRdPPrs')
|
sect = Sectionizer(infile, 'PNRdPPrs')
|
||||||
er = EreaderProcessor(sect.loadSection, name, cc)
|
er = EreaderProcessor(sect.loadSection, name, cc)
|
||||||
@@ -382,62 +412,14 @@ def convertEreaderToPml(infile, name, cc, outdir):
|
|||||||
# file(os.path.join(outdir, 'bookinfo.txt'),'wb').write(bkinfo)
|
# file(os.path.join(outdir, 'bookinfo.txt'),'wb').write(bkinfo)
|
||||||
|
|
||||||
|
|
||||||
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"
|
|
||||||
return
|
|
||||||
|
|
||||||
def main(argv=None):
|
|
||||||
global bookname
|
|
||||||
try:
|
|
||||||
opts, args = getopt.getopt(sys.argv[1:], "h", ["make-pmlz"])
|
|
||||||
except getopt.GetoptError, err:
|
|
||||||
print str(err)
|
|
||||||
usage()
|
|
||||||
return 1
|
|
||||||
make_pmlz = False
|
|
||||||
zipname = None
|
|
||||||
for o, a in opts:
|
|
||||||
if o == "-h":
|
|
||||||
usage()
|
|
||||||
return 0
|
|
||||||
elif o == "--make-pmlz":
|
|
||||||
make_pmlz = True
|
|
||||||
zipname = ''
|
|
||||||
|
|
||||||
print "eRdr2Pml v%s. Copyright (c) 2009 The Dark Reverser" % __version__
|
|
||||||
|
|
||||||
if len(args)!=3 and len(args)!=4:
|
|
||||||
usage()
|
|
||||||
return 1
|
|
||||||
else:
|
|
||||||
if len(args)==3:
|
|
||||||
infile, name, cc = args[0], args[1], args[2]
|
|
||||||
outdir = infile[:-4] + '_Source'
|
|
||||||
elif len(args)==4:
|
|
||||||
infile, outdir, name, cc = args[0], args[1], args[2], args[3]
|
|
||||||
|
|
||||||
|
def decryptBook(infile, outdir, name, cc, make_pmlz):
|
||||||
if make_pmlz :
|
if make_pmlz :
|
||||||
# ignore specified outdir, use tempdir instead
|
# ignore specified outdir, use tempdir instead
|
||||||
outdir = tempfile.mkdtemp()
|
outdir = tempfile.mkdtemp()
|
||||||
|
|
||||||
bookname = os.path.splitext(os.path.basename(infile))[0]
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
print "Processing..."
|
print "Processing..."
|
||||||
import time
|
|
||||||
start_time = time.time()
|
|
||||||
convertEreaderToPml(infile, name, cc, outdir)
|
convertEreaderToPml(infile, name, cc, outdir)
|
||||||
|
|
||||||
if make_pmlz :
|
if make_pmlz :
|
||||||
import zipfile
|
import zipfile
|
||||||
import shutil
|
import shutil
|
||||||
@@ -460,12 +442,7 @@ def main(argv=None):
|
|||||||
myZipFile.write(imagePath, localname)
|
myZipFile.write(imagePath, localname)
|
||||||
myZipFile.close()
|
myZipFile.close()
|
||||||
# remove temporary directory
|
# remove temporary directory
|
||||||
shutil.rmtree(outdir)
|
shutil.rmtree(outdir, True)
|
||||||
|
|
||||||
end_time = time.time()
|
|
||||||
search_time = end_time - start_time
|
|
||||||
print 'elapsed time: %.2f seconds' % (search_time, )
|
|
||||||
if make_pmlz :
|
|
||||||
print 'output is %s' % zipname
|
print 'output is %s' % zipname
|
||||||
else :
|
else :
|
||||||
print 'output in %s' % outdir
|
print 'output in %s' % outdir
|
||||||
@@ -475,6 +452,53 @@ def main(argv=None):
|
|||||||
return 1
|
return 1
|
||||||
return 0
|
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"
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv=None):
|
||||||
|
try:
|
||||||
|
opts, args = getopt.getopt(sys.argv[1:], "h", ["make-pmlz"])
|
||||||
|
except getopt.GetoptError, err:
|
||||||
|
print str(err)
|
||||||
|
usage()
|
||||||
|
return 1
|
||||||
|
make_pmlz = False
|
||||||
|
for o, a in opts:
|
||||||
|
if o == "-h":
|
||||||
|
usage()
|
||||||
|
return 0
|
||||||
|
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'
|
||||||
|
elif len(args)==4:
|
||||||
|
infile, outdir, name, cc = args[0], args[1], args[2], args[3]
|
||||||
|
|
||||||
|
return decryptBook(infile, outdir, name, cc, make_pmlz)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
sys.stdout=Unbuffered(sys.stdout)
|
||||||
sys.exit(main())
|
sys.exit(main())
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -4,7 +4,7 @@
|
|||||||
# Released under the terms of the GNU General Public Licence, version 3 or
|
# Released under the terms of the GNU General Public Licence, version 3 or
|
||||||
# later. <http://www.gnu.org/licenses/>
|
# later. <http://www.gnu.org/licenses/>
|
||||||
#
|
#
|
||||||
# Requires Calibre version 0.6.44 or higher.
|
# Requires Calibre version 0.7.55 or higher.
|
||||||
#
|
#
|
||||||
# All credit given to I <3 Cabbages for the original standalone scripts.
|
# All credit given to I <3 Cabbages for the original standalone scripts.
|
||||||
# I had the much easier job of converting them to Calibre a plugin.
|
# I had the much easier job of converting them to Calibre a plugin.
|
||||||
@@ -45,6 +45,10 @@
|
|||||||
# 0.1.1 - Allow Windows users to make use of openssl if they have it installed.
|
# 0.1.1 - Allow Windows users to make use of openssl if they have it installed.
|
||||||
# - Incorporated SomeUpdates zipfix routine.
|
# - Incorporated SomeUpdates zipfix routine.
|
||||||
# 0.1.2 - bug fix for non-ascii file names in encryption.xml
|
# 0.1.2 - bug fix for non-ascii file names in encryption.xml
|
||||||
|
# 0.1.3 - Try PyCrypto on Windows first
|
||||||
|
# 0.1.4 - update zipfix to deal with mimetype not in correct place
|
||||||
|
# 0.1.5 - update zipfix to deal with completely missing mimetype files
|
||||||
|
# 0.1.6 - update ot the new calibre plugin interface
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Decrypt Barnes & Noble ADEPT encrypted EPUB books.
|
Decrypt Barnes & Noble ADEPT encrypted EPUB books.
|
||||||
@@ -169,7 +173,10 @@ def _load_crypto_pycrypto():
|
|||||||
|
|
||||||
def _load_crypto():
|
def _load_crypto():
|
||||||
_aes = _aes2 = None
|
_aes = _aes2 = None
|
||||||
for loader in (_load_crypto_libcrypto, _load_crypto_pycrypto):
|
cryptolist = (_load_crypto_libcrypto, _load_crypto_pycrypto)
|
||||||
|
if sys.platform.startswith('win'):
|
||||||
|
cryptolist = (_load_crypto_pycrypto, _load_crypto_libcrypto)
|
||||||
|
for loader in cryptolist:
|
||||||
try:
|
try:
|
||||||
_aes, _aes2 = loader()
|
_aes, _aes2 = loader()
|
||||||
break
|
break
|
||||||
@@ -260,6 +267,7 @@ def plugin_main(userkey, inpath, outpath):
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
from calibre.customize import FileTypePlugin
|
from calibre.customize import FileTypePlugin
|
||||||
|
from calibre.constants import iswindows, isosx
|
||||||
|
|
||||||
class IgnobleDeDRM(FileTypePlugin):
|
class IgnobleDeDRM(FileTypePlugin):
|
||||||
name = 'Ignoble Epub DeDRM'
|
name = 'Ignoble Epub DeDRM'
|
||||||
@@ -267,8 +275,8 @@ class IgnobleDeDRM(FileTypePlugin):
|
|||||||
Credit given to I <3 Cabbages for the original stand-alone scripts.'
|
Credit given to I <3 Cabbages for the original stand-alone scripts.'
|
||||||
supported_platforms = ['linux', 'osx', 'windows']
|
supported_platforms = ['linux', 'osx', 'windows']
|
||||||
author = 'DiapDealer'
|
author = 'DiapDealer'
|
||||||
version = (0, 1, 2)
|
version = (0, 1, 6)
|
||||||
minimum_calibre_version = (0, 6, 44) # Compiled python libraries cannot be imported in earlier versions.
|
minimum_calibre_version = (0, 7, 55) # Compiled python libraries cannot be imported in earlier versions.
|
||||||
file_types = set(['epub'])
|
file_types = set(['epub'])
|
||||||
on_import = True
|
on_import = True
|
||||||
|
|
||||||
@@ -276,10 +284,6 @@ class IgnobleDeDRM(FileTypePlugin):
|
|||||||
global AES
|
global AES
|
||||||
global AES2
|
global AES2
|
||||||
|
|
||||||
from calibre.gui2 import is_ok_to_use_qt
|
|
||||||
from PyQt4.Qt import QMessageBox
|
|
||||||
from calibre.constants import iswindows, isosx
|
|
||||||
|
|
||||||
AES, AES2 = _load_crypto()
|
AES, AES2 = _load_crypto()
|
||||||
|
|
||||||
if AES == None or AES2 == None:
|
if AES == None or AES2 == None:
|
||||||
@@ -335,7 +339,7 @@ class IgnobleDeDRM(FileTypePlugin):
|
|||||||
for userkey in userkeys:
|
for userkey in userkeys:
|
||||||
# Create a TemporaryPersistent file to work with.
|
# Create a TemporaryPersistent file to work with.
|
||||||
# Check original epub archive for zip errors.
|
# Check original epub archive for zip errors.
|
||||||
import zipfix
|
from calibre_plugins.ignobleepub import zipfix
|
||||||
inf = self.temporary_file('.epub')
|
inf = self.temporary_file('.epub')
|
||||||
try:
|
try:
|
||||||
fr = zipfix.fixZip(path_to_ebook, inf.name)
|
fr = zipfix.fixZip(path_to_ebook, inf.name)
|
||||||
|
|||||||
@@ -13,9 +13,20 @@ _FILENAME_LEN_OFFSET = 26
|
|||||||
_EXTRA_LEN_OFFSET = 28
|
_EXTRA_LEN_OFFSET = 28
|
||||||
_FILENAME_OFFSET = 30
|
_FILENAME_OFFSET = 30
|
||||||
_MAX_SIZE = 64 * 1024
|
_MAX_SIZE = 64 * 1024
|
||||||
|
_MIMETYPE = 'application/epub+zip'
|
||||||
|
|
||||||
|
class ZipInfo(zipfile.ZipInfo):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
if 'compress_type' in kwargs:
|
||||||
|
compress_type = kwargs.pop('compress_type')
|
||||||
|
super(ZipInfo, self).__init__(*args, **kwargs)
|
||||||
|
self.compress_type = compress_type
|
||||||
|
|
||||||
class fixZip:
|
class fixZip:
|
||||||
def __init__(self, zinput, zoutput):
|
def __init__(self, zinput, zoutput):
|
||||||
|
self.ztype = 'zip'
|
||||||
|
if zinput.lower().find('.epub') >= 0 :
|
||||||
|
self.ztype = 'epub'
|
||||||
self.inzip = zipfile.ZipFile(zinput,'r')
|
self.inzip = zipfile.ZipFile(zinput,'r')
|
||||||
self.outzip = zipfile.ZipFile(zoutput,'w')
|
self.outzip = zipfile.ZipFile(zoutput,'w')
|
||||||
# open the input zip for reading only as a raw file
|
# open the input zip for reading only as a raw file
|
||||||
@@ -82,12 +93,18 @@ class fixZip:
|
|||||||
# and copy member over to output archive
|
# and copy member over to output archive
|
||||||
# if problems exist with local vs central filename, fix them
|
# if problems exist with local vs central filename, fix them
|
||||||
|
|
||||||
for i, zinfo in enumerate(self.inzip.infolist()):
|
# if epub write mimetype file first, with no compression
|
||||||
|
if self.ztype == 'epub':
|
||||||
|
nzinfo = ZipInfo('mimetype', compress_type=zipfile.ZIP_STORED)
|
||||||
|
self.outzip.writestr(nzinfo, _MIMETYPE)
|
||||||
|
|
||||||
|
# write the rest of the files
|
||||||
|
for zinfo in self.inzip.infolist():
|
||||||
|
if zinfo.filename != "mimetype" or self.ztype == '.zip':
|
||||||
data = None
|
data = None
|
||||||
nzinfo = zinfo
|
nzinfo = zinfo
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data = self.inzip.read(zinfo)
|
data = self.inzip.read(zinfo.filename)
|
||||||
except zipfile.BadZipfile or zipfile.error:
|
except zipfile.BadZipfile or zipfile.error:
|
||||||
local_name = self.getlocalname(zinfo)
|
local_name = self.getlocalname(zinfo)
|
||||||
data = self.getfiledata(zinfo)
|
data = self.getfiledata(zinfo)
|
||||||
@@ -111,14 +128,7 @@ def usage():
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def main(argv=sys.argv):
|
def repairBook(infile, outfile):
|
||||||
if len(argv)!=3:
|
|
||||||
usage()
|
|
||||||
return 1
|
|
||||||
infile = None
|
|
||||||
outfile = None
|
|
||||||
infile = argv[1]
|
|
||||||
outfile = argv[2]
|
|
||||||
if not os.path.exists(infile):
|
if not os.path.exists(infile):
|
||||||
print "Error: Input Zip File does not exist"
|
print "Error: Input Zip File does not exist"
|
||||||
return 1
|
return 1
|
||||||
@@ -130,6 +140,16 @@ def main(argv=sys.argv):
|
|||||||
print "Error Occurred ", e
|
print "Error Occurred ", e
|
||||||
return 2
|
return 2
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv=sys.argv):
|
||||||
|
if len(argv)!=3:
|
||||||
|
usage()
|
||||||
|
return 1
|
||||||
|
infile = argv[1]
|
||||||
|
outfile = argv[2]
|
||||||
|
return repairBook(infile, outfile)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__' :
|
if __name__ == '__main__' :
|
||||||
sys.exit(main())
|
sys.exit(main())
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -4,7 +4,7 @@
|
|||||||
# Released under the terms of the GNU General Public Licence, version 3 or
|
# Released under the terms of the GNU General Public Licence, version 3 or
|
||||||
# later. <http://www.gnu.org/licenses/>
|
# later. <http://www.gnu.org/licenses/>
|
||||||
#
|
#
|
||||||
# Requires Calibre version 0.6.44 or higher.
|
# Requires Calibre version 0.7.55 or higher.
|
||||||
#
|
#
|
||||||
# All credit given to I <3 Cabbages for the original standalone scripts.
|
# All credit given to I <3 Cabbages for the original standalone scripts.
|
||||||
# I had the much easier job of converting them to a Calibre plugin.
|
# I had the much easier job of converting them to a Calibre plugin.
|
||||||
@@ -46,8 +46,10 @@
|
|||||||
# 0.1.2 - Removed Carbon dependency for Mac users. Fixes an issue that was a
|
# 0.1.2 - Removed Carbon dependency for Mac users. Fixes an issue that was a
|
||||||
# result of Calibre changing to python 2.7.
|
# result of Calibre changing to python 2.7.
|
||||||
# 0.1.3 - bug fix for epubs with non-ascii chars in file names
|
# 0.1.3 - bug fix for epubs with non-ascii chars in file names
|
||||||
|
# 0.1.4 - default to try PyCrypto first on Windows
|
||||||
|
# 0.1.5 - update zipfix to handle out of position mimetypes
|
||||||
|
# 0.1.6 - update zipfix to handle completely missing mimetype files
|
||||||
|
# 0.1.7 - update to new calibre plugin interface
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Decrypt Adobe ADEPT-encrypted EPUB books.
|
Decrypt Adobe ADEPT-encrypted EPUB books.
|
||||||
@@ -285,7 +287,10 @@ def _load_crypto_pycrypto():
|
|||||||
|
|
||||||
def _load_crypto():
|
def _load_crypto():
|
||||||
_aes = _rsa = None
|
_aes = _rsa = None
|
||||||
for loader in (_load_crypto_libcrypto, _load_crypto_pycrypto):
|
cryptolist = (_load_crypto_libcrypto, _load_crypto_pycrypto)
|
||||||
|
if sys.platform.startswith('win'):
|
||||||
|
cryptolist = (_load_crypto_pycrypto, _load_crypto_libcrypto)
|
||||||
|
for loader in cryptolist:
|
||||||
try:
|
try:
|
||||||
_aes, _rsa = loader()
|
_aes, _rsa = loader()
|
||||||
break
|
break
|
||||||
@@ -361,6 +366,7 @@ def plugin_main(userkey, inpath, outpath):
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
from calibre.customize import FileTypePlugin
|
from calibre.customize import FileTypePlugin
|
||||||
|
from calibre.constants import iswindows, isosx
|
||||||
|
|
||||||
class IneptDeDRM(FileTypePlugin):
|
class IneptDeDRM(FileTypePlugin):
|
||||||
name = 'Inept Epub DeDRM'
|
name = 'Inept Epub DeDRM'
|
||||||
@@ -368,8 +374,8 @@ class IneptDeDRM(FileTypePlugin):
|
|||||||
Credit given to I <3 Cabbages for the original stand-alone scripts.'
|
Credit given to I <3 Cabbages for the original stand-alone scripts.'
|
||||||
supported_platforms = ['linux', 'osx', 'windows']
|
supported_platforms = ['linux', 'osx', 'windows']
|
||||||
author = 'DiapDealer'
|
author = 'DiapDealer'
|
||||||
version = (0, 1, 3)
|
version = (0, 1, 7)
|
||||||
minimum_calibre_version = (0, 6, 44) # Compiled python libraries cannot be imported in earlier versions.
|
minimum_calibre_version = (0, 7, 55) # Compiled python libraries cannot be imported in earlier versions.
|
||||||
file_types = set(['epub'])
|
file_types = set(['epub'])
|
||||||
on_import = True
|
on_import = True
|
||||||
priority = 100
|
priority = 100
|
||||||
@@ -378,10 +384,6 @@ class IneptDeDRM(FileTypePlugin):
|
|||||||
global AES
|
global AES
|
||||||
global RSA
|
global RSA
|
||||||
|
|
||||||
from calibre.gui2 import is_ok_to_use_qt
|
|
||||||
from PyQt4.Qt import QMessageBox
|
|
||||||
from calibre.constants import iswindows, isosx
|
|
||||||
|
|
||||||
AES, RSA = _load_crypto()
|
AES, RSA = _load_crypto()
|
||||||
|
|
||||||
if AES == None or RSA == None:
|
if AES == None or RSA == None:
|
||||||
@@ -414,7 +416,7 @@ class IneptDeDRM(FileTypePlugin):
|
|||||||
# Calibre's configuration directory for future use.
|
# Calibre's configuration directory for future use.
|
||||||
if iswindows or isosx:
|
if iswindows or isosx:
|
||||||
# ADE key retrieval script included in respective OS folder.
|
# ADE key retrieval script included in respective OS folder.
|
||||||
from ade_key import retrieve_key
|
from calibre_plugins.ineptepub.ade_key import retrieve_key
|
||||||
try:
|
try:
|
||||||
keydata = retrieve_key()
|
keydata = retrieve_key()
|
||||||
userkeys.append(keydata)
|
userkeys.append(keydata)
|
||||||
@@ -435,7 +437,7 @@ class IneptDeDRM(FileTypePlugin):
|
|||||||
for userkey in userkeys:
|
for userkey in userkeys:
|
||||||
# Create a TemporaryPersistent file to work with.
|
# Create a TemporaryPersistent file to work with.
|
||||||
# Check original epub archive for zip errors.
|
# Check original epub archive for zip errors.
|
||||||
import zipfix
|
from calibre_plugins.ineptepub import zipfix
|
||||||
inf = self.temporary_file('.epub')
|
inf = self.temporary_file('.epub')
|
||||||
try:
|
try:
|
||||||
fr = zipfix.fixZip(path_to_ebook, inf.name)
|
fr = zipfix.fixZip(path_to_ebook, inf.name)
|
||||||
@@ -79,7 +79,7 @@ if iswindows:
|
|||||||
|
|
||||||
def _load_crypto():
|
def _load_crypto():
|
||||||
AES = None
|
AES = None
|
||||||
for loader in (_load_crypto_libcrypto, _load_crypto_pycrypto):
|
for loader in (_load_crypto_pycrypto, _load_crypto_libcrypto):
|
||||||
try:
|
try:
|
||||||
AES = loader()
|
AES = loader()
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -13,9 +13,20 @@ _FILENAME_LEN_OFFSET = 26
|
|||||||
_EXTRA_LEN_OFFSET = 28
|
_EXTRA_LEN_OFFSET = 28
|
||||||
_FILENAME_OFFSET = 30
|
_FILENAME_OFFSET = 30
|
||||||
_MAX_SIZE = 64 * 1024
|
_MAX_SIZE = 64 * 1024
|
||||||
|
_MIMETYPE = 'application/epub+zip'
|
||||||
|
|
||||||
|
class ZipInfo(zipfile.ZipInfo):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
if 'compress_type' in kwargs:
|
||||||
|
compress_type = kwargs.pop('compress_type')
|
||||||
|
super(ZipInfo, self).__init__(*args, **kwargs)
|
||||||
|
self.compress_type = compress_type
|
||||||
|
|
||||||
class fixZip:
|
class fixZip:
|
||||||
def __init__(self, zinput, zoutput):
|
def __init__(self, zinput, zoutput):
|
||||||
|
self.ztype = 'zip'
|
||||||
|
if zinput.lower().find('.epub') >= 0 :
|
||||||
|
self.ztype = 'epub'
|
||||||
self.inzip = zipfile.ZipFile(zinput,'r')
|
self.inzip = zipfile.ZipFile(zinput,'r')
|
||||||
self.outzip = zipfile.ZipFile(zoutput,'w')
|
self.outzip = zipfile.ZipFile(zoutput,'w')
|
||||||
# open the input zip for reading only as a raw file
|
# open the input zip for reading only as a raw file
|
||||||
@@ -82,12 +93,18 @@ class fixZip:
|
|||||||
# and copy member over to output archive
|
# and copy member over to output archive
|
||||||
# if problems exist with local vs central filename, fix them
|
# if problems exist with local vs central filename, fix them
|
||||||
|
|
||||||
for i, zinfo in enumerate(self.inzip.infolist()):
|
# if epub write mimetype file first, with no compression
|
||||||
|
if self.ztype == 'epub':
|
||||||
|
nzinfo = ZipInfo('mimetype', compress_type=zipfile.ZIP_STORED)
|
||||||
|
self.outzip.writestr(nzinfo, _MIMETYPE)
|
||||||
|
|
||||||
|
# write the rest of the files
|
||||||
|
for zinfo in self.inzip.infolist():
|
||||||
|
if zinfo.filename != "mimetype" or self.ztype == '.zip':
|
||||||
data = None
|
data = None
|
||||||
nzinfo = zinfo
|
nzinfo = zinfo
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data = self.inzip.read(zinfo)
|
data = self.inzip.read(zinfo.filename)
|
||||||
except zipfile.BadZipfile or zipfile.error:
|
except zipfile.BadZipfile or zipfile.error:
|
||||||
local_name = self.getlocalname(zinfo)
|
local_name = self.getlocalname(zinfo)
|
||||||
data = self.getfiledata(zinfo)
|
data = self.getfiledata(zinfo)
|
||||||
@@ -111,14 +128,7 @@ def usage():
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def main(argv=sys.argv):
|
def repairBook(infile, outfile):
|
||||||
if len(argv)!=3:
|
|
||||||
usage()
|
|
||||||
return 1
|
|
||||||
infile = None
|
|
||||||
outfile = None
|
|
||||||
infile = argv[1]
|
|
||||||
outfile = argv[2]
|
|
||||||
if not os.path.exists(infile):
|
if not os.path.exists(infile):
|
||||||
print "Error: Input Zip File does not exist"
|
print "Error: Input Zip File does not exist"
|
||||||
return 1
|
return 1
|
||||||
@@ -130,6 +140,16 @@ def main(argv=sys.argv):
|
|||||||
print "Error Occurred ", e
|
print "Error Occurred ", e
|
||||||
return 2
|
return 2
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv=sys.argv):
|
||||||
|
if len(argv)!=3:
|
||||||
|
usage()
|
||||||
|
return 1
|
||||||
|
infile = argv[1]
|
||||||
|
outfile = argv[2]
|
||||||
|
return repairBook(infile, outfile)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__' :
|
if __name__ == '__main__' :
|
||||||
sys.exit(main())
|
sys.exit(main())
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -1,6 +1,6 @@
|
|||||||
#! /usr/bin/env python
|
#! /usr/bin/env python
|
||||||
|
|
||||||
# ineptpdf_plugin.py
|
# ineptpdf plugin __init__.py
|
||||||
# Released under the terms of the GNU General Public Licence, version 3 or
|
# Released under the terms of the GNU General Public Licence, version 3 or
|
||||||
# later. <http://www.gnu.org/licenses/>
|
# later. <http://www.gnu.org/licenses/>
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
# be able to read OUR books on whatever device we want and to keep
|
# be able to read OUR books on whatever device we want and to keep
|
||||||
# readable for a long, long time
|
# readable for a long, long time
|
||||||
|
|
||||||
# Requires Calibre version 0.6.44 or higher.
|
# Requires Calibre version 0.7.55 or higher.
|
||||||
#
|
#
|
||||||
# All credit given to I <3 Cabbages for the original standalone scripts.
|
# All credit given to I <3 Cabbages for the original standalone scripts.
|
||||||
# I had the much easier job of converting them to a Calibre plugin.
|
# I had the much easier job of converting them to a Calibre plugin.
|
||||||
@@ -48,6 +48,10 @@
|
|||||||
#
|
#
|
||||||
# Revision history:
|
# Revision history:
|
||||||
# 0.1 - Initial release
|
# 0.1 - Initial release
|
||||||
|
# 0.1.1 - back port ineptpdf 8.4.X support for increased number of encryption methods
|
||||||
|
# 0.1.2 - back port ineptpdf 8.4.X bug fixes
|
||||||
|
# 0.1.3 - add in fix for improper rejection of session bookkeys with len(bookkey) = length + 1
|
||||||
|
# 0.1.4 - update to the new calibre plugin interface
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Decrypts Adobe ADEPT-encrypted PDF files.
|
Decrypts Adobe ADEPT-encrypted PDF files.
|
||||||
@@ -171,6 +175,7 @@ def _load_crypto_libcrypto():
|
|||||||
return out.raw
|
return out.raw
|
||||||
|
|
||||||
class AES(object):
|
class AES(object):
|
||||||
|
MODE_CBC = 0
|
||||||
@classmethod
|
@classmethod
|
||||||
def new(cls, userkey, mode, iv):
|
def new(cls, userkey, mode, iv):
|
||||||
self = AES()
|
self = AES()
|
||||||
@@ -336,7 +341,10 @@ def _load_crypto_pycrypto():
|
|||||||
|
|
||||||
def _load_crypto():
|
def _load_crypto():
|
||||||
ARC4 = RSA = AES = None
|
ARC4 = RSA = AES = None
|
||||||
for loader in (_load_crypto_libcrypto, _load_crypto_pycrypto):
|
cryptolist = (_load_crypto_libcrypto, _load_crypto_pycrypto)
|
||||||
|
if sys.platform.startswith('win'):
|
||||||
|
cryptolist = (_load_crypto_pycrypto, _load_crypto_libcrypto)
|
||||||
|
for loader in cryptolist:
|
||||||
try:
|
try:
|
||||||
ARC4, RSA, AES = loader()
|
ARC4, RSA, AES = loader()
|
||||||
break
|
break
|
||||||
@@ -1541,16 +1549,30 @@ class PDFDocument(object):
|
|||||||
bookkey = bookkey[index:]
|
bookkey = bookkey[index:]
|
||||||
ebx_V = int_value(param.get('V', 4))
|
ebx_V = int_value(param.get('V', 4))
|
||||||
ebx_type = int_value(param.get('EBX_ENCRYPTIONTYPE', 6))
|
ebx_type = int_value(param.get('EBX_ENCRYPTIONTYPE', 6))
|
||||||
# added because of the booktype / decryption book session key error
|
# added because of improper booktype / decryption book session key errors
|
||||||
|
if length > 0:
|
||||||
|
if len(bookkey) == length:
|
||||||
if ebx_V == 3:
|
if ebx_V == 3:
|
||||||
V = 3
|
V = 3
|
||||||
elif ebx_V < 4 or ebx_type < 6:
|
else:
|
||||||
|
V = 2
|
||||||
|
elif len(bookkey) == length + 1:
|
||||||
V = ord(bookkey[0])
|
V = ord(bookkey[0])
|
||||||
bookkey = bookkey[1:]
|
bookkey = bookkey[1:]
|
||||||
|
else:
|
||||||
|
print "ebx_V is %d and ebx_type is %d" % (ebx_V, ebx_type)
|
||||||
|
print "length is %d and len(bookkey) is %d" % (length, len(bookkey))
|
||||||
|
print "bookkey[0] is %d" % ord(bookkey[0])
|
||||||
|
raise ADEPTError('error decrypting book session key - mismatched length')
|
||||||
|
else:
|
||||||
|
# proper length unknown try with whatever you have
|
||||||
|
print "ebx_V is %d and ebx_type is %d" % (ebx_V, ebx_type)
|
||||||
|
print "length is %d and len(bookkey) is %d" % (length, len(bookkey))
|
||||||
|
print "bookkey[0] is %d" % ord(bookkey[0])
|
||||||
|
if ebx_V == 3:
|
||||||
|
V = 3
|
||||||
else:
|
else:
|
||||||
V = 2
|
V = 2
|
||||||
if length and len(bookkey) != length:
|
|
||||||
raise ADEPTError('error decrypting book session key')
|
|
||||||
self.decrypt_key = bookkey
|
self.decrypt_key = bookkey
|
||||||
self.genkey = self.genkey_v3 if V == 3 else self.genkey_v2
|
self.genkey = self.genkey_v3 if V == 3 else self.genkey_v2
|
||||||
self.decipher = self.decrypt_rc4
|
self.decipher = self.decrypt_rc4
|
||||||
@@ -2106,6 +2128,7 @@ def plugin_main(keypath, inpath, outpath):
|
|||||||
|
|
||||||
|
|
||||||
from calibre.customize import FileTypePlugin
|
from calibre.customize import FileTypePlugin
|
||||||
|
from calibre.constants import iswindows, isosx
|
||||||
|
|
||||||
class IneptPDFDeDRM(FileTypePlugin):
|
class IneptPDFDeDRM(FileTypePlugin):
|
||||||
name = 'Inept PDF DeDRM'
|
name = 'Inept PDF DeDRM'
|
||||||
@@ -2113,17 +2136,14 @@ class IneptPDFDeDRM(FileTypePlugin):
|
|||||||
Credit given to I <3 Cabbages for the original stand-alone scripts.'
|
Credit given to I <3 Cabbages for the original stand-alone scripts.'
|
||||||
supported_platforms = ['linux', 'osx', 'windows']
|
supported_platforms = ['linux', 'osx', 'windows']
|
||||||
author = 'DiapDealer'
|
author = 'DiapDealer'
|
||||||
version = (0, 1, 1)
|
version = (0, 1, 4)
|
||||||
minimum_calibre_version = (0, 6, 44) # Compiled python libraries cannot be imported in earlier versions.
|
minimum_calibre_version = (0, 7, 55) # for the new plugin interface
|
||||||
file_types = set(['pdf'])
|
file_types = set(['pdf'])
|
||||||
on_import = True
|
on_import = True
|
||||||
|
|
||||||
def run(self, path_to_ebook):
|
def run(self, path_to_ebook):
|
||||||
global ARC4, RSA, AES
|
global ARC4, RSA, AES
|
||||||
|
|
||||||
from calibre.gui2 import is_ok_to_use_qt
|
|
||||||
from PyQt4.Qt import QMessageBox
|
|
||||||
from calibre.constants import iswindows, isosx
|
|
||||||
|
|
||||||
ARC4, RSA, AES = _load_crypto()
|
ARC4, RSA, AES = _load_crypto()
|
||||||
|
|
||||||
@@ -2157,7 +2177,7 @@ class IneptPDFDeDRM(FileTypePlugin):
|
|||||||
# Calibre's configuration directory for future use.
|
# Calibre's configuration directory for future use.
|
||||||
if iswindows or isosx:
|
if iswindows or isosx:
|
||||||
# ADE key retrieval script.
|
# ADE key retrieval script.
|
||||||
from ade_key import retrieve_key
|
from calibre_plugins.ineptpdf.ade_key import retrieve_key
|
||||||
try:
|
try:
|
||||||
keydata = retrieve_key()
|
keydata = retrieve_key()
|
||||||
userkeys.append(keydata)
|
userkeys.append(keydata)
|
||||||
@@ -79,7 +79,7 @@ if iswindows:
|
|||||||
|
|
||||||
def _load_crypto():
|
def _load_crypto():
|
||||||
AES = None
|
AES = None
|
||||||
for loader in (_load_crypto_libcrypto, _load_crypto_pycrypto):
|
for loader in (_load_crypto_pycrypto, _load_crypto_libcrypto):
|
||||||
try:
|
try:
|
||||||
AES = loader()
|
AES = loader()
|
||||||
break
|
break
|
||||||
|
|||||||
Binary file not shown.
@@ -1,333 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
|
|
||||||
# engine to remove drm from Kindle for Mac and Kindle for PC books
|
|
||||||
# for personal use for archiving and converting your ebooks
|
|
||||||
|
|
||||||
# PLEASE DO NOT PIRATE EBOOKS!
|
|
||||||
|
|
||||||
# We want all authors and publishers, and eBook stores to live
|
|
||||||
# long and prosperous lives but at the same time we just want to
|
|
||||||
# be able to read OUR books on whatever device we want and to keep
|
|
||||||
# readable for a long, long time
|
|
||||||
|
|
||||||
# This borrows very heavily from works by CMBDTC, IHeartCabbages, skindle,
|
|
||||||
# unswindle, DarkReverser, ApprenticeAlf, DiapDealer, some_updates
|
|
||||||
# and many many others
|
|
||||||
|
|
||||||
# It can run standalone to convert K4M/K4PC/Mobi files, or it can be installed as a
|
|
||||||
# plugin for Calibre (http://calibre-ebook.com/about) so that importing
|
|
||||||
# K4 or Mobi with DRM is no londer a multi-step process.
|
|
||||||
#
|
|
||||||
# ***NOTE*** If you are using this script as a calibre plugin for a K4M or K4PC ebook
|
|
||||||
# then calibre must be installed on the same machine and in the same account as K4PC or K4M
|
|
||||||
# for the plugin version to function properly.
|
|
||||||
#
|
|
||||||
# To create a Calibre plugin, rename this file so that the filename
|
|
||||||
# ends in '_plugin.py', put it into a ZIP file with all its supporting python routines
|
|
||||||
# and import that ZIP into Calibre using its plugin configuration GUI.
|
|
||||||
|
|
||||||
from __future__ import with_statement
|
|
||||||
|
|
||||||
__version__ = '1.4'
|
|
||||||
|
|
||||||
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 os, csv, getopt
|
|
||||||
import binascii
|
|
||||||
import zlib
|
|
||||||
import re
|
|
||||||
import zlib, zipfile, tempfile, shutil
|
|
||||||
from struct import pack, unpack, unpack_from
|
|
||||||
|
|
||||||
class DrmException(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
if 'calibre' in sys.modules:
|
|
||||||
inCalibre = True
|
|
||||||
else:
|
|
||||||
inCalibre = False
|
|
||||||
|
|
||||||
def zipUpDir(myzip, tempdir,localname):
|
|
||||||
currentdir = tempdir
|
|
||||||
if localname != "":
|
|
||||||
currentdir = os.path.join(currentdir,localname)
|
|
||||||
list = os.listdir(currentdir)
|
|
||||||
for file in list:
|
|
||||||
afilename = file
|
|
||||||
localfilePath = os.path.join(localname, afilename)
|
|
||||||
realfilePath = os.path.join(currentdir,file)
|
|
||||||
if os.path.isfile(realfilePath):
|
|
||||||
myzip.write(realfilePath, localfilePath)
|
|
||||||
elif os.path.isdir(realfilePath):
|
|
||||||
zipUpDir(myzip, tempdir, localfilePath)
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
#
|
|
||||||
# Main
|
|
||||||
#
|
|
||||||
def main(argv=sys.argv):
|
|
||||||
import mobidedrm
|
|
||||||
import topazextract
|
|
||||||
import kgenpids
|
|
||||||
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 ' '
|
|
||||||
try:
|
|
||||||
opts, args = getopt.getopt(sys.argv[1:], "k:p:s:")
|
|
||||||
except getopt.GetoptError, err:
|
|
||||||
print str(err)
|
|
||||||
usage(progname)
|
|
||||||
sys.exit(2)
|
|
||||||
if len(args)<2:
|
|
||||||
usage(progname)
|
|
||||||
sys.exit(2)
|
|
||||||
|
|
||||||
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 = a.split(',')
|
|
||||||
|
|
||||||
# try with built in Kindle Info files
|
|
||||||
k4 = True
|
|
||||||
|
|
||||||
infile = args[0]
|
|
||||||
outdir = args[1]
|
|
||||||
|
|
||||||
# handle the obvious cases at the beginning
|
|
||||||
if not os.path.isfile(infile):
|
|
||||||
print "Error: Input file does not exist"
|
|
||||||
return 1
|
|
||||||
|
|
||||||
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:
|
|
||||||
tempdir = tempfile.mkdtemp()
|
|
||||||
mb = topazextract.TopazBook(infile, tempdir)
|
|
||||||
|
|
||||||
title = mb.getBookTitle()
|
|
||||||
print "Processing Book: ", title
|
|
||||||
|
|
||||||
# build pid list
|
|
||||||
md1, md2 = mb.getPIDMetaInfo()
|
|
||||||
pidlst = kgenpids.getPidList(md1, md2, k4, pids, serials, kInfoFiles)
|
|
||||||
|
|
||||||
try:
|
|
||||||
if mobi:
|
|
||||||
unlocked_file = mb.processBook(pidlst)
|
|
||||||
else:
|
|
||||||
mb.processBook(pidlst)
|
|
||||||
|
|
||||||
except mobidedrm.DrmException, e:
|
|
||||||
print " ... not suceessful " + str(e) + "\n"
|
|
||||||
return 1
|
|
||||||
except topazextract.TpzDRMError, e:
|
|
||||||
print str(e)
|
|
||||||
print " Creating DeBug Full Zip Archive of Book"
|
|
||||||
zipname = os.path.join(outdir, bookname + '_debug' + '.zip')
|
|
||||||
myzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
|
||||||
zipUpDir(myzip, tempdir, '')
|
|
||||||
myzip.close()
|
|
||||||
return 1
|
|
||||||
|
|
||||||
if mobi:
|
|
||||||
outfile = os.path.join(outdir,bookname + '_nodrm' + '.azw')
|
|
||||||
file(outfile, 'wb').write(unlocked_file)
|
|
||||||
return 0
|
|
||||||
|
|
||||||
# topaz: build up zip archives of results
|
|
||||||
print " Creating HTML ZIP Archive"
|
|
||||||
zipname = os.path.join(outdir, bookname + '_nodrm' + '.zip')
|
|
||||||
myzip1 = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
|
||||||
myzip1.write(os.path.join(tempdir,'book.html'),'book.html')
|
|
||||||
myzip1.write(os.path.join(tempdir,'book.opf'),'book.opf')
|
|
||||||
if os.path.isfile(os.path.join(tempdir,'cover.jpg')):
|
|
||||||
myzip1.write(os.path.join(tempdir,'cover.jpg'),'cover.jpg')
|
|
||||||
myzip1.write(os.path.join(tempdir,'style.css'),'style.css')
|
|
||||||
zipUpDir(myzip1, tempdir, 'img')
|
|
||||||
myzip1.close()
|
|
||||||
|
|
||||||
print " Creating SVG ZIP Archive"
|
|
||||||
zipname = os.path.join(outdir, bookname + '_SVG' + '.zip')
|
|
||||||
myzip2 = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
|
||||||
myzip2.write(os.path.join(tempdir,'index_svg.xhtml'),'index_svg.xhtml')
|
|
||||||
zipUpDir(myzip2, tempdir, 'svg')
|
|
||||||
zipUpDir(myzip2, tempdir, 'img')
|
|
||||||
myzip2.close()
|
|
||||||
|
|
||||||
print " Creating XML ZIP Archive"
|
|
||||||
zipname = os.path.join(outdir, bookname + '_XML' + '.zip')
|
|
||||||
myzip3 = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
|
||||||
targetdir = os.path.join(tempdir,'xml')
|
|
||||||
zipUpDir(myzip3, targetdir, '')
|
|
||||||
zipUpDir(myzip3, tempdir, 'img')
|
|
||||||
myzip3.close()
|
|
||||||
|
|
||||||
shutil.rmtree(tempdir)
|
|
||||||
return 0
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
sys.stdout=Unbuffered(sys.stdout)
|
|
||||||
sys.exit(main())
|
|
||||||
|
|
||||||
if not __name__ == "__main__" and inCalibre:
|
|
||||||
from calibre.customize import FileTypePlugin
|
|
||||||
|
|
||||||
class K4DeDRM(FileTypePlugin):
|
|
||||||
name = 'K4PC, K4Mac, Mobi DeDRM' # Name of the plugin
|
|
||||||
description = 'Removes DRM from K4PC and Mac, Kindle Mobi and Topaz files. \
|
|
||||||
Provided by the work of many including DiapDealer, SomeUpdates, IHeartCabbages, CMBDTC, Skindle, DarkReverser, ApprenticeAlf, etc.'
|
|
||||||
supported_platforms = ['osx', 'windows', 'linux'] # Platforms this plugin will run on
|
|
||||||
author = 'DiapDealer, SomeUpdates' # The author of this plugin
|
|
||||||
version = (0, 1, 7) # The version number of this plugin
|
|
||||||
file_types = set(['prc','mobi','azw','azw1','tpz']) # The file types that this plugin will be applied to
|
|
||||||
on_import = True # Run this plugin during the import
|
|
||||||
priority = 210 # run this plugin before mobidedrm, k4pcdedrm, k4dedrm
|
|
||||||
|
|
||||||
def run(self, path_to_ebook):
|
|
||||||
from calibre.gui2 import is_ok_to_use_qt
|
|
||||||
from PyQt4.Qt import QMessageBox
|
|
||||||
from calibre.ptempfile import PersistentTemporaryDirectory
|
|
||||||
|
|
||||||
import kgenpids
|
|
||||||
import zlib
|
|
||||||
import zipfile
|
|
||||||
import topazextract
|
|
||||||
import mobidedrm
|
|
||||||
|
|
||||||
k4 = True
|
|
||||||
pids = []
|
|
||||||
serials = []
|
|
||||||
kInfoFiles = []
|
|
||||||
|
|
||||||
# Get supplied list of PIDs to try from plugin customization.
|
|
||||||
customvalues = self.site_customization.split(',')
|
|
||||||
for customvalue in customvalues:
|
|
||||||
customvalue = str(customvalue)
|
|
||||||
customvalue = customvalue.strip()
|
|
||||||
if len(customvalue) == 10 or len(customvalue) == 8:
|
|
||||||
pids.append(customvalue)
|
|
||||||
else :
|
|
||||||
if len(customvalue) == 16 and customvalue[0] == 'B':
|
|
||||||
serials.append(customvalue)
|
|
||||||
else:
|
|
||||||
print "%s is not a valid Kindle serial number or PID." % str(customvalue)
|
|
||||||
|
|
||||||
# Load any kindle info files (*.info) included Calibre's config directory.
|
|
||||||
try:
|
|
||||||
# Find Calibre's configuration directory.
|
|
||||||
confpath = os.path.split(os.path.split(self.plugin_path)[0])[0]
|
|
||||||
print 'K4MobiDeDRM: Calibre configuration directory = %s' % confpath
|
|
||||||
files = os.listdir(confpath)
|
|
||||||
filefilter = re.compile("\.info$", re.IGNORECASE)
|
|
||||||
files = filter(filefilter.search, files)
|
|
||||||
|
|
||||||
if files:
|
|
||||||
for filename in files:
|
|
||||||
fpath = os.path.join(confpath, filename)
|
|
||||||
kInfoFiles.append(fpath)
|
|
||||||
print 'K4MobiDeDRM: Kindle info file %s found in config folder.' % filename
|
|
||||||
except IOError:
|
|
||||||
print 'K4MobiDeDRM: Error reading kindle info files from config directory.'
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
mobi = True
|
|
||||||
magic3 = file(path_to_ebook,'rb').read(3)
|
|
||||||
if magic3 == 'TPZ':
|
|
||||||
mobi = False
|
|
||||||
|
|
||||||
bookname = os.path.splitext(os.path.basename(path_to_ebook))[0]
|
|
||||||
|
|
||||||
if mobi:
|
|
||||||
mb = mobidedrm.MobiBook(path_to_ebook)
|
|
||||||
else:
|
|
||||||
tempdir = PersistentTemporaryDirectory()
|
|
||||||
mb = topazextract.TopazBook(path_to_ebook, tempdir)
|
|
||||||
|
|
||||||
title = mb.getBookTitle()
|
|
||||||
md1, md2 = mb.getPIDMetaInfo()
|
|
||||||
pidlst = kgenpids.getPidList(md1, md2, k4, pids, serials, kInfoFiles)
|
|
||||||
|
|
||||||
try:
|
|
||||||
if mobi:
|
|
||||||
unlocked_file = mb.processBook(pidlst)
|
|
||||||
else:
|
|
||||||
mb.processBook(pidlst)
|
|
||||||
|
|
||||||
except mobidedrm.DrmException:
|
|
||||||
#if you reached here then no luck raise and exception
|
|
||||||
if is_ok_to_use_qt():
|
|
||||||
d = QMessageBox(QMessageBox.Warning, "K4MobiDeDRM Plugin", "Error decoding: %s\n" % path_to_ebook)
|
|
||||||
d.show()
|
|
||||||
d.raise_()
|
|
||||||
d.exec_()
|
|
||||||
raise Exception("K4MobiDeDRM plugin could not decode the file")
|
|
||||||
return ""
|
|
||||||
except topazextract.TpzDRMError:
|
|
||||||
#if you reached here then no luck raise and exception
|
|
||||||
if is_ok_to_use_qt():
|
|
||||||
d = QMessageBox(QMessageBox.Warning, "K4MobiDeDRM Plugin", "Error decoding: %s\n" % path_to_ebook)
|
|
||||||
d.show()
|
|
||||||
d.raise_()
|
|
||||||
d.exec_()
|
|
||||||
raise Exception("K4MobiDeDRM plugin could not decode the file")
|
|
||||||
return ""
|
|
||||||
|
|
||||||
print "Success!"
|
|
||||||
if mobi:
|
|
||||||
of = self.temporary_file(bookname+'.mobi')
|
|
||||||
of.write(unlocked_file)
|
|
||||||
of.close()
|
|
||||||
return of.name
|
|
||||||
|
|
||||||
# topaz: build up zip archives of results
|
|
||||||
print " Creating HTML ZIP Archive"
|
|
||||||
of = self.temporary_file(bookname + '.zip')
|
|
||||||
myzip = zipfile.ZipFile(of.name,'w',zipfile.ZIP_DEFLATED, False)
|
|
||||||
myzip.write(os.path.join(tempdir,'book.html'),'book.html')
|
|
||||||
myzip.write(os.path.join(tempdir,'book.opf'),'book.opf')
|
|
||||||
if os.path.isfile(os.path.join(tempdir,'cover.jpg')):
|
|
||||||
myzip.write(os.path.join(tempdir,'cover.jpg'),'cover.jpg')
|
|
||||||
myzip.write(os.path.join(tempdir,'style.css'),'style.css')
|
|
||||||
zipUpDir(myzip, tempdir, 'img')
|
|
||||||
myzip.close()
|
|
||||||
return of.name
|
|
||||||
|
|
||||||
def customization_help(self, gui=False):
|
|
||||||
return 'Enter 10 character PIDs and/or Kindle serial numbers, separated by commas.'
|
|
||||||
@@ -1,12 +1,15 @@
|
|||||||
# standlone set of Mac OSX specific routines needed for K4DeDRM
|
# standlone set of Mac OSX specific routines needed for KindleBooks
|
||||||
|
|
||||||
from __future__ import with_statement
|
from __future__ import with_statement
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
|
import os.path
|
||||||
|
|
||||||
import subprocess
|
import subprocess
|
||||||
|
from struct import pack, unpack, unpack_from
|
||||||
|
|
||||||
|
class DrmException(Exception):
|
||||||
class K4MDrmException(Exception):
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@@ -18,7 +21,7 @@ def _load_crypto_libcrypto():
|
|||||||
|
|
||||||
libcrypto = find_library('crypto')
|
libcrypto = find_library('crypto')
|
||||||
if libcrypto is None:
|
if libcrypto is None:
|
||||||
raise K4MDrmException('libcrypto not found')
|
raise DrmException('libcrypto not found')
|
||||||
libcrypto = CDLL(libcrypto)
|
libcrypto = CDLL(libcrypto)
|
||||||
|
|
||||||
AES_MAXNR = 14
|
AES_MAXNR = 14
|
||||||
@@ -51,27 +54,24 @@ def _load_crypto_libcrypto():
|
|||||||
def set_decrypt_key(self, userkey, iv):
|
def set_decrypt_key(self, userkey, iv):
|
||||||
self._blocksize = len(userkey)
|
self._blocksize = len(userkey)
|
||||||
if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) :
|
if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) :
|
||||||
raise K4MDrmException('AES improper key used')
|
raise DrmException('AES improper key used')
|
||||||
return
|
return
|
||||||
keyctx = self._keyctx = AES_KEY()
|
keyctx = self._keyctx = AES_KEY()
|
||||||
self.iv = iv
|
self.iv = iv
|
||||||
rv = AES_set_decrypt_key(userkey, len(userkey) * 8, keyctx)
|
rv = AES_set_decrypt_key(userkey, len(userkey) * 8, keyctx)
|
||||||
if rv < 0:
|
if rv < 0:
|
||||||
raise K4MDrmException('Failed to initialize AES key')
|
raise DrmException('Failed to initialize AES key')
|
||||||
|
|
||||||
def decrypt(self, data):
|
def decrypt(self, data):
|
||||||
out = create_string_buffer(len(data))
|
out = create_string_buffer(len(data))
|
||||||
rv = AES_cbc_encrypt(data, out, len(data), self._keyctx, self.iv, 0)
|
rv = AES_cbc_encrypt(data, out, len(data), self._keyctx, self.iv, 0)
|
||||||
if rv == 0:
|
if rv == 0:
|
||||||
raise K4MDrmException('AES decryption failed')
|
raise DrmException('AES decryption failed')
|
||||||
return out.raw
|
return out.raw
|
||||||
|
|
||||||
def keyivgen(self, passwd):
|
def keyivgen(self, passwd, salt, iter, keylen):
|
||||||
salt = '16743'
|
saltlen = len(salt)
|
||||||
saltlen = 5
|
|
||||||
passlen = len(passwd)
|
passlen = len(passwd)
|
||||||
iter = 0x3e8
|
|
||||||
keylen = 80
|
|
||||||
out = create_string_buffer(keylen)
|
out = create_string_buffer(keylen)
|
||||||
rv = PKCS5_PBKDF2_HMAC_SHA1(passwd, passlen, salt, saltlen, iter, keylen, out)
|
rv = PKCS5_PBKDF2_HMAC_SHA1(passwd, passlen, salt, saltlen, iter, keylen, out)
|
||||||
return out.raw
|
return out.raw
|
||||||
@@ -81,7 +81,7 @@ def _load_crypto():
|
|||||||
LibCrypto = None
|
LibCrypto = None
|
||||||
try:
|
try:
|
||||||
LibCrypto = _load_crypto_libcrypto()
|
LibCrypto = _load_crypto_libcrypto()
|
||||||
except (ImportError, K4MDrmException):
|
except (ImportError, DrmException):
|
||||||
pass
|
pass
|
||||||
return LibCrypto
|
return LibCrypto
|
||||||
|
|
||||||
@@ -91,13 +91,79 @@ LibCrypto = _load_crypto()
|
|||||||
# Utility Routines
|
# Utility Routines
|
||||||
#
|
#
|
||||||
|
|
||||||
|
# crypto digestroutines
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
def MD5(message):
|
||||||
|
ctx = hashlib.md5()
|
||||||
|
ctx.update(message)
|
||||||
|
return ctx.digest()
|
||||||
|
|
||||||
|
def SHA1(message):
|
||||||
|
ctx = hashlib.sha1()
|
||||||
|
ctx.update(message)
|
||||||
|
return ctx.digest()
|
||||||
|
|
||||||
|
def SHA256(message):
|
||||||
|
ctx = hashlib.sha256()
|
||||||
|
ctx.update(message)
|
||||||
|
return ctx.digest()
|
||||||
|
|
||||||
# Various character maps used to decrypt books. Probably supposed to act as obfuscation
|
# Various character maps used to decrypt books. Probably supposed to act as obfuscation
|
||||||
charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M"
|
charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M"
|
||||||
charMap2 = "ZB0bYyc1xDdW2wEV3Ff7KkPpL8UuGA4gz-Tme9Nn_tHh5SvXCsIiR6rJjQaqlOoM"
|
charMap2 = "ZB0bYyc1xDdW2wEV3Ff7KkPpL8UuGA4gz-Tme9Nn_tHh5SvXCsIiR6rJjQaqlOoM"
|
||||||
charMap3 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
|
||||||
charMap4 = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789"
|
|
||||||
|
|
||||||
|
# For kinf approach of K4PC/K4Mac
|
||||||
|
# On K4PC charMap5 = "AzB0bYyCeVvaZ3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_c1XxDdW2wE"
|
||||||
|
# For Mac they seem to re-use charMap2 here
|
||||||
|
charMap5 = charMap2
|
||||||
|
|
||||||
|
def encode(data, map):
|
||||||
|
result = ""
|
||||||
|
for char in data:
|
||||||
|
value = ord(char)
|
||||||
|
Q = (value ^ 0x80) // len(map)
|
||||||
|
R = value % len(map)
|
||||||
|
result += map[Q]
|
||||||
|
result += map[R]
|
||||||
|
return result
|
||||||
|
|
||||||
|
# Hash the bytes in data and then encode the digest with the characters in map
|
||||||
|
def encodeHash(data,map):
|
||||||
|
return encode(MD5(data),map)
|
||||||
|
|
||||||
|
# Decode the string in data with the characters in map. Returns the decoded bytes
|
||||||
|
def decode(data,map):
|
||||||
|
result = ""
|
||||||
|
for i in range (0,len(data)-1,2):
|
||||||
|
high = map.find(data[i])
|
||||||
|
low = map.find(data[i+1])
|
||||||
|
if (high == -1) or (low == -1) :
|
||||||
|
break
|
||||||
|
value = (((high * len(map)) ^ 0x80) & 0xFF) + low
|
||||||
|
result += pack("B",value)
|
||||||
|
return result
|
||||||
|
|
||||||
|
# For .kinf approach of K4PC and now K4Mac
|
||||||
|
# generate table of prime number less than or equal to int n
|
||||||
|
def primes(n):
|
||||||
|
if n==2: return [2]
|
||||||
|
elif n<2: return []
|
||||||
|
s=range(3,n+1,2)
|
||||||
|
mroot = n ** 0.5
|
||||||
|
half=(n+1)/2-1
|
||||||
|
i=0
|
||||||
|
m=3
|
||||||
|
while m <= mroot:
|
||||||
|
if s[i]:
|
||||||
|
j=(m*m-3)/2
|
||||||
|
s[j]=0
|
||||||
|
while j<half:
|
||||||
|
s[j]=0
|
||||||
|
j+=m
|
||||||
|
i=i+1
|
||||||
|
m=2*i+3
|
||||||
|
return [2]+[x for x in s if x]
|
||||||
|
|
||||||
|
|
||||||
# uses a sub process to get the Hard Drive Serial Number using ioreg
|
# uses a sub process to get the Hard Drive Serial Number using ioreg
|
||||||
@@ -129,48 +195,223 @@ def GetVolumeSerialNumber():
|
|||||||
foundIt = True
|
foundIt = True
|
||||||
break
|
break
|
||||||
if not foundIt:
|
if not foundIt:
|
||||||
sernum = '9999999999'
|
sernum = ''
|
||||||
return sernum
|
return sernum
|
||||||
|
|
||||||
|
def GetUserHomeAppSupKindleDirParitionName():
|
||||||
|
home = os.getenv('HOME')
|
||||||
|
dpath = home + '/Library/Application Support/Kindle'
|
||||||
|
cmdline = '/sbin/mount'
|
||||||
|
cmdline = cmdline.encode(sys.getfilesystemencoding())
|
||||||
|
p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
|
||||||
|
out1, out2 = p.communicate()
|
||||||
|
reslst = out1.split('\n')
|
||||||
|
cnt = len(reslst)
|
||||||
|
disk = ''
|
||||||
|
foundIt = False
|
||||||
|
for j in xrange(cnt):
|
||||||
|
resline = reslst[j]
|
||||||
|
if resline.startswith('/dev'):
|
||||||
|
(devpart, mpath) = resline.split(' on ')
|
||||||
|
dpart = devpart[5:]
|
||||||
|
pp = mpath.find('(')
|
||||||
|
if pp >= 0:
|
||||||
|
mpath = mpath[:pp-1]
|
||||||
|
if dpath.startswith(mpath):
|
||||||
|
disk = dpart
|
||||||
|
return disk
|
||||||
|
|
||||||
|
# uses a sub process to get the UUID of the specified disk partition using ioreg
|
||||||
|
def GetDiskPartitionUUID(diskpart):
|
||||||
|
uuidnum = os.getenv('MYUUIDNUMBER')
|
||||||
|
if uuidnum != None:
|
||||||
|
return uuidnum
|
||||||
|
cmdline = '/usr/sbin/ioreg -l -S -w 0 -r -c AppleAHCIDiskDriver'
|
||||||
|
cmdline = cmdline.encode(sys.getfilesystemencoding())
|
||||||
|
p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
|
||||||
|
out1, out2 = p.communicate()
|
||||||
|
reslst = out1.split('\n')
|
||||||
|
cnt = len(reslst)
|
||||||
|
bsdname = None
|
||||||
|
uuidnum = None
|
||||||
|
foundIt = False
|
||||||
|
nest = 0
|
||||||
|
uuidnest = -1
|
||||||
|
partnest = -2
|
||||||
|
for j in xrange(cnt):
|
||||||
|
resline = reslst[j]
|
||||||
|
if resline.find('{') >= 0:
|
||||||
|
nest += 1
|
||||||
|
if resline.find('}') >= 0:
|
||||||
|
nest -= 1
|
||||||
|
pp = resline.find('"UUID" = "')
|
||||||
|
if pp >= 0:
|
||||||
|
uuidnum = resline[pp+10:-1]
|
||||||
|
uuidnum = uuidnum.strip()
|
||||||
|
uuidnest = nest
|
||||||
|
if partnest == uuidnest and uuidnest > 0:
|
||||||
|
foundIt = True
|
||||||
|
break
|
||||||
|
bb = resline.find('"BSD Name" = "')
|
||||||
|
if bb >= 0:
|
||||||
|
bsdname = resline[bb+14:-1]
|
||||||
|
bsdname = bsdname.strip()
|
||||||
|
if (bsdname == diskpart):
|
||||||
|
partnest = nest
|
||||||
|
else :
|
||||||
|
partnest = -2
|
||||||
|
if partnest == uuidnest and partnest > 0:
|
||||||
|
foundIt = True
|
||||||
|
break
|
||||||
|
if nest == 0:
|
||||||
|
partnest = -2
|
||||||
|
uuidnest = -1
|
||||||
|
uuidnum = None
|
||||||
|
bsdname = None
|
||||||
|
if not foundIt:
|
||||||
|
uuidnum = ''
|
||||||
|
return uuidnum
|
||||||
|
|
||||||
|
def GetMACAddressMunged():
|
||||||
|
macnum = os.getenv('MYMACNUM')
|
||||||
|
if macnum != None:
|
||||||
|
return macnum
|
||||||
|
cmdline = '/sbin/ifconfig en0'
|
||||||
|
cmdline = cmdline.encode(sys.getfilesystemencoding())
|
||||||
|
p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
|
||||||
|
out1, out2 = p.communicate()
|
||||||
|
reslst = out1.split('\n')
|
||||||
|
cnt = len(reslst)
|
||||||
|
macnum = None
|
||||||
|
foundIt = False
|
||||||
|
for j in xrange(cnt):
|
||||||
|
resline = reslst[j]
|
||||||
|
pp = resline.find('ether ')
|
||||||
|
if pp >= 0:
|
||||||
|
macnum = resline[pp+6:-1]
|
||||||
|
macnum = macnum.strip()
|
||||||
|
# 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(':')
|
||||||
|
n = len(maclst)
|
||||||
|
if n != 6:
|
||||||
|
fountIt = False
|
||||||
|
break
|
||||||
|
for i in range(6):
|
||||||
|
maclst[i] = int('0x' + maclst[i], 0)
|
||||||
|
mlst = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
|
||||||
|
mlst[5] = maclst[5] ^ 0xa5
|
||||||
|
mlst[4] = maclst[3] ^ 0xa5
|
||||||
|
mlst[3] = maclst[4] ^ 0xa5
|
||||||
|
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])
|
||||||
|
foundIt = True
|
||||||
|
break
|
||||||
|
if not foundIt:
|
||||||
|
macnum = ''
|
||||||
|
return macnum
|
||||||
|
|
||||||
|
|
||||||
# uses unix env to get username instead of using sysctlbyname
|
# uses unix env to get username instead of using sysctlbyname
|
||||||
def GetUserName():
|
def GetUserName():
|
||||||
username = os.getenv('USER')
|
username = os.getenv('USER')
|
||||||
return username
|
return username
|
||||||
|
|
||||||
|
|
||||||
def encode(data, map):
|
|
||||||
result = ""
|
|
||||||
for char in data:
|
|
||||||
value = ord(char)
|
|
||||||
Q = (value ^ 0x80) // len(map)
|
|
||||||
R = value % len(map)
|
|
||||||
result += map[Q]
|
|
||||||
result += map[R]
|
|
||||||
return result
|
|
||||||
|
|
||||||
import hashlib
|
|
||||||
|
|
||||||
def SHA256(message):
|
|
||||||
ctx = hashlib.sha256()
|
|
||||||
ctx.update(message)
|
|
||||||
return ctx.digest()
|
|
||||||
|
|
||||||
# implements an Pseudo Mac Version of Windows built-in Crypto routine
|
# implements an Pseudo Mac Version of Windows built-in Crypto routine
|
||||||
|
# used by Kindle for Mac versions < 1.6.0
|
||||||
def CryptUnprotectData(encryptedData):
|
def CryptUnprotectData(encryptedData):
|
||||||
sp = GetVolumeSerialNumber() + '!@#' + GetUserName()
|
sernum = GetVolumeSerialNumber()
|
||||||
|
if sernum == '':
|
||||||
|
sernum = '9999999999'
|
||||||
|
sp = sernum + '!@#' + GetUserName()
|
||||||
passwdData = encode(SHA256(sp),charMap1)
|
passwdData = encode(SHA256(sp),charMap1)
|
||||||
|
salt = '16743'
|
||||||
|
iter = 0x3e8
|
||||||
|
keylen = 0x80
|
||||||
crp = LibCrypto()
|
crp = LibCrypto()
|
||||||
key_iv = crp.keyivgen(passwdData)
|
key_iv = crp.keyivgen(passwdData, salt, iter, keylen)
|
||||||
key = key_iv[0:32]
|
key = key_iv[0:32]
|
||||||
iv = key_iv[32:48]
|
iv = key_iv[32:48]
|
||||||
crp.set_decrypt_key(key,iv)
|
crp.set_decrypt_key(key,iv)
|
||||||
cleartext = crp.decrypt(encryptedData)
|
cleartext = crp.decrypt(encryptedData)
|
||||||
|
cleartext = decode(cleartext,charMap1)
|
||||||
return cleartext
|
return cleartext
|
||||||
|
|
||||||
|
|
||||||
# Locate and open the .kindle-info file
|
def isNewInstall():
|
||||||
def openKindleInfo(kInfoFile=None):
|
home = os.getenv('HOME')
|
||||||
if kInfoFile == None:
|
# soccer game fan anyone
|
||||||
|
dpath = home + '/Library/Application Support/Kindle/storage/.pes2011'
|
||||||
|
# print dpath, os.path.exists(dpath)
|
||||||
|
if os.path.exists(dpath):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def GetIDString():
|
||||||
|
# K4Mac now has an extensive set of ids strings it uses
|
||||||
|
# in encoding pids and in creating unique passwords
|
||||||
|
# for use in its own version of CryptUnprotectDataV2
|
||||||
|
|
||||||
|
# BUT Amazon has now become nasty enough to detect when its app
|
||||||
|
# is being run under a debugger and actually changes code paths
|
||||||
|
# including which one of these strings is chosen, all to try
|
||||||
|
# to prevent reverse engineering
|
||||||
|
|
||||||
|
# Sad really ... they will only hurt their own sales ...
|
||||||
|
# true book lovers really want to keep their books forever
|
||||||
|
# and move them to their devices and DRM prevents that so they
|
||||||
|
# will just buy from someplace else that they can remove
|
||||||
|
# the DRM from
|
||||||
|
|
||||||
|
# Amazon should know by now that true book lover's are not like
|
||||||
|
# penniless kids that pirate music, we do not pirate books
|
||||||
|
|
||||||
|
if isNewInstall():
|
||||||
|
mungedmac = GetMACAddressMunged()
|
||||||
|
if len(mungedmac) > 7:
|
||||||
|
return mungedmac
|
||||||
|
sernum = GetVolumeSerialNumber()
|
||||||
|
if len(sernum) > 7:
|
||||||
|
return sernum
|
||||||
|
diskpart = GetUserHomeAppSupKindleDirParitionName()
|
||||||
|
uuidnum = GetDiskPartitionUUID(diskpart)
|
||||||
|
if len(uuidnum) > 7:
|
||||||
|
return uuidnum
|
||||||
|
mungedmac = GetMACAddressMunged()
|
||||||
|
if len(mungedmac) > 7:
|
||||||
|
return mungedmac
|
||||||
|
return '9999999999'
|
||||||
|
|
||||||
|
|
||||||
|
# implements an Pseudo Mac Version of Windows built-in Crypto routine
|
||||||
|
# used for Kindle for Mac Versions >= 1.6.0
|
||||||
|
def CryptUnprotectDataV2(encryptedData):
|
||||||
|
sp = GetUserName() + ':&%:' + GetIDString()
|
||||||
|
passwdData = encode(SHA256(sp),charMap5)
|
||||||
|
# salt generation as per the code
|
||||||
|
salt = 0x0512981d * 2 * 1 * 1
|
||||||
|
salt = str(salt) + GetUserName()
|
||||||
|
salt = encode(salt,charMap5)
|
||||||
|
crp = LibCrypto()
|
||||||
|
iter = 0x800
|
||||||
|
keylen = 0x400
|
||||||
|
key_iv = crp.keyivgen(passwdData, salt, iter, keylen)
|
||||||
|
key = key_iv[0:32]
|
||||||
|
iv = key_iv[32:48]
|
||||||
|
crp.set_decrypt_key(key,iv)
|
||||||
|
cleartext = crp.decrypt(encryptedData)
|
||||||
|
cleartext = decode(cleartext, charMap5)
|
||||||
|
return cleartext
|
||||||
|
|
||||||
|
|
||||||
|
# Locate the .kindle-info files
|
||||||
|
def getKindleInfoFiles(kInfoFiles):
|
||||||
|
# first search for current .kindle-info files
|
||||||
home = os.getenv('HOME')
|
home = os.getenv('HOME')
|
||||||
cmdline = 'find "' + home + '/Library/Application Support" -name ".kindle-info"'
|
cmdline = 'find "' + home + '/Library/Application Support" -name ".kindle-info"'
|
||||||
cmdline = cmdline.encode(sys.getfilesystemencoding())
|
cmdline = cmdline.encode(sys.getfilesystemencoding())
|
||||||
@@ -178,15 +419,133 @@ def openKindleInfo(kInfoFile=None):
|
|||||||
out1, out2 = p1.communicate()
|
out1, out2 = p1.communicate()
|
||||||
reslst = out1.split('\n')
|
reslst = out1.split('\n')
|
||||||
kinfopath = 'NONE'
|
kinfopath = 'NONE'
|
||||||
cnt = len(reslst)
|
found = False
|
||||||
for j in xrange(cnt):
|
for resline in reslst:
|
||||||
resline = reslst[j]
|
if os.path.isfile(resline):
|
||||||
pp = resline.find('.kindle-info')
|
kInfoFiles.append(resline)
|
||||||
if pp >= 0:
|
found = True
|
||||||
kinfopath = resline
|
# add any .kinf files
|
||||||
|
cmdline = 'find "' + home + '/Library/Application Support" -name ".rainier*-kinf"'
|
||||||
|
cmdline = cmdline.encode(sys.getfilesystemencoding())
|
||||||
|
p1 = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
|
||||||
|
out1, out2 = p1.communicate()
|
||||||
|
reslst = out1.split('\n')
|
||||||
|
for resline in reslst:
|
||||||
|
if os.path.isfile(resline):
|
||||||
|
kInfoFiles.append(resline)
|
||||||
|
found = True
|
||||||
|
if not found:
|
||||||
|
print('No kindle-info files have been found.')
|
||||||
|
return kInfoFiles
|
||||||
|
|
||||||
|
# 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"]
|
||||||
|
DB = {}
|
||||||
|
cnt = 0
|
||||||
|
infoReader = open(kInfoFile, 'r')
|
||||||
|
hdr = infoReader.read(1)
|
||||||
|
data = infoReader.read()
|
||||||
|
|
||||||
|
if data.find('[') != -1 :
|
||||||
|
# older style kindle-info file
|
||||||
|
items = data.split('[')
|
||||||
|
for item in items:
|
||||||
|
if item != '':
|
||||||
|
keyhash, rawdata = item.split(':')
|
||||||
|
keyname = "unknown"
|
||||||
|
for name in names:
|
||||||
|
if encodeHash(name,charMap2) == keyhash:
|
||||||
|
keyname = name
|
||||||
break
|
break
|
||||||
if not os.path.exists(kinfopath):
|
if keyname == "unknown":
|
||||||
raise K4MDrmException('Error: .kindle-info file can not be found')
|
keyname = keyhash
|
||||||
return open(kinfopath,'r')
|
encryptedValue = decode(rawdata,charMap2)
|
||||||
else:
|
cleartext = CryptUnprotectData(encryptedValue)
|
||||||
return open(kInfoFile, 'r')
|
DB[keyname] = cleartext
|
||||||
|
cnt = cnt + 1
|
||||||
|
if cnt == 0:
|
||||||
|
DB = None
|
||||||
|
return DB
|
||||||
|
|
||||||
|
# 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
|
||||||
|
data = data[:-1]
|
||||||
|
items = data.split('/')
|
||||||
|
|
||||||
|
# loop through the item records until all are processed
|
||||||
|
while len(items) > 0:
|
||||||
|
|
||||||
|
# get the first item record
|
||||||
|
item = items.pop(0)
|
||||||
|
|
||||||
|
# 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"
|
||||||
|
|
||||||
|
# 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 = SHA1(keyhash)
|
||||||
|
|
||||||
|
# the remainder of the first record when decoded with charMap5
|
||||||
|
# has the ':' split char followed by the string representation
|
||||||
|
# of the number of records that follow
|
||||||
|
# and make up the contents
|
||||||
|
srcnt = decode(item[34:],charMap5)
|
||||||
|
rcnt = int(srcnt)
|
||||||
|
|
||||||
|
# read and store in rcnt records of data
|
||||||
|
# that make up the contents value
|
||||||
|
edlst = []
|
||||||
|
for i in xrange(rcnt):
|
||||||
|
item = items.pop(0)
|
||||||
|
edlst.append(item)
|
||||||
|
|
||||||
|
keyname = "unknown"
|
||||||
|
for name in names:
|
||||||
|
if encodeHash(name,charMap5) == keyhash:
|
||||||
|
keyname = name
|
||||||
|
break
|
||||||
|
if keyname == "unknown":
|
||||||
|
keyname = keyhash
|
||||||
|
|
||||||
|
# the charMap5 encoded contents data has had a length
|
||||||
|
# of chars (always odd) cut off of the front and moved
|
||||||
|
# to the end to prevent decoding using charMap5 from
|
||||||
|
# working properly, and thereby preventing the ensuing
|
||||||
|
# CryptUnprotectData call from succeeding.
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
# move first offsets chars to end to align for decode by charMap5
|
||||||
|
encdata = "".join(edlst)
|
||||||
|
contlen = len(encdata)
|
||||||
|
|
||||||
|
# now properly split and recombine
|
||||||
|
# by moving noffset chars from the start of the
|
||||||
|
# string to the end of the string
|
||||||
|
noffset = contlen - primes(int(contlen/3))[-1]
|
||||||
|
pfx = encdata[0:noffset]
|
||||||
|
encdata = encdata[noffset:]
|
||||||
|
encdata = encdata + pfx
|
||||||
|
|
||||||
|
# decode using charMap5 to get the CryptProtect Data
|
||||||
|
encryptedValue = decode(encdata,charMap5)
|
||||||
|
cleartext = CryptUnprotectDataV2(encryptedValue)
|
||||||
|
# Debugging
|
||||||
|
# print keyname
|
||||||
|
# print cleartext
|
||||||
|
# print cleartext.encode('hex')
|
||||||
|
# print
|
||||||
|
DB[keyname] = cleartext
|
||||||
|
cnt = cnt + 1
|
||||||
|
|
||||||
|
if cnt == 0:
|
||||||
|
DB = None
|
||||||
|
return DB
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
# K4PC Windows specific routines
|
# K4PC Windows specific routines
|
||||||
|
|
||||||
from __future__ import with_statement
|
from __future__ import with_statement
|
||||||
|
|
||||||
import sys, os
|
import sys, os
|
||||||
|
from struct import pack, unpack, unpack_from
|
||||||
|
|
||||||
from ctypes import windll, c_char_p, c_wchar_p, c_uint, POINTER, byref, \
|
from ctypes import windll, c_char_p, c_wchar_p, c_uint, POINTER, byref, \
|
||||||
create_unicode_buffer, create_string_buffer, CFUNCTYPE, addressof, \
|
create_unicode_buffer, create_string_buffer, CFUNCTYPE, addressof, \
|
||||||
@@ -10,25 +12,86 @@ from ctypes import windll, c_char_p, c_wchar_p, c_uint, POINTER, byref, \
|
|||||||
|
|
||||||
import _winreg as winreg
|
import _winreg as winreg
|
||||||
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
MAX_PATH = 255
|
MAX_PATH = 255
|
||||||
|
|
||||||
kernel32 = windll.kernel32
|
kernel32 = windll.kernel32
|
||||||
advapi32 = windll.advapi32
|
advapi32 = windll.advapi32
|
||||||
crypt32 = windll.crypt32
|
crypt32 = windll.crypt32
|
||||||
|
|
||||||
|
import traceback
|
||||||
|
|
||||||
# Various character maps used to decrypt books. Probably supposed to act as obfuscation
|
# crypto digestroutines
|
||||||
charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M"
|
import hashlib
|
||||||
|
|
||||||
|
def MD5(message):
|
||||||
|
ctx = hashlib.md5()
|
||||||
|
ctx.update(message)
|
||||||
|
return ctx.digest()
|
||||||
|
|
||||||
|
def SHA1(message):
|
||||||
|
ctx = hashlib.sha1()
|
||||||
|
ctx.update(message)
|
||||||
|
return ctx.digest()
|
||||||
|
|
||||||
|
|
||||||
|
# simple primes table (<= n) calculator
|
||||||
|
def primes(n):
|
||||||
|
if n==2: return [2]
|
||||||
|
elif n<2: return []
|
||||||
|
s=range(3,n+1,2)
|
||||||
|
mroot = n ** 0.5
|
||||||
|
half=(n+1)/2-1
|
||||||
|
i=0
|
||||||
|
m=3
|
||||||
|
while m <= mroot:
|
||||||
|
if s[i]:
|
||||||
|
j=(m*m-3)/2
|
||||||
|
s[j]=0
|
||||||
|
while j<half:
|
||||||
|
s[j]=0
|
||||||
|
j+=m
|
||||||
|
i=i+1
|
||||||
|
m=2*i+3
|
||||||
|
return [2]+[x for x in s if x]
|
||||||
|
|
||||||
|
|
||||||
|
# Various character maps used to decrypt kindle info values.
|
||||||
|
# Probably supposed to act as obfuscation
|
||||||
charMap2 = "AaZzB0bYyCc1XxDdW2wEeVv3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_"
|
charMap2 = "AaZzB0bYyCc1XxDdW2wEeVv3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_"
|
||||||
charMap3 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
charMap5 = "AzB0bYyCeVvaZ3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_c1XxDdW2wE"
|
||||||
charMap4 = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789"
|
|
||||||
|
|
||||||
class DrmException(Exception):
|
class DrmException(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# Encode the bytes in data with the characters in map
|
||||||
|
def encode(data, map):
|
||||||
|
result = ""
|
||||||
|
for char in data:
|
||||||
|
value = ord(char)
|
||||||
|
Q = (value ^ 0x80) // len(map)
|
||||||
|
R = value % len(map)
|
||||||
|
result += map[Q]
|
||||||
|
result += map[R]
|
||||||
|
return result
|
||||||
|
|
||||||
|
# Hash the bytes in data and then encode the digest with the characters in map
|
||||||
|
def encodeHash(data,map):
|
||||||
|
return encode(MD5(data),map)
|
||||||
|
|
||||||
|
# Decode the string in data with the characters in map. Returns the decoded bytes
|
||||||
|
def decode(data,map):
|
||||||
|
result = ""
|
||||||
|
for i in range (0,len(data)-1,2):
|
||||||
|
high = map.find(data[i])
|
||||||
|
low = map.find(data[i+1])
|
||||||
|
if (high == -1) or (low == -1) :
|
||||||
|
break
|
||||||
|
value = (((high * len(map)) ^ 0x80) & 0xFF) + low
|
||||||
|
result += pack("B",value)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# interface with Windows OS Routines
|
||||||
class DataBlob(Structure):
|
class DataBlob(Structure):
|
||||||
_fields_ = [('cbData', c_uint),
|
_fields_ = [('cbData', c_uint),
|
||||||
('pbData', c_void_p)]
|
('pbData', c_void_p)]
|
||||||
@@ -59,47 +122,187 @@ def GetVolumeSerialNumber():
|
|||||||
return GetVolumeSerialNumber
|
return GetVolumeSerialNumber
|
||||||
GetVolumeSerialNumber = GetVolumeSerialNumber()
|
GetVolumeSerialNumber = GetVolumeSerialNumber()
|
||||||
|
|
||||||
|
def GetIDString():
|
||||||
|
return GetVolumeSerialNumber()
|
||||||
|
|
||||||
|
def getLastError():
|
||||||
|
GetLastError = kernel32.GetLastError
|
||||||
|
GetLastError.argtypes = None
|
||||||
|
GetLastError.restype = c_uint
|
||||||
|
def getLastError():
|
||||||
|
return GetLastError()
|
||||||
|
return getLastError
|
||||||
|
getLastError = getLastError()
|
||||||
|
|
||||||
def GetUserName():
|
def GetUserName():
|
||||||
GetUserNameW = advapi32.GetUserNameW
|
GetUserNameW = advapi32.GetUserNameW
|
||||||
GetUserNameW.argtypes = [c_wchar_p, POINTER(c_uint)]
|
GetUserNameW.argtypes = [c_wchar_p, POINTER(c_uint)]
|
||||||
GetUserNameW.restype = c_uint
|
GetUserNameW.restype = c_uint
|
||||||
def GetUserName():
|
def GetUserName():
|
||||||
buffer = create_unicode_buffer(32)
|
buffer = create_unicode_buffer(2)
|
||||||
size = c_uint(len(buffer))
|
size = c_uint(len(buffer))
|
||||||
while not GetUserNameW(buffer, byref(size)):
|
while not GetUserNameW(buffer, byref(size)):
|
||||||
|
errcd = getLastError()
|
||||||
|
if errcd == 234:
|
||||||
|
# bad wine implementation up through wine 1.3.21
|
||||||
|
return "AlternateUserName"
|
||||||
buffer = create_unicode_buffer(len(buffer) * 2)
|
buffer = create_unicode_buffer(len(buffer) * 2)
|
||||||
size.value = len(buffer)
|
size.value = len(buffer)
|
||||||
return buffer.value.encode('utf-16-le')[::2]
|
return buffer.value.encode('utf-16-le')[::2]
|
||||||
return GetUserName
|
return GetUserName
|
||||||
GetUserName = GetUserName()
|
GetUserName = GetUserName()
|
||||||
|
|
||||||
|
|
||||||
def CryptUnprotectData():
|
def CryptUnprotectData():
|
||||||
_CryptUnprotectData = crypt32.CryptUnprotectData
|
_CryptUnprotectData = crypt32.CryptUnprotectData
|
||||||
_CryptUnprotectData.argtypes = [DataBlob_p, c_wchar_p, DataBlob_p,
|
_CryptUnprotectData.argtypes = [DataBlob_p, c_wchar_p, DataBlob_p,
|
||||||
c_void_p, c_void_p, c_uint, DataBlob_p]
|
c_void_p, c_void_p, c_uint, DataBlob_p]
|
||||||
_CryptUnprotectData.restype = c_uint
|
_CryptUnprotectData.restype = c_uint
|
||||||
def CryptUnprotectData(indata, entropy):
|
def CryptUnprotectData(indata, entropy, flags):
|
||||||
indatab = create_string_buffer(indata)
|
indatab = create_string_buffer(indata)
|
||||||
indata = DataBlob(len(indata), cast(indatab, c_void_p))
|
indata = DataBlob(len(indata), cast(indatab, c_void_p))
|
||||||
entropyb = create_string_buffer(entropy)
|
entropyb = create_string_buffer(entropy)
|
||||||
entropy = DataBlob(len(entropy), cast(entropyb, c_void_p))
|
entropy = DataBlob(len(entropy), cast(entropyb, c_void_p))
|
||||||
outdata = DataBlob()
|
outdata = DataBlob()
|
||||||
if not _CryptUnprotectData(byref(indata), None, byref(entropy),
|
if not _CryptUnprotectData(byref(indata), None, byref(entropy),
|
||||||
None, None, 0, byref(outdata)):
|
None, None, flags, byref(outdata)):
|
||||||
raise DrmException("Failed to Unprotect Data")
|
raise DrmException("Failed to Unprotect Data")
|
||||||
return string_at(outdata.pbData, outdata.cbData)
|
return string_at(outdata.pbData, outdata.cbData)
|
||||||
return CryptUnprotectData
|
return CryptUnprotectData
|
||||||
CryptUnprotectData = CryptUnprotectData()
|
CryptUnprotectData = CryptUnprotectData()
|
||||||
|
|
||||||
#
|
|
||||||
# Locate and open the Kindle.info file.
|
# Locate all of the kindle-info style files and return as list
|
||||||
#
|
def getKindleInfoFiles(kInfoFiles):
|
||||||
def openKindleInfo(kInfoFile=None):
|
|
||||||
if kInfoFile == None:
|
|
||||||
regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\")
|
regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\")
|
||||||
path = winreg.QueryValueEx(regkey, 'Local AppData')[0]
|
path = winreg.QueryValueEx(regkey, 'Local AppData')[0]
|
||||||
return open(path+'\\Amazon\\Kindle For PC\\{AMAwzsaPaaZAzmZzZQzgZCAkZ3AjA_AY}\\kindle.info','r')
|
|
||||||
|
# first look for older kindle-info files
|
||||||
|
kinfopath = path +'\\Amazon\\Kindle For PC\\{AMAwzsaPaaZAzmZzZQzgZCAkZ3AjA_AY}\\kindle.info'
|
||||||
|
if not os.path.isfile(kinfopath):
|
||||||
|
print('No kindle.info files have not been found.')
|
||||||
else:
|
else:
|
||||||
return open(kInfoFile, 'r')
|
kInfoFiles.append(kinfopath)
|
||||||
|
|
||||||
|
# now look for newer (K4PC 1.5.0 and later rainier.2.1.1.kinf file
|
||||||
|
|
||||||
|
kinfopath = path +'\\Amazon\\Kindle For PC\\storage\\rainier.2.1.1.kinf'
|
||||||
|
if not os.path.isfile(kinfopath):
|
||||||
|
print('No K4PC 1.5.X .kinf files have not been found.')
|
||||||
|
else:
|
||||||
|
kInfoFiles.append(kinfopath)
|
||||||
|
|
||||||
|
# now look for even newer (K4PC 1.6.0 and later) rainier.2.1.1.kinf file
|
||||||
|
kinfopath = path +'\\Amazon\\Kindle\\storage\\rainier.2.1.1.kinf'
|
||||||
|
if not os.path.isfile(kinfopath):
|
||||||
|
print('No K4PC 1.6.X .kinf files have not been found.')
|
||||||
|
else:
|
||||||
|
kInfoFiles.append(kinfopath)
|
||||||
|
|
||||||
|
return kInfoFiles
|
||||||
|
|
||||||
|
|
||||||
|
# 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"]
|
||||||
|
DB = {}
|
||||||
|
cnt = 0
|
||||||
|
infoReader = open(kInfoFile, 'r')
|
||||||
|
hdr = infoReader.read(1)
|
||||||
|
data = infoReader.read()
|
||||||
|
|
||||||
|
if data.find('{') != -1 :
|
||||||
|
|
||||||
|
# older style kindle-info file
|
||||||
|
items = data.split('{')
|
||||||
|
for item in items:
|
||||||
|
if item != '':
|
||||||
|
keyhash, rawdata = item.split(':')
|
||||||
|
keyname = "unknown"
|
||||||
|
for name in names:
|
||||||
|
if encodeHash(name,charMap2) == keyhash:
|
||||||
|
keyname = name
|
||||||
|
break
|
||||||
|
if keyname == "unknown":
|
||||||
|
keyname = keyhash
|
||||||
|
encryptedValue = decode(rawdata,charMap2)
|
||||||
|
DB[keyname] = CryptUnprotectData(encryptedValue, "", 0)
|
||||||
|
cnt = cnt + 1
|
||||||
|
if cnt == 0:
|
||||||
|
DB = None
|
||||||
|
return DB
|
||||||
|
|
||||||
|
# else newer style .kinf file
|
||||||
|
# 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('/')
|
||||||
|
|
||||||
|
# loop through the item records until all are processed
|
||||||
|
while len(items) > 0:
|
||||||
|
|
||||||
|
# get the first item record
|
||||||
|
item = items.pop(0)
|
||||||
|
|
||||||
|
# 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]
|
||||||
|
|
||||||
|
# the raw keyhash string is also used to create entropy for the actual
|
||||||
|
# CryptProtectData Blob that represents that keys contents
|
||||||
|
entropy = SHA1(keyhash)
|
||||||
|
|
||||||
|
# the remainder of the first record when decoded with charMap5
|
||||||
|
# has the ':' split char followed by the string representation
|
||||||
|
# of the number of records that follow
|
||||||
|
# and make up the contents
|
||||||
|
srcnt = decode(item[34:],charMap5)
|
||||||
|
rcnt = int(srcnt)
|
||||||
|
|
||||||
|
# read and store in rcnt records of data
|
||||||
|
# that make up the contents value
|
||||||
|
edlst = []
|
||||||
|
for i in xrange(rcnt):
|
||||||
|
item = items.pop(0)
|
||||||
|
edlst.append(item)
|
||||||
|
|
||||||
|
keyname = "unknown"
|
||||||
|
for name in names:
|
||||||
|
if encodeHash(name,charMap5) == keyhash:
|
||||||
|
keyname = name
|
||||||
|
break
|
||||||
|
if keyname == "unknown":
|
||||||
|
keyname = keyhash
|
||||||
|
|
||||||
|
# the charMap5 encoded contents data has had a length
|
||||||
|
# of chars (always odd) cut off of the front and moved
|
||||||
|
# to the end to prevent decoding using charMap5 from
|
||||||
|
# working properly, and thereby preventing the ensuing
|
||||||
|
# CryptUnprotectData call from succeeding.
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
# move first offsets chars to end to align for decode by charMap5
|
||||||
|
encdata = "".join(edlst)
|
||||||
|
contlen = len(encdata)
|
||||||
|
noffset = contlen - primes(int(contlen/3))[-1]
|
||||||
|
|
||||||
|
# now properly split and recombine
|
||||||
|
# by moving noffset chars from the start of the
|
||||||
|
# string to the end of the string
|
||||||
|
pfx = encdata[0:noffset]
|
||||||
|
encdata = encdata[noffset:]
|
||||||
|
encdata = encdata + pfx
|
||||||
|
|
||||||
|
# decode using Map5 to get the CryptProtect Data
|
||||||
|
encryptedValue = decode(encdata,charMap5)
|
||||||
|
DB[keyname] = CryptUnprotectData(encryptedValue, entropy, 1)
|
||||||
|
cnt = cnt + 1
|
||||||
|
|
||||||
|
if cnt == 0:
|
||||||
|
DB = None
|
||||||
|
return DB
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -42,8 +42,19 @@
|
|||||||
# 0.20 - Correction: It seems that multibyte entries are encrypted in a v6 file.
|
# 0.20 - Correction: It seems that multibyte entries are encrypted in a v6 file.
|
||||||
# 0.21 - Added support for multiple pids
|
# 0.21 - Added support for multiple pids
|
||||||
# 0.22 - revised structure to hold MobiBook as a class to allow an extended interface
|
# 0.22 - revised structure to hold MobiBook as a class to allow an extended interface
|
||||||
|
# 0.23 - fixed problem with older files with no EXTH section
|
||||||
|
# 0.24 - add support for type 1 encryption and 'TEXtREAd' books as well
|
||||||
|
# 0.25 - Fixed support for 'BOOKMOBI' type 1 encryption
|
||||||
|
# 0.26 - Now enables Text-To-Speech flag and sets clipping limit to 100%
|
||||||
|
# 0.27 - Correct pid metadata token generation to match that used by skindle (Thank You Bart!)
|
||||||
|
# 0.28 - slight additional changes to metadata token generation (None -> '')
|
||||||
|
# 0.29 - It seems that the ideas about when multibyte trailing characters were
|
||||||
|
# included in the encryption were wrong. They are for DOC compressed
|
||||||
|
# files, but they are not for HUFF/CDIC compress files!
|
||||||
|
# 0.30 - Modified interface slightly to work better with new calibre plugin style
|
||||||
|
# 0.31 - The multibyte encrytion info is true for version 7 files too.
|
||||||
|
|
||||||
__version__ = '0.22'
|
__version__ = '0.31'
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
@@ -57,6 +68,7 @@ class Unbuffered:
|
|||||||
return getattr(self.stream, attr)
|
return getattr(self.stream, attr)
|
||||||
sys.stdout=Unbuffered(sys.stdout)
|
sys.stdout=Unbuffered(sys.stdout)
|
||||||
|
|
||||||
|
import os
|
||||||
import struct
|
import struct
|
||||||
import binascii
|
import binascii
|
||||||
|
|
||||||
@@ -153,9 +165,12 @@ class MobiBook:
|
|||||||
def __init__(self, infile):
|
def __init__(self, infile):
|
||||||
# initial sanity check on file
|
# initial sanity check on file
|
||||||
self.data_file = file(infile, 'rb').read()
|
self.data_file = file(infile, 'rb').read()
|
||||||
|
self.mobi_data = ''
|
||||||
self.header = self.data_file[0:78]
|
self.header = self.data_file[0:78]
|
||||||
if self.header[0x3C:0x3C+8] != 'BOOKMOBI':
|
if self.header[0x3C:0x3C+8] != 'BOOKMOBI' and self.header[0x3C:0x3C+8] != 'TEXtREAd':
|
||||||
raise DrmException("invalid file format")
|
raise DrmException("invalid file format")
|
||||||
|
self.magic = self.header[0x3C:0x3C+8]
|
||||||
|
self.crypto_type = -1
|
||||||
|
|
||||||
# build up section offset and flag info
|
# build up section offset and flag info
|
||||||
self.num_sections, = struct.unpack('>H', self.header[76:78])
|
self.num_sections, = struct.unpack('>H', self.header[76:78])
|
||||||
@@ -168,6 +183,15 @@ class MobiBook:
|
|||||||
# parse information from section 0
|
# parse information from section 0
|
||||||
self.sect = self.loadSection(0)
|
self.sect = self.loadSection(0)
|
||||||
self.records, = struct.unpack('>H', self.sect[0x8:0x8+2])
|
self.records, = struct.unpack('>H', self.sect[0x8:0x8+2])
|
||||||
|
self.compression, = struct.unpack('>H', self.sect[0x0:0x0+2])
|
||||||
|
|
||||||
|
if self.magic == 'TEXtREAd':
|
||||||
|
print "Book has format: ", self.magic
|
||||||
|
self.extra_data_flags = 0
|
||||||
|
self.mobi_length = 0
|
||||||
|
self.mobi_version = -1
|
||||||
|
self.meta_array = {}
|
||||||
|
return
|
||||||
self.mobi_length, = struct.unpack('>L',self.sect[0x14:0x18])
|
self.mobi_length, = struct.unpack('>L',self.sect[0x14:0x18])
|
||||||
self.mobi_version, = struct.unpack('>L',self.sect[0x68:0x6C])
|
self.mobi_version, = struct.unpack('>L',self.sect[0x68:0x6C])
|
||||||
print "MOBI header version = %d, length = %d" %(self.mobi_version, self.mobi_length)
|
print "MOBI header version = %d, length = %d" %(self.mobi_version, self.mobi_length)
|
||||||
@@ -175,24 +199,37 @@ class MobiBook:
|
|||||||
if (self.mobi_length >= 0xE4) and (self.mobi_version >= 5):
|
if (self.mobi_length >= 0xE4) and (self.mobi_version >= 5):
|
||||||
self.extra_data_flags, = struct.unpack('>H', self.sect[0xF2:0xF4])
|
self.extra_data_flags, = struct.unpack('>H', self.sect[0xF2:0xF4])
|
||||||
print "Extra Data Flags = %d" % self.extra_data_flags
|
print "Extra Data Flags = %d" % self.extra_data_flags
|
||||||
if self.mobi_version < 7:
|
if (self.compression != 17480):
|
||||||
# multibyte utf8 data is included in the encryption for mobi_version 6 and below
|
# multibyte utf8 data is included in the encryption for PalmDoc compression
|
||||||
# so clear that byte so that we leave it to be decrypted.
|
# so clear that byte so that we leave it to be decrypted.
|
||||||
self.extra_data_flags &= 0xFFFE
|
self.extra_data_flags &= 0xFFFE
|
||||||
|
|
||||||
# if exth region exists parse it for metadata array
|
# if exth region exists parse it for metadata array
|
||||||
self.meta_array = {}
|
self.meta_array = {}
|
||||||
|
try:
|
||||||
exth_flag, = struct.unpack('>L', self.sect[0x80:0x84])
|
exth_flag, = struct.unpack('>L', self.sect[0x80:0x84])
|
||||||
exth = ''
|
exth = 'NONE'
|
||||||
if exth_flag & 0x40:
|
if exth_flag & 0x40:
|
||||||
exth = self.sect[16 + self.mobi_length:]
|
exth = self.sect[16 + self.mobi_length:]
|
||||||
|
if (len(exth) >= 4) and (exth[:4] == 'EXTH'):
|
||||||
nitems, = struct.unpack('>I', exth[8:12])
|
nitems, = struct.unpack('>I', exth[8:12])
|
||||||
pos = 12
|
pos = 12
|
||||||
for i in xrange(nitems):
|
for i in xrange(nitems):
|
||||||
type, size = struct.unpack('>II', exth[pos: pos + 8])
|
type, size = struct.unpack('>II', exth[pos: pos + 8])
|
||||||
content = exth[pos + 8: pos + size]
|
content = exth[pos + 8: pos + size]
|
||||||
self.meta_array[type] = content
|
self.meta_array[type] = content
|
||||||
|
# reset the text to speech flag and clipping limit, if present
|
||||||
|
if type == 401 and size == 9:
|
||||||
|
# set clipping limit to 100%
|
||||||
|
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)
|
||||||
|
# print type, size, content, content.encode('hex')
|
||||||
pos += size
|
pos += size
|
||||||
|
except:
|
||||||
|
self.meta_array = {}
|
||||||
|
pass
|
||||||
|
|
||||||
def getBookTitle(self):
|
def getBookTitle(self):
|
||||||
title = ''
|
title = ''
|
||||||
@@ -208,18 +245,18 @@ class MobiBook:
|
|||||||
return title
|
return title
|
||||||
|
|
||||||
def getPIDMetaInfo(self):
|
def getPIDMetaInfo(self):
|
||||||
rec209 = None
|
rec209 = ''
|
||||||
token = None
|
token = ''
|
||||||
if 209 in self.meta_array:
|
if 209 in self.meta_array:
|
||||||
rec209 = self.meta_array[209]
|
rec209 = self.meta_array[209]
|
||||||
data = rec209
|
data = rec209
|
||||||
# Parse the 209 data to find the the exth record with the token data.
|
# The 209 data comes in five byte groups. Interpret the last four bytes
|
||||||
# The last character of the 209 data points to the record with the token.
|
# of each group as a big endian unsigned integer to get a key value
|
||||||
# Always 208 from my experience, but I'll leave the logic in case that changes.
|
# if that key exists in the meta_array, append its contents to the token
|
||||||
for i in xrange(len(data)):
|
for i in xrange(0,len(data),5):
|
||||||
if ord(data[i]) != 0:
|
val, = struct.unpack('>I',data[i+1:i+5])
|
||||||
if self.meta_array[ord(data[i])] != None:
|
sval = self.meta_array.get(val,'')
|
||||||
token = self.meta_array[ord(data[i])]
|
token += sval
|
||||||
return rec209, token
|
return rec209, token
|
||||||
|
|
||||||
def patch(self, off, new):
|
def patch(self, off, new):
|
||||||
@@ -267,14 +304,18 @@ class MobiBook:
|
|||||||
break
|
break
|
||||||
return [found_key,pid]
|
return [found_key,pid]
|
||||||
|
|
||||||
|
def getMobiFile(self, outpath):
|
||||||
|
file(outpath,'wb').write(self.mobi_data)
|
||||||
|
|
||||||
def processBook(self, pidlist):
|
def processBook(self, pidlist):
|
||||||
crypto_type, = struct.unpack('>H', self.sect[0xC:0xC+2])
|
crypto_type, = struct.unpack('>H', self.sect[0xC:0xC+2])
|
||||||
|
print 'Crypto Type is: ', crypto_type
|
||||||
|
self.crypto_type = crypto_type
|
||||||
if crypto_type == 0:
|
if crypto_type == 0:
|
||||||
print "This book is not encrypted."
|
print "This book is not encrypted."
|
||||||
return self.data_file
|
self.mobi_data = self.data_file
|
||||||
if crypto_type == 1:
|
return
|
||||||
raise DrmException("Cannot decode Mobipocket encryption type 1")
|
if crypto_type != 2 and crypto_type != 1:
|
||||||
if crypto_type != 2:
|
|
||||||
raise DrmException("Cannot decode unknown Mobipocket encryption type %d" % crypto_type)
|
raise DrmException("Cannot decode unknown Mobipocket encryption type %d" % crypto_type)
|
||||||
|
|
||||||
goodpids = []
|
goodpids = []
|
||||||
@@ -286,6 +327,17 @@ class MobiBook:
|
|||||||
elif len(pid)==8:
|
elif len(pid)==8:
|
||||||
goodpids.append(pid)
|
goodpids.append(pid)
|
||||||
|
|
||||||
|
if self.crypto_type == 1:
|
||||||
|
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"
|
||||||
|
found_key = PC1(t1_keyvec, bookkey_data)
|
||||||
|
else :
|
||||||
# calculate the keys
|
# calculate the keys
|
||||||
drm_ptr, drm_count, drm_size, drm_flags = struct.unpack('>LLLL', self.sect[0xA8:0xA8+16])
|
drm_ptr, drm_count, drm_size, drm_flags = struct.unpack('>LLLL', self.sect[0xA8:0xA8+16])
|
||||||
if drm_count == 0:
|
if drm_count == 0:
|
||||||
@@ -293,61 +345,66 @@ class MobiBook:
|
|||||||
found_key, pid = self.parseDRM(self.sect[drm_ptr:drm_ptr+drm_size], drm_count, goodpids)
|
found_key, pid = self.parseDRM(self.sect[drm_ptr:drm_ptr+drm_size], drm_count, goodpids)
|
||||||
if not found_key:
|
if not found_key:
|
||||||
raise DrmException("No key found. Most likely the correct PID has not been given.")
|
raise DrmException("No key found. Most likely the correct PID has not been given.")
|
||||||
|
# kill the drm keys
|
||||||
|
self.patchSection(0, "\0" * drm_size, drm_ptr)
|
||||||
|
# kill the drm pointers
|
||||||
|
self.patchSection(0, "\xff" * 4 + "\0" * 12, 0xA8)
|
||||||
|
|
||||||
if pid=="00000000":
|
if pid=="00000000":
|
||||||
print "File has default encryption, no specific PID."
|
print "File has default encryption, no specific PID."
|
||||||
else:
|
else:
|
||||||
print "File is encoded with PID "+checksumPid(pid)+"."
|
print "File is encoded with PID "+checksumPid(pid)+"."
|
||||||
|
|
||||||
# kill the drm keys
|
|
||||||
self.patchSection(0, "\0" * drm_size, drm_ptr)
|
|
||||||
# kill the drm pointers
|
|
||||||
self.patchSection(0, "\xff" * 4 + "\0" * 12, 0xA8)
|
|
||||||
# clear the crypto type
|
# clear the crypto type
|
||||||
self.patchSection(0, "\0" * 2, 0xC)
|
self.patchSection(0, "\0" * 2, 0xC)
|
||||||
|
|
||||||
# decrypt sections
|
# decrypt sections
|
||||||
print "Decrypting. Please wait . . .",
|
print "Decrypting. Please wait . . .",
|
||||||
new_data = self.data_file[:self.sections[1][0]]
|
self.mobi_data = self.data_file[:self.sections[1][0]]
|
||||||
for i in xrange(1, self.records+1):
|
for i in xrange(1, self.records+1):
|
||||||
data = self.loadSection(i)
|
data = self.loadSection(i)
|
||||||
extra_size = getSizeOfTrailingDataEntries(data, len(data), self.extra_data_flags)
|
extra_size = getSizeOfTrailingDataEntries(data, len(data), self.extra_data_flags)
|
||||||
if i%100 == 0:
|
if i%100 == 0:
|
||||||
print ".",
|
print ".",
|
||||||
# print "record %d, extra_size %d" %(i,extra_size)
|
# print "record %d, extra_size %d" %(i,extra_size)
|
||||||
new_data += PC1(found_key, data[0:len(data) - extra_size])
|
self.mobi_data += PC1(found_key, data[0:len(data) - extra_size])
|
||||||
if extra_size > 0:
|
if extra_size > 0:
|
||||||
new_data += data[-extra_size:]
|
self.mobi_data += data[-extra_size:]
|
||||||
if self.num_sections > self.records+1:
|
if self.num_sections > self.records+1:
|
||||||
new_data += self.data_file[self.sections[self.records+1][0]:]
|
self.mobi_data += self.data_file[self.sections[self.records+1][0]:]
|
||||||
self.data_file = new_data
|
|
||||||
print "done"
|
print "done"
|
||||||
return self.data_file
|
return
|
||||||
|
|
||||||
def getUnencryptedBook(infile,pid):
|
def getUnencryptedBook(infile,pid):
|
||||||
if not os.path.isfile(infile):
|
if not os.path.isfile(infile):
|
||||||
raise DrmException('Input File Not Found')
|
raise DrmException('Input File Not Found')
|
||||||
book = MobiBook(infile)
|
book = MobiBook(infile)
|
||||||
return book.processBook([pid])
|
book.processBook([pid])
|
||||||
|
return book.mobi_data
|
||||||
|
|
||||||
def getUnencryptedBookWithList(infile,pidlist):
|
def getUnencryptedBookWithList(infile,pidlist):
|
||||||
if not os.path.isfile(infile):
|
if not os.path.isfile(infile):
|
||||||
raise DrmException('Input File Not Found')
|
raise DrmException('Input File Not Found')
|
||||||
book = MobiBook(infile)
|
book = MobiBook(infile)
|
||||||
return book.processBook(pidlist)
|
book.processBook(pidlist)
|
||||||
|
return book.mobi_data
|
||||||
|
|
||||||
|
|
||||||
def main(argv=sys.argv):
|
def main(argv=sys.argv):
|
||||||
print ('MobiDeDrm v%(__version__)s. '
|
print ('MobiDeDrm v%(__version__)s. '
|
||||||
'Copyright 2008-2010 The Dark Reverser.' % globals())
|
'Copyright 2008-2010 The Dark Reverser.' % globals())
|
||||||
if len(argv)<4:
|
if len(argv)<3 or len(argv)>4:
|
||||||
print "Removes protection from Mobipocket books"
|
print "Removes protection from Mobipocket books"
|
||||||
print "Usage:"
|
print "Usage:"
|
||||||
print " %s <infile> <outfile> <Comma separated list of PIDs to try>" % sys.argv[0]
|
print " %s <infile> <outfile> [<Comma separated list of PIDs to try>]" % sys.argv[0]
|
||||||
return 1
|
return 1
|
||||||
else:
|
else:
|
||||||
infile = argv[1]
|
infile = argv[1]
|
||||||
outfile = argv[2]
|
outfile = argv[2]
|
||||||
|
if len(argv) is 4:
|
||||||
pidlist = argv[3].split(',')
|
pidlist = argv[3].split(',')
|
||||||
|
else:
|
||||||
|
pidlist = {}
|
||||||
try:
|
try:
|
||||||
stripped_file = getUnencryptedBookWithList(infile, pidlist)
|
stripped_file = getUnencryptedBookWithList(infile, pidlist)
|
||||||
file(outfile, 'wb').write(stripped_file)
|
file(outfile, 'wb').write(stripped_file)
|
||||||
|
|||||||
Binary file not shown.
@@ -24,7 +24,7 @@
|
|||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>droplet</string>
|
<string>droplet</string>
|
||||||
<key>CFBundleGetInfoString</key>
|
<key>CFBundleGetInfoString</key>
|
||||||
<string>DeDRM 1.4, Copyright © 2010 by Apprentice Alf.</string>
|
<string>DeDRM 2.9, Written 2010–2011 by Apprentice Alf and others.</string>
|
||||||
<key>CFBundleIconFile</key>
|
<key>CFBundleIconFile</key>
|
||||||
<string>droplet</string>
|
<string>droplet</string>
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
@@ -34,23 +34,27 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.4</string>
|
<string>2.9</string>
|
||||||
<key>LSMinimumSystemVersion</key>
|
|
||||||
<string>10.5.0</string>
|
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>dplt</string>
|
<string>dplt</string>
|
||||||
|
<key>LSMinimumSystemVersion</key>
|
||||||
|
<string>10.5.0</string>
|
||||||
<key>LSRequiresCarbon</key>
|
<key>LSRequiresCarbon</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>WindowState</key>
|
<key>WindowState</key>
|
||||||
<dict>
|
<dict>
|
||||||
|
<key>dividerCollapsed</key>
|
||||||
|
<true/>
|
||||||
|
<key>eventLogLevel</key>
|
||||||
|
<integer>-1</integer>
|
||||||
<key>name</key>
|
<key>name</key>
|
||||||
<string>ScriptWindowState</string>
|
<string>ScriptWindowState</string>
|
||||||
<key>positionOfDivider</key>
|
<key>positionOfDivider</key>
|
||||||
<real>739</real>
|
<real>0</real>
|
||||||
<key>savedFrame</key>
|
<key>savedFrame</key>
|
||||||
<string>1533 -24 1262 818 1440 -150 1680 1050 </string>
|
<string>1578 27 862 788 1440 -150 1680 1050 </string>
|
||||||
<key>selectedTabView</key>
|
<key>selectedTabView</key>
|
||||||
<string>result</string>
|
<string>event log</string>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -235,6 +235,7 @@ class PageParser(object):
|
|||||||
|
|
||||||
'group' : (1, 'snippets', 1, 0),
|
'group' : (1, 'snippets', 1, 0),
|
||||||
'group.type' : (1, 'scalar_text', 0, 0),
|
'group.type' : (1, 'scalar_text', 0, 0),
|
||||||
|
'group._tag' : (1, 'scalar_text', 0, 0),
|
||||||
|
|
||||||
'region' : (1, 'snippets', 1, 0),
|
'region' : (1, 'snippets', 1, 0),
|
||||||
'region.type' : (1, 'scalar_text', 0, 0),
|
'region.type' : (1, 'scalar_text', 0, 0),
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{\rtf1\ansi\ansicpg1252\cocoartf949\cocoasubrtf540
|
{\rtf1\ansi\ansicpg1252\cocoartf1038\cocoasubrtf350
|
||||||
{\fonttbl}
|
{\fonttbl}
|
||||||
{\colortbl;\red255\green255\blue255;}
|
{\colortbl;\red255\green255\blue255;}
|
||||||
}
|
}
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 362 B After Width: | Height: | Size: 362 B |
@@ -55,29 +55,12 @@
|
|||||||
# 0.14 - contributed enhancement to support --make-pmlz switch
|
# 0.14 - contributed enhancement to support --make-pmlz switch
|
||||||
# 0.15 - enabled high-ascii to pml character encoding. DropBook now works on Mac.
|
# 0.15 - enabled high-ascii to pml character encoding. DropBook now works on Mac.
|
||||||
# 0.16 - convert to use openssl DES (very very fast) or pure python DES if openssl's libcrypto is not available
|
# 0.16 - convert to use openssl DES (very very fast) or pure python DES if openssl's libcrypto is not available
|
||||||
|
# 0.17 - added support for pycrypto's DES as well
|
||||||
|
# 0.18 - on Windows try PyCrypto first and OpenSSL next
|
||||||
|
# 0.19 - Modify the interface to allow use of import
|
||||||
|
# 0.20 - modify to allow use inside new interface for calibre plugins
|
||||||
|
|
||||||
Des = None
|
__version__='0.20'
|
||||||
|
|
||||||
import openssl_des
|
|
||||||
Des = openssl_des.load_libcrypto()
|
|
||||||
|
|
||||||
# if that did not work then use pure python implementation
|
|
||||||
# of DES and try to speed it up with Psycho
|
|
||||||
if Des == None:
|
|
||||||
import python_des
|
|
||||||
Des = python_des.Des
|
|
||||||
# Import Psyco if available
|
|
||||||
try:
|
|
||||||
# Dumb speed hack 1
|
|
||||||
# http://psyco.sourceforge.net
|
|
||||||
import psyco
|
|
||||||
psyco.full()
|
|
||||||
pass
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
__version__='0.16'
|
|
||||||
|
|
||||||
class Unbuffered:
|
class Unbuffered:
|
||||||
def __init__(self, stream):
|
def __init__(self, stream):
|
||||||
@@ -89,22 +72,73 @@ class Unbuffered:
|
|||||||
return getattr(self.stream, attr)
|
return getattr(self.stream, attr)
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
sys.stdout=Unbuffered(sys.stdout)
|
|
||||||
|
|
||||||
import struct, binascii, getopt, zlib, os, os.path, urllib, tempfile
|
import struct, binascii, getopt, zlib, os, os.path, urllib, tempfile
|
||||||
|
|
||||||
|
if 'calibre' in sys.modules:
|
||||||
|
inCalibre = True
|
||||||
|
else:
|
||||||
|
inCalibre = False
|
||||||
|
|
||||||
|
Des = None
|
||||||
|
if sys.platform.startswith('win'):
|
||||||
|
# first try with pycrypto
|
||||||
|
if inCalibre:
|
||||||
|
from calibre_plugins.erdrpdb2pml import pycrypto_des
|
||||||
|
else:
|
||||||
|
import pycrypto_des
|
||||||
|
Des = pycrypto_des.load_pycrypto()
|
||||||
|
if Des == None:
|
||||||
|
# they try with openssl
|
||||||
|
if inCalibre:
|
||||||
|
from calibre_plugins.erdrpdb2pml import openssl_des
|
||||||
|
else:
|
||||||
|
import openssl_des
|
||||||
|
Des = openssl_des.load_libcrypto()
|
||||||
|
else:
|
||||||
|
# first try with openssl
|
||||||
|
if inCalibre:
|
||||||
|
from calibre_plugins.erdrpdb2pml import openssl_des
|
||||||
|
else:
|
||||||
|
import openssl_des
|
||||||
|
Des = openssl_des.load_libcrypto()
|
||||||
|
if Des == None:
|
||||||
|
# then try with pycrypto
|
||||||
|
if inCalibre:
|
||||||
|
from calibre_plugins.erdrpdb2pml import pycrypto_des
|
||||||
|
else:
|
||||||
|
import pycrypto_des
|
||||||
|
Des = pycrypto_des.load_pycrypto()
|
||||||
|
|
||||||
|
# if that did not work then use pure python implementation
|
||||||
|
# of DES and try to speed it up with Psycho
|
||||||
|
if Des == None:
|
||||||
|
if inCalibre:
|
||||||
|
from calibre_plugins.erdrpdb2pml import python_des
|
||||||
|
else:
|
||||||
|
import python_des
|
||||||
|
Des = python_des.Des
|
||||||
|
# Import Psyco if available
|
||||||
|
try:
|
||||||
|
# http://psyco.sourceforge.net
|
||||||
|
import psyco
|
||||||
|
psyco.full()
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from hashlib import sha1
|
from hashlib import sha1
|
||||||
except ImportError:
|
except ImportError:
|
||||||
# older Python release
|
# older Python release
|
||||||
import sha
|
import sha
|
||||||
sha1 = lambda s: sha.new(s)
|
sha1 = lambda s: sha.new(s)
|
||||||
|
|
||||||
import cgi
|
import cgi
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logging.basicConfig()
|
logging.basicConfig()
|
||||||
#logging.basicConfig(level=logging.DEBUG)
|
#logging.basicConfig(level=logging.DEBUG)
|
||||||
|
|
||||||
|
|
||||||
class Sectionizer(object):
|
class Sectionizer(object):
|
||||||
def __init__(self, filename, ident):
|
def __init__(self, filename, ident):
|
||||||
self.contents = file(filename, 'rb').read()
|
self.contents = file(filename, 'rb').read()
|
||||||
@@ -352,7 +386,7 @@ def cleanPML(pml):
|
|||||||
def convertEreaderToPml(infile, name, cc, outdir):
|
def convertEreaderToPml(infile, name, cc, outdir):
|
||||||
if not os.path.exists(outdir):
|
if not os.path.exists(outdir):
|
||||||
os.makedirs(outdir)
|
os.makedirs(outdir)
|
||||||
|
bookname = os.path.splitext(os.path.basename(infile))[0]
|
||||||
print " Decoding File"
|
print " Decoding File"
|
||||||
sect = Sectionizer(infile, 'PNRdPPrs')
|
sect = Sectionizer(infile, 'PNRdPPrs')
|
||||||
er = EreaderProcessor(sect.loadSection, name, cc)
|
er = EreaderProcessor(sect.loadSection, name, cc)
|
||||||
@@ -378,62 +412,14 @@ def convertEreaderToPml(infile, name, cc, outdir):
|
|||||||
# file(os.path.join(outdir, 'bookinfo.txt'),'wb').write(bkinfo)
|
# file(os.path.join(outdir, 'bookinfo.txt'),'wb').write(bkinfo)
|
||||||
|
|
||||||
|
|
||||||
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"
|
|
||||||
return
|
|
||||||
|
|
||||||
def main(argv=None):
|
|
||||||
global bookname
|
|
||||||
try:
|
|
||||||
opts, args = getopt.getopt(sys.argv[1:], "h", ["make-pmlz"])
|
|
||||||
except getopt.GetoptError, err:
|
|
||||||
print str(err)
|
|
||||||
usage()
|
|
||||||
return 1
|
|
||||||
make_pmlz = False
|
|
||||||
zipname = None
|
|
||||||
for o, a in opts:
|
|
||||||
if o == "-h":
|
|
||||||
usage()
|
|
||||||
return 0
|
|
||||||
elif o == "--make-pmlz":
|
|
||||||
make_pmlz = True
|
|
||||||
zipname = ''
|
|
||||||
|
|
||||||
print "eRdr2Pml v%s. Copyright (c) 2009 The Dark Reverser" % __version__
|
|
||||||
|
|
||||||
if len(args)!=3 and len(args)!=4:
|
|
||||||
usage()
|
|
||||||
return 1
|
|
||||||
else:
|
|
||||||
if len(args)==3:
|
|
||||||
infile, name, cc = args[0], args[1], args[2]
|
|
||||||
outdir = infile[:-4] + '_Source'
|
|
||||||
elif len(args)==4:
|
|
||||||
infile, outdir, name, cc = args[0], args[1], args[2], args[3]
|
|
||||||
|
|
||||||
|
def decryptBook(infile, outdir, name, cc, make_pmlz):
|
||||||
if make_pmlz :
|
if make_pmlz :
|
||||||
# ignore specified outdir, use tempdir instead
|
# ignore specified outdir, use tempdir instead
|
||||||
outdir = tempfile.mkdtemp()
|
outdir = tempfile.mkdtemp()
|
||||||
|
|
||||||
bookname = os.path.splitext(os.path.basename(infile))[0]
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
print "Processing..."
|
print "Processing..."
|
||||||
import time
|
|
||||||
start_time = time.time()
|
|
||||||
convertEreaderToPml(infile, name, cc, outdir)
|
convertEreaderToPml(infile, name, cc, outdir)
|
||||||
|
|
||||||
if make_pmlz :
|
if make_pmlz :
|
||||||
import zipfile
|
import zipfile
|
||||||
import shutil
|
import shutil
|
||||||
@@ -456,12 +442,7 @@ def main(argv=None):
|
|||||||
myZipFile.write(imagePath, localname)
|
myZipFile.write(imagePath, localname)
|
||||||
myZipFile.close()
|
myZipFile.close()
|
||||||
# remove temporary directory
|
# remove temporary directory
|
||||||
shutil.rmtree(outdir)
|
shutil.rmtree(outdir, True)
|
||||||
|
|
||||||
end_time = time.time()
|
|
||||||
search_time = end_time - start_time
|
|
||||||
print 'elapsed time: %.2f seconds' % (search_time, )
|
|
||||||
if make_pmlz :
|
|
||||||
print 'output is %s' % zipname
|
print 'output is %s' % zipname
|
||||||
else :
|
else :
|
||||||
print 'output in %s' % outdir
|
print 'output in %s' % outdir
|
||||||
@@ -471,6 +452,53 @@ def main(argv=None):
|
|||||||
return 1
|
return 1
|
||||||
return 0
|
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"
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv=None):
|
||||||
|
try:
|
||||||
|
opts, args = getopt.getopt(sys.argv[1:], "h", ["make-pmlz"])
|
||||||
|
except getopt.GetoptError, err:
|
||||||
|
print str(err)
|
||||||
|
usage()
|
||||||
|
return 1
|
||||||
|
make_pmlz = False
|
||||||
|
for o, a in opts:
|
||||||
|
if o == "-h":
|
||||||
|
usage()
|
||||||
|
return 0
|
||||||
|
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'
|
||||||
|
elif len(args)==4:
|
||||||
|
infile, outdir, name, cc = args[0], args[1], args[2], args[3]
|
||||||
|
|
||||||
|
return decryptBook(infile, outdir, name, cc, make_pmlz)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
sys.stdout=Unbuffered(sys.stdout)
|
||||||
sys.exit(main())
|
sys.exit(main())
|
||||||
|
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ class DocParser(object):
|
|||||||
ys = []
|
ys = []
|
||||||
gdefs = []
|
gdefs = []
|
||||||
|
|
||||||
# get path defintions, positions, dimensions for ecah glyph
|
# get path defintions, positions, dimensions for each glyph
|
||||||
# that makes up the image, and find min x and min y to reposition origin
|
# that makes up the image, and find min x and min y to reposition origin
|
||||||
minx = -1
|
minx = -1
|
||||||
miny = -1
|
miny = -1
|
||||||
@@ -305,6 +305,15 @@ class DocParser(object):
|
|||||||
lastGlyph = firstglyphList[last]
|
lastGlyph = firstglyphList[last]
|
||||||
else :
|
else :
|
||||||
lastGlyph = len(gidList)
|
lastGlyph = len(gidList)
|
||||||
|
|
||||||
|
# handle case of white sapce paragraphs with no actual glyphs in them
|
||||||
|
# by reverting to text based paragraph
|
||||||
|
if firstGlyph >= lastGlyph:
|
||||||
|
# revert to standard text based paragraph
|
||||||
|
for wordnum in xrange(first, last):
|
||||||
|
result.append(('ocr', wordnum))
|
||||||
|
return pclass, result
|
||||||
|
|
||||||
for glyphnum in xrange(firstGlyph, lastGlyph):
|
for glyphnum in xrange(firstGlyph, lastGlyph):
|
||||||
glyphList.append(glyphnum)
|
glyphList.append(glyphnum)
|
||||||
# include any extratokens if they exist
|
# include any extratokens if they exist
|
||||||
|
|||||||
@@ -21,6 +21,17 @@ from struct import unpack
|
|||||||
|
|
||||||
|
|
||||||
# local support routines
|
# local support routines
|
||||||
|
if 'calibre' in sys.modules:
|
||||||
|
inCalibre = True
|
||||||
|
else:
|
||||||
|
inCalibre = False
|
||||||
|
|
||||||
|
if inCalibre :
|
||||||
|
from calibre_plugins.k4mobidedrm import convert2xml
|
||||||
|
from calibre_plugins.k4mobidedrm import flatxml2html
|
||||||
|
from calibre_plugins.k4mobidedrm import flatxml2svg
|
||||||
|
from calibre_plugins.k4mobidedrm import stylexml2css
|
||||||
|
else :
|
||||||
import convert2xml
|
import convert2xml
|
||||||
import flatxml2html
|
import flatxml2html
|
||||||
import flatxml2svg
|
import flatxml2svg
|
||||||
@@ -192,6 +203,8 @@ class GParser(object):
|
|||||||
argres[j] = int(argres[j])
|
argres[j] = int(argres[j])
|
||||||
return result
|
return result
|
||||||
def getGlyphDim(self, gly):
|
def getGlyphDim(self, gly):
|
||||||
|
if self.gdpi[gly] == 0:
|
||||||
|
return 0, 0
|
||||||
maxh = (self.gh[gly] * self.dpi) / self.gdpi[gly]
|
maxh = (self.gh[gly] * self.dpi) / self.gdpi[gly]
|
||||||
maxw = (self.gw[gly] * self.dpi) / self.gdpi[gly]
|
maxw = (self.gw[gly] * self.dpi) / self.gdpi[gly]
|
||||||
return maxh, maxw
|
return maxh, maxw
|
||||||
@@ -320,6 +333,18 @@ def generateBook(bookDir, raw, fixedimage):
|
|||||||
print 'Processing Meta Data and creating OPF'
|
print 'Processing Meta Data and creating OPF'
|
||||||
meta_array = getMetaArray(metaFile)
|
meta_array = getMetaArray(metaFile)
|
||||||
|
|
||||||
|
# replace special chars in title and authors like & < >
|
||||||
|
title = meta_array.get('Title','No Title Provided')
|
||||||
|
title = title.replace('&','&')
|
||||||
|
title = title.replace('<','<')
|
||||||
|
title = title.replace('>','>')
|
||||||
|
meta_array['Title'] = title
|
||||||
|
authors = meta_array.get('Authors','No Authors Provided')
|
||||||
|
authors = authors.replace('&','&')
|
||||||
|
authors = authors.replace('<','<')
|
||||||
|
authors = authors.replace('>','>')
|
||||||
|
meta_array['Authors'] = authors
|
||||||
|
|
||||||
xname = os.path.join(xmlDir, 'metadata.xml')
|
xname = os.path.join(xmlDir, 'metadata.xml')
|
||||||
metastr = ''
|
metastr = ''
|
||||||
for key in meta_array:
|
for key in meta_array:
|
||||||
@@ -399,7 +424,9 @@ def generateBook(bookDir, raw, fixedimage):
|
|||||||
htmlstr += '<title>' + meta_array['Title'] + ' by ' + meta_array['Authors'] + '</title>\n'
|
htmlstr += '<title>' + meta_array['Title'] + ' by ' + meta_array['Authors'] + '</title>\n'
|
||||||
htmlstr += '<meta name="Author" content="' + meta_array['Authors'] + '" />\n'
|
htmlstr += '<meta name="Author" content="' + meta_array['Authors'] + '" />\n'
|
||||||
htmlstr += '<meta name="Title" content="' + meta_array['Title'] + '" />\n'
|
htmlstr += '<meta name="Title" content="' + meta_array['Title'] + '" />\n'
|
||||||
|
if 'ASIN' in meta_array:
|
||||||
htmlstr += '<meta name="ASIN" content="' + meta_array['ASIN'] + '" />\n'
|
htmlstr += '<meta name="ASIN" content="' + meta_array['ASIN'] + '" />\n'
|
||||||
|
if 'GUID' in meta_array:
|
||||||
htmlstr += '<meta name="GUID" content="' + meta_array['GUID'] + '" />\n'
|
htmlstr += '<meta name="GUID" content="' + meta_array['GUID'] + '" />\n'
|
||||||
htmlstr += '<link href="style.css" rel="stylesheet" type="text/css" />\n'
|
htmlstr += '<link href="style.css" rel="stylesheet" type="text/css" />\n'
|
||||||
htmlstr += '</head>\n<body>\n'
|
htmlstr += '</head>\n<body>\n'
|
||||||
@@ -416,7 +443,9 @@ def generateBook(bookDir, raw, fixedimage):
|
|||||||
svgindex += '<title>' + meta_array['Title'] + '</title>\n'
|
svgindex += '<title>' + meta_array['Title'] + '</title>\n'
|
||||||
svgindex += '<meta name="Author" content="' + meta_array['Authors'] + '" />\n'
|
svgindex += '<meta name="Author" content="' + meta_array['Authors'] + '" />\n'
|
||||||
svgindex += '<meta name="Title" content="' + meta_array['Title'] + '" />\n'
|
svgindex += '<meta name="Title" content="' + meta_array['Title'] + '" />\n'
|
||||||
|
if 'ASIN' in meta_array:
|
||||||
svgindex += '<meta name="ASIN" content="' + meta_array['ASIN'] + '" />\n'
|
svgindex += '<meta name="ASIN" content="' + meta_array['ASIN'] + '" />\n'
|
||||||
|
if 'GUID' in meta_array:
|
||||||
svgindex += '<meta name="GUID" content="' + meta_array['GUID'] + '" />\n'
|
svgindex += '<meta name="GUID" content="' + meta_array['GUID'] + '" />\n'
|
||||||
svgindex += '</head>\n'
|
svgindex += '</head>\n'
|
||||||
svgindex += '<body>\n'
|
svgindex += '<body>\n'
|
||||||
@@ -471,8 +500,11 @@ def generateBook(bookDir, raw, fixedimage):
|
|||||||
opfstr += '<package xmlns="http://www.idpf.org/2007/opf" unique-identifier="guid_id">\n'
|
opfstr += '<package xmlns="http://www.idpf.org/2007/opf" unique-identifier="guid_id">\n'
|
||||||
# adding metadata
|
# adding metadata
|
||||||
opfstr += ' <metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf">\n'
|
opfstr += ' <metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf">\n'
|
||||||
|
if 'GUID' in meta_array:
|
||||||
opfstr += ' <dc:identifier opf:scheme="GUID" id="guid_id">' + meta_array['GUID'] + '</dc:identifier>\n'
|
opfstr += ' <dc:identifier opf:scheme="GUID" id="guid_id">' + meta_array['GUID'] + '</dc:identifier>\n'
|
||||||
|
if 'ASIN' in meta_array:
|
||||||
opfstr += ' <dc:identifier opf:scheme="ASIN">' + meta_array['ASIN'] + '</dc:identifier>\n'
|
opfstr += ' <dc:identifier opf:scheme="ASIN">' + meta_array['ASIN'] + '</dc:identifier>\n'
|
||||||
|
if 'oASIN' in meta_array:
|
||||||
opfstr += ' <dc:identifier opf:scheme="oASIN">' + meta_array['oASIN'] + '</dc:identifier>\n'
|
opfstr += ' <dc:identifier opf:scheme="oASIN">' + meta_array['oASIN'] + '</dc:identifier>\n'
|
||||||
opfstr += ' <dc:title>' + meta_array['Title'] + '</dc:title>\n'
|
opfstr += ' <dc:title>' + meta_array['Title'] + '</dc:title>\n'
|
||||||
opfstr += ' <dc:creator opf:role="aut">' + meta_array['Authors'] + '</dc:creator>\n'
|
opfstr += ' <dc:creator opf:role="aut">' + meta_array['Authors'] + '</dc:creator>\n'
|
||||||
@@ -483,7 +515,7 @@ def generateBook(bookDir, raw, fixedimage):
|
|||||||
opfstr += ' </metadata>\n'
|
opfstr += ' </metadata>\n'
|
||||||
opfstr += '<manifest>\n'
|
opfstr += '<manifest>\n'
|
||||||
opfstr += ' <item id="book" href="book.html" media-type="application/xhtml+xml"/>\n'
|
opfstr += ' <item id="book" href="book.html" media-type="application/xhtml+xml"/>\n'
|
||||||
opfstr += ' <item id="stylesheet" href="style.css" media-type="text.css"/>\n'
|
opfstr += ' <item id="stylesheet" href="style.css" media-type="text/css"/>\n'
|
||||||
# adding image files to manifest
|
# adding image files to manifest
|
||||||
filenames = os.listdir(imgDir)
|
filenames = os.listdir(imgDir)
|
||||||
filenames = sorted(filenames)
|
filenames = sorted(filenames)
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
#! /usr/bin/python
|
#! /usr/bin/python
|
||||||
|
|
||||||
# ignobleepub.pyw, version 3
|
from __future__ import with_statement
|
||||||
|
|
||||||
|
# ignobleepub.pyw, version 3.4
|
||||||
|
|
||||||
# To run this program install Python 2.6 from <http://www.python.org/download/>
|
# 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
|
# and OpenSSL or PyCrypto from http://www.voidspace.org.uk/python/modules.shtml#pycrypto
|
||||||
@@ -12,8 +14,10 @@
|
|||||||
# 2 - Added OS X support by using OpenSSL when available
|
# 2 - Added OS X support by using OpenSSL when available
|
||||||
# 3 - screen out improper key lengths to prevent segfaults on Linux
|
# 3 - screen out improper key lengths to prevent segfaults on Linux
|
||||||
# 3.1 - Allow Windows versions of libcrypto to be found
|
# 3.1 - Allow Windows versions of libcrypto to be found
|
||||||
|
# 3.2 - add support for encoding to 'utf-8' when building up list of files to cecrypt from encryption.xml
|
||||||
|
# 3.3 - On Windows try PyCrypto first and OpenSSL next
|
||||||
|
# 3.4 - Modify interace to allow use with import
|
||||||
|
|
||||||
from __future__ import with_statement
|
|
||||||
|
|
||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
|
|
||||||
@@ -105,15 +109,18 @@ def _load_crypto_pycrypto():
|
|||||||
|
|
||||||
def _load_crypto():
|
def _load_crypto():
|
||||||
AES = None
|
AES = None
|
||||||
for loader in (_load_crypto_libcrypto, _load_crypto_pycrypto):
|
cryptolist = (_load_crypto_libcrypto, _load_crypto_pycrypto)
|
||||||
|
if sys.platform.startswith('win'):
|
||||||
|
cryptolist = (_load_crypto_pycrypto, _load_crypto_libcrypto)
|
||||||
|
for loader in cryptolist:
|
||||||
try:
|
try:
|
||||||
AES = loader()
|
AES = loader()
|
||||||
break
|
break
|
||||||
except (ImportError, IGNOBLEError):
|
except (ImportError, IGNOBLEError):
|
||||||
pass
|
pass
|
||||||
return AES
|
return AES
|
||||||
AES = _load_crypto()
|
|
||||||
|
|
||||||
|
AES = _load_crypto()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -164,49 +171,6 @@ class Decryptor(object):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def cli_main(argv=sys.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,)
|
|
||||||
return 1
|
|
||||||
keypath, inpath, outpath = argv[1:]
|
|
||||||
with open(keypath, 'rb') as f:
|
|
||||||
keyb64 = f.read()
|
|
||||||
key = keyb64.decode('base64')[:16]
|
|
||||||
# aes = AES.new(key, AES.MODE_CBC)
|
|
||||||
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:
|
|
||||||
raise IGNOBLEError('%s: not an B&N ADEPT EPUB' % (inpath,))
|
|
||||||
for name in META_NAMES:
|
|
||||||
namelist.remove(name)
|
|
||||||
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
|
|
||||||
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
|
||||||
expr = './/%s' % (adept('encryptedKey'),)
|
|
||||||
bookkey = ''.join(rights.findtext(expr))
|
|
||||||
bookkey = aes.decrypt(bookkey.decode('base64'))
|
|
||||||
bookkey = bookkey[:-ord(bookkey[-1])]
|
|
||||||
encryption = inf.read('META-INF/encryption.xml')
|
|
||||||
decryptor = Decryptor(bookkey[-16:], encryption)
|
|
||||||
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
|
|
||||||
with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf:
|
|
||||||
zi = ZipInfo('mimetype', compress_type=ZIP_STORED)
|
|
||||||
outf.writestr(zi, inf.read('mimetype'))
|
|
||||||
for path in namelist:
|
|
||||||
data = inf.read(path)
|
|
||||||
outf.writestr(path, decryptor.decrypt(path, data))
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
class DecryptionDialog(Tkinter.Frame):
|
class DecryptionDialog(Tkinter.Frame):
|
||||||
def __init__(self, root):
|
def __init__(self, root):
|
||||||
Tkinter.Frame.__init__(self, root, border=5)
|
Tkinter.Frame.__init__(self, root, border=5)
|
||||||
@@ -302,6 +266,53 @@ class DecryptionDialog(Tkinter.Frame):
|
|||||||
return
|
return
|
||||||
self.status['text'] = 'File successfully decrypted'
|
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)
|
||||||
|
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:
|
||||||
|
raise IGNOBLEError('%s: not an B&N ADEPT EPUB' % (inpath,))
|
||||||
|
for name in META_NAMES:
|
||||||
|
namelist.remove(name)
|
||||||
|
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
|
||||||
|
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
||||||
|
expr = './/%s' % (adept('encryptedKey'),)
|
||||||
|
bookkey = ''.join(rights.findtext(expr))
|
||||||
|
bookkey = aes.decrypt(bookkey.decode('base64'))
|
||||||
|
bookkey = bookkey[:-ord(bookkey[-1])]
|
||||||
|
encryption = inf.read('META-INF/encryption.xml')
|
||||||
|
decryptor = Decryptor(bookkey[-16:], encryption)
|
||||||
|
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
|
||||||
|
with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf:
|
||||||
|
zi = ZipInfo('mimetype', compress_type=ZIP_STORED)
|
||||||
|
outf.writestr(zi, inf.read('mimetype'))
|
||||||
|
for path in namelist:
|
||||||
|
data = inf.read(path)
|
||||||
|
outf.writestr(path, decryptor.decrypt(path, data))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cli_main(argv=sys.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,)
|
||||||
|
return 1
|
||||||
|
keypath, inpath, outpath = argv[1:]
|
||||||
|
return decryptBook(keypath, inpath, outpath)
|
||||||
|
|
||||||
|
|
||||||
def gui_main():
|
def gui_main():
|
||||||
root = Tkinter.Tk()
|
root = Tkinter.Tk()
|
||||||
if AES is None:
|
if AES is None:
|
||||||
@@ -318,6 +329,7 @@ def gui_main():
|
|||||||
root.mainloop()
|
root.mainloop()
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
if len(sys.argv) > 1:
|
if len(sys.argv) > 1:
|
||||||
sys.exit(cli_main())
|
sys.exit(cli_main())
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
#! /usr/bin/python
|
#! /usr/bin/python
|
||||||
|
|
||||||
# ignoblekeygen.pyw, version 2
|
from __future__ import with_statement
|
||||||
|
|
||||||
|
# ignoblekeygen.pyw, version 2.3
|
||||||
|
|
||||||
# To run this program install Python 2.6 from <http://www.python.org/download/>
|
# 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
|
# and OpenSSL or PyCrypto from http://www.voidspace.org.uk/python/modules.shtml#pycrypto
|
||||||
@@ -11,13 +13,13 @@
|
|||||||
# 1 - Initial release
|
# 1 - Initial release
|
||||||
# 2 - Add OS X support by using OpenSSL when available (taken/modified from ineptepub v5)
|
# 2 - Add OS X support by using OpenSSL when available (taken/modified from ineptepub v5)
|
||||||
# 2.1 - Allow Windows versions of libcrypto to be found
|
# 2.1 - Allow Windows versions of libcrypto to be found
|
||||||
|
# 2.2 - On Windows try PyCrypto first and then OpenSSL next
|
||||||
|
# 2.3 - Modify interface to allow use of import
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Generate Barnes & Noble EPUB user key from name and credit card number.
|
Generate Barnes & Noble EPUB user key from name and credit card number.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import with_statement
|
|
||||||
|
|
||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
@@ -102,11 +104,12 @@ def _load_crypto_pycrypto():
|
|||||||
|
|
||||||
return AES
|
return AES
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _load_crypto():
|
def _load_crypto():
|
||||||
AES = None
|
AES = None
|
||||||
for loader in (_load_crypto_libcrypto, _load_crypto_pycrypto):
|
cryptolist = (_load_crypto_libcrypto, _load_crypto_pycrypto)
|
||||||
|
if sys.platform.startswith('win'):
|
||||||
|
cryptolist = (_load_crypto_pycrypto, _load_crypto_libcrypto)
|
||||||
|
for loader in cryptolist:
|
||||||
try:
|
try:
|
||||||
AES = loader()
|
AES = loader()
|
||||||
break
|
break
|
||||||
@@ -119,6 +122,7 @@ AES = _load_crypto()
|
|||||||
def normalize_name(name):
|
def normalize_name(name):
|
||||||
return ''.join(x for x in name.lower() if x != ' ')
|
return ''.join(x for x in name.lower() if x != ' ')
|
||||||
|
|
||||||
|
|
||||||
def generate_keyfile(name, ccn, outpath):
|
def generate_keyfile(name, ccn, outpath):
|
||||||
name = normalize_name(name) + '\x00'
|
name = normalize_name(name) + '\x00'
|
||||||
ccn = ccn + '\x00'
|
ccn = ccn + '\x00'
|
||||||
@@ -132,19 +136,6 @@ def generate_keyfile(name, ccn, outpath):
|
|||||||
f.write(userkey.encode('base64'))
|
f.write(userkey.encode('base64'))
|
||||||
return userkey
|
return userkey
|
||||||
|
|
||||||
def cli_main(argv=sys.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 NAME CC# OUTFILE" % (progname,)
|
|
||||||
return 1
|
|
||||||
name, ccn, outpath = argv[1:]
|
|
||||||
generate_keyfile(name, ccn, outpath)
|
|
||||||
return 0
|
|
||||||
|
|
||||||
class DecryptionDialog(Tkinter.Frame):
|
class DecryptionDialog(Tkinter.Frame):
|
||||||
def __init__(self, root):
|
def __init__(self, root):
|
||||||
@@ -210,6 +201,22 @@ class DecryptionDialog(Tkinter.Frame):
|
|||||||
return
|
return
|
||||||
self.status['text'] = 'Keyfile successfully generated'
|
self.status['text'] = 'Keyfile successfully generated'
|
||||||
|
|
||||||
|
|
||||||
|
def cli_main(argv=sys.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 NAME CC# OUTFILE" % (progname,)
|
||||||
|
return 1
|
||||||
|
name, ccn, outpath = argv[1:]
|
||||||
|
generate_keyfile(name, ccn, outpath)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def gui_main():
|
def gui_main():
|
||||||
root = Tkinter.Tk()
|
root = Tkinter.Tk()
|
||||||
if AES is None:
|
if AES is None:
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
#! /usr/bin/python
|
#! /usr/bin/python
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# ineptepub.pyw, version 5.4
|
from __future__ import with_statement
|
||||||
|
|
||||||
|
# ineptepub.pyw, version 5.6
|
||||||
# Copyright © 2009-2010 i♥cabbages
|
# Copyright © 2009-2010 i♥cabbages
|
||||||
|
|
||||||
# Released under the terms of the GNU General Public Licence, version 3 or
|
# Released under the terms of the GNU General Public Licence, version 3 or
|
||||||
@@ -26,13 +28,12 @@
|
|||||||
# 5.2 - Fix ctypes error causing segfaults on some systems
|
# 5.2 - Fix ctypes error causing segfaults on some systems
|
||||||
# 5.3 - add support for OpenSSL on Windows, fix bug with some versions of libcrypto 0.9.8 prior to path level o
|
# 5.3 - add support for OpenSSL on Windows, fix bug with some versions of libcrypto 0.9.8 prior to path level o
|
||||||
# 5.4 - add support for encoding to 'utf-8' when building up list of files to decrypt from encryption.xml
|
# 5.4 - add support for encoding to 'utf-8' when building up list of files to decrypt from encryption.xml
|
||||||
|
# 5.5 - On Windows try PyCrypto first, OpenSSL next
|
||||||
|
# 5.6 - Modify interface to allow use with import
|
||||||
"""
|
"""
|
||||||
Decrypt Adobe ADEPT-encrypted EPUB books.
|
Decrypt Adobe ADEPT-encrypted EPUB books.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import with_statement
|
|
||||||
|
|
||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
@@ -259,7 +260,10 @@ def _load_crypto_pycrypto():
|
|||||||
|
|
||||||
def _load_crypto():
|
def _load_crypto():
|
||||||
AES = RSA = None
|
AES = RSA = None
|
||||||
for loader in (_load_crypto_libcrypto, _load_crypto_pycrypto):
|
cryptolist = (_load_crypto_libcrypto, _load_crypto_pycrypto)
|
||||||
|
if sys.platform.startswith('win'):
|
||||||
|
cryptolist = (_load_crypto_pycrypto, _load_crypto_libcrypto)
|
||||||
|
for loader in cryptolist:
|
||||||
try:
|
try:
|
||||||
AES, RSA = loader()
|
AES, RSA = loader()
|
||||||
break
|
break
|
||||||
@@ -308,45 +312,6 @@ class Decryptor(object):
|
|||||||
data = self.decompress(data)
|
data = self.decompress(data)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def cli_main(argv=sys.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,)
|
|
||||||
return 1
|
|
||||||
keypath, inpath, outpath = argv[1:]
|
|
||||||
with open(keypath, 'rb') as f:
|
|
||||||
keyder = f.read()
|
|
||||||
rsa = RSA(keyder)
|
|
||||||
with closing(ZipFile(open(inpath, 'rb'))) as inf:
|
|
||||||
namelist = set(inf.namelist())
|
|
||||||
if 'META-INF/rights.xml' not in namelist or \
|
|
||||||
'META-INF/encryption.xml' not in namelist:
|
|
||||||
raise ADEPTError('%s: not an ADEPT EPUB' % (inpath,))
|
|
||||||
for name in META_NAMES:
|
|
||||||
namelist.remove(name)
|
|
||||||
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
|
|
||||||
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
|
||||||
expr = './/%s' % (adept('encryptedKey'),)
|
|
||||||
bookkey = ''.join(rights.findtext(expr))
|
|
||||||
bookkey = rsa.decrypt(bookkey.decode('base64'))
|
|
||||||
# Padded as per RSAES-PKCS1-v1_5
|
|
||||||
if bookkey[-17] != '\x00':
|
|
||||||
raise ADEPTError('problem decrypting session key')
|
|
||||||
encryption = inf.read('META-INF/encryption.xml')
|
|
||||||
decryptor = Decryptor(bookkey[-16:], encryption)
|
|
||||||
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
|
|
||||||
with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf:
|
|
||||||
zi = ZipInfo('mimetype', compress_type=ZIP_STORED)
|
|
||||||
outf.writestr(zi, inf.read('mimetype'))
|
|
||||||
for path in namelist:
|
|
||||||
data = inf.read(path)
|
|
||||||
outf.writestr(path, decryptor.decrypt(path, data))
|
|
||||||
return 0
|
|
||||||
|
|
||||||
class DecryptionDialog(Tkinter.Frame):
|
class DecryptionDialog(Tkinter.Frame):
|
||||||
def __init__(self, root):
|
def __init__(self, root):
|
||||||
@@ -442,6 +407,52 @@ class DecryptionDialog(Tkinter.Frame):
|
|||||||
return
|
return
|
||||||
self.status['text'] = 'File successfully decrypted'
|
self.status['text'] = 'File successfully decrypted'
|
||||||
|
|
||||||
|
|
||||||
|
def decryptBook(keypath, inpath, outpath):
|
||||||
|
with open(keypath, 'rb') as f:
|
||||||
|
keyder = f.read()
|
||||||
|
rsa = RSA(keyder)
|
||||||
|
with closing(ZipFile(open(inpath, 'rb'))) as inf:
|
||||||
|
namelist = set(inf.namelist())
|
||||||
|
if 'META-INF/rights.xml' not in namelist or \
|
||||||
|
'META-INF/encryption.xml' not in namelist:
|
||||||
|
raise ADEPTError('%s: not an ADEPT EPUB' % (inpath,))
|
||||||
|
for name in META_NAMES:
|
||||||
|
namelist.remove(name)
|
||||||
|
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
|
||||||
|
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
||||||
|
expr = './/%s' % (adept('encryptedKey'),)
|
||||||
|
bookkey = ''.join(rights.findtext(expr))
|
||||||
|
bookkey = rsa.decrypt(bookkey.decode('base64'))
|
||||||
|
# Padded as per RSAES-PKCS1-v1_5
|
||||||
|
if bookkey[-17] != '\x00':
|
||||||
|
raise ADEPTError('problem decrypting session key')
|
||||||
|
encryption = inf.read('META-INF/encryption.xml')
|
||||||
|
decryptor = Decryptor(bookkey[-16:], encryption)
|
||||||
|
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
|
||||||
|
with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf:
|
||||||
|
zi = ZipInfo('mimetype', compress_type=ZIP_STORED)
|
||||||
|
outf.writestr(zi, inf.read('mimetype'))
|
||||||
|
for path in namelist:
|
||||||
|
data = inf.read(path)
|
||||||
|
outf.writestr(path, decryptor.decrypt(path, data))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cli_main(argv=sys.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,)
|
||||||
|
return 1
|
||||||
|
keypath, inpath, outpath = argv[1:]
|
||||||
|
return decryptBook(keypath, inpath, outpath)
|
||||||
|
|
||||||
|
|
||||||
def gui_main():
|
def gui_main():
|
||||||
root = Tkinter.Tk()
|
root = Tkinter.Tk()
|
||||||
if AES is None:
|
if AES is None:
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
#! /usr/bin/python
|
#! /usr/bin/python
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# ineptkey.pyw, version 5
|
from __future__ import with_statement
|
||||||
|
|
||||||
|
# ineptkey.pyw, version 5.4
|
||||||
# Copyright © 2009-2010 i♥cabbages
|
# Copyright © 2009-2010 i♥cabbages
|
||||||
|
|
||||||
# Released under the terms of the GNU General Public Licence, version 3 or
|
# Released under the terms of the GNU General Public Licence, version 3 or
|
||||||
@@ -32,13 +34,13 @@
|
|||||||
# Clean up and merge OS X support by unknown
|
# Clean up and merge OS X support by unknown
|
||||||
# 5.1 - add support for using OpenSSL on Windows in place of PyCrypto
|
# 5.1 - add support for using OpenSSL on Windows in place of PyCrypto
|
||||||
# 5.2 - added support for output of key to a particular file
|
# 5.2 - added support for output of key to a particular file
|
||||||
|
# 5.3 - On Windows try PyCrypto first, OpenSSL next
|
||||||
|
# 5.4 - Modify interface to allow use of import
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Retrieve Adobe ADEPT user key.
|
Retrieve Adobe ADEPT user key.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import with_statement
|
|
||||||
|
|
||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
@@ -115,7 +117,7 @@ if sys.platform.startswith('win'):
|
|||||||
|
|
||||||
def _load_crypto():
|
def _load_crypto():
|
||||||
AES = None
|
AES = None
|
||||||
for loader in (_load_crypto_libcrypto, _load_crypto_pycrypto):
|
for loader in (_load_crypto_pycrypto, _load_crypto_libcrypto):
|
||||||
try:
|
try:
|
||||||
AES = loader()
|
AES = loader()
|
||||||
break
|
break
|
||||||
@@ -414,10 +416,11 @@ class ExceptionDialog(Tkinter.Frame):
|
|||||||
label.pack(fill=Tkconstants.X, expand=0)
|
label.pack(fill=Tkconstants.X, expand=0)
|
||||||
self.text = Tkinter.Text(self)
|
self.text = Tkinter.Text(self)
|
||||||
self.text.pack(fill=Tkconstants.BOTH, expand=1)
|
self.text.pack(fill=Tkconstants.BOTH, expand=1)
|
||||||
|
|
||||||
self.text.insert(Tkconstants.END, text)
|
self.text.insert(Tkconstants.END, text)
|
||||||
|
|
||||||
def cli_main(argv=sys.argv):
|
|
||||||
keypath = argv[1]
|
def extractKeyfile(keypath):
|
||||||
try:
|
try:
|
||||||
success = retrieve_key(keypath)
|
success = retrieve_key(keypath)
|
||||||
except ADEPTError, e:
|
except ADEPTError, e:
|
||||||
@@ -430,6 +433,12 @@ def cli_main(argv=sys.argv):
|
|||||||
return 1
|
return 1
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cli_main(argv=sys.argv):
|
||||||
|
keypath = argv[1]
|
||||||
|
return extractKeyfile(keypath)
|
||||||
|
|
||||||
|
|
||||||
def main(argv=sys.argv):
|
def main(argv=sys.argv):
|
||||||
root = Tkinter.Tk()
|
root = Tkinter.Tk()
|
||||||
root.withdraw()
|
root.withdraw()
|
||||||
2244
DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ineptpdf.py
Normal file
2244
DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ineptpdf.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,7 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
from __future__ import with_statement
|
||||||
|
|
||||||
# engine to remove drm from Kindle for Mac and Kindle for PC books
|
# engine to remove drm from Kindle for Mac and Kindle for PC books
|
||||||
# for personal use for archiving and converting your ebooks
|
# for personal use for archiving and converting your ebooks
|
||||||
|
|
||||||
@@ -14,21 +16,8 @@
|
|||||||
# unswindle, DarkReverser, ApprenticeAlf, DiapDealer, some_updates
|
# unswindle, DarkReverser, ApprenticeAlf, DiapDealer, some_updates
|
||||||
# and many many others
|
# and many many others
|
||||||
|
|
||||||
# It can run standalone to convert K4M/K4PC/Mobi files, or it can be installed as a
|
|
||||||
# plugin for Calibre (http://calibre-ebook.com/about) so that importing
|
|
||||||
# K4 or Mobi with DRM is no londer a multi-step process.
|
|
||||||
#
|
|
||||||
# ***NOTE*** If you are using this script as a calibre plugin for a K4M or K4PC ebook
|
|
||||||
# then calibre must be installed on the same machine and in the same account as K4PC or K4M
|
|
||||||
# for the plugin version to function properly.
|
|
||||||
#
|
|
||||||
# To create a Calibre plugin, rename this file so that the filename
|
|
||||||
# ends in '_plugin.py', put it into a ZIP file with all its supporting python routines
|
|
||||||
# and import that ZIP into Calibre using its plugin configuration GUI.
|
|
||||||
|
|
||||||
from __future__ import with_statement
|
__version__ = '3.6'
|
||||||
|
|
||||||
__version__ = '1.4'
|
|
||||||
|
|
||||||
class Unbuffered:
|
class Unbuffered:
|
||||||
def __init__(self, stream):
|
def __init__(self, stream):
|
||||||
@@ -41,11 +30,8 @@ class Unbuffered:
|
|||||||
|
|
||||||
import sys
|
import sys
|
||||||
import os, csv, getopt
|
import os, csv, getopt
|
||||||
import binascii
|
import string
|
||||||
import zlib
|
|
||||||
import re
|
import re
|
||||||
import zlib, zipfile, tempfile, shutil
|
|
||||||
from struct import pack, unpack, unpack_from
|
|
||||||
|
|
||||||
class DrmException(Exception):
|
class DrmException(Exception):
|
||||||
pass
|
pass
|
||||||
@@ -55,19 +41,103 @@ if 'calibre' in sys.modules:
|
|||||||
else:
|
else:
|
||||||
inCalibre = False
|
inCalibre = False
|
||||||
|
|
||||||
def zipUpDir(myzip, tempdir,localname):
|
if inCalibre:
|
||||||
currentdir = tempdir
|
from calibre_plugins.k4mobidedrm import mobidedrm
|
||||||
if localname != "":
|
from calibre_plugins.k4mobidedrm import topazextract
|
||||||
currentdir = os.path.join(currentdir,localname)
|
from calibre_plugins.k4mobidedrm import kgenpids
|
||||||
list = os.listdir(currentdir)
|
else:
|
||||||
for file in list:
|
import mobidedrm
|
||||||
afilename = file
|
import topazextract
|
||||||
localfilePath = os.path.join(localname, afilename)
|
import kgenpids
|
||||||
realfilePath = os.path.join(currentdir,file)
|
|
||||||
if os.path.isfile(realfilePath):
|
|
||||||
myzip.write(realfilePath, localfilePath)
|
# cleanup bytestring filenames
|
||||||
elif os.path.isdir(realfilePath):
|
# borrowed from calibre from calibre/src/calibre/__init__.py
|
||||||
zipUpDir(myzip, tempdir, localfilePath)
|
# added in removal of non-printing chars
|
||||||
|
# and removal of . at start
|
||||||
|
# convert spaces to underscores
|
||||||
|
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):
|
||||||
|
# handle the obvious cases at the beginning
|
||||||
|
if not os.path.isfile(infile):
|
||||||
|
print "Error: Input file does not exist"
|
||||||
|
return 1
|
||||||
|
|
||||||
|
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 = bookname
|
||||||
|
if len(bookname)>4 and len(filenametitle)>4 and bookname[:4] != filenametitle[:4]:
|
||||||
|
outfilename = outfilename + "_" + filenametitle
|
||||||
|
|
||||||
|
# build pid list
|
||||||
|
md1, md2 = mb.getPIDMetaInfo()
|
||||||
|
pidlst = kgenpids.getPidList(md1, md2, k4, pids, serials, kInfoFiles)
|
||||||
|
|
||||||
|
try:
|
||||||
|
mb.processBook(pidlst)
|
||||||
|
|
||||||
|
except mobidedrm.DrmException, e:
|
||||||
|
print "Error: " + str(e) + "\nDRM Removal Failed.\n"
|
||||||
|
return 1
|
||||||
|
except topazextract.TpzDRMError, e:
|
||||||
|
print "Error: " + str(e) + "\nDRM Removal Failed.\n"
|
||||||
|
return 1
|
||||||
|
except Exception, e:
|
||||||
|
print "Error: " + str(e) + "\nDRM Removal Failed.\n"
|
||||||
|
return 1
|
||||||
|
|
||||||
|
if mobi:
|
||||||
|
outfile = os.path.join(outdir, outfilename + '_nodrm' + '.mobi')
|
||||||
|
mb.getMobiFile(outfile)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# topaz:
|
||||||
|
print " Creating NoDRM HTMLZ Archive"
|
||||||
|
zipname = os.path.join(outdir, outfilename + '_nodrm' + '.htmlz')
|
||||||
|
mb.getHTMLZip(zipname)
|
||||||
|
|
||||||
|
print " Creating SVG HTMLZ Archive"
|
||||||
|
zipname = os.path.join(outdir, outfilename + '_SVG' + '.htmlz')
|
||||||
|
mb.getSVGZip(zipname)
|
||||||
|
|
||||||
|
print " Creating XML ZIP Archive"
|
||||||
|
zipname = os.path.join(outdir, outfilename + '_XML' + '.zip')
|
||||||
|
mb.getXMLZip(zipname)
|
||||||
|
|
||||||
|
# remove internal temporary directory of Topaz pieces
|
||||||
|
mb.cleanup()
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def usage(progname):
|
def usage(progname):
|
||||||
print "Removes DRM protection from K4PC/M, Kindle, Mobi and Topaz ebooks"
|
print "Removes DRM protection from K4PC/M, Kindle, Mobi and Topaz ebooks"
|
||||||
@@ -78,9 +148,6 @@ def usage(progname):
|
|||||||
# Main
|
# Main
|
||||||
#
|
#
|
||||||
def main(argv=sys.argv):
|
def main(argv=sys.argv):
|
||||||
import mobidedrm
|
|
||||||
import topazextract
|
|
||||||
import kgenpids
|
|
||||||
progname = os.path.basename(argv[0])
|
progname = os.path.basename(argv[0])
|
||||||
|
|
||||||
k4 = False
|
k4 = False
|
||||||
@@ -118,216 +185,15 @@ def main(argv=sys.argv):
|
|||||||
|
|
||||||
# try with built in Kindle Info files
|
# try with built in Kindle Info files
|
||||||
k4 = True
|
k4 = True
|
||||||
|
if sys.platform.startswith('linux'):
|
||||||
|
k4 = False
|
||||||
|
kInfoFiles = None
|
||||||
infile = args[0]
|
infile = args[0]
|
||||||
outdir = args[1]
|
outdir = args[1]
|
||||||
|
return decryptBook(infile, outdir, k4, kInfoFiles, serials, pids)
|
||||||
|
|
||||||
# handle the obvious cases at the beginning
|
|
||||||
if not os.path.isfile(infile):
|
|
||||||
print "Error: Input file does not exist"
|
|
||||||
return 1
|
|
||||||
|
|
||||||
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:
|
|
||||||
tempdir = tempfile.mkdtemp()
|
|
||||||
mb = topazextract.TopazBook(infile, tempdir)
|
|
||||||
|
|
||||||
title = mb.getBookTitle()
|
|
||||||
print "Processing Book: ", title
|
|
||||||
|
|
||||||
# build pid list
|
|
||||||
md1, md2 = mb.getPIDMetaInfo()
|
|
||||||
pidlst = kgenpids.getPidList(md1, md2, k4, pids, serials, kInfoFiles)
|
|
||||||
|
|
||||||
try:
|
|
||||||
if mobi:
|
|
||||||
unlocked_file = mb.processBook(pidlst)
|
|
||||||
else:
|
|
||||||
mb.processBook(pidlst)
|
|
||||||
|
|
||||||
except mobidedrm.DrmException, e:
|
|
||||||
print " ... not suceessful " + str(e) + "\n"
|
|
||||||
return 1
|
|
||||||
except topazextract.TpzDRMError, e:
|
|
||||||
print str(e)
|
|
||||||
print " Creating DeBug Full Zip Archive of Book"
|
|
||||||
zipname = os.path.join(outdir, bookname + '_debug' + '.zip')
|
|
||||||
myzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
|
||||||
zipUpDir(myzip, tempdir, '')
|
|
||||||
myzip.close()
|
|
||||||
return 1
|
|
||||||
|
|
||||||
if mobi:
|
|
||||||
outfile = os.path.join(outdir,bookname + '_nodrm' + '.azw')
|
|
||||||
file(outfile, 'wb').write(unlocked_file)
|
|
||||||
return 0
|
|
||||||
|
|
||||||
# topaz: build up zip archives of results
|
|
||||||
print " Creating HTML ZIP Archive"
|
|
||||||
zipname = os.path.join(outdir, bookname + '_nodrm' + '.zip')
|
|
||||||
myzip1 = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
|
||||||
myzip1.write(os.path.join(tempdir,'book.html'),'book.html')
|
|
||||||
myzip1.write(os.path.join(tempdir,'book.opf'),'book.opf')
|
|
||||||
if os.path.isfile(os.path.join(tempdir,'cover.jpg')):
|
|
||||||
myzip1.write(os.path.join(tempdir,'cover.jpg'),'cover.jpg')
|
|
||||||
myzip1.write(os.path.join(tempdir,'style.css'),'style.css')
|
|
||||||
zipUpDir(myzip1, tempdir, 'img')
|
|
||||||
myzip1.close()
|
|
||||||
|
|
||||||
print " Creating SVG ZIP Archive"
|
|
||||||
zipname = os.path.join(outdir, bookname + '_SVG' + '.zip')
|
|
||||||
myzip2 = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
|
||||||
myzip2.write(os.path.join(tempdir,'index_svg.xhtml'),'index_svg.xhtml')
|
|
||||||
zipUpDir(myzip2, tempdir, 'svg')
|
|
||||||
zipUpDir(myzip2, tempdir, 'img')
|
|
||||||
myzip2.close()
|
|
||||||
|
|
||||||
print " Creating XML ZIP Archive"
|
|
||||||
zipname = os.path.join(outdir, bookname + '_XML' + '.zip')
|
|
||||||
myzip3 = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
|
||||||
targetdir = os.path.join(tempdir,'xml')
|
|
||||||
zipUpDir(myzip3, targetdir, '')
|
|
||||||
zipUpDir(myzip3, tempdir, 'img')
|
|
||||||
myzip3.close()
|
|
||||||
|
|
||||||
shutil.rmtree(tempdir)
|
|
||||||
return 0
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
sys.stdout=Unbuffered(sys.stdout)
|
sys.stdout=Unbuffered(sys.stdout)
|
||||||
sys.exit(main())
|
sys.exit(main())
|
||||||
|
|
||||||
if not __name__ == "__main__" and inCalibre:
|
|
||||||
from calibre.customize import FileTypePlugin
|
|
||||||
|
|
||||||
class K4DeDRM(FileTypePlugin):
|
|
||||||
name = 'K4PC, K4Mac, Kindle Mobi and Topaz DeDRM' # Name of the plugin
|
|
||||||
description = 'Removes DRM from K4PC and Mac, Kindle Mobi and Topaz files. \
|
|
||||||
Provided by the work of many including DiapDealer, SomeUpdates, IHeartCabbages, CMBDTC, Skindle, DarkReverser, ApprenticeAlf, etc.'
|
|
||||||
supported_platforms = ['osx', 'windows', 'linux'] # Platforms this plugin will run on
|
|
||||||
author = 'DiapDealer, SomeUpdates' # The author of this plugin
|
|
||||||
version = (0, 1, 7) # The version number of this plugin
|
|
||||||
file_types = set(['prc','mobi','azw','azw1','tpz']) # The file types that this plugin will be applied to
|
|
||||||
on_import = True # Run this plugin during the import
|
|
||||||
priority = 210 # run this plugin before mobidedrm, k4pcdedrm, k4dedrm
|
|
||||||
|
|
||||||
def run(self, path_to_ebook):
|
|
||||||
from calibre.gui2 import is_ok_to_use_qt
|
|
||||||
from PyQt4.Qt import QMessageBox
|
|
||||||
from calibre.ptempfile import PersistentTemporaryDirectory
|
|
||||||
|
|
||||||
import kgenpids
|
|
||||||
import zlib
|
|
||||||
import zipfile
|
|
||||||
import topazextract
|
|
||||||
import mobidedrm
|
|
||||||
|
|
||||||
k4 = True
|
|
||||||
pids = []
|
|
||||||
serials = []
|
|
||||||
kInfoFiles = []
|
|
||||||
|
|
||||||
# Get supplied list of PIDs to try from plugin customization.
|
|
||||||
customvalues = self.site_customization.split(',')
|
|
||||||
for customvalue in customvalues:
|
|
||||||
customvalue = str(customvalue)
|
|
||||||
customvalue = customvalue.strip()
|
|
||||||
if len(customvalue) == 10 or len(customvalue) == 8:
|
|
||||||
pids.append(customvalue)
|
|
||||||
else :
|
|
||||||
if len(customvalue) == 16 and customvalue[0] == 'B':
|
|
||||||
serials.append(customvalue)
|
|
||||||
else:
|
|
||||||
print "%s is not a valid Kindle serial number or PID." % str(customvalue)
|
|
||||||
|
|
||||||
# Load any kindle info files (*.info) included Calibre's config directory.
|
|
||||||
try:
|
|
||||||
# Find Calibre's configuration directory.
|
|
||||||
confpath = os.path.split(os.path.split(self.plugin_path)[0])[0]
|
|
||||||
print 'K4MobiDeDRM: Calibre configuration directory = %s' % confpath
|
|
||||||
files = os.listdir(confpath)
|
|
||||||
filefilter = re.compile("\.info$", re.IGNORECASE)
|
|
||||||
files = filter(filefilter.search, files)
|
|
||||||
|
|
||||||
if files:
|
|
||||||
for filename in files:
|
|
||||||
fpath = os.path.join(confpath, filename)
|
|
||||||
kInfoFiles.append(fpath)
|
|
||||||
print 'K4MobiDeDRM: Kindle info file %s found in config folder.' % filename
|
|
||||||
except IOError:
|
|
||||||
print 'K4MobiDeDRM: Error reading kindle info files from config directory.'
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
mobi = True
|
|
||||||
magic3 = file(path_to_ebook,'rb').read(3)
|
|
||||||
if magic3 == 'TPZ':
|
|
||||||
mobi = False
|
|
||||||
|
|
||||||
bookname = os.path.splitext(os.path.basename(path_to_ebook))[0]
|
|
||||||
|
|
||||||
if mobi:
|
|
||||||
mb = mobidedrm.MobiBook(path_to_ebook)
|
|
||||||
else:
|
|
||||||
tempdir = PersistentTemporaryDirectory()
|
|
||||||
mb = topazextract.TopazBook(path_to_ebook, tempdir)
|
|
||||||
|
|
||||||
title = mb.getBookTitle()
|
|
||||||
md1, md2 = mb.getPIDMetaInfo()
|
|
||||||
pidlst = kgenpids.getPidList(md1, md2, k4, pids, serials, kInfoFiles)
|
|
||||||
|
|
||||||
try:
|
|
||||||
if mobi:
|
|
||||||
unlocked_file = mb.processBook(pidlst)
|
|
||||||
else:
|
|
||||||
mb.processBook(pidlst)
|
|
||||||
|
|
||||||
except mobidedrm.DrmException:
|
|
||||||
#if you reached here then no luck raise and exception
|
|
||||||
if is_ok_to_use_qt():
|
|
||||||
d = QMessageBox(QMessageBox.Warning, "K4MobiDeDRM Plugin", "Error decoding: %s\n" % path_to_ebook)
|
|
||||||
d.show()
|
|
||||||
d.raise_()
|
|
||||||
d.exec_()
|
|
||||||
raise Exception("K4MobiDeDRM plugin could not decode the file")
|
|
||||||
return ""
|
|
||||||
except topazextract.TpzDRMError:
|
|
||||||
#if you reached here then no luck raise and exception
|
|
||||||
if is_ok_to_use_qt():
|
|
||||||
d = QMessageBox(QMessageBox.Warning, "K4MobiDeDRM Plugin", "Error decoding: %s\n" % path_to_ebook)
|
|
||||||
d.show()
|
|
||||||
d.raise_()
|
|
||||||
d.exec_()
|
|
||||||
raise Exception("K4MobiDeDRM plugin could not decode the file")
|
|
||||||
return ""
|
|
||||||
|
|
||||||
print "Success!"
|
|
||||||
if mobi:
|
|
||||||
of = self.temporary_file(bookname+'.mobi')
|
|
||||||
of.write(unlocked_file)
|
|
||||||
of.close()
|
|
||||||
return of.name
|
|
||||||
|
|
||||||
# topaz: build up zip archives of results
|
|
||||||
print " Creating HTML ZIP Archive"
|
|
||||||
of = self.temporary_file(bookname + '.zip')
|
|
||||||
myzip = zipfile.ZipFile(of.name,'w',zipfile.ZIP_DEFLATED, False)
|
|
||||||
myzip.write(os.path.join(tempdir,'book.html'),'book.html')
|
|
||||||
myzip.write(os.path.join(tempdir,'book.opf'),'book.opf')
|
|
||||||
if os.path.isfile(os.path.join(tempdir,'cover.jpg')):
|
|
||||||
myzip.write(os.path.join(tempdir,'cover.jpg'),'cover.jpg')
|
|
||||||
myzip.write(os.path.join(tempdir,'style.css'),'style.css')
|
|
||||||
zipUpDir(myzip, tempdir, 'img')
|
|
||||||
myzip.close()
|
|
||||||
return of.name
|
|
||||||
|
|
||||||
def customization_help(self, gui=False):
|
|
||||||
return 'Enter 10 character PIDs and/or Kindle serial numbers, separated by commas.'
|
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
# standlone set of Mac OSX specific routines needed for K4DeDRM
|
# standlone set of Mac OSX specific routines needed for KindleBooks
|
||||||
|
|
||||||
from __future__ import with_statement
|
from __future__ import with_statement
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
|
import os.path
|
||||||
|
|
||||||
import subprocess
|
import subprocess
|
||||||
|
from struct import pack, unpack, unpack_from
|
||||||
|
|
||||||
|
class DrmException(Exception):
|
||||||
class K4MDrmException(Exception):
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@@ -18,7 +21,7 @@ def _load_crypto_libcrypto():
|
|||||||
|
|
||||||
libcrypto = find_library('crypto')
|
libcrypto = find_library('crypto')
|
||||||
if libcrypto is None:
|
if libcrypto is None:
|
||||||
raise K4MDrmException('libcrypto not found')
|
raise DrmException('libcrypto not found')
|
||||||
libcrypto = CDLL(libcrypto)
|
libcrypto = CDLL(libcrypto)
|
||||||
|
|
||||||
AES_MAXNR = 14
|
AES_MAXNR = 14
|
||||||
@@ -51,27 +54,24 @@ def _load_crypto_libcrypto():
|
|||||||
def set_decrypt_key(self, userkey, iv):
|
def set_decrypt_key(self, userkey, iv):
|
||||||
self._blocksize = len(userkey)
|
self._blocksize = len(userkey)
|
||||||
if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) :
|
if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) :
|
||||||
raise K4MDrmException('AES improper key used')
|
raise DrmException('AES improper key used')
|
||||||
return
|
return
|
||||||
keyctx = self._keyctx = AES_KEY()
|
keyctx = self._keyctx = AES_KEY()
|
||||||
self.iv = iv
|
self.iv = iv
|
||||||
rv = AES_set_decrypt_key(userkey, len(userkey) * 8, keyctx)
|
rv = AES_set_decrypt_key(userkey, len(userkey) * 8, keyctx)
|
||||||
if rv < 0:
|
if rv < 0:
|
||||||
raise K4MDrmException('Failed to initialize AES key')
|
raise DrmException('Failed to initialize AES key')
|
||||||
|
|
||||||
def decrypt(self, data):
|
def decrypt(self, data):
|
||||||
out = create_string_buffer(len(data))
|
out = create_string_buffer(len(data))
|
||||||
rv = AES_cbc_encrypt(data, out, len(data), self._keyctx, self.iv, 0)
|
rv = AES_cbc_encrypt(data, out, len(data), self._keyctx, self.iv, 0)
|
||||||
if rv == 0:
|
if rv == 0:
|
||||||
raise K4MDrmException('AES decryption failed')
|
raise DrmException('AES decryption failed')
|
||||||
return out.raw
|
return out.raw
|
||||||
|
|
||||||
def keyivgen(self, passwd):
|
def keyivgen(self, passwd, salt, iter, keylen):
|
||||||
salt = '16743'
|
saltlen = len(salt)
|
||||||
saltlen = 5
|
|
||||||
passlen = len(passwd)
|
passlen = len(passwd)
|
||||||
iter = 0x3e8
|
|
||||||
keylen = 80
|
|
||||||
out = create_string_buffer(keylen)
|
out = create_string_buffer(keylen)
|
||||||
rv = PKCS5_PBKDF2_HMAC_SHA1(passwd, passlen, salt, saltlen, iter, keylen, out)
|
rv = PKCS5_PBKDF2_HMAC_SHA1(passwd, passlen, salt, saltlen, iter, keylen, out)
|
||||||
return out.raw
|
return out.raw
|
||||||
@@ -81,7 +81,7 @@ def _load_crypto():
|
|||||||
LibCrypto = None
|
LibCrypto = None
|
||||||
try:
|
try:
|
||||||
LibCrypto = _load_crypto_libcrypto()
|
LibCrypto = _load_crypto_libcrypto()
|
||||||
except (ImportError, K4MDrmException):
|
except (ImportError, DrmException):
|
||||||
pass
|
pass
|
||||||
return LibCrypto
|
return LibCrypto
|
||||||
|
|
||||||
@@ -91,13 +91,79 @@ LibCrypto = _load_crypto()
|
|||||||
# Utility Routines
|
# Utility Routines
|
||||||
#
|
#
|
||||||
|
|
||||||
|
# crypto digestroutines
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
def MD5(message):
|
||||||
|
ctx = hashlib.md5()
|
||||||
|
ctx.update(message)
|
||||||
|
return ctx.digest()
|
||||||
|
|
||||||
|
def SHA1(message):
|
||||||
|
ctx = hashlib.sha1()
|
||||||
|
ctx.update(message)
|
||||||
|
return ctx.digest()
|
||||||
|
|
||||||
|
def SHA256(message):
|
||||||
|
ctx = hashlib.sha256()
|
||||||
|
ctx.update(message)
|
||||||
|
return ctx.digest()
|
||||||
|
|
||||||
# Various character maps used to decrypt books. Probably supposed to act as obfuscation
|
# Various character maps used to decrypt books. Probably supposed to act as obfuscation
|
||||||
charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M"
|
charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M"
|
||||||
charMap2 = "ZB0bYyc1xDdW2wEV3Ff7KkPpL8UuGA4gz-Tme9Nn_tHh5SvXCsIiR6rJjQaqlOoM"
|
charMap2 = "ZB0bYyc1xDdW2wEV3Ff7KkPpL8UuGA4gz-Tme9Nn_tHh5SvXCsIiR6rJjQaqlOoM"
|
||||||
charMap3 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
|
||||||
charMap4 = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789"
|
|
||||||
|
|
||||||
|
# For kinf approach of K4PC/K4Mac
|
||||||
|
# On K4PC charMap5 = "AzB0bYyCeVvaZ3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_c1XxDdW2wE"
|
||||||
|
# For Mac they seem to re-use charMap2 here
|
||||||
|
charMap5 = charMap2
|
||||||
|
|
||||||
|
def encode(data, map):
|
||||||
|
result = ""
|
||||||
|
for char in data:
|
||||||
|
value = ord(char)
|
||||||
|
Q = (value ^ 0x80) // len(map)
|
||||||
|
R = value % len(map)
|
||||||
|
result += map[Q]
|
||||||
|
result += map[R]
|
||||||
|
return result
|
||||||
|
|
||||||
|
# Hash the bytes in data and then encode the digest with the characters in map
|
||||||
|
def encodeHash(data,map):
|
||||||
|
return encode(MD5(data),map)
|
||||||
|
|
||||||
|
# Decode the string in data with the characters in map. Returns the decoded bytes
|
||||||
|
def decode(data,map):
|
||||||
|
result = ""
|
||||||
|
for i in range (0,len(data)-1,2):
|
||||||
|
high = map.find(data[i])
|
||||||
|
low = map.find(data[i+1])
|
||||||
|
if (high == -1) or (low == -1) :
|
||||||
|
break
|
||||||
|
value = (((high * len(map)) ^ 0x80) & 0xFF) + low
|
||||||
|
result += pack("B",value)
|
||||||
|
return result
|
||||||
|
|
||||||
|
# For .kinf approach of K4PC and now K4Mac
|
||||||
|
# generate table of prime number less than or equal to int n
|
||||||
|
def primes(n):
|
||||||
|
if n==2: return [2]
|
||||||
|
elif n<2: return []
|
||||||
|
s=range(3,n+1,2)
|
||||||
|
mroot = n ** 0.5
|
||||||
|
half=(n+1)/2-1
|
||||||
|
i=0
|
||||||
|
m=3
|
||||||
|
while m <= mroot:
|
||||||
|
if s[i]:
|
||||||
|
j=(m*m-3)/2
|
||||||
|
s[j]=0
|
||||||
|
while j<half:
|
||||||
|
s[j]=0
|
||||||
|
j+=m
|
||||||
|
i=i+1
|
||||||
|
m=2*i+3
|
||||||
|
return [2]+[x for x in s if x]
|
||||||
|
|
||||||
|
|
||||||
# uses a sub process to get the Hard Drive Serial Number using ioreg
|
# uses a sub process to get the Hard Drive Serial Number using ioreg
|
||||||
@@ -129,48 +195,223 @@ def GetVolumeSerialNumber():
|
|||||||
foundIt = True
|
foundIt = True
|
||||||
break
|
break
|
||||||
if not foundIt:
|
if not foundIt:
|
||||||
sernum = '9999999999'
|
sernum = ''
|
||||||
return sernum
|
return sernum
|
||||||
|
|
||||||
|
def GetUserHomeAppSupKindleDirParitionName():
|
||||||
|
home = os.getenv('HOME')
|
||||||
|
dpath = home + '/Library/Application Support/Kindle'
|
||||||
|
cmdline = '/sbin/mount'
|
||||||
|
cmdline = cmdline.encode(sys.getfilesystemencoding())
|
||||||
|
p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
|
||||||
|
out1, out2 = p.communicate()
|
||||||
|
reslst = out1.split('\n')
|
||||||
|
cnt = len(reslst)
|
||||||
|
disk = ''
|
||||||
|
foundIt = False
|
||||||
|
for j in xrange(cnt):
|
||||||
|
resline = reslst[j]
|
||||||
|
if resline.startswith('/dev'):
|
||||||
|
(devpart, mpath) = resline.split(' on ')
|
||||||
|
dpart = devpart[5:]
|
||||||
|
pp = mpath.find('(')
|
||||||
|
if pp >= 0:
|
||||||
|
mpath = mpath[:pp-1]
|
||||||
|
if dpath.startswith(mpath):
|
||||||
|
disk = dpart
|
||||||
|
return disk
|
||||||
|
|
||||||
|
# uses a sub process to get the UUID of the specified disk partition using ioreg
|
||||||
|
def GetDiskPartitionUUID(diskpart):
|
||||||
|
uuidnum = os.getenv('MYUUIDNUMBER')
|
||||||
|
if uuidnum != None:
|
||||||
|
return uuidnum
|
||||||
|
cmdline = '/usr/sbin/ioreg -l -S -w 0 -r -c AppleAHCIDiskDriver'
|
||||||
|
cmdline = cmdline.encode(sys.getfilesystemencoding())
|
||||||
|
p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
|
||||||
|
out1, out2 = p.communicate()
|
||||||
|
reslst = out1.split('\n')
|
||||||
|
cnt = len(reslst)
|
||||||
|
bsdname = None
|
||||||
|
uuidnum = None
|
||||||
|
foundIt = False
|
||||||
|
nest = 0
|
||||||
|
uuidnest = -1
|
||||||
|
partnest = -2
|
||||||
|
for j in xrange(cnt):
|
||||||
|
resline = reslst[j]
|
||||||
|
if resline.find('{') >= 0:
|
||||||
|
nest += 1
|
||||||
|
if resline.find('}') >= 0:
|
||||||
|
nest -= 1
|
||||||
|
pp = resline.find('"UUID" = "')
|
||||||
|
if pp >= 0:
|
||||||
|
uuidnum = resline[pp+10:-1]
|
||||||
|
uuidnum = uuidnum.strip()
|
||||||
|
uuidnest = nest
|
||||||
|
if partnest == uuidnest and uuidnest > 0:
|
||||||
|
foundIt = True
|
||||||
|
break
|
||||||
|
bb = resline.find('"BSD Name" = "')
|
||||||
|
if bb >= 0:
|
||||||
|
bsdname = resline[bb+14:-1]
|
||||||
|
bsdname = bsdname.strip()
|
||||||
|
if (bsdname == diskpart):
|
||||||
|
partnest = nest
|
||||||
|
else :
|
||||||
|
partnest = -2
|
||||||
|
if partnest == uuidnest and partnest > 0:
|
||||||
|
foundIt = True
|
||||||
|
break
|
||||||
|
if nest == 0:
|
||||||
|
partnest = -2
|
||||||
|
uuidnest = -1
|
||||||
|
uuidnum = None
|
||||||
|
bsdname = None
|
||||||
|
if not foundIt:
|
||||||
|
uuidnum = ''
|
||||||
|
return uuidnum
|
||||||
|
|
||||||
|
def GetMACAddressMunged():
|
||||||
|
macnum = os.getenv('MYMACNUM')
|
||||||
|
if macnum != None:
|
||||||
|
return macnum
|
||||||
|
cmdline = '/sbin/ifconfig en0'
|
||||||
|
cmdline = cmdline.encode(sys.getfilesystemencoding())
|
||||||
|
p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
|
||||||
|
out1, out2 = p.communicate()
|
||||||
|
reslst = out1.split('\n')
|
||||||
|
cnt = len(reslst)
|
||||||
|
macnum = None
|
||||||
|
foundIt = False
|
||||||
|
for j in xrange(cnt):
|
||||||
|
resline = reslst[j]
|
||||||
|
pp = resline.find('ether ')
|
||||||
|
if pp >= 0:
|
||||||
|
macnum = resline[pp+6:-1]
|
||||||
|
macnum = macnum.strip()
|
||||||
|
# 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(':')
|
||||||
|
n = len(maclst)
|
||||||
|
if n != 6:
|
||||||
|
fountIt = False
|
||||||
|
break
|
||||||
|
for i in range(6):
|
||||||
|
maclst[i] = int('0x' + maclst[i], 0)
|
||||||
|
mlst = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
|
||||||
|
mlst[5] = maclst[5] ^ 0xa5
|
||||||
|
mlst[4] = maclst[3] ^ 0xa5
|
||||||
|
mlst[3] = maclst[4] ^ 0xa5
|
||||||
|
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])
|
||||||
|
foundIt = True
|
||||||
|
break
|
||||||
|
if not foundIt:
|
||||||
|
macnum = ''
|
||||||
|
return macnum
|
||||||
|
|
||||||
|
|
||||||
# uses unix env to get username instead of using sysctlbyname
|
# uses unix env to get username instead of using sysctlbyname
|
||||||
def GetUserName():
|
def GetUserName():
|
||||||
username = os.getenv('USER')
|
username = os.getenv('USER')
|
||||||
return username
|
return username
|
||||||
|
|
||||||
|
|
||||||
def encode(data, map):
|
|
||||||
result = ""
|
|
||||||
for char in data:
|
|
||||||
value = ord(char)
|
|
||||||
Q = (value ^ 0x80) // len(map)
|
|
||||||
R = value % len(map)
|
|
||||||
result += map[Q]
|
|
||||||
result += map[R]
|
|
||||||
return result
|
|
||||||
|
|
||||||
import hashlib
|
|
||||||
|
|
||||||
def SHA256(message):
|
|
||||||
ctx = hashlib.sha256()
|
|
||||||
ctx.update(message)
|
|
||||||
return ctx.digest()
|
|
||||||
|
|
||||||
# implements an Pseudo Mac Version of Windows built-in Crypto routine
|
# implements an Pseudo Mac Version of Windows built-in Crypto routine
|
||||||
|
# used by Kindle for Mac versions < 1.6.0
|
||||||
def CryptUnprotectData(encryptedData):
|
def CryptUnprotectData(encryptedData):
|
||||||
sp = GetVolumeSerialNumber() + '!@#' + GetUserName()
|
sernum = GetVolumeSerialNumber()
|
||||||
|
if sernum == '':
|
||||||
|
sernum = '9999999999'
|
||||||
|
sp = sernum + '!@#' + GetUserName()
|
||||||
passwdData = encode(SHA256(sp),charMap1)
|
passwdData = encode(SHA256(sp),charMap1)
|
||||||
|
salt = '16743'
|
||||||
|
iter = 0x3e8
|
||||||
|
keylen = 0x80
|
||||||
crp = LibCrypto()
|
crp = LibCrypto()
|
||||||
key_iv = crp.keyivgen(passwdData)
|
key_iv = crp.keyivgen(passwdData, salt, iter, keylen)
|
||||||
key = key_iv[0:32]
|
key = key_iv[0:32]
|
||||||
iv = key_iv[32:48]
|
iv = key_iv[32:48]
|
||||||
crp.set_decrypt_key(key,iv)
|
crp.set_decrypt_key(key,iv)
|
||||||
cleartext = crp.decrypt(encryptedData)
|
cleartext = crp.decrypt(encryptedData)
|
||||||
|
cleartext = decode(cleartext,charMap1)
|
||||||
return cleartext
|
return cleartext
|
||||||
|
|
||||||
|
|
||||||
# Locate and open the .kindle-info file
|
def isNewInstall():
|
||||||
def openKindleInfo(kInfoFile=None):
|
home = os.getenv('HOME')
|
||||||
if kInfoFile == None:
|
# soccer game fan anyone
|
||||||
|
dpath = home + '/Library/Application Support/Kindle/storage/.pes2011'
|
||||||
|
# print dpath, os.path.exists(dpath)
|
||||||
|
if os.path.exists(dpath):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def GetIDString():
|
||||||
|
# K4Mac now has an extensive set of ids strings it uses
|
||||||
|
# in encoding pids and in creating unique passwords
|
||||||
|
# for use in its own version of CryptUnprotectDataV2
|
||||||
|
|
||||||
|
# BUT Amazon has now become nasty enough to detect when its app
|
||||||
|
# is being run under a debugger and actually changes code paths
|
||||||
|
# including which one of these strings is chosen, all to try
|
||||||
|
# to prevent reverse engineering
|
||||||
|
|
||||||
|
# Sad really ... they will only hurt their own sales ...
|
||||||
|
# true book lovers really want to keep their books forever
|
||||||
|
# and move them to their devices and DRM prevents that so they
|
||||||
|
# will just buy from someplace else that they can remove
|
||||||
|
# the DRM from
|
||||||
|
|
||||||
|
# Amazon should know by now that true book lover's are not like
|
||||||
|
# penniless kids that pirate music, we do not pirate books
|
||||||
|
|
||||||
|
if isNewInstall():
|
||||||
|
mungedmac = GetMACAddressMunged()
|
||||||
|
if len(mungedmac) > 7:
|
||||||
|
return mungedmac
|
||||||
|
sernum = GetVolumeSerialNumber()
|
||||||
|
if len(sernum) > 7:
|
||||||
|
return sernum
|
||||||
|
diskpart = GetUserHomeAppSupKindleDirParitionName()
|
||||||
|
uuidnum = GetDiskPartitionUUID(diskpart)
|
||||||
|
if len(uuidnum) > 7:
|
||||||
|
return uuidnum
|
||||||
|
mungedmac = GetMACAddressMunged()
|
||||||
|
if len(mungedmac) > 7:
|
||||||
|
return mungedmac
|
||||||
|
return '9999999999'
|
||||||
|
|
||||||
|
|
||||||
|
# implements an Pseudo Mac Version of Windows built-in Crypto routine
|
||||||
|
# used for Kindle for Mac Versions >= 1.6.0
|
||||||
|
def CryptUnprotectDataV2(encryptedData):
|
||||||
|
sp = GetUserName() + ':&%:' + GetIDString()
|
||||||
|
passwdData = encode(SHA256(sp),charMap5)
|
||||||
|
# salt generation as per the code
|
||||||
|
salt = 0x0512981d * 2 * 1 * 1
|
||||||
|
salt = str(salt) + GetUserName()
|
||||||
|
salt = encode(salt,charMap5)
|
||||||
|
crp = LibCrypto()
|
||||||
|
iter = 0x800
|
||||||
|
keylen = 0x400
|
||||||
|
key_iv = crp.keyivgen(passwdData, salt, iter, keylen)
|
||||||
|
key = key_iv[0:32]
|
||||||
|
iv = key_iv[32:48]
|
||||||
|
crp.set_decrypt_key(key,iv)
|
||||||
|
cleartext = crp.decrypt(encryptedData)
|
||||||
|
cleartext = decode(cleartext, charMap5)
|
||||||
|
return cleartext
|
||||||
|
|
||||||
|
|
||||||
|
# Locate the .kindle-info files
|
||||||
|
def getKindleInfoFiles(kInfoFiles):
|
||||||
|
# first search for current .kindle-info files
|
||||||
home = os.getenv('HOME')
|
home = os.getenv('HOME')
|
||||||
cmdline = 'find "' + home + '/Library/Application Support" -name ".kindle-info"'
|
cmdline = 'find "' + home + '/Library/Application Support" -name ".kindle-info"'
|
||||||
cmdline = cmdline.encode(sys.getfilesystemencoding())
|
cmdline = cmdline.encode(sys.getfilesystemencoding())
|
||||||
@@ -178,15 +419,133 @@ def openKindleInfo(kInfoFile=None):
|
|||||||
out1, out2 = p1.communicate()
|
out1, out2 = p1.communicate()
|
||||||
reslst = out1.split('\n')
|
reslst = out1.split('\n')
|
||||||
kinfopath = 'NONE'
|
kinfopath = 'NONE'
|
||||||
cnt = len(reslst)
|
found = False
|
||||||
for j in xrange(cnt):
|
for resline in reslst:
|
||||||
resline = reslst[j]
|
if os.path.isfile(resline):
|
||||||
pp = resline.find('.kindle-info')
|
kInfoFiles.append(resline)
|
||||||
if pp >= 0:
|
found = True
|
||||||
kinfopath = resline
|
# add any .kinf files
|
||||||
|
cmdline = 'find "' + home + '/Library/Application Support" -name ".rainier*-kinf"'
|
||||||
|
cmdline = cmdline.encode(sys.getfilesystemencoding())
|
||||||
|
p1 = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
|
||||||
|
out1, out2 = p1.communicate()
|
||||||
|
reslst = out1.split('\n')
|
||||||
|
for resline in reslst:
|
||||||
|
if os.path.isfile(resline):
|
||||||
|
kInfoFiles.append(resline)
|
||||||
|
found = True
|
||||||
|
if not found:
|
||||||
|
print('No kindle-info files have been found.')
|
||||||
|
return kInfoFiles
|
||||||
|
|
||||||
|
# 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"]
|
||||||
|
DB = {}
|
||||||
|
cnt = 0
|
||||||
|
infoReader = open(kInfoFile, 'r')
|
||||||
|
hdr = infoReader.read(1)
|
||||||
|
data = infoReader.read()
|
||||||
|
|
||||||
|
if data.find('[') != -1 :
|
||||||
|
# older style kindle-info file
|
||||||
|
items = data.split('[')
|
||||||
|
for item in items:
|
||||||
|
if item != '':
|
||||||
|
keyhash, rawdata = item.split(':')
|
||||||
|
keyname = "unknown"
|
||||||
|
for name in names:
|
||||||
|
if encodeHash(name,charMap2) == keyhash:
|
||||||
|
keyname = name
|
||||||
break
|
break
|
||||||
if not os.path.exists(kinfopath):
|
if keyname == "unknown":
|
||||||
raise K4MDrmException('Error: .kindle-info file can not be found')
|
keyname = keyhash
|
||||||
return open(kinfopath,'r')
|
encryptedValue = decode(rawdata,charMap2)
|
||||||
else:
|
cleartext = CryptUnprotectData(encryptedValue)
|
||||||
return open(kInfoFile, 'r')
|
DB[keyname] = cleartext
|
||||||
|
cnt = cnt + 1
|
||||||
|
if cnt == 0:
|
||||||
|
DB = None
|
||||||
|
return DB
|
||||||
|
|
||||||
|
# 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
|
||||||
|
data = data[:-1]
|
||||||
|
items = data.split('/')
|
||||||
|
|
||||||
|
# loop through the item records until all are processed
|
||||||
|
while len(items) > 0:
|
||||||
|
|
||||||
|
# get the first item record
|
||||||
|
item = items.pop(0)
|
||||||
|
|
||||||
|
# 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"
|
||||||
|
|
||||||
|
# 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 = SHA1(keyhash)
|
||||||
|
|
||||||
|
# the remainder of the first record when decoded with charMap5
|
||||||
|
# has the ':' split char followed by the string representation
|
||||||
|
# of the number of records that follow
|
||||||
|
# and make up the contents
|
||||||
|
srcnt = decode(item[34:],charMap5)
|
||||||
|
rcnt = int(srcnt)
|
||||||
|
|
||||||
|
# read and store in rcnt records of data
|
||||||
|
# that make up the contents value
|
||||||
|
edlst = []
|
||||||
|
for i in xrange(rcnt):
|
||||||
|
item = items.pop(0)
|
||||||
|
edlst.append(item)
|
||||||
|
|
||||||
|
keyname = "unknown"
|
||||||
|
for name in names:
|
||||||
|
if encodeHash(name,charMap5) == keyhash:
|
||||||
|
keyname = name
|
||||||
|
break
|
||||||
|
if keyname == "unknown":
|
||||||
|
keyname = keyhash
|
||||||
|
|
||||||
|
# the charMap5 encoded contents data has had a length
|
||||||
|
# of chars (always odd) cut off of the front and moved
|
||||||
|
# to the end to prevent decoding using charMap5 from
|
||||||
|
# working properly, and thereby preventing the ensuing
|
||||||
|
# CryptUnprotectData call from succeeding.
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
# move first offsets chars to end to align for decode by charMap5
|
||||||
|
encdata = "".join(edlst)
|
||||||
|
contlen = len(encdata)
|
||||||
|
|
||||||
|
# now properly split and recombine
|
||||||
|
# by moving noffset chars from the start of the
|
||||||
|
# string to the end of the string
|
||||||
|
noffset = contlen - primes(int(contlen/3))[-1]
|
||||||
|
pfx = encdata[0:noffset]
|
||||||
|
encdata = encdata[noffset:]
|
||||||
|
encdata = encdata + pfx
|
||||||
|
|
||||||
|
# decode using charMap5 to get the CryptProtect Data
|
||||||
|
encryptedValue = decode(encdata,charMap5)
|
||||||
|
cleartext = CryptUnprotectDataV2(encryptedValue)
|
||||||
|
# Debugging
|
||||||
|
# print keyname
|
||||||
|
# print cleartext
|
||||||
|
# print cleartext.encode('hex')
|
||||||
|
# print
|
||||||
|
DB[keyname] = cleartext
|
||||||
|
cnt = cnt + 1
|
||||||
|
|
||||||
|
if cnt == 0:
|
||||||
|
DB = None
|
||||||
|
return DB
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
# K4PC Windows specific routines
|
# K4PC Windows specific routines
|
||||||
|
|
||||||
from __future__ import with_statement
|
from __future__ import with_statement
|
||||||
|
|
||||||
import sys, os
|
import sys, os
|
||||||
|
from struct import pack, unpack, unpack_from
|
||||||
|
|
||||||
from ctypes import windll, c_char_p, c_wchar_p, c_uint, POINTER, byref, \
|
from ctypes import windll, c_char_p, c_wchar_p, c_uint, POINTER, byref, \
|
||||||
create_unicode_buffer, create_string_buffer, CFUNCTYPE, addressof, \
|
create_unicode_buffer, create_string_buffer, CFUNCTYPE, addressof, \
|
||||||
@@ -10,30 +12,86 @@ from ctypes import windll, c_char_p, c_wchar_p, c_uint, POINTER, byref, \
|
|||||||
|
|
||||||
import _winreg as winreg
|
import _winreg as winreg
|
||||||
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
MAX_PATH = 255
|
MAX_PATH = 255
|
||||||
|
|
||||||
kernel32 = windll.kernel32
|
kernel32 = windll.kernel32
|
||||||
advapi32 = windll.advapi32
|
advapi32 = windll.advapi32
|
||||||
crypt32 = windll.crypt32
|
crypt32 = windll.crypt32
|
||||||
|
|
||||||
|
import traceback
|
||||||
|
|
||||||
#
|
# crypto digestroutines
|
||||||
# Various character maps used to decrypt books. Probably supposed to act as obfuscation
|
import hashlib
|
||||||
#
|
|
||||||
charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M"
|
def MD5(message):
|
||||||
|
ctx = hashlib.md5()
|
||||||
|
ctx.update(message)
|
||||||
|
return ctx.digest()
|
||||||
|
|
||||||
|
def SHA1(message):
|
||||||
|
ctx = hashlib.sha1()
|
||||||
|
ctx.update(message)
|
||||||
|
return ctx.digest()
|
||||||
|
|
||||||
|
|
||||||
|
# simple primes table (<= n) calculator
|
||||||
|
def primes(n):
|
||||||
|
if n==2: return [2]
|
||||||
|
elif n<2: return []
|
||||||
|
s=range(3,n+1,2)
|
||||||
|
mroot = n ** 0.5
|
||||||
|
half=(n+1)/2-1
|
||||||
|
i=0
|
||||||
|
m=3
|
||||||
|
while m <= mroot:
|
||||||
|
if s[i]:
|
||||||
|
j=(m*m-3)/2
|
||||||
|
s[j]=0
|
||||||
|
while j<half:
|
||||||
|
s[j]=0
|
||||||
|
j+=m
|
||||||
|
i=i+1
|
||||||
|
m=2*i+3
|
||||||
|
return [2]+[x for x in s if x]
|
||||||
|
|
||||||
|
|
||||||
|
# Various character maps used to decrypt kindle info values.
|
||||||
|
# Probably supposed to act as obfuscation
|
||||||
charMap2 = "AaZzB0bYyCc1XxDdW2wEeVv3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_"
|
charMap2 = "AaZzB0bYyCc1XxDdW2wEeVv3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_"
|
||||||
charMap3 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
charMap5 = "AzB0bYyCeVvaZ3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_c1XxDdW2wE"
|
||||||
charMap4 = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789"
|
|
||||||
|
|
||||||
#
|
|
||||||
# Exceptions for all the problems that might happen during the script
|
|
||||||
#
|
|
||||||
class DrmException(Exception):
|
class DrmException(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# Encode the bytes in data with the characters in map
|
||||||
|
def encode(data, map):
|
||||||
|
result = ""
|
||||||
|
for char in data:
|
||||||
|
value = ord(char)
|
||||||
|
Q = (value ^ 0x80) // len(map)
|
||||||
|
R = value % len(map)
|
||||||
|
result += map[Q]
|
||||||
|
result += map[R]
|
||||||
|
return result
|
||||||
|
|
||||||
|
# Hash the bytes in data and then encode the digest with the characters in map
|
||||||
|
def encodeHash(data,map):
|
||||||
|
return encode(MD5(data),map)
|
||||||
|
|
||||||
|
# Decode the string in data with the characters in map. Returns the decoded bytes
|
||||||
|
def decode(data,map):
|
||||||
|
result = ""
|
||||||
|
for i in range (0,len(data)-1,2):
|
||||||
|
high = map.find(data[i])
|
||||||
|
low = map.find(data[i+1])
|
||||||
|
if (high == -1) or (low == -1) :
|
||||||
|
break
|
||||||
|
value = (((high * len(map)) ^ 0x80) & 0xFF) + low
|
||||||
|
result += pack("B",value)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# interface with Windows OS Routines
|
||||||
class DataBlob(Structure):
|
class DataBlob(Structure):
|
||||||
_fields_ = [('cbData', c_uint),
|
_fields_ = [('cbData', c_uint),
|
||||||
('pbData', c_void_p)]
|
('pbData', c_void_p)]
|
||||||
@@ -64,47 +122,187 @@ def GetVolumeSerialNumber():
|
|||||||
return GetVolumeSerialNumber
|
return GetVolumeSerialNumber
|
||||||
GetVolumeSerialNumber = GetVolumeSerialNumber()
|
GetVolumeSerialNumber = GetVolumeSerialNumber()
|
||||||
|
|
||||||
|
def GetIDString():
|
||||||
|
return GetVolumeSerialNumber()
|
||||||
|
|
||||||
|
def getLastError():
|
||||||
|
GetLastError = kernel32.GetLastError
|
||||||
|
GetLastError.argtypes = None
|
||||||
|
GetLastError.restype = c_uint
|
||||||
|
def getLastError():
|
||||||
|
return GetLastError()
|
||||||
|
return getLastError
|
||||||
|
getLastError = getLastError()
|
||||||
|
|
||||||
def GetUserName():
|
def GetUserName():
|
||||||
GetUserNameW = advapi32.GetUserNameW
|
GetUserNameW = advapi32.GetUserNameW
|
||||||
GetUserNameW.argtypes = [c_wchar_p, POINTER(c_uint)]
|
GetUserNameW.argtypes = [c_wchar_p, POINTER(c_uint)]
|
||||||
GetUserNameW.restype = c_uint
|
GetUserNameW.restype = c_uint
|
||||||
def GetUserName():
|
def GetUserName():
|
||||||
buffer = create_unicode_buffer(32)
|
buffer = create_unicode_buffer(2)
|
||||||
size = c_uint(len(buffer))
|
size = c_uint(len(buffer))
|
||||||
while not GetUserNameW(buffer, byref(size)):
|
while not GetUserNameW(buffer, byref(size)):
|
||||||
|
errcd = getLastError()
|
||||||
|
if errcd == 234:
|
||||||
|
# bad wine implementation up through wine 1.3.21
|
||||||
|
return "AlternateUserName"
|
||||||
buffer = create_unicode_buffer(len(buffer) * 2)
|
buffer = create_unicode_buffer(len(buffer) * 2)
|
||||||
size.value = len(buffer)
|
size.value = len(buffer)
|
||||||
return buffer.value.encode('utf-16-le')[::2]
|
return buffer.value.encode('utf-16-le')[::2]
|
||||||
return GetUserName
|
return GetUserName
|
||||||
GetUserName = GetUserName()
|
GetUserName = GetUserName()
|
||||||
|
|
||||||
|
|
||||||
def CryptUnprotectData():
|
def CryptUnprotectData():
|
||||||
_CryptUnprotectData = crypt32.CryptUnprotectData
|
_CryptUnprotectData = crypt32.CryptUnprotectData
|
||||||
_CryptUnprotectData.argtypes = [DataBlob_p, c_wchar_p, DataBlob_p,
|
_CryptUnprotectData.argtypes = [DataBlob_p, c_wchar_p, DataBlob_p,
|
||||||
c_void_p, c_void_p, c_uint, DataBlob_p]
|
c_void_p, c_void_p, c_uint, DataBlob_p]
|
||||||
_CryptUnprotectData.restype = c_uint
|
_CryptUnprotectData.restype = c_uint
|
||||||
def CryptUnprotectData(indata, entropy):
|
def CryptUnprotectData(indata, entropy, flags):
|
||||||
indatab = create_string_buffer(indata)
|
indatab = create_string_buffer(indata)
|
||||||
indata = DataBlob(len(indata), cast(indatab, c_void_p))
|
indata = DataBlob(len(indata), cast(indatab, c_void_p))
|
||||||
entropyb = create_string_buffer(entropy)
|
entropyb = create_string_buffer(entropy)
|
||||||
entropy = DataBlob(len(entropy), cast(entropyb, c_void_p))
|
entropy = DataBlob(len(entropy), cast(entropyb, c_void_p))
|
||||||
outdata = DataBlob()
|
outdata = DataBlob()
|
||||||
if not _CryptUnprotectData(byref(indata), None, byref(entropy),
|
if not _CryptUnprotectData(byref(indata), None, byref(entropy),
|
||||||
None, None, 0, byref(outdata)):
|
None, None, flags, byref(outdata)):
|
||||||
raise DrmException("Failed to Unprotect Data")
|
raise DrmException("Failed to Unprotect Data")
|
||||||
return string_at(outdata.pbData, outdata.cbData)
|
return string_at(outdata.pbData, outdata.cbData)
|
||||||
return CryptUnprotectData
|
return CryptUnprotectData
|
||||||
CryptUnprotectData = CryptUnprotectData()
|
CryptUnprotectData = CryptUnprotectData()
|
||||||
|
|
||||||
#
|
|
||||||
# Locate and open the Kindle.info file.
|
# Locate all of the kindle-info style files and return as list
|
||||||
#
|
def getKindleInfoFiles(kInfoFiles):
|
||||||
def openKindleInfo(kInfoFile=None):
|
|
||||||
if kInfoFile == None:
|
|
||||||
regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\")
|
regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\")
|
||||||
path = winreg.QueryValueEx(regkey, 'Local AppData')[0]
|
path = winreg.QueryValueEx(regkey, 'Local AppData')[0]
|
||||||
return open(path+'\\Amazon\\Kindle For PC\\{AMAwzsaPaaZAzmZzZQzgZCAkZ3AjA_AY}\\kindle.info','r')
|
|
||||||
|
# first look for older kindle-info files
|
||||||
|
kinfopath = path +'\\Amazon\\Kindle For PC\\{AMAwzsaPaaZAzmZzZQzgZCAkZ3AjA_AY}\\kindle.info'
|
||||||
|
if not os.path.isfile(kinfopath):
|
||||||
|
print('No kindle.info files have not been found.')
|
||||||
else:
|
else:
|
||||||
return open(kInfoFile, 'r')
|
kInfoFiles.append(kinfopath)
|
||||||
|
|
||||||
|
# now look for newer (K4PC 1.5.0 and later rainier.2.1.1.kinf file
|
||||||
|
|
||||||
|
kinfopath = path +'\\Amazon\\Kindle For PC\\storage\\rainier.2.1.1.kinf'
|
||||||
|
if not os.path.isfile(kinfopath):
|
||||||
|
print('No K4PC 1.5.X .kinf files have not been found.')
|
||||||
|
else:
|
||||||
|
kInfoFiles.append(kinfopath)
|
||||||
|
|
||||||
|
# now look for even newer (K4PC 1.6.0 and later) rainier.2.1.1.kinf file
|
||||||
|
kinfopath = path +'\\Amazon\\Kindle\\storage\\rainier.2.1.1.kinf'
|
||||||
|
if not os.path.isfile(kinfopath):
|
||||||
|
print('No K4PC 1.6.X .kinf files have not been found.')
|
||||||
|
else:
|
||||||
|
kInfoFiles.append(kinfopath)
|
||||||
|
|
||||||
|
return kInfoFiles
|
||||||
|
|
||||||
|
|
||||||
|
# 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"]
|
||||||
|
DB = {}
|
||||||
|
cnt = 0
|
||||||
|
infoReader = open(kInfoFile, 'r')
|
||||||
|
hdr = infoReader.read(1)
|
||||||
|
data = infoReader.read()
|
||||||
|
|
||||||
|
if data.find('{') != -1 :
|
||||||
|
|
||||||
|
# older style kindle-info file
|
||||||
|
items = data.split('{')
|
||||||
|
for item in items:
|
||||||
|
if item != '':
|
||||||
|
keyhash, rawdata = item.split(':')
|
||||||
|
keyname = "unknown"
|
||||||
|
for name in names:
|
||||||
|
if encodeHash(name,charMap2) == keyhash:
|
||||||
|
keyname = name
|
||||||
|
break
|
||||||
|
if keyname == "unknown":
|
||||||
|
keyname = keyhash
|
||||||
|
encryptedValue = decode(rawdata,charMap2)
|
||||||
|
DB[keyname] = CryptUnprotectData(encryptedValue, "", 0)
|
||||||
|
cnt = cnt + 1
|
||||||
|
if cnt == 0:
|
||||||
|
DB = None
|
||||||
|
return DB
|
||||||
|
|
||||||
|
# else newer style .kinf file
|
||||||
|
# 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('/')
|
||||||
|
|
||||||
|
# loop through the item records until all are processed
|
||||||
|
while len(items) > 0:
|
||||||
|
|
||||||
|
# get the first item record
|
||||||
|
item = items.pop(0)
|
||||||
|
|
||||||
|
# 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]
|
||||||
|
|
||||||
|
# the raw keyhash string is also used to create entropy for the actual
|
||||||
|
# CryptProtectData Blob that represents that keys contents
|
||||||
|
entropy = SHA1(keyhash)
|
||||||
|
|
||||||
|
# the remainder of the first record when decoded with charMap5
|
||||||
|
# has the ':' split char followed by the string representation
|
||||||
|
# of the number of records that follow
|
||||||
|
# and make up the contents
|
||||||
|
srcnt = decode(item[34:],charMap5)
|
||||||
|
rcnt = int(srcnt)
|
||||||
|
|
||||||
|
# read and store in rcnt records of data
|
||||||
|
# that make up the contents value
|
||||||
|
edlst = []
|
||||||
|
for i in xrange(rcnt):
|
||||||
|
item = items.pop(0)
|
||||||
|
edlst.append(item)
|
||||||
|
|
||||||
|
keyname = "unknown"
|
||||||
|
for name in names:
|
||||||
|
if encodeHash(name,charMap5) == keyhash:
|
||||||
|
keyname = name
|
||||||
|
break
|
||||||
|
if keyname == "unknown":
|
||||||
|
keyname = keyhash
|
||||||
|
|
||||||
|
# the charMap5 encoded contents data has had a length
|
||||||
|
# of chars (always odd) cut off of the front and moved
|
||||||
|
# to the end to prevent decoding using charMap5 from
|
||||||
|
# working properly, and thereby preventing the ensuing
|
||||||
|
# CryptUnprotectData call from succeeding.
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
# move first offsets chars to end to align for decode by charMap5
|
||||||
|
encdata = "".join(edlst)
|
||||||
|
contlen = len(encdata)
|
||||||
|
noffset = contlen - primes(int(contlen/3))[-1]
|
||||||
|
|
||||||
|
# now properly split and recombine
|
||||||
|
# by moving noffset chars from the start of the
|
||||||
|
# string to the end of the string
|
||||||
|
pfx = encdata[0:noffset]
|
||||||
|
encdata = encdata[noffset:]
|
||||||
|
encdata = encdata + pfx
|
||||||
|
|
||||||
|
# decode using Map5 to get the CryptProtect Data
|
||||||
|
encryptedValue = decode(encdata,charMap5)
|
||||||
|
DB[keyname] = CryptUnprotectData(encryptedValue, entropy, 1)
|
||||||
|
cnt = cnt + 1
|
||||||
|
|
||||||
|
if cnt == 0:
|
||||||
|
DB = None
|
||||||
|
return DB
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -11,16 +11,28 @@ from struct import pack, unpack, unpack_from
|
|||||||
class DrmException(Exception):
|
class DrmException(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
global kindleDatabase
|
|
||||||
global charMap1
|
global charMap1
|
||||||
global charMap2
|
|
||||||
global charMap3
|
global charMap3
|
||||||
global charMap4
|
global charMap4
|
||||||
|
|
||||||
|
if 'calibre' in sys.modules:
|
||||||
|
inCalibre = True
|
||||||
|
else:
|
||||||
|
inCalibre = False
|
||||||
|
|
||||||
|
if inCalibre:
|
||||||
if sys.platform.startswith('win'):
|
if sys.platform.startswith('win'):
|
||||||
from k4pcutils import openKindleInfo, CryptUnprotectData, GetUserName, GetVolumeSerialNumber, charMap2
|
from calibre_plugins.k4mobidedrm.k4pcutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString
|
||||||
|
|
||||||
if sys.platform.startswith('darwin'):
|
if sys.platform.startswith('darwin'):
|
||||||
from k4mutils import openKindleInfo, CryptUnprotectData, GetUserName, GetVolumeSerialNumber, charMap2
|
from calibre_plugins.k4mobidedrm.k4mutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString
|
||||||
|
else:
|
||||||
|
if sys.platform.startswith('win'):
|
||||||
|
from k4pcutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString
|
||||||
|
|
||||||
|
if sys.platform.startswith('darwin'):
|
||||||
|
from k4mutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString
|
||||||
|
|
||||||
|
|
||||||
charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M"
|
charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M"
|
||||||
charMap3 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
charMap3 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
||||||
@@ -67,62 +79,6 @@ def decode(data,map):
|
|||||||
result += pack("B",value)
|
result += pack("B",value)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
# Parse the Kindle.info file and return the records as a list of key-values
|
|
||||||
def parseKindleInfo(kInfoFile):
|
|
||||||
DB = {}
|
|
||||||
infoReader = openKindleInfo(kInfoFile)
|
|
||||||
infoReader.read(1)
|
|
||||||
data = infoReader.read()
|
|
||||||
if sys.platform.startswith('win'):
|
|
||||||
items = data.split('{')
|
|
||||||
else :
|
|
||||||
items = data.split('[')
|
|
||||||
for item in items:
|
|
||||||
splito = item.split(':')
|
|
||||||
DB[splito[0]] =splito[1]
|
|
||||||
return DB
|
|
||||||
|
|
||||||
# Get a record from the Kindle.info file for the key "hashedKey" (already hashed and encoded). Return the decoded and decrypted record
|
|
||||||
def getKindleInfoValueForHash(hashedKey):
|
|
||||||
global kindleDatabase
|
|
||||||
global charMap1
|
|
||||||
global charMap2
|
|
||||||
encryptedValue = decode(kindleDatabase[hashedKey],charMap2)
|
|
||||||
if sys.platform.startswith('win'):
|
|
||||||
return CryptUnprotectData(encryptedValue,"")
|
|
||||||
else:
|
|
||||||
cleartext = CryptUnprotectData(encryptedValue)
|
|
||||||
return decode(cleartext, charMap1)
|
|
||||||
|
|
||||||
# Get a record from the Kindle.info file for the string in "key" (plaintext). Return the decoded and decrypted record
|
|
||||||
def getKindleInfoValueForKey(key):
|
|
||||||
global charMap2
|
|
||||||
return getKindleInfoValueForHash(encodeHash(key,charMap2))
|
|
||||||
|
|
||||||
# Find if the original string for a hashed/encoded string is known. If so return the original string othwise return an empty string.
|
|
||||||
def findNameForHash(hash):
|
|
||||||
global charMap2
|
|
||||||
names = ["kindle.account.tokens","kindle.cookie.item","eulaVersionAccepted","login_date","kindle.token.item","login","kindle.key.item","kindle.name.info","kindle.device.info", "MazamaRandomNumber"]
|
|
||||||
result = ""
|
|
||||||
for name in names:
|
|
||||||
if hash == encodeHash(name, charMap2):
|
|
||||||
result = name
|
|
||||||
break
|
|
||||||
return result
|
|
||||||
|
|
||||||
# Print all the records from the kindle.info file (option -i)
|
|
||||||
def printKindleInfo():
|
|
||||||
for record in kindleDatabase:
|
|
||||||
name = findNameForHash(record)
|
|
||||||
if name != "" :
|
|
||||||
print (name)
|
|
||||||
print ("--------------------------")
|
|
||||||
else :
|
|
||||||
print ("Unknown Record")
|
|
||||||
print getKindleInfoValueForHash(record)
|
|
||||||
print "\n"
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# PID generation routines
|
# PID generation routines
|
||||||
#
|
#
|
||||||
@@ -221,8 +177,6 @@ def pidFromSerial(s, l):
|
|||||||
|
|
||||||
# Parse the EXTH header records and use the Kindle serial number to calculate the book pid.
|
# Parse the EXTH header records and use the Kindle serial number to calculate the book pid.
|
||||||
def getKindlePid(pidlst, rec209, token, serialnum):
|
def getKindlePid(pidlst, rec209, token, serialnum):
|
||||||
|
|
||||||
if rec209 != None:
|
|
||||||
# Compute book PID
|
# Compute book PID
|
||||||
pidHash = SHA1(serialnum+rec209+token)
|
pidHash = SHA1(serialnum+rec209+token)
|
||||||
bookPID = encodePID(pidHash)
|
bookPID = encodePID(pidHash)
|
||||||
@@ -237,33 +191,41 @@ def getKindlePid(pidlst, rec209, token, serialnum):
|
|||||||
return pidlst
|
return pidlst
|
||||||
|
|
||||||
|
|
||||||
# Parse the EXTH header records and parse the Kindleinfo
|
# parse the Kindleinfo file to calculate the book pid.
|
||||||
# file to calculate the book pid.
|
|
||||||
|
|
||||||
def getK4Pids(pidlst, rec209, token, kInfoFile=None):
|
keynames = ["kindle.account.tokens","kindle.cookie.item","eulaVersionAccepted","login_date","kindle.token.item","login","kindle.key.item","kindle.name.info","kindle.device.info", "MazamaRandomNumber"]
|
||||||
global kindleDatabase
|
|
||||||
|
def getK4Pids(pidlst, rec209, token, kInfoFile):
|
||||||
global charMap1
|
global charMap1
|
||||||
kindleDatabase = None
|
kindleDatabase = None
|
||||||
try:
|
try:
|
||||||
kindleDatabase = parseKindleInfo(kInfoFile)
|
kindleDatabase = getDBfromFile(kInfoFile)
|
||||||
except Exception, message:
|
except Exception, message:
|
||||||
print(message)
|
print(message)
|
||||||
|
kindleDatabase = None
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if kindleDatabase == None :
|
if kindleDatabase == None :
|
||||||
return pidlst
|
return pidlst
|
||||||
|
|
||||||
|
try:
|
||||||
# Get the Mazama Random number
|
# Get the Mazama Random number
|
||||||
MazamaRandomNumber = getKindleInfoValueForKey("MazamaRandomNumber")
|
MazamaRandomNumber = kindleDatabase["MazamaRandomNumber"]
|
||||||
|
|
||||||
# Get the HDD serial
|
# Get the kindle account token
|
||||||
encodedSystemVolumeSerialNumber = encodeHash(GetVolumeSerialNumber(),charMap1)
|
kindleAccountToken = kindleDatabase["kindle.account.tokens"]
|
||||||
|
except KeyError:
|
||||||
|
print "Keys not found in " + kInfoFile
|
||||||
|
return pidlst
|
||||||
|
|
||||||
|
# Get the ID string used
|
||||||
|
encodedIDString = encodeHash(GetIDString(),charMap1)
|
||||||
|
|
||||||
# Get the current user name
|
# Get the current user name
|
||||||
encodedUsername = encodeHash(GetUserName(),charMap1)
|
encodedUsername = encodeHash(GetUserName(),charMap1)
|
||||||
|
|
||||||
# concat, hash and encode to calculate the DSN
|
# concat, hash and encode to calculate the DSN
|
||||||
DSN = encode(SHA1(MazamaRandomNumber+encodedSystemVolumeSerialNumber+encodedUsername),charMap1)
|
DSN = encode(SHA1(MazamaRandomNumber+encodedIDString+encodedUsername),charMap1)
|
||||||
|
|
||||||
# Compute the device PID (for which I can tell, is used for nothing).
|
# Compute the device PID (for which I can tell, is used for nothing).
|
||||||
table = generatePidEncryptionTable()
|
table = generatePidEncryptionTable()
|
||||||
@@ -271,13 +233,7 @@ def getK4Pids(pidlst, rec209, token, kInfoFile=None):
|
|||||||
devicePID = checksumPid(devicePID)
|
devicePID = checksumPid(devicePID)
|
||||||
pidlst.append(devicePID)
|
pidlst.append(devicePID)
|
||||||
|
|
||||||
# Compute book PID
|
# Compute book PIDs
|
||||||
if rec209 == None:
|
|
||||||
print "\nNo EXTH record type 209 - Perhaps not a K4 file?"
|
|
||||||
return pidlst
|
|
||||||
|
|
||||||
# Get the kindle account token
|
|
||||||
kindleAccountToken = getKindleInfoValueForKey("kindle.account.tokens")
|
|
||||||
|
|
||||||
# book pid
|
# book pid
|
||||||
pidHash = SHA1(DSN+kindleAccountToken+rec209+token)
|
pidHash = SHA1(DSN+kindleAccountToken+rec209+token)
|
||||||
@@ -301,8 +257,10 @@ def getK4Pids(pidlst, rec209, token, kInfoFile=None):
|
|||||||
|
|
||||||
def getPidList(md1, md2, k4, pids, serials, kInfoFiles):
|
def getPidList(md1, md2, k4, pids, serials, kInfoFiles):
|
||||||
pidlst = []
|
pidlst = []
|
||||||
|
if kInfoFiles is None:
|
||||||
|
kInfoFiles = []
|
||||||
if k4:
|
if k4:
|
||||||
pidlst = getK4Pids(pidlst, md1, md2)
|
kInfoFiles = getKindleInfoFiles(kInfoFiles)
|
||||||
for infoFile in kInfoFiles:
|
for infoFile in kInfoFiles:
|
||||||
pidlst = getK4Pids(pidlst, md1, md2, infoFile)
|
pidlst = getK4Pids(pidlst, md1, md2, infoFile)
|
||||||
for serialnum in serials:
|
for serialnum in serials:
|
||||||
|
|||||||
@@ -42,8 +42,19 @@
|
|||||||
# 0.20 - Correction: It seems that multibyte entries are encrypted in a v6 file.
|
# 0.20 - Correction: It seems that multibyte entries are encrypted in a v6 file.
|
||||||
# 0.21 - Added support for multiple pids
|
# 0.21 - Added support for multiple pids
|
||||||
# 0.22 - revised structure to hold MobiBook as a class to allow an extended interface
|
# 0.22 - revised structure to hold MobiBook as a class to allow an extended interface
|
||||||
|
# 0.23 - fixed problem with older files with no EXTH section
|
||||||
|
# 0.24 - add support for type 1 encryption and 'TEXtREAd' books as well
|
||||||
|
# 0.25 - Fixed support for 'BOOKMOBI' type 1 encryption
|
||||||
|
# 0.26 - Now enables Text-To-Speech flag and sets clipping limit to 100%
|
||||||
|
# 0.27 - Correct pid metadata token generation to match that used by skindle (Thank You Bart!)
|
||||||
|
# 0.28 - slight additional changes to metadata token generation (None -> '')
|
||||||
|
# 0.29 - It seems that the ideas about when multibyte trailing characters were
|
||||||
|
# included in the encryption were wrong. They are for DOC compressed
|
||||||
|
# files, but they are not for HUFF/CDIC compress files!
|
||||||
|
# 0.30 - Modified interface slightly to work better with new calibre plugin style
|
||||||
|
# 0.31 - The multibyte encrytion info is true for version 7 files too.
|
||||||
|
|
||||||
__version__ = '0.22'
|
__version__ = '0.31'
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
@@ -57,6 +68,7 @@ class Unbuffered:
|
|||||||
return getattr(self.stream, attr)
|
return getattr(self.stream, attr)
|
||||||
sys.stdout=Unbuffered(sys.stdout)
|
sys.stdout=Unbuffered(sys.stdout)
|
||||||
|
|
||||||
|
import os
|
||||||
import struct
|
import struct
|
||||||
import binascii
|
import binascii
|
||||||
|
|
||||||
@@ -153,9 +165,12 @@ class MobiBook:
|
|||||||
def __init__(self, infile):
|
def __init__(self, infile):
|
||||||
# initial sanity check on file
|
# initial sanity check on file
|
||||||
self.data_file = file(infile, 'rb').read()
|
self.data_file = file(infile, 'rb').read()
|
||||||
|
self.mobi_data = ''
|
||||||
self.header = self.data_file[0:78]
|
self.header = self.data_file[0:78]
|
||||||
if self.header[0x3C:0x3C+8] != 'BOOKMOBI':
|
if self.header[0x3C:0x3C+8] != 'BOOKMOBI' and self.header[0x3C:0x3C+8] != 'TEXtREAd':
|
||||||
raise DrmException("invalid file format")
|
raise DrmException("invalid file format")
|
||||||
|
self.magic = self.header[0x3C:0x3C+8]
|
||||||
|
self.crypto_type = -1
|
||||||
|
|
||||||
# build up section offset and flag info
|
# build up section offset and flag info
|
||||||
self.num_sections, = struct.unpack('>H', self.header[76:78])
|
self.num_sections, = struct.unpack('>H', self.header[76:78])
|
||||||
@@ -168,6 +183,15 @@ class MobiBook:
|
|||||||
# parse information from section 0
|
# parse information from section 0
|
||||||
self.sect = self.loadSection(0)
|
self.sect = self.loadSection(0)
|
||||||
self.records, = struct.unpack('>H', self.sect[0x8:0x8+2])
|
self.records, = struct.unpack('>H', self.sect[0x8:0x8+2])
|
||||||
|
self.compression, = struct.unpack('>H', self.sect[0x0:0x0+2])
|
||||||
|
|
||||||
|
if self.magic == 'TEXtREAd':
|
||||||
|
print "Book has format: ", self.magic
|
||||||
|
self.extra_data_flags = 0
|
||||||
|
self.mobi_length = 0
|
||||||
|
self.mobi_version = -1
|
||||||
|
self.meta_array = {}
|
||||||
|
return
|
||||||
self.mobi_length, = struct.unpack('>L',self.sect[0x14:0x18])
|
self.mobi_length, = struct.unpack('>L',self.sect[0x14:0x18])
|
||||||
self.mobi_version, = struct.unpack('>L',self.sect[0x68:0x6C])
|
self.mobi_version, = struct.unpack('>L',self.sect[0x68:0x6C])
|
||||||
print "MOBI header version = %d, length = %d" %(self.mobi_version, self.mobi_length)
|
print "MOBI header version = %d, length = %d" %(self.mobi_version, self.mobi_length)
|
||||||
@@ -175,24 +199,37 @@ class MobiBook:
|
|||||||
if (self.mobi_length >= 0xE4) and (self.mobi_version >= 5):
|
if (self.mobi_length >= 0xE4) and (self.mobi_version >= 5):
|
||||||
self.extra_data_flags, = struct.unpack('>H', self.sect[0xF2:0xF4])
|
self.extra_data_flags, = struct.unpack('>H', self.sect[0xF2:0xF4])
|
||||||
print "Extra Data Flags = %d" % self.extra_data_flags
|
print "Extra Data Flags = %d" % self.extra_data_flags
|
||||||
if self.mobi_version < 7:
|
if (self.compression != 17480):
|
||||||
# multibyte utf8 data is included in the encryption for mobi_version 6 and below
|
# multibyte utf8 data is included in the encryption for PalmDoc compression
|
||||||
# so clear that byte so that we leave it to be decrypted.
|
# so clear that byte so that we leave it to be decrypted.
|
||||||
self.extra_data_flags &= 0xFFFE
|
self.extra_data_flags &= 0xFFFE
|
||||||
|
|
||||||
# if exth region exists parse it for metadata array
|
# if exth region exists parse it for metadata array
|
||||||
self.meta_array = {}
|
self.meta_array = {}
|
||||||
|
try:
|
||||||
exth_flag, = struct.unpack('>L', self.sect[0x80:0x84])
|
exth_flag, = struct.unpack('>L', self.sect[0x80:0x84])
|
||||||
exth = ''
|
exth = 'NONE'
|
||||||
if exth_flag & 0x40:
|
if exth_flag & 0x40:
|
||||||
exth = self.sect[16 + self.mobi_length:]
|
exth = self.sect[16 + self.mobi_length:]
|
||||||
|
if (len(exth) >= 4) and (exth[:4] == 'EXTH'):
|
||||||
nitems, = struct.unpack('>I', exth[8:12])
|
nitems, = struct.unpack('>I', exth[8:12])
|
||||||
pos = 12
|
pos = 12
|
||||||
for i in xrange(nitems):
|
for i in xrange(nitems):
|
||||||
type, size = struct.unpack('>II', exth[pos: pos + 8])
|
type, size = struct.unpack('>II', exth[pos: pos + 8])
|
||||||
content = exth[pos + 8: pos + size]
|
content = exth[pos + 8: pos + size]
|
||||||
self.meta_array[type] = content
|
self.meta_array[type] = content
|
||||||
|
# reset the text to speech flag and clipping limit, if present
|
||||||
|
if type == 401 and size == 9:
|
||||||
|
# set clipping limit to 100%
|
||||||
|
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)
|
||||||
|
# print type, size, content, content.encode('hex')
|
||||||
pos += size
|
pos += size
|
||||||
|
except:
|
||||||
|
self.meta_array = {}
|
||||||
|
pass
|
||||||
|
|
||||||
def getBookTitle(self):
|
def getBookTitle(self):
|
||||||
title = ''
|
title = ''
|
||||||
@@ -208,18 +245,18 @@ class MobiBook:
|
|||||||
return title
|
return title
|
||||||
|
|
||||||
def getPIDMetaInfo(self):
|
def getPIDMetaInfo(self):
|
||||||
rec209 = None
|
rec209 = ''
|
||||||
token = None
|
token = ''
|
||||||
if 209 in self.meta_array:
|
if 209 in self.meta_array:
|
||||||
rec209 = self.meta_array[209]
|
rec209 = self.meta_array[209]
|
||||||
data = rec209
|
data = rec209
|
||||||
# Parse the 209 data to find the the exth record with the token data.
|
# The 209 data comes in five byte groups. Interpret the last four bytes
|
||||||
# The last character of the 209 data points to the record with the token.
|
# of each group as a big endian unsigned integer to get a key value
|
||||||
# Always 208 from my experience, but I'll leave the logic in case that changes.
|
# if that key exists in the meta_array, append its contents to the token
|
||||||
for i in xrange(len(data)):
|
for i in xrange(0,len(data),5):
|
||||||
if ord(data[i]) != 0:
|
val, = struct.unpack('>I',data[i+1:i+5])
|
||||||
if self.meta_array[ord(data[i])] != None:
|
sval = self.meta_array.get(val,'')
|
||||||
token = self.meta_array[ord(data[i])]
|
token += sval
|
||||||
return rec209, token
|
return rec209, token
|
||||||
|
|
||||||
def patch(self, off, new):
|
def patch(self, off, new):
|
||||||
@@ -267,14 +304,18 @@ class MobiBook:
|
|||||||
break
|
break
|
||||||
return [found_key,pid]
|
return [found_key,pid]
|
||||||
|
|
||||||
|
def getMobiFile(self, outpath):
|
||||||
|
file(outpath,'wb').write(self.mobi_data)
|
||||||
|
|
||||||
def processBook(self, pidlist):
|
def processBook(self, pidlist):
|
||||||
crypto_type, = struct.unpack('>H', self.sect[0xC:0xC+2])
|
crypto_type, = struct.unpack('>H', self.sect[0xC:0xC+2])
|
||||||
|
print 'Crypto Type is: ', crypto_type
|
||||||
|
self.crypto_type = crypto_type
|
||||||
if crypto_type == 0:
|
if crypto_type == 0:
|
||||||
print "This book is not encrypted."
|
print "This book is not encrypted."
|
||||||
return self.data_file
|
self.mobi_data = self.data_file
|
||||||
if crypto_type == 1:
|
return
|
||||||
raise DrmException("Cannot decode Mobipocket encryption type 1")
|
if crypto_type != 2 and crypto_type != 1:
|
||||||
if crypto_type != 2:
|
|
||||||
raise DrmException("Cannot decode unknown Mobipocket encryption type %d" % crypto_type)
|
raise DrmException("Cannot decode unknown Mobipocket encryption type %d" % crypto_type)
|
||||||
|
|
||||||
goodpids = []
|
goodpids = []
|
||||||
@@ -286,6 +327,17 @@ class MobiBook:
|
|||||||
elif len(pid)==8:
|
elif len(pid)==8:
|
||||||
goodpids.append(pid)
|
goodpids.append(pid)
|
||||||
|
|
||||||
|
if self.crypto_type == 1:
|
||||||
|
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"
|
||||||
|
found_key = PC1(t1_keyvec, bookkey_data)
|
||||||
|
else :
|
||||||
# calculate the keys
|
# calculate the keys
|
||||||
drm_ptr, drm_count, drm_size, drm_flags = struct.unpack('>LLLL', self.sect[0xA8:0xA8+16])
|
drm_ptr, drm_count, drm_size, drm_flags = struct.unpack('>LLLL', self.sect[0xA8:0xA8+16])
|
||||||
if drm_count == 0:
|
if drm_count == 0:
|
||||||
@@ -293,61 +345,66 @@ class MobiBook:
|
|||||||
found_key, pid = self.parseDRM(self.sect[drm_ptr:drm_ptr+drm_size], drm_count, goodpids)
|
found_key, pid = self.parseDRM(self.sect[drm_ptr:drm_ptr+drm_size], drm_count, goodpids)
|
||||||
if not found_key:
|
if not found_key:
|
||||||
raise DrmException("No key found. Most likely the correct PID has not been given.")
|
raise DrmException("No key found. Most likely the correct PID has not been given.")
|
||||||
|
# kill the drm keys
|
||||||
|
self.patchSection(0, "\0" * drm_size, drm_ptr)
|
||||||
|
# kill the drm pointers
|
||||||
|
self.patchSection(0, "\xff" * 4 + "\0" * 12, 0xA8)
|
||||||
|
|
||||||
if pid=="00000000":
|
if pid=="00000000":
|
||||||
print "File has default encryption, no specific PID."
|
print "File has default encryption, no specific PID."
|
||||||
else:
|
else:
|
||||||
print "File is encoded with PID "+checksumPid(pid)+"."
|
print "File is encoded with PID "+checksumPid(pid)+"."
|
||||||
|
|
||||||
# kill the drm keys
|
|
||||||
self.patchSection(0, "\0" * drm_size, drm_ptr)
|
|
||||||
# kill the drm pointers
|
|
||||||
self.patchSection(0, "\xff" * 4 + "\0" * 12, 0xA8)
|
|
||||||
# clear the crypto type
|
# clear the crypto type
|
||||||
self.patchSection(0, "\0" * 2, 0xC)
|
self.patchSection(0, "\0" * 2, 0xC)
|
||||||
|
|
||||||
# decrypt sections
|
# decrypt sections
|
||||||
print "Decrypting. Please wait . . .",
|
print "Decrypting. Please wait . . .",
|
||||||
new_data = self.data_file[:self.sections[1][0]]
|
self.mobi_data = self.data_file[:self.sections[1][0]]
|
||||||
for i in xrange(1, self.records+1):
|
for i in xrange(1, self.records+1):
|
||||||
data = self.loadSection(i)
|
data = self.loadSection(i)
|
||||||
extra_size = getSizeOfTrailingDataEntries(data, len(data), self.extra_data_flags)
|
extra_size = getSizeOfTrailingDataEntries(data, len(data), self.extra_data_flags)
|
||||||
if i%100 == 0:
|
if i%100 == 0:
|
||||||
print ".",
|
print ".",
|
||||||
# print "record %d, extra_size %d" %(i,extra_size)
|
# print "record %d, extra_size %d" %(i,extra_size)
|
||||||
new_data += PC1(found_key, data[0:len(data) - extra_size])
|
self.mobi_data += PC1(found_key, data[0:len(data) - extra_size])
|
||||||
if extra_size > 0:
|
if extra_size > 0:
|
||||||
new_data += data[-extra_size:]
|
self.mobi_data += data[-extra_size:]
|
||||||
if self.num_sections > self.records+1:
|
if self.num_sections > self.records+1:
|
||||||
new_data += self.data_file[self.sections[self.records+1][0]:]
|
self.mobi_data += self.data_file[self.sections[self.records+1][0]:]
|
||||||
self.data_file = new_data
|
|
||||||
print "done"
|
print "done"
|
||||||
return self.data_file
|
return
|
||||||
|
|
||||||
def getUnencryptedBook(infile,pid):
|
def getUnencryptedBook(infile,pid):
|
||||||
if not os.path.isfile(infile):
|
if not os.path.isfile(infile):
|
||||||
raise DrmException('Input File Not Found')
|
raise DrmException('Input File Not Found')
|
||||||
book = MobiBook(infile)
|
book = MobiBook(infile)
|
||||||
return book.processBook([pid])
|
book.processBook([pid])
|
||||||
|
return book.mobi_data
|
||||||
|
|
||||||
def getUnencryptedBookWithList(infile,pidlist):
|
def getUnencryptedBookWithList(infile,pidlist):
|
||||||
if not os.path.isfile(infile):
|
if not os.path.isfile(infile):
|
||||||
raise DrmException('Input File Not Found')
|
raise DrmException('Input File Not Found')
|
||||||
book = MobiBook(infile)
|
book = MobiBook(infile)
|
||||||
return book.processBook(pidlist)
|
book.processBook(pidlist)
|
||||||
|
return book.mobi_data
|
||||||
|
|
||||||
|
|
||||||
def main(argv=sys.argv):
|
def main(argv=sys.argv):
|
||||||
print ('MobiDeDrm v%(__version__)s. '
|
print ('MobiDeDrm v%(__version__)s. '
|
||||||
'Copyright 2008-2010 The Dark Reverser.' % globals())
|
'Copyright 2008-2010 The Dark Reverser.' % globals())
|
||||||
if len(argv)<4:
|
if len(argv)<3 or len(argv)>4:
|
||||||
print "Removes protection from Mobipocket books"
|
print "Removes protection from Mobipocket books"
|
||||||
print "Usage:"
|
print "Usage:"
|
||||||
print " %s <infile> <outfile> <Comma separated list of PIDs to try>" % sys.argv[0]
|
print " %s <infile> <outfile> [<Comma separated list of PIDs to try>]" % sys.argv[0]
|
||||||
return 1
|
return 1
|
||||||
else:
|
else:
|
||||||
infile = argv[1]
|
infile = argv[1]
|
||||||
outfile = argv[2]
|
outfile = argv[2]
|
||||||
|
if len(argv) is 4:
|
||||||
pidlist = argv[3].split(',')
|
pidlist = argv[3].split(',')
|
||||||
|
else:
|
||||||
|
pidlist = {}
|
||||||
try:
|
try:
|
||||||
stripped_file = getUnencryptedBookWithList(infile, pidlist)
|
stripped_file = getUnencryptedBookWithList(infile, pidlist)
|
||||||
file(outfile, 'wb').write(stripped_file)
|
file(outfile, 'wb').write(stripped_file)
|
||||||
|
|||||||
@@ -10,7 +10,12 @@ class Unbuffered:
|
|||||||
return getattr(self.stream, attr)
|
return getattr(self.stream, attr)
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
sys.stdout=Unbuffered(sys.stdout)
|
|
||||||
|
if 'calibre' in sys.modules:
|
||||||
|
inCalibre = True
|
||||||
|
else:
|
||||||
|
inCalibre = False
|
||||||
|
|
||||||
import os, csv, getopt
|
import os, csv, getopt
|
||||||
import zlib, zipfile, tempfile, shutil
|
import zlib, zipfile, tempfile, shutil
|
||||||
from struct import pack
|
from struct import pack
|
||||||
@@ -19,9 +24,31 @@ from struct import unpack
|
|||||||
class TpzDRMError(Exception):
|
class TpzDRMError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
# local support routines
|
# local support routines
|
||||||
|
if inCalibre:
|
||||||
|
from calibre_plugins.k4mobidedrm import kgenpids
|
||||||
|
from calibre_plugins.k4mobidedrm import genbook
|
||||||
|
else:
|
||||||
import kgenpids
|
import kgenpids
|
||||||
import genbook
|
import genbook
|
||||||
|
|
||||||
|
|
||||||
|
# recursive zip creation support routine
|
||||||
|
def zipUpDir(myzip, tdir, localname):
|
||||||
|
currentdir = tdir
|
||||||
|
if localname != "":
|
||||||
|
currentdir = os.path.join(currentdir,localname)
|
||||||
|
list = os.listdir(currentdir)
|
||||||
|
for file in list:
|
||||||
|
afilename = file
|
||||||
|
localfilePath = os.path.join(localname, afilename)
|
||||||
|
realfilePath = os.path.join(currentdir,file)
|
||||||
|
if os.path.isfile(realfilePath):
|
||||||
|
myzip.write(realfilePath, localfilePath)
|
||||||
|
elif os.path.isdir(realfilePath):
|
||||||
|
zipUpDir(myzip, tdir, localfilePath)
|
||||||
|
|
||||||
#
|
#
|
||||||
# Utility routines
|
# Utility routines
|
||||||
#
|
#
|
||||||
@@ -110,9 +137,9 @@ def decryptDkeyRecords(data,PID):
|
|||||||
|
|
||||||
|
|
||||||
class TopazBook:
|
class TopazBook:
|
||||||
def __init__(self, filename, outdir):
|
def __init__(self, filename):
|
||||||
self.fo = file(filename, 'rb')
|
self.fo = file(filename, 'rb')
|
||||||
self.outdir = outdir
|
self.outdir = tempfile.mkdtemp()
|
||||||
self.bookPayloadOffset = 0
|
self.bookPayloadOffset = 0
|
||||||
self.bookHeaderRecords = {}
|
self.bookHeaderRecords = {}
|
||||||
self.bookMetadata = {}
|
self.bookMetadata = {}
|
||||||
@@ -157,17 +184,22 @@ class TopazBook:
|
|||||||
raise TpzDRMError("Parse Error : Record Names Don't Match")
|
raise TpzDRMError("Parse Error : Record Names Don't Match")
|
||||||
flags = ord(self.fo.read(1))
|
flags = ord(self.fo.read(1))
|
||||||
nbRecords = ord(self.fo.read(1))
|
nbRecords = ord(self.fo.read(1))
|
||||||
|
# print nbRecords
|
||||||
for i in range (0,nbRecords) :
|
for i in range (0,nbRecords) :
|
||||||
record = [bookReadString(self.fo), bookReadString(self.fo)]
|
keyval = bookReadString(self.fo)
|
||||||
self.bookMetadata[record[0]] = record[1]
|
content = bookReadString(self.fo)
|
||||||
|
# print keyval
|
||||||
|
# print content
|
||||||
|
self.bookMetadata[keyval] = content
|
||||||
return self.bookMetadata
|
return self.bookMetadata
|
||||||
|
|
||||||
def getPIDMetaInfo(self):
|
def getPIDMetaInfo(self):
|
||||||
keysRecord = None
|
keysRecord = self.bookMetadata.get('keys','')
|
||||||
KeysRecordRecord = None
|
keysRecordRecord = ''
|
||||||
if 'keys' in self.bookMetadata:
|
if keysRecord != '':
|
||||||
keysRecord = self.bookMetadata['keys']
|
keylst = keysRecord.split(',')
|
||||||
keysRecordRecord = self.bookMetadata[keysRecord]
|
for keyval in keylst:
|
||||||
|
keysRecordRecord += self.bookMetadata.get(keyval,'')
|
||||||
return keysRecord, keysRecordRecord
|
return keysRecord, keysRecordRecord
|
||||||
|
|
||||||
def getBookTitle(self):
|
def getBookTitle(self):
|
||||||
@@ -312,21 +344,33 @@ class TopazBook:
|
|||||||
file(outputFile, 'wb').write(record)
|
file(outputFile, 'wb').write(record)
|
||||||
print " "
|
print " "
|
||||||
|
|
||||||
|
def getHTMLZip(self, zipname):
|
||||||
|
htmlzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||||
|
htmlzip.write(os.path.join(self.outdir,'book.html'),'book.html')
|
||||||
|
htmlzip.write(os.path.join(self.outdir,'book.opf'),'book.opf')
|
||||||
|
if os.path.isfile(os.path.join(self.outdir,'cover.jpg')):
|
||||||
|
htmlzip.write(os.path.join(self.outdir,'cover.jpg'),'cover.jpg')
|
||||||
|
htmlzip.write(os.path.join(self.outdir,'style.css'),'style.css')
|
||||||
|
zipUpDir(htmlzip, self.outdir, 'img')
|
||||||
|
htmlzip.close()
|
||||||
|
|
||||||
def zipUpDir(myzip, tempdir,localname):
|
def getSVGZip(self, zipname):
|
||||||
currentdir = tempdir
|
svgzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||||
if localname != "":
|
svgzip.write(os.path.join(self.outdir,'index_svg.xhtml'),'index_svg.xhtml')
|
||||||
currentdir = os.path.join(currentdir,localname)
|
zipUpDir(svgzip, self.outdir, 'svg')
|
||||||
list = os.listdir(currentdir)
|
zipUpDir(svgzip, self.outdir, 'img')
|
||||||
for file in list:
|
svgzip.close()
|
||||||
afilename = file
|
|
||||||
localfilePath = os.path.join(localname, afilename)
|
|
||||||
realfilePath = os.path.join(currentdir,file)
|
|
||||||
if os.path.isfile(realfilePath):
|
|
||||||
myzip.write(realfilePath, localfilePath)
|
|
||||||
elif os.path.isdir(realfilePath):
|
|
||||||
zipUpDir(myzip, tempdir, localfilePath)
|
|
||||||
|
|
||||||
|
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):
|
def usage(progname):
|
||||||
print "Removes DRM protection from Topaz ebooks and extract the contents"
|
print "Removes DRM protection from Topaz ebooks and extract the contents"
|
||||||
@@ -378,57 +422,46 @@ def main(argv=sys.argv):
|
|||||||
return 1
|
return 1
|
||||||
|
|
||||||
bookname = os.path.splitext(os.path.basename(infile))[0]
|
bookname = os.path.splitext(os.path.basename(infile))[0]
|
||||||
tempdir = tempfile.mkdtemp()
|
|
||||||
|
|
||||||
tb = TopazBook(infile, tempdir)
|
tb = TopazBook(infile)
|
||||||
title = tb.getBookTitle()
|
title = tb.getBookTitle()
|
||||||
print "Processing Book: ", title
|
print "Processing Book: ", title
|
||||||
keysRecord, keysRecordRecord = tb.getPIDMetaInfo()
|
keysRecord, keysRecordRecord = tb.getPIDMetaInfo()
|
||||||
pidlst = kgenpids.getPidList(keysRecord, keysRecordRecord, k4, pids, serials, kInfoFiles)
|
pidlst = kgenpids.getPidList(keysRecord, keysRecordRecord, k4, pids, serials, kInfoFiles)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
print "Decrypting Book"
|
||||||
tb.processBook(pidlst)
|
tb.processBook(pidlst)
|
||||||
except TpzDRMError, e:
|
|
||||||
print str(e)
|
|
||||||
print " Creating DeBug Full Zip Archive of Book"
|
|
||||||
zipname = os.path.join(outdir, bookname + '_debug' + '.zip')
|
|
||||||
myzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
|
||||||
zipUpDir(myzip, tempdir, '')
|
|
||||||
myzip.close()
|
|
||||||
return 1
|
|
||||||
|
|
||||||
print " Creating HTML ZIP Archive"
|
print " Creating HTML ZIP Archive"
|
||||||
zipname = os.path.join(outdir, bookname + '_nodrm' + '.zip')
|
zipname = os.path.join(outdir, bookname + '_nodrm' + '.htmlz')
|
||||||
myzip1 = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
tb.getHTMLZip(zipname)
|
||||||
myzip1.write(os.path.join(tempdir,'book.html'),'book.html')
|
|
||||||
myzip1.write(os.path.join(tempdir,'book.opf'),'book.opf')
|
|
||||||
if os.path.isfile(os.path.join(tempdir,'cover.jpg')):
|
|
||||||
myzip1.write(os.path.join(tempdir,'cover.jpg'),'cover.jpg')
|
|
||||||
myzip1.write(os.path.join(tempdir,'style.css'),'style.css')
|
|
||||||
zipUpDir(myzip1, tempdir, 'img')
|
|
||||||
myzip1.close()
|
|
||||||
|
|
||||||
print " Creating SVG ZIP Archive"
|
print " Creating SVG ZIP Archive"
|
||||||
zipname = os.path.join(outdir, bookname + '_SVG' + '.zip')
|
zipname = os.path.join(outdir, bookname + '_SVG' + '.htmlz')
|
||||||
myzip2 = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
tb.getSVGZip(zipname)
|
||||||
myzip2.write(os.path.join(tempdir,'index_svg.xhtml'),'index_svg.xhtml')
|
|
||||||
zipUpDir(myzip2, tempdir, 'svg')
|
|
||||||
zipUpDir(myzip2, tempdir, 'img')
|
|
||||||
myzip2.close()
|
|
||||||
|
|
||||||
print " Creating XML ZIP Archive"
|
print " Creating XML ZIP Archive"
|
||||||
zipname = os.path.join(outdir, bookname + '_XML' + '.zip')
|
zipname = os.path.join(outdir, bookname + '_XML' + '.zip')
|
||||||
myzip3 = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
tb.getXMLZip(zipname)
|
||||||
targetdir = os.path.join(tempdir,'xml')
|
|
||||||
zipUpDir(myzip3, targetdir, '')
|
|
||||||
zipUpDir(myzip3, tempdir, 'img')
|
|
||||||
myzip3.close()
|
|
||||||
|
|
||||||
shutil.rmtree(tempdir)
|
# removing internal temporary directory of pieces
|
||||||
|
tb.cleanup()
|
||||||
|
|
||||||
|
except TpzDRMError, e:
|
||||||
|
print str(e)
|
||||||
|
tb.cleanup()
|
||||||
|
return 1
|
||||||
|
|
||||||
|
except Exception, e:
|
||||||
|
print str(e)
|
||||||
|
tb.cleanup
|
||||||
|
return 1
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
sys.stdout=Unbuffered(sys.stdout)
|
||||||
sys.exit(main())
|
sys.exit(main())
|
||||||
|
|
||||||
|
|||||||
@@ -13,9 +13,20 @@ _FILENAME_LEN_OFFSET = 26
|
|||||||
_EXTRA_LEN_OFFSET = 28
|
_EXTRA_LEN_OFFSET = 28
|
||||||
_FILENAME_OFFSET = 30
|
_FILENAME_OFFSET = 30
|
||||||
_MAX_SIZE = 64 * 1024
|
_MAX_SIZE = 64 * 1024
|
||||||
|
_MIMETYPE = 'application/epub+zip'
|
||||||
|
|
||||||
|
class ZipInfo(zipfile.ZipInfo):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
if 'compress_type' in kwargs:
|
||||||
|
compress_type = kwargs.pop('compress_type')
|
||||||
|
super(ZipInfo, self).__init__(*args, **kwargs)
|
||||||
|
self.compress_type = compress_type
|
||||||
|
|
||||||
class fixZip:
|
class fixZip:
|
||||||
def __init__(self, zinput, zoutput):
|
def __init__(self, zinput, zoutput):
|
||||||
|
self.ztype = 'zip'
|
||||||
|
if zinput.lower().find('.epub') >= 0 :
|
||||||
|
self.ztype = 'epub'
|
||||||
self.inzip = zipfile.ZipFile(zinput,'r')
|
self.inzip = zipfile.ZipFile(zinput,'r')
|
||||||
self.outzip = zipfile.ZipFile(zoutput,'w')
|
self.outzip = zipfile.ZipFile(zoutput,'w')
|
||||||
# open the input zip for reading only as a raw file
|
# open the input zip for reading only as a raw file
|
||||||
@@ -82,7 +93,14 @@ class fixZip:
|
|||||||
# and copy member over to output archive
|
# and copy member over to output archive
|
||||||
# if problems exist with local vs central filename, fix them
|
# if problems exist with local vs central filename, fix them
|
||||||
|
|
||||||
|
# if epub write mimetype file first, with no compression
|
||||||
|
if self.ztype == 'epub':
|
||||||
|
nzinfo = ZipInfo('mimetype', compress_type=zipfile.ZIP_STORED)
|
||||||
|
self.outzip.writestr(nzinfo, _MIMETYPE)
|
||||||
|
|
||||||
|
# write the rest of the files
|
||||||
for zinfo in self.inzip.infolist():
|
for zinfo in self.inzip.infolist():
|
||||||
|
if zinfo.filename != "mimetype" or self.ztype == '.zip':
|
||||||
data = None
|
data = None
|
||||||
nzinfo = zinfo
|
nzinfo = zinfo
|
||||||
try:
|
try:
|
||||||
@@ -110,14 +128,7 @@ def usage():
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def main(argv=sys.argv):
|
def repairBook(infile, outfile):
|
||||||
if len(argv)!=3:
|
|
||||||
usage()
|
|
||||||
return 1
|
|
||||||
infile = None
|
|
||||||
outfile = None
|
|
||||||
infile = argv[1]
|
|
||||||
outfile = argv[2]
|
|
||||||
if not os.path.exists(infile):
|
if not os.path.exists(infile):
|
||||||
print "Error: Input Zip File does not exist"
|
print "Error: Input Zip File does not exist"
|
||||||
return 1
|
return 1
|
||||||
@@ -129,6 +140,16 @@ def main(argv=sys.argv):
|
|||||||
print "Error Occurred ", e
|
print "Error Occurred ", e
|
||||||
return 2
|
return 2
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv=sys.argv):
|
||||||
|
if len(argv)!=3:
|
||||||
|
usage()
|
||||||
|
return 1
|
||||||
|
infile = argv[1]
|
||||||
|
outfile = argv[2]
|
||||||
|
return repairBook(infile, outfile)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__' :
|
if __name__ == '__main__' :
|
||||||
sys.exit(main())
|
sys.exit(main())
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
echo off
|
||||||
|
set PWD=%~dp0
|
||||||
|
cd /d %PWD%\DeDRM_lib && start /min python DeDRM_app.pyw %*
|
||||||
|
exit
|
||||||
587
DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/DeDRM_app.pyw
Normal file
587
DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/DeDRM_app.pyw
Normal file
@@ -0,0 +1,587 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os, os.path
|
||||||
|
sys.path.append(sys.path[0]+os.sep+'lib')
|
||||||
|
import shutil
|
||||||
|
import Tkinter
|
||||||
|
from Tkinter import *
|
||||||
|
import Tkconstants
|
||||||
|
import tkFileDialog
|
||||||
|
from scrolltextwidget import ScrolledText
|
||||||
|
from activitybar import ActivityBar
|
||||||
|
import subprocess
|
||||||
|
from subprocess import Popen, PIPE, STDOUT
|
||||||
|
import subasyncio
|
||||||
|
from subasyncio import Process
|
||||||
|
import re
|
||||||
|
import simpleprefs
|
||||||
|
|
||||||
|
class DrmException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class MainApp(Tk):
|
||||||
|
def __init__(self, apphome, dnd=False, filenames=[]):
|
||||||
|
Tk.__init__(self)
|
||||||
|
self.withdraw()
|
||||||
|
self.dnd = dnd
|
||||||
|
self.apphome = apphome
|
||||||
|
# preference settings
|
||||||
|
# [dictionary key, file in preferences directory where info is stored]
|
||||||
|
description = [ ['pids' , 'pidlist.txt' ],
|
||||||
|
['serials', 'seriallist.txt'],
|
||||||
|
['sdrms' , 'sdrmlist.txt' ],
|
||||||
|
['outdir' , 'outdir.txt' ]]
|
||||||
|
self.po = simpleprefs.SimplePrefs('DeDRM',description)
|
||||||
|
if self.dnd:
|
||||||
|
self.cd = ConvDialog(self)
|
||||||
|
prefs = self.getPreferences()
|
||||||
|
self.cd.doit(prefs, filenames)
|
||||||
|
else:
|
||||||
|
prefs = self.getPreferences()
|
||||||
|
self.pd = PrefsDialog(self, prefs)
|
||||||
|
self.cd = ConvDialog(self)
|
||||||
|
self.pd.show()
|
||||||
|
|
||||||
|
def getPreferences(self):
|
||||||
|
prefs = self.po.getPreferences()
|
||||||
|
prefdir = prefs['dir']
|
||||||
|
keyfile = os.path.join(prefdir,'adeptkey.der')
|
||||||
|
if not os.path.exists(keyfile):
|
||||||
|
import ineptkey
|
||||||
|
try:
|
||||||
|
ineptkey.extractKeyfile(keyfile)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return prefs
|
||||||
|
|
||||||
|
def setPreferences(self, newprefs):
|
||||||
|
prefdir = self.po.prefdir
|
||||||
|
if 'adkfile' in newprefs:
|
||||||
|
dfile = newprefs['adkfile']
|
||||||
|
fname = os.path.basename(dfile)
|
||||||
|
nfile = os.path.join(prefdir,fname)
|
||||||
|
if os.path.isfile(dfile):
|
||||||
|
shutil.copyfile(dfile,nfile)
|
||||||
|
if 'bnkfile' in newprefs:
|
||||||
|
dfile = newprefs['bnkfile']
|
||||||
|
fname = os.path.basename(dfile)
|
||||||
|
nfile = os.path.join(prefdir,fname)
|
||||||
|
if os.path.isfile(dfile):
|
||||||
|
shutil.copyfile(dfile,nfile)
|
||||||
|
if 'kinfofile' in newprefs:
|
||||||
|
dfile = newprefs['kinfofile']
|
||||||
|
fname = os.path.basename(dfile)
|
||||||
|
nfile = os.path.join(prefdir,fname)
|
||||||
|
if os.path.isfile(dfile):
|
||||||
|
shutil.copyfile(dfile,nfile)
|
||||||
|
self.po.setPreferences(newprefs)
|
||||||
|
return
|
||||||
|
|
||||||
|
def alldone(self):
|
||||||
|
if not self.dnd:
|
||||||
|
self.pd.enablebuttons()
|
||||||
|
else:
|
||||||
|
self.destroy()
|
||||||
|
|
||||||
|
class PrefsDialog(Toplevel):
|
||||||
|
def __init__(self, mainapp, prefs_array):
|
||||||
|
Toplevel.__init__(self, mainapp)
|
||||||
|
self.withdraw()
|
||||||
|
self.protocol("WM_DELETE_WINDOW", self.withdraw)
|
||||||
|
self.title("DeDRM")
|
||||||
|
self.prefs_array = prefs_array
|
||||||
|
self.status = Tkinter.Label(self, text='Setting Preferences')
|
||||||
|
self.status.pack(fill=Tkconstants.X, expand=1)
|
||||||
|
body = Tkinter.Frame(self)
|
||||||
|
self.body = body
|
||||||
|
body.pack(fill=Tkconstants.X, expand=1)
|
||||||
|
sticky = Tkconstants.E + Tkconstants.W
|
||||||
|
body.grid_columnconfigure(1, weight=2)
|
||||||
|
|
||||||
|
Tkinter.Label(body, text='Adept Key file (adeptkey.der)').grid(row=0, sticky=Tkconstants.E)
|
||||||
|
self.adkpath = Tkinter.Entry(body, width=50)
|
||||||
|
self.adkpath.grid(row=0, column=1, sticky=sticky)
|
||||||
|
prefdir = self.prefs_array['dir']
|
||||||
|
keyfile = os.path.join(prefdir,'adeptkey.der')
|
||||||
|
if os.path.isfile(keyfile):
|
||||||
|
path = keyfile
|
||||||
|
path = path.encode('utf-8')
|
||||||
|
self.adkpath.insert(0, path)
|
||||||
|
button = Tkinter.Button(body, text="...", command=self.get_adkpath)
|
||||||
|
button.grid(row=0, column=2)
|
||||||
|
|
||||||
|
Tkinter.Label(body, text='Barnes and Noble Key file (bnepubkey.b64)').grid(row=1, sticky=Tkconstants.E)
|
||||||
|
self.bnkpath = Tkinter.Entry(body, width=50)
|
||||||
|
self.bnkpath.grid(row=1, column=1, sticky=sticky)
|
||||||
|
prefdir = self.prefs_array['dir']
|
||||||
|
keyfile = os.path.join(prefdir,'bnepubkey.b64')
|
||||||
|
if os.path.isfile(keyfile):
|
||||||
|
path = keyfile
|
||||||
|
path = path.encode('utf-8')
|
||||||
|
self.bnkpath.insert(0, path)
|
||||||
|
button = Tkinter.Button(body, text="...", command=self.get_bnkpath)
|
||||||
|
button.grid(row=1, column=2)
|
||||||
|
|
||||||
|
Tkinter.Label(body, text='Additional kindle.info or .kinf file').grid(row=2, sticky=Tkconstants.E)
|
||||||
|
self.altinfopath = Tkinter.Entry(body, width=50)
|
||||||
|
self.altinfopath.grid(row=2, column=1, sticky=sticky)
|
||||||
|
prefdir = self.prefs_array['dir']
|
||||||
|
path = ''
|
||||||
|
infofile = os.path.join(prefdir,'kindle.info')
|
||||||
|
ainfofile = os.path.join(prefdir,'.kinf')
|
||||||
|
if os.path.isfile(infofile):
|
||||||
|
path = infofile
|
||||||
|
elif os.path.isfile(ainfofile):
|
||||||
|
path = ainfofile
|
||||||
|
path = path.encode('utf-8')
|
||||||
|
self.altinfopath.insert(0, path)
|
||||||
|
button = Tkinter.Button(body, text="...", command=self.get_altinfopath)
|
||||||
|
button.grid(row=2, column=2)
|
||||||
|
|
||||||
|
Tkinter.Label(body, text='PID list (10 characters, no spaces, comma separated)').grid(row=3, sticky=Tkconstants.E)
|
||||||
|
self.pidnums = Tkinter.StringVar()
|
||||||
|
self.pidinfo = Tkinter.Entry(body, width=50, textvariable=self.pidnums)
|
||||||
|
if 'pids' in self.prefs_array:
|
||||||
|
self.pidnums.set(self.prefs_array['pids'])
|
||||||
|
self.pidinfo.grid(row=3, column=1, sticky=sticky)
|
||||||
|
|
||||||
|
Tkinter.Label(body, text='Kindle Serial Number list (16 characters, no spaces, comma separated)').grid(row=4, sticky=Tkconstants.E)
|
||||||
|
self.sernums = Tkinter.StringVar()
|
||||||
|
self.serinfo = Tkinter.Entry(body, width=50, textvariable=self.sernums)
|
||||||
|
if 'serials' in self.prefs_array:
|
||||||
|
self.sernums.set(self.prefs_array['serials'])
|
||||||
|
self.serinfo.grid(row=4, column=1, sticky=sticky)
|
||||||
|
|
||||||
|
Tkinter.Label(body, text='eReader data list (name:last 8 digits on credit card, comma separated)').grid(row=5, sticky=Tkconstants.E)
|
||||||
|
self.sdrmnums = Tkinter.StringVar()
|
||||||
|
self.sdrminfo = Tkinter.Entry(body, width=50, textvariable=self.sdrmnums)
|
||||||
|
if 'sdrms' in self.prefs_array:
|
||||||
|
self.sdrmnums.set(self.prefs_array['sdrms'])
|
||||||
|
self.sdrminfo.grid(row=5, column=1, sticky=sticky)
|
||||||
|
|
||||||
|
Tkinter.Label(body, text="Output Folder (if blank, use input ebook's folder)").grid(row=6, sticky=Tkconstants.E)
|
||||||
|
self.outpath = Tkinter.Entry(body, width=50)
|
||||||
|
self.outpath.grid(row=6, column=1, sticky=sticky)
|
||||||
|
if 'outdir' in self.prefs_array:
|
||||||
|
dpath = self.prefs_array['outdir']
|
||||||
|
dpath = dpath.encode('utf-8')
|
||||||
|
self.outpath.insert(0, dpath)
|
||||||
|
button = Tkinter.Button(body, text="...", command=self.get_outpath)
|
||||||
|
button.grid(row=6, column=2)
|
||||||
|
|
||||||
|
Tkinter.Label(body, text='').grid(row=7, column=0, columnspan=2, sticky=Tkconstants.N)
|
||||||
|
|
||||||
|
Tkinter.Label(body, text='Alternatively Process an eBook').grid(row=8, column=0, columnspan=2, sticky=Tkconstants.N)
|
||||||
|
|
||||||
|
Tkinter.Label(body, text='Select an eBook to Process*').grid(row=9, sticky=Tkconstants.E)
|
||||||
|
self.bookpath = Tkinter.Entry(body, width=50)
|
||||||
|
self.bookpath.grid(row=9, column=1, sticky=sticky)
|
||||||
|
button = Tkinter.Button(body, text="...", command=self.get_bookpath)
|
||||||
|
button.grid(row=9, column=2)
|
||||||
|
|
||||||
|
Tkinter.Label(body, font=("Helvetica", "10", "italic"), text='*To DeDRM multiple ebooks simultaneously, set your preferences and quit.\nThen drag and drop ebooks or folders onto the DeDRM_Drop_Target').grid(row=10, column=1, sticky=Tkconstants.E)
|
||||||
|
|
||||||
|
Tkinter.Label(body, text='').grid(row=11, column=0, columnspan=2, sticky=Tkconstants.E)
|
||||||
|
|
||||||
|
buttons = Tkinter.Frame(self)
|
||||||
|
buttons.pack()
|
||||||
|
self.sbotton = Tkinter.Button(buttons, text="Set Prefs", width=14, command=self.setprefs)
|
||||||
|
self.sbotton.pack(side=Tkconstants.LEFT)
|
||||||
|
|
||||||
|
buttons.pack()
|
||||||
|
self.pbotton = Tkinter.Button(buttons, text="Process eBook", width=14, command=self.doit)
|
||||||
|
self.pbotton.pack(side=Tkconstants.LEFT)
|
||||||
|
buttons.pack()
|
||||||
|
self.qbotton = Tkinter.Button(buttons, text="Quit", width=14, command=self.quitting)
|
||||||
|
self.qbotton.pack(side=Tkconstants.RIGHT)
|
||||||
|
buttons.pack()
|
||||||
|
|
||||||
|
def disablebuttons(self):
|
||||||
|
self.sbotton.configure(state='disabled')
|
||||||
|
self.pbotton.configure(state='disabled')
|
||||||
|
self.qbotton.configure(state='disabled')
|
||||||
|
|
||||||
|
def enablebuttons(self):
|
||||||
|
self.sbotton.configure(state='normal')
|
||||||
|
self.pbotton.configure(state='normal')
|
||||||
|
self.qbotton.configure(state='normal')
|
||||||
|
|
||||||
|
def show(self):
|
||||||
|
self.deiconify()
|
||||||
|
self.tkraise()
|
||||||
|
|
||||||
|
def hide(self):
|
||||||
|
self.withdraw()
|
||||||
|
|
||||||
|
def get_outpath(self):
|
||||||
|
cpath = self.outpath.get()
|
||||||
|
outpath = tkFileDialog.askdirectory(
|
||||||
|
parent=None, title='Folder to Store Unencrypted file(s) into',
|
||||||
|
initialdir=cpath, initialfile=None)
|
||||||
|
if outpath:
|
||||||
|
outpath = os.path.normpath(outpath)
|
||||||
|
self.outpath.delete(0, Tkconstants.END)
|
||||||
|
self.outpath.insert(0, outpath)
|
||||||
|
return
|
||||||
|
|
||||||
|
def get_adkpath(self):
|
||||||
|
cpath = self.adkpath.get()
|
||||||
|
adkpath = tkFileDialog.askopenfilename(initialdir = cpath, parent=None, title='Select Adept Key file',
|
||||||
|
defaultextension='.der', filetypes=[('Adept Key file', '.der'), ('All Files', '.*')])
|
||||||
|
if adkpath:
|
||||||
|
adkpath = os.path.normpath(adkpath)
|
||||||
|
self.adkpath.delete(0, Tkconstants.END)
|
||||||
|
self.adkpath.insert(0, adkpath)
|
||||||
|
return
|
||||||
|
|
||||||
|
def get_bnkpath(self):
|
||||||
|
cpath = self.bnkpath.get()
|
||||||
|
bnkpath = tkFileDialog.askopenfilename(initialdir = cpath, parent=None, title='Select Barnes and Noble Key file',
|
||||||
|
defaultextension='.b64', filetypes=[('Barnes and Noble Key file', '.b64'), ('All Files', '.*')])
|
||||||
|
if bnkpath:
|
||||||
|
bnkpath = os.path.normpath(bnkpath)
|
||||||
|
self.bnkpath.delete(0, Tkconstants.END)
|
||||||
|
self.bnkpath.insert(0, bnkpath)
|
||||||
|
return
|
||||||
|
|
||||||
|
def get_altinfopath(self):
|
||||||
|
cpath = self.altinfopath.get()
|
||||||
|
altinfopath = tkFileDialog.askopenfilename(parent=None, title='Select Alternative kindle.info or .kinf File',
|
||||||
|
defaultextension='.info', filetypes=[('Kindle Info', '.info'),('Kindle KInf','.kinf')('All Files', '.*')],
|
||||||
|
initialdir=cpath)
|
||||||
|
if altinfopath:
|
||||||
|
altinfopath = os.path.normpath(altinfopath)
|
||||||
|
self.altinfopath.delete(0, Tkconstants.END)
|
||||||
|
self.altinfopath.insert(0, altinfopath)
|
||||||
|
return
|
||||||
|
|
||||||
|
def get_bookpath(self):
|
||||||
|
cpath = self.bookpath.get()
|
||||||
|
bookpath = tkFileDialog.askopenfilename(parent=None, title='Select eBook for DRM Removal',
|
||||||
|
filetypes=[('ePub Files','.epub'),
|
||||||
|
('Kindle','.azw'),
|
||||||
|
('Kindle','.azw1'),
|
||||||
|
('Kindle','.tpz'),
|
||||||
|
('Kindle','.mobi'),
|
||||||
|
('Kindle','.prc'),
|
||||||
|
('eReader','.pdb'),
|
||||||
|
('PDF','.pdf'),
|
||||||
|
('All Files', '.*')],
|
||||||
|
initialdir=cpath)
|
||||||
|
if bookpath:
|
||||||
|
bookpath = os.path.normpath(bookpath)
|
||||||
|
self.bookpath.delete(0, Tkconstants.END)
|
||||||
|
self.bookpath.insert(0, bookpath)
|
||||||
|
return
|
||||||
|
|
||||||
|
def quitting(self):
|
||||||
|
self.master.destroy()
|
||||||
|
|
||||||
|
def setprefs(self):
|
||||||
|
# setting new prefereces
|
||||||
|
new_prefs = {}
|
||||||
|
prefdir = self.prefs_array['dir']
|
||||||
|
new_prefs['dir'] = prefdir
|
||||||
|
new_prefs['pids'] = self.pidinfo.get().strip()
|
||||||
|
new_prefs['serials'] = self.serinfo.get().strip()
|
||||||
|
new_prefs['sdrms'] = self.sdrminfo.get().strip()
|
||||||
|
new_prefs['outdir'] = self.outpath.get().strip()
|
||||||
|
adkpath = self.adkpath.get()
|
||||||
|
if os.path.dirname(adkpath) != prefdir:
|
||||||
|
new_prefs['adkfile'] = adkpath
|
||||||
|
bnkpath = self.bnkpath.get()
|
||||||
|
if os.path.dirname(bnkpath) != prefdir:
|
||||||
|
new_prefs['bnkfile'] = bnkpath
|
||||||
|
altinfopath = self.altinfopath.get()
|
||||||
|
if os.path.dirname(altinfopath) != prefdir:
|
||||||
|
new_prefs['kinfofile'] = altinfopath
|
||||||
|
self.master.setPreferences(new_prefs)
|
||||||
|
|
||||||
|
def doit(self):
|
||||||
|
self.disablebuttons()
|
||||||
|
filenames=[]
|
||||||
|
bookpath = self.bookpath.get()
|
||||||
|
bookpath = os.path.abspath(bookpath)
|
||||||
|
filenames.append(bookpath)
|
||||||
|
self.master.cd.doit(self.prefs_array,filenames)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class ConvDialog(Toplevel):
|
||||||
|
def __init__(self, master, prefs_array={}, filenames=[]):
|
||||||
|
Toplevel.__init__(self, master)
|
||||||
|
self.withdraw()
|
||||||
|
self.protocol("WM_DELETE_WINDOW", self.withdraw)
|
||||||
|
self.title("DeDRM Processing")
|
||||||
|
self.master = master
|
||||||
|
self.apphome = self.master.apphome
|
||||||
|
self.prefs_array = prefs_array
|
||||||
|
self.filenames = filenames
|
||||||
|
self.interval = 50
|
||||||
|
self.p2 = None
|
||||||
|
self.running = 'inactive'
|
||||||
|
self.numgood = 0
|
||||||
|
self.numbad = 0
|
||||||
|
self.log = ''
|
||||||
|
self.status = Tkinter.Label(self, text='DeDRM processing...')
|
||||||
|
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='Activity Bar').grid(row=0, sticky=Tkconstants.E)
|
||||||
|
self.bar = ActivityBar(body, length=80, height=15, barwidth=5)
|
||||||
|
self.bar.grid(row=0, column=1, sticky=sticky)
|
||||||
|
|
||||||
|
msg1 = ''
|
||||||
|
self.stext = ScrolledText(body, bd=5, relief=Tkconstants.RIDGE, height=4, width=80, wrap=Tkconstants.WORD)
|
||||||
|
self.stext.grid(row=2, column=0, columnspan=2,sticky=sticky)
|
||||||
|
self.stext.insert(Tkconstants.END,msg1)
|
||||||
|
|
||||||
|
buttons = Tkinter.Frame(self)
|
||||||
|
buttons.pack()
|
||||||
|
self.qbutton = Tkinter.Button(buttons, text="Quit", width=14, command=self.quitting)
|
||||||
|
self.qbutton.pack(side=Tkconstants.BOTTOM)
|
||||||
|
self.status['text'] = ''
|
||||||
|
|
||||||
|
def show(self):
|
||||||
|
self.deiconify()
|
||||||
|
self.tkraise()
|
||||||
|
|
||||||
|
def hide(self):
|
||||||
|
self.withdraw()
|
||||||
|
|
||||||
|
def doit(self, prefs, filenames):
|
||||||
|
self.running = 'inactive'
|
||||||
|
self.prefs_array = prefs
|
||||||
|
self.filenames = filenames
|
||||||
|
self.show()
|
||||||
|
self.processBooks()
|
||||||
|
|
||||||
|
def conversion_done(self):
|
||||||
|
self.hide()
|
||||||
|
self.master.alldone()
|
||||||
|
|
||||||
|
def processBooks(self):
|
||||||
|
while self.running == 'inactive':
|
||||||
|
rscpath = self.prefs_array['dir']
|
||||||
|
filename = None
|
||||||
|
if len(self.filenames) > 0:
|
||||||
|
filename = self.filenames.pop(0)
|
||||||
|
if filename == None:
|
||||||
|
msg = '\nComplete: '
|
||||||
|
msg += 'Successes: %d, ' % self.numgood
|
||||||
|
msg += 'Failures: %d\n' % self.numbad
|
||||||
|
self.showCmdOutput(msg)
|
||||||
|
if self.numbad == 0:
|
||||||
|
self.after(2000,self.conversion_done())
|
||||||
|
logfile = os.path.join(rscpath,'dedrm.log')
|
||||||
|
file(logfile,'w').write(self.log)
|
||||||
|
return
|
||||||
|
infile = filename
|
||||||
|
bname = os.path.basename(infile)
|
||||||
|
msg = 'Processing: ' + bname + ' ... '
|
||||||
|
self.log += msg
|
||||||
|
self.showCmdOutput(msg)
|
||||||
|
outdir = os.path.dirname(filename)
|
||||||
|
if 'outdir' in self.prefs_array:
|
||||||
|
dpath = self.prefs_array['outdir']
|
||||||
|
if dpath.strip() != '':
|
||||||
|
outdir = dpath
|
||||||
|
rv = self.decrypt_ebook(infile, outdir, rscpath)
|
||||||
|
if rv == 0:
|
||||||
|
self.bar.start()
|
||||||
|
self.running = 'active'
|
||||||
|
self.processPipe()
|
||||||
|
else:
|
||||||
|
msg = 'Unknown File: ' + bname + '\n'
|
||||||
|
self.log += msg
|
||||||
|
self.showCmdOutput(msg)
|
||||||
|
self.numbad += 1
|
||||||
|
|
||||||
|
def quitting(self):
|
||||||
|
# kill any still running subprocess
|
||||||
|
self.running = 'stopped'
|
||||||
|
if self.p2 != None:
|
||||||
|
if (self.p2.wait('nowait') == None):
|
||||||
|
self.p2.terminate()
|
||||||
|
self.conversion_done()
|
||||||
|
|
||||||
|
# post output from subprocess in scrolled text widget
|
||||||
|
def showCmdOutput(self, msg):
|
||||||
|
if msg and msg !='':
|
||||||
|
msg = msg.encode('utf-8')
|
||||||
|
if sys.platform.startswith('win'):
|
||||||
|
msg = msg.replace('\r\n','\n')
|
||||||
|
self.stext.insert(Tkconstants.END,msg)
|
||||||
|
self.stext.yview_pickplace(Tkconstants.END)
|
||||||
|
return
|
||||||
|
|
||||||
|
# read from subprocess pipe without blocking
|
||||||
|
# invoked every interval via the widget "after"
|
||||||
|
# option being used, so need to reset it for the next time
|
||||||
|
def processPipe(self):
|
||||||
|
if self.p2 == None:
|
||||||
|
# nothing to wait for so just return
|
||||||
|
return
|
||||||
|
poll = self.p2.wait('nowait')
|
||||||
|
if poll != None:
|
||||||
|
self.bar.stop()
|
||||||
|
if poll == 0:
|
||||||
|
msg = 'Success\n'
|
||||||
|
self.numgood += 1
|
||||||
|
text = self.p2.read()
|
||||||
|
text += self.p2.readerr()
|
||||||
|
self.log += text
|
||||||
|
self.log += msg
|
||||||
|
if poll != 0:
|
||||||
|
msg = 'Failed\n'
|
||||||
|
text = self.p2.read()
|
||||||
|
text += self.p2.readerr()
|
||||||
|
msg += text
|
||||||
|
msg += '\n'
|
||||||
|
self.numbad += 1
|
||||||
|
self.log += msg
|
||||||
|
self.showCmdOutput(msg)
|
||||||
|
self.p2 = None
|
||||||
|
self.running = 'inactive'
|
||||||
|
self.after(50,self.processBooks)
|
||||||
|
return
|
||||||
|
# make sure we get invoked again by event loop after interval
|
||||||
|
self.stext.after(self.interval,self.processPipe)
|
||||||
|
return
|
||||||
|
|
||||||
|
def decrypt_ebook(self, infile, outdir, rscpath):
|
||||||
|
apphome = self.apphome
|
||||||
|
rv = 1
|
||||||
|
name, ext = os.path.splitext(os.path.basename(infile))
|
||||||
|
ext = ext.lower()
|
||||||
|
if ext == '.epub':
|
||||||
|
self.p2 = processEPUB(apphome, infile, outdir, rscpath)
|
||||||
|
return 0
|
||||||
|
if ext == '.pdb':
|
||||||
|
self.p2 = processPDB(apphome, infile, outdir, rscpath)
|
||||||
|
return 0
|
||||||
|
if ext in ['.azw', '.azw1', '.prc', '.mobi', '.tpz']:
|
||||||
|
self.p2 = processK4MOBI(apphome, infile, outdir, rscpath)
|
||||||
|
return 0
|
||||||
|
if ext == '.pdf':
|
||||||
|
self.p2 = processPDF(apphome, infile, outdir, rscpath)
|
||||||
|
return 0
|
||||||
|
return rv
|
||||||
|
|
||||||
|
|
||||||
|
# run as a subprocess via pipes and collect stdout, stderr, and return value
|
||||||
|
def runit(apphome, ncmd, nparms):
|
||||||
|
cmdline = 'python ' + '"' + os.path.join(apphome, ncmd) + '" '
|
||||||
|
if sys.platform.startswith('win'):
|
||||||
|
search_path = os.environ['PATH']
|
||||||
|
search_path = search_path.lower()
|
||||||
|
if search_path.find('python') < 0:
|
||||||
|
# if no python hope that win registry finds what is associated with py extension
|
||||||
|
cmdline = '"' + os.path.join(apphome, ncmd) + '" '
|
||||||
|
cmdline += nparms
|
||||||
|
cmdline = cmdline.encode(sys.getfilesystemencoding())
|
||||||
|
p2 = subasyncio.Process(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
|
||||||
|
return p2
|
||||||
|
|
||||||
|
def processK4MOBI(apphome, infile, outdir, rscpath):
|
||||||
|
cmd = os.path.join('lib','k4mobidedrm.py')
|
||||||
|
parms = ''
|
||||||
|
pidnums = ''
|
||||||
|
pidspath = os.path.join(rscpath,'pidlist.txt')
|
||||||
|
if os.path.exists(pidspath):
|
||||||
|
pidnums = file(pidspath,'r').read()
|
||||||
|
pidnums = pidnums.rstrip(os.linesep)
|
||||||
|
if pidnums != '':
|
||||||
|
parms += '-p "' + pidnums + '" '
|
||||||
|
serialnums = ''
|
||||||
|
serialnumspath = os.path.join(rscpath,'seriallist.txt')
|
||||||
|
if os.path.exists(serialnumspath):
|
||||||
|
serialnums = file(serialnumspath,'r').read()
|
||||||
|
serialnums = serialnums.rstrip(os.linesep)
|
||||||
|
if serialnums != '':
|
||||||
|
parms += '-s "' + serialnums + '" '
|
||||||
|
|
||||||
|
files = os.listdir(rscpath)
|
||||||
|
filefilter = re.compile("\.info$|\.kinf$", re.IGNORECASE)
|
||||||
|
files = filter(filefilter.search, files)
|
||||||
|
if files:
|
||||||
|
for filename in files:
|
||||||
|
dpath = os.path.join(rscpath,filename)
|
||||||
|
parms += '-k "' + dpath + '" '
|
||||||
|
parms += '"' + infile +'" "' + outdir + '"'
|
||||||
|
p2 = runit(apphome, cmd, parms)
|
||||||
|
return p2
|
||||||
|
|
||||||
|
def processPDF(apphome, infile, outdir, rscpath):
|
||||||
|
cmd = os.path.join('lib','decryptpdf.py')
|
||||||
|
parms = '"' + infile + '" "' + outdir + '" "' + rscpath + '"'
|
||||||
|
p2 = runit(apphome, cmd, parms)
|
||||||
|
return p2
|
||||||
|
|
||||||
|
def processEPUB(apphome, infile, outdir, rscpath):
|
||||||
|
# invoke routine to check both Adept and Barnes and Noble
|
||||||
|
cmd = os.path.join('lib','decryptepub.py')
|
||||||
|
parms = '"' + infile + '" "' + outdir + '" "' + rscpath + '"'
|
||||||
|
p2 = runit(apphome, cmd, parms)
|
||||||
|
return p2
|
||||||
|
|
||||||
|
def processPDB(apphome, infile, outdir, rscpath):
|
||||||
|
cmd = os.path.join('lib','decryptpdb.py')
|
||||||
|
parms = '"' + infile + '" "' + outdir + '" "' + rscpath + '"'
|
||||||
|
p2 = runit(apphome, cmd, parms)
|
||||||
|
return p2
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv=sys.argv):
|
||||||
|
apphome = os.path.dirname(sys.argv[0])
|
||||||
|
apphome = os.path.abspath(apphome)
|
||||||
|
|
||||||
|
# windows may pass a spurious quoted null string as argv[1] from bat file
|
||||||
|
# simply work around this until we can figure out a better way to handle things
|
||||||
|
if len(argv) == 2:
|
||||||
|
temp = argv[1]
|
||||||
|
temp = temp.strip('"')
|
||||||
|
temp = temp.strip()
|
||||||
|
if temp == '':
|
||||||
|
argv.pop()
|
||||||
|
|
||||||
|
if len(argv) == 1:
|
||||||
|
filenames = []
|
||||||
|
dnd = False
|
||||||
|
|
||||||
|
else : # processing books via drag and drop
|
||||||
|
dnd = True
|
||||||
|
# build a list of the files to be processed
|
||||||
|
infilelst = argv[1:]
|
||||||
|
filenames = []
|
||||||
|
for infile in infilelst:
|
||||||
|
infile = infile.replace('"','')
|
||||||
|
infile = os.path.abspath(infile)
|
||||||
|
if os.path.isdir(infile):
|
||||||
|
bpath = infile
|
||||||
|
filelst = os.listdir(infile)
|
||||||
|
for afile in filelst:
|
||||||
|
if not afile.startswith('.'):
|
||||||
|
filepath = os.path.join(bpath,afile)
|
||||||
|
if os.path.isfile(filepath):
|
||||||
|
filenames.append(filepath)
|
||||||
|
else :
|
||||||
|
afile = os.path.basename(infile)
|
||||||
|
if not afile.startswith('.'):
|
||||||
|
if os.path.isfile(infile):
|
||||||
|
filenames.append(infile)
|
||||||
|
|
||||||
|
# start up gui app
|
||||||
|
app = MainApp(apphome, dnd, filenames)
|
||||||
|
app.mainloop()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
|
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import sys
|
||||||
|
import Tkinter
|
||||||
|
import Tkconstants
|
||||||
|
|
||||||
|
class ActivityBar(Tkinter.Frame):
|
||||||
|
|
||||||
|
def __init__(self, master, length=300, height=20, barwidth=15, interval=50, bg='white', fillcolor='orchid1',\
|
||||||
|
bd=2, relief=Tkconstants.GROOVE, *args, **kw):
|
||||||
|
Tkinter.Frame.__init__(self, master, bg=bg, width=length, height=height, *args, **kw)
|
||||||
|
self._master = master
|
||||||
|
self._interval = interval
|
||||||
|
self._maximum = length
|
||||||
|
self._startx = 0
|
||||||
|
self._barwidth = barwidth
|
||||||
|
self._bardiv = length / barwidth
|
||||||
|
if self._bardiv < 10:
|
||||||
|
self._bardiv = 10
|
||||||
|
stopx = self._startx + self._barwidth
|
||||||
|
if stopx > self._maximum:
|
||||||
|
stopx = self._maximum
|
||||||
|
# self._canv = Tkinter.Canvas(self, bg=self['bg'], width=self['width'], height=self['height'],\
|
||||||
|
# highlightthickness=0, relief='flat', bd=0)
|
||||||
|
self._canv = Tkinter.Canvas(self, bg=self['bg'], width=self['width'], height=self['height'],\
|
||||||
|
highlightthickness=0, relief=relief, bd=bd)
|
||||||
|
self._canv.pack(fill='both', expand=1)
|
||||||
|
self._rect = self._canv.create_rectangle(0, 0, self._canv.winfo_reqwidth(), self._canv.winfo_reqheight(), fill=fillcolor, width=0)
|
||||||
|
|
||||||
|
self._set()
|
||||||
|
self.bind('<Configure>', self._update_coords)
|
||||||
|
self._running = False
|
||||||
|
|
||||||
|
def _update_coords(self, event):
|
||||||
|
'''Updates the position of the rectangle inside the canvas when the size of
|
||||||
|
the widget gets changed.'''
|
||||||
|
# looks like we have to call update_idletasks() twice to make sure
|
||||||
|
# to get the results we expect
|
||||||
|
self._canv.update_idletasks()
|
||||||
|
self._maximum = self._canv.winfo_width()
|
||||||
|
self._startx = 0
|
||||||
|
self._barwidth = self._maximum / self._bardiv
|
||||||
|
if self._barwidth < 2:
|
||||||
|
self._barwidth = 2
|
||||||
|
stopx = self._startx + self._barwidth
|
||||||
|
if stopx > self._maximum:
|
||||||
|
stopx = self._maximum
|
||||||
|
self._canv.coords(self._rect, 0, 0, stopx, self._canv.winfo_height())
|
||||||
|
self._canv.update_idletasks()
|
||||||
|
|
||||||
|
def _set(self):
|
||||||
|
if self._startx < 0:
|
||||||
|
self._startx = 0
|
||||||
|
if self._startx > self._maximum:
|
||||||
|
self._startx = self._startx % self._maximum
|
||||||
|
stopx = self._startx + self._barwidth
|
||||||
|
if stopx > self._maximum:
|
||||||
|
stopx = self._maximum
|
||||||
|
self._canv.coords(self._rect, self._startx, 0, stopx, self._canv.winfo_height())
|
||||||
|
self._canv.update_idletasks()
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
self._running = True
|
||||||
|
self.after(self._interval, self._step)
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self._running = False
|
||||||
|
self._set()
|
||||||
|
|
||||||
|
def _step(self):
|
||||||
|
if self._running:
|
||||||
|
stepsize = self._barwidth / 4
|
||||||
|
if stepsize < 2:
|
||||||
|
stepsize = 2
|
||||||
|
self._startx += stepsize
|
||||||
|
self._set()
|
||||||
|
self.after(self._interval, self._step)
|
||||||
@@ -235,6 +235,7 @@ class PageParser(object):
|
|||||||
|
|
||||||
'group' : (1, 'snippets', 1, 0),
|
'group' : (1, 'snippets', 1, 0),
|
||||||
'group.type' : (1, 'scalar_text', 0, 0),
|
'group.type' : (1, 'scalar_text', 0, 0),
|
||||||
|
'group._tag' : (1, 'scalar_text', 0, 0),
|
||||||
|
|
||||||
'region' : (1, 'snippets', 1, 0),
|
'region' : (1, 'snippets', 1, 0),
|
||||||
'region.type' : (1, 'scalar_text', 0, 0),
|
'region.type' : (1, 'scalar_text', 0, 0),
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
|
||||||
|
|
||||||
|
class Unbuffered:
|
||||||
|
def __init__(self, stream):
|
||||||
|
self.stream = stream
|
||||||
|
def write(self, data):
|
||||||
|
self.stream.write(data)
|
||||||
|
self.stream.flush()
|
||||||
|
def __getattr__(self, attr):
|
||||||
|
return getattr(self.stream, attr)
|
||||||
|
|
||||||
|
import sys
|
||||||
|
sys.stdout=Unbuffered(sys.stdout)
|
||||||
|
import os
|
||||||
|
|
||||||
|
import ineptepub
|
||||||
|
import ignobleepub
|
||||||
|
import zipfix
|
||||||
|
import re
|
||||||
|
|
||||||
|
def main(argv=sys.argv):
|
||||||
|
args = argv[1:]
|
||||||
|
if len(args) != 3:
|
||||||
|
return -1
|
||||||
|
infile = args[0]
|
||||||
|
outdir = args[1]
|
||||||
|
rscpath = args[2]
|
||||||
|
errlog = ''
|
||||||
|
|
||||||
|
# first fix the epub to make sure we do not get errors
|
||||||
|
name, ext = os.path.splitext(os.path.basename(infile))
|
||||||
|
bpath = os.path.dirname(infile)
|
||||||
|
zippath = os.path.join(bpath,name + '_temp.zip')
|
||||||
|
rv = zipfix.repairBook(infile, zippath)
|
||||||
|
if rv != 0:
|
||||||
|
print "Error while trying to fix epub"
|
||||||
|
return rv
|
||||||
|
|
||||||
|
# determine a good name for the output file
|
||||||
|
outfile = os.path.join(outdir, name + '_nodrm.epub')
|
||||||
|
|
||||||
|
rv = 1
|
||||||
|
# first try with the Adobe adept epub
|
||||||
|
# try with any keyfiles (*.der) in the rscpath
|
||||||
|
files = os.listdir(rscpath)
|
||||||
|
filefilter = re.compile("\.der$", re.IGNORECASE)
|
||||||
|
files = filter(filefilter.search, files)
|
||||||
|
if files:
|
||||||
|
for filename in files:
|
||||||
|
keypath = os.path.join(rscpath, filename)
|
||||||
|
try:
|
||||||
|
rv = ineptepub.decryptBook(keypath, zippath, outfile)
|
||||||
|
if rv == 0:
|
||||||
|
break
|
||||||
|
except Exception, e:
|
||||||
|
errlog += str(e)
|
||||||
|
rv = 1
|
||||||
|
pass
|
||||||
|
if rv == 0:
|
||||||
|
os.remove(zippath)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# still no luck
|
||||||
|
# now try with ignoble epub
|
||||||
|
# try with any keyfiles (*.b64) in the rscpath
|
||||||
|
files = os.listdir(rscpath)
|
||||||
|
filefilter = re.compile("\.b64$", re.IGNORECASE)
|
||||||
|
files = filter(filefilter.search, files)
|
||||||
|
if files:
|
||||||
|
for filename in files:
|
||||||
|
keypath = os.path.join(rscpath, filename)
|
||||||
|
try:
|
||||||
|
rv = ignobleepub.decryptBook(keypath, zippath, outfile)
|
||||||
|
if rv == 0:
|
||||||
|
break
|
||||||
|
except Exception, e:
|
||||||
|
errlog += str(e)
|
||||||
|
rv = 1
|
||||||
|
pass
|
||||||
|
os.remove(zippath)
|
||||||
|
if rv != 0:
|
||||||
|
print errlog
|
||||||
|
return rv
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
|
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
|
||||||
|
|
||||||
|
class Unbuffered:
|
||||||
|
def __init__(self, stream):
|
||||||
|
self.stream = stream
|
||||||
|
def write(self, data):
|
||||||
|
self.stream.write(data)
|
||||||
|
self.stream.flush()
|
||||||
|
def __getattr__(self, attr):
|
||||||
|
return getattr(self.stream, attr)
|
||||||
|
|
||||||
|
import sys
|
||||||
|
sys.stdout=Unbuffered(sys.stdout)
|
||||||
|
import os
|
||||||
|
|
||||||
|
import erdr2pml
|
||||||
|
|
||||||
|
def main(argv=sys.argv):
|
||||||
|
args = argv[1:]
|
||||||
|
if len(args) != 3:
|
||||||
|
return -1
|
||||||
|
infile = args[0]
|
||||||
|
outdir = args[1]
|
||||||
|
rscpath = args[2]
|
||||||
|
rv = 1
|
||||||
|
socialpath = os.path.join(rscpath,'sdrmlist.txt')
|
||||||
|
if os.path.exists(socialpath):
|
||||||
|
keydata = file(socialpath,'r').read()
|
||||||
|
keydata = keydata.rstrip(os.linesep)
|
||||||
|
ar = keydata.split(',')
|
||||||
|
for i in ar:
|
||||||
|
try:
|
||||||
|
name, cc8 = i.split(':')
|
||||||
|
except ValueError:
|
||||||
|
print ' Error parsing user supplied social drm data.'
|
||||||
|
return 1
|
||||||
|
rv = erdr2pml.decryptBook(infile, outdir, name, cc8, True)
|
||||||
|
if rv == 0:
|
||||||
|
break
|
||||||
|
return rv
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
|
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
|
||||||
|
|
||||||
|
class Unbuffered:
|
||||||
|
def __init__(self, stream):
|
||||||
|
self.stream = stream
|
||||||
|
def write(self, data):
|
||||||
|
self.stream.write(data)
|
||||||
|
self.stream.flush()
|
||||||
|
def __getattr__(self, attr):
|
||||||
|
return getattr(self.stream, attr)
|
||||||
|
|
||||||
|
import sys
|
||||||
|
sys.stdout=Unbuffered(sys.stdout)
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import ineptpdf
|
||||||
|
|
||||||
|
def main(argv=sys.argv):
|
||||||
|
args = argv[1:]
|
||||||
|
if len(args) != 3:
|
||||||
|
return -1
|
||||||
|
infile = args[0]
|
||||||
|
outdir = args[1]
|
||||||
|
rscpath = args[2]
|
||||||
|
errlog = ''
|
||||||
|
rv = 1
|
||||||
|
|
||||||
|
# determine a good name for the output file
|
||||||
|
name, ext = os.path.splitext(os.path.basename(infile))
|
||||||
|
outfile = os.path.join(outdir, name + '_nodrm.pdf')
|
||||||
|
|
||||||
|
# try with any keyfiles (*.der) in the rscpath
|
||||||
|
files = os.listdir(rscpath)
|
||||||
|
filefilter = re.compile("\.der$", re.IGNORECASE)
|
||||||
|
files = filter(filefilter.search, files)
|
||||||
|
if files:
|
||||||
|
for filename in files:
|
||||||
|
keypath = os.path.join(rscpath, filename)
|
||||||
|
try:
|
||||||
|
rv = ineptpdf.decryptBook(keypath, infile, outfile)
|
||||||
|
if rv == 0:
|
||||||
|
break
|
||||||
|
except Exception, e:
|
||||||
|
errlog += str(e)
|
||||||
|
rv = 1
|
||||||
|
pass
|
||||||
|
if rv != 0:
|
||||||
|
print errlog
|
||||||
|
return rv
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
|
|
||||||
504
DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/erdr2pml.py
Normal file
504
DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/erdr2pml.py
Normal file
@@ -0,0 +1,504 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
|
||||||
|
#
|
||||||
|
# erdr2pml.py
|
||||||
|
#
|
||||||
|
# This is a python script. You need a Python interpreter to run it.
|
||||||
|
# For example, ActiveState Python, which exists for windows.
|
||||||
|
# Changelog
|
||||||
|
#
|
||||||
|
# Based on ereader2html version 0.08 plus some later small fixes
|
||||||
|
#
|
||||||
|
# 0.01 - Initial version
|
||||||
|
# 0.02 - Support more eReader files. Support bold text and links. Fix PML decoder parsing bug.
|
||||||
|
# 0.03 - Fix incorrect variable usage at one place.
|
||||||
|
# 0.03b - enhancement by DeBockle (version 259 support)
|
||||||
|
# Custom version 0.03 - no change to eReader support, only usability changes
|
||||||
|
# - start of pep-8 indentation (spaces not tab), fix trailing blanks
|
||||||
|
# - version variable, only one place to change
|
||||||
|
# - added main routine, now callable as a library/module,
|
||||||
|
# means tools can add optional support for ereader2html
|
||||||
|
# - outdir is no longer a mandatory parameter (defaults based on input name if missing)
|
||||||
|
# - time taken output to stdout
|
||||||
|
# - Psyco support - reduces runtime by a factor of (over) 3!
|
||||||
|
# E.g. (~600Kb file) 90 secs down to 24 secs
|
||||||
|
# - newstyle classes
|
||||||
|
# - changed map call to list comprehension
|
||||||
|
# may not work with python 2.3
|
||||||
|
# without Psyco this reduces runtime to 90%
|
||||||
|
# E.g. 90 secs down to 77 secs
|
||||||
|
# Psyco with map calls takes longer, do not run with map in Psyco JIT!
|
||||||
|
# - izip calls used instead of zip (if available), further reduction
|
||||||
|
# in run time (factor of 4.5).
|
||||||
|
# E.g. (~600Kb file) 90 secs down to 20 secs
|
||||||
|
# - Python 2.6+ support, avoid DeprecationWarning with sha/sha1
|
||||||
|
# 0.04 - Footnote support, PML output, correct charset in html, support more PML tags
|
||||||
|
# - Feature change, dump out PML file
|
||||||
|
# - Added supprt for footnote tags. NOTE footnote ids appear to be bad (not usable)
|
||||||
|
# in some pdb files :-( due to the same id being used multiple times
|
||||||
|
# - Added correct charset encoding (pml is based on cp1252)
|
||||||
|
# - Added logging support.
|
||||||
|
# 0.05 - Improved type 272 support for sidebars, links, chapters, metainfo, etc
|
||||||
|
# 0.06 - Merge of 0.04 and 0.05. Improved HTML output
|
||||||
|
# Placed images in subfolder, so that it's possible to just
|
||||||
|
# drop the book.pml file onto DropBook to make an unencrypted
|
||||||
|
# copy of the eReader file.
|
||||||
|
# Using that with Calibre works a lot better than the HTML
|
||||||
|
# conversion in this code.
|
||||||
|
# 0.07 - Further Improved type 272 support for sidebars with all earlier fixes
|
||||||
|
# 0.08 - fixed typos, removed extraneous things
|
||||||
|
# 0.09 - fixed typos in first_pages to first_page to again support older formats
|
||||||
|
# 0.10 - minor cleanups
|
||||||
|
# 0.11 - fixups for using correct xml for footnotes and sidebars for use with Dropbook
|
||||||
|
# 0.12 - Fix added to prevent lowercasing of image names when the pml code itself uses a different case in the link name.
|
||||||
|
# 0.13 - change to unbuffered stdout for use with gui front ends
|
||||||
|
# 0.14 - contributed enhancement to support --make-pmlz switch
|
||||||
|
# 0.15 - enabled high-ascii to pml character encoding. DropBook now works on Mac.
|
||||||
|
# 0.16 - convert to use openssl DES (very very fast) or pure python DES if openssl's libcrypto is not available
|
||||||
|
# 0.17 - added support for pycrypto's DES as well
|
||||||
|
# 0.18 - on Windows try PyCrypto first and OpenSSL next
|
||||||
|
# 0.19 - Modify the interface to allow use of import
|
||||||
|
# 0.20 - modify to allow use inside new interface for calibre plugins
|
||||||
|
|
||||||
|
__version__='0.20'
|
||||||
|
|
||||||
|
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 struct, binascii, getopt, zlib, os, os.path, urllib, tempfile
|
||||||
|
|
||||||
|
if 'calibre' in sys.modules:
|
||||||
|
inCalibre = True
|
||||||
|
else:
|
||||||
|
inCalibre = False
|
||||||
|
|
||||||
|
Des = None
|
||||||
|
if sys.platform.startswith('win'):
|
||||||
|
# first try with pycrypto
|
||||||
|
if inCalibre:
|
||||||
|
from calibre_plugins.erdrpdb2pml import pycrypto_des
|
||||||
|
else:
|
||||||
|
import pycrypto_des
|
||||||
|
Des = pycrypto_des.load_pycrypto()
|
||||||
|
if Des == None:
|
||||||
|
# they try with openssl
|
||||||
|
if inCalibre:
|
||||||
|
from calibre_plugins.erdrpdb2pml import openssl_des
|
||||||
|
else:
|
||||||
|
import openssl_des
|
||||||
|
Des = openssl_des.load_libcrypto()
|
||||||
|
else:
|
||||||
|
# first try with openssl
|
||||||
|
if inCalibre:
|
||||||
|
from calibre_plugins.erdrpdb2pml import openssl_des
|
||||||
|
else:
|
||||||
|
import openssl_des
|
||||||
|
Des = openssl_des.load_libcrypto()
|
||||||
|
if Des == None:
|
||||||
|
# then try with pycrypto
|
||||||
|
if inCalibre:
|
||||||
|
from calibre_plugins.erdrpdb2pml import pycrypto_des
|
||||||
|
else:
|
||||||
|
import pycrypto_des
|
||||||
|
Des = pycrypto_des.load_pycrypto()
|
||||||
|
|
||||||
|
# if that did not work then use pure python implementation
|
||||||
|
# of DES and try to speed it up with Psycho
|
||||||
|
if Des == None:
|
||||||
|
if inCalibre:
|
||||||
|
from calibre_plugins.erdrpdb2pml import python_des
|
||||||
|
else:
|
||||||
|
import python_des
|
||||||
|
Des = python_des.Des
|
||||||
|
# Import Psyco if available
|
||||||
|
try:
|
||||||
|
# http://psyco.sourceforge.net
|
||||||
|
import psyco
|
||||||
|
psyco.full()
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
from hashlib import sha1
|
||||||
|
except ImportError:
|
||||||
|
# older Python release
|
||||||
|
import sha
|
||||||
|
sha1 = lambda s: sha.new(s)
|
||||||
|
|
||||||
|
import cgi
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logging.basicConfig()
|
||||||
|
#logging.basicConfig(level=logging.DEBUG)
|
||||||
|
|
||||||
|
|
||||||
|
class Sectionizer(object):
|
||||||
|
def __init__(self, filename, ident):
|
||||||
|
self.contents = file(filename, 'rb').read()
|
||||||
|
self.header = self.contents[0:72]
|
||||||
|
self.num_sections, = struct.unpack('>H', self.contents[76:78])
|
||||||
|
if self.header[0x3C:0x3C+8] != ident:
|
||||||
|
raise ValueError('Invalid file format')
|
||||||
|
self.sections = []
|
||||||
|
for i in xrange(self.num_sections):
|
||||||
|
offset, a1,a2,a3,a4 = struct.unpack('>LBBBB', self.contents[78+i*8:78+i*8+8])
|
||||||
|
flags, val = a1, a2<<16|a3<<8|a4
|
||||||
|
self.sections.append( (offset, flags, val) )
|
||||||
|
def loadSection(self, section):
|
||||||
|
if section + 1 == self.num_sections:
|
||||||
|
end_off = len(self.contents)
|
||||||
|
else:
|
||||||
|
end_off = self.sections[section + 1][0]
|
||||||
|
off = self.sections[section][0]
|
||||||
|
return self.contents[off:end_off]
|
||||||
|
|
||||||
|
def sanitizeFileName(s):
|
||||||
|
r = ''
|
||||||
|
for c in s:
|
||||||
|
if c in "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_.-":
|
||||||
|
r += c
|
||||||
|
return r
|
||||||
|
|
||||||
|
def fixKey(key):
|
||||||
|
def fixByte(b):
|
||||||
|
return b ^ ((b ^ (b<<1) ^ (b<<2) ^ (b<<3) ^ (b<<4) ^ (b<<5) ^ (b<<6) ^ (b<<7) ^ 0x80) & 0x80)
|
||||||
|
return "".join([chr(fixByte(ord(a))) for a in key])
|
||||||
|
|
||||||
|
def deXOR(text, sp, table):
|
||||||
|
r=''
|
||||||
|
j = sp
|
||||||
|
for i in xrange(len(text)):
|
||||||
|
r += chr(ord(table[j]) ^ ord(text[i]))
|
||||||
|
j = j + 1
|
||||||
|
if j == len(table):
|
||||||
|
j = 0
|
||||||
|
return r
|
||||||
|
|
||||||
|
class EreaderProcessor(object):
|
||||||
|
def __init__(self, section_reader, username, creditcard):
|
||||||
|
self.section_reader = section_reader
|
||||||
|
data = section_reader(0)
|
||||||
|
version, = struct.unpack('>H', data[0:2])
|
||||||
|
self.version = version
|
||||||
|
logging.info('eReader file format version %s', version)
|
||||||
|
if version != 272 and version != 260 and version != 259:
|
||||||
|
raise ValueError('incorrect eReader version %d (error 1)' % version)
|
||||||
|
data = section_reader(1)
|
||||||
|
self.data = data
|
||||||
|
des = Des(fixKey(data[0:8]))
|
||||||
|
cookie_shuf, cookie_size = struct.unpack('>LL', des.decrypt(data[-8:]))
|
||||||
|
if cookie_shuf < 3 or cookie_shuf > 0x14 or cookie_size < 0xf0 or cookie_size > 0x200:
|
||||||
|
raise ValueError('incorrect eReader version (error 2)')
|
||||||
|
input = des.decrypt(data[-cookie_size:])
|
||||||
|
def unshuff(data, shuf):
|
||||||
|
r = [''] * len(data)
|
||||||
|
j = 0
|
||||||
|
for i in xrange(len(data)):
|
||||||
|
j = (j + shuf) % len(data)
|
||||||
|
r[j] = data[i]
|
||||||
|
assert len("".join(r)) == len(data)
|
||||||
|
return "".join(r)
|
||||||
|
r = unshuff(input[0:-8], cookie_shuf)
|
||||||
|
|
||||||
|
def fixUsername(s):
|
||||||
|
r = ''
|
||||||
|
for c in s.lower():
|
||||||
|
if (c >= 'a' and c <= 'z' or c >= '0' and c <= '9'):
|
||||||
|
r += c
|
||||||
|
return r
|
||||||
|
|
||||||
|
user_key = struct.pack('>LL', binascii.crc32(fixUsername(username)) & 0xffffffff, binascii.crc32(creditcard[-8:])& 0xffffffff)
|
||||||
|
drm_sub_version = struct.unpack('>H', r[0:2])[0]
|
||||||
|
self.num_text_pages = struct.unpack('>H', r[2:4])[0] - 1
|
||||||
|
self.num_image_pages = struct.unpack('>H', r[26:26+2])[0]
|
||||||
|
self.first_image_page = struct.unpack('>H', r[24:24+2])[0]
|
||||||
|
if self.version == 272:
|
||||||
|
self.num_footnote_pages = struct.unpack('>H', r[46:46+2])[0]
|
||||||
|
self.first_footnote_page = struct.unpack('>H', r[44:44+2])[0]
|
||||||
|
self.num_sidebar_pages = struct.unpack('>H', r[38:38+2])[0]
|
||||||
|
self.first_sidebar_page = struct.unpack('>H', r[36:36+2])[0]
|
||||||
|
# self.num_bookinfo_pages = struct.unpack('>H', r[34:34+2])[0]
|
||||||
|
# self.first_bookinfo_page = struct.unpack('>H', r[32:32+2])[0]
|
||||||
|
# self.num_chapter_pages = struct.unpack('>H', r[22:22+2])[0]
|
||||||
|
# self.first_chapter_page = struct.unpack('>H', r[20:20+2])[0]
|
||||||
|
# self.num_link_pages = struct.unpack('>H', r[30:30+2])[0]
|
||||||
|
# self.first_link_page = struct.unpack('>H', r[28:28+2])[0]
|
||||||
|
# self.num_xtextsize_pages = struct.unpack('>H', r[54:54+2])[0]
|
||||||
|
# self.first_xtextsize_page = struct.unpack('>H', r[52:52+2])[0]
|
||||||
|
|
||||||
|
# **before** data record 1 was decrypted and unshuffled, it contained data
|
||||||
|
# to create an XOR table and which is used to fix footnote record 0, link records, chapter records, etc
|
||||||
|
self.xortable_offset = struct.unpack('>H', r[40:40+2])[0]
|
||||||
|
self.xortable_size = struct.unpack('>H', r[42:42+2])[0]
|
||||||
|
self.xortable = self.data[self.xortable_offset:self.xortable_offset + self.xortable_size]
|
||||||
|
else:
|
||||||
|
self.num_footnote_pages = 0
|
||||||
|
self.num_sidebar_pages = 0
|
||||||
|
self.first_footnote_page = -1
|
||||||
|
self.first_sidebar_page = -1
|
||||||
|
# self.num_bookinfo_pages = 0
|
||||||
|
# self.num_chapter_pages = 0
|
||||||
|
# self.num_link_pages = 0
|
||||||
|
# self.num_xtextsize_pages = 0
|
||||||
|
# self.first_bookinfo_page = -1
|
||||||
|
# self.first_chapter_page = -1
|
||||||
|
# self.first_link_page = -1
|
||||||
|
# self.first_xtextsize_page = -1
|
||||||
|
|
||||||
|
logging.debug('self.num_text_pages %d', self.num_text_pages)
|
||||||
|
logging.debug('self.num_footnote_pages %d, self.first_footnote_page %d', self.num_footnote_pages , self.first_footnote_page)
|
||||||
|
logging.debug('self.num_sidebar_pages %d, self.first_sidebar_page %d', self.num_sidebar_pages , self.first_sidebar_page)
|
||||||
|
self.flags = struct.unpack('>L', r[4:8])[0]
|
||||||
|
reqd_flags = (1<<9) | (1<<7) | (1<<10)
|
||||||
|
if (self.flags & reqd_flags) != reqd_flags:
|
||||||
|
print "Flags: 0x%X" % self.flags
|
||||||
|
raise ValueError('incompatible eReader file')
|
||||||
|
des = Des(fixKey(user_key))
|
||||||
|
if version == 259:
|
||||||
|
if drm_sub_version != 7:
|
||||||
|
raise ValueError('incorrect eReader version %d (error 3)' % drm_sub_version)
|
||||||
|
encrypted_key_sha = r[44:44+20]
|
||||||
|
encrypted_key = r[64:64+8]
|
||||||
|
elif version == 260:
|
||||||
|
if drm_sub_version != 13:
|
||||||
|
raise ValueError('incorrect eReader version %d (error 3)' % drm_sub_version)
|
||||||
|
encrypted_key = r[44:44+8]
|
||||||
|
encrypted_key_sha = r[52:52+20]
|
||||||
|
elif version == 272:
|
||||||
|
encrypted_key = r[172:172+8]
|
||||||
|
encrypted_key_sha = r[56:56+20]
|
||||||
|
self.content_key = des.decrypt(encrypted_key)
|
||||||
|
if sha1(self.content_key).digest() != encrypted_key_sha:
|
||||||
|
raise ValueError('Incorrect Name and/or Credit Card')
|
||||||
|
|
||||||
|
def getNumImages(self):
|
||||||
|
return self.num_image_pages
|
||||||
|
|
||||||
|
def getImage(self, i):
|
||||||
|
sect = self.section_reader(self.first_image_page + i)
|
||||||
|
name = sect[4:4+32].strip('\0')
|
||||||
|
data = sect[62:]
|
||||||
|
return sanitizeFileName(name), data
|
||||||
|
|
||||||
|
|
||||||
|
# def getChapterNamePMLOffsetData(self):
|
||||||
|
# cv = ''
|
||||||
|
# if self.num_chapter_pages > 0:
|
||||||
|
# for i in xrange(self.num_chapter_pages):
|
||||||
|
# chaps = self.section_reader(self.first_chapter_page + i)
|
||||||
|
# j = i % self.xortable_size
|
||||||
|
# offname = deXOR(chaps, j, self.xortable)
|
||||||
|
# offset = struct.unpack('>L', offname[0:4])[0]
|
||||||
|
# name = offname[4:].strip('\0')
|
||||||
|
# cv += '%d|%s\n' % (offset, name)
|
||||||
|
# return cv
|
||||||
|
|
||||||
|
# def getLinkNamePMLOffsetData(self):
|
||||||
|
# lv = ''
|
||||||
|
# if self.num_link_pages > 0:
|
||||||
|
# for i in xrange(self.num_link_pages):
|
||||||
|
# links = self.section_reader(self.first_link_page + i)
|
||||||
|
# j = i % self.xortable_size
|
||||||
|
# offname = deXOR(links, j, self.xortable)
|
||||||
|
# offset = struct.unpack('>L', offname[0:4])[0]
|
||||||
|
# name = offname[4:].strip('\0')
|
||||||
|
# lv += '%d|%s\n' % (offset, name)
|
||||||
|
# return lv
|
||||||
|
|
||||||
|
# def getExpandedTextSizesData(self):
|
||||||
|
# ts = ''
|
||||||
|
# if self.num_xtextsize_pages > 0:
|
||||||
|
# tsize = deXOR(self.section_reader(self.first_xtextsize_page), 0, self.xortable)
|
||||||
|
# for i in xrange(self.num_text_pages):
|
||||||
|
# xsize = struct.unpack('>H', tsize[0:2])[0]
|
||||||
|
# ts += "%d\n" % xsize
|
||||||
|
# tsize = tsize[2:]
|
||||||
|
# return ts
|
||||||
|
|
||||||
|
# def getBookInfo(self):
|
||||||
|
# bkinfo = ''
|
||||||
|
# if self.num_bookinfo_pages > 0:
|
||||||
|
# info = self.section_reader(self.first_bookinfo_page)
|
||||||
|
# bkinfo = deXOR(info, 0, self.xortable)
|
||||||
|
# bkinfo = bkinfo.replace('\0','|')
|
||||||
|
# bkinfo += '\n'
|
||||||
|
# return bkinfo
|
||||||
|
|
||||||
|
def getText(self):
|
||||||
|
des = Des(fixKey(self.content_key))
|
||||||
|
r = ''
|
||||||
|
for i in xrange(self.num_text_pages):
|
||||||
|
logging.debug('get page %d', i)
|
||||||
|
r += zlib.decompress(des.decrypt(self.section_reader(1 + i)))
|
||||||
|
|
||||||
|
# now handle footnotes pages
|
||||||
|
if self.num_footnote_pages > 0:
|
||||||
|
r += '\n'
|
||||||
|
# the record 0 of the footnote section must pass through the Xor Table to make it useful
|
||||||
|
sect = self.section_reader(self.first_footnote_page)
|
||||||
|
fnote_ids = deXOR(sect, 0, self.xortable)
|
||||||
|
# the remaining records of the footnote sections need to be decoded with the content_key and zlib inflated
|
||||||
|
des = Des(fixKey(self.content_key))
|
||||||
|
for i in xrange(1,self.num_footnote_pages):
|
||||||
|
logging.debug('get footnotepage %d', i)
|
||||||
|
id_len = ord(fnote_ids[2])
|
||||||
|
id = fnote_ids[3:3+id_len]
|
||||||
|
fmarker = '<footnote id="%s">\n' % id
|
||||||
|
fmarker += zlib.decompress(des.decrypt(self.section_reader(self.first_footnote_page + i)))
|
||||||
|
fmarker += '\n</footnote>\n'
|
||||||
|
r += fmarker
|
||||||
|
fnote_ids = fnote_ids[id_len+4:]
|
||||||
|
|
||||||
|
# now handle sidebar pages
|
||||||
|
if self.num_sidebar_pages > 0:
|
||||||
|
r += '\n'
|
||||||
|
# the record 0 of the sidebar section must pass through the Xor Table to make it useful
|
||||||
|
sect = self.section_reader(self.first_sidebar_page)
|
||||||
|
sbar_ids = deXOR(sect, 0, self.xortable)
|
||||||
|
# the remaining records of the sidebar sections need to be decoded with the content_key and zlib inflated
|
||||||
|
des = Des(fixKey(self.content_key))
|
||||||
|
for i in xrange(1,self.num_sidebar_pages):
|
||||||
|
id_len = ord(sbar_ids[2])
|
||||||
|
id = sbar_ids[3:3+id_len]
|
||||||
|
smarker = '<sidebar id="%s">\n' % id
|
||||||
|
smarker += zlib.decompress(des.decrypt(self.section_reader(self.first_footnote_page + i)))
|
||||||
|
smarker += '\n</sidebar>\n'
|
||||||
|
r += smarker
|
||||||
|
sbar_ids = sbar_ids[id_len+4:]
|
||||||
|
|
||||||
|
return r
|
||||||
|
|
||||||
|
def cleanPML(pml):
|
||||||
|
# Convert special characters to proper PML code. High ASCII start at (\x80, \a128) and go up to (\xff, \a255)
|
||||||
|
pml2 = pml
|
||||||
|
for k in 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)
|
||||||
|
bookname = os.path.splitext(os.path.basename(infile))[0]
|
||||||
|
print " Decoding File"
|
||||||
|
sect = Sectionizer(infile, 'PNRdPPrs')
|
||||||
|
er = EreaderProcessor(sect.loadSection, 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
|
||||||
|
outdir = tempfile.mkdtemp()
|
||||||
|
try:
|
||||||
|
print "Processing..."
|
||||||
|
convertEreaderToPml(infile, name, cc, outdir)
|
||||||
|
if make_pmlz :
|
||||||
|
import zipfile
|
||||||
|
import shutil
|
||||||
|
print " Creating PMLZ file"
|
||||||
|
zipname = infile[:-4] + '.pmlz'
|
||||||
|
myZipFile = zipfile.ZipFile(zipname,'w',zipfile.ZIP_STORED, False)
|
||||||
|
list = os.listdir(outdir)
|
||||||
|
for file in list:
|
||||||
|
localname = file
|
||||||
|
filePath = os.path.join(outdir,file)
|
||||||
|
if os.path.isfile(filePath):
|
||||||
|
myZipFile.write(filePath, localname)
|
||||||
|
elif os.path.isdir(filePath):
|
||||||
|
imageList = os.listdir(filePath)
|
||||||
|
localimgdir = os.path.basename(filePath)
|
||||||
|
for image in imageList:
|
||||||
|
localname = os.path.join(localimgdir,image)
|
||||||
|
imagePath = os.path.join(filePath,image)
|
||||||
|
if os.path.isfile(imagePath):
|
||||||
|
myZipFile.write(imagePath, localname)
|
||||||
|
myZipFile.close()
|
||||||
|
# remove temporary directory
|
||||||
|
shutil.rmtree(outdir, True)
|
||||||
|
print 'output is %s' % zipname
|
||||||
|
else :
|
||||||
|
print 'output in %s' % outdir
|
||||||
|
print "done"
|
||||||
|
except ValueError, e:
|
||||||
|
print "Error: %s" % e
|
||||||
|
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"
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv=None):
|
||||||
|
try:
|
||||||
|
opts, args = getopt.getopt(sys.argv[1:], "h", ["make-pmlz"])
|
||||||
|
except getopt.GetoptError, err:
|
||||||
|
print str(err)
|
||||||
|
usage()
|
||||||
|
return 1
|
||||||
|
make_pmlz = False
|
||||||
|
for o, a in opts:
|
||||||
|
if o == "-h":
|
||||||
|
usage()
|
||||||
|
return 0
|
||||||
|
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'
|
||||||
|
elif len(args)==4:
|
||||||
|
infile, outdir, name, cc = args[0], args[1], args[2], args[3]
|
||||||
|
|
||||||
|
return decryptBook(infile, outdir, name, cc, make_pmlz)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.stdout=Unbuffered(sys.stdout)
|
||||||
|
sys.exit(main())
|
||||||
|
|
||||||
@@ -68,7 +68,7 @@ class DocParser(object):
|
|||||||
ys = []
|
ys = []
|
||||||
gdefs = []
|
gdefs = []
|
||||||
|
|
||||||
# get path defintions, positions, dimensions for ecah glyph
|
# get path defintions, positions, dimensions for each glyph
|
||||||
# that makes up the image, and find min x and min y to reposition origin
|
# that makes up the image, and find min x and min y to reposition origin
|
||||||
minx = -1
|
minx = -1
|
||||||
miny = -1
|
miny = -1
|
||||||
@@ -305,6 +305,15 @@ class DocParser(object):
|
|||||||
lastGlyph = firstglyphList[last]
|
lastGlyph = firstglyphList[last]
|
||||||
else :
|
else :
|
||||||
lastGlyph = len(gidList)
|
lastGlyph = len(gidList)
|
||||||
|
|
||||||
|
# handle case of white sapce paragraphs with no actual glyphs in them
|
||||||
|
# by reverting to text based paragraph
|
||||||
|
if firstGlyph >= lastGlyph:
|
||||||
|
# revert to standard text based paragraph
|
||||||
|
for wordnum in xrange(first, last):
|
||||||
|
result.append(('ocr', wordnum))
|
||||||
|
return pclass, result
|
||||||
|
|
||||||
for glyphnum in xrange(firstGlyph, lastGlyph):
|
for glyphnum in xrange(firstGlyph, lastGlyph):
|
||||||
glyphList.append(glyphnum)
|
glyphList.append(glyphnum)
|
||||||
# include any extratokens if they exist
|
# include any extratokens if they exist
|
||||||
@@ -21,6 +21,17 @@ from struct import unpack
|
|||||||
|
|
||||||
|
|
||||||
# local support routines
|
# local support routines
|
||||||
|
if 'calibre' in sys.modules:
|
||||||
|
inCalibre = True
|
||||||
|
else:
|
||||||
|
inCalibre = False
|
||||||
|
|
||||||
|
if inCalibre :
|
||||||
|
from calibre_plugins.k4mobidedrm import convert2xml
|
||||||
|
from calibre_plugins.k4mobidedrm import flatxml2html
|
||||||
|
from calibre_plugins.k4mobidedrm import flatxml2svg
|
||||||
|
from calibre_plugins.k4mobidedrm import stylexml2css
|
||||||
|
else :
|
||||||
import convert2xml
|
import convert2xml
|
||||||
import flatxml2html
|
import flatxml2html
|
||||||
import flatxml2svg
|
import flatxml2svg
|
||||||
@@ -192,6 +203,8 @@ class GParser(object):
|
|||||||
argres[j] = int(argres[j])
|
argres[j] = int(argres[j])
|
||||||
return result
|
return result
|
||||||
def getGlyphDim(self, gly):
|
def getGlyphDim(self, gly):
|
||||||
|
if self.gdpi[gly] == 0:
|
||||||
|
return 0, 0
|
||||||
maxh = (self.gh[gly] * self.dpi) / self.gdpi[gly]
|
maxh = (self.gh[gly] * self.dpi) / self.gdpi[gly]
|
||||||
maxw = (self.gw[gly] * self.dpi) / self.gdpi[gly]
|
maxw = (self.gw[gly] * self.dpi) / self.gdpi[gly]
|
||||||
return maxh, maxw
|
return maxh, maxw
|
||||||
@@ -320,6 +333,18 @@ def generateBook(bookDir, raw, fixedimage):
|
|||||||
print 'Processing Meta Data and creating OPF'
|
print 'Processing Meta Data and creating OPF'
|
||||||
meta_array = getMetaArray(metaFile)
|
meta_array = getMetaArray(metaFile)
|
||||||
|
|
||||||
|
# replace special chars in title and authors like & < >
|
||||||
|
title = meta_array.get('Title','No Title Provided')
|
||||||
|
title = title.replace('&','&')
|
||||||
|
title = title.replace('<','<')
|
||||||
|
title = title.replace('>','>')
|
||||||
|
meta_array['Title'] = title
|
||||||
|
authors = meta_array.get('Authors','No Authors Provided')
|
||||||
|
authors = authors.replace('&','&')
|
||||||
|
authors = authors.replace('<','<')
|
||||||
|
authors = authors.replace('>','>')
|
||||||
|
meta_array['Authors'] = authors
|
||||||
|
|
||||||
xname = os.path.join(xmlDir, 'metadata.xml')
|
xname = os.path.join(xmlDir, 'metadata.xml')
|
||||||
metastr = ''
|
metastr = ''
|
||||||
for key in meta_array:
|
for key in meta_array:
|
||||||
@@ -399,7 +424,9 @@ def generateBook(bookDir, raw, fixedimage):
|
|||||||
htmlstr += '<title>' + meta_array['Title'] + ' by ' + meta_array['Authors'] + '</title>\n'
|
htmlstr += '<title>' + meta_array['Title'] + ' by ' + meta_array['Authors'] + '</title>\n'
|
||||||
htmlstr += '<meta name="Author" content="' + meta_array['Authors'] + '" />\n'
|
htmlstr += '<meta name="Author" content="' + meta_array['Authors'] + '" />\n'
|
||||||
htmlstr += '<meta name="Title" content="' + meta_array['Title'] + '" />\n'
|
htmlstr += '<meta name="Title" content="' + meta_array['Title'] + '" />\n'
|
||||||
|
if 'ASIN' in meta_array:
|
||||||
htmlstr += '<meta name="ASIN" content="' + meta_array['ASIN'] + '" />\n'
|
htmlstr += '<meta name="ASIN" content="' + meta_array['ASIN'] + '" />\n'
|
||||||
|
if 'GUID' in meta_array:
|
||||||
htmlstr += '<meta name="GUID" content="' + meta_array['GUID'] + '" />\n'
|
htmlstr += '<meta name="GUID" content="' + meta_array['GUID'] + '" />\n'
|
||||||
htmlstr += '<link href="style.css" rel="stylesheet" type="text/css" />\n'
|
htmlstr += '<link href="style.css" rel="stylesheet" type="text/css" />\n'
|
||||||
htmlstr += '</head>\n<body>\n'
|
htmlstr += '</head>\n<body>\n'
|
||||||
@@ -416,7 +443,9 @@ def generateBook(bookDir, raw, fixedimage):
|
|||||||
svgindex += '<title>' + meta_array['Title'] + '</title>\n'
|
svgindex += '<title>' + meta_array['Title'] + '</title>\n'
|
||||||
svgindex += '<meta name="Author" content="' + meta_array['Authors'] + '" />\n'
|
svgindex += '<meta name="Author" content="' + meta_array['Authors'] + '" />\n'
|
||||||
svgindex += '<meta name="Title" content="' + meta_array['Title'] + '" />\n'
|
svgindex += '<meta name="Title" content="' + meta_array['Title'] + '" />\n'
|
||||||
|
if 'ASIN' in meta_array:
|
||||||
svgindex += '<meta name="ASIN" content="' + meta_array['ASIN'] + '" />\n'
|
svgindex += '<meta name="ASIN" content="' + meta_array['ASIN'] + '" />\n'
|
||||||
|
if 'GUID' in meta_array:
|
||||||
svgindex += '<meta name="GUID" content="' + meta_array['GUID'] + '" />\n'
|
svgindex += '<meta name="GUID" content="' + meta_array['GUID'] + '" />\n'
|
||||||
svgindex += '</head>\n'
|
svgindex += '</head>\n'
|
||||||
svgindex += '<body>\n'
|
svgindex += '<body>\n'
|
||||||
@@ -471,8 +500,11 @@ def generateBook(bookDir, raw, fixedimage):
|
|||||||
opfstr += '<package xmlns="http://www.idpf.org/2007/opf" unique-identifier="guid_id">\n'
|
opfstr += '<package xmlns="http://www.idpf.org/2007/opf" unique-identifier="guid_id">\n'
|
||||||
# adding metadata
|
# adding metadata
|
||||||
opfstr += ' <metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf">\n'
|
opfstr += ' <metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf">\n'
|
||||||
|
if 'GUID' in meta_array:
|
||||||
opfstr += ' <dc:identifier opf:scheme="GUID" id="guid_id">' + meta_array['GUID'] + '</dc:identifier>\n'
|
opfstr += ' <dc:identifier opf:scheme="GUID" id="guid_id">' + meta_array['GUID'] + '</dc:identifier>\n'
|
||||||
|
if 'ASIN' in meta_array:
|
||||||
opfstr += ' <dc:identifier opf:scheme="ASIN">' + meta_array['ASIN'] + '</dc:identifier>\n'
|
opfstr += ' <dc:identifier opf:scheme="ASIN">' + meta_array['ASIN'] + '</dc:identifier>\n'
|
||||||
|
if 'oASIN' in meta_array:
|
||||||
opfstr += ' <dc:identifier opf:scheme="oASIN">' + meta_array['oASIN'] + '</dc:identifier>\n'
|
opfstr += ' <dc:identifier opf:scheme="oASIN">' + meta_array['oASIN'] + '</dc:identifier>\n'
|
||||||
opfstr += ' <dc:title>' + meta_array['Title'] + '</dc:title>\n'
|
opfstr += ' <dc:title>' + meta_array['Title'] + '</dc:title>\n'
|
||||||
opfstr += ' <dc:creator opf:role="aut">' + meta_array['Authors'] + '</dc:creator>\n'
|
opfstr += ' <dc:creator opf:role="aut">' + meta_array['Authors'] + '</dc:creator>\n'
|
||||||
@@ -483,7 +515,7 @@ def generateBook(bookDir, raw, fixedimage):
|
|||||||
opfstr += ' </metadata>\n'
|
opfstr += ' </metadata>\n'
|
||||||
opfstr += '<manifest>\n'
|
opfstr += '<manifest>\n'
|
||||||
opfstr += ' <item id="book" href="book.html" media-type="application/xhtml+xml"/>\n'
|
opfstr += ' <item id="book" href="book.html" media-type="application/xhtml+xml"/>\n'
|
||||||
opfstr += ' <item id="stylesheet" href="style.css" media-type="text.css"/>\n'
|
opfstr += ' <item id="stylesheet" href="style.css" media-type="text/css"/>\n'
|
||||||
# adding image files to manifest
|
# adding image files to manifest
|
||||||
filenames = os.listdir(imgDir)
|
filenames = os.listdir(imgDir)
|
||||||
filenames = sorted(filenames)
|
filenames = sorted(filenames)
|
||||||
@@ -0,0 +1,336 @@
|
|||||||
|
#! /usr/bin/python
|
||||||
|
|
||||||
|
from __future__ import with_statement
|
||||||
|
|
||||||
|
# ignobleepub.pyw, version 3.4
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
# Revision history:
|
||||||
|
# 1 - Initial release
|
||||||
|
# 2 - Added OS X support by using OpenSSL when available
|
||||||
|
# 3 - screen out improper key lengths to prevent segfaults on Linux
|
||||||
|
# 3.1 - Allow Windows versions of libcrypto to be found
|
||||||
|
# 3.2 - add support for encoding to 'utf-8' when building up list of files to cecrypt from encryption.xml
|
||||||
|
# 3.3 - On Windows try PyCrypto first and OpenSSL next
|
||||||
|
# 3.4 - Modify interace to allow use with import
|
||||||
|
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import zlib
|
||||||
|
import zipfile
|
||||||
|
from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED
|
||||||
|
from contextlib import closing
|
||||||
|
import xml.etree.ElementTree as etree
|
||||||
|
import Tkinter
|
||||||
|
import Tkconstants
|
||||||
|
import tkFileDialog
|
||||||
|
import tkMessageBox
|
||||||
|
|
||||||
|
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'):
|
||||||
|
libcrypto = find_library('libeay32')
|
||||||
|
else:
|
||||||
|
libcrypto = find_library('crypto')
|
||||||
|
if libcrypto is None:
|
||||||
|
raise IGNOBLEError('libcrypto not found')
|
||||||
|
libcrypto = CDLL(libcrypto)
|
||||||
|
|
||||||
|
AES_MAXNR = 14
|
||||||
|
|
||||||
|
c_char_pp = POINTER(c_char_p)
|
||||||
|
c_int_p = POINTER(c_int)
|
||||||
|
|
||||||
|
class AES_KEY(Structure):
|
||||||
|
_fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))),
|
||||||
|
('rounds', c_int)]
|
||||||
|
AES_KEY_p = POINTER(AES_KEY)
|
||||||
|
|
||||||
|
def F(restype, name, argtypes):
|
||||||
|
func = getattr(libcrypto, name)
|
||||||
|
func.restype = restype
|
||||||
|
func.argtypes = argtypes
|
||||||
|
return func
|
||||||
|
|
||||||
|
AES_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',
|
||||||
|
[c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p,
|
||||||
|
c_int])
|
||||||
|
|
||||||
|
class AES(object):
|
||||||
|
def __init__(self, userkey):
|
||||||
|
self._blocksize = len(userkey)
|
||||||
|
if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) :
|
||||||
|
raise IGNOBLEError('AES improper key used')
|
||||||
|
return
|
||||||
|
key = self._key = AES_KEY()
|
||||||
|
rv = AES_set_decrypt_key(userkey, len(userkey) * 8, key)
|
||||||
|
if rv < 0:
|
||||||
|
raise IGNOBLEError('Failed to initialize AES key')
|
||||||
|
|
||||||
|
def decrypt(self, data):
|
||||||
|
out = create_string_buffer(len(data))
|
||||||
|
iv = ("\x00" * self._blocksize)
|
||||||
|
rv = AES_cbc_encrypt(data, out, len(data), self._key, iv, 0)
|
||||||
|
if rv == 0:
|
||||||
|
raise IGNOBLEError('AES decryption failed')
|
||||||
|
return out.raw
|
||||||
|
|
||||||
|
return AES
|
||||||
|
|
||||||
|
def _load_crypto_pycrypto():
|
||||||
|
from Crypto.Cipher import AES as _AES
|
||||||
|
|
||||||
|
class AES(object):
|
||||||
|
def __init__(self, key):
|
||||||
|
self._aes = _AES.new(key, _AES.MODE_CBC)
|
||||||
|
|
||||||
|
def decrypt(self, data):
|
||||||
|
return self._aes.decrypt(data)
|
||||||
|
|
||||||
|
return AES
|
||||||
|
|
||||||
|
def _load_crypto():
|
||||||
|
AES = None
|
||||||
|
cryptolist = (_load_crypto_libcrypto, _load_crypto_pycrypto)
|
||||||
|
if sys.platform.startswith('win'):
|
||||||
|
cryptolist = (_load_crypto_pycrypto, _load_crypto_libcrypto)
|
||||||
|
for loader in cryptolist:
|
||||||
|
try:
|
||||||
|
AES = loader()
|
||||||
|
break
|
||||||
|
except (ImportError, IGNOBLEError):
|
||||||
|
pass
|
||||||
|
return AES
|
||||||
|
|
||||||
|
AES = _load_crypto()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
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#'}
|
||||||
|
|
||||||
|
class ZipInfo(zipfile.ZipInfo):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
if 'compress_type' in kwargs:
|
||||||
|
compress_type = kwargs.pop('compress_type')
|
||||||
|
super(ZipInfo, self).__init__(*args, **kwargs)
|
||||||
|
self.compress_type = compress_type
|
||||||
|
|
||||||
|
class Decryptor(object):
|
||||||
|
def __init__(self, bookkey, encryption):
|
||||||
|
enc = lambda tag: '{%s}%s' % (NSMAP['enc'], tag)
|
||||||
|
# self._aes = AES.new(bookkey, AES.MODE_CBC)
|
||||||
|
self._aes = AES(bookkey)
|
||||||
|
encryption = etree.fromstring(encryption)
|
||||||
|
self._encrypted = encrypted = set()
|
||||||
|
expr = './%s/%s/%s' % (enc('EncryptedData'), enc('CipherData'),
|
||||||
|
enc('CipherReference'))
|
||||||
|
for elem in encryption.findall(expr):
|
||||||
|
path = elem.get('URI', None)
|
||||||
|
path = path.encode('utf-8')
|
||||||
|
if path is not None:
|
||||||
|
encrypted.add(path)
|
||||||
|
|
||||||
|
def decompress(self, bytes):
|
||||||
|
dc = zlib.decompressobj(-15)
|
||||||
|
bytes = dc.decompress(bytes)
|
||||||
|
ex = dc.decompress('Z') + dc.flush()
|
||||||
|
if ex:
|
||||||
|
bytes = bytes + ex
|
||||||
|
return bytes
|
||||||
|
|
||||||
|
def decrypt(self, path, data):
|
||||||
|
if path in self._encrypted:
|
||||||
|
data = self._aes.decrypt(data)[16:]
|
||||||
|
data = data[:-ord(data[-1])]
|
||||||
|
data = self.decompress(data)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
class 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)
|
||||||
|
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:
|
||||||
|
raise IGNOBLEError('%s: not an B&N ADEPT EPUB' % (inpath,))
|
||||||
|
for name in META_NAMES:
|
||||||
|
namelist.remove(name)
|
||||||
|
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
|
||||||
|
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
||||||
|
expr = './/%s' % (adept('encryptedKey'),)
|
||||||
|
bookkey = ''.join(rights.findtext(expr))
|
||||||
|
bookkey = aes.decrypt(bookkey.decode('base64'))
|
||||||
|
bookkey = bookkey[:-ord(bookkey[-1])]
|
||||||
|
encryption = inf.read('META-INF/encryption.xml')
|
||||||
|
decryptor = Decryptor(bookkey[-16:], encryption)
|
||||||
|
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
|
||||||
|
with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf:
|
||||||
|
zi = ZipInfo('mimetype', compress_type=ZIP_STORED)
|
||||||
|
outf.writestr(zi, inf.read('mimetype'))
|
||||||
|
for path in namelist:
|
||||||
|
data = inf.read(path)
|
||||||
|
outf.writestr(path, decryptor.decrypt(path, data))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cli_main(argv=sys.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,)
|
||||||
|
return 1
|
||||||
|
keypath, inpath, outpath = argv[1:]
|
||||||
|
return decryptBook(keypath, inpath, outpath)
|
||||||
|
|
||||||
|
|
||||||
|
def gui_main():
|
||||||
|
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.resizable(True, False)
|
||||||
|
root.minsize(300, 0)
|
||||||
|
DecryptionDialog(root).pack(fill=Tkconstants.X, expand=1)
|
||||||
|
root.mainloop()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
sys.exit(cli_main())
|
||||||
|
sys.exit(gui_main())
|
||||||
@@ -0,0 +1,239 @@
|
|||||||
|
#! /usr/bin/python
|
||||||
|
|
||||||
|
from __future__ import with_statement
|
||||||
|
|
||||||
|
# ignoblekeygen.pyw, version 2.3
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
# Revision history:
|
||||||
|
# 1 - Initial release
|
||||||
|
# 2 - Add OS X support by using OpenSSL when available (taken/modified from ineptepub v5)
|
||||||
|
# 2.1 - Allow Windows versions of libcrypto to be found
|
||||||
|
# 2.2 - On Windows try PyCrypto first and then OpenSSL next
|
||||||
|
# 2.3 - Modify interface to allow use of import
|
||||||
|
|
||||||
|
"""
|
||||||
|
Generate Barnes & Noble EPUB user key from name and credit card number.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import hashlib
|
||||||
|
import Tkinter
|
||||||
|
import Tkconstants
|
||||||
|
import tkFileDialog
|
||||||
|
import tkMessageBox
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# 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'):
|
||||||
|
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)
|
||||||
|
|
||||||
|
AES_MAXNR = 14
|
||||||
|
|
||||||
|
c_char_pp = POINTER(c_char_p)
|
||||||
|
c_int_p = POINTER(c_int)
|
||||||
|
|
||||||
|
class AES_KEY(Structure):
|
||||||
|
_fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))),
|
||||||
|
('rounds', c_int)]
|
||||||
|
AES_KEY_p = POINTER(AES_KEY)
|
||||||
|
|
||||||
|
def F(restype, name, argtypes):
|
||||||
|
func = getattr(libcrypto, name)
|
||||||
|
func.restype = restype
|
||||||
|
func.argtypes = argtypes
|
||||||
|
return func
|
||||||
|
|
||||||
|
AES_set_encrypt_key = F(c_int, 'AES_set_encrypt_key',
|
||||||
|
[c_char_p, c_int, AES_KEY_p])
|
||||||
|
AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',
|
||||||
|
[c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p,
|
||||||
|
c_int])
|
||||||
|
class AES(object):
|
||||||
|
def __init__(self, userkey, iv):
|
||||||
|
self._blocksize = len(userkey)
|
||||||
|
self._iv = iv
|
||||||
|
key = self._key = AES_KEY()
|
||||||
|
rv = AES_set_encrypt_key(userkey, len(userkey) * 8, key)
|
||||||
|
if rv < 0:
|
||||||
|
raise IGNOBLEError('Failed to initialize AES Encrypt key')
|
||||||
|
|
||||||
|
def encrypt(self, data):
|
||||||
|
out = create_string_buffer(len(data))
|
||||||
|
rv = AES_cbc_encrypt(data, out, len(data), self._key, self._iv, 1)
|
||||||
|
if rv == 0:
|
||||||
|
raise IGNOBLEError('AES encryption failed')
|
||||||
|
return out.raw
|
||||||
|
|
||||||
|
return AES
|
||||||
|
|
||||||
|
|
||||||
|
def _load_crypto_pycrypto():
|
||||||
|
from Crypto.Cipher import AES as _AES
|
||||||
|
|
||||||
|
class AES(object):
|
||||||
|
def __init__(self, key, iv):
|
||||||
|
self._aes = _AES.new(key, _AES.MODE_CBC, iv)
|
||||||
|
|
||||||
|
def encrypt(self, data):
|
||||||
|
return self._aes.encrypt(data)
|
||||||
|
|
||||||
|
return AES
|
||||||
|
|
||||||
|
def _load_crypto():
|
||||||
|
AES = None
|
||||||
|
cryptolist = (_load_crypto_libcrypto, _load_crypto_pycrypto)
|
||||||
|
if sys.platform.startswith('win'):
|
||||||
|
cryptolist = (_load_crypto_pycrypto, _load_crypto_libcrypto)
|
||||||
|
for loader in cryptolist:
|
||||||
|
try:
|
||||||
|
AES = loader()
|
||||||
|
break
|
||||||
|
except (ImportError, IGNOBLEError):
|
||||||
|
pass
|
||||||
|
return AES
|
||||||
|
|
||||||
|
AES = _load_crypto()
|
||||||
|
|
||||||
|
def normalize_name(name):
|
||||||
|
return ''.join(x for x in name.lower() if x != ' ')
|
||||||
|
|
||||||
|
|
||||||
|
def generate_keyfile(name, ccn, outpath):
|
||||||
|
name = normalize_name(name) + '\x00'
|
||||||
|
ccn = ccn + '\x00'
|
||||||
|
name_sha = hashlib.sha1(name).digest()[:16]
|
||||||
|
ccn_sha = hashlib.sha1(ccn).digest()[:16]
|
||||||
|
both_sha = hashlib.sha1(name + ccn).digest()
|
||||||
|
aes = AES(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
|
||||||
|
|
||||||
|
|
||||||
|
class DecryptionDialog(Tkinter.Frame):
|
||||||
|
def __init__(self, root):
|
||||||
|
Tkinter.Frame.__init__(self, root, border=5)
|
||||||
|
self.status = Tkinter.Label(self, text='Enter parameters')
|
||||||
|
self.status.pack(fill=Tkconstants.X, expand=1)
|
||||||
|
body = Tkinter.Frame(self)
|
||||||
|
body.pack(fill=Tkconstants.X, expand=1)
|
||||||
|
sticky = Tkconstants.E + Tkconstants.W
|
||||||
|
body.grid_columnconfigure(1, weight=2)
|
||||||
|
Tkinter.Label(body, text='Name').grid(row=1)
|
||||||
|
self.name = Tkinter.Entry(body, width=30)
|
||||||
|
self.name.grid(row=1, column=1, sticky=sticky)
|
||||||
|
Tkinter.Label(body, text='CC#').grid(row=2)
|
||||||
|
self.ccn = Tkinter.Entry(body, width=30)
|
||||||
|
self.ccn.grid(row=2, column=1, sticky=sticky)
|
||||||
|
Tkinter.Label(body, text='Output file').grid(row=0)
|
||||||
|
self.keypath = Tkinter.Entry(body, width=30)
|
||||||
|
self.keypath.grid(row=0, column=1, sticky=sticky)
|
||||||
|
self.keypath.insert(0, 'bnepubkey.b64')
|
||||||
|
button = Tkinter.Button(body, text="...", command=self.get_keypath)
|
||||||
|
button.grid(row=0, column=2)
|
||||||
|
buttons = Tkinter.Frame(self)
|
||||||
|
buttons.pack()
|
||||||
|
botton = Tkinter.Button(
|
||||||
|
buttons, text="Generate", width=10, command=self.generate)
|
||||||
|
botton.pack(side=Tkconstants.LEFT)
|
||||||
|
Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT)
|
||||||
|
button = Tkinter.Button(
|
||||||
|
buttons, text="Quit", width=10, command=self.quit)
|
||||||
|
button.pack(side=Tkconstants.RIGHT)
|
||||||
|
|
||||||
|
def get_keypath(self):
|
||||||
|
keypath = tkFileDialog.asksaveasfilename(
|
||||||
|
parent=None, title='Select B&N EPUB key file to produce',
|
||||||
|
defaultextension='.b64',
|
||||||
|
filetypes=[('base64-encoded files', '.b64'),
|
||||||
|
('All Files', '.*')])
|
||||||
|
if keypath:
|
||||||
|
keypath = os.path.normpath(keypath)
|
||||||
|
self.keypath.delete(0, Tkconstants.END)
|
||||||
|
self.keypath.insert(0, keypath)
|
||||||
|
return
|
||||||
|
|
||||||
|
def generate(self):
|
||||||
|
name = self.name.get()
|
||||||
|
ccn = self.ccn.get()
|
||||||
|
keypath = self.keypath.get()
|
||||||
|
if not name:
|
||||||
|
self.status['text'] = 'Name not specified'
|
||||||
|
return
|
||||||
|
if not ccn:
|
||||||
|
self.status['text'] = 'Credit card number not specified'
|
||||||
|
return
|
||||||
|
if not keypath:
|
||||||
|
self.status['text'] = 'Output keyfile path not specified'
|
||||||
|
return
|
||||||
|
self.status['text'] = 'Generating...'
|
||||||
|
try:
|
||||||
|
generate_keyfile(name, ccn, keypath)
|
||||||
|
except Exception, e:
|
||||||
|
self.status['text'] = 'Error: ' + str(e)
|
||||||
|
return
|
||||||
|
self.status['text'] = 'Keyfile successfully generated'
|
||||||
|
|
||||||
|
|
||||||
|
def cli_main(argv=sys.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 NAME CC# OUTFILE" % (progname,)
|
||||||
|
return 1
|
||||||
|
name, ccn, outpath = argv[1:]
|
||||||
|
generate_keyfile(name, ccn, outpath)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def gui_main():
|
||||||
|
root = Tkinter.Tk()
|
||||||
|
if AES is None:
|
||||||
|
root.withdraw()
|
||||||
|
tkMessageBox.showerror(
|
||||||
|
"Ignoble EPUB Keyfile Generator",
|
||||||
|
"This script requires OpenSSL or PyCrypto, which must be installed "
|
||||||
|
"separately. Read the top-of-script comment for details.")
|
||||||
|
return 1
|
||||||
|
root.title('Ignoble EPUB Keyfile Generator')
|
||||||
|
root.resizable(True, False)
|
||||||
|
root.minsize(300, 0)
|
||||||
|
DecryptionDialog(root).pack(fill=Tkconstants.X, expand=1)
|
||||||
|
root.mainloop()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
sys.exit(cli_main())
|
||||||
|
sys.exit(gui_main())
|
||||||
@@ -0,0 +1,476 @@
|
|||||||
|
#! /usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from __future__ import with_statement
|
||||||
|
|
||||||
|
# ineptepub.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/>
|
||||||
|
|
||||||
|
# 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
|
||||||
|
# 2 - Rename to INEPT, fix exit code
|
||||||
|
# 5 - Version bump to avoid (?) confusion;
|
||||||
|
# Improve OS X support by using OpenSSL when available
|
||||||
|
# 5.1 - Improve OpenSSL error checking
|
||||||
|
# 5.2 - Fix ctypes error causing segfaults on some systems
|
||||||
|
# 5.3 - add support for OpenSSL on Windows, fix bug with some versions of libcrypto 0.9.8 prior to path level o
|
||||||
|
# 5.4 - add support for encoding to 'utf-8' when building up list of files to decrypt from encryption.xml
|
||||||
|
# 5.5 - On Windows try PyCrypto first, OpenSSL next
|
||||||
|
# 5.6 - Modify interface to allow use with import
|
||||||
|
"""
|
||||||
|
Decrypt Adobe ADEPT-encrypted EPUB books.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import zlib
|
||||||
|
import zipfile
|
||||||
|
from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED
|
||||||
|
from contextlib import closing
|
||||||
|
import xml.etree.ElementTree as etree
|
||||||
|
import Tkinter
|
||||||
|
import Tkconstants
|
||||||
|
import tkFileDialog
|
||||||
|
import tkMessageBox
|
||||||
|
|
||||||
|
class ADEPTError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _load_crypto_libcrypto():
|
||||||
|
from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_int, c_long, \
|
||||||
|
Structure, c_ulong, create_string_buffer, cast
|
||||||
|
from ctypes.util import find_library
|
||||||
|
|
||||||
|
if sys.platform.startswith('win'):
|
||||||
|
libcrypto = find_library('libeay32')
|
||||||
|
else:
|
||||||
|
libcrypto = find_library('crypto')
|
||||||
|
|
||||||
|
if libcrypto is None:
|
||||||
|
raise ADEPTError('libcrypto not found')
|
||||||
|
libcrypto = CDLL(libcrypto)
|
||||||
|
|
||||||
|
RSA_NO_PADDING = 3
|
||||||
|
AES_MAXNR = 14
|
||||||
|
|
||||||
|
c_char_pp = POINTER(c_char_p)
|
||||||
|
c_int_p = POINTER(c_int)
|
||||||
|
|
||||||
|
class RSA(Structure):
|
||||||
|
pass
|
||||||
|
RSA_p = POINTER(RSA)
|
||||||
|
|
||||||
|
class AES_KEY(Structure):
|
||||||
|
_fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))),
|
||||||
|
('rounds', c_int)]
|
||||||
|
AES_KEY_p = POINTER(AES_KEY)
|
||||||
|
|
||||||
|
def F(restype, name, argtypes):
|
||||||
|
func = getattr(libcrypto, name)
|
||||||
|
func.restype = restype
|
||||||
|
func.argtypes = argtypes
|
||||||
|
return func
|
||||||
|
|
||||||
|
d2i_RSAPrivateKey = F(RSA_p, 'd2i_RSAPrivateKey',
|
||||||
|
[RSA_p, c_char_pp, c_long])
|
||||||
|
RSA_size = F(c_int, 'RSA_size', [RSA_p])
|
||||||
|
RSA_private_decrypt = F(c_int, 'RSA_private_decrypt',
|
||||||
|
[c_int, c_char_p, c_char_p, RSA_p, c_int])
|
||||||
|
RSA_free = F(None, 'RSA_free', [RSA_p])
|
||||||
|
AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key',
|
||||||
|
[c_char_p, c_int, AES_KEY_p])
|
||||||
|
AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',
|
||||||
|
[c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p,
|
||||||
|
c_int])
|
||||||
|
|
||||||
|
class RSA(object):
|
||||||
|
def __init__(self, der):
|
||||||
|
buf = create_string_buffer(der)
|
||||||
|
pp = c_char_pp(cast(buf, c_char_p))
|
||||||
|
rsa = self._rsa = d2i_RSAPrivateKey(None, pp, len(der))
|
||||||
|
if rsa is None:
|
||||||
|
raise ADEPTError('Error parsing ADEPT user key DER')
|
||||||
|
|
||||||
|
def decrypt(self, from_):
|
||||||
|
rsa = self._rsa
|
||||||
|
to = create_string_buffer(RSA_size(rsa))
|
||||||
|
dlen = RSA_private_decrypt(len(from_), from_, to, rsa,
|
||||||
|
RSA_NO_PADDING)
|
||||||
|
if dlen < 0:
|
||||||
|
raise ADEPTError('RSA decryption failed')
|
||||||
|
return to[:dlen]
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
if self._rsa is not None:
|
||||||
|
RSA_free(self._rsa)
|
||||||
|
self._rsa = None
|
||||||
|
|
||||||
|
class AES(object):
|
||||||
|
def __init__(self, userkey):
|
||||||
|
self._blocksize = len(userkey)
|
||||||
|
if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) :
|
||||||
|
raise ADEPTError('AES improper key used')
|
||||||
|
return
|
||||||
|
key = self._key = AES_KEY()
|
||||||
|
rv = AES_set_decrypt_key(userkey, len(userkey) * 8, key)
|
||||||
|
if rv < 0:
|
||||||
|
raise ADEPTError('Failed to initialize AES key')
|
||||||
|
|
||||||
|
def decrypt(self, data):
|
||||||
|
out = create_string_buffer(len(data))
|
||||||
|
iv = ("\x00" * self._blocksize)
|
||||||
|
rv = AES_cbc_encrypt(data, out, len(data), self._key, iv, 0)
|
||||||
|
if rv == 0:
|
||||||
|
raise ADEPTError('AES decryption failed')
|
||||||
|
return out.raw
|
||||||
|
|
||||||
|
return (AES, RSA)
|
||||||
|
|
||||||
|
def _load_crypto_pycrypto():
|
||||||
|
from Crypto.Cipher import AES as _AES
|
||||||
|
from Crypto.PublicKey import RSA as _RSA
|
||||||
|
|
||||||
|
# ASN.1 parsing code from tlslite
|
||||||
|
class ASN1Error(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class ASN1Parser(object):
|
||||||
|
class Parser(object):
|
||||||
|
def __init__(self, bytes):
|
||||||
|
self.bytes = bytes
|
||||||
|
self.index = 0
|
||||||
|
|
||||||
|
def get(self, length):
|
||||||
|
if self.index + length > len(self.bytes):
|
||||||
|
raise ASN1Error("Error decoding ASN.1")
|
||||||
|
x = 0
|
||||||
|
for count in range(length):
|
||||||
|
x <<= 8
|
||||||
|
x |= self.bytes[self.index]
|
||||||
|
self.index += 1
|
||||||
|
return x
|
||||||
|
|
||||||
|
def getFixBytes(self, lengthBytes):
|
||||||
|
bytes = self.bytes[self.index : self.index+lengthBytes]
|
||||||
|
self.index += lengthBytes
|
||||||
|
return bytes
|
||||||
|
|
||||||
|
def getVarBytes(self, lengthLength):
|
||||||
|
lengthBytes = self.get(lengthLength)
|
||||||
|
return self.getFixBytes(lengthBytes)
|
||||||
|
|
||||||
|
def getFixList(self, length, lengthList):
|
||||||
|
l = [0] * lengthList
|
||||||
|
for x in range(lengthList):
|
||||||
|
l[x] = self.get(length)
|
||||||
|
return l
|
||||||
|
|
||||||
|
def getVarList(self, length, lengthLength):
|
||||||
|
lengthList = self.get(lengthLength)
|
||||||
|
if lengthList % length != 0:
|
||||||
|
raise ASN1Error("Error decoding ASN.1")
|
||||||
|
lengthList = int(lengthList/length)
|
||||||
|
l = [0] * lengthList
|
||||||
|
for x in range(lengthList):
|
||||||
|
l[x] = self.get(length)
|
||||||
|
return l
|
||||||
|
|
||||||
|
def startLengthCheck(self, lengthLength):
|
||||||
|
self.lengthCheck = self.get(lengthLength)
|
||||||
|
self.indexCheck = self.index
|
||||||
|
|
||||||
|
def setLengthCheck(self, length):
|
||||||
|
self.lengthCheck = length
|
||||||
|
self.indexCheck = self.index
|
||||||
|
|
||||||
|
def stopLengthCheck(self):
|
||||||
|
if (self.index - self.indexCheck) != self.lengthCheck:
|
||||||
|
raise ASN1Error("Error decoding ASN.1")
|
||||||
|
|
||||||
|
def atLengthCheck(self):
|
||||||
|
if (self.index - self.indexCheck) < self.lengthCheck:
|
||||||
|
return False
|
||||||
|
elif (self.index - self.indexCheck) == self.lengthCheck:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
raise ASN1Error("Error decoding ASN.1")
|
||||||
|
|
||||||
|
def __init__(self, bytes):
|
||||||
|
p = self.Parser(bytes)
|
||||||
|
p.get(1)
|
||||||
|
self.length = self._getASN1Length(p)
|
||||||
|
self.value = p.getFixBytes(self.length)
|
||||||
|
|
||||||
|
def getChild(self, which):
|
||||||
|
p = self.Parser(self.value)
|
||||||
|
for x in range(which+1):
|
||||||
|
markIndex = p.index
|
||||||
|
p.get(1)
|
||||||
|
length = self._getASN1Length(p)
|
||||||
|
p.getFixBytes(length)
|
||||||
|
return ASN1Parser(p.bytes[markIndex:p.index])
|
||||||
|
|
||||||
|
def _getASN1Length(self, p):
|
||||||
|
firstLength = p.get(1)
|
||||||
|
if firstLength<=127:
|
||||||
|
return firstLength
|
||||||
|
else:
|
||||||
|
lengthLength = firstLength & 0x7F
|
||||||
|
return p.get(lengthLength)
|
||||||
|
|
||||||
|
class AES(object):
|
||||||
|
def __init__(self, key):
|
||||||
|
self._aes = _AES.new(key, _AES.MODE_CBC)
|
||||||
|
|
||||||
|
def decrypt(self, data):
|
||||||
|
return self._aes.decrypt(data)
|
||||||
|
|
||||||
|
class RSA(object):
|
||||||
|
def __init__(self, der):
|
||||||
|
key = ASN1Parser([ord(x) for x in der])
|
||||||
|
key = [key.getChild(x).value for x in xrange(1, 4)]
|
||||||
|
key = [self.bytesToNumber(v) for v in key]
|
||||||
|
self._rsa = _RSA.construct(key)
|
||||||
|
|
||||||
|
def bytesToNumber(self, bytes):
|
||||||
|
total = 0L
|
||||||
|
for byte in bytes:
|
||||||
|
total = (total << 8) + byte
|
||||||
|
return total
|
||||||
|
|
||||||
|
def decrypt(self, data):
|
||||||
|
return self._rsa.decrypt(data)
|
||||||
|
|
||||||
|
return (AES, RSA)
|
||||||
|
|
||||||
|
def _load_crypto():
|
||||||
|
AES = RSA = None
|
||||||
|
cryptolist = (_load_crypto_libcrypto, _load_crypto_pycrypto)
|
||||||
|
if sys.platform.startswith('win'):
|
||||||
|
cryptolist = (_load_crypto_pycrypto, _load_crypto_libcrypto)
|
||||||
|
for loader in cryptolist:
|
||||||
|
try:
|
||||||
|
AES, RSA = loader()
|
||||||
|
break
|
||||||
|
except (ImportError, ADEPTError):
|
||||||
|
pass
|
||||||
|
return (AES, RSA)
|
||||||
|
AES, RSA = _load_crypto()
|
||||||
|
|
||||||
|
META_NAMES = ('mimetype', 'META-INF/rights.xml', 'META-INF/encryption.xml')
|
||||||
|
NSMAP = {'adept': 'http://ns.adobe.com/adept',
|
||||||
|
'enc': 'http://www.w3.org/2001/04/xmlenc#'}
|
||||||
|
|
||||||
|
class ZipInfo(zipfile.ZipInfo):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
if 'compress_type' in kwargs:
|
||||||
|
compress_type = kwargs.pop('compress_type')
|
||||||
|
super(ZipInfo, self).__init__(*args, **kwargs)
|
||||||
|
self.compress_type = compress_type
|
||||||
|
|
||||||
|
class Decryptor(object):
|
||||||
|
def __init__(self, bookkey, encryption):
|
||||||
|
enc = lambda tag: '{%s}%s' % (NSMAP['enc'], tag)
|
||||||
|
self._aes = AES(bookkey)
|
||||||
|
encryption = etree.fromstring(encryption)
|
||||||
|
self._encrypted = encrypted = set()
|
||||||
|
expr = './%s/%s/%s' % (enc('EncryptedData'), enc('CipherData'),
|
||||||
|
enc('CipherReference'))
|
||||||
|
for elem in encryption.findall(expr):
|
||||||
|
path = elem.get('URI', None)
|
||||||
|
if path is not None:
|
||||||
|
path = path.encode('utf-8')
|
||||||
|
encrypted.add(path)
|
||||||
|
|
||||||
|
def decompress(self, bytes):
|
||||||
|
dc = zlib.decompressobj(-15)
|
||||||
|
bytes = dc.decompress(bytes)
|
||||||
|
ex = dc.decompress('Z') + dc.flush()
|
||||||
|
if ex:
|
||||||
|
bytes = bytes + ex
|
||||||
|
return bytes
|
||||||
|
|
||||||
|
def decrypt(self, path, data):
|
||||||
|
if path in self._encrypted:
|
||||||
|
data = self._aes.decrypt(data)[16:]
|
||||||
|
data = data[:-ord(data[-1])]
|
||||||
|
data = self.decompress(data)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
class 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)
|
||||||
|
with closing(ZipFile(open(inpath, 'rb'))) as inf:
|
||||||
|
namelist = set(inf.namelist())
|
||||||
|
if 'META-INF/rights.xml' not in namelist or \
|
||||||
|
'META-INF/encryption.xml' not in namelist:
|
||||||
|
raise ADEPTError('%s: not an ADEPT EPUB' % (inpath,))
|
||||||
|
for name in META_NAMES:
|
||||||
|
namelist.remove(name)
|
||||||
|
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
|
||||||
|
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
||||||
|
expr = './/%s' % (adept('encryptedKey'),)
|
||||||
|
bookkey = ''.join(rights.findtext(expr))
|
||||||
|
bookkey = rsa.decrypt(bookkey.decode('base64'))
|
||||||
|
# Padded as per RSAES-PKCS1-v1_5
|
||||||
|
if bookkey[-17] != '\x00':
|
||||||
|
raise ADEPTError('problem decrypting session key')
|
||||||
|
encryption = inf.read('META-INF/encryption.xml')
|
||||||
|
decryptor = Decryptor(bookkey[-16:], encryption)
|
||||||
|
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
|
||||||
|
with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf:
|
||||||
|
zi = ZipInfo('mimetype', compress_type=ZIP_STORED)
|
||||||
|
outf.writestr(zi, inf.read('mimetype'))
|
||||||
|
for path in namelist:
|
||||||
|
data = inf.read(path)
|
||||||
|
outf.writestr(path, decryptor.decrypt(path, data))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cli_main(argv=sys.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,)
|
||||||
|
return 1
|
||||||
|
keypath, inpath, outpath = argv[1:]
|
||||||
|
return decryptBook(keypath, inpath, outpath)
|
||||||
|
|
||||||
|
|
||||||
|
def gui_main():
|
||||||
|
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.resizable(True, False)
|
||||||
|
root.minsize(300, 0)
|
||||||
|
DecryptionDialog(root).pack(fill=Tkconstants.X, expand=1)
|
||||||
|
root.mainloop()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
sys.exit(cli_main())
|
||||||
|
sys.exit(gui_main())
|
||||||
467
DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/ineptkey.py
Normal file
467
DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/ineptkey.py
Normal file
@@ -0,0 +1,467 @@
|
|||||||
|
#! /usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from __future__ import with_statement
|
||||||
|
|
||||||
|
# ineptkey.pyw, version 5.4
|
||||||
|
# Copyright © 2009-2010 i♥cabbages
|
||||||
|
|
||||||
|
# Released under the terms of the GNU General Public Licence, version 3 or
|
||||||
|
# later. <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
|
||||||
|
# <http://www.voidspace.org.uk/python/modules.shtml#pycrypto> (make certain
|
||||||
|
# to install the version for Python 2.6). Then save this script file as
|
||||||
|
# ineptkey.pyw and double-click on it to run it. It will create a file named
|
||||||
|
# adeptkey.der in the same directory. This is your ADEPT user key.
|
||||||
|
#
|
||||||
|
# Mac OS X users: Save this script file as ineptkey.pyw. You can run this
|
||||||
|
# program from the command line (pythonw ineptkey.pyw) or by double-clicking
|
||||||
|
# it when it has been associated with PythonLauncher. It will create a file
|
||||||
|
# named adeptkey.der in the same directory. This is your ADEPT user key.
|
||||||
|
|
||||||
|
# Revision history:
|
||||||
|
# 1 - Initial release, for Adobe Digital Editions 1.7
|
||||||
|
# 2 - Better algorithm for finding pLK; improved error handling
|
||||||
|
# 3 - Rename to INEPT
|
||||||
|
# 4 - Series of changes by joblack (and others?) --
|
||||||
|
# 4.1 - quick beta fix for ADE 1.7.2 (anon)
|
||||||
|
# 4.2 - added old 1.7.1 processing
|
||||||
|
# 4.3 - better key search
|
||||||
|
# 4.4 - Make it working on 64-bit Python
|
||||||
|
# 5 - Clean up and improve 4.x changes;
|
||||||
|
# Clean up and merge OS X support by unknown
|
||||||
|
# 5.1 - add support for using OpenSSL on Windows in place of PyCrypto
|
||||||
|
# 5.2 - added support for output of key to a particular file
|
||||||
|
# 5.3 - On Windows try PyCrypto first, OpenSSL next
|
||||||
|
# 5.4 - Modify interface to allow use of import
|
||||||
|
|
||||||
|
"""
|
||||||
|
Retrieve Adobe ADEPT user key.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import struct
|
||||||
|
import Tkinter
|
||||||
|
import Tkconstants
|
||||||
|
import tkMessageBox
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
class ADEPTError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
if sys.platform.startswith('win'):
|
||||||
|
from ctypes import windll, c_char_p, c_wchar_p, c_uint, POINTER, byref, \
|
||||||
|
create_unicode_buffer, create_string_buffer, CFUNCTYPE, addressof, \
|
||||||
|
string_at, Structure, c_void_p, cast, c_size_t, memmove, CDLL, c_int, \
|
||||||
|
c_long, c_ulong
|
||||||
|
|
||||||
|
from ctypes.wintypes import LPVOID, DWORD, BOOL
|
||||||
|
import _winreg as winreg
|
||||||
|
|
||||||
|
def _load_crypto_libcrypto():
|
||||||
|
from ctypes.util import find_library
|
||||||
|
libcrypto = find_library('libeay32')
|
||||||
|
if libcrypto is None:
|
||||||
|
raise ADEPTError('libcrypto not found')
|
||||||
|
libcrypto = CDLL(libcrypto)
|
||||||
|
AES_MAXNR = 14
|
||||||
|
c_char_pp = POINTER(c_char_p)
|
||||||
|
c_int_p = POINTER(c_int)
|
||||||
|
class AES_KEY(Structure):
|
||||||
|
_fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))),
|
||||||
|
('rounds', c_int)]
|
||||||
|
AES_KEY_p = POINTER(AES_KEY)
|
||||||
|
|
||||||
|
def F(restype, name, argtypes):
|
||||||
|
func = getattr(libcrypto, name)
|
||||||
|
func.restype = restype
|
||||||
|
func.argtypes = argtypes
|
||||||
|
return func
|
||||||
|
|
||||||
|
AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key',
|
||||||
|
[c_char_p, c_int, AES_KEY_p])
|
||||||
|
AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',
|
||||||
|
[c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p,
|
||||||
|
c_int])
|
||||||
|
class AES(object):
|
||||||
|
def __init__(self, userkey):
|
||||||
|
self._blocksize = len(userkey)
|
||||||
|
if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) :
|
||||||
|
raise ADEPTError('AES improper key used')
|
||||||
|
key = self._key = AES_KEY()
|
||||||
|
rv = AES_set_decrypt_key(userkey, len(userkey) * 8, key)
|
||||||
|
if rv < 0:
|
||||||
|
raise ADEPTError('Failed to initialize AES key')
|
||||||
|
def decrypt(self, data):
|
||||||
|
out = create_string_buffer(len(data))
|
||||||
|
iv = ("\x00" * self._blocksize)
|
||||||
|
rv = AES_cbc_encrypt(data, out, len(data), self._key, iv, 0)
|
||||||
|
if rv == 0:
|
||||||
|
raise ADEPTError('AES decryption failed')
|
||||||
|
return out.raw
|
||||||
|
return AES
|
||||||
|
|
||||||
|
def _load_crypto_pycrypto():
|
||||||
|
from Crypto.Cipher import AES as _AES
|
||||||
|
class AES(object):
|
||||||
|
def __init__(self, key):
|
||||||
|
self._aes = _AES.new(key, _AES.MODE_CBC)
|
||||||
|
def decrypt(self, data):
|
||||||
|
return self._aes.decrypt(data)
|
||||||
|
return AES
|
||||||
|
|
||||||
|
def _load_crypto():
|
||||||
|
AES = None
|
||||||
|
for loader in (_load_crypto_pycrypto, _load_crypto_libcrypto):
|
||||||
|
try:
|
||||||
|
AES = loader()
|
||||||
|
break
|
||||||
|
except (ImportError, ADEPTError):
|
||||||
|
pass
|
||||||
|
return AES
|
||||||
|
|
||||||
|
AES = _load_crypto()
|
||||||
|
|
||||||
|
|
||||||
|
DEVICE_KEY_PATH = r'Software\Adobe\Adept\Device'
|
||||||
|
PRIVATE_LICENCE_KEY_PATH = r'Software\Adobe\Adept\Activation'
|
||||||
|
|
||||||
|
MAX_PATH = 255
|
||||||
|
|
||||||
|
kernel32 = windll.kernel32
|
||||||
|
advapi32 = windll.advapi32
|
||||||
|
crypt32 = windll.crypt32
|
||||||
|
|
||||||
|
def GetSystemDirectory():
|
||||||
|
GetSystemDirectoryW = kernel32.GetSystemDirectoryW
|
||||||
|
GetSystemDirectoryW.argtypes = [c_wchar_p, c_uint]
|
||||||
|
GetSystemDirectoryW.restype = c_uint
|
||||||
|
def GetSystemDirectory():
|
||||||
|
buffer = create_unicode_buffer(MAX_PATH + 1)
|
||||||
|
GetSystemDirectoryW(buffer, len(buffer))
|
||||||
|
return buffer.value
|
||||||
|
return GetSystemDirectory
|
||||||
|
GetSystemDirectory = GetSystemDirectory()
|
||||||
|
|
||||||
|
def GetVolumeSerialNumber():
|
||||||
|
GetVolumeInformationW = kernel32.GetVolumeInformationW
|
||||||
|
GetVolumeInformationW.argtypes = [c_wchar_p, c_wchar_p, c_uint,
|
||||||
|
POINTER(c_uint), POINTER(c_uint),
|
||||||
|
POINTER(c_uint), c_wchar_p, c_uint]
|
||||||
|
GetVolumeInformationW.restype = c_uint
|
||||||
|
def GetVolumeSerialNumber(path):
|
||||||
|
vsn = c_uint(0)
|
||||||
|
GetVolumeInformationW(
|
||||||
|
path, None, 0, byref(vsn), None, None, None, 0)
|
||||||
|
return vsn.value
|
||||||
|
return GetVolumeSerialNumber
|
||||||
|
GetVolumeSerialNumber = GetVolumeSerialNumber()
|
||||||
|
|
||||||
|
def GetUserName():
|
||||||
|
GetUserNameW = advapi32.GetUserNameW
|
||||||
|
GetUserNameW.argtypes = [c_wchar_p, POINTER(c_uint)]
|
||||||
|
GetUserNameW.restype = c_uint
|
||||||
|
def GetUserName():
|
||||||
|
buffer = create_unicode_buffer(32)
|
||||||
|
size = c_uint(len(buffer))
|
||||||
|
while not GetUserNameW(buffer, byref(size)):
|
||||||
|
buffer = create_unicode_buffer(len(buffer) * 2)
|
||||||
|
size.value = len(buffer)
|
||||||
|
return buffer.value.encode('utf-16-le')[::2]
|
||||||
|
return GetUserName
|
||||||
|
GetUserName = GetUserName()
|
||||||
|
|
||||||
|
PAGE_EXECUTE_READWRITE = 0x40
|
||||||
|
MEM_COMMIT = 0x1000
|
||||||
|
MEM_RESERVE = 0x2000
|
||||||
|
|
||||||
|
def VirtualAlloc():
|
||||||
|
_VirtualAlloc = kernel32.VirtualAlloc
|
||||||
|
_VirtualAlloc.argtypes = [LPVOID, c_size_t, DWORD, DWORD]
|
||||||
|
_VirtualAlloc.restype = LPVOID
|
||||||
|
def VirtualAlloc(addr, size, alloctype=(MEM_COMMIT | MEM_RESERVE),
|
||||||
|
protect=PAGE_EXECUTE_READWRITE):
|
||||||
|
return _VirtualAlloc(addr, size, alloctype, protect)
|
||||||
|
return VirtualAlloc
|
||||||
|
VirtualAlloc = VirtualAlloc()
|
||||||
|
|
||||||
|
MEM_RELEASE = 0x8000
|
||||||
|
|
||||||
|
def VirtualFree():
|
||||||
|
_VirtualFree = kernel32.VirtualFree
|
||||||
|
_VirtualFree.argtypes = [LPVOID, c_size_t, DWORD]
|
||||||
|
_VirtualFree.restype = BOOL
|
||||||
|
def VirtualFree(addr, size=0, freetype=MEM_RELEASE):
|
||||||
|
return _VirtualFree(addr, size, freetype)
|
||||||
|
return VirtualFree
|
||||||
|
VirtualFree = VirtualFree()
|
||||||
|
|
||||||
|
class NativeFunction(object):
|
||||||
|
def __init__(self, restype, argtypes, insns):
|
||||||
|
self._buf = buf = VirtualAlloc(None, len(insns))
|
||||||
|
memmove(buf, insns, len(insns))
|
||||||
|
ftype = CFUNCTYPE(restype, *argtypes)
|
||||||
|
self._native = ftype(buf)
|
||||||
|
|
||||||
|
def __call__(self, *args):
|
||||||
|
return self._native(*args)
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
if self._buf is not None:
|
||||||
|
VirtualFree(self._buf)
|
||||||
|
self._buf = None
|
||||||
|
|
||||||
|
if struct.calcsize("P") == 4:
|
||||||
|
CPUID0_INSNS = (
|
||||||
|
"\x53" # push %ebx
|
||||||
|
"\x31\xc0" # xor %eax,%eax
|
||||||
|
"\x0f\xa2" # cpuid
|
||||||
|
"\x8b\x44\x24\x08" # mov 0x8(%esp),%eax
|
||||||
|
"\x89\x18" # mov %ebx,0x0(%eax)
|
||||||
|
"\x89\x50\x04" # mov %edx,0x4(%eax)
|
||||||
|
"\x89\x48\x08" # mov %ecx,0x8(%eax)
|
||||||
|
"\x5b" # pop %ebx
|
||||||
|
"\xc3" # ret
|
||||||
|
)
|
||||||
|
CPUID1_INSNS = (
|
||||||
|
"\x53" # push %ebx
|
||||||
|
"\x31\xc0" # xor %eax,%eax
|
||||||
|
"\x40" # inc %eax
|
||||||
|
"\x0f\xa2" # cpuid
|
||||||
|
"\x5b" # pop %ebx
|
||||||
|
"\xc3" # ret
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
CPUID0_INSNS = (
|
||||||
|
"\x49\x89\xd8" # mov %rbx,%r8
|
||||||
|
"\x49\x89\xc9" # mov %rcx,%r9
|
||||||
|
"\x48\x31\xc0" # xor %rax,%rax
|
||||||
|
"\x0f\xa2" # cpuid
|
||||||
|
"\x4c\x89\xc8" # mov %r9,%rax
|
||||||
|
"\x89\x18" # mov %ebx,0x0(%rax)
|
||||||
|
"\x89\x50\x04" # mov %edx,0x4(%rax)
|
||||||
|
"\x89\x48\x08" # mov %ecx,0x8(%rax)
|
||||||
|
"\x4c\x89\xc3" # mov %r8,%rbx
|
||||||
|
"\xc3" # retq
|
||||||
|
)
|
||||||
|
CPUID1_INSNS = (
|
||||||
|
"\x53" # push %rbx
|
||||||
|
"\x48\x31\xc0" # xor %rax,%rax
|
||||||
|
"\x48\xff\xc0" # inc %rax
|
||||||
|
"\x0f\xa2" # cpuid
|
||||||
|
"\x5b" # pop %rbx
|
||||||
|
"\xc3" # retq
|
||||||
|
)
|
||||||
|
|
||||||
|
def cpuid0():
|
||||||
|
_cpuid0 = NativeFunction(None, [c_char_p], CPUID0_INSNS)
|
||||||
|
buf = create_string_buffer(12)
|
||||||
|
def cpuid0():
|
||||||
|
_cpuid0(buf)
|
||||||
|
return buf.raw
|
||||||
|
return cpuid0
|
||||||
|
cpuid0 = cpuid0()
|
||||||
|
|
||||||
|
cpuid1 = NativeFunction(c_uint, [], CPUID1_INSNS)
|
||||||
|
|
||||||
|
class DataBlob(Structure):
|
||||||
|
_fields_ = [('cbData', c_uint),
|
||||||
|
('pbData', c_void_p)]
|
||||||
|
DataBlob_p = POINTER(DataBlob)
|
||||||
|
|
||||||
|
def CryptUnprotectData():
|
||||||
|
_CryptUnprotectData = crypt32.CryptUnprotectData
|
||||||
|
_CryptUnprotectData.argtypes = [DataBlob_p, c_wchar_p, DataBlob_p,
|
||||||
|
c_void_p, c_void_p, c_uint, DataBlob_p]
|
||||||
|
_CryptUnprotectData.restype = c_uint
|
||||||
|
def CryptUnprotectData(indata, entropy):
|
||||||
|
indatab = create_string_buffer(indata)
|
||||||
|
indata = DataBlob(len(indata), cast(indatab, c_void_p))
|
||||||
|
entropyb = create_string_buffer(entropy)
|
||||||
|
entropy = DataBlob(len(entropy), cast(entropyb, c_void_p))
|
||||||
|
outdata = DataBlob()
|
||||||
|
if not _CryptUnprotectData(byref(indata), None, byref(entropy),
|
||||||
|
None, None, 0, byref(outdata)):
|
||||||
|
raise ADEPTError("Failed to decrypt user key key (sic)")
|
||||||
|
return string_at(outdata.pbData, outdata.cbData)
|
||||||
|
return CryptUnprotectData
|
||||||
|
CryptUnprotectData = CryptUnprotectData()
|
||||||
|
|
||||||
|
def retrieve_key(keypath):
|
||||||
|
if AES is None:
|
||||||
|
tkMessageBox.showerror(
|
||||||
|
"ADEPT Key",
|
||||||
|
"This script requires PyCrypto or OpenSSL which must be installed "
|
||||||
|
"separately. Read the top-of-script comment for details.")
|
||||||
|
return False
|
||||||
|
root = GetSystemDirectory().split('\\')[0] + '\\'
|
||||||
|
serial = GetVolumeSerialNumber(root)
|
||||||
|
vendor = cpuid0()
|
||||||
|
signature = struct.pack('>I', cpuid1())[1:]
|
||||||
|
user = GetUserName()
|
||||||
|
entropy = struct.pack('>I12s3s13s', serial, vendor, signature, user)
|
||||||
|
cuser = winreg.HKEY_CURRENT_USER
|
||||||
|
try:
|
||||||
|
regkey = winreg.OpenKey(cuser, DEVICE_KEY_PATH)
|
||||||
|
except WindowsError:
|
||||||
|
raise ADEPTError("Adobe Digital Editions not activated")
|
||||||
|
device = winreg.QueryValueEx(regkey, 'key')[0]
|
||||||
|
keykey = CryptUnprotectData(device, entropy)
|
||||||
|
userkey = None
|
||||||
|
try:
|
||||||
|
plkroot = winreg.OpenKey(cuser, PRIVATE_LICENCE_KEY_PATH)
|
||||||
|
except WindowsError:
|
||||||
|
raise ADEPTError("Could not locate ADE activation")
|
||||||
|
for i in xrange(0, 16):
|
||||||
|
try:
|
||||||
|
plkparent = winreg.OpenKey(plkroot, "%04d" % (i,))
|
||||||
|
except WindowsError:
|
||||||
|
break
|
||||||
|
ktype = winreg.QueryValueEx(plkparent, None)[0]
|
||||||
|
if ktype != 'credentials':
|
||||||
|
continue
|
||||||
|
for j in xrange(0, 16):
|
||||||
|
try:
|
||||||
|
plkkey = winreg.OpenKey(plkparent, "%04d" % (j,))
|
||||||
|
except WindowsError:
|
||||||
|
break
|
||||||
|
ktype = winreg.QueryValueEx(plkkey, None)[0]
|
||||||
|
if ktype != 'privateLicenseKey':
|
||||||
|
continue
|
||||||
|
userkey = winreg.QueryValueEx(plkkey, 'value')[0]
|
||||||
|
break
|
||||||
|
if userkey is not None:
|
||||||
|
break
|
||||||
|
if userkey is None:
|
||||||
|
raise ADEPTError('Could not locate privateLicenseKey')
|
||||||
|
userkey = userkey.decode('base64')
|
||||||
|
aes = AES(keykey)
|
||||||
|
userkey = aes.decrypt(userkey)
|
||||||
|
userkey = userkey[26:-ord(userkey[-1])]
|
||||||
|
with open(keypath, 'wb') as f:
|
||||||
|
f.write(userkey)
|
||||||
|
return True
|
||||||
|
|
||||||
|
elif sys.platform.startswith('darwin'):
|
||||||
|
import xml.etree.ElementTree as etree
|
||||||
|
import Carbon.File
|
||||||
|
import Carbon.Folder
|
||||||
|
import Carbon.Folders
|
||||||
|
import MacOS
|
||||||
|
|
||||||
|
ACTIVATION_PATH = 'Adobe/Digital Editions/activation.dat'
|
||||||
|
NSMAP = {'adept': 'http://ns.adobe.com/adept',
|
||||||
|
'enc': 'http://www.w3.org/2001/04/xmlenc#'}
|
||||||
|
|
||||||
|
def find_folder(domain, dtype):
|
||||||
|
try:
|
||||||
|
fsref = Carbon.Folder.FSFindFolder(domain, dtype, False)
|
||||||
|
return Carbon.File.pathname(fsref)
|
||||||
|
except MacOS.Error:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def find_app_support_file(subpath):
|
||||||
|
dtype = Carbon.Folders.kApplicationSupportFolderType
|
||||||
|
for domain in Carbon.Folders.kUserDomain, Carbon.Folders.kLocalDomain:
|
||||||
|
path = find_folder(domain, dtype)
|
||||||
|
if path is None:
|
||||||
|
continue
|
||||||
|
path = os.path.join(path, subpath)
|
||||||
|
if os.path.isfile(path):
|
||||||
|
return path
|
||||||
|
return None
|
||||||
|
|
||||||
|
def retrieve_key(keypath):
|
||||||
|
actpath = find_app_support_file(ACTIVATION_PATH)
|
||||||
|
if actpath is None:
|
||||||
|
raise ADEPTError("Could not locate ADE activation")
|
||||||
|
tree = etree.parse(actpath)
|
||||||
|
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
|
||||||
|
expr = '//%s/%s' % (adept('credentials'), adept('privateLicenseKey'))
|
||||||
|
userkey = tree.findtext(expr)
|
||||||
|
userkey = userkey.decode('base64')
|
||||||
|
userkey = userkey[26:]
|
||||||
|
with open(keypath, 'wb') as f:
|
||||||
|
f.write(userkey)
|
||||||
|
return True
|
||||||
|
|
||||||
|
elif sys.platform.startswith('cygwin'):
|
||||||
|
def retrieve_key(keypath):
|
||||||
|
tkMessageBox.showerror(
|
||||||
|
"ADEPT Key",
|
||||||
|
"This script requires a Windows-native Python, and cannot be run "
|
||||||
|
"under Cygwin. Please install a Windows-native Python and/or "
|
||||||
|
"check your file associations.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
else:
|
||||||
|
def retrieve_key(keypath):
|
||||||
|
tkMessageBox.showerror(
|
||||||
|
"ADEPT Key",
|
||||||
|
"This script only supports Windows and Mac OS X. For Linux "
|
||||||
|
"you should be able to run ADE and this script under Wine (with "
|
||||||
|
"an appropriate version of Windows Python installed).")
|
||||||
|
return False
|
||||||
|
|
||||||
|
class ExceptionDialog(Tkinter.Frame):
|
||||||
|
def __init__(self, root, text):
|
||||||
|
Tkinter.Frame.__init__(self, root, border=5)
|
||||||
|
label = Tkinter.Label(self, text="Unexpected error:",
|
||||||
|
anchor=Tkconstants.W, justify=Tkconstants.LEFT)
|
||||||
|
label.pack(fill=Tkconstants.X, expand=0)
|
||||||
|
self.text = Tkinter.Text(self)
|
||||||
|
self.text.pack(fill=Tkconstants.BOTH, expand=1)
|
||||||
|
|
||||||
|
self.text.insert(Tkconstants.END, text)
|
||||||
|
|
||||||
|
|
||||||
|
def extractKeyfile(keypath):
|
||||||
|
try:
|
||||||
|
success = retrieve_key(keypath)
|
||||||
|
except ADEPTError, e:
|
||||||
|
print "Key generation Error: " + str(e)
|
||||||
|
return 1
|
||||||
|
except Exception, e:
|
||||||
|
print "General Error: " + str(e)
|
||||||
|
return 1
|
||||||
|
if not success:
|
||||||
|
return 1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cli_main(argv=sys.argv):
|
||||||
|
keypath = argv[1]
|
||||||
|
return extractKeyfile(keypath)
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv=sys.argv):
|
||||||
|
root = Tkinter.Tk()
|
||||||
|
root.withdraw()
|
||||||
|
progname = os.path.basename(argv[0])
|
||||||
|
keypath = 'adeptkey.der'
|
||||||
|
success = False
|
||||||
|
try:
|
||||||
|
success = retrieve_key(keypath)
|
||||||
|
except ADEPTError, e:
|
||||||
|
tkMessageBox.showerror("ADEPT Key", "Error: " + str(e))
|
||||||
|
except Exception:
|
||||||
|
root.wm_state('normal')
|
||||||
|
root.title('ADEPT Key')
|
||||||
|
text = traceback.format_exc()
|
||||||
|
ExceptionDialog(root, text).pack(fill=Tkconstants.BOTH, expand=1)
|
||||||
|
root.mainloop()
|
||||||
|
if not success:
|
||||||
|
return 1
|
||||||
|
tkMessageBox.showinfo(
|
||||||
|
"ADEPT Key", "Key successfully retrieved to %s" % (keypath))
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
sys.exit(cli_main())
|
||||||
|
sys.exit(main())
|
||||||
2244
DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/ineptpdf.py
Normal file
2244
DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/ineptpdf.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,199 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
from __future__ import with_statement
|
||||||
|
|
||||||
|
# engine to remove drm from Kindle for Mac and Kindle for PC books
|
||||||
|
# for personal use for archiving and converting your ebooks
|
||||||
|
|
||||||
|
# PLEASE DO NOT PIRATE EBOOKS!
|
||||||
|
|
||||||
|
# We want all authors and publishers, and eBook stores to live
|
||||||
|
# long and prosperous lives but at the same time we just want to
|
||||||
|
# be able to read OUR books on whatever device we want and to keep
|
||||||
|
# readable for a long, long time
|
||||||
|
|
||||||
|
# This borrows very heavily from works by CMBDTC, IHeartCabbages, skindle,
|
||||||
|
# unswindle, DarkReverser, ApprenticeAlf, DiapDealer, some_updates
|
||||||
|
# and many many others
|
||||||
|
|
||||||
|
|
||||||
|
__version__ = '3.6'
|
||||||
|
|
||||||
|
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 os, csv, getopt
|
||||||
|
import string
|
||||||
|
import re
|
||||||
|
|
||||||
|
class DrmException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
if 'calibre' in sys.modules:
|
||||||
|
inCalibre = True
|
||||||
|
else:
|
||||||
|
inCalibre = False
|
||||||
|
|
||||||
|
if inCalibre:
|
||||||
|
from calibre_plugins.k4mobidedrm import mobidedrm
|
||||||
|
from calibre_plugins.k4mobidedrm import topazextract
|
||||||
|
from calibre_plugins.k4mobidedrm import kgenpids
|
||||||
|
else:
|
||||||
|
import mobidedrm
|
||||||
|
import topazextract
|
||||||
|
import kgenpids
|
||||||
|
|
||||||
|
|
||||||
|
# cleanup bytestring filenames
|
||||||
|
# borrowed from calibre from calibre/src/calibre/__init__.py
|
||||||
|
# added in removal of non-printing chars
|
||||||
|
# and removal of . at start
|
||||||
|
# convert spaces to underscores
|
||||||
|
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):
|
||||||
|
# handle the obvious cases at the beginning
|
||||||
|
if not os.path.isfile(infile):
|
||||||
|
print "Error: Input file does not exist"
|
||||||
|
return 1
|
||||||
|
|
||||||
|
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 = bookname
|
||||||
|
if len(bookname)>4 and len(filenametitle)>4 and bookname[:4] != filenametitle[:4]:
|
||||||
|
outfilename = outfilename + "_" + filenametitle
|
||||||
|
|
||||||
|
# build pid list
|
||||||
|
md1, md2 = mb.getPIDMetaInfo()
|
||||||
|
pidlst = kgenpids.getPidList(md1, md2, k4, pids, serials, kInfoFiles)
|
||||||
|
|
||||||
|
try:
|
||||||
|
mb.processBook(pidlst)
|
||||||
|
|
||||||
|
except mobidedrm.DrmException, e:
|
||||||
|
print "Error: " + str(e) + "\nDRM Removal Failed.\n"
|
||||||
|
return 1
|
||||||
|
except topazextract.TpzDRMError, e:
|
||||||
|
print "Error: " + str(e) + "\nDRM Removal Failed.\n"
|
||||||
|
return 1
|
||||||
|
except Exception, e:
|
||||||
|
print "Error: " + str(e) + "\nDRM Removal Failed.\n"
|
||||||
|
return 1
|
||||||
|
|
||||||
|
if mobi:
|
||||||
|
outfile = os.path.join(outdir, outfilename + '_nodrm' + '.mobi')
|
||||||
|
mb.getMobiFile(outfile)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# topaz:
|
||||||
|
print " Creating NoDRM HTMLZ Archive"
|
||||||
|
zipname = os.path.join(outdir, outfilename + '_nodrm' + '.htmlz')
|
||||||
|
mb.getHTMLZip(zipname)
|
||||||
|
|
||||||
|
print " Creating SVG HTMLZ Archive"
|
||||||
|
zipname = os.path.join(outdir, outfilename + '_SVG' + '.htmlz')
|
||||||
|
mb.getSVGZip(zipname)
|
||||||
|
|
||||||
|
print " Creating XML ZIP Archive"
|
||||||
|
zipname = os.path.join(outdir, outfilename + '_XML' + '.zip')
|
||||||
|
mb.getXMLZip(zipname)
|
||||||
|
|
||||||
|
# remove internal temporary directory of Topaz pieces
|
||||||
|
mb.cleanup()
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
#
|
||||||
|
# Main
|
||||||
|
#
|
||||||
|
def main(argv=sys.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 ' '
|
||||||
|
try:
|
||||||
|
opts, args = getopt.getopt(sys.argv[1:], "k:p:s:")
|
||||||
|
except getopt.GetoptError, err:
|
||||||
|
print str(err)
|
||||||
|
usage(progname)
|
||||||
|
sys.exit(2)
|
||||||
|
if len(args)<2:
|
||||||
|
usage(progname)
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
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 = 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)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.stdout=Unbuffered(sys.stdout)
|
||||||
|
sys.exit(main())
|
||||||
|
|
||||||
551
DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/k4mutils.py
Normal file
551
DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/k4mutils.py
Normal file
@@ -0,0 +1,551 @@
|
|||||||
|
# standlone set of Mac OSX specific routines needed for KindleBooks
|
||||||
|
|
||||||
|
from __future__ import with_statement
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import os.path
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
from struct import pack, unpack, unpack_from
|
||||||
|
|
||||||
|
class DrmException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# interface to needed routines in openssl's libcrypto
|
||||||
|
def _load_crypto_libcrypto():
|
||||||
|
from ctypes import CDLL, byref, POINTER, c_void_p, c_char_p, c_int, c_long, \
|
||||||
|
Structure, c_ulong, create_string_buffer, addressof, string_at, cast
|
||||||
|
from ctypes.util import find_library
|
||||||
|
|
||||||
|
libcrypto = find_library('crypto')
|
||||||
|
if libcrypto is None:
|
||||||
|
raise DrmException('libcrypto not found')
|
||||||
|
libcrypto = CDLL(libcrypto)
|
||||||
|
|
||||||
|
AES_MAXNR = 14
|
||||||
|
c_char_pp = POINTER(c_char_p)
|
||||||
|
c_int_p = POINTER(c_int)
|
||||||
|
|
||||||
|
class AES_KEY(Structure):
|
||||||
|
_fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))), ('rounds', c_int)]
|
||||||
|
AES_KEY_p = POINTER(AES_KEY)
|
||||||
|
|
||||||
|
def F(restype, name, argtypes):
|
||||||
|
func = getattr(libcrypto, name)
|
||||||
|
func.restype = restype
|
||||||
|
func.argtypes = argtypes
|
||||||
|
return func
|
||||||
|
|
||||||
|
AES_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])
|
||||||
|
|
||||||
|
PKCS5_PBKDF2_HMAC_SHA1 = F(c_int, 'PKCS5_PBKDF2_HMAC_SHA1',
|
||||||
|
[c_char_p, c_ulong, c_char_p, c_ulong, c_ulong, c_ulong, c_char_p])
|
||||||
|
|
||||||
|
class LibCrypto(object):
|
||||||
|
def __init__(self):
|
||||||
|
self._blocksize = 0
|
||||||
|
self._keyctx = None
|
||||||
|
self.iv = 0
|
||||||
|
|
||||||
|
def set_decrypt_key(self, userkey, iv):
|
||||||
|
self._blocksize = len(userkey)
|
||||||
|
if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) :
|
||||||
|
raise DrmException('AES improper key used')
|
||||||
|
return
|
||||||
|
keyctx = self._keyctx = AES_KEY()
|
||||||
|
self.iv = iv
|
||||||
|
rv = AES_set_decrypt_key(userkey, len(userkey) * 8, keyctx)
|
||||||
|
if rv < 0:
|
||||||
|
raise DrmException('Failed to initialize AES key')
|
||||||
|
|
||||||
|
def decrypt(self, data):
|
||||||
|
out = create_string_buffer(len(data))
|
||||||
|
rv = AES_cbc_encrypt(data, out, len(data), self._keyctx, self.iv, 0)
|
||||||
|
if rv == 0:
|
||||||
|
raise DrmException('AES decryption failed')
|
||||||
|
return out.raw
|
||||||
|
|
||||||
|
def keyivgen(self, passwd, salt, iter, keylen):
|
||||||
|
saltlen = len(salt)
|
||||||
|
passlen = len(passwd)
|
||||||
|
out = create_string_buffer(keylen)
|
||||||
|
rv = PKCS5_PBKDF2_HMAC_SHA1(passwd, passlen, salt, saltlen, iter, keylen, out)
|
||||||
|
return out.raw
|
||||||
|
return LibCrypto
|
||||||
|
|
||||||
|
def _load_crypto():
|
||||||
|
LibCrypto = None
|
||||||
|
try:
|
||||||
|
LibCrypto = _load_crypto_libcrypto()
|
||||||
|
except (ImportError, DrmException):
|
||||||
|
pass
|
||||||
|
return LibCrypto
|
||||||
|
|
||||||
|
LibCrypto = _load_crypto()
|
||||||
|
|
||||||
|
#
|
||||||
|
# Utility Routines
|
||||||
|
#
|
||||||
|
|
||||||
|
# crypto digestroutines
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
def MD5(message):
|
||||||
|
ctx = hashlib.md5()
|
||||||
|
ctx.update(message)
|
||||||
|
return ctx.digest()
|
||||||
|
|
||||||
|
def SHA1(message):
|
||||||
|
ctx = hashlib.sha1()
|
||||||
|
ctx.update(message)
|
||||||
|
return ctx.digest()
|
||||||
|
|
||||||
|
def SHA256(message):
|
||||||
|
ctx = hashlib.sha256()
|
||||||
|
ctx.update(message)
|
||||||
|
return ctx.digest()
|
||||||
|
|
||||||
|
# Various character maps used to decrypt books. Probably supposed to act as obfuscation
|
||||||
|
charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M"
|
||||||
|
charMap2 = "ZB0bYyc1xDdW2wEV3Ff7KkPpL8UuGA4gz-Tme9Nn_tHh5SvXCsIiR6rJjQaqlOoM"
|
||||||
|
|
||||||
|
# For kinf approach of K4PC/K4Mac
|
||||||
|
# On K4PC charMap5 = "AzB0bYyCeVvaZ3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_c1XxDdW2wE"
|
||||||
|
# For Mac they seem to re-use charMap2 here
|
||||||
|
charMap5 = charMap2
|
||||||
|
|
||||||
|
def encode(data, map):
|
||||||
|
result = ""
|
||||||
|
for char in data:
|
||||||
|
value = ord(char)
|
||||||
|
Q = (value ^ 0x80) // len(map)
|
||||||
|
R = value % len(map)
|
||||||
|
result += map[Q]
|
||||||
|
result += map[R]
|
||||||
|
return result
|
||||||
|
|
||||||
|
# Hash the bytes in data and then encode the digest with the characters in map
|
||||||
|
def encodeHash(data,map):
|
||||||
|
return encode(MD5(data),map)
|
||||||
|
|
||||||
|
# Decode the string in data with the characters in map. Returns the decoded bytes
|
||||||
|
def decode(data,map):
|
||||||
|
result = ""
|
||||||
|
for i in range (0,len(data)-1,2):
|
||||||
|
high = map.find(data[i])
|
||||||
|
low = map.find(data[i+1])
|
||||||
|
if (high == -1) or (low == -1) :
|
||||||
|
break
|
||||||
|
value = (((high * len(map)) ^ 0x80) & 0xFF) + low
|
||||||
|
result += pack("B",value)
|
||||||
|
return result
|
||||||
|
|
||||||
|
# For .kinf approach of K4PC and now K4Mac
|
||||||
|
# generate table of prime number less than or equal to int n
|
||||||
|
def primes(n):
|
||||||
|
if n==2: return [2]
|
||||||
|
elif n<2: return []
|
||||||
|
s=range(3,n+1,2)
|
||||||
|
mroot = n ** 0.5
|
||||||
|
half=(n+1)/2-1
|
||||||
|
i=0
|
||||||
|
m=3
|
||||||
|
while m <= mroot:
|
||||||
|
if s[i]:
|
||||||
|
j=(m*m-3)/2
|
||||||
|
s[j]=0
|
||||||
|
while j<half:
|
||||||
|
s[j]=0
|
||||||
|
j+=m
|
||||||
|
i=i+1
|
||||||
|
m=2*i+3
|
||||||
|
return [2]+[x for x in s if x]
|
||||||
|
|
||||||
|
|
||||||
|
# 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"
|
||||||
|
def GetVolumeSerialNumber():
|
||||||
|
sernum = os.getenv('MYSERIALNUMBER')
|
||||||
|
if sernum != None:
|
||||||
|
return sernum
|
||||||
|
cmdline = '/usr/sbin/ioreg -l -S -w 0 -r -c AppleAHCIDiskDriver'
|
||||||
|
cmdline = cmdline.encode(sys.getfilesystemencoding())
|
||||||
|
p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
|
||||||
|
out1, out2 = p.communicate()
|
||||||
|
reslst = out1.split('\n')
|
||||||
|
cnt = len(reslst)
|
||||||
|
bsdname = None
|
||||||
|
sernum = None
|
||||||
|
foundIt = False
|
||||||
|
for j in xrange(cnt):
|
||||||
|
resline = reslst[j]
|
||||||
|
pp = resline.find('"Serial Number" = "')
|
||||||
|
if pp >= 0:
|
||||||
|
sernum = resline[pp+19:-1]
|
||||||
|
sernum = sernum.strip()
|
||||||
|
bb = resline.find('"BSD Name" = "')
|
||||||
|
if bb >= 0:
|
||||||
|
bsdname = resline[bb+14:-1]
|
||||||
|
bsdname = bsdname.strip()
|
||||||
|
if (bsdname == 'disk0') and (sernum != None):
|
||||||
|
foundIt = True
|
||||||
|
break
|
||||||
|
if not foundIt:
|
||||||
|
sernum = ''
|
||||||
|
return sernum
|
||||||
|
|
||||||
|
def GetUserHomeAppSupKindleDirParitionName():
|
||||||
|
home = os.getenv('HOME')
|
||||||
|
dpath = home + '/Library/Application Support/Kindle'
|
||||||
|
cmdline = '/sbin/mount'
|
||||||
|
cmdline = cmdline.encode(sys.getfilesystemencoding())
|
||||||
|
p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
|
||||||
|
out1, out2 = p.communicate()
|
||||||
|
reslst = out1.split('\n')
|
||||||
|
cnt = len(reslst)
|
||||||
|
disk = ''
|
||||||
|
foundIt = False
|
||||||
|
for j in xrange(cnt):
|
||||||
|
resline = reslst[j]
|
||||||
|
if resline.startswith('/dev'):
|
||||||
|
(devpart, mpath) = resline.split(' on ')
|
||||||
|
dpart = devpart[5:]
|
||||||
|
pp = mpath.find('(')
|
||||||
|
if pp >= 0:
|
||||||
|
mpath = mpath[:pp-1]
|
||||||
|
if dpath.startswith(mpath):
|
||||||
|
disk = dpart
|
||||||
|
return disk
|
||||||
|
|
||||||
|
# uses a sub process to get the UUID of the specified disk partition using ioreg
|
||||||
|
def GetDiskPartitionUUID(diskpart):
|
||||||
|
uuidnum = os.getenv('MYUUIDNUMBER')
|
||||||
|
if uuidnum != None:
|
||||||
|
return uuidnum
|
||||||
|
cmdline = '/usr/sbin/ioreg -l -S -w 0 -r -c AppleAHCIDiskDriver'
|
||||||
|
cmdline = cmdline.encode(sys.getfilesystemencoding())
|
||||||
|
p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
|
||||||
|
out1, out2 = p.communicate()
|
||||||
|
reslst = out1.split('\n')
|
||||||
|
cnt = len(reslst)
|
||||||
|
bsdname = None
|
||||||
|
uuidnum = None
|
||||||
|
foundIt = False
|
||||||
|
nest = 0
|
||||||
|
uuidnest = -1
|
||||||
|
partnest = -2
|
||||||
|
for j in xrange(cnt):
|
||||||
|
resline = reslst[j]
|
||||||
|
if resline.find('{') >= 0:
|
||||||
|
nest += 1
|
||||||
|
if resline.find('}') >= 0:
|
||||||
|
nest -= 1
|
||||||
|
pp = resline.find('"UUID" = "')
|
||||||
|
if pp >= 0:
|
||||||
|
uuidnum = resline[pp+10:-1]
|
||||||
|
uuidnum = uuidnum.strip()
|
||||||
|
uuidnest = nest
|
||||||
|
if partnest == uuidnest and uuidnest > 0:
|
||||||
|
foundIt = True
|
||||||
|
break
|
||||||
|
bb = resline.find('"BSD Name" = "')
|
||||||
|
if bb >= 0:
|
||||||
|
bsdname = resline[bb+14:-1]
|
||||||
|
bsdname = bsdname.strip()
|
||||||
|
if (bsdname == diskpart):
|
||||||
|
partnest = nest
|
||||||
|
else :
|
||||||
|
partnest = -2
|
||||||
|
if partnest == uuidnest and partnest > 0:
|
||||||
|
foundIt = True
|
||||||
|
break
|
||||||
|
if nest == 0:
|
||||||
|
partnest = -2
|
||||||
|
uuidnest = -1
|
||||||
|
uuidnum = None
|
||||||
|
bsdname = None
|
||||||
|
if not foundIt:
|
||||||
|
uuidnum = ''
|
||||||
|
return uuidnum
|
||||||
|
|
||||||
|
def GetMACAddressMunged():
|
||||||
|
macnum = os.getenv('MYMACNUM')
|
||||||
|
if macnum != None:
|
||||||
|
return macnum
|
||||||
|
cmdline = '/sbin/ifconfig en0'
|
||||||
|
cmdline = cmdline.encode(sys.getfilesystemencoding())
|
||||||
|
p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
|
||||||
|
out1, out2 = p.communicate()
|
||||||
|
reslst = out1.split('\n')
|
||||||
|
cnt = len(reslst)
|
||||||
|
macnum = None
|
||||||
|
foundIt = False
|
||||||
|
for j in xrange(cnt):
|
||||||
|
resline = reslst[j]
|
||||||
|
pp = resline.find('ether ')
|
||||||
|
if pp >= 0:
|
||||||
|
macnum = resline[pp+6:-1]
|
||||||
|
macnum = macnum.strip()
|
||||||
|
# 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(':')
|
||||||
|
n = len(maclst)
|
||||||
|
if n != 6:
|
||||||
|
fountIt = False
|
||||||
|
break
|
||||||
|
for i in range(6):
|
||||||
|
maclst[i] = int('0x' + maclst[i], 0)
|
||||||
|
mlst = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
|
||||||
|
mlst[5] = maclst[5] ^ 0xa5
|
||||||
|
mlst[4] = maclst[3] ^ 0xa5
|
||||||
|
mlst[3] = maclst[4] ^ 0xa5
|
||||||
|
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])
|
||||||
|
foundIt = True
|
||||||
|
break
|
||||||
|
if not foundIt:
|
||||||
|
macnum = ''
|
||||||
|
return macnum
|
||||||
|
|
||||||
|
|
||||||
|
# uses unix env to get username instead of using sysctlbyname
|
||||||
|
def GetUserName():
|
||||||
|
username = os.getenv('USER')
|
||||||
|
return username
|
||||||
|
|
||||||
|
|
||||||
|
# implements an Pseudo Mac Version of Windows built-in Crypto routine
|
||||||
|
# used by Kindle for Mac versions < 1.6.0
|
||||||
|
def CryptUnprotectData(encryptedData):
|
||||||
|
sernum = GetVolumeSerialNumber()
|
||||||
|
if sernum == '':
|
||||||
|
sernum = '9999999999'
|
||||||
|
sp = sernum + '!@#' + GetUserName()
|
||||||
|
passwdData = encode(SHA256(sp),charMap1)
|
||||||
|
salt = '16743'
|
||||||
|
iter = 0x3e8
|
||||||
|
keylen = 0x80
|
||||||
|
crp = LibCrypto()
|
||||||
|
key_iv = crp.keyivgen(passwdData, salt, iter, keylen)
|
||||||
|
key = key_iv[0:32]
|
||||||
|
iv = key_iv[32:48]
|
||||||
|
crp.set_decrypt_key(key,iv)
|
||||||
|
cleartext = crp.decrypt(encryptedData)
|
||||||
|
cleartext = decode(cleartext,charMap1)
|
||||||
|
return cleartext
|
||||||
|
|
||||||
|
|
||||||
|
def isNewInstall():
|
||||||
|
home = os.getenv('HOME')
|
||||||
|
# soccer game fan anyone
|
||||||
|
dpath = home + '/Library/Application Support/Kindle/storage/.pes2011'
|
||||||
|
# print dpath, os.path.exists(dpath)
|
||||||
|
if os.path.exists(dpath):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def GetIDString():
|
||||||
|
# K4Mac now has an extensive set of ids strings it uses
|
||||||
|
# in encoding pids and in creating unique passwords
|
||||||
|
# for use in its own version of CryptUnprotectDataV2
|
||||||
|
|
||||||
|
# BUT Amazon has now become nasty enough to detect when its app
|
||||||
|
# is being run under a debugger and actually changes code paths
|
||||||
|
# including which one of these strings is chosen, all to try
|
||||||
|
# to prevent reverse engineering
|
||||||
|
|
||||||
|
# Sad really ... they will only hurt their own sales ...
|
||||||
|
# true book lovers really want to keep their books forever
|
||||||
|
# and move them to their devices and DRM prevents that so they
|
||||||
|
# will just buy from someplace else that they can remove
|
||||||
|
# the DRM from
|
||||||
|
|
||||||
|
# Amazon should know by now that true book lover's are not like
|
||||||
|
# penniless kids that pirate music, we do not pirate books
|
||||||
|
|
||||||
|
if isNewInstall():
|
||||||
|
mungedmac = GetMACAddressMunged()
|
||||||
|
if len(mungedmac) > 7:
|
||||||
|
return mungedmac
|
||||||
|
sernum = GetVolumeSerialNumber()
|
||||||
|
if len(sernum) > 7:
|
||||||
|
return sernum
|
||||||
|
diskpart = GetUserHomeAppSupKindleDirParitionName()
|
||||||
|
uuidnum = GetDiskPartitionUUID(diskpart)
|
||||||
|
if len(uuidnum) > 7:
|
||||||
|
return uuidnum
|
||||||
|
mungedmac = GetMACAddressMunged()
|
||||||
|
if len(mungedmac) > 7:
|
||||||
|
return mungedmac
|
||||||
|
return '9999999999'
|
||||||
|
|
||||||
|
|
||||||
|
# implements an Pseudo Mac Version of Windows built-in Crypto routine
|
||||||
|
# used for Kindle for Mac Versions >= 1.6.0
|
||||||
|
def CryptUnprotectDataV2(encryptedData):
|
||||||
|
sp = GetUserName() + ':&%:' + GetIDString()
|
||||||
|
passwdData = encode(SHA256(sp),charMap5)
|
||||||
|
# salt generation as per the code
|
||||||
|
salt = 0x0512981d * 2 * 1 * 1
|
||||||
|
salt = str(salt) + GetUserName()
|
||||||
|
salt = encode(salt,charMap5)
|
||||||
|
crp = LibCrypto()
|
||||||
|
iter = 0x800
|
||||||
|
keylen = 0x400
|
||||||
|
key_iv = crp.keyivgen(passwdData, salt, iter, keylen)
|
||||||
|
key = key_iv[0:32]
|
||||||
|
iv = key_iv[32:48]
|
||||||
|
crp.set_decrypt_key(key,iv)
|
||||||
|
cleartext = crp.decrypt(encryptedData)
|
||||||
|
cleartext = decode(cleartext, charMap5)
|
||||||
|
return cleartext
|
||||||
|
|
||||||
|
|
||||||
|
# Locate the .kindle-info files
|
||||||
|
def getKindleInfoFiles(kInfoFiles):
|
||||||
|
# first search for current .kindle-info files
|
||||||
|
home = os.getenv('HOME')
|
||||||
|
cmdline = 'find "' + home + '/Library/Application Support" -name ".kindle-info"'
|
||||||
|
cmdline = cmdline.encode(sys.getfilesystemencoding())
|
||||||
|
p1 = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
|
||||||
|
out1, out2 = p1.communicate()
|
||||||
|
reslst = out1.split('\n')
|
||||||
|
kinfopath = 'NONE'
|
||||||
|
found = False
|
||||||
|
for resline in reslst:
|
||||||
|
if os.path.isfile(resline):
|
||||||
|
kInfoFiles.append(resline)
|
||||||
|
found = True
|
||||||
|
# add any .kinf files
|
||||||
|
cmdline = 'find "' + home + '/Library/Application Support" -name ".rainier*-kinf"'
|
||||||
|
cmdline = cmdline.encode(sys.getfilesystemencoding())
|
||||||
|
p1 = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
|
||||||
|
out1, out2 = p1.communicate()
|
||||||
|
reslst = out1.split('\n')
|
||||||
|
for resline in reslst:
|
||||||
|
if os.path.isfile(resline):
|
||||||
|
kInfoFiles.append(resline)
|
||||||
|
found = True
|
||||||
|
if not found:
|
||||||
|
print('No kindle-info files have been found.')
|
||||||
|
return kInfoFiles
|
||||||
|
|
||||||
|
# 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"]
|
||||||
|
DB = {}
|
||||||
|
cnt = 0
|
||||||
|
infoReader = open(kInfoFile, 'r')
|
||||||
|
hdr = infoReader.read(1)
|
||||||
|
data = infoReader.read()
|
||||||
|
|
||||||
|
if data.find('[') != -1 :
|
||||||
|
# older style kindle-info file
|
||||||
|
items = data.split('[')
|
||||||
|
for item in items:
|
||||||
|
if item != '':
|
||||||
|
keyhash, rawdata = item.split(':')
|
||||||
|
keyname = "unknown"
|
||||||
|
for name in names:
|
||||||
|
if encodeHash(name,charMap2) == keyhash:
|
||||||
|
keyname = name
|
||||||
|
break
|
||||||
|
if keyname == "unknown":
|
||||||
|
keyname = keyhash
|
||||||
|
encryptedValue = decode(rawdata,charMap2)
|
||||||
|
cleartext = CryptUnprotectData(encryptedValue)
|
||||||
|
DB[keyname] = cleartext
|
||||||
|
cnt = cnt + 1
|
||||||
|
if cnt == 0:
|
||||||
|
DB = None
|
||||||
|
return DB
|
||||||
|
|
||||||
|
# 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
|
||||||
|
data = data[:-1]
|
||||||
|
items = data.split('/')
|
||||||
|
|
||||||
|
# loop through the item records until all are processed
|
||||||
|
while len(items) > 0:
|
||||||
|
|
||||||
|
# get the first item record
|
||||||
|
item = items.pop(0)
|
||||||
|
|
||||||
|
# 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"
|
||||||
|
|
||||||
|
# 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 = SHA1(keyhash)
|
||||||
|
|
||||||
|
# the remainder of the first record when decoded with charMap5
|
||||||
|
# has the ':' split char followed by the string representation
|
||||||
|
# of the number of records that follow
|
||||||
|
# and make up the contents
|
||||||
|
srcnt = decode(item[34:],charMap5)
|
||||||
|
rcnt = int(srcnt)
|
||||||
|
|
||||||
|
# read and store in rcnt records of data
|
||||||
|
# that make up the contents value
|
||||||
|
edlst = []
|
||||||
|
for i in xrange(rcnt):
|
||||||
|
item = items.pop(0)
|
||||||
|
edlst.append(item)
|
||||||
|
|
||||||
|
keyname = "unknown"
|
||||||
|
for name in names:
|
||||||
|
if encodeHash(name,charMap5) == keyhash:
|
||||||
|
keyname = name
|
||||||
|
break
|
||||||
|
if keyname == "unknown":
|
||||||
|
keyname = keyhash
|
||||||
|
|
||||||
|
# the charMap5 encoded contents data has had a length
|
||||||
|
# of chars (always odd) cut off of the front and moved
|
||||||
|
# to the end to prevent decoding using charMap5 from
|
||||||
|
# working properly, and thereby preventing the ensuing
|
||||||
|
# CryptUnprotectData call from succeeding.
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
# move first offsets chars to end to align for decode by charMap5
|
||||||
|
encdata = "".join(edlst)
|
||||||
|
contlen = len(encdata)
|
||||||
|
|
||||||
|
# now properly split and recombine
|
||||||
|
# by moving noffset chars from the start of the
|
||||||
|
# string to the end of the string
|
||||||
|
noffset = contlen - primes(int(contlen/3))[-1]
|
||||||
|
pfx = encdata[0:noffset]
|
||||||
|
encdata = encdata[noffset:]
|
||||||
|
encdata = encdata + pfx
|
||||||
|
|
||||||
|
# decode using charMap5 to get the CryptProtect Data
|
||||||
|
encryptedValue = decode(encdata,charMap5)
|
||||||
|
cleartext = CryptUnprotectDataV2(encryptedValue)
|
||||||
|
# Debugging
|
||||||
|
# print keyname
|
||||||
|
# print cleartext
|
||||||
|
# print cleartext.encode('hex')
|
||||||
|
# print
|
||||||
|
DB[keyname] = cleartext
|
||||||
|
cnt = cnt + 1
|
||||||
|
|
||||||
|
if cnt == 0:
|
||||||
|
DB = None
|
||||||
|
return DB
|
||||||
@@ -0,0 +1,308 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# K4PC Windows specific routines
|
||||||
|
|
||||||
|
from __future__ import with_statement
|
||||||
|
|
||||||
|
import sys, os
|
||||||
|
from struct import pack, unpack, unpack_from
|
||||||
|
|
||||||
|
from ctypes import windll, c_char_p, c_wchar_p, c_uint, POINTER, byref, \
|
||||||
|
create_unicode_buffer, create_string_buffer, CFUNCTYPE, addressof, \
|
||||||
|
string_at, Structure, c_void_p, cast
|
||||||
|
|
||||||
|
import _winreg as winreg
|
||||||
|
|
||||||
|
MAX_PATH = 255
|
||||||
|
|
||||||
|
kernel32 = windll.kernel32
|
||||||
|
advapi32 = windll.advapi32
|
||||||
|
crypt32 = windll.crypt32
|
||||||
|
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
# crypto digestroutines
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
def MD5(message):
|
||||||
|
ctx = hashlib.md5()
|
||||||
|
ctx.update(message)
|
||||||
|
return ctx.digest()
|
||||||
|
|
||||||
|
def SHA1(message):
|
||||||
|
ctx = hashlib.sha1()
|
||||||
|
ctx.update(message)
|
||||||
|
return ctx.digest()
|
||||||
|
|
||||||
|
|
||||||
|
# simple primes table (<= n) calculator
|
||||||
|
def primes(n):
|
||||||
|
if n==2: return [2]
|
||||||
|
elif n<2: return []
|
||||||
|
s=range(3,n+1,2)
|
||||||
|
mroot = n ** 0.5
|
||||||
|
half=(n+1)/2-1
|
||||||
|
i=0
|
||||||
|
m=3
|
||||||
|
while m <= mroot:
|
||||||
|
if s[i]:
|
||||||
|
j=(m*m-3)/2
|
||||||
|
s[j]=0
|
||||||
|
while j<half:
|
||||||
|
s[j]=0
|
||||||
|
j+=m
|
||||||
|
i=i+1
|
||||||
|
m=2*i+3
|
||||||
|
return [2]+[x for x in s if x]
|
||||||
|
|
||||||
|
|
||||||
|
# Various character maps used to decrypt kindle info values.
|
||||||
|
# Probably supposed to act as obfuscation
|
||||||
|
charMap2 = "AaZzB0bYyCc1XxDdW2wEeVv3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_"
|
||||||
|
charMap5 = "AzB0bYyCeVvaZ3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_c1XxDdW2wE"
|
||||||
|
|
||||||
|
class DrmException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Encode the bytes in data with the characters in map
|
||||||
|
def encode(data, map):
|
||||||
|
result = ""
|
||||||
|
for char in data:
|
||||||
|
value = ord(char)
|
||||||
|
Q = (value ^ 0x80) // len(map)
|
||||||
|
R = value % len(map)
|
||||||
|
result += map[Q]
|
||||||
|
result += map[R]
|
||||||
|
return result
|
||||||
|
|
||||||
|
# Hash the bytes in data and then encode the digest with the characters in map
|
||||||
|
def encodeHash(data,map):
|
||||||
|
return encode(MD5(data),map)
|
||||||
|
|
||||||
|
# Decode the string in data with the characters in map. Returns the decoded bytes
|
||||||
|
def decode(data,map):
|
||||||
|
result = ""
|
||||||
|
for i in range (0,len(data)-1,2):
|
||||||
|
high = map.find(data[i])
|
||||||
|
low = map.find(data[i+1])
|
||||||
|
if (high == -1) or (low == -1) :
|
||||||
|
break
|
||||||
|
value = (((high * len(map)) ^ 0x80) & 0xFF) + low
|
||||||
|
result += pack("B",value)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# interface with Windows OS Routines
|
||||||
|
class DataBlob(Structure):
|
||||||
|
_fields_ = [('cbData', c_uint),
|
||||||
|
('pbData', c_void_p)]
|
||||||
|
DataBlob_p = POINTER(DataBlob)
|
||||||
|
|
||||||
|
|
||||||
|
def GetSystemDirectory():
|
||||||
|
GetSystemDirectoryW = kernel32.GetSystemDirectoryW
|
||||||
|
GetSystemDirectoryW.argtypes = [c_wchar_p, c_uint]
|
||||||
|
GetSystemDirectoryW.restype = c_uint
|
||||||
|
def GetSystemDirectory():
|
||||||
|
buffer = create_unicode_buffer(MAX_PATH + 1)
|
||||||
|
GetSystemDirectoryW(buffer, len(buffer))
|
||||||
|
return buffer.value
|
||||||
|
return GetSystemDirectory
|
||||||
|
GetSystemDirectory = GetSystemDirectory()
|
||||||
|
|
||||||
|
def GetVolumeSerialNumber():
|
||||||
|
GetVolumeInformationW = kernel32.GetVolumeInformationW
|
||||||
|
GetVolumeInformationW.argtypes = [c_wchar_p, c_wchar_p, c_uint,
|
||||||
|
POINTER(c_uint), POINTER(c_uint),
|
||||||
|
POINTER(c_uint), c_wchar_p, c_uint]
|
||||||
|
GetVolumeInformationW.restype = c_uint
|
||||||
|
def GetVolumeSerialNumber(path = GetSystemDirectory().split('\\')[0] + '\\'):
|
||||||
|
vsn = c_uint(0)
|
||||||
|
GetVolumeInformationW(path, None, 0, byref(vsn), None, None, None, 0)
|
||||||
|
return str(vsn.value)
|
||||||
|
return GetVolumeSerialNumber
|
||||||
|
GetVolumeSerialNumber = GetVolumeSerialNumber()
|
||||||
|
|
||||||
|
def GetIDString():
|
||||||
|
return GetVolumeSerialNumber()
|
||||||
|
|
||||||
|
def getLastError():
|
||||||
|
GetLastError = kernel32.GetLastError
|
||||||
|
GetLastError.argtypes = None
|
||||||
|
GetLastError.restype = c_uint
|
||||||
|
def getLastError():
|
||||||
|
return GetLastError()
|
||||||
|
return getLastError
|
||||||
|
getLastError = getLastError()
|
||||||
|
|
||||||
|
def GetUserName():
|
||||||
|
GetUserNameW = advapi32.GetUserNameW
|
||||||
|
GetUserNameW.argtypes = [c_wchar_p, POINTER(c_uint)]
|
||||||
|
GetUserNameW.restype = c_uint
|
||||||
|
def GetUserName():
|
||||||
|
buffer = create_unicode_buffer(2)
|
||||||
|
size = c_uint(len(buffer))
|
||||||
|
while not GetUserNameW(buffer, byref(size)):
|
||||||
|
errcd = getLastError()
|
||||||
|
if errcd == 234:
|
||||||
|
# bad wine implementation up through wine 1.3.21
|
||||||
|
return "AlternateUserName"
|
||||||
|
buffer = create_unicode_buffer(len(buffer) * 2)
|
||||||
|
size.value = len(buffer)
|
||||||
|
return buffer.value.encode('utf-16-le')[::2]
|
||||||
|
return GetUserName
|
||||||
|
GetUserName = GetUserName()
|
||||||
|
|
||||||
|
def CryptUnprotectData():
|
||||||
|
_CryptUnprotectData = crypt32.CryptUnprotectData
|
||||||
|
_CryptUnprotectData.argtypes = [DataBlob_p, c_wchar_p, DataBlob_p,
|
||||||
|
c_void_p, c_void_p, c_uint, DataBlob_p]
|
||||||
|
_CryptUnprotectData.restype = c_uint
|
||||||
|
def CryptUnprotectData(indata, entropy, flags):
|
||||||
|
indatab = create_string_buffer(indata)
|
||||||
|
indata = DataBlob(len(indata), cast(indatab, c_void_p))
|
||||||
|
entropyb = create_string_buffer(entropy)
|
||||||
|
entropy = DataBlob(len(entropy), cast(entropyb, c_void_p))
|
||||||
|
outdata = DataBlob()
|
||||||
|
if not _CryptUnprotectData(byref(indata), None, byref(entropy),
|
||||||
|
None, None, flags, byref(outdata)):
|
||||||
|
raise DrmException("Failed to Unprotect Data")
|
||||||
|
return string_at(outdata.pbData, outdata.cbData)
|
||||||
|
return CryptUnprotectData
|
||||||
|
CryptUnprotectData = CryptUnprotectData()
|
||||||
|
|
||||||
|
|
||||||
|
# Locate all of the kindle-info style files and return as list
|
||||||
|
def getKindleInfoFiles(kInfoFiles):
|
||||||
|
regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\")
|
||||||
|
path = winreg.QueryValueEx(regkey, 'Local AppData')[0]
|
||||||
|
|
||||||
|
# first look for older kindle-info files
|
||||||
|
kinfopath = path +'\\Amazon\\Kindle For PC\\{AMAwzsaPaaZAzmZzZQzgZCAkZ3AjA_AY}\\kindle.info'
|
||||||
|
if not os.path.isfile(kinfopath):
|
||||||
|
print('No kindle.info files have not been found.')
|
||||||
|
else:
|
||||||
|
kInfoFiles.append(kinfopath)
|
||||||
|
|
||||||
|
# now look for newer (K4PC 1.5.0 and later rainier.2.1.1.kinf file
|
||||||
|
|
||||||
|
kinfopath = path +'\\Amazon\\Kindle For PC\\storage\\rainier.2.1.1.kinf'
|
||||||
|
if not os.path.isfile(kinfopath):
|
||||||
|
print('No K4PC 1.5.X .kinf files have not been found.')
|
||||||
|
else:
|
||||||
|
kInfoFiles.append(kinfopath)
|
||||||
|
|
||||||
|
# now look for even newer (K4PC 1.6.0 and later) rainier.2.1.1.kinf file
|
||||||
|
kinfopath = path +'\\Amazon\\Kindle\\storage\\rainier.2.1.1.kinf'
|
||||||
|
if not os.path.isfile(kinfopath):
|
||||||
|
print('No K4PC 1.6.X .kinf files have not been found.')
|
||||||
|
else:
|
||||||
|
kInfoFiles.append(kinfopath)
|
||||||
|
|
||||||
|
return kInfoFiles
|
||||||
|
|
||||||
|
|
||||||
|
# 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"]
|
||||||
|
DB = {}
|
||||||
|
cnt = 0
|
||||||
|
infoReader = open(kInfoFile, 'r')
|
||||||
|
hdr = infoReader.read(1)
|
||||||
|
data = infoReader.read()
|
||||||
|
|
||||||
|
if data.find('{') != -1 :
|
||||||
|
|
||||||
|
# older style kindle-info file
|
||||||
|
items = data.split('{')
|
||||||
|
for item in items:
|
||||||
|
if item != '':
|
||||||
|
keyhash, rawdata = item.split(':')
|
||||||
|
keyname = "unknown"
|
||||||
|
for name in names:
|
||||||
|
if encodeHash(name,charMap2) == keyhash:
|
||||||
|
keyname = name
|
||||||
|
break
|
||||||
|
if keyname == "unknown":
|
||||||
|
keyname = keyhash
|
||||||
|
encryptedValue = decode(rawdata,charMap2)
|
||||||
|
DB[keyname] = CryptUnprotectData(encryptedValue, "", 0)
|
||||||
|
cnt = cnt + 1
|
||||||
|
if cnt == 0:
|
||||||
|
DB = None
|
||||||
|
return DB
|
||||||
|
|
||||||
|
# else newer style .kinf file
|
||||||
|
# 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('/')
|
||||||
|
|
||||||
|
# loop through the item records until all are processed
|
||||||
|
while len(items) > 0:
|
||||||
|
|
||||||
|
# get the first item record
|
||||||
|
item = items.pop(0)
|
||||||
|
|
||||||
|
# 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]
|
||||||
|
|
||||||
|
# the raw keyhash string is also used to create entropy for the actual
|
||||||
|
# CryptProtectData Blob that represents that keys contents
|
||||||
|
entropy = SHA1(keyhash)
|
||||||
|
|
||||||
|
# the remainder of the first record when decoded with charMap5
|
||||||
|
# has the ':' split char followed by the string representation
|
||||||
|
# of the number of records that follow
|
||||||
|
# and make up the contents
|
||||||
|
srcnt = decode(item[34:],charMap5)
|
||||||
|
rcnt = int(srcnt)
|
||||||
|
|
||||||
|
# read and store in rcnt records of data
|
||||||
|
# that make up the contents value
|
||||||
|
edlst = []
|
||||||
|
for i in xrange(rcnt):
|
||||||
|
item = items.pop(0)
|
||||||
|
edlst.append(item)
|
||||||
|
|
||||||
|
keyname = "unknown"
|
||||||
|
for name in names:
|
||||||
|
if encodeHash(name,charMap5) == keyhash:
|
||||||
|
keyname = name
|
||||||
|
break
|
||||||
|
if keyname == "unknown":
|
||||||
|
keyname = keyhash
|
||||||
|
|
||||||
|
# the charMap5 encoded contents data has had a length
|
||||||
|
# of chars (always odd) cut off of the front and moved
|
||||||
|
# to the end to prevent decoding using charMap5 from
|
||||||
|
# working properly, and thereby preventing the ensuing
|
||||||
|
# CryptUnprotectData call from succeeding.
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
# move first offsets chars to end to align for decode by charMap5
|
||||||
|
encdata = "".join(edlst)
|
||||||
|
contlen = len(encdata)
|
||||||
|
noffset = contlen - primes(int(contlen/3))[-1]
|
||||||
|
|
||||||
|
# now properly split and recombine
|
||||||
|
# by moving noffset chars from the start of the
|
||||||
|
# string to the end of the string
|
||||||
|
pfx = encdata[0:noffset]
|
||||||
|
encdata = encdata[noffset:]
|
||||||
|
encdata = encdata + pfx
|
||||||
|
|
||||||
|
# decode using Map5 to get the CryptProtect Data
|
||||||
|
encryptedValue = decode(encdata,charMap5)
|
||||||
|
DB[keyname] = CryptUnprotectData(encryptedValue, entropy, 1)
|
||||||
|
cnt = cnt + 1
|
||||||
|
|
||||||
|
if cnt == 0:
|
||||||
|
DB = None
|
||||||
|
return DB
|
||||||
|
|
||||||
|
|
||||||
@@ -11,16 +11,28 @@ from struct import pack, unpack, unpack_from
|
|||||||
class DrmException(Exception):
|
class DrmException(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
global kindleDatabase
|
|
||||||
global charMap1
|
global charMap1
|
||||||
global charMap2
|
|
||||||
global charMap3
|
global charMap3
|
||||||
global charMap4
|
global charMap4
|
||||||
|
|
||||||
|
if 'calibre' in sys.modules:
|
||||||
|
inCalibre = True
|
||||||
|
else:
|
||||||
|
inCalibre = False
|
||||||
|
|
||||||
|
if inCalibre:
|
||||||
if sys.platform.startswith('win'):
|
if sys.platform.startswith('win'):
|
||||||
from k4pcutils import openKindleInfo, CryptUnprotectData, GetUserName, GetVolumeSerialNumber, charMap2
|
from calibre_plugins.k4mobidedrm.k4pcutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString
|
||||||
|
|
||||||
if sys.platform.startswith('darwin'):
|
if sys.platform.startswith('darwin'):
|
||||||
from k4mutils import openKindleInfo, CryptUnprotectData, GetUserName, GetVolumeSerialNumber, charMap2
|
from calibre_plugins.k4mobidedrm.k4mutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString
|
||||||
|
else:
|
||||||
|
if sys.platform.startswith('win'):
|
||||||
|
from k4pcutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString
|
||||||
|
|
||||||
|
if sys.platform.startswith('darwin'):
|
||||||
|
from k4mutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString
|
||||||
|
|
||||||
|
|
||||||
charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M"
|
charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M"
|
||||||
charMap3 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
charMap3 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
||||||
@@ -67,62 +79,6 @@ def decode(data,map):
|
|||||||
result += pack("B",value)
|
result += pack("B",value)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
# Parse the Kindle.info file and return the records as a list of key-values
|
|
||||||
def parseKindleInfo(kInfoFile):
|
|
||||||
DB = {}
|
|
||||||
infoReader = openKindleInfo(kInfoFile)
|
|
||||||
infoReader.read(1)
|
|
||||||
data = infoReader.read()
|
|
||||||
if sys.platform.startswith('win'):
|
|
||||||
items = data.split('{')
|
|
||||||
else :
|
|
||||||
items = data.split('[')
|
|
||||||
for item in items:
|
|
||||||
splito = item.split(':')
|
|
||||||
DB[splito[0]] =splito[1]
|
|
||||||
return DB
|
|
||||||
|
|
||||||
# Get a record from the Kindle.info file for the key "hashedKey" (already hashed and encoded). Return the decoded and decrypted record
|
|
||||||
def getKindleInfoValueForHash(hashedKey):
|
|
||||||
global kindleDatabase
|
|
||||||
global charMap1
|
|
||||||
global charMap2
|
|
||||||
encryptedValue = decode(kindleDatabase[hashedKey],charMap2)
|
|
||||||
if sys.platform.startswith('win'):
|
|
||||||
return CryptUnprotectData(encryptedValue,"")
|
|
||||||
else:
|
|
||||||
cleartext = CryptUnprotectData(encryptedValue)
|
|
||||||
return decode(cleartext, charMap1)
|
|
||||||
|
|
||||||
# Get a record from the Kindle.info file for the string in "key" (plaintext). Return the decoded and decrypted record
|
|
||||||
def getKindleInfoValueForKey(key):
|
|
||||||
global charMap2
|
|
||||||
return getKindleInfoValueForHash(encodeHash(key,charMap2))
|
|
||||||
|
|
||||||
# Find if the original string for a hashed/encoded string is known. If so return the original string othwise return an empty string.
|
|
||||||
def findNameForHash(hash):
|
|
||||||
global charMap2
|
|
||||||
names = ["kindle.account.tokens","kindle.cookie.item","eulaVersionAccepted","login_date","kindle.token.item","login","kindle.key.item","kindle.name.info","kindle.device.info", "MazamaRandomNumber"]
|
|
||||||
result = ""
|
|
||||||
for name in names:
|
|
||||||
if hash == encodeHash(name, charMap2):
|
|
||||||
result = name
|
|
||||||
break
|
|
||||||
return result
|
|
||||||
|
|
||||||
# Print all the records from the kindle.info file (option -i)
|
|
||||||
def printKindleInfo():
|
|
||||||
for record in kindleDatabase:
|
|
||||||
name = findNameForHash(record)
|
|
||||||
if name != "" :
|
|
||||||
print (name)
|
|
||||||
print ("--------------------------")
|
|
||||||
else :
|
|
||||||
print ("Unknown Record")
|
|
||||||
print getKindleInfoValueForHash(record)
|
|
||||||
print "\n"
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# PID generation routines
|
# PID generation routines
|
||||||
#
|
#
|
||||||
@@ -221,8 +177,6 @@ def pidFromSerial(s, l):
|
|||||||
|
|
||||||
# Parse the EXTH header records and use the Kindle serial number to calculate the book pid.
|
# Parse the EXTH header records and use the Kindle serial number to calculate the book pid.
|
||||||
def getKindlePid(pidlst, rec209, token, serialnum):
|
def getKindlePid(pidlst, rec209, token, serialnum):
|
||||||
|
|
||||||
if rec209 != None:
|
|
||||||
# Compute book PID
|
# Compute book PID
|
||||||
pidHash = SHA1(serialnum+rec209+token)
|
pidHash = SHA1(serialnum+rec209+token)
|
||||||
bookPID = encodePID(pidHash)
|
bookPID = encodePID(pidHash)
|
||||||
@@ -237,33 +191,41 @@ def getKindlePid(pidlst, rec209, token, serialnum):
|
|||||||
return pidlst
|
return pidlst
|
||||||
|
|
||||||
|
|
||||||
# Parse the EXTH header records and parse the Kindleinfo
|
# parse the Kindleinfo file to calculate the book pid.
|
||||||
# file to calculate the book pid.
|
|
||||||
|
|
||||||
def getK4Pids(pidlst, rec209, token, kInfoFile=None):
|
keynames = ["kindle.account.tokens","kindle.cookie.item","eulaVersionAccepted","login_date","kindle.token.item","login","kindle.key.item","kindle.name.info","kindle.device.info", "MazamaRandomNumber"]
|
||||||
global kindleDatabase
|
|
||||||
|
def getK4Pids(pidlst, rec209, token, kInfoFile):
|
||||||
global charMap1
|
global charMap1
|
||||||
kindleDatabase = None
|
kindleDatabase = None
|
||||||
try:
|
try:
|
||||||
kindleDatabase = parseKindleInfo(kInfoFile)
|
kindleDatabase = getDBfromFile(kInfoFile)
|
||||||
except Exception, message:
|
except Exception, message:
|
||||||
print(message)
|
print(message)
|
||||||
|
kindleDatabase = None
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if kindleDatabase == None :
|
if kindleDatabase == None :
|
||||||
return pidlst
|
return pidlst
|
||||||
|
|
||||||
|
try:
|
||||||
# Get the Mazama Random number
|
# Get the Mazama Random number
|
||||||
MazamaRandomNumber = getKindleInfoValueForKey("MazamaRandomNumber")
|
MazamaRandomNumber = kindleDatabase["MazamaRandomNumber"]
|
||||||
|
|
||||||
# Get the HDD serial
|
# Get the kindle account token
|
||||||
encodedSystemVolumeSerialNumber = encodeHash(GetVolumeSerialNumber(),charMap1)
|
kindleAccountToken = kindleDatabase["kindle.account.tokens"]
|
||||||
|
except KeyError:
|
||||||
|
print "Keys not found in " + kInfoFile
|
||||||
|
return pidlst
|
||||||
|
|
||||||
|
# Get the ID string used
|
||||||
|
encodedIDString = encodeHash(GetIDString(),charMap1)
|
||||||
|
|
||||||
# Get the current user name
|
# Get the current user name
|
||||||
encodedUsername = encodeHash(GetUserName(),charMap1)
|
encodedUsername = encodeHash(GetUserName(),charMap1)
|
||||||
|
|
||||||
# concat, hash and encode to calculate the DSN
|
# concat, hash and encode to calculate the DSN
|
||||||
DSN = encode(SHA1(MazamaRandomNumber+encodedSystemVolumeSerialNumber+encodedUsername),charMap1)
|
DSN = encode(SHA1(MazamaRandomNumber+encodedIDString+encodedUsername),charMap1)
|
||||||
|
|
||||||
# Compute the device PID (for which I can tell, is used for nothing).
|
# Compute the device PID (for which I can tell, is used for nothing).
|
||||||
table = generatePidEncryptionTable()
|
table = generatePidEncryptionTable()
|
||||||
@@ -271,13 +233,7 @@ def getK4Pids(pidlst, rec209, token, kInfoFile=None):
|
|||||||
devicePID = checksumPid(devicePID)
|
devicePID = checksumPid(devicePID)
|
||||||
pidlst.append(devicePID)
|
pidlst.append(devicePID)
|
||||||
|
|
||||||
# Compute book PID
|
# Compute book PIDs
|
||||||
if rec209 == None:
|
|
||||||
print "\nNo EXTH record type 209 - Perhaps not a K4 file?"
|
|
||||||
return pidlst
|
|
||||||
|
|
||||||
# Get the kindle account token
|
|
||||||
kindleAccountToken = getKindleInfoValueForKey("kindle.account.tokens")
|
|
||||||
|
|
||||||
# book pid
|
# book pid
|
||||||
pidHash = SHA1(DSN+kindleAccountToken+rec209+token)
|
pidHash = SHA1(DSN+kindleAccountToken+rec209+token)
|
||||||
@@ -301,8 +257,10 @@ def getK4Pids(pidlst, rec209, token, kInfoFile=None):
|
|||||||
|
|
||||||
def getPidList(md1, md2, k4, pids, serials, kInfoFiles):
|
def getPidList(md1, md2, k4, pids, serials, kInfoFiles):
|
||||||
pidlst = []
|
pidlst = []
|
||||||
|
if kInfoFiles is None:
|
||||||
|
kInfoFiles = []
|
||||||
if k4:
|
if k4:
|
||||||
pidlst = getK4Pids(pidlst, md1, md2)
|
kInfoFiles = getKindleInfoFiles(kInfoFiles)
|
||||||
for infoFile in kInfoFiles:
|
for infoFile in kInfoFiles:
|
||||||
pidlst = getK4Pids(pidlst, md1, md2, infoFile)
|
pidlst = getK4Pids(pidlst, md1, md2, infoFile)
|
||||||
for serialnum in serials:
|
for serialnum in serials:
|
||||||
@@ -42,8 +42,19 @@
|
|||||||
# 0.20 - Correction: It seems that multibyte entries are encrypted in a v6 file.
|
# 0.20 - Correction: It seems that multibyte entries are encrypted in a v6 file.
|
||||||
# 0.21 - Added support for multiple pids
|
# 0.21 - Added support for multiple pids
|
||||||
# 0.22 - revised structure to hold MobiBook as a class to allow an extended interface
|
# 0.22 - revised structure to hold MobiBook as a class to allow an extended interface
|
||||||
|
# 0.23 - fixed problem with older files with no EXTH section
|
||||||
|
# 0.24 - add support for type 1 encryption and 'TEXtREAd' books as well
|
||||||
|
# 0.25 - Fixed support for 'BOOKMOBI' type 1 encryption
|
||||||
|
# 0.26 - Now enables Text-To-Speech flag and sets clipping limit to 100%
|
||||||
|
# 0.27 - Correct pid metadata token generation to match that used by skindle (Thank You Bart!)
|
||||||
|
# 0.28 - slight additional changes to metadata token generation (None -> '')
|
||||||
|
# 0.29 - It seems that the ideas about when multibyte trailing characters were
|
||||||
|
# included in the encryption were wrong. They are for DOC compressed
|
||||||
|
# files, but they are not for HUFF/CDIC compress files!
|
||||||
|
# 0.30 - Modified interface slightly to work better with new calibre plugin style
|
||||||
|
# 0.31 - The multibyte encrytion info is true for version 7 files too.
|
||||||
|
|
||||||
__version__ = '0.22'
|
__version__ = '0.31'
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
@@ -57,6 +68,7 @@ class Unbuffered:
|
|||||||
return getattr(self.stream, attr)
|
return getattr(self.stream, attr)
|
||||||
sys.stdout=Unbuffered(sys.stdout)
|
sys.stdout=Unbuffered(sys.stdout)
|
||||||
|
|
||||||
|
import os
|
||||||
import struct
|
import struct
|
||||||
import binascii
|
import binascii
|
||||||
|
|
||||||
@@ -153,9 +165,12 @@ class MobiBook:
|
|||||||
def __init__(self, infile):
|
def __init__(self, infile):
|
||||||
# initial sanity check on file
|
# initial sanity check on file
|
||||||
self.data_file = file(infile, 'rb').read()
|
self.data_file = file(infile, 'rb').read()
|
||||||
|
self.mobi_data = ''
|
||||||
self.header = self.data_file[0:78]
|
self.header = self.data_file[0:78]
|
||||||
if self.header[0x3C:0x3C+8] != 'BOOKMOBI':
|
if self.header[0x3C:0x3C+8] != 'BOOKMOBI' and self.header[0x3C:0x3C+8] != 'TEXtREAd':
|
||||||
raise DrmException("invalid file format")
|
raise DrmException("invalid file format")
|
||||||
|
self.magic = self.header[0x3C:0x3C+8]
|
||||||
|
self.crypto_type = -1
|
||||||
|
|
||||||
# build up section offset and flag info
|
# build up section offset and flag info
|
||||||
self.num_sections, = struct.unpack('>H', self.header[76:78])
|
self.num_sections, = struct.unpack('>H', self.header[76:78])
|
||||||
@@ -168,6 +183,15 @@ class MobiBook:
|
|||||||
# parse information from section 0
|
# parse information from section 0
|
||||||
self.sect = self.loadSection(0)
|
self.sect = self.loadSection(0)
|
||||||
self.records, = struct.unpack('>H', self.sect[0x8:0x8+2])
|
self.records, = struct.unpack('>H', self.sect[0x8:0x8+2])
|
||||||
|
self.compression, = struct.unpack('>H', self.sect[0x0:0x0+2])
|
||||||
|
|
||||||
|
if self.magic == 'TEXtREAd':
|
||||||
|
print "Book has format: ", self.magic
|
||||||
|
self.extra_data_flags = 0
|
||||||
|
self.mobi_length = 0
|
||||||
|
self.mobi_version = -1
|
||||||
|
self.meta_array = {}
|
||||||
|
return
|
||||||
self.mobi_length, = struct.unpack('>L',self.sect[0x14:0x18])
|
self.mobi_length, = struct.unpack('>L',self.sect[0x14:0x18])
|
||||||
self.mobi_version, = struct.unpack('>L',self.sect[0x68:0x6C])
|
self.mobi_version, = struct.unpack('>L',self.sect[0x68:0x6C])
|
||||||
print "MOBI header version = %d, length = %d" %(self.mobi_version, self.mobi_length)
|
print "MOBI header version = %d, length = %d" %(self.mobi_version, self.mobi_length)
|
||||||
@@ -175,24 +199,37 @@ class MobiBook:
|
|||||||
if (self.mobi_length >= 0xE4) and (self.mobi_version >= 5):
|
if (self.mobi_length >= 0xE4) and (self.mobi_version >= 5):
|
||||||
self.extra_data_flags, = struct.unpack('>H', self.sect[0xF2:0xF4])
|
self.extra_data_flags, = struct.unpack('>H', self.sect[0xF2:0xF4])
|
||||||
print "Extra Data Flags = %d" % self.extra_data_flags
|
print "Extra Data Flags = %d" % self.extra_data_flags
|
||||||
if self.mobi_version < 7:
|
if (self.compression != 17480):
|
||||||
# multibyte utf8 data is included in the encryption for mobi_version 6 and below
|
# multibyte utf8 data is included in the encryption for PalmDoc compression
|
||||||
# so clear that byte so that we leave it to be decrypted.
|
# so clear that byte so that we leave it to be decrypted.
|
||||||
self.extra_data_flags &= 0xFFFE
|
self.extra_data_flags &= 0xFFFE
|
||||||
|
|
||||||
# if exth region exists parse it for metadata array
|
# if exth region exists parse it for metadata array
|
||||||
self.meta_array = {}
|
self.meta_array = {}
|
||||||
|
try:
|
||||||
exth_flag, = struct.unpack('>L', self.sect[0x80:0x84])
|
exth_flag, = struct.unpack('>L', self.sect[0x80:0x84])
|
||||||
exth = ''
|
exth = 'NONE'
|
||||||
if exth_flag & 0x40:
|
if exth_flag & 0x40:
|
||||||
exth = self.sect[16 + self.mobi_length:]
|
exth = self.sect[16 + self.mobi_length:]
|
||||||
|
if (len(exth) >= 4) and (exth[:4] == 'EXTH'):
|
||||||
nitems, = struct.unpack('>I', exth[8:12])
|
nitems, = struct.unpack('>I', exth[8:12])
|
||||||
pos = 12
|
pos = 12
|
||||||
for i in xrange(nitems):
|
for i in xrange(nitems):
|
||||||
type, size = struct.unpack('>II', exth[pos: pos + 8])
|
type, size = struct.unpack('>II', exth[pos: pos + 8])
|
||||||
content = exth[pos + 8: pos + size]
|
content = exth[pos + 8: pos + size]
|
||||||
self.meta_array[type] = content
|
self.meta_array[type] = content
|
||||||
|
# reset the text to speech flag and clipping limit, if present
|
||||||
|
if type == 401 and size == 9:
|
||||||
|
# set clipping limit to 100%
|
||||||
|
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)
|
||||||
|
# print type, size, content, content.encode('hex')
|
||||||
pos += size
|
pos += size
|
||||||
|
except:
|
||||||
|
self.meta_array = {}
|
||||||
|
pass
|
||||||
|
|
||||||
def getBookTitle(self):
|
def getBookTitle(self):
|
||||||
title = ''
|
title = ''
|
||||||
@@ -208,18 +245,18 @@ class MobiBook:
|
|||||||
return title
|
return title
|
||||||
|
|
||||||
def getPIDMetaInfo(self):
|
def getPIDMetaInfo(self):
|
||||||
rec209 = None
|
rec209 = ''
|
||||||
token = None
|
token = ''
|
||||||
if 209 in self.meta_array:
|
if 209 in self.meta_array:
|
||||||
rec209 = self.meta_array[209]
|
rec209 = self.meta_array[209]
|
||||||
data = rec209
|
data = rec209
|
||||||
# Parse the 209 data to find the the exth record with the token data.
|
# The 209 data comes in five byte groups. Interpret the last four bytes
|
||||||
# The last character of the 209 data points to the record with the token.
|
# of each group as a big endian unsigned integer to get a key value
|
||||||
# Always 208 from my experience, but I'll leave the logic in case that changes.
|
# if that key exists in the meta_array, append its contents to the token
|
||||||
for i in xrange(len(data)):
|
for i in xrange(0,len(data),5):
|
||||||
if ord(data[i]) != 0:
|
val, = struct.unpack('>I',data[i+1:i+5])
|
||||||
if self.meta_array[ord(data[i])] != None:
|
sval = self.meta_array.get(val,'')
|
||||||
token = self.meta_array[ord(data[i])]
|
token += sval
|
||||||
return rec209, token
|
return rec209, token
|
||||||
|
|
||||||
def patch(self, off, new):
|
def patch(self, off, new):
|
||||||
@@ -267,14 +304,18 @@ class MobiBook:
|
|||||||
break
|
break
|
||||||
return [found_key,pid]
|
return [found_key,pid]
|
||||||
|
|
||||||
|
def getMobiFile(self, outpath):
|
||||||
|
file(outpath,'wb').write(self.mobi_data)
|
||||||
|
|
||||||
def processBook(self, pidlist):
|
def processBook(self, pidlist):
|
||||||
crypto_type, = struct.unpack('>H', self.sect[0xC:0xC+2])
|
crypto_type, = struct.unpack('>H', self.sect[0xC:0xC+2])
|
||||||
|
print 'Crypto Type is: ', crypto_type
|
||||||
|
self.crypto_type = crypto_type
|
||||||
if crypto_type == 0:
|
if crypto_type == 0:
|
||||||
print "This book is not encrypted."
|
print "This book is not encrypted."
|
||||||
return self.data_file
|
self.mobi_data = self.data_file
|
||||||
if crypto_type == 1:
|
return
|
||||||
raise DrmException("Cannot decode Mobipocket encryption type 1")
|
if crypto_type != 2 and crypto_type != 1:
|
||||||
if crypto_type != 2:
|
|
||||||
raise DrmException("Cannot decode unknown Mobipocket encryption type %d" % crypto_type)
|
raise DrmException("Cannot decode unknown Mobipocket encryption type %d" % crypto_type)
|
||||||
|
|
||||||
goodpids = []
|
goodpids = []
|
||||||
@@ -286,6 +327,17 @@ class MobiBook:
|
|||||||
elif len(pid)==8:
|
elif len(pid)==8:
|
||||||
goodpids.append(pid)
|
goodpids.append(pid)
|
||||||
|
|
||||||
|
if self.crypto_type == 1:
|
||||||
|
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"
|
||||||
|
found_key = PC1(t1_keyvec, bookkey_data)
|
||||||
|
else :
|
||||||
# calculate the keys
|
# calculate the keys
|
||||||
drm_ptr, drm_count, drm_size, drm_flags = struct.unpack('>LLLL', self.sect[0xA8:0xA8+16])
|
drm_ptr, drm_count, drm_size, drm_flags = struct.unpack('>LLLL', self.sect[0xA8:0xA8+16])
|
||||||
if drm_count == 0:
|
if drm_count == 0:
|
||||||
@@ -293,61 +345,66 @@ class MobiBook:
|
|||||||
found_key, pid = self.parseDRM(self.sect[drm_ptr:drm_ptr+drm_size], drm_count, goodpids)
|
found_key, pid = self.parseDRM(self.sect[drm_ptr:drm_ptr+drm_size], drm_count, goodpids)
|
||||||
if not found_key:
|
if not found_key:
|
||||||
raise DrmException("No key found. Most likely the correct PID has not been given.")
|
raise DrmException("No key found. Most likely the correct PID has not been given.")
|
||||||
|
# kill the drm keys
|
||||||
|
self.patchSection(0, "\0" * drm_size, drm_ptr)
|
||||||
|
# kill the drm pointers
|
||||||
|
self.patchSection(0, "\xff" * 4 + "\0" * 12, 0xA8)
|
||||||
|
|
||||||
if pid=="00000000":
|
if pid=="00000000":
|
||||||
print "File has default encryption, no specific PID."
|
print "File has default encryption, no specific PID."
|
||||||
else:
|
else:
|
||||||
print "File is encoded with PID "+checksumPid(pid)+"."
|
print "File is encoded with PID "+checksumPid(pid)+"."
|
||||||
|
|
||||||
# kill the drm keys
|
|
||||||
self.patchSection(0, "\0" * drm_size, drm_ptr)
|
|
||||||
# kill the drm pointers
|
|
||||||
self.patchSection(0, "\xff" * 4 + "\0" * 12, 0xA8)
|
|
||||||
# clear the crypto type
|
# clear the crypto type
|
||||||
self.patchSection(0, "\0" * 2, 0xC)
|
self.patchSection(0, "\0" * 2, 0xC)
|
||||||
|
|
||||||
# decrypt sections
|
# decrypt sections
|
||||||
print "Decrypting. Please wait . . .",
|
print "Decrypting. Please wait . . .",
|
||||||
new_data = self.data_file[:self.sections[1][0]]
|
self.mobi_data = self.data_file[:self.sections[1][0]]
|
||||||
for i in xrange(1, self.records+1):
|
for i in xrange(1, self.records+1):
|
||||||
data = self.loadSection(i)
|
data = self.loadSection(i)
|
||||||
extra_size = getSizeOfTrailingDataEntries(data, len(data), self.extra_data_flags)
|
extra_size = getSizeOfTrailingDataEntries(data, len(data), self.extra_data_flags)
|
||||||
if i%100 == 0:
|
if i%100 == 0:
|
||||||
print ".",
|
print ".",
|
||||||
# print "record %d, extra_size %d" %(i,extra_size)
|
# print "record %d, extra_size %d" %(i,extra_size)
|
||||||
new_data += PC1(found_key, data[0:len(data) - extra_size])
|
self.mobi_data += PC1(found_key, data[0:len(data) - extra_size])
|
||||||
if extra_size > 0:
|
if extra_size > 0:
|
||||||
new_data += data[-extra_size:]
|
self.mobi_data += data[-extra_size:]
|
||||||
if self.num_sections > self.records+1:
|
if self.num_sections > self.records+1:
|
||||||
new_data += self.data_file[self.sections[self.records+1][0]:]
|
self.mobi_data += self.data_file[self.sections[self.records+1][0]:]
|
||||||
self.data_file = new_data
|
|
||||||
print "done"
|
print "done"
|
||||||
return self.data_file
|
return
|
||||||
|
|
||||||
def getUnencryptedBook(infile,pid):
|
def getUnencryptedBook(infile,pid):
|
||||||
if not os.path.isfile(infile):
|
if not os.path.isfile(infile):
|
||||||
raise DrmException('Input File Not Found')
|
raise DrmException('Input File Not Found')
|
||||||
book = MobiBook(infile)
|
book = MobiBook(infile)
|
||||||
return book.processBook([pid])
|
book.processBook([pid])
|
||||||
|
return book.mobi_data
|
||||||
|
|
||||||
def getUnencryptedBookWithList(infile,pidlist):
|
def getUnencryptedBookWithList(infile,pidlist):
|
||||||
if not os.path.isfile(infile):
|
if not os.path.isfile(infile):
|
||||||
raise DrmException('Input File Not Found')
|
raise DrmException('Input File Not Found')
|
||||||
book = MobiBook(infile)
|
book = MobiBook(infile)
|
||||||
return book.processBook(pidlist)
|
book.processBook(pidlist)
|
||||||
|
return book.mobi_data
|
||||||
|
|
||||||
|
|
||||||
def main(argv=sys.argv):
|
def main(argv=sys.argv):
|
||||||
print ('MobiDeDrm v%(__version__)s. '
|
print ('MobiDeDrm v%(__version__)s. '
|
||||||
'Copyright 2008-2010 The Dark Reverser.' % globals())
|
'Copyright 2008-2010 The Dark Reverser.' % globals())
|
||||||
if len(argv)<4:
|
if len(argv)<3 or len(argv)>4:
|
||||||
print "Removes protection from Mobipocket books"
|
print "Removes protection from Mobipocket books"
|
||||||
print "Usage:"
|
print "Usage:"
|
||||||
print " %s <infile> <outfile> <Comma separated list of PIDs to try>" % sys.argv[0]
|
print " %s <infile> <outfile> [<Comma separated list of PIDs to try>]" % sys.argv[0]
|
||||||
return 1
|
return 1
|
||||||
else:
|
else:
|
||||||
infile = argv[1]
|
infile = argv[1]
|
||||||
outfile = argv[2]
|
outfile = argv[2]
|
||||||
|
if len(argv) is 4:
|
||||||
pidlist = argv[3].split(',')
|
pidlist = argv[3].split(',')
|
||||||
|
else:
|
||||||
|
pidlist = {}
|
||||||
try:
|
try:
|
||||||
stripped_file = getUnencryptedBookWithList(infile, pidlist)
|
stripped_file = getUnencryptedBookWithList(infile, pidlist)
|
||||||
file(outfile, 'wb').write(stripped_file)
|
file(outfile, 'wb').write(stripped_file)
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
|
||||||
|
|
||||||
|
# implement just enough of des from openssl to make erdr2pml.py happy
|
||||||
|
|
||||||
|
def load_libcrypto():
|
||||||
|
from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_char, c_int, c_long, \
|
||||||
|
Structure, c_ulong, create_string_buffer, cast
|
||||||
|
from ctypes.util import find_library
|
||||||
|
import sys
|
||||||
|
|
||||||
|
if sys.platform.startswith('win'):
|
||||||
|
libcrypto = find_library('libeay32')
|
||||||
|
else:
|
||||||
|
libcrypto = find_library('crypto')
|
||||||
|
|
||||||
|
if libcrypto is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
libcrypto = CDLL(libcrypto)
|
||||||
|
|
||||||
|
# typedef struct DES_ks
|
||||||
|
# {
|
||||||
|
# union
|
||||||
|
# {
|
||||||
|
# DES_cblock cblock;
|
||||||
|
# /* make sure things are correct size on machines with
|
||||||
|
# * 8 byte longs */
|
||||||
|
# DES_LONG deslong[2];
|
||||||
|
# } ks[16];
|
||||||
|
# } DES_key_schedule;
|
||||||
|
|
||||||
|
# just create a big enough place to hold everything
|
||||||
|
# it will have alignment of structure so we should be okay (16 byte aligned?)
|
||||||
|
class DES_KEY_SCHEDULE(Structure):
|
||||||
|
_fields_ = [('DES_cblock1', c_char * 16),
|
||||||
|
('DES_cblock2', c_char * 16),
|
||||||
|
('DES_cblock3', c_char * 16),
|
||||||
|
('DES_cblock4', c_char * 16),
|
||||||
|
('DES_cblock5', c_char * 16),
|
||||||
|
('DES_cblock6', c_char * 16),
|
||||||
|
('DES_cblock7', c_char * 16),
|
||||||
|
('DES_cblock8', c_char * 16),
|
||||||
|
('DES_cblock9', c_char * 16),
|
||||||
|
('DES_cblock10', c_char * 16),
|
||||||
|
('DES_cblock11', c_char * 16),
|
||||||
|
('DES_cblock12', c_char * 16),
|
||||||
|
('DES_cblock13', c_char * 16),
|
||||||
|
('DES_cblock14', c_char * 16),
|
||||||
|
('DES_cblock15', c_char * 16),
|
||||||
|
('DES_cblock16', c_char * 16)]
|
||||||
|
|
||||||
|
DES_KEY_SCHEDULE_p = POINTER(DES_KEY_SCHEDULE)
|
||||||
|
|
||||||
|
def F(restype, name, argtypes):
|
||||||
|
func = getattr(libcrypto, name)
|
||||||
|
func.restype = restype
|
||||||
|
func.argtypes = argtypes
|
||||||
|
return func
|
||||||
|
|
||||||
|
DES_set_key = F(None, 'DES_set_key',[c_char_p, DES_KEY_SCHEDULE_p])
|
||||||
|
DES_ecb_encrypt = F(None, 'DES_ecb_encrypt',[c_char_p, c_char_p, DES_KEY_SCHEDULE_p, c_int])
|
||||||
|
|
||||||
|
|
||||||
|
class DES(object):
|
||||||
|
def __init__(self, key):
|
||||||
|
if len(key) != 8 :
|
||||||
|
raise Error('DES improper key used')
|
||||||
|
return
|
||||||
|
self.key = key
|
||||||
|
self.keyschedule = DES_KEY_SCHEDULE()
|
||||||
|
DES_set_key(self.key, self.keyschedule)
|
||||||
|
def desdecrypt(self, data):
|
||||||
|
ob = create_string_buffer(len(data))
|
||||||
|
DES_ecb_encrypt(data, ob, self.keyschedule, 0)
|
||||||
|
return ob.raw
|
||||||
|
def decrypt(self, data):
|
||||||
|
if not data:
|
||||||
|
return ''
|
||||||
|
i = 0
|
||||||
|
result = []
|
||||||
|
while i < len(data):
|
||||||
|
block = data[i:i+8]
|
||||||
|
processed_block = self.desdecrypt(block)
|
||||||
|
result.append(processed_block)
|
||||||
|
i += 8
|
||||||
|
return ''.join(result)
|
||||||
|
|
||||||
|
return DES
|
||||||
|
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
|
||||||
|
|
||||||
|
|
||||||
|
def load_pycrypto():
|
||||||
|
try :
|
||||||
|
from Crypto.Cipher import DES as _DES
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
|
class DES(object):
|
||||||
|
def __init__(self, key):
|
||||||
|
if len(key) != 8 :
|
||||||
|
raise Error('DES improper key used')
|
||||||
|
self.key = key
|
||||||
|
self._des = _DES.new(key,_DES.MODE_ECB)
|
||||||
|
def desdecrypt(self, data):
|
||||||
|
return self._des.decrypt(data)
|
||||||
|
def decrypt(self, data):
|
||||||
|
if not data:
|
||||||
|
return ''
|
||||||
|
i = 0
|
||||||
|
result = []
|
||||||
|
while i < len(data):
|
||||||
|
block = data[i:i+8]
|
||||||
|
processed_block = self.desdecrypt(block)
|
||||||
|
result.append(processed_block)
|
||||||
|
i += 8
|
||||||
|
return ''.join(result)
|
||||||
|
return DES
|
||||||
|
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
|
||||||
|
import sys
|
||||||
|
|
||||||
|
ECB = 0
|
||||||
|
CBC = 1
|
||||||
|
class Des(object):
|
||||||
|
__pc1 = [56, 48, 40, 32, 24, 16, 8, 0, 57, 49, 41, 33, 25, 17,
|
||||||
|
9, 1, 58, 50, 42, 34, 26, 18, 10, 2, 59, 51, 43, 35,
|
||||||
|
62, 54, 46, 38, 30, 22, 14, 6, 61, 53, 45, 37, 29, 21,
|
||||||
|
13, 5, 60, 52, 44, 36, 28, 20, 12, 4, 27, 19, 11, 3]
|
||||||
|
__left_rotations = [1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1]
|
||||||
|
__pc2 = [13, 16, 10, 23, 0, 4,2, 27, 14, 5, 20, 9,
|
||||||
|
22, 18, 11, 3, 25, 7, 15, 6, 26, 19, 12, 1,
|
||||||
|
40, 51, 30, 36, 46, 54, 29, 39, 50, 44, 32, 47,
|
||||||
|
43, 48, 38, 55, 33, 52, 45, 41, 49, 35, 28, 31]
|
||||||
|
__ip = [57, 49, 41, 33, 25, 17, 9, 1, 59, 51, 43, 35, 27, 19, 11, 3,
|
||||||
|
61, 53, 45, 37, 29, 21, 13, 5, 63, 55, 47, 39, 31, 23, 15, 7,
|
||||||
|
56, 48, 40, 32, 24, 16, 8, 0, 58, 50, 42, 34, 26, 18, 10, 2,
|
||||||
|
60, 52, 44, 36, 28, 20, 12, 4, 62, 54, 46, 38, 30, 22, 14, 6]
|
||||||
|
__expansion_table = [31, 0, 1, 2, 3, 4, 3, 4, 5, 6, 7, 8,
|
||||||
|
7, 8, 9, 10, 11, 12,11, 12, 13, 14, 15, 16,
|
||||||
|
15, 16, 17, 18, 19, 20,19, 20, 21, 22, 23, 24,
|
||||||
|
23, 24, 25, 26, 27, 28,27, 28, 29, 30, 31, 0]
|
||||||
|
__sbox = [[14, 4, 13, 1, 2, 15, 11, 8, 3, 10, 6, 12, 5, 9, 0, 7,
|
||||||
|
0, 15, 7, 4, 14, 2, 13, 1, 10, 6, 12, 11, 9, 5, 3, 8,
|
||||||
|
4, 1, 14, 8, 13, 6, 2, 11, 15, 12, 9, 7, 3, 10, 5, 0,
|
||||||
|
15, 12, 8, 2, 4, 9, 1, 7, 5, 11, 3, 14, 10, 0, 6, 13],
|
||||||
|
[15, 1, 8, 14, 6, 11, 3, 4, 9, 7, 2, 13, 12, 0, 5, 10,
|
||||||
|
3, 13, 4, 7, 15, 2, 8, 14, 12, 0, 1, 10, 6, 9, 11, 5,
|
||||||
|
0, 14, 7, 11, 10, 4, 13, 1, 5, 8, 12, 6, 9, 3, 2, 15,
|
||||||
|
13, 8, 10, 1, 3, 15, 4, 2, 11, 6, 7, 12, 0, 5, 14, 9],
|
||||||
|
[10, 0, 9, 14, 6, 3, 15, 5, 1, 13, 12, 7, 11, 4, 2, 8,
|
||||||
|
13, 7, 0, 9, 3, 4, 6, 10, 2, 8, 5, 14, 12, 11, 15, 1,
|
||||||
|
13, 6, 4, 9, 8, 15, 3, 0, 11, 1, 2, 12, 5, 10, 14, 7,
|
||||||
|
1, 10, 13, 0, 6, 9, 8, 7, 4, 15, 14, 3, 11, 5, 2, 12],
|
||||||
|
[7, 13, 14, 3, 0, 6, 9, 10, 1, 2, 8, 5, 11, 12, 4, 15,
|
||||||
|
13, 8, 11, 5, 6, 15, 0, 3, 4, 7, 2, 12, 1, 10, 14, 9,
|
||||||
|
10, 6, 9, 0, 12, 11, 7, 13, 15, 1, 3, 14, 5, 2, 8, 4,
|
||||||
|
3, 15, 0, 6, 10, 1, 13, 8, 9, 4, 5, 11, 12, 7, 2, 14],
|
||||||
|
[2, 12, 4, 1, 7, 10, 11, 6, 8, 5, 3, 15, 13, 0, 14, 9,
|
||||||
|
14, 11, 2, 12, 4, 7, 13, 1, 5, 0, 15, 10, 3, 9, 8, 6,
|
||||||
|
4, 2, 1, 11, 10, 13, 7, 8, 15, 9, 12, 5, 6, 3, 0, 14,
|
||||||
|
11, 8, 12, 7, 1, 14, 2, 13, 6, 15, 0, 9, 10, 4, 5, 3],
|
||||||
|
[12, 1, 10, 15, 9, 2, 6, 8, 0, 13, 3, 4, 14, 7, 5, 11,
|
||||||
|
10, 15, 4, 2, 7, 12, 9, 5, 6, 1, 13, 14, 0, 11, 3, 8,
|
||||||
|
9, 14, 15, 5, 2, 8, 12, 3, 7, 0, 4, 10, 1, 13, 11, 6,
|
||||||
|
4, 3, 2, 12, 9, 5, 15, 10, 11, 14, 1, 7, 6, 0, 8, 13],
|
||||||
|
[4, 11, 2, 14, 15, 0, 8, 13, 3, 12, 9, 7, 5, 10, 6, 1,
|
||||||
|
13, 0, 11, 7, 4, 9, 1, 10, 14, 3, 5, 12, 2, 15, 8, 6,
|
||||||
|
1, 4, 11, 13, 12, 3, 7, 14, 10, 15, 6, 8, 0, 5, 9, 2,
|
||||||
|
6, 11, 13, 8, 1, 4, 10, 7, 9, 5, 0, 15, 14, 2, 3, 12],
|
||||||
|
[13, 2, 8, 4, 6, 15, 11, 1, 10, 9, 3, 14, 5, 0, 12, 7,
|
||||||
|
1, 15, 13, 8, 10, 3, 7, 4, 12, 5, 6, 11, 0, 14, 9, 2,
|
||||||
|
7, 11, 4, 1, 9, 12, 14, 2, 0, 6, 10, 13, 15, 3, 5, 8,
|
||||||
|
2, 1, 14, 7, 4, 10, 8, 13, 15, 12, 9, 0, 3, 5, 6, 11],]
|
||||||
|
__p = [15, 6, 19, 20, 28, 11,27, 16, 0, 14, 22, 25,
|
||||||
|
4, 17, 30, 9, 1, 7,23,13, 31, 26, 2, 8,18, 12, 29, 5, 21, 10,3, 24]
|
||||||
|
__fp = [39, 7, 47, 15, 55, 23, 63, 31,38, 6, 46, 14, 54, 22, 62, 30,
|
||||||
|
37, 5, 45, 13, 53, 21, 61, 29,36, 4, 44, 12, 52, 20, 60, 28,
|
||||||
|
35, 3, 43, 11, 51, 19, 59, 27,34, 2, 42, 10, 50, 18, 58, 26,
|
||||||
|
33, 1, 41, 9, 49, 17, 57, 25,32, 0, 40, 8, 48, 16, 56, 24]
|
||||||
|
# Type of crypting being done
|
||||||
|
ENCRYPT = 0x00
|
||||||
|
DECRYPT = 0x01
|
||||||
|
def __init__(self, key, mode=ECB, IV=None):
|
||||||
|
if len(key) != 8:
|
||||||
|
raise ValueError("Invalid DES key size. Key must be exactly 8 bytes long.")
|
||||||
|
self.block_size = 8
|
||||||
|
self.key_size = 8
|
||||||
|
self.__padding = ''
|
||||||
|
self.setMode(mode)
|
||||||
|
if IV:
|
||||||
|
self.setIV(IV)
|
||||||
|
self.L = []
|
||||||
|
self.R = []
|
||||||
|
self.Kn = [ [0] * 48 ] * 16 # 16 48-bit keys (K1 - K16)
|
||||||
|
self.final = []
|
||||||
|
self.setKey(key)
|
||||||
|
def getKey(self):
|
||||||
|
return self.__key
|
||||||
|
def setKey(self, key):
|
||||||
|
self.__key = key
|
||||||
|
self.__create_sub_keys()
|
||||||
|
def getMode(self):
|
||||||
|
return self.__mode
|
||||||
|
def setMode(self, mode):
|
||||||
|
self.__mode = mode
|
||||||
|
def getIV(self):
|
||||||
|
return self.__iv
|
||||||
|
def setIV(self, IV):
|
||||||
|
if not IV or len(IV) != self.block_size:
|
||||||
|
raise ValueError("Invalid Initial Value (IV), must be a multiple of " + str(self.block_size) + " bytes")
|
||||||
|
self.__iv = IV
|
||||||
|
def getPadding(self):
|
||||||
|
return self.__padding
|
||||||
|
def __String_to_BitList(self, data):
|
||||||
|
l = len(data) * 8
|
||||||
|
result = [0] * l
|
||||||
|
pos = 0
|
||||||
|
for c in data:
|
||||||
|
i = 7
|
||||||
|
ch = ord(c)
|
||||||
|
while i >= 0:
|
||||||
|
if ch & (1 << i) != 0:
|
||||||
|
result[pos] = 1
|
||||||
|
else:
|
||||||
|
result[pos] = 0
|
||||||
|
pos += 1
|
||||||
|
i -= 1
|
||||||
|
return result
|
||||||
|
def __BitList_to_String(self, data):
|
||||||
|
result = ''
|
||||||
|
pos = 0
|
||||||
|
c = 0
|
||||||
|
while pos < len(data):
|
||||||
|
c += data[pos] << (7 - (pos % 8))
|
||||||
|
if (pos % 8) == 7:
|
||||||
|
result += chr(c)
|
||||||
|
c = 0
|
||||||
|
pos += 1
|
||||||
|
return result
|
||||||
|
def __permutate(self, table, block):
|
||||||
|
return [block[x] for x in table]
|
||||||
|
def __create_sub_keys(self):
|
||||||
|
key = self.__permutate(Des.__pc1, self.__String_to_BitList(self.getKey()))
|
||||||
|
i = 0
|
||||||
|
self.L = key[:28]
|
||||||
|
self.R = key[28:]
|
||||||
|
while i < 16:
|
||||||
|
j = 0
|
||||||
|
while j < Des.__left_rotations[i]:
|
||||||
|
self.L.append(self.L[0])
|
||||||
|
del self.L[0]
|
||||||
|
self.R.append(self.R[0])
|
||||||
|
del self.R[0]
|
||||||
|
j += 1
|
||||||
|
self.Kn[i] = self.__permutate(Des.__pc2, self.L + self.R)
|
||||||
|
i += 1
|
||||||
|
def __des_crypt(self, block, crypt_type):
|
||||||
|
block = self.__permutate(Des.__ip, block)
|
||||||
|
self.L = block[:32]
|
||||||
|
self.R = block[32:]
|
||||||
|
if crypt_type == Des.ENCRYPT:
|
||||||
|
iteration = 0
|
||||||
|
iteration_adjustment = 1
|
||||||
|
else:
|
||||||
|
iteration = 15
|
||||||
|
iteration_adjustment = -1
|
||||||
|
i = 0
|
||||||
|
while i < 16:
|
||||||
|
tempR = self.R[:]
|
||||||
|
self.R = self.__permutate(Des.__expansion_table, self.R)
|
||||||
|
self.R = [x ^ y for x,y in zip(self.R, self.Kn[iteration])]
|
||||||
|
B = [self.R[:6], self.R[6:12], self.R[12:18], self.R[18:24], self.R[24:30], self.R[30:36], self.R[36:42], self.R[42:]]
|
||||||
|
j = 0
|
||||||
|
Bn = [0] * 32
|
||||||
|
pos = 0
|
||||||
|
while j < 8:
|
||||||
|
m = (B[j][0] << 1) + B[j][5]
|
||||||
|
n = (B[j][1] << 3) + (B[j][2] << 2) + (B[j][3] << 1) + B[j][4]
|
||||||
|
v = Des.__sbox[j][(m << 4) + n]
|
||||||
|
Bn[pos] = (v & 8) >> 3
|
||||||
|
Bn[pos + 1] = (v & 4) >> 2
|
||||||
|
Bn[pos + 2] = (v & 2) >> 1
|
||||||
|
Bn[pos + 3] = v & 1
|
||||||
|
pos += 4
|
||||||
|
j += 1
|
||||||
|
self.R = self.__permutate(Des.__p, Bn)
|
||||||
|
self.R = [x ^ y for x, y in zip(self.R, self.L)]
|
||||||
|
self.L = tempR
|
||||||
|
i += 1
|
||||||
|
iteration += iteration_adjustment
|
||||||
|
self.final = self.__permutate(Des.__fp, self.R + self.L)
|
||||||
|
return self.final
|
||||||
|
def crypt(self, data, crypt_type):
|
||||||
|
if not data:
|
||||||
|
return ''
|
||||||
|
if len(data) % self.block_size != 0:
|
||||||
|
if crypt_type == Des.DECRYPT: # Decryption must work on 8 byte blocks
|
||||||
|
raise ValueError("Invalid data length, data must be a multiple of " + str(self.block_size) + " bytes\n.")
|
||||||
|
if not self.getPadding():
|
||||||
|
raise ValueError("Invalid data length, data must be a multiple of " + str(self.block_size) + " bytes\n. Try setting the optional padding character")
|
||||||
|
else:
|
||||||
|
data += (self.block_size - (len(data) % self.block_size)) * self.getPadding()
|
||||||
|
if self.getMode() == CBC:
|
||||||
|
if self.getIV():
|
||||||
|
iv = self.__String_to_BitList(self.getIV())
|
||||||
|
else:
|
||||||
|
raise ValueError("For CBC mode, you must supply the Initial Value (IV) for ciphering")
|
||||||
|
i = 0
|
||||||
|
dict = {}
|
||||||
|
result = []
|
||||||
|
while i < len(data):
|
||||||
|
block = self.__String_to_BitList(data[i:i+8])
|
||||||
|
if self.getMode() == CBC:
|
||||||
|
if crypt_type == Des.ENCRYPT:
|
||||||
|
block = [x ^ y for x, y in zip(block, iv)]
|
||||||
|
processed_block = self.__des_crypt(block, crypt_type)
|
||||||
|
if crypt_type == Des.DECRYPT:
|
||||||
|
processed_block = [x ^ y for x, y in zip(processed_block, iv)]
|
||||||
|
iv = block
|
||||||
|
else:
|
||||||
|
iv = processed_block
|
||||||
|
else:
|
||||||
|
processed_block = self.__des_crypt(block, crypt_type)
|
||||||
|
result.append(self.__BitList_to_String(processed_block))
|
||||||
|
i += 8
|
||||||
|
if crypt_type == Des.DECRYPT and self.getPadding():
|
||||||
|
s = result[-1]
|
||||||
|
while s[-1] == self.getPadding():
|
||||||
|
s = s[:-1]
|
||||||
|
result[-1] = s
|
||||||
|
return ''.join(result)
|
||||||
|
def encrypt(self, data, pad=''):
|
||||||
|
self.__padding = pad
|
||||||
|
return self.crypt(data, Des.ENCRYPT)
|
||||||
|
def decrypt(self, data, pad=''):
|
||||||
|
self.__padding = pad
|
||||||
|
return self.crypt(data, Des.DECRYPT)
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os, os.path
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
class SimplePrefsError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class SimplePrefs(object):
|
||||||
|
def __init__(self, target, description):
|
||||||
|
self.prefs = {}
|
||||||
|
self.key2file={}
|
||||||
|
self.file2key={}
|
||||||
|
for keyfilemap in description:
|
||||||
|
[key, filename] = keyfilemap
|
||||||
|
self.key2file[key] = filename
|
||||||
|
self.file2key[filename] = key
|
||||||
|
self.target = target + 'Prefs'
|
||||||
|
if sys.platform.startswith('win'):
|
||||||
|
import _winreg as winreg
|
||||||
|
regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\")
|
||||||
|
path = winreg.QueryValueEx(regkey, 'Local AppData')[0]
|
||||||
|
prefdir = path + os.sep + self.target
|
||||||
|
elif sys.platform.startswith('darwin'):
|
||||||
|
home = os.getenv('HOME')
|
||||||
|
prefdir = os.path.join(home,'Library','Preferences','org.' + self.target)
|
||||||
|
else:
|
||||||
|
# linux and various flavors of unix
|
||||||
|
home = os.getenv('HOME')
|
||||||
|
prefdir = os.path.join(home,'.' + self.target)
|
||||||
|
if not os.path.exists(prefdir):
|
||||||
|
os.makedirs(prefdir)
|
||||||
|
self.prefdir = prefdir
|
||||||
|
self.prefs['dir'] = self.prefdir
|
||||||
|
self._loadPreferences()
|
||||||
|
|
||||||
|
def _loadPreferences(self):
|
||||||
|
filenames = os.listdir(self.prefdir)
|
||||||
|
for filename in filenames:
|
||||||
|
if filename in self.file2key:
|
||||||
|
key = self.file2key[filename]
|
||||||
|
filepath = os.path.join(self.prefdir,filename)
|
||||||
|
if os.path.isfile(filepath):
|
||||||
|
try :
|
||||||
|
data = file(filepath,'rb').read()
|
||||||
|
self.prefs[key] = data
|
||||||
|
except Exception, e:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def getPreferences(self):
|
||||||
|
return self.prefs
|
||||||
|
|
||||||
|
def setPreferences(self, newprefs={}):
|
||||||
|
if 'dir' not in newprefs:
|
||||||
|
raise SimplePrefsError('Error: Attempt to Set Preferences in unspecified directory')
|
||||||
|
if newprefs['dir'] != self.prefs['dir']:
|
||||||
|
raise SimplePrefsError('Error: Attempt to Set Preferences in unspecified directory')
|
||||||
|
for key in newprefs:
|
||||||
|
if key != 'dir':
|
||||||
|
if key in self.key2file:
|
||||||
|
filename = self.key2file[key]
|
||||||
|
filepath = os.path.join(self.prefdir,filename)
|
||||||
|
data = newprefs[key]
|
||||||
|
if data != None:
|
||||||
|
data = str(data)
|
||||||
|
if data == None or data == '':
|
||||||
|
if os.path.exists(filepath):
|
||||||
|
os.remove(filepath)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
file(filepath,'wb').write(data)
|
||||||
|
except Exception, e:
|
||||||
|
pass
|
||||||
|
self.prefs = newprefs
|
||||||
|
return
|
||||||
|
|
||||||
@@ -10,7 +10,12 @@ class Unbuffered:
|
|||||||
return getattr(self.stream, attr)
|
return getattr(self.stream, attr)
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
sys.stdout=Unbuffered(sys.stdout)
|
|
||||||
|
if 'calibre' in sys.modules:
|
||||||
|
inCalibre = True
|
||||||
|
else:
|
||||||
|
inCalibre = False
|
||||||
|
|
||||||
import os, csv, getopt
|
import os, csv, getopt
|
||||||
import zlib, zipfile, tempfile, shutil
|
import zlib, zipfile, tempfile, shutil
|
||||||
from struct import pack
|
from struct import pack
|
||||||
@@ -19,9 +24,31 @@ from struct import unpack
|
|||||||
class TpzDRMError(Exception):
|
class TpzDRMError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
# local support routines
|
# local support routines
|
||||||
|
if inCalibre:
|
||||||
|
from calibre_plugins.k4mobidedrm import kgenpids
|
||||||
|
from calibre_plugins.k4mobidedrm import genbook
|
||||||
|
else:
|
||||||
import kgenpids
|
import kgenpids
|
||||||
import genbook
|
import genbook
|
||||||
|
|
||||||
|
|
||||||
|
# recursive zip creation support routine
|
||||||
|
def zipUpDir(myzip, tdir, localname):
|
||||||
|
currentdir = tdir
|
||||||
|
if localname != "":
|
||||||
|
currentdir = os.path.join(currentdir,localname)
|
||||||
|
list = os.listdir(currentdir)
|
||||||
|
for file in list:
|
||||||
|
afilename = file
|
||||||
|
localfilePath = os.path.join(localname, afilename)
|
||||||
|
realfilePath = os.path.join(currentdir,file)
|
||||||
|
if os.path.isfile(realfilePath):
|
||||||
|
myzip.write(realfilePath, localfilePath)
|
||||||
|
elif os.path.isdir(realfilePath):
|
||||||
|
zipUpDir(myzip, tdir, localfilePath)
|
||||||
|
|
||||||
#
|
#
|
||||||
# Utility routines
|
# Utility routines
|
||||||
#
|
#
|
||||||
@@ -110,9 +137,9 @@ def decryptDkeyRecords(data,PID):
|
|||||||
|
|
||||||
|
|
||||||
class TopazBook:
|
class TopazBook:
|
||||||
def __init__(self, filename, outdir):
|
def __init__(self, filename):
|
||||||
self.fo = file(filename, 'rb')
|
self.fo = file(filename, 'rb')
|
||||||
self.outdir = outdir
|
self.outdir = tempfile.mkdtemp()
|
||||||
self.bookPayloadOffset = 0
|
self.bookPayloadOffset = 0
|
||||||
self.bookHeaderRecords = {}
|
self.bookHeaderRecords = {}
|
||||||
self.bookMetadata = {}
|
self.bookMetadata = {}
|
||||||
@@ -157,17 +184,22 @@ class TopazBook:
|
|||||||
raise TpzDRMError("Parse Error : Record Names Don't Match")
|
raise TpzDRMError("Parse Error : Record Names Don't Match")
|
||||||
flags = ord(self.fo.read(1))
|
flags = ord(self.fo.read(1))
|
||||||
nbRecords = ord(self.fo.read(1))
|
nbRecords = ord(self.fo.read(1))
|
||||||
|
# print nbRecords
|
||||||
for i in range (0,nbRecords) :
|
for i in range (0,nbRecords) :
|
||||||
record = [bookReadString(self.fo), bookReadString(self.fo)]
|
keyval = bookReadString(self.fo)
|
||||||
self.bookMetadata[record[0]] = record[1]
|
content = bookReadString(self.fo)
|
||||||
|
# print keyval
|
||||||
|
# print content
|
||||||
|
self.bookMetadata[keyval] = content
|
||||||
return self.bookMetadata
|
return self.bookMetadata
|
||||||
|
|
||||||
def getPIDMetaInfo(self):
|
def getPIDMetaInfo(self):
|
||||||
keysRecord = None
|
keysRecord = self.bookMetadata.get('keys','')
|
||||||
KeysRecordRecord = None
|
keysRecordRecord = ''
|
||||||
if 'keys' in self.bookMetadata:
|
if keysRecord != '':
|
||||||
keysRecord = self.bookMetadata['keys']
|
keylst = keysRecord.split(',')
|
||||||
keysRecordRecord = self.bookMetadata[keysRecord]
|
for keyval in keylst:
|
||||||
|
keysRecordRecord += self.bookMetadata.get(keyval,'')
|
||||||
return keysRecord, keysRecordRecord
|
return keysRecord, keysRecordRecord
|
||||||
|
|
||||||
def getBookTitle(self):
|
def getBookTitle(self):
|
||||||
@@ -312,21 +344,33 @@ class TopazBook:
|
|||||||
file(outputFile, 'wb').write(record)
|
file(outputFile, 'wb').write(record)
|
||||||
print " "
|
print " "
|
||||||
|
|
||||||
|
def getHTMLZip(self, zipname):
|
||||||
|
htmlzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||||
|
htmlzip.write(os.path.join(self.outdir,'book.html'),'book.html')
|
||||||
|
htmlzip.write(os.path.join(self.outdir,'book.opf'),'book.opf')
|
||||||
|
if os.path.isfile(os.path.join(self.outdir,'cover.jpg')):
|
||||||
|
htmlzip.write(os.path.join(self.outdir,'cover.jpg'),'cover.jpg')
|
||||||
|
htmlzip.write(os.path.join(self.outdir,'style.css'),'style.css')
|
||||||
|
zipUpDir(htmlzip, self.outdir, 'img')
|
||||||
|
htmlzip.close()
|
||||||
|
|
||||||
def zipUpDir(myzip, tempdir,localname):
|
def getSVGZip(self, zipname):
|
||||||
currentdir = tempdir
|
svgzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
||||||
if localname != "":
|
svgzip.write(os.path.join(self.outdir,'index_svg.xhtml'),'index_svg.xhtml')
|
||||||
currentdir = os.path.join(currentdir,localname)
|
zipUpDir(svgzip, self.outdir, 'svg')
|
||||||
list = os.listdir(currentdir)
|
zipUpDir(svgzip, self.outdir, 'img')
|
||||||
for file in list:
|
svgzip.close()
|
||||||
afilename = file
|
|
||||||
localfilePath = os.path.join(localname, afilename)
|
|
||||||
realfilePath = os.path.join(currentdir,file)
|
|
||||||
if os.path.isfile(realfilePath):
|
|
||||||
myzip.write(realfilePath, localfilePath)
|
|
||||||
elif os.path.isdir(realfilePath):
|
|
||||||
zipUpDir(myzip, tempdir, localfilePath)
|
|
||||||
|
|
||||||
|
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):
|
def usage(progname):
|
||||||
print "Removes DRM protection from Topaz ebooks and extract the contents"
|
print "Removes DRM protection from Topaz ebooks and extract the contents"
|
||||||
@@ -378,57 +422,46 @@ def main(argv=sys.argv):
|
|||||||
return 1
|
return 1
|
||||||
|
|
||||||
bookname = os.path.splitext(os.path.basename(infile))[0]
|
bookname = os.path.splitext(os.path.basename(infile))[0]
|
||||||
tempdir = tempfile.mkdtemp()
|
|
||||||
|
|
||||||
tb = TopazBook(infile, tempdir)
|
tb = TopazBook(infile)
|
||||||
title = tb.getBookTitle()
|
title = tb.getBookTitle()
|
||||||
print "Processing Book: ", title
|
print "Processing Book: ", title
|
||||||
keysRecord, keysRecordRecord = tb.getPIDMetaInfo()
|
keysRecord, keysRecordRecord = tb.getPIDMetaInfo()
|
||||||
pidlst = kgenpids.getPidList(keysRecord, keysRecordRecord, k4, pids, serials, kInfoFiles)
|
pidlst = kgenpids.getPidList(keysRecord, keysRecordRecord, k4, pids, serials, kInfoFiles)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
print "Decrypting Book"
|
||||||
tb.processBook(pidlst)
|
tb.processBook(pidlst)
|
||||||
except TpzDRMError, e:
|
|
||||||
print str(e)
|
|
||||||
print " Creating DeBug Full Zip Archive of Book"
|
|
||||||
zipname = os.path.join(outdir, bookname + '_debug' + '.zip')
|
|
||||||
myzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
|
||||||
zipUpDir(myzip, tempdir, '')
|
|
||||||
myzip.close()
|
|
||||||
return 1
|
|
||||||
|
|
||||||
print " Creating HTML ZIP Archive"
|
print " Creating HTML ZIP Archive"
|
||||||
zipname = os.path.join(outdir, bookname + '_nodrm' + '.zip')
|
zipname = os.path.join(outdir, bookname + '_nodrm' + '.htmlz')
|
||||||
myzip1 = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
tb.getHTMLZip(zipname)
|
||||||
myzip1.write(os.path.join(tempdir,'book.html'),'book.html')
|
|
||||||
myzip1.write(os.path.join(tempdir,'book.opf'),'book.opf')
|
|
||||||
if os.path.isfile(os.path.join(tempdir,'cover.jpg')):
|
|
||||||
myzip1.write(os.path.join(tempdir,'cover.jpg'),'cover.jpg')
|
|
||||||
myzip1.write(os.path.join(tempdir,'style.css'),'style.css')
|
|
||||||
zipUpDir(myzip1, tempdir, 'img')
|
|
||||||
myzip1.close()
|
|
||||||
|
|
||||||
print " Creating SVG ZIP Archive"
|
print " Creating SVG ZIP Archive"
|
||||||
zipname = os.path.join(outdir, bookname + '_SVG' + '.zip')
|
zipname = os.path.join(outdir, bookname + '_SVG' + '.htmlz')
|
||||||
myzip2 = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
tb.getSVGZip(zipname)
|
||||||
myzip2.write(os.path.join(tempdir,'index_svg.xhtml'),'index_svg.xhtml')
|
|
||||||
zipUpDir(myzip2, tempdir, 'svg')
|
|
||||||
zipUpDir(myzip2, tempdir, 'img')
|
|
||||||
myzip2.close()
|
|
||||||
|
|
||||||
print " Creating XML ZIP Archive"
|
print " Creating XML ZIP Archive"
|
||||||
zipname = os.path.join(outdir, bookname + '_XML' + '.zip')
|
zipname = os.path.join(outdir, bookname + '_XML' + '.zip')
|
||||||
myzip3 = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
|
tb.getXMLZip(zipname)
|
||||||
targetdir = os.path.join(tempdir,'xml')
|
|
||||||
zipUpDir(myzip3, targetdir, '')
|
|
||||||
zipUpDir(myzip3, tempdir, 'img')
|
|
||||||
myzip3.close()
|
|
||||||
|
|
||||||
shutil.rmtree(tempdir)
|
# removing internal temporary directory of pieces
|
||||||
|
tb.cleanup()
|
||||||
|
|
||||||
|
except TpzDRMError, e:
|
||||||
|
print str(e)
|
||||||
|
tb.cleanup()
|
||||||
|
return 1
|
||||||
|
|
||||||
|
except Exception, e:
|
||||||
|
print str(e)
|
||||||
|
tb.cleanup
|
||||||
|
return 1
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
sys.stdout=Unbuffered(sys.stdout)
|
||||||
sys.exit(main())
|
sys.exit(main())
|
||||||
|
|
||||||
156
DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/zipfix.py
Normal file
156
DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/zipfix.py
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import zlib
|
||||||
|
import zipfile
|
||||||
|
import os
|
||||||
|
import os.path
|
||||||
|
import getopt
|
||||||
|
from struct import unpack
|
||||||
|
|
||||||
|
|
||||||
|
_FILENAME_LEN_OFFSET = 26
|
||||||
|
_EXTRA_LEN_OFFSET = 28
|
||||||
|
_FILENAME_OFFSET = 30
|
||||||
|
_MAX_SIZE = 64 * 1024
|
||||||
|
_MIMETYPE = 'application/epub+zip'
|
||||||
|
|
||||||
|
class ZipInfo(zipfile.ZipInfo):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
if 'compress_type' in kwargs:
|
||||||
|
compress_type = kwargs.pop('compress_type')
|
||||||
|
super(ZipInfo, self).__init__(*args, **kwargs)
|
||||||
|
self.compress_type = compress_type
|
||||||
|
|
||||||
|
class fixZip:
|
||||||
|
def __init__(self, zinput, zoutput):
|
||||||
|
self.ztype = 'zip'
|
||||||
|
if zinput.lower().find('.epub') >= 0 :
|
||||||
|
self.ztype = 'epub'
|
||||||
|
self.inzip = zipfile.ZipFile(zinput,'r')
|
||||||
|
self.outzip = zipfile.ZipFile(zoutput,'w')
|
||||||
|
# open the input zip for reading only as a raw file
|
||||||
|
self.bzf = file(zinput,'rb')
|
||||||
|
|
||||||
|
def getlocalname(self, zi):
|
||||||
|
local_header_offset = zi.header_offset
|
||||||
|
self.bzf.seek(local_header_offset + _FILENAME_LEN_OFFSET)
|
||||||
|
leninfo = self.bzf.read(2)
|
||||||
|
local_name_length, = unpack('<H', leninfo)
|
||||||
|
self.bzf.seek(local_header_offset + _FILENAME_OFFSET)
|
||||||
|
local_name = self.bzf.read(local_name_length)
|
||||||
|
return local_name
|
||||||
|
|
||||||
|
def uncompress(self, cmpdata):
|
||||||
|
dc = zlib.decompressobj(-15)
|
||||||
|
data = ''
|
||||||
|
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(self, zi):
|
||||||
|
# get file name length and exta data length to find start of file data
|
||||||
|
local_header_offset = zi.header_offset
|
||||||
|
|
||||||
|
self.bzf.seek(local_header_offset + _FILENAME_LEN_OFFSET)
|
||||||
|
leninfo = self.bzf.read(2)
|
||||||
|
local_name_length, = unpack('<H', leninfo)
|
||||||
|
|
||||||
|
self.bzf.seek(local_header_offset + _EXTRA_LEN_OFFSET)
|
||||||
|
exinfo = self.bzf.read(2)
|
||||||
|
extra_field_length, = unpack('<H', exinfo)
|
||||||
|
|
||||||
|
self.bzf.seek(local_header_offset + _FILENAME_OFFSET + local_name_length + extra_field_length)
|
||||||
|
data = None
|
||||||
|
|
||||||
|
# if not compressed we are good to go
|
||||||
|
if zi.compress_type == zipfile.ZIP_STORED:
|
||||||
|
data = self.bzf.read(zi.file_size)
|
||||||
|
|
||||||
|
# if compressed we must decompress it using zlib
|
||||||
|
if zi.compress_type == zipfile.ZIP_DEFLATED:
|
||||||
|
cmpdata = self.bzf.read(zi.compress_size)
|
||||||
|
data = self.uncompress(cmpdata)
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def fix(self):
|
||||||
|
# get the zipinfo for each member of the input archive
|
||||||
|
# and copy member over to output archive
|
||||||
|
# if problems exist with local vs central filename, fix them
|
||||||
|
|
||||||
|
# if epub write mimetype file first, with no compression
|
||||||
|
if self.ztype == 'epub':
|
||||||
|
nzinfo = ZipInfo('mimetype', compress_type=zipfile.ZIP_STORED)
|
||||||
|
self.outzip.writestr(nzinfo, _MIMETYPE)
|
||||||
|
|
||||||
|
# write the rest of the files
|
||||||
|
for zinfo in self.inzip.infolist():
|
||||||
|
if zinfo.filename != "mimetype" or self.ztype == '.zip':
|
||||||
|
data = None
|
||||||
|
nzinfo = zinfo
|
||||||
|
try:
|
||||||
|
data = self.inzip.read(zinfo.filename)
|
||||||
|
except zipfile.BadZipfile or zipfile.error:
|
||||||
|
local_name = self.getlocalname(zinfo)
|
||||||
|
data = self.getfiledata(zinfo)
|
||||||
|
nzinfo.filename = local_name
|
||||||
|
|
||||||
|
nzinfo.date_time = zinfo.date_time
|
||||||
|
nzinfo.compress_type = zinfo.compress_type
|
||||||
|
nzinfo.flag_bits = 0
|
||||||
|
nzinfo.internal_attr = 0
|
||||||
|
self.outzip.writestr(nzinfo,data)
|
||||||
|
|
||||||
|
self.bzf.close()
|
||||||
|
self.inzip.close()
|
||||||
|
self.outzip.close()
|
||||||
|
|
||||||
|
|
||||||
|
def usage():
|
||||||
|
print """usage: zipfix.py inputzip outputzip
|
||||||
|
inputzip is the source zipfile to fix
|
||||||
|
outputzip is the fixed zip archive
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def repairBook(infile, outfile):
|
||||||
|
if not os.path.exists(infile):
|
||||||
|
print "Error: Input Zip File does not exist"
|
||||||
|
return 1
|
||||||
|
try:
|
||||||
|
fr = fixZip(infile, outfile)
|
||||||
|
fr.fix()
|
||||||
|
return 0
|
||||||
|
except Exception, e:
|
||||||
|
print "Error Occurred ", e
|
||||||
|
return 2
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv=sys.argv):
|
||||||
|
if len(argv)!=3:
|
||||||
|
usage()
|
||||||
|
return 1
|
||||||
|
infile = argv[1]
|
||||||
|
outfile = argv[2]
|
||||||
|
return repairBook(infile, outfile)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__' :
|
||||||
|
sys.exit(main())
|
||||||
|
|
||||||
|
|
||||||
58
DeDRM_Windows_Application/ReadMe_DeDRM_WinApp.txt
Normal file
58
DeDRM_Windows_Application/ReadMe_DeDRM_WinApp.txt
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
ReadMe_DeDRM_WinApp_vX.X
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
DeDRM_WinApp is a pure python drag and drop application that allows users to drag and drop ebooks or folders of ebooks onto theDeDRM_Drop_Target to have the DRM removed. It repackages the"tools" python software in one easy to use program.
|
||||||
|
|
||||||
|
It should work out of the box with Kindle for PC ebooks and Adobe Adept epub and pdf ebooks.
|
||||||
|
|
||||||
|
To remove the DRM from standalone Kindle ebooks, eReader pdb ebooks, Barnes and Noble epubs, and Mobipocket ebooks requires the user to double-click the DeDRM_Drop_Target and set some additional Preferences including:
|
||||||
|
|
||||||
|
Kindle 16 digit Serial Number
|
||||||
|
Barnes & Noble key files (bnepubkey.b64)
|
||||||
|
eReader Social DRM: (Name:Last 8 digits of CC number)
|
||||||
|
MobiPocket, Kindle for iPhone/iPad/iPodTouch 10 digit PID
|
||||||
|
|
||||||
|
Once these preferences have been set, the user can simply drag and drop ebooks onto the DeDRM_Drop_Target to remove the DRM.
|
||||||
|
|
||||||
|
This program requires that the proper 32 bit version of Python 2.X (tested with Python 2.5 through Python 2.7) and PyCrypto be installed on your computer before it will work. See below for where to get theese programs for Windows.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Installation
|
||||||
|
------------
|
||||||
|
|
||||||
|
1. Download the latest DeDRM_WinApp_vx.x.zip and fully Extract its contents.
|
||||||
|
|
||||||
|
2. Move the resulting DeDRM_WinApp_vX.X folder to whereever you keep you other programs.
|
||||||
|
(I typically use an "Applications" folder inside of my home directory)
|
||||||
|
|
||||||
|
3. Open the folder, and create a short-cut to DeDRM_Drop_Target and move that short-cut to your Desktop.
|
||||||
|
|
||||||
|
4. To set the preferences simply double-click on your just created short-cut.
|
||||||
|
|
||||||
|
|
||||||
|
If you already have a correct version of Python and PyCrypto installed and in your path, you are ready to go!
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
If not, see where you can get these additional pieces.
|
||||||
|
|
||||||
|
|
||||||
|
Installing Python on Windows
|
||||||
|
----------------------------
|
||||||
|
I strongly recommend installing ActiveState’s Active Python, Community Edition for Windows (x86) 32 bits. This is a free, full version of the Python. It comes with some important additional modules that are not included in the bare-bones version from www.python.org unless you choose to install everything.
|
||||||
|
|
||||||
|
1. Download ActivePython 2.7.1 for Windows (x86) (or later 2.7 version for Windows (x86) ) from http://www.activestate.com/activepython/downloads. Do not download the ActivePython 2.7.1 for Windows (64-bit, x64) verson, even if you are running 64-bit Windows.
|
||||||
|
|
||||||
|
2. When it has finished downloading, run the installer. Accept the default options.
|
||||||
|
|
||||||
|
Installing PyCrypto on Windows
|
||||||
|
------------------------------
|
||||||
|
PyCrypto is a set of encryption/decryption routines that work with Python. The sources are freely available, and compiled versions are available from several sources. You must install a version that is for 32-bit Windows and Python 2.7. I recommend the installer linked from Michael Foord’s blog.
|
||||||
|
|
||||||
|
1. Download PyCrypto 2.1 for 32bit Windows and Python 2.7 from http://www.voidspace.org.uk/python/modules.shtml#pycrypto
|
||||||
|
|
||||||
|
2. When it has finished downloading, unzip it. This will produce a file “pycrypto-2.1.0.win32-py2.7.exe”.
|
||||||
|
|
||||||
|
3. Double-click “pycrypto-2.1.0.win32-py2.7.exe” to run it. Accept the default options.
|
||||||
|
|
||||||
@@ -110,6 +110,8 @@ class MainDialog(Tkinter.Frame):
|
|||||||
def showCmdOutput(self, msg):
|
def showCmdOutput(self, msg):
|
||||||
if msg and msg !='':
|
if msg and msg !='':
|
||||||
msg = msg.encode('utf-8')
|
msg = msg.encode('utf-8')
|
||||||
|
if sys.platform.startswith('win'):
|
||||||
|
msg = msg.replace('\r\n','\n')
|
||||||
self.stext.insert(Tkconstants.END,msg)
|
self.stext.insert(Tkconstants.END,msg)
|
||||||
self.stext.yview_pickplace(Tkconstants.END)
|
self.stext.yview_pickplace(Tkconstants.END)
|
||||||
return
|
return
|
||||||
@@ -205,7 +207,7 @@ class MainDialog(Tkinter.Frame):
|
|||||||
|
|
||||||
tpz = False
|
tpz = False
|
||||||
# Identify any Topaz Files
|
# Identify any Topaz Files
|
||||||
with open(mobipath, 'rb') as f:
|
f = file(mobipath, 'rb')
|
||||||
raw = f.read(3)
|
raw = f.read(3)
|
||||||
if raw.startswith('TPZ'):
|
if raw.startswith('TPZ'):
|
||||||
tpz = True
|
tpz = True
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user