diff --git a/src/Android/MainActivity.cs b/src/Android/MainActivity.cs index 8e4d2432a..e31860b36 100644 --- a/src/Android/MainActivity.cs +++ b/src/Android/MainActivity.cs @@ -51,7 +51,7 @@ namespace Bit.Android Console.WriteLine("A OnCreate"); Window.SetSoftInputMode(SoftInput.StateHidden); - //Window.AddFlags(WindowManagerFlags.Secure); + Window.AddFlags(WindowManagerFlags.Secure); var appIdService = Resolver.Resolve(); var authService = Resolver.Resolve(); diff --git a/src/Android/MainApplication.cs b/src/Android/MainApplication.cs index 4b3092e64..6f308cd37 100644 --- a/src/Android/MainApplication.cs +++ b/src/Android/MainApplication.cs @@ -234,6 +234,7 @@ namespace Bit.Android container.RegisterSingleton(); container.RegisterSingleton(); container.RegisterSingleton(); + container.RegisterSingleton(); // Other container.RegisterSingleton(CrossSettings.Current); diff --git a/src/App/Abstractions/Repositories/ITwoFactorApiRepository.cs b/src/App/Abstractions/Repositories/ITwoFactorApiRepository.cs new file mode 100644 index 000000000..73fa2221c --- /dev/null +++ b/src/App/Abstractions/Repositories/ITwoFactorApiRepository.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using Bit.App.Models.Api; + +namespace Bit.App.Abstractions +{ + public interface ITwoFactorApiRepository + { + Task PostSendEmailLoginAsync(TwoFactorEmailRequest requestObj); + } +} \ No newline at end of file diff --git a/src/App/App.csproj b/src/App/App.csproj index 431572c5c..0d829ff2e 100644 --- a/src/App/App.csproj +++ b/src/App/App.csproj @@ -35,6 +35,7 @@ 4 + @@ -97,6 +98,7 @@ + @@ -135,7 +137,6 @@ - @@ -159,6 +160,7 @@ + diff --git a/src/App/Models/Api/Request/TwoFactorEmailRequest.cs b/src/App/Models/Api/Request/TwoFactorEmailRequest.cs new file mode 100644 index 000000000..5d834b6bc --- /dev/null +++ b/src/App/Models/Api/Request/TwoFactorEmailRequest.cs @@ -0,0 +1,8 @@ +namespace Bit.App.Models.Api +{ + public class TwoFactorEmailRequest + { + public string Email { get; set; } + public string MasterPasswordHash { get; set; } + } +} diff --git a/src/App/Pages/LoginTwoFactorPage.cs b/src/App/Pages/LoginTwoFactorPage.cs index 6e9439dfc..e6067c260 100644 --- a/src/App/Pages/LoginTwoFactorPage.cs +++ b/src/App/Pages/LoginTwoFactorPage.cs @@ -11,7 +11,6 @@ using Bit.App.Models; using Bit.App.Utilities; using Bit.App.Enums; using System.Collections.Generic; -using System.Linq; using System.Net; using FFImageLoading.Forms; @@ -25,12 +24,13 @@ namespace Bit.App.Pages private ISyncService _syncService; private IDeviceInfoService _deviceInfoService; private IGoogleAnalyticsService _googleAnalyticsService; + private ITwoFactorApiRepository _twoFactorApiRepository; private IPushNotification _pushNotification; private readonly string _email; private readonly string _masterPasswordHash; private readonly SymmetricCryptoKey _key; private readonly Dictionary> _providers; - private readonly TwoFactorProviderType? _providerType; + private TwoFactorProviderType? _providerType; private readonly FullLoginResult _result; public LoginTwoFactorPage(string email, FullLoginResult result, TwoFactorProviderType? type = null) @@ -49,11 +49,10 @@ namespace Bit.App.Pages _userDialogs = Resolver.Resolve(); _syncService = Resolver.Resolve(); _googleAnalyticsService = Resolver.Resolve(); + _twoFactorApiRepository = Resolver.Resolve(); _pushNotification = Resolver.Resolve(); Init(); - - SubscribeYubiKey(true); } public FormEntryCell TokenCell { get; set; } @@ -61,6 +60,13 @@ namespace Bit.App.Pages private void Init() { + SubscribeYubiKey(true); + if(_providers.Count > 1) + { + var sendEmailTask = SendEmailAsync(false); + } + + ToolbarItems.Clear(); var scrollView = new ScrollView(); var anotherMethodButton = new ExtendedButton @@ -70,7 +76,8 @@ namespace Bit.App.Pages Margin = new Thickness(15, 0, 15, 25), Command = new Command(() => AnotherMethodAsync()), Uppercase = false, - BackgroundColor = Color.Transparent + BackgroundColor = Color.Transparent, + VerticalOptions = LayoutOptions.Start }; var instruction = new Label @@ -107,7 +114,7 @@ namespace Bit.App.Pages var continueToolbarItem = new ToolbarItem(AppResources.Continue, null, async () => { var token = TokenCell?.Entry.Text.Trim().Replace(" ", ""); - await LogInAsync(token, RememberCell.On); + await LogInAsync(token); }, ToolbarItemOrder.Default, 0); var padding = Helpers.OnPlatform( @@ -130,7 +137,7 @@ namespace Bit.App.Pages var layout = new StackLayout { - Children = { instruction, table, anotherMethodButton }, + Children = { instruction, table }, Spacing = 0 }; @@ -140,27 +147,24 @@ namespace Bit.App.Pages { case TwoFactorProviderType.Authenticator: instruction.Text = "Enter the 6 digit verification code from your authenticator app."; - layout.Children.Add(instruction); - layout.Children.Add(table); layout.Children.Add(anotherMethodButton); break; case TwoFactorProviderType.Email: var emailParams = _providers[TwoFactorProviderType.Email]; var redactedEmail = emailParams["Email"].ToString(); - instruction.Text = "Enter the 6 digit verification code from your authenticator app."; + instruction.Text = $"Enter the 6 digit verification code that was emailed to {redactedEmail}."; var resendEmailButton = new ExtendedButton { - Text = $"Enter the 6 digit verification code that was emailed to {redactedEmail}.", + Text = "Send verification code email again", Style = (Style)Application.Current.Resources["btn-primaryAccent"], - Margin = new Thickness(15, 0, 15, 25), - Command = new Command(() => SendEmail()), + Margin = new Thickness(15, 0, 15, 0), + Command = new Command(async () => await SendEmailAsync(true)), Uppercase = false, - BackgroundColor = Color.Transparent + BackgroundColor = Color.Transparent, + VerticalOptions = LayoutOptions.Start }; - layout.Children.Add(instruction); - layout.Children.Add(table); layout.Children.Add(resendEmailButton); layout.Children.Add(anotherMethodButton); break; @@ -184,15 +188,30 @@ namespace Bit.App.Pages { Uri = $"http://192.168.1.6:4001/duo-mobile.html?host={host}&request={req}", HorizontalOptions = LayoutOptions.FillAndExpand, - VerticalOptions = LayoutOptions.FillAndExpand + VerticalOptions = LayoutOptions.FillAndExpand, + MinimumHeightRequest = 400 }; webView.RegisterAction(async (sig) => { - await LogInAsync(sig, false); + await LogInAsync(sig); }); + var table = new TwoFactorTable( + new TableSection(" ") + { + RememberCell + }); + + var layout = new StackLayout + { + Children = { webView, table, anotherMethodButton }, + Spacing = 0 + }; + + scrollView.Content = layout; + Title = "Duo"; - Content = webView; + Content = scrollView; } else if(_providerType == TwoFactorProviderType.YubiKey) { @@ -254,28 +273,99 @@ namespace Bit.App.Pages private async void AnotherMethodAsync() { - await Navigation.PushForDeviceAsync(new TwoFactorMethodsPage(_email, _result)); + var beforeProviderType = _providerType; + + var options = new List(); + if(_providers.ContainsKey(TwoFactorProviderType.Authenticator)) + { + options.Add("Authenticator App"); + } + + if(_providers.ContainsKey(TwoFactorProviderType.Duo)) + { + options.Add("Duo"); + } + + if(_providers.ContainsKey(TwoFactorProviderType.YubiKey)) + { + var nfcKey = _providers[TwoFactorProviderType.YubiKey].ContainsKey("Nfc") && + (bool)_providers[TwoFactorProviderType.YubiKey]["Nfc"]; + if(_deviceInfoService.NfcEnabled || nfcKey) + { + options.Add("YubiKey NFC Security Key"); + } + } + + if(_providers.ContainsKey(TwoFactorProviderType.Email)) + { + options.Add("Email"); + } + + options.Add("Recovery Code"); + + var selection = await DisplayActionSheet("Two-step Login Options", AppResources.Cancel, null, options.ToArray()); + if(selection == "Authenticator App") + { + _providerType = TwoFactorProviderType.Authenticator; + } + else if(selection == "Duo") + { + _providerType = TwoFactorProviderType.Duo; + } + else if(selection == "YubiKey NFC Security Key") + { + _providerType = TwoFactorProviderType.YubiKey; + } + else if(selection == "Email") + { + _providerType = TwoFactorProviderType.Email; + } + else if(selection == "Recovery Code") + { + Device.OpenUri(new Uri("https://help.bitwarden.com/article/lost-two-step-device/")); + return; + } + + if(beforeProviderType != _providerType) + { + Init(); + ListenYubiKey(false, beforeProviderType == TwoFactorProviderType.YubiKey); + ListenYubiKey(true); + } } - private void SendEmail() + private async Task SendEmailAsync(bool doToast) { + if(_providerType != TwoFactorProviderType.Email) + { + return; + } - } + var response = await _twoFactorApiRepository.PostSendEmailLoginAsync(new Models.Api.TwoFactorEmailRequest + { + Email = _email, + MasterPasswordHash = _masterPasswordHash + }); - private void Recover() - { - Device.OpenUri(new Uri("https://help.bitwarden.com/article/lost-two-step-device/")); + if(response.Succeeded && doToast) + { + _userDialogs.Toast("Verification email sent."); + } + else if(!response.Succeeded) + { + _userDialogs.Alert("Could not send verification email. Try again."); + } } private async void Entry_Completed(object sender, EventArgs e) { var token = TokenCell.Entry.Text.Trim().Replace(" ", ""); - await LogInAsync(token, RememberCell.On); + await LogInAsync(token); } - private async Task LogInAsync(string token, bool remember) + private async Task LogInAsync(string token) { - if(_lastAction.LastActionWasRecent()) + if(!_providerType.HasValue || _lastAction.LastActionWasRecent()) { return; } @@ -288,8 +378,8 @@ namespace Bit.App.Pages return; } - _userDialogs.ShowLoading(AppResources.ValidatingCode, MaskType.Black); - var response = await _authService.TokenPostTwoFactorAsync(_providerType.Value, token, remember, + _userDialogs.ShowLoading(string.Concat(AppResources.Validating, "..."), MaskType.Black); + var response = await _authService.TokenPostTwoFactorAsync(_providerType.Value, token, RememberCell.On, _email, _masterPasswordHash, _key); _userDialogs.HideLoading(); if(!response.Success) @@ -299,7 +389,7 @@ namespace Bit.App.Pages return; } - _googleAnalyticsService.TrackAppEvent("LoggedIn From Two-step"); + _googleAnalyticsService.TrackAppEvent("LoggedIn From Two-step", _providerType.Value.ToString()); if(Device.RuntimePlatform == Device.Android) { @@ -320,11 +410,6 @@ namespace Bit.App.Pages if(_providers != null) { - if(_providers.Count == 1) - { - return _providers.First().Key; - } - foreach(var p in _providers) { switch(p.Key) @@ -348,7 +433,8 @@ namespace Bit.App.Pages } break; case TwoFactorProviderType.YubiKey: - if(!_deviceInfoService.NfcEnabled) + var nfcKey = p.Value.ContainsKey("Nfc") && (bool)p.Value["Nfc"]; + if(!_deviceInfoService.NfcEnabled || !nfcKey) { continue; } @@ -364,9 +450,9 @@ namespace Bit.App.Pages return provider; } - private void ListenYubiKey(bool listen) + private void ListenYubiKey(bool listen, bool overrideCheck = false) { - if(_providerType == TwoFactorProviderType.YubiKey) + if(_providerType == TwoFactorProviderType.YubiKey || overrideCheck) { MessagingCenter.Send(Application.Current, "ListenYubiKeyOTP", listen); } @@ -391,7 +477,7 @@ namespace Bit.App.Pages MessagingCenter.Unsubscribe(Application.Current, "GotYubiKeyOTP"); if(_providerType == TwoFactorProviderType.YubiKey) { - await LogInAsync(otp, RememberCell.On); + await LogInAsync(otp); } }); diff --git a/src/App/Pages/TwoFactorMethodsPage.cs b/src/App/Pages/TwoFactorMethodsPage.cs deleted file mode 100644 index a46c0090a..000000000 --- a/src/App/Pages/TwoFactorMethodsPage.cs +++ /dev/null @@ -1,141 +0,0 @@ -using System; -using Bit.App.Controls; -using Xamarin.Forms; -using Bit.App.Models; - -namespace Bit.App.Pages -{ - public class TwoFactorMethodsPage : ExtendedContentPage - { - private readonly string _email; - private readonly FullLoginResult _result; - - public TwoFactorMethodsPage(string email, FullLoginResult result) - : base(updateActivity: false) - { - _email = email; - _result = result; - - Init(); - } - - public ExtendedTextCell AuthenticatorCell { get; set; } - public ExtendedTextCell EmailCell { get; set; } - public ExtendedTextCell DuoCell { get; set; } - public ExtendedTextCell RecoveryCell { get; set; } - - private void Init() - { - var section = new TableSection(" "); - - if(_result.TwoFactorProviders.ContainsKey(Enums.TwoFactorProviderType.Authenticator)) - { - AuthenticatorCell = new ExtendedTextCell - { - Text = "Authenticator App", - Detail = "Use an authenticator app (such as Authy or Google Authenticator) to generate time-based verification codes." - }; - section.Add(AuthenticatorCell); - } - - if(_result.TwoFactorProviders.ContainsKey(Enums.TwoFactorProviderType.Duo)) - { - DuoCell = new ExtendedTextCell - { - Text = "Duo", - Detail = "Use duo." - }; - section.Add(DuoCell); - } - - if(_result.TwoFactorProviders.ContainsKey(Enums.TwoFactorProviderType.Email)) - { - EmailCell = new ExtendedTextCell - { - Text = "Email", - Detail = "Verification codes will be emailed to you." - }; - section.Add(EmailCell); - } - - RecoveryCell = new ExtendedTextCell - { - Text = "Recovery Code", - Detail = "Lost access to all of your two-factor providers? Use your recovery code to disable all two-factor providers from your account." - }; - section.Add(RecoveryCell); - - var table = new ExtendedTableView - { - EnableScrolling = true, - Intent = TableIntent.Settings, - HasUnevenRows = true, - Root = new TableRoot - { - section - } - }; - - if(Device.RuntimePlatform == Device.iOS) - { - table.RowHeight = -1; - table.EstimatedRowHeight = 100; - } - - Title = "Two-step Login Options"; - Content = table; - } - - protected override void OnAppearing() - { - base.OnAppearing(); - if(AuthenticatorCell != null) - { - AuthenticatorCell.Tapped += AuthenticatorCell_Tapped; - } - if(DuoCell != null) - { - DuoCell.Tapped += DuoCell_Tapped; - } - if(EmailCell != null) - { - EmailCell.Tapped += EmailCell_Tapped; - } - RecoveryCell.Tapped += RecoveryCell_Tapped; - } - - protected override void OnDisappearing() - { - base.OnDisappearing(); - if(AuthenticatorCell != null) - { - AuthenticatorCell.Tapped -= AuthenticatorCell_Tapped; - } - if(DuoCell != null) - { - DuoCell.Tapped -= DuoCell_Tapped; - } - if(EmailCell != null) - { - EmailCell.Tapped -= EmailCell_Tapped; - } - RecoveryCell.Tapped -= RecoveryCell_Tapped; - } - - private void AuthenticatorCell_Tapped(object sender, EventArgs e) - { - } - - private void RecoveryCell_Tapped(object sender, EventArgs e) - { - } - - private void EmailCell_Tapped(object sender, EventArgs e) - { - } - - private void DuoCell_Tapped(object sender, EventArgs e) - { - } - } -} diff --git a/src/App/Repositories/TwoFactorApiRepository.cs b/src/App/Repositories/TwoFactorApiRepository.cs new file mode 100644 index 000000000..8540d0f64 --- /dev/null +++ b/src/App/Repositories/TwoFactorApiRepository.cs @@ -0,0 +1,53 @@ +using System; +using System.Net.Http; +using System.Threading.Tasks; +using Bit.App.Abstractions; +using Bit.App.Models.Api; +using Plugin.Connectivity.Abstractions; + +namespace Bit.App.Repositories +{ + public class TwoFactorApiRepository : BaseApiRepository, ITwoFactorApiRepository + { + public TwoFactorApiRepository( + IConnectivity connectivity, + IHttpService httpService, + ITokenService tokenService) + : base(connectivity, httpService, tokenService) + { } + + protected override string ApiRoute => "two-factor"; + + public virtual async Task PostSendEmailLoginAsync(TwoFactorEmailRequest requestObj) + { + if(!Connectivity.IsConnected) + { + return HandledNotConnected(); + } + + using(var client = HttpService.ApiClient) + { + var requestMessage = new TokenHttpRequestMessage(requestObj) + { + Method = HttpMethod.Post, + RequestUri = new Uri(client.BaseAddress, string.Concat(ApiRoute, "/send-email-login")), + }; + + try + { + var response = await client.SendAsync(requestMessage).ConfigureAwait(false); + if(!response.IsSuccessStatusCode) + { + return await HandleErrorAsync(response).ConfigureAwait(false); + } + + return ApiResult.Success(response.StatusCode); + } + catch + { + return HandledWebException(); + } + } + } + } +} diff --git a/src/App/Resources/AppResources.Designer.cs b/src/App/Resources/AppResources.Designer.cs index 1deef147c..47457fbe6 100644 --- a/src/App/Resources/AppResources.Designer.cs +++ b/src/App/Resources/AppResources.Designer.cs @@ -1943,11 +1943,11 @@ namespace Bit.App.Resources { } /// - /// Looks up a localized string similar to Validating code.... + /// Looks up a localized string similar to Validating. /// - public static string ValidatingCode { + public static string Validating { get { - return ResourceManager.GetString("ValidatingCode", resourceCulture); + return ResourceManager.GetString("Validating", resourceCulture); } } diff --git a/src/App/Resources/AppResources.resx b/src/App/Resources/AppResources.resx index 4fcdcb7ab..81e8cabf3 100644 --- a/src/App/Resources/AppResources.resx +++ b/src/App/Resources/AppResources.resx @@ -728,8 +728,8 @@ Unlock with PIN Code - - Validating code... + + Validating Message shown when interacting with the server diff --git a/src/iOS/AppDelegate.cs b/src/iOS/AppDelegate.cs index 306f0bff1..bdf7fd14a 100644 --- a/src/iOS/AppDelegate.cs +++ b/src/iOS/AppDelegate.cs @@ -280,6 +280,7 @@ namespace Bit.iOS container.RegisterSingleton(); container.RegisterSingleton(); container.RegisterSingleton(); + container.RegisterSingleton(); // Other container.RegisterSingleton(CrossConnectivity.Current);