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\logo_rounded.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>
<AndroidResource Include="Resources\drawable\splash_screen.xml" />

View File

@@ -12,6 +12,7 @@ using Android.Runtime;
using Android.Views;
using Bit.App.Abstractions;
using Bit.App.Models;
using Bit.App.Resources;
using Bit.App.Utilities;
using Bit.Core;
using Bit.Core.Abstractions;
@@ -86,6 +87,7 @@ namespace Bit.Droid
Xamarin.Essentials.Platform.Init(this, savedInstanceState);
Xamarin.Forms.Forms.Init(this, savedInstanceState);
_appOptions = GetOptions();
CreateNotificationChannel();
LoadApplication(new App.App(_appOptions));
DisableAndroidFontScale();
@@ -407,6 +409,25 @@ namespace Bit.Droid
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()
{
try

View File

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

View File

@@ -1,5 +1,5 @@
<?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-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.NFC" />

View File

@@ -1,7 +1,9 @@
#if !FDROID
using System;
using Android.App;
using Bit.App.Abstractions;
using Bit.Core.Abstractions;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Firebase.Messaging;
using Newtonsoft.Json;
@@ -16,34 +18,41 @@ namespace Bit.Droid.Push
{
public async override void OnNewToken(string token)
{
var stateService = ServiceContainer.Resolve<IStateService>("stateService");
var pushNotificationService = ServiceContainer.Resolve<IPushNotificationService>("pushNotificationService");
try {
var stateService = ServiceContainer.Resolve<IStateService>("stateService");
var pushNotificationService = ServiceContainer.Resolve<IPushNotificationService>("pushNotificationService");
await stateService.SetPushRegisteredTokenAsync(token);
await pushNotificationService.RegisterAsync();
await stateService.SetPushRegisteredTokenAsync(token);
await pushNotificationService.RegisterAsync();
}
catch (Exception ex)
{
Logger.Instance.Exception(ex);
}
}
public async override void OnMessageReceived(RemoteMessage message)
{
if (message?.Data == null)
{
return;
}
var data = message.Data.ContainsKey("data") ? message.Data["data"] : null;
if (data == null)
{
return;
}
try
{
if (message?.Data == null)
{
return;
}
var data = message.Data.ContainsKey("data") ? message.Data["data"] : null;
if (data == null)
{
return;
}
var obj = JObject.Parse(data);
var listener = ServiceContainer.Resolve<IPushNotificationListenerService>(
"pushNotificationListenerService");
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:accessibilityFlags="flagReportViewIds|flagRetrieveInteractiveWindows"
android:notificationTimeout="100"
android:canRetrieveWindowContent="true"/>
android:canRetrieveWindowContent="true"
android:isAccessibilityTool="false"/>

View File

@@ -1,9 +1,14 @@
#if !FDROID
using System;
using System.Threading.Tasks;
using Android.App;
using Android.Content;
using Android.OS;
using AndroidX.Core.App;
using Bit.App.Abstractions;
using Bit.Core;
using Bit.Core.Abstractions;
using Bit.Droid.Utilities;
using Xamarin.Forms;
namespace Bit.Droid.Services
@@ -23,6 +28,11 @@ namespace Bit.Droid.Services
public bool IsRegisteredForPush => NotificationManagerCompat.From(Android.App.Application.Context)?.AreNotificationsEnabled() ?? false;
public Task<bool> AreNotificationsSettingsEnabledAsync()
{
return Task.FromResult(IsRegisteredForPush);
}
public async Task<string> GetTokenAsync()
{
return await _stateService.GetPushCurrentTokenAsync();
@@ -47,6 +57,39 @@ namespace Bit.Droid.Services
// Do we ever need to unregister?
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

View File

@@ -964,5 +964,14 @@ namespace Bit.Droid.Services
}
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();
Task OnAccountSwitchCompleteAsync();
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
{
public interface IPushNotificationService
{
bool IsRegisteredForPush { get; }
Task<bool> AreNotificationsSettingsEnabledAsync();
Task<string> GetTokenAsync();
Task RegisterAsync();
Task UnregisterAsync();
void SendLocalNotification(string title, string message, string notificationId);
void DismissLocalNotification(string notificationId);
}
}

View File

@@ -123,6 +123,9 @@
<SubType>Code</SubType>
</Compile>
<Compile Remove="Pages\Accounts\AccountsPopupPage.xaml.cs" />
<Compile Update="Pages\Accounts\LoginPasswordlessPage.xaml.cs">
<DependentUpon>LoginPasswordlessPage.xaml</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>

View File

@@ -7,6 +7,7 @@ using Bit.App.Resources;
using Bit.App.Services;
using Bit.App.Utilities;
using Bit.App.Utilities.AccountManagement;
using Bit.Core;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
@@ -25,13 +26,13 @@ namespace Bit.App
private readonly IStateService _stateService;
private readonly IVaultTimeoutService _vaultTimeoutService;
private readonly ISyncService _syncService;
private readonly IPlatformUtilsService _platformUtilsService;
private readonly IAuthService _authService;
private readonly IStorageService _secureStorageService;
private readonly IDeviceActionService _deviceActionService;
private readonly IAccountsManager _accountsManager;
private readonly IPushNotificationService _pushNotificationService;
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)
{
@@ -47,10 +48,9 @@ namespace Bit.App
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
_syncService = ServiceContainer.Resolve<ISyncService>("syncService");
_authService = ServiceContainer.Resolve<IAuthService>("authService");
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
_secureStorageService = ServiceContainer.Resolve<IStorageService>("secureStorageService");
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
_accountsManager = ServiceContainer.Resolve<IAccountsManager>("accountsManager");
_pushNotificationService = ServiceContainer.Resolve<IPushNotificationService>();
_accountsManager.Init(() => Options, this);
@@ -140,6 +140,10 @@ namespace Bit.App
new NavigationPage(new RemoveMasterPasswordPage()));
});
}
else if (message.Command == "passwordlessLoginRequest" || message.Command == "unlocked" || message.Command == AccountsManagerMessageCommands.ACCOUNT_SWITCH_COMPLETED)
{
CheckPasswordlessLoginRequestsAsync().FireAndForget();
}
}
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; }
protected async override void OnStart()
{
System.Diagnostics.Debug.WriteLine("XF App: OnStart");
_isResumed = true;
await ClearCacheIfNeededAsync();
Prime();
if (string.IsNullOrWhiteSpace(Options.Uri))
@@ -164,6 +212,10 @@ namespace Bit.App
SyncIfNeeded();
}
}
if (_pendingCheckPasswordlessLoginRequests)
{
CheckPasswordlessLoginRequestsAsync().FireAndForget();
}
if (Device.RuntimePlatform == Device.Android)
{
await _vaultTimeoutService.CheckVaultTimeoutAsync();
@@ -196,6 +248,10 @@ namespace Bit.App
{
System.Diagnostics.Debug.WriteLine("XF App: OnResume");
_isResumed = true;
if (_pendingCheckPasswordlessLoginRequests)
{
CheckPasswordlessLoginRequestsAsync().FireAndForget();
}
if (Device.RuntimePlatform == Device.Android)
{
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 IClipboardService _clipboardService;
private readonly IUsernameGenerationService _usernameGenerationService;
private readonly ITokenService _tokenService;
readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
private PasswordGenerationOptions _options;
@@ -49,8 +50,6 @@ namespace Bit.App.Pages
private bool _showFirefoxRelayApiAccessToken;
private bool _showAnonAddyApiAccessToken;
private bool _showSimpleLoginApiKey;
private UsernameEmailType _catchAllEmailTypeSelected;
private UsernameEmailType _plusAddressedEmailTypeSelected;
private bool _editMode;
public GeneratorPageViewModel()
@@ -59,6 +58,7 @@ namespace Bit.App.Pages
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>();
_clipboardService = ServiceContainer.Resolve<IClipboardService>();
_usernameGenerationService = ServiceContainer.Resolve<IUsernameGenerationService>();
_tokenService = ServiceContainer.Resolve<ITokenService>();
PageTitle = AppResources.Generator;
GeneratorTypeOptions = new List<GeneratorType> {
@@ -157,7 +157,7 @@ namespace Bit.App.Pages
public bool ShowUsernameEmailType
{
get => !string.IsNullOrWhiteSpace(EmailWebsite) || EditMode;
get => !string.IsNullOrWhiteSpace(EmailWebsite);
}
public int Length
@@ -367,7 +367,7 @@ namespace Bit.App.Pages
IsUsername = value == GeneratorType.Username;
TriggerPropertyChanged(nameof(GeneratorTypeSelected));
SaveOptionsAsync().FireAndForget();
SaveUsernameOptionsAsync(false).FireAndForget();
SaveUsernameOptionsAsync().FireAndForget();
}
}
}
@@ -382,6 +382,7 @@ namespace Bit.App.Pages
IsPassword = value == 0;
TriggerPropertyChanged(nameof(PasswordTypeSelectedIndex));
SaveOptionsAsync().FireAndForget();
SaveUsernameOptionsAsync().FireAndForget();
}
}
}
@@ -396,7 +397,7 @@ namespace Bit.App.Pages
_usernameOptions.Type = value;
Username = Constants.DefaultUsernameGenerated;
TriggerPropertyChanged(nameof(UsernameTypeSelected), new string[] { nameof(UsernameTypeDescriptionLabel) });
SaveUsernameOptionsAsync(false).FireAndForget();
SaveUsernameOptionsAsync().FireAndForget();
}
}
}
@@ -564,26 +565,28 @@ namespace Bit.App.Pages
public UsernameEmailType PlusAddressedEmailTypeSelected
{
get => _plusAddressedEmailTypeSelected;
get => _usernameOptions.PlusAddressedEmailType;
set
{
if (SetProperty(ref _plusAddressedEmailTypeSelected, value))
if (_usernameOptions.PlusAddressedEmailType != value)
{
_usernameOptions.PlusAddressedEmailType = value;
SaveUsernameOptionsAsync(false).FireAndForget();
TriggerPropertyChanged(nameof(PlusAddressedEmailTypeSelected));
SaveUsernameOptionsAsync().FireAndForget();
}
}
}
public UsernameEmailType CatchAllEmailTypeSelected
{
get => _catchAllEmailTypeSelected;
get => _usernameOptions.CatchAllEmailType;
set
{
if (SetProperty(ref _catchAllEmailTypeSelected, value))
if (_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();
LoadFromOptions();
await RegenerateAsync();
_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();
Username = Constants.DefaultUsernameGenerated;
_doneIniting = true;
}
@@ -668,10 +683,14 @@ namespace Bit.App.Pages
await _usernameGenerationService.SaveOptionsAsync(_usernameOptions);
if (regenerate)
if (regenerate && UsernameTypeSelected != UsernameType.ForwardedEmailAlias)
{
await RegenerateUsernameAsync();
}
else
{
Username = Constants.DefaultUsernameGenerated;
}
}
public async Task SliderChangedAsync()

View File

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

View File

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

View File

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

View File

@@ -30,7 +30,7 @@ namespace Bit.App.Pages
private readonly IKeyConnectorService _keyConnectorService;
private readonly IClipboardService _clipboardService;
private readonly ILogger _loggerService;
private readonly IPushNotificationService _pushNotificationService;
private const int CustomVaultTimeoutValue = -100;
private bool _supportsBiometric;
@@ -42,6 +42,7 @@ namespace Bit.App.Pages
private string _vaultTimeoutActionDisplayValue;
private bool _showChangeMasterPassword;
private bool _reportLoggingEnabled;
private bool _approvePasswordlessLoginRequests;
private List<KeyValuePair<string, int?>> _vaultTimeouts =
new List<KeyValuePair<string, int?>>
@@ -83,6 +84,7 @@ namespace Bit.App.Pages
_keyConnectorService = ServiceContainer.Resolve<IKeyConnectorService>("keyConnectorService");
_clipboardService = ServiceContainer.Resolve<IClipboardService>("clipboardService");
_loggerService = ServiceContainer.Resolve<ILogger>("logger");
_pushNotificationService = ServiceContainer.Resolve<IPushNotificationService>();
GroupedItems = new ObservableRangeCollection<ISettingsPageListItem>();
PageTitle = AppResources.Settings;
@@ -133,6 +135,7 @@ namespace Bit.App.Pages
_showChangeMasterPassword = IncludeLinksWithSubscriptionInfo() &&
!await _keyConnectorService.GetUsesKeyConnector();
_reportLoggingEnabled = await _loggerService.IsEnabled();
_approvePasswordlessLoginRequests = await _stateService.GetApprovePasswordlessLoginsAsync();
BuildList();
}
@@ -326,6 +329,38 @@ namespace Bit.App.Pages
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()
{
var options = _vaultTimeoutActions.Select(o =>
@@ -504,6 +539,12 @@ namespace Bit.App.Pages
ExecuteAsync = () => UpdatePinAsync()
},
new SettingsPageListItem
{
Name = AppResources.ApproveLoginRequests,
SubLabel = _approvePasswordlessLoginRequests ? AppResources.On : AppResources.Off,
ExecuteAsync = () => ApproveLoginRequestsAsync()
},
new SettingsPageListItem
{
Name = AppResources.LockNow,
ExecuteAsync = () => LockAsync()

View File

@@ -605,6 +605,8 @@ namespace Bit.App.Pages
return;
}
var website = Cipher?.Login?.Uris?.FirstOrDefault()?.Host;
var page = new GeneratorPage(false, async (username) =>
{
try
@@ -617,7 +619,7 @@ namespace Bit.App.Pages
{
OnGenerateUsernameException(ex);
}
}, isUsernameGenerator: true, emailWebsite: Cipher?.Name, editMode: true);
}, isUsernameGenerator: true, emailWebsite: website, editMode: true);
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">
<value>Are you sure you want to enable Screen Capture?</value>
</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">
<value>Password Type</value>
</data>
@@ -2389,4 +2449,19 @@ select Add TOTP to store the key safely</value>
<data name="Random" xml:space="preserve">
<value>Random</value>
</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>

