1
0
mirror of https://github.com/bitwarden/mobile synced 2025-12-05 23:53:33 +00:00

Compare commits

...

10 Commits

Author SHA1 Message Date
github-actions[bot]
c50028d0d3 Bumped version to 2022.10.0 (#2130)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
(cherry picked from commit a5ad43b134)
2022-10-12 17:43:40 +01:00
André Bispo
0b9a16beef [SG-690] DateTime to Utc fix (#2115)
* [SG-690] DateTime to Utc fix

* [SG-690] Removed Utc from server side datetime.

(cherry picked from commit 1e5eab0574)
2022-10-05 16:24:04 -04:00
André Bispo
9b93dbb8e3 [SG-687] added try catch to cancellation token disposal. (#2114) 2022-10-04 20:27:51 +01:00
André Bispo
261610b700 [SG-691] Login request is not displayed after changing accounts (#2111) 2022-10-04 11:47:23 +01:00
André Bispo
fd18dccce9 [SG-687] Request time not updating (#2108) 2022-10-04 11:46:34 +01:00
André Bispo
04daaf4e3a [SG-690] Login Request does not disappear after 15 minutes (#2106) 2022-10-04 11:44:49 +01:00
André Bispo
a08d89a002 [SG-696] Android notification icon blank (#2105) 2022-10-04 11:42:56 +01:00
Carlos Gonçalves
63e1185537 [SG-666][SG-667] Email is not prefilled and username isn't generated automatically (#2109)
* SG-666 SG-667 - Email is now prefilled for plus addressed email username type
* Username is auto generated upon navigation

* SG-666 - Fixed PR comments
* Added missing property initialization

(cherry picked from commit a890ee6612)
2022-10-03 12:40:15 -04:00
mp-bw
1e8a6ca81f added a11y disclosure prompt for Android (#2102) 2022-09-28 10:46:11 -04:00
André Bispo
6fe7e9ce1b 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

(cherry picked from commit f9a32e4abc)
2022-09-26 20:43:34 -04:00
53 changed files with 7301 additions and 4088 deletions

View File

@@ -217,6 +217,10 @@
<AndroidResource Include="Resources\drawable-v26\splash_screen_round.xml" /> <AndroidResource Include="Resources\drawable-v26\splash_screen_round.xml" />
<AndroidResource Include="Resources\drawable\logo_rounded.xml" /> <AndroidResource Include="Resources\drawable\logo_rounded.xml" />
<AndroidResource Include="Resources\drawable-night-v26\splash_screen_round.xml" /> <AndroidResource Include="Resources\drawable-night-v26\splash_screen_round.xml" />
<AndroidResource Include="Resources\drawable\ic_notification.xml">
<SubType></SubType>
<Generator></Generator>
</AndroidResource>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<AndroidResource Include="Resources\drawable\splash_screen.xml" /> <AndroidResource Include="Resources\drawable\splash_screen.xml" />

View File

@@ -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

View File

@@ -72,7 +72,8 @@ namespace Bit.Droid
ServiceContainer.Resolve<IStateService>("stateService"), ServiceContainer.Resolve<IStateService>("stateService"),
ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService"), ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService"),
ServiceContainer.Resolve<IAuthService>("authService"), ServiceContainer.Resolve<IAuthService>("authService"),
ServiceContainer.Resolve<ILogger>("logger")); ServiceContainer.Resolve<ILogger>("logger"),
ServiceContainer.Resolve<IMessagingService>("messagingService"));
ServiceContainer.Register<IAccountsManager>("accountsManager", accountsManager); ServiceContainer.Register<IAccountsManager>("accountsManager", accountsManager);
} }
#if !FDROID #if !FDROID

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:versionCode="1" android:versionName="2022.9.2" android:installLocation="internalOnly" package="com.x8bit.bitwarden"> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:versionCode="1" android:versionName="2022.10.0" android:installLocation="internalOnly" package="com.x8bit.bitwarden">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="32" /> <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="32" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.NFC" /> <uses-permission android:name="android.permission.NFC" />

View File

@@ -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);
} }
} }
} }

