1
0
mirror of https://github.com/bitwarden/mobile.git synced 2024-12-31 17:47:43 +01:00

EC-312 Fix crash on entering invalid credentials five times on Autofill (#1988)

This commit is contained in:
Federico Maccaroni 2022-07-14 19:17:04 -03:00 committed by GitHub
parent 2d2a883b96
commit d2fbf5bdea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 236 additions and 170 deletions

View File

@ -8,5 +8,6 @@ namespace Bit.App.Abstractions
{ {
void Init(Func<AppOptions> getOptionsFunc, IAccountsManagerHost accountsManagerHost); void Init(Func<AppOptions> getOptionsFunc, IAccountsManagerHost accountsManagerHost);
Task NavigateOnAccountChangeAsync(bool? isAuthed = null); Task NavigateOnAccountChangeAsync(bool? isAuthed = null);
Task LogOutAsync(string userId, bool userInitiated, bool expired);
} }
} }

View File

@ -301,7 +301,7 @@ namespace Bit.App
UpdateThemeAsync(); UpdateThemeAsync();
}; };
Current.MainPage = new NavigationPage(new HomePage(Options)); Current.MainPage = new NavigationPage(new HomePage(Options));
var mainPageTask = _accountsManager.NavigateOnAccountChangeAsync(); _accountsManager.NavigateOnAccountChangeAsync().FireAndForget();
ServiceContainer.Resolve<MobilePlatformUtilsService>("platformUtilsService").Init(); ServiceContainer.Resolve<MobilePlatformUtilsService>("platformUtilsService").Init();
} }

View File

