From bc968f8eca63ec9be4c02cb8d4294582fe63a452 Mon Sep 17 00:00:00 2001 From: Apprentice Alf Date: Mon, 17 Jan 2011 07:24:53 +0000 Subject: [PATCH] tools v3.2 First appearance of combined windows python app --- Adobe_EPUB_Tools/ineptepub.pyw | 93 +- Adobe_EPUB_Tools/ineptkey.pyw | 18 +- Adobe_PDF_Tools/ineptkey.pyw | 18 +- Adobe_PDF_Tools/ineptpdf.pyw | 49 +- Barnes_and_Noble_EPUB_Tools/ignobleepub.pyw | 98 +- Barnes_and_Noble_EPUB_Tools/ignoblekeygen.pyw | 38 +- .../K4MobiDeDRM_plugin/convert2xml.py | 1 + Calibre_Plugins/README-Ineptpdf-plugin.txt | 78 +- Calibre_Plugins/README-K4MobiDeDRM-plugin.txt | 46 +- .../README-eReaderPDB2PML-plugin.txt | 46 +- Calibre_Plugins/README-ignobleepub-plugin.txt | 80 +- Calibre_Plugins/README-ineptepub-plugin.txt | 78 +- Calibre_Plugins/k4mobidedrm_plugin.zip | Bin 43219 -> 43885 bytes .../k4mobidedrm_plugin/k4mobidedrm_plugin.py | 205 +- .../k4mobidedrm_plugin/mobidedrm.py | 30 +- DeDRM_Macintosh_Application/DeDRM.app.txt | Bin 47208 -> 102368 bytes .../DeDRM.app/Contents/Info.plist | 8 +- .../Contents/Resources/Scripts/main.scpt | Bin 205564 -> 220916 bytes .../Contents/Resources/convert2xml.py | 1 + .../DeDRM.app/Contents/Resources/erdr2pml.py | 110 +- .../Contents/Resources/k4mobidedrm.py | 213 +- .../DeDRM.app/Contents/Resources/mobidedrm.py | 30 +- .../DeDRM.app/Contents/Resources/zipfix.py | 69 +- .../DeDRM_WinApp/DeDRM_Drop_Target.bat | 4 + .../DeDRM_WinApp/DeDRM_lib/DeDRM_app.pyw | 581 +++++ .../DeDRM_WinApp/DeDRM_lib/lib/activitybar.py | 75 + .../DeDRM_WinApp/DeDRM_lib/lib/convert2xml.py | 818 ++++++ .../DeDRM_WinApp/DeDRM_lib/lib/decryptepub.py | 86 + .../DeDRM_WinApp/DeDRM_lib/lib/decryptpdb.py | 46 + .../DeDRM_WinApp/DeDRM_lib/lib/decryptpdf.py | 50 + .../DeDRM_WinApp/DeDRM_lib/lib/erdr2pml.py | 484 ++++ .../DeDRM_lib/lib/flatxml2html.py | 706 ++++++ .../DeDRM_WinApp/DeDRM_lib/lib/flatxml2svg.py | 151 ++ .../DeDRM_WinApp/DeDRM_lib/lib/genbook.py | 561 +++++ .../DeDRM_WinApp/DeDRM_lib/lib/ignobleepub.py | 336 +++ .../DeDRM_lib/lib/ignoblekeygen.py | 239 ++ .../DeDRM_WinApp/DeDRM_lib/lib/ineptepub.py | 476 ++++ .../DeDRM_WinApp/DeDRM_lib/lib/ineptkey.py | 467 ++++ .../DeDRM_WinApp/DeDRM_lib/lib/ineptpdf.py | 2228 +++++++++++++++++ .../DeDRM_WinApp/DeDRM_lib/lib/k4mobidedrm.py | 367 +++ .../DeDRM_WinApp/DeDRM_lib/lib/k4mutils.py | 194 ++ .../DeDRM_WinApp/DeDRM_lib/lib/k4pcutils.py | 110 + .../DeDRM_WinApp/DeDRM_lib/lib/kgenpids.py | 316 +++ .../DeDRM_WinApp/DeDRM_lib/lib/mobidedrm.py | 406 +++ .../DeDRM_WinApp/DeDRM_lib/lib/openssl_des.py | 90 + .../DeDRM_lib/lib/pycrypto_des.py | 31 + .../DeDRM_WinApp/DeDRM_lib/lib/python_des.py | 220 ++ .../DeDRM_lib/lib/scrolltextwidget.py | 27 + .../DeDRM_WinApp/DeDRM_lib/lib/simpleprefs.py | 78 + .../DeDRM_lib/lib/stylexml2css.py | 243 ++ .../DeDRM_WinApp/DeDRM_lib/lib/subasyncio.py | 149 ++ .../DeDRM_lib/lib/topazextract.py | 436 ++++ .../DeDRM_WinApp/DeDRM_lib/lib/zipfix.py | 160 ++ .../ReadMe_DeDRM_WinApp.txt | 58 + .../KindleBooks/lib/convert2xml.py | 1 + .../KindleBooks/lib/k4mobidedrm.py | 203 +- .../KindleBooks/lib/mobidedrm.py | 30 +- .../Kindle_4_Mac_Unswindle/lib/mobidedrm.py | 30 +- .../Kindle_4_PC_Unswindle/mobidedrm.py | 30 +- KindleBooks_Tools/MobiDeDRM.py | 30 +- Mobi_Additional_Tools/lib/mobidedrm.py | 30 +- ReadMe_First.txt | 18 +- ePub_Fixer/lib/zipfix.py | 69 +- eReader_PDB_Tools/lib/erdr2pml.py | 110 +- 64 files changed, 11284 insertions(+), 769 deletions(-) create mode 100644 DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_Drop_Target.bat create mode 100644 DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/DeDRM_app.pyw create mode 100644 DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/activitybar.py create mode 100644 DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/convert2xml.py create mode 100644 DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/decryptepub.py create mode 100644 DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/decryptpdb.py create mode 100644 DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/decryptpdf.py create mode 100644 DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/erdr2pml.py create mode 100644 DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/flatxml2html.py create mode 100644 DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/flatxml2svg.py create mode 100644 DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/genbook.py create mode 100644 DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/ignobleepub.py create mode 100644 DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/ignoblekeygen.py create mode 100644 DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/ineptepub.py create mode 100644 DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/ineptkey.py create mode 100644 DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/ineptpdf.py create mode 100644 DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/k4mobidedrm.py create mode 100644 DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/k4mutils.py create mode 100644 DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/k4pcutils.py create mode 100644 DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/kgenpids.py create mode 100644 DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/mobidedrm.py create mode 100644 DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/openssl_des.py create mode 100644 DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/pycrypto_des.py create mode 100644 DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/python_des.py create mode 100644 DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/scrolltextwidget.py create mode 100644 DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/simpleprefs.py create mode 100644 DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/stylexml2css.py create mode 100644 DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/subasyncio.py create mode 100644 DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/topazextract.py create mode 100644 DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/zipfix.py create mode 100644 DeDRM_Windows_Application/ReadMe_DeDRM_WinApp.txt diff --git a/Adobe_EPUB_Tools/ineptepub.pyw b/Adobe_EPUB_Tools/ineptepub.pyw index 9d95720..48a75f9 100644 --- a/Adobe_EPUB_Tools/ineptepub.pyw +++ b/Adobe_EPUB_Tools/ineptepub.pyw @@ -1,7 +1,9 @@ #! /usr/bin/python # -*- coding: utf-8 -*- -# ineptepub.pyw, version 5.5 +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 @@ -27,13 +29,11 @@ # 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. """ -from __future__ import with_statement - __license__ = 'GPL v3' import sys @@ -312,45 +312,6 @@ class Decryptor(object): data = self.decompress(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): def __init__(self, root): @@ -446,6 +407,52 @@ class DecryptionDialog(Tkinter.Frame): 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: diff --git a/Adobe_EPUB_Tools/ineptkey.pyw b/Adobe_EPUB_Tools/ineptkey.pyw index fd90508..8eab14f 100644 --- a/Adobe_EPUB_Tools/ineptkey.pyw +++ b/Adobe_EPUB_Tools/ineptkey.pyw @@ -1,7 +1,9 @@ #! /usr/bin/python # -*- coding: utf-8 -*- -# ineptkey.pyw, version 5.3 +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 @@ -33,13 +35,12 @@ # 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. """ -from __future__ import with_statement - __license__ = 'GPL v3' import sys @@ -415,10 +416,11 @@ class ExceptionDialog(Tkinter.Frame): 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 cli_main(argv=sys.argv): - keypath = argv[1] + +def extractKeyfile(keypath): try: success = retrieve_key(keypath) except ADEPTError, e: @@ -431,6 +433,12 @@ def cli_main(argv=sys.argv): 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() diff --git a/Adobe_PDF_Tools/ineptkey.pyw b/Adobe_PDF_Tools/ineptkey.pyw index fd90508..8eab14f 100644 --- a/Adobe_PDF_Tools/ineptkey.pyw +++ b/Adobe_PDF_Tools/ineptkey.pyw @@ -1,7 +1,9 @@ #! /usr/bin/python # -*- coding: utf-8 -*- -# ineptkey.pyw, version 5.3 +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 @@ -33,13 +35,12 @@ # 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. """ -from __future__ import with_statement - __license__ = 'GPL v3' import sys @@ -415,10 +416,11 @@ class ExceptionDialog(Tkinter.Frame): 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 cli_main(argv=sys.argv): - keypath = argv[1] + +def extractKeyfile(keypath): try: success = retrieve_key(keypath) except ADEPTError, e: @@ -431,6 +433,12 @@ def cli_main(argv=sys.argv): 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() diff --git a/Adobe_PDF_Tools/ineptpdf.pyw b/Adobe_PDF_Tools/ineptpdf.pyw index d73e069..ccdd9e4 100644 --- a/Adobe_PDF_Tools/ineptpdf.pyw +++ b/Adobe_PDF_Tools/ineptpdf.pyw @@ -1,6 +1,8 @@ #! /usr/bin/env python # ineptpdf.pyw, version 7.7 +from __future__ import with_statement + # To run this program install Python 2.6 from http://www.python.org/download/ # and OpenSSL (already installed on Mac OS X and Linux) OR # PyCrypto from http://www.voidspace.org.uk/python/modules.shtml#pycrypto @@ -30,13 +32,12 @@ # fixed minor typos # 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 """ Decrypts Adobe ADEPT-encrypted PDF files. """ -from __future__ import with_statement - __license__ = 'GPL v3' import sys @@ -2076,25 +2077,6 @@ class PDFSerializer(object): self.write('\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): def __init__(self, root): @@ -2198,6 +2180,31 @@ class DecryptionDialog(Tkinter.Frame): 'Close this window or decrypt another pdf file.' 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(): root = Tkinter.Tk() if RSA is None: diff --git a/Barnes_and_Noble_EPUB_Tools/ignobleepub.pyw b/Barnes_and_Noble_EPUB_Tools/ignobleepub.pyw index 0afc2bc..a7c48c9 100644 --- a/Barnes_and_Noble_EPUB_Tools/ignobleepub.pyw +++ b/Barnes_and_Noble_EPUB_Tools/ignobleepub.pyw @@ -1,6 +1,8 @@ #! /usr/bin/python -# ignobleepub.pyw, version 3.3 +from __future__ import with_statement + +# ignobleepub.pyw, version 3.4 # To run this program install Python 2.6 from # and OpenSSL or PyCrypto from http://www.voidspace.org.uk/python/modules.shtml#pycrypto @@ -14,10 +16,9 @@ # 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' import sys @@ -170,49 +171,6 @@ class Decryptor(object): 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): def __init__(self, root): Tkinter.Frame.__init__(self, root, border=5) @@ -308,6 +266,53 @@ class DecryptionDialog(Tkinter.Frame): 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: @@ -324,6 +329,7 @@ def gui_main(): root.mainloop() return 0 + if __name__ == '__main__': if len(sys.argv) > 1: sys.exit(cli_main()) diff --git a/Barnes_and_Noble_EPUB_Tools/ignoblekeygen.pyw b/Barnes_and_Noble_EPUB_Tools/ignoblekeygen.pyw index b2607ea..cdedc48 100644 --- a/Barnes_and_Noble_EPUB_Tools/ignoblekeygen.pyw +++ b/Barnes_and_Noble_EPUB_Tools/ignoblekeygen.pyw @@ -1,6 +1,8 @@ #! /usr/bin/python -# ignoblekeygen.pyw, version 2.2 +from __future__ import with_statement + +# ignoblekeygen.pyw, version 2.3 # To run this program install Python 2.6 from # and OpenSSL or PyCrypto from http://www.voidspace.org.uk/python/modules.shtml#pycrypto @@ -12,12 +14,12 @@ # 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. """ -from __future__ import with_statement - __license__ = 'GPL v3' import sys @@ -120,6 +122,7 @@ 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' @@ -133,19 +136,6 @@ def generate_keyfile(name, ccn, outpath): f.write(userkey.encode('base64')) return userkey -def cli_main(argv=sys.argv): - progname = os.path.basename(argv[0]) - if AES is None: - print "%s: This script requires 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): def __init__(self, root): @@ -211,6 +201,22 @@ class DecryptionDialog(Tkinter.Frame): 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: diff --git a/Calibre_Plugins/K4MobiDeDRM_plugin/convert2xml.py b/Calibre_Plugins/K4MobiDeDRM_plugin/convert2xml.py index 3070ab6..3c27ed0 100644 --- a/Calibre_Plugins/K4MobiDeDRM_plugin/convert2xml.py +++ b/Calibre_Plugins/K4MobiDeDRM_plugin/convert2xml.py @@ -235,6 +235,7 @@ class PageParser(object): 'group' : (1, 'snippets', 1, 0), 'group.type' : (1, 'scalar_text', 0, 0), + 'group._tag' : (1, 'scalar_text', 0, 0), 'region' : (1, 'snippets', 1, 0), 'region.type' : (1, 'scalar_text', 0, 0), diff --git a/Calibre_Plugins/README-Ineptpdf-plugin.txt b/Calibre_Plugins/README-Ineptpdf-plugin.txt index 4d668fc..457adb1 100644 --- a/Calibre_Plugins/README-Ineptpdf-plugin.txt +++ b/Calibre_Plugins/README-Ineptpdf-plugin.txt @@ -1,39 +1,39 @@ -Inept PDF Plugin - ineptpdf_vXX_plugin.zip -Requires Calibre version 0.6.44 or higher. - -All credit given to IHeartCabbages for the original standalone scripts. -I had the much easier job of converting them to a Calibre plugin. - -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: - -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. - -Please note: Calibre does not provide any immediate feedback to indicate that adding the plugin was a success. You can always click on the File-Type plugins to see if the plugin was added. - - -Configuration: - -When first run, the plugin will attempt to find your Adobe Digital Editions installation (on Windows and Mac OS's). If successful, it will create an 'adeptkey.der' file and save it in Calibre's configuration directory. It will use that file on subsequent runs. If there are already '*.der' files in the directory, the plugin won't attempt to -find the Adobe Digital Editions installation installation. - -So if you have Adobe Digital Editions installation installed on the same machine as Calibre... you are ready to go. If not... keep reading. - -If you already have keyfiles generated with I <3 Cabbages' ineptkey.pyw script, you can put those keyfiles in Calibre's configuration directory. The easiest way to find the correct directory is to go to Calibre's Preferences page... click on the 'Miscellaneous' button (looks like a gear), and then click the 'Open Calibre configuration directory' button. Paste your keyfiles in there. Just make sure that -they have different names and are saved with the '.der' extension (like the ineptkey script produces). This directory isn't touched when upgrading Calibre, so it's quite safe to leave them there. - -Since there is no Linux version of Adobe Digital Editions, Linux users will have to obtain a keyfile through other methods and put the file in Calibre's configuration directory. - -All keyfiles with a '.der' extension found in Calibre's configuration directory will be used to attempt to decrypt a book. - -** NOTE ** There is no plugin customization data for the Inept PDF plugin. - -Troubleshooting: - -If you find that it's not working for you (imported PDFs still have DRM), you can save a lot of time and trouble by trying to add the PDF to Calibre with the command line tools. This will print out a lot of helpful debugging info that can be copied into any online help requests. I'm going to ask you to do it first, anyway, so you might -as well get used to it. ;) - -Open a command prompt (terminal) and change to the directory where the ebook you're trying to import resides. Then type the command "calibredb add your_ebook.pdf". Don't type the quotes and obviously change the 'your_ebook.pdf' to whatever the filename of your book is. Copy the resulting output and paste it into any online help request you make. - -** Note: the Mac version of Calibre doesn't install the command line tools by default. If you go to the 'Preferences' page and click on the miscellaneous button, you'll see the option to install the command line tools. \ No newline at end of file +Inept PDF Plugin - ineptpdf_vXX_plugin.zip +Requires Calibre version 0.6.44 or higher. + +All credit given to IHeartCabbages for the original standalone scripts. +I had the much easier job of converting them to a Calibre plugin. + +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: + +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. + +Please note: Calibre does not provide any immediate feedback to indicate that adding the plugin was a success. You can always click on the File-Type plugins to see if the plugin was added. + + +Configuration: + +When first run, the plugin will attempt to find your Adobe Digital Editions installation (on Windows and Mac OS's). If successful, it will create an 'adeptkey.der' file and save it in Calibre's configuration directory. It will use that file on subsequent runs. If there are already '*.der' files in the directory, the plugin won't attempt to +find the Adobe Digital Editions installation installation. + +So if you have Adobe Digital Editions installation installed on the same machine as Calibre... you are ready to go. If not... keep reading. + +If you already have keyfiles generated with I <3 Cabbages' ineptkey.pyw script, you can put those keyfiles in Calibre's configuration directory. The easiest way to find the correct directory is to go to Calibre's Preferences page... click on the 'Miscellaneous' button (looks like a gear), and then click the 'Open Calibre configuration directory' button. Paste your keyfiles in there. Just make sure that +they have different names and are saved with the '.der' extension (like the ineptkey script produces). This directory isn't touched when upgrading Calibre, so it's quite safe to leave them there. + +Since there is no Linux version of Adobe Digital Editions, Linux users will have to obtain a keyfile through other methods and put the file in Calibre's configuration directory. + +All keyfiles with a '.der' extension found in Calibre's configuration directory will be used to attempt to decrypt a book. + +** NOTE ** There is no plugin customization data for the Inept PDF plugin. + +Troubleshooting: + +If you find that it's not working for you (imported PDFs still have DRM), you can save a lot of time and trouble by trying to add the PDF to Calibre with the command line tools. This will print out a lot of helpful debugging info that can be copied into any online help requests. I'm going to ask you to do it first, anyway, so you might +as well get used to it. ;) + +Open a command prompt (terminal) and change to the directory where the ebook you're trying to import resides. Then type the command "calibredb add your_ebook.pdf". Don't type the quotes and obviously change the 'your_ebook.pdf' to whatever the filename of your book is. Copy the resulting output and paste it into any online help request you make. + +** Note: the Mac version of Calibre doesn't install the command line tools by default. If you go to the 'Preferences' page and click on the miscellaneous button, you'll see the option to install the command line tools. diff --git a/Calibre_Plugins/README-K4MobiDeDRM-plugin.txt b/Calibre_Plugins/README-K4MobiDeDRM-plugin.txt index 9d392f3..d0909f5 100644 --- a/Calibre_Plugins/README-K4MobiDeDRM-plugin.txt +++ b/Calibre_Plugins/README-K4MobiDeDRM-plugin.txt @@ -1,23 +1,23 @@ -Plugin for K4PC, K4Mac, standalone Kindles, Mobi Books, and for Devices with Fixed PIDs. - -This plugin supersedes MobiDeDRM, K4DeDRM, and K4PCDeDRM and K4X plugins. If you install this plugin, those plugins can be safely removed. - -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. - -Installation: -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. - -Please note: Calibre does not provide any immediate feedback to indicate that adding the plugin was a success. You can always click on the File-Type plugins to see if the plugin was added. - -Configuration: -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. - - -Troubleshooting: -If you find that it's not working for you, you can save a lot of time and trouble by trying to add the azw file to Calibre with the command line tools. This will print out a lot of helpful debugging info that can be copied into any online help requests. I'm going to ask you to do it first, anyway, so you might -as well get used to it. ;) - -Open a command prompt (terminal) and change to the directory where the ebook you're trying to import resides. Then type the command "calibredb add your_ebook.azw". Don't type the quotes and obviously change the 'your_ebook.azw' to whatever the filename of your book is. Copy the resulting output and paste it into any online help request you make. - -** Note: the Mac version of Calibre doesn't install the command line tools by default. If you go to the 'Preferences' page and click on the miscellaneous button, you'll see the option to install the command line tools. - +Plugin for K4PC, K4Mac, standalone Kindles, Mobi Books, and for Devices with Fixed PIDs. + +This plugin supersedes MobiDeDRM, K4DeDRM, and K4PCDeDRM and K4X plugins. If you install this plugin, those plugins can be safely removed. + +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. + +Installation: +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. + +Please note: Calibre does not provide any immediate feedback to indicate that adding the plugin was a success. You can always click on the File-Type plugins to see if the plugin was added. + +Configuration: +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. + + +Troubleshooting: +If you find that it's not working for you, you can save a lot of time and trouble by trying to add the azw file to Calibre with the command line tools. This will print out a lot of helpful debugging info that can be copied into any online help requests. I'm going to ask you to do it first, anyway, so you might +as well get used to it. ;) + +Open a command prompt (terminal) and change to the directory where the ebook you're trying to import resides. Then type the command "calibredb add your_ebook.azw". Don't type the quotes and obviously change the 'your_ebook.azw' to whatever the filename of your book is. Copy the resulting output and paste it into any online help request you make. + +** Note: the Mac version of Calibre doesn't install the command line tools by default. If you go to the 'Preferences' page and click on the miscellaneous button, you'll see the option to install the command line tools. + diff --git a/Calibre_Plugins/README-eReaderPDB2PML-plugin.txt b/Calibre_Plugins/README-eReaderPDB2PML-plugin.txt index 573f8ec..ff98316 100644 --- a/Calibre_Plugins/README-eReaderPDB2PML-plugin.txt +++ b/Calibre_Plugins/README-eReaderPDB2PML-plugin.txt @@ -1,23 +1,23 @@ -eReader PDB2PML - eReaderPDB2PML_vXX_plugin.zip - -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. - -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: -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. - -Please note: Calibre does not provide any immediate feedback to indicate that adding the plugin was a success. You can always click on the File-Type plugins to see if the plugin was added. - -Configuration: -Highlight the plugin (eReader PDB 2 PML under the "File type plugins" category) and click the "Customize Plugin" button on Calibre's Preferences->Plugins page. Enter your name and last 8 digits of the credit card number separated by a comma: Your Name,12341234 - -If you've purchased books with more than one credit card, separate the info with a colon: Your Name,12341234:Other Name,23452345 (NOTE: Do NOT put quotes around your name like you do with the original script!!) - -Troubleshooting: -If you find that it's not working for you (imported pdb's are not converted to pmlz format), you can save a lot of time and trouble by trying to add the pdb to Calibre with the command line tools. This will print out a lot of helpful debugging info that can be copied into any online help requests. I'm going to ask you to do it first, anyway, so you might -as well get used to it. ;) - -Open a command prompt (terminal) and change to the directory where the ebook you're trying to import resides. Then type the command "calibredb add your_ebook.pdb". Don't type the quotes and obviously change the 'your_ebook.pdb' to whatever the filename of your book is. Copy the resulting output and paste it into any online help request you make. - -** Note: the Mac version of Calibre doesn't install the command line tools by default. If you go to the 'Preferences' page and click on the miscellaneous button, you'll see the option to install the command line tools. +eReader PDB2PML - eReaderPDB2PML_vXX_plugin.zip + +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. + +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: +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. + +Please note: Calibre does not provide any immediate feedback to indicate that adding the plugin was a success. You can always click on the File-Type plugins to see if the plugin was added. + +Configuration: +Highlight the plugin (eReader PDB 2 PML under the "File type plugins" category) and click the "Customize Plugin" button on Calibre's Preferences->Plugins page. Enter your name and last 8 digits of the credit card number separated by a comma: Your Name,12341234 + +If you've purchased books with more than one credit card, separate the info with a colon: Your Name,12341234:Other Name,23452345 (NOTE: Do NOT put quotes around your name like you do with the original script!!) + +Troubleshooting: +If you find that it's not working for you (imported pdb's are not converted to pmlz format), you can save a lot of time and trouble by trying to add the pdb to Calibre with the command line tools. This will print out a lot of helpful debugging info that can be copied into any online help requests. I'm going to ask you to do it first, anyway, so you might +as well get used to it. ;) + +Open a command prompt (terminal) and change to the directory where the ebook you're trying to import resides. Then type the command "calibredb add your_ebook.pdb". Don't type the quotes and obviously change the 'your_ebook.pdb' to whatever the filename of your book is. Copy the resulting output and paste it into any online help request you make. + +** Note: the Mac version of Calibre doesn't install the command line tools by default. If you go to the 'Preferences' page and click on the miscellaneous button, you'll see the option to install the command line tools. diff --git a/Calibre_Plugins/README-ignobleepub-plugin.txt b/Calibre_Plugins/README-ignobleepub-plugin.txt index 15de927..ad52f53 100644 --- a/Calibre_Plugins/README-ignobleepub-plugin.txt +++ b/Calibre_Plugins/README-ignobleepub-plugin.txt @@ -1,40 +1,40 @@ -Ignoble Epub DeDRM - ignobleepub_vXX_plugin.zip -Requires Calibre version 0.6.44 or higher. - -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. - -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: - -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 -click the 'Add' button. you're done. - -Please note: Calibre does not provide any immediate feedback to indicate that adding the plugin was a success. You can always click on the File-Type plugins to see if the plugin was added. - -Configuration: - -1) The easiest way to configure the plugin is to enter your name (Barnes & Noble account name) and credit card number (the one used to purchase the books) into the plugin's customization window. It's the same info you would enter into the ignoblekeygen script. Highlight the plugin (Ignoble Epub DeDRM) and click the "Customize Plugin" button on -Calibre's Preferences->Plugins page. Enter the name and credit card number separated by a comma: Your Name,1234123412341234 - -If you've purchased books with more than one credit card, separate that other info with a colon: Your Name,1234123412341234:Other Name,2345234523452345 - -** NOTE ** The above method is your only option if you don't have/can't run the original I <3 Cabbages scripts on your particular machine. - -** NOTE ** Your credit card number will be on display in Calibre's Plugin configuration page when using the above method. If other people have access to your computer, you may want to use the second configuration method below. - -2) If you already have keyfiles generated with I <3 Cabbages' ignoblekeygen.pyw script, you can put those keyfiles into Calibre's configuration directory. The easiest way to find the correct directory is to go to Calibre's Preferences page... click on the 'Miscellaneous' button (looks like a gear), and then click the 'Open Calibre -configuration directory' button. Paste your keyfiles in there. Just make sure that they have different names and are saved with the '.b64' extension (like the ignoblekeygen script produces). This directory isn't touched when upgrading Calibre, so it's quite safe to leave them there. - -All keyfiles from method 2 and all data entered from method 1 will be used to attempt to decrypt a book. You can use method 1 or method 2, or a combination of both. - -Troubleshooting: - -If you find that it's not working for you (imported epubs still have DRM), you can save a lot of time and trouble by trying to add the epub to Calibre with the command line tools. This will print out a lot of helpful debugging info that can be copied into any online help requests. I'm going to ask you to do it first, anyway, so you might -as well get used to it. ;) - -Open a command prompt (terminal) and change to the directory where the ebook you're trying to import resides. Then type the command "calibredb add your_ebook.epub". Don't type the quotes and obviously change the 'your_ebook.epub' to whatever the filename of your book is. Copy the resulting output and paste it into any online help request you make. - -** Note: the Mac version of Calibre doesn't install the command line tools by default. If you go to the 'Preferences' page and click on the miscellaneous button, you'll see the option to install the command line tools. +Ignoble Epub DeDRM - ignobleepub_vXX_plugin.zip +Requires Calibre version 0.6.44 or higher. + +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. + +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: + +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 +click the 'Add' button. you're done. + +Please note: Calibre does not provide any immediate feedback to indicate that adding the plugin was a success. You can always click on the File-Type plugins to see if the plugin was added. + +Configuration: + +1) The easiest way to configure the plugin is to enter your name (Barnes & Noble account name) and credit card number (the one used to purchase the books) into the plugin's customization window. It's the same info you would enter into the ignoblekeygen script. Highlight the plugin (Ignoble Epub DeDRM) and click the "Customize Plugin" button on +Calibre's Preferences->Plugins page. Enter the name and credit card number separated by a comma: Your Name,1234123412341234 + +If you've purchased books with more than one credit card, separate that other info with a colon: Your Name,1234123412341234:Other Name,2345234523452345 + +** NOTE ** The above method is your only option if you don't have/can't run the original I <3 Cabbages scripts on your particular machine. + +** NOTE ** Your credit card number will be on display in Calibre's Plugin configuration page when using the above method. If other people have access to your computer, you may want to use the second configuration method below. + +2) If you already have keyfiles generated with I <3 Cabbages' ignoblekeygen.pyw script, you can put those keyfiles into Calibre's configuration directory. The easiest way to find the correct directory is to go to Calibre's Preferences page... click on the 'Miscellaneous' button (looks like a gear), and then click the 'Open Calibre +configuration directory' button. Paste your keyfiles in there. Just make sure that they have different names and are saved with the '.b64' extension (like the ignoblekeygen script produces). This directory isn't touched when upgrading Calibre, so it's quite safe to leave them there. + +All keyfiles from method 2 and all data entered from method 1 will be used to attempt to decrypt a book. You can use method 1 or method 2, or a combination of both. + +Troubleshooting: + +If you find that it's not working for you (imported epubs still have DRM), you can save a lot of time and trouble by trying to add the epub to Calibre with the command line tools. This will print out a lot of helpful debugging info that can be copied into any online help requests. I'm going to ask you to do it first, anyway, so you might +as well get used to it. ;) + +Open a command prompt (terminal) and change to the directory where the ebook you're trying to import resides. Then type the command "calibredb add your_ebook.epub". Don't type the quotes and obviously change the 'your_ebook.epub' to whatever the filename of your book is. Copy the resulting output and paste it into any online help request you make. + +** Note: the Mac version of Calibre doesn't install the command line tools by default. If you go to the 'Preferences' page and click on the miscellaneous button, you'll see the option to install the command line tools. diff --git a/Calibre_Plugins/README-ineptepub-plugin.txt b/Calibre_Plugins/README-ineptepub-plugin.txt index 56f95b8..1bc1d7a 100644 --- a/Calibre_Plugins/README-ineptepub-plugin.txt +++ b/Calibre_Plugins/README-ineptepub-plugin.txt @@ -1,39 +1,39 @@ -Inept Epub DeDRM - ineptepub_vXX_plugin.zip -Requires Calibre version 0.6.44 or higher. - -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. - -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: - -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. - -Please note: Calibre does not provide any immediate feedback to indicate that adding the plugin was a success. You can always click on the File-Type plugins to see if the plugin was added. - - -Configuration: - -When first run, the plugin will attempt to find your Adobe Digital Editions installation (on Windows and Mac OS's). If successful, it will create an 'adeptkey.der' file and save it in Calibre's configuration directory. It will use that file on subsequent runs. If there are already '*.der' files in the directory, the plugin won't attempt to -find the Adobe Digital Editions installation installation. - -So if you have Adobe Digital Editions installation installed on the same machine as Calibre... you are ready to go. If not... keep reading. - -If you already have keyfiles generated with I <3 Cabbages' ineptkey.pyw script, you can put those keyfiles in Calibre's configuration directory. The easiest way to find the correct directory is to go to Calibre's Preferences page... click on the 'Miscellaneous' button (looks like a gear), and then click the 'Open Calibre configuration directory' button. Paste your keyfiles in there. Just make sure that -they have different names and are saved with the '.der' extension (like the ineptkey script produces). This directory isn't touched when upgrading Calibre, so it's quite safe to leave them there. - -Since there is no Linux version of Adobe Digital Editions, Linux users will have to obtain a keyfile through other methods and put the file in Calibre's configuration directory. - -All keyfiles with a '.der' extension found in Calibre's configuration directory will be used to attempt to decrypt a book. - -** NOTE ** There is no plugin customization data for the Inept Epub DeDRM plugin. - -Troubleshooting: - -If you find that it's not working for you (imported epubs still have DRM), you can save a lot of time and trouble by trying to add the epub to Calibre with the command line tools. This will print out a lot of helpful debugging info that can be copied into any online help requests. I'm going to ask you to do it first, anyway, so you might -as well get used to it. ;) - -Open a command prompt (terminal) and change to the directory where the ebook you're trying to import resides. Then type the command "calibredb add your_ebook.epub". Don't type the quotes and obviously change the 'your_ebook.epub' to whatever the filename of your book is. Copy the resulting output and paste it into any online help request you make. - -** Note: the Mac version of Calibre doesn't install the command line tools by default. If you go to the 'Preferences' page and click on the miscellaneous button, you'll see the option to install the command line tools. \ No newline at end of file +Inept Epub DeDRM - ineptepub_vXX_plugin.zip +Requires Calibre version 0.6.44 or higher. + +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. + +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: + +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. + +Please note: Calibre does not provide any immediate feedback to indicate that adding the plugin was a success. You can always click on the File-Type plugins to see if the plugin was added. + + +Configuration: + +When first run, the plugin will attempt to find your Adobe Digital Editions installation (on Windows and Mac OS's). If successful, it will create an 'adeptkey.der' file and save it in Calibre's configuration directory. It will use that file on subsequent runs. If there are already '*.der' files in the directory, the plugin won't attempt to +find the Adobe Digital Editions installation installation. + +So if you have Adobe Digital Editions installation installed on the same machine as Calibre... you are ready to go. If not... keep reading. + +If you already have keyfiles generated with I <3 Cabbages' ineptkey.pyw script, you can put those keyfiles in Calibre's configuration directory. The easiest way to find the correct directory is to go to Calibre's Preferences page... click on the 'Miscellaneous' button (looks like a gear), and then click the 'Open Calibre configuration directory' button. Paste your keyfiles in there. Just make sure that +they have different names and are saved with the '.der' extension (like the ineptkey script produces). This directory isn't touched when upgrading Calibre, so it's quite safe to leave them there. + +Since there is no Linux version of Adobe Digital Editions, Linux users will have to obtain a keyfile through other methods and put the file in Calibre's configuration directory. + +All keyfiles with a '.der' extension found in Calibre's configuration directory will be used to attempt to decrypt a book. + +** NOTE ** There is no plugin customization data for the Inept Epub DeDRM plugin. + +Troubleshooting: + +If you find that it's not working for you (imported epubs still have DRM), you can save a lot of time and trouble by trying to add the epub to Calibre with the command line tools. This will print out a lot of helpful debugging info that can be copied into any online help requests. I'm going to ask you to do it first, anyway, so you might +as well get used to it. ;) + +Open a command prompt (terminal) and change to the directory where the ebook you're trying to import resides. Then type the command "calibredb add your_ebook.epub". Don't type the quotes and obviously change the 'your_ebook.epub' to whatever the filename of your book is. Copy the resulting output and paste it into any online help request you make. + +** Note: the Mac version of Calibre doesn't install the command line tools by default. If you go to the 'Preferences' page and click on the miscellaneous button, you'll see the option to install the command line tools. diff --git a/Calibre_Plugins/k4mobidedrm_plugin.zip b/Calibre_Plugins/k4mobidedrm_plugin.zip index aff65bdb16b4a64b446c3d02e58fc6eada74a995..5ef572b264d3575d27fec5a87a42396f0322fa8e 100644 GIT binary patch delta 14406 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 c5|oeRx0B zT~}W&gDuQ~aVkiIL!g6zfWUy*>8J8~=v$?5;DCTA*@A!|g5ZM~+uOOBI=L`=*xJxL zc&e)W00DQHLzlgyAg|+;g#ijgjVO40AT8}@mPwTW1j$ zw(A(K_yHa1m(lvZSN(7DlNn}07?Mj~nIK4_**VE~d_8^K?AJ3xLn zKWEl8rFU!#NlxO~BAp!RY4GypYQ1?5nvU@=mlRiC+aiimn!4yB2O;t_l@<@hU_zri zSVoDHCs#u53Uho0y)WIA5O3eE&4n`j=rI3O^x>h)<@$SG+l4UyjqOlO28-m)yVnXN zVR~nYRk9DMhf>%md(hGjj6$5Wc0Lr|Q|SqN+^iyn~%xKuc?Nfr_ix%yf< zqeaf7-{DTNm*3`IXLt^Jg170o^b4Oo2r$?fR(Pa#$4=k!fpp9l(`*s8??69EvEvCI z=<05N#7k#G&?%mno()6FQ+?Za>>9PnDpMQgl0?Uk#3s~nPs7nRP~O}A zC6om-u7eYphU4k}8L4aGw@PiPNcOXxUJlHY^)6qvh_n>h-68?Ij-v|Kv;~3maDI5i zS(Z}aH?U+?(5H9nRh?aq2wT$5G7y-A=(7agr-!!LSVq?rP!p_*aG;(TftAZHV29DE znN*Mf>bzotjYh1DLq-)KO;oTZn1TXr)9pdt>x`c7%Pt zryq@L2RD7KK^oItdZjBRM`n)^$=EZ*MI<;wvrLH|_ZUa=2i32xnz}FEbpdv8?q9Yu z#a;t$x>2xfj&!{7J1Go1o_>zL+=kaeD0ljHkf_p;a!68FJ?wg!fjOMrlCWV>gXu&M z2%uz*ZgTb!Kuat_&L9prbY!0X0`Xh(G%qn-dticWhRvd;pkojrQ_VH5Oche?M{2At z=0@F*Ig+Yq(D}=jsIO2?XpCgyfE{46HR48jj%(LY>|mEKr4jIF2`l@p%5X`tgXt)Y zyN&U>;{_zsw<=v^*8&#f`DfPMihe@}EdY!*`lynX7N8kl74JWmSDAL?>QZarSQ#&1 zdr=BRF}>LQ4Cwl|%ibH;iw;AptvdKOt=K?)B|uuCdjeK*beezrcM;0lJ%{PE?_V zlfFGo%m9D#VtNJnPS*jDC2%%r@=;*XoNg4aDm9mt;)`2`6_5RI2DgGAA(BYJvpEji)brz`Eg9 z!&SyrOA)rB6PP3FMXeT29toXkrtAyL1gETich1MkQp0VkT5eUr6o%0A2eK4&`lT$g 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\+/]') + 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:] + return one + +def decryptBook(infile, outdir, k4, kInfoFiles, serials, pids): + import mobidedrm + import topazextract + import kgenpids + + # 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 + 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: + if mobi: + unlocked_file = mb.processBook(pidlst) + else: + mb.processBook(pidlst) + + except mobidedrm.DrmException, e: + print "Error: " + str(e) + "\nDRM Removal Failed.\n" + return 1 + except Exception, e: + if not mobi: + print "Error: " + str(e) + "\nDRM Removal Failed.\n" + 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() + shutil.rmtree(tempdir, True) + return 1 + pass + + if mobi: + outfile = os.path.join(outdir,outfilename + '_nodrm' + '.mobi') + 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, outfilename + '_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, outfilename + '_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, outfilename + '_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, True) + return 0 + + def usage(progname): print "Removes DRM protection from K4PC/M, Kindle, Mobi and Topaz ebooks" print "Usage:" @@ -78,9 +192,6 @@ def usage(progname): # Main # def main(argv=sys.argv): - import mobidedrm - import topazextract - import kgenpids progname = os.path.basename(argv[0]) k4 = False @@ -118,89 +229,11 @@ def main(argv=sys.argv): # 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 + return decryptBook(infile, outdir, k4, kInfoFiles, serials, pids) - 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() - shutil.rmtree(tempdir, True) - 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, True) - return 0 if __name__ == '__main__': sys.stdout=Unbuffered(sys.stdout) @@ -210,12 +243,12 @@ if not __name__ == "__main__" and inCalibre: from calibre.customize import FileTypePlugin class K4DeDRM(FileTypePlugin): - name = 'K4PC, K4Mac, Mobi DeDRM' # Name of the plugin + 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, 9) # The version number of this plugin + version = (0, 2, 1) # 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 diff --git a/Calibre_Plugins/k4mobidedrm_plugin/mobidedrm.py b/Calibre_Plugins/k4mobidedrm_plugin/mobidedrm.py index 864b545..2266329 100644 --- a/Calibre_Plugins/k4mobidedrm_plugin/mobidedrm.py +++ b/Calibre_Plugins/k4mobidedrm_plugin/mobidedrm.py @@ -44,8 +44,10 @@ # 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% -__version__ = '0.24' +__version__ = '0.26' import sys @@ -205,7 +207,18 @@ class MobiBook: pos = 12 for i in xrange(nitems): type, size = struct.unpack('>II', exth[pos: pos + 8]) - content = exth[pos + 8: pos + size] + # 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) + content = "\144" + elif type == 404 and size == 9: + # make sure text to speech is enabled + self.patchSection(0, "\0", 16 + self.mobi_length + pos + 8) + content = "\0" + else: + content = exth[pos + 8: pos + size] + #print type, size, content self.meta_array[type] = content pos += size except: @@ -308,8 +321,10 @@ class MobiBook: t1_keyvec = "QDCVEPMU675RUBSZ" if self.magic == 'TEXtREAd': bookkey_data = self.sect[0x0E:0x0E+16] - else: + 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 : @@ -366,15 +381,18 @@ def getUnencryptedBookWithList(infile,pidlist): def main(argv=sys.argv): print ('MobiDeDrm v%(__version__)s. ' '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 "Usage:" - print " %s " % sys.argv[0] + print " %s []" % sys.argv[0] return 1 else: infile = argv[1] outfile = argv[2] - pidlist = argv[3].split(',') + if len(argv) is 4: + pidlist = argv[3].split(',') + else: + pidlist = {} try: stripped_file = getUnencryptedBookWithList(infile, pidlist) file(outfile, 'wb').write(stripped_file) diff --git a/DeDRM_Macintosh_Application/DeDRM.app.txt b/DeDRM_Macintosh_Application/DeDRM.app.txt index 66b743ffa35229f2b0898d063c7cf24db3c2cc30..62ba3533a7b654eeee53ef57023eb4c1ba61edea 100644 GIT binary patch literal 102368 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 literal 47208 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