View File

@@ -0,0 +1,4 @@
<vector android:height="24dp" android:viewportHeight="420"
android:viewportWidth="420" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FFFFFF" android:pathData="M350.43,40.516C347.563,37.65 344.153,36.178 340.281,36.178L79.487,36.178C75.538,36.178 72.206,37.65 69.338,40.516C66.472,43.384 65,46.716 65,50.665L65,224.527C65,237.466 67.557,250.405 72.593,263.112C77.629,275.895 83.904,287.207 91.42,297.046C98.857,306.964 107.768,316.571 118.149,325.869C128.455,335.242 138.063,342.99 146.817,349.19C155.573,355.387 164.715,361.198 174.243,366.699C183.773,372.2 190.514,375.919 194.543,377.933C198.572,379.871 201.749,381.421 204.151,382.426C205.932,383.357 207.948,383.821 210.04,383.821C212.131,383.821 214.145,383.357 215.929,382.426C218.329,381.344 221.584,379.871 225.534,377.933C229.563,375.997 236.304,372.2 245.832,366.699C255.365,361.198 264.506,355.311 273.262,349.19C282.017,342.99 291.545,335.242 301.928,325.869C312.232,316.493 321.142,306.886 328.657,297.046C336.096,287.129 342.372,275.819 347.407,263.112C352.444,250.328 355,237.466 355,224.527L355,50.665C354.768,46.716 353.296,43.384 350.43,40.516ZM316.804,226.154C316.804,289.067 209.883,343.302 209.883,343.302L209.883,73.368L316.804,73.368C316.804,73.368 316.804,163.242 316.804,226.154Z"/>
</vector>

View File

@@ -6,4 +6,5 @@
android:accessibilityFeedbackType="feedbackGeneric" android:accessibilityFeedbackType="feedbackGeneric"
android:accessibilityFlags="flagReportViewIds|flagRetrieveInteractiveWindows" android:accessibilityFlags="flagReportViewIds|flagRetrieveInteractiveWindows"
android:notificationTimeout="100" android:notificationTimeout="100"
android:canRetrieveWindowContent="true"/> android:canRetrieveWindowContent="true"
android:isAccessibilityTool="false"/>

View File

@@ -1,9 +1,14 @@
#if !FDROID #if !FDROID
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Android.App;
using Android.Content;
using Android.OS;
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 Bit.Droid.Utilities;
using Xamarin.Forms; using Xamarin.Forms;
namespace Bit.Droid.Services namespace Bit.Droid.Services
@@ -23,6 +28,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 +57,39 @@ 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 pendingIntentFlags = AndroidHelpers.AddPendingIntentMutabilityFlag(PendingIntentFlags.UpdateCurrent, true);
var pendingIntent = PendingIntent.GetActivity(context, 20220801, intent, pendingIntentFlags);
var builder = new NotificationCompat.Builder(context, Constants.AndroidNotificationChannelId)
.SetContentIntent(pendingIntent)
.SetContentTitle(title)
.SetContentText(message)
.SetTimeoutAfter(Constants.PasswordlessNotificationTimeoutInMinutes * 60000)
.SetSmallIcon(Resource.Drawable.ic_notification)
.SetColor((int)Android.Graphics.Color.White)
.SetAutoCancel(true);
var notificationManager = NotificationManagerCompat.From(context);
notificationManager.Notify(int.Parse(notificationId), builder.Build());
}
} }
} }
#endif #endif

View File

@@ -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);
}
} }
} }

View File

@@ -49,5 +49,6 @@ namespace Bit.App.Abstractions
float GetSystemFontSizeScale(); float GetSystemFontSizeScale();
Task OnAccountSwitchCompleteAsync(); Task OnAccountSwitchCompleteAsync();
Task SetScreenCaptureAllowedAsync(); Task SetScreenCaptureAllowedAsync();
void OpenAppSettings();
} }
} }

View File

@@ -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);
} }
} }

View File

@@ -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>

View File

