[PM-3273][PM-4679] New owner/admin permission on login (#2837)

* [PM-3273] Add property for password set. Add labels. Update sync service.

* [PM-3273] Set password needs set in state. Read value on sync and nav to page.

* [PM-3273] Add navigation to Set Password on vault landing if needed.

* [PM-3273] Update SetPasswordPage copy

* [PM-3273] Add ManageResetPassword to Org Permissions, handle it on sync.

* [PM-3273] Change user has master password state when set master password is complete.

* [PM-3273] Code clean up

* [PM-3273] Remove unnecessary property from account profile

* [PM-3273] Add check for remembered org identifier

* [PM-4679] Added logging calls for future checks.

---------

Co-authored-by: Federico Maccaroni <fedemkr@gmail.com>
This commit is contained in:
André Bispo 2023-11-09 17:21:00 +00:00 committed by GitHub
parent 3a13ba4efa
commit 793c5fef6f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 203544 additions and 33 deletions

View file

@ -171,6 +171,11 @@ namespace Bit.App
new NavigationPage(new UpdateTempPasswordPage()));
});
}
else if (message.Command == Constants.ForceSetPassword)
{
await Device.InvokeOnMainThreadAsync(() => Application.Current.MainPage.Navigation.PushModalAsync(
new NavigationPage(new SetPasswordPage(orgIdentifier: (string)message.Data))));
}
else if (message.Command == "syncCompleted")
{
await _configService.GetAsync(true);

View file

@ -230,19 +230,18 @@ namespace Bit.App.Pages
StartDeviceApprovalOptionsAction?.Invoke();
return;
}
// If user doesn't have a MP, but has reset password permission, they must set a MP
if (!decryptOptions.HasMasterPassword &&
decryptOptions.TrustedDeviceOption.HasManageResetPasswordPermission)
{
StartSetPasswordAction?.Invoke();
return;
}
// Update temp password only if the device is trusted and therefore has a decrypted User Key set
if (response.ForcePasswordReset)
{
UpdateTempPasswordAction?.Invoke();
return;
}
// If user doesn't have a MP, but has reset password permission, they must set a MP
if (!decryptOptions.HasMasterPassword &&
decryptOptions.TrustedDeviceOption.HasManageResetPasswordPermission)
{
await _stateService.SetForcePasswordResetReasonAsync(ForcePasswordResetReason.TdeUserWithoutPasswordHasPasswordResetPermission);
}
// Device is trusted and has keys, so we can decrypt
_syncService.FullSyncAsync(true).FireAndForget();
SsoAuthSuccessAction?.Invoke();

View file

@ -28,7 +28,7 @@
<StackLayout Spacing="20">
<StackLayout StyleClass="box">
<StackLayout StyleClass="box-row">
<Label Text="{u:I18n SetMasterPasswordSummary}"
<Label Text="{Binding SetMasterPasswordSummary}"
StyleClass="text-md"
HorizontalTextAlignment="Start"></Label>
</StackLayout>

View file

