Skip to content

Commit f9bcabb

Browse files
committed
PYTHON-2371 Add Azure and GCP support for CSFLE
1 parent c8be79f commit f9bcabb

File tree

7 files changed

+250
-1
lines changed

7 files changed

+250
-1
lines changed

.evergreen/run-tests.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ if [ -n "$TEST_ENCRYPTION" ]; then
152152
exit 1
153153
fi
154154

155-
git clone --branch master git@github.com:mongodb/libmongocrypt.git libmongocrypt_git
155+
git clone --branch master git@github.com:com:mongodb/libmongocrypt.git libmongocrypt_git
156156
$PYTHON -m pip install --upgrade ./libmongocrypt_git/bindings/python
157157
# TODO: use a virtualenv
158158
trap "$PYTHON -m pip uninstall -y pymongocrypt" EXIT HUP

pymongo/encryption.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,17 @@ def __init__(self, kms_providers, key_vault_namespace, key_vault_client,
358358
- `aws`: Map with "accessKeyId" and "secretAccessKey" as strings.
359359
These are the AWS access key ID and AWS secret access key used
360360
to generate KMS messages.
361+
- `azure`: Map with "tenantId", "clientId", and "clientSecret" as
362+
strings. Additionally, "identityPlatformEndpoint" may also be
363+
specified as a string (defaults to 'login.microsoftonline.com').
364+
These are the Azure Active Directory credentials used to
365+
generate Azure Key Vault messages.
366+
- `gcp`: Map with "email" as a string and "privateKey" as a
367+
base64 encoded string or `bytes`. Python 2 users must specify
368+
"privateKey" as a base64 encoded string. Additionally,
369+
"endpoint" may also be specified as a string (defaults to
370+
'oauth2.googleapis.com'). These are the credentials used to
371+
generate Google Cloud KMS messages.
361372
- `local`: Map with "key" as a 96-byte array or string. "key"
362373
is the master key used to encrypt/decrypt data keys. This key
363374
should be generated and stored as securely as possible.

pymongo/encryption_options.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,17 @@ def __init__(self, kms_providers, key_vault_namespace,
5959
- `aws`: Map with "accessKeyId" and "secretAccessKey" as strings.
6060
These are the AWS access key ID and AWS secret access key used
6161
to generate KMS messages.
62+
- `azure`: Map with "tenantId", "clientId", and "clientSecret" as
63+
strings. Additionally, "identityPlatformEndpoint" may also be
64+
specified as a string (defaults to 'login.microsoftonline.com').
65+
These are the Azure Active Directory credentials used to
66+
generate Azure Key Vault messages.
67+
- `gcp`: Map with "email" as a string and "privateKey" as a
68+
base64 encoded string or `bytes`. Python 2 users must specify
69+
"privateKey" as a base64 encoded string. Additionally,
70+
"endpoint" may also be specified as a string (defaults to
71+
'oauth2.googleapis.com'). These are the credentials used to
72+
generate Google Cloud KMS messages.
6273
- `local`: Map with "key" as a 96-byte array or string. "key"
6374
is the master key used to encrypt/decrypt data keys. This key
6475
should be generated and stored as securely as possible.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"_id": {
3+
"$binary": {
4+
"base64": "As3URE1jRcyHOPjaLWHOXA==",
5+
"subType": "04"
6+
}
7+
},
8+
"keyMaterial": {
9+
"$binary": {
10+
"base64": "df6fFLZqBsZSnQz2SnTYWNBtznIHktVSDMaidAdL7yVVgxBJQ0DyPZUR2HDQB4hdYym3w4C+VGqzcyTZNJOXn6nJzpGrGlIQMcjv93HE4sP2d245ShQCi1nTkLmMaXN63E2fzltOY3jW7ojf5Z4+r8kxmzyfymmSRgo0w8AF7lUWvFhnBYoE4tE322L31vtAK3Zj8pTPvw8/TcUdMSI9Y669IIzxbMy5yMPmdzpnb8nceUv6/CJoeiLhbt5GgaHqIAv7tHFOY8ZX8ztowMLa3GeAjd9clvzraDTqrfMFYco/kDKAW5iPQQ+Xuy1fP8tyFp0ZwaL/7Ed2sc819j8FTQ==",
11+
"subType": "00"
12+
}
13+
},
14+
"creationDate": {
15+
"$date": {
16+
"$numberLong": "1601573901680"
17+
}
18+
},
19+
"updateDate": {
20+
"$date": {
21+
"$numberLong": "1601573901680"
22+
}
23+
},
24+
"status": {
25+
"$numberInt": "0"
26+
},
27+
"masterKey": {
28+
"provider": "azure",
29+
"keyVaultEndpoint": "key-vault-kevinalbs.vault.azure.net",
30+
"keyName": "test-key"
31+
}
32+
}
33+
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"db.coll": {
3+
"bsonType": "object",
4+
"properties": {
5+
"secret_azure": {
6+
"encrypt": {
7+
"keyId": [{
8+
"$binary": {
9+
"base64": "As3URE1jRcyHOPjaLWHOXA==",
10+
"subType": "04"
11+
}
12+
}],
13+
"algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic",
14+
"bsonType": "string"
15+
}
16+
},
17+
"secret_gcp": {
18+
"encrypt": {
19+
"keyId": [{
20+
"$binary": {
21+
"base64": "osU8SLxJRHONbl8Oh5o+eg==",
22+
"subType": "04"
23+
}
24+
}],
25+
"algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic",
26+
"bsonType": "string"
27+
}
28+
}
29+
}
30+
}
31+
}
32+
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"_id": {
3+
"$binary": {
4+
"base64": "osU8SLxJRHONbl8Oh5o+eg==",
5+
"subType": "04"
6+
}
7+
},
8+
"keyMaterial": {
9+
"$binary": {
10+
"base64": "CiQAg4LDql74hjYPZ957Z7YpCrD6yTVVXKegflJDstQ/xngTyx0SiQEAkWNo/fjPj6jMNSvEop07/29Fu72QHFDRYM3e/KFHfnMQjKzfxb1yX1dC6MbO5FZG/UNBkXlJgPqbHNVuizea3QC24kV5iOiEb4nTM7+RW+8TfVb6QerWWe6MjC+kNpj4LMVcc1lFfVDeGgpJLyMLNGitrjR16qH8qQTNbGNy0toTL69JUmgS8Q==",
11+
"subType": "00"
12+
}
13+
},
14+
"creationDate": {
15+
"$date": {
16+
"$numberLong": "1601574333107"
17+
}
18+
},
19+
"updateDate": {
20+
"$date": {
21+
"$numberLong": "1601574333107"
22+
}
23+
},
24+
"status": {
25+
"$numberInt": "0"
26+
},
27+
"masterKey": {
28+
"provider": "gcp",
29+
"projectId": "csfle-poc",
30+
"location": "global",
31+
"keyRing": "test",
32+
"keyName": "quickstart"
33+
}
34+
}
35+