@@ -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" || message.Command == AccountsManagerMessageCommands.ACCOUNT_SWITCH_COMPLETED)
{
CheckPasswordlessLoginRequestsAsync().FireAndForget();
}
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -148,11 +152,55 @@ 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);
if (loginRequestData.CreationDate.AddMinutes(Constants.PasswordlessNotificationTimeoutInMinutes) > DateTime.UtcNow)
{
await Device.InvokeOnMainThreadAsync(() => 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 +212,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 +248,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();

View 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>

View File

@@ -0,0 +1,43 @@
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();
}
}
protected override void OnAppearing()
{
base.OnAppearing();
_vm.StartRequestTimeUpdater();
}
protected override void OnDisappearing()
{
base.OnDisappearing();
_vm.StopRequestTimeUpdater();
}
}
}

View File

@@ -0,0 +1,175 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
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.Services;
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;
private CancellationTokenSource _requestTimeCts;
private Task _requestTimeTask;
private const int REQUEST_TIME_UPDATE_PERIOD_IN_MINUTES = 5;
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),
});
}
}
public void StopRequestTimeUpdater()
{
try
{
_requestTimeCts?.Cancel();
_requestTimeCts?.Dispose();
}
catch (Exception ex)
{
_logger.Exception(ex);
}
}
public void StartRequestTimeUpdater()
{
try
{
_requestTimeCts?.Cancel();
_requestTimeCts = new CancellationTokenSource();
_requestTimeTask = new TimerTask(_logger, UpdateRequestTime, _requestTimeCts).RunPeriodic(TimeSpan.FromMinutes(REQUEST_TIME_UPDATE_PERIOD_IN_MINUTES));
}
catch (Exception ex)
{
_logger.Exception(ex);
}
}
private async Task UpdateRequestTime()
{
TriggerPropertyChanged(nameof(TimeOfRequestText));
if (DateTime.UtcNow > LoginRequest?.RequestDate.AddMinutes(Constants.PasswordlessNotificationTimeoutInMinutes))
{
StopRequestTimeUpdater();
await _platformUtilsService.ShowDialogAsync(AppResources.LoginRequestHasAlreadyExpired);
await Page.Navigation.PopModalAsync();
}
}
private async Task PasswordlessLoginAsync(bool approveRequest)
{
if (LoginRequest.RequestDate.AddMinutes(Constants.PasswordlessNotificationTimeoutInMinutes) <= DateTime.UtcNow)
{
await _platformUtilsService.ShowDialogAsync(AppResources.LoginRequestHasAlreadyExpired);
await Page.Navigation.PopModalAsync();
return;
}
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);
StopRequestTimeUpdater();
}
private string CreateRequestDate(DateTime? requestDate)
{
if (!requestDate.HasValue)
{
return string.Empty;
}
if (DateTime.UtcNow < requestDate.Value.ToUniversalTime().AddMinutes(REQUEST_TIME_UPDATE_PERIOD_IN_MINUTES))
{
return AppResources.JustNow;
}
return string.Format(AppResources.XMinutesAgo, DateTime.UtcNow.Minute - requestDate.Value.ToUniversalTime().Minute);
}
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; }
}
}

View File