@ -5,7 +5,6 @@ using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Bit.App.Abstractions;
using Bit.App.Resources;
using Bit.App.Utilities;
using Bit.Core;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
@ -28,6 +27,7 @@ namespace Bit.App.Pages
private readonly IPolicyService _policyService;
private readonly IPasswordGenerationService _passwordGenerationService;
private readonly II18nService _i18nService;
private readonly ISyncService _syncService;
private bool _showPassword;
private bool _isPolicyInEffect;
@ -46,6 +46,7 @@ namespace Bit.App.Pages
_passwordGenerationService =
ServiceContainer.Resolve<IPasswordGenerationService>("passwordGenerationService");
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService");
_syncService = ServiceContainer.Resolve<ISyncService>();
PageTitle = AppResources.SetMasterPassword;
TogglePasswordCommand = new Command(TogglePassword);
@ -100,11 +101,17 @@ namespace Bit.App.Pages
public Action CloseAction { get; set; }
public string OrgIdentifier { get; set; }
public string OrgId { get; set; }
public ForcePasswordResetReason? ForceSetPasswordReason { get; private set; }
public string SetMasterPasswordSummary => ForceSetPasswordReason == ForcePasswordResetReason.TdeUserWithoutPasswordHasPasswordResetPermission
? AppResources.YourOrganizationPermissionsWereUpdatedRequeringYouToSetAMasterPassword
: AppResources.YourOrganizationRequiresYouToSetAMasterPassword;
public async Task InitAsync()
{
await CheckPasswordPolicy();
ForceSetPasswordReason = await _stateService.GetForcePasswordResetReasonAsync();
TriggerPropertyChanged(nameof(SetMasterPasswordSummary));
try
{
var response = await _apiService.GetOrganizationAutoEnrollStatusAsync(OrgIdentifier);
@ -171,8 +178,7 @@ namespace Bit.App.Pages
var (newUserKey, newProtectedUserKey) = await _cryptoService.EncryptUserKeyWithMasterKeyAsync(newMasterKey,
await _cryptoService.GetUserKeyAsync() ?? await _cryptoService.MakeUserKeyAsync());
var (newPublicKey, newProtectedPrivateKey) = await _cryptoService.MakeKeyPairAsync(newUserKey);
var keysRequest = await GetKeysForSetPasswordRequestAsync(newUserKey);
var request = new SetPasswordRequest
{
MasterPasswordHash = masterPasswordHash,
@ -183,16 +189,12 @@ namespace Bit.App.Pages
KdfMemory = kdfConfig.Memory,
KdfParallelism = kdfConfig.Parallelism,
OrgIdentifier = OrgIdentifier,
Keys = new KeysRequest
{
PublicKey = newPublicKey,
EncryptedPrivateKey = newProtectedPrivateKey.EncryptedString
}
Keys = keysRequest
};
try
{
await _deviceActionService.ShowLoadingAsync(AppResources.CreatingAccount);
await _deviceActionService.ShowLoadingAsync(AppResources.Loading);
// Set Password and relevant information
await _apiService.SetPasswordAsync(request);
await _stateService.SetKdfConfigurationAsync(kdfConfig);
@ -200,7 +202,13 @@ namespace Bit.App.Pages
await _cryptoService.SetMasterKeyAsync(newMasterKey);
await _cryptoService.SetMasterKeyHashAsync(localMasterPasswordHash);
await _cryptoService.SetMasterKeyEncryptedUserKeyAsync(newProtectedUserKey.EncryptedString);
await _cryptoService.SetUserPrivateKeyAsync(newProtectedPrivateKey.EncryptedString);
// Set private key only for new JIT provisioned users in MP encryption orgs
// Existing TDE users will have private key set on sync or on login
if (keysRequest != null)
{
await _cryptoService.SetUserPrivateKeyAsync(keysRequest.EncryptedPrivateKey);
}
if (ResetPasswordAutoEnroll)
{
@ -221,6 +229,9 @@ namespace Bit.App.Pages
await _apiService.PutOrganizationUserResetPasswordEnrollmentAsync(OrgId, userId, resetRequest);
}
await _stateService.SetForcePasswordResetReasonAsync(null);
await _stateService.SetUserHasMasterPasswordAsync(true);
await _syncService.FullSyncAsync(true);
await _deviceActionService.HideLoadingAsync();
SetPasswordSuccessAction?.Invoke();
}
@ -235,6 +246,21 @@ namespace Bit.App.Pages
}
}
private async Task<KeysRequest> GetKeysForSetPasswordRequestAsync(UserKey newUserKey)
{
if (ForceSetPasswordReason == ForcePasswordResetReason.TdeUserWithoutPasswordHasPasswordResetPermission)
{
return null;
}
var (newPublicKey, newProtectedPrivateKey) = await _cryptoService.MakeKeyPairAsync(newUserKey);
return new KeysRequest
{
PublicKey = newPublicKey,
EncryptedPrivateKey = newProtectedPrivateKey.EncryptedString
};
}
public void TogglePassword()
{
ShowPassword = !ShowPassword;

View file

@ -10,6 +10,7 @@ using Bit.App.Utilities;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Domain;
using Bit.Core.Models.Request;
using Bit.Core.Services;
using Bit.Core.Utilities;
@ -335,20 +336,18 @@ namespace Bit.App.Pages
StartDeviceApprovalOptionsAction?.Invoke();
return;
}
// If user doesn't have a MP, but has reset password permission, they must set a MP
if (!decryptOptions.HasMasterPassword &&
decryptOptions.TrustedDeviceOption.HasManageResetPasswordPermission)
{
StartSetPasswordAction?.Invoke();
return;
}
// Update temp password only if the device is trusted and therefore has a decrypted User Key set
if (result.ForcePasswordReset)
{
UpdateTempPasswordAction?.Invoke();
return;
}
// If user doesn't have a MP, but has reset password permission, they must set a MP
if (!decryptOptions.HasMasterPassword &&
decryptOptions.TrustedDeviceOption.HasManageResetPasswordPermission)
{
await _stateService.SetForcePasswordResetReasonAsync(ForcePasswordResetReason.TdeUserWithoutPasswordHasPasswordResetPermission);
}
// Device is trusted and has keys, so we can decrypt
_syncService.FullSyncAsync(true).FireAndForget();
await TwoFactorAuthSuccessAsync();