@ -123,7 +123,11 @@ namespace Bit.App.Utilities.AccountManagement
await _vaultTimeoutService.LockAsync(true); await _vaultTimeoutService.LockAsync(true);
break; break;
case AccountsManagerMessageCommands.LOGOUT: case AccountsManagerMessageCommands.LOGOUT:
await Device.InvokeOnMainThreadAsync(() => LogOutAsync(message.Data as Tuple<string, bool, bool>)); var extras = message.Data as Tuple<string, bool, bool>;
var userId = extras?.Item1;
var userInitiated = extras?.Item2 ?? true;
var expired = extras?.Item3 ?? false;
await Device.InvokeOnMainThreadAsync(() => LogOutAsync(userId, userInitiated, expired));
break; break;
case AccountsManagerMessageCommands.LOGGED_OUT: case AccountsManagerMessageCommands.LOGGED_OUT:
// Clean up old migrated key if they ever log out. // Clean up old migrated key if they ever log out.
@ -181,12 +185,8 @@ namespace Bit.App.Utilities.AccountManagement
}); });
} }
private async Task LogOutAsync(Tuple<string, bool, bool> extras) public async Task LogOutAsync(string userId, bool userInitiated, bool expired)
{ {
var userId = extras?.Item1;
var userInitiated = extras?.Item2 ?? true;
var expired = extras?.Item3 ?? false;
await AppHelpers.LogOutAsync(userId, userInitiated); await AppHelpers.LogOutAsync(userId, userInitiated);
await NavigateOnAccountChangeAsync(); await NavigateOnAccountChangeAsync();
_authService.LogOut(() => _authService.LogOut(() =>

View File

@ -6,6 +6,7 @@ using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Models.Domain; using Bit.Core.Models.Domain;
using Bit.Core.Models.Request; using Bit.Core.Models.Request;
using Bit.Core.Utilities;
namespace Bit.Core.Services namespace Bit.Core.Services
{ {
@ -173,7 +174,7 @@ namespace Bit.Core.Services
public void LogOut(Action callback) public void LogOut(Action callback)
{ {
callback.Invoke(); callback.Invoke();
_messagingService.Send("loggedOut"); _messagingService.Send(AccountsManagerMessageCommands.LOGGED_OUT);
} }
public List<TwoFactorProvider> GetSupportedTwoFactorProviders() public List<TwoFactorProvider> GetSupportedTwoFactorProviders()

View File

@ -8,10 +8,12 @@ using Bit.App.Utilities;
using Bit.App.Utilities.AccountManagement; using Bit.App.Utilities.AccountManagement;
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Services;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Bit.iOS.Autofill.Models; using Bit.iOS.Autofill.Models;
using Bit.iOS.Core.Utilities; using Bit.iOS.Core.Utilities;
using Bit.iOS.Core.Views; using Bit.iOS.Core.Views;
using CoreFoundation;
using CoreNFC; using CoreNFC;
using Foundation; using Foundation;
using UIKit; using UIKit;
@ -36,88 +38,128 @@ namespace Bit.iOS.Autofill
public override void ViewDidLoad() public override void ViewDidLoad()
{ {
InitApp(); try
base.ViewDidLoad();
Logo.Image = new UIImage(ThemeHelpers.LightTheme ? "logo.png" : "logo_white.png");
View.BackgroundColor = ThemeHelpers.SplashBackgroundColor;
_context = new Context
{ {
ExtContext = ExtensionContext InitApp();
}; base.ViewDidLoad();
Logo.Image = new UIImage(ThemeHelpers.LightTheme ? "logo.png" : "logo_white.png");
View.BackgroundColor = ThemeHelpers.SplashBackgroundColor;
_context = new Context
{
ExtContext = ExtensionContext
};
}
catch (Exception ex)
{
LoggerHelper.LogEvenIfCantBeResolved(ex);
throw;
}
} }
public override async void PrepareCredentialList(ASCredentialServiceIdentifier[] serviceIdentifiers) public override async void PrepareCredentialList(ASCredentialServiceIdentifier[] serviceIdentifiers)
{ {
InitAppIfNeeded(); try
_context.ServiceIdentifiers = serviceIdentifiers;
if (serviceIdentifiers.Length > 0)
{ {
var uri = serviceIdentifiers[0].Identifier; InitAppIfNeeded();
if (serviceIdentifiers[0].Type == ASCredentialServiceIdentifierType.Domain) _context.ServiceIdentifiers = serviceIdentifiers;
if (serviceIdentifiers.Length > 0)
{ {
uri = string.Concat("https://", uri); var uri = serviceIdentifiers[0].Identifier;
if (serviceIdentifiers[0].Type == ASCredentialServiceIdentifierType.Domain)
{
uri = string.Concat("https://", uri);
}
_context.UrlString = uri;
} }
_context.UrlString = uri; if (!await IsAuthed())
}
if (!await IsAuthed())
{
await _accountsManager.NavigateOnAccountChangeAsync(false);
}
else if (await IsLocked())
{
PerformSegue("lockPasswordSegue", this);
}
else
{
if (_context.ServiceIdentifiers == null || _context.ServiceIdentifiers.Length == 0)
{ {
PerformSegue("loginSearchSegue", this); await _accountsManager.NavigateOnAccountChangeAsync(false);
}
else if (await IsLocked())
{
PerformSegue("lockPasswordSegue", this);
} }
else else
{ {
PerformSegue("loginListSegue", this); if (_context.ServiceIdentifiers == null || _context.ServiceIdentifiers.Length == 0)
{
PerformSegue("loginSearchSegue", this);
}
else
{
PerformSegue("loginListSegue", this);
}
} }
} }
catch (Exception ex)
{
LoggerHelper.LogEvenIfCantBeResolved(ex);
throw;
}
} }
public override async void ProvideCredentialWithoutUserInteraction(ASPasswordCredentialIdentity credentialIdentity) public override async void ProvideCredentialWithoutUserInteraction(ASPasswordCredentialIdentity credentialIdentity)
{ {
InitAppIfNeeded(); try
await _stateService.Value.SetPasswordRepromptAutofillAsync(false);
await _stateService.Value.SetPasswordVerifiedAutofillAsync(false);
if (!await IsAuthed() || await IsLocked())
{ {
var err = new NSError(new NSString("ASExtensionErrorDomain"), InitAppIfNeeded();
Convert.ToInt32(ASExtensionErrorCode.UserInteractionRequired), null); await _stateService.Value.SetPasswordRepromptAutofillAsync(false);
ExtensionContext.CancelRequest(err); await _stateService.Value.SetPasswordVerifiedAutofillAsync(false);
return; if (!await IsAuthed() || await IsLocked())
{
var err = new NSError(new NSString("ASExtensionErrorDomain"),
Convert.ToInt32(ASExtensionErrorCode.UserInteractionRequired), null);
ExtensionContext.CancelRequest(err);
return;
}
_context.CredentialIdentity = credentialIdentity;
await ProvideCredentialAsync(false);
}
catch (Exception ex)
{
LoggerHelper.LogEvenIfCantBeResolved(ex);
throw;
} }
_context.CredentialIdentity = credentialIdentity;
await ProvideCredentialAsync(false);
} }
public override async void PrepareInterfaceToProvideCredential(ASPasswordCredentialIdentity credentialIdentity) public override async void PrepareInterfaceToProvideCredential(ASPasswordCredentialIdentity credentialIdentity)
{ {
InitAppIfNeeded(); try
if (!await IsAuthed())
{ {
await _accountsManager.NavigateOnAccountChangeAsync(false); InitAppIfNeeded();
return; if (!await IsAuthed())
{
await _accountsManager.NavigateOnAccountChangeAsync(false);
return;
}
_context.CredentialIdentity = credentialIdentity;
await CheckLockAsync(async () => await ProvideCredentialAsync());
}
catch (Exception ex)
{
LoggerHelper.LogEvenIfCantBeResolved(ex);
throw;
} }
_context.CredentialIdentity = credentialIdentity;
CheckLock(async () => await ProvideCredentialAsync());
} }
public override async void PrepareInterfaceForExtensionConfiguration() public override async void PrepareInterfaceForExtensionConfiguration()
{ {
InitAppIfNeeded(); try
_context.Configuring = true;
if (!await IsAuthed())
{ {
await _accountsManager.NavigateOnAccountChangeAsync(false); InitAppIfNeeded();
return; _context.Configuring = true;
if (!await IsAuthed())
{
await _accountsManager.NavigateOnAccountChangeAsync(false);
return;
}
await CheckLockAsync(() => PerformSegue("setupSegue", this));
}
catch (Exception ex)
{
LoggerHelper.LogEvenIfCantBeResolved(ex);
throw;
} }
CheckLock(() => PerformSegue("setupSegue", this));
} }
public void CompleteRequest(string id = null, string username = null, public void CompleteRequest(string id = null, string username = null,
@ -159,34 +201,43 @@ namespace Bit.iOS.Autofill
public override void PrepareForSegue(UIStoryboardSegue segue, NSObject sender) public override void PrepareForSegue(UIStoryboardSegue segue, NSObject sender)
{ {
if (segue.DestinationViewController is UINavigationController navController) try
{ {
if (navController.TopViewController is LoginListViewController listLoginController) if (segue.DestinationViewController is UINavigationController navController)
{ {
listLoginController.Context = _context; if (navController.TopViewController is LoginListViewController listLoginController)
listLoginController.CPViewController = this; {
segue.DestinationViewController.PresentationController.Delegate = listLoginController.Context = _context;
new CustomPresentationControllerDelegate(listLoginController.DismissModalAction); listLoginController.CPViewController = this;
} segue.DestinationViewController.PresentationController.Delegate =
else if (navController.TopViewController is LoginSearchViewController listSearchController) new CustomPresentationControllerDelegate(listLoginController.DismissModalAction);
{ }
listSearchController.Context = _context; else if (navController.TopViewController is LoginSearchViewController listSearchController)
listSearchController.CPViewController = this; {
segue.DestinationViewController.PresentationController.Delegate = listSearchController.Context = _context;
new CustomPresentationControllerDelegate(listSearchController.DismissModalAction); listSearchController.CPViewController = this;
} segue.DestinationViewController.PresentationController.Delegate =
else if (navController.TopViewController is LockPasswordViewController passwordViewController) new CustomPresentationControllerDelegate(listSearchController.DismissModalAction);
{ }
passwordViewController.CPViewController = this; else if (navController.TopViewController is LockPasswordViewController passwordViewController)
segue.DestinationViewController.PresentationController.Delegate = {
new CustomPresentationControllerDelegate(passwordViewController.DismissModalAction); passwordViewController.CPViewController = this;
} segue.DestinationViewController.PresentationController.Delegate =
else if (navController.TopViewController is SetupViewController setupViewController) new CustomPresentationControllerDelegate(passwordViewController.DismissModalAction);
{ }
setupViewController.CPViewController = this; else if (navController.TopViewController is SetupViewController setupViewController)
segue.DestinationViewController.PresentationController.Delegate = {
new CustomPresentationControllerDelegate(setupViewController.DismissModalAction); setupViewController.CPViewController = this;
segue.DestinationViewController.PresentationController.Delegate =
new CustomPresentationControllerDelegate(setupViewController.DismissModalAction);
}
} }
}
catch (Exception ex)
{
LoggerHelper.LogEvenIfCantBeResolved(ex);
throw;
} }
} }
@ -194,93 +245,109 @@ namespace Bit.iOS.Autofill
{ {
DismissViewController(false, async () => DismissViewController(false, async () =>
{ {
if (_context.CredentialIdentity != null) try
{ {
await ProvideCredentialAsync(); if (_context.CredentialIdentity != null)
return; {
await ProvideCredentialAsync();
return;
}
if (_context.Configuring)
{
PerformSegue("setupSegue", this);
return;
}
if (_context.ServiceIdentifiers == null || _context.ServiceIdentifiers.Length == 0)
{
PerformSegue("loginSearchSegue", this);
}
else
{
PerformSegue("loginListSegue", this);
}
} }
if (_context.Configuring) catch (Exception ex)
{ {
PerformSegue("setupSegue", this); LoggerHelper.LogEvenIfCantBeResolved(ex);
return; throw;
}
if (_context.ServiceIdentifiers == null || _context.ServiceIdentifiers.Length == 0)
{
PerformSegue("loginSearchSegue", this);
}
else
{
PerformSegue("loginListSegue", this);
} }
}); });
} }
private async Task ProvideCredentialAsync(bool userInteraction = true) private async Task ProvideCredentialAsync(bool userInteraction = true)
{ {
var cipherService = ServiceContainer.Resolve<ICipherService>("cipherService", true); try
Bit.Core.Models.Domain.Cipher cipher = null;
var cancel = cipherService == null || _context.CredentialIdentity?.RecordIdentifier == null;
if (!cancel)
{ {
cipher = await cipherService.GetAsync(_context.CredentialIdentity.RecordIdentifier); var cipherService = ServiceContainer.Resolve<ICipherService>("cipherService", true);
cancel = cipher == null || cipher.Type != Bit.Core.Enums.CipherType.Login || cipher.Login == null; Bit.Core.Models.Domain.Cipher cipher = null;
} var cancel = cipherService == null || _context.CredentialIdentity?.RecordIdentifier == null;
if (cancel) if (!cancel)
{ {
var err = new NSError(new NSString("ASExtensionErrorDomain"), cipher = await cipherService.GetAsync(_context.CredentialIdentity.RecordIdentifier);
Convert.ToInt32(ASExtensionErrorCode.CredentialIdentityNotFound), null); cancel = cipher == null || cipher.Type != Bit.Core.Enums.CipherType.Login || cipher.Login == null;
ExtensionContext?.CancelRequest(err); }
return; if (cancel)
}
var decCipher = await cipher.DecryptAsync();
if (decCipher.Reprompt != Bit.Core.Enums.CipherRepromptType.None)
{
// Prompt for password using either the lock screen or dialog unless
// already verified the password.
if (!userInteraction)
{ {
await _stateService.Value.SetPasswordRepromptAutofillAsync(true);
var err = new NSError(new NSString("ASExtensionErrorDomain"), var err = new NSError(new NSString("ASExtensionErrorDomain"),
Convert.ToInt32(ASExtensionErrorCode.UserInteractionRequired), null); Convert.ToInt32(ASExtensionErrorCode.CredentialIdentityNotFound), null);
ExtensionContext?.CancelRequest(err); ExtensionContext?.CancelRequest(err);
return; return;
} }
else if (!await _stateService.Value.GetPasswordVerifiedAutofillAsync())
var decCipher = await cipher.DecryptAsync();
if (decCipher.Reprompt != Bit.Core.Enums.CipherRepromptType.None)
{ {
// Add a timeout to resolve keyboard not always showing up. // Prompt for password using either the lock screen or dialog unless
await Task.Delay(250); // already verified the password.
var passwordRepromptService = ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService"); if (!userInteraction)
if (!await passwordRepromptService.ShowPasswordPromptAsync())
{ {
await _stateService.Value.SetPasswordRepromptAutofillAsync(true);
var err = new NSError(new NSString("ASExtensionErrorDomain"), var err = new NSError(new NSString("ASExtensionErrorDomain"),
Convert.ToInt32(ASExtensionErrorCode.UserCanceled), null); Convert.ToInt32(ASExtensionErrorCode.UserInteractionRequired), null);
ExtensionContext?.CancelRequest(err); ExtensionContext?.CancelRequest(err);
return; return;
} }
else if (!await _stateService.Value.GetPasswordVerifiedAutofillAsync())
{
// Add a timeout to resolve keyboard not always showing up.
await Task.Delay(250);
var passwordRepromptService = ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService");
if (!await passwordRepromptService.ShowPasswordPromptAsync())
{
var err = new NSError(new NSString("ASExtensionErrorDomain"),
Convert.ToInt32(ASExtensionErrorCode.UserCanceled), null);
ExtensionContext?.CancelRequest(err);
return;
}
}
} }
} string totpCode = null;
string totpCode = null; var disableTotpCopy = await _stateService.Value.GetDisableAutoTotpCopyAsync();
var disableTotpCopy = await _stateService.Value.GetDisableAutoTotpCopyAsync(); if (!disableTotpCopy.GetValueOrDefault(false))
if (!disableTotpCopy.GetValueOrDefault(false))
{
var canAccessPremiumAsync = await _stateService.Value.CanAccessPremiumAsync();
if (!string.IsNullOrWhiteSpace(decCipher.Login.Totp) &&
(canAccessPremiumAsync || cipher.OrganizationUseTotp))
{ {
var totpService = ServiceContainer.Resolve<ITotpService>("totpService"); var canAccessPremiumAsync = await _stateService.Value.CanAccessPremiumAsync();
totpCode = await totpService.GetCodeAsync(decCipher.Login.Totp); if (!string.IsNullOrWhiteSpace(decCipher.Login.Totp) &&
(canAccessPremiumAsync || cipher.OrganizationUseTotp))
{
var totpService = ServiceContainer.Resolve<ITotpService>("totpService");
totpCode = await totpService.GetCodeAsync(decCipher.Login.Totp);
}
} }
}
CompleteRequest(decCipher.Id, decCipher.Login.Username, decCipher.Login.Password, totpCode); CompleteRequest(decCipher.Id, decCipher.Login.Username, decCipher.Login.Password, totpCode);
}
catch (Exception ex)
{
LoggerHelper.LogEvenIfCantBeResolved(ex);
throw;
}
} }
private async void CheckLock(Action notLockedAction) private async Task CheckLockAsync(Action notLockedAction)
{ {
if (await IsLocked() || await _stateService.Value.GetPasswordRepromptAutofillAsync()) if (await IsLocked() || await _stateService.Value.GetPasswordRepromptAutofillAsync())
{ {
PerformSegue("lockPasswordSegue", this); DispatchQueue.MainQueue.DispatchAsync(() => PerformSegue("lockPasswordSegue", this));
} }
else else
{ {
@ -303,15 +370,21 @@ namespace Bit.iOS.Autofill
{ {
NSRunLoop.Main.BeginInvokeOnMainThread(async () => NSRunLoop.Main.BeginInvokeOnMainThread(async () =>
{ {
if (await IsAuthed()) try
{ {
await AppHelpers.LogOutAsync(await _stateService.Value.GetActiveUserIdAsync()); if (await IsAuthed())
var deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
if (deviceActionService.SystemMajorVersion() >= 12)
{ {
await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync(); await AppHelpers.LogOutAsync(await _stateService.Value.GetActiveUserIdAsync());
if (UIDevice.CurrentDevice.CheckSystemVersion(12, 0))
{
await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync();
}
} }
} }
catch (Exception ex)
{
LoggerHelper.LogEvenIfCantBeResolved(ex);
}
}); });
} }

View File

@ -28,6 +28,7 @@ namespace Bit.iOS.Core.Controllers
private IPlatformUtilsService _platformUtilsService; private IPlatformUtilsService _platformUtilsService;
private IBiometricService _biometricService; private IBiometricService _biometricService;
private IKeyConnectorService _keyConnectorService; private IKeyConnectorService _keyConnectorService;
private IAccountsManager _accountManager;
private bool _isPinProtected; private bool _isPinProtected;
private bool _isPinProtectedWithKey; private bool _isPinProtectedWithKey;
private bool _pinLock; private bool _pinLock;
@ -84,7 +85,7 @@ namespace Bit.iOS.Core.Controllers
} }
public abstract UITableView TableView { get; } public abstract UITableView TableView { get; }
public override async void ViewDidLoad() public override async void ViewDidLoad()
{ {
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService"); _vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
@ -95,6 +96,7 @@ namespace Bit.iOS.Core.Controllers
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService"); _platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
_biometricService = ServiceContainer.Resolve<IBiometricService>("biometricService"); _biometricService = ServiceContainer.Resolve<IBiometricService>("biometricService");
_keyConnectorService = ServiceContainer.Resolve<IKeyConnectorService>("keyConnectorService"); _keyConnectorService = ServiceContainer.Resolve<IKeyConnectorService>("keyConnectorService");
_accountManager = ServiceContainer.Resolve<IAccountsManager>("accountsManager");
// We re-use the lock screen for autofill extension to verify master password // We re-use the lock screen for autofill extension to verify master password
// when trying to access protected items. // when trying to access protected items.
@ -265,13 +267,7 @@ namespace Bit.iOS.Core.Controllers
} }
if (failed) if (failed)
{ {
var invalidUnlockAttempts = await AppHelpers.IncrementInvalidUnlockAttemptsAsync(); await HandleFailedCredentialsAsync();
if (invalidUnlockAttempts >= 5)
{
await LogOutAsync();
return;
}
InvalidValue();
} }
} }
else else
@ -306,17 +302,22 @@ namespace Bit.iOS.Core.Controllers
} }
else else
{ {
var invalidUnlockAttempts = await AppHelpers.IncrementInvalidUnlockAttemptsAsync(); await HandleFailedCredentialsAsync();
if (invalidUnlockAttempts >= 5)
{
await LogOutAsync();
return;
}
InvalidValue();
} }
} }
} }
private async Task HandleFailedCredentialsAsync()
{
var invalidUnlockAttempts = await AppHelpers.IncrementInvalidUnlockAttemptsAsync();
if (invalidUnlockAttempts >= 5)
{
await _accountManager.LogOutAsync(await _stateService.GetActiveUserIdAsync(), false, false);
return;
}
InvalidValue();
}
public async Task PromptBiometricAsync() public async Task PromptBiometricAsync()
{ {
if (!_biometricLock || !_biometricIntegrityValid) if (!_biometricLock || !_biometricIntegrityValid)
@ -395,16 +396,6 @@ namespace Bit.iOS.Core.Controllers
PresentViewController(alert, true, null); PresentViewController(alert, true, null);
} }
private async Task LogOutAsync()
{
await AppHelpers.LogOutAsync(await _stateService.GetActiveUserIdAsync());
var authService = ServiceContainer.Resolve<IAuthService>("authService");
authService.LogOut(() =>
{
Cancel?.Invoke();
});
}
protected override void Dispose(bool disposing) protected override void Dispose(bool disposing)
{ {
base.Dispose(disposing); base.Dispose(disposing);