Skip to content

Commit b37b146

Browse files
authored
PYTHON-3053 Key Management API (#958)
1 parent 0631039 commit b37b146

File tree

49 files changed

+1780
-165
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+1780
-165
lines changed

pymongo/encryption.py

Lines changed: 183 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
import contextlib
1818
import enum
1919
import socket
20-
import uuid
2120
import weakref
2221
from typing import Any, Mapping, Optional, Sequence
2322

@@ -40,6 +39,7 @@
4039
from bson.raw_bson import DEFAULT_RAW_BSON_OPTIONS, RawBSONDocument, _inflate_bson
4140
from bson.son import SON
4241
from pymongo import _csot
42+
from pymongo.cursor import Cursor
4343
from pymongo.daemon import _spawn_daemon
4444
from pymongo.encryption_options import AutoEncryptionOpts
4545
from pymongo.errors import (
@@ -50,8 +50,10 @@
5050
)
5151
from pymongo.mongo_client import MongoClient
5252
from pymongo.network import BLOCKING_IO_ERRORS
53+
from pymongo.operations import UpdateOne
5354
from pymongo.pool import PoolOptions, _configured_socket
5455
from pymongo.read_concern import ReadConcern
56+
from pymongo.results import BulkWriteResult, DeleteResult
5557
from pymongo.ssl_support import get_ssl_context
5658
from pymongo.uri_parser import parse_host
5759
from pymongo.write_concern import WriteConcern
@@ -60,10 +62,11 @@
6062
_KMS_CONNECT_TIMEOUT = 10 # TODO: CDRIVER-3262 will define this value.
6163
_MONGOCRYPTD_TIMEOUT_MS = 10000
6264

65+
6366
_DATA_KEY_OPTS: CodecOptions = CodecOptions(document_class=SON, uuid_representation=STANDARD)
6467
# Use RawBSONDocument codec options to avoid needlessly decoding
6568
# documents from the key vault.
66-
_KEY_VAULT_OPTS = CodecOptions(document_class=RawBSONDocument, uuid_representation=STANDARD)
69+
_KEY_VAULT_OPTS = CodecOptions(document_class=RawBSONDocument)
6770

6871

6972
@contextlib.contextmanager
@@ -225,11 +228,11 @@ def insert_data_key(self, data_key):
225228
"""
226229
raw_doc = RawBSONDocument(data_key, _KEY_VAULT_OPTS)
227230
data_key_id = raw_doc.get("_id")
228-
if not isinstance(data_key_id, uuid.UUID):
229-
raise TypeError("data_key _id must be a UUID")
231+
if not isinstance(data_key_id, Binary) or data_key_id.subtype != UUID_SUBTYPE:
232+
raise TypeError("data_key _id must be Binary with a UUID subtype")
230233

231234
self.key_vault_coll.insert_one(raw_doc)
232-
return Binary(data_key_id.bytes, subtype=UUID_SUBTYPE)
235+
return data_key_id
233236

234237
def bson_encode(self, doc):
235238
"""Encode a document to BSON.
@@ -256,6 +259,30 @@ def close(self):
256259
self.mongocryptd_client = None
257260

258261

262+
class RewrapManyDataKeyResult(object):
263+
def __init__(self, bulk_write_result: Optional[BulkWriteResult] = None) -> None:
264+
"""Result object returned by a ``rewrap_many_data_key`` operation.
265+
266+
:Parameters:
267+
- `bulk_write_result`: The result of the bulk write operation used to
268+
update the key vault collection with one or more rewrapped data keys.
269+
If ``rewrap_many_data_key()`` does not find any matching keys to
270+
rewrap, no bulk write operation will be executed and this field will
271+
be ``None``.
272+
"""
273+
self._bulk_write_result = bulk_write_result
274+
275+
@property
276+
def bulk_write_result(self) -> Optional[BulkWriteResult]:
277+
"""The result of the bulk write operation used to update the key vault
278+
collection with one or more rewrapped data keys. If
279+
``rewrap_many_data_key()`` does not find any matching keys to rewrap,
280+
no bulk write operation will be executed and this field will be
281+
``None``.
282+
"""
283+
return self._bulk_write_result
284+
285+
259286
class _Encrypter(object):
260287
"""Encrypts and decrypts MongoDB commands.
261288
@@ -514,12 +541,15 @@ def __init__(
514541
self._encryption = ExplicitEncrypter(
515542
self._io_callbacks, MongoCryptOptions(kms_providers, None)
516543
)
544+
# Use the same key vault collection as the callback.
545+
self._key_vault_coll = self._io_callbacks.key_vault_coll
517546

518547
def create_data_key(
519548
self,
520549
kms_provider: str,
521550
master_key: Optional[Mapping[str, Any]] = None,
522551
key_alt_names: Optional[Sequence[str]] = None,
552+
key_material: Optional[bytes] = None,
523553
) -> Binary:
524554
"""Create and insert a new data key into the key vault collection.
525555
@@ -580,16 +610,24 @@ def create_data_key(
580610
# reference the key with the alternate name
581611
client_encryption.encrypt("457-55-5462", keyAltName="name1",
582612
algorithm=Algorithm.AEAD_AES_256_CBC_HMAC_SHA_512_Random)
613+
- `key_material` (optional): Sets the custom key material to be used
614+
by the data key for encryption and decryption.
583615
584616
:Returns:
585617
The ``_id`` of the created data key document as a
586618
:class:`~bson.binary.Binary` with subtype
587619
:data:`~bson.binary.UUID_SUBTYPE`.
620+
621+
.. versionchanged:: 4.2
622+
Added the `key_material` parameter.
588623
"""
589624
self._check_closed()
590625
with _wrap_encryption_errors():
591626
return self._encryption.create_data_key(
592-
kms_provider, master_key=master_key, key_alt_names=key_alt_names
627+
kms_provider,
628+
master_key=master_key,
629+
key_alt_names=key_alt_names,
630+
key_material=key_material,
593631
)
594632

595633
def encrypt(
@@ -676,6 +714,145 @@ def decrypt(self, value: Binary) -> Any:
676714
decrypted_doc = self._encryption.decrypt(doc)
677715
return decode(decrypted_doc, codec_options=self._codec_options)["v"]
678716

717+
def get_key(self, id: Binary) -> Optional[RawBSONDocument]:
718+
"""Get a data key by id.
719+
720+
:Parameters:
721+
- `id` (Binary): The UUID of a key a which must be a
722+
:class:`~bson.binary.Binary` with subtype 4 (
723+
:attr:`~bson.binary.UUID_SUBTYPE`).
724+
725+
:Returns:
726+
The key document.
727+
"""
728+
self._check_closed()
729+
return self._key_vault_coll.find_one({"_id": id})
730+
731+
def get_keys(self) -> Cursor[RawBSONDocument]:
732+
"""Get all of the data keys.
733+
734+
:Returns:
735+
An instance of :class:`~pymongo.cursor.Cursor` over the data key
736+
documents.
737+
"""
738+
self._check_closed()
739+
return self._key_vault_coll.find({})
740+
741+
def delete_key(self, id: Binary) -> DeleteResult:
742+
"""Delete a key document in the key vault collection that has the given ``key_id``.
743+
744+
:Parameters:
745+
- `id` (Binary): The UUID of a key a which must be a
746+
:class:`~bson.binary.Binary` with subtype 4 (
747+
:attr:`~bson.binary.UUID_SUBTYPE`).
748+
749+
:Returns:
750+
The delete result.
751+
"""
752+
self._check_closed()
753+
return self._key_vault_coll.delete_one({"_id": id})
754+
755+
def add_key_alt_name(self, id: Binary, key_alt_name: str) -> Any:
756+
"""Add ``key_alt_name`` to the set of alternate names in the key document with UUID ``key_id``.
757+
758+
:Parameters:
759+
- ``id``: The UUID of a key a which must be a
760+
:class:`~bson.binary.Binary` with subtype 4 (
761+
:attr:`~bson.binary.UUID_SUBTYPE`).
762+
- ``key_alt_name``: The key alternate name to add.
763+
764+
:Returns:
765+
The previous version of the key document.
766+
"""
767+
self._check_closed()
768+
update = {"$addToSet": {"keyAltNames": key_alt_name}}
769+
return self._key_vault_coll.find_one_and_update({"_id": id}, update)
770+
771+
def get_key_by_alt_name(self, key_alt_name: str) -> Optional[RawBSONDocument]:
772+
"""Get a key document in the key vault collection that has the given ``key_alt_name``.
773+
774+
:Parameters:
775+
- `key_alt_name`: (str): The key alternate name of the key to get.
776+
777+
:Returns:
778+
The key document.
779+
"""
780+
self._check_closed()
781+
return self._key_vault_coll.find_one({"keyAltNames": key_alt_name})
782+
783+
def remove_key_alt_name(self, id: Binary, key_alt_name: str) -> Optional[RawBSONDocument]:
784+
"""Remove ``key_alt_name`` from the set of keyAltNames in the key document with UUID ``id``.
785+
786+
Also removes the ``keyAltNames`` field from the key document if it would otherwise be empty.
787+
788+
:Parameters:
789+
- ``id``: The UUID of a key a which must be a
790+
:class:`~bson.binary.Binary` with subtype 4 (
791+
:attr:`~bson.binary.UUID_SUBTYPE`).
792+
- ``key_alt_name``: The key alternate name to remove.
793+
794+
:Returns:
795+
Returns the previous version of the key document.
796+
"""
797+
self._check_closed()
798+
pipeline = [
799+
{
800+
"$set": {
801+
"keyAltNames": {
802+
"$cond": [
803+
{"$eq": ["$keyAltNames", [key_alt_name]]},
804+
"$$REMOVE",
805+
{
806+
"$filter": {
807+
"input": "$keyAltNames",
808+
"cond": {"$ne": ["$$this", key_alt_name]},
809+
}
810+
},
811+
]
812+
}
813+
}
814+
}
815+
]
816+
return self._key_vault_coll.find_one_and_update({"_id": id}, pipeline)
817+
818+
def rewrap_many_data_key(
819+
self,
820+
filter: Mapping[str, Any],
821+
provider: Optional[str] = None,
822+
master_key: Optional[Mapping[str, Any]] = None,
823+
) -> RewrapManyDataKeyResult:
824+
"""Decrypts and encrypts all matching data keys in the key vault with a possibly new `master_key` value.
825+
826+
:Parameters:
827+
- `filter`: A document used to filter the data keys.
828+
- `provider`: The new KMS provider to use to encrypt the data keys,
829+
or ``None`` to use the current KMS provider(s).
830+
- ``master_key``: The master key fields corresponding to the new KMS
831+
provider when ``provider`` is not ``None``.
832+
833+
:Returns:
834+
A :class:`RewrapManyDataKeyResult`.
835+
"""
836+
self._check_closed()
837+
with _wrap_encryption_errors():
838+
raw_result = self._encryption.rewrap_many_data_key(filter, provider, master_key)
839+
if raw_result is None:
840+
return RewrapManyDataKeyResult()
841+
842+
raw_doc = RawBSONDocument(raw_result, DEFAULT_RAW_BSON_OPTIONS)
843+
replacements = []
844+
for key in raw_doc["v"]:
845+
update_model = {
846+
"$set": {"keyMaterial": key["keyMaterial"], "masterKey": key["masterKey"]},
847+
"$currentDate": {"updateDate": True},
848+
}
849+
op = UpdateOne({"_id": key["_id"]}, update_model)
850+
replacements.append(op)
851+
if not replacements:
852+
return RewrapManyDataKeyResult()
853+
result = self._key_vault_coll.bulk_write(replacements)
854+
return RewrapManyDataKeyResult(result)
855+
679856
def __enter__(self) -> "ClientEncryption":
680857
return self
681858

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ def build_extension(self, ext):
277277

278278
extras_require = {
279279
"encryption": [
280-
"pymongocrypt@git+ssh://[email protected]/mongodb/libmongocrypt.git@pymongocrypt-1.3.0b0#subdirectory=bindings/python"
280+
"pymongocrypt@git+ssh://[email protected]/mongodb/libmongocrypt.git@161dbc8ae#subdirectory=bindings/python"
281281
],
282282
"ocsp": pyopenssl_reqs,
283283
"snappy": ["python-snappy"],

test/__init__.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"""Test suite for pymongo, bson, and gridfs.
1616
"""
1717

18+
import base64
1819
import gc
1920
import os
2021
import socket
@@ -116,6 +117,27 @@
116117
COMPRESSORS = COMPRESSORS or "zlib"
117118

118119

120+
# Shared KMS data.
121+
LOCAL_MASTER_KEY = base64.b64decode(
122+
b"Mng0NCt4ZHVUYUJCa1kxNkVyNUR1QURhZ2h2UzR2d2RrZzh0cFBwM3R6NmdWMDFBMUN3YkQ"
123+
b"5aXRRMkhGRGdQV09wOGVNYUMxT2k3NjZKelhaQmRCZGJkTXVyZG9uSjFk"
124+
)
125+
AWS_CREDS = {
126+
"accessKeyId": os.environ.get("FLE_AWS_KEY", ""),
127+
"secretAccessKey": os.environ.get("FLE_AWS_SECRET", ""),
128+
}
129+
AZURE_CREDS = {
130+
"tenantId": os.environ.get("FLE_AZURE_TENANTID", ""),
131+
"clientId": os.environ.get("FLE_AZURE_CLIENTID", ""),
132+
"clientSecret": os.environ.get("FLE_AZURE_CLIENTSECRET", ""),
133+
}
134+
GCP_CREDS = {
135+
"email": os.environ.get("FLE_GCP_EMAIL", ""),
136+
"privateKey": os.environ.get("FLE_GCP_PRIVATEKEY", ""),
137+
}
138+
KMIP_CREDS = {"endpoint": os.environ.get("FLE_KMIP_ENDPOINT", "localhost:5698")}
139+
140+
119141
def is_server_resolvable():
120142
"""Returns True if 'server' is resolvable."""
121143
socket_timeout = socket.getdefaulttimeout()

test/client-side-encryption/spec/unified/addKeyAltName.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,11 @@
2222
"keyVaultClient": "client0",
2323
"keyVaultNamespace": "keyvault.datakeys",
2424
"kmsProviders": {
25-
"local": {}
25+
"local": {
26+
"key": {
27+
"$$placeholder": 1
28+
}
29+
}
2630
}
2731
}
2832
}

test/client-side-encryption/spec/unified/createKey-kms_providers-invalid.json renamed to test/client-side-encryption/spec/unified/createDataKey-kms_providers-invalid.json

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"description": "createKey-provider-invalid",
2+
"description": "createDataKey-provider-invalid",
33
"schemaVersion": "1.8",
44
"runOnRequirements": [
55
{
@@ -24,7 +24,14 @@
2424
"keyVaultClient": "client0",
2525
"keyVaultNamespace": "keyvault.datakeys",
2626
"kmsProviders": {
27-
"aws": {}
27+
"aws": {
28+
"accessKeyId": {
29+
"$$placeholder": 1
30+
},
31+
"secretAccessKey": {
32+
"$$placeholder": 1
33+
}
34+
}
2835
}
2936
}
3037
}
@@ -35,7 +42,7 @@
3542
"description": "create data key without required master key fields",
3643
"operations": [
3744
{
38-
"name": "createKey",
45+
"name": "createDataKey",
3946
"object": "clientEncryption0",
4047
"arguments": {
4148
"kmsProvider": "aws",
@@ -59,7 +66,7 @@
5966
"description": "create data key with invalid master key field",
6067
"operations": [
6168
{
62-
"name": "createKey",
69+
"name": "createDataKey",
6370
"object": "clientEncryption0",
6471
"arguments": {
6572
"kmsProvider": "local",
@@ -85,7 +92,7 @@
8592
"description": "create data key with invalid master key",
8693
"operations": [
8794
{
88-
"name": "createKey",
95+
"name": "createDataKey",
8996
"object": "clientEncryption0",
9097
"arguments": {
9198
"kmsProvider": "aws",

0 commit comments

Comments
 (0)