View file

@ -1,4 +1,5 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Bit.App.Effects;
using Bit.App.Models;
@ -7,6 +8,7 @@ using Bit.App.Utilities;
using Bit.Core;
using Bit.Core.Abstractions;
using Bit.Core.Models.Data;
using Bit.Core.Models.Domain;
using Bit.Core.Utilities;
using Xamarin.Forms;
@ -100,11 +102,38 @@ namespace Bit.App.Pages
_messagingService.Send("convertAccountToKeyConnector");
}
var forcePasswordResetReason = await _stateService.GetForcePasswordResetReasonAsync();
await ForcePasswordResetIfNeededAsync();
}
if (forcePasswordResetReason.HasValue)
private async Task ForcePasswordResetIfNeededAsync()
{
var forcePasswordResetReason = await _stateService.GetForcePasswordResetReasonAsync();
switch (forcePasswordResetReason)
{
case ForcePasswordResetReason.TdeUserWithoutPasswordHasPasswordResetPermission:
// TDE users should only have one org
var userOrgs = await _stateService.GetOrganizationsAsync();
if (userOrgs != null && userOrgs.Any())
{
_messagingService.Send(Constants.ForceSetPassword, userOrgs.First().Value.Identifier);
return;
}
_logger.Value.Error("TDE user needs to set password but has no organizations.");
var rememberedOrg = _stateService.GetRememberedOrgIdentifierAsync();
if (rememberedOrg == null)
{
_logger.Value.Error("TDE user needs to set password but has no organizations or remembered org identifier.");
return;
}
_messagingService.Send(Constants.ForceSetPassword, rememberedOrg);
return;
case ForcePasswordResetReason.AdminForcePasswordReset:
case ForcePasswordResetReason.WeakMasterPasswordOnLogin:
_messagingService.Send(Constants.ForceUpdatePassword);
break;
default:
return;
}
}

View file

@ -7775,6 +7775,24 @@ namespace Bit.App.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Your organization permissions were updated, requiring you to set a master password..
/// </summary>
public static string YourOrganizationPermissionsWereUpdatedRequeringYouToSetAMasterPassword {
get {
return ResourceManager.GetString("YourOrganizationPermissionsWereUpdatedRequeringYouToSetAMasterPassword", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Your organization requires you to set a master password..
/// </summary>
public static string YourOrganizationRequiresYouToSetAMasterPassword {
get {
return ResourceManager.GetString("YourOrganizationRequiresYouToSetAMasterPassword", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Your request has been sent to your admin..
/// </summary>

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -2862,6 +2862,12 @@ Do you want to switch to this account?</value>
<data name="AccountLoggedOutBiometricExceeded" xml:space="preserve">
<value>Account logged out.</value>
</data>
<data name="YourOrganizationPermissionsWereUpdatedRequeringYouToSetAMasterPassword" xml:space="preserve">
<value>Your organization permissions were updated, requiring you to set a master password.</value>
</data>
<data name="YourOrganizationRequiresYouToSetAMasterPassword" xml:space="preserve">
<value>Your organization requires you to set a master password.</value>
</data>
<data name="SetUpAnUnlockOptionToChangeYourVaultTimeoutAction" xml:space="preserve">
<value>Set up an unlock option to change your vault timeout action.</value>
</data>

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -185,10 +185,10 @@ namespace Bit.Core.Abstractions
void SetConfigs(ConfigResponse value);
Task<bool> GetShouldTrustDeviceAsync();
Task SetShouldTrustDeviceAsync(bool value);
Task SetUserHasMasterPasswordAsync(bool value, string userId = null);
Task<Region?> GetActiveUserRegionAsync();
Task<Region?> GetPreAuthRegionAsync();
Task SetPreAuthRegionAsync(Region value);
[Obsolete("Use GetPinKeyEncryptedUserKeyAsync instead, left for migration purposes")]
Task<string> GetPinProtectedAsync(string userId = null);
[Obsolete("Use SetPinKeyEncryptedUserKeyAsync instead, left for migration purposes")]

View file

@ -57,6 +57,7 @@ namespace Bit.Core
public const string AppLocaleKey = "appLocale";
public const string ClearSensitiveFields = "clearSensitiveFields";
public const string ForceUpdatePassword = "forceUpdatePassword";
public const string ForceSetPassword = "forceSetPassword";
public const string ShouldTrustDevice = "shouldTrustDevice";
public const int SelectFileRequestCode = 42;
public const int SelectFilePermissionRequestCode = 43;

View file

@ -15,5 +15,6 @@
public bool ManagePolicies { get; set; }
public bool ManageSso { get; set; }
public bool ManageUsers { get; set; }
public bool ManageResetPassword { get; set; }
}
}

View file

@ -11,6 +11,12 @@
/// Occurs when a user logs in with a master password that does not meet an organization's master password
/// policy that is enforced on login.
/// </summary>
WeakMasterPasswordOnLogin
WeakMasterPasswordOnLogin,
/// <summary>
/// Occurs when a TDE user without a password obtains the password reset permission.
/// Set post login & decryption client side and by server in sync (to catch logged in users).
/// </summary>
TdeUserWithoutPasswordHasPasswordResetPermission,
}
}

View file

@ -7,7 +7,7 @@ namespace Bit.Core.Models.Request
public string MasterPasswordHash { get; set; }
public string Key { get; set; }
public string MasterPasswordHint { get; set; }
public KeysRequest Keys { get; set; }
public KeysRequest? Keys { get; set; }
public KdfType Kdf { get; set; }
public int KdfIterations { get; set; }
public int? KdfMemory { get; set; }

View file

@ -20,5 +20,6 @@ namespace Bit.Core.Models.Response
public List<ProfileOrganizationResponse> Organizations { get; set; }
public bool UsesKeyConnector { get; set; }
public string AvatarColor { get; set; }
public bool HasManageResetPasswordPermission { get; set; }
}
}

