]> xmof Git - DeDRM.git/commitdiff
Second release of kindlepid and kindlefix
authorIgor Skochinsky <igorsk@gmail.com>
Tue, 10 Mar 2009 02:10:10 +0000 (02:10 +0000)
committerApprentice Alf <apprenticealf@gmail.com>
Thu, 26 Feb 2015 17:52:33 +0000 (17:52 +0000)
With needed prc library mistakenly left off last commit

Kindle_Mobi_Tools/lib/kindlefix.py
Kindle_Mobi_Tools/lib/kindlepid.py
Kindle_Mobi_Tools/lib/prc.py [new file with mode: 0644]
Kindle_Mobi_Tools/lib/readme.txt

index 728191556a59a823280e4c08b251f9e407938d2d..492faceda3ddaca40580e97d813da19fff5395a3 100644 (file)
@@ -1,4 +1,16 @@
+#!/usr/bin/python
+# The Kindleizer v0.2. Copyright (c) 2007, 2009 Igor Skochinsky <skochinsky@mail.ru>
+# This script enables encrypted Mobipocket books to be readable by Kindle
+# History: 
+#  0.1 initial release
+#  0.2 fixed corrupted metadata issue (thanks to Mark Peek)
+
 import prc, sys, struct
+
+if sys.hexversion >= 0x3000000:
+  print "This script is incompatible with Python 3.x. Please install Python 2.6.x from python.org"
+  sys.exit(2)
+
 from binascii import hexlify
 
 def strByte(s,off=0):
@@ -92,7 +104,7 @@ def find_key(rec0, pid):
             drmInfo = strPutDWord(drmInfo,4,(dw4|0x800))
             dw0, dw4, dw18, dw1c = struct.unpack(">II16xII", drmInfo)
             #print "Updated drmInfo:", "%08X, %08X, %s, %08X, %08X"%(dw0, dw4, hexlify(drmInfo[0x8:0x18]), dw18, dw1c)
-            return rec0[:iOff+0x10] + PC1(temp_key, drmInfo, False) + rec0[:iOff+0x30]
+            return rec0[:iOff+0x10] + PC1(temp_key, drmInfo, False) + rec0[iOff+0x30:]
       iOff += dwSize
     return None
 
@@ -147,7 +159,7 @@ def main(fname, pid):
   print "Output written to "+outfname
   return 0
 
-print "The Kindleizer v0.1. Copyright (c) 2007 Igor Skochinsky"
+print "The Kindleizer v0.2. Copyright (c) 2007, 2009 Igor Skochinsky"
 if len(sys.argv)<3:
   print "Fixes encrypted Mobipocket books to be readable by Kindle"
   print "Usage: kindlefix.py file.mobi PID"
index 70a3f35b184936e044e063340e62a31941458e37..5eaa37a434d088febd209cff1379247fb2aea997 100644 (file)
@@ -1,5 +1,16 @@
+#!/usr/bin/python
+# Mobipocket PID calculator v0.2 for Amazon Kindle.
+# Copyright (c) 2007, 2009 Igor Skochinsky <skochinsky@mail.ru>
+# History:
+#  0.1 Initial release
+#  0.2 Added support for generating PID for iPhone (thanks to mbp)
+
 import sys, binascii
 
+if sys.hexversion >= 0x3000000:
+  print "This script is incompatible with Python 3.x. Please install Python 2.6.x from python.org"
+  sys.exit(2)
+
 letters = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789"
 
 def crc32(s):
@@ -37,9 +48,25 @@ def pidFromSerial(s, l):
 
   return pid
 
-print "Mobipocket PID calculator for Amazon Kindle. Copyright (c) 2007 Igor Skochinsky <skochinsky@mail.ru>"
+print "Mobipocket PID calculator for Amazon Kindle. Copyright (c) 2007, 2009 Igor Skochinsky"
 if len(sys.argv)>1:
