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: + *

+ * + *

Prerequisites: + *

+ */ +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: + *

+ */ +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()); + } +}