mirror of
https://github.com/bitwarden/mobile
synced 2026-01-02 08:33:17 +00:00
PM-3349 PM-3350 MAUI Migration Initial
This commit is contained in:
@@ -16,6 +16,7 @@ using Bit.Core.Utilities;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
using DeviceType = Bit.Core.Enums.DeviceType;
|
||||
|
||||
namespace Bit.Core.Services
|
||||
{
|
||||
|
||||
42
src/Core/Services/BaseBiometricService.cs
Normal file
42
src/Core/Services/BaseBiometricService.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Abstractions;
|
||||
|
||||
namespace Bit.App.Services
|
||||
{
|
||||
public abstract class BaseBiometricService : IBiometricService
|
||||
{
|
||||
protected readonly IStateService _stateService;
|
||||
protected readonly ICryptoService _cryptoService;
|
||||
|
||||
protected BaseBiometricService(IStateService stateService, ICryptoService cryptoService)
|
||||
{
|
||||
_stateService = stateService;
|
||||
_cryptoService = cryptoService;
|
||||
}
|
||||
|
||||
public async Task<bool> CanUseBiometricsUnlockAsync()
|
||||
{
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
return await _cryptoService.GetBiometricUnlockKeyAsync() != null || await _stateService.GetKeyEncryptedAsync() != null;
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
}
|
||||
|
||||
public async Task SetCanUnlockWithBiometricsAsync(bool canUnlockWithBiometrics)
|
||||
{
|
||||
if (canUnlockWithBiometrics)
|
||||
{
|
||||
await SetupBiometricAsync();
|
||||
await _stateService.SetBiometricUnlockAsync(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _stateService.SetBiometricUnlockAsync(null);
|
||||
}
|
||||
await _stateService.SetBiometricLockedAsync(false);
|
||||
await _cryptoService.RefreshKeysAsync();
|
||||
}
|
||||
|
||||
public abstract Task<bool> IsSystemBiometricIntegrityValidAsync(string bioIntegritySrcKey = null);
|
||||
public abstract Task<bool> SetupBiometricAsync(string bioIntegritySrcKey = null);
|
||||
}
|
||||
}
|
||||
142
src/Core/Services/BaseWatchDeviceService.cs
Normal file
142
src/Core/Services/BaseWatchDeviceService.cs
Normal file
@@ -0,0 +1,142 @@
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models;
|
||||
using Bit.Core.Models.View;
|
||||
using MessagePack;
|
||||
using MessagePack.Resolvers;
|
||||
|
||||
namespace Bit.App.Services
|
||||
{
|
||||
public abstract class BaseWatchDeviceService : IWatchDeviceService
|
||||
{
|
||||
private readonly ICipherService _cipherService;
|
||||
private readonly IEnvironmentService _environmentService;
|
||||
private readonly IStateService _stateService;
|
||||
private readonly IVaultTimeoutService _vaultTimeoutService;
|
||||
|
||||
protected BaseWatchDeviceService(ICipherService cipherService,
|
||||
IEnvironmentService environmentService,
|
||||
IStateService stateService,
|
||||
IVaultTimeoutService vaultTimeoutService)
|
||||
{
|
||||
_cipherService = cipherService;
|
||||
_environmentService = environmentService;
|
||||
_stateService = stateService;
|
||||
_vaultTimeoutService = vaultTimeoutService;
|
||||
}
|
||||
|
||||
public abstract bool IsConnected { get; }
|
||||
|
||||
protected abstract bool CanSendData { get; }
|
||||
protected abstract bool IsSupported { get; }
|
||||
|
||||
public async Task SyncDataToWatchAsync()
|
||||
{
|
||||
if (!IsSupported)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var shouldConnect = await _stateService.GetShouldConnectToWatchAsync();
|
||||
if (shouldConnect && !IsConnected)
|
||||
{
|
||||
ConnectToWatch();
|
||||
}
|
||||
|
||||
if (!CanSendData)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var userData = await _stateService.GetActiveUserCustomDataAsync(a => a?.Profile is null ? null : new WatchDTO.UserDataDto
|
||||
{
|
||||
Id = a.Profile.UserId,
|
||||
Name = a.Profile.Name,
|
||||
Email = a.Profile.Email
|
||||
});
|
||||
var state = await GetStateAsync(userData?.Id, shouldConnect);
|
||||
if (state != WatchState.Valid)
|
||||
{
|
||||
await SendDataToWatchAsync(new WatchDTO(state));
|
||||
return;
|
||||
}
|
||||
|
||||
var ciphersWithTotp = await _cipherService.GetAllDecryptedAsync(c => c.DeletedDate == null && c.Login?.Totp != null);
|
||||
|
||||
if (!ciphersWithTotp.Any())
|
||||
{
|
||||
await SendDataToWatchAsync(new WatchDTO(WatchState.Need2FAItem));
|
||||
return;
|
||||
}
|
||||
|
||||
var watchDto = new WatchDTO(state)
|
||||
{
|
||||
Ciphers = ciphersWithTotp.Select(c => new SimpleCipherView(c)).ToList(),
|
||||
UserData = userData,
|
||||
EnvironmentData = new WatchDTO.EnvironmentUrlDataDto
|
||||
{
|
||||
Base = _environmentService.BaseUrl,
|
||||
Icons = _environmentService.IconsUrl
|
||||
}
|
||||
//SettingsData = new WatchDTO.SettingsDataDto
|
||||
//{
|
||||
// VaultTimeoutInMinutes = await _vaultTimeoutService.GetVaultTimeout(userData?.Id),
|
||||
// VaultTimeoutAction = await _stateService.GetVaultTimeoutActionAsync(userData?.Id) ?? VaultTimeoutAction.Lock
|
||||
//}
|
||||
};
|
||||
await SendDataToWatchAsync(watchDto);
|
||||
}
|
||||
|
||||
private async Task<WatchState> GetStateAsync(string userId, bool shouldConnectToWatch)
|
||||
{
|
||||
if (await _stateService.GetLastUserShouldConnectToWatchAsync()
|
||||
&&
|
||||
(userId is null || !await _stateService.IsAuthenticatedAsync()))
|
||||
{
|
||||
// if the last user had "Connect to Watch" enabled and there's no user authenticated
|
||||
return WatchState.NeedLogin;
|
||||
}
|
||||
|
||||
if (!shouldConnectToWatch)
|
||||
{
|
||||
return WatchState.NeedSetup;
|
||||
}
|
||||
|
||||
//if (await _vaultTimeoutService.IsLockedAsync() ||
|
||||
// await _vaultTimeoutService.ShouldLockAsync())
|
||||
//{
|
||||
// return WatchState.NeedUnlock;
|
||||
//}
|
||||
|
||||
if (!await _stateService.CanAccessPremiumAsync(userId))
|
||||
{
|
||||
return WatchState.NeedPremium;
|
||||
}
|
||||
|
||||
return WatchState.Valid;
|
||||
}
|
||||
|
||||
public async Task SetShouldConnectToWatchAsync(bool shouldConnectToWatch)
|
||||
{
|
||||
await _stateService.SetShouldConnectToWatchAsync(shouldConnectToWatch);
|
||||
await SyncDataToWatchAsync();
|
||||
}
|
||||
|
||||
protected async Task SendDataToWatchAsync(WatchDTO watchDto)
|
||||
{
|
||||
var options = MessagePackSerializerOptions.Standard
|
||||
.WithResolver(CompositeResolver.Create(
|
||||
GeneratedResolver.Instance,
|
||||
StandardResolver.Instance
|
||||
));
|
||||
|
||||
await SendDataToWatchAsync(MessagePackSerializer.Serialize(watchDto, options));
|
||||
}
|
||||
|
||||
protected abstract Task SendDataToWatchAsync(byte[] rawData);
|
||||
|
||||
protected abstract void ConnectToWatch();
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ using Bit.Core.Models.Request;
|
||||
using Bit.Core.Models.Response;
|
||||
using Bit.Core.Models.View;
|
||||
using Bit.Core.Utilities;
|
||||
using View = Bit.Core.Models.View.View;
|
||||
|
||||
namespace Bit.Core.Services
|
||||
{
|
||||
|
||||
@@ -8,6 +8,7 @@ using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Bit.Core.Models.View;
|
||||
using Bit.Core.Utilities;
|
||||
using CollectionView = Bit.Core.Models.View.CollectionView;
|
||||
|
||||
namespace Bit.Core.Services
|
||||
{
|
||||
|
||||
30
src/Core/Services/DeepLinkContext.cs
Normal file
30
src/Core/Services/DeepLinkContext.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
|
||||
namespace Bit.App.Services
|
||||
{
|
||||
public class DeepLinkContext : IDeepLinkContext
|
||||
{
|
||||
public const string NEW_OTP_MESSAGE = "handleOTPUriMessage";
|
||||
|
||||
private readonly IMessagingService _messagingService;
|
||||
|
||||
public DeepLinkContext(IMessagingService messagingService)
|
||||
{
|
||||
_messagingService = messagingService;
|
||||
}
|
||||
|
||||
public bool OnNewUri(Uri uri)
|
||||
{
|
||||
if (uri.Scheme == Constants.OtpAuthScheme)
|
||||
{
|
||||
_messagingService.Send(NEW_OTP_MESSAGE, uri.AbsoluteUri);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,8 @@ using System.Runtime.CompilerServices;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AppCenter;
|
||||
using Microsoft.AppCenter.Crashes;
|
||||
//using Microsoft.AppCenter;
|
||||
//using Microsoft.AppCenter.Crashes;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Bit.Core.Services
|
||||
@@ -64,35 +64,36 @@ namespace Bit.Core.Services
|
||||
_userId = await ServiceContainer.Resolve<IStateService>("stateService").GetActiveUserIdAsync();
|
||||
_appId = await ServiceContainer.Resolve<IAppIdService>("appIdService").GetAppIdAsync();
|
||||
|
||||
switch (device)
|
||||
{
|
||||
case Enums.DeviceType.Android:
|
||||
AppCenter.Start(DroidAppSecret, typeof(Crashes));
|
||||
break;
|
||||
case Enums.DeviceType.iOS:
|
||||
AppCenter.Start(iOSAppSecret, typeof(Crashes));
|
||||
break;
|
||||
default:
|
||||
throw new AppCenterException("Cannot start AppCenter. Device type is not configured.");
|
||||
// TODO: [MAUI-Migration] [Critical]
|
||||
//switch (device)
|
||||
//{
|
||||
// case Enums.DeviceType.Android:
|
||||
// AppCenter.Start(DroidAppSecret, typeof(Crashes));
|
||||
// break;
|
||||
// case Enums.DeviceType.iOS:
|
||||
// AppCenter.Start(iOSAppSecret, typeof(Crashes));
|
||||
// break;
|
||||
// default:
|
||||
// throw new AppCenterException("Cannot start AppCenter. Device type is not configured.");
|
||||
|
||||
}
|
||||
//}
|
||||
|
||||
AppCenter.SetUserId(_userId);
|
||||
//AppCenter.SetUserId(_userId);
|
||||
|
||||
Crashes.GetErrorAttachments = (ErrorReport report) =>
|
||||
{
|
||||
return new ErrorAttachmentLog[]
|
||||
{
|
||||
ErrorAttachmentLog.AttachmentWithText(Description, "crshdesc.txt"),
|
||||
};
|
||||
};
|
||||
//Crashes.GetErrorAttachments = (ErrorReport report) =>
|
||||
//{
|
||||
// return new ErrorAttachmentLog[]
|
||||
// {
|
||||
// ErrorAttachmentLog.AttachmentWithText(Description, "crshdesc.txt"),
|
||||
// };
|
||||
//};
|
||||
|
||||
_isInitialised = true;
|
||||
}
|
||||
|
||||
public async Task<bool> IsEnabled() => await AppCenter.IsEnabledAsync();
|
||||
public async Task<bool> IsEnabled() => false;// await AppCenter.IsEnabledAsync();
|
||||
|
||||
public async Task SetEnabled(bool value) => await AppCenter.SetEnabledAsync(value);
|
||||
public async Task SetEnabled(bool value) { }// await AppCenter.SetEnabledAsync(value);
|
||||
|
||||
public void Error(string message,
|
||||
IDictionary<string, string> extraData = null,
|
||||
@@ -108,28 +109,28 @@ namespace Bit.Core.Services
|
||||
["Method"] = memberName
|
||||
};
|
||||
|
||||
var exception = new Exception(message ?? $"Error found in: {classAndMethod}");
|
||||
if (extraData == null)
|
||||
{
|
||||
Crashes.TrackError(exception, properties);
|
||||
}
|
||||
else
|
||||
{
|
||||
var data = properties.Concat(extraData).ToDictionary(x => x.Key, x => x.Value);
|
||||
Crashes.TrackError(exception, data);
|
||||
}
|
||||
//var exception = new Exception(message ?? $"Error found in: {classAndMethod}");
|
||||
//if (extraData == null)
|
||||
//{
|
||||
// Crashes.TrackError(exception, properties);
|
||||
//}
|
||||
//else
|
||||
//{
|
||||
// var data = properties.Concat(extraData).ToDictionary(x => x.Key, x => x.Value);
|
||||
// Crashes.TrackError(exception, data);
|
||||
//}
|
||||
}
|
||||
|
||||
public void Exception(Exception exception)
|
||||
{
|
||||
try
|
||||
{
|
||||
Crashes.TrackError(exception);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine(ex.Message);
|
||||
}
|
||||
//try
|
||||
//{
|
||||
// Crashes.TrackError(exception);
|
||||
//}
|
||||
//catch (Exception ex)
|
||||
//{
|
||||
// Debug.WriteLine(ex.Message);
|
||||
//}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,8 @@ namespace Bit.Core.Services
|
||||
#if !FDROID
|
||||
// just in case the caller throws the exception in a moment where the logger can't be resolved
|
||||
// we need to track the error as well
|
||||
Microsoft.AppCenter.Crashes.Crashes.TrackError(ex);
|
||||
// [MAUI-Migration] [Critical]
|
||||
//Microsoft.AppCenter.Crashes.Crashes.TrackError(ex);
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
21
src/Core/Services/MobileBroadcasterMessagingService.cs
Normal file
21
src/Core/Services/MobileBroadcasterMessagingService.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Models.Domain;
|
||||
|
||||
namespace Bit.App.Services
|
||||
{
|
||||
public class MobileBroadcasterMessagingService : IMessagingService
|
||||
{
|
||||
private readonly IBroadcasterService _broadcasterService;
|
||||
|
||||
public MobileBroadcasterMessagingService(IBroadcasterService broadcasterService)
|
||||
{
|
||||
_broadcasterService = broadcasterService;
|
||||
}
|
||||
|
||||
public void Send(string subscriber, object arg = null)
|
||||
{
|
||||
var message = new Message { Command = subscriber, Data = arg };
|
||||
_broadcasterService.Send(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
153
src/Core/Services/MobileI18nService.cs
Normal file
153
src/Core/Services/MobileI18nService.cs
Normal file
@@ -0,0 +1,153 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Reflection;
|
||||
using System.Resources;
|
||||
using System.Threading;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.Core.Abstractions;
|
||||
|
||||
namespace Bit.App.Services
|
||||
{
|
||||
public class MobileI18nService : II18nService
|
||||
{
|
||||
private const string ResourceId = "Bit.Core.Resources.Localization.AppResources";
|
||||
|
||||
private static readonly Lazy<ResourceManager> _resourceManager = new Lazy<ResourceManager>(() =>
|
||||
new ResourceManager(ResourceId, IntrospectionExtensions.GetTypeInfo(typeof(MobileI18nService)).Assembly));
|
||||
|
||||
private readonly CultureInfo _defaultCulture = new CultureInfo("en-US");
|
||||
private bool _inited;
|
||||
private StringComparer _stringComparer;
|
||||
private Dictionary<string, string> _localeNames;
|
||||
|
||||
public MobileI18nService(CultureInfo systemCulture)
|
||||
{
|
||||
Culture = systemCulture;
|
||||
}
|
||||
|
||||
public CultureInfo Culture { get; set; }
|
||||
public StringComparer StringComparer
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_stringComparer == null)
|
||||
{
|
||||
_stringComparer = StringComparer.Create(Culture, false);
|
||||
}
|
||||
return _stringComparer;
|
||||
}
|
||||
}
|
||||
public Dictionary<string, string> LocaleNames
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_localeNames == null)
|
||||
{
|
||||
_localeNames = new Dictionary<string, string>
|
||||
{
|
||||
["af"] = "Afrikaans",
|
||||
["be"] = "Беларуская",
|
||||
["bg"] = "български",
|
||||
["ca"] = "català",
|
||||
["cs"] = "čeština",
|
||||
["da"] = "Dansk",
|
||||
["de"] = "Deutsch",
|
||||
["el"] = "Ελληνικά",
|
||||
["en"] = "English",
|
||||
["en-GB"] = "English (British)",
|
||||
["eo"] = "Esperanto",
|
||||
["es"] = "Español",
|
||||
["et"] = "eesti",
|
||||
["fa"] = "فارسی",
|
||||
["fi"] = "suomi",
|
||||
["fr"] = "Français",
|
||||
["he"] = "עברית",
|
||||
["hi"] = "हिन्दी",
|
||||
["hr"] = "hrvatski",
|
||||
["hu"] = "magyar",
|
||||
["id"] = "Bahasa Indonesia",
|
||||
["it"] = "Italiano",
|
||||
["ja"] = "日本語",
|
||||
["ko"] = "한국어",
|
||||
["lv"] = "Latvietis",
|
||||
["ml"] = "മലയാളം",
|
||||
["nb"] = "norsk (bokmål)",
|
||||
["nl"] = "Nederlands",
|
||||
["pl"] = "Polski",
|
||||
["pt-BR"] = "Português do Brasil",
|
||||
["pt-PT"] = "Português",
|
||||
["ro"] = "română",
|
||||
["ru"] = "русский",
|
||||
["sk"] = "slovenčina",
|
||||
["sv"] = "svenska",
|
||||
["th"] = "ไทย",
|
||||
["tr"] = "Türkçe",
|
||||
["uk"] = "українська",
|
||||
["vi"] = "Tiếng Việt",
|
||||
["zh-CN"] = "中文(中国大陆)",
|
||||
["zh-TW"] = "中文(台灣)"
|
||||
};
|
||||
}
|
||||
return _localeNames;
|
||||
}
|
||||
}
|
||||
|
||||
public void Init(CultureInfo culture = null)
|
||||
{
|
||||
if (_inited)
|
||||
{
|
||||
throw new Exception("I18n already inited.");
|
||||
}
|
||||
_inited = true;
|
||||
SetCurrentCulture(culture);
|
||||
}
|
||||
|
||||
public void SetCurrentCulture(CultureInfo culture)
|
||||
{
|
||||
if (culture != null)
|
||||
{
|
||||
Culture = culture;
|
||||
}
|
||||
|
||||
AppResources.Culture = Culture;
|
||||
Thread.CurrentThread.CurrentCulture = Culture;
|
||||
Thread.CurrentThread.CurrentUICulture = Culture;
|
||||
}
|
||||
|
||||
public string T(string id, string p1 = null, string p2 = null, string p3 = null)
|
||||
{
|
||||
return Translate(id, p1, p2, p3);
|
||||
}
|
||||
|
||||
public string Translate(string id, string p1 = null, string p2 = null, string p3 = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(id))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
var result = _resourceManager.Value.GetString(id, Culture);
|
||||
if (result == null)
|
||||
{
|
||||
result = _resourceManager.Value.GetString(id, _defaultCulture);
|
||||
if (result == null)
|
||||
{
|
||||
result = $"{{{id}}}";
|
||||
}
|
||||
}
|
||||
if (p1 == null && p2 == null && p3 == null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
else if (p2 == null && p3 == null)
|
||||
{
|
||||
return string.Format(result, p1);
|
||||
}
|
||||
else if (p3 == null)
|
||||
{
|
||||
return string.Format(result, p1, p2);
|
||||
}
|
||||
return string.Format(result, p1, p2, p3);
|
||||
}
|
||||
}
|
||||
}
|
||||
71
src/Core/Services/MobilePasswordRepromptService.cs
Normal file
71
src/Core/Services/MobilePasswordRepromptService.cs
Normal file
@@ -0,0 +1,71 @@
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.App.Utilities;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.App.Services
|
||||
{
|
||||
public class MobilePasswordRepromptService : IPasswordRepromptService
|
||||
{
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private readonly ICryptoService _cryptoService;
|
||||
private readonly IStateService _stateService;
|
||||
|
||||
public MobilePasswordRepromptService(IPlatformUtilsService platformUtilsService, ICryptoService cryptoService, IStateService stateService)
|
||||
{
|
||||
_platformUtilsService = platformUtilsService;
|
||||
_cryptoService = cryptoService;
|
||||
_stateService = stateService;
|
||||
}
|
||||
|
||||
public string[] ProtectedFields { get; } = { "LoginTotp", "LoginPassword", "H_FieldValue", "CardNumber", "CardCode" };
|
||||
|
||||
public async Task<bool> PromptAndCheckPasswordIfNeededAsync(CipherRepromptType repromptType = CipherRepromptType.Password)
|
||||
{
|
||||
if (repromptType == CipherRepromptType.None || await ShouldByPassMasterPasswordRepromptAsync())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return await _platformUtilsService.ShowPasswordDialogAsync(AppResources.PasswordConfirmation, AppResources.PasswordConfirmationDesc, ValidatePasswordAsync);
|
||||
}
|
||||
|
||||
public async Task<(string password, bool valid)> ShowPasswordPromptAndGetItAsync()
|
||||
{
|
||||
return await _platformUtilsService.ShowPasswordDialogAndGetItAsync(AppResources.PasswordConfirmation, AppResources.PasswordConfirmationDesc, ValidatePasswordAsync);
|
||||
}
|
||||
|
||||
private async Task<bool> ValidatePasswordAsync(string password)
|
||||
{
|
||||
// Assume user has canceled.
|
||||
if (string.IsNullOrWhiteSpace(password))
|
||||
{
|
||||
return false;
|
||||
};
|
||||
|
||||
var masterKey = await _cryptoService.GetOrDeriveMasterKeyAsync(password);
|
||||
var passwordValid = await _cryptoService.CompareAndUpdateKeyHashAsync(password, masterKey);
|
||||
if (passwordValid)
|
||||
{
|
||||
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
|
||||
|
||||
var userKey = await _cryptoService.DecryptUserKeyWithMasterKeyAsync(masterKey);
|
||||
await _cryptoService.SetMasterKeyAsync(masterKey);
|
||||
var hasKey = await _cryptoService.HasUserKeyAsync();
|
||||
if (!hasKey)
|
||||
{
|
||||
await _cryptoService.SetUserKeyAsync(userKey);
|
||||
}
|
||||
}
|
||||
|
||||
return passwordValid;
|
||||
}
|
||||
|
||||
private async Task<bool> ShouldByPassMasterPasswordRepromptAsync()
|
||||
{
|
||||
return await _cryptoService.GetMasterKeyHashAsync() is null;
|
||||
}
|
||||
}
|
||||
}
|
||||
293
src/Core/Services/MobilePlatformUtilsService.cs
Normal file
293
src/Core/Services/MobilePlatformUtilsService.cs
Normal file
@@ -0,0 +1,293 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Models;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Utilities;
|
||||
using Plugin.Fingerprint;
|
||||
using Plugin.Fingerprint.Abstractions;
|
||||
using Microsoft.Maui.ApplicationModel.DataTransfer;
|
||||
using Microsoft.Maui.ApplicationModel;
|
||||
using Microsoft.Maui.Devices;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui;
|
||||
|
||||
namespace Bit.App.Services
|
||||
{
|
||||
public class MobilePlatformUtilsService : IPlatformUtilsService
|
||||
{
|
||||
private static readonly Random _random = new Random();
|
||||
|
||||
private const int DialogPromiseExpiration = 600000; // 10 minutes
|
||||
|
||||
private readonly IDeviceActionService _deviceActionService;
|
||||
private readonly IClipboardService _clipboardService;
|
||||
private readonly IMessagingService _messagingService;
|
||||
private readonly IBroadcasterService _broadcasterService;
|
||||
|
||||
private readonly Dictionary<int, Tuple<TaskCompletionSource<bool>, DateTime>> _showDialogResolves =
|
||||
new Dictionary<int, Tuple<TaskCompletionSource<bool>, DateTime>>();
|
||||
|
||||
public MobilePlatformUtilsService(
|
||||
IDeviceActionService deviceActionService,
|
||||
IClipboardService clipboardService,
|
||||
IMessagingService messagingService,
|
||||
IBroadcasterService broadcasterService
|
||||
)
|
||||
{
|
||||
_deviceActionService = deviceActionService;
|
||||
_clipboardService = clipboardService;
|
||||
_messagingService = messagingService;
|
||||
_broadcasterService = broadcasterService;
|
||||
}
|
||||
|
||||
public void Init()
|
||||
{
|
||||
_broadcasterService.Subscribe(nameof(MobilePlatformUtilsService), (message) =>
|
||||
{
|
||||
if (message.Command == "showDialogResolve")
|
||||
{
|
||||
var details = message.Data as Tuple<int, bool>;
|
||||
var dialogId = details.Item1;
|
||||
var confirmed = details.Item2;
|
||||
if (_showDialogResolves.ContainsKey(dialogId))
|
||||
{
|
||||
var resolveObj = _showDialogResolves[dialogId].Item1;
|
||||
resolveObj.TrySetResult(confirmed);
|
||||
}
|
||||
|
||||
// Clean up old tasks
|
||||
var deleteIds = new HashSet<int>();
|
||||
foreach (var item in _showDialogResolves)
|
||||
{
|
||||
var age = DateTime.UtcNow - item.Value.Item2;
|
||||
if (age.TotalMilliseconds > DialogPromiseExpiration)
|
||||
{
|
||||
deleteIds.Add(item.Key);
|
||||
}
|
||||
}
|
||||
foreach (var id in deleteIds)
|
||||
{
|
||||
_showDialogResolves.Remove(id);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the device type on the server enum
|
||||
/// </summary>
|
||||
public Core.Enums.DeviceType GetDevice()
|
||||
{
|
||||
// Can't use Device.RuntimePlatform here because it gets called before Forms.Init() and throws.
|
||||
// so we need to get the DeviceType ourselves
|
||||
return _deviceActionService.DeviceType;
|
||||
}
|
||||
|
||||
public string GetDeviceString()
|
||||
{
|
||||
return DeviceInfo.Model;
|
||||
}
|
||||
|
||||
public ClientType GetClientType()
|
||||
{
|
||||
return ClientType.Mobile;
|
||||
}
|
||||
|
||||
public bool IsViewOpen()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public void LaunchUri(string uri, Dictionary<string, object> options = null)
|
||||
{
|
||||
if ((uri.StartsWith("http://") || uri.StartsWith("https://")) &&
|
||||
Uri.TryCreate(uri, UriKind.Absolute, out var parsedUri))
|
||||
{
|
||||
try
|
||||
{
|
||||
Browser.OpenAsync(uri, BrowserLaunchMode.External);
|
||||
}
|
||||
catch (FeatureNotSupportedException) { }
|
||||
}
|
||||
else
|
||||
{
|
||||
var launched = false;
|
||||
if (GetDevice() == Core.Enums.DeviceType.Android && uri.StartsWith("androidapp://"))
|
||||
{
|
||||
launched = _deviceActionService.LaunchApp(uri);
|
||||
}
|
||||
if (!launched && (options?.ContainsKey("page") ?? false))
|
||||
{
|
||||
(options["page"] as Page).DisplayAlert(null, "", ""); // TODO
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string GetApplicationVersion()
|
||||
{
|
||||
return AppInfo.VersionString;
|
||||
}
|
||||
|
||||
public bool SupportsDuo()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public void ShowToastForCopiedValue(string valueNameCopied)
|
||||
{
|
||||
ShowToast("info", null, string.Format(AppResources.ValueHasBeenCopied, valueNameCopied));
|
||||
}
|
||||
|
||||
public bool SupportsFido2()
|
||||
{
|
||||
return _deviceActionService.SupportsFido2();
|
||||
}
|
||||
|
||||
public void ShowToast(string type, string title, string text, Dictionary<string, object> options = null)
|
||||
{
|
||||
ShowToast(type, title, new string[] { text }, options);
|
||||
}
|
||||
|
||||
public void ShowToast(string type, string title, string[] text, Dictionary<string, object> options = null)
|
||||
{
|
||||
if (text.Length > 0)
|
||||
{
|
||||
var longDuration = options != null && options.ContainsKey("longDuration") ?
|
||||
(bool)options["longDuration"] : false;
|
||||
_deviceActionService.Toast(text[0], longDuration);
|
||||
}
|
||||
}
|
||||
|
||||
public Task<bool> ShowDialogAsync(string text, string title = null, string confirmText = null,
|
||||
string cancelText = null, string type = null)
|
||||
{
|
||||
var dialogId = 0;
|
||||
lock (_random)
|
||||
{
|
||||
dialogId = _random.Next(0, int.MaxValue);
|
||||
}
|
||||
_messagingService.Send("showDialog", new DialogDetails
|
||||
{
|
||||
Text = text,
|
||||
Title = title,
|
||||
ConfirmText = confirmText,
|
||||
CancelText = cancelText,
|
||||
Type = type,
|
||||
DialogId = dialogId
|
||||
});
|
||||
var tcs = new TaskCompletionSource<bool>();
|
||||
_showDialogResolves.Add(dialogId, new Tuple<TaskCompletionSource<bool>, DateTime>(tcs, DateTime.UtcNow));
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
public async Task<bool> ShowPasswordDialogAsync(string title, string body, Func<string, Task<bool>> validator)
|
||||
{
|
||||
return (await ShowPasswordDialogAndGetItAsync(title, body, validator)).valid;
|
||||
}
|
||||
|
||||
public async Task<(string password, bool valid)> ShowPasswordDialogAndGetItAsync(string title, string body, Func<string, Task<bool>> validator)
|
||||
{
|
||||
var password = await _deviceActionService.DisplayPromptAync(AppResources.PasswordConfirmation,
|
||||
AppResources.PasswordConfirmationDesc, null, AppResources.Submit, AppResources.Cancel, password: true);
|
||||
|
||||
if (password == null)
|
||||
{
|
||||
return (password, false);
|
||||
}
|
||||
|
||||
var valid = await validator(password);
|
||||
|
||||
if (!valid)
|
||||
{
|
||||
await ShowDialogAsync(AppResources.InvalidMasterPassword, null, AppResources.Ok);
|
||||
}
|
||||
|
||||
return (password, valid);
|
||||
}
|
||||
|
||||
public bool IsSelfHost()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public async Task<string> ReadFromClipboardAsync(Dictionary<string, object> options = null)
|
||||
{
|
||||
return await Clipboard.GetTextAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> SupportsBiometricAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
return await CrossFingerprint.Current.IsAvailableAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> IsBiometricIntegrityValidAsync(string bioIntegritySrcKey = null)
|
||||
{
|
||||
bioIntegritySrcKey ??= Core.Constants.BiometricIntegritySourceKey;
|
||||
|
||||
var biometricService = ServiceContainer.Resolve<IBiometricService>();
|
||||
if (!await biometricService.IsSystemBiometricIntegrityValidAsync(bioIntegritySrcKey))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var stateService = ServiceContainer.Resolve<IStateService>();
|
||||
return await stateService.IsAccountBiometricIntegrityValidAsync(bioIntegritySrcKey);
|
||||
}
|
||||
|
||||
public async Task<bool> AuthenticateBiometricAsync(string text = null, string fallbackText = null,
|
||||
Action fallback = null, bool logOutOnTooManyAttempts = false)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (text == null)
|
||||
{
|
||||
text = AppResources.BiometricsDirection;
|
||||
// TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes
|
||||
if (Device.RuntimePlatform == Device.iOS)
|
||||
{
|
||||
var supportsFace = await _deviceActionService.SupportsFaceBiometricAsync();
|
||||
text = supportsFace ? AppResources.FaceIDDirection : AppResources.FingerprintDirection;
|
||||
}
|
||||
}
|
||||
var biometricRequest = new AuthenticationRequestConfiguration(AppResources.Bitwarden, text)
|
||||
{
|
||||
CancelTitle = AppResources.Cancel,
|
||||
FallbackTitle = fallbackText
|
||||
};
|
||||
var result = await CrossFingerprint.Current.AuthenticateAsync(biometricRequest);
|
||||
if (result.Authenticated)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (result.Status == FingerprintAuthenticationResultStatus.FallbackRequested)
|
||||
{
|
||||
fallback?.Invoke();
|
||||
}
|
||||
if (result.Status == FingerprintAuthenticationResultStatus.TooManyAttempts
|
||||
&& logOutOnTooManyAttempts)
|
||||
{
|
||||
await ShowDialogAsync(AppResources.AccountLoggedOutBiometricExceeded, AppResources.TooManyAttempts, AppResources.Ok);
|
||||
_messagingService.Send(AccountsManagerMessageCommands.LOGOUT);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
return false;
|
||||
}
|
||||
|
||||
public long GetActiveTime()
|
||||
{
|
||||
return _deviceActionService.GetActiveTime();
|
||||
}
|
||||
}
|
||||
}
|
||||
95
src/Core/Services/MobileStorageService.cs
Normal file
95
src/Core/Services/MobileStorageService.cs
Normal file
@@ -0,0 +1,95 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
|
||||
namespace Bit.App.Services
|
||||
{
|
||||
public class MobileStorageService : IStorageService, IDisposable
|
||||
{
|
||||
private readonly IStorageService _preferencesStorageService;
|
||||
private readonly IStorageService _liteDbStorageService;
|
||||
|
||||
private readonly HashSet<string> _liteDbStorageKeys = new HashSet<string>
|
||||
{
|
||||
Constants.EventCollectionKey,
|
||||
Constants.CiphersKey(""),
|
||||
Constants.FoldersKey(""),
|
||||
Constants.CollectionsKey(""),
|
||||
Constants.CiphersLocalDataKey(""),
|
||||
Constants.SendsKey(""),
|
||||
Constants.PassGenHistoryKey(""),
|
||||
Constants.SettingsKey(""),
|
||||
};
|
||||
|
||||
public MobileStorageService(
|
||||
IStorageService preferenceStorageService,
|
||||
IStorageService liteDbStorageService)
|
||||
{
|
||||
_preferencesStorageService = preferenceStorageService;
|
||||
_liteDbStorageService = liteDbStorageService;
|
||||
}
|
||||
|
||||
public async Task<T> GetAsync<T>(string key)
|
||||
{
|
||||
if (IsLiteDbKey(key))
|
||||
{
|
||||
return await _liteDbStorageService.GetAsync<T>(key);
|
||||
}
|
||||
|
||||
return await _preferencesStorageService.GetAsync<T>(key) ?? await TryMigrateLiteDbToPrefsAsync<T>(key);
|
||||
}
|
||||
|
||||
public Task SaveAsync<T>(string key, T obj)
|
||||
{
|
||||
if (IsLiteDbKey(key))
|
||||
{
|
||||
return _liteDbStorageService.SaveAsync(key, obj);
|
||||
}
|
||||
return _preferencesStorageService.SaveAsync(key, obj);
|
||||
}
|
||||
|
||||
public Task RemoveAsync(string key)
|
||||
{
|
||||
if (IsLiteDbKey(key))
|
||||
{
|
||||
return _liteDbStorageService.RemoveAsync(key);
|
||||
}
|
||||
return _preferencesStorageService.RemoveAsync(key);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_liteDbStorageService is IDisposable disposableLiteDbService)
|
||||
{
|
||||
disposableLiteDbService.Dispose();
|
||||
}
|
||||
if (_preferencesStorageService is IDisposable disposablePrefService)
|
||||
{
|
||||
disposablePrefService.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// Helpers
|
||||
|
||||
private bool IsLiteDbKey(string key)
|
||||
{
|
||||
return _liteDbStorageKeys.Any(key.StartsWith) ||
|
||||
_liteDbStorageKeys.Contains(key);
|
||||
}
|
||||
|
||||
private async Task<T> TryMigrateLiteDbToPrefsAsync<T>(string key)
|
||||
{
|
||||
var currentValue = await _liteDbStorageService.GetAsync<T>(key);
|
||||
if (currentValue != null)
|
||||
{
|
||||
await _preferencesStorageService.SaveAsync(key, currentValue);
|
||||
await _liteDbStorageService.RemoveAsync(key);
|
||||
}
|
||||
|
||||
return currentValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
43
src/Core/Services/NoopPushNotificationListenerService.cs
Normal file
43
src/Core/Services/NoopPushNotificationListenerService.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Models;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace Bit.App.Services
|
||||
{
|
||||
public class NoopPushNotificationListenerService : IPushNotificationListenerService
|
||||
{
|
||||
public Task OnMessageAsync(JObject value, string deviceType)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task OnRegisteredAsync(string token, string deviceType)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public void OnUnregistered(string deviceType)
|
||||
{
|
||||
}
|
||||
|
||||
public void OnError(string message, string deviceType)
|
||||
{
|
||||
}
|
||||
|
||||
public bool ShouldShowNotification()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public Task OnNotificationTapped(BaseNotificationData data)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task OnNotificationDismissed(BaseNotificationData data)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
36
src/Core/Services/NoopPushNotificationService.cs
Normal file
36
src/Core/Services/NoopPushNotificationService.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Models;
|
||||
|
||||
namespace Bit.App.Services
|
||||
{
|
||||
public class NoopPushNotificationService : IPushNotificationService
|
||||
{
|
||||
public bool IsRegisteredForPush => false;
|
||||
|
||||
public Task<bool> AreNotificationsSettingsEnabledAsync()
|
||||
{
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
public Task<string> GetTokenAsync()
|
||||
{
|
||||
return Task.FromResult(null as string);
|
||||
}
|
||||
|
||||
public Task RegisterAsync()
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task UnregisterAsync()
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public void DismissLocalNotification(string notificationId) { }
|
||||
|
||||
public void SendLocalNotification(string title, string message, BaseNotificationData data) { }
|
||||
}
|
||||
}
|
||||
149
src/Core/Services/PreferencesStorageService.cs
Normal file
149
src/Core/Services/PreferencesStorageService.cs
Normal file
@@ -0,0 +1,149 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Abstractions;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
|
||||
namespace Bit.App.Services
|
||||
{
|
||||
public class PreferencesStorageService : IStorageService, ISynchronousStorageService
|
||||
{
|
||||
public static string KeyFormat = "bwPreferencesStorage:{0}";
|
||||
|
||||
private readonly string _sharedName;
|
||||
private readonly JsonSerializerSettings _jsonSettings = new JsonSerializerSettings
|
||||
{
|
||||
ContractResolver = new CamelCasePropertyNamesContractResolver(),
|
||||
NullValueHandling = NullValueHandling.Ignore
|
||||
};
|
||||
|
||||
public PreferencesStorageService(string sharedName)
|
||||
{
|
||||
_sharedName = sharedName;
|
||||
}
|
||||
|
||||
public Task<T> GetAsync<T>(string key) => Task.FromResult(Get<T>(key));
|
||||
|
||||
public Task SaveAsync<T>(string key, T obj)
|
||||
{
|
||||
Save(key, obj);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task RemoveAsync(string key)
|
||||
{
|
||||
Remove(key);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public T Get<T>(string key)
|
||||
{
|
||||
var formattedKey = string.Format(KeyFormat, key);
|
||||
if (!Microsoft.Maui.Storage.Preferences.ContainsKey(formattedKey, _sharedName))
|
||||
{
|
||||
return default(T);
|
||||
}
|
||||
|
||||
var objType = typeof(T);
|
||||
if (objType == typeof(string))
|
||||
{
|
||||
var val = Microsoft.Maui.Storage.Preferences.Get(formattedKey, default(string), _sharedName);
|
||||
return (T)(object)val;
|
||||
}
|
||||
else if (objType == typeof(bool) || objType == typeof(bool?))
|
||||
{
|
||||
var val = Microsoft.Maui.Storage.Preferences.Get(formattedKey, default(bool), _sharedName);
|
||||
return ChangeType<T>(val);
|
||||
}
|
||||
else if (objType == typeof(int) || objType == typeof(int?))
|
||||
{
|
||||
var val = Microsoft.Maui.Storage.Preferences.Get(formattedKey, default(int), _sharedName);
|
||||
return ChangeType<T>(val);
|
||||
}
|
||||
else if (objType == typeof(long) || objType == typeof(long?))
|
||||
{
|
||||
var val = Microsoft.Maui.Storage.Preferences.Get(formattedKey, default(long), _sharedName);
|
||||
return ChangeType<T>(val);
|
||||
}
|
||||
else if (objType == typeof(double) || objType == typeof(double?))
|
||||
{
|
||||
var val = Microsoft.Maui.Storage.Preferences.Get(formattedKey, default(double), _sharedName);
|
||||
return ChangeType<T>(val);
|
||||
}
|
||||
else if (objType == typeof(DateTime) || objType == typeof(DateTime?))
|
||||
{
|
||||
var val = Microsoft.Maui.Storage.Preferences.Get(formattedKey, default(DateTime), _sharedName);
|
||||
return ChangeType<T>(val);
|
||||
}
|
||||
else
|
||||
{
|
||||
var val = Microsoft.Maui.Storage.Preferences.Get(formattedKey, default(string), _sharedName);
|
||||
return JsonConvert.DeserializeObject<T>(val, _jsonSettings);
|
||||
}
|
||||
}
|
||||
|
||||
public void Save<T>(string key, T obj)
|
||||
{
|
||||
if (obj == null)
|
||||
{
|
||||
Remove(key);
|
||||
return;
|
||||
}
|
||||
|
||||
var formattedKey = string.Format(KeyFormat, key);
|
||||
var objType = typeof(T);
|
||||
if (objType == typeof(string))
|
||||
{
|
||||
Microsoft.Maui.Storage.Preferences.Set(formattedKey, obj as string, _sharedName);
|
||||
}
|
||||
else if (objType == typeof(bool) || objType == typeof(bool?))
|
||||
{
|
||||
Microsoft.Maui.Storage.Preferences.Set(formattedKey, (obj as bool?).Value, _sharedName);
|
||||
}
|
||||
else if (objType == typeof(int) || objType == typeof(int?))
|
||||
{
|
||||
Microsoft.Maui.Storage.Preferences.Set(formattedKey, (obj as int?).Value, _sharedName);
|
||||
}
|
||||
else if (objType == typeof(long) || objType == typeof(long?))
|
||||
{
|
||||
Microsoft.Maui.Storage.Preferences.Set(formattedKey, (obj as long?).Value, _sharedName);
|
||||
}
|
||||
else if (objType == typeof(double) || objType == typeof(double?))
|
||||
{
|
||||
Microsoft.Maui.Storage.Preferences.Set(formattedKey, (obj as double?).Value, _sharedName);
|
||||
}
|
||||
else if (objType == typeof(DateTime) || objType == typeof(DateTime?))
|
||||
{
|
||||
Microsoft.Maui.Storage.Preferences.Set(formattedKey, (obj as DateTime?).Value, _sharedName);
|
||||
}
|
||||
else
|
||||
{
|
||||
Microsoft.Maui.Storage.Preferences.Set(formattedKey, JsonConvert.SerializeObject(obj, _jsonSettings),
|
||||
_sharedName);
|
||||
}
|
||||
}
|
||||
|
||||
public void Remove(string key)
|
||||
{
|
||||
var formattedKey = string.Format(KeyFormat, key);
|
||||
if (Microsoft.Maui.Storage.Preferences.ContainsKey(formattedKey, _sharedName))
|
||||
{
|
||||
Microsoft.Maui.Storage.Preferences.Remove(formattedKey, _sharedName);
|
||||
}
|
||||
}
|
||||
|
||||
private static T ChangeType<T>(object value)
|
||||
{
|
||||
var t = typeof(T);
|
||||
if (t.IsGenericType && t.GetGenericTypeDefinition().Equals(typeof(Nullable<>)))
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
return default(T);
|
||||
}
|
||||
t = Nullable.GetUnderlyingType(t);
|
||||
}
|
||||
return (T)Convert.ChangeType(value, t);
|
||||
}
|
||||
}
|
||||
}
|
||||
291
src/Core/Services/PushNotificationListenerService.cs
Normal file
291
src/Core/Services/PushNotificationListenerService.cs
Normal file
@@ -0,0 +1,291 @@
|
||||
#if !FDROID
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Models;
|
||||
using Bit.App.Pages;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Response;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui;
|
||||
|
||||
namespace Bit.App.Services
|
||||
{
|
||||
public class PushNotificationListenerService : IPushNotificationListenerService
|
||||
{
|
||||
const string TAG = "##PUSH NOTIFICATIONS";
|
||||
|
||||
private bool _showNotification;
|
||||
private LazyResolve<ISyncService> _syncService = new LazyResolve<ISyncService>();
|
||||
private LazyResolve<IStateService> _stateService = new LazyResolve<IStateService>();
|
||||
private LazyResolve<IAppIdService> _appIdService = new LazyResolve<IAppIdService>();
|
||||
private LazyResolve<IApiService> _apiService = new LazyResolve<IApiService>();
|
||||
private LazyResolve<IMessagingService> _messagingService = new LazyResolve<IMessagingService>();
|
||||
private LazyResolve<IPushNotificationService> _pushNotificationService = new LazyResolve<IPushNotificationService>();
|
||||
private LazyResolve<ILogger> _logger = new LazyResolve<ILogger>();
|
||||
|
||||
public async Task OnMessageAsync(JObject value, string deviceType)
|
||||
{
|
||||
Debug.WriteLine($"{TAG} OnMessageAsync called");
|
||||
|
||||
if (value == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_showNotification = false;
|
||||
Debug.WriteLine($"{TAG} Message Arrived: {JsonConvert.SerializeObject(value)}");
|
||||
|
||||
NotificationResponse notification = null;
|
||||
if (deviceType == Device.Android)
|
||||
{
|
||||
notification = value.ToObject<NotificationResponse>();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!value.TryGetValue("data", StringComparison.OrdinalIgnoreCase, out JToken dataToken) ||
|
||||
dataToken == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
notification = dataToken.ToObject<NotificationResponse>();
|
||||
}
|
||||
|
||||
Debug.WriteLine($"{TAG} - Notification object created: t:{notification?.Type} - p:{notification?.Payload}");
|
||||
|
||||
var appId = await _appIdService.Value.GetAppIdAsync();
|
||||
if (notification?.Payload == null || notification.ContextId == appId)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var myUserId = await _stateService.Value.GetActiveUserIdAsync();
|
||||
var isAuthenticated = await _stateService.Value.IsAuthenticatedAsync();
|
||||
switch (notification.Type)
|
||||
{
|
||||
case NotificationType.SyncCipherUpdate:
|
||||
case NotificationType.SyncCipherCreate:
|
||||
var cipherCreateUpdateMessage = JsonConvert.DeserializeObject<SyncCipherNotification>(
|
||||
notification.Payload);
|
||||
if (isAuthenticated && cipherCreateUpdateMessage.UserId == myUserId)
|
||||
{
|
||||
await _syncService.Value.SyncUpsertCipherAsync(cipherCreateUpdateMessage,
|
||||
notification.Type == NotificationType.SyncCipherUpdate);
|
||||
}
|
||||
break;
|
||||
case NotificationType.SyncFolderUpdate:
|
||||
case NotificationType.SyncFolderCreate:
|
||||
var folderCreateUpdateMessage = JsonConvert.DeserializeObject<SyncFolderNotification>(
|
||||
notification.Payload);
|
||||
if (isAuthenticated && folderCreateUpdateMessage.UserId == myUserId)
|
||||
{
|
||||
await _syncService.Value.SyncUpsertFolderAsync(folderCreateUpdateMessage,
|
||||
notification.Type == NotificationType.SyncFolderUpdate);
|
||||
}
|
||||
break;
|
||||
case NotificationType.SyncLoginDelete:
|
||||
case NotificationType.SyncCipherDelete:
|
||||
var loginDeleteMessage = JsonConvert.DeserializeObject<SyncCipherNotification>(
|
||||
notification.Payload);
|
||||
if (isAuthenticated && loginDeleteMessage.UserId == myUserId)
|
||||
{
|
||||
await _syncService.Value.SyncDeleteCipherAsync(loginDeleteMessage);
|
||||
}
|
||||
break;
|
||||
case NotificationType.SyncFolderDelete:
|
||||
var folderDeleteMessage = JsonConvert.DeserializeObject<SyncFolderNotification>(
|
||||
notification.Payload);
|
||||
if (isAuthenticated && folderDeleteMessage.UserId == myUserId)
|
||||
{
|
||||
await _syncService.Value.SyncDeleteFolderAsync(folderDeleteMessage);
|
||||
}
|
||||
break;
|
||||
case NotificationType.SyncCiphers:
|
||||
case NotificationType.SyncVault:
|
||||
case NotificationType.SyncSettings:
|
||||
if (isAuthenticated)
|
||||
{
|
||||
await _syncService.Value.FullSyncAsync(false);
|
||||
}
|
||||
break;
|
||||
case NotificationType.SyncOrgKeys:
|
||||
if (isAuthenticated)
|
||||
{
|
||||
await _apiService.Value.RefreshIdentityTokenAsync();
|
||||
await _syncService.Value.FullSyncAsync(true);
|
||||
}
|
||||
break;
|
||||
case NotificationType.LogOut:
|
||||
if (isAuthenticated)
|
||||
{
|
||||
_messagingService.Value.Send("logout");
|
||||
}
|
||||
break;
|
||||
case NotificationType.SyncSendCreate:
|
||||
case NotificationType.SyncSendUpdate:
|
||||
var sendCreateUpdateMessage = JsonConvert.DeserializeObject<SyncSendNotification>(
|
||||
notification.Payload);
|
||||
if (isAuthenticated && sendCreateUpdateMessage.UserId == myUserId)
|
||||
{
|
||||
await _syncService.Value.SyncUpsertSendAsync(sendCreateUpdateMessage,
|
||||
notification.Type == NotificationType.SyncSendUpdate);
|
||||
}
|
||||
break;
|
||||
case NotificationType.SyncSendDelete:
|
||||
var sendDeleteMessage = JsonConvert.DeserializeObject<SyncSendNotification>(
|
||||
notification.Payload);
|
||||
if (isAuthenticated && sendDeleteMessage.UserId == myUserId)
|
||||
{
|
||||
await _syncService.Value.SyncDeleteSendAsync(sendDeleteMessage);
|
||||
}
|
||||
break;
|
||||
case NotificationType.AuthRequest:
|
||||
var passwordlessLoginMessage = JsonConvert.DeserializeObject<PasswordlessRequestNotification>(notification.Payload);
|
||||
|
||||
// if the user has not enabled passwordless logins ignore requests
|
||||
if (!await _stateService.Value.GetApprovePasswordlessLoginsAsync(passwordlessLoginMessage?.UserId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// if there is a request modal opened ignore all incoming requests
|
||||
// App.Current can be null if the app is killed
|
||||
if (App.Current != null && App.Current.MainPage.Navigation.ModalStack.Any(p => p is NavigationPage navPage && navPage.CurrentPage is LoginPasswordlessPage))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await _stateService.Value.SetPasswordlessLoginNotificationAsync(passwordlessLoginMessage);
|
||||
var userEmail = await _stateService.Value.GetEmailAsync(passwordlessLoginMessage?.UserId);
|
||||
|
||||
var notificationData = new PasswordlessNotificationData()
|
||||
{
|
||||
Id = Constants.PasswordlessNotificationId,
|
||||
TimeoutInMinutes = Constants.PasswordlessNotificationTimeoutInMinutes,
|
||||
UserEmail = userEmail,
|
||||
};
|
||||
|
||||
_pushNotificationService.Value.SendLocalNotification(AppResources.LogInRequested, String.Format(AppResources.ConfimLogInAttempForX, userEmail), notificationData);
|
||||
_messagingService.Value.Send(Constants.PasswordlessLoginRequestKey, passwordlessLoginMessage);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task OnRegisteredAsync(string token, string deviceType)
|
||||
{
|
||||
Debug.WriteLine($"{TAG} - Device Registered - Token : {token}");
|
||||
var isAuthenticated = await _stateService.Value.IsAuthenticatedAsync();
|
||||
if (!isAuthenticated)
|
||||
{
|
||||
Debug.WriteLine($"{TAG} - not auth");
|
||||
return;
|
||||
}
|
||||
|
||||
var appId = await _appIdService.Value.GetAppIdAsync();
|
||||
try
|
||||
{
|
||||
#if DEBUG
|
||||
await _stateService.Value.SetPushInstallationRegistrationErrorAsync(null);
|
||||
#endif
|
||||
|
||||
await _apiService.Value.PutDeviceTokenAsync(appId,
|
||||
new Core.Models.Request.DeviceTokenRequest { PushToken = token });
|
||||
|
||||
Debug.WriteLine($"{TAG} Registered device with server.");
|
||||
|
||||
await _stateService.Value.SetPushLastRegistrationDateAsync(DateTime.UtcNow);
|
||||
if (deviceType == Device.Android)
|
||||
{
|
||||
await _stateService.Value.SetPushCurrentTokenAsync(token);
|
||||
}
|
||||
}
|
||||
#if DEBUG
|
||||
catch (ApiException apiEx)
|
||||
{
|
||||
Debug.WriteLine($"{TAG} Failed to register device.");
|
||||
|
||||
await _stateService.Value.SetPushInstallationRegistrationErrorAsync(apiEx.Error?.Message);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
await _stateService.Value.SetPushInstallationRegistrationErrorAsync(e.Message);
|
||||
throw;
|
||||
}
|
||||
#else
|
||||
catch (ApiException)
|
||||
{
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
public void OnUnregistered(string deviceType)
|
||||
{
|
||||
Debug.WriteLine($"{TAG} - Device Unnregistered");
|
||||
}
|
||||
|
||||
public void OnError(string message, string deviceType)
|
||||
{
|
||||
Debug.WriteLine($"{TAG} error - {message}");
|
||||
}
|
||||
|
||||
public async Task OnNotificationTapped(BaseNotificationData data)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (data is PasswordlessNotificationData passwordlessNotificationData)
|
||||
{
|
||||
var notificationUserId = await _stateService.Value.GetUserIdAsync(passwordlessNotificationData.UserEmail);
|
||||
var activeUserEmail = await _stateService.Value.GetActiveUserEmailAsync();
|
||||
var notificationSaved = await _stateService.Value.GetPasswordlessLoginNotificationAsync();
|
||||
if (activeUserEmail != passwordlessNotificationData.UserEmail && notificationUserId != null && notificationSaved != null)
|
||||
{
|
||||
await _stateService.Value.SetActiveUserAsync(notificationUserId);
|
||||
_messagingService.Value.Send(AccountsManagerMessageCommands.SWITCHED_ACCOUNT);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Value.Exception(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task OnNotificationDismissed(BaseNotificationData data)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (data is PasswordlessNotificationData passwordlessNotificationData)
|
||||
{
|
||||
var savedNotification = await _stateService.Value.GetPasswordlessLoginNotificationAsync();
|
||||
if (savedNotification != null)
|
||||
{
|
||||
await _stateService.Value.SetPasswordlessLoginNotificationAsync(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Value.Exception(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShouldShowNotification()
|
||||
{
|
||||
return _showNotification;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
56
src/Core/Services/SecureStorageService.cs
Normal file
56
src/Core/Services/SecureStorageService.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Abstractions;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
|
||||
namespace Bit.App.Services
|
||||
{
|
||||
public class SecureStorageService : IStorageService
|
||||
{
|
||||
private readonly string _keyFormat = "bwSecureStorage:{0}";
|
||||
private readonly JsonSerializerSettings _jsonSettings = new JsonSerializerSettings
|
||||
{
|
||||
ContractResolver = new CamelCasePropertyNamesContractResolver()
|
||||
};
|
||||
|
||||
public async Task<T> GetAsync<T>(string key)
|
||||
{
|
||||
var formattedKey = string.Format(_keyFormat, key);
|
||||
var val = await Microsoft.Maui.Storage.SecureStorage.GetAsync(formattedKey);
|
||||
if (typeof(T) == typeof(string))
|
||||
{
|
||||
return (T)(object)val;
|
||||
}
|
||||
else
|
||||
{
|
||||
return JsonConvert.DeserializeObject<T>(val, _jsonSettings);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SaveAsync<T>(string key, T obj)
|
||||
{
|
||||
if (obj == null)
|
||||
{
|
||||
await RemoveAsync(key);
|
||||
return;
|
||||
}
|
||||
var formattedKey = string.Format(_keyFormat, key);
|
||||
if (typeof(T) == typeof(string))
|
||||
{
|
||||
await Microsoft.Maui.Storage.SecureStorage.SetAsync(formattedKey, obj as string);
|
||||
}
|
||||
else
|
||||
{
|
||||
await Microsoft.Maui.Storage.SecureStorage.SetAsync(formattedKey,
|
||||
JsonConvert.SerializeObject(obj, _jsonSettings));
|
||||
}
|
||||
}
|
||||
|
||||
public Task RemoveAsync(string key)
|
||||
{
|
||||
var formattedKey = string.Format(_keyFormat, key);
|
||||
Microsoft.Maui.Storage.SecureStorage.Remove(formattedKey);
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Bit.Core.Utilities;
|
||||
using DeviceType = Bit.Core.Enums.DeviceType;
|
||||
|
||||
namespace Bit.Core.Services
|
||||
{
|
||||
|
||||
38
src/Core/Services/UserPinService.cs
Normal file
38
src/Core/Services/UserPinService.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Abstractions;
|
||||
|
||||
namespace Bit.App.Services
|
||||
{
|
||||
public class UserPinService : IUserPinService
|
||||
{
|
||||
private readonly IStateService _stateService;
|
||||
private readonly ICryptoService _cryptoService;
|
||||
|
||||
public UserPinService(IStateService stateService, ICryptoService cryptoService)
|
||||
{
|
||||
_stateService = stateService;
|
||||
_cryptoService = cryptoService;
|
||||
}
|
||||
|
||||
public async Task SetupPinAsync(string pin, bool requireMasterPasswordOnRestart)
|
||||
{
|
||||
var kdfConfig = await _stateService.GetActiveUserCustomDataAsync(a => new KdfConfig(a?.Profile));
|
||||
var email = await _stateService.GetEmailAsync();
|
||||
var pinKey = await _cryptoService.MakePinKeyAsync(pin, email, kdfConfig);
|
||||
var userKey = await _cryptoService.GetUserKeyAsync();
|
||||
var protectedPinKey = await _cryptoService.EncryptAsync(userKey.Key, pinKey);
|
||||
|
||||
var encPin = await _cryptoService.EncryptAsync(pin);
|
||||
await _stateService.SetProtectedPinAsync(encPin.EncryptedString);
|
||||
|
||||
if (requireMasterPasswordOnRestart)
|
||||
{
|
||||
await _stateService.SetPinKeyEncryptedUserKeyEphemeralAsync(protectedPinKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _stateService.SetPinKeyEncryptedUserKeyAsync(protectedPinKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user