Skip to content

Commit d019e15

Browse files
committedMay 1, 2018
initial draft of ProviderStore, MetaStore, and MostRecentProvider
1 parent c7fbad8 commit d019e15

File tree

6 files changed

+704
-3
lines changed

6 files changed

+704
-3
lines changed
 

‎doc/index.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,11 @@ Modules
2121
dynamodb_encryption_sdk.encrypted.table
2222
dynamodb_encryption_sdk.material_providers
2323
dynamodb_encryption_sdk.material_providers.aws_kms
24+
dynamodb_encryption_sdk.material_providers.most_recent
2425
dynamodb_encryption_sdk.material_providers.static
2526
dynamodb_encryption_sdk.material_providers.wrapped
27+
dynamodb_encryption_sdk.material_providers.store
28+
dynamodb_encryption_sdk.material_providers.store.meta
2629
dynamodb_encryption_sdk.materials
2730
dynamodb_encryption_sdk.materials.raw
2831
dynamodb_encryption_sdk.materials.wrapped

‎src/dynamodb_encryption_sdk/exceptions.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,19 @@ class SigningError(DynamodbEncryptionSdkError):
8787

8888
class SignatureVerificationError(DynamodbEncryptionSdkError):
8989
"""Otherwise undifferentiated error encountered while verifying a signature."""
90+
91+
92+
class ProviderStoreError(DynamodbEncryptionSdkError):
93+
"""Otherwise undifferentiated error encountered by a provider store."""
94+
95+
96+
class NoKnownVersionError(ProviderStoreError):
97+
"""Raised if a provider store cannot locate any version of the requested material."""
98+
99+
100+
class InvalidVersionError(ProviderStoreError):
101+
"""Raised if an invalid version of a material is requested."""
102+
103+
104+
class VersionAlreadyExistsError(ProviderStoreError):
105+
"""Raised if a version that is being added to a provider store already exists."""
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
# Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License"). You
4+
# may not use this file except in compliance with the License. A copy of
5+
# the License is located at
6+
#
7+
# http://aws.amazon.com/apache2.0/
8+
#
9+
# or in the "license" file accompanying this file. This file is
10+
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11+
# ANY KIND, either express or implied. See the License for the specific
12+
# language governing permissions and limitations under the License.
13+
"""Meta cryptographic provider store."""
14+
from collections import OrderedDict
15+
import logging
16+
from threading import RLock
17+
import time
18+
19+
import attr
20+
import six
21+
22+
try: # Python 3.5.0 and 3.5.1 have incompatible typing modules
23+
from typing import Any, Text # noqa pylint: disable=unused-import
24+
except ImportError: # pragma: no cover
25+
# We only actually need these imports when running the mypy checks
26+
pass
27+
28+
from dynamodb_encryption_sdk.exceptions import InvalidVersionError, NoKnownVersionError
29+
from dynamodb_encryption_sdk.identifiers import LOGGER_NAME
30+
from dynamodb_encryption_sdk.materials import CryptographicMaterials # noqa pylint: disable=unused-import
31+
from dynamodb_encryption_sdk.structures import EncryptionContext # noqa pylint: disable=unused-import
32+
from . import CryptographicMaterialsProvider
33+
from .store import ProviderStore
34+
35+
__all__ = ('MostRecentProvider',)
36+
_LOGGER = logging.getLogger(LOGGER_NAME)
37+
38+
39+
def _min_capacity_validator(instance, attribute, value):
40+
"""Attrs validator to require that value is at least 1."""
41+
if value < 1:
42+
raise ValueError('Cache capacity must be at least 1')
43+
44+
45+
@attr.s(init=False)
46+
class BasicCache(object):
47+
"""Most basic LRU cache."""
48+
49+
capacity = attr.ib(validator=(
50+
attr.validators.instance_of(int),
51+
_min_capacity_validator
52+
))
53+
54+
def __init__(self, capacity):
55+
# type: (int) -> None
56+
"""Workaround pending resolution of attrs/mypy interaction.
57+
https://github.com/python/mypy/issues/2088
58+
https://github.com/python-attrs/attrs/issues/215
59+
"""
60+
self.capacity = capacity
61+
attr.validate(self)
62+
self.__attrs_post_init__()
63+
64+
def __attrs_post_init__(self):
65+
# type; () -> None
66+
"""Initialize the internal cache."""
67+
self._cache_lock = RLock() # attrs confuses pylint: disable=attribute-defined-outside-init
68+
self.clear()
69+
70+
def _prune(self):
71+
# type: () -> None
72+
"""Prunes internal cache until internal cache is within the defined limit."""
73+
with self._cache_lock:
74+
while len(self._cache) > self.capacity:
75+
self._cache.popitem(last=False)
76+
77+
def put(self, name, value):
78+
# type: (Any, Any) -> None
79+
"""Add a value to the cache.
80+
81+
:param name: Hashable object to identify the value in the cache
82+
:param value: Value to add to cache
83+
"""
84+
with self._cache_lock:
85+
self._cache[name] = value
86+
self._prune()
87+
88+
def get(self, name):
89+
# type: (Any) -> Any
90+
"""Get a value from the cache."""
91+
with self._cache_lock:
92+
value = self._cache.pop(name)
93+
self.put(name, value) # bump to the from of the LRU
94+
return value
95+
96+
def clear(self):
97+
# type: () -> None
98+
"""Clear the cache."""
99+
with self._cache_lock:
100+
self._cache = OrderedDict() # type: OrderedDict[Any, Any]
101+
102+
103+
@attr.s(init=False)
104+
class MostRecentProvider(CryptographicMaterialsProvider):
105+
"""Cryptographic materials provider that uses a provider store to obtain cryptography
106+
materials.
107+
108+
When encrypting, the most recent provider that the provider store knows about will always
109+
be used.
110+
111+
:param provider_store: Provider store to use
112+
:type provider_store: dynamodb_encryption_sdk.material_providers.store.ProviderStore
113+
:param str material_name: Name of materials for which to ask the provider store
114+
:param float version_ttl: Max time in seconds to go until checking with provider store
115+
for a more recent version
116+
"""
117+
118+
_provider_store = attr.ib(validator=attr.validators.instance_of(ProviderStore))
119+
_material_name = attr.ib(validator=attr.validators.instance_of(six.string_types))
120+
_version_ttl = attr.ib(validator=attr.validators.instance_of(float))
121+
122+
def __init__(
123+
self,
124+
provider_store, # type: ProviderStore
125+
material_name, # type: Text
126+
version_ttl # type: float
127+
):
128+
# type: (...) -> None
129+
"""Workaround pending resolution of attrs/mypy interaction.
130+
https://github.com/python/mypy/issues/2088
131+
https://github.com/python-attrs/attrs/issues/215
132+
"""
133+
self._provider_store = provider_store
134+
self._material_name = material_name
135+
self._version_ttl = version_ttl
136+
attr.validate(self)
137+
self.__attrs_post_init__()
138+
139+
def __attrs_post_init__(self):
140+
# type: () -> None
141+
"""Initialize the cache."""
142+
self._lock = RLock()
143+
self._cache = BasicCache(1000)
144+
self.refresh()
145+
146+
def decryption_materials(self, encryption_context):
147+
# type: (EncryptionContext) -> CryptographicMaterials
148+
"""Return decryption materials.
149+
150+
:param encryption_context: Encryption context for request
151+
:type encryption_context: dynamodb_encryption_sdk.structures.EncryptionContext
152+
:raises AttributeError: if no decryption materials are available
153+
"""
154+
version = self._provider_store.version_from_material_description(encryption_context.material_description)
155+
try:
156+
provider = self._cache.get(version)
157+
except KeyError:
158+
try:
159+
provider = self._provider_store.provider(self._material_name, version)
160+
except InvalidVersionError:
161+
_LOGGER.exception('Unable to get decryption materials from provider store.')
162+
raise AttributeError('No decryption materials available')
163+
164+
self._cache.put(version, provider)
165+
166+
return provider.decryption_materials(encryption_context)
167+
168+
def _can_use_current(self):
169+
# type: () -> bool
170+
"""Determine if we can use the current known max version without asking the provider store.
171+
172+
:returns: decision
173+
:rtype: bool
174+
"""
175+
if self._version is None:
176+
return False
177+
178+
return time.time() - self._last_updated < self._version_ttl
179+
180+
def _set_most_recent_version(self, version):
181+
# type: (int) -> None
182+
"""Set the most recent version and update the last updated time.
183+
184+
:param int version: Version to set
185+
"""
186+
with self._lock:
187+
self._version = version
188+
self._last_updated = time.time()
189+
190+
def encryption_materials(self, encryption_context):
191+
# type: (EncryptionContext) -> CryptographicMaterials
192+
"""Return encryption materials.
193+
194+
:param encryption_context: Encryption context for request
195+
:type encryption_context: dynamodb_encryption_sdk.structures.EncryptionContext
196+
:raises AttributeError: if no encryption materials are available
197+
"""
198+
if self._can_use_current():
199+
return self._cache.get(self._version)
200+
201+
try:
202+
version = self._provider_store.max_version(self._material_name)
203+
except NoKnownVersionError:
204+
version = 0
205+
206+
try:
207+
provider = self._provider_store.get_or_create_provider(self._material_name, version)
208+
except InvalidVersionError:
209+
_LOGGER.exception('Unable to get encryption materials from provider store.')
210+
raise AttributeError('No encryption materials available')
211+
actual_version = self._provider_store.version_from_material_description(provider._material_description)
212+
# TODO: ^ should we promote material description from hidden?
213+
214+
self._cache.put(actual_version, provider)
215+
self._set_most_recent_version(actual_version)
216+
217+
return provider.encryption_materials(encryption_context)
218+
219+
def refresh(self):
220+
# type: () -> None
221+
"""Clear all local caches for this provider."""
222+
with self._lock:
223+
self._cache.clear()
224+
self._version = None # type: int
225+
self._last_updated = None # type: CryptographicMaterialsProvider
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License"). You
4+
# may not use this file except in compliance with the License. A copy of
5+
# the License is located at
6+
#
7+
# http://aws.amazon.com/apache2.0/
8+
#
9+
# or in the "license" file accompanying this file. This file is
10+
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11+
# ANY KIND, either express or implied. See the License for the specific
12+
# language governing permissions and limitations under the License.
13+
"""Cryptographic materials provider stores."""
14+
import abc
15+
16+
import six
17+
18+
try: # Python 3.5.0 and 3.5.1 have incompatible typing modules
19+
from typing import Dict, Text, Optional # noqa pylint: disable=unused-import
20+
except ImportError: # pragma: no cover
21+
# We only actually need these imports when running the mypy checks
22+
pass
23+
24+
from dynamodb_encryption_sdk.exceptions import InvalidVersionError, NoKnownVersionError
25+
from dynamodb_encryption_sdk.material_providers import CryptographicMaterialsProvider
26+
27+
__all__ = ('ProviderStore',)
28+
29+
30+
@six.add_metaclass(abc.ABCMeta)
31+
class ProviderStore(object):
32+
"""Provide a standard way to retrieve and/or create cryptographic materials providers."""
33+
34+
@abc.abstractmethod
35+
def get_or_create_provider(self, material_name, version):
36+
# type: (Text, int) -> CryptographicMaterialsProvider
37+
"""Obtain a cryptographic materials provider identified by a name and version.
38+
39+
If the requested version does not exist, a new one might be created.
40+
41+
:param str material_name: Material to locate
42+
:param int version: Version of material to locate (optional)
43+
:returns: cryptographic materials provider
44+
:rtype: dynamodb_encryption_sdk.material_providers.CryptographicMaterialsProvider
45+
:raises InvalidVersionError: if the requested version is not available and cannot be created
46+
"""
47+
48+
@abc.abstractmethod
49+
def version_from_material_description(self, material_description):
50+
# (Dict[Text, Text]) -> int
51+
"""Determine the version from the provided material description.
52+
53+
:param dict material_description: Material description to use with this request
54+
:returns: version to use
55+
:rtype: int
56+
"""
57+
58+
def max_version(self, material_name):
59+
# (Text) -> int
60+
"""Find the maximum known version of the specified material.
61+
62+
.. note::
63+
64+
Child classes should usually override this method.
65+
66+
:param str material_name: Material to locate
67+
:returns: Maximum known version
68+
:rtype: int
69+
:raises NoKnownVersion: if no version can be found
70+
"""
71+
raise NoKnownVersionError('No known version for name: "{}"'.format(material_name))
72+
73+
def provider(self, material_name, version=None):
74+
# type: (Text, Optional[int]) -> CryptographicMaterialsProvider
75+
"""Obtain a cryptographic materials provider identified by a name and version.
76+
77+
If the version is not provided, the maximum version will be used.
78+
79+
:param str material_name: Material to locate
80+
:param int version: Version of material to locate (optional)
81+
:returns: cryptographic materials provider
82+
:rtype: dynamodb_encryption_sdk.material_providers.CryptographicMaterialsProvider
83+
:raises InvalidVersionError: if the requested version is not found
84+
"""
85+
if version is None:
86+
try:
87+
version = self.max_version(material_name)
88+
except NoKnownVersionError:
89+
version = 0
90+
return self.get_or_create_provider(material_name, version)
91+
92+
def new_provider(self, material_name):
93+
# type: (Text) -> CryptographicMaterialsProvider
94+
"""Create a new provider with a version one greater than the current known maximum.
95+
96+
:param str material_name: Material to locate
97+
:returns: cryptographic materials provider
98+
:rtype: dynamodb_encryption_sdk.material_providers.CryptographicMaterialsProvider
99+
"""
100+
version = self.max_version(material_name) + 1
101+
return self.get_or_create_provider(material_name, version)
Lines changed: 343 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,343 @@
1+
# Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License"). You
4+
# may not use this file except in compliance with the License. A copy of
5+
# the License is located at
6+
#
7+
# http://aws.amazon.com/apache2.0/
8+
#
9+
# or in the "license" file accompanying this file. This file is
10+
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11+
# ANY KIND, either express or implied. See the License for the specific
12+
# language governing permissions and limitations under the License.
13+
"""Meta cryptographic provider store."""
14+
from enum import Enum
15+
16+
import attr
17+
from boto3.dynamodb.conditions import Attr, Key
18+
from boto3.dynamodb.types import Binary
19+
import botocore
20+
21+
try: # Python 3.5.0 and 3.5.1 have incompatible typing modules
22+
from typing import Dict, Optional, Text, Tuple # noqa pylint: disable=unused-import
23+
except ImportError: # pragma: no cover
24+
# We only actually need these imports when running the mypy checks
25+
pass
26+
27+
from dynamodb_encryption_sdk.delegated_keys.jce import JceNameLocalDelegatedKey
28+
from dynamodb_encryption_sdk.encrypted.table import EncryptedTable
29+
from dynamodb_encryption_sdk.exceptions import InvalidVersionError, NoKnownVersionError, VersionAlreadyExistsError
30+
from dynamodb_encryption_sdk.identifiers import EncryptionKeyType, KeyEncodingType
31+
from dynamodb_encryption_sdk.material_providers import CryptographicMaterialsProvider
32+
from dynamodb_encryption_sdk.material_providers.wrapped import WrappedCryptographicMaterialsProvider
33+
from . import ProviderStore
34+
35+
__all__ = ('MetaStore',)
36+
37+
38+
class MetaStoreAttributeNames(Enum):
39+
"""Names of attributes in the MetaStore table."""
40+
41+
PARTITION = 'N'
42+
SORT = 'V'
43+
INTEGRITY_ALGORITHM = 'intAlg'
44+
INTEGRITY_KEY = 'int'
45+
ENCRYPTION_ALGORITHM = 'encAlg'
46+
ENCRYPTION_KEY = 'enc'
47+
MATERIAL_TYPE_VERSION = 't'
48+
49+
50+
class MetaStoreValues(Enum):
51+
"""Static values for use by MetaStore."""
52+
53+
INTEGRITY_ALGORITHM = 'HmacSHA256'
54+
ENCRYPTION_ALGORITHM = 'AES'
55+
MATERIAL_TYPE_VERSION = '0'
56+
KEY_BITS = 256
57+
58+
59+
#: Field in material description to use for the MetaStore material name and version.
60+
_MATERIAL_DESCRIPTION_META_FIELD = 'amzn-ddb-meta-id'
61+
62+
63+
@attr.s(init=False)
64+
class MetaStore(ProviderStore):
65+
"""Create and retrieve wrapped cryptographic materials providers, storing their cryptographic
66+
materials using the provided encrypted table.
67+
68+
:param table: Pre-configured boto3 DynamoDB Table object
69+
:type table: boto3.resources.base.ServiceResource
70+
:param materials_provider: Cryptographic materials provider to use
71+
:type materials_provider: dynamodb_encryption_sdk.material_providers.CryptographicMaterialsProvider
72+
"""
73+
74+
_table = attr.ib(validator=attr.validators.instance_of(botocore.client.BaseClient))
75+
_materials_provider = attr.ib(validator=attr.validators.instance_of(CryptographicMaterialsProvider))
76+
77+
def __init__(self, table, materials_provider):
78+
# type: (botocore.client.BaseClient, CryptographicMaterialsProvider) -> None
79+
"""Workaround pending resolution of attrs/mypy interaction.
80+
https://github.com/python/mypy/issues/2088
81+
https://github.com/python-attrs/attrs/issues/215
82+
"""
83+
self._table = table
84+
self._materials_provider = materials_provider
85+
attr.validate(self)
86+
self.__attrs_post_init__()
87+
88+
def __attrs_post_init__(self):
89+
# type: () -> None
90+
"""Prepare the encrypted table resource from the provided table and materials provider."""
91+
self._encrypted_table = EncryptedTable( # attrs confuses pylint: disable=attribute-defined-outside-init
92+
table=self._table,
93+
materials_provider=self._materials_provider
94+
)
95+
96+
def create_table(self, read_units, write_units):
97+
# type: (int, int) -> None
98+
"""Create the table for this MetaStore.
99+
100+
:param int read_units: Read capacity units to provision
101+
:param int write_units: Write capacity units to provision
102+
"""
103+
try:
104+
self._table.meta.client.create_table(
105+
TableName=self._table.name,
106+
AttributeDefinitions=[
107+
{
108+
'AttributeName': MetaStoreAttributeNames.PARTITION.value,
109+
'AttributeType': 'S'
110+
},
111+
{
112+
'AttributeName': MetaStoreAttributeNames.SORT.value,
113+
'AttributeName': 'N'
114+
}
115+
],
116+
KeySchema=[
117+
{
118+
'AttributeName': MetaStoreAttributeNames.PARTITION.value,
119+
'KeyType': 'HASH'
120+
},
121+
{
122+
'AttributeName': MetaStoreAttributeNames.SORT.value,
123+
'KeyType': 'RANGE'
124+
}
125+
],
126+
ProvisionedThroughput={
127+
'ReadCapacityUnits': read_units,
128+
'WriteCapacityUnits': write_units
129+
}
130+
)
131+
except botocore.exceptions.ClientError:
132+
raise Exception('TODO: Could not create table')
133+
134+
def _load_materials(self, material_name, version):
135+
# type: (Text, int) -> Tuple[JceNameLocalDelegatedKey, JceNameLocalDelegatedKey]
136+
"""Load materials from table.
137+
138+
:returns: Materials loaded into delegated keys
139+
:rtype: tuple of JceNameLocalDelegatedKey
140+
"""
141+
key = {
142+
MetaStoreAttributeNames.PARTITION.value: material_name,
143+
MetaStoreAttributeNames.SORT.value: version
144+
}
145+
response = self._encrypted_table.get_item(Key=key)
146+
try:
147+
item = response['Item']
148+
except KeyError:
149+
raise InvalidVersionError('Version not found: "{}#{}"'.format(material_name, version))
150+
151+
try:
152+
encryption_key_kwargs = dict(
153+
key=item[MetaStoreAttributeNames.ENCRYPTION_KEY.value],
154+
algorithm=item[MetaStoreAttributeNames.ENCRYPTION_ALGORITHM.value],
155+
key_type=EncryptionKeyType.SYMMETRIC,
156+
key_encoding=KeyEncodingType.RAW
157+
)
158+
signing_key_kwargs = dict(
159+
key=item[MetaStoreAttributeNames.INTEGRITY_KEY.value],
160+
algorithm=item[MetaStoreAttributeNames.INTEGRITY_ALGORITHM.value],
161+
key_type=EncryptionKeyType.SYMMETRIC,
162+
key_encoding=KeyEncodingType.RAW
163+
)
164+
except KeyError:
165+
raise Exception('TODO: Invalid record')
166+
167+
if item[MetaStoreAttributeNames.MATERIAL_TYPE_VERSION] != MetaStoreValues.MATERIAL_TYPE_VERSION:
168+
raise InvalidVersionError('Unsupported material type: "{}"'.format(
169+
item[MetaStoreAttributeNames.MATERIAL_TYPE_VERSION]
170+
))
171+
172+
encryption_key = JceNameLocalDelegatedKey(**encryption_key_kwargs)
173+
signing_key = JceNameLocalDelegatedKey(**signing_key_kwargs)
174+
return encryption_key, signing_key
175+
176+
def _save_materials(self, material_name, version, encryption_key, signing_key):
177+
# type: (Text, int, JceNameLocalDelegatedKey, JceNameLocalDelegatedKey) -> None
178+
"""Save materials to the table, raising an error if the version already exists.
179+
180+
:param str material_name: Material to locate
181+
:param int version: Version of material to locate
182+
:raises VersionAlreadyExistsError: if the specified version already exists
183+
"""
184+
item = {
185+
MetaStoreAttributeNames.PARTITION.value: material_name,
186+
MetaStoreAttributeNames.SORT.value: version,
187+
MetaStoreAttributeNames.MATERIAL_TYPE_VERSION.value: MetaStoreValues.MATERIAL_TYPE_VERSION.value,
188+
MetaStoreAttributeNames.ENCRYPTION_ALGORITHM.value: encryption_key.algorithm,
189+
MetaStoreAttributeNames.ENCRYPTION_KEY.value: Binary(encryption_key.key),
190+
MetaStoreAttributeNames.INTEGRITY_ALGORITHM.value: signing_key.algorithm,
191+
MetaStoreAttributeNames.INTEGRITY_KEY.value: Binary(signing_key.key)
192+
}
193+
try:
194+
self._encrypted_table.put_item(
195+
Item=item,
196+
ConditionExpression=(
197+
Attr(MetaStoreAttributeNames.PARTITION.value).not_exists() &
198+
Attr(MetaStoreAttributeNames.SORT.value).not_exists()
199+
)
200+
)
201+
except botocore.exceptions.ClientError as error:
202+
if error.response['Error']['Code'] == 'ConditionalCheckFailedException':
203+
raise VersionAlreadyExistsError('Version already exists: "{}#{}"'.format(material_name, version))
204+
205+
def _save_or_load_materials(
206+
self,
207+
material_name, # type: Text
208+
version, # type: int
209+
encryption_key, # type: JceNameLocalDelegatedKey
210+
signing_key # type: JceNameLocalDelegatedKey
211+
):
212+
# type: (...) -> Tuple[JceNameLocalDelegatedKey, JceNameLocalDelegatedKey]
213+
"""Attempt to save the materials to the table.
214+
215+
If the specified version already exists, the existing materials will be loaded from
216+
the table and returned. Otherwise, the provided materials will be returned.
217+
218+
:param str material_name: Material to locate
219+
:param int version: Version of material to locate
220+
:param encryption_key: Loaded encryption key
221+
:type encryption_key: dynamodb_encryption_sdk.delegated_keys.jce.JceNameLocalDelegatedKey
222+
:param signing_key: Loaded signing key
223+
:type signing_key: dynamodb_encryption_sdk.delegated_keys.jce.JceNameLocalDelegatedKey
224+
"""
225+
try:
226+
self._save_materials(material_name, version, encryption_key, signing_key)
227+
return encryption_key, signing_key
228+
except VersionAlreadyExistsError:
229+
return self._load_materials(material_name, version)
230+
231+
@staticmethod
232+
def _material_description(material_name, version):
233+
# type: (Text, int) -> Dict[Text, Text]
234+
"""Build a material description from a material name and version.
235+
236+
:param str material_name: Material to locate
237+
:param int version: Version of material to locate
238+
"""
239+
return {_MATERIAL_DESCRIPTION_META_FIELD: '{name}#{version}'.format(name=material_name, version=version)}
240+
241+
def _load_provider_from_table(self, material_name, version):
242+
# type: (Text, int) -> CryptographicMaterialsProvider
243+
"""Load a provider from the table.
244+
245+
:param str material_name: Material to locate
246+
:param int version: Version of material to locate
247+
"""
248+
encryption_key, signing_key = self._load_materials(material_name, version)
249+
return WrappedCryptographicMaterialsProvider(
250+
signing_key=signing_key,
251+
wrapping_key=encryption_key,
252+
unwrapping_key=encryption_key,
253+
material_description=self._material_description(material_name, version)
254+
)
255+
256+
def get_or_create_provider(self, material_name, version):
257+
# type: (Text, int) -> CryptographicMaterialsProvider
258+
"""Obtain a cryptographic materials provider identified by a name and version.
259+
260+
If the requested version does not exist, a new one will be created.
261+
262+
:param str material_name: Material to locate
263+
:param int version: Version of material to locate
264+
:returns: cryptographic materials provider
265+
:rtype: dynamodb_encryption_sdk.material_providers.CryptographicMaterialsProvider
266+
:raises InvalidVersionError: if the requested version is not available and cannot be created
267+
"""
268+
encryption_key = JceNameLocalDelegatedKey.generate(
269+
MetaStoreValues.ENCRYPTION_ALGORITHM.value,
270+
MetaStoreValues.KEY_BITS.value
271+
)
272+
signing_key = JceNameLocalDelegatedKey.generate(
273+
MetaStoreValues.INTEGRITY_ALGORITHM.value,
274+
MetaStoreValues.KEY_BITS.value
275+
)
276+
encryption_key, signing_key = self._save_or_load_materials(material_name, version, encryption_key, signing_key)
277+
return WrappedCryptographicMaterialsProvider(
278+
signing_key=signing_key,
279+
wrapping_key=encryption_key,
280+
unwrapping_key=encryption_key,
281+
material_description=self._material_description(material_name, version)
282+
)
283+
284+
def provider(self, material_name, version=None):
285+
# type: (Text, Optional[int]) -> CryptographicMaterialsProvider
286+
"""Obtain a cryptographic materials provider identified by a name and version.
287+
288+
If the version is provided, an error will be raised if that version is not found.
289+
290+
If the version is not provided, the maximum version will be used.
291+
292+
:param str material_name: Material to locate
293+
:param int version: Version of material to locate (optional)
294+
:returns: cryptographic materials provider
295+
:rtype: dynamodb_encryption_sdk.material_providers.CryptographicMaterialsProvider
296+
:raises InvalidVersionError: if the requested version is not found
297+
"""
298+
if version is not None:
299+
return self._load_materials(material_name, version)
300+
301+
return super(MetaStore, self).provider(material_name, version)
302+
303+
def version_from_material_description(self, material_description):
304+
# (Dict[Text, Text]) -> int
305+
"""Determine the version from the provided material description.
306+
307+
:param dict material_description: Material description to use with this request
308+
:returns: version to use
309+
:rtype: int
310+
"""
311+
try:
312+
info = material_description[_MATERIAL_DESCRIPTION_META_FIELD]
313+
except KeyError:
314+
raise Exception('TODO: No info found')
315+
316+
try:
317+
return int(info.split('#', 1)[1])
318+
except (IndexError, ValueError):
319+
raise Exception('TODO: Malformed info')
320+
321+
def max_version(self, material_name):
322+
# (Text) -> int
323+
"""Find the maximum known version of the specified material.
324+
325+
:param str material_name: Material to locate
326+
:returns: Maximum known version
327+
:rtype: int
328+
:raises NoKnownVersion: if no version can be found
329+
"""
330+
response = self._encrypted_table.query(
331+
KeyConditionExpression=Key(MetaStoreAttributeNames.PARTITION.value).eq(material_name),
332+
ScanIndexForward=False,
333+
Limit=1
334+
)
335+
336+
if not response['Items']:
337+
raise NoKnownVersionError('No known version for name: "{}"'.format(material_name))
338+
339+
return response['Items'][0][MetaStoreAttributeNames.SORT.value]
340+
341+
def replicate(self, material_name, version, target):
342+
""""""
343+
raise NotImplementedError('TODO: implement this')

