tools v2.1

combined kindle/mobi plugin
This commit is contained in:
Apprentice Alf
2010-10-26 18:18:46 +01:00
parent bf03edd18c
commit 5f0671db7f
110 changed files with 5872 additions and 7089 deletions

View File

@@ -4,15 +4,15 @@
import sys
sys.path.append('lib')
import os, os.path, urllib
import subprocess
from subprocess import Popen, PIPE, STDOUT
import subasyncio
from subasyncio import Process
import Tkinter
import Tkconstants
import tkFileDialog
import tkMessageBox
from scrolltextwidget import ScrolledText
import subprocess
from subprocess import Popen, PIPE, STDOUT
import subasyncio
from subasyncio import Process
class MainDialog(Tkinter.Frame):
def __init__(self, root):
@@ -20,14 +20,14 @@ class MainDialog(Tkinter.Frame):
self.root = root
self.interval = 2000
self.p2 = None
self.status = Tkinter.Label(self, text='Remove Encryption from a Mobi eBook')
self.status = Tkinter.Label(self, text='Remove Encryption from a K4PC, K4M, or Mobi eBook')
self.status.pack(fill=Tkconstants.X, expand=1)
body = Tkinter.Frame(self)
body.pack(fill=Tkconstants.X, expand=1)
sticky = Tkconstants.E + Tkconstants.W
body.grid_columnconfigure(1, weight=2)
Tkinter.Label(body, text='Mobi eBook input file').grid(row=0, sticky=Tkconstants.E)
Tkinter.Label(body, text='K4 or Mobi eBook input file').grid(row=0, sticky=Tkconstants.E)
self.mobipath = Tkinter.Entry(body, width=50)
self.mobipath.grid(row=0, column=1, sticky=sticky)
cwd = os.getcwdu()
@@ -36,21 +36,24 @@ class MainDialog(Tkinter.Frame):
button = Tkinter.Button(body, text="...", command=self.get_mobipath)
button.grid(row=0, column=2)
Tkinter.Label(body, text='Name for Unencrypted Output File').grid(row=1, sticky=Tkconstants.E)
Tkinter.Label(body, text='Directory for the Unencrypted Output File').grid(row=1, sticky=Tkconstants.E)
self.outpath = Tkinter.Entry(body, width=50)
self.outpath.grid(row=1, column=1, sticky=sticky)
self.outpath.insert(0, '')
cwd = os.getcwdu()
cwd = cwd.encode('utf-8')
outname = cwd
self.outpath.insert(0, outname)
button = Tkinter.Button(body, text="...", command=self.get_outpath)
button.grid(row=1, column=2)
Tkinter.Label(body, text='10 Character PID').grid(row=2, sticky=Tkconstants.E)
self.pidnum = Tkinter.StringVar()
self.pidinfo = Tkinter.Entry(body, width=12, textvariable=self.pidnum)
Tkinter.Label(body, text='Comma Separated List of 10 Character PIDs (no spaces)').grid(row=2, sticky=Tkconstants.E)
self.pidnums = Tkinter.StringVar()
self.pidinfo = Tkinter.Entry(body, width=50, textvariable=self.pidnums)
self.pidinfo.grid(row=2, column=1, sticky=sticky)
msg1 = 'Conversion Log \n\n'
self.stext = ScrolledText(body, bd=5, relief=Tkconstants.RIDGE, height=15, width=60, wrap=Tkconstants.WORD)
self.stext.grid(row=3, column=0, columnspan=2,sticky=sticky)
self.stext.grid(row=4, column=0, columnspan=2,sticky=sticky)
self.stext.insert(Tkconstants.END,msg1)
buttons = Tkinter.Frame(self)
@@ -73,8 +76,10 @@ class MainDialog(Tkinter.Frame):
text = self.p2.readerr()
text += self.p2.read()
msg = text + '\n\n' + 'Encryption successfully removed\n'
if poll != 0:
if poll == 1:
msg = text + '\n\n' + 'Error: Encryption Removal Failed\n'
if poll == 2:
msg = text + '\n\n' + 'Input File was Not Encrypted - No Output File Needed\n'
self.showCmdOutput(msg)
self.p2 = None
self.sbotton.configure(state='normal')
@@ -95,16 +100,16 @@ class MainDialog(Tkinter.Frame):
return
# run as a subprocess via pipes and collect stdout
def mobirdr(self, infile, outfile, pidnum):
def mobirdr(self, infile, outfile, pidnums):
# os.putenv('PYTHONUNBUFFERED', '1')
cmdline = 'python ./lib/mobidedrm.py "' + infile + '" "' + outfile + '" "' + pidnum + '"'
if sys.platform[0:3] == 'win':
cmdline = 'python ./lib/k4mobidedrm.py "' + infile + '" "' + outfile + '" "' + pidnums + '"'
if sys.platform.startswith('win'):
search_path = os.environ['PATH']
search_path = search_path.lower()
if search_path.find('python') >= 0:
cmdline = 'python lib\mobidedrm.py "' + infile + '" "' + outfile + '" "' + pidnum + '"'
cmdline = 'python lib\k4mobidedrm.py "' + infile + '" "' + outfile + '" "' + pidnums + '"'
else :
cmdline = 'lib\mobidedrm.py "' + infile + '" "' + outfile + '" "' + pidnum + '"'
cmdline = 'lib\k4mobidedrm.py "' + infile + '" "' + outfile + '" "' + pidnums + '"'
cmdline = cmdline.encode(sys.getfilesystemencoding())
p2 = Process(cmdline, shell=True, bufsize=1, stdin=None, stdout=PIPE, stderr=PIPE, close_fds=False)
@@ -112,8 +117,10 @@ class MainDialog(Tkinter.Frame):
def get_mobipath(self):
cpath = self.mobipath.get()
mobipath = tkFileDialog.askopenfilename(
parent=None, title='Select Mobi eBook File',
initialdir = cpath,
parent=None, title='Select K4PC, K4M or Mobi eBook File',
defaultextension='.prc', filetypes=[('Mobi eBook File', '.prc'), ('Mobi eBook File', '.azw'),('Mobi eBook File', '.mobi'),
('All Files', '.*')])
if mobipath:
@@ -123,15 +130,11 @@ class MainDialog(Tkinter.Frame):
return
def get_outpath(self):
mobipath = self.mobipath.get()
initname = os.path.basename(mobipath)
p = initname.find('.')
if p >= 0: initname = initname[0:p]
initname += '_nodrm.mobi'
outpath = tkFileDialog.asksaveasfilename(
parent=None, title='Select Unencrypted Mobi File to produce',
defaultextension='.mobi', initialfile=initname,
filetypes=[('Mobi files', '.mobi'), ('All files', '.*')])
cwd = os.getcwdu()
cwd = cwd.encode('utf-8')
outpath = tkFileDialog.askdirectory(
parent=None, title='Directory to Store Unencrypted file into',
initialdir=cwd, initialfile=None)
if outpath:
outpath = os.path.normpath(outpath)
self.outpath.delete(0, Tkconstants.END)
@@ -151,29 +154,34 @@ class MainDialog(Tkinter.Frame):
self.sbotton.configure(state='disabled')
mobipath = self.mobipath.get()
outpath = self.outpath.get()
pidnum = self.pidinfo.get()
pidnums = self.pidinfo.get()
if not mobipath or not os.path.exists(mobipath):
self.status['text'] = 'Specified Mobi eBook file does not exist'
self.status['text'] = 'Specified K4PC, K4M or Mobi eBook file does not exist'
self.sbotton.configure(state='normal')
return
if not outpath:
self.status['text'] = 'No output file specified'
self.status['text'] = 'No output directory specified'
self.sbotton.configure(state='normal')
return
if not pidnum or pidnum == '':
self.status['text'] = 'No PID specified'
if not os.path.isdir(outpath):
self.status['text'] = 'Error specified output directory does not exist'
self.sbotton.configure(state='normal')
return
# default output file name to be input file name + '_nodrm.mobi'
initname = os.path.splitext(os.path.basename(mobipath))[0]
initname += '_nodrm.mobi'
outpath += os.sep + initname
log = 'Command = "python mobidedrm.py"\n'
log += 'Mobi Path = "'+ mobipath + '"\n'
log = 'Command = "python k4mobidedrm.py"\n'
log += 'K4PC, K4M or Mobi Path = "'+ mobipath + '"\n'
log += 'Output File = "' + outpath + '"\n'
log += 'PID = "' + pidnum + '"\n'
log += 'PID list = "' + pidnums + '"\n'
log += '\n\n'
log += 'Please Wait ...\n\n'
log = log.encode('utf-8')
self.stext.insert(Tkconstants.END,log)
self.p2 = self.mobirdr(mobipath, outpath, pidnum)
self.p2 = self.mobirdr(mobipath, outpath, pidnums)
# python does not seem to allow you to create
# your own eventloop which every other gui does - strange
@@ -185,7 +193,7 @@ class MainDialog(Tkinter.Frame):
def main(argv=None):
root = Tkinter.Tk()
root.title('Mobi eBook Encryption Removal')
root.title('K4PC/K4M/Mobi eBook Encryption Removal')
root.resizable(True, False)
root.minsize(300, 0)
MainDialog(root).pack(fill=Tkconstants.X, expand=1)

View File

@@ -0,0 +1,23 @@
K4MobiDeDRM
This tools combines functionality of MobiDeDRM with that of K4PCDeDRM, K4MDeDRM, and K4DeDRM. Effectively, it provides one-stop shopping for all your Mobipocket, Kindle for iPhone/iPad/iPodTouch, Kindle for PC, and Kindle for Mac needs.
****
Please Note: If you a happy user of MobiDeDRM, K4DeDRM, K4PCDeDRM, or K4MUnswindle, please continue to use these programs as there is no additional capability provided by this tool over the others. In the long run, if you have problems with any of those tools, you might want to try this one as it will continue under development eventually replacing all of those tools.
****
1. double-click on K4MobiDeDRM.pyw
2. In the window that opens:
hit the first '...' button to locate your DRM Kindle-style ebook
3. Then hit the second '...' button to select an output directory for the unlocked file
4. Then add in any PIDs you need from KindleV1, Kindle for iPhone/iPad/iPodTouch, or other single PID devices to the provided box as a comma separated list of 10 digit PID numbers.
If this is a Kindle for Mac or a Kindle for PC book then you can leave this box blank
5. hit the 'Start' button
After a short delay, you should see progress in the Conversion Log window indicating is the unlocking was a success or failure.

View File

