]> xmof Git - DeDRM.git/commitdiff
Trying to add ability to load raw key files
authorSatsuoni <satsuoni@gmail.com>
Sun, 18 May 2025 08:32:52 +0000 (17:32 +0900)
committerSatsuoni <satsuoni@gmail.com>
Sun, 18 May 2025 08:32:52 +0000 (17:32 +0900)
DeDRM_plugin/__init__.py
DeDRM_plugin/__version.py
DeDRM_plugin/config.py
DeDRM_plugin/ion.py
DeDRM_plugin/k4mobidedrm.py
DeDRM_plugin/kfxdedrm.py
DeDRM_plugin/prefs.py

index 8200122779e3ab24c88610b89a0ec2313cd5c6b0..bacea0316c1e81f292df81d8c3adcc7eb0c72a63 100644 (file)
@@ -927,7 +927,7 @@ class DeDRM(FileTypePlugin):
         kindleDatabases = list(dedrmprefs['kindlekeys'].items())
 
         try:
-            book = k4mobidedrm.GetDecryptedBook(path_to_ebook,kindleDatabases,androidFiles,serials,pids,self.starttime)
+            book = k4mobidedrm.GetDecryptedBook(path_to_ebook,kindleDatabases,androidFiles,serials,pids,self.starttime,dedrmprefs["kindleextrakeyfile"])
         except Exception as e:
             decoded = False
             # perhaps we need to get a new default Kindle for Mac/PC key
index e83b3a41b9d39d8a3277fef57091d3576fab20f4..4f2d03b15fa20ea644e8bd9ad3435a72a3c32cda 100644 (file)
@@ -4,7 +4,7 @@
 #@@CALIBRE_COMPAT_CODE@@
 
 PLUGIN_NAME = "DeDRM"
-__version__ = '10.0.9'
+__version__ = '10.0.10'
 
 PLUGIN_VERSION_TUPLE = tuple([int(x) for x in __version__.split(".")])
 PLUGIN_VERSION = ".".join([str(x)for x in PLUGIN_VERSION_TUPLE])
index 19b40fda1bcdd8fa31c6eaa4c2a05b002c0ace1c..a9de709194ee62d709d6ee444e85492da03aa998 100755 (executable)
@@ -95,7 +95,7 @@ class ConfigWidget(QWidget):
         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'])
-
+        self.tempdedrmprefs['kindleextrakeyfile']=self.dedrmprefs['kindleextrakeyfile']
         # Start Qt Gui dialog layout
         layout = QVBoxLayout(self)
         self.setLayout(layout)
@@ -157,6 +157,24 @@ class ConfigWidget(QWidget):
         button_layout.addWidget(self.kindle_serial_button)
         button_layout.addWidget(self.kindle_android_button)
         button_layout.addWidget(self.kindle_key_button)
+        ## mine 
+        self._hbox = QWidget ()
+        #self._hbox.setFlat(True)
+        hlayout = QHBoxLayout()
+        self._extrakeyfile_ledit = QLineEdit(self.tempdedrmprefs['kindleextrakeyfile'], self)
+        self._extrakeyfile_ledit.setReadOnly(True)
+        hlayout.addWidget(self._extrakeyfile_ledit)
+        
+        self._select_extrakeyfile_button = QPushButton("")
+        self._select_extrakeyfile_button.setIcon(QIcon(I('document_open.png')))
+        self._select_extrakeyfile_button.setToolTip("Select a file with extra key candidates for kindle books")
+        self._select_extrakeyfile_button.clicked.connect(self.select_kindleextrakeyfile_dialog)
+
+        hlayout.addWidget(self._select_extrakeyfile_button)
+        self._hbox.setLayout(hlayout)
+        self._hbox.resize(self._hbox.sizeHint())
+        button_layout.addWidget(self._hbox)
+        ##
         button_layout.addSpacing(15)
         button_layout.addWidget(self.adept_button)
         button_layout.addWidget(self.bandn_button)
@@ -178,7 +196,34 @@ class ConfigWidget(QWidget):
         button_layout.addWidget(self.chkRemoveWatermarks)
 
         self.resize(self.sizeHint())
