12
12
# See the License for the specific language governing permissions and
13
13
# limitations under the License.
14
14
15
- """Client side encryption implementation ."""
15
+ """Client side encryption."""
16
16
17
17
import subprocess
18
+ import uuid
18
19
import weakref
19
20
20
- from pymongocrypt .auto_encrypter import AutoEncrypter
21
- from pymongocrypt .errors import MongoCryptError
22
- from pymongocrypt .mongocrypt import MongoCryptOptions
23
- from pymongocrypt .state_machine import MongoCryptCallback
24
-
25
- from bson import _bson_to_dict , _dict_to_bson
21
+ try :
22
+ from pymongocrypt .auto_encrypter import AutoEncrypter
23
+ from pymongocrypt .errors import MongoCryptError
24
+ from pymongocrypt .explicit_encrypter import ExplicitEncrypter
25
+ from pymongocrypt .mongocrypt import MongoCryptOptions
26
+ from pymongocrypt .state_machine import MongoCryptCallback
27
+ _HAVE_PYMONGOCRYPT = True
28
+ except ImportError :
29
+ _HAVE_PYMONGOCRYPT = False
30
+ MongoCryptCallback = object
31
+
32
+ from bson import _bson_to_dict , _dict_to_bson , decode , encode
26
33
from bson .binary import STANDARD
27
34
from bson .codec_options import CodecOptions
28
35
from bson .raw_bson import (DEFAULT_RAW_BSON_OPTIONS ,
29
36
RawBSONDocument ,
30
37
_inflate_bson )
31
38
from bson .son import SON
32
39
33
- from pymongo .errors import (EncryptionError ,
40
+ from pymongo .errors import (ConfigurationError ,
41
+ EncryptionError ,
34
42
ServerSelectionTimeoutError )
35
43
from pymongo .mongo_client import MongoClient
36
44
from pymongo .pool import _configured_socket , PoolOptions
@@ -52,7 +60,10 @@ class _EncryptionIO(MongoCryptCallback):
52
60
def __init__ (self , client , key_vault_coll , mongocryptd_client , opts ):
53
61
"""Internal class to perform I/O on behalf of pymongocrypt."""
54
62
# Use a weak ref to break reference cycle.
55
- self .client_ref = weakref .ref (client )
63
+ if client is not None :
64
+ self .client_ref = weakref .ref (client )
65
+ else :
66
+ self .client_ref = None
56
67
self .key_vault_coll = key_vault_coll .with_options (
57
68
codec_options = _KEY_VAULT_OPTS )
58
69
self .mongocryptd_client = mongocryptd_client
@@ -167,15 +178,29 @@ def insert_data_key(self, data_key):
167
178
res = self .key_vault_coll .insert_one (doc )
168
179
return res .inserted_id
169
180
181
+ def bson_encode (self , doc ):
182
+ """Encode a document to BSON.
183
+
184
+ A document can be any mapping type (like :class:`dict`).
185
+
186
+ :Parameters:
187
+ - `doc`: mapping type representing a document
188
+
189
+ :Returns:
190
+ The encoded BSON bytes.
191
+ """
192
+ return encode (doc )
193
+
170
194
def close (self ):
171
195
"""Release resources.
172
196
173
197
Note it is not safe to call this method from __del__ or any GC hooks.
174
198
"""
175
199
self .client_ref = None
176
200
self .key_vault_coll = None
177
- self .mongocryptd_client .close ()
178
- self .mongocryptd_client = None
201
+ if self .mongocryptd_client :
202
+ self .mongocryptd_client .close ()
203
+ self .mongocryptd_client = None
179
204
180
205
181
206
class _Encrypter (object ):
@@ -262,3 +287,147 @@ def create(client, opts):
262
287
io_callbacks = _EncryptionIO (
263
288
client , key_vault_coll , mongocryptd_client , opts )
264
289
return _Encrypter (io_callbacks , opts )
290
+
291
+
292
+ class Algorithm (object ):
293
+ """An enum that defines the supported encryption algorithms."""
294
+ Deterministic = "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic"
295
+ Random = "AEAD_AES_256_CBC_HMAC_SHA_512-Random"
296
+
297
+
298
+ class ClientEncryption (object ):
299
+ """Explicit client side encryption."""
300
+
301
+ def __init__ (self , kms_providers , key_vault_namespace , key_vault_client ):
302
+ """Explicit client side encryption.
303
+
304
+ The ClientEncryption class encapsulates explicit operations on a key
305
+ vault collection that cannot be done directly on a MongoClient. Similar
306
+ to configuring auto encryption on a MongoClient, it is constructed with
307
+ a MongoClient (to a MongoDB cluster containing the key vault
308
+ collection), KMS provider configuration, and keyVaultNamespace. It
309
+ provides an API for explicitly encrypting and decrypting values, and
310
+ creating data keys. It does not provide an API to query keys from the
311
+ key vault collection, as this can be done directly on the MongoClient.
312
+
313
+ :Parameters:
314
+ - `kms_providers`: Map of KMS provider options. Two KMS providers
315
+ are supported: "aws" and "local". The kmsProviders map values
316
+ differ by provider:
317
+
318
+ - `aws`: Map with "accessKeyId" and "secretAccessKey" as strings.
319
+ These are the AWS access key ID and AWS secret access key used
320
+ to generate KMS messages.
321
+ - `local`: Map with "key" as a 96-byte array or string. "key"
322
+ is the master key used to encrypt/decrypt data keys. This key
323
+ should be generated and stored as securely as possible.
324
+
325
+ - `key_vault_namespace`: The namespace for the key vault collection.
326
+ The key vault collection contains all data keys used for encryption
327
+ and decryption. Data keys are stored as documents in this MongoDB
328
+ collection. Data keys are protected with encryption by a KMS
329
+ provider.
330
+ - `key_vault_client`: A MongoClient connected to a MongoDB cluster
331
+ containing the `key_vault_namespace` collection.
332
+
333
+ .. versionadded:: 3.9
334
+ """
335
+ if not _HAVE_PYMONGOCRYPT :
336
+ raise ConfigurationError (
337
+ "client side encryption requires the pymongocrypt library: "
338
+ "install a compatible version with: "
339
+ "python -m pip install pymongo['encryption']" )
340
+
341
+ self ._kms_providers = kms_providers
342
+ self ._key_vault_namespace = key_vault_namespace
343
+ self ._key_vault_client = key_vault_client
344
+
345
+ db , coll = key_vault_namespace .split ('.' , 1 )
346
+ key_vault_coll = key_vault_client [db ][coll ]
347
+
348
+ self ._io_callbacks = _EncryptionIO (None , key_vault_coll , None , None )
349
+ self ._encryption = ExplicitEncrypter (
350
+ self ._io_callbacks , MongoCryptOptions (kms_providers , None ))
351
+
352
+ def create_data_key (self , kms_provider , master_key = None ,
353
+ key_alt_names = None ):
354
+ """Create and insert a new data key into the key vault collection.
355
+
356
+ :Parameters:
357
+ - `kms_provider`: The KMS provider to use. Supported values are
358
+ "aws" and "local".
359
+ - `master_key`: The `master_key` identifies a KMS-specific key used
360
+ to encrypt the new data key. If the kmsProvider is "local" the
361
+ `master_key` is not applicable and may be omitted.
362
+ If the `kms_provider` is "aws", `master_key` is required and must
363
+ have the following fields:
364
+
365
+ - `region` (string): The AWS region as a string.
366
+ - `key` (string): The Amazon Resource Name (ARN) to the AWS
367
+ customer master key (CMK).
368
+
369
+ - `key_alt_names` (optional): An optional list of string alternate
370
+ names used to reference a key. If a key is created with alternate
371
+ names, then encryption may refer to the key by the unique alternate
372
+ name instead of by ``key_id``. The following example shows creating
373
+ and referring to a data key by alternate name::
374
+
375
+ client_encryption.create_data_key("local", keyAltNames=["name1"])
376
+ # reference the key with the alternate name
377
+ client_encryption.encrypt("457-55-5462", keyAltName="name1",
378
+ algorithm=Algorithm.Random)
379
+
380
+ :Returns:
381
+ The ``_id`` of the created data key document.
382
+ """
383
+ return self ._encryption .create_data_key (
384
+ kms_provider , master_key = master_key , key_alt_names = key_alt_names )
385
+
386
+ def encrypt (self , value , algorithm , key_id = None , key_alt_name = None ):
387
+ """Encrypt a BSON value with a given key and algorithm.
388
+
389
+ Note that exactly one of ``key_id`` or ``key_alt_name`` must be
390
+ provided.
391
+
392
+ :Parameters:
393
+ - `value`: The BSON value to encrypt.
394
+ - `algorithm` (string): The encryption algorithm to use. See
395
+ :class:`Algorithm` for some valid options.
396
+ - `key_id`: Identifies a data key by ``_id`` which must be a UUID
397
+ or a :class:`~bson.binary.Binary` with subtype 4.
398
+ - `key_alt_name`: Identifies a key vault document by 'keyAltName'.
399
+
400
+ :Returns:
401
+ The encrypted value, a :class:`~bson.binary.Binary` with subtype 6.
402
+ """
403
+ # TODO: Add a required codec_options argument for encoding?
404
+ doc = encode ({'v' : value })
405
+ if isinstance (key_id , uuid .UUID ):
406
+ raw_key_id = key_id .bytes
407
+ else :
408
+ raw_key_id = key_id
409
+ encrypted_doc = self ._encryption .encrypt (
410
+ doc , algorithm , key_id = raw_key_id , key_alt_name = key_alt_name )
411
+ return decode (encrypted_doc )['v' ]
412
+
413
+ def decrypt (self , value ):
414
+ """Decrypt an encrypted value.
415
+
416
+ :Parameters:
417
+ - `value` (Binary): The encrypted value, a
418
+ :class:`~bson.binary.Binary` with subtype 6.
419
+
420
+ :Returns:
421
+ The decrypted BSON value.
422
+ """
423
+ doc = encode ({'v' : value })
424
+ decrypted_doc = self ._encryption .decrypt (doc )
425
+ # TODO: Add a required codec_options argument for decoding?
426
+ return decode (decrypted_doc )['v' ]
427
+
428
+ def close (self ):
429
+ """Release resources."""
430
+ self ._io_callbacks .close ()
431
+ self ._encryption .close ()
432
+ self ._io_callbacks = None
433
+ self ._encryption = None
0 commit comments