# (c) 2012-2014, Michael DeHaan # # This file is part of Ansible # # 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 . from __future__ import (absolute_import, division, print_function) __metaclass__ = type import multiprocessing import os import stat import tempfile import time import warnings import random from ansible import constants as C from ansible.errors import AnsibleError from ansible.module_utils.six import text_type from ansible.module_utils._text import to_text, to_bytes PASSLIB_AVAILABLE = False try: import passlib.hash PASSLIB_AVAILABLE = True except: pass try: from __main__ import display except ImportError: from ansible.utils.display import Display display = Display() KEYCZAR_AVAILABLE = False try: try: # some versions of pycrypto may not have this? from Crypto.pct_warnings import PowmInsecureWarning except ImportError: PowmInsecureWarning = RuntimeWarning with warnings.catch_warnings(record=True) as warning_handler: warnings.simplefilter("error", PowmInsecureWarning) try: import keyczar.errors as key_errors from keyczar.keys import AesKey except PowmInsecureWarning: display.system_warning( "The version of gmp you have installed has a known issue regarding " "timing vulnerabilities when used with pycrypto. " "If possible, you should update it (i.e. yum update gmp)." ) warnings.resetwarnings() warnings.simplefilter("ignore") import keyczar.errors as key_errors from keyczar.keys import AesKey KEYCZAR_AVAILABLE = True except ImportError: pass __all__ = ['do_encrypt'] _LOCK = multiprocessing.Lock() def do_encrypt(result, encrypt, salt_size=None, salt=None): if PASSLIB_AVAILABLE: try: crypt = getattr(passlib.hash, encrypt) except: raise AnsibleError("passlib does not support '%s' algorithm" % encrypt) if salt_size: result = crypt.encrypt(result, salt_size=salt_size) elif salt: if crypt._salt_is_bytes: salt = to_bytes(salt, encoding='ascii', errors='strict') else: salt = to_text(salt, encoding='ascii', errors='strict') result = crypt.encrypt(result, salt=salt) else: result = crypt.encrypt(result) else: raise AnsibleError("passlib must be installed to encrypt vars_prompt values") # Hashes from passlib.hash should be represented as ascii strings of hex # digits so this should not traceback. If it's not representable as such # we need to traceback and then blacklist such algorithms because it may # impact calling code. return to_text(result, errors='strict') 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 if not KEYCZAR_AVAILABLE: raise AnsibleError("python-keyczar must be installed on the control machine to use accelerated modes") key_path = os.path.expanduser(C.ACCELERATE_KEYS_DIR) if not os.path.exists(key_path): # avoid race with multiple forks trying to create paths on host # but limit when locking is needed to creation only with(_LOCK): if not os.path.exists(key_path): # use a temp directory and rename to ensure the directory # searched for only appears after permissions applied. tmp_dir = tempfile.mkdtemp(dir=os.path.dirname(key_path)) os.chmod(tmp_dir, int(C.ACCELERATE_KEYS_DIR_PERMS, 8)) os.rename(tmp_dir, key_path) elif not os.path.isdir(key_path): raise AnsibleError('ACCELERATE_KEYS_DIR is not a directory.') if stat.S_IMODE(os.stat(key_path).st_mode) != int(C.ACCELERATE_KEYS_DIR_PERMS, 8): raise AnsibleError('Incorrect permissions on the private key directory. Use `chmod 0%o %s` to correct this issue, and make sure any of the keys files ' 'contained within that directory are set to 0%o' % (int(C.ACCELERATE_KEYS_DIR_PERMS, 8), C.ACCELERATE_KEYS_DIR, int(C.ACCELERATE_KEYS_FILE_PERMS, 8))) key_path = os.path.join(key_path, hostname) # 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): # avoid race with multiple forks trying to create key # but limit when locking is needed to creation only with(_LOCK): if not os.path.exists(key_path) or (time.time() - os.path.getmtime(key_path) > 60 * 60 * 2): key = AesKey.Generate() # use temp file to ensure file only appears once it has # desired contents and permissions with tempfile.NamedTemporaryFile(mode='w', dir=os.path.dirname(key_path), delete=False) as fh: tmp_key_path = fh.name fh.write(str(key)) os.chmod(tmp_key_path, int(C.ACCELERATE_KEYS_FILE_PERMS, 8)) os.rename(tmp_key_path, key_path) return key if stat.S_IMODE(os.stat(key_path).st_mode) != int(C.ACCELERATE_KEYS_FILE_PERMS, 8): raise AnsibleError('Incorrect permissions on the key file for this host. Use `chmod 0%o %s` to ' 'correct this issue.' % (int(C.ACCELERATE_KEYS_FILE_PERMS, 8), key_path)) fh = open(key_path) key = AesKey.Read(fh.read()) fh.close() return key def keyczar_encrypt(key, msg): return key.Encrypt(msg.encode('utf-8')) def keyczar_decrypt(key, msg): try: return key.Decrypt(msg) except key_errors.InvalidSignatureError: raise AnsibleError("decryption failed") DEFAULT_PASSWORD_LENGTH = 20 def random_password(length=DEFAULT_PASSWORD_LENGTH, chars=C.DEFAULT_PASSWORD_CHARS): '''Return a random password string of length containing only chars :kwarg length: The number of characters in the new password. Defaults to 20. :kwarg chars: The characters to choose from. The default is all ascii letters, ascii digits, and these symbols ``.,:-_`` ''' assert isinstance(chars, text_type), '%s (%s) is not a text_type' % (chars, type(chars)) random_generator = random.SystemRandom() return u''.join(random_generator.choice(chars) for dummy in range(length))