‎src/dynamodb_encryption_sdk/material_providers/wrapped.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,17 @@
1212
# language governing permissions and limitations under the License.
1313
"""Cryptographic materials provider to use ephemeral content encryption keys wrapped by delegated keys."""
1414
import attr
15+
import six
1516

1617
try: # Python 3.5.0 and 3.5.1 have incompatible typing modules
17-
from typing import Optional # noqa pylint: disable=unused-import
18+
from typing import Dict, Optional, Text # noqa pylint: disable=unused-import
1819
except ImportError: # pragma: no cover
1920
# We only actually need these imports when running the mypy checks
2021
pass
2122

2223
from dynamodb_encryption_sdk.delegated_keys import DelegatedKey
2324
from dynamodb_encryption_sdk.exceptions import UnwrappingError, WrappingError
25+
from dynamodb_encryption_sdk.internal.validators import dictionary_validator
2426
from dynamodb_encryption_sdk.materials.wrapped import WrappedCryptographicMaterials
2527
from dynamodb_encryption_sdk.structures import EncryptionContext # noqa pylint: disable=unused-import
2628
from . import CryptographicMaterialsProvider
@@ -59,21 +61,30 @@ class WrappedCryptographicMaterialsProvider(CryptographicMaterialsProvider):
5961
validator=attr.validators.optional(attr.validators.instance_of(DelegatedKey)),
6062
default=None
6163
)
64+
_material_description = attr.ib(
65+
validator=attr.validators.optional(dictionary_validator(six.string_types, six.string_types)),
66+
default=attr.Factory(dict)
67+
)
6268

