Implement cat-like filtering behaviour for encrypt/decrypt

This allows the following invocations:

    # Interactive use, like gpg
    ansible-vault encrypt --output x

    # Non-interactive, for scripting
    echo plaintext|ansible-vault encrypt --output x

    # Separate input and output files
    ansible-vault encrypt input.yml --output output.yml

    # Existing usage (in-place encryption) unchanged
    ansible-vault encrypt inout.yml

…and the analogous cases for ansible-vault decrypt as well.

In all cases, the input and output files can be '-' to read from stdin
or write to stdout. This permits sensitive data to be encrypted and
decrypted without ever hitting disk.
This commit is contained in:
Abhijit Menon-Sen 2015-08-26 22:51:20 +05:30
parent 32b38d4e29
commit e7eebb6954
3 changed files with 44 additions and 23 deletions

View file

@ -262,6 +262,8 @@ class CLI(object):
parser.add_option('--new-vault-password-file', parser.add_option('--new-vault-password-file',
dest='new_vault_password_file', help="new vault password file for rekey", action="callback", dest='new_vault_password_file', help="new vault password file for rekey", action="callback",
callback=CLI.expand_tilde, type=str) callback=CLI.expand_tilde, type=str)
parser.add_option('--output', default=None, dest='output_file',
help='output file name for encrypt or decrypt; use - for stdout')
if subset_opts: if subset_opts:

View file

@ -63,7 +63,19 @@ class VaultCLI(CLI):
self.options, self.args = self.parser.parse_args() self.options, self.args = self.parser.parse_args()
self.display.verbosity = self.options.verbosity self.display.verbosity = self.options.verbosity
if len(self.args) == 0: if self.options.output_file:
if self.action not in ['encrypt','decrypt']:
raise AnsibleOptionsError("The --output option can be used only with ansible-vault encrypt/decrypt")
# This restriction should remain in place until it's possible to
# load multiple YAML records from a single file, or it's too easy
# to create an encrypted file that can't be read back in. But in
# the meanwhile, "cat a b c|ansible-vault encrypt --output x" is
# a workaround.
if len(self.args) > 1:
raise AnsibleOptionsError("At most one input file may be used with the --output option")
elif len(self.args) == 0:
raise AnsibleOptionsError("Vault requires at least one filename as a parameter") raise AnsibleOptionsError("Vault requires at least one filename as a parameter")
def run(self): def run(self):
@ -87,6 +99,20 @@ class VaultCLI(CLI):
self.execute() self.execute()
def execute_encrypt(self):
for f in self.args or ['-']:
self.editor.encrypt_file(f, output_file=self.options.output_file)
self.display.display("Encryption successful", stderr=True)
def execute_decrypt(self):
for f in self.args or ['-']:
self.editor.decrypt_file(f, output_file=self.options.output_file)
self.display.display("Decryption successful", stderr=True)
def execute_create(self): def execute_create(self):
if len(self.args) > 1: if len(self.args) > 1:
@ -94,13 +120,6 @@ class VaultCLI(CLI):
self.editor.create_file(self.args[0]) self.editor.create_file(self.args[0])
def execute_decrypt(self):
for f in self.args:
self.editor.decrypt_file(f)
self.display.display("Decryption successful", stderr=True)
def execute_edit(self): def execute_edit(self):
for f in self.args: for f in self.args:
self.editor.edit_file(f) self.editor.edit_file(f)
@ -110,13 +129,6 @@ class VaultCLI(CLI):
for f in self.args: for f in self.args:
self.editor.view_file(f) self.editor.view_file(f)
def execute_encrypt(self):
for f in self.args:
self.editor.encrypt_file(f)
self.display.display("Encryption successful", stderr=True)
def execute_rekey(self): def execute_rekey(self):
for f in self.args: for f in self.args:
if not (os.path.isfile(f)): if not (os.path.isfile(f)):

View file

@ -20,6 +20,7 @@ __metaclass__ = type
import os import os
import shlex import shlex
import shutil import shutil
import sys
import tempfile import tempfile
from io import BytesIO from io import BytesIO
from subprocess import call from subprocess import call
@ -258,21 +259,21 @@ class VaultEditor:
# and restore umask # and restore umask
os.umask(old_umask) os.umask(old_umask)
def encrypt_file(self, filename): def encrypt_file(self, filename, output_file=None):
check_prereqs() check_prereqs()
plaintext = self.read_data(filename) plaintext = self.read_data(filename)
ciphertext = self.vault.encrypt(plaintext) ciphertext = self.vault.encrypt(plaintext)
self.write_data(ciphertext, filename) self.write_data(ciphertext, output_file or filename)
def decrypt_file(self, filename): def decrypt_file(self, filename, output_file=None):
check_prereqs() check_prereqs()
ciphertext = self.read_data(filename) ciphertext = self.read_data(filename)
plaintext = self.vault.decrypt(ciphertext) plaintext = self.vault.decrypt(ciphertext)
self.write_data(plaintext, filename) self.write_data(plaintext, output_file or filename)
def create_file(self, filename): def create_file(self, filename):
""" create a new encrypted file """ """ create a new encrypted file """
@ -327,7 +328,10 @@ class VaultEditor:
def read_data(self, filename): def read_data(self, filename):
try: try:
f = open(filename, "rb") if filename == '-':
f = sys.stdin
else:
f = open(filename, "rb")
data = f.read() data = f.read()
f.close() f.close()
except Exception as e: except Exception as e:
@ -336,9 +340,12 @@ class VaultEditor:
return data return data
def write_data(self, data, filename): def write_data(self, data, filename):
if os.path.isfile(filename): if filename == '-':
os.remove(filename) f = sys.stdout
f = open(filename, "wb") else:
if os.path.isfile(filename):
os.remove(filename)
f = open(filename, "wb")
f.write(to_bytes(data, errors='strict')) f.write(to_bytes(data, errors='strict'))
f.close() f.close()