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/keys/.gitkeep b/examples/components/crypto/keys/.gitkeep new file mode 100644 index 0000000000..8c6c3d3b5d --- /dev/null +++ b/examples/components/crypto/keys/.gitkeep @@ -0,0 +1,2 @@ +# This file ensures the keys directory is tracked by git +# RSA keys are generated at runtime by the example diff --git a/examples/components/crypto/localstorage.yaml b/examples/components/crypto/localstorage.yaml new file mode 100644 index 0000000000..3cb456af29 --- /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 path is relative to the resources-path directory + - name: path + value: "/home/runner/work/java-sdk/java-sdk/examples/components/crypto/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..4fb8d75244 --- /dev/null +++ b/examples/src/main/java/io/dapr/examples/crypto/README.md @@ -0,0 +1,178 @@ +## 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 +mkdir -p ./components/crypto/keys && openssl genrsa -out ./components/crypto/keys/rsa-private-key.pem 4096 && openssl rsa -in ./components/crypto/keys/rsa-private-key.pem -pubout -out ./components/crypto/keys/rsa-private-key.pub.pem && echo "Keys generated successfully" +``` + + + + + +```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 0dfb1b644b..05b555b5e9 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; @@ -2108,4 +2110,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 545b8e5dc5..373d124c8d 100644 --- a/sdk/src/main/java/io/dapr/client/DaprPreviewClient.java +++ b/sdk/src/main/java/io/dapr/client/DaprPreviewClient.java @@ -22,7 +22,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; @@ -339,4 +341,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/DaprPreviewClientGrpcTest.java b/sdk/src/test/java/io/dapr/client/DaprPreviewClientGrpcTest.java index a42c4f946c..d2b595043f 100644 --- a/sdk/src/test/java/io/dapr/client/DaprPreviewClientGrpcTest.java +++ b/sdk/src/test/java/io/dapr/client/DaprPreviewClientGrpcTest.java @@ -35,6 +35,7 @@ import io.dapr.client.domain.ConversationResultChoices; import io.dapr.client.domain.ConversationToolCalls; import io.dapr.client.domain.ConversationTools; +import io.dapr.client.domain.DecryptRequestAlpha1; import io.dapr.client.domain.DeleteJobRequest; import io.dapr.client.domain.DeveloperMessage; import io.dapr.client.domain.GetJobRequest; @@ -45,6 +46,7 @@ import io.dapr.client.domain.ConversationResponse; import io.dapr.client.domain.DeleteJobRequest; import io.dapr.client.domain.DropFailurePolicy; +import io.dapr.client.domain.EncryptRequestAlpha1; import io.dapr.client.domain.GetJobRequest; import io.dapr.client.domain.GetJobResponse; import io.dapr.client.domain.JobSchedule; @@ -70,11 +72,13 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.ArgumentMatchers; import org.mockito.Mockito; import org.mockito.stubbing.Answer; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.io.IOException; @@ -99,6 +103,7 @@ import static io.dapr.utils.TestUtils.assertThrowsDaprException; import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -1926,4 +1931,663 @@ private DaprProtos.QueryStateItem buildQueryStateItem(QueryStateItem item) th private static StatusRuntimeException newStatusRuntimeException(String status, String message) { return new StatusRuntimeException(Status.fromCode(Status.Code.valueOf(status)).withDescription(message)); } + + // ==================== Encrypt Tests ==================== + + @Test + @DisplayName("encrypt should throw IllegalArgumentException when request is null") + public void encryptNullRequestTest() { + assertThrows(IllegalArgumentException.class, () -> { + previewClient.encrypt(null).blockFirst(); + }); + } + + @Test + @DisplayName("encrypt should throw IllegalArgumentException when component name is null") + public void encryptNullComponentNameTest() { + Flux plainTextStream = Flux.just("test data".getBytes(StandardCharsets.UTF_8)); + EncryptRequestAlpha1 request = new EncryptRequestAlpha1( + null, + plainTextStream, + "mykey", + "RSA-OAEP-256" + ); + + assertThrows(IllegalArgumentException.class, () -> { + previewClient.encrypt(request).blockFirst(); + }); + } + + @Test + @DisplayName("encrypt should throw IllegalArgumentException when component name is empty") + public void encryptEmptyComponentNameTest() { + Flux plainTextStream = Flux.just("test data".getBytes(StandardCharsets.UTF_8)); + EncryptRequestAlpha1 request = new EncryptRequestAlpha1( + "", + plainTextStream, + "mykey", + "RSA-OAEP-256" + ); + + assertThrows(IllegalArgumentException.class, () -> { + previewClient.encrypt(request).blockFirst(); + }); + } + + @Test + @DisplayName("encrypt should throw IllegalArgumentException when component name is whitespace only") + public void encryptWhitespaceComponentNameTest() { + Flux plainTextStream = Flux.just("test data".getBytes(StandardCharsets.UTF_8)); + EncryptRequestAlpha1 request = new EncryptRequestAlpha1( + " ", + plainTextStream, + "mykey", + "RSA-OAEP-256" + ); + + assertThrows(IllegalArgumentException.class, () -> { + previewClient.encrypt(request).blockFirst(); + }); + } + + @Test + @DisplayName("encrypt should throw IllegalArgumentException when key name is null") + public void encryptNullKeyNameTest() { + Flux plainTextStream = Flux.just("test data".getBytes(StandardCharsets.UTF_8)); + EncryptRequestAlpha1 request = new EncryptRequestAlpha1( + "mycomponent", + plainTextStream, + null, + "RSA-OAEP-256" + ); + + assertThrows(IllegalArgumentException.class, () -> { + previewClient.encrypt(request).blockFirst(); + }); + } + + @Test + @DisplayName("encrypt should throw IllegalArgumentException when key name is empty") + public void encryptEmptyKeyNameTest() { + Flux plainTextStream = Flux.just("test data".getBytes(StandardCharsets.UTF_8)); + EncryptRequestAlpha1 request = new EncryptRequestAlpha1( + "mycomponent", + plainTextStream, + "", + "RSA-OAEP-256" + ); + + assertThrows(IllegalArgumentException.class, () -> { + previewClient.encrypt(request).blockFirst(); + }); + } + + @Test + @DisplayName("encrypt should throw IllegalArgumentException when key wrap algorithm is null") + public void encryptNullKeyWrapAlgorithmTest() { + Flux plainTextStream = Flux.just("test data".getBytes(StandardCharsets.UTF_8)); + EncryptRequestAlpha1 request = new EncryptRequestAlpha1( + "mycomponent", + plainTextStream, + "mykey", + null + ); + + assertThrows(IllegalArgumentException.class, () -> { + previewClient.encrypt(request).blockFirst(); + }); + } + + @Test + @DisplayName("encrypt should throw IllegalArgumentException when key wrap algorithm is empty") + public void encryptEmptyKeyWrapAlgorithmTest() { + Flux plainTextStream = Flux.just("test data".getBytes(StandardCharsets.UTF_8)); + EncryptRequestAlpha1 request = new EncryptRequestAlpha1( + "mycomponent", + plainTextStream, + "mykey", + "" + ); + + assertThrows(IllegalArgumentException.class, () -> { + previewClient.encrypt(request).blockFirst(); + }); + } + + @Test + @DisplayName("encrypt should throw IllegalArgumentException when plaintext stream is null") + public void encryptNullPlaintextStreamTest() { + EncryptRequestAlpha1 request = new EncryptRequestAlpha1( + "mycomponent", + null, + "mykey", + "RSA-OAEP-256" + ); + + assertThrows(IllegalArgumentException.class, () -> { + previewClient.encrypt(request).blockFirst(); + }); + } + + @Test + @DisplayName("encrypt should successfully encrypt data with required fields") + public void encryptSuccessTest() { + byte[] plaintext = "Hello, World!".getBytes(StandardCharsets.UTF_8); + byte[] encryptedData = "encrypted-data".getBytes(StandardCharsets.UTF_8); + + doAnswer((Answer>) invocation -> { + StreamObserver responseObserver = + (StreamObserver) invocation.getArguments()[0]; + + // Simulate returning encrypted data + DaprProtos.EncryptResponse response = DaprProtos.EncryptResponse.newBuilder() + .setPayload(CommonProtos.StreamPayload.newBuilder() + .setData(ByteString.copyFrom(encryptedData)) + .setSeq(0) + .build()) + .build(); + responseObserver.onNext(response); + responseObserver.onCompleted(); + + return mock(StreamObserver.class); + }).when(daprStub).encryptAlpha1(any()); + + Flux plainTextStream = Flux.just(plaintext); + EncryptRequestAlpha1 request = new EncryptRequestAlpha1( + "mycomponent", + plainTextStream, + "mykey", + "RSA-OAEP-256" + ); + + List results = previewClient.encrypt(request).collectList().block(); + + assertNotNull(results); + assertEquals(1, results.size()); + assertArrayEquals(encryptedData, results.get(0)); + } + + @Test + @DisplayName("encrypt should handle multiple response chunks") + public void encryptMultipleChunksResponseTest() { + byte[] plaintext = "Hello, World!".getBytes(StandardCharsets.UTF_8); + byte[] chunk1 = "chunk1".getBytes(StandardCharsets.UTF_8); + byte[] chunk2 = "chunk2".getBytes(StandardCharsets.UTF_8); + byte[] chunk3 = "chunk3".getBytes(StandardCharsets.UTF_8); + + doAnswer((Answer>) invocation -> { + StreamObserver responseObserver = + (StreamObserver) invocation.getArguments()[0]; + + // Simulate returning multiple chunks + responseObserver.onNext(DaprProtos.EncryptResponse.newBuilder() + .setPayload(CommonProtos.StreamPayload.newBuilder() + .setData(ByteString.copyFrom(chunk1)) + .setSeq(0) + .build()) + .build()); + responseObserver.onNext(DaprProtos.EncryptResponse.newBuilder() + .setPayload(CommonProtos.StreamPayload.newBuilder() + .setData(ByteString.copyFrom(chunk2)) + .setSeq(1) + .build()) + .build()); + responseObserver.onNext(DaprProtos.EncryptResponse.newBuilder() + .setPayload(CommonProtos.StreamPayload.newBuilder() + .setData(ByteString.copyFrom(chunk3)) + .setSeq(2) + .build()) + .build()); + responseObserver.onCompleted(); + + return mock(StreamObserver.class); + }).when(daprStub).encryptAlpha1(any()); + + Flux plainTextStream = Flux.just(plaintext); + EncryptRequestAlpha1 request = new EncryptRequestAlpha1( + "mycomponent", + plainTextStream, + "mykey", + "RSA-OAEP-256" + ); + + List results = previewClient.encrypt(request).collectList().block(); + + assertNotNull(results); + assertEquals(3, results.size()); + assertArrayEquals(chunk1, results.get(0)); + assertArrayEquals(chunk2, results.get(1)); + assertArrayEquals(chunk3, results.get(2)); + } + + @Test + @DisplayName("encrypt should handle optional data encryption cipher") + public void encryptWithDataEncryptionCipherTest() { + byte[] plaintext = "Hello, World!".getBytes(StandardCharsets.UTF_8); + byte[] encryptedData = "encrypted-data".getBytes(StandardCharsets.UTF_8); + + doAnswer((Answer>) invocation -> { + StreamObserver responseObserver = + (StreamObserver) invocation.getArguments()[0]; + + DaprProtos.EncryptResponse response = DaprProtos.EncryptResponse.newBuilder() + .setPayload(CommonProtos.StreamPayload.newBuilder() + .setData(ByteString.copyFrom(encryptedData)) + .setSeq(0) + .build()) + .build(); + responseObserver.onNext(response); + responseObserver.onCompleted(); + + return mock(StreamObserver.class); + }).when(daprStub).encryptAlpha1(any()); + + Flux plainTextStream = Flux.just(plaintext); + EncryptRequestAlpha1 request = new EncryptRequestAlpha1( + "mycomponent", + plainTextStream, + "mykey", + "RSA-OAEP-256" + ).setDataEncryptionCipher("aes-gcm"); + + List results = previewClient.encrypt(request).collectList().block(); + + assertNotNull(results); + assertEquals(1, results.size()); + } + + @Test + @DisplayName("encrypt should handle omit decryption key name option") + public void encryptWithOmitDecryptionKeyNameTest() { + byte[] plaintext = "Hello, World!".getBytes(StandardCharsets.UTF_8); + byte[] encryptedData = "encrypted-data".getBytes(StandardCharsets.UTF_8); + + doAnswer((Answer>) invocation -> { + StreamObserver responseObserver = + (StreamObserver) invocation.getArguments()[0]; + + DaprProtos.EncryptResponse response = DaprProtos.EncryptResponse.newBuilder() + .setPayload(CommonProtos.StreamPayload.newBuilder() + .setData(ByteString.copyFrom(encryptedData)) + .setSeq(0) + .build()) + .build(); + responseObserver.onNext(response); + responseObserver.onCompleted(); + + return mock(StreamObserver.class); + }).when(daprStub).encryptAlpha1(any()); + + Flux plainTextStream = Flux.just(plaintext); + EncryptRequestAlpha1 request = new EncryptRequestAlpha1( + "mycomponent", + plainTextStream, + "mykey", + "RSA-OAEP-256" + ).setOmitDecryptionKeyName(true); + + List results = previewClient.encrypt(request).collectList().block(); + + assertNotNull(results); + assertEquals(1, results.size()); + } + + @Test + @DisplayName("encrypt should handle decryption key name option") + public void encryptWithDecryptionKeyNameTest() { + byte[] plaintext = "Hello, World!".getBytes(StandardCharsets.UTF_8); + byte[] encryptedData = "encrypted-data".getBytes(StandardCharsets.UTF_8); + + doAnswer((Answer>) invocation -> { + StreamObserver responseObserver = + (StreamObserver) invocation.getArguments()[0]; + + DaprProtos.EncryptResponse response = DaprProtos.EncryptResponse.newBuilder() + .setPayload(CommonProtos.StreamPayload.newBuilder() + .setData(ByteString.copyFrom(encryptedData)) + .setSeq(0) + .build()) + .build(); + responseObserver.onNext(response); + responseObserver.onCompleted(); + + return mock(StreamObserver.class); + }).when(daprStub).encryptAlpha1(any()); + + Flux plainTextStream = Flux.just(plaintext); + EncryptRequestAlpha1 request = new EncryptRequestAlpha1( + "mycomponent", + plainTextStream, + "mykey", + "RSA-OAEP-256" + ).setDecryptionKeyName("different-key"); + + List results = previewClient.encrypt(request).collectList().block(); + + assertNotNull(results); + assertEquals(1, results.size()); + } + + @Test + @DisplayName("encrypt should handle all optional fields") + public void encryptWithAllOptionalFieldsTest() { + byte[] plaintext = "Hello, World!".getBytes(StandardCharsets.UTF_8); + byte[] encryptedData = "encrypted-data".getBytes(StandardCharsets.UTF_8); + + doAnswer((Answer>) invocation -> { + StreamObserver responseObserver = + (StreamObserver) invocation.getArguments()[0]; + + DaprProtos.EncryptResponse response = DaprProtos.EncryptResponse.newBuilder() + .setPayload(CommonProtos.StreamPayload.newBuilder() + .setData(ByteString.copyFrom(encryptedData)) + .setSeq(0) + .build()) + .build(); + responseObserver.onNext(response); + responseObserver.onCompleted(); + + return mock(StreamObserver.class); + }).when(daprStub).encryptAlpha1(any()); + + Flux plainTextStream = Flux.just(plaintext); + EncryptRequestAlpha1 request = new EncryptRequestAlpha1( + "mycomponent", + plainTextStream, + "mykey", + "RSA-OAEP-256" + ) + .setDataEncryptionCipher("chacha20-poly1305") + .setOmitDecryptionKeyName(true) + .setDecryptionKeyName("decrypt-key"); + + List results = previewClient.encrypt(request).collectList().block(); + + assertNotNull(results); + assertEquals(1, results.size()); + } + + @Test + @DisplayName("encrypt should filter empty data from response") + public void encryptFilterEmptyDataTest() { + byte[] plaintext = "Hello, World!".getBytes(StandardCharsets.UTF_8); + byte[] validData = "valid-data".getBytes(StandardCharsets.UTF_8); + + doAnswer((Answer>) invocation -> { + StreamObserver responseObserver = + (StreamObserver) invocation.getArguments()[0]; + + // Send empty data - should be filtered + responseObserver.onNext(DaprProtos.EncryptResponse.newBuilder() + .setPayload(CommonProtos.StreamPayload.newBuilder() + .setData(ByteString.EMPTY) + .setSeq(0) + .build()) + .build()); + + // Send valid data + responseObserver.onNext(DaprProtos.EncryptResponse.newBuilder() + .setPayload(CommonProtos.StreamPayload.newBuilder() + .setData(ByteString.copyFrom(validData)) + .setSeq(1) + .build()) + .build()); + + responseObserver.onCompleted(); + + return mock(StreamObserver.class); + }).when(daprStub).encryptAlpha1(any()); + + Flux plainTextStream = Flux.just(plaintext); + EncryptRequestAlpha1 request = new EncryptRequestAlpha1( + "mycomponent", + plainTextStream, + "mykey", + "RSA-OAEP-256" + ); + + List results = previewClient.encrypt(request).collectList().block(); + + assertNotNull(results); + assertEquals(1, results.size()); + assertArrayEquals(validData, results.get(0)); + } + + // ==================== Decrypt Tests ==================== + + @Test + @DisplayName("decrypt should throw IllegalArgumentException when request is null") + public void decryptNullRequestTest() { + assertThrows(IllegalArgumentException.class, () -> { + previewClient.decrypt(null).blockFirst(); + }); + } + + @Test + @DisplayName("decrypt should throw IllegalArgumentException when component name is null") + public void decryptNullComponentNameTest() { + Flux cipherTextStream = Flux.just("encrypted data".getBytes(StandardCharsets.UTF_8)); + DecryptRequestAlpha1 request = new DecryptRequestAlpha1(null, cipherTextStream); + + assertThrows(IllegalArgumentException.class, () -> { + previewClient.decrypt(request).blockFirst(); + }); + } + + @Test + @DisplayName("decrypt should throw IllegalArgumentException when component name is empty") + public void decryptEmptyComponentNameTest() { + Flux cipherTextStream = Flux.just("encrypted data".getBytes(StandardCharsets.UTF_8)); + DecryptRequestAlpha1 request = new DecryptRequestAlpha1("", cipherTextStream); + + assertThrows(IllegalArgumentException.class, () -> { + previewClient.decrypt(request).blockFirst(); + }); + } + + @Test + @DisplayName("decrypt should throw IllegalArgumentException when component name is whitespace only") + public void decryptWhitespaceComponentNameTest() { + Flux cipherTextStream = Flux.just("encrypted data".getBytes(StandardCharsets.UTF_8)); + DecryptRequestAlpha1 request = new DecryptRequestAlpha1(" ", cipherTextStream); + + assertThrows(IllegalArgumentException.class, () -> { + previewClient.decrypt(request).blockFirst(); + }); + } + + @Test + @DisplayName("decrypt should throw IllegalArgumentException when ciphertext stream is null") + public void decryptNullCiphertextStreamTest() { + DecryptRequestAlpha1 request = new DecryptRequestAlpha1("mycomponent", null); + + assertThrows(IllegalArgumentException.class, () -> { + previewClient.decrypt(request).blockFirst(); + }); + } + + @Test + @DisplayName("decrypt should successfully decrypt data with required fields") + public void decryptSuccessTest() { + byte[] ciphertext = "encrypted-data".getBytes(StandardCharsets.UTF_8); + byte[] decryptedData = "Hello, World!".getBytes(StandardCharsets.UTF_8); + + doAnswer((Answer>) invocation -> { + StreamObserver responseObserver = + (StreamObserver) invocation.getArguments()[0]; + + DaprProtos.DecryptResponse response = DaprProtos.DecryptResponse.newBuilder() + .setPayload(CommonProtos.StreamPayload.newBuilder() + .setData(ByteString.copyFrom(decryptedData)) + .setSeq(0) + .build()) + .build(); + responseObserver.onNext(response); + responseObserver.onCompleted(); + + return mock(StreamObserver.class); + }).when(daprStub).decryptAlpha1(any()); + + Flux cipherTextStream = Flux.just(ciphertext); + DecryptRequestAlpha1 request = new DecryptRequestAlpha1("mycomponent", cipherTextStream); + + List results = previewClient.decrypt(request).collectList().block(); + + assertNotNull(results); + assertEquals(1, results.size()); + assertArrayEquals(decryptedData, results.get(0)); + } + + @Test + @DisplayName("decrypt should handle multiple response chunks") + public void decryptMultipleChunksResponseTest() { + byte[] ciphertext = "encrypted-data".getBytes(StandardCharsets.UTF_8); + byte[] chunk1 = "chunk1".getBytes(StandardCharsets.UTF_8); + byte[] chunk2 = "chunk2".getBytes(StandardCharsets.UTF_8); + byte[] chunk3 = "chunk3".getBytes(StandardCharsets.UTF_8); + + doAnswer((Answer>) invocation -> { + StreamObserver responseObserver = + (StreamObserver) invocation.getArguments()[0]; + + responseObserver.onNext(DaprProtos.DecryptResponse.newBuilder() + .setPayload(CommonProtos.StreamPayload.newBuilder() + .setData(ByteString.copyFrom(chunk1)) + .setSeq(0) + .build()) + .build()); + responseObserver.onNext(DaprProtos.DecryptResponse.newBuilder() + .setPayload(CommonProtos.StreamPayload.newBuilder() + .setData(ByteString.copyFrom(chunk2)) + .setSeq(1) + .build()) + .build()); + responseObserver.onNext(DaprProtos.DecryptResponse.newBuilder() + .setPayload(CommonProtos.StreamPayload.newBuilder() + .setData(ByteString.copyFrom(chunk3)) + .setSeq(2) + .build()) + .build()); + responseObserver.onCompleted(); + + return mock(StreamObserver.class); + }).when(daprStub).decryptAlpha1(any()); + + Flux cipherTextStream = Flux.just(ciphertext); + DecryptRequestAlpha1 request = new DecryptRequestAlpha1("mycomponent", cipherTextStream); + + List results = previewClient.decrypt(request).collectList().block(); + + assertNotNull(results); + assertEquals(3, results.size()); + assertArrayEquals(chunk1, results.get(0)); + assertArrayEquals(chunk2, results.get(1)); + assertArrayEquals(chunk3, results.get(2)); + } + + @Test + @DisplayName("decrypt should handle optional key name") + public void decryptWithKeyNameTest() { + byte[] ciphertext = "encrypted-data".getBytes(StandardCharsets.UTF_8); + byte[] decryptedData = "Hello, World!".getBytes(StandardCharsets.UTF_8); + + doAnswer((Answer>) invocation -> { + StreamObserver responseObserver = + (StreamObserver) invocation.getArguments()[0]; + + DaprProtos.DecryptResponse response = DaprProtos.DecryptResponse.newBuilder() + .setPayload(CommonProtos.StreamPayload.newBuilder() + .setData(ByteString.copyFrom(decryptedData)) + .setSeq(0) + .build()) + .build(); + responseObserver.onNext(response); + responseObserver.onCompleted(); + + return mock(StreamObserver.class); + }).when(daprStub).decryptAlpha1(any()); + + Flux cipherTextStream = Flux.just(ciphertext); + DecryptRequestAlpha1 request = new DecryptRequestAlpha1("mycomponent", cipherTextStream) + .setKeyName("mykey"); + + List results = previewClient.decrypt(request).collectList().block(); + + assertNotNull(results); + assertEquals(1, results.size()); + assertArrayEquals(decryptedData, results.get(0)); + } + + @Test + @DisplayName("decrypt should filter empty data from response") + public void decryptFilterEmptyDataTest() { + byte[] ciphertext = "encrypted-data".getBytes(StandardCharsets.UTF_8); + byte[] validData = "valid-data".getBytes(StandardCharsets.UTF_8); + + doAnswer((Answer>) invocation -> { + StreamObserver responseObserver = + (StreamObserver) invocation.getArguments()[0]; + + // Send empty data - should be filtered + responseObserver.onNext(DaprProtos.DecryptResponse.newBuilder() + .setPayload(CommonProtos.StreamPayload.newBuilder() + .setData(ByteString.EMPTY) + .setSeq(0) + .build()) + .build()); + + // Send valid data + responseObserver.onNext(DaprProtos.DecryptResponse.newBuilder() + .setPayload(CommonProtos.StreamPayload.newBuilder() + .setData(ByteString.copyFrom(validData)) + .setSeq(1) + .build()) + .build()); + + responseObserver.onCompleted(); + + return mock(StreamObserver.class); + }).when(daprStub).decryptAlpha1(any()); + + Flux cipherTextStream = Flux.just(ciphertext); + DecryptRequestAlpha1 request = new DecryptRequestAlpha1("mycomponent", cipherTextStream); + + List results = previewClient.decrypt(request).collectList().block(); + + assertNotNull(results); + assertEquals(1, results.size()); + assertArrayEquals(validData, results.get(0)); + } + + @Test + @DisplayName("decrypt should handle key name with version") + public void decryptWithKeyNameVersionTest() { + byte[] ciphertext = "encrypted-data".getBytes(StandardCharsets.UTF_8); + byte[] decryptedData = "Hello, World!".getBytes(StandardCharsets.UTF_8); + + doAnswer((Answer>) invocation -> { + StreamObserver responseObserver = + (StreamObserver) invocation.getArguments()[0]; + + DaprProtos.DecryptResponse response = DaprProtos.DecryptResponse.newBuilder() + .setPayload(CommonProtos.StreamPayload.newBuilder() + .setData(ByteString.copyFrom(decryptedData)) + .setSeq(0) + .build()) + .build(); + responseObserver.onNext(response); + responseObserver.onCompleted(); + + return mock(StreamObserver.class); + }).when(daprStub).decryptAlpha1(any()); + + Flux cipherTextStream = Flux.just(ciphertext); + DecryptRequestAlpha1 request = new DecryptRequestAlpha1("mycomponent", cipherTextStream) + .setKeyName("mykey/v2"); + + List results = previewClient.decrypt(request).collectList().block(); + + assertNotNull(results); + assertEquals(1, results.size()); + } } 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..499615a030 --- /dev/null +++ b/sdk/src/test/java/io/dapr/client/domain/DecryptRequestAlpha1Test.java @@ -0,0 +1,346 @@ +/* + * 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.DisplayName; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; +import reactor.test.StepVerifier; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +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.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +public class DecryptRequestAlpha1Test { + + private static final String COMPONENT_NAME = "mycomponent"; + private static final String ENCRYPTED_DATA = "encrypted data"; + + @Test + @DisplayName("Constructor should set required fields correctly") + public void testConstructorWithRequiredFields() { + Flux cipherTextStream = Flux.just(ENCRYPTED_DATA.getBytes(StandardCharsets.UTF_8)); + + DecryptRequestAlpha1 request = new DecryptRequestAlpha1( + COMPONENT_NAME, + cipherTextStream + ); + + assertEquals(COMPONENT_NAME, request.getComponentName()); + assertNotNull(request.getCipherTextStream()); + assertNull(request.getKeyName()); + } + + @Test + @DisplayName("setKeyName should set key name correctly") + public void testFluentSetKeyName() { + Flux cipherTextStream = Flux.just(ENCRYPTED_DATA.getBytes(StandardCharsets.UTF_8)); + + DecryptRequestAlpha1 request = new DecryptRequestAlpha1( + COMPONENT_NAME, + cipherTextStream + ).setKeyName("mykey"); + + assertEquals("mykey", request.getKeyName()); + } + + @Test + @DisplayName("setKeyName should return same instance for method chaining") + public void testFluentSetterReturnsSameInstance() { + Flux cipherTextStream = Flux.just(ENCRYPTED_DATA.getBytes(StandardCharsets.UTF_8)); + + DecryptRequestAlpha1 request = new DecryptRequestAlpha1( + COMPONENT_NAME, + cipherTextStream + ); + + DecryptRequestAlpha1 sameRequest = request.setKeyName("mykey"); + assertSame(request, sameRequest); + } + + @Test + @DisplayName("Constructor should accept null component name") + public void testNullComponentName() { + Flux cipherTextStream = Flux.just(ENCRYPTED_DATA.getBytes(StandardCharsets.UTF_8)); + + DecryptRequestAlpha1 request = new DecryptRequestAlpha1( + null, + cipherTextStream + ); + + assertNull(request.getComponentName()); + } + + @Test + @DisplayName("Constructor should accept null ciphertext stream") + public void testNullCipherTextStream() { + DecryptRequestAlpha1 request = new DecryptRequestAlpha1( + COMPONENT_NAME, + null + ); + + assertNull(request.getCipherTextStream()); + } + + @Test + @DisplayName("Constructor should accept empty stream") + public void testEmptyStream() { + Flux emptyStream = Flux.empty(); + + DecryptRequestAlpha1 request = new DecryptRequestAlpha1( + COMPONENT_NAME, + emptyStream + ); + + assertNotNull(request.getCipherTextStream()); + StepVerifier.create(request.getCipherTextStream()) + .verifyComplete(); + } + + @Test + @DisplayName("Should handle multiple chunks in stream") + public void testMultipleChunksStream() { + byte[] chunk1 = "chunk1".getBytes(StandardCharsets.UTF_8); + byte[] chunk2 = "chunk2".getBytes(StandardCharsets.UTF_8); + byte[] chunk3 = "chunk3".getBytes(StandardCharsets.UTF_8); + + Flux multiChunkStream = Flux.just(chunk1, chunk2, chunk3); + + DecryptRequestAlpha1 request = new DecryptRequestAlpha1( + COMPONENT_NAME, + multiChunkStream + ); + + assertNotNull(request.getCipherTextStream()); + + List collectedChunks = new ArrayList<>(); + StepVerifier.create(request.getCipherTextStream()) + .recordWith(() -> collectedChunks) + .expectNextCount(3) + .verifyComplete(); + + assertEquals(3, collectedChunks.size()); + assertArrayEquals(chunk1, collectedChunks.get(0)); + assertArrayEquals(chunk2, collectedChunks.get(1)); + assertArrayEquals(chunk3, collectedChunks.get(2)); + } + + @Test + @DisplayName("setKeyName should accept null value") + public void testSetKeyNameNull() { + Flux cipherTextStream = Flux.just(ENCRYPTED_DATA.getBytes(StandardCharsets.UTF_8)); + + DecryptRequestAlpha1 request = new DecryptRequestAlpha1( + COMPONENT_NAME, + cipherTextStream + ) + .setKeyName("some-key") + .setKeyName(null); + + assertNull(request.getKeyName()); + } + + @Test + @DisplayName("Should handle key name with version") + public void testKeyNameWithVersion() { + Flux cipherTextStream = Flux.just(ENCRYPTED_DATA.getBytes(StandardCharsets.UTF_8)); + String keyNameWithVersion = "mykey/v1"; + + DecryptRequestAlpha1 request = new DecryptRequestAlpha1( + COMPONENT_NAME, + cipherTextStream + ).setKeyName(keyNameWithVersion); + + assertEquals(keyNameWithVersion, request.getKeyName()); + } + + @Test + @DisplayName("Should handle empty component name") + public void testEmptyComponentName() { + Flux cipherTextStream = Flux.just(ENCRYPTED_DATA.getBytes(StandardCharsets.UTF_8)); + + DecryptRequestAlpha1 request = new DecryptRequestAlpha1( + "", + cipherTextStream + ); + + assertEquals("", request.getComponentName()); + } + + @Test + @DisplayName("Should handle whitespace-only component name") + public void testWhitespaceComponentName() { + Flux cipherTextStream = Flux.just(ENCRYPTED_DATA.getBytes(StandardCharsets.UTF_8)); + + DecryptRequestAlpha1 request = new DecryptRequestAlpha1( + " ", + cipherTextStream + ); + + assertEquals(" ", request.getComponentName()); + } + + @Test + @DisplayName("Should handle empty key name") + public void testEmptyKeyName() { + Flux cipherTextStream = Flux.just(ENCRYPTED_DATA.getBytes(StandardCharsets.UTF_8)); + + DecryptRequestAlpha1 request = new DecryptRequestAlpha1( + COMPONENT_NAME, + cipherTextStream + ).setKeyName(""); + + assertEquals("", request.getKeyName()); + } + + @Test + @DisplayName("Should handle large data stream") + public void testLargeDataStream() { + byte[] largeChunk = new byte[1024 * 1024]; // 1MB chunk + for (int i = 0; i < largeChunk.length; i++) { + largeChunk[i] = (byte) (i % 256); + } + + Flux largeStream = Flux.just(largeChunk); + + DecryptRequestAlpha1 request = new DecryptRequestAlpha1( + COMPONENT_NAME, + largeStream + ); + + assertNotNull(request.getCipherTextStream()); + + StepVerifier.create(request.getCipherTextStream()) + .expectNextMatches(data -> data.length == 1024 * 1024) + .verifyComplete(); + } + + @Test + @DisplayName("Should handle empty byte array in stream") + public void testEmptyByteArrayInStream() { + byte[] emptyArray = new byte[0]; + + Flux stream = Flux.just(emptyArray); + + DecryptRequestAlpha1 request = new DecryptRequestAlpha1( + COMPONENT_NAME, + stream + ); + + assertNotNull(request.getCipherTextStream()); + + StepVerifier.create(request.getCipherTextStream()) + .expectNextMatches(data -> data.length == 0) + .verifyComplete(); + } + + @Test + @DisplayName("Should handle stream with binary data") + public void testStreamWithBinaryData() { + byte[] binaryData = new byte[] {0x00, 0x01, 0x02, (byte) 0xFF, (byte) 0xFE, (byte) 0xFD}; + + Flux stream = Flux.just(binaryData); + + DecryptRequestAlpha1 request = new DecryptRequestAlpha1( + COMPONENT_NAME, + stream + ); + + StepVerifier.create(request.getCipherTextStream()) + .expectNextMatches(data -> { + if (data.length != binaryData.length) return false; + for (int i = 0; i < data.length; i++) { + if (data[i] != binaryData[i]) return false; + } + return true; + }) + .verifyComplete(); + } + + @Test + @DisplayName("Complete decryption request with key name") + public void testCompleteConfiguration() { + Flux cipherTextStream = Flux.just(ENCRYPTED_DATA.getBytes(StandardCharsets.UTF_8)); + + DecryptRequestAlpha1 request = new DecryptRequestAlpha1( + COMPONENT_NAME, + cipherTextStream + ).setKeyName("decryption-key/v2"); + + assertEquals(COMPONENT_NAME, request.getComponentName()); + assertNotNull(request.getCipherTextStream()); + assertEquals("decryption-key/v2", request.getKeyName()); + } + + @Test + @DisplayName("Should handle multiple setKeyName calls") + public void testMultipleSetKeyNameCalls() { + Flux cipherTextStream = Flux.just(ENCRYPTED_DATA.getBytes(StandardCharsets.UTF_8)); + + DecryptRequestAlpha1 request = new DecryptRequestAlpha1( + COMPONENT_NAME, + cipherTextStream + ) + .setKeyName("key1") + .setKeyName("key2") + .setKeyName("key3"); + + assertEquals("key3", request.getKeyName()); + } + + @Test + @DisplayName("Should handle many chunks stream") + public void testManyChunksStream() { + int numberOfChunks = 100; + List chunks = new ArrayList<>(); + for (int i = 0; i < numberOfChunks; i++) { + chunks.add(("chunk" + i).getBytes(StandardCharsets.UTF_8)); + } + + Flux manyChunksStream = Flux.fromIterable(chunks); + + DecryptRequestAlpha1 request = new DecryptRequestAlpha1( + COMPONENT_NAME, + manyChunksStream + ); + + assertNotNull(request.getCipherTextStream()); + + StepVerifier.create(request.getCipherTextStream()) + .expectNextCount(numberOfChunks) + .verifyComplete(); + } + + @Test + @DisplayName("Should handle stream with special characters in data") + public void testStreamWithSpecialCharacters() { + String specialData = "特殊字符 🔓 データ"; + Flux stream = Flux.just(specialData.getBytes(StandardCharsets.UTF_8)); + + DecryptRequestAlpha1 request = new DecryptRequestAlpha1( + COMPONENT_NAME, + stream + ); + + StepVerifier.create(request.getCipherTextStream()) + .expectNextMatches(data -> new String(data, StandardCharsets.UTF_8).equals(specialData)) + .verifyComplete(); + } +} 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..b4479ce948 --- /dev/null +++ b/sdk/src/test/java/io/dapr/client/domain/EncryptRequestAlpha1Test.java @@ -0,0 +1,476 @@ +/* + * 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.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import reactor.core.publisher.Flux; +import reactor.test.StepVerifier; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +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.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class EncryptRequestAlpha1Test { + + private static final String COMPONENT_NAME = "mycomponent"; + private static final String KEY_NAME = "mykey"; + private static final String KEY_WRAP_ALGORITHM = "RSA-OAEP-256"; + private static final String TEST_DATA = "test data"; + + @Test + @DisplayName("Constructor should set all required fields correctly") + public void testConstructorWithRequiredFields() { + Flux plainTextStream = Flux.just(TEST_DATA.getBytes(StandardCharsets.UTF_8)); + + EncryptRequestAlpha1 request = new EncryptRequestAlpha1( + COMPONENT_NAME, + plainTextStream, + KEY_NAME, + KEY_WRAP_ALGORITHM + ); + + assertEquals(COMPONENT_NAME, request.getComponentName()); + assertNotNull(request.getPlainTextStream()); + assertEquals(KEY_NAME, request.getKeyName()); + assertEquals(KEY_WRAP_ALGORITHM, request.getKeyWrapAlgorithm()); + assertNull(request.getDataEncryptionCipher()); + assertFalse(request.isOmitDecryptionKeyName()); + assertNull(request.getDecryptionKeyName()); + } + + @Test + @DisplayName("Fluent setters should set optional fields correctly") + public void testFluentSetters() { + Flux plainTextStream = Flux.just(TEST_DATA.getBytes(StandardCharsets.UTF_8)); + + EncryptRequestAlpha1 request = new EncryptRequestAlpha1( + COMPONENT_NAME, + plainTextStream, + KEY_NAME, + KEY_WRAP_ALGORITHM + ) + .setDataEncryptionCipher("aes-gcm") + .setOmitDecryptionKeyName(true) + .setDecryptionKeyName("decrypt-key"); + + assertEquals("aes-gcm", request.getDataEncryptionCipher()); + assertTrue(request.isOmitDecryptionKeyName()); + assertEquals("decrypt-key", request.getDecryptionKeyName()); + } + + @Test + @DisplayName("Fluent setters should return same instance for method chaining") + public void testFluentSettersReturnSameInstance() { + Flux plainTextStream = Flux.just(TEST_DATA.getBytes(StandardCharsets.UTF_8)); + + EncryptRequestAlpha1 request = new EncryptRequestAlpha1( + COMPONENT_NAME, + plainTextStream, + KEY_NAME, + KEY_WRAP_ALGORITHM + ); + + EncryptRequestAlpha1 sameRequest = request.setDataEncryptionCipher("aes-gcm"); + assertSame(request, sameRequest); + + sameRequest = request.setOmitDecryptionKeyName(true); + assertSame(request, sameRequest); + + sameRequest = request.setDecryptionKeyName("decrypt-key"); + assertSame(request, sameRequest); + } + + @Test + @DisplayName("Constructor should accept null component name") + public void testNullComponentName() { + Flux plainTextStream = Flux.just(TEST_DATA.getBytes(StandardCharsets.UTF_8)); + + EncryptRequestAlpha1 request = new EncryptRequestAlpha1( + null, + plainTextStream, + KEY_NAME, + KEY_WRAP_ALGORITHM + ); + + assertNull(request.getComponentName()); + } + + @Test + @DisplayName("Constructor should accept null plaintext stream") + public void testNullPlainTextStream() { + EncryptRequestAlpha1 request = new EncryptRequestAlpha1( + COMPONENT_NAME, + null, + KEY_NAME, + KEY_WRAP_ALGORITHM + ); + + assertNull(request.getPlainTextStream()); + } + + @Test + @DisplayName("Constructor should accept null key name") + public void testNullKeyName() { + Flux plainTextStream = Flux.just(TEST_DATA.getBytes(StandardCharsets.UTF_8)); + + EncryptRequestAlpha1 request = new EncryptRequestAlpha1( + COMPONENT_NAME, + plainTextStream, + null, + KEY_WRAP_ALGORITHM + ); + + assertNull(request.getKeyName()); + } + + @Test + @DisplayName("Constructor should accept null key wrap algorithm") + public void testNullKeyWrapAlgorithm() { + Flux plainTextStream = Flux.just(TEST_DATA.getBytes(StandardCharsets.UTF_8)); + + EncryptRequestAlpha1 request = new EncryptRequestAlpha1( + COMPONENT_NAME, + plainTextStream, + KEY_NAME, + null + ); + + assertNull(request.getKeyWrapAlgorithm()); + } + + @Test + @DisplayName("Constructor should accept empty stream") + public void testEmptyStream() { + Flux emptyStream = Flux.empty(); + + EncryptRequestAlpha1 request = new EncryptRequestAlpha1( + COMPONENT_NAME, + emptyStream, + KEY_NAME, + KEY_WRAP_ALGORITHM + ); + + assertNotNull(request.getPlainTextStream()); + StepVerifier.create(request.getPlainTextStream()) + .verifyComplete(); + } + + @Test + @DisplayName("Should handle multiple chunks in stream") + public void testMultipleChunksStream() { + byte[] chunk1 = "chunk1".getBytes(StandardCharsets.UTF_8); + byte[] chunk2 = "chunk2".getBytes(StandardCharsets.UTF_8); + byte[] chunk3 = "chunk3".getBytes(StandardCharsets.UTF_8); + + Flux multiChunkStream = Flux.just(chunk1, chunk2, chunk3); + + EncryptRequestAlpha1 request = new EncryptRequestAlpha1( + COMPONENT_NAME, + multiChunkStream, + KEY_NAME, + "A256KW" + ); + + assertNotNull(request.getPlainTextStream()); + assertEquals("A256KW", request.getKeyWrapAlgorithm()); + + List collectedChunks = new ArrayList<>(); + StepVerifier.create(request.getPlainTextStream()) + .recordWith(() -> collectedChunks) + .expectNextCount(3) + .verifyComplete(); + + assertEquals(3, collectedChunks.size()); + assertArrayEquals(chunk1, collectedChunks.get(0)); + assertArrayEquals(chunk2, collectedChunks.get(1)); + assertArrayEquals(chunk3, collectedChunks.get(2)); + } + + @ParameterizedTest + @DisplayName("Should support various key wrap algorithms") + @ValueSource(strings = {"A256KW", "AES", "A128CBC", "A192CBC", "A256CBC", "RSA-OAEP-256", "RSA"}) + public void testVariousKeyWrapAlgorithms(String algorithm) { + Flux plainTextStream = Flux.just(TEST_DATA.getBytes(StandardCharsets.UTF_8)); + + EncryptRequestAlpha1 request = new EncryptRequestAlpha1( + COMPONENT_NAME, + plainTextStream, + KEY_NAME, + algorithm + ); + + assertEquals(algorithm, request.getKeyWrapAlgorithm()); + } + + @ParameterizedTest + @DisplayName("Should support various data encryption ciphers") + @ValueSource(strings = {"aes-gcm", "chacha20-poly1305"}) + public void testVariousDataEncryptionCiphers(String cipher) { + Flux plainTextStream = Flux.just(TEST_DATA.getBytes(StandardCharsets.UTF_8)); + + EncryptRequestAlpha1 request = new EncryptRequestAlpha1( + COMPONENT_NAME, + plainTextStream, + KEY_NAME, + KEY_WRAP_ALGORITHM + ).setDataEncryptionCipher(cipher); + + assertEquals(cipher, request.getDataEncryptionCipher()); + } + + @Test + @DisplayName("setDataEncryptionCipher should accept null value") + public void testSetDataEncryptionCipherNull() { + Flux plainTextStream = Flux.just(TEST_DATA.getBytes(StandardCharsets.UTF_8)); + + EncryptRequestAlpha1 request = new EncryptRequestAlpha1( + COMPONENT_NAME, + plainTextStream, + KEY_NAME, + KEY_WRAP_ALGORITHM + ) + .setDataEncryptionCipher("aes-gcm") + .setDataEncryptionCipher(null); + + assertNull(request.getDataEncryptionCipher()); + } + + @Test + @DisplayName("setDecryptionKeyName should accept null value") + public void testSetDecryptionKeyNameNull() { + Flux plainTextStream = Flux.just(TEST_DATA.getBytes(StandardCharsets.UTF_8)); + + EncryptRequestAlpha1 request = new EncryptRequestAlpha1( + COMPONENT_NAME, + plainTextStream, + KEY_NAME, + KEY_WRAP_ALGORITHM + ) + .setDecryptionKeyName("some-key") + .setDecryptionKeyName(null); + + assertNull(request.getDecryptionKeyName()); + } + + @Test + @DisplayName("setOmitDecryptionKeyName should toggle boolean value") + public void testSetOmitDecryptionKeyNameToggle() { + Flux plainTextStream = Flux.just(TEST_DATA.getBytes(StandardCharsets.UTF_8)); + + EncryptRequestAlpha1 request = new EncryptRequestAlpha1( + COMPONENT_NAME, + plainTextStream, + KEY_NAME, + KEY_WRAP_ALGORITHM + ); + + assertFalse(request.isOmitDecryptionKeyName()); + + request.setOmitDecryptionKeyName(true); + assertTrue(request.isOmitDecryptionKeyName()); + + request.setOmitDecryptionKeyName(false); + assertFalse(request.isOmitDecryptionKeyName()); + } + + @Test + @DisplayName("Should handle large data stream") + public void testLargeDataStream() { + byte[] largeChunk = new byte[1024 * 1024]; // 1MB chunk + for (int i = 0; i < largeChunk.length; i++) { + largeChunk[i] = (byte) (i % 256); + } + + Flux largeStream = Flux.just(largeChunk); + + EncryptRequestAlpha1 request = new EncryptRequestAlpha1( + COMPONENT_NAME, + largeStream, + KEY_NAME, + KEY_WRAP_ALGORITHM + ); + + assertNotNull(request.getPlainTextStream()); + + StepVerifier.create(request.getPlainTextStream()) + .expectNextMatches(data -> data.length == 1024 * 1024) + .verifyComplete(); + } + + @Test + @DisplayName("Should handle empty byte array in stream") + public void testEmptyByteArrayInStream() { + byte[] emptyArray = new byte[0]; + + Flux stream = Flux.just(emptyArray); + + EncryptRequestAlpha1 request = new EncryptRequestAlpha1( + COMPONENT_NAME, + stream, + KEY_NAME, + KEY_WRAP_ALGORITHM + ); + + assertNotNull(request.getPlainTextStream()); + + StepVerifier.create(request.getPlainTextStream()) + .expectNextMatches(data -> data.length == 0) + .verifyComplete(); + } + + @Test + @DisplayName("Should handle key name with version") + public void testKeyNameWithVersion() { + Flux plainTextStream = Flux.just(TEST_DATA.getBytes(StandardCharsets.UTF_8)); + String keyNameWithVersion = "mykey/v1"; + + EncryptRequestAlpha1 request = new EncryptRequestAlpha1( + COMPONENT_NAME, + plainTextStream, + keyNameWithVersion, + KEY_WRAP_ALGORITHM + ); + + assertEquals(keyNameWithVersion, request.getKeyName()); + } + + @Test + @DisplayName("Should handle decryption key name with version") + public void testDecryptionKeyNameWithVersion() { + Flux plainTextStream = Flux.just(TEST_DATA.getBytes(StandardCharsets.UTF_8)); + String decryptionKeyWithVersion = "decrypt-key/v2"; + + EncryptRequestAlpha1 request = new EncryptRequestAlpha1( + COMPONENT_NAME, + plainTextStream, + KEY_NAME, + KEY_WRAP_ALGORITHM + ).setDecryptionKeyName(decryptionKeyWithVersion); + + assertEquals(decryptionKeyWithVersion, request.getDecryptionKeyName()); + } + + @Test + @DisplayName("Should handle empty component name") + public void testEmptyComponentName() { + Flux plainTextStream = Flux.just(TEST_DATA.getBytes(StandardCharsets.UTF_8)); + + EncryptRequestAlpha1 request = new EncryptRequestAlpha1( + "", + plainTextStream, + KEY_NAME, + KEY_WRAP_ALGORITHM + ); + + assertEquals("", request.getComponentName()); + } + + @Test + @DisplayName("Should handle whitespace-only component name") + public void testWhitespaceComponentName() { + Flux plainTextStream = Flux.just(TEST_DATA.getBytes(StandardCharsets.UTF_8)); + + EncryptRequestAlpha1 request = new EncryptRequestAlpha1( + " ", + plainTextStream, + KEY_NAME, + KEY_WRAP_ALGORITHM + ); + + assertEquals(" ", request.getComponentName()); + } + + @Test + @DisplayName("Should handle empty key name") + public void testEmptyKeyName() { + Flux plainTextStream = Flux.just(TEST_DATA.getBytes(StandardCharsets.UTF_8)); + + EncryptRequestAlpha1 request = new EncryptRequestAlpha1( + COMPONENT_NAME, + plainTextStream, + "", + KEY_WRAP_ALGORITHM + ); + + assertEquals("", request.getKeyName()); + } + + @Test + @DisplayName("Should handle empty key wrap algorithm") + public void testEmptyKeyWrapAlgorithm() { + Flux plainTextStream = Flux.just(TEST_DATA.getBytes(StandardCharsets.UTF_8)); + + EncryptRequestAlpha1 request = new EncryptRequestAlpha1( + COMPONENT_NAME, + plainTextStream, + KEY_NAME, + "" + ); + + assertEquals("", request.getKeyWrapAlgorithm()); + } + + @Test + @DisplayName("Should handle stream with special characters in data") + public void testStreamWithSpecialCharacters() { + String specialData = "特殊字符 🔐 データ"; + Flux stream = Flux.just(specialData.getBytes(StandardCharsets.UTF_8)); + + EncryptRequestAlpha1 request = new EncryptRequestAlpha1( + COMPONENT_NAME, + stream, + KEY_NAME, + KEY_WRAP_ALGORITHM + ); + + StepVerifier.create(request.getPlainTextStream()) + .expectNextMatches(data -> new String(data, StandardCharsets.UTF_8).equals(specialData)) + .verifyComplete(); + } + + @Test + @DisplayName("Complete configuration with all optional fields") + public void testCompleteConfiguration() { + Flux plainTextStream = Flux.just(TEST_DATA.getBytes(StandardCharsets.UTF_8)); + + EncryptRequestAlpha1 request = new EncryptRequestAlpha1( + COMPONENT_NAME, + plainTextStream, + KEY_NAME, + KEY_WRAP_ALGORITHM + ) + .setDataEncryptionCipher("chacha20-poly1305") + .setOmitDecryptionKeyName(true) + .setDecryptionKeyName("different-key/v3"); + + assertEquals(COMPONENT_NAME, request.getComponentName()); + assertNotNull(request.getPlainTextStream()); + assertEquals(KEY_NAME, request.getKeyName()); + assertEquals(KEY_WRAP_ALGORITHM, request.getKeyWrapAlgorithm()); + assertEquals("chacha20-poly1305", request.getDataEncryptionCipher()); + assertTrue(request.isOmitDecryptionKeyName()); + assertEquals("different-key/v3", request.getDecryptionKeyName()); + } +}