// ------------------------------------------------------------------------ // Copyright 2025 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. // ------------------------------------------------------------------------ #nullable enable using System; using System.Collections.Generic; using System.IO; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; using Google.Protobuf; using Grpc.Core; using Autogenerated = Dapr.Client.Autogen.Grpc.v1; #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously namespace Dapr.Client.Crypto; /// /// Provides the implementation to decrypt a stream of plaintext data with the Dapr runtime. /// internal sealed class DecryptionStreamProcessor : IDisposable { private bool disposed; private readonly Channel> outputChannel = Channel.CreateUnbounded>(); /// /// Surfaces any exceptions encountered while asynchronously processing the inbound and outbound streams. /// internal event EventHandler? OnException; /// /// Sends the provided bytes in chunks to the sidecar for the encryption operation. /// /// The stream containing the bytes to decrypt. /// The call to make to the sidecar to process the encryption operation. /// The size, in bytes, of the streaming blocks. /// The decryption options. /// Token used to cancel the ongoing request. public async Task ProcessStreamAsync( Stream inputStream, AsyncDuplexStreamingCall call, int streamingBlockSizeInBytes, Autogenerated.DecryptRequestOptions options, CancellationToken cancellationToken) { //Read from the input stream and write to the gRPC call _ = Task.Run(async () => { try { await using var bufferedStream = new BufferedStream(inputStream, streamingBlockSizeInBytes); var buffer = new byte[streamingBlockSizeInBytes]; int bytesRead; ulong sequenceNumber = 0; while ((bytesRead = await bufferedStream.ReadAsync(buffer, cancellationToken)) > 0) { var request = new Autogenerated.DecryptRequest { Payload = new Autogenerated.StreamPayload { Data = ByteString.CopyFrom(buffer, 0, bytesRead), Seq = sequenceNumber } }; //Only include the options in the first message if (sequenceNumber == 0) { request.Options = options; } await call.RequestStream.WriteAsync(request, cancellationToken); //Increment the sequence number sequenceNumber++; } } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { // Expected cancellation exception } catch (Exception ex) { OnException?.Invoke(this, ex); } finally { await call.RequestStream.CompleteAsync(); } }, cancellationToken); //Start reading from the gRPC call and writing to the output channel _ = Task.Run(async () => { try { await foreach (var response in call.ResponseStream.ReadAllAsync(cancellationToken)) { await outputChannel.Writer.WriteAsync(response.Payload.Data.Memory, cancellationToken); } } catch (Exception ex) { OnException?.Invoke(this, ex); } finally { outputChannel.Writer.Complete(); } }, cancellationToken); } /// /// Retrieves the processed bytes from the operation from the sidecar and /// returns as an enumerable stream. /// public async IAsyncEnumerable> GetProcessedDataAsync([EnumeratorCancellation] CancellationToken cancellationToken) { await foreach (var data in outputChannel.Reader.ReadAllAsync(cancellationToken)) { yield return data; } } public void Dispose() { Dispose(true); } private void Dispose(bool disposing) { if (!disposed) { if (disposing) { outputChannel.Writer.TryComplete(); } disposed = true; } } }