From fd078df8ca3027fd6f5dd036dea3a623b2f77a36 Mon Sep 17 00:00:00 2001 From: sirivarma Date: Mon, 1 Dec 2025 13:52:32 -0800 Subject: [PATCH 01/10] Add crypto Signed-off-by: sirivarma --- .../java/io/dapr/client/DaprClientImpl.java | 192 ++++++++++++++++++ .../io/dapr/client/DaprPreviewClient.java | 22 ++ .../client/domain/DecryptRequestAlpha1.java | 79 +++++++ .../client/domain/EncryptRequestAlpha1.java | 152 ++++++++++++++ 4 files changed, 445 insertions(+) create mode 100644 sdk/src/main/java/io/dapr/client/domain/DecryptRequestAlpha1.java create mode 100644 sdk/src/main/java/io/dapr/client/domain/EncryptRequestAlpha1.java diff --git a/sdk/src/main/java/io/dapr/client/DaprClientImpl.java b/sdk/src/main/java/io/dapr/client/DaprClientImpl.java index 0dfb1b644b..cc36623478 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,194 @@ 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 + 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(); + } + }; + + // Get the request stream observer from gRPC + StreamObserver requestObserver = + intercept(null, asyncStub).encryptAlpha1(responseObserver); + + // 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}; + + // 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 + 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(); + } + }; + + // Get the request stream observer from gRPC + StreamObserver requestObserver = + intercept(null, asyncStub).decryptAlpha1(responseObserver); + + // 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}; + + // 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; + } +} From 18e3e010cff60b544302afed870486c1ac358563 Mon Sep 17 00:00:00 2001 From: sirivarma Date: Mon, 1 Dec 2025 14:23:29 -0800 Subject: [PATCH 02/10] Add tests Signed-off-by: sirivarma --- .../crypto/DaprPreviewClientCryptoIT.java | 368 ++++++++++++++++++ .../java/io/dapr/client/DaprClientImpl.java | 94 ++--- .../domain/DecryptRequestAlpha1Test.java | 115 ++++++ .../domain/EncryptRequestAlpha1Test.java | 148 +++++++ 4 files changed, 679 insertions(+), 46 deletions(-) create mode 100644 sdk-tests/src/test/java/io/dapr/it/testcontainers/crypto/DaprPreviewClientCryptoIT.java create mode 100644 sdk/src/test/java/io/dapr/client/domain/DecryptRequestAlpha1Test.java create mode 100644 sdk/src/test/java/io/dapr/client/domain/EncryptRequestAlpha1Test.java 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..080fe870ba --- /dev/null +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/crypto/DaprPreviewClientCryptoIT.java @@ -0,0 +1,368 @@ +/* + * 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); + } + + @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 cc36623478..05b555b5e9 100644 --- a/sdk/src/main/java/io/dapr/client/DaprClientImpl.java +++ b/sdk/src/main/java/io/dapr/client/DaprClientImpl.java @@ -2135,32 +2135,29 @@ public Flux encrypt(EncryptRequestAlpha1 request) { return Flux.create(sink -> { // Create response observer to receive encrypted data - 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); + 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(); - } - }; + @Override + public void onError(Throwable t) { + sink.error(DaprException.propagate(new DaprException("ENCRYPT_ERROR", + "Error during encryption: " + t.getMessage(), t))); + } - // Get the request stream observer from gRPC - StreamObserver requestObserver = - intercept(null, asyncStub).encryptAlpha1(responseObserver); + @Override + public void onCompleted() { + sink.complete(); + } + }; // Build options for the first message DaprProtos.EncryptRequestOptions.Builder optionsBuilder = DaprProtos.EncryptRequestOptions.newBuilder() @@ -2180,6 +2177,10 @@ public void onCompleted() { 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 -> { @@ -2230,32 +2231,29 @@ public Flux decrypt(DecryptRequestAlpha1 request) { return Flux.create(sink -> { // Create response observer to receive decrypted data - 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); + 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(); - } - }; + @Override + public void onError(Throwable t) { + sink.error(DaprException.propagate(new DaprException("DECRYPT_ERROR", + "Error during decryption: " + t.getMessage(), t))); + } - // Get the request stream observer from gRPC - StreamObserver requestObserver = - intercept(null, asyncStub).decryptAlpha1(responseObserver); + @Override + public void onCompleted() { + sink.complete(); + } + }; // Build options for the first message DaprProtos.DecryptRequestOptions.Builder optionsBuilder = DaprProtos.DecryptRequestOptions.newBuilder() @@ -2269,6 +2267,10 @@ public void onCompleted() { 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 -> { diff --git a/sdk/src/test/java/io/dapr/client/domain/DecryptRequestAlpha1Test.java b/sdk/src/test/java/io/dapr/client/domain/DecryptRequestAlpha1Test.java new file mode 100644 index 0000000000..856da7b13f --- /dev/null +++ b/sdk/src/test/java/io/dapr/client/domain/DecryptRequestAlpha1Test.java @@ -0,0 +1,115 @@ +/* + * Copyright 2024 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.client.domain; + +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; + +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class DecryptRequestAlpha1Test { + + @Test + public void testConstructorWithRequiredFields() { + Flux cipherTextStream = Flux.just("encrypted data".getBytes(StandardCharsets.UTF_8)); + + DecryptRequestAlpha1 request = new DecryptRequestAlpha1( + "mycomponent", + cipherTextStream + ); + + assertEquals("mycomponent", request.getComponentName()); + assertNotNull(request.getCipherTextStream()); + assertNull(request.getKeyName()); + } + + @Test + public void testFluentSetKeyName() { + Flux cipherTextStream = Flux.just("encrypted data".getBytes(StandardCharsets.UTF_8)); + + DecryptRequestAlpha1 request = new DecryptRequestAlpha1( + "mycomponent", + cipherTextStream + ).setKeyName("mykey"); + + assertEquals("mykey", request.getKeyName()); + } + + @Test + public void testFluentSetterReturnsSameInstance() { + Flux cipherTextStream = Flux.just("encrypted data".getBytes(StandardCharsets.UTF_8)); + + DecryptRequestAlpha1 request = new DecryptRequestAlpha1( + "mycomponent", + cipherTextStream + ); + + DecryptRequestAlpha1 sameRequest = request.setKeyName("mykey"); + assertEquals(request, sameRequest); + } + + @Test + public void testNullComponentName() { + Flux cipherTextStream = Flux.just("encrypted data".getBytes(StandardCharsets.UTF_8)); + + DecryptRequestAlpha1 request = new DecryptRequestAlpha1( + null, + cipherTextStream + ); + + assertNull(request.getComponentName()); + } + + @Test + public void testNullCipherTextStream() { + DecryptRequestAlpha1 request = new DecryptRequestAlpha1( + "mycomponent", + null + ); + + assertNull(request.getCipherTextStream()); + } + + @Test + public void testEmptyStream() { + Flux emptyStream = Flux.empty(); + + DecryptRequestAlpha1 request = new DecryptRequestAlpha1( + "mycomponent", + emptyStream + ); + + assertNotNull(request.getCipherTextStream()); + } + + @Test + public void testMultipleChunksStream() { + Flux multiChunkStream = Flux.just( + "chunk1".getBytes(StandardCharsets.UTF_8), + "chunk2".getBytes(StandardCharsets.UTF_8), + "chunk3".getBytes(StandardCharsets.UTF_8) + ); + + DecryptRequestAlpha1 request = new DecryptRequestAlpha1( + "mycomponent", + multiChunkStream + ); + + assertNotNull(request.getCipherTextStream()); + } +} diff --git a/sdk/src/test/java/io/dapr/client/domain/EncryptRequestAlpha1Test.java b/sdk/src/test/java/io/dapr/client/domain/EncryptRequestAlpha1Test.java new file mode 100644 index 0000000000..b02b2c92eb --- /dev/null +++ b/sdk/src/test/java/io/dapr/client/domain/EncryptRequestAlpha1Test.java @@ -0,0 +1,148 @@ +/* + * Copyright 2024 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.client.domain; + +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; + +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class EncryptRequestAlpha1Test { + + @Test + public void testConstructorWithRequiredFields() { + Flux plainTextStream = Flux.just("test data".getBytes(StandardCharsets.UTF_8)); + + EncryptRequestAlpha1 request = new EncryptRequestAlpha1( + "mycomponent", + plainTextStream, + "mykey", + "RSA-OAEP-256" + ); + + assertEquals("mycomponent", request.getComponentName()); + assertNotNull(request.getPlainTextStream()); + assertEquals("mykey", request.getKeyName()); + assertEquals("RSA-OAEP-256", request.getKeyWrapAlgorithm()); + assertNull(request.getDataEncryptionCipher()); + assertFalse(request.isOmitDecryptionKeyName()); + assertNull(request.getDecryptionKeyName()); + } + + @Test + public void testFluentSetters() { + Flux plainTextStream = Flux.just("test data".getBytes(StandardCharsets.UTF_8)); + + EncryptRequestAlpha1 request = new EncryptRequestAlpha1( + "mycomponent", + plainTextStream, + "mykey", + "RSA-OAEP-256" + ) + .setDataEncryptionCipher("AES-GCM") + .setOmitDecryptionKeyName(true) + .setDecryptionKeyName("decrypt-key"); + + assertEquals("AES-GCM", request.getDataEncryptionCipher()); + assertTrue(request.isOmitDecryptionKeyName()); + assertEquals("decrypt-key", request.getDecryptionKeyName()); + } + + @Test + public void testFluentSettersReturnSameInstance() { + Flux plainTextStream = Flux.just("test data".getBytes(StandardCharsets.UTF_8)); + + EncryptRequestAlpha1 request = new EncryptRequestAlpha1( + "mycomponent", + plainTextStream, + "mykey", + "RSA-OAEP-256" + ); + + EncryptRequestAlpha1 sameRequest = request.setDataEncryptionCipher("AES-GCM"); + assertEquals(request, sameRequest); + + sameRequest = request.setOmitDecryptionKeyName(true); + assertEquals(request, sameRequest); + + sameRequest = request.setDecryptionKeyName("decrypt-key"); + assertEquals(request, sameRequest); + } + + @Test + public void testNullComponentName() { + Flux plainTextStream = Flux.just("test data".getBytes(StandardCharsets.UTF_8)); + + EncryptRequestAlpha1 request = new EncryptRequestAlpha1( + null, + plainTextStream, + "mykey", + "RSA-OAEP-256" + ); + + assertNull(request.getComponentName()); + } + + @Test + public void testNullPlainTextStream() { + EncryptRequestAlpha1 request = new EncryptRequestAlpha1( + "mycomponent", + null, + "mykey", + "RSA-OAEP-256" + ); + + assertNull(request.getPlainTextStream()); + } + + @Test + public void testEmptyStream() { + Flux emptyStream = Flux.empty(); + + EncryptRequestAlpha1 request = new EncryptRequestAlpha1( + "mycomponent", + emptyStream, + "mykey", + "RSA-OAEP-256" + ); + + assertNotNull(request.getPlainTextStream()); + } + + @Test + public void testMultipleChunksStream() { + Flux multiChunkStream = Flux.just( + "chunk1".getBytes(StandardCharsets.UTF_8), + "chunk2".getBytes(StandardCharsets.UTF_8), + "chunk3".getBytes(StandardCharsets.UTF_8) + ); + + EncryptRequestAlpha1 request = new EncryptRequestAlpha1( + "mycomponent", + multiChunkStream, + "mykey", + "A256KW" + ); + + assertNotNull(request.getPlainTextStream()); + assertEquals("A256KW", request.getKeyWrapAlgorithm()); + } +} From 92fbdb82b3f87787d70891b68ef00e9f3c2061da Mon Sep 17 00:00:00 2001 From: sirivarma Date: Mon, 1 Dec 2025 21:14:56 -0800 Subject: [PATCH 03/10] fix things Signed-off-by: sirivarma --- .../it/testcontainers/crypto/DaprPreviewClientCryptoIT.java | 5 +++++ 1 file changed, 5 insertions(+) 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 index 080fe870ba..984be02974 100644 --- 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 @@ -111,6 +111,11 @@ private static void generateAndSaveRsaKeyPair(Path keysDir) throws NoSuchAlgorit 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 From 9ac20586a97c292caa88ceb7660e6f0c958cd35f Mon Sep 17 00:00:00 2001 From: sirivarma Date: Mon, 1 Dec 2025 22:45:18 -0800 Subject: [PATCH 04/10] Add crypto exaple Signed-off-by: sirivarma --- .github/workflows/validate.yml | 5 + examples/components/crypto/localstorage.yaml | 13 ++ .../dapr/examples/crypto/CryptoExample.java | 144 ++++++++++++ .../java/io/dapr/examples/crypto/README.md | 186 +++++++++++++++ .../crypto/StreamingCryptoExample.java | 212 ++++++++++++++++++ .../java/io/dapr/client/DaprClientImpl.java | 2 +- .../dapr/client/ProtobufValueHelperTest.java | 26 +-- 7 files changed, 574 insertions(+), 14 deletions(-) create mode 100644 examples/components/crypto/localstorage.yaml create mode 100644 examples/src/main/java/io/dapr/examples/crypto/CryptoExample.java create mode 100644 examples/src/main/java/io/dapr/examples/crypto/README.md create mode 100644 examples/src/main/java/io/dapr/examples/crypto/StreamingCryptoExample.java diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 727f783df1..a140cea481 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -185,4 +185,9 @@ jobs: working-directory: ./examples run: | mm.py ./src/main/java/io/dapr/examples/pubsub/stream/README.md + - name: Validate crypto example + working-directory: ./examples + run: | + mm.py ./src/main/java/io/dapr/examples/crypto/README.md + diff --git a/examples/components/crypto/localstorage.yaml b/examples/components/crypto/localstorage.yaml new file mode 100644 index 0000000000..ac33f3ca0d --- /dev/null +++ b/examples/components/crypto/localstorage.yaml @@ -0,0 +1,13 @@ +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) + # On Linux/Mac: ~/.dapr/keys + # On Windows: %USERPROFILE%\.dapr\keys + - name: path + value: "${HOME}/.dapr/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..5a3719e4bf --- /dev/null +++ b/examples/src/main/java/io/dapr/examples/crypto/CryptoExample.java @@ -0,0 +1,144 @@ +/* + * 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.nio.charset.StandardCharsets; +import java.util.Map; + +/** + * CryptoExample demonstrates using the Dapr Cryptography building block + * to encrypt and decrypt data using a cryptography component. + * + *

