some modifications - VaultPass GPG-encrypted creds are almost working.
This commit is contained in:
		
							parent
							
								
									feb032b84f
								
							
						
					
					
						commit
						2545138ae1
					
				| @ -175,7 +175,8 @@ To determine the behaviour of how this behaves, please refer to the below table. | |||||||
| <!-- SNIP --> | <!-- SNIP --> | ||||||
|     <auth> |     <auth> | ||||||
|         <!-- "Automagic" (#1). |         <!-- "Automagic" (#1). | ||||||
|              First $VAULT_TOKEN environment variable is checked, then ~/.vault-token is checked. --> |              First $VAULT_TOKEN environment variable is checked, | ||||||
|  |              then ~/.vault-token is checked. --> | ||||||
|         <token/> |         <token/> | ||||||
| 
 | 
 | ||||||
|         <!-- Source is considered the only place to fetch token from (#2). --> |         <!-- Source is considered the only place to fetch token from (#2). --> | ||||||
| @ -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 <<gpg_elements, below>>) and allow you to use GPG to | elements. These elements are of the same composition (described <<gpg_elements, below>>) and allow you to use GPG to | ||||||
| encrypt that sensitive information. | encrypt that sensitive information. | ||||||
| 
 | 
 | ||||||
| Note that while this does increase security, it breaks compatibility with other XML parsers - they won't be able to | While this does increase security, it breaks compatibility with other XML parsers - they won't be able to decrypt and | ||||||
| decrypt and parse the encrypted snippet unless explicitly coded to do so. | parse the encrypted snippet unless explicitly coded to do so. | ||||||
| 
 | 
 | ||||||
| ==== `*Gpg` elements | ==== `*Gpg` elements | ||||||
| `*Gpg` elements (`authGpg`, `unsealGpg`) have the same structure: | `*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. | . `unsealGpg`/`authGpg`, the container element. | ||||||
| .. The path to the encrypted file as the contained text. | .. The path to the encrypted file as the contained text. | ||||||
| 
 | 
 | ||||||
| It has some optional attributes as well: | 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 | ||||||
| .`*Gpg` element attributes | `~/.gnupg/` (or whatever the compiled-in default is). | ||||||
| |=== |  | ||||||
| |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/`. |  | ||||||
| |=== |  | ||||||
| 
 | 
 | ||||||
| The contents of the encrypted file should match the **unencrypted** XML content it's replacing. | 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 | 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. | Let's look at an example of GPG-encrypted elements. | ||||||
| 
 | 
 | ||||||
| @ -273,11 +267,9 @@ Let's look at an example of GPG-encrypted elements. | |||||||
| 
 | 
 | ||||||
|     <server> |     <server> | ||||||
|         <uri>http://localhost:8000/</uri> |         <uri>http://localhost:8000/</uri> | ||||||
|         <unsealGpg keyFPR="D34DB33FD34DB33FD34DB33FD34DB33FD34DB33F" |         <unsealGpg gpgHome="~/.gnupg">~/.private/vaultpass/unseal.asc</unsealGpg> | ||||||
|                    gpgHome="~/.gnupg">~/.private/vaultpass/unseal.asc</unsealGpg> |  | ||||||
|     </server> |     </server> | ||||||
|     <authGpg keyFPR="D34DB33FD34DB33FD34DB33FD34DB33FD34DB33F" |     <authGpg gpgHome="~/.gnupg">~/.private/vaultpass/auth.gpg</unsealGpg> | ||||||
|                gpgHome="~/.gnupg">~/.private/vaultpass/auth.gpg</unsealGpg> |  | ||||||
| </vaultpass> | </vaultpass> | ||||||
| ---- | ---- | ||||||
| 
 | 
 | ||||||
|  | |||||||
							
								
								
									
										7
									
								
								testxml.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										7
									
								
								testxml.py
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,7 @@ | |||||||
|  | #!/usr/bin/env python3 | ||||||
|  | 
 | ||||||
|  | import vaultpass | ||||||
|  | 
 | ||||||
|  | cfg = vaultpass.config.LocalFile('/tmp/vaultpass.xml') | ||||||
|  | cfg.main() | ||||||
|  | 
 | ||||||
| @ -16,7 +16,6 @@ class PassMan(object): | |||||||
| 
 | 
 | ||||||
|     def __init__(self, cfg = '~/.config/vaultpass.xml'): |     def __init__(self, cfg = '~/.config/vaultpass.xml'): | ||||||
|         self.cfg = config.getConfig(cfg) |         self.cfg = config.getConfig(cfg) | ||||||
|         self.cfg.main() |  | ||||||
|         self._getURI() |         self._getURI() | ||||||
|         self.getClient() |         self.getClient() | ||||||
| 
 | 
 | ||||||
| @ -29,6 +28,8 @@ class PassMan(object): | |||||||
|     def getClient(self): |     def getClient(self): | ||||||
|         # This may need to be re-tooled in the future. |         # This may need to be re-tooled in the future. | ||||||
|         auth_xml = self.cfg.xml.find('auth') |         auth_xml = self.cfg.xml.find('auth') | ||||||
|  |         if auth_xml is None: | ||||||
|  |             raise RuntimeError('Could not find authentication') | ||||||
|         authmethod_xml = auth_xml.getchildren()[0] |         authmethod_xml = auth_xml.getchildren()[0] | ||||||
|         for a in dir(auth): |         for a in dir(auth): | ||||||
|             if a.startswith('_'): |             if a.startswith('_'): | ||||||
|  | |||||||
| @ -128,6 +128,7 @@ class Token(_AuthBase): | |||||||
|                     self.token = self._getEnv(e) |                     self.token = self._getEnv(e) | ||||||
|                 else: |                 else: | ||||||
|                     self.token = self._getFile(a) |                     self.token = self._getFile(a) | ||||||
|  |         self.client = hvac.Client(url = self.uri) | ||||||
|         self.client.token = self.token |         self.client.token = self.token | ||||||
|         self.authCheck() |         self.authCheck() | ||||||
|         return(None) |         return(None) | ||||||
|  | |||||||
| @ -3,6 +3,7 @@ import os | |||||||
| import logging | import logging | ||||||
| import re | import re | ||||||
| ## | ## | ||||||
|  | from . import gpg_handler | ||||||
| import requests | import requests | ||||||
| from lxml import etree | from lxml import etree | ||||||
| 
 | 
 | ||||||
| @ -12,6 +13,8 @@ _logger = logging.getLogger() | |||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class Config(object): | class Config(object): | ||||||
|  |     gpg = None | ||||||
|  |     gpg_elems = ('authGpg', 'unsealGpg') | ||||||
|     xsd_path = None |     xsd_path = None | ||||||
|     tree = None |     tree = None | ||||||
|     namespaced_tree = None |     namespaced_tree = None | ||||||
| @ -31,6 +34,31 @@ class Config(object): | |||||||
|             self.populateDefaults() |             self.populateDefaults() | ||||||
|         if validate: |         if validate: | ||||||
|             self.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) |         return(None) | ||||||
| 
 | 
 | ||||||
