1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-03 00:52:49 -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] - Operating system: [e.g. Windows 10, Mac OS Catalina]
- Environment: [e.g. Docker, EKS, ECS, K8S] - Environment: [e.g. Docker, EKS, ECS, K8S]
- Hardware: [e.g. Intel 6-core, 8GB RAM] - 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 - name: Checkout Branch
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 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 - name: Create Version Branch
run: | run: |
git switch -c version_bump_${{ github.event.inputs.version_number }} git switch -c version_bump_${{ github.event.inputs.version_number }}
@ -28,8 +48,8 @@ jobs:
- name: Setup git - name: Setup git
run: | run: |
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com" git config --local user.email "106330231+bitwarden-devops-bot@users.noreply.github.com"
git config --local user.name "github-actions[bot]" git config --local user.name "bitwarden-devops-bot"
- name: Check if version changed - name: Check if version changed
id: version-changed id: version-changed

View File

@ -98,6 +98,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Api.IntegrationTest", "test
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Scim.IntegrationTest", "bitwarden_license\test\Scim.IntegrationTest\Scim.IntegrationTest.csproj", "{FE998849-5FC8-41A2-B7C9-9227901471A0}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Scim.IntegrationTest", "bitwarden_license\test\Scim.IntegrationTest\Scim.IntegrationTest.csproj", "{FE998849-5FC8-41A2-B7C9-9227901471A0}"
EndProject 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}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Scim.Test", "bitwarden_license\test\Scim.Test\Scim.Test.csproj", "{B1595DA3-4C60-41AA-8BF0-499A5F75A885}"
EndProject EndProject
Global Global
@ -240,6 +244,10 @@ Global
{FE998849-5FC8-41A2-B7C9-9227901471A0}.Debug|Any CPU.Build.0 = 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.ActiveCfg = Release|Any CPU
{FE998849-5FC8-41A2-B7C9-9227901471A0}.Release|Any CPU.Build.0 = 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.ActiveCfg = Debug|Any CPU
{B1595DA3-4C60-41AA-8BF0-499A5F75A885}.Debug|Any CPU.Build.0 = 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 {B1595DA3-4C60-41AA-8BF0-499A5F75A885}.Release|Any CPU.ActiveCfg = Release|Any CPU
@ -282,6 +290,7 @@ Global
{7EFB1124-F40A-40EB-9EDA-94FD540AA8FD} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F} {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} {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} {B1595DA3-4C60-41AA-8BF0-499A5F75A885} = {287CFF34-BBDB-4BC4-AF88-1E19A5A4679B}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution

View File