View File

@@ -1,4 +1,5 @@
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Threading.Tasks;
using Bit.App.Abstractions;
namespace Bit.App.Services
@@ -7,6 +8,11 @@ namespace Bit.App.Services
{
public bool IsRegisteredForPush => false;
public Task<bool> AreNotificationsSettingsEnabledAsync()
{
return Task.FromResult(false);
}
public Task<string> GetTokenAsync()
{
return Task.FromResult(null as string);
@@ -21,5 +27,9 @@ namespace Bit.App.Services
{
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
using System;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Bit.App.Abstractions;
using Bit.App.Pages;
using Bit.App.Resources;
using Bit.Core;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
@@ -26,6 +29,7 @@ namespace Bit.App.Services
private IAppIdService _appIdService;
private IApiService _apiService;
private IMessagingService _messagingService;
private IPushNotificationService _pushNotificationService;
public async Task OnMessageAsync(JObject value, string deviceType)
{
@@ -125,6 +129,27 @@ namespace Bit.App.Services
_messagingService.Send("logout");
}
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:
break;
}
@@ -204,6 +229,7 @@ namespace Bit.App.Services
_appIdService = ServiceContainer.Resolve<IAppIdService>("appIdService");
_apiService = ServiceContainer.Resolve<IApiService>("apiService");
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
_pushNotificationService = ServiceContainer.Resolve<IPushNotificationService>();
_resolved = true;
}
}

