diff --git a/all.sln b/all.sln index 47fc9098c..bf0b87352 100644 --- a/all.sln +++ b/all.sln @@ -104,6 +104,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BulkPublishEventExample", " EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkflowUnitTest", "examples\Workflow\WorkflowUnitTest\WorkflowUnitTest.csproj", "{8CA09061-2BEF-4506-A763-07062D2BD6AC}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cryptography", "examples\Client\Cryptography\Cryptography.csproj", "{C74FBA78-13E8-407F-A173-4555AEE41FF3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -248,6 +250,14 @@ Global {DDC41278-FB60-403A-B969-2AEBD7C2D83C}.Release|Any CPU.Build.0 = Release|Any CPU {8CA09061-2BEF-4506-A763-07062D2BD6AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8CA09061-2BEF-4506-A763-07062D2BD6AC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C74FBA78-13E8-407F-A173-4555AEE41FF3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C74FBA78-13E8-407F-A173-4555AEE41FF3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C74FBA78-13E8-407F-A173-4555AEE41FF3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C74FBA78-13E8-407F-A173-4555AEE41FF3}.Release|Any CPU.Build.0 = Release|Any CPU + {D1786E2B-CAA0-4B2D-A974-9845EB9E420F}.Debug|Any CPU.ActiveCfg = Debug + {D1786E2B-CAA0-4B2D-A974-9845EB9E420F}.Debug|Any CPU.Build.0 = Debug + {D1786E2B-CAA0-4B2D-A974-9845EB9E420F}.Release|Any CPU.ActiveCfg = Release + {D1786E2B-CAA0-4B2D-A974-9845EB9E420F}.Release|Any CPU.Build.0 = Release EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -293,6 +303,7 @@ Global {4A175C27-EAFE-47E7-90F6-873B37863656} = {0EF6EA64-D7C3-420D-9890-EAE8D54A57E6} {DDC41278-FB60-403A-B969-2AEBD7C2D83C} = {0EF6EA64-D7C3-420D-9890-EAE8D54A57E6} {8CA09061-2BEF-4506-A763-07062D2BD6AC} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9} + {C74FBA78-13E8-407F-A173-4555AEE41FF3} = {A7F41094-8648-446B-AECD-DCC2CC871F73} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {65220BF2-EAE1-4CB2-AA58-EBE80768CB40} diff --git a/examples/Client/Cryptography/Components/azurekeyvault.yaml b/examples/Client/Cryptography/Components/azurekeyvault.yaml new file mode 100644 index 000000000..5932e0bc8 --- /dev/null +++ b/examples/Client/Cryptography/Components/azurekeyvault.yaml @@ -0,0 +1,25 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: azurekeyvault +spec: + type: crypto.azure.keyvault + metadata: + - name: vaultName + value: "" + - name: azureEnvironment + value: AZUREPUBLICCLOUD + - name: azureTenantId + secretKeyRef: + name: read_azure_tenant_id + key: read_azure_tenant_id + - name: azureClientId + secretKeyRef: + name: read_azure_client_id + key: read_azure_client_id + - name: azureClientSecret + secretKeyRef: + name: read_azure_client_secret + key: read_azure_client_secret +auth: + secureStore: envvar-secret-store \ No newline at end of file diff --git a/examples/Client/Cryptography/Components/env-secretstore.yaml b/examples/Client/Cryptography/Components/env-secretstore.yaml new file mode 100644 index 000000000..fb191414d --- /dev/null +++ b/examples/Client/Cryptography/Components/env-secretstore.yaml @@ -0,0 +1,7 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: envvar-secret-store +spec: + type: secretstores.local.env + version: v1 \ No newline at end of file diff --git a/examples/Client/Cryptography/Cryptography.csproj b/examples/Client/Cryptography/Cryptography.csproj new file mode 100644 index 000000000..525c38562 --- /dev/null +++ b/examples/Client/Cryptography/Cryptography.csproj @@ -0,0 +1,25 @@ + + + + Exe + net6.0 + enable + enable + latest + + + + + + + + + + + + + PreserveNewest + + + + \ No newline at end of file diff --git a/examples/Client/Cryptography/Example.cs b/examples/Client/Cryptography/Example.cs new file mode 100644 index 000000000..2c2d41626 --- /dev/null +++ b/examples/Client/Cryptography/Example.cs @@ -0,0 +1,22 @@ +// ------------------------------------------------------------------------ +// Copyright 2023 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 Cryptography +{ + internal abstract class Example + { + public abstract string DisplayName { get; } + + public abstract Task RunAsync(CancellationToken cancellationToken); + } +} diff --git a/examples/Client/Cryptography/Examples/EncryptDecryptFileStreamExample.cs b/examples/Client/Cryptography/Examples/EncryptDecryptFileStreamExample.cs new file mode 100644 index 000000000..aa9c404a7 --- /dev/null +++ b/examples/Client/Cryptography/Examples/EncryptDecryptFileStreamExample.cs @@ -0,0 +1,77 @@ +// ------------------------------------------------------------------------ +// Copyright 2023 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 Dapr.Client; +#pragma warning disable CS0618 // Type or member is obsolete + +namespace Cryptography.Examples +{ + internal class EncryptDecryptFileStreamExample : Example + { + public override string DisplayName => "Use Cryptography to encrypt and decrypt a file"; + public override async Task RunAsync(CancellationToken cancellationToken) + { + using var client = new DaprClientBuilder().Build(); + + const string componentName = "azurekeyvault"; // Change this to match the name of the component containing your vault + const string keyName = "myKey"; + + // The name of the file we're using as an example + const string fileName = "file.txt"; + + Console.WriteLine("Original file contents:"); + foreach (var line in await File.ReadAllLinesAsync(fileName, cancellationToken)) + { + Console.WriteLine(line); + } + Console.WriteLine(); + + //Encrypt from a file stream and buffer the resulting bytes to an in-memory buffer + await using var encryptFs = new FileStream(fileName, FileMode.Open); + + var bufferedEncryptedBytes = new ArrayBufferWriter(); + await foreach (var bytes in (await client.EncryptAsync(componentName, encryptFs, keyName, + new EncryptionOptions(KeyWrapAlgorithm.Rsa), cancellationToken)) + .WithCancellation(cancellationToken)) + { + bufferedEncryptedBytes.Write(bytes.Span); + } + + Console.WriteLine($"Encrypted bytes: {Convert.ToBase64String(bufferedEncryptedBytes.GetSpan())}"); + Console.WriteLine(); + + //We'll write to a temporary file via a FileStream + var tempDecryptedFile = Path.GetTempFileName(); + await using var decryptFs = new FileStream(tempDecryptedFile, FileMode.Create); + + //We'll stream the decrypted bytes from a MemoryStream into the above temporary file + await using var encryptedMs = new MemoryStream(bufferedEncryptedBytes.WrittenMemory.ToArray()); + await foreach (var result in (await client.DecryptAsync(componentName, encryptedMs, keyName, + cancellationToken)).WithCancellation(cancellationToken)) + { + decryptFs.Write(result.Span); + } + + decryptFs.Close(); + + //Let's confirm the value as written to the file + var decryptedValue = await File.ReadAllTextAsync(tempDecryptedFile, cancellationToken); + Console.WriteLine($"Decrypted value: "); + Console.WriteLine(decryptedValue); + + //And some cleanup to delete our temp file + File.Delete(tempDecryptedFile); + } + } +} diff --git a/examples/Client/Cryptography/Examples/EncryptDecryptStringExample.cs b/examples/Client/Cryptography/Examples/EncryptDecryptStringExample.cs new file mode 100644 index 000000000..a37ca1b8b --- /dev/null +++ b/examples/Client/Cryptography/Examples/EncryptDecryptStringExample.cs @@ -0,0 +1,47 @@ +// ------------------------------------------------------------------------ +// Copyright 2023 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.Text; +using Dapr.Client; +#pragma warning disable CS0618 // Type or member is obsolete + +namespace Cryptography.Examples +{ + internal class EncryptDecryptStringExample : Example + { + public override string DisplayName => "Using Cryptography to encrypt and decrypt a string"; + + public override async Task RunAsync(CancellationToken cancellationToken) + { + using var client = new DaprClientBuilder().Build(); + + const string componentName = "azurekeyvault"; //Change this to match the name of the component containing your vault + const string keyName = "myKey"; //Change this to match the name of the key in your Vault + + + const string plaintextStr = "This is the value we're going to encrypt today"; + Console.WriteLine($"Original string value: '{plaintextStr}'"); + + //Encrypt the string + var plaintextBytes = Encoding.UTF8.GetBytes(plaintextStr); + var encryptedBytesResult = await client.EncryptAsync(componentName, plaintextBytes, keyName, new EncryptionOptions(KeyWrapAlgorithm.Rsa), + cancellationToken); + + Console.WriteLine($"Encrypted bytes: '{Convert.ToBase64String(encryptedBytesResult.Span)}'"); + + //Decrypt the string + var decryptedBytes = await client.DecryptAsync(componentName, encryptedBytesResult, keyName, new DecryptionOptions(), cancellationToken); + Console.WriteLine($"Decrypted string: '{Encoding.UTF8.GetString(decryptedBytes.ToArray())}'"); + } + } +} diff --git a/examples/Client/Cryptography/Program.cs b/examples/Client/Cryptography/Program.cs new file mode 100644 index 000000000..74e3c7f48 --- /dev/null +++ b/examples/Client/Cryptography/Program.cs @@ -0,0 +1,47 @@ +// ------------------------------------------------------------------------ +// Copyright 2023 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 Cryptography; +using Cryptography.Examples; + +namespace Samples.Client +{ + class Program + { + private static readonly Example[] Examples = new Example[] + { + new EncryptDecryptStringExample(), + new EncryptDecryptFileStreamExample() + }; + + static async Task Main(string[] args) + { + if (args.Length > 0 && int.TryParse(args[0], out var index) && index >= 0 && index < Examples.Length) + { + var cts = new CancellationTokenSource(); + Console.CancelKeyPress += (object? sender, ConsoleCancelEventArgs e) => cts.Cancel(); + + await Examples[index].RunAsync(cts.Token); + return 0; + } + + Console.WriteLine("Hello, please choose a sample to run:"); + for (var i = 0; i < Examples.Length; i++) + { + Console.WriteLine($"{i}: {Examples[i].DisplayName}"); + } + Console.WriteLine(); + return 1; + } + } +} diff --git a/examples/Client/Cryptography/README.md b/examples/Client/Cryptography/README.md new file mode 100644 index 000000000..c0c884369 --- /dev/null +++ b/examples/Client/Cryptography/README.md @@ -0,0 +1,92 @@ +# Dapr .NET SDK Cryptography example + +## Prerequisites + +- [.NET 8+](https://dotnet.microsoft.com/download) installed +- [Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli) +- [Initialized Dapr environment](https://docs.dapr.io/getting-started/installation) +- [Dapr .NET SDK](https://docs.dapr.io/developing-applications/sdks/dotnet/) +- [Azure Key Vault instance](https://learn.microsoft.com/en-us/azure/key-vault/general/quick-create-portal) +- [Entra Service Principal](https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-register-app) + +### Service Principal/Environment Variables Setup +In your Azure portal, open Microsoft Entra ID and click `App Registrations`. Click the button at the top to create a new registration. Select a name for your service principal +and click register, noting this name for later. + +Once the registration is completed, open it from the list and select Certificates & Secrets from the left navigation. Select "Client secrets" from the page body (middle column) +and click the button to add a new client secret giving it an optional description and changing the expiry date as you desire. Click Add to create the secret. Record the secret +value it shows you - it will not be shown to you again without creating another client secret. + +Click Overview from the left navigation and record the "Application (client) ID" and the "Directory (tenant) ID" values. + +On your computer (assuming Windows), open your start menu and type "Environment Variables". An option should appear named "Edit the system environment variables". Select this +and your System Properties window will open. Click the "Environment Variables" button in the bottom and said window will appear. Click the "New..." button under System variables +to add the requisite service principal values to your environment variables. You can change these names as to want by updating the `./Components/azurekeyvault.yaml` names, but for now +configure as follows: + +| Variable Name | Value | +|--|--| +| read_azure_client_id | Paste the value from your app registration overview for "Application (client) ID" | +| read_azure_client_secret | Paste the value of the client secret you generated for your app registration | +| read_azure_tenant_id | Paste the valeu from your app registration overview for "Directory (tenant) ID" | + +Click OK to save your environment variables and to close your System Properties window. You may need to close restart your command line tool for it to recognize the new values. + +### Azure Key Vault Setup + +This example is implemented using the Azure Key Vault and will not work without it. Assuming you have a Key Vault instance configured, ensure that +you have the `Key Vault Crypto Officer` role assigned to yourself as you'll need to in order to generate a new key in the instance. After selecting Keys +under the Objects header, click the `Generate/Import` button at the top of the instance panel. + +Under options, select `Generate` and name your key. This example is pre-configured to assume a key name of 'myKey', but feel free to change this (but also update the name in the example +you wish to run). The other default options are fine for our purposes, so click Create at the bottom and if you've got the appropriate roles, it will show up in the list of Keys. + +Update your `./Components/azurekeyvault.yaml` file with the name of your Key Vault under `vaultName` where it currently reads "changeMe". This sample assumes authentication +via a service principal, so you might also need to set this up. + +Back in the Azure Portal, assign at least the `Key Vault Crypto User` role to the service principal you previously created in the last step. Do this by clicking +`Access Control (IAM)` from the left navigation, clicking "Add" from the top and clicking "Add Role Assignment". Select `Key Vault Crypto User` from the list and click the Next +button. Ensuring that the "User, group or service principal" option is selected, click the "Select members" link and search for the name of the app registration you created. Click +Add to add this service principal to the list of members for the new role assignment and click Review + Assign twice to assign the role. This will take effect within a few seconds +or minutes. This step ensures that while Dapr can authenticate as your service principal, that it also has permission to access and use the key in your Key Vault. + +## Running the example + +To run the sample locally, run this command in the DaprClient directory: + +```sh +dapr run --resources-path ./Components --app-id DaprClient -- dotnet run +``` + +Running the following command will output a list of the samples included: + +```sh +dapr run --resources-path ./Components --app-id DaprClient -- dotnet run +``` + +Press Ctrl+C to exit, and then run the command again and provide a sample number to run the samples. + +For example, run this command to run the first sample from the list produced earlier (the 0th example): + +```sh +dapr run --resources-path ./Components --app-id DaprClient -- dotnet run 0 +``` + +## Encryption/Decryption with strings +See [EncryptDecryptStringExample.cs](./EncryptDecryptStringExample.cs) for an example of using `DaprClient` for basic +string-based encryption and decryption operations as performed against UTF-8 encoded byte arrays. + +## Encryption/Decryption with streams +See [EncryptDecryptFileStreamExample.cs](./EncryptDecryptFileStreamExample.cs) for an example of using `DaprClient` +to perform an encrypt and decrypt operation against a stream of data. In the example, we stream a local file to the +sidecar to encrypt and write the result (as it's streamed back) to an in-memory buffer. Once the operation fully +completes, we perform the decrypt operation against this in-memory buffer and write the decrypted result back out to a +temporary file. + +In either operation, rather than load the entire stream into memory and send all at once to the +sidecar as we do in the other string-based example (as this might cause you to run out of memory either on the +node the app is running on or do the same to the sidecar itself), this example instead breaks the input stream into +more manageable 4KB chunks (a value you can override via the `EncryptionOptions` or `DecryptionOptions` parameters +respectively up to 64KB. Further, rather than waiting for the entire stream to send to the sidecar before the +encryption operation proceeds, it immediately works to process the sidecar response, continuing to minimize resource +usage. diff --git a/examples/Client/Cryptography/file.txt b/examples/Client/Cryptography/file.txt new file mode 100644 index 000000000..9e8638939 --- /dev/null +++ b/examples/Client/Cryptography/file.txt @@ -0,0 +1,26 @@ +# The Road Not Taken +## By Robert Lee Frost + +Two roads diverged in a yellow wood, +And sorry I could not travel both +And be one traveler, long I stood +And looked down one as far as I could +To where it bent in the undergrowth; + +Then took the other, as just as fair +And having perhaps the better claim, +Because it was grassy and wanted wear; +Though as for that, the passing there +Had worn them really about the same, + +And both that morning equally lay +In leaves no step had trodden black +Oh, I kept the first for another day! +Yet knowing how way leads on to way, +I doubted if I should ever come back. + +I shall be telling this with a sigh +Somewhere ages and ages hence: +Two roads diverged in a wood, and I, +I took the one less traveled by, +And that has made all the difference. \ No newline at end of file diff --git a/src/Dapr.Client/CryptographyEnums.cs b/src/Dapr.Client/CryptographyEnums.cs new file mode 100644 index 000000000..f5955b389 --- /dev/null +++ b/src/Dapr.Client/CryptographyEnums.cs @@ -0,0 +1,76 @@ +// ------------------------------------------------------------------------ +// Copyright 2023 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.Client +{ + /// + /// 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 + }; + + /// + /// 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.Client/CryptographyOptions.cs b/src/Dapr.Client/CryptographyOptions.cs new file mode 100644 index 000000000..ae94a8f2f --- /dev/null +++ b/src/Dapr.Client/CryptographyOptions.cs @@ -0,0 +1,80 @@ +#nullable enable +using System; + +namespace Dapr.Client +{ + /// + /// 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 = 4 * 1024; // 4 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; } = null; + + /// + /// The name of the cipher to use for the encryption operation. + /// + public DataEncryptionCipher EncryptionCipher { get; set; } = DataEncryptionCipher.AesGcm; + } + + /// + /// A collection fo options used to configure how decryption cryptographic operations are performed. + /// + public 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. + /// + public int StreamingBlockSizeInBytes + { + get => streamingBlockSizeInBytes; + set + { + if (value <= 0) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + + streamingBlockSizeInBytes = value; + } + } + } +} diff --git a/src/Dapr.Client/DaprClient.cs b/src/Dapr.Client/DaprClient.cs index 361ac54bc..20c37d9e7 100644 --- a/src/Dapr.Client/DaprClient.cs +++ b/src/Dapr.Client/DaprClient.cs @@ -3,7 +3,7 @@ // 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 +// 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. @@ -13,6 +13,7 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; @@ -939,6 +940,286 @@ public abstract Task UnsubscribeConfiguration( string id, CancellationToken cancellationToken = default); + #region Cryptography + + /// + /// 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 Task>> 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, + 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. + /// 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, 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 Task>> DecryptAsync(string vaultResourceName, Stream ciphertextStream, + string keyName, DecryptionOptions options, 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. + /// 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 Task>> DecryptAsync(string vaultResourceName, Stream ciphertextStream, + string keyName, CancellationToken cancellationToken = default); + + #endregion + + #region Cryptography - Subtle API + + ///// + ///// Retrieves the value of the specified key from the vault. + ///// + ///// The name of the vault resource used by the operation. + ///// The name of the key to retrieve the value of. + ///// The format to use for the key result. + ///// A that can be used to cancel the operation. + ///// The name (and possibly version as name/version) of the key and its public key. + //[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<(string Name, string PublicKey)> GetKeyAsync(string vaultResourceName, string keyName, SubtleGetKeyRequest.Types.KeyFormat keyFormat, + // CancellationToken cancellationToken = default); + + ///// + ///// Encrypts an array of bytes using the Dapr Cryptography functionality. + ///// + ///// The name of the vault resource used by the operation. + ///// The bytes of the plaintext value to encrypt. + ///// The name of the algorithm that should be used to perform the encryption. + ///// The name of the key used to perform the encryption operation. + ///// The bytes comprising the nonce. + ///// Any associated data when using AEAD ciphers. + ///// A that can be used to cancel the operation. + ///// The 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<(byte[] CipherTextBytes, byte[] AuthenticationTag)> EncryptAsync( + // string vaultResourceName, + // byte[] plainTextBytes, + // string algorithm, + // string keyName, + // byte[] nonce, + // byte[] associatedData, + // CancellationToken cancellationToken = default); + + ///// + ///// Encrypts an array of bytes using the Dapr Cryptography functionality. + ///// + ///// The name of the vault resource used by the operation. + ///// The bytes of the plaintext value to encrypt. + ///// The name of the algorithm that should be used to perform the encryption. + ///// The name of the key used to perform the encryption operation. + ///// The bytes comprising the nonce. + ///// A that can be used to cancel the operation. + ///// The 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 async Task<(byte[] CipherTextBytes, byte[] AuthenticationTag)> EncryptAsync( + // string vaultResourceName, + // byte[] plainTextBytes, + // string algorithm, + // string keyName, + // byte[] nonce, + // CancellationToken cancellationToken = default) => + // await EncryptAsync(vaultResourceName, plainTextBytes, algorithm, keyName, nonce, Array.Empty(), + // cancellationToken); + + ///// + ///// Decrypts an array of bytes using the Dapr Cryptography functionality. + ///// + ///// The name of the vault the key is retrieved from for the decryption operation. + ///// The array of bytes to decrypt. + ///// A that can be used to cancel the operation. + ///// The algorithm to use to perform the decryption operation. + ///// The name of the key used for the decryption. + ///// The nonce value used. + ///// + ///// Any associated data when using AEAD ciphers. + ///// The array of plaintext 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, byte[] cipherTextBytes, + // string algorithm, string keyName, byte[] nonce, byte[] tag, byte[] associatedData, + // CancellationToken cancellationToken = default); + + ///// + ///// Decrypts an array of bytes using the Dapr Cryptography functionality. + ///// + ///// The name of the vault the key is retrieved from for the decryption operation. + ///// The array of bytes to decrypt. + ///// A that can be used to cancel the operation. + ///// The algorithm to use to perform the decryption operation. + ///// The name of the key used for the decryption. + ///// The nonce value used. + ///// + ///// The array of plaintext 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 async Task DecryptAsync(string vaultResourceName, byte[] cipherTextBytes, + // string algorithm, string keyName, byte[] nonce, byte[] tag, CancellationToken cancellationToken = default) => + // await DecryptAsync(vaultResourceName, cipherTextBytes, algorithm, keyName, nonce, tag, Array.Empty(), cancellationToken); + + ///// + ///// Wraps the plaintext key using another. + ///// + ///// The name of the vault to retrieve the key from. + ///// The plaintext bytes comprising the key to wrap. + ///// The name of the key used to wrap the value. + ///// The algorithm to use to perform the wrap operation. + ///// The none used. + ///// Any associated data when using AEAD ciphers. + ///// A that can be used to cancel the operation. + ///// The bytes comprising the wrapped plain-text key and the authentication tag, if applicable. + //[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<(byte[] WrappedKey, byte[] AuthenticationTag)> WrapKeyAsync(string vaultResourceName, byte[] plainTextKey, string keyName, string algorithm, byte[] nonce, byte[] associatedData, + // CancellationToken cancellationToken = default); + + ///// + ///// Wraps the plaintext key using another. + ///// + ///// The name of the vault to retrieve the key from. + ///// The plaintext bytes comprising the key to wrap. + ///// The name of the key used to wrap the value. + ///// The algorithm to use to perform the wrap operation. + ///// The none used. + ///// A that can be used to cancel the operation. + ///// The bytes comprising the unwrapped key and the authentication tag, if applicable. + //[Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //public async Task<(byte[] WrappedKey, byte[] AuthenticationTag)> WrapKeyAsync(string vaultResourceName, byte[] plainTextKey, string keyName, string algorithm, + // byte[] nonce, CancellationToken cancellationToken = default) => await WrapKeyAsync(vaultResourceName, plainTextKey, + // keyName, algorithm, nonce, Array.Empty(), cancellationToken); + + ///// + ///// Used to unwrap the specified key. + ///// + ///// The name of the vault to retrieve the key from. + ///// The byte comprising the wrapped key. + ///// The algorithm to use in unwrapping the key. + ///// The name of the key used to unwrap the wrapped key bytes. + ///// The nonce value. + ///// The bytes comprising the authentication tag. + ///// Any associated data when using AEAD ciphers. + ///// A that can be used to cancel the operation. + ///// The bytes comprising the unwrapped key. + //[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 UnwrapKeyAsync(string vaultResourceName, byte[] wrappedKey, string algorithm, string keyName, byte[] nonce, byte[] tag, byte[] associatedData, + // CancellationToken cancellationToken = default); + + ///// + ///// Used to unwrap the specified key. + ///// + ///// The name of the vault to retrieve the key from. + ///// The byte comprising the wrapped key. + ///// The algorithm to use in unwrapping the key. + ///// The name of the key used to unwrap the wrapped key bytes. + ///// The nonce value. + ///// The bytes comprising the authentication tag. + ///// A that can be used to cancel the operation. + ///// The bytes comprising the unwrapped key. + //[Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //public async Task UnwrapKeyAsync(string vaultResourceName, byte[] wrappedKey, string algorithm, string keyName, + // byte[] nonce, byte[] tag, + // CancellationToken cancellationToken = default) => await UnwrapKeyAsync(vaultResourceName, + // wrappedKey, algorithm, keyName, nonce, Array.Empty(), Array.Empty(), cancellationToken); + + ///// + ///// Used to unwrap the specified key. + ///// + ///// The name of the vault to retrieve the key from. + ///// The byte comprising the wrapped key. + ///// The algorithm to use in unwrapping the key. + ///// The name of the key used to unwrap the wrapped key bytes. + ///// The nonce value. + ///// A that can be used to cancel the operation. + ///// The bytes comprising the unwrapped key. + //[Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + //public async Task UnwrapKeyAsync(string vaultResourceName, byte[] wrappedKey, string algorithm, string keyName, + // byte[] nonce, CancellationToken cancellationToken = default) => await UnwrapKeyAsync(vaultResourceName, + // wrappedKey, algorithm, keyName, nonce, Array.Empty(), Array.Empty(), cancellationToken); + + ///// + ///// Creates a signature of a digest value. + ///// + ///// The name of the vault to retrieve the key from. + ///// The digest value to create the signature for. + ///// The algorithm used to create the signature. + ///// The name of the key used. + ///// A that can be used to cancel the operation. + ///// The bytes comprising the signature. + //[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 SignAsync(string vaultResourceName, byte[] digest, string algorithm, string keyName, + // CancellationToken cancellationToken = default); + + ///// + ///// Validates a signature. + ///// + ///// The name of the vault to retrieve the key from. + ///// The digest to validate the signature with. + ///// The signature to validate. + ///// The algorithm to validate the signature with. + ///// The name of the key used. + ///// A that can be used to cancel the operation. + ///// True if the signature verification is successful; otherwise false. + //[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 VerifyAsync(string vaultResourceName, byte[] digest, byte[] signature, string algorithm, string keyName, + // CancellationToken cancellationToken = default); + + #endregion + /// /// Attempt to lock the given resourceId with response indicating success. /// diff --git a/src/Dapr.Client/DaprClientGrpc.cs b/src/Dapr.Client/DaprClientGrpc.cs index 75df09323..9c99b9eee 100644 --- a/src/Dapr.Client/DaprClientGrpc.cs +++ b/src/Dapr.Client/DaprClientGrpc.cs @@ -3,7 +3,7 @@ // 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 +// 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. @@ -14,10 +14,14 @@ namespace Dapr.Client { using System; + using System.Buffers; using System.Collections.Generic; + using System.IO; using System.Linq; 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; @@ -1373,6 +1377,498 @@ public override async Task UnsubscribeConfigur var resp = await client.UnsubscribeConfigurationAsync(request, options); return new UnsubscribeConfigurationResponse(resp.Ok, resp.Message); } + + #endregion + + #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.")] + public override async Task> EncryptAsync(string vaultResourceName, + ReadOnlyMemory plaintextBytes, string keyName, EncryptionOptions encryptionOptions, + CancellationToken cancellationToken = default) + { + if (MemoryMarshal.TryGetArray(plaintextBytes, out var plaintextSegment) && plaintextSegment.Array != null) + { + var encryptionResult = await EncryptAsync(vaultResourceName, new MemoryStream(plaintextSegment.Array), keyName, encryptionOptions, + cancellationToken); + + var bufferedResult = new ArrayBufferWriter(); + + await foreach (var item in encryptionResult.WithCancellation(cancellationToken)) + { + bufferedResult.Write(item.Span); + } + + return bufferedResult.WrittenMemory; + } + + throw new ArgumentException("The input instance doesn't have a valid underlying data store.", nameof(plaintextBytes)); + } + + /// + [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, Stream plaintextStream, + string keyName, EncryptionOptions encryptionOptions, CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); + ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); + ArgumentVerifier.ThrowIfNull(plaintextStream, nameof(plaintextStream)); + ArgumentVerifier.ThrowIfNull(encryptionOptions, nameof(encryptionOptions)); + + var shouldOmitDecryptionKeyName = string.IsNullOrWhiteSpace(encryptionOptions.DecryptionKeyName); //Whitespace isn't likely a valid key name either + + var encryptRequestOptions = new Autogenerated.EncryptRequestOptions + { + ComponentName = vaultResourceName, + DataEncryptionCipher = encryptionOptions.EncryptionCipher.GetValueFromEnumMember(), + KeyName = keyName, + KeyWrapAlgorithm = encryptionOptions.KeyWrapAlgorithm.GetValueFromEnumMember(), + OmitDecryptionKeyName = shouldOmitDecryptionKeyName + }; + + if (!shouldOmitDecryptionKeyName) + { + ArgumentVerifier.ThrowIfNullOrEmpty(encryptionOptions.DecryptionKeyName, nameof(encryptionOptions.DecryptionKeyName)); + encryptRequestOptions.DecryptionKeyName = encryptRequestOptions.DecryptionKeyName; + } + + 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); + } + + /// + /// Sends the plaintext bytes in chunks to the sidecar to be encrypted. + /// + private 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); + + //Send the plaintext 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) + { + 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++; + } + } + + //Send the completion message + await duplexStream.RequestStream.CompleteAsync(); + } + + /// + /// Retrieves the encrypted bytes from the encryption operation on the sidecar and returns as an enumerable stream. + /// + private async IAsyncEnumerable> RetrieveEncryptedStreamAsync(AsyncDuplexStreamingCall duplexStream, [EnumeratorCancellation] CancellationToken cancellationToken) + { + await foreach (var encryptResponse in duplexStream.ResponseStream.ReadAllAsync(cancellationToken) + .ConfigureAwait(false)) + { + yield return encryptResponse.Payload.Data.Memory; + } + } + + /// + [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) + { + ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); + ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); + ArgumentVerifier.ThrowIfNull(ciphertextStream, nameof(ciphertextStream)); + ArgumentVerifier.ThrowIfNull(decryptionOptions, nameof(decryptionOptions)); + + var decryptRequestOptions = new Autogenerated.DecryptRequestOptions + { + ComponentName = vaultResourceName, + KeyName = keyName + }; + + var options = CreateCallOptions(headers: null, cancellationToken); + 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); + } + + /// + [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 Task>> DecryptAsync(string vaultResourceName, + Stream ciphertextStream, string keyName, CancellationToken cancellationToken = default) => + DecryptAsync(vaultResourceName, ciphertextStream, keyName, new DecryptionOptions(), + cancellationToken); + + /// + /// Sends the ciphertext bytes in chunks to the sidecar to be decrypted. + /// + private 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); + + //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) + { + 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++; + } + } + + //Send the completion message + await duplexStream.RequestStream.CompleteAsync(); + } + + /// + /// Retrieves the decrypted bytes from the decryption operation on the sidecar and returns as an enumerable stream. + /// + private async IAsyncEnumerable> RetrieveDecryptedStreamAsync( + AsyncDuplexStreamingCall duplexStream, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + await foreach (var decryptResponse in duplexStream.ResponseStream.ReadAllAsync(cancellationToken) + .ConfigureAwait(false)) + { + yield return decryptResponse.Payload.Data.Memory; + } + } + + /// + [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 decryptionOptions, + CancellationToken cancellationToken = default) + { + if (MemoryMarshal.TryGetArray(ciphertextBytes, out var ciphertextSegment) && ciphertextSegment.Array != null) + { + var decryptionResult = await DecryptAsync(vaultResourceName, new MemoryStream(ciphertextSegment.Array), + keyName, decryptionOptions, cancellationToken); + + var bufferedResult = new ArrayBufferWriter(); + await foreach (var item in decryptionResult.WithCancellation(cancellationToken)) + { + bufferedResult.Write(item.Span); + } + + return bufferedResult.WrittenMemory; + } + + throw new ArgumentException("The input instance doesn't have a valid underlying data store", nameof(ciphertextBytes)); + } + + /// + [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, CancellationToken cancellationToken = default) => + await DecryptAsync(vaultResourceName, ciphertextBytes, keyName, + new DecryptionOptions(), cancellationToken); + + #region Subtle Crypto Implementation + + ///// + //[Obsolete("This 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<(string Name, string PublicKey)> GetKeyAsync(string vaultResourceName, string keyName, Autogenerated.SubtleGetKeyRequest.Types.KeyFormat keyFormat, + // CancellationToken cancellationToken = default) + //{ + // ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); + // ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); + + // var envelope = new Autogenerated.SubtleGetKeyRequest() + // { + // ComponentName = vaultResourceName, Format = keyFormat, Name = keyName + // }; + + // var options = CreateCallOptions(headers: null, cancellationToken); + // Autogenerated.SubtleGetKeyResponse response; + + // try + // { + // response = await client.SubtleGetKeyAlpha1Async(envelope, options); + // } + // catch (RpcException ex) + // { + // throw new DaprException( + // "Cryptography operation failed: the Dapr endpoint indicated a failure. See InnerException for details", ex); + // } + + // return (response.Name, response.PublicKey); + //} + + ///// + //[Obsolete("This 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<(byte[] CipherTextBytes, byte[] AuthenticationTag)> EncryptAsync(string vaultResourceName, byte[] plainTextBytes, string algorithm, + // string keyName, byte[] nonce, byte[] associatedData, CancellationToken cancellationToken = default) + //{ + // ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); + // ArgumentVerifier.ThrowIfNullOrEmpty(algorithm, nameof(algorithm)); + // ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); + + // var envelope = new Autogenerated.SubtleEncryptRequest + // { + // ComponentName = vaultResourceName, + // Algorithm = algorithm, + // KeyName = keyName, + // Nonce = ByteString.CopyFrom(nonce), + // Plaintext = ByteString.CopyFrom(plainTextBytes), + // AssociatedData = ByteString.CopyFrom(associatedData) + // }; + + // var options = CreateCallOptions(headers: null, cancellationToken); + // Autogenerated.SubtleEncryptResponse response; + + // try + // { + // response = await client.SubtleEncryptAlpha1Async(envelope, options); + // } + // catch (RpcException ex) + // { + // throw new DaprException( + // "Cryptography operation failed: the Dapr endpoint indicated a failure. See InnerException for details", + // ex); + // } + + // return (response.Ciphertext.ToByteArray(), response.Tag.ToByteArray() ?? Array.Empty()); + //} + + ///// + //[Obsolete("This 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, byte[] cipherTextBytes, string algorithm, string keyName, byte[] nonce, byte[] tag, + // byte[] associatedData, CancellationToken cancellationToken = default) + //{ + // ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); + // ArgumentVerifier.ThrowIfNullOrEmpty(algorithm, nameof(algorithm)); + // ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); + + // var envelope = new Autogenerated.SubtleDecryptRequest + // { + // ComponentName = vaultResourceName, + // Algorithm = algorithm, + // KeyName = keyName, + // Nonce = ByteString.CopyFrom(nonce), + // Ciphertext = ByteString.CopyFrom(cipherTextBytes), + // AssociatedData = ByteString.CopyFrom(associatedData), + // Tag = ByteString.CopyFrom(tag) + // }; + + // var options = CreateCallOptions(headers: null, cancellationToken); + // Autogenerated.SubtleDecryptResponse response; + + // try + // { + // response = await client.SubtleDecryptAlpha1Async(envelope, options); + // } + // catch (RpcException ex) + // { + // throw new DaprException( + // "Cryptography operation failed: the Dapr endpoint included a failure. See InnerException for details", ex); + // } + + // return response.Plaintext.ToByteArray(); + //} + + ///// + //[Obsolete("This 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<(byte[] WrappedKey, byte[] AuthenticationTag)> WrapKeyAsync(string vaultResourceName, byte[] plainTextKey, string keyName, + // string algorithm, byte[] nonce, byte[] associatedData, CancellationToken cancellationToken = default) + //{ + // ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); + // ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); + // ArgumentVerifier.ThrowIfNullOrEmpty(algorithm, nameof(algorithm)); + + // var envelope = new Autogenerated.SubtleWrapKeyRequest + // { + // ComponentName = vaultResourceName, + // Algorithm = algorithm, + // KeyName = keyName, + // Nonce = ByteString.CopyFrom(nonce), + // PlaintextKey = ByteString.CopyFrom(plainTextKey), + // AssociatedData = ByteString.CopyFrom(associatedData) + // }; + + // var options = CreateCallOptions(headers: null, cancellationToken); + // Autogenerated.SubtleWrapKeyResponse response; + + // try + // { + // response = await client.SubtleWrapKeyAlpha1Async(envelope, options); + // } + // catch (RpcException ex) + // { + // throw new DaprException( + // "Cryptography operation failed: the Dapr endpoint included a failure. See InnerException for details", + // ex); + // } + + // return (response.WrappedKey.ToByteArray(), response.Tag.ToByteArray() ?? Array.Empty()); + //} + + ///// + //[Obsolete("This 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 UnwrapKeyAsync(string vaultResourceName, byte[] wrappedKey, string algorithm, + // string keyName, byte[] nonce, byte[] tag, byte[] associatedData, CancellationToken cancellationToken = default) + //{ + // ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); + // ArgumentVerifier.ThrowIfNullOrEmpty(algorithm, nameof(algorithm)); + // ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); + + // var envelope = new Autogenerated.SubtleUnwrapKeyRequest + // { + // ComponentName = vaultResourceName, + // WrappedKey = ByteString.CopyFrom(wrappedKey), + // AssociatedData = ByteString.CopyFrom(associatedData), + // Algorithm = algorithm, + // KeyName = keyName, + // Nonce = ByteString.CopyFrom(nonce), + // Tag = ByteString.CopyFrom(tag) + // }; + + // var options = CreateCallOptions(headers: null, cancellationToken); + // Autogenerated.SubtleUnwrapKeyResponse response; + + // try + // { + // response = await client.SubtleUnwrapKeyAlpha1Async(envelope, options); + // } + // catch (RpcException ex) + // { + // throw new DaprException( + // "Cryptography operation failed: the Dapr endpoint included a failure. See InnerException for details", + // ex); + // } + + // return response.PlaintextKey.ToByteArray(); + //} + + ///// + //[Obsolete("This 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 SignAsync(string vaultResourceName, byte[] digest, string algorithm, string keyName, CancellationToken cancellationToken = default) + //{ + // ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); + // ArgumentVerifier.ThrowIfNullOrEmpty(algorithm, nameof(algorithm)); + // ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); + + // var envelope = new Autogenerated.SubtleSignRequest + // { + // ComponentName = vaultResourceName, + // Digest = ByteString.CopyFrom(digest), + // Algorithm = algorithm, + // KeyName = keyName + // }; + + // var options = CreateCallOptions(headers: null, cancellationToken); + // Autogenerated.SubtleSignResponse response; + + // try + // { + // response = await client.SubtleSignAlpha1Async(envelope, options); + // } + // catch (RpcException ex) + // { + // throw new DaprException( + // "Cryptography operation failed: the Dapr endpoint included a failure. See InnerException for details", + // ex); + // } + + // return response.Signature.ToByteArray(); + //} + + ///// + //[Obsolete("This 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 VerifyAsync(string vaultResourceName, byte[] digest, byte[] signature, + // string algorithm, string keyName, CancellationToken cancellationToken = default) + //{ + // ArgumentVerifier.ThrowIfNullOrEmpty(vaultResourceName, nameof(vaultResourceName)); + // ArgumentVerifier.ThrowIfNullOrEmpty(algorithm, nameof(algorithm)); + // ArgumentVerifier.ThrowIfNullOrEmpty(keyName, nameof(keyName)); + + // var envelope = new Autogenerated.SubtleVerifyRequest + // { + // ComponentName = vaultResourceName, + // Algorithm = algorithm, + // KeyName = keyName, + // Signature = ByteString.CopyFrom(signature), + // Digest = ByteString.CopyFrom(digest) + // }; + + // var options = CreateCallOptions(headers: null, cancellationToken); + // Autogenerated.SubtleVerifyResponse response; + + // try + // { + // response = await client.SubtleVerifyAlpha1Async(envelope, options); + // } + // catch (RpcException ex) + // { + // throw new DaprException( + // "Cryptography operation failed: the Dapr endpoint included a failure. See InnerException for details", + // ex); + // } + + // return response.Valid; + //} + + #endregion + + #endregion #region Distributed Lock API diff --git a/src/Dapr.Client/EnumExtensions.cs b/src/Dapr.Client/EnumExtensions.cs new file mode 100644 index 000000000..6b058ca77 --- /dev/null +++ b/src/Dapr.Client/EnumExtensions.cs @@ -0,0 +1,38 @@ +// ------------------------------------------------------------------------ +// Copyright 2023 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.Reflection; +using System.Runtime.Serialization; + +namespace Dapr.Client +{ + internal static class EnumExtensions + { + /// + /// Reads the value of an enum out of the attached attribute. + /// + /// The enum. + /// The value of the enum to pull the value for. + /// + public static string GetValueFromEnumMember(this T value) where T : Enum + { + var memberInfo = typeof(T).GetMember(value.ToString(), BindingFlags.Static | BindingFlags.Public | BindingFlags.DeclaredOnly); + if (memberInfo.Length <= 0) + return value.ToString(); + + var attributes = memberInfo[0].GetCustomAttributes(typeof(EnumMemberAttribute), false); + return attributes.Length > 0 ? ((EnumMemberAttribute)attributes[0]).Value : value.ToString(); + } + } +} diff --git a/test/Dapr.Client.Test/CryptographyApiTest.cs b/test/Dapr.Client.Test/CryptographyApiTest.cs new file mode 100644 index 000000000..a7d57a096 --- /dev/null +++ b/test/Dapr.Client.Test/CryptographyApiTest.cs @@ -0,0 +1,95 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Xunit; +#pragma warning disable CS0618 // Type or member is obsolete + +namespace Dapr.Client.Test +{ + public class CryptographyApiTest + { + [Fact] + public async Task EncryptAsync_ByteArray_VaultResourceName_ArgumentVerifierException() + { + var client = new DaprClientBuilder().Build(); + const string vaultResourceName = ""; + //Get response and validate + await Assert.ThrowsAsync(async () => await client.EncryptAsync(vaultResourceName, + (ReadOnlyMemory)Array.Empty(), "MyKey", new EncryptionOptions(KeyWrapAlgorithm.Rsa), + CancellationToken.None)); + } + + [Fact] + public async Task EncryptAsync_ByteArray_KeyName_ArgumentVerifierException() + { + var client = new DaprClientBuilder().Build(); + const string keyName = ""; + //Get response and validate + await Assert.ThrowsAsync(async () => await client.EncryptAsync( "myVault", + (ReadOnlyMemory) Array.Empty(), keyName, new EncryptionOptions(KeyWrapAlgorithm.Rsa), CancellationToken.None)); + } + + [Fact] + public async Task EncryptAsync_Stream_VaultResourceName_ArgumentVerifierException() + { + var client = new DaprClientBuilder().Build(); + const string vaultResourceName = ""; + //Get response and validate + await Assert.ThrowsAsync(async () => await client.EncryptAsync(vaultResourceName, + new MemoryStream(), "MyKey", new EncryptionOptions(KeyWrapAlgorithm.Rsa), + CancellationToken.None)); + } + + [Fact] + public async Task EncryptAsync_Stream_KeyName_ArgumentVerifierException() + { + var client = new DaprClientBuilder().Build(); + const string keyName = ""; + //Get response and validate + await Assert.ThrowsAsync(async () => await client.EncryptAsync("myVault", + (Stream) new MemoryStream(), keyName, new EncryptionOptions(KeyWrapAlgorithm.Rsa), + CancellationToken.None)); + } + + [Fact] + public async Task DecryptAsync_ByteArray_VaultResourceName_ArgumentVerifierException() + { + var client = new DaprClientBuilder().Build(); + const string vaultResourceName = ""; + //Get response and validate + await Assert.ThrowsAsync(async () => await client.DecryptAsync(vaultResourceName, + Array.Empty(), "myKey", new DecryptionOptions(), CancellationToken.None)); + } + + [Fact] + public async Task DecryptAsync_ByteArray_KeyName_ArgumentVerifierException() + { + var client = new DaprClientBuilder().Build(); + const string keyName = ""; + //Get response and validate + await Assert.ThrowsAsync(async () => await client.DecryptAsync("myVault", + Array.Empty(), keyName, new DecryptionOptions(), CancellationToken.None)); + } + + [Fact] + public async Task DecryptAsync_Stream_VaultResourceName_ArgumentVerifierException() + { + var client = new DaprClientBuilder().Build(); + const string vaultResourceName = ""; + //Get response and validate + await Assert.ThrowsAsync(async () => await client.DecryptAsync(vaultResourceName, + new MemoryStream(), "MyKey", new DecryptionOptions(), CancellationToken.None)); + } + + [Fact] + public async Task DecryptAsync_Stream_KeyName_ArgumentVerifierException() + { + var client = new DaprClientBuilder().Build(); + const string keyName = ""; + //Get response and validate + await Assert.ThrowsAsync(async () => await client.DecryptAsync("myVault", + new MemoryStream(), keyName, new DecryptionOptions(), CancellationToken.None)); + } + } +} diff --git a/test/Dapr.Client.Test/EnumExtensionTest.cs b/test/Dapr.Client.Test/EnumExtensionTest.cs new file mode 100644 index 000000000..be78c3861 --- /dev/null +++ b/test/Dapr.Client.Test/EnumExtensionTest.cs @@ -0,0 +1,38 @@ +using System.Runtime.Serialization; +using Xunit; + +namespace Dapr.Client.Test +{ + public class EnumExtensionTest + { + [Fact] + public void GetValueFromEnumMember_RedResolvesAsExpected() + { + var value = TestEnum.Red.GetValueFromEnumMember(); + Assert.Equal("red", value); + } + + [Fact] + public void GetValueFromEnumMember_YellowResolvesAsExpected() + { + var value = TestEnum.Yellow.GetValueFromEnumMember(); + Assert.Equal("YELLOW", value); + } + + [Fact] + public void GetValueFromEnumMember_BlueResolvesAsExpected() + { + var value = TestEnum.Blue.GetValueFromEnumMember(); + Assert.Equal("Blue", value); + } + } + + public enum TestEnum + { + [EnumMember(Value="red")] + Red, + [EnumMember(Value="YELLOW")] + Yellow, + Blue + } +}