Skip to content

Commit 6a064b1

Browse files
Creating a specific provider instead a client to avoid any http call from AWS SDK Encryption lib
1 parent bf0e4ed commit 6a064b1

File tree

16 files changed

+173
-63
lines changed

16 files changed

+173
-63
lines changed

Makefile

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ target:
77
dev:
88
pip install --upgrade pip pre-commit poetry
99
@$(MAKE) dev-version-plugin
10-
poetry install --extras "all"
10+
poetry install --extras "all datamasking-aws-sdk"
1111
pre-commit install
1212

1313
dev-gitpod:
1414
pip install --upgrade pip poetry
1515
@$(MAKE) dev-version-plugin
16-
poetry install --extras "all"
16+
poetry install --extras "all datamasking-aws-sdk"
1717
pre-commit install
1818

1919
format:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from aws_lambda_powertools.utilities.data_masking.base import DataMasking
2+
3+
__all__ = [
4+
"DataMasking",
5+
]

aws_lambda_powertools/utilities/data_masking/base.py

+32
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,38 @@
55

66

77
class DataMasking:
8+
"""
9+
A utility class for masking sensitive data within various data types.
10+
11+
This class provides methods for masking sensitive information, such as personal
12+
identifiers or confidential data, within different data types such as strings,
13+
dictionaries, lists, and more. It helps protect sensitive information while
14+
preserving the structure of the original data.
15+
16+
Usage:
17+
Instantiate an object of this class and use its methods to mask sensitive data
18+
based on the data type. Supported data types include strings, dictionaries,
19+
and more.
20+
21+
Example:
22+
```
23+
from aws_lambda_powertools.utilities.data_masking.base import DataMasking
24+
25+
def lambda_handler(event, context):
26+
masker = DataMasking()
27+
28+
data = {
29+
"project": "powertools",
30+
"sensitive": "xxxxxxxxxx"
31+
}
32+
33+
masked = masker.mask(data,fields=["sensitive"])
34+
35+
return masked
36+
37+
```
38+
"""
39+
840
def __init__(self, provider: Optional[BaseProvider] = None):
941
self.provider = provider or BaseProvider()
1042

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from aws_lambda_powertools.utilities.data_masking.provider.base import BaseProvider
2+
3+
__all__ = [
4+
"BaseProvider",
5+
]

aws_lambda_powertools/utilities/data_masking/provider.py renamed to aws_lambda_powertools/utilities/data_masking/provider/base.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import json
2-
from typing import Any, Union
2+
from typing import Any
33

44
from aws_lambda_powertools.utilities.data_masking.constants import DATA_MASKING_STRING
55

@@ -20,7 +20,7 @@ def default_json_serializer(self, data):
2020
def default_json_deserializer(self, data):
2121
return json.loads(data.decode("utf-8"))
2222

23-
def encrypt(self, data) -> Union[bytes, str]:
23+
def encrypt(self, data) -> str:
2424
raise NotImplementedError("Subclasses must implement encrypt()")
2525

2626
def decrypt(self, data) -> Any:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from aws_lambda_powertools.utilities.data_masking.provider.kms.aws_encryption_sdk import AwsEncryptionSdkProvider
2+
3+
__all__ = [
4+
"AwsEncryptionSdkProvider",
5+
]

aws_lambda_powertools/utilities/data_masking/providers/aws_encryption_sdk.py renamed to aws_lambda_powertools/utilities/data_masking/provider/kms/aws_encryption_sdk.py

+81-21
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
from __future__ import annotations
2+
13
import base64
2-
from typing import Any, Callable, Dict, List, Optional, Union
4+
from typing import Any, Callable, Dict, List
35

