diff --git a/Directory.Packages.props b/Directory.Packages.props index e249a871f..08bd2efc9 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -44,6 +44,7 @@ + diff --git a/all.sln b/all.sln index 9a163b1d9..425339f89 100644 --- a/all.sln +++ b/all.sln @@ -155,6 +155,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JobsSample", "examples\Jobs EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Workflow.Test", "test\Dapr.Workflow.Test\Dapr.Workflow.Test.csproj", "{E90114C6-86FC-43B8-AE5C-D9273CF21FE4}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Cryptography", "src\Dapr.Cryptography\Dapr.Cryptography.csproj", "{7628358D-FE3B-42F2-BE10-D0A6F7C03242}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Cryptography.Test", "test\Dapr.Cryptography.Test\Dapr.Cryptography.Test.csproj", "{0637289C-D284-417C-8032-182E31C5ACA5}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Cryptography", "Cryptography", "{0F81BB97-07B1-4186-992C-31EB42D06383}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CryptographySample", "examples\Cryptography\CryptographySample\CryptographySample.csproj", "{640E587A-43C6-4ADC-BC60-374474AFDA64}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -403,6 +411,18 @@ Global {E90114C6-86FC-43B8-AE5C-D9273CF21FE4}.Debug|Any CPU.Build.0 = Debug|Any CPU {E90114C6-86FC-43B8-AE5C-D9273CF21FE4}.Release|Any CPU.ActiveCfg = Release|Any CPU {E90114C6-86FC-43B8-AE5C-D9273CF21FE4}.Release|Any CPU.Build.0 = Release|Any CPU + {7628358D-FE3B-42F2-BE10-D0A6F7C03242}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7628358D-FE3B-42F2-BE10-D0A6F7C03242}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7628358D-FE3B-42F2-BE10-D0A6F7C03242}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7628358D-FE3B-42F2-BE10-D0A6F7C03242}.Release|Any CPU.Build.0 = Release|Any CPU + {0637289C-D284-417C-8032-182E31C5ACA5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0637289C-D284-417C-8032-182E31C5ACA5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0637289C-D284-417C-8032-182E31C5ACA5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0637289C-D284-417C-8032-182E31C5ACA5}.Release|Any CPU.Build.0 = Release|Any CPU + {640E587A-43C6-4ADC-BC60-374474AFDA64}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {640E587A-43C6-4ADC-BC60-374474AFDA64}.Debug|Any CPU.Build.0 = Debug|Any CPU + {640E587A-43C6-4ADC-BC60-374474AFDA64}.Release|Any CPU.ActiveCfg = Release|Any CPU + {640E587A-43C6-4ADC-BC60-374474AFDA64}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -477,6 +497,10 @@ Global {D9697361-232F-465D-A136-4561E0E88488} = {D687DDC4-66C5-4667-9E3A-FD8B78ECAA78} {9CAF360E-5AD3-4C4F-89A0-327EEB70D673} = {D9697361-232F-465D-A136-4561E0E88488} {E90114C6-86FC-43B8-AE5C-D9273CF21FE4} = {DD020B34-460F-455F-8D17-CF4A949F100B} + {7628358D-FE3B-42F2-BE10-D0A6F7C03242} = {27C5D71D-0721-4221-9286-B94AB07B58CF} + {0637289C-D284-417C-8032-182E31C5ACA5} = {DD020B34-460F-455F-8D17-CF4A949F100B} + {0F81BB97-07B1-4186-992C-31EB42D06383} = {D687DDC4-66C5-4667-9E3A-FD8B78ECAA78} + {640E587A-43C6-4ADC-BC60-374474AFDA64} = {0F81BB97-07B1-4186-992C-31EB42D06383} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {65220BF2-EAE1-4CB2-AA58-EBE80768CB40} diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-cryptography/_index.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-cryptography/_index.md new file mode 100644 index 000000000..82bb9e214 --- /dev/null +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-cryptography/_index.md @@ -0,0 +1,13 @@ +--- +type: docs +title: "Dapr Jobs .NET SDK" +linkTitle: "Jobs" +weight: 70000 +description: Get up and running with Dapr Jobs and the Dapr .NET SDK +--- + +With the Dapr Job package, you can interact with the Dapr Cryptography APIs from a .NET application to trigger future operations +to run according to a predefined schedule with an optional payload. + +To get started, walk through the [Dapr Cryptography]({{< ref dotnet-cryptography-howto.md >}}) how-to guide and refer to +[best practices documentation]({{< ref dotnet-cryptographyclient-usage.md >}}) for additional guidance. diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-cryptography/dotnet-cryptography-howto.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-cryptography/dotnet-cryptography-howto.md new file mode 100644 index 000000000..92793ba18 --- /dev/null +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-cryptography/dotnet-cryptography-howto.md @@ -0,0 +1,208 @@ +--- +type: docs +title: "How to: Author and manage Dapr Cryptography operations in the .NET SDK" +linkTitle: "How to: Author & manage cryptography operations" +weight: 71000 +description: Learn how to author and manage Dapr Cryptography operations using the .NET SDK +--- + +Let's encrypt some data and subsequently decrypt this information using the Cryptography capabilities of +the Dapr .NET SDK. We'll use the [simple example provided here](), for the following demonstration and walk through +it as an explainer of how you can encrypt and decrypt arbitrary byte arrays or streams of data. In this guide, you will: + +- Deploy a .NET Web API application ([CryptographSample]()) +- Utilize the Dapr .NET Cryptography SDK to encrypt and decrypt a payload + +In the .NET example project: +- The main [`Program.cs`]() file comprises the entirety of this demonstration. + +## Prerequisites +- [Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/) +- [Initialized Dapr environment](https://docs.dapr.io/getting-started/install-dapr-selfhost) +- [.NET 8](https://dotnet.microsoft.com/download/dotnet/8.0) or [.NET 9](https://dotnet.microsoft.com/download/dotnet/9.0) installed +- [Dapr.Cryptography](https://www.nuget.org/packages/Dapr.Cryptography) NuGet package installed to your project + +## Set up the environment +Clone the [.NET SDK repo](https://github.com/dapr/dotnet-sdk). + +```sh +git clone https://github.com/dapr/dotnet-sdk.git +``` + +From the .NET SDK root directory, navigate to the Dapr Cryptography example. + +```sh +cd examples/Cryptography +``` + +## Run the application locally + +To run the Dapr application, you need to start the .NET program and a Dapr sidecar. Navigate to the `CryptographySample` directory. + +```sh +cd CryptographySample +``` + +We'll run a command that starts both the Dapr sidecar and the .NET program at the same time. + +```sh +dapr run --app-id cryptoapp --dapr-grpc-port 4001 --dapr-http-port 3500 -- dotnet run +``` + +> Dapr listens for HTTP requests at `http://localhost:3500` and internal Jobs gRPC requests at `http://localhost:4001`. + +## Register the Dapr Encryption client with dependency injection +The Dapr Cryptography SDK provides an extension method to simplify the registration of the Dapr encryption client. +Before completing the dependency injection registration in `Program.cs`, add the following line: + +```cs +var builder = WebApplication.CreateBuilder(args); + +//Add anywhere between these two lines +builder.Services.AddDaprEncryptionClient(); + +var app = builder.Build(); +``` + +It's possible that you may want to provide some configuration options to the Dapr encryption client that +should be present with each call to the sidecar such as a Dapr API token, or you want to use a non-standard +HTTP or gRPC endpoint. This is possible through use of an overload of the registration method that allows +configuration of a `DaprEncryptionClientBuilder` instance: + +```cs +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddDaprEncryptionClient((_, daprEncryptionClientBuilder) => +{ + daprEncryptionClientBuilder.UseDaprApiToken("abc123"); + daprEncryptionClientBuilder.UseHttpEndpoint("http://localhost:8512"); //Non-standard sidecar HTTP endpoint +}); + +var app = builder.Build(); +``` + +Still, it's possible that whatever values you wish to inject need to be retrieved from some other source, itself +registered as a dependency. There's one more overload you can use to inject an `IServiceProvider` into the +configuration action method. In the following example, we register a fictional singleton that can retrieve +secrets from somewhere and pass it into the configuration method for `AddDaprEncryptionClient` so +we can retrieve our Dapr API token from somewhere else for registration here: + +```cs +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddSingleton(); +builder.Services.AddDaprJobsClient((serviceProvider, daprEncryptionClientBuilder) => +{ + var secretRetriever = serviceProvider.GetRequiredService(); + var daprApiToken = secretRetriever.GetSecret("DaprApiToken").Value; + daprJobsClientBuilder.UseDaprApiToken(daprApiToken); + + daprJobsClientBuilder.UseHttpEndpoint("http://localhost:8512"); +}); + +var app = builder.Build(); +``` + +## Use the Dapr Encryption client using IConfiguration +It's possible to configure the Dapr Encryption client using the values in your registered `IConfiguration` as well without +explicitly specifying each of the value overrides using the `DaprEncryptionlientBuilder` as demonstrated in the previous +section. Rather, by populating an `IConfiguration` made available through dependency injection the `AddDaprEncryptionClient()` +registration will automatically use these values over their respective defaults. + +Start by populating the values in your configuration. This can be done in several different ways as demonstrated below. + +### Configuration via `ConfigurationBuilder` +Application settings can be configured without using a configuration source and by instead populating the value in-memory +using a `ConfigurationBuilder` instance: + +```csharp +var builder = WebApplication.CreateBuilder(); + +//Create the configuration +var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary { + { "DAPR_HTTP_ENDPOINT", "http://localhost:54321" }, + { "DAPR_API_TOKEN", "abc123" } + }) + .Build(); + +builder.Configuration.AddConfiguration(configuration); +builder.Services.AddDaprEncryptionClient(); //This will automatically populate the HTTP endpoint and API token values from the IConfiguration +``` + +### Configuration via Environment Variables +Application settings can be accessed from environment variables available to your application. + +The following environment variables will be used to populate both the HTTP endpoint and API token used to register the +Dapr Jobs client. + +| Key | Value | +| --- | --- | +| DAPR_HTTP_ENDPOINT | http://localhost:54321 | +| DAPR_API_TOKEN | abc123 | + +```csharp +var builder = WebApplication.CreateBuilder(); + +builder.Configuration.AddEnvironmentVariables(); +builder.Services.AddDaprEncryptionClient(); +``` + +The Dapr Encryption client will be configured to use both the HTTP endpoint `http://localhost:54321` and populate all outbound +requests with the API token header `abc123`. + +### Configuration via prefixed Environment Variables + +However, in shared-host scenarios where there are multiple applications all running on the same machine without using +containers or in development environments, it's not uncommon to prefix environment variables. The following example +assumes that both the HTTP endpoint and the API token will be pulled from environment variables prefixed with the +value "myapp_". The two environment variables used in this scenario are as follows: + +| Key | Value | +| --- | --- | +| myapp_DAPR_HTTP_ENDPOINT | http://localhost:54321 | +| myapp_DAPR_API_TOKEN | abc123 | + +These environment variables will be loaded into the registered configuration in the following example and made available +without the prefix attached. + +```csharp +var builder = WebApplication.CreateBuilder(); + +builder.Configuration.AddEnvironmentVariables(prefix: "myapp_"); +builder.Services.AddDaprEncryptionClient(); +``` + +The Dapr Jobs client will be configured to use both the HTTP endpoint `http://localhost:54321` and populate all outbound +requests with the API token header `abc123`. + +## Use the Dapr Encryption client without relying on dependency injection +While the use of dependency injection simplifies the use of complex types in .NET and makes it easier to +deal with complicated configurations, you're not required to register the `DaprEncryptionClient` in this way. Rather, +you can also elect to create an instance of it from a `DaprEncryptionClientBuilder` instance as demonstrated below: + +```cs + +public class MySampleClass +{ + public void DoSomething() + { + var daprEncryptionClientBuilder = new DaprEncryptionClientBuilder(); + var daprEncryptionClient = daprEncryptionClientBuilder.Build(); + + //Do something with the `daprEncryptionClient` + } +} +``` + +## Encrypt a byte-array payload + + +## Encrypt a stream-based payload + + +## Decrypt a payload from a byte array + + +## Decrypt a stream-based payload + diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-cryptography/dotnet-cryptographyclient-usage.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-cryptography/dotnet-cryptographyclient-usage.md new file mode 100644 index 000000000..e69de29bb diff --git a/examples/Client/PublishSubscribe/StreamingSubscriptionExample/Program.cs b/examples/Client/PublishSubscribe/StreamingSubscriptionExample/Program.cs index ac00d8798..77eebe92e 100644 --- a/examples/Client/PublishSubscribe/StreamingSubscriptionExample/Program.cs +++ b/examples/Client/PublishSubscribe/StreamingSubscriptionExample/Program.cs @@ -6,6 +6,20 @@ builder.Services.AddDaprPubSubClient(); var app = builder.Build(); +var messagingClient = app.Services.GetRequiredService(); + +//Create a dynamic streaming subscription and subscribe with a timeout of 30 seconds and 10 seconds for message handling +var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(30)); +var subscription = await messagingClient.SubscribeAsync("pubsub", "myTopic", + new DaprSubscriptionOptions(new MessageHandlingPolicy(TimeSpan.FromSeconds(10), TopicResponseAction.Retry)), + HandleMessageAsync, cancellationTokenSource.Token); + +await Task.Delay(TimeSpan.FromMinutes(1)); + +//When you're done with the subscription, simply dispose of it +await subscription.DisposeAsync(); +return; + //Process each message returned from the subscription Task HandleMessageAsync(TopicMessage message, CancellationToken cancellationToken = default) { @@ -20,16 +34,3 @@ Task HandleMessageAsync(TopicMessage message, CancellationT return Task.FromResult(TopicResponseAction.Retry); } } - -var messagingClient = app.Services.GetRequiredService(); - -//Create a dynamic streaming subscription and subscribe with a timeout of 30 seconds and 10 seconds for message handling -var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(30)); -var subscription = await messagingClient.SubscribeAsync("pubsub", "myTopic", - new DaprSubscriptionOptions(new MessageHandlingPolicy(TimeSpan.FromSeconds(10), TopicResponseAction.Retry)), - HandleMessageAsync, cancellationTokenSource.Token); - -await Task.Delay(TimeSpan.FromMinutes(1)); - -//When you're done with the subscription, simply dispose of it -await subscription.DisposeAsync(); diff --git a/examples/Cryptography/CryptographySample/CryptographySample.csproj b/examples/Cryptography/CryptographySample/CryptographySample.csproj new file mode 100644 index 000000000..cbb758a11 --- /dev/null +++ b/examples/Cryptography/CryptographySample/CryptographySample.csproj @@ -0,0 +1,12 @@ + + + + enable + enable + + + + + + + diff --git a/examples/Cryptography/CryptographySample/EncryptionOperation.cs b/examples/Cryptography/CryptographySample/EncryptionOperation.cs new file mode 100644 index 000000000..d69caaf4e --- /dev/null +++ b/examples/Cryptography/CryptographySample/EncryptionOperation.cs @@ -0,0 +1,128 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 The Dapr Authors +// 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.Security.Cryptography; +using Dapr.Cryptography.Encryption; +#pragma warning disable CS0618 // Type or member is obsolete + +namespace CryptographySample; + +public sealed class EncryptionOperation(ILogger logger, DaprEncryptionClient encryptionClient) : IHostedService +{ + private const string VaultComponentName = "myvault"; + private const string KeyName = "samplekey"; + + /// + /// Triggered when the application host is ready to start the service. + /// + /// Indicates that the start process has been aborted. + /// A that represents the asynchronous Start operation. + public async Task StartAsync(CancellationToken cancellationToken) + { + //Download a medium file to use for this demonstration + var (filePath, checksum) = await DownloadMediumFileAsync(); + logger.LogInformation("Original checksum: {checksum}", checksum); + + //Load and encrypt the file + var encryptedFilePath = await EncryptFileAsync(filePath, cancellationToken); + + //Get the checksum of the encrypted file + var encryptedChecksum = await CalculateChecksum(encryptedFilePath); + logger.LogInformation("Encrypted checksum: {checksum}", encryptedChecksum); + + //Decrypt the file + var decryptedFilePath = await DecryptFileAsync(encryptedFilePath, cancellationToken); + + //Get the decrypted file's checksum + var decryptedChecksum = await CalculateChecksum(decryptedFilePath); + logger.LogInformation("Decrypted checksum: {checksum}", decryptedChecksum); + + logger.LogInformation("Original and decrypted checksums {evaluationResult} a match!", + (string.Equals(checksum, decryptedChecksum) ? "are" : "are NOT")); + } + + private async Task EncryptFileAsync(string filePath, CancellationToken cancellationToken) + { + await using var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.None); + var encryptedBytes = await encryptionClient.EncryptAsync(VaultComponentName, fileStream, KeyName, + new EncryptionOptions(KeyWrapAlgorithm.Rsa), cancellationToken); + + //Write the encrypted file to the file system + const string encryptedFileName = "enc.file"; + await using var encryptedFileStream = new FileStream(encryptedFileName, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: 1024 * 4, useAsync: true); + await foreach (var memory in encryptedBytes) + { + await encryptedFileStream.WriteAsync(memory, cancellationToken); + } + + return encryptedFileStream.Name; + } + + private async Task DecryptFileAsync( + string filePath, + CancellationToken cancellationToken) + { + await using var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.None); + var decryptedBytes = await encryptionClient.DecryptAsync(VaultComponentName, fileStream, KeyName, + cancellationToken: cancellationToken); + + //Write the decrypted file to the file system + const string decryptedFileName = "dec.file"; + await using var decryptedFileStream = new FileStream(decryptedFileName, FileMode.Create, FileAccess.Write, + FileShare.None, bufferSize: 1024 * 4, useAsync: true); + await foreach (var memory in decryptedBytes) + { + await decryptedFileStream.WriteAsync(memory, cancellationToken); + } + + return decryptedFileStream.Name; + } + + /// + /// Triggered when the application host is performing a graceful shutdown. + /// + /// Indicates that the shutdown process should no longer be graceful. + /// A that represents the asynchronous Stop operation. + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + /// + /// Downloads ZIP file of Dapr Docs repository - ~50MB. + /// + /// + private static async Task<(string filePath, string checksum)> DownloadMediumFileAsync() + { + const string fileName = "mediumFile.zip"; + using var httpClient = new HttpClient(); + var response = + await httpClient.GetStreamAsync("https://github.com/dapr/docs/archive/refs/heads/master.zip"); + await using var fileStream = new FileStream(fileName, FileMode.Create, FileAccess.Write, FileShare.None); + await response.CopyToAsync(fileStream); + var checksum = await CalculateChecksum(fileStream.Name); + return (fileStream.Name, checksum); + } + + /// + /// Calculates a checksum for a given file given its path. + /// + /// The path of the file to evaluate. + /// + private static async Task CalculateChecksum(string filePath) + { + await using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read); + using var sha256 = SHA256.Create(); + var hash = await sha256.ComputeHashAsync(fs); + return BitConverter.ToString(hash).Replace("-", string.Empty).ToLowerInvariant(); + } +} + + diff --git a/examples/Cryptography/CryptographySample/Program.cs b/examples/Cryptography/CryptographySample/Program.cs new file mode 100644 index 000000000..ddcf72808 --- /dev/null +++ b/examples/Cryptography/CryptographySample/Program.cs @@ -0,0 +1,28 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 The Dapr Authors +// 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.Security.Cryptography; +using CryptographySample; +using Dapr.Cryptography.Encryption.Extensions; + +#pragma warning disable CS0618 // Type or member is obsolete + +var builder = WebApplication.CreateBuilder(args); +builder.Services.AddHostedService(); +builder.Services.AddDaprEncryptionClient(); +builder.Logging.ClearProviders(); +builder.Logging.AddConsole(); + +var app = builder.Build(); + +await app.RunAsync(); diff --git a/examples/Cryptography/CryptographySample/Properties/launchSettings.json b/examples/Cryptography/CryptographySample/Properties/launchSettings.json new file mode 100644 index 000000000..8616cebc7 --- /dev/null +++ b/examples/Cryptography/CryptographySample/Properties/launchSettings.json @@ -0,0 +1,38 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:48832", + "sslPort": 44391 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5279", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7204;http://localhost:5279", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/examples/Cryptography/CryptographySample/appsettings.Development.json b/examples/Cryptography/CryptographySample/appsettings.Development.json new file mode 100644 index 000000000..0c208ae91 --- /dev/null +++ b/examples/Cryptography/CryptographySample/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/examples/Cryptography/CryptographySample/appsettings.json b/examples/Cryptography/CryptographySample/appsettings.json new file mode 100644 index 000000000..10f68b8c8 --- /dev/null +++ b/examples/Cryptography/CryptographySample/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/src/Dapr.Client/DaprClientGrpc.cs b/src/Dapr.Client/DaprClientGrpc.cs index 394b313e2..3e5900333 100644 --- a/src/Dapr.Client/DaprClientGrpc.cs +++ b/src/Dapr.Client/DaprClientGrpc.cs @@ -11,8 +11,6 @@ // limitations under the License. // ------------------------------------------------------------------------ -using Dapr.Common.Extensions; - namespace Dapr.Client; using System; @@ -23,7 +21,6 @@ namespace Dapr.Client; using System.Net.Http; using System.Net.Http.Json; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -1664,8 +1661,7 @@ public override async Task UnsubscribeConfigur #region Cryptography /// - [Obsolete( - "The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + [Obsolete("Use `EncryptAsync` from the `Dapr.Configuration` NuGet package instead as this method will be removed from `Dapr.Client` with the release of v1.17")] public override async Task> EncryptAsync(string vaultResourceName, ReadOnlyMemory plaintextBytes, string keyName, EncryptionOptions encryptionOptions, CancellationToken cancellationToken = default) @@ -1685,11 +1681,13 @@ public override async Task> EncryptAsync(string vaultResour } /// - [Obsolete( - "The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public override async Task>> EncryptAsync(string vaultResourceName, + [Obsolete("Use `EncryptAsync` from the `Dapr.Configuration` NuGet package instead as this method will be removed from `Dapr.Client` with the release of v1.17")] + public override async Task>> EncryptAsync( + string vaultResourceName, Stream plaintextStream, - string keyName, EncryptionOptions encryptionOptions, CancellationToken cancellationToken = default) + string keyName, + EncryptionOptions encryptionOptions, + CancellationToken cancellationToken = default) { ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); @@ -1718,53 +1716,60 @@ public override async Task>> EncryptAsync( var options = CreateCallOptions(headers: null, cancellationToken); var duplexStream = client.EncryptAlpha1(options); - + //Run both operations at the same time, but return the output of the streaming values coming from the operation - var receiveResult = Task.FromResult(RetrieveEncryptedStreamAsync(duplexStream, cancellationToken)); - return await Task.WhenAll( - //Stream the plaintext data to the sidecar in chunks - SendPlaintextStreamAsync(plaintextStream, encryptionOptions.StreamingBlockSizeInBytes, - duplexStream, encryptRequestOptions, cancellationToken), - //At the same time, retrieve the encrypted response from the sidecar - receiveResult).ContinueWith(_ => receiveResult.Result, cancellationToken); + var sendTask = Task.Run(() => SendPlaintextStreamAsync(plaintextStream, + encryptionOptions.StreamingBlockSizeInBytes, + duplexStream, encryptRequestOptions, cancellationToken), cancellationToken); + var receiveTask = Task.Run(() => + RetrieveEncryptedStreamAsync(duplexStream, cancellationToken), cancellationToken); + + await Task.WhenAll(sendTask, receiveTask).ConfigureAwait(false); + return receiveTask.Result; } /// /// Sends the plaintext bytes in chunks to the sidecar to be encrypted. /// - private async Task SendPlaintextStreamAsync(Stream plaintextStream, + private static async Task SendPlaintextStreamAsync( + Stream plaintextStream, int streamingBlockSizeInBytes, AsyncDuplexStreamingCall duplexStream, Autogenerated.EncryptRequestOptions encryptRequestOptions, CancellationToken cancellationToken) { //Start with passing the metadata about the encryption request itself in the first message - await duplexStream.RequestStream.WriteAsync( - new Autogenerated.EncryptRequest { Options = encryptRequestOptions }, cancellationToken); + await duplexStream.RequestStream.WriteAsync(new Autogenerated.EncryptRequest { Options = encryptRequestOptions }, cancellationToken); - //Send the plaintext bytes in blocks in subsequent messages + //Send the ciphertext bytes in blocks in subsequent messages await using (var bufferedStream = new BufferedStream(plaintextStream, streamingBlockSizeInBytes)) { - var buffer = new byte[streamingBlockSizeInBytes]; - int bytesRead; - ulong sequenceNumber = 0; - - while ((bytesRead = - await bufferedStream.ReadAsync(buffer.AsMemory(0, streamingBlockSizeInBytes), - cancellationToken)) != - 0) + var buffer = ArrayPool.Shared.Rent(streamingBlockSizeInBytes); + try { - await duplexStream.RequestStream.WriteAsync( - new Autogenerated.EncryptRequest - { - Payload = new Autogenerated.StreamPayload - { - Data = ByteString.CopyFrom(buffer, 0, bytesRead), Seq = sequenceNumber - } - }, cancellationToken); + int bytesRead; + ulong sequenceNumber = 0; - //Increment the sequence number - sequenceNumber++; + while ((bytesRead = + await bufferedStream.ReadAsync(buffer.AsMemory(0, streamingBlockSizeInBytes), + cancellationToken)) != 0) + { + await duplexStream.RequestStream.WriteAsync( + new Autogenerated.EncryptRequest + { + Payload = new Autogenerated.StreamPayload + { + Data = ByteString.CopyFrom(buffer, 0, bytesRead), Seq = sequenceNumber + } + }, cancellationToken); + + //Increment the sequence number + sequenceNumber++; + } + } + finally + { + ArrayPool.Shared.Return(buffer); } } @@ -1775,7 +1780,7 @@ await duplexStream.RequestStream.WriteAsync( /// /// Retrieves the encrypted bytes from the encryption operation on the sidecar and returns as an enumerable stream. /// - private async IAsyncEnumerable> RetrieveEncryptedStreamAsync( + private static async IAsyncEnumerable> RetrieveEncryptedStreamAsync( AsyncDuplexStreamingCall duplexStream, [EnumeratorCancellation] CancellationToken cancellationToken) { @@ -1787,11 +1792,13 @@ private async IAsyncEnumerable> RetrieveEncryptedStreamAsyn } /// - [Obsolete( - "The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] - public override async Task>> DecryptAsync(string vaultResourceName, - Stream ciphertextStream, string keyName, - DecryptionOptions decryptionOptions, CancellationToken cancellationToken = default) + [Obsolete("Use `DecryptAsync` from the `Dapr.Configuration` NuGet package instead as this method will be removed from `Dapr.Client` with the release of v1.17")] + public override async Task>> DecryptAsync( + string vaultResourceName, + Stream ciphertextStream, + string keyName, + DecryptionOptions decryptionOptions, + CancellationToken cancellationToken = default) { ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); @@ -1807,20 +1814,18 @@ public override async Task>> DecryptAsync( var duplexStream = client.DecryptAlpha1(options); //Run both operations at the same time, but return the output of the streaming values coming from the operation - var receiveResult = Task.FromResult(RetrieveDecryptedStreamAsync(duplexStream, cancellationToken)); - return await Task.WhenAll( - //Stream the ciphertext data to the sidecar in chunks - SendCiphertextStreamAsync(ciphertextStream, decryptionOptions.StreamingBlockSizeInBytes, - duplexStream, decryptRequestOptions, cancellationToken), - //At the same time, retrieve the decrypted response from the sidecar - receiveResult) - //Return only the result of the `RetrieveEncryptedStreamAsync` method - .ContinueWith(t => receiveResult.Result, cancellationToken); + var sendTask = Task.Run(() => SendCiphertextStreamAsync(ciphertextStream, + decryptionOptions.StreamingBlockSizeInBytes, duplexStream, decryptRequestOptions, cancellationToken), + cancellationToken); + var receiveTask = Task.Run(() => + RetrieveDecryptedStreamAsync(duplexStream, cancellationToken), cancellationToken); + + await Task.WhenAll(sendTask, receiveTask).ConfigureAwait(false); + return receiveTask.Result; } /// - [Obsolete( - "The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + [Obsolete("Use `DecryptAsync` from the `Dapr.Configuration` NuGet package instead as this method will be removed from `Dapr.Client` with the release of v1.17")] public override Task>> DecryptAsync(string vaultResourceName, Stream ciphertextStream, string keyName, CancellationToken cancellationToken = default) => DecryptAsync(vaultResourceName, ciphertextStream, keyName, new DecryptionOptions(), @@ -1829,41 +1834,51 @@ public override Task>> DecryptAsync(string /// /// Sends the ciphertext bytes in chunks to the sidecar to be decrypted. /// - private async Task SendCiphertextStreamAsync(Stream ciphertextStream, + private static async Task SendCiphertextStreamAsync(Stream ciphertextStream, int streamingBlockSizeInBytes, AsyncDuplexStreamingCall duplexStream, Autogenerated.DecryptRequestOptions decryptRequestOptions, CancellationToken cancellationToken) { //Start with passing the metadata about the decryption request itself in the first message - await duplexStream.RequestStream.WriteAsync( - new Autogenerated.DecryptRequest { Options = decryptRequestOptions }, cancellationToken); - + var request = new Autogenerated.DecryptRequest(); + if (decryptRequestOptions is not null) + { + request.Options = decryptRequestOptions; + } + await duplexStream.RequestStream.WriteAsync(request, cancellationToken); + //Send the ciphertext bytes in blocks in subsequent messages await using (var bufferedStream = new BufferedStream(ciphertextStream, streamingBlockSizeInBytes)) { - var buffer = new byte[streamingBlockSizeInBytes]; - int bytesRead; - ulong sequenceNumber = 0; - - while ((bytesRead = - await bufferedStream.ReadAsync(buffer.AsMemory(0, streamingBlockSizeInBytes), - cancellationToken)) != 0) + var buffer = ArrayPool.Shared.Rent(streamingBlockSizeInBytes); + try { - await duplexStream.RequestStream.WriteAsync( - new Autogenerated.DecryptRequest - { - Payload = new Autogenerated.StreamPayload - { - Data = ByteString.CopyFrom(buffer, 0, bytesRead), Seq = sequenceNumber - } - }, cancellationToken); + int bytesRead; + ulong sequenceNumber = 0; - //Increment the sequence number - sequenceNumber++; + while ((bytesRead = await bufferedStream.ReadAsync(buffer.AsMemory(0, streamingBlockSizeInBytes), + cancellationToken: cancellationToken)) != 0) + { + await duplexStream.RequestStream.WriteAsync( + new Autogenerated.DecryptRequest + { + Payload = new Autogenerated.StreamPayload + { + Data = ByteString.CopyFrom(buffer, 0, bytesRead), Seq = sequenceNumber + } + }, cancellationToken); + + //Increment the sequence number + sequenceNumber++; + } + } + finally + { + ArrayPool.Shared.Return(buffer); } } - + //Send the completion message await duplexStream.RequestStream.CompleteAsync(); } @@ -1871,7 +1886,7 @@ await duplexStream.RequestStream.WriteAsync( /// /// Retrieves the decrypted bytes from the decryption operation on the sidecar and returns as an enumerable stream. /// - private async IAsyncEnumerable> RetrieveDecryptedStreamAsync( + private static async IAsyncEnumerable> RetrieveDecryptedStreamAsync( AsyncDuplexStreamingCall duplexStream, [EnumeratorCancellation] CancellationToken cancellationToken) { @@ -1883,8 +1898,7 @@ private async IAsyncEnumerable> RetrieveDecryptedStreamAsyn } /// - [Obsolete( - "The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + [Obsolete("Use `DecryptAsync` from the `Dapr.Configuration` NuGet package instead as this method will be removed from `Dapr.Client` with the release of v1.17")] public override async Task> DecryptAsync(string vaultResourceName, ReadOnlyMemory ciphertextBytes, string keyName, DecryptionOptions decryptionOptions, CancellationToken cancellationToken = default) @@ -1904,8 +1918,7 @@ public override async Task> DecryptAsync(string vaultResour } /// - [Obsolete( - "The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + [Obsolete("Use `DecryptAsync` from the `Dapr.Configuration` NuGet package instead as this method will be removed from `Dapr.Client` with the release of v1.17")] public override async Task> DecryptAsync(string vaultResourceName, ReadOnlyMemory ciphertextBytes, string keyName, CancellationToken cancellationToken = default) => await DecryptAsync(vaultResourceName, ciphertextBytes, keyName, diff --git a/src/Dapr.Common/AssemblyInfo.cs b/src/Dapr.Common/AssemblyInfo.cs index 3037485a9..d0698cbbd 100644 --- a/src/Dapr.Common/AssemblyInfo.cs +++ b/src/Dapr.Common/AssemblyInfo.cs @@ -19,6 +19,7 @@ [assembly: InternalsVisibleTo("Dapr.AI, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.AspNetCore, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.Client, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.Cryptography, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.Jobs, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.Messaging, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.Extensions.Configuration, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] @@ -34,6 +35,7 @@ [assembly: InternalsVisibleTo("Dapr.AspNetCore.IntegrationTest.App, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.AspNetCore.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.Client.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.Crytography.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.Common.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.E2E.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] [assembly: InternalsVisibleTo("Dapr.E2E.Test.Actors, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] diff --git a/src/Dapr.Cryptography/AssemblyInfo.cs b/src/Dapr.Cryptography/AssemblyInfo.cs new file mode 100644 index 000000000..4294b4d4f --- /dev/null +++ b/src/Dapr.Cryptography/AssemblyInfo.cs @@ -0,0 +1,16 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// 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.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Dapr.Cryptography.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] diff --git a/src/Dapr.Cryptography/Dapr.Cryptography.csproj b/src/Dapr.Cryptography/Dapr.Cryptography.csproj new file mode 100644 index 000000000..e2f3fdc76 --- /dev/null +++ b/src/Dapr.Cryptography/Dapr.Cryptography.csproj @@ -0,0 +1,25 @@ + + + + enable + enable + This package contains the reference assemblies for developing services using Cryptography using Dapr. + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + diff --git a/src/Dapr.Cryptography/Encryption/DaprDecryptionProcessor.cs b/src/Dapr.Cryptography/Encryption/DaprDecryptionProcessor.cs new file mode 100644 index 000000000..9b3319a0f --- /dev/null +++ b/src/Dapr.Cryptography/Encryption/DaprDecryptionProcessor.cs @@ -0,0 +1,95 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 The Dapr Authors +// 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.Buffers; +using System.Runtime.CompilerServices; +using Google.Protobuf; +using Grpc.Core; +using Autogenerated = Dapr.Client.Autogen.Grpc.v1; + +namespace Dapr.Cryptography.Encryption; + +/// +/// Processor for handling Dapr decryption pipeline-based operations. +/// +internal sealed class DaprDecryptionProcessor : DaprEncryptionPipelineProcessorBase +{ + /// + /// Sends the stream from the SDK to the Dapr sidecar. + /// + /// The stream containing the data to be processed. + /// The size of the blocks to be read from the stream. + /// The duplex stream used for sending and receiving data. + /// The options for the request. + /// A token to monitor for cancellation requests. + /// A task representing the asynchronous operation. + protected override async Task SendStreamAsync( + Stream stream, + int blockSize, + AsyncDuplexStreamingCall duplexStream, + Autogenerated.DecryptRequestOptions? options, + CancellationToken cancellationToken) + { + await duplexStream.RequestStream.WriteAsync(new Autogenerated.DecryptRequest { Options = options }, + cancellationToken); + + await using (var bufferedStream = new BufferedStream(stream, blockSize)) + { + var buffer = ArrayPool.Shared.Rent(blockSize); + try + { + int bytesRead; + ulong sequenceNumber = 0; + + while ((bytesRead = await bufferedStream.ReadAsync(buffer.AsMemory(0, blockSize), cancellationToken)) != + 0) + { + await duplexStream.RequestStream.WriteAsync( + new Autogenerated.DecryptRequest + { + Payload = new Autogenerated.StreamPayload + { + Data = ByteString.CopyFrom(buffer, 0, bytesRead), Seq = sequenceNumber + } + }, cancellationToken); + + //Increment the sequence number + sequenceNumber++; + } + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + await duplexStream.RequestStream.CompleteAsync(); + } + + /// + /// Retrieves the processed stream data from the Dapr sidecar. + /// + /// The duplex stream used for receiving data. + /// A token to monitor for cancellation requests. + /// An asynchronous enumerable of the processed data. + protected override async IAsyncEnumerable> RetrieveStreamAsync( + AsyncDuplexStreamingCall duplexStream, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + await foreach (var encryptResponse in duplexStream.ResponseStream.ReadAllAsync(cancellationToken) + .ConfigureAwait(false)) + { + yield return encryptResponse.Payload.Data.Memory; + } + } +} diff --git a/src/Dapr.Cryptography/Encryption/DaprEncryptionClient.cs b/src/Dapr.Cryptography/Encryption/DaprEncryptionClient.cs new file mode 100644 index 000000000..7be80c71f --- /dev/null +++ b/src/Dapr.Cryptography/Encryption/DaprEncryptionClient.cs @@ -0,0 +1,112 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 The Dapr Authors +// 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. +// ------------------------------------------------------------------------ + +namespace Dapr.Cryptography.Encryption; + +/// +/// The base implementation of the Dapr encryption/decryption client. +/// +public abstract class DaprEncryptionClient : IDisposable, IDaprEncryptionClient +{ + private bool disposed; + + /// + /// Encrypts an array of bytes using the Dapr Cryptography encryption functionality. + /// + /// The name of the vault resource used by the operation. + /// The bytes of the plaintext value to encrypt. + /// The name of the key to use from the Vault for the encryption operation. + /// Options informing how the encryption operation should be configured. + /// A that can be used to cancel the operation. + /// An array of encrypted bytes. + [Obsolete( + "The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public abstract Task> EncryptAsync( + string vaultResourceName, + ReadOnlyMemory plaintextBytes, + string keyName, + EncryptionOptions encryptionOptions, + CancellationToken cancellationToken = default); + + /// + /// Encrypts a stream using the Dapr Cryptography encryption functionality. + /// + /// The name of the vault resource used by the operation. + /// The stream containing the bytes of the plaintext value to encrypt. + /// The name of the key to use from the Vault for the encryption operation. + /// Options informing how the encryption operation should be configured. + /// A that can be used to cancel the operation. + /// An array of encrypted bytes. + [Obsolete( + "The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public abstract IAsyncEnumerable> EncryptAsync( + string vaultResourceName, + Stream plaintextStream, + string keyName, + EncryptionOptions encryptionOptions, + CancellationToken cancellationToken = default); + + /// + /// Decrypts the specified ciphertext bytes using the Dapr Cryptography encryption functionality. + /// + /// The name of the vault resource used by the operation. + /// The bytes of the ciphertext value to decrypt. + /// The name of the key to use from the Vault for the decryption operation. + /// Options informing how the decryption operation should be configured. + /// A that can be used to cancel the operation. + /// An array of decrypted bytes. + [Obsolete( + "The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public abstract Task> DecryptAsync( + string vaultResourceName, + ReadOnlyMemory ciphertextBytes, + string keyName, + DecryptionOptions? options = null, + CancellationToken cancellationToken = default); + + /// + /// Decrypts the specified stream of ciphertext using the Dapr Cryptography encryption functionality. + /// + /// The name of the vault resource used by the operation. + /// The stream containing the bytes of the ciphertext value to decrypt. + /// The name of the key to use from the Vault for the decryption operation. + /// Options informing how the decryption operation should be configured. + /// A that can be used to cancel the operation. + /// An asynchronously enumerable array of decrypted bytes. + [Obsolete( + "The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public abstract IAsyncEnumerable> DecryptAsync( + string vaultResourceName, + Stream ciphertextStream, + string keyName, + DecryptionOptions? options = null, + CancellationToken cancellationToken = default); + + /// + public void Dispose() + { + if (!this.disposed) + { + Dispose(disposing: true); + this.disposed = true; + } + } + + /// + /// Disposes the resources associated with the object. + /// + /// true if called by a call to the Dispose method; otherwise false. + protected virtual void Dispose(bool disposing) + { + } +} diff --git a/src/Dapr.Cryptography/Encryption/DaprEncryptionClientBuilder.cs b/src/Dapr.Cryptography/Encryption/DaprEncryptionClientBuilder.cs new file mode 100644 index 000000000..2e88e52e0 --- /dev/null +++ b/src/Dapr.Cryptography/Encryption/DaprEncryptionClientBuilder.cs @@ -0,0 +1,39 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 The Dapr Authors +// 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 Dapr.Common; +using Microsoft.Extensions.Configuration; +using Autogenerated = Dapr.Client.Autogen.Grpc.v1; + +namespace Dapr.Cryptography.Encryption; + +/// +/// Builds a . +/// +/// An optional instance of . +public sealed class DaprEncryptionClientBuilder(IConfiguration? configuration = null) : DaprGenericClientBuilder(configuration) +{ + /// + /// Builds the client instance from the properties of the builder. + /// + /// The Dapr client instance. + /// + /// Builds the client instance from the properties of the builder. + /// + public override DaprEncryptionClient Build() + { + var daprClientDependencies = this.BuildDaprClientDependencies(typeof(DaprEncryptionClient).Assembly); + var client = new Autogenerated.Dapr.DaprClient(daprClientDependencies.channel); + return new DaprEncryptionGrpcClient(client, daprClientDependencies.httpClient, daprClientDependencies.daprApiToken); + } +} diff --git a/src/Dapr.Cryptography/Encryption/DaprEncryptionGrpcClient.cs b/src/Dapr.Cryptography/Encryption/DaprEncryptionGrpcClient.cs new file mode 100644 index 000000000..bc7cc416f --- /dev/null +++ b/src/Dapr.Cryptography/Encryption/DaprEncryptionGrpcClient.cs @@ -0,0 +1,180 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 The Dapr Authors +// 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.Buffers; +using System.Runtime.CompilerServices; +using Dapr.Common; +using Dapr.Common.Extensions; +using P = Dapr.Client.Autogen.Grpc.v1.Dapr; +using Autogenerated = Dapr.Client.Autogen.Grpc.v1; + +namespace Dapr.Cryptography.Encryption; + +/// +/// A client for interacting with the Dapr cryptography encryption and decryption endpoints. +/// +internal sealed class DaprEncryptionGrpcClient(P.DaprClient client, HttpClient httpClient, string? daprApiToken) : DaprEncryptionClient +{ + /// + /// The HTTP client used by the client for calling the Dapr runtime. + /// + /// + /// Property exposed for testing purposes. + /// + internal readonly HttpClient HttpClient = httpClient; + /// + /// The Dapr API token value. + /// + /// + /// Property exposed for testing purposes. + /// + internal readonly string? DaprApiToken = daprApiToken; + + /// + [Obsolete( + "The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public override async Task> EncryptAsync( + string vaultResourceName, + ReadOnlyMemory plaintextBytes, + string keyName, + EncryptionOptions encryptionOptions, + CancellationToken cancellationToken = default) + { + using var memoryStream = new MemoryStream(plaintextBytes.Length); + await memoryStream.WriteAsync(plaintextBytes, cancellationToken); + memoryStream.Position = 0; + + var encryptionResult = EncryptAsync(vaultResourceName, memoryStream, keyName, encryptionOptions, cancellationToken); + + var bufferedResult = new ArrayBufferWriter(); + await foreach (var item in encryptionResult) + { + bufferedResult.Write(item.Span); + } + + return bufferedResult.WrittenMemory; + } + + /// + [Obsolete( + "The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public override async IAsyncEnumerable> EncryptAsync( + string vaultResourceName, + Stream plaintextStream, + string keyName, + EncryptionOptions encryptionOptions, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + ArgumentException.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); + ArgumentException.ThrowIfNullOrEmpty(keyName, nameof(keyName)); + ArgumentNullException.ThrowIfNull(plaintextStream, nameof(plaintextStream)); + ArgumentNullException.ThrowIfNull(encryptionOptions, nameof(encryptionOptions)); + + var duplexStream = client.EncryptAlpha1(DaprClientUtilities.ConfigureGrpcCallOptions( + typeof(DaprEncryptionClient).Assembly, + this.DaprApiToken, cancellationToken)); + var processor = new DaprEncryptionProcessor(); + + await foreach (var encryptedData in processor.ProcessStreamAsync( + plaintextStream, + encryptionOptions.StreamingBlockSizeInBytes, + duplexStream, + new Autogenerated.EncryptRequest + { + Options = new Autogenerated.EncryptRequestOptions + { + ComponentName = vaultResourceName, + DataEncryptionCipher = encryptionOptions.EncryptionCipher.GetValueFromEnumMember(), + KeyName = keyName, + DecryptionKeyName = encryptionOptions.DecryptionKeyName, + KeyWrapAlgorithm = encryptionOptions.KeyWrapAlgorithm.GetValueFromEnumMember(), + OmitDecryptionKeyName = + string.IsNullOrWhiteSpace(encryptionOptions.DecryptionKeyName) + } + }, + cancellationToken)) + { + yield return encryptedData; + } + } + + /// + [Obsolete( + "The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public override async Task> DecryptAsync( + string vaultResourceName, + ReadOnlyMemory ciphertextBytes, + string keyName, + DecryptionOptions? options = null, + CancellationToken cancellationToken = default) + { + using var memoryStream = new MemoryStream(ciphertextBytes.Length); + await memoryStream.WriteAsync(ciphertextBytes, cancellationToken); + memoryStream.Position = 0; + + var decryptionResult = DecryptAsync(vaultResourceName, memoryStream, keyName, options, cancellationToken); + + var bufferedResult = new ArrayBufferWriter(); + await foreach(var item in decryptionResult) + { + bufferedResult.Write(item.Span); + } + + return bufferedResult.WrittenMemory; + } + + /// + [Obsolete( + "The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public override async IAsyncEnumerable> DecryptAsync( + string vaultResourceName, + Stream ciphertextStream, + string keyName, + DecryptionOptions? options = null, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + ArgumentException.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); + ArgumentNullException.ThrowIfNull(ciphertextStream, nameof(ciphertextStream)); + ArgumentException.ThrowIfNullOrEmpty(keyName, nameof(keyName)); + + options ??= new DecryptionOptions(); + + var duplexStream = client.DecryptAlpha1(DaprClientUtilities.ConfigureGrpcCallOptions( + typeof(DaprEncryptionClient).Assembly, + this.DaprApiToken, cancellationToken)); + var processor = new DaprDecryptionProcessor(); + + await foreach (var decryptedData in processor.ProcessStreamAsync( + ciphertextStream, + options.StreamingBlockSizeInBytes, + duplexStream, + new Autogenerated.DecryptRequest + { + Options = new Autogenerated.DecryptRequestOptions + { + ComponentName = vaultResourceName, KeyName = keyName + } + }, cancellationToken)) + { + yield return decryptedData; + } + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + this.HttpClient.Dispose(); + } + } +} diff --git a/src/Dapr.Cryptography/Encryption/DaprEncryptionPipelineProcessorBase.cs b/src/Dapr.Cryptography/Encryption/DaprEncryptionPipelineProcessorBase.cs new file mode 100644 index 000000000..1af792a31 --- /dev/null +++ b/src/Dapr.Cryptography/Encryption/DaprEncryptionPipelineProcessorBase.cs @@ -0,0 +1,120 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 The Dapr Authors +// 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.Pipelines; +using System.Runtime.CompilerServices; +using Grpc.Core; + +namespace Dapr.Cryptography.Encryption; + +/// +/// Base class for process streams of data for both encryption and decryption operations. +/// +/// The type of request message presented to the Dapr sidecar for the operation. +/// The type of request message options presented to the Dapr sidecar to configure the operation. +/// The type of response message provided by the Dapr sidecar for the operation. +internal abstract class DaprEncryptionPipelineProcessorBase +{ + /// + /// Sends the stream from the SDK to the Dapr sidecar. + /// + /// The stream containing the data to be processed. + /// The size of the blocks to be read from the stream. + /// The duplex stream used for sending and receiving data. + /// The options for the request. + /// A token to monitor for cancellation requests. + /// A task representing the asynchronous operation. + protected abstract Task SendStreamAsync( + Stream stream, + int blockSize, + AsyncDuplexStreamingCall duplexStream, + TRequestOptions options, + CancellationToken cancellationToken); + + /// + /// Retrieves the processed stream data from the Dapr sidecar. + /// + /// The duplex stream used for receiving data. + /// A token to monitor for cancellation requests. + /// An asynchronous enumerable of the processed data. + protected abstract IAsyncEnumerable> RetrieveStreamAsync( + AsyncDuplexStreamingCall duplexStream, + CancellationToken cancellationToken); + + /// + /// Processes the stream by reading from the input stream and writing to the duplex stream. + /// + /// The stream containing the data to be processed. + /// The size of the blocks to be read from the stream. + /// The duplex stream used for sending and receiving data. + /// The request to the sidecar. + /// A token to monitor for cancellation requests. + /// An asynchronous enumerable of the processed data. + public async IAsyncEnumerable> ProcessStreamAsync + (Stream stream, + int blockSize, + AsyncDuplexStreamingCall duplexStream, + TRequest request, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + var pipe = new Pipe(); + var writer = FillPipeAsync(pipe.Writer, stream, blockSize, request, duplexStream, cancellationToken); + var reading = RetrieveStreamAsync(duplexStream, cancellationToken); + + var writingTask = Task.Run(() => writer, cancellationToken); + + await foreach (var data in reading) + { + yield return data; + } + + await writingTask.ConfigureAwait(false); + } + + /// + /// Fills the pipe by reading from the input stream and writing to the pipe. + /// + /// The pipe writer to write the data to. + /// The stream containing the source data to be processed. + /// The size of the blocks to be read from the input stream. + /// The input request. + /// The duplex stream used for sending encryption data. + /// A token to monitor for cancellation requests. + private static async Task FillPipeAsync( + PipeWriter writer, + Stream stream, + int blockSize, + TRequest request, + AsyncDuplexStreamingCall duplexStream, + CancellationToken cancellationToken) + { + await duplexStream.RequestStream.WriteAsync(request, cancellationToken); + + while (true) + { + var memory = writer.GetMemory(blockSize); + var bytesRead = await stream.ReadAsync(memory, cancellationToken); + if (bytesRead == 0) + break; + + writer.Advance(bytesRead); + + var result = await writer.FlushAsync(cancellationToken); + if (result.IsCompleted) + break; + } + + await writer.CompleteAsync(); + await duplexStream.RequestStream.CompleteAsync(); + } +} diff --git a/src/Dapr.Cryptography/Encryption/DaprEncryptionProcessor.cs b/src/Dapr.Cryptography/Encryption/DaprEncryptionProcessor.cs new file mode 100644 index 000000000..6245a985e --- /dev/null +++ b/src/Dapr.Cryptography/Encryption/DaprEncryptionProcessor.cs @@ -0,0 +1,95 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 The Dapr Authors +// 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.Buffers; +using System.Runtime.CompilerServices; +using Google.Protobuf; +using Grpc.Core; +using Autogenerated = Dapr.Client.Autogen.Grpc.v1; + +namespace Dapr.Cryptography.Encryption; + +/// +/// Processor for handling Dapr encryption pipeline-based operations. +/// +internal sealed class DaprEncryptionProcessor : DaprEncryptionPipelineProcessorBase +{ + /// + /// Sends the stream from the SDK to the Dapr sidecar. + /// + /// The stream containing the data to be processed. + /// The size of the blocks to be read from the stream. + /// The duplex stream used for sending and receiving data. + /// The options for the request. + /// A token to monitor for cancellation requests. + /// A task representing the asynchronous operation. + protected override async Task SendStreamAsync( + Stream stream, + int blockSize, + AsyncDuplexStreamingCall duplexStream, + Autogenerated.EncryptRequestOptions options, + CancellationToken cancellationToken) + { + await duplexStream.RequestStream.WriteAsync(new Autogenerated.EncryptRequest { Options = options }, + cancellationToken); + + await using (var bufferedStream = new BufferedStream(stream, blockSize)) + { + var buffer = ArrayPool.Shared.Rent(blockSize); + try + { + int bytesRead; + ulong sequenceNumber = 0; + + while ((bytesRead = await bufferedStream.ReadAsync(buffer.AsMemory(0, blockSize), cancellationToken)) != + 0) + { + await duplexStream.RequestStream.WriteAsync( + new Autogenerated.EncryptRequest + { + Payload = new Autogenerated.StreamPayload + { + Data = ByteString.CopyFrom(buffer, 0, bytesRead), Seq = sequenceNumber + } + }, cancellationToken); + + //Increment the sequence number + sequenceNumber++; + } + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + await duplexStream.RequestStream.CompleteAsync(); + } + + /// + /// Retrieves the processed stream data from the Dapr sidecar. + /// + /// The duplex stream used for receiving data. + /// A token to monitor for cancellation requests. + /// An asynchronous enumerable of the processed data. + protected override async IAsyncEnumerable> RetrieveStreamAsync( + AsyncDuplexStreamingCall duplexStream, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + await foreach (var encryptResponse in duplexStream.ResponseStream.ReadAllAsync(cancellationToken) + .ConfigureAwait(false)) + { + yield return encryptResponse.Payload.Data.Memory; + } + } +} diff --git a/src/Dapr.Cryptography/Encryption/DataEncryptionCipher.cs b/src/Dapr.Cryptography/Encryption/DataEncryptionCipher.cs new file mode 100644 index 000000000..2d07e5621 --- /dev/null +++ b/src/Dapr.Cryptography/Encryption/DataEncryptionCipher.cs @@ -0,0 +1,33 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 The Dapr Authors +// 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.Runtime.Serialization; + +namespace Dapr.Cryptography.Encryption; + +/// +/// The cipher used for data encryption operations. +/// +public enum DataEncryptionCipher +{ + /// + /// The default data encryption cipher used, this represents AES GCM. + /// + [EnumMember(Value = "aes-gcm")] + AesGcm, + /// + /// Represents the ChaCha20-Poly1305 data encryption cipher. + /// + [EnumMember(Value = "chacha20-poly1305")] + ChaCha20Poly1305 +} diff --git a/src/Dapr.Cryptography/Encryption/DecryptionOptions.cs b/src/Dapr.Cryptography/Encryption/DecryptionOptions.cs new file mode 100644 index 000000000..646726889 --- /dev/null +++ b/src/Dapr.Cryptography/Encryption/DecryptionOptions.cs @@ -0,0 +1,39 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 The Dapr Authors +// 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. +// ------------------------------------------------------------------------ + +namespace Dapr.Cryptography.Encryption; + +/// +/// A collection of options used to configure how decryption cryptographic operations are performed. +/// +public sealed class DecryptionOptions +{ + private int streamingBlockSizeInBytes = 4 * 1024; // 4KB + + /// + /// The size of the block in bytes used to send data to the sidecar for cryptography operations. + /// + /// Thrown if a number less than or equal to zero + /// is used for the block size. + public int StreamingBlockSizeInBytes + { + get => streamingBlockSizeInBytes; + set + { + if (value <= 0) + throw new ArgumentOutOfRangeException(nameof(value)); + + streamingBlockSizeInBytes = value; + } + } +} diff --git a/src/Dapr.Cryptography/Encryption/EncryptionOptions.cs b/src/Dapr.Cryptography/Encryption/EncryptionOptions.cs new file mode 100644 index 000000000..ab48ac159 --- /dev/null +++ b/src/Dapr.Cryptography/Encryption/EncryptionOptions.cs @@ -0,0 +1,65 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 The Dapr Authors +// 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. +// ------------------------------------------------------------------------ + +namespace Dapr.Cryptography.Encryption; + +/// +/// A collection of options used to configure how encryption cryptographic operations are performed. +/// +public class EncryptionOptions +{ + /// + /// Creates a new instance of the . + /// + /// + public EncryptionOptions(KeyWrapAlgorithm keyWrapAlgorithm) + { + KeyWrapAlgorithm = keyWrapAlgorithm; + } + + /// + /// The name of the algorithm used to wrap the encryption key. + /// + public KeyWrapAlgorithm KeyWrapAlgorithm { get; set; } + + private int streamingBlockSizeInBytes = 32 * 1024; // 32 KB + /// + /// The size of the block in bytes used to send data to the sidecar for cryptography operations. + /// + /// + /// This defaults to 4KB and generally should not exceed 64KB. + /// + public int StreamingBlockSizeInBytes + { + get => streamingBlockSizeInBytes; + set + { + if (value <= 0) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + + streamingBlockSizeInBytes = value; + } + } + + /// + /// The optional name (and optionally a version) of the key specified to use during decryption. + /// + public string DecryptionKeyName { get; set; } = string.Empty; + + /// + /// The name of the cipher to use for the encryption operation. + /// + public DataEncryptionCipher EncryptionCipher { get; set; } = DataEncryptionCipher.AesGcm; +} diff --git a/src/Dapr.Cryptography/Encryption/Extensions/EncryptionServiceCollectionExtensions.cs b/src/Dapr.Cryptography/Encryption/Extensions/EncryptionServiceCollectionExtensions.cs new file mode 100644 index 000000000..866d92599 --- /dev/null +++ b/src/Dapr.Cryptography/Encryption/Extensions/EncryptionServiceCollectionExtensions.cs @@ -0,0 +1,77 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 The Dapr Authors +// 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 Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Dapr.Cryptography.Encryption.Extensions; + +/// +/// Contains extension methods for using Dapr Encryption/Decryption capabilities with dependency injection. +/// +public static class EncryptionServiceCollectionExtensions +{ + /// + /// Adds Dapr encryption/decryption support to the service collection. + /// + /// The . + /// Optionally allows greater configuration of the . + /// The lifetime of the registered services. + /// + public static IServiceCollection AddDaprEncryptionClient( + this IServiceCollection services, + Action? configure = null, + ServiceLifetime lifetime = ServiceLifetime.Singleton) + { + ArgumentNullException.ThrowIfNull(services, nameof(services)); + + //Register the IHttpClientFactory implementation + services.AddHttpClient(); + + var registration = new Func(serviceProvider => + { + var httpClientFactory = serviceProvider.GetRequiredService(); + var configuration = serviceProvider.GetService(); + + var builder = new DaprEncryptionClientBuilder(configuration); + builder.UseHttpClientFactory(httpClientFactory); + + configure?.Invoke(serviceProvider, builder); + + return builder.Build(); + }); + + switch (lifetime) + { + case ServiceLifetime.Scoped: + services.TryAddScoped(registration); + // ReSharper disable once RedundantTypeArgumentsOfMethod + services.TryAddScoped(registration); + break; + case ServiceLifetime.Transient: + services.TryAddTransient(registration); + // ReSharper disable once RedundantTypeArgumentsOfMethod + services.TryAddTransient(registration); + break; + default: + case ServiceLifetime.Singleton: + services.TryAddSingleton(registration); + // ReSharper disable once RedundantTypeArgumentsOfMethod + services.TryAddSingleton(registration); + break; + } + + return services; + } +} diff --git a/src/Dapr.Cryptography/Encryption/IDaprEncryptionClient.cs b/src/Dapr.Cryptography/Encryption/IDaprEncryptionClient.cs new file mode 100644 index 000000000..441505ff6 --- /dev/null +++ b/src/Dapr.Cryptography/Encryption/IDaprEncryptionClient.cs @@ -0,0 +1,92 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 The Dapr Authors +// 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. +// ------------------------------------------------------------------------ + +namespace Dapr.Cryptography.Encryption; + +/// +/// Provides the methods required to implement a . +/// +public interface IDaprEncryptionClient +{ + /// + /// Encrypts an array of bytes using the Dapr Cryptography encryption functionality. + /// + /// The name of the vault resource used by the operation. + /// The bytes of the plaintext value to encrypt. + /// The name of the key to use from the Vault for the encryption operation. + /// Options informing how the encryption operation should be configured. + /// A that can be used to cancel the operation. + /// An array of encrypted bytes. + [Obsolete( + "The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public Task> EncryptAsync( + string vaultResourceName, + ReadOnlyMemory plaintextBytes, + string keyName, + EncryptionOptions encryptionOptions, + CancellationToken cancellationToken = default); + + /// + /// Encrypts a stream using the Dapr Cryptography encryption functionality. + /// + /// The name of the vault resource used by the operation. + /// The stream containing the bytes of the plaintext value to encrypt. + /// The name of the key to use from the Vault for the encryption operation. + /// Options informing how the encryption operation should be configured. + /// A that can be used to cancel the operation. + /// An array of encrypted bytes. + [Obsolete( + "The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public IAsyncEnumerable> EncryptAsync( + string vaultResourceName, + Stream plaintextStream, + string keyName, + EncryptionOptions encryptionOptions, + CancellationToken cancellationToken = default); + + /// + /// Decrypts the specified ciphertext bytes using the Dapr Cryptography encryption functionality. + /// + /// The name of the vault resource used by the operation. + /// The bytes of the ciphertext value to decrypt. + /// The name of the key to use from the Vault for the decryption operation. + /// Options informing how the decryption operation should be configured. + /// A that can be used to cancel the operation. + /// An array of decrypted bytes. + [Obsolete( + "The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public Task> DecryptAsync( + string vaultResourceName, + ReadOnlyMemory ciphertextBytes, + string keyName, + DecryptionOptions? options = null, + CancellationToken cancellationToken = default); + + /// + /// Decrypts the specified stream of ciphertext using the Dapr Cryptography encryption functionality. + /// + /// The name of the vault resource used by the operation. + /// The stream containing the bytes of the ciphertext value to decrypt. + /// The name of the key to use from the Vault for the decryption operation. + /// Options informing how the decryption operation should be configured. + /// A that can be used to cancel the operation. + /// An asynchronously enumerable array of decrypted bytes. + [Obsolete( + "The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public IAsyncEnumerable> DecryptAsync( + string vaultResourceName, + Stream ciphertextStream, + string keyName, + DecryptionOptions? options = null, + CancellationToken cancellationToken = default); +} diff --git a/src/Dapr.Cryptography/Encryption/KeyWrapAlgorithm.cs b/src/Dapr.Cryptography/Encryption/KeyWrapAlgorithm.cs new file mode 100644 index 000000000..06fa315ea --- /dev/null +++ b/src/Dapr.Cryptography/Encryption/KeyWrapAlgorithm.cs @@ -0,0 +1,58 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 The Dapr Authors +// 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.Runtime.Serialization; + +namespace Dapr.Cryptography.Encryption; + +/// +/// The algorithm used for key wrapping cryptographic operations. +/// +public enum KeyWrapAlgorithm +{ + /// + /// Represents the AES key wrap algorithm. + /// + [EnumMember(Value="A256KW")] + Aes, + /// + /// An alias for the AES key wrap algorithm. + /// + [EnumMember(Value="A256KW")] + A256kw, + /// + /// Represents the AES 128 CBC key wrap algorithm. + /// + [EnumMember(Value="A128CBC")] + A128cbc, + /// + /// Represents the AES 192 CBC key wrap algorithm. + /// + [EnumMember(Value="A192CBC")] + A192cbc, + /// + /// Represents the AES 256 CBC key wrap algorithm. + /// + [EnumMember(Value="A256CBC")] + A256cbc, + /// + /// Represents the RSA key wrap algorithm. + /// + [EnumMember(Value= "RSA-OAEP-256")] + Rsa, + /// + /// An alias for the RSA key wrap algorithm. + /// + [EnumMember(Value= "RSA-OAEP-256")] + RsaOaep256 //Alias for RSA +} diff --git a/src/Dapr.Cryptography/Extensions/AsyncEnumerableExtensions.cs b/src/Dapr.Cryptography/Extensions/AsyncEnumerableExtensions.cs new file mode 100644 index 000000000..987d5c22c --- /dev/null +++ b/src/Dapr.Cryptography/Extensions/AsyncEnumerableExtensions.cs @@ -0,0 +1,23 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 The Dapr Authors +// 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. +// ------------------------------------------------------------------------ + +namespace Dapr.Cryptography.Extensions; + +internal static class AsyncEnumerableExtensions +{ + public static async IAsyncEnumerable Empty() + { + await Task.CompletedTask; + yield break; + } +} diff --git a/test/Dapr.Cryptography.Test/Dapr.Cryptography.Test.csproj b/test/Dapr.Cryptography.Test/Dapr.Cryptography.Test.csproj new file mode 100644 index 000000000..f6b0fba19 --- /dev/null +++ b/test/Dapr.Cryptography.Test/Dapr.Cryptography.Test.csproj @@ -0,0 +1,28 @@ + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + diff --git a/test/Dapr.Cryptography.Test/Encryption/DaprEncryptionClientBuilderTests.cs b/test/Dapr.Cryptography.Test/Encryption/DaprEncryptionClientBuilderTests.cs new file mode 100644 index 000000000..08974f8a5 --- /dev/null +++ b/test/Dapr.Cryptography.Test/Encryption/DaprEncryptionClientBuilderTests.cs @@ -0,0 +1,48 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 The Dapr Authors +// 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 Dapr.Cryptography.Encryption; +using Microsoft.Extensions.Configuration; +using Moq; + +namespace Dapr.Cryptography.Test.Encryption; + +public class DaprEncryptionClientBuilderTests +{ + [Fact] + public void Build_ShouldReturnNonNullDaprEncryptionClient() + { + // Arrange + var builder = new DaprEncryptionClientBuilder(); + + // Act + var client = builder.Build(); + + // Assert + Assert.NotNull(client); + } + + [Fact] + public void Build_ShouldHandleConfiguration() + { + // Arrange + var mockConfiguration = new Mock(); + var builder = new DaprEncryptionClientBuilder(mockConfiguration.Object); + + // Act + var client = builder.Build(); + + // Assert + Assert.NotNull(client); + } +} diff --git a/test/Dapr.Cryptography.Test/Encryption/DaprEncryptionGrpcClientTests.cs b/test/Dapr.Cryptography.Test/Encryption/DaprEncryptionGrpcClientTests.cs new file mode 100644 index 000000000..7acaa21c8 --- /dev/null +++ b/test/Dapr.Cryptography.Test/Encryption/DaprEncryptionGrpcClientTests.cs @@ -0,0 +1,95 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 The Dapr Authors +// 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.Http; +using System.Threading; +using Dapr.Cryptography.Encryption; +using Moq; +// ReSharper disable UnusedVariable + +namespace Dapr.Cryptography.Test.Encryption; + +public class DaprEncryptionGrpcClientTests +{ + [Fact] + public void EncryptAsync_VaultNameCannotBeEmpty() + { + var mockClient = Mock.Of(); + var httpClient = Mock.Of(); + + var client = new DaprEncryptionGrpcClient(mockClient, httpClient, null); + +#pragma warning disable CS0618 // Type or member is obsolete + var result = Assert.ThrowsAsync(async () => + { + var bytes = Array.Empty(); + await client.EncryptAsync(string.Empty, bytes, "key", new EncryptionOptions(KeyWrapAlgorithm.A128cbc), CancellationToken.None); + + }); +#pragma warning restore CS0618 // Type or member is obsolete + } + + [Fact] + public void EncryptAsync_KeyNameCannotBeEmpty() + { + var mockClient = Mock.Of(); + var httpClient = Mock.Of(); + + var client = new DaprEncryptionGrpcClient(mockClient, httpClient, null); + +#pragma warning disable CS0618 // Type or member is obsolete + var result = Assert.ThrowsAsync(async () => + { + var bytes = Array.Empty(); + await client.EncryptAsync("myVault", bytes, string.Empty, new EncryptionOptions(KeyWrapAlgorithm.A128cbc), CancellationToken.None); + + }); +#pragma warning restore CS0618 // Type or member is obsolete + } + + [Fact] + public void EncryptAsync_StreamCannotBeNull() + { + var mockClient = Mock.Of(); + var httpClient = Mock.Of(); + + var client = new DaprEncryptionGrpcClient(mockClient, httpClient, null); + +#pragma warning disable CS0618 // Type or member is obsolete + var result = Assert.ThrowsAsync(async () => + { + // ReSharper disable once AssignNullToNotNullAttribute + await client.EncryptAsync("myVault", (Stream)null, string.Empty, new EncryptionOptions(KeyWrapAlgorithm.A128cbc), CancellationToken.None); + }); +#pragma warning restore CS0618 // Type or member is obsolete + } + + [Fact] + public void EncryptAsync_OptionsCannotBeNull() + { + var mockClient = Mock.Of(); + var httpClient = Mock.Of(); + + var client = new DaprEncryptionGrpcClient(mockClient, httpClient, null); + +#pragma warning disable CS0618 // Type or member is obsolete + var result = Assert.ThrowsAsync(async () => + { + // ReSharper disable once AssignNullToNotNullAttribute + await client.EncryptAsync("myVault", Array.Empty(), string.Empty, null, CancellationToken.None); + }); +#pragma warning restore CS0618 // Type or member is obsolete + } +} diff --git a/test/Dapr.Cryptography.Test/Encryption/DecryptionOptionsTests.cs b/test/Dapr.Cryptography.Test/Encryption/DecryptionOptionsTests.cs new file mode 100644 index 000000000..b4830081c --- /dev/null +++ b/test/Dapr.Cryptography.Test/Encryption/DecryptionOptionsTests.cs @@ -0,0 +1,59 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 The Dapr Authors +// 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 Dapr.Cryptography.Encryption; + +namespace Dapr.Cryptography.Test.Encryption; + +public class DecryptionOptionsTests +{ + [Fact] + public void StreamingBlockSizeInBytes_ShouldReturnDefaultValue() + { + // Arrange + var options = new DecryptionOptions(); + + // Act + var blockSize = options.StreamingBlockSizeInBytes; + + // Assert + Assert.Equal(4 * 1024, blockSize); // Default value is 4KB + } + + [Fact] + public void StreamingBlockSizeInBytes_ShouldSetValidValue() + { + // Arrange + var options = new DecryptionOptions(); + const int newBlockSize = 8 * 1024; // 8KB + + // Act + options.StreamingBlockSizeInBytes = newBlockSize; + var blockSize = options.StreamingBlockSizeInBytes; + + // Assert + Assert.Equal(newBlockSize, blockSize); + } + + [Fact] + public void StreamingBlockSizeInBytes_ShouldThrowArgumentOutOfRangeException_ForInvalidValue() + { + // Arrange + var options = new DecryptionOptions(); + + // Act & Assert + Assert.Throws(() => options.StreamingBlockSizeInBytes = 0); + Assert.Throws(() => options.StreamingBlockSizeInBytes = -1); + } +} diff --git a/test/Dapr.Cryptography.Test/Encryption/EncryptionOptionsTests.cs b/test/Dapr.Cryptography.Test/Encryption/EncryptionOptionsTests.cs new file mode 100644 index 000000000..e51f196e2 --- /dev/null +++ b/test/Dapr.Cryptography.Test/Encryption/EncryptionOptionsTests.cs @@ -0,0 +1,98 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 The Dapr Authors +// 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 Dapr.Cryptography.Encryption; + +namespace Dapr.Cryptography.Test.Encryption; + +public class EncryptionOptionsTests +{ + [Fact] + public void Constructor_ShouldInitializeKeyWrapAlgorithm() + { + // Arrange + const KeyWrapAlgorithm keyWrapAlgorithm = KeyWrapAlgorithm.RsaOaep256; + + // Act + var options = new EncryptionOptions(keyWrapAlgorithm); + + // Assert + Assert.Equal(keyWrapAlgorithm, options.KeyWrapAlgorithm); + } + + [Fact] + public void StreamingBlockSizeInBytes_ShouldReturnDefaultValue() + { + // Arrange + var options = new EncryptionOptions(KeyWrapAlgorithm.RsaOaep256); + + // Act + var blockSize = options.StreamingBlockSizeInBytes; + + // Assert + Assert.Equal(4 * 1024, blockSize); // Default value is 4KB + } + + [Fact] + public void StreamingBlockSizeInBytes_ShouldSetValidValue() + { + // Arrange + var options = new EncryptionOptions(KeyWrapAlgorithm.RsaOaep256); + const int newBlockSize = 8 * 1024; // 8KB + + // Act + options.StreamingBlockSizeInBytes = newBlockSize; + var blockSize = options.StreamingBlockSizeInBytes; + + // Assert + Assert.Equal(newBlockSize, blockSize); + } + + [Fact] + public void StreamingBlockSizeInBytes_ShouldThrowArgumentOutOfRangeException_ForInvalidValue() + { + // Arrange + var options = new EncryptionOptions(KeyWrapAlgorithm.RsaOaep256); + + // Act & Assert + Assert.Throws(() => options.StreamingBlockSizeInBytes = 0); + Assert.Throws(() => options.StreamingBlockSizeInBytes = -1); + } + + [Fact] + public void DecryptionKeyName_ShouldBeNullByDefault() + { + // Arrange + var options = new EncryptionOptions(KeyWrapAlgorithm.RsaOaep256); + + // Act + var decryptionKeyName = options.DecryptionKeyName; + + // Assert + Assert.Null(decryptionKeyName); + } + + [Fact] + public void EncryptionCipher_ShouldReturnDefaultValue() + { + // Arrange + var options = new EncryptionOptions(KeyWrapAlgorithm.RsaOaep256); + + // Act + var encryptionCipher = options.EncryptionCipher; + + // Assert + Assert.Equal(DataEncryptionCipher.AesGcm, encryptionCipher); // Default value is AesGcm + } +} diff --git a/test/Dapr.Cryptography.Test/Encryption/Extensions/EncryptionServiceCollectionExtensionsTests.cs b/test/Dapr.Cryptography.Test/Encryption/Extensions/EncryptionServiceCollectionExtensionsTests.cs new file mode 100644 index 000000000..352f64a34 --- /dev/null +++ b/test/Dapr.Cryptography.Test/Encryption/Extensions/EncryptionServiceCollectionExtensionsTests.cs @@ -0,0 +1,169 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 The Dapr Authors +// 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.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using Dapr.Cryptography.Encryption; +using Dapr.Cryptography.Encryption.Extensions; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Dapr.Cryptography.Test.Encryption.Extensions; + +public class EncryptionServiceCollectionExtensionsTests +{ + [Fact] + public void AddDaprEncryptionClient_FromIConfiguration() + { + const string apiToken = "acb123"; + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary { { "DAPR_API_TOKEN", apiToken } }) + .Build(); + + var services = new ServiceCollection(); + services.AddSingleton(configuration); + + services.AddDaprEncryptionClient(); + + var app = services.BuildServiceProvider(); + + var encryptionClient = app.GetRequiredService() as DaprEncryptionGrpcClient; + + Assert.NotNull(encryptionClient!.DaprApiToken); + Assert.Equal(apiToken, encryptionClient.DaprApiToken); + } + + [Fact] + public void AddDaprEncryptionClient_RegistersClientOnlyOnce() + { + var services = new ServiceCollection(); + + var clientBuilder = new Action((sp, builder) => + { + builder.UseDaprApiToken("abc"); + }); + + services.AddDaprEncryptionClient(); //Sets a default API token value of an empty string + services.AddDaprEncryptionClient(clientBuilder); //Sets the API token value + + var serviceProvider = services.BuildServiceProvider(); + var daprEncryptionClient = serviceProvider.GetService() as DaprEncryptionGrpcClient; + + Assert.NotNull(daprEncryptionClient!.HttpClient); + Assert.False(daprEncryptionClient.HttpClient.DefaultRequestHeaders.TryGetValues("dapr-api-token", out _)); + } + + [Fact] + public void AddDaprEncryptionClient_RegistersIHttpClientFactory() + { + var services = new ServiceCollection(); + + services.AddDaprEncryptionClient(); + + var serviceProvider = services.BuildServiceProvider(); + + var httpClientFactory = serviceProvider.GetService(); + Assert.NotNull(httpClientFactory); + + var daprEncryptionClient = serviceProvider.GetService(); + Assert.NotNull(daprEncryptionClient); + } + + [Fact] + public void AddDaprEncryptionClient_RegistersUsingDependencyFromIServiceProvider() + { + var services = new ServiceCollection(); + services.AddSingleton(); + services.AddDaprEncryptionClient((provider, builder) => + { + var configProvider = provider.GetRequiredService(); + var apiToken = configProvider.GetApiTokenValue(); + builder.UseDaprApiToken(apiToken); + }); + + var serviceProvider = services.BuildServiceProvider(); + var client = serviceProvider.GetRequiredService() as DaprEncryptionGrpcClient; + + //Validate it's set on the GrpcClient - not that it doesn't get set on the HttpClient + Assert.NotNull(client); + Assert.NotNull(client.DaprApiToken); + Assert.Equal("abcdef", client.DaprApiToken); + Assert.NotNull(client.HttpClient); + + if (!client.HttpClient.DefaultRequestHeaders.TryGetValues("dapr-api-token", out var daprApiToken)) + { + Assert.Fail(); + } + + Assert.Equal("abcdef", daprApiToken.FirstOrDefault()); + } + + [Fact] + public void AddDaprEncryptionClient_ShouldRegisterSingleton_WhenLifetimeIsSingleton() + { + var services = new ServiceCollection(); + + services.AddDaprEncryptionClient((_, _) => { }, ServiceLifetime.Singleton); + var serviceProvider = services.BuildServiceProvider(); + + var encryptionClient1 = serviceProvider.GetService(); + var encryptionClient2 = serviceProvider.GetService(); + + Assert.NotNull(encryptionClient1); + Assert.NotNull(encryptionClient2); + + Assert.Same(encryptionClient1, encryptionClient2); + } + + [Fact] + public void AddDaprEncryptionClient_ShouldRegisterTransient_WhenLifetimeIsTransient() + { + var services = new ServiceCollection(); + + services.AddDaprEncryptionClient((_, _) => { }, ServiceLifetime.Transient); + var serviceProvider = services.BuildServiceProvider(); + + var encryptionClient1 = serviceProvider.GetService(); + var encryptionClient2 = serviceProvider.GetService(); + + Assert.NotNull(encryptionClient1); + Assert.NotNull(encryptionClient2); + } + + [Fact] + public async Task AddDaprEncryptionClient_ShouldRegisterScoped_WhenLifetimeIsScoped() + { + var services = new ServiceCollection(); + + services.AddDaprEncryptionClient((_, _) => { }, ServiceLifetime.Scoped); + var serviceProvider = services.BuildServiceProvider(); + + await using var scope1 = serviceProvider.CreateAsyncScope(); + var encryptionClient1 = scope1.ServiceProvider.GetService(); + Assert.NotNull(encryptionClient1); + + await using var scope2 = serviceProvider.CreateAsyncScope(); + var encryptionClient2 = scope2.ServiceProvider.GetService(); + Assert.NotNull(encryptionClient2); + + Assert.NotSame(encryptionClient1, encryptionClient2); + } + + private sealed class TestSecretRetriever + { + public string GetApiTokenValue() => "abcdef"; + } +} diff --git a/test/Dapr.Cryptography.Test/Extensions/AsyncEnumerableExtensionsTests.cs b/test/Dapr.Cryptography.Test/Extensions/AsyncEnumerableExtensionsTests.cs new file mode 100644 index 000000000..154c1c7a6 --- /dev/null +++ b/test/Dapr.Cryptography.Test/Extensions/AsyncEnumerableExtensionsTests.cs @@ -0,0 +1,64 @@ +// ------------------------------------------------------------------------ +// Copyright 2025 The Dapr Authors +// 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.Threading.Tasks; +using Dapr.Cryptography.Extensions; + +namespace Dapr.Cryptography.Test.Extensions; + +public class AsyncEnumerableExtensionsTests +{ + [Fact] + public async Task Empty_ShouldReturnEmptyAsyncEnumerable() + { + // Arrange + var asyncEnumerable = AsyncEnumerableExtensions.Empty(); + + // Act & Assert + await foreach (var item in asyncEnumerable) + { + Assert.Fail("Expected no items in the async enumerable."); + } + } + + [Fact] + public async Task Empty_ShouldReturnEmptyAsyncEnumerable_ForStringType() + { + // Arrange + var asyncEnumerable = AsyncEnumerableExtensions.Empty(); + + // Act & Assert + await foreach (var item in asyncEnumerable) + { + Assert.Fail("Expected no items in the async enumerable."); + } + } + + [Fact] + public async Task Empty_ShouldReturnEmptyAsyncEnumerable_ForCustomType() + { + // Arrange + var asyncEnumerable = AsyncEnumerableExtensions.Empty(); + + // Act & Assert + await foreach (var item in asyncEnumerable) + { + Assert.Fail("Expected no items in the async enumerable."); + } + } + + private class MyCustomType + { + // Custom type definition + } +}