2020-09-27 12:54:49 +02:00
|
|
|
#!/usr/bin/env python3
|
2018-03-13 01:35:52 +01:00
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
|
|
# Engine to remove drm from Kindle KFX ebooks
|
|
|
|
|
2020-09-27 12:54:49 +02:00
|
|
|
# 2.0 - Python 3 for calibre 5.0
|
2021-04-11 16:28:33 +02:00
|
|
|
# 2.1 - Some fixes for debugging
|
2021-04-11 17:43:16 +02:00
|
|
|
# 2.1.1 - Whitespace!
|
2020-09-26 22:22:47 +02:00
|
|
|
|
|
|
|
|
2018-03-13 01:35:52 +01:00
|
|
|
import os
|
|
|
|
import shutil
|
2021-04-11 16:28:33 +02:00
|
|
|
import traceback
|
2018-03-13 01:35:52 +01:00
|
|
|
import zipfile
|
|
|
|
|
2020-10-14 17:23:49 +02:00
|
|
|
from io import BytesIO
|
2020-11-28 17:25:54 +01:00
|
|
|
try:
|
|
|
|
from ion import DrmIon, DrmIonVoucher
|
|
|
|
except:
|
|
|
|
from calibre_plugins.dedrm.ion import DrmIon, DrmIonVoucher
|
2018-03-13 01:35:52 +01:00
|
|
|
|
|
|
|
|
|
|
|
__license__ = 'GPL v3'
|
2020-09-26 22:22:47 +02:00
|
|
|
__version__ = '2.0'
|
2018-03-13 01:35:52 +01:00
|
|
|
|
|
|
|
|
|
|
|
class KFXZipBook:
|
|
|
|
def __init__(self, infile):
|
|
|
|
self.infile = infile
|
|
|
|
self.voucher = None
|
|
|
|
self.decrypted = {}
|
|
|
|
|
|
|
|
def getPIDMetaInfo(self):
|
|
|
|
return (None, None)
|
|
|
|
|
|
|
|
def processBook(self, totalpids):
|
|
|
|
with zipfile.ZipFile(self.infile, 'r') as zf:
|
|
|
|
for filename in zf.namelist():
|
2018-04-05 19:32:59 +02:00
|
|
|
with zf.open(filename) as fh:
|
|
|
|
data = fh.read(8)
|
2020-11-27 15:01:18 +01:00
|
|
|
if data != b'\xeaDRMION\xee':
|
2018-04-05 19:32:59 +02:00
|
|
|
continue
|
|
|
|
data += fh.read()
|
2018-03-13 01:35:52 +01:00
|
|
|
if self.voucher is None:
|
|
|
|
self.decrypt_voucher(totalpids)
|
2020-09-27 12:54:49 +02:00
|
|
|
print("Decrypting KFX DRMION: {0}".format(filename))
|
2020-10-14 17:23:49 +02:00
|
|
|
outfile = BytesIO()
|
2020-11-28 04:20:53 +01:00
|
|
|
DrmIon(BytesIO(data[8:-8]), lambda name: self.voucher).parse(outfile)
|
2018-03-13 01:35:52 +01:00
|
|
|
self.decrypted[filename] = outfile.getvalue()
|
|
|
|
|
|
|
|
if not self.decrypted:
|
2020-09-27 12:54:49 +02:00
|
|
|
print("The .kfx-zip archive does not contain an encrypted DRMION file")
|
2018-03-13 01:35:52 +01:00
|
|
|
|
|
|
|
def decrypt_voucher(self, totalpids):
|
|
|
|
with zipfile.ZipFile(self.infile, 'r') as zf:
|
|
|
|
for info in zf.infolist():
|
2018-04-05 19:32:59 +02:00
|
|
|
with zf.open(info.filename) as fh:
|
|
|
|
data = fh.read(4)
|
2020-11-27 15:01:18 +01:00
|
|
|
if data != b'\xe0\x01\x00\xea':
|
2018-04-05 19:32:59 +02:00
|
|
|
continue
|
|
|
|
|
|
|
|
data += fh.read()
|
2020-11-27 15:01:18 +01:00
|
|
|
if b'ProtectedData' in data:
|
2018-03-13 01:35:52 +01:00
|
|
|
break # found DRM voucher
|
|
|
|
else:
|
2020-09-27 12:54:49 +02:00
|
|
|
raise Exception("The .kfx-zip archive contains an encrypted DRMION file without a DRM voucher")
|
2018-03-13 01:35:52 +01:00
|
|
|
|
2020-09-27 12:54:49 +02:00
|
|
|
print("Decrypting KFX DRM voucher: {0}".format(info.filename))
|
2018-03-13 01:35:52 +01:00
|
|
|
|
|
|
|
for pid in [''] + totalpids:
|
2021-04-11 17:43:16 +02:00
|
|
|
# Belt and braces. PIDs should be unicode strings, but just in case...
|
2021-04-11 16:28:33 +02:00
|
|
|
if isinstance(pid, bytes):
|
|
|
|
pid = pid.decode('ascii')
|
2019-06-14 21:20:56 +02:00
|
|
|
for dsn_len,secret_len in [(0,0), (16,0), (16,40), (32,40), (40,0), (40,40)]:
|
2018-03-13 01:35:52 +01:00
|
|
|
if len(pid) == dsn_len + secret_len:
|
|
|
|
break # split pid into DSN and account secret
|
|
|
|
else:
|
|
|
|
continue
|
|
|
|
|
|
|
|
try:
|
2020-11-28 04:20:53 +01:00
|
|
|
voucher = DrmIonVoucher(BytesIO(data), pid[:dsn_len], pid[dsn_len:])
|
2018-03-13 01:35:52 +01:00
|
|
|
voucher.parse()
|
|
|
|
voucher.decryptvoucher()
|
|
|
|
break
|
|
|
|
except:
|
2021-04-11 17:43:16 +02:00
|
|
|
traceback.print_exc()
|
|
|
|
pass
|
2018-03-13 01:35:52 +01:00
|
|
|
else:
|
2020-09-27 12:54:49 +02:00
|
|
|
raise Exception("Failed to decrypt KFX DRM voucher with any key")
|
2018-03-13 01:35:52 +01:00
|
|
|
|
2020-09-27 12:54:49 +02:00
|
|
|
print("KFX DRM voucher successfully decrypted")
|
2018-03-13 01:35:52 +01:00
|
|
|
|
|
|
|
license_type = voucher.getlicensetype()
|
|
|
|
if license_type != "Purchase":
|
2021-11-15 13:59:20 +01:00
|
|
|
#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))
|
2018-03-13 01:35:52 +01:00
|
|
|
|
|
|
|
self.voucher = voucher
|
|
|
|
|
|
|
|
def getBookTitle(self):
|
|
|
|
return os.path.splitext(os.path.split(self.infile)[1])[0]
|
|
|
|
|
|
|
|
def getBookExtension(self):
|
|
|
|
return '.kfx-zip'
|
|
|
|
|
|
|
|
def getBookType(self):
|
|
|
|
return 'KFX-ZIP'
|
|
|
|
|
|
|
|
def cleanup(self):
|
|
|
|
pass
|
|
|
|
|
|
|
|
def getFile(self, outpath):
|
|
|
|
if not self.decrypted:
|
|
|
|
shutil.copyfile(self.infile, outpath)
|
|
|
|
else:
|
|
|
|
with zipfile.ZipFile(self.infile, 'r') as zif:
|
|
|
|
with zipfile.ZipFile(outpath, 'w') as zof:
|
|
|
|
for info in zif.infolist():
|
|
|
|
zof.writestr(info, self.decrypted.get(info.filename, zif.read(info.filename)))
|