From 2545138ae132b3938ce872e8361a9678deae6807 Mon Sep 17 00:00:00 2001 From: brent s Date: Sun, 29 Mar 2020 19:45:25 -0400 Subject: [PATCH] some modifications - VaultPass GPG-encrypted creds are almost working. --- docs/README.adoc | 32 +++++++++-------------- testxml.py | 7 +++++ vaultpass/__init__.py | 3 ++- vaultpass/auth.py | 1 + vaultpass/config.py | 55 ++++++++++++++++++++++++++++++++++++---- vaultpass/gpg_handler.py | 39 ++++++++++++++++++++++++++++ vaultpass/gpgauth.py | 16 ------------ 7 files changed, 111 insertions(+), 42 deletions(-) create mode 100755 testxml.py create mode 100644 vaultpass/gpg_handler.py delete mode 100644 vaultpass/gpgauth.py diff --git a/docs/README.adoc b/docs/README.adoc index 5c77ea6..97cc3f1 100644 --- a/docs/README.adoc +++ b/docs/README.adoc @@ -175,7 +175,8 @@ To determine the behaviour of how this behaves, please refer to the below table. + First $VAULT_TOKEN environment variable is checked, + then ~/.vault-token is checked. --> @@ -232,8 +233,8 @@ To get around needing to store plaintext credentials on-disk in any form, VaultP elements. These elements are of the same composition (described <>) and allow you to use GPG to encrypt that sensitive information. -Note that while this does increase security, it breaks compatibility with other XML parsers - they won't be able to -decrypt and parse the encrypted snippet unless explicitly coded to do so. +While this does increase security, it breaks compatibility with other XML parsers - they won't be able to decrypt and +parse the encrypted snippet unless explicitly coded to do so. ==== `*Gpg` elements `*Gpg` elements (`authGpg`, `unsealGpg`) have the same structure: @@ -241,23 +242,16 @@ decrypt and parse the encrypted snippet unless explicitly coded to do so. . `unsealGpg`/`authGpg`, the container element. .. The path to the encrypted file as the contained text. -It has some optional attributes as well: - -.`*Gpg` element attributes -|=== -|Attribute |Content - -|`keyFPR` footnote:optelem[] | The GPG key to use to decrypt the file. It accepts multiple key ID formats, but it's *highly* recommended to -use the full 40-character (without spaces) key fingerprint. If not specified, VaultPass will use the first private key -it finds in the keyring. -|`gpgHome` footnote:optelem[] | The GPG home directory. If not specified, VaultPass will first check the **`GNUPGHOME`** environment -variable. If that's empty, we'll default to `~/.gnupg/`. -|=== +It has one optional attribute, `gpgHome` footnote:optelem[] -- the GPG home directory to use. If not specified, +VaultPass will first check the **`GNUPGHOME`** environment variable. If that isn't defined, we'll default to +`~/.gnupg/` (or whatever the compiled-in default is). The contents of the encrypted file should match the **unencrypted** XML content it's replacing. CAUTION: Note that if you use namespaces in your `vaultpass.xml` config file, you **MUST** use matching declarations in -your encrypted file. +your encrypted file. You **MAY** exclude the `xsi:schemaLocation` specification, however, if it's the same as your +`vaultpass.xml`. It is **highly** recommended that you use the same xsi:shemaLocation, however (or leave it out +entirely). Let's look at an example of GPG-encrypted elements. @@ -273,11 +267,9 @@ Let's look at an example of GPG-encrypted elements. http://localhost:8000/ - ~/.private/vaultpass/unseal.asc + ~/.private/vaultpass/unseal.asc - ~/.private/vaultpass/auth.gpg + ~/.private/vaultpass/auth.gpg ---- diff --git a/testxml.py b/testxml.py new file mode 100755 index 0000000..8e96509 --- /dev/null +++ b/testxml.py @@ -0,0 +1,7 @@ +#!/usr/bin/env python3 + +import vaultpass + +cfg = vaultpass.config.LocalFile('/tmp/vaultpass.xml') +cfg.main() + diff --git a/vaultpass/__init__.py b/vaultpass/__init__.py index c88180e..ee1ae1c 100644 --- a/vaultpass/__init__.py +++ b/vaultpass/__init__.py @@ -16,7 +16,6 @@ class PassMan(object): def __init__(self, cfg = '~/.config/vaultpass.xml'): self.cfg = config.getConfig(cfg) - self.cfg.main() self._getURI() self.getClient() @@ -29,6 +28,8 @@ class PassMan(object): def getClient(self): # This may need to be re-tooled in the future. auth_xml = self.cfg.xml.find('auth') + if auth_xml is None: + raise RuntimeError('Could not find authentication') authmethod_xml = auth_xml.getchildren()[0] for a in dir(auth): if a.startswith('_'): diff --git a/vaultpass/auth.py b/vaultpass/auth.py index 2924607..58c70fd 100644 --- a/vaultpass/auth.py +++ b/vaultpass/auth.py @@ -128,6 +128,7 @@ class Token(_AuthBase): self.token = self._getEnv(e) else: self.token = self._getFile(a) + self.client = hvac.Client(url = self.uri) self.client.token = self.token self.authCheck() return(None) diff --git a/vaultpass/config.py b/vaultpass/config.py index 7b0efa1..11090f7 100644 --- a/vaultpass/config.py +++ b/vaultpass/config.py @@ -3,6 +3,7 @@ import os import logging import re ## +from . import gpg_handler import requests from lxml import etree @@ -12,6 +13,8 @@ _logger = logging.getLogger() class Config(object): + gpg = None + gpg_elems = ('authGpg', 'unsealGpg') xsd_path = None tree = None namespaced_tree = None @@ -31,6 +34,31 @@ class Config(object): self.populateDefaults() if validate: self.validate() + g = self.parseGpg() + if g: + # And do it again. + if populate_defaults: + self.populateDefaults() + if validate: + self.validate() + return(None) + + def decryptGpg(self, gpg_xml): + home = gpg_xml.attrib.get('gpgHome') + tag = gpg_xml.tag + ns_xml = self.xml.find(tag) + xml = self.stripNS(obj = ns_xml).tag + fpath = gpg_xml.text + if not self.gpg: + self.gpg = gpg_handler.GPG(home = home) + else: + self.gpg.gpg.home = home + self.gpg.initHome() + ns_dcrpt_xml = etree.fromstring(self.gpg.decrypt(fpath)) + dcrpt_xml = self.stripNS(obj = ns_dcrpt_xml) + ns_xml.getparent().replace(ns_xml, ns_dcrpt_xml) + xml.getparent().replace(xml, dcrpt_xml) + self.parse() return(None) def fetch(self): # Just a fail-safe; this is overridden by specific subclasses. @@ -87,11 +115,8 @@ class Config(object): _logger.info('Rendered XSD.') return(None) - def parseRaw(self, parser = None): - self.xml = etree.fromstring(self.raw, parser = parser) - _logger.debug('Generated xml.') - self.namespaced_xml = etree.fromstring(self.raw, parser = parser) - _logger.debug('Generated namespaced xml.') + def parse(self): + # This can used to "re-parse" the self.xml and self.namespaced_xml. self.tree = self.xml.getroottree() _logger.debug('Generated tree.') self.namespaced_tree = self.namespaced_xml.getroottree() @@ -103,6 +128,26 @@ class Config(object): self.stripNS() return(None) + def parseGpg(self): + gpg_elem_found = False # Change to True if we find any GPG-encrypted elems + search = [] + for x in self.gpg_elems: + search.append("local-name()='{0}'".format(x)) + search = '[{0}]'.format(' or '.join(search)) + print(search) + gpg_elems = self.namespaced_xml.findall('|'.join(search)) + for e in gpg_elems: + print(e) + return(gpg_elem_found) + + def parseRaw(self, parser = None): + self.xml = etree.fromstring(self.raw, parser = parser) + _logger.debug('Generated xml.') + self.namespaced_xml = etree.fromstring(self.raw, parser = parser) + _logger.debug('Generated namespaced xml.') + self.parse() + return(None) + def populateDefaults(self): _logger.info('Populating missing values with defaults from XSD.') if not self.xsd: diff --git a/vaultpass/gpg_handler.py b/vaultpass/gpg_handler.py new file mode 100644 index 0000000..dfa2d01 --- /dev/null +++ b/vaultpass/gpg_handler.py @@ -0,0 +1,39 @@ +import io +import logging +import os +## +import gpg # https://pypi.org/project/gpg/ + + +_logger = logging.getLogger() + + +class GPG(object): + home = None + gpg = None + + def __init__(self, home = None): + if home: + self.home = home + self.initHome() + + def decrypt(self, fpath): + fpath = os.path.abspath(os.path.expanduser(fpath)) + with open(fpath, 'rb') as fh: + iobuf = io.BytesIO(fh.read()) + iobuf.seek(0, 0) + rslt = self.gpg.decrypt(iobuf) + decrypted = rslt[0] + return(decrypted) + + def initHome(self): + if not self.home: + h = os.environ.get('GNUPGHOME') + if h: + self.home = h + if self.home: + self.home = os.path.abspath(os.path.expanduser(self.home)) + if not os.path.isdir(self.home): + raise ValueError('GPG home does not exist') + _logger.debug('Set GPG home to explicitly specified value {0}'.format(self.home)) + return(None) diff --git a/vaultpass/gpgauth.py b/vaultpass/gpgauth.py deleted file mode 100644 index 9c1282e..0000000 --- a/vaultpass/gpgauth.py +++ /dev/null @@ -1,16 +0,0 @@ -import os -import logging -## -import gpg - - -# Special shoutout to Jthan for ruining my life. - - -_logger = logging.getLogger() - - -class GPGAuth(object): - def __init__(self, gpgauth_xml): - pass -