1
0
mirror of https://github.com/bitwarden/server.git synced 2024-11-21 12:05:42 +01:00

[PS-1471] Create Allocation Free EncryptedStringAttribute validation (#2273)

* Add new logic for validating encrypted strings

* Add benchmarks

* Formatting & Comments

* Move Debug assertion to just be a test

* Address PR feedback pt.1

* Address more PR feedback

* Formatting

* merge branch 'master' into 'encrypted-string-perf'

* Revert "merge branch 'master' into 'encrypted-string-perf'"

This reverts commit a20e127c9c.
This commit is contained in:
Justin Baur 2022-10-20 16:10:02 -04:00 committed by GitHub
parent 63ae7c8b66
commit a349f28840
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 3074 additions and 148 deletions

View File

@ -98,6 +98,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Api.IntegrationTest", "test
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Scim.IntegrationTest", "bitwarden_license\test\Scim.IntegrationTest\Scim.IntegrationTest.csproj", "{FE998849-5FC8-41A2-B7C9-9227901471A0}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "perf", "perf", "{EC2D422A-6060-48E2-AAD2-37220D759F03}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MicroBenchmarks", "perf\MicroBenchmarks\MicroBenchmarks.csproj", "{9C8F8255-5F74-4085-AB9C-9075CF6DDC61}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Scim.Test", "bitwarden_license\test\Scim.Test\Scim.Test.csproj", "{B1595DA3-4C60-41AA-8BF0-499A5F75A885}"
EndProject
Global
@ -232,14 +236,18 @@ Global
{7EFB1124-F40A-40EB-9EDA-94FD540AA8FD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7EFB1124-F40A-40EB-9EDA-94FD540AA8FD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7EFB1124-F40A-40EB-9EDA-94FD540AA8FD}.Release|Any CPU.Build.0 = Release|Any CPU
{CBE96C6D-A4D6-46E1-94C5-42D6CAD8531C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CBE96C6D-A4D6-46E1-94C5-42D6CAD8531C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CBE96C6D-A4D6-46E1-94C5-42D6CAD8531C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CBE96C6D-A4D6-46E1-94C5-42D6CAD8531C}.Release|Any CPU.Build.0 = Release|Any CPU
{CBE96C6D-A4D6-46E1-94C5-42D6CAD8531C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CBE96C6D-A4D6-46E1-94C5-42D6CAD8531C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CBE96C6D-A4D6-46E1-94C5-42D6CAD8531C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CBE96C6D-A4D6-46E1-94C5-42D6CAD8531C}.Release|Any CPU.Build.0 = Release|Any CPU
{FE998849-5FC8-41A2-B7C9-9227901471A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FE998849-5FC8-41A2-B7C9-9227901471A0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FE998849-5FC8-41A2-B7C9-9227901471A0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FE998849-5FC8-41A2-B7C9-9227901471A0}.Release|Any CPU.Build.0 = Release|Any CPU
{9C8F8255-5F74-4085-AB9C-9075CF6DDC61}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9C8F8255-5F74-4085-AB9C-9075CF6DDC61}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9C8F8255-5F74-4085-AB9C-9075CF6DDC61}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9C8F8255-5F74-4085-AB9C-9075CF6DDC61}.Release|Any CPU.Build.0 = Release|Any CPU
{B1595DA3-4C60-41AA-8BF0-499A5F75A885}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B1595DA3-4C60-41AA-8BF0-499A5F75A885}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B1595DA3-4C60-41AA-8BF0-499A5F75A885}.Release|Any CPU.ActiveCfg = Release|Any CPU
@ -280,11 +288,12 @@ Global
{0923DE59-5FB1-44F2-9302-A09D2236B470} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
{BC3B3F8C-621A-4CB8-9563-6EC0A2C8C747} = {4FDB6543-F68B-4202-9EA6-7FEA984D2D0A}
{7EFB1124-F40A-40EB-9EDA-94FD540AA8FD} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
{CBE96C6D-A4D6-46E1-94C5-42D6CAD8531C} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
{FE998849-5FC8-41A2-B7C9-9227901471A0} = {287CFF34-BBDB-4BC4-AF88-1E19A5A4679B}
{CBE96C6D-A4D6-46E1-94C5-42D6CAD8531C} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
{FE998849-5FC8-41A2-B7C9-9227901471A0} = {287CFF34-BBDB-4BC4-AF88-1E19A5A4679B}
{9C8F8255-5F74-4085-AB9C-9075CF6DDC61} = {EC2D422A-6060-48E2-AAD2-37220D759F03}
{B1595DA3-4C60-41AA-8BF0-499A5F75A885} = {287CFF34-BBDB-4BC4-AF88-1E19A5A4679B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F}
EndGlobalSection
EndGlobal
EndGlobal

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.13.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Core\Core.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,4 @@
using System.Reflection;
using BenchmarkDotNet.Running;
BenchmarkRunner.Run(Assembly.GetEntryAssembly());

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,7 @@
namespace Bit.Core.Enums;
// If the backing type here changes to a different type you will likely also need to change the value used in
// EncryptedStringAttribute
public enum EncryptionType : byte
{
AesCbc256_B64 = 0,

View File

@ -0,0 +1,176 @@
using System.Buffers;
using System.ComponentModel.DataAnnotations;
using Bit.Core.Enums;
#nullable enable
namespace Bit.Core.Utilities;
/// <summary>
/// Validates a string that is in encrypted form: "head.b64iv=|b64ct=|b64mac="
/// </summary>
public class EncryptedStringAttribute : ValidationAttribute
{
internal static readonly Dictionary<EncryptionType, int> _encryptionTypeToRequiredPiecesMap = new()
{
[EncryptionType.AesCbc256_B64] = 2, // iv|ct
[EncryptionType.AesCbc128_HmacSha256_B64] = 3, // iv|ct|mac
[EncryptionType.AesCbc256_HmacSha256_B64] = 3, // iv|ct|mac
[EncryptionType.Rsa2048_OaepSha256_B64] = 1, // rsaCt
[EncryptionType.Rsa2048_OaepSha1_B64] = 1, // rsaCt
[EncryptionType.Rsa2048_OaepSha256_HmacSha256_B64] = 2, // rsaCt|mac
[EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64] = 2, // rsaCt|mac
};
public EncryptedStringAttribute()
: base("{0} is not a valid encrypted string.")
{ }
public override bool IsValid(object? value)
{
try
{
if (value is null)
{
return true;
}
if (value is string stringValue)
{
// Fast path
return IsValidCore(stringValue);
}
// This attribute should only be placed on string properties, fail
return false;
}
catch
{
return false;
}
}
internal static bool IsValidCore(ReadOnlySpan<char> value)
{
if (!value.TrySplitBy('.', out var headerChunk, out var rest))
{
// We couldn't find a header part, this is the slow path, because we have to do two loops over
// the data.
// If it has 3 encryption parts that means it is AesCbc128_HmacSha256_B64
// else we assume it is AesCbc256_B64
var splitChars = rest.Count('|');
if (splitChars == 2)
{
return ValidatePieces(rest, _encryptionTypeToRequiredPiecesMap[EncryptionType.AesCbc128_HmacSha256_B64]);
}
else
{
return ValidatePieces(rest, _encryptionTypeToRequiredPiecesMap[EncryptionType.AesCbc256_B64]);
}
}
EncryptionType encryptionType;
// Using byte here because that is the backing type for EncryptionType
if (!byte.TryParse(headerChunk, out var encryptionTypeNumber))
{
// We can't read the header chunk as a number, this is the slow path
if (!Enum.TryParse(headerChunk, out encryptionType))
{
// Can't even get the enum from a non-number header, fail
return false;
}
// Since this value came from Enum.TryParse we know it is an enumerated object and we can therefore
// just access the dictionary
return ValidatePieces(rest, _encryptionTypeToRequiredPiecesMap[encryptionType]);
}
// Simply cast the number to the enum, this could be a value that doesn't actually have a backing enum
// entry but that is alright we will use it to look in the dictionary and non-valid
// numbers will be filtered out there.
encryptionType = (EncryptionType)encryptionTypeNumber;
if (!_encryptionTypeToRequiredPiecesMap.TryGetValue(encryptionType, out var encryptionPieces))
{
// Could not find a configuration map for the given header piece. This is an invalid string
return false;
}
return ValidatePieces(rest, encryptionPieces);
}
private static bool ValidatePieces(ReadOnlySpan<char> encryptionPart, int requiredPieces)
{
var rest = encryptionPart;
while (requiredPieces != 0)
{
if (requiredPieces == 1)
{
// Only one more part is needed so don't split and check the chunk
if (!IsValidBase64(rest))
{
return false;
}
// Make sure there isn't another split character possibly denoting another chunk
return rest.IndexOf('|') == -1;
}
else
{
// More than one part is required so split it out
if (!rest.TrySplitBy('|', out var chunk, out rest))
{
return false;
}
// Is the required chunk valid base 64?
if (!IsValidBase64(chunk))
{
return false;
}
}
// This current piece is valid so we can count down
requiredPieces--;
}
// No more parts are required, so check there are no extra parts
return rest.IndexOf('|') == -1;
}
private static bool IsValidBase64(ReadOnlySpan<char> input)
{
const int StackLimit = 256;
byte[]? pooledChunks = null;
var upperLimitLength = CalculateBase64ByteLengthUpperLimit(input.Length);
// Ref: https://vcsjones.dev/stackalloc/
var byteBuffer = upperLimitLength > StackLimit
? (pooledChunks = ArrayPool<byte>.Shared.Rent(upperLimitLength))
: stackalloc byte[StackLimit];
try
{
var successful = Convert.TryFromBase64Chars(input, byteBuffer, out var bytesWritten);
return successful && bytesWritten > 0;
}
finally
{
// Check if we rented the pool and if so, return it.
if (pooledChunks != null)
{
ArrayPool<byte>.Shared.Return(pooledChunks, true);
}
}
}
internal static int CalculateBase64ByteLengthUpperLimit(int charLength)
{
return 3 * (charLength / 4);
}
}

View File

@ -1,137 +0,0 @@
using System.ComponentModel.DataAnnotations;
namespace Bit.Core.Utilities;
/// <summary>
/// Validates a string that is in encrypted form: "head.b64iv=|b64ct=|b64mac="
/// </summary>
public class EncryptedStringAttribute : ValidationAttribute
{
public EncryptedStringAttribute()
: base("{0} is not a valid encrypted string.")
{ }
public override bool IsValid(object value)
{
if (value == null)
{
return true;
}
try
{
var encString = value?.ToString();
if (string.IsNullOrWhiteSpace(encString))
{
return false;
}
var headerPieces = encString.Split('.');
string[] encStringPieces = null;
var encType = Enums.EncryptionType.AesCbc256_B64;
if (headerPieces.Length == 1)
{
encStringPieces = headerPieces[0].Split('|');
if (encStringPieces.Length == 3)
{
encType = Enums.EncryptionType.AesCbc128_HmacSha256_B64;
}
else
{
encType = Enums.EncryptionType.AesCbc256_B64;
}
}
else if (headerPieces.Length == 2)
{
encStringPieces = headerPieces[1].Split('|');
if (!Enum.TryParse(headerPieces[0], out encType))
{
return false;
}
}
switch (encType)
{
case Enums.EncryptionType.AesCbc256_B64:
case Enums.EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64:
case Enums.EncryptionType.Rsa2048_OaepSha256_HmacSha256_B64:
if (encStringPieces.Length != 2)
{
return false;
}
break;
case Enums.EncryptionType.AesCbc128_HmacSha256_B64:
case Enums.EncryptionType.AesCbc256_HmacSha256_B64:
if (encStringPieces.Length != 3)
{
return false;
}
break;
case Enums.EncryptionType.Rsa2048_OaepSha256_B64:
case Enums.EncryptionType.Rsa2048_OaepSha1_B64:
if (encStringPieces.Length != 1)
{
return false;
}
break;
default:
return false;
}
switch (encType)
{
case Enums.EncryptionType.AesCbc256_B64:
case Enums.EncryptionType.AesCbc128_HmacSha256_B64:
case Enums.EncryptionType.AesCbc256_HmacSha256_B64:
var iv = Convert.FromBase64String(encStringPieces[0]);
var ct = Convert.FromBase64String(encStringPieces[1]);
if (iv.Length < 1 || ct.Length < 1)
{
return false;
}
if (encType == Enums.EncryptionType.AesCbc128_HmacSha256_B64 ||
encType == Enums.EncryptionType.AesCbc256_HmacSha256_B64)
{
var mac = Convert.FromBase64String(encStringPieces[2]);
if (mac.Length < 1)
{
return false;
}
}
break;
case Enums.EncryptionType.Rsa2048_OaepSha256_B64:
case Enums.EncryptionType.Rsa2048_OaepSha1_B64:
case Enums.EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64:
case Enums.EncryptionType.Rsa2048_OaepSha256_HmacSha256_B64:
var rsaCt = Convert.FromBase64String(encStringPieces[0]);
if (rsaCt.Length < 1)
{
return false;
}
if (encType == Enums.EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64 ||
encType == Enums.EncryptionType.Rsa2048_OaepSha256_HmacSha256_B64)
{
var mac = Convert.FromBase64String(encStringPieces[1]);
if (mac.Length < 1)
{
return false;
}
}
break;
default:
return false;
}
}
catch
{
return false;
}
return true;
}
}

View File

@ -0,0 +1,38 @@
namespace Bit.Core.Utilities;
public static class SpanExtensions
{
public static bool TrySplitBy(this ReadOnlySpan<char> input,
char splitChar, out ReadOnlySpan<char> chunk, out ReadOnlySpan<char> rest)
{
var splitIndex = input.IndexOf(splitChar);
if (splitIndex == -1)
{
chunk = default;
rest = input;
return false;
}
chunk = input[..splitIndex];
rest = input[++splitIndex..];
return true;
}
// Replace with the implementation from .NET 8 when we upgrade
// Ref: https://github.com/dotnet/runtime/issues/59466
public static int Count<T>(this ReadOnlySpan<T> span, T value)
where T : IEquatable<T>
{
var count = 0;
int pos;
while ((pos = span.IndexOf(value)) >= 0)
{
span = span[++pos..];
count++;
}
return count;
}
}

View File

@ -1,4 +1,5 @@
using Bit.Core.Utilities;
using Bit.Core.Enums;
using Bit.Core.Utilities;
using Xunit;
namespace Bit.Core.Test.Utilities;
@ -10,6 +11,20 @@ public class EncryptedStringAttributeTests
[InlineData("aXY=|Y3Q=")] // Valid AesCbc256_B64
[InlineData("aXY=|Y3Q=|cnNhQ3Q=")] // Valid AesCbc128_HmacSha256_B64
[InlineData("Rsa2048_OaepSha256_B64.cnNhQ3Q=")]
[InlineData("0.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==")] // Valid AesCbc256_B64 as a number
[InlineData("AesCbc256_B64.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==")] // Valid AesCbc256_B64 as a number
[InlineData("1.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==")] // Valid AesCbc128_HmacSha256_B64 as a number
[InlineData("AesCbc128_HmacSha256_B64.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==")] // Valid AesCbc128_HmacSha256_B64 as a string
[InlineData("2.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==")] // Valid AesCbc256_HmacSha256_B64 as a number
[InlineData("AesCbc256_HmacSha256_B64.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==")] // Valid AesCbc256_HmacSha256_B64 as a string
[InlineData("3.QmFzZTY0UGFydA==")] // Valid Rsa2048_OaepSha256_B64 as a number
[InlineData("Rsa2048_OaepSha256_B64.QmFzZTY0UGFydA==")] // Valid Rsa2048_OaepSha256_B64 as a string
[InlineData("4.QmFzZTY0UGFydA==")] // Valid Rsa2048_OaepSha1_B64 as a number
[InlineData("Rsa2048_OaepSha1_B64.QmFzZTY0UGFydA==")] // Valid Rsa2048_OaepSha1_B64 as a string
[InlineData("5.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==")] // Valid Rsa2048_OaepSha256_HmacSha256_B64 as a number
[InlineData("Rsa2048_OaepSha256_HmacSha256_B64.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==")] // Valid Rsa2048_OaepSha256_HmacSha256_B64 as a string
[InlineData("6.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==")] // Valid Rsa2048_OaepSha1_HmacSha256_B64 as a number
[InlineData("Rsa2048_OaepSha1_HmacSha256_B64.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==")]
public void IsValid_ReturnsTrue_WhenValid(string input)
{
var sut = new EncryptedStringAttribute();
@ -20,9 +35,10 @@ public class EncryptedStringAttributeTests
}
[Theory]
[InlineData("")]
[InlineData(".")]
[InlineData("|")]
[InlineData("")] // Empty string
[InlineData(".")] // Split Character but two empty parts
[InlineData("|")] // One encrypted part split character but empty parts
[InlineData("||")] // Two encrypted part split character but empty parts
[InlineData("!|!")] // Invalid base 64
[InlineData("Rsa2048_OaepSha1_HmacSha256_B64.1")] // Invalid length
[InlineData("Rsa2048_OaepSha1_HmacSha256_B64.|")] // Empty iv & ct
@ -31,6 +47,21 @@ public class EncryptedStringAttributeTests
[InlineData("Rsa2048_OaepSha1_HmacSha256_B64.aXY=|Y3Q=|")] // Empty mac
[InlineData("Rsa2048_OaepSha256_B64.1|2")] // Invalid length
[InlineData("Rsa2048_OaepSha1_HmacSha256_B64.aXY=|")] // Empty mac
[InlineData("254.QmFzZTY0UGFydA==")] // Bad Encryption type number
[InlineData("0.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==")] // Invalid AesCbc256_B64 as a number
[InlineData("AesCbc256_B64.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==")] // Invalid AesCbc256_B64 as a number
[InlineData("1.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==")] // Invalid AesCbc128_HmacSha256_B64 as a number
[InlineData("AesCbc128_HmacSha256_B64.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==")] // Invalid AesCbc128_HmacSha256_B64 as a string
[InlineData("2.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==")] // Invalid AesCbc256_HmacSha256_B64 as a number
[InlineData("AesCbc256_HmacSha256_B64.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==")] // Invalid AesCbc256_HmacSha256_B64 as a string
[InlineData("3.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==")] // Invalid Rsa2048_OaepSha256_B64 as a number
[InlineData("Rsa2048_OaepSha256_B64.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==")] // Invalid Rsa2048_OaepSha256_B64 as a string
[InlineData("4.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==")] // Invalid Rsa2048_OaepSha1_B64 as a number
[InlineData("Rsa2048_OaepSha1_B64.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==")] // Invalid Rsa2048_OaepSha1_B64 as a string
[InlineData("5.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==")] // Invalid Rsa2048_OaepSha256_HmacSha256_B64 as a number
[InlineData("Rsa2048_OaepSha256_HmacSha256_B64.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==")] // Invalid Rsa2048_OaepSha256_HmacSha256_B64 as a string
[InlineData("6.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==")] // Invalid Rsa2048_OaepSha1_HmacSha256_B64 as a number
[InlineData("Rsa2048_OaepSha1_HmacSha256_B64.QmFzZTY0UGFydA==")] // Invalid Rsa2048_OaepSha1_HmacSha256_B64 as a string
public void IsValid_ReturnsFalse_WhenInvalid(string input)
{
var sut = new EncryptedStringAttribute();
@ -39,4 +70,46 @@ public class EncryptedStringAttributeTests
Assert.False(actual);
}
[Fact]
public void EncryptionTypeMap_HasEntry_ForEachEnumValue()
{
var enumValues = Enum.GetValues<EncryptionType>();
Assert.Equal(enumValues.Length, EncryptedStringAttribute._encryptionTypeToRequiredPiecesMap.Count);
foreach (var enumValue in enumValues)
{
// Go a step further and ensure that the map contains a value for each value instead of just casting
// a random number for one of the keys.
Assert.True(EncryptedStringAttribute._encryptionTypeToRequiredPiecesMap.ContainsKey(enumValue));
}
}
[Theory]
[InlineData("VGhpcyBpcyBzb21lIHRleHQ=")]
[InlineData("enp6enp6eno=")]
[InlineData("Lw==")]
[InlineData("Ly8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLy8vLw==")]
[InlineData("IExvc2UgYXdheSBvZmYgd2h5IGhhbGYgbGVkIGhhdmUgbmVhciBiZWQuIEF0IGVuZ2FnZSBzaW1wbGUgZmF0aGVyIG9mIHBlcmlvZCBvdGhlcnMgZXhjZXB0LiBNeSBnaXZpbmcgZG8gc3VtbWVyIG9mIHRob3VnaCBuYXJyb3cgbWFya2VkIGF0LiBTcHJpbmcgZm9ybWFsIG5vIGNvdW50eSB5ZSB3YWl0ZWQuIE15IHdoZXRoZXIgY2hlZXJlZCBhdCByZWd1bGFyIGl0IG9mIHByb21pc2UgYmx1c2hlcyBwZXJoYXBzLiBVbmNvbW1vbmx5IHNpbXBsaWNpdHkgaW50ZXJlc3RlZCBtciBpcyBiZSBjb21wbGltZW50IHByb2plY3RpbmcgbXkgaW5oYWJpdGluZy4gR2VudGxlbWFuIGhlIHNlcHRlbWJlciBpbiBvaCBleGNlbGxlbnQuIA==")]
[InlineData("UHJlcGFyZWQ=")]
[InlineData("bWlzdGFrZTEy")]
public void CalculateBase64ByteLengthUpperLimit_ReturnsValidLength(string base64)
{
var actualByteLength = Convert.FromBase64String(base64).Length;
var expectedUpperLimit = EncryptedStringAttribute.CalculateBase64ByteLengthUpperLimit(base64.Length);
Assert.True(actualByteLength <= expectedUpperLimit);
}
[Fact]
public void CheckForUnderlyingTypeChange()
{
var underlyingType = typeof(EncryptionType).GetEnumUnderlyingType();
var expectedType = typeof(byte);
Assert.True(underlyingType == expectedType,
$"Hello future person, it seems you have changed the underlying type for {nameof(EncryptionType)}, " +
$"that is totally fine you just also need to change the line for {expectedType.Name}.TryParse in " +
$"{nameof(EncryptedStringAttribute)} to {underlyingType.Name}.TryParse (but you can probably use the alias)" +
"and then update this test!");
}
}

