From a186ae1c5acd8226ea491cae1c5bc01cd2c38179 Mon Sep 17 00:00:00 2001 From: Igor Skochinsky Date: Tue, 10 Mar 2009 02:10:10 +0000 Subject: [PATCH] Second release of kindlepid and kindlefix With needed prc library mistakenly left off last commit --- Kindle_Mobi_Tools/lib/kindlefix.py | 16 +- Kindle_Mobi_Tools/lib/kindlepid.py | 35 +- Kindle_Mobi_Tools/lib/prc.py | 529 +++++++++++++++++++++++++++++ Kindle_Mobi_Tools/lib/readme.txt | 18 +- 4 files changed, 585 insertions(+), 13 deletions(-) create mode 100644 Kindle_Mobi_Tools/lib/prc.py diff --git a/Kindle_Mobi_Tools/lib/kindlefix.py b/Kindle_Mobi_Tools/lib/kindlefix.py index 7281915..492face 100644 --- a/Kindle_Mobi_Tools/lib/kindlefix.py +++ b/Kindle_Mobi_Tools/lib/kindlefix.py @@ -1,4 +1,16 @@ +#!/usr/bin/python +# The Kindleizer v0.2. Copyright (c) 2007, 2009 Igor Skochinsky +# 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" diff --git a/Kindle_Mobi_Tools/lib/kindlepid.py b/Kindle_Mobi_Tools/lib/kindlepid.py index 70a3f35..5eaa37a 100644 --- a/Kindle_Mobi_Tools/lib/kindlepid.py +++ b/Kindle_Mobi_Tools/lib/kindlepid.py @@ -1,5 +1,16 @@ +#!/usr/bin/python +# Mobipocket PID calculator v0.2 for Amazon Kindle. +# Copyright (c) 2007, 2009 Igor Skochinsky +# 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 " +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 " + print "Usage: kindlepid.py /" diff --git a/Kindle_Mobi_Tools/lib/prc.py b/Kindle_Mobi_Tools/lib/prc.py new file mode 100644 index 0000000..c65370c --- /dev/null +++ b/Kindle_Mobi_Tools/lib/prc.py @@ -0,0 +1,529 @@ +# +# $Id: prc.py,v 1.3 2001/12/27 08:48:02 rob Exp $ +# +# Copyright 1998-2001 Rob Tillotson +# 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 ' + + +# 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) diff --git a/Kindle_Mobi_Tools/lib/readme.txt b/Kindle_Mobi_Tools/lib/readme.txt index d4eb89d..3a53cdb 100644 --- a/Kindle_Mobi_Tools/lib/readme.txt +++ b/Kindle_Mobi_Tools/lib/readme.txt @@ -1,18 +1,19 @@ -Kindle Mobipocket tools 0.1 -Copyright (c) 2007 Igor Skochinsky +Kindle Mobipocket tools 0.2 +Copyright (c) 2007, 2009 Igor Skochinsky 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 + 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)