-
+    def select_kindleextrakeyfile_dialog(self):
+        unique_dlg_name = PLUGIN_NAME + "import Kindle book key candidates".replace(' ', '_') #takes care of automatically remembering last directory
+        caption = "Select file generated by key extractor to import"
+        filters = []
+        files = choose_files(self, unique_dlg_name, caption, filters, all_files=False,select_only_single_file=True)
+        counter = 0
+        skipped = 0
+        if files:
+            for filename in files:
+                #print(filename)
+                fpath = os.path.join(config_dir, filename)
+                filename = os.path.basename(filename)
+                new_key_name = os.path.splitext(os.path.basename(filename))[0]
+                goodenough=False
+                print(fpath)
+                with open(fpath,'r') as keyfile:
+                    for line in keyfile: #just check one
+                        sline=line.strip()
+                        ls=sline.split("$")
+                        #print(ls)
+                        if len(ls)>1:
+                            goodenough=True
+                        break
+                if goodenough:
+                    self.tempdedrmprefs['kindleextrakeyfile']=fpath
+                    self._extrakeyfile_ledit.setText(fpath)
+                    counter=1
+        return counter > 0
     def kindle_serials(self):
         d = ManageKeysDialog(self,"EInk Kindle Serial Number",self.tempdedrmprefs['serials'], AddSerialDialog)
         d.exec_()
@@ -251,6 +296,7 @@ class ConfigWidget(QWidget):
         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.set('kindleextrakeyfile', self.tempdedrmprefs['kindleextrakeyfile'])
         self.dedrmprefs.writeprefs()
 
     def load_resource(self, name):
index 33a2b05308b0be51afe18b2121c27b279f017c68..d2257cbf5eb3a947497e26553761d6bbe4b51cc5 100644 (file)
@@ -57,8 +57,10 @@ except ImportError:
             except ImportError:
                 # Windows-friendly choice: pylzma wheels
                 import pylzma as lzma
-
-from .kfxtables import *
+try:
+ from .kfxtables import *
+except:
+ from kfxtables import *
 
 TID_NULL = 0
 TID_BOOLEAN = 1
@@ -744,7 +746,9 @@ SYM_NAMES = [ 'com.amazon.drm.Envelope@1.0',
               ] + ['com.amazon.drm.VoucherEnvelope@%d.0' % n
                    for n in list(range(2, 29)) + [
                                    9708, 1031, 2069, 9041, 3646,
-                                   6052, 9479, 9888, 4648, 5683]]
+                                   6052, 9479, 9888, 4648, 5683,7384,2746,3332]+list(range(10001,11111))] #this assumes there are no new types added aside from voucher envelopes. So far it was largely correct
+                                   
+
 
 def addprottable(ion):
     ion.addtocatalog("ProtectedData", 1, SYM_NAMES)
@@ -1176,7 +1180,7 @@ def obfuscate(secret, version):
     if version == 1:  # v1 does not use obfuscation
         return secret
 
-    magic, word = OBFUSCATION_TABLE["V%d" % version]
+    magic, word = OBFUSCATION_TABLE.get("V%d" % version,(1,b"unknown"))
 
     # extend secret so that its length is divisible by the magic number
     if len(secret) % magic != 0:
@@ -1210,7 +1214,7 @@ def scramble(st,magic):
 def obfuscate2(secret, version):
     if version == 1:  # v1 does not use obfuscation
         return secret
-    magic, word = OBFUSCATION_TABLE["V%d" % version]
+    magic, word = OBFUSCATION_TABLE.get("V%d" % version,(1,b"unknown"))
     # extend secret so that its length is divisible by the magic number
     if len(secret) % magic != 0:
         secret = secret + b'\x00' * (magic - len(secret) % magic)
@@ -1281,7 +1285,7 @@ def scramble3(st,magic):
 def obfuscate3(secret, version):
     if version == 1:  # v1 does not use obfuscation
         return secret
-    magic, word = OBFUSCATION_TABLE["V%d" % version]
+    magic, word = OBFUSCATION_TABLE.get("V%d" % version,(1,b"unknown"))
     # extend secret so that its length is divisible by the magic number
     if len(secret) % magic != 0:
         secret = secret + b'\x00' * (magic - len(secret) % magic)
