diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 3191d515732..5e06fa5de84 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -6,6 +6,8 @@ # Unless a later match takes precedence # @bitwarden/tech-leads +@bitwarden/dept-development-mobile + ## Auth team files ## ## Platform team files ## diff --git a/src/Android/Autofill/AutofillHelpers.cs b/src/Android/Autofill/AutofillHelpers.cs index 3d12f8c97a1..86d6cc82822 100644 --- a/src/Android/Autofill/AutofillHelpers.cs +++ b/src/Android/Autofill/AutofillHelpers.cs @@ -20,6 +20,7 @@ using Bit.Core.Abstractions; using SaveFlags = Android.Service.Autofill.SaveFlags; using Bit.Droid.Utilities; +using Bit.Core.Services; namespace Bit.Droid.Autofill { @@ -152,8 +153,9 @@ public static class AutofillHelpers "androidapp://com.oneplus.applocker", }; - public static async Task> GetFillItemsAsync(Parser parser, ICipherService cipherService) + public static async Task> GetFillItemsAsync(Parser parser, ICipherService cipherService, IUserVerificationService userVerificationService) { + var userHasMasterPassword = await userVerificationService.HasMasterPasswordAsync(); if (parser.FieldCollection.FillableForLogin) { var ciphers = await cipherService.GetAllDecryptedByUrlAsync(parser.Uri); @@ -161,14 +163,14 @@ public static async Task> GetFillItemsAsync(Parser parser, ICip { var allCiphers = ciphers.Item1.ToList(); allCiphers.AddRange(ciphers.Item2.ToList()); - var nonPromptCiphers = allCiphers.Where(cipher => cipher.Reprompt == CipherRepromptType.None); + var nonPromptCiphers = allCiphers.Where(cipher => !userHasMasterPassword || cipher.Reprompt == CipherRepromptType.None); return nonPromptCiphers.Select(c => new FilledItem(c)).ToList(); } } else if (parser.FieldCollection.FillableForCard) { var ciphers = await cipherService.GetAllDecryptedAsync(); - return ciphers.Where(c => c.Type == CipherType.Card && c.Reprompt == CipherRepromptType.None).Select(c => new FilledItem(c)).ToList(); + return ciphers.Where(c => c.Type == CipherType.Card && (!userHasMasterPassword || c.Reprompt == CipherRepromptType.None)).Select(c => new FilledItem(c)).ToList(); } return new List(); } diff --git a/src/Android/Autofill/AutofillService.cs b/src/Android/Autofill/AutofillService.cs index 3becfe4ac03..15e384839cb 100644 --- a/src/Android/Autofill/AutofillService.cs +++ b/src/Android/Autofill/AutofillService.cs @@ -11,6 +11,7 @@ using Bit.Core; using Bit.Core.Abstractions; using Bit.Core.Enums; +using Bit.Core.Services; using Bit.Core.Utilities; namespace Bit.Droid.Autofill @@ -26,6 +27,7 @@ public class AutofillService : Android.Service.Autofill.AutofillService private IPolicyService _policyService; private IStateService _stateService; private LazyResolve _logger = new LazyResolve("logger"); + private IUserVerificationService _userVerificationService; public async override void OnFillRequest(FillRequest request, CancellationSignal cancellationSignal, FillCallback callback) @@ -64,11 +66,9 @@ public async override void OnFillRequest(FillRequest request, CancellationSignal var locked = await _vaultTimeoutService.IsLockedAsync(); if (!locked) { - if (_cipherService == null) - { - _cipherService = ServiceContainer.Resolve("cipherService"); - } - items = await AutofillHelpers.GetFillItemsAsync(parser, _cipherService); + _cipherService ??= ServiceContainer.Resolve(); + _userVerificationService ??= ServiceContainer.Resolve(); + items = await AutofillHelpers.GetFillItemsAsync(parser, _cipherService, _userVerificationService); } // build response diff --git a/src/Android/Properties/AndroidManifest.xml b/src/Android/Properties/AndroidManifest.xml index 2c688b83d31..62208bf8b0e 100644 --- a/src/Android/Properties/AndroidManifest.xml +++ b/src/Android/Properties/AndroidManifest.xml @@ -1,5 +1,5 @@  - + diff --git a/src/App/Controls/AccountViewCell/AccountViewCellViewModel.cs b/src/App/Controls/AccountViewCell/AccountViewCellViewModel.cs index 45e2f24552c..2cc33d54a01 100644 --- a/src/App/Controls/AccountViewCell/AccountViewCellViewModel.cs +++ b/src/App/Controls/AccountViewCell/AccountViewCellViewModel.cs @@ -36,7 +36,7 @@ public bool IsAccount public bool ShowHostname { - get => !string.IsNullOrWhiteSpace(AccountView.Hostname) && AccountView.Hostname != "vault.bitwarden.com"; + get => !string.IsNullOrWhiteSpace(AccountView.Hostname); } public bool IsActive diff --git a/src/App/Pages/Accounts/LockPageViewModel.cs b/src/App/Pages/Accounts/LockPageViewModel.cs index 3e6946c6ae5..c5aaf1ab769 100644 --- a/src/App/Pages/Accounts/LockPageViewModel.cs +++ b/src/App/Pages/Accounts/LockPageViewModel.cs @@ -7,6 +7,7 @@ using Bit.Core; 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; @@ -72,11 +73,12 @@ public LockPageViewModel() TogglePasswordCommand = new Command(TogglePassword); SubmitCommand = new Command(async () => await SubmitAsync()); - AccountSwitchingOverlayViewModel = new AccountSwitchingOverlayViewModel(_stateService, _messagingService, _logger) - { - AllowAddAccountRow = true, - AllowActiveAccountSelection = true - }; + AccountSwitchingOverlayViewModel = + new AccountSwitchingOverlayViewModel(_stateService, _messagingService, _logger) + { + AllowAddAccountRow = true, + AllowActiveAccountSelection = true + }; } public string MasterPassword @@ -155,8 +157,12 @@ public string LockedVerifyText public Command SubmitCommand { get; } public Command TogglePasswordCommand { get; } + public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye; - public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow; + public string PasswordVisibilityAccessibilityText => ShowPassword + ? AppResources.PasswordIsVisibleTapToHide + : AppResources.PasswordIsNotVisibleTapToShow; + public Action UnlockedAction { get; set; } public event Action FocusSecretEntry { @@ -178,8 +184,9 @@ public async Task InitAsync() var ephemeralPinSet = await _stateService.GetPinKeyEncryptedUserKeyEphemeralAsync() ?? await _stateService.GetPinProtectedKeyAsync(); PinEnabled = (_pinStatus == PinLockType.Transient && ephemeralPinSet != null) || - _pinStatus == PinLockType.Persistent; - BiometricEnabled = await _vaultTimeoutService.IsBiometricLockSetAsync() && await _biometricService.CanUseBiometricsUnlockAsync(); + _pinStatus == PinLockType.Persistent; + + BiometricEnabled = await IsBiometricsEnabledAsync(); // Users without MP and without biometric or pin has no MP to unlock with _hasMasterPassword = await _userVerificationService.HasMasterPasswordAsync(); @@ -214,7 +221,9 @@ public async Task InitAsync() else { PageTitle = _hasMasterPassword ? AppResources.VerifyMasterPassword : AppResources.UnlockVault; - LockedVerifyText = _hasMasterPassword ? AppResources.VaultLockedMasterPassword : AppResources.VaultLockedIdentity; + LockedVerifyText = _hasMasterPassword + ? AppResources.VaultLockedMasterPassword + : AppResources.VaultLockedIdentity; } if (BiometricEnabled) @@ -233,163 +242,198 @@ public async Task InitAsync() BiometricButtonText = supportsFace ? AppResources.UseFaceIDToUnlock : AppResources.UseFingerprintToUnlock; } - } } public async Task SubmitAsync() { - if (PinEnabled && string.IsNullOrWhiteSpace(Pin)) + ShowPassword = false; + try { - await Page.DisplayAlert(AppResources.AnErrorHasOccurred, - string.Format(AppResources.ValidationFieldRequired, AppResources.PIN), - AppResources.Ok); - return; + var kdfConfig = await _stateService.GetActiveUserCustomDataAsync(a => new KdfConfig(a?.Profile)); + if (PinEnabled) + { + await UnlockWithPinAsync(kdfConfig); + } + else + { + await UnlockWithMasterPasswordAsync(kdfConfig); + } + } - if (!PinEnabled && string.IsNullOrWhiteSpace(MasterPassword)) + catch (LegacyUserException) + { + await HandleLegacyUserAsync(); + } + } + + private async Task UnlockWithPinAsync(KdfConfig kdfConfig) + { + if (PinEnabled && string.IsNullOrWhiteSpace(Pin)) { await Page.DisplayAlert(AppResources.AnErrorHasOccurred, - string.Format(AppResources.ValidationFieldRequired, AppResources.MasterPassword), + string.Format(AppResources.ValidationFieldRequired, AppResources.PIN), AppResources.Ok); return; } - ShowPassword = false; - var kdfConfig = await _stateService.GetActiveUserCustomDataAsync(a => new KdfConfig(a?.Profile)); - - if (PinEnabled) + var failed = true; + try { - var failed = true; - try + EncString userKeyPin; + EncString oldPinProtected; + switch (_pinStatus) { - EncString userKeyPin = null; - EncString oldPinProtected = null; - if (_pinStatus == PinLockType.Persistent) - { - userKeyPin = await _stateService.GetPinKeyEncryptedUserKeyAsync(); - var oldEncryptedKey = await _stateService.GetPinProtectedAsync(); - oldPinProtected = oldEncryptedKey != null ? new EncString(oldEncryptedKey) : null; - } - else if (_pinStatus == PinLockType.Transient) - { + case PinLockType.Persistent: + { + userKeyPin = await _stateService.GetPinKeyEncryptedUserKeyAsync(); + var oldEncryptedKey = await _stateService.GetPinProtectedAsync(); + oldPinProtected = oldEncryptedKey != null ? new EncString(oldEncryptedKey) : null; + break; + } + case PinLockType.Transient: userKeyPin = await _stateService.GetPinKeyEncryptedUserKeyEphemeralAsync(); oldPinProtected = await _stateService.GetPinProtectedKeyAsync(); - } - - UserKey userKey; - if (oldPinProtected != null) - { - userKey = await _cryptoService.DecryptAndMigrateOldPinKeyAsync( - _pinStatus == PinLockType.Transient, - Pin, - _email, - kdfConfig, - oldPinProtected - ); - } - else - { - userKey = await _cryptoService.DecryptUserKeyWithPinAsync( - Pin, - _email, - kdfConfig, - userKeyPin - ); - } - - var protectedPin = await _stateService.GetProtectedPinAsync(); - var decryptedPin = await _cryptoService.DecryptToUtf8Async(new EncString(protectedPin), userKey); - failed = decryptedPin != Pin; - if (!failed) - { - Pin = string.Empty; - await AppHelpers.ResetInvalidUnlockAttemptsAsync(); - await SetUserKeyAndContinueAsync(userKey); - } + break; + case PinLockType.Disabled: + default: + throw new Exception("Pin is disabled"); + } + + UserKey userKey; + if (oldPinProtected != null) + { + userKey = await _cryptoService.DecryptAndMigrateOldPinKeyAsync( + _pinStatus == PinLockType.Transient, + Pin, + _email, + kdfConfig, + oldPinProtected + ); } - catch + else { - failed = true; + userKey = await _cryptoService.DecryptUserKeyWithPinAsync( + Pin, + _email, + kdfConfig, + userKeyPin + ); } - if (failed) + + var protectedPin = await _stateService.GetProtectedPinAsync(); + var decryptedPin = await _cryptoService.DecryptToUtf8Async(new EncString(protectedPin), userKey); + failed = decryptedPin != Pin; + if (!failed) { - var invalidUnlockAttempts = await AppHelpers.IncrementInvalidUnlockAttemptsAsync(); - if (invalidUnlockAttempts >= 5) - { - _messagingService.Send("logout"); - return; - } - await _platformUtilsService.ShowDialogAsync(AppResources.InvalidPIN, - AppResources.AnErrorHasOccurred); + Pin = string.Empty; + await AppHelpers.ResetInvalidUnlockAttemptsAsync(); + await SetUserKeyAndContinueAsync(userKey); } } + catch (LegacyUserException) + { + throw; + } + catch + { + failed = true; + } + if (failed) + { + var invalidUnlockAttempts = await AppHelpers.IncrementInvalidUnlockAttemptsAsync(); + if (invalidUnlockAttempts >= 5) + { + _messagingService.Send("logout"); + return; + } + await _platformUtilsService.ShowDialogAsync(AppResources.InvalidPIN, + AppResources.AnErrorHasOccurred); + } + } + + private async Task UnlockWithMasterPasswordAsync(KdfConfig kdfConfig) + { + if (!PinEnabled && string.IsNullOrWhiteSpace(MasterPassword)) + { + await Page.DisplayAlert(AppResources.AnErrorHasOccurred, + string.Format(AppResources.ValidationFieldRequired, AppResources.MasterPassword), + AppResources.Ok); + return; + } + + var masterKey = await _cryptoService.MakeMasterKeyAsync(MasterPassword, _email, kdfConfig); + if (await _cryptoService.IsLegacyUserAsync(masterKey)) + { + throw new LegacyUserException(); + } + + var storedKeyHash = await _cryptoService.GetMasterKeyHashAsync(); + var passwordValid = false; + MasterPasswordPolicyOptions enforcedMasterPasswordOptions = null; + + if (storedKeyHash != null) + { + // Offline unlock possible + passwordValid = await _cryptoService.CompareAndUpdateKeyHashAsync(MasterPassword, masterKey); + } else { - var masterKey = await _cryptoService.MakeMasterKeyAsync(MasterPassword, _email, kdfConfig); - var storedKeyHash = await _cryptoService.GetMasterKeyHashAsync(); - var passwordValid = false; - MasterPasswordPolicyOptions enforcedMasterPasswordOptions = null; + // Online unlock required + await _deviceActionService.ShowLoadingAsync(AppResources.Loading); + var keyHash = await _cryptoService.HashMasterKeyAsync(MasterPassword, masterKey, + HashPurpose.ServerAuthorization); + var request = new PasswordVerificationRequest(); + request.MasterPasswordHash = keyHash; - if (storedKeyHash != null) + try { - // Offline unlock possible - passwordValid = await _cryptoService.CompareAndUpdateKeyHashAsync(MasterPassword, masterKey); + var response = await _apiService.PostAccountVerifyPasswordAsync(request); + enforcedMasterPasswordOptions = response.MasterPasswordPolicy; + passwordValid = true; + var localKeyHash = await _cryptoService.HashMasterKeyAsync(MasterPassword, masterKey, + HashPurpose.LocalAuthorization); + await _cryptoService.SetMasterKeyHashAsync(localKeyHash); } - else + catch (Exception e) { - // Online unlock required - await _deviceActionService.ShowLoadingAsync(AppResources.Loading); - var keyHash = await _cryptoService.HashMasterKeyAsync(MasterPassword, masterKey, HashPurpose.ServerAuthorization); - var request = new PasswordVerificationRequest(); - request.MasterPasswordHash = keyHash; - - try - { - var response = await _apiService.PostAccountVerifyPasswordAsync(request); - enforcedMasterPasswordOptions = response.MasterPasswordPolicy; - passwordValid = true; - var localKeyHash = await _cryptoService.HashMasterKeyAsync(MasterPassword, masterKey, HashPurpose.LocalAuthorization); - await _cryptoService.SetMasterKeyHashAsync(localKeyHash); - } - catch (Exception e) - { - System.Diagnostics.Debug.WriteLine(">>> {0}: {1}", e.GetType(), e.StackTrace); - } - await _deviceActionService.HideLoadingAsync(); + System.Diagnostics.Debug.WriteLine(">>> {0}: {1}", e.GetType(), e.StackTrace); } - if (passwordValid) + await _deviceActionService.HideLoadingAsync(); + } + + if (passwordValid) + { + if (await RequirePasswordChangeAsync(enforcedMasterPasswordOptions)) { - if (await RequirePasswordChangeAsync(enforcedMasterPasswordOptions)) - { - // Save the ForcePasswordResetReason to force a password reset after unlock - await _stateService.SetForcePasswordResetReasonAsync( - ForcePasswordResetReason.WeakMasterPasswordOnLogin); - } - - MasterPassword = string.Empty; - await AppHelpers.ResetInvalidUnlockAttemptsAsync(); + // Save the ForcePasswordResetReason to force a password reset after unlock + await _stateService.SetForcePasswordResetReasonAsync( + ForcePasswordResetReason.WeakMasterPasswordOnLogin); + } - var userKey = await _cryptoService.DecryptUserKeyWithMasterKeyAsync(masterKey); - await _cryptoService.SetMasterKeyAsync(masterKey); - await SetUserKeyAndContinueAsync(userKey); + MasterPassword = string.Empty; + await AppHelpers.ResetInvalidUnlockAttemptsAsync(); - // Re-enable biometrics - if (BiometricEnabled & !BiometricIntegrityValid) - { - await _biometricService.SetupBiometricAsync(); - } + var userKey = await _cryptoService.DecryptUserKeyWithMasterKeyAsync(masterKey); + await _cryptoService.SetMasterKeyAsync(masterKey); + await SetUserKeyAndContinueAsync(userKey); + + // Re-enable biometrics + if (BiometricEnabled & !BiometricIntegrityValid) + { + await _biometricService.SetupBiometricAsync(); } - else + } + else + { + var invalidUnlockAttempts = await AppHelpers.IncrementInvalidUnlockAttemptsAsync(); + if (invalidUnlockAttempts >= 5) { - var invalidUnlockAttempts = await AppHelpers.IncrementInvalidUnlockAttemptsAsync(); - if (invalidUnlockAttempts >= 5) - { - _messagingService.Send("logout"); - return; - } - await _platformUtilsService.ShowDialogAsync(AppResources.InvalidMasterPassword, - AppResources.AnErrorHasOccurred); + _messagingService.Send("logout"); + return; } + await _platformUtilsService.ShowDialogAsync(AppResources.InvalidMasterPassword, + AppResources.AnErrorHasOccurred); } } @@ -452,25 +496,36 @@ public void TogglePassword() { ShowPassword = !ShowPassword; var secret = PinEnabled ? Pin : MasterPassword; - _secretEntryFocusWeakEventManager.RaiseEvent(string.IsNullOrEmpty(secret) ? 0 : secret.Length, nameof(FocusSecretEntry)); + _secretEntryFocusWeakEventManager.RaiseEvent(string.IsNullOrEmpty(secret) ? 0 : secret.Length, + nameof(FocusSecretEntry)); } public async Task PromptBiometricAsync() { - BiometricIntegrityValid = await _platformUtilsService.IsBiometricIntegrityValidAsync(); - BiometricButtonVisible = BiometricIntegrityValid; - if (!BiometricEnabled || !BiometricIntegrityValid) + try { - return; + BiometricIntegrityValid = await _platformUtilsService.IsBiometricIntegrityValidAsync(); + BiometricButtonVisible = BiometricIntegrityValid; + if (!BiometricEnabled || !BiometricIntegrityValid) + { + return; + } + + var success = await _platformUtilsService.AuthenticateBiometricAsync(null, + PinEnabled ? AppResources.PIN : AppResources.MasterPassword, + () => _secretEntryFocusWeakEventManager.RaiseEvent((int?)null, nameof(FocusSecretEntry)), + !PinEnabled && !HasMasterPassword); + + await _stateService.SetBiometricLockedAsync(!success); + if (success) + { + var userKey = await _cryptoService.GetBiometricUnlockKeyAsync(); + await SetUserKeyAndContinueAsync(userKey); + } } - var success = await _platformUtilsService.AuthenticateBiometricAsync(null, - PinEnabled ? AppResources.PIN : AppResources.MasterPassword, - () => _secretEntryFocusWeakEventManager.RaiseEvent((int?)null, nameof(FocusSecretEntry))); - await _stateService.SetBiometricLockedAsync(!success); - if (success) + catch (LegacyUserException) { - var userKey = await _cryptoService.GetBiometricUnlockKeyAsync(); - await SetUserKeyAndContinueAsync(userKey); + await HandleLegacyUserAsync(); } } @@ -493,5 +548,29 @@ private async Task DoContinueAsync() _messagingService.Send("unlocked"); UnlockedAction?.Invoke(); } + + private async Task IsBiometricsEnabledAsync() + { + try + { + return await _vaultTimeoutService.IsBiometricLockSetAsync() && + await _biometricService.CanUseBiometricsUnlockAsync(); + } + catch (LegacyUserException) + { + await HandleLegacyUserAsync(); + } + return false; + } + + private async Task HandleLegacyUserAsync() + { + // Legacy users must migrate on web vault. + await _platformUtilsService.ShowDialogAsync(AppResources.EncryptionKeyMigrationRequiredDescriptionLong, + AppResources.AnErrorHasOccurred, + AppResources.Ok); + await _vaultTimeoutService.LogOutAsync(); + } + } } diff --git a/src/App/Pages/Accounts/LoginPageViewModel.cs b/src/App/Pages/Accounts/LoginPageViewModel.cs index 387fec44f4b..f6e8592ea2f 100644 --- a/src/App/Pages/Accounts/LoginPageViewModel.cs +++ b/src/App/Pages/Accounts/LoginPageViewModel.cs @@ -244,6 +244,14 @@ await _platformUtilsService.ShowDialogAsync( await _deviceActionService.HideLoadingAsync(); + if (response.RequiresEncryptionKeyMigration) + { + // Legacy users must migrate on web vault. + await _platformUtilsService.ShowDialogAsync(AppResources.EncryptionKeyMigrationRequiredDescriptionLong, AppResources.AnErrorHasOccurred, + AppResources.Ok); + return; + } + if (response.TwoFactor) { StartTwoFactorAction?.Invoke(); diff --git a/src/App/Pages/Settings/ExportVaultPage.xaml b/src/App/Pages/Settings/ExportVaultPage.xaml index c1a54d4b545..0010e26ba61 100644 --- a/src/App/Pages/Settings/ExportVaultPage.xaml +++ b/src/App/Pages/Settings/ExportVaultPage.xaml @@ -38,7 +38,8 @@