Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
e3ff948
feat(encryption): enhance EncryptionProcessor internal API visibility
adamnova Aug 14, 2025
33d4b0c
fix(encryption-tests): update test project configuration
adamnova Aug 14, 2025
137e776
feat(encryption-tests): add centralized test crypto utilities
adamnova Aug 14, 2025
8f48492
test(encryption): add EncryptionContainer patch operation tests
adamnova Aug 14, 2025
e998a2e
test(encryption): add encryption diagnostics tests
adamnova Aug 14, 2025
5ae877b
test(encryption): add end-to-end EncryptionProcessor tests
adamnova Aug 14, 2025
7130bea
test(encryption): add argument and settings validation tests
adamnova Aug 14, 2025
97b650b
test(encryption): add type serialization and validation tests
adamnova Aug 14, 2025
5330b6b
test(encryption): add ID field escaping and Unicode tests
adamnova Aug 14, 2025
4919df6
test(encryption): add depth and traversal tests
adamnova Aug 14, 2025
cd045e2
test(encryption): add stream handling and no-op decryption tests
adamnova Aug 14, 2025
dc650e4
test(encryption): add feed response processing tests
adamnova Aug 14, 2025
bffc2d6
test(encryption): add specialized algorithm and edge case tests
adamnova Aug 14, 2025
8397d03
chore: update development environment configuration
adamnova Aug 14, 2025
58626c7
chore: add empty helpers directory for future .NET 8 support
adamnova Aug 14, 2025
826a380
refactor: Reorganize EncryptionProcessor tests into meaningful partia…
adamnova Aug 14, 2025
3912492
Refactor EncryptionProcessor tests: consolidate classes and improve s…
adamnova Aug 14, 2025
a7766fe
Delete tasks.json
adamnova Aug 14, 2025
9921565
revert: restore Microsoft.Azure.Cosmos.sln to upstream master
adamnova Aug 14, 2025
2b4f530
Cleanup
adamnova Aug 15, 2025
ff48295
Added new tests
adamnova Aug 15, 2025
dc4b71a
Cleanup
adamnova Aug 15, 2025
e2f7a88
Cleanup
adamnova Aug 15, 2025
73c3ed2
Merge branch 'master' into feature/encryption-processor-test-suite-ov…
adamnova Aug 22, 2025
1507eda
Merge branch 'master' into feature/encryption-processor-test-suite-ov…
adamnova Aug 27, 2025
48ab628
Merge branch 'master' into feature/encryption-processor-test-suite-ov…
adamnova Oct 6, 2025
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
138 changes: 69 additions & 69 deletions Microsoft.Azure.Cosmos.Encryption/src/EncryptionProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ internal static class EncryptionProcessor
// UTF-8 Encoding
private static readonly SqlVarCharSerializer SqlVarcharSerializer = new SqlVarCharSerializer(size: -1, codePageCharacterEncoding: 65001);

