-
Notifications
You must be signed in to change notification settings - Fork 85
/
Copy pathcaching.py
245 lines (203 loc) · 10.5 KB
/
caching.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
"""Caching crypto material manager."""
import logging
import uuid
import attr
import six
from ..caches import (
CryptoMaterialsCacheEntryHints,
build_decryption_materials_cache_key,
build_encryption_materials_cache_key,
)
from ..caches.base import CryptoMaterialsCache
from ..exceptions import CacheKeyError
from ..internal.defaults import MAX_BYTES_PER_KEY, MAX_MESSAGES_PER_KEY
from ..internal.str_ops import to_bytes
from ..key_providers.base import MasterKeyProvider
from . import EncryptionMaterialsRequest
from .base import CryptoMaterialsManager
from .default import DefaultCryptoMaterialsManager
_LOGGER = logging.getLogger(__name__)
@attr.s(hash=False)
class CachingCryptoMaterialsManager(CryptoMaterialsManager):
"""Crypto material manager which caches results from an underlying material manager.
.. versionadded:: 1.3.0
>>> import aws_encryption_sdk
>>> kms_key_provider = aws_encryption_sdk.KMSMasterKeyProvider(key_ids=[
... 'arn:aws:kms:us-east-1:2222222222222:key/22222222-2222-2222-2222-222222222222',
... 'arn:aws:kms:us-east-1:3333333333333:key/33333333-3333-3333-3333-333333333333'
... ])
>>> local_cache = aws_encryption_sdk.LocalCryptoMaterialsCache(capacity=100)
>>> caching_materials_manager = aws_encryption_sdk.CachingCryptoMaterialsManager(
... master_key_provider=kms_key_provider,
... cache=local_cache,
... max_age=600.0,
... max_messages_encrypted=10
... )
.. note::
The partition name is used to enable a single cache instance to be used by multiple
material manager instances by partitioning the entries in that cache based on this
value. If no partition name is provided, a random UUID will be used.
.. note::
Either `backing_materials_manager` or `master_key_provider` must be provided.
`backing_materials_manager` will always be used if present.
:param cache: Crypto cache to use with material manager
:type cache: aws_encryption_sdk.caches.base.CryptoMaterialsCache
:param backing_materials_manager: Crypto material manager to back this caching material manager
(either `backing_materials_manager` or `master_key_provider` required)
:type backing_materials_manager: aws_encryption_sdk.materials_managers.base.CryptoMaterialsManager
:param master_key_provider: Master key provider to use (either `backing_materials_manager` or
`master_key_provider` required)
:type master_key_provider: aws_encryption_sdk.key_providers.base.MasterKeyProvider
:param float max_age: Maximum time in seconds that a cache entry may be kept in the cache
:param int max_messages_encrypted: Maximum number of messages that may be encrypted under
a cache entry (optional)
:param int max_bytes_encrypted: Maximum number of bytes that a cache entry may be used
to process (optional)
:param bytes partition_name: Partition name to use for this instance (optional)
"""
cache = attr.ib(validator=attr.validators.instance_of(CryptoMaterialsCache))
max_age = attr.ib(validator=attr.validators.instance_of(float))
max_messages_encrypted = attr.ib(
default=MAX_MESSAGES_PER_KEY, validator=attr.validators.instance_of(six.integer_types)
)
max_bytes_encrypted = attr.ib(default=MAX_BYTES_PER_KEY, validator=attr.validators.instance_of(six.integer_types))
partition_name = attr.ib(
default=None, converter=to_bytes, validator=attr.validators.optional(attr.validators.instance_of(bytes))
)
master_key_provider = attr.ib(
default=None, validator=attr.validators.optional(attr.validators.instance_of(MasterKeyProvider))
)
backing_materials_manager = attr.ib(
default=None, validator=attr.validators.optional(attr.validators.instance_of(CryptoMaterialsManager))
)
def __attrs_post_init__(self):
"""Applies post-processing which cannot be handled by attrs."""
if self.max_messages_encrypted < 1:
raise ValueError("max_messages_encrypted cannot be less than 1")
if self.max_bytes_encrypted < 0:
raise ValueError("max_bytes_encrypted cannot be less than 0")
if self.max_messages_encrypted > MAX_MESSAGES_PER_KEY:
raise ValueError("max_messages_encrypted cannot exceed {}".format(MAX_MESSAGES_PER_KEY))
if self.max_bytes_encrypted > MAX_BYTES_PER_KEY:
raise ValueError("max_bytes_encrypted cannot exceed {}".format(MAX_BYTES_PER_KEY))
if self.max_age <= 0.0:
raise ValueError("max_age cannot be less than or equal to 0")
if self.backing_materials_manager is None:
if self.master_key_provider is None:
raise TypeError("Either backing_materials_manager or master_key_provider must be defined")
self.backing_materials_manager = DefaultCryptoMaterialsManager(self.master_key_provider)
if self.partition_name is None:
self.partition_name = to_bytes(str(uuid.uuid4()))
def _cache_entry_has_encrypted_too_many_bytes(self, entry):
"""Determines if a cache entry has exceeded the max allowed bytes encrypted.
:param entry: Entry to evaluate
:type entry: aws_encryption_sdk.caches.CryptoCacheEntry
:rtype: bool
"""
return entry.bytes_encrypted > self.max_bytes_encrypted
def _cache_entry_has_encrypted_too_many_messages(self, entry):
"""Determines if a cache entry has exceeded the max allowed messages encrypted.
:param entry: Entry to evaluate
:type entry: aws_encryption_sdk.caches.CryptoCacheEntry
:rtype: bool
"""
return entry.messages_encrypted > self.max_messages_encrypted
def _cache_entry_is_too_old(self, entry):
"""Determines if a cache entry has exceeded the max allowed age.
:param entry: Entry to evaluate
:type entry: aws_encryption_sdk.caches.CryptoCacheEntry
:rtype: bool
"""
return entry.age > self.max_age
def _cache_entry_has_exceeded_limits(self, entry):
"""Determines if a cache entry has exceeded any security limits.
:param entry: Entry to evaluate
:type entry: aws_encryption_sdk.caches.CryptoCacheEntry
:rtype: bool
"""
return (
self._cache_entry_is_too_old(entry)
or self._cache_entry_has_encrypted_too_many_messages(entry)
or self._cache_entry_has_encrypted_too_many_bytes(entry)
)
def _should_cache_encryption_request(self, request):
"""Determines whether the encryption materials request should be cached.
:param request: Encryption materials request
:type request: aws_encryption_sdk.materials_managers.EncryptionMaterialsRequest
:rtype: bool
"""
if request.plaintext_length is None:
_LOGGER.debug("Encryption materials request not cached because plaintext length is unknown")
return False
if request.algorithm is not None and not request.algorithm.safe_to_cache():
_LOGGER.debug("Encryption materials request not cached because algorithm suite is not safe to cache")
return False
return True
def get_encryption_materials(self, request):
"""Provides encryption materials appropriate for the request.
:param request: Encryption materials request
:type request: aws_encryption_sdk.materials_managers.EncryptionMaterialsRequest
:returns: encryption materials
:rtype: aws_encryption_sdk.materials_managers.EncryptionMaterials
"""
if not self._should_cache_encryption_request(request):
return self.backing_materials_manager.get_encryption_materials(request)
# Inner request strips any information about the plaintext from the actual request.
# This is done because the resulting encryption materials may be used to encrypt
# multiple plaintexts.
inner_request = EncryptionMaterialsRequest(
encryption_context=request.encryption_context,
frame_length=request.frame_length,
algorithm=request.algorithm,
commitment_policy=request.commitment_policy,
)
cache_key = build_encryption_materials_cache_key(partition=self.partition_name, request=inner_request)
# Attempt to retrieve from cache
try:
cache_entry = self.cache.get_encryption_materials(
cache_key=cache_key, plaintext_length=request.plaintext_length
)
except CacheKeyError:
pass
else:
if self._cache_entry_has_exceeded_limits(cache_entry):
self.cache.remove(cache_entry)
else:
return cache_entry.value
# Nothing found in cache: try the material manager
new_result = self.backing_materials_manager.get_encryption_materials(inner_request)
if not new_result.algorithm.safe_to_cache() or request.plaintext_length >= self.max_bytes_encrypted:
return new_result
# Add results into cache
self.cache.put_encryption_materials(
cache_key=cache_key,
encryption_materials=new_result,
plaintext_length=request.plaintext_length,
entry_hints=CryptoMaterialsCacheEntryHints(lifetime=self.max_age),
)
return new_result
def decrypt_materials(self, request):
"""Provides decryption materials appropriate for the request.
:param request: decrypt materials request
:type request: aws_encryption_sdk.materials_managers.DecryptionMaterialsRequest
:returns: decryption materials
:rtype: aws_encryption_sdk.materials_managers.DecryptionMaterials
"""
cache_key = build_decryption_materials_cache_key(partition=self.partition_name, request=request)
# Attempt to retrieve from cache
try:
cache_entry = self.cache.get_decryption_materials(cache_key)
except CacheKeyError:
pass
else:
if self._cache_entry_is_too_old(cache_entry):
self.cache.remove(cache_entry)
else:
return cache_entry.value
# Nothing found in cache: try the material manager
new_result = self.backing_materials_manager.decrypt_materials(request)
# Add results into cache
self.cache.put_decryption_materials(cache_key=cache_key, decryption_materials=new_result)
return new_result