You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
DeDRM_tools/DeDRM_plugin/k4mobidedrm.py

359 lines
13 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# k4mobidedrm.py
# Copyright © 2008-2020 by Apprentice Harper et al.
11 years ago
__license__ = 'GPL v3'
__version__ = '6.0'
# Engine to remove drm from Kindle and Mobipocket ebooks
11 years ago
# 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
11 years ago
# 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
11 years ago
# 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
11 years ago
import sys, os, re
import csv
import getopt
import re
import traceback
11 years ago
import time
import html.entities
11 years ago
import json
class DrmException(Exception):
pass
11 years ago
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
11 years ago
else:
import mobidedrm
import topazextract
import kgenpids
import androidkindlekey
import kfxdedrm
11 years ago
# 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):
11 years ago
data = data.encode(self.encoding,"replace")
self.stream.buffer.write(data)
self.stream.buffer.flush()
11 years ago
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)]
11 years ago
# if we don't have any arguments at all, just pass back script name
# this should never happen
return ["mobidedrm.py"]
11 years ago
else:
argvencoding = sys.stdin.encoding or "utf-8"
return [arg if isinstance(arg, str) else str(arg, argvencoding) for arg in sys.argv]
11 years ago
# 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
11 years ago
def cleanup_name(name):
# substitute filename unfriendly characters
name = name.replace("<","[").replace(">","]").replace(" : "," ").replace(": "," ").replace(":","").replace("/","_").replace("\\","_").replace("|","_").replace("\"","\'").replace("*","_").replace("?","")
11 years ago
# 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)
11 years ago
# remove leading dots
while len(name)>0 and name[0] == ".":
11 years ago
name = name[1:]
# remove trailing dots (Windows doesn't like them)
while name.endswith("."):
11 years ago
name = name[:-1]
if len(name)==0:
name="DecryptedBook"
11 years ago
return name
# must be passed unicode
def unescape(text):
def fixup(m):
text = m.group(0)
if text[:2] == "&#":
11 years ago
# character reference
try:
if text[:3] == "&#x":
return chr(int(text[3:-1], 16))
11 years ago
else:
return chr(int(text[2:-1]))
11 years ago
except ValueError:
pass
else:
# named entity
try:
text = chr(html.entities.name2codepoint[text[1:-1]])
11 years ago
except KeyError:
pass
return text # leave as is
return re.sub("&#?\\w+;", fixup, text)
11 years ago
def GetDecryptedBook(infile, kDatabases, androidFiles, serials, pids, starttime = time.time()):
11 years ago
# handle the obvious cases at the beginning
if not os.path.isfile(infile):
raise DrmException("Input file does not exist.")
11 years ago
mobi = True
magic8 = open(infile,'rb').read(8)
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.")
magic3 = magic8[:3]
if magic3 == b'TPZ':
11 years ago
mobi = False
if magic8[:4] == b'PK\x03\x04':
mb = kfxdedrm.KFXZipBook(infile)
elif mobi:
11 years ago
mb = mobidedrm.MobiBook(infile)
else:
mb = topazextract.TopazBook(infile)
bookname = unescape(mb.getBookTitle())
print("Decrypting {1} ebook: {0}".format(bookname, mb.getBookType()))
11 years ago
# 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
11 years ago
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:
11 years ago
mb.processBook(totalpids)
except:
mb.cleanup
raise
print("Decryption succeeded after {0:.1f} seconds".format(time.time()-starttime))
11 years ago
return mb
11 years ago
# kDatabaseFiles is a list of files created by kindlekey
def decryptBook(infile, outdir, kDatabaseFiles, androidFiles, serials, pids):
11 years ago
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))
11 years ago
traceback.print_exc()
11 years ago
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))
11 years ago
traceback.print_exc()
return 1
# 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)
else: # E Ink Kindle, which already uses a reasonable name
outfilename = orig_fn_root
11 years ago
# avoid excessively long file names
if len(outfilename)>150:
outfilename = outfilename[:99]+"--"+outfilename[-49:]
outfilename = outfilename+"_nodrm"
11 years ago
outfile = os.path.join(outdir, outfilename + book.getBookExtension())
11 years ago
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")
11 years ago
book.getSVGZip(zipname)
print("Saved SVG ZIP Archive for {1:s} after {0:.1f} seconds".format(time.time()-starttime, outfilename))
11 years ago
# remove internal temporary directory of Topaz pieces
book.cleanup()
return 0
11 years ago
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))
11 years ago
#
# Main
#
def cli_main():
argv=unicode_argv()
progname = os.path.basename(argv[0])
print("K4MobiDeDrm v{0}.\nCopyright © 2008-2020 Apprentice Harper et al.".format(__version__))
11 years ago
11 years ago
try:
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]))
11 years ago
usage(progname)
sys.exit(2)
if len(args)<2:
usage(progname)
sys.exit(2)
infile = args[0]
outdir = args[1]
kDatabaseFiles = []
androidFiles = []
11 years ago
serials = []
pids = []
11 years ago
for o, a in opts:
if o == "-h":
usage(progname)
sys.exit(0)
11 years ago
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',')
11 years ago
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)
11 years ago
return decryptBook(infile, outdir, kDatabaseFiles, androidFiles, serials, pids)
11 years ago
if __name__ == '__main__':
sys.stdout=SafeUnbuffered(sys.stdout)
sys.stderr=SafeUnbuffered(sys.stderr)
sys.exit(cli_main())