File: System\Security\Cryptography\CryptoStream.cs
Web Access
Project: src\src\libraries\System.Security.Cryptography\src\System.Security.Cryptography.csproj (System.Security.Cryptography)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Buffers;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
 
namespace System.Security.Cryptography
{
    public class CryptoStream : Stream, IDisposable
    {
        // Member variables
        private readonly Stream _stream;
        private readonly ICryptoTransform _transform;
        private byte[] _inputBuffer;  // read from _stream before _Transform
        private int _inputBufferIndex;
        private readonly int _inputBlockSize;
        private byte[] _outputBuffer; // buffered output of _Transform
        private int _outputBufferIndex;
        private readonly int _outputBlockSize;
        private bool _canRead;
        private bool _canWrite;
        private bool _finalBlockTransformed;
        private SemaphoreSlim? _lazyAsyncActiveSemaphore;
        private readonly bool _leaveOpen;
 
        // Constructors
 
        public CryptoStream(Stream stream, ICryptoTransform transform, CryptoStreamMode mode)
            : this(stream, transform, mode, false)
        {
        }
 
        public CryptoStream(Stream stream, ICryptoTransform transform, CryptoStreamMode mode, bool leaveOpen)
        {
            ArgumentNullException.ThrowIfNull(transform);
 
            _stream = stream;
            _transform = transform;
            _leaveOpen = leaveOpen;
 
            switch (mode)
            {
                case CryptoStreamMode.Read:
                    if (!_stream.CanRead)
                    {
                        throw new ArgumentException(SR.Argument_StreamNotReadable, nameof(stream));
                    }
                    _canRead = true;
                    break;
 
                case CryptoStreamMode.Write:
                    if (!_stream.CanWrite)
                    {
                        throw new ArgumentException(SR.Argument_StreamNotWritable, nameof(stream));
                    }
                    _canWrite = true;
                    break;
 
                default:
                    throw new ArgumentException(SR.Argument_InvalidValue, nameof(mode));
            }
 
            _inputBlockSize = _transform.InputBlockSize;
            _inputBuffer = new byte[_inputBlockSize];
            _outputBlockSize = _transform.OutputBlockSize;
            _outputBuffer = new byte[_outputBlockSize];
        }
 
        public override bool CanRead
        {
            get { return _canRead; }
        }
 
        // For now, assume we can never seek into the middle of a cryptostream
        // and get the state right.  This is too strict.
        public override bool CanSeek
        {
            get { return false; }
        }
 
        public override bool CanWrite
        {
            get { return _canWrite; }
        }
 
        public override long Length
        {
            get { throw new NotSupportedException(SR.NotSupported_UnseekableStream); }
        }
 
        public override long Position
        {
            get { throw new NotSupportedException(SR.NotSupported_UnseekableStream); }
            set { throw new NotSupportedException(SR.NotSupported_UnseekableStream); }
        }
 
        public bool HasFlushedFinalBlock
        {
            get { return _finalBlockTransformed; }
        }
 
        // The flush final block functionality used to be part of close, but that meant you couldn't do something like this:
        // MemoryStream ms = new MemoryStream();
        // CryptoStream cs = new CryptoStream(ms, des.CreateEncryptor(), CryptoStreamMode.Write);
        // cs.Write(foo, 0, foo.Length);
        // cs.Close();
        // and get the encrypted data out of ms, because the cs.Close also closed ms and the data went away.
        // so now do this:
        // cs.Write(foo, 0, foo.Length);
        // cs.FlushFinalBlock() // which can only be called once
        // byte[] ciphertext = ms.ToArray();
        // cs.Close();
        public void FlushFinalBlock() =>
            FlushFinalBlockAsync(useAsync: false, default).AsTask().GetAwaiter().GetResult();
 