View File

@@ -71,6 +71,6 @@
<Color x:Key="NavigationBarTextColor">#ffffff</Color>
<Color x:Key="HyperlinkColor">#52bdfb</Color>
<Color x:Key="FingerprintPhrase">#F08DC7</Color>
<Color x:Key="ScanningToggleModeTextColor">#80BDFF</Color>
</ResourceDictionary>

View File

@@ -71,6 +71,6 @@
<Color x:Key="NavigationBarTextColor">#ffffff</Color>
<Color x:Key="HyperlinkColor">#52bdfb</Color>
<Color x:Key="FingerprintPhrase">#F08DC7</Color>
<Color x:Key="ScanningToggleModeTextColor">#80BDFF</Color>
</ResourceDictionary>

View File

@@ -71,6 +71,6 @@
<Color x:Key="NavigationBarTextColor">#ffffff</Color>
<Color x:Key="HyperlinkColor">#175DDC</Color>
<Color x:Key="FingerprintPhrase">#C01176</Color>
<Color x:Key="ScanningToggleModeTextColor">#80BDFF</Color>
</ResourceDictionary>

View File

@@ -71,6 +71,6 @@
<Color x:Key="NavigationBarTextColor">#e5e9f0</Color>
<Color x:Key="HyperlinkColor">#81a1c1</Color>
<Color x:Key="FingerprintPhrase">#F08DC7</Color>
<Color x:Key="ScanningToggleModeTextColor">#80BDFF</Color>
</ResourceDictionary>