@@ -20,6 +20,7 @@ namespace Bit.App.Pages
private readonly IPlatformUtilsService _platformUtilsService; private readonly IPlatformUtilsService _platformUtilsService;
private readonly IClipboardService _clipboardService; private readonly IClipboardService _clipboardService;
private readonly IUsernameGenerationService _usernameGenerationService; private readonly IUsernameGenerationService _usernameGenerationService;
private readonly ITokenService _tokenService;
readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger"); readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
private PasswordGenerationOptions _options; private PasswordGenerationOptions _options;
@@ -49,8 +50,6 @@ namespace Bit.App.Pages
private bool _showFirefoxRelayApiAccessToken; private bool _showFirefoxRelayApiAccessToken;
private bool _showAnonAddyApiAccessToken; private bool _showAnonAddyApiAccessToken;
private bool _showSimpleLoginApiKey; private bool _showSimpleLoginApiKey;
private UsernameEmailType _catchAllEmailTypeSelected;
private UsernameEmailType _plusAddressedEmailTypeSelected;
private bool _editMode; private bool _editMode;
public GeneratorPageViewModel() public GeneratorPageViewModel()
@@ -59,6 +58,7 @@ namespace Bit.App.Pages
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>(); _platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>();
_clipboardService = ServiceContainer.Resolve<IClipboardService>(); _clipboardService = ServiceContainer.Resolve<IClipboardService>();
_usernameGenerationService = ServiceContainer.Resolve<IUsernameGenerationService>(); _usernameGenerationService = ServiceContainer.Resolve<IUsernameGenerationService>();
_tokenService = ServiceContainer.Resolve<ITokenService>();
PageTitle = AppResources.Generator; PageTitle = AppResources.Generator;
GeneratorTypeOptions = new List<GeneratorType> { GeneratorTypeOptions = new List<GeneratorType> {
@@ -157,7 +157,7 @@ namespace Bit.App.Pages
public bool ShowUsernameEmailType public bool ShowUsernameEmailType
{ {
get => !string.IsNullOrWhiteSpace(EmailWebsite) || EditMode; get => !string.IsNullOrWhiteSpace(EmailWebsite);
} }
public int Length public int Length
@@ -367,7 +367,7 @@ namespace Bit.App.Pages
IsUsername = value == GeneratorType.Username; IsUsername = value == GeneratorType.Username;
TriggerPropertyChanged(nameof(GeneratorTypeSelected)); TriggerPropertyChanged(nameof(GeneratorTypeSelected));
SaveOptionsAsync().FireAndForget(); SaveOptionsAsync().FireAndForget();
SaveUsernameOptionsAsync(false).FireAndForget(); SaveUsernameOptionsAsync().FireAndForget();
} }
} }
} }
@@ -382,6 +382,7 @@ namespace Bit.App.Pages
IsPassword = value == 0; IsPassword = value == 0;
TriggerPropertyChanged(nameof(PasswordTypeSelectedIndex)); TriggerPropertyChanged(nameof(PasswordTypeSelectedIndex));
SaveOptionsAsync().FireAndForget(); SaveOptionsAsync().FireAndForget();
SaveUsernameOptionsAsync().FireAndForget();
} }
} }
} }
@@ -396,7 +397,7 @@ namespace Bit.App.Pages
_usernameOptions.Type = value; _usernameOptions.Type = value;
Username = Constants.DefaultUsernameGenerated; Username = Constants.DefaultUsernameGenerated;
TriggerPropertyChanged(nameof(UsernameTypeSelected), new string[] { nameof(UsernameTypeDescriptionLabel) }); TriggerPropertyChanged(nameof(UsernameTypeSelected), new string[] { nameof(UsernameTypeDescriptionLabel) });
SaveUsernameOptionsAsync(false).FireAndForget(); SaveUsernameOptionsAsync().FireAndForget();
} }
} }
} }
@@ -564,26 +565,28 @@ namespace Bit.App.Pages
public UsernameEmailType PlusAddressedEmailTypeSelected public UsernameEmailType PlusAddressedEmailTypeSelected
{ {
get => _plusAddressedEmailTypeSelected; get => _usernameOptions.PlusAddressedEmailType;
set set
{ {
if (SetProperty(ref _plusAddressedEmailTypeSelected, value)) if (_usernameOptions.PlusAddressedEmailType != value)
{ {
_usernameOptions.PlusAddressedEmailType = value; _usernameOptions.PlusAddressedEmailType = value;
SaveUsernameOptionsAsync(false).FireAndForget(); TriggerPropertyChanged(nameof(PlusAddressedEmailTypeSelected));
SaveUsernameOptionsAsync().FireAndForget();
} }
} }
} }
public UsernameEmailType CatchAllEmailTypeSelected public UsernameEmailType CatchAllEmailTypeSelected
{ {
get => _catchAllEmailTypeSelected; get => _usernameOptions.CatchAllEmailType;
set set
{ {
if (SetProperty(ref _catchAllEmailTypeSelected, value)) if (_usernameOptions.CatchAllEmailType != value)
{ {
_usernameOptions.CatchAllEmailType = value; _usernameOptions.CatchAllEmailType = value;
SaveUsernameOptionsAsync(false).FireAndForget(); TriggerPropertyChanged(nameof(CatchAllEmailTypeSelected));
SaveUsernameOptionsAsync().FireAndForget();
} }
} }
} }
@@ -601,16 +604,28 @@ namespace Bit.App.Pages
{ {
(_options, EnforcedPolicyOptions) = await _passwordGenerationService.GetOptionsAsync(); (_options, EnforcedPolicyOptions) = await _passwordGenerationService.GetOptionsAsync();
LoadFromOptions(); LoadFromOptions();
await RegenerateAsync();
_usernameOptions = await _usernameGenerationService.GetOptionsAsync(); _usernameOptions = await _usernameGenerationService.GetOptionsAsync();
_usernameOptions.PlusAddressedEmail = _tokenService.GetEmail();
_usernameOptions.EmailWebsite = EmailWebsite;
_usernameOptions.CatchAllEmailType = _usernameOptions.PlusAddressedEmailType = string.IsNullOrWhiteSpace(EmailWebsite) || !EditMode ? UsernameEmailType.Random : UsernameEmailType.Website;
if (!EditMode) if (!IsUsername)
{ {
_usernameOptions.CatchAllEmailType = _usernameOptions.PlusAddressedEmailType = UsernameEmailType.Random; await RegenerateAsync();
}
else
{
if (UsernameTypeSelected != UsernameType.ForwardedEmailAlias)
{
await RegenerateUsernameAsync();
}
else
{
Username = Constants.DefaultUsernameGenerated;
}
} }
TriggerUsernamePropertiesChanged(); TriggerUsernamePropertiesChanged();
Username = Constants.DefaultUsernameGenerated;
_doneIniting = true; _doneIniting = true;
} }
@@ -668,10 +683,14 @@ namespace Bit.App.Pages
await _usernameGenerationService.SaveOptionsAsync(_usernameOptions); await _usernameGenerationService.SaveOptionsAsync(_usernameOptions);
if (regenerate) if (regenerate && UsernameTypeSelected != UsernameType.ForwardedEmailAlias)
{ {
await RegenerateUsernameAsync(); await RegenerateUsernameAsync();
} }
else
{
Username = Constants.DefaultUsernameGenerated;
}
} }
public async Task SliderChangedAsync() public async Task SliderChangedAsync()

