summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorChris St. Pierre <chris.a.st.pierre@gmail.com>2012-06-15 10:55:58 -0400
committerChris St. Pierre <chris.a.st.pierre@gmail.com>2012-06-15 10:55:58 -0400
commite3131034dd00c61ed5ca4f6a38f74250f0ac5726 (patch)
tree94f3de0fe729437f6baac9ab5be048bfb026c1d8
parent9b08b9179e11ef092396662afd1a71e57ca5e528 (diff)
downloadbcfg2-e3131034dd00c61ed5ca4f6a38f74250f0ac5726.tar.gz
bcfg2-e3131034dd00c61ed5ca4f6a38f74250f0ac5726.tar.bz2
bcfg2-e3131034dd00c61ed5ca4f6a38f74250f0ac5726.zip
added support for encrypting different elements in a single Properties file with different passphrases
-rw-r--r--doc/server/plugins/connectors/properties.txt18
-rw-r--r--src/lib/Bcfg2/Server/Plugins/Properties.py29
-rwxr-xr-xsrc/sbin/bcfg2-crypt281
-rw-r--r--tools/manpagegen/bcfg2-crypt.8.ronn52
4 files changed, 213 insertions, 167 deletions
diff --git a/doc/server/plugins/connectors/properties.txt b/doc/server/plugins/connectors/properties.txt
index 19814a54f..ca0e9cf63 100644
--- a/doc/server/plugins/connectors/properties.txt
+++ b/doc/server/plugins/connectors/properties.txt
@@ -128,12 +128,12 @@ then you need to use the ``--properties`` flag to ``bcfg2-crypt``::
The first time you run ``bcfg2-crypt`` on a Properties file, it will
encrypt all character data of all elements. Additionally, it will add
-``encrypted="true"`` to each element that has encrypted character
-data. It also adds ``encryption="<key name>"`` to the top-level
+``encrypted="<key name>"`` to each element that has encrypted character
+data. It also adds ``encryption="true"`` to the top-level
``<Properties>`` tag as a flag to the server that it should try to
decrypt the data in that file. (If you are using Properties schemas,
you will need to make sure to add support for these attributes.) On
-subsequent runs, only those elements flagged with ``encrypted="true"``
+subsequent runs, only those elements flagged with ``encrypted="*"``
are encrypted or decrypted.
To decrypt a Properties file, simply re-run ``bcfg2-crypt``::
@@ -141,19 +141,19 @@ To decrypt a Properties file, simply re-run ``bcfg2-crypt``::
bcfg2-crypt foo.xml
This decrypts the encrypted elements, but it does *not* remove the
-``encrypted="true"`` attribute; this way, you can decrypt a Properties
+``encrypted`` attribute; this way, you can decrypt a Properties
file, modify the contents, and then simply re-run ``bcfg2-crypt`` to
encrypt it again. If you added elements that you also want to be
-encrypted, you can either add the ``encrypted="true"`` attribute to
+encrypted, you can either add the ``encrypted`` attribute to
them manually, or run::
bcfg2-crypt --xpath '*' foo.xml
You can also use the ``--xpath`` option to specify more restrictive
-XPath expressions to only encrypt a subset of elements.
-
-All encrypted elements in a single Properties file must be encrypted
-with the same passphrase.
+XPath expressions to only encrypt a subset of elements, or to encrypt
+different elements with different passphrases. Alternatively, you can
+manally set the ``encrypted`` attribute on various elements and
+``bcfg2-crypt`` will automatically do the right thing.
Accessing Properties contents from TGenshi
==========================================
diff --git a/src/lib/Bcfg2/Server/Plugins/Properties.py b/src/lib/Bcfg2/Server/Plugins/Properties.py
index a81cdadd2..0271e89ba 100644
--- a/src/lib/Bcfg2/Server/Plugins/Properties.py
+++ b/src/lib/Bcfg2/Server/Plugins/Properties.py
@@ -28,7 +28,6 @@ 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
@@ -70,35 +69,35 @@ class PropertyFile(Bcfg2.Server.Plugin.StructFile):
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))
+ raise Bcfg2.Server.Plugin.PluginExecutionError(msg)
+ for el in self.xdata.xpath("*[@encrypted]"):
try:
- el.text = self._decrypt(el.text)
+ el.text = self._decrypt(el)
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():
+ def _decrypt(self, element):
+ passphrases = passphrases()
+ try:
+ passphrase = passphrases[element.get("encrypted")]
+ try:
+ return ssl_decrypt(crypted, self.passphrase)
+ except EVPError:
+ # error is raised below
+ pass
+ except KeyError:
+ 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):
diff --git a/src/sbin/bcfg2-crypt b/src/sbin/bcfg2-crypt
index 17f90ca27..89dfe3e2a 100755
--- a/src/sbin/bcfg2-crypt
+++ b/src/sbin/bcfg2-crypt
@@ -3,9 +3,7 @@
import os
import sys
-import copy
import logging
-import getpass
import lxml.etree
import Bcfg2.Logger
import Bcfg2.Options
@@ -45,137 +43,162 @@ class Encryptor(object):
def get_plaintext_filename(self, encrypted_filename):
return encrypted_filename
- def encrypt(self, fname):
+ def chunk(self, data):
+ yield data
+
+ def unchunk(self, data, original):
+ return data[0]
+
+ def set_passphrase(self):
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 self.passphrase:
+ self.logger.debug("Using previously determined passphrase %s" %
+ self.pname)
+ return True
+
+ if self.setup['passphrase']:
+ self.pname = self.setup['passphrase']
+
+ if self.pname:
+ if self.setup.cfp.has_option("encryption", self.pname):
+ self.passphrase = self.setup.cfp.get("encryption",
+ self.pname)
+ self.logger.debug("Using passphrase %s specified on command "
+ "line" % self.pname)
+ return True
+ else:
+ self.logger.error("Could not find passphrase %s in %s" %
+ (self.pname, self.setup['configfile']))
+ return False
+ else:
+ pnames = self.setup.cfp.options("encryption")
+ if len(pnames) == 1:
+ self.passphrase = self.setup.cfp.get(pnames[0])
+ self.pname = pnames[0]
+ self.logger.info("Using passphrase %s" % pnames[0])
+ return True
+ self.logger.info("No passphrase could be determined")
+ return False
+
+ def encrypt(self, fname):
try:
plaintext = open(fname).read()
except IOError:
err = sys.exc_info()[1]
- self.logger.error("Error reading %s, skipping: %s" (fname, err))
+ self.logger.error("Error reading %s, skipping: %s" % (fname, err))
return False
- if not self.passphrase:
- self.pname = self.get_passphrase(plaintext)
- if self.setup['passphrase']:
- if self.pname:
- self.logger.warning("Passphrase given on command line "
- "differs from passphrase embedded in "
- "file, using command-line option")
- self.pname = self.setup['passphrase']
-
- if self.pname:
- if self.setup.cfp.has_option("encryption", self.pname):
- self.passphrase = self.setup.cfp.get("encryption",
- self.pname)
- else:
- self.logger.error("Could not find passphrase %s in %s" %
- (self.pname, 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]
- 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
-
- crypted = self._encrypt(plaintext, self.passphrase, name=self.pname)
+ self.set_passphrase()
+
+ crypted = []
+ for chunk in self.chunk(plaintext):
+ try:
+ passphrase, pname = self.get_passphrase(chunk)
+ except TypeError:
+ return False
+
+ crypted.append(self._encrypt(chunk, passphrase, name=pname))
+
+ new_fname = self.get_encrypted_filename(fname)
try:
- open(self.get_encrypted_filename(fname), "wb").write(crypted)
- self.logger.info("Wrote encrypted data to %s" %
- self.get_encrypted_filename(fname))
+ open(new_fname, "wb").write(self.unchunk(crypted, plaintext))
+ self.logger.info("Wrote encrypted data to %s" % new_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))
+ (fname, new_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))
+ self.logger.error("Error reading %s, skipping: %s" % (fname, err))
return False
- plaintext = None
- pname = self.get_passphrase(crypted)
- if pname and self.setup['passphrase']:
- self.logger.warning("Passphrase given on command line differs from "
- "passphrase embedded in file, using "
- "passphrase in file")
- self.setup['passphrase'] = None
- if self.setup['passphrase']:
- pname = self.setup['passphrase']
- if not pname:
- for pname in self.setup.cfp.options('encryption'):
- self.logger.debug("Trying passphrase %s" % pname)
- passphrase = self.setup.cfp.get('encryption', pname)
+ self.set_passphrase()
+
+ plaintext = []
+ for chunk in self.chunk(crypted):
+ try:
+ passphrase, pname = self.get_passphrase(chunk)
try:
- plaintext = self._decrypt(crypted, passphrase)
- break
+ plaintext.append(self._decrypt(chunk, passphrase))
except Bcfg2.Encryption.EVPError:
- pass
+ self.logger.info("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))
- if not plaintext:
- self.logger.error("Could not decrypt %s with any passphrase in "
- "%s" % (fname, self.setup['configfile']))
- return False
- else:
- if not self.setup.cfp.has_option("encryption", pname):
- self.logger.error("Could not find passphrase %s in %s" %
- (pname,
- self.setup['configfile']))
- return False
- passphrase = self.setup.cfp.get("encryption", pname)
- 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))
- return False
+ return False
+ except TypeError:
+ pchunk = None
+ for pname in self.setup.cfp.options('encryption'):
+ self.logger.debug("Trying passphrase %s" % pname)
+ passphrase = self.setup.cfp.get('encryption', pname)
+ try:
+ pchunk = self._decrypt(chunk, passphrase)
+ break
+ except Bcfg2.Encryption.EVPError:
+ pass
+ except:
+ err = sys.exc_info()[1]
+ self.logger.error("Error decrypting %s: %s" %
+ (fname, err))
+ if pchunk is not None:
+ plaintext.append(pchunk)
+ else:
+ self.logger.error("Could not decrypt %s with any "
+ "passphrase in %s" %
+ (fname, self.setup['configfile']))
+ return False
+ new_fname = self.get_plaintext_filename(fname)
try:
- open(self.get_plaintext_filename(fname), "wb").write(plaintext)
- self.logger.info("Wrote decrypted data to %s" %
- self.get_plaintext_filename(fname))
+ open(new_fname, "wb").write(self.unchunk(plaintext, crypted))
+ self.logger.info("Wrote decrypted data to %s" % new_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))
+ (fname, new_fname, err))
return False
- def get_passphrase(self, crypted):
+ def get_passphrase(self, chunk):
+ pname = self._get_passphrase(chunk)
+ if not self.pname:
+ if not pname:
+ self.logger.info("No passphrase given on command line or "
+ "found in file")
+ return False
+ elif self.setup.cfp.has_option("encryption", pname):
+ passphrase = self.setup.cfp.get("encryption", pname)
+ else:
+ self.logger.error("Could not find passphrase %s in %s" %
+ (pname, self.setup['configfile']))
+ return False
+ else:
+ pname = self.pname
+ passphrase = self.passphrase
+ if self.pname != pname:
+ self.logger.warning("Passphrase given on command line (%s) "
+ "differs from passphrase embedded in "
+ "file (%s), using command-line option" %
+ (self.pname, pname))
+ return (passphrase, pname)
+
+ def _get_passphrase(self, chunk):
return None
def _decrypt(self, crypted, passphrase):
@@ -195,46 +218,53 @@ class CfgEncryptor(Encryptor):
class PropertiesEncryptor(Encryptor):
def _encrypt(self, plaintext, passphrase, name=None):
- xdata = lxml.etree.XML(plaintext)
+ # plaintext is an lxml.etree._Element
+ if name is None:
+ name = "true"
+ if plaintext.text and plaintext.text.strip():
+ plaintext.text = Bcfg2.Encryption.ssl_encrypt(plaintext.text,
+ passphrase)
+ plaintext.set("encrypted", name)
+ return plaintext
+
+ def chunk(self, data):
+ xdata = lxml.etree.XML(data)
if self.setup['xpath']:
elements = xdata.xpath(self.setup['xpath'])
else:
- elements = xdata.xpath('//*[@encrypted="true"]')
+ elements = xdata.xpath('//*[@encrypted]')
if not elements:
elements = list(xdata.getiterator())
-
- for el in elements:
- if el.text and el.text.strip():
- 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)
+ # this is not a good use of a generator, but we need to
+ # generate the full list of elements in order to ensure that
+ # some exist before we know what to return
+ for elt in elements:
+ yield elt
+
+ def unchunk(self, data, original):
+ # Properties elements are modified in-place, so we don't
+ # actually need to unchunk anything
+ xdata = data[0]
+ # find root element
+ while xdata.getparent() != None:
+ xdata = xdata.getparent()
+ xdata.set("encryption", "true")
return lxml.etree.tostring(xdata)
- def get_passphrase(self, crypted):
- xdata = lxml.etree.XML(crypted)
- pname = xdata.get("encryption")
+ def _get_passphrase(self, chunk):
+ pname = chunk.get("encrypted") or chunk.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)
+ # crypted is in lxml.etree._Element
+ if not crypted.text or not crypted.text.strip():
+ self.logger.warning("Skipping empty element %s" % crypted.tag)
+ return crypted
+ rv = Bcfg2.Encryption.ssl_decrypt(crypted.text, passphrase)
+ crypted.text = rv
+ return crypted
def main():
@@ -288,7 +318,7 @@ def main():
props = False
except IOError:
err = sys.exc_info()[1]
- logger.error("Error reading %s, skipping: %s" (fname, err))
+ logger.error("Error reading %s, skipping: %s" % (fname, err))
continue
except lxml.etree.XMLSyntaxError:
props = False
@@ -302,23 +332,24 @@ def main():
if setup['encrypt']:
if not encryptor.encrypt(fname):
- continue
+ print("Failed to encrypt %s, skipping" % fname)
elif setup['decrypt']:
if not encryptor.decrypt(fname):
- continue
+ print("Failed to decrypt %s, skipping" % fname)
else:
- logger.info("Neither --encrypt nor --decrypt specified, determining mode")
+ 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
+ print("Failed to encrypt %s, skipping" % fname)
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))
+ logger.error("Error removing %s: %s" % (fname, err))
continue
if __name__ == '__main__':
diff --git a/tools/manpagegen/bcfg2-crypt.8.ronn b/tools/manpagegen/bcfg2-crypt.8.ronn
index edf9660da..a164d47f1 100644
--- a/tools/manpagegen/bcfg2-crypt.8.ronn
+++ b/tools/manpagegen/bcfg2-crypt.8.ronn
@@ -41,18 +41,14 @@ what to do.
* `--xpath <xpath>`:
Encrypt the character content of all elements that match the
- specified XPath expression. The default is `*[@encrypted="true"]`
+ specified XPath expression. The default is `*[@encrypted]`
or `*`; see [MODES] below for more details. Only meaningful for
Properties files.
* `-p <passphrase>`:
- Specify the encryption/decryption passphrase. This can either be
- the literal passphrase, or the name of a passphrase specified in
- the `[encryption]` section of `bcfg2.conf`. If no passphrase is
- specified, then a) when decrypting, all passphrases will be tried
- sequentially; and b) when encrypting, you will be prompted for a
- passphrase from `bcfg2.conf`. It is never necessary to specify
- `-p` if you only have a single passphrase in `bcfg2.conf`.
+ Specify the name of a passphrase specified in the `[encryption]`
+ section of `bcfg2.conf`. See [SELECTING PASSPHRASE] below for
+ more details.
* `-v`:
Be verbose.
@@ -75,18 +71,38 @@ handled very differently.
* Properties:
When `bcfg2-crypt` is used on a Properties file, it encrypts the
character content of elements matching the XPath expression given
- by `--xpath`. By default the expression is
- `*[@encrypted="true"]`, which matches all elements with an
- `encrypted` attribute set to `true`. If you are encrypting a file
- and that expression doesn't match any elements, then the default
- is `*`, which matches everything. When `bcfg2-crypt` encrypts the
- character content of an element, it also adds the `encrypted`
- attribute, but when it decrypts an element it does not remove it;
- this lets you easily and efficiently run `bcfg2-crypt` against a
- single Properties file to encrypt and decrypt it without needing
- to specify a long list of options. See the online Bcfg2 docs on
+ by `--xpath`. By default the expression is `*[@encrypted]`, which
+ matches all elements with an `encrypted` attribute. If you are
+ encrypting a file and that expression doesn't match any elements,
+ then the default is `*`, which matches everything. When
+ `bcfg2-crypt` encrypts the character content of an element, it
+ also adds the `encrypted` attribute, set to the name of the
+ passphrase used to encrypt that element. When it decrypts an
+ element it does not remove `encrypted`, though; this lets you
+ easily and efficiently run `bcfg2-crypt` against a single
+ Properties file to encrypt and decrypt it without needing to
+ specify a long list of options. See the online Bcfg2 docs on
Properties files for more information on how this works.
+## SELECTING PASSPHRASE
+
+The passphrase used to encrypt or decrypt a file is discovered in the
+following order:
+
+ * First, the passphrase given on the command line using `-p` is
+ used.
+
+ * Next, if exactly one passphrase is specified in `bcfg2.conf`, it
+ will be used.
+
+ * Next, if operating in Properties mode, `bcfg2-crypt` will attempt
+ to read the name of the passphrase from the encrypted elements.
+
+ * Next, if decrypting, all passphrases will be tried sequentially.
+
+ * If no passphrase has been determined at this point, an error is
+ produced and the file being encrypted or decrypted is skipped.
+
## SEE ALSO
bcfg2-server(8)