This example shows: + *

    + *
  • Encrypting plaintext data with a specified key and algorithm
  • + *
  • Decrypting ciphertext data back to plaintext
  • + *
  • Working with streaming data for large payloads
  • + *
+ * + *

Prerequisites: + *

    + *
  • Dapr installed and initialized
  • + *
  • A cryptography component configured (e.g., local storage crypto)
  • + *
  • A key pair available for the crypto component
  • + *
+ */ +public class CryptoExample { + + // The crypto component name as defined in the component YAML file + private static final String CRYPTO_COMPONENT_NAME = "localstoragecrypto"; + + // The key name to use for encryption/decryption + private static final String KEY_NAME = "mykey"; + + // The key wrap algorithm - RSA-OAEP-256 for RSA keys + private static final String KEY_WRAP_ALGORITHM = "RSA-OAEP-256"; + + /** + * The main method demonstrating encryption and decryption with Dapr. + * + * @param args Command line arguments (unused). + */ + public static void main(String[] args) { + Map, String> overrides = Map.of( + Properties.HTTP_PORT, "3500", + Properties.GRPC_PORT, "50001" + ); + + try (DaprPreviewClient client = new DaprClientBuilder().withPropertyOverrides(overrides).buildPreviewClient()) { + + // Original message to encrypt + String originalMessage = "Hello, Dapr Cryptography! 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 + ); + + // Collect encrypted data from the stream + byte[] encryptedData = client.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(); + + 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) + ); + + // Collect decrypted data from the stream + byte[] decryptedData = client.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(); + + String decryptedMessage = new String(decryptedData, StandardCharsets.UTF_8); + System.out.println("Decryption successful!"); + System.out.println("Decrypted message: " + decryptedMessage); + System.out.println(); + + // Verify the message matches + 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); + } + } +} 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..5668d75bc1 --- /dev/null +++ b/examples/src/main/java/io/dapr/examples/crypto/README.md @@ -0,0 +1,186 @@ +## 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. + +### Setting Up the Cryptography Component + +Before running the examples, you need to set up a cryptography component. This example uses the local storage crypto component. + +1. Create a directory for your keys: + +```bash +mkdir -p ~/.dapr/keys +``` + +2. Generate an RSA key pair (you can use OpenSSL): + +```bash +# Generate a 4096-bit RSA private key +openssl genrsa -out ~/.dapr/keys/mykey 4096 + +# Extract the public key +openssl rsa -in ~/.dapr/keys/mykey -pubout -out ~/.dapr/keys/mykey.pub +``` + +The component configuration file is already provided in `./components/crypto/localstorage.yaml`. + +### Running the Example + +This example uses the Java SDK Dapr client to **Encrypt and Decrypt** data. + +#### 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 = "mykey"; + private static final String KEY_WRAP_ALGORITHM = "RSA-OAEP-256"; + + public static void main(String[] args) { + try (DaprPreviewClient client = new DaprClientBuilder().buildPreviewClient()) { + + String originalMessage = "Hello, Dapr Cryptography!"; + byte[] plainText = originalMessage.getBytes(StandardCharsets.UTF_8); + + // Encrypt the message + EncryptRequestAlpha1 encryptRequest = new EncryptRequestAlpha1( + CRYPTO_COMPONENT_NAME, + Flux.just(plainText), + KEY_NAME, + KEY_WRAP_ALGORITHM + ); + + byte[] encryptedData = client.encrypt(encryptRequest) + .collectList() + .map(chunks -> /* combine chunks */) + .block(); + + // Decrypt the message + DecryptRequestAlpha1 decryptRequest = new DecryptRequestAlpha1( + CRYPTO_COMPONENT_NAME, + Flux.just(encryptedData) + ); + + byte[] decryptedData = client.decrypt(decryptRequest) + .collectList() + .map(chunks -> /* combine chunks */) + .block(); + } + } +} +``` + +Use the following command to run this example: + + + +```bash +dapr run --resources-path ./components/crypto --app-id crypto-app --dapr-http-port 3500 --dapr-grpc-port 50001 -- java -jar target/dapr-java-sdk-examples-exec.jar io.dapr.examples.crypto.CryptoExample +``` + + + +#### Example 2: Streaming Crypto Example + +`StreamingCryptoExample.java` demonstrates advanced scenarios including: +- Multi-chunk data encryption +- Large data encryption (100KB+) +- Custom encryption ciphers + +```bash +dapr run --resources-path ./components/crypto --app-id crypto-app --dapr-http-port 3500 --dapr-grpc-port 50001 -- java -jar target/dapr-java-sdk-examples-exec.jar io.dapr.examples.crypto.StreamingCryptoExample +``` + +### Sample Output + +``` +=== Dapr Cryptography Example === +Original message: Hello, Dapr Cryptography! This is a secret message. + +Encrypting message... +Encryption successful! +Encrypted data length: 512 bytes + +Decrypting message... +Decryption successful! +Decrypted message: Hello, Dapr Cryptography! 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..85b71e5f03 --- /dev/null +++ b/examples/src/main/java/io/dapr/examples/crypto/StreamingCryptoExample.java @@ -0,0 +1,212 @@ +/* + * 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.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.Random; + +/** + * StreamingCryptoExample demonstrates using the Dapr Cryptography building block + * with streaming data for handling large payloads efficiently. + * + *

This example shows: + *

    + *
  • Encrypting large data using streaming
  • + *
  • Using optional parameters like data encryption cipher
  • + *
  • Handling chunked data for encryption/decryption
  • + *
+ */ +public class StreamingCryptoExample { + + private static final String CRYPTO_COMPONENT_NAME = "localstoragecrypto"; + private static final String KEY_NAME = "mykey"; + private static final String KEY_WRAP_ALGORITHM = "RSA-OAEP-256"; + + /** + * The main method demonstrating streaming encryption and decryption with Dapr. + * + * @param args Command line arguments (unused). + */ + public static void main(String[] args) { + 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); + } + } + + /** + * Demonstrates encrypting data sent in multiple chunks. + */ + private static void demonstrateChunkedEncryption(DaprPreviewClient client) { + // Create multiple data chunks 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); + + // Calculate original data for verification + 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..."); + + // Encrypt with multiple chunks in the stream + 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"); + + // Decrypt + 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) { + // Generate a large data payload (100KB) + int size = 100 * 1024; + byte[] largeData = new byte[size]; + new Random().nextBytes(largeData); + + System.out.println("Original data size: " + size + " bytes (100KB)"); + + // Encrypt + 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)"); + + // Decrypt + 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)"); + + // Verify + 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); + + // Encrypt with custom data encryption cipher + EncryptRequestAlpha1 encryptRequest = new EncryptRequestAlpha1( + CRYPTO_COMPONENT_NAME, + Flux.just(plainText), + KEY_NAME, + KEY_WRAP_ALGORITHM + ).setDataEncryptionCipher("aes-gcm"); // Use AES-GCM cipher + + byte[] encryptedData = collectBytes(client.encrypt(encryptRequest)); + System.out.println("Encrypted with aes-gcm cipher, size: " + encryptedData.length + " bytes"); + + // Decrypt + 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/src/main/java/io/dapr/client/DaprClientImpl.java b/sdk/src/main/java/io/dapr/client/DaprClientImpl.java index 05b555b5e9..d93412e718 100644 --- a/sdk/src/main/java/io/dapr/client/DaprClientImpl.java +++ b/sdk/src/main/java/io/dapr/client/DaprClientImpl.java @@ -2257,7 +2257,7 @@ public void onCompleted() { // Build options for the first message DaprProtos.DecryptRequestOptions.Builder optionsBuilder = DaprProtos.DecryptRequestOptions.newBuilder() - .setComponentName(request.getComponentName()); + .setComponentName(request.getComponentName()) if (request.getKeyName() != null && !request.getKeyName().isEmpty()) { optionsBuilder.setKeyName(request.getKeyName()); 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(); From b0611b9dfcff575b4d3e2d9803c366c4ccc79dd2 Mon Sep 17 00:00:00 2001 From: Siri Varma Vegiraju Date: Tue, 2 Dec 2025 09:23:27 -0800 Subject: [PATCH 05/10] Fix formatting of DecryptRequestOptions builder Signed-off-by: Siri Varma Vegiraju --- sdk/src/main/java/io/dapr/client/DaprClientImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/src/main/java/io/dapr/client/DaprClientImpl.java b/sdk/src/main/java/io/dapr/client/DaprClientImpl.java index d93412e718..05b555b5e9 100644 --- a/sdk/src/main/java/io/dapr/client/DaprClientImpl.java +++ b/sdk/src/main/java/io/dapr/client/DaprClientImpl.java @@ -2257,7 +2257,7 @@ public void onCompleted() { // Build options for the first message DaprProtos.DecryptRequestOptions.Builder optionsBuilder = DaprProtos.DecryptRequestOptions.newBuilder() - .setComponentName(request.getComponentName()) + .setComponentName(request.getComponentName()); if (request.getKeyName() != null && !request.getKeyName().isEmpty()) { optionsBuilder.setKeyName(request.getKeyName()); From e7195676494a99adc6aadc59a44673e176b65923 Mon Sep 17 00:00:00 2001 From: sirivarma Date: Wed, 3 Dec 2025 08:06:26 -0800 Subject: [PATCH 06/10] add component Signed-off-by: sirivarma --- .github/workflows/validate.yml | 9 +- examples/components/crypto/localstorage.yaml | 5 +- .../dapr/examples/crypto/CryptoExample.java | 107 ++- .../java/io/dapr/examples/crypto/README.md | 44 +- .../crypto/StreamingCryptoExample.java | 71 +- sdk-tests/components/secret.json | 2 +- .../client/DaprPreviewClientGrpcTest.java | 664 ++++++++++++++++++ .../domain/DecryptRequestAlpha1Test.java | 265 ++++++- .../domain/EncryptRequestAlpha1Test.java | 404 ++++++++++- 9 files changed, 1417 insertions(+), 154 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index a140cea481..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: | @@ -185,9 +189,6 @@ jobs: working-directory: ./examples run: | mm.py ./src/main/java/io/dapr/examples/pubsub/stream/README.md - - name: Validate crypto example - working-directory: ./examples - run: | - mm.py ./src/main/java/io/dapr/examples/crypto/README.md + diff --git a/examples/components/crypto/localstorage.yaml b/examples/components/crypto/localstorage.yaml index ac33f3ca0d..29134564da 100644 --- a/examples/components/crypto/localstorage.yaml +++ b/examples/components/crypto/localstorage.yaml @@ -7,7 +7,6 @@ spec: version: v1 metadata: # Path to the directory containing keys (PEM files) - # On Linux/Mac: ~/.dapr/keys - # On Windows: %USERPROFILE%\.dapr\keys + # This path is relative to where the dapr run command is executed - name: path - value: "${HOME}/.dapr/keys" + value: "./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 index 5a3719e4bf..977ac76c30 100644 --- a/examples/src/main/java/io/dapr/examples/crypto/CryptoExample.java +++ b/examples/src/main/java/io/dapr/examples/crypto/CryptoExample.java @@ -21,44 +21,50 @@ import io.dapr.config.Property; import reactor.core.publisher.Flux; +import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; import java.util.Map; /** * CryptoExample demonstrates using the Dapr Cryptography building block * to encrypt and decrypt data using a cryptography component. - * + * *

This example shows: *

    *
  • Encrypting plaintext data with a specified key and algorithm
  • *
  • Decrypting ciphertext data back to plaintext
  • - *
  • Working with streaming data for large payloads
  • + *
  • Automatic key generation if keys don't exist
  • *
- * + * *

Prerequisites: *

    *
  • Dapr installed and initialized
  • *
  • A cryptography component configured (e.g., local storage crypto)
  • - *
  • A key pair available for the crypto component
  • *
*/ public class CryptoExample { - // The crypto component name as defined in the component YAML file private static final String CRYPTO_COMPONENT_NAME = "localstoragecrypto"; - - // The key name to use for encryption/decryption - private static final String KEY_NAME = "mykey"; - - // The key wrap algorithm - RSA-OAEP-256 for RSA keys - private static final String KEY_WRAP_ALGORITHM = "RSA-OAEP-256"; + 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) { + 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" @@ -66,8 +72,7 @@ public static void main(String[] args) { try (DaprPreviewClient client = new DaprClientBuilder().withPropertyOverrides(overrides).buildPreviewClient()) { - // Original message to encrypt - String originalMessage = "Hello, Dapr Cryptography! This is a secret message."; + String originalMessage = "This is a secret message"; byte[] plainText = originalMessage.getBytes(StandardCharsets.UTF_8); System.out.println("=== Dapr Cryptography Example ==="); @@ -83,19 +88,9 @@ public static void main(String[] args) { KEY_WRAP_ALGORITHM ); - // Collect encrypted data from the stream byte[] encryptedData = client.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; - }) + .map(CryptoExample::combineChunks) .block(); System.out.println("Encryption successful!"); @@ -109,19 +104,9 @@ public static void main(String[] args) { Flux.just(encryptedData) ); - // Collect decrypted data from the stream byte[] decryptedData = client.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; - }) + .map(CryptoExample::combineChunks) .block(); String decryptedMessage = new String(decryptedData, StandardCharsets.UTF_8); @@ -129,11 +114,10 @@ public static void main(String[] args) { System.out.println("Decrypted message: " + decryptedMessage); System.out.println(); - // Verify the message matches if (originalMessage.equals(decryptedMessage)) { - System.out.println("✓ Success! The decrypted message matches the original."); + System.out.println("SUCCESS: The decrypted message matches the original."); } else { - System.out.println("✗ Error! The decrypted message does not match the original."); + System.out.println("ERROR: The decrypted message does not match the original."); } } catch (Exception e) { @@ -141,4 +125,49 @@ public static void main(String[] args) { 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 index 5668d75bc1..229586084d 100644 --- a/examples/src/main/java/io/dapr/examples/crypto/README.md +++ b/examples/src/main/java/io/dapr/examples/crypto/README.md @@ -43,31 +43,9 @@ cd examples Run `dapr init` to initialize Dapr in Self-Hosted Mode if it's not already initialized. -### Setting Up the Cryptography Component - -Before running the examples, you need to set up a cryptography component. This example uses the local storage crypto component. - -1. Create a directory for your keys: - -```bash -mkdir -p ~/.dapr/keys -``` - -2. Generate an RSA key pair (you can use OpenSSL): - -```bash -# Generate a 4096-bit RSA private key -openssl genrsa -out ~/.dapr/keys/mykey 4096 - -# Extract the public key -openssl rsa -in ~/.dapr/keys/mykey -pubout -out ~/.dapr/keys/mykey.pub -``` - -The component configuration file is already provided in `./components/crypto/localstorage.yaml`. - ### Running the Example -This example uses the Java SDK Dapr client to **Encrypt and Decrypt** data. +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 @@ -76,13 +54,13 @@ This example uses the Java SDK Dapr client to **Encrypt and Decrypt** data. ```java public class CryptoExample { private static final String CRYPTO_COMPONENT_NAME = "localstoragecrypto"; - private static final String KEY_NAME = "mykey"; - private static final String KEY_WRAP_ALGORITHM = "RSA-OAEP-256"; + 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 = "Hello, Dapr Cryptography!"; + + String originalMessage = "This is a secret message"; byte[] plainText = originalMessage.getBytes(StandardCharsets.UTF_8); // Encrypt the message @@ -92,7 +70,7 @@ public class CryptoExample { KEY_NAME, KEY_WRAP_ALGORITHM ); - + byte[] encryptedData = client.encrypt(encryptRequest) .collectList() .map(chunks -> /* combine chunks */) @@ -103,7 +81,7 @@ public class CryptoExample { CRYPTO_COMPONENT_NAME, Flux.just(encryptedData) ); - + byte[] decryptedData = client.decrypt(decryptRequest) .collectList() .map(chunks -> /* combine chunks */) @@ -118,7 +96,7 @@ Use the following command to run this example: ```bash -dapr stop --app-id crypto-app +dapr stop --app-id crypto-app || true && rm -rf ./components/crypto/keys ``` From 04bc78ab8fb6404bf7989e63ce4d821f0f6648b5 Mon Sep 17 00:00:00 2001 From: siri-varma Date: Sun, 14 Dec 2025 17:03:50 -0800 Subject: [PATCH 08/10] Fix key gen Signed-off-by: siri-varma --- examples/components/crypto/keys/.gitkeep | 2 ++ examples/components/crypto/localstorage.yaml | 4 ++-- .../main/java/io/dapr/examples/crypto/README.md | 16 +++++++++++++++- 3 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 examples/components/crypto/keys/.gitkeep 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 index 29134564da..bf188c888d 100644 --- a/examples/components/crypto/localstorage.yaml +++ b/examples/components/crypto/localstorage.yaml @@ -7,6 +7,6 @@ spec: version: v1 metadata: # Path to the directory containing keys (PEM files) - # This path is relative to where the dapr run command is executed + # This path is relative to the resources-path directory - name: path - value: "./components/crypto/keys" + value: "keys" diff --git a/examples/src/main/java/io/dapr/examples/crypto/README.md b/examples/src/main/java/io/dapr/examples/crypto/README.md index 487c25150d..4fb8d75244 100644 --- a/examples/src/main/java/io/dapr/examples/crypto/README.md +++ b/examples/src/main/java/io/dapr/examples/crypto/README.md @@ -93,6 +93,20 @@ public class CryptoExample { 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 stop --app-id crypto-app || true && rm -rf ./components/crypto/keys +dapr stop --app-id crypto-app ``` From f7edec4ad936c1ea9b6c60a7fe007db173b964ca Mon Sep 17 00:00:00 2001 From: siri-varma Date: Sun, 14 Dec 2025 20:46:52 -0800 Subject: [PATCH 09/10] Fix key gen Signed-off-by: siri-varma --- examples/components/crypto/localstorage.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/components/crypto/localstorage.yaml b/examples/components/crypto/localstorage.yaml index bf188c888d..9787977936 100644 --- a/examples/components/crypto/localstorage.yaml +++ b/examples/components/crypto/localstorage.yaml @@ -9,4 +9,4 @@ spec: # Path to the directory containing keys (PEM files) # This path is relative to the resources-path directory - name: path - value: "keys" + value: "/components/crypto/keys" From bb8a9d4d86105a44adc7ce53f0ec23b1db92edcc Mon Sep 17 00:00:00 2001 From: siri-varma Date: Sun, 14 Dec 2025 21:03:14 -0800 Subject: [PATCH 10/10] Fix key gen Signed-off-by: siri-varma --- examples/components/crypto/localstorage.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/components/crypto/localstorage.yaml b/examples/components/crypto/localstorage.yaml index 9787977936..3cb456af29 100644 --- a/examples/components/crypto/localstorage.yaml +++ b/examples/components/crypto/localstorage.yaml @@ -9,4 +9,4 @@ spec: # Path to the directory containing keys (PEM files) # This path is relative to the resources-path directory - name: path - value: "/components/crypto/keys" + value: "/home/runner/work/java-sdk/java-sdk/examples/components/crypto/keys/"