2014-02-11 17:03:11 +00:00
|
|
|
# (c) 2014, James Tanner <tanner.jc@gmail.com>
|
|
|
|
#
|
|
|
|
# Ansible is free software: you can redistribute it and/or modify
|
|
|
|
# it under the terms of the GNU General Public License as published by
|
|
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
|
|
# (at your option) any later version.
|
|
|
|
#
|
|
|
|
# Ansible is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
# GNU General Public License for more details.
|
|
|
|
#
|
|
|
|
# You should have received a copy of the GNU General Public License
|
|
|
|
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
#
|
|
|
|
# ansible-pull is a script that runs ansible in local mode
|
|
|
|
# after checking out a playbooks directory from source repo. There is an
|
|
|
|
# example playbook to bootstrap this script in the examples/ dir which
|
|
|
|
# installs ansible and sets it up to run on cron.
|
|
|
|
|
|
|
|
import os
|
|
|
|
import shutil
|
|
|
|
import tempfile
|
|
|
|
from io import BytesIO
|
|
|
|
from subprocess import call
|
|
|
|
from ansible import errors
|
|
|
|
from hashlib import sha256
|
|
|
|
from hashlib import md5
|
|
|
|
from binascii import hexlify
|
|
|
|
from binascii import unhexlify
|
|
|
|
from ansible import constants as C
|
|
|
|
|
|
|
|
# AES IMPORTS
|
|
|
|
try:
|
2014-02-24 18:09:36 +00:00
|
|
|
from Crypto.Cipher import AES as AES
|
2014-02-11 17:03:11 +00:00
|
|
|
HAS_AES = True
|
|
|
|
except ImportError:
|
|
|
|
HAS_AES = False
|
|
|
|
|
|
|
|
HEADER='$ANSIBLE_VAULT'
|
2014-02-24 18:09:36 +00:00
|
|
|
CIPHER_WHITELIST=['AES']
|
2014-02-11 17:03:11 +00:00
|
|
|
|
2014-02-24 18:09:36 +00:00
|
|
|
class VaultLib(object):
|
|
|
|
|
|
|
|
def __init__(self, password):
|
|
|
|
self.password = password
|
|
|
|
self.cipher_name = None
|
|
|
|
self.version = '1.0'
|
|
|
|
|
|
|
|
def is_encrypted(self, data):
|
|
|
|
if data.startswith(HEADER):
|
|
|
|
return True
|
|
|
|
else:
|
2014-02-22 02:06:04 +00:00
|
|
|
return False
|
2014-02-11 17:03:11 +00:00
|
|
|
|
2014-02-24 18:09:36 +00:00
|
|
|
def encrypt(self, data):
|
2014-02-11 17:03:11 +00:00
|
|
|
|
2014-02-24 18:09:36 +00:00
|
|
|
if self.is_encrypted(data):
|
|
|
|
raise errors.AnsibleError("data is already encrypted")
|
2014-02-11 17:03:11 +00:00
|
|
|
|
2014-02-24 18:09:36 +00:00
|
|
|
if 'Vault' + self.cipher_name in globals() and self.cipher_name in CIPHER_WHITELIST:
|
|
|
|
cipher = globals()['Vault' + self.cipher_name]
|
|
|
|
this_cipher = cipher()
|
|
|
|
else:
|
|
|
|
raise errors.AnsibleError("%s cipher could not be found" % self.cipher_name)
|
|
|
|
|
|
|
|
# combine sha + data
|
|
|
|
this_sha = sha256(data).hexdigest()
|
|
|
|
tmp_data = this_sha + "\n" + data
|
|
|
|
# encrypt sha + data
|
|
|
|
tmp_data = this_cipher.encrypt(tmp_data, self.password)
|
|
|
|
# add header
|
|
|
|
tmp_data = self._add_headers_and_hexify_encrypted_data(tmp_data)
|
|
|
|
return tmp_data
|
|
|
|
|
|
|
|
def decrypt(self, data):
|
|
|
|
if self.password is None:
|
|
|
|
raise errors.AnsibleError("A vault password must be specified to decrypt data")
|
|
|
|
|
|
|
|
if not self.is_encrypted(data):
|
|
|
|
raise errors.AnsibleError("data is not encrypted")
|
|
|
|
|
|
|
|
# clean out header, hex and sha
|
|
|
|
data = self._split_headers_and_get_unhexified_data(data)
|
|
|
|
|
|
|
|
# create the cipher object
|
|
|
|
if 'Vault' + self.cipher_name in globals() and self.cipher_name in CIPHER_WHITELIST:
|
|
|
|
cipher = globals()['Vault' + self.cipher_name]
|
|
|
|
this_cipher = cipher()
|
|
|
|
else:
|
|
|
|
raise errors.AnsibleError("%s cipher could not be found" % self.cipher_name)
|
2014-02-11 17:03:11 +00:00
|
|
|
|
2014-02-24 18:09:36 +00:00
|
|
|
# try to unencrypt data
|
|
|
|
data = this_cipher.decrypt(data, self.password)
|
2014-02-11 17:03:11 +00:00
|
|
|
|
2014-02-24 18:09:36 +00:00
|
|
|
# split out sha and verify decryption
|
|
|
|
split_data = data.split("\n")
|
|
|
|
this_sha = split_data[0]
|
|
|
|
this_data = '\n'.join(split_data[1:])
|
|
|
|
test_sha = sha256(this_data).hexdigest()
|
|
|
|
if this_sha != test_sha:
|
2014-02-24 18:20:27 +00:00
|
|
|
raise errors.AnsibleError("Decryption failed")
|
2014-02-11 17:03:11 +00:00
|
|
|
|
2014-02-24 18:09:36 +00:00
|
|
|
return this_data
|
2014-02-11 17:03:11 +00:00
|
|
|
|
2014-02-24 18:09:36 +00:00
|
|
|
def _add_headers_and_hexify_encrypted_data(self, data):
|
|
|
|
# combine header and hexlified encrypted data in 80 char columns
|
2014-02-11 17:03:11 +00:00
|
|
|
|
2014-02-24 18:09:36 +00:00
|
|
|
tmpdata = hexlify(data)
|
|
|
|
tmpdata = [tmpdata[i:i+80] for i in range(0, len(tmpdata), 80)]
|
2014-02-11 17:03:11 +00:00
|
|
|
|
2014-02-24 18:09:36 +00:00
|
|
|
if not self.cipher_name:
|
|
|
|
raise errors.AnsibleError("the cipher must be set before adding a header")
|
2014-02-11 17:03:11 +00:00
|
|
|
|
2014-02-24 18:09:36 +00:00
|
|
|
dirty_data = HEADER + ";" + str(self.version) + ";" + self.cipher_name + "\n"
|
|
|
|
for l in tmpdata:
|
|
|
|
dirty_data += l + '\n'
|
2014-02-11 17:03:11 +00:00
|
|
|
|
2014-02-24 18:09:36 +00:00
|
|
|
return dirty_data
|
2014-02-11 17:03:11 +00:00
|
|
|
|
|
|
|
|
2014-02-24 18:09:36 +00:00
|
|
|
def _split_headers_and_get_unhexified_data(self, data):
|
|
|
|
# used by decrypt
|
2014-02-11 17:03:11 +00:00
|
|
|
|
2014-02-24 18:09:36 +00:00
|
|
|
tmpdata = data.split('\n')
|
|
|
|
tmpheader = tmpdata[0].strip().split(';')
|
2014-02-11 17:03:11 +00:00
|
|
|
|
2014-02-24 18:09:36 +00:00
|
|
|
self.version = str(tmpheader[1].strip())
|
|
|
|
self.cipher_name = str(tmpheader[2].strip())
|
|
|
|
clean_data = ''.join(tmpdata[1:])
|
2014-02-11 17:03:11 +00:00
|
|
|
|
2014-02-24 18:09:36 +00:00
|
|
|
# strip out newline, join, unhex
|
|
|
|
clean_data = [ x.strip() for x in clean_data ]
|
|
|
|
clean_data = unhexlify(''.join(clean_data))
|
|
|
|
|
|
|
|
return clean_data
|
|
|
|
|
|
|
|
class VaultEditor(object):
|
|
|
|
# uses helper methods for write_file(self, filename, data)
|
|
|
|
# to write a file so that code isn't duplicated for simple
|
|
|
|
# file I/O, ditto read_file(self, filename) and launch_editor(self, filename)
|
|
|
|
# ... "Don't Repeat Yourself", etc.
|
|
|
|
|
|
|
|
def __init__(self, cipher_name, password, filename):
|
|
|
|
# instantiates a member variable for VaultLib
|
|
|
|
self.cipher_name = cipher_name
|
|
|
|
self.password = password
|
|
|
|
self.filename = filename
|
|
|
|
|
|
|
|
def create_file(self):
|
2014-02-11 17:03:11 +00:00
|
|
|
""" create a new encrypted file """
|
|
|
|
|
|
|
|
if os.path.isfile(self.filename):
|
|
|
|
raise errors.AnsibleError("%s exists, please use 'edit' instead" % self.filename)
|
|
|
|
|
|
|
|
# drop the user into vim on file
|
|
|
|
EDITOR = os.environ.get('EDITOR','vim')
|
|
|
|
call([EDITOR, self.filename])
|
2014-02-24 18:09:36 +00:00
|
|
|
tmpdata = self.read_data(self.filename)
|
|
|
|
this_vault = VaultLib(self.password)
|
|
|
|
this_vault.cipher_name = self.cipher_name
|
|
|
|
enc_data = this_vault.encrypt(tmpdata)
|
|
|
|
self.write_data(enc_data, self.filename)
|
|
|
|
|
|
|
|
def decrypt_file(self):
|
|
|
|
if not os.path.isfile(self.filename):
|
|
|
|
raise errors.AnsibleError("%s does not exist" % self.filename)
|
2014-02-11 17:03:11 +00:00
|
|
|
|
2014-02-24 18:09:36 +00:00
|
|
|
tmpdata = self.read_data(self.filename)
|
|
|
|
this_vault = VaultLib(self.password)
|
|
|
|
if this_vault.is_encrypted(tmpdata):
|
|
|
|
dec_data = this_vault.decrypt(tmpdata)
|
|
|
|
self.write_data(dec_data, self.filename)
|
|
|
|
else:
|
2014-02-11 17:03:11 +00:00
|
|
|
raise errors.AnsibleError("%s is not encrypted" % self.filename)
|
|
|
|
|
2014-02-24 18:09:36 +00:00
|
|
|
def edit_file(self):
|
2014-02-11 17:03:11 +00:00
|
|
|
|
2014-02-24 18:09:36 +00:00
|
|
|
# decrypt to tmpfile
|
|
|
|
tmpdata = self.read_data(self.filename)
|
|
|
|
this_vault = VaultLib(self.password)
|
|
|
|
dec_data = this_vault.decrypt(tmpdata)
|
|
|
|
_, tmp_path = tempfile.mkstemp()
|
|
|
|
self.write_data(dec_data, tmp_path)
|
2014-02-11 17:03:11 +00:00
|
|
|
|
2014-02-24 18:09:36 +00:00
|
|
|
# drop the user into vim on the tmp file
|
2014-02-11 17:03:11 +00:00
|
|
|
EDITOR = os.environ.get('EDITOR','vim')
|
2014-02-24 18:09:36 +00:00
|
|
|
call([EDITOR, tmp_path])
|
|
|
|
new_data = self.read_data(tmp_path)
|
2014-02-11 17:03:11 +00:00
|
|
|
|
2014-02-24 18:09:36 +00:00
|
|
|
# create new vault and set cipher to old
|
|
|
|
new_vault = VaultLib(self.password)
|
|
|
|
new_vault.cipher_name = this_vault.cipher_name
|
2014-02-11 17:03:11 +00:00
|
|
|
|
2014-02-24 18:09:36 +00:00
|
|
|
# encrypt new data a write out to tmp
|
|
|
|
enc_data = new_vault.encrypt(new_data)
|
|
|
|
self.write_data(enc_data, tmp_path)
|
2014-02-11 17:03:11 +00:00
|
|
|
|
2014-02-24 18:09:36 +00:00
|
|
|
# shuffle tmp file into place
|
|
|
|
self.shuffle_files(tmp_path, self.filename)
|
2014-02-11 17:03:11 +00:00
|
|
|
|
2014-02-24 18:09:36 +00:00
|
|
|
def encrypt_file(self):
|
|
|
|
if not os.path.isfile(self.filename):
|
|
|
|
raise errors.AnsibleError("%s does not exist" % self.filename)
|
|
|
|
|
|
|
|
tmpdata = self.read_data(self.filename)
|
|
|
|
this_vault = VaultLib(self.password)
|
|
|
|
this_vault.cipher_name = self.cipher_name
|
|
|
|
if not this_vault.is_encrypted(tmpdata):
|
|
|
|
enc_data = this_vault.encrypt(tmpdata)
|
|
|
|
self.write_data(enc_data, self.filename)
|
2014-02-11 17:03:11 +00:00
|
|
|
else:
|
2014-02-24 18:09:36 +00:00
|
|
|
raise errors.AnsibleError("%s is already encrypted" % self.filename)
|
2014-02-11 17:03:11 +00:00
|
|
|
|
2014-02-24 18:09:36 +00:00
|
|
|
def rekey_file(self, new_password):
|
|
|
|
# decrypt
|
|
|
|
tmpdata = self.read_data(self.filename)
|
|
|
|
this_vault = VaultLib(self.password)
|
|
|
|
dec_data = this_vault.decrypt(tmpdata)
|
2014-02-11 17:03:11 +00:00
|
|
|
|
2014-02-24 18:09:36 +00:00
|
|
|
# create new vault, set cipher to old and password to new
|
|
|
|
new_vault = VaultLib(new_password)
|
|
|
|
new_vault.cipher_name = this_vault.cipher_name
|
2014-02-11 17:03:11 +00:00
|
|
|
|
2014-02-24 18:09:36 +00:00
|
|
|
# re-encrypt data and re-write file
|
|
|
|
enc_data = new_vault.encrypt(dec_data)
|
|
|
|
self.write_data(enc_data, self.filename)
|
2014-02-11 17:03:11 +00:00
|
|
|
|
2014-02-24 18:09:36 +00:00
|
|
|
def read_data(self, filename):
|
|
|
|
f = open(filename, "rb")
|
|
|
|
tmpdata = f.read()
|
2014-02-11 17:03:11 +00:00
|
|
|
f.close()
|
2014-02-24 18:09:36 +00:00
|
|
|
return tmpdata
|
2014-02-11 17:03:11 +00:00
|
|
|
|
2014-02-24 18:09:36 +00:00
|
|
|
def write_data(self, data, filename):
|
|
|
|
if os.path.isfile(filename):
|
2014-02-11 17:03:11 +00:00
|
|
|
os.remove(filename)
|
|
|
|
f = open(filename, "wb")
|
2014-02-24 18:09:36 +00:00
|
|
|
f.write(data)
|
2014-02-11 17:03:11 +00:00
|
|
|
f.close()
|
|
|
|
|
2014-02-24 18:09:36 +00:00
|
|
|
def shuffle_files(self, src, dest):
|
|
|
|
# overwrite dest with src
|
|
|
|
if os.path.isfile(dest):
|
|
|
|
os.remove(dest)
|
|
|
|
shutil.move(src, dest)
|
2014-02-11 17:03:11 +00:00
|
|
|
|
2014-02-24 18:09:36 +00:00
|
|
|
########################################
|
|
|
|
# CIPHERS #
|
|
|
|
########################################
|
2014-02-11 17:03:11 +00:00
|
|
|
|
2014-02-24 18:09:36 +00:00
|
|
|
class VaultAES(object):
|
2014-02-11 17:03:11 +00:00
|
|
|
|
|
|
|
# http://stackoverflow.com/a/16761459
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
if not HAS_AES:
|
|
|
|
raise errors.AnsibleError("pycrypto is not installed. Fix this with your package manager, for instance, yum-install python-crypto OR (apt equivalent)")
|
|
|
|
|
|
|
|
def aes_derive_key_and_iv(self, password, salt, key_length, iv_length):
|
|
|
|
|
|
|
|
""" Create a key and an initialization vector """
|
|
|
|
|
|
|
|
d = d_i = ''
|
|
|
|
while len(d) < key_length + iv_length:
|
|
|
|
d_i = md5(d_i + password + salt).digest()
|
|
|
|
d += d_i
|
|
|
|
|
|
|
|
key = d[:key_length]
|
|
|
|
iv = d[key_length:key_length+iv_length]
|
|
|
|
|
|
|
|
return key, iv
|
|
|
|
|
2014-02-24 18:09:36 +00:00
|
|
|
def encrypt(self, data, password, key_length=32):
|
2014-02-11 17:03:11 +00:00
|
|
|
|
|
|
|
""" Read plaintext data from in_file and write encrypted to out_file """
|
|
|
|
|
2014-02-24 18:09:36 +00:00
|
|
|
in_file = BytesIO(data)
|
|
|
|
in_file.seek(0)
|
|
|
|
out_file = BytesIO()
|
|
|
|
|
|
|
|
bs = AES.block_size
|
2014-02-11 17:03:11 +00:00
|
|
|
|
|
|
|
# Get a block of random data. EL does not have Crypto.Random.new()
|
|
|
|
# so os.urandom is used for cross platform purposes
|
|
|
|
salt = os.urandom(bs - len('Salted__'))
|
|
|
|
|
|
|
|
key, iv = self.aes_derive_key_and_iv(password, salt, key_length, bs)
|
2014-02-24 18:09:36 +00:00
|
|
|
cipher = AES.new(key, AES.MODE_CBC, iv)
|
2014-02-11 17:03:11 +00:00
|
|
|
out_file.write('Salted__' + salt)
|
|
|
|
finished = False
|
|
|
|
while not finished:
|
|
|
|
chunk = in_file.read(1024 * bs)
|
|
|
|
if len(chunk) == 0 or len(chunk) % bs != 0:
|
|
|
|
padding_length = (bs - len(chunk) % bs) or bs
|
|
|
|
chunk += padding_length * chr(padding_length)
|
|
|
|
finished = True
|
|
|
|
out_file.write(cipher.encrypt(chunk))
|
|
|
|
|
2014-02-24 18:09:36 +00:00
|
|
|
out_file.seek(0)
|
|
|
|
return out_file.read()
|
|
|
|
|
|
|
|
def decrypt(self, data, password, key_length=32):
|
2014-02-11 17:03:11 +00:00
|
|
|
|
|
|
|
""" Read encrypted data from in_file and write decrypted to out_file """
|
|
|
|
|
|
|
|
# http://stackoverflow.com/a/14989032
|
|
|
|
|
2014-02-24 18:09:36 +00:00
|
|
|
in_file = BytesIO(data)
|
|
|
|
in_file.seek(0)
|
|
|
|
out_file = BytesIO()
|
|
|
|
|
|
|
|
bs = AES.block_size
|
2014-02-11 17:03:11 +00:00
|
|
|
salt = in_file.read(bs)[len('Salted__'):]
|
|
|
|
key, iv = self.aes_derive_key_and_iv(password, salt, key_length, bs)
|
2014-02-24 18:09:36 +00:00
|
|
|
cipher = AES.new(key, AES.MODE_CBC, iv)
|
2014-02-11 17:03:11 +00:00
|
|
|
next_chunk = ''
|
|
|
|
finished = False
|
|
|
|
|
|
|
|
while not finished:
|
|
|
|
chunk, next_chunk = next_chunk, cipher.decrypt(in_file.read(1024 * bs))
|
|
|
|
if len(next_chunk) == 0:
|
|
|
|
padding_length = ord(chunk[-1])
|
|
|
|
chunk = chunk[:-padding_length]
|
|
|
|
finished = True
|
2014-02-19 16:35:00 +00:00
|
|
|
out_file.write(chunk)
|
2014-02-11 17:03:11 +00:00
|
|
|
|
|
|
|
# reset the stream pointer to the beginning
|
2014-02-24 18:09:36 +00:00
|
|
|
out_file.seek(0)
|
|
|
|
return out_file.read()
|
|
|
|
|
|
|
|
|