Skip to content

Commit 5a29382

Browse files
committed
PYTHON-1947 Add codec_options to ClientEncryption
1 parent 45da03a commit 5a29382

File tree

2 files changed

+63
-22
lines changed

2 files changed

+63
-22
lines changed

pymongo/encryption.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,8 @@ class Algorithm(object):
309309
class ClientEncryption(object):
310310
"""Explicit client side encryption."""
311311

312-
def __init__(self, kms_providers, key_vault_namespace, key_vault_client):
312+
def __init__(self, kms_providers, key_vault_namespace, key_vault_client,
313+
codec_options):
313314
"""Explicit client side encryption.
314315
315316
The ClientEncryption class encapsulates explicit operations on a key
@@ -340,6 +341,9 @@ def __init__(self, kms_providers, key_vault_namespace, key_vault_client):
340341
provider.
341342
- `key_vault_client`: A MongoClient connected to a MongoDB cluster
342343
containing the `key_vault_namespace` collection.
344+
- `codec_options`: An instance of
345+
:class:`~bson.codec_options.CodecOptions` to use when encoding a
346+
value for encryption and decoding the decrypted BSON value.
343347
344348
.. versionadded:: 3.9
345349
"""
@@ -349,9 +353,14 @@ def __init__(self, kms_providers, key_vault_namespace, key_vault_client):
349353
"install a compatible version with: "
350354
"python -m pip install pymongo['encryption']")
351355

356+
if not isinstance(codec_options, CodecOptions):
357+
raise TypeError("codec_options must be an instance of "
358+
"bson.codec_options.CodecOptions")
359+
352360
self._kms_providers = kms_providers
353361
self._key_vault_namespace = key_vault_namespace
354362
self._key_vault_client = key_vault_client
363+
self._codec_options = codec_options
355364

356365
db, coll = key_vault_namespace.split('.', 1)
357366
key_vault_coll = key_vault_client[db][coll]
@@ -413,8 +422,7 @@ def encrypt(self, value, algorithm, key_id=None, key_alt_name=None):
413422
:Returns:
414423
The encrypted value, a :class:`~bson.binary.Binary` with subtype 6.
415424
"""
416-
# TODO: Add a required codec_options argument for encoding?
417-
doc = encode({'v': value}, codec_options=_DATA_KEY_OPTS)
425+
doc = encode({'v': value}, codec_options=self._codec_options)
418426
if isinstance(key_id, uuid.UUID):
419427
raw_key_id = key_id.bytes
420428
else:
@@ -428,8 +436,7 @@ def _decrypt(self, value):
428436
"""Internal decrypt helper."""
429437
doc = encode({'v': value})
430438
decrypted_doc = self._encryption.decrypt(doc)
431-
# TODO: Add a required codec_options argument for decoding?
432-
return decode(decrypted_doc, codec_options=_DATA_KEY_OPTS)['v']
439+
return decode(decrypted_doc, codec_options=self._codec_options)['v']
433440

434441
def decrypt(self, value):
435442
"""Decrypt an encrypted value.

test/test_encryption.py

Lines changed: 51 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@
2525
sys.path[0:0] = [""]
2626

2727
from bson import BSON, json_util
28-
from bson.binary import STANDARD, Binary, UUID_SUBTYPE
28+
from bson.binary import (Binary,
29+
JAVA_LEGACY,
30+
STANDARD,
31+
UUID_SUBTYPE)
2932
from bson.codec_options import CodecOptions
3033
from bson.errors import BSONError
3134
from bson.json_util import JSONOptions
@@ -35,6 +38,8 @@
3538
from pymongo.errors import (ConfigurationError,
3639
EncryptionError,
3740
OperationFailure)
41+
from pymongo.encryption import (Algorithm,
42+
ClientEncryption)
3843
from pymongo.encryption_options import AutoEncryptionOpts, _HAVE_PYMONGOCRYPT
3944
from pymongo.mongo_client import MongoClient
4045
from pymongo.write_concern import WriteConcern
@@ -47,16 +52,6 @@
4752
from test.utils_spec_runner import SpecRunner
4853

4954

50-
if _HAVE_PYMONGOCRYPT:
51-
# Load the mongocrypt library.
52-
from pymongocrypt.binding import init
53-
init(os.environ.get('MONGOCRYPT_LIB', 'mongocrypt'))
54-
55-
# This has to be imported after calling init().
56-
from pymongo.encryption import (Algorithm,
57-
ClientEncryption)
58-
59-
6055
def get_client_opts(client):
6156
return client._MongoClient__options
6257

@@ -261,7 +256,7 @@ class TestExplicitSimple(EncryptionIntegrationTest):
261256

