diff --git a/src/App/Pages/Accounts/TwoFactorPage.xaml.cs b/src/App/Pages/Accounts/TwoFactorPage.xaml.cs index edd5a4597..ba2a82d1c 100644 --- a/src/App/Pages/Accounts/TwoFactorPage.xaml.cs +++ b/src/App/Pages/Accounts/TwoFactorPage.xaml.cs @@ -1,5 +1,4 @@ using System; -using Xamarin.Forms; namespace Bit.App.Pages { @@ -14,25 +13,33 @@ namespace Bit.App.Pages _vm.Page = this; } - protected override async void OnAppearing() + protected override void OnAppearing() { base.OnAppearing(); - await _vm.InitAsync(); + _vm.Init(); } - private void Continue_Clicked(object sender, EventArgs e) + private async void Continue_Clicked(object sender, EventArgs e) { if(DoOnce()) { - + await _vm.SubmitAsync(); } } - private void Methods_Clicked(object sender, EventArgs e) + private async void Methods_Clicked(object sender, EventArgs e) { if(DoOnce()) { + await _vm.AnotherMethodAsync(); + } + } + private async void ResendEmail_Clicked(object sender, EventArgs e) + { + if(DoOnce()) + { + await _vm.SendEmailAsync(true, true); } } } diff --git a/src/App/Pages/Accounts/TwoFactorPageViewModel.cs b/src/App/Pages/Accounts/TwoFactorPageViewModel.cs index f9812c296..14497d4a2 100644 --- a/src/App/Pages/Accounts/TwoFactorPageViewModel.cs +++ b/src/App/Pages/Accounts/TwoFactorPageViewModel.cs @@ -1,8 +1,11 @@ using Bit.App.Abstractions; using Bit.App.Resources; using Bit.Core.Abstractions; +using Bit.Core.Enums; using Bit.Core.Exceptions; +using Bit.Core.Models.Request; using Bit.Core.Utilities; +using System.Linq; using System.Threading.Tasks; using Xamarin.Forms; @@ -14,8 +17,12 @@ namespace Bit.App.Pages private readonly IAuthService _authService; private readonly ISyncService _syncService; private readonly IStorageService _storageService; + private readonly IApiService _apiService; + private readonly IPlatformUtilsService _platformUtilsService; - private string _email; + private bool _u2fSupported = false; + private TwoFactorProviderType? _selectedProviderType; + private string _twoFactorEmail; public TwoFactorPageViewModel() { @@ -23,22 +30,178 @@ namespace Bit.App.Pages _authService = ServiceContainer.Resolve("authService"); _syncService = ServiceContainer.Resolve("syncService"); _storageService = ServiceContainer.Resolve("storageService"); + _apiService = ServiceContainer.Resolve("apiService"); + _platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); } - public string Email + public string TwoFactorEmail { - get => _email; - set => SetProperty(ref _email, value); + get => _twoFactorEmail; + set => SetProperty(ref _twoFactorEmail, value); } - public async Task InitAsync() - { + public bool Remember { get; set; } + public string Token { get; set; } + + public bool DuoMethod => SelectedProviderType == TwoFactorProviderType.Email; + + public bool YubikeyMethod => SelectedProviderType == TwoFactorProviderType.YubiKey; + + public bool AuthenticatorMethod => SelectedProviderType == TwoFactorProviderType.Authenticator; + + public bool EmailMethod => SelectedProviderType == TwoFactorProviderType.Email; + + public TwoFactorProviderType? SelectedProviderType + { + get => _selectedProviderType; + set => SetProperty(ref _selectedProviderType, value, additionalPropertyNames: new string[] + { + nameof(EmailMethod), + nameof(DuoMethod), + nameof(YubikeyMethod), + nameof(AuthenticatorMethod) + }); + } + + public void Init() + { + if(string.IsNullOrWhiteSpace(_authService.Email) || + string.IsNullOrWhiteSpace(_authService.MasterPasswordHash) || + _authService.TwoFactorProvidersData == null) + { + // TODO: dismiss modal? + return; + } + + // TODO: init U2F + _u2fSupported = false; + + var selectedProviderType = _authService.GetDefaultTwoFactorProvider(_u2fSupported); + Load(); + } + + public void Load() + { + if(SelectedProviderType == null) + { + PageTitle = AppResources.LoginUnavailable; + return; + } + PageTitle = _authService.TwoFactorProviders[SelectedProviderType.Value].Name; + var providerData = _authService.TwoFactorProvidersData[SelectedProviderType.Value]; + switch(SelectedProviderType.Value) + { + case TwoFactorProviderType.U2f: + // TODO + break; + case TwoFactorProviderType.Duo: + case TwoFactorProviderType.OrganizationDuo: + // TODO: init duo + var host = providerData["Host"] as string; + var signature = providerData["Signature"] as string; + break; + case TwoFactorProviderType.Email: + TwoFactorEmail = providerData["Email"] as string; + if(_authService.TwoFactorProvidersData.Count > 1) + { + var emailTask = Task.Run(() => SendEmailAsync(false, false)); + } + break; + default: + break; + } } public async Task SubmitAsync() { + if(SelectedProviderType == null) + { + return; + } + if(string.IsNullOrWhiteSpace(Token)) + { + await _platformUtilsService.ShowDialogAsync( + string.Format(AppResources.ValidationFieldRequired, AppResources.VerificationCode), + AppResources.AnErrorHasOccurred); + } + if(SelectedProviderType == TwoFactorProviderType.Email || + SelectedProviderType == TwoFactorProviderType.Authenticator) + { + Token = Token.Replace(" ", string.Empty).Trim(); + } + try + { + await _deviceActionService.ShowLoadingAsync(AppResources.LoggingIn); + await _authService.LogInTwoFactorAsync(SelectedProviderType.Value, Token, Remember); + await _deviceActionService.HideLoadingAsync(); + var task = Task.Run(() => _syncService.FullSyncAsync(true)); + Application.Current.MainPage = new TabsPage(); + } + catch(ApiException e) + { + await _deviceActionService.HideLoadingAsync(); + await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(), + AppResources.AnErrorHasOccurred); + } + } + + public async Task AnotherMethodAsync() + { + var supportedProviders = _authService.GetSupportedTwoFactorProviders(); + var options = supportedProviders.Select(p => p.Name).ToList(); + options.Add(AppResources.RecoveryCodeTitle); + var method = await Page.DisplayActionSheet(AppResources.TwoStepLoginOptions, AppResources.Cancel, + null, options.ToArray()); + if(method == AppResources.RecoveryCodeTitle) + { + _platformUtilsService.LaunchUri("https://help.bitwarden.com/article/lost-two-step-device/"); + } + else + { + SelectedProviderType = supportedProviders.FirstOrDefault(p => p.Name == method)?.Type; + Load(); + } + } + + public async Task SendEmailAsync(bool showLoading, bool doToast) + { + if(SelectedProviderType != TwoFactorProviderType.Email) + { + return false; + } + try + { + if(showLoading) + { + await _deviceActionService.ShowLoadingAsync(AppResources.Submitting); + } + var request = new TwoFactorEmailRequest + { + Email = _authService.Email, + MasterPasswordHash = _authService.MasterPasswordHash + }; + await _apiService.PostTwoFactorEmailAsync(request); + if(showLoading) + { + await _deviceActionService.HideLoadingAsync(); + } + if(doToast) + { + _platformUtilsService.ShowToast("success", null, AppResources.VerificationEmailSent); + } + return true; + } + catch(ApiException) + { + if(showLoading) + { + await _deviceActionService.HideLoadingAsync(); + } + await _platformUtilsService.ShowDialogAsync(AppResources.VerificationEmailNotSent); + return false; + } } } } diff --git a/src/Core/Abstractions/IApiService.cs b/src/Core/Abstractions/IApiService.cs index 049f01982..e8dae133b 100644 --- a/src/Core/Abstractions/IApiService.cs +++ b/src/Core/Abstractions/IApiService.cs @@ -44,5 +44,6 @@ namespace Bit.Core.Abstractions Task PostShareCipherAttachmentAsync(string id, string attachmentId, MultipartFormDataContent data, string organizationId); Task> GetHibpBreachAsync(string username); + Task PostTwoFactorEmailAsync(TwoFactorEmailRequest request); } } diff --git a/src/Core/Abstractions/IAuthService.cs b/src/Core/Abstractions/IAuthService.cs index 83b575314..538abf338 100644 --- a/src/Core/Abstractions/IAuthService.cs +++ b/src/Core/Abstractions/IAuthService.cs @@ -11,7 +11,8 @@ namespace Bit.Core.Abstractions string Email { get; set; } string MasterPasswordHash { get; set; } TwoFactorProviderType? SelectedTwoFactorProviderType { get; set; } - Dictionary> TwoFactorProviders { get; set; } + Dictionary TwoFactorProviders { get; set; } + Dictionary> TwoFactorProvidersData { get; set; } TwoFactorProviderType? GetDefaultTwoFactorProvider(bool u2fSupported); List GetSupportedTwoFactorProviders(); diff --git a/src/Core/Models/Request/TwoFactorEmailRequest.cs b/src/Core/Models/Request/TwoFactorEmailRequest.cs new file mode 100644 index 000000000..3446df586 --- /dev/null +++ b/src/Core/Models/Request/TwoFactorEmailRequest.cs @@ -0,0 +1,8 @@ +namespace Bit.Core.Models.Request +{ + public class TwoFactorEmailRequest + { + public string Email { get; set; } + public string MasterPasswordHash { get; set; } + } +} diff --git a/src/Core/Services/ApiService.cs b/src/Core/Services/ApiService.cs index 3bc42479b..cb44fc70a 100644 --- a/src/Core/Services/ApiService.cs +++ b/src/Core/Services/ApiService.cs @@ -271,6 +271,16 @@ namespace Bit.Core.Services #endregion + #region Two Factor APIs + + public Task PostTwoFactorEmailAsync(TwoFactorEmailRequest request) + { + return SendAsync( + HttpMethod.Post, "/two-factor/send-email-login", request, false, false); + } + + #endregion + #region HIBP APIs public Task> GetHibpBreachAsync(string username) diff --git a/src/Core/Services/AuthService.cs b/src/Core/Services/AuthService.cs index 8b0472e5e..4d5a24ffa 100644 --- a/src/Core/Services/AuthService.cs +++ b/src/Core/Services/AuthService.cs @@ -24,7 +24,6 @@ namespace Bit.Core.Services private SymmetricCryptoKey _key; private KdfType? _kdf; private int? _kdfIterations; - private Dictionary _twoFactorProviders; public AuthService( ICryptoService cryptoService, @@ -47,21 +46,21 @@ namespace Bit.Core.Services _messagingService = messagingService; _setCryptoKeys = setCryptoKeys; - _twoFactorProviders = new Dictionary(); - _twoFactorProviders.Add(TwoFactorProviderType.Authenticator, new TwoFactorProvider + TwoFactorProviders = new Dictionary(); + TwoFactorProviders.Add(TwoFactorProviderType.Authenticator, new TwoFactorProvider { Type = TwoFactorProviderType.Authenticator, Priority = 1, Sort = 1 }); - _twoFactorProviders.Add(TwoFactorProviderType.YubiKey, new TwoFactorProvider + TwoFactorProviders.Add(TwoFactorProviderType.YubiKey, new TwoFactorProvider { Type = TwoFactorProviderType.YubiKey, Priority = 3, Sort = 2, Premium = true }); - _twoFactorProviders.Add(TwoFactorProviderType.Duo, new TwoFactorProvider + TwoFactorProviders.Add(TwoFactorProviderType.Duo, new TwoFactorProvider { Type = TwoFactorProviderType.Duo, Name = "Duo", @@ -69,21 +68,21 @@ namespace Bit.Core.Services Sort = 3, Premium = true }); - _twoFactorProviders.Add(TwoFactorProviderType.OrganizationDuo, new TwoFactorProvider + TwoFactorProviders.Add(TwoFactorProviderType.OrganizationDuo, new TwoFactorProvider { Type = TwoFactorProviderType.OrganizationDuo, Name = "Duo (Organization)", Priority = 10, Sort = 4 }); - _twoFactorProviders.Add(TwoFactorProviderType.U2f, new TwoFactorProvider + TwoFactorProviders.Add(TwoFactorProviderType.U2f, new TwoFactorProvider { Type = TwoFactorProviderType.U2f, Priority = 4, Sort = 5, Premium = true }); - _twoFactorProviders.Add(TwoFactorProviderType.Email, new TwoFactorProvider + TwoFactorProviders.Add(TwoFactorProviderType.Email, new TwoFactorProvider { Type = TwoFactorProviderType.Email, Priority = 0, @@ -93,25 +92,26 @@ namespace Bit.Core.Services public string Email { get; set; } public string MasterPasswordHash { get; set; } - public Dictionary> TwoFactorProviders { get; set; } + public Dictionary TwoFactorProviders { get; set; } + public Dictionary> TwoFactorProvidersData { get; set; } public TwoFactorProviderType? SelectedTwoFactorProviderType { get; set; } public void Init() { - _twoFactorProviders[TwoFactorProviderType.Email].Name = _i18nService.T("EmailTitle"); - _twoFactorProviders[TwoFactorProviderType.Email].Description = _i18nService.T("EmailDesc"); - _twoFactorProviders[TwoFactorProviderType.Authenticator].Name = _i18nService.T("AuthenticatorAppTitle"); - _twoFactorProviders[TwoFactorProviderType.Authenticator].Description = + TwoFactorProviders[TwoFactorProviderType.Email].Name = _i18nService.T("EmailTitle"); + TwoFactorProviders[TwoFactorProviderType.Email].Description = _i18nService.T("EmailDesc"); + TwoFactorProviders[TwoFactorProviderType.Authenticator].Name = _i18nService.T("AuthenticatorAppTitle"); + TwoFactorProviders[TwoFactorProviderType.Authenticator].Description = _i18nService.T("AuthenticatorAppDesc"); - _twoFactorProviders[TwoFactorProviderType.Duo].Description = _i18nService.T("DuoDesc"); - _twoFactorProviders[TwoFactorProviderType.OrganizationDuo].Name = + TwoFactorProviders[TwoFactorProviderType.Duo].Description = _i18nService.T("DuoDesc"); + TwoFactorProviders[TwoFactorProviderType.OrganizationDuo].Name = string.Format("Duo ({0})", _i18nService.T("Organization")); - _twoFactorProviders[TwoFactorProviderType.OrganizationDuo].Description = + TwoFactorProviders[TwoFactorProviderType.OrganizationDuo].Description = _i18nService.T("DuoOrganizationDesc"); - _twoFactorProviders[TwoFactorProviderType.U2f].Name = _i18nService.T("U2fTitle"); - _twoFactorProviders[TwoFactorProviderType.U2f].Description = _i18nService.T("U2fDesc"); - _twoFactorProviders[TwoFactorProviderType.YubiKey].Name = _i18nService.T("YubiKeyTitle"); - _twoFactorProviders[TwoFactorProviderType.YubiKey].Description = _i18nService.T("YubiKeyDesc"); + TwoFactorProviders[TwoFactorProviderType.U2f].Name = _i18nService.T("U2fTitle"); + TwoFactorProviders[TwoFactorProviderType.U2f].Description = _i18nService.T("U2fDesc"); + TwoFactorProviders[TwoFactorProviderType.YubiKey].Name = _i18nService.T("YubiKeyTitle"); + TwoFactorProviders[TwoFactorProviderType.YubiKey].Description = _i18nService.T("YubiKeyDesc"); } public async Task LogInAsync(string email, string masterPassword) @@ -146,56 +146,56 @@ namespace Bit.Core.Services public List GetSupportedTwoFactorProviders() { var providers = new List(); - if(TwoFactorProviders == null) + if(TwoFactorProvidersData == null) { return providers; } - if(TwoFactorProviders.ContainsKey(TwoFactorProviderType.OrganizationDuo) && + if(TwoFactorProvidersData.ContainsKey(TwoFactorProviderType.OrganizationDuo) && _platformUtilsService.SupportsDuo()) { - providers.Add(_twoFactorProviders[TwoFactorProviderType.OrganizationDuo]); + providers.Add(TwoFactorProviders[TwoFactorProviderType.OrganizationDuo]); } - if(TwoFactorProviders.ContainsKey(TwoFactorProviderType.Authenticator)) + if(TwoFactorProvidersData.ContainsKey(TwoFactorProviderType.Authenticator)) { - providers.Add(_twoFactorProviders[TwoFactorProviderType.Authenticator]); + providers.Add(TwoFactorProviders[TwoFactorProviderType.Authenticator]); } - if(TwoFactorProviders.ContainsKey(TwoFactorProviderType.YubiKey)) + if(TwoFactorProvidersData.ContainsKey(TwoFactorProviderType.YubiKey)) { - providers.Add(_twoFactorProviders[TwoFactorProviderType.YubiKey]); + providers.Add(TwoFactorProviders[TwoFactorProviderType.YubiKey]); } - if(TwoFactorProviders.ContainsKey(TwoFactorProviderType.Duo) && _platformUtilsService.SupportsDuo()) + if(TwoFactorProvidersData.ContainsKey(TwoFactorProviderType.Duo) && _platformUtilsService.SupportsDuo()) { - providers.Add(_twoFactorProviders[TwoFactorProviderType.Duo]); + providers.Add(TwoFactorProviders[TwoFactorProviderType.Duo]); } - if(TwoFactorProviders.ContainsKey(TwoFactorProviderType.U2f) && _platformUtilsService.SupportsU2f()) + if(TwoFactorProvidersData.ContainsKey(TwoFactorProviderType.U2f) && _platformUtilsService.SupportsU2f()) { - providers.Add(_twoFactorProviders[TwoFactorProviderType.U2f]); + providers.Add(TwoFactorProviders[TwoFactorProviderType.U2f]); } - if(TwoFactorProviders.ContainsKey(TwoFactorProviderType.Email)) + if(TwoFactorProvidersData.ContainsKey(TwoFactorProviderType.Email)) { - providers.Add(_twoFactorProviders[TwoFactorProviderType.Email]); + providers.Add(TwoFactorProviders[TwoFactorProviderType.Email]); } return providers; } public TwoFactorProviderType? GetDefaultTwoFactorProvider(bool u2fSupported) { - if(TwoFactorProviders == null) + if(TwoFactorProvidersData == null) { return null; } if(SelectedTwoFactorProviderType != null && - TwoFactorProviders.ContainsKey(SelectedTwoFactorProviderType.Value)) + TwoFactorProvidersData.ContainsKey(SelectedTwoFactorProviderType.Value)) { return SelectedTwoFactorProviderType.Value; } TwoFactorProviderType? providerType = null; var providerPriority = -1; - foreach(var providerKvp in TwoFactorProviders) + foreach(var providerKvp in TwoFactorProvidersData) { - if(_twoFactorProviders.ContainsKey(providerKvp.Key)) + if(TwoFactorProviders.ContainsKey(providerKvp.Key)) { - var provider = _twoFactorProviders[providerKvp.Key]; + var provider = TwoFactorProviders[providerKvp.Key]; if(provider.Priority > providerPriority) { if(providerKvp.Key == TwoFactorProviderType.U2f && !u2fSupported) @@ -274,7 +274,7 @@ namespace Bit.Core.Services Email = email; MasterPasswordHash = hashedPassword; _key = _setCryptoKeys ? key : null; - TwoFactorProviders = twoFactorResponse.TwoFactorProviders2; + TwoFactorProvidersData = twoFactorResponse.TwoFactorProviders2; result.TwoFactorProviders = twoFactorResponse.TwoFactorProviders2; return result; } @@ -320,7 +320,7 @@ namespace Bit.Core.Services { Email = null; MasterPasswordHash = null; - TwoFactorProviders = null; + TwoFactorProvidersData = null; SelectedTwoFactorProviderType = null; } }