From fd2aabaa27258d0af832aeb120ee555b8ebed529 Mon Sep 17 00:00:00 2001 From: James Cammarata Date: Wed, 7 Aug 2013 09:12:25 -0400 Subject: [PATCH] Initial work for the AES cipher class This is based somewhat loosely on how Keyczar does things. Their implementation does things in a much more generic way to allow for more variance in how the cipher is created, but since we're only using one key type most of our values are hard-coded. They also add a header to their messages, which I am not doing (don't see the need for it currently). --- lib/ansible/utils/__init__.py | 156 ++++++++++++++++++++++++++-------- 1 file changed, 119 insertions(+), 37 deletions(-) diff --git a/lib/ansible/utils/__init__.py b/lib/ansible/utils/__init__.py index a7305b3ded5..7a28d215be2 100644 --- a/lib/ansible/utils/__init__.py +++ b/lib/ansible/utils/__init__.py @@ -40,6 +40,10 @@ import warnings import traceback import getpass +from Crypto.Cipher import +from Crypto import Random +from Crypto.Random.random import StrongRandom + VERBOSITY=0 MAX_FILE_SIZE_FOR_DIFF=1*1024*1024 @@ -61,50 +65,128 @@ try: except: pass -KEYCZAR_AVAILABLE=False -try: - import keyczar.errors as key_errors - from keyczar.keys import AesKey - KEYCZAR_AVAILABLE=True -except ImportError: - pass - ############################################################### -# abtractions around keyczar +# Abstractions around PyCrypto +############################################################### -def key_for_hostname(hostname): - # fireball mode is an implementation of ansible firing up zeromq via SSH - # to use no persistent daemons or key management +class AES256Cipher(object): + """ + Class abstraction of an AES 256 cipher. This class + also keeps track of the time since the key was last + generated, so you know when to rekey. Rekeying would + be done as follows: - if not KEYCZAR_AVAILABLE: - raise errors.AnsibleError("python-keyczar must be installed to use fireball mode") + k = AES256Cipher.gen_key() + + AES26Cipher.set_key(k) - key_path = os.path.expanduser("~/.fireball.keys") - if not os.path.exists(key_path): - os.makedirs(key_path) - key_path = os.path.expanduser("~/.fireball.keys/%s" % hostname) + From this point on the new key would be used until + the lifetime is exceeded. + """ + def __init__(self, lifetime=60*30, mode=AES.MODE_CFB): + self.lifetime = lifetime + self.mode = mode + self.set_key(self.gen_key()) - # use new AES keys every 2 hours, which means fireball must not allow running for longer either - if not os.path.exists(key_path) or (time.time() - os.path.getmtime(key_path) > 60*60*2): - key = AesKey.Generate() - fh = open(key_path, "w") - fh.write(str(key)) - fh.close() - return key - else: - fh = open(key_path) - key = AesKey.Read(fh.read()) - fh.close() - return key + def gen_key(self): + """ + Generates a 256-bit (32 byte) key to be used for the + AES block encryption. + """ + return b"".join(StrongRandom().sample(string.letters+string.digits+string.punctuation,32)) -def encrypt(key, msg): - return key.Encrypt(msg) + def set_key(self,key): + """ + Sets the internal key to the one provided and resets the + internal time to now. This key should ONLY be set to one + generated by gen_key() + """ + self.init_time = time.time() + self.key = key -def decrypt(key, msg): - try: - return key.Decrypt(msg) - except key_errors.InvalidSignatureError: - raise errors.AnsibleError("decryption failed") + def should_rekey(self): + """ + Returns true if the lifetime of the current key has + exceeded the set lifetime. + """ + if ((time.time() - self.init_time) > self.lifetime): + return True + else: + return False + + def _pad(self, msg): + """ + Adds padding to the message so that it is a full + AES block size. Used during encryption of the message. + """ + pad = AES.block_size - len(msg) % AES.block_size + return msg + pad * chr(pad) + + def _unpad(self, msg): + """ + Strips out the padding that _pad added. Used during + the decryption of the message. + """ + pad = ord(msg[-1]) + return msg[:-pad] + + def gen_sig(self, msg): + """ + Generates an HMAC-SHA1 signature for the message + """ + return hmac.new(self.key, msg, hashlib.sha1).digest() + + def validate_sig(self, msg, sig): + """ + Verifies the generated signature of the message matches + the signature provided. + """ + new_sig = self.gen_sig(msg) + return (new_sig == sig) + + def encrypt(self, msg): + """ + Encrypt the message using AES. The signature + is appended to the end of the message and is + used to verify the integrity of the IV and data. + + Returns a base64-encoded version of the following: + + rval[0:16] = initialization vector + rval[16:-20] = cipher text + rval[-20:] = signature + """ + msg = self._pad(msg) + iv = Random.new().read(AES.block_size) + cipher = AES.new(self.key, self.mode, iv) + data = iv + cipher.encrypt(msg) + sig = self.gen_sig(data) + return (data + sig).encode('base64') + + def decrypt(self, msg): + """ + Decrypt the message using AES. The signature is + used to verify the IV and data before decoding to + ensure the integrity of the message. This is an + HMAC-SHA1 hash, so it is always 20 characters + + The incoming message format (after base64 decoding) + is as follows: + + msg[0:16] = initialization vector + msg[16:-20] = cipher text + msg[-20:] = signature (HMAC-SHA1) + + Returns the plain-text of the cipher. + """ + msg = msg.decode('base64') + data = msg[0:-20] # iv + cipher text + msig = msg[-20:] # hmac-sha1 hash + if not self.validate_sig(data,msig): + raise Exception("Failed to validate the message signature") + iv = msg[:AES.block_size] + cipher = AES.new(self.key, self.mode, iv) + return self._unpad(cipher.decrypt(msg)[AES.block_size:]) ############################################################### # UTILITY FUNCTIONS FOR COMMAND LINE TOOLS