tools v2.0

Most tools now have plugins
pull/6/head v2.0
Apprentice Alf 14 years ago
parent d427f758f6
commit bf03edd18c

@ -0,0 +1,29 @@
From Apprentice Alf's Blog
Adobe Adept ePub and PDF, .epub, .pdf
The wonderful I♥CABBAGES has produced scripts that will remove the DRM from ePubs and PDFs encryped with Adobes DRM. Installing these scripts is a little more complex that the Mobipocket and eReader decryption tools, as they require installation of the PyCrypto package for Windows Boxes. For Mac OS X and Linux boxes, these scripts use the already installed OpenSSL libcrypto so there is no additional requirements for these platforms.
For more info, see the author's blog:
http://i-u2665-cabbages.blogspot.com/2009_02_01_archive.html
There are two scripts:
The first is called ineptkey_v5.pyw. Simply double-click to launch it and it will create a key file that is needed later to actually remove the DRM. This script need only be run once unless you change your ADE account information.
The second is called in ineptepub_v5.pyw. Simply double-click to launch it. It will ask for your previously generated key file and the path to the book you want to remove the DRM from.
Both of these scripts are gui python programs. Python 2.X (32 bit) is already installed in Mac OSX. We recommend ActiveState's Active Python Version 2.X (32 bit) for Windows users.
The latest version of ineptpdf to use is version 8.4.42, which improves support for some PDF files.
ineptpdf version 8.4.42 can be found here:
http://pastebin.com/kuKMXXsC
It is not included in the tools archive.
If that link is down, please check out the following website for some of the latest releases of these tools:
http://ainept.freewebspace.com/

@ -0,0 +1,19 @@
Readme.txt
Barnes and Noble EPUB ebooks use a form of Social DRM which requires information on your Credit Card Number and the Name on the Credit card used to purchase the book to actually unencrypt the book.
For more info, see the author's blog:
http://i-u2665-cabbages.blogspot.com/2009_12_01_archive.html
The original scripts by IHeartCabbages are available here as well. These scripts have been modified to allow the use of OpenSSL in place of PyCrypto to make them easier to run on Linux and Mac OS X.
There are 2 scripts:
The first is ignoblekeygen_v2.pyw. Double-click to launch it and provide the required information, and this program will generate a key file needed to remove the DRM from the books. This key file need only be generated once unless either you change your credit card number or your name on the credit card (or if you use a different credit card to purchase your book).
The second is ignobleepub_v3.pyw. Double-click it and it will ask for your key file and the path to the book to remove the DRM from.
All of these scripts are gui python programs. Python 2.X (32 bit) is already installed in Mac OSX. We recommend ActiveState's Active Python Version 2.X (32 bit) for Windows users.
These scripts are based on the IHeartCabbages original scripts that allow the replacement of the requirement for PyCrypto with OpenSSL's libcrypto which is already installed on all Mac OS X machines and Linux Boxes. Window's Users will still have to install PyCrypto or OpenSSL to get these scripts to work properly.