View File

@@ -20,7 +20,7 @@ namespace Bit.App.Utilities.AccountManagement
private readonly IPlatformUtilsService _platformUtilsService;
private readonly IAuthService _authService;
private readonly ILogger _logger;
private readonly IMessagingService _messagingService;
Func<AppOptions> _getOptionsFunc;
private IAccountsManagerHost _accountsManagerHost;
@@ -30,7 +30,8 @@ namespace Bit.App.Utilities.AccountManagement
IStateService stateService,
IPlatformUtilsService platformUtilsService,
IAuthService authService,
ILogger logger)
ILogger logger,
IMessagingService messagingService)
{
_broadcasterService = broadcasterService;
_vaultTimeoutService = vaultTimeoutService;
@@ -39,6 +40,7 @@ namespace Bit.App.Utilities.AccountManagement
_platformUtilsService = platformUtilsService;
_authService = authService;
_logger = logger;
_messagingService = messagingService;
}
private AppOptions Options => _getOptionsFunc?.Invoke() ?? new AppOptions { IosExtension = true };
@@ -213,6 +215,7 @@ namespace Bit.App.Utilities.AccountManagement
}
await Task.Delay(50);
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 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;
_action = action ?? throw new ArgumentNullException();
_cancellationToken = cancellationToken;
_action = action ?? throw new ArgumentNullException(nameof(action));
_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)
@@ -26,23 +34,31 @@ namespace Bit.App.Utilities
{
try
{
while (!_cancellationToken.IsCancellationRequested)
while (!_cancellationTokenSource.IsCancellationRequested)
{
await Device.InvokeOnMainThreadAsync(() =>
await Device.InvokeOnMainThreadAsync(async () =>
{
if (!_cancellationToken.IsCancellationRequested)
if (!_cancellationTokenSource.IsCancellationRequested)
{
try
{
_action();
if (_action != null)
{
_action();
}
else if (_actionTask != null)
{
await _actionTask();
}
}
catch (Exception ex)
{
_cancellationTokenSource?.Cancel();
_logger?.Exception(ex);
}
}
});
await Task.Delay(interval.Value, _cancellationToken.Token);
await Task.Delay(interval.Value, _cancellationTokenSource.Token);
}
}
catch (TaskCanceledException) { }
@@ -50,7 +66,7 @@ namespace Bit.App.Utilities
{
_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> PutSendRemovePasswordAsync(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);
}
}

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Threading.Tasks;
using Bit.Core.Enums;
using Bit.Core.Models.Domain;
using Bit.Core.Models.Response;
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> 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<PasswordlessLoginResponse> GetPasswordlessLoginRequestByIdAsync(string id);
Task<PasswordlessLoginResponse> PasswordlessLoginAsync(string id, string pubKey, bool requestApproved);
void LogOut(Action callback);
void Init();
}

