2020-09-27 12:54:49 +02:00
|
|
|
|
#!/usr/bin/env python3
|
2012-12-19 14:48:11 +01:00
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
2017-07-04 08:05:51 +02:00
|
|
|
|
# k4mobidedrm.py
|
2020-09-26 22:22:47 +02:00
|
|
|
|
# Copyright © 2008-2020 by Apprentice Harper et al.
|
2013-10-02 20:59:40 +02:00
|
|
|
|
|
2017-07-04 08:05:51 +02:00
|
|
|
|
__license__ = 'GPL v3'
|
2020-09-26 22:22:47 +02:00
|
|
|
|
__version__ = '6.0'
|
2017-07-04 08:05:51 +02:00
|
|
|
|
|
|
|
|
|
# Engine to remove drm from Kindle and Mobipocket ebooks
|
2013-10-02 20:59:40 +02:00
|
|
|
|
# for personal use for archiving and converting your ebooks
|
|
|
|
|
|
|
|
|
|
# PLEASE DO NOT PIRATE EBOOKS!
|
|
|
|
|
|
2015-07-29 19:11:19 +02:00
|
|
|
|
# We want all authors and publishers, and ebook stores to live
|
2013-10-02 20:59:40 +02: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,
|
2017-07-04 08:05:51 +02:00
|
|
|
|
# unswindle, DarkReverser, ApprenticeAlf, and many many others
|
|
|
|
|
|
2013-10-02 20:59:40 +02: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
|
2015-07-29 19:11:19 +02:00
|
|
|
|
# 5.3 - Changed Android support to allow passing of backup .ab files
|
2017-01-12 08:24:42 +01:00
|
|
|
|
# 5.4 - Recognise KFX files masquerading as azw, even if we can't decrypt them yet.
|
2017-07-04 08:05:51 +02:00
|
|
|
|
# 5.5 - Added GPL v3 licence explicitly.
|
2019-03-30 16:02:40 +01:00
|
|
|
|
# 5.6 - Invoke KFXZipBook to handle zipped KFX files
|
|
|
|
|
# 5.7 - Revamp cleanup_name
|
2020-09-26 22:22:47 +02:00
|
|
|
|
# 6.0 - Added Python 3 compatibility for calibre 5.0
|
|
|
|
|
|
2013-10-02 20:59:40 +02:00
|
|
|
|
|
|
|
|
|
import sys, os, re
|
|
|
|
|
import csv
|
|
|
|
|
import getopt
|
2012-12-19 14:48:11 +01:00
|
|
|
|
import re
|
|
|
|
|
import traceback
|
2013-10-02 20:59:40 +02:00
|
|
|
|
import time
|
2022-03-19 15:23:07 +01:00
|
|
|
|
try:
|
|
|
|
|
import html.entities as htmlentitydefs
|
|
|
|
|
except:
|
|
|
|
|
import htmlentitydefs
|
|
|
|
|
|
2013-10-02 20:59:40 +02:00
|
|
|
|
import json
|
2012-12-19 14:48:11 +01:00
|
|
|
|
|
2021-12-29 11:36:59 +01:00
|
|
|
|
#@@CALIBRE_COMPAT_CODE@@
|
2021-12-29 09:26:29 +01:00
|
|
|
|
|
|
|
|
|
|
2012-12-19 14:48:11 +01:00
|
|
|
|
class DrmException(Exception):
|
|
|
|
|
pass
|
|
|
|
|
|
2021-12-29 09:26:29 +01:00
|
|
|
|
import mobidedrm
|
|
|
|
|
import topazextract
|
|
|
|
|
import kgenpids
|
|
|
|
|
import androidkindlekey
|
|
|
|
|
import kfxdedrm
|
2013-10-02 20:59:40 +02:00
|
|
|
|
|
2023-08-03 20:45:06 +02:00
|
|
|
|
from .utilities import SafeUnbuffered
|
2013-10-02 20:59:40 +02:00
|
|
|
|
|
2023-08-03 20:45:06 +02:00
|
|
|
|
from .argv_utils import unicode_argv
|
2022-08-06 20:19:18 +02:00
|
|
|
|
|
2013-10-02 20:59:40 +02: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
|
2019-03-30 16:02:40 +01:00
|
|
|
|
# and some improvements suggested by jhaisley
|
2013-10-02 20:59:40 +02:00
|
|
|
|
def cleanup_name(name):
|
|
|
|
|
# substitute filename unfriendly characters
|
2020-10-14 17:23:49 +02:00
|
|
|
|
name = name.replace("<","[").replace(">","]").replace(" : "," – ").replace(": "," – ").replace(":","—").replace("/","_").replace("\\","_").replace("|","_").replace("\"","\'").replace("*","_").replace("?","")
|
2013-10-02 20:59:40 +02:00
|
|
|
|
# white space to single space, delete leading and trailing while space
|
2020-09-27 12:54:49 +02:00
|
|
|
|
name = re.sub(r"\s", " ", name).strip()
|
2019-03-30 16:02:40 +01:00
|
|
|
|
# delete control characters
|
2020-10-14 17:23:49 +02:00
|
|
|
|
name = "".join(char for char in name if ord(char)>=32)
|
2019-03-30 16:02:40 +01:00
|
|
|
|
# delete non-ascii characters
|
2020-10-14 17:23:49 +02:00
|
|
|
|
name = "".join(char for char in name if ord(char)<=126)
|
2013-10-02 20:59:40 +02:00
|
|
|
|
# remove leading dots
|
2020-09-27 12:54:49 +02:00
|
|
|
|
while len(name)>0 and name[0] == ".":
|
2013-10-02 20:59:40 +02:00
|
|
|
|
name = name[1:]
|
|
|
|
|
# remove trailing dots (Windows doesn't like them)
|
2020-09-27 12:54:49 +02:00
|
|
|
|
while name.endswith("."):
|
2013-10-02 20:59:40 +02:00
|
|
|
|
name = name[:-1]
|
2019-03-30 16:02:40 +01:00
|
|
|
|
if len(name)==0:
|
2020-09-27 12:54:49 +02:00
|
|
|
|
name="DecryptedBook"
|
2013-10-02 20:59:40 +02:00
|
|
|
|
return name
|
|
|
|
|
|
|
|
|
|
# must be passed unicode
|
|
|
|
|
def unescape(text):
|
|
|
|
|
def fixup(m):
|
|
|
|
|
text = m.group(0)
|
2020-09-27 12:54:49 +02:00
|
|
|
|
if text[:2] == "&#":
|
2013-10-02 20:59:40 +02:00
|
|
|
|
# character reference
|
|
|
|
|
try:
|
2020-09-27 12:54:49 +02:00
|
|
|
|
if text[:3] == "&#x":
|
2020-09-26 22:22:47 +02:00
|
|
|
|
return chr(int(text[3:-1], 16))
|
2013-10-02 20:59:40 +02:00
|
|
|
|
else:
|
2020-09-26 22:22:47 +02:00
|
|
|
|
return chr(int(text[2:-1]))
|
2013-10-02 20:59:40 +02:00
|
|
|
|
except ValueError:
|
|
|
|
|
pass
|
|
|
|
|
else:
|
|
|
|
|
# named entity
|
|
|
|
|
try:
|
2022-03-19 15:23:07 +01:00
|
|
|
|
text = chr(htmlentitydefs.name2codepoint[text[1:-1]])
|
2013-10-02 20:59:40 +02:00
|
|
|
|
except KeyError:
|
|
|
|
|
pass
|
|
|
|
|
return text # leave as is
|
2020-09-27 12:54:49 +02:00
|
|
|
|
return re.sub("&#?\\w+;", fixup, text)
|
2013-10-02 20:59:40 +02:00
|
|
|
|
|
2015-07-29 19:11:19 +02:00
|
|
|
|
def GetDecryptedBook(infile, kDatabases, androidFiles, serials, pids, starttime = time.time()):
|
2013-10-02 20:59:40 +02:00
|
|
|
|
# handle the obvious cases at the beginning
|
|
|
|
|
if not os.path.isfile(infile):
|
2020-09-27 12:54:49 +02:00
|
|
|
|
raise DrmException("Input file does not exist.")
|
2013-10-02 20:59:40 +02:00
|
|
|
|
|
|
|
|
|
mobi = True
|
2017-01-12 08:24:42 +01:00
|
|
|
|
magic8 = open(infile,'rb').read(8)
|
2020-10-16 14:22:19 +02:00
|
|
|
|
if magic8 == b'\xeaDRMION\xee':
|
2020-09-27 12:54:49 +02:00
|
|
|
|
raise DrmException("The .kfx DRMION file cannot be decrypted by itself. A .kfx-zip archive containing a DRM voucher is required.")
|
2018-03-13 01:33:33 +01:00
|
|
|
|
|
2017-01-12 08:24:42 +01:00
|
|
|
|
magic3 = magic8[:3]
|
2020-10-16 14:22:19 +02:00
|
|
|
|
if magic3 == b'TPZ':
|
2013-10-02 20:59:40 +02:00
|
|
|
|
mobi = False
|
|
|
|
|
|
2020-10-16 14:22:19 +02:00
|
|
|
|
if magic8[:4] == b'PK\x03\x04':
|
2018-03-13 01:33:33 +01:00
|
|
|
|
mb = kfxdedrm.KFXZipBook(infile)
|
|
|
|
|
elif mobi:
|
2013-10-02 20:59:40 +02:00
|
|
|
|
mb = mobidedrm.MobiBook(infile)
|
|
|
|
|
else:
|
|
|
|
|
mb = topazextract.TopazBook(infile)
|
|
|
|
|
|
2022-03-19 15:23:07 +01: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 20:59:40 +02:00
|
|
|
|
|
|
|
|
|
# copy list of pids
|
|
|
|
|
totalpids = list(pids)
|
2015-07-29 19:11:19 +02:00
|
|
|
|
# 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 20:59:40 +02:00
|
|
|
|
md1, md2 = mb.getPIDMetaInfo()
|
|
|
|
|
totalpids.extend(kgenpids.getPidList(md1, md2, serials, kDatabases))
|
2015-07-29 19:11:19 +02:00
|
|
|
|
# remove any duplicates
|
2017-01-12 08:24:42 +01:00
|
|
|
|
totalpids = list(set(totalpids))
|
2020-09-27 12:54:49 +02:00
|
|
|
|
print("Found {1:d} keys to try after {0:.1f} seconds".format(time.time()-starttime, len(totalpids)))
|
2017-01-12 08:24:42 +01:00
|
|
|
|
#print totalpids
|
2012-12-19 14:48:11 +01:00
|
|
|
|
|
|
|
|
|
try:
|
2013-10-02 20:59:40 +02:00
|
|
|
|
mb.processBook(totalpids)
|
|
|
|
|
except:
|
2021-12-29 09:26:29 +01:00
|
|
|
|
mb.cleanup()
|
2013-10-02 20:59:40 +02:00
|
|
|
|
raise
|
2012-12-19 14:48:11 +01:00
|
|
|
|
|
2020-09-27 12:54:49 +02:00
|
|
|
|
print("Decryption succeeded after {0:.1f} seconds".format(time.time()-starttime))
|
2013-10-02 20:59:40 +02:00
|
|
|
|
return mb
|
2012-12-19 14:48:11 +01:00
|
|
|
|
|
|
|
|
|
|
2013-10-02 20:59:40 +02:00
|
|
|
|
# kDatabaseFiles is a list of files created by kindlekey
|
2015-07-29 19:11:19 +02:00
|
|
|
|
def decryptBook(infile, outdir, kDatabaseFiles, androidFiles, serials, pids):
|
2013-10-02 20:59:40 +02: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])
|
2020-09-26 22:22:47 +02:00
|
|
|
|
except Exception as e:
|
2020-09-27 12:54:49 +02:00
|
|
|
|
print("Error getting database from file {0:s}: {1:s}".format(dbfile,e))
|
2013-10-02 20:59:40 +02:00
|
|
|
|
traceback.print_exc()
|
2013-03-20 11:23:54 +01:00
|
|
|
|
|
|
|
|
|
|
2012-12-19 14:48:11 +01:00
|
|
|
|
|
2013-10-02 20:59:40 +02:00
|
|
|
|
try:
|
2015-07-29 19:11:19 +02:00
|
|
|
|
book = GetDecryptedBook(infile, kDatabases, androidFiles, serials, pids, starttime)
|
2020-09-26 22:22:47 +02:00
|
|
|
|
except Exception as e:
|
2020-09-27 12:54:49 +02:00
|
|
|
|
print("Error decrypting book after {1:.1f} seconds: {0}".format(e.args[0],time.time()-starttime))
|
2013-10-02 20:59:40 +02:00
|
|
|
|
traceback.print_exc()
|
|
|
|
|
return 1
|
2012-12-19 14:48:11 +01:00
|
|
|
|
|
2018-04-18 11:21:44 +02: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
|
2022-04-15 18:02:18 +02:00
|
|
|
|
re.match('^[0-9A-F-]{36}$', orig_fn_root)
|
2018-04-18 11:21:44 +02:00
|
|
|
|
): # Kindle for PC / Mac / Android / Fire / iOS
|
|
|
|
|
clean_title = cleanup_name(book.getBookTitle())
|
2020-09-27 12:54:49 +02:00
|
|
|
|
outfilename = "{}_{}".format(orig_fn_root, clean_title)
|
2018-04-18 11:21:44 +02:00
|
|
|
|
else: # E Ink Kindle, which already uses a reasonable name
|
|
|
|
|
outfilename = orig_fn_root
|
2012-12-19 14:48:11 +01:00
|
|
|
|
|
2013-10-02 20:59:40 +02:00
|
|
|
|
# avoid excessively long file names
|
|
|
|
|
if len(outfilename)>150:
|
2015-07-29 19:11:19 +02:00
|
|
|
|
outfilename = outfilename[:99]+"--"+outfilename[-49:]
|
2012-12-19 14:48:11 +01:00
|
|
|
|
|
2020-09-27 12:54:49 +02:00
|
|
|
|
outfilename = outfilename+"_nodrm"
|
2013-10-02 20:59:40 +02:00
|
|
|
|
outfile = os.path.join(outdir, outfilename + book.getBookExtension())
|
2012-12-19 14:48:11 +01:00
|
|
|
|
|
2013-10-02 20:59:40 +02:00
|
|
|
|
book.getFile(outfile)
|
2020-09-27 12:54:49 +02:00
|
|
|
|
print("Saved decrypted book {1:s} after {0:.1f} seconds".format(time.time()-starttime, outfilename))
|
2012-12-19 14:48:11 +01:00
|
|
|
|
|
2020-09-27 12:54:49 +02:00
|
|
|
|
if book.getBookType()=="Topaz":
|
|
|
|
|
zipname = os.path.join(outdir, outfilename + "_SVG.zip")
|
2013-10-02 20:59:40 +02:00
|
|
|
|
book.getSVGZip(zipname)
|
2020-09-27 12:54:49 +02:00
|
|
|
|
print("Saved SVG ZIP Archive for {1:s} after {0:.1f} seconds".format(time.time()-starttime, outfilename))
|
2012-12-19 14:48:11 +01:00
|
|
|
|
|
2013-10-02 20:59:40 +02:00
|
|
|
|
# remove internal temporary directory of Topaz pieces
|
|
|
|
|
book.cleanup()
|
|
|
|
|
return 0
|
2012-12-19 14:48:11 +01:00
|
|
|
|
|
|
|
|
|
|
2013-10-02 20:59:40 +02:00
|
|
|
|
def usage(progname):
|
2020-09-27 12:54:49 +02:00
|
|
|
|
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))
|
2012-12-19 14:48:11 +01:00
|
|
|
|
|
2013-10-02 20:59:40 +02:00
|
|
|
|
#
|
|
|
|
|
# Main
|
|
|
|
|
#
|
|
|
|
|
def cli_main():
|
2022-08-06 20:19:18 +02:00
|
|
|
|
argv=unicode_argv("k4mobidedrm.py")
|
2013-10-02 20:59:40 +02:00
|
|
|
|
progname = os.path.basename(argv[0])
|
2020-10-16 14:22:19 +02:00
|
|
|
|
print("K4MobiDeDrm v{0}.\nCopyright © 2008-2020 Apprentice Harper et al.".format(__version__))
|
2013-04-05 18:44:48 +02:00
|
|
|
|
|
2013-10-02 20:59:40 +02:00
|
|
|
|
try:
|
2020-10-16 14:22:19 +02:00
|
|
|
|
opts, args = getopt.getopt(argv[1:], "k:p:s:a:h")
|
2020-09-26 22:22:47 +02:00
|
|
|
|
except getopt.GetoptError as err:
|
2020-09-27 12:54:49 +02:00
|
|
|
|
print("Error in options or arguments: {0}".format(err.args[0]))
|
2013-10-02 20:59:40 +02:00
|
|
|
|
usage(progname)
|
|
|
|
|
sys.exit(2)
|
|
|
|
|
if len(args)<2:
|
|
|
|
|
usage(progname)
|
|
|
|
|
sys.exit(2)
|
|
|
|
|
|
|
|
|
|
infile = args[0]
|
|
|
|
|
outdir = args[1]
|
|
|
|
|
kDatabaseFiles = []
|
2015-07-29 19:11:19 +02:00
|
|
|
|
androidFiles = []
|
2013-10-02 20:59:40 +02:00
|
|
|
|
serials = []
|
|
|
|
|
pids = []
|
2012-12-19 14:48:11 +01:00
|
|
|
|
|
2013-10-02 20:59:40 +02:00
|
|
|
|
for o, a in opts:
|
2020-10-16 14:22:19 +02:00
|
|
|
|
if o == "-h":
|
|
|
|
|
usage(progname)
|
|
|
|
|
sys.exit(0)
|
2013-10-02 20:59:40 +02: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")
|
2020-10-04 21:36:12 +02:00
|
|
|
|
pids = a.encode('utf-8').split(b',')
|
2013-10-02 20:59:40 +02:00
|
|
|
|
if o == "-s":
|
|
|
|
|
if a == None :
|
|
|
|
|
raise DrmException("Invalid parameter for -s")
|
|
|
|
|
serials = a.split(',')
|
|
|
|
|
if o == '-a':
|
|
|
|
|
if a == None:
|
2015-08-02 12:09:35 +02:00
|
|
|
|
raise DrmException("Invalid parameter for -a")
|
|
|
|
|
androidFiles.append(a)
|
2013-10-02 20:59:40 +02:00
|
|
|
|
|
2015-07-29 19:11:19 +02:00
|
|
|
|
return decryptBook(infile, outdir, kDatabaseFiles, androidFiles, serials, pids)
|
2013-10-02 20:59:40 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
|
sys.stdout=SafeUnbuffered(sys.stdout)
|
|
|
|
|
sys.stderr=SafeUnbuffered(sys.stderr)
|
|
|
|
|
sys.exit(cli_main())
|