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