diff --git a/src/App/App.csproj b/src/App/App.csproj index b178d6813..b767773cf 100644 --- a/src/App/App.csproj +++ b/src/App/App.csproj @@ -29,6 +29,9 @@ HintPage.xaml + + LockPage.xaml + RegisterPage.xaml diff --git a/src/App/App.xaml.cs b/src/App/App.xaml.cs index 53d1ec65d..e2ec08a7e 100644 --- a/src/App/App.xaml.cs +++ b/src/App/App.xaml.cs @@ -20,6 +20,18 @@ namespace Bit.App private readonly IBroadcasterService _broadcasterService; private readonly IMessagingService _messagingService; private readonly IStateService _stateService; + private readonly ILockService _lockService; + private readonly ISyncService _syncService; + private readonly ITokenService _tokenService; + private readonly ICryptoService _cryptoService; + private readonly ICipherService _cipherService; + private readonly IFolderService _folderService; + private readonly ICollectionService _collectionService; + private readonly ISettingsService _settingsService; + private readonly IPasswordGenerationService _passwordGenerationService; + private readonly ISearchService _searchService; + private readonly IPlatformUtilsService _platformUtilsService; + private readonly IAuthService _authService; public App() { @@ -27,6 +39,19 @@ namespace Bit.App _broadcasterService = ServiceContainer.Resolve("broadcasterService"); _messagingService = ServiceContainer.Resolve("messagingService"); _stateService = ServiceContainer.Resolve("stateService"); + _lockService = ServiceContainer.Resolve("lockService"); + _syncService = ServiceContainer.Resolve("syncService"); + _tokenService = ServiceContainer.Resolve("tokenService"); + _cryptoService = ServiceContainer.Resolve("cryptoService"); + _cipherService = ServiceContainer.Resolve("cipherService"); + _folderService = ServiceContainer.Resolve("folderService"); + _settingsService = ServiceContainer.Resolve("settingsService"); + _collectionService = ServiceContainer.Resolve("collectionService"); + _searchService = ServiceContainer.Resolve("searchService"); + _authService = ServiceContainer.Resolve("authService"); + _platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); + _passwordGenerationService = ServiceContainer.Resolve( + "passwordGenerationService"); _i18nService = ServiceContainer.Resolve("i18nService") as MobileI18nService; InitializeComponent(); @@ -58,8 +83,23 @@ namespace Bit.App else if(message.Command == "locked") { await _stateService.PurgeAsync(); + MainPage = new NavigationPage(new LockPage()); + } + else if(message.Command == "lockVault") + { + await _lockService.LockAsync(true); + } + else if(message.Command == "logout") + { + await LogOutAsync(false); + } + else if(message.Command == "loggedOut") + { + // TODO + } + else if(message.Command == "unlocked" || message.Command == "loggedIn") + { // TODO - // MainPage = new LockPage(); } }); } @@ -87,12 +127,46 @@ namespace Bit.App new System.Globalization.UmAlQuraCalendar(); } + private async Task LogOutAsync(bool expired) + { + var userId = await _userService.GetUserIdAsync(); + await Task.WhenAll( + _syncService.SetLastSyncAsync(DateTime.MinValue), + _tokenService.ClearTokenAsync(), + _cryptoService.ClearKeysAsync(), + _userService.ClearAsync(), + _settingsService.ClearAsync(userId), + _cipherService.ClearAsync(userId), + _folderService.ClearAsync(userId), + _collectionService.ClearAsync(userId), + _passwordGenerationService.ClearAsync(), + _lockService.ClearAsync()); + _lockService.PinLocked = false; + _searchService.ClearIndex(); + _authService.LogOut(() => + { + if(expired) + { + // TODO: Toast? + } + MainPage = new HomePage(); + }); + } + private async Task SetMainPageAsync() { var authed = await _userService.IsAuthenticatedAsync(); if(authed) { - Current.MainPage = new TabsPage(); + var locked = await _lockService.IsLockedAsync(); + if(locked) + { + Current.MainPage = new NavigationPage(new LockPage()); + } + else + { + Current.MainPage = new TabsPage(); + } } else { diff --git a/src/App/Pages/Accounts/LockPage.xaml b/src/App/Pages/Accounts/LockPage.xaml new file mode 100644 index 000000000..f800b5178 --- /dev/null +++ b/src/App/Pages/Accounts/LockPage.xaml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/App/Pages/Accounts/LockPage.xaml.cs b/src/App/Pages/Accounts/LockPage.xaml.cs new file mode 100644 index 000000000..bacab30d8 --- /dev/null +++ b/src/App/Pages/Accounts/LockPage.xaml.cs @@ -0,0 +1,52 @@ +using System; +using Xamarin.Forms; + +namespace Bit.App.Pages +{ + public partial class LockPage : BaseContentPage + { + private LockPageViewModel _vm; + + public LockPage() + { + InitializeComponent(); + _vm = BindingContext as LockPageViewModel; + _vm.Page = this; + MasterPasswordEntry = _masterPassword; + PinEntry = _pin; + } + + public Entry MasterPasswordEntry { get; set; } + public Entry PinEntry { get; set; } + + protected override async void OnAppearing() + { + base.OnAppearing(); + await _vm.InitAsync(); + if(_vm.PinLock) + { + RequestFocus(PinEntry); + } + else + { + RequestFocus(MasterPasswordEntry); + } + } + + private async void Unlock_Clicked(object sender, EventArgs e) + { + if(DoOnce()) + { + await _vm.SubmitAsync(); + } + } + + private async void LogOut_Clicked(object sender, EventArgs e) + { + if(DoOnce()) + { + await _vm.LogOutAsync(); + } + } + } +} diff --git a/src/App/Pages/Accounts/LockPageViewModel.cs b/src/App/Pages/Accounts/LockPageViewModel.cs new file mode 100644 index 000000000..b9d60ee25 --- /dev/null +++ b/src/App/Pages/Accounts/LockPageViewModel.cs @@ -0,0 +1,180 @@ +using Bit.App.Abstractions; +using Bit.App.Resources; +using Bit.Core; +using Bit.Core.Abstractions; +using Bit.Core.Enums; +using Bit.Core.Models.Domain; +using Bit.Core.Utilities; +using System; +using System.Threading.Tasks; +using Xamarin.Forms; + +namespace Bit.App.Pages +{ + public class LockPageViewModel : BaseViewModel + { + private readonly IPlatformUtilsService _platformUtilsService; + private readonly IDeviceActionService _deviceActionService; + private readonly ILockService _lockService; + private readonly ICryptoService _cryptoService; + private readonly IStorageService _storageService; + private readonly IUserService _userService; + private readonly IMessagingService _messagingService; + + private string _email; + private bool _showPassword; + private bool _pinLock; + private int _invalidPinAttempts = 0; + private Tuple _pinSet; + + public LockPageViewModel() + { + _platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); + _deviceActionService = ServiceContainer.Resolve("deviceActionService"); + _lockService = ServiceContainer.Resolve("lockService"); + _cryptoService = ServiceContainer.Resolve("cryptoService"); + _storageService = ServiceContainer.Resolve("storageService"); + _userService = ServiceContainer.Resolve("userService"); + _messagingService = ServiceContainer.Resolve("messagingService"); + + PageTitle = AppResources.VerifyMasterPassword; + TogglePasswordCommand = new Command(TogglePassword); + } + + public bool ShowPassword + { + get => _showPassword; + set => SetProperty(ref _showPassword, value, + additionalPropertyNames: new string[] + { + nameof(ShowPasswordIcon) + }); + } + + public bool PinLock + { + get => _pinLock; + set => SetProperty(ref _pinLock, value); + } + + public Command TogglePasswordCommand { get; } + public string ShowPasswordIcon => ShowPassword ? "" : ""; + public string MasterPassword { get; set; } + public string Pin { get; set; } + + public async Task InitAsync() + { + _pinSet = await _lockService.IsPinLockSetAsync(); + var hasKey = await _cryptoService.HasKeyAsync(); + PinLock = (_pinSet.Item1 && hasKey) || _pinSet.Item2; + _email = await _userService.GetEmailAsync(); + PageTitle = PinLock ? AppResources.VerifyPIN : AppResources.VerifyMasterPassword; + } + + public async Task SubmitAsync() + { + if(PinLock && string.IsNullOrWhiteSpace(Pin)) + { + await Page.DisplayAlert(AppResources.AnErrorHasOccurred, + string.Format(AppResources.ValidationFieldRequired, AppResources.PIN), + AppResources.Ok); + return; + } + if(!PinLock && string.IsNullOrWhiteSpace(MasterPassword)) + { + await Page.DisplayAlert(AppResources.AnErrorHasOccurred, + string.Format(AppResources.ValidationFieldRequired, AppResources.MasterPassword), + AppResources.Ok); + return; + } + + var kdf = await _userService.GetKdfAsync(); + var kdfIterations = await _userService.GetKdfIterationsAsync(); + + if(PinLock) + { + var failed = true; + try + { + if(_pinSet.Item1) + { + var protectedPin = await _storageService.GetAsync(Constants.ProtectedPin); + var decPin = await _cryptoService.DecryptToUtf8Async(new CipherString(protectedPin)); + failed = decPin != Pin; + _lockService.PinLocked = failed; + if(failed) + { + DoContinue(); + } + } + else + { + var key = await _cryptoService.MakeKeyFromPinAsync(Pin, _email, + kdf.GetValueOrDefault(KdfType.PBKDF2_SHA256), kdfIterations.GetValueOrDefault(5000)); + failed = false; + await SetKeyAndContinueAsync(key); + } + } + catch + { + failed = true; + } + if(failed) + { + _invalidPinAttempts++; + if(_invalidPinAttempts >= 5) + { + _messagingService.Send("logout"); + return; + } + await _platformUtilsService.ShowDialogAsync(AppResources.InvalidPIN, + AppResources.AnErrorHasOccurred); + } + } + else + { + var key = await _cryptoService.MakeKeyAsync(MasterPassword, _email, kdf, kdfIterations); + var keyHash = await _cryptoService.HashPasswordAsync(MasterPassword, key); + var storedKeyHash = await _cryptoService.GetKeyHashAsync(); + if(storedKeyHash != null && keyHash != null && storedKeyHash == keyHash) + { + await SetKeyAndContinueAsync(key); + } + else + { + await _platformUtilsService.ShowDialogAsync(AppResources.InvalidMasterPassword, + AppResources.AnErrorHasOccurred); + } + } + } + + public async Task LogOutAsync() + { + var confirmed = await _platformUtilsService.ShowDialogAsync(AppResources.LogoutConfirmation, + AppResources.LogOut, AppResources.Yes, AppResources.Cancel); + if(confirmed) + { + _messagingService.Send("logout"); + } + } + + public void TogglePassword() + { + ShowPassword = !ShowPassword; + var page = (Page as LockPage); + var entry = PinLock ? page.PinEntry : page.MasterPasswordEntry; + entry.Focus(); + } + + private async Task SetKeyAndContinueAsync(SymmetricCryptoKey key) + { + await _cryptoService.SetKeyAsync(key); + } + + private void DoContinue() + { + _messagingService.Send("unlocked"); + Application.Current.MainPage = new TabsPage(); + } + } +} diff --git a/src/App/Resources/AppResources.Designer.cs b/src/App/Resources/AppResources.Designer.cs index a61d3c8a4..1c77242c8 100644 --- a/src/App/Resources/AppResources.Designer.cs +++ b/src/App/Resources/AppResources.Designer.cs @@ -2850,6 +2850,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to PIN. + /// + public static string PIN { + get { + return ResourceManager.GetString("PIN", resourceCulture); + } + } + /// /// Looks up a localized string similar to Possible Matching Items. /// diff --git a/src/App/Resources/AppResources.resx b/src/App/Resources/AppResources.resx index 312a0c724..488441cfb 100644 --- a/src/App/Resources/AppResources.resx +++ b/src/App/Resources/AppResources.resx @@ -1484,4 +1484,7 @@ Lock Now + + PIN + \ No newline at end of file diff --git a/src/Core/Services/SettingsService.cs b/src/Core/Services/SettingsService.cs index 7f607c86f..89176c907 100644 --- a/src/Core/Services/SettingsService.cs +++ b/src/Core/Services/SettingsService.cs @@ -24,7 +24,7 @@ namespace Bit.Core.Services public void ClearCache() { - _settingsCache.Clear(); + _settingsCache?.Clear(); _settingsCache = null; }