1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-02 16:42:50 -05:00

Merge branch 'master' into EC-343-server-gate-custom-permissions-behind-enterprise

This commit is contained in:
Rui Tome
2022-10-24 11:18:46 +01:00
37 changed files with 3310 additions and 209 deletions

View File

@ -71,3 +71,11 @@ body:
- Operating system: [e.g. Windows 10, Mac OS Catalina]
- Environment: [e.g. Docker, EKS, ECS, K8S]
- Hardware: [e.g. Intel 6-core, 8GB RAM]
- type: checkboxes
id: issue-tracking-info
attributes:
label: Issue Tracking Info
description: |
Issue tracking information
options:
- label: I understand that work is tracked outside of Github. A PR will be linked to this issue should one be opened to address it, but Bitwarden doesn't use fields like "assigned", "milestone", or "project" to track progress.

View File

@ -16,6 +16,26 @@ jobs:
- name: Checkout Branch
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
- name: Login to Azure - Prod Subscription
uses: Azure/login@1f63701bf3e6892515f1b7ce2d2bf1708b46beaf
with:
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
- name: Retrieve secrets
id: retrieve-secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@c3b3285993151c5af47cefcb3b9134c28ab479af
with:
keyvault: "bitwarden-prod-kv"
secrets: "github-gpg-private-key, github-gpg-private-key-passphrase"
- name: Import GPG key
uses: crazy-max/ghaction-import-gpg@c8bb57c57e8df1be8c73ff3d59deab1dbc00e0d1
with:
gpg_private_key: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key }}
passphrase: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key-passphrase }}
git_user_signingkey: true
git_commit_gpgsign: true
- name: Create Version Branch
run: |
git switch -c version_bump_${{ github.event.inputs.version_number }}
@ -28,8 +48,8 @@ jobs:
- name: Setup git
run: |
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git config --local user.email "106330231+bitwarden-devops-bot@users.noreply.github.com"
git config --local user.name "bitwarden-devops-bot"
- name: Check if version changed
id: version-changed

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,8 +288,9 @@ 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}
{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

View File

@ -6,12 +6,44 @@
# in the future and investigate if we can migrate back.
# docker-compose --profile mssql exec mssql bash /mnt/helpers/run_migrations.sh @args
docker run `
-v "$(pwd)/helpers/mssql:/mnt/helpers" `
-v "$(pwd)/../util/Migrator:/mnt/migrator/" `
-v "$(pwd)/.data/mssql:/mnt/data" `
--env-file .env `
--network=bitwardenserver_default `
--rm `
mcr.microsoft.com/mssql-tools `
/mnt/helpers/run_migrations.sh @args
param([switch]$all = $false, [switch]$postgres = $false, [switch]$mysql = $false, [switch]$mssql = $false)
if (!$all -and !$postgres -and !$mysql) {
$mssql = $true;
}
if ($all -or $postgres -or $mysql) {
dotnet ef *> $null
if ($LASTEXITCODE -ne 0) {
Write-Host "Entity Framework Core tools were not found in the dotnet global tools. Attempting to install"
dotnet tool install dotnet-ef -g
}
}
if ($all -or $mssql) {
Write-Host "Starting Microsoft SQL Server Migrations"
docker run `
-v "$(pwd)/helpers/mssql:/mnt/helpers" `
-v "$(pwd)/../util/Migrator:/mnt/migrator/" `
-v "$(pwd)/.data/mssql:/mnt/data" `
--env-file .env `
--network=bitwardenserver_default `
--rm `
mcr.microsoft.com/mssql-tools `
/mnt/helpers/run_migrations.sh @args
}
$currentDir = Get-Location
if ($all -or $mysql) {
Write-Host "Starting MySQL Migrations"
Set-Location "$currentDir/../util/MySqlMigrations/"
dotnet ef database update
}
if ($all -or $postgres) {
Write-Host "Starting PostgreSQL Migrations"
Set-Location "$currentDir/../util/PostgresMigrations/"
dotnet ef database update
}
Set-Location "$currentDir"

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

@ -24,8 +24,8 @@
<option asp-selected="@(Model.Project == "Identity")" value="Identity">Identity</option>
<option asp-selected="@(Model.Project == "Notifications")" value="Notifications">Notifications</option>
<option asp-selected="@(Model.Project == "Icons")" value="Icons">Icons</option>
<option asp-selected="@(Model.Project == "Business Portal")" value="Business Portal">Business Portal</option>
<option asp-selected="@(Model.Project == "SSO")" value="SSO">SSO</option>
<option asp-selected="@(Model.Project == "Scim")" value="Scim">SCIM</option>
</select>
<input class="form-control mb-2 mr-2" type="datetime-local" asp-for="Start" name="start" placeholder="Start Date">
<input class="form-control mb-2 mr-2" type="datetime-local" asp-for="End" name="end" placeholder="End Date">

View File

@ -6,6 +6,7 @@ using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.LoginFeatures.PasswordlessLogin.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
@ -28,6 +29,7 @@ public class TwoFactorController : Controller
private readonly GlobalSettings _globalSettings;
private readonly UserManager<User> _userManager;
private readonly ICurrentContext _currentContext;
private readonly IVerifyAuthRequestCommand _verifyAuthRequestCommand;
public TwoFactorController(
IUserService userService,
@ -35,7 +37,8 @@ public class TwoFactorController : Controller
IOrganizationService organizationService,
GlobalSettings globalSettings,
UserManager<User> userManager,
ICurrentContext currentContext)
ICurrentContext currentContext,
IVerifyAuthRequestCommand verifyAuthRequestCommand)
{
_userService = userService;
_organizationRepository = organizationRepository;
@ -43,6 +46,7 @@ public class TwoFactorController : Controller
_globalSettings = globalSettings;
_userManager = userManager;
_currentContext = currentContext;
_verifyAuthRequestCommand = verifyAuthRequestCommand;
}
[HttpGet("")]
@ -285,19 +289,27 @@ public class TwoFactorController : Controller
var user = await _userManager.FindByEmailAsync(model.Email.ToLowerInvariant());
if (user != null)
{
if (await _userService.VerifySecretAsync(user, model.Secret))
// check if 2FA email is from passwordless
if (!string.IsNullOrEmpty(model.AuthRequestAccessCode))
{
var isBecauseNewDeviceLogin = false;
if (user.GetTwoFactorProvider(TwoFactorProviderType.Email) is null
&&
await _userService.Needs2FABecauseNewDeviceAsync(user, model.DeviceIdentifier, null))
if (await _verifyAuthRequestCommand
.VerifyAuthRequestAsync(new Guid(model.AuthRequestId), model.AuthRequestAccessCode))
{
model.ToUser(user);
isBecauseNewDeviceLogin = true;
}
var isBecauseNewDeviceLogin = await IsNewDeviceLoginAsync(user, model);
await _userService.SendTwoFactorEmailAsync(user, isBecauseNewDeviceLogin);
return;
await _userService.SendTwoFactorEmailAsync(user, isBecauseNewDeviceLogin);
return;
}
}
else
{
if (await _userService.VerifySecretAsync(user, model.Secret))
{
var isBecauseNewDeviceLogin = await IsNewDeviceLoginAsync(user, model);
await _userService.SendTwoFactorEmailAsync(user, isBecauseNewDeviceLogin);
return;
}
}
}
@ -455,4 +467,17 @@ public class TwoFactorController : Controller
await Task.Delay(500);
}
}
private async Task<bool> IsNewDeviceLoginAsync(User user, TwoFactorEmailRequestModel model)
{
if (user.GetTwoFactorProvider(TwoFactorProviderType.Email) is null
&&
await _userService.Needs2FABecauseNewDeviceAsync(user, model.DeviceIdentifier, null))
{
model.ToUser(user);
return true;
}
return false;
}
}