@@ -1296,6 +1300,28 @@ def obfuscate3(secret, version):
         obfuscated[i] = shuffled[i] ^ wordhash[i % 16]
     return obfuscated
 
+class SKeyList(object):
+    def __init__(self, skeyfile):
+      self.keycandidates={}
+      self.secretkeys={} #let us hope there is one key per voucher...
+      if skeyfile is None: return
+      if os.path.isfile(skeyfile):
+          with open(skeyfile,"r",encoding="utf8") as fl:
+              for line in fl:
+                  sline=line.strip()
+                  if len(sline)<32: continue 
+                  lst=sline.split("$")
+                  if len(lst)<2: continue 
+                  voucherid=lst[0]
+                  for key in lst[1:]:
+                      skey=key.split(":")
+                      if skey[0]=="secret_key":
+                          self.secretkeys[voucherid]=bytes.fromhex(skey[1])
+                      elif skey[0]=="shared_key":
+                          curlist=self.keycandidates.get(voucherid,[])
+                          curlist.append(bytes.fromhex(skey[1]))
+                          self.keycandidates[voucherid]=curlist
+                          
 class DrmIonVoucher(object):
     envelope = None
     version = None
@@ -1313,7 +1339,7 @@ class DrmIonVoucher(object):
     cipheriv = b""
     secretkey = b""
 
-    def __init__(self, voucherenv, dsn, secret):
+    def __init__(self, voucherenv, dsn, secret,skeylist=None):
         self.dsn, self.secret = dsn, secret
 
         if isinstance(dsn, str):
@@ -1323,7 +1349,10 @@ class DrmIonVoucher(object):
             self.secret = secret.encode('ASCII')
 
         self.lockparams = []
-
+        self.keycandidates=[]
+        self.secretkeycandidate=None
+        self.skeylist=skeylist
+        self.voucher_id=""
         self.envelope = BinaryIonParser(voucherenv)
         addprottable(self.envelope)
 
@@ -1348,8 +1377,9 @@ class DrmIonVoucher(object):
 
         decrypted=False
         lastexception = None # type: Exception | None
-        for sharedsecret in sharedsecrets:
-            key = hmac.new(sharedsecret, b"PIDv3", digestmod=hashlib.sha256).digest()
+        keycandidates=self.keycandidates+[hmac.new(sharedsecret, b"PIDv3", digestmod=hashlib.sha256).digest() for sharedsecret in sharedsecrets]
+        for key in keycandidates:
+            print(f"{key.hex()} {self.cipheriv[:16].hex()}")
             aes = AES.new(key[:32], AES.MODE_CBC, self.cipheriv[:16])
             try:
                 b = aes.decrypt(self.ciphertext)
@@ -1367,7 +1397,15 @@ class DrmIonVoucher(object):
                 lastexception = ex
                 print("Decryption failed, trying next fallback ")
         if not decrypted:
-            raise lastexception
+            if self.secretkeycandidate is None:
+              print("Failed all decryption attempts and no key candidate available")
+              raise lastexception
+            else:
+                print("Failed all decryption attempts but we have a key candidate")
+                self.secretkey =self.secretkeycandidate
+                self.drmkey=None
+                return
+                 
 
         self.drmkey.stepin()
         while self.drmkey.hasnext():
@@ -1393,8 +1431,10 @@ class DrmIonVoucher(object):
     def parse(self):
         self.envelope.reset()
         _assert(self.envelope.hasnext(), "Envelope is empty")