-  pid = pidFromSerial(sys.argv[1],7)+"*"
-  print "Mobipocked PID for Kindle serial# "+sys.argv[1]+" is "+checksumPid(pid)
+  serial = sys.argv[1]
+  if len(serial)==16:
+    if serial.startswith("B001"):
+      print "Kindle 1 serial number detected"
+    elif serial.startswith("B002"):
+      print "Kindle 2 serial number detected"
+    else:
+      print "Warning: unrecognized serial number. Please recheck input."
+      sys.exit(1)
+    pid = pidFromSerial(serial,7)+"*"
+    print "Mobipocked PID for Kindle serial# "+serial+" is "+checksumPid(pid)
+  elif len(serial)==40:
+    print "iPhone serial number (UDID) detected"
+    pid = pidFromSerial(serial,8)
+    print "Mobipocked PID for iPhone serial# "+serial+" is "+checksumPid(pid)
+  else:
+    print "Warning: unrecognized serial number. Please recheck input."
+    sys.exit(1)
 else:
-  print "Usage: kindlepid.py <Kindle Serial Number>"
+  print "Usage: kindlepid.py <Kindle Serial Number>/<iPhone/iPod Touch UDID>"
diff --git a/Kindle_Mobi_Tools/lib/prc.py b/Kindle_Mobi_Tools/lib/prc.py
new file mode 100644 (file)
index 0000000..c65370c
--- /dev/null
@@ -0,0 +1,529 @@
+#
+#  $Id: prc.py,v 1.3 2001/12/27 08:48:02 rob Exp $
+#
+#  Copyright 1998-2001 Rob Tillotson <rob@pyrite.org>
+#  All Rights Reserved
+#
+#  Permission to use, copy, modify, and distribute this software and
+#  its documentation for any purpose and without fee or royalty is
+#  hereby granted, provided that the above copyright notice appear in
+#  all copies and that both the copyright notice and this permission
+#  notice appear in supporting documentation or portions thereof,
+#  including modifications, that you you make.
+#
+#  THE AUTHOR ROB TILLOTSON DISCLAIMS ALL WARRANTIES WITH REGARD TO
+#  THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+#  AND FITNESS.  IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+#  SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
+#  RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
+#  CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+#  CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE!
+#
+"""PRC/PDB file I/O in pure Python.
+
+  This module serves two purposes: one, it allows access to Palm OS(tm)
+  database files on the desktop in pure Python without requiring
+  pilot-link (hence, it may be useful for import/export utilities),
+  and two, it caches the contents of the file in memory so it can
+  be freely modified using an identical API to databases over a
+  DLP connection.
+"""
+
+__version__ = '$Id: prc.py,v 1.3 2001/12/27 08:48:02 rob Exp $'
+
+__copyright__ = 'Copyright 1998-2001 Rob Tillotson <robt@debian.org>'
+
+
+# temporary hack until we get gettext support again
+def _(s): return s
+
+#
+# DBInfo structure:
+#
+#   int more
+#   unsigned int flags
+#   unsigned int miscflags
+#   unsigned long type
+#   unsigned long creator
+#   unsigned int version
+#   unsigned long modnum
+#   time_t createDate, modifydate, backupdate
+#   unsigned int index
+#   char name[34]
+#
+#
+# DB Header:
+#   32 name
+#   2  flags
+#   2  version
+#   4  creation time
+#   4  modification time
+#   4  backup time
+#   4  modification number
+#   4  appinfo offset
+#   4  sortinfo offset
+#   4  type
+#   4  creator
+#   4  unique id seed (garbage?)
+#   4  next record list id (normally 0)
+#   2  num of records for this header
+#   (maybe 2 more bytes)
+#
+# Resource entry header: (if low bit of attr = 1)
+#   4  type
+#   2  id
+#   4  offset
+#
+# record entry header: (if low bit of attr = 0)
+#   4  offset
+#   1  attributes
+#   3  unique id
+#
+# then 2 bytes of 0
+#
+# then appinfo then sortinfo
+#
+
+import sys, os, stat, struct
+
+PI_HDR_SIZE = 78
+PI_RESOURCE_ENT_SIZE = 10
+PI_RECORD_ENT_SIZE = 8
+
+PILOT_TIME_DELTA = 2082844800L
+
+flagResource = 0x0001
+flagReadOnly = 0x0002
+flagAppInfoDirty = 0x0004
+flagBackup = 0x0008
+flagOpen = 0x8000
+# 2.x
+flagNewer = 0x0010
+flagReset = 0x0020
+#
+flagExcludeFromSync = 0x0080
+
+attrDeleted = 0x80
+attrDirty = 0x40
+attrBusy = 0x20
+attrSecret = 0x10
+attrArchived = 0x08
+
+default_info = {
+    'name': '',
+    'type': 'DATA',
+    'creator': '    ',
+    'createDate': 0,
+    'modifyDate': 0,
+    'backupDate': 0,
+    'modnum': 0,
+    'version': 0,
+    'flagReset': 0,
+    'flagResource': 0,
+    'flagNewer': 0,
+    'flagExcludeFromSync': 0,
+    'flagAppInfoDirty': 0,
+    'flagReadOnly': 0,
+    'flagBackup': 0,
+    'flagOpen': 0,
+    'more': 0,
+    'index': 0
+    }
+
+def null_terminated(s):
+    for x in range(0, len(s)):
+       if s[x] == '\000': return s[:x]
+    return s
+
+def trim_null(s):
+    return string.split(s, '\0')[0]
+
+def pad_null(s, l):    
+    if len(s) > l - 1:
+        s = s[:l-1]
+       s = s + '\0'
+    if len(s) < l: s = s + '\0' * (l - len(s))
+    return s
+
+#
+# new stuff
+
+# Record object to be put in tree...
+class PRecord:
+    def __init__(self, attr=0, id=0, category=0, raw=''):
+       self.raw = raw
+       self.id = id
+       self.attr = attr
+       self.category = category
+
+    # comparison and hashing are done by ID;
+    # thus, the id value *may not be changed* once
+    # the object is created.
+    def __cmp__(self, obj):
+       if type(obj) == type(0):
+           return cmp(self.id, obj)
+       else:
+           return cmp(self.id, obj.id)
+
+    def __hash__(self):
+       return self.id
+
+class PResource:
+    def __init__(self, typ='    ', id=0, raw=''):
+       self.raw = raw
+       self.id = id
+       self.type = typ
+
+    def __cmp__(self, obj):
+       if type(obj) == type(()):
+           return cmp( (self.type, self.id), obj)
+       else:
+           return cmp( (self.type, self.id), (obj.type, obj.id) )
+
+    def __hash__(self):
+       return hash((self.type, self.id))
+    
+
+class PCache:
+    def __init__(self):
+       self.data = []
+       self.appblock = ''
+       self.sortblock = ''
+       self.dirty = 0
+       self.next = 0
+       self.info = {}
+       self.info.update(default_info)
+       # if allow_zero_ids is 1, then this prc behaves appropriately
+       # for a desktop database.  That is, it never attempts to assign
+       # an ID, and lets new records be inserted with an ID of zero.
+       self.allow_zero_ids = 0
+       
+    # pi-file API
+    def getRecords(self): return len(self.data)
+    def getAppBlock(self): return self.appblock and self.appblock or None
+    def setAppBlock(self, raw):
+       self.dirty = 1
+       self.appblock = raw
+    def getSortBlock(self): return self.sortblock and self.sortblock or None
+    def setSortBlock(self, raw):
+       self.dirty = 1
+       self.appblock = raw
+    def checkID(self, id): return id in self.data
+    def getRecord(self, i):
+       try: r = self.data[i]
+       except: return None
+       return r.raw, i, r.id, r.attr, r.category
+    def getRecordByID(self, id):
+       try:
+           i = self.data.index(id)
+           r = self.data[i]
+       except: return None
+       return r.raw, i, r.id, r.attr, r.category
+    def getResource(self, i):
+       try: r = self.data[i]
+       except: return None
+       return r.raw, r.type, r.id
+    def getDBInfo(self): return self.info
+    def setDBInfo(self, info):
+       self.dirty = 1
+       self.info = {}
+       self.info.update(info)
+
+    def updateDBInfo(self, info):
+        self.dirty = 1
+        self.info.update(info)
+        
+    def setRecord(self, attr, id, cat, data):
+       if not self.allow_zero_ids and not id:
+           if not len(self.data): id = 1
+           else:
+               xid = self.data[0].id + 1
+               while xid in self.data: xid = xid + 1
+               id = xid
+               
+       r = PRecord(attr, id, cat, data)
+       if id and id in self.data:
+           self.data.remove(id)
+       self.data.append(r)
+       self.dirty = 1
+       return id
+    
+    def setRecordIdx(self, i, data):
+       self.data[i].raw = data
+       self.dirty = 1
+    
+    def setResource(self, typ, id, data):
+       if (typ, id) in self.data:
+           self.data.remove((typ,id))
+       r = PResource(typ, id, data)
+       self.data.append(r)
+       self.dirty = 1
+       return id
+           
+    def getNextRecord(self, cat):
+       while self.next < len(self.data):
+           r = self.data[self.next]
+           i = self.next
+           self.next = self.next + 1
+           if r.category == cat:
+               return r.raw, i, r.id, r.attr, r.category
+       return ''
+
+    def getNextModRecord(self, cat=-1):
+       while self.next < len(self.data):
+           r = self.data[self.next]
+           i = self.next
+           self.next = self.next + 1
+           if (r.attr & attrModified) and (cat < 0 or r.category == cat):
+               return r.raw, i, r.id, r.attr, r.category
+
+    def getResourceByID(self, type, id):
+       try: r = self.data[self.data.index((type,id))]
+       except: return None
+       return r.raw, r.type, r.id
+
+    def deleteRecord(self, id):
+       if not id in self.data: return None
+       self.data.remove(id)
+       self.dirty = 1
+
+    def deleteRecords(self):
+       self.data = []
+       self.dirty = 1
+
+    def deleteResource(self, type, id):
+       if not (type,id) in self.data: return None
+       self.data.remove((type,id))
+       self.dirty = 1
+
+    def deleteResources(self):
+       self.data = []
+       self.dirty = 1
+
+    def getRecordIDs(self, sort=0):
+       m = map(lambda x: x.id, self.data)
+       if sort: m.sort()
+       return m
+
+    def moveCategory(self, frm, to):
+       for r in self.data:
+           if r.category == frm:
+               r.category = to
+       self.dirty = 1
+
+    def deleteCategory(self, cat):
+       raise RuntimeError, _("unimplemented")
+
+    def purge(self):
+       ndata = []
+       # change to filter later
+       for r in self.data:
+           if (r.attr & attrDeleted):
+               continue
+           ndata.append(r)
+       self.data = ndata
+       self.dirty = 1
+
+    def resetNext(self):
+       self.next = 0
+
+    def resetFlags(self):
+       # special behavior for resources
+       if not self.info.get('flagResource',0):
+           # use map()
+           for r in self.data:
+               r.attr = r.attr & ~attrDirty
+           self.dirty = 1
+
+import pprint
+class File(PCache):
+    def __init__(self, name=None, read=1, write=0, info={}):
+       PCache.__init__(self)
+       self.filename = name
+       self.info.update(info)
+       self.writeback = write
+       self.isopen = 0
+       
+       if read:
+           self.load(name)
+           self.isopen = 1
+
+    def close(self):
+       if self.writeback and self.dirty:
+           self.save(self.filename)
+       self.isopen = 0
+
+    def __del__(self):
+       if self.isopen: self.close()
+       
+    def load(self, f):
+       if type(f) == type(''): f = open(f, 'rb')
+
+       data = f.read()
+       self.unpack(data)
+       
+    def unpack(self, data):
+       if len(data) < PI_HDR_SIZE: raise IOError, _("file too short")
+       (name, flags, ver, ctime, mtime, btime, mnum, appinfo, sortinfo,
+        typ, creator, uid, nextrec, numrec) \
+        = struct.unpack('>32shhLLLlll4s4sllh', data[:PI_HDR_SIZE])
+
+       if nextrec or appinfo < 0 or sortinfo < 0 or numrec < 0:
+           raise IOError, _("invalid database header")
+
+       self.info = {
+           'name': null_terminated(name),
+           'type': typ,
+           'creator': creator,
+           'createDate': ctime - PILOT_TIME_DELTA,
+           'modifyDate': mtime - PILOT_TIME_DELTA,
+           'backupDate': btime - PILOT_TIME_DELTA,
+           'modnum': mnum,
+           'version': ver,
+           'flagReset': flags & flagReset,
+           'flagResource': flags & flagResource,
+           'flagNewer': flags & flagNewer,
+           'flagExcludeFromSync': flags & flagExcludeFromSync,
+           'flagAppInfoDirty': flags & flagAppInfoDirty,
+           'flagReadOnly': flags & flagReadOnly,
+           'flagBackup': flags & flagBackup,
+           'flagOpen': flags & flagOpen,
+           'more': 0,
+           'index': 0
+        }
+       
+       rsrc = flags & flagResource
+       if rsrc: s = PI_RESOURCE_ENT_SIZE
+       else: s = PI_RECORD_ENT_SIZE
+
+       entries = []
+
+       pos = PI_HDR_SIZE
+       for x in range(0,numrec):
+           hstr = data[pos:pos+s]
+           pos = pos + s
+           if not hstr or len(hstr) < s:
+               raise IOError, _("bad database header")
+
+           if rsrc:
+               (typ, id, offset) = struct.unpack('>4shl', hstr)
+               entries.append((offset, typ, id))
+           else:
+               (offset, auid) = struct.unpack('>ll', hstr)
+               attr = (auid & 0xff000000) >> 24
+               uid = auid & 0x00ffffff
+               entries.append((offset, attr, uid))
+
+       offset = len(data)
+       entries.reverse()
+       for of, q, id in entries:
+           size = offset - of
+           if size < 0: raise IOError, _("bad pdb/prc record entry (size < 0)")
+           d = data[of:offset]
+           offset = of
+           if len(d) != size: raise IOError, _("failed to read record")
+           if rsrc:
+               r = PResource(q, id, d)
+               self.data.append(r)
+           else:
+               r = PRecord(q & 0xf0, id, q & 0x0f, d)
+               self.data.append(r)
+       self.data.reverse()
+
+       if sortinfo:
+           sortinfo_size = offset - sortinfo
+           offset = sortinfo
+       else:
+           sortinfo_size = 0
+
+       if appinfo:
+           appinfo_size = offset - appinfo
+           offset = appinfo
+       else:
+           appinfo_size = 0
+
+       if appinfo_size < 0 or sortinfo_size < 0:
+           raise IOError, _("bad database header (appinfo or sortinfo size < 0)")
+
+       if appinfo_size:
+           self.appblock = data[appinfo:appinfo+appinfo_size]
+           if len(self.appblock) != appinfo_size:
+               raise IOError, _("failed to read appinfo block")
+
+       if sortinfo_size:
+           self.sortblock = data[sortinfo:sortinfo+sortinfo_size]
+           if len(self.sortblock) != sortinfo_size:
+               raise IOError, _("failed to read sortinfo block")
+
+    def save(self, f):
+       """Dump the cache to a file.
+       """
+       if type(f) == type(''): f = open(f, 'wb')
+       
+       # first, we need to precalculate the offsets.
+       if self.info.get('flagResource'):
+           entries_len = 10 * len(self.data)
+       else: entries_len = 8 * len(self.data)
+
+       off = PI_HDR_SIZE + entries_len + 2
+       if self.appblock:
+           appinfo_offset = off
+           off = off + len(self.appblock)
+       else:
+           appinfo_offset = 0
+       if self.sortblock:
+           sortinfo_offset = off
+           off = off + len(self.sortblock)
+       else:
+           sortinfo_offset = 0
+       
+       rec_offsets = []
+       for x in self.data:
+           rec_offsets.append(off)
+           off = off + len(x.raw)
+
+       info = self.info
+       flg = 0
+       if info.get('flagResource',0): flg = flg | flagResource
+       if info.get('flagReadOnly',0): flg = flg | flagReadOnly
+       if info.get('flagAppInfoDirty',0): flg = flg | flagAppInfoDirty
+       if info.get('flagBackup',0): flg = flg | flagBackup
+       if info.get('flagOpen',0): flg = flg | flagOpen
+       if info.get('flagNewer',0): flg = flg | flagNewer
+       if info.get('flagReset',0): flg = flg | flagReset
+       # excludefromsync doesn't actually get stored?
+       hdr = struct.pack('>32shhLLLlll4s4sllh',
+                         pad_null(info.get('name',''), 32),
+                         flg,
+                         info.get('version',0),
+                         info.get('createDate',0L)+PILOT_TIME_DELTA,
+                         info.get('modifyDate',0L)+PILOT_TIME_DELTA,
+                         info.get('backupDate',0L)+PILOT_TIME_DELTA,
+                         info.get('modnum',0),
+                         appinfo_offset, # appinfo
+                         sortinfo_offset, # sortinfo
+                         info.get('type','    '),
+                         info.get('creator','    '),
+                         0, # uid???
+                         0, # nextrec???
+                         len(self.data))
+
+       f.write(hdr)
+
+       entries = []
+       record_data = []
+       rsrc = self.info.get('flagResource')
+       for x, off in map(None, self.data, rec_offsets):
+           if rsrc:
+               record_data.append(x.raw)
+               entries.append(struct.pack('>4shl', x.type, x.id, off))
+           else:
+               record_data.append(x.raw)
+               a = ((x.attr | x.category) << 24) | x.id
+               entries.append(struct.pack('>ll', off, a))
+
+       for x in entries: f.write(x)
+       f.write('\0\0') # padding?  dunno, it's always there.
+       if self.appblock: f.write(self.appblock)
+       if self.sortblock: f.write(self.sortblock)
+       for x in record_data: f.write(x)
index d4eb89dd0fff9c6674d84ab242ec2488a173a40a..3a53cdbb0727363a01f7604a59f9b847f4ce55ff 100644 (file)
@@ -1,18 +1,19 @@
-Kindle Mobipocket tools 0.
-Copyright (c) 2007 Igor Skochinsky
+Kindle Mobipocket tools 0.2
+Copyright (c) 2007, 2009 Igor Skochinsky <skochinsky@mail.ru>
 
 These scripts allow one to read legally purchased Secure Mobipocket books
