forked from Azure/azure-cosmos-dotnet-v3
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathEncryptionSettingForProperty.cs
More file actions
206 lines (185 loc) · 11.1 KB
/
EncryptionSettingForProperty.cs
File metadata and controls
206 lines (185 loc) · 11.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
//------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
//------------------------------------------------------------
namespace Microsoft.Azure.Cosmos.Encryption
{
using System;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using global::Azure;
using Microsoft.Data.Encryption.Cryptography;
internal sealed class EncryptionSettingForProperty
{
private readonly string databaseRid;
private readonly EncryptionContainer encryptionContainer;
// Test-only hook: when provided, BuildEncryptionAlgorithmForSettingAsync returns this instance
// instead of constructing one via key fetching/unwrapping. This is internal and used only by tests
// through InternalsVisibleTo.
private readonly Microsoft.Data.Encryption.Cryptography.AeadAes256CbcHmac256EncryptionAlgorithm injectedAlgorithm;
public EncryptionSettingForProperty(
string clientEncryptionKeyId,
Data.Encryption.Cryptography.EncryptionType encryptionType,
EncryptionContainer encryptionContainer,
string databaseRid)
{
this.ClientEncryptionKeyId = string.IsNullOrEmpty(clientEncryptionKeyId) ? throw new ArgumentNullException(nameof(clientEncryptionKeyId)) : clientEncryptionKeyId;
this.EncryptionType = encryptionType;
this.encryptionContainer = encryptionContainer ?? throw new ArgumentNullException(nameof(encryptionContainer));
this.databaseRid = string.IsNullOrEmpty(databaseRid) ? throw new ArgumentNullException(nameof(databaseRid)) : databaseRid;
}
// Internal constructor for tests to inject a ready algorithm to enable end-to-end unit testing
// without standing up key providers. Other parameters remain for traceability but are not used
// when an injected algorithm is supplied.
internal EncryptionSettingForProperty(
string clientEncryptionKeyId,
Data.Encryption.Cryptography.EncryptionType encryptionType,
EncryptionContainer encryptionContainer,
string databaseRid,
AeadAes256CbcHmac256EncryptionAlgorithm injectedAlgorithm)
: this(clientEncryptionKeyId, encryptionType, encryptionContainer, databaseRid)
{
this.injectedAlgorithm = injectedAlgorithm ?? throw new ArgumentNullException(nameof(injectedAlgorithm));
}
public string ClientEncryptionKeyId { get; }
public Data.Encryption.Cryptography.EncryptionType EncryptionType { get; }
public async Task<AeadAes256CbcHmac256EncryptionAlgorithm> BuildEncryptionAlgorithmForSettingAsync(CancellationToken cancellationToken)
{
// Return the injected algorithm if provided (test-only path)
if (this.injectedAlgorithm != null)
{
return this.injectedAlgorithm;
}
ClientEncryptionKeyProperties clientEncryptionKeyProperties = await this.encryptionContainer.EncryptionCosmosClient.GetClientEncryptionKeyPropertiesAsync(
clientEncryptionKeyId: this.ClientEncryptionKeyId,
encryptionContainer: this.encryptionContainer,
databaseRid: this.databaseRid,
ifNoneMatchEtag: null,
shouldForceRefresh: false,
cancellationToken: cancellationToken);
ProtectedDataEncryptionKey protectedDataEncryptionKey;
try
{
// we pull out the Encrypted Data Encryption Key and build the Protected Data Encryption key
// Here a request is sent out to unwrap using the Master Key configured via the Key Encryption Key.
protectedDataEncryptionKey = await this.BuildProtectedDataEncryptionKeyAsync(
clientEncryptionKeyProperties,
this.ClientEncryptionKeyId,
cancellationToken);
}
catch (RequestFailedException ex) when (ex.Status == (int)HttpStatusCode.Forbidden)
{
// The access to master key was probably revoked. Try to fetch the latest ClientEncryptionKeyProperties from the backend.
// This will succeed provided the user has rewraped the Client Encryption Key with right set of meta data.
// This is based on the AKV provider implementaion so we expect a RequestFailedException in case other providers are used in unwrap implementation.
// first try to force refresh the local cache, we might have a stale cache.
clientEncryptionKeyProperties = await this.encryptionContainer.EncryptionCosmosClient.GetClientEncryptionKeyPropertiesAsync(
clientEncryptionKeyId: this.ClientEncryptionKeyId,
encryptionContainer: this.encryptionContainer,
databaseRid: this.databaseRid,
ifNoneMatchEtag: null,
shouldForceRefresh: true,
cancellationToken: cancellationToken);
try
{
// try to build the ProtectedDataEncryptionKey. If it fails, try to force refresh the gateway cache and get the latest client encryption key.
protectedDataEncryptionKey = await this.BuildProtectedDataEncryptionKeyAsync(
clientEncryptionKeyProperties,
this.ClientEncryptionKeyId,
cancellationToken);
}
catch (RequestFailedException exOnRetry) when (exOnRetry.Status == (int)HttpStatusCode.Forbidden)
{
// the gateway cache could be stale. Force refresh the gateway cache.
// bail out if this fails.
protectedDataEncryptionKey = await this.ForceRefreshGatewayCacheAndBuildProtectedDataEncryptionKeyAsync(
existingCekEtag: clientEncryptionKeyProperties.ETag,
refreshRetriedOnException: exOnRetry,
cancellationToken: cancellationToken);
}
}
AeadAes256CbcHmac256EncryptionAlgorithm aeadAes256CbcHmac256EncryptionAlgorithm = new AeadAes256CbcHmac256EncryptionAlgorithm(
protectedDataEncryptionKey,
this.EncryptionType);
return aeadAes256CbcHmac256EncryptionAlgorithm;
}
/// <summary>
/// Helper function which force refreshes the gateway cache to fetch the latest client encryption key to build ProtectedDataEncryptionKey object for the encryption setting.
/// </summary>
/// <param name="existingCekEtag">Client encryption key etag to be passed, which is used as If-None-Match Etag for the request. </param>
/// <param name="refreshRetriedOnException"> KEK expired exception. </param>
/// <param name="cancellationToken"> Cancellation token. </param>
/// <returns>ProtectedDataEncryptionKey object. </returns>
private async Task<ProtectedDataEncryptionKey> ForceRefreshGatewayCacheAndBuildProtectedDataEncryptionKeyAsync(
string existingCekEtag,
Exception refreshRetriedOnException,
CancellationToken cancellationToken)
{
ClientEncryptionKeyProperties clientEncryptionKeyProperties;
try
{
// passing ifNoneMatchEtags results in request being sent out with IfNoneMatchEtag set in RequestOptions, this results in the Gateway cache getting force refreshed.
// shouldForceRefresh is set to true so that we dont look up our client cache.
clientEncryptionKeyProperties = await this.encryptionContainer.EncryptionCosmosClient.GetClientEncryptionKeyPropertiesAsync(
clientEncryptionKeyId: this.ClientEncryptionKeyId,
encryptionContainer: this.encryptionContainer,
databaseRid: this.databaseRid,
ifNoneMatchEtag: existingCekEtag,
shouldForceRefresh: true,
cancellationToken: cancellationToken);
}
catch (CosmosException ex)
{
// if there was a retry with ifNoneMatchEtags, the server will send back NotModified if the key resource has not been modified and is up to date.
if (ex.StatusCode == HttpStatusCode.NotModified)
{
// looks like the key was never rewrapped with a valid Key Encryption Key.
throw new EncryptionCosmosException(
$"The Client Encryption Key with key id:{this.ClientEncryptionKeyId} on database:{this.encryptionContainer.Database.Id} and container:{this.encryptionContainer.Id} , needs to be rewrapped with a valid Key Encryption Key using RewrapClientEncryptionKeyAsync. " +
$" The Key Encryption Key used to wrap the Client Encryption Key has been revoked: {refreshRetriedOnException.Message}. {ex.Message}." +
$" Please refer to https://aka.ms/CosmosClientEncryption for more details. ",
HttpStatusCode.BadRequest,
int.Parse(Constants.IncorrectContainerRidSubStatus),
ex.ActivityId,
ex.RequestCharge,
ex.Diagnostics);
}
else
{
throw;
}
}
ProtectedDataEncryptionKey protectedDataEncryptionKey = await this.BuildProtectedDataEncryptionKeyAsync(
clientEncryptionKeyProperties,
this.ClientEncryptionKeyId,
cancellationToken);
return protectedDataEncryptionKey;
}
private async Task<ProtectedDataEncryptionKey> BuildProtectedDataEncryptionKeyAsync(
ClientEncryptionKeyProperties clientEncryptionKeyProperties,
string keyId,
CancellationToken cancellationToken)
{
if (await EncryptionCosmosClient.EncryptionKeyCacheSemaphore.WaitAsync(-1, cancellationToken))
{
try
{
KeyEncryptionKey keyEncryptionKey = KeyEncryptionKey.GetOrCreate(
clientEncryptionKeyProperties.EncryptionKeyWrapMetadata.Name,
clientEncryptionKeyProperties.EncryptionKeyWrapMetadata.Value,
this.encryptionContainer.EncryptionCosmosClient.EncryptionKeyStoreProviderImpl);
ProtectedDataEncryptionKey protectedDataEncryptionKey = ProtectedDataEncryptionKey.GetOrCreate(
keyId,
keyEncryptionKey,
clientEncryptionKeyProperties.WrappedDataEncryptionKey);
return protectedDataEncryptionKey;
}
finally
{
EncryptionCosmosClient.EncryptionKeyCacheSemaphore.Release(1);
}
}
throw new InvalidOperationException("Failed to build ProtectedDataEncryptionKey. ");
}
}
}