├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.md ├── figgypy ├── __init__.py ├── config.py ├── decrypt.py ├── exceptions.py └── util.py ├── setup.py └── tests ├── __init__.py ├── config_test.py ├── decrypt_test.py ├── resources ├── test-config.yaml └── test-keys │ ├── .gpg-v21-migrated │ ├── foo.pub │ ├── foo.sec │ ├── key-input │ ├── private-keys-v1.d │ └── 1074E0168426CE0993D31200165D0BD653353B5C.key │ ├── pubring.gpg │ ├── pubring.gpg~ │ ├── secring.gpg │ └── trustdb.gpg └── util_test.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/python 2 | 3 | ### Python ### 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | env/ 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *,cover 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | 57 | # Sphinx documentation 58 | docs/_build/ 59 | 60 | # PyBuilder 61 | target/ 62 | 63 | Pipfile* 64 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - '3.4' 4 | - '3.5' 5 | - '3.6' 6 | - '3.7' 7 | install: 8 | - pip install . 9 | script: 10 | - python setup.py test 11 | # deploy: 12 | # provider: pypi 13 | # user: theherk 14 | # password: 15 | # secure: uAZwPnFCEO+CBux2eik7Dmb7NkBHqkDv3YwdHhY+t3Wu5LJlbf27sEzRWngp7KlJ3N7397Fn3Ow2CM0ppfB1Xj0ZCUNjJfgPaz15DA0ssg9348wMyVqzuKX4DnjN0GgeGU/8upEgX2HDAl640nCg6ogf7PBiq4TXgocUGjqoHmHo6N/Ppvk2c6QaPfq1iu5znUPwv9xYaXer8FV8XJ7NbEYW/YpnGLNeDFGLTE0/cQD4BTwRYNaKDUkoGQ8ijr2mXNffrcPFh5BwziGXFz2b7v0hTEVYjnkKIZpyQ27mOMcc/O1hC8vkuEfOjZAiBVb8J6NxX5h+fB3VvBQ/xsMvPs3FnVFOvb7nRWZv9tff6Il+sbRJdfZp/zc4vWQ08ALXKTtyoK6pLJJUSkR3QJM3/IPiSp6hw53udrB/i23yphDwUq3B9/I1EhBiddQhEnRzokXTBZMLxq23OkrUAP1YjHMMOIgf1y2DK9NIp6e8fcNhdoelWWpl7sC/T9iX8/2hj7ZVi+JrieJqh1pwg+yzt7OfTmgagWx7gbTzQI/SUFbB9YfkAIUyqK/JVYCv9KKa3mUBoZZfJn4RdLr/5u/uMsB8KGhFvkGkauKv1EW5ZzA84zdlnHrO9hOGhNzhmDc68zkuYp5x+1kQ1Kn34La4j5uMUqhv0s5nKpwAV8AM/vI= 16 | # on: 17 | # tags: true 18 | # branch: master 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Herkermer Sherwood 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.md -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | figgypy 2 | ======= 3 | 4 | [![Chat on Gitter](https://badges.gitter.im/theherk/figgypy.svg)](https://gitter.im/theherk/figgypy?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 5 | [![Build Status](https://travis-ci.org/theherk/figgypy.svg)](https://travis-ci.org/theherk/figgypy) 6 | 7 | A simple configuration parser. 8 | 9 | Installation 10 | ------------ 11 | 12 | pip install figgypy 13 | 14 | Usage 15 | ----- 16 | 17 | ``` python 18 | import figgypy 19 | cfg = figgypy.set_config(conf_file) 20 | cfg.get_value('somevalue', optional_default) 21 | # or 22 | cfg.values['somevalue'] 23 | # or 24 | cfg.values.get('somevalue', optional_default) 25 | # or 26 | figgypy.get_value('somevalue', optional_default) 27 | ``` 28 | 29 | Config object can be created with a filename only, relative path, or absolute path. 30 | If only name or relative path is provided, look in this order: 31 | 32 | 1. current directory 33 | 2. `~/.config/` 34 | 3. `/etc/` 35 | 36 | It is a good idea to include you `__package__` in the file name. 37 | For example, `cfg = Config(os.path.join(__package__, 'config.yaml'))`. 38 | This way it will look for `your_package/config.yaml`, 39 | `~/.config/your_package/config.yaml`, and `/etc/your_package/config.yaml`. 40 | 41 | ### Features ### 42 | 43 | #### Supports multiple formats #### 44 | 45 | The configuration file currently supports json, _xml*_, and yaml. 46 | 47 | _* note_ - xml will work, but since it requires having only one root, all of the configuration will be in a dictionary named that root. See examples below. 48 | 49 | #### Global configuration (optional) #### 50 | 51 | ``` python 52 | # a.py 53 | from figgypy import Config, set_config 54 | cfg = Config(config_file='config.yaml') 55 | figgypy.set_config(cfg) 56 | 57 | # b.py 58 | import figgypy 59 | figgypy.get_value('somevalue') 60 | ``` 61 | 62 | #### No file needed #### 63 | 64 | ``` python 65 | import figgypy 66 | cfg = figgypy.Config() 67 | cfg.set_value('somedict', {'a': 'aye', 'b': 'bee'}) 68 | ``` 69 | 70 | #### Optional decryption #### 71 | 72 | _note_: By default each is configured to run the decryption routine. This can be disabled. 73 | 74 | ``` python 75 | import figgypy 76 | cfg = figgypy.Config(config_file='config.yaml', decrypt_gpg=False, decrypt_kms=False) 77 | cfg.decrypt_kms = True 78 | # configuration is reloaded and decrypted 79 | ``` 80 | 81 | #### Reconstruct with updated settings #### 82 | 83 | You can run Config.setup to reconstruct the same Config object with new settings. Like this: 84 | 85 | ``` python 86 | # in shared.py 87 | import figgypy 88 | cfg = figgypy.Config() 89 | figgypy.set_config(cfg) 90 | 91 | # in worker.py 92 | import figgypy 93 | cfg = get_config() 94 | cfg.setup(config_file=file_, kms_decrypt=False, gpg_config=gpgconf) 95 | ``` 96 | 97 | These changes should also make testing in your applications easier, because in the tests you can reload a different configuration on the same object: 98 | 99 | ``` python 100 | import figgypy 101 | from mylib import totest 102 | totest.config_file = 'tests/resources/config.yaml' 103 | ``` 104 | 105 | Examples 106 | -------- 107 | 108 | ### json ### 109 | 110 | ```json 111 | { 112 | "db": { 113 | "url": "mydburl.com", 114 | "name": "mydbname", 115 | "user": "myusername", 116 | "pass": "correcthorsebatterystable" 117 | }, 118 | "log": { 119 | "file": "/var/log/cool_project.log", 120 | "level": "INFO" 121 | } 122 | } 123 | ``` 124 | 125 | cfg = Config('theabove.json') 126 | 127 | This yields object `cfg` with attributes `db` and `log`, each of which are dictionaries. 128 | 129 | ### xml ### 130 | 131 | ```xml 132 | 133 | 134 | 135 | mydburl.com 136 | mydbname 137 | myusername 138 | correcthorsebatterystable 139 | 140 | 141 | /var/log/cool_project.log 142 | INFO 143 | 144 | 145 | ``` 146 | 147 | cfg = Config('theabove.xml') 148 | 149 | This yields object `cfg` with attribute `config`, which is the complete dictionary. 150 | 151 | ### yaml ### 152 | 153 | ```yaml 154 | db: 155 | url: mydburl.com 156 | name: mydbname 157 | user: myusername 158 | pass: correcthorsebatterystable 159 | log: 160 | file: /var/log/cool_project.log 161 | level: INFO 162 | ``` 163 | 164 | cfg = Config('theabove.yaml') 165 | 166 | This yields object `cfg` with attributes `db` and `log`, each of which are dictionaries. This is the exact same behaviour as json, which makes sense given the close relationship of yaml and json. 167 | 168 | Secrets 169 | -------- 170 | 171 | It is possible to use gpg to store PGP and KMS encrypted secrets in a config file. 172 | 173 | ```yaml 174 | db: 175 | host: db.heck.ya 176 | pass: | 177 | -----BEGIN PGP MESSAGE----- 178 | Version: GnuPG v2 179 | 180 | hQIMAzf92ZrOUZL3ARAAgWexav8+pc2lnqISEuQafFZrqYI0pU3xCuMXnFZp+hpU 181 | gb0LsaExZ136p4ATIinFHuaLt94hFx7gULgqoSigt/2fubnUCsOGedq122xYZdtV 182 | Ep/24WPVQPcMVIP9pDTJTk82A41BQsOrVYorAGjjB13zFizizYHApNTcWKr4/gfR 183 | jmCqAX5qusXB84fXBecCJ886uEQI2v7+Vxnk+fQMqNt3ybd/uLuBLShMSygr6uLX 184 | zktyeZvP2QqPSWe0OpttdcvD792/SI/CTznsjbMe0wr1L81csEQcj++4o5wJop3Y 185 | mbQvG/FxeDdRi2aCxh7JK2xdCsrQzXKTNG2QZMwWqatB5Lb6lJ1mNiJQGX2YK+nI 186 | lbjy5Cp2lHlNxa9QfB+KglueMnH9gDku5YqBDos6rCEuqK/aTDdMx0V7YGYTamZ3 187 | 3Za+OGi+hl/+4WX2gm+bOM2WWrIysiu9k1HMI1/onui/3hr1nClR8rGb4a5qDlpg 188 | yRrt7LuLRU4vGXpYm05dXlUeI3uT04ur/DwLo32ujnPo3dc8LFegX8N8p1LLS9vq 189 | vvrvXRnWsgeAvAYFBprbEYcz7sOU04HM9OGcyjYREMs3Ih6H2oBi3GavJ2x0MG75 190 | M9JSTu/yytD8GCM3s+3RncKuEAxfZIk1Gbdz0pjb+U6G43qq8/vQPKtKuAeqJHDS 191 | SAER9YkKqbp0y85LbhUWNWPpHQ2zy8WB71TfYE6vBP5qjoxiqP/QGWjT/3jhCY+t 192 | 5k7R6XqvdvbSu1avFlEgApknzn94I+gsWQ== 193 | =QuDe 194 | -----END PGP MESSAGE----- 195 | ``` 196 | 197 | If you are using json, you'll need newlines. I achieved the following example with `cat the_above.yaml | seria -j -`. 198 | 199 | ```json 200 | { 201 | "db": { 202 | "host": "db.heck.ya", 203 | "pass": "-----BEGIN PGP MESSAGE-----\nVersion: GnuPG v2\n\nhQIMAzf92ZrOUZL3ARAAgWexav8+pc2lnqISEuQafFZrqYI0pU3xCuMXnFZp+hpU\ngb0LsaExZ136p4ATIinFHuaLt94hFx7gULgqoSigt/2fubnUCsOGedq122xYZdtV\nEp/24WPVQPcMVIP9pDTJTk82A41BQsOrVYorAGjjB13zFizizYHApNTcWKr4/gfR\njmCqAX5qusXB84fXBecCJ886uEQI2v7+Vxnk+fQMqNt3ybd/uLuBLShMSygr6uLX\nzktyeZvP2QqPSWe0OpttdcvD792/SI/CTznsjbMe0wr1L81csEQcj++4o5wJop3Y\nmbQvG/FxeDdRi2aCxh7JK2xdCsrQzXKTNG2QZMwWqatB5Lb6lJ1mNiJQGX2YK+nI\nlbjy5Cp2lHlNxa9QfB+KglueMnH9gDku5YqBDos6rCEuqK/aTDdMx0V7YGYTamZ3\n3Za+OGi+hl/+4WX2gm+bOM2WWrIysiu9k1HMI1/onui/3hr1nClR8rGb4a5qDlpg\nyRrt7LuLRU4vGXpYm05dXlUeI3uT04ur/DwLo32ujnPo3dc8LFegX8N8p1LLS9vq\nvvrvXRnWsgeAvAYFBprbEYcz7sOU04HM9OGcyjYREMs3Ih6H2oBi3GavJ2x0MG75\nM9JSTu/yytD8GCM3s+3RncKuEAxfZIk1Gbdz0pjb+U6G43qq8/vQPKtKuAeqJHDS\nSAER9YkKqbp0y85LbhUWNWPpHQ2zy8WB71TfYE6vBP5qjoxiqP/QGWjT/3jhCY+t\n5k7R6XqvdvbSu1avFlEgApknzn94I+gsWQ==\n=QuDe\n-----END PGP MESSAGE-----" 204 | } 205 | } 206 | ``` 207 | 208 | To store a KMS secret, just add the `_kms` key to the configuration file. 209 | 210 | ```yaml 211 | db: 212 | host: db.heck.ya 213 | pass: 214 | _kms: your KMS encrypted value 215 | ``` 216 | 217 | See [below](#kms) for instructions on generating this value. 218 | 219 | That's easy, right? Now this value will be decrypted and available just like you had typed in the value in the configuration file. 220 | 221 | ### Passed in parameters ### 222 | 223 | These can also be passed in as arguments when initializing. 224 | 225 | ```python 226 | aws_config = {'aws_access_key_id': aws_access_key_id, 227 | 'aws_secret_access_key': aws_secret_access_key, 228 | 'region_name': 'us-east-1'} 229 | gpg_config = {'homedir': 'noplace/like/home', 230 | 'keyring': 'pubring.kbx'} 231 | cfg = figgypy.Config('config.yaml', aws_config=aws_config, gpg_config=gpg_config) 232 | ``` 233 | 234 | ### To encrypt a value ### 235 | 236 | #### GPG #### 237 | 238 | echo -n "Your super secret password" | gpg --encrypt --armor -r KEY_ID 239 | 240 | Add the resulting armor to your configuration where necessary. If you are using yaml, this is very simple. Here is an example: 241 | 242 | #### KMS #### 243 | 244 | aws kms encrypt --key-id 'alias/your-key' --plaintext "your secret" --query CiphertextBlob --output text 245 | 246 | or the preferred method: 247 | 248 | ```python 249 | from figgypy.util import kms_encrypt 250 | encrypted = kms_encrypt('your secret', 'key or alias/key-alias', optional_aws_config) 251 | ``` 252 | 253 | Thanks 254 | ------ 255 | 256 | This tool uses [Seria](https://github.com/rtluckie/seria) to serialize between supported formats. Seria is a great tool if you want convert json, xml, or yaml to another of the same three formats. 257 | -------------------------------------------------------------------------------- /figgypy/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """figgypy is a simple configuration manager.""" 3 | 4 | __title__ = 'figgypy' 5 | __author__ = 'Adam Sherwood' 6 | 7 | from figgypy.config import Config 8 | 9 | _config = None 10 | 11 | 12 | def get_config(): 13 | """Get the global configuration. 14 | 15 | For this to work you must first call figgypy.set_config. See set_config for help. 16 | 17 | The only purpose of this helper, is so that we can raise an error telling the library 18 | user that they must run figgypy.set_config first. If we had them use just use 19 | figgypy._config, the initial value of None would give no indication of how to 20 | initialize the Config object. 21 | """ 22 | global _config 23 | if _config is None: 24 | raise ValueError('configuration not set; run figgypy.set_config first') 25 | return _config 26 | 27 | 28 | def get_value(*args, **kwargs): 29 | """Get from config object by exposing Config.get_value method. 30 | 31 | dict.get() method on Config.values 32 | """ 33 | global _config 34 | if _config is None: 35 | raise ValueError('configuration not set; must run figgypy.set_config first') 36 | return _config.get_value(*args, **kwargs) 37 | 38 | 39 | def set_config(config): 40 | """Set a global config. 41 | 42 | This should work properly whether or not you import the full package namespace. 43 | # a.py 44 | import figgypy 45 | cfg = figgypy.Config() 46 | figgypy.set_config(cfg) 47 | 48 | # b.py 49 | import figgypy 50 | cfg = figgypy.get_config() 51 | 52 | import a 53 | import b 54 | # same cfg from a 55 | 56 | You will get new instances if you import from the namespace. i.e. 57 | # c.py 58 | from figgypy import Config, set_config 59 | cfg = Config() 60 | set_config(cfg) 61 | 62 | # d.py 63 | from figgypy import get_config 64 | cfg = get_config() 65 | 66 | import c 67 | import d 68 | # same cfg from c 69 | """ 70 | global _config 71 | _config = config 72 | 73 | 74 | def set_value(*args, **kwargs): 75 | """Set value in the global Config object.""" 76 | global _config 77 | if _config is None: 78 | raise ValueError('configuration not set; must run figgypy.set_config first') 79 | return _config.set_value(*args, **kwargs) 80 | 81 | 82 | __all__ = ['Config', 'get_config', 'get_value', 'set_config', 'set_value'] 83 | -------------------------------------------------------------------------------- /figgypy/config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import logging 3 | import os 4 | import yaml 5 | 6 | import seria 7 | 8 | from figgypy.decrypt import ( 9 | gpg_decrypt, 10 | kms_decrypt, 11 | ssm_decrypt, 12 | ) 13 | from figgypy.exceptions import FiggypyError 14 | 15 | 16 | class Config(object): 17 | """Configuration object 18 | 19 | Args: 20 | config_file (optional[str]): filename 21 | see config_file property for more details 22 | aws_config (optional[dict]): aws credentials 23 | see aws_config property for more details 24 | dict of arguments passed into boto3 session 25 | example: 26 | aws_creds = {'aws_access_key_id': aws_access_key_id, 27 | 'aws_secret_access_key': aws_secret_access_key, 28 | 'region_name': 'us-east-1'} 29 | gpg_config (optional[dict]): gpg configuration 30 | see gpg_config property for more details 31 | dict of arguments for gpg including: 32 | homedir, binary, and keyring (require all if any) 33 | example: 34 | gpg_config = {'homedir': '~/.gnupg/', 35 | 'binary': 'gpg', 36 | 'keyring': 'pubring.kbx'} 37 | decrypt_gpg (optional[bool]): decrypt gpg secrets 38 | see decrypt_gpg property for more details 39 | defaults to True 40 | decrypt_kms (optional[bool]): decrypt kms secrets 41 | see decrypt_kms property for more details 42 | defaults to True 43 | decrypt_ssm (optional[bool]): decrypt/retrieve parameters 44 | from ssm parameter store. 45 | see decrypt_ssm property for more details 46 | defaults to True 47 | 48 | Returns: 49 | object: configuration object with 'values' dictionary 50 | 51 | move to config_file 52 | Object can be created with a filename only, relative path, or absolute path. 53 | If only name or relative path is provided, look in this order: 54 | 55 | 1. current directory 56 | 2. `~/.config/` 57 | 3. `/etc/` 58 | 59 | It is a good idea to include you __package__ in the file name. 60 | For example, `cfg = Config(os.path.join(__package__, 'config.yaml'))`. 61 | This way it will look for your_package/config.yaml, 62 | ~/.config/your_package/config.yaml, and /etc/your_package/config.yaml. 63 | """ 64 | _dirs = [ 65 | os.curdir, 66 | os.path.join(os.path.expanduser("~"), '.config'), 67 | "/etc/" 68 | ] 69 | 70 | def __init__(self, config_file=None, aws_config=None, gpg_config=None, 71 | decrypt_gpg=True, decrypt_kms=True, decrypt_ssm=True): 72 | # Must initialize values first, since other setters may load self.values 73 | self.values = {} 74 | self._aws_config = aws_config 75 | self._gpg_config = gpg_config 76 | self._decrypt_gpg = decrypt_gpg 77 | self._decrypt_kms = decrypt_kms 78 | self._decrypt_ssm = decrypt_ssm 79 | self._config_file = None 80 | # Load the file last so it can rely on the other properties. 81 | if config_file is not None: 82 | self.config_file = config_file 83 | 84 | @staticmethod 85 | def _find_file(f): 86 | """Find a config file if possible.""" 87 | if os.path.isabs(f): 88 | return f 89 | else: 90 | for d in Config._dirs: 91 | _f = os.path.join(d, f) 92 | if os.path.isfile(_f): 93 | return _f 94 | raise FiggypyError( 95 | "could not find configuration file {} in dirs {}" 96 | .format(f, Config._dirs) 97 | ) 98 | 99 | def _load_file(self, f): 100 | """Get values from config file""" 101 | try: 102 | with open(f, 'r') as _fo: 103 | _seria_in = seria.load(_fo) 104 | _y = _seria_in.dump('yaml') 105 | except IOError: 106 | raise FiggypyError("could not open configuration file") 107 | self.values.update(yaml.full_load(_y)) 108 | 109 | def _post_load_process(self): 110 | if self.decrypt_gpg: 111 | gpg_decrypt(self.values, self.gpg_config) 112 | if self.decrypt_kms: 113 | kms_decrypt(self.values, self.aws_config) 114 | if self.decrypt_ssm: 115 | ssm_decrypt(self.values, self.aws_config) 116 | for k, v in self.values.items(): 117 | setattr(self, k, v) 118 | 119 | @property 120 | def aws_config(self): 121 | if self._aws_config is None: 122 | self._aws_config = {} 123 | return self._aws_config 124 | 125 | @aws_config.setter 126 | def aws_config(self, value): 127 | if not isinstance(value, dict): 128 | raise ValueError('aws_config must be a dict') 129 | # Further validation for dict contents may be warranted. 130 | self._aws_config = value 131 | if self.values: 132 | self._post_load_process() 133 | 134 | @property 135 | def config_file(self): 136 | """Configuration file. 137 | 138 | File can be located with a filename only, relative path, or absolute path. 139 | If only name or relative path is provided, look in this order: 140 | 141 | 1. current directory 142 | 2. `~/.config/` 143 | 3. `/etc/` 144 | 145 | It is a good idea to include you __package__ in the file name. 146 | For example, `cfg = Config(os.path.join(__package__, 'config.yaml'))`. 147 | This way it will look for your_package/config.yaml, 148 | ~/.config/your_package/config.yaml, and /etc/your_package/config.yaml. 149 | """ 150 | return self._config_file 151 | 152 | @config_file.setter 153 | def config_file(self, config_file): 154 | self._load_file(self._find_file(config_file)) 155 | self._config_file = config_file 156 | self._post_load_process() 157 | 158 | @property 159 | def decrypt_gpg(self): 160 | return self._decrypt_gpg is not False 161 | 162 | @decrypt_gpg.setter 163 | def decrypt_gpg(self, value): 164 | self._decrypt_gpg = value is not False 165 | if self.values: 166 | self._post_load_process() 167 | 168 | @property 169 | def decrypt_kms(self): 170 | return self._decrypt_kms is not False 171 | 172 | @decrypt_kms.setter 173 | def decrypt_kms(self, value): 174 | self._decrypt_kms = value is not False 175 | if self.values: 176 | self._post_load_process() 177 | 178 | @property 179 | def decrypt_ssm(self): 180 | return self._decrypt_ssm is not False 181 | 182 | @decrypt_ssm.setter 183 | def decrypt_ssm(self, value): 184 | self._decrypt_ssm = value is not False 185 | if self.values: 186 | self._post_load_process() 187 | 188 | def get_value(self, *args, **kwargs): 189 | """Get from values dictionary by exposing self.values.get method. 190 | 191 | dict.get() method on Config.values 192 | """ 193 | return self.values.get(*args, **kwargs) 194 | 195 | @property 196 | def gpg_config(self): 197 | if self._gpg_config is None: 198 | self._gpg_config = {} 199 | return self._gpg_config 200 | 201 | @gpg_config.setter 202 | def gpg_config(self, value): 203 | if not isinstance(value, dict): 204 | raise ValueError('gpg_config must be a dict') 205 | # Further validation for dict contents may be warranted. 206 | self._gpg_config = value 207 | if self.values: 208 | self._post_load_process() 209 | 210 | def set_value(self, key, value): 211 | """Set value in values dict.""" 212 | self.values[key] = value 213 | 214 | def setup(self, config_file=None, aws_config=None, gpg_config=None, 215 | decrypt_gpg=True, decrypt_kms=True, decrypt_ssm=True): 216 | """Make setup easier by providing a constructor method. 217 | 218 | Move to config_file 219 | File can be located with a filename only, relative path, or absolute path. 220 | If only name or relative path is provided, look in this order: 221 | 222 | 1. current directory 223 | 2. `~/.config/` 224 | 3. `/etc/` 225 | 226 | It is a good idea to include you __package__ in the file name. 227 | For example, `cfg = Config(os.path.join(__package__, 'config.yaml'))`. 228 | This way it will look for your_package/config.yaml, 229 | ~/.config/your_package/config.yaml, and /etc/your_package/config.yaml. 230 | """ 231 | if aws_config is not None: 232 | self.aws_config = aws_config 233 | if gpg_config is not None: 234 | self.gpg_config = gpg_config 235 | if decrypt_kms is not None: 236 | self.decrypt_kms = decrypt_kms 237 | if decrypt_gpg is not None: 238 | self.decrypt_gpg = decrypt_gpg 239 | if decrypt_ssm is not None: 240 | self.decrypt_ssm = decrypt_ssm 241 | # Again, load the file last so that it can rely on other properties. 242 | if config_file is not None: 243 | self.config_file = config_file 244 | return self 245 | -------------------------------------------------------------------------------- /figgypy/decrypt.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Decrypt objects in Config.""" 3 | from __future__ import unicode_literals 4 | from future.utils import bytes_to_native_str as n 5 | 6 | from base64 import b64decode 7 | import logging 8 | import os 9 | 10 | import boto3 11 | from botocore.exceptions import ClientError, NoRegionError 12 | 13 | from figgypy.exceptions import FiggypyError 14 | 15 | LOG = logging.getLogger(__name__) 16 | 17 | GPG_IMPORTED = False 18 | try: 19 | from pretty_bad_protocol import gnupg 20 | import pretty_bad_protocol._parsers 21 | gnupg._parsers.Verify.TRUST_LEVELS["DECRYPTION_COMPLIANCE_MODE"] = 23 22 | GPG_IMPORTED = True 23 | except ImportError: 24 | LOG.exception('Could not load gnupg. Will be unable to unpack secrets.') 25 | 26 | 27 | def gpg_decrypt(cfg, gpg_config=None): 28 | """Decrypt GPG objects in configuration. 29 | 30 | Args: 31 | cfg (dict): configuration dictionary 32 | gpg_config (optional[dict]): gpg configuration 33 | dict of arguments for gpg including: 34 | homedir, binary, and keyring 35 | example: 36 | gpg_config = {'homedir': '~/.gnupg/', 37 | 'binary': 'gpg', 38 | 'keyring': 'pubring.kbx'} 39 | 40 | Returns: 41 | dict: decrypted configuration dictionary 42 | 43 | The aim is to find in the dictionary items which have been encrypted 44 | with gpg, then decrypt them if possible. 45 | 46 | We will either detect the encryption based on the PGP block text or a 47 | user can create a key "_gpg" in which to store the data. Either case 48 | will work. In the case of the "_gpg" key all data at this level will 49 | be replaced with the decrypted contents. For example: 50 | 51 | {'component': {'key': 'PGP Block ...'}} 52 | 53 | will transform to: 54 | 55 | {'component': {'key': 'decrypted value'}} 56 | 57 | However: 58 | 59 | {'component': {'key': {'_gpg': 'PGP Block ...', 'nothing': 'should go here'}}} 60 | 61 | will transform to: 62 | 63 | {'component': {'key': 'decrypted value'}} 64 | """ 65 | def decrypt(obj): 66 | """Decrypt the object. 67 | 68 | It is an inner function because we must first verify that gpg 69 | is ready. If we did them in the same function we would end up 70 | calling the gpg checks several times, potentially, since we are 71 | calling this recursively. 72 | """ 73 | if isinstance(obj, list): 74 | res_v = [] 75 | for item in obj: 76 | res_v.append(decrypt(item)) 77 | return res_v 78 | elif isinstance(obj, dict): 79 | if '_gpg' in obj: 80 | try: 81 | decrypted = gpg.decrypt(obj['_gpg']) 82 | if decrypted.ok: 83 | obj = n(decrypted.data.decode('utf-8').encode()) 84 | else: 85 | LOG.error("gpg error unpacking secrets %s", decrypted.stderr) 86 | except Exception as err: 87 | LOG.error("error unpacking secrets %s", err) 88 | else: 89 | for k, v in obj.items(): 90 | obj[k] = decrypt(v) 91 | else: 92 | try: 93 | if 'BEGIN PGP' in obj: 94 | try: 95 | decrypted = gpg.decrypt(obj) 96 | if decrypted.ok: 97 | obj = n(decrypted.data.decode('utf-8').encode()) 98 | else: 99 | LOG.error("gpg error unpacking secrets %s", decrypted.stderr) 100 | except Exception as err: 101 | LOG.error("error unpacking secrets %s", err) 102 | except TypeError: 103 | LOG.debug('Pass on decryption. Only decrypt strings') 104 | return obj 105 | 106 | gpg_config = gpg_config if gpg_config is not None else {} 107 | if GPG_IMPORTED: 108 | try: 109 | gpg = gnupg.GPG(**gpg_config) 110 | except (OSError, RuntimeError): 111 | LOG.exception('Failed to configure gpg. Will be unable to decrypt secrets.') 112 | return decrypt(cfg) 113 | return cfg 114 | 115 | 116 | def kms_decrypt(cfg, aws_config=None): 117 | """Decrypt KMS objects in configuration. 118 | 119 | Args: 120 | cfg (dict): configuration dictionary 121 | aws_config (dict): aws credentials 122 | dict of arguments passed into boto3 session 123 | example: 124 | aws_creds = {'aws_access_key_id': aws_access_key_id, 125 | 'aws_secret_access_key': aws_secret_access_key, 126 | 'region_name': 'us-east-1'} 127 | 128 | Returns: 129 | dict: decrypted configuration dictionary 130 | 131 | AWS credentials follow the standard boto flow. Provided values first, 132 | followed by environment, and then configuration files on the machine. 133 | Ideally, one would set up an IAM role for this machine to authenticate. 134 | 135 | The aim is to find in the dictionary items which have been encrypted 136 | with KMS, then decrypt them if possible. 137 | 138 | A user can create a key "_kms" in which to store the data. All data 139 | at this level will be replaced with the decrypted contents. For example: 140 | 141 | {'component': {'key': {'_kms': 'encrypted cipher text', 'nothing': 'should go here'}}} 142 | 143 | will transform to: 144 | 145 | {'component': {'key': 'decrypted value'}} 146 | 147 | To get the value to be stored as a KMS encrypted string: 148 | 149 | from figgypy.util import kms_encrypt 150 | encrypted = kms_encrypt('your secret', 'your key or alias', optional_aws_config) 151 | """ 152 | def decrypt(obj): 153 | """Decrypt the object. 154 | 155 | It is an inner function because we must first configure our KMS 156 | client. Then we call this recursively on the object. 157 | """ 158 | if isinstance(obj, list): 159 | res_v = [] 160 | for item in obj: 161 | res_v.append(decrypt(item)) 162 | return res_v 163 | elif isinstance(obj, dict): 164 | if '_kms' in obj: 165 | try: 166 | res = client.decrypt(CiphertextBlob=b64decode(obj['_kms'])) 167 | obj = n(res['Plaintext']) 168 | except ClientError as err: 169 | if 'AccessDeniedException' in err.args[0]: 170 | LOG.warning('Unable to decrypt %s. Key does not exist or no access', obj['_kms']) 171 | else: 172 | raise 173 | else: 174 | for k, v in obj.items(): 175 | obj[k] = decrypt(v) 176 | else: 177 | pass 178 | return obj 179 | 180 | aws_config = aws_config if aws_config is None else {} 181 | try: 182 | aws = boto3.session.Session(**aws_config) 183 | client = aws.client('kms') 184 | except NoRegionError: 185 | LOG.exception('Missing or invalid aws configuration. Will not be able to unpack KMS secrets.') 186 | return cfg 187 | return decrypt(cfg) 188 | 189 | 190 | def ssm_decrypt(cfg, aws_config=None): 191 | """Decrypt/get ssm parameter store objects in configuration. 192 | Args: 193 | cfg (dict): configuration dictionary 194 | aws_config (dict): aws credentials 195 | dict of arguments passed into boto3 session 196 | example: 197 | aws_creds = {'aws_access_key_id': aws_access_key_id, 198 | 'aws_secret_access_key': aws_secret_access_key, 199 | 'region_name': 'us-east-1'} 200 | 201 | Returns: 202 | dict: decrypted configuration dictionary 203 | 204 | AWS credentials follow the standard boto flow. Provided values first, 205 | followed by environment, and then configuration files on the machine. 206 | Ideally, one would set up an IAM role for this machine to authenticate. 207 | 208 | The aim is to find in the dictionary items which have been encrypted/stored 209 | with the ssm parameter store. 210 | 211 | A user can create a key "_ssm" in which to store the data. All data 212 | at this level will be treated as a parameter name and retrieved from ssm 213 | parameter store. For example: 214 | 215 | {'component': {'key': {'_ssm': 'name of parameter to get', 'nothing': 'should go here'}}} 216 | 217 | will transform to: 218 | 219 | {'component': {'key': 'retrieved/decrypted value'}} 220 | 221 | 222 | """ 223 | def decrypt(obj): 224 | """Decrypt/get the object. 225 | 226 | It is an inner function because we must first configure our ssm 227 | client. Then we call this recursively on the object. 228 | """ 229 | if isinstance(obj, list): 230 | res_v = [] 231 | for item in obj: 232 | res_v.append(decrypt(item)) 233 | return res_v 234 | elif isinstance(obj, dict): 235 | if '_ssm' in obj: 236 | try: 237 | res = client.get_parameter(Name=obj['_ssm'], WithDecryption=True) 238 | obj = n(res['Parameter']['Value']) 239 | except ClientError as err: 240 | if 'AccessDeniedException' in err.args[0]: 241 | LOG.warning('Unable to decrypt %s. Parameter does not exist or no access', obj['_ssm']) 242 | else: 243 | raise 244 | else: 245 | for k, v in obj.items(): 246 | obj[k] = decrypt(v) 247 | else: 248 | pass 249 | return obj 250 | 251 | aws_config = aws_config if aws_config is None else {} 252 | try: 253 | aws = boto3.session.Session(**aws_config) 254 | client = aws.client('ssm') 255 | except NoRegionError: 256 | LOG.info('Missing or invalid aws configuration. Will not be able to unpack KMS secrets.') 257 | return decrypt(cfg) 258 | -------------------------------------------------------------------------------- /figgypy/exceptions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Exceptions for figgypy.""" 3 | class FiggypyError(Exception): 4 | pass 5 | -------------------------------------------------------------------------------- /figgypy/util.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | from future.utils import bytes_to_native_str as n 4 | 5 | from base64 import b64encode 6 | import os 7 | 8 | import boto3 9 | 10 | 11 | def kms_encrypt(value, key, aws_config=None): 12 | """Encrypt and value with KMS key. 13 | 14 | Args: 15 | value (str): value to encrypt 16 | key (str): key id or alias 17 | aws_config (optional[dict]): aws credentials 18 | dict of arguments passed into boto3 session 19 | example: 20 | aws_creds = {'aws_access_key_id': aws_access_key_id, 21 | 'aws_secret_access_key': aws_secret_access_key, 22 | 'region_name': 'us-east-1'} 23 | 24 | Returns: 25 | str: encrypted cipher text 26 | """ 27 | aws_config = aws_config or {} 28 | aws = boto3.session.Session(**aws_config) 29 | client = aws.client('kms') 30 | enc_res = client.encrypt(KeyId=key, Plaintext=value) 31 | return n(b64encode(enc_res['CiphertextBlob'])) 32 | 33 | 34 | def ssm_store_parameter(name, value, key=None, aws_config=None): 35 | """Store a value in SSM Parameter Store. 36 | 37 | Args: 38 | name (str): name to store value under 39 | value (str): value to encrypt 40 | key (optional[str]): key id of kms key to use. if not passed in account 41 | default key will be used. 42 | aws_config (optional[dict]): aws credentials 43 | dict of arguments passed into boto3 session 44 | example: 45 | aws_creds = {'aws_access_key_id': aws_access_key_id, 46 | 'aws_secret_access_key': aws_secret_access_key, 47 | 'region_name': 'us-east-1'} 48 | 49 | Returns: 50 | str: name of parameter stored 51 | """ 52 | aws_config = aws_config or {} 53 | aws = boto3.session.Session(**aws_config) 54 | client = aws.client('ssm') 55 | params = { 56 | Name: name, 57 | Value: value, 58 | Overwrite: True, 59 | Description: "Figgypy created/updated parameter", 60 | Type: "SecureString", 61 | } 62 | if key: 63 | params["KeyId"] = key 64 | client.put_parameter(**params) 65 | return name 66 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup, find_packages 4 | import sys 5 | 6 | 7 | with open('README.md') as f: 8 | readme = f.read() 9 | 10 | install_requires = [ 11 | 'boto3', 12 | 'future', 13 | 'pretty-bad-protocol', 14 | 'seria', 15 | 'pyyaml' 16 | ] 17 | 18 | setup( 19 | name='figgypy', 20 | version='1.1.9', 21 | description='Simple configuration tool. Get config from yaml, json, or xml.', 22 | long_description=readme, 23 | long_description_content_type='text/markdown', 24 | author='Adam Sherwood', 25 | author_email='theherk@gmail.com', 26 | url='https://github.com/theherk/figgypy', 27 | download_url='https://github.com/theherk/figgypy/archive/1.2.dev.zip', 28 | packages=find_packages(), 29 | platforms=['all'], 30 | license='MIT', 31 | install_requires=install_requires, 32 | test_suite='tests' 33 | ) 34 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theherk/figgypy/1b4b61dd4920f6d040c96132705839459a60b520/tests/__init__.py -------------------------------------------------------------------------------- /tests/config_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | import unittest 4 | 5 | import figgypy.config 6 | 7 | 8 | class TestConfig(unittest.TestCase): 9 | def test_config_pass_on_int(self): 10 | c = figgypy.config.Config('tests/resources/test-config.yaml') 11 | self.assertEqual(c.number, 1) 12 | 13 | def test_config_load_with_gpg(self): 14 | c = figgypy.config.Config( 15 | config_file='tests/resources/test-config.yaml', 16 | gpg_config={'homedir': 'tests/resources/test-keys'} 17 | ) 18 | self.assertEqual(c.db['host'], 'db.heck.ya') 19 | self.assertEqual(c.db['pass'], 'test password') 20 | 21 | def test_config_load_without_gpg(self): 22 | figgypy.decrypt.GPG_IMPORTED = False 23 | c = figgypy.config.Config('tests/resources/test-config.yaml') 24 | encrypted_password = ( 25 | '-----BEGIN PGP MESSAGE-----\n' 26 | 'Version: GnuPG v2\n' 27 | '\n' 28 | 'hQIMAzf92ZrOUZL3ARAAgWexav8+pc2lnqISEuQafFZrqYI0pU3xCuMXnFZp+hpU\n' 29 | 'gb0LsaExZ136p4ATIinFHuaLt94hFx7gULgqoSigt/2fubnUCsOGedq122xYZdtV\n' 30 | 'Ep/24WPVQPcMVIP9pDTJTk82A41BQsOrVYorAGjjB13zFizizYHApNTcWKr4/gfR\n' 31 | 'jmCqAX5qusXB84fXBecCJ886uEQI2v7+Vxnk+fQMqNt3ybd/uLuBLShMSygr6uLX\n' 32 | 'zktyeZvP2QqPSWe0OpttdcvD792/SI/CTznsjbMe0wr1L81csEQcj++4o5wJop3Y\n' 33 | 'mbQvG/FxeDdRi2aCxh7JK2xdCsrQzXKTNG2QZMwWqatB5Lb6lJ1mNiJQGX2YK+nI\n' 34 | 'lbjy5Cp2lHlNxa9QfB+KglueMnH9gDku5YqBDos6rCEuqK/aTDdMx0V7YGYTamZ3\n' 35 | '3Za+OGi+hl/+4WX2gm+bOM2WWrIysiu9k1HMI1/onui/3hr1nClR8rGb4a5qDlpg\n' 36 | 'yRrt7LuLRU4vGXpYm05dXlUeI3uT04ur/DwLo32ujnPo3dc8LFegX8N8p1LLS9vq\n' 37 | 'vvrvXRnWsgeAvAYFBprbEYcz7sOU04HM9OGcyjYREMs3Ih6H2oBi3GavJ2x0MG75\n' 38 | 'M9JSTu/yytD8GCM3s+3RncKuEAxfZIk1Gbdz0pjb+U6G43qq8/vQPKtKuAeqJHDS\n' 39 | 'SAER9YkKqbp0y85LbhUWNWPpHQ2zy8WB71TfYE6vBP5qjoxiqP/QGWjT/3jhCY+t\n' 40 | '5k7R6XqvdvbSu1avFlEgApknzn94I+gsWQ==\n' 41 | '=QuDe\n' 42 | '-----END PGP MESSAGE-----\n' 43 | ) 44 | self.assertEqual(c.db['host'], 'db.heck.ya') 45 | self.assertEqual(c.db['pass'].rstrip('\n'), encrypted_password.rstrip('\n')) 46 | 47 | 48 | if __name__ == '__main__': 49 | unittest.main() 50 | -------------------------------------------------------------------------------- /tests/decrypt_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import unittest 3 | 4 | 5 | class TestDecrypt(unittest.TestCase): 6 | def test_gpg_decrypt(self): 7 | return 8 | 9 | def test_kms_decrypt(self): 10 | return 11 | 12 | def test_ssm_decrypt(self): 13 | return 14 | 15 | 16 | if __name__ == "__main__": 17 | unittest.main() 18 | -------------------------------------------------------------------------------- /tests/resources/test-config.yaml: -------------------------------------------------------------------------------- 1 | db: 2 | host: db.heck.ya 3 | pass: | 4 | -----BEGIN PGP MESSAGE----- 5 | Version: GnuPG v2 6 | 7 | hQIMAzf92ZrOUZL3ARAAgWexav8+pc2lnqISEuQafFZrqYI0pU3xCuMXnFZp+hpU 8 | gb0LsaExZ136p4ATIinFHuaLt94hFx7gULgqoSigt/2fubnUCsOGedq122xYZdtV 9 | Ep/24WPVQPcMVIP9pDTJTk82A41BQsOrVYorAGjjB13zFizizYHApNTcWKr4/gfR 10 | jmCqAX5qusXB84fXBecCJ886uEQI2v7+Vxnk+fQMqNt3ybd/uLuBLShMSygr6uLX 11 | zktyeZvP2QqPSWe0OpttdcvD792/SI/CTznsjbMe0wr1L81csEQcj++4o5wJop3Y 12 | mbQvG/FxeDdRi2aCxh7JK2xdCsrQzXKTNG2QZMwWqatB5Lb6lJ1mNiJQGX2YK+nI 13 | lbjy5Cp2lHlNxa9QfB+KglueMnH9gDku5YqBDos6rCEuqK/aTDdMx0V7YGYTamZ3 14 | 3Za+OGi+hl/+4WX2gm+bOM2WWrIysiu9k1HMI1/onui/3hr1nClR8rGb4a5qDlpg 15 | yRrt7LuLRU4vGXpYm05dXlUeI3uT04ur/DwLo32ujnPo3dc8LFegX8N8p1LLS9vq 16 | vvrvXRnWsgeAvAYFBprbEYcz7sOU04HM9OGcyjYREMs3Ih6H2oBi3GavJ2x0MG75 17 | M9JSTu/yytD8GCM3s+3RncKuEAxfZIk1Gbdz0pjb+U6G43qq8/vQPKtKuAeqJHDS 18 | SAER9YkKqbp0y85LbhUWNWPpHQ2zy8WB71TfYE6vBP5qjoxiqP/QGWjT/3jhCY+t 19 | 5k7R6XqvdvbSu1avFlEgApknzn94I+gsWQ== 20 | =QuDe 21 | -----END PGP MESSAGE----- 22 | number: 1 -------------------------------------------------------------------------------- /tests/resources/test-keys/.gpg-v21-migrated: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theherk/figgypy/1b4b61dd4920f6d040c96132705839459a60b520/tests/resources/test-keys/.gpg-v21-migrated -------------------------------------------------------------------------------- /tests/resources/test-keys/foo.pub: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | Version: GnuPG/MacGPG2 v2 3 | 4 | mQINBFYrwN0BEAC02x1PBWI65s+aciUzEE2hgz4IUhZwZqbfZ43GwP9clZ7S52FT 5 | O1XbHORNlA97nARvWgUV34HIUMKKdBh2dcy02qlfs4kUE49PRomeoMIpsJ1bYeu1 6 | Ii9ggFvPWBe8jQIrp5A29IiKTDxUaoqhOf0kLWYD++0/b0K+lMhGpd0n6VDAxSuX 7 | Bgm4yNNqdHAk1TLGKzxN3oJ48bwlvGuB3FBasNQoRZdXd1w3j7b69VDBQG04nKE8 8 | 0UN6Bm3ut8PDYi7tfYjVkZhCtditBY4BUFF0TreW2HjHe6g+Iub0o6kEGYGIBlTW 9 | mmA0s4eBLD+zZkI5AyITClD0bvnbs9WepzRbOXorIFQOqd5DXw/ZpX7bZdNWQGQ+ 10 | 1fBnqeGKQ2reJ8EiMM2kp9us9ctU1tyP5t/z7ccCHW7mkPOeSk9/n6Tg0gZlDZSf 11 | 0frKlj6AnN4qIY17rxlCxWd5CFupxXEmLzp9LOYQ4RxQ69OfmsZp8CAlSRqOB+8D 12 | Fyqni5R1eiTnC0wp4Hj1J6w6JcSl1q2+nLsKxtbdH5DBrNrzbfIdzkT78on1dVqX 13 | BFa/7K8PoEXdUWvzZTZPiwY58/ySRf9Phc7QjBZrupHLSPczyqiGaZedQRbwQSmr 14 | F8azi1IS+E1UrrO1/5bT9WbXn1XMwnisVpmy6jhL7oXiVL8NG3Jq79J0hwARAQAB 15 | tBt0ZXN0LWtleSA8dGVzdC1rZXlAZmlnZ3lweT6JAjkEEwEIACMFAlYrwN0CGy8H 16 | CwkIBwMCAQYVCAIJCgsEFgIDAQIeAQIXgAAKCRA3/dmazlGS9287EACSKZcCm67a 17 | VxjNa8n2IL1HDFBn7JmOWFJhPur1mjYnJ37eEEnNCVl7SkGId59j5PQ43B5zoX4b 18 | zYxXg2XF17tu1A/kOGS27joiZR9NaSc4QozOJahIgErAV5Q1/UO7Ugj3vxBnEMCw 19 | 3F4GjvpapaRAU2W0h61c5nT6aLjGHDr8sTEL+Bynp50HkUda8CARcZRJb296fJBc 20 | OmuzLslCmV/riYyHK4fNlpcuvA3T+Qj6Uz9x/mpJmWUU+7EJNbU2szEbbSg7qIer 21 | YpBY22cnwgBm08lKLtZpEGgTLnXcTsI1lMGbTqQ9CS4DR43QdJcY1ls+KEnclQZS 22 | Y9qyt+1mLXiuZjhzOm6mnFKf23k6STwd4eqC8S4Ez6ls8L/iSNgbzPMzFRKeBN4V 23 | 9CJvUUW4a9vGN7pOdhbmT5SoQwftpuWq3MgSaikJ3czjBS48l4FSAPvlKS1KGm/I 24 | XPtl1rePJ2BO/EwBEzLsK/vsf76FL3qoxsQii8YMawsKj4HN7Dtu8Log4zNss2yQ 25 | y3PTelZQwtYVwzeTvsYwHwGxrGYBv8NdS+GM4en7PJ1jbU1nPxgdDcQ6XAEDRDUb 26 | pSYXC4ELFpV8GEkIXZ6NqxShAA4FXfkZysJhInaJolWsz9E/Oy9DVPk1SeEEH3l6 27 | jHNDNilc1F5+6igFePaMCxlel8Zgs7LmzQ== 28 | =GEwd 29 | -----END PGP PUBLIC KEY BLOCK----- 30 | -------------------------------------------------------------------------------- /tests/resources/test-keys/foo.sec: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PRIVATE KEY BLOCK----- 2 | Version: GnuPG/MacGPG2 v2 3 | 4 | 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 54 | XNRefuooBXj2jAsZXpfGYLOy5s0= 55 | =phvL 56 | -----END PGP PRIVATE KEY BLOCK----- 57 | -------------------------------------------------------------------------------- /tests/resources/test-keys/key-input: -------------------------------------------------------------------------------- 1 | Key-Type: RSA 2 | Key-Length: 4096 3 | Name-Real: test-key 4 | Name-Email: test-key@figgypy 5 | Expire-Date: 0 6 | %pubring foo.pub 7 | %secring foo.sec 8 | %no-protection 9 | %commit 10 | #| %pubring foo.gpg 11 | #| %secring sec.gpg 12 | #| %commit 13 | -------------------------------------------------------------------------------- /tests/resources/test-keys/private-keys-v1.d/1074E0168426CE0993D31200165D0BD653353B5C.key: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theherk/figgypy/1b4b61dd4920f6d040c96132705839459a60b520/tests/resources/test-keys/private-keys-v1.d/1074E0168426CE0993D31200165D0BD653353B5C.key -------------------------------------------------------------------------------- /tests/resources/test-keys/pubring.gpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theherk/figgypy/1b4b61dd4920f6d040c96132705839459a60b520/tests/resources/test-keys/pubring.gpg -------------------------------------------------------------------------------- /tests/resources/test-keys/pubring.gpg~: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theherk/figgypy/1b4b61dd4920f6d040c96132705839459a60b520/tests/resources/test-keys/pubring.gpg~ -------------------------------------------------------------------------------- /tests/resources/test-keys/secring.gpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theherk/figgypy/1b4b61dd4920f6d040c96132705839459a60b520/tests/resources/test-keys/secring.gpg -------------------------------------------------------------------------------- /tests/resources/test-keys/trustdb.gpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theherk/figgypy/1b4b61dd4920f6d040c96132705839459a60b520/tests/resources/test-keys/trustdb.gpg -------------------------------------------------------------------------------- /tests/util_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | from future.utils import bytes_to_native_str as n 4 | 5 | from base64 import b64decode 6 | from distutils.util import strtobool 7 | import os 8 | import unittest 9 | 10 | import boto3 11 | 12 | from figgypy.util import kms_encrypt 13 | 14 | 15 | @unittest.skipUnless(strtobool(os.getenv('INTEGRATION', 'false')) == 1, 16 | reason="credentials are required") 17 | class TestEncryptIntegration(unittest.TestCase): 18 | def test_kms_encrypt(self): 19 | key = 'alias/figgypy-test' 20 | secret = 'correct horse battery staple' 21 | client = boto3.client('kms') 22 | encrypted = kms_encrypt(secret, key) 23 | dec_res = client.decrypt(CiphertextBlob=b64decode(encrypted)) 24 | decrypted = n(dec_res['Plaintext']) 25 | assert decrypted == secret 26 | 27 | def test_ssm_store_parameter(self): 28 | return 29 | 30 | 31 | if __name__ == '__main__': 32 | unittest.main() 33 | --------------------------------------------------------------------------------