View file

@ -1363,6 +1363,15 @@ namespace Bit.Core.Services
_storageMediatorService.Save(Constants.ConfigsKey, value);
}
public async Task SetUserHasMasterPasswordAsync(bool value, string userId = null)
{
var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId },
await GetDefaultStorageOptionsAsync());
var account = await GetAccountAsync(reconciledOptions);
account.Profile.UserDecryptionOptions.HasMasterPassword = value;
await SaveAccountAsync(account, reconciledOptions);
}
public async Task<Region?> GetActiveUserRegionAsync()
{
return await GetActiveUserCustomDataAsync(a => a?.Settings?.Region);

View file

@ -5,6 +5,7 @@ using System.Threading.Tasks;
using Bit.Core.Abstractions;
using Bit.Core.Exceptions;
using Bit.Core.Models.Data;
using Bit.Core.Models.Domain;
using Bit.Core.Models.Response;
using Bit.Core.Utilities;
@ -398,6 +399,33 @@ namespace Bit.Core.Services
await _stateService.SetPersonalPremiumAsync(response.Premium);
await _stateService.SetAvatarColorAsync(response.AvatarColor);
await _keyConnectorService.SetUsesKeyConnectorAsync(response.UsesKeyConnector);
await SetPasswordSetReasonIfNeededAsync(response);
}
private async Task SetPasswordSetReasonIfNeededAsync(ProfileResponse response)
{
// The `ForcePasswordReset` flag indicates an admin has reset the user's password and must be updated
if (response.ForcePasswordReset)
{
await _stateService.SetForcePasswordResetReasonAsync(ForcePasswordResetReason.AdminForcePasswordReset);
}
var hasManageResetPasswordPermission = response.Organizations.Any(org =>
org.Type == Enums.OrganizationUserType.Owner ||
org.Type == Enums.OrganizationUserType.Admin ||
org.Permissions?.ManageResetPassword == true);
if (!hasManageResetPasswordPermission)
{
return;
}
var decryptionOptions = await _stateService.GetAccountDecryptionOptions();
if (decryptionOptions?.HasMasterPassword == false)
{
// TDE user w/out MP went from having no password reset permission to having it.
// Must set the force password reset reason so the auth guard will redirect to the set password page.
await _stateService.SetForcePasswordResetReasonAsync(ForcePasswordResetReason.TdeUserWithoutPasswordHasPasswordResetPermission);
}
}
private async Task SyncFoldersAsync(string userId, List<FolderResponse> response)