View File

@@ -4,6 +4,7 @@ using System.Threading.Tasks;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
using Bit.Core.Models.Domain;
using Bit.Core.Models.Response;
using Bit.Core.Models.View;
namespace Bit.Core.Abstractions
@@ -151,6 +152,10 @@ namespace Bit.Core.Abstractions
Task<bool> GetScreenCaptureAllowedAsync(string userId = null);
Task SetScreenCaptureAllowedAsync(bool value, string userId = null);
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 SetUsernameGenerationOptionsAsync(UsernameGenerationOptions value, string userId = null);
}

View File

@@ -30,9 +30,13 @@
public static string EventCollectionKey = "eventCollection";
public static string RememberedEmailKey = "rememberedEmail";
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 SelectFilePermissionRequestCode = 43;
public const int SaveFileRequestCode = 44;
public const int TotpDefaultTimer = 30;
public const int PasswordlessNotificationTimeoutInMinutes = 15;
public static readonly string[] AndroidAllClearCipherCacheKeys =
{
@@ -84,6 +88,8 @@
public static string ProtectedPinKey(string userId) => $"protectedPin_{userId}";
public static string LastSyncKey(string userId) => $"lastSync_{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}";
}
}

View File

@@ -16,5 +16,12 @@
SyncSettings = 10,
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 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
#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
public async Task<string> GetActiveBearerTokenAsync()

