Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Dapr Crypto .NET Quickstart #1132

Open
wants to merge 6 commits into
base: release-1.15
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions cryptography/csharp/sdk/README.md
Original file line number Diff line number Diff line change
@@ -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:

<!-- STEP
name: Run crypto service
expected-stdout_lines:
- 'Starting Dapr with id crypto. HTTP Port:'
- '== APP == Encrypting string with key rsa-private-key.pem with plaintext value 'P@assw0rd''
- '== APP == Decrypted string with key rsa-private-key.pem with plaintext value 'P@assw0rd''
- '== APP == Encrypted string from plaintext value 'P@assw0rd' and decrypted to 'P@assw0rd''
- '== APP == Encrypted file ''
- '== APP == Decrypting in-memory bytes to file ''
- 'and the validation check returns 'True''
output_match_mode: substring
match_order: none
background: true
sleep: 15
timeout_seconds: 60
-->

```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
```
106 changes: 106 additions & 0 deletions cryptography/csharp/sdk/cryptography/Program.cs
Original file line number Diff line number Diff line change
@@ -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<StringCryptographyOperations>();
builder.Services.TryAddSingleton<StreamCryptographyOperations>();

var app = builder.Build();

var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(30));
var logger = app.Services.GetRequiredService<ILogger<Program>>();

//Encrypt a string value
var stringOps = app.Services.GetRequiredService<StringCryptographyOperations>();
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<StreamCryptographyOperations>();
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<string> 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<bool> 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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using System.Buffers;
using Dapr.Client;

namespace cryptography;

internal sealed partial class StreamCryptographyOperations(DaprClient daprClient, ILogger<StreamCryptographyOperations> logger)
{
public async Task<string> EncryptAsync(
string filePath,
CancellationToken cancellationToken)
{
await using var sourceFile = new FileStream(filePath, FileMode.Open);
var bufferedEncryptedBytes = new ArrayBufferWriter<byte>();
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);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using System.Text;
using Dapr.Client;

namespace cryptography;

internal sealed partial class StringCryptographyOperations(DaprClient daprClient, ILogger<StringCryptographyOperations> logger)
{
public async Task<string> 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<string> 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);
}
14 changes: 14 additions & 0 deletions cryptography/csharp/sdk/cryptography/cryptography.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Dapr.AspNetCore" Version="1.15.0-rc02" />
</ItemGroup>

