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