-on Amazon Kindle.
+on Amazon Kindle or Kindle for iPhone.
 
 * kindlepid.py
-  This script generates Mobipocket PID from the Kindle serial number. That
-  PID can then be added at a Mobi retailer site and used for downloading 
+  This script generates Mobipocket PID from the Kindle serial number or iPhone/iPod Touch
+  identifier (UDID). That PID can then be added at a Mobi retailer site and used for downloading 
   books locked to the Kindle.
   
   Example: 
     
     > kindlepid.py B001BAB012345678
-    Mobipocket PID calculator for Amazon Kindle. Copyright (c) 2007 Igor Skochinsky <skochinsky@mail.ru>
+    Mobipocket PID calculator for Amazon Kindle. Copyright (c) 2007, 2009 Igor Skochinsky
+    Kindle 1 serial number detected
     Mobipocked PID for Kindle serial# B001BAB012345678 is V176CXM*FZ
 
 * kindlefix.py
@@ -23,7 +24,7 @@ on Amazon Kindle.
 
   Example:
     > kindlefix.py MyBook.mobi V176CXM*FZ
-    The Kindleizer v0.1. Copyright (c) 2007 Igor Skochinsky
+    The Kindleizer v0.2. Copyright (c) 2007, 2009 Igor Skochinsky
     Encryption: 2
     Mobi publication type: 2
     Mobi format version: 4
@@ -32,3 +33,6 @@ on Amazon Kindle.
 
 * History
   2007-12-12 Initial release
+  2009-03-10 Updated scripts to version 0.2
+    kindlepid.py: Added support for generating PID for iPhone (thanks to mbp)
+    kindlefix.py: Fixed corrupted metadata issue (thanks to Mark Peek)