View File

@@ -83,7 +83,7 @@
StyleClass="box-value" StyleClass="box-value"
HorizontalOptions="End" /> HorizontalOptions="End" />
<Button <Button
Clicked="ToggleAccessibility" Command="{Binding ToggleAccessibilityCommand}"
StyleClass="box-overlay" StyleClass="box-overlay"
RelativeLayout.XConstraint="0" RelativeLayout.XConstraint="0"
RelativeLayout.YConstraint="0" RelativeLayout.YConstraint="0"

View File

@@ -55,14 +55,6 @@ namespace Bit.App.Pages
_vm.ToggleInlineAutofill(); _vm.ToggleInlineAutofill();
} }
private void ToggleAccessibility(object sender, EventArgs e)
{
if (DoOnce())
{
_vm.ToggleAccessibility();
}
}
private void ToggleDrawOver(object sender, EventArgs e) private void ToggleDrawOver(object sender, EventArgs e)
{ {
if (DoOnce()) if (DoOnce())

View File

@@ -1,9 +1,11 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Input;
using Bit.App.Abstractions; using Bit.App.Abstractions;
using Bit.App.Resources; using Bit.App.Resources;
using Bit.App.Services; using Bit.App.Services;
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Xamarin.CommunityToolkit.ObjectModel;
namespace Bit.App.Pages namespace Bit.App.Pages
{ {
@@ -12,6 +14,8 @@ namespace Bit.App.Pages
private readonly IDeviceActionService _deviceActionService; private readonly IDeviceActionService _deviceActionService;
private readonly IStateService _stateService; private readonly IStateService _stateService;
private readonly MobileI18nService _i18nService; private readonly MobileI18nService _i18nService;
private readonly IPlatformUtilsService _platformUtilsService;
readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
private bool _autofillServiceToggled; private bool _autofillServiceToggled;
private bool _inlineAutofillToggled; private bool _inlineAutofillToggled;
@@ -24,7 +28,11 @@ namespace Bit.App.Pages
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService"); _deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
_stateService = ServiceContainer.Resolve<IStateService>("stateService"); _stateService = ServiceContainer.Resolve<IStateService>("stateService");
_i18nService = ServiceContainer.Resolve<II18nService>("i18nService") as MobileI18nService; _i18nService = ServiceContainer.Resolve<II18nService>("i18nService") as MobileI18nService;
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
PageTitle = AppResources.AutofillServices; PageTitle = AppResources.AutofillServices;
ToggleAccessibilityCommand = new AsyncCommand(ToggleAccessibilityAsync,
onException: ex => _logger.Value.Exception(ex),
allowsMultipleExecutions: false);
} }
#region Autofill Service #region Autofill Service
@@ -74,6 +82,8 @@ namespace Bit.App.Pages
#region Accessibility #region Accessibility
public ICommand ToggleAccessibilityCommand { get; }
public string AccessibilityDescriptionLabel public string AccessibilityDescriptionLabel
{ {
get get
@@ -176,8 +186,18 @@ namespace Bit.App.Pages
InlineAutofillToggled = !InlineAutofillToggled; InlineAutofillToggled = !InlineAutofillToggled;
} }
public void ToggleAccessibility() public async Task ToggleAccessibilityAsync()
{ {
if (!_deviceActionService.AutofillAccessibilityServiceRunning())
{
var accept = await _platformUtilsService.ShowDialogAsync(AppResources.AccessibilityDisclosureText,
AppResources.AccessibilityServiceDisclosure, AppResources.Accept,
AppResources.Decline);
if (!accept)
{
return;
}
}
_deviceActionService.OpenAccessibilitySettings(); _deviceActionService.OpenAccessibilitySettings();
} }

View File

@@ -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()

View File

@@ -605,6 +605,8 @@ namespace Bit.App.Pages
return; return;
} }
var website = Cipher?.Login?.Uris?.FirstOrDefault()?.Host;
var page = new GeneratorPage(false, async (username) => var page = new GeneratorPage(false, async (username) =>
{ {
try try
@@ -617,7 +619,7 @@ namespace Bit.App.Pages
{ {
OnGenerateUsernameException(ex); OnGenerateUsernameException(ex);
} }
}, isUsernameGenerator: true, emailWebsite: Cipher?.Name, editMode: true); }, isUsernameGenerator: true, emailWebsite: website, editMode: true);
await Page.Navigation.PushModalAsync(new NavigationPage(page)); await Page.Navigation.PushModalAsync(new NavigationPage(page));
} }