View File

@ -10,7 +10,7 @@ public class KdfRequestModel : PasswordRequestModel, IValidatableObject
[Required]
public int? KdfIterations { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
public override IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (Kdf.HasValue && KdfIterations.HasValue)
{

View File

@ -7,13 +7,14 @@ public class SecretVerificationRequestModel : IValidatableObject
[StringLength(300)]
public string MasterPasswordHash { get; set; }
public string OTP { get; set; }
public string AuthRequestAccessCode { get; set; }
public string Secret => !string.IsNullOrEmpty(MasterPasswordHash) ? MasterPasswordHash : OTP;
public virtual IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (string.IsNullOrEmpty(Secret))
if (string.IsNullOrEmpty(Secret) && string.IsNullOrEmpty(AuthRequestAccessCode))
{
yield return new ValidationResult("MasterPasswordHash or OTP must be supplied.");
yield return new ValidationResult("MasterPasswordHash, OTP or AccessCode must be supplied.");
}
}
}

View File

@ -102,7 +102,7 @@ public class UpdateTwoFactorDuoRequestModel : SecretVerificationRequestModel, IV
return extistingOrg;
}
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
public override IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (!Core.Utilities.Duo.DuoApi.ValidHost(Host))
{
@ -160,7 +160,7 @@ public class UpdateTwoFactorYubicoOtpRequestModel : SecretVerificationRequestMod
return keyValue.Substring(0, 12);
}
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
public override IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (string.IsNullOrWhiteSpace(Key1) && string.IsNullOrWhiteSpace(Key2) && string.IsNullOrWhiteSpace(Key3) &&
string.IsNullOrWhiteSpace(Key4) && string.IsNullOrWhiteSpace(Key5))
@ -204,6 +204,8 @@ public class TwoFactorEmailRequestModel : SecretVerificationRequestModel
public string DeviceIdentifier { get; set; }
public string AuthRequestId { get; set; }
public User ToUser(User extistingUser)
{
var providers = extistingUser.GetTwoFactorProviders();

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,14 @@
using Bit.Core.LoginFeatures.PasswordlessLogin;
using Bit.Core.LoginFeatures.PasswordlessLogin.Interfaces;
using Microsoft.Extensions.DependencyInjection;
namespace Bit.Core.LoginFeatures;
public static class LoginServiceCollectionExtensions
{
public static void AddLoginServices(this IServiceCollection services)
{
services.AddScoped<IVerifyAuthRequestCommand, VerifyAuthRequestCommand>();
}
}

View File

@ -0,0 +1,6 @@
namespace Bit.Core.LoginFeatures.PasswordlessLogin.Interfaces;
public interface IVerifyAuthRequestCommand
{
Task<bool> VerifyAuthRequestAsync(Guid authRequestId, string accessCode);
}

View File

@ -0,0 +1,24 @@
using Bit.Core.LoginFeatures.PasswordlessLogin.Interfaces;
using Bit.Core.Repositories;
namespace Bit.Core.LoginFeatures.PasswordlessLogin;
public class VerifyAuthRequestCommand : IVerifyAuthRequestCommand
{
private readonly IAuthRequestRepository _authRequestRepository;
public VerifyAuthRequestCommand(IAuthRequestRepository authRequestRepository)
{
_authRequestRepository = authRequestRepository;
}
public async Task<bool> VerifyAuthRequestAsync(Guid authRequestId, string accessCode)
{
var authRequest = await _authRequestRepository.GetByIdAsync(authRequestId);
if (authRequest == null || authRequest.AccessCode != accessCode)
{
return false;
}
return true;
}
}

View File

@ -54,16 +54,19 @@ public abstract class BaseIdentityClientService : IDisposable
protected async Task<TResult> SendAsync<TRequest, TResult>(HttpMethod method, string path, TRequest requestModel)
{
var fullRequestPath = string.Concat(Client.BaseAddress, path);
var tokenStateResponse = await HandleTokenStateAsync();
if (!tokenStateResponse)
{
_logger.LogError("Unable to send {method} request to {requestUri} because an access token was unable to be obtained", method.Method, fullRequestPath);
return default;
}
var message = new TokenHttpRequestMessage(requestModel, AccessToken)
{
Method = method,
RequestUri = new Uri(string.Concat(Client.BaseAddress, path))
RequestUri = new Uri(fullRequestPath)
};
try
{
@ -120,7 +123,7 @@ public abstract class BaseIdentityClientService : IDisposable
if (!response.IsSuccessStatusCode)
{
_logger.LogInformation("Unsuccessful token response with status code {StatusCode}", response.StatusCode);
_logger.LogInformation("Unsuccessful token response from {identity} for client {clientId} with status code {StatusCode}", IdentityClient.BaseAddress, _identityClientId, response.StatusCode);
if (response.StatusCode == HttpStatusCode.BadRequest)
{

View File

@ -44,7 +44,7 @@ public class MultiServicePushNotificationService : IPushNotificationService
if (CoreHelpers.SettingHasValue(globalSettings.NotificationHub.ConnectionString))
{
_services.Add(new NotificationHubPushNotificationService(installationDeviceRepository,
globalSettings, httpContextAccessor));
globalSettings, httpContextAccessor, hubLogger));
}
if (CoreHelpers.SettingHasValue(globalSettings.Notifications?.ConnectionString))
{

View File

@ -9,6 +9,7 @@ using Bit.Core.Repositories;
using Bit.Core.Settings;
using Microsoft.AspNetCore.Http;
using Microsoft.Azure.NotificationHubs;
using Microsoft.Extensions.Logging;
namespace Bit.Core.Services;
@ -17,20 +18,23 @@ public class NotificationHubPushNotificationService : IPushNotificationService
private readonly IInstallationDeviceRepository _installationDeviceRepository;
private readonly GlobalSettings _globalSettings;
private readonly IHttpContextAccessor _httpContextAccessor;
private NotificationHubClient _client = null;
private ILogger _logger;
public NotificationHubPushNotificationService(
IInstallationDeviceRepository installationDeviceRepository,
GlobalSettings globalSettings,
IHttpContextAccessor httpContextAccessor)
IHttpContextAccessor httpContextAccessor,
ILogger<NotificationsApiPushNotificationService> logger)
{
_installationDeviceRepository = installationDeviceRepository;
_globalSettings = globalSettings;
_httpContextAccessor = httpContextAccessor;
_client = NotificationHubClient.CreateClientFromConnectionString(
_globalSettings.NotificationHub.ConnectionString,
_globalSettings.NotificationHub.HubName);
_globalSettings.NotificationHub.HubName,
_globalSettings.NotificationHub.EnableSendTracing);
_logger = logger;
}
public async Task PushSyncCipherCreateAsync(Cipher cipher, IEnumerable<Guid> collectionIds)
@ -244,12 +248,17 @@ public class NotificationHubPushNotificationService : IPushNotificationService
private async Task SendPayloadAsync(string tag, PushType type, object payload)
{
await _client.SendTemplateNotificationAsync(
var outcome = await _client.SendTemplateNotificationAsync(
new Dictionary<string, string>
{
{ "type", ((byte)type).ToString() },
{ "payload", JsonSerializer.Serialize(payload) }
}, tag);
if (_globalSettings.NotificationHub.EnableSendTracing)
{
_logger.LogInformation("Azure Notification Hub Tracking ID: {id} | {type} push notification with {success} successes and {failure} failures with a payload of {@payload} and result of {@results}",
outcome.TrackingId, type, outcome.Success, outcome.Failure, payload, outcome.Results);
}
}
private string SanitizeTagInput(string input)

View File

@ -412,6 +412,12 @@ public class GlobalSettings : IGlobalSettings
set => _connectionString = value.Trim('"');
}
public string HubName { get; set; }
/// <summary>
/// Enables TestSend on the Azure Notification Hub, which allows tracing of the request through the hub and to the platform-specific push notification service (PNS).
/// Enabling this will result in delayed responses because the Hub must wait on delivery to the PNS. This should ONLY be enabled in a non-production environment, as results are throttled.
/// </summary>
public bool EnableSendTracing { get; set; } = false;
}
public class YubicoSettings

View File

@ -23,7 +23,6 @@ public class DataProtectorTokenFactory<T> : IDataProtectorTokenFactory<T> where
/// Unprotect token
/// </summary>
/// <param name="token">The token to parse</param>
/// <typeparam name="T">The tokenable type to parse to</typeparam>
/// <returns>The parsed tokenable</returns>
/// <exception>Throws CryptographicException if fails to unprotect</exception>
public T Unprotect(string token) =>

View File

@ -27,7 +27,6 @@ public static class CoreHelpers
private static readonly DateTime _epoc = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
private static readonly DateTime _max = new DateTime(9999, 1, 1, 0, 0, 0, DateTimeKind.Utc);
private static readonly Random _random = new Random();
private static string _version;
private static readonly string CloudFlareConnectingIp = "CF-Connecting-IP";
private static readonly string RealIp = "X-Real-IP";

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

@ -30,7 +30,7 @@ public class StaticStore
GlobalDomains.Add(GlobalEquivalentDomainsType.Avon, new List<string> { "avon.com", "youravon.com" });
GlobalDomains.Add(GlobalEquivalentDomainsType.Diapers, new List<string> { "diapers.com", "soap.com", "wag.com", "yoyo.com", "beautybar.com", "casa.com", "afterschool.com", "vine.com", "bookworm.com", "look.com", "vinemarket.com" });
GlobalDomains.Add(GlobalEquivalentDomainsType.Contacts, new List<string> { "1800contacts.com", "800contacts.com" });
GlobalDomains.Add(GlobalEquivalentDomainsType.Amazon, new List<string> { "amazon.com", "amazon.ae", "amazon.ca", "amazon.co.uk", "amazon.com.au", "amazon.com.br", "amazon.com.mx", "amazon.com.tr", "amazon.de", "amazon.es", "amazon.fr", "amazon.in", "amazon.it", "amazon.nl", "amazon.pl", "amazon.sa", "amazon.se", "amazon.sg" });
GlobalDomains.Add(GlobalEquivalentDomainsType.Amazon, new List<string> { "amazon.com", "amazon.com.be", "amazon.ae", "amazon.ca", "amazon.co.uk", "amazon.com.au", "amazon.com.br", "amazon.com.mx", "amazon.com.tr", "amazon.de", "amazon.es", "amazon.fr", "amazon.in", "amazon.it", "amazon.nl", "amazon.pl", "amazon.sa", "amazon.se", "amazon.sg" });
GlobalDomains.Add(GlobalEquivalentDomainsType.Cox, new List<string> { "cox.com", "cox.net", "coxbusiness.com" });
GlobalDomains.Add(GlobalEquivalentDomainsType.Norton, new List<string> { "mynortonaccount.com", "norton.com" });
GlobalDomains.Add(GlobalEquivalentDomainsType.Verizon, new List<string> { "verizon.com", "verizon.net" });

View File

@ -31,16 +31,14 @@ public class StrictEmailAddressAttribute : ValidationAttribute
return false;
}
/**
The regex below is intended to catch edge cases that are not handled by the general parsing check above.
This enforces the following rules:
* Requires ASCII only in the local-part (code points 0-127)
* Requires an @ symbol
* Allows any char in second-level domain name, including unicode and symbols
* Requires at least one period (.) separating SLD from TLD
* Must end in a letter (including unicode)
See the unit tests for examples of what is allowed.
**/
// The regex below is intended to catch edge cases that are not handled by the general parsing check above.
// This enforces the following rules:
// * Requires ASCII only in the local-part (code points 0-127)
// * Requires an @ symbol
// * Allows any char in second-level domain name, including unicode and symbols
// * Requires at least one period (.) separating SLD from TLD
// * Must end in a letter (including unicode)
// See the unit tests for examples of what is allowed.
var emailFormat = @"^[\x00-\x7F]+@.+\.\p{L}+$";
if (!Regex.IsMatch(emailAddress, emailFormat))
{

View File

@ -113,7 +113,7 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
if (authRequest != null)
{
var requestAge = DateTime.UtcNow - authRequest.CreationDate;
if (requestAge < TimeSpan.FromHours(1) && !authRequest.AuthenticationDate.HasValue &&
if (requestAge < TimeSpan.FromHours(1) &&
CoreHelpers.FixedTimeEquals(authRequest.AccessCode, context.Password))
{
authRequest.AuthenticationDate = DateTime.UtcNow;
@ -123,14 +123,12 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
}
return false;
}
else
if (!await _userService.CheckPasswordAsync(validatorContext.User, context.Password))
{
if (!await _userService.CheckPasswordAsync(validatorContext.User, context.Password))
{
return false;
}
return true;
return false;
}
return true;
}
protected override Task SetSuccessResult(ResourceOwnerPasswordValidationContext context, User user,

View File

@ -1,5 +1,6 @@
using Bit.Infrastructure.EntityFramework.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Bit.Infrastructure.EntityFramework.Repositories;
@ -139,5 +140,29 @@ public class DatabaseContext : DbContext
eOrganizationApiKey.ToTable(nameof(OrganizationApiKey));
eOrganizationConnection.ToTable(nameof(OrganizationConnection));
eAuthRequest.ToTable(nameof(AuthRequest));
ConfigureDateTimeUTCQueries(builder);
}
// Make sure this is called after configuring all the entities as it iterates through all setup entities.
private static void ConfigureDateTimeUTCQueries(ModelBuilder builder)
{
foreach (var entityType in builder.Model.GetEntityTypes())
{
if (entityType.IsKeyless)
{
continue;
}
foreach (var property in entityType.GetProperties())
{
if (property.ClrType == typeof(DateTime) || property.ClrType == typeof(DateTime?))
{
property.SetValueConverter(
new ValueConverter<DateTime, DateTime>(
v => v,
v => new DateTime(v.Ticks, DateTimeKind.Utc)));
}
}
}
}
}