@@ -0,0 +1,490 @@
#!/usr/bin/env python
# engine to remove drm from Kindle for Mac and Kindle for PC books
# for personal use for archiving and converting your ebooks
# PLEASE DO NOT PIRATE EBOOKS!
# We want all authors and publishers, and eBook stores to live
# 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,
# unswindle, DarkReverser, ApprenticeAlf, DiapDealer, some_updates
# and many many others
# It can run standalone to convert K4M/K4PC/Mobi files, or it can be installed as a
# plugin for Calibre (http://calibre-ebook.com/about) so that importing
# K4 or Mobi with DRM is no londer a multi-step process.
#
# ***NOTE*** If you are using this script as a calibre plugin for a K4M or K4PC ebook
# then calibre must be installed on the same machine and in the same account as K4PC or K4M
# for the plugin version to function properly.
#
# To create a Calibre plugin, rename this file so that the filename
# ends in '_plugin.py', put it into a ZIP file with all its supporting python routines
# and import that ZIP into Calibre using its plugin configuration GUI.
from __future__ import with_statement
__version__ = '1.1'
class Unbuffered:
def __init__(self, stream):
self.stream = stream
def write(self, data):
self.stream.write(data)
self.stream.flush()
def __getattr__(self, attr):
return getattr(self.stream, attr)
import sys
import os, csv, getopt
import binascii
import zlib
from struct import pack, unpack, unpack_from
#Exception Handling
class DrmException(Exception):
pass
#
# crypto digestroutines
#
import hashlib
def MD5(message):
ctx = hashlib.md5()
ctx.update(message)
return ctx.digest()
def SHA1(message):
ctx = hashlib.sha1()
ctx.update(message)
return ctx.digest()
# determine if we are running as a calibre plugin
if 'calibre' in sys.modules:
inCalibre = True
global openKindleInfo, CryptUnprotectData, GetUserName, GetVolumeSerialNumber, charMap1, charMap2, charMap3, charMap4
else:
inCalibre = False
#
# start of Kindle specific routines
#
if not inCalibre:
import mobidedrm
if sys.platform.startswith('win'):
from k4pcutils import openKindleInfo, CryptUnprotectData, GetUserName, GetVolumeSerialNumber, charMap1, charMap2, charMap3, charMap4
if sys.platform.startswith('darwin'):
from k4mutils import openKindleInfo, CryptUnprotectData, GetUserName, GetVolumeSerialNumber, charMap1, charMap2, charMap3, charMap4
global kindleDatabase
# Encode the bytes in data with the characters in map
def encode(data, map):
result = ""
for char in data:
value = ord(char)
Q = (value ^ 0x80) // len(map)
R = value % len(map)
result += map[Q]
result += map[R]
return result
# Hash the bytes in data and then encode the digest with the characters in map
def encodeHash(data,map):
return encode(MD5(data),map)
# Decode the string in data with the characters in map. Returns the decoded bytes
def decode(data,map):
result = ""
for i in range (0,len(data)-1,2):
high = map.find(data[i])
low = map.find(data[i+1])
if (high == -1) or (low == -1) :
break
value = (((high * len(map)) ^ 0x80) & 0xFF) + low
result += pack("B",value)
return result
# Parse the Kindle.info file and return the records as a list of key-values
def parseKindleInfo():
DB = {}
infoReader = openKindleInfo()
infoReader.read(1)
data = infoReader.read()
if sys.platform.startswith('win'):
items = data.split('{')
else :
items = data.split('[')
for item in items:
splito = item.split(':')
DB[splito[0]] =splito[1]
return DB
# Get a record from the Kindle.info file for the key "hashedKey" (already hashed and encoded). Return the decoded and decrypted record
def getKindleInfoValueForHash(hashedKey):
global kindleDatabase
encryptedValue = decode(kindleDatabase[hashedKey],charMap2)
if sys.platform.startswith('win'):
return CryptUnprotectData(encryptedValue,"")
else:
cleartext = CryptUnprotectData(encryptedValue)
return decode(cleartext, charMap1)
# Get a record from the Kindle.info file for the string in "key" (plaintext). Return the decoded and decrypted record
def getKindleInfoValueForKey(key):
return getKindleInfoValueForHash(encodeHash(key,charMap2))
# Find if the original string for a hashed/encoded string is known. If so return the original string othwise return an empty string.
def findNameForHash(hash):
names = ["kindle.account.tokens","kindle.cookie.item","eulaVersionAccepted","login_date","kindle.token.item","login","kindle.key.item","kindle.name.info","kindle.device.info", "MazamaRandomNumber"]
result = ""
for name in names:
if hash == encodeHash(name, charMap2):
result = name
break
return result
# Print all the records from the kindle.info file (option -i)
def printKindleInfo():
for record in kindleDatabase:
name = findNameForHash(record)
if name != "" :
print (name)
print ("--------------------------")
else :
print ("Unknown Record")
print getKindleInfoValueForHash(record)
print "\n"
#
# PID generation routines
#
# Returns two bit at offset from a bit field
def getTwoBitsFromBitField(bitField,offset):
byteNumber = offset // 4
bitPosition = 6 - 2*(offset % 4)
return ord(bitField[byteNumber]) >> bitPosition & 3
# Returns the six bits at offset from a bit field
def getSixBitsFromBitField(bitField,offset):
offset *= 3
value = (getTwoBitsFromBitField(bitField,offset) <<4) + (getTwoBitsFromBitField(bitField,offset+1) << 2) +getTwoBitsFromBitField(bitField,offset+2)
return value
# 8 bits to six bits encoding from hash to generate PID string
def encodePID(hash):
global charMap3
PID = ""
for position in range (0,8):
PID += charMap3[getSixBitsFromBitField(hash,position)]
return PID
# Encryption table used to generate the device PID
def generatePidEncryptionTable() :
table = []
for counter1 in range (0,0x100):
value = counter1
for counter2 in range (0,8):
if (value & 1 == 0) :
value = value >> 1
else :
value = value >> 1
value = value ^ 0xEDB88320
table.append(value)
return table
# Seed value used to generate the device PID
def generatePidSeed(table,dsn) :
value = 0
for counter in range (0,4) :
index = (ord(dsn[counter]) ^ value) &0xFF
value = (value >> 8) ^ table[index]
return value
# Generate the device PID
def generateDevicePID(table,dsn,nbRoll):
seed = generatePidSeed(table,dsn)
pidAscii = ""
pid = [(seed >>24) &0xFF,(seed >> 16) &0xff,(seed >> 8) &0xFF,(seed) & 0xFF,(seed>>24) & 0xFF,(seed >> 16) &0xff,(seed >> 8) &0xFF,(seed) & 0xFF]
index = 0
for counter in range (0,nbRoll):
pid[index] = pid[index] ^ ord(dsn[counter])
index = (index+1) %8
for counter in range (0,8):
index = ((((pid[counter] >>5) & 3) ^ pid[counter]) & 0x1f) + (pid[counter] >> 7)
pidAscii += charMap4[index]
return pidAscii
# convert from 8 digit PID to 10 digit PID with checksum
def checksumPid(s):
letters = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789"
crc = (~binascii.crc32(s,-1))&0xFFFFFFFF
crc = crc ^ (crc >> 16)
res = s
l = len(letters)
for i in (0,1):
b = crc & 0xff
pos = (b // l) ^ (b % l)
res += letters[pos%l]
crc >>= 8
return res
class MobiPeek:
def loadSection(self, section):
before, after = self.sections[section:section+2]
self.f.seek(before)
return self.f.read(after - before)
def __init__(self, filename):
self.f = file(filename, 'rb')
self.header = self.f.read(78)
self.ident = self.header[0x3C:0x3C+8]
if self.ident != 'BOOKMOBI' and self.ident != 'TEXtREAd':
raise DrmException('invalid file format')
self.num_sections, = unpack_from('>H', self.header, 76)
sections = self.f.read(self.num_sections*8)
self.sections = unpack_from('>%dL' % (self.num_sections*2), sections, 0)[::2] + (0xfffffff, )
self.sect0 = self.loadSection(0)
self.f.close()
def getBookTitle(self):
# get book title
toff, tlen = unpack('>II', self.sect0[0x54:0x5c])
tend = toff + tlen
title = self.sect0[toff:tend]
return title
def getexthData(self):
# if exth region exists then grab it
# get length of this header
length, type, codepage, unique_id, version = unpack('>LLLLL', self.sect0[20:40])
exth_flag, = unpack('>L', self.sect0[0x80:0x84])
exth = ''
if exth_flag & 0x40:
exth = self.sect0[16 + length:]
return exth
def isNotEncrypted(self):
lock_type, = unpack('>H', self.sect0[0xC:0xC+2])
if lock_type == 0:
return True
return False
# DiapDealer's stuff: Parse the EXTH header records and parse the Kindleinfo
# file to calculate the book pid.
def getK4Pids(exth, title):
global kindleDatabase
try:
kindleDatabase = parseKindleInfo()
except Exception as message:
print(message)
if kindleDatabase != None :
# Get the Mazama Random number
MazamaRandomNumber = getKindleInfoValueForKey("MazamaRandomNumber")
# Get the HDD serial
encodedSystemVolumeSerialNumber = encodeHash(GetVolumeSerialNumber(),charMap1)
# Get the current user name
encodedUsername = encodeHash(GetUserName(),charMap1)
# concat, hash and encode to calculate the DSN
DSN = encode(SHA1(MazamaRandomNumber+encodedSystemVolumeSerialNumber+encodedUsername),charMap1)
print("\nDSN: " + DSN)
# Compute the device PID (for which I can tell, is used for nothing).
# But hey, stuff being printed out is apparently cool.
table = generatePidEncryptionTable()
devicePID = generateDevicePID(table,DSN,4)
print("Device PID: " + checksumPid(devicePID))
# Compute book PID
exth_records = {}
nitems, = unpack('>I', exth[8:12])
pos = 12
# Parse the exth records, storing data indexed by type
for i in xrange(nitems):
type, size = unpack('>II', exth[pos: pos + 8])
content = exth[pos + 8: pos + size]
exth_records[type] = content
pos += size
# Grab the contents of the type 209 exth record
if exth_records[209] != None:
data = exth_records[209]
else:
raise DrmException("\nNo EXTH record type 209 - Perhaps not a K4 file?")
# Parse the 209 data to find the the exth record with the token data.
# The last character of the 209 data points to the record with the token.
# Always 208 from my experience, but I'll leave the logic in case that changes.
for i in xrange(len(data)):
if ord(data[i]) != 0:
if exth_records[ord(data[i])] != None:
token = exth_records[ord(data[i])]
# Get the kindle account token
kindleAccountToken = getKindleInfoValueForKey("kindle.account.tokens")
print("Account Token: " + kindleAccountToken)
pidHash = SHA1(DSN+kindleAccountToken+exth_records[209]+token)
bookPID = encodePID(pidHash)
bookPID = checksumPid(bookPID)
if exth_records[503] != None:
print "Pid for " + exth_records[503] + ": " + bookPID
else:
print "Pid for " + title + ":" + bookPID
return bookPID
raise DrmException("\nCould not access K4 data - Perhaps K4 is not installed/configured?")
return null
#
# Main
#
def main(argv=sys.argv):
global kindleDatabase
import mobidedrm
print ('K4MobiDeDrm v%(__version__)s '
'provided by the work of many including DiapDealer, SomeUpdates, IHeartCabbages, CMBDTC, Skindle, DarkReverser, ApprenticeAlf, etc .' % globals())
if len(argv)<3:
print "Removes DRM protection from K4PC, K4M, and Mobi ebooks"
print "Usage:"
print " %s <infile> <outfile> [<pidnums>]" % argv[0]
return 1
if len(argv) == 4:
pidnums = argv[3]
if len(argv) == 3:
pidnums = ""
kindleDatabase = None
infile = argv[1]
outfile = argv[2]
try:
# first try with K4PC/K4M
ex = MobiPeek(infile)
if ex.isNotEncrypted():
print "File was Not Encrypted"
return 2
title = ex.getBookTitle()
exth = ex.getexthData()
pid = getK4Pids(exth, title)
unlocked_file = mobidedrm.getUnencryptedBook(infile, pid)
except DrmException:
pass
except mobidedrm.DrmException:
pass
else:
file(outfile, 'wb').write(unlocked_file)
return 0
# now try from the pid list
pids = pidnums.split(',')
for pid in pids:
try:
print 'Trying: "'+ pid + '"'
unlocked_file = mobidedrm.getUnencryptedBook(infile, pid)
except mobidedrm.DrmException:
pass
else:
file(outfile, 'wb').write(unlocked_file)
return 0
# we could not unencrypt book
print "Error: Could Not Unencrypt Book"
return 1
if __name__ == '__main__':
sys.stdout=Unbuffered(sys.stdout)
sys.exit(main())
if not __name__ == "__main__" and inCalibre:
from calibre.customize import FileTypePlugin
class K4DeDRM(FileTypePlugin):
name = 'K4PC, K4Mac, Mobi DeDRM' # Name of the plugin
description = 'Removes DRM from K4PC, K4Mac, and Mobi files. \
Provided by the work of many including DiapDealer, SomeUpdates, IHeartCabbages, CMBDTC, Skindle, DarkReverser, ApprenticeAlf, etc.'
supported_platforms = ['osx', 'windows', 'linux'] # Platforms this plugin will run on
author = 'DiapDealer, SomeUpdates' # The author of this plugin
version = (0, 0, 1) # The version number of this plugin
file_types = set(['prc','mobi','azw']) # The file types that this plugin will be applied to
on_import = True # Run this plugin during the import
priority = 200 # run this plugin before mobidedrm, k4pcdedrm, k4dedrm
def run(self, path_to_ebook):
from calibre.gui2 import is_ok_to_use_qt
from PyQt4.Qt import QMessageBox
global kindleDatabase
global openKindleInfo, CryptUnprotectData, GetUserName, GetVolumeSerialNumber, charMap1, charMap2, charMap3, charMap4
if sys.platform.startswith('win'):
from k4pcutils import openKindleInfo, CryptUnprotectData, GetUserName, GetVolumeSerialNumber, charMap1, charMap2, charMap3, charMap4
if sys.platform.startswith('darwin'):
from k4mutils import openKindleInfo, CryptUnprotectData, GetUserName, GetVolumeSerialNumber, charMap1, charMap2, charMap3, charMap4
import mobidedrm
pidnums = self.site_customization
# first try with book specifc pid from K4PC or K4M
try:
kindleDatabase = None
ex = MobiPeek(path_to_ebook)
if ex.isNotEncrypted():
return path_to_ebook
title = ex.getBookTitle()
exth = ex.getexthData()
pid = getK4Pids(exth, title)
unlocked_file = mobidedrm.getUnencryptedBook(path_to_ebook,pid)
except DrmException:
pass
except mobidedrm.DrmException:
pass
else:
of = self.temporary_file('.mobi')
of.write(unlocked_file)
of.close()
return of.name
# now try from the pid list
pids = pidnums.split(',')
for pid in pids:
try:
unlocked_file = mobidedrm.getUnencryptedBook(path_to_ebook, pid)
except mobidedrm.DrmException:
pass
else:
of = self.temporary_file('.mobi')
of.write(unlocked_file)
of.close()
return of.name
#if you reached here then no luck raise and exception
if is_ok_to_use_qt():
d = QMessageBox(QMessageBox.Warning, "K4MobiDeDRM Plugin", "Error decoding: %s\n" % path_to_ebook)
d.show()
d.raise_()
d.exec_()
raise Exception("K4MobiDeDRM plugin could not decode the file")
return ""
def customization_help(self, gui=False):
return 'Enter each 10 character PID separated by a comma (no spaces).'

View File

@@ -0,0 +1,319 @@
# standlone set of Mac OSX specific routines needed for K4DeDRM
from __future__ import with_statement
import sys
import os
#Exception Handling
class K4MDrmException(Exception):
pass
import signal
import threading
import subprocess
from subprocess import Popen, PIPE, STDOUT
# **heavily** chopped up and modfied version of asyncproc.py
# to make it actually work on Windows as well as Mac/Linux
# For the original see:
# "http://www.lysator.liu.se/~bellman/download/"
# author is "Thomas Bellman <bellman@lysator.liu.se>"
# available under GPL version 3 or Later
# create an asynchronous subprocess whose output can be collected in
# a non-blocking manner
# What a mess! Have to use threads just to get non-blocking io
# in a cross-platform manner
# luckily all thread use is hidden within this class
class Process(object):
def __init__(self, *params, **kwparams):
if len(params) <= 3:
kwparams.setdefault('stdin', subprocess.PIPE)
if len(params) <= 4:
kwparams.setdefault('stdout', subprocess.PIPE)
if len(params) <= 5:
kwparams.setdefault('stderr', subprocess.PIPE)
self.__pending_input = []
self.__collected_outdata = []
self.__collected_errdata = []
self.__exitstatus = None
self.__lock = threading.Lock()
self.__inputsem = threading.Semaphore(0)
self.__quit = False
self.__process = subprocess.Popen(*params, **kwparams)
if self.__process.stdin:
self.__stdin_thread = threading.Thread(
name="stdin-thread",
target=self.__feeder, args=(self.__pending_input,
self.__process.stdin))
self.__stdin_thread.setDaemon(True)
self.__stdin_thread.start()
if self.__process.stdout:
self.__stdout_thread = threading.Thread(
name="stdout-thread",
target=self.__reader, args=(self.__collected_outdata,
self.__process.stdout))
self.__stdout_thread.setDaemon(True)
self.__stdout_thread.start()
if self.__process.stderr:
self.__stderr_thread = threading.Thread(
name="stderr-thread",
target=self.__reader, args=(self.__collected_errdata,
self.__process.stderr))
self.__stderr_thread.setDaemon(True)
self.__stderr_thread.start()
def pid(self):
return self.__process.pid
def kill(self, signal):
self.__process.send_signal(signal)
# check on subprocess (pass in 'nowait') to act like poll
def wait(self, flag):
if flag.lower() == 'nowait':
rc = self.__process.poll()
else:
rc = self.__process.wait()
if rc != None:
if self.__process.stdin:
self.closeinput()
if self.__process.stdout:
self.__stdout_thread.join()
if self.__process.stderr:
self.__stderr_thread.join()
return self.__process.returncode
def terminate(self):
if self.__process.stdin:
self.closeinput()
self.__process.terminate()
# thread gets data from subprocess stdout
def __reader(self, collector, source):
while True:
data = os.read(source.fileno(), 65536)
self.__lock.acquire()
collector.append(data)
self.__lock.release()
if data == "":
source.close()
break
return
# thread feeds data to subprocess stdin
def __feeder(self, pending, drain):
while True:
self.__inputsem.acquire()
self.__lock.acquire()
if not pending and self.__quit:
drain.close()
self.__lock.release()
break
data = pending.pop(0)
self.__lock.release()
drain.write(data)
# non-blocking read of data from subprocess stdout
def read(self):
self.__lock.acquire()
outdata = "".join(self.__collected_outdata)
del self.__collected_outdata[:]
self.__lock.release()
return outdata
# non-blocking read of data from subprocess stderr
def readerr(self):
self.__lock.acquire()
errdata = "".join(self.__collected_errdata)
del self.__collected_errdata[:]
self.__lock.release()
return errdata
# non-blocking write to stdin of subprocess
def write(self, data):
if self.__process.stdin is None:
raise ValueError("Writing to process with stdin not a pipe")
self.__lock.acquire()
self.__pending_input.append(data)
self.__inputsem.release()
self.__lock.release()
# close stdinput of subprocess
def closeinput(self):
self.__lock.acquire()
self.__quit = True
self.__inputsem.release()
self.__lock.release()
# interface to needed routines in openssl's libcrypto
def _load_crypto_libcrypto():
from ctypes import CDLL, byref, POINTER, c_void_p, c_char_p, c_int, c_long, \
Structure, c_ulong, create_string_buffer, addressof, string_at, cast
from ctypes.util import find_library
libcrypto = find_library('crypto')
if libcrypto is None:
raise K4MDrmException('libcrypto not found')
libcrypto = CDLL(libcrypto)
AES_MAXNR = 14
c_char_pp = POINTER(c_char_p)
c_int_p = POINTER(c_int)
class AES_KEY(Structure):
_fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))), ('rounds', c_int)]
AES_KEY_p = POINTER(AES_KEY)
def F(restype, name, argtypes):
func = getattr(libcrypto, name)
func.restype = restype
func.argtypes = argtypes
return func
AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',[c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p,c_int])
AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key',[c_char_p, c_int, AES_KEY_p])
PKCS5_PBKDF2_HMAC_SHA1 = F(c_int, 'PKCS5_PBKDF2_HMAC_SHA1',
[c_char_p, c_ulong, c_char_p, c_ulong, c_ulong, c_ulong, c_char_p])
class LibCrypto(object):
def __init__(self):
self._blocksize = 0
self._keyctx = None
self.iv = 0
def set_decrypt_key(self, userkey, iv):
self._blocksize = len(userkey)
if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) :
raise K4MDrmException('AES improper key used')
return
keyctx = self._keyctx = AES_KEY()
self.iv = iv
rv = AES_set_decrypt_key(userkey, len(userkey) * 8, keyctx)
if rv < 0:
raise K4MDrmException('Failed to initialize AES key')
def decrypt(self, data):
out = create_string_buffer(len(data))
rv = AES_cbc_encrypt(data, out, len(data), self._keyctx, self.iv, 0)
if rv == 0:
raise K4MDrmException('AES decryption failed')
return out.raw
def keyivgen(self, passwd):
salt = '16743'
saltlen = 5
passlen = len(passwd)
iter = 0x3e8
keylen = 80
out = create_string_buffer(keylen)
rv = PKCS5_PBKDF2_HMAC_SHA1(passwd, passlen, salt, saltlen, iter, keylen, out)
return out.raw
return LibCrypto
def _load_crypto():
LibCrypto = None
try:
LibCrypto = _load_crypto_libcrypto()
except (ImportError, K4MDrmException):
pass
return LibCrypto
LibCrypto = _load_crypto()
#
# Utility Routines
#
# uses a sub process to get the Hard Drive Serial Number using ioreg
# returns with the first found serial number in that class
def GetVolumeSerialNumber():
cmdline = '/usr/sbin/ioreg -r -c AppleAHCIDiskDriver'
cmdline = cmdline.encode(sys.getfilesystemencoding())
p = Process(cmdline, shell=True, bufsize=1, stdin=None, stdout=PIPE, stderr=PIPE, close_fds=False)
poll = p.wait('wait')
results = p.read()
reslst = results.split('\n')
sernum = '9999999999'
cnt = len(reslst)
for j in xrange(cnt):
resline = reslst[j]
pp = resline.find('"Serial Number" = "')
if pp >= 0:
sernum = resline[pp+19:]
sernum = sernum[:-1]
sernum = sernum.lstrip()
break
return sernum
# uses unix env to get username instead of using sysctlbyname
def GetUserName():
username = os.getenv('USER')
return username
# Various character maps used to decrypt books. Probably supposed to act as obfuscation
charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M"
charMap2 = "ZB0bYyc1xDdW2wEV3Ff7KkPpL8UuGA4gz-Tme9Nn_tHh5SvXCsIiR6rJjQaqlOoM"
charMap3 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
charMap4 = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789"
def encode(data, map):
result = ""
for char in data:
value = ord(char)
Q = (value ^ 0x80) // len(map)
R = value % len(map)
result += map[Q]
result += map[R]
return result
import hashlib
def SHA256(message):
ctx = hashlib.sha256()
ctx.update(message)
return ctx.digest()
# implements an Pseudo Mac Version of Windows built-in Crypto routine
def CryptUnprotectData(encryptedData):
sp = GetVolumeSerialNumber() + '!@#' + GetUserName()
passwdData = encode(SHA256(sp),charMap1)
crp = LibCrypto()
key_iv = crp.keyivgen(passwdData)
key = key_iv[0:32]
iv = key_iv[32:48]
crp.set_decrypt_key(key,iv)
cleartext = crp.decrypt(encryptedData)
return cleartext
# Locate and open the .kindle-info file
def openKindleInfo():
home = os.getenv('HOME')
cmdline = 'find "' + home + '/Library/Application Support" -name ".kindle-info"'
cmdline = cmdline.encode(sys.getfilesystemencoding())
p1 = Process(cmdline, shell=True, bufsize=1, stdin=None, stdout=PIPE, stderr=PIPE, close_fds=False)
poll = p1.wait('wait')
results = p1.read()
reslst = results.split('\n')
kinfopath = 'NONE'
cnt = len(reslst)
for j in xrange(cnt):
resline = reslst[j]
pp = resline.find('.kindle-info')
if pp >= 0:
kinfopath = resline
break
if not os.path.exists(kinfopath):
raise K4MDrmException('Error: .kindle-info file can not be found')
return open(kinfopath,'r')

View File

