diff --git a/.github/workflows/validate-examples.yml b/.github/workflows/validate-examples.yml index 0f79637..e6e461f 100644 --- a/.github/workflows/validate-examples.yml +++ b/.github/workflows/validate-examples.yml @@ -22,7 +22,7 @@ on: required: false default: "" repository_dispatch: - types: [validate-examples] + types: [ validate-examples ] merge_group: jobs: setup: @@ -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 diff --git a/Cargo.toml b/Cargo.toml index 6d72c75..c871b3e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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] @@ -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" diff --git a/examples/crypto/README.md b/examples/crypto/README.md new file mode 100644 index 0000000..7b00a5c --- /dev/null +++ b/examples/crypto/README.md @@ -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: + +```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 +``` + + + +2. Run the multi-app run template: + + + +```bash +dapr run -f . +``` + + + +2. Stop with `ctrl + c` diff --git a/examples/crypto/components/local-storage.yml b/examples/crypto/components/local-storage.yml new file mode 100644 index 0000000..a678ef2 --- /dev/null +++ b/examples/crypto/components/local-storage.yml @@ -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 \ No newline at end of file diff --git a/examples/crypto/dapr.yaml b/examples/crypto/dapr.yaml new file mode 100644 index 0000000..91243ee --- /dev/null +++ b/examples/crypto/dapr.yaml @@ -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 \ No newline at end of file diff --git a/examples/crypto/image.png b/examples/crypto/image.png new file mode 100644 index 0000000..7bdad7f Binary files /dev/null and b/examples/crypto/image.png differ diff --git a/examples/crypto/main.rs b/examples/crypto/main.rs new file mode 100644 index 0000000..8dab74c --- /dev/null +++ b/examples/crypto/main.rs @@ -0,0 +1,81 @@ +use std::fs; + +use tokio::fs::File; +use tokio::time::sleep; + +use dapr::client::ReaderStream; + +#[tokio::main] +async fn main() -> Result<(), Box> { + 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::::connect(addr).await?; + + let encrypted = client + .encrypt( + ReaderStream::new("Test".as_bytes()), + dapr::client::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, + dapr::client::DecryptRequestOptions { + 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), + dapr::client::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, + dapr::client::DecryptRequestOptions { + 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(()) +} diff --git a/src/client.rs b/src/client.rs index 6c9e3ec..712280e 100644 --- a/src/client.rs +++ b/src/client.rs @@ -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); @@ -379,6 +383,78 @@ impl Client { }; self.0.unsubscribe_configuration(request).await } + + /// Encrypt binary data using Dapr. returns Vec to be used in decrypt method + /// + /// # Arguments + /// + /// * `payload` - ReaderStream to the data to encrypt + /// * `request_option` - Encryption request options. + pub async fn encrypt( + &mut self, + payload: ReaderStream, + request_options: EncryptRequestOptions, + ) -> Result, 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 = 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. + /// + /// # Arguments + /// + /// * `encrypted` - Encrypted data usually returned from encrypted, Vec + /// * `options` - Decryption request options. + pub async fn decrypt( + &mut self, + encrypted: Vec, + options: DecryptRequestOptions, + ) -> Result, Status> { + let requested_items: Vec = 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] @@ -420,6 +496,11 @@ pub trait DaprInterface: Sized { &mut self, request: UnsubscribeConfigurationRequest, ) -> Result; + + async fn encrypt(&mut self, payload: Vec) + -> Result, Status>; + + async fn decrypt(&mut self, payload: Vec) -> Result, Status>; } #[async_trait] @@ -535,6 +616,51 @@ impl DaprInterface for dapr_v1::dapr_client::DaprClient { .await? .into_inner()) } + + /// Encrypt binary data using Dapr. returns Vec 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, + ) -> Result, 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. + /// + /// # Arguments + /// + /// * `encrypted` - Encrypted data usually returned from encrypted, Vec + /// * `options` - Decryption request options. + async fn decrypt(&mut self, request: Vec) -> Result, 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 @@ -614,6 +740,19 @@ pub type UnsubscribeConfigurationResponse = dapr_v1::UnsubscribeConfigurationRes /// A tonic based gRPC client pub type TonicClient = dapr_v1::dapr_client::DaprClient; +/// Encryption gRPC request +pub type EncryptRequest = crate::dapr::dapr::proto::runtime::v1::EncryptRequest; + +/// Decrypt gRPC request +pub type DecryptRequest = crate::dapr::dapr::proto::runtime::v1::DecryptRequest; + +/// Encryption request options +pub type EncryptRequestOptions = crate::dapr::dapr::proto::runtime::v1::EncryptRequestOptions; + +/// Decryption request options +pub type DecryptRequestOptions = crate::dapr::dapr::proto::runtime::v1::DecryptRequestOptions; + +type StreamPayload = crate::dapr::dapr::proto::common::v1::StreamPayload; impl From<(K, Vec)> for common_v1::StateItem where K: Into, @@ -626,3 +765,11 @@ where } } } + +pub struct ReaderStream(tokio_util::io::ReaderStream); + +impl ReaderStream { + pub fn new(data: T) -> Self { + ReaderStream(tokio_util::io::ReaderStream::new(data)) + } +} diff --git a/src/lib.rs b/src/lib.rs index 501e129..72f881b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,14 +1,13 @@ +extern crate dapr_macros; + +pub use dapr_macros::actor; +pub use serde; +pub use serde_json; + +pub use client::Client; + pub mod appcallback; pub mod client; pub mod dapr; pub mod error; pub mod server; - -pub use serde; - -pub use serde_json; - -pub use client::Client; - -extern crate dapr_macros; -pub use dapr_macros::actor;