diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml
index 727f783df1..43d19bcd37 100644
--- a/.github/workflows/validate.yml
+++ b/.github/workflows/validate.yml
@@ -113,6 +113,10 @@ jobs:
run: sleep 30 && docker logs dapr_scheduler && nc -vz localhost 50006
- name: Install jars
run: ./mvnw clean install -DskipTests -q
+ - name: Validate crypto example
+ working-directory: ./examples
+ run: |
+ mm.py ./src/main/java/io/dapr/examples/crypto/README.md
- name: Validate workflows example
working-directory: ./examples
run: |
@@ -186,3 +190,5 @@ jobs:
run: |
mm.py ./src/main/java/io/dapr/examples/pubsub/stream/README.md
+
+
diff --git a/examples/components/crypto/localstorage.yaml b/examples/components/crypto/localstorage.yaml
new file mode 100644
index 0000000000..04673f0702
--- /dev/null
+++ b/examples/components/crypto/localstorage.yaml
@@ -0,0 +1,12 @@
+apiVersion: dapr.io/v1alpha1
+kind: Component
+metadata:
+ name: localstoragecrypto
+spec:
+ type: crypto.dapr.localstorage
+ version: v1
+ metadata:
+ # Path to the directory containing keys (PEM files)
+ # This should point to a directory containing your key files
+ - name: path
+ value: "./keys"
diff --git a/examples/src/main/java/io/dapr/examples/crypto/CryptoExample.java b/examples/src/main/java/io/dapr/examples/crypto/CryptoExample.java
new file mode 100644
index 0000000000..977ac76c30
--- /dev/null
+++ b/examples/src/main/java/io/dapr/examples/crypto/CryptoExample.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright 2021 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.
+*/
+
+package io.dapr.examples.crypto;
+
+import io.dapr.client.DaprClientBuilder;
+import io.dapr.client.DaprPreviewClient;
+import io.dapr.client.domain.DecryptRequestAlpha1;
+import io.dapr.client.domain.EncryptRequestAlpha1;
+import io.dapr.config.Properties;
+import io.dapr.config.Property;
+import reactor.core.publisher.Flux;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.util.Base64;
+import java.util.Map;
+
+/**
+ * CryptoExample demonstrates using the Dapr Cryptography building block
+ * to encrypt and decrypt data using a cryptography component.
+ *
+ *
This example shows:
+ *
+ * - Encrypting plaintext data with a specified key and algorithm
+ * - Decrypting ciphertext data back to plaintext
+ * - Automatic key generation if keys don't exist
+ *
+ *
+ * Prerequisites:
+ *
+ * - Dapr installed and initialized
+ * - A cryptography component configured (e.g., local storage crypto)
+ *
+ */
+public class CryptoExample {
+
+ private static final String CRYPTO_COMPONENT_NAME = "localstoragecrypto";
+ private static final String KEY_NAME = "rsa-private-key";
+ private static final String KEY_WRAP_ALGORITHM = "RSA";
+ private static final String KEYS_DIR = "components/crypto/keys";
+
+ /**
+ * The main method demonstrating encryption and decryption with Dapr.
+ *
+ * @param args Command line arguments (unused).
+ */
+ public static void main(String[] args) throws Exception {
+ // Generate keys if they don't exist
+ generateKeysIfNeeded();
+
+ Map, String> overrides = Map.of(
+ Properties.HTTP_PORT, "3500",
+ Properties.GRPC_PORT, "50001"
+ );
+
+ try (DaprPreviewClient client = new DaprClientBuilder().withPropertyOverrides(overrides).buildPreviewClient()) {
+
+ String originalMessage = "This is a secret message";
+ byte[] plainText = originalMessage.getBytes(StandardCharsets.UTF_8);
+
+ System.out.println("=== Dapr Cryptography Example ===");
+ System.out.println("Original message: " + originalMessage);
+ System.out.println();
+
+ // Encrypt the message
+ System.out.println("Encrypting message...");
+ EncryptRequestAlpha1 encryptRequest = new EncryptRequestAlpha1(
+ CRYPTO_COMPONENT_NAME,
+ Flux.just(plainText),
+ KEY_NAME,
+ KEY_WRAP_ALGORITHM
+ );
+
+ byte[] encryptedData = client.encrypt(encryptRequest)
+ .collectList()
+ .map(CryptoExample::combineChunks)
+ .block();
+
+ System.out.println("Encryption successful!");
+ System.out.println("Encrypted data length: " + encryptedData.length + " bytes");
+ System.out.println();
+
+ // Decrypt the message
+ System.out.println("Decrypting message...");
+ DecryptRequestAlpha1 decryptRequest = new DecryptRequestAlpha1(
+ CRYPTO_COMPONENT_NAME,
+ Flux.just(encryptedData)
+ );
+
+ byte[] decryptedData = client.decrypt(decryptRequest)
+ .collectList()
+ .map(CryptoExample::combineChunks)
+ .block();
+
+ String decryptedMessage = new String(decryptedData, StandardCharsets.UTF_8);
+ System.out.println("Decryption successful!");
+ System.out.println("Decrypted message: " + decryptedMessage);
+ System.out.println();
+
+ if (originalMessage.equals(decryptedMessage)) {
+ System.out.println("SUCCESS: The decrypted message matches the original.");
+ } else {
+ System.out.println("ERROR: The decrypted message does not match the original.");
+ }
+
+ } catch (Exception e) {
+ System.err.println("Error during crypto operations: " + e.getMessage());
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Generates RSA key pair if the key file doesn't exist.
+ */
+ private static void generateKeysIfNeeded() throws NoSuchAlgorithmException, IOException {
+ Path keysDir = Paths.get(KEYS_DIR);
+ Path keyFile = keysDir.resolve(KEY_NAME + ".pem");
+
+ if (Files.exists(keyFile)) {
+ System.out.println("Using existing key: " + keyFile.toAbsolutePath());
+ return;
+ }
+
+ System.out.println("Generating RSA key pair...");
+ Files.createDirectories(keysDir);
+
+ KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
+ keyGen.initialize(4096);
+ KeyPair keyPair = keyGen.generateKeyPair();
+
+ String privateKeyPem = "-----BEGIN PRIVATE KEY-----\n"
+ + Base64.getMimeEncoder(64, "\n".getBytes()).encodeToString(keyPair.getPrivate().getEncoded())
+ + "\n-----END PRIVATE KEY-----\n";
+
+ String publicKeyPem = "-----BEGIN PUBLIC KEY-----\n"
+ + Base64.getMimeEncoder(64, "\n".getBytes()).encodeToString(keyPair.getPublic().getEncoded())
+ + "\n-----END PUBLIC KEY-----\n";
+
+ Files.writeString(keyFile, privateKeyPem + publicKeyPem);
+ System.out.println("Key generated: " + keyFile.toAbsolutePath());
+ }
+
+ /**
+ * Combines byte array chunks into a single byte array.
+ */
+ private static byte[] combineChunks(java.util.List chunks) {
+ int totalSize = chunks.stream().mapToInt(chunk -> chunk.length).sum();
+ byte[] result = new byte[totalSize];
+ int pos = 0;
+ for (byte[] chunk : chunks) {
+ System.arraycopy(chunk, 0, result, pos, chunk.length);
+ pos += chunk.length;
+ }
+ return result;
+ }
+}
diff --git a/examples/src/main/java/io/dapr/examples/crypto/README.md b/examples/src/main/java/io/dapr/examples/crypto/README.md
new file mode 100644
index 0000000000..229586084d
--- /dev/null
+++ b/examples/src/main/java/io/dapr/examples/crypto/README.md
@@ -0,0 +1,164 @@
+## Dapr Cryptography API Examples
+
+This example provides the different capabilities provided by Dapr Java SDK for Cryptography. For further information about Cryptography APIs please refer to [this link](https://docs.dapr.io/developing-applications/building-blocks/cryptography/cryptography-overview/)
+
+### Using the Cryptography API
+
+The Java SDK exposes several methods for this -
+* `client.encrypt(...)` for encrypting data using a cryptography component.
+* `client.decrypt(...)` for decrypting data using a cryptography component.
+
+## Pre-requisites
+
+* [Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/).
+* Java JDK 11 (or greater):
+ * [Microsoft JDK 11](https://docs.microsoft.com/en-us/java/openjdk/download#openjdk-11)
+ * [Oracle JDK 11](https://www.oracle.com/technetwork/java/javase/downloads/index.html#JDK11)
+ * [OpenJDK 11](https://jdk.java.net/11/)
+* [Apache Maven](https://maven.apache.org/install.html) version 3.x.
+
+### Checking out the code
+
+Clone this repository:
+
+```sh
+git clone https://github.com/dapr/java-sdk.git
+cd java-sdk
+```
+
+Then build the Maven project:
+
+```sh
+# make sure you are in the `java-sdk` directory.
+mvn install
+```
+
+Then get into the examples directory:
+
+```sh
+cd examples
+```
+
+### Initialize Dapr
+
+Run `dapr init` to initialize Dapr in Self-Hosted Mode if it's not already initialized.
+
+### Running the Example
+
+This example uses the Java SDK Dapr client to **Encrypt and Decrypt** data. The example automatically generates RSA keys if they don't exist.
+
+#### Example 1: Basic Crypto Example
+
+`CryptoExample.java` demonstrates basic encryption and decryption of a simple message.
+
+```java
+public class CryptoExample {
+ private static final String CRYPTO_COMPONENT_NAME = "localstoragecrypto";
+ private static final String KEY_NAME = "rsa-private-key";
+ private static final String KEY_WRAP_ALGORITHM = "RSA";
+
+ public static void main(String[] args) {
+ try (DaprPreviewClient client = new DaprClientBuilder().buildPreviewClient()) {
+
+ String originalMessage = "This is a secret message";
+ byte[] plainText = originalMessage.getBytes(StandardCharsets.UTF_8);
+
+ // Encrypt the message
+ EncryptRequestAlpha1 encryptRequest = new EncryptRequestAlpha1(
+ CRYPTO_COMPONENT_NAME,
+ Flux.just(plainText),
+ KEY_NAME,
+ KEY_WRAP_ALGORITHM
+ );
+
+ byte[] encryptedData = client.encrypt(encryptRequest)
+ .collectList()
+ .map(chunks -> /* combine chunks */)
+ .block();
+
+ // Decrypt the message
+ DecryptRequestAlpha1 decryptRequest = new DecryptRequestAlpha1(
+ CRYPTO_COMPONENT_NAME,
+ Flux.just(encryptedData)
+ );
+
+ byte[] decryptedData = client.decrypt(decryptRequest)
+ .collectList()
+ .map(chunks -> /* combine chunks */)
+ .block();
+ }
+ }
+}
+```
+
+Use the following command to run this example:
+
+
+
+```bash
+dapr run --resources-path ./components/crypto --app-id crypto-app --dapr-http-port 3500 --dapr-grpc-port 50001 -- java -jar target/dapr-java-sdk-examples-exec.jar io.dapr.examples.crypto.CryptoExample
+```
+
+
+
+#### Example 2: Streaming Crypto Example
+
+`StreamingCryptoExample.java` demonstrates advanced scenarios including:
+- Multi-chunk data encryption
+- Large data encryption (100KB+)
+- Custom encryption ciphers
+
+```bash
+dapr run --resources-path ./components/crypto --app-id crypto-app --dapr-http-port 3500 --dapr-grpc-port 50001 -- java -jar target/dapr-java-sdk-examples-exec.jar io.dapr.examples.crypto.StreamingCryptoExample
+```
+
+### Sample Output
+
+```
+=== Dapr Cryptography Example ===
+Original message: This is a secret message
+
+Encrypting message...
+Encryption successful!
+Encrypted data length: 512 bytes
+
+Decrypting message...
+Decryption successful!
+Decrypted message: This is a secret message
+
+SUCCESS: The decrypted message matches the original.
+```
+
+### Supported Key Wrap Algorithms
+
+The following key wrap algorithms are supported:
+- `A256KW` (alias: `AES`) - AES key wrap
+- `A128CBC`, `A192CBC`, `A256CBC` - AES CBC modes
+- `RSA-OAEP-256` (alias: `RSA`) - RSA OAEP with SHA-256
+
+### Supported Data Encryption Ciphers
+
+Optional data encryption ciphers:
+- `aes-gcm` (default) - AES in GCM mode
+- `chacha20-poly1305` - ChaCha20-Poly1305 cipher
+
+### Cleanup
+
+To stop the app, run (or press CTRL+C):
+
+
+
+```bash
+dapr stop --app-id crypto-app
+```
+
+
diff --git a/examples/src/main/java/io/dapr/examples/crypto/StreamingCryptoExample.java b/examples/src/main/java/io/dapr/examples/crypto/StreamingCryptoExample.java
new file mode 100644
index 0000000000..038cf8ad9b
--- /dev/null
+++ b/examples/src/main/java/io/dapr/examples/crypto/StreamingCryptoExample.java
@@ -0,0 +1,245 @@
+/*
+ * Copyright 2021 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.
+*/
+
+package io.dapr.examples.crypto;
+
+import io.dapr.client.DaprClientBuilder;
+import io.dapr.client.DaprPreviewClient;
+import io.dapr.client.domain.DecryptRequestAlpha1;
+import io.dapr.client.domain.EncryptRequestAlpha1;
+import io.dapr.config.Properties;
+import io.dapr.config.Property;
+import reactor.core.publisher.Flux;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.util.Base64;
+import java.util.Map;
+import java.util.Random;
+
+/**
+ * StreamingCryptoExample demonstrates using the Dapr Cryptography building block
+ * with streaming data for handling large payloads efficiently.
+ *
+ * This example shows:
+ *
+ * - Encrypting large data using streaming
+ * - Using optional parameters like data encryption cipher
+ * - Handling chunked data for encryption/decryption
+ *
+ */
+public class StreamingCryptoExample {
+
+ private static final String CRYPTO_COMPONENT_NAME = "localstoragecrypto";
+ private static final String KEY_NAME = "rsa-private-key";
+ private static final String KEY_WRAP_ALGORITHM = "RSA";
+ private static final String KEYS_DIR = "components/crypto/keys";
+
+ /**
+ * The main method demonstrating streaming encryption and decryption with Dapr.
+ *
+ * @param args Command line arguments (unused).
+ */
+ public static void main(String[] args) throws Exception {
+ // Generate keys if they don't exist
+ generateKeysIfNeeded();
+
+ Map, String> overrides = Map.of(
+ Properties.HTTP_PORT, "3500",
+ Properties.GRPC_PORT, "50001"
+ );
+
+ try (DaprPreviewClient client = new DaprClientBuilder().withPropertyOverrides(overrides).buildPreviewClient()) {
+
+ System.out.println("=== Dapr Streaming Cryptography Example ===");
+ System.out.println();
+
+ // Example 1: Streaming multiple chunks
+ System.out.println("--- Example 1: Multi-chunk Encryption ---");
+ demonstrateChunkedEncryption(client);
+ System.out.println();
+
+ // Example 2: Large data encryption
+ System.out.println("--- Example 2: Large Data Encryption ---");
+ demonstrateLargeDataEncryption(client);
+ System.out.println();
+
+ // Example 3: Custom encryption cipher
+ System.out.println("--- Example 3: Custom Encryption Cipher ---");
+ demonstrateCustomCipher(client);
+
+ } catch (Exception e) {
+ System.err.println("Error during crypto operations: " + e.getMessage());
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Generates RSA key pair if the key file doesn't exist.
+ */
+ private static void generateKeysIfNeeded() throws NoSuchAlgorithmException, IOException {
+ Path keysDir = Paths.get(KEYS_DIR);
+ Path keyFile = keysDir.resolve(KEY_NAME + ".pem");
+
+ if (Files.exists(keyFile)) {
+ System.out.println("Using existing key: " + keyFile.toAbsolutePath());
+ return;
+ }
+
+ System.out.println("Generating RSA key pair...");
+ Files.createDirectories(keysDir);
+
+ KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
+ keyGen.initialize(4096);
+ KeyPair keyPair = keyGen.generateKeyPair();
+
+ String privateKeyPem = "-----BEGIN PRIVATE KEY-----\n"
+ + Base64.getMimeEncoder(64, "\n".getBytes()).encodeToString(keyPair.getPrivate().getEncoded())
+ + "\n-----END PRIVATE KEY-----\n";
+
+ String publicKeyPem = "-----BEGIN PUBLIC KEY-----\n"
+ + Base64.getMimeEncoder(64, "\n".getBytes()).encodeToString(keyPair.getPublic().getEncoded())
+ + "\n-----END PUBLIC KEY-----\n";
+
+ Files.writeString(keyFile, privateKeyPem + publicKeyPem);
+ System.out.println("Key generated: " + keyFile.toAbsolutePath());
+ }
+
+ /**
+ * Demonstrates encrypting data sent in multiple chunks.
+ */
+ private static void demonstrateChunkedEncryption(DaprPreviewClient client) {
+ byte[] chunk1 = "First chunk of data. ".getBytes(StandardCharsets.UTF_8);
+ byte[] chunk2 = "Second chunk of data. ".getBytes(StandardCharsets.UTF_8);
+ byte[] chunk3 = "Third and final chunk.".getBytes(StandardCharsets.UTF_8);
+
+ byte[] fullData = new byte[chunk1.length + chunk2.length + chunk3.length];
+ System.arraycopy(chunk1, 0, fullData, 0, chunk1.length);
+ System.arraycopy(chunk2, 0, fullData, chunk1.length, chunk2.length);
+ System.arraycopy(chunk3, 0, fullData, chunk1.length + chunk2.length, chunk3.length);
+
+ System.out.println("Original data: " + new String(fullData, StandardCharsets.UTF_8));
+ System.out.println("Sending as 3 chunks...");
+
+ EncryptRequestAlpha1 encryptRequest = new EncryptRequestAlpha1(
+ CRYPTO_COMPONENT_NAME,
+ Flux.just(chunk1, chunk2, chunk3),
+ KEY_NAME,
+ KEY_WRAP_ALGORITHM
+ );
+
+ byte[] encryptedData = collectBytes(client.encrypt(encryptRequest));
+ System.out.println("Encrypted data size: " + encryptedData.length + " bytes");
+
+ DecryptRequestAlpha1 decryptRequest = new DecryptRequestAlpha1(
+ CRYPTO_COMPONENT_NAME,
+ Flux.just(encryptedData)
+ );
+
+ byte[] decryptedData = collectBytes(client.decrypt(decryptRequest));
+ String decryptedMessage = new String(decryptedData, StandardCharsets.UTF_8);
+ System.out.println("Decrypted data: " + decryptedMessage);
+ System.out.println("Verification: " + (new String(fullData, StandardCharsets.UTF_8).equals(decryptedMessage)
+ ? "SUCCESS" : "FAILED"));
+ }
+
+ /**
+ * Demonstrates encrypting a large data payload.
+ */
+ private static void demonstrateLargeDataEncryption(DaprPreviewClient client) {
+ int size = 100 * 1024;
+ byte[] largeData = new byte[size];
+ new Random().nextBytes(largeData);
+
+ System.out.println("Original data size: " + size + " bytes (100KB)");
+
+ EncryptRequestAlpha1 encryptRequest = new EncryptRequestAlpha1(
+ CRYPTO_COMPONENT_NAME,
+ Flux.just(largeData),
+ KEY_NAME,
+ KEY_WRAP_ALGORITHM
+ );
+
+ long startTime = System.currentTimeMillis();
+ byte[] encryptedData = collectBytes(client.encrypt(encryptRequest));
+ long encryptTime = System.currentTimeMillis() - startTime;
+ System.out.println("Encrypted data size: " + encryptedData.length + " bytes (took " + encryptTime + "ms)");
+
+ DecryptRequestAlpha1 decryptRequest = new DecryptRequestAlpha1(
+ CRYPTO_COMPONENT_NAME,
+ Flux.just(encryptedData)
+ );
+
+ startTime = System.currentTimeMillis();
+ byte[] decryptedData = collectBytes(client.decrypt(decryptRequest));
+ long decryptTime = System.currentTimeMillis() - startTime;
+ System.out.println("Decrypted data size: " + decryptedData.length + " bytes (took " + decryptTime + "ms)");
+
+ boolean matches = java.util.Arrays.equals(largeData, decryptedData);
+ System.out.println("Verification: " + (matches ? "SUCCESS" : "FAILED"));
+ }
+
+ /**
+ * Demonstrates using a custom data encryption cipher.
+ */
+ private static void demonstrateCustomCipher(DaprPreviewClient client) {
+ String message = "Message encrypted with custom cipher (aes-gcm)";
+ byte[] plainText = message.getBytes(StandardCharsets.UTF_8);
+
+ System.out.println("Original message: " + message);
+
+ EncryptRequestAlpha1 encryptRequest = new EncryptRequestAlpha1(
+ CRYPTO_COMPONENT_NAME,
+ Flux.just(plainText),
+ KEY_NAME,
+ KEY_WRAP_ALGORITHM
+ ).setDataEncryptionCipher("aes-gcm");
+
+ byte[] encryptedData = collectBytes(client.encrypt(encryptRequest));
+ System.out.println("Encrypted with aes-gcm cipher, size: " + encryptedData.length + " bytes");
+
+ DecryptRequestAlpha1 decryptRequest = new DecryptRequestAlpha1(
+ CRYPTO_COMPONENT_NAME,
+ Flux.just(encryptedData)
+ );
+
+ byte[] decryptedData = collectBytes(client.decrypt(decryptRequest));
+ String decryptedMessage = new String(decryptedData, StandardCharsets.UTF_8);
+ System.out.println("Decrypted message: " + decryptedMessage);
+ System.out.println("Verification: " + (message.equals(decryptedMessage) ? "SUCCESS" : "FAILED"));
+ }
+
+ /**
+ * Helper method to collect streaming bytes into a single byte array.
+ */
+ private static byte[] collectBytes(Flux stream) {
+ return stream.collectList()
+ .map(chunks -> {
+ int totalSize = chunks.stream().mapToInt(chunk -> chunk.length).sum();
+ byte[] result = new byte[totalSize];
+ int pos = 0;
+ for (byte[] chunk : chunks) {
+ System.arraycopy(chunk, 0, result, pos, chunk.length);
+ pos += chunk.length;
+ }
+ return result;
+ })
+ .block();
+ }
+}
diff --git a/sdk-tests/components/secret.json b/sdk-tests/components/secret.json
index 9e26dfeeb6..d82d0e2c60 100644
--- a/sdk-tests/components/secret.json
+++ b/sdk-tests/components/secret.json
@@ -1 +1 @@
-{}
\ No newline at end of file
+{"4734acbd-5ccc-4690-a87b-1ebb08928f06":{"year":"2020","title":"The Metrics IV"},"0acfcc9b-87aa-4864-93a4-943845c72fac":{"name":"Jon Doe"}}
\ No newline at end of file
diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/crypto/DaprPreviewClientCryptoIT.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/crypto/DaprPreviewClientCryptoIT.java
new file mode 100644
index 0000000000..984be02974
--- /dev/null
+++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/crypto/DaprPreviewClientCryptoIT.java
@@ -0,0 +1,373 @@
+/*
+ * Copyright 2024 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.
+*/
+
+package io.dapr.it.testcontainers.crypto;
+
+import io.dapr.client.DaprClientBuilder;
+import io.dapr.client.DaprPreviewClient;
+import io.dapr.client.domain.DecryptRequestAlpha1;
+import io.dapr.client.domain.EncryptRequestAlpha1;
+import io.dapr.config.Properties;
+import io.dapr.testcontainers.Component;
+import io.dapr.testcontainers.DaprContainer;
+import io.dapr.testcontainers.MetadataEntry;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.testcontainers.containers.BindMode;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+import reactor.core.publisher.Flux;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.List;
+import java.util.Random;
+
+import static io.dapr.it.testcontainers.ContainerConstants.DAPR_RUNTIME_IMAGE_TAG;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Integration tests for the Dapr Cryptography Alpha1 API.
+ */
+@Testcontainers
+@Tag("testcontainers")
+public class DaprPreviewClientCryptoIT {
+
+ private static final String CRYPTO_COMPONENT_NAME = "localstoragecrypto";
+ private static final String KEY_NAME = "testkey";
+ private static final String CONTAINER_KEYS_PATH = "/keys";
+
+ private static Path tempKeysDir;
+ private static DaprPreviewClient daprPreviewClient;
+
+ @Container
+ private static final DaprContainer DAPR_CONTAINER = createDaprContainer();
+
+ private static DaprContainer createDaprContainer() {
+ try {
+ // Create temporary directory for keys
+ tempKeysDir = Files.createTempDirectory("dapr-crypto-keys");
+
+ // Generate and save a test RSA key pair in PEM format
+ generateAndSaveRsaKeyPair(tempKeysDir);
+
+ // Create the crypto component
+ Component cryptoComponent = new Component(
+ CRYPTO_COMPONENT_NAME,
+ "crypto.dapr.localstorage",
+ "v1",
+ List.of(new MetadataEntry("path", CONTAINER_KEYS_PATH))
+ );
+
+ return new DaprContainer(DAPR_RUNTIME_IMAGE_TAG)
+ .withAppName("crypto-test-app")
+ .withComponent(cryptoComponent)
+ .withFileSystemBind(tempKeysDir.toString(), CONTAINER_KEYS_PATH, BindMode.READ_ONLY);
+
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to initialize test container", e);
+ }
+ }
+
+ private static void generateAndSaveRsaKeyPair(Path keysDir) throws NoSuchAlgorithmException, IOException {
+ KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
+ keyGen.initialize(4096);
+ KeyPair keyPair = keyGen.generateKeyPair();
+
+ // Save the private key in PEM format
+ String privateKeyPem = "-----BEGIN PRIVATE KEY-----\n" +
+ Base64.getMimeEncoder(64, "\n".getBytes()).encodeToString(keyPair.getPrivate().getEncoded()) +
+ "\n-----END PRIVATE KEY-----\n";
+
+ // Save the public key in PEM format
+ String publicKeyPem = "-----BEGIN PUBLIC KEY-----\n" +
+ Base64.getMimeEncoder(64, "\n".getBytes()).encodeToString(keyPair.getPublic().getEncoded()) +
+ "\n-----END PUBLIC KEY-----\n";
+
+ // Combine both keys in one PEM file
+ String combinedPem = privateKeyPem + publicKeyPem;
+
+ Path keyFile = keysDir.resolve(KEY_NAME);
+ Files.writeString(keyFile, combinedPem);
+
+ // Make the key file and directory readable by all (needed for container access)
+ keyFile.toFile().setReadable(true, false);
+ keysDir.toFile().setReadable(true, false);
+ keysDir.toFile().setExecutable(true, false);
+ }
+
+ @BeforeAll
+ static void setUp() {
+ daprPreviewClient = new DaprClientBuilder()
+ .withPropertyOverride(Properties.HTTP_ENDPOINT, DAPR_CONTAINER.getHttpEndpoint())
+ .withPropertyOverride(Properties.GRPC_ENDPOINT, DAPR_CONTAINER.getGrpcEndpoint())
+ .buildPreviewClient();
+ }
+
+ @AfterAll
+ static void tearDown() throws Exception {
+ if (daprPreviewClient != null) {
+ daprPreviewClient.close();
+ }
+ // Clean up temp keys directory
+ if (tempKeysDir != null && Files.exists(tempKeysDir)) {
+ Files.walk(tempKeysDir)
+ .sorted((a, b) -> -a.compareTo(b))
+ .forEach(path -> {
+ try {
+ Files.delete(path);
+ } catch (IOException e) {
+ // Ignore cleanup errors
+ }
+ });
+ }
+ }
+
+ @Test
+ public void testEncryptAndDecryptSmallData() {
+ String originalData = "Hello, World! This is a test message.";
+ byte[] plainText = originalData.getBytes(StandardCharsets.UTF_8);
+
+ // Encrypt
+ EncryptRequestAlpha1 encryptRequest = new EncryptRequestAlpha1(
+ CRYPTO_COMPONENT_NAME,
+ Flux.just(plainText),
+ KEY_NAME,
+ "RSA-OAEP-256"
+ );
+
+ byte[] encryptedData = daprPreviewClient.encrypt(encryptRequest)
+ .collectList()
+ .map(chunks -> {
+ int totalSize = chunks.stream().mapToInt(chunk -> chunk.length).sum();
+ byte[] result = new byte[totalSize];
+ int pos = 0;
+ for (byte[] chunk : chunks) {
+ System.arraycopy(chunk, 0, result, pos, chunk.length);
+ pos += chunk.length;
+ }
+ return result;
+ })
+ .block();
+
+ assertNotNull(encryptedData);
+ assertTrue(encryptedData.length > 0);
+
+ // Decrypt
+ DecryptRequestAlpha1 decryptRequest = new DecryptRequestAlpha1(
+ CRYPTO_COMPONENT_NAME,
+ Flux.just(encryptedData)
+ );
+
+ byte[] decryptedData = daprPreviewClient.decrypt(decryptRequest)
+ .collectList()
+ .map(chunks -> {
+ int totalSize = chunks.stream().mapToInt(chunk -> chunk.length).sum();
+ byte[] result = new byte[totalSize];
+ int pos = 0;
+ for (byte[] chunk : chunks) {
+ System.arraycopy(chunk, 0, result, pos, chunk.length);
+ pos += chunk.length;
+ }
+ return result;
+ })
+ .block();
+
+ assertNotNull(decryptedData);
+ assertArrayEquals(plainText, decryptedData);
+ assertEquals(originalData, new String(decryptedData, StandardCharsets.UTF_8));
+ }
+
+ @Test
+ public void testEncryptAndDecryptLargeData() {
+ // Generate a large data payload (1MB)
+ byte[] largeData = new byte[1024 * 1024];
+ new Random().nextBytes(largeData);
+
+ // Encrypt
+ EncryptRequestAlpha1 encryptRequest = new EncryptRequestAlpha1(
+ CRYPTO_COMPONENT_NAME,
+ Flux.just(largeData),
+ KEY_NAME,
+ "RSA-OAEP-256"
+ );
+
+ byte[] encryptedData = daprPreviewClient.encrypt(encryptRequest)
+ .collectList()
+ .map(chunks -> {
+ int totalSize = chunks.stream().mapToInt(chunk -> chunk.length).sum();
+ byte[] result = new byte[totalSize];
+ int pos = 0;
+ for (byte[] chunk : chunks) {
+ System.arraycopy(chunk, 0, result, pos, chunk.length);
+ pos += chunk.length;
+ }
+ return result;
+ })
+ .block();
+
+ assertNotNull(encryptedData);
+ assertTrue(encryptedData.length > 0);
+
+ // Decrypt
+ DecryptRequestAlpha1 decryptRequest = new DecryptRequestAlpha1(
+ CRYPTO_COMPONENT_NAME,
+ Flux.just(encryptedData)
+ );
+
+ byte[] decryptedData = daprPreviewClient.decrypt(decryptRequest)
+ .collectList()
+ .map(chunks -> {
+ int totalSize = chunks.stream().mapToInt(chunk -> chunk.length).sum();
+ byte[] result = new byte[totalSize];
+ int pos = 0;
+ for (byte[] chunk : chunks) {
+ System.arraycopy(chunk, 0, result, pos, chunk.length);
+ pos += chunk.length;
+ }
+ return result;
+ })
+ .block();
+
+ assertNotNull(decryptedData);
+ assertArrayEquals(largeData, decryptedData);
+ }
+
+ @Test
+ public void testEncryptAndDecryptStreamedData() {
+ // Create chunked data to simulate streaming
+ byte[] chunk1 = "First chunk of data. ".getBytes(StandardCharsets.UTF_8);
+ byte[] chunk2 = "Second chunk of data. ".getBytes(StandardCharsets.UTF_8);
+ byte[] chunk3 = "Third and final chunk.".getBytes(StandardCharsets.UTF_8);
+
+ // Combine for comparison later
+ byte[] fullData = new byte[chunk1.length + chunk2.length + chunk3.length];
+ System.arraycopy(chunk1, 0, fullData, 0, chunk1.length);
+ System.arraycopy(chunk2, 0, fullData, chunk1.length, chunk2.length);
+ System.arraycopy(chunk3, 0, fullData, chunk1.length + chunk2.length, chunk3.length);
+
+ // Encrypt with multiple chunks
+ EncryptRequestAlpha1 encryptRequest = new EncryptRequestAlpha1(
+ CRYPTO_COMPONENT_NAME,
+ Flux.just(chunk1, chunk2, chunk3),
+ KEY_NAME,
+ "RSA-OAEP-256"
+ );
+
+ byte[] encryptedData = daprPreviewClient.encrypt(encryptRequest)
+ .collectList()
+ .map(chunks -> {
+ int totalSize = chunks.stream().mapToInt(chunk -> chunk.length).sum();
+ byte[] result = new byte[totalSize];
+ int pos = 0;
+ for (byte[] chunk : chunks) {
+ System.arraycopy(chunk, 0, result, pos, chunk.length);
+ pos += chunk.length;
+ }
+ return result;
+ })
+ .block();
+
+ assertNotNull(encryptedData);
+ assertTrue(encryptedData.length > 0);
+
+ // Decrypt
+ DecryptRequestAlpha1 decryptRequest = new DecryptRequestAlpha1(
+ CRYPTO_COMPONENT_NAME,
+ Flux.just(encryptedData)
+ );
+
+ byte[] decryptedData = daprPreviewClient.decrypt(decryptRequest)
+ .collectList()
+ .map(chunks -> {
+ int totalSize = chunks.stream().mapToInt(chunk -> chunk.length).sum();
+ byte[] result = new byte[totalSize];
+ int pos = 0;
+ for (byte[] chunk : chunks) {
+ System.arraycopy(chunk, 0, result, pos, chunk.length);
+ pos += chunk.length;
+ }
+ return result;
+ })
+ .block();
+
+ assertNotNull(decryptedData);
+ assertArrayEquals(fullData, decryptedData);
+ }
+
+ @Test
+ public void testEncryptWithOptionalParameters() {
+ String originalData = "Test message with optional parameters.";
+ byte[] plainText = originalData.getBytes(StandardCharsets.UTF_8);
+
+ // Encrypt with optional data encryption cipher
+ EncryptRequestAlpha1 encryptRequest = new EncryptRequestAlpha1(
+ CRYPTO_COMPONENT_NAME,
+ Flux.just(plainText),
+ KEY_NAME,
+ "RSA-OAEP-256"
+ ).setDataEncryptionCipher("aes-gcm");
+
+ byte[] encryptedData = daprPreviewClient.encrypt(encryptRequest)
+ .collectList()
+ .map(chunks -> {
+ int totalSize = chunks.stream().mapToInt(chunk -> chunk.length).sum();
+ byte[] result = new byte[totalSize];
+ int pos = 0;
+ for (byte[] chunk : chunks) {
+ System.arraycopy(chunk, 0, result, pos, chunk.length);
+ pos += chunk.length;
+ }
+ return result;
+ })
+ .block();
+
+ assertNotNull(encryptedData);
+ assertTrue(encryptedData.length > 0);
+
+ // Decrypt
+ DecryptRequestAlpha1 decryptRequest = new DecryptRequestAlpha1(
+ CRYPTO_COMPONENT_NAME,
+ Flux.just(encryptedData)
+ );
+
+ byte[] decryptedData = daprPreviewClient.decrypt(decryptRequest)
+ .collectList()
+ .map(chunks -> {
+ int totalSize = chunks.stream().mapToInt(chunk -> chunk.length).sum();
+ byte[] result = new byte[totalSize];
+ int pos = 0;
+ for (byte[] chunk : chunks) {
+ System.arraycopy(chunk, 0, result, pos, chunk.length);
+ pos += chunk.length;
+ }
+ return result;
+ })
+ .block();
+
+ assertNotNull(decryptedData);
+ assertArrayEquals(plainText, decryptedData);
+ }
+}
diff --git a/sdk/src/main/java/io/dapr/client/DaprClientImpl.java b/sdk/src/main/java/io/dapr/client/DaprClientImpl.java
index 012921a89e..80b2830b71 100644
--- a/sdk/src/main/java/io/dapr/client/DaprClientImpl.java
+++ b/sdk/src/main/java/io/dapr/client/DaprClientImpl.java
@@ -48,9 +48,11 @@
import io.dapr.client.domain.ConversationTools;
import io.dapr.client.domain.ConversationToolsFunction;
import io.dapr.client.domain.DaprMetadata;
+import io.dapr.client.domain.DecryptRequestAlpha1;
import io.dapr.client.domain.DeleteJobRequest;
import io.dapr.client.domain.DeleteStateRequest;
import io.dapr.client.domain.DropFailurePolicy;
+import io.dapr.client.domain.EncryptRequestAlpha1;
import io.dapr.client.domain.ExecuteStateTransactionRequest;
import io.dapr.client.domain.FailurePolicy;
import io.dapr.client.domain.FailurePolicyType;
@@ -2071,4 +2073,196 @@ private AppConnectionPropertiesHealthMetadata getAppConnectionPropertiesHealth(
return new AppConnectionPropertiesHealthMetadata(healthCheckPath, healthProbeInterval, healthProbeTimeout,
healthThreshold);
}
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Flux encrypt(EncryptRequestAlpha1 request) {
+ try {
+ if (request == null) {
+ throw new IllegalArgumentException("EncryptRequestAlpha1 cannot be null.");
+ }
+ if (request.getComponentName() == null || request.getComponentName().trim().isEmpty()) {
+ throw new IllegalArgumentException("Component name cannot be null or empty.");
+ }
+ if (request.getKeyName() == null || request.getKeyName().trim().isEmpty()) {
+ throw new IllegalArgumentException("Key name cannot be null or empty.");
+ }
+ if (request.getKeyWrapAlgorithm() == null || request.getKeyWrapAlgorithm().trim().isEmpty()) {
+ throw new IllegalArgumentException("Key wrap algorithm cannot be null or empty.");
+ }
+ if (request.getPlainTextStream() == null) {
+ throw new IllegalArgumentException("Plaintext stream cannot be null.");
+ }
+
+ return Flux.create(sink -> {
+ // Create response observer to receive encrypted data
+ final StreamObserver responseObserver =
+ new StreamObserver() {
+ @Override
+ public void onNext(DaprProtos.EncryptResponse response) {
+ if (response.hasPayload()) {
+ byte[] data = response.getPayload().getData().toByteArray();
+ if (data.length > 0) {
+ sink.next(data);
+ }
+ }
+ }
+
+ @Override
+ public void onError(Throwable t) {
+ sink.error(DaprException.propagate(new DaprException("ENCRYPT_ERROR",
+ "Error during encryption: " + t.getMessage(), t)));
+ }
+
+ @Override
+ public void onCompleted() {
+ sink.complete();
+ }
+ };
+
+ // Build options for the first message
+ DaprProtos.EncryptRequestOptions.Builder optionsBuilder = DaprProtos.EncryptRequestOptions.newBuilder()
+ .setComponentName(request.getComponentName())
+ .setKeyName(request.getKeyName())
+ .setKeyWrapAlgorithm(request.getKeyWrapAlgorithm());
+
+ if (request.getDataEncryptionCipher() != null && !request.getDataEncryptionCipher().isEmpty()) {
+ optionsBuilder.setDataEncryptionCipher(request.getDataEncryptionCipher());
+ }
+ optionsBuilder.setOmitDecryptionKeyName(request.isOmitDecryptionKeyName());
+ if (request.getDecryptionKeyName() != null && !request.getDecryptionKeyName().isEmpty()) {
+ optionsBuilder.setDecryptionKeyName(request.getDecryptionKeyName());
+ }
+
+ final DaprProtos.EncryptRequestOptions options = optionsBuilder.build();
+ final long[] sequenceNumber = {0};
+ final boolean[] firstMessage = {true};
+
+ // Get the request stream observer from gRPC
+ final StreamObserver requestObserver =
+ intercept(null, asyncStub).encryptAlpha1(responseObserver);
+
+ // Subscribe to the plaintext stream and send chunks
+ request.getPlainTextStream()
+ .doOnNext(chunk -> {
+ DaprProtos.EncryptRequest.Builder reqBuilder = DaprProtos.EncryptRequest.newBuilder()
+ .setPayload(CommonProtos.StreamPayload.newBuilder()
+ .setData(ByteString.copyFrom(chunk))
+ .setSeq(sequenceNumber[0]++)
+ .build());
+
+ // Include options only in the first message
+ if (firstMessage[0]) {
+ reqBuilder.setOptions(options);
+ firstMessage[0] = false;
+ }
+
+ requestObserver.onNext(reqBuilder.build());
+ })
+ .doOnError(error -> {
+ requestObserver.onError(error);
+ sink.error(DaprException.propagate(new DaprException("ENCRYPT_ERROR",
+ "Error reading plaintext stream: " + error.getMessage(), error)));
+ })
+ .doOnComplete(() -> {
+ requestObserver.onCompleted();
+ })
+ .subscribe();
+ });
+ } catch (Exception ex) {
+ return DaprException.wrapFlux(ex);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Flux decrypt(DecryptRequestAlpha1 request) {
+ try {
+ if (request == null) {
+ throw new IllegalArgumentException("DecryptRequestAlpha1 cannot be null.");
+ }
+ if (request.getComponentName() == null || request.getComponentName().trim().isEmpty()) {
+ throw new IllegalArgumentException("Component name cannot be null or empty.");
+ }
+ if (request.getCipherTextStream() == null) {
+ throw new IllegalArgumentException("Ciphertext stream cannot be null.");
+ }
+
+ return Flux.create(sink -> {
+ // Create response observer to receive decrypted data
+ final StreamObserver responseObserver =
+ new StreamObserver() {
+ @Override
+ public void onNext(DaprProtos.DecryptResponse response) {
+ if (response.hasPayload()) {
+ byte[] data = response.getPayload().getData().toByteArray();
+ if (data.length > 0) {
+ sink.next(data);
+ }
+ }
+ }
+
+ @Override
+ public void onError(Throwable t) {
+ sink.error(DaprException.propagate(new DaprException("DECRYPT_ERROR",
+ "Error during decryption: " + t.getMessage(), t)));
+ }
+
+ @Override
+ public void onCompleted() {
+ sink.complete();
+ }
+ };
+
+ // Build options for the first message
+ DaprProtos.DecryptRequestOptions.Builder optionsBuilder = DaprProtos.DecryptRequestOptions.newBuilder()
+ .setComponentName(request.getComponentName());
+
+ if (request.getKeyName() != null && !request.getKeyName().isEmpty()) {
+ optionsBuilder.setKeyName(request.getKeyName());
+ }
+
+ final DaprProtos.DecryptRequestOptions options = optionsBuilder.build();
+ final long[] sequenceNumber = {0};
+ final boolean[] firstMessage = {true};
+
+ // Get the request stream observer from gRPC
+ final StreamObserver requestObserver =
+ intercept(null, asyncStub).decryptAlpha1(responseObserver);
+
+ // Subscribe to the ciphertext stream and send chunks
+ request.getCipherTextStream()
+ .doOnNext(chunk -> {
+ DaprProtos.DecryptRequest.Builder reqBuilder = DaprProtos.DecryptRequest.newBuilder()
+ .setPayload(CommonProtos.StreamPayload.newBuilder()
+ .setData(ByteString.copyFrom(chunk))
+ .setSeq(sequenceNumber[0]++)
+ .build());
+
+ // Include options only in the first message
+ if (firstMessage[0]) {
+ reqBuilder.setOptions(options);
+ firstMessage[0] = false;
+ }
+
+ requestObserver.onNext(reqBuilder.build());
+ })
+ .doOnError(error -> {
+ requestObserver.onError(error);
+ sink.error(DaprException.propagate(new DaprException("DECRYPT_ERROR",
+ "Error reading ciphertext stream: " + error.getMessage(), error)));
+ })
+ .doOnComplete(() -> {
+ requestObserver.onCompleted();
+ })
+ .subscribe();
+ });
+ } catch (Exception ex) {
+ return DaprException.wrapFlux(ex);
+ }
+ }
}
diff --git a/sdk/src/main/java/io/dapr/client/DaprPreviewClient.java b/sdk/src/main/java/io/dapr/client/DaprPreviewClient.java
index 92c6a61c3e..28878c8b66 100644
--- a/sdk/src/main/java/io/dapr/client/DaprPreviewClient.java
+++ b/sdk/src/main/java/io/dapr/client/DaprPreviewClient.java
@@ -21,7 +21,9 @@
import io.dapr.client.domain.ConversationRequestAlpha2;
import io.dapr.client.domain.ConversationResponse;
import io.dapr.client.domain.ConversationResponseAlpha2;
+import io.dapr.client.domain.DecryptRequestAlpha1;
import io.dapr.client.domain.DeleteJobRequest;
+import io.dapr.client.domain.EncryptRequestAlpha1;
import io.dapr.client.domain.GetJobRequest;
import io.dapr.client.domain.GetJobResponse;
import io.dapr.client.domain.LockRequest;
@@ -32,6 +34,7 @@
import io.dapr.client.domain.UnlockResponseStatus;
import io.dapr.client.domain.query.Query;
import io.dapr.utils.TypeRef;
+import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.List;
@@ -325,4 +328,24 @@ Subscription subscribeToEvents(
* @return {@link ConversationResponseAlpha2}.
*/
public Mono converseAlpha2(ConversationRequestAlpha2 conversationRequestAlpha2);
+
+ /**
+ * Encrypt data using the Dapr cryptography building block.
+ * This method uses streaming to handle large payloads efficiently.
+ *
+ * @param request The encryption request containing component name, key information, and plaintext stream.
+ * @return A Flux of encrypted byte arrays (ciphertext chunks).
+ * @throws IllegalArgumentException if required parameters are missing.
+ */
+ Flux encrypt(EncryptRequestAlpha1 request);
+
+ /**
+ * Decrypt data using the Dapr cryptography building block.
+ * This method uses streaming to handle large payloads efficiently.
+ *
+ * @param request The decryption request containing component name, optional key name, and ciphertext stream.
+ * @return A Flux of decrypted byte arrays (plaintext chunks).
+ * @throws IllegalArgumentException if required parameters are missing.
+ */
+ Flux decrypt(DecryptRequestAlpha1 request);
}
diff --git a/sdk/src/main/java/io/dapr/client/domain/DecryptRequestAlpha1.java b/sdk/src/main/java/io/dapr/client/domain/DecryptRequestAlpha1.java
new file mode 100644
index 0000000000..61cf3206e6
--- /dev/null
+++ b/sdk/src/main/java/io/dapr/client/domain/DecryptRequestAlpha1.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2024 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.
+*/
+
+package io.dapr.client.domain;
+
+import reactor.core.publisher.Flux;
+
+/**
+ * Request to decrypt data using the Dapr Cryptography building block.
+ * Uses streaming to handle large payloads efficiently.
+ */
+public class DecryptRequestAlpha1 {
+
+ private final String componentName;
+ private final Flux cipherTextStream;
+ private String keyName;
+
+ /**
+ * Constructor for DecryptRequestAlpha1.
+ *
+ * @param componentName Name of the cryptography component. Required.
+ * @param cipherTextStream Stream of ciphertext data to decrypt. Required.
+ */
+ public DecryptRequestAlpha1(String componentName, Flux cipherTextStream) {
+ this.componentName = componentName;
+ this.cipherTextStream = cipherTextStream;
+ }
+
+ /**
+ * Gets the cryptography component name.
+ *
+ * @return the component name
+ */
+ public String getComponentName() {
+ return componentName;
+ }
+
+ /**
+ * Gets the ciphertext data stream to decrypt.
+ *
+ * @return the ciphertext stream as Flux of byte arrays
+ */
+ public Flux getCipherTextStream() {
+ return cipherTextStream;
+ }
+
+ /**
+ * Gets the key name (or name/version) to use for decryption.
+ *
+ * @return the key name, or null if using the key embedded in the ciphertext
+ */
+ public String getKeyName() {
+ return keyName;
+ }
+
+ /**
+ * Sets the key name (or name/version) to decrypt the message.
+ * This overrides any key reference included in the message if present.
+ * This is required if the message doesn't include a key reference
+ * (i.e., was created with omitDecryptionKeyName set to true).
+ *
+ * @param keyName the key name to use for decryption
+ * @return this request instance for method chaining
+ */
+ public DecryptRequestAlpha1 setKeyName(String keyName) {
+ this.keyName = keyName;
+ return this;
+ }
+}
diff --git a/sdk/src/main/java/io/dapr/client/domain/EncryptRequestAlpha1.java b/sdk/src/main/java/io/dapr/client/domain/EncryptRequestAlpha1.java
new file mode 100644
index 0000000000..bab82bc22d
--- /dev/null
+++ b/sdk/src/main/java/io/dapr/client/domain/EncryptRequestAlpha1.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2024 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.
+*/
+
+package io.dapr.client.domain;
+
+import reactor.core.publisher.Flux;
+
+/**
+ * Request to encrypt data using the Dapr Cryptography building block.
+ * Uses streaming to handle large payloads efficiently.
+ */
+public class EncryptRequestAlpha1 {
+
+ private final String componentName;
+ private final Flux plainTextStream;
+ private final String keyName;
+ private final String keyWrapAlgorithm;
+ private String dataEncryptionCipher;
+ private boolean omitDecryptionKeyName;
+ private String decryptionKeyName;
+
+ /**
+ * Constructor for EncryptRequestAlpha1.
+ *
+ * @param componentName Name of the cryptography component. Required.
+ * @param plainTextStream Stream of plaintext data to encrypt. Required.
+ * @param keyName Name (or name/version) of the key to use for encryption. Required.
+ * @param keyWrapAlgorithm Key wrapping algorithm to use. Required.
+ * Supported options: A256KW (alias: AES), A128CBC, A192CBC, A256CBC,
+ * RSA-OAEP-256 (alias: RSA).
+ */
+ public EncryptRequestAlpha1(String componentName, Flux plainTextStream,
+ String keyName, String keyWrapAlgorithm) {
+ this.componentName = componentName;
+ this.plainTextStream = plainTextStream;
+ this.keyName = keyName;
+ this.keyWrapAlgorithm = keyWrapAlgorithm;
+ }
+
+ /**
+ * Gets the cryptography component name.
+ *
+ * @return the component name
+ */
+ public String getComponentName() {
+ return componentName;
+ }
+
+ /**
+ * Gets the plaintext data stream to encrypt.
+ *
+ * @return the plaintext stream as Flux of byte arrays
+ */
+ public Flux getPlainTextStream() {
+ return plainTextStream;
+ }
+
+ /**
+ * Gets the key name (or name/version).
+ *
+ * @return the key name
+ */
+ public String getKeyName() {
+ return keyName;
+ }
+
+ /**
+ * Gets the key wrap algorithm.
+ *
+ * @return the key wrap algorithm
+ */
+ public String getKeyWrapAlgorithm() {
+ return keyWrapAlgorithm;
+ }
+
+ /**
+ * Gets the data encryption cipher.
+ *
+ * @return the data encryption cipher, or null if not set
+ */
+ public String getDataEncryptionCipher() {
+ return dataEncryptionCipher;
+ }
+
+ /**
+ * Sets the cipher used to encrypt data.
+ * Optional. Supported values: "aes-gcm" (default), "chacha20-poly1305".
+ *
+ * @param dataEncryptionCipher the cipher to use for data encryption
+ * @return this request instance for method chaining
+ */
+ public EncryptRequestAlpha1 setDataEncryptionCipher(String dataEncryptionCipher) {
+ this.dataEncryptionCipher = dataEncryptionCipher;
+ return this;
+ }
+
+ /**
+ * Checks if the decryption key name should be omitted from the encrypted document.
+ *
+ * @return true if the key name should be omitted
+ */
+ public boolean isOmitDecryptionKeyName() {
+ return omitDecryptionKeyName;
+ }
+
+ /**
+ * Sets whether to omit the decryption key name from the encrypted document.
+ * If true, calls to decrypt must provide a key reference (name or name/version).
+ * Defaults to false.
+ *
+ * @param omitDecryptionKeyName whether to omit the key name
+ * @return this request instance for method chaining
+ */
+ public EncryptRequestAlpha1 setOmitDecryptionKeyName(boolean omitDecryptionKeyName) {
+ this.omitDecryptionKeyName = omitDecryptionKeyName;
+ return this;
+ }
+
+ /**
+ * Gets the decryption key name to embed in the encrypted document.
+ *
+ * @return the decryption key name, or null if not set
+ */
+ public String getDecryptionKeyName() {
+ return decryptionKeyName;
+ }
+
+ /**
+ * Sets the key reference to embed in the encrypted document (name or name/version).
+ * This is helpful if the reference of the key used to decrypt the document is
+ * different from the one used to encrypt it.
+ * If unset, uses the reference of the key used to encrypt the document.
+ * This option is ignored if omitDecryptionKeyName is true.
+ *
+ * @param decryptionKeyName the key name to embed for decryption
+ * @return this request instance for method chaining
+ */
+ public EncryptRequestAlpha1 setDecryptionKeyName(String decryptionKeyName) {
+ this.decryptionKeyName = decryptionKeyName;
+ return this;
+ }
+}
diff --git a/sdk/src/test/java/io/dapr/client/ProtobufValueHelperTest.java b/sdk/src/test/java/io/dapr/client/ProtobufValueHelperTest.java
index c345f34ff6..c6bfa5eb28 100644
--- a/sdk/src/test/java/io/dapr/client/ProtobufValueHelperTest.java
+++ b/sdk/src/test/java/io/dapr/client/ProtobufValueHelperTest.java
@@ -353,49 +353,49 @@ public void testToProtobufValue_OpenAPIFunctionSchema() throws IOException {
functionSchema.put("type", "function");
functionSchema.put("name", "get_horoscope");
functionSchema.put("description", "Get today's horoscope for an astrological sign.");
-
+
Map parameters = new LinkedHashMap<>();
parameters.put("type", "object");
-
+
Map properties = new LinkedHashMap<>();
Map signProperty = new LinkedHashMap<>();
signProperty.put("type", "string");
signProperty.put("description", "An astrological sign like Taurus or Aquarius");
properties.put("sign", signProperty);
-
+
parameters.put("properties", properties);
parameters.put("required", Arrays.asList("sign"));
-
+
functionSchema.put("parameters", parameters);
-
+
Value result = ProtobufValueHelper.toProtobufValue(functionSchema);
-
+
assertNotNull(result);
assertTrue(result.hasStructValue());
Struct rootStruct = result.getStructValue();
-
+
// Verify root level fields
assertEquals("function", rootStruct.getFieldsMap().get("type").getStringValue());
assertEquals("get_horoscope", rootStruct.getFieldsMap().get("name").getStringValue());
- assertEquals("Get today's horoscope for an astrological sign.",
+ assertEquals("Get today's horoscope for an astrological sign.",
rootStruct.getFieldsMap().get("description").getStringValue());
-
+
// Verify parameters object
assertTrue(rootStruct.getFieldsMap().get("parameters").hasStructValue());
Struct parametersStruct = rootStruct.getFieldsMap().get("parameters").getStructValue();
assertEquals("object", parametersStruct.getFieldsMap().get("type").getStringValue());
-
+
// Verify properties object
assertTrue(parametersStruct.getFieldsMap().get("properties").hasStructValue());
Struct propertiesStruct = parametersStruct.getFieldsMap().get("properties").getStructValue();
-
+
// Verify sign property
assertTrue(propertiesStruct.getFieldsMap().get("sign").hasStructValue());
Struct signStruct = propertiesStruct.getFieldsMap().get("sign").getStructValue();
assertEquals("string", signStruct.getFieldsMap().get("type").getStringValue());
- assertEquals("An astrological sign like Taurus or Aquarius",
+ assertEquals("An astrological sign like Taurus or Aquarius",
signStruct.getFieldsMap().get("description").getStringValue());
-
+
// Verify required array
assertTrue(parametersStruct.getFieldsMap().get("required").hasListValue());
ListValue requiredList = parametersStruct.getFieldsMap().get("required").getListValue();
diff --git a/sdk/src/test/java/io/dapr/client/domain/DecryptRequestAlpha1Test.java b/sdk/src/test/java/io/dapr/client/domain/DecryptRequestAlpha1Test.java
new file mode 100644
index 0000000000..856da7b13f
--- /dev/null
+++ b/sdk/src/test/java/io/dapr/client/domain/DecryptRequestAlpha1Test.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2024 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.
+*/
+
+package io.dapr.client.domain;
+
+import org.junit.jupiter.api.Test;
+import reactor.core.publisher.Flux;
+
+import java.nio.charset.StandardCharsets;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+public class DecryptRequestAlpha1Test {
+
+ @Test
+ public void testConstructorWithRequiredFields() {
+ Flux cipherTextStream = Flux.just("encrypted data".getBytes(StandardCharsets.UTF_8));
+
+ DecryptRequestAlpha1 request = new DecryptRequestAlpha1(
+ "mycomponent",
+ cipherTextStream
+ );
+
+ assertEquals("mycomponent", request.getComponentName());
+ assertNotNull(request.getCipherTextStream());
+ assertNull(request.getKeyName());
+ }
+
+ @Test
+ public void testFluentSetKeyName() {
+ Flux cipherTextStream = Flux.just("encrypted data".getBytes(StandardCharsets.UTF_8));
+
+ DecryptRequestAlpha1 request = new DecryptRequestAlpha1(
+ "mycomponent",
+ cipherTextStream
+ ).setKeyName("mykey");
+
+ assertEquals("mykey", request.getKeyName());
+ }
+
+ @Test
+ public void testFluentSetterReturnsSameInstance() {
+ Flux cipherTextStream = Flux.just("encrypted data".getBytes(StandardCharsets.UTF_8));
+
+ DecryptRequestAlpha1 request = new DecryptRequestAlpha1(
+ "mycomponent",
+ cipherTextStream
+ );
+
+ DecryptRequestAlpha1 sameRequest = request.setKeyName("mykey");
+ assertEquals(request, sameRequest);
+ }
+
+ @Test
+ public void testNullComponentName() {
+ Flux cipherTextStream = Flux.just("encrypted data".getBytes(StandardCharsets.UTF_8));
+
+ DecryptRequestAlpha1 request = new DecryptRequestAlpha1(
+ null,
+ cipherTextStream
+ );
+
+ assertNull(request.getComponentName());
+ }
+
+ @Test
+ public void testNullCipherTextStream() {
+ DecryptRequestAlpha1 request = new DecryptRequestAlpha1(
+ "mycomponent",
+ null
+ );
+
+ assertNull(request.getCipherTextStream());
+ }
+
+ @Test
+ public void testEmptyStream() {
+ Flux emptyStream = Flux.empty();
+
+ DecryptRequestAlpha1 request = new DecryptRequestAlpha1(
+ "mycomponent",
+ emptyStream
+ );
+
+ assertNotNull(request.getCipherTextStream());
+ }
+
+ @Test
+ public void testMultipleChunksStream() {
+ Flux multiChunkStream = Flux.just(
+ "chunk1".getBytes(StandardCharsets.UTF_8),
+ "chunk2".getBytes(StandardCharsets.UTF_8),
+ "chunk3".getBytes(StandardCharsets.UTF_8)
+ );
+
+ DecryptRequestAlpha1 request = new DecryptRequestAlpha1(
+ "mycomponent",
+ multiChunkStream
+ );
+
+ assertNotNull(request.getCipherTextStream());
+ }
+}
diff --git a/sdk/src/test/java/io/dapr/client/domain/EncryptRequestAlpha1Test.java b/sdk/src/test/java/io/dapr/client/domain/EncryptRequestAlpha1Test.java
new file mode 100644
index 0000000000..b02b2c92eb
--- /dev/null
+++ b/sdk/src/test/java/io/dapr/client/domain/EncryptRequestAlpha1Test.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright 2024 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.
+*/
+
+package io.dapr.client.domain;
+
+import org.junit.jupiter.api.Test;
+import reactor.core.publisher.Flux;
+
+import java.nio.charset.StandardCharsets;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class EncryptRequestAlpha1Test {
+
+ @Test
+ public void testConstructorWithRequiredFields() {
+ Flux plainTextStream = Flux.just("test data".getBytes(StandardCharsets.UTF_8));
+
+ EncryptRequestAlpha1 request = new EncryptRequestAlpha1(
+ "mycomponent",
+ plainTextStream,
+ "mykey",
+ "RSA-OAEP-256"
+ );
+
+ assertEquals("mycomponent", request.getComponentName());
+ assertNotNull(request.getPlainTextStream());
+ assertEquals("mykey", request.getKeyName());
+ assertEquals("RSA-OAEP-256", request.getKeyWrapAlgorithm());
+ assertNull(request.getDataEncryptionCipher());
+ assertFalse(request.isOmitDecryptionKeyName());
+ assertNull(request.getDecryptionKeyName());
+ }
+
+ @Test
+ public void testFluentSetters() {
+ Flux plainTextStream = Flux.just("test data".getBytes(StandardCharsets.UTF_8));
+
+ EncryptRequestAlpha1 request = new EncryptRequestAlpha1(
+ "mycomponent",
+ plainTextStream,
+ "mykey",
+ "RSA-OAEP-256"
+ )
+ .setDataEncryptionCipher("AES-GCM")
+ .setOmitDecryptionKeyName(true)
+ .setDecryptionKeyName("decrypt-key");
+
+ assertEquals("AES-GCM", request.getDataEncryptionCipher());
+ assertTrue(request.isOmitDecryptionKeyName());
+ assertEquals("decrypt-key", request.getDecryptionKeyName());
+ }
+
+ @Test
+ public void testFluentSettersReturnSameInstance() {
+ Flux plainTextStream = Flux.just("test data".getBytes(StandardCharsets.UTF_8));
+
+ EncryptRequestAlpha1 request = new EncryptRequestAlpha1(
+ "mycomponent",
+ plainTextStream,
+ "mykey",
+ "RSA-OAEP-256"
+ );
+
+ EncryptRequestAlpha1 sameRequest = request.setDataEncryptionCipher("AES-GCM");
+ assertEquals(request, sameRequest);
+
+ sameRequest = request.setOmitDecryptionKeyName(true);
+ assertEquals(request, sameRequest);
+
+ sameRequest = request.setDecryptionKeyName("decrypt-key");
+ assertEquals(request, sameRequest);
+ }
+
+ @Test
+ public void testNullComponentName() {
+ Flux plainTextStream = Flux.just("test data".getBytes(StandardCharsets.UTF_8));
+
+ EncryptRequestAlpha1 request = new EncryptRequestAlpha1(
+ null,
+ plainTextStream,
+ "mykey",
+ "RSA-OAEP-256"
+ );
+
+ assertNull(request.getComponentName());
+ }
+
+ @Test
+ public void testNullPlainTextStream() {
+ EncryptRequestAlpha1 request = new EncryptRequestAlpha1(
+ "mycomponent",
+ null,
+ "mykey",
+ "RSA-OAEP-256"
+ );
+
+ assertNull(request.getPlainTextStream());
+ }
+
+ @Test
+ public void testEmptyStream() {
+ Flux emptyStream = Flux.empty();
+
+ EncryptRequestAlpha1 request = new EncryptRequestAlpha1(
+ "mycomponent",
+ emptyStream,
+ "mykey",
+ "RSA-OAEP-256"
+ );
+
+ assertNotNull(request.getPlainTextStream());
+ }
+
+ @Test
+ public void testMultipleChunksStream() {
+ Flux multiChunkStream = Flux.just(
+ "chunk1".getBytes(StandardCharsets.UTF_8),
+ "chunk2".getBytes(StandardCharsets.UTF_8),
+ "chunk3".getBytes(StandardCharsets.UTF_8)
+ );
+
+ EncryptRequestAlpha1 request = new EncryptRequestAlpha1(
+ "mycomponent",
+ multiChunkStream,
+ "mykey",
+ "A256KW"
+ );
+
+ assertNotNull(request.getPlainTextStream());
+ assertEquals("A256KW", request.getKeyWrapAlgorithm());
+ }
+}