File diff suppressed because it is too large Load Diff

View File

@@ -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>
@@ -2389,4 +2449,19 @@ select Add TOTP to store the key safely</value>
<data name="Random" xml:space="preserve"> <data name="Random" xml:space="preserve">
<value>Random</value> <value>Random</value>
</data> </data>
<data name="AccessibilityServiceDisclosure" xml:space="preserve">
<value>Accessibility Service Disclosure</value>
</data>
<data name="AccessibilityDisclosureText" xml:space="preserve">
<value>Bitwarden uses the Accessibility Service to search for login fields in apps and websites, then establish the appropriate field IDs for entering a username &amp; password when a match for the app or site is found. We do not store any of the information presented to us by the service, nor do we make any attempt to control any on-screen elements beyond text entry of credentials.</value>
</data>
<data name="Accept" xml:space="preserve">
<value>Accept</value>
</data>
<data name="Decline" xml:space="preserve">
<value>Decline</value>
</data>
<data name="LoginRequestHasAlreadyExpired" xml:space="preserve">
<value>Login request has already expired.</value>
</data>
</root> </root>

View File

@@ -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) { }
} }
} }

View File

@@ -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;
} }
} }

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -20,7 +20,7 @@ namespace Bit.App.Utilities.AccountManagement
private readonly IPlatformUtilsService _platformUtilsService; private readonly IPlatformUtilsService _platformUtilsService;
private readonly IAuthService _authService; private readonly IAuthService _authService;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IMessagingService _messagingService;
Func<AppOptions> _getOptionsFunc; Func<AppOptions> _getOptionsFunc;
private IAccountsManagerHost _accountsManagerHost; private IAccountsManagerHost _accountsManagerHost;
@@ -30,7 +30,8 @@ namespace Bit.App.Utilities.AccountManagement
IStateService stateService, IStateService stateService,
IPlatformUtilsService platformUtilsService, IPlatformUtilsService platformUtilsService,
IAuthService authService, IAuthService authService,
ILogger logger) ILogger logger,
IMessagingService messagingService)
{ {
_broadcasterService = broadcasterService; _broadcasterService = broadcasterService;
_vaultTimeoutService = vaultTimeoutService; _vaultTimeoutService = vaultTimeoutService;
@@ -39,6 +40,7 @@ namespace Bit.App.Utilities.AccountManagement
_platformUtilsService = platformUtilsService; _platformUtilsService = platformUtilsService;
_authService = authService; _authService = authService;
_logger = logger; _logger = logger;
_messagingService = messagingService;
} }
private AppOptions Options => _getOptionsFunc?.Invoke() ?? new AppOptions { IosExtension = true }; private AppOptions Options => _getOptionsFunc?.Invoke() ?? new AppOptions { IosExtension = true };
@@ -213,6 +215,7 @@ namespace Bit.App.Utilities.AccountManagement
} }
await Task.Delay(50); await Task.Delay(50);
await _accountsManagerHost.UpdateThemeAsync(); await _accountsManagerHost.UpdateThemeAsync();
_messagingService.Send(AccountsManagerMessageCommands.ACCOUNT_SWITCH_COMPLETED);
}); });
} }
} }