46
import botocore
57
from aws_encryption_sdk import (
@@ -26,43 +28,97 @@ def __init__(self, key):
2628

2729
class AwsEncryptionSdkProvider(BaseProvider):
2830
"""
29-
The AwsEncryptionSdkProvider is to be used as a Provider for the Datamasking class.
30-
31-
Example
32-
-------
33-
>>> data_masker = DataMasking(provider=AwsEncryptionSdkProvider(keys=[keyARN1, keyARN2,...,]))
34-
>>> encrypted_data = data_masker.encrypt("a string")
35-
"encrptedBase64String"
36-
>>> decrypted_data = data_masker.decrypt(encrypted_data)
37-
"a string"
38-
"""
31+
The AwsEncryptionSdkProvider is used as a provider for the DataMasking class.
32+
33+
This provider allows you to perform data masking using the AWS Encryption SDK
34+
for encryption and decryption. It integrates with the DataMasking class to
35+
securely encrypt and decrypt sensitive data.
36+
37+
Usage Example:
38+
```
39+
from aws_lambda_powertools.utilities.data_masking import DataMasking
40+
from aws_lambda_powertools.utilities.data_masking.providers.kms.aws_encryption_sdk import (
41+
AwsEncryptionSdkProvider,
42+
)
43+
44+
45+
def lambda_handler(event, context):
46+
provider = AwsEncryptionSdkProvider(["arn:aws:kms:us-east-1:0123456789012:key/key-id"])
47+
masker = DataMasking(provider=provider)
48+
49+
data = {
50+
"project": "powertools",
51+
"sensitive": "xxxxxxxxxx"
52+
}
53+
54+
masked = masker.encrypt(data,fields=["sensitive"])
3955
40-
session = botocore.session.Session()
41-
register_feature_to_botocore_session(session, "data-masking")
56+
return masked
57+
58+
```
59+
"""
4260

4361
def __init__(
4462
self,
4563
keys: List[str],
46-
client: Optional[EncryptionSDKClient] = None,
64+
key_provider=None,
4765
local_cache_capacity: int = CACHE_CAPACITY,
4866
max_cache_age_seconds: float = MAX_CACHE_AGE_SECONDS,
4967
max_messages_encrypted: int = MAX_MESSAGES_ENCRYPTED,
50-
json_serializer: Optional[Callable[[Dict], str]] = None,
51-
json_deserializer: Optional[Callable[[Union[Dict, str, bool, int, float]], str]] = None,
68+
json_serializer: Callable | None = None,
69+
json_deserializer: Callable | None = None,
5270
):
5371
super().__init__(json_serializer=json_serializer, json_deserializer=json_deserializer)
54-
self.client = client or EncryptionSDKClient()
72+
73+
self._key_provider = key_provider or KMSKeyProvider(
74+
keys=keys,
75+
local_cache_capacity=local_cache_capacity,
76+
max_cache_age_seconds=max_cache_age_seconds,
77+
max_messages_encrypted=max_messages_encrypted,
78+
json_serializer=self.json_serializer,
79+
json_deserializer=self.json_deserializer,
80+
)
81+
82+
def encrypt(self, data: bytes | str | Dict | int, **provider_options) -> str:
83+
return self._key_provider.encrypt(data=data, **provider_options)
84+
85+
def decrypt(self, data: str, **provider_options) -> Any:
86+
return self._key_provider.decrypt(data=data, **provider_options)
87+
88+
89+
class KMSKeyProvider:
90+
91+
"""
92+
The KMSKeyProvider is responsible for assembling an AWS Key Management Service (KMS)
93+
client, a caching mechanism, and a keyring for secure key management and data encryption.
94+
"""
95+
96+
def __init__(
97+
self,
98+
keys: List[str],
99+
json_serializer: Callable,
100+
json_deserializer: Callable,
101+
local_cache_capacity: int = CACHE_CAPACITY,
102+
max_cache_age_seconds: float = MAX_CACHE_AGE_SECONDS,
103+
max_messages_encrypted: int = MAX_MESSAGES_ENCRYPTED,
104+
):
105+
session = botocore.session.Session()
106+
register_feature_to_botocore_session(session, "data-masking")
107+
108+
self.json_serializer = json_serializer
109+
self.json_deserializer = json_deserializer
110+
self.client = EncryptionSDKClient()
55111
self.keys = keys
56112
self.cache = LocalCryptoMaterialsCache(local_cache_capacity)
57-
self.key_provider = StrictAwsKmsMasterKeyProvider(key_ids=self.keys, botocore_session=self.session)
113+
self.key_provider = StrictAwsKmsMasterKeyProvider(key_ids=self.keys, botocore_session=session)
58114
self.cache_cmm = CachingCryptoMaterialsManager(
59115
master_key_provider=self.key_provider,
60116
cache=self.cache,
61117
max_age=max_cache_age_seconds,
62118
max_messages_encrypted=max_messages_encrypted,
63119
)
64120

65-
def encrypt(self, data: Union[bytes, str], **provider_options) -> bytes:
121+
def encrypt(self, data: bytes | str | Dict | float, **provider_options) -> str:
66122
"""
67123
Encrypt data using the AwsEncryptionSdkProvider.
68124
@@ -78,8 +134,12 @@ def encrypt(self, data: Union[bytes, str], **provider_options) -> bytes:
78134
ciphertext : str
79135
The encrypted data, as a base64-encoded string.
80136
"""
81-
data = self.json_serializer(data)
82-
ciphertext, _ = self.client.encrypt(source=data, materials_manager=self.cache_cmm, **provider_options)
137+
data_encoded = self.json_serializer(data)
138+
ciphertext, _ = self.client.encrypt(
139+
source=data_encoded,
140+
materials_manager=self.cache_cmm,
141+
**provider_options,
142+
)
83143
ciphertext = base64.b64encode(ciphertext).decode()
84144
return ciphertext
85145

aws_lambda_powertools/utilities/data_masking/providers/__init__.py

Whitespace-only changes.

pyproject.toml

+2-3
Original file line numberDiff line numberDiff line change
@@ -92,14 +92,13 @@ datadog-lambda = "^4.77.0"
9292

9393
[tool.poetry.extras]
9494
parser = ["pydantic"]
95-
datamasking-aws-sdk= ["aws-encryption-sdk"]
96-
datamasking-all = ["aws-encryption-sdk"]
9795
validation = ["fastjsonschema"]
9896
tracer = ["aws-xray-sdk"]
99-
all = ["pydantic", "aws-xray-sdk", "fastjsonschema", "aws-encryption-sdk"]
97+
all = ["pydantic", "aws-xray-sdk", "fastjsonschema"]
10098
# allow customers to run code locally without emulators (SAM CLI, etc.)
10199
aws-sdk = ["boto3"]
102100
datadog = ["datadog-lambda"]
101+
datamasking-aws-sdk = ["aws-encryption-sdk"]
103102

104103
[tool.poetry.group.dev.dependencies]
105104
cfn-lint = "0.80.3"

tests/e2e/data_masking/handlers/basic_handler.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from aws_lambda_powertools import Logger
2-
from aws_lambda_powertools.utilities.data_masking.base import DataMasking
3-
from aws_lambda_powertools.utilities.data_masking.providers.aws_encryption_sdk import AwsEncryptionSdkProvider
2+
from aws_lambda_powertools.utilities.data_masking import DataMasking
3+
from aws_lambda_powertools.utilities.data_masking.provider.kms.aws_encryption_sdk import AwsEncryptionSdkProvider
44

55
logger = Logger()
66

tests/e2e/data_masking/test_e2e_data_masking.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
import pytest
55
from aws_encryption_sdk.exceptions import DecryptKeyError
66

7-
from aws_lambda_powertools.utilities.data_masking.base import DataMasking
8-
from aws_lambda_powertools.utilities.data_masking.providers.aws_encryption_sdk import (
7+
from aws_lambda_powertools.utilities.data_masking import DataMasking
8+
from aws_lambda_powertools.utilities.data_masking.provider.kms.aws_encryption_sdk import (
99
AwsEncryptionSdkProvider,
1010
ContextMismatchError,
1111
)
-17
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,6 @@
1-
from __future__ import annotations
2-
3-
from typing import Tuple
4-
51
from pytest_socket import disable_socket
62

73

84
def pytest_runtest_setup():
95
"""Disable Unix and TCP sockets for Data masking tests"""
106
disable_socket()
11-
12-
13-
class FakeEncryptionClient:
14-
ENCRYPTION_HEADER = "test"
15-
16-
def encrypt(self, source: bytes | str, **kwargs) -> Tuple[bytes, str]:
17-
if isinstance(source, str):
18-
return source.encode(), self.ENCRYPTION_HEADER
19-
20-
return source, self.ENCRYPTION_HEADER
21-
22-
def decrypt(self, source: bytes, **kwargs) -> Tuple[bytes, str]:
23-
return source, "dummy_decryption_header"

tests/functional/data_masking/test_aws_encryption_sdk.py

+29-8
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,46 @@
11
from __future__ import annotations
22

3+
import base64
34
import json
5+
from typing import Any, Callable, Dict, Union
46

57
import pytest
68

7-
from aws_lambda_powertools.utilities.data_masking.base import DataMasking
9+
from aws_lambda_powertools.utilities.data_masking import DataMasking
810
from aws_lambda_powertools.utilities.data_masking.constants import DATA_MASKING_STRING
9-
from aws_lambda_powertools.utilities.data_masking.providers.aws_encryption_sdk import (
11+
from aws_lambda_powertools.utilities.data_masking.provider import BaseProvider
12+
from aws_lambda_powertools.utilities.data_masking.provider.kms import (
1013
AwsEncryptionSdkProvider,
1114
)
12-
from tests.functional.data_masking.conftest import FakeEncryptionClient
15+
16+
17+
class FakeEncryptionKeyProvider(BaseProvider):
18+
def __init__(
19+
self,
20+
json_serializer: Callable[[Dict], str] | None = None,
21+
json_deserializer: Callable[[Union[Dict, str, bool, int, float]], str] | None = None,
22+
):
23+
super().__init__(json_serializer=json_serializer, json_deserializer=json_deserializer)
24+
25+
def encrypt(self, data: bytes | str, **kwargs) -> str:
26+
data = self.json_serializer(data)
27+
ciphertext = base64.b64encode(data).decode()
28+
return ciphertext
29+
30+
def decrypt(self, data: bytes, **kwargs) -> Any:
31+
ciphertext_decoded = base64.b64decode(data)
32+
ciphertext = self.json_deserializer(ciphertext_decoded)
33+
return ciphertext
1334

1435

1536
@pytest.fixture
1637
def data_masker(monkeypatch) -> DataMasking:
17-
# Setting a default region
18-
monkeypatch.setenv("AWS_DEFAULT_REGION", "us-east-1")
19-
2038
"""DataMasking using AWS Encryption SDK Provider with a fake client"""
21-
fake_client = FakeEncryptionClient()
22-
provider = AwsEncryptionSdkProvider(keys=["dummy"], client=fake_client)
39+
fake_key_provider = FakeEncryptionKeyProvider()
40+
provider = AwsEncryptionSdkProvider(
41+
keys=["dummy"],
42+
key_provider=fake_key_provider,
43+
)
2344
return DataMasking(provider=provider)
2445

2546

tests/performance/data_masking/load_test_data_masking/pt-load-test-stack/function_1024/app.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
from aws_lambda_powertools import Logger, Tracer
44
from aws_lambda_powertools.event_handler import APIGatewayRestResolver
55
from aws_lambda_powertools.logging import correlation_paths
6-
from aws_lambda_powertools.utilities.data_masking.base import DataMasking
7-
from aws_lambda_powertools.utilities.data_masking.providers.aws_encryption_sdk import AwsEncryptionSdkProvider
6+
from aws_lambda_powertools.utilities.data_masking import DataMasking
7+
from aws_lambda_powertools.utilities.data_masking.provider.kms.aws_encryption_sdk import AwsEncryptionSdkProvider
88
from aws_lambda_powertools.utilities.typing import LambdaContext
99

1010
KMS_KEY_ARN = os.environ["KMS_KEY_ARN"]

tests/performance/data_masking/load_test_data_masking/pt-load-test-stack/function_128/app.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
from aws_lambda_powertools import Logger, Tracer
44
from aws_lambda_powertools.event_handler import APIGatewayRestResolver
55
from aws_lambda_powertools.logging import correlation_paths
6-
from aws_lambda_powertools.utilities.data_masking.base import DataMasking
7-
from aws_lambda_powertools.utilities.data_masking.providers.aws_encryption_sdk import AwsEncryptionSdkProvider
6+
from aws_lambda_powertools.utilities.data_masking import DataMasking
7+
from aws_lambda_powertools.utilities.data_masking.provider.kms.aws_encryption_sdk import AwsEncryptionSdkProvider
88
from aws_lambda_powertools.utilities.typing import LambdaContext
99

1010
KMS_KEY_ARN = os.environ["KMS_KEY_ARN"]

tests/performance/data_masking/load_test_data_masking/pt-load-test-stack/function_1769/app.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
from aws_lambda_powertools import Logger, Tracer
44
from aws_lambda_powertools.event_handler import APIGatewayRestResolver
55
from aws_lambda_powertools.logging import correlation_paths
6-
from aws_lambda_powertools.utilities.data_masking.base import DataMasking
7-
from aws_lambda_powertools.utilities.data_masking.providers.aws_encryption_sdk import AwsEncryptionSdkProvider
6+
from aws_lambda_powertools.utilities.data_masking import DataMasking
7+
from aws_lambda_powertools.utilities.data_masking.provider.kms.aws_encryption_sdk import AwsEncryptionSdkProvider
88
from aws_lambda_powertools.utilities.typing import LambdaContext
99

1010
KMS_KEY_ARN = os.environ["KMS_KEY_ARN"]

0 commit comments

Comments
 (0)