@@ -0,0 +1,107 @@
# K4PC Windows specific routines
from __future__ import with_statement
import sys, os
from ctypes import windll, c_char_p, c_wchar_p, c_uint, POINTER, byref, \
create_unicode_buffer, create_string_buffer, CFUNCTYPE, addressof, \
string_at, Structure, c_void_p, cast
import _winreg as winreg
import traceback
MAX_PATH = 255
kernel32 = windll.kernel32
advapi32 = windll.advapi32
crypt32 = windll.crypt32
#
# Various character maps used to decrypt books. Probably supposed to act as obfuscation
#
charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M"
charMap2 = "AaZzB0bYyCc1XxDdW2wEeVv3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_"
charMap3 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
charMap4 = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789"
#
# Exceptions for all the problems that might happen during the script
#
class DrmException(Exception):
pass
class DataBlob(Structure):
_fields_ = [('cbData', c_uint),
('pbData', c_void_p)]
DataBlob_p = POINTER(DataBlob)
def GetSystemDirectory():
GetSystemDirectoryW = kernel32.GetSystemDirectoryW
GetSystemDirectoryW.argtypes = [c_wchar_p, c_uint]
GetSystemDirectoryW.restype = c_uint
def GetSystemDirectory():
buffer = create_unicode_buffer(MAX_PATH + 1)
GetSystemDirectoryW(buffer, len(buffer))
return buffer.value
return GetSystemDirectory
GetSystemDirectory = GetSystemDirectory()
def GetVolumeSerialNumber():
GetVolumeInformationW = kernel32.GetVolumeInformationW
GetVolumeInformationW.argtypes = [c_wchar_p, c_wchar_p, c_uint,
POINTER(c_uint), POINTER(c_uint),
POINTER(c_uint), c_wchar_p, c_uint]
GetVolumeInformationW.restype = c_uint
def GetVolumeSerialNumber(path = GetSystemDirectory().split('\\')[0] + '\\'):
vsn = c_uint(0)
GetVolumeInformationW(path, None, 0, byref(vsn), None, None, None, 0)
return str(vsn.value)
return GetVolumeSerialNumber
GetVolumeSerialNumber = GetVolumeSerialNumber()
def GetUserName():
GetUserNameW = advapi32.GetUserNameW
GetUserNameW.argtypes = [c_wchar_p, POINTER(c_uint)]
GetUserNameW.restype = c_uint
def GetUserName():
buffer = create_unicode_buffer(32)
size = c_uint(len(buffer))
while not GetUserNameW(buffer, byref(size)):
buffer = create_unicode_buffer(len(buffer) * 2)
size.value = len(buffer)
return buffer.value.encode('utf-16-le')[::2]
return GetUserName
GetUserName = GetUserName()
def CryptUnprotectData():
_CryptUnprotectData = crypt32.CryptUnprotectData
_CryptUnprotectData.argtypes = [DataBlob_p, c_wchar_p, DataBlob_p,
c_void_p, c_void_p, c_uint, DataBlob_p]
_CryptUnprotectData.restype = c_uint
def CryptUnprotectData(indata, entropy):
indatab = create_string_buffer(indata)
indata = DataBlob(len(indata), cast(indatab, c_void_p))
entropyb = create_string_buffer(entropy)
entropy = DataBlob(len(entropy), cast(entropyb, c_void_p))
outdata = DataBlob()
if not _CryptUnprotectData(byref(indata), None, byref(entropy),
None, None, 0, byref(outdata)):
raise DrmException("Failed to Unprotect Data")
return string_at(outdata.pbData, outdata.cbData)
return CryptUnprotectData
CryptUnprotectData = CryptUnprotectData()
#
# Locate and open the Kindle.info file.
#
def openKindleInfo():
regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\")
path = winreg.QueryValueEx(regkey, 'Local AppData')[0]
return open(path+'\\Amazon\\Kindle For PC\\{AMAwzsaPaaZAzmZzZQzgZCAkZ3AjA_AY}\\kindle.info','r')

View File

