mirror of
https://github.com/bitwarden/server
synced 2026-01-29 15:53:36 +00:00
PM-2035: PRF Unlock (#6401)
* Initial refactor
* Add WebauthnPRFOptions to syncResponse
* MAYBE: Use KM owned ResponseModel?
* REVERT ^- Keep using PrfUnlockOptions for simplicity
This reverts commit 5a34e7dfa8.
* UserDecryptionOptions: Only send one credential
* format
* Update UserDecryptionOptions.cs
* format
* Added feature flag (#6600)
This commit is contained in:
@@ -6,6 +6,7 @@ using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
@@ -44,6 +45,7 @@ public class SyncController : Controller
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IApplicationCacheService _applicationCacheService;
|
||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||
private readonly IWebAuthnCredentialRepository _webAuthnCredentialRepository;
|
||||
private readonly IUserAccountKeysQuery _userAccountKeysQuery;
|
||||
|
||||
public SyncController(
|
||||
@@ -61,6 +63,7 @@ public class SyncController : Controller
|
||||
IFeatureService featureService,
|
||||
IApplicationCacheService applicationCacheService,
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||
IWebAuthnCredentialRepository webAuthnCredentialRepository,
|
||||
IUserAccountKeysQuery userAccountKeysQuery)
|
||||
{
|
||||
_userService = userService;
|
||||
@@ -77,6 +80,7 @@ public class SyncController : Controller
|
||||
_featureService = featureService;
|
||||
_applicationCacheService = applicationCacheService;
|
||||
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||
_webAuthnCredentialRepository = webAuthnCredentialRepository;
|
||||
_userAccountKeysQuery = userAccountKeysQuery;
|
||||
}
|
||||
|
||||
@@ -120,6 +124,9 @@ public class SyncController : Controller
|
||||
var organizationIdsClaimingActiveUser = organizationClaimingActiveUser.Select(o => o.Id);
|
||||
|
||||
var organizationAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
|
||||
var webAuthnCredentials = _featureService.IsEnabled(FeatureFlagKeys.PM2035PasskeyUnlock)
|
||||
? await _webAuthnCredentialRepository.GetManyByUserIdAsync(user.Id)
|
||||
: [];
|
||||
|
||||
UserAccountKeysData userAccountKeys = null;
|
||||
// JIT TDE users and some broken/old users may not have a private key.
|
||||
@@ -130,7 +137,7 @@ public class SyncController : Controller
|
||||
|
||||
var response = new SyncResponseModel(_globalSettings, user, userAccountKeys, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationAbilities,
|
||||
organizationIdsClaimingActiveUser, organizationUserDetails, providerUserDetails, providerUserOrganizationDetails,
|
||||
folders, collections, ciphers, collectionCiphersGroupDict, excludeDomains, policies, sends);
|
||||
folders, collections, ciphers, collectionCiphersGroupDict, excludeDomains, policies, sends, webAuthnCredentials);
|
||||
return response;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,9 @@ using Bit.Api.Models.Response;
|
||||
using Bit.Api.Tools.Models.Response;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Models.Data.Provider;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models.Api.Response;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.KeyManagement.Models.Api.Response;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
@@ -39,7 +42,8 @@ public class SyncResponseModel() : ResponseModel("sync")
|
||||
IDictionary<Guid, IGrouping<Guid, CollectionCipher>> collectionCiphersDict,
|
||||
bool excludeDomains,
|
||||
IEnumerable<Policy> policies,
|
||||
IEnumerable<Send> sends)
|
||||
IEnumerable<Send> sends,
|
||||
IEnumerable<WebAuthnCredential> webAuthnCredentials)
|
||||
: this()
|
||||
{
|
||||
Profile = new ProfileResponseModel(user, userAccountKeysData, organizationUserDetails, providerUserDetails,
|
||||
@@ -57,6 +61,16 @@ public class SyncResponseModel() : ResponseModel("sync")
|
||||
Domains = excludeDomains ? null : new DomainsResponseModel(user, false);
|
||||
Policies = policies?.Select(p => new PolicyResponseModel(p)) ?? new List<PolicyResponseModel>();
|
||||
Sends = sends.Select(s => new SendResponseModel(s));
|
||||
var webAuthnPrfOptions = webAuthnCredentials
|
||||
.Where(c => c.GetPrfStatus() == WebAuthnPrfStatus.Enabled)
|
||||
.Select(c => new WebAuthnPrfDecryptionOption(
|
||||
c.EncryptedPrivateKey,
|
||||
c.EncryptedUserKey,
|
||||
c.CredentialId,
|
||||
[] // transports as empty array
|
||||
))
|
||||
.ToArray();
|
||||
|
||||
UserDecryption = new UserDecryptionResponseModel
|
||||
{
|
||||
MasterPasswordUnlock = user.HasMasterPassword()
|
||||
@@ -72,7 +86,8 @@ public class SyncResponseModel() : ResponseModel("sync")
|
||||
MasterKeyEncryptedUserKey = user.Key!,
|
||||
Salt = user.Email.ToLowerInvariant()
|
||||
}
|
||||
: null
|
||||
: null,
|
||||
WebAuthnPrfOptions = webAuthnPrfOptions.Length > 0 ? webAuthnPrfOptions : null
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -45,13 +45,19 @@ public class WebAuthnPrfDecryptionOption
|
||||
{
|
||||
public string EncryptedPrivateKey { get; }
|
||||
public string EncryptedUserKey { get; }
|
||||
public string CredentialId { get; }
|
||||
public string[] Transports { get; }
|
||||
|
||||
public WebAuthnPrfDecryptionOption(
|
||||
string encryptedPrivateKey,
|
||||
string encryptedUserKey)
|
||||
string encryptedUserKey,
|
||||
string credentialId,
|
||||
string[]? transports = null)
|
||||
{
|
||||
EncryptedPrivateKey = encryptedPrivateKey;
|
||||
EncryptedUserKey = encryptedUserKey;
|
||||
CredentialId = credentialId;
|
||||
Transports = transports ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -160,6 +160,7 @@ public static class FeatureFlagKeys
|
||||
public const string PM24579_PreventSsoOnExistingNonCompliantUsers = "pm-24579-prevent-sso-on-existing-non-compliant-users";
|
||||
public const string DisableAlternateLoginMethods = "pm-22110-disable-alternate-login-methods";
|
||||
public const string MJMLBasedEmailTemplates = "mjml-based-email-templates";
|
||||
public const string PM2035PasskeyUnlock = "pm-2035-passkey-unlock";
|
||||
public const string MjmlWelcomeEmailTemplates = "pm-21741-mjml-welcome-email";
|
||||
public const string OrganizationConfirmationEmail = "pm-28402-update-confirmed-to-org-email-template";
|
||||
public const string MarketingInitiatedPremiumFlow = "pm-26140-marketing-initiated-premium-flow";
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
namespace Bit.Core.KeyManagement.Models.Api.Response;
|
||||
using System.Text.Json.Serialization;
|
||||
using Bit.Core.Auth.Models.Api.Response;
|
||||
|
||||
namespace Bit.Core.KeyManagement.Models.Api.Response;
|
||||
|
||||
public class UserDecryptionResponseModel
|
||||
{
|
||||
@@ -6,4 +9,10 @@ public class UserDecryptionResponseModel
|
||||
/// Returns the unlock data when the user has a master password that can be used to decrypt their vault.
|
||||
/// </summary>
|
||||
public MasterPasswordUnlockResponseModel? MasterPasswordUnlock { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the WebAuthn PRF decryption keys.
|
||||
/// </summary>
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public WebAuthnPrfDecryptionOption[]? WebAuthnPrfOptions { get; set; }
|
||||
}
|
||||
|
||||
@@ -64,8 +64,12 @@ public class UserDecryptionOptionsBuilder : IUserDecryptionOptionsBuilder
|
||||
{
|
||||
if (credential.GetPrfStatus() == WebAuthnPrfStatus.Enabled)
|
||||
{
|
||||
_options.WebAuthnPrfOption =
|
||||
new WebAuthnPrfDecryptionOption(credential.EncryptedPrivateKey, credential.EncryptedUserKey);
|
||||
_options.WebAuthnPrfOption = new WebAuthnPrfDecryptionOption(
|
||||
credential.EncryptedPrivateKey,
|
||||
credential.EncryptedUserKey,
|
||||
credential.CredentialId,
|
||||
[] // Stored credentials currently lack Transports, just send an empty array for now
|
||||
);
|
||||
}
|
||||
|
||||
return this;
|
||||
|
||||
@@ -60,6 +60,7 @@ public class UserDecryptionOptionsBuilderTests
|
||||
{
|
||||
Assert.NotNull(result.WebAuthnPrfOption);
|
||||
Assert.Equal(credential.EncryptedPrivateKey, result.WebAuthnPrfOption!.EncryptedPrivateKey);
|
||||
Assert.Equal(credential.CredentialId, result.WebAuthnPrfOption!.CredentialId);
|
||||
Assert.Equal(credential.EncryptedUserKey, result.WebAuthnPrfOption!.EncryptedUserKey);
|
||||
}
|
||||
else
|
||||
|
||||
Reference in New Issue
Block a user