|
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'])
|
|
Karol Babioch |
9f3f7a |
logger.debug('in: {}'.format(message))
|
|
Karol Babioch |
9f3f7a |
out, err = cmd.communicate(message)
|
|
Karol Babioch |
9f3f7a |
logger.debug('return: {}, out: {}, err: {}'.format(cmd.returncode, out, 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)
|
|
Karol Babioch |
9f3f7a |
logger.debug('in: {}'.format(message))
|
|
Karol Babioch |
9f3f7a |
out, err = cmd.communicate(message)
|
|
Karol Babioch |
9f3f7a |
logger.debug('return: {}, out: {}, err: {}'.format(cmd.returncode, out, 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())
|
|
Karol Babioch |
9f3f7a |
logger.debug('recipients: {}'.format(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 |
|
|
Karol Babioch |
9f3f7a |
logger.debug('args: {}'.format(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
|
|
Karol Babioch |
9f3f7a |
logger.debug('pillars: {}'.format(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
|
|
Karol Babioch |
9f3f7a |
file = open(pillar, 'r')
|
|
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)
|
|
Karol Babioch |
9f3f7a |
except DecryptError as error:
|
|
Karol Babioch |
9f3f7a |
logger.error('Failed to decrypt data in file: {}, skipping'.format(pillar))
|
|
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()
|
|
Karol Babioch |
9f3f7a |
logger.info('Successfully reencrypted all data in file: {}'.format(pillar))
|
|
Karol Babioch |
9f3f7a |
success += 1
|
|
Karol Babioch |
9f3f7a |
|
|
Karol Babioch |
9f3f7a |
print('total: {}, skipped: {}, successful: {}, failed: {}'.format(total, total - success - failure, success, failure))
|
|
Karol Babioch |
9f3f7a |
|
|
Karol Babioch |
9f3f7a |
if failure > 0:
|
|
Karol Babioch |
9f3f7a |
sys.exit(1)
|