1
0
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:
Federico Maccaroni
2023-09-29 11:02:19 -03:00
parent bbef0f8c93
commit 8ef9443b1e
717 changed files with 5367 additions and 4702 deletions

View File

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

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

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

View File

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

View File

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

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

View File

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

View File

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

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

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

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

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

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

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

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

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

View 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

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

View File

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

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