2012-11-20 14:28:12 +01:00
|
|
|
#!/usr/bin/env python
|
2012-12-19 14:48:11 +01:00
|
|
|
# -*- coding: utf-8 -*-
|
2012-11-20 14:28:12 +01:00
|
|
|
|
|
|
|
from __future__ import with_statement
|
|
|
|
import sys
|
|
|
|
import os, csv
|
|
|
|
import binascii
|
|
|
|
import zlib
|
|
|
|
import re
|
|
|
|
from struct import pack, unpack, unpack_from
|
|
|
|
|
|
|
|
class DrmException(Exception):
|
|
|
|
pass
|
|
|
|
|
|
|
|
global charMap1
|
|
|
|
global charMap3
|
|
|
|
global charMap4
|
|
|
|
|
|
|
|
if 'calibre' in sys.modules:
|
|
|
|
inCalibre = True
|
2012-12-19 14:48:11 +01:00
|
|
|
from calibre.constants import iswindows, isosx
|
|
|
|
if iswindows:
|
2012-11-20 14:28:12 +01:00
|
|
|
from calibre_plugins.k4mobidedrm.k4pcutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString
|
2012-12-19 14:48:11 +01:00
|
|
|
if isosx:
|
2012-11-20 14:28:12 +01:00
|
|
|
from calibre_plugins.k4mobidedrm.k4mutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString
|
|
|
|
else:
|
2012-12-19 14:48:11 +01:00
|
|
|
inCalibre = False
|
|
|
|
iswindows = sys.platform.startswith('win')
|
|
|
|
isosx = sys.platform.startswith('darwin')
|
|
|
|
if iswindows:
|
2012-11-20 14:28:12 +01:00
|
|
|
from k4pcutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString
|
2012-12-19 14:48:11 +01:00
|
|
|
if isosx:
|
2012-11-20 14:28:12 +01:00
|
|
|
from k4mutils import getKindleInfoFiles, getDBfromFile, GetUserName, GetIDString
|
|
|
|
|
|
|
|
|
2012-12-19 14:48:11 +01:00
|
|
|
charMap1 = 'n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M'
|
|
|
|
charMap3 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
|
|
|
|
charMap4 = 'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789'
|
2012-11-20 14:28:12 +01:00
|
|
|
|
|
|
|
# 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()
|
|
|
|
|
|
|
|
|
|
|
|
# Encode the bytes in data with the characters in map
|
|
|
|
def encode(data, map):
|
2012-12-19 14:48:11 +01:00
|
|
|
result = ''
|
2012-11-20 14:28:12 +01:00
|
|
|
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):
|
2012-12-19 14:48:11 +01:00
|
|
|
result = ''
|
2012-11-20 14:28:12 +01:00
|
|
|
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
|
2012-12-19 14:48:11 +01:00
|
|
|
result += pack('B',value)
|
2012-11-20 14:28:12 +01:00
|
|
|
return result
|
|
|
|
|
|
|
|
#
|
|
|
|
# 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
|
2012-12-19 14:48:11 +01:00
|
|
|
PID = ''
|
2012-11-20 14:28:12 +01:00
|
|
|
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):
|
|
|
|
global charMap4
|
|
|
|
seed = generatePidSeed(table,dsn)
|
2012-12-19 14:48:11 +01:00
|
|
|
pidAscii = ''
|
2012-11-20 14:28:12 +01:00
|
|
|
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
|
|
|
|
|
|
|
|
def crc32(s):
|
|
|
|
return (~binascii.crc32(s,-1))&0xFFFFFFFF
|
|
|
|
|
|
|
|
# convert from 8 digit PID to 10 digit PID with checksum
|
|
|
|
def checksumPid(s):
|
|
|
|
global charMap4
|
|
|
|
crc = crc32(s)
|
|
|
|
crc = crc ^ (crc >> 16)
|
|
|
|
res = s
|
|
|
|
l = len(charMap4)
|
|
|
|
for i in (0,1):
|
|
|
|
b = crc & 0xff
|
|
|
|
pos = (b // l) ^ (b % l)
|
|
|
|
res += charMap4[pos%l]
|
|
|
|
crc >>= 8
|
|
|
|
return res
|
|
|
|
|
|
|
|
|
|
|
|
# old kindle serial number to fixed pid
|
|
|
|
def pidFromSerial(s, l):
|
|
|
|
global charMap4
|
|
|
|
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+=charMap4[(b >> 7) + ((b >> 5 & 3) ^ (b & 0x1f))]
|
|
|
|
return pid
|
|
|
|
|
|
|
|
|
|
|
|
# Parse the EXTH header records and use the Kindle serial number to calculate the book pid.
|
2012-12-19 14:48:11 +01:00
|
|
|
def getKindlePids(rec209, token, serialnum):
|
|
|
|
pids=[]
|
|
|
|
|
2012-11-20 14:28:12 +01:00
|
|
|
# Compute book PID
|
|
|
|
pidHash = SHA1(serialnum+rec209+token)
|
|
|
|
bookPID = encodePID(pidHash)
|
|
|
|
bookPID = checksumPid(bookPID)
|
2012-12-19 14:48:11 +01:00
|
|
|
pids.append(bookPID)
|
2012-11-20 14:28:12 +01:00
|
|
|
|
|
|
|
# compute fixed pid for old pre 2.5 firmware update pid as well
|
2012-12-19 14:48:11 +01:00
|
|
|
kindlePID = pidFromSerial(serialnum, 7) + "*"
|
|
|
|
kindlePID = checksumPid(kindlePID)
|
|
|
|
pids.append(kindlePID)
|
2012-11-20 14:28:12 +01:00
|
|
|
|
2012-12-19 14:48:11 +01:00
|
|
|
return pids
|
2012-11-20 14:28:12 +01:00
|
|
|
|
|
|
|
|
|
|
|
# parse the Kindleinfo file to calculate the book pid.
|
|
|
|
|
2012-12-19 14:48:11 +01:00
|
|
|
keynames = ['kindle.account.tokens','kindle.cookie.item','eulaVersionAccepted','login_date','kindle.token.item','login','kindle.key.item','kindle.name.info','kindle.device.info', 'MazamaRandomNumber']
|
2012-11-20 14:28:12 +01:00
|
|
|
|
2012-12-19 14:48:11 +01:00
|
|
|
def getK4Pids(rec209, token, kInfoFile):
|
2012-11-20 14:28:12 +01:00
|
|
|
global charMap1
|
|
|
|
kindleDatabase = None
|
2012-12-19 14:48:11 +01:00
|
|
|
pids = []
|
2012-11-20 14:28:12 +01:00
|
|
|
try:
|
|
|
|
kindleDatabase = getDBfromFile(kInfoFile)
|
|
|
|
except Exception, message:
|
|
|
|
print(message)
|
|
|
|
kindleDatabase = None
|
|
|
|
pass
|
|
|
|
|
|
|
|
if kindleDatabase == None :
|
2012-12-19 14:48:11 +01:00
|
|
|
return pids
|
2012-11-20 14:28:12 +01:00
|
|
|
|
|
|
|
try:
|
|
|
|
# Get the Mazama Random number
|
2012-12-19 14:48:11 +01:00
|
|
|
MazamaRandomNumber = kindleDatabase['MazamaRandomNumber']
|
2012-11-20 14:28:12 +01:00
|
|
|
|
|
|
|
# Get the kindle account token
|
2012-12-19 14:48:11 +01:00
|
|
|
kindleAccountToken = kindleDatabase['kindle.account.tokens']
|
2012-11-20 14:28:12 +01:00
|
|
|
except KeyError:
|
2012-12-19 14:48:11 +01:00
|
|
|
print u"Keys not found in {0}".format(os.path.basename(kInfoFile))
|
|
|
|
return pids
|
2012-11-20 14:28:12 +01:00
|
|
|
|
|
|
|
# Get the ID string used
|
|
|
|
encodedIDString = encodeHash(GetIDString(),charMap1)
|
|
|
|
|
|
|
|
# Get the current user name
|
|
|
|
encodedUsername = encodeHash(GetUserName(),charMap1)
|
|
|
|
|
|
|
|
# concat, hash and encode to calculate the DSN
|
|
|
|
DSN = encode(SHA1(MazamaRandomNumber+encodedIDString+encodedUsername),charMap1)
|
|
|
|
|
|
|
|
# Compute the device PID (for which I can tell, is used for nothing).
|
|
|
|
table = generatePidEncryptionTable()
|
|
|
|
devicePID = generateDevicePID(table,DSN,4)
|
|
|
|
devicePID = checksumPid(devicePID)
|
2012-12-19 14:48:11 +01:00
|
|
|
pids.append(devicePID)
|
2012-11-20 14:28:12 +01:00
|
|
|
|
|
|
|
# Compute book PIDs
|
|
|
|
|
|
|
|
# book pid
|
|
|
|
pidHash = SHA1(DSN+kindleAccountToken+rec209+token)
|
|
|
|
bookPID = encodePID(pidHash)
|
|
|
|
bookPID = checksumPid(bookPID)
|
2012-12-19 14:48:11 +01:00
|
|
|
pids.append(bookPID)
|
2012-11-20 14:28:12 +01:00
|
|
|
|
|
|
|
# variant 1
|
|
|
|
pidHash = SHA1(kindleAccountToken+rec209+token)
|
|
|
|
bookPID = encodePID(pidHash)
|
|
|
|
bookPID = checksumPid(bookPID)
|
2012-12-19 14:48:11 +01:00
|
|
|
pids.append(bookPID)
|
2012-11-20 14:28:12 +01:00
|
|
|
|
|
|
|
# variant 2
|
|
|
|
pidHash = SHA1(DSN+rec209+token)
|
|
|
|
bookPID = encodePID(pidHash)
|
|
|
|
bookPID = checksumPid(bookPID)
|
2012-12-19 14:48:11 +01:00
|
|
|
pids.append(bookPID)
|
2012-11-20 14:28:12 +01:00
|
|
|
|
2012-12-19 14:48:11 +01:00
|
|
|
return pids
|
2012-11-20 14:28:12 +01:00
|
|
|
|
2012-12-19 14:48:11 +01:00
|
|
|
def getPidList(md1, md2, serials=[], kInfoFiles=[]):
|
2012-11-20 14:28:12 +01:00
|
|
|
pidlst = []
|
|
|
|
if kInfoFiles is None:
|
|
|
|
kInfoFiles = []
|
2012-12-19 14:48:11 +01:00
|
|
|
if serials is None:
|
|
|
|
serials = []
|
|
|
|
if iswindows or isosx:
|
2012-11-20 14:28:12 +01:00
|
|
|
kInfoFiles.extend(getKindleInfoFiles())
|
|
|
|
for infoFile in kInfoFiles:
|
|
|
|
try:
|
2012-12-19 14:48:11 +01:00
|
|
|
pidlst.extend(getK4Pids(md1, md2, infoFile))
|
|
|
|
except Exception, e:
|
|
|
|
print u"Error getting PIDs from {0}: {1}".format(os.path.basename(infoFile),e.args[0])
|
2012-11-20 14:28:12 +01:00
|
|
|
for serialnum in serials:
|
|
|
|
try:
|
2012-12-19 14:48:11 +01:00
|
|
|
pidlst.extend(getKindlePids(md1, md2, serialnum))
|
2012-11-20 14:28:12 +01:00
|
|
|
except Exception, message:
|
2012-12-19 14:48:11 +01:00
|
|
|
print u"Error getting PIDs from serial number {0}: {1}".format(serialnum ,e.args[0])
|
2012-11-20 14:28:12 +01:00
|
|
|
return pidlst
|