View File

@@ -10,13 +10,21 @@ namespace Bit.App.Utilities
{ {
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly Action _action; private readonly Action _action;
private readonly CancellationTokenSource _cancellationToken; private readonly Func<Task> _actionTask;
private readonly CancellationTokenSource _cancellationTokenSource;
public TimerTask(ILogger logger, Action action, CancellationTokenSource cancellationToken) public TimerTask(ILogger logger, Action action, CancellationTokenSource cancellationTokenSource)
{ {
_logger = logger; _logger = logger;
_action = action ?? throw new ArgumentNullException(); _action = action ?? throw new ArgumentNullException(nameof(action));
_cancellationToken = cancellationToken; _cancellationTokenSource = cancellationTokenSource;
}
public TimerTask(ILogger logger, Func<Task> actionTask, CancellationTokenSource cancellationTokenSource)
{
_logger = logger;
_actionTask = actionTask ?? throw new ArgumentNullException(nameof(actionTask));
_cancellationTokenSource = cancellationTokenSource;
} }
public Task RunPeriodic(TimeSpan? interval = null) public Task RunPeriodic(TimeSpan? interval = null)
@@ -26,23 +34,31 @@ namespace Bit.App.Utilities
{ {
try try
{ {
while (!_cancellationToken.IsCancellationRequested) while (!_cancellationTokenSource.IsCancellationRequested)
{ {
await Device.InvokeOnMainThreadAsync(() => await Device.InvokeOnMainThreadAsync(async () =>
{ {
if (!_cancellationToken.IsCancellationRequested) if (!_cancellationTokenSource.IsCancellationRequested)
{ {
try try
{
if (_action != null)
{ {
_action(); _action();
} }
else if (_actionTask != null)
{
await _actionTask();
}
}
catch (Exception ex) catch (Exception ex)
{ {
_cancellationTokenSource?.Cancel();
_logger?.Exception(ex); _logger?.Exception(ex);
} }
} }
}); });
await Task.Delay(interval.Value, _cancellationToken.Token); await Task.Delay(interval.Value, _cancellationTokenSource.Token);
} }
} }
catch (TaskCanceledException) { } catch (TaskCanceledException) { }
@@ -50,7 +66,7 @@ namespace Bit.App.Utilities
{ {
_logger?.Exception(ex); _logger?.Exception(ex);
} }
}, _cancellationToken.Token); }, _cancellationTokenSource.Token);
} }
} }
} }

