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:
8
.github/ISSUE_TEMPLATE/bug.yml
vendored
8
.github/ISSUE_TEMPLATE/bug.yml
vendored
@ -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.
|
||||||
|
24
.github/workflows/version-bump.yml
vendored
24
.github/workflows/version-bump.yml
vendored
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -6,7 +6,23 @@
|
|||||||
# 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
|
||||||
|
|
||||||
docker run `
|
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)/helpers/mssql:/mnt/helpers" `
|
||||||
-v "$(pwd)/../util/Migrator:/mnt/migrator/" `
|
-v "$(pwd)/../util/Migrator:/mnt/migrator/" `
|
||||||
-v "$(pwd)/.data/mssql:/mnt/data" `
|
-v "$(pwd)/.data/mssql:/mnt/data" `
|
||||||
@ -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"
|
||||||
|
23
perf/MicroBenchmarks/Core/EncryptedStringAttributeTests.cs
Normal file
23
perf/MicroBenchmarks/Core/EncryptedStringAttributeTests.cs
Normal file
File diff suppressed because one or more lines are too long
18
perf/MicroBenchmarks/MicroBenchmarks.csproj
Normal file
18
perf/MicroBenchmarks/MicroBenchmarks.csproj
Normal 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>
|
4
perf/MicroBenchmarks/Program.cs
Normal file
4
perf/MicroBenchmarks/Program.cs
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using BenchmarkDotNet.Running;
|
||||||
|
|
||||||
|
BenchmarkRunner.Run(Assembly.GetEntryAssembly());
|
2668
perf/MicroBenchmarks/packages.lock.json
Normal file
2668
perf/MicroBenchmarks/packages.lock.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -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">
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
{
|
{
|
||||||
|
@ -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.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
@ -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,
|
||||||
|
14
src/Core/LoginFeatures/LoginServiceCollectionExtensions.cs
Normal file
14
src/Core/LoginFeatures/LoginServiceCollectionExtensions.cs
Normal 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>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
|||||||
|
namespace Bit.Core.LoginFeatures.PasswordlessLogin.Interfaces;
|
||||||
|
|
||||||
|
public interface IVerifyAuthRequestCommand
|
||||||
|
{
|
||||||
|
Task<bool> VerifyAuthRequestAsync(Guid authRequestId, string accessCode);
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
{
|
{
|
||||||
|
@ -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))
|
||||||
{
|
{
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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) =>
|
||||||
|
@ -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";
|
||||||
|
|
||||||
|
176
src/Core/Utilities/EncryptedStringAttribute.cs
Normal file
176
src/Core/Utilities/EncryptedStringAttribute.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
38
src/Core/Utilities/SpanExtensions.cs
Normal file
38
src/Core/Utilities/SpanExtensions.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -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" });
|
||||||
|
@ -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))
|
||||||
{
|
{
|
||||||
|
@ -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)
|
||||||
|
@ -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)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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);
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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!");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
52
test/Core.Test/Utilities/SpanExtensionsTests.cs
Normal file
52
test/Core.Test/Utilities/SpanExtensionsTests.cs
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
||||||
|
Reference in New Issue
Block a user