Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ private AutoEncryptionLibMongoCryptController(
IMongoClient metadataClient,
CryptClient cryptClient,
AutoEncryptionOptions autoEncryptionOptions)
: base(cryptClient, keyVaultClient, autoEncryptionOptions.KeyVaultNamespace, autoEncryptionOptions.KmsProviders, autoEncryptionOptions.TlsOptions)
: base(cryptClient, keyVaultClient, autoEncryptionOptions.KeyVaultNamespace, autoEncryptionOptions.KmsProviders, autoEncryptionOptions.TlsOptions, autoEncryptionOptions.KmsConnector)
{
_internalClient = internalClient; // can be null
_metadataClient = metadataClient; // can be null
Expand Down
22 changes: 19 additions & 3 deletions src/MongoDB.Driver.Encryption/ClientEncryptionOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public sealed class ClientEncryptionOptions
{
// private fields
private TimeSpan? _keyExpiration;
private readonly IKmsConnector _kmsConnector;
private readonly IMongoClient _keyVaultClient;
private readonly CollectionNamespace _keyVaultNamespace;
private readonly IReadOnlyDictionary<string, IReadOnlyDictionary<string, object>> _kmsProviders;
Expand All @@ -39,12 +40,14 @@ public sealed class ClientEncryptionOptions
/// <param name="keyVaultNamespace">The key vault namespace.</param>
/// <param name="kmsProviders">The KMS providers.</param>
/// <param name="tlsOptions">The tls options.</param>
/// <param name="kmsConnector">The KMS connector used to open connections to KMS hosts.</param>
public ClientEncryptionOptions(
IMongoClient keyVaultClient,
CollectionNamespace keyVaultNamespace,
IReadOnlyDictionary<string, IReadOnlyDictionary<string, object>> kmsProviders,
Optional<IReadOnlyDictionary<string, SslSettings>> tlsOptions = default)
: this(keyVaultClient, keyVaultNamespace, kmsProviders, tlsOptions, keyExpiration: null)
Optional<IReadOnlyDictionary<string, SslSettings>> tlsOptions = default,
Optional<IKmsConnector> kmsConnector = default)
: this(keyVaultClient, keyVaultNamespace, kmsProviders, tlsOptions, kmsConnector, keyExpiration: null)
Comment on lines 44 to +50

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@adelinowona I guess we'll address this during backporting?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep

{
}

Expand All @@ -53,12 +56,14 @@ private ClientEncryptionOptions(
CollectionNamespace keyVaultNamespace,
IReadOnlyDictionary<string, IReadOnlyDictionary<string, object>> kmsProviders,
Optional<IReadOnlyDictionary<string, SslSettings>> tlsOptions = default,
Optional<IKmsConnector> kmsConnector = default,
Optional<TimeSpan?> keyExpiration = default)
{
_keyVaultClient = Ensure.IsNotNull(keyVaultClient, nameof(keyVaultClient));
_keyVaultNamespace = Ensure.IsNotNull(keyVaultNamespace, nameof(keyVaultNamespace));
_kmsProviders = Ensure.IsNotNull(kmsProviders, nameof(kmsProviders));
_tlsOptions = tlsOptions.WithDefault(new Dictionary<string, SslSettings>());
_kmsConnector = kmsConnector.WithDefault(null);
_keyExpiration = keyExpiration.WithDefault(null);

EnsureKmsProvidersAreValid(_kmsProviders);
Expand All @@ -72,6 +77,14 @@ private ClientEncryptionOptions(
/// </summary>
public TimeSpan? KeyExpiration => _keyExpiration;

/// <summary>
/// Gets the KMS connector used to open connections to KMS hosts.
/// </summary>
/// <value>
/// The KMS connector used to open connections to KMS hosts.
/// </value>
public IKmsConnector KmsConnector => _kmsConnector;

/// <summary>
/// Gets the key vault client.
/// </summary>
Expand Down Expand Up @@ -111,18 +124,21 @@ private ClientEncryptionOptions(
/// <param name="keyVaultNamespace">The key vault namespace.</param>
/// <param name="kmsProviders">The KMS providers.</param>
/// <param name="tlsOptions">The tls options.</param>
/// <param name="kmsConnector">The KMS connector used to open connections to KMS hosts.</param>
/// <returns>A new ClientEncryptionOptions instance.</returns>
public ClientEncryptionOptions With(
Optional<IMongoClient> keyVaultClient = default,
Optional<CollectionNamespace> keyVaultNamespace = default,
Optional<IReadOnlyDictionary<string, IReadOnlyDictionary<string, object>>> kmsProviders = default,
Optional<IReadOnlyDictionary<string, SslSettings>> tlsOptions = default)
Optional<IReadOnlyDictionary<string, SslSettings>> tlsOptions = default,
Optional<IKmsConnector> kmsConnector = default)
{
return new ClientEncryptionOptions(
keyVaultClient: keyVaultClient.WithDefault(_keyVaultClient),
keyVaultNamespace: keyVaultNamespace.WithDefault(_keyVaultNamespace),
kmsProviders: kmsProviders.WithDefault(_kmsProviders),
tlsOptions: Optional.Create(tlsOptions.WithDefault(_tlsOptions)),
kmsConnector: Optional.Create(kmsConnector.WithDefault(_kmsConnector)),
keyExpiration: _keyExpiration);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public ExplicitEncryptionLibMongoCryptController(
ClientEncryptionOptions clientEncryptionOptions)
: base(cryptClient,
Ensure.IsNotNull(Ensure.IsNotNull(clientEncryptionOptions, nameof(clientEncryptionOptions)).KeyVaultClient, nameof(clientEncryptionOptions.KeyVaultClient)),
clientEncryptionOptions.KeyVaultNamespace, clientEncryptionOptions.KmsProviders, clientEncryptionOptions.TlsOptions)
clientEncryptionOptions.KeyVaultNamespace, clientEncryptionOptions.KmsProviders, clientEncryptionOptions.TlsOptions, clientEncryptionOptions.KmsConnector)
{
}

Expand Down
59 changes: 59 additions & 0 deletions src/MongoDB.Driver.Encryption/KmsConnectorStreamFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/* Copyright 2019-present MongoDB Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

using System;
using System.IO;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using MongoDB.Driver.Core.Connections;
using MongoDB.Driver.Core.Misc;

namespace MongoDB.Driver.Encryption;

internal sealed class KmsConnectorStreamFactory : IStreamFactory
{
private readonly IKmsConnector _kmsConnector;

public KmsConnectorStreamFactory(IKmsConnector kmsConnector)
{
_kmsConnector = Ensure.IsNotNull(kmsConnector, nameof(kmsConnector));
}

public Stream CreateStream(EndPoint endPoint, CancellationToken cancellationToken)
{
var (host, port) = GetHostAndPort(endPoint);
var stream = _kmsConnector.Connect(host, port, cancellationToken);
return EnsureConnectResult(stream, nameof(IKmsConnector.Connect));
}

public async Task<Stream> CreateStreamAsync(EndPoint endPoint, CancellationToken cancellationToken)
{
var (host, port) = GetHostAndPort(endPoint);
var stream = await _kmsConnector.ConnectAsync(host, port, cancellationToken).ConfigureAwait(false);
return EnsureConnectResult(stream, nameof(IKmsConnector.ConnectAsync));
}

private static Stream EnsureConnectResult(Stream stream, string methodName)
{
return stream ?? throw new InvalidOperationException($"{nameof(IKmsConnector)}.{methodName} returned null.");
}

private static (string Host, int Port) GetHostAndPort(EndPoint endPoint)
{
var dnsEndPoint = (DnsEndPoint)endPoint;
return (dnsEndPoint.Host, dnsEndPoint.Port);
}
}
11 changes: 6 additions & 5 deletions src/MongoDB.Driver.Encryption/LibMongoCryptControllerBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ internal abstract class LibMongoCryptControllerBase
protected readonly CollectionNamespace _keyVaultNamespace;

// private fields
private readonly IStreamFactory _kmsStreamFactory;
private readonly IReadOnlyDictionary<string, IReadOnlyDictionary<string, object>> _kmsProviders;
private readonly IStreamFactory _networkStreamFactory;
private readonly IReadOnlyDictionary<string, SslSettings> _tlsOptions;

// constructors
Expand All @@ -49,15 +49,16 @@ protected LibMongoCryptControllerBase(
IMongoClient keyVaultClient,
CollectionNamespace keyVaultNamespace,
IReadOnlyDictionary<string, IReadOnlyDictionary<string, object>> kmsProviders,
IReadOnlyDictionary<string, SslSettings> tlsOptions)
IReadOnlyDictionary<string, SslSettings> tlsOptions,
IKmsConnector kmsConnector)
{
_cryptClient = Ensure.IsNotNull(cryptClient, nameof(cryptClient));
_keyVaultClient = Ensure.IsNotNull(keyVaultClient, nameof(keyVaultClient)); // _keyVaultClient might not be fully constructed at this point, don't call any instance methods on it yet
_keyVaultNamespace = Ensure.IsNotNull(keyVaultNamespace, nameof(keyVaultNamespace));
_keyVaultCollection = new Lazy<IMongoCollection<BsonDocument>>(GetKeyVaultCollection); // delay use _keyVaultClient
_kmsProviders = Ensure.IsNotNull(kmsProviders, nameof(kmsProviders));
_networkStreamFactory = new NetworkStreamFactory();
_tlsOptions = Ensure.IsNotNull(tlsOptions, nameof(tlsOptions));
_kmsStreamFactory = kmsConnector != null ? new KmsConnectorStreamFactory(kmsConnector) : new NetworkStreamFactory(); // kmsConnector is optional; null means connect directly to the KMS host
}

// public properties
Expand Down Expand Up @@ -287,7 +288,7 @@ private void SendKmsRequest(KmsRequest request, CancellationToken cancellation)
var endpoint = CreateKmsEndPoint(request.Endpoint);

var tlsStreamSettings = GetTlsStreamSettings(request.KmsProvider);
var sslStreamFactory = new SslStreamFactory(tlsStreamSettings, _networkStreamFactory);
var sslStreamFactory = new SslStreamFactory(tlsStreamSettings, _kmsStreamFactory);
using var sslStream = sslStreamFactory.CreateStream(endpoint, cancellation);

var sleepMs = request.Sleep;
Expand Down Expand Up @@ -331,7 +332,7 @@ private async Task SendKmsRequestAsync(KmsRequest request, CancellationToken can
var endpoint = CreateKmsEndPoint(request.Endpoint);

var tlsStreamSettings = GetTlsStreamSettings(request.KmsProvider);
var sslStreamFactory = new SslStreamFactory(tlsStreamSettings, _networkStreamFactory);
var sslStreamFactory = new SslStreamFactory(tlsStreamSettings, _kmsStreamFactory);
using var sslStream = await sslStreamFactory.CreateStreamAsync(endpoint, cancellation).ConfigureAwait(false);

var sleepMs = request.Sleep;
Expand Down
24 changes: 21 additions & 3 deletions src/MongoDB.Driver/AutoEncryptionOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ public sealed class AutoEncryptionOptions
private TimeSpan? _keyExpiration;
private readonly IReadOnlyDictionary<string, BsonDocument> _encryptedFieldsMap;
private readonly IReadOnlyDictionary<string, object> _extraOptions;
private readonly IKmsConnector _kmsConnector;
private readonly IMongoClient _keyVaultClient;
private readonly CollectionNamespace _keyVaultNamespace;
private readonly IReadOnlyDictionary<string, IReadOnlyDictionary<string, object>> _kmsProviders;
Expand All @@ -57,6 +58,7 @@ public sealed class AutoEncryptionOptions
/// <param name="tlsOptions">The tls options.</param>
/// <param name="encryptedFieldsMap">The encryptedFields map.</param>
/// <param name="bypassQueryAnalysis">The bypass query analysis flag.</param>
/// <param name="kmsConnector">The KMS connector used to open connections to KMS hosts.</param>
public AutoEncryptionOptions(
CollectionNamespace keyVaultNamespace,
IReadOnlyDictionary<string, IReadOnlyDictionary<string, object>> kmsProviders,
Expand All @@ -66,8 +68,9 @@ public AutoEncryptionOptions(
Optional<IReadOnlyDictionary<string, BsonDocument>> schemaMap = default,
Optional<IReadOnlyDictionary<string, SslSettings>> tlsOptions = default,
Optional<IReadOnlyDictionary<string, BsonDocument>> encryptedFieldsMap = default,
Optional<bool?> bypassQueryAnalysis = default)
: this(keyVaultNamespace, kmsProviders, bypassAutoEncryption, extraOptions, keyVaultClient, schemaMap, tlsOptions, encryptedFieldsMap, bypassQueryAnalysis, keyExpiration: null)
Optional<bool?> bypassQueryAnalysis = default,
Optional<IKmsConnector> kmsConnector = default)
: this(keyVaultNamespace, kmsProviders, bypassAutoEncryption, extraOptions, keyVaultClient, schemaMap, tlsOptions, encryptedFieldsMap, bypassQueryAnalysis, kmsConnector, keyExpiration: null)
Comment thread
adelinowona marked this conversation as resolved.
{
}

Expand All @@ -81,6 +84,7 @@ private AutoEncryptionOptions(
Optional<IReadOnlyDictionary<string, SslSettings>> tlsOptions,
Optional<IReadOnlyDictionary<string, BsonDocument>> encryptedFieldsMap,
Optional<bool?> bypassQueryAnalysis,
Optional<IKmsConnector> kmsConnector,
Optional<TimeSpan?> keyExpiration)
{
_keyVaultNamespace = Ensure.IsNotNull(keyVaultNamespace, nameof(keyVaultNamespace));
Expand All @@ -89,6 +93,7 @@ private AutoEncryptionOptions(
_bypassQueryAnalysis = bypassQueryAnalysis.WithDefault(null);
_keyExpiration = keyExpiration.WithDefault(null);
_extraOptions = extraOptions.WithDefault(null);
_kmsConnector = kmsConnector.WithDefault(null);
_keyVaultClient = keyVaultClient.WithDefault(null);
_schemaMap = schemaMap.WithDefault(null);
_tlsOptions = tlsOptions.WithDefault(new Dictionary<string, SslSettings>());
Expand Down Expand Up @@ -137,6 +142,14 @@ private AutoEncryptionOptions(
/// </remarks>
public IReadOnlyDictionary<string, object> ExtraOptions => _extraOptions;

/// <summary>
/// Gets the KMS connector used to open connections to KMS hosts.
/// </summary>
/// <value>
/// The KMS connector used to open connections to KMS hosts.
/// </value>
public IKmsConnector KmsConnector => _kmsConnector;

/// <summary>
/// Gets the key vault client.
/// </summary>
Expand Down Expand Up @@ -199,6 +212,7 @@ public void SetKeyExpiration(TimeSpan? keyExpiration)
/// <param name="schemaMap">The schema map.</param>
/// <param name="tlsOptions">The tls options.</param>
/// <param name="encryptedFieldsMap">The encryptedFields map.</param>
/// <param name="kmsConnector">The KMS connector used to open connections to KMS hosts.</param>
/// <returns>A new instance of <see cref="AutoEncryptionOptions"/>.</returns>
public AutoEncryptionOptions With(
Optional<CollectionNamespace> keyVaultNamespace = default,
Expand All @@ -209,7 +223,8 @@ public AutoEncryptionOptions With(
Optional<IMongoClient> keyVaultClient = default,
Optional<IReadOnlyDictionary<string, BsonDocument>> schemaMap = default,
Optional<IReadOnlyDictionary<string, SslSettings>> tlsOptions = default,
Optional<IReadOnlyDictionary<string, BsonDocument>> encryptedFieldsMap = default)
Optional<IReadOnlyDictionary<string, BsonDocument>> encryptedFieldsMap = default,
Optional<IKmsConnector> kmsConnector = default)
{
return new AutoEncryptionOptions(
keyVaultNamespace.WithDefault(_keyVaultNamespace),
Expand All @@ -221,6 +236,7 @@ public AutoEncryptionOptions With(
Optional.Create(tlsOptions.WithDefault(_tlsOptions)),
Optional.Create(encryptedFieldsMap.WithDefault(_encryptedFieldsMap)),
Optional.Create(bypassQueryAnalysis.WithDefault(_bypassQueryAnalysis)),
Optional.Create(kmsConnector.WithDefault(_kmsConnector)),
_keyExpiration);
}

Expand All @@ -235,6 +251,7 @@ public override bool Equals(object obj)
_bypassQueryAnalysis == rhs._bypassQueryAnalysis &&
_keyExpiration == rhs._keyExpiration &&
ExtraOptionsEquals(_extraOptions, rhs._extraOptions) &&
object.ReferenceEquals(_kmsConnector, rhs._kmsConnector) &&
object.ReferenceEquals(_keyVaultClient, rhs._keyVaultClient) &&
_keyVaultNamespace.Equals(rhs._keyVaultNamespace) &&
KmsProvidersEqualityHelper.Equals(_kmsProviders, rhs._kmsProviders) &&
Expand All @@ -251,6 +268,7 @@ public override int GetHashCode()
.Hash(_bypassQueryAnalysis)
.Hash(_keyExpiration)
.HashElements(_extraOptions)
.Hash(_kmsConnector)
.Hash(_keyVaultClient)
.Hash(_keyVaultNamespace)
.HashElements(_kmsProviders)
Expand Down
54 changes: 54 additions & 0 deletions src/MongoDB.Driver/Encryption/IKmsConnector.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/* Copyright 2019-present MongoDB Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

using System.IO;
using System.Threading;
using System.Threading.Tasks;

namespace MongoDB.Driver.Encryption;

/// <summary>
/// Opens the transport connection used to reach a KMS host. When supplied via
/// <c>ClientEncryptionOptions</c> or <see cref="AutoEncryptionOptions"/>, the driver
/// invokes this instead of opening a direct TCP connection to the KMS host, then wraps the
/// returned stream in TLS using the KMS provider's configured TLS options.
/// The primary use case is routing KMS traffic through an HTTP proxy via HTTPS CONNECT.
/// </summary>
/// <remarks>
/// Both <see cref="Connect"/> and <see cref="ConnectAsync"/> must be implemented, even if the
/// application only uses one of the driver's sync or async encryption APIs: the driver calls
/// whichever method matches the API used for the operation in progress. An implementation that
/// only supports one direction can have the other throw.
/// </remarks>
public interface IKmsConnector

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this new abstraction? IStreamFactory looks very similar and it's public too. Can we let users provide the stream factory and do not introduce a new interface?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed offline but I added some reasoning in the PR description.

{
/// <summary>
/// Opens a connection to the specified KMS host.
/// </summary>
/// <param name="host">The KMS hostname (for example, <c>kms.us-east-1.amazonaws.com</c>).</param>
/// <param name="port">The KMS port.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A stream connected to the KMS host. The driver wraps this stream in TLS. Must not be null.</returns>
Stream Connect(string host, int port, CancellationToken cancellationToken);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we use Endpoint instead of host and port?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed offline as well but for visibility:
I'd keep host/port. EndPoint looks tidier but the base class doesn't actually expose Host/Port — they only exist on DnsEndPoint, so a Connect(EndPoint, ...) signature forces every user to downcast and just know it's always a DnsEndPoint. host/port is self-documenting, needs no cast, and maps 1:1 to the spec's callback.


/// <summary>
/// Opens a connection to the specified KMS host.
/// </summary>
/// <param name="host">The KMS hostname (for example, <c>kms.us-east-1.amazonaws.com</c>).</param>
/// <param name="port">The KMS port.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A stream connected to the KMS host. The driver wraps this stream in TLS. Must not be null.</returns>
Task<Stream> ConnectAsync(string host, int port, CancellationToken cancellationToken);
}
Loading
Loading