private enum TypeMarker : byte
internal enum TypeMarker : byte
{
Null = 1, // not used
Boolean = 2,
Expand Down Expand Up @@ -222,7 +222,7 @@ await EncryptJTokenAsync(
return EncryptionProcessor.BaseSerializer.ToStream(encryptedPropertyValue);
}

private static (TypeMarker, byte[]) Serialize(JToken propertyValue)
internal static (TypeMarker, byte[]) Serialize(JToken propertyValue)
{
return propertyValue.Type switch
{
Expand All @@ -234,7 +234,7 @@ private static (TypeMarker, byte[]) Serialize(JToken propertyValue)
};
}

private static JToken DeserializeAndAddProperty(
internal static JToken DeserializeAndAddProperty(
byte[] serializedBytes,
TypeMarker typeMarker)
{
Expand All @@ -248,11 +248,11 @@ private static JToken DeserializeAndAddProperty(
};
}

private static async Task EncryptJTokenAsync(
JToken jTokenToEncrypt,
EncryptionSettingForProperty encryptionSettingForProperty,
bool shouldEscape,
CancellationToken cancellationToken)
internal static async Task EncryptJTokenAsync(
JToken jTokenToEncrypt,
EncryptionSettingForProperty encryptionSettingForProperty,
bool shouldEscape,
CancellationToken cancellationToken)
{
// Top Level can be an Object
if (jTokenToEncrypt.Type == JTokenType.Object)
Expand Down Expand Up @@ -292,6 +292,63 @@ await EncryptJTokenAsync(
return;
}

internal static async Task DecryptJTokenAsync(
JToken jTokenToDecrypt,
EncryptionSettingForProperty encryptionSettingForProperty,
bool isEscaped,
CancellationToken cancellationToken)
{
if (jTokenToDecrypt.Type == JTokenType.Object)
{
foreach (JProperty jProperty in jTokenToDecrypt.Children<JProperty>())
{
await DecryptJTokenAsync(
jProperty.Value,
encryptionSettingForProperty,
isEscaped,
cancellationToken);
}
}
else if (jTokenToDecrypt.Type == JTokenType.Array)
{
if (jTokenToDecrypt.Children().Any())
{
for (int i = 0; i < jTokenToDecrypt.Count(); i++)
{
await DecryptJTokenAsync(
jTokenToDecrypt[i],
encryptionSettingForProperty,
isEscaped,
cancellationToken);
}
}
}
else
{
jTokenToDecrypt.Replace(await DecryptAndDeserializeValueAsync(
jTokenToDecrypt,
encryptionSettingForProperty,
isEscaped,
cancellationToken));
}
}

internal static string ConvertToBase64UriSafeString(byte[] bytesToProcess)
{
string base64String = Convert.ToBase64String(bytesToProcess);

// Base 64 Encoding with URL and Filename Safe Alphabet https://datatracker.ietf.org/doc/html/rfc4648#section-5
// https://docs.microsoft.com/en-us/azure/cosmos-db/concepts-limits#per-item-limits, due to base64 conversion and encryption
// the permissible size of the property will further reduce.
return new StringBuilder(base64String, base64String.Length).Replace("/", "_").Replace("+", "-").ToString();
}

internal static byte[] ConvertFromBase64UriSafeString(string uriSafeBase64String)
{
StringBuilder fromUriSafeBase64String = new StringBuilder(uriSafeBase64String, uriSafeBase64String.Length).Replace("_", "/").Replace("-", "+");
return Convert.FromBase64String(fromUriSafeBase64String.ToString());
}

private static async Task<JToken> SerializeAndEncryptValueAsync(
JToken jTokenToEncrypt,
EncryptionSettingForProperty encryptionSettingForProperty,
Expand Down Expand Up @@ -337,10 +394,10 @@ private static async Task<JToken> SerializeAndEncryptValueAsync(
}

private static async Task<JToken> DecryptAndDeserializeValueAsync(
JToken jToken,
EncryptionSettingForProperty encryptionSettingForProperty,
bool isEscaped,
CancellationToken cancellationToken)
JToken jToken,
EncryptionSettingForProperty encryptionSettingForProperty,
bool isEscaped,
CancellationToken cancellationToken)
{
byte[] cipherTextWithTypeMarker = null;

Expand Down Expand Up @@ -379,47 +436,6 @@ private static async Task<JToken> DecryptAndDeserializeValueAsync(
(TypeMarker)cipherTextWithTypeMarker[0]);
}

private static async Task DecryptJTokenAsync(
JToken jTokenToDecrypt,
EncryptionSettingForProperty encryptionSettingForProperty,
bool isEscaped,
CancellationToken cancellationToken)
{
if (jTokenToDecrypt.Type == JTokenType.Object)
{
foreach (JProperty jProperty in jTokenToDecrypt.Children<JProperty>())
{
await DecryptJTokenAsync(
jProperty.Value,
encryptionSettingForProperty,
isEscaped,
cancellationToken);
}
}
else if (jTokenToDecrypt.Type == JTokenType.Array)
{
if (jTokenToDecrypt.Children().Any())
{
for (int i = 0; i < jTokenToDecrypt.Count(); i++)
{
await DecryptJTokenAsync(
jTokenToDecrypt[i],
encryptionSettingForProperty,
isEscaped,
cancellationToken);
}
}
}
else
{
jTokenToDecrypt.Replace(await DecryptAndDeserializeValueAsync(
jTokenToDecrypt,
encryptionSettingForProperty,
isEscaped,
cancellationToken));
}
}

private static async Task<int> DecryptObjectAsync(
JObject document,
EncryptionSettings encryptionSettings,
Expand Down Expand Up @@ -471,21 +487,5 @@ private static JObject RetrieveItem(

return itemJObj;
}

private static string ConvertToBase64UriSafeString(byte[] bytesToProcess)
{
string base64String = Convert.ToBase64String(bytesToProcess);

// Base 64 Encoding with URL and Filename Safe Alphabet https://datatracker.ietf.org/doc/html/rfc4648#section-5
// https://docs.microsoft.com/en-us/azure/cosmos-db/concepts-limits#per-item-limits, due to base64 conversion and encryption
// the permissible size of the property will further reduce.
return new StringBuilder(base64String, base64String.Length).Replace("/", "_").Replace("+", "-").ToString();
}

private static byte[] ConvertFromBase64UriSafeString(string uriSafeBase64String)
{
StringBuilder fromUriSafeBase64String = new StringBuilder(uriSafeBase64String, uriSafeBase64String.Length).Replace("_", "/").Replace("-", "+");
return Convert.FromBase64String(fromUriSafeBase64String.ToString());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ internal sealed class EncryptionSettingForProperty

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,
Expand All @@ -29,12 +34,32 @@ public EncryptionSettingForProperty(
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,
Expand Down
17 changes: 8 additions & 9 deletions Microsoft.Azure.Cosmos.Encryption/src/EncryptionSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ internal sealed class EncryptionSettings
{
private readonly Dictionary<string, EncryptionSettingForProperty> encryptionSettingsDictByPropertyName;

private EncryptionSettings(string containerRidValue, IReadOnlyList<string> partitionKeyPath)
internal EncryptionSettings(string containerRidValue, IReadOnlyList<string> partitionKeyPath)
{
this.ContainerRidValue = containerRidValue;
this.PartitionKeyPaths = partitionKeyPath;
Expand All @@ -38,7 +38,6 @@ public static Task<EncryptionSettings> CreateAsync(EncryptionContainer encryptio
public EncryptionSettingForProperty GetEncryptionSettingForProperty(string propertyName)
{
this.encryptionSettingsDictByPropertyName.TryGetValue(propertyName, out EncryptionSettingForProperty encryptionSettingsForProperty);

return encryptionSettingsForProperty;
}

Expand All @@ -51,6 +50,13 @@ public void SetRequestHeaders(RequestOptions requestOptions)
};
}

internal void SetEncryptionSettingForProperty(
string propertyName,
EncryptionSettingForProperty encryptionSettingsForProperty)
{
this.encryptionSettingsDictByPropertyName[propertyName] = encryptionSettingsForProperty;
}

private static Data.Encryption.Cryptography.EncryptionType GetEncryptionTypeForProperty(ClientEncryptionIncludedPath clientEncryptionIncludedPath)
{
return clientEncryptionIncludedPath.EncryptionType switch
Expand Down Expand Up @@ -135,12 +141,5 @@ await encryptionContainer.EncryptionCosmosClient.GetClientEncryptionKeyPropertie

return encryptionSettings;
}

private void SetEncryptionSettingForProperty(
string propertyName,
EncryptionSettingForProperty encryptionSettingsForProperty)
{
this.encryptionSettingsDictByPropertyName[propertyName] = encryptionSettingsForProperty;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
//------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
//------------------------------------------------------------

namespace Microsoft.Azure.Cosmos.Encryption.Tests
{
using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;

[TestClass]
public class EncryptionContainerPatchTests
{
private static T CreateUninitialized<T>() where T : class
{
return (T)FormatterServices.GetUninitializedObject(typeof(T));
}

private static EncryptionSettings CreateEncryptionSettingsWithEncryptedProperty(string propertyName, EncryptionContainer container)
{
if (container == null) throw new ArgumentNullException(nameof(container));

EncryptionSettings settings = new EncryptionSettings("rid", new List<string> { "/id" });
EncryptionSettingForProperty forProperty = new EncryptionSettingForProperty(
clientEncryptionKeyId: "cek1",
encryptionType: Microsoft.Data.Encryption.Cryptography.EncryptionType.Randomized,
encryptionContainer: container,
databaseRid: "dbRid");

settings.SetEncryptionSettingForProperty(propertyName, forProperty);
return settings;
}

[TestMethod]
public async Task EncryptPatchOperationsAsync_Increment_On_Encrypted_Path_Throws()
{
// Arrange
EncryptionContainer container = CreateUninitialized<EncryptionContainer>();
EncryptionSettings settings = CreateEncryptionSettingsWithEncryptedProperty("Sensitive", container);
List<PatchOperation> ops = new List<PatchOperation>
{
PatchOperation.Increment("/Sensitive", 1)
};

EncryptionDiagnosticsContext diag = new EncryptionDiagnosticsContext();

// Act + Assert
InvalidOperationException ex = await Assert.ThrowsExceptionAsync<InvalidOperationException>(
async () => await container.EncryptPatchOperationsAsync(ops, settings, diag, CancellationToken.None));

StringAssert.Contains(ex.Message, "Increment patch operation is not allowed for encrypted path");
StringAssert.Contains(ex.Message, "/Sensitive");
}

[TestMethod]
public async Task EncryptPatchOperationsAsync_Increment_On_NonEncrypted_Path_Passes_Through()
{
// Arrange: No encrypted settings for this path
EncryptionContainer container = CreateUninitialized<EncryptionContainer>();
EncryptionSettings settings = CreateEncryptionSettingsWithEncryptedProperty("Sensitive", container); // different property than used below

PatchOperation op = PatchOperation.Increment("/Plain", 2);
List<PatchOperation> ops = new List<PatchOperation> { op };
EncryptionDiagnosticsContext diag = new EncryptionDiagnosticsContext();

// Act
List<PatchOperation> result = await container.EncryptPatchOperationsAsync(ops, settings, diag, CancellationToken.None);

// Assert: operation should be passed through unchanged
Assert.AreEqual(1, result.Count);
Assert.AreSame(op, result[0]);
Assert.AreEqual(PatchOperationType.Increment, result[0].OperationType);
}
}
}
Loading
Loading