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

Implement Cryptography API #144

Merged
merged 19 commits into from
Mar 25, 2024
Merged
Show file tree
Hide file tree
Changes from 14 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
4 changes: 2 additions & 2 deletions .github/workflows/validate-examples.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ on:
required: false
default: ""
repository_dispatch:
types: [validate-examples]
types: [ validate-examples ]
merge_group:
jobs:
setup:
Expand Down Expand Up @@ -144,7 +144,7 @@ jobs:
fail-fast: false
matrix:
examples:
["actors", "client", "configuration", "invoke/grpc", "invoke/grpc-proxying", "pubsub", "secrets-bulk"]
[ "actors", "client", "configuration", "crypto", "invoke/grpc", "invoke/grpc-proxying", "pubsub", "secrets-bulk" ]
steps:
- name: Check out code
uses: actions/checkout@v4
Expand Down
9 changes: 7 additions & 2 deletions Cargo.toml
zedgell marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
axum = "0.7.4"
tokio = { version = "1.29", features = ["sync"] }
tokio-util = { version = "0.7.10", features = ["io"] }
chrono = "0.4.24"

[build-dependencies]
Expand All @@ -45,13 +46,17 @@ path = "examples/actors/client.rs"
name = "actor-server"
path = "examples/actors/server.rs"

[[example]]
name = "client"
path = "examples/client/client.rs"

[[example]]
name = "configuration"
path = "examples/configuration/main.rs"

[[example]]
name = "client"
path = "examples/client/client.rs"
name = "crypto"
path = "examples/crypto/main.rs"

[[example]]
name = "invoke-grpc-client"
Expand Down
48 changes: 48 additions & 0 deletions examples/crypto/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Crypto Example

This is a simple example that demonstrates Dapr's Cryptography capabilities.

> **Note:** Make sure to use latest version of proto bindings.

## Running

To run this example:

1. Generate keys in examples/crypto/keys directory:
<!-- STEP
name: Generate keys
background: false
sleep: 5
timeout_seconds: 30
-->
```bash
mkdir -p keys
# Generate a private RSA key, 4096-bit keys
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:4096 -out keys/rsa-private-key.pem
# Generate a 256-bit key for AES
openssl rand -out keys/symmetric-key-256 32
```

<!-- END_STEP -->

2. Run the multi-app run template:

<!-- STEP
name: Run multi-app
output_match_mode: substring
match_order: none
expected_stdout_lines:
- '== APP - crypto-example == Successfully Decrypted String'
- '== APP - crypto-example == Successfully Decrypted Image'
background: true
sleep: 30
timeout_seconds: 90
-->

```bash
dapr run -f .
```

<!-- END_STEP -->

2. Stop with `ctrl + c`
11 changes: 11 additions & 0 deletions examples/crypto/components/local-storage.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: localstorage
spec:
type: crypto.dapr.localstorage
version: v1
metadata:
- name: path
# Path is relative to the folder where the example is located
value: ./keys
10 changes: 10 additions & 0 deletions examples/crypto/dapr.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
version: 1
common:
daprdLogDestination: console
apps:
- appID: crypto-example
appDirPath: ./
daprGRPCPort: 35002
logLevel: debug
command: [ "cargo", "run", "--example", "crypto" ]
resourcesPath: ./components
Binary file added examples/crypto/image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
82 changes: 82 additions & 0 deletions examples/crypto/main.rs
zedgell marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
use std::fs;

use tokio::fs::File;
use tokio::time::sleep;

use dapr::client::ReaderStream;
use dapr::dapr::dapr::proto::runtime::v1::{DecryptRequestOptions, EncryptRequestOptions};
zedgell marked this conversation as resolved.
Show resolved Hide resolved

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
sleep(std::time::Duration::new(2, 0)).await;
let port: u16 = std::env::var("DAPR_GRPC_PORT")?.parse()?;
let addr = format!("https://127.0.0.1:{}", port);

let mut client = dapr::Client::<dapr::client::TonicClient>::connect(addr).await?;