test/test_encryption.py

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import traceback
2121
import socket
2222
import sys
23+
import textwrap
2324
import uuid
2425

2526
sys.path[0:0] = [""]
@@ -52,6 +53,7 @@
5253
from test.utils import (TestCreator,
5354
camel_to_snake_args,
5455
OvertCommandListener,
56+
WhiteListEventListener,
5557
rs_or_single_client,
5658
wait_until)
5759
from test.utils_spec_runner import SpecRunner
@@ -1105,5 +1107,130 @@ def test_05_endpoint_invalid_host(self):
11051107
'aws', master_key=master_key)
11061108

11071109

1110+
class AzureGCPEncryptionTestMixin(object):
1111+
DEK = None
1112+
KMS_PROVIDER_MAP = None
1113+
KEYVAULT_DB = 'keyvault'
1114+
KEYVAULT_COLL = 'datakeys'
1115+
1116+
def setUp(self):
1117+
keyvault = self.client.get_database(
1118+
self.KEYVAULT_DB).get_collection(
1119+
self.KEYVAULT_COLL)
1120+
create_key_vault(keyvault, self.DEK)
1121+
1122+
def _test_explicit(self, expectation):
1123+
client_encryption = ClientEncryption(
1124+
self.KMS_PROVIDER_MAP,
1125+
'.'.join([self.KEYVAULT_DB, self.KEYVAULT_COLL]),
1126+
client_context.client,
1127+
OPTS)
1128+
1129+
ciphertext = client_encryption.encrypt(
1130+
'test',
1131+
algorithm=Algorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic,
1132+
key_id=Binary.from_uuid(self.DEK['_id'], STANDARD))
1133+
1134+
self.assertEqual(bytes(ciphertext), base64.b64decode(expectation))
1135+
self.assertEqual(client_encryption.decrypt(ciphertext), 'test')
1136+
1137+
def _test_automatic(self, expectation_extjson, payload):
1138+
encrypted_db = "db"
1139+
encrypted_coll = "coll"
1140+
keyvault_namespace = '.'.join([self.KEYVAULT_DB, self.KEYVAULT_COLL])
1141+
1142+
encryption_opts = AutoEncryptionOpts(
1143+
self.KMS_PROVIDER_MAP,
1144+
keyvault_namespace,
1145+
schema_map=self.SCHEMA_MAP)
1146+
1147+
insert_listener = WhiteListEventListener('insert')
1148+
client = rs_or_single_client(
1149+
auto_encryption_opts=encryption_opts,
1150+
event_listeners=[insert_listener])
1151+
1152+
coll = client.get_database(encrypted_db).get_collection(
1153+
encrypted_coll, codec_options=OPTS,
1154+
write_concern=WriteConcern("majority"))
1155+
coll.drop()
1156+
1157+
expected_document = json_util.loads(
1158+
expectation_extjson, json_options=JSON_OPTS)
1159+
1160+
coll.insert_one(payload)
1161+
event = insert_listener.results['started'][0]
1162+
inserted_doc = event.command['documents'][0]
1163+
1164+
for key, value in expected_document.items():
1165+
self.assertEqual(value, inserted_doc[key])
1166+
1167+
output_doc = coll.find_one({})
1168+
for key, value in payload.items():
1169+
self.assertEqual(output_doc[key], value)
1170+
1171+
1172+
AZURE_CREDS = {
1173+
'tenantId': os.environ.get('FLE_AZURE_TENANTID', ''),
1174+
'clientId': os.environ.get('FLE_AZURE_CLIENTID', ''),
1175+
'clientSecret': os.environ.get('FLE_AZURE_CLIENTSECRET', '')}
1176+
1177+
1178+
class TestAzureEncryption(AzureGCPEncryptionTestMixin,
1179+
EncryptionIntegrationTest):
1180+
@classmethod
1181+
@unittest.skipUnless(any(AZURE_CREDS.values()),
1182+
'Azure environment credentials are not set')
1183+
def setUpClass(cls):
1184+
cls.KMS_PROVIDER_MAP = {'azure': AZURE_CREDS}
1185+
cls.DEK = json_data(BASE, 'custom', 'azure-dek.json')
1186+
cls.SCHEMA_MAP = json_data(BASE, 'custom', 'azure-gcp-schema.json')
1187+
super(TestAzureEncryption, cls).setUpClass()
1188+
1189+
def test_explicit(self):
1190+
return self._test_explicit(
1191+
'AQLN1ERNY0XMhzj42i1hzlwC8/OSU9bHfaQRmmRF5l7d5ZpqJX13qF5zSyExo8N9c1b6uS/LoKrHNzcEMKNrkpi3jf2HiShTFRF0xi8AOD9yfw==')
1192+
1193+
def test_automatic(self):
1194+
expected_document_extjson = textwrap.dedent("""
1195+
{"secret_azure": {
1196+
"$binary": {
1197+
"base64": "AQLN1ERNY0XMhzj42i1hzlwC8/OSU9bHfaQRmmRF5l7d5ZpqJX13qF5zSyExo8N9c1b6uS/LoKrHNzcEMKNrkpi3jf2HiShTFRF0xi8AOD9yfw==",
1198+
"subType": "06"}
1199+
}}""")
1200+
return self._test_automatic(
1201+
expected_document_extjson, {"secret_azure": "test"})
1202+
1203+
1204+
GCP_CREDS = {
1205+
'email': os.environ.get('FLE_GCP_EMAIL', ''),
1206+
'privateKey': os.environ.get('FLE_GCP_PRIVATEKEY', '')}
1207+
1208+
1209+
class TestGCPEncryption(AzureGCPEncryptionTestMixin,
1210+
EncryptionIntegrationTest):
1211+
@classmethod
1212+
@unittest.skipUnless(any(GCP_CREDS.values()),
1213+
'GCP environment credentials are not set')
1214+
def setUpClass(cls):
1215+
cls.KMS_PROVIDER_MAP = {'gcp': GCP_CREDS}
1216+
cls.DEK = json_data(BASE, 'custom', 'gcp-dek.json')
1217+
cls.SCHEMA_MAP = json_data(BASE, 'custom', 'azure-gcp-schema.json')
1218+
super(TestGCPEncryption, cls).setUpClass()
1219+
1220+
def test_explicit(self):
1221+
return self._test_explicit(
1222+
'AaLFPEi8SURzjW5fDoeaPnoCGcOFAmFOPpn5584VPJJ8iXIgml3YDxMRZD9IWv5otyoft8fBzL1LsDEp0lTeB32cV1gOj0IYeAKHhGIleuHZtA==')
1223+
1224+
def test_automatic(self):
1225+
expected_document_extjson = textwrap.dedent("""
1226+
{"secret_gcp": {
1227+
"$binary": {
1228+
"base64": "AaLFPEi8SURzjW5fDoeaPnoCGcOFAmFOPpn5584VPJJ8iXIgml3YDxMRZD9IWv5otyoft8fBzL1LsDEp0lTeB32cV1gOj0IYeAKHhGIleuHZtA==",
1229+
"subType": "06"}
1230+
}}""")
1231+
return self._test_automatic(
1232+
expected_document_extjson, {"secret_gcp": "test"})
1233+
1234+
11081235
if __name__ == "__main__":
11091236
unittest.main()

0 commit comments

Comments
 (0)