</Project>
52 changes: 52 additions & 0 deletions cryptography/csharp/sdk/cryptography/keys/rsa-private-key.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
-----BEGIN PRIVATE KEY-----
MIIJQQIBADANBgkqhkiG9w0BAQEFAASCCSswggknAgEAAoICAQC0URLpxZCqDv7S
WfROh2Kei4VCEayNu/TK3NaD/QlIpip1rrsPKgTfTOZoRmkmG0Qj59srEJi2GEhL
xpjvRQpA/C/OS+KELU8AeGrqHw7uN/a99NkoAr+zYDCyY9yckPeC5wGxc0/Q6HQT
mWp+YcpR9wFO0PmTVlObssibagjjRNX7z/ZosecOOqjnAqlnYoHMavvoCD5fxM7y
cm7so0JWooXwVaZKgehBEBg1W5F0q5e9ssAQk3lY6IUd5sOskiylTNf/+3r1JU0j
YM8ik3a1/dyDALVXpLSfz7FM9VEj4QjiPF4UuXeBHPDFFiKWbiKfbjqvZ2Sz7Gl7
c5rTk1Fozpr70E/wihrrv22Mxs0sEPdtemQgHXroQfRW8K4FhI0WHs7tR2gVxLHu
OAU9LzCngz4yITh1eixVDmm/B5ZtNVrTQmaY84vGqhrFp+asyFNiXbhUAcT7D/q6
w/c4aQ635ntCFSPYpWvhKqrqVDsoanD/5AWfc3+6Ek2/GVMyEQq+9tnCMM10EVSX
8PsoAWHESDFude5zkHzn7IKy8mh6lfheEbBI5zN9z7WGexyiBgljmyUHXx6Pd8Uc
yxpLRm94kynkDXD9SapQLzXmz+D+X/OYeADMIDWlbdXiIb1+2Q62H1lo6n10KVP7
oEr8BHvcMFY89kwK4lKscUupn8xkzwIDAQABAoICACDuu78Rc8Hzeivt/PZIuMTP
I5f1BWhffy571fwGP2dS3edfcc+rs3cbIuvBjFvG2BOcuYUsg0+isLWSQIVWvTAw
PwT1DBpq8gZad+Bpqr7sXrbD3NN3aQ64TzyNi5HW0jXIviDsOBQmGGkp+G67qol8
zPLZrPNxbVS++u+Tlqr3fAOBMHZfo50QLp/+dvUoYx90HKz8sHOqTMewCb1Tdf6/
sSm7YuMxxbr4VwuLvU2rN0wQtQ5x+NQ5p3JWHr/KdLf+CGc6xXK3jNaczEf62dAU
XO1aOESZEtorQy0Ukuy0IXy8XMx5MS/WGs1MJSYHWHB43+QARL6tu3guHYVt3wyv
W6YTglQsSKc6uuK4JTZOx1VYZjjnSdeY/xiUmZGYp4ZiC9p8b9NvXmZT2EwqhCVt
4OTcX4lkwGAsKcoEdLHi0K5CbBfYJsRgVVheDjP0xUFjCJCYqfqo2rE5YMXMTeY7
clYEOXKGxwuy1Iu8nKqtWAV5r/eSmXBdxBqEBW9oxJfnnwNPG+yOk0Qkd1vaRj00
mdKCOjgB2fOuPX2JRZ2z41Cem3gqhH0NQGrx3APV4egGrYAMClasgtZkUeUOIgK5
xLlC/6svuHNyKXAKFpOubEy1FM8jz7111eNHxHRDP3+vH3u4CfAD2Sl+VDZdg51i
WmVpT+B/DrnlHVSP2/XNAoIBAQD7F49oSdveKuO/lAyqkE9iF61i09G0b0ouDGUI
qx+pd5/8vUcqi4upCxz+3AqMPWZRIqOyo8EUP7f4rSJrXn8U2SwnFfi4k2jiqmEA
Wr0b8z5P1q5MH6BtVDa0Sr1R8xI9s3UgIs4pUKgBoQu9+U4Du4NSucQFcea8nIVY
lLCqQcRhz8bCJPCNuHay5c77kK3Te197KPMasNurTNMOJcPMG95CZLB8Clf4A+pw
fixvA1/fE4mFo1L7Ymxoz5lFYVWOTY9hh50Kqz57wxw4laU4ii+MaJj+YHuNR83N
cO6FztUYKMR8BPgtl3/POTHTofSg7eIOiUYwcfRr6jbMWlsDAoIBAQC311xiMpho
Hvdcvp3/urrIp2QhdD05n6TnZOPkpnd9kwGku2RA+occDQOg/BzADVwJaR/aE97F
jbfRlfBesTZlUec0EwjKIFbeYh+QS/RmjQe9zpPQWMo1M7y0fMWU+yXRUcNBpcuy
R6KlphK0k4xFkIAdC3QHmJQ0XvOpqvrhFy3i/Prc5Wlg29FYBBTAF0WZCZ4uCG34
D0eG0CNaf8w9g9ClbU6nGLBCMcgjEOPYfyrJaedM+jXennLDPG6ySytrGwnwLAQc
Okx+SrIiNHUpQGKteT88Kdpgo3F4KUX/pm84uGdxrOpDS7L0T9/G4CbjzCe1nHeS
fJJsw5JN+Z9FAoIBAGn5S6FsasudtnnI9n+WYKq564fmdn986QX+XTYHY1mXD4MQ
L9UZCFzUP+yg2iLOVzyvLf/bdUYijnb6O6itPV2DO0tTzqG4NXBVEJOhuGbvhsET
joS6ZG9AN8ZoNPc9a9l2wFxL1E9Dp2Ton5gSfIa+wXJMzRqvM/8u4Gi+eMGi+Et/
8hdGl/B4hkCDFZS/P14el/HXGqONOWlXB0zVS4n9yRSkgogXpYEbxfqshfxkpDX2
fPhWMlO++ppR5BKQPhfNTFKRdgpms/xwIJ0RK6ZtTBwqmUfjWMIMKCQpIcJ/xRhp
PGRLhKNZaawAK7Nyi1jQjbQs497WeZ6CP5aIHBkCggEALHyl83FQ5ilQLJZH/6E9
H9854MqTIkWajxAgAa2yzqVrSWS7XuoBFe2kSimX/3V8Jx7UQV57kwy3RbVl5FQ3
2I7YRwawItFulAPkpXNr4gEQtYKuzEUgMX2ilX54BZQ804lYmaM4Rp0FI9arQh1O
XWsZRW4HFut6Oa4cgptIeH22ce5L+nZdaL3oy8a5Cr7W7bChIXySt+tioKHvXC/+
yYgDTnTECrVzuaD4UFv+9t3XCcRh34PQ010+YjZWhzifehyh7AeKuxX0er8ymgpd
q6zT9CyZ+8IZATer9qruMG4jDfO5vI1eZwiDdpF5klOdtZQqq80ANmeEu2McHVhh
jQKCAQBbohPxMb3QYdukGp8IsIF04GfnTgaDbRgl4KeUyzdBN3nzvCKK0HDluptR
4Ua64JksGG24gsTBy6yuQoGRCG0LJe0Ty3TRRnvZ8MpADoNMObspMSC8n8kk6ps+
SoG1U9t6HYlIgQagvTc7mTmCmwYX1zlCoZp24yz5pDkKxqoPFDtrGlXxeUgOhpDT
Mzi+DNTz9sH9vod4ibQiOseUxITwQpXHTJVrtNfvva6xjlhq+GGCuKIUwkUKOvBC
ds7SR9demn69aWCyzXqD1cTnmxtn6bNPukwowg7a07ieUyKftcJ1icOWQ/bdQkEf
dV1dhNiQEnqs4vDBVn40dnTKSSG2
-----END PRIVATE KEY-----
2 changes: 2 additions & 0 deletions cryptography/csharp/sdk/makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
include ../../../docker.mk
include ../../../validate.mk
Loading
Loading