DeDRM_tools/DeDRM_plugin/k4mobidedrm.py

363 lines
13 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# k4mobidedrm.py
# Copyright © 2008-2020 by Apprentice Harper et al.
2013-10-02 12:59:40 -06:00
__license__ = 'GPL v3'
__version__ = '6.0'
# Engine to remove drm from Kindle and Mobipocket ebooks
2013-10-02 12:59:40 -06:00
# for personal use for archiving and converting your ebooks
# PLEASE DO NOT PIRATE EBOOKS!
# We want all authors and publishers, and ebook stores to live
2013-10-02 12:59:40 -06:00
# long and prosperous lives but at the same time we just want to
# be able to read OUR books on whatever device we want and to keep
# readable for a long, long time
# This borrows very heavily from works by CMBDTC, IHeartCabbages, skindle,
# unswindle, DarkReverser, ApprenticeAlf, and many many others
2013-10-02 12:59:40 -06:00
# Special thanks to The Dark Reverser for MobiDeDrm and CMBDTC for cmbdtc_dump
# from which this script borrows most unashamedly.
# Changelog
# 1.0 - Name change to k4mobidedrm. Adds Mac support, Adds plugin code
# 1.1 - Adds support for additional kindle.info files
# 1.2 - Better error handling for older Mobipocket
# 1.3 - Don't try to decrypt Topaz books
# 1.7 - Add support for Topaz books and Kindle serial numbers. Split code.
# 1.9 - Tidy up after Topaz, minor exception changes
# 2.1 - Topaz fix and filename sanitizing
# 2.2 - Topaz Fix and minor Mac code fix
# 2.3 - More Topaz fixes
# 2.4 - K4PC/Mac key generation fix
# 2.6 - Better handling of non-K4PC/Mac ebooks
# 2.7 - Better trailing bytes handling in mobidedrm
# 2.8 - Moved parsing of kindle.info files to mac & pc util files.
# 3.1 - Updated for new calibre interface. Now __init__ in plugin.
# 3.5 - Now support Kindle for PC/Mac 1.6
# 3.6 - Even better trailing bytes handling in mobidedrm
# 3.7 - Add support for Amazon Print Replica ebooks.
# 3.8 - Improved Topaz support
# 4.1 - Improved Topaz support and faster decryption with alfcrypto
# 4.2 - Added support for Amazon's KF8 format ebooks
# 4.4 - Linux calls to Wine added, and improved configuration dialog
# 4.5 - Linux works again without Wine. Some Mac key file search changes
# 4.6 - First attempt to handle unicode properly
# 4.7 - Added timing reports, and changed search for Mac key files
# 4.8 - Much better unicode handling, matching the updated inept and ignoble scripts
# - Moved back into plugin, __init__ in plugin now only contains plugin code.
# 4.9 - Missed some invalid characters in cleanup_name
# 5.0 - Extraction of info from Kindle for PC/Mac moved into kindlekey.py
# - tweaked GetDecryptedBook interface to leave passed parameters unchanged
# 5.1 - moved unicode_argv call inside main for Windows DeDRM compatibility
# 5.2 - Fixed error in command line processing of unicode arguments
# 5.3 - Changed Android support to allow passing of backup .ab files
# 5.4 - Recognise KFX files masquerading as azw, even if we can't decrypt them yet.
# 5.5 - Added GPL v3 licence explicitly.
# 5.6 - Invoke KFXZipBook to handle zipped KFX files
# 5.7 - Revamp cleanup_name
# 6.0 - Added Python 3 compatibility for calibre 5.0
2013-10-02 12:59:40 -06:00
import sys, os, re
import csv
import getopt
import re
import traceback
2013-10-02 12:59:40 -06:00
import time
2022-03-19 08:23:07 -06:00
try:
import html.entities as htmlentitydefs
except:
import htmlentitydefs
2013-10-02 12:59:40 -06:00
import json
#@@CALIBRE_COMPAT_CODE@@
class DrmException(Exception):
pass
import mobidedrm
import topazextract
import kgenpids
import androidkindlekey
import kfxdedrm
2013-10-02 12:59:40 -06:00
# 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):
2021-11-16 03:09:03 -07:00
if isinstance(data,str) or isinstance(data,unicode):
# str for Python3, unicode for Python2
2013-10-02 12:59:40 -06:00
data = data.encode(self.encoding,"replace")
2021-11-16 03:09:03 -07:00
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
2013-10-02 12:59:40 -06:00
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)]
2013-10-02 12:59:40 -06:00
# if we don't have any arguments at all, just pass back script name
# this should never happen
return ["mobidedrm.py"]
2013-10-02 12:59:40 -06:00
else:
argvencoding = sys.stdin.encoding or "utf-8"
2021-11-16 03:09:03 -07:00
return [arg if (isinstance(arg, str) or isinstance(arg,unicode)) else str(arg, argvencoding) for arg in sys.argv]
2013-10-02 12:59:40 -06:00
# cleanup unicode filenames
# borrowed from calibre from calibre/src/calibre/__init__.py
# added in removal of control (<32) chars
# and removal of . at start and end
# and with some (heavily edited) code from Paul Durrant's kindlenamer.py
# and some improvements suggested by jhaisley
2013-10-02 12:59:40 -06:00
def cleanup_name(name):
# substitute filename unfriendly characters
name = name.replace("<","[").replace(">","]").replace(" : "," ").replace(": "," ").replace(":","").replace("/","_").replace("\\","_").replace("|","_").replace("\"","\'").replace("*","_").replace("?","")
2013-10-02 12:59:40 -06:00
# white space to single space, delete leading and trailing while space
name = re.sub(r"\s", " ", name).strip()
# delete control characters
name = "".join(char for char in name if ord(char)>=32)
# delete non-ascii characters
name = "".join(char for char in name if ord(char)<=126)
2013-10-02 12:59:40 -06:00
# remove leading dots
while len(name)>0 and name[0] == ".":
2013-10-02 12:59:40 -06:00
name = name[1:]
# remove trailing dots (Windows doesn't like them)
while name.endswith("."):
2013-10-02 12:59:40 -06:00
name = name[:-1]
if len(name)==0:
name="DecryptedBook"
2013-10-02 12:59:40 -06:00
return name
# must be passed unicode
def unescape(text):
def fixup(m):
text = m.group(0)
if text[:2] == "&#":
2013-10-02 12:59:40 -06:00
# character reference
try:
if text[:3] == "&#x":
return chr(int(text[3:-1], 16))
2013-10-02 12:59:40 -06:00
else:
return chr(int(text[2:-1]))
2013-10-02 12:59:40 -06:00
except ValueError:
pass
else:
# named entity
try:
2022-03-19 08:23:07 -06:00
text = chr(htmlentitydefs.name2codepoint[text[1:-1]])
2013-10-02 12:59:40 -06:00
except KeyError:
pass
return text # leave as is
return re.sub("&#?\\w+;", fixup, text)
2013-10-02 12:59:40 -06:00
def GetDecryptedBook(infile, kDatabases, androidFiles, serials, pids, starttime = time.time()):
2013-10-02 12:59:40 -06:00
# handle the obvious cases at the beginning
if not os.path.isfile(infile):
raise DrmException("Input file does not exist.")
2013-10-02 12:59:40 -06:00
mobi = True
magic8 = open(infile,'rb').read(8)
2020-10-16 06:22:19 -06:00
if magic8 == b'\xeaDRMION\xee':
raise DrmException("The .kfx DRMION file cannot be decrypted by itself. A .kfx-zip archive containing a DRM voucher is required.")
2018-03-12 18:33:33 -06:00
magic3 = magic8[:3]
2020-10-16 06:22:19 -06:00
if magic3 == b'TPZ':
2013-10-02 12:59:40 -06:00
mobi = False
2020-10-16 06:22:19 -06:00
if magic8[:4] == b'PK\x03\x04':
2018-03-12 18:33:33 -06:00
mb = kfxdedrm.KFXZipBook(infile)
elif mobi:
2013-10-02 12:59:40 -06:00
mb = mobidedrm.MobiBook(infile)
else:
mb = topazextract.TopazBook(infile)
2022-03-19 08:23:07 -06:00
try:
bookname = unescape(mb.getBookTitle())
print("Decrypting {1} ebook: {0}".format(bookname, mb.getBookType()))
except:
print("Decrypting {0} ebook.".format(mb.getBookType()))
2013-10-02 12:59:40 -06:00
# copy list of pids
totalpids = list(pids)
# extend list of serials with serials from android databases
for aFile in androidFiles:
serials.extend(androidkindlekey.get_serials(aFile))
# extend PID list with book-specific PIDs from seriala and kDatabases
2013-10-02 12:59:40 -06:00
md1, md2 = mb.getPIDMetaInfo()
totalpids.extend(kgenpids.getPidList(md1, md2, serials, kDatabases))
# remove any duplicates
totalpids = list(set(totalpids))
print("Found {1:d} keys to try after {0:.1f} seconds".format(time.time()-starttime, len(totalpids)))
#print totalpids
try:
2013-10-02 12:59:40 -06:00
mb.processBook(totalpids)
except:
mb.cleanup()
2013-10-02 12:59:40 -06:00
raise
print("Decryption succeeded after {0:.1f} seconds".format(time.time()-starttime))
2013-10-02 12:59:40 -06:00
return mb
2013-10-02 12:59:40 -06:00
# kDatabaseFiles is a list of files created by kindlekey
def decryptBook(infile, outdir, kDatabaseFiles, androidFiles, serials, pids):
2013-10-02 12:59:40 -06:00
starttime = time.time()
kDatabases = []
for dbfile in kDatabaseFiles:
kindleDatabase = {}
try:
with open(dbfile, 'r') as keyfilein:
kindleDatabase = json.loads(keyfilein.read())
kDatabases.append([dbfile,kindleDatabase])
except Exception as e:
print("Error getting database from file {0:s}: {1:s}".format(dbfile,e))
2013-10-02 12:59:40 -06:00
traceback.print_exc()
2013-10-02 12:59:40 -06:00
try:
book = GetDecryptedBook(infile, kDatabases, androidFiles, serials, pids, starttime)
except Exception as e:
print("Error decrypting book after {1:.1f} seconds: {0}".format(e.args[0],time.time()-starttime))
2013-10-02 12:59:40 -06:00
traceback.print_exc()
return 1
2018-04-18 03:21:44 -06:00
# Try to infer a reasonable name
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)
): # Kindle for PC / Mac / Android / Fire / iOS
clean_title = cleanup_name(book.getBookTitle())
outfilename = "{}_{}".format(orig_fn_root, clean_title)
2018-04-18 03:21:44 -06:00
else: # E Ink Kindle, which already uses a reasonable name
outfilename = orig_fn_root
2013-10-02 12:59:40 -06:00
# avoid excessively long file names
if len(outfilename)>150:
outfilename = outfilename[:99]+"--"+outfilename[-49:]
outfilename = outfilename+"_nodrm"
2013-10-02 12:59:40 -06:00
outfile = os.path.join(outdir, outfilename + book.getBookExtension())
2013-10-02 12:59:40 -06:00
book.getFile(outfile)
print("Saved decrypted book {1:s} after {0:.1f} seconds".format(time.time()-starttime, outfilename))
if book.getBookType()=="Topaz":
zipname = os.path.join(outdir, outfilename + "_SVG.zip")
2013-10-02 12:59:40 -06:00
book.getSVGZip(zipname)
print("Saved SVG ZIP Archive for {1:s} after {0:.1f} seconds".format(time.time()-starttime, outfilename))
2013-10-02 12:59:40 -06:00
# remove internal temporary directory of Topaz pieces
book.cleanup()
return 0
2013-10-02 12:59:40 -06:00
def usage(progname):
print("Removes DRM protection from Mobipocket, Amazon KF8, Amazon Print Replica and Amazon Topaz ebooks")
print("Usage:")
print(" {0} [-k <kindle.k4i>] [-p <comma separated PIDs>] [-s <comma separated Kindle serial numbers>] [ -a <AmazonSecureStorage.xml|backup.ab> ] <infile> <outdir>".format(progname))
2013-10-02 12:59:40 -06:00
#
# Main
#
def cli_main():
argv=unicode_argv()
progname = os.path.basename(argv[0])
2020-10-16 06:22:19 -06:00
print("K4MobiDeDrm v{0}.\nCopyright © 2008-2020 Apprentice Harper et al.".format(__version__))
2013-04-05 10:44:48 -06:00
2013-10-02 12:59:40 -06:00
try:
2020-10-16 06:22:19 -06:00
opts, args = getopt.getopt(argv[1:], "k:p:s:a:h")
except getopt.GetoptError as err:
print("Error in options or arguments: {0}".format(err.args[0]))
2013-10-02 12:59:40 -06:00
usage(progname)
sys.exit(2)
if len(args)<2:
usage(progname)
sys.exit(2)
infile = args[0]
outdir = args[1]
kDatabaseFiles = []
androidFiles = []
2013-10-02 12:59:40 -06:00
serials = []
pids = []
2013-10-02 12:59:40 -06:00
for o, a in opts:
2020-10-16 06:22:19 -06:00
if o == "-h":
usage(progname)
sys.exit(0)
2013-10-02 12:59:40 -06:00
if o == "-k":
if a == None :
raise DrmException("Invalid parameter for -k")
kDatabaseFiles.append(a)
if o == "-p":
if a == None :
raise DrmException("Invalid parameter for -p")
pids = a.encode('utf-8').split(b',')
2013-10-02 12:59:40 -06:00
if o == "-s":
if a == None :
raise DrmException("Invalid parameter for -s")
serials = a.split(',')
if o == '-a':
if a == None:
raise DrmException("Invalid parameter for -a")
androidFiles.append(a)
2013-10-02 12:59:40 -06:00
return decryptBook(infile, outdir, kDatabaseFiles, androidFiles, serials, pids)
2013-10-02 12:59:40 -06:00
if __name__ == '__main__':
sys.stdout=SafeUnbuffered(sys.stdout)
sys.stderr=SafeUnbuffered(sys.stderr)
sys.exit(cli_main())