let encrypted = client
.encrypt(
ReaderStream::new("Test".as_bytes()),
EncryptRequestOptions {
component_name: "localstorage".to_string(),
key_name: "rsa-private-key.pem".to_string(),
key_wrap_algorithm: "RSA".to_string(),
data_encryption_cipher: "aes-gcm".to_string(),
omit_decryption_key_name: false,
decryption_key_name: "rsa-private-key.pem".to_string(),
},
)
.await
.unwrap();

let decrypted = client
.decrypt(
encrypted,
DecryptRequestOptions {
zedgell marked this conversation as resolved.
Show resolved Hide resolved
component_name: "localstorage".to_string(),
key_name: "rsa-private-key.pem".to_string(),
},
)
.await
.unwrap();

assert_eq!(String::from_utf8(decrypted).unwrap().as_str(), "Test");

println!("Successfully Decrypted String");

let image = File::open("./image.png").await.unwrap();

let encrypted = client
.encrypt(
ReaderStream::new(image),
EncryptRequestOptions {
zedgell marked this conversation as resolved.
Show resolved Hide resolved
component_name: "localstorage".to_string(),
key_name: "rsa-private-key.pem".to_string(),
key_wrap_algorithm: "RSA".to_string(),
data_encryption_cipher: "aes-gcm".to_string(),
omit_decryption_key_name: false,
decryption_key_name: "rsa-private-key.pem".to_string(),
},
)
.await
.unwrap();

let decrypted = client
.decrypt(
encrypted,
DecryptRequestOptions {
zedgell marked this conversation as resolved.
Show resolved Hide resolved
component_name: "localstorage".to_string(),
key_name: "rsa-private-key.pem".to_string(),
},
)
.await
.unwrap();

let image = fs::read("./image.png").unwrap();

assert_eq!(decrypted, image);

println!("Successfully Decrypted Image");

Ok(())
}
157 changes: 152 additions & 5 deletions src/client.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
use crate::dapr::dapr::proto::{common::v1 as common_v1, runtime::v1 as dapr_v1};
use prost_types::Any;
use std::collections::HashMap;
use tonic::Streaming;
use tonic::{transport::Channel as TonicChannel, Request};

use crate::error::Error;
use async_trait::async_trait;
use futures::StreamExt;
use prost_types::Any;
use serde::{Deserialize, Serialize};
use tokio::io::AsyncRead;
use tonic::codegen::tokio_stream;
use tonic::{transport::Channel as TonicChannel, Request};
use tonic::{Status, Streaming};

use crate::dapr::dapr::proto::{common::v1 as common_v1, runtime::v1 as dapr_v1};
use crate::error::Error;

#[derive(Clone)]
pub struct Client<T>(T);
Expand Down Expand Up @@ -379,6 +383,78 @@ impl<T: DaprInterface> Client<T> {
};
self.0.unsubscribe_configuration(request).await
}

/// Encrypt binary data using Dapr. returns Vec<StreamPayload> to be used in decrypt method
///
/// # Arguments
///
/// * `payload` - ReaderStream to the data to encrypt
/// * `request_option` - Encryption request options.
pub async fn encrypt<R>(
&mut self,
payload: ReaderStream<R>,
request_options: EncryptRequestOptions,
) -> Result<Vec<StreamPayload>, Status>
where
R: AsyncRead + Send,
{
// have to have it as a reference for the async move below
let request_options = &Some(request_options);
let requested_items: Vec<EncryptRequest> = payload
.0
.enumerate()
.fold(vec![], |mut init, (i, bytes)| async move {
let stream_payload = StreamPayload {
data: bytes.unwrap().to_vec(),
seq: 0,
};
if i == 0 {
init.push(EncryptRequest {
options: request_options.clone(),
payload: Some(stream_payload),
});
} else {
init.push(EncryptRequest {
options: None,
payload: Some(stream_payload),
});
}
init
})
.await;
self.0.encrypt(requested_items).await
}

