summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorChris St. Pierre <chris.a.st.pierre@gmail.com>2012-06-06 09:31:14 -0400
committerChris St. Pierre <chris.a.st.pierre@gmail.com>2012-06-06 09:31:48 -0400
commit1291e5b09efb956d42e7ab83d485d41542f438f4 (patch)
treecde83f5ecbe7b72ab20e013dea06098742ebed26 /src
parentf46d4216cd4d6a4b272bfff1465a19a5649a93e7 (diff)
downloadbcfg2-1291e5b09efb956d42e7ab83d485d41542f438f4.tar.gz
bcfg2-1291e5b09efb956d42e7ab83d485d41542f438f4.tar.bz2
bcfg2-1291e5b09efb956d42e7ab83d485d41542f438f4.zip
added properties element encryption
added bcfg2-crypt utility for encrypting Properties and Cfg files
Diffstat (limited to 'src')
-rw-r--r--src/lib/Bcfg2/Options.py45
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Cfg/CfgEncryptedGenerator.py2
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Properties.py58
-rwxr-xr-xsrc/sbin/bcfg2-crypt321
4 files changed, 424 insertions, 2 deletions
diff --git a/src/lib/Bcfg2/Options.py b/src/lib/Bcfg2/Options.py
index 0791ce343..6d3dd0a8c 100644
--- a/src/lib/Bcfg2/Options.py
+++ b/src/lib/Bcfg2/Options.py
@@ -730,6 +730,43 @@ CFG_VALIDATION = \
long_arg=True,
cook=get_bool)
+# bcfg2-crypt options
+ENCRYPT = \
+ Option('Encrypt the specified file',
+ default=False,
+ cmd='--encrypt',
+ long_arg=True)
+DECRYPT = \
+ Option('Decrypt the specified file',
+ default=False,
+ cmd='--decrypt',
+ long_arg=True)
+CRYPT_PASSPHRASE = \
+ Option('Encryption passphrase (name or passphrase)',
+ default=None,
+ cmd='-p',
+ odesc='<passphrase>')
+CRYPT_XPATH = \
+ Option('XPath expression to select elements to encrypt',
+ default=None,
+ cmd='--xpath',
+ odesc='<xpath>',
+ long_arg=True)
+CRYPT_PROPERTIES = \
+ Option('Encrypt the specified file as a Properties file',
+ default=False,
+ cmd="--properties",
+ long_arg=True)
+CRYPT_CFG = \
+ Option('Encrypt the specified file as a Cfg file',
+ default=False,
+ cmd="--cfg",
+ long_arg=True)
+CRYPT_REMOVE = \
+ Option('Remove the plaintext file after encrypting',
+ default=False,
+ cmd="--remove",
+ long_arg=True)
# Option groups
CLI_COMMON_OPTIONS = dict(configfile=CFILE,
@@ -754,6 +791,14 @@ SERVER_COMMON_OPTIONS = dict(repo=SERVER_REPOSITORY,
ca=SERVER_CA,
protocol=SERVER_PROTOCOL)
+CRYPT_OPTIONS = dict(encrypt=ENCRYPT,
+ decrypt=DECRYPT,
+ passphrase=CRYPT_PASSPHRASE,
+ xpath=CRYPT_XPATH,
+ properties=CRYPT_PROPERTIES,
+ cfg=CRYPT_CFG,
+ remove=CRYPT_REMOVE)
+
DRIVER_OPTIONS = \
dict(apt_install_path=CLIENT_APT_TOOLS_INSTALL_PATH,
apt_var_path=CLIENT_APT_TOOLS_VAR_PATH,
diff --git a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgEncryptedGenerator.py b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgEncryptedGenerator.py
index 0839e3536..2c926fae7 100644
--- a/src/lib/Bcfg2/Server/Plugins/Cfg/CfgEncryptedGenerator.py
+++ b/src/lib/Bcfg2/Server/Plugins/Cfg/CfgEncryptedGenerator.py
@@ -10,7 +10,7 @@ except ImportError:
logger = logging.getLogger(__name__)
def passphrases():
- section = "cfg:encryption"
+ section = "encryption"
if SETUP.cfp.has_section(section):
return dict([(o, SETUP.cfp.get(section, o))
for o in SETUP.cfp.options(section)])
diff --git a/src/lib/Bcfg2/Server/Plugins/Properties.py b/src/lib/Bcfg2/Server/Plugins/Properties.py
index 680881858..a81cdadd2 100644
--- a/src/lib/Bcfg2/Server/Plugins/Properties.py
+++ b/src/lib/Bcfg2/Server/Plugins/Properties.py
@@ -5,11 +5,31 @@ import copy
import logging
import lxml.etree
import Bcfg2.Server.Plugin
+try:
+ from Bcfg2.Encryption import ssl_decrypt, EVPError
+ have_crypto = True
+except ImportError:
+ have_crypto = False
+
+logger = logging.getLogger(__name__)
+
+SETUP = None
+
+def passphrases():
+ section = "encryption"
+ if SETUP.cfp.has_section(section):
+ return dict([(o, SETUP.cfp.get(section, o))
+ for o in SETUP.cfp.options(section)])
+ else:
+ return dict()
-logger = logging.getLogger('Bcfg2.Plugins.Properties')
class PropertyFile(Bcfg2.Server.Plugin.StructFile):
"""Class for properties files."""
+ def __init__(self, name):
+ Bcfg2.Server.Plugin.StructFile.__init__(self, name)
+ self.passphrase = None
+
def write(self):
""" Write the data in this data structure back to the property
file """
@@ -47,6 +67,39 @@ class PropertyFile(Bcfg2.Server.Plugin.StructFile):
else:
return True
+ def Index(self):
+ Bcfg2.Server.Plugin.StructFile.Index(self)
+ if self.xdata.get("encryption", "false").lower() != "false":
+ logger.error("decrypting data in %s" % self.name)
+ if not have_crypto:
+ msg = "Properties: M2Crypto is not available: %s" % self.name
+ logger.error(msg)
+ raise Bcxfg2.Server.Plugin.PluginExecutionError(msg)
+ for el in self.xdata.xpath("*[@encrypted='true']"):
+ logger.error("decrypting data in %s in %s" % (el.tag, self.name))
+ try:
+ el.text = self._decrypt(el.text)
+ except EVPError:
+ msg = "Failed to decrypt %s element in %s" % (el.tag,
+ self.name)
+ logger.error(msg)
+ raise Bcfg2.Server.PluginExecutionError(msg)
+
+ def _decrypt(self, crypted):
+ if self.passphrase is None:
+ for passwd in passphrases().values():
+ try:
+ rv = ssl_decrypt(crypted, passwd)
+ self.passphrase = passwd
+ return rv
+ except EVPError:
+ pass
+ else:
+ try:
+ return ssl_decrypt(crypted, self.passphrase)
+ except EVPError:
+ pass
+ raise EVPError("Failed to decrypt")
class PropDirectoryBacked(Bcfg2.Server.Plugin.DirectoryBacked):
__child__ = PropertyFile
@@ -62,6 +115,7 @@ class Properties(Bcfg2.Server.Plugin.Plugin,
name = 'Properties'
def __init__(self, core, datastore):
+ global SETUP
Bcfg2.Server.Plugin.Plugin.__init__(self, core, datastore)
Bcfg2.Server.Plugin.Connector.__init__(self)
try:
@@ -72,5 +126,7 @@ class Properties(Bcfg2.Server.Plugin.Plugin,
(e.strerror, e.filename))
raise Bcfg2.Server.Plugin.PluginInitError
+ SETUP = core.setup
+
def get_additional_data(self, _):
return copy.copy(self.store.entries)
diff --git a/src/sbin/bcfg2-crypt b/src/sbin/bcfg2-crypt
new file mode 100755
index 000000000..b9b8f1ae5
--- /dev/null
+++ b/src/sbin/bcfg2-crypt
@@ -0,0 +1,321 @@
+#!/usr/bin/env python
+""" helper for encrypting/decrypting Cfg and Properties files """
+
+import os
+import sys
+import copy
+import logging
+import getpass
+import lxml.etree
+import Bcfg2.Logger
+import Bcfg2.Options
+import Bcfg2.Encryption
+
+LOGGER = None
+
+def get_logger(verbose=0):
+ """ set up logging according to the verbose level given on the
+ command line """
+ global LOGGER
+ if LOGGER is None:
+ LOGGER = logging.getLogger(sys.argv[0])
+ stderr = logging.StreamHandler()
+ if verbose:
+ level = logging.DEBUG
+ else:
+ level = logging.WARNING
+ LOGGER.setLevel(level)
+ LOGGER.addHandler(stderr)
+ syslog = logging.handlers.SysLogHandler("/dev/log")
+ syslog.setFormatter(logging.Formatter("%(name)s: %(message)s"))
+ LOGGER.addHandler(syslog)
+ return LOGGER
+
+
+class Encryptor(object):
+ def __init__(self, setup):
+ self.setup = setup
+ self.logger = get_logger()
+ self.passphrase = None
+ self.pname = None
+
+ def get_encrypted_filename(self, plaintext_filename):
+ return plaintext_filename
+
+ def get_plaintext_filename(self, encrypted_filename):
+ return encrypted_filename
+
+ def encrypt(self, fname):
+ if (not self.setup.cfp.has_section("encryption") or
+ self.setup.cfp.options("encryption") == 0):
+ self.logger.error("No passphrases available in %s" %
+ self.setup['configfile'])
+ return False
+ if not self.passphrase:
+ if self.setup['passphrase']:
+ if self.setup.cfp.has_option("encryption",
+ self.setup['passphrase']):
+ self.passphrase = \
+ self.setup.cfp.get("encryption",
+ self.setup['passphrase'])
+ self.pname = self.setup['passphrase']
+ else:
+ self.logger.error("Could not find passphrase %s in %s" %
+ (self.setup['passphrase'],
+ self.setup['configfile']))
+ else:
+ pnames = self.setup.cfp.options("encryption")
+ if len(pnames) == 1:
+ self.passphrase = self.setup.cfp.get(pnames[0])
+ self.pname = pnames[0]x
+ self.logger.info("Using passphrase %s" % pnames[0])
+ else:
+ name = None
+ while (not name or
+ not self.setup.cfp.has_option("encryption", name)):
+ print("Available passphrases: ")
+ for pname in pnames:
+ print(pname)
+ name = raw_input("Passphrase: ")
+ self.passphrase = self.setup.cfp.get("encryption", name)
+ self.pname = name
+ try:
+ plaintext = open(fname).read()
+ except IOError:
+ err = sys.exc_info()[1]
+ self.logger.error("Error reading %s, skipping: %s" (fname, err))
+ return False
+ crypted = self._encrypt(plaintext, self.passphrase, name=pname)
+ try:
+ open(self.get_encrypted_filename(fname), "wb").write(crypted)
+ self.logger.info("Wrote encrypted data to %s" %
+ self.get_encrypted_filename(fname))
+ return True
+ except IOError:
+ err = sys.exc_info()[1]
+ self.logger.error("Error writing encrypted data from %s to %s: %s" %
+ (fname, self.get_encrypted_filename(fname), err))
+ return False
+
+ def _encrypt(self, plaintext, passphrase, name=None):
+ return Bcfg2.Encryption.ssl_encrypt(plaintext, passphrase)
+
+ def decrypt(self, fname):
+ if (not self.setup.cfp.has_section("encryption") or
+ self.setup.cfp.options("encryption") == 0):
+ self.logger.error("No passphrases available in %s" %
+ self.setup['configfile'])
+ return False
+
+ try:
+ crypted = open(fname).read()
+ except IOError:
+ err = sys.exc_info()[1]
+ self.logger.error("Error reading %s, skipping: %s" (fname, err))
+ return False
+
+ plaintext = None
+ if self.setup['passphrase']:
+ if self.setup.cfp.has_option("encryption",
+ self.setup['passphrase']):
+ passphrase = self.setup.cfp.get("encryption",
+ self.setup['passphrase'])
+ else:
+ self.logger.error("Could not find passphrase %s in %s" %
+ (self.setup['passphrase'],
+ self.setup['configfile']))
+ try:
+ plaintext = self._decrypt(crypted, passphrase)
+ except Bcfg2.Encryption.EVPError:
+ self.logger.error("Could not decrypt %s with the specified passphrase" % fname)
+ return False
+ except:
+ err = sys.exc_info()[1]
+ self.logger.error("Error decrypting %s: %s" % (fname, err))
+ else:
+ # figure out the right passphrase
+ pname = self.get_decryption_passphrase(crypted)
+ if pname:
+ passphrase = self.setup.cfp.get('encryption', pname)
+ try:
+ plaintext = self._decrypt(crypted, passphrase)
+ except:
+ err = sys.exc_info()[1]
+ self.logger.error("Error decrypting %s: %s" %
+ (fname, err))
+ else:
+ for pname in self.setup.cfp.options('encryption'):
+ self.logger.debug("Trying passphrase %s" % pname)
+ passphrase = self.setup.cfp.get('encryption', pname)
+ try:
+ plaintext = self._decrypt(crypted, passphrase)
+ break
+ except Bcfg2.Encryption.EVPError:
+ pass
+ except:
+ err = sys.exc_info()[1]
+ self.logger.error("Error decrypting %s: %s" %
+ (fname, err))
+ if not plaintext:
+ self.logger.error("Could not decrypt %s with any passphrase in %s" %
+ (fname, self.setup['configfile']))
+ return False
+
+ try:
+ open(self.get_plaintext_filename(fname), "wb").write(plaintext)
+ self.logger.info("Wrote decrypted data to %s" %
+ self.get_plaintext_filename(fname))
+ return True
+ except IOError:
+ err = sys.exc_info()[1]
+ self.logger.error("Error writing encrypted data from %s to %s: %s" %
+ (fname, self.get_plaintext_filename(fname), err))
+ return False
+
+ def get_decryption_passphrase(self, crypted):
+ return None
+
+ def _decrypt(self, crypted, passphrase):
+ return Bcfg2.Encryption.ssl_decrypt(crypted, passphrase)
+
+
+class CfgEncryptor(Encryptor):
+ def get_encrypted_filename(self, plaintext_filename):
+ return plaintext_filename + ".crypt"
+
+ def get_plaintext_filename(self, encrypted_filename):
+ if encrypted_filename.endswith(".crypt"):
+ return encrypted_filename[:-6]
+ else:
+ return Encryptor.get_plaintext_filename(self, encrypted_filename)
+
+
+class PropertiesEncryptor(Encryptor):
+ def _encrypt(self, plaintext, passphrase, name=None):
+ xdata = lxml.etree.XML(plaintext)
+ if self.setup['xpath']:
+ elements = xdata.xpath(self.setup['xpath'])
+ else:
+ elements = xdata.xpath('*[@encrypted="true"]')
+ if not elements:
+ elements = list(xdata.getiterator())
+
+ for el in elements:
+ el.text = Bcfg2.Encryption.ssl_encrypt(el.text, passphrase)
+ el.set("encrypted", "true")
+ if name is None:
+ xdata.set("encryption", "true")
+ else:
+ xdata.set("encryption", name)
+ return lxml.etree.tostring(xdata)
+
+ def get_decryption_passphrase(self, crypted):
+ xdata = lxml.etree.XML(crypted)
+ pname = xdata.get("encryption")
+ if pname and pname.lower() != "true":
+ return pname
+ return None
+
+ def _decrypt(self, crypted, passphrase):
+ xdata = lxml.etree.XML(crypted)
+ if self.setup['xpath']:
+ elements = xdata.xpath(self.setup['xpath'])
+ else:
+ elements = xdata.xpath("*[@encrypted='true']")
+ if not elements:
+ self.logger.info("No elements found to decrypt")
+ return False
+ for el in elements:
+ if not el.text.strip():
+ self.logger.warning("Skipping empty element %s" % el.tag)
+ continue
+ el.text = Bcfg2.Encryption.ssl_decrypt(el.text, passphrase)
+ return lxml.etree.tostring(xdata)
+
+
+def main():
+ optinfo = dict()
+ optinfo.update(Bcfg2.Options.CRYPT_OPTIONS)
+ optinfo.update(Bcfg2.Options.CLI_COMMON_OPTIONS)
+ setup = Bcfg2.Options.OptionParser(optinfo)
+ setup.hm = "Usage: bcfg2-crypt [options] <filename>\nOptions:\n %s" % \
+ setup.buildHelpMessage()
+ setup.parse(sys.argv[1:])
+
+ if not setup['args']:
+ print(setup.hm)
+ raise SystemExit(1)
+ elif setup['encrypt'] and setup['decrypt']:
+ print("You cannot specify both --encrypt) and --decrypt")
+ raise SystemExit(1)
+ elif setup['cfg'] and setup['properties']:
+ print("You cannot specify both --cfg and --properties")
+ raise SystemExit(1)
+ elif setup['cfg'] and setup['properties']:
+ print("Specifying --xpath with --cfg is nonsensical, ignoring --xpath")
+ setup['xpath'] = Bcfg2.Options.CRYPT_XPATH.default
+ elif setup['decrypt'] and setup['remove']:
+ print("--remove cannot be used with --decrypt, ignoring")
+ setup['remove'] = Bcfg2.Options.CRYPT_REMOVE.default
+
+ logger = get_logger(setup['verbose'])
+
+ props_crypt = PropertiesEncryptor(setup)
+ cfg_crypt = CfgEncryptor(setup)
+
+ for fname in setup['args']:
+ if not os.path.exists(fname):
+ logger.error("%s does not exist, skipping" % fname)
+ continue
+
+ # figure out if we need to encrypt this as a Properties file
+ # or as a Cfg file
+ props = False
+ if setup['properties']:
+ props = True
+ elif setup['cfg']:
+ props = False
+ elif fname.endswith(".xml"):
+ try:
+ xroot = lxml.etree.parse(fname).getroot()
+ if xroot.tag == "Properties":
+ props = True
+ else:
+ props = False
+ except IOError:
+ err = sys.exc_info()[1]
+ logger.error("Error reading %s, skipping: %s" (fname, err))
+ continue
+ except lxml.etree.XMLSyntaxError:
+ props = False
+ else:
+ props = False
+
+ if props:
+ encryptor = props_crypt
+ else:
+ encryptor = cfg_crypt
+
+ if setup['encrypt']:
+ if not encryptor.encrypt(fname):
+ continue
+ elif setup['decrypt']:
+ if not encryptor.decrypt(fname):
+ continue
+ else:
+ logger.info("Neither --encrypt nor --decrypt specified, determining mode")
+ if not encryptor.decrypt(fname):
+ logger.info("Failed to decrypt %s, trying encryption" % fname)
+ if not encryptor.encrypt(fname):
+ continue
+
+ if setup['remove'] and encryptor.get_encrypted_filename(fname) != fname:
+ try:
+ os.unlink(fname)
+ except IOError:
+ err = sys.exc_info()[1]
+ logger.error("Error removing %s: %s" (fname, err))
+ continue
+
+if __name__ == '__main__':
+ sys.exit(main())