        /// <summary>
        /// Asynchronously updates the underlying data source or repository with the
        /// current state of the buffer, then clears the buffer.
        /// </summary>
        /// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is <see cref="CancellationToken.None"/>.</param>
        /// <returns>A task that represents the asynchronous flush operation.</returns>
        public ValueTask FlushFinalBlockAsync(CancellationToken cancellationToken = default)
        {
            if (cancellationToken.IsCancellationRequested)
                return ValueTask.FromCanceled(cancellationToken);
 
            return FlushFinalBlockAsync(useAsync: true, cancellationToken);
        }
 
        private async ValueTask FlushFinalBlockAsync(bool useAsync, CancellationToken cancellationToken)
        {
            if (_finalBlockTransformed)
                throw new NotSupportedException(SR.Cryptography_CryptoStream_FlushFinalBlockTwice);
            _finalBlockTransformed = true;
 
            // Transform and write out the final bytes.
            if (_canWrite)
            {
                Debug.Assert(_outputBufferIndex == 0, "The output index can only ever be non-zero when in read mode.");
 
                byte[] finalBytes = _transform.TransformFinalBlock(_inputBuffer!, 0, _inputBufferIndex);
                if (useAsync)
                {
                    await _stream.WriteAsync(new ReadOnlyMemory<byte>(finalBytes), cancellationToken).ConfigureAwait(false);
                }
                else
                {
                    _stream.Write(finalBytes, 0, finalBytes.Length);
                }
            }
 
            // If the inner stream is a CryptoStream, then we want to call FlushFinalBlock on it too, otherwise just Flush.
            if (_stream is CryptoStream innerCryptoStream)
            {
                if (!innerCryptoStream.HasFlushedFinalBlock)
                {
                    await innerCryptoStream.FlushFinalBlockAsync(useAsync, cancellationToken).ConfigureAwait(false);
                }
            }
            else
            {
                if (useAsync)
                {
                    await _stream.FlushAsync(cancellationToken).ConfigureAwait(false);
                }
                else
                {
                    _stream.Flush();
                }
            }
 
            // zeroize plain text material before returning
            if (_inputBuffer != null)
                Array.Clear(_inputBuffer);
            if (_outputBuffer != null)
                Array.Clear(_outputBuffer);
        }
 
        public override void Flush()
        {
            if (_canWrite)
            {
                _stream.Flush();
            }
        }
 
        public override Task FlushAsync(CancellationToken cancellationToken)
        {
            // If we have been inherited into a subclass, the following implementation could be incorrect
            // since it does not call through to Flush() which a subclass might have overridden.  To be safe
            // we will only use this implementation in cases where we know it is safe to do so,
            // and delegate to our base class (which will call into Flush) when we are not sure.
            if (GetType() != typeof(CryptoStream))
                return base.FlushAsync(cancellationToken);
 
            return cancellationToken.IsCancellationRequested ?
                Task.FromCanceled(cancellationToken) :
                !_canWrite ? Task.CompletedTask :
                _stream.FlushAsync(cancellationToken);
        }
 
        public override long Seek(long offset, SeekOrigin origin)
        {
            throw new NotSupportedException(SR.NotSupported_UnseekableStream);
        }
 
        public override void SetLength(long value)
        {
            throw new NotSupportedException(SR.NotSupported_UnseekableStream);
        }
 
        public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
        {
            CheckReadArguments(buffer, offset, count);
            return ReadAsyncInternal(buffer.AsMemory(offset, count), cancellationToken).AsTask();
        }
 
        /// <inheritdoc/>
        public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
        {
            if (!CanRead)
                return ValueTask.FromException<int>(new NotSupportedException(SR.NotSupported_UnreadableStream));
 
            return ReadAsyncInternal(buffer, cancellationToken);
        }
 
        private async ValueTask<int> ReadAsyncInternal(Memory<byte> buffer, CancellationToken cancellationToken = default)
        {
            // To avoid a race with a stream's position pointer & generating race
            // conditions with internal buffer indexes in our own streams that
            // don't natively support async IO operations when there are multiple
            // async requests outstanding, we will block the application's main
            // thread if it does a second IO request until the first one completes.
 
            await AsyncActiveSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
            try
            {
                return await ReadAsyncCore(buffer, cancellationToken, useAsync: true).ConfigureAwait(false);
            }
            finally
            {
                _lazyAsyncActiveSemaphore.Release();
            }
        }
 
