mirror of
https://github.com/bitwarden/android.git
synced 2024-12-27 03:18:27 +03:00
Passwordless feature branch PR (#2100)
* [SG-471] Passwordless device login screen (#2017) * [SSG-471] Added UI for the device login request response. * [SG-471] Added text resources and arguments to Page. * [SG-471] Added properties to speed up page bindings * [SG-471] Added mock services. Added Accept/reject command binding, navigation and toast messages. * [SG-471] fixed code styling with dotnet-format * [SG-471] Fixed back button placement. PR fixes. * [SG-471] Added new Origin parameter to the page. * [SG-471] PR Fixes * [SG-471] PR fixes * [SG-471] PR Fix: added FireAndForget. * [SG-471] Moved fire and forget to run on ui thread task. * [SG-381] Passwordless - Add setting to Mobile (#2037) * [SG-381] Added settings option to approve passwordless login request. If user has notifications disabled, prompt to go to settings and enable them. * [SG-381] Update settings pop up texts. * [SG-381] Added new method to get notifications state on device settings. Added userId to property saved on device to differentiate value between users. * [SG-381] Added text for the popup on selection. * [SG-381] PR Fixes * [SG-408] Implement passwordless api methods (#2055) * [SG-408] Update notification model. * [SG-408] removed duplicated resource * [SG-408] Added implementation to Api Service of new passwordless methods. * removed qa endpoints * [SG-408] Changed auth methods implementation, added method call to viewmodel. * [SG-408] ran code format * [SG-408] PR fixes * [SG-472] Add configuration for new notification type (#2056) * [SG-472] Added methods to present local notification to the user. Configured new notification type for passwordless logins * [SG-472] Updated code to new api service changes. * [SG-472] ran dotnet format * [SG-472] PR Fixes. * [SG-472] PR Fixes * [SG-169] End-to-end testing refactor. (#2073) * [SG-169] Passwordless demo change requests (#2079) * [SG-169] End-to-end testing refactor. * [SG-169] Fixed labels. Changed color of Fingerprint phrase. Waited for app to be in foreground to launch passwordless modal to fix Android issues. * [SG-169] Anchored buttons to the bottom of the screen. * [SG-169] Changed device type from enum to string. * [SG-169] PR fixes * [SG-169] PR fixes * [SG-169] Added comment on static variable
This commit is contained in:
parent
2f4cd36595
commit
f9a32e4abc
35 changed files with 852 additions and 30 deletions
|
@ -12,6 +12,7 @@ using Android.Runtime;
|
||||||
using Android.Views;
|
using Android.Views;
|
||||||
using Bit.App.Abstractions;
|
using Bit.App.Abstractions;
|
||||||
using Bit.App.Models;
|
using Bit.App.Models;
|
||||||
|
using Bit.App.Resources;
|
||||||
using Bit.App.Utilities;
|
using Bit.App.Utilities;
|
||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
|
@ -86,6 +87,7 @@ namespace Bit.Droid
|
||||||
Xamarin.Essentials.Platform.Init(this, savedInstanceState);
|
Xamarin.Essentials.Platform.Init(this, savedInstanceState);
|
||||||
Xamarin.Forms.Forms.Init(this, savedInstanceState);
|
Xamarin.Forms.Forms.Init(this, savedInstanceState);
|
||||||
_appOptions = GetOptions();
|
_appOptions = GetOptions();
|
||||||
|
CreateNotificationChannel();
|
||||||
LoadApplication(new App.App(_appOptions));
|
LoadApplication(new App.App(_appOptions));
|
||||||
DisableAndroidFontScale();
|
DisableAndroidFontScale();
|
||||||
|
|
||||||
|
@ -407,6 +409,25 @@ namespace Bit.Droid
|
||||||
await _eventService.UploadEventsAsync();
|
await _eventService.UploadEventsAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void CreateNotificationChannel()
|
||||||
|
{
|
||||||
|
#if !FDROID
|
||||||
|
if (Build.VERSION.SdkInt < BuildVersionCodes.O)
|
||||||
|
{
|
||||||
|
// Notification channels are new in API 26 (and not a part of the
|
||||||
|
// support library). There is no need to create a notification
|
||||||
|
// channel on older versions of Android.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var channel = new NotificationChannel(Constants.AndroidNotificationChannelId, AppResources.AllNotifications, NotificationImportance.Default);
|
||||||
|
if(GetSystemService(NotificationService) is NotificationManager notificationManager)
|
||||||
|
{
|
||||||
|
notificationManager.CreateNotificationChannel(channel);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
private void DisableAndroidFontScale()
|
private void DisableAndroidFontScale()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
#if !FDROID
|
#if !FDROID
|
||||||
|
using System;
|
||||||
using Android.App;
|
using Android.App;
|
||||||
using Bit.App.Abstractions;
|
using Bit.App.Abstractions;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Firebase.Messaging;
|
using Firebase.Messaging;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
@ -16,14 +18,22 @@ namespace Bit.Droid.Push
|
||||||
{
|
{
|
||||||
public async override void OnNewToken(string token)
|
public async override void OnNewToken(string token)
|
||||||
{
|
{
|
||||||
|
try {
|
||||||
var stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
var stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
||||||
var pushNotificationService = ServiceContainer.Resolve<IPushNotificationService>("pushNotificationService");
|
var pushNotificationService = ServiceContainer.Resolve<IPushNotificationService>("pushNotificationService");
|
||||||
|
|
||||||
await stateService.SetPushRegisteredTokenAsync(token);
|
await stateService.SetPushRegisteredTokenAsync(token);
|
||||||
await pushNotificationService.RegisterAsync();
|
await pushNotificationService.RegisterAsync();
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.Instance.Exception(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async override void OnMessageReceived(RemoteMessage message)
|
public async override void OnMessageReceived(RemoteMessage message)
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
if (message?.Data == null)
|
if (message?.Data == null)
|
||||||
{
|
{
|
||||||
|
@ -34,16 +44,15 @@ namespace Bit.Droid.Push
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try
|
|
||||||
{
|
|
||||||
var obj = JObject.Parse(data);
|
var obj = JObject.Parse(data);
|
||||||
var listener = ServiceContainer.Resolve<IPushNotificationListenerService>(
|
var listener = ServiceContainer.Resolve<IPushNotificationListenerService>(
|
||||||
"pushNotificationListenerService");
|
"pushNotificationListenerService");
|
||||||
await listener.OnMessageAsync(obj, Device.Android);
|
await listener.OnMessageAsync(obj, Device.Android);
|
||||||
}
|
}
|
||||||
catch (JsonReaderException ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
System.Diagnostics.Debug.WriteLine(ex.ToString());
|
Logger.Instance.Exception(ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
#if !FDROID
|
#if !FDROID
|
||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Android.App;
|
||||||
|
using Android.Content;
|
||||||
using AndroidX.Core.App;
|
using AndroidX.Core.App;
|
||||||
using Bit.App.Abstractions;
|
using Bit.App.Abstractions;
|
||||||
|
using Bit.Core;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Xamarin.Forms;
|
using Xamarin.Forms;
|
||||||
|
|
||||||
|
@ -23,6 +26,11 @@ namespace Bit.Droid.Services
|
||||||
|
|
||||||
public bool IsRegisteredForPush => NotificationManagerCompat.From(Android.App.Application.Context)?.AreNotificationsEnabled() ?? false;
|
public bool IsRegisteredForPush => NotificationManagerCompat.From(Android.App.Application.Context)?.AreNotificationsEnabled() ?? false;
|
||||||
|
|
||||||
|
public Task<bool> AreNotificationsSettingsEnabledAsync()
|
||||||
|
{
|
||||||
|
return Task.FromResult(IsRegisteredForPush);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<string> GetTokenAsync()
|
public async Task<string> GetTokenAsync()
|
||||||
{
|
{
|
||||||
return await _stateService.GetPushCurrentTokenAsync();
|
return await _stateService.GetPushCurrentTokenAsync();
|
||||||
|
@ -47,6 +55,36 @@ namespace Bit.Droid.Services
|
||||||
// Do we ever need to unregister?
|
// Do we ever need to unregister?
|
||||||
return Task.FromResult(0);
|
return Task.FromResult(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void DismissLocalNotification(string notificationId)
|
||||||
|
{
|
||||||
|
if (int.TryParse(notificationId, out int intNotificationId))
|
||||||
|
{
|
||||||
|
var notificationManager = NotificationManagerCompat.From(Android.App.Application.Context);
|
||||||
|
notificationManager.Cancel(intNotificationId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SendLocalNotification(string title, string message, string notificationId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(notificationId))
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException("notificationId cannot be null or empty.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var context = Android.App.Application.Context;
|
||||||
|
var intent = new Intent(context, typeof(MainActivity));
|
||||||
|
var pendingIntent = PendingIntent.GetActivity(context, 20220801, intent, PendingIntentFlags.UpdateCurrent);
|
||||||
|
var builder = new NotificationCompat.Builder(context, Constants.AndroidNotificationChannelId)
|
||||||
|
.SetContentIntent(pendingIntent)
|
||||||
|
.SetContentTitle(title)
|
||||||
|
.SetContentText(message)
|
||||||
|
.SetSmallIcon(Resource.Mipmap.ic_launcher)
|
||||||
|
.SetAutoCancel(true);
|
||||||
|
|
||||||
|
var notificationManager = NotificationManagerCompat.From(context);
|
||||||
|
notificationManager.Notify(int.Parse(notificationId), builder.Build());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -964,5 +964,14 @@ namespace Bit.Droid.Services
|
||||||
}
|
}
|
||||||
activity.RunOnUiThread(() => activity.Window.AddFlags(WindowManagerFlags.Secure));
|
activity.RunOnUiThread(() => activity.Window.AddFlags(WindowManagerFlags.Secure));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void OpenAppSettings()
|
||||||
|
{
|
||||||
|
var intent = new Intent(Android.Provider.Settings.ActionApplicationDetailsSettings);
|
||||||
|
intent.AddFlags(ActivityFlags.NewTask);
|
||||||
|
var uri = Android.Net.Uri.FromParts("package", Application.Context.PackageName, null);
|
||||||
|
intent.SetData(uri);
|
||||||
|
Application.Context.StartActivity(intent);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,5 +49,6 @@ namespace Bit.App.Abstractions
|
||||||
float GetSystemFontSizeScale();
|
float GetSystemFontSizeScale();
|
||||||
Task OnAccountSwitchCompleteAsync();
|
Task OnAccountSwitchCompleteAsync();
|
||||||
Task SetScreenCaptureAllowedAsync();
|
Task SetScreenCaptureAllowedAsync();
|
||||||
|
void OpenAppSettings();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,16 @@
|
||||||
using System.Threading.Tasks;
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Bit.App.Abstractions
|
namespace Bit.App.Abstractions
|
||||||
{
|
{
|
||||||
public interface IPushNotificationService
|
public interface IPushNotificationService
|
||||||
{
|
{
|
||||||
bool IsRegisteredForPush { get; }
|
bool IsRegisteredForPush { get; }
|
||||||
|
Task<bool> AreNotificationsSettingsEnabledAsync();
|
||||||
Task<string> GetTokenAsync();
|
Task<string> GetTokenAsync();
|
||||||
Task RegisterAsync();
|
Task RegisterAsync();
|
||||||
Task UnregisterAsync();
|
Task UnregisterAsync();
|
||||||
|
void SendLocalNotification(string title, string message, string notificationId);
|
||||||
|
void DismissLocalNotification(string notificationId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -123,6 +123,9 @@
|
||||||
<SubType>Code</SubType>
|
<SubType>Code</SubType>
|
||||||
</Compile>
|
</Compile>
|
||||||
<Compile Remove="Pages\Accounts\AccountsPopupPage.xaml.cs" />
|
<Compile Remove="Pages\Accounts\AccountsPopupPage.xaml.cs" />
|
||||||
|
<Compile Update="Pages\Accounts\LoginPasswordlessPage.xaml.cs">
|
||||||
|
<DependentUpon>LoginPasswordlessPage.xaml</DependentUpon>
|
||||||
|
</Compile>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
@ -7,6 +7,7 @@ using Bit.App.Resources;
|
||||||
using Bit.App.Services;
|
using Bit.App.Services;
|
||||||
using Bit.App.Utilities;
|
using Bit.App.Utilities;
|
||||||
using Bit.App.Utilities.AccountManagement;
|
using Bit.App.Utilities.AccountManagement;
|
||||||
|
using Bit.Core;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
|
@ -25,13 +26,13 @@ namespace Bit.App
|
||||||
private readonly IStateService _stateService;
|
private readonly IStateService _stateService;
|
||||||
private readonly IVaultTimeoutService _vaultTimeoutService;
|
private readonly IVaultTimeoutService _vaultTimeoutService;
|
||||||
private readonly ISyncService _syncService;
|
private readonly ISyncService _syncService;
|
||||||
private readonly IPlatformUtilsService _platformUtilsService;
|
|
||||||
private readonly IAuthService _authService;
|
private readonly IAuthService _authService;
|
||||||
private readonly IStorageService _secureStorageService;
|
|
||||||
private readonly IDeviceActionService _deviceActionService;
|
private readonly IDeviceActionService _deviceActionService;
|
||||||
private readonly IAccountsManager _accountsManager;
|
private readonly IAccountsManager _accountsManager;
|
||||||
|
private readonly IPushNotificationService _pushNotificationService;
|
||||||
private static bool _isResumed;
|
private static bool _isResumed;
|
||||||
|
// this variable is static because the app is launching new activities on notification click, creating new instances of App.
|
||||||
|
private static bool _pendingCheckPasswordlessLoginRequests;
|
||||||
|
|
||||||
public App(AppOptions appOptions)
|
public App(AppOptions appOptions)
|
||||||
{
|
{
|
||||||
|
@ -47,10 +48,9 @@ namespace Bit.App
|
||||||
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
|
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
|
||||||
_syncService = ServiceContainer.Resolve<ISyncService>("syncService");
|
_syncService = ServiceContainer.Resolve<ISyncService>("syncService");
|
||||||
_authService = ServiceContainer.Resolve<IAuthService>("authService");
|
_authService = ServiceContainer.Resolve<IAuthService>("authService");
|
||||||
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
|
||||||
_secureStorageService = ServiceContainer.Resolve<IStorageService>("secureStorageService");
|
|
||||||
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||||
_accountsManager = ServiceContainer.Resolve<IAccountsManager>("accountsManager");
|
_accountsManager = ServiceContainer.Resolve<IAccountsManager>("accountsManager");
|
||||||
|
_pushNotificationService = ServiceContainer.Resolve<IPushNotificationService>();
|
||||||
|
|
||||||
_accountsManager.Init(() => Options, this);
|
_accountsManager.Init(() => Options, this);
|
||||||
|
|
||||||
|
@ -140,6 +140,10 @@ namespace Bit.App
|
||||||
new NavigationPage(new RemoveMasterPasswordPage()));
|
new NavigationPage(new RemoveMasterPasswordPage()));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
else if (message.Command == "passwordlessLoginRequest" || message.Command == "unlocked")
|
||||||
|
{
|
||||||
|
CheckPasswordlessLoginRequestsAsync().FireAndForget();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
@ -148,11 +152,52 @@ namespace Bit.App
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task CheckPasswordlessLoginRequestsAsync()
|
||||||
|
{
|
||||||
|
if (!_isResumed)
|
||||||
|
{
|
||||||
|
_pendingCheckPasswordlessLoginRequests = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_pendingCheckPasswordlessLoginRequests = false;
|
||||||
|
if (await _vaultTimeoutService.IsLockedAsync())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var notification = await _stateService.GetPasswordlessLoginNotificationAsync();
|
||||||
|
if (notification == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delay to wait for the vault page to appear
|
||||||
|
await Task.Delay(2000);
|
||||||
|
var loginRequestData = await _authService.GetPasswordlessLoginRequestByIdAsync(notification.Id);
|
||||||
|
var page = new LoginPasswordlessPage(new LoginPasswordlessDetails()
|
||||||
|
{
|
||||||
|
PubKey = loginRequestData.PublicKey,
|
||||||
|
Id = loginRequestData.Id,
|
||||||
|
IpAddress = loginRequestData.RequestIpAddress,
|
||||||
|
Email = await _stateService.GetEmailAsync(),
|
||||||
|
FingerprintPhrase = loginRequestData.RequestFingerprint,
|
||||||
|
RequestDate = loginRequestData.CreationDate,
|
||||||
|
DeviceType = loginRequestData.RequestDeviceType,
|
||||||
|
Origin = loginRequestData.Origin,
|
||||||
|
});
|
||||||
|
await _stateService.SetPasswordlessLoginNotificationAsync(null);
|
||||||
|
_pushNotificationService.DismissLocalNotification(Constants.PasswordlessNotificationId);
|
||||||
|
await Device.InvokeOnMainThreadAsync(async () => await Application.Current.MainPage.Navigation.PushModalAsync(new NavigationPage(page)));
|
||||||
|
}
|
||||||
|
|
||||||
public AppOptions Options { get; private set; }
|
public AppOptions Options { get; private set; }
|
||||||
|
|
||||||
protected async override void OnStart()
|
protected async override void OnStart()
|
||||||
{
|
{
|
||||||
System.Diagnostics.Debug.WriteLine("XF App: OnStart");
|
System.Diagnostics.Debug.WriteLine("XF App: OnStart");
|
||||||
|
_isResumed = true;
|
||||||
await ClearCacheIfNeededAsync();
|
await ClearCacheIfNeededAsync();
|
||||||
Prime();
|
Prime();
|
||||||
if (string.IsNullOrWhiteSpace(Options.Uri))
|
if (string.IsNullOrWhiteSpace(Options.Uri))
|
||||||
|
@ -164,6 +209,10 @@ namespace Bit.App
|
||||||
SyncIfNeeded();
|
SyncIfNeeded();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (_pendingCheckPasswordlessLoginRequests)
|
||||||
|
{
|
||||||
|
CheckPasswordlessLoginRequestsAsync().FireAndForget();
|
||||||
|
}
|
||||||
if (Device.RuntimePlatform == Device.Android)
|
if (Device.RuntimePlatform == Device.Android)
|
||||||
{
|
{
|
||||||
await _vaultTimeoutService.CheckVaultTimeoutAsync();
|
await _vaultTimeoutService.CheckVaultTimeoutAsync();
|
||||||
|
@ -196,6 +245,10 @@ namespace Bit.App
|
||||||
{
|
{
|
||||||
System.Diagnostics.Debug.WriteLine("XF App: OnResume");
|
System.Diagnostics.Debug.WriteLine("XF App: OnResume");
|
||||||
_isResumed = true;
|
_isResumed = true;
|
||||||
|
if (_pendingCheckPasswordlessLoginRequests)
|
||||||
|
{
|
||||||
|
CheckPasswordlessLoginRequestsAsync().FireAndForget();
|
||||||
|
}
|
||||||
if (Device.RuntimePlatform == Device.Android)
|
if (Device.RuntimePlatform == Device.Android)
|
||||||
{
|
{
|
||||||
ResumedAsync().FireAndForget();
|
ResumedAsync().FireAndForget();
|
||||||
|
|
85
src/App/Pages/Accounts/LoginPasswordlessPage.xaml
Normal file
85
src/App/Pages/Accounts/LoginPasswordlessPage.xaml
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<pages:BaseContentPage
|
||||||
|
xmlns="http://xamarin.com/schemas/2014/forms"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
|
x:Class="Bit.App.Pages.LoginPasswordlessPage"
|
||||||
|
xmlns:pages="clr-namespace:Bit.App.Pages"
|
||||||
|
xmlns:controls="clr-namespace:Bit.App.Controls"
|
||||||
|
xmlns:u="clr-namespace:Bit.App.Utilities"
|
||||||
|
x:DataType="pages:LoginPasswordlessViewModel"
|
||||||
|
Title="{Binding PageTitle}">
|
||||||
|
|
||||||
|
<ContentPage.BindingContext>
|
||||||
|
<pages:LoginPasswordlessViewModel />
|
||||||
|
</ContentPage.BindingContext>
|
||||||
|
|
||||||
|
<ContentPage.Resources>
|
||||||
|
<ResourceDictionary>
|
||||||
|
<ToolbarItem Text="{u:I18n Close}" Clicked="Close_Clicked" Order="Primary" Priority="-1"
|
||||||
|
x:Name="_closeItem" x:Key="closeItem" />
|
||||||
|
</ResourceDictionary>
|
||||||
|
</ContentPage.Resources>
|
||||||
|
<StackLayout
|
||||||
|
Padding="7, 0, 7, 20">
|
||||||
|
<ScrollView
|
||||||
|
VerticalOptions="FillAndExpand">
|
||||||
|
<StackLayout>
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n AreYouTryingToLogIn}"
|
||||||
|
FontSize="Title"
|
||||||
|
FontAttributes="Bold"
|
||||||
|
Margin="0,14,0,21"/>
|
||||||
|
<Label
|
||||||
|
Text="{Binding LogInAttemptByLabel}"
|
||||||
|
FontSize="Small"
|
||||||
|
Margin="0,0,0,24"/>
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n FingerprintPhrase}"
|
||||||
|
FontSize="Small"
|
||||||
|
FontAttributes="Bold"/>
|
||||||
|
<controls:MonoLabel
|
||||||
|
FormattedText="{Binding LoginRequest.FingerprintPhrase}"
|
||||||
|
FontSize="Medium"
|
||||||
|
TextColor="{DynamicResource FingerprintPhrase}"
|
||||||
|
Margin="0,0,0,27"/>
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n DeviceType}"
|
||||||
|
FontSize="Small"
|
||||||
|
FontAttributes="Bold"/>
|
||||||
|
<Label
|
||||||
|
Text="{Binding LoginRequest.DeviceType}"
|
||||||
|
FontSize="Small"
|
||||||
|
Margin="0,0,0,21"/>
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n IpAddress}"
|
||||||
|
IsVisible="{Binding ShowIpAddress}"
|
||||||
|
FontSize="Small"
|
||||||
|
FontAttributes="Bold"/>
|
||||||
|
<Label
|
||||||
|
Text="{Binding LoginRequest.IpAddress}"
|
||||||
|
IsVisible="{Binding ShowIpAddress}"
|
||||||
|
FontSize="Small"
|
||||||
|
Margin="0,0,0,21"/>
|
||||||
|
<Label
|
||||||
|
Text="{u:I18n Time}"
|
||||||
|
FontSize="Small"
|
||||||
|
FontAttributes="Bold"/>
|
||||||
|
<Label
|
||||||
|
Text="{Binding TimeOfRequestText}"
|
||||||
|
FontSize="Small"
|
||||||
|
Margin="0,0,0,57"/>
|
||||||
|
</StackLayout>
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
Text="{u:I18n ConfirmLogIn}"
|
||||||
|
Command="{Binding AcceptRequestCommand}"
|
||||||
|
Margin="0,0,0,17"
|
||||||
|
StyleClass="btn-primary"/>
|
||||||
|
<Button
|
||||||
|
Text="{u:I18n DenyLogIn}"
|
||||||
|
Command="{Binding RejectRequestCommand}"
|
||||||
|
StyleClass="btn-secundary"/>
|
||||||
|
|
||||||
|
</StackLayout>
|
||||||
|
</pages:BaseContentPage>
|
31
src/App/Pages/Accounts/LoginPasswordlessPage.xaml.cs
Normal file
31
src/App/Pages/Accounts/LoginPasswordlessPage.xaml.cs
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
using Xamarin.Forms;
|
||||||
|
|
||||||
|
namespace Bit.App.Pages
|
||||||
|
{
|
||||||
|
public partial class LoginPasswordlessPage : BaseContentPage
|
||||||
|
{
|
||||||
|
private LoginPasswordlessViewModel _vm;
|
||||||
|
|
||||||
|
public LoginPasswordlessPage(LoginPasswordlessDetails loginPasswordlessDetails)
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
_vm = BindingContext as LoginPasswordlessViewModel;
|
||||||
|
_vm.Page = this;
|
||||||
|
|
||||||
|
_vm.LoginRequest = loginPasswordlessDetails;
|
||||||
|
|
||||||
|
if (Device.RuntimePlatform == Device.iOS)
|
||||||
|
{
|
||||||
|
ToolbarItems.Add(_closeItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void Close_Clicked(object sender, System.EventArgs e)
|
||||||
|
{
|
||||||
|
if (DoOnce())
|
||||||
|
{
|
||||||
|
await Navigation.PopModalAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
126
src/App/Pages/Accounts/LoginPasswordlessViewModel.cs
Normal file
126
src/App/Pages/Accounts/LoginPasswordlessViewModel.cs
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using Bit.App.Abstractions;
|
||||||
|
using Bit.App.Resources;
|
||||||
|
using Bit.App.Utilities;
|
||||||
|
using Bit.Core;
|
||||||
|
using Bit.Core.Abstractions;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Xamarin.CommunityToolkit.ObjectModel;
|
||||||
|
using Xamarin.Forms;
|
||||||
|
|
||||||
|
namespace Bit.App.Pages
|
||||||
|
{
|
||||||
|
public class LoginPasswordlessViewModel : BaseViewModel
|
||||||
|
{
|
||||||
|
private IDeviceActionService _deviceActionService;
|
||||||
|
private IAuthService _authService;
|
||||||
|
private IPlatformUtilsService _platformUtilsService;
|
||||||
|
private ILogger _logger;
|
||||||
|
private LoginPasswordlessDetails _resquest;
|
||||||
|
|
||||||
|
public LoginPasswordlessViewModel()
|
||||||
|
{
|
||||||
|
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||||
|
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
||||||
|
_authService = ServiceContainer.Resolve<IAuthService>("authService");
|
||||||
|
_logger = ServiceContainer.Resolve<ILogger>("logger");
|
||||||
|
|
||||||
|
PageTitle = AppResources.LogInRequested;
|
||||||
|
|
||||||
|
AcceptRequestCommand = new AsyncCommand(() => PasswordlessLoginAsync(true),
|
||||||
|
onException: ex => HandleException(ex),
|
||||||
|
allowsMultipleExecutions: false);
|
||||||
|
RejectRequestCommand = new AsyncCommand(() => PasswordlessLoginAsync(false),
|
||||||
|
onException: ex => HandleException(ex),
|
||||||
|
allowsMultipleExecutions: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ICommand AcceptRequestCommand { get; }
|
||||||
|
|
||||||
|
public ICommand RejectRequestCommand { get; }
|
||||||
|
|
||||||
|
public string LogInAttemptByLabel => LoginRequest != null ? string.Format(AppResources.LogInAttemptByXOnY, LoginRequest.Email, LoginRequest.Origin) : string.Empty;
|
||||||
|
|
||||||
|
public string TimeOfRequestText => CreateRequestDate(LoginRequest?.RequestDate);
|
||||||
|
|
||||||
|
public bool ShowIpAddress => !string.IsNullOrEmpty(LoginRequest?.IpAddress);
|
||||||
|
|
||||||
|
public LoginPasswordlessDetails LoginRequest
|
||||||
|
{
|
||||||
|
get => _resquest;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
SetProperty(ref _resquest, value, additionalPropertyNames: new string[]
|
||||||
|
{
|
||||||
|
nameof(LogInAttemptByLabel),
|
||||||
|
nameof(TimeOfRequestText),
|
||||||
|
nameof(ShowIpAddress),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task PasswordlessLoginAsync(bool approveRequest)
|
||||||
|
{
|
||||||
|
await _deviceActionService.ShowLoadingAsync(AppResources.Loading);
|
||||||
|
await _authService.PasswordlessLoginAsync(LoginRequest.Id, LoginRequest.PubKey, approveRequest);
|
||||||
|
await _deviceActionService.HideLoadingAsync();
|
||||||
|
await Page.Navigation.PopModalAsync();
|
||||||
|
_platformUtilsService.ShowToast("info", null, approveRequest ? AppResources.LogInAccepted : AppResources.LogInDenied);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string CreateRequestDate(DateTime? requestDate)
|
||||||
|
{
|
||||||
|
if (!requestDate.HasValue)
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var minutesSinceRequest = requestDate.Value.ToUniversalTime().Minute - DateTime.UtcNow.Minute;
|
||||||
|
if (minutesSinceRequest < 5)
|
||||||
|
{
|
||||||
|
return AppResources.JustNow;
|
||||||
|
}
|
||||||
|
if (minutesSinceRequest < 59)
|
||||||
|
{
|
||||||
|
return string.Format(AppResources.XMinutesAgo, minutesSinceRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
return requestDate.Value.ToShortTimeString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleException(Exception ex)
|
||||||
|
{
|
||||||
|
Xamarin.Essentials.MainThread.InvokeOnMainThreadAsync(async () =>
|
||||||
|
{
|
||||||
|
await _deviceActionService.HideLoadingAsync();
|
||||||
|
await _platformUtilsService.ShowDialogAsync(AppResources.GenericErrorMessage);
|
||||||
|
}).FireAndForget();
|
||||||
|
_logger.Exception(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class LoginPasswordlessDetails
|
||||||
|
{
|
||||||
|
public string Id { get; set; }
|
||||||
|
|
||||||
|
public string Key { get; set; }
|
||||||
|
|
||||||
|
public string PubKey { get; set; }
|
||||||
|
|
||||||
|
public string Origin { get; set; }
|
||||||
|
|
||||||
|
public string Email { get; set; }
|
||||||
|
|
||||||
|
public string FingerprintPhrase { get; set; }
|
||||||
|
|
||||||
|
public DateTime RequestDate { get; set; }
|
||||||
|
|
||||||
|
public string DeviceType { get; set; }
|
||||||
|
|
||||||
|
public string IpAddress { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -30,7 +30,7 @@ namespace Bit.App.Pages
|
||||||
private readonly IKeyConnectorService _keyConnectorService;
|
private readonly IKeyConnectorService _keyConnectorService;
|
||||||
private readonly IClipboardService _clipboardService;
|
private readonly IClipboardService _clipboardService;
|
||||||
private readonly ILogger _loggerService;
|
private readonly ILogger _loggerService;
|
||||||
|
private readonly IPushNotificationService _pushNotificationService;
|
||||||
private const int CustomVaultTimeoutValue = -100;
|
private const int CustomVaultTimeoutValue = -100;
|
||||||
|
|
||||||
private bool _supportsBiometric;
|
private bool _supportsBiometric;
|
||||||
|
@ -42,6 +42,7 @@ namespace Bit.App.Pages
|
||||||
private string _vaultTimeoutActionDisplayValue;
|
private string _vaultTimeoutActionDisplayValue;
|
||||||
private bool _showChangeMasterPassword;
|
private bool _showChangeMasterPassword;
|
||||||
private bool _reportLoggingEnabled;
|
private bool _reportLoggingEnabled;
|
||||||
|
private bool _approvePasswordlessLoginRequests;
|
||||||
|
|
||||||
private List<KeyValuePair<string, int?>> _vaultTimeouts =
|
private List<KeyValuePair<string, int?>> _vaultTimeouts =
|
||||||
new List<KeyValuePair<string, int?>>
|
new List<KeyValuePair<string, int?>>
|
||||||
|
@ -83,6 +84,7 @@ namespace Bit.App.Pages
|
||||||
_keyConnectorService = ServiceContainer.Resolve<IKeyConnectorService>("keyConnectorService");
|
_keyConnectorService = ServiceContainer.Resolve<IKeyConnectorService>("keyConnectorService");
|
||||||
_clipboardService = ServiceContainer.Resolve<IClipboardService>("clipboardService");
|
_clipboardService = ServiceContainer.Resolve<IClipboardService>("clipboardService");
|
||||||
_loggerService = ServiceContainer.Resolve<ILogger>("logger");
|
_loggerService = ServiceContainer.Resolve<ILogger>("logger");
|
||||||
|
_pushNotificationService = ServiceContainer.Resolve<IPushNotificationService>();
|
||||||
|
|
||||||
GroupedItems = new ObservableRangeCollection<ISettingsPageListItem>();
|
GroupedItems = new ObservableRangeCollection<ISettingsPageListItem>();
|
||||||
PageTitle = AppResources.Settings;
|
PageTitle = AppResources.Settings;
|
||||||
|
@ -133,6 +135,7 @@ namespace Bit.App.Pages
|
||||||
_showChangeMasterPassword = IncludeLinksWithSubscriptionInfo() &&
|
_showChangeMasterPassword = IncludeLinksWithSubscriptionInfo() &&
|
||||||
!await _keyConnectorService.GetUsesKeyConnector();
|
!await _keyConnectorService.GetUsesKeyConnector();
|
||||||
_reportLoggingEnabled = await _loggerService.IsEnabled();
|
_reportLoggingEnabled = await _loggerService.IsEnabled();
|
||||||
|
_approvePasswordlessLoginRequests = await _stateService.GetApprovePasswordlessLoginsAsync();
|
||||||
BuildList();
|
BuildList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -326,6 +329,38 @@ namespace Bit.App.Pages
|
||||||
BuildList();
|
BuildList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task ApproveLoginRequestsAsync()
|
||||||
|
{
|
||||||
|
var options = new[]
|
||||||
|
{
|
||||||
|
CreateSelectableOption(AppResources.Yes, _approvePasswordlessLoginRequests),
|
||||||
|
CreateSelectableOption(AppResources.No, !_approvePasswordlessLoginRequests),
|
||||||
|
};
|
||||||
|
|
||||||
|
var selection = await Page.DisplayActionSheet(AppResources.UseThisDeviceToApproveLoginRequestsMadeFromOtherDevices, AppResources.Cancel, null, options);
|
||||||
|
|
||||||
|
if (selection == null || selection == AppResources.Cancel)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_approvePasswordlessLoginRequests = CompareSelection(selection, AppResources.Yes);
|
||||||
|
await _stateService.SetApprovePasswordlessLoginsAsync(_approvePasswordlessLoginRequests);
|
||||||
|
|
||||||
|
BuildList();
|
||||||
|
|
||||||
|
if (!_approvePasswordlessLoginRequests || await _pushNotificationService.AreNotificationsSettingsEnabledAsync())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var openAppSettingsResult = await _platformUtilsService.ShowDialogAsync(AppResources.ReceivePushNotificationsForNewLoginRequests, title: string.Empty, confirmText: AppResources.Settings, cancelText: AppResources.NoThanks);
|
||||||
|
if (openAppSettingsResult)
|
||||||
|
{
|
||||||
|
_deviceActionService.OpenAppSettings();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async Task VaultTimeoutActionAsync()
|
public async Task VaultTimeoutActionAsync()
|
||||||
{
|
{
|
||||||
var options = _vaultTimeoutActions.Select(o =>
|
var options = _vaultTimeoutActions.Select(o =>
|
||||||
|
@ -504,6 +539,12 @@ namespace Bit.App.Pages
|
||||||
ExecuteAsync = () => UpdatePinAsync()
|
ExecuteAsync = () => UpdatePinAsync()
|
||||||
},
|
},
|
||||||
new SettingsPageListItem
|
new SettingsPageListItem
|
||||||
|
{
|
||||||
|
Name = AppResources.ApproveLoginRequests,
|
||||||
|
SubLabel = _approvePasswordlessLoginRequests ? AppResources.On : AppResources.Off,
|
||||||
|
ExecuteAsync = () => ApproveLoginRequestsAsync()
|
||||||
|
},
|
||||||
|
new SettingsPageListItem
|
||||||
{
|
{
|
||||||
Name = AppResources.LockNow,
|
Name = AppResources.LockNow,
|
||||||
ExecuteAsync = () => LockAsync()
|
ExecuteAsync = () => LockAsync()
|
||||||
|
|
121
src/App/Resources/AppResources.Designer.cs
generated
121
src/App/Resources/AppResources.Designer.cs
generated
|
@ -4151,6 +4151,127 @@ namespace Bit.App.Resources {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string LogInRequested {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("LogInRequested", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string AreYouTryingToLogIn {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("AreYouTryingToLogIn", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string LogInAttemptByXOnY {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("LogInAttemptByXOnY", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string DeviceType {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("DeviceType", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string IpAddress {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("IpAddress", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string Time {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("Time", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string Near {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("Near", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ConfirmLogIn {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("ConfirmLogIn", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string DenyLogIn {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("DenyLogIn", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string JustNow {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("JustNow", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string XMinutesAgo {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("XMinutesAgo", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string LogInAccepted {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("LogInAccepted", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string LogInDenied {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("LogInDenied", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ApproveLoginRequests {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("ApproveLoginRequests", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string UseThisDeviceToApproveLoginRequestsMadeFromOtherDevices {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("UseThisDeviceToApproveLoginRequestsMadeFromOtherDevices", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string AllowNotifications {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("AllowNotifications", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ReceivePushNotificationsForNewLoginRequests {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("ReceivePushNotificationsForNewLoginRequests", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string NoThanks {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("NoThanks", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ConfimLogInAttempForX {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("ConfimLogInAttempForX", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string AllNotifications
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return ResourceManager.GetString("AllNotifications", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
public static string PasswordType {
|
public static string PasswordType {
|
||||||
get {
|
get {
|
||||||
return ResourceManager.GetString("PasswordType", resourceCulture);
|
return ResourceManager.GetString("PasswordType", resourceCulture);
|
||||||
|
|
|
@ -2314,6 +2314,66 @@ select Add TOTP to store the key safely</value>
|
||||||
<data name="AreYouSureYouWantToEnableScreenCapture" xml:space="preserve">
|
<data name="AreYouSureYouWantToEnableScreenCapture" xml:space="preserve">
|
||||||
<value>Are you sure you want to enable Screen Capture?</value>
|
<value>Are you sure you want to enable Screen Capture?</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="LogInRequested" xml:space="preserve">
|
||||||
|
<value>Login requested</value>
|
||||||
|
</data>
|
||||||
|
<data name="AreYouTryingToLogIn" xml:space="preserve">
|
||||||
|
<value>Are you trying to log in?</value>
|
||||||
|
</data>
|
||||||
|
<data name="LogInAttemptByXOnY" xml:space="preserve">
|
||||||
|
<value>Login attempt by {0} on {1}</value>
|
||||||
|
</data>
|
||||||
|
<data name="DeviceType" xml:space="preserve">
|
||||||
|
<value>Device type</value>
|
||||||
|
</data>
|
||||||
|
<data name="IpAddress" xml:space="preserve">
|
||||||
|
<value>IP address</value>
|
||||||
|
</data>
|
||||||
|
<data name="Time" xml:space="preserve">
|
||||||
|
<value>Time</value>
|
||||||
|
</data>
|
||||||
|
<data name="Near" xml:space="preserve">
|
||||||
|
<value>Near</value>
|
||||||
|
</data>
|
||||||
|
<data name="ConfirmLogIn" xml:space="preserve">
|
||||||
|
<value>Confirm login</value>
|
||||||
|
</data>
|
||||||
|
<data name="DenyLogIn" xml:space="preserve">
|
||||||
|
<value>Deny login</value>
|
||||||
|
</data>
|
||||||
|
<data name="JustNow" xml:space="preserve">
|
||||||
|
<value>Just now</value>
|
||||||
|
</data>
|
||||||
|
<data name="XMinutesAgo" xml:space="preserve">
|
||||||
|
<value>{0} minutes ago</value>
|
||||||
|
</data>
|
||||||
|
<data name="LogInAccepted" xml:space="preserve">
|
||||||
|
<value>Login confirmed</value>
|
||||||
|
</data>
|
||||||
|
<data name="LogInDenied" xml:space="preserve">
|
||||||
|
<value>Login denied</value>
|
||||||
|
</data>
|
||||||
|
<data name="ApproveLoginRequests" xml:space="preserve">
|
||||||
|
<value>Approve login requests</value>
|
||||||
|
</data>
|
||||||
|
<data name="UseThisDeviceToApproveLoginRequestsMadeFromOtherDevices" xml:space="preserve">
|
||||||
|
<value>Use this device to approve login requests made from other devices.</value>
|
||||||
|
</data>
|
||||||
|
<data name="AllowNotifications" xml:space="preserve">
|
||||||
|
<value>Allow notifications</value>
|
||||||
|
</data>
|
||||||
|
<data name="ReceivePushNotificationsForNewLoginRequests" xml:space="preserve">
|
||||||
|
<value>Receive push notifications for new login requests</value>
|
||||||
|
</data>
|
||||||
|
<data name="NoThanks" xml:space="preserve">
|
||||||
|
<value>No thanks</value>
|
||||||
|
</data>
|
||||||
|
<data name="ConfimLogInAttempForX" xml:space="preserve">
|
||||||
|
<value>Confirm login attempt for {0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="AllNotifications" xml:space="preserve">
|
||||||
|
<value>All notifications</value>
|
||||||
|
</data>
|
||||||
<data name="PasswordType" xml:space="preserve">
|
<data name="PasswordType" xml:space="preserve">
|
||||||
<value>Password Type</value>
|
<value>Password Type</value>
|
||||||
</data>
|
</data>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using System.Threading.Tasks;
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using Bit.App.Abstractions;
|
using Bit.App.Abstractions;
|
||||||
|
|
||||||
namespace Bit.App.Services
|
namespace Bit.App.Services
|
||||||
|
@ -7,6 +8,11 @@ namespace Bit.App.Services
|
||||||
{
|
{
|
||||||
public bool IsRegisteredForPush => false;
|
public bool IsRegisteredForPush => false;
|
||||||
|
|
||||||
|
public Task<bool> AreNotificationsSettingsEnabledAsync()
|
||||||
|
{
|
||||||
|
return Task.FromResult(false);
|
||||||
|
}
|
||||||
|
|
||||||
public Task<string> GetTokenAsync()
|
public Task<string> GetTokenAsync()
|
||||||
{
|
{
|
||||||
return Task.FromResult(null as string);
|
return Task.FromResult(null as string);
|
||||||
|
@ -21,5 +27,9 @@ namespace Bit.App.Services
|
||||||
{
|
{
|
||||||
return Task.FromResult(0);
|
return Task.FromResult(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void DismissLocalNotification(string notificationId) { }
|
||||||
|
|
||||||
|
public void SendLocalNotification(string title, string message, string notificationId) { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
#if !FDROID
|
#if !FDROID
|
||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Bit.App.Abstractions;
|
using Bit.App.Abstractions;
|
||||||
|
using Bit.App.Pages;
|
||||||
|
using Bit.App.Resources;
|
||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
|
@ -26,6 +29,7 @@ namespace Bit.App.Services
|
||||||
private IAppIdService _appIdService;
|
private IAppIdService _appIdService;
|
||||||
private IApiService _apiService;
|
private IApiService _apiService;
|
||||||
private IMessagingService _messagingService;
|
private IMessagingService _messagingService;
|
||||||
|
private IPushNotificationService _pushNotificationService;
|
||||||
|
|
||||||
public async Task OnMessageAsync(JObject value, string deviceType)
|
public async Task OnMessageAsync(JObject value, string deviceType)
|
||||||
{
|
{
|
||||||
|
@ -125,6 +129,27 @@ namespace Bit.App.Services
|
||||||
_messagingService.Send("logout");
|
_messagingService.Send("logout");
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case NotificationType.AuthRequest:
|
||||||
|
var passwordlessLoginMessage = JsonConvert.DeserializeObject<PasswordlessRequestNotification>(notification.Payload);
|
||||||
|
|
||||||
|
// if the user has not enabled passwordless logins ignore requests
|
||||||
|
if (!await _stateService.GetApprovePasswordlessLoginsAsync(passwordlessLoginMessage?.UserId))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if there is a request modal opened ignore all incoming requests
|
||||||
|
if (App.Current.MainPage.Navigation.ModalStack.Any(p => p is NavigationPage navPage && navPage.CurrentPage is LoginPasswordlessPage))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _stateService.SetPasswordlessLoginNotificationAsync(passwordlessLoginMessage, passwordlessLoginMessage?.UserId);
|
||||||
|
var userEmail = await _stateService.GetEmailAsync(passwordlessLoginMessage?.UserId);
|
||||||
|
|
||||||
|
_pushNotificationService.SendLocalNotification(AppResources.LogInRequested, String.Format(AppResources.ConfimLogInAttempForX, userEmail), Constants.PasswordlessNotificationId);
|
||||||
|
_messagingService.Send("passwordlessLoginRequest", passwordlessLoginMessage);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -204,6 +229,7 @@ namespace Bit.App.Services
|
||||||
_appIdService = ServiceContainer.Resolve<IAppIdService>("appIdService");
|
_appIdService = ServiceContainer.Resolve<IAppIdService>("appIdService");
|
||||||
_apiService = ServiceContainer.Resolve<IApiService>("apiService");
|
_apiService = ServiceContainer.Resolve<IApiService>("apiService");
|
||||||
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
||||||
|
_pushNotificationService = ServiceContainer.Resolve<IPushNotificationService>();
|
||||||
_resolved = true;
|
_resolved = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,6 +71,6 @@
|
||||||
<Color x:Key="NavigationBarTextColor">#ffffff</Color>
|
<Color x:Key="NavigationBarTextColor">#ffffff</Color>
|
||||||
|
|
||||||
<Color x:Key="HyperlinkColor">#52bdfb</Color>
|
<Color x:Key="HyperlinkColor">#52bdfb</Color>
|
||||||
|
<Color x:Key="FingerprintPhrase">#F08DC7</Color>
|
||||||
<Color x:Key="ScanningToggleModeTextColor">#80BDFF</Color>
|
<Color x:Key="ScanningToggleModeTextColor">#80BDFF</Color>
|
||||||
</ResourceDictionary>
|
</ResourceDictionary>
|
||||||
|
|
|
@ -71,6 +71,6 @@
|
||||||
<Color x:Key="NavigationBarTextColor">#ffffff</Color>
|
<Color x:Key="NavigationBarTextColor">#ffffff</Color>
|
||||||
|
|
||||||
<Color x:Key="HyperlinkColor">#52bdfb</Color>
|
<Color x:Key="HyperlinkColor">#52bdfb</Color>
|
||||||
|
<Color x:Key="FingerprintPhrase">#F08DC7</Color>
|
||||||
<Color x:Key="ScanningToggleModeTextColor">#80BDFF</Color>
|
<Color x:Key="ScanningToggleModeTextColor">#80BDFF</Color>
|
||||||
</ResourceDictionary>
|
</ResourceDictionary>
|
||||||
|
|
|
@ -71,6 +71,6 @@
|
||||||
<Color x:Key="NavigationBarTextColor">#ffffff</Color>
|
<Color x:Key="NavigationBarTextColor">#ffffff</Color>
|
||||||
|
|
||||||
<Color x:Key="HyperlinkColor">#175DDC</Color>
|
<Color x:Key="HyperlinkColor">#175DDC</Color>
|
||||||
|
<Color x:Key="FingerprintPhrase">#C01176</Color>
|
||||||
<Color x:Key="ScanningToggleModeTextColor">#80BDFF</Color>
|
<Color x:Key="ScanningToggleModeTextColor">#80BDFF</Color>
|
||||||
</ResourceDictionary>
|
</ResourceDictionary>
|
||||||
|
|
|
@ -71,6 +71,6 @@
|
||||||
<Color x:Key="NavigationBarTextColor">#e5e9f0</Color>
|
<Color x:Key="NavigationBarTextColor">#e5e9f0</Color>
|
||||||
|
|
||||||
<Color x:Key="HyperlinkColor">#81a1c1</Color>
|
<Color x:Key="HyperlinkColor">#81a1c1</Color>
|
||||||
|
<Color x:Key="FingerprintPhrase">#F08DC7</Color>
|
||||||
<Color x:Key="ScanningToggleModeTextColor">#80BDFF</Color>
|
<Color x:Key="ScanningToggleModeTextColor">#80BDFF</Color>
|
||||||
</ResourceDictionary>
|
</ResourceDictionary>
|
||||||
|
|
|
@ -83,6 +83,8 @@ namespace Bit.Core.Abstractions
|
||||||
Task<SendResponse> PutSendAsync(string id, SendRequest request);
|
Task<SendResponse> PutSendAsync(string id, SendRequest request);
|
||||||
Task<SendResponse> PutSendRemovePasswordAsync(string id);
|
Task<SendResponse> PutSendRemovePasswordAsync(string id);
|
||||||
Task DeleteSendAsync(string id);
|
Task DeleteSendAsync(string id);
|
||||||
|
Task<PasswordlessLoginResponse> GetAuthRequestAsync(string id);
|
||||||
|
Task<PasswordlessLoginResponse> PutAuthRequestAsync(string id, string key, string masterPasswordHash, string deviceIdentifier, bool requestApproved);
|
||||||
Task<string> GetUsernameFromAsync(ForwardedEmailServiceType service, UsernameGeneratorConfig config);
|
Task<string> GetUsernameFromAsync(ForwardedEmailServiceType service, UsernameGeneratorConfig config);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.Domain;
|
using Bit.Core.Models.Domain;
|
||||||
|
using Bit.Core.Models.Response;
|
||||||
|
|
||||||
namespace Bit.Core.Abstractions
|
namespace Bit.Core.Abstractions
|
||||||
{
|
{
|
||||||
|
@ -25,6 +26,10 @@ namespace Bit.Core.Abstractions
|
||||||
Task<AuthResult> LogInSsoAsync(string code, string codeVerifier, string redirectUrl, string orgId);
|
Task<AuthResult> LogInSsoAsync(string code, string codeVerifier, string redirectUrl, string orgId);
|
||||||
Task<AuthResult> LogInCompleteAsync(string email, string masterPassword, TwoFactorProviderType twoFactorProvider, string twoFactorToken, bool? remember = null);
|
Task<AuthResult> LogInCompleteAsync(string email, string masterPassword, TwoFactorProviderType twoFactorProvider, string twoFactorToken, bool? remember = null);
|
||||||
Task<AuthResult> LogInTwoFactorAsync(TwoFactorProviderType twoFactorProvider, string twoFactorToken, string captchaToken, bool? remember = null);
|
Task<AuthResult> LogInTwoFactorAsync(TwoFactorProviderType twoFactorProvider, string twoFactorToken, string captchaToken, bool? remember = null);
|
||||||
|
|
||||||
|
Task<PasswordlessLoginResponse> GetPasswordlessLoginRequestByIdAsync(string id);
|
||||||
|
Task<PasswordlessLoginResponse> PasswordlessLoginAsync(string id, string pubKey, bool requestApproved);
|
||||||
|
|
||||||
void LogOut(Action callback);
|
void LogOut(Action callback);
|
||||||
void Init();
|
void Init();
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ using System.Threading.Tasks;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
using Bit.Core.Models.Domain;
|
using Bit.Core.Models.Domain;
|
||||||
|
using Bit.Core.Models.Response;
|
||||||
using Bit.Core.Models.View;
|
using Bit.Core.Models.View;
|
||||||
|
|
||||||
namespace Bit.Core.Abstractions
|
namespace Bit.Core.Abstractions
|
||||||
|
@ -151,6 +152,10 @@ namespace Bit.Core.Abstractions
|
||||||
Task<bool> GetScreenCaptureAllowedAsync(string userId = null);
|
Task<bool> GetScreenCaptureAllowedAsync(string userId = null);
|
||||||
Task SetScreenCaptureAllowedAsync(bool value, string userId = null);
|
Task SetScreenCaptureAllowedAsync(bool value, string userId = null);
|
||||||
Task SaveExtensionActiveUserIdToStorageAsync(string userId);
|
Task SaveExtensionActiveUserIdToStorageAsync(string userId);
|
||||||
|
Task<bool> GetApprovePasswordlessLoginsAsync(string userId = null);
|
||||||
|
Task SetApprovePasswordlessLoginsAsync(bool? value, string userId = null);
|
||||||
|
Task<PasswordlessRequestNotification> GetPasswordlessLoginNotificationAsync(string userId = null);
|
||||||
|
Task SetPasswordlessLoginNotificationAsync(PasswordlessRequestNotification value, string userId = null);
|
||||||
Task<UsernameGenerationOptions> GetUsernameGenerationOptionsAsync(string userId = null);
|
Task<UsernameGenerationOptions> GetUsernameGenerationOptionsAsync(string userId = null);
|
||||||
Task SetUsernameGenerationOptionsAsync(UsernameGenerationOptions value, string userId = null);
|
Task SetUsernameGenerationOptionsAsync(UsernameGenerationOptions value, string userId = null);
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,8 @@
|
||||||
public static string EventCollectionKey = "eventCollection";
|
public static string EventCollectionKey = "eventCollection";
|
||||||
public static string RememberedEmailKey = "rememberedEmail";
|
public static string RememberedEmailKey = "rememberedEmail";
|
||||||
public static string RememberedOrgIdentifierKey = "rememberedOrgIdentifier";
|
public static string RememberedOrgIdentifierKey = "rememberedOrgIdentifier";
|
||||||
|
public const string PasswordlessNotificationId = "26072022";
|
||||||
|
public const string AndroidNotificationChannelId = "general_notification_channel";
|
||||||
public const int SelectFileRequestCode = 42;
|
public const int SelectFileRequestCode = 42;
|
||||||
public const int SelectFilePermissionRequestCode = 43;
|
public const int SelectFilePermissionRequestCode = 43;
|
||||||
public const int SaveFileRequestCode = 44;
|
public const int SaveFileRequestCode = 44;
|
||||||
|
@ -85,6 +87,8 @@
|
||||||
public static string ProtectedPinKey(string userId) => $"protectedPin_{userId}";
|
public static string ProtectedPinKey(string userId) => $"protectedPin_{userId}";
|
||||||
public static string LastSyncKey(string userId) => $"lastSync_{userId}";
|
public static string LastSyncKey(string userId) => $"lastSync_{userId}";
|
||||||
public static string BiometricUnlockKey(string userId) => $"biometricUnlock_{userId}";
|
public static string BiometricUnlockKey(string userId) => $"biometricUnlock_{userId}";
|
||||||
|
public static string ApprovePasswordlessLoginsKey(string userId) => $"approvePasswordlessLogins_{userId}";
|
||||||
|
public static string PasswordlessLoginNofiticationKey(string userId) => $"passwordlessLoginNofitication_{userId}";
|
||||||
public static string UsernameGenOptionsKey(string userId) => $"usernameGenerationOptions_{userId}";
|
public static string UsernameGenOptionsKey(string userId) => $"usernameGenerationOptions_{userId}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,5 +16,12 @@
|
||||||
SyncSettings = 10,
|
SyncSettings = 10,
|
||||||
|
|
||||||
LogOut = 11,
|
LogOut = 11,
|
||||||
|
|
||||||
|
SyncSendCreate = 12,
|
||||||
|
SyncSendUpdate = 13,
|
||||||
|
SyncSendDelete = 14,
|
||||||
|
|
||||||
|
AuthRequest = 15,
|
||||||
|
AuthRequestResponse = 16,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
21
src/Core/Models/Request/PasswordlessLoginRequest.cs
Normal file
21
src/Core/Models/Request/PasswordlessLoginRequest.cs
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Bit.Core.Models.Request
|
||||||
|
{
|
||||||
|
public class PasswordlessLoginRequest
|
||||||
|
{
|
||||||
|
public PasswordlessLoginRequest(string key, string masterPasswordHash, string deviceIdentifier,
|
||||||
|
bool requestApproved)
|
||||||
|
{
|
||||||
|
Key = key ?? throw new ArgumentNullException(nameof(key));
|
||||||
|
MasterPasswordHash = masterPasswordHash ?? throw new ArgumentNullException(nameof(masterPasswordHash));
|
||||||
|
DeviceIdentifier = deviceIdentifier ?? throw new ArgumentNullException(nameof(deviceIdentifier));
|
||||||
|
RequestApproved = requestApproved;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Key { get; set; }
|
||||||
|
public string MasterPasswordHash { get; set; }
|
||||||
|
public string DeviceIdentifier { get; set; }
|
||||||
|
public bool RequestApproved { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -33,4 +33,10 @@ namespace Bit.Core.Models.Response
|
||||||
public string UserId { get; set; }
|
public string UserId { get; set; }
|
||||||
public DateTime Date { get; set; }
|
public DateTime Date { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class PasswordlessRequestNotification
|
||||||
|
{
|
||||||
|
public string UserId { get; set; }
|
||||||
|
public string Id { get; set; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
19
src/Core/Models/Response/PasswordlessLoginResponse.cs
Normal file
19
src/Core/Models/Response/PasswordlessLoginResponse.cs
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
using System;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
|
||||||
|
namespace Bit.Core.Models.Response
|
||||||
|
{
|
||||||
|
public class PasswordlessLoginResponse
|
||||||
|
{
|
||||||
|
public string Id { get; set; }
|
||||||
|
public string PublicKey { get; set; }
|
||||||
|
public string RequestDeviceType { get; set; }
|
||||||
|
public string RequestIpAddress { get; set; }
|
||||||
|
public string RequestFingerprint { get; set; }
|
||||||
|
public string Key { get; set; }
|
||||||
|
public string MasterPasswordHash { get; set; }
|
||||||
|
public DateTime CreationDate { get; set; }
|
||||||
|
public bool RequestApproved { get; set; }
|
||||||
|
public string Origin { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -534,6 +534,21 @@ namespace Bit.Core.Services
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region PasswordlessLogin
|
||||||
|
|
||||||
|
public Task<PasswordlessLoginResponse> GetAuthRequestAsync(string id)
|
||||||
|
{
|
||||||
|
return SendAsync<object, PasswordlessLoginResponse>(HttpMethod.Get, $"/auth-requests/{id}", null, true, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<PasswordlessLoginResponse> PutAuthRequestAsync(string id, string encKey, string encMasterPasswordHash, string deviceIdentifier, bool requestApproved)
|
||||||
|
{
|
||||||
|
var request = new PasswordlessLoginRequest(encKey, encMasterPasswordHash, deviceIdentifier, requestApproved);
|
||||||
|
return SendAsync<object, PasswordlessLoginResponse>(HttpMethod.Put, $"/auth-requests/{id}", request, true, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
#region Helpers
|
#region Helpers
|
||||||
|
|
||||||
public async Task<string> GetActiveBearerTokenAsync()
|
public async Task<string> GetActiveBearerTokenAsync()
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Models.Domain;
|
using Bit.Core.Models.Domain;
|
||||||
using Bit.Core.Models.Request;
|
using Bit.Core.Models.Request;
|
||||||
|
using Bit.Core.Models.Response;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
namespace Bit.Core.Services
|
namespace Bit.Core.Services
|
||||||
|
@ -468,5 +470,20 @@ namespace Bit.Core.Services
|
||||||
TwoFactorProvidersData = null;
|
TwoFactorProvidersData = null;
|
||||||
SelectedTwoFactorProviderType = null;
|
SelectedTwoFactorProviderType = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<PasswordlessLoginResponse> GetPasswordlessLoginRequestByIdAsync(string id)
|
||||||
|
{
|
||||||
|
return await _apiService.GetAuthRequestAsync(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<PasswordlessLoginResponse> PasswordlessLoginAsync(string id, string pubKey, bool requestApproved)
|
||||||
|
{
|
||||||
|
var publicKey = CoreHelpers.Base64UrlDecode(pubKey);
|
||||||
|
var masterKey = await _cryptoService.GetKeyAsync();
|
||||||
|
var encryptedKey = await _cryptoService.RsaEncryptAsync(masterKey.EncKey, publicKey);
|
||||||
|
var encryptedMasterPassword = await _cryptoService.RsaEncryptAsync(Encoding.UTF8.GetBytes(await _stateService.GetKeyHashAsync()), publicKey);
|
||||||
|
var deviceId = await _appIdService.GetAppIdAsync();
|
||||||
|
return await _apiService.PutAuthRequestAsync(id, encryptedKey.EncryptedString, encryptedMasterPassword.EncryptedString, deviceId, requestApproved);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
using Bit.Core.Models.Domain;
|
using Bit.Core.Models.Domain;
|
||||||
|
using Bit.Core.Models.Response;
|
||||||
using Bit.Core.Models.View;
|
using Bit.Core.Models.View;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
|
@ -1260,6 +1261,37 @@ namespace Bit.Core.Services
|
||||||
await SetValueAsync(key, value, reconciledOptions);
|
await SetValueAsync(key, value, reconciledOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<bool> GetApprovePasswordlessLoginsAsync(string userId = null)
|
||||||
|
{
|
||||||
|
var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId },
|
||||||
|
await GetDefaultStorageOptionsAsync());
|
||||||
|
var key = Constants.ApprovePasswordlessLoginsKey(reconciledOptions.UserId);
|
||||||
|
return await GetValueAsync<bool?>(key, reconciledOptions) ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SetApprovePasswordlessLoginsAsync(bool? value, string userId = null)
|
||||||
|
{
|
||||||
|
var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId },
|
||||||
|
await GetDefaultStorageOptionsAsync());
|
||||||
|
var key = Constants.ApprovePasswordlessLoginsKey(reconciledOptions.UserId);
|
||||||
|
await SetValueAsync(key, value, reconciledOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<PasswordlessRequestNotification> GetPasswordlessLoginNotificationAsync(string userId = null)
|
||||||
|
{
|
||||||
|
var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId },
|
||||||
|
await GetDefaultStorageOptionsAsync());
|
||||||
|
var key = Constants.PasswordlessLoginNofiticationKey(reconciledOptions.UserId);
|
||||||
|
return await GetValueAsync<PasswordlessRequestNotification>(key, reconciledOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SetPasswordlessLoginNotificationAsync(PasswordlessRequestNotification value, string userId = null)
|
||||||
|
{
|
||||||
|
var reconciledOptions = ReconcileOptions(new StorageOptions { UserId = userId },
|
||||||
|
await GetDefaultStorageOptionsAsync());
|
||||||
|
var key = Constants.PasswordlessLoginNofiticationKey(reconciledOptions.UserId);
|
||||||
|
await SetValueAsync(key, value, reconciledOptions);
|
||||||
|
}
|
||||||
// Helpers
|
// Helpers
|
||||||
|
|
||||||
private async Task<T> GetValueAsync<T>(string key, StorageOptions options)
|
private async Task<T> GetValueAsync<T>(string key, StorageOptions options)
|
||||||
|
@ -1455,6 +1487,7 @@ namespace Bit.Core.Services
|
||||||
await SetEncryptedPasswordGenerationHistoryAsync(null, userId);
|
await SetEncryptedPasswordGenerationHistoryAsync(null, userId);
|
||||||
await SetEncryptedSendsAsync(null, userId);
|
await SetEncryptedSendsAsync(null, userId);
|
||||||
await SetSettingsAsync(null, userId);
|
await SetSettingsAsync(null, userId);
|
||||||
|
await SetApprovePasswordlessLoginsAsync(null, userId);
|
||||||
|
|
||||||
if (userInitiated)
|
if (userInitiated)
|
||||||
{
|
{
|
||||||
|
@ -1474,6 +1507,7 @@ namespace Bit.Core.Services
|
||||||
await SetAutoDarkThemeAsync(null, userId);
|
await SetAutoDarkThemeAsync(null, userId);
|
||||||
await SetAddSitePromptShownAsync(null, userId);
|
await SetAddSitePromptShownAsync(null, userId);
|
||||||
await SetPasswordGenerationOptionsAsync(null, userId);
|
await SetPasswordGenerationOptionsAsync(null, userId);
|
||||||
|
await SetApprovePasswordlessLoginsAsync(null, userId);
|
||||||
await SetUsernameGenerationOptionsAsync(null, userId);
|
await SetUsernameGenerationOptionsAsync(null, userId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -643,5 +643,11 @@ namespace Bit.iOS.Core.Services
|
||||||
_deviceActionService.PickedDocument(url);
|
_deviceActionService.PickedDocument(url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void OpenAppSettings()
|
||||||
|
{
|
||||||
|
var url = new NSUrl(UIApplication.OpenSettingsUrlString);
|
||||||
|
UIApplication.SharedApplication.OpenUrl(url);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ using Bit.iOS.Services;
|
||||||
using CoreNFC;
|
using CoreNFC;
|
||||||
using Foundation;
|
using Foundation;
|
||||||
using UIKit;
|
using UIKit;
|
||||||
|
using UserNotifications;
|
||||||
using Xamarin.Forms;
|
using Xamarin.Forms;
|
||||||
using Xamarin.Forms.Platform.iOS;
|
using Xamarin.Forms.Platform.iOS;
|
||||||
|
|
||||||
|
@ -56,7 +57,6 @@ namespace Bit.iOS
|
||||||
LoadApplication(new App.App(null));
|
LoadApplication(new App.App(null));
|
||||||
iOSCoreHelpers.AppearanceAdjustments();
|
iOSCoreHelpers.AppearanceAdjustments();
|
||||||
ZXing.Net.Mobile.Forms.iOS.Platform.Init();
|
ZXing.Net.Mobile.Forms.iOS.Platform.Init();
|
||||||
|
|
||||||
_broadcasterService.Subscribe(nameof(AppDelegate), async (message) =>
|
_broadcasterService.Subscribe(nameof(AppDelegate), async (message) =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|
|
@ -83,7 +83,6 @@ namespace Bit.iOS.Services
|
||||||
public void WillPresentNotification(UNUserNotificationCenter center, UNNotification notification, Action<UNNotificationPresentationOptions> completionHandler)
|
public void WillPresentNotification(UNUserNotificationCenter center, UNNotification notification, Action<UNNotificationPresentationOptions> completionHandler)
|
||||||
{
|
{
|
||||||
Debug.WriteLine($"{TAG} WillPresentNotification {notification?.Request?.Content?.UserInfo}");
|
Debug.WriteLine($"{TAG} WillPresentNotification {notification?.Request?.Content?.UserInfo}");
|
||||||
|
|
||||||
OnMessageReceived(notification?.Request?.Content?.UserInfo);
|
OnMessageReceived(notification?.Request?.Content?.UserInfo);
|
||||||
completionHandler(UNNotificationPresentationOptions.Alert);
|
completionHandler(UNNotificationPresentationOptions.Alert);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
using System.Diagnostics;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Bit.App.Abstractions;
|
using Bit.App.Abstractions;
|
||||||
|
using Bit.Core.Services;
|
||||||
using Foundation;
|
using Foundation;
|
||||||
using UIKit;
|
using UIKit;
|
||||||
using UserNotifications;
|
using UserNotifications;
|
||||||
|
@ -19,6 +23,12 @@ namespace Bit.iOS.Services
|
||||||
|
|
||||||
public bool IsRegisteredForPush => UIApplication.SharedApplication.IsRegisteredForRemoteNotifications;
|
public bool IsRegisteredForPush => UIApplication.SharedApplication.IsRegisteredForRemoteNotifications;
|
||||||
|
|
||||||
|
public async Task<bool> AreNotificationsSettingsEnabledAsync()
|
||||||
|
{
|
||||||
|
var settings = await UNUserNotificationCenter.Current.GetNotificationSettingsAsync();
|
||||||
|
return settings.AlertSetting == UNNotificationSetting.Enabled;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task RegisterAsync()
|
public async Task RegisterAsync()
|
||||||
{
|
{
|
||||||
Debug.WriteLine($"{TAG} RegisterAsync");
|
Debug.WriteLine($"{TAG} RegisterAsync");
|
||||||
|
@ -58,5 +68,39 @@ namespace Bit.iOS.Services
|
||||||
NSUserDefaults.StandardUserDefaults.Synchronize();
|
NSUserDefaults.StandardUserDefaults.Synchronize();
|
||||||
return Task.FromResult(0);
|
return Task.FromResult(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void SendLocalNotification(string title, string message, string notificationId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(notificationId))
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException("notificationId cannot be null or empty.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var content = new UNMutableNotificationContent()
|
||||||
|
{
|
||||||
|
Title = title,
|
||||||
|
Body = message
|
||||||
|
};
|
||||||
|
|
||||||
|
var request = UNNotificationRequest.FromIdentifier(notificationId, content, null);
|
||||||
|
UNUserNotificationCenter.Current.AddNotificationRequest(request, (err) =>
|
||||||
|
{
|
||||||
|
if (err != null)
|
||||||
|
{
|
||||||
|
Logger.Instance.Exception(new Exception($"Failed to schedule notification: {err}"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DismissLocalNotification(string notificationId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(notificationId))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
UNUserNotificationCenter.Current.RemovePendingNotificationRequests(new string[] { notificationId });
|
||||||
|
UNUserNotificationCenter.Current.RemoveDeliveredNotifications(new string[] { notificationId });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in a new issue