View File

@ -7,6 +7,7 @@ using Bit.Core.Enums;
using Bit.Core.HostedServices;
using Bit.Core.Identity;
using Bit.Core.IdentityServer;
using Bit.Core.LoginFeatures;
using Bit.Core.Models.Business.Tokenables;
using Bit.Core.OrganizationFeatures;
using Bit.Core.Repositories;
@ -108,6 +109,7 @@ public static class ServiceCollectionExtensions
services.AddSingleton<IAppleIapService, AppleIapService>();
services.AddScoped<ISsoConfigService, SsoConfigService>();
services.AddScoped<ISendService, SendService>();
services.AddLoginServices();
}
public static void AddTokenizers(this IServiceCollection services)

View File

@ -25,7 +25,7 @@ public class AccountsControllerTest : IClassFixture<ApiApplicationFactory>
var content = await response.Content.ReadFromJsonAsync<ProfileResponseModel>();
Assert.NotEmpty(content.Id);
Assert.NotEmpty(content!.Id);
Assert.Equal("integration-test@bitwarden.com", content.Email);
Assert.Null(content.Name);
Assert.False(content.EmailVerified);

View File

@ -71,7 +71,7 @@ public class FreshdeskControllerTests
return Send(request, cancellationToken);
}
public virtual Task<HttpResponseMessage> Send(HttpRequestMessage request, CancellationToken cancellationToken)
public new virtual Task<HttpResponseMessage> Send(HttpRequestMessage request, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}