/// Decrypt binary data using Dapr. returns Vec<u8>.
///
/// # Arguments
///
/// * `encrypted` - Encrypted data usually returned from encrypted, Vec<StreamPayload>
/// * `options` - Decryption request options.
pub async fn decrypt(
&mut self,
encrypted: Vec<StreamPayload>,
options: DecryptRequestOptions,
) -> Result<Vec<u8>, Status> {
let requested_items: Vec<DecryptRequest> = encrypted
.iter()
.enumerate()
.map(|(i, item)| {
if i == 0 {
DecryptRequest {
options: Some(options.clone()),
payload: Some(item.clone()),
}
} else {
DecryptRequest {
options: None,
payload: Some(item.clone()),
}
}
})
.collect();
self.0.decrypt(requested_items).await
}
}

#[async_trait]
Expand Down Expand Up @@ -420,6 +496,11 @@ pub trait DaprInterface: Sized {
&mut self,
request: UnsubscribeConfigurationRequest,
) -> Result<UnsubscribeConfigurationResponse, Error>;

async fn encrypt(&mut self, payload: Vec<EncryptRequest>)
-> Result<Vec<StreamPayload>, Status>;

async fn decrypt(&mut self, payload: Vec<DecryptRequest>) -> Result<Vec<u8>, Status>;
}

#[async_trait]
Expand Down Expand Up @@ -535,6 +616,51 @@ impl DaprInterface for dapr_v1::dapr_client::DaprClient<TonicChannel> {
.await?
.into_inner())
}

/// Encrypt binary data using Dapr. returns Vec<StreamPayload> to be used in decrypt method
///
/// # Arguments
///
/// * `payload` - ReaderStream to the data to encrypt
/// * `request_option` - Encryption request options.
async fn encrypt(
&mut self,
request: Vec<EncryptRequest>,
) -> Result<Vec<StreamPayload>, Status> {
let request = Request::new(tokio_stream::iter(request));
let stream = self.encrypt_alpha1(request).await?;
let mut stream = stream.into_inner();
let mut return_data = vec![];
while let Some(resp) = stream.next().await {
if let Ok(resp) = resp {
if let Some(data) = resp.payload {
return_data.push(data)
}
}
}
Ok(return_data)
}

/// Decrypt binary data using Dapr. returns Vec<u8>.
///
/// # Arguments
///
/// * `encrypted` - Encrypted data usually returned from encrypted, Vec<StreamPayload>
/// * `options` - Decryption request options.
async fn decrypt(&mut self, request: Vec<DecryptRequest>) -> Result<Vec<u8>, Status> {
let request = Request::new(tokio_stream::iter(request));
let stream = self.decrypt_alpha1(request).await?;
let mut stream = stream.into_inner();
let mut data = vec![];
while let Some(resp) = stream.next().await {
if let Ok(resp) = resp {
if let Some(mut payload) = resp.payload {
data.append(payload.data.as_mut())
}
}
}
Ok(data)
}
}

/// A request from invoking a service
Expand Down Expand Up @@ -614,6 +740,19 @@ pub type UnsubscribeConfigurationResponse = dapr_v1::UnsubscribeConfigurationRes
/// A tonic based gRPC client
pub type TonicClient = dapr_v1::dapr_client::DaprClient<TonicChannel>;

/// Encryption gRPC request
type EncryptRequest = crate::dapr::dapr::proto::runtime::v1::EncryptRequest;

/// Decrypt gRPC request
type DecryptRequest = crate::dapr::dapr::proto::runtime::v1::DecryptRequest;

/// Encryption request options
type EncryptRequestOptions = crate::dapr::dapr::proto::runtime::v1::EncryptRequestOptions;

/// Decryption request options
type DecryptRequestOptions = crate::dapr::dapr::proto::runtime::v1::DecryptRequestOptions;

type StreamPayload = crate::dapr::dapr::proto::common::v1::StreamPayload;
impl<K> From<(K, Vec<u8>)> for common_v1::StateItem
zedgell marked this conversation as resolved.
Show resolved Hide resolved
where
K: Into<String>,
Expand All @@ -626,3 +765,11 @@ where
}
}
}

pub struct ReaderStream<T>(tokio_util::io::ReaderStream<T>);

impl<T: AsyncRead> ReaderStream<T> {
pub fn new(data: T) -> Self {
ReaderStream(tokio_util::io::ReaderStream::new(data))
}
}
Loading
Loading