262257
def test_encrypt_decrypt(self):
263258
client_encryption = ClientEncryption(
264-
KMS_PROVIDERS, 'admin.datakeys', client_context.client)
259+
KMS_PROVIDERS, 'admin.datakeys', client_context.client, OPTS)
265260
self.addCleanup(client_encryption.close)
266261
# Use standard UUID representation.
267262
key_vault = client_context.client.admin.get_collection(
@@ -296,7 +291,7 @@ def test_encrypt_decrypt(self):
296291

297292
def test_validation(self):
298293
client_encryption = ClientEncryption(
299-
KMS_PROVIDERS, 'admin.datakeys', client_context.client)
294+
KMS_PROVIDERS, 'admin.datakeys', client_context.client, OPTS)
300295
self.addCleanup(client_encryption.close)
301296

302297
msg = 'value to decrypt must be a bson.binary.Binary with subtype 6'
@@ -307,7 +302,7 @@ def test_validation(self):
307302

308303
def test_bson_errors(self):
309304
client_encryption = ClientEncryption(
310-
KMS_PROVIDERS, 'admin.datakeys', client_context.client)
305+
KMS_PROVIDERS, 'admin.datakeys', client_context.client, OPTS)
311306
self.addCleanup(client_encryption.close)
312307

313308
# Attempt to encrypt an unencodable object.
@@ -317,6 +312,43 @@ def test_bson_errors(self):
317312
unencodable_value, Algorithm.Deterministic,
318313
key_id=Binary(uuid.uuid4().bytes, UUID_SUBTYPE))
319314

315+
def test_codec_options(self):
316+
with self.assertRaisesRegex(TypeError, 'codec_options must be'):
317+
ClientEncryption(
318+
KMS_PROVIDERS, 'admin.datakeys', client_context.client, None)
319+
320+
opts = CodecOptions(uuid_representation=JAVA_LEGACY)
321+
client_encryption_legacy = ClientEncryption(
322+
KMS_PROVIDERS, 'admin.datakeys', client_context.client, opts)
323+
self.addCleanup(client_encryption_legacy.close)
324+
325+
# Create the encrypted field's data key.
326+
key_id = client_encryption_legacy.create_data_key('local')
327+
328+
# Encrypt a UUID with JAVA_LEGACY codec options.
329+
value = uuid.uuid4()
330+
encrypted_legacy = client_encryption_legacy.encrypt(
331+
value, Algorithm.Deterministic, key_id=key_id)
332+
decrypted_value_legacy = client_encryption_legacy.decrypt(
333+
encrypted_legacy)
334+
self.assertEqual(decrypted_value_legacy, value)
335+
336+
# Encrypt the same UUID with STANDARD codec options.
337+
client_encryption = ClientEncryption(
338+
KMS_PROVIDERS, 'admin.datakeys', client_context.client, OPTS)
339+
self.addCleanup(client_encryption.close)
340+
encrypted_standard = client_encryption.encrypt(
341+
value, Algorithm.Deterministic, key_id=key_id)
342+
decrypted_standard = client_encryption.decrypt(encrypted_standard)
343+
self.assertEqual(decrypted_standard, value)
344+
345+
# Test that codec_options is applied during encryption.
346+
self.assertNotEqual(encrypted_standard, encrypted_legacy)
347+
# Test that codec_options is applied during decryption.
348+
self.assertEqual(
349+
client_encryption_legacy.decrypt(encrypted_standard), value)
350+
self.assertNotEqual(
351+
client_encryption.decrypt(encrypted_legacy), value)
320352

321353
# Spec tests
322354

@@ -485,7 +517,8 @@ def test_data_key(self):
485517
self.addCleanup(client_encrypted.close)
486518

487519
client_encryption = ClientEncryption(
488-
self.kms_providers(), 'admin.datakeys', client_context.client)
520+
self.kms_providers(), 'admin.datakeys', client_context.client,
521+
OPTS)
489522
self.addCleanup(client_encryption.close)
490523

491524
# Local create data key.
@@ -577,7 +610,7 @@ def _test_external_key_vault(self, with_external_key_vault):
577610
self.addCleanup(client_encrypted.close)
578611

579612
client_encryption = ClientEncryption(
580-
self.kms_providers(), 'admin.datakeys', key_vault_client)
613+
self.kms_providers(), 'admin.datakeys', key_vault_client, OPTS)
581614
self.addCleanup(client_encryption.close)
582615

583616
if with_external_key_vault:
@@ -684,7 +717,8 @@ def _test_corpus(self, opts):
684717
self.addCleanup(client_encrypted.close)
685718

686719
client_encryption = ClientEncryption(
687-
self.kms_providers(), 'admin.datakeys', client_context.client)
720+
self.kms_providers(), 'admin.datakeys', client_context.client,
721+
OPTS)
688722
self.addCleanup(client_encryption.close)
689723

690724
corpus = self.fix_up_curpus(json_data('corpus', 'corpus.json'))

0 commit comments

Comments
 (0)