Beispiel #1
0
    def _test_encrypt_decrypt(self, key_id=None, key_alt_name=None):
        encrypter = ExplicitEncrypter(
            MockCallback(key_docs=[bson_data('key-document.json')],
                         kms_reply=http_data('kms-reply.txt')),
            self.mongo_crypt_opts())
        self.addCleanup(encrypter.close)

        val = {'v': 'hello'}
        encoded_val = BSON.encode(val)
        algo = "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic"
        encrypted = encrypter.encrypt(encoded_val,
                                      algo,
                                      key_id=key_id,
                                      key_alt_name=key_alt_name)
        self.assertEqual(
            BSON(encrypted).decode(), json_data('encrypted-value.json'))
        self.assertEqual(encrypted, bson_data('encrypted-value.json'))

        decrypted = encrypter.decrypt(encrypted)
        self.assertEqual(BSON(decrypted).decode(), val)
        self.assertEqual(encoded_val, decrypted)
Beispiel #2
0
class ClientEncryption(object):
    """Explicit client-side field level encryption."""
    def __init__(self, kms_providers, key_vault_namespace, key_vault_client,
                 codec_options):
        """Explicit client-side field level encryption.

        The ClientEncryption class encapsulates explicit operations on a key
        vault collection that cannot be done directly on a MongoClient. Similar
        to configuring auto encryption on a MongoClient, it is constructed with
        a MongoClient (to a MongoDB cluster containing the key vault
        collection), KMS provider configuration, and keyVaultNamespace. It
        provides an API for explicitly encrypting and decrypting values, and
        creating data keys. It does not provide an API to query keys from the
        key vault collection, as this can be done directly on the MongoClient.

        See :ref:`explicit-client-side-encryption` for an example.

        :Parameters:
          - `kms_providers`: Map of KMS provider options. Two KMS providers
            are supported: "aws" and "local". The kmsProviders map values
            differ by provider:

              - `aws`: Map with "accessKeyId" and "secretAccessKey" as strings.
                These are the AWS access key ID and AWS secret access key used
                to generate KMS messages. An optional "sessionToken" may be
                included to support temporary AWS credentials.
              - `azure`: Map with "tenantId", "clientId", and "clientSecret" as
                strings. Additionally, "identityPlatformEndpoint" may also be
                specified as a string (defaults to 'login.microsoftonline.com').
                These are the Azure Active Directory credentials used to
                generate Azure Key Vault messages.
              - `gcp`: Map with "email" as a string and "privateKey"
                as `bytes` or a base64 encoded string.
                Additionally, "endpoint" may also be specified as a string
                (defaults to 'oauth2.googleapis.com'). These are the
                credentials used to generate Google Cloud KMS messages.
              - `local`: Map with "key" as `bytes` (96 bytes in length) or
                a base64 encoded string which decodes
                to 96 bytes. "key" is the master key used to encrypt/decrypt
                data keys. This key should be generated and stored as securely
                as possible.

          - `key_vault_namespace`: The namespace for the key vault collection.
            The key vault collection contains all data keys used for encryption
            and decryption. Data keys are stored as documents in this MongoDB
            collection. Data keys are protected with encryption by a KMS
            provider.
          - `key_vault_client`: A MongoClient connected to a MongoDB cluster
            containing the `key_vault_namespace` collection.
          - `codec_options`: An instance of
            :class:`~bson.codec_options.CodecOptions` to use when encoding a
            value for encryption and decoding the decrypted BSON value. This
            should be the same CodecOptions instance configured on the
            MongoClient, Database, or Collection used to access application
            data.

        .. versionadded:: 3.9
        """
        if not _HAVE_PYMONGOCRYPT:
            raise ConfigurationError(
                "client-side field level encryption requires the pymongocrypt "
                "library: install a compatible version with: "
                "python -m pip install 'pymongo[encryption]'")

        if not isinstance(codec_options, CodecOptions):
            raise TypeError("codec_options must be an instance of "
                            "bson.codec_options.CodecOptions")

        self._kms_providers = kms_providers
        self._key_vault_namespace = key_vault_namespace
        self._key_vault_client = key_vault_client
        self._codec_options = codec_options

        db, coll = key_vault_namespace.split('.', 1)
        key_vault_coll = key_vault_client[db][coll]

        self._io_callbacks = _EncryptionIO(None, key_vault_coll, None, None)
        self._encryption = ExplicitEncrypter(
            self._io_callbacks, MongoCryptOptions(kms_providers, None))

    def create_data_key(self,
                        kms_provider,
                        master_key=None,
                        key_alt_names=None):
        """Create and insert a new data key into the key vault collection.

        :Parameters:
          - `kms_provider`: The KMS provider to use. Supported values are
            "aws" and "local".
          - `master_key`: Identifies a KMS-specific key used to encrypt the
            new data key. If the kmsProvider is "local" the `master_key` is
            not applicable and may be omitted.

            If the `kms_provider` is "aws" it is required and has the
            following fields::

              - `region` (string): Required. The AWS region, e.g. "us-east-1".
              - `key` (string): Required. The Amazon Resource Name (ARN) to
                 the AWS customer.
              - `endpoint` (string): Optional. An alternate host to send KMS
                requests to. May include port number, e.g.
                "kms.us-east-1.amazonaws.com:443".

            If the `kms_provider` is "azure" it is required and has the
            following fields::

              - `keyVaultEndpoint` (string): Required. Host with optional
                 port, e.g. "example.vault.azure.net".
              - `keyName` (string): Required. Key name in the key vault.
              - `keyVersion` (string): Optional. Version of the key to use.

            If the `kms_provider` is "gcp" it is required and has the
            following fields::

              - `projectId` (string): Required. The Google cloud project ID.
              - `location` (string): Required. The GCP location, e.g. "us-east1".
              - `keyRing` (string): Required. Name of the key ring that contains
                the key to use.
              - `keyName` (string): Required. Name of the key to use.
              - `keyVersion` (string): Optional. Version of the key to use.
              - `endpoint` (string): Optional. Host with optional port.
                Defaults to "cloudkms.googleapis.com".

          - `key_alt_names` (optional): An optional list of string alternate
            names used to reference a key. If a key is created with alternate
            names, then encryption may refer to the key by the unique alternate
            name instead of by ``key_id``. The following example shows creating
            and referring to a data key by alternate name::

              client_encryption.create_data_key("local", keyAltNames=["name1"])
              # reference the key with the alternate name
              client_encryption.encrypt("457-55-5462", keyAltName="name1",
                                        algorithm=Algorithm.Random)

        :Returns:
          The ``_id`` of the created data key document as a
          :class:`~bson.binary.Binary` with subtype
          :data:`~bson.binary.UUID_SUBTYPE`.
        """
        self._check_closed()
        with _wrap_encryption_errors():
            return self._encryption.create_data_key(
                kms_provider,
                master_key=master_key,
                key_alt_names=key_alt_names)

    def encrypt(self, value, algorithm, key_id=None, key_alt_name=None):
        """Encrypt a BSON value with a given key and algorithm.

        Note that exactly one of ``key_id`` or  ``key_alt_name`` must be
        provided.

        :Parameters:
          - `value`: The BSON value to encrypt.
          - `algorithm` (string): The encryption algorithm to use. See
            :class:`Algorithm` for some valid options.
          - `key_id`: Identifies a data key by ``_id`` which must be a
            :class:`~bson.binary.Binary` with subtype 4 (
            :attr:`~bson.binary.UUID_SUBTYPE`).
          - `key_alt_name`: Identifies a key vault document by 'keyAltName'.

        :Returns:
          The encrypted value, a :class:`~bson.binary.Binary` with subtype 6.
        """
        self._check_closed()
        if (key_id is not None and not (isinstance(key_id, Binary)
                                        and key_id.subtype == UUID_SUBTYPE)):
            raise TypeError(
                'key_id must be a bson.binary.Binary with subtype 4')

        doc = encode({'v': value}, codec_options=self._codec_options)
        with _wrap_encryption_errors():
            encrypted_doc = self._encryption.encrypt(doc,
                                                     algorithm,
                                                     key_id=key_id,
                                                     key_alt_name=key_alt_name)
            return decode(encrypted_doc)['v']

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

        :Parameters:
          - `value` (Binary): The encrypted value, a
            :class:`~bson.binary.Binary` with subtype 6.

        :Returns:
          The decrypted BSON value.
        """
        self._check_closed()
        if not (isinstance(value, Binary) and value.subtype == 6):
            raise TypeError(
                'value to decrypt must be a bson.binary.Binary with subtype 6')

        with _wrap_encryption_errors():
            doc = encode({'v': value})
            decrypted_doc = self._encryption.decrypt(doc)
            return decode(decrypted_doc,
                          codec_options=self._codec_options)['v']

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.close()

    def _check_closed(self):
        if self._encryption is None:
            raise InvalidOperation("Cannot use closed ClientEncryption")

    def close(self):
        """Release resources.

        Note that using this class in a with-statement will automatically call
        :meth:`close`::

            with ClientEncryption(...) as client_encryption:
                encrypted = client_encryption.encrypt(value, ...)
                decrypted = client_encryption.decrypt(encrypted)

        """
        if self._io_callbacks:
            self._io_callbacks.close()
            self._encryption.close()
            self._io_callbacks = None
            self._encryption = None
Beispiel #3
0
class ClientEncryption(object):
    """Explicit client side encryption."""
    def __init__(self, kms_providers, key_vault_namespace, key_vault_client,
                 codec_options):
        """Explicit client side encryption.

        The ClientEncryption class encapsulates explicit operations on a key
        vault collection that cannot be done directly on a MongoClient. Similar
        to configuring auto encryption on a MongoClient, it is constructed with
        a MongoClient (to a MongoDB cluster containing the key vault
        collection), KMS provider configuration, and keyVaultNamespace. It
        provides an API for explicitly encrypting and decrypting values, and
        creating data keys. It does not provide an API to query keys from the
        key vault collection, as this can be done directly on the MongoClient.

        .. note:: Support for client side encryption is in beta.
           Backwards-breaking changes may be made before the final release.

        :Parameters:
          - `kms_providers`: Map of KMS provider options. Two KMS providers
            are supported: "aws" and "local". The kmsProviders map values
            differ by provider:

              - `aws`: Map with "accessKeyId" and "secretAccessKey" as strings.
                These are the AWS access key ID and AWS secret access key used
                to generate KMS messages.
              - `local`: Map with "key" as a 96-byte array or string. "key"
                is the master key used to encrypt/decrypt data keys. This key
                should be generated and stored as securely as possible.

          - `key_vault_namespace`: The namespace for the key vault collection.
            The key vault collection contains all data keys used for encryption
            and decryption. Data keys are stored as documents in this MongoDB
            collection. Data keys are protected with encryption by a KMS
            provider.
          - `key_vault_client`: A MongoClient connected to a MongoDB cluster
            containing the `key_vault_namespace` collection.
          - `codec_options`: An instance of
            :class:`~bson.codec_options.CodecOptions` to use when encoding a
            value for encryption and decoding the decrypted BSON value.

        .. versionadded:: 3.9
        """
        if not _HAVE_PYMONGOCRYPT:
            raise ConfigurationError(
                "client side encryption requires the pymongocrypt library: "
                "install a compatible version with: "
                "python -m pip install pymongo['encryption']")

        if not isinstance(codec_options, CodecOptions):
            raise TypeError("codec_options must be an instance of "
                            "bson.codec_options.CodecOptions")

        self._kms_providers = kms_providers
        self._key_vault_namespace = key_vault_namespace
        self._key_vault_client = key_vault_client
        self._codec_options = codec_options

        db, coll = key_vault_namespace.split('.', 1)
        key_vault_coll = key_vault_client[db][coll]

        self._io_callbacks = _EncryptionIO(None, key_vault_coll, None, None)
        self._encryption = ExplicitEncrypter(
            self._io_callbacks, MongoCryptOptions(kms_providers, None))

    def create_data_key(self,
                        kms_provider,
                        master_key=None,
                        key_alt_names=None):
        """Create and insert a new data key into the key vault collection.

        :Parameters:
          - `kms_provider`: The KMS provider to use. Supported values are
            "aws" and "local".
          - `master_key`: The `master_key` identifies a KMS-specific key used
            to encrypt the new data key. If the kmsProvider is "local" the
            `master_key` is not applicable and may be omitted.
            If the `kms_provider` is "aws", `master_key` is required and must
            have the following fields:

              - `region` (string): The AWS region as a string.
              - `key` (string): The Amazon Resource Name (ARN) to the AWS
                customer master key (CMK).

          - `key_alt_names` (optional): An optional list of string alternate
            names used to reference a key. If a key is created with alternate
            names, then encryption may refer to the key by the unique alternate
            name instead of by ``key_id``. The following example shows creating
            and referring to a data key by alternate name::

              client_encryption.create_data_key("local", keyAltNames=["name1"])
              # reference the key with the alternate name
              client_encryption.encrypt("457-55-5462", keyAltName="name1",
                                        algorithm=Algorithm.Random)

        :Returns:
          The ``_id`` of the created data key document.
        """
        self._check_closed()
        with _wrap_encryption_errors():
            return self._encryption.create_data_key(
                kms_provider,
                master_key=master_key,
                key_alt_names=key_alt_names)

    def encrypt(self, value, algorithm, key_id=None, key_alt_name=None):
        """Encrypt a BSON value with a given key and algorithm.

        Note that exactly one of ``key_id`` or  ``key_alt_name`` must be
        provided.

        :Parameters:
          - `value`: The BSON value to encrypt.
          - `algorithm` (string): The encryption algorithm to use. See
            :class:`Algorithm` for some valid options.
          - `key_id`: Identifies a data key by ``_id`` which must be a
            :class:`~bson.binary.Binary` with subtype 4 (
            :attr:`~bson.binary.UUID_SUBTYPE`).
          - `key_alt_name`: Identifies a key vault document by 'keyAltName'.

        :Returns:
          The encrypted value, a :class:`~bson.binary.Binary` with subtype 6.
        """
        self._check_closed()
        if (key_id is not None and not (isinstance(key_id, Binary)
                                        and key_id.subtype == UUID_SUBTYPE)):
            raise TypeError(
                'key_id must be a bson.binary.Binary with subtype 4')

        doc = encode({'v': value}, codec_options=self._codec_options)
        with _wrap_encryption_errors():
            encrypted_doc = self._encryption.encrypt(doc,
                                                     algorithm,
                                                     key_id=key_id,
                                                     key_alt_name=key_alt_name)
            return decode(encrypted_doc)['v']

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

        :Parameters:
          - `value` (Binary): The encrypted value, a
            :class:`~bson.binary.Binary` with subtype 6.

        :Returns:
          The decrypted BSON value.
        """
        self._check_closed()
        if not (isinstance(value, Binary) and value.subtype == 6):
            raise TypeError(
                'value to decrypt must be a bson.binary.Binary with subtype 6')

        with _wrap_encryption_errors():
            doc = encode({'v': value})
            decrypted_doc = self._encryption.decrypt(doc)
            return decode(decrypted_doc,
                          codec_options=self._codec_options)['v']

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.close()

    def _check_closed(self):
        if self._encryption is None:
            raise InvalidOperation("Cannot use closed ClientEncryption")

    def close(self):
        """Release resources.

        Note that using this class in a with-statement will automatically call
        :meth:`close`::

            with ClientEncryption(...) as client_encryption:
                encrypted = client_encryption.encrypt(value, ...)
                decrypted = client_encryption.decrypt(encrypted)

        """
        if self._io_callbacks:
            self._io_callbacks.close()
            self._encryption.close()
            self._io_callbacks = None
            self._encryption = None