tools v5.4
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Ignoble Epub DeDRM Plugin Configuration</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<h1>Ignoble Epub DeDRM Plugin</h1>
|
||||
<h3>(version 0.2.3)</h3>
|
||||
<h3> For additional help read the <a href="http://apprenticealf.wordpress.com/2011/01/17/frequently-asked-questions-about-the-drm-removal-tools/" target="_blank">FAQ</a> on <a href="http://apprenticealf.wordpress.com" target="_blank">Apprentice Alf's Blog</a> and ask questions in the comments section of the <a href="http://apprenticealf.wordpress.com/2012/09/10/drm-removal-tools-for-ebooks/" target="_blank">first post</a>.</h3>
|
||||
|
||||
<p>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).</p>
|
||||
|
||||
<p>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.</p>
|
||||
|
||||
<p>This help file is always available from within the plugin's customization dialog in calibre (when installed, of course). The "Plugin Help" link can be found in the upper-right portion of the customization dialog.</p>
|
||||
|
||||
<h3>Installation:</h3>
|
||||
|
||||
<p>Go to calibre's Preferences page. Do **NOT** select "Get plugins to enhance calibre" as this is reserved for "official" calibre plugins, instead select "Change calibre behavior". Under "Advanced" click on the Plugins button. Use the "Load plugin from file" button to select the plugin's zip file (ignobleepub_v02.3_plugin.zip) and click the 'Add' button. Click 'Yes' in the the "Are you sure?" dialog. Click OK in the "Success" dialog. <b><u>Now restart calibre</u></b>.</p>
|
||||
|
||||
|
||||
<h3>Configuration:</h3>
|
||||
|
||||
<p>Upon first installing the plugin (or upgrading from a version earlier than 0.2.0), the plugin will be unconfigured. Until you create at least one B&N key—or migrate your existing key(s)/data from an earlier version of the plugin—the plugin will not function. When unconfigured (no saved keys)... an error message will occur whenever ePubs are imported to calibre. To eliminate the error message, open the plugin's customization dialog and create/import/migrate a key (or disable/uninstall the plugin). You can get to the plugin's customization dialog by opening calibre's Preferences dialog, and clicking Plugins (under the Advanced section). Once in the Plugin Preferences, expand the "File type plugins" section and look for the "Ignoble Epub DeDRM" plugin. Highlight that plugin and click the "Customize plugin" button.</p>
|
||||
|
||||
<p>If you are upgrading from an earlier version of this plugin and have provided your name(s) and credit card number(s) as part of the old plugin's customization string, you will be prompted to migrate this data to the plugin's new, more secure, key storage method when you open the customization dialog for the first time. If you choose NOT to migrate that data, you will be prompted to save that data as a text file in a location of your choosing. Either way, this plugin will no longer be storing names and credit card numbers in plain sight (or anywhere for that matter) on your computer or in calibre. If you don't choose to migrate OR save the data, that data will be lost. You have been warned!!</p>
|
||||
|
||||
<p>Upon configuring for the first time, you may also be asked if you wish to import your existing *.b64 keyfiles (if you use them) to the plugin's new key storage method. The new plugin no longer looks for keyfiles in calibre's configuration directory, so it's highly recommended that you import any existing keyfiles when prompted ... but you <i>always</i> have the ability to import existing keyfiles anytime you might need/want to.</p>
|
||||
|
||||
<p>If you have upgraded from an earlier version of the plugin, the above instructions may be all you need to do to get the new plugin up and running. Continue reading for new-key generation and existing-key management instructions.</p>
|
||||
|
||||
<h4 style="margin-left: 1.0em;"><u>Creating New Keys:</u></h4>
|
||||
|
||||
<p style="margin-left: 1.0em">On the right-hand side of the plugin's customization dialog, you will see a button with an icon that looks like a green plus sign (+). Clicking this button will open a new dialog for entering the necessary data to generate a new key.</p>
|
||||
<ul style="margin-left: 2.0em;">
|
||||
<li><b>Unique Key Name:</b> this is a unique name you choose to help you identify the key after it's created. This name will show in the list of configured keys. Choose something that will help you remember the data (name, cc#) it was created with.</i>
|
||||
<li style="margin-top: 0.5em;"><b>Your Name:</b> Your name as set in your Barnes & Noble account, My Account page, directly under PERSONAL INFORMATION. It is usually just your first name and last name separated by a space. This name will not be stored anywhere on your computer or in calibre. It will only be used in the creation of the one-way hash/key that's stored in the preferences.</i>
|
||||
<li style="margin-top: 0.5em;"><b>Credit Card#:</b> this is the default credit card number that was on file with Barnes & Noble at the time of download of the ebook to be de-DRMed. Nothing fancy here; no dashes or spaces ... just the 16 (15 for American Express) digits. Again... this number will not be stored anywhere on your computer or in calibre. It will only be used in the creation of the one-way hash/key that's stored in the preferences.</i>
|
||||
</ul>
|
||||
|
||||
<p style="margin-left: 1.0em;">Click the 'OK" button to create and store the generated key. Or Cancel if you didn't want to create a key.</p>
|
||||
|
||||
<h4 style="margin-left: 1.0em;"><u>Deleting Keys:</u></h4>
|
||||
|
||||
<p style="margin-left: 1.0em;">On the right-hand side of the plugin's customization dialog, you will see a button with an icon that looks like a red "X". Clicking this button will delete the highlighted key in the list. You will be prompted once to be sure that's what you truly mean to do. Once gone, it's permanently gone.</p>
|
||||
|
||||
<h4 style="margin-left: 1.0em;"><u>Exporting Keys:</u></h4>
|
||||
|
||||
<p style="margin-left: 1.0em;">On the right-hand side of the plugin's customization dialog, you will see a button with an icon that looks like a computer's hard-drive. Use this button to export the highlighted key to a file (*.b64). Used for backup purposes or to migrate key data to other computers/calibre installations. The dialog will prompt you for a place to save the file.</p>
|
||||
|
||||
<h4 style="margin-left: 1.0em;"><u>Importing Existing Keyfiles:</u></h4>
|
||||
|
||||
<p style="margin-left: 1.0em;">At the bottom-left of the plugin's customization dialog, you will see a button labeled "Import Existing Keyfiles". Use this button to import existing *.b64 keyfiles. Used for migrating keyfiles from older versions of the plugin (or keys generated with the original I <3 Cabbages script), or moving keyfiles from computer to computer, or restoring a backup. Some very basic validation is done to try to avoid overwriting already configured keys with incoming, imported keyfiles with the same base file name, but I'm sure that could be broken if someone tried hard. Just take care when importing.</p>
|
||||
|
||||
<p>Once done creating/importing/exporting/deleting decryption keys; click "OK" to exit the customization dialogue (the cancel button will actually work the same way here ... at this point all data/changes are committed already, so take your pick).</p>
|
||||
|
||||
<h3>Troubleshooting:</h3>
|
||||
|
||||
<p style="margin-top: 0.5em;">If you find that it's not working for you (imported Barnes & Noble 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. ;)</p>
|
||||
|
||||
<p>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.</p>
|
||||
|
||||
<p>Another way to debug (perhaps easier if you're not all that comfortable with command-line stuff) is to launch calibre in debug mode. Open a command prompt (terminal) and type "calibre-debug -g" (again without the quotes). Calibre will launch, and you can can add the problem book(s) using the normal gui method. The debug info will be output to the original command prompt (terminal window). Copy the resulting output and paste it into any online help request you make.</p>
|
||||
<p> </p>
|
||||
<p>** 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.</p>
|
||||
|
||||
<p> </p>
|
||||
<h4>Revision history:</h4>
|
||||
<pre>
|
||||
0.1.0 - Initial release
|
||||
0.1.1 - Allow Windows users to make use of openssl if they have it installed.
|
||||
- Incorporated SomeUpdates zipfix routine.
|
||||
0.1.2 - bug fix for non-ascii file names in encryption.xml
|
||||
0.1.3 - Try PyCrypto on Windows first
|
||||
0.1.4 - update zipfix to deal with mimetype not in correct place
|
||||
0.1.5 - update zipfix to deal with completely missing mimetype files
|
||||
0.1.6 - update to the new calibre plugin interface
|
||||
0.1.7 - Fix for potential problem with PyCrypto
|
||||
0.1.8 - an updated/modified zipfix.py and included zipfilerugged.py
|
||||
0.2.0 - Completely overhauled plugin configuration dialog and key management/storage
|
||||
0.2.1 - an updated/modified zipfix.py and included zipfilerugged.py
|
||||
0.2.2 - added in potential fixes from 0.1.7 that had been missed.
|
||||
0.2.3 - fixed possible output/unicode problem
|
||||
</pre>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,6 +1,11 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
|
||||
from __future__ import with_statement
|
||||
__license__ = 'GPL v3'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
|
||||
# ignobleepub_plugin.py
|
||||
# Released under the terms of the GNU General Public Licence, version 3 or
|
||||
# later. <http://www.gnu.org/licenses/>
|
||||
#
|
||||
@@ -10,35 +15,15 @@
|
||||
# I had the much easier job of converting them to Calibre a 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
|
||||
# with a version of 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.
|
||||
#
|
||||
# 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. 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 the info with
|
||||
# a colon: Your Name,1234123412341234:Other Name,2345234523452345
|
||||
#
|
||||
# ** Method 1 is your only option if you don't have/can't run the original
|
||||
# I <3 Cabbages scripts on your particular machine. **
|
||||
#
|
||||
# 2) If you already have keyfiles generated with I <3 Cabbages' ignoblekeygen.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 '.b64' extension (like the ignoblekeygen
|
||||
# script produces). This directory isn't touched when upgrading Calibre, so it's quite safe
|
||||
# to leave then there.
|
||||
#
|
||||
# All keyfiles from option 2 and all data entered from option 1 will be used to attempt
|
||||
# to decrypt a book. You can use option 1 or option 2, or a combination of both.
|
||||
#
|
||||
# Check out the plugin's configuration settings by clicking the "Customize plugin"
|
||||
# button when you have the "BnN ePub DeDRM" plugin highlighted (under Preferences->
|
||||
# Plugins->File type plugins). Once you have the configuration dialog open, you'll
|
||||
# see a Help link on the top right-hand side.
|
||||
#
|
||||
# Revision history:
|
||||
# 0.1.0 - Initial release
|
||||
@@ -48,28 +33,35 @@
|
||||
# 0.1.3 - Try PyCrypto on Windows first
|
||||
# 0.1.4 - update zipfix to deal with mimetype not in correct place
|
||||
# 0.1.5 - update zipfix to deal with completely missing mimetype files
|
||||
# 0.1.6 - update ot the new calibre plugin interface
|
||||
# 0.1.6 - update for the new calibre plugin interface
|
||||
# 0.1.7 - Fix for potential problem with PyCrypto
|
||||
# 0.1.8 - an updated/modified zipfix.py and included zipfilerugged.py
|
||||
# 0.2.0 - Completely overhauled plugin configuration dialog and key management/storage
|
||||
# 0.2.1 - an updated/modified zipfix.py and included zipfilerugged.py
|
||||
# 0.2.2 - added in potential fixes from 0.1.7 that had been missed.
|
||||
# 0.2.3 - fixed possible output/unicode problem
|
||||
|
||||
"""
|
||||
Decrypt Barnes & Noble ADEPT encrypted EPUB books.
|
||||
"""
|
||||
|
||||
from __future__ import with_statement
|
||||
PLUGIN_NAME = 'Ignoble Epub DeDRM'
|
||||
PLUGIN_VERSION_TUPLE = (0, 2, 3)
|
||||
PLUGIN_VERSION = '.'.join([str(x) for x in PLUGIN_VERSION_TUPLE])
|
||||
# Include an html helpfile in the plugin's zipfile with the following name.
|
||||
RESOURCE_NAME = PLUGIN_NAME + '_Help.htm'
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
|
||||
import sys
|
||||
import os
|
||||
import hashlib
|
||||
import zlib
|
||||
import zipfile
|
||||
import re
|
||||
import sys, os, zlib, re
|
||||
from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED
|
||||
import xml.etree.ElementTree as etree
|
||||
from zipfile import ZipInfo as _ZipInfo
|
||||
#from lxml import etree
|
||||
try:
|
||||
import xml.etree.cElementTree as etree
|
||||
except ImportError:
|
||||
import xml.etree.ElementTree as etree
|
||||
from contextlib import closing
|
||||
|
||||
global AES
|
||||
global AES2
|
||||
|
||||
META_NAMES = ('mimetype', 'META-INF/rights.xml', 'META-INF/encryption.xml')
|
||||
NSMAP = {'adept': 'http://ns.adobe.com/adept',
|
||||
@@ -77,7 +69,7 @@ NSMAP = {'adept': 'http://ns.adobe.com/adept',
|
||||
|
||||
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
|
||||
@@ -88,7 +80,7 @@ def _load_crypto_libcrypto():
|
||||
else:
|
||||
libcrypto = find_library('crypto')
|
||||
if libcrypto is None:
|
||||
raise IGNOBLEError('libcrypto not found')
|
||||
raise IGNOBLEError('%s Plugin v%s: libcrypto not found' % (PLUGIN_NAME, PLUGIN_VERSION))
|
||||
libcrypto = CDLL(libcrypto)
|
||||
|
||||
AES_MAXNR = 14
|
||||
@@ -107,8 +99,6 @@ def _load_crypto_libcrypto():
|
||||
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_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',
|
||||
@@ -119,87 +109,51 @@ def _load_crypto_libcrypto():
|
||||
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')
|
||||
raise IGNOBLEError('%s Plugin v%s: AES improper key used' % (PLUGIN_NAME, PLUGIN_VERSION))
|
||||
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')
|
||||
raise IGNOBLEError('%s Plugin v%s: Failed to initialize AES key' % (PLUGIN_NAME, PLUGIN_VERSION))
|
||||
|
||||
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')
|
||||
raise IGNOBLEError('%s Plugin v%s: AES decryption failed' % (PLUGIN_NAME, PLUGIN_VERSION))
|
||||
return out.raw
|
||||
|
||||
class AES2(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
|
||||
print 'IgnobleEpub: Using libcrypto.'
|
||||
return (AES, AES2)
|
||||
print '%s Plugin v%s: Using libcrypto.' %(PLUGIN_NAME, PLUGIN_VERSION)
|
||||
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)
|
||||
self._aes = _AES.new(key, _AES.MODE_CBC, '\x00'*16)
|
||||
|
||||
def decrypt(self, data):
|
||||
return self._aes.decrypt(data)
|
||||
|
||||
class AES2(object):
|
||||
def __init__(self, key, iv):
|
||||
self._aes = _AES.new(key, _AES.MODE_CBC, iv)
|
||||
|
||||
def encrypt(self, data):
|
||||
return self._aes.encrypt(data)
|
||||
print 'IgnobleEpub: Using PyCrypto.'
|
||||
return (AES, AES2)
|
||||
print '%s Plugin v%s: Using PyCrypto.' %(PLUGIN_NAME, PLUGIN_VERSION)
|
||||
return AES
|
||||
|
||||
def _load_crypto():
|
||||
_aes = _aes2 = None
|
||||
_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, _aes2 = loader()
|
||||
_aes = loader()
|
||||
break
|
||||
except (ImportError, IGNOBLEError):
|
||||
pass
|
||||
return (_aes, _aes2)
|
||||
return _aes
|
||||
|
||||
def normalize_name(name): # Strip spaces and convert to lowercase.
|
||||
return ''.join(x for x in name.lower() if x != ' ')
|
||||
|
||||
def generate_keyfile(name, ccn):
|
||||
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 = AES2(ccn_sha, name_sha)
|
||||
crypt = aes.encrypt(both_sha + ('\x0c' * 0x0c))
|
||||
userkey = hashlib.sha1(crypt).digest()
|
||||
|
||||
return userkey.encode('base64')
|
||||
|
||||
class ZipInfo(zipfile.ZipInfo):
|
||||
class ZipInfo(_ZipInfo):
|
||||
def __init__(self, *args, **kwargs):
|
||||
if 'compress_type' in kwargs:
|
||||
compress_type = kwargs.pop('compress_type')
|
||||
@@ -241,8 +195,8 @@ def plugin_main(userkey, inpath, outpath):
|
||||
|
||||
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:
|
||||
if 'META-INF/rights.xml' not in namelist or 'META-INF/encryption.xml' not in namelist:
|
||||
print '%s Plugin: Not Encrypted.' % PLUGIN_NAME
|
||||
return 1
|
||||
for name in META_NAMES:
|
||||
namelist.remove(name)
|
||||
@@ -267,116 +221,116 @@ def plugin_main(userkey, inpath, outpath):
|
||||
return 0
|
||||
|
||||
from calibre.customize import FileTypePlugin
|
||||
from calibre.constants import iswindows, isosx
|
||||
from calibre.gui2 import is_ok_to_use_qt
|
||||
|
||||
class IgnobleDeDRM(FileTypePlugin):
|
||||
name = 'Ignoble Epub DeDRM'
|
||||
description = 'Removes DRM from secure Barnes & Noble epub files. \
|
||||
Credit given to I <3 Cabbages for the original stand-alone scripts.'
|
||||
name = PLUGIN_NAME
|
||||
description = 'Removes DRM from secure Barnes & Noble epub files. Credit given to I <3 Cabbages for the original stand-alone scripts.'
|
||||
supported_platforms = ['linux', 'osx', 'windows']
|
||||
author = 'DiapDealer'
|
||||
version = (0, 1, 6)
|
||||
version = PLUGIN_VERSION_TUPLE
|
||||
minimum_calibre_version = (0, 7, 55) # Compiled python libraries cannot be imported in earlier versions.
|
||||
file_types = set(['epub'])
|
||||
on_import = True
|
||||
|
||||
|
||||
def run(self, path_to_ebook):
|
||||
from calibre_plugins.ignoble_epub import outputfix
|
||||
|
||||
if sys.stdout.encoding == None:
|
||||
sys.stdout = outputfix.getwriter('utf-8')(sys.stdout)
|
||||
else:
|
||||
sys.stdout = outputfix.getwriter(sys.stdout.encoding)(sys.stdout)
|
||||
if sys.stderr.encoding == None:
|
||||
sys.stderr = outputfix.getwriter('utf-8')(sys.stderr)
|
||||
else:
|
||||
sys.stderr = outputfix.getwriter(sys.stderr.encoding)(sys.stderr)
|
||||
|
||||
global AES
|
||||
global AES2
|
||||
|
||||
AES, AES2 = _load_crypto()
|
||||
|
||||
if AES == None or AES2 == None:
|
||||
|
||||
print '\n\nRunning {0} v{1} on "{2}"'.format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook))
|
||||
AES = _load_crypto()
|
||||
if AES == None:
|
||||
# Failed to load libcrypto or PyCrypto... Adobe Epubs can't be decrypted.'
|
||||
raise IGNOBLEError('IgnobleEpub - Failed to load crypto libs.')
|
||||
return
|
||||
raise Exception('%s Plugin v%s: Failed to load crypto libs.' % (PLUGIN_NAME, PLUGIN_VERSION))
|
||||
|
||||
# Load any keyfiles (*.b64) included Calibre's config directory.
|
||||
userkeys = []
|
||||
# First time use or first time after upgrade to new key-handling/storage method
|
||||
# or no keys configured. Give a visual prompt to configure.
|
||||
import calibre_plugins.ignoble_epub.config as cfg
|
||||
if not cfg.prefs['configured']:
|
||||
titlemsg = '%s v%s' % (PLUGIN_NAME, PLUGIN_VERSION)
|
||||
errmsg = 'Plugin not configured! Decryption unsuccessful.\n' + \
|
||||
'\nThis may be the first time you\'ve used this plugin\n' + \
|
||||
'(or the first time since upgrading this plugin).\n' + \
|
||||
'\nYou\'ll need to open the customization dialog (Preferences->Plugins->File type plugins).'
|
||||
if is_ok_to_use_qt():
|
||||
from PyQt4.Qt import QMessageBox
|
||||
d = QMessageBox(QMessageBox.Warning, titlemsg, errmsg )
|
||||
d.show()
|
||||
d.raise_()
|
||||
d.exec_()
|
||||
raise Exception('%s Plugin v%s: Plugin not configured.' % (PLUGIN_NAME, PLUGIN_VERSION))
|
||||
|
||||
# Check original epub archive for zip errors.
|
||||
from calibre_plugins.ignoble_epub import zipfix
|
||||
inf = self.temporary_file('.epub')
|
||||
try:
|
||||
# Find Calibre's configuration directory.
|
||||
confpath = os.path.split(os.path.split(self.plugin_path)[0])[0]
|
||||
print 'IgnobleEpub: Calibre configuration directory = %s' % confpath
|
||||
files = os.listdir(confpath)
|
||||
filefilter = re.compile("\.b64$", re.IGNORECASE)
|
||||
files = filter(filefilter.search, files)
|
||||
|
||||
if files:
|
||||
for filename in files:
|
||||
fpath = os.path.join(confpath, filename)
|
||||
with open(fpath, 'rb') as f:
|
||||
userkeys.append(f.read())
|
||||
print 'IgnobleEpub: Keyfile %s found in config folder.' % filename
|
||||
else:
|
||||
print 'IgnobleEpub: No keyfiles found. Checking plugin customization string.'
|
||||
except IOError:
|
||||
print 'IgnobleEpub: Error reading keyfiles from config directory.'
|
||||
pass
|
||||
|
||||
# Get name and credit card number from Plugin Customization
|
||||
if not userkeys and not self.site_customization:
|
||||
# Plugin hasn't been configured... do nothing.
|
||||
raise IGNOBLEError('IgnobleEpub - No keys found. Plugin not configured.')
|
||||
return
|
||||
|
||||
if self.site_customization:
|
||||
keystuff = self.site_customization
|
||||
ar = keystuff.split(':')
|
||||
keycount = 0
|
||||
for i in ar:
|
||||
try:
|
||||
name, ccn = i.split(',')
|
||||
keycount += 1
|
||||
except ValueError:
|
||||
raise IGNOBLEError('IgnobleEpub - Error parsing user supplied data.')
|
||||
return
|
||||
|
||||
# Generate Barnes & Noble EPUB user key from name and credit card number.
|
||||
userkeys.append( generate_keyfile(name, ccn) )
|
||||
print 'IgnobleEpub: %d userkey(s) generated from customization data.' % keycount
|
||||
print '%s Plugin: Verifying zip archive integrity.' % PLUGIN_NAME
|
||||
fr = zipfix.fixZip(path_to_ebook, inf.name)
|
||||
fr.fix()
|
||||
except Exception, e:
|
||||
print '%s Plugin: unforeseen zip archive issue.' % PLUGIN_NAME
|
||||
raise Exception(e)
|
||||
# Create a TemporaryPersistent file to work with.
|
||||
of = self.temporary_file('.epub')
|
||||
|
||||
# Attempt to decrypt epub with each encryption key (generated or provided).
|
||||
for userkey in userkeys:
|
||||
# Create a TemporaryPersistent file to work with.
|
||||
# Check original epub archive for zip errors.
|
||||
from calibre_plugins.ignobleepub import zipfix
|
||||
inf = self.temporary_file('.epub')
|
||||
try:
|
||||
fr = zipfix.fixZip(path_to_ebook, inf.name)
|
||||
fr.fix()
|
||||
except Exception, e:
|
||||
raise Exception(e)
|
||||
return
|
||||
of = self.temporary_file('.epub')
|
||||
|
||||
key_counter = 1
|
||||
for keyname, userkey in cfg.prefs['keys'].items():
|
||||
keyname_masked = keyname[:4] + ''.join('x' for x in keyname[4:])
|
||||
# Give the user key, ebook and TemporaryPersistent file to the Stripper function.
|
||||
result = plugin_main(userkey, inf.name, of.name)
|
||||
|
||||
|
||||
# Ebook is not a B&N Adept epub... do nothing and pass it on.
|
||||
# This allows a non-encrypted epub to be imported without error messages.
|
||||
if result == 1:
|
||||
print 'IgnobleEpub: Not a B&N Adept Epub... punting.'
|
||||
print '%s Plugin: Not a B&N Epub - doing nothing.\n' % PLUGIN_NAME
|
||||
of.close()
|
||||
return path_to_ebook
|
||||
break
|
||||
|
||||
|
||||
# Decryption was successful return the modified PersistentTemporary
|
||||
# file to Calibre's import process.
|
||||
if result == 0:
|
||||
print 'IgnobleEpub: Encryption successfully removed.'
|
||||
print '{0} Plugin: Encryption key {1} ("{2}") correct!'.format(PLUGIN_NAME, key_counter, keyname_masked)
|
||||
of.close()
|
||||
return of.name
|
||||
break
|
||||
|
||||
print 'IgnobleEpub: Encryption key invalid... trying others.'
|
||||
of.close()
|
||||
|
||||
|
||||
print '{0} Plugin: Encryption key {1} ("{2}") incorrect!'.format(PLUGIN_NAME, key_counter, keyname_masked)
|
||||
key_counter += 1
|
||||
|
||||
# Something went wrong with decryption.
|
||||
# Import the original unmolested epub.
|
||||
of.close
|
||||
raise IGNOBLEError('IgnobleEpub - Ultimately failed to decrypt.')
|
||||
return
|
||||
|
||||
|
||||
def customization_help(self, gui=False):
|
||||
return 'Enter B&N Account name and CC# (separate name and CC# with a comma)'
|
||||
raise Exception('%s Plugin v%s: Ultimately failed to decrypt.\n' % (PLUGIN_NAME, PLUGIN_VERSION))
|
||||
|
||||
def is_customizable(self):
|
||||
# return true to allow customization via the Plugin->Preferences.
|
||||
return True
|
||||
|
||||
def config_widget(self):
|
||||
from calibre_plugins.ignoble_epub.config import ConfigWidget
|
||||
# Extract the helpfile contents from in the plugin's zipfile.
|
||||
# The helpfile must be named <plugin name variable> + '_Help.htm'
|
||||
return ConfigWidget(self.load_resources(RESOURCE_NAME)[RESOURCE_NAME])
|
||||
|
||||
def load_resources(self, names):
|
||||
ans = {}
|
||||
with ZipFile(self.plugin_path, 'r') as zf:
|
||||
for candidate in zf.namelist():
|
||||
if candidate in names:
|
||||
ans[candidate] = zf.read(candidate)
|
||||
return ans
|
||||
|
||||
def save_settings(self, config_widget):
|
||||
config_widget.save_settings()
|
||||
274
Calibre_Plugins/ignobleepub_plugin/config.py
Normal file
274
Calibre_Plugins/ignobleepub_plugin/config.py
Normal file
@@ -0,0 +1,274 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from __future__ import with_statement
|
||||
__license__ = 'GPL v3'
|
||||
|
||||
# Standard Python modules.
|
||||
import os, sys, re, hashlib
|
||||
|
||||
# PyQT4 modules (part of calibre).
|
||||
from PyQt4.Qt import (Qt, QWidget, QHBoxLayout, QVBoxLayout, QLabel, QLineEdit,
|
||||
QGroupBox, QPushButton, QListWidget, QListWidgetItem,
|
||||
QAbstractItemView, QIcon, QDialog, QUrl, QString)
|
||||
from PyQt4 import QtGui
|
||||
|
||||
# calibre modules and constants.
|
||||
from calibre.gui2 import (error_dialog, question_dialog, info_dialog, open_url,
|
||||
choose_dir, choose_files)
|
||||
from calibre.utils.config import dynamic, config_dir, JSONConfig
|
||||
|
||||
# modules from this plugin's zipfile.
|
||||
from calibre_plugins.ignoble_epub.__init__ import PLUGIN_NAME, PLUGIN_VERSION
|
||||
from calibre_plugins.ignoble_epub.__init__ import RESOURCE_NAME as help_file_name
|
||||
from calibre_plugins.ignoble_epub.utilities import (_load_crypto, normalize_name,
|
||||
generate_keyfile, caselessStrCmp, AddKeyDialog,
|
||||
DETAILED_MESSAGE, parseCustString)
|
||||
|
||||
JSON_NAME = PLUGIN_NAME.strip().lower().replace(' ', '_')
|
||||
JSON_PATH = 'plugins/' + JSON_NAME + '.json'
|
||||
|
||||
# This is where all preferences for this plugin will be stored
|
||||
# You should always prefix your config file name with plugins/,
|
||||
# so as to ensure you dont accidentally clobber a calibre config file
|
||||
prefs = JSONConfig(JSON_PATH)
|
||||
|
||||
# Set defaults
|
||||
prefs.defaults['keys'] = {}
|
||||
prefs.defaults['configured'] = False
|
||||
|
||||
class ConfigWidget(QWidget):
|
||||
def __init__(self, help_file_data):
|
||||
QWidget.__init__(self)
|
||||
|
||||
self.help_file_data = help_file_data
|
||||
self.plugin_keys = prefs['keys']
|
||||
|
||||
# Handle the old plugin's customization string by either converting the
|
||||
# old string to stored keys or by saving the string to a text file of the
|
||||
# user's choice. Either way... get that personal data out of plain sight.
|
||||
from calibre.customize.ui import config
|
||||
sc = config['plugin_customization']
|
||||
val = sc.get(PLUGIN_NAME, None)
|
||||
if val is not None:
|
||||
title = 'Convert existing customization data?'
|
||||
msg = '<p>Convert your existing insecure customization data? (Please '+ \
|
||||
'read the detailed message)'
|
||||
det_msg = DETAILED_MESSAGE
|
||||
|
||||
# Offer to convert the old string to the new format
|
||||
if question_dialog(self, _(title), _(msg), det_msg, True, True):
|
||||
userkeys = parseCustString(str(val))
|
||||
if userkeys:
|
||||
counter = 0
|
||||
# Yay! We found valid customization data... add it to the new plugin
|
||||
for k in userkeys:
|
||||
counter += 1
|
||||
self.plugin_keys['Converted Old Plugin Key - ' + str(counter)] = k
|
||||
msg = '<p><b>' + str(counter) + '</b> User key(s) configured from old plugin customization string'
|
||||
inf = info_dialog(None, _(PLUGIN_NAME + 'info_dlg'), _(msg), show=True)
|
||||
val = sc.pop(PLUGIN_NAME, None)
|
||||
if val is not None:
|
||||
config['plugin_customization'] = sc
|
||||
else:
|
||||
# The existing customization string was invalid and wouldn't have
|
||||
# worked anyway. Offer to save it as a text file and get rid of it.
|
||||
errmsg = '<p>Unknown Error converting user supplied-customization string'
|
||||
r = error_dialog(None, PLUGIN_NAME,
|
||||
_(errmsg), show=True, show_copy_button=False)
|
||||
self.saveOldCustomizationData(str(val))
|
||||
val = sc.pop(PLUGIN_NAME, None)
|
||||
if val is not None:
|
||||
config['plugin_customization'] = sc
|
||||
# If they don't want to convert the old string to keys then
|
||||
# offer to save the old string to a text file and delete the
|
||||
# the old customization string.
|
||||
else:
|
||||
self.saveOldCustomizationData(str(val))
|
||||
val = sc.pop(PLUGIN_NAME, None)
|
||||
if val is not None:
|
||||
config['plugin_customization'] = sc
|
||||
|
||||
# First time run since upgrading to new key storage method, or 0 keys configured.
|
||||
# Prompt to import pre-existing key files.
|
||||
if not prefs['configured']:
|
||||
title = 'Import existing key files?'
|
||||
msg = '<p>This plugin no longer uses *.b64 keyfiles stored in calibre\'s configuration '+ \
|
||||
'directory. Do you have any exsiting key files there (or anywhere) that you\'d '+ \
|
||||
'like to migrate into the new plugin preferences method?'
|
||||
if question_dialog(self, _(title), _(msg)):
|
||||
self.migrate_files()
|
||||
|
||||
# Start Qt Gui dialog layout
|
||||
layout = QVBoxLayout(self)
|
||||
self.setLayout(layout)
|
||||
|
||||
help_layout = QHBoxLayout()
|
||||
layout.addLayout(help_layout)
|
||||
# Add hyperlink to a help file at the right. We will replace the correct name when it is clicked.
|
||||
help_label = QLabel('<a href="http://www.foo.com/">Plugin Help</a>', self)
|
||||
help_label.setTextInteractionFlags(Qt.LinksAccessibleByMouse | Qt.LinksAccessibleByKeyboard)
|
||||
help_label.setAlignment(Qt.AlignRight)
|
||||
help_label.linkActivated.connect(self.help_link_activated)
|
||||
help_layout.addWidget(help_label)
|
||||
|
||||
keys_group_box = QGroupBox(_('Configured Ignoble Keys:'), self)
|
||||
layout.addWidget(keys_group_box)
|
||||
keys_group_box_layout = QHBoxLayout()
|
||||
keys_group_box.setLayout(keys_group_box_layout)
|
||||
|
||||
self.listy = QListWidget(self)
|
||||
self.listy.setToolTip(_('<p>Stored Ignoble keys that will be used for decryption'))
|
||||
self.listy.setSelectionMode(QAbstractItemView.SingleSelection)
|
||||
self.populate_list()
|
||||
keys_group_box_layout.addWidget(self.listy)
|
||||
|
||||
button_layout = QVBoxLayout()
|
||||
keys_group_box_layout.addLayout(button_layout)
|
||||
self._add_key_button = QtGui.QToolButton(self)
|
||||
self._add_key_button.setToolTip(_('Create new key'))
|
||||
self._add_key_button.setIcon(QIcon(I('plus.png')))
|
||||
self._add_key_button.clicked.connect(self.add_key)
|
||||
button_layout.addWidget(self._add_key_button)
|
||||
|
||||
self._delete_key_button = QtGui.QToolButton(self)
|
||||
self._delete_key_button.setToolTip(_('Delete highlighted key'))
|
||||
self._delete_key_button.setIcon(QIcon(I('list_remove.png')))
|
||||
self._delete_key_button.clicked.connect(self.delete_key)
|
||||
button_layout.addWidget(self._delete_key_button)
|
||||
|
||||
self.export_key_button = QtGui.QToolButton(self)
|
||||
self.export_key_button.setToolTip(_('Export highlighted key'))
|
||||
self.export_key_button.setIcon(QIcon(I('save.png')))
|
||||
self.export_key_button.clicked.connect(self.export_key)
|
||||
button_layout.addWidget(self.export_key_button)
|
||||
spacerItem = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
|
||||
button_layout.addItem(spacerItem)
|
||||
|
||||
layout.addSpacing(20)
|
||||
migrate_layout = QHBoxLayout()
|
||||
layout.addLayout(migrate_layout)
|
||||
self.migrate_btn = QPushButton(_('Import Existing Keyfiles'), self)
|
||||
self.migrate_btn.setToolTip(_('<p>Import *.b64 keyfiles (used by older versions of the plugin).'))
|
||||
self.migrate_btn.clicked.connect(self.migrate_wrapper)
|
||||
migrate_layout.setAlignment(Qt.AlignLeft)
|
||||
migrate_layout.addWidget(self.migrate_btn)
|
||||
|
||||
self.resize(self.sizeHint())
|
||||
|
||||
def populate_list(self):
|
||||
for key in self.plugin_keys.keys():
|
||||
self.listy.addItem(QListWidgetItem(key))
|
||||
|
||||
def add_key(self):
|
||||
d = AddKeyDialog(self)
|
||||
d.exec_()
|
||||
|
||||
if d.result() != d.Accepted:
|
||||
# New key generation cancelled.
|
||||
return
|
||||
self.plugin_keys[d.key_name] = generate_keyfile(d.user_name, d.cc_number)
|
||||
|
||||
self.listy.clear()
|
||||
self.populate_list()
|
||||
|
||||
def delete_key(self):
|
||||
if not self.listy.currentItem():
|
||||
return
|
||||
keyname = unicode(self.listy.currentItem().text())
|
||||
if not question_dialog(self, _('Are you sure?'), _('<p>'+
|
||||
'Do you really want to delete the Ignoble key named <strong>%s</strong>?') % keyname,
|
||||
show_copy_button=False, default_yes=False):
|
||||
return
|
||||
del self.plugin_keys[keyname]
|
||||
|
||||
self.listy.clear()
|
||||
self.populate_list()
|
||||
|
||||
def help_link_activated(self, url):
|
||||
def get_help_file_resource():
|
||||
# Copy the HTML helpfile to the plugin directory each time the
|
||||
# link is clicked in case the helpfile is updated in newer plugins.
|
||||
file_path = os.path.join(config_dir, 'plugins', help_file_name)
|
||||
with open(file_path,'w') as f:
|
||||
f.write(self.help_file_data)
|
||||
return file_path
|
||||
url = 'file:///' + get_help_file_resource()
|
||||
open_url(QUrl(url))
|
||||
|
||||
def save_settings(self):
|
||||
prefs['keys'] = self.plugin_keys
|
||||
if prefs['keys']:
|
||||
prefs['configured'] = True
|
||||
else:
|
||||
prefs['configured'] = False
|
||||
|
||||
def migrate_files(self):
|
||||
dynamic[PLUGIN_NAME + 'config_dir'] = config_dir
|
||||
files = choose_files(self, PLUGIN_NAME + 'config_dir',
|
||||
_('Select Ignoble keyfiles to import'), [('Ignoble Keyfiles', ['b64'])], False)
|
||||
if files:
|
||||
counter = 0
|
||||
skipped = 0
|
||||
for filename in files:
|
||||
fpath = os.path.join(config_dir, filename)
|
||||
new_key_name = os.path.splitext(os.path.basename(filename))[0]
|
||||
match = False
|
||||
for key in self.plugin_keys.keys():
|
||||
if caselessStrCmp(new_key_name, key) == 0:
|
||||
match = True
|
||||
break
|
||||
if not match:
|
||||
with open(fpath, 'rb') as f:
|
||||
counter += 1
|
||||
self.plugin_keys[unicode(new_key_name)] = f.read()
|
||||
else:
|
||||
skipped += 1
|
||||
msg = '<p>A key with the name <strong>' + new_key_name + '</strong> already exists! </p>' + \
|
||||
'<p>Skipping key file named <strong>' + filename + '</strong>.</p>' + \
|
||||
'<p>Either delete the existing key and re-migrate, or ' + \
|
||||
'create that key manually with a different name.'
|
||||
inf = info_dialog(None, _(PLUGIN_NAME + 'info_dlg'),
|
||||
_(msg), show=True)
|
||||
|
||||
msg = '<p>Done migrating <strong>' + str(counter) + '</strong> ' + \
|
||||
'key files...</p><p>Skipped <strong>' + str(skipped) + '</strong> key files.'
|
||||
inf = info_dialog(None, _(PLUGIN_NAME + 'info_dlg'),
|
||||
_(msg), show=True)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
def migrate_wrapper(self):
|
||||
if self.migrate_files():
|
||||
self.listy.clear()
|
||||
self.populate_list()
|
||||
|
||||
def export_key(self):
|
||||
if not self.listy.currentItem():
|
||||
errmsg = '<p>No keyfile selected to export. Highlight a keyfile first.'
|
||||
r = error_dialog(None, PLUGIN_NAME,
|
||||
_(errmsg), show=True, show_copy_button=False)
|
||||
return
|
||||
filter = QString('Ignoble Key Files (*.b64)')
|
||||
keyname = unicode(self.listy.currentItem().text())
|
||||
if dynamic.get(PLUGIN_NAME + 'save_dir'):
|
||||
defaultname = os.path.join(dynamic.get(PLUGIN_NAME + 'save_dir'), keyname + '.b64')
|
||||
else:
|
||||
defaultname = os.path.join(os.path.expanduser('~'), keyname + '.b64')
|
||||
filename = str(QtGui.QFileDialog.getSaveFileName(self, "Save Ignoble Key File as...", defaultname,
|
||||
"Ignoble Key Files (*.b64)", filter))
|
||||
if filename:
|
||||
dynamic[PLUGIN_NAME + 'save_dir'] = os.path.split(filename)[0]
|
||||
fname = open(filename, 'w')
|
||||
fname.write(self.plugin_keys[keyname])
|
||||
fname.close()
|
||||
|
||||
def saveOldCustomizationData(self, strdata):
|
||||
filter = QString('Text files (*.txt)')
|
||||
default_basefilename = PLUGIN_NAME + ' old customization data.txt'
|
||||
defaultname = os.path.join(os.path.expanduser('~'), default_basefilename)
|
||||
filename = str(QtGui.QFileDialog.getSaveFileName(self, "Save old plugin style customization data as...", defaultname,
|
||||
"Text Files (*.txt)", filter))
|
||||
if filename:
|
||||
fname = open(filename, 'w')
|
||||
fname.write(strdata)
|
||||
fname.close()
|
||||
45
Calibre_Plugins/ignobleepub_plugin/outputfix.py
Normal file
45
Calibre_Plugins/ignobleepub_plugin/outputfix.py
Normal file
@@ -0,0 +1,45 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Adapted and simplified from the kitchen project
|
||||
#
|
||||
# Kitchen Project Copyright (c) 2012 Red Hat, Inc.
|
||||
#
|
||||
# kitchen is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2.1 of the License, or (at your option) any later version.
|
||||
#
|
||||
# kitchen is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with kitchen; if not, see <http://www.gnu.org/licenses/>
|
||||
#
|
||||
# Authors:
|
||||
# Toshio Kuratomi <toshio@fedoraproject.org>
|
||||
# Seth Vidal
|
||||
#
|
||||
# Portions of code taken from yum/i18n.py and
|
||||
# python-fedora: fedora/textutils.py
|
||||
|
||||
import codecs
|
||||
|
||||
# returns a char string unchanged
|
||||
# returns a unicode string converted to a char string of the passed encoding
|
||||
# return the empty string for anything else
|
||||
def getwriter(encoding):
|
||||
class _StreamWriter(codecs.StreamWriter):
|
||||
def __init__(self, stream):
|
||||
codecs.StreamWriter.__init__(self, stream, 'replace')
|
||||
|
||||
def encode(self, msg, errors='replace'):
|
||||
if isinstance(msg, basestring):
|
||||
if isinstance(msg, str):
|
||||
return (msg, len(msg))
|
||||
return (msg.encode(self.encoding, 'replace'), len(msg))
|
||||
return ('',0)
|
||||
|
||||
_StreamWriter.encoding = encoding
|
||||
return _StreamWriter
|
||||
260
Calibre_Plugins/ignobleepub_plugin/utilities.py
Normal file
260
Calibre_Plugins/ignobleepub_plugin/utilities.py
Normal file
@@ -0,0 +1,260 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from __future__ import with_statement
|
||||
__license__ = 'GPL v3'
|
||||
|
||||
import hashlib
|
||||
|
||||
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
|
||||
|
||||
from PyQt4.Qt import (Qt, QHBoxLayout, QVBoxLayout, QLabel, QLineEdit,
|
||||
QGroupBox, QDialog, QDialogButtonBox)
|
||||
|
||||
from calibre.gui2 import error_dialog
|
||||
from calibre.constants import iswindows
|
||||
|
||||
from calibre_plugins.ignoble_epub.__init__ import PLUGIN_NAME, PLUGIN_VERSION
|
||||
|
||||
DETAILED_MESSAGE = \
|
||||
'You have personal information stored in this plugin\'s customization '+ \
|
||||
'string from a previous version of this plugin.\n\n'+ \
|
||||
'This new version of the plugin can convert that info '+ \
|
||||
'into key data that the new plugin can then use (which doesn\'t '+ \
|
||||
'require personal information to be stored/displayed in an insecure '+ \
|
||||
'manner like the old plugin did).\n\nIf you choose NOT to migrate this data at this time '+ \
|
||||
'you will be prompted to save that personal data to a file elsewhere; and you\'ll have '+ \
|
||||
'to manually re-configure this plugin with your information.\n\nEither way... ' + \
|
||||
'this new version of the plugin will not be responsible for storing that personal '+ \
|
||||
'info in plain sight any longer.'
|
||||
|
||||
class IGNOBLEError(Exception):
|
||||
pass
|
||||
|
||||
def normalize_name(name): # Strip spaces and convert to lowercase.
|
||||
return ''.join(x for x in name.lower() if x != ' ')
|
||||
|
||||
# These are the key ENCRYPTING aes crypto functions
|
||||
def generate_keyfile(name, ccn):
|
||||
# Load the necessary crypto libs.
|
||||
AES = _load_crypto()
|
||||
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()
|
||||
|
||||
return userkey.encode('base64')
|
||||
|
||||
def _load_crypto_libcrypto():
|
||||
if iswindows:
|
||||
libcrypto = find_library('libeay32')
|
||||
else:
|
||||
libcrypto = find_library('crypto')
|
||||
if libcrypto is None:
|
||||
raise IGNOBLEError('libcrypto not found')
|
||||
libcrypto = CDLL(libcrypto)
|
||||
|
||||
AES_MAXNR = 14
|
||||
|
||||
c_char_pp = POINTER(c_char_p)
|
||||
c_int_p = POINTER(c_int)
|
||||
|
||||
class AES_KEY(Structure):
|
||||
_fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))),
|
||||
('rounds', c_int)]
|
||||
AES_KEY_p = POINTER(AES_KEY)
|
||||
|
||||
def F(restype, name, argtypes):
|
||||
func = getattr(libcrypto, name)
|
||||
func.restype = restype
|
||||
func.argtypes = argtypes
|
||||
return func
|
||||
|
||||
AES_set_encrypt_key = F(c_int, 'AES_set_encrypt_key',
|
||||
[c_char_p, c_int, AES_KEY_p])
|
||||
AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',
|
||||
[c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p,
|
||||
c_int])
|
||||
|
||||
class AES(object):
|
||||
def __init__(self, userkey, iv):
|
||||
self._blocksize = len(userkey)
|
||||
self._iv = iv
|
||||
key = self._key = AES_KEY()
|
||||
rv = AES_set_encrypt_key(userkey, len(userkey) * 8, key)
|
||||
if rv < 0:
|
||||
raise IGNOBLEError('Failed to initialize AES Encrypt key')
|
||||
|
||||
def encrypt(self, data):
|
||||
out = create_string_buffer(len(data))
|
||||
rv = AES_cbc_encrypt(data, out, len(data), self._key, self._iv, 1)
|
||||
if rv == 0:
|
||||
raise IGNOBLEError('AES encryption failed')
|
||||
return out.raw
|
||||
return AES
|
||||
|
||||
def _load_crypto_pycrypto():
|
||||
from Crypto.Cipher import AES as _AES
|
||||
|
||||
class AES(object):
|
||||
def __init__(self, key, iv):
|
||||
self._aes = _AES.new(key, _AES.MODE_CBC, iv)
|
||||
|
||||
def encrypt(self, data):
|
||||
return self._aes.encrypt(data)
|
||||
return AES
|
||||
|
||||
def _load_crypto():
|
||||
_aes = None
|
||||
cryptolist = (_load_crypto_libcrypto, _load_crypto_pycrypto)
|
||||
if iswindows:
|
||||
cryptolist = (_load_crypto_pycrypto, _load_crypto_libcrypto)
|
||||
for loader in cryptolist:
|
||||
try:
|
||||
_aes = loader()
|
||||
break
|
||||
except (ImportError, IGNOBLEError):
|
||||
pass
|
||||
return _aes
|
||||
|
||||
def caselessStrCmp(s1, s2):
|
||||
"""
|
||||
A function to case-insensitively compare strings. Python's .lower() function
|
||||
isn't always very accurate when it comes to unicode. Using the standard C lib's
|
||||
strcasecmp instead. Maybe a tad slower, but we're not scouring scads of string lists here.
|
||||
"""
|
||||
str1 = unicode(s1)
|
||||
str2 = unicode(s2)
|
||||
|
||||
c_char_pp = POINTER(c_char_p)
|
||||
c_int_p = POINTER(c_int)
|
||||
|
||||
if iswindows:
|
||||
libc = find_library('msvcrt')
|
||||
else:
|
||||
libc = find_library('c')
|
||||
if libc is None:
|
||||
raise IgnobleError('libc not found')
|
||||
libc = CDLL(libc)
|
||||
|
||||
def F(restype, name, argtypes):
|
||||
func = getattr(libc, name)
|
||||
func.restype = restype
|
||||
func.argtypes = argtypes
|
||||
return func
|
||||
|
||||
if iswindows:
|
||||
_stricmp = F(c_int, '_stricmp', [c_char_p, c_char_p])
|
||||
return _stricmp(str1, str2)
|
||||
strcasecmp = F(c_int, 'strcasecmp', [c_char_p, c_char_p])
|
||||
return strcasecmp(str1, str2)
|
||||
|
||||
class AddKeyDialog(QDialog):
|
||||
def __init__(self, parent=None,):
|
||||
QDialog.__init__(self, parent)
|
||||
self.parent = parent
|
||||
self.setWindowTitle('Create New Ignoble Key')
|
||||
layout = QVBoxLayout(self)
|
||||
self.setLayout(layout)
|
||||
|
||||
data_group_box = QGroupBox('', self)
|
||||
layout.addWidget(data_group_box)
|
||||
data_group_box_layout = QVBoxLayout()
|
||||
data_group_box.setLayout(data_group_box_layout)
|
||||
|
||||
key_group = QHBoxLayout()
|
||||
data_group_box_layout.addLayout(key_group)
|
||||
key_group.addWidget(QLabel('Unique Key Name:', self))
|
||||
self.key_ledit = QLineEdit('', self)
|
||||
self.key_ledit.setToolTip(_('<p>Enter an identifying name for this new Ignoble key.</p>' +
|
||||
'<p>It should be something that will help you remember ' +
|
||||
'what personal information was used to create it.'))
|
||||
key_group.addWidget(self.key_ledit)
|
||||
key_label = QLabel(_(''), self)
|
||||
key_label.setAlignment(Qt.AlignHCenter)
|
||||
data_group_box_layout.addWidget(key_label)
|
||||
|
||||
name_group = QHBoxLayout()
|
||||
data_group_box_layout.addLayout(name_group)
|
||||
name_group.addWidget(QLabel('Your Name:', self))
|
||||
self.name_ledit = QLineEdit('', self)
|
||||
self.name_ledit.setToolTip(_('<p>Enter your name as it appears in your B&N ' +
|
||||
'account and/or on your credit card.</p>' +
|
||||
'<p>It will only be used to generate this ' +
|
||||
'one-time key and won\'t be stored anywhere ' +
|
||||
'in calibre or on your computer.</p>' +
|
||||
'<p>(ex: Jonathan Smith)'))
|
||||
name_group.addWidget(self.name_ledit)
|
||||
name_disclaimer_label = QLabel(_('Will not be stored/saved in configuration data:'), self)
|
||||
name_disclaimer_label.setAlignment(Qt.AlignHCenter)
|
||||
data_group_box_layout.addWidget(name_disclaimer_label)
|
||||
|
||||
ccn_group = QHBoxLayout()
|
||||
data_group_box_layout.addLayout(ccn_group)
|
||||
ccn_group.addWidget(QLabel('Credit Card#:', self))
|
||||
self.cc_ledit = QLineEdit('', self)
|
||||
self.cc_ledit.setToolTip(_('<p>Enter the full credit card number on record ' +
|
||||
'in your B&N account.</p>' +
|
||||
'<p>No spaces or dashes... just the numbers. ' +
|
||||
'This CC# will only be used to generate this ' +
|
||||
'one-time key and won\'t be stored anywhere in ' +
|
||||
'calibre or on your computer.'))
|
||||
ccn_group.addWidget(self.cc_ledit)
|
||||
ccn_disclaimer_label = QLabel(_('Will not be stored/saved in configuration data:'), self)
|
||||
ccn_disclaimer_label.setAlignment(Qt.AlignHCenter)
|
||||
data_group_box_layout.addWidget(ccn_disclaimer_label)
|
||||
layout.addSpacing(20)
|
||||
|
||||
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||||
self.button_box.accepted.connect(self.accept)
|
||||
self.button_box.rejected.connect(self.reject)
|
||||
layout.addWidget(self.button_box)
|
||||
|
||||
self.resize(self.parent.sizeHint())
|
||||
|
||||
def accept(self):
|
||||
match = False
|
||||
if (self.key_ledit.text().isEmpty() or self.name_ledit.text().isEmpty()
|
||||
or self.cc_ledit.text().isEmpty()):
|
||||
errmsg = '<p>All fields are required!'
|
||||
return error_dialog(None, PLUGIN_NAME + 'error_dialog',
|
||||
_(errmsg), show=True, show_copy_button=False)
|
||||
for k in self.parent.plugin_keys.keys():
|
||||
if caselessStrCmp(self.key_ledit.text(), k) == 0:
|
||||
match = True
|
||||
break
|
||||
if match:
|
||||
errmsg = '<p>The key name <strong>%s</strong> is already being used.' % self.key_ledit.text()
|
||||
return error_dialog(None, PLUGIN_NAME + 'error_dialog',
|
||||
_(errmsg), show=True, show_copy_button=False)
|
||||
else:
|
||||
QDialog.accept(self)
|
||||
|
||||
@property
|
||||
def user_name(self):
|
||||
return unicode(self.name_ledit.text()).strip().lower().replace(' ', '')
|
||||
|
||||
@property
|
||||
def cc_number(self):
|
||||
return unicode(self.cc_ledit.text()).strip().replace(' ', '').replace('-','')
|
||||
|
||||
@property
|
||||
def key_name(self):
|
||||
return unicode(self.key_ledit.text())
|
||||
|
||||
def parseCustString(keystuff):
|
||||
userkeys = []
|
||||
ar = keystuff.split(':')
|
||||
for i in ar:
|
||||
try:
|
||||
name, ccn = i.split(',')
|
||||
except:
|
||||
return False
|
||||
# Generate Barnes & Noble EPUB user key from name and credit card number.
|
||||
userkeys.append(generate_keyfile(name, ccn))
|
||||
return userkeys
|
||||
1400
Calibre_Plugins/ignobleepub_plugin/zipfilerugged.py
Normal file
1400
Calibre_Plugins/ignobleepub_plugin/zipfilerugged.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
||||
|
||||
import sys
|
||||
import zlib
|
||||
import zipfile
|
||||
import zipfilerugged
|
||||
import os
|
||||
import os.path
|
||||
import getopt
|
||||
@@ -15,7 +15,7 @@ _FILENAME_OFFSET = 30
|
||||
_MAX_SIZE = 64 * 1024
|
||||
_MIMETYPE = 'application/epub+zip'
|
||||
|
||||
class ZipInfo(zipfile.ZipInfo):
|
||||
class ZipInfo(zipfilerugged.ZipInfo):
|
||||
def __init__(self, *args, **kwargs):
|
||||
if 'compress_type' in kwargs:
|
||||
compress_type = kwargs.pop('compress_type')
|
||||
@@ -27,11 +27,11 @@ class fixZip:
|
||||
self.ztype = 'zip'
|
||||
if zinput.lower().find('.epub') >= 0 :
|
||||
self.ztype = 'epub'
|
||||
self.inzip = zipfile.ZipFile(zinput,'r')
|
||||
self.outzip = zipfile.ZipFile(zoutput,'w')
|
||||
self.inzip = zipfilerugged.ZipFile(zinput,'r')
|
||||
self.outzip = zipfilerugged.ZipFile(zoutput,'w')
|
||||
# open the input zip for reading only as a raw file
|
||||
self.bzf = file(zinput,'rb')
|
||||
|
||||
self.bzf = file(zinput,'rb')
|
||||
|
||||
def getlocalname(self, zi):
|
||||
local_header_offset = zi.header_offset
|
||||
self.bzf.seek(local_header_offset + _FILENAME_LEN_OFFSET)
|
||||
@@ -76,17 +76,17 @@ class fixZip:
|
||||
data = None
|
||||
|
||||
# if not compressed we are good to go
|
||||
if zi.compress_type == zipfile.ZIP_STORED:
|
||||
if zi.compress_type == zipfilerugged.ZIP_STORED:
|
||||
data = self.bzf.read(zi.file_size)
|
||||
|
||||
# if compressed we must decompress it using zlib
|
||||
if zi.compress_type == zipfile.ZIP_DEFLATED:
|
||||
if zi.compress_type == zipfilerugged.ZIP_DEFLATED:
|
||||
cmpdata = self.bzf.read(zi.compress_size)
|
||||
data = self.uncompress(cmpdata)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
|
||||
|
||||
def fix(self):
|
||||
# get the zipinfo for each member of the input archive
|
||||
@@ -95,7 +95,7 @@ class fixZip:
|
||||
|
||||
# if epub write mimetype file first, with no compression
|
||||
if self.ztype == 'epub':
|
||||
nzinfo = ZipInfo('mimetype', compress_type=zipfile.ZIP_STORED)
|
||||
nzinfo = ZipInfo('mimetype', compress_type=zipfilerugged.ZIP_STORED)
|
||||
self.outzip.writestr(nzinfo, _MIMETYPE)
|
||||
|
||||
# write the rest of the files
|
||||
@@ -103,9 +103,9 @@ class fixZip:
|
||||
if zinfo.filename != "mimetype" or self.ztype == '.zip':
|
||||
data = None
|
||||
nzinfo = zinfo
|
||||
try:
|
||||
try:
|
||||
data = self.inzip.read(zinfo.filename)
|
||||
except zipfile.BadZipfile or zipfile.error:
|
||||
except zipfilerugged.BadZipfile or zipfilerugged.error:
|
||||
local_name = self.getlocalname(zinfo)
|
||||
data = self.getfiledata(zinfo)
|
||||
nzinfo.filename = local_name
|
||||
@@ -126,7 +126,7 @@ def usage():
|
||||
inputzip is the source zipfile to fix
|
||||
outputzip is the fixed zip archive
|
||||
"""
|
||||
|
||||
|
||||
|
||||
def repairBook(infile, outfile):
|
||||
if not os.path.exists(infile):
|
||||
@@ -152,5 +152,3 @@ def main(argv=sys.argv):
|
||||
|
||||
if __name__ == '__main__' :
|
||||
sys.exit(main())
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user