UT-OX?j@}4`jER}X8q*ldf&fE_O9qgG>!2Fb z>1p)fpTq+ZHucW7`sQ^}V-KtSE(!k~{M44DL5OgcnF&{uGY#gX%L1?Evzpn{Z?WUd z?~a7i6fzM%v2XLqQdrz2q;Ikb(!Tml-2ofn4#5lXqhzby%T>s54B-a-TBhIESVfDq zLtEnADGuT26R5rg25e4Z-w_Tm`dmA3X;Gk#D!yTc{hh5X0 zoOT@O+3OKjAU{&Ck_#{CkLtm9@4mcPk5xp#Uo^&QVPFU z*)dWVM}iOIFJ+_QPGViB+Eyl^%U z#0n;ge?*<9cZ5Bgm5Wn<$MQ6PCngE5u^Ls_B6~^ZukqP)AHPlVw1YyYjM6O6w;P1_k7b`>@0;`xpblM( z{6GJs)^oq1w6oX-d#FpmDo0v>rpe_Mhp%RFLH{W|-5 zenV(>OEzIqq&qvxsI9{>gEac!8q&6bT9?8U5{jy~HPkDU@k(k%uCI9g0W(ulczIkL zzxq1!)8J;0P>$NMAkvBP1Q=1!$bE|%Q;9U4@7Apkd^QXw!Qy;T<4dc93wCW5cwE8 z_T1h5`S8S;l7);&-YJwT$lY}Az$q$xxe1Sk`e_diGtKe~9)!l}Uj6J2w#VdVMsmeP zdAY2a4=GW*2-NY<6%s3)(-Fk~Nx0MI35G9ci0WN6A%>8mbIGbm+>Z)Kl;fV4^J?@K zET>ri3KGgxML3?pR_YUl78J_^cSsjrxjUZBhUxa_eSZ|$g9Z-n;u7e1T*{L+Pmg3a zzhV(kyN4}zkn#nNPfxhPJ(Hh)`Y8*NT9^04S?i!BuK1Bk z;G$z6p%JbUlIY286ba+>H$jOPHN|b5IO@&hWHnxnaQ24h5R`;R*>S~B)ZwF12p!1D zSd_Dt08pP}3mhWBTSH4gcr?K!Az)=-++^-6vsxgApE`=vu{`V9)CCH&IF%Wi$tiX;Y^saJHP6!5xU>f`B7{~o12$_&_7S** zjt|gyG`rz#ry!Al0-1RX`2a*7mlycgEz<#MrC;0t>tmkSS`==Cc3RXWP{-Pn?tW;T z2gAp`p;MrCoD0YbHf#{pQ>nGXvSX;}9N<9fa)vE-?D>NM<#XZ78g*Ve;IybmR1bJH zt7eBhAFNMwgNo%wVDo8#Pm5wcCuk-NLZPdtB1o8p-eDvMHNi^z|9Q1o%pcyrkI#jG zA7)K)gxc);zs7ah5=`pZ`<~ld9{`Jb0P#cFJ?`?|+mSQQYm4YWB)X*8V%KGVlk zlD>)NZ^O^$yfwqOHr0WWBa}Yr$I*OKd8%3@gm%(A`v3e%n*_Ix|IiZg>S$zMxpd%X ziN04@Czo?eXP8V;93jq8+}Wo*CCE6560K*1>3z7bS9V`K@t=t4z7kMRqz#8EY% zx-SB#?kgMHSXg9GUW+<*8r#3Kc|5Xx_~M177iJ8lKXki3Q07B11myA1M}%0`irP!} z!@6-E2H-Sg!9Wrk%f^R2v%M=2#tbZ z$d#ZIvl9+%;32|opM7ibg2kMG@TKAiU$k&EfUom!3tn(IkgE6%%hS5sr~Cu50qCMAx{pcf$! zc%-YcI*wj>qy?Ds2pokP>>^MgjuuECsWu^biOQ>j`7$-3;?t-<955BEbdv$6ai(R% zYV-)A01XTnF>QN$+TIQB&7o_ieZ^1AavjXRi98?mNl+|U1Cd(+*)Ih0J}{z8*7sT^ z0BIM){xC$MUW5j#xxcqRd0%=P&XCFBundleExecutable droplet CFBundleGetInfoString - DeDRM 1.6, Copyright © 2010–2011 by Apprentice Alf. + DeDRM 2.0, Copyright © 2010–2011 by Apprentice Alf. CFBundleIconFile droplet CFBundleInfoDictionaryVersion @@ -34,7 +34,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.6 + 2.0 CFBundleSignature dplt LSMinimumSystemVersion @@ -46,9 +46,9 @@ name ScriptWindowState positionOfDivider - 686 + 709 savedFrame - 2161 -75 907 765 1440 -150 1680 1050 + 1617 62 862 788 1440 -150 1680 1050 selectedTabView result diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/Scripts/main.scpt b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/Scripts/main.scpt index b400b1067c64d41709838f24543a17d0b5ef3090..193f3821804402a44c6ffb9d38944abb16919ac9 100644 GIT binary patch literal 220916 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⋙t^CI5$KriWT=KYn!sy4- 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
%Tnx*PbL%w$={K>&XrJ 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 literal 205564 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

;+s2AbaOoe*E8E3?D*7N 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 Dy^UXi diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/convert2xml.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/convert2xml.py index 3070ab6..3c27ed0 100644 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/convert2xml.py +++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/convert2xml.py @@ -235,6 +235,7 @@ class PageParser(object): 'group' : (1, 'snippets', 1, 0), 'group.type' : (1, 'scalar_text', 0, 0), + 'group._tag' : (1, 'scalar_text', 0, 0), 'region' : (1, 'snippets', 1, 0), 'region.type' : (1, 'scalar_text', 0, 0), diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/erdr2pml.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/erdr2pml.py index ce6945d..6df9e13 100644 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/erdr2pml.py +++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/erdr2pml.py @@ -57,8 +57,9 @@ # 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 -__version__='0.18' +__version__='0.19' class Unbuffered: def __init__(self, stream): @@ -111,12 +112,14 @@ 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() @@ -364,7 +367,7 @@ def cleanPML(pml): 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) @@ -390,6 +393,47 @@ def convertEreaderToPml(infile, name, cc, outdir): # 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:" @@ -404,8 +448,8 @@ def usage(): 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: @@ -413,75 +457,27 @@ def main(argv=None): 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] - if make_pmlz : - # ignore specified outdir, use tempdir instead - outdir = tempfile.mkdtemp() - - bookname = os.path.splitext(os.path.basename(infile))[0] + 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] - try: - print "Processing..." - import time - start_time = time.time() - convertEreaderToPml(infile, name, cc, outdir) + return decryptBook(infile, outdir, name, cc, make_pmlz) - 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) - - 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 - else : - print 'output in %s' % outdir - print "done" - except ValueError, e: - print "Error: %s" % e - return 1 - return 0 if __name__ == "__main__": sys.exit(main()) diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4mobidedrm.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4mobidedrm.py index f9625a6..7088c06 100644 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4mobidedrm.py +++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4mobidedrm.py @@ -1,5 +1,7 @@ #!/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 @@ -26,9 +28,8 @@ # 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' +__version__ = '2.1' class Unbuffered: def __init__(self, stream): @@ -41,6 +42,7 @@ class Unbuffered: import sys import os, csv, getopt +import string import binascii import zlib import re @@ -69,6 +71,116 @@ def zipUpDir(myzip, tempdir,localname): elif os.path.isdir(realfilePath): zipUpDir(myzip, tempdir, localfilePath) +# cleanup bytestring filenames +# borrowed from calibre from calibre/src/calibre/__init__.py +# added in removal of non-printing chars +# and removal of . at start +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:] + return one + +def decryptBook(infile, outdir, k4, kInfoFiles, serials, pids): + import mobidedrm + import topazextract + import kgenpids + + # 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 + 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: + if mobi: + unlocked_file = mb.processBook(pidlst) + else: + mb.processBook(pidlst) + + except mobidedrm.DrmException, e: + print "Error: " + str(e) + "\nDRM Removal Failed.\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() + shutil.rmtree(tempdir, True) + return 1 + + if mobi: + outfile = os.path.join(outdir,outfilename + '_nodrm' + '.mobi') + 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, outfilename + '_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, outfilename + '_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, outfilename + '_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, True) + return 0 + + def usage(progname): print "Removes DRM protection from K4PC/M, Kindle, Mobi and Topaz ebooks" print "Usage:" @@ -78,9 +190,6 @@ def usage(progname): # Main # def main(argv=sys.argv): - import mobidedrm - import topazextract - import kgenpids progname = os.path.basename(argv[0]) k4 = False @@ -89,7 +198,7 @@ def main(argv=sys.argv): pids = [] print ('K4MobiDeDrm v%(__version__)s ' - 'provided by the work of many including DiapDealer, SomeUpdates, IHeartCabbages, CMBDTC, Skindle, DarkReverser, ApprenticeAlf, etc .' % globals()) + 'provided by the work of many including DiapDealer, SomeUpdates, IHeartCabbages, CMBDTC, Skindle, DarkReverser, ApprenticeAlf, etc .' % globals()) print ' ' try: @@ -118,89 +227,11 @@ def main(argv=sys.argv): # 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 + return decryptBook(infile, outdir, k4, kInfoFiles, serials, pids) - 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() - shutil.rmtree(tempdir, True) - 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, True) - return 0 if __name__ == '__main__': sys.stdout=Unbuffered(sys.stdout) @@ -215,7 +246,7 @@ if not __name__ == "__main__" and inCalibre: 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 + version = (0, 2, 1) # 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 @@ -241,15 +272,15 @@ if not __name__ == "__main__" and inCalibre: for customvalue in customvalues: customvalue = str(customvalue) customvalue = customvalue.strip() - if len(customvalue) == 10 or len(customvalue) == 8: + if len(customvalue) == 10 or len(customvalue) == 8: pids.append(customvalue) - else : + 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. + + # 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] @@ -257,7 +288,7 @@ if not __name__ == "__main__" and inCalibre: 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) diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/mobidedrm.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/mobidedrm.py index 864b545..2266329 100644 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/mobidedrm.py +++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/mobidedrm.py @@ -44,8 +44,10 @@ # 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% -__version__ = '0.24' +__version__ = '0.26' import sys @@ -205,7 +207,18 @@ class MobiBook: pos = 12 for i in xrange(nitems): type, size = struct.unpack('>II', exth[pos: pos + 8]) - content = exth[pos + 8: pos + size] + # 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) + content = "\144" + elif type == 404 and size == 9: + # make sure text to speech is enabled + self.patchSection(0, "\0", 16 + self.mobi_length + pos + 8) + content = "\0" + else: + content = exth[pos + 8: pos + size] + #print type, size, content self.meta_array[type] = content pos += size except: @@ -308,8 +321,10 @@ class MobiBook: t1_keyvec = "QDCVEPMU675RUBSZ" if self.magic == 'TEXtREAd': bookkey_data = self.sect[0x0E:0x0E+16] - else: + 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 : @@ -366,15 +381,18 @@ def getUnencryptedBookWithList(infile,pidlist): def main(argv=sys.argv): print ('MobiDeDrm v%(__version__)s. ' '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 "Usage:" - print " %s " % sys.argv[0] + print " %s []" % sys.argv[0] return 1 else: infile = argv[1] outfile = argv[2] - pidlist = argv[3].split(',') + if len(argv) is 4: + pidlist = argv[3].split(',') + else: + pidlist = {} try: stripped_file = getUnencryptedBookWithList(infile, pidlist) file(outfile, 'wb').write(stripped_file) diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/zipfix.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/zipfix.py index 536a21d..4c862a7 100644 --- a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/zipfix.py +++ b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/zipfix.py @@ -81,22 +81,44 @@ class fixZip: # 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 - + # also fix bad epub compression + + # write mimetype file first, if present, and with no compression for zinfo in self.inzip.infolist(): - 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 + if zinfo.filename == "mimetype": + 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) + nzinfo.date_time = zinfo.date_time + nzinfo.compress_type = zipfile.ZIP_STORED + nzinfo.flag_bits = 0 + nzinfo.internal_attr = 0 + nzinfo.extra = "" + self.outzip.writestr(nzinfo,data) + break + + # write the rest of the files + for zinfo in self.inzip.infolist(): + if zinfo.filename != "mimetype": + 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() @@ -110,14 +132,7 @@ def usage(): """ -def main(argv=sys.argv): - if len(argv)!=3: - usage() - return 1 - infile = None - outfile = None - infile = argv[1] - outfile = argv[2] +def repairBook(infile, outfile): if not os.path.exists(infile): print "Error: Input Zip File does not exist" return 1 @@ -129,6 +144,16 @@ def main(argv=sys.argv): 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()) diff --git a/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_Drop_Target.bat b/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_Drop_Target.bat new file mode 100644 index 0000000..9e33348 --- /dev/null +++ b/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_Drop_Target.bat @@ -0,0 +1,4 @@ +echo off +set PWD=%~dp0 +cd /d %PWD%\DeDRM_lib && start /min python DeDRM_app.pyw %* +exit diff --git a/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/DeDRM_app.pyw b/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/DeDRM_app.pyw new file mode 100644 index 0000000..d3d6bda --- /dev/null +++ b/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/DeDRM_app.pyw @@ -0,0 +1,581 @@ +#!/usr/bin/env python +# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab + +import sys +sys.path.append('lib') +import os, os.path +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, dnd=False, filenames=[]): + Tk.__init__(self) + self.withdraw() + self.dnd = dnd + # 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 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'] + infofile = os.path.join(prefdir,'kindle.info') + path = '' + if os.path.isfile(infofile): + path = infofile + 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 File', + defaultextension='.info', filetypes=[('Kindle Info', '.info'),('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.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=50, 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=50, 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.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): + rv = 1 + name, ext = os.path.splitext(os.path.basename(infile)) + ext = ext.lower() + if ext == '.epub': + outfile = os.path.join(outdir, name + '_nodrm.epub') + self.p2 = processEPUB(infile, outfile, rscpath) + return 0 + if ext == '.pdb': + self.p2 = processPDB(infile, outdir, rscpath) + return 0 + if ext in ['.azw', '.azw1', '.prc', '.mobi', '.tpz']: + self.p2 = processK4MOBI(infile, outdir, rscpath) + return 0 + if ext == '.pdf': + outfile = os.path.join(outdir, name + '_nodrm.pdf') + self.p2 = processPDF(infile, outfile, rscpath) + return 0 + return rv + + +# run as a subprocess via pipes and collect stdout, stderr, and return value +def runit(ncmd, nparms): + cmdline = 'python ' + 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 = 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(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$", 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(cmd, parms) + return p2 + +def processPDF(infile, outfile, rscpath): + cmd = '"' + os.path.join('lib','decryptpdf.py') + '" ' + parms = '"' + infile + '" "' + outfile + '" "' + rscpath + '"' + p2 = runit(cmd, parms) + return p2 + +def processEPUB(infile, outfile, rscpath): + # invoke routine to check both Adept and Barnes and Noble + cmd = '"' + os.path.join('lib','decryptepub.py') + '" ' + parms = '"' + infile + '" "' + outfile + '" "' + rscpath + '"' + p2 = runit(cmd, parms) + return p2 + +def processPDB(infile, outdir, rscpath): + cmd = '"' + os.path.join('lib','decryptpdb.py') + '" ' + parms = '"' + infile + '" "' + outdir + '" "' + rscpath + '"' + p2 = runit(cmd, parms) + return p2 + + +def main(argv=sys.argv): + # 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(dnd, filenames) + app.mainloop() + return 0 + + +if __name__ == "__main__": + sys.exit(main()) + diff --git a/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/activitybar.py b/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/activitybar.py new file mode 100644 index 0000000..d2289c9 --- /dev/null +++ b/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/activitybar.py @@ -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('', 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) diff --git a/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/convert2xml.py b/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/convert2xml.py new file mode 100644 index 0000000..3c27ed0 --- /dev/null +++ b/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/convert2xml.py @@ -0,0 +1,818 @@ +#! /usr/bin/python +# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab +# For use with Topaz Scripts Version 2.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 +sys.stdout=Unbuffered(sys.stdout) + +import csv +import os +import getopt +from struct import pack +from struct import unpack + + +# Get a 7 bit encoded number from string. The most +# significant byte comes first and has the high bit (8th) set + +def readEncodedNumber(file): + flag = False + c = file.read(1) + if (len(c) == 0): + return None + data = ord(c) + + if data == 0xFF: + flag = True + c = file.read(1) + if (len(c) == 0): + return None + data = ord(c) + + if data >= 0x80: + datax = (data & 0x7F) + while data >= 0x80 : + c = file.read(1) + if (len(c) == 0): + return None + data = ord(c) + datax = (datax <<7) + (data & 0x7F) + data = datax + + if flag: + data = -data + return data + + +# returns a binary string that encodes a number into 7 bits +# most significant byte first which has the high bit set + +def encodeNumber(number): + result = "" + negative = False + flag = 0 + + if number < 0 : + number = -number + 1 + negative = True + + while True: + byte = number & 0x7F + number = number >> 7 + byte += flag + result += chr(byte) + flag = 0x80 + if number == 0 : + if (byte == 0xFF and negative == False) : + result += chr(0x80) + break + + if negative: + result += chr(0xFF) + + return result[::-1] + + + +# create / read a length prefixed string from the file + +def lengthPrefixString(data): + return encodeNumber(len(data))+data + +def readString(file): + stringLength = readEncodedNumber(file) + if (stringLength == None): + return "" + sv = file.read(stringLength) + if (len(sv) != stringLength): + return "" + return unpack(str(stringLength)+"s",sv)[0] + + +# convert a binary string generated by encodeNumber (7 bit encoded number) +# to the value you would find inside the page*.dat files to be processed + +def convert(i): + result = '' + val = encodeNumber(i) + for j in xrange(len(val)): + c = ord(val[j:j+1]) + result += '%02x' % c + return result + + + +# the complete string table used to store all book text content +# as well as the xml tokens and values that make sense out of it + +class Dictionary(object): + def __init__(self, dictFile): + self.filename = dictFile + self.size = 0 + self.fo = file(dictFile,'rb') + self.stable = [] + self.size = readEncodedNumber(self.fo) + for i in xrange(self.size): + self.stable.append(self.escapestr(readString(self.fo))) + self.pos = 0 + + def escapestr(self, str): + str = str.replace('&','&') + str = str.replace('<','<') + str = str.replace('>','>') + str = str.replace('=','=') + return str + + def lookup(self,val): + if ((val >= 0) and (val < self.size)) : + self.pos = val + return self.stable[self.pos] + else: + print "Error - %d outside of string table limits" % val + sys.exit(-1) + + def getSize(self): + return self.size + + def getPos(self): + return self.pos + + def dumpDict(self): + for i in xrange(self.size): + print "%d %s %s" % (i, convert(i), self.stable[i]) + return + +# parses the xml snippets that are represented by each page*.dat file. +# also parses the other0.dat file - the main stylesheet +# and information used to inject the xml snippets into page*.dat files + +class PageParser(object): + def __init__(self, filename, dict, debug, flat_xml): + self.fo = file(filename,'rb') + self.id = os.path.basename(filename).replace('.dat','') + self.dict = dict + self.debug = debug + self.flat_xml = flat_xml + self.tagpath = [] + self.doc = [] + self.snippetList = [] + + + # hash table used to enable the decoding process + # This has all been developed by trial and error so it may still have omissions or + # contain errors + # Format: + # tag : (number of arguments, argument type, subtags present, special case of subtags presents when escaped) + + token_tags = { + 'x' : (1, 'scalar_number', 0, 0), + 'y' : (1, 'scalar_number', 0, 0), + 'h' : (1, 'scalar_number', 0, 0), + 'w' : (1, 'scalar_number', 0, 0), + 'firstWord' : (1, 'scalar_number', 0, 0), + 'lastWord' : (1, 'scalar_number', 0, 0), + 'rootID' : (1, 'scalar_number', 0, 0), + 'stemID' : (1, 'scalar_number', 0, 0), + 'type' : (1, 'scalar_text', 0, 0), + + 'info' : (0, 'number', 1, 0), + + 'info.word' : (0, 'number', 1, 1), + 'info.word.ocrText' : (1, 'text', 0, 0), + 'info.word.firstGlyph' : (1, 'raw', 0, 0), + 'info.word.lastGlyph' : (1, 'raw', 0, 0), + 'info.word.bl' : (1, 'raw', 0, 0), + 'info.word.link_id' : (1, 'number', 0, 0), + + 'glyph' : (0, 'number', 1, 1), + 'glyph.x' : (1, 'number', 0, 0), + 'glyph.y' : (1, 'number', 0, 0), + 'glyph.glyphID' : (1, 'number', 0, 0), + + 'dehyphen' : (0, 'number', 1, 1), + 'dehyphen.rootID' : (1, 'number', 0, 0), + 'dehyphen.stemID' : (1, 'number', 0, 0), + 'dehyphen.stemPage' : (1, 'number', 0, 0), + 'dehyphen.sh' : (1, 'number', 0, 0), + + 'links' : (0, 'number', 1, 1), + 'links.page' : (1, 'number', 0, 0), + 'links.rel' : (1, 'number', 0, 0), + 'links.row' : (1, 'number', 0, 0), + 'links.title' : (1, 'text', 0, 0), + 'links.href' : (1, 'text', 0, 0), + 'links.type' : (1, 'text', 0, 0), + + 'paraCont' : (0, 'number', 1, 1), + 'paraCont.rootID' : (1, 'number', 0, 0), + 'paraCont.stemID' : (1, 'number', 0, 0), + 'paraCont.stemPage' : (1, 'number', 0, 0), + + 'paraStems' : (0, 'number', 1, 1), + 'paraStems.stemID' : (1, 'number', 0, 0), + + 'wordStems' : (0, 'number', 1, 1), + 'wordStems.stemID' : (1, 'number', 0, 0), + + 'empty' : (1, 'snippets', 1, 0), + + 'page' : (1, 'snippets', 1, 0), + 'page.pageid' : (1, 'scalar_text', 0, 0), + 'page.pagelabel' : (1, 'scalar_text', 0, 0), + 'page.type' : (1, 'scalar_text', 0, 0), + 'page.h' : (1, 'scalar_number', 0, 0), + 'page.w' : (1, 'scalar_number', 0, 0), + 'page.startID' : (1, 'scalar_number', 0, 0), + + 'group' : (1, 'snippets', 1, 0), + 'group.type' : (1, 'scalar_text', 0, 0), + 'group._tag' : (1, 'scalar_text', 0, 0), + + 'region' : (1, 'snippets', 1, 0), + 'region.type' : (1, 'scalar_text', 0, 0), + 'region.x' : (1, 'scalar_number', 0, 0), + 'region.y' : (1, 'scalar_number', 0, 0), + 'region.h' : (1, 'scalar_number', 0, 0), + 'region.w' : (1, 'scalar_number', 0, 0), + + 'empty_text_region' : (1, 'snippets', 1, 0), + + 'img' : (1, 'snippets', 1, 0), + 'img.x' : (1, 'scalar_number', 0, 0), + 'img.y' : (1, 'scalar_number', 0, 0), + 'img.h' : (1, 'scalar_number', 0, 0), + 'img.w' : (1, 'scalar_number', 0, 0), + 'img.src' : (1, 'scalar_number', 0, 0), + 'img.color_src' : (1, 'scalar_number', 0, 0), + + 'paragraph' : (1, 'snippets', 1, 0), + 'paragraph.class' : (1, 'scalar_text', 0, 0), + 'paragraph.firstWord' : (1, 'scalar_number', 0, 0), + 'paragraph.lastWord' : (1, 'scalar_number', 0, 0), + + 'word_semantic' : (1, 'snippets', 1, 1), + 'word_semantic.type' : (1, 'scalar_text', 0, 0), + 'word_semantic.firstWord' : (1, 'scalar_number', 0, 0), + 'word_semantic.lastWord' : (1, 'scalar_number', 0, 0), + + 'word' : (1, 'snippets', 1, 0), + 'word.type' : (1, 'scalar_text', 0, 0), + 'word.class' : (1, 'scalar_text', 0, 0), + 'word.firstGlyph' : (1, 'scalar_number', 0, 0), + 'word.lastGlyph' : (1, 'scalar_number', 0, 0), + + '_span' : (1, 'snippets', 1, 0), + '_span.firstWord' : (1, 'scalar_number', 0, 0), + '-span.lastWord' : (1, 'scalar_number', 0, 0), + + 'span' : (1, 'snippets', 1, 0), + 'span.firstWord' : (1, 'scalar_number', 0, 0), + 'span.lastWord' : (1, 'scalar_number', 0, 0), + + 'extratokens' : (1, 'snippets', 1, 0), + 'extratokens.type' : (1, 'scalar_text', 0, 0), + 'extratokens.firstGlyph' : (1, 'scalar_number', 0, 0), + 'extratokens.lastGlyph' : (1, 'scalar_number', 0, 0), + + 'glyph.h' : (1, 'number', 0, 0), + 'glyph.w' : (1, 'number', 0, 0), + 'glyph.use' : (1, 'number', 0, 0), + 'glyph.vtx' : (1, 'number', 0, 1), + 'glyph.len' : (1, 'number', 0, 1), + 'glyph.dpi' : (1, 'number', 0, 0), + 'vtx' : (0, 'number', 1, 1), + 'vtx.x' : (1, 'number', 0, 0), + 'vtx.y' : (1, 'number', 0, 0), + 'len' : (0, 'number', 1, 1), + 'len.n' : (1, 'number', 0, 0), + + 'book' : (1, 'snippets', 1, 0), + 'version' : (1, 'snippets', 1, 0), + 'version.FlowEdit_1_id' : (1, 'scalar_text', 0, 0), + 'version.FlowEdit_1_version' : (1, 'scalar_text', 0, 0), + 'version.Schema_id' : (1, 'scalar_text', 0, 0), + 'version.Schema_version' : (1, 'scalar_text', 0, 0), + 'version.Topaz_version' : (1, 'scalar_text', 0, 0), + 'version.WordDetailEdit_1_id' : (1, 'scalar_text', 0, 0), + 'version.WordDetailEdit_1_version' : (1, 'scalar_text', 0, 0), + 'version.ZoneEdit_1_id' : (1, 'scalar_text', 0, 0), + 'version.ZoneEdit_1_version' : (1, 'scalar_text', 0, 0), + 'version.chapterheaders' : (1, 'scalar_text', 0, 0), + 'version.creation_date' : (1, 'scalar_text', 0, 0), + 'version.header_footer' : (1, 'scalar_text', 0, 0), + 'version.init_from_ocr' : (1, 'scalar_text', 0, 0), + 'version.letter_insertion' : (1, 'scalar_text', 0, 0), + 'version.xmlinj_convert' : (1, 'scalar_text', 0, 0), + 'version.xmlinj_reflow' : (1, 'scalar_text', 0, 0), + 'version.xmlinj_transform' : (1, 'scalar_text', 0, 0), + 'version.findlists' : (1, 'scalar_text', 0, 0), + 'version.page_num' : (1, 'scalar_text', 0, 0), + 'version.page_type' : (1, 'scalar_text', 0, 0), + 'version.bad_text' : (1, 'scalar_text', 0, 0), + 'version.glyph_mismatch' : (1, 'scalar_text', 0, 0), + 'version.margins' : (1, 'scalar_text', 0, 0), + 'version.staggered_lines' : (1, 'scalar_text', 0, 0), + 'version.paragraph_continuation' : (1, 'scalar_text', 0, 0), + 'version.toc' : (1, 'scalar_text', 0, 0), + + 'stylesheet' : (1, 'snippets', 1, 0), + 'style' : (1, 'snippets', 1, 0), + 'style._tag' : (1, 'scalar_text', 0, 0), + 'style.type' : (1, 'scalar_text', 0, 0), + 'style._parent_type' : (1, 'scalar_text', 0, 0), + 'style.class' : (1, 'scalar_text', 0, 0), + 'style._after_class' : (1, 'scalar_text', 0, 0), + 'rule' : (1, 'snippets', 1, 0), + 'rule.attr' : (1, 'scalar_text', 0, 0), + 'rule.value' : (1, 'scalar_text', 0, 0), + + 'original' : (0, 'number', 1, 1), + 'original.pnum' : (1, 'number', 0, 0), + 'original.pid' : (1, 'text', 0, 0), + 'pages' : (0, 'number', 1, 1), + 'pages.ref' : (1, 'number', 0, 0), + 'pages.id' : (1, 'number', 0, 0), + 'startID' : (0, 'number', 1, 1), + 'startID.page' : (1, 'number', 0, 0), + 'startID.id' : (1, 'number', 0, 0), + + } + + + # full tag path record keeping routines + def tag_push(self, token): + self.tagpath.append(token) + def tag_pop(self): + if len(self.tagpath) > 0 : + self.tagpath.pop() + def tagpath_len(self): + return len(self.tagpath) + def get_tagpath(self, i): + cnt = len(self.tagpath) + if i < cnt : result = self.tagpath[i] + for j in xrange(i+1, cnt) : + result += '.' + self.tagpath[j] + return result + + + # list of absolute command byte values values that indicate + # various types of loop meachanisms typically used to generate vectors + + cmd_list = (0x76, 0x76) + + # peek at and return 1 byte that is ahead by i bytes + def peek(self, aheadi): + c = self.fo.read(aheadi) + if (len(c) == 0): + return None + self.fo.seek(-aheadi,1) + c = c[-1:] + return ord(c) + + + # get the next value from the file being processed + def getNext(self): + nbyte = self.peek(1); + if (nbyte == None): + return None + val = readEncodedNumber(self.fo) + return val + + + # format an arg by argtype + def formatArg(self, arg, argtype): + if (argtype == 'text') or (argtype == 'scalar_text') : + result = self.dict.lookup(arg) + elif (argtype == 'raw') or (argtype == 'number') or (argtype == 'scalar_number') : + result = arg + elif (argtype == 'snippets') : + result = arg + else : + print "Error Unknown argtype %s" % argtype + sys.exit(-2) + return result + + + # process the next tag token, recursively handling subtags, + # arguments, and commands + def procToken(self, token): + + known_token = False + self.tag_push(token) + + if self.debug : print 'Processing: ', self.get_tagpath(0) + cnt = self.tagpath_len() + for j in xrange(cnt): + tkn = self.get_tagpath(j) + if tkn in self.token_tags : + num_args = self.token_tags[tkn][0] + argtype = self.token_tags[tkn][1] + subtags = self.token_tags[tkn][2] + splcase = self.token_tags[tkn][3] + ntags = -1 + known_token = True + break + + if known_token : + + # handle subtags if present + subtagres = [] + if (splcase == 1): + # this type of tag uses of escape marker 0x74 indicate subtag count + if self.peek(1) == 0x74: + skip = readEncodedNumber(self.fo) + subtags = 1 + num_args = 0 + + if (subtags == 1): + ntags = readEncodedNumber(self.fo) + if self.debug : print 'subtags: ' + token + ' has ' + str(ntags) + for j in xrange(ntags): + val = readEncodedNumber(self.fo) + subtagres.append(self.procToken(self.dict.lookup(val))) + + # arguments can be scalars or vectors of text or numbers + argres = [] + if num_args > 0 : + firstarg = self.peek(1) + if (firstarg in self.cmd_list) and (argtype != 'scalar_number') and (argtype != 'scalar_text'): + # single argument is a variable length vector of data + arg = readEncodedNumber(self.fo) + argres = self.decodeCMD(arg,argtype) + else : + # num_arg scalar arguments + for i in xrange(num_args): + argres.append(self.formatArg(readEncodedNumber(self.fo), argtype)) + + # build the return tag + result = [] + tkn = self.get_tagpath(0) + result.append(tkn) + result.append(subtagres) + result.append(argtype) + result.append(argres) + self.tag_pop() + return result + + # all tokens that need to be processed should be in the hash + # table if it may indicate a problem, either new token + # or an out of sync condition + else: + result = [] + if (self.debug): + print 'Unknown Token:', token + self.tag_pop() + return result + + + # special loop used to process code snippets + # it is NEVER used to format arguments. + # builds the snippetList + def doLoop72(self, argtype): + cnt = readEncodedNumber(self.fo) + if self.debug : + result = 'Set of '+ str(cnt) + ' xml snippets. The overall structure \n' + result += 'of the document is indicated by snippet number sets at the\n' + result += 'end of each snippet. \n' + print result + for i in xrange(cnt): + if self.debug: print 'Snippet:',str(i) + snippet = [] + snippet.append(i) + val = readEncodedNumber(self.fo) + snippet.append(self.procToken(self.dict.lookup(val))) + self.snippetList.append(snippet) + return + + + + # general loop code gracisouly submitted by "skindle" - thank you! + def doLoop76Mode(self, argtype, cnt, mode): + result = [] + adj = 0 + if mode & 1: + adj = readEncodedNumber(self.fo) + mode = mode >> 1 + x = [] + for i in xrange(cnt): + x.append(readEncodedNumber(self.fo) - adj) + for i in xrange(mode): + for j in xrange(1, cnt): + x[j] = x[j] + x[j - 1] + for i in xrange(cnt): + result.append(self.formatArg(x[i],argtype)) + return result + + + # dispatches loop commands bytes with various modes + # The 0x76 style loops are used to build vectors + + # This was all derived by trial and error and + # new loop types may exist that are not handled here + # since they did not appear in the test cases + + def decodeCMD(self, cmd, argtype): + if (cmd == 0x76): + + # loop with cnt, and mode to control loop styles + cnt = readEncodedNumber(self.fo) + mode = readEncodedNumber(self.fo) + + if self.debug : print 'Loop for', cnt, 'with mode', mode, ': ' + return self.doLoop76Mode(argtype, cnt, mode) + + if self.dbug: print "Unknown command", cmd + result = [] + return result + + + + # add full tag path to injected snippets + def updateName(self, tag, prefix): + name = tag[0] + subtagList = tag[1] + argtype = tag[2] + argList = tag[3] + nname = prefix + '.' + name + nsubtaglist = [] + for j in subtagList: + nsubtaglist.append(self.updateName(j,prefix)) + ntag = [] + ntag.append(nname) + ntag.append(nsubtaglist) + ntag.append(argtype) + ntag.append(argList) + return ntag + + + + # perform depth first injection of specified snippets into this one + def injectSnippets(self, snippet): + snipno, tag = snippet + name = tag[0] + subtagList = tag[1] + argtype = tag[2] + argList = tag[3] + nsubtagList = [] + if len(argList) > 0 : + for j in argList: + asnip = self.snippetList[j] + aso, atag = self.injectSnippets(asnip) + atag = self.updateName(atag, name) + nsubtagList.append(atag) + argtype='number' + argList=[] + if len(nsubtagList) > 0 : + subtagList.extend(nsubtagList) + tag = [] + tag.append(name) + tag.append(subtagList) + tag.append(argtype) + tag.append(argList) + snippet = [] + snippet.append(snipno) + snippet.append(tag) + return snippet + + + + # format the tag for output + def formatTag(self, node): + name = node[0] + subtagList = node[1] + argtype = node[2] + argList = node[3] + fullpathname = name.split('.') + nodename = fullpathname.pop() + ilvl = len(fullpathname) + indent = ' ' * (3 * ilvl) + result = indent + '<' + nodename + '>' + if len(argList) > 0: + argres = '' + for j in argList: + if (argtype == 'text') or (argtype == 'scalar_text') : + argres += j + '|' + else : + argres += str(j) + ',' + argres = argres[0:-1] + if argtype == 'snippets' : + result += 'snippets:' + argres + else : + result += argres + if len(subtagList) > 0 : + result += '\n' + for j in subtagList: + if len(j) > 0 : + result += self.formatTag(j) + result += indent + '\n' + else: + result += '\n' + return result + + + # flatten tag + def flattenTag(self, node): + name = node[0] + subtagList = node[1] + argtype = node[2] + argList = node[3] + result = name + if (len(argList) > 0): + argres = '' + for j in argList: + if (argtype == 'text') or (argtype == 'scalar_text') : + argres += j + '|' + else : + argres += str(j) + '|' + argres = argres[0:-1] + if argtype == 'snippets' : + result += '.snippets=' + argres + else : + result += '=' + argres + result += '\n' + for j in subtagList: + if len(j) > 0 : + result += self.flattenTag(j) + return result + + + # reduce create xml output + def formatDoc(self, flat_xml): + result = '' + for j in self.doc : + if len(j) > 0: + if flat_xml: + result += self.flattenTag(j) + else: + result += self.formatTag(j) + if self.debug : print result + return result + + + + # main loop - parse the page.dat files + # to create structured document and snippets + + # FIXME: value at end of magic appears to be a subtags count + # but for what? For now, inject an 'info" tag as it is in + # every dictionary and seems close to what is meant + # The alternative is to special case the last _ "0x5f" to mean something + + def process(self): + + # peek at the first bytes to see what type of file it is + magic = self.fo.read(9) + if (magic[0:1] == 'p') and (magic[2:9] == 'marker_'): + first_token = 'info' + elif (magic[0:1] == 'p') and (magic[2:9] == '__PAGE_'): + skip = self.fo.read(2) + first_token = 'info' + elif (magic[0:1] == 'p') and (magic[2:8] == '_PAGE_'): + first_token = 'info' + elif (magic[0:1] == 'g') and (magic[2:9] == '__GLYPH'): + skip = self.fo.read(3) + first_token = 'info' + else : + # other0.dat file + first_token = None + self.fo.seek(-9,1) + + + # main loop to read and build the document tree + while True: + + if first_token != None : + # use "inserted" first token 'info' for page and glyph files + tag = self.procToken(first_token) + if len(tag) > 0 : + self.doc.append(tag) + first_token = None + + v = self.getNext() + if (v == None): + break + + if (v == 0x72): + self.doLoop72('number') + elif (v > 0) and (v < self.dict.getSize()) : + tag = self.procToken(self.dict.lookup(v)) + if len(tag) > 0 : + self.doc.append(tag) + else: + if self.debug: + print "Main Loop: Unknown value: %x" % v + if (v == 0): + if (self.peek(1) == 0x5f): + skip = self.fo.read(1) + first_token = 'info' + + # now do snippet injection + if len(self.snippetList) > 0 : + if self.debug : print 'Injecting Snippets:' + snippet = self.injectSnippets(self.snippetList[0]) + snipno = snippet[0] + tag_add = snippet[1] + if self.debug : print self.formatTag(tag_add) + if len(tag_add) > 0: + self.doc.append(tag_add) + + # handle generation of xml output + xmlpage = self.formatDoc(self.flat_xml) + + return xmlpage + + +def fromData(dict, fname): + flat_xml = True + debug = False + pp = PageParser(fname, dict, debug, flat_xml) + xmlpage = pp.process() + return xmlpage + +def getXML(dict, fname): + flat_xml = False + debug = False + pp = PageParser(fname, dict, debug, flat_xml) + xmlpage = pp.process() + return xmlpage + +def usage(): + print 'Usage: ' + print ' convert2xml.py dict0000.dat infile.dat ' + print ' ' + print ' Options:' + print ' -h print this usage help message ' + print ' -d turn on debug output to check for potential errors ' + print ' --flat-xml output the flattened xml page description only ' + print ' ' + print ' This program will attempt to convert a page*.dat file or ' + print ' glyphs*.dat file, using the dict0000.dat file, to its xml description. ' + print ' ' + print ' Use "cmbtc_dump.py" first to unencrypt, uncompress, and dump ' + print ' the *.dat files from a Topaz format e-book.' + +# +# Main +# + +def main(argv): + dictFile = "" + pageFile = "" + debug = False + flat_xml = False + printOutput = False + if len(argv) == 0: + printOutput = True + argv = sys.argv + + try: + opts, args = getopt.getopt(argv[1:], "hd", ["flat-xml"]) + + except getopt.GetoptError, err: + + # print help information and exit: + print str(err) # will print something like "option -a not recognized" + usage() + sys.exit(2) + + if len(opts) == 0 and len(args) == 0 : + usage() + sys.exit(2) + + for o, a in opts: + if o =="-d": + debug=True + if o =="-h": + usage() + sys.exit(0) + if o =="--flat-xml": + flat_xml = True + + dictFile, pageFile = args[0], args[1] + + # read in the string table dictionary + dict = Dictionary(dictFile) + # dict.dumpDict() + + # create a page parser + pp = PageParser(pageFile, dict, debug, flat_xml) + + xmlpage = pp.process() + + if printOutput: + print xmlpage + return 0 + + return xmlpage + +if __name__ == '__main__': + sys.exit(main('')) diff --git a/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/decryptepub.py b/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/decryptepub.py new file mode 100644 index 0000000..b9c9330 --- /dev/null +++ b/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/decryptepub.py @@ -0,0 +1,86 @@ +#!/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] + outfile = 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 + + 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()) + diff --git a/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/decryptpdb.py b/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/decryptpdb.py new file mode 100644 index 0000000..93b4d86 --- /dev/null +++ b/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/decryptpdb.py @@ -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()) + diff --git a/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/decryptpdf.py b/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/decryptpdf.py new file mode 100644 index 0000000..f18e75e --- /dev/null +++ b/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/decryptpdf.py @@ -0,0 +1,50 @@ +#!/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] + outfile = args[1] + rscpath = args[2] + errlog = '' + rv = 1 + # 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()) + diff --git a/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/erdr2pml.py b/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/erdr2pml.py new file mode 100644 index 0000000..6df9e13 --- /dev/null +++ b/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/erdr2pml.py @@ -0,0 +1,484 @@ +#!/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 + +__version__='0.19' + +class Unbuffered: + def __init__(self, stream): + self.stream = stream + def write(self, data): + self.stream.write(data) + self.stream.flush() + def __getattr__(self, attr): + return getattr(self.stream, attr) + +import sys +sys.stdout=Unbuffered(sys.stdout) + +import struct, binascii, getopt, zlib, os, os.path, urllib, tempfile + +Des = None +if sys.platform.startswith('win'): + # first try with pycrypto + import pycrypto_des + Des = pycrypto_des.load_pycrypto() + if Des == None: + # they try with openssl + import openssl_des + Des = openssl_des.load_libcrypto() +else: + # first try with openssl + import openssl_des + Des = openssl_des.load_libcrypto() + if Des == None: + # then try with pycrypto + 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 + +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 = '\n' % id + fmarker += zlib.decompress(des.decrypt(self.section_reader(self.first_footnote_page + i))) + fmarker += '\n\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 = '\n' % id + smarker += zlib.decompress(des.decrypt(self.section_reader(self.first_footnote_page + i))) + smarker += '\n\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.exit(main()) + diff --git a/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/flatxml2html.py b/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/flatxml2html.py new file mode 100644 index 0000000..81d93bc --- /dev/null +++ b/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/flatxml2html.py @@ -0,0 +1,706 @@ +#! /usr/bin/python +# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab +# For use with Topaz Scripts Version 2.6 + +import sys +import csv +import os +import math +import getopt +from struct import pack +from struct import unpack + + +class DocParser(object): + def __init__(self, flatxml, classlst, fileid, bookDir, gdict, fixedimage): + self.id = os.path.basename(fileid).replace('.dat','') + self.svgcount = 0 + self.docList = flatxml.split('\n') + self.docSize = len(self.docList) + self.classList = {} + self.bookDir = bookDir + self.gdict = gdict + tmpList = classlst.split('\n') + for pclass in tmpList: + if pclass != '': + # remove the leading period from the css name + cname = pclass[1:] + self.classList[cname] = True + self.fixedimage = fixedimage + self.ocrtext = [] + self.link_id = [] + self.link_title = [] + self.link_page = [] + self.link_href = [] + self.link_type = [] + self.dehyphen_rootid = [] + self.paracont_stemid = [] + self.parastems_stemid = [] + + + def getGlyph(self, gid): + result = '' + id='id="gl%d"' % gid + return self.gdict.lookup(id) + + def glyphs_to_image(self, glyphList): + + def extract(path, key): + b = path.find(key) + len(key) + e = path.find(' ',b) + return int(path[b:e]) + + svgDir = os.path.join(self.bookDir,'svg') + + imgDir = os.path.join(self.bookDir,'img') + imgname = self.id + '_%04d.svg' % self.svgcount + imgfile = os.path.join(imgDir,imgname) + + # get glyph information + gxList = self.getData('info.glyph.x',0,-1) + gyList = self.getData('info.glyph.y',0,-1) + gidList = self.getData('info.glyph.glyphID',0,-1) + + gids = [] + maxws = [] + maxhs = [] + xs = [] + ys = [] + gdefs = [] + + # get path defintions, positions, dimensions for ecah glyph + # that makes up the image, and find min x and min y to reposition origin + minx = -1 + miny = -1 + for j in glyphList: + gid = gidList[j] + gids.append(gid) + + xs.append(gxList[j]) + if minx == -1: minx = gxList[j] + else : minx = min(minx, gxList[j]) + + ys.append(gyList[j]) + if miny == -1: miny = gyList[j] + else : miny = min(miny, gyList[j]) + + path = self.getGlyph(gid) + gdefs.append(path) + + maxws.append(extract(path,'width=')) + maxhs.append(extract(path,'height=')) + + + # change the origin to minx, miny and calc max height and width + maxw = maxws[0] + xs[0] - minx + maxh = maxhs[0] + ys[0] - miny + for j in xrange(0, len(xs)): + xs[j] = xs[j] - minx + ys[j] = ys[j] - miny + maxw = max( maxw, (maxws[j] + xs[j]) ) + maxh = max( maxh, (maxhs[j] + ys[j]) ) + + # open the image file for output + ifile = open(imgfile,'w') + ifile.write('\n') + ifile.write('\n') + ifile.write('\n' % (math.floor(maxw/10), math.floor(maxh/10), maxw, maxh)) + ifile.write('\n') + for j in xrange(0,len(gdefs)): + ifile.write(gdefs[j]) + ifile.write('\n') + for j in xrange(0,len(gids)): + ifile.write('\n' % (gids[j], xs[j], ys[j])) + ifile.write('') + ifile.close() + + return 0 + + + + # return tag at line pos in document + def lineinDoc(self, pos) : + if (pos >= 0) and (pos < self.docSize) : + item = self.docList[pos] + if item.find('=') >= 0: + (name, argres) = item.split('=',1) + else : + name = item + argres = '' + return name, argres + + + # find tag in doc if within pos to end inclusive + def findinDoc(self, tagpath, pos, end) : + result = None + if end == -1 : + end = self.docSize + else: + end = min(self.docSize, end) + foundat = -1 + for j in xrange(pos, end): + item = self.docList[j] + if item.find('=') >= 0: + (name, argres) = item.split('=',1) + else : + name = item + argres = '' + if name.endswith(tagpath) : + result = argres + foundat = j + break + return foundat, result + + + # return list of start positions for the tagpath + def posinDoc(self, tagpath): + startpos = [] + pos = 0 + res = "" + while res != None : + (foundpos, res) = self.findinDoc(tagpath, pos, -1) + if res != None : + startpos.append(foundpos) + pos = foundpos + 1 + return startpos + + + # returns a vector of integers for the tagpath + def getData(self, tagpath, pos, end): + argres=[] + (foundat, argt) = self.findinDoc(tagpath, pos, end) + if (argt != None) and (len(argt) > 0) : + argList = argt.split('|') + argres = [ int(strval) for strval in argList] + return argres + + + # get the class + def getClass(self, pclass): + nclass = pclass + + # class names are an issue given topaz may start them with numerals (not allowed), + # use a mix of cases (which cause some browsers problems), and actually + # attach numbers after "_reclustered*" to the end to deal classeses that inherit + # from a base class (but then not actually provide all of these _reclustereed + # classes in the stylesheet! + + # so we clean this up by lowercasing, prepend 'cl-', and getting any baseclass + # that exists in the stylesheet first, and then adding this specific class + # after + + # also some class names have spaces in them so need to convert to dashes + if nclass != None : + nclass = nclass.replace(' ','-') + classres = '' + nclass = nclass.lower() + nclass = 'cl-' + nclass + baseclass = '' + # graphic is the base class for captions + if nclass.find('cl-cap-') >=0 : + classres = 'graphic' + ' ' + else : + # strip to find baseclass + p = nclass.find('_') + if p > 0 : + baseclass = nclass[0:p] + if baseclass in self.classList: + classres += baseclass + ' ' + classres += nclass + nclass = classres + return nclass + + + # develop a sorted description of the starting positions of + # groups and regions on the page, as well as the page type + def PageDescription(self): + + def compare(x, y): + (xtype, xval) = x + (ytype, yval) = y + if xval > yval: + return 1 + if xval == yval: + return 0 + return -1 + + result = [] + (pos, pagetype) = self.findinDoc('page.type',0,-1) + + groupList = self.posinDoc('page.group') + groupregionList = self.posinDoc('page.group.region') + pageregionList = self.posinDoc('page.region') + # integrate into one list + for j in groupList: + result.append(('grpbeg',j)) + for j in groupregionList: + result.append(('gregion',j)) + for j in pageregionList: + result.append(('pregion',j)) + result.sort(compare) + + # insert group end and page end indicators + inGroup = False + j = 0 + while True: + if j == len(result): break + rtype = result[j][0] + rval = result[j][1] + if not inGroup and (rtype == 'grpbeg') : + inGroup = True + j = j + 1 + elif inGroup and (rtype in ('grpbeg', 'pregion')): + result.insert(j,('grpend',rval)) + inGroup = False + else: + j = j + 1 + if inGroup: + result.append(('grpend',-1)) + result.append(('pageend', -1)) + return pagetype, result + + + + # build a description of the paragraph + def getParaDescription(self, start, end, regtype): + + result = [] + + # paragraph + (pos, pclass) = self.findinDoc('paragraph.class',start,end) + + pclass = self.getClass(pclass) + + # build up a description of the paragraph in result and return it + # first check for the basic - all words paragraph + (pos, sfirst) = self.findinDoc('paragraph.firstWord',start,end) + (pos, slast) = self.findinDoc('paragraph.lastWord',start,end) + if (sfirst != None) and (slast != None) : + first = int(sfirst) + last = int(slast) + + makeImage = (regtype == 'vertical') or (regtype == 'table') + if self.fixedimage: + makeImage = makeImage or (regtype == 'fixed') + + if (pclass != None): + makeImage = makeImage or (pclass.find('.inverted') >= 0) + if self.fixedimage : + makeImage = makeImage or (pclass.find('cl-f-') >= 0) + + if not makeImage : + # standard all word paragraph + for wordnum in xrange(first, last): + result.append(('ocr', wordnum)) + return pclass, result + + # convert paragraph to svg image + # translate first and last word into first and last glyphs + # and generate inline image and include it + glyphList = [] + firstglyphList = self.getData('word.firstGlyph',0,-1) + gidList = self.getData('info.glyph.glyphID',0,-1) + firstGlyph = firstglyphList[first] + if last < len(firstglyphList): + lastGlyph = firstglyphList[last] + else : + lastGlyph = len(gidList) + for glyphnum in xrange(firstGlyph, lastGlyph): + glyphList.append(glyphnum) + # include any extratokens if they exist + (pos, sfg) = self.findinDoc('extratokens.firstGlyph',start,end) + (pos, slg) = self.findinDoc('extratokens.lastGlyph',start,end) + if (sfg != None) and (slg != None): + for glyphnum in xrange(int(sfg), int(slg)): + glyphList.append(glyphnum) + num = self.svgcount + self.glyphs_to_image(glyphList) + self.svgcount += 1 + result.append(('svg', num)) + return pclass, result + + # this type of paragraph may be made up of multiple spans, inline + # word monograms (images), and words with semantic meaning, + # plus glyphs used to form starting letter of first word + + # need to parse this type line by line + line = start + 1 + word_class = '' + + # if end is -1 then we must search to end of document + if end == -1 : + end = self.docSize + + # seems some xml has last* coming before first* so we have to + # handle any order + sp_first = -1 + sp_last = -1 + + gl_first = -1 + gl_last = -1 + + ws_first = -1 + ws_last = -1 + + word_class = '' + + while (line < end) : + + (name, argres) = self.lineinDoc(line) + + if name.endswith('span.firstWord') : + sp_first = int(argres) + + elif name.endswith('span.lastWord') : + sp_last = int(argres) + + elif name.endswith('word.firstGlyph') : + gl_first = int(argres) + + elif name.endswith('word.lastGlyph') : + gl_last = int(argres) + + elif name.endswith('word_semantic.firstWord'): + ws_first = int(argres) + + elif name.endswith('word_semantic.lastWord'): + ws_last = int(argres) + + elif name.endswith('word.class'): + (cname, space) = argres.split('-',1) + if space == '' : space = '0' + if (cname == 'spaceafter') and (int(space) > 0) : + word_class = 'sa' + + elif name.endswith('word.img.src'): + result.append(('img' + word_class, int(argres))) + word_class = '' + + elif name.endswith('region.img.src'): + result.append(('img' + word_class, int(argres))) + + if (sp_first != -1) and (sp_last != -1): + for wordnum in xrange(sp_first, sp_last): + result.append(('ocr', wordnum)) + sp_first = -1 + sp_last = -1 + + if (gl_first != -1) and (gl_last != -1): + glyphList = [] + for glyphnum in xrange(gl_first, gl_last): + glyphList.append(glyphnum) + num = self.svgcount + self.glyphs_to_image(glyphList) + self.svgcount += 1 + result.append(('svg', num)) + gl_first = -1 + gl_last = -1 + + if (ws_first != -1) and (ws_last != -1): + for wordnum in xrange(ws_first, ws_last): + result.append(('ocr', wordnum)) + ws_first = -1 + ws_last = -1 + + line += 1 + + return pclass, result + + + def buildParagraph(self, pclass, pdesc, type, regtype) : + parares = '' + sep ='' + + classres = '' + if pclass : + classres = ' class="' + pclass + '"' + + br_lb = (regtype == 'fixed') or (regtype == 'chapterheading') or (regtype == 'vertical') + + handle_links = len(self.link_id) > 0 + + if (type == 'full') or (type == 'begin') : + parares += '' + + if (type == 'end'): + parares += ' ' + + lstart = len(parares) + + cnt = len(pdesc) + + for j in xrange( 0, cnt) : + + (wtype, num) = pdesc[j] + + if wtype == 'ocr' : + word = self.ocrtext[num] + sep = ' ' + + if handle_links: + link = self.link_id[num] + if (link > 0): + linktype = self.link_type[link-1] + title = self.link_title[link-1] + if (title == "") or (parares.rfind(title) < 0): + title=parares[lstart:] + if linktype == 'external' : + linkhref = self.link_href[link-1] + linkhtml = '' % linkhref + else : + if len(self.link_page) >= link : + ptarget = self.link_page[link-1] - 1 + linkhtml = '' % ptarget + else : + # just link to the current page + linkhtml = '' + linkhtml += title + '' + pos = parares.rfind(title) + if pos >= 0: + parares = parares[0:pos] + linkhtml + parares[pos+len(title):] + else : + parares += linkhtml + lstart = len(parares) + if word == '_link_' : word = '' + elif (link < 0) : + if word == '_link_' : word = '' + + if word == '_lb_': + if ((num-1) in self.dehyphen_rootid ) or handle_links: + word = '' + sep = '' + elif br_lb : + word = '
\n' + sep = '' + else : + word = '\n' + sep = '' + + if num in self.dehyphen_rootid : + word = word[0:-1] + sep = '' + + parares += word + sep + + elif wtype == 'img' : + sep = '' + parares += '' % num + parares += sep + + elif wtype == 'imgsa' : + sep = ' ' + parares += '' % num + parares += sep + + elif wtype == 'svg' : + sep = '' + parares += '' % num + parares += sep + + if len(sep) > 0 : parares = parares[0:-1] + if (type == 'full') or (type == 'end') : + parares += '

' + return parares + + + + # walk the document tree collecting the information needed + # to build an html page using the ocrText + + def process(self): + + htmlpage = '' + + # get the ocr text + (pos, argres) = self.findinDoc('info.word.ocrText',0,-1) + if argres : self.ocrtext = argres.split('|') + + # get information to dehyphenate the text + self.dehyphen_rootid = self.getData('info.dehyphen.rootID',0,-1) + + # determine if first paragraph is continued from previous page + (pos, self.parastems_stemid) = self.findinDoc('info.paraStems.stemID',0,-1) + first_para_continued = (self.parastems_stemid != None) + + # determine if last paragraph is continued onto the next page + (pos, self.paracont_stemid) = self.findinDoc('info.paraCont.stemID',0,-1) + last_para_continued = (self.paracont_stemid != None) + + # collect link ids + self.link_id = self.getData('info.word.link_id',0,-1) + + # collect link destination page numbers + self.link_page = self.getData('info.links.page',0,-1) + + # collect link types (container versus external) + (pos, argres) = self.findinDoc('info.links.type',0,-1) + if argres : self.link_type = argres.split('|') + + # collect link destinations + (pos, argres) = self.findinDoc('info.links.href',0,-1) + if argres : self.link_href = argres.split('|') + + # collect link titles + (pos, argres) = self.findinDoc('info.links.title',0,-1) + if argres : + self.link_title = argres.split('|') + else: + self.link_title.append('') + + # get a descriptions of the starting points of the regions + # and groups on the page + (pagetype, pageDesc) = self.PageDescription() + regcnt = len(pageDesc) - 1 + + anchorSet = False + breakSet = False + inGroup = False + + # process each region on the page and convert what you can to html + + for j in xrange(regcnt): + + (etype, start) = pageDesc[j] + (ntype, end) = pageDesc[j+1] + + + # set anchor for link target on this page + if not anchorSet and not first_para_continued: + htmlpage += '\n' + anchorSet = True + + # handle groups of graphics with text captions + if (etype == 'grpbeg'): + (pos, grptype) = self.findinDoc('group.type', start, end) + if grptype != None: + if grptype == 'graphic': + gcstr = ' class="' + grptype + '"' + htmlpage += '' + inGroup = True + + elif (etype == 'grpend'): + if inGroup: + htmlpage += '\n' + inGroup = False + + else: + (pos, regtype) = self.findinDoc('region.type',start,end) + + if regtype == 'graphic' : + (pos, simgsrc) = self.findinDoc('img.src',start,end) + if simgsrc: + if inGroup: + htmlpage += '' % int(simgsrc) + else: + htmlpage += '
' % int(simgsrc) + + elif regtype == 'chapterheading' : + (pclass, pdesc) = self.getParaDescription(start,end, regtype) + if not breakSet: + htmlpage += '
 
\n' + breakSet = True + tag = 'h1' + if pclass and (len(pclass) >= 7): + if pclass[3:7] == 'ch1-' : tag = 'h1' + if pclass[3:7] == 'ch2-' : tag = 'h2' + if pclass[3:7] == 'ch3-' : tag = 'h3' + htmlpage += '<' + tag + ' class="' + pclass + '">' + else: + htmlpage += '<' + tag + '>' + htmlpage += self.buildParagraph(pclass, pdesc, 'middle', regtype) + htmlpage += '' + + elif (regtype == 'text') or (regtype == 'fixed') or (regtype == 'insert') or (regtype == 'listitem'): + ptype = 'full' + # check to see if this is a continution from the previous page + if first_para_continued : + ptype = 'end' + first_para_continued = False + (pclass, pdesc) = self.getParaDescription(start,end, regtype) + if pclass and (len(pclass) >= 6) and (ptype == 'full'): + tag = 'p' + if pclass[3:6] == 'h1-' : tag = 'h4' + if pclass[3:6] == 'h2-' : tag = 'h5' + if pclass[3:6] == 'h3-' : tag = 'h6' + htmlpage += '<' + tag + ' class="' + pclass + '">' + htmlpage += self.buildParagraph(pclass, pdesc, 'middle', regtype) + htmlpage += '' + else : + htmlpage += self.buildParagraph(pclass, pdesc, ptype, regtype) + + elif (regtype == 'tocentry') : + ptype = 'full' + if first_para_continued : + ptype = 'end' + first_para_continued = False + (pclass, pdesc) = self.getParaDescription(start,end, regtype) + htmlpage += self.buildParagraph(pclass, pdesc, ptype, regtype) + + + elif (regtype == 'vertical') or (regtype == 'table') : + ptype = 'full' + if inGroup: + ptype = 'middle' + if first_para_continued : + ptype = 'end' + first_para_continued = False + (pclass, pdesc) = self.getParaDescription(start, end, regtype) + htmlpage += self.buildParagraph(pclass, pdesc, ptype, regtype) + + + elif (regtype == 'synth_fcvr.center'): + (pos, simgsrc) = self.findinDoc('img.src',start,end) + if simgsrc: + htmlpage += '
' % int(simgsrc) + + else : + print ' Making region type', regtype, + (pos, temp) = self.findinDoc('paragraph',start,end) + (pos2, temp) = self.findinDoc('span',start,end) + if pos != -1 or pos2 != -1: + print ' a "text" region' + orig_regtype = regtype + regtype = 'fixed' + ptype = 'full' + # check to see if this is a continution from the previous page + if first_para_continued : + ptype = 'end' + first_para_continued = False + (pclass, pdesc) = self.getParaDescription(start,end, regtype) + if not pclass: + if orig_regtype.endswith('.right') : pclass = 'cl-right' + elif orig_regtype.endswith('.center') : pclass = 'cl-center' + elif orig_regtype.endswith('.left') : pclass = 'cl-left' + elif orig_regtype.endswith('.justify') : pclass = 'cl-justify' + if pclass and (ptype == 'full') and (len(pclass) >= 6): + tag = 'p' + if pclass[3:6] == 'h1-' : tag = 'h4' + if pclass[3:6] == 'h2-' : tag = 'h5' + if pclass[3:6] == 'h3-' : tag = 'h6' + htmlpage += '<' + tag + ' class="' + pclass + '">' + htmlpage += self.buildParagraph(pclass, pdesc, 'middle', regtype) + htmlpage += '' + else : + htmlpage += self.buildParagraph(pclass, pdesc, ptype, regtype) + else : + print ' a "graphic" region' + (pos, simgsrc) = self.findinDoc('img.src',start,end) + if simgsrc: + htmlpage += '
' % int(simgsrc) + + + if last_para_continued : + if htmlpage[-4:] == '

': + htmlpage = htmlpage[0:-4] + last_para_continued = False + + return htmlpage + + + +def convert2HTML(flatxml, classlst, fileid, bookDir, gdict, fixedimage): + # create a document parser + dp = DocParser(flatxml, classlst, fileid, bookDir, gdict, fixedimage) + htmlpage = dp.process() + return htmlpage diff --git a/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/flatxml2svg.py b/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/flatxml2svg.py new file mode 100644 index 0000000..6f6795d --- /dev/null +++ b/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/flatxml2svg.py @@ -0,0 +1,151 @@ +#! /usr/bin/python +# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab + +import sys +import csv +import os +import getopt +from struct import pack +from struct import unpack + + +class PParser(object): + def __init__(self, gd, flatxml): + self.gd = gd + self.flatdoc = flatxml.split('\n') + self.temp = [] + foo = self.getData('page.h') or self.getData('book.h') + self.ph = foo[0] + foo = self.getData('page.w') or self.getData('book.w') + self.pw = foo[0] + self.gx = self.getData('info.glyph.x') + self.gy = self.getData('info.glyph.y') + self.gid = self.getData('info.glyph.glyphID') + def getData(self, path): + result = None + cnt = len(self.flatdoc) + for j in xrange(cnt): + item = self.flatdoc[j] + if item.find('=') >= 0: + (name, argt) = item.split('=') + argres = argt.split('|') + else: + name = item + argres = [] + if (name.endswith(path)): + result = argres + break + if (len(argres) > 0) : + for j in xrange(0,len(argres)): + argres[j] = int(argres[j]) + return result + def getDataTemp(self, path): + result = None + cnt = len(self.temp) + for j in xrange(cnt): + item = self.temp[j] + if item.find('=') >= 0: + (name, argt) = item.split('=') + argres = argt.split('|') + else: + name = item + argres = [] + if (name.endswith(path)): + result = argres + self.temp.pop(j) + break + if (len(argres) > 0) : + for j in xrange(0,len(argres)): + argres[j] = int(argres[j]) + return result + def getImages(self): + result = [] + self.temp = self.flatdoc + while (self.getDataTemp('img') != None): + h = self.getDataTemp('img.h')[0] + w = self.getDataTemp('img.w')[0] + x = self.getDataTemp('img.x')[0] + y = self.getDataTemp('img.y')[0] + src = self.getDataTemp('img.src')[0] + result.append('\n' % (src, x, y, w, h)) + return result + def getGlyphs(self): + result = [] + if (self.gid != None) and (len(self.gid) > 0): + glyphs = [] + for j in set(self.gid): + glyphs.append(j) + glyphs.sort() + for gid in glyphs: + id='id="gl%d"' % gid + path = self.gd.lookup(id) + if path: + result.append(id + ' ' + path) + return result + + +def convert2SVG(gdict, flat_xml, counter, numfiles, svgDir, raw, meta_array, scaledpi): + ml = '' + pp = PParser(gdict, flat_xml) + ml += '\n' + if (raw): + ml += '\n' + ml += '\n' % (pp.pw / scaledpi, pp.ph / scaledpi, pp.pw -1, pp.ph -1) + ml += 'Page %d - %s by %s\n' % (counter, meta_array['Title'],meta_array['Authors']) + else: + ml += '\n' + ml += '\n' + ml += 'Page %d - %s by %s\n' % (counter, meta_array['Title'],meta_array['Authors']) + ml += '\n' + ml += '\n' + ml += '\n' + ml += '\n' + ml += '\n' + ml += '\n' + ml += '\n' + return ml + diff --git a/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/genbook.py b/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/genbook.py new file mode 100644 index 0000000..a483dec --- /dev/null +++ b/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/genbook.py @@ -0,0 +1,561 @@ +#! /usr/bin/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 csv +import os +import getopt +from struct import pack +from struct import unpack + + +# local support routines +import convert2xml +import flatxml2html +import flatxml2svg +import stylexml2css + + +# Get a 7 bit encoded number from a file +def readEncodedNumber(file): + flag = False + c = file.read(1) + if (len(c) == 0): + return None + data = ord(c) + if data == 0xFF: + flag = True + c = file.read(1) + if (len(c) == 0): + return None + data = ord(c) + if data >= 0x80: + datax = (data & 0x7F) + while data >= 0x80 : + c = file.read(1) + if (len(c) == 0): + return None + data = ord(c) + datax = (datax <<7) + (data & 0x7F) + data = datax + if flag: + data = -data + return data + +# Get a length prefixed string from the file +def lengthPrefixString(data): + return encodeNumber(len(data))+data + +def readString(file): + stringLength = readEncodedNumber(file) + if (stringLength == None): + return None + sv = file.read(stringLength) + if (len(sv) != stringLength): + return "" + return unpack(str(stringLength)+"s",sv)[0] + +def getMetaArray(metaFile): + # parse the meta file + result = {} + fo = file(metaFile,'rb') + size = readEncodedNumber(fo) + for i in xrange(size): + tag = readString(fo) + value = readString(fo) + result[tag] = value + # print tag, value + fo.close() + return result + + +# dictionary of all text strings by index value +class Dictionary(object): + def __init__(self, dictFile): + self.filename = dictFile + self.size = 0 + self.fo = file(dictFile,'rb') + self.stable = [] + self.size = readEncodedNumber(self.fo) + for i in xrange(self.size): + self.stable.append(self.escapestr(readString(self.fo))) + self.pos = 0 + def escapestr(self, str): + str = str.replace('&','&') + str = str.replace('<','<') + str = str.replace('>','>') + str = str.replace('=','=') + return str + def lookup(self,val): + if ((val >= 0) and (val < self.size)) : + self.pos = val + return self.stable[self.pos] + else: + print "Error - %d outside of string table limits" % val + sys.exit(-1) + def getSize(self): + return self.size + def getPos(self): + return self.pos + + +class PageDimParser(object): + def __init__(self, flatxml): + self.flatdoc = flatxml.split('\n') + # find tag if within pos to end inclusive + def findinDoc(self, tagpath, pos, end) : + result = None + docList = self.flatdoc + cnt = len(docList) + if end == -1 : + end = cnt + else: + end = min(cnt,end) + foundat = -1 + for j in xrange(pos, end): + item = docList[j] + if item.find('=') >= 0: + (name, argres) = item.split('=') + else : + name = item + argres = '' + if name.endswith(tagpath) : + result = argres + foundat = j + break + return foundat, result + def process(self): + (pos, sph) = self.findinDoc('page.h',0,-1) + (pos, spw) = self.findinDoc('page.w',0,-1) + if (sph == None): sph = '-1' + if (spw == None): spw = '-1' + return sph, spw + +def getPageDim(flatxml): + # create a document parser + dp = PageDimParser(flatxml) + (ph, pw) = dp.process() + return ph, pw + +class GParser(object): + def __init__(self, flatxml): + self.flatdoc = flatxml.split('\n') + self.dpi = 1440 + self.gh = self.getData('info.glyph.h') + self.gw = self.getData('info.glyph.w') + self.guse = self.getData('info.glyph.use') + if self.guse : + self.count = len(self.guse) + else : + self.count = 0 + self.gvtx = self.getData('info.glyph.vtx') + self.glen = self.getData('info.glyph.len') + self.gdpi = self.getData('info.glyph.dpi') + self.vx = self.getData('info.vtx.x') + self.vy = self.getData('info.vtx.y') + self.vlen = self.getData('info.len.n') + if self.vlen : + self.glen.append(len(self.vlen)) + elif self.glen: + self.glen.append(0) + if self.vx : + self.gvtx.append(len(self.vx)) + elif self.gvtx : + self.gvtx.append(0) + def getData(self, path): + result = None + cnt = len(self.flatdoc) + for j in xrange(cnt): + item = self.flatdoc[j] + if item.find('=') >= 0: + (name, argt) = item.split('=') + argres = argt.split('|') + else: + name = item + argres = [] + if (name == path): + result = argres + break + if (len(argres) > 0) : + for j in xrange(0,len(argres)): + argres[j] = int(argres[j]) + return result + def getGlyphDim(self, gly): + maxh = (self.gh[gly] * self.dpi) / self.gdpi[gly] + maxw = (self.gw[gly] * self.dpi) / self.gdpi[gly] + return maxh, maxw + def getPath(self, gly): + path = '' + if (gly < 0) or (gly >= self.count): + return path + tx = self.vx[self.gvtx[gly]:self.gvtx[gly+1]] + ty = self.vy[self.gvtx[gly]:self.gvtx[gly+1]] + p = 0 + for k in xrange(self.glen[gly], self.glen[gly+1]): + if (p == 0): + zx = tx[0:self.vlen[k]+1] + zy = ty[0:self.vlen[k]+1] + else: + zx = tx[self.vlen[k-1]+1:self.vlen[k]+1] + zy = ty[self.vlen[k-1]+1:self.vlen[k]+1] + p += 1 + j = 0 + while ( j < len(zx) ): + if (j == 0): + # Start Position. + path += 'M %d %d ' % (zx[j] * self.dpi / self.gdpi[gly], zy[j] * self.dpi / self.gdpi[gly]) + elif (j <= len(zx)-3): + # Cubic Bezier Curve + path += 'C %d %d %d %d %d %d ' % (zx[j] * self.dpi / self.gdpi[gly], zy[j] * self.dpi / self.gdpi[gly], zx[j+1] * self.dpi / self.gdpi[gly], zy[j+1] * self.dpi / self.gdpi[gly], zx[j+2] * self.dpi / self.gdpi[gly], zy[j+2] * self.dpi / self.gdpi[gly]) + j += 2 + elif (j == len(zx)-2): + # Cubic Bezier Curve to Start Position + path += 'C %d %d %d %d %d %d ' % (zx[j] * self.dpi / self.gdpi[gly], zy[j] * self.dpi / self.gdpi[gly], zx[j+1] * self.dpi / self.gdpi[gly], zy[j+1] * self.dpi / self.gdpi[gly], zx[0] * self.dpi / self.gdpi[gly], zy[0] * self.dpi / self.gdpi[gly]) + j += 1 + elif (j == len(zx)-1): + # Quadratic Bezier Curve to Start Position + path += 'Q %d %d %d %d ' % (zx[j] * self.dpi / self.gdpi[gly], zy[j] * self.dpi / self.gdpi[gly], zx[0] * self.dpi / self.gdpi[gly], zy[0] * self.dpi / self.gdpi[gly]) + + j += 1 + path += 'z' + return path + + + +# dictionary of all text strings by index value +class GlyphDict(object): + def __init__(self): + self.gdict = {} + def lookup(self, id): + # id='id="gl%d"' % val + if id in self.gdict: + return self.gdict[id] + return None + def addGlyph(self, val, path): + id='id="gl%d"' % val + self.gdict[id] = path + + +def generateBook(bookDir, raw, fixedimage): + # sanity check Topaz file extraction + if not os.path.exists(bookDir) : + print "Can not find directory with unencrypted book" + return 1 + + dictFile = os.path.join(bookDir,'dict0000.dat') + if not os.path.exists(dictFile) : + print "Can not find dict0000.dat file" + return 1 + + pageDir = os.path.join(bookDir,'page') + if not os.path.exists(pageDir) : + print "Can not find page directory in unencrypted book" + return 1 + + imgDir = os.path.join(bookDir,'img') + if not os.path.exists(imgDir) : + print "Can not find image directory in unencrypted book" + return 1 + + glyphsDir = os.path.join(bookDir,'glyphs') + if not os.path.exists(glyphsDir) : + print "Can not find glyphs directory in unencrypted book" + return 1 + + metaFile = os.path.join(bookDir,'metadata0000.dat') + if not os.path.exists(metaFile) : + print "Can not find metadata0000.dat in unencrypted book" + return 1 + + svgDir = os.path.join(bookDir,'svg') + if not os.path.exists(svgDir) : + os.makedirs(svgDir) + + xmlDir = os.path.join(bookDir,'xml') + if not os.path.exists(xmlDir) : + os.makedirs(xmlDir) + + otherFile = os.path.join(bookDir,'other0000.dat') + if not os.path.exists(otherFile) : + print "Can not find other0000.dat in unencrypted book" + return 1 + + print "Updating to color images if available" + spath = os.path.join(bookDir,'color_img') + dpath = os.path.join(bookDir,'img') + filenames = os.listdir(spath) + filenames = sorted(filenames) + for filename in filenames: + imgname = filename.replace('color','img') + sfile = os.path.join(spath,filename) + dfile = os.path.join(dpath,imgname) + imgdata = file(sfile,'rb').read() + file(dfile,'wb').write(imgdata) + + print "Creating cover.jpg" + isCover = False + cpath = os.path.join(bookDir,'img') + cpath = os.path.join(cpath,'img0000.jpg') + if os.path.isfile(cpath): + cover = file(cpath, 'rb').read() + cpath = os.path.join(bookDir,'cover.jpg') + file(cpath, 'wb').write(cover) + isCover = True + + + print 'Processing Dictionary' + dict = Dictionary(dictFile) + + print 'Processing Meta Data and creating OPF' + meta_array = getMetaArray(metaFile) + + xname = os.path.join(xmlDir, 'metadata.xml') + metastr = '' + for key in meta_array: + metastr += '\n' + file(xname, 'wb').write(metastr) + + print 'Processing StyleSheet' + # get some scaling info from metadata to use while processing styles + fontsize = '135' + if 'fontSize' in meta_array: + fontsize = meta_array['fontSize'] + + # also get the size of a normal text page + spage = '1' + if 'firstTextPage' in meta_array: + spage = meta_array['firstTextPage'] + pnum = int(spage) + + # get page height and width from first text page for use in stylesheet scaling + pname = 'page%04d.dat' % (pnum + 1) + fname = os.path.join(pageDir,pname) + flat_xml = convert2xml.fromData(dict, fname) + + (ph, pw) = getPageDim(flat_xml) + if (ph == '-1') or (ph == '0') : ph = '11000' + if (pw == '-1') or (pw == '0') : pw = '8500' + + # print ' ', 'other0000.dat' + xname = os.path.join(bookDir, 'style.css') + flat_xml = convert2xml.fromData(dict, otherFile) + cssstr , classlst = stylexml2css.convert2CSS(flat_xml, fontsize, ph, pw) + file(xname, 'wb').write(cssstr) + xname = os.path.join(xmlDir, 'other0000.xml') + file(xname, 'wb').write(convert2xml.getXML(dict, otherFile)) + + print 'Processing Glyphs' + gd = GlyphDict() + filenames = os.listdir(glyphsDir) + filenames = sorted(filenames) + glyfname = os.path.join(svgDir,'glyphs.svg') + glyfile = open(glyfname, 'w') + glyfile.write('\n') + glyfile.write('\n') + glyfile.write('\n') + glyfile.write('Glyphs for %s\n' % meta_array['Title']) + glyfile.write('\n') + counter = 0 + for filename in filenames: + # print ' ', filename + print '.', + fname = os.path.join(glyphsDir,filename) + flat_xml = convert2xml.fromData(dict, fname) + + xname = os.path.join(xmlDir, filename.replace('.dat','.xml')) + file(xname, 'wb').write(convert2xml.getXML(dict, fname)) + + gp = GParser(flat_xml) + for i in xrange(0, gp.count): + path = gp.getPath(i) + maxh, maxw = gp.getGlyphDim(i) + fullpath = '\n' % (counter * 256 + i, path, maxw, maxh) + glyfile.write(fullpath) + gd.addGlyph(counter * 256 + i, fullpath) + counter += 1 + glyfile.write('\n') + glyfile.write('\n') + glyfile.close() + print " " + + # start up the html + htmlFileName = "book.html" + htmlstr = '\n' + htmlstr += '\n' + htmlstr += '\n' + htmlstr += '\n' + htmlstr += '\n' + htmlstr += '' + meta_array['Title'] + ' by ' + meta_array['Authors'] + '\n' + htmlstr += '\n' + htmlstr += '\n' + htmlstr += '\n' + htmlstr += '\n' + htmlstr += '\n' + htmlstr += '\n\n' + + print 'Processing Pages' + # Books are at 1440 DPI. This is rendering at twice that size for + # readability when rendering to the screen. + scaledpi = 1440.0 + + svgindex = '\n' + svgindex += '\n' + svgindex += '' + svgindex += '\n' + svgindex += '' + meta_array['Title'] + '\n' + svgindex += '\n' + svgindex += '\n' + svgindex += '\n' + svgindex += '\n' + svgindex += '\n' + svgindex += '\n' + + filenames = os.listdir(pageDir) + filenames = sorted(filenames) + numfiles = len(filenames) + counter = 0 + + for filename in filenames: + # print ' ', filename + print ".", + + fname = os.path.join(pageDir,filename) + flat_xml = convert2xml.fromData(dict, fname) + + xname = os.path.join(xmlDir, filename.replace('.dat','.xml')) + file(xname, 'wb').write(convert2xml.getXML(dict, fname)) + + # first get the html + htmlstr += flatxml2html.convert2HTML(flat_xml, classlst, fname, bookDir, gd, fixedimage) + + # now get the svg image of the page + svgxml = flatxml2svg.convert2SVG(gd, flat_xml, counter, numfiles, svgDir, raw, meta_array, scaledpi) + + if (raw) : + pfile = open(os.path.join(svgDir,filename.replace('.dat','.svg')), 'w') + svgindex += 'Page %d\n' % (counter, counter) + else : + pfile = open(os.path.join(svgDir,'page%04d.xhtml' % counter), 'w') + svgindex += 'Page %d\n' % (counter, counter) + + + pfile.write(svgxml) + pfile.close() + + counter += 1 + + print " " + + # finish up the html string and output it + htmlstr += '\n\n' + file(os.path.join(bookDir, htmlFileName), 'wb').write(htmlstr) + + # finish up the svg index string and output it + svgindex += '\n\n' + file(os.path.join(bookDir, 'index_svg.xhtml'), 'wb').write(svgindex) + + # build the opf file + opfname = os.path.join(bookDir, 'book.opf') + opfstr = '\n' + opfstr += '\n' + # adding metadata + opfstr += ' \n' + opfstr += ' ' + meta_array['GUID'] + '\n' + opfstr += ' ' + meta_array['ASIN'] + '\n' + opfstr += ' ' + meta_array['oASIN'] + '\n' + opfstr += ' ' + meta_array['Title'] + '\n' + opfstr += ' ' + meta_array['Authors'] + '\n' + opfstr += ' en\n' + opfstr += ' ' + meta_array['UpdateTime'] + '\n' + if isCover: + opfstr += ' \n' + opfstr += ' \n' + opfstr += '\n' + opfstr += ' \n' + opfstr += ' \n' + # adding image files to manifest + filenames = os.listdir(imgDir) + filenames = sorted(filenames) + for filename in filenames: + imgname, imgext = os.path.splitext(filename) + if imgext == '.jpg': + imgext = 'jpeg' + if imgext == '.svg': + imgext = 'svg+xml' + opfstr += ' \n' + if isCover: + opfstr += ' \n' + opfstr += '\n' + # adding spine + opfstr += '\n \n\n' + if isCover: + opfstr += ' \n' + opfstr += ' \n' + opfstr += ' \n' + opfstr += '\n' + file(opfname, 'wb').write(opfstr) + + print 'Processing Complete' + + return 0 + +def usage(): + print "genbook.py generates a book from the extract Topaz Files" + print "Usage:" + print " genbook.py [-r] [-h [--fixed-image] " + print " " + print "Options:" + print " -h : help - print this usage message" + print " -r : generate raw svg files (not wrapped in xhtml)" + print " --fixed-image : genearate any Fixed Area as an svg image in the html" + print " " + + +def main(argv): + bookDir = '' + + if len(argv) == 0: + argv = sys.argv + + try: + opts, args = getopt.getopt(argv[1:], "rh:",["fixed-image"]) + + except getopt.GetoptError, err: + print str(err) + usage() + return 1 + + if len(opts) == 0 and len(args) == 0 : + usage() + return 1 + + raw = 0 + fixedimage = False + for o, a in opts: + if o =="-h": + usage() + return 0 + if o =="-r": + raw = 1 + if o =="--fixed-image": + fixedimage = True + + bookDir = args[0] + + rv = generateBook(bookDir, raw, fixedimage) + return rv + + +if __name__ == '__main__': + sys.exit(main('')) diff --git a/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/ignobleepub.py b/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/ignobleepub.py new file mode 100644 index 0000000..a7c48c9 --- /dev/null +++ b/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/ignobleepub.py @@ -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 +# 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()) diff --git a/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/ignoblekeygen.py b/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/ignoblekeygen.py new file mode 100644 index 0000000..cdedc48 --- /dev/null +++ b/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/ignoblekeygen.py @@ -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 +# 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()) diff --git a/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/ineptepub.py b/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/ineptepub.py new file mode 100644 index 0000000..48a75f9 --- /dev/null +++ b/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/ineptepub.py @@ -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. + +# Windows users: Before running this program, you must first install Python 2.6 +# from and PyCrypto from +# (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()) diff --git a/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/ineptkey.py b/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/ineptkey.py new file mode 100644 index 0000000..8eab14f --- /dev/null +++ b/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/ineptkey.py @@ -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. + +# Windows users: Before running this program, you must first install Python 2.6 +# from and PyCrypto from +# (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()) diff --git a/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/ineptpdf.py b/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/ineptpdf.py new file mode 100644 index 0000000..ccdd9e4 --- /dev/null +++ b/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/ineptpdf.py @@ -0,0 +1,2228 @@ +#! /usr/bin/env python +# ineptpdf.pyw, version 7.7 + +from __future__ import with_statement + +# To run this program install Python 2.6 from http://www.python.org/download/ +# and OpenSSL (already installed on Mac OS X and Linux) OR +# PyCrypto from http://www.voidspace.org.uk/python/modules.shtml#pycrypto +# (make sure to install the version for Python 2.6). Save this script file as +# ineptpdf.pyw and double-click on it to run it. + +# Revision history: +# 1 - Initial release +# 2 - Improved determination of key-generation algorithm +# 3 - Correctly handle PDF >=1.5 cross-reference streams +# 4 - Removal of ciando's personal ID +# 5 - Automated decryption of a complete directory +# 6.1 - backward compatibility for 1.7.1 and old adeptkey.der +# 7 - Get cross reference streams and object streams working for input. +# Not yet supported on output but this only effects file size, +# not functionality. (anon2) +# 7.1 - Correct a problem when an old trailer is not followed by startxref +# 7.2 - Correct malformed Mac OS resource forks for Stanza (anon2) +# - Support for cross ref streams on output (decreases file size) +# 7.3 - Correct bug in trailer with cross ref stream that caused the error +# "The root object is missing or invalid" in Adobe Reader. (anon2) +# 7.4 - Force all generation numbers in output file to be 0, like in v6. +# Fallback code for wrong xref improved (search till last trailer +# instead of first) (anon2) +# 7.5 - allow support for OpenSSL to replace pycrypto on all platforms +# implemented ARC4 interface to OpenSSL +# fixed minor typos +# 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 + +""" +Decrypts Adobe ADEPT-encrypted PDF files. +""" + +__license__ = 'GPL v3' + +import sys +import os +import re +import zlib +import struct +import hashlib +from itertools import chain, islice +import xml.etree.ElementTree as etree +import Tkinter +import Tkconstants +import tkFileDialog +import tkMessageBox + +class ADEPTError(Exception): + pass + + +import hashlib + +def SHA256(message): + ctx = hashlib.sha256() + ctx.update(message) + return ctx.digest() + + +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) + + AES_MAXNR = 14 + + RSA_NO_PADDING = 3 + + 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) + + class RC4_KEY(Structure): + _fields_ = [('x', c_int), ('y', c_int), ('box', c_int * 256)] + RC4_KEY_p = POINTER(RC4_KEY) + + class RSA(Structure): + pass + RSA_p = POINTER(RSA) + + 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]) + + RC4_set_key = F(None,'RC4_set_key',[RC4_KEY_p, c_int, c_char_p]) + RC4_crypt = F(None,'RC4',[RC4_KEY_p, c_int, c_char_p, c_char_p]) + + 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]) + + 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[1:dlen] + + def __del__(self): + if self._rsa is not None: + RSA_free(self._rsa) + self._rsa = None + + class ARC4(object): + @classmethod + def new(cls, userkey): + self = ARC4() + self._blocksize = len(userkey) + key = self._key = RC4_KEY() + RC4_set_key(key, self._blocksize, userkey) + return self + def __init__(self): + self._blocksize = 0 + self._key = None + def decrypt(self, data): + out = create_string_buffer(len(data)) + RC4_crypt(self._key, len(data), data, out) + return out.raw + + class AES(object): + @classmethod + def new(cls, userkey, mode, iv): + self = AES() + self._blocksize = len(userkey) + # mode is ignored since CBCMODE is only thing supported/used so far + self._mode = mode + if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) : + raise ADEPTError('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 ADEPTError('Failed to initialize AES key') + return self + def __init__(self): + self._blocksize = 0 + self._keyctx = None + self._iv = 0 + self._mode = 0 + 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 ADEPTError('AES decryption failed') + return out.raw + + return (ARC4, RSA, AES) + + +def _load_crypto_pycrypto(): + from Crypto.PublicKey import RSA as _RSA + from Crypto.Cipher import ARC4 as _ARC4 + from Crypto.Cipher import AES as _AES + + # 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 ARC4(object): + @classmethod + def new(cls, userkey): + self = ARC4() + self._arc4 = _ARC4.new(userkey) + return self + def __init__(self): + self._arc4 = None + def decrypt(self, data): + return self._arc4.decrypt(data) + + class AES(object): + @classmethod + def new(cls, userkey, mode, iv): + self = AES() + self._aes = _AES.new(userkey, mode, iv) + return self + def __init__(self): + self._aes = None + 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 (ARC4, RSA, AES) + +def _load_crypto(): + ARC4 = RSA = 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: + ARC4, RSA, AES = loader() + break + except (ImportError, ADEPTError): + pass + return (ARC4, RSA, AES) +ARC4, RSA, AES = _load_crypto() + + +try: + from cStringIO import StringIO +except ImportError: + from StringIO import StringIO + + +# Do we generate cross reference streams on output? +# 0 = never +# 1 = only if present in input +# 2 = always + +GEN_XREF_STM = 1 + +# This is the value for the current document +gen_xref_stm = False # will be set in PDFSerializer + +# PDF parsing routines from pdfminer, with changes for EBX_HANDLER + +# Utilities + +def choplist(n, seq): + '''Groups every n elements of the list.''' + r = [] + for x in seq: + r.append(x) + if len(r) == n: + yield tuple(r) + r = [] + return + +def nunpack(s, default=0): + '''Unpacks up to 4 bytes big endian.''' + l = len(s) + if not l: + return default + elif l == 1: + return ord(s) + elif l == 2: + return struct.unpack('>H', s)[0] + elif l == 3: + return struct.unpack('>L', '\x00'+s)[0] + elif l == 4: + return struct.unpack('>L', s)[0] + else: + return TypeError('invalid length: %d' % l) + + +STRICT = 0 + + +# PS Exceptions + +class PSException(Exception): pass +class PSEOF(PSException): pass +class PSSyntaxError(PSException): pass +class PSTypeError(PSException): pass +class PSValueError(PSException): pass + + +# Basic PostScript Types + + +# PSLiteral +class PSObject(object): pass + +class PSLiteral(PSObject): + ''' + PS literals (e.g. "/Name"). + Caution: Never create these objects directly. + Use PSLiteralTable.intern() instead. + ''' + def __init__(self, name): + self.name = name + return + + def __repr__(self): + name = [] + for char in self.name: + if not char.isalnum(): + char = '#%02x' % ord(char) + name.append(char) + return '/%s' % ''.join(name) + +# PSKeyword +class PSKeyword(PSObject): + ''' + PS keywords (e.g. "showpage"). + Caution: Never create these objects directly. + Use PSKeywordTable.intern() instead. + ''' + def __init__(self, name): + self.name = name + return + + def __repr__(self): + return self.name + +# PSSymbolTable +class PSSymbolTable(object): + + ''' + Symbol table that stores PSLiteral or PSKeyword. + ''' + + def __init__(self, classe): + self.dic = {} + self.classe = classe + return + + def intern(self, name): + if name in self.dic: + lit = self.dic[name] + else: + lit = self.classe(name) + self.dic[name] = lit + return lit + +PSLiteralTable = PSSymbolTable(PSLiteral) +PSKeywordTable = PSSymbolTable(PSKeyword) +LIT = PSLiteralTable.intern +KWD = PSKeywordTable.intern +KEYWORD_BRACE_BEGIN = KWD('{') +KEYWORD_BRACE_END = KWD('}') +KEYWORD_ARRAY_BEGIN = KWD('[') +KEYWORD_ARRAY_END = KWD(']') +KEYWORD_DICT_BEGIN = KWD('<<') +KEYWORD_DICT_END = KWD('>>') + + +def literal_name(x): + if not isinstance(x, PSLiteral): + if STRICT: + raise PSTypeError('Literal required: %r' % x) + else: + return str(x) + return x.name + +def keyword_name(x): + if not isinstance(x, PSKeyword): + if STRICT: + raise PSTypeError('Keyword required: %r' % x) + else: + return str(x) + return x.name + + +## PSBaseParser +## +EOL = re.compile(r'[\r\n]') +SPC = re.compile(r'\s') +NONSPC = re.compile(r'\S') +HEX = re.compile(r'[0-9a-fA-F]') +END_LITERAL = re.compile(r'[#/%\[\]()<>{}\s]') +END_HEX_STRING = re.compile(r'[^\s0-9a-fA-F]') +HEX_PAIR = re.compile(r'[0-9a-fA-F]{2}|.') +END_NUMBER = re.compile(r'[^0-9]') +END_KEYWORD = re.compile(r'[#/%\[\]()<>{}\s]') +END_STRING = re.compile(r'[()\134]') +OCT_STRING = re.compile(r'[0-7]') +ESC_STRING = { 'b':8, 't':9, 'n':10, 'f':12, 'r':13, '(':40, ')':41, '\\':92 } + +class PSBaseParser(object): + + ''' + Most basic PostScript parser that performs only basic tokenization. + ''' + BUFSIZ = 4096 + + def __init__(self, fp): + self.fp = fp + self.seek(0) + return + + def __repr__(self): + return '' % (self.fp, self.bufpos) + + def flush(self): + return + + def close(self): + self.flush() + return + + def tell(self): + return self.bufpos+self.charpos + + def poll(self, pos=None, n=80): + pos0 = self.fp.tell() + if not pos: + pos = self.bufpos+self.charpos + self.fp.seek(pos) + ##print >>sys.stderr, 'poll(%d): %r' % (pos, self.fp.read(n)) + self.fp.seek(pos0) + return + + def seek(self, pos): + ''' + Seeks the parser to the given position. + ''' + self.fp.seek(pos) + # reset the status for nextline() + self.bufpos = pos + self.buf = '' + self.charpos = 0 + # reset the status for nexttoken() + self.parse1 = self.parse_main + self.tokens = [] + return + + def fillbuf(self): + if self.charpos < len(self.buf): return + # fetch next chunk. + self.bufpos = self.fp.tell() + self.buf = self.fp.read(self.BUFSIZ) + if not self.buf: + raise PSEOF('Unexpected EOF') + self.charpos = 0 + return + + def parse_main(self, s, i): + m = NONSPC.search(s, i) + if not m: + return (self.parse_main, len(s)) + j = m.start(0) + c = s[j] + self.tokenstart = self.bufpos+j + if c == '%': + self.token = '%' + return (self.parse_comment, j+1) + if c == '/': + self.token = '' + return (self.parse_literal, j+1) + if c in '-+' or c.isdigit(): + self.token = c + return (self.parse_number, j+1) + if c == '.': + self.token = c + return (self.parse_float, j+1) + if c.isalpha(): + self.token = c + return (self.parse_keyword, j+1) + if c == '(': + self.token = '' + self.paren = 1 + return (self.parse_string, j+1) + if c == '<': + self.token = '' + return (self.parse_wopen, j+1) + if c == '>': + self.token = '' + return (self.parse_wclose, j+1) + self.add_token(KWD(c)) + return (self.parse_main, j+1) + + def add_token(self, obj): + self.tokens.append((self.tokenstart, obj)) + return + + def parse_comment(self, s, i): + m = EOL.search(s, i) + if not m: + self.token += s[i:] + return (self.parse_comment, len(s)) + j = m.start(0) + self.token += s[i:j] + # We ignore comments. + #self.tokens.append(self.token) + return (self.parse_main, j) + + def parse_literal(self, s, i): + m = END_LITERAL.search(s, i) + if not m: + self.token += s[i:] + return (self.parse_literal, len(s)) + j = m.start(0) + self.token += s[i:j] + c = s[j] + if c == '#': + self.hex = '' + return (self.parse_literal_hex, j+1) + self.add_token(LIT(self.token)) + return (self.parse_main, j) + + def parse_literal_hex(self, s, i): + c = s[i] + if HEX.match(c) and len(self.hex) < 2: + self.hex += c + return (self.parse_literal_hex, i+1) + if self.hex: + self.token += chr(int(self.hex, 16)) + return (self.parse_literal, i) + + def parse_number(self, s, i): + m = END_NUMBER.search(s, i) + if not m: + self.token += s[i:] + return (self.parse_number, len(s)) + j = m.start(0) + self.token += s[i:j] + c = s[j] + if c == '.': + self.token += c + return (self.parse_float, j+1) + try: + self.add_token(int(self.token)) + except ValueError: + pass + return (self.parse_main, j) + def parse_float(self, s, i): + m = END_NUMBER.search(s, i) + if not m: + self.token += s[i:] + return (self.parse_float, len(s)) + j = m.start(0) + self.token += s[i:j] + self.add_token(float(self.token)) + return (self.parse_main, j) + + def parse_keyword(self, s, i): + m = END_KEYWORD.search(s, i) + if not m: + self.token += s[i:] + return (self.parse_keyword, len(s)) + j = m.start(0) + self.token += s[i:j] + if self.token == 'true': + token = True + elif self.token == 'false': + token = False + else: + token = KWD(self.token) + self.add_token(token) + return (self.parse_main, j) + + def parse_string(self, s, i): + m = END_STRING.search(s, i) + if not m: + self.token += s[i:] + return (self.parse_string, len(s)) + j = m.start(0) + self.token += s[i:j] + c = s[j] + if c == '\\': + self.oct = '' + return (self.parse_string_1, j+1) + if c == '(': + self.paren += 1 + self.token += c + return (self.parse_string, j+1) + if c == ')': + self.paren -= 1 + if self.paren: + self.token += c + return (self.parse_string, j+1) + self.add_token(self.token) + return (self.parse_main, j+1) + def parse_string_1(self, s, i): + c = s[i] + if OCT_STRING.match(c) and len(self.oct) < 3: + self.oct += c + return (self.parse_string_1, i+1) + if self.oct: + self.token += chr(int(self.oct, 8)) + return (self.parse_string, i) + if c in ESC_STRING: + self.token += chr(ESC_STRING[c]) + return (self.parse_string, i+1) + + def parse_wopen(self, s, i): + c = s[i] + if c.isspace() or HEX.match(c): + return (self.parse_hexstring, i) + if c == '<': + self.add_token(KEYWORD_DICT_BEGIN) + i += 1 + return (self.parse_main, i) + + def parse_wclose(self, s, i): + c = s[i] + if c == '>': + self.add_token(KEYWORD_DICT_END) + i += 1 + return (self.parse_main, i) + + def parse_hexstring(self, s, i): + m = END_HEX_STRING.search(s, i) + if not m: + self.token += s[i:] + return (self.parse_hexstring, len(s)) + j = m.start(0) + self.token += s[i:j] + token = HEX_PAIR.sub(lambda m: chr(int(m.group(0), 16)), + SPC.sub('', self.token)) + self.add_token(token) + return (self.parse_main, j) + + def nexttoken(self): + while not self.tokens: + self.fillbuf() + (self.parse1, self.charpos) = self.parse1(self.buf, self.charpos) + token = self.tokens.pop(0) + return token + + def nextline(self): + ''' + Fetches a next line that ends either with \\r or \\n. + ''' + linebuf = '' + linepos = self.bufpos + self.charpos + eol = False + while 1: + self.fillbuf() + if eol: + c = self.buf[self.charpos] + # handle '\r\n' + if c == '\n': + linebuf += c + self.charpos += 1 + break + m = EOL.search(self.buf, self.charpos) + if m: + linebuf += self.buf[self.charpos:m.end(0)] + self.charpos = m.end(0) + if linebuf[-1] == '\r': + eol = True + else: + break + else: + linebuf += self.buf[self.charpos:] + self.charpos = len(self.buf) + return (linepos, linebuf) + + def revreadlines(self): + ''' + Fetches a next line backword. This is used to locate + the trailers at the end of a file. + ''' + self.fp.seek(0, 2) + pos = self.fp.tell() + buf = '' + while 0 < pos: + prevpos = pos + pos = max(0, pos-self.BUFSIZ) + self.fp.seek(pos) + s = self.fp.read(prevpos-pos) + if not s: break + while 1: + n = max(s.rfind('\r'), s.rfind('\n')) + if n == -1: + buf = s + buf + break + yield s[n:]+buf + s = s[:n] + buf = '' + return + + +## PSStackParser +## +class PSStackParser(PSBaseParser): + + def __init__(self, fp): + PSBaseParser.__init__(self, fp) + self.reset() + return + + def reset(self): + self.context = [] + self.curtype = None + self.curstack = [] + self.results = [] + return + + def seek(self, pos): + PSBaseParser.seek(self, pos) + self.reset() + return + + def push(self, *objs): + self.curstack.extend(objs) + return + def pop(self, n): + objs = self.curstack[-n:] + self.curstack[-n:] = [] + return objs + def popall(self): + objs = self.curstack + self.curstack = [] + return objs + def add_results(self, *objs): + self.results.extend(objs) + return + + def start_type(self, pos, type): + self.context.append((pos, self.curtype, self.curstack)) + (self.curtype, self.curstack) = (type, []) + return + def end_type(self, type): + if self.curtype != type: + raise PSTypeError('Type mismatch: %r != %r' % (self.curtype, type)) + objs = [ obj for (_,obj) in self.curstack ] + (pos, self.curtype, self.curstack) = self.context.pop() + return (pos, objs) + + def do_keyword(self, pos, token): + return + + def nextobject(self, direct=False): + ''' + Yields a list of objects: keywords, literals, strings, + numbers, arrays and dictionaries. Arrays and dictionaries + are represented as Python sequence and dictionaries. + ''' + while not self.results: + (pos, token) = self.nexttoken() + ##print (pos,token), (self.curtype, self.curstack) + if (isinstance(token, int) or + isinstance(token, float) or + isinstance(token, bool) or + isinstance(token, str) or + isinstance(token, PSLiteral)): + # normal token + self.push((pos, token)) + elif token == KEYWORD_ARRAY_BEGIN: + # begin array + self.start_type(pos, 'a') + elif token == KEYWORD_ARRAY_END: + # end array + try: + self.push(self.end_type('a')) + except PSTypeError: + if STRICT: raise + elif token == KEYWORD_DICT_BEGIN: + # begin dictionary + self.start_type(pos, 'd') + elif token == KEYWORD_DICT_END: + # end dictionary + try: + (pos, objs) = self.end_type('d') + if len(objs) % 2 != 0: + raise PSSyntaxError( + 'Invalid dictionary construct: %r' % objs) + d = dict((literal_name(k), v) \ + for (k,v) in choplist(2, objs)) + self.push((pos, d)) + except PSTypeError: + if STRICT: raise + else: + self.do_keyword(pos, token) + if self.context: + continue + else: + if direct: + return self.pop(1)[0] + self.flush() + obj = self.results.pop(0) + return obj + + +LITERAL_CRYPT = PSLiteralTable.intern('Crypt') +LITERALS_FLATE_DECODE = (PSLiteralTable.intern('FlateDecode'), PSLiteralTable.intern('Fl')) +LITERALS_LZW_DECODE = (PSLiteralTable.intern('LZWDecode'), PSLiteralTable.intern('LZW')) +LITERALS_ASCII85_DECODE = (PSLiteralTable.intern('ASCII85Decode'), PSLiteralTable.intern('A85')) + + +## PDF Objects +## +class PDFObject(PSObject): pass + +class PDFException(PSException): pass +class PDFTypeError(PDFException): pass +class PDFValueError(PDFException): pass +class PDFNotImplementedError(PSException): pass + + +## PDFObjRef +## +class PDFObjRef(PDFObject): + + def __init__(self, doc, objid, genno): + if objid == 0: + if STRICT: + raise PDFValueError('PDF object id cannot be 0.') + self.doc = doc + self.objid = objid + self.genno = genno + return + + def __repr__(self): + return '' % (self.objid, self.genno) + + def resolve(self): + return self.doc.getobj(self.objid) + + +# resolve +def resolve1(x): + ''' + Resolve an object. If this is an array or dictionary, + it may still contains some indirect objects inside. + ''' + while isinstance(x, PDFObjRef): + x = x.resolve() + return x + +def resolve_all(x): + ''' + Recursively resolve X and all the internals. + Make sure there is no indirect reference within the nested object. + This procedure might be slow. + ''' + while isinstance(x, PDFObjRef): + x = x.resolve() + if isinstance(x, list): + x = [ resolve_all(v) for v in x ] + elif isinstance(x, dict): + for (k,v) in x.iteritems(): + x[k] = resolve_all(v) + return x + +def decipher_all(decipher, objid, genno, x): + ''' + Recursively decipher X. + ''' + if isinstance(x, str): + return decipher(objid, genno, x) + decf = lambda v: decipher_all(decipher, objid, genno, v) + if isinstance(x, list): + x = [decf(v) for v in x] + elif isinstance(x, dict): + x = dict((k, decf(v)) for (k, v) in x.iteritems()) + return x + + +# Type cheking +def int_value(x): + x = resolve1(x) + if not isinstance(x, int): + if STRICT: + raise PDFTypeError('Integer required: %r' % x) + return 0 + return x + +def float_value(x): + x = resolve1(x) + if not isinstance(x, float): + if STRICT: + raise PDFTypeError('Float required: %r' % x) + return 0.0 + return x + +def num_value(x): + x = resolve1(x) + if not (isinstance(x, int) or isinstance(x, float)): + if STRICT: + raise PDFTypeError('Int or Float required: %r' % x) + return 0 + return x + +def str_value(x): + x = resolve1(x) + if not isinstance(x, str): + if STRICT: + raise PDFTypeError('String required: %r' % x) + return '' + return x + +def list_value(x): + x = resolve1(x) + if not (isinstance(x, list) or isinstance(x, tuple)): + if STRICT: + raise PDFTypeError('List required: %r' % x) + return [] + return x + +def dict_value(x): + x = resolve1(x) + if not isinstance(x, dict): + if STRICT: + raise PDFTypeError('Dict required: %r' % x) + return {} + return x + +def stream_value(x): + x = resolve1(x) + if not isinstance(x, PDFStream): + if STRICT: + raise PDFTypeError('PDFStream required: %r' % x) + return PDFStream({}, '') + return x + +# ascii85decode(data) +def ascii85decode(data): + n = b = 0 + out = '' + for c in data: + if '!' <= c and c <= 'u': + n += 1 + b = b*85+(ord(c)-33) + if n == 5: + out += struct.pack('>L',b) + n = b = 0 + elif c == 'z': + assert n == 0 + out += '\0\0\0\0' + elif c == '~': + if n: + for _ in range(5-n): + b = b*85+84 + out += struct.pack('>L',b)[:n-1] + break + return out + + +## PDFStream type +class PDFStream(PDFObject): + def __init__(self, dic, rawdata, decipher=None): + length = int_value(dic.get('Length', 0)) + eol = rawdata[length:] + # quick and dirty fix for false length attribute, + # might not work if the pdf stream parser has a problem + if decipher != None and decipher.__name__ == 'decrypt_aes': + if (len(rawdata) % 16) != 0: + cutdiv = len(rawdata) // 16 + rawdata = rawdata[:16*cutdiv] + else: + if eol in ('\r', '\n', '\r\n'): + rawdata = rawdata[:length] + + self.dic = dic + self.rawdata = rawdata + self.decipher = decipher + self.data = None + self.decdata = None + self.objid = None + self.genno = None + return + + def set_objid(self, objid, genno): + self.objid = objid + self.genno = genno + return + + def __repr__(self): + if self.rawdata: + return '' % \ + (self.objid, len(self.rawdata), self.dic) + else: + return '' % \ + (self.objid, len(self.data), self.dic) + + def decode(self): + assert self.data is None and self.rawdata is not None + data = self.rawdata + if self.decipher: + # Handle encryption + data = self.decipher(self.objid, self.genno, data) + if gen_xref_stm: + self.decdata = data # keep decrypted data + if 'Filter' not in self.dic: + self.data = data + self.rawdata = None + ##print self.dict + return + filters = self.dic['Filter'] + if not isinstance(filters, list): + filters = [ filters ] + for f in filters: + if f in LITERALS_FLATE_DECODE: + # will get errors if the document is encrypted. + data = zlib.decompress(data) + elif f in LITERALS_LZW_DECODE: + data = ''.join(LZWDecoder(StringIO(data)).run()) + elif f in LITERALS_ASCII85_DECODE: + data = ascii85decode(data) + elif f == LITERAL_CRYPT: + raise PDFNotImplementedError('/Crypt filter is unsupported') + else: + raise PDFNotImplementedError('Unsupported filter: %r' % f) + # apply predictors + if 'DP' in self.dic: + params = self.dic['DP'] + else: + params = self.dic.get('DecodeParms', {}) + if 'Predictor' in params: + pred = int_value(params['Predictor']) + if pred: + if pred != 12: + raise PDFNotImplementedError( + 'Unsupported predictor: %r' % pred) + if 'Columns' not in params: + raise PDFValueError( + 'Columns undefined for predictor=12') + columns = int_value(params['Columns']) + buf = '' + ent0 = '\x00' * columns + for i in xrange(0, len(data), columns+1): + pred = data[i] + ent1 = data[i+1:i+1+columns] + if pred == '\x02': + ent1 = ''.join(chr((ord(a)+ord(b)) & 255) \ + for (a,b) in zip(ent0,ent1)) + buf += ent1 + ent0 = ent1 + data = buf + self.data = data + self.rawdata = None + return + + def get_data(self): + if self.data is None: + self.decode() + return self.data + + def get_rawdata(self): + return self.rawdata + + def get_decdata(self): + if self.decdata is not None: + return self.decdata + data = self.rawdata + if self.decipher and data: + # Handle encryption + data = self.decipher(self.objid, self.genno, data) + return data + + +## PDF Exceptions +## +class PDFSyntaxError(PDFException): pass +class PDFNoValidXRef(PDFSyntaxError): pass +class PDFEncryptionError(PDFException): pass +class PDFPasswordIncorrect(PDFEncryptionError): pass + +# some predefined literals and keywords. +LITERAL_OBJSTM = PSLiteralTable.intern('ObjStm') +LITERAL_XREF = PSLiteralTable.intern('XRef') +LITERAL_PAGE = PSLiteralTable.intern('Page') +LITERAL_PAGES = PSLiteralTable.intern('Pages') +LITERAL_CATALOG = PSLiteralTable.intern('Catalog') + + +## XRefs +## + +## PDFXRef +## +class PDFXRef(object): + + def __init__(self): + self.offsets = None + return + + def __repr__(self): + return '' % len(self.offsets) + + def objids(self): + return self.offsets.iterkeys() + + def load(self, parser): + self.offsets = {} + while 1: + try: + (pos, line) = parser.nextline() + except PSEOF: + raise PDFNoValidXRef('Unexpected EOF - file corrupted?') + if not line: + raise PDFNoValidXRef('Premature eof: %r' % parser) + if line.startswith('trailer'): + parser.seek(pos) + break + f = line.strip().split(' ') + if len(f) != 2: + raise PDFNoValidXRef('Trailer not found: %r: line=%r' % (parser, line)) + try: + (start, nobjs) = map(int, f) + except ValueError: + raise PDFNoValidXRef('Invalid line: %r: line=%r' % (parser, line)) + for objid in xrange(start, start+nobjs): + try: + (_, line) = parser.nextline() + except PSEOF: + raise PDFNoValidXRef('Unexpected EOF - file corrupted?') + f = line.strip().split(' ') + if len(f) != 3: + raise PDFNoValidXRef('Invalid XRef format: %r, line=%r' % (parser, line)) + (pos, genno, use) = f + if use != 'n': continue + self.offsets[objid] = (int(genno), int(pos)) + self.load_trailer(parser) + return + + KEYWORD_TRAILER = PSKeywordTable.intern('trailer') + def load_trailer(self, parser): + try: + (_,kwd) = parser.nexttoken() + assert kwd is self.KEYWORD_TRAILER + (_,dic) = parser.nextobject(direct=True) + except PSEOF: + x = parser.pop(1) + if not x: + raise PDFNoValidXRef('Unexpected EOF - file corrupted') + (_,dic) = x[0] + self.trailer = dict_value(dic) + return + + def getpos(self, objid): + try: + (genno, pos) = self.offsets[objid] + except KeyError: + raise + return (None, pos) + + +## PDFXRefStream +## +class PDFXRefStream(object): + + def __init__(self): + self.index = None + self.data = None + self.entlen = None + self.fl1 = self.fl2 = self.fl3 = None + return + + def __repr__(self): + return '' % self.index + + def objids(self): + for first, size in self.index: + for objid in xrange(first, first + size): + yield objid + + def load(self, parser, debug=0): + (_,objid) = parser.nexttoken() # ignored + (_,genno) = parser.nexttoken() # ignored + (_,kwd) = parser.nexttoken() + (_,stream) = parser.nextobject() + if not isinstance(stream, PDFStream) or \ + stream.dic['Type'] is not LITERAL_XREF: + raise PDFNoValidXRef('Invalid PDF stream spec.') + size = stream.dic['Size'] + index = stream.dic.get('Index', (0,size)) + self.index = zip(islice(index, 0, None, 2), + islice(index, 1, None, 2)) + (self.fl1, self.fl2, self.fl3) = stream.dic['W'] + self.data = stream.get_data() + self.entlen = self.fl1+self.fl2+self.fl3 + self.trailer = stream.dic + return + + def getpos(self, objid): + offset = 0 + for first, size in self.index: + if first <= objid and objid < (first + size): + break + offset += size + else: + raise KeyError(objid) + i = self.entlen * ((objid - first) + offset) + ent = self.data[i:i+self.entlen] + f1 = nunpack(ent[:self.fl1], 1) + if f1 == 1: + pos = nunpack(ent[self.fl1:self.fl1+self.fl2]) + genno = nunpack(ent[self.fl1+self.fl2:]) + return (None, pos) + elif f1 == 2: + objid = nunpack(ent[self.fl1:self.fl1+self.fl2]) + index = nunpack(ent[self.fl1+self.fl2:]) + return (objid, index) + # this is a free object + raise KeyError(objid) + + +## PDFDocument +## +## A PDFDocument object represents a PDF document. +## Since a PDF file is usually pretty big, normally it is not loaded +## at once. Rather it is parsed dynamically as processing goes. +## A PDF parser is associated with the document. +## +class PDFDocument(object): + + def __init__(self): + self.xrefs = [] + self.objs = {} + self.parsed_objs = {} + self.root = None + self.catalog = None + self.parser = None + self.encryption = None + self.decipher = None + return + + # set_parser(parser) + # Associates the document with an (already initialized) parser object. + def set_parser(self, parser): + if self.parser: return + self.parser = parser + # The document is set to be temporarily ready during collecting + # all the basic information about the document, e.g. + # the header, the encryption information, and the access rights + # for the document. + self.ready = True + # Retrieve the information of each header that was appended + # (maybe multiple times) at the end of the document. + self.xrefs = parser.read_xref() + for xref in self.xrefs: + trailer = xref.trailer + if not trailer: continue + + # If there's an encryption info, remember it. + if 'Encrypt' in trailer: + #assert not self.encryption + try: + self.encryption = (list_value(trailer['ID']), + dict_value(trailer['Encrypt'])) + # fix for bad files + except: + self.encryption = ('ffffffffffffffffffffffffffffffffffff', + dict_value(trailer['Encrypt'])) + if 'Root' in trailer: + self.set_root(dict_value(trailer['Root'])) + break + else: + raise PDFSyntaxError('No /Root object! - Is this really a PDF?') + # The document is set to be non-ready again, until all the + # proper initialization (asking the password key and + # verifying the access permission, so on) is finished. + self.ready = False + return + + # set_root(root) + # Set the Root dictionary of the document. + # Each PDF file must have exactly one /Root dictionary. + def set_root(self, root): + self.root = root + self.catalog = dict_value(self.root) + if self.catalog.get('Type') is not LITERAL_CATALOG: + if STRICT: + raise PDFSyntaxError('Catalog not found!') + return + # initialize(password='') + # Perform the initialization with a given password. + # This step is mandatory even if there's no password associated + # with the document. + def initialize(self, password=''): + if not self.encryption: + self.is_printable = self.is_modifiable = self.is_extractable = True + self.ready = True + return + (docid, param) = self.encryption + type = literal_name(param['Filter']) + if type == 'Adobe.APS': + return self.initialize_adobe_ps(password, docid, param) + if type == 'Standard': + return self.initialize_standard(password, docid, param) + if type == 'EBX_HANDLER': + return self.initialize_ebx(password, docid, param) + raise PDFEncryptionError('Unknown filter: param=%r' % param) + + def initialize_adobe_ps(self, password, docid, param): + global KEYFILEPATH + self.decrypt_key = self.genkey_adobe_ps(param) + self.genkey = self.genkey_v4 + self.decipher = self.decrypt_aes + self.ready = True + return + + def genkey_adobe_ps(self, param): + # nice little offline principal keys dictionary + # global static principal key for German Onleihe / Bibliothek Digital + principalkeys = { 'bibliothek-digital.de': 'rRwGv2tbpKov1krvv7PO0ws9S436/lArPlfipz5Pqhw='.decode('base64')} + self.is_printable = self.is_modifiable = self.is_extractable = True + length = int_value(param.get('Length', 0)) / 8 + edcdata = str_value(param.get('EDCData')).decode('base64') + pdrllic = str_value(param.get('PDRLLic')).decode('base64') + pdrlpol = str_value(param.get('PDRLPol')).decode('base64') + edclist = [] + for pair in edcdata.split('\n'): + edclist.append(pair) + # principal key request + for key in principalkeys: + if key in pdrllic: + principalkey = principalkeys[key] + else: + raise ADEPTError('Cannot find principal key for this pdf') + shakey = SHA256(principalkey) + ivector = 16 * chr(0) + plaintext = AES.new(shakey,AES.MODE_CBC,ivector).decrypt(edclist[9].decode('base64')) + if plaintext[-16:] != 16 * chr(16): + raise ADEPTError('Offlinekey cannot be decrypted, aborting ...') + pdrlpol = AES.new(plaintext[16:32],AES.MODE_CBC,edclist[2].decode('base64')).decrypt(pdrlpol) + if ord(pdrlpol[-1]) < 1 or ord(pdrlpol[-1]) > 16: + raise ADEPTError('Could not decrypt PDRLPol, aborting ...') + else: + cutter = -1 * ord(pdrlpol[-1]) + pdrlpol = pdrlpol[:cutter] + return plaintext[:16] + + PASSWORD_PADDING = '(\xbfN^Nu\x8aAd\x00NV\xff\xfa\x01\x08..' \ + '\x00\xb6\xd0h>\x80/\x0c\xa9\xfedSiz' + # experimental aes pw support + def initialize_standard(self, password, docid, param): + # copy from a global variable + V = int_value(param.get('V', 0)) + if (V <=0 or V > 4): + raise PDFEncryptionError('Unknown algorithm: param=%r' % param) + length = int_value(param.get('Length', 40)) # Key length (bits) + O = str_value(param['O']) + R = int_value(param['R']) # Revision + if 5 <= R: + raise PDFEncryptionError('Unknown revision: %r' % R) + U = str_value(param['U']) + P = int_value(param['P']) + try: + EncMetadata = str_value(param['EncryptMetadata']) + except: + EncMetadata = 'True' + self.is_printable = bool(P & 4) + self.is_modifiable = bool(P & 8) + self.is_extractable = bool(P & 16) + self.is_annotationable = bool(P & 32) + self.is_formsenabled = bool(P & 256) + self.is_textextractable = bool(P & 512) + self.is_assemblable = bool(P & 1024) + self.is_formprintable = bool(P & 2048) + # Algorithm 3.2 + password = (password+self.PASSWORD_PADDING)[:32] # 1 + hash = hashlib.md5(password) # 2 + hash.update(O) # 3 + hash.update(struct.pack('= 3: + # Algorithm 3.5 + hash = hashlib.md5(self.PASSWORD_PADDING) # 2 + hash.update(docid[0]) # 3 + x = ARC4.new(key).decrypt(hash.digest()[:16]) # 4 + for i in xrange(1,19+1): + k = ''.join( chr(ord(c) ^ i) for c in key ) + x = ARC4.new(k).decrypt(x) + u1 = x+x # 32bytes total + if R == 2: + is_authenticated = (u1 == U) + else: + is_authenticated = (u1[:16] == U[:16]) + if not is_authenticated: + raise ADEPTError('Password is not correct.') + self.decrypt_key = key + # genkey method + if V == 1 or V == 2: + self.genkey = self.genkey_v2 + elif V == 3: + self.genkey = self.genkey_v3 + elif V == 4: + self.genkey = self.genkey_v2 + #self.genkey = self.genkey_v3 if V == 3 else self.genkey_v2 + # rc4 + if V != 4: + self.decipher = self.decipher_rc4 # XXX may be AES + # aes + elif V == 4 and Length == 128: + elf.decipher = self.decipher_aes + elif V == 4 and Length == 256: + raise PDFNotImplementedError('AES256 encryption is currently unsupported') + self.ready = True + return + + def initialize_ebx(self, password, docid, param): + self.is_printable = self.is_modifiable = self.is_extractable = True + with open(password, 'rb') as f: + keyder = f.read() + rsa = RSA(keyder) + length = int_value(param.get('Length', 0)) / 8 + rights = str_value(param.get('ADEPT_LICENSE')).decode('base64') + rights = zlib.decompress(rights, -15) + rights = etree.fromstring(rights) + expr = './/{http://ns.adobe.com/adept}encryptedKey' + bookkey = ''.join(rights.findtext(expr)).decode('base64') + bookkey = rsa.decrypt(bookkey) + if bookkey[0] != '\x02': + raise ADEPTError('error decrypting book session key') + index = bookkey.index('\0') + 1 + bookkey = bookkey[index:] + ebx_V = int_value(param.get('V', 4)) + ebx_type = int_value(param.get('EBX_ENCRYPTIONTYPE', 6)) + # added because of the booktype / decryption book session key error + if ebx_V == 3: + V = 3 + elif ebx_V < 4 or ebx_type < 6: + V = ord(bookkey[0]) + bookkey = bookkey[1:] + else: + V = 2 + if length and len(bookkey) != length: + raise ADEPTError('error decrypting book session key') + self.decrypt_key = bookkey + self.genkey = self.genkey_v3 if V == 3 else self.genkey_v2 + self.decipher = self.decrypt_rc4 + self.ready = True + return + + # genkey functions + def genkey_v2(self, objid, genno): + objid = struct.pack(' PDFObjStmRef.maxindex: + PDFObjStmRef.maxindex = index + + +## PDFParser +## +class PDFParser(PSStackParser): + + def __init__(self, doc, fp): + PSStackParser.__init__(self, fp) + self.doc = doc + self.doc.set_parser(self) + return + + def __repr__(self): + return '' + + KEYWORD_R = PSKeywordTable.intern('R') + KEYWORD_ENDOBJ = PSKeywordTable.intern('endobj') + KEYWORD_STREAM = PSKeywordTable.intern('stream') + KEYWORD_XREF = PSKeywordTable.intern('xref') + KEYWORD_STARTXREF = PSKeywordTable.intern('startxref') + def do_keyword(self, pos, token): + if token in (self.KEYWORD_XREF, self.KEYWORD_STARTXREF): + self.add_results(*self.pop(1)) + return + if token is self.KEYWORD_ENDOBJ: + self.add_results(*self.pop(4)) + return + + if token is self.KEYWORD_R: + # reference to indirect object + try: + ((_,objid), (_,genno)) = self.pop(2) + (objid, genno) = (int(objid), int(genno)) + obj = PDFObjRef(self.doc, objid, genno) + self.push((pos, obj)) + except PSSyntaxError: + pass + return + + if token is self.KEYWORD_STREAM: + # stream object + ((_,dic),) = self.pop(1) + dic = dict_value(dic) + try: + objlen = int_value(dic['Length']) + except KeyError: + if STRICT: + raise PDFSyntaxError('/Length is undefined: %r' % dic) + objlen = 0 + self.seek(pos) + try: + (_, line) = self.nextline() # 'stream' + except PSEOF: + if STRICT: + raise PDFSyntaxError('Unexpected EOF') + return + pos += len(line) + self.fp.seek(pos) + data = self.fp.read(objlen) + self.seek(pos+objlen) + while 1: + try: + (linepos, line) = self.nextline() + except PSEOF: + if STRICT: + raise PDFSyntaxError('Unexpected EOF') + break + if 'endstream' in line: + i = line.index('endstream') + objlen += i + data += line[:i] + break + objlen += len(line) + data += line + self.seek(pos+objlen) + obj = PDFStream(dic, data, self.doc.decipher) + self.push((pos, obj)) + return + + # others + self.push((pos, token)) + return + + def find_xref(self): + # search the last xref table by scanning the file backwards. + prev = None + for line in self.revreadlines(): + line = line.strip() + if line == 'startxref': break + if line: + prev = line + else: + raise PDFNoValidXRef('Unexpected EOF') + return int(prev) + + # read xref table + def read_xref_from(self, start, xrefs): + self.seek(start) + self.reset() + try: + (pos, token) = self.nexttoken() + except PSEOF: + raise PDFNoValidXRef('Unexpected EOF') + if isinstance(token, int): + # XRefStream: PDF-1.5 + if GEN_XREF_STM == 1: + global gen_xref_stm + gen_xref_stm = True + self.seek(pos) + self.reset() + xref = PDFXRefStream() + xref.load(self) + else: + if token is not self.KEYWORD_XREF: + raise PDFNoValidXRef('xref not found: pos=%d, token=%r' % + (pos, token)) + self.nextline() + xref = PDFXRef() + xref.load(self) + xrefs.append(xref) + trailer = xref.trailer + if 'XRefStm' in trailer: + pos = int_value(trailer['XRefStm']) + self.read_xref_from(pos, xrefs) + if 'Prev' in trailer: + # find previous xref + pos = int_value(trailer['Prev']) + self.read_xref_from(pos, xrefs) + return + + # read xref tables and trailers + def read_xref(self): + xrefs = [] + trailerpos = None + try: + pos = self.find_xref() + self.read_xref_from(pos, xrefs) + except PDFNoValidXRef: + # fallback + self.seek(0) + pat = re.compile(r'^(\d+)\s+(\d+)\s+obj\b') + offsets = {} + xref = PDFXRef() + while 1: + try: + (pos, line) = self.nextline() + except PSEOF: + break + if line.startswith('trailer'): + trailerpos = pos # remember last trailer + m = pat.match(line) + if not m: continue + (objid, genno) = m.groups() + offsets[int(objid)] = (0, pos) + if not offsets: raise + xref.offsets = offsets + if trailerpos: + self.seek(trailerpos) + xref.load_trailer(self) + xrefs.append(xref) + return xrefs + +## PDFObjStrmParser +## +class PDFObjStrmParser(PDFParser): + + def __init__(self, data, doc): + PSStackParser.__init__(self, StringIO(data)) + self.doc = doc + return + + def flush(self): + self.add_results(*self.popall()) + return + + KEYWORD_R = KWD('R') + def do_keyword(self, pos, token): + if token is self.KEYWORD_R: + # reference to indirect object + try: + ((_,objid), (_,genno)) = self.pop(2) + (objid, genno) = (int(objid), int(genno)) + obj = PDFObjRef(self.doc, objid, genno) + self.push((pos, obj)) + except PSSyntaxError: + pass + return + # others + self.push((pos, token)) + return + +### +### My own code, for which there is none else to blame + +class PDFSerializer(object): + def __init__(self, inf, keypath): + global GEN_XREF_STM, gen_xref_stm + gen_xref_stm = GEN_XREF_STM > 1 + self.version = inf.read(8) + inf.seek(0) + self.doc = doc = PDFDocument() + parser = PDFParser(doc, inf) + doc.initialize(keypath) + self.objids = objids = set() + for xref in reversed(doc.xrefs): + trailer = xref.trailer + for objid in xref.objids(): + objids.add(objid) + trailer = dict(trailer) + trailer.pop('Prev', None) + trailer.pop('XRefStm', None) + if 'Encrypt' in trailer: + objids.remove(trailer.pop('Encrypt').objid) + self.trailer = trailer + + def dump(self, outf): + self.outf = outf + self.write(self.version) + self.write('\n%\xe2\xe3\xcf\xd3\n') + doc = self.doc + objids = self.objids + xrefs = {} + maxobj = max(objids) + trailer = dict(self.trailer) + trailer['Size'] = maxobj + 1 + for objid in objids: + obj = doc.getobj(objid) + if isinstance(obj, PDFObjStmRef): + xrefs[objid] = obj + continue + if obj is not None: + try: + genno = obj.genno + except AttributeError: + genno = 0 + xrefs[objid] = (self.tell(), genno) + self.serialize_indirect(objid, obj) + startxref = self.tell() + + if not gen_xref_stm: + self.write('xref\n') + self.write('0 %d\n' % (maxobj + 1,)) + for objid in xrange(0, maxobj + 1): + if objid in xrefs: + # force the genno to be 0 + self.write("%010d 00000 n \n" % xrefs[objid][0]) + else: + self.write("%010d %05d f \n" % (0, 65535)) + + self.write('trailer\n') + self.serialize_object(trailer) + self.write('\nstartxref\n%d\n%%%%EOF' % startxref) + + else: # Generate crossref stream. + + # Calculate size of entries + maxoffset = max(startxref, maxobj) + maxindex = PDFObjStmRef.maxindex + fl2 = 2 + power = 65536 + while maxoffset >= power: + fl2 += 1 + power *= 256 + fl3 = 1 + power = 256 + while maxindex >= power: + fl3 += 1 + power *= 256 + + index = [] + first = None + prev = None + data = [] + # Put the xrefstream's reference in itself + startxref = self.tell() + maxobj += 1 + xrefs[maxobj] = (startxref, 0) + for objid in sorted(xrefs): + if first is None: + first = objid + elif objid != prev + 1: + index.extend((first, prev - first + 1)) + first = objid + prev = objid + objref = xrefs[objid] + if isinstance(objref, PDFObjStmRef): + f1 = 2 + f2 = objref.stmid + f3 = objref.index + else: + f1 = 1 + f2 = objref[0] + # we force all generation numbers to be 0 + # f3 = objref[1] + f3 = 0 + + data.append(struct.pack('>B', f1)) + data.append(struct.pack('>L', f2)[-fl2:]) + data.append(struct.pack('>L', f3)[-fl3:]) + index.extend((first, prev - first + 1)) + data = zlib.compress(''.join(data)) + dic = {'Type': LITERAL_XREF, 'Size': prev + 1, 'Index': index, + 'W': [1, fl2, fl3], 'Length': len(data), + 'Filter': LITERALS_FLATE_DECODE[0], + 'Root': trailer['Root'],} + if 'Info' in trailer: + dic['Info'] = trailer['Info'] + xrefstm = PDFStream(dic, data) + self.serialize_indirect(maxobj, xrefstm) + self.write('startxref\n%d\n%%%%EOF' % startxref) + def write(self, data): + self.outf.write(data) + self.last = data[-1:] + + def tell(self): + return self.outf.tell() + + def escape_string(self, string): + string = string.replace('\\', '\\\\') + string = string.replace('\n', r'\n') + string = string.replace('(', r'\(') + string = string.replace(')', r'\)') + # get rid of ciando id + regularexp = re.compile(r'http://www.ciando.com/index.cfm/intRefererID/\d{5}') + if regularexp.match(string): return ('http://www.ciando.com') + return string + + def serialize_object(self, obj): + if isinstance(obj, dict): + # Correct malformed Mac OS resource forks for Stanza + if 'ResFork' in obj and 'Type' in obj and 'Subtype' not in obj \ + and isinstance(obj['Type'], int): + obj['Subtype'] = obj['Type'] + del obj['Type'] + # end - hope this doesn't have bad effects + self.write('<<') + for key, val in obj.items(): + self.write('/%s' % key) + self.serialize_object(val) + self.write('>>') + elif isinstance(obj, list): + self.write('[') + for val in obj: + self.serialize_object(val) + self.write(']') + elif isinstance(obj, str): + self.write('(%s)' % self.escape_string(obj)) + elif isinstance(obj, bool): + if self.last.isalnum(): + self.write(' ') + self.write(str(obj).lower()) + elif isinstance(obj, (int, long, float)): + if self.last.isalnum(): + self.write(' ') + self.write(str(obj)) + elif isinstance(obj, PDFObjRef): + if self.last.isalnum(): + self.write(' ') + self.write('%d %d R' % (obj.objid, 0)) + elif isinstance(obj, PDFStream): + ### If we don't generate cross ref streams the object streams + ### are no longer useful, as we have extracted all objects from + ### them. Therefore leave them out from the output. + if obj.dic.get('Type') == LITERAL_OBJSTM and not gen_xref_stm: + self.write('(deleted)') + else: + data = obj.get_decdata() + self.serialize_object(obj.dic) + self.write('stream\n') + self.write(data) + self.write('\nendstream') + else: + data = str(obj) + if data[0].isalnum() and self.last.isalnum(): + self.write(' ') + self.write(data) + + def serialize_indirect(self, objid, obj): + self.write('%d 0 obj' % (objid,)) + self.serialize_object(obj) + if self.last.isalnum(): + self.write('\n') + self.write('endobj\n') + + +class DecryptionDialog(Tkinter.Frame): + def __init__(self, root): + Tkinter.Frame.__init__(self, root, border=5) + ltext='Select file for decryption\n' + self.status = Tkinter.Label(self, text=ltext) + self.status.pack(fill=Tkconstants.X, expand=1) + body = Tkinter.Frame(self) + body.pack(fill=Tkconstants.X, expand=1) + sticky = Tkconstants.E + Tkconstants.W + body.grid_columnconfigure(1, weight=2) + Tkinter.Label(body, text='Key file').grid(row=0) + self.keypath = Tkinter.Entry(body, width=30) + self.keypath.grid(row=0, column=1, sticky=sticky) + if os.path.exists('adeptkey.der'): + self.keypath.insert(0, 'adeptkey.der') + button = Tkinter.Button(body, text="...", command=self.get_keypath) + button.grid(row=0, column=2) + Tkinter.Label(body, text='Input file').grid(row=1) + self.inpath = Tkinter.Entry(body, width=30) + self.inpath.grid(row=1, column=1, sticky=sticky) + button = Tkinter.Button(body, text="...", command=self.get_inpath) + button.grid(row=1, column=2) + Tkinter.Label(body, text='Output file').grid(row=2) + self.outpath = Tkinter.Entry(body, width=30) + self.outpath.grid(row=2, column=1, sticky=sticky) + button = Tkinter.Button(body, text="...", command=self.get_outpath) + button.grid(row=2, column=2) + buttons = Tkinter.Frame(self) + buttons.pack() + + + botton = Tkinter.Button( + buttons, text="Decrypt", width=10, command=self.decrypt) + botton.pack(side=Tkconstants.LEFT) + Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT) + button = Tkinter.Button( + buttons, text="Quit", width=10, command=self.quit) + button.pack(side=Tkconstants.RIGHT) + + + def get_keypath(self): + keypath = tkFileDialog.askopenfilename( + parent=None, title='Select ADEPT key file', + defaultextension='.der', filetypes=[('DER-encoded files', '.der'), + ('All Files', '.*')]) + if keypath: + keypath = os.path.normpath(os.path.realpath(keypath)) + self.keypath.delete(0, Tkconstants.END) + self.keypath.insert(0, keypath) + return + + def get_inpath(self): + inpath = tkFileDialog.askopenfilename( + parent=None, title='Select ADEPT encrypted PDF file to decrypt', + defaultextension='.pdf', filetypes=[('PDF files', '.pdf'), + ('All files', '.*')]) + if inpath: + inpath = os.path.normpath(os.path.realpath(inpath)) + self.inpath.delete(0, Tkconstants.END) + self.inpath.insert(0, inpath) + return + + def get_outpath(self): + outpath = tkFileDialog.asksaveasfilename( + parent=None, title='Select unencrypted PDF file to produce', + defaultextension='.pdf', filetypes=[('PDF files', '.pdf'), + ('All files', '.*')]) + if outpath: + outpath = os.path.normpath(os.path.realpath(outpath)) + self.outpath.delete(0, Tkconstants.END) + self.outpath.insert(0, outpath) + return + + def decrypt(self): + keypath = self.keypath.get() + inpath = self.inpath.get() + outpath = self.outpath.get() + if not keypath or not os.path.exists(keypath): + # keyfile doesn't exist + self.status['text'] = 'Specified Adept key file does not exist' + return + if not inpath or not os.path.exists(inpath): + self.status['text'] = 'Specified input file does not exist' + return + if not outpath: + self.status['text'] = 'Output file not specified' + return + if inpath == outpath: + self.status['text'] = 'Must have different input and output files' + return + # patch for non-ascii characters + argv = [sys.argv[0], keypath, inpath, outpath] + self.status['text'] = 'Processing ...' + try: + cli_main(argv) + except Exception, a: + self.status['text'] = 'Error: ' + str(a) + return + self.status['text'] = 'File successfully decrypted.\n'+\ + 'Close this window or decrypt another pdf file.' + return + + +def decryptBook(keypath, inpath, outpath): + 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(): + root = Tkinter.Tk() + if RSA is None: + root.withdraw() + tkMessageBox.showerror( + "INEPT PDF", + "This script requires OpenSSL or PyCrypto, which must be installed " + "separately. Read the top-of-script comment for details.") + return 1 + root.title('INEPT PDF Decrypter') + root.resizable(True, False) + root.minsize(370, 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()) diff --git a/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/k4mobidedrm.py b/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/k4mobidedrm.py new file mode 100644 index 0000000..880690f --- /dev/null +++ b/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/k4mobidedrm.py @@ -0,0 +1,367 @@ +#!/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 + +# 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. + + +__version__ = '2.1' + +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 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) + +# cleanup bytestring filenames +# borrowed from calibre from calibre/src/calibre/__init__.py +# added in removal of non-printing chars +# and removal of . at start +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:] + return one + +def decryptBook(infile, outdir, k4, kInfoFiles, serials, pids): + import mobidedrm + import topazextract + import kgenpids + + # 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 + 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: + if mobi: + unlocked_file = mb.processBook(pidlst) + else: + mb.processBook(pidlst) + + except mobidedrm.DrmException, e: + print "Error: " + str(e) + "\nDRM Removal Failed.\n" + return 1 + except Exception, e: + if not mobi: + print "Error: " + str(e) + "\nDRM Removal Failed.\n" + 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() + shutil.rmtree(tempdir, True) + return 1 + pass + + if mobi: + outfile = os.path.join(outdir,outfilename + '_nodrm' + '.mobi') + 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, outfilename + '_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, outfilename + '_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, outfilename + '_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, True) + return 0 + + +def usage(progname): + print "Removes DRM protection from K4PC/M, Kindle, Mobi and Topaz ebooks" + print "Usage:" + print " %s [-k ] [-p ] [-s ] " % 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 + 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()) + +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, 2, 1) # 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.' diff --git a/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/k4mutils.py b/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/k4mutils.py new file mode 100644 index 0000000..1b501ba --- /dev/null +++ b/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/k4mutils.py @@ -0,0 +1,194 @@ +# standlone set of Mac OSX specific routines needed for K4DeDRM + +from __future__ import with_statement +import sys +import os +import subprocess + + +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 = '16743' + saltlen = 5 + passlen = len(passwd) + iter = 0x3e8 + keylen = 80 + 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 +# + + +# Various character maps used to decrypt books. Probably supposed to act as obfuscation +charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M" +charMap2 = "ZB0bYyc1xDdW2wEV3Ff7KkPpL8UuGA4gz-Tme9Nn_tHh5SvXCsIiR6rJjQaqlOoM" +charMap3 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" +charMap4 = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789" + + + +# 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 = '9999999999' + return sernum + +# uses unix env to get username instead of using sysctlbyname +def GetUserName(): + username = os.getenv('USER') + 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 +def CryptUnprotectData(encryptedData): + sp = GetVolumeSerialNumber() + '!@#' + GetUserName() + passwdData = encode(SHA256(sp),charMap1) + crp = LibCrypto() + key_iv = crp.keyivgen(passwdData) + key = key_iv[0:32] + iv = key_iv[32:48] + crp.set_decrypt_key(key,iv) + cleartext = crp.decrypt(encryptedData) + return cleartext + + +# Locate and open the .kindle-info file +def openKindleInfo(kInfoFile=None): + if kInfoFile == None: + 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' + cnt = len(reslst) + for j in xrange(cnt): + resline = reslst[j] + pp = resline.find('.kindle-info') + if pp >= 0: + kinfopath = resline + break + if not os.path.isfile(kinfopath): + raise DrmException('Error: .kindle-info file can not be found') + return open(kinfopath,'r') + else: + if not os.path.isfile(kinfoFile): + raise DrmException('Error: kindle-info file can not be found') + return open(kInfoFile, 'r') diff --git a/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/k4pcutils.py b/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/k4pcutils.py new file mode 100644 index 0000000..efc310d --- /dev/null +++ b/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/k4pcutils.py @@ -0,0 +1,110 @@ +# K4PC Windows specific routines + +from __future__ import with_statement + +import sys, os + +from ctypes import windll, c_char_p, c_wchar_p, c_uint, POINTER, byref, \ + create_unicode_buffer, create_string_buffer, CFUNCTYPE, addressof, \ + string_at, Structure, c_void_p, cast + +import _winreg as winreg + +import traceback + +MAX_PATH = 255 + +kernel32 = windll.kernel32 +advapi32 = windll.advapi32 +crypt32 = windll.crypt32 + + +# Various character maps used to decrypt books. Probably supposed to act as obfuscation +charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M" +charMap2 = "AaZzB0bYyCc1XxDdW2wEeVv3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_" +charMap3 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" +charMap4 = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789" + +class DrmException(Exception): + pass + + +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 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() + + +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 DrmException("Failed to Unprotect Data") + return string_at(outdata.pbData, outdata.cbData) + return CryptUnprotectData +CryptUnprotectData = CryptUnprotectData() + +# +# Locate and open the Kindle.info file. +# +def openKindleInfo(kInfoFile=None): + if kInfoFile == None: + regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\") + path = winreg.QueryValueEx(regkey, 'Local AppData')[0] + kinfopath = path +'\\Amazon\\Kindle For PC\\{AMAwzsaPaaZAzmZzZQzgZCAkZ3AjA_AY}\\kindle.info' + if not os.path.isfile(kinfopath): + raise DrmException('Error: kindle.info file can not be found') + return open(kinfopath,'r') + else: + if not os.path.isfile(kInfoFile): + raise DrmException('Error: kindle.info file can not be found') + return open(kInfoFile, 'r') diff --git a/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/kgenpids.py b/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/kgenpids.py new file mode 100644 index 0000000..6dcbf73 --- /dev/null +++ b/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/kgenpids.py @@ -0,0 +1,316 @@ +#!/usr/bin/env python + +from __future__ import with_statement +import sys +import os, csv +import binascii +import zlib +import re +from struct import pack, unpack, unpack_from + +class DrmException(Exception): + pass + +global kindleDatabase +global charMap1 +global charMap2 +global charMap3 +global charMap4 + +if sys.platform.startswith('win'): + from k4pcutils import openKindleInfo, CryptUnprotectData, GetUserName, GetVolumeSerialNumber, charMap2 +if sys.platform.startswith('darwin'): + from k4mutils import openKindleInfo, CryptUnprotectData, GetUserName, GetVolumeSerialNumber, charMap2 + +charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M" +charMap3 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" +charMap4 = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789" + +# crypto digestroutines +import hashlib + +def MD5(message): + ctx = hashlib.md5() + ctx.update(message) + return ctx.digest() + +def SHA1(message): + ctx = hashlib.sha1() + ctx.update(message) + return ctx.digest() + + +# Encode the bytes in data with the characters in map +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 + + +# 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 +# + +# Returns two bit at offset from a bit field +def getTwoBitsFromBitField(bitField,offset): + byteNumber = offset // 4 + bitPosition = 6 - 2*(offset % 4) + return ord(bitField[byteNumber]) >> bitPosition & 3 + +# Returns the six bits at offset from a bit field +def getSixBitsFromBitField(bitField,offset): + offset *= 3 + value = (getTwoBitsFromBitField(bitField,offset) <<4) + (getTwoBitsFromBitField(bitField,offset+1) << 2) +getTwoBitsFromBitField(bitField,offset+2) + return value + +# 8 bits to six bits encoding from hash to generate PID string +def encodePID(hash): + global charMap3 + PID = "" + for position in range (0,8): + PID += charMap3[getSixBitsFromBitField(hash,position)] + return PID + +# Encryption table used to generate the device PID +def generatePidEncryptionTable() : + table = [] + for counter1 in range (0,0x100): + value = counter1 + for counter2 in range (0,8): + if (value & 1 == 0) : + value = value >> 1 + else : + value = value >> 1 + value = value ^ 0xEDB88320 + table.append(value) + return table + +# Seed value used to generate the device PID +def generatePidSeed(table,dsn) : + value = 0 + for counter in range (0,4) : + index = (ord(dsn[counter]) ^ value) &0xFF + value = (value >> 8) ^ table[index] + return value + +# Generate the device PID +def generateDevicePID(table,dsn,nbRoll): + global charMap4 + seed = generatePidSeed(table,dsn) + pidAscii = "" + pid = [(seed >>24) &0xFF,(seed >> 16) &0xff,(seed >> 8) &0xFF,(seed) & 0xFF,(seed>>24) & 0xFF,(seed >> 16) &0xff,(seed >> 8) &0xFF,(seed) & 0xFF] + index = 0 + for counter in range (0,nbRoll): + pid[index] = pid[index] ^ ord(dsn[counter]) + index = (index+1) %8 + for counter in range (0,8): + index = ((((pid[counter] >>5) & 3) ^ pid[counter]) & 0x1f) + (pid[counter] >> 7) + pidAscii += charMap4[index] + return pidAscii + +def crc32(s): + return (~binascii.crc32(s,-1))&0xFFFFFFFF + +# convert from 8 digit PID to 10 digit PID with checksum +def checksumPid(s): + global charMap4 + crc = crc32(s) + crc = crc ^ (crc >> 16) + res = s + l = len(charMap4) + for i in (0,1): + b = crc & 0xff + pos = (b // l) ^ (b % l) + res += charMap4[pos%l] + crc >>= 8 + return res + + +# old kindle serial number to fixed pid +def pidFromSerial(s, l): + global charMap4 + crc = crc32(s) + arr1 = [0]*l + for i in xrange(len(s)): + arr1[i%l] ^= ord(s[i]) + crc_bytes = [crc >> 24 & 0xff, crc >> 16 & 0xff, crc >> 8 & 0xff, crc & 0xff] + for i in xrange(l): + arr1[i] ^= crc_bytes[i&3] + pid = "" + for i in xrange(l): + b = arr1[i] & 0xff + pid+=charMap4[(b >> 7) + ((b >> 5 & 3) ^ (b & 0x1f))] + return pid + + +# Parse the EXTH header records and use the Kindle serial number to calculate the book pid. +def getKindlePid(pidlst, rec209, token, serialnum): + + if rec209 != None and token != None: + # Compute book PID + pidHash = SHA1(serialnum+rec209+token) + bookPID = encodePID(pidHash) + bookPID = checksumPid(bookPID) + pidlst.append(bookPID) + + # compute fixed pid for old pre 2.5 firmware update pid as well + bookPID = pidFromSerial(serialnum, 7) + "*" + bookPID = checksumPid(bookPID) + pidlst.append(bookPID) + + return pidlst + + +# Parse the EXTH header records and parse the Kindleinfo +# file to calculate the book pid. + +def getK4Pids(pidlst, rec209, token, kInfoFile=None): + global kindleDatabase + global charMap1 + kindleDatabase = None + try: + kindleDatabase = parseKindleInfo(kInfoFile) + except Exception, message: + print(message) + kindleDatabase = None + pass + + if kindleDatabase == None : + return pidlst + + # Get the Mazama Random number + MazamaRandomNumber = getKindleInfoValueForKey("MazamaRandomNumber") + + # Get the HDD serial + encodedSystemVolumeSerialNumber = encodeHash(GetVolumeSerialNumber(),charMap1) + + # Get the current user name + encodedUsername = encodeHash(GetUserName(),charMap1) + + # concat, hash and encode to calculate the DSN + DSN = encode(SHA1(MazamaRandomNumber+encodedSystemVolumeSerialNumber+encodedUsername),charMap1) + + # Compute the device PID (for which I can tell, is used for nothing). + table = generatePidEncryptionTable() + devicePID = generateDevicePID(table,DSN,4) + devicePID = checksumPid(devicePID) + pidlst.append(devicePID) + + # Compute book PID + if rec209 == None or token == None: + print "\nNo EXTH record type 209 or token - Perhaps not a K4 file?" + return pidlst + + # Get the kindle account token + kindleAccountToken = getKindleInfoValueForKey("kindle.account.tokens") + + # book pid + pidHash = SHA1(DSN+kindleAccountToken+rec209+token) + bookPID = encodePID(pidHash) + bookPID = checksumPid(bookPID) + pidlst.append(bookPID) + + # variant 1 + pidHash = SHA1(kindleAccountToken+rec209+token) + bookPID = encodePID(pidHash) + bookPID = checksumPid(bookPID) + pidlst.append(bookPID) + + # variant 2 + pidHash = SHA1(DSN+rec209+token) + bookPID = encodePID(pidHash) + bookPID = checksumPid(bookPID) + pidlst.append(bookPID) + + return pidlst + +def getPidList(md1, md2, k4, pids, serials, kInfoFiles): + pidlst = [] + if k4: + pidlst = getK4Pids(pidlst, md1, md2) + for infoFile in kInfoFiles: + pidlst = getK4Pids(pidlst, md1, md2, infoFile) + for serialnum in serials: + pidlst = getKindlePid(pidlst, md1, md2, serialnum) + for pid in pids: + pidlst.append(pid) + return pidlst diff --git a/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/mobidedrm.py b/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/mobidedrm.py new file mode 100644 index 0000000..2266329 --- /dev/null +++ b/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/mobidedrm.py @@ -0,0 +1,406 @@ +#!/usr/bin/python +# +# This is a python script. You need a Python interpreter to run it. +# For example, ActiveState Python, which exists for windows. +# +# Changelog +# 0.01 - Initial version +# 0.02 - Huffdic compressed books were not properly decrypted +# 0.03 - Wasn't checking MOBI header length +# 0.04 - Wasn't sanity checking size of data record +# 0.05 - It seems that the extra data flags take two bytes not four +# 0.06 - And that low bit does mean something after all :-) +# 0.07 - The extra data flags aren't present in MOBI header < 0xE8 in size +# 0.08 - ...and also not in Mobi header version < 6 +# 0.09 - ...but they are there with Mobi header version 6, header size 0xE4! +# 0.10 - Outputs unencrypted files as-is, so that when run as a Calibre +# import filter it works when importing unencrypted files. +# Also now handles encrypted files that don't need a specific PID. +# 0.11 - use autoflushed stdout and proper return values +# 0.12 - Fix for problems with metadata import as Calibre plugin, report errors +# 0.13 - Formatting fixes: retabbed file, removed trailing whitespace +# and extra blank lines, converted CR/LF pairs at ends of each line, +# and other cosmetic fixes. +# 0.14 - Working out when the extra data flags are present has been problematic +# Versions 7 through 9 have tried to tweak the conditions, but have been +# only partially successful. Closer examination of lots of sample +# files reveals that a confusion has arisen because trailing data entries +# are not encrypted, but it turns out that the multibyte entries +# in utf8 file are encrypted. (Although neither kind gets compressed.) +# This knowledge leads to a simplification of the test for the +# trailing data byte flags - version 5 and higher AND header size >= 0xE4. +# 0.15 - Now outputs 'heartbeat', and is also quicker for long files. +# 0.16 - And reverts to 'done' not 'done.' at the end for unswindle compatibility. +# 0.17 - added modifications to support its use as an imported python module +# both inside calibre and also in other places (ie K4DeDRM tools) +# 0.17a- disabled the standalone plugin feature since a plugin can not import +# a plugin +# 0.18 - It seems that multibyte entries aren't encrypted in a v7 file... +# Removed the disabled Calibre plug-in code +# Permit use of 8-digit PIDs +# 0.19 - It seems that multibyte entries aren't encrypted in a v6 file either. +# 0.20 - Correction: It seems that multibyte entries are encrypted in a v6 file. +# 0.21 - Added support for multiple pids +# 0.22 - revised structure to hold MobiBook as a class to allow an extended interface +# 0.23 - fixed problem with older files with no EXTH section +# 0.24 - add support for type 1 encryption and 'TEXtREAd' books as well +# 0.25 - Fixed support for 'BOOKMOBI' type 1 encryption +# 0.26 - Now enables Text-To-Speech flag and sets clipping limit to 100% + +__version__ = '0.26' + +import sys + +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) +sys.stdout=Unbuffered(sys.stdout) + +import os +import struct +import binascii + +class DrmException(Exception): + pass + + +# +# MobiBook Utility Routines +# + +# Implementation of Pukall Cipher 1 +def PC1(key, src, decryption=True): + sum1 = 0; + sum2 = 0; + keyXorVal = 0; + if len(key)!=16: + print "Bad key length!" + return None + wkey = [] + for i in xrange(8): + wkey.append(ord(key[i*2])<<8 | ord(key[i*2+1])) + dst = "" + for i in xrange(len(src)): + temp1 = 0; + byteXorVal = 0; + for j in xrange(8): + temp1 ^= wkey[j] + sum2 = (sum2+j)*20021 + sum1 + sum1 = (temp1*346)&0xFFFF + sum2 = (sum2+sum1)&0xFFFF + temp1 = (temp1*20021+1)&0xFFFF + byteXorVal ^= temp1 ^ sum2 + curByte = ord(src[i]) + if not decryption: + keyXorVal = curByte * 257; + curByte = ((curByte ^ (byteXorVal >> 8)) ^ byteXorVal) & 0xFF + if decryption: + keyXorVal = curByte * 257; + for j in xrange(8): + wkey[j] ^= keyXorVal; + dst+=chr(curByte) + return dst + +def checksumPid(s): + letters = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789" + crc = (~binascii.crc32(s,-1))&0xFFFFFFFF + crc = crc ^ (crc >> 16) + res = s + l = len(letters) + for i in (0,1): + b = crc & 0xff + pos = (b // l) ^ (b % l) + res += letters[pos%l] + crc >>= 8 + return res + +def getSizeOfTrailingDataEntries(ptr, size, flags): + def getSizeOfTrailingDataEntry(ptr, size): + bitpos, result = 0, 0 + if size <= 0: + return result + while True: + v = ord(ptr[size-1]) + result |= (v & 0x7F) << bitpos + bitpos += 7 + size -= 1 + if (v & 0x80) != 0 or (bitpos >= 28) or (size == 0): + return result + num = 0 + testflags = flags >> 1 + while testflags: + if testflags & 1: + num += getSizeOfTrailingDataEntry(ptr, size - num) + testflags >>= 1 + # Check the low bit to see if there's multibyte data present. + # if multibyte data is included in the encryped data, we'll + # have already cleared this flag. + if flags & 1: + num += (ord(ptr[size - num - 1]) & 0x3) + 1 + return num + + + +class MobiBook: + def loadSection(self, section): + if (section + 1 == self.num_sections): + endoff = len(self.data_file) + else: + endoff = self.sections[section + 1][0] + off = self.sections[section][0] + return self.data_file[off:endoff] + + def __init__(self, infile): + # initial sanity check on file + self.data_file = file(infile, 'rb').read() + self.header = self.data_file[0:78] + if self.header[0x3C:0x3C+8] != 'BOOKMOBI' and self.header[0x3C:0x3C+8] != 'TEXtREAd': + raise DrmException("invalid file format") + self.magic = self.header[0x3C:0x3C+8] + self.crypto_type = -1 + + # build up section offset and flag info + self.num_sections, = struct.unpack('>H', self.header[76:78]) + self.sections = [] + for i in xrange(self.num_sections): + offset, a1,a2,a3,a4 = struct.unpack('>LBBBB', self.data_file[78+i*8:78+i*8+8]) + flags, val = a1, a2<<16|a3<<8|a4 + self.sections.append( (offset, flags, val) ) + + # parse information from section 0 + self.sect = self.loadSection(0) + self.records, = struct.unpack('>H', self.sect[0x8:0x8+2]) + + 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_version, = struct.unpack('>L',self.sect[0x68:0x6C]) + print "MOBI header version = %d, length = %d" %(self.mobi_version, self.mobi_length) + self.extra_data_flags = 0 + if (self.mobi_length >= 0xE4) and (self.mobi_version >= 5): + self.extra_data_flags, = struct.unpack('>H', self.sect[0xF2:0xF4]) + print "Extra Data Flags = %d" % self.extra_data_flags + if self.mobi_version < 7: + # multibyte utf8 data is included in the encryption for mobi_version 6 and below + # so clear that byte so that we leave it to be decrypted. + self.extra_data_flags &= 0xFFFE + + # if exth region exists parse it for metadata array + self.meta_array = {} + try: + exth_flag, = struct.unpack('>L', self.sect[0x80:0x84]) + exth = 'NONE' + if exth_flag & 0x40: + exth = self.sect[16 + self.mobi_length:] + if (len(exth) >= 4) and (exth[:4] == 'EXTH'): + nitems, = struct.unpack('>I', exth[8:12]) + pos = 12 + for i in xrange(nitems): + type, size = struct.unpack('>II', exth[pos: pos + 8]) + # 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) + content = "\144" + elif type == 404 and size == 9: + # make sure text to speech is enabled + self.patchSection(0, "\0", 16 + self.mobi_length + pos + 8) + content = "\0" + else: + content = exth[pos + 8: pos + size] + #print type, size, content + self.meta_array[type] = content + pos += size + except: + self.meta_array = {} + pass + + def getBookTitle(self): + title = '' + if 503 in self.meta_array: + title = self.meta_array[503] + else : + toff, tlen = struct.unpack('>II', self.sect[0x54:0x5c]) + tend = toff + tlen + title = self.sect[toff:tend] + if title == '': + title = self.header[:32] + title = title.split("\0")[0] + return title + + def getPIDMetaInfo(self): + rec209 = None + token = None + if 209 in self.meta_array: + rec209 = self.meta_array[209] + data = rec209 + # Parse the 209 data to find the the exth record with the token data. + # The last character of the 209 data points to the record with the token. + # Always 208 from my experience, but I'll leave the logic in case that changes. + for i in xrange(len(data)): + if ord(data[i]) != 0: + if self.meta_array[ord(data[i])] != None: + token = self.meta_array[ord(data[i])] + return rec209, token + + def patch(self, off, new): + self.data_file = self.data_file[:off] + new + self.data_file[off+len(new):] + + def patchSection(self, section, new, in_off = 0): + if (section + 1 == self.num_sections): + endoff = len(self.data_file) + else: + endoff = self.sections[section + 1][0] + off = self.sections[section][0] + assert off + in_off + len(new) <= endoff + self.patch(off + in_off, new) + + def parseDRM(self, data, count, pidlist): + found_key = None + keyvec1 = "\x72\x38\x33\xB0\xB4\xF2\xE3\xCA\xDF\x09\x01\xD6\xE2\xE0\x3F\x96" + for pid in pidlist: + bigpid = pid.ljust(16,'\0') + temp_key = PC1(keyvec1, bigpid, False) + temp_key_sum = sum(map(ord,temp_key)) & 0xff + found_key = None + for i in xrange(count): + verification, size, type, cksum, cookie = struct.unpack('>LLLBxxx32s', data[i*0x30:i*0x30+0x30]) + if cksum == temp_key_sum: + cookie = PC1(temp_key, cookie) + ver,flags,finalkey,expiry,expiry2 = struct.unpack('>LL16sLL', cookie) + if verification == ver and (flags & 0x1F) == 1: + found_key = finalkey + break + if found_key != None: + break + if not found_key: + # Then try the default encoding that doesn't require a PID + pid = "00000000" + temp_key = keyvec1 + temp_key_sum = sum(map(ord,temp_key)) & 0xff + for i in xrange(count): + verification, size, type, cksum, cookie = struct.unpack('>LLLBxxx32s', data[i*0x30:i*0x30+0x30]) + if cksum == temp_key_sum: + cookie = PC1(temp_key, cookie) + ver,flags,finalkey,expiry,expiry2 = struct.unpack('>LL16sLL', cookie) + if verification == ver: + found_key = finalkey + break + return [found_key,pid] + + def processBook(self, pidlist): + 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: + print "This book is not encrypted." + return self.data_file + if crypto_type != 2 and crypto_type != 1: + raise DrmException("Cannot decode unknown Mobipocket encryption type %d" % crypto_type) + + goodpids = [] + for pid in pidlist: + if len(pid)==10: + if checksumPid(pid[0:-2]) != pid: + print "Warning: PID " + pid + " has incorrect checksum, should have been "+checksumPid(pid[0:-2]) + goodpids.append(pid[0:-2]) + elif len(pid)==8: + 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 + drm_ptr, drm_count, drm_size, drm_flags = struct.unpack('>LLLL', self.sect[0xA8:0xA8+16]) + if drm_count == 0: + raise DrmException("Not yet initialised with PID. Must be opened with Mobipocket Reader first.") + found_key, pid = self.parseDRM(self.sect[drm_ptr:drm_ptr+drm_size], drm_count, goodpids) + if not found_key: + raise DrmException("No key found. 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": + print "File has default encryption, no specific PID." + else: + print "File is encoded with PID "+checksumPid(pid)+"." + + # clear the crypto type + self.patchSection(0, "\0" * 2, 0xC) + + # decrypt sections + print "Decrypting. Please wait . . .", + new_data = self.data_file[:self.sections[1][0]] + for i in xrange(1, self.records+1): + data = self.loadSection(i) + extra_size = getSizeOfTrailingDataEntries(data, len(data), self.extra_data_flags) + if i%100 == 0: + print ".", + # print "record %d, extra_size %d" %(i,extra_size) + new_data += PC1(found_key, data[0:len(data) - extra_size]) + if extra_size > 0: + new_data += data[-extra_size:] + if self.num_sections > self.records+1: + new_data += self.data_file[self.sections[self.records+1][0]:] + self.data_file = new_data + print "done" + return self.data_file + +def getUnencryptedBook(infile,pid): + if not os.path.isfile(infile): + raise DrmException('Input File Not Found') + book = MobiBook(infile) + return book.processBook([pid]) + +def getUnencryptedBookWithList(infile,pidlist): + if not os.path.isfile(infile): + raise DrmException('Input File Not Found') + book = MobiBook(infile) + return book.processBook(pidlist) + +def main(argv=sys.argv): + print ('MobiDeDrm v%(__version__)s. ' + 'Copyright 2008-2010 The Dark Reverser.' % globals()) + if len(argv)<3 or len(argv)>4: + print "Removes protection from Mobipocket books" + print "Usage:" + print " %s []" % sys.argv[0] + return 1 + else: + infile = argv[1] + outfile = argv[2] + if len(argv) is 4: + pidlist = argv[3].split(',') + else: + pidlist = {} + try: + stripped_file = getUnencryptedBookWithList(infile, pidlist) + file(outfile, 'wb').write(stripped_file) + except DrmException, e: + print "Error: %s" % e + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/openssl_des.py b/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/openssl_des.py new file mode 100644 index 0000000..8a044fa --- /dev/null +++ b/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/openssl_des.py @@ -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 + diff --git a/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/pycrypto_des.py b/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/pycrypto_des.py new file mode 100644 index 0000000..81502c8 --- /dev/null +++ b/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/pycrypto_des.py @@ -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 + diff --git a/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/python_des.py b/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/python_des.py new file mode 100644 index 0000000..cfb4f59 --- /dev/null +++ b/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/python_des.py @@ -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) diff --git a/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/scrolltextwidget.py b/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/scrolltextwidget.py new file mode 100644 index 0000000..98b4147 --- /dev/null +++ b/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/scrolltextwidget.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python +# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab + +import Tkinter +import Tkconstants + +# basic scrolled text widget +class ScrolledText(Tkinter.Text): + def __init__(self, master=None, **kw): + self.frame = Tkinter.Frame(master) + self.vbar = Tkinter.Scrollbar(self.frame) + self.vbar.pack(side=Tkconstants.RIGHT, fill=Tkconstants.Y) + kw.update({'yscrollcommand': self.vbar.set}) + Tkinter.Text.__init__(self, self.frame, **kw) + self.pack(side=Tkconstants.LEFT, fill=Tkconstants.BOTH, expand=True) + self.vbar['command'] = self.yview + # Copy geometry methods of self.frame without overriding Text + # methods = hack! + text_meths = vars(Tkinter.Text).keys() + methods = vars(Tkinter.Pack).keys() + vars(Tkinter.Grid).keys() + vars(Tkinter.Place).keys() + methods = set(methods).difference(text_meths) + for m in methods: + if m[0] != '_' and m != 'config' and m != 'configure': + setattr(self, m, getattr(self.frame, m)) + + def __str__(self): + return str(self.frame) diff --git a/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/simpleprefs.py b/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/simpleprefs.py new file mode 100644 index 0000000..10919d2 --- /dev/null +++ b/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/simpleprefs.py @@ -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 + diff --git a/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/stylexml2css.py b/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/stylexml2css.py new file mode 100644 index 0000000..73f798f --- /dev/null +++ b/DeDRM_Windows_Application/DeDRM_WinApp/DeDRM_lib/lib/stylexml2css.py @@ -0,0 +1,243 @@ +#! /usr/bin/python +# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab +# For use with Topaz Scripts Version 2.6 + +import csv +import sys +import os +import getopt +from struct import pack +from struct import unpack + + +class DocParser(object): + def __init__(self, flatxml, fontsize, ph, pw): + self.flatdoc = flatxml.split('\n') + self.fontsize = int(fontsize) + self.ph = int(ph) * 1.0 + self.pw = int(pw) * 1.0 + + stags = { + 'paragraph' : 'p', + 'graphic' : '.graphic' + } + + attr_val_map = { + 'hang' : 'text-indent: ', + 'indent' : 'text-indent: ', + 'line-space' : 'line-height: ', + 'margin-bottom' : 'margin-bottom: ', + 'margin-left' : 'margin-left: ', + 'margin-right' : 'margin-right: ', + 'margin-top' : 'margin-top: ', + 'space-after' : 'padding-bottom: ', + } + + attr_str_map = { + 'align-center' : 'text-align: center; margin-left: auto; margin-right: auto;', + 'align-left' : 'text-align: left;', + 'align-right' : 'text-align: right;', + 'align-justify' : 'text-align: justify;', + 'display-inline' : 'display: inline;', + 'pos-left' : 'text-align: left;', + 'pos-right' : 'text-align: right;', + 'pos-center' : 'text-align: center; margin-left: auto; margin-right: auto;', + } + + + # find tag if within pos to end inclusive + def findinDoc(self, tagpath, pos, end) : + result = None + docList = self.flatdoc + cnt = len(docList) + if end == -1 : + end = cnt + else: + end = min(cnt,end) + foundat = -1 + for j in xrange(pos, end): + item = docList[j] + if item.find('=') >= 0: + (name, argres) = item.split('=',1) + else : + name = item + argres = '' + if name.endswith(tagpath) : + result = argres + foundat = j + break + return foundat, result + + + # return list of start positions for the tagpath + def posinDoc(self, tagpath): + startpos = [] + pos = 0 + res = "" + while res != None : + (foundpos, res) = self.findinDoc(tagpath, pos, -1) + if res != None : + startpos.append(foundpos) + pos = foundpos + 1 + return startpos + + + def process(self): + + classlst = '' + csspage = '.cl-center { text-align: center; margin-left: auto; margin-right: auto; }\n' + csspage += '.cl-right { text-align: right; }\n' + csspage += '.cl-left { text-align: left; }\n' + csspage += '.cl-justify { text-align: justify; }\n' + + # generate a list of each