        public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) =>
            TaskToAsyncResult.Begin(ReadAsync(buffer, offset, count, CancellationToken.None), callback, state);
 
        public override int EndRead(IAsyncResult asyncResult) =>
            TaskToAsyncResult.End<int>(asyncResult);
 
        public override int ReadByte()
        {
            // If we have enough bytes in the buffer such that reading 1 will still leave bytes
            // in the buffer, then take the faster path of simply returning the first byte.
            // (This unfortunately still involves shifting down the bytes in the buffer, as it
            // does in Read.  If/when that's fixed for Read, it should be fixed here, too.)
            if (_outputBufferIndex > 1)
            {
                Debug.Assert(_outputBuffer != null);
                byte b = _outputBuffer[0];
                Buffer.BlockCopy(_outputBuffer, 1, _outputBuffer, 0, _outputBufferIndex - 1);
                _outputBufferIndex -= 1;
                return b;
            }
 
            // Otherwise, fall back to the more robust but expensive path of using the base
            // Stream.ReadByte to call Read.
            return base.ReadByte();
        }
 
        public override void WriteByte(byte value)
        {
            // If there's room in the input buffer such that even with this byte we wouldn't
            // complete a block, simply add the byte to the input buffer.
            if (_inputBufferIndex + 1 < _inputBlockSize)
            {
                _inputBuffer![_inputBufferIndex++] = value;
                return;
            }
 
            // Otherwise, the logic is complicated, so we simply fall back to the base
            // implementation that'll use Write.
            base.WriteByte(value);
        }
 
        public override int Read(byte[] buffer, int offset, int count)
        {
            CheckReadArguments(buffer, offset, count);
            ValueTask<int> completedValueTask = ReadAsyncCore(buffer.AsMemory(offset, count), default(CancellationToken), useAsync: false);
            Debug.Assert(completedValueTask.IsCompleted);
 
            return completedValueTask.GetAwaiter().GetResult();
        }
 
        private void CheckReadArguments(byte[] buffer, int offset, int count)
        {
            ValidateBufferArguments(buffer, offset, count);
            if (!CanRead)
                throw new NotSupportedException(SR.NotSupported_UnreadableStream);
        }
 
