PageRenderTime 81ms CodeModel.GetById 15ms app.highlight 55ms RepoModel.GetById 1ms app.codeStats 1ms

/lib/ansible/parsing/vault/__init__.py

https://github.com/debfx/ansible
Python | 1332 lines | 1251 code | 30 blank | 51 comment | 21 complexity | be8fe15b95f0fcb7c5253c9fb2a2a391 MD5 | raw file

Large files files are truncated, but you can click here to view the full file

   1# (c) 2014, James Tanner <tanner.jc@gmail.com>
   2# (c) 2016, Adrian Likins <alikins@redhat.com>
   3# (c) 2016 Toshio Kuratomi <tkuratomi@ansible.com>
   4#
   5# Ansible is free software: you can redistribute it and/or modify
   6# it under the terms of the GNU General Public License as published by
   7# the Free Software Foundation, either version 3 of the License, or
   8# (at your option) any later version.
   9#
  10# Ansible is distributed in the hope that it will be useful,
  11# but WITHOUT ANY WARRANTY; without even the implied warranty of
  12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  13# GNU General Public License for more details.
  14#
  15# You should have received a copy of the GNU General Public License
  16# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
  17
  18# Make coding more python3-ish
  19from __future__ import (absolute_import, division, print_function)
  20__metaclass__ = type
  21
  22import os
  23import random
  24import shlex
  25import shutil
  26import subprocess
  27import sys
  28import tempfile
  29import warnings
  30from binascii import hexlify
  31from binascii import unhexlify
  32from binascii import Error as BinasciiError
  33
  34HAS_CRYPTOGRAPHY = False
  35HAS_PYCRYPTO = False
  36HAS_SOME_PYCRYPTO = False
  37CRYPTOGRAPHY_BACKEND = None
  38try:
  39    with warnings.catch_warnings():
  40        warnings.simplefilter("ignore", DeprecationWarning)
  41        from cryptography.exceptions import InvalidSignature
  42    from cryptography.hazmat.backends import default_backend
  43    from cryptography.hazmat.primitives import hashes, padding
  44    from cryptography.hazmat.primitives.hmac import HMAC
  45    from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
  46    from cryptography.hazmat.primitives.ciphers import (
  47        Cipher as C_Cipher, algorithms, modes
  48    )
  49    CRYPTOGRAPHY_BACKEND = default_backend()
  50    HAS_CRYPTOGRAPHY = True
  51except ImportError:
  52    pass
  53
  54try:
  55    from Crypto.Cipher import AES as AES_pycrypto
  56    HAS_SOME_PYCRYPTO = True
  57
  58    # Note: Only used for loading obsolete VaultAES files.  All files are written
  59    # using the newer VaultAES256 which does not require md5
  60    from Crypto.Hash import SHA256 as SHA256_pycrypto
  61    from Crypto.Hash import HMAC as HMAC_pycrypto
  62
  63    # Counter import fails for 2.0.1, requires >= 2.6.1 from pip
  64    from Crypto.Util import Counter as Counter_pycrypto
  65
  66    # KDF import fails for 2.0.1, requires >= 2.6.1 from pip
  67    from Crypto.Protocol.KDF import PBKDF2 as PBKDF2_pycrypto
  68    HAS_PYCRYPTO = True
  69except ImportError:
  70    pass
  71
  72from ansible.errors import AnsibleError, AnsibleAssertionError
  73from ansible import constants as C
  74from ansible.module_utils.six import PY3, binary_type
  75# Note: on py2, this zip is izip not the list based zip() builtin
  76from ansible.module_utils.six.moves import zip
  77from ansible.module_utils._text import to_bytes, to_text, to_native
  78from ansible.utils.display import Display
  79from ansible.utils.path import makedirs_safe
  80
  81display = Display()
  82
  83
  84b_HEADER = b'$ANSIBLE_VAULT'
  85CIPHER_WHITELIST = frozenset((u'AES', u'AES256'))
  86CIPHER_WRITE_WHITELIST = frozenset((u'AES256',))
  87# See also CIPHER_MAPPING at the bottom of the file which maps cipher strings
  88# (used in VaultFile header) to a cipher class
  89
  90NEED_CRYPTO_LIBRARY = "ansible-vault requires either the cryptography library (preferred) or"
  91if HAS_SOME_PYCRYPTO:
  92    NEED_CRYPTO_LIBRARY += " a newer version of"
  93NEED_CRYPTO_LIBRARY += " pycrypto in order to function."
  94
  95
  96class AnsibleVaultError(AnsibleError):
  97    pass
  98
  99
 100class AnsibleVaultPasswordError(AnsibleVaultError):
 101    pass
 102
 103
 104class AnsibleVaultFormatError(AnsibleError):
 105    pass
 106
 107
 108def is_encrypted(data):
 109    """ Test if this is vault encrypted data blob
 110
 111    :arg data: a byte or text string to test whether it is recognized as vault
 112        encrypted data
 113    :returns: True if it is recognized.  Otherwise, False.
 114    """
 115    try:
 116        # Make sure we have a byte string and that it only contains ascii
 117        # bytes.
 118        b_data = to_bytes(to_text(data, encoding='ascii', errors='strict', nonstring='strict'), encoding='ascii', errors='strict')
 119    except (UnicodeError, TypeError):
 120        # The vault format is pure ascii so if we failed to encode to bytes
 121        # via ascii we know that this is not vault data.
 122        # Similarly, if it's not a string, it's not vault data
 123        return False
 124
 125    if b_data.startswith(b_HEADER):
 126        return True
 127    return False
 128
 129
 130def is_encrypted_file(file_obj, start_pos=0, count=-1):
 131    """Test if the contents of a file obj are a vault encrypted data blob.
 132
 133    :arg file_obj: A file object that will be read from.
 134    :kwarg start_pos: A byte offset in the file to start reading the header
 135        from.  Defaults to 0, the beginning of the file.
 136    :kwarg count: Read up to this number of bytes from the file to determine
 137        if it looks like encrypted vault data.  The default is -1, read to the
 138        end of file.
 139    :returns: True if the file looks like a vault file. Otherwise, False.
 140    """
 141    # read the header and reset the file stream to where it started
 142    current_position = file_obj.tell()
 143    try:
 144        file_obj.seek(start_pos)
 145        return is_encrypted(file_obj.read(count))
 146
 147    finally:
 148        file_obj.seek(current_position)
 149
 150
 151def _parse_vaulttext_envelope(b_vaulttext_envelope, default_vault_id=None):
 152
 153    b_tmpdata = b_vaulttext_envelope.splitlines()
 154    b_tmpheader = b_tmpdata[0].strip().split(b';')
 155
 156    b_version = b_tmpheader[1].strip()
 157    cipher_name = to_text(b_tmpheader[2].strip())
 158    vault_id = default_vault_id
 159
 160    # Only attempt to find vault_id if the vault file is version 1.2 or newer
 161    # if self.b_version == b'1.2':
 162    if len(b_tmpheader) >= 4:
 163        vault_id = to_text(b_tmpheader[3].strip())
 164
 165    b_ciphertext = b''.join(b_tmpdata[1:])
 166
 167    return b_ciphertext, b_version, cipher_name, vault_id
 168
 169
 170def parse_vaulttext_envelope(b_vaulttext_envelope, default_vault_id=None, filename=None):
 171    """Parse the vaulttext envelope
 172
 173    When data is saved, it has a header prepended and is formatted into 80
 174    character lines.  This method extracts the information from the header
 175    and then removes the header and the inserted newlines.  The string returned
 176    is suitable for processing by the Cipher classes.
 177
 178    :arg b_vaulttext: byte str containing the data from a save file
 179    :kwarg default_vault_id: The vault_id name to use if the vaulttext does not provide one.
 180    :kwarg filename: The filename that the data came from.  This is only
 181        used to make better error messages in case the data cannot be
 182        decrypted. This is optional.
 183    :returns: A tuple of byte str of the vaulttext suitable to pass to parse_vaultext,
 184        a byte str of the vault format version,
 185        the name of the cipher used, and the vault_id.
 186    :raises: AnsibleVaultFormatError: if the vaulttext_envelope format is invalid
 187    """
 188    # used by decrypt
 189    default_vault_id = default_vault_id or C.DEFAULT_VAULT_IDENTITY
 190
 191    try:
 192        return _parse_vaulttext_envelope(b_vaulttext_envelope, default_vault_id)
 193    except Exception as exc:
 194        msg = "Vault envelope format error"
 195        if filename:
 196            msg += ' in %s' % (filename)
 197        msg += ': %s' % exc
 198        raise AnsibleVaultFormatError(msg)
 199
 200
 201def format_vaulttext_envelope(b_ciphertext, cipher_name, version=None, vault_id=None):
 202    """ Add header and format to 80 columns
 203
 204        :arg b_ciphertext: the encrypted and hexlified data as a byte string
 205        :arg cipher_name: unicode cipher name (for ex, u'AES256')
 206        :arg version: unicode vault version (for ex, '1.2'). Optional ('1.1' is default)
 207        :arg vault_id: unicode vault identifier. If provided, the version will be bumped to 1.2.
 208        :returns: a byte str that should be dumped into a file.  It's
 209            formatted to 80 char columns and has the header prepended
 210    """
 211
 212    if not cipher_name:
 213        raise AnsibleError("the cipher must be set before adding a header")
 214
 215    version = version or '1.1'
 216
 217    # If we specify a vault_id, use format version 1.2. For no vault_id, stick to 1.1
 218    if vault_id and vault_id != u'default':
 219        version = '1.2'
 220
 221    b_version = to_bytes(version, 'utf-8', errors='strict')
 222    b_vault_id = to_bytes(vault_id, 'utf-8', errors='strict')
 223    b_cipher_name = to_bytes(cipher_name, 'utf-8', errors='strict')
 224
 225    header_parts = [b_HEADER,
 226                    b_version,
 227                    b_cipher_name]
 228
 229    if b_version == b'1.2' and b_vault_id:
 230        header_parts.append(b_vault_id)
 231
 232    header = b';'.join(header_parts)
 233
 234    b_vaulttext = [header]
 235    b_vaulttext += [b_ciphertext[i:i + 80] for i in range(0, len(b_ciphertext), 80)]
 236    b_vaulttext += [b'']
 237    b_vaulttext = b'\n'.join(b_vaulttext)
 238
 239    return b_vaulttext
 240
 241
 242def _unhexlify(b_data):
 243    try:
 244        return unhexlify(b_data)
 245    except (BinasciiError, TypeError) as exc:
 246        raise AnsibleVaultFormatError('Vault format unhexlify error: %s' % exc)
 247
 248
 249def _parse_vaulttext(b_vaulttext):
 250    b_vaulttext = _unhexlify(b_vaulttext)
 251    b_salt, b_crypted_hmac, b_ciphertext = b_vaulttext.split(b"\n", 2)
 252    b_salt = _unhexlify(b_salt)
 253    b_ciphertext = _unhexlify(b_ciphertext)
 254
 255    return b_ciphertext, b_salt, b_crypted_hmac
 256
 257
 258def parse_vaulttext(b_vaulttext):
 259    """Parse the vaulttext
 260
 261    :arg b_vaulttext: byte str containing the vaulttext (ciphertext, salt, crypted_hmac)
 262    :returns: A tuple of byte str of the ciphertext suitable for passing to a
 263        Cipher class's decrypt() function, a byte str of the salt,
 264        and a byte str of the crypted_hmac
 265    :raises: AnsibleVaultFormatError: if the vaulttext format is invalid
 266    """
 267    # SPLIT SALT, DIGEST, AND DATA
 268    try:
 269        return _parse_vaulttext(b_vaulttext)
 270    except AnsibleVaultFormatError:
 271        raise
 272    except Exception as exc:
 273        msg = "Vault vaulttext format error: %s" % exc
 274        raise AnsibleVaultFormatError(msg)
 275
 276
 277def verify_secret_is_not_empty(secret, msg=None):
 278    '''Check the secret against minimal requirements.
 279
 280    Raises: AnsibleVaultPasswordError if the password does not meet requirements.
 281
 282    Currently, only requirement is that the password is not None or an empty string.
 283    '''
 284    msg = msg or 'Invalid vault password was provided'
 285    if not secret:
 286        raise AnsibleVaultPasswordError(msg)
 287
 288
 289class VaultSecret:
 290    '''Opaque/abstract objects for a single vault secret. ie, a password or a key.'''
 291    def __init__(self, _bytes=None):
 292        # FIXME: ? that seems wrong... Unset etc?
 293        self._bytes = _bytes
 294
 295    @property
 296    def bytes(self):
 297        '''The secret as a bytestring.
 298
 299        Sub classes that store text types will need to override to encode the text to bytes.
 300        '''
 301        return self._bytes
 302
 303    def load(self):
 304        return self._bytes
 305
 306
 307class PromptVaultSecret(VaultSecret):
 308    default_prompt_formats = ["Vault password (%s): "]
 309
 310    def __init__(self, _bytes=None, vault_id=None, prompt_formats=None):
 311        super(PromptVaultSecret, self).__init__(_bytes=_bytes)
 312        self.vault_id = vault_id
 313
 314        if prompt_formats is None:
 315            self.prompt_formats = self.default_prompt_formats
 316        else:
 317            self.prompt_formats = prompt_formats
 318
 319    @property
 320    def bytes(self):
 321        return self._bytes
 322
 323    def load(self):
 324        self._bytes = self.ask_vault_passwords()
 325
 326    def ask_vault_passwords(self):
 327        b_vault_passwords = []
 328
 329        for prompt_format in self.prompt_formats:
 330            prompt = prompt_format % {'vault_id': self.vault_id}
 331            try:
 332                vault_pass = display.prompt(prompt, private=True)
 333            except EOFError:
 334                raise AnsibleVaultError('EOFError (ctrl-d) on prompt for (%s)' % self.vault_id)
 335
 336            verify_secret_is_not_empty(vault_pass)
 337
 338            b_vault_pass = to_bytes(vault_pass, errors='strict', nonstring='simplerepr').strip()
 339            b_vault_passwords.append(b_vault_pass)
 340
 341        # Make sure the passwords match by comparing them all to the first password
 342        for b_vault_password in b_vault_passwords:
 343            self.confirm(b_vault_passwords[0], b_vault_password)
 344
 345        if b_vault_passwords:
 346            return b_vault_passwords[0]
 347
 348        return None
 349
 350    def confirm(self, b_vault_pass_1, b_vault_pass_2):
 351        # enforce no newline chars at the end of passwords
 352
 353        if b_vault_pass_1 != b_vault_pass_2:
 354            # FIXME: more specific exception
 355            raise AnsibleError("Passwords do not match")
 356
 357
 358def script_is_client(filename):
 359    '''Determine if a vault secret script is a client script that can be given --vault-id args'''
 360
 361    # if password script is 'something-client' or 'something-client.[sh|py|rb|etc]'
 362    # script_name can still have '.' or could be entire filename if there is no ext
 363    script_name, dummy = os.path.splitext(filename)
 364
 365    # TODO: for now, this is entirely based on filename
 366    if script_name.endswith('-client'):
 367        return True
 368
 369    return False
 370
 371
 372def get_file_vault_secret(filename=None, vault_id=None, encoding=None, loader=None):
 373    this_path = os.path.realpath(os.path.expanduser(filename))
 374
 375    if not os.path.exists(this_path):
 376        raise AnsibleError("The vault password file %s was not found" % this_path)
 377
 378    if loader.is_executable(this_path):
 379        if script_is_client(filename):
 380            display.vvvv('The vault password file %s is a client script.' % filename)
 381            # TODO: pass vault_id_name to script via cli
 382            return ClientScriptVaultSecret(filename=this_path, vault_id=vault_id,
 383                                           encoding=encoding, loader=loader)
 384        # just a plain vault password script. No args, returns a byte array
 385        return ScriptVaultSecret(filename=this_path, encoding=encoding, loader=loader)
 386
 387    return FileVaultSecret(filename=this_path, encoding=encoding, loader=loader)
 388
 389
 390# TODO: mv these classes to a separate file so we don't pollute vault with 'subprocess' etc
 391class FileVaultSecret(VaultSecret):
 392    def __init__(self, filename=None, encoding=None, loader=None):
 393        super(FileVaultSecret, self).__init__()
 394        self.filename = filename
 395        self.loader = loader
 396
 397        self.encoding = encoding or 'utf8'
 398
 399        # We could load from file here, but that is eventually a pain to test
 400        self._bytes = None
 401        self._text = None
 402
 403    @property
 404    def bytes(self):
 405        if self._bytes:
 406            return self._bytes
 407        if self._text:
 408            return self._text.encode(self.encoding)
 409        return None
 410
 411    def load(self):
 412        self._bytes = self._read_file(self.filename)
 413
 414    def _read_file(self, filename):
 415        """
 416        Read a vault password from a file or if executable, execute the script and
 417        retrieve password from STDOUT
 418        """
 419
 420        # TODO: replace with use of self.loader
 421        try:
 422            f = open(filename, "rb")
 423            vault_pass = f.read().strip()
 424            f.close()
 425        except (OSError, IOError) as e:
 426            raise AnsibleError("Could not read vault password file %s: %s" % (filename, e))
 427
 428        b_vault_data, dummy = self.loader._decrypt_if_vault_data(vault_pass, filename)
 429
 430        vault_pass = b_vault_data.strip(b'\r\n')
 431
 432        verify_secret_is_not_empty(vault_pass,
 433                                   msg='Invalid vault password was provided from file (%s)' % filename)
 434
 435        return vault_pass
 436
 437    def __repr__(self):
 438        if self.filename:
 439            return "%s(filename='%s')" % (self.__class__.__name__, self.filename)
 440        return "%s()" % (self.__class__.__name__)
 441
 442
 443class ScriptVaultSecret(FileVaultSecret):
 444    def _read_file(self, filename):
 445        if not self.loader.is_executable(filename):
 446            raise AnsibleVaultError("The vault password script %s was not executable" % filename)
 447
 448        command = self._build_command()
 449
 450        stdout, stderr, p = self._run(command)
 451
 452        self._check_results(stdout, stderr, p)
 453
 454        vault_pass = stdout.strip(b'\r\n')
 455
 456        empty_password_msg = 'Invalid vault password was provided from script (%s)' % filename
 457        verify_secret_is_not_empty(vault_pass,
 458                                   msg=empty_password_msg)
 459
 460        return vault_pass
 461
 462    def _run(self, command):
 463        try:
 464            # STDERR not captured to make it easier for users to prompt for input in their scripts
 465            p = subprocess.Popen(command, stdout=subprocess.PIPE)
 466        except OSError as e:
 467            msg_format = "Problem running vault password script %s (%s)." \
 468                " If this is not a script, remove the executable bit from the file."
 469            msg = msg_format % (self.filename, e)
 470
 471            raise AnsibleError(msg)
 472
 473        stdout, stderr = p.communicate()
 474        return stdout, stderr, p
 475
 476    def _check_results(self, stdout, stderr, popen):
 477        if popen.returncode != 0:
 478            raise AnsibleError("Vault password script %s returned non-zero (%s): %s" %
 479                               (self.filename, popen.returncode, stderr))
 480
 481    def _build_command(self):
 482        return [self.filename]
 483
 484
 485class ClientScriptVaultSecret(ScriptVaultSecret):
 486    VAULT_ID_UNKNOWN_RC = 2
 487
 488    def __init__(self, filename=None, encoding=None, loader=None, vault_id=None):
 489        super(ClientScriptVaultSecret, self).__init__(filename=filename,
 490                                                      encoding=encoding,
 491                                                      loader=loader)
 492        self._vault_id = vault_id
 493        display.vvvv('Executing vault password client script: %s --vault-id %s' % (filename, vault_id))
 494
 495    def _run(self, command):
 496        try:
 497            p = subprocess.Popen(command,
 498                                 stdout=subprocess.PIPE,
 499                                 stderr=subprocess.PIPE)
 500        except OSError as e:
 501            msg_format = "Problem running vault password client script %s (%s)." \
 502                " If this is not a script, remove the executable bit from the file."
 503            msg = msg_format % (self.filename, e)
 504
 505            raise AnsibleError(msg)
 506
 507        stdout, stderr = p.communicate()
 508        return stdout, stderr, p
 509
 510    def _check_results(self, stdout, stderr, popen):
 511        if popen.returncode == self.VAULT_ID_UNKNOWN_RC:
 512            raise AnsibleError('Vault password client script %s did not find a secret for vault-id=%s: %s' %
 513                               (self.filename, self._vault_id, stderr))
 514
 515        if popen.returncode != 0:
 516            raise AnsibleError("Vault password client script %s returned non-zero (%s) when getting secret for vault-id=%s: %s" %
 517                               (self.filename, popen.returncode, self._vault_id, stderr))
 518
 519    def _build_command(self):
 520        command = [self.filename]
 521        if self._vault_id:
 522            command.extend(['--vault-id', self._vault_id])
 523
 524        return command
 525
 526    def __repr__(self):
 527        if self.filename:
 528            return "%s(filename='%s', vault_id='%s')" % \
 529                (self.__class__.__name__, self.filename, self._vault_id)
 530        return "%s()" % (self.__class__.__name__)
 531
 532
 533def match_secrets(secrets, target_vault_ids):
 534    '''Find all VaultSecret objects that are mapped to any of the target_vault_ids in secrets'''
 535    if not secrets:
 536        return []
 537
 538    matches = [(vault_id, secret) for vault_id, secret in secrets if vault_id in target_vault_ids]
 539    return matches
 540
 541
 542def match_best_secret(secrets, target_vault_ids):
 543    '''Find the best secret from secrets that matches target_vault_ids
 544
 545    Since secrets should be ordered so the early secrets are 'better' than later ones, this
 546    just finds all the matches, then returns the first secret'''
 547    matches = match_secrets(secrets, target_vault_ids)
 548    if matches:
 549        return matches[0]
 550    # raise exception?
 551    return None
 552
 553
 554def match_encrypt_vault_id_secret(secrets, encrypt_vault_id=None):
 555    # See if the --encrypt-vault-id matches a vault-id
 556    display.vvvv('encrypt_vault_id=%s' % encrypt_vault_id)
 557
 558    if encrypt_vault_id is None:
 559        raise AnsibleError('match_encrypt_vault_id_secret requires a non None encrypt_vault_id')
 560
 561    encrypt_vault_id_matchers = [encrypt_vault_id]
 562    encrypt_secret = match_best_secret(secrets, encrypt_vault_id_matchers)
 563
 564    # return the best match for --encrypt-vault-id
 565    if encrypt_secret:
 566        return encrypt_secret
 567
 568    # If we specified a encrypt_vault_id and we couldn't find it, dont
 569    # fallback to using the first/best secret
 570    raise AnsibleVaultError('Did not find a match for --encrypt-vault-id=%s in the known vault-ids %s' % (encrypt_vault_id,
 571                                                                                                          [_v for _v, _vs in secrets]))
 572
 573
 574def match_encrypt_secret(secrets, encrypt_vault_id=None):
 575    '''Find the best/first/only secret in secrets to use for encrypting'''
 576
 577    display.vvvv('encrypt_vault_id=%s' % encrypt_vault_id)
 578    # See if the --encrypt-vault-id matches a vault-id
 579    if encrypt_vault_id:
 580        return match_encrypt_vault_id_secret(secrets,
 581                                             encrypt_vault_id=encrypt_vault_id)
 582
 583    # Find the best/first secret from secrets since we didnt specify otherwise
 584    # ie, consider all of the available secrets as matches
 585    _vault_id_matchers = [_vault_id for _vault_id, dummy in secrets]
 586    best_secret = match_best_secret(secrets, _vault_id_matchers)
 587
 588    # can be empty list sans any tuple
 589    return best_secret
 590
 591
 592class VaultLib:
 593    def __init__(self, secrets=None):
 594        self.secrets = secrets or []
 595        self.cipher_name = None
 596        self.b_version = b'1.2'
 597
 598    def encrypt(self, plaintext, secret=None, vault_id=None):
 599        """Vault encrypt a piece of data.
 600
 601        :arg plaintext: a text or byte string to encrypt.
 602        :returns: a utf-8 encoded byte str of encrypted data.  The string
 603            contains a header identifying this as vault encrypted data and
 604            formatted to newline terminated lines of 80 characters.  This is
 605            suitable for dumping as is to a vault file.
 606
 607        If the string passed in is a text string, it will be encoded to UTF-8
 608        before encryption.
 609        """
 610
 611        if secret is None:
 612            if self.secrets:
 613                dummy, secret = match_encrypt_secret(self.secrets)
 614            else:
 615                raise AnsibleVaultError("A vault password must be specified to encrypt data")
 616
 617        b_plaintext = to_bytes(plaintext, errors='surrogate_or_strict')
 618
 619        if is_encrypted(b_plaintext):
 620            raise AnsibleError("input is already encrypted")
 621
 622        if not self.cipher_name or self.cipher_name not in CIPHER_WRITE_WHITELIST:
 623            self.cipher_name = u"AES256"
 624
 625        try:
 626            this_cipher = CIPHER_MAPPING[self.cipher_name]()
 627        except KeyError:
 628            raise AnsibleError(u"{0} cipher could not be found".format(self.cipher_name))
 629
 630        # encrypt data
 631        if vault_id:
 632            display.vvvvv('Encrypting with vault_id "%s" and vault secret %s' % (vault_id, secret))
 633        else:
 634            display.vvvvv('Encrypting without a vault_id using vault secret %s' % secret)
 635
 636        b_ciphertext = this_cipher.encrypt(b_plaintext, secret)
 637
 638        # format the data for output to the file
 639        b_vaulttext = format_vaulttext_envelope(b_ciphertext,
 640                                                self.cipher_name,
 641                                                vault_id=vault_id)
 642        return b_vaulttext
 643
 644    def decrypt(self, vaulttext, filename=None):
 645        '''Decrypt a piece of vault encrypted data.
 646
 647        :arg vaulttext: a string to decrypt.  Since vault encrypted data is an
 648            ascii text format this can be either a byte str or unicode string.
 649        :kwarg filename: a filename that the data came from.  This is only
 650            used to make better error messages in case the data cannot be
 651            decrypted.
 652        :returns: a byte string containing the decrypted data and the vault-id that was used
 653
 654        '''
 655        plaintext, vault_id, vault_secret = self.decrypt_and_get_vault_id(vaulttext, filename=filename)
 656        return plaintext
 657
 658    def decrypt_and_get_vault_id(self, vaulttext, filename=None):
 659        """Decrypt a piece of vault encrypted data.
 660
 661        :arg vaulttext: a string to decrypt.  Since vault encrypted data is an
 662            ascii text format this can be either a byte str or unicode string.
 663        :kwarg filename: a filename that the data came from.  This is only
 664            used to make better error messages in case the data cannot be
 665            decrypted.
 666        :returns: a byte string containing the decrypted data and the vault-id vault-secret that was used
 667
 668        """
 669        b_vaulttext = to_bytes(vaulttext, errors='strict', encoding='utf-8')
 670
 671        if self.secrets is None:
 672            raise AnsibleVaultError("A vault password must be specified to decrypt data")
 673
 674        if not is_encrypted(b_vaulttext):
 675            msg = "input is not vault encrypted data"
 676            if filename:
 677                msg += "%s is not a vault encrypted file" % to_native(filename)
 678            raise AnsibleError(msg)
 679
 680        b_vaulttext, dummy, cipher_name, vault_id = parse_vaulttext_envelope(b_vaulttext,
 681                                                                             filename=filename)
 682
 683        # create the cipher object, note that the cipher used for decrypt can
 684        # be different than the cipher used for encrypt
 685        if cipher_name in CIPHER_WHITELIST:
 686            this_cipher = CIPHER_MAPPING[cipher_name]()
 687        else:
 688            raise AnsibleError("{0} cipher could not be found".format(cipher_name))
 689
 690        b_plaintext = None
 691
 692        if not self.secrets:
 693            raise AnsibleVaultError('Attempting to decrypt but no vault secrets found')
 694
 695        # WARNING: Currently, the vault id is not required to match the vault id in the vault blob to
 696        #          decrypt a vault properly. The vault id in the vault blob is not part of the encrypted
 697        #          or signed vault payload. There is no cryptographic checking/verification/validation of the
 698        #          vault blobs vault id. It can be tampered with and changed. The vault id is just a nick
 699        #          name to use to pick the best secret and provide some ux/ui info.
 700
 701        # iterate over all the applicable secrets (all of them by default) until one works...
 702        # if we specify a vault_id, only the corresponding vault secret is checked and
 703        # we check it first.
 704
 705        vault_id_matchers = []
 706        vault_id_used = None
 707        vault_secret_used = None
 708
 709        if vault_id:
 710            display.vvvvv('Found a vault_id (%s) in the vaulttext' % (vault_id))
 711            vault_id_matchers.append(vault_id)
 712            _matches = match_secrets(self.secrets, vault_id_matchers)
 713            if _matches:
 714                display.vvvvv('We have a secret associated with vault id (%s), will try to use to decrypt %s' % (vault_id, to_text(filename)))
 715            else:
 716                display.vvvvv('Found a vault_id (%s) in the vault text, but we do not have a associated secret (--vault-id)' % (vault_id))
 717
 718        # Not adding the other secrets to vault_secret_ids enforces a match between the vault_id from the vault_text and
 719        # the known vault secrets.
 720        if not C.DEFAULT_VAULT_ID_MATCH:
 721            # Add all of the known vault_ids as candidates for decrypting a vault.
 722            vault_id_matchers.extend([_vault_id for _vault_id, _dummy in self.secrets if _vault_id != vault_id])
 723
 724        matched_secrets = match_secrets(self.secrets, vault_id_matchers)
 725
 726        # for vault_secret_id in vault_secret_ids:
 727        for vault_secret_id, vault_secret in matched_secrets:
 728            display.vvvvv('Trying to use vault secret=(%s) id=%s to decrypt %s' % (vault_secret, vault_secret_id, to_text(filename)))
 729
 730            try:
 731                # secret = self.secrets[vault_secret_id]
 732                display.vvvv('Trying secret %s for vault_id=%s' % (vault_secret, vault_secret_id))
 733                b_plaintext = this_cipher.decrypt(b_vaulttext, vault_secret)
 734                if b_plaintext is not None:
 735                    vault_id_used = vault_secret_id
 736                    vault_secret_used = vault_secret
 737                    file_slug = ''
 738                    if filename:
 739                        file_slug = ' of "%s"' % filename
 740                    display.vvvvv('Decrypt%s successful with secret=%s and vault_id=%s' % (to_text(file_slug), vault_secret, vault_secret_id))
 741                    break
 742            except AnsibleVaultFormatError as exc:
 743                msg = "There was a vault format error"
 744                if filename:
 745                    msg += ' in %s' % (to_text(filename))
 746                msg += ': %s' % exc
 747                display.warning(msg)
 748                raise
 749            except AnsibleError as e:
 750                display.vvvv('Tried to use the vault secret (%s) to decrypt (%s) but it failed. Error: %s' %
 751                             (vault_secret_id, to_text(filename), e))
 752                continue
 753        else:
 754            msg = "Decryption failed (no vault secrets were found that could decrypt)"
 755            if filename:
 756                msg += " on %s" % to_native(filename)
 757            raise AnsibleVaultError(msg)
 758
 759        if b_plaintext is None:
 760            msg = "Decryption failed"
 761            if filename:
 762                msg += " on %s" % to_native(filename)
 763            raise AnsibleError(msg)
 764
 765        return b_plaintext, vault_id_used, vault_secret_used
 766
 767
 768class VaultEditor:
 769
 770    def __init__(self, vault=None):
 771        # TODO: it may be more useful to just make VaultSecrets and index of VaultLib objects...
 772        self.vault = vault or VaultLib()
 773
 774    # TODO: mv shred file stuff to it's own class
 775    def _shred_file_custom(self, tmp_path):
 776        """"Destroy a file, when shred (core-utils) is not available
 777
 778        Unix `shred' destroys files "so that they can be recovered only with great difficulty with
 779        specialised hardware, if at all". It is based on the method from the paper
 780        "Secure Deletion of Data from Magnetic and Solid-State Memory",
 781        Proceedings of the Sixth USENIX Security Symposium (San Jose, California, July 22-25, 1996).
 782
 783        We do not go to that length to re-implement shred in Python; instead, overwriting with a block
 784        of random data should suffice.
 785
 786        See https://github.com/ansible/ansible/pull/13700 .
 787        """
 788
 789        file_len = os.path.getsize(tmp_path)
 790
 791        if file_len > 0:  # avoid work when file was empty
 792            max_chunk_len = min(1024 * 1024 * 2, file_len)
 793
 794            passes = 3
 795            with open(tmp_path, "wb") as fh:
 796                for _ in range(passes):
 797                    fh.seek(0, 0)
 798                    # get a random chunk of data, each pass with other length
 799                    chunk_len = random.randint(max_chunk_len // 2, max_chunk_len)
 800                    data = os.urandom(chunk_len)
 801
 802                    for _ in range(0, file_len // chunk_len):
 803                        fh.write(data)
 804                    fh.write(data[:file_len % chunk_len])
 805
 806                    # FIXME remove this assert once we have unittests to check its accuracy
 807                    if fh.tell() != file_len:
 808                        raise AnsibleAssertionError()
 809
 810                    os.fsync(fh)
 811
 812    def _shred_file(self, tmp_path):
 813        """Securely destroy a decrypted file
 814
 815        Note standard limitations of GNU shred apply (For flash, overwriting would have no effect
 816        due to wear leveling; for other storage systems, the async kernel->filesystem->disk calls never
 817        guarantee data hits the disk; etc). Furthermore, if your tmp dirs is on tmpfs (ramdisks),
 818        it is a non-issue.
 819
 820        Nevertheless, some form of overwriting the data (instead of just removing the fs index entry) is
 821        a good idea. If shred is not available (e.g. on windows, or no core-utils installed), fall back on
 822        a custom shredding method.
 823        """
 824
 825        if not os.path.isfile(tmp_path):
 826            # file is already gone
 827            return
 828
 829        try:
 830            r = subprocess.call(['shred', tmp_path])
 831        except (OSError, ValueError):
 832            # shred is not available on this system, or some other error occurred.
 833            # ValueError caught because macOS El Capitan is raising an
 834            # exception big enough to hit a limit in python2-2.7.11 and below.
 835            # Symptom is ValueError: insecure pickle when shred is not
 836            # installed there.
 837            r = 1
 838
 839        if r != 0:
 840            # we could not successfully execute unix shred; therefore, do custom shred.
 841            self._shred_file_custom(tmp_path)
 842
 843        os.remove(tmp_path)
 844
 845    def _edit_file_helper(self, filename, secret,
 846                          existing_data=None, force_save=False, vault_id=None):
 847
 848        # Create a tempfile
 849        root, ext = os.path.splitext(os.path.realpath(filename))
 850        fd, tmp_path = tempfile.mkstemp(suffix=ext)
 851        os.close(fd)
 852
 853        cmd = self._editor_shell_command(tmp_path)
 854        try:
 855            if existing_data:
 856                self.write_data(existing_data, tmp_path, shred=False)
 857
 858            # drop the user into an editor on the tmp file
 859            subprocess.call(cmd)
 860        except Exception as e:
 861            # whatever happens, destroy the decrypted file
 862            self._shred_file(tmp_path)
 863            raise AnsibleError('Unable to execute the command "%s": %s' % (' '.join(cmd), to_native(e)))
 864
 865        b_tmpdata = self.read_data(tmp_path)
 866
 867        # Do nothing if the content has not changed
 868        if existing_data == b_tmpdata and not force_save:
 869            self._shred_file(tmp_path)
 870            return
 871
 872        # encrypt new data and write out to tmp
 873        # An existing vaultfile will always be UTF-8,
 874        # so decode to unicode here
 875        b_ciphertext = self.vault.encrypt(b_tmpdata, secret, vault_id=vault_id)
 876        self.write_data(b_ciphertext, tmp_path)
 877
 878        # shuffle tmp file into place
 879        self.shuffle_files(tmp_path, filename)
 880        display.vvvvv('Saved edited file "%s" encrypted using %s and  vault id "%s"' % (filename, secret, vault_id))
 881
 882    def _real_path(self, filename):
 883        # '-' is special to VaultEditor, dont expand it.
 884        if filename == '-':
 885            return filename
 886
 887        real_path = os.path.realpath(filename)
 888        return real_path
 889
 890    def encrypt_bytes(self, b_plaintext, secret, vault_id=None):
 891
 892        b_ciphertext = self.vault.encrypt(b_plaintext, secret, vault_id=vault_id)
 893
 894        return b_ciphertext
 895
 896    def encrypt_file(self, filename, secret, vault_id=None, output_file=None):
 897
 898        # A file to be encrypted into a vaultfile could be any encoding
 899        # so treat the contents as a byte string.
 900
 901        # follow the symlink
 902        filename = self._real_path(filename)
 903
 904        b_plaintext = self.read_data(filename)
 905        b_ciphertext = self.vault.encrypt(b_plaintext, secret, vault_id=vault_id)
 906        self.write_data(b_ciphertext, output_file or filename)
 907
 908    def decrypt_file(self, filename, output_file=None):
 909
 910        # follow the symlink
 911        filename = self._real_path(filename)
 912
 913        ciphertext = self.read_data(filename)
 914
 915        try:
 916            plaintext = self.vault.decrypt(ciphertext, filename=filename)
 917        except AnsibleError as e:
 918            raise AnsibleError("%s for %s" % (to_native(e), to_native(filename)))
 919        self.write_data(plaintext, output_file or filename, shred=False)
 920
 921    def create_file(self, filename, secret, vault_id=None):
 922        """ create a new encrypted file """
 923
 924        dirname = os.path.dirname(filename)
 925        if dirname and not os.path.exists(dirname):
 926            display.warning("%s does not exist, creating..." % dirname)
 927            makedirs_safe(dirname)
 928
 929        # FIXME: If we can raise an error here, we can probably just make it
 930        # behave like edit instead.
 931        if os.path.isfile(filename):
 932            raise AnsibleError("%s exists, please use 'edit' instead" % filename)
 933
 934        self._edit_file_helper(filename, secret, vault_id=vault_id)
 935
 936    def edit_file(self, filename):
 937        vault_id_used = None
 938        vault_secret_used = None
 939        # follow the symlink
 940        filename = self._real_path(filename)
 941
 942        b_vaulttext = self.read_data(filename)
 943
 944        # vault or yaml files are always utf8
 945        vaulttext = to_text(b_vaulttext)
 946
 947        try:
 948            # vaulttext gets converted back to bytes, but alas
 949            # TODO: return the vault_id that worked?
 950            plaintext, vault_id_used, vault_secret_used = self.vault.decrypt_and_get_vault_id(vaulttext)
 951        except AnsibleError as e:
 952            raise AnsibleError("%s for %s" % (to_native(e), to_native(filename)))
 953
 954        # Figure out the vault id from the file, to select the right secret to re-encrypt it
 955        # (duplicates parts of decrypt, but alas...)
 956        dummy, dummy, cipher_name, vault_id = parse_vaulttext_envelope(b_vaulttext,
 957                                                                       filename=filename)
 958
 959        # vault id here may not be the vault id actually used for decrypting
 960        # as when the edited file has no vault-id but is decrypted by non-default id in secrets
 961        # (vault_id=default, while a different vault-id decrypted)
 962
 963        # Keep the same vault-id (and version) as in the header
 964        if cipher_name not in CIPHER_WRITE_WHITELIST:
 965            # we want to get rid of files encrypted with the AES cipher
 966            self._edit_file_helper(filename, vault_secret_used, existing_data=plaintext,
 967                                   force_save=True, vault_id=vault_id)
 968        else:
 969            self._edit_file_helper(filename, vault_secret_used, existing_data=plaintext,
 970                                   force_save=False, vault_id=vault_id)
 971
 972    def plaintext(self, filename):
 973
 974        b_vaulttext = self.read_data(filename)
 975        vaulttext = to_text(b_vaulttext)
 976
 977        try:
 978            plaintext = self.vault.decrypt(vaulttext, filename=filename)
 979            return plaintext
 980        except AnsibleError as e:
 981            raise AnsibleVaultError("%s for %s" % (to_native(e), to_native(filename)))
 982
 983    # FIXME/TODO: make this use VaultSecret
 984    def rekey_file(self, filename, new_vault_secret, new_vault_id=None):
 985
 986        # follow the symlink
 987        filename = self._real_path(filename)
 988
 989        prev = os.stat(filename)
 990        b_vaulttext = self.read_data(filename)
 991        vaulttext = to_text(b_vaulttext)
 992
 993        display.vvvvv('Rekeying file "%s" to with new vault-id "%s" and vault secret %s' %
 994                      (filename, new_vault_id, new_vault_secret))
 995        try:
 996            plaintext, vault_id_used, _dummy = self.vault.decrypt_and_get_vault_id(vaulttext)
 997        except AnsibleError as e:
 998            raise AnsibleError("%s for %s" % (to_native(e), to_native(filename)))
 999
1000        # This is more or less an assert, see #18247
1001        if new_vault_secret is None:
1002            raise AnsibleError('The value for the new_password to rekey %s with is not valid' % filename)
1003
1004        # FIXME: VaultContext...?  could rekey to a different vault_id in the same VaultSecrets
1005
1006        # Need a new VaultLib because the new vault data can be a different
1007        # vault lib format or cipher (for ex, when we migrate 1.0 style vault data to
1008        # 1.1 style data we change the version and the cipher). This is where a VaultContext might help
1009
1010        # the new vault will only be used for encrypting, so it doesn't need the vault secrets
1011        # (we will pass one in directly to encrypt)
1012        new_vault = VaultLib(secrets={})
1013        b_new_vaulttext = new_vault.encrypt(plaintext, new_vault_secret, vault_id=new_vault_id)
1014
1015        self.write_data(b_new_vaulttext, filename)
1016
1017        # preserve permissions
1018        os.chmod(filename, prev.st_mode)
1019        os.chown(filename, prev.st_uid, prev.st_gid)
1020
1021        display.vvvvv('Rekeyed file "%s" (decrypted with vault id "%s") was encrypted with new vault-id "%s" and vault secret %s' %
1022                      (filename, vault_id_used, new_vault_id, new_vault_secret))
1023
1024    def read_data(self, filename):
1025
1026        try:
1027            if filename == '-':
1028                data = sys.stdin.read()
1029            else:
1030                with open(filename, "rb") as fh:
1031                    data = fh.read()
1032        except Exception as e:
1033            msg = to_native(e)
1034            if not msg:
1035                msg = repr(e)
1036            raise AnsibleError('Unable to read source file (%s): %s' % (to_native(filename), msg))
1037
1038        return data
1039
1040    # TODO: add docstrings for arg types since this code is picky about that
1041    def write_data(self, data, filename, shred=True):
1042        """Write the data bytes to given path
1043
1044        This is used to write a byte string to a file or stdout. It is used for
1045        writing the results of vault encryption or decryption. It is used for
1046        saving the ciphertext after encryption and it is also used for saving the
1047        plaintext after decrypting a vault. The type of the 'data' arg should be bytes,
1048        since in the plaintext case, the original contents can be of any text encoding
1049        or arbitrary binary data.
1050
1051        When used to write the result of vault encryption, the val of the 'data' arg
1052        should be a utf-8 encoded byte string and not a text typ and not a text type..
1053
1054        When used to write the result of vault decryption, the val of the 'data' arg
1055        should be a byte string and not a text type.
1056
1057        :arg data: the byte string (bytes) data
1058        :arg filename: filename to save 'data' to.
1059        :arg shred: if shred==True, make sure that the original data is first shredded so that is cannot be recovered.
1060        :returns: None
1061        """
1062        # FIXME: do we need this now? data_bytes should always be a utf-8 byte string
1063        b_file_data = to_bytes(data, errors='strict')
1064
1065        # get a ref to either sys.stdout.buffer for py3 or plain old sys.stdout for py2
1066        # We need sys.stdout.buffer on py3 so we can write bytes to it since the plaintext
1067        # of the vaulted object could be anything/binary/etc
1068        output = getattr(sys.stdout, 'buffer', sys.stdout)
1069
1070        if filename == '-':
1071            output.write(b_file_data)
1072        else:
1073            if os.path.isfile(filename):
1074                if shred:
1075                    self._shred_file(filename)
1076                else:
1077                    os.remove(filename)
1078            with open(filename, "wb") as fh:
1079                fh.write(b_file_data)
1080
1081    def shuffle_files(self, src, dest):
1082        prev = None
1083        # overwrite dest with src
1084        if os.path.isfile(dest):
1085            prev = os.stat(dest)
1086            # old file 'dest' was encrypted, no need to _shred_file
1087            os.remove(dest)
1088        shutil.move(src, dest)
1089
1090        # reset permissions if needed
1091        if prev is not None:
1092            # TODO: selinux, ACLs, xattr?
1093            os.chmod(dest, prev.st_mode)
1094            os.chown(dest, prev.st_uid, prev.st_gid)
1095
1096    def _editor_shell_command(self, filename):
1097        env_editor = os.environ.get('EDITOR', 'vi')
1098        editor = shlex.split(env_editor)
1099        editor.append(filename)
1100
1101        return editor
1102
1103
1104########################################
1105#               CIPHERS                #
1106########################################
1107
1108class VaultAES256:
1109
1110    """
1111    Vault implementation using AES-CTR with an HMAC-SHA256 authentication code.
1112    Keys are derived using PBKDF2
1113    """
1114
1115    # http://www.daemonology.net/blog/2009-06-11-cryptographic-right-answers.html
1116
1117    # Note: strings in this class should be byte strings by default.
1118
1119    def __init__(self):
1120        if not HAS_CRYPTOGRAPHY and not HAS_PYCRYPTO:
1121            raise AnsibleError(NEED_CRYPTO_LIBRARY)
1122
1123    @staticmethod
1124    def _create_key_cryptography(b_password, b_salt, key_length, iv_length):
1125        kdf = PBKDF2HMAC(
1126            algorithm=hashes.SHA256(),
1127            length=2 * key_length + iv_length,
1128            salt=b_salt,
1129            iterations=10000,
1130            backend=CRYPTOGRAPHY_BACKEND)
1131        b_derivedkey = kdf.derive(b_password)
1132
1133        return b_derivedkey
1134
1135    @staticmethod
1136    def _pbkdf2_prf(p, s):
1137        hash_function = SHA256_pycrypto
1138        return HMAC_pycrypto.new(p, s, hash_function).digest()
1139
1140    @classmethod
1141    def _create_key_pycrypto(cls, b_password, b_salt, key_length, iv_length):
1142
1143        # make two keys and one iv
1144
1145        b_derivedkey = PBKDF2_pycrypto(b_password, b_salt, dkLen=(2 * key_length) + iv_length,
1146                                       count=10000, prf=cls._pbkdf2_prf)
1147        return b_derivedkey
1148
1149    @classmethod
1150    def _gen_key_initctr(cls, b_password, b_salt):
1151        # 16 for AES 128, 32 for AES256
1152        key_length = 32
1153
1154        if HAS_CRYPTOGRAPHY:
1155            # AES is a 128-bit block cipher, so IVs and counter nonces are 16 bytes
1156            iv_length = algorithms.AES.block_size // 8
1157
1158            b_derivedkey = cls._create_key_cryptography(b_password, b_salt, key_length, iv_length)
1159            b_iv = b_derivedkey[(key_length * 2):(key_length * 2) + iv_length]
1160        elif HAS_PYCRYPTO:
1161            # match the size used for counter.new to avoid extra work
1162            iv_length = 16
1163
1164            b_derivedkey = cls._create_key_pycrypto(b_password, b_salt, key_length, iv_length)
1165            b_iv = hexlify(b_derivedkey[(key_length * 2):(key_length * 2) + iv_length])
1166        else:
1167            raise AnsibleError(NEED_CRYPTO_LIBRARY + '(Detected in initctr)')
1168
1169        b_key1 = b_derivedkey[:key_length]
1170        b_key2 = b_derivedkey[key_length:(key_length * 2)]
1171
1172        return b_key1, b_key2, b_iv
1173
1174    @staticmethod
1175    def _encrypt_cryptography(b_plaintext, b_key1, b_key2, b_iv):
1176        cipher = C_Cipher(algorithms.AES(b_key1), modes.CTR(b_iv), CRYPTOGRAPHY_BACKEND)
1177        encryptor = cipher.encryptor()
1178        padder = padding.PKCS7(algorithms.AES.block_size).padder()
1179        b_ciphertext = encryptor.update(padder.update(b_plaintext) + padder.finalize())
1180        b_ciphertext += encryptor.finalize()
1181
1182        # COMBINE SALT, DIGEST AND DATA
1183        hmac = HMAC(b_key2, hashes.SHA256(), CRYPTOGRAPHY_BACKEND)
1184        hmac.update(b_ciphertext)
1185        b_hmac = hmac.finalize()
1186
1187        return to_bytes(hexlify(b_hmac), errors='surrogate_or_strict'), hexlify(b_ciphertext)
1188
1189    @staticmethod
1190    def _encrypt_pycrypto(b_plaintext, b_key1, b_key2, b_iv):
1191        # PKCS#7 PAD DATA http://tools.ietf.org/html/rfc5652#section-6.3
1192        bs = AES_pycrypto.block_size
1193        padding_length = (bs - len(b_plaintext) % bs) or bs
1194        b_plaintext += to_bytes(padding_length * chr(padding_length), encoding='ascii', errors='strict')
1195
1196        # COUNTER.new PARAMETERS
1197        # 1) nbits (integer) - Length of the counter, in bits.
1198        # 2) initial_value (integer) - initial value of the counter. "iv" from _gen_key_initctr
1199
1200        ctr = Counter_pycrypto.new(128, initial_value=int(b_iv, 16))
1201
1202        # AES.new PARAMETERS
1203        # 1) AES key, must be either 16, 24, or 32 bytes long -- "key" from _gen_key_initctr
1204        # 2) MODE_CTR, is the recommended mode
1205        # 3) counter=<CounterObject>
1206
1207        cipher = AES_pycrypto.new(b_key1, AES_pycrypto.MODE_CTR, counter=ctr)
1208
1209        # ENCRYPT PADDED DATA
1210        b_ciphertext = cipher.encrypt(b_plaintext)
1211
1212        # COMBINE SALT, DIGEST AND DATA
1213        hmac = HMAC_pycrypto.new(b_key2, b_ciphertext, SHA256_pycrypto)
1214
1215        return to_bytes(hmac.hexdigest(), errors='surrogate_or_strict'), hexlify(b_ciphertext)
1216
1217    @classmethod
1218    def encrypt(cls, b_plaintext, secret):
1219        if secret is None:
1220            raise AnsibleVaultError('The secret passed to encrypt() was None')
1221        b_salt = os.urandom(32)
1222        b_password = secret.bytes
1223        b_key1, b_key2, b_iv = cls._gen_key_initctr(b_password, b_salt)
1224
1225        if HAS_CRYPTOGRAPHY:
1226            b_hmac, b_ciphertext = cls._encrypt_cryptography(b_plaintext, b_key1, b_key2, b_iv)
1227        elif HAS_PYCRYPTO:
1228            b_hmac, b_ciphertext = cls._encrypt_pycrypto(b_plaintext, b_key1, b_key2, b_iv)
1229        else:
1230            raise AnsibleError(NEED_CRYPTO_LIBRARY + '(Detected in encrypt)')
1231
1232        b_vaulttext = b'\n'.join([hexlify(b_salt), b_hmac, b_ciphertext])
1233        # Unnecessary but getting rid of it is a backwards incompatible vault
1234        # format change
1235        b_vaulttext = hexlify(b_vaulttext)
1236        return b_vaulttext
1237
1238    @classmethod
1239    def _decrypt_cryptography(cls, b_ciphertext, b_crypted_hmac, b_key1, b_key2, b_iv):
1240        # b_key1, b_key2, b_iv = self._gen_key_initctr(b_password, b_salt)
1241        # EXIT EARLY IF DIGEST DOESN'T MATCH
1242        hmac = HMAC(b_key2, hashes.SHA256(), CRYPTOGRAPHY_BACKEND)
1243        hmac.update(b_ciphertext)
1244        try:
1245            hmac.verify(_unhexlify(b_crypted_hmac))
1246        except InvalidSignature as e:
1247            raise AnsibleVaultError('HMAC verification failed: %s' % e)
1248
1249        cipher = C_Cipher(algorithms.AES(b_key1), modes.CTR(b_iv), CRYPTOGRAPHY_BACKEND)
1250        decryptor = cipher.decryptor()
1251        unpadder = padding.PKCS7(128).unpadder()
1252        b_plaintext = unpadder.update(
1253            decryptor.update(b_ciphertext) + decryptor.finalize()
1254        ) + unpadder.finalize()
1255
1256        return b_plaintext
1257
1258    @staticmethod
1259    def _is_equal(b_a, b_b):
1260        """
1261        Comparing 2 byte arrrays in constant time
1262        to avoid timing attacks.
1263
1264        It would be nice if there was a library for this but
1265        hey.
1266        """
1267        if not (isinstance(b_a, binary_type) and isinstance(b_b, binary_type)):
1268            raise TypeError('_is_equal can only be used to compare two byte strings')
1269
1270        # http://codahale.com/a-lesson-in-timing-attacks/
1271        if len(b_a) != 

Large files files are truncated, but you can click here to view the full file