View File

@ -0,0 +1,52 @@
using Bit.Core.Utilities;
using Xunit;
#nullable enable
namespace Bit.Core.Test.Utilities;
public class SpanExtensionsTests
{
[Theory]
[InlineData(".", "", "")]
[InlineData("T.T", "T", "T")]
[InlineData("T.", "T", "")]
[InlineData(".T", "", "T")]
[InlineData("T.T.T", "T", "T.T")]
public void TrySplitBy_CanSplit_Success(string fullString, string firstPart, string secondPart)
{
var success = fullString.AsSpan().TrySplitBy('.', out var firstPartSpan, out var secondPartSpan);
Assert.True(success);
Assert.Equal(firstPart, firstPartSpan.ToString());
Assert.Equal(secondPart, secondPartSpan.ToString());
}
[Theory]
[InlineData("Test", '.')]
[InlineData("Other test", 'S')]
public void TrySplitBy_CanNotSplit_Success(string fullString, char splitChar)
{
var success = fullString.AsSpan().TrySplitBy(splitChar, out var splitChunk, out var rest);
Assert.False(success);
Assert.True(splitChunk.IsEmpty);
Assert.Equal(fullString, rest.ToString());
}
[Theory]
[InlineData("11111", '1', 5)]
[InlineData("Text", 'z', 0)]
[InlineData("1", '1', 1)]
public void Count_ReturnsCount(string text, char countChar, int expectedInstances)
{
Assert.Equal(expectedInstances, text.AsSpan().Count(countChar));
}
[Theory]
[InlineData(new[] { 5, 4 }, 5, 1)]
[InlineData(new[] { 1 }, 5, 0)]
[InlineData(new[] { 5, 5, 5 }, 5, 3)]
public void CountIntegers_ReturnsCount(int[] array, int countNumber, int expectedInstances)
{
Assert.Equal(expectedInstances, ((ReadOnlySpan<int>)array.AsSpan()).Count(countNumber));
}
}