#@+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 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("", 1) #k2 = k1[1].split("") ##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 / Args: - type - string to use in header/footer - eg 'Key', 'Message' - msg - binary string to wrap """ return "\n%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("" % type, 1) k2 = k1[1].split("" % 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 ... 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