|     def fetch(self):  # Just a fail-safe; this is overridden by specific subclasses. |     def fetch(self):  # Just a fail-safe; this is overridden by specific subclasses. | ||||||
| @ -87,11 +115,8 @@ class Config(object): | |||||||
|         _logger.info('Rendered XSD.') |         _logger.info('Rendered XSD.') | ||||||
|         return(None) |         return(None) | ||||||
| 
 | 
 | ||||||
|     def parseRaw(self, parser = None): |     def parse(self): | ||||||
|         self.xml = etree.fromstring(self.raw, parser = parser) |         # This can used to "re-parse" the self.xml and self.namespaced_xml. | ||||||
|         _logger.debug('Generated xml.') |  | ||||||
|         self.namespaced_xml = etree.fromstring(self.raw, parser = parser) |  | ||||||
|         _logger.debug('Generated namespaced xml.') |  | ||||||
|         self.tree = self.xml.getroottree() |         self.tree = self.xml.getroottree() | ||||||
|         _logger.debug('Generated tree.') |         _logger.debug('Generated tree.') | ||||||
|         self.namespaced_tree = self.namespaced_xml.getroottree() |         self.namespaced_tree = self.namespaced_xml.getroottree() | ||||||
| @ -103,6 +128,26 @@ class Config(object): | |||||||
|         self.stripNS() |         self.stripNS() | ||||||
|         return(None) |         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): |     def populateDefaults(self): | ||||||
|         _logger.info('Populating missing values with defaults from XSD.') |         _logger.info('Populating missing values with defaults from XSD.') | ||||||
|         if not self.xsd: |         if not self.xsd: | ||||||
|  | |||||||
							
								
								
									
										39
									
								
								vaultpass/gpg_handler.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								vaultpass/gpg_handler.py
									
									
									
									
									
										Normal file
									
								
							| @ -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) | ||||||
| @ -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 |  | ||||||
| 
 |  | ||||||
		Reference in New Issue
	
	Block a user