-        _assert(self.envelope.next() == TID_STRUCT and str.startswith(self.envelope.gettypename(), "com.amazon.drm.VoucherEnvelope@"),
+        tn=self.envelope.gettypename()
+        _assert(self.envelope.next() == TID_STRUCT and str.startswith(tn, "com.amazon.drm.VoucherEnvelope@"),
                 "Unknown type encountered in envelope, expected VoucherEnvelope")
+        print(f"Envelope version {tn}")
         self.version = int(self.envelope.gettypename().split('@')[1][:-2])
 
         self.envelope.stepin()
@@ -1430,7 +1470,13 @@ class DrmIonVoucher(object):
             self.envelope.stepout()
 
         self.parsevoucher()
-
+        if self.skeylist is not None:
+            self.keycandidates=self.skeylist.keycandidates.get(self.voucher_id,[])
+            print(f"Got {len(self.keycandidates)} shared key candidates {self.skeylist.keycandidates} {self.voucher_id}")
+            self.secretkeycandidate=self.skeylist.secretkeys.get(self.voucher_id,None)
+            if self.secretkeycandidate is not None:
+                print(f"Got secret key candidate from file: {self.secretkeycandidate.hex()}")
+           
     def parsevoucher(self):
         _assert(self.voucher.hasnext(), "Voucher is empty")
         _assert(self.voucher.next() == TID_STRUCT and self.voucher.gettypename() == "com.amazon.drm.Voucher@1.0",
@@ -1444,6 +1490,8 @@ class DrmIonVoucher(object):
                 self.cipheriv = self.voucher.lobvalue()
             elif self.voucher.getfieldname() == "cipher_text":
                 self.ciphertext = self.voucher.lobvalue()
+            elif self.voucher.getfieldname() == "id":
+                self.voucher_id = self.voucher.stringvalue()
             elif self.voucher.getfieldname() == "license":
                 _assert(self.voucher.gettypename() == "com.amazon.drm.License@1.0",
                         "Unknown license: %s" % self.voucher.gettypename())
index 4a22318fb2fc757d6cd292d846cb98344275dfde..ea7dfc1b0a4363780a80f1d870ba05c77d981016 100644 (file)
@@ -140,7 +140,7 @@ def unescape(text):
         return text # leave as is
     return re.sub("&#?\\w+;", fixup, text)
 
-def GetDecryptedBook(infile, kDatabases, androidFiles, serials, pids, starttime = time.time()):
+def GetDecryptedBook(infile, kDatabases, androidFiles, serials, pids, starttime = time.time(),skeyfile=None):
     # handle the obvious cases at the beginning
     if not os.path.isfile(infile):
         raise DrmException("Input file does not exist.")
@@ -155,7 +155,7 @@ def GetDecryptedBook(infile, kDatabases, androidFiles, serials, pids, starttime
         mobi = False
 
     if magic8[:4] == b'PK\x03\x04':
-        mb = kfxdedrm.KFXZipBook(infile)
+        mb = kfxdedrm.KFXZipBook(infile,skeyfile)
     elif mobi:
         mb = mobidedrm.MobiBook(infile)
     else:
index 419653e6abc4b31768826f009d344a6b86e24969..071e326e4b74f6d8ea567becdd75ace873d2e29d 100644 (file)
@@ -19,7 +19,7 @@ from io import BytesIO
 #@@CALIBRE_COMPAT_CODE@@
 
 
-from ion import DrmIon, DrmIonVoucher
+from ion import DrmIon, DrmIonVoucher, SKeyList
 
 
 
@@ -28,8 +28,9 @@ __version__ = '2.0'
 
 
 class KFXZipBook:
-    def __init__(self, infile):
+    def __init__(self, infile,skeyfile=None):
         self.infile = infile
+        self.skeylist=SKeyList(skeyfile)
         self.voucher = None
         self.decrypted = {}
 
@@ -81,7 +82,7 @@ class KFXZipBook:
                 continue
 
             try:
-                voucher = DrmIonVoucher(BytesIO(data), pid[:dsn_len], pid[dsn_len:])
+                voucher = DrmIonVoucher(BytesIO(data), pid[:dsn_len], pid[dsn_len:],self.skeylist)
                 voucher.parse()
                 voucher.decryptvoucher()
                 break
index 0ae39434c030c8d92fc27f69830a52ed16bc640b..f03f2126fdd0471adf6b735b4503136af91dc42f 100755 (executable)
@@ -42,6 +42,7 @@ class DeDRM_Prefs():
         self.dedrmprefs.defaults['adobe_pdf_passphrases'] = []
         self.dedrmprefs.defaults['adobewineprefix'] = ""
         self.dedrmprefs.defaults['kindlewineprefix'] = ""
+        self.dedrmprefs.defaults['kindleextrakeyfile'] = ""
 
         # initialise
         # we must actually set the prefs that are dictionaries and lists