// This file contains two ICryptoTransforms: ToBase64Transform and FromBase64Transform
// they may be attached to a CryptoStream in either read or write mode
-using System.Text;
+using System.Buffers;
+using System.Buffers.Text;
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
namespace System.Security.Cryptography
{
public int TransformBlock(byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset)
{
- ValidateTransformBlock(inputBuffer, inputOffset, inputCount);
+ // inputCount < InputBlockSize is not allowed
+ ThrowHelper.ValidateTransformBlock(inputBuffer, inputOffset, inputCount, InputBlockSize);
+
+ if (outputBuffer == null)
+ ThrowHelper.ThrowArgumentNull(ThrowHelper.ExceptionArgument.outputBuffer);
// For now, only convert 3 bytes to 4
- byte[] tempBytes = ConvertToBase64(inputBuffer, inputOffset, 3);
+ Span<byte> input = inputBuffer.AsSpan(inputOffset, InputBlockSize);
+ Span<byte> output = outputBuffer.AsSpan(outputOffset, OutputBlockSize);
+
+ OperationStatus status = Base64.EncodeToUtf8(input, output, out int consumed, out int written, isFinalBlock: false);
+
+ if (written != OutputBlockSize)
+ {
+ ThrowHelper.ThrowCryptographicException();
+ }
+
+ Debug.Assert(status == OperationStatus.NeedMoreData);
+ Debug.Assert(consumed == InputBlockSize);
- Buffer.BlockCopy(tempBytes, 0, outputBuffer, outputOffset, tempBytes.Length);
- return tempBytes.Length;
+ return written;
}
public byte[] TransformFinalBlock(byte[] inputBuffer, int inputOffset, int inputCount)
{
- ValidateTransformBlock(inputBuffer, inputOffset, inputCount);
+ // inputCount <= InputBlockSize is allowed
+ ThrowHelper.ValidateTransformBlock(inputBuffer, inputOffset, inputCount);
// Convert.ToBase64CharArray already does padding, so all we have to check is that
// the inputCount wasn't 0
{
return Array.Empty<byte>();
}
+ else if (inputCount > InputBlockSize)
+ {
+ ThrowHelper.ThrowArgumentOutOfRange(ThrowHelper.ExceptionArgument.inputCount);
+ }
// Again, for now only a block at a time
- return ConvertToBase64(inputBuffer, inputOffset, inputCount);
- }
+ Span<byte> input = inputBuffer.AsSpan(inputOffset, inputCount);
+ byte[] output = new byte[OutputBlockSize];
- private byte[] ConvertToBase64(byte[] inputBuffer, int inputOffset, int inputCount)
- {
- char[] temp = new char[4];
- Convert.ToBase64CharArray(inputBuffer, inputOffset, inputCount, temp, 0);
- byte[] tempBytes = Encoding.ASCII.GetBytes(temp);
- if (tempBytes.Length != 4)
- throw new CryptographicException(SR.Cryptography_SSE_InvalidDataSize);
+ OperationStatus status = Base64.EncodeToUtf8(input, output, out int consumed, out int written, isFinalBlock: true);
- return tempBytes;
- }
+ if (written != OutputBlockSize)
+ {
+ ThrowHelper.ThrowCryptographicException();
+ }
- private static void ValidateTransformBlock(byte[] inputBuffer, int inputOffset, int inputCount)
- {
- if (inputBuffer == null) throw new ArgumentNullException(nameof(inputBuffer));
- if (inputOffset < 0) throw new ArgumentOutOfRangeException(nameof(inputOffset), SR.ArgumentOutOfRange_NeedNonNegNum);
- if (inputCount < 0 || (inputCount > inputBuffer.Length)) throw new ArgumentException(SR.Argument_InvalidValue);
- if ((inputBuffer.Length - inputCount) < inputOffset) throw new ArgumentException(SR.Argument_InvalidOffLen);
+ Debug.Assert(status == OperationStatus.Done);
+ Debug.Assert(consumed == inputCount);
+
+ return output;
}
// Must implement IDisposable, but in this case there's nothing to do.
{
private byte[] _inputBuffer = new byte[4];
private int _inputIndex;
- private FromBase64TransformMode _whitespaces;
+ private readonly FromBase64TransformMode _whitespaces;
public FromBase64Transform() : this(FromBase64TransformMode.IgnoreWhiteSpaces) { }
public FromBase64Transform(FromBase64TransformMode whitespaces)
{
_whitespaces = whitespaces;
- _inputIndex = 0;
}
// Converting from Base64 generates 3 bytes output from each 4 bytes input block
+ private const int Base64InputBlockSize = 4;
+ // A buffer with size 32 is stack allocated, to cover common cases and benefit from JIT's optimizations.
+ private const int StackAllocSize = 32;
public int InputBlockSize => 1;
public int OutputBlockSize => 3;
public bool CanTransformMultipleBlocks => false;
public int TransformBlock(byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset)
{
- ValidateTransformBlock(inputBuffer, inputOffset, inputCount);
- if (_inputBuffer == null) throw new ObjectDisposedException(null, SR.ObjectDisposed_Generic);
+ // inputCount != InputBlockSize is allowed
+ ThrowHelper.ValidateTransformBlock(inputBuffer, inputOffset, inputCount);
+
+ if (_inputBuffer == null)
+ ThrowHelper.ThrowObjectDisposed();
+
+ if (outputBuffer == null)
+ ThrowHelper.ThrowArgumentNull(ThrowHelper.ExceptionArgument.outputBuffer);
- int effectiveCount;
- byte[] temp = GetTempBuffer(inputBuffer, inputOffset, inputCount, out effectiveCount);
+ // The common case is inputCount = InputBlockSize
+ byte[] tmpBufferArray = null;
+ Span<byte> tmpBuffer = stackalloc byte[StackAllocSize];
+ if (inputCount > StackAllocSize)
+ {
+ tmpBuffer = tmpBufferArray = CryptoPool.Rent(inputCount);
+ }
- if (effectiveCount + _inputIndex < 4)
+ tmpBuffer = GetTempBuffer(inputBuffer.AsSpan(inputOffset, inputCount), tmpBuffer);
+ int bytesToTransform = _inputIndex + tmpBuffer.Length;
+
+ // Too little data to decode: save data to _inputBuffer, so it can be transformed later
+ if (bytesToTransform < Base64InputBlockSize)
{
- Buffer.BlockCopy(temp, 0, _inputBuffer, _inputIndex, effectiveCount);
- _inputIndex += effectiveCount;
+ tmpBuffer.CopyTo(_inputBuffer.AsSpan(_inputIndex));
+
+ _inputIndex = bytesToTransform;
+
+ ReturnToCryptoPool(tmpBufferArray, tmpBuffer.Length);
+
return 0;
}
- byte[] result = ConvertFromBase64(temp, effectiveCount);
+ ConvertFromBase64(tmpBuffer, outputBuffer.AsSpan(outputOffset), out _, out int written);
- Buffer.BlockCopy(result, 0, outputBuffer, outputOffset, result.Length);
+ ReturnToCryptoPool(tmpBufferArray, tmpBuffer.Length);
- return result.Length;
+ return written;
}
public byte[] TransformFinalBlock(byte[] inputBuffer, int inputOffset, int inputCount)
{
- ValidateTransformBlock(inputBuffer, inputOffset, inputCount);
- if (_inputBuffer == null) throw new ObjectDisposedException(null, SR.ObjectDisposed_Generic);
+ // inputCount != InputBlockSize is allowed
+ ThrowHelper.ValidateTransformBlock(inputBuffer, inputOffset, inputCount);
- int effectiveCount;
- byte[] temp = GetTempBuffer(inputBuffer, inputOffset, inputCount, out effectiveCount);
+ if (_inputBuffer == null)
+ {
+ ThrowHelper.ThrowObjectDisposed();
+ }
+
+ if (inputCount == 0)
+ {
+ return Array.Empty<byte>();
+ }
- if (effectiveCount + _inputIndex < 4)
+ // The common case is inputCount <= Base64InputBlockSize
+ byte[] tmpBufferArray = null;
+ Span<byte> tmpBuffer = stackalloc byte[StackAllocSize];
+ if (inputCount > StackAllocSize)
{
+ tmpBuffer = tmpBufferArray = CryptoPool.Rent(inputCount);
+ }
+
+ tmpBuffer = GetTempBuffer(inputBuffer.AsSpan(inputOffset, inputCount), tmpBuffer);
+ int bytesToTransform = _inputIndex + tmpBuffer.Length;
+
+ // Too little data to decode
+ if (bytesToTransform < Base64InputBlockSize)
+ {
+ // reinitialize the transform
Reset();
+
+ ReturnToCryptoPool(tmpBufferArray, tmpBuffer.Length);
+
return Array.Empty<byte>();
}
- byte[] result = ConvertFromBase64(temp, effectiveCount);
+ int outputSize = GetOutputSize(bytesToTransform, tmpBuffer);
+ byte[] output = new byte[outputSize];
+
+ ConvertFromBase64(tmpBuffer, output, out int consumed, out int written);
+ Debug.Assert(written == outputSize);
+
+ ReturnToCryptoPool(tmpBufferArray, tmpBuffer.Length);
// reinitialize the transform
Reset();
- return result;
+ return output;
}
- private byte[] GetTempBuffer(byte[] inputBuffer, int inputOffset, int inputCount, out int effectiveCount)
+ private Span<byte> GetTempBuffer(Span<byte> inputBuffer, Span<byte> tmpBuffer)
{
- byte[] temp;
-
- if (_whitespaces == FromBase64TransformMode.IgnoreWhiteSpaces)
+ if (_whitespaces == FromBase64TransformMode.DoNotIgnoreWhiteSpaces)
{
- temp = DiscardWhiteSpaces(inputBuffer, inputOffset, inputCount);
- effectiveCount = temp.Length;
+ return inputBuffer;
}
- else
+
+ return DiscardWhiteSpaces(inputBuffer, tmpBuffer);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static Span<byte> DiscardWhiteSpaces(Span<byte> inputBuffer, Span<byte> tmpBuffer)
+ {
+ int count = 0;
+
+ for (int i = 0; i < inputBuffer.Length; i++)
{
- temp = new byte[inputCount];
- Buffer.BlockCopy(inputBuffer, inputOffset, temp, 0, inputCount);
- effectiveCount = inputCount;
+ if (!IsWhitespace(inputBuffer[i]))
+ {
+ tmpBuffer[count++] = inputBuffer[i];
+ }
}
- return temp;
+ return tmpBuffer.Slice(0, count);
}
- private byte[] ConvertFromBase64(byte[] temp, int effectiveCount)
+ private static bool IsWhitespace(byte value)
{
- // Get the number of 4 bytes blocks to transform
- int numBlocks = (effectiveCount + _inputIndex) / 4;
-
- byte[] transformBuffer = new byte[_inputIndex + effectiveCount];
- Buffer.BlockCopy(_inputBuffer, 0, transformBuffer, 0, _inputIndex);
- Buffer.BlockCopy(temp, 0, transformBuffer, _inputIndex, effectiveCount);
+ // We assume ASCII encoded data. If there is any non-ASCII char, it is invalid
+ // Base64 and will be caught during decoding.
- _inputIndex = (effectiveCount + _inputIndex) % 4;
- Buffer.BlockCopy(temp, effectiveCount - _inputIndex, _inputBuffer, 0, _inputIndex);
+ // SPACE 32
+ // TAB 9
+ // LF 10
+ // VTAB 11
+ // FORM FEED 12
+ // CR 13
- char[] tempChar = Encoding.ASCII.GetChars(transformBuffer, 0, 4 * numBlocks);
- byte[] tempBytes = Convert.FromBase64CharArray(tempChar, 0, 4 * numBlocks);
- return tempBytes;
+ return value == 32 || ((uint)value - 9 <= (13 - 9));
}
- private byte[] DiscardWhiteSpaces(byte[] inputBuffer, int inputOffset, int inputCount)
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private int GetOutputSize(int bytesToTransform, Span<byte> tmpBuffer)
{
- int i, iCount = 0;
- for (i = 0; i < inputCount; i++)
+ int outputSize = Base64.GetMaxDecodedFromUtf8Length(bytesToTransform);
+
+ const byte padding = (byte)'=';
+ int len = tmpBuffer.Length;
+
+ // In Base64 there are maximum 2 padding chars
+
+ if (tmpBuffer[len - 2] == padding)
{
- if (char.IsWhiteSpace((char)inputBuffer[inputOffset + i])) iCount++;
+ outputSize--;
}
- // If there's nothing to do, leave early
- if (iCount == 0 && inputOffset == 0 &&
- inputCount == inputBuffer.Length)
+ if (tmpBuffer[len - 1] == padding)
{
- return inputBuffer;
+ outputSize--;
}
- byte[] rgbOut = new byte[inputCount - iCount];
- iCount = 0;
- for (i = 0; i < inputCount; i++)
+ return outputSize;
+ }
+
+ private void ConvertFromBase64(Span<byte> tmpBuffer, Span<byte> outputBuffer, out int consumed, out int written)
+ {
+ int bytesToTransform = _inputIndex + tmpBuffer.Length;
+ Debug.Assert(bytesToTransform >= 4);
+
+ byte[] transformBufferArray = null;
+ Span<byte> transformBuffer = stackalloc byte[StackAllocSize];
+ if (bytesToTransform > StackAllocSize)
{
- if (!char.IsWhiteSpace((char)inputBuffer[inputOffset + i]))
- {
- rgbOut[iCount++] = inputBuffer[inputOffset + i];
- }
+ transformBuffer = transformBufferArray = CryptoPool.Rent(bytesToTransform);
+ }
+
+ // Copy _inputBuffer to transformBuffer and append tmpBuffer
+ Debug.Assert(_inputIndex < _inputBuffer.Length);
+ _inputBuffer.AsSpan(0, _inputIndex).CopyTo(transformBuffer);
+ tmpBuffer.CopyTo(transformBuffer.Slice(_inputIndex));
+
+ // Save data that won't be transformed to _inputBuffer, so it can be transformed later
+ _inputIndex = bytesToTransform & 3; // bit hack for % 4
+ bytesToTransform -= _inputIndex; // only transform up to the next multiple of 4
+ Debug.Assert(_inputIndex < _inputBuffer.Length);
+ tmpBuffer.Slice(tmpBuffer.Length - _inputIndex).CopyTo(_inputBuffer);
+
+ transformBuffer = transformBuffer.Slice(0, bytesToTransform);
+ OperationStatus status = Base64.DecodeFromUtf8(transformBuffer, outputBuffer, out consumed, out written);
+
+ if (status == OperationStatus.Done)
+ {
+ Debug.Assert(consumed == bytesToTransform);
+ }
+ else
+ {
+ Debug.Assert(status == OperationStatus.InvalidData);
+ ThrowHelper.ThrowBase64FormatException();
}
- return rgbOut;
+ ReturnToCryptoPool(transformBufferArray, transformBuffer.Length);
}
- private static void ValidateTransformBlock(byte[] inputBuffer, int inputOffset, int inputCount)
+ private void ReturnToCryptoPool(byte[] array, int clearSize)
{
- if (inputBuffer == null) throw new ArgumentNullException(nameof(inputBuffer));
- if (inputOffset < 0) throw new ArgumentOutOfRangeException(nameof(inputOffset), SR.ArgumentOutOfRange_NeedNonNegNum);
- if (inputCount < 0 || (inputCount > inputBuffer.Length)) throw new ArgumentException(SR.Argument_InvalidValue);
- if ((inputBuffer.Length - inputCount) < inputOffset) throw new ArgumentException(SR.Argument_InvalidOffLen);
+ if (array != null)
+ {
+ CryptoPool.Return(array, clearSize);
+ }
}
- // must implement IDisposable, which in this case means clearing the input buffer
-
- public void Dispose()
+ public void Clear()
{
- Dispose(true);
- GC.SuppressFinalize(this);
+ Dispose();
}
// Reset the state of the transform so it can be used again
_inputIndex = 0;
}
- public void Clear()
+ // must implement IDisposable, which in this case means clearing the input buffer
+
+ public void Dispose()
{
- Dispose();
+ Dispose(true);
+ GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
if (disposing)
{
if (_inputBuffer != null)
- Array.Clear(_inputBuffer, 0, _inputBuffer.Length);
- _inputBuffer = null;
- _inputIndex = 0;
+ {
+ CryptographicOperations.ZeroMemory(_inputBuffer);
+ _inputBuffer = null;
+ }
+
+ Reset();
}
}
Dispose(false);
}
}
+
+ internal class ThrowHelper
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void ValidateTransformBlock(byte[] inputBuffer, int inputOffset, int inputCount)
+ {
+ if (inputBuffer == null)
+ ThrowArgumentNull(ExceptionArgument.inputBuffer);
+
+ if ((uint)inputCount > inputBuffer.Length)
+ ThrowArgumentOutOfRange(ExceptionArgument.inputCount);
+
+ if (inputOffset < 0)
+ ThrowArgumentOutOfRange(ExceptionArgument.inputOffset);
+
+ if ((inputBuffer.Length - inputCount) < inputOffset)
+ ThrowInvalidOffLen();
+ }
+
+ public static void ValidateTransformBlock(byte[] inputBuffer, int inputOffset, int inputCount, int inputBlockSize)
+ {
+ ValidateTransformBlock(inputBuffer, inputOffset, inputCount);
+
+ if (inputCount < inputBlockSize)
+ ThrowArgumentOutOfRange(ExceptionArgument.inputCount);
+ }
+
+ public static void ThrowArgumentNull(ExceptionArgument argument) => throw new ArgumentNullException(argument.ToString());
+ public static void ThrowArgumentOutOfRange(ExceptionArgument argument) => throw new ArgumentOutOfRangeException(argument.ToString(), SR.ArgumentOutOfRange_NeedNonNegNum);
+ public static void ThrowInvalidOffLen() => throw new ArgumentException(SR.Argument_InvalidOffLen);
+ public static void ThrowObjectDisposed() => throw new ObjectDisposedException(null, SR.ObjectDisposed_Generic);
+ public static void ThrowCryptographicException() => throw new CryptographicException(SR.Cryptography_SSE_InvalidDataSize);
+ public static void ThrowBase64FormatException() => throw new FormatException();
+
+ public enum ExceptionArgument
+ {
+ inputBuffer,
+ outputBuffer,
+ inputOffset,
+ inputCount
+ }
+ }
}
using System.Collections.Generic;
using System.IO;
-using Test.Cryptography;
using Xunit;
namespace System.Security.Cryptography.Encoding.Tests
public static IEnumerable<object[]> TestData_Ascii_Whitespace()
{
+ yield return new object[] { "fo", "\tZ\tm8=\r" };
yield return new object[] { "fo", "\tZ\tm8=\n" };
+ yield return new object[] { "fo", "\tZ\tm8=\r\n" };
yield return new object[] { "foo", " Z m 9 v" };
}
yield return new object[] { "Zm9v////", 0, 4, "foo" };
yield return new object[] { "////Zm9v", 4, 4, "foo" };
yield return new object[] { "////Zm9v////", 4, 4, "foo" };
+ yield return new object[] { "Zm9vYmFyYm", 0, 10, "foobar" };
}
[Fact]
InvalidInput_Base64Transform(transform);
// These exceptions only thrown in ToBase
- AssertExtensions.Throws<ArgumentOutOfRangeException>("offsetOut", () => transform.TransformFinalBlock(data_5bytes, 0, 5));
+ AssertExtensions.Throws<ArgumentOutOfRangeException>("inputCount", () => transform.TransformFinalBlock(data_5bytes, 0, 5));
}
}
AssertExtensions.Throws<ArgumentNullException>("inputBuffer", () => transform.TransformBlock(null, 0, 0, null, 0));
AssertExtensions.Throws<ArgumentOutOfRangeException>("inputOffset", () => transform.TransformBlock(Array.Empty<byte>(), -1, 0, null, 0));
- AssertExtensions.Throws<ArgumentNullException>("dst", () => transform.TransformBlock(data_4bytes, 0, 4, null, 0));
- AssertExtensions.Throws<ArgumentException>(null, () => transform.TransformBlock(Array.Empty<byte>(), 0, 1, null, 0));
+ AssertExtensions.Throws<ArgumentNullException>("outputBuffer", () => transform.TransformBlock(data_4bytes, 0, 4, null, 0));
+ AssertExtensions.Throws<ArgumentOutOfRangeException>("inputCount", () => transform.TransformBlock(Array.Empty<byte>(), 0, 1, null, 0));
AssertExtensions.Throws<ArgumentException>(null, () => transform.TransformBlock(Array.Empty<byte>(), 1, 0, null, 0));
AssertExtensions.Throws<ArgumentNullException>("inputBuffer", () => transform.TransformFinalBlock(null, 0, 0));
Assert.True(inputBytes.Length > 4);
// Test passing blocks > 4 characters to TransformFinalBlock (not supported)
- AssertExtensions.Throws<ArgumentOutOfRangeException>("offsetOut", () => transform.TransformFinalBlock(inputBytes, 0, inputBytes.Length));
+ AssertExtensions.Throws<ArgumentOutOfRangeException>("inputCount", () => transform.TransformFinalBlock(inputBytes, 0, inputBytes.Length));
}
}
[Theory, MemberData(nameof(TestData_LongBlock_Ascii))]
- public static void ValidateFromBase64TransformFinalBlock(string expected, string encoding)
+ public static void ValidateFromBase64TransformFinalBlock(string expected, string encoded)
{
using (var transform = new FromBase64Transform())
{
- byte[] inputBytes = Text.Encoding.ASCII.GetBytes(encoding);
+ byte[] inputBytes = Text.Encoding.ASCII.GetBytes(encoded);
Assert.True(inputBytes.Length > 4);
// Test passing blocks > 4 characters to TransformFinalBlock (supported)
}
}
+ [Theory, MemberData(nameof(TestData_LongBlock_Ascii))]
+ public static void ValidateFromBase64TransformBlock(string expected, string encoded)
+ {
+ using (var transform = new FromBase64Transform())
+ {
+ byte[] inputBytes = Text.Encoding.ASCII.GetBytes(encoded);
+ Assert.True(inputBytes.Length > 4);
+
+ byte[] outputBytes = new byte[100];
+ int bytesWritten = transform.TransformBlock(inputBytes, 0, inputBytes.Length, outputBytes, 0);
+ string outputText = Text.Encoding.ASCII.GetString(outputBytes, 0, bytesWritten);
+
+ Assert.Equal(expected, outputText);
+ }
+ }
+
[Theory, MemberData(nameof(TestData_Ascii_NoPadding))]
public static void ValidateFromBase64_NoPadding(string data)
{
byte[] outputBytes = new byte[100];
// Verify default of FromBase64TransformMode.IgnoreWhiteSpaces
- using (var base64Transform = new FromBase64Transform())
+ using (var base64Transform = new FromBase64Transform())
using (var ms = new MemoryStream(inputBytes))
using (var cs = new CryptoStream(ms, base64Transform, CryptoStreamMode.Read))
{