View File

@@ -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);
} }
} }

View File

@@ -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();
} }

View File

@@ -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);
} }

View File

@@ -30,9 +30,13 @@
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;
public const int TotpDefaultTimer = 30;
public const int PasswordlessNotificationTimeoutInMinutes = 15;
public static readonly string[] AndroidAllClearCipherCacheKeys = public static readonly string[] AndroidAllClearCipherCacheKeys =
{ {
@@ -84,6 +88,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}";
} }
} }

View File

@@ -16,5 +16,12 @@
SyncSettings = 10, SyncSettings = 10,
LogOut = 11, LogOut = 11,
SyncSendCreate = 12,
SyncSendUpdate = 13,
SyncSendDelete = 14,
AuthRequest = 15,
AuthRequestResponse = 16,
} }
} }

View 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; }
}
}

View File

@@ -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; }
}
} }

View 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; }
}
}

View File

@@ -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()

View File

@@ -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);
}
} }
} }

View File

@@ -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);
} }
} }

View File

@@ -9,5 +9,6 @@
public const string ADD_ACCOUNT = "addAccount"; public const string ADD_ACCOUNT = "addAccount";
public const string ACCOUNT_ADDED = "accountAdded"; public const string ACCOUNT_ADDED = "accountAdded";
public const string SWITCHED_ACCOUNT = "switchedAccount"; public const string SWITCHED_ACCOUNT = "switchedAccount";
public const string ACCOUNT_SWITCH_COMPLETED = "accountSwitchCompleted";
} }
} }

View File

@@ -11,7 +11,7 @@
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
<string>com.8bit.bitwarden.autofill</string> <string>com.8bit.bitwarden.autofill</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>2022.9.2</string> <string>2022.10.0</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1</string> <string>1</string>
<key>CFBundleLocalizations</key> <key>CFBundleLocalizations</key>

View File

@@ -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);
}
} }
} }

View File

@@ -222,7 +222,8 @@ namespace Bit.iOS.Core.Utilities
ServiceContainer.Resolve<IStateService>("stateService"), ServiceContainer.Resolve<IStateService>("stateService"),
ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService"), ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService"),
ServiceContainer.Resolve<IAuthService>("authService"), ServiceContainer.Resolve<IAuthService>("authService"),
ServiceContainer.Resolve<ILogger>("logger")); ServiceContainer.Resolve<ILogger>("logger"),
ServiceContainer.Resolve<IMessagingService>("messagingService"));
ServiceContainer.Register<IAccountsManager>("accountsManager", accountsManager); ServiceContainer.Register<IAccountsManager>("accountsManager", accountsManager);
if (postBootstrapFunc != null) if (postBootstrapFunc != null)

View File

@@ -11,7 +11,7 @@
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
<string>com.8bit.bitwarden.find-login-action-extension</string> <string>com.8bit.bitwarden.find-login-action-extension</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>2022.9.2</string> <string>2022.10.0</string>
<key>CFBundleLocalizations</key> <key>CFBundleLocalizations</key>
<array> <array>
<string>en</string> <string>en</string>

View File

@@ -15,7 +15,7 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>XPC!</string> <string>XPC!</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>2022.9.2</string> <string>2022.10.0</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1</string> <string>1</string>
<key>MinimumOSVersion</key> <key>MinimumOSVersion</key>

View File

@@ -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

View File

@@ -11,7 +11,7 @@
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
<string>com.8bit.bitwarden</string> <string>com.8bit.bitwarden</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>2022.9.2</string> <string>2022.10.0</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1</string> <string>1</string>
<key>CFBundleIconName</key> <key>CFBundleIconName</key>

View File

@@ -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);
} }

View File

@@ -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 });
}
} }
} }