mirror of
https://github.com/bitwarden/android.git
synced 2026-05-23 06:31:34 -05:00
* Account Switching (#1720) * Account switching * WIP * wip * wip * updates to send test logic * fixed Send tests * fixes for theme handling on account switching and re-adding existing account * switch fixes * fixes * fixes * cleanup * vault timeout fixes * account list status enhancements * logout fixes and token handling improvements * merge latest (#1727) * remove duplicate dependency * fix for initial login token storage paradox (#1730) * Fix avatar color update toolbar item issue on iOS for account switching (#1735) * Updated account switching menu UI (#1733) * updated account switching menu UI * additional changes * add key suffix to constant * GetFirstLetters method tweaks * Fix crash on account switching when logging out when having more than user at a time (#1740) * single account migration to multi-account on app update (#1741) * Account Switching Tap to dismiss (#1743) * Added tap to dismiss on the Account switching overlay and improved a bit the code * Fix account switching overlay background transparent on the proper place * Fixed transparent background and the shadow on the account switching overlay * Fix iOS top space on Account switching list overlay after modal (#1746) * Fix top space added to Account switching list overlay after closing modal * Fix top space added to Account switching list overlay after closing modal on lock, login and home views just in case we add modals in the future there as well * Usability: dismiss account list on certain events (#1748) * dismiss account list on certain events * use new FireAndForget method for back button logic * Create and use Account Switching overlay control (#1753) * Added Account switching overlay control and its own ViewModel and refactored accordingly * Fix account switching Accounts list binding update * Implemented dismiss account switching overlay when changing tabs and when selecting the same tab. Also updated the deprecated listener on CustomTabbedRenderer on Android (#1755) * Overriden Equals on AvatarImageSource so it doesn't get set multiple times when it's the same image thus producing blinking on tab chaged (#1756) * Usability improvements for logout on vault timeout (#1781) * accountswitching fixes (#1784) * Fix for invalid PIN lock state when switching accounts (#1792) * fix for pin lock flow * named tuple values and updated async * clear send service cache on account switch (#1796) * Global theme and account removal (#1793) * Global theme and account removal * remove redundant call to hide account list overlay * cleanup and additional tweaks * add try/catch to remove account dialog flow Co-authored-by: Federico Maccaroni <fedemkr@gmail.com>
500 lines
20 KiB
C#
500 lines
20 KiB
C#
using System;
|
|
using UIKit;
|
|
using Foundation;
|
|
using Bit.iOS.Core.Views;
|
|
using Bit.App.Resources;
|
|
using Bit.iOS.Core.Utilities;
|
|
using Bit.App.Abstractions;
|
|
using Bit.Core.Abstractions;
|
|
using Bit.Core.Utilities;
|
|
using System.Threading.Tasks;
|
|
using Bit.App.Utilities;
|
|
using Bit.Core.Models.Domain;
|
|
using Bit.Core.Enums;
|
|
using Bit.App.Pages;
|
|
using Bit.App.Models;
|
|
using Xamarin.Forms;
|
|
using Bit.Core;
|
|
|
|
namespace Bit.iOS.Core.Controllers
|
|
{
|
|
public abstract class LockPasswordViewController : ExtendedUITableViewController
|
|
{
|
|
private IVaultTimeoutService _vaultTimeoutService;
|
|
private ICryptoService _cryptoService;
|
|
private IDeviceActionService _deviceActionService;
|
|
private IStateService _stateService;
|
|
private IStorageService _secureStorageService;
|
|
private IPlatformUtilsService _platformUtilsService;
|
|
private IBiometricService _biometricService;
|
|
private IKeyConnectorService _keyConnectorService;
|
|
private bool _isPinProtected;
|
|
private bool _isPinProtectedWithKey;
|
|
private bool _pinLock;
|
|
private bool _biometricLock;
|
|
private bool _biometricIntegrityValid = true;
|
|
private bool _passwordReprompt = false;
|
|
private bool _usesKeyConnector;
|
|
private bool _biometricUnlockOnly = false;
|
|
|
|
protected bool autofillExtension = false;
|
|
|
|
public LockPasswordViewController(IntPtr handle)
|
|
: base(handle)
|
|
{ }
|
|
|
|
public abstract UINavigationItem BaseNavItem { get; }
|
|
public abstract UIBarButtonItem BaseCancelButton { get; }
|
|
public abstract UIBarButtonItem BaseSubmitButton { get; }
|
|
public abstract Action Success { get; }
|
|
public abstract Action Cancel { get; }
|
|
|
|
public FormEntryTableViewCell MasterPasswordCell { get; set; } = new FormEntryTableViewCell(
|
|
AppResources.MasterPassword, useButton: true);
|
|
|
|
public string BiometricIntegrityKey { get; set; }
|
|
|
|
public UITableViewCell BiometricCell
|
|
{
|
|
get
|
|
{
|
|
var cell = new UITableViewCell();
|
|
cell.BackgroundColor = ThemeHelpers.BackgroundColor;
|
|
if (_biometricIntegrityValid)
|
|
{
|
|
var biometricButtonText = _deviceActionService.SupportsFaceBiometric() ?
|
|
AppResources.UseFaceIDToUnlock : AppResources.UseFingerprintToUnlock;
|
|
cell.TextLabel.TextColor = ThemeHelpers.PrimaryColor;
|
|
cell.TextLabel.Text = biometricButtonText;
|
|
}
|
|
else
|
|
{
|
|
cell.TextLabel.TextColor = ThemeHelpers.DangerColor;
|
|
cell.TextLabel.Font = ThemeHelpers.GetDangerFont();
|
|
cell.TextLabel.Lines = 0;
|
|
cell.TextLabel.LineBreakMode = UILineBreakMode.WordWrap;
|
|
cell.TextLabel.Text = AppResources.BiometricInvalidatedExtension;
|
|
}
|
|
return cell;
|
|
}
|
|
}
|
|
|
|
public override async void ViewDidLoad()
|
|
{
|
|
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
|
|
_cryptoService = ServiceContainer.Resolve<ICryptoService>("cryptoService");
|
|
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
|
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
|
_secureStorageService = ServiceContainer.Resolve<IStorageService>("secureStorageService");
|
|
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
|
_biometricService = ServiceContainer.Resolve<IBiometricService>("biometricService");
|
|
_keyConnectorService = ServiceContainer.Resolve<IKeyConnectorService>("keyConnectorService");
|
|
|
|
// We re-use the lock screen for autofill extension to verify master password
|
|
// when trying to access protected items.
|
|
if (autofillExtension && await _stateService.GetPasswordRepromptAutofillAsync())
|
|
{
|
|
_passwordReprompt = true;
|
|
_isPinProtected = false;
|
|
_isPinProtectedWithKey = false;
|
|
_pinLock = false;
|
|
_biometricLock = false;
|
|
}
|
|
else
|
|
{
|
|
(_isPinProtected, _isPinProtectedWithKey) = await _vaultTimeoutService.IsPinLockSetAsync();
|
|
_pinLock = (_isPinProtected && await _stateService.GetPinProtectedKeyAsync() != null) ||
|
|
_isPinProtectedWithKey;
|
|
_biometricLock = await _vaultTimeoutService.IsBiometricLockSetAsync() &&
|
|
await _cryptoService.HasKeyAsync();
|
|
_biometricIntegrityValid = await _biometricService.ValidateIntegrityAsync(BiometricIntegrityKey);
|
|
_usesKeyConnector = await _keyConnectorService.GetUsesKeyConnector();
|
|
_biometricUnlockOnly = _usesKeyConnector && _biometricLock && !_pinLock;
|
|
}
|
|
|
|
if (_pinLock)
|
|
{
|
|
BaseNavItem.Title = AppResources.VerifyPIN;
|
|
}
|
|
else if (_usesKeyConnector)
|
|
{
|
|
BaseNavItem.Title = AppResources.UnlockVault;
|
|
}
|
|
else
|
|
{
|
|
BaseNavItem.Title = AppResources.VerifyMasterPassword;
|
|
}
|
|
|
|
BaseCancelButton.Title = AppResources.Cancel;
|
|
|
|
if (_biometricUnlockOnly)
|
|
{
|
|
BaseSubmitButton.Title = null;
|
|
BaseSubmitButton.Enabled = false;
|
|
}
|
|
else
|
|
{
|
|
BaseSubmitButton.Title = AppResources.Submit;
|
|
}
|
|
|
|
var descriptor = UIFontDescriptor.PreferredBody;
|
|
|
|
if (!_biometricUnlockOnly)
|
|
{
|
|
MasterPasswordCell.Label.Text = _pinLock ? AppResources.PIN : AppResources.MasterPassword;
|
|
MasterPasswordCell.TextField.SecureTextEntry = true;
|
|
MasterPasswordCell.TextField.ReturnKeyType = UIReturnKeyType.Go;
|
|
MasterPasswordCell.TextField.ShouldReturn += (UITextField tf) =>
|
|
{
|
|
CheckPasswordAsync().GetAwaiter().GetResult();
|
|
return true;
|
|
};
|
|
if (_pinLock)
|
|
{
|
|
MasterPasswordCell.TextField.KeyboardType = UIKeyboardType.NumberPad;
|
|
}
|
|
MasterPasswordCell.Button.TitleLabel.Font = UIFont.FromName("bwi-font", 28f);
|
|
MasterPasswordCell.Button.SetTitle(BitwardenIcons.Eye, UIControlState.Normal);
|
|
MasterPasswordCell.Button.TouchUpInside += (sender, e) => {
|
|
MasterPasswordCell.TextField.SecureTextEntry = !MasterPasswordCell.TextField.SecureTextEntry;
|
|
MasterPasswordCell.Button.SetTitle(MasterPasswordCell.TextField.SecureTextEntry ? BitwardenIcons.Eye : BitwardenIcons.EyeSlash, UIControlState.Normal);
|
|
};
|
|
}
|
|
|
|
TableView.RowHeight = UITableView.AutomaticDimension;
|
|
TableView.EstimatedRowHeight = 70;
|
|
TableView.Source = new TableSource(this);
|
|
TableView.AllowsSelection = true;
|
|
|
|
base.ViewDidLoad();
|
|
|
|
if (_biometricLock)
|
|
{
|
|
if (!_biometricIntegrityValid)
|
|
{
|
|
return;
|
|
}
|
|
var tasks = Task.Run(async () =>
|
|
{
|
|
await Task.Delay(500);
|
|
NSRunLoop.Main.BeginInvokeOnMainThread(async () => await PromptBiometricAsync());
|
|
});
|
|
}
|
|
}
|
|
|
|
public override async void ViewDidAppear(bool animated)
|
|
{
|
|
base.ViewDidAppear(animated);
|
|
|
|
// Users with key connector and without biometric or pin has no MP to unlock with
|
|
if (_usesKeyConnector)
|
|
{
|
|
if (!(_pinLock || _biometricLock) ||
|
|
(_biometricLock && !_biometricIntegrityValid))
|
|
{
|
|
PromptSSO();
|
|
}
|
|
}
|
|
else if (!_biometricLock || !_biometricIntegrityValid)
|
|
{
|
|
MasterPasswordCell.TextField.BecomeFirstResponder();
|
|
}
|
|
}
|
|
|
|
protected async Task CheckPasswordAsync()
|
|
{
|
|
if (string.IsNullOrWhiteSpace(MasterPasswordCell.TextField.Text))
|
|
{
|
|
var alert = Dialogs.CreateAlert(AppResources.AnErrorHasOccurred,
|
|
string.Format(AppResources.ValidationFieldRequired,
|
|
_pinLock ? AppResources.PIN : AppResources.MasterPassword),
|
|
AppResources.Ok);
|
|
PresentViewController(alert, true, null);
|
|
return;
|
|
}
|
|
|
|
var email = await _stateService.GetEmailAsync();
|
|
var kdf = await _stateService.GetKdfTypeAsync();
|
|
var kdfIterations = await _stateService.GetKdfIterationsAsync();
|
|
var inputtedValue = MasterPasswordCell.TextField.Text;
|
|
|
|
if (_pinLock)
|
|
{
|
|
var failed = true;
|
|
try
|
|
{
|
|
if (_isPinProtected)
|
|
{
|
|
var key = await _cryptoService.MakeKeyFromPinAsync(inputtedValue, email,
|
|
kdf.GetValueOrDefault(KdfType.PBKDF2_SHA256), kdfIterations.GetValueOrDefault(5000),
|
|
await _stateService.GetPinProtectedKeyAsync());
|
|
var encKey = await _cryptoService.GetEncKeyAsync(key);
|
|
var protectedPin = await _stateService.GetProtectedPinAsync();
|
|
var decPin = await _cryptoService.DecryptToUtf8Async(new EncString(protectedPin), encKey);
|
|
failed = decPin != inputtedValue;
|
|
if (!failed)
|
|
{
|
|
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
|
|
await SetKeyAndContinueAsync(key);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
var key2 = await _cryptoService.MakeKeyFromPinAsync(inputtedValue, email,
|
|
kdf.GetValueOrDefault(KdfType.PBKDF2_SHA256), kdfIterations.GetValueOrDefault(5000));
|
|
failed = false;
|
|
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
|
|
await SetKeyAndContinueAsync(key2);
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
failed = true;
|
|
}
|
|
if (failed)
|
|
{
|
|
var invalidUnlockAttempts = await AppHelpers.IncrementInvalidUnlockAttemptsAsync();
|
|
if (invalidUnlockAttempts >= 5)
|
|
{
|
|
await LogOutAsync();
|
|
return;
|
|
}
|
|
InvalidValue();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
var key2 = await _cryptoService.MakeKeyAsync(inputtedValue, email, kdf, kdfIterations);
|
|
|
|
var storedKeyHash = await _cryptoService.GetKeyHashAsync();
|
|
if (storedKeyHash == null)
|
|
{
|
|
var oldKey = await _secureStorageService.GetAsync<string>("oldKey");
|
|
if (key2.KeyB64 == oldKey)
|
|
{
|
|
var localKeyHash = await _cryptoService.HashPasswordAsync(inputtedValue, key2, HashPurpose.LocalAuthorization);
|
|
await _secureStorageService.RemoveAsync("oldKey");
|
|
await _cryptoService.SetKeyHashAsync(localKeyHash);
|
|
}
|
|
}
|
|
var passwordValid = await _cryptoService.CompareAndUpdateKeyHashAsync(inputtedValue, key2);
|
|
if (passwordValid)
|
|
{
|
|
if (_isPinProtected)
|
|
{
|
|
var protectedPin = await _stateService.GetProtectedPinAsync();
|
|
var encKey = await _cryptoService.GetEncKeyAsync(key2);
|
|
var decPin = await _cryptoService.DecryptToUtf8Async(new EncString(protectedPin), encKey);
|
|
var pinKey = await _cryptoService.MakePinKeyAysnc(decPin, email,
|
|
kdf.GetValueOrDefault(KdfType.PBKDF2_SHA256), kdfIterations.GetValueOrDefault(5000));
|
|
await _stateService.SetPinProtectedKeyAsync(await _cryptoService.EncryptAsync(key2.Key, pinKey));
|
|
}
|
|
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
|
|
await SetKeyAndContinueAsync(key2, true);
|
|
}
|
|
else
|
|
{
|
|
var invalidUnlockAttempts = await AppHelpers.IncrementInvalidUnlockAttemptsAsync();
|
|
if (invalidUnlockAttempts >= 5)
|
|
{
|
|
await LogOutAsync();
|
|
return;
|
|
}
|
|
InvalidValue();
|
|
}
|
|
}
|
|
}
|
|
|
|
public async Task PromptBiometricAsync()
|
|
{
|
|
if (!_biometricLock || !_biometricIntegrityValid)
|
|
{
|
|
return;
|
|
}
|
|
var success = await _platformUtilsService.AuthenticateBiometricAsync(null,
|
|
_pinLock ? AppResources.PIN : AppResources.MasterPassword,
|
|
() => MasterPasswordCell.TextField.BecomeFirstResponder());
|
|
_stateService.BiometricLocked = !success;
|
|
if (success)
|
|
{
|
|
DoContinue();
|
|
}
|
|
}
|
|
|
|
public void PromptSSO()
|
|
{
|
|
var loginPage = new LoginSsoPage();
|
|
var app = new App.App(new AppOptions { IosExtension = true });
|
|
ThemeManager.SetTheme(app.Resources);
|
|
ThemeManager.ApplyResourcesToPage(loginPage);
|
|
if (loginPage.BindingContext is LoginSsoPageViewModel vm)
|
|
{
|
|
vm.SsoAuthSuccessAction = () => DoContinue();
|
|
vm.CloseAction = Cancel;
|
|
}
|
|
|
|
var navigationPage = new NavigationPage(loginPage);
|
|
var loginController = navigationPage.CreateViewController();
|
|
loginController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
|
|
PresentViewController(loginController, true, null);
|
|
}
|
|
|
|
private async Task SetKeyAndContinueAsync(SymmetricCryptoKey key, bool masterPassword = false)
|
|
{
|
|
var hasKey = await _cryptoService.HasKeyAsync();
|
|
if (!hasKey)
|
|
{
|
|
await _cryptoService.SetKeyAsync(key);
|
|
}
|
|
DoContinue(masterPassword);
|
|
}
|
|
|
|
private async void DoContinue(bool masterPassword = false)
|
|
{
|
|
if (masterPassword)
|
|
{
|
|
await _stateService.SetPasswordVerifiedAutofillAsync(true);
|
|
}
|
|
await EnableBiometricsIfNeeded();
|
|
_stateService.BiometricLocked = false;
|
|
MasterPasswordCell.TextField.ResignFirstResponder();
|
|
Success();
|
|
}
|
|
|
|
private async Task EnableBiometricsIfNeeded()
|
|
{
|
|
// Re-enable biometrics if initial use
|
|
if (_biometricLock & !_biometricIntegrityValid)
|
|
{
|
|
await _biometricService.SetupBiometricAsync(BiometricIntegrityKey);
|
|
}
|
|
}
|
|
|
|
private void InvalidValue()
|
|
{
|
|
var alert = Dialogs.CreateAlert(AppResources.AnErrorHasOccurred,
|
|
string.Format(null, _pinLock ? AppResources.PIN : AppResources.InvalidMasterPassword),
|
|
AppResources.Ok, (a) =>
|
|
{
|
|
|
|
MasterPasswordCell.TextField.Text = string.Empty;
|
|
MasterPasswordCell.TextField.BecomeFirstResponder();
|
|
});
|
|
PresentViewController(alert, true, null);
|
|
}
|
|
|
|
private async Task LogOutAsync()
|
|
{
|
|
await AppHelpers.LogOutAsync(await _stateService.GetActiveUserIdAsync());
|
|
var authService = ServiceContainer.Resolve<IAuthService>("authService");
|
|
authService.LogOut(() =>
|
|
{
|
|
Cancel?.Invoke();
|
|
});
|
|
}
|
|
|
|
public class TableSource : ExtendedUITableViewSource
|
|
{
|
|
private LockPasswordViewController _controller;
|
|
|
|
public TableSource(LockPasswordViewController controller)
|
|
{
|
|
_controller = controller;
|
|
}
|
|
|
|
public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath)
|
|
{
|
|
if (indexPath.Section == 0)
|
|
{
|
|
if (indexPath.Row == 0)
|
|
{
|
|
if (_controller._biometricUnlockOnly)
|
|
{
|
|
return _controller.BiometricCell;
|
|
}
|
|
else
|
|
{
|
|
return _controller.MasterPasswordCell;
|
|
}
|
|
}
|
|
}
|
|
else if (indexPath.Section == 1)
|
|
{
|
|
if (indexPath.Row == 0)
|
|
{
|
|
if (_controller._passwordReprompt)
|
|
{
|
|
var cell = new ExtendedUITableViewCell();
|
|
cell.TextLabel.TextColor = ThemeHelpers.DangerColor;
|
|
cell.TextLabel.Font = ThemeHelpers.GetDangerFont();
|
|
cell.TextLabel.Lines = 0;
|
|
cell.TextLabel.LineBreakMode = UILineBreakMode.WordWrap;
|
|
cell.TextLabel.Text = AppResources.PasswordConfirmationDesc;
|
|
return cell;
|
|
}
|
|
else if (!_controller._biometricUnlockOnly)
|
|
{
|
|
return _controller.BiometricCell;
|
|
}
|
|
}
|
|
}
|
|
return new ExtendedUITableViewCell();
|
|
}
|
|
|
|
public override nfloat GetHeightForRow(UITableView tableView, NSIndexPath indexPath)
|
|
{
|
|
return UITableView.AutomaticDimension;
|
|
}
|
|
|
|
public override nint NumberOfSections(UITableView tableView)
|
|
{
|
|
return (!_controller._biometricUnlockOnly && _controller._biometricLock) ||
|
|
_controller._passwordReprompt
|
|
? 2
|
|
: 1;
|
|
}
|
|
|
|
public override nint RowsInSection(UITableView tableview, nint section)
|
|
{
|
|
if (section <= 1)
|
|
{
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
public override nfloat GetHeightForHeader(UITableView tableView, nint section)
|
|
{
|
|
return section == 1 ? 0.00001f : UITableView.AutomaticDimension;
|
|
}
|
|
|
|
public override string TitleForHeader(UITableView tableView, nint section)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
public override void RowSelected(UITableView tableView, NSIndexPath indexPath)
|
|
{
|
|
tableView.DeselectRow(indexPath, true);
|
|
tableView.EndEditing(true);
|
|
if (indexPath.Row == 0 &&
|
|
((_controller._biometricUnlockOnly && indexPath.Section == 0) ||
|
|
indexPath.Section == 1))
|
|
{
|
|
var task = _controller.PromptBiometricAsync();
|
|
return;
|
|
}
|
|
var cell = tableView.CellAt(indexPath);
|
|
if (cell == null)
|
|
{
|
|
return;
|
|
}
|
|
if (cell is ISelectable selectableCell)
|
|
{
|
|
selectableCell.Select();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|