calibre/src/libprs500/ezPyCrypto.py
Kovid Goyal a1e74e3531
2008-02-28 19:36:40 +00:00

1334 lines
42 KiB
Python
Executable file

#@+leo
#@+node:0::@file easy/ezPyCrypto.py
#@+body
#@@language python
#@<< ezPyCrypto declarations >>
#@+node:1::<< ezPyCrypto declarations >>
#@+body
"""
ezPyCrypto - very simple API for military-grade cryptography
in Python.
Designed to be approachable for even total crypto newbies,
this may be the only crypto API for Python you ever need.
Features:
- Create, Import and Export public keys and public/private keypairs - easy
- Encrypt and Decrypt arbitrary-sized pieces of data, such as
strings or files
- Open up 'streams', so this object can be used as an encrypting/decrypting
filter - good for socket-based comms and crypto of large files
- Sign and Verify documents without fuss
- Create private keys with or without a passphrase
- Export private keys with a different (or no) passphrase
- Sensible defaults - no need to specify a zillion options (or any options
at all) unless you want to
- Algorithms include RSA, ElGamal, DSA, ARC2, Blowfish, CAST, DES3, IDEA and RC5
(default RSA and Blowfish)
- Choose your own public and session key sizes (or accept defaults)
Contains an easily-used yet versatile cryptography class, called
L{key}, that performs stream and block encryption.
Packaged with a suite of very simple example programs, which demonstrate
ezPyCrypto and speed learning.
ezPyCrypto requires the PyCrypto library (which I have hand-picked from
several different Python crypto APIs, since it's the only
API that doesn't lead its programmers on a wild goose chase
of 3rd party libs, or require dozens/hundreds of lines of
code just to do basic stuff, or lack in documentation.
PyCrypto is available from http://pycrypto.sf.net)
PyCrypto is a very usable and well implemented lower-level
crypto API for Python. C backends give it speed, while
well designed OO interface makes it relatively fast to learn.
Also, it compiles cleanly and smoothly on Linux and Windows
with no dramas.
But I've written this module because PyCrypto is relatively
low-level, and does present a harder learning curve for newbies.
ezPyCrypto is written by David McNab <david@freenet.org.nz>
Released under the GNU General Public License.
No warranty, yada yada
Refer to the documentation for class 'key' for more info.
"""
from pdb import set_trace as trace
import pickle
import types
import base64
import zlib
import Crypto
from Crypto.PublicKey import ElGamal, DSA, RSA
from Crypto.Util.randpool import RandomPool
from Crypto.Util.number import getPrime
from Crypto.Cipher import ARC2, Blowfish, CAST, DES3, IDEA, RC5
from Crypto.Hash import MD5
#@-body
#@-node:1::<< ezPyCrypto declarations >>
#@+others
#@+node:2::exceptions
#@+body
# Define some exceptions for the various problems that can happen
class CryptoKeyError(Exception):
"Attempt to import invalid key"
#@-body
#@-node:2::exceptions
#@+node:3::class key
#@+body
class key:
"""
This may well be the only crypto class for Python that you'll ever need.
Think of this class, and the ezPyCrypto module, as 'cryptography for
the rest of us'.
Designed to strike the optimal balance between ease of use, features
and performance.
Basic High-level methods:
- L{encString} - encrypt a string
- L{decString} - decrypt a string
- L{encStringToAscii} - encrypt a string to a printable, mailable format
- L{decStringFromAscii} - decrypt an ascii-format encrypted string
- L{signString} - produce ascii-format signature of a string
- L{verifyString} - verify a string against a signature
- L{importKey} - import public key (and possibly private key too)
- L{exportKey} - export public key only, as printable mailable string
- L{exportKeyPrivate} - same, but export private key as well
- L{makeNewKeys} - generate a new, random private/public key pair
Middle-level (stream-oriented) methods:
- L{encStart} - start a stream encryption session
- L{encNext} - encrypt another piece of data
- L{encEnd} - finalise stream encryption session
- L{decStart} - start a stream decryption session
- L{decNext} - decrypt the next piece of available data
- L{decEnd} - finalise stream decryption session
Low-level methods:
- refer to the source code
Principle of operation:
- Data is encrypted with choice of symmetric block-mode session cipher
(or default Blowfish if user doesn't care)
- CFB block chaining is used for added security - each next block's
key is affected by the previous block
- The session key and initial value (IV) are encrypted against an RSA
or ElGamal public key (user's choice, default RSA)
- Each block in the stream is prepended with a 'length' byte, indicating
how many bytes in the decrypted block are significant - needed when
total data len mod block size is non-zero
- Format of encrypted data is:
- public key len - 2 bytes, little-endian - size of public key in bytes
- public key - public key of recipient
- block cipher len - unencrypted length byte - size of block cipher in bytes
- block cipher - encrypted against public key, index into array
of session algorithms
- block key len - unencrypted length byte - size of block key in bytes
- block key - encrypted against public key
- block IV len - unencrypted length of block cipher IV - IV length in bytes
- block cipher IV - encrypted against public key, prefixed 1-byte length
- block1 len - 1 byte - number of significant chars in block1 *
- block1 data - always 8 bytes, encrypted against session key
- ...
- blockn len
- blockn data
- If last data block is of the same size as the session cipher blocksize,
a final byte 0x00 is sent.
"""
#@<< class key declarations >>
#@+node:1::<< class key declarations >>
#@+body
# Various lookup tables for encryption algorithms
_algosPub = {'ElGamal':ElGamal, 'RSA':RSA}
_algosPub1 = {ElGamal:'ElGamal', RSA:'RSA'}
_algosSes = { "ARC2":ARC2, "Blowfish":Blowfish, "CAST":CAST,
"DES3":DES3, "IDEA":IDEA, "RC5":RC5}
_algosSes1 = {'ARC2':0, 'Blowfish':1, 'CAST':2, 'DES3':3, 'IDEA':4, 'RC5':5}
_algosSes2 = [ARC2, Blowfish, CAST, DES3, IDEA, RC5]
_algosSes3 = {ARC2:'ARC2', Blowfish:'Blowfish', CAST:'CAST',
DES3:'DES3', IDEA:'IDEA', RC5:'RC5'}
# Generate IV for passphrase encryption
_passIV = "w8Z4(51fKH#p{!29Q05HWcb@K 6(1qdyv{9|4=+gvji$chw!9$38^2cyGK#;}'@DHx%3)q_skvh4#0*="
# Buffer for yet-to-be-encrypted stream data
_encBuf = ''
#@-body
#@-node:1::<< class key declarations >>
#@+others
#@+node:2::__init__
#@+body
def __init__(self, something = 512, algoPub=None, algoSess=None, **kwds):
"""Constructor. Creates a key object
This constructor, when creating the key object, does one of
two things:
1. Creates a completely new keypair, OR
2. Imports an existing keypair
Arguments:
1. If new keys are desired:
- key size in bits (int), default 512 - advise at least 1536
- algoPub - either 'RSA' or 'ElGamal' (default 'RSA')
- algoSess - one of 'ARC2', 'Blowfish', 'CAST', 'DES3', 'IDEA', 'RC5',
(default 'Blowfish')
2. If importing an existing key or keypair:
- keyobj (string) - result of a prior exportKey() call
Keywords:
- passphrase - default '':
- If creating new keypair, this passphrase is used to encrypt privkey when
exporting.
- If importing a new keypair, the passphrase is used to authenticate and
grant/deny access to private key
"""
passphrase = kwds.get('passphrase', '')
if type(something) is types.IntType:
# which public key algorithm did they choose?
if algoPub == None:
algoPub = 'RSA'
algoP = self._algosPub.get(algoPub, None)
if algoP == None:
# Whoops - don't know that one
raise Exception("AlgoPub must be one of 'ElGamel', 'RSA' or 'DSA'")
self.algoPub = algoP
self.algoPname = algoPub
# which session key algorithm?
if algoSess == None:
algoSess = 'Blowfish'
algoS = self._algosSes.get(algoSess, None)
if algoS == None:
# Whoops - don't know that session algorithm
raise Exception("AlgoSess must be one of AES/ARC2/Blowfish/CAST/DES/DES3/IDEA/RC5")
self.algoSes = algoS
self.algoSname = algoSess
# organise random data pool
self.randpool = RandomPool()
self.randfunc = self.randpool.get_bytes
# now create the keypair
self.makeNewKeys(something, passphrase=passphrase)
elif type(something) is types.StringType:
if algoPub != None:
raise Exception("Don't specify algoPub if importing a key")
if self.importKey(something, passphrase=passphrase) == False:
raise CryptoKeyError(
"Attempted to import invalid key, or passphrase is bad")
self.randpool = RandomPool()
self.randfunc = self.randpool.get_bytes
else:
raise Exception("Must pass keysize or importable keys")
#@-body
#@-node:2::__init__
#@+node:3::makeNewKeys()
#@+body
def makeNewKeys(self, keysize=512, **kwds):
"""
Creates a new keypair in cipher object, and a new session key
Arguments:
- keysize (default 512), advise at least 1536
Returns:
- None
Keywords:
- passphrase - used to secure exported private key - default '' (no passphrase)
Keypair gets stored within the key object. Refer L{exportKey},
L{exportKeyPrivate} and L{importKey}.
Generally no need to call this yourself, since the constructor
calls this in cases where you aren't instantiating with an
importable key.
"""
passphrase = kwds.get('passphrase', '')
if passphrase == None:
passphrase = ''
self.passphrase = passphrase
# set up a public key object
self.randpool.stir()
self.k = self.algoPub.generate(keysize, self.randfunc)
self.randpool.stir()
self._calcPubBlkSize()
# Generate random session key
self._genNewSessKey()
# Create session cipher object
self.randpool.stir()
#trace()
# Create a new block cipher object
self._initBlkCipher()
#@-body
#@-node:3::makeNewKeys()
#@+node:4::importKey()
#@+body
def importKey(self, keystring, **kwds):
"""
Imports a public key or private/public key pair.
(as previously exported from this object
with the L{exportKey} or L{exportKeyPrivate} methods.)
Arguments:
- keystring - a string previously imported with
L{exportKey} or L{exportKeyPrivate}
Keywords:
- passphrase - string (default '', meaning 'try to import without passphrase')
Returns:
- True if import successful, False if failed
You don't have to call this if you instantiate your key object
in 'import' mode - ie, by calling it with a previously exported key.
Note - you shouldn't give a 'passphrase' when importing a public key.
"""
passphrase = kwds.get('passphrase', '')
if passphrase == None:
passphrase = ''
try:
#k1 = keystring.split("<StartPycryptoKey>", 1)
#k2 = k1[1].split("<EndPycryptoKey>")
##print "decoding:\n", k2[0]
#k = base64.decodestring(k2[0])
#trace()
keypickle = self._unwrap("Key", keystring)
keytuple = pickle.loads(keypickle)
haspass, size, keyobj = keytuple
if haspass:
# decrypt against passphrase
blksiz = 8 # lazy of me
# create temporary symmetric cipher object for passphrase - hardwire to Blowfish
ppCipher = Blowfish.new(passphrase,
Blowfish.MODE_CFB,
self._passIV[0:blksiz])
enclen = len(keyobj)
decpriv = ''
i = 0
while i < enclen:
decbit = ppCipher.decrypt(keyobj[i:i+blksiz])
decpriv += decbit
i += blksiz
keyobj = decpriv[0:size]
self.algoPname, self.k = pickle.loads(keyobj)
self.algoPub = self._algosPub[self.algoPname]
#raise Exception("Tried to import Invalid Key")
self._calcPubBlkSize()
self.passphrase = passphrase
return True
except:
return False
#@-body
#@-node:4::importKey()
#@+node:5::exportKey()
#@+body
def exportKey(self):
"""
Exports the public key as a printable string.
Exported keys can be imported elsewhere into MyCipher instances
with the L{importKey} method.
Note that this object contains only the public key. If you want to
export the private key as well, call L{exportKeyPrivate} instaead.
Note also that the exported string is Base64-encoded, and safe for sending
in email.
Arguments:
- None
Returns:
- a base64-encoded string containing an importable key
"""
rawpub = self._rawPubKey()
expTuple = (False, None, rawpub)
expPickle = pickle.dumps(expTuple, True)
return self._wrap("Key", expPickle)
#@-body
#@-node:5::exportKey()
#@+node:6::exportKeyPrivate()
#@+body
def exportKeyPrivate(self, **kwds):
"""
Exports public/private key pair as a printable string.
This string is a binary string consisting of a pickled key object,
that can be imported elsewhere into MyCipher instances
with the L{importKey} method.
Note that this object contains the public AND PRIVATE keys.
Don't EVER email any keys you export with this function (unless you
know what you're doing, and you encrypt the exported keys against
another key). When in doubt, use L{exportKey} instead.
Keep your private keys safe at all times. You have been warned.
Note also that the exported string is Base64-encoded, and safe for sending
in email.
Arguments:
- None
Keywords:
- passphrase - default (None) to using existing passphrase. Set to '' to export
without passphrase (if this is really what you want to do!)
Returns:
- a base64-encoded string containing an importable key
"""
passphrase = kwds.get('passphrase', None)
if passphrase == None:
passphrase = self.passphrase
# exported key is a pickle of the tuple:
# (haspassphrase, keylen, keypickle)
# if using passphrase, 'keypickle' is encrypted against blowfish, and 'keylen'
# indicates the number of significant bytes.
rawpriv = pickle.dumps((self.algoPname, self.k), True)
# prepare the key tuple, depending on whether we're using passphrases
if passphrase != '':
blksiz = 8 # i'm getting lazy, assuming 8 for blowfish
# encrypt this against passphrase
ppCipher = Blowfish.new(passphrase,
Blowfish.MODE_CFB,
self._passIV[0:blksiz])
keylen = len(rawpriv)
extras = (blksiz - (keylen % blksiz)) % blksiz
rawpriv += self.randfunc(extras) # padd with random bytes
newlen = len(rawpriv)
encpriv = ''
#print "newlen = %d" % newlen
#trace()
i = 0
while i < newlen:
rawbit = rawpriv[i:i+blksiz]
encbit = ppCipher.encrypt(rawpriv[i:i+blksiz])
#print "i=%d rawbit len=%d, encbit len=%d" % (i, len(rawbit), len(encbit))
encpriv += encbit
i += blksiz
#print "keylen=%d, newlen=%d, len(encpriv)=%d" % (keylen, newlen, len(encpriv))
#trace()
keytuple = (True, keylen, encpriv)
else:
keytuple = (False, None, rawpriv)
# prepare final pickle, base64 encode, wrap
keypickle = pickle.dumps(keytuple, True)
return self._wrap("Key", keypickle)
#@-body
#@-node:6::exportKeyPrivate()
#@+node:7::encString()
#@+body
def encString(self, raw):
"""
Encrypt a string of data
High-level func. encrypts an entire string of data, returning the encrypted
string as binary.
Arguments:
- raw string to encrypt
Returns:
- encrypted string as binary
Note - the encrypted string can be stored in files, but I'd suggest
not emailing them - use L{encStringToAscii} instead. The sole advantage
of this method is that it produces more compact data, and works a bit faster.
"""
# All the work gets done by the stream level
self.encStart()
# carve up into segments, because Python gets really slow
# at manipulating large strings
size = len(raw)
bits = []
pos = 0
chunklen = 1024
while pos < size:
bits.append(self.encNext(raw[pos:pos+chunklen]))
pos += chunklen
bits.append(self.encEnd())
return "".join(bits)
#@-body
#@-node:7::encString()
#@+node:8::encStringToAscii()
#@+body
def encStringToAscii(self, raw):
"""
Encrypts a string of data to printable ASCII format
Use this method instead of L{encString}, unless size and speed are
major issues.
This method returns encrypted data in bracketed base64 format,
safe for sending in email.
Arguments:
- raw - string to encrypt
Returns:
- enc - encrypted string, text-wrapped and Base-64 encoded, safe for
mailing.
There's an overhead with base64-encoding. It costs size, bandwidth and
speed. Unless you need ascii-safety, use encString() instead.
"""
enc = self.encString(raw)
return self._wrap("Message", enc)
#@-body
#@-node:8::encStringToAscii()
#@+node:9::decString()
#@+body
def decString(self, enc):
"""
Decrypts a previously encrypted string.
Arguments:
- enc - string, previously encrypted in binary mode with encString
Returns:
- dec - raw decrypted string
"""
chunklen = 1024
size = len(enc)
bits = []
pos = 0
self.decStart()
# carve up into small chunks so we don't get any order n^2 on large strings
while pos < size:
bits.append(self.decNext(enc[pos:pos+chunklen]))
pos += chunklen
self.decEnd()
dec = "".join(bits)
return dec
#@-body
#@-node:9::decString()
#@+node:10::decStringFromAscii()
#@+body
def decStringFromAscii(self, enc):
"""
Decrypts a previously encrypted string in ASCII (base64)
format, as created by encryptAscii()
Arguments:
- enc - ascii-encrypted string, as previously encrypted with
encStringToAscii()
Returns:
- dec - decrypted string
May generate an exception if the public key of the encrypted string
doesn't match the public/private keypair in this key object.
To work around this problem, either instantiate a key object with
the saved keypair, or use the importKey() function.
Exception will also occur if this object is not holding a private key
(which can happen if you import a key which was previously exported
via exportKey(). If you get this problem, use exportKeyPrivate() instead
to export your keypair.
"""
#trace()
wrapped = self._unwrap("Message", enc)
return self.decString(wrapped)
#@-body
#@-node:10::decStringFromAscii()
#@+node:11::signString()
#@+body
def signString(self, raw):
"""
Sign a string using private key
Arguments:
- raw - string to be signed
Returns:
- wrapped, base-64 encoded string of signature
Note - private key must already be present in the key object.
Call L{importKey} for the right private key first if needed.
"""
# hash the key with MD5
m = MD5.new()
m.update(raw)
d = m.digest()
#print "sign: digest"
#print repr(d)
# sign the hash with our current public key cipher
self.randpool.stir()
k = getPrime(128, self.randfunc)
self.randpool.stir()
s = self.k.sign(d, k)
# now wrap into a tuple with the public key cipher
tup = (self.algoPname, s)
# and pickle it
p = pickle.dumps(tup, True)
# lastly, wrap it into our base64
w = self._wrap("Signature", p)
return w
#@-body
#@-node:11::signString()
#@+node:12::verifyString()
#@+body
def verifyString(self, raw, signature):
"""
Verifies a string against a signature.
Object must first have the correct public key loaded. (see
L{importKey}). An exception will occur if this is not the case.
Arguments:
- raw - string to be verified
- signature - as produced when key is signed with L{signString}
Returns:
- True if signature is authentic, or False if not
"""
# unrwap the signature to a pickled tuple
p = self._unwrap("Signature", signature)
# unpickle
algoname, rawsig = pickle.loads(p)
# ensure we've got the right algorithm
if algoname != self.algoPname:
return False # wrong algorithm - automatic fail
# hash the string
m = MD5.new()
m.update(raw)
d = m.digest()
#print "verify: digest"
#print repr(d)
# now verify the hash against sig
if self.k.verify(d, rawsig):
return True # signature valid, or very clever forgery
else:
return False # sorry
#@-body
#@-node:12::verifyString()
#@+node:13::test()
#@+body
def test(self, raw):
"""
Encrypts, then decrypts a string. What you get back should
be the same as what you put in.
This is totally useless - it just gives a way to test if this API
is doing what it should.
"""
enc = self.encString(raw)
dec = self.decString(enc)
return dec
#@-body
#@-node:13::test()
#@+node:14::testAscii()
#@+body
def testAscii(self, raw):
"""
Encrypts, then decrypts a string. What you get back should
be the same as what you put in.
This is totally useless - it just gives a way to test if this API
is doing what it should.
"""
enc = self.encStringToAscii(raw)
dec = self.decStringFromAscii(enc)
return dec
#@-body
#@-node:14::testAscii()
#@+node:15::Stream Methods
#@+body
# ---------------------------------------------
#
# These methods provide stream-level encryption
#
# ---------------------------------------------
#@-body
#@+node:1::encStart()
#@+body
def encStart(self):
"""
Starts a stream encryption session
Sets up internal buffers for accepting ad-hoc data.
No arguments needed, nothing returned.
"""
# Create a header block of segments, each segment is
# encrypted against recipient's public key, to enable
# recipient to decrypt the rest of the stream.
# format of header block is:
# - recipient public key
# - stream algorithm id
# - stream session key
# - stream cipher initial value
# Take algorithm index and pad it to the max length
# stick in pubkey
pubkey = self._rawPubKey()
pubkeyLen = len(pubkey)
self._tstSessKey0 = ''
self._tstSessKey1 = ''
self._tstIV0 = ''
self._tstIV1 = ''
self._tstBlk0 = ''
self._tstBlk1 = ''
#print "pub key len=%d" % pubkeyLen
len0 = pubkeyLen % 256
len1 = pubkeyLen / 256
# Create algorithms info blk. Structure is:
# 1byte - index into session ciphers table
# 2bytes - session key len, LSB first
# 1byte - session IV len, LSB first
while 1:
self._encHdrs = chr(len0) + chr(len1) + pubkey
# add algorithms index
algInfo = chr(self._algosSes2.index(self.algoSes))
# Create new session key
self._genNewSessKey()
# add session key length
sessKeyLen = len(self.sessKey)
sessKeyLenL = sessKeyLen % 256
sessKeyLenH = sessKeyLen / 256
algInfo += chr(sessKeyLenL) + chr(sessKeyLenH)
# add session IV length
sessIVLen = len(self.sessIV)
algInfo += chr(sessIVLen)
#alg += self.randfunc(self.pubBlkSize - 1) # add random chaff
#encAlgNum = self._encRawPub(alg)
encAlgEnc = self._encRawPub(self._padToPubBlkSize(algInfo))
if encAlgEnc == None:
continue
#encAlgLen = len(encAlgNum)
#self._encHdrs += chr(encAlgLen) + encAlgNum
self._encHdrs += encAlgEnc
# ensure we can encrypt session key in one hit
if len(self.sessKey) > self.pubBlkSize:
raise Exception(
"encStart: you need a bigger public key length")
# encrypt and add session key
sKeyEnc = self._encRawPub(self._padToPubBlkSize(self.sessKey))
if sKeyEnc == None:
continue
# sKeyLen = len(sKeyEnc)
# self._encHdrs += chr(sKeyLen) + sKeyEnc
self._encHdrs += sKeyEnc
# encrypt and add session cipher initial value
sCipherInit = self._encRawPub(self._padToPubBlkSize(self.sessIV))
if sCipherInit == None:
continue
# sCipherIVLen = len(sCipherInit)
# self._encHdrs += chr(sCipherIVLen) + sCipherInit
self._encHdrs += sCipherInit
self._tstSessKey0 = self.sessKey
self._tstIV0 = self.sessIV
# Create a new block cipher object
self._initBlkCipher()
# ready to go!
self._encBuf = ''
# success
break
#@-body
#@-node:1::encStart()
#@+node:2::encNext()
#@+body
def encNext(self, raw=''):
"""
Encrypt the next piece of data in a stream.
Arguments:
- raw - raw piece of data to encrypt
Returns - one of:
- '' - not enough data to encrypt yet - stored for later
- encdata - string of encrypted data
"""
if raw == '':
return ''
# grab any headers
enc = self._encHdrs
self._encHdrs = ''
# add given string to our yet-to-be-encrypted buffer
self._encBuf += raw
# Loop on data, breaking it up and encrypting it in blocks. Don't
# touch the last (n mod b) bytes in buffer, where n is total size and
# b is blocksize
size = len(self._encBuf)
next = 0
while next <= size - self.sesBlkSize: # skip trailing bytes for now
# extract next block
blk = self._encBuf[next:next+self.sesBlkSize]
if self._tstBlk0 == '':
self._tstBlk0 = blk
# encrypt block against session key
encpart = self.blkCipher.encrypt(blk)
# add length byte and crypted block to internal buffer
enc += chr(self.sesBlkSize) + encpart
next += self.sesBlkSize
# ditch what we've consumed from buffer
self._encBuf = self._encBuf[next:]
# return whatever we've encrypted so far
return enc
#@-body
#@-node:2::encNext()
#@+node:3::encEnd()
#@+body
def encEnd(self):
"""
Called to terminate a stream session.
Encrypts any remaining data in buffer.
Arguments:
- None
Returns - one of:
- last block of data, as a string
"""
buf = ''
if self._encBuf == '':
# no trailing data - pass back empty packet
return chr(0)
# break up remaining data into packets, and encrypt
while len(self._encBuf) > 0:
# extract session blocksize worth of data from buf
blk = self._encBuf[0:self.sesBlkSize]
self._encBuf = self._encBuf[self.sesBlkSize:]
blklen = len(blk)
# pad if needed
if blklen < self.sesBlkSize:
blk += self.randfunc(self.sesBlkSize - blklen)
# encrypt against session key, and add
buf += chr(blklen)
buf += self.blkCipher.encrypt(blk)
# clean up and get out
return buf
#@-body
#@-node:3::encEnd()
#@+node:4::decStart()
#@+body
def decStart(self):
"""
Start a stream decryption session.
Call this method first, then feed in pieces of stream data into decNext until
there's no more data to decrypt
Arguments:
- None
Returns:
- None
"""
# Start with fresh buffer and initial state
self._decBuf = ''
self._decState = 'p'
self._decEmpty = False
self._tstSessKey1 = ''
self._tstIV1 = ''
self._tstBlk1 = ''
# states - 'p'->awaiting public key
# 'c'->awaiting cipher index
# 'k'->awaiting session key
# 'i'->awaiting cipher initial data
# 'd'->awaiting data block
#@-body
#@-node:4::decStart()
#@+node:5::decNext()
#@+body
def decNext(self, chunk):
"""
Decrypt the next piece of incoming stream data.
Arguments:
- chunk - some more of the encrypted stream
Returns (depending on state)
- '' - no more decrypted data available just yet
- data - the next available piece of decrypted data
- None - session is complete - no more data available
"""
if self._decEmpty:
return None
# add chunk to our buffer
self._decBuf += chunk
# bail out if nothing to do
chunklen = len(self._decBuf)
if chunklen < 2:
return ''
# start with empty decryption buffer
decData = ''
# loop around processing as much data as we can
#print "decNext: started"
while 1:
if self._decState == 'p':
size = ord(self._decBuf[0]) + 256 * ord(self._decBuf[1])
if chunklen < size + 2:
# don't have full pubkey yet
return ''
else:
pubkey = self._decBuf[2:size+2]
if not self._testPubKey(pubkey):
raise Exception("Can't decrypt - public key mismatch")
self._decBuf = self._decBuf[size+2:]
self._decState = 'c'
continue
if self._decState == 'd':
#trace()
# awaiting next data chunk
sizeReqd = self.sesBlkSize + 1
size = len(self._decBuf)
if size < sizeReqd:
return decData
nbytes = ord(self._decBuf[0])
if nbytes == 0:
self._decEmpty = True
return None
blk = self._decBuf[1:sizeReqd]
self._decBuf = self._decBuf[sizeReqd:]
decBlk = self.blkCipher.decrypt(blk)
if self._tstBlk1 == '':
self._tstBlk1 = decBlk
decBlk = decBlk[0:nbytes]
decData += decBlk
if nbytes < self.sesBlkSize:
self._decEmpty = True
return decData
continue
if len(self._decBuf) < 2:
return decData
sizeReqd = ord(self._decBuf[0]) + 256 * ord(self._decBuf[1]) + 2
size = len(self._decBuf)
# bail if we have insufficient data
if size < sizeReqd:
return decData
# extract length byte plus block
#blksize = sizeReqd - 1
#blk = self._decBuf[1:sizeReqd]
#self._decBuf = self._decBuf[sizeReqd:]
blk = self._decBuf[0:sizeReqd]
self._decBuf = self._decBuf[sizeReqd:]
# state-dependent processing
if self._decState == 'c':
#print "decrypting cipher info"
# awaiting cipher info
blk = self._decRawPub(blk)
# session cipher index
c = ord(blk[0])
self.algoSes = self._algosSes2[c]
# session key len
self._tmpSessKeyLen = ord(blk[1]) + 256 * ord(blk[2])
# session IV len
self._tmpSessIVLen = ord(blk[3])
# ignore the rest - it's just chaff
self._decState = 'k'
continue
elif self._decState == 'k':
# awaiting session key
#print "decrypting session key"
blk = self._decRawPub(blk)
self.sessKey = blk[0:self._tmpSessKeyLen]
self._tstSessKey1 = self.sessKey
self._decState = 'i'
continue
elif self._decState == 'i':
# awaiting cipher start value
#print "decrypting IV"
blk = self._decRawPub(blk)
self.sessIV = blk[0:self._tmpSessIVLen]
self._tstIV1 = self.sessIV
# Create cipher object, now we have what we need
self.blkCipher = self.algoSes.new(self.sessKey,
getattr(self.algoSes, "MODE_CFB"),
self.sessIV)
self._calcSesBlkSize()
self._decState = 'd'
continue
else:
raise Exception(
"decNext: strange state '%s'" % self._decState[0])
#@-body
#@-node:5::decNext()
#@+node:6::decEnd()
#@+body
def decEnd(self):
"""
Ends a stream decryption session.
"""
# nothing really to do here - decNext() has taken care of it
# just reset internal state
self._decBuf = ''
self._decState = 'c'
#@-body
#@-node:6::decEnd()
#@-node:15::Stream Methods
#@+node:16::Low Level
#@+node:1::_wrap()
#@+body
def _wrap(self, type, msg):
"""
Encodes message as base64 and wraps with <StartPyCryptoname>/<EndPycryptoname>
Args:
- type - string to use in header/footer - eg 'Key', 'Message'
- msg - binary string to wrap
"""
return "<StartPycrypto%s>\n%s<EndPycrypto%s>\n" \
% (type, base64.encodestring(msg), type)
#@-body
#@-node:1::_wrap()
#@+node:2::_unwrap()
#@+body
def _unwrap(self, type, msg):
"""
Unwraps a previously _wrap()'ed message.
"""
try:
#trace()
k1 = msg.split("<StartPycrypto%s>" % type, 1)
k2 = k1[1].split("<EndPycrypto%s>" % type)
k = k2[0]
#print "raw = "
#print k
bin = base64.decodestring(k)
return bin
except:
raise Exception("Tried to import Invalid %s" % type)
self._calcBlkSize()
#@-body
#@-node:2::_unwrap()
#@+node:3::_calcPubBlkSize()
#@+body
def _calcPubBlkSize(self):
"""
Determine size of public key
"""
self.pubBlkSize = (self.k.size() - 7) / 8
#@-body
#@-node:3::_calcPubBlkSize()
#@+node:4::_encRawPub()
#@+body
def _encRawPub(self, raw):
"""
Encrypt a small raw string using the public key
algorithm. Input must not exceed the allowable
block size.
Arguments:
- raw - small raw bit of string to encrypt
Returns:
- binary representation of encrypted chunk, or None if verify failed
"""
if len(raw) > self.pubBlkSize:
raise Exception(
"_encraw: max len %d, passed %d bytes" % (self.pubBlkSize, len(raw)))
self.randpool.stir()
k = getPrime(128, self.randfunc)
s = self.k.encrypt(raw, k)
#d = self.k.decrypt(s)
#if d != raw:
# #print "_encRawPub: decrypt verify fail"
# return None
#trace()
# format this tuple into <len><nitems><item1len><item1bytes><item2len><item2bytes>...
enc = chr(len(s))
for item in s:
itemLen = len(item)
itemLenL = itemLen % 256
itemLenH = itemLen / 256
#enc += chr(len(item))
enc += chr(itemLenL) + chr(itemLenH)
enc += item
encLen = len(enc)
encLenL = encLen % 256
encLenH = encLen / 256
#enc = chr(len(enc)) + enc
enc = chr(encLenL) + chr(encLenH) + enc
#d = self._decRawPub(enc)
#if d != raw:
# print "panic:_encRawPub: decrypt verify fail!"
return enc
#@-body
#@-node:4::_encRawPub()
#@+node:5::_decRawPub()
#@+body
def _decRawPub(self, enc):
"""
Decrypt a public-key encrypted block, and return the decrypted string
Arguments:
- enc - the encrypted string, in the format as created by _encRawPub()
Returns:
- decrypted block
"""
#trace()
blklen = ord(enc[0]) + 256 * ord(enc[1])
nparts = ord(enc[2])
enc = enc[3:]
if blklen != len(enc)+1:
raise Exception(
"_decRawPub: bad block length %d, should be %d" % (len(enc), blklen))
parts = []
for i in range(nparts):
partlen = ord(enc[0]) + 256 * ord(enc[1])
part = enc[2:partlen+2]
enc = enc[partlen+2:]
parts.append(part)
partsTuple = tuple(parts)
dec = self.k.decrypt(partsTuple)
return dec
#@-body
#@-node:5::_decRawPub()
#@+node:6::_initBlkCipher()
#@+body
def _initBlkCipher(self):
"""
Create a new block cipher object, set up with a new session key
and IV
"""
self.blkCipher = self.algoSes.new(self.sessKey,
getattr(self.algoSes, "MODE_CFB"),
self.sessIV)
self._calcSesBlkSize()
#@-body
#@-node:6::_initBlkCipher()
#@+node:7::_calcSesBlkSize()
#@+body
def _calcSesBlkSize(self):
"""
Determine size of session blocks
"""
self.sesBlkSize = (self.blkCipher.block_size)
#@-body
#@-node:7::_calcSesBlkSize()
#@+node:8::_testPubKey()
#@+body
def _testPubKey(self, k):
"""
Checks if binary-encoded key matches this object's pubkey
"""
if k == self._rawPubKey():
return True
else:
return False
#@-body
#@-node:8::_testPubKey()
#@+node:9::_rawPubKey()
#@+body
def _rawPubKey(self):
"""
Returns a binary-encoded string of public key
"""
return pickle.dumps((self.algoPname, self.k.publickey()), True)
#@-body
#@-node:9::_rawPubKey()
#@+node:10::_padToPubBlkSize()
#@+body
def _padToPubBlkSize(self, raw):
"""
padToPubBlkSize - pad a string to max size encryptable by public key
Defence against factoring attacks that can uplift a session key when
that key is encrypted by itself against public key
Arguments:
- raw - string to pad with random bytes
returns:
- padded string. Note - it is the responsibility of the decryption
code to know how much of the string to extract once decrypted.
"""
rawlen = len(raw)
extras = self.randfunc(self.pubBlkSize - rawlen)
#print "padToPubBlkSize: len=%d, added %d bytes of chaff :)" \
# % (rawlen, len(extras))
return raw + extras
#@-body
#@-node:10::_padToPubBlkSize()
#@+node:11::_genNewSessKey()
#@+body
def _genNewSessKey(self):
"""
Generate a new random session key
"""
self.randpool.stir()
self.sessKey = self.randfunc(32)
self.randpool.stir()
self.sessIV = self.randfunc(8)
#@-body
#@-node:11::_genNewSessKey()
#@-node:16::Low Level
#@-others
#@-body
#@-node:3::class key
#@-others
#@-body
#@-node:0::@file easy/ezPyCrypto.py
#@-leo