        private async ValueTask<int> ReadAsyncCore(Memory<byte> buffer, CancellationToken cancellationToken, bool useAsync)
        {
            while (true)
            {
                // If there are currently any bytes stored in the output buffer, hand back as many as we can.
                if (_outputBufferIndex != 0)
                {
                    int bytesToCopy = Math.Min(_outputBufferIndex, buffer.Length);
                    if (bytesToCopy != 0)
                    {
                        // Copy as many bytes as we can, then shift down the remaining bytes.
                        new ReadOnlySpan<byte>(_outputBuffer, 0, bytesToCopy).CopyTo(buffer.Span);
                        _outputBufferIndex -= bytesToCopy;
                        _outputBuffer.AsSpan(bytesToCopy).CopyTo(_outputBuffer);
                        CryptographicOperations.ZeroMemory(_outputBuffer.AsSpan(_outputBufferIndex, bytesToCopy));
                    }
                    return bytesToCopy;
                }
 
                // If we've already hit the end of the stream, there's nothing more to do.
                Debug.Assert(_outputBufferIndex == 0);
                if (_finalBlockTransformed)
                {
                    Debug.Assert(_inputBufferIndex == 0);
                    return 0;
                }
 
                int bytesRead = 0;
                bool eof = false;
 
                // If the transform supports transforming multiple blocks, try to read as large a chunk as would yield
                // data to fill the output buffer and do the appropriate transform directly into the output buffer.
                int blocksToProcess = buffer.Length / _outputBlockSize;
                if (blocksToProcess > 1 && _transform.CanTransformMultipleBlocks)
                {
                    // Use ArrayPool.Shared instead of CryptoPool because the array is passed out.
                    int numWholeBlocksInBytes = checked(blocksToProcess * _inputBlockSize);
                    byte[] tempInputBuffer = ArrayPool<byte>.Shared.Rent(numWholeBlocksInBytes);
                    try
                    {
                        // Read into our temporary input buffer, leaving enough room at the beginning for any existing data
                        // we have in _inputBuffer.
                        bytesRead = useAsync ?
                            await _stream.ReadAsync(new Memory<byte>(tempInputBuffer, _inputBufferIndex, numWholeBlocksInBytes - _inputBufferIndex), cancellationToken).ConfigureAwait(false) :
                            _stream.Read(tempInputBuffer, _inputBufferIndex, numWholeBlocksInBytes - _inputBufferIndex);
                        eof = bytesRead == 0;
 
                        // If we got enough data to form at least one block, transform as much as we can.
                        int totalInput = _inputBufferIndex + bytesRead;
                        if (totalInput >= _inputBlockSize)
                        {
                            // Copy any held data into tempInputBuffer now that we know we're proceeding to handle
                            // decrypting all the received data.
                            Buffer.BlockCopy(_inputBuffer, 0, tempInputBuffer, 0, _inputBufferIndex);
                            CryptographicOperations.ZeroMemory(new Span<byte>(_inputBuffer, 0, _inputBufferIndex));
                            bytesRead += _inputBufferIndex;
 
                            // Determine how many entire blocks worth of data we read.
                            int numWholeReadBlocks = bytesRead / _inputBlockSize;
                            int numWholeReadBlocksInBytes = numWholeReadBlocks * _inputBlockSize;
 
                            // If there's anything left over, copy that back into _inputBuffer for a later read.
                            _inputBufferIndex = bytesRead - numWholeReadBlocksInBytes;
                            if (_inputBufferIndex != 0)
                            {
                                Buffer.BlockCopy(tempInputBuffer, numWholeReadBlocksInBytes, _inputBuffer, 0, _inputBufferIndex);
                            }
 
                            // Transform the read data into the caller's buffer.
                            int numOutputBytes;
                            if (MemoryMarshal.TryGetArray(buffer, out ArraySegment<byte> bufferArray))
                            {
                                // Because TransformBlock is based on arrays, we can only write directly into the output
                                // buffer if it's backed by an array; otherwise, we need to rent from the pool.
                                numOutputBytes = _transform.TransformBlock(tempInputBuffer, 0, numWholeReadBlocksInBytes, bufferArray.Array!, bufferArray.Offset);
                            }
                            else
                            {
                                // Otherwise, we need to rent a temporary from the pool.
                                byte[] tempOutputBuffer = ArrayPool<byte>.Shared.Rent(numWholeReadBlocks * _outputBlockSize);
                                numOutputBytes = numWholeReadBlocks * _outputBlockSize;
                                try
                                {
                                    numOutputBytes = _transform.TransformBlock(tempInputBuffer, 0, numWholeReadBlocksInBytes, tempOutputBuffer, 0);
                                    tempOutputBuffer.AsSpan(0, numOutputBytes).CopyTo(buffer.Span);
                                }
                                finally
                                {
                                    CryptographicOperations.ZeroMemory(new Span<byte>(tempOutputBuffer, 0, numOutputBytes));
                                    ArrayPool<byte>.Shared.Return(tempOutputBuffer);
                                }
                            }
 
                            // Return anything we've got at this point.
                            if (numOutputBytes != 0)
                            {
                                return numOutputBytes;
                            }
                        }
                        else
                        {
                            // We have less than a block's worth of data.  Copy the new data back into the _inputBuffer
                            // and fall back to using the single block code path.
                            Buffer.BlockCopy(tempInputBuffer, _inputBufferIndex, _inputBuffer, _inputBufferIndex, bytesRead);
                            _inputBufferIndex = totalInput;
                        }
                    }
                    finally
                    {
                        CryptographicOperations.ZeroMemory(new Span<byte>(tempInputBuffer, 0, numWholeBlocksInBytes));
                        ArrayPool<byte>.Shared.Return(tempInputBuffer);
                    }
                }
 
                // Read enough to fill one input block, as anything less won't be able to be transformed to produce output.
                if (!eof)
                {
                    while (_inputBufferIndex < _inputBlockSize)
                    {
                        bytesRead = useAsync ?
                            await _stream.ReadAsync(new Memory<byte>(_inputBuffer, _inputBufferIndex, _inputBlockSize - _inputBufferIndex), cancellationToken).ConfigureAwait(false) :
                            _stream.Read(_inputBuffer, _inputBufferIndex, _inputBlockSize - _inputBufferIndex);
                        if (bytesRead <= 0)
                        {
                            break;
                        }
 
                        _inputBufferIndex += bytesRead;
                    }
                }
 
                // Transform the received data.
                if (bytesRead <= 0)
                {
                    _outputBuffer = _transform.TransformFinalBlock(_inputBuffer, 0, _inputBufferIndex);
                    _outputBufferIndex = _outputBuffer.Length;
                    _finalBlockTransformed = true;
                }
                else
                {
                    _outputBufferIndex = _transform.TransformBlock(_inputBuffer, 0, _inputBufferIndex, _outputBuffer, 0);
                }
 
                // All input data has been processed.
                _inputBufferIndex = 0;
            }
        }
 
