diff --git a/cryptography/csharp/sdk/README.md b/cryptography/csharp/sdk/README.md new file mode 100644 index 000000000..aabce5fbe --- /dev/null +++ b/cryptography/csharp/sdk/README.md @@ -0,0 +1,73 @@ +# Dapr cryptography + +In this quickstart, you'll create a service that demonstrates how the Cryptography API can be readily utilized in a .NET application. +Our application will perform two operations using an RSA asymmetric key: +- Encrypt and decrypt an in-memory string value +- Encrypt and decrypt a file as a stream + +Visit [this link](https://docs.dapr.io/developing-applications/building-blocks/cryptography/cryptography-overview/) for more information about the Dapr Cryptography API. + +## Generating the key +This sample requires a private RSA key to be generated and placed in the `/keys` directory within the project. +While a key has been generated and distributed with this sample, the following command can be used to generate +your own key if you have OpenSSL installed on your machine: + +```bash +openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:4096 -out keys/rsa-private-key.pem +``` + +> **WARNING: This RSA key is included in this project strictly for demonstration and testing purposes.** +> - Do **NOT** use this key in any production environment or for any real-world applications. +> - This key is publicly available and should be considered compromised. +> - Generating and using your own secure keys is essential for maintaining security in your projects. + +## Run the quickstart +1. Open a new terminal window. Change the directory to that of the `cryptography` .NET project, then launch Dapr and +2. the service with the following command: + + + +```bash +dapr run --app-id crypto --resources-path "../../../components/" -- dotnet run +``` + +The terminal console should show standard Dapr startup logs, and then start your application showing the +output similar to the following: + +```text +Dapr sidecar is up and running. +Updating metadata for appPID: 12908 +Updating metadata for app command: dotnet run +You're up and running! Both Dapr and your app logs will appear here. +== APP == info: cryptography.StringCryptographyOperations[1125053159] +== APP == Encrypting string with key rsa-private-key.pem with plaintext value 'P@assw0rd' +== APP == info: cryptography.StringCryptographyOperations[644187503] +== APP == Decrypted string with key rsa-private-key.pem with plaintext value 'P@assw0rd' +== APP == info: Program[1815579667] +== APP == Encrypted string from plaintext value 'P@assw0rd' and decrypted to 'P@assw0rd' +== APP == info: cryptography.StreamCryptographyOperations[855741432] +== APP == Encrypted file '***\AppData\Local\Temp\tmp44plip.tmp' spanning 1604 bytes +== APP == info: cryptography.StreamCryptographyOperations[880999840] +== APP == Decrypting in-memory bytes to file '***\AppData\Local\Temp\tmpx23nes.tmp' +== APP == info: Program[1262437064] +== APP == Encrypted from file stream '***\AppData\Local\Temp\tmp44plip.tmp' and decrypted back from an in-memory stream to a file '***\AppData\Local\Temp\tmpx23nes.tmp' and the validation check returns 'True' +Exited App successfully + +terminated signal received: shutting down +Exited Dapr successfully +``` diff --git a/cryptography/csharp/sdk/cryptography/Program.cs b/cryptography/csharp/sdk/cryptography/Program.cs new file mode 100644 index 000000000..1bf5a7904 --- /dev/null +++ b/cryptography/csharp/sdk/cryptography/Program.cs @@ -0,0 +1,106 @@ +using System.Security.Cryptography; +using cryptography; +using Microsoft.Extensions.DependencyInjection.Extensions; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddDaprClient(); +builder.Services.TryAddSingleton(); +builder.Services.TryAddSingleton(); + +var app = builder.Build(); + +var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(30)); +var logger = app.Services.GetRequiredService>(); + +//Encrypt a string value +var stringOps = app.Services.GetRequiredService(); +const string plaintextValue = "P@assw0rd"; +var encryptedBase64String = await stringOps.EncryptAsync(plaintextValue, cancellationTokenSource.Token); +var decryptedBase64String = await stringOps.DecryptAsync(encryptedBase64String, cancellationTokenSource.Token); +Log.LogStringEncryption(logger, plaintextValue, decryptedBase64String); + +//Encrypt a file +var testFilePath = await FileGenerator.GenerateSmallTestFileAsync(cancellationTokenSource.Token); +var streamOps = app.Services.GetRequiredService(); +var encryptedFileBytes = await streamOps.EncryptAsync(testFilePath, cancellationTokenSource.Token); + +using var encryptedMs = new MemoryStream(Convert.FromBase64String(encryptedFileBytes)); +var decryptedFilePath = Path.GetTempFileName(); +await streamOps.DecryptAsync(encryptedMs, decryptedFilePath, cancellationTokenSource.Token); +var areIdentical = await FileValidator.AreIdentical(testFilePath, decryptedFilePath); +Log.LogStreamEncryption(logger, testFilePath, decryptedFilePath, areIdentical); + +//Clean up the created files +File.Delete(testFilePath); +File.Delete(decryptedFilePath); + +static partial class Log +{ + //It should go without saying that you should not log your plaintext values in production - this is for + //demonstration purposes only. + [LoggerMessage(LogLevel.Information, "Encrypted string from plaintext value '{plaintextValue}' and decrypted to '{decryptedValue}'")] + internal static partial void LogStringEncryption(ILogger logger, string plaintextValue, string decryptedValue); + + [LoggerMessage(LogLevel.Information, "Encrypted from file stream '{plaintextFilePath}' and decrypted back from an in-memory stream to a file '{decryptedFilePath}' and the validation check returns '{areIdentical}'")] + internal static partial void LogStreamEncryption(ILogger logger, string plaintextFilePath, string decryptedFilePath, bool areIdentical); +} + +static class Constants +{ + public const string ComponentName = "localstorage"; + public const string KeyName = "rsa-private-key.pem"; +} + +static class FileGenerator +{ + public static async Task GenerateSmallTestFileAsync(CancellationToken cancellationToken) + { + var tempFilePath = Path.GetTempFileName(); + await File.WriteAllTextAsync(tempFilePath, """ + # 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. + """, cancellationToken); + + return tempFilePath; + } +} + +static class FileValidator +{ + public static async Task AreIdentical(string path1, string path2) + { + await using var path1Reader = new FileStream(path1, FileMode.Open); + await using var path2Reader = new FileStream(path2, FileMode.Open); + + using var md5 = MD5.Create(); + var file1Hash = await md5.ComputeHashAsync(path1Reader); + var file2Hash = await md5.ComputeHashAsync(path2Reader); + + return file1Hash.SequenceEqual(file2Hash); + } +} \ No newline at end of file diff --git a/cryptography/csharp/sdk/cryptography/StreamCryptographyOperations.cs b/cryptography/csharp/sdk/cryptography/StreamCryptographyOperations.cs new file mode 100644 index 000000000..a1cee932d --- /dev/null +++ b/cryptography/csharp/sdk/cryptography/StreamCryptographyOperations.cs @@ -0,0 +1,48 @@ +using System.Buffers; +using Dapr.Client; + +namespace cryptography; + +internal sealed partial class StreamCryptographyOperations(DaprClient daprClient, ILogger logger) +{ + public async Task EncryptAsync( + string filePath, + CancellationToken cancellationToken) + { + await using var sourceFile = new FileStream(filePath, FileMode.Open); + var bufferedEncryptedBytes = new ArrayBufferWriter(); + await foreach (var bytes in (await daprClient.EncryptAsync(Constants.ComponentName, sourceFile, + Constants.KeyName, new EncryptionOptions(KeyWrapAlgorithm.Rsa), cancellationToken)) + .WithCancellation(cancellationToken)) + { + bufferedEncryptedBytes.Write(bytes.Span); + } + + sourceFile.Close(); + + LogEncryptFile(logger, filePath, bufferedEncryptedBytes.WrittenMemory.Span.Length); + + var base64String = Convert.ToBase64String(bufferedEncryptedBytes.WrittenMemory.Span); + return base64String; + } + + public async Task DecryptAsync(MemoryStream encryptedBytes, string decryptedFilePath, CancellationToken cancellationToken) + { + await using var decryptedFile = new FileStream(decryptedFilePath, FileMode.Create); + await foreach (var bytes in (await daprClient.DecryptAsync(Constants.ComponentName, encryptedBytes, + Constants.KeyName, cancellationToken)) + .WithCancellation(cancellationToken)) + { + await decryptedFile.WriteAsync(bytes, cancellationToken); + } + + LogDecryptFile(logger, decryptedFilePath); + decryptedFile.Close(); + } + + [LoggerMessage(LogLevel.Information, "Encrypted file '{filePath}' spanning {encryptedByteCount} bytes")] + static partial void LogEncryptFile(ILogger logger, string filePath, int encryptedByteCount); + + [LoggerMessage(LogLevel.Information, "Decrypting in-memory bytes to file '{filePath}'")] + static partial void LogDecryptFile(ILogger logger, string filePath); +} \ No newline at end of file diff --git a/cryptography/csharp/sdk/cryptography/StringCryptographyOperations.cs b/cryptography/csharp/sdk/cryptography/StringCryptographyOperations.cs new file mode 100644 index 000000000..2fa3ee6d0 --- /dev/null +++ b/cryptography/csharp/sdk/cryptography/StringCryptographyOperations.cs @@ -0,0 +1,31 @@ +using System.Text; +using Dapr.Client; + +namespace cryptography; + +internal sealed partial class StringCryptographyOperations(DaprClient daprClient, ILogger logger) +{ + public async Task EncryptAsync(string plaintextValue, CancellationToken cancellationToken) + { + var plaintextBytes = Encoding.UTF8.GetBytes(plaintextValue); + var encryptedBytes = await daprClient.EncryptAsync(Constants.ComponentName, plaintextBytes, Constants.KeyName, new EncryptionOptions(KeyWrapAlgorithm.Rsa), cancellationToken); + LogEncryptionOperation(logger, Constants.KeyName, plaintextValue); + return Convert.ToBase64String(encryptedBytes.Span); + } + + public async Task DecryptAsync(string base64EncryptedValue, CancellationToken cancellationToken) + { + var ciphertextBytes = Convert.FromBase64String(base64EncryptedValue); + var decryptedBytes = + await daprClient.DecryptAsync(Constants.ComponentName, ciphertextBytes, Constants.KeyName, cancellationToken); + var plaintextValue = Encoding.UTF8.GetString(decryptedBytes.Span); + LogDecryptionOperation(logger, Constants.KeyName, plaintextValue); + return plaintextValue; + } + + [LoggerMessage(LogLevel.Information, "Encrypting string with key {keyName} with plaintext value '{plaintextValue}'")] + static partial void LogEncryptionOperation(ILogger logger, string keyName, string plaintextValue); + + [LoggerMessage(LogLevel.Information, "Decrypted string with key {keyName} with plaintext value '{plaintextValue}'")] + static partial void LogDecryptionOperation(ILogger logger, string keyName, string plaintextValue); +} \ No newline at end of file diff --git a/cryptography/csharp/sdk/cryptography/cryptography.csproj b/cryptography/csharp/sdk/cryptography/cryptography.csproj new file mode 100644 index 000000000..b59c55323 --- /dev/null +++ b/cryptography/csharp/sdk/cryptography/cryptography.csproj @@ -0,0 +1,14 @@ + + + + Exe + net9.0 + enable + enable + + + + + + + diff --git a/cryptography/csharp/sdk/cryptography/keys/rsa-private-key.pem b/cryptography/csharp/sdk/cryptography/keys/rsa-private-key.pem new file mode 100644 index 000000000..f4508f7ae --- /dev/null +++ b/cryptography/csharp/sdk/cryptography/keys/rsa-private-key.pem @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +dV1dhNiQEnqs4vDBVn40dnTKSSG2 +-----END PRIVATE KEY----- diff --git a/cryptography/csharp/sdk/makefile b/cryptography/csharp/sdk/makefile new file mode 100644 index 000000000..f458626ba --- /dev/null +++ b/cryptography/csharp/sdk/makefile @@ -0,0 +1,2 @@ +include ../../../docker.mk +include ../../../validate.mk \ No newline at end of file