View File

@@ -1,11 +1,13 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Domain;
using Bit.Core.Models.Request;
using Bit.Core.Models.Response;
using Bit.Core.Utilities;
namespace Bit.Core.Services
@@ -468,5 +470,20 @@ namespace Bit.Core.Services
TwoFactorProvidersData = 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.Models.Data;
using Bit.Core.Models.Domain;
using Bit.Core.Models.Response;
using Bit.Core.Models.View;
using Bit.Core.Utilities;
@@ -1260,6 +1261,37 @@ namespace Bit.Core.Services
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
private async Task<T> GetValueAsync<T>(string key, StorageOptions options)
@@ -1455,6 +1487,7 @@ namespace Bit.Core.Services
await SetEncryptedPasswordGenerationHistoryAsync(null, userId);
await SetEncryptedSendsAsync(null, userId);
await SetSettingsAsync(null, userId);
await SetApprovePasswordlessLoginsAsync(null, userId);
if (userInitiated)
{
@@ -1474,6 +1507,7 @@ namespace Bit.Core.Services
await SetAutoDarkThemeAsync(null, userId);
await SetAddSitePromptShownAsync(null, userId);
await SetPasswordGenerationOptionsAsync(null, userId);
await SetApprovePasswordlessLoginsAsync(null, userId);
await SetUsernameGenerationOptionsAsync(null, userId);
}
}

View File

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

View File

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

View File

@@ -643,5 +643,11 @@ namespace Bit.iOS.Core.Services
_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<IPlatformUtilsService>("platformUtilsService"),
ServiceContainer.Resolve<IAuthService>("authService"),
ServiceContainer.Resolve<ILogger>("logger"));
ServiceContainer.Resolve<ILogger>("logger"),
ServiceContainer.Resolve<IMessagingService>("messagingService"));
ServiceContainer.Register<IAccountsManager>("accountsManager", accountsManager);
if (postBootstrapFunc != null)

View File

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

View File

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

View File

@@ -16,6 +16,7 @@ using Bit.iOS.Services;
using CoreNFC;
using Foundation;
using UIKit;
using UserNotifications;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;
@@ -56,7 +57,6 @@ namespace Bit.iOS
LoadApplication(new App.App(null));
iOSCoreHelpers.AppearanceAdjustments();
ZXing.Net.Mobile.Forms.iOS.Platform.Init();
_broadcasterService.Subscribe(nameof(AppDelegate), async (message) =>
{
try

View File

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

View File

@@ -83,7 +83,6 @@ namespace Bit.iOS.Services
public void WillPresentNotification(UNUserNotificationCenter center, UNNotification notification, Action<UNNotificationPresentationOptions> completionHandler)
{
Debug.WriteLine($"{TAG} WillPresentNotification {notification?.Request?.Content?.UserInfo}");
OnMessageReceived(notification?.Request?.Content?.UserInfo);
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 Bit.App.Abstractions;
using Bit.Core.Services;
using Foundation;
using UIKit;
using UserNotifications;
@@ -19,6 +23,12 @@ namespace Bit.iOS.Services
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()
{
Debug.WriteLine($"{TAG} RegisterAsync");
@@ -58,5 +68,39 @@ namespace Bit.iOS.Services
NSUserDefaults.StandardUserDefaults.Synchronize();
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 });
}
}
}