@ -6,6 +6,22 @@
# in the future and investigate if we can migrate back. # in the future and investigate if we can migrate back.
# docker-compose --profile mssql exec mssql bash /mnt/helpers/run_migrations.sh @args # docker-compose --profile mssql exec mssql bash /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 ` docker run `
-v "$(pwd)/helpers/mssql:/mnt/helpers" ` -v "$(pwd)/helpers/mssql:/mnt/helpers" `
-v "$(pwd)/../util/Migrator:/mnt/migrator/" ` -v "$(pwd)/../util/Migrator:/mnt/migrator/" `
@ -15,3 +31,19 @@ docker run `
--rm ` --rm `
mcr.microsoft.com/mssql-tools ` mcr.microsoft.com/mssql-tools `
/mnt/helpers/run_migrations.sh @args /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 == "Identity")" value="Identity">Identity</option>
<option asp-selected="@(Model.Project == "Notifications")" value="Notifications">Notifications</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 == "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 == "SSO")" value="SSO">SSO</option>
<option asp-selected="@(Model.Project == "Scim")" value="Scim">SCIM</option>
</select> </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="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"> <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.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.LoginFeatures.PasswordlessLogin.Interfaces;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Settings; using Bit.Core.Settings;
@ -28,6 +29,7 @@ public class TwoFactorController : Controller
private readonly GlobalSettings _globalSettings; private readonly GlobalSettings _globalSettings;
private readonly UserManager<User> _userManager; private readonly UserManager<User> _userManager;
private readonly ICurrentContext _currentContext; private readonly ICurrentContext _currentContext;
private readonly IVerifyAuthRequestCommand _verifyAuthRequestCommand;
public TwoFactorController( public TwoFactorController(
IUserService userService, IUserService userService,
@ -35,7 +37,8 @@ public class TwoFactorController : Controller
IOrganizationService organizationService, IOrganizationService organizationService,
GlobalSettings globalSettings, GlobalSettings globalSettings,
UserManager<User> userManager, UserManager<User> userManager,
ICurrentContext currentContext) ICurrentContext currentContext,
IVerifyAuthRequestCommand verifyAuthRequestCommand)
{ {
_userService = userService; _userService = userService;
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
@ -43,6 +46,7 @@ public class TwoFactorController : Controller
_globalSettings = globalSettings; _globalSettings = globalSettings;
_userManager = userManager; _userManager = userManager;
_currentContext = currentContext; _currentContext = currentContext;
_verifyAuthRequestCommand = verifyAuthRequestCommand;
} }
[HttpGet("")] [HttpGet("")]
@ -285,21 +289,29 @@ public class TwoFactorController : Controller
var user = await _userManager.FindByEmailAsync(model.Email.ToLowerInvariant()); var user = await _userManager.FindByEmailAsync(model.Email.ToLowerInvariant());
if (user != null) 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 (await _verifyAuthRequestCommand
if (user.GetTwoFactorProvider(TwoFactorProviderType.Email) is null .VerifyAuthRequestAsync(new Guid(model.AuthRequestId), model.AuthRequestAccessCode))
&&
await _userService.Needs2FABecauseNewDeviceAsync(user, model.DeviceIdentifier, null))
{ {
model.ToUser(user); var isBecauseNewDeviceLogin = await IsNewDeviceLoginAsync(user, model);
isBecauseNewDeviceLogin = true;
}
await _userService.SendTwoFactorEmailAsync(user, isBecauseNewDeviceLogin); await _userService.SendTwoFactorEmailAsync(user, isBecauseNewDeviceLogin);
return; return;
} }
} }
else
{
if (await _userService.VerifySecretAsync(user, model.Secret))
{
var isBecauseNewDeviceLogin = await IsNewDeviceLoginAsync(user, model);
await _userService.SendTwoFactorEmailAsync(user, isBecauseNewDeviceLogin);
return;
}
}
}
await Task.Delay(2000); await Task.Delay(2000);
throw new BadRequestException("Cannot send two-factor email."); throw new BadRequestException("Cannot send two-factor email.");
@ -455,4 +467,17 @@ public class TwoFactorController : Controller
await Task.Delay(500); 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] [Required]
public int? KdfIterations { get; set; } public int? KdfIterations { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) public override IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{ {
if (Kdf.HasValue && KdfIterations.HasValue) if (Kdf.HasValue && KdfIterations.HasValue)
{ {

View File

@ -7,13 +7,14 @@ public class SecretVerificationRequestModel : IValidatableObject
[StringLength(300)] [StringLength(300)]
public string MasterPasswordHash { get; set; } public string MasterPasswordHash { get; set; }
public string OTP { get; set; } public string OTP { get; set; }
public string AuthRequestAccessCode { get; set; }
public string Secret => !string.IsNullOrEmpty(MasterPasswordHash) ? MasterPasswordHash : OTP; public string Secret => !string.IsNullOrEmpty(MasterPasswordHash) ? MasterPasswordHash : OTP;
public virtual IEnumerable<ValidationResult> Validate(ValidationContext validationContext) 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; return extistingOrg;
} }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) public override IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{ {
if (!Core.Utilities.Duo.DuoApi.ValidHost(Host)) if (!Core.Utilities.Duo.DuoApi.ValidHost(Host))
{ {
@ -160,7 +160,7 @@ public class UpdateTwoFactorYubicoOtpRequestModel : SecretVerificationRequestMod
return keyValue.Substring(0, 12); 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) && if (string.IsNullOrWhiteSpace(Key1) && string.IsNullOrWhiteSpace(Key2) && string.IsNullOrWhiteSpace(Key3) &&
string.IsNullOrWhiteSpace(Key4) && string.IsNullOrWhiteSpace(Key5)) string.IsNullOrWhiteSpace(Key4) && string.IsNullOrWhiteSpace(Key5))
@ -204,6 +204,8 @@ public class TwoFactorEmailRequestModel : SecretVerificationRequestModel
public string DeviceIdentifier { get; set; } public string DeviceIdentifier { get; set; }
public string AuthRequestId { get; set; }
public User ToUser(User extistingUser) public User ToUser(User extistingUser)
{ {
var providers = extistingUser.GetTwoFactorProviders(); var providers = extistingUser.GetTwoFactorProviders();

View File

@ -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,

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) protected async Task<TResult> SendAsync<TRequest, TResult>(HttpMethod method, string path, TRequest requestModel)
{ {
var fullRequestPath = string.Concat(Client.BaseAddress, path);
var tokenStateResponse = await HandleTokenStateAsync(); var tokenStateResponse = await HandleTokenStateAsync();
if (!tokenStateResponse) 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; return default;
} }
var message = new TokenHttpRequestMessage(requestModel, AccessToken) var message = new TokenHttpRequestMessage(requestModel, AccessToken)
{ {
Method = method, Method = method,
RequestUri = new Uri(string.Concat(Client.BaseAddress, path)) RequestUri = new Uri(fullRequestPath)
}; };
try try
{ {
@ -120,7 +123,7 @@ public abstract class BaseIdentityClientService : IDisposable
if (!response.IsSuccessStatusCode) 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) if (response.StatusCode == HttpStatusCode.BadRequest)
{ {

View File

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

View File

@ -9,6 +9,7 @@ using Bit.Core.Repositories;
using Bit.Core.Settings; using Bit.Core.Settings;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Azure.NotificationHubs; using Microsoft.Azure.NotificationHubs;
using Microsoft.Extensions.Logging;
namespace Bit.Core.Services; namespace Bit.Core.Services;
@ -17,20 +18,23 @@ public class NotificationHubPushNotificationService : IPushNotificationService
private readonly IInstallationDeviceRepository _installationDeviceRepository; private readonly IInstallationDeviceRepository _installationDeviceRepository;
private readonly GlobalSettings _globalSettings; private readonly GlobalSettings _globalSettings;
private readonly IHttpContextAccessor _httpContextAccessor; private readonly IHttpContextAccessor _httpContextAccessor;
private NotificationHubClient _client = null; private NotificationHubClient _client = null;
private ILogger _logger;
public NotificationHubPushNotificationService( public NotificationHubPushNotificationService(
IInstallationDeviceRepository installationDeviceRepository, IInstallationDeviceRepository installationDeviceRepository,
GlobalSettings globalSettings, GlobalSettings globalSettings,
IHttpContextAccessor httpContextAccessor) IHttpContextAccessor httpContextAccessor,
ILogger<NotificationsApiPushNotificationService> logger)
{ {
_installationDeviceRepository = installationDeviceRepository; _installationDeviceRepository = installationDeviceRepository;
_globalSettings = globalSettings; _globalSettings = globalSettings;
_httpContextAccessor = httpContextAccessor; _httpContextAccessor = httpContextAccessor;
_client = NotificationHubClient.CreateClientFromConnectionString( _client = NotificationHubClient.CreateClientFromConnectionString(
_globalSettings.NotificationHub.ConnectionString, _globalSettings.NotificationHub.ConnectionString,
_globalSettings.NotificationHub.HubName); _globalSettings.NotificationHub.HubName,
_globalSettings.NotificationHub.EnableSendTracing);
_logger = logger;
} }
public async Task PushSyncCipherCreateAsync(Cipher cipher, IEnumerable<Guid> collectionIds) 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) private async Task SendPayloadAsync(string tag, PushType type, object payload)
{ {
await _client.SendTemplateNotificationAsync( var outcome = await _client.SendTemplateNotificationAsync(
new Dictionary<string, string> new Dictionary<string, string>
{ {
{ "type", ((byte)type).ToString() }, { "type", ((byte)type).ToString() },
{ "payload", JsonSerializer.Serialize(payload) } { "payload", JsonSerializer.Serialize(payload) }
}, tag); }, 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) private string SanitizeTagInput(string input)

View File

@ -412,6 +412,12 @@ public class GlobalSettings : IGlobalSettings
set => _connectionString = value.Trim('"'); set => _connectionString = value.Trim('"');
} }
public string HubName { get; set; } 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 public class YubicoSettings

View File

@ -23,7 +23,6 @@ public class DataProtectorTokenFactory<T> : IDataProtectorTokenFactory<T> where
/// Unprotect token /// Unprotect token
/// </summary> /// </summary>
/// <param name="token">The token to parse</param> /// <param name="token">The token to parse</param>
/// <typeparam name="T">The tokenable type to parse to</typeparam>
/// <returns>The parsed tokenable</returns> /// <returns>The parsed tokenable</returns>
/// <exception>Throws CryptographicException if fails to unprotect</exception> /// <exception>Throws CryptographicException if fails to unprotect</exception>
public T Unprotect(string token) => 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 _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 DateTime _max = new DateTime(9999, 1, 1, 0, 0, 0, DateTimeKind.Utc);
private static readonly Random _random = new Random(); private static readonly Random _random = new Random();
private static string _version;
private static readonly string CloudFlareConnectingIp = "CF-Connecting-IP"; private static readonly string CloudFlareConnectingIp = "CF-Connecting-IP";
private static readonly string RealIp = "X-Real-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.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.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.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.Cox, new List<string> { "cox.com", "cox.net", "coxbusiness.com" });
GlobalDomains.Add(GlobalEquivalentDomainsType.Norton, new List<string> { "mynortonaccount.com", "norton.com" }); GlobalDomains.Add(GlobalEquivalentDomainsType.Norton, new List<string> { "mynortonaccount.com", "norton.com" });
GlobalDomains.Add(GlobalEquivalentDomainsType.Verizon, new List<string> { "verizon.com", "verizon.net" }); GlobalDomains.Add(GlobalEquivalentDomainsType.Verizon, new List<string> { "verizon.com", "verizon.net" });

View File

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

View File

@ -113,7 +113,7 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
if (authRequest != null) if (authRequest != null)
{ {
var requestAge = DateTime.UtcNow - authRequest.CreationDate; 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)) CoreHelpers.FixedTimeEquals(authRequest.AccessCode, context.Password))
{ {
authRequest.AuthenticationDate = DateTime.UtcNow; authRequest.AuthenticationDate = DateTime.UtcNow;
@ -123,15 +123,13 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
} }
return false; return false;
} }
else
{
if (!await _userService.CheckPasswordAsync(validatorContext.User, context.Password)) if (!await _userService.CheckPasswordAsync(validatorContext.User, context.Password))
{ {
return false; return false;
} }
return true; return true;
} }
}
protected override Task SetSuccessResult(ResourceOwnerPasswordValidationContext context, User user, protected override Task SetSuccessResult(ResourceOwnerPasswordValidationContext context, User user,
List<Claim> claims, Dictionary<string, object> customResponse) List<Claim> claims, Dictionary<string, object> customResponse)

View File

@ -1,5 +1,6 @@
using Bit.Infrastructure.EntityFramework.Models; using Bit.Infrastructure.EntityFramework.Models;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Bit.Infrastructure.EntityFramework.Repositories; namespace Bit.Infrastructure.EntityFramework.Repositories;
@ -139,5 +140,29 @@ public class DatabaseContext : DbContext
eOrganizationApiKey.ToTable(nameof(OrganizationApiKey)); eOrganizationApiKey.ToTable(nameof(OrganizationApiKey));
eOrganizationConnection.ToTable(nameof(OrganizationConnection)); eOrganizationConnection.ToTable(nameof(OrganizationConnection));
eAuthRequest.ToTable(nameof(AuthRequest)); 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.HostedServices;
using Bit.Core.Identity; using Bit.Core.Identity;
using Bit.Core.IdentityServer; using Bit.Core.IdentityServer;
using Bit.Core.LoginFeatures;
using Bit.Core.Models.Business.Tokenables; using Bit.Core.Models.Business.Tokenables;
using Bit.Core.OrganizationFeatures; using Bit.Core.OrganizationFeatures;
using Bit.Core.Repositories; using Bit.Core.Repositories;
@ -108,6 +109,7 @@ public static class ServiceCollectionExtensions
services.AddSingleton<IAppleIapService, AppleIapService>(); services.AddSingleton<IAppleIapService, AppleIapService>();
services.AddScoped<ISsoConfigService, SsoConfigService>(); services.AddScoped<ISsoConfigService, SsoConfigService>();
services.AddScoped<ISendService, SendService>(); services.AddScoped<ISendService, SendService>();
services.AddLoginServices();
} }
public static void AddTokenizers(this IServiceCollection services) 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>(); var content = await response.Content.ReadFromJsonAsync<ProfileResponseModel>();
Assert.NotEmpty(content.Id); Assert.NotEmpty(content!.Id);
Assert.Equal("integration-test@bitwarden.com", content.Email); Assert.Equal("integration-test@bitwarden.com", content.Email);
Assert.Null(content.Name); Assert.Null(content.Name);
Assert.False(content.EmailVerified); Assert.False(content.EmailVerified);

View File

@ -71,7 +71,7 @@ public class FreshdeskControllerTests
return Send(request, cancellationToken); 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(); throw new NotImplementedException();
} }

View File

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

View File

@ -1,4 +1,5 @@
using Bit.Core.Utilities; using Bit.Core.Enums;
using Bit.Core.Utilities;
using Xunit; using Xunit;
namespace Bit.Core.Test.Utilities; namespace Bit.Core.Test.Utilities;
@ -10,6 +11,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 +35,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 +47,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();
@ -39,4 +70,46 @@ public class EncryptedStringAttributeTests
Assert.False(actual); 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] [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, CipherCompare equalityComparer, List<EfRepo.CipherRepository> suts, List<EfRepo.UserRepository> efUserRepos,
List<EfRepo.OrganizationRepository> efOrgRepos, SqlRepo.CipherRepository sqlCipherRepo, List<EfRepo.OrganizationRepository> efOrgRepos, SqlRepo.CipherRepository sqlCipherRepo,
SqlRepo.UserRepository sqlUserRepo, SqlRepo.OrganizationRepository sqlOrgRepo) => CreateAsync_Works_DataMatches( SqlRepo.UserRepository sqlUserRepo, SqlRepo.OrganizationRepository sqlOrgRepo) => CreateAsync_Works_DataMatches(
cipher, user, org, equalityComparer, suts, efUserRepos, efOrgRepos, sqlCipherRepo, sqlUserRepo, sqlOrgRepo); cipher, user, org, equalityComparer, suts, efUserRepos, efOrgRepos, sqlCipherRepo, sqlUserRepo, sqlOrgRepo);
[CiSkippedTheory, EfOrganizationCipherCustomize, BitAutoData] [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, CipherCompare equalityComparer, List<EfRepo.CipherRepository> suts, List<EfRepo.UserRepository> efUserRepos,
List<EfRepo.OrganizationRepository> efOrgRepos, SqlRepo.CipherRepository sqlCipherRepo, List<EfRepo.OrganizationRepository> efOrgRepos, SqlRepo.CipherRepository sqlCipherRepo,
SqlRepo.UserRepository sqlUserRepo, SqlRepo.OrganizationRepository sqlOrgRepo) => CreateAsync_Works_DataMatches( SqlRepo.UserRepository sqlUserRepo, SqlRepo.OrganizationRepository sqlOrgRepo) => CreateAsync_Works_DataMatches(
@ -177,7 +177,7 @@ public class CipherRepositoryTests
List<EfRepo.OrganizationRepository> efOrgRepos List<EfRepo.OrganizationRepository> efOrgRepos
) => await DeleteAsync_CipherIsDeleted(cipher, user, org, suts, efUserRepos, efOrgRepos); ) => await DeleteAsync_CipherIsDeleted(cipher, user, org, suts, efUserRepos, efOrgRepos);
[CiSkippedTheory, EfOrganizationCipherCustomize, BitAutoData] [CiSkippedTheory, EfOrganizationCipherCustomize, BitAutoData]
public async Task OrganizationCipher_DeleteAsync_CipherIsDeleted( public Task OrganizationCipher_DeleteAsync_CipherIsDeleted(
Cipher cipher, Cipher cipher,
User user, User user,
Organization org, Organization org,