View File

@ -2,6 +2,7 @@
using Bit.Core.Services;
using Bit.Core.Settings;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using NSubstitute;
using Xunit;
@ -14,17 +15,20 @@ public class NotificationHubPushNotificationServiceTests
private readonly IInstallationDeviceRepository _installationDeviceRepository;
private readonly GlobalSettings _globalSettings;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly ILogger<NotificationsApiPushNotificationService> _logger;
public NotificationHubPushNotificationServiceTests()
{
_installationDeviceRepository = Substitute.For<IInstallationDeviceRepository>();
_globalSettings = new GlobalSettings();
_httpContextAccessor = Substitute.For<IHttpContextAccessor>();
_logger = Substitute.For<ILogger<NotificationsApiPushNotificationService>>();
_sut = new NotificationHubPushNotificationService(
_installationDeviceRepository,
_globalSettings,
_httpContextAccessor
_httpContextAccessor,
_logger
);
}

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));
}
}

View File

@ -24,14 +24,14 @@ public class CipherRepositoryTests
}
[CiSkippedTheory, EfUserCipherCustomize, BitAutoData]
public async void UserCipher_CreateAsync_Works_DataMatches(Cipher cipher, User user, Organization org,
public void UserCipher_CreateAsync_Works_DataMatches(Cipher cipher, User user, Organization org,
CipherCompare equalityComparer, List<EfRepo.CipherRepository> suts, List<EfRepo.UserRepository> efUserRepos,
List<EfRepo.OrganizationRepository> efOrgRepos, SqlRepo.CipherRepository sqlCipherRepo,
SqlRepo.UserRepository sqlUserRepo, SqlRepo.OrganizationRepository sqlOrgRepo) => CreateAsync_Works_DataMatches(
cipher, user, org, equalityComparer, suts, efUserRepos, efOrgRepos, sqlCipherRepo, sqlUserRepo, sqlOrgRepo);
[CiSkippedTheory, EfOrganizationCipherCustomize, BitAutoData]
public async void OrganizationCipher_CreateAsync_Works_DataMatches(Cipher cipher, User user, Organization org,
public void OrganizationCipher_CreateAsync_Works_DataMatches(Cipher cipher, User user, Organization org,
CipherCompare equalityComparer, List<EfRepo.CipherRepository> suts, List<EfRepo.UserRepository> efUserRepos,
List<EfRepo.OrganizationRepository> efOrgRepos, SqlRepo.CipherRepository sqlCipherRepo,
SqlRepo.UserRepository sqlUserRepo, SqlRepo.OrganizationRepository sqlOrgRepo) => CreateAsync_Works_DataMatches(
@ -177,7 +177,7 @@ public class CipherRepositoryTests
List<EfRepo.OrganizationRepository> efOrgRepos
) => await DeleteAsync_CipherIsDeleted(cipher, user, org, suts, efUserRepos, efOrgRepos);
[CiSkippedTheory, EfOrganizationCipherCustomize, BitAutoData]
public async Task OrganizationCipher_DeleteAsync_CipherIsDeleted(
public Task OrganizationCipher_DeleteAsync_CipherIsDeleted(
Cipher cipher,
User user,
Organization org,