@@ -0,0 +1,325 @@
#!/usr/bin/python
#
# This is a python script. You need a Python interpreter to run it.
# For example, ActiveState Python, which exists for windows.
#
# It can run standalone to convert files, or it can be installed as a
# plugin for Calibre (http://calibre-ebook.com/about) so that
# importing files with DRM 'Just Works'.
#
# To create a Calibre plugin, rename this file so that the filename
# ends in '_plugin.py', put it into a ZIP file and import that Calibre
# using its plugin configuration GUI.
#
# Changelog
# 0.01 - Initial version
# 0.02 - Huffdic compressed books were not properly decrypted
# 0.03 - Wasn't checking MOBI header length
# 0.04 - Wasn't sanity checking size of data record
# 0.05 - It seems that the extra data flags take two bytes not four
# 0.06 - And that low bit does mean something after all :-)
# 0.07 - The extra data flags aren't present in MOBI header < 0xE8 in size
# 0.08 - ...and also not in Mobi header version < 6
# 0.09 - ...but they are there with Mobi header version 6, header size 0xE4!
# 0.10 - Outputs unencrypted files as-is, so that when run as a Calibre
# import filter it works when importing unencrypted files.
# Also now handles encrypted files that don't need a specific PID.
# 0.11 - use autoflushed stdout and proper return values
# 0.12 - Fix for problems with metadata import as Calibre plugin, report errors
# 0.13 - Formatting fixes: retabbed file, removed trailing whitespace
# and extra blank lines, converted CR/LF pairs at ends of each line,
# and other cosmetic fixes.
# 0.14 - Working out when the extra data flags are present has been problematic
# Versions 7 through 9 have tried to tweak the conditions, but have been
# only partially successful. Closer examination of lots of sample
# files reveals that a confusin has arisen because trailing data entries
# are not encrypted, but it turns out that the multibyte entries
# in utf8 file are encrypted. (Although neither kind gets compressed.)
# This knowledge leads to a simplification of the test for the
# trailing data byte flags - version 5 and higher AND header size >= 0xE4.
# 0.15 - Now outputs 'hearbeat', and is also quicker for long files.
# 0.16 - And reverts to 'done' not 'done.' at the end for unswindle compatibility.
# 0.17 - added modifications to support its use as an imported python module
# both inside calibre and also in other places (ie K4DeDRM tools)
# 0.17a - disabled the standalone plugin feature since a plugin can not import
# a plugin
__version__ = '0.17'
import sys
import struct
import binascii
class Unbuffered:
def __init__(self, stream):
self.stream = stream
def write(self, data):
self.stream.write(data)
self.stream.flush()
def __getattr__(self, attr):
return getattr(self.stream, attr)
class DrmException(Exception):
pass
# Implementation of Pukall Cipher 1
def PC1(key, src, decryption=True):
sum1 = 0;
sum2 = 0;
keyXorVal = 0;
if len(key)!=16:
print "Bad key length!"
return None
wkey = []
for i in xrange(8):
wkey.append(ord(key[i*2])<<8 | ord(key[i*2+1]))
dst = ""
for i in xrange(len(src)):
temp1 = 0;
byteXorVal = 0;
for j in xrange(8):
temp1 ^= wkey[j]
sum2 = (sum2+j)*20021 + sum1
sum1 = (temp1*346)&0xFFFF
sum2 = (sum2+sum1)&0xFFFF
temp1 = (temp1*20021+1)&0xFFFF
byteXorVal ^= temp1 ^ sum2
curByte = ord(src[i])
if not decryption:
keyXorVal = curByte * 257;
curByte = ((curByte ^ (byteXorVal >> 8)) ^ byteXorVal) & 0xFF
if decryption:
keyXorVal = curByte * 257;
for j in xrange(8):
wkey[j] ^= keyXorVal;
dst+=chr(curByte)
return dst
def checksumPid(s):
letters = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789"
crc = (~binascii.crc32(s,-1))&0xFFFFFFFF
crc = crc ^ (crc >> 16)
res = s
l = len(letters)
for i in (0,1):
b = crc & 0xff
pos = (b // l) ^ (b % l)
res += letters[pos%l]
crc >>= 8
return res
def getSizeOfTrailingDataEntries(ptr, size, flags):
def getSizeOfTrailingDataEntry(ptr, size):
bitpos, result = 0, 0
if size <= 0:
return result
while True:
v = ord(ptr[size-1])
result |= (v & 0x7F) << bitpos
bitpos += 7
size -= 1
if (v & 0x80) != 0 or (bitpos >= 28) or (size == 0):
return result
num = 0
testflags = flags >> 1
while testflags:
if testflags & 1:
num += getSizeOfTrailingDataEntry(ptr, size - num)
testflags >>= 1
# Multibyte data, if present, is included in the encryption, so
# we do not need to check the low bit.
# if flags & 1:
# num += (ord(ptr[size - num - 1]) & 0x3) + 1
return num
class DrmStripper:
def loadSection(self, section):
if (section + 1 == self.num_sections):
endoff = len(self.data_file)
else:
endoff = self.sections[section + 1][0]
off = self.sections[section][0]
return self.data_file[off:endoff]
def patch(self, off, new):
self.data_file = self.data_file[:off] + new + self.data_file[off+len(new):]
def patchSection(self, section, new, in_off = 0):
if (section + 1 == self.num_sections):
endoff = len(self.data_file)
else:
endoff = self.sections[section + 1][0]
off = self.sections[section][0]
assert off + in_off + len(new) <= endoff
self.patch(off + in_off, new)
def parseDRM(self, data, count, pid):
pid = pid.ljust(16,'\0')
keyvec1 = "\x72\x38\x33\xB0\xB4\xF2\xE3\xCA\xDF\x09\x01\xD6\xE2\xE0\x3F\x96"
temp_key = PC1(keyvec1, pid, False)
temp_key_sum = sum(map(ord,temp_key)) & 0xff
found_key = None
for i in xrange(count):
verification, size, type, cksum, cookie = struct.unpack('>LLLBxxx32s', data[i*0x30:i*0x30+0x30])
cookie = PC1(temp_key, cookie)
ver,flags,finalkey,expiry,expiry2 = struct.unpack('>LL16sLL', cookie)
if verification == ver and cksum == temp_key_sum and (flags & 0x1F) == 1:
found_key = finalkey
break
if not found_key:
# Then try the default encoding that doesn't require a PID
temp_key = keyvec1
temp_key_sum = sum(map(ord,temp_key)) & 0xff
for i in xrange(count):
verification, size, type, cksum, cookie = struct.unpack('>LLLBxxx32s', data[i*0x30:i*0x30+0x30])
cookie = PC1(temp_key, cookie)
ver,flags,finalkey,expiry,expiry2 = struct.unpack('>LL16sLL', cookie)
if verification == ver and cksum == temp_key_sum:
found_key = finalkey
break
return found_key
def __init__(self, data_file, pid):
if checksumPid(pid[0:-2]) != pid:
raise DrmException("invalid PID checksum")
pid = pid[0:-2]
self.data_file = data_file
header = data_file[0:72]
if header[0x3C:0x3C+8] != 'BOOKMOBI':
raise DrmException("invalid file format")
self.num_sections, = struct.unpack('>H', data_file[76:78])
self.sections = []
for i in xrange(self.num_sections):
offset, a1,a2,a3,a4 = struct.unpack('>LBBBB', data_file[78+i*8:78+i*8+8])
flags, val = a1, a2<<16|a3<<8|a4
self.sections.append( (offset, flags, val) )
sect = self.loadSection(0)
records, = struct.unpack('>H', sect[0x8:0x8+2])
mobi_length, = struct.unpack('>L',sect[0x14:0x18])
mobi_version, = struct.unpack('>L',sect[0x68:0x6C])
extra_data_flags = 0
print "MOBI header version = %d, length = %d" %(mobi_version, mobi_length)
if (mobi_length >= 0xE4) and (mobi_version >= 5):
extra_data_flags, = struct.unpack('>H', sect[0xF2:0xF4])
print "Extra Data Flags = %d" %extra_data_flags
crypto_type, = struct.unpack('>H', sect[0xC:0xC+2])
if crypto_type == 0:
print "This book is not encrypted."
else:
if crypto_type == 1:
raise DrmException("cannot decode Mobipocket encryption type 1")
if crypto_type != 2:
raise DrmException("unknown encryption type: %d" % crypto_type)
# calculate the keys
drm_ptr, drm_count, drm_size, drm_flags = struct.unpack('>LLLL', sect[0xA8:0xA8+16])
if drm_count == 0:
raise DrmException("no PIDs found in this file")
found_key = self.parseDRM(sect[drm_ptr:drm_ptr+drm_size], drm_count, pid)
if not found_key:
raise DrmException("no key found. maybe the PID is incorrect")
# kill the drm keys
self.patchSection(0, "\0" * drm_size, drm_ptr)
# kill the drm pointers
self.patchSection(0, "\xff" * 4 + "\0" * 12, 0xA8)
# clear the crypto type
self.patchSection(0, "\0" * 2, 0xC)
# decrypt sections
print "Decrypting. Please wait . . .",
new_data = self.data_file[:self.sections[1][0]]
for i in xrange(1, records+1):
data = self.loadSection(i)
extra_size = getSizeOfTrailingDataEntries(data, len(data), extra_data_flags)
if i%100 == 0:
print ".",
# print "record %d, extra_size %d" %(i,extra_size)
new_data += PC1(found_key, data[0:len(data) - extra_size])
if extra_size > 0:
new_data += data[-extra_size:]
#self.patchSection(i, PC1(found_key, data[0:len(data) - extra_size]))
if self.num_sections > records+1:
new_data += self.data_file[self.sections[records+1][0]:]
self.data_file = new_data
print "done"
def getResult(self):
return self.data_file
def getUnencryptedBook(infile,pid):
sys.stdout=Unbuffered(sys.stdout)
data_file = file(infile, 'rb').read()
strippedFile = DrmStripper(data_file, pid)
return strippedFile.getResult()
def main(argv=sys.argv):
sys.stdout=Unbuffered(sys.stdout)
print ('MobiDeDrm v%(__version__)s. '
'Copyright 2008-2010 The Dark Reverser.' % globals())
if len(argv)<4:
print "Removes protection from Mobipocket books"
print "Usage:"
print " %s <infile> <outfile> <PID>" % sys.argv[0]
return 1
else:
infile = argv[1]
outfile = argv[2]
pid = argv[3]
try:
stripped_file = getUnencryptedBook(infile, pid)
file(outfile, 'wb').write(stripped_file)
except DrmException, e:
print "Error: %s" % e
return 1
return 0
if __name__ == "__main__":
sys.exit(main())
#if not __name__ == "__main__":
if False:
# note a calibre plugin can not import code with another calibre plugin
# in it as it ends up registering two different plugins
from calibre.customize import FileTypePlugin
class MobiDeDRM(FileTypePlugin):
name = 'MobiDeDRM' # Name of the plugin
description = 'Removes DRM from secure Mobi files'
supported_platforms = ['linux', 'osx', 'windows'] # Platforms this plugin will run on
author = 'The Dark Reverser' # The author of this plugin
version = (0, 1, 7) # The version number of this plugin
file_types = set(['prc','mobi','azw']) # The file types that this plugin will be applied to
on_import = True # Run this plugin during the import
def run(self, path_to_ebook):
from calibre.gui2 import is_ok_to_use_qt
from PyQt4.Qt import QMessageBox
PID = self.site_customization
data_file = file(path_to_ebook, 'rb').read()
ar = PID.split(',')
for i in ar:
try:
unlocked_file = DrmStripper(data_file, i).getResult()
except DrmException:
if is_ok_to_use_qt():
d = QMessageBox(QMessageBox.Warning, "MobiDeDRM Plugin", "Error decoding: %s\n" % path_to_ebook)
d.show()
d.raise_()
d.exec_()
raise Exception("MobiDeDRM Plugin: Error decoding ebook")
else:
of = self.temporary_file('.mobi')
of.write(unlocked_file)
of.close()
return of.name
def customization_help(self, gui=False):
return 'Enter PID (separate multiple PIDs with comma)'

View File

@@ -1,140 +0,0 @@
#!/usr/bin/env python
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
import sys
sys.path.append('lib')
import os, os.path, urllib
import subprocess
from subprocess import Popen, PIPE, STDOUT
import subasyncio
from subasyncio import Process
import Tkinter
import Tkconstants
import tkFileDialog
import tkMessageBox
from scrolltextwidget import ScrolledText
class MainDialog(Tkinter.Frame):
def __init__(self, root):
Tkinter.Frame.__init__(self, root, border=5)
self.root = root
self.interval = 2000
self.p2 = None
self.status = Tkinter.Label(self, text='Find your Kindle PID')
self.status.pack(fill=Tkconstants.X, expand=1)
body = Tkinter.Frame(self)
body.pack(fill=Tkconstants.X, expand=1)
sticky = Tkconstants.E + Tkconstants.W
body.grid_columnconfigure(1, weight=2)
Tkinter.Label(body, text='Kindle Serial # or iPhone UDID').grid(row=1, sticky=Tkconstants.E)
self.serialnum = Tkinter.StringVar()
self.serialinfo = Tkinter.Entry(body, width=45, textvariable=self.serialnum)
self.serialinfo.grid(row=1, column=1, sticky=sticky)
msg1 = 'Conversion Log \n\n'
self.stext = ScrolledText(body, bd=5, relief=Tkconstants.RIDGE, height=15, width=60, wrap=Tkconstants.WORD)
self.stext.grid(row=3, column=0, columnspan=2,sticky=sticky)
self.stext.insert(Tkconstants.END,msg1)
buttons = Tkinter.Frame(self)
buttons.pack()
self.sbotton = Tkinter.Button(
buttons, text="Start", width=10, command=self.convertit)
self.sbotton.pack(side=Tkconstants.LEFT)
Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT)
self.qbutton = Tkinter.Button(
buttons, text="Quit", width=10, command=self.quitting)
self.qbutton.pack(side=Tkconstants.RIGHT)
# read from subprocess pipe without blocking
# invoked every interval via the widget "after"
# option being used, so need to reset it for the next time
def processPipe(self):
poll = self.p2.wait('nowait')
if poll != None:
text = self.p2.readerr()
text += self.p2.read()
msg = text + '\n\n' + 'Kindle PID Successfully Determined\n'
if poll != 0:
msg = text + '\n\n' + 'Error: Kindle PID Failed\n'
self.showCmdOutput(msg)
self.p2 = None
self.sbotton.configure(state='normal')
return
text = self.p2.readerr()
text += self.p2.read()
self.showCmdOutput(text)
# make sure we get invoked again by event loop after interval
self.stext.after(self.interval,self.processPipe)
return
# post output from subprocess in scrolled text widget
def showCmdOutput(self, msg):
if msg and msg !='':
msg = msg.encode('utf-8')
self.stext.insert(Tkconstants.END,msg)
self.stext.yview_pickplace(Tkconstants.END)
return
# run as a subprocess via pipes and collect stdout
def pidrdr(self, serial):
# os.putenv('PYTHONUNBUFFERED', '1')
cmdline = 'python ./lib/kindlepid.py "' + serial + '"'
if sys.platform[0:3] == 'win':
search_path = os.environ['PATH']
search_path = search_path.lower()
if search_path.find('python') >= 0:
cmdline = 'python lib\kindlepid.py "' + serial + '"'
else :
cmdline = 'lib\kindlepid.py "' + serial + '"'
cmdline = cmdline.encode(sys.getfilesystemencoding())
p2 = Process(cmdline, shell=True, bufsize=1, stdin=None, stdout=PIPE, stderr=PIPE, close_fds=False)
return p2
def quitting(self):
# kill any still running subprocess
if self.p2 != None:
if (self.p2.wait('nowait') == None):
self.p2.terminate()
self.root.destroy()
# actually ready to run the subprocess and get its output
def convertit(self):
# now disable the button to prevent multiple launches
self.sbotton.configure(state='disabled')
serial = self.serialinfo.get()
if not serial or serial == '':
self.status['text'] = 'No Kindle Serial Number or iPhone UDID specified'
self.sbotton.configure(state='normal')
return
log = 'Command = "python kindlepid.py"\n'
log += 'Serial = "' + serial + '"\n'
log += '\n\n'
log += 'Please Wait ...\n\n'
log = log.encode('utf-8')
self.stext.insert(Tkconstants.END,log)
self.p2 = self.pidrdr(serial)
# python does not seem to allow you to create
# your own eventloop which every other gui does - strange
# so need to use the widget "after" command to force
# event loop to run non-gui events every interval
self.stext.after(self.interval,self.processPipe)
return
def main(argv=None):
root = Tkinter.Tk()
root.title('Kindle and iPhone PID Calculator')
root.resizable(True, False)
root.minsize(300, 0)
MainDialog(root).pack(fill=Tkconstants.X, expand=1)
root.mainloop()
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1,287 @@
#!/usr/bin/env python
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
import sys
sys.path.append('lib')
import os, os.path, urllib
import subprocess
from subprocess import Popen, PIPE, STDOUT
import subasyncio
from subasyncio import Process
import Tkinter
import Tkconstants
import tkFileDialog
import tkMessageBox
from scrolltextwidget import ScrolledText
import binascii
import hashlib
#
# Returns the SHA1 digest of "message"
#
def SHA1(message):
ctx = hashlib.sha1()
ctx.update(message)
return ctx.hexdigest()
class MainDialog(Tkinter.Frame):
def __init__(self, root):
Tkinter.Frame.__init__(self, root, border=5)
self.root = root
self.interval = 2000
self.p2 = None
self.status = Tkinter.Label(self, text='Remove Encryption from Kindle for Mac Mobi eBook')
self.status.pack(fill=Tkconstants.X, expand=1)
body = Tkinter.Frame(self)
body.pack(fill=Tkconstants.X, expand=1)
sticky = Tkconstants.E + Tkconstants.W
body.grid_columnconfigure(1, weight=2)
Tkinter.Label(body, text='Locate your Kindle Applications').grid(row=0, sticky=Tkconstants.E)
self.k4mpath = Tkinter.Entry(body, width=50)
self.k4mpath.grid(row=0, column=1, sticky=sticky)
self.appname = '/Applications/Kindle for Mac.app'
if not os.path.exists(self.appname):
self.appname = '/Applications/Kindle.app'
cwd = self.appname
cwd = cwd.encode('utf-8')
self.k4mpath.insert(0, cwd)
button = Tkinter.Button(body, text="...", command=self.get_k4mpath)
button.grid(row=0, column=2)
Tkinter.Label(body, text='Directory for Unencrypted Output File').grid(row=1, sticky=Tkconstants.E)
self.outpath = Tkinter.Entry(body, width=50)
self.outpath.grid(row=1, column=1, sticky=sticky)
desktoppath = os.getenv('HOME') + '/Desktop/'
desktoppath = desktoppath.encode('utf-8')
self.outpath.insert(0, desktoppath)
button = Tkinter.Button(body, text="...", command=self.get_outpath)
button.grid(row=1, column=2)
msg1 = 'Conversion Log \n\n'
self.stext = ScrolledText(body, bd=5, relief=Tkconstants.RIDGE, height=15, width=60, wrap=Tkconstants.WORD)
self.stext.grid(row=3, column=0, columnspan=2,sticky=sticky)
self.stext.insert(Tkconstants.END,msg1)
buttons = Tkinter.Frame(self)
buttons.pack()
self.sbotton = Tkinter.Button(
buttons, text="Start", width=10, command=self.convertit)
self.sbotton.pack(side=Tkconstants.LEFT)
Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT)
self.qbutton = Tkinter.Button(
buttons, text="Quit", width=10, command=self.quitting)
self.qbutton.pack(side=Tkconstants.RIGHT)
# read from subprocess pipe without blocking
# invoked every interval via the widget "after"
# option being used, so need to reset it for the next time
def processPipe(self):
poll = self.p2.wait('nowait')
if poll != None:
text = self.p2.readerr()
text += self.p2.read()
msg = text + '\n\n' + 'Encryption successfully removed\n'
if poll != 0:
msg = text + '\n\n' + 'Error: Encryption Removal Failed\n'
self.showCmdOutput(msg)
self.p2 = None
self.sbotton.configure(state='normal')
return
text = self.p2.readerr()
text += self.p2.read()
self.showCmdOutput(text)
# make sure we get invoked again by event loop after interval
self.stext.after(self.interval,self.processPipe)
return
# post output from subprocess in scrolled text widget
def showCmdOutput(self, msg):
if msg and msg !='':
msg = msg.encode('utf-8')
self.stext.insert(Tkconstants.END,msg)
self.stext.yview_pickplace(Tkconstants.END)
return
# run as a subprocess via pipes and collect stdout
def mobirdr(self, infile, outfile, pidnum):
cmdline = 'python ./lib/mobidedrm.py "' + infile + '" "' + outfile + '" "' + pidnum + '"'
cmdline = cmdline.encode(sys.getfilesystemencoding())
p2 = Process(cmdline, shell=True, bufsize=1, stdin=None, stdout=PIPE, stderr=PIPE, close_fds=False)
return p2
def get_k4mpath(self):
k4mpath = tkFileDialog.askopenfilename(
parent=None, title='Select Your Kindle Application',
defaultextension='.app', filetypes=[('Kindle for Mac Application', '.app')])
if k4mpath:
k4mpath = os.path.normpath(k4mpath)
self.k4mpath.delete(0, Tkconstants.END)
self.k4mpath.insert(0, k4mpath)
return
def get_outpath(self):
cwd = os.getcwdu()
cwd = cwd.encode('utf-8')
outpath = tkFileDialog.askdirectory(
parent=None, title='Directory to Put Non-DRM eBook into',
initialdir=cwd, initialfile=None)
if outpath:
outpath = os.path.normpath(outpath)
self.outpath.delete(0, Tkconstants.END)
self.outpath.insert(0, outpath)
return
def quitting(self):
# kill any still running subprocess
if self.p2 != None:
if (self.p2.wait('nowait') == None):
self.p2.terminate()
self.root.destroy()
# run as a gdb subprocess via pipes and collect stdout
def gdbrdr(self, k4mappfile, gdbcmds):
cmdline = 'gdb -q -silent -readnow -batch -x ' + gdbcmds + ' "' + k4mappfile + '"'
cmdline = cmdline.encode(sys.getfilesystemencoding())
p3 = Process(cmdline, shell=True, bufsize=1, stdin=None, stdout=PIPE, stderr=PIPE, close_fds=False)
poll = p3.wait('wait')
results = p3.read()
pidnum = 'NOTAPID+'
topazbook = 0
bookpath = 'book not found'
# parse the gdb results to get the last pid and the last azw/prc file name in the gdb listing
reslst = results.split('\n')
cnt = len(reslst)
for j in xrange(cnt):
resline = reslst[j]
pp = resline.find('PID is ')
if pp == 0:
pidnum = resline[7:]
topazbook = 0
if pp > 0:
pidnum = resline[13:]
topazbook = 1
fp = resline.find('File is ')
if fp >= 0:
tp1 = resline.find('.azw')
tp2 = resline.find('.prc')
if tp1 >= 0 or tp2 >= 0:
bookpath = resline[8:]
# put code here to get pid and file name
return pidnum, bookpath, topazbook
# convert from 8 digit PID to proper 10 digit PID
def checksumPid(self, s):
letters = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789"
crc = (~binascii.crc32(s,-1))&0xFFFFFFFF
crc = crc ^ (crc >> 16)
res = s
l = len(letters)
for i in (0,1):
b = crc & 0xff
pos = (b // l) ^ (b % l)
res += letters[pos%l]
crc >>= 8
return res
# start the process
def convertit(self):
# dictionary of all known Kindle for Mac Binaries
sha1_app_digests = {
'e197ed2171ceb44a35c24bd30263b7253331694f' : 'gdb_kindle_cmds_r1.txt',
'4f702436171f84acc13bdf9f94fae91525aecef5' : 'gdb_kindle_cmds_r2.txt',
'no_sha1_digest_key_here_________________' : 'no_gdb_kindle_cmds.txt',
}
# now disable the button to prevent multiple launches
self.sbotton.configure(state='disabled')
k4mpath = self.k4mpath.get()
outpath = self.outpath.get()
# basic error checking
if not k4mpath or not os.path.exists(k4mpath):
self.status['text'] = 'Error: Specified Kindle for Mac Application does not exist'
self.sbotton.configure(state='normal')
return
if not outpath:
self.status['text'] = 'Error: No output directory specified'
self.sbotton.configure(state='normal')
return
if not os.path.isdir(outpath):
self.status['text'] = 'Error specified outputdirectory does not exist'
self.sbotton.configure(state='normal')
return
if not os.path.isfile('/usr/bin/gdb'):
self.status['text'] = 'Error: gdb does not exist, install the XCode Develoepr Tools'
self.sbotton.configure(state='normal')
return
# now check if the K4M app bianry is known and if so which gdbcmds to use
binary_app_file = k4mpath + '/Contents/MacOS/Kindle for Mac'
if not os.path.exists(binary_app_file):
binary_app_file = k4mpath + '/Contents/MacOS/Kindle'
digest = SHA1(file(binary_app_file, 'rb').read())
# print digest
gdbcmds = None
if digest in sha1_app_digests:
gdbcmds = sha1_app_digests[digest]
else :
self.status['text'] = 'Error: Kindle Application does not match any known version, sha1sum is ' + digest
self.sbotton.configure(state='normal')
return
# run Kindle for Mac in gdb to get what we need
(pidnum, bookpath, topazbook) = self.gdbrdr(k4mpath, gdbcmds)
if topazbook == 1:
log = 'Warning: ' + bookpath + ' is a Topaz book\n'
log += '\n\n'
log += 'To convert this book please use the Topaz Tools\n'
log += 'With the 8 digit PID: "' + pidnum + '"\n'
log += '\n\n'
log = log.encode('utf-8')
self.stext.insert(Tkconstants.END,log)
return
pidnum = self.checksumPid(pidnum)
# default output file name to be input file name + '_nodrm.mobi'
initname = os.path.splitext(os.path.basename(bookpath))[0]
initname += '_nodrm.mobi'
outpath += '/' + initname
log = 'Command = "python mobidedrm.py"\n'
log += 'Mobi Path = "'+ bookpath + '"\n'
log += 'Output file = "' + outpath + '"\n'
log += 'PID = "' + pidnum + '"\n'
log += '\n\n'
log += 'Please Wait ...\n\n'
log = log.encode('utf-8')
self.stext.insert(Tkconstants.END,log)
self.p2 = self.mobirdr(bookpath, outpath, pidnum)
# python does not seem to allow you to create
# your own eventloop which every other gui does - strange
# so need to use the widget "after" command to force
# event loop to run non-gui events every interval
self.stext.after(self.interval,self.processPipe)
return
def main(argv=None):
root = Tkinter.Tk()
root.title('Kindle for Mac eBook Encryption Removal')
root.resizable(True, False)
root.minsize(300, 0)
MainDialog(root).pack(fill=Tkconstants.X, expand=1)
root.mainloop()
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1,58 @@
K4MUnswindle
Prerequisites:
- Kindle for Mac.app Version 1.0.0 Beta 1 (27214)
(this is the original version)
or
Kindle.app Version 1.2.0 (30689)
(this is the current version at Amazon)
- XCode Developer Tools **must** be Installed
(see your latest Mac OS X Install Disk for the installer)
The directions for use are:
1. double-click on K4Munswindle.pyw
In the window that opens:
hit the first '...' button to locate your Kindle Application
if it is not in /Applications
hit the second '...' button to select an output directory
(defaults to your Desktop)
hit the 'Start' button
After a short delay, your Kindle application should open up automagically
2. In Kindle for Mac:
- hit the “Home” button to go home.
- double-click on ONE of your books.
This should open the book.
3. Once the book you want is open
- hit the “Home” button and then exit the Kindle for Mac application
4. Once you have exited the Kindle for Mac application you should see one of the following:
- If the book you selected was a Topaz Book:
A Warning message will appear in the Conversion Log indicating
that the book you opened was Topaz, along with the 8 digit PID
needed to convert it using Topaz_Tools
- If the book you selected was a Mobi book:
MobiDeDRM will be automagically started in the Conversion Log
window and if successful you should find your decoded book in
the output directory.

View File

@@ -0,0 +1,13 @@
set verbose 0
break * 0x00b2a56a
commands 1
printf "PID is %s\n", $edx
continue
end
break * 0x014ca2af
commands 2
printf "File is %s\n", $eax
continue
end
condition 2 $eax != 0
run

View File

@@ -0,0 +1,18 @@
set verbose 0
break * 0x00e37be4
commands 1
printf "PID is %s\n", $eax
continue
end
break * 0x0142cd94
commands 2
printf "File is %s\n", $eax
continue
end
condition 2 $eax != 0
break * 0x01009c88
commands 3
printf "TOPAZ PID is %s\n", *(long*)($esp+12)
continue
end
run

View File

@@ -0,0 +1,27 @@
#!/usr/bin/env python
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
import Tkinter
import Tkconstants
# basic scrolled text widget
class ScrolledText(Tkinter.Text):
def __init__(self, master=None, **kw):
self.frame = Tkinter.Frame(master)
self.vbar = Tkinter.Scrollbar(self.frame)
self.vbar.pack(side=Tkconstants.RIGHT, fill=Tkconstants.Y)
kw.update({'yscrollcommand': self.vbar.set})
Tkinter.Text.__init__(self, self.frame, **kw)
self.pack(side=Tkconstants.LEFT, fill=Tkconstants.BOTH, expand=True)
self.vbar['command'] = self.yview
# Copy geometry methods of self.frame without overriding Text
# methods = hack!
text_meths = vars(Tkinter.Text).keys()
methods = vars(Tkinter.Pack).keys() + vars(Tkinter.Grid).keys() + vars(Tkinter.Place).keys()
methods = set(methods).difference(text_meths)
for m in methods:
if m[0] != '_' and m != 'config' and m != 'configure':
setattr(self, m, getattr(self.frame, m))
def __str__(self):
return str(self.frame)

View File

@@ -0,0 +1,149 @@
#!/usr/bin/env python
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
import os, sys
import signal
import threading
import subprocess
from subprocess import Popen, PIPE, STDOUT
# **heavily** chopped up and modfied version of asyncproc.py
# to make it actually work on Windows as well as Mac/Linux
# For the original see:
# "http://www.lysator.liu.se/~bellman/download/"
# author is "Thomas Bellman <bellman@lysator.liu.se>"
# available under GPL version 3 or Later
# create an asynchronous subprocess whose output can be collected in
# a non-blocking manner
# What a mess! Have to use threads just to get non-blocking io
# in a cross-platform manner
# luckily all thread use is hidden within this class
class Process(object):
def __init__(self, *params, **kwparams):
if len(params) <= 3:
kwparams.setdefault('stdin', subprocess.PIPE)
if len(params) <= 4:
kwparams.setdefault('stdout', subprocess.PIPE)
if len(params) <= 5:
kwparams.setdefault('stderr', subprocess.PIPE)
self.__pending_input = []
self.__collected_outdata = []
self.__collected_errdata = []
self.__exitstatus = None
self.__lock = threading.Lock()
self.__inputsem = threading.Semaphore(0)
self.__quit = False
self.__process = subprocess.Popen(*params, **kwparams)
if self.__process.stdin:
self.__stdin_thread = threading.Thread(
name="stdin-thread",
target=self.__feeder, args=(self.__pending_input,
self.__process.stdin))
self.__stdin_thread.setDaemon(True)
self.__stdin_thread.start()
if self.__process.stdout:
self.__stdout_thread = threading.Thread(
name="stdout-thread",
target=self.__reader, args=(self.__collected_outdata,
self.__process.stdout))
self.__stdout_thread.setDaemon(True)
self.__stdout_thread.start()
if self.__process.stderr:
self.__stderr_thread = threading.Thread(
name="stderr-thread",
target=self.__reader, args=(self.__collected_errdata,
self.__process.stderr))
self.__stderr_thread.setDaemon(True)
self.__stderr_thread.start()
def pid(self):
return self.__process.pid
def kill(self, signal):
self.__process.send_signal(signal)
# check on subprocess (pass in 'nowait') to act like poll
def wait(self, flag):
if flag.lower() == 'nowait':
rc = self.__process.poll()
else:
rc = self.__process.wait()
if rc != None:
if self.__process.stdin:
self.closeinput()
if self.__process.stdout:
self.__stdout_thread.join()
if self.__process.stderr:
self.__stderr_thread.join()
return self.__process.returncode
def terminate(self):
if self.__process.stdin:
self.closeinput()
self.__process.terminate()
# thread gets data from subprocess stdout
def __reader(self, collector, source):
while True:
data = os.read(source.fileno(), 65536)
self.__lock.acquire()
collector.append(data)
self.__lock.release()
if data == "":
source.close()
break
return
# thread feeds data to subprocess stdin
def __feeder(self, pending, drain):
while True:
self.__inputsem.acquire()
self.__lock.acquire()
if not pending and self.__quit:
drain.close()
self.__lock.release()
break
data = pending.pop(0)
self.__lock.release()
drain.write(data)
# non-blocking read of data from subprocess stdout
def read(self):
self.__lock.acquire()
outdata = "".join(self.__collected_outdata)
del self.__collected_outdata[:]
self.__lock.release()
return outdata
# non-blocking read of data from subprocess stderr
def readerr(self):
self.__lock.acquire()
errdata = "".join(self.__collected_errdata)
del self.__collected_errdata[:]
self.__lock.release()
return errdata
# non-blocking write to stdin of subprocess
def write(self, data):
if self.__process.stdin is None:
raise ValueError("Writing to process with stdin not a pipe")
self.__lock.acquire()
self.__pending_input.append(data)
self.__inputsem.release()
self.__lock.release()
# close stdinput of subprocess
def closeinput(self):
self.__lock.acquire()
self.__quit = True
self.__inputsem.release()
self.__lock.release()

View File

@@ -0,0 +1,5 @@
README
unswindle can be used to find the book specific PID but it needs to be updated for each version of Kindle4PC that Amazon releases (and therefore is also useful for Linux users who have Wine). This program “patches” the Kindle4PC executable and therefore is very release specific.
Unfortunately unswindle v7 the latest, has not been updated to work with the latest version of Kindle for PC. You will need to find one of the older versions of Kindle4PC and prevent later updates in order to use this tool.

View File

@@ -0,0 +1,310 @@
#!/usr/bin/python
#
# This is a python script. You need a Python interpreter to run it.
# For example, ActiveState Python, which exists for windows.
#
# It can run standalone to convert files, or it can be installed as a
# plugin for Calibre (http://calibre-ebook.com/about) so that
# importing files with DRM 'Just Works'.
#
# To create a Calibre plugin, rename this file so that the filename
# ends in '_plugin.py', put it into a ZIP file and import that Calibre
# using its plugin configuration GUI.
#
# Changelog
# 0.01 - Initial version
# 0.02 - Huffdic compressed books were not properly decrypted
# 0.03 - Wasn't checking MOBI header length
# 0.04 - Wasn't sanity checking size of data record
# 0.05 - It seems that the extra data flags take two bytes not four
# 0.06 - And that low bit does mean something after all :-)
# 0.07 - The extra data flags aren't present in MOBI header < 0xE8 in size
# 0.08 - ...and also not in Mobi header version < 6
# 0.09 - ...but they are there with Mobi header version 6, header size 0xE4!
# 0.10 - Outputs unencrypted files as-is, so that when run as a Calibre
# import filter it works when importing unencrypted files.
# Also now handles encrypted files that don't need a specific PID.
# 0.11 - use autoflushed stdout and proper return values
# 0.12 - Fix for problems with metadata import as Calibre plugin, report errors
# 0.13 - Formatting fixes: retabbed file, removed trailing whitespace
# and extra blank lines, converted CR/LF pairs at ends of each line,
# and other cosmetic fixes.
# 0.14 - Working out when the extra data flags are present has been problematic
# Versions 7 through 9 have tried to tweak the conditions, but have been
# only partially successful. Closer examination of lots of sample
# files reveals that a confusin has arisen because trailing data entries
# are not encrypted, but it turns out that the multibyte entries
# in utf8 file are encrypted. (Although neither kind gets compressed.)
# This knowledge leads to a simplification of the test for the
# trailing data byte flags - version 5 and higher AND header size >= 0xE4.
# 0.15 - Now outputs 'hearbeat', and is also quicker for long files.
# 0.16 - And reverts to 'done' not 'done.' at the end for unswindle compatibility.
__version__ = '0.16'
import sys
import struct
import binascii
class Unbuffered:
def __init__(self, stream):
self.stream = stream
def write(self, data):
self.stream.write(data)
self.stream.flush()
def __getattr__(self, attr):
return getattr(self.stream, attr)
class DrmException(Exception):
pass
# Implementation of Pukall Cipher 1
def PC1(key, src, decryption=True):
sum1 = 0;
sum2 = 0;
keyXorVal = 0;
if len(key)!=16:
print "Bad key length!"
return None
wkey = []
for i in xrange(8):
wkey.append(ord(key[i*2])<<8 | ord(key[i*2+1]))
dst = ""
for i in xrange(len(src)):
temp1 = 0;
byteXorVal = 0;
for j in xrange(8):
temp1 ^= wkey[j]
sum2 = (sum2+j)*20021 + sum1
sum1 = (temp1*346)&0xFFFF
sum2 = (sum2+sum1)&0xFFFF
temp1 = (temp1*20021+1)&0xFFFF
byteXorVal ^= temp1 ^ sum2
curByte = ord(src[i])
if not decryption:
keyXorVal = curByte * 257;
curByte = ((curByte ^ (byteXorVal >> 8)) ^ byteXorVal) & 0xFF
if decryption:
keyXorVal = curByte * 257;
for j in xrange(8):
wkey[j] ^= keyXorVal;
dst+=chr(curByte)
return dst
def checksumPid(s):
letters = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789"
crc = (~binascii.crc32(s,-1))&0xFFFFFFFF
crc = crc ^ (crc >> 16)
res = s
l = len(letters)
for i in (0,1):
b = crc & 0xff
pos = (b // l) ^ (b % l)
res += letters[pos%l]
crc >>= 8
return res
def getSizeOfTrailingDataEntries(ptr, size, flags):
def getSizeOfTrailingDataEntry(ptr, size):
bitpos, result = 0, 0
if size <= 0:
return result
while True:
v = ord(ptr[size-1])
result |= (v & 0x7F) << bitpos
bitpos += 7
size -= 1
if (v & 0x80) != 0 or (bitpos >= 28) or (size == 0):
return result
num = 0
testflags = flags >> 1
while testflags:
if testflags & 1:
num += getSizeOfTrailingDataEntry(ptr, size - num)
testflags >>= 1
# Multibyte data, if present, is included in the encryption, so
# we do not need to check the low bit.
# if flags & 1:
# num += (ord(ptr[size - num - 1]) & 0x3) + 1
return num
class DrmStripper:
def loadSection(self, section):
if (section + 1 == self.num_sections):
endoff = len(self.data_file)
else:
endoff = self.sections[section + 1][0]
off = self.sections[section][0]
return self.data_file[off:endoff]
def patch(self, off, new):
self.data_file = self.data_file[:off] + new + self.data_file[off+len(new):]
def patchSection(self, section, new, in_off = 0):
if (section + 1 == self.num_sections):
endoff = len(self.data_file)
else:
endoff = self.sections[section + 1][0]
off = self.sections[section][0]
assert off + in_off + len(new) <= endoff
self.patch(off + in_off, new)
def parseDRM(self, data, count, pid):
pid = pid.ljust(16,'\0')
keyvec1 = "\x72\x38\x33\xB0\xB4\xF2\xE3\xCA\xDF\x09\x01\xD6\xE2\xE0\x3F\x96"
temp_key = PC1(keyvec1, pid, False)
temp_key_sum = sum(map(ord,temp_key)) & 0xff
found_key = None
for i in xrange(count):
verification, size, type, cksum, cookie = struct.unpack('>LLLBxxx32s', data[i*0x30:i*0x30+0x30])
cookie = PC1(temp_key, cookie)
ver,flags,finalkey,expiry,expiry2 = struct.unpack('>LL16sLL', cookie)
if verification == ver and cksum == temp_key_sum and (flags & 0x1F) == 1:
found_key = finalkey
break
if not found_key:
# Then try the default encoding that doesn't require a PID
temp_key = keyvec1
temp_key_sum = sum(map(ord,temp_key)) & 0xff
for i in xrange(count):
verification, size, type, cksum, cookie = struct.unpack('>LLLBxxx32s', data[i*0x30:i*0x30+0x30])
cookie = PC1(temp_key, cookie)
ver,flags,finalkey,expiry,expiry2 = struct.unpack('>LL16sLL', cookie)
if verification == ver and cksum == temp_key_sum:
found_key = finalkey
break
return found_key
def __init__(self, data_file, pid):
if checksumPid(pid[0:-2]) != pid:
raise DrmException("invalid PID checksum")
pid = pid[0:-2]
self.data_file = data_file
header = data_file[0:72]
if header[0x3C:0x3C+8] != 'BOOKMOBI':
raise DrmException("invalid file format")
self.num_sections, = struct.unpack('>H', data_file[76:78])
self.sections = []
for i in xrange(self.num_sections):
offset, a1,a2,a3,a4 = struct.unpack('>LBBBB', data_file[78+i*8:78+i*8+8])
flags, val = a1, a2<<16|a3<<8|a4
self.sections.append( (offset, flags, val) )
sect = self.loadSection(0)
records, = struct.unpack('>H', sect[0x8:0x8+2])
mobi_length, = struct.unpack('>L',sect[0x14:0x18])
mobi_version, = struct.unpack('>L',sect[0x68:0x6C])
extra_data_flags = 0
print "MOBI header version = %d, length = %d" %(mobi_version, mobi_length)
if (mobi_length >= 0xE4) and (mobi_version >= 5):
extra_data_flags, = struct.unpack('>H', sect[0xF2:0xF4])
print "Extra Data Flags = %d" %extra_data_flags
crypto_type, = struct.unpack('>H', sect[0xC:0xC+2])
if crypto_type == 0:
print "This book is not encrypted."
else:
if crypto_type == 1:
raise DrmException("cannot decode Mobipocket encryption type 1")
if crypto_type != 2:
raise DrmException("unknown encryption type: %d" % crypto_type)
# calculate the keys
drm_ptr, drm_count, drm_size, drm_flags = struct.unpack('>LLLL', sect[0xA8:0xA8+16])
if drm_count == 0:
raise DrmException("no PIDs found in this file")
found_key = self.parseDRM(sect[drm_ptr:drm_ptr+drm_size], drm_count, pid)
if not found_key:
raise DrmException("no key found. maybe the PID is incorrect")
# kill the drm keys
self.patchSection(0, "\0" * drm_size, drm_ptr)
# kill the drm pointers
self.patchSection(0, "\xff" * 4 + "\0" * 12, 0xA8)
# clear the crypto type
self.patchSection(0, "\0" * 2, 0xC)
# decrypt sections
print "Decrypting. Please wait . . .",
new_data = self.data_file[:self.sections[1][0]]
for i in xrange(1, records+1):
data = self.loadSection(i)
extra_size = getSizeOfTrailingDataEntries(data, len(data), extra_data_flags)
if i%100 == 0:
print ".",
# print "record %d, extra_size %d" %(i,extra_size)
new_data += PC1(found_key, data[0:len(data) - extra_size])
if extra_size > 0:
new_data += data[-extra_size:]
#self.patchSection(i, PC1(found_key, data[0:len(data) - extra_size]))
if self.num_sections > records+1:
new_data += self.data_file[self.sections[records+1][0]:]
self.data_file = new_data
print "done"
def getResult(self):
return self.data_file
if not __name__ == "__main__":
from calibre.customize import FileTypePlugin
class MobiDeDRM(FileTypePlugin):
name = 'MobiDeDRM' # Name of the plugin
description = 'Removes DRM from secure Mobi files'
supported_platforms = ['linux', 'osx', 'windows'] # Platforms this plugin will run on
author = 'The Dark Reverser' # The author of this plugin
version = (0, 1, 6) # The version number of this plugin
file_types = set(['prc','mobi','azw']) # The file types that this plugin will be applied to
on_import = True # Run this plugin during the import
def run(self, path_to_ebook):
from calibre.gui2 import is_ok_to_use_qt
from PyQt4.Qt import QMessageBox
PID = self.site_customization
data_file = file(path_to_ebook, 'rb').read()
ar = PID.split(',')
for i in ar:
try:
unlocked_file = DrmStripper(data_file, i).getResult()
except DrmException:
# ignore the error
pass
else:
of = self.temporary_file('.mobi')
of.write(unlocked_file)
of.close()
return of.name
if is_ok_to_use_qt():
d = QMessageBox(QMessageBox.Warning, "MobiDeDRM Plugin", "Couldn't decode: %s\n\nImporting encrypted version." % path_to_ebook)
d.show()
d.raise_()
d.exec_()
return path_to_ebook
def customization_help(self, gui=False):
return 'Enter PID (separate multiple PIDs with comma)'
if __name__ == "__main__":
sys.stdout=Unbuffered(sys.stdout)
print ('MobiDeDrm v%(__version__)s. '
'Copyright 2008-2010 The Dark Reverser.' % globals())
if len(sys.argv)<4:
print "Removes protection from Mobipocket books"
print "Usage:"
print " %s <infile> <outfile> <PID>" % sys.argv[0]
sys.exit(1)
else:
infile = sys.argv[1]
outfile = sys.argv[2]
pid = sys.argv[3]
data_file = file(infile, 'rb').read()
try:
strippedFile = DrmStripper(data_file, pid)
file(outfile, 'wb').write(strippedFile.getResult())
except DrmException, e:
print "Error: %s" % e
sys.exit(1)
sys.exit(0)

View File

@@ -0,0 +1,883 @@
#! /usr/bin/python
# -*- coding: utf-8 -*-
# unswindle.pyw, version 7
# Copyright © 2009-2010 i♥cabbages
# Released under the terms of the GNU General Public Licence, version 3 or
# later. <http://www.gnu.org/licenses/>
# Before running this program, you must first install Python 2.6 from
# <http://www.python.org/download/>. Save this script file as unswindle.pyw.
# Find and save in the same directory a copy of mobidedrm.py. Double-click on
# unswindle.pyw. It will run Kindle For PC. Open the book you want to
# decrypt. Close Kindle For PC. A dialog will open allowing you to select the
# output file. And you're done!
# Revision history:
# 1 - Initial release
# 2 - Fixes to work properly on Windows versions >XP
# 3 - Fix minor bug in path extraction
# 4 - Fix error opening threads; detect Topaz books;
# detect unsupported versions of K4PC
# 5 - Work with new (20091222) version of K4PC
# 6 - Detect and just copy DRM-free books
# 7 - Work with new (20100629) version of K4PC
"""
Decrypt Kindle For PC encrypted Mobipocket books.
"""
from __future__ import with_statement
__license__ = 'GPL v3'
import sys
import os
import re
import tempfile
import shutil
import subprocess
import struct
import hashlib
import ctypes
from ctypes import *
from ctypes.wintypes import *
import binascii
import _winreg as winreg
import Tkinter
import Tkconstants
import tkMessageBox
import tkFileDialog
import traceback
#
# _extrawintypes.py
UBYTE = c_ubyte
ULONG_PTR = POINTER(ULONG)
PULONG = ULONG_PTR
PVOID = LPVOID
LPCTSTR = LPTSTR = c_wchar_p
LPBYTE = c_char_p
SIZE_T = c_uint
SIZE_T_p = POINTER(SIZE_T)
#
# _ntdll.py
NTSTATUS = DWORD
ntdll = windll.ntdll
class PROCESS_BASIC_INFORMATION(Structure):
_fields_ = [('Reserved1', PVOID),
('PebBaseAddress', PVOID),
('Reserved2', PVOID * 2),
('UniqueProcessId', ULONG_PTR),
('Reserved3', PVOID)]
# NTSTATUS WINAPI NtQueryInformationProcess(
# __in HANDLE ProcessHandle,
# __in PROCESSINFOCLASS ProcessInformationClass,
# __out PVOID ProcessInformation,
# __in ULONG ProcessInformationLength,
# __out_opt PULONG ReturnLength
# );
NtQueryInformationProcess = ntdll.NtQueryInformationProcess
NtQueryInformationProcess.argtypes = [HANDLE, DWORD, PVOID, ULONG, PULONG]
NtQueryInformationProcess.restype = NTSTATUS
#
# _kernel32.py
INFINITE = 0xffffffff
CREATE_UNICODE_ENVIRONMENT = 0x00000400
DEBUG_ONLY_THIS_PROCESS = 0x00000002
DEBUG_PROCESS = 0x00000001
THREAD_GET_CONTEXT = 0x0008
THREAD_QUERY_INFORMATION = 0x0040
THREAD_SET_CONTEXT = 0x0010
THREAD_SET_INFORMATION = 0x0020
EXCEPTION_BREAKPOINT = 0x80000003
EXCEPTION_SINGLE_STEP = 0x80000004
EXCEPTION_ACCESS_VIOLATION = 0xC0000005
DBG_CONTINUE = 0x00010002L
DBG_EXCEPTION_NOT_HANDLED = 0x80010001L
EXCEPTION_DEBUG_EVENT = 1
CREATE_THREAD_DEBUG_EVENT = 2
CREATE_PROCESS_DEBUG_EVENT = 3
EXIT_THREAD_DEBUG_EVENT = 4
EXIT_PROCESS_DEBUG_EVENT = 5
LOAD_DLL_DEBUG_EVENT = 6
UNLOAD_DLL_DEBUG_EVENT = 7
OUTPUT_DEBUG_STRING_EVENT = 8
RIP_EVENT = 9
class DataBlob(Structure):
_fields_ = [('cbData', c_uint),
('pbData', c_void_p)]
DataBlob_p = POINTER(DataBlob)
class SECURITY_ATTRIBUTES(Structure):
_fields_ = [('nLength', DWORD),
('lpSecurityDescriptor', LPVOID),
('bInheritHandle', BOOL)]
LPSECURITY_ATTRIBUTES = POINTER(SECURITY_ATTRIBUTES)
class STARTUPINFO(Structure):
_fields_ = [('cb', DWORD),
('lpReserved', LPTSTR),
('lpDesktop', LPTSTR),
('lpTitle', LPTSTR),
('dwX', DWORD),
('dwY', DWORD),
('dwXSize', DWORD),
('dwYSize', DWORD),
('dwXCountChars', DWORD),
('dwYCountChars', DWORD),
('dwFillAttribute', DWORD),
('dwFlags', DWORD),
('wShowWindow', WORD),
('cbReserved2', WORD),
('lpReserved2', LPBYTE),
('hStdInput', HANDLE),
('hStdOutput', HANDLE),
('hStdError', HANDLE)]
LPSTARTUPINFO = POINTER(STARTUPINFO)
class PROCESS_INFORMATION(Structure):
_fields_ = [('hProcess', HANDLE),
('hThread', HANDLE),
('dwProcessId', DWORD),
('dwThreadId', DWORD)]
LPPROCESS_INFORMATION = POINTER(PROCESS_INFORMATION)
EXCEPTION_MAXIMUM_PARAMETERS = 15
class EXCEPTION_RECORD(Structure):
pass
EXCEPTION_RECORD._fields_ = [
('ExceptionCode', DWORD),
('ExceptionFlags', DWORD),
('ExceptionRecord', POINTER(EXCEPTION_RECORD)),
('ExceptionAddress', LPVOID),
('NumberParameters', DWORD),
('ExceptionInformation', ULONG_PTR * EXCEPTION_MAXIMUM_PARAMETERS)]
class EXCEPTION_DEBUG_INFO(Structure):
_fields_ = [('ExceptionRecord', EXCEPTION_RECORD),
('dwFirstChance', DWORD)]
class CREATE_THREAD_DEBUG_INFO(Structure):
_fields_ = [('hThread', HANDLE),
('lpThreadLocalBase', LPVOID),
('lpStartAddress', LPVOID)]
class CREATE_PROCESS_DEBUG_INFO(Structure):
_fields_ = [('hFile', HANDLE),
('hProcess', HANDLE),
('hThread', HANDLE),
('dwDebugInfoFileOffset', DWORD),
('nDebugInfoSize', DWORD),
('lpThreadLocalBase', LPVOID),
('lpStartAddress', LPVOID),
('lpImageName', LPVOID),
('fUnicode', WORD)]
class EXIT_THREAD_DEBUG_INFO(Structure):
_fields_ = [('dwExitCode', DWORD)]
class EXIT_PROCESS_DEBUG_INFO(Structure):
_fields_ = [('dwExitCode', DWORD)]
class LOAD_DLL_DEBUG_INFO(Structure):
_fields_ = [('hFile', HANDLE),
('lpBaseOfDll', LPVOID),
('dwDebugInfoFileOffset', DWORD),
('nDebugInfoSize', DWORD),
('lpImageName', LPVOID),
('fUnicode', WORD)]
class UNLOAD_DLL_DEBUG_INFO(Structure):
_fields_ = [('lpBaseOfDll', LPVOID)]
class OUTPUT_DEBUG_STRING_INFO(Structure):
_fields_ = [('lpDebugStringData', LPSTR),
('fUnicode', WORD),
('nDebugStringLength', WORD)]
class RIP_INFO(Structure):
_fields_ = [('dwError', DWORD),
('dwType', DWORD)]
class _U(Union):
_fields_ = [('Exception', EXCEPTION_DEBUG_INFO),
('CreateThread', CREATE_THREAD_DEBUG_INFO),
('CreateProcessInfo', CREATE_PROCESS_DEBUG_INFO),
('ExitThread', EXIT_THREAD_DEBUG_INFO),
('ExitProcess', EXIT_PROCESS_DEBUG_INFO),
('LoadDll', LOAD_DLL_DEBUG_INFO),
('UnloadDll', UNLOAD_DLL_DEBUG_INFO),
('DebugString', OUTPUT_DEBUG_STRING_INFO),
('RipInfo', RIP_INFO)]
class DEBUG_EVENT(Structure):
_anonymous_ = ('u',)
_fields_ = [('dwDebugEventCode', DWORD),
('dwProcessId', DWORD),
('dwThreadId', DWORD),
('u', _U)]
LPDEBUG_EVENT = POINTER(DEBUG_EVENT)
CONTEXT_X86 = 0x00010000
CONTEXT_i386 = CONTEXT_X86
CONTEXT_i486 = CONTEXT_X86
CONTEXT_CONTROL = (CONTEXT_i386 | 0x0001) # SS:SP, CS:IP, FLAGS, BP
CONTEXT_INTEGER = (CONTEXT_i386 | 0x0002) # AX, BX, CX, DX, SI, DI
CONTEXT_SEGMENTS = (CONTEXT_i386 | 0x0004) # DS, ES, FS, GS
CONTEXT_FLOATING_POINT = (CONTEXT_i386 | 0x0008L) # 387 state
CONTEXT_DEBUG_REGISTERS = (CONTEXT_i386 | 0x0010L) # DB 0-3,6,7
CONTEXT_EXTENDED_REGISTERS = (CONTEXT_i386 | 0x0020L)
CONTEXT_FULL = (CONTEXT_CONTROL | CONTEXT_INTEGER | CONTEXT_SEGMENTS)
CONTEXT_ALL = (CONTEXT_CONTROL | CONTEXT_INTEGER | CONTEXT_SEGMENTS |
CONTEXT_FLOATING_POINT | CONTEXT_DEBUG_REGISTERS |
CONTEXT_EXTENDED_REGISTERS)
SIZE_OF_80387_REGISTERS = 80
class FLOATING_SAVE_AREA(Structure):
_fields_ = [('ControlWord', DWORD),
('StatusWord', DWORD),
('TagWord', DWORD),
('ErrorOffset', DWORD),
('ErrorSelector', DWORD),
('DataOffset', DWORD),
('DataSelector', DWORD),
('RegisterArea', BYTE * SIZE_OF_80387_REGISTERS),
('Cr0NpxState', DWORD)]
MAXIMUM_SUPPORTED_EXTENSION = 512
class CONTEXT(Structure):
_fields_ = [('ContextFlags', DWORD),
('Dr0', DWORD),
('Dr1', DWORD),
('Dr2', DWORD),
('Dr3', DWORD),
('Dr6', DWORD),
('Dr7', DWORD),
('FloatSave', FLOATING_SAVE_AREA),
('SegGs', DWORD),
('SegFs', DWORD),
('SegEs', DWORD),
('SegDs', DWORD),
('Edi', DWORD),
('Esi', DWORD),
('Ebx', DWORD),
('Edx', DWORD),
('Ecx', DWORD),
('Eax', DWORD),
('Ebp', DWORD),
('Eip', DWORD),
('SegCs', DWORD),
('EFlags', DWORD),
('Esp', DWORD),
('SegSs', DWORD),
('ExtendedRegisters', BYTE * MAXIMUM_SUPPORTED_EXTENSION)]
LPCONTEXT = POINTER(CONTEXT)
class LDT_ENTRY(Structure):
_fields_ = [('LimitLow', WORD),
('BaseLow', WORD),
('BaseMid', UBYTE),
('Flags1', UBYTE),
('Flags2', UBYTE),
('BaseHi', UBYTE)]
LPLDT_ENTRY = POINTER(LDT_ENTRY)
kernel32 = windll.kernel32
# BOOL WINAPI CloseHandle(
# __in HANDLE hObject
# );
CloseHandle = kernel32.CloseHandle
CloseHandle.argtypes = [HANDLE]
CloseHandle.restype = BOOL
# BOOL WINAPI CreateProcess(
# __in_opt LPCTSTR lpApplicationName,
# __inout_opt LPTSTR lpCommandLine,
# __in_opt LPSECURITY_ATTRIBUTES lpProcessAttributes,
# __in_opt LPSECURITY_ATTRIBUTES lpThreadAttributes,
# __in BOOL bInheritHandles,
# __in DWORD dwCreationFlags,
# __in_opt LPVOID lpEnvironment,
# __in_opt LPCTSTR lpCurrentDirectory,
# __in LPSTARTUPINFO lpStartupInfo,
# __out LPPROCESS_INFORMATION lpProcessInformation
# );
CreateProcess = kernel32.CreateProcessW
CreateProcess.argtypes = [LPCTSTR, LPTSTR, LPSECURITY_ATTRIBUTES,
LPSECURITY_ATTRIBUTES, BOOL, DWORD, LPVOID, LPCTSTR,
LPSTARTUPINFO, LPPROCESS_INFORMATION]
CreateProcess.restype = BOOL
# HANDLE WINAPI OpenThread(
# __in DWORD dwDesiredAccess,
# __in BOOL bInheritHandle,
# __in DWORD dwThreadId
# );
OpenThread = kernel32.OpenThread
OpenThread.argtypes = [DWORD, BOOL, DWORD]
OpenThread.restype = HANDLE
# BOOL WINAPI ContinueDebugEvent(
# __in DWORD dwProcessId,
# __in DWORD dwThreadId,
# __in DWORD dwContinueStatus
# );
ContinueDebugEvent = kernel32.ContinueDebugEvent
ContinueDebugEvent.argtypes = [DWORD, DWORD, DWORD]
ContinueDebugEvent.restype = BOOL
# BOOL WINAPI DebugActiveProcess(
# __in DWORD dwProcessId
# );
DebugActiveProcess = kernel32.DebugActiveProcess
DebugActiveProcess.argtypes = [DWORD]
DebugActiveProcess.restype = BOOL
# BOOL WINAPI GetThreadContext(
# __in HANDLE hThread,
# __inout LPCONTEXT lpContext
# );
GetThreadContext = kernel32.GetThreadContext
GetThreadContext.argtypes = [HANDLE, LPCONTEXT]
GetThreadContext.restype = BOOL
# BOOL WINAPI GetThreadSelectorEntry(
# __in HANDLE hThread,
# __in DWORD dwSelector,
# __out LPLDT_ENTRY lpSelectorEntry
# );
GetThreadSelectorEntry = kernel32.GetThreadSelectorEntry
GetThreadSelectorEntry.argtypes = [HANDLE, DWORD, LPLDT_ENTRY]
GetThreadSelectorEntry.restype = BOOL
# BOOL WINAPI ReadProcessMemory(
# __in HANDLE hProcess,
# __in LPCVOID lpBaseAddress,
# __out LPVOID lpBuffer,
# __in SIZE_T nSize,
# __out SIZE_T *lpNumberOfBytesRead
# );
ReadProcessMemory = kernel32.ReadProcessMemory
ReadProcessMemory.argtypes = [HANDLE, LPCVOID, LPVOID, SIZE_T, SIZE_T_p]
ReadProcessMemory.restype = BOOL
# BOOL WINAPI SetThreadContext(
# __in HANDLE hThread,
# __in const CONTEXT *lpContext
# );
SetThreadContext = kernel32.SetThreadContext
SetThreadContext.argtypes = [HANDLE, LPCONTEXT]
SetThreadContext.restype = BOOL
# BOOL WINAPI WaitForDebugEvent(
# __out LPDEBUG_EVENT lpDebugEvent,
# __in DWORD dwMilliseconds
# );
WaitForDebugEvent = kernel32.WaitForDebugEvent
WaitForDebugEvent.argtypes = [LPDEBUG_EVENT, DWORD]
WaitForDebugEvent.restype = BOOL
# BOOL WINAPI WriteProcessMemory(
# __in HANDLE hProcess,
# __in LPVOID lpBaseAddress,
# __in LPCVOID lpBuffer,
# __in SIZE_T nSize,
# __out SIZE_T *lpNumberOfBytesWritten
# );
WriteProcessMemory = kernel32.WriteProcessMemory
WriteProcessMemory.argtypes = [HANDLE, LPVOID, LPCVOID, SIZE_T, SIZE_T_p]
WriteProcessMemory.restype = BOOL
# BOOL WINAPI FlushInstructionCache(
# __in HANDLE hProcess,
# __in LPCVOID lpBaseAddress,
# __in SIZE_T dwSize
# );
FlushInstructionCache = kernel32.FlushInstructionCache
FlushInstructionCache.argtypes = [HANDLE, LPCVOID, SIZE_T]
FlushInstructionCache.restype = BOOL
#
# debugger.py
FLAG_TRACE_BIT = 0x100
class DebuggerError(Exception):
pass
class Debugger(object):
def __init__(self, process_info):
self.process_info = process_info
self.pid = process_info.dwProcessId
self.tid = process_info.dwThreadId
self.hprocess = process_info.hProcess
self.hthread = process_info.hThread
self._threads = {self.tid: self.hthread}
self._processes = {self.pid: self.hprocess}
self._bps = {}
self._inactive = {}
def read_process_memory(self, addr, size=None, type=str):
if issubclass(type, basestring):
buf = ctypes.create_string_buffer(size)
ref = buf
else:
size = ctypes.sizeof(type)
buf = type()
ref = byref(buf)
copied = SIZE_T(0)
rv = ReadProcessMemory(self.hprocess, addr, ref, size, byref(copied))
if not rv:
addr = getattr(addr, 'value', addr)
raise DebuggerError("could not read memory @ 0x%08x" % (addr,))
if copied.value != size:
raise DebuggerError("insufficient memory read")
if issubclass(type, basestring):
return buf.raw
return buf
def set_bp(self, addr, callback, bytev=None):
hprocess = self.hprocess
if bytev is None:
byte = self.read_process_memory(addr, type=ctypes.c_byte)
bytev = byte.value
else:
byte = ctypes.c_byte(0)
self._bps[addr] = (bytev, callback)
byte.value = 0xcc
copied = SIZE_T(0)
rv = WriteProcessMemory(hprocess, addr, byref(byte), 1, byref(copied))
if not rv:
addr = getattr(addr, 'value', addr)
raise DebuggerError("could not write memory @ 0x%08x" % (addr,))
if copied.value != 1:
raise DebuggerError("insufficient memory written")
rv = FlushInstructionCache(hprocess, None, 0)
if not rv:
raise DebuggerError("could not flush instruction cache")
return
def _restore_bps(self):
for addr, (bytev, callback) in self._inactive.items():
self.set_bp(addr, callback, bytev=bytev)
self._inactive.clear()
def _handle_bp(self, addr):
hprocess = self.hprocess
hthread = self.hthread
bytev, callback = self._inactive[addr] = self._bps.pop(addr)
byte = ctypes.c_byte(bytev)
copied = SIZE_T(0)
rv = WriteProcessMemory(hprocess, addr, byref(byte), 1, byref(copied))
if not rv:
raise DebuggerError("could not write memory")
if copied.value != 1:
raise DebuggerError("insufficient memory written")
rv = FlushInstructionCache(hprocess, None, 0)
if not rv:
raise DebuggerError("could not flush instruction cache")
context = CONTEXT(ContextFlags=CONTEXT_FULL)
rv = GetThreadContext(hthread, byref(context))
if not rv:
raise DebuggerError("could not get thread context")
context.Eip = addr
callback(self, context)
context.EFlags |= FLAG_TRACE_BIT
rv = SetThreadContext(hthread, byref(context))
if not rv:
raise DebuggerError("could not set thread context")
return
def _get_peb_address(self):
hthread = self.hthread
hprocess = self.hprocess
try:
pbi = PROCESS_BASIC_INFORMATION()
rv = NtQueryInformationProcess(hprocess, 0, byref(pbi),
sizeof(pbi), None)
if rv != 0:
raise DebuggerError("could not query process information")
return pbi.PebBaseAddress
except DebuggerError:
pass
try:
context = CONTEXT(ContextFlags=CONTEXT_FULL)
rv = GetThreadContext(hthread, byref(context))
if not rv:
raise DebuggerError("could not get thread context")
entry = LDT_ENTRY()
rv = GetThreadSelectorEntry(hthread, context.SegFs, byref(entry))
if not rv:
raise DebuggerError("could not get selector entry")
low, mid, high = entry.BaseLow, entry.BaseMid, entry.BaseHi
fsbase = low | (mid << 16) | (high << 24)
pebaddr = self.read_process_memory(fsbase + 0x30, type=c_voidp)
return pebaddr.value
except DebuggerError:
pass
return 0x7ffdf000
def get_base_address(self):
addr = self._get_peb_address() + (2 * 4)
baseaddr = self.read_process_memory(addr, type=c_voidp)
return baseaddr.value
def main_loop(self):
event = DEBUG_EVENT()
finished = False
while not finished:
rv = WaitForDebugEvent(byref(event), INFINITE)
if not rv:
raise DebuggerError("could not get debug event")
self.pid = pid = event.dwProcessId
self.tid = tid = event.dwThreadId
self.hprocess = self._processes.get(pid, None)
self.hthread = self._threads.get(tid, None)
status = DBG_CONTINUE
evid = event.dwDebugEventCode
if evid == EXCEPTION_DEBUG_EVENT:
first = event.Exception.dwFirstChance
record = event.Exception.ExceptionRecord
exid = record.ExceptionCode
flags = record.ExceptionFlags
addr = record.ExceptionAddress
if exid == EXCEPTION_BREAKPOINT:
if addr in self._bps:
self._handle_bp(addr)
elif exid == EXCEPTION_SINGLE_STEP:
self._restore_bps()
else:
status = DBG_EXCEPTION_NOT_HANDLED
elif evid == LOAD_DLL_DEBUG_EVENT:
hfile = event.LoadDll.hFile
if hfile is not None:
rv = CloseHandle(hfile)
if not rv:
raise DebuggerError("error closing file handle")
elif evid == CREATE_THREAD_DEBUG_EVENT:
info = event.CreateThread
self.hthread = info.hThread
self._threads[tid] = self.hthread
elif evid == EXIT_THREAD_DEBUG_EVENT:
hthread = self._threads.pop(tid, None)
if hthread is not None:
rv = CloseHandle(hthread)
if not rv:
raise DebuggerError("error closing thread handle")
elif evid == CREATE_PROCESS_DEBUG_EVENT:
info = event.CreateProcessInfo
self.hprocess = info.hProcess
self._processes[pid] = self.hprocess
elif evid == EXIT_PROCESS_DEBUG_EVENT:
hprocess = self._processes.pop(pid, None)
if hprocess is not None:
rv = CloseHandle(hprocess)
if not rv:
raise DebuggerError("error closing process handle")
if pid == self.process_info.dwProcessId:
finished = True
rv = ContinueDebugEvent(pid, tid, status)
if not rv:
raise DebuggerError("could not continue debug")
return True
#
# unswindle.py
KINDLE_REG_KEY = \
r'Software\Classes\Amazon.KindleForPC.content\shell\open\command'
class UnswindleError(Exception):
pass
class PC1KeyGrabber(object):
HOOKS = {
'b9f7e422094b8c8966a0e881e6358116e03e5b7b': {
0x004a719d: '_no_debugger_here',
0x005a795b: '_no_debugger_here',
0x0054f7e0: '_get_pc1_pid',
0x004f9c79: '_get_book_path',
},
'd5124ee20dab10e44b41a039363f6143725a5417': {
0x0041150d: '_i_like_wine',
0x004a681d: '_no_debugger_here',
0x005a438b: '_no_debugger_here',
0x0054c9e0: '_get_pc1_pid',
0x004f8ac9: '_get_book_path',
},
'd791f52dd2ecc68722212d801ad52cb79d1b6fc9': {
0x0041724d: '_i_like_wine',
0x004bfe3d: '_no_debugger_here',
0x005bd9db: '_no_debugger_here',
0x00565920: '_get_pc1_pid',
0x0050fde9: '_get_book_path',
},
}
MOBI_EXTENSIONS = set(['.prc', '.pdb', '.mobi', '.azw', '.az1', '.azw1'])
@classmethod
def supported_version(cls, hexdigest):
return (hexdigest in cls.HOOKS)
def _taddr(self, addr):
return (addr - 0x00400000) + self.baseaddr
def __init__(self, debugger, hexdigest):
self.book_path = None
self.book_pid = None
self.baseaddr = debugger.get_base_address()
hooks = self.HOOKS[hexdigest]
for addr, mname in hooks.items():
debugger.set_bp(self._taddr(addr), getattr(self, mname))
def _i_like_wine(self, debugger, context):
context.Eax = 1
return
def _no_debugger_here(self, debugger, context):
context.Eip += 2
context.Eax = 0
return
def _get_book_path(self, debugger, context):
addr = debugger.read_process_memory(context.Esp, type=ctypes.c_voidp)
try:
path = debugger.read_process_memory(addr, 4096)
except DebuggerError:
pgrest = 0x1000 - (addr.value & 0xfff)
path = debugger.read_process_memory(addr, pgrest)
path = path.decode('utf-16', 'ignore')
if u'\0' in path:
path = path[:path.index(u'\0')]
root, ext = os.path.splitext(path)
if ext.lower() not in self.MOBI_EXTENSIONS:
return
self.book_path = path
def _get_pc1_pid(self, debugger, context):
addr = context.Esp + ctypes.sizeof(ctypes.c_voidp)
addr = debugger.read_process_memory(addr, type=ctypes.c_char_p)
pid = debugger.read_process_memory(addr, 8)
pid = self._checksum_pid(pid)
self.book_pid = pid
def _checksum_pid(self, s):
letters = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789"
crc = (~binascii.crc32(s,-1))&0xFFFFFFFF
crc = crc ^ (crc >> 16)
res = s
l = len(letters)
for i in (0,1):
b = crc & 0xff
pos = (b // l) ^ (b % l)
res += letters[pos%l]
crc >>= 8
return res
class MobiParser(object):
def __init__(self, data):
self.data = data
header = data[0:72]
if header[0x3C:0x3C+8] != 'BOOKMOBI':
raise UnswindleError("invalid file format")
self.nsections = nsections = struct.unpack('>H', data[76:78])[0]
self.sections = sections = []
for i in xrange(nsections):
offset, a1, a2, a3, a4 = \
struct.unpack('>LBBBB', data[78+i*8:78+i*8+8])
flags, val = a1, ((a2 << 16) | (a3 << 8) | a4)
sections.append((offset, flags, val))
sect = self.load_section(0)
self.crypto_type = struct.unpack('>H', sect[0x0c:0x0c+2])[0]
def load_section(self, snum):
if (snum + 1) == self.nsections:
endoff = len(self.data)
else:
endoff = self.sections[snum + 1][0]
off = self.sections[snum][0]
return self.data[off:endoff]
class Unswindler(object):
def __init__(self):
self._exepath = self._get_exe_path()
self._hexdigest = self._get_hexdigest()
self._exedir = os.path.dirname(self._exepath)
self._mobidedrmpath = self._get_mobidedrm_path()
def _get_mobidedrm_path(self):
basedir = sys.modules[self.__module__].__file__
basedir = os.path.dirname(basedir)
for basename in ('mobidedrm', 'mobidedrm.py', 'mobidedrm.pyw'):
path = os.path.join(basedir, basename)
if os.path.isfile(path):
return path
raise UnswindleError("could not locate MobiDeDRM script")
def _get_exe_path(self):
path = None
for root in (winreg.HKEY_CURRENT_USER, winreg.HKEY_LOCAL_MACHINE):
try:
regkey = winreg.OpenKey(root, KINDLE_REG_KEY)
path = winreg.QueryValue(regkey, None)
break
except WindowsError:
pass
else:
raise UnswindleError("Kindle For PC installation not found")
if '"' in path:
path = re.search(r'"(.*?)"', path).group(1)
return path
def _get_hexdigest(self):
path = self._exepath
sha1 = hashlib.sha1()
with open(path, 'rb') as f:
data = f.read(4096)
while data:
sha1.update(data)
data = f.read(4096)
hexdigest = sha1.hexdigest()
if not PC1KeyGrabber.supported_version(hexdigest):
raise UnswindleError("Unsupported version of Kindle For PC")
return hexdigest
def _check_topaz(self, path):
with open(path, 'rb') as f:
magic = f.read(4)
if magic == 'TPZ0':
return True
return False
def _check_drm_free(self, path):
with open(path, 'rb') as f:
crypto = MobiParser(f.read()).crypto_type
return (crypto == 0)
def get_book(self):
creation_flags = (CREATE_UNICODE_ENVIRONMENT |
DEBUG_PROCESS |
DEBUG_ONLY_THIS_PROCESS)
startup_info = STARTUPINFO()
process_info = PROCESS_INFORMATION()
path = pid = None
try:
rv = CreateProcess(self._exepath, None, None, None, False,
creation_flags, None, self._exedir,
byref(startup_info), byref(process_info))
if not rv:
raise UnswindleError("failed to launch Kindle For PC")
debugger = Debugger(process_info)
grabber = PC1KeyGrabber(debugger, self._hexdigest)
debugger.main_loop()
path = grabber.book_path
pid = grabber.book_pid
finally:
if process_info.hThread is not None:
CloseHandle(process_info.hThread)
if process_info.hProcess is not None:
CloseHandle(process_info.hProcess)
if path is None:
raise UnswindleError("failed to determine book path")
if self._check_topaz(path):
raise UnswindleError("cannot decrypt Topaz format book")
return (path, pid)
def decrypt_book(self, inpath, outpath, pid):
if self._check_drm_free(inpath):
shutil.copy(inpath, outpath)
else:
self._mobidedrm(inpath, outpath, pid)
return
def _mobidedrm(self, inpath, outpath, pid):
# darkreverser didn't protect mobidedrm's script execution to allow
# importing, so we have to just run it in a subprocess
if pid is None:
raise UnswindleError("failed to determine book PID")
with tempfile.NamedTemporaryFile(delete=False) as tmpf:
tmppath = tmpf.name
args = [sys.executable, self._mobidedrmpath, inpath, tmppath, pid]
mobidedrm = subprocess.Popen(args, stderr=subprocess.STDOUT,
stdout=subprocess.PIPE,
universal_newlines=True)
output = mobidedrm.communicate()[0]
if not output.endswith("done\n"):
try:
os.remove(tmppath)
except OSError:
pass
raise UnswindleError("problem running MobiDeDRM:\n" + output)
shutil.move(tmppath, outpath)
return
class ExceptionDialog(Tkinter.Frame):
def __init__(self, root, text):
Tkinter.Frame.__init__(self, root, border=5)
label = Tkinter.Label(self, text="Unexpected error:",
anchor=Tkconstants.W, justify=Tkconstants.LEFT)
label.pack(fill=Tkconstants.X, expand=0)
self.text = Tkinter.Text(self)
self.text.pack(fill=Tkconstants.BOTH, expand=1)
self.text.insert(Tkconstants.END, text)
def gui_main(argv=sys.argv):
root = Tkinter.Tk()
root.withdraw()
progname = os.path.basename(argv[0])
try:
unswindler = Unswindler()
inpath, pid = unswindler.get_book()
outpath = tkFileDialog.asksaveasfilename(
parent=None, title='Select unencrypted Mobipocket file to produce',
defaultextension='.mobi', filetypes=[('MOBI files', '.mobi'),
('All files', '.*')])
if not outpath:
return 0
unswindler.decrypt_book(inpath, outpath, pid)
except UnswindleError, e:
tkMessageBox.showerror("Unswindle For PC", "Error: " + str(e))
return 1
except Exception:
root.wm_state('normal')
root.title('Unswindle For PC')
text = traceback.format_exc()
ExceptionDialog(root, text).pack(fill=Tkconstants.BOTH, expand=1)
root.mainloop()
return 1
def cli_main(argv=sys.argv):
progname = os.path.basename(argv[0])
args = argv[1:]
if len(args) != 1:
sys.stderr.write("usage: %s OUTFILE\n" % (progname,))
return 1
outpath = args[0]
unswindler = Unswindler()
inpath, pid = unswindler.get_book()
unswindler.decrypt_book(inpath, outpath, pid)
return 0
if __name__ == '__main__':
sys.exit(gui_main())

View File

@@ -1,168 +0,0 @@
#!/usr/bin/env python
# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
import sys
sys.path.append('lib')
import os, os.path, urllib
import subprocess
from subprocess import Popen, PIPE, STDOUT
import subasyncio
from subasyncio import Process
import Tkinter
import Tkconstants
import tkFileDialog
import tkMessageBox
from scrolltextwidget import ScrolledText
class MainDialog(Tkinter.Frame):
def __init__(self, root):
Tkinter.Frame.__init__(self, root, border=5)
self.root = root
self.interval = 2000
self.p2 = None
self.status = Tkinter.Label(self, text='Fix Encrypted Mobi eBooks so the Kindle can read them')
self.status.pack(fill=Tkconstants.X, expand=1)
body = Tkinter.Frame(self)
body.pack(fill=Tkconstants.X, expand=1)
sticky = Tkconstants.E + Tkconstants.W
body.grid_columnconfigure(1, weight=2)
Tkinter.Label(body, text='Mobi eBook input file').grid(row=0, sticky=Tkconstants.E)
self.mobipath = Tkinter.Entry(body, width=50)
self.mobipath.grid(row=0, column=1, sticky=sticky)
cwd = os.getcwdu()
cwd = cwd.encode('utf-8')
self.mobipath.insert(0, cwd)
button = Tkinter.Button(body, text="...", command=self.get_mobipath)
button.grid(row=0, column=2)
Tkinter.Label(body, text='10 Character PID').grid(row=1, sticky=Tkconstants.E)
self.pidnum = Tkinter.StringVar()
self.pidinfo = Tkinter.Entry(body, width=12, textvariable=self.pidnum)
self.pidinfo.grid(row=1, column=1, sticky=sticky)
msg1 = 'Conversion Log \n\n'
self.stext = ScrolledText(body, bd=5, relief=Tkconstants.RIDGE, height=15, width=60, wrap=Tkconstants.WORD)
self.stext.grid(row=2, column=0, columnspan=2,sticky=sticky)
self.stext.insert(Tkconstants.END,msg1)
buttons = Tkinter.Frame(self)
buttons.pack()
self.sbotton = Tkinter.Button(
buttons, text="Start", width=10, command=self.convertit)
self.sbotton.pack(side=Tkconstants.LEFT)
Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT)
self.qbutton = Tkinter.Button(
buttons, text="Quit", width=10, command=self.quitting)
self.qbutton.pack(side=Tkconstants.RIGHT)
# read from subprocess pipe without blocking
# invoked every interval via the widget "after"
# option being used, so need to reset it for the next time
def processPipe(self):
poll = self.p2.wait('nowait')
if poll != None:
text = self.p2.readerr()
text += self.p2.read()
msg = text + '\n\n' + 'Fix for Kindle successful\n'
if poll != 0:
msg = text + '\n\n' + 'Error: Fix for Kindle Failed\n'
self.showCmdOutput(msg)
self.p2 = None
self.sbotton.configure(state='normal')
return
text = self.p2.readerr()
text += self.p2.read()
self.showCmdOutput(text)
# make sure we get invoked again by event loop after interval
self.stext.after(self.interval,self.processPipe)
return
# post output from subprocess in scrolled text widget
def showCmdOutput(self, msg):
if msg and msg !='':
msg = msg.encode('utf-8')
self.stext.insert(Tkconstants.END,msg)
self.stext.yview_pickplace(Tkconstants.END)
return
# run as a subprocess via pipes and collect stdout
def krdr(self, infile, pidnum):
# os.putenv('PYTHONUNBUFFERED', '1')
cmdline = 'python ./lib/kindlefix.py "' + infile + '" "' + pidnum + '"'
if sys.platform[0:3] == 'win':
search_path = os.environ['PATH']
search_path = search_path.lower()
if search_path.find('python') >= 0:
cmdline = 'python lib\kindlefix.py "' + infile + '" "' + pidnum + '"'
else :
cmdline = 'lib\kindlefix.py "' + infile + '" "' + pidnum + '"'
cmdline = cmdline.encode(sys.getfilesystemencoding())
p2 = Process(cmdline, shell=True, bufsize=1, stdin=None, stdout=PIPE, stderr=PIPE, close_fds=False)
return p2
def get_mobipath(self):
mobipath = tkFileDialog.askopenfilename(
parent=None, title='Select Mobi eBook File',
defaultextension='.prc', filetypes=[('Mobi eBook File', '.prc'), ('Mobi eBook File', '.mobi'),
('All Files', '.*')])
if mobipath:
mobipath = os.path.normpath(mobipath)
self.mobipath.delete(0, Tkconstants.END)
self.mobipath.insert(0, mobipath)
return
def quitting(self):
# kill any still running subprocess
if self.p2 != None:
if (self.p2.wait('nowait') == None):
self.p2.terminate()
self.root.destroy()
# actually ready to run the subprocess and get its output
def convertit(self):
# now disable the button to prevent multiple launches
self.sbotton.configure(state='disabled')
mobipath = self.mobipath.get()
pidnum = self.pidinfo.get()
if not mobipath or not os.path.exists(mobipath):
self.status['text'] = 'Specified Mobi eBook file does not exist'
self.sbotton.configure(state='normal')
return
if not pidnum or pidnum == '':
self.status['text'] = 'No PID specified'
self.sbotton.configure(state='normal')
return
log = 'Command = "python kindlefix.py"\n'
log += 'Mobi Path = "'+ mobipath + '"\n'
log += 'PID = "' + pidnum + '"\n'
log += '\n\n'
log += 'Please Wait ...\n\n'
log = log.encode('utf-8')
self.stext.insert(Tkconstants.END,log)
self.p2 = self.krdr(mobipath, pidnum)
# python does not seem to allow you to create
# your own eventloop which every other gui does - strange
# so need to use the widget "after" command to force
# event loop to run non-gui events every interval
self.stext.after(self.interval,self.processPipe)
return
def main(argv=None):
root = Tkinter.Tk()
root.title('Fix Encrypted Mobi eBooks to work with the Kindle')
root.resizable(True, False)
root.minsize(300, 0)
MainDialog(root).pack(fill=Tkconstants.X, expand=1)
root.mainloop()
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -1,41 +0,0 @@
#!/usr/bin/python
#
# This is a python script. You need a Python interpreter to run it.
# For example, ActiveState Python, which exists for windows.
#
# Changelog
# 1.00 - Initial version
__version__ = '1.00'
import sys
import struct
import binascii
def checksumPid(s):
letters = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789"
crc = (~binascii.crc32(s,-1))&0xFFFFFFFF
crc = crc ^ (crc >> 16)
res = s
l = len(letters)
for i in (0,1):
b = crc & 0xff
pos = (b // l) ^ (b % l)
res += letters[pos%l]
crc >>= 8
return res
if __name__ == "__main__":
if len(sys.argv) != 2:
print "Checks Mobipocket PID checksum"
print "Usage:"
print " %s <PID>" % sys.argv[0]
sys.exit(1)
else:
pid = sys.argv[1]
if len(pid) == 8:
pid = checksumPid(pid)
else:
pid = checksumPid(pid[:8])
print pid
sys.exit(0)

View File

@@ -1,29 +0,0 @@
Kindle for iPhone, iPod Touch, iPad
The Kindle application for iOS (iPhone/iPod Touch/iPad) uses a PID derived from the serial number of the iPhone/iPod Touch/iPad. Kindlepid.py is a python script that turns the serial number into the equivalent PID, which can then be used with the MobiDeDRM script.
So, to remove the DRM from (Mobipocket) Kindle books downloaded to your iPhone/iPodTouch/iPad, youll need the latest toolsvx.x.zip archive and
some way to extract the book files from the backup of your device on your computer. There are several free tools around to do this.
Double-click on KindlePID.pyw to get your devices PID, then use the extractor to get your book files, then double-click on MobiDeDRM.pyw with the PID and the files to get Drm-free versions of your books.
Kindlefix gives you another way to use the PID generated by kindlepid. Some ebook stores and libraries will accept the PIDs generated by kindlepid (some wont), and you can then download ebooks from the store or library encrypted to work with your Kindle. Except they dont. Theres a flag byte set in encrypted Kindle ebooks, and Kindles and the Kindle app wont read encrypted mobipocket ebooks unless that flag is set. Kindlefix will set that flag for you. If your library have Mobipocket ebooks to lend and will accept your Kindles PID, you can now check out library ebooks, run kindlefix on them, and then read them on your Kindle, and when your loan period ends, theyll automatically become unreadable.
To extract the files from your iPod Touch (not archived Kindle ebooks, but ones actually on your iPod Touch) its necessary to first do a back-up of the iPod Touch using iTunes. That creates a backup file on your Mac, and you can then extract the Mobipocket files from that using iPhone/ipod Touch Backup Extractor free software from here: http://supercrazyawesome.com/
Ok, so that will get your the .azw Kindle Mobipocket files.
Now you need the PID used to encrypt them. To get that youll need your iPod Touch UDID number you can find it in iTunes when your iPod Touch is connected in the Summary page click on the serial number and it changes to the (40 digit!) UDID.
And then you need to double-click the KindlePID.pyw script and enter your 40 digit UDID in the window and hit "Start".
and you should get back a response something like:
Mobipocket PID calculator for Amazon Kindle. Copyright (c) 2007, 2009 Igor Skochinsky
iPhone serial number (UDID) detected
Mobipocked PID for iPhone serial# FOURTYDIGITUDIDNUMBERGIVENHERE is TENDIGITPID
which gives you the PID to be used with MobiDeDRM to de-drm the files you extracted.
All of these scripts are gui python programs. Python 2.X (32 bit) is already installed in Mac OSX and Linux. We recommend ActiveState's Active Python Version 2.X (32 bit) for Windows users.

View File

@@ -1,6 +0,0 @@
- MobiDeDRM.pyw is a simple graphical front end to the mobidedrm.py progam that actually does the DRM removal as long as you know the correct PID to use with it (which depends on the versions of Kindle software used). This is a gui program, simply double-click to launch it.
PIDCHeck.py is a command line python tools to take an 8 digit PID and add the 2 checksum digits to the end to generate a 10 digit PID
All of these scripts are python programs. Python 2.X (32 bit) is already installed in Mac OSX and Linux. We recommend ActiveState's Active Python Version 2.X (32 bit) for Windows users.

View File

@@ -1,172 +0,0 @@
class Unbuffered:
def __init__(self, stream):
self.stream = stream
def write(self, data):
self.stream.write(data)
self.stream.flush()
def __getattr__(self, attr):
return getattr(self.stream, attr)
import sys
sys.stdout=Unbuffered(sys.stdout)
import prc, struct
from binascii import hexlify
def strByte(s,off=0):
return struct.unpack(">B",s[off])[0];
def strSWord(s,off=0):
return struct.unpack(">h",s[off:off+2])[0];
def strWord(s,off=0):
return struct.unpack(">H",s[off:off+2])[0];
def strDWord(s,off=0):
return struct.unpack(">L",s[off:off+4])[0];
def strPutDWord(s,off,i):
return s[:off]+struct.pack(">L",i)+s[off+4:];
keyvec1 = "\x72\x38\x33\xB0\xB4\xF2\xE3\xCA\xDF\x09\x01\xD6\xE2\xE0\x3F\x96"
#implementation of Pukall Cipher 1
def PC1(key, src, decryption=True):
sum1 = 0;
sum2 = 0;
keyXorVal = 0;
if len(key)!=16:
print "Bad key length!"
return None
wkey = []
for i in xrange(8):
wkey.append(ord(key[i*2])<<8 | ord(key[i*2+1]))
dst = ""
for i in xrange(len(src)):
temp1 = 0;
byteXorVal = 0;
for j in xrange(8):
temp1 ^= wkey[j]
sum2 = (sum2+j)*20021 + sum1
sum1 = (temp1*346)&0xFFFF
sum2 = (sum2+sum1)&0xFFFF
temp1 = (temp1*20021+1)&0xFFFF
byteXorVal ^= temp1 ^ sum2
curByte = ord(src[i])
if not decryption:
keyXorVal = curByte * 257;
curByte = ((curByte ^ (byteXorVal >> 8)) ^ byteXorVal) & 0xFF
if decryption:
keyXorVal = curByte * 257;
for j in xrange(8):
wkey[j] ^= keyXorVal;
dst+=chr(curByte)
return dst
def find_key(rec0, pid):
off1 = strDWord(rec0, 0xA8)
if off1==0xFFFFFFFF or off1==0:
print "No DRM"
return None
size1 = strDWord(rec0, 0xB0)
cnt = strDWord(rec0, 0xAC)
flag = strDWord(rec0, 0xB4)
temp_key = PC1(keyvec1, pid.ljust(16,'\0'), False)
cksum = 0
#print pid, "->", hexlify(temp_key)
for i in xrange(len(temp_key)):
cksum += ord(temp_key[i])
cksum &= 0xFF
temp_key = temp_key.ljust(16,'\0')
#print "pid cksum: %02X"%cksum
#print "Key records: %02X-%02X, count: %d, flag: %02X"%(off1, off1+size1, cnt, flag)
iOff = off1
drm_key = None
for i in xrange(cnt):
dwCheck = strDWord(rec0, iOff)
dwSize = strDWord(rec0, iOff+4)
dwType = strDWord(rec0, iOff+8)
nCksum = strByte(rec0, iOff+0xC)
#print "Key record %d: check=%08X, size=%d, type=%d, cksum=%02X"%(i, dwCheck, dwSize, dwType, nCksum)
if nCksum==cksum:
drmInfo = PC1(temp_key, rec0[iOff+0x10:iOff+0x30])
dw0, dw4, dw18, dw1c = struct.unpack(">II16xII", drmInfo)
#print "Decrypted drmInfo:", "%08X, %08X, %s, %08X, %08X"%(dw0, dw4, hexlify(drmInfo[0x8:0x18]), dw18, dw1c)
#print "Decrypted drmInfo:", hexlify(drmInfo)
if dw0==dwCheck:
print "Found the matching record; setting the CustomDRM flag for Kindle"
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]
iOff += dwSize
return None
def replaceext(filename, newext):
nameparts = filename.split(".")
if len(nameparts)>1:
return (".".join(nameparts[:-1]))+newext
else:
return nameparts[0]+newext
def main(argv=sys.argv):
print "The Kindleizer v0.2. Copyright (c) 2007 Igor Skochinsky"
if len(sys.argv) != 3:
print "Fixes encrypted Mobipocket books to be readable by Kindle"
print "Usage: kindlefix.py file.mobi PID"
return 1
fname = sys.argv[1]
pid = sys.argv[2]
if len(pid)==10 and pid[-3]=='*':
pid = pid[:-2]
if len(pid)!=8 or pid[-1]!='*':
print "PID is not valid! (should be in format AAAAAAA*DD)"
return 3
db = prc.File(fname)
#print dir(db)
if db.getDBInfo()["creator"]!='MOBI':
print "Not a Mobi file!"
return 1
rec0 = db.getRecord(0)[0]
enc = strSWord(rec0, 0xC)
print "Encryption:", enc
if enc!=2:
print "Unknown encryption type"
return 1
if len(rec0)<0x28 or rec0[0x10:0x14] != 'MOBI':
print "bad file format"
return 1
print "Mobi publication type:", strDWord(rec0, 0x18)
formatVer = strDWord(rec0, 0x24)
print "Mobi format version:", formatVer
last_rec = strWord(rec0, 8)
dwE0 = 0
if formatVer>=4:
new_rec0 = find_key(rec0, pid)
if new_rec0:
db.setRecordIdx(0,new_rec0)
else:
print "PID doesn't match this file"
return 2
else:
print "Wrong Mobi format version"
return 1
outfname = replaceext(fname, ".azw")
if outfname==fname:
outfname = replaceext(fname, "_fixed.azw")
db.save(outfname)
print "Output written to "+outfname
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -1,99 +0,0 @@
#!/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)
# 0.3 changed to autoflush stdout, fixed return code usage
class Unbuffered:
def __init__(self, stream):
self.stream = stream
def write(self, data):
self.stream.write(data)
self.stream.flush()
def __getattr__(self, attr):
return getattr(self.stream, attr)
import sys
sys.stdout=Unbuffered(sys.stdout)
import 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):
return (~binascii.crc32(s,-1))&0xFFFFFFFF
def checksumPid(s):
crc = crc32(s)
crc = crc ^ (crc >> 16)
res = s
l = len(letters)
for i in (0,1):
b = crc & 0xff
pos = (b // l) ^ (b % l)
res += letters[pos%l]
crc >>= 8
return res
def pidFromSerial(s, l):
crc = crc32(s)
arr1 = [0]*l
for i in xrange(len(s)):
arr1[i%l] ^= ord(s[i])
crc_bytes = [crc >> 24 & 0xff, crc >> 16 & 0xff, crc >> 8 & 0xff, crc & 0xff]
for i in xrange(l):
arr1[i] ^= crc_bytes[i&3]
pid = ""
for i in xrange(l):
b = arr1[i] & 0xff
pid+=letters[(b >> 7) + ((b >> 5 & 3) ^ (b & 0x1f))]
return pid
def main(argv=sys.argv):
print "Mobipocket PID calculator for Amazon Kindle. Copyright (c) 2007, 2009 Igor Skochinsky"
if len(sys.argv)==2:
serial = sys.argv[1]
else:
print "Usage: kindlepid.py <Kindle Serial Number>/<iPhone/iPod Touch UDID>"
return 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"
elif serial.startswith("B003"):
print "Kindle 2 Global serial number detected"
elif serial.startswith("B004"):
print "Kindle DX serial number detected"
elif serial.startswith("B005"):
print "Kindle DX International serial number detected"
else:
print "Warning: unrecognized serial number. Please recheck input."
return 1
pid = pidFromSerial(serial,7)+"*"
print "Mobipocked PID for Kindle serial# "+serial+" is "+checksumPid(pid)
return 0
elif len(serial)==40:
print "iPhone serial number (UDID) detected"
pid = pidFromSerial(serial,8)
print "Mobipocked PID for iPhone serial# "+serial+" is "+checksumPid(pid)
return 0
else:
print "Warning: unrecognized serial number. Please recheck input."
return 1
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -1,189 +0,0 @@
# This is a python script. You need a Python interpreter to run it.
# For example, ActiveState Python, which exists for windows.
#
# Big Thanks to Igor SKOCHINSKY for providing me with all his information
# and source code relating to the inner workings of this compression scheme.
# Without it, I wouldn't be able to solve this as easily.
#
# Changelog
# 0.01 - Initial version
# 0.02 - Fix issue with size computing
# 0.03 - Fix issue with some files
# 0.04 - make stdout self flushing and fix return values
class Unbuffered:
def __init__(self, stream):
self.stream = stream
def write(self, data):
self.stream.write(data)
self.stream.flush()
def __getattr__(self, attr):
return getattr(self.stream, attr)
import sys
sys.stdout=Unbuffered(sys.stdout)
import struct
class BitReader:
def __init__(self, data):
self.data, self.pos, self.nbits = data + "\x00\x00\x00\x00", 0, len(data) * 8
def peek(self, n):
r, g = 0, 0
while g < n:
r, g = (r << 8) | ord(self.data[(self.pos+g)>>3]), g + 8 - ((self.pos+g) & 7)
return (r >> (g - n)) & ((1 << n) - 1)
def eat(self, n):
self.pos += n
return self.pos <= self.nbits
def left(self):
return self.nbits - self.pos
class HuffReader:
def __init__(self, huffs):
self.huffs = huffs
h = huffs[0]
if huffs[0][0:4] != 'HUFF' or huffs[0][4:8] != '\x00\x00\x00\x18':
raise ValueError('invalid huff1 header')
if huffs[1][0:4] != 'CDIC' or huffs[1][4:8] != '\x00\x00\x00\x10':
raise ValueError('invalid huff2 header')
self.entry_bits, = struct.unpack('>L', huffs[1][12:16])
off1,off2 = struct.unpack('>LL', huffs[0][16:24])
self.dict1 = struct.unpack('<256L', huffs[0][off1:off1+256*4])
self.dict2 = struct.unpack('<64L', huffs[0][off2:off2+64*4])
self.dicts = huffs[1:]
self.r = ''
def _unpack(self, bits, depth = 0):
if depth > 32:
raise ValueError('corrupt file')
while bits.left():
dw = bits.peek(32)
v = self.dict1[dw >> 24]
codelen = v & 0x1F
assert codelen != 0
code = dw >> (32 - codelen)
r = (v >> 8)
if not (v & 0x80):
while code < self.dict2[(codelen-1)*2]:
codelen += 1
code = dw >> (32 - codelen)
r = self.dict2[(codelen-1)*2+1]
r -= code
assert codelen != 0
if not bits.eat(codelen):
return
dicno = r >> self.entry_bits
off1 = 16 + (r - (dicno << self.entry_bits)) * 2
dic = self.dicts[dicno]
off2 = 16 + ord(dic[off1]) * 256 + ord(dic[off1+1])
blen = ord(dic[off2]) * 256 + ord(dic[off2+1])
slice = dic[off2+2:off2+2+(blen&0x7fff)]
if blen & 0x8000:
self.r += slice
else:
self._unpack(BitReader(slice), depth + 1)
def unpack(self, data):
self.r = ''
self._unpack(BitReader(data))
return self.r
class Sectionizer:
def __init__(self, filename, ident):
self.contents = file(filename, 'rb').read()
self.header = self.contents[0:72]
self.num_sections, = struct.unpack('>H', self.contents[76:78])
if self.header[0x3C:0x3C+8] != ident:
raise ValueError('Invalid file format')
self.sections = []
for i in xrange(self.num_sections):
offset, a1,a2,a3,a4 = struct.unpack('>LBBBB', self.contents[78+i*8:78+i*8+8])
flags, val = a1, a2<<16|a3<<8|a4
self.sections.append( (offset, flags, val) )
def loadSection(self, section):
if section + 1 == self.num_sections:
end_off = len(self.contents)
else:
end_off = self.sections[section + 1][0]
off = self.sections[section][0]
return self.contents[off:end_off]
def getSizeOfTrailingDataEntry(ptr, size):
bitpos, result = 0, 0
while True:
v = ord(ptr[size-1])
result |= (v & 0x7F) << bitpos
bitpos += 7
size -= 1
if (v & 0x80) != 0 or (bitpos >= 28) or (size == 0):
return result
def getSizeOfTrailingDataEntries(ptr, size, flags):
num = 0
flags >>= 1
while flags:
if flags & 1:
num += getSizeOfTrailingDataEntry(ptr, size - num)
flags >>= 1
return num
def unpackBook(input_file):
sect = Sectionizer(input_file, 'BOOKMOBI')
header = sect.loadSection(0)
crypto_type, = struct.unpack('>H', header[0xC:0xC+2])
if crypto_type != 0:
raise ValueError('The book is encrypted. Run mobidedrm first')
if header[0:2] != 'DH':
raise ValueError('invalid compression type')
extra_flags, = struct.unpack('>L', header[0xF0:0xF4])
records, = struct.unpack('>H', header[0x8:0x8+2])
huffoff,huffnum = struct.unpack('>LL', header[0x70:0x78])
huffs = [sect.loadSection(i) for i in xrange(huffoff, huffoff+huffnum)]
huff = HuffReader(huffs)
def decompressSection(nr):
data = sect.loadSection(nr)
trail_size = getSizeOfTrailingDataEntries(data, len(data), extra_flags)
return huff.unpack(data[0:len(data)-trail_size])
r = ''
for i in xrange(1, records+1):
r += decompressSection(i)
return r
def main(argv=sys.argv):
print "MobiHuff v0.03"
print " Copyright (c) 2008 The Dark Reverser <dark.reverser@googlemail.com>"
if len(sys.argv)!=3:
print ""
print "Description:"
print " Unpacks the new mobipocket huffdic compression."
print " This program works with unencrypted files only."
print "Usage:"
print " mobihuff.py infile.mobi outfile.html"
return 1
else:
infile = sys.argv[1]
outfile = sys.argv[2]
try:
print "Decompressing...",
result = unpackBook(infile)
file(outfile, 'wb').write(result)
print "done"
except ValueError, e:
print
print "Error: %s" % e
return 1
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -1,529 +0,0 @@
#
# $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,38 +0,0 @@
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 or Kindle for iPhone.
* kindlepid.py
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, 2009 Igor Skochinsky
Kindle 1 serial number detected
Mobipocked PID for Kindle serial# B001BAB012345678 is V176CXM*FZ
* kindlefix.py
This script adds a "CustomDRM" flag necessary for opening Secure
Mobipocket books on Kindle. The book has to be enabled for Kindle's PID
(generated by kindlepid.py). The "fixed" book is written with
extension ".azw". That file can then be uploaded to Kindle for reading.
Example:
> kindlefix.py MyBook.mobi V176CXM*FZ
The Kindleizer v0.2. Copyright (c) 2007, 2009 Igor Skochinsky
Encryption: 2
Mobi publication type: 2
Mobi format version: 4
Found the matching record; setting the CustomDRM flag for Kindle
Output written to MyBook.azw
* 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)