6369
def __init__(
6470
self,
6571
signing_key, # type: DelegatedKey
6672
wrapping_key=None, # type: Optional[DelegatedKey]
67-
unwrapping_key=None # type: Optional[DelegatedKey]
73+
unwrapping_key=None, # type: Optional[DelegatedKey]
74+
material_description=None # type: Optional[Dict[Text, Text]]
6875
):
6976
# type: (...) -> None
7077
"""Workaround pending resolution of attrs/mypy interaction.
7178
https://github.com/python/mypy/issues/2088
7279
https://github.com/python-attrs/attrs/issues/215
7380
"""
81+
if material_description is None:
82+
material_description = {}
83+
7484
self._signing_key = signing_key
7585
self._wrapping_key = wrapping_key
7686
self._unwrapping_key = unwrapping_key
87+
self._material_description = material_description
7788
attr.validate(self)
7889

7990
def _build_materials(self, encryption_context):
@@ -85,11 +96,13 @@ def _build_materials(self, encryption_context):
8596
:returns: Wrapped cryptographic materials
8697
:rtype: dynamodb_encryption_sdk.materials.wrapped.WrappedCryptographicMaterials
8798
"""
99+
material_description = self._material_description.copy()
100+
material_description.update(encryption_context.material_description)
88101
return WrappedCryptographicMaterials(
89102
wrapping_key=self._wrapping_key,
90103
unwrapping_key=self._unwrapping_key,
91104
signing_key=self._signing_key,
92-
material_description=encryption_context.material_description.copy()
105+
material_description=material_description
93106
)
94107

95108
def encryption_materials(self, encryption_context):

0 commit comments

Comments
 (0)
Please sign in to comment.