Second release of kindlepid and kindlefix

With needed prc library mistakenly left off last commit
This commit is contained in:
Igor Skochinsky 2009-03-10 02:10:10 +00:00 committed by Apprentice Alf
parent fa62e11f8c
commit a186ae1c5a
4 changed files with 585 additions and 13 deletions

View 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"

View 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 "Usage: kindlepid.py <Kindle Serial Number>"
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>/<iPhone/iPod Touch UDID>"

View file

@ -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)

View file

@ -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 <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)