@ -1,18 +1,16 @@
#! /usr/bin/python
# ignobleepub.pyw, version 1-rc2
# ignobleepub.pyw, version 3
# To run this program install Python 2.6 from <http://www.python.org/download/>
# and PyCrypto from http://www.voidspace.org.uk/python/modules.shtml#pycrypto
# and OpenSSL or PyCrypto from http://www.voidspace.org.uk/python/modules.shtml#pycrypto
# (make sure to install the version for Python 2.6). Save this script file as
# ignobleepub.pyw and double-click on it to run it.
# Revision history:
# 1 - Initial release
"""
Decrypt Barnes & Noble ADEPT encrypted EPUB books.
"""
# 2 - Added OS X support by using OpenSSL when available
# 3 - screen out improper key lengths to prevent segfaults on Linux
from __future__ import with_statement
@ -30,10 +28,95 @@ import Tkconstants
import tkFileDialog
import tkMessageBox
try:
from Crypto.Cipher import AES
except ImportError:
class IGNOBLEError(Exception):
pass
def _load_crypto_libcrypto():
from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_int, c_long, \
Structure, c_ulong, create_string_buffer, cast
from ctypes.util import find_library
libcrypto = find_library('crypto')
if libcrypto is None:
raise IGNOBLEError('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])
AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',
[c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p,
c_int])
class AES(object):
def __init__(self, userkey):
self._blocksize = len(userkey)
if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) :
raise IGNOBLEError('AES improper key used')
return
key = self._key = AES_KEY()
rv = AES_set_decrypt_key(userkey, len(userkey) * 8, key)
if rv < 0:
raise IGNOBLEError('Failed to initialize AES key')
def decrypt(self, data):
out = create_string_buffer(len(data))
iv = ("\x00" * self._blocksize)
rv = AES_cbc_encrypt(data, out, len(data), self._key, iv, 0)
if rv == 0:
raise IGNOBLEError('AES decryption failed')
return out.raw
return AES
def _load_crypto_pycrypto():
from Crypto.Cipher import AES as _AES
class AES(object):
def __init__(self, key):
self._aes = _AES.new(key, _AES.MODE_CBC)
def decrypt(self, data):
return self._aes.decrypt(data)
return AES
def _load_crypto():
AES = None
for loader in (_load_crypto_libcrypto, _load_crypto_pycrypto):
try:
AES = loader()
break
except (ImportError, IGNOBLEError):
pass
return AES
AES = _load_crypto()
"""
Decrypt Barnes & Noble ADEPT encrypted EPUB books.
"""
META_NAMES = ('mimetype', 'META-INF/rights.xml', 'META-INF/encryption.xml')
NSMAP = {'adept': 'http://ns.adobe.com/adept',
@ -49,7 +132,8 @@ class ZipInfo(zipfile.ZipInfo):
class Decryptor(object):
def __init__(self, bookkey, encryption):
enc = lambda tag: '{%s}%s' % (NSMAP['enc'], tag)
self._aes = AES.new(bookkey, AES.MODE_CBC)
# self._aes = AES.new(bookkey, AES.MODE_CBC)
self._aes = AES(bookkey)
encryption = etree.fromstring(encryption)
self._encrypted = encrypted = set()
expr = './%s/%s/%s' % (enc('EncryptedData'), enc('CipherData'),
@ -75,13 +159,11 @@ class Decryptor(object):
return data
class ADEPTError(Exception):
pass
def cli_main(argv=sys.argv):
progname = os.path.basename(argv[0])
if AES is None:
print "%s: This script requires PyCrypto, which must be installed " \
print "%s: This script requires OpenSSL or PyCrypto, which must be installed " \
"separately. Read the top-of-script comment for details." % \
(progname,)
return 1
@ -92,12 +174,14 @@ def cli_main(argv=sys.argv):
with open(keypath, 'rb') as f:
keyb64 = f.read()
key = keyb64.decode('base64')[:16]
aes = AES.new(key, AES.MODE_CBC)
# aes = AES.new(key, AES.MODE_CBC)
aes = AES(key)
with closing(ZipFile(open(inpath, 'rb'))) as inf:
namelist = set(inf.namelist())
if 'META-INF/rights.xml' not in namelist or \
'META-INF/encryption.xml' not in namelist:
raise ADEPTError('%s: not an B&N ADEPT EPUB' % (inpath,))
raise IGNOBLEError('%s: not an B&N ADEPT EPUB' % (inpath,))
for name in META_NAMES:
namelist.remove(name)
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
@ -219,7 +303,7 @@ def gui_main():
root.withdraw()
tkMessageBox.showerror(
"Ignoble EPUB Decrypter",
"This script requires PyCrypto, which must be installed "
"This script requires OpenSSL or PyCrypto, which must be installed "
"separately. Read the top-of-script comment for details.")
return 1
root.title('Ignoble EPUB Decrypter')

@ -1,14 +1,15 @@
#! /usr/bin/python
# ignoblekeygen.pyw, version 1
# ignoblekeygen.pyw, version 2
# To run this program install Python 2.6 from <http://www.python.org/download/>
# and PyCrypto from http://www.voidspace.org.uk/python/modules.shtml#pycrypto
# and OpenSSL or PyCrypto from http://www.voidspace.org.uk/python/modules.shtml#pycrypto
# (make sure to install the version for Python 2.6). Save this script file as
# ignoblekeygen.pyw and double-click on it to run it.
# Revision history:
# 1 - Initial release
# 2 - Add OS X support by using OpenSSL when available (taken/modified from ineptepub v5)
"""
Generate Barnes & Noble EPUB user key from name and credit card number.
@ -26,10 +27,90 @@ import Tkconstants
import tkFileDialog
import tkMessageBox
try:
from Crypto.Cipher import AES
except ImportError:
# use openssl's libcrypt if it exists in place of pycrypto
# code extracted from the Adobe Adept DRM removal code also by I HeartCabbages
class IGNOBLEError(Exception):
pass
def _load_crypto_libcrypto():
from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_int, c_long, \
Structure, c_ulong, create_string_buffer, cast
from ctypes.util import find_library
libcrypto = find_library('crypto')
if libcrypto is None:
print 'libcrypto not found'
raise IGNOBLEError('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_set_encrypt_key = F(c_int, 'AES_set_encrypt_key',
[c_char_p, c_int, AES_KEY_p])
AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',
[c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p,
c_int])
class AES(object):
def __init__(self, userkey, iv):
self._blocksize = len(userkey)
self._iv = iv
key = self._key = AES_KEY()
rv = AES_set_encrypt_key(userkey, len(userkey) * 8, key)
if rv < 0:
raise IGNOBLEError('Failed to initialize AES Encrypt key')
def encrypt(self, data):
out = create_string_buffer(len(data))
rv = AES_cbc_encrypt(data, out, len(data), self._key, self._iv, 1)
if rv == 0:
raise IGNOBLEError('AES encryption failed')
return out.raw
return AES
def _load_crypto_pycrypto():
from Crypto.Cipher import AES as _AES
class AES(object):
def __init__(self, key, iv):
self._aes = _AES.new(key, _AES.MODE_CBC, iv)
def encrypt(self, data):
return self._aes.encrypt(data)
return AES
def _load_crypto():
AES = None
for loader in (_load_crypto_libcrypto, _load_crypto_pycrypto):
try:
AES = loader()
break
except (ImportError, IGNOBLEError):
pass
return AES
AES = _load_crypto()
def normalize_name(name):
return ''.join(x for x in name.lower() if x != ' ')
@ -40,7 +121,7 @@ def generate_keyfile(name, ccn, outpath):
name_sha = hashlib.sha1(name).digest()[:16]
ccn_sha = hashlib.sha1(ccn).digest()[:16]
both_sha = hashlib.sha1(name + ccn).digest()
aes = AES.new(ccn_sha, AES.MODE_CBC, name_sha)
aes = AES(ccn_sha, name_sha)
crypt = aes.encrypt(both_sha + ('\x0c' * 0x0c))
userkey = hashlib.sha1(crypt).digest()
with open(outpath, 'wb') as f:
@ -50,7 +131,7 @@ def generate_keyfile(name, ccn, outpath):
def cli_main(argv=sys.argv):
progname = os.path.basename(argv[0])
if AES is None:
print "%s: This script requires PyCrypto, which must be installed " \
print "%s: This script requires OpenSSL or PyCrypto, which must be installed " \
"separately. Read the top-of-script comment for details." % \
(progname,)
return 1
@ -131,7 +212,7 @@ def gui_main():
root.withdraw()
tkMessageBox.showerror(
"Ignoble EPUB Keyfile Generator",
"This script requires PyCrypto, which must be installed "
"This script requires OpenSSL or PyCrypto, which must be installed "
"separately. Read the top-of-script comment for details.")
return 1
root.title('Ignoble EPUB Keyfile Generator')

@ -0,0 +1,682 @@
#!/usr/bin/env python
#
# This is a WINDOWS python script. You need a Python interpreter to run it.
# For example, ActiveState Python, which exists for windows.
#
# It can run standalone to convert K4PC files, or it can be installed as a
# plugin for Calibre (http://calibre-ebook.com/about) so that importing
# K4PC files with DRM is no londer a multi-step process.
#
# ***NOTE*** Calibre and K4PC must be installed on the same windows machine
# 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 and import that ZIP into Calibre
# using its plugin configuration GUI.
#
# Thanks to The Dark Reverser for MobiDeDrm and CMBDTC for cmbdtc_dump from
# which this script steals most unashamedly.
#
# Changelog
# 0.01 - Initial version - Utilizes skindle and CMBDTC method of obtaining
# book specific pids from K4PC books. If Calibre and K4PC are installed
# on the same windows machine, Calibre plugin functionality is once
# again restored.
"""
Comprehensive Mazama Book DRM with Topaz Cryptography V2.0
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDdBHJ4CNc6DNFCw4MRCw4SWAK6
M8hYfnNEI0yQmn5Ti+W8biT7EatpauE/5jgQMPBmdNrDr1hbHyHBSP7xeC2qlRWC
B62UCxeu/fpfnvNHDN/wPWWH4jynZ2M6cdcnE5LQ+FfeKqZn7gnG2No1U9h7oOHx
y2/pHuYme7U1TsgSjwIDAQAB
-----END PUBLIC KEY-----
"""
from __future__ import with_statement
import csv
import sys
import os
import getopt
import zlib
import binascii
from struct import pack
from struct import unpack
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
import hashlib
__version__ = '0.01'
global kindleDatabase
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):
vsn = c_uint(0)
GetVolumeInformationW(path, None, 0, byref(vsn), None, None, None, 0)
return 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()
#
# Returns the MD5 digest of "message"
#
def MD5(message):
ctx = hashlib.md5()
ctx.update(message)
return ctx.digest()
#
# Returns the MD5 digest of "message"
#
def SHA1(message):
ctx = hashlib.sha1()
ctx.update(message)
return ctx.digest()
#
# 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')
#
# 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()
items = data.split('{')
for item in items:
splito = item.split(':')
DB[splito[0]] =splito[1]
return DB
#
# Find if the original string for a hashed/encoded string is known. If so return the original string othwise return an empty string. (Totally not optimal)
#
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 name
#
# Print all the records from the kindle.info file.
#
def printKindleInfo():
for record in kindleDatabase:
name = findNameForHash(record)
if name != "" :
print (name)
print ("--------------------------\n")
else :
print ("Unknown Record")
print getKindleInfoValueForHash(record)
print "\n"
#
# 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)
return CryptUnprotectData(encryptedValue,"")
#
# 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))
#
# 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
#
# Hash the bytes in data and then encode the digest with the characters in map
#
def encodeHash(data,map):
return encode(MD5(data),map)
#
# 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
#
# 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),2):
high = map.find(data[i])
low = map.find(data[i+1])
value = (((high * 0x40) ^ 0x80) & 0xFF) + low
result += pack("B",value)
return result
#
# 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
#
# 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
#
# MobiDeDrm-0.16 Stuff
#
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)
# 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 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
#
# This class does all the heavy lifting.
#
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 = unpack('>LLLBxxx32s', data[i*0x30:i*0x30+0x30])
cookie = PC1(temp_key, cookie)
ver,flags,finalkey,expiry,expiry2 = 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 = unpack('>LLLBxxx32s', data[i*0x30:i*0x30+0x30])
cookie = PC1(temp_key, cookie)
ver,flags,finalkey,expiry,expiry2 = unpack('>LL16sLL', cookie)
if verification == ver and cksum == temp_key_sum:
found_key = finalkey
break
return found_key
def __init__(self, data_file):
self.data_file = data_file
header = data_file[0:72]
if header[0x3C:0x3C+8] != 'BOOKMOBI':
raise DrmException("invalid file format")
self.num_sections, = unpack('>H', data_file[76:78])
self.sections = []
for i in xrange(self.num_sections):
offset, a1,a2,a3,a4 = 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, = unpack('>H', sect[0x8:0x8+2])
mobi_length, = unpack('>L',sect[0x14:0x18])
mobi_version, = 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, = unpack('>H', sect[0xF2:0xF4])
print "Extra Data Flags = %d" %extra_data_flags
crypto_type, = 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)
# determine the EXTH Offset.
exth_off = unpack('>I', sect[20:24])[0] + 16 + self.sections[0][0]
# Grab the entire EXTH block and feed it to the getK4PCPids function.
exth = data_file[exth_off:self.sections[0+1][0]]
pid = getK4PCPids(exth)
# calculate the keys
drm_ptr, drm_count, drm_size, drm_flags = 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 "\nDecrypting. 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!"
print "\nPlease only use your new-found powers for good."
def getResult(self):
return self.data_file
#
# DiapDealer's stuff: Parse the EXTH header records and parse the Kindleinfo
# file to calculate the book pid.
#
def getK4PCPids(exth):
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(str(GetVolumeSerialNumber(GetSystemDirectory().split('\\')[0] + '\\')),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: " + 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 K4PC 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)
if exth_records[503] != None:
print "Pid for " + exth_records[503] + ": " + bookPID
else:
print ("Book PID: " + bookPID )
return bookPID
raise DrmException("\nCould not access K4PC data - Perhaps K4PC is not installed/configured?")
return null
if not __name__ == "__main__":
from calibre.customize import FileTypePlugin
class K4PCDeDRM(FileTypePlugin):
name = 'K4PCDeDRM' # Name of the plugin
description = 'Removes DRM from K4PC files'
supported_platforms = ['windows'] # Platforms this plugin will run on
author = 'DiapDealer' # 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
def run(self, path_to_ebook):
from calibre.gui2 import is_ok_to_use_qt
from PyQt4.Qt import QMessageBox
data_file = file(path_to_ebook, 'rb').read()
try:
unlocked_file = DrmStripper(data_file).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, "K4PCDeDRM 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 ('K4PCDeDrm v%(__version__)s '
'provided DiapDealer.' % globals())
if len(sys.argv)<3:
print "Removes DRM protection from K4PC books"
print "Usage:"
print " %s <infile> <outfile>" % sys.argv[0]
sys.exit(1)
else:
infile = sys.argv[1]
outfile = sys.argv[2]
data_file = file(infile, 'rb').read()
try:
strippedFile = DrmStripper(data_file)
file(outfile, 'wb').write(strippedFile.getResult())
except DrmException, e:
print "Error: %s" % e
sys.exit(1)
sys.exit(0)

@ -0,0 +1,12 @@
K4PCDeDRM - K4PCDeDRM_X.XX_plugin.zip
Requires Calibre version 0.6.44 or higher.
This work is based on the work of cmbtc, skindle, mobidedrm. and skindleAll I had the much easier job of converting them to a Calibre plugin.
This plugin is meant to Kindle for PC azw ebooks that are protected
with Amazon's Mobi based encryption. It is meant to function without having to install any dependencies... other than having both Calibre installed and Kindle for PC on the same machine, of course.
Installation:
Go to Calibre's Preferences page... click on the Plugins button. Use the file dialog button to select the plugin's zip file (K4PCDeDRM_X.XX_plugin.zip) and click the 'Add' button. you're done.

@ -0,0 +1,38 @@
Ignoble Epub DeDRM - ignobleepub_vXX_plugin.zip
Requires Calibre version 0.6.44 or higher.
All credit given to I <3 Cabbages for the original standalone scripts.
I had the much easier job of converting them to a Calibre plugin.
This plugin is meant to decrypt Barnes & Noble Epubs that are protected
with Adobe's Adept encryption. It is meant to function without having to install any dependencies... other than having Calibre installed, of course. It will still work if you have Python and PyCrypto already installed, but they aren't necessary.
Installation:
Go to Calibre's Preferences page... click on the Plugins button. Use the file dialog button to select the plugin's zip file (ignobleepub_vXX_plugin.zip) and
click the 'Add' button. you're done.
Configuration:
1) The easiest way to configure the plugin is to enter your name (Barnes & Noble account name) and credit card number (the one used to purchase the books) into the plugin's customization window. It's the same info you would enter into the ignoblekeygen script. Highlight the plugin (Ignoble Epub DeDRM) and click the "Customize Plugin" button on
Calibre's Preferences->Plugins page. Enter the name and credit card number separated by a comma: Your Name,1234123412341234
If you've purchased books with more than one credit card, separate that other info with a colon: Your Name,1234123412341234:Other Name,2345234523452345
** NOTE ** The above method is your only option if you don't have/can't run the original I <3 Cabbages scripts on your particular machine.
** NOTE ** Your credit card number will be on display in Calibre's Plugin configuration page when using the above method. If other people have access to your computer, you may want to use the second configuration method below.
2) If you already have keyfiles generated with I <3 Cabbages' ignoblekeygen.pyw script, you can put those keyfiles into Calibre's configuration directory. The easiest way to find the correct directory is to go to Calibre's Preferences page... click on the 'Miscellaneous' button (looks like a gear), and then click the 'Open Calibre
configuration directory' button. Paste your keyfiles in there. Just make sure that they have different names and are saved with the '.b64' extension (like the ignoblekeygen script produces). This directory isn't touched when upgrading Calibre, so it's quite safe to leave them there.
All keyfiles from method 2 and all data entered from method 1 will be used to attempt to decrypt a book. You can use method 1 or method 2, or a combination of both.
Troubleshooting:
If you find that it's not working for you (imported epubs still have DRM), you can save a lot of time and trouble by trying to add the epub to Calibre with the command line tools. This will print out a lot of helpful debugging info that can be copied into any online help requests. I'm going to ask you to do it first, anyway, so you might
as well get used to it. ;)
Open a command prompt (terminal) and change to the directory where the ebook you're trying to import resides. Then type the command "calibredb add your_ebook.epub". Don't type the quotes and obviously change the 'your_ebook.epub' to whatever the filename of your book is. Copy the resulting output and paste it into any online help request you make.
** Note: the Mac version of Calibre doesn't install the command line tools by default. If you go to the 'Preferences' page and click on the miscellaneous button, you'll see the option to install the command line tools.

@ -0,0 +1,36 @@
Inept Epub DeDRM - ineptepub_vXX_plugin.zip
Requires Calibre version 0.6.44 or higher.
All credit given to I <3 Cabbages for the original standalone scripts.
I had the much easier job of converting them to a Calibre plugin.
This plugin is meant to decrypt Adobe Digital Edition Epubs that are protected with Adobe's Adept encryption. It is meant to function without having to install any dependencies... other than having Calibre installed, of course. It will still work if you have Python and PyCrypto already installed, but they aren't necessary.
Installation:
Go to Calibre's Preferences page... click on the Plugins button. Use the file dialog button to select the plugin's zip file (ineptepub_vXX_plugin.zip) and click the 'Add' button. you're done.
Configuration:
When first run, the plugin will attempt to find your Adobe Digital Editions installation (on Windows and Mac OS's). If successful, it will create an 'adeptkey.der' file and save it in Calibre's configuration directory. It will use that file on subsequent runs. If there are already '*.der' files in the directory, the plugin won't attempt to
find the Adobe Digital Editions installation installation.
So if you have Adobe Digital Editions installation installed on the same machine as Calibre... you are ready to go. If not... keep reading.
If you already have keyfiles generated with I <3 Cabbages' ineptkey.pyw script, you can put those keyfiles in Calibre's configuration directory. The easiest way to find the correct directory is to go to Calibre's Preferences page... click on the 'Miscellaneous' button (looks like a gear), and then click the 'Open Calibre configuration directory' button. Paste your keyfiles in there. Just make sure that
they have different names and are saved with the '.der' extension (like the ineptkey script produces). This directory isn't touched when upgrading Calibre, so it's quite safe to leave them there.
Since there is no Linux version of Adobe Digital Editions, Linux users will have to obtain a keyfile through other methods and put the file in Calibre's configuration directory.
All keyfiles with a '.der' extension found in Calibre's configuration directory will be used to attempt to decrypt a book.
** NOTE ** There is no plugin customization data for the Inept Epub DeDRM plugin.
Troubleshooting:
If you find that it's not working for you (imported epubs still have DRM), you can save a lot of time and trouble by trying to add the epub to Calibre with the command line tools. This will print out a lot of helpful debugging info that can be copied into any online help requests. I'm going to ask you to do it first, anyway, so you might
as well get used to it. ;)
Open a command prompt (terminal) and change to the directory where the ebook you're trying to import resides. Then type the command "calibredb add your_ebook.epub". Don't type the quotes and obviously change the 'your_ebook.epub' to whatever the filename of your book is. Copy the resulting output and paste it into any online help request you make.
** Note: the Mac version of Calibre doesn't install the command line tools by default. If you go to the 'Preferences' page and click on the miscellaneous button, you'll see the option to install the command line tools.

@ -0,0 +1,13 @@
MobiDeDRM - MobiDeDRM_X.XX_plugin.zip
Requires Calibre version 0.6.44 or higher.
This work is based on the current mobidedrm.py code.
This plugin is meant to Mobipocket and Kindle ebooks that are protected
with Amazon's Mobi based encryption. It is meant to function without having to install any dependencies... other than having both Calibre installed. You must know the PID orf the device you are using or the book specific PID to use this plugin.
Installation:
Go to Calibre's Preferences page... click on the Plugins button. Use the file dialog button to select the plugin's zip file (MobiDeDRM_X.XX_plugin.zip) and click the 'Add' button.
Then enter your PIDS in the plugin customization window separated by commas (with no spaces).

@ -0,0 +1,65 @@
Ignoble Epub DeDRM - ignobleepub_vXX_plugin.zip
Requires Calibre version 0.6.44 or higher.
All credit given to I <3 Cabbages for the original standalone scripts.
I had the much easier job of converting them to a Calibre plugin.
This plugin is meant to decrypt Barnes & Noble Epubs that are protected
with Adobe's Adept encryption. It is meant to function without having to install
any dependencies... other than having Calibre installed, of course. It will still
work if you have Python and PyCrypto already installed, but they aren't necessary.
Installation:
Go to Calibre's Preferences page... click on the Plugins button. Use the file
dialog button to select the plugin's zip file (ignobleepub_vXX_plugin.zip) and
click the 'Add' button. you're done.
Configuration:
1) The easiest way to configure the plugin is to enter your name (Barnes & Noble account
name) and credit card number (the one used to purchase the books) into the plugin's
customization window. It's the same info you would enter into the ignoblekeygen script.
Highlight the plugin (Ignoble Epub DeDRM) and click the "Customize Plugin" button on
Calibre's Preferences->Plugins page. Enter the name and credit card number separated
by a comma: Your Name,1234123412341234
If you've purchased books with more than one credit card, separate that other info with
a colon: Your Name,1234123412341234:Other Name,2345234523452345
** NOTE ** The above method is your only option if you don't have/can't run the original
I <3 Cabbages scripts on your particular machine.
** NOTE ** Your credit card number will be on display in Calibre's Plugin configuration
page when using the above method. If other people have access to your computer,
you may want to use the second configuration method below.
2) If you already have keyfiles generated with I <3 Cabbages' ignoblekeygen.pyw
script, you can put those keyfiles into Calibre's configuration directory. The easiest
way to find the correct directory is to go to Calibre's Preferences page... click
on the 'Miscellaneous' button (looks like a gear), and then click the 'Open Calibre
configuration directory' button. Paste your keyfiles in there. Just make sure that
they have different names and are saved with the '.b64' extension (like the ignoblekeygen
script produces). This directory isn't touched when upgrading Calibre, so it's quite safe
to leave then there.
All keyfiles from method 2 and all data entered from method 1 will be used to attempt
to decrypt a book. You can use method 1 or method 2, or a combination of both.
Troubleshooting:
If you find that it's not working for you (imported epubs still have DRM), you can
save a lot of time and trouble by trying to add the epub to Calibre with the command
line tools. This will print out a lot of helpful debugging info that can be copied into
any online help requests. I'm going to ask you to do it first, anyway, so you might
as well get used to it. ;)
Open a command prompt (terminal) and change to the directory where the ebook you're
trying to import resides. Then type the command "calibredb add your_ebook.epub".
Don't type the quotes and obviously change the 'your_ebook.epub' to whatever the
filename of your book is. Copy the resulting output and paste it into any online
help request you make.
** Note: the Mac version of Calibre doesn't install the command line tools by default.
If you go to the 'Preferences' page and click on the miscellaneous button, you'll
see the option to install the command line tools.

@ -0,0 +1,375 @@
#!/usr/bin/env python
# ignobleepub_v01_plugin.py
# Released under the terms of the GNU General Public Licence, version 3 or
# later. <http://www.gnu.org/licenses/>
#
# Requires Calibre version 0.6.44 or higher.
#
# All credit given to I <3 Cabbages for the original standalone scripts.
# I had the much easier job of converting them to Calibre a plugin.
#
# This plugin is meant to decrypt Barnes & Noble Epubs that are protected
# with Adobe's Adept encryption. It is meant to function without having to install
# any dependencies... other than having Calibre installed, of course. It will still
# work if you have Python and PyCrypto already installed, but they aren't necessary.
#
# Configuration:
# 1) The easiest way to configure the plugin is to enter your name (Barnes & Noble account
# name) and credit card number (the one used to purchase the books) into the plugin's
# customization window. Highlight the plugin (Ignoble Epub DeDRM) and click the
# "Customize Plugin" button on Calibre's Preferences->Plugins page.
# Enter the name and credit card number separated by a comma: Your Name,1234123412341234
#
# If you've purchased books with more than one credit card, separate the info with
# a colon: Your Name,1234123412341234:Other Name,2345234523452345
#
# ** Method 1 is your only option if you don't have/can't run the original
# I <3 Cabbages scripts on your particular machine. **
#
# 2) If you already have keyfiles generated with I <3 Cabbages' ignoblekeygen.pyw
# script, you can put those keyfiles in Calibre's configuration directory. The easiest
# way to find the correct directory is to go to Calibre's Preferences page... click
# on the 'Miscellaneous' button (looks like a gear), and then click the 'Open Calibre
# configuration directory' button. Paste your keyfiles in there. Just make sure that
# they have different names and are saved with the '.b64' extension (like the ignoblekeygen
# script produces). This directory isn't touched when upgrading Calibre, so it's quite safe
# to leave then there.
#
# All keyfiles from option 2 and all data entered from option 1 will be used to attempt
# to decrypt a book. You can use option 1 or option 2, or a combination of both.
#
#
# Revision history:
# 0.1 - Initial release
"""
Decrypt Barnes & Noble ADEPT encrypted EPUB books.
"""
from __future__ import with_statement
__license__ = 'GPL v3'
import sys
import os
import hashlib
import zlib
import zipfile
import re
from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED
import xml.etree.ElementTree as etree
from contextlib import closing
global AES
global AES2
META_NAMES = ('mimetype', 'META-INF/rights.xml', 'META-INF/encryption.xml')
NSMAP = {'adept': 'http://ns.adobe.com/adept',
'enc': 'http://www.w3.org/2001/04/xmlenc#'}
class IGNOBLEError(Exception):
pass
def _load_crypto_libcrypto():
from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_int, c_long, \
Structure, c_ulong, create_string_buffer, cast
from ctypes.util import find_library
libcrypto = find_library('crypto')
if libcrypto is None:
raise IGNOBLEError('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_set_encrypt_key = F(c_int, 'AES_set_encrypt_key',
[c_char_p, c_int, AES_KEY_p])
AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key',
[c_char_p, c_int, AES_KEY_p])
AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',
[c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p,
c_int])
class AES(object):
def __init__(self, userkey):
self._blocksize = len(userkey)
if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) :
raise IGNOBLEError('AES improper key used')
return
key = self._key = AES_KEY()
rv = AES_set_decrypt_key(userkey, len(userkey) * 8, key)
if rv < 0:
raise IGNOBLEError('Failed to initialize AES key')
def decrypt(self, data):
out = create_string_buffer(len(data))
iv = ("\x00" * self._blocksize)
rv = AES_cbc_encrypt(data, out, len(data), self._key, iv, 0)
if rv == 0:
raise IGNOBLEError('AES decryption failed')
return out.raw
class AES2(object):
def __init__(self, userkey, iv):
self._blocksize = len(userkey)
self._iv = iv
key = self._key = AES_KEY()
rv = AES_set_encrypt_key(userkey, len(userkey) * 8, key)
if rv < 0:
raise IGNOBLEError('Failed to initialize AES Encrypt key')
def encrypt(self, data):
out = create_string_buffer(len(data))
rv = AES_cbc_encrypt(data, out, len(data), self._key, self._iv, 1)
if rv == 0:
raise IGNOBLEError('AES encryption failed')
return out.raw
print 'IgnobleEpub: Using libcrypto.'
return (AES, AES2)
def _load_crypto_pycrypto():
from Crypto.Cipher import AES as _AES
class AES(object):
def __init__(self, key):
self._aes = _AES.new(key, _AES.MODE_CBC)
def decrypt(self, data):
return self._aes.decrypt(data)
class AES2(object):
def __init__(self, key, iv):
self._aes = _AES.new(key, _AES.MODE_CBC, iv)
def encrypt(self, data):
return self._aes.encrypt(data)
print 'IgnobleEpub: Using PyCrypto.'
return (AES, AES2)
def _load_crypto():
_aes = _aes2 = None
for loader in (_load_crypto_libcrypto, _load_crypto_pycrypto):
try:
_aes, _aes2 = loader()
break
except (ImportError, IGNOBLEError):
pass
return (_aes, _aes2)
def normalize_name(name): # Strip spaces and convert to lowercase.
return ''.join(x for x in name.lower() if x != ' ')
def generate_keyfile(name, ccn):
name = normalize_name(name) + '\x00'
ccn = ccn + '\x00'
name_sha = hashlib.sha1(name).digest()[:16]
ccn_sha = hashlib.sha1(ccn).digest()[:16]
both_sha = hashlib.sha1(name + ccn).digest()
aes = AES2(ccn_sha, name_sha)
crypt = aes.encrypt(both_sha + ('\x0c' * 0x0c))
userkey = hashlib.sha1(crypt).digest()
return userkey.encode('base64')
class ZipInfo(zipfile.ZipInfo):
def __init__(self, *args, **kwargs):
if 'compress_type' in kwargs:
compress_type = kwargs.pop('compress_type')
super(ZipInfo, self).__init__(*args, **kwargs)
self.compress_type = compress_type
class Decryptor(object):
def __init__(self, bookkey, encryption):
enc = lambda tag: '{%s}%s' % (NSMAP['enc'], tag)
self._aes = AES(bookkey)
encryption = etree.fromstring(encryption)
self._encrypted = encrypted = set()
expr = './%s/%s/%s' % (enc('EncryptedData'), enc('CipherData'),
enc('CipherReference'))
for elem in encryption.findall(expr):
path = elem.get('URI', None)
if path is not None:
encrypted.add(path)
def decompress(self, bytes):
dc = zlib.decompressobj(-15)
bytes = dc.decompress(bytes)
ex = dc.decompress('Z') + dc.flush()
if ex:
bytes = bytes + ex
return bytes
def decrypt(self, path, data):
if path in self._encrypted:
data = self._aes.decrypt(data)[16:]
data = data[:-ord(data[-1])]
data = self.decompress(data)
return data
def plugin_main(userkey, inpath, outpath):
key = userkey.decode('base64')[:16]
aes = AES(key)
with closing(ZipFile(open(inpath, 'rb'))) as inf:
namelist = set(inf.namelist())
if 'META-INF/rights.xml' not in namelist or \
'META-INF/encryption.xml' not in namelist:
return 1
for name in META_NAMES:
namelist.remove(name)
try: # If the generated keyfile doesn't match the bookkey, this is where it's likely to blow up.
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
expr = './/%s' % (adept('encryptedKey'),)
bookkey = ''.join(rights.findtext(expr))
bookkey = aes.decrypt(bookkey.decode('base64'))
bookkey = bookkey[:-ord(bookkey[-1])]
encryption = inf.read('META-INF/encryption.xml')
decryptor = Decryptor(bookkey[-16:], encryption)
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf:
zi = ZipInfo('mimetype', compress_type=ZIP_STORED)
outf.writestr(zi, inf.read('mimetype'))
for path in namelist:
data = inf.read(path)
outf.writestr(path, decryptor.decrypt(path, data))
except:
return 2
return 0
from calibre.customize import FileTypePlugin
class IgnobleDeDRM(FileTypePlugin):
name = 'Ignoble Epub DeDRM'
description = 'Removes DRM from secure Barnes & Noble epub files. \
Credit given to I <3 Cabbages for the original stand-alone scripts.'
supported_platforms = ['linux', 'osx', 'windows']
author = 'DiapDealer'
version = (0, 1, 0)
minimum_calibre_version = (0, 6, 44) # Compiled python libraries cannot be imported in earlier versions.
file_types = set(['epub'])
on_import = True
def run(self, path_to_ebook):
global AES
global AES2
from calibre.gui2 import is_ok_to_use_qt
from PyQt4.Qt import QMessageBox
from calibre.constants import iswindows, isosx
# Add the included pycrypto import directory for Windows users.
pdir = 'windows' if iswindows else 'osx' if isosx else 'linux'
ppath = os.path.join(self.sys_insertion_path, pdir)
#sys.path.insert(0, ppath)
sys.path.append(ppath)
AES, AES2 = _load_crypto()
if AES == None or AES2 == None:
# Failed to load libcrypto or PyCrypto... Adobe Epubs can\'t be decrypted.'
sys.path.remove(ppath)
raise IGNOBLEError('IgnobleEpub - Failed to load crypto libs.')
return
# Load any keyfiles (*.b64) included Calibre's config directory.
userkeys = []
try:
# Find Calibre's configuration directory.
confpath = os.path.split(os.path.split(self.plugin_path)[0])[0]
print 'IgnobleEpub: Calibre configuration directory = %s' % confpath
files = os.listdir(confpath)
filefilter = re.compile("\.b64$", re.IGNORECASE)
files = filter(filefilter.search, files)
if files:
for filename in files:
fpath = os.path.join(confpath, filename)
with open(fpath, 'rb') as f:
userkeys.append(f.read())
print 'IgnobleEpub: Keyfile %s found in config folder.' % filename
else:
print 'IgnobleEpub: No keyfiles found. Checking plugin customization string.'
except IOError:
print 'IgnobleEpub: Error reading keyfiles from config directory.'
pass
# Get name and credit card number from Plugin Customization
if not userkeys and not self.site_customization:
# Plugin hasn't been configured... do nothing.
sys.path.remove(ppath)
raise IGNOBLEError('IgnobleEpub - No keys found. Plugin not configured.')
return
if self.site_customization:
keystuff = self.site_customization
ar = keystuff.split(':')
keycount = 0
for i in ar:
try:
name, ccn = i.split(',')
keycount += 1
except ValueError:
sys.path.remove(ppath)
raise IGNOBLEError('IgnobleEpub - Error parsing user supplied data.')
return
# Generate Barnes & Noble EPUB user key from name and credit card number.
userkeys.append( generate_keyfile(name, ccn) )
print 'IgnobleEpub: %d userkey(s) generated from customization data.' % keycount
# Attempt to decrypt epub with each encryption key (generated or provided).
for userkey in userkeys:
# Create a TemporaryPersistent file to work with.
of = self.temporary_file('.epub')
# Give the user key, ebook and TemporaryPersistent file to the Stripper function.
result = plugin_main(userkey, path_to_ebook, of.name)
# Ebook is not a B&N Adept epub... do nothing and pass it on.
# This allows a non-encrypted epub to be imported without error messages.
if result == 1:
print 'IgnobleEpub: Not a B&N Adept Epub... punting.'
of.close()
sys.path.remove(ppath)
return path_to_ebook
break
# Decryption was successful return the modified PersistentTemporary
# file to Calibre's import process.
if result == 0:
print 'IgnobleEpub: Encryption successfully removed.'
of.close()
sys.path.remove(ppath)
return of.name
break
print 'IgnobleEpub: Encryption key invalid... trying others.'
of.close()
# Something went wrong with decryption.
# Import the original unmolested epub.
of.close
sys.path.remove(ppath)
raise IGNOBLEError('IgnobleEpub - Ultimately failed to decrypt.')
return
def customization_help(self, gui=False):
return 'Enter B&N Account name and CC# (separate name and CC# with a comma)'

@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-
#
# ===================================================================
# The contents of this file are dedicated to the public domain. To
# the extent that dedication to the public domain is not available,
# everyone is granted a worldwide, perpetual, royalty-free,
# non-exclusive license to exercise all rights associated with the
# contents of this file for any purpose whatsoever.
# No rights are reserved.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# ===================================================================
"""Secret-key encryption algorithms.
Secret-key encryption algorithms transform plaintext in some way that
is dependent on a key, producing ciphertext. This transformation can
easily be reversed, if (and, hopefully, only if) one knows the key.
The encryption modules here all support the interface described in PEP
272, "API for Block Encryption Algorithms".
If you don't know which algorithm to choose, use AES because it's
standard and has undergone a fair bit of examination.
Crypto.Cipher.AES Advanced Encryption Standard
Crypto.Cipher.ARC2 Alleged RC2
Crypto.Cipher.ARC4 Alleged RC4
Crypto.Cipher.Blowfish
Crypto.Cipher.CAST
Crypto.Cipher.DES The Data Encryption Standard. Very commonly used
in the past, but today its 56-bit keys are too small.
Crypto.Cipher.DES3 Triple DES.
Crypto.Cipher.XOR The simple XOR cipher.
"""
__all__ = ['AES', 'ARC2', 'ARC4',
'Blowfish', 'CAST', 'DES', 'DES3',
'XOR'
]
__revision__ = "$Id$"

@ -0,0 +1,46 @@
# -*- coding: utf-8 -*-
#
# ===================================================================
# The contents of this file are dedicated to the public domain. To
# the extent that dedication to the public domain is not available,
# everyone is granted a worldwide, perpetual, royalty-free,
# non-exclusive license to exercise all rights associated with the
# contents of this file for any purpose whatsoever.
# No rights are reserved.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# ===================================================================
"""Python Cryptography Toolkit
A collection of cryptographic modules implementing various algorithms
and protocols.
Subpackages:
Crypto.Cipher Secret-key encryption algorithms (AES, DES, ARC4)
Crypto.Hash Hashing algorithms (MD5, SHA, HMAC)
Crypto.Protocol Cryptographic protocols (Chaffing, all-or-nothing
transform). This package does not contain any
network protocols.
Crypto.PublicKey Public-key encryption and signature algorithms
(RSA, DSA)
Crypto.Util Various useful modules and functions (long-to-string
conversion, random number generation, number
theoretic functions)
"""
__all__ = ['Cipher', 'Hash', 'Protocol', 'PublicKey', 'Util']
__version__ = '2.3' # See also below and setup.py
__revision__ = "$Id$"
# New software should look at this instead of at __version__ above.
version_info = (2, 1, 0, 'final', 0) # See also above and setup.py

@ -0,0 +1,57 @@
# -*- coding: ascii -*-
#
# pct_warnings.py : PyCrypto warnings file
#
# Written in 2008 by Dwayne C. Litzenberger <dlitz@dlitz.net>
#
# ===================================================================
# The contents of this file are dedicated to the public domain. To
# the extent that dedication to the public domain is not available,
# everyone is granted a worldwide, perpetual, royalty-free,
# non-exclusive license to exercise all rights associated with the
# contents of this file for any purpose whatsoever.
# No rights are reserved.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# ===================================================================
#
# Base classes. All our warnings inherit from one of these in order to allow
# the user to specifically filter them.
#
class CryptoWarning(Warning):
"""Base class for PyCrypto warnings"""
class CryptoDeprecationWarning(DeprecationWarning, CryptoWarning):
"""Base PyCrypto DeprecationWarning class"""
class CryptoRuntimeWarning(RuntimeWarning, CryptoWarning):
"""Base PyCrypto RuntimeWarning class"""
#
# Warnings that we might actually use
#
class RandomPool_DeprecationWarning(CryptoDeprecationWarning):
"""Issued when Crypto.Util.randpool.RandomPool is instantiated."""
class ClockRewindWarning(CryptoRuntimeWarning):
"""Warning for when the system clock moves backwards."""
class GetRandomNumber_DeprecationWarning(CryptoDeprecationWarning):
"""Issued when Crypto.Util.number.getRandomNumber is invoked."""
# By default, we want this warning to be shown every time we compensate for
# clock rewinding.
import warnings as _warnings
_warnings.filterwarnings('always', category=ClockRewindWarning, append=1)
# vim:set ts=4 sw=4 sts=4 expandtab:

@ -0,0 +1,62 @@
Inept Epub DeDRM - ineptepub_vXX_plugin.zip
Requires Calibre version 0.6.44 or higher.
All credit given to I <3 Cabbages for the original standalone scripts.
I had the much easier job of converting them to a Calibre plugin.
This plugin is meant to decrypt Adobe Digital Edition Epubs that are protected
with Adobe's Adept encryption. It is meant to function without having to install
any dependencies... other than having Calibre installed, of course. It will still
work if you have Python and PyCrypto already installed, but they aren't necessary.
Installation:
Go to Calibre's Preferences page... click on the Plugins button. Use the file
dialog button to select the plugin's zip file (ineptepub_vXX_plugin.zip) and
click the 'Add' button. you're done.
Configuration:
When first run, the plugin will attempt to find your Adobe Digital Editions installation
(on Windows and Mac OS's). If successful, it will create an 'adeptkey.der' file and
save it in Calibre's configuration directory. It will use that file on subsequent runs.
If there are already '*.der' files in the directory, the plugin won't attempt to
find the Adobe Digital Editions installation installation.
So if you have Adobe Digital Editions installation installed on the same machine as Calibre...
you are ready to go. If not... keep reading.
If you already have keyfiles generated with I <3 Cabbages' ineptkey.pyw script,
you can put those keyfiles in Calibre's configuration directory. The easiest
way to find the correct directory is to go to Calibre's Preferences page... click
on the 'Miscellaneous' button (looks like a gear), and then click the 'Open Calibre
configuration directory' button. Paste your keyfiles in there. Just make sure that
they have different names and are saved with the '.der' extension (like the ineptkey
script produces). This directory isn't touched when upgrading Calibre, so it's quite
safe to leave them there.
Since there is no Linux version of Adobe Digital Editions, Linux users will have to
obtain a keyfile through other methods and put the file in Calibre's configuration directory.
All keyfiles with a '.der' extension found in Calibre's configuration directory will
be used to attempt to decrypt a book.
** NOTE ** There is no plugin customization data for the Inept Epub DeDRM plugin.
Troubleshooting:
If you find that it's not working for you (imported epubs still have DRM), you can
save a lot of time and trouble by trying to add the epub to Calibre with the command
line tools. This will print out a lot of helpful debugging info that can be copied into
any online help requests. I'm going to ask you to do it first, anyway, so you might
as well get used to it. ;)
Open a command prompt (terminal) and change to the directory where the ebook you're
trying to import resides. Then type the command "calibredb add your_ebook.epub".
Don't type the quotes and obviously change the 'your_ebook.epub' to whatever the
filename of your book is. Copy the resulting output and paste it into any online
help request you make.
** Note: the Mac version of Calibre doesn't install the command line tools by default.
If you go to the 'Preferences' page and click on the miscellaneous button, you'll
see the option to install the command line tools.

@ -0,0 +1,283 @@
#!/usr/bin/env python
"""
Retrieve Adobe ADEPT user key.
"""
from __future__ import with_statement
__license__ = 'GPL v3'
import sys
import os
import struct
from calibre.constants import iswindows, isosx
class ADEPTError(Exception):
pass
if iswindows:
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, c_size_t, memmove
from ctypes.wintypes import LPVOID, DWORD, BOOL
import _winreg as winreg
try:
from Crypto.Cipher import AES as _aes
except ImportError:
_aes = None
DEVICE_KEY_PATH = r'Software\Adobe\Adept\Device'
PRIVATE_LICENCE_KEY_PATH = r'Software\Adobe\Adept\Activation'
MAX_PATH = 255
kernel32 = windll.kernel32
advapi32 = windll.advapi32
crypt32 = windll.crypt32
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):
vsn = c_uint(0)
GetVolumeInformationW(
path, None, 0, byref(vsn), None, None, None, 0)
return 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()
PAGE_EXECUTE_READWRITE = 0x40
MEM_COMMIT = 0x1000
MEM_RESERVE = 0x2000
def VirtualAlloc():
_VirtualAlloc = kernel32.VirtualAlloc
_VirtualAlloc.argtypes = [LPVOID, c_size_t, DWORD, DWORD]
_VirtualAlloc.restype = LPVOID
def VirtualAlloc(addr, size, alloctype=(MEM_COMMIT | MEM_RESERVE),
protect=PAGE_EXECUTE_READWRITE):
return _VirtualAlloc(addr, size, alloctype, protect)
return VirtualAlloc
VirtualAlloc = VirtualAlloc()
MEM_RELEASE = 0x8000
def VirtualFree():
_VirtualFree = kernel32.VirtualFree
_VirtualFree.argtypes = [LPVOID, c_size_t, DWORD]
_VirtualFree.restype = BOOL
def VirtualFree(addr, size=0, freetype=MEM_RELEASE):
return _VirtualFree(addr, size, freetype)
return VirtualFree
VirtualFree = VirtualFree()
class NativeFunction(object):
def __init__(self, restype, argtypes, insns):
self._buf = buf = VirtualAlloc(None, len(insns))
memmove(buf, insns, len(insns))
ftype = CFUNCTYPE(restype, *argtypes)
self._native = ftype(buf)
def __call__(self, *args):
return self._native(*args)
def __del__(self):
if self._buf is not None:
VirtualFree(self._buf)
self._buf = None
if struct.calcsize("P") == 4:
CPUID0_INSNS = (
"\x53" # push %ebx
"\x31\xc0" # xor %eax,%eax
"\x0f\xa2" # cpuid
"\x8b\x44\x24\x08" # mov 0x8(%esp),%eax
"\x89\x18" # mov %ebx,0x0(%eax)
"\x89\x50\x04" # mov %edx,0x4(%eax)
"\x89\x48\x08" # mov %ecx,0x8(%eax)
"\x5b" # pop %ebx
"\xc3" # ret
)
CPUID1_INSNS = (
"\x53" # push %ebx
"\x31\xc0" # xor %eax,%eax
"\x40" # inc %eax
"\x0f\xa2" # cpuid
"\x5b" # pop %ebx
"\xc3" # ret
)
else:
CPUID0_INSNS = (
"\x49\x89\xd8" # mov %rbx,%r8
"\x49\x89\xc9" # mov %rcx,%r9
"\x48\x31\xc0" # xor %rax,%rax
"\x0f\xa2" # cpuid
"\x4c\x89\xc8" # mov %r9,%rax
"\x89\x18" # mov %ebx,0x0(%rax)
"\x89\x50\x04" # mov %edx,0x4(%rax)
"\x89\x48\x08" # mov %ecx,0x8(%rax)
"\x4c\x89\xc3" # mov %r8,%rbx
"\xc3" # retq
)
CPUID1_INSNS = (
"\x53" # push %rbx
"\x48\x31\xc0" # xor %rax,%rax
"\x48\xff\xc0" # inc %rax
"\x0f\xa2" # cpuid
"\x5b" # pop %rbx
"\xc3" # retq
)
def cpuid0():
_cpuid0 = NativeFunction(None, [c_char_p], CPUID0_INSNS)
buf = create_string_buffer(12)
def cpuid0():
_cpuid0(buf)
return buf.raw
return cpuid0
cpuid0 = cpuid0()
cpuid1 = NativeFunction(c_uint, [], CPUID1_INSNS)
class DataBlob(Structure):
_fields_ = [('cbData', c_uint),
('pbData', c_void_p)]
DataBlob_p = POINTER(DataBlob)
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 ADEPTError("Failed to decrypt user key key (sic)")
return string_at(outdata.pbData, outdata.cbData)
return CryptUnprotectData
CryptUnprotectData = CryptUnprotectData()
def retrieve_key():
if _aes is None:
raise ADEPTError("Couldn\'t load PyCrypto")
root = GetSystemDirectory().split('\\')[0] + '\\'
serial = GetVolumeSerialNumber(root)
vendor = cpuid0()
signature = struct.pack('>I', cpuid1())[1:]
user = GetUserName()
entropy = struct.pack('>I12s3s13s', serial, vendor, signature, user)
cuser = winreg.HKEY_CURRENT_USER
try:
regkey = winreg.OpenKey(cuser, DEVICE_KEY_PATH)
except WindowsError:
raise ADEPTError("Adobe Digital Editions not activated")
device = winreg.QueryValueEx(regkey, 'key')[0]
keykey = CryptUnprotectData(device, entropy)
userkey = None
try:
plkroot = winreg.OpenKey(cuser, PRIVATE_LICENCE_KEY_PATH)
except WindowsError:
raise ADEPTError("Could not locate ADE activation")
for i in xrange(0, 16):
try:
plkparent = winreg.OpenKey(plkroot, "%04d" % (i,))
except WindowsError:
break
ktype = winreg.QueryValueEx(plkparent, None)[0]
if ktype != 'credentials':
continue
for j in xrange(0, 16):
try:
plkkey = winreg.OpenKey(plkparent, "%04d" % (j,))
except WindowsError:
break
ktype = winreg.QueryValueEx(plkkey, None)[0]
if ktype != 'privateLicenseKey':
continue
userkey = winreg.QueryValueEx(plkkey, 'value')[0]
break
if userkey is not None:
break
if userkey is None:
raise ADEPTError('Could not locate privateLicenseKey')
userkey = userkey.decode('base64')
userkey = _aes.new(keykey, _aes.MODE_CBC).decrypt(userkey)
userkey = userkey[26:-ord(userkey[-1])]
return userkey
else:
import xml.etree.ElementTree as etree
import Carbon.File
import Carbon.Folder
import Carbon.Folders
import MacOS
ACTIVATION_PATH = 'Adobe/Digital Editions/activation.dat'
NSMAP = {'adept': 'http://ns.adobe.com/adept',
'enc': 'http://www.w3.org/2001/04/xmlenc#'}
def find_folder(domain, dtype):
try:
fsref = Carbon.Folder.FSFindFolder(domain, dtype, False)
return Carbon.File.pathname(fsref)
except MacOS.Error:
return None
def find_app_support_file(subpath):
dtype = Carbon.Folders.kApplicationSupportFolderType
for domain in Carbon.Folders.kUserDomain, Carbon.Folders.kLocalDomain:
path = find_folder(domain, dtype)
if path is None:
continue
path = os.path.join(path, subpath)
if os.path.isfile(path):
return path
return None
def retrieve_key():
actpath = find_app_support_file(ACTIVATION_PATH)
if actpath is None:
raise ADEPTError("Could not locate ADE activation")
tree = etree.parse(actpath)
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
expr = '//%s/%s' % (adept('credentials'), adept('privateLicenseKey'))
userkey = tree.findtext(expr)
userkey = userkey.decode('base64')
userkey = userkey[26:]
return userkey

@ -0,0 +1,468 @@
#! /usr/bin/python
# ineptepub_v01_plugin.py
# Released under the terms of the GNU General Public Licence, version 3 or
# later. <http://www.gnu.org/licenses/>
#
# Requires Calibre version 0.6.44 or higher.
#
# All credit given to I <3 Cabbages for the original standalone scripts.
# I had the much easier job of converting them to a Calibre plugin.
#
# This plugin is meant to decrypt Adobe Digital Edition Epubs that are protected
# with Adobe's Adept encryption. It is meant to function without having to install
# any dependencies... other than having Calibre installed, of course. It will still
# work if you have Python and PyCrypto already installed, but they aren't necessary.
#
# Configuration:
# When first run, the plugin will attempt to find your Adobe Digital Editions installation
# (on Windows and Mac OS's). If successful, it will create an 'adeptkey.der' file and
# save it in Calibre's configuration directory. It will use that file on subsequent runs.
# If there are already '*.der' files in the directory, the plugin won't attempt to
# find the ADE installation. So if you have ADE installed on the same machine as Calibre...
# you are ready to go.
#
# If you already have keyfiles generated with I <3 Cabbages' ineptkey.pyw script,
# you can put those keyfiles in Calibre's configuration directory. The easiest
# way to find the correct directory is to go to Calibre's Preferences page... click
# on the 'Miscellaneous' button (looks like a gear), and then click the 'Open Calibre
# configuration directory' button. Paste your keyfiles in there. Just make sure that
# they have different names and are saved with the '.der' extension (like the ineptkey
# script produces). This directory isn't touched when upgrading Calibre, so it's quite
# safe to leave them there.
#
# Since there is no Linux version of Adobe Digital Editions, Linux users will have to
# obtain a keyfile through other methods and put the file in Calibre's configuration directory.
#
# All keyfiles with a '.der' extension found in Calibre's configuration directory will
# be used to attempt to decrypt a book.
#
# ** NOTE ** There is no plugin customization data for the Inept Epub DeDRM plugin.
#
# Revision history:
# 0.1 - Initial release
"""
Decrypt Adobe ADEPT-encrypted EPUB books.
"""
from __future__ import with_statement
__license__ = 'GPL v3'
import sys
import os
import zlib
import zipfile
import re
from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED
from contextlib import closing
import xml.etree.ElementTree as etree
global AES
global RSA
META_NAMES = ('mimetype', 'META-INF/rights.xml', 'META-INF/encryption.xml')
NSMAP = {'adept': 'http://ns.adobe.com/adept',
'enc': 'http://www.w3.org/2001/04/xmlenc#'}
class ADEPTError(Exception):
pass
def _load_crypto_libcrypto():
from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_int, c_long, \
Structure, c_ulong, create_string_buffer, cast
from ctypes.util import find_library
libcrypto = find_library('crypto')
if libcrypto is None:
raise ADEPTError('libcrypto not found')
libcrypto = CDLL(libcrypto)
RSA_NO_PADDING = 3
AES_MAXNR = 14
c_char_pp = POINTER(c_char_p)
c_int_p = POINTER(c_int)
class RSA(Structure):
pass
RSA_p = POINTER(RSA)
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
d2i_RSAPrivateKey = F(RSA_p, 'd2i_RSAPrivateKey',
[RSA_p, c_char_pp, c_long])
RSA_size = F(c_int, 'RSA_size', [RSA_p])
RSA_private_decrypt = F(c_int, 'RSA_private_decrypt',
[c_int, c_char_p, c_char_p, RSA_p, c_int])
RSA_free = F(None, 'RSA_free', [RSA_p])
AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key',
[c_char_p, c_int, AES_KEY_p])
AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',
[c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p,
c_int])
class RSA(object):
def __init__(self, der):
buf = create_string_buffer(der)
pp = c_char_pp(cast(buf, c_char_p))
rsa = self._rsa = d2i_RSAPrivateKey(None, pp, len(der))
if rsa is None:
raise ADEPTError('Error parsing ADEPT user key DER')
def decrypt(self, from_):
rsa = self._rsa
to = create_string_buffer(RSA_size(rsa))
dlen = RSA_private_decrypt(len(from_), from_, to, rsa,
RSA_NO_PADDING)
if dlen < 0:
raise ADEPTError('RSA decryption failed')
return to[:dlen]
def __del__(self):
if self._rsa is not None:
RSA_free(self._rsa)
self._rsa = None
class AES(object):
def __init__(self, userkey):
self._blocksize = len(userkey)
if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) :
raise ADEPTError('AES improper key used')
return
key = self._key = AES_KEY()
rv = AES_set_decrypt_key(userkey, len(userkey) * 8, key)
if rv < 0:
raise ADEPTError('Failed to initialize AES key')
def decrypt(self, data):
out = create_string_buffer(len(data))
iv = ("\x00" * self._blocksize)
rv = AES_cbc_encrypt(data, out, len(data), self._key, iv, 0)
if rv == 0:
raise ADEPTError('AES decryption failed')
return out.raw
print 'IneptEpub: Using libcrypto.'
return (AES, RSA)
def _load_crypto_pycrypto():
from Crypto.Cipher import AES as _AES
from Crypto.PublicKey import RSA as _RSA
# ASN.1 parsing code from tlslite
class ASN1Error(Exception):
pass
class ASN1Parser(object):
class Parser(object):
def __init__(self, bytes):
self.bytes = bytes
self.index = 0
def get(self, length):
if self.index + length > len(self.bytes):
raise ASN1Error("Error decoding ASN.1")
x = 0
for count in range(length):
x <<= 8
x |= self.bytes[self.index]
self.index += 1
return x
def getFixBytes(self, lengthBytes):
bytes = self.bytes[self.index : self.index+lengthBytes]
self.index += lengthBytes
return bytes
def getVarBytes(self, lengthLength):
lengthBytes = self.get(lengthLength)
return self.getFixBytes(lengthBytes)
def getFixList(self, length, lengthList):
l = [0] * lengthList
for x in range(lengthList):
l[x] = self.get(length)
return l
def getVarList(self, length, lengthLength):
lengthList = self.get(lengthLength)
if lengthList % length != 0:
raise ASN1Error("Error decoding ASN.1")
lengthList = int(lengthList/length)
l = [0] * lengthList
for x in range(lengthList):
l[x] = self.get(length)
return l
def startLengthCheck(self, lengthLength):
self.lengthCheck = self.get(lengthLength)
self.indexCheck = self.index
def setLengthCheck(self, length):
self.lengthCheck = length
self.indexCheck = self.index
def stopLengthCheck(self):
if (self.index - self.indexCheck) != self.lengthCheck:
raise ASN1Error("Error decoding ASN.1")
def atLengthCheck(self):
if (self.index - self.indexCheck) < self.lengthCheck:
return False
elif (self.index - self.indexCheck) == self.lengthCheck:
return True
else:
raise ASN1Error("Error decoding ASN.1")
def __init__(self, bytes):
p = self.Parser(bytes)
p.get(1)
self.length = self._getASN1Length(p)
self.value = p.getFixBytes(self.length)
def getChild(self, which):
p = self.Parser(self.value)
for x in range(which+1):
markIndex = p.index
p.get(1)
length = self._getASN1Length(p)
p.getFixBytes(length)
return ASN1Parser(p.bytes[markIndex:p.index])
def _getASN1Length(self, p):
firstLength = p.get(1)
if firstLength<=127:
return firstLength
else:
lengthLength = firstLength & 0x7F
return p.get(lengthLength)
class AES(object):
def __init__(self, key):
self._aes = _AES.new(key, _AES.MODE_CBC)
def decrypt(self, data):
return self._aes.decrypt(data)
class RSA(object):
def __init__(self, der):
key = ASN1Parser([ord(x) for x in der])
key = [key.getChild(x).value for x in xrange(1, 4)]
key = [self.bytesToNumber(v) for v in key]
self._rsa = _RSA.construct(key)
def bytesToNumber(self, bytes):
total = 0L
for byte in bytes:
total = (total << 8) + byte
return total
def decrypt(self, data):
return self._rsa.decrypt(data)
print 'IneptEpub: Using pycrypto.'
return (AES, RSA)
def _load_crypto():
_aes = _rsa = None
for loader in (_load_crypto_libcrypto, _load_crypto_pycrypto):
try:
_aes, _rsa = loader()
break
except (ImportError, ADEPTError):
pass
return (_aes, _rsa)
class ZipInfo(zipfile.ZipInfo):
def __init__(self, *args, **kwargs):
if 'compress_type' in kwargs:
compress_type = kwargs.pop('compress_type')
super(ZipInfo, self).__init__(*args, **kwargs)
self.compress_type = compress_type
class Decryptor(object):
def __init__(self, bookkey, encryption):
enc = lambda tag: '{%s}%s' % (NSMAP['enc'], tag)
self._aes = AES(bookkey)
encryption = etree.fromstring(encryption)
self._encrypted = encrypted = set()
expr = './%s/%s/%s' % (enc('EncryptedData'), enc('CipherData'),
enc('CipherReference'))
for elem in encryption.findall(expr):
path = elem.get('URI', None)
if path is not None:
encrypted.add(path)
def decompress(self, bytes):
dc = zlib.decompressobj(-15)
bytes = dc.decompress(bytes)
ex = dc.decompress('Z') + dc.flush()
if ex:
bytes = bytes + ex
return bytes
def decrypt(self, path, data):
if path in self._encrypted:
data = self._aes.decrypt(data)[16:]
data = data[:-ord(data[-1])]
data = self.decompress(data)
return data
def plugin_main(userkey, inpath, outpath):
rsa = RSA(userkey)
with closing(ZipFile(open(inpath, 'rb'))) as inf:
namelist = set(inf.namelist())
if 'META-INF/rights.xml' not in namelist or \
'META-INF/encryption.xml' not in namelist:
return 1
for name in META_NAMES:
namelist.remove(name)
try:
rights = etree.fromstring(inf.read('META-INF/rights.xml'))
adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
expr = './/%s' % (adept('encryptedKey'),)
bookkey = ''.join(rights.findtext(expr))
bookkey = rsa.decrypt(bookkey.decode('base64'))
# Padded as per RSAES-PKCS1-v1_5
if bookkey[-17] != '\x00':
raise ADEPTError('problem decrypting session key')
encryption = inf.read('META-INF/encryption.xml')
decryptor = Decryptor(bookkey[-16:], encryption)
kwds = dict(compression=ZIP_DEFLATED, allowZip64=False)
with closing(ZipFile(open(outpath, 'wb'), 'w', **kwds)) as outf:
zi = ZipInfo('mimetype', compress_type=ZIP_STORED)
outf.writestr(zi, inf.read('mimetype'))
for path in namelist:
data = inf.read(path)
outf.writestr(path, decryptor.decrypt(path, data))
except:
return 2
return 0
from calibre.customize import FileTypePlugin
class IneptDeDRM(FileTypePlugin):
name = 'Inept Epub DeDRM'
description = 'Removes DRM from secure Adobe epub files. \
Credit given to I <3 Cabbages for the original stand-alone scripts.'
supported_platforms = ['linux', 'osx', 'windows']
author = 'DiapDealer'
version = (0, 1, 0)
minimum_calibre_version = (0, 6, 44) # Compiled python libraries cannot be imported in earlier versions.
file_types = set(['epub'])
on_import = True
priority = 100
def run(self, path_to_ebook):
global AES
global RSA
from calibre.gui2 import is_ok_to_use_qt
from PyQt4.Qt import QMessageBox
from calibre.constants import iswindows, isosx
# Add the included pycrypto import directory for Windows users.
# Add the included Carbon import directory for Mac users.
pdir = 'windows' if iswindows else 'osx' if isosx else 'linux'
ppath = os.path.join(self.sys_insertion_path, pdir)
#sys.path.insert(0, ppath)
sys.path.append(ppath)
AES, RSA = _load_crypto()
if AES == None or RSA == None:
# Failed to load libcrypto or PyCrypto... Adobe Epubs can\'t be decrypted.'
sys.path.remove(ppath)
raise ADEPTError('IneptEpub: Failed to load crypto libs... Adobe Epubs can\'t be decrypted.')
return
# Load any keyfiles (*.der) included Calibre's config directory.
userkeys = []
# Find Calibre's configuration directory.
confpath = os.path.split(os.path.split(self.plugin_path)[0])[0]
print 'IneptEpub: Calibre configuration directory = %s' % confpath
files = os.listdir(confpath)
filefilter = re.compile("\.der$", re.IGNORECASE)
files = filter(filefilter.search, files)
if files:
try:
for filename in files:
fpath = os.path.join(confpath, filename)
with open(fpath, 'rb') as f:
userkeys.append(f.read())
print 'IneptEpub: Keyfile %s found in config folder.' % filename
except IOError:
print 'IneptEpub: Error reading keyfiles from config directory.'
pass
else:
# Try to find key from ADE install and save the key in
# Calibre's configuration directory for future use.
if iswindows or isosx:
# ADE key retrieval script included in respective OS folder.
from ade_key import retrieve_key
try:
keydata = retrieve_key()
userkeys.append(keydata)
keypath = os.path.join(confpath, 'adeptkey.der')
with open(keypath, 'wb') as f:
f.write(keydata)
print 'IneptEpub: Created keyfile from ADE install.'
except:
print 'IneptEpub: Couldn\'t Retrieve key from ADE install.'
pass
if not userkeys:
# No user keys found... bail out.
sys.path.remove(ppath)
raise ADEPTError('IneptEpub - No keys found. Check keyfile(s)/ADE install')
return
# Attempt to decrypt epub with each encryption key found.
for userkey in userkeys:
# Create a TemporaryPersistent file to work with.
of = self.temporary_file('.epub')
# Give the user key, ebook and TemporaryPersistent file to the plugin_main function.
result = plugin_main(userkey, path_to_ebook, of.name)
# Ebook is not an Adobe Adept epub... do nothing and pass it on.
# This allows a non-encrypted epub to be imported without error messages.
if result == 1:
print 'IneptEpub: Not an Adobe Adept Epub... punting.'
of.close()
sys.path.remove(ppath)
return path_to_ebook
break
# Decryption was successful return the modified PersistentTemporary
# file to Calibre's import process.
if result == 0:
print 'IneptEpub: Encryption successfully removed.'
of.close
sys.path.remove(ppath)
return of.name
break
print 'IneptEpub: Encryption key invalid... trying others.'
of.close()
# Something went wrong with decryption.
# Import the original unmolested epub.
of.close
sys.path.remove(ppath)
raise ADEPTError('IneptEpub - Ultimately failed to decrypt')
return

@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-
#
# ===================================================================
# The contents of this file are dedicated to the public domain. To
# the extent that dedication to the public domain is not available,
# everyone is granted a worldwide, perpetual, royalty-free,
# non-exclusive license to exercise all rights associated with the
# contents of this file for any purpose whatsoever.
# No rights are reserved.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# ===================================================================
"""Secret-key encryption algorithms.
Secret-key encryption algorithms transform plaintext in some way that
is dependent on a key, producing ciphertext. This transformation can
easily be reversed, if (and, hopefully, only if) one knows the key.
The encryption modules here all support the interface described in PEP
272, "API for Block Encryption Algorithms".
If you don't know which algorithm to choose, use AES because it's
standard and has undergone a fair bit of examination.
Crypto.Cipher.AES Advanced Encryption Standard
Crypto.Cipher.ARC2 Alleged RC2
Crypto.Cipher.ARC4 Alleged RC4
Crypto.Cipher.Blowfish
Crypto.Cipher.CAST
Crypto.Cipher.DES The Data Encryption Standard. Very commonly used
in the past, but today its 56-bit keys are too small.
Crypto.Cipher.DES3 Triple DES.
Crypto.Cipher.XOR The simple XOR cipher.
"""
__all__ = ['AES', 'ARC2', 'ARC4',
'Blowfish', 'CAST', 'DES', 'DES3',
'XOR'
]
__revision__ = "$Id$"

@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-
#
# ===================================================================
# The contents of this file are dedicated to the public domain. To
# the extent that dedication to the public domain is not available,
# everyone is granted a worldwide, perpetual, royalty-free,
# non-exclusive license to exercise all rights associated with the
# contents of this file for any purpose whatsoever.
# No rights are reserved.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# ===================================================================
"""Hashing algorithms
Hash functions take arbitrary strings as input, and produce an output
of fixed size that is dependent on the input; it should never be
possible to derive the input data given only the hash function's
output. Hash functions can be used simply as a checksum, or, in
association with a public-key algorithm, can be used to implement
digital signatures.
The hashing modules here all support the interface described in PEP
247, "API for Cryptographic Hash Functions".
Submodules:
Crypto.Hash.HMAC RFC 2104: Keyed-Hashing for Message Authentication
Crypto.Hash.MD2
Crypto.Hash.MD4
Crypto.Hash.MD5
Crypto.Hash.RIPEMD160
Crypto.Hash.SHA
"""
__all__ = ['HMAC', 'MD2', 'MD4', 'MD5', 'RIPEMD', 'RIPEMD160', 'SHA', 'SHA256']
__revision__ = "$Id$"

@ -0,0 +1,184 @@
# -*- coding: utf-8 -*-
#
# PublicKey/RSA.py : RSA public key primitive
#
# Written in 2008 by Dwayne C. Litzenberger <dlitz@dlitz.net>
#
# ===================================================================
# The contents of this file are dedicated to the public domain. To
# the extent that dedication to the public domain is not available,
# everyone is granted a worldwide, perpetual, royalty-free,
# non-exclusive license to exercise all rights associated with the
# contents of this file for any purpose whatsoever.
# No rights are reserved.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# ===================================================================
"""RSA public-key cryptography algorithm."""
__revision__ = "$Id$"
__all__ = ['generate', 'construct', 'error']
from Crypto.Util.python_compat import *
from Crypto.PublicKey import _RSA, _slowmath, pubkey
from Crypto import Random
try:
from Crypto.PublicKey import _fastmath
except ImportError:
_fastmath = None
class _RSAobj(pubkey.pubkey):
keydata = ['n', 'e', 'd', 'p', 'q', 'u']
def __init__(self, implementation, key):
self.implementation = implementation
self.key = key
def __getattr__(self, attrname):
if attrname in self.keydata:
# For backward compatibility, allow the user to get (not set) the
# RSA key parameters directly from this object.
return getattr(self.key, attrname)
else:
raise AttributeError("%s object has no %r attribute" % (self.__class__.__name__, attrname,))
def _encrypt(self, c, K):
return (self.key._encrypt(c),)
def _decrypt(self, c):
#(ciphertext,) = c
(ciphertext,) = c[:1] # HACK - We should use the previous line
# instead, but this is more compatible and we're
# going to replace the Crypto.PublicKey API soon
# anyway.
return self.key._decrypt(ciphertext)
def _blind(self, m, r):
return self.key._blind(m, r)
def _unblind(self, m, r):
return self.key._unblind(m, r)
def _sign(self, m, K=None):
return (self.key._sign(m),)
def _verify(self, m, sig):
#(s,) = sig
(s,) = sig[:1] # HACK - We should use the previous line instead, but
# this is more compatible and we're going to replace
# the Crypto.PublicKey API soon anyway.
return self.key._verify(m, s)
def has_private(self):
return self.key.has_private()
def size(self):
return self.key.size()
def can_blind(self):
return True
def can_encrypt(self):
return True
def can_sign(self):
return True
def publickey(self):
return self.implementation.construct((self.key.n, self.key.e))
def __getstate__(self):
d = {}
for k in self.keydata:
try:
d[k] = getattr(self.key, k)
except AttributeError:
pass
return d
def __setstate__(self, d):
if not hasattr(self, 'implementation'):
self.implementation = RSAImplementation()
t = []
for k in self.keydata:
if not d.has_key(k):
break
t.append(d[k])
self.key = self.implementation._math.rsa_construct(*tuple(t))
def __repr__(self):
attrs = []
for k in self.keydata:
if k == 'n':
attrs.append("n(%d)" % (self.size()+1,))
elif hasattr(self.key, k):
attrs.append(k)
if self.has_private():
attrs.append("private")
return "<%s @0x%x %s>" % (self.__class__.__name__, id(self), ",".join(attrs))
class RSAImplementation(object):
def __init__(self, **kwargs):
# 'use_fast_math' parameter:
# None (default) - Use fast math if available; Use slow math if not.
# True - Use fast math, and raise RuntimeError if it's not available.
# False - Use slow math.
use_fast_math = kwargs.get('use_fast_math', None)
if use_fast_math is None: # Automatic
if _fastmath is not None:
self._math = _fastmath
else:
self._math = _slowmath
elif use_fast_math: # Explicitly select fast math
if _fastmath is not None:
self._math = _fastmath
else:
raise RuntimeError("fast math module not available")
else: # Explicitly select slow math
self._math = _slowmath
self.error = self._math.error
# 'default_randfunc' parameter:
# None (default) - use Random.new().read
# not None - use the specified function
self._default_randfunc = kwargs.get('default_randfunc', None)
self._current_randfunc = None
def _get_randfunc(self, randfunc):
if randfunc is not None:
return randfunc
elif self._current_randfunc is None:
self._current_randfunc = Random.new().read
return self._current_randfunc
def generate(self, bits, randfunc=None, progress_func=None):
rf = self._get_randfunc(randfunc)
obj = _RSA.generate_py(bits, rf, progress_func) # TODO: Don't use legacy _RSA module
key = self._math.rsa_construct(obj.n, obj.e, obj.d, obj.p, obj.q, obj.u)
return _RSAobj(self, key)
def construct(self, tup):
key = self._math.rsa_construct(*tup)
return _RSAobj(self, key)
_impl = RSAImplementation()
generate = _impl.generate
construct = _impl.construct
error = _impl.error
# vim:set ts=4 sw=4 sts=4 expandtab:

@ -0,0 +1,95 @@
#
# RSA.py : RSA encryption/decryption
#
# Part of the Python Cryptography Toolkit
#
# Written by Andrew Kuchling, Paul Swartz, and others
#
# ===================================================================
# The contents of this file are dedicated to the public domain. To
# the extent that dedication to the public domain is not available,
# everyone is granted a worldwide, perpetual, royalty-free,
# non-exclusive license to exercise all rights associated with the
# contents of this file for any purpose whatsoever.
# No rights are reserved.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# ===================================================================
#
__revision__ = "$Id$"
from Crypto.PublicKey import pubkey
from Crypto.Util import number
def generate_py(bits, randfunc, progress_func=None):
"""generate(bits:int, randfunc:callable, progress_func:callable)
Generate an RSA key of length 'bits', using 'randfunc' to get
random data and 'progress_func', if present, to display
the progress of the key generation.
"""
obj=RSAobj()
obj.e = 65537L
# Generate the prime factors of n
if progress_func:
progress_func('p,q\n')
p = q = 1L
while number.size(p*q) < bits:
# Note that q might be one bit longer than p if somebody specifies an odd
# number of bits for the key. (Why would anyone do that? You don't get
# more security.)
#
# Note also that we ensure that e is coprime to (p-1) and (q-1).
# This is needed for encryption to work properly, according to the 1997
# paper by Robert D. Silverman of RSA Labs, "Fast generation of random,
# strong RSA primes", available at
# http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.17.2713&rep=rep1&type=pdf
# Since e=65537 is prime, it is sufficient to check that e divides
# neither (p-1) nor (q-1).
p = 1L
while (p - 1) % obj.e == 0:
if progress_func:
progress_func('p\n')
p = pubkey.getPrime(bits/2, randfunc)
q = 1L
while (q - 1) % obj.e == 0:
if progress_func:
progress_func('q\n')
q = pubkey.getPrime(bits - (bits/2), randfunc)
# p shall be smaller than q (for calc of u)
if p > q:
(p, q)=(q, p)
obj.p = p
obj.q = q
if progress_func:
progress_func('u\n')
obj.u = pubkey.inverse(obj.p, obj.q)
obj.n = obj.p*obj.q
if progress_func:
progress_func('d\n')
obj.d=pubkey.inverse(obj.e, (obj.p-1)*(obj.q-1))
assert bits <= 1+obj.size(), "Generated key is too small"
return obj
class RSAobj(pubkey.pubkey):
def size(self):
"""size() : int
Return the maximum number of bits that can be handled by this key.
"""
return number.size(self.n) - 1

@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
#
# ===================================================================
# The contents of this file are dedicated to the public domain. To
# the extent that dedication to the public domain is not available,
# everyone is granted a worldwide, perpetual, royalty-free,
# non-exclusive license to exercise all rights associated with the
# contents of this file for any purpose whatsoever.
# No rights are reserved.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# ===================================================================
"""Public-key encryption and signature algorithms.
Public-key encryption uses two different keys, one for encryption and
one for decryption. The encryption key can be made public, and the
decryption key is kept private. Many public-key algorithms can also
be used to sign messages, and some can *only* be used for signatures.
Crypto.PublicKey.DSA Digital Signature Algorithm. (Signature only)
Crypto.PublicKey.ElGamal (Signing and encryption)
Crypto.PublicKey.RSA (Signing, encryption, and blinding)
Crypto.PublicKey.qNEW (Signature only)
"""
__all__ = ['RSA', 'DSA', 'ElGamal', 'qNEW']
__revision__ = "$Id$"

@ -0,0 +1,134 @@
# -*- coding: utf-8 -*-
#
# PubKey/RSA/_slowmath.py : Pure Python implementation of the RSA portions of _fastmath
#
# Written in 2008 by Dwayne C. Litzenberger <dlitz@dlitz.net>
#
# ===================================================================
# The contents of this file are dedicated to the public domain. To
# the extent that dedication to the public domain is not available,
# everyone is granted a worldwide, perpetual, royalty-free,
# non-exclusive license to exercise all rights associated with the
# contents of this file for any purpose whatsoever.
# No rights are reserved.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# ===================================================================
"""Pure Python implementation of the RSA-related portions of Crypto.PublicKey._fastmath."""
__revision__ = "$Id$"
__all__ = ['rsa_construct']
from Crypto.Util.python_compat import *
from Crypto.Util.number import size, inverse
class error(Exception):
pass
class _RSAKey(object):
def _blind(self, m, r):
# compute r**e * m (mod n)
return m * pow(r, self.e, self.n)
def _unblind(self, m, r):
# compute m / r (mod n)
return inverse(r, self.n) * m % self.n
def _decrypt(self, c):
# compute c**d (mod n)
if not self.has_private():
raise TypeError("No private key")
return pow(c, self.d, self.n) # TODO: CRT exponentiation
def _encrypt(self, m):
# compute m**d (mod n)
return pow(m, self.e, self.n)
def _sign(self, m): # alias for _decrypt
if not self.has_private():
raise TypeError("No private key")
return self._decrypt(m)
def _verify(self, m, sig):
return self._encrypt(sig) == m
def has_private(self):
return hasattr(self, 'd')
def size(self):
"""Return the maximum number of bits that can be encrypted"""
return size(self.n) - 1
def rsa_construct(n, e, d=None, p=None, q=None, u=None):
"""Construct an RSAKey object"""
assert isinstance(n, long)
assert isinstance(e, long)
assert isinstance(d, (long, type(None)))
assert isinstance(p, (long, type(None)))
assert isinstance(q, (long, type(None)))
assert isinstance(u, (long, type(None)))
obj = _RSAKey()
obj.n = n
obj.e = e
if d is not None: obj.d = d
if p is not None: obj.p = p
if q is not None: obj.q = q
if u is not None: obj.u = u
return obj
class _DSAKey(object):
def size(self):
"""Return the maximum number of bits that can be encrypted"""
return size(self.p) - 1
def has_private(self):
return hasattr(self, 'x')
def _sign(self, m, k): # alias for _decrypt
# SECURITY TODO - We _should_ be computing SHA1(m), but we don't because that's the API.
if not self.has_private():
raise TypeError("No private key")
if not (1L < k < self.q):
raise ValueError("k is not between 2 and q-1")
inv_k = inverse(k, self.q) # Compute k**-1 mod q
r = pow(self.g, k, self.p) % self.q # r = (g**k mod p) mod q
s = (inv_k * (m + self.x * r)) % self.q
return (r, s)
def _verify(self, m, r, s):
# SECURITY TODO - We _should_ be computing SHA1(m), but we don't because that's the API.
if not (0 < r < self.q) or not (0 < s < self.q):
return False
w = inverse(s, self.q)
u1 = (m*w) % self.q
u2 = (r*w) % self.q
v = (pow(self.g, u1, self.p) * pow(self.y, u2, self.p) % self.p) % self.q
return v == r
def dsa_construct(y, g, p, q, x=None):
assert isinstance(y, long)
assert isinstance(g, long)
assert isinstance(p, long)
assert isinstance(q, long)
assert isinstance(x, (long, type(None)))
obj = _DSAKey()
obj.y = y
obj.g = g
obj.p = p
obj.q = q
if x is not None: obj.x = x
return obj
# vim:set ts=4 sw=4 sts=4 expandtab:

@ -0,0 +1,192 @@
#
# pubkey.py : Internal functions for public key operations
#
# Part of the Python Cryptography Toolkit
#
# Written by Andrew Kuchling, Paul Swartz, and others
#
# ===================================================================
# The contents of this file are dedicated to the public domain. To
# the extent that dedication to the public domain is not available,
# everyone is granted a worldwide, perpetual, royalty-free,
# non-exclusive license to exercise all rights associated with the
# contents of this file for any purpose whatsoever.
# No rights are reserved.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# ===================================================================
#
__revision__ = "$Id$"
import types, warnings
from Crypto.Util.number import *
# Basic public key class
class pubkey:
def __init__(self):
pass
def __getstate__(self):
"""To keep key objects platform-independent, the key data is
converted to standard Python long integers before being
written out. It will then be reconverted as necessary on
restoration."""
d=self.__dict__
for key in self.keydata:
if d.has_key(key): d[key]=long(d[key])
return d
def __setstate__(self, d):
"""On unpickling a key object, the key data is converted to the big
number representation being used, whether that is Python long
integers, MPZ objects, or whatever."""
for key in self.keydata:
if d.has_key(key): self.__dict__[key]=bignum(d[key])
def encrypt(self, plaintext, K):
"""encrypt(plaintext:string|long, K:string|long) : tuple
Encrypt the string or integer plaintext. K is a random
parameter required by some algorithms.
"""
wasString=0
if isinstance(plaintext, types.StringType):
plaintext=bytes_to_long(plaintext) ; wasString=1
if isinstance(K, types.StringType):
K=bytes_to_long(K)
ciphertext=self._encrypt(plaintext, K)
if wasString: return tuple(map(long_to_bytes, ciphertext))
else: return ciphertext
def decrypt(self, ciphertext):
"""decrypt(ciphertext:tuple|string|long): string
Decrypt 'ciphertext' using this key.
"""
wasString=0
if not isinstance(ciphertext, types.TupleType):
ciphertext=(ciphertext,)
if isinstance(ciphertext[0], types.StringType):
ciphertext=tuple(map(bytes_to_long, ciphertext)) ; wasString=1
plaintext=self._decrypt(ciphertext)
if wasString: return long_to_bytes(plaintext)
else: return plaintext
def sign(self, M, K):
"""sign(M : string|long, K:string|long) : tuple
Return a tuple containing the signature for the message M.
K is a random parameter required by some algorithms.
"""
if (not self.has_private()):
raise TypeError('Private key not available in this object')
if isinstance(M, types.StringType): M=bytes_to_long(M)
if isinstance(K, types.StringType): K=bytes_to_long(K)
return self._sign(M, K)
def verify (self, M, signature):
"""verify(M:string|long, signature:tuple) : bool
Verify that the signature is valid for the message M;
returns true if the signature checks out.
"""
if isinstance(M, types.StringType): M=bytes_to_long(M)
return self._verify(M, signature)
# alias to compensate for the old validate() name
def validate (self, M, signature):
warnings.warn("validate() method name is obsolete; use verify()",
DeprecationWarning)
def blind(self, M, B):
"""blind(M : string|long, B : string|long) : string|long
Blind message M using blinding factor B.
"""
wasString=0
if isinstance(M, types.StringType):
M=bytes_to_long(M) ; wasString=1
if isinstance(B, types.StringType): B=bytes_to_long(B)
blindedmessage=self._blind(M, B)
if wasString: return long_to_bytes(blindedmessage)
else: return blindedmessage
def unblind(self, M, B):
"""unblind(M : string|long, B : string|long) : string|long
Unblind message M using blinding factor B.
"""
wasString=0
if isinstance(M, types.StringType):
M=bytes_to_long(M) ; wasString=1
if isinstance(B, types.StringType): B=bytes_to_long(B)
unblindedmessage=self._unblind(M, B)
if wasString: return long_to_bytes(unblindedmessage)
else: return unblindedmessage
# The following methods will usually be left alone, except for
# signature-only algorithms. They both return Boolean values
# recording whether this key's algorithm can sign and encrypt.
def can_sign (self):
"""can_sign() : bool
Return a Boolean value recording whether this algorithm can
generate signatures. (This does not imply that this
particular key object has the private information required to
to generate a signature.)
"""
return 1
def can_encrypt (self):
"""can_encrypt() : bool
Return a Boolean value recording whether this algorithm can
encrypt data. (This does not imply that this
particular key object has the private information required to
to decrypt a message.)
"""
return 1
def can_blind (self):
"""can_blind() : bool
Return a Boolean value recording whether this algorithm can
blind data. (This does not imply that this
particular key object has the private information required to
to blind a message.)
"""
return 0
# The following methods will certainly be overridden by
# subclasses.
def size (self):
"""size() : int
Return the maximum number of bits that can be handled by this key.
"""
return 0
def has_private (self):
"""has_private() : bool
Return a Boolean denoting whether the object contains
private components.
"""
return 0
def publickey (self):
"""publickey(): object
Return a new key object containing only the public information.
"""
return self
def __eq__ (self, other):
"""__eq__(other): 0, 1
Compare us to other for equality.
"""
return self.__getstate__() == other.__getstate__()
def __ne__ (self, other):
"""__ne__(other): 0, 1
Compare us to other for inequality.
"""
return not self.__eq__(other)

@ -0,0 +1,139 @@
# -*- coding: ascii -*-
#
# FortunaAccumulator.py : Fortuna's internal accumulator
#
# Written in 2008 by Dwayne C. Litzenberger <dlitz@dlitz.net>
#
# ===================================================================
# The contents of this file are dedicated to the public domain. To
# the extent that dedication to the public domain is not available,
# everyone is granted a worldwide, perpetual, royalty-free,
# non-exclusive license to exercise all rights associated with the
# contents of this file for any purpose whatsoever.
# No rights are reserved.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# ===================================================================
__revision__ = "$Id$"
from Crypto.Util.python_compat import *
from binascii import b2a_hex
import time
import warnings
from Crypto.pct_warnings import ClockRewindWarning
import SHAd256
import FortunaGenerator
class FortunaPool(object):
"""Fortuna pool type
This object acts like a hash object, with the following differences:
- It keeps a count (the .length attribute) of the number of bytes that
have been added to the pool
- It supports a .reset() method for in-place reinitialization
- The method to add bytes to the pool is .append(), not .update().
"""
digest_size = SHAd256.digest_size
def __init__(self):
self.reset()
def append(self, data):
self._h.update(data)
self.length += len(data)
def digest(self):
return self._h.digest()
def hexdigest(self):
return b2a_hex(self.digest())
def reset(self):
self._h = SHAd256.new()
self.length = 0
def which_pools(r):
"""Return a list of pools indexes (in range(32)) that are to be included during reseed number r.
According to _Practical Cryptography_, chapter 10.5.2 "Pools":
"Pool P_i is included if 2**i is a divisor of r. Thus P_0 is used
every reseed, P_1 every other reseed, P_2 every fourth reseed, etc."
"""
# This is a separate function so that it can be unit-tested.
assert r >= 1
retval = []
mask = 0
for i in range(32):
# "Pool P_i is included if 2**i is a divisor of [reseed_count]"
if (r & mask) == 0:
retval.append(i)
else:
break # optimization. once this fails, it always fails
mask = (mask << 1) | 1L
return retval
class FortunaAccumulator(object):
min_pool_size = 64 # TODO: explain why
reseed_interval = 0.100 # 100 ms TODO: explain why
def __init__(self):
self.reseed_count = 0
self.generator = FortunaGenerator.AESGenerator()
self.last_reseed = None
# Initialize 32 FortunaPool instances.
# NB: This is _not_ equivalent to [FortunaPool()]*32, which would give
# us 32 references to the _same_ FortunaPool instance (and cause the
# assertion below to fail).
self.pools = [FortunaPool() for i in range(32)] # 32 pools
assert(self.pools[0] is not self.pools[1])
def random_data(self, bytes):
current_time = time.time()
if self.last_reseed > current_time:
warnings.warn("Clock rewind detected. Resetting last_reseed.", ClockRewindWarning)
self.last_reseed = None
if (self.pools[0].length >= self.min_pool_size and
(self.last_reseed is None or
current_time > self.last_reseed + self.reseed_interval)):
self._reseed(current_time)
# The following should fail if we haven't seeded the pool yet.
return self.generator.pseudo_random_data(bytes)
def _reseed(self, current_time=None):
if current_time is None:
current_time = time.time()
seed = []
self.reseed_count += 1
self.last_reseed = current_time
for i in which_pools(self.reseed_count):
seed.append(self.pools[i].digest())
self.pools[i].reset()
seed = "".join(seed)
self.generator.reseed(seed)
def add_random_event(self, source_number, pool_number, data):
assert 1 <= len(data) <= 32
assert 0 <= source_number <= 255
assert 0 <= pool_number <= 31
self.pools[pool_number].append(chr(source_number))
self.pools[pool_number].append(chr(len(data)))
self.pools[pool_number].append(data)
# vim:set ts=4 sw=4 sts=4 expandtab:

@ -0,0 +1,128 @@
# -*- coding: ascii -*-
#
# FortunaGenerator.py : Fortuna's internal PRNG
#
# Written in 2008 by Dwayne C. Litzenberger <dlitz@dlitz.net>
#
# ===================================================================
# The contents of this file are dedicated to the public domain. To
# the extent that dedication to the public domain is not available,
# everyone is granted a worldwide, perpetual, royalty-free,
# non-exclusive license to exercise all rights associated with the
# contents of this file for any purpose whatsoever.
# No rights are reserved.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# ===================================================================
__revision__ = "$Id$"
from Crypto.Util.python_compat import *
import struct
from Crypto.Util.number import ceil_shift, exact_log2, exact_div
from Crypto.Util import Counter
from Crypto.Cipher import AES
import SHAd256
class AESGenerator(object):
"""The Fortuna "generator"
This is used internally by the Fortuna PRNG to generate arbitrary amounts
of pseudorandom data from a smaller amount of seed data.
The output is generated by running AES-256 in counter mode and re-keying
after every mebibyte (2**16 blocks) of output.
"""
block_size = AES.block_size # output block size in octets (128 bits)
key_size = 32 # key size in octets (256 bits)
# Because of the birthday paradox, we expect to find approximately one
# collision for every 2**64 blocks of output from a real random source.
# However, this code generates pseudorandom data by running AES in
# counter mode, so there will be no collisions until the counter
# (theoretically) wraps around at 2**128 blocks. Thus, in order to prevent
# Fortuna's pseudorandom output from deviating perceptibly from a true
# random source, Ferguson and Schneier specify a limit of 2**16 blocks
# without rekeying.
max_blocks_per_request = 2**16 # Allow no more than this number of blocks per _pseudo_random_data request
_four_kiblocks_of_zeros = "\0" * block_size * 4096
def __init__(self):
self.counter = Counter.new(nbits=self.block_size*8, initial_value=0, little_endian=True)
self.key = None
# Set some helper constants
self.block_size_shift = exact_log2(self.block_size)
assert (1 << self.block_size_shift) == self.block_size
self.blocks_per_key = exact_div(self.key_size, self.block_size)
assert self.key_size == self.blocks_per_key * self.block_size
self.max_bytes_per_request = self.max_blocks_per_request * self.block_size
def reseed(self, seed):
if self.key is None:
self.key = "\0" * self.key_size
self._set_key(SHAd256.new(self.key + seed).digest())
self.counter() # increment counter
assert len(self.key) == self.key_size
def pseudo_random_data(self, bytes):
assert bytes >= 0
num_full_blocks = bytes >> 20
remainder = bytes & ((1<<20)-1)
retval = []
for i in xrange(num_full_blocks):
retval.append(self._pseudo_random_data(1<<20))
retval.append(self._pseudo_random_data(remainder))
return "".join(retval)
def _set_key(self, key):
self.key = key
self._cipher = AES.new(key, AES.MODE_CTR, counter=self.counter)
def _pseudo_random_data(self, bytes):
if not (0 <= bytes <= self.max_bytes_per_request):
raise AssertionError("You cannot ask for more than 1 MiB of data per request")
num_blocks = ceil_shift(bytes, self.block_size_shift) # num_blocks = ceil(bytes / self.block_size)
# Compute the output
retval = self._generate_blocks(num_blocks)[:bytes]
# Switch to a new key to avoid later compromises of this output (i.e.
# state compromise extension attacks)
self._set_key(self._generate_blocks(self.blocks_per_key))
assert len(retval) == bytes
assert len(self.key) == self.key_size
return retval
def _generate_blocks(self, num_blocks):
if self.key is None:
raise AssertionError("generator must be seeded before use")
assert 0 <= num_blocks <= self.max_blocks_per_request
retval = []
for i in xrange(num_blocks >> 12): # xrange(num_blocks / 4096)
retval.append(self._cipher.encrypt(self._four_kiblocks_of_zeros))
remaining_bytes = (num_blocks & 4095) << self.block_size_shift # (num_blocks % 4095) * self.block_size
retval.append(self._cipher.encrypt(self._four_kiblocks_of_zeros[:remaining_bytes]))
return "".join(retval)
# vim:set ts=4 sw=4 sts=4 expandtab:

@ -0,0 +1,88 @@
# -*- coding: ascii -*-
#
# Random/Fortuna/SHAd256.py : SHA_d-256 hash function implementation
#
# Written in 2008 by Dwayne C. Litzenberger <dlitz@dlitz.net>
#
# ===================================================================
# The contents of this file are dedicated to the public domain. To
# the extent that dedication to the public domain is not available,
# everyone is granted a worldwide, perpetual, royalty-free,
# non-exclusive license to exercise all rights associated with the
# contents of this file for any purpose whatsoever.
# No rights are reserved.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# ===================================================================
"""\
SHA_d-256 hash function implementation.
This module should comply with PEP 247.
"""
__revision__ = "$Id$"
__all__ = ['new', 'digest_size']
from Crypto.Util.python_compat import *
from binascii import b2a_hex
from Crypto.Hash import SHA256
assert SHA256.digest_size == 32
class _SHAd256(object):
"""SHA-256, doubled.
Returns SHA-256(SHA-256(data)).
"""
digest_size = SHA256.digest_size
_internal = object()
def __init__(self, internal_api_check, sha256_hash_obj):
if internal_api_check is not self._internal:
raise AssertionError("Do not instantiate this class directly. Use %s.new()" % (__name__,))
self._h = sha256_hash_obj
# PEP 247 "copy" method
def copy(self):
"""Return a copy of this hashing object"""
return _SHAd256(SHAd256._internal, self._h.copy())
# PEP 247 "digest" method
def digest(self):
"""Return the hash value of this object as a binary string"""
retval = SHA256.new(self._h.digest()).digest()
assert len(retval) == 32
return retval
# PEP 247 "hexdigest" method
def hexdigest(self):
"""Return the hash value of this object as a (lowercase) hexadecimal string"""
retval = b2a_hex(self.digest())
assert len(retval) == 64
return retval
# PEP 247 "update" method
def update(self, data):
self._h.update(data)
# PEP 247 module-level "digest_size" variable
digest_size = _SHAd256.digest_size
# PEP 247 module-level "new" function
def new(data=""):
"""Return a new SHAd256 hashing object"""
return _SHAd256(_SHAd256._internal, SHA256.new(data))
# vim:set ts=4 sw=4 sts=4 expandtab:

@ -0,0 +1,40 @@
#
# Random/OSRNG/__init__.py : Platform-independent OS RNG API
#
# Written in 2008 by Dwayne C. Litzenberger <dlitz@dlitz.net>
#
# ===================================================================
# The contents of this file are dedicated to the public domain. To
# the extent that dedication to the public domain is not available,
# everyone is granted a worldwide, perpetual, royalty-free,
# non-exclusive license to exercise all rights associated with the
# contents of this file for any purpose whatsoever.
# No rights are reserved.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# ===================================================================
"""Provides a platform-independent interface to the random number generators
supplied by various operating systems."""
__revision__ = "$Id$"
import os
if os.name == 'posix':
from Crypto.Random.OSRNG.posix import new
elif os.name == 'nt':
from Crypto.Random.OSRNG.nt import new
elif hasattr(os, 'urandom'):
from Crypto.Random.OSRNG.fallback import new
else:
raise ImportError("Not implemented")
# vim:set ts=4 sw=4 sts=4 expandtab:

@ -0,0 +1,46 @@
#
# Random/OSRNG/fallback.py : Fallback entropy source for systems with os.urandom
#
# Written in 2008 by Dwayne C. Litzenberger <dlitz@dlitz.net>
#
# ===================================================================
# The contents of this file are dedicated to the public domain. To
# the extent that dedication to the public domain is not available,
# everyone is granted a worldwide, perpetual, royalty-free,
# non-exclusive license to exercise all rights associated with the
# contents of this file for any purpose whatsoever.
# No rights are reserved.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# ===================================================================
__revision__ = "$Id$"
__all__ = ['PythonOSURandomRNG']
import os
from rng_base import BaseRNG
class PythonOSURandomRNG(BaseRNG):
name = "<os.urandom>"
def __init__(self):
self._read = os.urandom
BaseRNG.__init__(self)
def _close(self):
self._read = None
def new(*args, **kwargs):
return PythonOSURandomRNG(*args, **kwargs)
# vim:set ts=4 sw=4 sts=4 expandtab:

@ -0,0 +1,74 @@
#
# Random/OSRNG/nt.py : OS entropy source for MS Windows
#
# Written in 2008 by Dwayne C. Litzenberger <dlitz@dlitz.net>
#
# ===================================================================
# The contents of this file are dedicated to the public domain. To
# the extent that dedication to the public domain is not available,
# everyone is granted a worldwide, perpetual, royalty-free,
# non-exclusive license to exercise all rights associated with the
# contents of this file for any purpose whatsoever.
# No rights are reserved.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# ===================================================================
__revision__ = "$Id$"
__all__ = ['WindowsRNG']
import winrandom
from rng_base import BaseRNG
class WindowsRNG(BaseRNG):
name = "<CryptGenRandom>"
def __init__(self):
self.__winrand = winrandom.new()
BaseRNG.__init__(self)
def flush(self):
"""Work around weakness in Windows RNG.
The CryptGenRandom mechanism in some versions of Windows allows an
attacker to learn 128 KiB of past and future output. As a workaround,
this function reads 128 KiB of 'random' data from Windows and discards
it.
For more information about the weaknesses in CryptGenRandom, see
_Cryptanalysis of the Random Number Generator of the Windows Operating
System_, by Leo Dorrendorf and Zvi Gutterman and Benny Pinkas
http://eprint.iacr.org/2007/419
"""
if self.closed:
raise ValueError("I/O operation on closed file")
data = self.__winrand.get_bytes(128*1024)
assert (len(data) == 128*1024)
BaseRNG.flush(self)
def _close(self):
self.__winrand = None
def _read(self, N):
# Unfortunately, research shows that CryptGenRandom doesn't provide
# forward secrecy and fails the next-bit test unless we apply a
# workaround, which we do here. See http://eprint.iacr.org/2007/419
# for information on the vulnerability.
self.flush()
data = self.__winrand.get_bytes(N)
self.flush()
return data
def new(*args, **kwargs):
return WindowsRNG(*args, **kwargs)
# vim:set ts=4 sw=4 sts=4 expandtab:

@ -0,0 +1,86 @@
#
# Random/OSRNG/rng_base.py : Base class for OSRNG
#
# Written in 2008 by Dwayne C. Litzenberger <dlitz@dlitz.net>
#
# ===================================================================
# The contents of this file are dedicated to the public domain. To
# the extent that dedication to the public domain is not available,
# everyone is granted a worldwide, perpetual, royalty-free,
# non-exclusive license to exercise all rights associated with the
# contents of this file for any purpose whatsoever.
# No rights are reserved.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# ===================================================================
__revision__ = "$Id$"
from Crypto.Util.python_compat import *
class BaseRNG(object):
def __init__(self):
self.closed = False
self._selftest()
def __del__(self):
self.close()
def _selftest(self):
# Test that urandom can return data
data = self.read(16)
if len(data) != 16:
raise AssertionError("read truncated")
# Test that we get different data every time (if we don't, the RNG is
# probably malfunctioning)
data2 = self.read(16)
if data == data2:
raise AssertionError("OS RNG returned duplicate data")
# PEP 343: Support for the "with" statement
def __enter__(self):
pass
def __exit__(self):
"""PEP 343 support"""
self.close()
def close(self):
if not self.closed:
self._close()
self.closed = True
def flush(self):
pass
def read(self, N=-1):
"""Return N bytes from the RNG."""
if self.closed:
raise ValueError("I/O operation on closed file")
if not isinstance(N, (long, int)):
raise TypeError("an integer is required")
if N < 0:
raise ValueError("cannot read to end of infinite stream")
elif N == 0:
return ""
data = self._read(N)
if len(data) != N:
raise AssertionError("%s produced truncated output (requested %d, got %d)" % (self.name, N, len(data)))
return data
def _close(self):
raise NotImplementedError("child class must implement this")
def _read(self, N):
raise NotImplementedError("child class must implement this")
# vim:set ts=4 sw=4 sts=4 expandtab:

@ -0,0 +1,213 @@
# -*- coding: utf-8 -*-
#
# Random/_UserFriendlyRNG.py : A user-friendly random number generator
#
# Written in 2008 by Dwayne C. Litzenberger <dlitz@dlitz.net>
#
# ===================================================================
# The contents of this file are dedicated to the public domain. To
# the extent that dedication to the public domain is not available,
# everyone is granted a worldwide, perpetual, royalty-free,
# non-exclusive license to exercise all rights associated with the
# contents of this file for any purpose whatsoever.
# No rights are reserved.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# ===================================================================
__revision__ = "$Id$"
from Crypto.Util.python_compat import *
import os
import threading
import struct
import time
from math import floor
from Crypto.Random import OSRNG
from Crypto.Random.Fortuna import FortunaAccumulator
class _EntropySource(object):
def __init__(self, accumulator, src_num):
self._fortuna = accumulator
self._src_num = src_num
self._pool_num = 0
def feed(self, data):
self._fortuna.add_random_event(self._src_num, self._pool_num, data)
self._pool_num = (self._pool_num + 1) & 31
class _EntropyCollector(object):
def __init__(self, accumulator):
self._osrng = OSRNG.new()
self._osrng_es = _EntropySource(accumulator, 255)
self._time_es = _EntropySource(accumulator, 254)
self._clock_es = _EntropySource(accumulator, 253)
def reinit(self):
# Add 256 bits to each of the 32 pools, twice. (For a total of 16384
# bits collected from the operating system.)
for i in range(2):
block = self._osrng.read(32*32)
for p in range(32):
self._osrng_es.feed(block[p*32:(p+1)*32])
block = None
self._osrng.flush()
def collect(self):
# Collect 64 bits of entropy from the operating system and feed it to Fortuna.
self._osrng_es.feed(self._osrng.read(8))
# Add the fractional part of time.time()
t = time.time()
self._time_es.feed(struct.pack("@I", int(2**30 * (t - floor(t)))))
# Add the fractional part of time.clock()
t = time.clock()
self._clock_es.feed(struct.pack("@I", int(2**30 * (t - floor(t)))))
class _UserFriendlyRNG(object):
def __init__(self):
self.closed = False
self._fa = FortunaAccumulator.FortunaAccumulator()
self._ec = _EntropyCollector(self._fa)
self.reinit()
def reinit(self):
"""Initialize the random number generator and seed it with entropy from
the operating system.
"""
self._pid = os.getpid()
self._ec.reinit()
def close(self):
self.closed = True
self._osrng = None
self._fa = None
def flush(self):
pass
def read(self, N):
"""Return N bytes from the RNG."""
if self.closed:
raise ValueError("I/O operation on closed file")
if not isinstance(N, (long, int)):
raise TypeError("an integer is required")
if N < 0:
raise ValueError("cannot read to end of infinite stream")
# Collect some entropy and feed it to Fortuna
self._ec.collect()
# Ask Fortuna to generate some bytes
retval = self._fa.random_data(N)
# Check that we haven't forked in the meantime. (If we have, we don't
# want to use the data, because it might have been duplicated in the
# parent process.
self._check_pid()
# Return the random data.
return retval
def _check_pid(self):
# Lame fork detection to remind developers to invoke Random.atfork()
# after every call to os.fork(). Note that this check is not reliable,
# since process IDs can be reused on most operating systems.
#
# You need to do Random.atfork() in the child process after every call
# to os.fork() to avoid reusing PRNG state. If you want to avoid
# leaking PRNG state to child processes (for example, if you are using
# os.setuid()) then you should also invoke Random.atfork() in the
# *parent* process.
if os.getpid() != self._pid:
raise AssertionError("PID check failed. RNG must be re-initialized after fork(). Hint: Try Random.atfork()")
class _LockingUserFriendlyRNG(_UserFriendlyRNG):
def __init__(self):
self._lock = threading.Lock()
_UserFriendlyRNG.__init__(self)
def close(self):
self._lock.acquire()
try:
return _UserFriendlyRNG.close(self)
finally:
self._lock.release()
def reinit(self):
self._lock.acquire()
try:
return _UserFriendlyRNG.reinit(self)
finally:
self._lock.release()
def read(self, bytes):
self._lock.acquire()
try:
return _UserFriendlyRNG.read(self, bytes)
finally:
self._lock.release()
class RNGFile(object):
def __init__(self, singleton):
self.closed = False
self._singleton = singleton
# PEP 343: Support for the "with" statement
def __enter__(self):
"""PEP 343 support"""
def __exit__(self):
"""PEP 343 support"""
self.close()
def close(self):
# Don't actually close the singleton, just close this RNGFile instance.
self.closed = True
self._singleton = None
def read(self, bytes):
if self.closed:
raise ValueError("I/O operation on closed file")
return self._singleton.read(bytes)
def flush(self):
if self.closed:
raise ValueError("I/O operation on closed file")
_singleton_lock = threading.Lock()
_singleton = None
def _get_singleton():
global _singleton
_singleton_lock.acquire()
try:
if _singleton is None:
_singleton = _LockingUserFriendlyRNG()
return _singleton
finally:
_singleton_lock.release()
def new():
return RNGFile(_get_singleton())
def reinit():
_get_singleton().reinit()
def get_random_bytes(n):
"""Return the specified number of cryptographically-strong random bytes."""
return _get_singleton().read(n)
# vim:set ts=4 sw=4 sts=4 expandtab:

@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
#
# Random/__init__.py : PyCrypto random number generation
#
# Written in 2008 by Dwayne C. Litzenberger <dlitz@dlitz.net>
#
# ===================================================================
# The contents of this file are dedicated to the public domain. To
# the extent that dedication to the public domain is not available,
# everyone is granted a worldwide, perpetual, royalty-free,
# non-exclusive license to exercise all rights associated with the
# contents of this file for any purpose whatsoever.
# No rights are reserved.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# ===================================================================
__revision__ = "$Id$"
__all__ = ['new']
import OSRNG
import _UserFriendlyRNG
def new(*args, **kwargs):
"""Return a file-like object that outputs cryptographically random bytes."""
return _UserFriendlyRNG.new(*args, **kwargs)
def atfork():
"""Call this whenever you call os.fork()"""
_UserFriendlyRNG.reinit()
def get_random_bytes(n):
"""Return the specified number of cryptographically-strong random bytes."""
return _UserFriendlyRNG.get_random_bytes(n)
# vim:set ts=4 sw=4 sts=4 expandtab:

@ -0,0 +1,143 @@
# -*- coding: utf-8 -*-
#
# Random/random.py : Strong alternative for the standard 'random' module
#
# Written in 2008 by Dwayne C. Litzenberger <dlitz@dlitz.net>
#
# ===================================================================
# The contents of this file are dedicated to the public domain. To
# the extent that dedication to the public domain is not available,
# everyone is granted a worldwide, perpetual, royalty-free,
# non-exclusive license to exercise all rights associated with the
# contents of this file for any purpose whatsoever.
# No rights are reserved.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# ===================================================================
"""A cryptographically strong version of Python's standard "random" module."""
__revision__ = "$Id$"
__all__ = ['StrongRandom', 'getrandbits', 'randrange', 'randint', 'choice', 'shuffle', 'sample']
from Crypto import Random
from Crypto.Util.python_compat import *
class StrongRandom(object):
def __init__(self, rng=None, randfunc=None):
if randfunc is None and rng is None:
self._randfunc = None
elif randfunc is not None and rng is None:
self._randfunc = randfunc
elif randfunc is None and rng is not None:
self._randfunc = rng.read
else:
raise ValueError("Cannot specify both 'rng' and 'randfunc'")
def getrandbits(self, k):
"""Return a python long integer with k random bits."""
if self._randfunc is None:
self._randfunc = Random.new().read
mask = (1L << k) - 1
return mask & bytes_to_long(self._randfunc(ceil_div(k, 8)))
def randrange(self, *args):
"""randrange([start,] stop[, step]):
Return a randomly-selected element from range(start, stop, step)."""
if len(args) == 3:
(start, stop, step) = args
elif len(args) == 2:
(start, stop) = args
step = 1
elif len(args) == 1:
(stop,) = args
start = 0
step = 1
else:
raise TypeError("randrange expected at most 3 arguments, got %d" % (len(args),))
if (not isinstance(start, (int, long))
or not isinstance(stop, (int, long))
or not isinstance(step, (int, long))):
raise TypeError("randrange requires integer arguments")
if step == 0:
raise ValueError("randrange step argument must not be zero")
num_choices = ceil_div(stop - start, step)
if num_choices < 0:
num_choices = 0
if num_choices < 1:
raise ValueError("empty range for randrange(%r, %r, %r)" % (start, stop, step))
# Pick a random number in the range of possible numbers
r = num_choices
while r >= num_choices:
r = self.getrandbits(size(num_choices))
return start + (step * r)
def randint(self, a, b):
"""Return a random integer N such that a <= N <= b."""
if not isinstance(a, (int, long)) or not isinstance(b, (int, long)):
raise TypeError("randint requires integer arguments")
N = self.randrange(a, b+1)
assert a <= N <= b
return N
def choice(self, seq):
"""Return a random element from a (non-empty) sequence.
If the seqence is empty, raises IndexError.
"""
if len(seq) == 0:
raise IndexError("empty sequence")
return seq[self.randrange(len(seq))]
def shuffle(self, x):
"""Shuffle the sequence in place."""
# Make a (copy) of the list of objects we want to shuffle
items = list(x)
# Choose a random item (without replacement) until all the items have been
# chosen.
for i in xrange(len(x)):
p = self.randint(len(items))
x[i] = items[p]
del items[p]
def sample(self, population, k):
"""Return a k-length list of unique elements chosen from the population sequence."""
num_choices = len(population)
if k > num_choices:
raise ValueError("sample larger than population")
retval = []
selected = {} # we emulate a set using a dict here
for i in xrange(k):
r = None
while r is None or r in selected:
r = self.randrange(num_choices)
retval.append(population[r])
selected[r] = 1
return retval
_r = StrongRandom()
getrandbits = _r.getrandbits
randrange = _r.randrange
randint = _r.randint
choice = _r.choice
shuffle = _r.shuffle
sample = _r.sample
# These are at the bottom to avoid problems with recursive imports
from Crypto.Util.number import ceil_div, bytes_to_long, long_to_bytes, size
# vim:set ts=4 sw=4 sts=4 expandtab:

@ -0,0 +1,61 @@
# -*- coding: ascii -*-
#
# Util/Counter.py : Fast counter for use with CTR-mode ciphers
#
# Written in 2008 by Dwayne C. Litzenberger <dlitz@dlitz.net>
#
# ===================================================================
# The contents of this file are dedicated to the public domain. To
# the extent that dedication to the public domain is not available,
# everyone is granted a worldwide, perpetual, royalty-free,
# non-exclusive license to exercise all rights associated with the
# contents of this file for any purpose whatsoever.
# No rights are reserved.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# ===================================================================
from Crypto.Util.python_compat import *
from Crypto.Util import _counter
import struct
# Factory function
def new(nbits, prefix="", suffix="", initial_value=1, overflow=0, little_endian=False, allow_wraparound=False, disable_shortcut=False):
# TODO: Document this
# Sanity-check the message size
(nbytes, remainder) = divmod(nbits, 8)
if remainder != 0:
# In the future, we might support arbitrary bit lengths, but for now we don't.
raise ValueError("nbits must be a multiple of 8; got %d" % (nbits,))
if nbytes < 1:
raise ValueError("nbits too small")
elif nbytes > 0xffff:
raise ValueError("nbits too large")
initval = _encode(initial_value, nbytes, little_endian)
if little_endian:
return _counter._newLE(str(prefix), str(suffix), initval, allow_wraparound=allow_wraparound, disable_shortcut=disable_shortcut)
else:
return _counter._newBE(str(prefix), str(suffix), initval, allow_wraparound=allow_wraparound, disable_shortcut=disable_shortcut)
def _encode(n, nbytes, little_endian=False):
retval = []
n = long(n)
for i in range(nbytes):
if little_endian:
retval.append(chr(n & 0xff))
else:
retval.insert(0, chr(n & 0xff))
n >>= 8
return "".join(retval)
# vim:set ts=4 sw=4 sts=4 expandtab:

@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
#
# ===================================================================
# The contents of this file are dedicated to the public domain. To
# the extent that dedication to the public domain is not available,
# everyone is granted a worldwide, perpetual, royalty-free,
# non-exclusive license to exercise all rights associated with the
# contents of this file for any purpose whatsoever.
# No rights are reserved.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# ===================================================================
"""Miscellaneous modules
Contains useful modules that don't belong into any of the
other Crypto.* subpackages.
Crypto.Util.number Number-theoretic functions (primality testing, etc.)
Crypto.Util.randpool Random number generation
Crypto.Util.RFC1751 Converts between 128-bit keys and human-readable
strings of words.
"""
__all__ = ['randpool', 'RFC1751', 'number', 'strxor']
__revision__ = "$Id$"

@ -0,0 +1,117 @@
# -*- coding: ascii -*-
#
# Util/_number_new.py : utility functions
#
# Written in 2008 by Dwayne C. Litzenberger <dlitz@dlitz.net>
#
# ===================================================================
# The contents of this file are dedicated to the public domain. To
# the extent that dedication to the public domain is not available,
# everyone is granted a worldwide, perpetual, royalty-free,
# non-exclusive license to exercise all rights associated with the
# contents of this file for any purpose whatsoever.
# No rights are reserved.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# ===================================================================
## NOTE: Do not import this module directly. Import these functions from Crypto.Util.number.
__revision__ = "$Id$"
__all__ = ['ceil_shift', 'ceil_div', 'floor_div', 'exact_log2', 'exact_div']
from Crypto.Util.python_compat import *
def ceil_shift(n, b):
"""Return ceil(n / 2**b) without performing any floating-point or division operations.
This is done by right-shifting n by b bits and incrementing the result by 1
if any '1' bits were shifted out.
"""
if not isinstance(n, (int, long)) or not isinstance(b, (int, long)):
raise TypeError("unsupported operand type(s): %r and %r" % (type(n).__name__, type(b).__name__))
assert n >= 0 and b >= 0 # I haven't tested or even thought about negative values
mask = (1L << b) - 1
if n & mask:
return (n >> b) + 1
else:
return n >> b
def ceil_div(a, b):
"""Return ceil(a / b) without performing any floating-point operations."""
if not isinstance(a, (int, long)) or not isinstance(b, (int, long)):
raise TypeError("unsupported operand type(s): %r and %r" % (type(a).__name__, type(b).__name__))
(q, r) = divmod(a, b)
if r:
return q + 1
else:
return q
def floor_div(a, b):
if not isinstance(a, (int, long)) or not isinstance(b, (int, long)):
raise TypeError("unsupported operand type(s): %r and %r" % (type(a).__name__, type(b).__name__))
(q, r) = divmod(a, b)
return q
def exact_log2(num):
"""Find and return an integer i >= 0 such that num == 2**i.
If no such integer exists, this function raises ValueError.
"""
if not isinstance(num, (int, long)):
raise TypeError("unsupported operand type: %r" % (type(num).__name__,))
n = long(num)
if n <= 0:
raise ValueError("cannot compute logarithm of non-positive number")
i = 0
while n != 0:
if (n & 1) and n != 1:
raise ValueError("No solution could be found")
i += 1
n >>= 1
i -= 1
assert num == (1L << i)
return i
def exact_div(p, d, allow_divzero=False):
"""Find and return an integer n such that p == n * d
If no such integer exists, this function raises ValueError.
Both operands must be integers.
If the second operand is zero, this function will raise ZeroDivisionError
unless allow_divzero is true (default: False).
"""
if not isinstance(p, (int, long)) or not isinstance(d, (int, long)):
raise TypeError("unsupported operand type(s): %r and %r" % (type(p).__name__, type(d).__name__))
if d == 0 and allow_divzero:
n = 0
if p != n * d:
raise ValueError("No solution could be found")
else:
(n, r) = divmod(p, d)
if r != 0:
raise ValueError("No solution could be found")
assert p == n * d
return n
# vim:set ts=4 sw=4 sts=4 expandtab:

@ -0,0 +1,250 @@
#
# number.py : Number-theoretic functions
#
# Part of the Python Cryptography Toolkit
#
# Written by Andrew M. Kuchling, Barry A. Warsaw, and others
#
# ===================================================================
# The contents of this file are dedicated to the public domain. To
# the extent that dedication to the public domain is not available,
# everyone is granted a worldwide, perpetual, royalty-free,
# non-exclusive license to exercise all rights associated with the
# contents of this file for any purpose whatsoever.
# No rights are reserved.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# ===================================================================
#
__revision__ = "$Id$"
bignum = long
try:
from Crypto.PublicKey import _fastmath
except ImportError:
_fastmath = None
# New functions
from _number_new import *
# Commented out and replaced with faster versions below
## def long2str(n):
## s=''
## while n>0:
## s=chr(n & 255)+s
## n=n>>8
## return s
## import types
## def str2long(s):
## if type(s)!=types.StringType: return s # Integers will be left alone
## return reduce(lambda x,y : x*256+ord(y), s, 0L)
def size (N):
"""size(N:long) : int
Returns the size of the number N in bits.
"""
bits, power = 0,1L
while N >= power:
bits += 1
power = power << 1
return bits
def getRandomNumber(N, randfunc=None):
"""getRandomNumber(N:int, randfunc:callable):long
Return a random N-bit number.
If randfunc is omitted, then Random.new().read is used.
NOTE: Confusingly, this function does NOT return N random bits; It returns
a random N-bit number, i.e. a random number between 2**(N-1) and (2**N)-1.
This function is for internal use only and may be renamed or removed in
the future.
"""
if randfunc is None:
_import_Random()
randfunc = Random.new().read
S = randfunc(N/8)
odd_bits = N % 8
if odd_bits != 0:
char = ord(randfunc(1)) >> (8-odd_bits)
S = chr(char) + S
value = bytes_to_long(S)
value |= 2L ** (N-1) # Ensure high bit is set
assert size(value) >= N
return value
def GCD(x,y):
"""GCD(x:long, y:long): long
Return the GCD of x and y.
"""
x = abs(x) ; y = abs(y)
while x > 0:
x, y = y % x, x
return y
def inverse(u, v):
"""inverse(u:long, u:long):long
Return the inverse of u mod v.
"""
u3, v3 = long(u), long(v)
u1, v1 = 1L, 0L
while v3 > 0:
q=u3 / v3
u1, v1 = v1, u1 - v1*q
u3, v3 = v3, u3 - v3*q
while u1<0:
u1 = u1 + v
return u1
# Given a number of bits to generate and a random generation function,
# find a prime number of the appropriate size.
def getPrime(N, randfunc=None):
"""getPrime(N:int, randfunc:callable):long
Return a random N-bit prime number.
If randfunc is omitted, then Random.new().read is used.
"""
if randfunc is None:
_import_Random()
randfunc = Random.new().read
number=getRandomNumber(N, randfunc) | 1
while (not isPrime(number, randfunc=randfunc)):
number=number+2
return number
def isPrime(N, randfunc=None):
"""isPrime(N:long, randfunc:callable):bool
Return true if N is prime.
If randfunc is omitted, then Random.new().read is used.
"""
_import_Random()
if randfunc is None:
randfunc = Random.new().read
randint = StrongRandom(randfunc=randfunc).randint
if N == 1:
return 0
if N in sieve:
return 1
for i in sieve:
if (N % i)==0:
return 0
# Use the accelerator if available
if _fastmath is not None:
return _fastmath.isPrime(N)
# Compute the highest bit that's set in N
N1 = N - 1L
n = 1L
while (n<N):
n=n<<1L
n = n >> 1L
# Rabin-Miller test
for c in sieve[:7]:
a=long(c) ; d=1L ; t=n
while (t): # Iterate over the bits in N1
x=(d*d) % N
if x==1L and d!=1L and d!=N1:
return 0 # Square root of 1 found
if N1 & t:
d=(x*a) % N
else:
d=x
t = t >> 1L
if d!=1L:
return 0
return 1
# Small primes used for checking primality; these are all the primes
# less than 256. This should be enough to eliminate most of the odd
# numbers before needing to do a Rabin-Miller test at all.
sieve=[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59,
61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127,
131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193,
197, 199, 211, 223, 227, 229, 233, 239, 241, 251]
# Improved conversion functions contributed by Barry Warsaw, after
# careful benchmarking
import struct
def long_to_bytes(n, blocksize=0):
"""long_to_bytes(n:long, blocksize:int) : string
Convert a long integer to a byte string.
If optional blocksize is given and greater than zero, pad the front of the
byte string with binary zeros so that the length is a multiple of
blocksize.
"""
# after much testing, this algorithm was deemed to be the fastest
s = ''
n = long(n)
pack = struct.pack
while n > 0:
s = pack('>I', n & 0xffffffffL) + s
n = n >> 32
# strip off leading zeros
for i in range(len(s)):
if s[i] != '\000':
break
else:
# only happens when n == 0
s = '\000'
i = 0
s = s[i:]
# add back some pad bytes. this could be done more efficiently w.r.t. the
# de-padding being done above, but sigh...
if blocksize > 0 and len(s) % blocksize:
s = (blocksize - len(s) % blocksize) * '\000' + s
return s
def bytes_to_long(s):
"""bytes_to_long(string) : long
Convert a byte string to a long integer.
This is (essentially) the inverse of long_to_bytes().
"""
acc = 0L
unpack = struct.unpack
length = len(s)
if length % 4:
extra = (4 - length % 4)
s = '\000' * extra + s
length = length + extra
for i in range(0, length, 4):
acc = (acc << 32) + unpack('>I', s[i:i+4])[0]
return acc
# For backwards compatibility...
import warnings
def long2str(n, blocksize=0):
warnings.warn("long2str() has been replaced by long_to_bytes()")
return long_to_bytes(n, blocksize)
def str2long(s):
warnings.warn("str2long() has been replaced by bytes_to_long()")
return bytes_to_long(s)
def _import_Random():
# This is called in a function instead of at the module level in order to avoid problems with recursive imports
global Random, StrongRandom
from Crypto import Random
from Crypto.Random.random import StrongRandom

@ -0,0 +1,84 @@
# -*- coding: utf-8 -*-
#
# Util/python_compat.py : Compatibility code for old versions of Python
#
# Written in 2008 by Dwayne C. Litzenberger <dlitz@dlitz.net>
#
# ===================================================================
# The contents of this file are dedicated to the public domain. To
# the extent that dedication to the public domain is not available,
# everyone is granted a worldwide, perpetual, royalty-free,
# non-exclusive license to exercise all rights associated with the
# contents of this file for any purpose whatsoever.
# No rights are reserved.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# ===================================================================
"""Compatibility code for old versions of Python
Currently, this just defines:
- True and False
- object
- isinstance
"""
__revision__ = "$Id$"
__all__ = []
import sys
import __builtin__
# 'True' and 'False' aren't defined in Python 2.1. Define them.
try:
True, False
except NameError:
(True, False) = (1, 0)
__all__ += ['True', 'False']
# New-style classes were introduced in Python 2.2. Defining "object" in Python
# 2.1 lets us use new-style classes in versions of Python that support them,
# while still maintaining backward compatibility with old-style classes
try:
object
except NameError:
class object: pass
__all__ += ['object']
# Starting with Python 2.2, isinstance allows a tuple for the second argument.
# Also, builtins like "tuple", "list", "str", "unicode", "int", and "long"
# became first-class types, rather than functions. We want to support
# constructs like:
# isinstance(x, (int, long))
# So we hack it for Python 2.1.
try:
isinstance(5, (int, long))
except TypeError:
__all__ += ['isinstance']
_builtin_type_map = {
tuple: type(()),
list: type([]),
str: type(""),
unicode: type(u""),
int: type(0),
long: type(0L),
}
def isinstance(obj, t):
if not __builtin__.isinstance(t, type(())):
# t is not a tuple
return __builtin__.isinstance(obj, _builtin_type_map.get(t, t))
else:
# t is a tuple
for typ in t:
if __builtin__.isinstance(obj, _builtin_type_map.get(typ, typ)):
return True
return False
# vim:set ts=4 sw=4 sts=4 expandtab:

@ -0,0 +1,46 @@
# -*- coding: utf-8 -*-
#
# ===================================================================
# The contents of this file are dedicated to the public domain. To
# the extent that dedication to the public domain is not available,
# everyone is granted a worldwide, perpetual, royalty-free,
# non-exclusive license to exercise all rights associated with the
# contents of this file for any purpose whatsoever.
# No rights are reserved.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# ===================================================================
"""Python Cryptography Toolkit
A collection of cryptographic modules implementing various algorithms
and protocols.
Subpackages:
Crypto.Cipher Secret-key encryption algorithms (AES, DES, ARC4)
Crypto.Hash Hashing algorithms (MD5, SHA, HMAC)
Crypto.Protocol Cryptographic protocols (Chaffing, all-or-nothing
transform). This package does not contain any
network protocols.
Crypto.PublicKey Public-key encryption and signature algorithms
(RSA, DSA)
Crypto.Util Various useful modules and functions (long-to-string
conversion, random number generation, number
theoretic functions)
"""
__all__ = ['Cipher', 'Hash', 'Protocol', 'PublicKey', 'Util']
__version__ = '2.3' # See also below and setup.py
__revision__ = "$Id$"
# New software should look at this instead of at __version__ above.
version_info = (2, 1, 0, 'final', 0) # See also above and setup.py

@ -0,0 +1,57 @@
# -*- coding: ascii -*-
#
# pct_warnings.py : PyCrypto warnings file
#
# Written in 2008 by Dwayne C. Litzenberger <dlitz@dlitz.net>
#
# ===================================================================
# The contents of this file are dedicated to the public domain. To
# the extent that dedication to the public domain is not available,
# everyone is granted a worldwide, perpetual, royalty-free,
# non-exclusive license to exercise all rights associated with the
# contents of this file for any purpose whatsoever.
# No rights are reserved.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# ===================================================================
#
# Base classes. All our warnings inherit from one of these in order to allow
# the user to specifically filter them.
#
class CryptoWarning(Warning):
"""Base class for PyCrypto warnings"""
class CryptoDeprecationWarning(DeprecationWarning, CryptoWarning):
"""Base PyCrypto DeprecationWarning class"""
class CryptoRuntimeWarning(RuntimeWarning, CryptoWarning):
"""Base PyCrypto RuntimeWarning class"""
#
# Warnings that we might actually use
#
class RandomPool_DeprecationWarning(CryptoDeprecationWarning):
"""Issued when Crypto.Util.randpool.RandomPool is instantiated."""
class ClockRewindWarning(CryptoRuntimeWarning):
"""Warning for when the system clock moves backwards."""
class GetRandomNumber_DeprecationWarning(CryptoDeprecationWarning):
"""Issued when Crypto.Util.number.getRandomNumber is invoked."""
# By default, we want this warning to be shown every time we compensate for
# clock rewinding.
import warnings as _warnings
_warnings.filterwarnings('always', category=ClockRewindWarning, append=1)
# vim:set ts=4 sw=4 sts=4 expandtab:

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

@ -0,0 +1,71 @@
Kindle_4_Mac_Tools version 1.2
Kindle_4_Mac_Tools_v1.2.zip
http://www.mediafire.com/?8nz9rkg6p9nq23r
New in this release:
- support to identify Topaz Books and print their PID
so that standard Topaz_Tools can be used later if
desired (only works with Kindle Version 1.2.0)
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 to unzip the Kindle_4_Mac_Tools_v1.2.zip
2. open the Kindle_4_Mac_Tools folder
3. 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
In Kindle for Mac:
- hit the “Home” button to go home.
- double-click on ONE of your books.
This should open the book.
Once the book you want is open
- hit the “Home” button and then exit the Kindle for Mac application
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.

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

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

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

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

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

@ -3,16 +3,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
import subasyncio
from subasyncio import Process
from scrolltextwidget import ScrolledText
class MainDialog(Tkinter.Frame):
@ -21,39 +20,32 @@ class MainDialog(Tkinter.Frame):
self.root = root
self.interval = 2000
self.p2 = None
self.status = Tkinter.Label(self, text='Extract Contents of Topaz eBook to a Directory')
self.status = Tkinter.Label(self, text='Remove Encryption from a K4PC 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='Topaz eBook input file').grid(row=0, sticky=Tkconstants.E)
self.tpzpath = Tkinter.Entry(body, width=50)
self.tpzpath.grid(row=0, column=1, sticky=sticky)
Tkinter.Label(body, text='K4PC 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.tpzpath.insert(0, cwd)
button = Tkinter.Button(body, text="...", command=self.get_tpzpath)
self.mobipath.insert(0, cwd)
button = Tkinter.Button(body, text="...", command=self.get_mobipath)
button.grid(row=0, column=2)
Tkinter.Label(body, text='Output Directory').grid(row=1, sticky=Tkconstants.E)
Tkinter.Label(body, text='Name 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)
cwd = os.getcwdu()
cwd = cwd.encode('utf-8')
self.outpath.insert(0, cwd)
self.outpath.insert(0, '')
button = Tkinter.Button(body, text="...", command=self.get_outpath)
button.grid(row=1, column=2)
Tkinter.Label(body, text='First 8 characters of PID').grid(row=3, sticky=Tkconstants.E)
self.pidnum = Tkinter.StringVar()
self.ccinfo = Tkinter.Entry(body, width=10, textvariable=self.pidnum)
self.ccinfo.grid(row=3, 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=4, column=0, columnspan=2,sticky=sticky)
self.stext.grid(row=3, column=0, columnspan=2,sticky=sticky)
self.stext.insert(Tkconstants.END,msg1)
buttons = Tkinter.Frame(self)
@ -75,9 +67,9 @@ class MainDialog(Tkinter.Frame):
if poll != None:
text = self.p2.readerr()
text += self.p2.read()
msg = text + '\n\n' + 'Files successfully extracted\n'
msg = text + '\n\n' + 'Encryption successfully removed\n'
if poll != 0:
msg = text + '\n\n' + 'Error: File Extraction Failed\n'
msg = text + '\n\n' + 'Error: Encryption Removal Failed\n'
self.showCmdOutput(msg)
self.p2 = None
self.sbotton.configure(state='normal')
@ -98,41 +90,43 @@ class MainDialog(Tkinter.Frame):
return
# run as a subprocess via pipes and collect stdout
def topazrdr(self, infile, outdir, pidnum):
def mobirdr(self, infile, outfile):
# os.putenv('PYTHONUNBUFFERED', '1')
pidoption = ' -p "' + pidnum + '" '
outoption = ' -o "' + outdir + '" '
cmdline = 'python ./lib/cmbtc_dump_nonK4PC.py -v -d ' + pidoption + outoption + '"' + infile + '"'
cmdline = 'python ./lib/k4pcdedrm.py "' + infile + '" "' + outfile + '"'
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\cmbtc_dump_nonK4PC.py -v -d ' + pidoption + outoption + '"' + infile + '"'
cmdline = 'python lib\k4pcdedrm.py "' + infile + '" "' + outfile + '"'
else :
cmdline = 'lib\cmbtc_dump_nonK4PC.py -v -d ' + pidoption + outoption + '"' + infile + '"'
cmdline = 'lib\k4pcdedrm.py "' + infile + '" "' + outfile + '"'
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_tpzpath(self):
tpzpath = tkFileDialog.askopenfilename(
parent=None, title='Select Topaz File',
defaultextension='.prc', filetypes=[('Topaz azw1', '.azw1'), ('Topaz prc', '.prc'),
def get_mobipath(self):
mobipath = tkFileDialog.askopenfilename(
parent=None, title='Select K4PC eBook File',
defaultextension='.prc', filetypes=[('Mobi eBook File', '.prc'), ('Mobi eBook File', '.azw'),('Mobi eBook File', '.mobi'),
('All Files', '.*')])
if tpzpath:
tpzpath = os.path.normpath(tpzpath)
self.tpzpath.delete(0, Tkconstants.END)
self.tpzpath.insert(0, tpzpath)
if mobipath:
mobipath = os.path.normpath(mobipath)
self.mobipath.delete(0, Tkconstants.END)
self.mobipath.insert(0, mobipath)
return
def get_outpath(self):
cwd = os.getcwdu()
cwd = cwd.encode('utf-8')
outpath = tkFileDialog.askdirectory(
parent=None, title='Directory to Extract Files into',
initialdir=cwd, initialfile=None)
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', '.*')])
if outpath:
outpath = os.path.normpath(outpath)
self.outpath.delete(0, Tkconstants.END)
@ -150,33 +144,25 @@ class MainDialog(Tkinter.Frame):
def convertit(self):
# now disable the button to prevent multiple launches
self.sbotton.configure(state='disabled')
tpzpath = self.tpzpath.get()
mobipath = self.mobipath.get()
outpath = self.outpath.get()
if not tpzpath or not os.path.exists(tpzpath):
self.status['text'] = 'Specified Topaz eBook file does not exist'
if not mobipath or not os.path.exists(mobipath):
self.status['text'] = 'Specified K4PC eBook file does not exist'
self.sbotton.configure(state='normal')
return
if not outpath:
self.status['text'] = 'No output directory specified'
self.sbotton.configure(state='normal')
return
if not os.path.exists(outpath):
os.makedirs(outpath)
pidnum = self.pidnum.get()
if not pidnum or pidnum == '':
self.status['text'] = 'You have not entered a PID '
self.status['text'] = 'No output file specified'
self.sbotton.configure(state='normal')
return
log = 'Command = "python cmbtc_dump_nonK4PC.py"\n'
log += 'Topaz Path Path = "'+ tpzpath + '"\n'
log += 'Output Directory = "' + outpath + '"\n'
log += 'First 8 chars of PID = "' + pidnum + '"\n'
log = 'Command = "python k4pcdedrm.py"\n'
log += 'K4PC Path = "'+ mobipath + '"\n'
log += 'Output File = "' + outpath + '"\n'
log += '\n\n'
log += 'Please Wait ...\n'
log += 'Please Wait ...\n\n'
log = log.encode('utf-8')
self.stext.insert(Tkconstants.END,log)
self.p2 = self.topazrdr(tpzpath, outpath, pidnum)
self.p2 = self.mobirdr(mobipath, outpath)
# python does not seem to allow you to create
# your own eventloop which every other gui does - strange
@ -188,7 +174,7 @@ class MainDialog(Tkinter.Frame):
def main(argv=None):
root = Tkinter.Tk()
root.title('Topaz eBook File Extraction')
root.title('K4PC eBook Encryption Removal')
root.resizable(True, False)
root.minsize(300, 0)
MainDialog(root).pack(fill=Tkconstants.X, expand=1)

@ -0,0 +1,9 @@
Readme.txt
- LZskindle4PC is a front end for running skindle that makes it easier for some people who do not like command lines to use it.
- skindle has completely reverse engineered how the book specific PID is generated for Kindle4PC so it can be very useful when new version of Kindle4PC are released and unswindle has not yet been updated. Unfortunately, skindle has some minor bugs that can actually result in corrupted eBooks. No one has yet tracked them down and fixed them. Until they do, use at your own risk.
- 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.

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

@ -0,0 +1,23 @@
Readme.txt
From Apprentice Alf's Blog:
When Amazon released Kindle for PC, there was a lot of interest in finding the PID used by an installed copy. Unfortunately, it turned out that Amazon had taken the opportunity to increase the complexity of the DRM applied to ebooks downloaded for use with Kindle for PC. In short, every book has its own PID.
The current best method is a script called K4PCDeDRM.py, which takes code from several previous tools and puts them together, giving a one-script solution to removing the DRM that is not dependent on the exact version of the Kindle for PC software.
To use this script requires Python Version 2.X 32 bit version. On Windows, we recommend downloading ActiveState's ActivePython for Windows (Version 2.X NOT 3.X (32bit).
We strongly recommend that at the moment people with Kindle for Windows should use K4PCDeDRM.pyw instead of any of the other methods (skindle, unswindle).
K4PCDeDRM is available in the large archive mentioned in the first post.
To run it simply completely extract the tools archive and open the Kindle_4_PC_Tools. Then double-click on K4PCDeDRM.pyw to run the gui version.
Please note, the tools archive must be on the same machine in the same account as the Kindle for PC application for things to work.
FYI:
Books downloaded to Kindle for PC are stored in a folder called “My Kindle Content” in the current users home folder.
IMPORTANT: There are two kinds of Kindle ebooks. Mobipocket and Topaz. For Topaz books its really necessary to use the Topaz specific tools mentioned in this post which not only remove the DRM but also convert the Topaz file into a format that can be converted into other formats.

@ -0,0 +1,682 @@
#!/usr/bin/env python
#
# This is a WINDOWS python script. You need a Python interpreter to run it.
# For example, ActiveState Python, which exists for windows.
#
# It can run standalone to convert K4PC files, or it can be installed as a
# plugin for Calibre (http://calibre-ebook.com/about) so that importing
# K4PC files with DRM is no londer a multi-step process.
#
# ***NOTE*** Calibre and K4PC must be installed on the same windows machine
# 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 and import that ZIP into Calibre
# using its plugin configuration GUI.
#
# Thanks to The Dark Reverser for MobiDeDrm and CMBDTC for cmbdtc_dump from
# which this script steals most unashamedly.
#
# Changelog
# 0.01 - Initial version - Utilizes skindle and CMBDTC method of obtaining
# book specific pids from K4PC books. If Calibre and K4PC are installed
# on the same windows machine, Calibre plugin functionality is once
# again restored.
"""
Comprehensive Mazama Book DRM with Topaz Cryptography V2.0
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDdBHJ4CNc6DNFCw4MRCw4SWAK6
M8hYfnNEI0yQmn5Ti+W8biT7EatpauE/5jgQMPBmdNrDr1hbHyHBSP7xeC2qlRWC
B62UCxeu/fpfnvNHDN/wPWWH4jynZ2M6cdcnE5LQ+FfeKqZn7gnG2No1U9h7oOHx
y2/pHuYme7U1TsgSjwIDAQAB
-----END PUBLIC KEY-----
"""
from __future__ import with_statement
import csv
import sys
import os
import getopt
import zlib
import binascii
from struct import pack
from struct import unpack
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
import hashlib
__version__ = '0.01'
global kindleDatabase
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):
vsn = c_uint(0)
GetVolumeInformationW(path, None, 0, byref(vsn), None, None, None, 0)
return 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()
#
# Returns the MD5 digest of "message"
#
def MD5(message):
ctx = hashlib.md5()
ctx.update(message)
return ctx.digest()
#
# Returns the MD5 digest of "message"
#
def SHA1(message):
ctx = hashlib.sha1()
ctx.update(message)
return ctx.digest()
#
# 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')
#
# 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()
items = data.split('{')
for item in items:
splito = item.split(':')
DB[splito[0]] =splito[1]
return DB
#
# Find if the original string for a hashed/encoded string is known. If so return the original string othwise return an empty string. (Totally not optimal)
#
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 name
#
# Print all the records from the kindle.info file.
#
def printKindleInfo():
for record in kindleDatabase:
name = findNameForHash(record)
if name != "" :
print (name)
print ("--------------------------\n")
else :
print ("Unknown Record")
print getKindleInfoValueForHash(record)
print "\n"
#
# 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)
return CryptUnprotectData(encryptedValue,"")
#
# 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))
#
# 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
#
# Hash the bytes in data and then encode the digest with the characters in map
#
def encodeHash(data,map):
return encode(MD5(data),map)
#
# 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
#
# 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),2):
high = map.find(data[i])
low = map.find(data[i+1])
value = (((high * 0x40) ^ 0x80) & 0xFF) + low
result += pack("B",value)
return result
#
# 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
#
# 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
#
# MobiDeDrm-0.16 Stuff
#
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)
# 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 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
#
# This class does all the heavy lifting.
#
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 = unpack('>LLLBxxx32s', data[i*0x30:i*0x30+0x30])
cookie = PC1(temp_key, cookie)
ver,flags,finalkey,expiry,expiry2 = 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 = unpack('>LLLBxxx32s', data[i*0x30:i*0x30+0x30])
cookie = PC1(temp_key, cookie)
ver,flags,finalkey,expiry,expiry2 = unpack('>LL16sLL', cookie)
if verification == ver and cksum == temp_key_sum:
found_key = finalkey
break
return found_key
def __init__(self, data_file):
self.data_file = data_file
header = data_file[0:72]
if header[0x3C:0x3C+8] != 'BOOKMOBI':
raise DrmException("invalid file format")
self.num_sections, = unpack('>H', data_file[76:78])
self.sections = []
for i in xrange(self.num_sections):
offset, a1,a2,a3,a4 = 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, = unpack('>H', sect[0x8:0x8+2])
mobi_length, = unpack('>L',sect[0x14:0x18])
mobi_version, = 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, = unpack('>H', sect[0xF2:0xF4])
print "Extra Data Flags = %d" %extra_data_flags
crypto_type, = 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)
# determine the EXTH Offset.
exth_off = unpack('>I', sect[20:24])[0] + 16 + self.sections[0][0]
# Grab the entire EXTH block and feed it to the getK4PCPids function.
exth = data_file[exth_off:self.sections[0+1][0]]
pid = getK4PCPids(exth)
# calculate the keys
drm_ptr, drm_count, drm_size, drm_flags = 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 "\nDecrypting. 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!"
print "\nPlease only use your new-found powers for good."
def getResult(self):
return self.data_file
#
# DiapDealer's stuff: Parse the EXTH header records and parse the Kindleinfo
# file to calculate the book pid.
#
def getK4PCPids(exth):
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(str(GetVolumeSerialNumber(GetSystemDirectory().split('\\')[0] + '\\')),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: " + 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 K4PC 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)
if exth_records[503] != None:
print "Pid for " + exth_records[503] + ": " + bookPID
else:
print ("Book PID: " + bookPID )
return bookPID
raise DrmException("\nCould not access K4PC data - Perhaps K4PC is not installed/configured?")
return null
if not __name__ == "__main__":
from calibre.customize import FileTypePlugin
class K4PCDeDRM(FileTypePlugin):
name = 'K4PCDeDRM' # Name of the plugin
description = 'Removes DRM from K4PC files'
supported_platforms = ['windows'] # Platforms this plugin will run on
author = 'DiapDealer' # 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
def run(self, path_to_ebook):
from calibre.gui2 import is_ok_to_use_qt
from PyQt4.Qt import QMessageBox
data_file = file(path_to_ebook, 'rb').read()
try:
unlocked_file = DrmStripper(data_file).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, "K4PCDeDRM 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 ('K4PCDeDrm v%(__version__)s '
'provided DiapDealer.' % globals())
if len(sys.argv)<3:
print "Removes DRM protection from K4PC books"
print "Usage:"
print " %s <infile> <outfile>" % sys.argv[0]
sys.exit(1)
else:
infile = sys.argv[1]
outfile = sys.argv[2]
data_file = file(infile, 'rb').read()
try:
strippedFile = DrmStripper(data_file)
file(outfile, 'wb').write(strippedFile.getResult())
except DrmException, e:
print "Error: %s" % e
sys.exit(1)
sys.exit(0)

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

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

@ -0,0 +1,41 @@
#!/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)

@ -0,0 +1,29 @@
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.

@ -0,0 +1,6 @@
- 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.

@ -0,0 +1,38 @@
The Topaz Tools only work for "Kindle for PC" books, and original standalone Kindles that have never been updated to firmware 2.5 or later, Kindle for iPhone/iPad/iPodTouch (where the PID is known) and Kindle for Mac (with the PID provided by the Kindle_4_Mac_Tools).
For Topaz:
1. Make sure you have Python 2.X installed (32 bit) and properly set as part of your SYSTEM PATH environment variable (On Windows I recommend ActivePython. See their web pages for instructions on how to install and how to properly set your PATH). On Mac OSX 10.6 everything you need is already installed.
2. Simply download the latest tools_vX.X.zip file (see the comments after the first post on this blog) and extract the entire archive. Do not move or rename anything after extracting the entire archive.
3. move to tools\Topaz_Tools\
4. If you have an old Kindle (never updated to 2.5 or later) or an iPod, iPhone, or iPad or Kindle for Mac and you know your PID then double-click on the following:
TopazExtract_iPhone_iPad_K4M.pyw
If you have Kindle for PC (and no Kindle for Mac will NOT work here) then instead double-click on the following:
TopazExtract_Kindle4PC.pyw
Hit the first “…” button to select the Topaz book with DRM that you want to convert
Hit the second “…” to select an entirely new directory to extract the many book pieces into
And add info for your PID (or extra PIDs) if needed (should not be needed for Kindle For PC).
Hit the Start button
3. Next double-click on TopazFiles2SVG.pyw
(use the “…” button to select the new directory you created from the previous step, and hit Start)
4. Finally double-click on TopazFiles2HTML.pyw
(use the “…” button to slect the new directory you created in step 2, and hit Start)
5. After all of this you should have book.html inside the directory you created with its own image directory and css style sheet. This file is created from the ocr that is done by Amazon and stored in the Topaz file. All errors belong to Amazon.
Inside of that same directory, you should have an svg directory which has an exact image of each page of the book. To see it, simply open the .xhtml page which has the embedded svg image in a good browser (Firefox, Safari, etc)
If you run into any problems and there can be problems because the format has not been completely reversed engineered, simply copy the entire contents of the Conversion Log window and paste them in a post here or on the Dark Reversers New Blog and I will find it and try to help

@ -654,12 +654,15 @@ def createDecryptedPayload(payload):
if name != "dkey" :
ext = '.dat'
if name == 'img' : ext = '.jpg'
if name == 'color' : ext = '.jpg'
for index in range (0,len(bookHeaderRecords[name])) :
fnum = "%04d" % index
fname = name + fnum + ext
destdir = payload
if name == 'img':
destdir = os.path.join(payload,'img')
if name == 'color':
destdir = os.path.join(payload,'color_img')
if name == 'page':
destdir = os.path.join(payload,'page')
if name == 'glyphs':
@ -679,6 +682,10 @@ def createDecryptedBook(outdir):
if not os.path.exists(destdir):
os.makedirs(destdir)
destdir = os.path.join(outdir,'color_img')
if not os.path.exists(destdir):
os.makedirs(destdir)
destdir = os.path.join(outdir,'page')
if not os.path.exists(destdir):
os.makedirs(destdir)

@ -360,12 +360,15 @@ def createDecryptedPayload(payload):
if name != "dkey" :
ext = '.dat'
if name == 'img' : ext = '.jpg'
if name == 'color' : ext = '.jpg'
for index in range (0,len(bookHeaderRecords[name])) :
fnum = "%04d" % index
fname = name + fnum + ext
destdir = payload
if name == 'img':
destdir = os.path.join(payload,'img')
if name == 'color':
destdir = os.path.join(payload,'color_img')
if name == 'page':
destdir = os.path.join(payload,'page')
if name == 'glyphs':
@ -385,6 +388,10 @@ def createDecryptedBook(outdir):
if not os.path.exists(destdir):
os.makedirs(destdir)
destdir = os.path.join(outdir,'color_img')
if not os.path.exists(destdir):
os.makedirs(destdir)
destdir = os.path.join(outdir,'page')
if not os.path.exists(destdir):
os.makedirs(destdir)

@ -0,0 +1,21 @@
From Apprentice Alf's Blog
Barnes & Noble/Fictionwise eReader, .pdb
Barnes & Nobles .pdb eReader format is one of the survivors from the early days of ebooks. Internally, text is marked up in PML Palm Markup Language. This makes conversion to other formats more difficult.
Barnes and Noble .pdb ebooks use a form of Social DRM which requires information on your Credit Card Number (last 8 digits only) and the Name on the Credit card to unencrypt the book that was purchased with that credit card.
There are three scripts used to decrypt and convert eReader ebooks.
The first, eReaderPDB2PML.pyw removes the DRM and extracts the PML and images from the ebook into a folder. Its then possible to use the free DropBook (available for Mac or Windows) to compile the PML back into a DRM-free eReader file.
The second script, Pml2HTML.pyw, converts the PML file extracted by the first script into xhtml, which can then be converted into your preferred ebook format with a variety of tools.
The last script is eReaderPDB2PMLZ.pyw and it removes the DRM and extracts the PML and images from the ebook into a zip archive that can be directly imported into Calibre.
All of these scripts are gui python programs. Python 2.X (32 bit) is already installed in Mac OSX. We recommend ActiveState's Active Python Version 2.X (32 bit) for Windows users.
Simply double-click to launch these applications and follow along.
Loading…
Cancel
Save