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).
This commit is contained in:
parent
ffb4d480cf
commit
fd2aabaa27
1 changed files with 119 additions and 37 deletions
|
@ -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()
|
||||
<exchange new key with client securely>
|
||||
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
|
||||
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 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 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:
|
||||
fh = open(key_path)
|
||||
key = AesKey.Read(fh.read())
|
||||
fh.close()
|
||||
return key
|
||||
return False
|
||||
|
||||
def encrypt(key, msg):
|
||||
return key.Encrypt(msg)
|
||||
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 decrypt(key, msg):
|
||||
try:
|
||||
return key.Decrypt(msg)
|
||||
except key_errors.InvalidSignatureError:
|
||||
raise errors.AnsibleError("decryption failed")
|
||||
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
|
||||
|
|
Loading…
Reference in a new issue