using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Threading.Tasks; using Bit.App.Abstractions; using Bit.App.Resources; using Bit.App.Utilities; using Bit.Core.Abstractions; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Request; using Bit.Core.Utilities; using Newtonsoft.Json; using Xamarin.Essentials; using Xamarin.Forms; namespace Bit.App.Pages { public class TwoFactorPageViewModel : CaptchaProtectedViewModel { private readonly IDeviceActionService _deviceActionService; private readonly IAuthService _authService; private readonly ISyncService _syncService; private readonly IApiService _apiService; private readonly IPlatformUtilsService _platformUtilsService; private readonly IEnvironmentService _environmentService; private readonly IMessagingService _messagingService; private readonly IBroadcasterService _broadcasterService; private readonly IStateService _stateService; private readonly II18nService _i18nService; private TwoFactorProviderType? _selectedProviderType; private string _totpInstruction; private string _webVaultUrl = "https://vault.bitwarden.com"; private bool _authingWithSso = false; private bool _enableContinue = false; private bool _showContinue = true; public TwoFactorPageViewModel() { _deviceActionService = ServiceContainer.Resolve("deviceActionService"); _authService = ServiceContainer.Resolve("authService"); _syncService = ServiceContainer.Resolve("syncService"); _apiService = ServiceContainer.Resolve("apiService"); _platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); _environmentService = ServiceContainer.Resolve("environmentService"); _messagingService = ServiceContainer.Resolve("messagingService"); _broadcasterService = ServiceContainer.Resolve("broadcasterService"); _stateService = ServiceContainer.Resolve("stateService"); _i18nService = ServiceContainer.Resolve("i18nService"); PageTitle = AppResources.TwoStepLogin; SubmitCommand = new Command(async () => await SubmitAsync()); } public string TotpInstruction { get => _totpInstruction; set => SetProperty(ref _totpInstruction, value); } public bool Remember { get; set; } public string Token { get; set; } public bool DuoMethod => SelectedProviderType == TwoFactorProviderType.Duo || SelectedProviderType == TwoFactorProviderType.OrganizationDuo; public bool Fido2Method => SelectedProviderType == TwoFactorProviderType.Fido2WebAuthn; public bool YubikeyMethod => SelectedProviderType == TwoFactorProviderType.YubiKey; public bool AuthenticatorMethod => SelectedProviderType == TwoFactorProviderType.Authenticator; public bool EmailMethod => SelectedProviderType == TwoFactorProviderType.Email; public bool TotpMethod => AuthenticatorMethod || EmailMethod; public bool ShowTryAgain => (YubikeyMethod && Device.RuntimePlatform == Device.iOS) || Fido2Method; public bool ShowContinue { get => _showContinue; set => SetProperty(ref _showContinue, value); } public bool EnableContinue { get => _enableContinue; set => SetProperty(ref _enableContinue, value); } public string YubikeyInstruction => Device.RuntimePlatform == Device.iOS ? AppResources.YubiKeyInstructionIos : AppResources.YubiKeyInstruction; public TwoFactorProviderType? SelectedProviderType { get => _selectedProviderType; set => SetProperty(ref _selectedProviderType, value, additionalPropertyNames: new string[] { nameof(EmailMethod), nameof(DuoMethod), nameof(Fido2Method), nameof(YubikeyMethod), nameof(AuthenticatorMethod), nameof(TotpMethod), nameof(ShowTryAgain), }); } public Command SubmitCommand { get; } public Action TwoFactorAuthSuccessAction { get; set; } public Action StartSetPasswordAction { get; set; } public Action CloseAction { get; set; } public Action UpdateTempPasswordAction { get; set; } protected override II18nService i18nService => _i18nService; protected override IEnvironmentService environmentService => _environmentService; protected override IDeviceActionService deviceActionService => _deviceActionService; protected override IPlatformUtilsService platformUtilsService => _platformUtilsService; public void Init() { if ((!_authService.AuthingWithSso() && !_authService.AuthingWithPassword()) || _authService.TwoFactorProvidersData == null) { // TODO: dismiss modal? return; } _authingWithSso = _authService.AuthingWithSso(); if (!string.IsNullOrWhiteSpace(_environmentService.BaseUrl)) { _webVaultUrl = _environmentService.BaseUrl; } else if (!string.IsNullOrWhiteSpace(_environmentService.WebVaultUrl)) { _webVaultUrl = _environmentService.WebVaultUrl; } SelectedProviderType = _authService.GetDefaultTwoFactorProvider(_platformUtilsService.SupportsFido2()); Load(); } public void Load() { if (SelectedProviderType == null) { PageTitle = AppResources.LoginUnavailable; return; } var page = Page as TwoFactorPage; PageTitle = _authService.TwoFactorProviders[SelectedProviderType.Value].Name; var providerData = _authService.TwoFactorProvidersData[SelectedProviderType.Value]; switch (SelectedProviderType.Value) { case TwoFactorProviderType.Fido2WebAuthn: Fido2AuthenticateAsync(providerData); break; case TwoFactorProviderType.YubiKey: _messagingService.Send("listenYubiKeyOTP", true); break; case TwoFactorProviderType.Duo: case TwoFactorProviderType.OrganizationDuo: var host = WebUtility.UrlEncode(providerData["Host"] as string); var req = WebUtility.UrlEncode(providerData["Signature"] as string); page.DuoWebView.Uri = $"{_webVaultUrl}/duo-connector.html?host={host}&request={req}"; page.DuoWebView.RegisterAction(sig => { Token = sig; Device.BeginInvokeOnMainThread(async () => await SubmitAsync()); }); break; case TwoFactorProviderType.Email: TotpInstruction = string.Format(AppResources.EnterVerificationCodeEmail, providerData["Email"] as string); if (_authService.TwoFactorProvidersData.Count > 1) { var emailTask = Task.Run(() => SendEmailAsync(false, false)); } break; case TwoFactorProviderType.Authenticator: TotpInstruction = AppResources.EnterVerificationCodeApp; break; default: break; } if (!YubikeyMethod) { _messagingService.Send("listenYubiKeyOTP", false); } ShowContinue = !(SelectedProviderType == null || DuoMethod || Fido2Method); } public async Task Fido2AuthenticateAsync(Dictionary providerData = null) { await _deviceActionService.ShowLoadingAsync(AppResources.Validating); if (providerData == null) { providerData = _authService.TwoFactorProvidersData[TwoFactorProviderType.Fido2WebAuthn]; } var callbackUri = "bitwarden://webauthn-callback"; var data = AppHelpers.EncodeDataParameter(new { callbackUri = callbackUri, data = JsonConvert.SerializeObject(providerData), headerText = AppResources.Fido2Title, btnText = AppResources.Fido2AuthenticateWebAuthn, btnReturnText = AppResources.Fido2ReturnToApp, }); var url = _webVaultUrl + "/webauthn-mobile-connector.html?" + "data=" + data + "&parent=" + Uri.EscapeDataString(callbackUri) + "&v=2"; WebAuthenticatorResult authResult = null; try { var options = new WebAuthenticatorOptions { Url = new Uri(url), CallbackUrl = new Uri(callbackUri), PrefersEphemeralWebBrowserSession = true, }; authResult = await WebAuthenticator.AuthenticateAsync(options); } catch (TaskCanceledException) { // user canceled await _deviceActionService.HideLoadingAsync(); return; } string response = null; if (authResult != null && authResult.Properties.TryGetValue("data", out var resultData)) { response = Uri.UnescapeDataString(resultData); } if (!string.IsNullOrWhiteSpace(response)) { Token = response; await SubmitAsync(false); } else { await _deviceActionService.HideLoadingAsync(); if (authResult != null && authResult.Properties.TryGetValue("error", out var resultError)) { var message = AppResources.Fido2CheckBrowser + "\n\n" + resultError; await _platformUtilsService.ShowDialogAsync(message, AppResources.AnErrorHasOccurred, AppResources.Ok); } else { await _platformUtilsService.ShowDialogAsync(AppResources.Fido2CheckBrowser, AppResources.AnErrorHasOccurred, AppResources.Ok); } } } public async Task SubmitAsync(bool showLoading = true) { if (SelectedProviderType == null) { return; } if (Xamarin.Essentials.Connectivity.NetworkAccess == Xamarin.Essentials.NetworkAccess.None) { await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage, AppResources.InternetConnectionRequiredTitle, AppResources.Ok); return; } if (string.IsNullOrWhiteSpace(Token)) { await _platformUtilsService.ShowDialogAsync( string.Format(AppResources.ValidationFieldRequired, AppResources.VerificationCode), AppResources.AnErrorHasOccurred, AppResources.Ok); return; } if (SelectedProviderType == TwoFactorProviderType.Email || SelectedProviderType == TwoFactorProviderType.Authenticator) { Token = Token.Replace(" ", string.Empty).Trim(); } try { if (showLoading) { await _deviceActionService.ShowLoadingAsync(AppResources.Validating); } var result = await _authService.LogInTwoFactorAsync(SelectedProviderType.Value, Token, _captchaToken, Remember); if (result.CaptchaNeeded) { if (await HandleCaptchaAsync(result.CaptchaSiteKey)) { await SubmitAsync(false); _captchaToken = null; } return; } _captchaToken = null; var task = Task.Run(() => _syncService.FullSyncAsync(true)); await _deviceActionService.HideLoadingAsync(); _messagingService.Send("listenYubiKeyOTP", false); _broadcasterService.Unsubscribe(nameof(TwoFactorPage)); if (_authingWithSso && result.ResetMasterPassword) { StartSetPasswordAction?.Invoke(); } else if (result.ForcePasswordReset) { UpdateTempPasswordAction?.Invoke(); } else { TwoFactorAuthSuccessAction?.Invoke(); } } catch (ApiException e) { _captchaToken = null; await _deviceActionService.HideLoadingAsync(); if (e?.Error != null) { await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(), AppResources.AnErrorHasOccurred, AppResources.Ok); } } } public async Task AnotherMethodAsync() { var supportedProviders = _authService.GetSupportedTwoFactorProviders(); var options = supportedProviders.Select(p => p.Name).ToList(); options.Add(AppResources.RecoveryCodeTitle); var method = await _deviceActionService.DisplayActionSheetAsync(AppResources.TwoStepLoginOptions, AppResources.Cancel, null, options.ToArray()); if (method == AppResources.RecoveryCodeTitle) { _platformUtilsService.LaunchUri("https://bitwarden.com/help/lost-two-step-device/"); } else if (method != AppResources.Cancel && method != null) { var selected = supportedProviders.FirstOrDefault(p => p.Name == method)?.Type; if (selected == SelectedProviderType) { // Nothing changed return; } SelectedProviderType = selected; Load(); } } public async Task SendEmailAsync(bool showLoading, bool doToast) { if (!EmailMethod) { return false; } if (Xamarin.Essentials.Connectivity.NetworkAccess == Xamarin.Essentials.NetworkAccess.None) { await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage, AppResources.InternetConnectionRequiredTitle, AppResources.Ok); 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, AppResources.AnErrorHasOccurred, AppResources.Ok); return false; } } } }