1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-01 08:02:49 -05:00

Add new logic for validating encrypted strings

This commit is contained in:
Justin Baur
2022-09-09 14:03:11 -04:00
parent e2b6f2a5bb
commit fc85475bcb
5 changed files with 241 additions and 140 deletions

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,185 @@
using System.Buffers;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics;
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
{
private static readonly Dictionary<EncryptionType, int> _encryptionTypeMap;
static EncryptedStringAttribute()
{
_encryptionTypeMap = new()
{
[EncryptionType.AesCbc256_B64] = 2,
[EncryptionType.AesCbc128_HmacSha256_B64] = 3,
[EncryptionType.AesCbc256_HmacSha256_B64] = 3,
[EncryptionType.Rsa2048_OaepSha256_B64] = 1,
[EncryptionType.Rsa2048_OaepSha1_B64] = 1,
[EncryptionType.Rsa2048_OaepSha256_HmacSha256_B64] = 2,
[EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64] = 2,
};
#if DEBUG
var enumValues = Enum.GetValues<EncryptionType>();
Debug.Assert(enumValues.Length == _encryptionTypeMap.Count,
$"New {nameof(EncryptionType)} enums should be added to the {nameof(_encryptionTypeMap)}");
#endif
}
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);
}
// Slow path (ish)
return IsValidCore(value.ToString());
}
catch
{
return false;
}
}
internal static bool IsValidCore(ReadOnlySpan<char> value)
{
if (!value.TrySplitBy('.', out var headerChunk, out var rest))
{
// We coundn'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 encryptionPiecesChunk = rest;
var pieces = 1;
var findIndex = encryptionPiecesChunk.IndexOf('|');
while(findIndex != -1)
{
pieces++;
encryptionPiecesChunk = encryptionPiecesChunk[++findIndex..];
findIndex = encryptionPiecesChunk.IndexOf('|');
}
if (pieces == 3)
{
return ValidatePieces(rest, _encryptionTypeMap[EncryptionType.AesCbc128_HmacSha256_B64]);
}
else
{
return ValidatePieces(rest, _encryptionTypeMap[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, _encryptionTypeMap[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 (!_encryptionTypeMap.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)
{
if (!IsValidBase64(rest))
{
return false;
}
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;
}
}
requiredPieces--;
}
// No more parts are required, so check there are no extra parts
return rest.IndexOf('|') == -1;
}
private static bool IsValidBase64(ReadOnlySpan<char> input)
{
byte[]? pooledChunks = null;
// Ref: https://vcsjones.dev/stackalloc/
var byteBuffer = input.Length > 128
? (pooledChunks = ArrayPool<byte>.Shared.Rent(input.Length * 2))
: stackalloc byte[256];
try
{
var successful = Convert.TryFromBase64Chars(input, byteBuffer, out var bytesWritten);
return successful && bytesWritten > 0;
}
finally
{
if (pooledChunks != null)
{
ArrayPool<byte>.Shared.Return(pooledChunks);
}
}
}
}

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,21 @@
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;
}
}

View File

@ -10,6 +10,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 +34,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 +46,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();