        public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
        {
            CheckWriteArguments(buffer, offset, count);
            return WriteAsyncInternal(buffer.AsMemory(offset, count), cancellationToken).AsTask();
        }
 
        /// <inheritdoc/>
        public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)
        {
            if (!CanWrite)
                return ValueTask.FromException(new NotSupportedException(SR.NotSupported_UnwritableStream));
 
            return WriteAsyncInternal(buffer, cancellationToken);
        }
 
        private async ValueTask WriteAsyncInternal(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)
        {
            // To avoid a race with a stream's position pointer & generating race
            // conditions with internal buffer indexes in our own streams that
            // don't natively support async IO operations when there are multiple
            // async requests outstanding, we will block the application's main
            // thread if it does a second IO request until the first one completes.
 
            await AsyncActiveSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
            try
            {
                await WriteAsyncCore(buffer, cancellationToken, useAsync: true).ConfigureAwait(false);
            }
            finally
            {
                _lazyAsyncActiveSemaphore.Release();
            }
        }
 
        public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) =>
            TaskToAsyncResult.Begin(WriteAsync(buffer, offset, count, CancellationToken.None), callback, state);
 
        public override void EndWrite(IAsyncResult asyncResult) =>
            TaskToAsyncResult.End(asyncResult);
 
        public override void Write(byte[] buffer, int offset, int count)
        {
            CheckWriteArguments(buffer, offset, count);
            WriteAsyncCore(buffer.AsMemory(offset, count), default, useAsync: false).AsTask().GetAwaiter().GetResult();
        }
 
        /// <inheritdoc/>
        public override void Write(ReadOnlySpan<byte> buffer)
        {
            // Logically this is doing the same thing as the base Stream, however CryptoStream clears arrays before
            // returning them to the pool, whereas the base Stream does not.
            // Use ArrayPool.Shared instead of CryptoPool because the array is passed out.
            byte[] sharedBuffer = ArrayPool<byte>.Shared.Rent(buffer.Length);
 
            try
            {
                buffer.CopyTo(sharedBuffer);
 
                // We want to keep calling the virtual Write(byte[]...) so that derived CryptoStream types continue
                // to get the array overload called from the span one.
                Write(sharedBuffer, 0, buffer.Length);
            }
            finally
            {
                CryptographicOperations.ZeroMemory(sharedBuffer.AsSpan(0, buffer.Length));
                ArrayPool<byte>.Shared.Return(sharedBuffer);
            }
        }
 
        private void CheckWriteArguments(byte[] buffer, int offset, int count)
        {
            ValidateBufferArguments(buffer, offset, count);
            if (!CanWrite)
                throw new NotSupportedException(SR.NotSupported_UnwritableStream);
        }
 
        private async ValueTask WriteAsyncCore(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken, bool useAsync)
        {
            // write <= count bytes to the output stream, transforming as we go.
            // Basic idea: using bytes in the _InputBuffer first, make whole blocks,
            // transform them, and write them out.  Cache any remaining bytes in the _InputBuffer.
            int bytesToWrite = buffer.Length;
            int currentInputIndex = 0;
            // if we have some bytes in the _InputBuffer, we have to deal with those first,
            // so let's try to make an entire block out of it
            if (_inputBufferIndex > 0)
            {
                Debug.Assert(_inputBuffer != null);
                if (buffer.Length >= _inputBlockSize - _inputBufferIndex)
                {
                    // we have enough to transform at least a block, so fill the input block
                    buffer.Slice(0, _inputBlockSize - _inputBufferIndex).CopyTo(_inputBuffer.AsMemory(_inputBufferIndex));
                    currentInputIndex += (_inputBlockSize - _inputBufferIndex);
                    bytesToWrite -= (_inputBlockSize - _inputBufferIndex);
                    _inputBufferIndex = _inputBlockSize;
                    // Transform the block and write it out
                }
                else
                {
                    // not enough to transform a block, so just copy the bytes into the _InputBuffer
                    // and return
                    buffer.CopyTo(_inputBuffer.AsMemory(_inputBufferIndex));
                    _inputBufferIndex += buffer.Length;
                    return;
                }
            }
 
            Debug.Assert(_outputBufferIndex == 0, "The output index can only ever be non-zero when in read mode.");
            // At this point, either the _InputBuffer is full, empty, or we've already returned.
            // If full, let's process it -- we now know the _OutputBuffer is empty
            int numOutputBytes;
            if (_inputBufferIndex == _inputBlockSize)
            {
                Debug.Assert(_inputBuffer != null && _outputBuffer != null);
                numOutputBytes = _transform.TransformBlock(_inputBuffer, 0, _inputBlockSize, _outputBuffer, 0);
                // write out the bytes we just got
                if (useAsync)
                    await _stream.WriteAsync(new ReadOnlyMemory<byte>(_outputBuffer, 0, numOutputBytes), cancellationToken).ConfigureAwait(false);
                else
                    _stream.Write(_outputBuffer, 0, numOutputBytes);
 
                // reset the _InputBuffer
                _inputBufferIndex = 0;
            }
            while (bytesToWrite > 0)
            {
                if (bytesToWrite >= _inputBlockSize)
                {
                    // We have at least an entire block's worth to transform
                    int numWholeBlocks = bytesToWrite / _inputBlockSize;
 
                    // If the transform will handle multiple blocks at once, do that
                    if (_transform.CanTransformMultipleBlocks && numWholeBlocks > 1)
                    {
                        int numWholeBlocksInBytes = numWholeBlocks * _inputBlockSize;
 
                        // Use ArrayPool.Shared instead of CryptoPool because the array is passed out.
                        byte[]? tempOutputBuffer = ArrayPool<byte>.Shared.Rent(checked(numWholeBlocks * _outputBlockSize));
                        numOutputBytes = 0;
 
                        try
                        {
                            numOutputBytes = TransformBlock(_transform, buffer.Slice(currentInputIndex, numWholeBlocksInBytes), tempOutputBuffer, 0);
 
                            if (useAsync)
                            {
                                await _stream.WriteAsync(new ReadOnlyMemory<byte>(tempOutputBuffer, 0, numOutputBytes), cancellationToken).ConfigureAwait(false);
                            }
                            else
                            {
                                _stream.Write(tempOutputBuffer, 0, numOutputBytes);
                            }
 
                            currentInputIndex += numWholeBlocksInBytes;
                            bytesToWrite -= numWholeBlocksInBytes;
                            CryptographicOperations.ZeroMemory(new Span<byte>(tempOutputBuffer, 0, numOutputBytes));
                            ArrayPool<byte>.Shared.Return(tempOutputBuffer);
                            tempOutputBuffer = null;
                        }
                        catch
                        {
                            CryptographicOperations.ZeroMemory(new Span<byte>(tempOutputBuffer, 0, numOutputBytes));
                            tempOutputBuffer = null;
                            throw;
                        }
                    }
                    else
                    {
                        Debug.Assert(_outputBuffer != null);
                        // do it the slow way
                        numOutputBytes = TransformBlock(_transform, buffer.Slice(currentInputIndex, _inputBlockSize), _outputBuffer, 0);
 
                        if (useAsync)
                            await _stream.WriteAsync(new ReadOnlyMemory<byte>(_outputBuffer, 0, numOutputBytes), cancellationToken).ConfigureAwait(false);
                        else
                            _stream.Write(_outputBuffer, 0, numOutputBytes);
 
                        currentInputIndex += _inputBlockSize;
                        bytesToWrite -= _inputBlockSize;
                    }
                }
                else
                {
                    Debug.Assert(_inputBuffer != null);
                    // In this case, we don't have an entire block's worth left, so store it up in the
                    // input buffer, which by now must be empty.
                    buffer.Slice(currentInputIndex, bytesToWrite).CopyTo(_inputBuffer);
                    _inputBufferIndex += bytesToWrite;
                    return;
                }
            }
            return;
 
            unsafe static int TransformBlock(ICryptoTransform transform, ReadOnlyMemory<byte> inputBuffer, byte[] outputBuffer, int outputOffset)
            {
                if (MemoryMarshal.TryGetArray(inputBuffer, out ArraySegment<byte> segment))
                {
                    // Skip the copy if readonlymemory is actually an array.
                    Debug.Assert(segment.Array is not null);
                    return transform.TransformBlock(segment.Array, segment.Offset, inputBuffer.Length, outputBuffer, outputOffset);
                }
                else
                {
                    // Use ArrayPool.Shared instead of CryptoPool because the array is passed out.
                    byte[]? rentedBuffer = ArrayPool<byte>.Shared.Rent(inputBuffer.Length);
                    int result = default;
 
                    // Pin the rented buffer for security.
                    fixed (byte* _ = &rentedBuffer[0])
                    {
                        try
                        {
                            inputBuffer.CopyTo(rentedBuffer);
                            result = transform.TransformBlock(rentedBuffer, 0, inputBuffer.Length, outputBuffer, outputOffset);
                        }
                        finally
                        {
                            CryptographicOperations.ZeroMemory(rentedBuffer.AsSpan(0, inputBuffer.Length));
                        }
                    }
 
                    ArrayPool<byte>.Shared.Return(rentedBuffer);
                    rentedBuffer = null;
                    return result;
                }
            }
        }
 
        /// <inheritdoc/>
        public override unsafe void CopyTo(Stream destination, int bufferSize)
        {
            CheckCopyToArguments(destination, bufferSize);
 
            // Use ArrayPool<byte>.Shared instead of CryptoPool because the array is passed out.
            byte[]? rentedBuffer = ArrayPool<byte>.Shared.Rent(bufferSize);
            // Pin the array for security.
            fixed (byte* _ = &rentedBuffer[0])
            {
                try
                {
                    int bytesRead;
                    do
                    {
                        bytesRead = Read(rentedBuffer, 0, bufferSize);
                        destination.Write(rentedBuffer, 0, bytesRead);
                    } while (bytesRead > 0);
                }
                finally
                {
                    CryptographicOperations.ZeroMemory(rentedBuffer.AsSpan(0, bufferSize));
                }
            }
            ArrayPool<byte>.Shared.Return(rentedBuffer);
            rentedBuffer = null;
        }
 
        /// <inheritdoc/>
        public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken)
        {
            CheckCopyToArguments(destination, bufferSize);
            return CopyToAsyncInternal(destination, bufferSize, cancellationToken);
        }
 
        private async Task CopyToAsyncInternal(Stream destination, int bufferSize, CancellationToken cancellationToken)
        {
            // Use ArrayPool<byte>.Shared instead of CryptoPool because the array is passed out.
            byte[]? rentedBuffer = ArrayPool<byte>.Shared.Rent(bufferSize);
            // Pin the array for security.
            GCHandle pinHandle = GCHandle.Alloc(rentedBuffer, GCHandleType.Pinned);
            try
            {
                int bytesRead;
                do
                {
                    bytesRead = await ReadAsync(rentedBuffer.AsMemory(0, bufferSize), cancellationToken).ConfigureAwait(false);
                    await destination.WriteAsync(rentedBuffer.AsMemory(0, bytesRead), cancellationToken).ConfigureAwait(false);
                } while (bytesRead > 0);
            }
            finally
            {
                CryptographicOperations.ZeroMemory(rentedBuffer.AsSpan(0, bufferSize));
                pinHandle.Free();
            }
            ArrayPool<byte>.Shared.Return(rentedBuffer);
        }
 
        private void CheckCopyToArguments(Stream destination, int bufferSize)
        {
            ArgumentNullException.ThrowIfNull(destination);
            ObjectDisposedException.ThrowIf(!destination.CanRead && !destination.CanWrite, destination);
 
            if (!destination.CanWrite)
                throw new NotSupportedException(SR.NotSupported_UnwritableStream);
            ArgumentOutOfRangeException.ThrowIfNegativeOrZero(bufferSize);
            if (!CanRead)
                throw new NotSupportedException(SR.NotSupported_UnreadableStream);
        }
 
        public void Clear()
        {
            Close();
        }
 
        protected override void Dispose(bool disposing)
        {
            try
            {
                if (disposing)
                {
                    if (!_finalBlockTransformed)
                    {
                        FlushFinalBlock();
                    }
                    if (!_leaveOpen)
                    {
                        _stream.Dispose();
                    }
                }
            }
            finally
            {
                try
                {
                    // Ensure we don't try to transform the final block again if we get disposed twice
                    // since it's null after this
                    _finalBlockTransformed = true;
                    // we need to clear all the internal buffers
                    if (_inputBuffer != null)
                        Array.Clear(_inputBuffer);
                    if (_outputBuffer != null)
                        Array.Clear(_outputBuffer);
 
                    _inputBuffer = null!;
                    _outputBuffer = null!;
                    _canRead = false;
                    _canWrite = false;
                }
                finally
                {
                    base.Dispose(disposing);
                }
            }
        }
 
        public override ValueTask DisposeAsync()
        {
            return GetType() != typeof(CryptoStream) ?
                base.DisposeAsync() :
                DisposeAsyncCore();
        }
 
        private async ValueTask DisposeAsyncCore()
        {
            // Same logic as in Dispose, but with async counterparts
            try
            {
                if (!_finalBlockTransformed)
                {
                    await FlushFinalBlockAsync(useAsync: true, default).ConfigureAwait(false);
                }
 
                if (!_leaveOpen)
                {
                    await _stream.DisposeAsync().ConfigureAwait(false);
                }
            }
            finally
            {
                // Ensure we don't try to transform the final block again if we get disposed twice
                // since it's null after this
                _finalBlockTransformed = true;
 
                // we need to clear all the internal buffers
                if (_inputBuffer != null)
                {
                    Array.Clear(_inputBuffer);
                }
 
                if (_outputBuffer != null)
                {
                    Array.Clear(_outputBuffer);
                }
 
                _inputBuffer = null!;
                _outputBuffer = null!;
                _canRead = false;
                _canWrite = false;
            }
        }
 
        [MemberNotNull(nameof(_lazyAsyncActiveSemaphore))]
        private SemaphoreSlim AsyncActiveSemaphore
        {
            get
            {
                // Lazily-initialize _lazyAsyncActiveSemaphore.  As we're never accessing the SemaphoreSlim's
                // WaitHandle, we don't need to worry about Disposing it.
                return LazyInitializer.EnsureInitialized(ref _lazyAsyncActiveSemaphore, () => new SemaphoreSlim(1, 1));
            }
        }
    }
}