#!/usr/bin/env python3
# Copyright (c) 2019 Karol Babioch <karol@babioch.de>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import argparse
import logging
import os
import re
import subprocess
import sys
# TODO Add instructions and mention that this needs to be run by someone with access, etc.
# TODO Proper error handling, since this is only a prototype
# TODO Docstrings
# TODO Multithreading ...
RE_PGP_MESSAGE = r'[ \t]*-----BEGIN PGP MESSAGE-----[ \t]*$\n.+?\n^[ \t]*-----END PGP MESSAGE-----[ \t]*$'
RE_PGP_RECIPIENT = r'^0x\w+'
RE_INDENT = r'^(\s*)'
class DecryptError(Exception):
pass
class EncryptError(Exception):
pass
def gpg(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE):
gpg_bin = '/usr/bin/gpg'
cmd = [gpg_bin] + cmd
logger.debug('Running: %s', cmd)
return subprocess.Popen(cmd, stdin=stdin, stdout=stdout, stderr=stderr, encoding=sys.getdefaultencoding())
def decrypt(message):
cmd = gpg(['--batch', '-d'])
logger.debug('in: {}'.format(message))
out, err = cmd.communicate(message)
logger.debug('return: {}, out: {}, err: {}'.format(cmd.returncode, out, err))
if cmd.returncode != 0:
raise DecryptError(err)
return out
def encrypt(message, recipients):
cmd = ['--batch', '--yes', '--trust-model', 'always', '--armor', '-e']
for r in recipients:
cmd += ['--recipient', r]
cmd = gpg(cmd)
logger.debug('in: {}'.format(message))
out, err = cmd.communicate(message)
logger.debug('return: {}, out: {}, err: {}'.format(cmd.returncode, out, err))
if cmd.returncode != 0:
raise EncryptError(err)
return out
def reencrypt(message, recipients):
indent = get_indent(message)
message = remove_indent(message)
message = decrypt(message)
message = encrypt(message, recipients)
message = add_indent(message, indent)
return message
def get_indent(block):
return re.match(RE_INDENT, block.splitlines()[0]).group(1)
def remove_indent(block):
return '\n'.join([ line.lstrip() for line in block.splitlines() ])
def add_indent(block, indent):
return '\n'.join([ indent + line for line in block.splitlines() ])
def get_recipients(file):
with open(file) as f:
regexp = re.compile(RE_PGP_RECIPIENT, re.MULTILINE)
recipients = re.findall(regexp, f.read())
logger.debug('recipients: {}'.format(recipients))
return recipients
# Initialize logging
logger = logging.getLogger(__name__)
logger.addHandler(logging.StreamHandler())
# Parse command line arguments
parser = argparse.ArgumentParser(description='Reencrypts pillar data with current list of recipients')
parser.add_argument('-v', '--verbose', dest='verbose', help='Increase verbosity', action='count', default=0)
parser.add_argument('-r', '--recursive', dest='recursive', help='Recursively look for pillar data', action='store_true')
parser.add_argument('--recipients-file', dest='recipients_file', help='File containing list of recipients', default='encrypted_pillar_recipients')
parser.add_argument('pillars', metavar='PILLAR', type=str, nargs='+', help='Pillar file(s)')
args = parser.parse_args()
# Enable logging if debug flag set
if args.verbose == 1:
logger.setLevel(logging.INFO)
elif args.verbose == 2:
logger.setLevel(logging.DEBUG)
logger.debug('args: {}'.format(args))
# Final list of pillars to reencrypt
pillars = []
# List of recipients parsed from file
recipients = get_recipients(args.recipients_file)
# Recursively scan for all pillar files
if args.recursive:
for pillar in args.pillars:
for dirpath, dirname, filename in os.walk(pillar):
for name in filename:
pillars.append(os.path.join(dirpath, name))
# Only consider what has been provided by user (i.e. non-recursive mode)
else:
pillars = args.pillars
# Log final list of pillar files
logger.debug('pillars: {}'.format(pillars))
# Track number of touched pillar files
total = 0
success = 0
failure = 0
# Iterate over all pillar files
for pillar in pillars:
total += 1
# Read data from pillar file
file = open(pillar, 'r')
data = file.read()
file.close()
# Search for PGP messages and reencrypt them
try:
data, count = re.subn(RE_PGP_MESSAGE, lambda x: reencrypt(x.group(0), recipients), data, flags=re.DOTALL|re.MULTILINE)
except DecryptError as error:
logger.error('Failed to decrypt data in file: {}, skipping'.format(pillar))
failure += 1
continue
# File was modified, re-write it
if count > 0:
file = open(pillar, 'w')
file.write(data)
file.close()
logger.info('Successfully reencrypted all data in file: {}'.format(pillar))
success += 1
print('total: {}, skipped: {}, successful: {}, failed: {}'.format(total, total - success - failure, success, failure))
if failure > 0:
sys.exit(1)