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    @staticmethod
603    def is_encrypted(vaulttext):
604        return is_encrypted(vaulttext)
605
606    def encrypt(self, plaintext, secret=None, vault_id=None):
607        """Vault encrypt a piece of data.
608
609        :arg plaintext: a text or byte string to encrypt.
610        :returns: a utf-8 encoded byte str of encrypted data.  The string
611            contains a header identifying this as vault encrypted data and
612            formatted to newline terminated lines of 80 characters.  This is
613            suitable for dumping as is to a vault file.
614
615        If the string passed in is a text string, it will be encoded to UTF-8
616        before encryption.
617        """
618
619        if secret is None:
620            if self.secrets:
621                dummy, secret = match_encrypt_secret(self.secrets)
622            else:
623                raise AnsibleVaultError("A vault password must be specified to encrypt data")
624
625        b_plaintext = to_bytes(plaintext, errors='surrogate_or_strict')
626
627        if is_encrypted(b_plaintext):
628            raise AnsibleError("input is already encrypted")
629
630        if not self.cipher_name or self.cipher_name not in CIPHER_WRITE_WHITELIST:
631            self.cipher_name = u"AES256"
632
633        try:
634            this_cipher = CIPHER_MAPPING[self.cipher_name]()
635        except KeyError:
636            raise AnsibleError(u"{0} cipher could not be found".format(self.cipher_name))
637
638        # encrypt data
639        if vault_id:
640            display.vvvvv(u'Encrypting with vault_id "%s" and vault secret %s' % (to_text(vault_id), to_text(secret)))
641        else:
642            display.vvvvv(u'Encrypting without a vault_id using vault secret %s' % to_text(secret))
643
644        b_ciphertext = this_cipher.encrypt(b_plaintext, secret)
645
646        # format the data for output to the file
647        b_vaulttext = format_vaulttext_envelope(b_ciphertext,
648                                                self.cipher_name,
649                                                vault_id=vault_id)
650        return b_vaulttext
651
652    def decrypt(self, vaulttext, filename=None, obj=None):
653        '''Decrypt a piece of vault encrypted data.
654
655        :arg vaulttext: a string to decrypt.  Since vault encrypted data is an
656            ascii text format this can be either a byte str or unicode string.
657        :kwarg filename: a filename that the data came from.  This is only
658            used to make better error messages in case the data cannot be
659            decrypted.
660        :returns: a byte string containing the decrypted data and the vault-id that was used
661
662        '''
663        plaintext, vault_id, vault_secret = self.decrypt_and_get_vault_id(vaulttext, filename=filename, obj=obj)
664        return plaintext
665
666    def decrypt_and_get_vault_id(self, vaulttext, filename=None, obj=None):
667        """Decrypt a piece of vault encrypted data.
668
669        :arg vaulttext: a string to decrypt.  Since vault encrypted data is an
670            ascii text format this can be either a byte str or unicode string.
671        :kwarg filename: a filename that the data came from.  This is only
672            used to make better error messages in case the data cannot be
673            decrypted.
674        :returns: a byte string containing the decrypted data and the vault-id vault-secret that was used
675
676        """
677        b_vaulttext = to_bytes(vaulttext, errors='strict', encoding='utf-8')
678
679        if self.secrets is None:
680            raise AnsibleVaultError("A vault password must be specified to decrypt data")
681
682        if not is_encrypted(b_vaulttext):
683            msg = "input is not vault encrypted data. "
684            if filename:
685                msg += "%s is not a vault encrypted file" % to_native(filename)
686            raise AnsibleError(msg)
687
688        b_vaulttext, dummy, cipher_name, vault_id = parse_vaulttext_envelope(b_vaulttext,
689                                                                             filename=filename)
690
691        # create the cipher object, note that the cipher used for decrypt can
692        # be different than the cipher used for encrypt
693        if cipher_name in CIPHER_WHITELIST:
694            this_cipher = CIPHER_MAPPING[cipher_name]()
695        else:
696            raise AnsibleError("{0} cipher could not be found".format(cipher_name))
697
698        b_plaintext = None
699
700        if not self.secrets:
701            raise AnsibleVaultError('Attempting to decrypt but no vault secrets found')
702
703        # WARNING: Currently, the vault id is not required to match the vault id in the vault blob to
704        #          decrypt a vault properly. The vault id in the vault blob is not part of the encrypted
705        #          or signed vault payload. There is no cryptographic checking/verification/validation of the
706        #          vault blobs vault id. It can be tampered with and changed. The vault id is just a nick
707        #          name to use to pick the best secret and provide some ux/ui info.
708
709        # iterate over all the applicable secrets (all of them by default) until one works...
710        # if we specify a vault_id, only the corresponding vault secret is checked and
711        # we check it first.
712
713        vault_id_matchers = []
714        vault_id_used = None
715        vault_secret_used = None
716
717        if vault_id:
718            display.vvvvv(u'Found a vault_id (%s) in the vaulttext' % to_text(vault_id))
719            vault_id_matchers.append(vault_id)
720            _matches = match_secrets(self.secrets, vault_id_matchers)
721            if _matches:
722                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)))
723            else:
724                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))
725
726        # Not adding the other secrets to vault_secret_ids enforces a match between the vault_id from the vault_text and
727        # the known vault secrets.
728        if not C.DEFAULT_VAULT_ID_MATCH:
729            # Add all of the known vault_ids as candidates for decrypting a vault.
730            vault_id_matchers.extend([_vault_id for _vault_id, _dummy in self.secrets if _vault_id != vault_id])
731
732        matched_secrets = match_secrets(self.secrets, vault_id_matchers)
733
734        # for vault_secret_id in vault_secret_ids:
735        for vault_secret_id, vault_secret in matched_secrets:
736            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)))
737
738            try:
739                # secret = self.secrets[vault_secret_id]
740                display.vvvv(u'Trying secret %s for vault_id=%s' % (to_text(vault_secret), to_text(vault_secret_id)))
741                b_plaintext = this_cipher.decrypt(b_vaulttext, vault_secret)
742                if b_plaintext is not None:
743                    vault_id_used = vault_secret_id
744                    vault_secret_used = vault_secret
745                    file_slug = ''
746                    if filename:
747                        file_slug = ' of "%s"' % filename
748                    display.vvvvv(
749                        u'Decrypt%s successful with secret=%s and vault_id=%s' % (to_text(file_slug), to_text(vault_secret), to_text(vault_secret_id))
750                    )
751                    break
752            except AnsibleVaultFormatError as exc:
753                exc.obj = obj
754                msg = u"There was a vault format error"
755                if filename:
756                    msg += u' in %s' % (to_text(filename))
757                msg += u': %s' % to_text(exc)
758                display.warning(msg, formatted=True)
759                raise
760            except AnsibleError as e:
761                display.vvvv(u'Tried to use the vault secret (%s) to decrypt (%s) but it failed. Error: %s' %
762                             (to_text(vault_secret_id), to_text(filename), e))
763                continue
764        else:
765            msg = "Decryption failed (no vault secrets were found that could decrypt)"
766            if filename:
767                msg += " on %s" % to_native(filename)
768            raise AnsibleVaultError(msg)
769
770        if b_plaintext is None:
771            msg = "Decryption failed"
772            if filename:
773                msg += " on %s" % to_native(filename)
774            raise AnsibleError(msg)
775
776        return b_plaintext, vault_id_used, vault_secret_used
777
778
779class VaultEditor:
780
781    def __init__(self, vault=None):
782        # TODO: it may be more useful to just make VaultSecrets and index of VaultLib objects...
783        self.vault = vault or VaultLib()
784
785    # TODO: mv shred file stuff to it's own class
786    def _shred_file_custom(self, tmp_path):
787        """"Destroy a file, when shred (core-utils) is not available
788
789        Unix `shred' destroys files "so that they can be recovered only with great difficulty with
790        specialised hardware, if at all". It is based on the method from the paper
791        "Secure Deletion of Data from Magnetic and Solid-State Memory",
792        Proceedings of the Sixth USENIX Security Symposium (San Jose, California, July 22-25, 1996).
793
794        We do not go to that length to re-implement shred in Python; instead, overwriting with a block
795        of random data should suffice.
796
797        See https://github.com/ansible/ansible/pull/13700 .
798        """
799
800        file_len = os.path.getsize(tmp_path)
801
802        if file_len > 0:  # avoid work when file was empty
803            max_chunk_len = min(1024 * 1024 * 2, file_len)
804
805            passes = 3
806            with open(tmp_path, "wb") as fh:
807                for _ in range(passes):
808                    fh.seek(0, 0)
809                    # get a random chunk of data, each pass with other length
810                    chunk_len = random.randint(max_chunk_len // 2, max_chunk_len)
811                    data = os.urandom(chunk_len)
812
813                    for _ in range(0, file_len // chunk_len):
814                        fh.write(data)
815                    fh.write(data[:file_len % chunk_len])
816
817                    # FIXME remove this assert once we have unittests to check its accuracy
818                    if fh.tell() != file_len:
819                        raise AnsibleAssertionError()
820
821                    os.fsync(fh)
822
823    def _shred_file(self, tmp_path):
824        """Securely destroy a decrypted file
825
826        Note standard limitations of GNU shred apply (For flash, overwriting would have no effect
827        due to wear leveling; for other storage systems, the async kernel->filesystem->disk calls never
828        guarantee data hits the disk; etc). Furthermore, if your tmp dirs is on tmpfs (ramdisks),
829        it is a non-issue.
830
831        Nevertheless, some form of overwriting the data (instead of just removing the fs index entry) is
832        a good idea. If shred is not available (e.g. on windows, or no core-utils installed), fall back on
833        a custom shredding method.
834        """
835
836        if not os.path.isfile(tmp_path):
837            # file is already gone
838            return
839
840        try:
841            r = subprocess.call(['shred', tmp_path])
842        except (OSError, ValueError):
843            # shred is not available on this system, or some other error occurred.
844            # ValueError caught because macOS El Capitan is raising an
845            # exception big enough to hit a limit in python2-2.7.11 and below.
846            # Symptom is ValueError: insecure pickle when shred is not
847            # installed there.
848            r = 1
849
850        if r != 0:
851            # we could not successfully execute unix shred; therefore, do custom shred.
852            self._shred_file_custom(tmp_path)
853
854        os.remove(tmp_path)
855
856    def _edit_file_helper(self, filename, secret, existing_data=None, force_save=False, vault_id=None):
857
858        # Create a tempfile
859        root, ext = os.path.splitext(os.path.realpath(filename))
860        fd, tmp_path = tempfile.mkstemp(suffix=ext, dir=C.DEFAULT_LOCAL_TMP)
861
862        cmd = self._editor_shell_command(tmp_path)
863        try:
864            if existing_data:
865                self.write_data(existing_data, fd, shred=False)
866        except Exception:
867            # if an error happens, destroy the decrypted file
868            self._shred_file(tmp_path)
869            raise
870        finally:
871            os.close(fd)
872
873        try:
874            # drop the user into an editor on the tmp file
875            subprocess.call(cmd)
876        except Exception as e:
877            # if an error happens, destroy the decrypted file
878            self._shred_file(tmp_path)
879            raise AnsibleError('Unable to execute the command "%s": %s' % (' '.join(cmd), to_native(e)))
880
881        b_tmpdata = self.read_data(tmp_path)
882
883        # Do nothing if the content has not changed
884        if force_save or existing_data != b_tmpdata:
885
886            # encrypt new data and write out to tmp
887            # An existing vaultfile will always be UTF-8,
888            # so decode to unicode here
889            b_ciphertext = self.vault.encrypt(b_tmpdata, secret, vault_id=vault_id)
890            self.write_data(b_ciphertext, tmp_path)
891
892            # shuffle tmp file into place
893            self.shuffle_files(tmp_path, filename)
894            display.vvvvv(u'Saved edited file "%s" encrypted using %s and  vault id "%s"' % (to_text(filename), to_text(secret), to_text(vault_id)))
895
896        # always shred temp, jic
897        self._shred_file(tmp_path)
898
899    def _real_path(self, filename):
900        # '-' is special to VaultEditor, dont expand it.
901        if filename == '-':
902            return filename
903
904        real_path = os.path.realpath(filename)
905        return real_path
906
907    def encrypt_bytes(self, b_plaintext, secret, vault_id=None):
908
909        b_ciphertext = self.vault.encrypt(b_plaintext, secret, vault_id=vault_id)
910
911        return b_ciphertext
912
913    def encrypt_file(self, filename, secret, vault_id=None, output_file=None):
914
915        # A file to be encrypted into a vaultfile could be any encoding
916        # so treat the contents as a byte string.
917
918        # follow the symlink
919        filename = self._real_path(filename)
920
921        b_plaintext = self.read_data(filename)
922        b_ciphertext = self.vault.encrypt(b_plaintext, secret, vault_id=vault_id)
923        self.write_data(b_ciphertext, output_file or filename)
924
925    def decrypt_file(self, filename, output_file=None):
926
927        # follow the symlink
928        filename = self._real_path(filename)
929
930        ciphertext = self.read_data(filename)
931
932        try:
933            plaintext = self.vault.decrypt(ciphertext, filename=filename)
934        except AnsibleError as e:
935            raise AnsibleError("%s for %s" % (to_native(e), to_native(filename)))
936        self.write_data(plaintext, output_file or filename, shred=False)
937
938    def create_file(self, filename, secret, vault_id=None):
939        """ create a new encrypted file """
940
941        dirname = os.path.dirname(filename)
942        if dirname and not os.path.exists(dirname):
943            display.warning(u"%s does not exist, creating..." % to_text(dirname))
944            makedirs_safe(dirname)
945
946        # FIXME: If we can raise an error here, we can probably just make it
947        # behave like edit instead.
948        if os.path.isfile(filename):
949            raise AnsibleError("%s exists, please use 'edit' instead" % filename)
950
951        self._edit_file_helper(filename, secret, vault_id=vault_id)
952
953    def edit_file(self, filename):
954        vault_id_used = None
955        vault_secret_used = None
956        # follow the symlink
957        filename = self._real_path(filename)
958
959        b_vaulttext = self.read_data(filename)
960
961        # vault or yaml files are always utf8
962        vaulttext = to_text(b_vaulttext)
963
964        try:
965            # vaulttext gets converted back to bytes, but alas
966            # TODO: return the vault_id that worked?
967            plaintext, vault_id_used, vault_secret_used = self.vault.decrypt_and_get_vault_id(vaulttext)
968        except AnsibleError as e:
969            raise AnsibleError("%s for %s" % (to_native(e), to_native(filename)))
970
971        # Figure out the vault id from the file, to select the right secret to re-encrypt it
972        # (duplicates parts of decrypt, but alas...)
973        dummy, dummy, cipher_name, vault_id = parse_vaulttext_envelope(b_vaulttext, filename=filename)
974
975        # vault id here may not be the vault id actually used for decrypting
976        # as when the edited file has no vault-id but is decrypted by non-default id in secrets
977        # (vault_id=default, while a different vault-id decrypted)
978
979        # we want to get rid of files encrypted with the AES cipher
980        force_save = (cipher_name not in CIPHER_WRITE_WHITELIST)
981
982        # Keep the same vault-id (and version) as in the header
983        self._edit_file_helper(filename, vault_secret_used, existing_data=plaintext, force_save=force_save, vault_id=vault_id)
984
985    def plaintext(self, filename):
986
987        b_vaulttext = self.read_data(filename)
988        vaulttext = to_text(b_vaulttext)
989
990        try:
991            plaintext = self.vault.decrypt(vaulttext, filename=filename)
992            return plaintext
993        except AnsibleError as e:
994            raise AnsibleVaultError("%s for %s" % (to_native(e), to_native(filename)))
995
996    # FIXME/TODO: make this use VaultSecret
997    def rekey_file(self, filename, new_vault_secret, new_vault_id=None):
998
999        # follow the symlink
1000        filename = self._real_path(filename)
1001
1002        prev = os.stat(filename)
1003        b_vaulttext = self.read_data(filename)
1004        vaulttext = to_text(b_vaulttext)
1005
1006        display.vvvvv(u'Rekeying file "%s" to with new vault-id "%s" and vault secret %s' %
1007                      (to_text(filename), to_text(new_vault_id), to_text(new_vault_secret)))
1008        try:
1009            plaintext, vault_id_used, _dummy = self.vault.decrypt_and_get_vault_id(vaulttext)
1010        except AnsibleError as e:
1011            raise AnsibleError("%s for %s" % (to_native(e), to_native(filename)))
1012
1013        # This is more or less an assert, see #18247
1014        if new_vault_secret is None:
1015            raise AnsibleError('The value for the new_password to rekey %s with is not valid' % filename)
1016
1017        # FIXME: VaultContext...?  could rekey to a different vault_id in the same VaultSecrets
1018
1019        # Need a new VaultLib because the new vault data can be a different
1020        # vault lib format or cipher (for ex, when we migrate 1.0 style vault data to
1021        # 1.1 style data we change the version and the cipher). This is where a VaultContext might help
1022
1023        # the new vault will only be used for encrypting, so it doesn't need the vault secrets
1024        # (we will pass one in directly to encrypt)
1025        new_vault = VaultLib(secrets={})
1026        b_new_vaulttext = new_vault.encrypt(plaintext, new_vault_secret, vault_id=new_vault_id)
1027
1028        self.write_data(b_new_vaulttext, filename)
1029
1030        # preserve permissions
1031        os.chmod(filename, prev.st_mode)
1032        os.chown(filename, prev.st_uid, prev.st_gid)
1033
1034        display.vvvvv(u'Rekeyed file "%s" (decrypted with vault id "%s") was encrypted with new vault-id "%s" and vault secret %s' %
1035                      (to_text(filename), to_text(vault_id_used), to_text(new_vault_id), to_text(new_vault_secret)))
1036
1037    def read_data(self, filename):
1038
1039        try:
1040            if filename == '-':
1041                if PY3:
1042                    data = sys.stdin.buffer.read()
1043                else:
1044                    data = sys.stdin.read()
1045            else:
1046                with open(filename, "rb") as fh:
1047                    data = fh.read()
1048        except Exception as e:
1049            msg = to_native(e)
1050            if not msg:
1051                msg = repr(e)
1052            raise AnsibleError('Unable to read source file (%s): %s' % (to_native(filename), msg))
1053
1054        return data
1055
1056    def write_data(self, data, thefile, shred=True, mode=0o600):
1057        # TODO: add docstrings for arg types since this code is picky about that
1058        """Write the data bytes to given path
1059
1060        This is used to write a byte string to a file or stdout. It is used for
1061        writing the results of vault encryption or decryption. It is used for
1062        saving the ciphertext after encryption and it is also used for saving the
1063        plaintext after decrypting a vault. The type of the 'data' arg should be bytes,
1064        since in the plaintext case, the original contents can be of any text encoding
1065        or arbitrary binary data.
1066
1067        When used to write the result of vault encryption, the val of the 'data' arg
1068        should be a utf-8 encoded byte string and not a text typ and not a text type..
1069
1070        When used to write the result of vault decryption, the val of the 'data' arg
1071        should be a byte string and not a text type.
1072
1073        :arg data: the byte string (bytes) data
1074        :arg thefile: file descriptor or filename to save 'data' to.
1075        :arg shred: if shred==True, make sure that the original data is first shredded so that is cannot be recovered.
1076        :returns: None
1077        """
1078        # FIXME: do we need this now? data_bytes should always be a utf-8 byte string
1079        b_file_data = to_bytes(data, errors='strict')
1080
1081        # check if we have a file descriptor instead of a path
1082        is_fd = False
1083        try:
1084            is_fd = (isinstance(thefile, int) and fcntl.fcntl(thefile, fcntl.F_GETFD) != -1)
1085        except Exception:
1086            pass
1087
1088        if is_fd:
1089            # if passed descriptor, use that to ensure secure access, otherwise it is a string.
1090            # assumes the fd is securely opened by caller (mkstemp)
1091            os.ftruncate(thefile, 0)
1092            os.write(thefile, b_file_data)
1093        elif thefile == '-':
1094            # get a ref to either sys.stdout.buffer for py3 or plain old sys.stdout for py2
1095            # We need sys.stdout.buffer on py3 so we can write bytes to it since the plaintext
1096            # of the vaulted object could be anything/binary/etc
1097            output = getattr(sys.stdout, 'buffer', sys.stdout)
1098            output.write(b_file_data)
1099        else:
1100            # file names are insecure and prone to race conditions, so remove and create securely
1101            if os.path.isfile(thefile):
1102                if shred:
1103                    self._shred_file(thefile)
1104                else:
1105                    os.remove(thefile)
1106
1107            # when setting new umask, we get previous as return
1108            current_umask = os.umask(0o077)
1109            try:
1110                try:
1111                    # create file with secure permissions
1112                    fd = os.open(thefile, os.O_CREAT | os.O_EXCL | os.O_RDWR | os.O_TRUNC, mode)
1113                except OSError as ose:
1114                    # Want to catch FileExistsError, which doesn't exist in Python 2, so catch OSError
1115                    # and compare the error number to get equivalent behavior in Python 2/3
1116                    if ose.errno == errno.EEXIST:
1117                        raise AnsibleError('Vault file got recreated while we were operating on it: %s' % to_native(ose))
1118
1119                    raise AnsibleError('Problem creating temporary vault file: %s' % to_native(ose))
1120
1121                try:
1122                    # now write to the file and ensure ours is only data in it
1123                    os.ftruncate(fd, 0)
1124                    os.write(fd, b_file_data)
1125                except OSError as e:
1126                    raise AnsibleError('Unable to write to temporary vault file: %s' % to_native(e))
1127                finally:
1128                    # Make sure the file descriptor is always closed and reset umask
1129                    os.close(fd)
1130            finally:
1131                os.umask(current_umask)
1132
1133    def shuffle_files(self, src, dest):
1134        prev = None
1135        # overwrite dest with src
1136        if os.path.isfile(dest):
1137            prev = os.stat(dest)
1138            # old file 'dest' was encrypted, no need to _shred_file
1139            os.remove(dest)
1140        shutil.move(src, dest)
1141
1142        # reset permissions if needed
1143        if prev is not None:
1144            # TODO: selinux, ACLs, xattr?
1145            os.chmod(dest, prev.st_mode)
1146            os.chown(dest, prev.st_uid, prev.st_gid)
1147
1148    def _editor_shell_command(self, filename):
1149        env_editor = os.environ.get('EDITOR', 'vi')
1150        editor = shlex.split(env_editor)
1151        editor.append(filename)
1152
1153        return editor
1154
1155
1156########################################
1157#               CIPHERS                #
1158########################################
1159
1160class VaultAES256:
1161
1162    """
1163    Vault implementation using AES-CTR with an HMAC-SHA256 authentication code.
1164    Keys are derived using PBKDF2
1165    """
1166
1167    # http://www.daemonology.net/blog/2009-06-11-cryptographic-right-answers.html
1168
1169    # Note: strings in this class should be byte strings by default.
1170
1171    def __init__(self):
1172        if not HAS_CRYPTOGRAPHY and not HAS_PYCRYPTO:
1173            raise AnsibleError(NEED_CRYPTO_LIBRARY)
1174
1175    @staticmethod
1176    def _create_key_cryptography(b_password, b_salt, key_length, iv_length):
1177        kdf = PBKDF2HMAC(
1178            algorithm=hashes.SHA256(),
1179            length=2 * key_length + iv_length,
1180            salt=b_salt,
1181            iterations=10000,
1182            backend=CRYPTOGRAPHY_BACKEND)
1183        b_derivedkey = kdf.derive(b_password)
1184
1185        return b_derivedkey
1186
1187    @staticmethod
1188    def _pbkdf2_prf(p, s):
1189        hash_function = SHA256_pycrypto
1190        return HMAC_pycrypto.new(p, s, hash_function).digest()
1191
1192    @classmethod
1193    def _create_key_pycrypto(cls, b_password, b_salt, key_length, iv_length):
1194
1195        # make two keys and one iv
1196
1197        b_derivedkey = PBKDF2_pycrypto(b_password, b_salt, dkLen=(2 * key_length) + iv_length,
1198                                       count=10000, prf=cls._pbkdf2_prf)
1199        return b_derivedkey
1200
1201    @classmethod
1202    def _gen_key_initctr(cls, b_password, b_salt):
1203        # 16 for AES 128, 32 for AES256
1204        key_length = 32
1205
1206        if HAS_CRYPTOGRAPHY:
1207            # AES is a 128-bit block cipher, so IVs and counter nonces are 16 bytes
1208            iv_length = algorithms.AES.block_size // 8
1209
1210            b_derivedkey = cls._create_key_cryptography(b_password, b_salt, key_length, iv_length)
1211            b_iv = b_derivedkey[(key_length * 2):(key_length * 2) + iv_length]
1212        elif HAS_PYCRYPTO:
1213            # match the size used for counter.new to avoid extra work
1214            iv_length = 16
1215
1216            b_derivedkey = cls._create_key_pycrypto(b_password, b_salt, key_length, iv_length)
1217            b_iv = hexlify(b_derivedkey[(key_length * 2):(key_length * 2) + iv_length])
1218        else:
1219            raise AnsibleError(NEED_CRYPTO_LIBRARY + '(Detected in initctr)')
1220
1221        b_key1 = b_derivedkey[:key_length]
1222        b_key2 = b_derivedkey[key_length:(key_length * 2)]
1223
1224        return b_key1, b_key2, b_iv
1225
1226    @staticmethod
1227    def _encrypt_cryptography(b_plaintext, b_key1, b_key2, b_iv):
1228        cipher = C_Cipher(algorithms.AES(b_key1), modes.CTR(b_iv), CRYPTOGRAPHY_BACKEND)
1229        encryptor = cipher.encryptor()
1230        padder = padding.PKCS7(algorithms.AES.block_size).padder()
1231        b_ciphertext = encryptor.update(padder.update(b_plaintext) + padder.finalize())
1232        b_ciphertext += encryptor.finalize()
1233
1234        # COMBINE SALT, DIGEST AND DATA
1235        hmac = HMAC(b_key2, hashes.SHA256(), CRYPTOGRAPHY_BACKEND)
1236        hmac.update(b_ciphertext)
1237        b_hmac = hmac.finalize()
1238
1239        return to_bytes(hexlify(b_hmac), errors='surrogate_or_strict'), hexlify(b_ciphertext)
1240
1241    @staticmethod
1242    def _encrypt_pycrypto(b_plaintext, b_key1, b_key2, b_iv):
1243        # PKCS#7 PAD DATA http://tools.ietf.org/html/rfc5652#section-6.3
1244        bs = AES_pycrypto.block_size
1245        padding_length = (bs - len(b_plaintext) % bs) or bs
1246        b_plaintext += to_bytes(padding_length * chr(padding_length), encoding='ascii', errors='strict')
1247
1248        # COUNTER.new PARAMETERS
1249        # 1) nbits (integer) - Length of the counter, in bits.
1250        # 2) initial_value (integer) - initial value of the counter. "iv" from _gen_key_initctr
1251
1252        ctr = Counter_pycrypto.new(128, initial_value=int(b_iv, 16))
1253
1254        # AES.new PARAMETERS
1255        # 1) AES key, must be either 16, 24, or 32 bytes long -- "key" from _gen_key_initctr
1256        # 2) MODE_CTR, is the recommended mode
1257        # 3) counter=<CounterObject>
1258
1259        cipher = AES_pycrypto.new(b_key1, AES_pycrypto.MODE_CTR, counter=ctr)
1260
1261        # ENCRYPT PADDED DATA
1262        b_ciphertext = cipher.encrypt(b_plaintext)
1263
1264        # COMBINE SALT, DIGEST AND DATA
1265        hmac = HMAC_pycrypto.new(b_key2, b_ciphertext, SHA256_pycrypto)
1266
1267        return to_bytes(hmac.hexdigest(), errors='surrogate_or_strict'), hexlify(b_ciphertext)
1268
1269    @classmethod
1270    def encrypt(cls, b_plaintext, secret):
1271        if secret is None:
1272            raise AnsibleVaultError('The secret passed to encrypt() was None')
1273        b_salt = os.urandom(32)
1274        b_password = secret.bytes
1275        b_key1, b_key2, b_iv = cls._gen_key_initctr(b_password, b_salt)
1276
1277        if HAS_CRYPTOGRAPHY:
1278            b_hmac, b_ciphertext = cls._encrypt_cryptography(b_plaintext, b_key1, b_key2, b_iv)
1279        elif HAS_PYCRYPTO:
1280            b_hmac, b_ciphertext = cls._encrypt_pycrypto(b_plaintext, b_key1, b_key2, b_iv)
1281        else:
1282            raise AnsibleError(NEED_CRYPTO_LIBRARY + '(Detected in encrypt)')
1283
1284        b_vaulttext = b'\n'.join([hexlify(b_salt), b_hmac, b_ciphertext])
1285        # Unnecessary but getting rid of it is a backwards incompatible vault
1286        # format change
1287        b_vaulttext = hexlify(b_vaulttext)
1288        return b_vaulttext
1289
1290    @classmethod
1291    def _decrypt_cryptography(cls, b_ciphertext, b_crypted_hmac, b_key1, b_key2, b_iv):
1292        # b_key1, b_key2, b_iv = self._gen_key_initctr(b_password, b_salt)
1293        # EXIT EARLY IF DIGEST DOESN'T MATCH
1294        hmac = HMAC(b_key2, hashes.SHA256(), CRYPTOGRAPHY_BACKEND)
1295        hmac.update(b_ciphertext)
1296        try:
1297            hmac.verify(_unhexlify(b_crypted_hmac))
1298        except InvalidSignature as e:
1299            raise AnsibleVaultError('HMAC verification failed: %s' % e)
1300
1301        cipher = C_Cipher(algorithms.AES(b_key1), modes.CTR(b_iv), CRYPTOGRAPHY_BACKEND)
1302        decryptor = cipher.decryptor()
1303        unpadder = padding.PKCS7(128).unpadder()
1304        b_plaintext = unpadder.update(
1305            decryptor.update(b_ciphertext) + decryptor.finalize()
1306        ) + unpadder.finalize()
1307
1308        return b_plaintext
1309
1310    @staticmethod
1311    def _is_equal(b_a, b_b):
1312        """
1313        Comparing 2 byte arrrays in constant time
1314        to avoid timing attacks.
1315
1316        It would be nice if there was a library for this but
1317        hey.
1318        """
1319        if not (isinstance(b_a, binary_type) and isinstance(b_b, binary_type)):
1320            raise TypeError('_is_equal can only be used to compare two byte strings')
1321
1322        # http://codahale.com/a-lesson-in-timing-attacks/
1323        if len(b_a) != len(b_b):
1324            return False
1325
1326        result = 0
1327        for b_x, b_y in zip(b_a, b_b):
1328            if PY3:
1329                result |= b_x ^ b_y
1330            else:
1331                result |= ord(b_x) ^ ord(b_y)
1332        return result == 0
1333
1334    @classmethod
1335    def _decrypt_pycrypto(cls, b_ciphertext, b_crypted_hmac, b_key1, b_key2, b_iv):
1336        # EXIT EARLY IF DIGEST DOESN'T MATCH
1337        hmac_decrypt = HMAC_pycrypto.new(b_key2, b_ciphertext, SHA256_pycrypto)
1338        if not cls._is_equal(b_crypted_hmac, to_bytes(hmac_decrypt.hexdigest())):
1339            return None
1340
1341        # SET THE COUNTER AND THE CIPHER
1342        ctr = Counter_pycrypto.new(128, initial_value=int(b_iv, 16))
1343        cipher = AES_pycrypto.new(b_key1, AES_pycrypto.MODE_CTR, counter=ctr)
1344
1345        # DECRYPT PADDED DATA
1346        b_plaintext = cipher.decrypt(b_ciphertext)
1347
1348        # UNPAD DATA
1349        if PY3:
1350            padding_length = b_plaintext[-1]
1351        else:
1352            padding_length = ord(b_plaintext[-1])
1353
1354        b_plaintext = b_plaintext[:-padding_length]
1355        return b_plaintext
1356
1357    @classmethod
1358    def decrypt(cls, b_vaulttext, secret):
1359
1360        b_ciphertext, b_salt, b_crypted_hmac = parse_vaulttext(b_vaulttext)
1361
1362        # TODO: would be nice if a VaultSecret could be passed directly to _decrypt_*
1363        #       (move _gen_key_initctr() to a AES256 VaultSecret or VaultContext impl?)
1364        # though, likely needs to be python cryptography specific impl that basically
1365        # creates a Cipher() with b_key1, a Mode.CTR() with b_iv, and a HMAC() with sign key b_key2
1366        b_password = secret.bytes
1367
1368        b_key1, b_key2, b_iv = cls._gen_key_initctr(b_password, b_salt)
1369
1370        if HAS_CRYPTOGRAPHY:
1371            b_plaintext = cls._decrypt_cryptography(b_ciphertext, b_crypted_hmac, b_key1, b_key2, b_iv)
1372        elif HAS_PYCRYPTO:
1373            b_plaintext = cls._decrypt_pycrypto(b_ciphertext, b_crypted_hmac, b_key1, b_key2, b_iv)
1374        else:
1375            raise AnsibleError(NEED_CRYPTO_LIBRARY + '(Detected in decrypt)')
1376
1377        return b_plaintext
1378
1379
1380# Keys could be made bytes later if the code that gets the data is more
1381# naturally byte-oriented
1382CIPHER_MAPPING = {
1383    u'AES256': VaultAES256,
1384}
1385