# pbkdf2.py This code may be freely used and modified for any purpose.
-import sys, os
import hmac
from struct import pack
import hashlib
-
-# interface to needed routines libalfcrypto
-def _load_libalfcrypto():
- import ctypes
- 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, sizeof
-
- pointer_size = ctypes.sizeof(ctypes.c_voidp)
- name_of_lib = None
- if sys.platform.startswith('darwin'):
- name_of_lib = 'libalfcrypto.dylib'
- elif sys.platform.startswith('win'):
- if pointer_size == 4:
- name_of_lib = 'alfcrypto.dll'
- else:
- name_of_lib = 'alfcrypto64.dll'
- else:
- if pointer_size == 4:
- name_of_lib = 'libalfcrypto32.so'
- else:
- name_of_lib = 'libalfcrypto64.so'
-
- # hard code to local location for libalfcrypto
- libalfcrypto = os.path.join(sys.path[0],name_of_lib)
- if not os.path.isfile(libalfcrypto):
- libalfcrypto = os.path.join(sys.path[0], 'lib', name_of_lib)
- if not os.path.isfile(libalfcrypto):
- libalfcrypto = os.path.join('.',name_of_lib)
- if not os.path.isfile(libalfcrypto):
- raise Exception('libalfcrypto not found at %s' % libalfcrypto)
-
- libalfcrypto = CDLL(libalfcrypto)
-
- c_char_pp = POINTER(c_char_p)
- c_int_p = POINTER(c_int)
-
-
- def F(restype, name, argtypes):
- func = getattr(libalfcrypto, name)
- func.restype = restype
- func.argtypes = argtypes
- return func
-
- # aes cbc decryption
- #
- # struct aes_key_st {
- # unsigned long rd_key[4 *(AES_MAXNR + 1)];
- # int rounds;
- # };
- #
- # typedef struct aes_key_st AES_KEY;
- #
- # int AES_set_decrypt_key(const unsigned char *userKey, const int bits, AES_KEY *key);
- #
- #
- # void AES_cbc_encrypt(const unsigned char *in, unsigned char *out,
- # const unsigned long length, const AES_KEY *key,
- # unsigned char *ivec, const int enc);
-
- AES_MAXNR = 14
-
- class AES_KEY(Structure):
- _fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))), ('rounds', c_int)]
-
- AES_KEY_p = POINTER(AES_KEY)
- 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])
-
-
-
- # Pukall 1 Cipher
- # unsigned char *PC1(const unsigned char *key, unsigned int klen, const unsigned char *src,
- # unsigned char *dest, unsigned int len, int decryption);
-
- PC1 = F(c_char_p, 'PC1', [c_char_p, c_ulong, c_char_p, c_char_p, c_ulong, c_ulong])
-
- # Topaz Encryption
- # typedef struct _TpzCtx {
- # unsigned int v[2];
- # } TpzCtx;
- #
- # void topazCryptoInit(TpzCtx *ctx, const unsigned char *key, int klen);
- # void topazCryptoDecrypt(const TpzCtx *ctx, const unsigned char *in, unsigned char *out, int len);
-
- class TPZ_CTX(Structure):
- _fields_ = [('v', c_long * 2)]
-
- TPZ_CTX_p = POINTER(TPZ_CTX)
- topazCryptoInit = F(None, 'topazCryptoInit', [TPZ_CTX_p, c_char_p, c_ulong])
- topazCryptoDecrypt = F(None, 'topazCryptoDecrypt', [TPZ_CTX_p, c_char_p, c_char_p, c_ulong])
-
-
- class AES_CBC(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 Exception('AES CBC 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 Exception('Failed to initialize AES CBC key')
-
- def decrypt(self, data):
- out = create_string_buffer(len(data))
- mutable_iv = create_string_buffer(self._iv, len(self._iv))
- rv = AES_cbc_encrypt(data, out, len(data), self._keyctx, mutable_iv, 0)
- if rv == 0:
- raise Exception('AES CBC decryption failed')
- return out.raw
-
- class Pukall_Cipher(object):
- def __init__(self):
- self.key = None
-
- def PC1(self, key, src, decryption=True):
- self.key = key
- out = create_string_buffer(len(src))
- de = 0
+import aescbc
+
+class Pukall_Cipher(object):
+ def __init__(self):
+ self.key = None
+
+ def PC1(self, key, src, decryption=True):
+ sum1 = 0;
+ sum2 = 0;
+ keyXorVal = 0;
+ if len(key)!=16:
+ raise Exception("PC1: Bad key length")
+ wkey = []
+ for i in range(8):
+ wkey.append(key[i*2]<<8 | key[i*2+1])
+ dst = bytearray(len(src))
+ for i in range(len(src)):
+ temp1 = 0;
+ byteXorVal = 0;
+ for j in range(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 = src[i]
+ if not decryption:
+ keyXorVal = curByte * 257;
+ curByte = ((curByte ^ (byteXorVal >> 8)) ^ byteXorVal) & 0xFF
if decryption:
- de = 1
- rv = PC1(key, len(key), src, out, len(src), de)
- return out.raw
-
- class Topaz_Cipher(object):
- def __init__(self):
- self._ctx = None
-
- def ctx_init(self, key):
- tpz_ctx = self._ctx = TPZ_CTX()
- topazCryptoInit(tpz_ctx, key, len(key))
- return tpz_ctx
-
- def decrypt(self, data, ctx=None):
- if ctx == None:
- ctx = self._ctx
- out = create_string_buffer(len(data))
- topazCryptoDecrypt(ctx, data, out, len(data))
- return out.raw
-
- print("Using Library AlfCrypto DLL/DYLIB/SO")
- return (AES_CBC, Pukall_Cipher, Topaz_Cipher)
-
-
-def _load_python_alfcrypto():
-
- import aescbc
-
- class Pukall_Cipher(object):
- def __init__(self):
- self.key = None
-
- def PC1(self, key, src, decryption=True):
- sum1 = 0;
- sum2 = 0;
- keyXorVal = 0;
- if len(key)!=16:
- raise Exception('Pukall_Cipher: Bad key length.')
- wkey = []
- for i in range(8):
- wkey.append(ord(key[i*2])<<8 | ord(key[i*2+1]))
- dst = ""
- for i in range(len(src)):
- temp1 = 0;
- byteXorVal = 0;
- for j in range(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 range(8):
- wkey[j] ^= keyXorVal;
- dst+=chr(curByte)
- return dst
-
- class Topaz_Cipher(object):
- def __init__(self):
- self._ctx = None
-
- def ctx_init(self, key):
- ctx1 = 0x0CAFFE19E
- for keyChar in key:
- keyByte = ord(keyChar)
- ctx2 = ctx1
- ctx1 = ((((ctx1 >>2) * (ctx1 >>7))&0xFFFFFFFF) ^ (keyByte * keyByte * 0x0F902007)& 0xFFFFFFFF )
- self._ctx = [ctx1, ctx2]
- return [ctx1,ctx2]
-
- def decrypt(self, data, ctx=None):
- if ctx == None:
- ctx = self._ctx
- ctx1 = ctx[0]
- ctx2 = ctx[1]
- plainText = ""
- for dataChar in data:
- dataByte = ord(dataChar)
- m = (dataByte ^ ((ctx1 >> 3) &0xFF) ^ ((ctx2<<3) & 0xFF)) &0xFF
- ctx2 = ctx1
- ctx1 = (((ctx1 >> 2) * (ctx1 >> 7)) &0xFFFFFFFF) ^((m * m * 0x0F902007) &0xFFFFFFFF)
- plainText += chr(m)
- return plainText
-
- class AES_CBC(object):
- def __init__(self):
- self._key = None
- self._iv = None
- self.aes = None
-
- def set_decrypt_key(self, userkey, iv):
- self._key = userkey
- self._iv = iv
- self.aes = aescbc.AES_CBC(userkey, aescbc.noPadding(), len(userkey))
-
- def decrypt(self, data):
- iv = self._iv
- cleartext = self.aes.decrypt(iv + data)
- return cleartext
-
- print("Using Library AlfCrypto Python")
- return (AES_CBC, Pukall_Cipher, Topaz_Cipher)
-
-
-def _load_crypto():
- AES_CBC = Pukall_Cipher = Topaz_Cipher = None
- cryptolist = (_load_libalfcrypto, _load_python_alfcrypto)
- for loader in cryptolist:
- try:
- AES_CBC, Pukall_Cipher, Topaz_Cipher = loader()
- break
- except (ImportError, Exception):
- pass
- return AES_CBC, Pukall_Cipher, Topaz_Cipher
-
-AES_CBC, Pukall_Cipher, Topaz_Cipher = _load_crypto()
+ keyXorVal = curByte * 257;
+ for j in range(8):
+ wkey[j] ^= keyXorVal;
+ dst[i] = curByte
+ return bytes(dst)
+
+class Topaz_Cipher(object):
+ def __init__(self):
+ self._ctx = None
+
+ def ctx_init(self, key):
+ ctx1 = 0x0CAFFE19E
+ if isinstance(key, str):
+ key = key.encode('latin-1')
+ for keyByte in key:
+ ctx2 = ctx1
+ ctx1 = ((((ctx1 >>2) * (ctx1 >>7))&0xFFFFFFFF) ^ (keyByte * keyByte * 0x0F902007)& 0xFFFFFFFF )
+ self._ctx = [ctx1, ctx2]
+ return [ctx1,ctx2]
+
+ def decrypt(self, data, ctx=None):
+ if ctx == None:
+ ctx = self._ctx
+ ctx1 = ctx[0]
+ ctx2 = ctx[1]
+ plainText = ""
+ if isinstance(data, str):
+ data = data.encode('latin-1')
+ for dataByte in data:
+ m = (dataByte ^ ((ctx1 >> 3) &0xFF) ^ ((ctx2<<3) & 0xFF)) &0xFF
+ ctx2 = ctx1
+ ctx1 = (((ctx1 >> 2) * (ctx1 >> 7)) &0xFFFFFFFF) ^((m * m * 0x0F902007) &0xFFFFFFFF)
+ plainText += chr(m)
+ return plainText
+
+class AES_CBC(object):
+ def __init__(self):
+ self._key = None
+ self._iv = None
+ self.aes = None
+
+ def set_decrypt_key(self, userkey, iv):
+ self._key = userkey
+ self._iv = iv
+ self.aes = aescbc.AES_CBC(userkey, aescbc.noPadding(), len(userkey))
+
+ def decrypt(self, data):
+ iv = self._iv
+ cleartext = self.aes.decrypt(iv + data)
+ return cleartext
class KeyIVGen(object):
diff --git a/DeDRM_plugin/alfcrypto64.dll b/DeDRM_plugin/alfcrypto64.dll
deleted file mode 100644
index 7bef68e..0000000
Binary files a/DeDRM_plugin/alfcrypto64.dll and /dev/null differ
diff --git a/DeDRM_plugin/alfcrypto_src.zip b/DeDRM_plugin/alfcrypto_src.zip
deleted file mode 100644
index 269810c..0000000
Binary files a/DeDRM_plugin/alfcrypto_src.zip and /dev/null differ
diff --git a/DeDRM_plugin/androidkindlekey.py b/DeDRM_plugin/androidkindlekey.py
index e4b6cbe..74208e0 100755
--- a/DeDRM_plugin/androidkindlekey.py
+++ b/DeDRM_plugin/androidkindlekey.py
@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-
# androidkindlekey.py
-# Copyright © 2010-20 by Thom, Apprentice Harper et al.
+# Copyright © 2010-22 by Thom, Apprentice Harper et al.
# Revision history:
# 1.0 - AmazonSecureStorage.xml decryption to serial number
@@ -14,13 +14,14 @@
# 1.4 - Fix some problems identified by Aldo Bleeker
# 1.5 - Fix another problem identified by Aldo Bleeker
# 2.0 - Python 3 compatibility
+# 2.1 - Remove OpenSSL support; only support PyCryptodome
"""
Retrieve Kindle for Android Serial Number.
"""
__license__ = 'GPL v3'
-__version__ = '2.0'
+__version__ = '2.1'
import os
import sys
@@ -33,67 +34,13 @@ from hashlib import md5
from io import BytesIO
from binascii import a2b_hex, b2a_hex
-# Routines common to Mac and PC
+try:
+ from Cryptodome.Cipher import AES, DES
+except ImportError:
+ from Crypto.Cipher import AES, DES
-# Wrap a stream so that output gets flushed immediately
-# and also make sure that any unicode strings get
-# encoded using "replace" before writing them.
-class SafeUnbuffered:
- def __init__(self, stream):
- self.stream = stream
- self.encoding = stream.encoding
- if self.encoding == None:
- self.encoding = "utf-8"
- def write(self, data):
- if isinstance(data,str):
- data = data.encode(self.encoding,"replace")
- self.stream.buffer.write(data)
- self.stream.buffer.flush()
-
- def __getattr__(self, attr):
- return getattr(self.stream, attr)
+# Routines common to Mac and PC
-try:
- from calibre.constants import iswindows, isosx
-except:
- iswindows = sys.platform.startswith('win')
- isosx = sys.platform.startswith('darwin')
-
-def unicode_argv():
- if iswindows:
- # Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
- # strings.
-
- # Versions 2.x of Python don't support Unicode in sys.argv on
- # Windows, with the underlying Windows API instead replacing multi-byte
- # characters with '?'. So use shell32.GetCommandLineArgvW to get sys.argv
- # as a list of Unicode strings and encode them as utf-8
-
- from ctypes import POINTER, byref, cdll, c_int, windll
- from ctypes.wintypes import LPCWSTR, LPWSTR
-
- GetCommandLineW = cdll.kernel32.GetCommandLineW
- GetCommandLineW.argtypes = []
- GetCommandLineW.restype = LPCWSTR
-
- CommandLineToArgvW = windll.shell32.CommandLineToArgvW
- CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
- CommandLineToArgvW.restype = POINTER(LPWSTR)
-
- cmd = GetCommandLineW()
- argc = c_int(0)
- argv = CommandLineToArgvW(cmd, byref(argc))
- if argc.value > 0:
- # Remove Python executable and commands if present
- start = argc.value - len(sys.argv)
- return [argv[i] for i in
- range(start, argc.value)]
- # if we don't have any arguments at all, just pass back script name
- # this should never happen
- return ["kindlekey.py"]
- else:
- argvencoding = sys.stdin.encoding or "utf-8"
- return [arg if isinstance(arg, str) else str(arg, argvencoding) for arg in sys.argv]
class DrmException(Exception):
pass
@@ -102,6 +49,20 @@ STORAGE = "backup.ab"
STORAGE1 = "AmazonSecureStorage.xml"
STORAGE2 = "map_data_storage.db"
+
+def unpad(data, padding=16):
+ if sys.version_info[0] == 2:
+ pad_len = ord(data[-1])
+ else:
+ pad_len = data[-1]
+
+ return data[:-pad_len]
+
+def pad(data, padding_len=16):
+ padding_data_len = padding_len - (len(data) % padding_len)
+ plaintext = data + chr(padding_data_len) * padding_data_len
+ return plaintext
+
class AndroidObfuscation(object):
'''AndroidObfuscation
For the key, it's written in java, and run in android dalvikvm
@@ -109,24 +70,16 @@ class AndroidObfuscation(object):
key = a2b_hex('0176e04c9408b1702d90be333fd53523')
+ def _get_cipher(self):
+ return AES.new(self.key, AES.MODE_ECB)
+
def encrypt(self, plaintext):
- cipher = self._get_cipher()
- padding = len(self.key) - len(plaintext) % len(self.key)
- plaintext += chr(padding) * padding
- return b2a_hex(cipher.encrypt(plaintext.encode('utf-8')))
+ pt = pad(plaintext.encode('utf-8'), 16)
+ return b2a_hex(self._get_cipher().encrypt(pt))
def decrypt(self, ciphertext):
- cipher = self._get_cipher()
- plaintext = cipher.decrypt(a2b_hex(ciphertext))
- return plaintext[:-ord(plaintext[-1])]
-
- def _get_cipher(self):
- try:
- from Crypto.Cipher import AES
- return AES.new(self.key)
- except ImportError:
- from aescbc import AES, noPadding
- return AES(self.key, padding=noPadding())
+ ct = a2b_hex(ciphertext)
+ return unpad(self._get_cipher().decrypt(ct), 16)
class AndroidObfuscationV2(AndroidObfuscation):
'''AndroidObfuscationV2
@@ -143,12 +96,7 @@ class AndroidObfuscationV2(AndroidObfuscation):
self.iv = key[8:16]
def _get_cipher(self):
- try :
- from Crypto.Cipher import DES
- return DES.new(self.key, DES.MODE_CBC, self.iv)
- except ImportError:
- from python_des import Des, CBC
- return Des(self.key, CBC, self.iv)
+ return DES.new(self.key, DES.MODE_CBC, self.iv)
def parse_preference(path):
''' parse android's shared preference xml '''
@@ -329,9 +277,7 @@ def usage(progname):
def cli_main():
- sys.stdout=SafeUnbuffered(sys.stdout)
- sys.stderr=SafeUnbuffered(sys.stderr)
- argv=unicode_argv()
+ argv=sys.argv
progname = os.path.basename(argv[0])
print("{0} v{1}\nCopyright © 2010-2020 Thom, Apprentice Harper et al.".format(progname,__version__))
@@ -450,7 +396,7 @@ def gui_main():
return
self.status['text'] = "Select backup.ab file"
- argv=unicode_argv()
+ argv=sys.argv()
progpath, progname = os.path.split(argv[0])
root = tkinter.Tk()
root.title("Kindle for Android Key Extraction v.{0}".format(__version__))
diff --git a/DeDRM_plugin/argv_utils.py b/DeDRM_plugin/argv_utils.py
index fd4e03b..2ca4776 100644
--- a/DeDRM_plugin/argv_utils.py
+++ b/DeDRM_plugin/argv_utils.py
@@ -1,14 +1,17 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
-import sys, os
-import locale
-import codecs
-import importlib
+import sys
# get sys.argv arguments and encode them into utf-8
-def unicode_argv():
- if sys.platform.startswith('win'):
+def unicode_argv(default_name):
+
+ try:
+ from calibre.constants import iswindows
+ except:
+ iswindows = sys.platform.startswith('win')
+
+ if iswindows:
# Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
# strings.
@@ -38,50 +41,8 @@ def unicode_argv():
range(start, argc.value)]
# if we don't have any arguments at all, just pass back script name
# this should never happen
- return ["DeDRM.py"]
+ return [ default_name ]
else:
argvencoding = sys.stdin.encoding or "utf-8"
- return [arg if isinstance(arg, str) else str(arg, argvencoding) for arg in sys.argv]
-
-
-def add_cp65001_codec():
- try:
- codecs.lookup('cp65001')
- except LookupError:
- codecs.register(
- lambda name: name == 'cp65001' and codecs.lookup('utf-8') or None)
- return
-
-
-def set_utf8_default_encoding():
- if sys.getdefaultencoding() == 'utf-8':
- return
-
- # Regenerate setdefaultencoding.
- importlib.reload(sys)
- sys.setdefaultencoding('utf-8')
-
- for attr in dir(locale):
- if attr[0:3] != 'LC_':
- continue
- aref = getattr(locale, attr)
- try:
- locale.setlocale(aref, '')
- except locale.Error:
- continue
- try:
- lang = locale.getlocale(aref)[0]
- except (TypeError, ValueError):
- continue
- if lang:
- try:
- locale.setlocale(aref, (lang, 'UTF-8'))
- except locale.Error:
- os.environ[attr] = lang + '.UTF-8'
- try:
- locale.setlocale(locale.LC_ALL, '')
- except locale.Error:
- pass
- return
-
+ return [arg if (isinstance(arg, str) or isinstance(arg,unicode)) else str(arg, argvencoding) for arg in sys.argv]
diff --git a/DeDRM_plugin/askfolder_ed.py b/DeDRM_plugin/askfolder_ed.py
deleted file mode 100644
index 8c586fe..0000000
--- a/DeDRM_plugin/askfolder_ed.py
+++ /dev/null
@@ -1,213 +0,0 @@
-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
-
-# to work around tk_chooseDirectory not properly returning unicode paths on Windows
-# need to use a dialog that can be hacked up to actually return full unicode paths
-# originally based on AskFolder from EasyDialogs for Windows but modified to fix it
-# to actually use unicode for path
-
-# The original license for EasyDialogs is as follows
-#
-# Copyright (c) 2003-2005 Jimmy Retzlaff
-#
-# Permission is hereby granted, free of charge, to any person obtaining a
-# copy of this software and associated documentation files (the "Software"),
-# to deal in the Software without restriction, including without limitation
-# the rights to use, copy, modify, merge, publish, distribute, sublicense,
-# and/or sell copies of the Software, and to permit persons to whom the
-# Software is furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
-# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
-# DEALINGS IN THE SOFTWARE.
-
-# Adjusted for Python 3, September 2020
-
-"""
-AskFolder(...) -- Ask the user to select a folder Windows specific
-"""
-
-import os
-
-import ctypes
-from ctypes import POINTER, byref, cdll, c_int, windll
-from ctypes.wintypes import LPCWSTR, LPWSTR
-import ctypes.wintypes as wintypes
-
-
-__all__ = ['AskFolder']
-
-# Load required Windows DLLs
-ole32 = ctypes.windll.ole32
-shell32 = ctypes.windll.shell32
-user32 = ctypes.windll.user32
-
-
-# Windows Constants
-BFFM_INITIALIZED = 1
-BFFM_SETOKTEXT = 1129
-BFFM_SETSELECTIONA = 1126
-BFFM_SETSELECTIONW = 1127
-BIF_EDITBOX = 16
-BS_DEFPUSHBUTTON = 1
-CB_ADDSTRING = 323
-CB_GETCURSEL = 327
-CB_SETCURSEL = 334
-CDM_SETCONTROLTEXT = 1128
-EM_GETLINECOUNT = 186
-EM_GETMARGINS = 212
-EM_POSFROMCHAR = 214
-EM_SETSEL = 177
-GWL_STYLE = -16
-IDC_STATIC = -1
-IDCANCEL = 2
-IDNO = 7
-IDOK = 1
-IDYES = 6
-MAX_PATH = 260
-OFN_ALLOWMULTISELECT = 512
-OFN_ENABLEHOOK = 32
-OFN_ENABLESIZING = 8388608
-OFN_ENABLETEMPLATEHANDLE = 128
-OFN_EXPLORER = 524288
-OFN_OVERWRITEPROMPT = 2
-OPENFILENAME_SIZE_VERSION_400 = 76
-PBM_GETPOS = 1032
-PBM_SETMARQUEE = 1034
-PBM_SETPOS = 1026
-PBM_SETRANGE = 1025
-PBM_SETRANGE32 = 1030
-PBS_MARQUEE = 8
-PM_REMOVE = 1
-SW_HIDE = 0
-SW_SHOW = 5
-SW_SHOWNORMAL = 1
-SWP_NOACTIVATE = 16
-SWP_NOMOVE = 2
-SWP_NOSIZE = 1
-SWP_NOZORDER = 4
-VER_PLATFORM_WIN32_NT = 2
-WM_COMMAND = 273
-WM_GETTEXT = 13
-WM_GETTEXTLENGTH = 14
-WM_INITDIALOG = 272
-WM_NOTIFY = 78
-
-# Windows function prototypes
-BrowseCallbackProc = ctypes.WINFUNCTYPE(ctypes.c_int, wintypes.HWND, ctypes.c_uint, wintypes.LPARAM, wintypes.LPARAM)
-
-# Windows types
-LPCTSTR = ctypes.c_char_p
-LPTSTR = ctypes.c_char_p
-LPVOID = ctypes.c_voidp
-TCHAR = ctypes.c_char
-
-class BROWSEINFO(ctypes.Structure):
- _fields_ = [
- ("hwndOwner", wintypes.HWND),
- ("pidlRoot", LPVOID),
- ("pszDisplayName", LPTSTR),
- ("lpszTitle", LPCTSTR),
- ("ulFlags", ctypes.c_uint),
- ("lpfn", BrowseCallbackProc),
- ("lParam", wintypes.LPARAM),
- ("iImage", ctypes.c_int)
- ]
-
-
-# Utilities
-def CenterWindow(hwnd):
- desktopRect = GetWindowRect(user32.GetDesktopWindow())
- myRect = GetWindowRect(hwnd)
- x = width(desktopRect) // 2 - width(myRect) // 2
- y = height(desktopRect) // 2 - height(myRect) // 2
- user32.SetWindowPos(hwnd, 0,
- desktopRect.left + x,
- desktopRect.top + y,
- 0, 0,
- SWP_NOACTIVATE | SWP_NOSIZE | SWP_NOZORDER
- )
-
-
-def GetWindowRect(hwnd):
- rect = wintypes.RECT()
- user32.GetWindowRect(hwnd, ctypes.byref(rect))
- return rect
-
-def width(rect):
- return rect.right-rect.left
-
-def height(rect):
- return rect.bottom-rect.top
-
-
-def AskFolder(
- message=None,
- version=None,
- defaultLocation=None,
- location=None,
- windowTitle=None,
- actionButtonLabel=None,
- cancelButtonLabel=None,
- multiple=None):
- """Display a dialog asking the user for select a folder.
- modified to use unicode strings as much as possible
- returns unicode path
- """
-
- def BrowseCallback(hwnd, uMsg, lParam, lpData):
- if uMsg == BFFM_INITIALIZED:
- if actionButtonLabel:
- label = str(actionButtonLabel, errors='replace')
- user32.SendMessageW(hwnd, BFFM_SETOKTEXT, 0, label)
- if cancelButtonLabel:
- label = str(cancelButtonLabel, errors='replace')
- cancelButton = user32.GetDlgItem(hwnd, IDCANCEL)
- if cancelButton:
- user32.SetWindowTextW(cancelButton, label)
- if windowTitle:
- title = str(windowTitle, errors='replace')
- user32.SetWindowTextW(hwnd, title)
- if defaultLocation:
- user32.SendMessageW(hwnd, BFFM_SETSELECTIONW, 1, defaultLocation.replace('/', '\\'))
- if location:
- x, y = location
- desktopRect = wintypes.RECT()
- user32.GetWindowRect(0, ctypes.byref(desktopRect))
- user32.SetWindowPos(hwnd, 0,
- desktopRect.left + x,
- desktopRect.top + y, 0, 0,
- SWP_NOACTIVATE | SWP_NOSIZE | SWP_NOZORDER)
- else:
- CenterWindow(hwnd)
- return 0
-
- # This next line is needed to prevent gc of the callback
- callback = BrowseCallbackProc(BrowseCallback)
-
- browseInfo = BROWSEINFO()
- browseInfo.pszDisplayName = ctypes.c_char_p('\0' * (MAX_PATH+1))
- browseInfo.lpszTitle = message
- browseInfo.lpfn = callback
-
- pidl = shell32.SHBrowseForFolder(ctypes.byref(browseInfo))
- if not pidl:
- result = None
- else:
- path = LPCWSTR(" " * (MAX_PATH+1))
- shell32.SHGetPathFromIDListW(pidl, path)
- ole32.CoTaskMemFree(pidl)
- result = path.value
- return result
-
-
-
-
diff --git a/DeDRM_plugin/config.py b/DeDRM_plugin/config.py
index 3d0f321..82ee89f 100755
--- a/DeDRM_plugin/config.py
+++ b/DeDRM_plugin/config.py
@@ -6,28 +6,69 @@ __license__ = 'GPL v3'
# Python 3, September 2020
# Standard Python modules.
-import os, traceback, json, codecs
+import sys, os, traceback, json, codecs, base64, time
from PyQt5.Qt import (Qt, QWidget, QHBoxLayout, QVBoxLayout, QLabel, QLineEdit,
- QGroupBox, QPushButton, QListWidget, QListWidgetItem,
- QAbstractItemView, QIcon, QDialog, QDialogButtonBox, QUrl)
+ QGroupBox, QPushButton, QListWidget, QListWidgetItem, QCheckBox,
+ QAbstractItemView, QIcon, QDialog, QDialogButtonBox, QUrl,
+ QCheckBox, QComboBox)
from PyQt5 import Qt as QtGui
from zipfile import ZipFile
+
+#@@CALIBRE_COMPAT_CODE@@
+
+
# calibre modules and constants.
from calibre.gui2 import (error_dialog, question_dialog, info_dialog, open_url,
choose_dir, choose_files, choose_save_file)
from calibre.utils.config import dynamic, config_dir, JSONConfig
from calibre.constants import iswindows, isosx
-# modules from this plugin's zipfile.
-from calibre_plugins.dedrm.__init__ import PLUGIN_NAME, PLUGIN_VERSION
-from calibre_plugins.dedrm.__init__ import RESOURCE_NAME as help_file_name
-from calibre_plugins.dedrm.utilities import uStrCmp
-import calibre_plugins.dedrm.prefs as prefs
-import calibre_plugins.dedrm.androidkindlekey as androidkindlekey
+from __init__ import PLUGIN_NAME, PLUGIN_VERSION
+from __version import RESOURCE_NAME as help_file_name
+from utilities import uStrCmp
+
+import prefs
+import androidkindlekey
+
+def checkForDeACSMkeys():
+ try:
+ from calibre_plugins.deacsm.libadobeAccount import exportAccountEncryptionKeyDER, getAccountUUID
+ except:
+ # Looks like DeACSM is not installed.
+ return None, None
+
+ try:
+ from calibre.ptempfile import TemporaryFile
+
+
+ acc_uuid = getAccountUUID()
+ if acc_uuid is None:
+ return None, None
+
+ name = "DeACSM_uuid_" + getAccountUUID()
+
+ # Unfortunately, the DeACSM plugin only has code to export to a file, not to return raw key bytes.
+ # Make a temporary file, have the plugin write to that, then read (& delete) that file.
+
+ with TemporaryFile(suffix='.der') as tmp_key_file:
+ export_result = exportAccountEncryptionKeyDER(tmp_key_file)
+
+ if (export_result is False):
+ return None, None
+
+ # Read key file
+ with open(tmp_key_file,'rb') as keyfile:
+ new_key_value = keyfile.read()
+
+ return new_key_value, name
+ except:
+ traceback.print_exc()
+ return None, None
+
class ConfigWidget(QWidget):
def __init__(self, plugin_path, alfdir):
@@ -50,6 +91,10 @@ class ConfigWidget(QWidget):
self.tempdedrmprefs['serials'] = list(self.dedrmprefs['serials'])
self.tempdedrmprefs['adobewineprefix'] = self.dedrmprefs['adobewineprefix']
self.tempdedrmprefs['kindlewineprefix'] = self.dedrmprefs['kindlewineprefix']
+ self.tempdedrmprefs['deobfuscate_fonts'] = self.dedrmprefs['deobfuscate_fonts']
+ self.tempdedrmprefs['remove_watermarks'] = self.dedrmprefs['remove_watermarks']
+ self.tempdedrmprefs['lcp_passphrases'] = list(self.dedrmprefs['lcp_passphrases'])
+ self.tempdedrmprefs['adobe_pdf_passphrases'] = list(self.dedrmprefs['adobe_pdf_passphrases'])
# Start Qt Gui dialog layout
layout = QVBoxLayout(self)
@@ -73,8 +118,8 @@ class ConfigWidget(QWidget):
button_layout = QVBoxLayout()
keys_group_box_layout.addLayout(button_layout)
self.bandn_button = QtGui.QPushButton(self)
- self.bandn_button.setToolTip(_("Click to manage keys for Barnes and Noble ebooks"))
- self.bandn_button.setText("Barnes and Noble ebooks")
+ self.bandn_button.setToolTip(_("Click to manage keys for ADE books with PassHash algorithm.
Commonly used by Barnes and Noble"))
+ self.bandn_button.setText("ADE PassHash (B&&N) ebooks")
self.bandn_button.clicked.connect(self.bandn_keys)
self.kindle_android_button = QtGui.QPushButton(self)
self.kindle_android_button.setToolTip(_("Click to manage keys for Kindle for Android ebooks"))
@@ -82,7 +127,7 @@ class ConfigWidget(QWidget):
self.kindle_android_button.clicked.connect(self.kindle_android)
self.kindle_serial_button = QtGui.QPushButton(self)
self.kindle_serial_button.setToolTip(_("Click to manage eInk Kindle serial numbers for Kindle ebooks"))
- self.kindle_serial_button.setText("eInk Kindle ebooks")
+ self.kindle_serial_button.setText("Kindle eInk ebooks")
self.kindle_serial_button.clicked.connect(self.kindle_serials)
self.kindle_key_button = QtGui.QPushButton(self)
self.kindle_key_button.setToolTip(_("Click to manage keys for Kindle for Mac/PC ebooks"))
@@ -100,13 +145,37 @@ class ConfigWidget(QWidget):
self.ereader_button.setToolTip(_("Click to manage keys for eReader ebooks"))
self.ereader_button.setText("eReader ebooks")
self.ereader_button.clicked.connect(self.ereader_keys)
+ self.lcp_button = QtGui.QPushButton(self)
+ self.lcp_button.setToolTip(_("Click to manage passphrases for Readium LCP ebooks"))
+ self.lcp_button.setText("Readium LCP ebooks")
+ self.lcp_button.clicked.connect(self.readium_lcp_keys)
+ self.pdf_keys_button = QtGui.QPushButton(self)
+ self.pdf_keys_button.setToolTip(_("Click to manage PDF file passphrases"))
+ self.pdf_keys_button.setText("Adobe PDF passwords")
+ self.pdf_keys_button.clicked.connect(self.pdf_passphrases)
+
button_layout.addWidget(self.kindle_serial_button)
button_layout.addWidget(self.kindle_android_button)
+ button_layout.addWidget(self.kindle_key_button)
+ button_layout.addSpacing(15)
+ button_layout.addWidget(self.adept_button)
button_layout.addWidget(self.bandn_button)
+ button_layout.addWidget(self.pdf_keys_button)
+ button_layout.addSpacing(15)
button_layout.addWidget(self.mobi_button)
button_layout.addWidget(self.ereader_button)
- button_layout.addWidget(self.adept_button)
- button_layout.addWidget(self.kindle_key_button)
+ button_layout.addWidget(self.lcp_button)
+
+
+ self.chkFontObfuscation = QtGui.QCheckBox(_("Deobfuscate EPUB fonts"))
+ self.chkFontObfuscation.setToolTip("Deobfuscates fonts in EPUB files after DRM removal")
+ self.chkFontObfuscation.setChecked(self.tempdedrmprefs["deobfuscate_fonts"])
+ button_layout.addWidget(self.chkFontObfuscation)
+
+ self.chkRemoveWatermarks = QtGui.QCheckBox(_("Remove watermarks"))
+ self.chkRemoveWatermarks.setToolTip("Tries to remove watermarks from files")
+ self.chkRemoveWatermarks.setChecked(self.tempdedrmprefs["remove_watermarks"])
+ button_layout.addWidget(self.chkRemoveWatermarks)
self.resize(self.sizeHint())
@@ -141,13 +210,21 @@ class ConfigWidget(QWidget):
d.exec_()
def bandn_keys(self):
- d = ManageKeysDialog(self,"Barnes and Noble Key",self.tempdedrmprefs['bandnkeys'], AddBandNKeyDialog, 'b64')
+ d = ManageKeysDialog(self,"ADE PassHash Key",self.tempdedrmprefs['bandnkeys'], AddBandNKeyDialog, 'b64')
d.exec_()
def ereader_keys(self):
d = ManageKeysDialog(self,"eReader Key",self.tempdedrmprefs['ereaderkeys'], AddEReaderDialog, 'b63')
d.exec_()
+ def readium_lcp_keys(self):
+ d = ManageKeysDialog(self,"Readium LCP passphrase",self.tempdedrmprefs['lcp_passphrases'], AddLCPKeyDialog)
+ d.exec_()
+
+ def pdf_passphrases(self):
+ d = ManageKeysDialog(self,"PDF passphrase",self.tempdedrmprefs['adobe_pdf_passphrases'], AddPDFPassDialog)
+ d.exec_()
+
def help_link_activated(self, url):
def get_help_file_resource():
# Copy the HTML helpfile to the plugin directory each time the
@@ -170,6 +247,10 @@ class ConfigWidget(QWidget):
self.dedrmprefs.set('adobewineprefix', self.tempdedrmprefs['adobewineprefix'])
self.dedrmprefs.set('kindlewineprefix', self.tempdedrmprefs['kindlewineprefix'])
self.dedrmprefs.set('configured', True)
+ self.dedrmprefs.set('deobfuscate_fonts', self.chkFontObfuscation.isChecked())
+ self.dedrmprefs.set('remove_watermarks', self.chkRemoveWatermarks.isChecked())
+ self.dedrmprefs.set('lcp_passphrases', self.tempdedrmprefs['lcp_passphrases'])
+ self.dedrmprefs.set('adobe_pdf_passphrases', self.tempdedrmprefs['adobe_pdf_passphrases'])
self.dedrmprefs.writeprefs()
def load_resource(self, name):
@@ -246,7 +327,13 @@ class ManageKeysDialog(QDialog):
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)
+ try:
+ # QT 6
+ spacerItem = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Policy.Minimum, QtGui.QSizePolicy.Policy.Expanding)
+ except AttributeError:
+ # QT 5
+ spacerItem = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
+
button_layout.addItem(spacerItem)
if self.wineprefix is not None:
@@ -290,6 +377,8 @@ class ManageKeysDialog(QDialog):
for key in self.plugin_keys:
self.listy.addItem(QListWidgetItem(key))
+ self.listy.setMinimumWidth(self.listy.sizeHintForColumn(0) + 20)
+
def add_key(self):
d = self.create_key(self)
d.exec_()
@@ -297,21 +386,62 @@ class ManageKeysDialog(QDialog):
if d.result() != d.Accepted:
# New key generation cancelled.
return
- new_key_value = d.key_value
- if type(self.plugin_keys) == dict:
- if new_key_value in self.plugin_keys.values():
- old_key_name = [name for name, value in self.plugin_keys.items() if value == new_key_value][0]
- info_dialog(None, "{0} {1}: Duplicate {2}".format(PLUGIN_NAME, PLUGIN_VERSION,self.key_type_name),
- "The new {1} is the same as the existing {1} named {0} and has not been added.".format(old_key_name,self.key_type_name), show=True)
- return
- self.plugin_keys[d.key_name] = new_key_value
+
+ if hasattr(d, "k_key_list") and d.k_key_list is not None:
+ # importing multiple keys
+ idx = -1
+ dup_key_count = 0
+ added_key_count = 0
+
+ while True:
+ idx = idx + 1
+ try:
+ new_key_value = d.k_key_list[idx]
+ except:
+ break
+
+ if type(self.plugin_keys) == dict:
+ if new_key_value in self.plugin_keys.values():
+ dup_key_count = dup_key_count + 1
+ continue
+ self.plugin_keys[d.k_name_list[idx]] = new_key_value
+ added_key_count = added_key_count + 1
+ else:
+ if new_key_value in self.plugin_keys:
+ dup_key_count = dup_key_count + 1
+ continue
+ self.plugin_keys.append(new_key_value)
+ added_key_count = added_key_count + 1
+
+ if (added_key_count > 0 or dup_key_count > 0):
+ if (added_key_count == 0):
+ info_dialog(None, "{0} {1}: Adding {2}".format(PLUGIN_NAME, PLUGIN_VERSION,self.key_type_name),
+ "Skipped adding {0} duplicate / existing keys.".format(dup_key_count), show=True, show_copy_button=False)
+ elif (dup_key_count == 0):
+ info_dialog(None, "{0} {1}: Adding {2}".format(PLUGIN_NAME, PLUGIN_VERSION,self.key_type_name),
+ "Added {0} new keys.".format(added_key_count), show=True, show_copy_button=False)
+ else:
+ info_dialog(None, "{0} {1}: Adding {2}".format(PLUGIN_NAME, PLUGIN_VERSION,self.key_type_name),
+ "Added {0} new keys, skipped adding {1} existing keys.".format(added_key_count, dup_key_count), show=True, show_copy_button=False)
+
else:
- if new_key_value in self.plugin_keys:
- info_dialog(None, "{0} {1}: Duplicate {2}".format(PLUGIN_NAME, PLUGIN_VERSION,self.key_type_name),
- "This {0} is already in the list of {0}s has not been added.".format(self.key_type_name), show=True)
- return
+ # Import single key
+ new_key_value = d.key_value
+ if type(self.plugin_keys) == dict:
+ if new_key_value in self.plugin_keys.values():
+ old_key_name = [name for name, value in self.plugin_keys.items() if value == new_key_value][0]
+ info_dialog(None, "{0} {1}: Duplicate {2}".format(PLUGIN_NAME, PLUGIN_VERSION,self.key_type_name),
+ "The new {1} is the same as the existing {1} named {0} and has not been added.".format(old_key_name,self.key_type_name), show=True)
+ return
+ self.plugin_keys[d.key_name] = new_key_value
+ else:
+ if new_key_value in self.plugin_keys:
+ info_dialog(None, "{0} {1}: Duplicate {2}".format(PLUGIN_NAME, PLUGIN_VERSION,self.key_type_name),
+ "This {0} is already in the list of {0}s has not been added.".format(self.key_type_name), show=True)
+ return
+
+ self.plugin_keys.append(d.key_value)
- self.plugin_keys.append(d.key_value)
self.listy.clear()
self.populate_list()
@@ -451,7 +581,6 @@ class ManageKeysDialog(QDialog):
class RenameKeyDialog(QDialog):
def __init__(self, parent=None,):
- print(repr(self), repr(parent))
QDialog.__init__(self, parent)
self.parent = parent
self.setWindowTitle("{0} {1}: Rename {0}".format(PLUGIN_NAME, PLUGIN_VERSION, parent.key_type_name))
@@ -502,91 +631,211 @@ class RenameKeyDialog(QDialog):
return str(self.key_ledit.text()).strip()
+class AddBandNKeyDialog(QDialog):
+ def update_form(self, idx):
+ self.cbType.hide()
+ if idx == 1:
+ self.add_fields_for_passhash()
+ elif idx == 2:
+ self.add_fields_for_b64_passhash()
+ elif idx == 3:
+ self.add_fields_for_ade_passhash()
+ elif idx == 4:
+ self.add_fields_for_windows_nook()
+ elif idx == 5:
+ self.add_fields_for_android_nook()
+ def add_fields_for_ade_passhash(self):
-class AddBandNKeyDialog(QDialog):
- def __init__(self, parent=None,):
- QDialog.__init__(self, parent)
- self.parent = parent
- self.setWindowTitle("{0} {1}: Create New Barnes & Noble Key".format(PLUGIN_NAME, PLUGIN_VERSION))
- layout = QVBoxLayout(self)
- self.setLayout(layout)
+ self.ade_extr_group_box = QGroupBox("", self)
+ ade_extr_group_box_layout = QVBoxLayout()
+ self.ade_extr_group_box.setLayout(ade_extr_group_box_layout)
- data_group_box = QGroupBox("", self)
- layout.addWidget(data_group_box)
- data_group_box_layout = QVBoxLayout()
- data_group_box.setLayout(data_group_box_layout)
+ self.layout.addWidget(self.ade_extr_group_box)
- key_group = QHBoxLayout()
- data_group_box_layout.addLayout(key_group)
- key_group.addWidget(QLabel("Unique Key Name:", self))
+ ade_extr_group_box_layout.addWidget(QLabel("Click \"OK\" to try and dump PassHash data \nfrom Adobe Digital Editions. This works if\nyou've opened your PassHash books in ADE before.", self))
+
+ self.button_box.hide()
+
+ self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
+ self.button_box.accepted.connect(self.accept_ade_dump_passhash)
+ self.button_box.rejected.connect(self.reject)
+ self.layout.addWidget(self.button_box)
+
+ self.resize(self.sizeHint())
+
+
+ def add_fields_for_android_nook(self):
+
+ self.andr_nook_group_box = QGroupBox("", self)
+ andr_nook_group_box_layout = QVBoxLayout()
+ self.andr_nook_group_box.setLayout(andr_nook_group_box_layout)
+
+ self.layout.addWidget(self.andr_nook_group_box)
+
+ ph_key_name_group = QHBoxLayout()
+ andr_nook_group_box_layout.addLayout(ph_key_name_group)
+ ph_key_name_group.addWidget(QLabel("Unique Key Name:", self))
+ self.key_ledit = QLineEdit("", self)
+ self.key_ledit.setToolTip(_("Enter an identifying name for this new key.
"))
+ ph_key_name_group.addWidget(self.key_ledit)
+
+ andr_nook_group_box_layout.addWidget(QLabel("Hidden in the Android application data is a " +
+ "folder\nnamed '.adobe-digital-editions'. Please enter\nthe full path to that folder.", self))
+
+ ph_path_group = QHBoxLayout()
+ andr_nook_group_box_layout.addLayout(ph_path_group)
+ ph_path_group.addWidget(QLabel("Path:", self))
+ self.cc_ledit = QLineEdit("", self)
+ self.cc_ledit.setToolTip(_("Enter path to .adobe-digital-editions folder.
"))
+ ph_path_group.addWidget(self.cc_ledit)
+
+ self.button_box.hide()
+
+ self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
+ self.button_box.accepted.connect(self.accept_android_nook)
+ self.button_box.rejected.connect(self.reject)
+ self.layout.addWidget(self.button_box)
+
+ self.resize(self.sizeHint())
+
+ def add_fields_for_windows_nook(self):
+
+ self.win_nook_group_box = QGroupBox("", self)
+ win_nook_group_box_layout = QVBoxLayout()
+ self.win_nook_group_box.setLayout(win_nook_group_box_layout)
+
+ self.layout.addWidget(self.win_nook_group_box)
+
+ ph_key_name_group = QHBoxLayout()
+ win_nook_group_box_layout.addLayout(ph_key_name_group)
+ ph_key_name_group.addWidget(QLabel("Unique Key Name:", self))
+ self.key_ledit = QLineEdit("", self)
+ self.key_ledit.setToolTip(_("Enter an identifying name for this new key.
"))
+ ph_key_name_group.addWidget(self.key_ledit)
+
+ self.button_box.hide()
+
+ self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
+ self.button_box.accepted.connect(self.accept_win_nook)
+ self.button_box.rejected.connect(self.reject)
+ self.layout.addWidget(self.button_box)
+
+ self.resize(self.sizeHint())
+
+ def add_fields_for_b64_passhash(self):
+
+ self.passhash_group_box = QGroupBox("", self)
+ passhash_group_box_layout = QVBoxLayout()
+ self.passhash_group_box.setLayout(passhash_group_box_layout)
+
+ self.layout.addWidget(self.passhash_group_box)
+
+ ph_key_name_group = QHBoxLayout()
+ passhash_group_box_layout.addLayout(ph_key_name_group)
+ ph_key_name_group.addWidget(QLabel("Unique Key Name:", self))
self.key_ledit = QLineEdit("", self)
self.key_ledit.setToolTip(_("Enter an identifying name for this new key.
" +
"It should be something that will help you remember " +
"what personal information was used to create it."))
- key_group.addWidget(self.key_ledit)
+ ph_key_name_group.addWidget(self.key_ledit)
- name_group = QHBoxLayout()
- data_group_box_layout.addLayout(name_group)
- name_group.addWidget(QLabel("B&N/nook account email address:", self))
+ ph_name_group = QHBoxLayout()
+ passhash_group_box_layout.addLayout(ph_name_group)
+ ph_name_group.addWidget(QLabel("Base64 key string:", self))
+ self.cc_ledit = QLineEdit("", self)
+ self.cc_ledit.setToolTip(_("
Enter the Base64 key string
"))
+ ph_name_group.addWidget(self.cc_ledit)
+
+ self.button_box.hide()
+
+ self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
+ self.button_box.accepted.connect(self.accept_b64_passhash)
+ self.button_box.rejected.connect(self.reject)
+ self.layout.addWidget(self.button_box)
+
+ self.resize(self.sizeHint())
+
+
+ def add_fields_for_passhash(self):
+
+ self.passhash_group_box = QGroupBox("", self)
+ passhash_group_box_layout = QVBoxLayout()
+ self.passhash_group_box.setLayout(passhash_group_box_layout)
+
+ self.layout.addWidget(self.passhash_group_box)
+
+ ph_key_name_group = QHBoxLayout()
+ passhash_group_box_layout.addLayout(ph_key_name_group)
+ ph_key_name_group.addWidget(QLabel("Unique Key Name:", self))
+ self.key_ledit = QLineEdit("", self)
+ self.key_ledit.setToolTip(_("Enter an identifying name for this new key.
" +
+ "It should be something that will help you remember " +
+ "what personal information was used to create it."))
+ ph_key_name_group.addWidget(self.key_ledit)
+
+ ph_name_group = QHBoxLayout()
+ passhash_group_box_layout.addLayout(ph_name_group)
+ ph_name_group.addWidget(QLabel("Username:", self))
self.name_ledit = QLineEdit("", self)
- self.name_ledit.setToolTip(_("
Enter your email address as it appears in your B&N " +
- "account.
" +
- "It will only be used to generate this " +
- "key and won\'t be stored anywhere " +
- "in calibre or on your computer.
" +
- "eg: apprenticeharper@gmail.com
"))
- name_group.addWidget(self.name_ledit)
- name_disclaimer_label = QLabel(_("(Will not be saved in configuration data)"), self)
- name_disclaimer_label.setAlignment(Qt.AlignHCenter)
- data_group_box_layout.addWidget(name_disclaimer_label)
+ self.name_ledit.setToolTip(_("Enter the PassHash username
"))
+ ph_name_group.addWidget(self.name_ledit)
- ccn_group = QHBoxLayout()
- data_group_box_layout.addLayout(ccn_group)
- ccn_group.addWidget(QLabel("B&N/nook account password:", self))
+ ph_pass_group = QHBoxLayout()
+ passhash_group_box_layout.addLayout(ph_pass_group)
+ ph_pass_group.addWidget(QLabel("Password:", self))
self.cc_ledit = QLineEdit("", self)
- self.cc_ledit.setToolTip(_("Enter the password " +
- "for your B&N account.
" +
- "The password will only be used to generate this " +
- "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 saved in configuration data)'), self)
- ccn_disclaimer_label.setAlignment(Qt.AlignHCenter)
- data_group_box_layout.addWidget(ccn_disclaimer_label)
- layout.addSpacing(10)
-
- key_group = QHBoxLayout()
- data_group_box_layout.addLayout(key_group)
- key_group.addWidget(QLabel("Retrieved key:", self))
- self.key_display = QLabel("", self)
- self.key_display.setToolTip(_("Click the Retrieve Key button to fetch your B&N encryption key from the B&N servers"))
- key_group.addWidget(self.key_display)
- self.retrieve_button = QtGui.QPushButton(self)
- self.retrieve_button.setToolTip(_("Click to retrieve your B&N encryption key from the B&N servers"))
- self.retrieve_button.setText("Retrieve Key")
- self.retrieve_button.clicked.connect(self.retrieve_key)
- key_group.addWidget(self.retrieve_button)
- layout.addSpacing(10)
+ self.cc_ledit.setToolTip(_("
Enter the PassHash password
"))
+ ph_pass_group.addWidget(self.cc_ledit)
+ self.button_box.hide()
+
self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
- self.button_box.accepted.connect(self.accept)
+ self.button_box.accepted.connect(self.accept_passhash)
self.button_box.rejected.connect(self.reject)
- layout.addWidget(self.button_box)
+ self.layout.addWidget(self.button_box)
+
+ self.resize(self.sizeHint())
+
+
+
+ def __init__(self, parent=None,):
+ QDialog.__init__(self, parent)
+ self.parent = parent
+ self.setWindowTitle("{0} {1}: Create New PassHash (B&N) Key".format(PLUGIN_NAME, PLUGIN_VERSION))
+ self.layout = QVBoxLayout(self)
+ self.setLayout(self.layout)
+
+ self.cbType = QComboBox()
+ self.cbType.addItem("--- Select key type ---")
+ self.cbType.addItem("Adobe PassHash username & password")
+ self.cbType.addItem("Base64-encoded PassHash key string")
+ self.cbType.addItem("Extract passhashes from Adobe Digital Editions")
+ self.cbType.addItem("Extract key from Nook Windows application")
+ self.cbType.addItem("Extract key from Nook Android application")
+ self.cbType.currentIndexChanged.connect(lambda: self.update_form(self.cbType.currentIndex()))
+ self.layout.addWidget(self.cbType)
+
+ self.button_box = QDialogButtonBox(QDialogButtonBox.Cancel)
+ self.button_box.rejected.connect(self.reject)
+ self.layout.addWidget(self.button_box)
self.resize(self.sizeHint())
@property
def key_name(self):
- return str(self.key_ledit.text()).strip()
+ try:
+ return str(self.key_ledit.text()).strip()
+ except:
+ return self.result_data_name
@property
def key_value(self):
- return str(self.key_display.text()).strip()
+ return self.result_data
@property
def user_name(self):
@@ -596,28 +845,194 @@ class AddBandNKeyDialog(QDialog):
def cc_number(self):
return str(self.cc_ledit.text()).strip()
- def retrieve_key(self):
- from calibre_plugins.dedrm.ignoblekeyfetch import fetch_key as fetch_bandn_key
- fetched_key = fetch_bandn_key(self.user_name,self.cc_number)
- if fetched_key == "":
- errmsg = "Could not retrieve key. Check username, password and intenet connectivity and try again."
+
+ @property
+ def k_name_list(self):
+ # If the plugin supports returning multiple keys, return a list of names.
+ if self.k_full_name_list is not None and self.k_full_key_list is not None:
+ return self.k_full_name_list
+ return None
+
+ @property
+ def k_key_list(self):
+ # If the plugin supports returning multiple keys, return a list of keys.
+ if self.k_full_name_list is not None and self.k_full_key_list is not None:
+ return self.k_full_key_list
+ return None
+
+
+
+ def accept_android_nook(self):
+
+ if len(self.key_name) < 4:
+ errmsg = "Key name must be at least 4 characters long!"
+ return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
+
+ path_to_ade_data = self.cc_number
+
+ if (os.path.isfile(os.path.join(path_to_ade_data, ".adobe-digital-editions", "activation.xml"))):
+ path_to_ade_data = os.path.join(path_to_ade_data, ".adobe-digital-editions")
+ elif (os.path.isfile(os.path.join(path_to_ade_data, "activation.xml"))):
+ pass
+ else:
+ errmsg = "This isn't the correct path, or the data is invalid."
+ return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
+
+ from ignoblekeyAndroid import dump_keys
+ store_result = dump_keys(path_to_ade_data)
+
+ if len(store_result) == 0:
+ errmsg = "Failed to extract keys. Is this the correct folder?"
+ return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
+
+ if len(store_result) == 1:
+ # Found exactly one key. Store it with that name.
+ self.result_data = store_result[0]
+ QDialog.accept(self)
+ return
+
+ # Found multiple keys
+ keys = []
+ names = []
+ idx = 1
+ for key in store_result:
+ keys.append(key)
+ names.append(self.key_name + "_" + str(idx))
+ idx = idx + 1
+
+ self.k_full_name_list = names
+ self.k_full_key_list = keys
+ QDialog.accept(self)
+ return
+
+
+ def accept_ade_dump_passhash(self):
+
+ try:
+ from adobekey_get_passhash import passhash_keys
+ keys, names = passhash_keys()
+ except:
+ errmsg = "Failed to grab PassHash keys from ADE."
+ return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
+
+ # Take the first new key we found.
+
+ idx = -1
+ new_keys = []
+ new_names = []
+ for key in keys:
+ idx = idx + 1
+ if key in self.parent.plugin_keys.values():
+ continue
+
+ new_keys.append(key)
+ new_names.append(names[idx])
+
+ if len(new_keys) == 0:
+ # Okay, we didn't find anything. How do we get rid of the window?
+ errmsg = "Didn't find any PassHash keys in ADE."
error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
- else:
- self.key_display.setText(fetched_key)
+ QDialog.reject(self)
+ return
- def accept(self):
+ # Add new keys to list.
+ self.k_full_name_list = new_names
+ self.k_full_key_list = new_keys
+ QDialog.accept(self)
+ return
+
+
+
+ def accept_win_nook(self):
+
+ if len(self.key_name) < 4:
+ errmsg = "Key name must be at least 4 characters long!"
+ return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
+
+ try:
+ from ignoblekeyWindowsStore import dump_keys
+ store_result = dump_keys(False)
+ except:
+ errmsg = "Failed to import from Nook Microsoft Store app."
+ return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
+
+ try:
+ # Try the Nook Study app
+ from ignoblekeyNookStudy import nookkeys
+ study_result = nookkeys()
+ except:
+ errmsg = "Failed to import from Nook Study app."
+ return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
+
+ # Add all found keys to a list
+ keys = []
+ names = []
+ idx = 1
+ for key in store_result:
+ keys.append(key)
+ names.append(self.key_name + "_nookStore_" + str(idx))
+ idx = idx + 1
+ idx = 1
+ for key in study_result:
+ keys.append(key)
+ names.append(self.key_name + "_nookStudy_" + str(idx))
+ idx = idx + 1
+
+ if len(keys) > 0:
+ self.k_full_name_list = names
+ self.k_full_key_list = keys
+ QDialog.accept(self)
+ return
+
+
+ # Okay, we didn't find anything.
+ errmsg = "Didn't find any Nook keys in the Windows app."
+ error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
+ QDialog.reject(self)
+
+
+ def accept_b64_passhash(self):
+ if len(self.key_name) == 0 or len(self.cc_number) == 0 or self.key_name.isspace() or self.cc_number.isspace():
+ errmsg = "All fields are required!"
+ return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
+
+ if len(self.key_name) < 4:
+ errmsg = "Key name must be at least 4 characters long!"
+ return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
+
+ try:
+ x = base64.b64decode(self.cc_number)
+ except:
+ errmsg = "Key data is no valid base64 string!"
+ return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
+
+
+ self.result_data = self.cc_number
+ QDialog.accept(self)
+
+ def accept_passhash(self):
if len(self.key_name) == 0 or len(self.user_name) == 0 or len(self.cc_number) == 0 or self.key_name.isspace() or self.user_name.isspace() or self.cc_number.isspace():
errmsg = "All fields are required!"
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
if len(self.key_name) < 4:
errmsg = "Key name must be at least 4 characters long!"
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
- if len(self.key_value) == 0:
- self.retrieve_key()
- if len(self.key_value) == 0:
- return
+
+ try:
+ from ignoblekeyGenPassHash import generate_key
+ self.result_data = generate_key(self.user_name, self.cc_number)
+ except:
+ errmsg = "Key generation failed."
+ return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
+
+ if len(self.result_data) == 0:
+ errmsg = "Key generation failed."
+ return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
+
QDialog.accept(self)
+
+
class AddEReaderDialog(QDialog):
def __init__(self, parent=None,):
QDialog.__init__(self, parent)
@@ -672,7 +1087,7 @@ class AddEReaderDialog(QDialog):
@property
def key_value(self):
- from calibre_plugins.dedrm.erdr2pml import getuser_key as generate_ereader_key
+ from erdr2pml import getuser_key as generate_ereader_key
return codecs.encode(generate_ereader_key(self.user_name, self.cc_number),'hex')
@property
@@ -697,57 +1112,90 @@ class AddEReaderDialog(QDialog):
QDialog.accept(self)
-class AddAdeptDialog(QDialog):
+class AddAdeptDialog():
+ # We don't actually need to show a dialog, but the wrapper class is expecting a QDialog here.
+ # Emulate enough methods and parameters so that that works ...
+
+ def exec_(self):
+ return
+
+ def result(self):
+ return True
+
+ @property
+ def Accepted(self):
+ return True
+
def __init__(self, parent=None,):
- QDialog.__init__(self, parent)
+
self.parent = parent
- self.setWindowTitle("{0} {1}: Getting Default Adobe Digital Editions Key".format(PLUGIN_NAME, PLUGIN_VERSION))
- layout = QVBoxLayout(self)
- self.setLayout(layout)
+ self.new_keys = []
+ self.new_names = []
try:
if iswindows or isosx:
- from calibre_plugins.dedrm.adobekey import adeptkeys
+ from adobekey import adeptkeys
- defaultkeys = adeptkeys()
+ defaultkeys, defaultnames = adeptkeys()
else: # linux
- from .wineutils import WineGetKeys
+ from wineutils import WineGetKeys
scriptpath = os.path.join(parent.parent.alfdir,"adobekey.py")
- defaultkeys = WineGetKeys(scriptpath, ".der",parent.getwineprefix())
+ defaultkeys, defaultnames = WineGetKeys(scriptpath, ".der",parent.getwineprefix())
- self.default_key = defaultkeys[0]
+ if sys.version_info[0] < 3:
+ # Python2
+ import itertools
+ zip_function = itertools.izip
+ else:
+ # Python3
+ zip_function = zip
+
+ for key, name in zip_function(defaultkeys, defaultnames):
+ key = codecs.encode(key,'hex').decode("latin-1")
+ if key in self.parent.plugin_keys.values():
+ print("Found key '{0}' in ADE - already present, skipping.".format(name))
+ else:
+ self.new_keys.append(key)
+ self.new_names.append(name)
except:
+ print("Exception while checking for ADE keys")
traceback.print_exc()
- self.default_key = ""
-
- self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
- if len(self.default_key)>0:
- 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("default_key", self)
- self.key_ledit.setToolTip("Enter an identifying name for the current default Adobe Digital Editions key.")
- key_group.addWidget(self.key_ledit)
+
+ # Check for keys in the DeACSM plugin
+ try:
+ key, name = checkForDeACSMkeys()
+
+ if key is not None:
+ key = codecs.encode(key,'hex').decode("latin-1")
+ if key in self.parent.plugin_keys.values():
+ print("Found key '{0}' in DeACSM - already present, skipping.".format(name))
+ else:
+ # Found new key, add that.
+ self.new_keys.append(key)
+ self.new_names.append(name)
+ except:
+ print("Exception while checking for DeACSM keys")
+ traceback.print_exc()
- self.button_box.accepted.connect(self.accept)
- else:
- default_key_error = QLabel("The default encryption key for Adobe Digital Editions could not be found.", self)
- default_key_error.setAlignment(Qt.AlignHCenter)
- layout.addWidget(default_key_error)
- # if no default, bot buttons do the same
- self.button_box.accepted.connect(self.reject)
+ # Just in case ADE and DeACSM are activated with the same account,
+ # check the new_keys list for duplicates and remove them, if they exist.
- self.button_box.rejected.connect(self.reject)
- layout.addWidget(self.button_box)
+ new_keys_2 = []
+ new_names_2 = []
+ i = 0
+ while True:
+ if i >= len(self.new_keys):
+ break
+ if not self.new_keys[i] in new_keys_2:
+ new_keys_2.append(self.new_keys[i])
+ new_names_2.append(self.new_names[i])
+ i = i + 1
+
+ self.k_full_key_list = new_keys_2
+ self.k_full_name_list = new_names_2
- self.resize(self.sizeHint())
@property
def key_name(self):
@@ -755,17 +1203,22 @@ class AddAdeptDialog(QDialog):
@property
def key_value(self):
- return codecs.encode(self.default_key,'hex')
+ return codecs.encode(self.new_keys[0],'hex').decode("latin-1")
+
+ @property
+ def k_name_list(self):
+ # If the plugin supports returning multiple keys, return a list of names.
+ if self.k_full_name_list is not None and self.k_full_key_list is not None:
+ return self.k_full_name_list
+ return None
- def accept(self):
- if len(self.key_name) == 0 or self.key_name.isspace():
- errmsg = "All fields are required!"
- return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
- if len(self.key_name) < 4:
- errmsg = "Key name must be at least 4 characters long!"
- return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
- QDialog.accept(self)
+ @property
+ def k_key_list(self):
+ # If the plugin supports returning multiple keys, return a list of keys.
+ if self.k_full_name_list is not None and self.k_full_key_list is not None:
+ return self.k_full_key_list
+ return None
class AddKindleDialog(QDialog):
@@ -778,14 +1231,14 @@ class AddKindleDialog(QDialog):
try:
if iswindows or isosx:
- from calibre_plugins.dedrm.kindlekey import kindlekeys
+ from kindlekey import kindlekeys
defaultkeys = kindlekeys()
else: # linux
- from .wineutils import WineGetKeys
+ from wineutils import WineGetKeys
scriptpath = os.path.join(parent.parent.alfdir,"kindlekey.py")
- defaultkeys = WineGetKeys(scriptpath, ".k4i",parent.getwineprefix())
+ defaultkeys, defaultnames = WineGetKeys(scriptpath, ".k4i",parent.getwineprefix())
self.default_key = defaultkeys[0]
except:
@@ -803,7 +1256,7 @@ class AddKindleDialog(QDialog):
key_group = QHBoxLayout()
data_group_box_layout.addLayout(key_group)
key_group.addWidget(QLabel("Unique Key Name:", self))
- self.key_ledit = QLineEdit("default_key", self)
+ self.key_ledit = QLineEdit("default_key_" + str(int(time.time())), self)
self.key_ledit.setToolTip("
Enter an identifying name for the current default Kindle for Mac/PC key.")
key_group.addWidget(self.key_ledit)
@@ -873,7 +1326,7 @@ class AddSerialDialog(QDialog):
@property
def key_value(self):
- return str(self.key_ledit.text()).replace(' ', '')
+ return str(self.key_ledit.text()).replace(' ', '').replace('\r', '').replace('\n', '').replace('\t', '')
def accept(self):
if len(self.key_name) == 0 or self.key_name.isspace():
@@ -1013,3 +1466,84 @@ class AddPIDDialog(QDialog):
QDialog.accept(self)
+class AddLCPKeyDialog(QDialog):
+ def __init__(self, parent=None,):
+ QDialog.__init__(self, parent)
+ self.parent = parent
+ self.setWindowTitle("{0} {1}: Add new Readium LCP passphrase".format(PLUGIN_NAME, PLUGIN_VERSION))
+ 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("Readium LCP passphrase:", self))
+ self.key_ledit = QLineEdit("", self)
+ self.key_ledit.setToolTip("Enter your Readium LCP passphrase")
+ key_group.addWidget(self.key_ledit)
+
+ self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
+ self.button_box.accepted.connect(self.accept)
+ self.button_box.rejected.connect(self.reject)
+ layout.addWidget(self.button_box)
+
+ self.resize(self.sizeHint())
+
+ @property
+ def key_name(self):
+ return None
+
+ @property
+ def key_value(self):
+ return str(self.key_ledit.text())
+
+ def accept(self):
+ if len(self.key_value) == 0 or self.key_value.isspace():
+ errmsg = "Please enter your LCP passphrase or click Cancel in the dialog."
+ return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
+ QDialog.accept(self)
+
+class AddPDFPassDialog(QDialog):
+ def __init__(self, parent=None,):
+ QDialog.__init__(self, parent)
+ self.parent = parent
+ self.setWindowTitle("{0} {1}: Add new PDF passphrase".format(PLUGIN_NAME, PLUGIN_VERSION))
+ 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("PDF password:", self))
+ self.key_ledit = QLineEdit("", self)
+ self.key_ledit.setToolTip("Enter the PDF file password.")
+ key_group.addWidget(self.key_ledit)
+
+ self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
+ self.button_box.accepted.connect(self.accept)
+ self.button_box.rejected.connect(self.reject)
+ layout.addWidget(self.button_box)
+
+ self.resize(self.sizeHint())
+
+ @property
+ def key_name(self):
+ return None
+
+ @property
+ def key_value(self):
+ return str(self.key_ledit.text())
+
+ def accept(self):
+ if len(self.key_value) == 0 or self.key_value.isspace():
+ errmsg = "Please enter a PDF password or click Cancel in the dialog."
+ return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
+ QDialog.accept(self)
diff --git a/DeDRM_plugin/convert2xml.py b/DeDRM_plugin/convert2xml.py
index abdaeb3..6af190b 100644
--- a/DeDRM_plugin/convert2xml.py
+++ b/DeDRM_plugin/convert2xml.py
@@ -5,30 +5,13 @@
# For use with Topaz Scripts Version 2.6
# Python 3, September 2020
-# Wrap a stream so that output gets flushed immediately
-# and also make sure that any unicode strings get
-# encoded using "replace" before writing them.
-class SafeUnbuffered:
- def __init__(self, stream):
- self.stream = stream
- self.encoding = stream.encoding
- if self.encoding == None:
- self.encoding = "utf-8"
- def write(self, data):
- if isinstance(data, str):
- data = data.encode(self.encoding,"replace")
- self.stream.buffer.write(data)
- self.stream.buffer.flush()
-
- def __getattr__(self, attr):
- return getattr(self.stream, attr)
+from utilities import SafeUnbuffered
import sys
import csv
import os
import getopt
-from struct import pack
-from struct import unpack
+from struct import pack, unpack
class TpzDRMError(Exception):
pass
diff --git a/DeDRM_plugin/epubfontdecrypt.py b/DeDRM_plugin/epubfontdecrypt.py
new file mode 100644
index 0000000..4baa375
--- /dev/null
+++ b/DeDRM_plugin/epubfontdecrypt.py
@@ -0,0 +1,328 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+# epubfontdecrypt.py
+# Copyright © 2021 by noDRM
+
+# Released under the terms of the GNU General Public Licence, version 3
+#
+
+
+# Revision history:
+# 1 - Initial release
+
+"""
+Decrypts / deobfuscates font files in EPUB files
+"""
+
+from __future__ import print_function
+
+__license__ = 'GPL v3'
+__version__ = "1"
+
+import os
+import traceback
+import zlib
+import zipfile
+from zipfile import ZipInfo, ZipFile, ZIP_STORED, ZIP_DEFLATED
+from zeroedzipinfo import ZeroedZipInfo
+from contextlib import closing
+from lxml import etree
+import itertools
+import hashlib
+import binascii
+
+
+class Decryptor(object):
+ def __init__(self, obfuscationkeyIETF, obfuscationkeyAdobe, encryption):
+ enc = lambda tag: '{%s}%s' % ('http://www.w3.org/2001/04/xmlenc#', tag)
+ dsig = lambda tag: '{%s}%s' % ('http://www.w3.org/2000/09/xmldsig#', tag)
+ self.obfuscation_key_Adobe = obfuscationkeyAdobe
+ self.obfuscation_key_IETF = obfuscationkeyIETF
+
+ self._encryption = etree.fromstring(encryption)
+ # This loops through all entries in the "encryption.xml" file
+ # to figure out which files need to be decrypted.
+ self._obfuscatedIETF = obfuscatedIETF = set()
+ self._obfuscatedAdobe = obfuscatedAdobe = set()
+ self._other = other = set()
+
+ self._json_elements_to_remove = json_elements_to_remove = set()
+ self._has_remaining_xml = False
+ expr = './%s/%s/%s' % (enc('EncryptedData'), enc('CipherData'),
+ enc('CipherReference'))
+ for elem in self._encryption.findall(expr):
+ path = elem.get('URI', None)
+ encryption_type_url = (elem.getparent().getparent().find("./%s" % (enc('EncryptionMethod'))).get('Algorithm', None))
+ if path is not None:
+
+ if encryption_type_url == "http://www.idpf.org/2008/embedding":
+ # Font files obfuscated with the IETF algorithm
+ path = path.encode('utf-8')
+ obfuscatedIETF.add(path)
+ if (self.obfuscation_key_IETF is None):
+ self._has_remaining_xml = True
+ else:
+ json_elements_to_remove.add(elem.getparent().getparent())
+
+ elif encryption_type_url == "http://ns.adobe.com/pdf/enc#RC":
+ # Font files obfuscated with the Adobe algorithm.
+ path = path.encode('utf-8')
+ obfuscatedAdobe.add(path)
+ if (self.obfuscation_key_Adobe is None):
+ self._has_remaining_xml = True
+ else:
+ json_elements_to_remove.add(elem.getparent().getparent())
+
+ else:
+ path = path.encode('utf-8')
+ other.add(path)
+ self._has_remaining_xml = True
+ # Other unsupported type.
+
+ for elem in json_elements_to_remove:
+ elem.getparent().remove(elem)
+
+ def check_if_remaining(self):
+ return self._has_remaining_xml
+
+ def get_xml(self):
+ return "\n" + etree.tostring(self._encryption, encoding="utf-8", pretty_print=True, xml_declaration=False).decode("utf-8")
+
+ def decompress(self, bytes):
+ dc = zlib.decompressobj(-15)
+ try:
+ decompressed_bytes = dc.decompress(bytes)
+ ex = dc.decompress(b'Z') + dc.flush()
+ if ex:
+ decompressed_bytes = decompressed_bytes + ex
+ except:
+ # possibly not compressed by zip - just return bytes
+ return bytes, False
+ return decompressed_bytes , True
+
+ def decrypt(self, path, data):
+ if path.encode('utf-8') in self._obfuscatedIETF and self.obfuscation_key_IETF is not None:
+ # de-obfuscate according to the IETF standard
+ data, was_decomp = self.decompress(data)
+
+ if len(data) <= 1040:
+ # de-obfuscate whole file
+ out = self.deobfuscate_single_data(self.obfuscation_key_IETF, data)
+ else:
+ out = self.deobfuscate_single_data(self.obfuscation_key_IETF, data[:1040]) + data[1040:]
+
+ if (not was_decomp):
+ out, was_decomp = self.decompress(out)
+ return out
+
+ elif path.encode('utf-8') in self._obfuscatedAdobe and self.obfuscation_key_Adobe is not None:
+ # de-obfuscate according to the Adobe standard
+ data, was_decomp = self.decompress(data)
+
+ if len(data) <= 1024:
+ # de-obfuscate whole file
+ out = self.deobfuscate_single_data(self.obfuscation_key_Adobe, data)
+ else:
+ out = self.deobfuscate_single_data(self.obfuscation_key_Adobe, data[:1024]) + data[1024:]
+
+ if (not was_decomp):
+ out, was_decomp = self.decompress(out)
+ return out
+
+ else:
+ # Not encrypted or obfuscated
+ return data
+
+ def deobfuscate_single_data(self, key, data):
+ try:
+ msg = bytes([c^k for c,k in zip(data, itertools.cycle(key))])
+ except TypeError:
+ # Python 2
+ msg = ''.join(chr(ord(c)^ord(k)) for c,k in itertools.izip(data, itertools.cycle(key)))
+ return msg
+
+
+
+def decryptFontsBook(inpath, outpath):
+
+ with closing(ZipFile(open(inpath, 'rb'))) as inf:
+ namelist = inf.namelist()
+ if 'META-INF/encryption.xml' not in namelist:
+ return 1
+
+ # Font key handling:
+
+ font_master_key = None
+ adobe_master_encryption_key = None
+
+ contNS = lambda tag: '{%s}%s' % ('urn:oasis:names:tc:opendocument:xmlns:container', tag)
+ path = None
+
+ try:
+ container = etree.fromstring(inf.read("META-INF/container.xml"))
+ rootfiles = container.find(contNS("rootfiles")).findall(contNS("rootfile"))
+ for rootfile in rootfiles:
+ path = rootfile.get("full-path", None)
+ if (path is not None):
+ break
+ except:
+ pass
+
+ # If path is None, we didn't find an OPF, so we probably don't have a font key.
+ # If path is set, it's the path to the main content OPF file.
+
+ if (path is None):
+ print("FontDecrypt: No OPF for font obfuscation found")
+ return 1
+ else:
+ packageNS = lambda tag: '{%s}%s' % ('http://www.idpf.org/2007/opf', tag)
+ metadataDCNS = lambda tag: '{%s}%s' % ('http://purl.org/dc/elements/1.1/', tag)
+
+ try:
+ container = etree.fromstring(inf.read(path))
+ except:
+ container = []
+
+ ## IETF font key algorithm:
+ print("FontDecrypt: Checking {0} for IETF font obfuscation keys ... ".format(path), end='')
+ secret_key_name = None
+ try:
+ secret_key_name = container.get("unique-identifier")
+ except:
+ pass
+
+ try:
+ identify_element = container.find(packageNS("metadata")).find(metadataDCNS("identifier"))
+ if (secret_key_name is None or secret_key_name == identify_element.get("id")):
+ font_master_key = identify_element.text
+ except:
+ pass
+
+ if (font_master_key is not None):
+ if (secret_key_name is None):
+ print("found '%s'" % (font_master_key))
+ else:
+ print("found '%s' (%s)" % (font_master_key, secret_key_name))
+
+ # Trim / remove forbidden characters from the key, then hash it:
+ font_master_key = font_master_key.replace(' ', '')
+ font_master_key = font_master_key.replace('\t', '')
+ font_master_key = font_master_key.replace('\r', '')
+ font_master_key = font_master_key.replace('\n', '')
+ font_master_key = font_master_key.encode('utf-8')
+ font_master_key = hashlib.sha1(font_master_key).digest()
+ else:
+ print("not found")
+
+ ## Adobe font key algorithm
+ print("FontDecrypt: Checking {0} for Adobe font obfuscation keys ... ".format(path), end='')
+
+ try:
+ metadata = container.find(packageNS("metadata"))
+ identifiers = metadata.findall(metadataDCNS("identifier"))
+
+ uid = None
+ uidMalformed = False
+
+ for identifier in identifiers:
+ if identifier.get(packageNS("scheme")) == "UUID":
+ if identifier.text[:9] == "urn:uuid:":
+ uid = identifier.text[9:]
+ else:
+ uid = identifier.text
+ break
+ if identifier.text[:9] == "urn:uuid:":
+ uid = identifier.text[9:]
+ break
+
+
+ if uid is not None:
+ uid = uid.replace(chr(0x20),'').replace(chr(0x09),'')
+ uid = uid.replace(chr(0x0D),'').replace(chr(0x0A),'').replace('-','')
+
+ if len(uid) < 16:
+ uidMalformed = True
+ if not all(c in "0123456789abcdefABCDEF" for c in uid):
+ uidMalformed = True
+
+
+ if not uidMalformed:
+ print("found '{0}'".format(uid))
+ uid = uid + uid
+ adobe_master_encryption_key = binascii.unhexlify(uid[:32])
+
+ if adobe_master_encryption_key is None:
+ print("not found")
+
+ except:
+ print("exception")
+ pass
+
+ # Begin decrypting.
+
+ try:
+ encryption = inf.read('META-INF/encryption.xml')
+ decryptor = Decryptor(font_master_key, adobe_master_encryption_key, encryption)
+ kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
+ with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf:
+
+ # Mimetype needs to be the first entry, so remove it from the list
+ # whereever it is, then add it at the beginning.
+ namelist.remove("mimetype")
+
+ for path in (["mimetype"] + namelist):
+ data = inf.read(path)
+ zi = ZipInfo(path)
+ zi.compress_type=ZIP_DEFLATED
+
+ if path == "mimetype":
+ # mimetype must not be compressed
+ zi.compress_type = ZIP_STORED
+
+ elif path == "META-INF/encryption.xml":
+ # Check if there's still other entries not related to fonts
+ if (decryptor.check_if_remaining()):
+ data = decryptor.get_xml()
+ print("FontDecrypt: There's remaining entries in encryption.xml, adding file ...")
+ else:
+ # No remaining entries, no need for that file.
+ continue
+
+ try:
+ # get the file info, including time-stamp
+ oldzi = inf.getinfo(path)
+ # copy across useful fields
+ zi.date_time = oldzi.date_time
+ zi.comment = oldzi.comment
+ zi.extra = oldzi.extra
+ zi.internal_attr = oldzi.internal_attr
+ # external attributes are dependent on the create system, so copy both.
+ zi.external_attr = oldzi.external_attr
+ zi.volume = oldzi.volume
+ zi.create_system = oldzi.create_system
+ zi.create_version = oldzi.create_version
+
+ if any(ord(c) >= 128 for c in path) or any(ord(c) >= 128 for c in zi.comment):
+ # If the file name or the comment contains any non-ASCII char, set the UTF8-flag
+ zi.flag_bits |= 0x800
+ except:
+ pass
+
+ # Python 3 has a bug where the external_attr is reset to `0o600 << 16`
+ # if it's NULL, so we need a workaround:
+ if zi.external_attr == 0:
+ zi = ZeroedZipInfo(zi)
+
+ if path == "mimetype":
+ outf.writestr(zi, inf.read('mimetype'))
+ elif path == "META-INF/encryption.xml":
+ outf.writestr(zi, data)
+ else:
+ outf.writestr(zi, decryptor.decrypt(path, data))
+ except:
+ print("FontDecrypt: Could not decrypt fonts in {0:s} because of an exception:\n{1:s}".format(os.path.basename(inpath), traceback.format_exc()))
+ traceback.print_exc()
+ return 2
+ return 0
+
diff --git a/DeDRM_plugin/epubtest.py b/DeDRM_plugin/epubtest.py
index ebae4fb..9213df8 100644
--- a/DeDRM_plugin/epubtest.py
+++ b/DeDRM_plugin/epubtest.py
@@ -12,6 +12,7 @@
# 1.00 - Cut to epubtest.py, testing ePub files only by Apprentice Alf
# 1.01 - Added routine for use by Windows DeDRM
# 2.00 - Python 3, September 2020
+# 2.01 - Add new Adobe DRM, add Readium LCP
#
# Written in 2011 by Paul Durrant
# Released with unlicense. See http://unlicense.org/
@@ -52,69 +53,13 @@ import sys, struct, os, traceback
import zlib
import zipfile
import xml.etree.ElementTree as etree
+from argv_utils import unicode_argv
NSMAP = {'adept': 'http://ns.adobe.com/adept',
'enc': 'http://www.w3.org/2001/04/xmlenc#'}
-# Wrap a stream so that output gets flushed immediately
-# and also make sure that any unicode strings get
-# encoded using "replace" before writing them.
-class SafeUnbuffered:
- def __init__(self, stream):
- self.stream = stream
- self.encoding = stream.encoding
- if self.encoding == None:
- self.encoding = "utf-8"
- def write(self, data):
- if isinstance(data, str):
- data = data.encode(self.encoding,"replace")
- self.stream.buffer.write(data)
- self.stream.buffer.flush()
-
- def __getattr__(self, attr):
- return getattr(self.stream, attr)
-
-try:
- from calibre.constants import iswindows, isosx
-except:
- iswindows = sys.platform.startswith('win')
- isosx = sys.platform.startswith('darwin')
-
-def unicode_argv():
- if iswindows:
- # Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
- # strings.
-
- # Versions 2.x of Python don't support Unicode in sys.argv on
- # Windows, with the underlying Windows API instead replacing multi-byte
- # characters with '?'. So use shell32.GetCommandLineArgvW to get sys.argv
- # as a list of Unicode strings and encode them as utf-8
-
- from ctypes import POINTER, byref, cdll, c_int, windll
- from ctypes.wintypes import LPCWSTR, LPWSTR
-
- GetCommandLineW = cdll.kernel32.GetCommandLineW
- GetCommandLineW.argtypes = []
- GetCommandLineW.restype = LPCWSTR
-
- CommandLineToArgvW = windll.shell32.CommandLineToArgvW
- CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
- CommandLineToArgvW.restype = POINTER(LPWSTR)
-
- cmd = GetCommandLineW()
- argc = c_int(0)
- argv = CommandLineToArgvW(cmd, byref(argc))
- if argc.value > 0:
- # Remove Python executable and commands if present
- start = argc.value - len(sys.argv)
- return [argv[i] for i in
- range(start, argc.value)]
- # if we don't have any arguments at all, just pass back script name
- # this should never happen
- return ["epubtest.py"]
- else:
- argvencoding = sys.stdin.encoding or "utf-8"
- return [arg if isinstance(arg, str) else str(arg, argvencoding) for arg in sys.argv]
+from utilities import SafeUnbuffered
+
_FILENAME_LEN_OFFSET = 26
_EXTRA_LEN_OFFSET = 28
@@ -168,36 +113,51 @@ def getfiledata(file, zi):
return data
def encryption(infile):
- # returns encryption: one of Unencrypted, Adobe, B&N and Unknown
- encryption = "Error When Checking."
+ # Supports Adobe (old & new), B&N, Kobo, Apple, Readium LCP.
+ encryption = "Error"
try:
with open(infile,'rb') as infileobject:
bookdata = infileobject.read(58)
# Check for Zip
if bookdata[0:0+2] == b"PK":
- foundrights = False
- foundencryption = False
inzip = zipfile.ZipFile(infile,'r')
namelist = set(inzip.namelist())
- if 'META-INF/rights.xml' not in namelist or 'META-INF/encryption.xml' not in namelist:
+ if (
+ 'META-INF/encryption.xml' in namelist and
+ 'META-INF/license.lcpl' in namelist and
+ b"EncryptedContentKey" in inzip.read("META-INF/encryption.xml")):
+ encryption = "Readium LCP"
+
+ elif 'META-INF/sinf.xml' in namelist and b"fairplay" in inzip.read("META-INF/sinf.xml"):
+ # Untested, just found this info on Google
+ encryption = "Apple"
+
+ elif 'META-INF/rights.xml' in namelist and b"" in inzip.read("META-INF/rights.xml"):
+ # Untested, just found this info on Google
+ encryption = "Kobo"
+
+ elif 'META-INF/rights.xml' not in namelist or 'META-INF/encryption.xml' not in namelist:
encryption = "Unencrypted"
else:
- rights = etree.fromstring(inzip.read('META-INF/rights.xml'))
- adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
- expr = './/%s' % (adept('encryptedKey'),)
- bookkey = ''.join(rights.findtext(expr))
- if len(bookkey) == 172:
- encryption = "Adobe"
- elif len(bookkey) == 64:
- encryption = "B&N"
- else:
+ try:
+ rights = etree.fromstring(inzip.read('META-INF/rights.xml'))
+ adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
+ expr = './/%s' % (adept('encryptedKey'),)
+ bookkey = ''.join(rights.findtext(expr))
+ if len(bookkey) >= 172:
+ encryption = "Adobe"
+ elif len(bookkey) == 64:
+ encryption = "B&N"
+ else:
+ encryption = "Unknown (key len " + str(len(bookkey)) + ")"
+ except:
encryption = "Unknown"
except:
traceback.print_exc()
return encryption
def main():
- argv=unicode_argv()
+ argv=unicode_argv("epubtest.py")
if len(argv) < 2:
print("Give an ePub file as a parameter.")
else:
diff --git a/DeDRM_plugin/epubwatermark.py b/DeDRM_plugin/epubwatermark.py
new file mode 100644
index 0000000..6719935
--- /dev/null
+++ b/DeDRM_plugin/epubwatermark.py
@@ -0,0 +1,344 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+# epubwatermark.py
+# Copyright © 2021 NoDRM
+
+# Revision history:
+# 1.0 - Initial version
+
+# Released under the terms of the GNU General Public Licence, version 3
+#
+
+"""
+Removes various watermarks from EPUB files
+"""
+
+import traceback
+from zipfile import ZipInfo, ZipFile, ZIP_STORED, ZIP_DEFLATED
+from zeroedzipinfo import ZeroedZipInfo
+from contextlib import closing
+from lxml import etree
+import re
+
+# Runs a RegEx over all HTML/XHTML files to remove watermakrs.
+def removeHTMLwatermarks(object, path_to_ebook):
+ try:
+ inf = ZipFile(open(path_to_ebook, 'rb'))
+ namelist = inf.namelist()
+
+ modded_names = []
+ modded_contents = []
+
+ count_adept = 0
+ count_pocketbook = 0
+ count_lemonink_invisible = 0
+ count_lemonink_visible = 0
+ lemonink_trackingID = None
+
+ for file in namelist:
+ if not (file.endswith('.html') or file.endswith('.xhtml') or file.endswith('.xml')):
+ continue
+
+ try:
+ file_str = inf.read(file).decode("utf-8")
+ str_new = file_str
+
+ # Remove Adobe ADEPT watermarks
+ # Match optional newline at the beginning, then a "meta" tag with name = "Adept.expected.resource" or "Adept.resource"
+ # and either a "value" or a "content" element with an Adobe UUID
+ pre_remove = str_new
+ str_new = re.sub(r'((\r\n|\r|\n)\s*)?\', '', str_new)
+ str_new = re.sub(r'((\r\n|\r|\n)\s*)?\', '', str_new)
+
+ if (str_new != pre_remove):
+ count_adept += 1
+
+ # Remove Pocketbook watermarks
+ pre_remove = str_new
+ str_new = re.sub(r'\(.*?)\<\/div\>', '', str_new)
+
+ if (str_new != pre_remove):
+ count_pocketbook += 1
+
+
+ # Remove eLibri / LemonInk watermark
+ # Run this in a loop, as it is possible a file has been watermarked twice ...
+ while True:
+ pre_remove = str_new
+ unique_id = re.search(r']+class="[^"]*(t0x[0-9a-fA-F]{25})[^"]*"[^>]*>', str_new)
+ if (unique_id):
+ lemonink_trackingID = unique_id.groups()[0]
+ count_lemonink_invisible += 1
+ str_new = re.sub(lemonink_trackingID, '', str_new)
+ pre_remove = str_new
+ pm = r'(]+class="[^"]*"[^>]*>)'
+ pm += r'\
(.*?)
'
+ pm += r'\
(.*?)
'
+ str_new = re.sub(pm, r'\1', str_new)
+
+ if (str_new != pre_remove):
+ count_lemonink_visible += 1
+ else:
+ break
+
+ except:
+ traceback.print_exc()
+ continue
+
+ if (file_str == str_new):
+ continue
+
+ modded_names.append(file)
+ modded_contents.append(str_new)
+
+
+ if len(modded_names) == 0:
+ # No file modified, return original
+ return path_to_ebook
+
+ if len(modded_names) != len(modded_contents):
+ # Something went terribly wrong, return original
+ print("Watermark: Error during watermark removal")
+ return path_to_ebook
+
+ # Re-package with modified files:
+ namelist.remove("mimetype")
+
+ try:
+ output = object.temporary_file(".epub").name
+ kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
+ with closing(ZipFile(open(output, 'wb'), 'w', **kwds)) as outf:
+ for path in (["mimetype"] + namelist):
+
+ data = inf.read(path)
+
+ try:
+ modded_index = None
+ modded_index = modded_names.index(path)
+ except:
+ pass
+
+ if modded_index is not None:
+ # Found modified file - replace contents
+ data = modded_contents[modded_index]
+
+ zi = ZipInfo(path)
+ oldzi = inf.getinfo(path)
+ try:
+ zi.compress_type = oldzi.compress_type
+ if path == "mimetype":
+ zi.compress_type = ZIP_STORED
+ zi.date_time = oldzi.date_time
+ zi.comment = oldzi.comment
+ zi.extra = oldzi.extra
+ zi.internal_attr = oldzi.internal_attr
+ zi.external_attr = oldzi.external_attr
+ zi.volume = oldzi.volume
+ zi.create_system = oldzi.create_system
+ zi.create_version = oldzi.create_version
+
+ if any(ord(c) >= 128 for c in path) or any(ord(c) >= 128 for c in zi.comment):
+ # If the file name or the comment contains any non-ASCII char, set the UTF8-flag
+ zi.flag_bits |= 0x800
+ except:
+ pass
+
+ # Python 3 has a bug where the external_attr is reset to `0o600 << 16`
+ # if it's NULL, so we need a workaround:
+ if zi.external_attr == 0:
+ zi = ZeroedZipInfo(zi)
+
+
+ outf.writestr(zi, data)
+ except:
+ traceback.print_exc()
+ return path_to_ebook
+
+ if (count_adept > 0):
+ print("Watermark: Successfully stripped {0} ADEPT watermark(s) from ebook.".format(count_adept))
+
+ if (count_lemonink_invisible > 0 or count_lemonink_visible > 0):
+ print("Watermark: Successfully stripped {0} visible and {1} invisible LemonInk watermark(s) (\"{2}\") from ebook."
+ .format(count_lemonink_visible, count_lemonink_invisible, lemonink_trackingID))
+
+ if (count_pocketbook > 0):
+ print("Watermark: Successfully stripped {0} Pocketbook watermark(s) from ebook.".format(count_pocketbook))
+
+ return output
+
+ except:
+ traceback.print_exc()
+ return path_to_ebook
+
+
+
+
+# Finds the main OPF file, then uses RegEx to remove watermarks
+def removeOPFwatermarks(object, path_to_ebook):
+ contNS = lambda tag: '{%s}%s' % ('urn:oasis:names:tc:opendocument:xmlns:container', tag)
+ opf_path = None
+
+ try:
+ inf = ZipFile(open(path_to_ebook, 'rb'))
+ container = etree.fromstring(inf.read("META-INF/container.xml"))
+ rootfiles = container.find(contNS("rootfiles")).findall(contNS("rootfile"))
+ for rootfile in rootfiles:
+ opf_path = rootfile.get("full-path", None)
+ if (opf_path is not None):
+ break
+ except:
+ traceback.print_exc()
+ return path_to_ebook
+
+ # If path is None, we didn't find an OPF, so we probably don't have a font key.
+ # If path is set, it's the path to the main content OPF file.
+
+ if (opf_path is None):
+ # No OPF found - no watermark
+ return path_to_ebook
+ else:
+ try:
+ container_str = inf.read(opf_path).decode("utf-8")
+ container_str_new = container_str
+
+ had_amazon = False
+ had_elibri = False
+
+ # Remove Amazon hex watermarks
+ # Match optional newline at the beginning, then spaces, then a "meta" tag with name = "Watermark" or "Watermark_(hex)" and a "content" element.
+ # This regex also matches DuMont watermarks with meta name="watermark", with the case-insensitive match on the "w" in watermark.
+ pre_remove = container_str_new
+ container_str_new = re.sub(r'((\r\n|\r|\n)\s*)?\
', '', container_str_new)
+ container_str_new = re.sub(r'((\r\n|\r|\n)\s*)?\
', '', container_str_new)
+ if pre_remove != container_str_new:
+ had_amazon = True
+
+ # Remove elibri / lemonink watermark
+ # Lemonink replaces all "id" fields in the opf with "idX_Y", with X being the watermark and Y being a number for that particular ID.
+ # This regex replaces all "idX_Y" IDs with "id_Y", removing the watermark IDs.
+ pre_remove = container_str_new
+ container_str_new = re.sub(r'((\r\n|\r|\n)\s*)?\<\!\-\-\s*Wygenerowane przez elibri dla zamówienia numer [0-9a-fA-F]+\s*\-\-\>', '', container_str_new)
+ if pre_remove != container_str_new:
+ # To prevent this Regex from applying to books without that watermark, only do that if the watermark above was found.
+ container_str_new = re.sub(r'\=\"id[0-9]+_([0-9]+)\"', r'="id_\1"', container_str_new)
+ if pre_remove != container_str_new:
+ had_elibri = True
+
+ except:
+ traceback.print_exc()
+ return path_to_ebook
+
+ if (container_str == container_str_new):
+ # container didn't change - no watermark
+ return path_to_ebook
+
+ # Re-package without watermark
+ namelist = inf.namelist()
+ namelist.remove("mimetype")
+
+ try:
+ output = object.temporary_file(".epub").name
+ kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
+ with closing(ZipFile(open(output, 'wb'), 'w', **kwds)) as outf:
+ for path in (["mimetype"] + namelist):
+
+ data = inf.read(path)
+ if path == opf_path:
+ # Found OPF, replacing ...
+ data = container_str_new
+
+ zi = ZipInfo(path)
+ oldzi = inf.getinfo(path)
+ try:
+ zi.compress_type = oldzi.compress_type
+ if path == "mimetype":
+ zi.compress_type = ZIP_STORED
+ zi.date_time = oldzi.date_time
+ zi.comment = oldzi.comment
+ zi.extra = oldzi.extra
+ zi.internal_attr = oldzi.internal_attr
+ zi.external_attr = oldzi.external_attr
+ zi.volume = oldzi.volume
+ zi.create_system = oldzi.create_system
+ zi.create_version = oldzi.create_version
+
+ if any(ord(c) >= 128 for c in path) or any(ord(c) >= 128 for c in zi.comment):
+ # If the file name or the comment contains any non-ASCII char, set the UTF8-flag
+ zi.flag_bits |= 0x800
+ except:
+ pass
+
+ # Python 3 has a bug where the external_attr is reset to `0o600 << 16`
+ # if it's NULL, so we need a workaround:
+ if zi.external_attr == 0:
+ zi = ZeroedZipInfo(zi)
+
+ outf.writestr(zi, data)
+ except:
+ traceback.print_exc()
+ return path_to_ebook
+
+ if had_elibri:
+ print("Watermark: Successfully stripped eLibri watermark from OPF file.")
+ if had_amazon:
+ print("Watermark: Successfully stripped Amazon watermark from OPF file.")
+
+ return output
+
+
+
+def removeCDPwatermark(object, path_to_ebook):
+ # "META-INF/cdp.info" is a watermark file used by some Tolino vendors.
+ # We don't want that in our eBooks, so lets remove that file.
+ try:
+ infile = ZipFile(open(path_to_ebook, 'rb'))
+ namelist = infile.namelist()
+ if 'META-INF/cdp.info' not in namelist:
+ return path_to_ebook
+
+ namelist.remove("mimetype")
+ namelist.remove("META-INF/cdp.info")
+
+ output = object.temporary_file(".epub").name
+
+ kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
+ with closing(ZipFile(open(output, 'wb'), 'w', **kwds)) as outf:
+ for path in (["mimetype"] + namelist):
+
+ data = infile.read(path)
+
+ zi = ZipInfo(path)
+ oldzi = infile.getinfo(path)
+ try:
+ zi.compress_type = oldzi.compress_type
+ if path == "mimetype":
+ zi.compress_type = ZIP_STORED
+ zi.date_time = oldzi.date_time
+ zi.comment = oldzi.comment
+ zi.extra = oldzi.extra
+ zi.internal_attr = oldzi.internal_attr
+ zi.external_attr = oldzi.external_attr
+ zi.volume = oldzi.volume
+ zi.create_system = oldzi.create_system
+ zi.create_version = oldzi.create_version
+
+ if any(ord(c) >= 128 for c in path) or any(ord(c) >= 128 for c in zi.comment):
+ # If the file name or the comment contains any non-ASCII char, set the UTF8-flag
+ zi.flag_bits |= 0x800
+ except:
+ pass
+
+ # Python 3 has a bug where the external_attr is reset to `0o600 << 16`
+ # if it's NULL, so we need a workaround:
+ if zi.external_attr == 0:
+ zi = ZeroedZipInfo(zi)
+
+ outf.writestr(zi, data)
+
+ print("Watermark: Successfully removed cdp.info watermark")
+ return output
+
+ except:
+ traceback.print_exc()
+ return path_to_ebook
\ No newline at end of file
diff --git a/DeDRM_plugin/erdr2pml.py b/DeDRM_plugin/erdr2pml.py
index a7ad95e..1ec9993 100755
--- a/DeDRM_plugin/erdr2pml.py
+++ b/DeDRM_plugin/erdr2pml.py
@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-
# erdr2pml.py
-# Copyright © 2008-2020 The Dark Reverser, Apprentice Harper et al.
+# Copyright © 2008-2022 The Dark Reverser, Apprentice Harper, noDRM et al.
#
# Changelog
#
@@ -64,125 +64,27 @@
# 0.22 - Unicode and plugin support, different image folders for PMLZ and source
# 0.23 - moved unicode_argv call inside main for Windows DeDRM compatibility
# 1.00 - Added Python 3 compatibility for calibre 5.0
+# 1.01 - Bugfixes for standalone version.
+# 1.02 - Remove OpenSSL support; only use PyCryptodome
-__version__='1.00'
+__version__='1.02'
import sys, re
-import struct, binascii, getopt, zlib, os, os.path, urllib, tempfile, traceback
-
-if 'calibre' in sys.modules:
- inCalibre = True
-else:
- inCalibre = False
-
-# Wrap a stream so that output gets flushed immediately
-# and also make sure that any unicode strings get
-# encoded using "replace" before writing them.
-class SafeUnbuffered:
- def __init__(self, stream):
- self.stream = stream
- self.encoding = stream.encoding
- if self.encoding == None:
- self.encoding = "utf-8"
- def write(self, data):
- if isinstance(data,str):
- data = data.encode(self.encoding,"replace")
- self.stream.buffer.write(data)
- self.stream.buffer.flush()
- def __getattr__(self, attr):
- return getattr(self.stream, attr)
+import struct, binascii, getopt, zlib, os, os.path, urllib, tempfile, traceback, hashlib
+
+try:
+ from Cryptodome.Cipher import DES
+except ImportError:
+ from Crypto.Cipher import DES
+
+#@@CALIBRE_COMPAT_CODE@@
+
+from utilities import SafeUnbuffered
iswindows = sys.platform.startswith('win')
isosx = sys.platform.startswith('darwin')
-def unicode_argv():
- if iswindows:
- # Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
- # strings.
-
- # Versions 2.x of Python don't support Unicode in sys.argv on
- # Windows, with the underlying Windows API instead replacing multi-byte
- # characters with '?'.
-
-
- from ctypes import POINTER, byref, cdll, c_int, windll
- from ctypes.wintypes import LPCWSTR, LPWSTR
-
- GetCommandLineW = cdll.kernel32.GetCommandLineW
- GetCommandLineW.argtypes = []
- GetCommandLineW.restype = LPCWSTR
-
- CommandLineToArgvW = windll.shell32.CommandLineToArgvW
- CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
- CommandLineToArgvW.restype = POINTER(LPWSTR)
-
- cmd = GetCommandLineW()
- argc = c_int(0)
- argv = CommandLineToArgvW(cmd, byref(argc))
- if argc.value > 0:
- # Remove Python executable and commands if present
- start = argc.value - len(sys.argv)
- return [argv[i] for i in
- range(start, argc.value)]
- # if we don't have any arguments at all, just pass back script name
- # this should never happen
- return ["mobidedrm.py"]
- else:
- argvencoding = sys.stdin.encoding or "utf-8"
- return [arg if isinstance(arg, str) else str(arg, argvencoding) for arg in sys.argv]
-
-Des = None
-if iswindows:
- # first try with pycrypto
- if inCalibre:
- from calibre_plugins.dedrm import pycrypto_des
- else:
- import pycrypto_des
- Des = pycrypto_des.load_pycrypto()
- if Des == None:
- # they try with openssl
- if inCalibre:
- from calibre_plugins.dedrm import openssl_des
- else:
- import openssl_des
- Des = openssl_des.load_libcrypto()
-else:
- # first try with openssl
- if inCalibre:
- from calibre_plugins.dedrm import openssl_des
- else:
- import openssl_des
- Des = openssl_des.load_libcrypto()
- if Des == None:
- # then try with pycrypto
- if inCalibre:
- from calibre_plugins.dedrm import pycrypto_des
- else:
- import pycrypto_des
- Des = pycrypto_des.load_pycrypto()
-
-# if that did not work then use pure python implementation
-# of DES and try to speed it up with Psycho
-if Des == None:
- if inCalibre:
- from calibre_plugins.dedrm import python_des
- else:
- import python_des
- Des = python_des.Des
- # Import Psyco if available
- try:
- # http://psyco.sourceforge.net
- import psyco
- psyco.full()
- except ImportError:
- pass
-
-try:
- from hashlib import sha1
-except ImportError:
- # older Python release
- import sha
- sha1 = lambda s: sha.new(s)
+from argv_utils import unicode_argv
import cgi
import logging
@@ -263,7 +165,7 @@ class EreaderProcessor(object):
raise ValueError('incorrect eReader version %d (error 1)' % version)
data = self.section_reader(1)
self.data = data
- des = Des(fixKey(data[0:8]))
+ des = DES.new(fixKey(data[0:8]), DES.MODE_ECB)
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)')
@@ -327,7 +229,7 @@ class EreaderProcessor(object):
if (self.flags & reqd_flags) != reqd_flags:
print("Flags: 0x%X" % self.flags)
raise ValueError('incompatible eReader file')
- des = Des(fixKey(user_key))
+ des = DES.new(fixKey(user_key), DES.MODE_ECB)
if version == 259:
if drm_sub_version != 7:
raise ValueError('incorrect eReader version %d (error 3)' % drm_sub_version)
@@ -403,7 +305,7 @@ class EreaderProcessor(object):
# return bkinfo
def getText(self):
- des = Des(fixKey(self.content_key))
+ des = DES.new(fixKey(self.content_key), DES.MODE_ECB)
r = b''
for i in range(self.num_text_pages):
logging.debug('get page %d', i)
@@ -416,7 +318,7 @@ class EreaderProcessor(object):
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))
+ des = DES.new(fixKey(self.content_key), DES.MODE_ECB)
for i in range(1,self.num_footnote_pages):
logging.debug('get footnotepage %d', i)
id_len = ord(fnote_ids[2])
@@ -440,7 +342,7 @@ class EreaderProcessor(object):
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))
+ des = DES.new(fixKey(self.content_key), DES.MODE_ECB)
for i in range(1,self.num_sidebar_pages):
id_len = ord(sbar_ids[2])
id = sbar_ids[3:3+id_len]
@@ -549,7 +451,7 @@ def getuser_key(name,cc):
def cli_main():
print("eRdr2Pml v{0}. Copyright © 2009–2020 The Dark Reverser et al.".format(__version__))
- argv=unicode_argv()
+ argv=unicode_argv("erdr2pml.py")
try:
opts, args = getopt.getopt(argv[1:], "hp", ["make-pmlz"])
except getopt.GetoptError as err:
diff --git a/DeDRM_plugin/flatxml2html.py b/DeDRM_plugin/flatxml2html.py
index 2fe80c3..63cd8f6 100644
--- a/DeDRM_plugin/flatxml2html.py
+++ b/DeDRM_plugin/flatxml2html.py
@@ -473,8 +473,10 @@ class DocParser(object):
if (link > 0):
linktype = self.link_type[link-1]
title = self.link_title[link-1]
- if (title == b"") or (parares.rfind(title.decode('utf-8')) < 0):
- title=parares[lstart:].encode('utf-8')
+ if isinstance(title, bytes):
+ title = title.decode('utf-8')
+ if (title == "") or (parares.rfind(title) < 0):
+ title=parares[lstart:]
if linktype == 'external' :
linkhref = self.link_href[link-1]
linkhtml = '
' % linkhref
@@ -485,9 +487,9 @@ class DocParser(object):
else :
# just link to the current page
linkhtml = ''
- linkhtml += title.decode('utf-8')
+ linkhtml += title
linkhtml += ''
- pos = parares.rfind(title.decode('utf-8'))
+ pos = parares.rfind(title)
if pos >= 0:
parares = parares[0:pos] + linkhtml + parares[pos+len(title):]
else :
diff --git a/DeDRM_plugin/genbook.py b/DeDRM_plugin/genbook.py
index 915bd30..b0624fd 100644
--- a/DeDRM_plugin/genbook.py
+++ b/DeDRM_plugin/genbook.py
@@ -4,23 +4,7 @@
# Python 3 for calibre 5.0
from __future__ import print_function
-# Wrap a stream so that output gets flushed immediately
-# and also make sure that any unicode strings get
-# encoded using "replace" before writing them.
-class SafeUnbuffered:
- def __init__(self, stream):
- self.stream = stream
- self.encoding = stream.encoding
- if self.encoding == None:
- self.encoding = "utf-8"
- def write(self, data):
- if isinstance(data, str):
- data = data.encode(self.encoding,"replace")
- self.stream.buffer.write(data)
- self.stream.buffer.flush()
-
- def __getattr__(self, attr):
- return getattr(self.stream, attr)
+from utilities import SafeUnbuffered
import sys
import csv
@@ -29,25 +13,17 @@ import getopt
from struct import pack
from struct import unpack
+#@@CALIBRE_COMPAT_CODE@@
+
+
class TpzDRMError(Exception):
pass
# local support routines
-if 'calibre' in sys.modules:
- inCalibre = True
-else:
- inCalibre = False
-
-if inCalibre :
- from calibre_plugins.dedrm import convert2xml
- from calibre_plugins.dedrm import flatxml2html
- from calibre_plugins.dedrm import flatxml2svg
- from calibre_plugins.dedrm import stylexml2css
-else :
- import convert2xml
- import flatxml2html
- import flatxml2svg
- import stylexml2css
+import convert2xml
+import flatxml2html
+import flatxml2svg
+import stylexml2css
# global switch
buildXML = False
diff --git a/DeDRM_plugin/ignobleepub.py b/DeDRM_plugin/ignobleepub.py
deleted file mode 100644
index 72e22f9..0000000
--- a/DeDRM_plugin/ignobleepub.py
+++ /dev/null
@@ -1,441 +0,0 @@
-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-
-# ignobleepub.py
-# Copyright © 2009-2020 by i♥cabbages, Apprentice Harper et al.
-
-# Released under the terms of the GNU General Public Licence, version 3
-#
-
-#
-# 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 decrypt from encryption.xml
-# 3.3 - On Windows try PyCrypto first, OpenSSL next
-# 3.4 - Modify interface to allow use with import
-# 3.5 - Fix for potential problem with PyCrypto
-# 3.6 - Revised to allow use in calibre plugins to eliminate need for duplicate code
-# 3.7 - Tweaked to match ineptepub more closely
-# 3.8 - Fixed to retain zip file metadata (e.g. file modification date)
-# 3.9 - moved unicode_argv call inside main for Windows DeDRM compatibility
-# 4.0 - Work if TkInter is missing
-# 4.1 - Import tkFileDialog, don't assume something else will import it.
-# 5.0 - Python 3 for calibre 5.0
-
-"""
-Decrypt Barnes & Noble encrypted ePub books.
-"""
-
-__license__ = 'GPL v3'
-__version__ = "5.0"
-
-import sys
-import os
-import traceback
-import base64
-import zlib
-import zipfile
-from zipfile import ZipInfo, ZipFile, ZIP_STORED, ZIP_DEFLATED
-from contextlib import closing
-import xml.etree.ElementTree as etree
-
-# Wrap a stream so that output gets flushed immediately
-# and also make sure that any unicode strings get
-# encoded using "replace" before writing them.
-class SafeUnbuffered:
- def __init__(self, stream):
- self.stream = stream
- self.encoding = stream.encoding
- if self.encoding == None:
- self.encoding = "utf-8"
- def write(self, data):
- if isinstance(data,str):
- data = data.encode(self.encoding,"replace")
- self.stream.buffer.write(data)
- self.stream.buffer.flush()
- def __getattr__(self, attr):
- return getattr(self.stream, attr)
-
-try:
- from calibre.constants import iswindows, isosx
-except:
- iswindows = sys.platform.startswith('win')
- isosx = sys.platform.startswith('darwin')
-
-def unicode_argv():
- if iswindows:
- # Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
- # strings.
-
- # Versions 2.x of Python don't support Unicode in sys.argv on
- # Windows, with the underlying Windows API instead replacing multi-byte
- # characters with '?'.
-
-
- from ctypes import POINTER, byref, cdll, c_int, windll
- from ctypes.wintypes import LPCWSTR, LPWSTR
-
- GetCommandLineW = cdll.kernel32.GetCommandLineW
- GetCommandLineW.argtypes = []
- GetCommandLineW.restype = LPCWSTR
-
- CommandLineToArgvW = windll.shell32.CommandLineToArgvW
- CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
- CommandLineToArgvW.restype = POINTER(LPWSTR)
-
- cmd = GetCommandLineW()
- argc = c_int(0)
- argv = CommandLineToArgvW(cmd, byref(argc))
- if argc.value > 0:
- # Remove Python executable and commands if present
- start = argc.value - len(sys.argv)
- return [argv[i] for i in
- range(start, argc.value)]
- return ["ineptepub.py"]
- else:
- argvencoding = sys.stdin.encoding or "utf-8"
- return [arg if isinstance(arg, str) else str(arg, argvencoding) for arg in sys.argv]
-
-
-class IGNOBLEError(Exception):
- pass
-
-def _load_crypto_libcrypto():
- from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_int, c_long, \
- Structure, c_ulong, create_string_buffer, cast
- from ctypes.util import find_library
-
- if iswindows:
- libcrypto = find_library('libeay32')
- else:
- libcrypto = find_library('crypto')
-
- if libcrypto is None:
- raise IGNOBLEError('libcrypto not found')
- libcrypto = CDLL(libcrypto)
-
- AES_MAXNR = 14
-
- c_char_pp = POINTER(c_char_p)
- c_int_p = POINTER(c_int)
-
- class AES_KEY(Structure):
- _fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))),
- ('rounds', c_int)]
- AES_KEY_p = POINTER(AES_KEY)
-
- def F(restype, name, argtypes):
- func = getattr(libcrypto, name)
- func.restype = restype
- func.argtypes = argtypes
- return func
-
- AES_set_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 = (b'\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, b'\x00'*16)
-
- 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()
-
-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 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(b'Z') + dc.flush()
- if ex:
- bytes = bytes + ex
- return bytes
-
- def decrypt(self, path, data):
- if bytes(path,'utf-8') in self._encrypted:
- data = self._aes.decrypt(data)[16:]
- data = data[:-data[-1]]
- data = self.decompress(data)
- return data
-
-# check file to make check whether it's probably an Adobe Adept encrypted ePub
-def ignobleBook(inpath):
- with closing(ZipFile(open(inpath, 'rb'))) as inf:
- namelist = set(inf.namelist())
- if 'META-INF/rights.xml' not in namelist or \
- 'META-INF/encryption.xml' not in namelist:
- return False
- try:
- rights = etree.fromstring(inf.read('META-INF/rights.xml'))
- adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
- expr = './/%s' % (adept('encryptedKey'),)
- bookkey = ''.join(rights.findtext(expr))
- if len(bookkey) == 64:
- return True
- except:
- # if we couldn't check, assume it is
- return True
- return False
-
-def decryptBook(keyb64, inpath, outpath):
- if AES is None:
- raise IGNOBLEError("PyCrypto or OpenSSL must be installed.")
- key = base64.b64decode(keyb64)[:16]
- aes = AES(key)
- with closing(ZipFile(open(inpath, 'rb'))) as inf:
- namelist = set(inf.namelist())
- if 'META-INF/rights.xml' not in namelist or \
- 'META-INF/encryption.xml' not in namelist:
- print("{0:s} is DRM-free.".format(os.path.basename(inpath)))
- return 1
- for name in META_NAMES:
- namelist.remove(name)
- try:
- rights = etree.fromstring(inf.read('META-INF/rights.xml'))
- adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
- expr = './/%s' % (adept('encryptedKey'),)
- bookkey = ''.join(rights.findtext(expr))
- if len(bookkey) != 64:
- print("{0:s} is not a secure Barnes & Noble ePub.".format(os.path.basename(inpath)))
- return 1
- bookkey = aes.decrypt(base64.b64decode(bookkey))
- bookkey = bookkey[:-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')
- zi.compress_type=ZIP_STORED
- try:
- # if the mimetype is present, get its info, including time-stamp
- oldzi = inf.getinfo('mimetype')
- # copy across fields to be preserved
- zi.date_time = oldzi.date_time
- zi.comment = oldzi.comment
- zi.extra = oldzi.extra
- zi.internal_attr = oldzi.internal_attr
- # external attributes are dependent on the create system, so copy both.
- zi.external_attr = oldzi.external_attr
- zi.create_system = oldzi.create_system
- except:
- pass
- outf.writestr(zi, inf.read('mimetype'))
- for path in namelist:
- data = inf.read(path)
- zi = ZipInfo(path)
- zi.compress_type=ZIP_DEFLATED
- try:
- # get the file info, including time-stamp
- oldzi = inf.getinfo(path)
- # copy across useful fields
- zi.date_time = oldzi.date_time
- zi.comment = oldzi.comment
- zi.extra = oldzi.extra
- zi.internal_attr = oldzi.internal_attr
- # external attributes are dependent on the create system, so copy both.
- zi.external_attr = oldzi.external_attr
- zi.create_system = oldzi.create_system
- except:
- pass
- outf.writestr(zi, decryptor.decrypt(path, data))
- except:
- print("Could not decrypt {0:s} because of an exception:\n{1:s}".format(os.path.basename(inpath), traceback.format_exc()))
- return 2
- return 0
-
-
-def cli_main():
- sys.stdout=SafeUnbuffered(sys.stdout)
- sys.stderr=SafeUnbuffered(sys.stderr)
- argv=unicode_argv()
- progname = os.path.basename(argv[0])
- if len(argv) != 4:
- print("usage: {0}
".format(progname))
- return 1
- keypath, inpath, outpath = argv[1:]
- userkey = open(keypath,'rb').read()
- result = decryptBook(userkey, inpath, outpath)
- if result == 0:
- print("Successfully decrypted {0:s} as {1:s}".format(os.path.basename(inpath),os.path.basename(outpath)))
- return result
-
-def gui_main():
- try:
- import tkinter
- import tkinter.constants
- import tkinter.filedialog
- import tkinter.messagebox
- import traceback
- except:
- return cli_main()
-
- class DecryptionDialog(tkinter.Frame):
- def __init__(self, root):
- tkinter.Frame.__init__(self, root, border=5)
- self.status = tkinter.Label(self, text="Select files for decryption")
- self.status.pack(fill=tkinter.constants.X, expand=1)
- body = tkinter.Frame(self)
- body.pack(fill=tkinter.constants.X, expand=1)
- sticky = tkinter.constants.E + tkinter.constants.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=tkinter.constants.LEFT)
- tkinter.Frame(buttons, width=10).pack(side=tkinter.constants.LEFT)
- button = tkinter.Button(
- buttons, text="Quit", width=10, command=self.quit)
- button.pack(side=tkinter.constants.RIGHT)
-
- def get_keypath(self):
- keypath = tkinter.filedialog.askopenfilename(
- parent=None, title="Select Barnes & Noble \'.b64\' key file",
- defaultextension=".b64",
- filetypes=[('base64-encoded files', '.b64'),
- ('All Files', '.*')])
- if keypath:
- keypath = os.path.normpath(keypath)
- self.keypath.delete(0, tkinter.constants.END)
- self.keypath.insert(0, keypath)
- return
-
- def get_inpath(self):
- inpath = tkinter.filedialog.askopenfilename(
- parent=None, title="Select B&N-encrypted ePub file to decrypt",
- defaultextension=".epub", filetypes=[('ePub files', '.epub')])
- if inpath:
- inpath = os.path.normpath(inpath)
- self.inpath.delete(0, tkinter.constants.END)
- self.inpath.insert(0, inpath)
- return
-
- def get_outpath(self):
- outpath = tkinter.filedialog.asksaveasfilename(
- parent=None, title="Select unencrypted ePub file to produce",
- defaultextension=".epub", filetypes=[('ePub files', '.epub')])
- if outpath:
- outpath = os.path.normpath(outpath)
- self.outpath.delete(0, tkinter.constants.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
- userkey = open(keypath,'rb').read()
- self.status['text'] = "Decrypting..."
- try:
- decrypt_status = decryptBook(userkey, inpath, outpath)
- except Exception as e:
- self.status['text'] = "Error: {0}".format(e.args[0])
- return
- if decrypt_status == 0:
- self.status['text'] = "File successfully decrypted"
- else:
- self.status['text'] = "The was an error decrypting the file."
-
- root = tkinter.Tk()
- root.title("Barnes & Noble ePub Decrypter v.{0}".format(__version__))
- root.resizable(True, False)
- root.minsize(300, 0)
- DecryptionDialog(root).pack(fill=tkinter.constants.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_plugin/ignoblekeyAndroid.py b/DeDRM_plugin/ignoblekeyAndroid.py
new file mode 100644
index 0000000..e0b2f23
--- /dev/null
+++ b/DeDRM_plugin/ignoblekeyAndroid.py
@@ -0,0 +1,68 @@
+'''
+Extracts the user's ccHash from an .adobe-digital-editions folder
+typically included in the Nook Android app's data folder.
+
+Based on ignoblekeyWindowsStore.py, updated for Android by noDRM.
+'''
+
+import sys
+import os
+import base64
+try:
+ from Cryptodome.Cipher import AES
+except ImportError:
+ from Crypto.Cipher import AES
+import hashlib
+from lxml import etree
+
+def unpad(data, padding=16):
+ if sys.version_info[0] == 2:
+ pad_len = ord(data[-1])
+ else:
+ pad_len = data[-1]
+
+ return data[:-pad_len]
+
+
+PASS_HASH_SECRET = "9ca588496a1bc4394553d9e018d70b9e"
+
+
+def dump_keys(path_to_adobe_folder):
+
+ activation_path = os.path.join(path_to_adobe_folder, "activation.xml")
+ device_path = os.path.join(path_to_adobe_folder, "device.xml")
+
+ if not os.path.isfile(activation_path):
+ print("Nook activation file is missing: %s\n" % activation_path)
+ return []
+ if not os.path.isfile(device_path):
+ print("Nook device file is missing: %s\n" % device_path)
+ return []
+
+ # Load files:
+ activation_xml = etree.parse(activation_path)
+ device_xml = etree.parse(device_path)
+
+ # Get fingerprint:
+ device_fingerprint = device_xml.findall(".//{http://ns.adobe.com/adept}fingerprint")[0].text
+ device_fingerprint = base64.b64decode(device_fingerprint).hex()
+
+ hash_key = hashlib.sha1(bytearray.fromhex(device_fingerprint + PASS_HASH_SECRET)).digest()[:16]
+
+ hashes = []
+
+ for pass_hash in activation_xml.findall(".//{http://ns.adobe.com/adept}passHash"):
+ try:
+ encrypted_cc_hash = base64.b64decode(pass_hash.text)
+ cc_hash = unpad(AES.new(hash_key, AES.MODE_CBC, encrypted_cc_hash[:16]).decrypt(encrypted_cc_hash[16:]))
+ hashes.append(base64.b64encode(cc_hash).decode("ascii"))
+ #print("Nook ccHash is %s" % (base64.b64encode(cc_hash).decode("ascii")))
+ except:
+ pass
+
+ return hashes
+
+
+
+if __name__ == "__main__":
+ print("No standalone version available.")
diff --git a/DeDRM_plugin/ignoblekeygen.py b/DeDRM_plugin/ignoblekeyGenPassHash.py
similarity index 53%
rename from DeDRM_plugin/ignoblekeygen.py
rename to DeDRM_plugin/ignoblekeyGenPassHash.py
index 07bfb51..6f09bc4 100644
--- a/DeDRM_plugin/ignoblekeygen.py
+++ b/DeDRM_plugin/ignoblekeyGenPassHash.py
@@ -1,8 +1,8 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
-# ignoblekeygen.py
-# Copyright © 2009-2020 i♥cabbages, Apprentice Harper et al.
+# ignoblekeyGenPassHash.py
+# Copyright © 2009-2022 i♥cabbages, Apprentice Harper et al.
# Released under the terms of the GNU General Public Licence, version 3
#
@@ -31,164 +31,32 @@
# 2.7 - Work if TkInter is missing
# 2.8 - Fix bug in stand-alone use (import tkFileDialog)
# 3.0 - Added Python 3 compatibility for calibre 5.0
+# 3.1 - Remove OpenSSL support, only PyCryptodome is supported now
"""
Generate Barnes & Noble EPUB user key from name and credit card number.
"""
__license__ = 'GPL v3'
-__version__ = "3.0"
+__version__ = "3.1"
import sys
import os
import hashlib
import base64
-# Wrap a stream so that output gets flushed immediately
-# and also make sure that any unicode strings get
-# encoded using "replace" before writing them.
-class SafeUnbuffered:
- def __init__(self, stream):
- self.stream = stream
- self.encoding = stream.encoding
- if self.encoding == None:
- self.encoding = "utf-8"
- def write(self, data):
- if isinstance(data, str):
- data = data.encode(self.encoding,"replace")
- self.stream.buffer.write(data)
- self.stream.buffer.flush()
-
- def __getattr__(self, attr):
- return getattr(self.stream, attr)
-
try:
- from calibre.constants import iswindows, isosx
-except:
- iswindows = sys.platform.startswith('win')
- isosx = sys.platform.startswith('darwin')
-
-def unicode_argv():
- if iswindows:
- # Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
- # strings.
-
- # Versions 2.x of Python don't support Unicode in sys.argv on
- # Windows, with the underlying Windows API instead replacing multi-byte
- # characters with '?'. So use shell32.GetCommandLineArgvW to get sys.argv
- # as a list of Unicode strings and encode them as utf-8
-
- from ctypes import POINTER, byref, cdll, c_int, windll
- from ctypes.wintypes import LPCWSTR, LPWSTR
-
- GetCommandLineW = cdll.kernel32.GetCommandLineW
- GetCommandLineW.argtypes = []
- GetCommandLineW.restype = LPCWSTR
-
- CommandLineToArgvW = windll.shell32.CommandLineToArgvW
- CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
- CommandLineToArgvW.restype = POINTER(LPWSTR)
+ from Cryptodome.Cipher import AES
+except ImportError:
+ from Crypto.Cipher import AES
- cmd = GetCommandLineW()
- argc = c_int(0)
- argv = CommandLineToArgvW(cmd, byref(argc))
- if argc.value > 0:
- # Remove Python executable and commands if present
- start = argc.value - len(sys.argv)
- return [argv[i] for i in
- range(start, argc.value)]
- # if we don't have any arguments at all, just pass back script name
- # this should never happen
- return ["ignoblekeygen.py"]
- else:
- argvencoding = sys.stdin.encoding or "utf-8"
- return [arg if isinstance(arg, str) else str(arg, argvencoding) for arg in sys.argv]
+from utilities import SafeUnbuffered
+from argv_utils import unicode_argv
class IGNOBLEError(Exception):
pass
-def _load_crypto_libcrypto():
- from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_int, c_long, \
- Structure, c_ulong, create_string_buffer, cast
- from ctypes.util import find_library
-
- if iswindows:
- libcrypto = find_library('libeay32')
- else:
- libcrypto = find_library('crypto')
-
- if libcrypto is None:
- raise IGNOBLEError('libcrypto not found')
- libcrypto = CDLL(libcrypto)
-
- AES_MAXNR = 14
-
- c_char_pp = POINTER(c_char_p)
- c_int_p = POINTER(c_int)
-
- class AES_KEY(Structure):
- _fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))),
- ('rounds', c_int)]
- AES_KEY_p = POINTER(AES_KEY)
-
- def F(restype, name, argtypes):
- func = getattr(libcrypto, name)
- func.restype = restype
- func.argtypes = argtypes
- return func
-
- AES_set_encrypt_key = F(c_int, 'AES_set_encrypt_key',
- [c_char_p, c_int, AES_KEY_p])
- AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',
- [c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p,
- c_int])
-
- class AES(object):
- def __init__(self, userkey, iv):
- self._blocksize = len(userkey)
- self._iv = iv
- key = self._key = AES_KEY()
- rv = AES_set_encrypt_key(userkey, len(userkey) * 8, key)
- if rv < 0:
- raise IGNOBLEError('Failed to initialize AES Encrypt key')
-
- def encrypt(self, data):
- out = create_string_buffer(len(data))
- rv = AES_cbc_encrypt(data, out, len(data), self._key, self._iv, 1)
- if rv == 0:
- raise IGNOBLEError('AES encryption failed')
- return out.raw
-
- return AES
-
-def _load_crypto_pycrypto():
- from Crypto.Cipher import AES as _AES
-
- class AES(object):
- def __init__(self, key, iv):
- self._aes = _AES.new(key, _AES.MODE_CBC, iv)
-
- def encrypt(self, data):
- return self._aes.encrypt(data)
-
- return AES
-
-def _load_crypto():
- AES = None
- cryptolist = (_load_crypto_libcrypto, _load_crypto_pycrypto)
- if sys.platform.startswith('win'):
- cryptolist = (_load_crypto_pycrypto, _load_crypto_libcrypto)
- for loader in cryptolist:
- try:
- AES = loader()
- break
- except (ImportError, IGNOBLEError):
- pass
- return AES
-
-AES = _load_crypto()
-
def normalize_name(name):
return ''.join(x for x in name.lower() if x != ' ')
@@ -209,8 +77,7 @@ def generate_key(name, ccn):
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 + (b'\x0c' * 0x0c))
+ crypt = AES.new(ccn_sha, AES.MODE_CBC, name_sha).encrypt(both_sha + (b'\x0c' * 0x0c))
userkey = hashlib.sha1(crypt).digest()
return base64.b64encode(userkey)
@@ -218,13 +85,8 @@ def generate_key(name, ccn):
def cli_main():
sys.stdout=SafeUnbuffered(sys.stdout)
sys.stderr=SafeUnbuffered(sys.stderr)
- argv=unicode_argv()
+ argv=unicode_argv("ignoblekeyGenPassHash.py")
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: {0} ".format(progname))
return 1
@@ -310,13 +172,6 @@ def gui_main():
self.status['text'] = "Keyfile successfully generated"
root = tkinter.Tk()
- if AES is None:
- root.withdraw()
- tkinter.messagebox.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("Barnes & Noble ePub Keyfile Generator v.{0}".format(__version__))
root.resizable(True, False)
root.minsize(300, 0)
diff --git a/DeDRM_plugin/ignoblekey.py b/DeDRM_plugin/ignoblekeyNookStudy.py
similarity index 76%
rename from DeDRM_plugin/ignoblekey.py
rename to DeDRM_plugin/ignoblekeyNookStudy.py
index 5e33e33..6a5f1cf 100644
--- a/DeDRM_plugin/ignoblekey.py
+++ b/DeDRM_plugin/ignoblekeyNookStudy.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
-# ignoblekey.py
+# ignoblekeyNookStudy.py
# Copyright © 2015-2020 Apprentice Alf, Apprentice Harper et al.
# Based on kindlekey.py, Copyright © 2010-2013 by some_updates and Apprentice Alf
@@ -27,65 +27,14 @@ import hashlib
import getopt
import re
-# Wrap a stream so that output gets flushed immediately
-# and also make sure that any unicode strings get
-# encoded using "replace" before writing them.
-class SafeUnbuffered:
- def __init__(self, stream):
- self.stream = stream
- self.encoding = stream.encoding
- if self.encoding == None:
- self.encoding = "utf-8"
- def write(self, data):
- if isinstance(data, str):
- data = data.encode(self.encoding,"replace")
- self.stream.buffer.write(data)
- self.stream.buffer.flush()
-
- def __getattr__(self, attr):
- return getattr(self.stream, attr)
+from utilities import SafeUnbuffered
try:
- from calibre.constants import iswindows, isosx
+ from calibre.constants import iswindows
except:
iswindows = sys.platform.startswith('win')
- isosx = sys.platform.startswith('darwin')
-def unicode_argv():
- if iswindows:
- # Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
- # strings.
-
- # Versions 2.x of Python don't support Unicode in sys.argv on
- # Windows, with the underlying Windows API instead replacing multi-byte
- # characters with '?'. So use shell32.GetCommandLineArgvW to get sys.argv
- # as a list of Unicode strings and encode them as utf-8
-
- from ctypes import POINTER, byref, cdll, c_int, windll
- from ctypes.wintypes import LPCWSTR, LPWSTR
-
- GetCommandLineW = cdll.kernel32.GetCommandLineW
- GetCommandLineW.argtypes = []
- GetCommandLineW.restype = LPCWSTR
-
- CommandLineToArgvW = windll.shell32.CommandLineToArgvW
- CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
- CommandLineToArgvW.restype = POINTER(LPWSTR)
-
- cmd = GetCommandLineW()
- argc = c_int(0)
- argv = CommandLineToArgvW(cmd, byref(argc))
- if argc.value > 0:
- # Remove Python executable and commands if present
- start = argc.value - len(sys.argv)
- return [argv[i] for i in
- range(start, argc.value)]
- # if we don't have any arguments at all, just pass back script name
- # this should never happen
- return ["ignoblekey.py"]
- else:
- argvencoding = sys.stdin.encoding or "utf-8"
- return [arg if isinstance(arg, str) else str(arg, argvencoding) for arg in sys.argv]
+from argv_utils import unicode_argv
class DrmException(Exception):
pass
@@ -95,7 +44,10 @@ def getNookLogFiles():
logFiles = []
found = False
if iswindows:
- import winreg
+ try:
+ import winreg
+ except ImportError:
+ import _winreg as winreg
# some 64 bit machines do not have the proper registry key for some reason
# or the python interface to the 32 vs 64 bit registry is broken
@@ -148,7 +100,7 @@ def getNookLogFiles():
logpath = path +'\\Barnes & Noble\\NOOKstudy\\logs\\BNClientLog.txt'
if os.path.isfile(logpath):
found = True
- print('Found nookStudy log file: ' + logpath.encode('ascii','ignore'))
+ print('Found nookStudy log file: ' + logpath, file=sys.stderr)
logFiles.append(logpath)
else:
home = os.getenv('HOME')
@@ -156,26 +108,26 @@ def getNookLogFiles():
testpath = home + '/Library/Application Support/Barnes & Noble/DesktopReader/logs/BNClientLog.txt'
if os.path.isfile(testpath):
logFiles.append(testpath)
- print('Found nookStudy log file: ' + testpath)
+ print('Found nookStudy log file: ' + testpath, file=sys.stderr)
found = True
testpath = home + '/Library/Application Support/Barnes & Noble/DesktopReader/indices/BNClientLog.txt'
if os.path.isfile(testpath):
logFiles.append(testpath)
- print('Found nookStudy log file: ' + testpath)
+ print('Found nookStudy log file: ' + testpath, file=sys.stderr)
found = True
testpath = home + '/Library/Application Support/Barnes & Noble/BNDesktopReader/logs/BNClientLog.txt'
if os.path.isfile(testpath):
logFiles.append(testpath)
- print('Found nookStudy log file: ' + testpath)
+ print('Found nookStudy log file: ' + testpath, file=sys.stderr)
found = True
testpath = home + '/Library/Application Support/Barnes & Noble/BNDesktopReader/indices/BNClientLog.txt'
if os.path.isfile(testpath):
logFiles.append(testpath)
- print('Found nookStudy log file: ' + testpath)
+ print('Found nookStudy log file: ' + testpath, file=sys.stderr)
found = True
if not found:
- print('No nook Study log files have been found.')
+ print('No nook Study log files have been found.', file=sys.stderr)
return logFiles
@@ -196,7 +148,7 @@ def nookkeys(files = []):
for file in files:
fileKeys = getKeysFromLog(file)
if fileKeys:
- print("Found {0} keys in the Nook Study log files".format(len(fileKeys)))
+ print("Found {0} keys in the Nook Study log files".format(len(fileKeys)), file=sys.stderr)
keys.extend(fileKeys)
return list(set(keys))
@@ -209,7 +161,7 @@ def getkey(outpath, files=[]):
outfile = outpath
with open(outfile, 'w') as keyfileout:
keyfileout.write(keys[-1])
- print("Saved a key to {0}".format(outfile))
+ print("Saved a key to {0}".format(outfile), file=sys.stderr)
else:
keycount = 0
for key in keys:
@@ -220,7 +172,7 @@ def getkey(outpath, files=[]):
break
with open(outfile, 'w') as keyfileout:
keyfileout.write(key)
- print("Saved a key to {0}".format(outfile))
+ print("Saved a key to {0}".format(outfile), file=sys.stderr)
return True
return False
@@ -235,7 +187,7 @@ def usage(progname):
def cli_main():
sys.stdout=SafeUnbuffered(sys.stdout)
sys.stderr=SafeUnbuffered(sys.stderr)
- argv=unicode_argv()
+ argv=unicode_argv("ignoblekeyNookStudy.py")
progname = os.path.basename(argv[0])
print("{0} v{1}\nCopyright © 2015 Apprentice Alf".format(progname,__version__))
@@ -296,7 +248,7 @@ def gui_main():
self.text.insert(tkinter.constants.END, text)
- argv=unicode_argv()
+ argv=unicode_argv("ignoblekeyNookStudy.py")
root = tkinter.Tk()
root.withdraw()
progpath, progname = os.path.split(argv[0])
diff --git a/DeDRM_plugin/ignoblekeyWindowsStore.py b/DeDRM_plugin/ignoblekeyWindowsStore.py
new file mode 100644
index 0000000..29df204
--- /dev/null
+++ b/DeDRM_plugin/ignoblekeyWindowsStore.py
@@ -0,0 +1,78 @@
+# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
+
+'''
+Obtain the user's ccHash from the Barnes & Noble Nook Windows Store app.
+https://www.microsoft.com/en-us/p/nook-books-magazines-newspapers-comics/9wzdncrfj33h
+(Requires a recent Windows version in a supported region (US).)
+This procedure has been tested with Nook app version 1.11.0.4 under Windows 11.
+
+Based on experimental standalone python script created by fesiwi at
+https://github.com/noDRM/DeDRM_tools/discussions/9
+'''
+
+import sys, os
+import apsw
+import base64
+import traceback
+try:
+ from Cryptodome.Cipher import AES
+except:
+ from Crypto.Cipher import AES
+import hashlib
+from lxml import etree
+
+def unpad(data, padding=16):
+ if sys.version_info[0] == 2:
+ pad_len = ord(data[-1])
+ else:
+ pad_len = data[-1]
+
+ return data[:-pad_len]
+
+
+NOOK_DATA_FOLDER = "%LOCALAPPDATA%\\Packages\\BarnesNoble.Nook_ahnzqzva31enc\\LocalState"
+PASS_HASH_SECRET = "9ca588496a1bc4394553d9e018d70b9e"
+
+
+def dump_keys(print_result=False):
+ db_filename = os.path.expandvars(NOOK_DATA_FOLDER + "\\NookDB.db3")
+
+
+ if not os.path.isfile(db_filename):
+ print("Database file not found. Is the Nook Windows Store app installed?")
+ return []
+
+
+ # Python2 has no fetchone() so we have to use fetchall() and discard everything but the first result.
+ # There should only be one result anyways.
+ serial_number = apsw.Connection(db_filename).cursor().execute(
+ "SELECT value FROM bn_internal_key_value_table WHERE key = 'serialNumber';").fetchall()[0][0]
+
+
+ hash_key = hashlib.sha1(bytearray.fromhex(serial_number + PASS_HASH_SECRET)).digest()[:16]
+
+ activation_file_name = os.path.expandvars(NOOK_DATA_FOLDER + "\\settings\\activation.xml")
+
+ if not os.path.isfile(activation_file_name):
+ print("Activation file not found. Are you logged in to your Nook account?")
+ return []
+
+
+ activation_xml = etree.parse(activation_file_name)
+
+ decrypted_hashes = []
+
+ for pass_hash in activation_xml.findall(".//{http://ns.adobe.com/adept}passHash"):
+ try:
+ encrypted_cc_hash = base64.b64decode(pass_hash.text)
+ cc_hash = unpad(AES.new(hash_key, AES.MODE_CBC, encrypted_cc_hash[:16]).decrypt(encrypted_cc_hash[16:]), 16)
+ decrypted_hashes.append((base64.b64encode(cc_hash).decode("ascii")))
+ if print_result:
+ print("Nook ccHash is %s" % (base64.b64encode(cc_hash).decode("ascii")))
+ except:
+ traceback.print_exc()
+
+ return decrypted_hashes
+
+if __name__ == "__main__":
+ dump_keys(True)
diff --git a/DeDRM_plugin/ignoblekeyfetch.py b/DeDRM_plugin/ignoblekeyfetch.py
deleted file mode 100644
index a0375f9..0000000
--- a/DeDRM_plugin/ignoblekeyfetch.py
+++ /dev/null
@@ -1,254 +0,0 @@
-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-
-# ignoblekeyfetch.py
-# Copyright © 2015-2020 Apprentice Harper et al.
-
-# Released under the terms of the GNU General Public Licence, version 3
-#
-
-# Based on discoveries by "Nobody You Know"
-# Code partly based on ignoblekeygen.py by several people.
-
-# Windows users: Before running this program, you must first install Python.
-# We recommend ActiveState Python 2.7.X for Windows from
-# http://www.activestate.com/activepython/downloads.
-# Then save this script file as ignoblekeyfetch.pyw and double-click on it to run it.
-#
-# Mac OS X users: Save this script file as ignoblekeyfetch.pyw. You can run this
-# program from the command line (python ignoblekeyfetch.pyw) or by double-clicking
-# it when it has been associated with PythonLauncher.
-
-# Revision history:
-# 1.0 - Initial version
-# 1.1 - Try second URL if first one fails
-# 2.0 - Python 3 for calibre 5.0
-
-"""
-Fetch Barnes & Noble EPUB user key from B&N servers using email and password
-"""
-
-__license__ = 'GPL v3'
-__version__ = "2.0"
-
-import sys
-import os
-
-# Wrap a stream so that output gets flushed immediately
-# and also make sure that any unicode strings get
-# encoded using "replace" before writing them.
-class SafeUnbuffered:
- def __init__(self, stream):
- self.stream = stream
- self.encoding = stream.encoding
- if self.encoding == None:
- self.encoding = "utf-8"
- def write(self, data):
- if isinstance(data, str):
- data = data.encode(self.encoding,"replace")
- self.stream.buffer.write(data)
- self.stream.buffer.flush()
-
- def __getattr__(self, attr):
- return getattr(self.stream, attr)
-
-try:
- from calibre.constants import iswindows, isosx
-except:
- iswindows = sys.platform.startswith('win')
- isosx = sys.platform.startswith('darwin')
-
-def unicode_argv():
- if iswindows:
- # Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
- # strings.
-
- # Versions 2.x of Python don't support Unicode in sys.argv on
- # Windows, with the underlying Windows API instead replacing multi-byte
- # characters with '?'. So use shell32.GetCommandLineArgvW to get sys.argv
- # as a list of Unicode strings and encode them as utf-8
-
- from ctypes import POINTER, byref, cdll, c_int, windll
- from ctypes.wintypes import LPCWSTR, LPWSTR
-
- GetCommandLineW = cdll.kernel32.GetCommandLineW
- GetCommandLineW.argtypes = []
- GetCommandLineW.restype = LPCWSTR
-
- CommandLineToArgvW = windll.shell32.CommandLineToArgvW
- CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
- CommandLineToArgvW.restype = POINTER(LPWSTR)
-
- cmd = GetCommandLineW()
- argc = c_int(0)
- argv = CommandLineToArgvW(cmd, byref(argc))
- if argc.value > 0:
- # Remove Python executable and commands if present
- start = argc.value - len(sys.argv)
- return [argv[i] for i in
- range(start, argc.value)]
- # if we don't have any arguments at all, just pass back script name
- # this should never happen
- return ["ignoblekeyfetch.py"]
- else:
- argvencoding = sys.stdin.encoding or "utf-8"
- return [arg if isinstance(arg, str) else str(arg, argvencoding) for arg in sys.argv]
-
-
-class IGNOBLEError(Exception):
- pass
-
-def fetch_key(email, password):
- # change email and password to utf-8 if unicode
- if type(email)==str:
- email = email.encode('utf-8')
- if type(password)==str:
- password = password.encode('utf-8')
-
- import random
- random = "%030x" % random.randrange(16**30)
-
- import urllib.parse, urllib.request, re
-
- # try the URL from nook for PC
- fetch_url = "https://cart4.barnesandnoble.com/services/service.aspx?Version=2&acctPassword="
- fetch_url += urllib.parse.quote(password,'')+"&devID=PC_BN_2.5.6.9575_"+random+"&emailAddress="
- fetch_url += urllib.parse.quote(email,"")+"&outFormat=5&schema=1&service=1&stage=deviceHashB"
- #print fetch_url
-
- found = ''
- try:
- response = urllib.request.urlopen(fetch_url)
- the_page = response.read()
- #print the_page
- found = re.search('ccHash>(.+?)(.+?) ".format(progname))
- return 1
- email, password, keypath = argv[1:]
- userkey = fetch_key(email, password)
- if len(userkey) == 28:
- open(keypath,'wb').write(userkey)
- return 0
- print("Failed to fetch key.")
- return 1
-
-
-def gui_main():
- try:
- import tkinter
- import tkinter.filedialog
- import tkinter.constants
- import tkinter.messagebox
- import traceback
- except:
- return cli_main()
-
- class DecryptionDialog(tkinter.Frame):
- def __init__(self, root):
- tkinter.Frame.__init__(self, root, border=5)
- self.status = tkinter.Label(self, text="Enter parameters")
- self.status.pack(fill=tkinter.constants.X, expand=1)
- body = tkinter.Frame(self)
- body.pack(fill=tkinter.constants.X, expand=1)
- sticky = tkinter.constants.E + tkinter.constants.W
- body.grid_columnconfigure(1, weight=2)
- tkinter.Label(body, text="Account email address").grid(row=0)
- self.name = tkinter.Entry(body, width=40)
- self.name.grid(row=0, column=1, sticky=sticky)
- tkinter.Label(body, text="Account password").grid(row=1)
- self.ccn = tkinter.Entry(body, width=40)
- self.ccn.grid(row=1, column=1, sticky=sticky)
- tkinter.Label(body, text="Output file").grid(row=2)
- self.keypath = tkinter.Entry(body, width=40)
- self.keypath.grid(row=2, column=1, sticky=sticky)
- self.keypath.insert(2, "bnepubkey.b64")
- button = tkinter.Button(body, text="...", command=self.get_keypath)
- button.grid(row=2, column=2)
- buttons = tkinter.Frame(self)
- buttons.pack()
- botton = tkinter.Button(
- buttons, text="Fetch", width=10, command=self.generate)
- botton.pack(side=tkinter.constants.LEFT)
- tkinter.Frame(buttons, width=10).pack(side=tkinter.constants.LEFT)
- button = tkinter.Button(
- buttons, text="Quit", width=10, command=self.quit)
- button.pack(side=tkinter.constants.RIGHT)
-
- def get_keypath(self):
- keypath = tkinter.filedialog.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, tkinter.constants.END)
- self.keypath.insert(0, keypath)
- return
-
- def generate(self):
- email = self.name.get()
- password = self.ccn.get()
- keypath = self.keypath.get()
- if not email:
- self.status['text'] = "Email address not given"
- return
- if not password:
- self.status['text'] = "Account password not given"
- return
- if not keypath:
- self.status['text'] = "Output keyfile path not set"
- return
- self.status['text'] = "Fetching..."
- try:
- userkey = fetch_key(email, password)
- except Exception as e:
- self.status['text'] = "Error: {0}".format(e.args[0])
- return
- if len(userkey) == 28:
- open(keypath,'wb').write(userkey)
- self.status['text'] = "Keyfile fetched successfully"
- else:
- self.status['text'] = "Keyfile fetch failed."
-
- root = tkinter.Tk()
- root.title("Barnes & Noble ePub Keyfile Fetch v.{0}".format(__version__))
- root.resizable(True, False)
- root.minsize(300, 0)
- DecryptionDialog(root).pack(fill=tkinter.constants.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_plugin/ignoblepdf.py b/DeDRM_plugin/ignoblepdf.py
deleted file mode 100644
index 365eae2..0000000
--- a/DeDRM_plugin/ignoblepdf.py
+++ /dev/null
@@ -1,2164 +0,0 @@
-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-
-
-# ignoblepdf.py
-# Copyright © 2009-2020 by Apprentice Harper et al.
-
-# Released under the terms of the GNU General Public Licence, version 3
-#
-
-# Based on version 8.0.6 of ineptpdf.py
-
-
-# Revision history:
-# 0.1 - Initial alpha testing release 2020 by Pu D. Pud
-# 0.2 - Python 3 for calibre 5.0 (in testing)
-
-
-"""
-Decrypts Barnes & Noble encrypted PDF files.
-"""
-
-__license__ = 'GPL v3'
-__version__ = "0.2"
-
-import sys
-import os
-import re
-import zlib
-import struct
-import hashlib
-from decimal import *
-from itertools import chain, islice
-import xml.etree.ElementTree as etree
-
-# Wrap a stream so that output gets flushed immediately
-# and also make sure that any unicode strings get
-# encoded using "replace" before writing them.
-class SafeUnbuffered:
- def __init__(self, stream):
- self.stream = stream
- self.encoding = stream.encoding
- if self.encoding == None:
- self.encoding = "utf-8"
- def write(self, data):
- if isinstance(data, str):
- data = data.encode(self.encoding,"replace")
- self.stream.buffer.write(data)
- self.stream.buffer.flush()
-
- def __getattr__(self, attr):
- return getattr(self.stream, attr)
-
-iswindows = sys.platform.startswith('win')
-isosx = sys.platform.startswith('darwin')
-
-def unicode_argv():
- if iswindows:
- # Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
- # strings.
-
- # Versions 2.x of Python don't support Unicode in sys.argv on
- # Windows, with the underlying Windows API instead replacing multi-byte
- # characters with '?'.
-
-
- from ctypes import POINTER, byref, cdll, c_int, windll
- from ctypes.wintypes import LPCWSTR, LPWSTR
-
- GetCommandLineW = cdll.kernel32.GetCommandLineW
- GetCommandLineW.argtypes = []
- GetCommandLineW.restype = LPCWSTR
-
- CommandLineToArgvW = windll.shell32.CommandLineToArgvW
- CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
- CommandLineToArgvW.restype = POINTER(LPWSTR)
-
- cmd = GetCommandLineW()
- argc = c_int(0)
- argv = CommandLineToArgvW(cmd, byref(argc))
- if argc.value > 0:
- # Remove Python executable and commands if present
- start = argc.value - len(sys.argv)
- return [argv[i] for i in
- range(start, argc.value)]
- return ["ignoblepdf.py"]
- else:
- argvencoding = sys.stdin.encoding or "utf-8"
- return [arg if isinstance(arg, str) else str(arg, argvencoding) for arg in sys.argv]
-
-
-class IGNOBLEError(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 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)
-
- class RC4_KEY(Structure):
- _fields_ = [('x', c_int), ('y', c_int), ('box', c_int * 256)]
- RC4_KEY_p = POINTER(RC4_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])
-
- 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])
-
- 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):
- MODE_CBC = 0
- @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 IGNOBLEError('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 IGNOBLEError('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 IGNOBLEError('AES decryption failed')
- return out.raw
-
- return (ARC4, AES)
-
-
-def _load_crypto_pycrypto():
- from Crypto.Cipher import ARC4 as _ARC4
- from Crypto.Cipher import AES as _AES
-
- 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):
- MODE_CBC = _AES.MODE_CBC
- @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)
-
- return (ARC4, AES)
-
-def _load_crypto():
- ARC4 = 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, AES = loader()
- break
- except (ImportError, IGNOBLEError):
- pass
- return (ARC4, AES)
-ARC4, AES = _load_crypto()
-
-
-from io import BytesIO
-
-
-# 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('poll(%d): %r' % (pos, self.fp.read(n)), file=sys.stderr)
- 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_decimal, 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_decimal, j+1)
- try:
- self.add_token(int(self.token))
- except ValueError:
- pass
- return (self.parse_main, j)
-
- def parse_decimal(self, s, i):
- m = END_NUMBER.search(s, i)
- if not m:
- self.token += s[i:]
- return (self.parse_decimal, len(s))
- j = m.start(0)
- self.token += s[i:j]
- self.add_token(Decimal(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, Decimal) 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:
- print("Incomplete dictionary construct")
- objs.append("") # this isn't necessary.
- # temporary fix. is this due to rental books?
- # 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 decimal_value(x):
- x = resolve1(x)
- if not isinstance(x, Decimal):
- if STRICT:
- raise PDFTypeError('Decimal required: %r' % x)
- return 0.0
- return x
-
-def num_value(x):
- x = resolve1(x)
- if not (isinstance(x, int) or isinstance(x, Decimal)):
- 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(BytesIO(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 range(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 range(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 range(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
- raise PDFEncryptionError('Document is not encrypted.')
- 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 IGNOBLEError('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 IGNOBLEError('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 IGNOBLEError('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 range(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 IGNOBLEError('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, keyb64, docid, param):
- self.is_printable = self.is_modifiable = self.is_extractable = True
- key = keyb64.decode('base64')[:16]
- aes = AES.new(key,AES.MODE_CBC,"\x00" * len(key))
- 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 = aes.decrypt(bookkey)
- bookkey = bookkey[:-ord(bookkey[-1])]
- bookkey = bookkey[-16:]
- ebx_V = int_value(param.get('V', 4))
- ebx_type = int_value(param.get('EBX_ENCRYPTIONTYPE', 6))
- # added because of improper booktype / decryption book session key errors
- if length > 0:
- if len(bookkey) == length:
- if ebx_V == 3:
- V = 3
- else:
- V = 2
- elif len(bookkey) == length + 1:
- V = ord(bookkey[0])
- bookkey = bookkey[1:]
- else:
- print("ebx_V is %d and ebx_type is %d" % (ebx_V, ebx_type))
- print("length is %d and len(bookkey) is %d" % (length, len(bookkey)))
- print("bookkey[0] is %d" % ord(bookkey[0]))
- raise IGNOBLEError('error decrypting book session key - mismatched length')
- else:
- # proper length unknown try with whatever you have
- print("ebx_V is %d and ebx_type is %d" % (ebx_V, ebx_type))
- print("length is %d and len(bookkey) is %d" % (length, len(bookkey)))
- print("bookkey[0] is %d" % ord(bookkey[0]))
- if ebx_V == 3:
- V = 3
- else:
- V = 2
- 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, BytesIO(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, userkey):
- global GEN_XREF_STM, gen_xref_stm
- gen_xref_stm = GEN_XREF_STM > 1
- self.version = inf.read(8)
- inf.seek(0)
- self.doc = doc = PDFDocument()
- parser = PDFParser(doc, inf)
- doc.initialize(userkey)
- 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 range(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)):
- if self.last.isalnum():
- self.write(' ')
- self.write(str(obj))
- elif isinstance(obj, Decimal):
- 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')
-
-
-
-
-def decryptBook(userkey, inpath, outpath):
- if AES is None:
- raise IGNOBLEError("PyCrypto or OpenSSL must be installed.")
- with open(inpath, 'rb') as inf:
- #try:
- serializer = PDFSerializer(inf, userkey)
- #except:
- # print("Error serializing pdf {0}. Probably wrong key.".format(os.path.basename(inpath)))
- # return 2
- # hope this will fix the 'bad file descriptor' problem
- with open(outpath, 'wb') as outf:
- # help construct to make sure the method runs to the end
- try:
- serializer.dump(outf)
- except Exception as e:
- print("error writing pdf: {0}".format(e.args[0]))
- return 2
- return 0
-
-
-def cli_main():
- sys.stdout=SafeUnbuffered(sys.stdout)
- sys.stderr=SafeUnbuffered(sys.stderr)
- argv=unicode_argv()
- progname = os.path.basename(argv[0])
- if len(argv) != 4:
- print("usage: {0} ".format(progname))
- return 1
- keypath, inpath, outpath = argv[1:]
- userkey = open(keypath,'rb').read()
- result = decryptBook(userkey, inpath, outpath)
- if result == 0:
- print("Successfully decrypted {0:s} as {1:s}".format(os.path.basename(inpath),os.path.basename(outpath)))
- return result
-
-
-def gui_main():
- try:
- import tkinter
- import tkinter.constants
- import tkinter.filedialog
- import tkinter.messagebox
- import traceback
- except:
- return cli_main()
-
- class DecryptionDialog(tkinter.Frame):
- def __init__(self, root):
- tkinter.Frame.__init__(self, root, border=5)
- self.status = tkinter.Label(self, text="Select files for decryption")
- self.status.pack(fill=tkinter.constants.X, expand=1)
- body = tkinter.Frame(self)
- body.pack(fill=tkinter.constants.X, expand=1)
- sticky = tkinter.constants.E + tkinter.constants.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("bnpdfkey.b64"):
- self.keypath.insert(0, "bnpdfkey.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=tkinter.constants.LEFT)
- tkinter.Frame(buttons, width=10).pack(side=tkinter.constants.LEFT)
- button = tkinter.Button(
- buttons, text="Quit", width=10, command=self.quit)
- button.pack(side=tkinter.constants.RIGHT)
-
- def get_keypath(self):
- keypath = tkinter.filedialog.askopenfilename(
- parent=None, title="Select Barnes & Noble \'.b64\' key file",
- defaultextension=".b64",
- filetypes=[('base64-encoded files', '.b64'),
- ('All Files', '.*')])
- if keypath:
- keypath = os.path.normpath(keypath)
- self.keypath.delete(0, tkinter.constants.END)
- self.keypath.insert(0, keypath)
- return
-
- def get_inpath(self):
- inpath = tkinter.filedialog.askopenfilename(
- parent=None, title="Select B&N-encrypted PDF file to decrypt",
- defaultextension=".pdf", filetypes=[('PDF files', '.pdf')])
- if inpath:
- inpath = os.path.normpath(inpath)
- self.inpath.delete(0, tkinter.constants.END)
- self.inpath.insert(0, inpath)
- return
-
- def get_outpath(self):
- outpath = tkinter.filedialog.asksaveasfilename(
- parent=None, title="Select unencrypted PDF file to produce",
- defaultextension=".pdf", filetypes=[('PDF files', '.pdf')])
- if outpath:
- outpath = os.path.normpath(outpath)
- self.outpath.delete(0, tkinter.constants.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
- userkey = open(keypath,'rb').read()
- self.status['text'] = "Decrypting..."
- try:
- decrypt_status = decryptBook(userkey, inpath, outpath)
- except Exception as e:
- self.status['text'] = "Error; {0}".format(e.args[0])
- return
- if decrypt_status == 0:
- self.status['text'] = "File successfully decrypted"
- else:
- self.status['text'] = "The was an error decrypting the file."
-
-
- root = tkinter.Tk()
- if AES is None:
- root.withdraw()
- tkinter.messagebox.showerror(
- "IGNOBLE PDF",
- "This script requires OpenSSL or PyCrypto, which must be installed "
- "separately. Read the top-of-script comment for details.")
- return 1
- root.title("Barnes & Noble PDF Decrypter v.{0}".format(__version__))
- root.resizable(True, False)
- root.minsize(370, 0)
- DecryptionDialog(root).pack(fill=tkinter.constants.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_plugin/ineptepub.py b/DeDRM_plugin/ineptepub.py
index 8bab717..8d112e4 100644
--- a/DeDRM_plugin/ineptepub.py
+++ b/DeDRM_plugin/ineptepub.py
@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-
# ineptepub.py
-# Copyright © 2009-2020 by i♥cabbages, Apprentice Harper et al.
+# Copyright © 2009-2022 by i♥cabbages, Apprentice Harper et al.
# Released under the terms of the GNU General Public Licence, version 3
#
@@ -30,326 +30,96 @@
# 6.5 - Completely remove erroneous check on DER file sanity
# 6.6 - Import tkFileDialog, don't assume something else will import it.
# 7.0 - Add Python 3 compatibility for calibre 5.0
+# 7.1 - Add ignoble support, dropping the dedicated ignobleepub.py script
+# 7.2 - Only support PyCryptodome; clean up the code
+# 8.0 - Add support for "hardened" Adobe DRM (RMSDK >= 10)
"""
Decrypt Adobe Digital Editions encrypted ePub books.
"""
__license__ = 'GPL v3'
-__version__ = "7.0"
+__version__ = "8.0"
-import codecs
import sys
import os
import traceback
+import base64
import zlib
import zipfile
from zipfile import ZipInfo, ZipFile, ZIP_STORED, ZIP_DEFLATED
+from zeroedzipinfo import ZeroedZipInfo
from contextlib import closing
-import xml.etree.ElementTree as etree
-
-# Wrap a stream so that output gets flushed immediately
-# and also make sure that any unicode strings get
-# encoded using "replace" before writing them.
-class SafeUnbuffered:
- def __init__(self, stream):
- self.stream = stream
- self.encoding = stream.encoding
- if self.encoding == None:
- self.encoding = "utf-8"
- def write(self, data):
- if isinstance(data, str):
- data = data.encode(self.encoding,"replace")
- self.stream.buffer.write(data)
- self.stream.buffer.flush()
-
- def __getattr__(self, attr):
- return getattr(self.stream, attr)
+from lxml import etree
+from uuid import UUID
+import hashlib
try:
- from calibre.constants import iswindows, isosx
-except:
- iswindows = sys.platform.startswith('win')
- isosx = sys.platform.startswith('darwin')
-
-def unicode_argv():
- if iswindows:
- # Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
- # strings.
-
- # Versions 2.x of Python don't support Unicode in sys.argv on
- # Windows, with the underlying Windows API instead replacing multi-byte
- # characters with '?'.
-
-
- from ctypes import POINTER, byref, cdll, c_int, windll
- from ctypes.wintypes import LPCWSTR, LPWSTR
-
- GetCommandLineW = cdll.kernel32.GetCommandLineW
- GetCommandLineW.argtypes = []
- GetCommandLineW.restype = LPCWSTR
-
- CommandLineToArgvW = windll.shell32.CommandLineToArgvW
- CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
- CommandLineToArgvW.restype = POINTER(LPWSTR)
-
- cmd = GetCommandLineW()
- argc = c_int(0)
- argv = CommandLineToArgvW(cmd, byref(argc))
- if argc.value > 0:
- # Remove Python executable and commands if present
- start = argc.value - len(sys.argv)
- return [argv[i] for i in
- range(start, argc.value)]
- return ["ineptepub.py"]
+ from Cryptodome.Cipher import AES, PKCS1_v1_5
+ from Cryptodome.PublicKey import RSA
+except ImportError:
+ from Crypto.Cipher import AES, PKCS1_v1_5
+ from Crypto.PublicKey import RSA
+
+
+def unpad(data, padding=16):
+ if sys.version_info[0] == 2:
+ pad_len = ord(data[-1])
else:
- argvencoding = sys.stdin.encoding or "utf-8"
- return [arg if isinstance(arg, str) else str(arg, argvencoding) for arg in sys.argv]
+ pad_len = data[-1]
+ return data[:-pad_len]
-class ADEPTError(Exception):
- pass
+from utilities import SafeUnbuffered
-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
+from argv_utils import unicode_argv
- if iswindows:
- 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 = (b"\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
- from Crypto.Cipher import PKCS1_v1_5 as _PKCS1_v1_5
-
- # 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, b'\x00'*16)
-
- def decrypt(self, data):
- return self._aes.decrypt(data)
-
- class RSA(object):
- def __init__(self, der):
- key = ASN1Parser([x for x in der])
- key = [key.getChild(x).value for x in range(1, 4)]
- key = [self.bytesToNumber(v) for v in key]
- self._rsa = _RSA.construct(key)
-
- def bytesToNumber(self, bytes):
- total = 0
- for byte in bytes:
- total = (total << 8) + byte
- return total
-
- def decrypt(self, data):
- return _PKCS1_v1_5.new(self._rsa).decrypt(data, 172)
-
- 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()
+class ADEPTError(Exception):
+ pass
+
+class ADEPTNewVersionError(Exception):
+ pass
-META_NAMES = ('mimetype', 'META-INF/rights.xml', 'META-INF/encryption.xml')
+META_NAMES = ('mimetype', 'META-INF/rights.xml')
NSMAP = {'adept': 'http://ns.adobe.com/adept',
'enc': 'http://www.w3.org/2001/04/xmlenc#'}
class Decryptor(object):
def __init__(self, bookkey, encryption):
enc = lambda tag: '{%s}%s' % (NSMAP['enc'], tag)
- self._aes = AES(bookkey)
+ self._aes = AES.new(bookkey, AES.MODE_CBC, b'\x00'*16)
encryption = etree.fromstring(encryption)
self._encrypted = encrypted = set()
+ self._otherData = otherData = set()
+
+ self._json_elements_to_remove = json_elements_to_remove = set()
+ self._has_remaining_xml = False
expr = './%s/%s/%s' % (enc('EncryptedData'), enc('CipherData'),
enc('CipherReference'))
for elem in encryption.findall(expr):
path = elem.get('URI', None)
+ encryption_type_url = (elem.getparent().getparent().find("./%s" % (enc('EncryptionMethod'))).get('Algorithm', None))
if path is not None:
- path = path.encode('utf-8')
- encrypted.add(path)
+ if (encryption_type_url == "http://www.w3.org/2001/04/xmlenc#aes128-cbc"):
+ # Adobe
+ path = path.encode('utf-8')
+ encrypted.add(path)
+ json_elements_to_remove.add(elem.getparent().getparent())
+ else:
+ path = path.encode('utf-8')
+ otherData.add(path)
+ self._has_remaining_xml = True
+
+ for elem in json_elements_to_remove:
+ elem.getparent().remove(elem)
+
+ def check_if_remaining(self):
+ return self._has_remaining_xml
+
+ def get_xml(self):
+ return "\n" + etree.tostring(self._encryption, encoding="utf-8", pretty_print=True, xml_declaration=False).decode("utf-8")
+
def decompress(self, bytes):
dc = zlib.decompressobj(-15)
@@ -361,8 +131,8 @@ class Decryptor(object):
except:
# possibly not compressed by zip - just return bytes
return bytes
- return decompressed_bytes
-
+ return decompressed_bytes
+
def decrypt(self, path, data):
if path.encode('utf-8') in self._encrypted:
data = self._aes.decrypt(data)[16:]
@@ -386,19 +156,68 @@ def adeptBook(inpath):
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
expr = './/%s' % (adept('encryptedKey'),)
bookkey = ''.join(rights.findtext(expr))
- if len(bookkey) == 172:
+ if len(bookkey) in [192, 172, 64]:
return True
except:
# if we couldn't check, assume it is
return True
return False
-def decryptBook(userkey, inpath, outpath):
- if AES is None:
- raise ADEPTError("PyCrypto or OpenSSL must be installed.")
- rsa = RSA(userkey)
+def isPassHashBook(inpath):
+ # If this is an Adobe book, check if it's a PassHash-encrypted book (B&N)
with closing(ZipFile(open(inpath, 'rb'))) as inf:
namelist = set(inf.namelist())
+ if 'META-INF/rights.xml' not in namelist or \
+ 'META-INF/encryption.xml' not in namelist:
+ return False
+ try:
+ rights = etree.fromstring(inf.read('META-INF/rights.xml'))
+ adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
+ expr = './/%s' % (adept('encryptedKey'),)
+ bookkey = ''.join(rights.findtext(expr))
+ if len(bookkey) == 64:
+ return True
+ except:
+ pass
+
+ return False
+
+# Checks the license file and returns the UUID the book is licensed for.
+# This is used so that the Calibre plugin can pick the correct decryption key
+# first try without having to loop through all possible keys.
+def adeptGetUserUUID(inpath):
+ with closing(ZipFile(open(inpath, 'rb'))) as inf:
+ try:
+ rights = etree.fromstring(inf.read('META-INF/rights.xml'))
+ adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
+ expr = './/%s' % (adept('user'),)
+ user_uuid = ''.join(rights.findtext(expr))
+ if user_uuid[:9] != "urn:uuid:":
+ return None
+ return user_uuid[9:]
+ except:
+ return None
+
+def removeHardening(rights, keytype, keydata):
+ adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
+ textGetter = lambda name: ''.join(rights.findtext('.//%s' % (adept(name),)))
+
+ # Gather what we need, and generate the IV
+ resourceuuid = UUID(textGetter("resource"))
+ deviceuuid = UUID(textGetter("device"))
+ fullfillmentuuid = UUID(textGetter("fulfillment")[:36])
+ kekiv = UUID(int=resourceuuid.int ^ deviceuuid.int ^ fullfillmentuuid.int).bytes
+
+ # Derive kek from just "keytype"
+ rem = int(keytype, 10) % 16
+ H = hashlib.sha256(keytype.encode("ascii")).digest()
+ kek = H[2*rem : 16 + rem] + H[rem : 2*rem]
+
+ return unpad(AES.new(kek, AES.MODE_CBC, kekiv).decrypt(keydata), 16) # PKCS#7
+
+def decryptBook(userkey, inpath, outpath):
+ with closing(ZipFile(open(inpath, 'rb'))) as inf:
+ namelist = inf.namelist()
if 'META-INF/rights.xml' not in namelist or \
'META-INF/encryption.xml' not in namelist:
print("{0:s} is DRM-free.".format(os.path.basename(inpath)))
@@ -409,42 +228,65 @@ def decryptBook(userkey, inpath, outpath):
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
expr = './/%s' % (adept('encryptedKey'),)
- bookkey = ''.join(rights.findtext(expr))
- if len(bookkey) != 172:
- print("{0:s} is not a secure Adobe Adept ePub.".format(os.path.basename(inpath)))
+ bookkeyelem = rights.find(expr)
+ bookkey = bookkeyelem.text
+ keytype = bookkeyelem.attrib.get('keyType', '0')
+ if len(bookkey) >= 172 and int(keytype, 10) > 2:
+ print("{0:s} is a secure Adobe Adept ePub with hardening.".format(os.path.basename(inpath)))
+ elif len(bookkey) == 172:
+ print("{0:s} is a secure Adobe Adept ePub.".format(os.path.basename(inpath)))
+ elif len(bookkey) == 64:
+ print("{0:s} is a secure Adobe PassHash (B&N) ePub.".format(os.path.basename(inpath)))
+ else:
+ print("{0:s} is not an Adobe-protected ePub!".format(os.path.basename(inpath)))
return 1
- bookkey = rsa.decrypt(codecs.decode(bookkey.encode('ascii'), 'base64'))
- # Padded as per RSAES-PKCS1-v1_5
- if len(bookkey) > 16:
- if bookkey[-17] == '\x00' or bookkey[-17] == 0:
- bookkey = bookkey[-16:]
- else:
+
+ if len(bookkey) != 64:
+ # Normal or "hardened" Adobe ADEPT
+ rsakey = RSA.importKey(userkey) # parses the ASN1 structure
+ bookkey = base64.b64decode(bookkey)
+ if int(keytype, 10) > 2:
+ bookkey = removeHardening(rights, keytype, bookkey)
+ try:
+ bookkey = PKCS1_v1_5.new(rsakey).decrypt(bookkey, None) # automatically unpads
+ except ValueError:
+ bookkey = None
+
+ if bookkey is None:
print("Could not decrypt {0:s}. Wrong key".format(os.path.basename(inpath)))
return 2
+ else:
+ # Adobe PassHash / B&N
+ key = base64.b64decode(userkey)[:16]
+ bookkey = base64.b64decode(bookkey)
+ bookkey = unpad(AES.new(key, AES.MODE_CBC, b'\x00'*16).decrypt(bookkey), 16) # PKCS#7
+
+ if len(bookkey) > 16:
+ bookkey = bookkey[-16:]
+
encryption = inf.read('META-INF/encryption.xml')
decryptor = Decryptor(bookkey, encryption)
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf:
- zi = ZipInfo('mimetype')
- zi.compress_type=ZIP_STORED
- try:
- # if the mimetype is present, get its info, including time-stamp
- oldzi = inf.getinfo('mimetype')
- # copy across fields to be preserved
- zi.date_time = oldzi.date_time
- zi.comment = oldzi.comment
- zi.extra = oldzi.extra
- zi.internal_attr = oldzi.internal_attr
- # external attributes are dependent on the create system, so copy both.
- zi.external_attr = oldzi.external_attr
- zi.create_system = oldzi.create_system
- except:
- pass
- outf.writestr(zi, inf.read('mimetype'))
- for path in namelist:
+
+ for path in (["mimetype"] + namelist):
data = inf.read(path)
zi = ZipInfo(path)
zi.compress_type=ZIP_DEFLATED
+
+ if path == "mimetype":
+ zi.compress_type = ZIP_STORED
+
+ elif path == "META-INF/encryption.xml":
+ # Check if there's still something in there
+ if (decryptor.check_if_remaining()):
+ data = decryptor.get_xml()
+ print("Adding encryption.xml for the remaining embedded files.")
+ # We removed DRM, but there's still stuff like obfuscated fonts.
+ else:
+ continue
+
+
try:
# get the file info, including time-stamp
oldzi = inf.getinfo(path)
@@ -455,10 +297,27 @@ def decryptBook(userkey, inpath, outpath):
zi.internal_attr = oldzi.internal_attr
# external attributes are dependent on the create system, so copy both.
zi.external_attr = oldzi.external_attr
+
+ zi.volume = oldzi.volume
zi.create_system = oldzi.create_system
+ zi.create_version = oldzi.create_version
+
+ if any(ord(c) >= 128 for c in path) or any(ord(c) >= 128 for c in zi.comment):
+ # If the file name or the comment contains any non-ASCII char, set the UTF8-flag
+ zi.flag_bits |= 0x800
except:
pass
- outf.writestr(zi, decryptor.decrypt(path, data))
+
+ # Python 3 has a bug where the external_attr is reset to `0o600 << 16`
+ # if it's NULL, so we need a workaround:
+ if zi.external_attr == 0:
+ zi = ZeroedZipInfo(zi)
+
+
+ if path == "META-INF/encryption.xml":
+ outf.writestr(zi, data)
+ else:
+ outf.writestr(zi, decryptor.decrypt(path, data))
except:
print("Could not decrypt {0:s} because of an exception:\n{1:s}".format(os.path.basename(inpath), traceback.format_exc()))
return 2
@@ -468,7 +327,7 @@ def decryptBook(userkey, inpath, outpath):
def cli_main():
sys.stdout=SafeUnbuffered(sys.stdout)
sys.stderr=SafeUnbuffered(sys.stderr)
- argv=unicode_argv()
+ argv=unicode_argv("ineptepub.py")
progname = os.path.basename(argv[0])
if len(argv) != 4:
print("usage: {0} ".format(progname))
diff --git a/DeDRM_plugin/ineptpdf.py b/DeDRM_plugin/ineptpdf.py
index e8385fe..adb9937 100755
--- a/DeDRM_plugin/ineptpdf.py
+++ b/DeDRM_plugin/ineptpdf.py
@@ -3,6 +3,7 @@
# ineptpdf.py
# Copyright © 2009-2020 by i♥cabbages, Apprentice Harper et al.
+# Copyright © 2021-2022 by noDRM et al.
# Released under the terms of the GNU General Public Licence, version 3
#
@@ -46,367 +47,70 @@
# 8.0.5 - Do not process DRM-free documents
# 8.0.6 - Replace use of float by Decimal for greater precision, and import tkFileDialog
# 9.0.0 - Add Python 3 compatibility for calibre 5
+# 9.1.0 - Support for decrypting with owner password, support for V=5, R=5 and R=6 PDF files, support for AES256-encrypted PDFs.
+# 9.1.1 - Only support PyCryptodome; clean up the code
+# 10.0.0 - Add support for "hardened" Adobe DRM (RMSDK >= 10)
+# 10.0.2 - Fix some Python2 stuff
+# 10.0.4 - Fix more Python2 stuff
"""
Decrypts Adobe ADEPT-encrypted PDF files.
"""
__license__ = 'GPL v3'
-__version__ = "9.0.0"
+__version__ = "10.0.4"
import codecs
+import hashlib
import sys
import os
import re
import zlib
import struct
-import hashlib
+import binascii
+import base64
from io import BytesIO
from decimal import Decimal
import itertools
import xml.etree.ElementTree as etree
+import traceback
+from uuid import UUID
-# Wrap a stream so that output gets flushed immediately
-# and also make sure that any unicode strings get
-# encoded using "replace" before writing them.
-class SafeUnbuffered:
- def __init__(self, stream):
- self.stream = stream
- self.encoding = stream.encoding
- if self.encoding == None:
- self.encoding = "utf-8"
- def write(self, data):
- if isinstance(data, str):
- data = data.encode(self.encoding,"replace")
- self.stream.buffer.write(data)
- self.stream.buffer.flush()
-
- def __getattr__(self, attr):
- return getattr(self.stream, attr)
-
-iswindows = sys.platform.startswith('win')
-isosx = sys.platform.startswith('darwin')
-
-def unicode_argv():
- if iswindows:
- # Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
- # strings.
+try:
+ from Cryptodome.Cipher import AES, ARC4, PKCS1_v1_5
+ from Cryptodome.PublicKey import RSA
+except ImportError:
+ from Crypto.Cipher import AES, ARC4, PKCS1_v1_5
+ from Crypto.PublicKey import RSA
- # Versions 2.x of Python don't support Unicode in sys.argv on
- # Windows, with the underlying Windows API instead replacing multi-byte
- # characters with '?'.
+def unpad(data, padding=16):
+ if sys.version_info[0] == 2:
+ pad_len = ord(data[-1])
+ else:
+ pad_len = data[-1]
- from ctypes import POINTER, byref, cdll, c_int, windll
- from ctypes.wintypes import LPCWSTR, LPWSTR
+ return data[:-pad_len]
- GetCommandLineW = cdll.kernel32.GetCommandLineW
- GetCommandLineW.argtypes = []
- GetCommandLineW.restype = LPCWSTR
- CommandLineToArgvW = windll.shell32.CommandLineToArgvW
- CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
- CommandLineToArgvW.restype = POINTER(LPWSTR)
+from utilities import SafeUnbuffered
- cmd = GetCommandLineW()
- argc = c_int(0)
- argv = CommandLineToArgvW(cmd, byref(argc))
- if argc.value > 0:
- # Remove Python executable and commands if present
- start = argc.value - len(sys.argv)
- return [argv[i] for i in
- range(start, argc.value)]
- return ["ineptpdf.py"]
- else:
- argvencoding = sys.stdin.encoding or "utf-8"
- return [arg if isinstance(arg, str) else str(arg, argvencoding) for arg in sys.argv]
+iswindows = sys.platform.startswith('win')
+isosx = sys.platform.startswith('darwin')
+from argv_utils import unicode_argv
class ADEPTError(Exception):
pass
+class ADEPTInvalidPasswordError(Exception):
+ pass
-import hashlib
+class ADEPTNewVersionError(Exception):
+ pass
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):
- MODE_CBC = 0
- @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
- from Crypto.Cipher import PKCS1_v1_5 as _PKCS1_v1_5
-
- # 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):
- MODE_CBC = _AES.MODE_CBC
- @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([x for x in der])
- key = [key.getChild(x).value for x in range(1, 4)]
- key = [self.bytesToNumber(v) for v in key]
- self._rsa = _RSA.construct(key)
-
- def bytesToNumber(self, bytes):
- total = 0
- for byte in bytes:
- total = (total << 8) + byte
- return total
-
- def decrypt(self, data):
- return _PKCS1_v1_5.new(self._rsa).decrypt(data, 172)
-
- 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()
-
-
-
+ return hashlib.sha256(message).digest()
# Do we generate cross reference streams on output?
# 0 = never
@@ -442,7 +146,10 @@ def nunpack(s, default=0):
elif l == 2:
return struct.unpack('>H', s)[0]
elif l == 3:
- return struct.unpack('>L', b'\x00'+s)[0]
+ if sys.version_info[0] == 2:
+ return struct.unpack('>L', '\x00'+s)[0]
+ else:
+ return struct.unpack('>L', bytes([0]) + s)[0]
elif l == 4:
return struct.unpack('>L', s)[0]
else:
@@ -550,19 +257,24 @@ def keyword_name(x):
## PSBaseParser
##
-EOL = re.compile(rb'[\r\n]')
-SPC = re.compile(rb'\s')
-NONSPC = re.compile(rb'\S')
-HEX = re.compile(rb'[0-9a-fA-F]')
-END_LITERAL = re.compile(rb'[#/%\[\]()<>{}\s]')
-END_HEX_STRING = re.compile(rb'[^\s0-9a-fA-F]')
-HEX_PAIR = re.compile(rb'[0-9a-fA-F]{2}|.')
-END_NUMBER = re.compile(rb'[^0-9]')
-END_KEYWORD = re.compile(rb'[#/%\[\]()<>{}\s]')
-END_STRING = re.compile(rb'[()\\]')
-OCT_STRING = re.compile(rb'[0-7]')
+EOL = re.compile(br'[\r\n]')
+SPC = re.compile(br'\s')
+NONSPC = re.compile(br'\S')
+HEX = re.compile(br'[0-9a-fA-F]')
+END_LITERAL = re.compile(br'[#/%\[\]()<>{}\s]')
+END_HEX_STRING = re.compile(br'[^\s0-9a-fA-F]')
+HEX_PAIR = re.compile(br'[0-9a-fA-F]{2}|.')
+END_NUMBER = re.compile(br'[^0-9]')
+END_KEYWORD = re.compile(br'[#/%\[\]()<>{}\s]')
+END_STRING = re.compile(br'[()\\]')
+OCT_STRING = re.compile(br'[0-7]')
ESC_STRING = { b'b':8, b't':9, b'n':10, b'f':12, b'r':13, b'(':40, b')':41, b'\\':92 }
+class EmptyArrayValue(object):
+ def __str__(self):
+ return "<>"
+
+
class PSBaseParser(object):
'''
@@ -625,7 +337,12 @@ class PSBaseParser(object):
if not m:
return (self.parse_main, len(s))
j = m.start(0)
- c = bytes([s[j]])
+ if isinstance(s[j], str):
+ # Python 2
+ c = s[j]
+ else:
+ # Python 3
+ c = bytes([s[j]])
self.tokenstart = self.bufpos+j
if c == b'%':
self.token = c
@@ -677,7 +394,10 @@ class PSBaseParser(object):
return (self.parse_literal, len(s))
j = m.start(0)
self.token += s[i:j]
- c = bytes([s[j]])
+ if isinstance(s[j], str):
+ c = s[j]
+ else:
+ c = bytes([s[j]])
if c == b'#':
self.hex = b''
return (self.parse_literal_hex, j+1)
@@ -685,12 +405,18 @@ class PSBaseParser(object):
return (self.parse_main, j)
def parse_literal_hex(self, s, i):
- c = bytes([s[i]])
+ if isinstance(s[i], str):
+ c = s[i]
+ else:
+ c = bytes([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 += bytes([int(self.hex, 16)])
+ if sys.version_info[0] == 2:
+ self.token += chr(int(self.hex, 16))
+ else:
+ self.token += bytes([int(self.hex, 16)])
return (self.parse_literal, i)
def parse_number(self, s, i):
@@ -700,7 +426,10 @@ class PSBaseParser(object):
return (self.parse_number, len(s))
j = m.start(0)
self.token += s[i:j]
- c = bytes([s[j]])
+ if isinstance(s[j], str):
+ c = s[j]
+ else:
+ c = bytes([s[j]])
if c == b'.':
self.token += c
return (self.parse_decimal, j+1)
@@ -743,7 +472,10 @@ class PSBaseParser(object):
return (self.parse_string, len(s))
j = m.start(0)
self.token += s[i:j]
- c = bytes([s[j]])
+ if isinstance(s[j], str):
+ c = s[j]
+ else:
+ c = bytes([s[j]])
if c == b'\\':
self.oct = ''
return (self.parse_string_1, j+1)
@@ -760,41 +492,69 @@ class PSBaseParser(object):
return (self.parse_main, j+1)
def parse_string_1(self, s, i):
- c = bytes([s[i]])
+ if isinstance(s[i], str):
+ c = s[i]
+ else:
+ c = bytes([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 += bytes([int(self.oct, 8)])
+ if sys.version_info[0] == 2:
+ self.token += chr(int(self.oct, 8))
+ else:
+ self.token += bytes([int(self.oct, 8)])
return (self.parse_string, i)
if c in ESC_STRING:
- self.token += bytes([ESC_STRING[c]])
+
+ if sys.version_info[0] == 2:
+ self.token += chr(ESC_STRING[c])
+ else:
+ self.token += bytes([ESC_STRING[c]])
+
return (self.parse_string, i+1)
def parse_wopen(self, s, i):
- c = bytes([s[i]])
+ if isinstance(s[i], str):
+ c = s[i]
+ else:
+ c = bytes([s[i]])
if c.isspace() or HEX.match(c):
return (self.parse_hexstring, i)
if c == b'<':
self.add_token(KEYWORD_DICT_BEGIN)
i += 1
+ if c == b'>':
+ # Empty array without any contents. Why though?
+ # We need to add some dummy python object that will serialize to
+ # nothing, otherwise the code removes the whole array.
+ self.add_token(EmptyArrayValue())
+ i += 1
+
return (self.parse_main, i)
def parse_wclose(self, s, i):
- c = bytes([s[i]])
+ if isinstance(s[i], str):
+ c = s[i]
+ else:
+ c = bytes([s[i]])
if c == b'>':
self.add_token(KEYWORD_DICT_END)
i += 1
return (self.parse_main, i)
def parse_hexstring(self, s, i):
- m1 = END_HEX_STRING.search(s, i)
- if not m1:
+ m = END_HEX_STRING.search(s, i)
+ if not m:
self.token += s[i:]
return (self.parse_hexstring, len(s))
- j = m1.start(0)
+ j = m.start(0)
self.token += s[i:j]
- token = HEX_PAIR.sub(lambda m2: bytes([int(m2.group(0), 16)]),
+ if sys.version_info[0] == 2:
+ token = HEX_PAIR.sub(lambda m: chr(int(m.group(0), 16)),
+ SPC.sub('', self.token))
+ else:
+ token = HEX_PAIR.sub(lambda m: bytes([int(m.group(0), 16)]),
SPC.sub(b'', self.token))
self.add_token(token)
return (self.parse_main, j)
@@ -816,7 +576,11 @@ class PSBaseParser(object):
while 1:
self.fillbuf()
if eol:
- c = bytes([self.buf[self.charpos]])
+ if sys.version_info[0] == 2:
+ c = self.buf[self.charpos]
+ else:
+ c = bytes([self.buf[self.charpos]])
+
# handle '\r\n'
if c == b'\n':
linebuf += c
@@ -826,10 +590,17 @@ class PSBaseParser(object):
if m:
linebuf += self.buf[self.charpos:m.end(0)]
self.charpos = m.end(0)
- if bytes([linebuf[-1]]) == b'\r':
- eol = True
- else:
- break
+ if sys.version_info[0] == 2:
+ if linebuf[-1] == b'\r':
+ eol = True
+ else:
+ break
+ else:
+ if bytes([linebuf[-1]]) == b'\r':
+ eol = True
+ else:
+ break
+
else:
linebuf += self.buf[self.charpos:]
self.charpos = len(self.buf)
@@ -923,6 +694,7 @@ class PSStackParser(PSBaseParser):
isinstance(token, bool) or
isinstance(token, bytearray) or
isinstance(token, bytes) or
+ isinstance(token, str) or
isinstance(token, PSLiteral)):
# normal token
self.push((pos, token))
@@ -1030,7 +802,7 @@ def decipher_all(decipher, objid, genno, x):
'''
Recursively decipher X.
'''
- if isinstance(x, bytearray) or isinstance(x,bytes):
+ if isinstance(x, bytearray) or isinstance(x,bytes) or isinstance(x,str):
return decipher(objid, genno, x)
decf = lambda v: decipher_all(decipher, objid, genno, v)
if isinstance(x, list):
@@ -1067,7 +839,7 @@ def num_value(x):
def str_value(x):
x = resolve1(x)
- if not (isinstance(x, bytearray) or isinstance(x, bytes)):
+ if not (isinstance(x, bytearray) or isinstance(x, bytes) or isinstance(x, str)):
if STRICT:
raise PDFTypeError('String required: %r' % x)
return ''
@@ -1204,9 +976,14 @@ class PDFStream(PDFObject):
for i in range(0, len(data), columns+1):
pred = data[i]
ent1 = data[i+1:i+1+columns]
- if pred == 2:
- ent1 = b''.join(bytes([(a+b) & 255]) \
- for (a,b) in zip(ent0,ent1))
+ if sys.version_info[0] == 2:
+ if pred == '\x02':
+ ent1 = ''.join(chr((ord(a)+ord(b)) & 255) \
+ for (a,b) in zip(ent0,ent1))
+ else:
+ if pred == 2:
+ ent1 = b''.join(bytes([(a+b) & 255]) \
+ for (a,b) in zip(ent0,ent1))
buf += ent1
ent0 = ent1
data = buf
@@ -1417,7 +1194,6 @@ class PDFDocument(object):
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
@@ -1453,7 +1229,7 @@ class PDFDocument(object):
# 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=b''):
+ def initialize(self, password=b'', inept=True):
if not self.encryption:
self.is_printable = self.is_modifiable = self.is_extractable = True
self.ready = True
@@ -1465,10 +1241,23 @@ class PDFDocument(object):
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)
+ if type == 'EBX_HANDLER' and inept is True:
+ return self.initialize_ebx_inept(password, docid, param)
+ if type == 'EBX_HANDLER' and inept is False:
+ return self.initialize_ebx_ignoble(password, docid, param)
+
raise PDFEncryptionError('Unknown filter: param=%r' % param)
+ def initialize_and_return_filter(self):
+ if not self.encryption:
+ self.is_printable = self.is_modifiable = self.is_extractable = True
+ self.ready = True
+ return None
+
+ (docid, param) = self.encryption
+ type = literal_name(param['Filter'])
+ return type
+
def initialize_adobe_ps(self, password, docid, param):
global KEYFILEPATH
self.decrypt_key = self.genkey_adobe_ps(param)
@@ -1511,30 +1300,171 @@ class PDFDocument(object):
PASSWORD_PADDING = b'(\xbfN^Nu\x8aAd\x00NV\xff\xfa\x01\x08..' \
b'\x00\xb6\xd0h>\x80/\x0c\xa9\xfedSiz'
# experimental aes pw support
- def initialize_standard(self, password, docid, param):
- # copy from a global variable
+
+ def check_user_password(self, password, docid, param):
+ V = int_value(param.get('V', 0))
+ if V < 5:
+ return self.check_user_password_V4(password, docid, param)
+ else:
+ return self.check_user_password_V5(password, param)
+
+ def check_owner_password(self, password, docid, param):
+ V = int_value(param.get('V', 0))
+ if V < 5:
+ return self.check_owner_password_V4(password, docid, param)
+ else:
+ return self.check_owner_password_V5(password, param)
+
+ def check_user_password_V5(self, password, param):
+ U = str_value(param['U'])
+ userdata = U[:32]
+ salt = U[32:32+8]
+ # Truncate password:
+ password = password[:min(127, len(password))]
+ if self.hash_V5(password, salt, b"", param) == userdata:
+ return True
+ return None
+
+ def check_owner_password_V5(self, password, param):
+ U = str_value(param['U'])
+ O = str_value(param['O'])
+ userdata = U[:48]
+ ownerdata = O[:32]
+ salt = O[32:32+8]
+ # Truncate password:
+ password = password[:min(127, len(password))]
+ if self.hash_V5(password, salt, userdata, param) == ownerdata:
+ return True
+ return None
+
+ def recover_encryption_key_with_password(self, password, docid, param):
+ # Truncate password:
+ key_password = password[:min(127, len(password))]
+
+ if self.check_owner_password_V5(key_password, param):
+ O = str_value(param['O'])
+ U = str_value(param['U'])
+ OE = str_value(param['OE'])
+ key_salt = O[40:40+8]
+ user_data = U[:48]
+ encrypted_file_key = OE[:32]
+ elif self.check_user_password_V5(key_password, param):
+ U = str_value(param['U'])
+ UE = str_value(param['UE'])
+ key_salt = U[40:40+8]
+ user_data = b""
+ encrypted_file_key = UE[:32]
+ else:
+ raise Exception("Trying to recover key, but neither user nor owner pass is correct.")
+
+ intermediate_key = self.hash_V5(key_password, key_salt, user_data, param)
+
+ file_key = self.process_with_aes(intermediate_key, False, encrypted_file_key)
+
+ return file_key
+
+
+ def process_with_aes(self, key, encrypt, data, repetitions = 1, iv = None):
+ if iv is None:
+ keylen = len(key)
+ iv = bytes([0x00]*keylen)
+
+ if not encrypt:
+ plaintext = AES.new(key,AES.MODE_CBC,iv, True).decrypt(data)
+ return plaintext
+ else:
+ aes = AES.new(key, AES.MODE_CBC, iv, False)
+ new_data = bytes(data * repetitions)
+ crypt = aes.encrypt(new_data)
+ return crypt
+
+
+ def hash_V5(self, password, salt, userdata, param):
+ R = int_value(param['R'])
+ K = SHA256(password + salt + userdata)
+ if R < 6:
+ return K
+ elif R == 6:
+ round_number = 0
+ done = False
+ while (not done):
+ round_number = round_number + 1
+ K1 = password + K + userdata
+ if len(K1) < 32:
+ raise Exception("K1 < 32 ...")
+ #def process_with_aes(self, key: bytes, encrypt: bool, data: bytes, repetitions: int = 1, iv: bytes = None):
+ E = self.process_with_aes(K[:16], True, K1, 64, K[16:32])
+ K = (hashlib.sha256, hashlib.sha384, hashlib.sha512)[sum(E) % 3](E).digest()
+
+ if round_number >= 64:
+ ch = int.from_bytes(E[-1:], "big", signed=False)
+ if ch <= round_number - 32:
+ done = True
+
+ result = K[0:32]
+ return result
+ else:
+ raise NotImplementedError("Revision > 6 not supported.")
+
+
+ def check_owner_password_V4(self, password, docid, param):
+
+ # compute_O_rc4_key:
+ V = int_value(param.get('V', 0))
+ if V >= 5:
+ raise Exception("compute_O_rc4_key not possible with V>= 5")
+
+ R = int_value(param.get('R', 0))
+
+ length = int_value(param.get('Length', 40)) # Key length (bits)
+ password = (password+self.PASSWORD_PADDING)[:32]
+ hash = hashlib.md5(password)
+ if R >= 3:
+ for _ in range(50):
+ hash = hashlib.md5(hash.digest()[:length//8])
+ hash = hash.digest()[:length//8]
+
+ # "hash" is the return value of compute_O_rc4_key
+
+ Odata = str_value(param.get('O'))
+ # now call iterate_rc4 ...
+ x = ARC4.new(hash).decrypt(Odata) # 4
+ if R >= 3:
+ for i in range(1,19+1):
+ if sys.version_info[0] == 2:
+ k = b''.join(chr(ord(c) ^ i) for c in hash )
+ else:
+ k = b''.join(bytes([c ^ i]) for c in hash )
+ x = ARC4.new(k).decrypt(x)
+
+
+ # "x" is now the padded user password.
+
+ # If we wanted to recover / extract the user password,
+ # we'd need to trim off the padding string from the end.
+ # As we just want to get access to the encryption key,
+ # we can just hand the password into the check_user_password
+ # as it is, as that function would be adding padding anyways.
+ # This trick only works with V4 and lower.
+
+ enc_key = self.check_user_password(x, docid, param)
+ if enc_key is not None:
+ return enc_key
+
+ return False
+
+
+
+
+ def check_user_password_V4(self, password, docid, param):
+
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 = b'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
@@ -1542,9 +1472,13 @@ class PDFDocument(object):
hash.update(struct.pack('= 4:
hash.update(codecs.decode(b'ffffffff','hex'))
- if 5 <= R:
+ if R >= 3:
# 8
for _ in range(50):
hash = hashlib.md5(hash.digest()[:length//8])
@@ -1558,51 +1492,207 @@ class PDFDocument(object):
hash.update(docid[0]) # 3
x = ARC4.new(key).decrypt(hash.digest()[:16]) # 4
for i in range(1,19+1):
- k = b''.join(bytes([c ^ i]) for c in key )
+ if sys.version_info[0] == 2:
+ k = b''.join(chr(ord(c) ^ i) for c in key )
+ else:
+ k = b''.join(bytes([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
+
+ if is_authenticated:
+ return key
+
+ return None
+
+ def initialize_standard(self, password, docid, param):
+
+ self.decrypt_key = None
+
+
+ # copy from a global variable
+ V = int_value(param.get('V', 0))
+ if (V <=0 or V > 5):
+ raise PDFEncryptionError('Unknown algorithm: %r' % V)
+ R = int_value(param['R']) # Revision
+ if R >= 7:
+ raise PDFEncryptionError('Unknown revision: %r' % R)
+
+ # check owner pass:
+ retval = self.check_owner_password(password, docid, param)
+ if retval is True or (retval is not False and retval is not None):
+ #print("Owner pass is valid")
+ if retval is True:
+ self.decrypt_key = self.recover_encryption_key_with_password(password, docid, param)
+ else:
+ self.decrypt_key = retval
+
+ if self.decrypt_key is None or self.decrypt_key is True or self.decrypt_key is False:
+ # That's not the owner password. Check if it's the user password.
+ retval = self.check_user_password(password, docid, param)
+ if retval is True or (retval is not False and retval is not None):
+ #print("User pass is valid")
+ if retval is True:
+ self.decrypt_key = self.recover_encryption_key_with_password(password, docid, param)
+ else:
+ self.decrypt_key = retval
+
+ if self.decrypt_key is None or self.decrypt_key is True or self.decrypt_key is False:
+ raise ADEPTInvalidPasswordError("Password invalid.")
+
+
+ P = int_value(param['P'])
+
+ 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)
+
+
# genkey method
- if V == 1 or V == 2:
+ if V == 1 or V == 2 or V == 4:
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
+ elif V >= 5:
+ self.genkey = self.genkey_v5
+
+ set_decipher = False
+
+ if V >= 4:
+ # Check if we need new genkey_v4 - only if we're using AES.
+ try:
+ for key in param['CF']:
+ algo = str(param["CF"][key]["CFM"])
+ if algo == "/AESV2":
+ if V == 4:
+ self.genkey = self.genkey_v4
+ set_decipher = True
+ self.decipher = self.decrypt_aes
+ elif algo == "/AESV3":
+ if V == 4:
+ self.genkey = self.genkey_v4
+ set_decipher = True
+ self.decipher = self.decrypt_aes
+ elif algo == "/V2":
+ set_decipher = True
+ self.decipher = self.decrypt_rc4
+ except:
+ pass
+
# rc4
- if V != 4:
- self.decipher = self.decipher_rc4 # XXX may be AES
+ if V < 4:
+ self.decipher = self.decrypt_rc4 # XXX may be AES
# aes
- elif V == 4 and length == 128:
- self.decipher = self.decipher_aes
- elif V == 4 and length == 256:
- raise PDFNotImplementedError('AES256 encryption is currently unsupported')
+ if not set_decipher:
+ # This should usually already be set by now.
+ # If it's not, assume that V4 and newer are using AES
+ if V >= 4:
+ self.decipher = self.decrypt_aes
self.ready = True
return
- def initialize_ebx(self, password, docid, param):
+
+ def initialize_ebx_ignoble(self, keyb64, docid, param):
self.is_printable = self.is_modifiable = self.is_extractable = True
- rsa = RSA(password)
+
+ try:
+ key = keyb64.decode('base64')[:16]
+ # This will probably always error, but I'm not 100% sure, so lets leave the old code in.
+ except AttributeError:
+ key = codecs.decode(keyb64.encode("ascii"), 'base64')[:16]
+
+
+ length = int_value(param.get('Length', 0)) / 8
+ rights = codecs.decode(str_value(param.get('ADEPT_LICENSE')), "base64")
+ rights = zlib.decompress(rights, -15)
+ rights = etree.fromstring(rights)
+ expr = './/{http://ns.adobe.com/adept}encryptedKey'
+ bookkey = ''.join(rights.findtext(expr))
+ bookkey = base64.b64decode(bookkey)
+ bookkey = AES.new(key, AES.MODE_CBC, b'\x00'*16).decrypt(bookkey)
+ bookkey = unpad(bookkey, 16) # PKCS#7
+ if len(bookkey) > 16:
+ bookkey = bookkey[-16:]
+ ebx_V = int_value(param.get('V', 4))
+ ebx_type = int_value(param.get('EBX_ENCRYPTIONTYPE', 6))
+ # added because of improper booktype / decryption book session key errors
+ if length > 0:
+ if len(bookkey) == length:
+ if ebx_V == 3:
+ V = 3
+ else:
+ V = 2
+ elif len(bookkey) == length + 1:
+ V = bookkey[0]
+ bookkey = bookkey[1:]
+ else:
+ print("ebx_V is %d and ebx_type is %d" % (ebx_V, ebx_type))
+ print("length is %d and len(bookkey) is %d" % (length, len(bookkey)))
+ print("bookkey[0] is %d" % bookkey[0])
+ raise ADEPTError('error decrypting book session key - mismatched length')
+ else:
+ # proper length unknown try with whatever you have
+ print("ebx_V is %d and ebx_type is %d" % (ebx_V, ebx_type))
+ print("length is %d and len(bookkey) is %d" % (length, len(bookkey)))
+ print("bookkey[0] is %d" % ord(bookkey[0]))
+ if ebx_V == 3:
+ V = 3
+ else:
+ V = 2
+ 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
+
+ @staticmethod
+ def removeHardening(rights, keytype, keydata):
+ adept = lambda tag: '{%s}%s' % ('http://ns.adobe.com/adept', tag)
+ textGetter = lambda name: ''.join(rights.findtext('.//%s' % (adept(name),)))
+
+ # Gather what we need, and generate the IV
+ resourceuuid = UUID(textGetter("resource"))
+ deviceuuid = UUID(textGetter("device"))
+ fullfillmentuuid = UUID(textGetter("fulfillment")[:36])
+ kekiv = UUID(int=resourceuuid.int ^ deviceuuid.int ^ fullfillmentuuid.int).bytes
+
+ # Derive kek from just "keytype"
+ rem = int(keytype, 10) % 16
+ H = SHA256(keytype.encode("ascii"))
+ kek = H[2*rem : 16 + rem] + H[rem : 2*rem]
+
+ return unpad(AES.new(kek, AES.MODE_CBC, kekiv).decrypt(keydata), 16)
+
+ def initialize_ebx_inept(self, password, docid, param):
+ self.is_printable = self.is_modifiable = self.is_extractable = True
+ rsakey = RSA.importKey(password) # parses the ASN1 structure
length = int_value(param.get('Length', 0)) // 8
rights = codecs.decode(param.get('ADEPT_LICENSE'), 'base64')
rights = zlib.decompress(rights, -15)
rights = etree.fromstring(rights)
expr = './/{http://ns.adobe.com/adept}encryptedKey'
- bookkey = codecs.decode(''.join(rights.findtext(expr)).encode('utf-8'),'base64')
- bookkey = rsa.decrypt(bookkey)
- #if bookkey[0] != 2:
- # raise ADEPTError('error decrypting book session key')
- if len(bookkey) > 16:
- if bookkey[-17] == '\x00' or bookkey[-17] == 0:
- bookkey = bookkey[-16:]
- length = 16
+ bookkeyelem = rights.find(expr)
+ bookkey = codecs.decode(bookkeyelem.text.encode('utf-8'),'base64')
+ keytype = bookkeyelem.attrib.get('keyType', '0')
+
+ if int(keytype, 10) > 2:
+ bookkey = PDFDocument.removeHardening(rights, keytype, bookkey)
+ try:
+ bookkey = PKCS1_v1_5.new(rsakey).decrypt(bookkey, None) # automatically unpads
+ except ValueError:
+ bookkey = None
+
+ if bookkey is None:
+ raise ADEPTError('error decrypting book session key')
+
ebx_V = int_value(param.get('V', 4))
ebx_type = int_value(param.get('EBX_ENCRYPTIONTYPE', 6))
# added because of improper booktype / decryption book session key errors
@@ -1648,7 +1738,7 @@ class PDFDocument(object):
objid = struct.pack(' 1
self.version = inf.read(8)
inf.seek(0)
self.doc = doc = PDFDocument()
parser = PDFParser(doc, inf)
- doc.initialize(userkey)
+ doc.initialize(userkey, inept)
self.objids = objids = set()
for xref in reversed(doc.xrefs):
trailer = xref.trailer
@@ -2098,9 +2216,9 @@ class PDFSerializer(object):
def escape_string(self, string):
string = string.replace(b'\\', b'\\\\')
- string = string.replace(b'\n', rb'\n')
- string = string.replace(b'(', rb'\(')
- string = string.replace(b')', rb'\)')
+ string = string.replace(b'\n', b'\\n')
+ string = string.replace(b'(', b'\\(')
+ string = string.replace(b')', b'\\)')
return string
def serialize_object(self, obj):
@@ -2124,7 +2242,7 @@ class PDFSerializer(object):
elif isinstance(obj, bytearray):
self.write(b'(%s)' % self.escape_string(obj))
elif isinstance(obj, bytes):
- self.write(b'(%s)' % self.escape_string(obj))
+ self.write(b'<%s>' % binascii.hexlify(obj).upper())
elif isinstance(obj, str):
self.write(b'(%s)' % self.escape_string(obj.encode('utf-8')))
elif isinstance(obj, bool):
@@ -2148,9 +2266,23 @@ class PDFSerializer(object):
### 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)')
+ self.write(b'(deleted)')
else:
data = obj.get_decdata()
+
+ # Fix length:
+ # We've decompressed and then recompressed the PDF stream.
+ # Depending on the algorithm, the implementation, and the compression level,
+ # the resulting recompressed stream is unlikely to have the same length as the original.
+ # So we need to update the PDF object to contain the new proper length.
+
+ # Without this change, all PDFs exported by this plugin are slightly corrupted -
+ # even though most if not all PDF readers can correct that on-the-fly.
+
+ if 'Length' in obj.dic:
+ obj.dic['Length'] = len(data)
+
+
self.serialize_object(obj.dic)
self.write(b'stream\n')
self.write(data)
@@ -2171,25 +2303,33 @@ class PDFSerializer(object):
-def decryptBook(userkey, inpath, outpath):
- if RSA is None:
- raise ADEPTError("PyCryptodome or OpenSSL must be installed.")
+def decryptBook(userkey, inpath, outpath, inept=True):
with open(inpath, 'rb') as inf:
- serializer = PDFSerializer(inf, userkey)
+ serializer = PDFSerializer(inf, userkey, inept)
with open(outpath, 'wb') as outf:
# help construct to make sure the method runs to the end
try:
serializer.dump(outf)
except Exception as e:
print("error writing pdf: {0}".format(e))
+ traceback.print_exc()
return 2
return 0
+def getPDFencryptionType(inpath):
+ with open(inpath, 'rb') as inf:
+ doc = doc = PDFDocument()
+ parser = PDFParser(doc, inf)
+ filter = doc.initialize_and_return_filter()
+ return filter
+
+
+
def cli_main():
sys.stdout=SafeUnbuffered(sys.stdout)
sys.stderr=SafeUnbuffered(sys.stderr)
- argv=unicode_argv()
+ argv=unicode_argv("ineptpdf.py")
progname = os.path.basename(argv[0])
if len(argv) != 4:
print("usage: {0} ".format(progname))
@@ -2310,13 +2450,6 @@ def gui_main():
root = tkinter.Tk()
- if RSA is None:
- root.withdraw()
- tkinter.messagebox.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("Adobe Adept PDF Decrypter v.{0}".format(__version__))
root.resizable(True, False)
root.minsize(370, 0)
diff --git a/DeDRM_plugin/ion.py b/DeDRM_plugin/ion.py
index f102ec5..45e9610 100644
--- a/DeDRM_plugin/ion.py
+++ b/DeDRM_plugin/ion.py
@@ -30,8 +30,12 @@ import struct
from io import BytesIO
-from Crypto.Cipher import AES
-from Crypto.Util.py3compat import bchr
+try:
+ from Cryptodome.Cipher import AES
+ from Cryptodome.Util.py3compat import bchr
+except ImportError:
+ from Crypto.Cipher import AES
+ from Crypto.Util.py3compat import bchr
try:
# lzma library from calibre 4.6.0 or later
@@ -761,6 +765,9 @@ def pkcs7unpad(msg, blocklen):
return msg[:-paddinglen]
+
+
+
# every VoucherEnvelope version has a corresponding "word" and magic number, used in obfuscating the shared secret
OBFUSCATION_TABLE = {
"V1": (0x00, None),
diff --git a/DeDRM_plugin/k4mobidedrm.py b/DeDRM_plugin/k4mobidedrm.py
index cede191..c3200b6 100644
--- a/DeDRM_plugin/k4mobidedrm.py
+++ b/DeDRM_plugin/k4mobidedrm.py
@@ -69,86 +69,29 @@ import getopt
import re
import traceback
import time
-import html.entities
+try:
+ import html.entities as htmlentitydefs
+except:
+ import htmlentitydefs
+
import json
+#@@CALIBRE_COMPAT_CODE@@
+
+
class DrmException(Exception):
pass
-if 'calibre' in sys.modules:
- inCalibre = True
-else:
- inCalibre = False
-
-if inCalibre:
- from calibre_plugins.dedrm import mobidedrm
- from calibre_plugins.dedrm import topazextract
- from calibre_plugins.dedrm import kgenpids
- from calibre_plugins.dedrm import androidkindlekey
- from calibre_plugins.dedrm import kfxdedrm
-else:
- import mobidedrm
- import topazextract
- import kgenpids
- import androidkindlekey
- import kfxdedrm
-
-# Wrap a stream so that output gets flushed immediately
-# and also make sure that any unicode strings get
-# encoded using "replace" before writing them.
-class SafeUnbuffered:
- def __init__(self, stream):
- self.stream = stream
- self.encoding = stream.encoding
- if self.encoding == None:
- self.encoding = "utf-8"
- def write(self, data):
- if isinstance(data, str):
- data = data.encode(self.encoding,"replace")
- self.stream.buffer.write(data)
- self.stream.buffer.flush()
-
- def __getattr__(self, attr):
- return getattr(self.stream, attr)
-
-iswindows = sys.platform.startswith('win')
-isosx = sys.platform.startswith('darwin')
-
-def unicode_argv():
- if iswindows:
- # Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
- # strings.
-
- # Versions 2.x of Python don't support Unicode in sys.argv on
- # Windows, with the underlying Windows API instead replacing multi-byte
- # characters with '?'.
-
-
- from ctypes import POINTER, byref, cdll, c_int, windll
- from ctypes.wintypes import LPCWSTR, LPWSTR
-
- GetCommandLineW = cdll.kernel32.GetCommandLineW
- GetCommandLineW.argtypes = []
- GetCommandLineW.restype = LPCWSTR
-
- CommandLineToArgvW = windll.shell32.CommandLineToArgvW
- CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
- CommandLineToArgvW.restype = POINTER(LPWSTR)
-
- cmd = GetCommandLineW()
- argc = c_int(0)
- argv = CommandLineToArgvW(cmd, byref(argc))
- if argc.value > 0:
- # Remove Python executable and commands if present
- start = argc.value - len(sys.argv)
- return [argv[i] for i in
- range(start, argc.value)]
- # if we don't have any arguments at all, just pass back script name
- # this should never happen
- return ["mobidedrm.py"]
- else:
- argvencoding = sys.stdin.encoding or "utf-8"
- return [arg if isinstance(arg, str) else str(arg, argvencoding) for arg in sys.argv]
+import mobidedrm
+import topazextract
+import kgenpids
+import androidkindlekey
+import kfxdedrm
+
+from utilities import SafeUnbuffered
+
+from argv_utils import unicode_argv
+
# cleanup unicode filenames
# borrowed from calibre from calibre/src/calibre/__init__.py
@@ -191,7 +134,7 @@ def unescape(text):
else:
# named entity
try:
- text = chr(html.entities.name2codepoint[text[1:-1]])
+ text = chr(htmlentitydefs.name2codepoint[text[1:-1]])
except KeyError:
pass
return text # leave as is
@@ -218,8 +161,11 @@ def GetDecryptedBook(infile, kDatabases, androidFiles, serials, pids, starttime
else:
mb = topazextract.TopazBook(infile)
- bookname = unescape(mb.getBookTitle())
- print("Decrypting {1} ebook: {0}".format(bookname, mb.getBookType()))
+ try:
+ bookname = unescape(mb.getBookTitle())
+ print("Decrypting {1} ebook: {0}".format(bookname, mb.getBookType()))
+ except:
+ print("Decrypting {0} ebook.".format(mb.getBookType()))
# copy list of pids
totalpids = list(pids)
@@ -237,7 +183,7 @@ def GetDecryptedBook(infile, kDatabases, androidFiles, serials, pids, starttime
try:
mb.processBook(totalpids)
except:
- mb.cleanup
+ mb.cleanup()
raise
print("Decryption succeeded after {0:.1f} seconds".format(time.time()-starttime))
@@ -271,7 +217,7 @@ def decryptBook(infile, outdir, kDatabaseFiles, androidFiles, serials, pids):
orig_fn_root = os.path.splitext(os.path.basename(infile))[0]
if (
re.match('^B[A-Z0-9]{9}(_EBOK|_EBSP|_sample)?$', orig_fn_root) or
- re.match('^{0-9A-F-}{36}$', orig_fn_root)
+ re.match('^[0-9A-F-]{36}$', orig_fn_root)
): # Kindle for PC / Mac / Android / Fire / iOS
clean_title = cleanup_name(book.getBookTitle())
outfilename = "{}_{}".format(orig_fn_root, clean_title)
@@ -307,7 +253,7 @@ def usage(progname):
# Main
#
def cli_main():
- argv=unicode_argv()
+ argv=unicode_argv("k4mobidedrm.py")
progname = os.path.basename(argv[0])
print("K4MobiDeDrm v{0}.\nCopyright © 2008-2020 Apprentice Harper et al.".format(__version__))
diff --git a/DeDRM_plugin/kfxdedrm.py b/DeDRM_plugin/kfxdedrm.py
index 67b10f8..cec764c 100644
--- a/DeDRM_plugin/kfxdedrm.py
+++ b/DeDRM_plugin/kfxdedrm.py
@@ -8,16 +8,19 @@
# 2.1.1 - Whitespace!
-import os
+import os, sys
import shutil
import traceback
import zipfile
from io import BytesIO
-try:
- from ion import DrmIon, DrmIonVoucher
-except:
- from calibre_plugins.dedrm.ion import DrmIon, DrmIonVoucher
+
+
+#@@CALIBRE_COMPAT_CODE@@
+
+
+from ion import DrmIon, DrmIonVoucher
+
__license__ = 'GPL v3'
@@ -92,8 +95,10 @@ class KFXZipBook:
license_type = voucher.getlicensetype()
if license_type != "Purchase":
- raise Exception(("This book is licensed as {0}. "
- 'These tools are intended for use on purchased books.').format(license_type))
+ #raise Exception(("This book is licensed as {0}. "
+ # 'These tools are intended for use on purchased books.').format(license_type))
+ print("Warning: This book is licensed as {0}. "
+ "These tools are intended for use on purchased books. Continuing ...".format(license_type))
self.voucher = voucher
diff --git a/DeDRM_plugin/kindlekey.py b/DeDRM_plugin/kindlekey.py
index 35baa46..60a6065 100644
--- a/DeDRM_plugin/kindlekey.py
+++ b/DeDRM_plugin/kindlekey.py
@@ -2,10 +2,10 @@
# -*- coding: utf-8 -*-
# kindlekey.py
-# Copyright © 2008-2020 Apprentice Harper et al.
+# Copyright © 2008-2022 Apprentice Harper et al.
__license__ = 'GPL v3'
-__version__ = '3.0'
+__version__ = '3.1'
# Revision history:
# 1.0 - Kindle info file decryption, extracted from k4mobidedrm, etc.
@@ -30,6 +30,7 @@ __version__ = '3.0'
# 2.7 - Finish .kinf2018 support, PC & Mac by Apprentice Sakuya
# 2.8 - Fix for Mac OS X Big Sur
# 3.0 - Python 3 for calibre 5.0
+# 3.1 - Only support PyCryptodome; clean up the code
"""
@@ -42,6 +43,16 @@ from struct import pack, unpack, unpack_from
import json
import getopt
import traceback
+import hashlib
+
+try:
+ from Cryptodome.Cipher import AES
+ from Cryptodome.Util import Counter
+ from Cryptodome.Protocol.KDF import PBKDF2
+except ImportError:
+ from Crypto.Cipher import AES
+ from Crypto.Util import Counter
+ from Crypto.Protocol.KDF import PBKDF2
try:
RegError
@@ -51,23 +62,7 @@ except NameError:
# Routines common to Mac and PC
-# Wrap a stream so that output gets flushed immediately
-# and also make sure that any unicode strings get
-# encoded using "replace" before writing them.
-class SafeUnbuffered:
- def __init__(self, stream):
- self.stream = stream
- self.encoding = stream.encoding
- if self.encoding == None:
- self.encoding = "utf-8"
- def write(self, data):
- if isinstance(data, str):
- data = data.encode(self.encoding,"replace")
- self.stream.buffer.write(data)
- self.stream.buffer.flush()
-
- def __getattr__(self, attr):
- return getattr(self.stream, attr)
+from utilities import SafeUnbuffered
try:
from calibre.constants import iswindows, isosx
@@ -75,62 +70,22 @@ except:
iswindows = sys.platform.startswith('win')
isosx = sys.platform.startswith('darwin')
-def unicode_argv():
- if iswindows:
- # Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
- # strings.
-
- # Versions 2.x of Python don't support Unicode in sys.argv on
- # Windows, with the underlying Windows API instead replacing multi-byte
- # characters with '?'. So use shell32.GetCommandLineArgvW to get sys.argv
- # as a list of Unicode strings and encode them as utf-8
-
- from ctypes import POINTER, byref, cdll, c_int, windll
- from ctypes.wintypes import LPCWSTR, LPWSTR
-
- GetCommandLineW = cdll.kernel32.GetCommandLineW
- GetCommandLineW.argtypes = []
- GetCommandLineW.restype = LPCWSTR
-
- CommandLineToArgvW = windll.shell32.CommandLineToArgvW
- CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
- CommandLineToArgvW.restype = POINTER(LPWSTR)
-
- cmd = GetCommandLineW()
- argc = c_int(0)
- argv = CommandLineToArgvW(cmd, byref(argc))
- if argc.value > 0:
- # Remove Python executable and commands if present
- start = argc.value - len(sys.argv)
- return [argv[i] for i in
- range(start, argc.value)]
- # if we don't have any arguments at all, just pass back script name
- # this should never happen
- return ["kindlekey.py"]
- else:
- argvencoding = sys.stdin.encoding or "utf-8"
- return [arg if isinstance(arg, str) else str(arg, argvencoding) for arg in sys.argv]
+from argv_utils import unicode_argv
class DrmException(Exception):
pass
# crypto digestroutines
-import hashlib
def MD5(message):
- ctx = hashlib.md5()
- ctx.update(message)
- return ctx.digest()
+ return hashlib.md5(message).digest()
def SHA1(message):
- ctx = hashlib.sha1()
- ctx.update(message)
- return ctx.digest()
+ return hashlib.sha1(message).digest()
def SHA256(message):
- ctx = hashlib.sha256()
- ctx.update(message)
- return ctx.digest()
+ return hashlib.sha256(message).digest()
+
# For K4M/PC 1.6.X and later
def primes(n):
@@ -183,648 +138,28 @@ def decode(data,map):
result += pack('B',value)
return result
+def UnprotectHeaderData(encryptedData):
+ passwdData = b'header_key_data'
+ salt = b'HEADER.2011'
+ key_iv = PBKDF2(passwdData, salt, dkLen=256, count=128)
+ return AES.new(key_iv[0:32], AES.MODE_CBC, key_iv[32:48]).decrypt(encryptedData)
+
# Routines unique to Mac and PC
if iswindows:
from ctypes import windll, c_char_p, c_wchar_p, c_uint, POINTER, byref, \
create_unicode_buffer, create_string_buffer, CFUNCTYPE, addressof, \
string_at, Structure, c_void_p, cast
- import winreg
+ try:
+ import winreg
+ except ImportError:
+ import _winreg as winreg
+
MAX_PATH = 255
kernel32 = windll.kernel32
advapi32 = windll.advapi32
crypt32 = windll.crypt32
- try:
- # try to get fast routines from alfcrypto
- from alfcrypto import AES_CBC, KeyIVGen
- except:
- # alfcrypto not available, so use python implementations
- """
- Routines for doing AES CBC in one file
-
- Modified by some_updates to extract
- and combine only those parts needed for AES CBC
- into one simple to add python file
-
- Original Version
- Copyright (c) 2002 by Paul A. Lambert
- Under:
- CryptoPy Artistic License Version 1.0
- See the wonderful pure python package cryptopy-1.2.5
- and read its LICENSE.txt for complete license details.
- """
-
- class CryptoError(Exception):
- """ Base class for crypto exceptions """
- def __init__(self,errorMessage='Error!'):
- self.message = errorMessage
- def __str__(self):
- return self.message
-
- class InitCryptoError(CryptoError):
- """ Crypto errors during algorithm initialization """
- class BadKeySizeError(InitCryptoError):
- """ Bad key size error """
- class EncryptError(CryptoError):
- """ Error in encryption processing """
- class DecryptError(CryptoError):
- """ Error in decryption processing """
- class DecryptNotBlockAlignedError(DecryptError):
- """ Error in decryption processing """
-
- def xor(a,b):
- """ XOR two byte arrays, to lesser length """
- x = []
- for i in range(min(len(a),len(b))):
- x.append( a[i] ^ b[i])
- return bytes(x)
-
- """
- Base 'BlockCipher' and Pad classes for cipher instances.
- BlockCipher supports automatic padding and type conversion. The BlockCipher
- class was written to make the actual algorithm code more readable and
- not for performance.
- """
-
- class BlockCipher:
- """ Block ciphers """
- def __init__(self):
- self.reset()
-
- def reset(self):
- self.resetEncrypt()
- self.resetDecrypt()
- def resetEncrypt(self):
- self.encryptBlockCount = 0
- self.bytesToEncrypt = b''
- def resetDecrypt(self):
- self.decryptBlockCount = 0
- self.bytesToDecrypt = b''
-
- def encrypt(self, plainText, more = None):
- """ Encrypt a string and return a binary string """
- self.bytesToEncrypt += plainText # append plainText to any bytes from prior encrypt
- numBlocks, numExtraBytes = divmod(len(self.bytesToEncrypt), self.blockSize)
- cipherText = ''
- for i in range(numBlocks):
- bStart = i*self.blockSize
- ctBlock = self.encryptBlock(self.bytesToEncrypt[bStart:bStart+self.blockSize])
- self.encryptBlockCount += 1
- cipherText += ctBlock
- if numExtraBytes > 0: # save any bytes that are not block aligned
- self.bytesToEncrypt = self.bytesToEncrypt[-numExtraBytes:]
- else:
- self.bytesToEncrypt = ''
-
- if more == None: # no more data expected from caller
- finalBytes = self.padding.addPad(self.bytesToEncrypt,self.blockSize)
- if len(finalBytes) > 0:
- ctBlock = self.encryptBlock(finalBytes)
- self.encryptBlockCount += 1
- cipherText += ctBlock
- self.resetEncrypt()
- return cipherText
-
- def decrypt(self, cipherText, more = None):
- """ Decrypt a string and return a string """
- self.bytesToDecrypt += cipherText # append to any bytes from prior decrypt
-
- numBlocks, numExtraBytes = divmod(len(self.bytesToDecrypt), self.blockSize)
- if more == None: # no more calls to decrypt, should have all the data
- if numExtraBytes != 0:
- raise DecryptNotBlockAlignedError('Data not block aligned on decrypt')
-
- # hold back some bytes in case last decrypt has zero len
- if (more != None) and (numExtraBytes == 0) and (numBlocks >0) :
- numBlocks -= 1
- numExtraBytes = self.blockSize
-
- plainText = b''
- for i in range(numBlocks):
- bStart = i*self.blockSize
- ptBlock = self.decryptBlock(self.bytesToDecrypt[bStart : bStart+self.blockSize])
- self.decryptBlockCount += 1
- plainText += ptBlock
-
- if numExtraBytes > 0: # save any bytes that are not block aligned
- self.bytesToEncrypt = self.bytesToEncrypt[-numExtraBytes:]
- else:
- self.bytesToEncrypt = ''
-
- if more == None: # last decrypt remove padding
- plainText = self.padding.removePad(plainText, self.blockSize)
- self.resetDecrypt()
- return plainText
-
-
- class Pad:
- def __init__(self):
- pass # eventually could put in calculation of min and max size extension
-
- class padWithPadLen(Pad):
- """ Pad a binary string with the length of the padding """
-
- def addPad(self, extraBytes, blockSize):
- """ Add padding to a binary string to make it an even multiple
- of the block size """
- blocks, numExtraBytes = divmod(len(extraBytes), blockSize)
- padLength = blockSize - numExtraBytes
- return extraBytes + padLength*chr(padLength)
-
- def removePad(self, paddedBinaryString, blockSize):
- """ Remove padding from a binary string """
- if not(0 6 and i%Nk == 4 :
- temp = [ Sbox[byte] for byte in temp ] # SubWord(temp)
- w.append( [ w[i-Nk][byte]^temp[byte] for byte in range(4) ] )
- return w
-
- Rcon = (0,0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80,0x1b,0x36, # note extra '0' !!!
- 0x6c,0xd8,0xab,0x4d,0x9a,0x2f,0x5e,0xbc,0x63,0xc6,
- 0x97,0x35,0x6a,0xd4,0xb3,0x7d,0xfa,0xef,0xc5,0x91)
-
- #-------------------------------------
- def AddRoundKey(algInstance, keyBlock):
- """ XOR the algorithm state with a block of key material """
- for column in range(algInstance.Nb):
- for row in range(4):
- algInstance.state[column][row] ^= keyBlock[column][row]
- #-------------------------------------
-
- def SubBytes(algInstance):
- for column in range(algInstance.Nb):
- for row in range(4):
- algInstance.state[column][row] = Sbox[algInstance.state[column][row]]
-
- def InvSubBytes(algInstance):
- for column in range(algInstance.Nb):
- for row in range(4):
- algInstance.state[column][row] = InvSbox[algInstance.state[column][row]]
-
- Sbox = (0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5,
- 0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76,
- 0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0,
- 0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0,
- 0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc,
- 0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15,
- 0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a,
- 0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75,
- 0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0,
- 0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84,
- 0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b,
- 0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf,
- 0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85,
- 0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8,
- 0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5,
- 0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2,
- 0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17,
- 0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73,
- 0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88,
- 0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb,
- 0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c,
- 0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79,
- 0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9,
- 0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08,
- 0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6,
- 0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a,
- 0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e,
- 0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e,
- 0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94,
- 0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf,
- 0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68,
- 0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16)
-
- InvSbox = (0x52,0x09,0x6a,0xd5,0x30,0x36,0xa5,0x38,
- 0xbf,0x40,0xa3,0x9e,0x81,0xf3,0xd7,0xfb,
- 0x7c,0xe3,0x39,0x82,0x9b,0x2f,0xff,0x87,
- 0x34,0x8e,0x43,0x44,0xc4,0xde,0xe9,0xcb,
- 0x54,0x7b,0x94,0x32,0xa6,0xc2,0x23,0x3d,
- 0xee,0x4c,0x95,0x0b,0x42,0xfa,0xc3,0x4e,
- 0x08,0x2e,0xa1,0x66,0x28,0xd9,0x24,0xb2,
- 0x76,0x5b,0xa2,0x49,0x6d,0x8b,0xd1,0x25,
- 0x72,0xf8,0xf6,0x64,0x86,0x68,0x98,0x16,
- 0xd4,0xa4,0x5c,0xcc,0x5d,0x65,0xb6,0x92,
- 0x6c,0x70,0x48,0x50,0xfd,0xed,0xb9,0xda,
- 0x5e,0x15,0x46,0x57,0xa7,0x8d,0x9d,0x84,
- 0x90,0xd8,0xab,0x00,0x8c,0xbc,0xd3,0x0a,
- 0xf7,0xe4,0x58,0x05,0xb8,0xb3,0x45,0x06,
- 0xd0,0x2c,0x1e,0x8f,0xca,0x3f,0x0f,0x02,
- 0xc1,0xaf,0xbd,0x03,0x01,0x13,0x8a,0x6b,
- 0x3a,0x91,0x11,0x41,0x4f,0x67,0xdc,0xea,
- 0x97,0xf2,0xcf,0xce,0xf0,0xb4,0xe6,0x73,
- 0x96,0xac,0x74,0x22,0xe7,0xad,0x35,0x85,
- 0xe2,0xf9,0x37,0xe8,0x1c,0x75,0xdf,0x6e,
- 0x47,0xf1,0x1a,0x71,0x1d,0x29,0xc5,0x89,
- 0x6f,0xb7,0x62,0x0e,0xaa,0x18,0xbe,0x1b,
- 0xfc,0x56,0x3e,0x4b,0xc6,0xd2,0x79,0x20,
- 0x9a,0xdb,0xc0,0xfe,0x78,0xcd,0x5a,0xf4,
- 0x1f,0xdd,0xa8,0x33,0x88,0x07,0xc7,0x31,
- 0xb1,0x12,0x10,0x59,0x27,0x80,0xec,0x5f,
- 0x60,0x51,0x7f,0xa9,0x19,0xb5,0x4a,0x0d,
- 0x2d,0xe5,0x7a,0x9f,0x93,0xc9,0x9c,0xef,
- 0xa0,0xe0,0x3b,0x4d,0xae,0x2a,0xf5,0xb0,
- 0xc8,0xeb,0xbb,0x3c,0x83,0x53,0x99,0x61,
- 0x17,0x2b,0x04,0x7e,0xba,0x77,0xd6,0x26,
- 0xe1,0x69,0x14,0x63,0x55,0x21,0x0c,0x7d)
-
- #-------------------------------------
- """ For each block size (Nb), the ShiftRow operation shifts row i
- by the amount Ci. Note that row 0 is not shifted.
- Nb C1 C2 C3
- ------------------- """
- shiftOffset = { 4 : ( 0, 1, 2, 3),
- 5 : ( 0, 1, 2, 3),
- 6 : ( 0, 1, 2, 3),
- 7 : ( 0, 1, 2, 4),
- 8 : ( 0, 1, 3, 4) }
- def ShiftRows(algInstance):
- tmp = [0]*algInstance.Nb # list of size Nb
- for r in range(1,4): # row 0 reamains unchanged and can be skipped
- for c in range(algInstance.Nb):
- tmp[c] = algInstance.state[(c+shiftOffset[algInstance.Nb][r]) % algInstance.Nb][r]
- for c in range(algInstance.Nb):
- algInstance.state[c][r] = tmp[c]
- def InvShiftRows(algInstance):
- tmp = [0]*algInstance.Nb # list of size Nb
- for r in range(1,4): # row 0 reamains unchanged and can be skipped
- for c in range(algInstance.Nb):
- tmp[c] = algInstance.state[(c+algInstance.Nb-shiftOffset[algInstance.Nb][r]) % algInstance.Nb][r]
- for c in range(algInstance.Nb):
- algInstance.state[c][r] = tmp[c]
- #-------------------------------------
- def MixColumns(a):
- Sprime = [0,0,0,0]
- for j in range(a.Nb): # for each column
- Sprime[0] = mul(2,a.state[j][0])^mul(3,a.state[j][1])^mul(1,a.state[j][2])^mul(1,a.state[j][3])
- Sprime[1] = mul(1,a.state[j][0])^mul(2,a.state[j][1])^mul(3,a.state[j][2])^mul(1,a.state[j][3])
- Sprime[2] = mul(1,a.state[j][0])^mul(1,a.state[j][1])^mul(2,a.state[j][2])^mul(3,a.state[j][3])
- Sprime[3] = mul(3,a.state[j][0])^mul(1,a.state[j][1])^mul(1,a.state[j][2])^mul(2,a.state[j][3])
- for i in range(4):
- a.state[j][i] = Sprime[i]
-
- def InvMixColumns(a):
- """ Mix the four bytes of every column in a linear way
- This is the opposite operation of Mixcolumn """
- Sprime = [0,0,0,0]
- for j in range(a.Nb): # for each column
- Sprime[0] = mul(0x0E,a.state[j][0])^mul(0x0B,a.state[j][1])^mul(0x0D,a.state[j][2])^mul(0x09,a.state[j][3])
- Sprime[1] = mul(0x09,a.state[j][0])^mul(0x0E,a.state[j][1])^mul(0x0B,a.state[j][2])^mul(0x0D,a.state[j][3])
- Sprime[2] = mul(0x0D,a.state[j][0])^mul(0x09,a.state[j][1])^mul(0x0E,a.state[j][2])^mul(0x0B,a.state[j][3])
- Sprime[3] = mul(0x0B,a.state[j][0])^mul(0x0D,a.state[j][1])^mul(0x09,a.state[j][2])^mul(0x0E,a.state[j][3])
- for i in range(4):
- a.state[j][i] = Sprime[i]
-
- #-------------------------------------
- def mul(a, b):
- """ Multiply two elements of GF(2^m)
- needed for MixColumn and InvMixColumn """
- if (a !=0 and b!=0):
- return Alogtable[(Logtable[a] + Logtable[b])%255]
- else:
- return 0
-
- Logtable = ( 0, 0, 25, 1, 50, 2, 26, 198, 75, 199, 27, 104, 51, 238, 223, 3,
- 100, 4, 224, 14, 52, 141, 129, 239, 76, 113, 8, 200, 248, 105, 28, 193,
- 125, 194, 29, 181, 249, 185, 39, 106, 77, 228, 166, 114, 154, 201, 9, 120,
- 101, 47, 138, 5, 33, 15, 225, 36, 18, 240, 130, 69, 53, 147, 218, 142,
- 150, 143, 219, 189, 54, 208, 206, 148, 19, 92, 210, 241, 64, 70, 131, 56,
- 102, 221, 253, 48, 191, 6, 139, 98, 179, 37, 226, 152, 34, 136, 145, 16,
- 126, 110, 72, 195, 163, 182, 30, 66, 58, 107, 40, 84, 250, 133, 61, 186,
- 43, 121, 10, 21, 155, 159, 94, 202, 78, 212, 172, 229, 243, 115, 167, 87,
- 175, 88, 168, 80, 244, 234, 214, 116, 79, 174, 233, 213, 231, 230, 173, 232,
- 44, 215, 117, 122, 235, 22, 11, 245, 89, 203, 95, 176, 156, 169, 81, 160,
- 127, 12, 246, 111, 23, 196, 73, 236, 216, 67, 31, 45, 164, 118, 123, 183,
- 204, 187, 62, 90, 251, 96, 177, 134, 59, 82, 161, 108, 170, 85, 41, 157,
- 151, 178, 135, 144, 97, 190, 220, 252, 188, 149, 207, 205, 55, 63, 91, 209,
- 83, 57, 132, 60, 65, 162, 109, 71, 20, 42, 158, 93, 86, 242, 211, 171,
- 68, 17, 146, 217, 35, 32, 46, 137, 180, 124, 184, 38, 119, 153, 227, 165,
- 103, 74, 237, 222, 197, 49, 254, 24, 13, 99, 140, 128, 192, 247, 112, 7)
-
- Alogtable= ( 1, 3, 5, 15, 17, 51, 85, 255, 26, 46, 114, 150, 161, 248, 19, 53,
- 95, 225, 56, 72, 216, 115, 149, 164, 247, 2, 6, 10, 30, 34, 102, 170,
- 229, 52, 92, 228, 55, 89, 235, 38, 106, 190, 217, 112, 144, 171, 230, 49,
- 83, 245, 4, 12, 20, 60, 68, 204, 79, 209, 104, 184, 211, 110, 178, 205,
- 76, 212, 103, 169, 224, 59, 77, 215, 98, 166, 241, 8, 24, 40, 120, 136,
- 131, 158, 185, 208, 107, 189, 220, 127, 129, 152, 179, 206, 73, 219, 118, 154,
- 181, 196, 87, 249, 16, 48, 80, 240, 11, 29, 39, 105, 187, 214, 97, 163,
- 254, 25, 43, 125, 135, 146, 173, 236, 47, 113, 147, 174, 233, 32, 96, 160,
- 251, 22, 58, 78, 210, 109, 183, 194, 93, 231, 50, 86, 250, 21, 63, 65,
- 195, 94, 226, 61, 71, 201, 64, 192, 91, 237, 44, 116, 156, 191, 218, 117,
- 159, 186, 213, 100, 172, 239, 42, 126, 130, 157, 188, 223, 122, 142, 137, 128,
- 155, 182, 193, 88, 232, 35, 101, 175, 234, 37, 111, 177, 200, 67, 197, 84,
- 252, 31, 33, 99, 165, 244, 7, 9, 27, 45, 119, 153, 176, 203, 70, 202,
- 69, 207, 74, 222, 121, 139, 134, 145, 168, 227, 62, 66, 198, 81, 243, 14,
- 18, 54, 90, 238, 41, 123, 141, 140, 143, 138, 133, 148, 167, 242, 13, 23,
- 57, 75, 221, 124, 132, 151, 162, 253, 28, 36, 108, 180, 199, 82, 246, 1)
-
-
-
-
- """
- AES Encryption Algorithm
- The AES algorithm is just Rijndael algorithm restricted to the default
- blockSize of 128 bits.
- """
-
- class AES(Rijndael):
- """ The AES algorithm is the Rijndael block cipher restricted to block
- sizes of 128 bits and key sizes of 128, 192 or 256 bits
- """
- def __init__(self, key = None, padding = padWithPadLen(), keySize=16):
- """ Initialize AES, keySize is in bytes """
- if not (keySize == 16 or keySize == 24 or keySize == 32) :
- raise BadKeySizeError('Illegal AES key size, must be 16, 24, or 32 bytes')
-
- Rijndael.__init__( self, key, padding=padding, keySize=keySize, blockSize=16 )
-
- self.name = 'AES'
-
-
- """
- CBC mode of encryption for block ciphers.
- This algorithm mode wraps any BlockCipher to make a
- Cipher Block Chaining mode.
- """
- from random import Random # should change to crypto.random!!!
-
-
- class CBC(BlockCipher):
- """ The CBC class wraps block ciphers to make cipher block chaining (CBC) mode
- algorithms. The initialization (IV) is automatic if set to None. Padding
- is also automatic based on the Pad class used to initialize the algorithm
- """
- def __init__(self, blockCipherInstance, padding = padWithPadLen()):
- """ CBC algorithms are created by initializing with a BlockCipher instance """
- self.baseCipher = blockCipherInstance
- self.name = self.baseCipher.name + '_CBC'
- self.blockSize = self.baseCipher.blockSize
- self.keySize = self.baseCipher.keySize
- self.padding = padding
- self.baseCipher.padding = noPadding() # baseCipher should NOT pad!!
- self.r = Random() # for IV generation, currently uses
- # mediocre standard distro version <----------------
- import time
- newSeed = time.ctime()+str(self.r) # seed with instance location
- self.r.seed(newSeed) # to make unique
- self.reset()
-
- def setKey(self, key):
- self.baseCipher.setKey(key)
-
- # Overload to reset both CBC state and the wrapped baseCipher
- def resetEncrypt(self):
- BlockCipher.resetEncrypt(self) # reset CBC encrypt state (super class)
- self.baseCipher.resetEncrypt() # reset base cipher encrypt state
-
- def resetDecrypt(self):
- BlockCipher.resetDecrypt(self) # reset CBC state (super class)
- self.baseCipher.resetDecrypt() # reset base cipher decrypt state
-
- def encrypt(self, plainText, iv=None, more=None):
- """ CBC encryption - overloads baseCipher to allow optional explicit IV
- when iv=None, iv is auto generated!
- """
- if self.encryptBlockCount == 0:
- self.iv = iv
- else:
- assert(iv==None), 'IV used only on first call to encrypt'
-
- return BlockCipher.encrypt(self,plainText, more=more)
-
- def decrypt(self, cipherText, iv=None, more=None):
- """ CBC decryption - overloads baseCipher to allow optional explicit IV
- when iv=None, iv is auto generated!
- """
- if self.decryptBlockCount == 0:
- self.iv = iv
- else:
- assert(iv==None), 'IV used only on first call to decrypt'
-
- return BlockCipher.decrypt(self, cipherText, more=more)
-
- def encryptBlock(self, plainTextBlock):
- """ CBC block encryption, IV is set with 'encrypt' """
- auto_IV = ''
- if self.encryptBlockCount == 0:
- if self.iv == None:
- # generate IV and use
- self.iv = ''.join([chr(self.r.randrange(256)) for i in range(self.blockSize)])
- self.prior_encr_CT_block = self.iv
- auto_IV = self.prior_encr_CT_block # prepend IV if it's automatic
- else: # application provided IV
- assert(len(self.iv) == self.blockSize ),'IV must be same length as block'
- self.prior_encr_CT_block = self.iv
- """ encrypt the prior CT XORed with the PT """
- ct = self.baseCipher.encryptBlock( xor(self.prior_encr_CT_block, plainTextBlock) )
- self.prior_encr_CT_block = ct
- return auto_IV+ct
-
- def decryptBlock(self, encryptedBlock):
- """ Decrypt a single block """
-
- if self.decryptBlockCount == 0: # first call, process IV
- if self.iv == None: # auto decrypt IV?
- self.prior_CT_block = encryptedBlock
- return b''
- else:
- assert(len(self.iv)==self.blockSize),"Bad IV size on CBC decryption"
- self.prior_CT_block = self.iv
-
- dct = self.baseCipher.decryptBlock(encryptedBlock)
- """ XOR the prior decrypted CT with the prior CT """
- dct_XOR_priorCT = xor( self.prior_CT_block, dct )
-
- self.prior_CT_block = encryptedBlock
-
- return dct_XOR_priorCT
-
-
- """
- AES_CBC Encryption Algorithm
- """
-
- class aescbc_AES_CBC(CBC):
- """ AES encryption in CBC feedback mode """
- def __init__(self, key=None, padding=padWithPadLen(), keySize=16):
- CBC.__init__( self, AES(key, noPadding(), keySize), padding)
- self.name = 'AES_CBC'
-
- class AES_CBC(object):
- def __init__(self):
- self._key = None
- self._iv = None
- self.aes = None
-
- def set_decrypt_key(self, userkey, iv):
- self._key = userkey
- self._iv = iv
- self.aes = aescbc_AES_CBC(userkey, noPadding(), len(userkey))
-
- def decrypt(self, data):
- iv = self._iv
- cleartext = self.aes.decrypt(iv + data)
- return cleartext
-
- import hmac
-
- class KeyIVGen(object):
- # this only exists in openssl so we will use pure python implementation instead
- # 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])
- def pbkdf2(self, passwd, salt, iter, keylen):
-
- def xorbytes( a, b ):
- if len(a) != len(b):
- raise Exception("xorbytes(): lengths differ")
- return bytes([x ^ y for x, y in zip(a, b)])
-
- def prf( h, data ):
- hm = h.copy()
- hm.update( data )
- return hm.digest()
-
- def pbkdf2_F( h, salt, itercount, blocknum ):
- U = prf( h, salt + pack('>i',blocknum ) )
- T = U
- for i in range(2, itercount+1):
- U = prf( h, U )
- T = xorbytes( T, U )
- return T
-
- sha = hashlib.sha1
- digest_size = sha().digest_size
- # l - number of output blocks to produce
- l = keylen // digest_size
- if keylen % digest_size != 0:
- l += 1
- h = hmac.new( passwd, None, sha )
- T = b""
- for i in range(1, l+1):
- T += pbkdf2_F( h, salt, iter, i )
- return T[0: keylen]
-
- def UnprotectHeaderData(encryptedData):
- passwdData = b'header_key_data'
- salt = b'HEADER.2011'
- iter = 0x80
- keylen = 0x100
- key_iv = KeyIVGen().pbkdf2(passwdData, salt, iter, keylen)
- key = key_iv[0:32]
- iv = key_iv[32:48]
- aes=AES_CBC()
- aes.set_decrypt_key(key, iv)
- cleartext = aes.decrypt(encryptedData)
- return cleartext
-
# Various character maps used to decrypt kindle info values.
# Probably supposed to act as obfuscation
charMap2 = b"AaZzB0bYyCc1XxDdW2wEeVv3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_"
@@ -1070,7 +405,7 @@ if iswindows:
salt = str(0x6d8 * int(build)).encode('utf-8') + guid
sp = GetUserName() + b'+@#$%+' + GetIDString().encode('utf-8')
passwd = encode(SHA256(sp), charMap5)
- key = KeyIVGen().pbkdf2(passwd, salt, 10000, 0x400)[:32] # this is very slow
+ key = PBKDF2(passwd, salt, count=10000, dkLen=0x400)[:32] # this is very slow
# loop through the item records until all are processed
while len(items) > 0:
@@ -1133,8 +468,6 @@ if iswindows:
entropy = SHA1(keyhash) + added_entropy
cleartext = CryptUnprotectData(encryptedValue, entropy, 1)
elif version == 6:
- from Crypto.Cipher import AES
- from Crypto.Util import Counter
# decode using new testMap8 to get IV + ciphertext
iv_ciphertext = decode(encdata, testMap8)
# pad IV so that we can substitute AES-CTR for GCM
@@ -1164,114 +497,8 @@ if iswindows:
DB = {}
return DB
elif isosx:
- import copy
import subprocess
- # 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:
- libcrypto = '/usr/lib/libcrypto.dylib'
- try:
- libcrypto = CDLL(libcrypto)
- except Exception as e:
- raise DrmException("libcrypto not found: " % e)
-
- # From OpenSSL's crypto aes header
- #
- # AES_ENCRYPT 1
- # AES_DECRYPT 0
- # AES_MAXNR 14 (in bytes)
- # AES_BLOCK_SIZE 16 (in bytes)
- #
- # struct aes_key_st {
- # unsigned long rd_key[4 *(AES_MAXNR + 1)];
- # int rounds;
- # };
- # typedef struct aes_key_st AES_KEY;
- #
- # int AES_set_decrypt_key(const unsigned char *userKey, const int bits, AES_KEY *key);
- #
- # note: the ivec string, and output buffer are both mutable
- # void AES_cbc_encrypt(const unsigned char *in, unsigned char *out,
- # const unsigned long length, const AES_KEY *key, unsigned char *ivec, const int enc);
-
- 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])
-
- # From OpenSSL's Crypto evp/p5_crpt2.c
- #
- # int PKCS5_PBKDF2_HMAC_SHA1(const char *pass, int passlen,
- # const unsigned char *salt, int saltlen, int iter,
- # int keylen, unsigned char *out);
-
- 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
- self._userkey = userkey
- 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))
- mutable_iv = create_string_buffer(self._iv, len(self._iv))
- keyctx = self._keyctx
- rv = AES_cbc_encrypt(data, out, len(data), keyctx, mutable_iv, 0)
- if rv == 0:
- raise DrmException("AES decryption failed")
- return out.raw
-
- def keyivgen(self, passwd, salt, iter, keylen):
- saltlen = len(salt)
- passlen = len(passwd)
- out = create_string_buffer(keylen)
- rv = PKCS5_PBKDF2_HMAC_SHA1(passwd, passlen, salt, saltlen, iter, keylen, out)
- return out.raw
- return LibCrypto
-
- def _load_crypto():
- LibCrypto = None
- try:
- LibCrypto = _load_crypto_libcrypto()
- except (ImportError, DrmException):
- pass
- return LibCrypto
-
- LibCrypto = _load_crypto()
-
# Various character maps used to decrypt books. Probably supposed to act as obfuscation
charMap1 = b'n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M'
charMap2 = b'ZB0bYyc1xDdW2wEV3Ff7KkPpL8UuGA4gz-Tme9Nn_tHh5SvXCsIiR6rJjQaqlOoM'
@@ -1401,33 +628,13 @@ elif isosx:
#print "ID Strings:\n",strings
return strings
-
- # unprotect the new header blob in .kinf2011
- # used in Kindle for Mac Version >= 1.9.0
- def UnprotectHeaderData(encryptedData):
- passwdData = b'header_key_data'
- salt = b'HEADER.2011'
- iter = 0x80
- keylen = 0x100
- crp = LibCrypto()
- key_iv = crp.keyivgen(passwdData, salt, iter, keylen)
- key = key_iv[0:32]
- iv = key_iv[32:48]
- crp.set_decrypt_key(key,iv)
- cleartext = crp.decrypt(encryptedData)
- return cleartext
-
-
# implements an Pseudo Mac Version of Windows built-in Crypto routine
class CryptUnprotectData(object):
def __init__(self, entropy, IDString):
sp = GetUserName() + b'+@#$%+' + IDString
passwdData = encode(SHA256(sp),charMap2)
salt = entropy
- self.crp = LibCrypto()
- iter = 0x800
- keylen = 0x400
- key_iv = self.crp.keyivgen(passwdData, salt, iter, keylen)
+ key_iv = PBKDF2(passwdData, salt, count=0x800, dkLen=0x400)
self.key = key_iv[0:32]
self.iv = key_iv[32:48]
self.crp.set_decrypt_key(self.key, self.iv)
@@ -1516,7 +723,7 @@ elif isosx:
b'SerialNumber',\
b'UsernameHash',\
b'kindle.directedid.info',\
- b'DSN'
+ b'DSN',\
b'kindle.accounttype.info',\
b'krx.flashcardsplugin.data.encryption_key',\
b'krx.notebookexportplugin.data.encryption_key',\
@@ -1565,7 +772,7 @@ elif isosx:
salt = str(0x6d8 * int(build)).encode('utf-8') + guid
sp = GetUserName() + b'+@#$%+' + IDString
passwd = encode(SHA256(sp), charMap5)
- key = LibCrypto().keyivgen(passwd, salt, 10000, 0x400)[:32]
+ key = PBKDF2(passwd, salt, count=10000, dkLen=0x400)[:32]
#print ("salt",salt)
#print ("sp",sp)
@@ -1637,8 +844,6 @@ elif isosx:
cleartext = cud.decrypt(encryptedValue)
elif version == 6:
- from Crypto.Cipher import AES
- from Crypto.Util import Counter
# decode using new testMap8 to get IV + ciphertext
iv_ciphertext = decode(encdata, testMap8)
# pad IV so that we can substitute AES-CTR for GCM
@@ -1728,7 +933,7 @@ def usage(progname):
def cli_main():
sys.stdout=SafeUnbuffered(sys.stdout)
sys.stderr=SafeUnbuffered(sys.stderr)
- argv=unicode_argv()
+ argv=unicode_argv("kindlekey.py")
progname = os.path.basename(argv[0])
print("{0} v{1}\nCopyright © 2010-2020 by some_updates, Apprentice Harper et al.".format(progname,__version__))
@@ -1789,7 +994,7 @@ def gui_main():
self.text.insert(tkinter.constants.END, text)
- argv=unicode_argv()
+ argv=unicode_argv("kindlekey.py")
root = tkinter.Tk()
root.withdraw()
progpath, progname = os.path.split(argv[0])
diff --git a/DeDRM_plugin/kindlepid.py b/DeDRM_plugin/kindlepid.py
index d640306..24e0fe8 100644
--- a/DeDRM_plugin/kindlepid.py
+++ b/DeDRM_plugin/kindlepid.py
@@ -16,62 +16,9 @@
import sys
import binascii
-# Wrap a stream so that output gets flushed immediately
-# and also make sure that any unicode strings get
-# encoded using "replace" before writing them.
-class SafeUnbuffered:
- def __init__(self, stream):
- self.stream = stream
- self.encoding = stream.encoding
- if self.encoding == None:
- self.encoding = "utf-8"
- def write(self, data):
- if isinstance(data, str):
- data = data.encode(self.encoding,"replace")
- self.stream.buffer.write(data)
- self.stream.buffer.flush()
-
- def __getattr__(self, attr):
- return getattr(self.stream, attr)
-
-iswindows = sys.platform.startswith('win')
-isosx = sys.platform.startswith('darwin')
-
-def unicode_argv():
- if iswindows:
- # Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
- # strings.
-
- # Versions 2.x of Python don't support Unicode in sys.argv on
- # Windows, with the underlying Windows API instead replacing multi-byte
- # characters with '?'.
-
-
- from ctypes import POINTER, byref, cdll, c_int, windll
- from ctypes.wintypes import LPCWSTR, LPWSTR
-
- GetCommandLineW = cdll.kernel32.GetCommandLineW
- GetCommandLineW.argtypes = []
- GetCommandLineW.restype = LPCWSTR
-
- CommandLineToArgvW = windll.shell32.CommandLineToArgvW
- CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
- CommandLineToArgvW.restype = POINTER(LPWSTR)
-
- cmd = GetCommandLineW()
- argc = c_int(0)
- argv = CommandLineToArgvW(cmd, byref(argc))
- if argc.value > 0:
- # Remove Python executable and commands if present
- start = argc.value - len(sys.argv)
- return [argv[i] for i in
- range(start, argc.value)]
- # if we don't have any arguments at all, just pass back script name
- # this should never happen
- return ["kindlepid.py"]
- else:
- argvencoding = sys.stdin.encoding or "utf-8"
- return [arg if isinstance(arg, str) else str(arg, argvencoding) for arg in sys.argv]
+from utilities import SafeUnbuffered
+
+from argv_utils import unicode_argv
letters = 'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789'
@@ -111,7 +58,7 @@ def pidFromSerial(s, l):
def cli_main():
print("Mobipocket PID calculator for Amazon Kindle. Copyright © 2007, 2009 Igor Skochinsky")
- argv=unicode_argv()
+ argv=unicode_argv("kindlepid.py")
if len(argv)==2:
serial = argv[1]
else:
diff --git a/DeDRM_plugin/lcpdedrm.py b/DeDRM_plugin/lcpdedrm.py
new file mode 100644
index 0000000..a7e848f
--- /dev/null
+++ b/DeDRM_plugin/lcpdedrm.py
@@ -0,0 +1,70 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+# lcpdedrm.py
+# Copyright © 2021-2022 NoDRM
+
+# Released under the terms of the GNU General Public Licence, version 3
+#
+
+
+# Revision history:
+# 1 - Initial release
+# 2 - LCP DRM code removed due to a DMCA takedown.
+
+"""
+This file used to contain code to remove the Readium LCP DRM
+from eBooks. Unfortunately, Readium has issued a DMCA takedown
+request, so I was forced to remove that code:
+
+https://github.com/github/dmca/blob/master/2022/01/2022-01-04-readium.md
+
+This file now just returns an error message when asked to remove LCP DRM.
+For more information, see this issue:
+https://github.com/noDRM/DeDRM_tools/issues/18
+"""
+
+__license__ = 'GPL v3'
+__version__ = "2"
+
+import json
+from zipfile import ZipFile
+from contextlib import closing
+
+
+class LCPError(Exception):
+ pass
+
+# Check file to see if this is an LCP-protected file
+def isLCPbook(inpath):
+ try:
+ with closing(ZipFile(open(inpath, 'rb'))) as lcpbook:
+ if ("META-INF/license.lcpl" not in lcpbook.namelist() or
+ "META-INF/encryption.xml" not in lcpbook.namelist() or
+ b"EncryptedContentKey" not in lcpbook.read("META-INF/encryption.xml")):
+ return False
+
+ license = json.loads(lcpbook.read('META-INF/license.lcpl'))
+
+ if "id" in license and "encryption" in license and "profile" in license["encryption"]:
+ return True
+
+ except:
+ return False
+
+ return False
+
+
+# Takes a file and a list of passphrases
+def decryptLCPbook(inpath, passphrases, parent_object):
+
+ if not isLCPbook(inpath):
+ raise LCPError("This is not an LCP-encrypted book")
+
+ print("LCP: LCP DRM removal no longer supported due to a DMCA takedown request.")
+ print("LCP: The takedown request can be found here: ")
+ print("LCP: https://github.com/github/dmca/blob/master/2022/01/2022-01-04-readium.md ")
+ print("LCP: More information can be found in the Github repository: ")
+ print("LCP: https://github.com/noDRM/DeDRM_tools/issues/18 ")
+
+ raise LCPError("LCP DRM removal no longer supported")
diff --git a/DeDRM_plugin/libalfcrypto.dylib b/DeDRM_plugin/libalfcrypto.dylib
deleted file mode 100644
index 01c348c..0000000
Binary files a/DeDRM_plugin/libalfcrypto.dylib and /dev/null differ
diff --git a/DeDRM_plugin/libalfcrypto32.so b/DeDRM_plugin/libalfcrypto32.so
deleted file mode 100644
index 9a5a442..0000000
Binary files a/DeDRM_plugin/libalfcrypto32.so and /dev/null differ
diff --git a/DeDRM_plugin/libalfcrypto64.so b/DeDRM_plugin/libalfcrypto64.so
deleted file mode 100644
index a08ac28..0000000
Binary files a/DeDRM_plugin/libalfcrypto64.so and /dev/null differ
diff --git a/DeDRM_plugin/mobidedrm.py b/DeDRM_plugin/mobidedrm.py
index 79cd720..0374836 100755
--- a/DeDRM_plugin/mobidedrm.py
+++ b/DeDRM_plugin/mobidedrm.py
@@ -7,7 +7,7 @@
from __future__ import print_function
__license__ = 'GPL v3'
-__version__ = "1.0"
+__version__ = "1.1"
# This is a python script. You need a Python interpreter to run it.
# For example, ActiveState Python, which exists for windows.
@@ -74,72 +74,17 @@ __version__ = "1.0"
# 0.41 - Fixed potential unicode problem in command line calls
# 0.42 - Added GPL v3 licence. updated/removed some print statements
# 1.0 - Python 3 compatibility for calibre 5.0
+# 1.1 - Speed Python PC1 implementation up a little bit
import sys
import os
import struct
import binascii
-try:
- from alfcrypto import Pukall_Cipher
-except:
- print("AlfCrypto not found. Using python PC1 implementation.")
-
-# Wrap a stream so that output gets flushed immediately
-# and also make sure that any unicode strings get
-# encoded using "replace" before writing them.
-class SafeUnbuffered:
- def __init__(self, stream):
- self.stream = stream
- self.encoding = stream.encoding
- if self.encoding == None:
- self.encoding = "utf-8"
- def write(self, data):
- if isinstance(data, str):
- data = data.encode(self.encoding,"replace")
- self.stream.buffer.write(data)
- self.stream.buffer.flush()
-
- def __getattr__(self, attr):
- return getattr(self.stream, attr)
-
-iswindows = sys.platform.startswith('win')
-isosx = sys.platform.startswith('darwin')
-
-def unicode_argv():
- if iswindows:
- # Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
- # strings.
-
- # Versions 2.x of Python don't support Unicode in sys.argv on
- # Windows, with the underlying Windows API instead replacing multi-byte
- # characters with '?'.
-
-
- from ctypes import POINTER, byref, cdll, c_int, windll
- from ctypes.wintypes import LPCWSTR, LPWSTR
-
- GetCommandLineW = cdll.kernel32.GetCommandLineW
- GetCommandLineW.argtypes = []
- GetCommandLineW.restype = LPCWSTR
-
- CommandLineToArgvW = windll.shell32.CommandLineToArgvW
- CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
- CommandLineToArgvW.restype = POINTER(LPWSTR)
-
- cmd = GetCommandLineW()
- argc = c_int(0)
- argv = CommandLineToArgvW(cmd, byref(argc))
- if argc.value > 0:
- # Remove Python executable and commands if present
- start = argc.value - len(sys.argv)
- return [argv[i] for i in
- range(start, argc.value)]
- # if we don't have any arguments at all, just pass back script name
- # this should never happen
- return ["mobidedrm.py"]
- else:
- argvencoding = sys.stdin.encoding or "utf-8"
- return [arg if isinstance(arg, str) else str(arg, argvencoding) for arg in sys.argv]
+from alfcrypto import Pukall_Cipher
+
+from utilities import SafeUnbuffered
+
+from argv_utils import unicode_argv
class DrmException(Exception):
@@ -155,41 +100,8 @@ def PC1(key, src, decryption=True):
# if we can get it from alfcrypto, use that
try:
return Pukall_Cipher().PC1(key,src,decryption)
- except NameError:
- pass
- except TypeError:
- pass
-
- # use slow python version, since Pukall_Cipher didn't load
- sum1 = 0;
- sum2 = 0;
- keyXorVal = 0;
- if len(key)!=16:
- DrmException ("PC1: Bad key length")
- wkey = []
- for i in range(8):
- wkey.append(key[i*2]<<8 | key[i*2+1])
- dst = b''
- for i in range(len(src)):
- temp1 = 0;
- byteXorVal = 0;
- for j in range(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 = src[i]
- if not decryption:
- keyXorVal = curByte * 257;
- curByte = ((curByte ^ (byteXorVal >> 8)) ^ byteXorVal) & 0xFF
- if decryption:
- keyXorVal = curByte * 257;
- for j in range(8):
- wkey[j] ^= keyXorVal;
- dst+=bytes([curByte])
- return dst
+ except:
+ raise
# accepts unicode returns unicode
def checksumPid(s):
@@ -247,12 +159,7 @@ class MobiBook:
pass
def __init__(self, infile):
- print("MobiDeDrm v{0:s}.\nCopyright © 2008-2020 The Dark Reverser, Apprentice Harper et al.".format(__version__))
-
- try:
- from alfcrypto import Pukall_Cipher
- except:
- print("AlfCrypto not found. Using python PC1 implementation.")
+ print("MobiDeDrm v{0:s}.\nCopyright © 2008-2022 The Dark Reverser, Apprentice Harper et al.".format(__version__))
# initial sanity check on file
self.data_file = open(infile, 'rb').read()
@@ -320,6 +227,15 @@ class MobiBook:
elif type == 404 and size == 9:
# make sure text to speech is enabled
self.patchSection(0, b'\0', 16 + self.mobi_length + pos + 8)
+ elif type == 405 and size == 9:
+ # remove rented book flag
+ self.patchSection(0, b'\0', 16 + self.mobi_length + pos + 8)
+ elif type == 406 and size == 16:
+ # remove rental due date
+ self.patchSection(0, b'\0'*8, 16 + self.mobi_length + pos + 8)
+ elif type == 208:
+ # remove watermark (atv:kin: stuff)
+ self.patchSection(0, b'\0'*(size-8), 16 + self.mobi_length + pos + 8)
# print type, size, content, content.encode('hex')
pos += size
except Exception as e:
@@ -437,7 +353,7 @@ class MobiBook:
if crypto_type == 0:
print("This book is not encrypted.")
# we must still check for Print Replica
- self.print_replica = (self.loadSection(1)[0:4] == '%MOP')
+ self.print_replica = (self.loadSection(1)[0:4] == b'%MOP')
self.mobi_data = self.data_file
return
if crypto_type != 2 and crypto_type != 1:
@@ -446,7 +362,8 @@ class MobiBook:
data406 = self.meta_array[406]
val406, = struct.unpack('>Q',data406)
if val406 != 0:
- raise DrmException("Cannot decode library or rented ebooks.")
+ print("Warning: This is a library or rented ebook ({0}). Continuing ...".format(val406))
+ #raise DrmException("Cannot decode library or rented ebooks.")
goodpids = []
# print("DEBUG ==== pidlist = ", pidlist)
@@ -507,7 +424,7 @@ class MobiBook:
# print "record %d, extra_size %d" %(i,extra_size)
decoded_data = PC1(found_key, data[0:len(data) - extra_size])
if i==1:
- self.print_replica = (decoded_data[0:4] == '%MOP')
+ self.print_replica = (decoded_data[0:4] == b'%MOP')
mobidataList.append(decoded_data)
if extra_size > 0:
mobidataList.append(data[-extra_size:])
@@ -527,7 +444,7 @@ def getUnencryptedBook(infile,pidlist):
def cli_main():
- argv=unicode_argv()
+ argv=unicode_argv("mobidedrm.py")
progname = os.path.basename(argv[0])
if len(argv)<3 or len(argv)>4:
print("MobiDeDrm v{0:s}.\nCopyright © 2008-2020 The Dark Reverser, Apprentice Harper et al.".format(__version__))
diff --git a/DeDRM_plugin/openssl_des.py b/DeDRM_plugin/openssl_des.py
deleted file mode 100644
index 9e455b4..0000000
--- a/DeDRM_plugin/openssl_des.py
+++ /dev/null
@@ -1,89 +0,0 @@
-#!/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 Exception('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 b''
- i = 0
- result = []
- while i < len(data):
- block = data[i:i+8]
- processed_block = self.desdecrypt(block)
- result.append(processed_block)
- i += 8
- return b''.join(result)
-
- return DES
diff --git a/DeDRM_plugin/prefs.py b/DeDRM_plugin/prefs.py
index 3e8d78b..0ae3943 100755
--- a/DeDRM_plugin/prefs.py
+++ b/DeDRM_plugin/prefs.py
@@ -5,20 +5,32 @@
__license__ = 'GPL v3'
# Standard Python modules.
-import os, sys, re, hashlib
-import codecs, json
+import os, sys
import traceback
-from calibre.utils.config import dynamic, config_dir, JSONConfig
-from calibre_plugins.dedrm.__init__ import PLUGIN_NAME, PLUGIN_VERSION
-from calibre.constants import iswindows, isosx
+
+#@@CALIBRE_COMPAT_CODE@@
+
+
+try:
+ from calibre.utils.config import JSONConfig
+except:
+ from standalone.jsonconfig import JSONConfig
+
+from __init__ import PLUGIN_NAME
class DeDRM_Prefs():
- def __init__(self):
- JSON_PATH = os.path.join("plugins", PLUGIN_NAME.strip().lower().replace(' ', '_') + '.json')
+ def __init__(self, json_path=None):
+ if json_path is None:
+ JSON_PATH = os.path.join("plugins", PLUGIN_NAME.strip().lower().replace(' ', '_') + '.json')
+ else:
+ JSON_PATH = json_path
+
self.dedrmprefs = JSONConfig(JSON_PATH)
self.dedrmprefs.defaults['configured'] = False
+ self.dedrmprefs.defaults['deobfuscate_fonts'] = True
+ self.dedrmprefs.defaults['remove_watermarks'] = False
self.dedrmprefs.defaults['bandnkeys'] = {}
self.dedrmprefs.defaults['adeptkeys'] = {}
self.dedrmprefs.defaults['ereaderkeys'] = {}
@@ -26,6 +38,8 @@ class DeDRM_Prefs():
self.dedrmprefs.defaults['androidkeys'] = {}
self.dedrmprefs.defaults['pids'] = []
self.dedrmprefs.defaults['serials'] = []
+ self.dedrmprefs.defaults['lcp_passphrases'] = []
+ self.dedrmprefs.defaults['adobe_pdf_passphrases'] = []
self.dedrmprefs.defaults['adobewineprefix'] = ""
self.dedrmprefs.defaults['kindlewineprefix'] = ""
@@ -47,6 +61,10 @@ class DeDRM_Prefs():
self.dedrmprefs['pids'] = []
if self.dedrmprefs['serials'] == []:
self.dedrmprefs['serials'] = []
+ if self.dedrmprefs['lcp_passphrases'] == []:
+ self.dedrmprefs['lcp_passphrases'] = []
+ if self.dedrmprefs['adobe_pdf_passphrases'] == []:
+ self.dedrmprefs['adobe_pdf_passphrases'] = []
def __getitem__(self,kind = None):
if kind is not None:
@@ -86,210 +104,3 @@ class DeDRM_Prefs():
except:
traceback.print_exc()
return False
-
-
-def convertprefs(always = False):
-
- def parseIgnobleString(keystuff):
- from calibre_plugins.dedrm.ignoblekeygen import generate_key
- userkeys = []
- ar = keystuff.split(':')
- for keystring in ar:
- try:
- name, ccn = keystring.split(',')
- # Generate Barnes & Noble EPUB user key from name and credit card number.
- keyname = "{0}_{1}".format(name.strip(),ccn.strip()[-4:])
- keyvalue = generate_key(name, ccn)
- userkeys.append([keyname,keyvalue])
- except Exception as e:
- traceback.print_exc()
- print(e.args[0])
- pass
- return userkeys
-
- def parseeReaderString(keystuff):
- from calibre_plugins.dedrm.erdr2pml import getuser_key
- userkeys = []
- ar = keystuff.split(':')
- for keystring in ar:
- try:
- name, cc = keystring.split(',')
- # Generate eReader user key from name and credit card number.
- keyname = "{0}_{1}".format(name.strip(),cc.strip()[-4:])
- keyvalue = codecs.encode(getuser_key(name,cc),'hex')
- userkeys.append([keyname,keyvalue])
- except Exception as e:
- traceback.print_exc()
- print(e.args[0])
- pass
- return userkeys
-
- def parseKindleString(keystuff):
- pids = []
- serials = []
- ar = keystuff.split(',')
- for keystring in ar:
- keystring = str(keystring).strip().replace(" ","")
- if len(keystring) == 10 or len(keystring) == 8 and keystring not in pids:
- pids.append(keystring)
- elif len(keystring) == 16 and keystring[0] == 'B' and keystring not in serials:
- serials.append(keystring)
- return (pids,serials)
-
- def getConfigFiles(extension, encoding = None):
- # get any files with extension 'extension' in the config dir
- userkeys = []
- files = [f for f in os.listdir(config_dir) if f.endswith(extension)]
- for filename in files:
- try:
- fpath = os.path.join(config_dir, filename)
- key = os.path.splitext(filename)[0]
- value = open(fpath, 'rb').read()
- if encoding is not None:
- value = codecs.encode(value,encoding)
- userkeys.append([key,value])
- except:
- traceback.print_exc()
- pass
- return userkeys
-
- dedrmprefs = DeDRM_Prefs()
-
- if (not always) and dedrmprefs['configured']:
- # We've already converted old preferences,
- # and we're not being forced to do it again, so just return
- return
-
-
- print("{0} v{1}: Importing configuration data from old DeDRM plugins".format(PLUGIN_NAME, PLUGIN_VERSION))
-
- IGNOBLEPLUGINNAME = "Ignoble Epub DeDRM"
- EREADERPLUGINNAME = "eReader PDB 2 PML"
- OLDKINDLEPLUGINNAME = "K4PC, K4Mac, Kindle Mobi and Topaz DeDRM"
-
- # get prefs from older tools
- kindleprefs = JSONConfig(os.path.join("plugins", "K4MobiDeDRM"))
- ignobleprefs = JSONConfig(os.path.join("plugins", "ignoble_epub_dedrm"))
-
- # Handle the old ignoble plugin's customization string by converting the
- # old string to stored keys... get that personal data out of plain sight.
- from calibre.customize.ui import config
- sc = config['plugin_customization']
- val = sc.pop(IGNOBLEPLUGINNAME, None)
- if val is not None:
- print("{0} v{1}: Converting old Ignoble plugin configuration string.".format(PLUGIN_NAME, PLUGIN_VERSION))
- priorkeycount = len(dedrmprefs['bandnkeys'])
- userkeys = parseIgnobleString(str(val))
- for keypair in userkeys:
- name = keypair[0]
- value = keypair[1]
- dedrmprefs.addnamedvaluetoprefs('bandnkeys', name, value)
- addedkeycount = len(dedrmprefs['bandnkeys'])-priorkeycount
- print("{0} v{1}: {2:d} Barnes and Noble {3} imported from old Ignoble plugin configuration string".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, "key" if addedkeycount==1 else "keys"))
- # Make the json write all the prefs to disk
- dedrmprefs.writeprefs(False)
-
- # Handle the old eReader plugin's customization string by converting the
- # old string to stored keys... get that personal data out of plain sight.
- val = sc.pop(EREADERPLUGINNAME, None)
- if val is not None:
- print("{0} v{1}: Converting old eReader plugin configuration string.".format(PLUGIN_NAME, PLUGIN_VERSION))
- priorkeycount = len(dedrmprefs['ereaderkeys'])
- userkeys = parseeReaderString(str(val))
- for keypair in userkeys:
- name = keypair[0]
- value = keypair[1]
- dedrmprefs.addnamedvaluetoprefs('ereaderkeys', name, value)
- addedkeycount = len(dedrmprefs['ereaderkeys'])-priorkeycount
- print("{0} v{1}: {2:d} eReader {3} imported from old eReader plugin configuration string".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, "key" if addedkeycount==1 else "keys"))
- # Make the json write all the prefs to disk
- dedrmprefs.writeprefs(False)
-
- # get old Kindle plugin configuration string
- val = sc.pop(OLDKINDLEPLUGINNAME, None)
- if val is not None:
- print("{0} v{1}: Converting old Kindle plugin configuration string.".format(PLUGIN_NAME, PLUGIN_VERSION))
- priorpidcount = len(dedrmprefs['pids'])
- priorserialcount = len(dedrmprefs['serials'])
- pids, serials = parseKindleString(val)
- for pid in pids:
- dedrmprefs.addvaluetoprefs('pids',pid)
- for serial in serials:
- dedrmprefs.addvaluetoprefs('serials',serial)
- addedpidcount = len(dedrmprefs['pids']) - priorpidcount
- addedserialcount = len(dedrmprefs['serials']) - priorserialcount
- print("{0} v{1}: {2:d} {3} and {4:d} {5} imported from old Kindle plugin configuration string.".format(PLUGIN_NAME, PLUGIN_VERSION, addedpidcount, "PID" if addedpidcount==1 else "PIDs", addedserialcount, "serial number" if addedserialcount==1 else "serial numbers"))
- # Make the json write all the prefs to disk
- dedrmprefs.writeprefs(False)
-
- # copy the customisations back into calibre preferences, as we've now removed the nasty plaintext
- config['plugin_customization'] = sc
-
- # get any .b64 files in the config dir
- priorkeycount = len(dedrmprefs['bandnkeys'])
- bandnfilekeys = getConfigFiles('.b64')
- for keypair in bandnfilekeys:
- name = keypair[0]
- value = keypair[1]
- dedrmprefs.addnamedvaluetoprefs('bandnkeys', name, value)
- addedkeycount = len(dedrmprefs['bandnkeys'])-priorkeycount
- if addedkeycount > 0:
- print("{0} v{1}: {2:d} Barnes and Noble {3} imported from config folder.".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, "key file" if addedkeycount==1 else "key files"))
- # Make the json write all the prefs to disk
- dedrmprefs.writeprefs(False)
-
- # get any .der files in the config dir
- priorkeycount = len(dedrmprefs['adeptkeys'])
- adeptfilekeys = getConfigFiles('.der','hex')
- for keypair in adeptfilekeys:
- name = keypair[0]
- value = keypair[1]
- dedrmprefs.addnamedvaluetoprefs('adeptkeys', name, value)
- addedkeycount = len(dedrmprefs['adeptkeys'])-priorkeycount
- if addedkeycount > 0:
- print("{0} v{1}: {2:d} Adobe Adept {3} imported from config folder.".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, "keyfile" if addedkeycount==1 else "keyfiles"))
- # Make the json write all the prefs to disk
- dedrmprefs.writeprefs(False)
-
- # get ignoble json prefs
- if 'keys' in ignobleprefs:
- priorkeycount = len(dedrmprefs['bandnkeys'])
- for name in ignobleprefs['keys']:
- value = ignobleprefs['keys'][name]
- dedrmprefs.addnamedvaluetoprefs('bandnkeys', name, value)
- addedkeycount = len(dedrmprefs['bandnkeys']) - priorkeycount
- # no need to delete old prefs, since they contain no recoverable private data
- if addedkeycount > 0:
- print("{0} v{1}: {2:d} Barnes and Noble {3} imported from Ignoble plugin preferences.".format(PLUGIN_NAME, PLUGIN_VERSION, addedkeycount, "key" if addedkeycount==1 else "keys"))
- # Make the json write all the prefs to disk
- dedrmprefs.writeprefs(False)
-
- # get kindle json prefs
- priorpidcount = len(dedrmprefs['pids'])
- priorserialcount = len(dedrmprefs['serials'])
- if 'pids' in kindleprefs:
- pids, serials = parseKindleString(kindleprefs['pids'])
- for pid in pids:
- dedrmprefs.addvaluetoprefs('pids',pid)
- if 'serials' in kindleprefs:
- pids, serials = parseKindleString(kindleprefs['serials'])
- for serial in serials:
- dedrmprefs.addvaluetoprefs('serials',serial)
- addedpidcount = len(dedrmprefs['pids']) - priorpidcount
- if addedpidcount > 0:
- print("{0} v{1}: {2:d} {3} imported from Kindle plugin preferences".format(PLUGIN_NAME, PLUGIN_VERSION, addedpidcount, "PID" if addedpidcount==1 else "PIDs"))
- addedserialcount = len(dedrmprefs['serials']) - priorserialcount
- if addedserialcount > 0:
- print("{0} v{1}: {2:d} {3} imported from Kindle plugin preferences".format(PLUGIN_NAME, PLUGIN_VERSION, addedserialcount, "serial number" if addedserialcount==1 else "serial numbers"))
- try:
- if 'wineprefix' in kindleprefs and kindleprefs['wineprefix'] != "":
- dedrmprefs.set('adobewineprefix',kindleprefs['wineprefix'])
- dedrmprefs.set('kindlewineprefix',kindleprefs['wineprefix'])
- print("{0} v{1}: WINEPREFIX ‘(2)’ imported from Kindle plugin preferences".format(PLUGIN_NAME, PLUGIN_VERSION, kindleprefs['wineprefix']))
- except:
- traceback.print_exc()
-
-
- # Make the json write all the prefs to disk
- dedrmprefs.writeprefs()
- print("{0} v{1}: Finished setting up configuration data.".format(PLUGIN_NAME, PLUGIN_VERSION))
diff --git a/DeDRM_plugin/pycrypto_des.py b/DeDRM_plugin/pycrypto_des.py
deleted file mode 100644
index 286df9f..0000000
--- a/DeDRM_plugin/pycrypto_des.py
+++ /dev/null
@@ -1,30 +0,0 @@
-#!/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 ValueError('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_plugin/python_des.py b/DeDRM_plugin/python_des.py
deleted file mode 100644
index bd02904..0000000
--- a/DeDRM_plugin/python_des.py
+++ /dev/null
@@ -1,220 +0,0 @@
-#!/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_plugin/scriptinterface.py b/DeDRM_plugin/scriptinterface.py
index 25a6c09..e65f426 100644
--- a/DeDRM_plugin/scriptinterface.py
+++ b/DeDRM_plugin/scriptinterface.py
@@ -5,15 +5,19 @@
import sys
import os
+
+
+#@@CALIBRE_COMPAT_CODE@@
+
+
import re
import traceback
-import calibre_plugins.dedrm.ineptepub
-import calibre_plugins.dedrm.ignobleepub
-import calibre_plugins.dedrm.epubtest
-import calibre_plugins.dedrm.zipfix
-import calibre_plugins.dedrm.ineptpdf
-import calibre_plugins.dedrm.erdr2pml
-import calibre_plugins.dedrm.k4mobidedrm
+import ineptepub
+import epubtest
+import zipfix
+import ineptpdf
+import erdr2pml
+import k4mobidedrm
def decryptepub(infile, outdir, rscpath):
errlog = ''
@@ -50,8 +54,8 @@ def decryptepub(infile, outdir, rscpath):
errlog += traceback.format_exc()
errlog += str(e)
rv = 1
- # now try with ignoble epub
- elif ignobleepub.ignobleBook(zippath):
+
+ # now try with ignoble epub
# try with any keyfiles (*.b64) in the rscpath
files = os.listdir(rscpath)
filefilter = re.compile("\.b64$", re.IGNORECASE)
@@ -62,7 +66,7 @@ def decryptepub(infile, outdir, rscpath):
userkey = open(keypath,'r').read()
#print userkey
try:
- rv = ignobleepub.decryptBook(userkey, zippath, outfile)
+ rv = ineptepub.decryptBook(userkey, zippath, outfile)
if rv == 0:
print("Decrypted B&N ePub with key file {0}".format(filename))
break
@@ -121,7 +125,7 @@ def decryptpdb(infile, outdir, rscpath):
rv = 1
socialpath = os.path.join(rscpath,'sdrmlist.txt')
if os.path.exists(socialpath):
- keydata = file(socialpath,'r').read()
+ keydata = open(socialpath,'r').read()
keydata = keydata.rstrip(os.linesep)
ar = keydata.split(',')
for i in ar:
@@ -148,7 +152,7 @@ def decryptk4mobi(infile, outdir, rscpath):
pidnums = []
pidspath = os.path.join(rscpath,'pidlist.txt')
if os.path.exists(pidspath):
- pidstr = file(pidspath,'r').read()
+ pidstr = open(pidspath,'r').read()
pidstr = pidstr.rstrip(os.linesep)
pidstr = pidstr.strip()
if pidstr != '':
@@ -156,7 +160,7 @@ def decryptk4mobi(infile, outdir, rscpath):
serialnums = []
serialnumspath = os.path.join(rscpath,'seriallist.txt')
if os.path.exists(serialnumspath):
- serialstr = file(serialnumspath,'r').read()
+ serialstr = open(serialnumspath,'r').read()
serialstr = serialstr.rstrip(os.linesep)
serialstr = serialstr.strip()
if serialstr != '':
diff --git a/DeDRM_plugin/scrolltextwidget.py b/DeDRM_plugin/scrolltextwidget.py
deleted file mode 100644
index c95a264..0000000
--- a/DeDRM_plugin/scrolltextwidget.py
+++ /dev/null
@@ -1,27 +0,0 @@
-#!/usr/bin/env python
-# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
-
-import tkinter
-import tkinter.constants
-
-# 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=tkinter.constants.RIGHT, fill=tkinter.constants.Y)
- kw.update({'yscrollcommand': self.vbar.set})
- tkinter.Text.__init__(self, self.frame, **kw)
- self.pack(side=tkinter.constants.LEFT, fill=tkinter.constants.BOTH, expand=True)
- self.vbar['command'] = self.yview
- # Copy geometry methods of self.frame without overriding Text
- # methods = hack!
- text_meths = list(vars(tkinter.Text).keys())
- methods = list(vars(tkinter.Pack).keys()) + list(vars(tkinter.Grid).keys()) + list(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_plugin/simpleprefs.py b/DeDRM_plugin/simpleprefs.py
deleted file mode 100644
index 65f2b59..0000000
--- a/DeDRM_plugin/simpleprefs.py
+++ /dev/null
@@ -1,78 +0,0 @@
-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-# 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
- 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 as 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 as e:
- pass
- self.prefs = newprefs
- return
diff --git a/DeDRM_plugin/standalone/__init__.py b/DeDRM_plugin/standalone/__init__.py
new file mode 100644
index 0000000..e5149bc
--- /dev/null
+++ b/DeDRM_plugin/standalone/__init__.py
@@ -0,0 +1,278 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+# CLI interface for the DeDRM plugin (useable without Calibre, too)
+
+from __future__ import absolute_import, print_function
+
+# Copyright © 2021 NoDRM
+
+OPT_SHORT_TO_LONG = [
+ ["c", "config"],
+ ["e", "extract"],
+ ["f", "force"],
+ ["h", "help"],
+ ["i", "import"],
+ ["o", "output"],
+ ["p", "password"],
+ ["q", "quiet"],
+ ["t", "test"],
+ ["u", "username"],
+ ["v", "verbose"],
+]
+
+#@@CALIBRE_COMPAT_CODE@@
+
+import os, sys
+
+
+global _additional_data
+global _additional_params
+global _function
+_additional_data = []
+_additional_params = []
+_function = None
+
+global config_file_path
+config_file_path = "dedrm.json"
+
+def print_fname(f, info):
+ print(" " + f.ljust(15) + " " + info)
+
+def print_opt(short, long, info):
+ if short is None:
+ short = " "
+ else:
+ short = " -" + short
+
+ if long is None:
+ long = " "
+ else:
+ long = "--" + long.ljust(16)
+
+ print(short + " " + long + " " + info, file=sys.stderr)
+
+def print_std_usage(name, param_string):
+ print("Usage: ", file=sys.stderr)
+ if "calibre" in sys.modules:
+ print(" calibre-debug -r \"DeDRM\" -- "+name+" " + param_string, file=sys.stderr)
+ else:
+ print(" python3 DeDRM_plugin.zip "+name+" "+param_string, file=sys.stderr)
+
+def print_err_header():
+ from __init__ import PLUGIN_NAME, PLUGIN_VERSION # type: ignore
+
+ print(PLUGIN_NAME + " v" + PLUGIN_VERSION + " - DRM removal plugin by noDRM")
+ print()
+
+def print_help():
+ from __version import PLUGIN_NAME, PLUGIN_VERSION
+ print(PLUGIN_NAME + " v" + PLUGIN_VERSION + " - DRM removal plugin by noDRM")
+ print("Based on DeDRM Calibre plugin by Apprentice Harper, Apprentice Alf and others.")
+ print("See https://github.com/noDRM/DeDRM_tools for more information.")
+ print()
+ if "calibre" in sys.modules:
+ print("This plugin can be run through Calibre - like you are doing right now - ")
+ print("but it can also be executed with a standalone Python interpreter.")
+ else:
+ print("This plugin can either be imported into Calibre, or be executed directly")
+ print("through Python like you are doing right now.")
+ print()
+ print("Available functions:")
+ print_fname("passhash", "Manage Adobe PassHashes")
+ print_fname("remove_drm", "Remove DRM from one or multiple books")
+ print()
+
+ # TODO: All parameters that are global should be listed here.
+
+def print_credits():
+ from __version import PLUGIN_NAME, PLUGIN_VERSION
+ print(PLUGIN_NAME + " v" + PLUGIN_VERSION + " - Calibre DRM removal plugin by noDRM")
+ print("Based on DeDRM Calibre plugin by Apprentice Harper, Apprentice Alf and others.")
+ print("See https://github.com/noDRM/DeDRM_tools for more information.")
+ print()
+ print("Credits:")
+ print(" - noDRM for the current release of the DeDRM plugin")
+ print(" - Apprentice Alf and Apprentice Harper for the previous versions of the DeDRM plugin")
+ print(" - The Dark Reverser for the Mobipocket and eReader script")
+ print(" - i ♥ cabbages for the Adobe Digital Editions scripts")
+ print(" - Skindle aka Bart Simpson for the Amazon Kindle for PC script")
+ print(" - CMBDTC for Amazon Topaz DRM removal script")
+ print(" - some_updates, clarknova and Bart Simpson for Amazon Topaz conversion scripts")
+ print(" - DiapDealer for the first calibre plugin versions of the tools")
+ print(" - some_updates, DiapDealer, Apprentice Alf and mdlnx for Amazon Kindle/Mobipocket tools")
+ print(" - some_updates for the DeDRM all-in-one Python tool")
+ print(" - Apprentice Alf for the DeDRM all-in-one AppleScript tool")
+
+
+def handle_single_argument(arg, next):
+ used_up = 0
+ global _additional_params
+ global config_file_path
+
+ if arg in ["--username", "--password", "--output", "--outputdir"]:
+ used_up = 1
+ _additional_params.append(arg)
+ if next is None or len(next) == 0:
+ print_err_header()
+ print("Missing parameter for argument " + arg, file=sys.stderr)
+ sys.exit(1)
+ else:
+ _additional_params.append(next[0])
+
+ elif arg == "--config":
+ if next is None or len(next) == 0:
+ print_err_header()
+ print("Missing parameter for argument " + arg, file=sys.stderr)
+ sys.exit(1)
+
+ config_file_path = next[0]
+ used_up = 1
+
+ elif arg in ["--help", "--credits", "--verbose", "--quiet", "--extract", "--import", "--overwrite", "--force"]:
+ _additional_params.append(arg)
+
+
+ else:
+ print_err_header()
+ print("Unknown argument: " + arg, file=sys.stderr)
+ sys.exit(1)
+
+
+ # Used up 0 additional arguments
+ return used_up
+
+
+
+def handle_data(data):
+ global _function
+ global _additional_data
+
+ if _function is None:
+ _function = str(data)
+ else:
+ _additional_data.append(str(data))
+
+def execute_action(action, filenames, params):
+ print("Executing '{0}' on file(s) {1} with parameters {2}".format(action, str(filenames), str(params)), file=sys.stderr)
+
+ if action == "help":
+ print_help()
+ sys.exit(0)
+
+ elif action == "passhash":
+ from standalone.passhash import perform_action
+ perform_action(params, filenames)
+
+ elif action == "remove_drm":
+ if not os.path.isfile(os.path.abspath(config_file_path)):
+ print("Config file missing ...")
+
+ from standalone.remove_drm import perform_action
+ perform_action(params, filenames)
+
+ elif action == "config":
+ import prefs
+ config = prefs.DeDRM_Prefs(os.path.abspath(config_file_path))
+ print(config["adeptkeys"])
+
+ else:
+ print("Command '"+action+"' is unknown.", file=sys.stderr)
+
+
+def main(argv):
+ arguments = argv
+ skip_opts = False
+
+ # First element is always the ZIP name, remove that.
+ if not arguments[0].lower().endswith(".zip") and not "calibre" in sys.modules:
+ print("Warning: File name does not end in .zip ...")
+ print(arguments)
+ arguments.pop(0)
+
+ while len(arguments) > 0:
+ arg = arguments.pop(0)
+
+ if arg == "--":
+ skip_opts = True
+ continue
+
+ if not skip_opts:
+ if arg.startswith("--"):
+ # Give the current arg, plus all remaining ones.
+ # Return the number of additional args we used.
+ used = handle_single_argument(arg, arguments)
+ for _ in range(used):
+ # Function returns number of additional arguments that were
+ # "used up" by that argument.
+ # Remove that amount of arguments from the list.
+ try:
+ arguments.pop(0)
+ except:
+ pass
+ continue
+ elif arg.startswith("-"):
+ single_args = list(arg[1:])
+ # single_args is now a list of single chars, for when you call the program like "ls -alR"
+ # with multiple single-letter options combined.
+ while len(single_args) > 0:
+ c = single_args.pop(0)
+
+ # See if we have a long name for that option.
+ for wrapper in OPT_SHORT_TO_LONG:
+ if wrapper[0] == c:
+ c = "--" + wrapper[1]
+ break
+ else:
+ c = "-" + c
+ # c is now the long term (unless there is no long version, then it's the short version).
+
+ if len(single_args) > 0:
+ # If we have more short arguments, the argument for this one must be None.
+ handle_single_argument(c, None)
+ used = 0
+ else:
+ # If not, then there might be parameters for this short argument.
+ used = handle_single_argument(c, arguments)
+
+ for _ in range(used):
+ # Function returns number of additional arguments that were
+ # "used up" by that argument.
+ # Remove that amount of arguments from the list.
+ try:
+ arguments.pop(0)
+ except:
+ pass
+
+ continue
+
+ handle_data(arg)
+
+
+ if _function is None and "--credits" in _additional_params:
+ print_credits()
+ sys.exit(0)
+
+ if _function is None and "--help" in _additional_params:
+ print_help()
+ sys.exit(0)
+
+ if _function is None:
+ print_help()
+ sys.exit(1)
+
+ # Okay, now actually begin doing stuff.
+ # This function gets told what to do and gets additional data (filenames).
+ # It also receives additional parameters.
+ # The rest of the code will be in different Python files.
+ execute_action(_function.lower(), _additional_data, _additional_params)
+
+
+
+
+
+if __name__ == "__main__":
+ # NOTE: This MUST not do anything else other than calling main()
+ # All the code must be in main(), not in here.
+ import sys
+ main(sys.argv)
\ No newline at end of file
diff --git a/DeDRM_plugin/standalone/jsonconfig.py b/DeDRM_plugin/standalone/jsonconfig.py
new file mode 100644
index 0000000..a4149bf
--- /dev/null
+++ b/DeDRM_plugin/standalone/jsonconfig.py
@@ -0,0 +1,140 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+# CLI interface for the DeDRM plugin (useable without Calibre, too)
+# Config implementation
+
+from __future__ import absolute_import, print_function
+
+# Taken from Calibre code - Copyright © 2008, Kovid Goyal kovid@kovidgoyal.net, GPLv3
+
+#@@CALIBRE_COMPAT_CODE@@
+
+import sys, os, codecs, json
+
+config_dir = "/"
+CONFIG_DIR_MODE = 0o700
+iswindows = sys.platform.startswith('win')
+
+
+filesystem_encoding = sys.getfilesystemencoding()
+if filesystem_encoding is None:
+ filesystem_encoding = 'utf-8'
+else:
+ try:
+ if codecs.lookup(filesystem_encoding).name == 'ascii':
+ filesystem_encoding = 'utf-8'
+ # On linux, unicode arguments to os file functions are coerced to an ascii
+ # bytestring if sys.getfilesystemencoding() == 'ascii', which is
+ # just plain dumb. This is fixed by the icu.py module which, when
+ # imported changes ascii to utf-8
+ except Exception:
+ filesystem_encoding = 'utf-8'
+
+
+class JSONConfig(dict):
+
+ EXTENSION = '.json'
+
+
+ def __init__(self, rel_path_to_cf_file, base_path=config_dir):
+ dict.__init__(self)
+ self.no_commit = False
+ self.defaults = {}
+ self.file_path = os.path.join(base_path,
+ *(rel_path_to_cf_file.split('/')))
+ self.file_path = os.path.abspath(self.file_path)
+ if not self.file_path.endswith(self.EXTENSION):
+ self.file_path += self.EXTENSION
+
+ self.refresh()
+
+ def mtime(self):
+ try:
+ return os.path.getmtime(self.file_path)
+ except OSError:
+ return 0
+
+ def touch(self):
+ try:
+ os.utime(self.file_path, None)
+ except OSError:
+ pass
+
+
+ def decouple(self, prefix):
+ self.file_path = os.path.join(os.path.dirname(self.file_path), prefix + os.path.basename(self.file_path))
+ self.refresh()
+
+ def refresh(self, clear_current=True):
+ d = {}
+ if os.path.exists(self.file_path):
+ with open(self.file_path, "rb") as f:
+ raw = f.read()
+ try:
+ d = self.raw_to_object(raw) if raw.strip() else {}
+ except SystemError:
+ pass
+ except:
+ import traceback
+ traceback.print_exc()
+ d = {}
+ if clear_current:
+ self.clear()
+ self.update(d)
+
+ def has_key(self, key):
+ return dict.__contains__(self, key)
+
+ def set(self, key, val):
+ self.__setitem__(key, val)
+
+ def __delitem__(self, key):
+ try:
+ dict.__delitem__(self, key)
+ except KeyError:
+ pass # ignore missing keys
+ else:
+ self.commit()
+
+ def commit(self):
+ if self.no_commit:
+ return
+ if hasattr(self, 'file_path') and self.file_path:
+ dpath = os.path.dirname(self.file_path)
+ if not os.path.exists(dpath):
+ os.makedirs(dpath, mode=CONFIG_DIR_MODE)
+ with open(self.file_path, "w") as f:
+ raw = self.to_raw()
+ f.seek(0)
+ f.truncate()
+ f.write(raw)
+
+ def __enter__(self):
+ self.no_commit = True
+
+ def __exit__(self, *args):
+ self.no_commit = False
+ self.commit()
+
+ def raw_to_object(self, raw):
+ return json.loads(raw)
+
+ def to_raw(self):
+ return json.dumps(self, ensure_ascii=False)
+
+ def __getitem__(self, key):
+ try:
+ return dict.__getitem__(self, key)
+ except KeyError:
+ return self.defaults[key]
+
+ def get(self, key, default=None):
+ try:
+ return dict.__getitem__(self, key)
+ except KeyError:
+ return self.defaults.get(key, default)
+
+ def __setitem__(self, key, val):
+ dict.__setitem__(self, key, val)
+ self.commit()
\ No newline at end of file
diff --git a/DeDRM_plugin/standalone/passhash.py b/DeDRM_plugin/standalone/passhash.py
new file mode 100644
index 0000000..f7bf565
--- /dev/null
+++ b/DeDRM_plugin/standalone/passhash.py
@@ -0,0 +1,122 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+# CLI interface for the DeDRM plugin (useable without Calibre, too)
+# Adobe PassHash implementation
+
+from __future__ import absolute_import, print_function
+
+# Copyright © 2021 NoDRM
+
+#@@CALIBRE_COMPAT_CODE@@
+
+import os, sys
+
+from standalone.__init__ import print_opt, print_std_usage
+
+iswindows = sys.platform.startswith('win')
+isosx = sys.platform.startswith('darwin')
+
+def print_passhash_help():
+ from __version import PLUGIN_NAME, PLUGIN_VERSION
+ print(PLUGIN_NAME + " v" + PLUGIN_VERSION + " - Calibre DRM removal plugin by noDRM")
+ print()
+ print("passhash: Manage Adobe PassHashes")
+ print()
+ print_std_usage("passhash", "[ -u username -p password | -b base64str ] [ -i ] ")
+
+ print()
+ print("Options: ")
+ print_opt("u", "username", "Generate a PassHash with the given username")
+ print_opt("p", "password", "Generate a PassHash with the given password")
+ print_opt("e", "extract", "Display PassHashes found on this machine")
+ print_opt("i", "import", "Import hashes into the JSON config file")
+
+def perform_action(params, files):
+ user = None
+ pwd = None
+
+ if len(params) == 0:
+ print_passhash_help()
+ return 0
+
+ extract = False
+ import_to_json = True
+
+ while len(params) > 0:
+ p = params.pop(0)
+ if p == "--username":
+ user = params.pop(0)
+ elif p == "--password":
+ pwd = params.pop(0)
+ elif p == "--extract":
+ extract = True
+ elif p == "--help":
+ print_passhash_help()
+ return 0
+ elif p == "--import":
+ import_to_json = True
+
+ if not extract and not import_to_json:
+ if user is None:
+ print("Missing parameter: --username", file=sys.stderr)
+ if pwd is None:
+ print("Missing parameter: --password", file=sys.stderr)
+ if user is None or pwd is None:
+ return 1
+
+ if user is None and pwd is not None:
+ print("Parameter --password also requires --username", file=sys.stderr)
+ return 1
+ if user is not None and pwd is None:
+ print("Parameter --username also requires --password", file=sys.stderr)
+ return 1
+
+ if user is not None and pwd is not None:
+ from ignoblekeyGenPassHash import generate_key
+ key = generate_key(user, pwd)
+ if import_to_json:
+ # TODO: Import the key to the JSON
+ pass
+
+ print(key.decode("utf-8"))
+
+ if extract or import_to_json:
+ if not iswindows and not isosx:
+ print("Extracting PassHash keys not supported on Linux.", file=sys.stderr)
+ return 1
+
+ keys = []
+
+ from ignoblekeyNookStudy import nookkeys
+ keys.extend(nookkeys())
+
+ if iswindows:
+ from ignoblekeyWindowsStore import dump_keys
+ keys.extend(dump_keys())
+
+ from adobekey_get_passhash import passhash_keys
+ ade_keys, ade_names = passhash_keys()
+ keys.extend(ade_keys)
+
+ # Trim duplicates
+ newkeys = []
+ for k in keys:
+ if not k in newkeys:
+ newkeys.append(k)
+
+ # Print all found keys
+ for k in newkeys:
+ if import_to_json:
+ # TODO: Add keys to json
+ pass
+
+ if extract:
+ print(k)
+
+
+ return 0
+
+
+if __name__ == "__main__":
+ print("This code is not intended to be executed directly!", file=sys.stderr)
\ No newline at end of file
diff --git a/DeDRM_plugin/standalone/remove_drm.py b/DeDRM_plugin/standalone/remove_drm.py
new file mode 100644
index 0000000..8273b97
--- /dev/null
+++ b/DeDRM_plugin/standalone/remove_drm.py
@@ -0,0 +1,209 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+# CLI interface for the DeDRM plugin (useable without Calibre, too)
+# DRM removal
+
+from __future__ import absolute_import, print_function
+
+# Copyright © 2021 NoDRM
+
+#@@CALIBRE_COMPAT_CODE@@
+
+import os, sys
+
+from zipfile import ZipInfo, ZipFile, ZIP_STORED, ZIP_DEFLATED
+from contextlib import closing
+
+from standalone.__init__ import print_opt, print_std_usage
+
+iswindows = sys.platform.startswith('win')
+isosx = sys.platform.startswith('darwin')
+
+def print_removedrm_help():
+ from __init__ import PLUGIN_NAME, PLUGIN_VERSION
+ print(PLUGIN_NAME + " v" + PLUGIN_VERSION + " - Calibre DRM removal plugin by noDRM")
+ print()
+ print("remove_drm: Remove DRM from one or multiple files")
+ print()
+ print_std_usage("remove_drm", " ... [ -o ] [ -f ]")
+
+ print()
+ print("Options: ")
+ print_opt(None, "outputdir", "Folder to export the file(s) to")
+ print_opt("o", "output", "File name to export the file to")
+ print_opt("f", "force", "Overwrite output file if it already exists")
+ print_opt(None, "overwrite", "Replace DRMed file with DRM-free file (implies --force)")
+
+
+def determine_file_type(file):
+ # Returns a file type:
+ # "PDF", "PDB", "MOBI", "TPZ", "LCP", "ADEPT", "ADEPT-PassHash", "KFX-ZIP", "ZIP" or None
+
+ f = open(file, "rb")
+ fdata = f.read(100)
+ f.close()
+
+ if fdata.startswith(b"PK\x03\x04"):
+ pass
+ # Either LCP, Adobe, or Amazon
+ elif fdata.startswith(b"%PDF"):
+ return "PDF"
+ elif fdata[0x3c:0x3c+8] == b"PNRdPPrs" or fdata[0x3c:0x3c+8] == b"PDctPPrs":
+ return "PDB"
+ elif fdata[0x3c:0x3c+8] == b"BOOKMOBI" or fdata[0x3c:0x3c+8] == b"TEXtREAd":
+ return "MOBI"
+ elif fdata.startswith(b"TPZ"):
+ return "TPZ"
+ else:
+ return None
+ # Unknown file type
+
+
+ # If it's a ZIP, determine the type.
+
+ from lcpdedrm import isLCPbook
+ if isLCPbook(file):
+ return "LCP"
+
+ from ineptepub import adeptBook, isPassHashBook
+ if adeptBook(file):
+ if isPassHashBook(file):
+ return "ADEPT-PassHash"
+ else:
+ return "ADEPT"
+
+ try:
+ # Amazon / KFX-ZIP has a file that starts with b'\xeaDRMION\xee' in the ZIP.
+ with closing(ZipFile(open(file, "rb"))) as book:
+ for subfilename in book.namelist():
+ with book.open(subfilename) as subfile:
+ data = subfile.read(8)
+ if data == b'\xeaDRMION\xee':
+ return "KFX-ZIP"
+ except:
+ pass
+
+ return "ZIP"
+
+
+
+
+def dedrm_single_file(input_file, output_file):
+ # When this runs, all the stupid file handling is done.
+ # Just take the file at the absolute path "input_file"
+ # and export it, DRM-free, to "output_file".
+
+ # Use a temp file as input_file and output_file
+ # might be identical.
+
+ # The output directory might not exist yet.
+
+ print("File " + input_file + " to " + output_file)
+
+ # Okay, first check the file type and don't rely on the extension.
+ try:
+ ftype = determine_file_type(input_file)
+ except:
+ print("Can't determine file type for this file.")
+ ftype = None
+
+ if ftype is None:
+ return
+
+
+
+
+
+def perform_action(params, files):
+ output = None
+ outputdir = None
+ force = False
+ overwrite_original = False
+
+
+ if len(files) == 0:
+ print_removedrm_help()
+ return 0
+
+ while len(params) > 0:
+ p = params.pop(0)
+ if p == "--output":
+ output = params.pop(0)
+ elif p == "--outputdir":
+ outputdir = params.pop(0)
+ elif p == "--force":
+ force = True
+ elif p == "--overwrite":
+ overwrite_original = True
+ force = True
+ elif p == "--help":
+ print_removedrm_help()
+ return 0
+
+ if overwrite_original and (output is not None or outputdir is not None):
+ print("Can't use --overwrite together with --output or --outputdir.", file=sys.stderr)
+ return 1
+
+ if output is not None and os.path.isfile(output) and not force:
+ print("Output file already exists. Use --force to overwrite.", file=sys.stderr)
+ return 1
+
+
+ if output is not None and len(files) > 1:
+ print("Cannot set output file name if there's multiple input files.", file=sys.stderr)
+ return 1
+
+ if outputdir is not None and output is not None and os.path.isabs(output):
+ print("--output parameter is absolute path despite --outputdir being set.", file=sys.stderr)
+ print("Remove --outputdir, or give a relative path to --output.", file=sys.stderr)
+ return 1
+
+
+
+ for file in files:
+
+ file = os.path.abspath(file)
+
+ if not os.path.isfile(file):
+ print("Skipping file " + file + " - not found.", file=sys.stderr)
+ continue
+
+ if overwrite_original:
+ output_filename = file
+ else:
+ if output is not None:
+ # Due to the check above, we DO only have one file here.
+ if outputdir is not None and not os.path.isabs(output):
+ output_filename = os.path.join(outputdir, output)
+ else:
+ output_filename = os.path.abspath(output)
+ else:
+ if outputdir is None:
+ outputdir = os.getcwd()
+ output_filename = os.path.join(outputdir, os.path.basename(file))
+ output_filename = os.path.abspath(output_filename)
+
+ if output_filename == file:
+ # If we export to the import folder, add a suffix to the file name.
+ fn, f_ext = os.path.splitext(output_filename)
+ output_filename = fn + "_nodrm" + f_ext
+
+
+
+ if os.path.isfile(output_filename) and not force:
+ print("Skipping file " + file + " because output file already exists (use --force).", file=sys.stderr)
+ continue
+
+
+
+ dedrm_single_file(file, output_filename)
+
+
+
+
+ return 0
+
+
+if __name__ == "__main__":
+ print("This code is not intended to be executed directly!", file=sys.stderr)
\ No newline at end of file
diff --git a/DeDRM_plugin/subasyncio.py b/DeDRM_plugin/subasyncio.py
deleted file mode 100644
index de084d3..0000000
--- a/DeDRM_plugin/subasyncio.py
+++ /dev/null
@@ -1,148 +0,0 @@
-#!/usr/bin/env python
-# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
-
-import os, sys
-import signal
-import threading
-import subprocess
-from subprocess import Popen, PIPE, STDOUT
-
-# **heavily** chopped up and modfied version of asyncproc.py
-# to make it actually work on Windows as well as Mac/Linux
-# For the original see:
-# "http://www.lysator.liu.se/~bellman/download/"
-# author is "Thomas Bellman "
-# available under GPL version 3 or Later
-
-# create an asynchronous subprocess whose output can be collected in
-# a non-blocking manner
-
-# What a mess! Have to use threads just to get non-blocking io
-# in a cross-platform manner
-
-# luckily all thread use is hidden within this class
-
-class Process(object):
- def __init__(self, *params, **kwparams):
- if len(params) <= 3:
- kwparams.setdefault('stdin', subprocess.PIPE)
- if len(params) <= 4:
- kwparams.setdefault('stdout', subprocess.PIPE)
- if len(params) <= 5:
- kwparams.setdefault('stderr', subprocess.PIPE)
- self.__pending_input = []
- self.__collected_outdata = []
- self.__collected_errdata = []
- self.__exitstatus = None
- self.__lock = threading.Lock()
- self.__inputsem = threading.Semaphore(0)
- self.__quit = False
-
- self.__process = subprocess.Popen(*params, **kwparams)
-
- if self.__process.stdin:
- self.__stdin_thread = threading.Thread(
- name="stdin-thread",
- target=self.__feeder, args=(self.__pending_input,
- self.__process.stdin))
- self.__stdin_thread.setDaemon(True)
- self.__stdin_thread.start()
-
- if self.__process.stdout:
- self.__stdout_thread = threading.Thread(
- name="stdout-thread",
- target=self.__reader, args=(self.__collected_outdata,
- self.__process.stdout))
- self.__stdout_thread.setDaemon(True)
- self.__stdout_thread.start()
-
- if self.__process.stderr:
- self.__stderr_thread = threading.Thread(
- name="stderr-thread",
- target=self.__reader, args=(self.__collected_errdata,
- self.__process.stderr))
- self.__stderr_thread.setDaemon(True)
- self.__stderr_thread.start()
-
- def pid(self):
- return self.__process.pid
-
- def kill(self, signal):
- self.__process.send_signal(signal)
-
- # check on subprocess (pass in 'nowait') to act like poll
- def wait(self, flag):
- if flag.lower() == 'nowait':
- rc = self.__process.poll()
- else:
- rc = self.__process.wait()
- if rc != None:
- if self.__process.stdin:
- self.closeinput()
- if self.__process.stdout:
- self.__stdout_thread.join()
- if self.__process.stderr:
- self.__stderr_thread.join()
- return self.__process.returncode
-
- def terminate(self):
- if self.__process.stdin:
- self.closeinput()
- self.__process.terminate()
-
- # thread gets data from subprocess stdout
- def __reader(self, collector, source):
- while True:
- data = os.read(source.fileno(), 65536)
- self.__lock.acquire()
- collector.append(data)
- self.__lock.release()
- if data == "":
- source.close()
- break
- return
-
- # thread feeds data to subprocess stdin
- def __feeder(self, pending, drain):
- while True:
- self.__inputsem.acquire()
- self.__lock.acquire()
- if not pending and self.__quit:
- drain.close()
- self.__lock.release()
- break
- data = pending.pop(0)
- self.__lock.release()
- drain.write(data)
-
- # non-blocking read of data from subprocess stdout
- def read(self):
- self.__lock.acquire()
- outdata = "".join(self.__collected_outdata)
- del self.__collected_outdata[:]
- self.__lock.release()
- return outdata
-
- # non-blocking read of data from subprocess stderr
- def readerr(self):
- self.__lock.acquire()
- errdata = "".join(self.__collected_errdata)
- del self.__collected_errdata[:]
- self.__lock.release()
- return errdata
-
- # non-blocking write to stdin of subprocess
- def write(self, data):
- if self.__process.stdin is None:
- raise ValueError("Writing to process with stdin not a pipe")
- self.__lock.acquire()
- self.__pending_input.append(data)
- self.__inputsem.release()
- self.__lock.release()
-
- # close stdinput of subprocess
- def closeinput(self):
- self.__lock.acquire()
- self.__quit = True
- self.__inputsem.release()
- self.__lock.release()
diff --git a/DeDRM_plugin/topazextract.py b/DeDRM_plugin/topazextract.py
index 5125d62..1eaa2a5 100644
--- a/DeDRM_plugin/topazextract.py
+++ b/DeDRM_plugin/topazextract.py
@@ -13,81 +13,25 @@ __version__ = '6.0'
import sys
import os, csv, getopt
+
+#@@CALIBRE_COMPAT_CODE@@
+
+
import zlib, zipfile, tempfile, shutil
import traceback
from struct import pack
from struct import unpack
-try:
- from calibre_plugins.dedrm.alfcrypto import Topaz_Cipher
-except:
- from alfcrypto import Topaz_Cipher
-
-# Wrap a stream so that output gets flushed immediately
-# and also make sure that any unicode strings get
-# encoded using "replace" before writing them.
-class SafeUnbuffered:
- def __init__(self, stream):
- self.stream = stream
- self.encoding = stream.encoding
- if self.encoding == None:
- self.encoding = "utf-8"
- def write(self, data):
- if isinstance(data, str):
- data = data.encode(self.encoding,"replace")
- self.stream.buffer.write(data)
- self.stream.buffer.flush()
-
- def __getattr__(self, attr):
- return getattr(self.stream, attr)
-
-iswindows = sys.platform.startswith('win')
-isosx = sys.platform.startswith('darwin')
-
-def unicode_argv():
- if iswindows:
- # Uses shell32.GetCommandLineArgvW to get sys.argv as a list of Unicode
- # strings.
-
- # Versions 2.x of Python don't support Unicode in sys.argv on
- # Windows, with the underlying Windows API instead replacing multi-byte
- # characters with '?'.
-
-
- from ctypes import POINTER, byref, cdll, c_int, windll
- from ctypes.wintypes import LPCWSTR, LPWSTR
-
- GetCommandLineW = cdll.kernel32.GetCommandLineW
- GetCommandLineW.argtypes = []
- GetCommandLineW.restype = LPCWSTR
-
- CommandLineToArgvW = windll.shell32.CommandLineToArgvW
- CommandLineToArgvW.argtypes = [LPCWSTR, POINTER(c_int)]
- CommandLineToArgvW.restype = POINTER(LPWSTR)
-
- cmd = GetCommandLineW()
- argc = c_int(0)
- argv = CommandLineToArgvW(cmd, byref(argc))
- if argc.value > 0:
- # Remove Python executable and commands if present
- start = argc.value - len(sys.argv)
- return [argv[i] for i in
- range(start, argc.value)]
- # if we don't have any arguments at all, just pass back script name
- # this should never happen
- return ["mobidedrm.py"]
- else:
- argvencoding = sys.stdin.encoding or "utf-8"
- return [arg if isinstance(arg, str) else str(arg, argvencoding) for arg in sys.argv]
+
+from alfcrypto import Topaz_Cipher
+from utilities import SafeUnbuffered
+
+from argv_utils import unicode_argv
+
#global switch
debug = False
-if 'calibre' in sys.modules:
- inCalibre = True
- from calibre_plugins.dedrm import kgenpids
-else:
- inCalibre = False
- import kgenpids
+import kgenpids
class DrmException(Exception):
@@ -172,6 +116,8 @@ def decryptRecord(data,PID):
# Try to decrypt a dkey record (contains the bookPID)
def decryptDkeyRecord(data,PID):
record = decryptRecord(data,PID)
+ if isinstance(record, str):
+ record = record.encode('latin-1')
fields = unpack('3sB8sB8s3s',record)
if fields[0] != b'PID' or fields[5] != b'pid' :
raise DrmException("Didn't find PID magic numbers in record")
@@ -315,6 +261,8 @@ class TopazBook:
raise DrmException("Error: Attempt to decrypt without bookKey")
if compressed:
+ if isinstance(record, str):
+ record = bytes(record, 'latin-1')
record = zlib.decompress(record)
return record
@@ -326,14 +274,11 @@ class TopazBook:
keydata = self.getBookPayloadRecord(b'dkey', 0)
except DrmException as e:
print("no dkey record found, book may not be encrypted")
- print("attempting to extrct files without a book key")
+ print("attempting to extract files without a book key")
self.createBookDirectory()
self.extractFiles()
print("Successfully Extracted Topaz contents")
- if inCalibre:
- from calibre_plugins.dedrm import genbook
- else:
- import genbook
+ import genbook
rv = genbook.generateBook(self.outdir, raw, fixedimage)
if rv == 0:
@@ -345,6 +290,8 @@ class TopazBook:
for pid in pidlst:
# use 8 digit pids here
pid = pid[0:8]
+ if isinstance(pid, str):
+ pid = pid.encode('latin-1')
print("Trying: {0}".format(pid))
bookKeys = []
data = keydata
@@ -358,16 +305,13 @@ class TopazBook:
break
if not bookKey:
- raise DrmException("No key found in {0:d} keys tried. Read the FAQs at Harper's repository: https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md".format(len(pidlst)))
+ raise DrmException("No key found in {0:d} keys tried. Read the FAQs at noDRM's repository: https://github.com/noDRM/DeDRM_tools/blob/master/FAQs.md".format(len(pidlst)))
self.setBookKey(bookKey)
self.createBookDirectory()
self.extractFiles()
print("Successfully Extracted Topaz contents")
- if inCalibre:
- from calibre_plugins.dedrm import genbook
- else:
- import genbook
+ import genbook
rv = genbook.generateBook(self.outdir, raw, fixedimage)
if rv == 0:
@@ -415,6 +359,8 @@ class TopazBook:
outputFile = os.path.join(destdir,fname)
print(".", end=' ')
record = self.getBookPayloadRecord(name,index)
+ if isinstance(record, str):
+ record=bytes(record, 'latin-1')
if record != b'':
open(outputFile, 'wb').write(record)
print(" ")
@@ -453,7 +399,7 @@ def usage(progname):
# Main
def cli_main():
- argv=unicode_argv()
+ argv=unicode_argv("topazextract.py")
progname = os.path.basename(argv[0])
print("TopazExtract v{0}.".format(__version__))
diff --git a/DeDRM_plugin/utilities.py b/DeDRM_plugin/utilities.py
index c6670cf..50a7de8 100644
--- a/DeDRM_plugin/utilities.py
+++ b/DeDRM_plugin/utilities.py
@@ -1,39 +1,49 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
-from calibre_plugins.dedrm.ignoblekeygen import generate_key
+#@@CALIBRE_COMPAT_CODE@@
-__license__ = 'GPL v3'
+import sys
-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.'
+__license__ = 'GPL v3'
def uStrCmp (s1, s2, caseless=False):
import unicodedata as ud
- str1 = s1 if isinstance(s1, str) else str(s1)
- str2 = s2 if isinstance(s2, str) else str(s2)
+ if sys.version_info[0] == 2:
+ str1 = s1 if isinstance(s1, unicode) else unicode(s1)
+ str2 = s2 if isinstance(s2, unicode) else unicode(s2)
+ else:
+ str1 = s1 if isinstance(s1, str) else str(s1)
+ str2 = s2 if isinstance(s2, str) else str(s2)
+
if caseless:
return ud.normalize('NFC', str1.lower()) == ud.normalize('NFC', str2.lower())
else:
return ud.normalize('NFC', str1) == ud.normalize('NFC', str2)
-def parseCustString(keystuff):
- userkeys = []
- ar = keystuff.split(':')
- for i in ar:
+
+
+# Wrap a stream so that output gets flushed immediately
+# and also make sure that any unicode strings get safely
+# encoded using "replace" before writing them.
+class SafeUnbuffered:
+ def __init__(self, stream):
+ self.stream = stream
+ self.encoding = stream.encoding
+ if self.encoding == None:
+ self.encoding = "utf-8"
+ def write(self, data):
+ if isinstance(data,str) or isinstance(data,unicode):
+ # str for Python3, unicode for Python2
+ data = data.encode(self.encoding,"replace")
try:
- name, ccn = i.split(',')
- # Generate Barnes & Noble EPUB user key from name and credit card number.
- userkeys.append(generate_key(name, ccn))
+ buffer = getattr(self.stream, 'buffer', self.stream)
+ # self.stream.buffer for Python3, self.stream for Python2
+ buffer.write(data)
+ buffer.flush()
except:
- pass
- return userkeys
+ # We can do nothing if a write fails
+ raise
+ def __getattr__(self, attr):
+ return getattr(self.stream, attr)
+
\ No newline at end of file
diff --git a/DeDRM_plugin/wineutils.py b/DeDRM_plugin/wineutils.py
index f48f4b1..ed4e653 100644
--- a/DeDRM_plugin/wineutils.py
+++ b/DeDRM_plugin/wineutils.py
@@ -5,7 +5,10 @@ __license__ = 'GPL v3'
# Standard Python modules.
import os, sys, re, hashlib, traceback
-from calibre_plugins.dedrm.__init__ import PLUGIN_NAME, PLUGIN_VERSION
+
+#@@CALIBRE_COMPAT_CODE@@
+
+from __init__ import PLUGIN_NAME, PLUGIN_VERSION
class NoWinePython3Exception(Exception):
@@ -74,7 +77,7 @@ def WineGetKeys(scriptpath, extension, wineprefix=""):
pyexec = WinePythonCLI(wineprefix)
except NoWinePython3Exception:
print('{0} v{1}: Unable to find python3 executable in WINEPREFIX="{2}"'.format(PLUGIN_NAME, PLUGIN_VERSION, wineprefix))
- return []
+ return [], []
basepath, script = os.path.split(scriptpath)
print("{0} v{1}: Running {2} under Wine".format(PLUGIN_NAME, PLUGIN_VERSION, script))
@@ -93,6 +96,7 @@ def WineGetKeys(scriptpath, extension, wineprefix=""):
# try finding winekeys anyway, even if above code errored
winekeys = []
+ winekey_names = []
# get any files with extension in the output dir
files = [f for f in os.listdir(outdirpath) if f.endswith(extension)]
for filename in files:
@@ -104,9 +108,10 @@ def WineGetKeys(scriptpath, extension, wineprefix=""):
else:
new_key_value = keyfile.read()
winekeys.append(new_key_value)
+ winekey_names.append(filename)
except:
print("{0} v{1}: Error loading file {2}".format(PLUGIN_NAME, PLUGIN_VERSION, filename))
traceback.print_exc()
os.remove(fpath)
print("{0} v{1}: Found and decrypted {2} {3}".format(PLUGIN_NAME, PLUGIN_VERSION, len(winekeys), "key file" if len(winekeys) == 1 else "key files"))
- return winekeys
+ return winekeys, winekey_names
diff --git a/DeDRM_plugin/zeroedzipinfo.py b/DeDRM_plugin/zeroedzipinfo.py
new file mode 100644
index 0000000..08c65d0
--- /dev/null
+++ b/DeDRM_plugin/zeroedzipinfo.py
@@ -0,0 +1,30 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+
+"""
+Python 3's "zipfile" has an annoying bug where the `external_attr` field
+of a ZIP file cannot be set to 0. However, if the original DRMed ZIP has
+that set to 0 then we want the DRM-free ZIP to have that as 0, too.
+See https://github.com/python/cpython/issues/87713
+
+We cannot just set the "external_attr" to 0 as the code to save the ZIP
+resets that variable.
+
+So, here's a class that inherits from ZipInfo and ensures that EVERY
+read access to that variable will return a 0 ...
+
+"""
+
+import zipfile
+
+class ZeroedZipInfo(zipfile.ZipInfo):
+ def __init__(self, zinfo):
+ for k in self.__slots__:
+ if hasattr(zinfo, k):
+ setattr(self, k, getattr(zinfo, k))
+
+ def __getattribute__(self, name):
+ if name == "external_attr":
+ return 0
+ return object.__getattribute__(self, name)
diff --git a/DeDRM_plugin/zipfilerugged.py b/DeDRM_plugin/zipfilerugged.py
index b2f3762..1941cc0 100755
--- a/DeDRM_plugin/zipfilerugged.py
+++ b/DeDRM_plugin/zipfilerugged.py
@@ -209,6 +209,7 @@ def _EndRecData(fpin):
fpin.seek(-sizeEndCentDir, 2)
except IOError:
return None
+
data = fpin.read()
if data[0:4] == stringEndArchive and data[-2:] == "\000\000":
# the signature is correct and there's no comment, unpack structure
@@ -393,6 +394,19 @@ class ZipInfo (object):
extra = extra[ln+4:]
+class ZeroedZipInfo(ZipInfo):
+ def __init__(self, zinfo):
+ for k in self.__slots__:
+ if hasattr(zinfo, k):
+ setattr(self, k, getattr(zinfo, k))
+
+ def __getattribute__(self, name):
+ if name == "external_attr":
+ return 0
+ return object.__getattribute__(self, name)
+
+
+
class _ZipDecrypter:
"""Class to handle decryption of files stored within a ZIP archive.
@@ -662,7 +676,8 @@ class ZipFile:
self.comment = b''
# Check if we were passed a file-like object
- if isinstance(file, str):
+ # "str" is python3, "unicode" is python2
+ if isinstance(file, str) or isinstance(file, unicode):
self._filePassed = 0
self.filename = file
modeDict = {'r' : 'rb', 'w': 'wb', 'a' : 'r+b'}
diff --git a/DeDRM_plugin/zipfix.py b/DeDRM_plugin/zipfix.py
index 54469d8..9cb4ff1 100644
--- a/DeDRM_plugin/zipfix.py
+++ b/DeDRM_plugin/zipfix.py
@@ -20,14 +20,13 @@ Re-write zip (or ePub) fixing problems with file names (and mimetype entry).
__license__ = 'GPL v3'
__version__ = "1.1"
-import sys
+import sys, os
+
+#@@CALIBRE_COMPAT_CODE@@
+
import zlib
-try:
- import zipfilerugged
-except:
- import calibre_plugins.dedrm.zipfilerugged as zipfilerugged
-import os
-import os.path
+import zipfilerugged
+from zipfilerugged import ZipInfo, ZeroedZipInfo
import getopt
from struct import unpack
@@ -38,12 +37,6 @@ _FILENAME_OFFSET = 30
_MAX_SIZE = 64 * 1024
_MIMETYPE = 'application/epub+zip'
-class ZipInfo(zipfilerugged.ZipInfo):
- def __init__(self, *args, **kwargs):
- if 'compress_type' in kwargs:
- compress_type = kwargs.pop('compress_type')
- super(ZipInfo, self).__init__(*args, **kwargs)
- self.compress_type = compress_type
class fixZip:
def __init__(self, zinput, zoutput):
@@ -119,7 +112,8 @@ class fixZip:
# if epub write mimetype file first, with no compression
if self.ztype == 'epub':
# first get a ZipInfo with current time and no compression
- mimeinfo = ZipInfo(b'mimetype',compress_type=zipfilerugged.ZIP_STORED)
+ mimeinfo = ZipInfo(b'mimetype')
+ mimeinfo.compress_type = zipfilerugged.ZIP_STORED
mimeinfo.internal_attr = 1 # text file
try:
# if the mimetype is present, get its info, including time-stamp
@@ -131,8 +125,16 @@ class fixZip:
mimeinfo.internal_attr = oldmimeinfo.internal_attr
mimeinfo.external_attr = oldmimeinfo.external_attr
mimeinfo.create_system = oldmimeinfo.create_system
+ mimeinfo.create_version = oldmimeinfo.create_version
+ mimeinfo.volume = oldmimeinfo.volume
except:
pass
+
+ # Python 3 has a bug where the external_attr is reset to `0o600 << 16`
+ # if it's NULL, so we need a workaround:
+ if mimeinfo.external_attr == 0:
+ mimeinfo = ZeroedZipInfo(mimeinfo)
+
self.outzip.writestr(mimeinfo, _MIMETYPE.encode('ascii'))
# write the rest of the files
@@ -147,13 +149,23 @@ class fixZip:
zinfo.filename = local_name
# create new ZipInfo with only the useful attributes from the old info
- nzinfo = ZipInfo(zinfo.filename, zinfo.date_time, compress_type=zinfo.compress_type)
+ nzinfo = ZipInfo(zinfo.filename)
+ nzinfo.date_time = zinfo.date_time
+ nzinfo.compress_type = zinfo.compress_type
nzinfo.comment=zinfo.comment
nzinfo.extra=zinfo.extra
nzinfo.internal_attr=zinfo.internal_attr
nzinfo.external_attr=zinfo.external_attr
nzinfo.create_system=zinfo.create_system
+ nzinfo.create_version = zinfo.create_version
+ nzinfo.volume = zinfo.volume
nzinfo.flag_bits = zinfo.flag_bits & 0x800 # preserve UTF-8 flag
+
+ # Python 3 has a bug where the external_attr is reset to `0o600 << 16`
+ # if it's NULL, so we need a workaround:
+ if nzinfo.external_attr == 0:
+ nzinfo = ZeroedZipInfo(nzinfo)
+
self.outzip.writestr(nzinfo,data)
self.bzf.close()
diff --git a/DeDRM_plugin_ReadMe.txt b/DeDRM_plugin_ReadMe.txt
index 96a6516..91e1d28 100644
--- a/DeDRM_plugin_ReadMe.txt
+++ b/DeDRM_plugin_ReadMe.txt
@@ -4,10 +4,10 @@ DeDRM_plugin.zip
This plugin will remove the DRM from:
- Kindle ebooks (files from Kindle for Mac/PC and eInk Kindles).
- - Adobe Digital Editions (v2.0.1***) ePubs (including Kobo and Google ePubs downloaded to ADE)
- - Adobe Digital Editions (v2.0.1) PDFs
+ - Adobe Digital Editions ePubs (including Kobo and Google ePubs downloaded to ADE)
+ - Adobe Digital Editions PDFs
-For limitations and work-arounds, see the FAQ at https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md
+For limitations and work-arounds, see the FAQ at https://github.com/noDRM/DeDRM_tools/blob/master/FAQs.md (or the FAQ in Apprentice Harper's original repository at https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md)
Installation
@@ -31,4 +31,6 @@ If you find that the DeDRM plugin is not working for you (imported ebooks still
- Once calibre has re-started, import the problem ebook.
- Now close calibre.
-A log will appear that you can copy and paste into a comment at Apprentice Alf's blog, http://apprenticealf.wordpress.com/ or an issue at Apprentice Harper's repository, https://github.com/apprenticeharper/DeDRM_tools/issues . You should also give details of your computer, and how you obtained the ebook file.
+A log will appear that you can copy and paste into a GitHub issue report at https://github.com/noDRM/DeDRM_tools/issues. Please also include information about the eBook file.
+
+If you're using Apprentice Harper's original version, you can also comment at Apprentice Alf's blog, http://apprenticealf.wordpress.com/ or open an issue at Apprentice Harper's repository, https://github.com/apprenticeharper/DeDRM_tools/issues.
diff --git a/FAQs.md b/FAQs.md
index 09bf19c..c5c43a0 100644
--- a/FAQs.md
+++ b/FAQs.md
@@ -9,19 +9,16 @@ DRM ("Digital Rights Management") is a way of using encryption to tie the books
When your ebooks have DRM you are unable to convert the ebook from one format to another (e.g. Kindle KF8 to Kobo ePub), so you are restricted in the range of ebook stores you can use. DRM also allows publishers to restrict what you can do with the ebook you've bought, e.g. preventing the use of text-to-speech software. Longer term, you can never be sure that you'll be able to come back and re-read your ebooks if they have DRM, even if you save back-up copies.
## So how can I remove DRM from my ebooks?
-Just download and use these tools, that's all! Uh, almost. There are a few, uh, provisos, a, a couple of quid pro quos.
+Just download and use these tools, that's all! Uh, almost. There are a few, uh, provisos, a couple of quid pro quos.
* The tools don't work on all ebooks. For example, they don't work on any ebooks from Apple's iBooks store.
* You must own the ebook - the tools won't work on library ebooks or rented ebooks or books from a friend.
* You must not use these tools to give your ebooks to a hundred of your closest friends. Or to a million strangers. Authors need to sell books to be able to write more books. Don't be mean to the authors.
-* Do NOT use Adobe Digital Editions 3.0 or later to download your ePubs. ADE 3.0 and later might use a new encryption scheme that the tools can't handle. While major ebook stores aren't using the new scheme yet, using ADE 2.0.1 will ensure that your ebooks are downloaded using the old scheme. Once a book has been downloaded with the new scheme, it's IMPOSSIBLE to re-download using the old scheme (without buying it again).
-
-But otherwise, if your ebook is from Amazon, Kobo, Barnes & Noble or any of the ebook stores selling ebooks compatible with Adobe Digital Editions 2.0.1, you should be able to remove the DRM that's been applied to your ebooks.
### Recent Changes to Kindle for PC/Kindle for Mac
Starting with version 1.19, Kindle for PC/Mac uses Amazon's new KFX format which isn't quite as good a source for conversion to ePub as the older KF8 (& MOBI) formats. There are two options to get the older formats. Either stick with version 1.17 or earlier, or modify the executable by changing a file name (PC) or disabling a component of the application (Mac).
-Version 1.17 of Kindle is no longer available directly from Amazon, so you will need to search for the proper file name and find it on a third party site. The name is `KindleForPC-installer-1.17.44170.exe` for PC and `KindleForMac-44182.dmg` for Mac.
+Version 1.17 of Kindle is no longer available directly from Amazon, so you will need to search for the proper file name and find it on a third party site. The name is `KindleForPC-installer-1.17.44170.exe` for PC and `KindleForMac-44182.dmg` for Mac. (Note that this is a 32-bit application on the Mac, so will not work on Catalina and newer versions of macOS.)
Verify the one of the following cryptographic hash values, using software of your choice, before installing the downloaded file in order to avoid viruses. If the hash does not match, delete the downloaded file and try again from another site.
#### Kindle for PC `KindleForPC-installer-1.17.44170.exe`:
@@ -34,7 +31,7 @@ Verify the one of the following cryptographic hash values, using software of you
* SHA-1: 7AB9A86B954CB23D622BD79E3257F8E2182D791C
* SHA-256: 28DC21246A9C7CDEDD2D6F0F4082E6BF7EF9DB9CE9D485548E8A9E1D19EAE2AC
-You will need to go to the preferences and uncheck the auto update checkbox. Then download and install 1.17 over the top of the newer installation. You'll also need to delete the KFX folders from your My Kindle Content folder. You may also need to take further action to prevent an auto update. The simplest wayis to find the 'updates' folder and replace it with a file. See [this thread] (http://www.mobileread.com/forums/showthread.php?t=283371) at MobileRead for a Script to do this on a PC. On a Mac you can find the folder at ~/Library/Application Support/Kindle/ just delete the folder 'updates' and save a blank text file called 'updates' in its place.
+You will need to go to the preferences and uncheck the auto update checkbox. Then download and install 1.17 over the top of the newer installation. You'll also need to delete the KFX folders from your My Kindle Content folder. You may also need to take further action to prevent an auto update. The simplest way is to find the 'updates' folder and replace it with a file. See [this thread] (http://www.mobileread.com/forums/showthread.php?t=283371) at MobileRead for a Script to do this on a PC. On a Mac you can find the folder at ~/Library/Application Support/Kindle/. Make the 'updates' folder read-only, or delete it and save a blank text file called 'updates' in its place.
Another possible solution is to use 1.19 or later, but disable KFX by renaming or disabling a necessary component of the application. This may or may not work on versions after 1.25. In a command window, enter the following commands when Kindle for PC/Mac is not running:
@@ -46,9 +43,9 @@ PC Note: The renderer-test program may be in a different location in some Kindle
#### Macintosh
`chmod -x /Applications/Kindle.app/Contents/MacOS/renderer-test`
-Mac Note: If the chmod command fails with a permission error try again using `sudo` before `chmod` - `sudo chmod` [...]
+Mac Note: If the chmod command fails with a permission error try again using `sudo` before `chmod` - `sudo chmod` [...]. This only works on Kindle for Mac 1.19 thru 1.31, it does NOT work with 1.32 or newer.
-After restarting the Kindle program any books previously downloaded in KFX format will no longer open. You will need to remove them from your device and re-download them. All future downloads will use the older Kindle formats instead of KFX although they will continue to be placed in one individual subdirectory per book. Note that books soudl be downoad by right-click and 'Download', not by just opening the book. Recent (1.25+) versions of Kindle for Mac/PC may convert KF8 files to a new format that is not supported by these tools when the book is opened for reading.
+After restarting the Kindle program any books previously downloaded in KFX format will no longer open. You will need to remove them from your device and re-download them. All future downloads will use the older Kindle formats instead of KFX although they will continue to be placed in one individual subdirectory per book. Note that books should be downloaded by right-click and 'Download', not by just opening the book. Recent (1.25+) versions of Kindle for Mac/PC may convert KF8 files to a new format that is not supported by these tools when the book is opened for reading.
#### Decrypting KFX
Thanks to work by several people, the tools can now decrypt KFX format ebooks from Kindle for Mac/PC. In addition to the DeDRM plugin, calibre users will also need to install jhowell's KFX Input plugin which is available through the standard plugin menu in calibre, or directly from [his plugin thread](https://www.mobileread.com/forums/showthread.php?t=291290) on Mobileread.
@@ -59,18 +56,18 @@ It's quite possible that Amazon will update their KFX DeDRM to prevent DRM remov
Thanks to jhowell for his investigations into KFX format and the KFX Input plugin. Some of these instructions are from [his thread on the subject](https://www.mobileread.com/forums/showthread.php?t=283371) at MobileRead.
## Where can I get the latest version of these free DRM removal tools?
-Right here at github. Just go to the [releases page](https://github.com/apprenticeharper/DeDRM_tools/releases) and download the latest zip archive of the tools, named `DeDRM\_tools\_X.X.X.zip`, where X.X.X is the version number. You do not need to download the source code archive.
+Right here at github. Just go to the [releases page](https://github.com/noDRM/DeDRM_tools/releases) and download the latest zip archive of the tools, named `DeDRM\_tools\_X.X.X.zip`, where X.X.X is the version number. You do not need to download the source code archive. This will get you the forked version by noDRM. If you want to download the original version by Apprentice Harper, go to [this page](https://github.com/apprenticeharper/DeDRM_tools/releases) instead.
## I've downloaded the tools archive. Now what?
-First, unzip the archive. You should now have a DeDRM folder containing several other folders and a `ReadMe_Overview.txt` file. Please read the `ReadMe_Overview.txt` file! That will explain what the folders are, and you'll be able to work out which of the tools you need.
+First, unzip the archive. You should now have a DeDRM folder containing several files, including a `ReadMe_Overview.txt` file. Please read the `ReadMe_Overview.txt` file! That will explain what the files are, and you'll be able to work out which of the tools you need.
## That's a big complicated ReadMe file! Isn't there a quick guide?
Install calibre. Install the DeDRM\_plugin in calibre. Install the Obok\_plugin in calibre. Restart calibre. In the DeDRM_plugin customisation dialog add in any E-Ink Kindle serial numbers. Remember that the plugin only tries to remove DRM when ebooks are imported.
# Installing the Tools
## The calibre plugin
-### I am trying to install the calibre plugin, but calibre says "ERROR: Unhandled exception: InvalidPlugin: The plugin in u’[path]DeDRM\_tools\_6.5.3.zip’ is invalid. It does not contain a top-level \_\_init\_\_.py file"
-You are trying to add the tools archive (e.g. `DeDRM_tools_6.8.0.zip`) instead of the plugin. The tools archive is not the plugin. It is a collection of DRM removal tools which includes the plugin. You must unzip the archive, and install the calibre plugin `DeDRM_plugin.zip` from a folder called `DeDRM_calibre_plugin` in the unzipped archive.
+### I am trying to install the calibre plugin, but calibre says "ERROR: Unhandled exception: InvalidPlugin: The plugin in '[path]DeDRM\_tools\_X.X.X.zip' is invalid. It does not contain a top-level \_\_init\_\_.py file"
+You are trying to add the tools archive (e.g. `DeDRM_tools_10.0.2.zip`) instead of the plugin. The tools archive is not the plugin. It is a collection of DRM removal tools which includes the plugin. You must unzip the archive, and install the calibre plugin `DeDRM_plugin.zip` from inside the unzipped archive.
### I’ve unzipped the tools archive, but I can’t find the calibre plugin when I try to add them to calibre. I use Windows.
You should select the zip file that is in the `DeDRM_calibre_plugin` folder, not any files inside the plugin’s zip archive. Make sure you are selecting from the folder that you created when you unzipped the tools archive and not selecting a file inside the still-zipped tools archive.
@@ -95,7 +92,7 @@ Your ebooks are stored on your computer or on your ebook reader. You need to fin
### Macintosh
Navigating from your home folder,
-Kindle for Mac ebooks are in either `Library/Application Support/Kindle/My Kindle Content` or `Documents/My Kindle Content or Library/Containers/com.amazon.Kindle/Data/Library/Application Support/Kindle/My Kindle Content`, depending on your version of Kindle for Mac.
+Kindle for Mac ebooks are in either `Library/Application Support/Kindle/My Kindle Content` or `Documents/My Kindle Content` or `Library/Containers/com.amazon.Kindle/Data/Library/Application Support/Kindle/My Kindle Content`, depending on your version of Kindle for Mac.
Adobe Digital Editions ebooks are in `Documents/Digital Editions`
@@ -120,21 +117,20 @@ If this book is from Kindle for Mac or Kindle for PC, you must have the Kindle S
If the book is from Kindle for PC or Kindle for Mac and you think you are doing everything right, and you are getting this message, it is possible that the files containing the encryption key aren’t quite in the format the tools expect. To try to fix this:
-1. Deregister Kindle for PC(Mac) from your Amazon account.
-1. Uninstall Kindle for PC(Mac)
-1. Delete the Kindle for PC(Mac) preferences
+1. Deregister Kindle for PC/Mac from your Amazon account.
+1. Uninstall Kindle for PC/Mac
+1. Delete the Kindle for PC/Mac preferences
* PC: Delete the directory `[home folder]\AppData\Local\Amazon` (it might be hidden) and `[home folder]\My Documents\My Kindle Content`
* Mac: Delete the directory `[home folder]/Library/Application Support/Kindle/` and/or `[home folder]/Library/Containers/com.amazon.Kindle/Data/Library/Application Support/Kindle/` (one or both may be present and should be deleted)
-1. Reinstall Kindle for PC(Mac) version 1.17 or earlier (see above for download links).
-1. Re-register Kindle for PC(Mac) with your Amazon account
+1. Reinstall Kindle for PC/Mac version 1.17 or earlier (see above for download links).
+1. Re-register Kindle for PC/Mac with your Amazon account
1. Download the ebook again. Do not use the files you have downloaded previously.
## Some of my books had their DRM removed, but some still say that they have DRM and will not convert.
There are several possible reasons why only some books get their DRM removed.
* You still don’t have the DRM removal tools working correctly, but some of your books didn’t have DRM in the first place.
-* Kindle only: It is a Topaz format book and contains some coding that the tools do not understand. You will need to get a log of the DeDRM attempt, and then create a [new issue at Apprentice Harper's github repository](https://github.com/apprenticeharper/DeDRM_tools/issues/), attaching the book and the log, so that the tools can be updated.
-If you are still having problems with particular books, you will need to create a log of the DRM removal attempt for one of the problem books, and post that in a comment at Apprentice Alf's blog or in a new issue at Apprentice Harper's github repository.
+If you are still having problems with particular books, you will need to create a log of the DRM removal attempt for one of the problem books. If you're using NoDRM's fork, open [a new issue](https://github.com/noDRM/DeDRM_tools/issues) in the GitHub repo. If you're using Apprentice Harper's version, post that logfile in a new issue at [Apprentice Harper's github repository](https://github.com/apprenticeharper/DeDRM_tools/issues).
## My Kindle book has imported and the DRM has been removed, but all the pictures are gone.
Most likely, this is a book downloaded from Amazon directly to an eInk Kindle (e.g. Paperwhite). Unfortunately, the pictures are probably in a `.azw6` file that the tools don't understand. You must download the book manually from Amazon's web site "For transfer via USB" to your Kindle. When you download the eBook in this manner, Amazon will package the pictures in the with text in a single file that the tools will be able to import successfully.
@@ -145,33 +141,32 @@ You have found a Print Replica Kindle ebook. This is a PDF in a Kindle wrapper.
## Do the tools work on books from Kobo?
If you use the Kobo desktop application for Mac or PC, install the Obok plugin. This will import and remove the DRM from your Kobo books, and is the easiest method for Kobo ebooks.
-## I registered Adobe Digital Editions 3.0 or later with an Adobe ID before downloading, but my epub or PDF still has DRM.
-Adobe introduced a new DRM scheme with ADE 3.0 and later. Install ADE 2.0.1 and register with the same Adobe ID. If you can't open your book in ADE 2.01, then you have a book with the new DRM scheme. These tools can't help. You can avoid the new DRM scheme by always downloading your ebooks with ADE 2.0.1. Some retailers will require ADE 3.0 or later, in which case you won't be able to download with ADE 2.0.1.
-
-## The DRM wasn't removed and the log says "Failed to decrypt with error: Cannot decode library or rented ebooks." What now?
-You're trying to remove the DRM from an ebook that's only on loan to you. No help will be given to remove DRM from such ebooks. If you think that you have received this message for a book you own, please create an issue at github, or comment at the blog.
-
## I cannot solve my problem with the DeDRM plugin, and now I need to ‘post a log’. How do I do that?
-Remove the DRMed book from calibre. Click the Preferences drop-down menu and choose 'Restart in debug mode'. Once calibre has re-started, import the problem ebook. Now close calibre. A log will appear that you can copy and paste into a comment at Apprentice Alf's blog, or into a new issue at Apprentice Harper's github repository.
+Remove the DRMed book from calibre. Click the Preferences drop-down menu and choose 'Restart in debug mode'. Once calibre has re-started, import the problem ebook. Now close calibre. A log will appear that you can copy and paste into [a new issue](https://github.com/noDRM/DeDRM_tools/issues) in NoDRM’s GitHub repo. If you're using Apprentice Harper’s version, post that logfile in a new issue at [Apprentice Harper's GitHub repository](https://github.com/apprenticeharper/DeDRM_tools/issues).
## Is there a way to use the DeDRM plugin for Calibre from the command line?
See the [Calibre command line interface (CLI) instructions](CALIBRE_CLI_INSTRUCTIONS.md).
+## The plugin displays a "MemoryError" in its log file during DRM removal.
+A "MemoryError" usually occurs when you're using the 32-bit version of Calibre (which is limited in the amount of useable RAM). If you have a 64-bit installation of your operating system (on Windows, press Windows+Break, then make sure it says "64-bit Operating System" under "System type"), try downloading the 64-bit version of Calibre instead of the 32-bit version.
+
+If the error still occurs, even with the 64-bit version, please open a bug report.
+
# General Questions
## Once the DRM has been removed, is there any trace of my personal identity left in the ebook?
-The tools only remove the DRM. No attempt is made to remove any personally identifying information.
+That question cannot be answered for sure. While it is easy to check if a book has DRM or not, it is very difficult to verify if all (traces of) personal information have been removed from a book. The tools attempt to remove watermarks when they are detected (optionally, there's an option in the plugin settings to enable that), but that will not be the case for all watermarks.
## Why do some of my Kindle ebooks import as HTMLZ format in calibre?
Most Amazon Kindle ebooks are Mobipocket format ebooks, or the new KF8 format. However, some are in a format known as Topaz. The Topaz format is only used by Amazon. A Topaz ebook is a collections of glyphs and their positions on each page tagged with some additional information from that page including OCRed text (Optical Character Recognition generated Text) to allow searching, and some additional layout information. Each page of a Topaz ebook is effectively a description of an image of that page. To convert a Topaz ebook to another format is not easy as there is not a one-to-one mapping between glyphs and characters/fonts. To account for this, two different formats are generated by the DRM removal software. The first is an html description built from the OCRtext and images stored in the Topaz file (HTMLZ). This format is easily reflowed but may suffer from typical OCRtext errors including typos, garbled text, missing italics, missing bolds, etc. The second format uses the glyph and position information to create an accurate scalable vector graphics (SVG) image of each page of the book that can be viewed in web browsers that support svg images (Safari, Firefox 4 or later, etc). Additional conversion software can be used to convert these SVG images to an image only PDF file. The DeDRM calibre plugin only imports the HTMLZ versions of the Topaz ebook. The html version can be manually cleaned up and spell checked and then converted using Sigil/calibre to epubs, mobi ebooks, and etc.
## Are the tools open source? How can I be sure they are safe and not a trojan horse?
-All the DRM removal tools hosted here are almost entirely scripts of one kind or another: Python, Applescript or Windows Batch files. So they are inherently open source, and open to inspection by everyone who downloads them.
+All the DRM removal tools hosted here are almost entirely written in Python. So they are inherently open source, and open to inspection by everyone who downloads them.
-There are some optional shared libraries (`*.dll`, `*.dylib`, and `*.so`) included for performance. The source for any compiled pieces are provided within `alfcrypto_src.zip`. If this is a concern either delete the binary files or manually rebuild them.
+There are some optional shared libraries (`*.dll`, `*.dylib`, and `*.so`) included for performance. The source for any compiled pieces are provided within `alfcrypto_src.zip`. If this is a concern either delete the binary files (there's fallback code in the plugin that allows it to work without these, it will just be slower) or manually rebuild them from source.
## What ebooks do these tools work on?
-The tools linked from this blog remove DRM from PDF, ePub, kePub (Kobo), eReader, Kindle (Mobipocket, KF8, Print Replica and Topaz) format ebooks using Adobe Adept, Barnes & Noble, Amazon, Kobo and eReader DRM schemes.
+The Calibre plugin removes DRM from PDF, ePub, kePub (Kobo), eReader, Kindle (Mobipocket, KF8, Print Replica and Topaz) format ebooks using Adobe Adept, Barnes & Noble, Amazon, Kobo and eReader DRM schemes. It used to remove Readium LCP DRM from ePub or PDF files in the past, but that functionality had to be removed due to a [DMCA takedown request](https://github.com/noDRM/DeDRM_tools/issues/18).
Note these tools do NOT ‘crack’ the DRM. They simply allow the book’s owner to use the encryption key information already stored someplace on their computer or device to decrypt the ebook in the same manner the official ebook reading software uses.
@@ -187,14 +182,19 @@ Amazon turned off backup for Kindle for Android, so the tools can no longer find
## Why don't the tools work on books from the Apple iBooks Store?
Apple regularly change the details of their DRM and so the tools in the main tools archive will not work with these ebooks. Apple’s Fairplay DRM scheme can be removed using Requiem if the appropriate version of iTunes can still be installed and used. See the post Apple and ebooks: iBookstore DRM and how to remove it at Apprentice Alf's blog for more details.
+## Why don't the tools work with LCP-encrypted ebooks? / Error message about a "DMCA takedown"
+Support for LCP DRM removal was included in the past, but Readium (the company who developed that particular DRM) has decided to [open a DMCA takedown request](https://github.com/github/dmca/blob/master/2022/01/2022-01-04-readium.md) in January 2022. This means that for legal reasons, this GitHub repository no longer contains the code needed to remove DRM from LCP-encrypted books. For more information please read [this bug report](https://github.com/noDRM/DeDRM_tools/issues/18).
+
## I’ve got the tools archive and I’ve read all the FAQs but I still can’t install the tools and/or the DRM removal doesn’t work
* Read the `ReadMe_Overview.txt` file in the top level of the tools archive
* Read the ReadMe file for the tool you want to use.
-* If you still can’t remove the DRM, ask in the comments section of Apprentice Alf's blog or create a new issue at Apprentice Harper's github repository, reporting the error as precisely as you can, what platform you use, what tool you have tried, what errors you get, and what versions you are using. If the problem happens when running one of the tools, post a log (see previous questions on how to do this).
+* If you still can’t remove the DRM, create a new [GitHub issue](https://github.com/noDRM/DeDRM_tools/issues). If you are using Apprentice Harper's original version and not this fork, you can also create a new issue at Apprentice Harper's github repository. If you do report an issue in any of the GitHub repositories, please report the error as precisely as you can. Include what platform you use, what tool you have tried, what errors you get, and what versions you are using. If the problem happens when running one of the tools, post a log (see previous questions on how to do this).
## Who wrote these scripts?
The authors tend to identify themselves only by pseudonyms:
* The Adobe Adept and Barnes & Noble scripts were created by i♥cabbages
+* The Adobe Adept support for ADE3.0+ DRM was added by a980e066a01
+* ~The Readium LCP support for this plugin was created by NoDRM~ (removed due to a DMCA takedown, see [#18](https://github.com/noDRM/DeDRM_tools/issues/18) )
* The Amazon Mobipocket and eReader scripts were created by The Dark Reverser
* The Amazon K4PC DRM/format was further decoded by Bart Simpson aka Skindle
* The Amazon K4 Mobi tool was created by by some_updates, mdlnx and others
diff --git a/Obok_plugin/__init__.py b/Obok_plugin/__init__.py
index 60daad3..f752d5c 100644
--- a/Obok_plugin/__init__.py
+++ b/Obok_plugin/__init__.py
@@ -3,7 +3,7 @@ from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
-__version__ = '7.2.1'
+__version__ = '10.0.3'
__docformat__ = 'restructuredtext en'
#####################################################################
@@ -20,7 +20,7 @@ except NameError:
PLUGIN_NAME = 'Obok DeDRM'
PLUGIN_SAFE_NAME = PLUGIN_NAME.strip().lower().replace(' ', '_')
PLUGIN_DESCRIPTION = _('Removes DRM from Kobo kepubs and adds them to the library.')
-PLUGIN_VERSION_TUPLE = (7, 2, 1)
+PLUGIN_VERSION_TUPLE = (10, 0, 3)
PLUGIN_VERSION = '.'.join([str(x) for x in PLUGIN_VERSION_TUPLE])
HELPFILE_NAME = PLUGIN_SAFE_NAME + '_Help.htm'
PLUGIN_AUTHORS = 'Anon'
diff --git a/Obok_plugin/action.py b/Obok_plugin/action.py
index 1a4515b..e4ef377 100644
--- a/Obok_plugin/action.py
+++ b/Obok_plugin/action.py
@@ -237,7 +237,10 @@ class InterfacePluginAction(InterfaceAction):
:param books_to_add: List of calibre bookmaps (created in get_decrypted_kobo_books)
'''
- added = self.db.add_books(books_to_add, add_duplicates=False, run_hooks=False)
+
+ cfg_add_duplicates = (cfg['finding_homes_for_formats'] == 'Add new entry')
+
+ added = self.db.add_books(books_to_add, add_duplicates=cfg_add_duplicates, run_hooks=False)
if len(added[0]):
# Record the id(s) that got added
for id in added[0]:
@@ -375,7 +378,6 @@ class InterfacePluginAction(InterfaceAction):
#print ('Kobo library filename: {0}'.format(book.filename))
for userkey in self.userkeys:
print (_('Trying key: '), codecs.encode(userkey, 'hex'))
- check = True
try:
fileout = PersistentTemporaryFile('.epub', dir=self.tdir)
#print ('Temp file: {0}'.format(fileout.name))
@@ -396,8 +398,7 @@ class InterfacePluginAction(InterfaceAction):
file = book.encryptedfiles[filename]
contents = file.decrypt(userkey, contents)
# Parse failures mean the key is probably wrong.
- if check:
- check = not file.check(contents)
+ file.check(contents)
zout.writestr(filename, contents)
zout.close()
zin.close()
diff --git a/Obok_plugin/common_utils.py b/Obok_plugin/common_utils.py
index 21b7f19..fe67f42 100644
--- a/Obok_plugin/common_utils.py
+++ b/Obok_plugin/common_utils.py
@@ -265,13 +265,13 @@ class ReadOnlyTableWidgetItem(QTableWidgetItem):
def __init__(self, text):
if text is None:
text = ''
- QTableWidgetItem.__init__(self, text, QTableWidgetItem.UserType)
+ QTableWidgetItem.__init__(self, text, QTableWidgetItem.ItemType.UserType)
self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)
class RatingTableWidgetItem(QTableWidgetItem):
def __init__(self, rating, is_read_only=False):
- QTableWidgetItem.__init__(self, '', QTableWidgetItem.UserType)
+ QTableWidgetItem.__init__(self, '', QTableWidgetItem.ItemType.UserType)
self.setData(Qt.DisplayRole, rating)
if is_read_only:
self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)
@@ -284,11 +284,11 @@ class DateTableWidgetItem(QTableWidgetItem):
if date_read is None or date_read == UNDEFINED_DATE and default_to_today:
date_read = now()
if is_read_only:
- QTableWidgetItem.__init__(self, format_date(date_read, fmt), QTableWidgetItem.UserType)
+ QTableWidgetItem.__init__(self, format_date(date_read, fmt), QTableWidgetItem.ItemType.UserType)
self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)
self.setData(Qt.DisplayRole, QDateTime(date_read))
else:
- QTableWidgetItem.__init__(self, '', QTableWidgetItem.UserType)
+ QTableWidgetItem.__init__(self, '', QTableWidgetItem.ItemType.UserType)
self.setData(Qt.DisplayRole, QDateTime(date_read))
from calibre.gui2.library.delegates import DateDelegate as _DateDelegate
diff --git a/Obok_plugin/config.py b/Obok_plugin/config.py
index 2830dad..fdfb424 100644
--- a/Obok_plugin/config.py
+++ b/Obok_plugin/config.py
@@ -39,8 +39,13 @@ class ConfigWidget(QWidget):
self.find_homes = QComboBox()
self.find_homes.setToolTip(_('Default behavior when duplicates are detected. None of the choices will cause calibre ebooks to be overwritten'))
layout.addWidget(self.find_homes)
- self.find_homes.addItems([_('Ask'), _('Always'), _('Never')])
+
+ self.find_homes.addItems([_('Ask'), _('Always'), _('Never'), _('Add new entry')])
+
index = self.find_homes.findText(plugin_prefs['finding_homes_for_formats'])
+ if index == -1:
+ index = self.find_homes.findText(_(plugin_prefs['finding_homes_for_formats']))
+
self.find_homes.setCurrentIndex(index)
self.serials_button = QtGui.QPushButton(self)
@@ -62,14 +67,31 @@ class ConfigWidget(QWidget):
def edit_kobo_directory(self):
- tmpkobodirectory = QFileDialog.getExistingDirectory(self, "Select Kobo directory", self.kobodirectory or "/home", QFileDialog.ShowDirsOnly)
+ tmpkobodirectory = QFileDialog.getExistingDirectory(self, "Select Kobo directory", self.kobodirectory or "/home", QFileDialog.Option.ShowDirsOnly)
if tmpkobodirectory != u"" and tmpkobodirectory is not None:
self.kobodirectory = tmpkobodirectory
def save_settings(self):
- plugin_prefs['finding_homes_for_formats'] = self.find_homes.currentText()
+
+
+ # Make sure the config file string is *always* english.
+ find_homes = None
+ if self.find_homes.currentText() == _('Ask'):
+ find_homes = 'Ask'
+ elif self.find_homes.currentText() == _('Always'):
+ find_homes = 'Always'
+ elif self.find_homes.currentText() == _('Never'):
+ find_homes = 'Never'
+ elif self.find_homes.currentText() == _('Add new entry'):
+ find_homes = 'Add new entry'
+
+ if find_homes is None:
+ # Fallback
+ find_homes = self.find_homes.currentText()
+
+ plugin_prefs['finding_homes_for_formats'] = find_homes
plugin_prefs['kobo_serials'] = self.tmpserials
plugin_prefs['kobo_directory'] = self.kobodirectory
@@ -118,7 +140,13 @@ class ManageKeysDialog(QDialog):
self._delete_key_button.clicked.connect(self.delete_key)
button_layout.addWidget(self._delete_key_button)
- spacerItem = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
+ try:
+ # QT 6
+ spacerItem = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Policy.Minimum, QtGui.QSizePolicy.Policy.Expanding)
+ except AttributeError:
+ # QT 5
+ spacerItem = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
+
button_layout.addItem(spacerItem)
layout.addSpacing(5)
@@ -204,7 +232,7 @@ class AddSerialDialog(QDialog):
def accept(self):
if len(self.key_name) == 0 or self.key_name.isspace():
- errmsg = "Please enter an eInk Kindle Serial Number or click Cancel in the dialog."
+ errmsg = "Please enter an eInk Kobo Serial Number or click Cancel in the dialog."
return error_dialog(None, "{0} {1}".format(PLUGIN_NAME, PLUGIN_VERSION), errmsg, show=True, show_copy_button=False)
if len(self.key_name) != 13:
errmsg = "EInk Kobo Serial Numbers must be 13 characters long. This is {0:d} characters long.".format(len(self.key_name))
diff --git a/Obok_plugin/dialogs.py b/Obok_plugin/dialogs.py
index 85abfaf..16bfcb0 100644
--- a/Obok_plugin/dialogs.py
+++ b/Obok_plugin/dialogs.py
@@ -409,7 +409,7 @@ class ReadOnlyTableWidgetItem(QTableWidgetItem):
def __init__(self, text):
if text is None:
text = ''
- QTableWidgetItem.__init__(self, text, QTableWidgetItem.UserType)
+ QTableWidgetItem.__init__(self, text, QTableWidgetItem.ItemType.UserType)
self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)
class AuthorTableWidgetItem(ReadOnlyTableWidgetItem):
@@ -448,7 +448,7 @@ class IconWidgetItem(ReadOnlyTableWidgetItem):
class NumericTableWidgetItem(QTableWidgetItem):
def __init__(self, number, is_read_only=False):
- QTableWidgetItem.__init__(self, '', QTableWidgetItem.UserType)
+ QTableWidgetItem.__init__(self, '', QTableWidgetItem.ItemType.UserType)
self.setData(Qt.DisplayRole, number)
if is_read_only:
self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)
diff --git a/Obok_plugin/obok/legacy_obok.py b/Obok_plugin/obok/legacy_obok.py
index caf24f2..9a9ae5d 100644
--- a/Obok_plugin/obok/legacy_obok.py
+++ b/Obok_plugin/obok/legacy_obok.py
@@ -42,7 +42,10 @@ class legacy_obok(object):
pwsdid = ''
try:
if sys.platform.startswith('win'):
- import winreg
+ try:
+ import winreg
+ except ImportError:
+ import _winreg as winreg
regkey_browser = winreg.OpenKey(winreg.HKEY_CURRENT_USER, 'Software\\Kobo\\Kobo Desktop Edition\\Browser')
cookies = winreg.QueryValueEx(regkey_browser, 'cookies')
bytearrays = cookies[0]
diff --git a/Obok_plugin/obok/obok.py b/Obok_plugin/obok/obok.py
index ba480ec..4de7644 100644
--- a/Obok_plugin/obok/obok.py
+++ b/Obok_plugin/obok/obok.py
@@ -1,9 +1,19 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
+# Version 10.0.3 July 2022
+# Fix Calibre 6
+#
+# Version 10.0.1 February 2022
+# Remove OpenSSL support to only support PyCryptodome; clean up the code.
+#
+# Version 10.0.0 November 2021
+# Merge https://github.com/apprenticeharper/DeDRM_tools/pull/1691 to fix
+# key fetch issues on some machines.
+#
# Version 4.1.0 February 2021
# Add detection for Kobo directory location on Linux
-
+#
# Version 4.0.0 September 2020
# Python 3.0
#
@@ -158,8 +168,8 @@
"""Manage all Kobo books, either encrypted or DRM-free."""
from __future__ import print_function
-__version__ = '4.0.0'
-__about__ = "Obok v{0}\nCopyright © 2012-2020 Physisticated et al.".format(__version__)
+__version__ = '10.0.1'
+__about__ = "Obok v{0}\nCopyright © 2012-2022 Physisticated et al.".format(__version__)
import sys
import os
@@ -176,6 +186,20 @@ import shutil
import argparse
import tempfile
+try:
+ from Cryptodome.Cipher import AES
+except ImportError:
+ from Crypto.Cipher import AES
+
+def unpad(data, padding=16):
+ if sys.version_info[0] == 2:
+ pad_len = ord(data[-1])
+ else:
+ pad_len = data[-1]
+
+ return data[:-pad_len]
+
+
can_parse_xml = True
try:
from xml.etree import ElementTree as ET
@@ -190,88 +214,6 @@ KOBO_HASH_KEYS = ['88b3a2e13', 'XzUhGYdFp', 'NoCanLook','QJhwzAtXL']
class ENCRYPTIONError(Exception):
pass
-def _load_crypto_libcrypto():
- from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_int, c_long, \
- Structure, c_ulong, create_string_buffer, cast
- from ctypes.util import find_library
-
- if sys.platform.startswith('win'):
- libcrypto = find_library('libeay32')
- else:
- libcrypto = find_library('crypto')
-
- if libcrypto is None:
- raise ENCRYPTIONError('libcrypto not found')
- libcrypto = CDLL(libcrypto)
-
- AES_MAXNR = 14
-
- c_char_pp = POINTER(c_char_p)
- c_int_p = POINTER(c_int)
-
- class AES_KEY(Structure):
- _fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))),
- ('rounds', c_int)]
- AES_KEY_p = POINTER(AES_KEY)
-
- def F(restype, name, argtypes):
- func = getattr(libcrypto, name)
- func.restype = restype
- func.argtypes = argtypes
- return func
-
- AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key',
- [c_char_p, c_int, AES_KEY_p])
- AES_ecb_encrypt = F(None, 'AES_ecb_encrypt',
- [c_char_p, c_char_p, AES_KEY_p, c_int])
-
- class AES(object):
- def __init__(self, userkey):
- self._blocksize = len(userkey)
- if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) :
- raise ENCRYPTIONError(_('AES improper key used'))
- return
- key = self._key = AES_KEY()
- rv = AES_set_decrypt_key(userkey, len(userkey) * 8, key)
- if rv < 0:
- raise ENCRYPTIONError(_('Failed to initialize AES key'))
-
- def decrypt(self, data):
- clear = b''
- for i in range(0, len(data), 16):
- out = create_string_buffer(16)
- rv = AES_ecb_encrypt(data[i:i+16], out, self._key, 0)
- if rv == 0:
- raise ENCRYPTIONError(_('AES decryption failed'))
- clear += out.raw
- return clear
-
- return AES
-
-def _load_crypto_pycrypto():
- from Crypto.Cipher import AES as _AES
- class AES(object):
- def __init__(self, key):
- self._aes = _AES.new(key, _AES.MODE_ECB)
-
- def decrypt(self, data):
- return self._aes.decrypt(data)
-
- return AES
-
-def _load_crypto():
- AES = None
- cryptolist = (_load_crypto_pycrypto, _load_crypto_libcrypto)
- for loader in cryptolist:
- try:
- AES = loader()
- break
- except (ImportError, ENCRYPTIONError):
- pass
- return AES
-
-AES = _load_crypto()
-
# Wrap a stream so that output gets flushed immediately
# and also make sure that any unicode strings get
# encoded using "replace" before writing them.
@@ -282,10 +224,17 @@ class SafeUnbuffered:
if self.encoding == None:
self.encoding = "utf-8"
def write(self, data):
- if isinstance(data,str):
+ if isinstance(data,str) or isinstance(data,unicode):
+ # str for Python3, unicode for Python2
data = data.encode(self.encoding,"replace")
- self.stream.buffer.write(data)
- self.stream.buffer.flush()
+ try:
+ buffer = getattr(self.stream, 'buffer', self.stream)
+ # self.stream.buffer for Python3, self.stream for Python2
+ buffer.write(data)
+ buffer.flush()
+ except:
+ # We can do nothing if a write fails
+ raise
def __getattr__(self, attr):
return getattr(self.stream, attr)
@@ -356,7 +305,10 @@ class KoboLibrary(object):
if (self.kobodir == u""):
if sys.platform.startswith('win'):
- import winreg
+ try:
+ import winreg
+ except ImportError:
+ import _winreg as winreg
if sys.getwindowsversion().major > 5:
if 'LOCALAPPDATA' in os.environ.keys():
# Python 2.x does not return unicode env. Use Python 3.x
@@ -417,6 +369,7 @@ class KoboLibrary(object):
olddb.close()
self.newdb.close()
self.__sqlite = sqlite3.connect(self.newdb.name)
+ self.__sqlite.text_factory = lambda b: b.decode("utf-8", errors="ignore")
self.__cursor = self.__sqlite.cursor()
self._userkeys = []
self._books = []
@@ -471,11 +424,18 @@ class KoboLibrary(object):
macaddrs = []
if sys.platform.startswith('win'):
c = re.compile('\s?(' + '[0-9a-f]{2}[:\-]' * 5 + '[0-9a-f]{2})(\s|$)', re.IGNORECASE)
- output = subprocess.Popen('wmic nic where PhysicalAdapter=True get MACAddress', shell=True, stdout=subprocess.PIPE, text=True).stdout
- for line in output:
- m = c.search(line)
- if m:
- macaddrs.append(re.sub("-", ":", m.group(1)).upper())
+ try:
+ output = subprocess.Popen('ipconfig /all', shell=True, stdout=subprocess.PIPE, text=True).stdout
+ for line in output:
+ m = c.search(line)
+ if m:
+ macaddrs.append(re.sub("-", ":", m.group(1)).upper())
+ except:
+ output = subprocess.Popen('wmic nic where PhysicalAdapter=True get MACAddress', shell=True, stdout=subprocess.PIPE, text=True).stdout
+ for line in output:
+ m = c.search(line)
+ if m:
+ macaddrs.append(re.sub("-", ":", m.group(1)).upper())
elif sys.platform.startswith('darwin'):
c = re.compile('\s(' + '[0-9a-f]{2}:' * 5 + '[0-9a-f]{2})(\s|$)', re.IGNORECASE)
output = subprocess.check_output('/sbin/ifconfig -a', shell=True, encoding='utf-8')
@@ -616,11 +576,9 @@ class KoboFile(object):
file page key. The caller must determine if the decrypted
data is correct."""
# The userkey decrypts the page key (self.key)
- keyenc = AES(userkey)
- decryptedkey = keyenc.decrypt(self.key)
- # The decrypted page key decrypts the content
- pageenc = AES(decryptedkey)
- return self.__removeaespadding(pageenc.decrypt(contents))
+ decryptedkey = AES.new(userkey, AES.MODE_ECB).decrypt(self.key)
+ # The decrypted page key decrypts the content. Padding is PKCS#7
+ return unpad(AES.new(decryptedkey, AES.MODE_ECB).decrypt(contents), 16)
def check (self, contents):
"""
@@ -690,23 +648,6 @@ class KoboFile(object):
raise ValueError()
return False
- def __removeaespadding (self, contents):
- """
- Remove the trailing padding, using what appears to be the CMS
- algorithm from RFC 5652 6.3"""
- lastchar = binascii.b2a_hex(contents[-1:])
- strlen = int(lastchar, 16)
- padding = strlen
- if strlen == 1:
- return contents[:-1]
- if strlen < 16:
- for i in range(strlen):
- testchar = binascii.b2a_hex(contents[-strlen:-(strlen-1)])
- if testchar != lastchar:
- padding = 0
- if padding > 0:
- contents = contents[:-padding]
- return contents
def decrypt_book(book, lib):
print("Converting {0}".format(book.title))
@@ -774,7 +715,7 @@ def cli_main():
books = [lib.books[num - 1]]
except (ValueError, IndexError):
print("Invalid choice. Exiting...")
- exit()
+ sys.exit()
results = [decrypt_book(book, lib) for book in books]
lib.close()
diff --git a/Obok_plugin/obok_dedrm_Help.htm b/Obok_plugin/obok_dedrm_Help.htm
index 251c23f..4d1247e 100644
--- a/Obok_plugin/obok_dedrm_Help.htm
+++ b/Obok_plugin/obok_dedrm_Help.htm
@@ -8,7 +8,7 @@
Obok DeDRM Plugin
-(version 3.1.3)
+(version 10.0.2)
Installation:
@@ -22,7 +22,7 @@
Troubleshooting:
-If you find that it’s not working for you , you can save a lot of time by using the plugin with Calibre in debug mode. This will print out a lot of helpful info that can be copied into any online help requests.
+If you find that it’s not working for you, you can save a lot of time by using the plugin with Calibre in debug mode. This will print out a lot of helpful info that can be copied into any online help requests.
Open a command prompt (terminal window) and type "calibre-debug -g" (without the quotes). Calibre will launch, and you can use the plugin the usual way. The debug info will be output to the original command prompt (terminal window). Copy the resulting output and paste it into the comment you make at Apprentice Alf's blog.
diff --git a/Obok_plugin/utilities.py b/Obok_plugin/utilities.py
index b2f02ad..ba0d7fc 100644
--- a/Obok_plugin/utilities.py
+++ b/Obok_plugin/utilities.py
@@ -224,5 +224,5 @@ class ReadOnlyTableWidgetItem(QTableWidgetItem):
def __init__(self, text):
if text is None:
text = ''
- QTableWidgetItem.__init__(self, text, QTableWidgetItem.UserType)
+ QTableWidgetItem.__init__(self, text, QTableWidgetItem.ItemType.UserType)
self.setFlags(Qt.ItemIsSelectable|Qt.ItemIsEnabled)
diff --git a/Other_Tools/DRM_Key_Scripts/Adobe_Digital_Editions/adobekey.pyw b/Other_Tools/DRM_Key_Scripts/Adobe_Digital_Editions/adobekey.pyw
index bc33567..8e9061e 100644
--- a/Other_Tools/DRM_Key_Scripts/Adobe_Digital_Editions/adobekey.pyw
+++ b/Other_Tools/DRM_Key_Scripts/Adobe_Digital_Editions/adobekey.pyw
@@ -68,12 +68,20 @@ class SafeUnbuffered:
if self.encoding == None:
self.encoding = "utf-8"
def write(self, data):
- if isinstance(data,unicode):
+ if isinstance(data,str) or isinstance(data,unicode):
+ # str for Python3, unicode for Python2
data = data.encode(self.encoding,"replace")
- self.stream.write(data)
- self.stream.flush()
+ try:
+ buffer = getattr(self.stream, 'buffer', self.stream)
+ # self.stream.buffer for Python3, self.stream for Python2
+ buffer.write(data)
+ buffer.flush()
+ except:
+ # We can do nothing if a write fails
+ raise
def __getattr__(self, attr):
return getattr(self.stream, attr)
+
try:
from calibre.constants import iswindows, isosx
@@ -129,7 +137,10 @@ if iswindows:
c_long, c_ulong
from ctypes.wintypes import LPVOID, DWORD, BOOL
- import winreg
+ try:
+ import winreg
+ except ImportError:
+ import _winreg as winreg
def _load_crypto_libcrypto():
from ctypes.util import find_library
diff --git a/Other_Tools/DRM_Key_Scripts/Barnes_and_Noble_ePubs/ignoblekey.pyw b/Other_Tools/DRM_Key_Scripts/Barnes_and_Noble_ePubs/ignoblekey.pyw
index 7365c94..b6ddb2c 100644
--- a/Other_Tools/DRM_Key_Scripts/Barnes_and_Noble_ePubs/ignoblekey.pyw
+++ b/Other_Tools/DRM_Key_Scripts/Barnes_and_Noble_ePubs/ignoblekey.pyw
@@ -39,12 +39,20 @@ class SafeUnbuffered:
if self.encoding == None:
self.encoding = "utf-8"
def write(self, data):
- if isinstance(data,unicode):
+ if isinstance(data,str) or isinstance(data,unicode):
+ # str for Python3, unicode for Python2
data = data.encode(self.encoding,"replace")
- self.stream.write(data)
- self.stream.flush()
+ try:
+ buffer = getattr(self.stream, 'buffer', self.stream)
+ # self.stream.buffer for Python3, self.stream for Python2
+ buffer.write(data)
+ buffer.flush()
+ except:
+ # We can do nothing if a write fails
+ raise
def __getattr__(self, attr):
return getattr(self.stream, attr)
+
try:
from calibre.constants import iswindows, isosx
@@ -98,7 +106,10 @@ def getNookLogFiles():
logFiles = []
found = False
if iswindows:
- import winreg
+ try:
+ import winreg
+ except ImportError:
+ import _winreg as winreg
# some 64 bit machines do not have the proper registry key for some reason
# or the python interface to the 32 vs 64 bit registry is broken
diff --git a/Other_Tools/DRM_Key_Scripts/Barnes_and_Noble_ePubs/ignoblekeyfetch.pyw b/Other_Tools/DRM_Key_Scripts/Barnes_and_Noble_ePubs/ignoblekeyfetch.pyw
index e9637a1..3bed979 100644
--- a/Other_Tools/DRM_Key_Scripts/Barnes_and_Noble_ePubs/ignoblekeyfetch.pyw
+++ b/Other_Tools/DRM_Key_Scripts/Barnes_and_Noble_ePubs/ignoblekeyfetch.pyw
@@ -45,12 +45,20 @@ class SafeUnbuffered:
if self.encoding == None:
self.encoding = "utf-8"
def write(self, data):
- if isinstance(data,unicode):
+ if isinstance(data,str) or isinstance(data,unicode):
+ # str for Python3, unicode for Python2
data = data.encode(self.encoding,"replace")
- self.stream.write(data)
- self.stream.flush()
+ try:
+ buffer = getattr(self.stream, 'buffer', self.stream)
+ # self.stream.buffer for Python3, self.stream for Python2
+ buffer.write(data)
+ buffer.flush()
+ except:
+ # We can do nothing if a write fails
+ raise
def __getattr__(self, attr):
return getattr(self.stream, attr)
+
try:
from calibre.constants import iswindows, isosx
diff --git a/Other_Tools/DRM_Key_Scripts/Barnes_and_Noble_ePubs/ignoblekeygen.pyw b/Other_Tools/DRM_Key_Scripts/Barnes_and_Noble_ePubs/ignoblekeygen.pyw
index d2917c7..8d3ea1a 100644
--- a/Other_Tools/DRM_Key_Scripts/Barnes_and_Noble_ePubs/ignoblekeygen.pyw
+++ b/Other_Tools/DRM_Key_Scripts/Barnes_and_Noble_ePubs/ignoblekeygen.pyw
@@ -56,12 +56,20 @@ class SafeUnbuffered:
if self.encoding == None:
self.encoding = "utf-8"
def write(self, data):
- if isinstance(data,unicode):
+ if isinstance(data,str) or isinstance(data,unicode):
+ # str for Python3, unicode for Python2
data = data.encode(self.encoding,"replace")
- self.stream.write(data)
- self.stream.flush()
+ try:
+ buffer = getattr(self.stream, 'buffer', self.stream)
+ # self.stream.buffer for Python3, self.stream for Python2
+ buffer.write(data)
+ buffer.flush()
+ except:
+ # We can do nothing if a write fails
+ raise
def __getattr__(self, attr):
return getattr(self.stream, attr)
+
try:
from calibre.constants import iswindows, isosx
diff --git a/Other_Tools/DRM_Key_Scripts/Kindle_for_Android/androidkindlekey.pyw b/Other_Tools/DRM_Key_Scripts/Kindle_for_Android/androidkindlekey.pyw
index ff8d1ee..1d8cc89 100644
--- a/Other_Tools/DRM_Key_Scripts/Kindle_for_Android/androidkindlekey.pyw
+++ b/Other_Tools/DRM_Key_Scripts/Kindle_for_Android/androidkindlekey.pyw
@@ -48,12 +48,20 @@ class SafeUnbuffered:
if self.encoding == None:
self.encoding = "utf-8"
def write(self, data):
- if isinstance(data,unicode):
+ if isinstance(data,str) or isinstance(data,unicode):
+ # str for Python3, unicode for Python2
data = data.encode(self.encoding,"replace")
- self.stream.write(data)
- self.stream.flush()
+ try:
+ buffer = getattr(self.stream, 'buffer', self.stream)
+ # self.stream.buffer for Python3, self.stream for Python2
+ buffer.write(data)
+ buffer.flush()
+ except:
+ # We can do nothing if a write fails
+ raise
def __getattr__(self, attr):
return getattr(self.stream, attr)
+
try:
from calibre.constants import iswindows, isosx
diff --git a/Other_Tools/DRM_Key_Scripts/Kindle_for_Mac_and_PC/kindlekey.pyw b/Other_Tools/DRM_Key_Scripts/Kindle_for_Mac_and_PC/kindlekey.pyw
index 4837627..a463f1f 100644
--- a/Other_Tools/DRM_Key_Scripts/Kindle_for_Mac_and_PC/kindlekey.pyw
+++ b/Other_Tools/DRM_Key_Scripts/Kindle_for_Mac_and_PC/kindlekey.pyw
@@ -51,12 +51,20 @@ class SafeUnbuffered:
if self.encoding == None:
self.encoding = "utf-8"
def write(self, data):
- if isinstance(data,unicode):
+ if isinstance(data,str) or isinstance(data,unicode):
+ # str for Python3, unicode for Python2
data = data.encode(self.encoding,"replace")
- self.stream.write(data)
- self.stream.flush()
+ try:
+ buffer = getattr(self.stream, 'buffer', self.stream)
+ # self.stream.buffer for Python3, self.stream for Python2
+ buffer.write(data)
+ buffer.flush()
+ except:
+ # We can do nothing if a write fails
+ raise
def __getattr__(self, attr):
return getattr(self.stream, attr)
+
try:
from calibre.constants import iswindows, isosx
@@ -177,7 +185,10 @@ if iswindows:
create_unicode_buffer, create_string_buffer, CFUNCTYPE, addressof, \
string_at, Structure, c_void_p, cast
- import winreg
+ try:
+ import winreg
+ except ImportError:
+ import _winreg as winreg
MAX_PATH = 255
kernel32 = windll.kernel32
advapi32 = windll.advapi32
@@ -289,7 +300,7 @@ if iswindows:
numBlocks, numExtraBytes = divmod(len(self.bytesToDecrypt), self.blockSize)
if more == None: # no more calls to decrypt, should have all the data
if numExtraBytes != 0:
- raise DecryptNotBlockAlignedError, 'Data not block aligned on decrypt'
+ raise DecryptNotBlockAlignedError('Data not block aligned on decrypt')
# hold back some bytes in case last decrypt has zero len
if (more != None) and (numExtraBytes == 0) and (numBlocks >0) :
@@ -331,7 +342,7 @@ if iswindows:
def removePad(self, paddedBinaryString, blockSize):
""" Remove padding from a binary string """
if not(0 5:
if 'LOCALAPPDATA' in os.environ.keys():
# Python 2.x does not return unicode env. Use Python 3.x
@@ -736,7 +747,7 @@ def cli_main():
books = [lib.books[num - 1]]
except (ValueError, IndexError):
print("Invalid choice. Exiting...")
- exit()
+ sys.exit()
results = [decrypt_book(book, lib) for book in books]
lib.close()
diff --git a/README.md b/README.md
index 5461e58..94366a9 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,18 @@
-# [Guide] How to remove DRM
-Refer to [Wiki Page](https://github.com/apprenticeharper/DeDRM_tools/wiki/Exactly-how-to-remove-DRM)
-
# DeDRM_tools
DeDRM tools for ebooks
+This is a fork of Apprentice Harper's version of the DeDRM tools. Apprentice Harper said that the original version of the plugin [is no longer maintained](https://github.com/apprenticeharper/DeDRM_tools#no-longer-maintained), so I've taken over, merged a bunch of open PRs, and added a ton more features and bugfixes.
+
+Take a look at [the CHANGELOG](https://github.com/noDRM/DeDRM_tools/blob/master/CHANGELOG.md) to see a list of changes since the last version by Apprentice Harper (v7.2.1). This plugin will start with version v10.0.0.
+
+The v10.0.0 versions of this plugin should both work with Calibre 5.x (Python 3) as well as Calibre 4.x and lower (Python 2). If you encounter issues with this plugin in Calibre 4.x or lower, please open a bug report.
+
+# Original README from Apprentice Harper
+
This is a repository that tracks all the scripts and other tools for removing DRM from ebooks that I could find, committed in date order as best as I could manage. (Except for the Requiem tools for Apple's iBooks, and Convert LIT for Microsoft's .lit ebooks.) This includes the tools from a time before Apprentice Alf had a blog, and continues through to when Apprentice Harper (with help) took over maintenance of the tools.
The individual scripts are now released as two plugins for calibre: DeDRM and Obok.
-The DeDRM plugin handles books that use Amazon DRM, Adobe Digital Editions DRM (version 1), Barnes & Noble DRM, and some historical formats.
+The DeDRM plugin handles books that use Amazon DRM, Adobe Digital Editions DRM, Barnes & Noble DRM, and some historical formats.
The Obok plugin handles Kobo DRM.
Users with calibre 5.x or later should use release 7.2.0 or later of the tools.
@@ -19,7 +24,7 @@ Note that Amazon changes the DRM for KFX files frequently. What works for KFX to
I welcome contributions from others to improve these tools, from expanding the range of books handled, improving key retrieval, to just general bug fixes, speed improvements and UI enhancements.
-I urge people to read the FAQs. But to cover the most common: Use ADE 2.0.1 to be sure not to get the new DRM scheme that these tools can't handle. Do remember to unzip the downloaded archive to get the plugin (beta versions may be just the plugin don't unzip that). You can't load the whole tools archive into calibre.
+I urge people to read the FAQs. But to cover the most common: Do remember to unzip the downloaded archive to get the plugin (beta versions may be just the plugin don't unzip that). You can't load the whole tools archive into calibre.
My special thanks to all those developers who have done the hard work of reverse engineering to provide the initial tools.
diff --git a/ReadMe_Overview.txt b/ReadMe_Overview.txt
index ca5bca1..82cd547 100644
--- a/ReadMe_Overview.txt
+++ b/ReadMe_Overview.txt
@@ -1,32 +1,33 @@
Welcome to the tools!
=====================
-This file is to give users a quick overview of what is available and how to get started. This document is part of the DeDRM Tools archive from Apprentice Harper's github repository: https://github.com/apprenticeharper/DeDRM_tools/
+This file is to give users a quick overview of what is available and how to get started. This document is part of the DeDRM Tools archive from noDRM's github repository: https://github.com/noDRM/DeDRM_tools/
This archive includes calibre plugins to remove DRM from:
- Kindle ebooks (files from Kindle for Mac/PC and eInk Kindles).
- - Adobe Digital Editions (v2.0.1***) ePubs (including Kobo and Google ePubs downloaded to ADE)
- - Adobe Digital Editions (v2.0.1) PDFs
+ - Adobe Digital Editions ePubs (including Kobo and Google ePubs downloaded to ADE)
+ - Adobe Digital Editions PDFs
- Kobo kePubs from the Kobo Desktop application and attached Kobo readers.
These tools do NOT work with Apple's iBooks FairPlay DRM. Use iBook Copy from TunesKit.
These tools no longer work well with books from Barnes & Noble.
+Due to a DMCA request, these tools no longer work with LCP-encrypted books - see https://github.com/noDRM/DeDRM_tools/issues/18 for details.
-For limitations and work-arounds, see the FAQ at https://github.com/apprenticeharper/DeDRM_tools/blob/master/FAQs.md
+For limitations and work-arounds, see the FAQ at https://github.com/noDRM/DeDRM_tools/blob/master/FAQs.md
About the tools
---------------
-These tools are updated and maintained by Apprentice Harper and many others. You can find the latest updates at Apprentice Harper's github repository https://github.com/apprenticeharper/DeDRM_tools/ and get support by creating an issue at the repository (github account required) or by posting a comment at Apprentice Alf's blog: http://www.apprenticealf.wordpress.com/
+These tools are updated and maintained by noDRM and many others. They are based on Apprentice Harper's Calibre plugin. You can find the latest updates at noDRM's github repository https://github.com/noDRM/DeDRM_tools/ and get support by creating an issue at the repository (github account required).
-If you re-post these tools, a link to the repository and/or the blog would be appreciated.
+If you re-post these tools, a link to the repository would be appreciated.
The tools are provided in the form of plugins for calibre. Calibre is an open source freeware ebook library manager. It is the best tool around for keeping track of your ebooks.
-DeDRM plugin for calibre (Mac OS X, Windows)
+DeDRM plugin for calibre (Linux, Mac OS X and Windows)
-------------------------------------------------------
-calibe 5.x and later are now written in Python 3, and plugins must also use Python 3. If you have calibre 5, you must use version 7.x or later of the plugins. For calibre 4.x and earlier, use version 6.8.x of the plugins.
+calibe 5.x and later are now written in Python 3, and plugins must also use Python 3.
The DeDRM plugin for calibre removes DRM from your Kindle and Adobe DRM ebooks when they are imported to calibre. Just install the DeDRM plugin (DeDRM_plugin.zip), following the instructions and configuration directions provided in the ReadMe file and the help links in the plugin's configuration dialogs.
@@ -40,27 +41,17 @@ To import ebooks from the Kobo Desktop app or from a Kobo ebook reader, install
For instructions, see the obok_plugin_ReadMe.txt file.
-DeDRM application for Mac OS X users: (Mac OS X 10.6 and above)
----------------------------------------------------------------
-DeDRM application for Windows users: (Windows XP through Windows 10)
-------------------------------------------------------------------
-As of Version 6.7 of the tools, these are no longer provided or supported.
-
-
-Linux support
--------------
-It may be possible to use the plugins on a Linux system, but no support is given at this time.
-
-
Credits
-------
The original inept and ignoble scripts were by i♥cabbages
+~The original Readium LCP DRM removal by NoDRM~ (removed due to a DMCA request)
The original mobidedrm and erdr2pml scripts were by The Dark Reverser
The original topaz DRM removal script was by CMBDTC
The original topaz format conversion scripts were by some_updates, clarknova and Bart Simpson
The original KFX format decryption was by lulzkabulz, converted to python by Apprentice Naomi and integrated into the tools by tomthumb1997
The alfcrypto library is by some_updates
-The DeDRM plugin was based on plugins by DiapDealer and is maintained by Apprentice Alf and Apprentice Harper
+The DeDRM plugin is based on plugins by DiapDealer and is currently maintained by noDRM
+The DeDRM plugin has been maintained by Apprentice Alf and Apprentice Harper until 2021.
The original obok script was by Physisticated
The plugin conversion was done anonymously.
diff --git a/make_release.py b/make_release.py
index 0e95173..95bebf6 100755
--- a/make_release.py
+++ b/make_release.py
@@ -14,19 +14,65 @@ import shutil
DEDRM_SRC_DIR = 'DeDRM_plugin'
+DEDRM_SRC_TMP_DIR = 'DeDRM_plugin_temp'
DEDRM_README= 'DeDRM_plugin_ReadMe.txt'
OBOK_SRC_DIR = 'Obok_plugin'
OBOK_README = 'obok_plugin_ReadMe.txt'
RELEASE_DIR = 'release'
+def patch_file(filepath):
+ f = open(filepath, "rb")
+ fn = open(filepath + ".tmp", "wb")
+ patch = open(os.path.join(DEDRM_SRC_DIR, "__calibre_compat_code.py"), "rb")
+ patchdata = patch.read()
+ patch.close()
+
+ while True:
+ line = f.readline()
+ if len(line) == 0:
+ break
+
+ if line.strip().startswith(b"#@@CALIBRE_COMPAT_CODE@@"):
+ fn.write(patchdata)
+ else:
+ fn.write(line)
+
+ f.close()
+ fn.close()
+ shutil.move(filepath + ".tmp", filepath)
+
+
def make_release(version):
try:
shutil.rmtree(RELEASE_DIR)
except:
pass
+ try:
+ shutil.rmtree(DEDRM_SRC_TMP_DIR)
+ except:
+ pass
+
os.mkdir(RELEASE_DIR)
- shutil.make_archive(DEDRM_SRC_DIR, 'zip', DEDRM_SRC_DIR)
+
+ # Copy folder
+ shutil.copytree(DEDRM_SRC_DIR, DEDRM_SRC_TMP_DIR)
+
+ # Modify folder
+ try:
+ shutil.rmtree(os.path.join(os.path.abspath(DEDRM_SRC_TMP_DIR), "__pycache__"))
+ except:
+ pass
+
+ # Patch file to add compat code.
+ for root, dirs, files in os.walk(DEDRM_SRC_TMP_DIR):
+ for name in files:
+ if name.endswith(".py"):
+ patch_file(os.path.join(root, name))
+
+
+ # Package
+ shutil.make_archive(DEDRM_SRC_DIR, 'zip', DEDRM_SRC_TMP_DIR)
shutil.make_archive(OBOK_SRC_DIR, 'zip', OBOK_SRC_DIR)
shutil.move(DEDRM_SRC_DIR+'.zip', RELEASE_DIR)
shutil.move(OBOK_SRC_DIR+'.zip', RELEASE_DIR)
@@ -34,7 +80,13 @@ def make_release(version):
shutil.copy(OBOK_README, RELEASE_DIR)
shutil.copy("ReadMe_Overview.txt", RELEASE_DIR)
- release_name = 'DeDRM_tools_{}'.format(version)
+ # Remove temp folder:
+ shutil.rmtree(DEDRM_SRC_TMP_DIR)
+
+ if version is not None:
+ release_name = 'DeDRM_tools_{}'.format(version)
+ else:
+ release_name = 'DeDRM_tools'
result = shutil.make_archive(release_name, 'zip', RELEASE_DIR)
try:
shutil.rmtree(RELEASE_DIR)
@@ -48,6 +100,6 @@ if __name__ == '__main__':
try:
version = sys.argv[1]
except IndexError:
- raise SystemExit('Usage: {} version'.format(__file__))
+ version = None
print(make_release(version))