Skip to content

Commit

Permalink
Allow for alternative key services via the entry point `credsmash.key…
Browse files Browse the repository at this point in the history
…_service`.
  • Loading branch information
nathan-muir committed Oct 3, 2016
1 parent c7491fa commit cfc7a2f
Show file tree
Hide file tree
Showing 5 changed files with 101 additions and 34 deletions.
22 changes: 17 additions & 5 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,17 @@
```cfg
[credsmash]
table_name = dev-credential-store
[credsmash:key_service:kms]
key_id = dev-credkey
[credsmash:encryption_context]
environment=production
purpose=web
encryption_context =
environment = production
purpose = web
```

You can override this by providing the `--table-name` and `--key-id` parameters to each command.

If you provide `--context` to a command, it will only append each key-value pair to the context,
rather than overwriting it completely.
Providing `--context` via the CLI will only work if manually specifying the `--key-id`, otherwise
it will read from the configuration file.

- The signatures of nearly every command has changed,

Expand Down Expand Up @@ -113,3 +114,14 @@
group: root
```
- You can define alternative key-services, by using the `credsmash.key_service` [entry point](http://setuptools.readthedocs.io/en/latest/pkg_resources.html#entry-points).

eg, to load a key service called `custom_ks`
```cfg
[credsmash]
key_service = custom_ks
[credsmash:key_service:custom_ks]
option_1 = a
option_2 = b
```
77 changes: 51 additions & 26 deletions credsmash/cli.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,34 @@
from __future__ import absolute_import, division, print_function

import codecs
import fnmatch
import logging
import operator
import sys
import os
import codecs
import sys

import boto3
import click
import pkg_resources

import credsmash.api
from credsmash.crypto import ALGO_AES_CTR
from credsmash.key_service import KeyService
from credsmash.util import set_stream_logger, detect_format, \
parse_config, read_one, read_many, write_one, write_many

logger = logging.getLogger(__name__)


class Environment(object):
def __init__(self, table_name, key_id, encryption_context, algorithm, algorithm_options):
def __init__(self, table_name, key_service_name, key_service_config, algorithm, algorithm_options):
self.table_name = table_name
self.key_id = key_id
self.encryption_context = encryption_context
self._key_service = None
self.key_service_name = key_service_name
self.key_service_config = key_service_config
self.algorithm = algorithm
self.algorithm_options = algorithm_options
self._session = None
self._dynamodb = None
self._kms = None

@property
def session(self):
Expand All @@ -41,22 +42,32 @@ def dynamodb(self):
self._dynamodb = self.session.resource('dynamodb')
return self._dynamodb

@property
def kms(self):
if self._kms is None:
self._kms = self.session.client('kms')
return self._kms
@staticmethod
def load_entry_point(group, name):
entry_points = pkg_resources.iter_entry_points(
group, name
)
for entry_point in entry_points:
return entry_point.load()
raise RuntimeError('Not found EntryPoint(group={0},name={1})'.format(group, name))

@property
def key_service(self):
return KeyService(self.kms, self.key_id, self.encryption_context)
if not self._key_service:
cls = self.load_entry_point('credsmash.key_service', self.key_service_name)
self._key_service = cls(
session=self.session,
**self.key_service_config
)
logger.debug('key_service=%r', self._key_service)
return self._key_service

@property
def secrets_table(self):
return self.dynamodb.Table(self.table_name)

def __repr__(self):
return 'Environment(table_name=%r,key_id=%r,ctx=%r)' % (self.table_name, self.key_id, self.encryption_context)
return 'Environment(table_name=%r,key_service=%r)' % (self.table_name, self._key_service)


@click.group()
Expand All @@ -71,7 +82,9 @@ def __repr__(self):
help="the KMS key-id of the master key "
"to use. See the README for more "
"information. Defaults to alias/credsmash")
@click.option('--context', type=(unicode, unicode), multiple=True)
@click.option('--context', type=(unicode, unicode), multiple=True,
help="the KMS encryption context to use."
"Only works if --key-id is passed.")
@click.pass_context
def main(ctx, config, table_name, key_id, context=None):
config_data = {}
Expand All @@ -80,18 +93,28 @@ def main(ctx, config, table_name, key_id, context=None):
with codecs.open(config, 'r') as config_fp:
sections = parse_config(config_fp)
config_data = sections.get('credsmash', {})
config_data['encryption_context'] = sections.get('credsmash:encryption_context', {})

if key_id:
# Using --key-id/-k will ignore the configuration file.
key_service_name = 'kms'
key_service_config = {
'key_id': key_id
}
if context:
key_service_config['encryption_context'] = dict(context)
else:
if context:
logger.warning('--context can only be used in conjunction with --key-id')
key_service_name = config_data.get('key_service', 'kms')
key_service_config = sections.get('credsmash:key_service:%s' % key_service_name, {})
if key_service_name == 'kms':
key_service_config.setdefault(
'key_id', config_data.get('key_id', 'alias/credsmash')
)

config_data.setdefault('table_name', 'secret-store')
if table_name:
config_data['table_name'] = table_name
config_data.setdefault('key_id', 'alias/credsmash')
if key_id:
config_data['key_id'] = key_id
config_data.setdefault('encryption_context', {})
if context:
# Start with the existing context, and append the new values to it
config_data['encryption_context'].update(dict(context))

algorithm = config_data.get('algorithm', ALGO_AES_CTR)
algorithm_options = sections.get('credsmash:%s' % algorithm, {})
Expand All @@ -100,10 +123,12 @@ def main(ctx, config, table_name, key_id, context=None):
level=config_data.get('log_level', 'INFO')
)
env = Environment(
config_data['table_name'], config_data['key_id'], config_data['encryption_context'],
algorithm, algorithm_options
config_data['table_name'],
key_service_name,
key_service_config,
algorithm,
algorithm_options
)
logger.debug('environment=%r', env)
ctx.obj = env


Expand Down
11 changes: 8 additions & 3 deletions credsmash/key_service.py → credsmash/kms_key_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
import botocore.exceptions


class KeyService(object):
def __init__(self, kms, key_id, encryption_context):
self.kms = kms
class KmsKeyService(object):
def __init__(self, session, key_id, encryption_context=None):
self.kms = session.client('kms')
self.key_id = key_id
if not encryption_context:
encryption_context = {}
self.encryption_context = encryption_context

def generate_key_data(self, number_of_bytes):
Expand Down Expand Up @@ -39,6 +41,9 @@ def decrypt(self, encoded_key):
raise KmsError(msg)
return kms_response['Plaintext']

def __repr__(self):
return 'KmsKeyService(key_id={0},context={1})'.format(self.key_id, self.encryption_context)


class KmsError(Exception):

Expand Down
22 changes: 22 additions & 0 deletions credsmash/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,10 +139,32 @@ def parse_config(fp):
config[section] = {}
for option in cp.options(section):
config_value = cp.get(section, option)
if config_value.startswith("\n"):
config_value = _parse_nested(config_value)
config[section][option] = config_value
return config


def _parse_nested(config_value):
# Given a value like this:
# \n
# foo = bar
# bar = baz
# We need to parse this into
# {'foo': 'bar', 'bar': 'baz}
parsed = {}
for line in config_value.splitlines():
line = line.strip()
if not line:
continue
# The caller will catch ValueError
# and raise an appropriate error
# if this fails.
key, value = line.split('=', 1)
parsed[key.strip()] = value.strip()
return parsed


def set_stream_logger(name='credsmash', level=logging.DEBUG, format_string=None):
if format_string is None:
format_string = "%(asctime)s %(name)s [%(levelname)s] %(message)s"
Expand Down
3 changes: 3 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ def read_markdown(*file_paths):
entry_points={
'console_scripts': [
'credsmash = credsmash.cli:main'
],
'credsmash.key_service': [
'kms = credsmash.kms_key_service:KmsKeyService',
]
},

Expand Down

0 comments on commit cfc7a2f

Please sign in to comment.