mirror of
https://github.com/bitwarden/server.git
synced 2025-07-01 16:12:49 -05:00
Add new logic for validating encrypted strings
This commit is contained in:
@ -1,5 +1,7 @@
|
|||||||
namespace Bit.Core.Enums;
|
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
|
public enum EncryptionType : byte
|
||||||
{
|
{
|
||||||
AesCbc256_B64 = 0,
|
AesCbc256_B64 = 0,
|
||||||
|
185
src/Core/Utilities/EncryptedStringAttribute.cs
Normal file
185
src/Core/Utilities/EncryptedStringAttribute.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
21
src/Core/Utilities/SpanExtensions.cs
Normal file
21
src/Core/Utilities/SpanExtensions.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -10,6 +10,20 @@ public class EncryptedStringAttributeTests
|
|||||||
[InlineData("aXY=|Y3Q=")] // Valid AesCbc256_B64
|
[InlineData("aXY=|Y3Q=")] // Valid AesCbc256_B64
|
||||||
[InlineData("aXY=|Y3Q=|cnNhQ3Q=")] // Valid AesCbc128_HmacSha256_B64
|
[InlineData("aXY=|Y3Q=|cnNhQ3Q=")] // Valid AesCbc128_HmacSha256_B64
|
||||||
[InlineData("Rsa2048_OaepSha256_B64.cnNhQ3Q=")]
|
[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)
|
public void IsValid_ReturnsTrue_WhenValid(string input)
|
||||||
{
|
{
|
||||||
var sut = new EncryptedStringAttribute();
|
var sut = new EncryptedStringAttribute();
|
||||||
@ -20,9 +34,10 @@ public class EncryptedStringAttributeTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("")]
|
[InlineData("")] // Empty string
|
||||||
[InlineData(".")]
|
[InlineData(".")] // Split Character but two empty parts
|
||||||
[InlineData("|")]
|
[InlineData("|")] // One encrypted part split character but empty parts
|
||||||
|
[InlineData("||")] // Two encrypted part split character but empty parts
|
||||||
[InlineData("!|!")] // Invalid base 64
|
[InlineData("!|!")] // Invalid base 64
|
||||||
[InlineData("Rsa2048_OaepSha1_HmacSha256_B64.1")] // Invalid length
|
[InlineData("Rsa2048_OaepSha1_HmacSha256_B64.1")] // Invalid length
|
||||||
[InlineData("Rsa2048_OaepSha1_HmacSha256_B64.|")] // Empty iv & ct
|
[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_OaepSha1_HmacSha256_B64.aXY=|Y3Q=|")] // Empty mac
|
||||||
[InlineData("Rsa2048_OaepSha256_B64.1|2")] // Invalid length
|
[InlineData("Rsa2048_OaepSha256_B64.1|2")] // Invalid length
|
||||||
[InlineData("Rsa2048_OaepSha1_HmacSha256_B64.aXY=|")] // Empty mac
|
[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)
|
public void IsValid_ReturnsFalse_WhenInvalid(string input)
|
||||||
{
|
{
|
||||||
var sut = new EncryptedStringAttribute();
|
var sut = new EncryptedStringAttribute();
|
||||||
|
Reference in New Issue
Block a user