From 57314990448dfc664788ba50fa4aaa0e621910ef Mon Sep 17 00:00:00 2001 From: Federico Maccaroni Date: Tue, 16 May 2023 22:00:09 +0200 Subject: [PATCH] PM-1575 Added discoverable passkeys and WIP non-discoverable ones --- src/Android/Assets/bwi-font.ttf | Bin 72280 -> 75636 bytes .../CipherViewCell/CipherViewCellViewModel.cs | 4 +- src/App/Pages/Vault/BaseCipherViewModel.cs | 2 + src/App/Pages/Vault/CipherAddEditPage.xaml | 32 +++++++ .../Pages/Vault/CipherAddEditPageViewModel.cs | 1 + src/App/Pages/Vault/CipherDetailsPage.xaml | 50 +++++++++++ src/App/Pages/Vault/CipherDetailsPage.xaml.cs | 4 +- .../Pages/Vault/CipherDetailsPageViewModel.cs | 9 +- .../GroupingsPage/GroupingsPageListItem.cs | 6 ++ .../GroupingsPage/GroupingsPageViewModel.cs | 42 ++++----- src/App/Pages/Vault/SharePage.xaml | 2 +- src/App/Pages/Vault/SharePage.xaml.cs | 13 --- src/App/Pages/Vault/SharePageViewModel.cs | 8 +- src/App/Resources/AppResources.Designer.cs | 81 ++++++++++++++---- src/App/Resources/AppResources.resx | 16 ++++ src/App/Utilities/AppHelpers.cs | 18 +++- src/App/Utilities/IconGlyphExtensions.cs | 19 ++-- src/App/Utilities/IconImageConverter.cs | 49 +++++++---- src/Core/BitwardenIcons.cs | 1 + src/Core/Models/Data/CipherData.cs | 6 +- src/Core/Models/Domain/Cipher.cs | 5 +- src/Core/Models/Request/CipherRequest.cs | 16 ++++ src/Core/Models/Response/CipherResponse.cs | 1 + src/Core/Models/View/CipherView.cs | 10 ++- src/Core/Models/View/Fido2KeyView.cs | 8 +- src/Core/Models/View/ILaunchableView.cs | 8 ++ src/Core/Models/View/LoginUriView.cs | 2 +- src/Core/Services/CipherService.cs | 34 ++++---- src/Core/Utilities/CipherTypeExtensions.cs | 14 +++ src/iOS.Autofill/Resources/bwi-font.ttf | Bin 72280 -> 75636 bytes src/iOS.Extension/Resources/bwi-font.ttf | Bin 72668 -> 75636 bytes src/iOS/Resources/bwi-font.ttf | Bin 72280 -> 75636 bytes 32 files changed, 339 insertions(+), 122 deletions(-) create mode 100644 src/Core/Models/View/ILaunchableView.cs create mode 100644 src/Core/Utilities/CipherTypeExtensions.cs diff --git a/src/Android/Assets/bwi-font.ttf b/src/Android/Assets/bwi-font.ttf index 7c7afd4cdb9d110404b67ba3e8cd89d95151c138..3c54d7041250872f3bc87f4a7f6cce615a969185 100644 GIT binary patch delta 5446 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 delta 2102 zcmZ`)Yitx%6u#%)*_qj$*`3+4-CbyDcXr!ZLLao--7U7IQY1W@7DNMR1lrOED{P@s zgyL&gQ?NuOJSq!@kd&aoN|aCxhG0??f+P@rC{Y83fIpgugdYS5Q9AX^ZYxA!X71d3 z?m73KbH4kXnY)*`-%fG~0)!BWBncyBi(j4*QkwZB&YZ#4+}hdFb+h>9WkMhqC&h*O*-~a(1zrOd+pK}tWP#KO^ z)SPBvSQP>sHiLfg|(MqU=Oddie+dw%buApKp8;}2~!PuMv3|eHK zmpG0Ci^W)`1hWSmc8JlsCm}GmF!q}uK%8U1TR2=Pb4I`2fyaV(k{L8f{3MzVIraTy z$V&vutqAORgpnEuY7X#VY@Q;g5w}W=VcK@l>awvdl1sGO8AlnDtj0R$bdT`@^jotf zDO-e35uk_LIbcaufhJ*+B)M%iC(jEU&+|^3Z)53vrP*l>68VyDhtL;L1yzG zZuDxJm)>y|IyOso&AX2Y+~{J{5Ee;_A{pTmroeK|_m8rw$`=HY(s-jJ4oYH!;_)aA zrYa>$qdP$pCF3HBL*M>$HtW>3sj!g>#Zb*g;MbIq52cCyT1koKD=jsa{D0ts>a@l; zu6V4+uQFk9cI-B-XBz%jJXm(-XE=aZr=dikpE{=X!Q7l_({gfy%*@}VoCZ*;ckvKC1a_!hGd7@9<^?{Fx5I+7I`sq|N#nfcPp;paYk?pjr{%BVh4 z&3U5-=Xi^ weE0BswaID8J0HCD!5_Vj-o9=3w}1EHp}t>sICjiU!+U@^NESI2D1;^SUkLq{T>t<8 diff --git a/src/App/Controls/CipherViewCell/CipherViewCellViewModel.cs b/src/App/Controls/CipherViewCell/CipherViewCellViewModel.cs index ea21df7dc..b5150b003 100644 --- a/src/App/Controls/CipherViewCell/CipherViewCellViewModel.cs +++ b/src/App/Controls/CipherViewCell/CipherViewCellViewModel.cs @@ -31,7 +31,7 @@ namespace Bit.App.Controls public bool ShowIconImage { get => WebsiteIconsEnabled - && !string.IsNullOrWhiteSpace(Cipher.Login?.Uri) + && !string.IsNullOrWhiteSpace(Cipher.LaunchUri) && IconImageSource != null; } @@ -41,7 +41,7 @@ namespace Bit.App.Controls { if (_iconImageSource == string.Empty) // default value since icon source can return null { - _iconImageSource = IconImageHelper.GetLoginIconImage(Cipher); + _iconImageSource = IconImageHelper.GetIconImage(Cipher); } return _iconImageSource; } diff --git a/src/App/Pages/Vault/BaseCipherViewModel.cs b/src/App/Pages/Vault/BaseCipherViewModel.cs index 871d8aa24..9cbfe7e83 100644 --- a/src/App/Pages/Vault/BaseCipherViewModel.cs +++ b/src/App/Pages/Vault/BaseCipherViewModel.cs @@ -37,6 +37,8 @@ namespace Bit.App.Pages set => SetProperty(ref _cipher, value, additionalPropertyNames: AdditionalPropertiesToRaiseOnCipherChanged); } + public string CreationDate => string.Format(AppResources.CreatedX, Cipher.CreationDate.ToShortDateString()); + public AsyncCommand CheckPasswordCommand { get; } protected async Task CheckPasswordAsync() diff --git a/src/App/Pages/Vault/CipherAddEditPage.xaml b/src/App/Pages/Vault/CipherAddEditPage.xaml index b2920a5de..af2d45810 100644 --- a/src/App/Pages/Vault/CipherAddEditPage.xaml +++ b/src/App/Pages/Vault/CipherAddEditPage.xaml @@ -550,6 +550,38 @@ StyleClass="box-value,capitalize-sentence-input" /> + + + + diff --git a/src/App/Pages/Vault/CipherAddEditPageViewModel.cs b/src/App/Pages/Vault/CipherAddEditPageViewModel.cs index b722d0b25..ad303dac9 100644 --- a/src/App/Pages/Vault/CipherAddEditPageViewModel.cs +++ b/src/App/Pages/Vault/CipherAddEditPageViewModel.cs @@ -297,6 +297,7 @@ namespace Bit.App.Pages public bool IsIdentity => Cipher?.Type == CipherType.Identity; public bool IsCard => Cipher?.Type == CipherType.Card; public bool IsSecureNote => Cipher?.Type == CipherType.SecureNote; + public bool IsFido2Key => Cipher?.Type == CipherType.Fido2Key; public bool ShowUris => IsLogin && Cipher.Login.HasUris; public bool ShowAttachments => Cipher.HasAttachments; public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye; diff --git a/src/App/Pages/Vault/CipherDetailsPage.xaml b/src/App/Pages/Vault/CipherDetailsPage.xaml index 4637d30b6..9666743fc 100644 --- a/src/App/Pages/Vault/CipherDetailsPage.xaml +++ b/src/App/Pages/Vault/CipherDetailsPage.xaml @@ -497,6 +497,56 @@ + + diff --git a/src/App/Pages/Vault/CipherDetailsPage.xaml.cs b/src/App/Pages/Vault/CipherDetailsPage.xaml.cs index 52c9cbf7e..3ee45b927 100644 --- a/src/App/Pages/Vault/CipherDetailsPage.xaml.cs +++ b/src/App/Pages/Vault/CipherDetailsPage.xaml.cs @@ -302,13 +302,13 @@ namespace Bit.App.Pages { ToolbarItems.Remove(_collectionsItem); } - if (!ToolbarItems.Contains(_cloneItem)) + if (_vm.Cipher.Type != Core.Enums.CipherType.Fido2Key && !ToolbarItems.Contains(_cloneItem)) { ToolbarItems.Insert(1, _cloneItem); } if (!ToolbarItems.Contains(_shareItem)) { - ToolbarItems.Insert(2, _shareItem); + ToolbarItems.Insert(_vm.Cipher.Type == Core.Enums.CipherType.Fido2Key ? 1 : 2, _shareItem); } } else diff --git a/src/App/Pages/Vault/CipherDetailsPageViewModel.cs b/src/App/Pages/Vault/CipherDetailsPageViewModel.cs index f0005ecf2..f2b462dc6 100644 --- a/src/App/Pages/Vault/CipherDetailsPageViewModel.cs +++ b/src/App/Pages/Vault/CipherDetailsPageViewModel.cs @@ -68,7 +68,7 @@ namespace Bit.App.Pages CopyCommand = new AsyncCommand((id) => CopyAsync(id, null), onException: ex => _logger.Exception(ex), allowsMultipleExecutions: false); CopyUriCommand = new AsyncCommand(uriView => CopyAsync("LoginUri", uriView.Uri), onException: ex => _logger.Exception(ex), allowsMultipleExecutions: false); CopyFieldCommand = new AsyncCommand(field => CopyAsync(field.Type == FieldType.Hidden ? "H_FieldValue" : "FieldValue", field.Value), onException: ex => _logger.Exception(ex), allowsMultipleExecutions: false); - LaunchUriCommand = new Command(LaunchUri); + LaunchUriCommand = new Command(LaunchUri); TogglePasswordCommand = new Command(TogglePassword); ToggleCardNumberCommand = new Command(ToggleCardNumber); ToggleCardCodeCommand = new Command(ToggleCardCode); @@ -146,6 +146,7 @@ namespace Bit.App.Pages public bool IsIdentity => Cipher?.Type == Core.Enums.CipherType.Identity; public bool IsCard => Cipher?.Type == Core.Enums.CipherType.Card; public bool IsSecureNote => Cipher?.Type == Core.Enums.CipherType.SecureNote; + public bool IsFido2Key => Cipher?.Type == Core.Enums.CipherType.Fido2Key; public FormattedString ColoredPassword => GeneratedValueFormatter.Format(Cipher.Login.Password); public FormattedString UpdatedText { @@ -668,11 +669,11 @@ namespace Bit.App.Pages } } - private void LaunchUri(LoginUriView uri) + private void LaunchUri(ILaunchableView launchableView) { - if (uri.CanLaunch && (Page as BaseContentPage).DoOnce()) + if (launchableView.CanLaunch && (Page as BaseContentPage).DoOnce()) { - _platformUtilsService.LaunchUri(uri.LaunchUri); + _platformUtilsService.LaunchUri(launchableView.LaunchUri); } } diff --git a/src/App/Pages/Vault/GroupingsPage/GroupingsPageListItem.cs b/src/App/Pages/Vault/GroupingsPage/GroupingsPageListItem.cs index 2df0350af..02ce7cf17 100644 --- a/src/App/Pages/Vault/GroupingsPage/GroupingsPageListItem.cs +++ b/src/App/Pages/Vault/GroupingsPage/GroupingsPageListItem.cs @@ -59,6 +59,9 @@ namespace Bit.App.Pages case CipherType.Identity: _name = AppResources.TypeIdentity; break; + case CipherType.Fido2Key: + _name = AppResources.Passkey; + break; default: break; } @@ -107,6 +110,9 @@ namespace Bit.App.Pages case CipherType.Identity: _icon = BitwardenIcons.IdCard; break; + case CipherType.Fido2Key: + _icon = BitwardenIcons.Passkey; + break; default: _icon = BitwardenIcons.Globe; break; diff --git a/src/App/Pages/Vault/GroupingsPage/GroupingsPageViewModel.cs b/src/App/Pages/Vault/GroupingsPage/GroupingsPageViewModel.cs index 8b4af6a02..9bb67dc68 100644 --- a/src/App/Pages/Vault/GroupingsPage/GroupingsPageViewModel.cs +++ b/src/App/Pages/Vault/GroupingsPage/GroupingsPageViewModel.cs @@ -235,34 +235,17 @@ namespace Bit.App.Pages { AddTotpGroupItem(groupedItems, uppercaseGroupNames); - groupedItems.Add(new GroupingsPageListGroup( - AppResources.Types, 4, uppercaseGroupNames, !hasFavorites) + var types = Enum.GetValues(typeof(CipherType)); + var typesGroup = new GroupingsPageListGroup(AppResources.Types, types.Length, uppercaseGroupNames, !hasFavorites); + foreach (CipherType t in types) { - new GroupingsPageListItem + typesGroup.Add(new GroupingsPageListItem { - Type = CipherType.Login, - ItemCount = (_typeCounts.ContainsKey(CipherType.Login) ? - _typeCounts[CipherType.Login] : 0).ToString("N0") - }, - new GroupingsPageListItem - { - Type = CipherType.Card, - ItemCount = (_typeCounts.ContainsKey(CipherType.Card) ? - _typeCounts[CipherType.Card] : 0).ToString("N0") - }, - new GroupingsPageListItem - { - Type = CipherType.Identity, - ItemCount = (_typeCounts.ContainsKey(CipherType.Identity) ? - _typeCounts[CipherType.Identity] : 0).ToString("N0") - }, - new GroupingsPageListItem - { - Type = CipherType.SecureNote, - ItemCount = (_typeCounts.ContainsKey(CipherType.SecureNote) ? - _typeCounts[CipherType.SecureNote] : 0).ToString("N0") - }, - }); + Type = t, + ItemCount = _typeCounts.GetValueOrDefault(t).ToString("N0") + }); + } + groupedItems.Add(typesGroup); } if (NestedFolders?.Any() ?? false) { @@ -472,6 +455,9 @@ namespace Bit.App.Pages case CipherType.Identity: title = AppResources.Identities; break; + case CipherType.Fido2Key: + title = AppResources.Passkeys; + break; default: break; } @@ -575,7 +561,9 @@ namespace Bit.App.Pages } else if (Type != null) { - Filter = c => c.Type == Type.Value && !c.IsDeleted; + Filter = c => !c.IsDeleted + && + Type.Value.IsEqualToOrCanSignIn(c.Type); } else if (FolderId != null) { diff --git a/src/App/Pages/Vault/SharePage.xaml b/src/App/Pages/Vault/SharePage.xaml index b3c660bcd..da484eae3 100644 --- a/src/App/Pages/Vault/SharePage.xaml +++ b/src/App/Pages/Vault/SharePage.xaml @@ -15,7 +15,7 @@ - + diff --git a/src/App/Pages/Vault/SharePage.xaml.cs b/src/App/Pages/Vault/SharePage.xaml.cs index b97c203f4..0e94b39cc 100644 --- a/src/App/Pages/Vault/SharePage.xaml.cs +++ b/src/App/Pages/Vault/SharePage.xaml.cs @@ -32,19 +32,6 @@ namespace Bit.App.Pages await LoadOnAppearedAsync(_scrollView, true, () => _vm.LoadAsync()); } - protected override void OnDisappearing() - { - base.OnDisappearing(); - } - - private async void Save_Clicked(object sender, System.EventArgs e) - { - if (DoOnce()) - { - await _vm.SubmitAsync(); - } - } - private async void Close_Clicked(object sender, System.EventArgs e) { if (DoOnce()) diff --git a/src/App/Pages/Vault/SharePageViewModel.cs b/src/App/Pages/Vault/SharePageViewModel.cs index c7e3cfbb1..159da9007 100644 --- a/src/App/Pages/Vault/SharePageViewModel.cs +++ b/src/App/Pages/Vault/SharePageViewModel.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using System.Windows.Input; using Bit.App.Abstractions; using Bit.App.Resources; using Bit.Core.Abstractions; @@ -8,6 +9,7 @@ using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.View; using Bit.Core.Utilities; +using Xamarin.CommunityToolkit.ObjectModel; namespace Bit.App.Pages { @@ -34,6 +36,8 @@ namespace Bit.App.Pages Collections = new ExtendedObservableCollection(); OrganizationOptions = new List>(); PageTitle = AppResources.MoveToOrganization; + + MoveCommand = new AsyncCommand(MoveAsync, onException: ex => HandleException(ex), allowsMultipleExecutions: false); } public string CipherId { get; set; } @@ -62,6 +66,8 @@ namespace Bit.App.Pages set => SetProperty(ref _hasOrganizations, value); } + public ICommand MoveCommand { get; } + public async Task LoadAsync() { var allCollections = await _collectionService.GetAllDecryptedAsync(); @@ -84,7 +90,7 @@ namespace Bit.App.Pages FilterCollections(); } - public async Task SubmitAsync() + public async Task MoveAsync() { var selectedCollectionIds = Collections?.Where(c => c.Checked).Select(c => c.Collection.Id); if (!selectedCollectionIds?.Any() ?? true) diff --git a/src/App/Resources/AppResources.Designer.cs b/src/App/Resources/AppResources.Designer.cs index 2245d5f78..87122249b 100644 --- a/src/App/Resources/AppResources.Designer.cs +++ b/src/App/Resources/AppResources.Designer.cs @@ -202,6 +202,24 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Biometric unlock for this account is disabled pending verification of master password.. + /// + public static string AccountBiometricInvalidated { + get { + return ResourceManager.GetString("AccountBiometricInvalidated", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Autofill biometric unlock for this account is disabled pending verification of master password.. + /// + public static string AccountBiometricInvalidatedExtension { + get { + return ResourceManager.GetString("AccountBiometricInvalidatedExtension", resourceCulture); + } + } + /// /// Looks up a localized string similar to Your new account has been created! You may now log in.. /// @@ -535,6 +553,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Application. + /// + public static string Application { + get { + return ResourceManager.GetString("Application", resourceCulture); + } + } + /// /// Looks up a localized string similar to Approve login requests. /// @@ -967,24 +994,6 @@ namespace Bit.App.Resources { } } - /// - /// Looks up a localized string similar to Biometric unlock disabled pending verification of master password.. - /// - public static string AccountBiometricInvalidated { - get { - return ResourceManager.GetString("AccountBiometricInvalidated", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Biometric unlock for autofill disabled pending verification of master password.. - /// - public static string AccountBiometricInvalidatedExtension { - get { - return ResourceManager.GetString("AccountBiometricInvalidatedExtension", resourceCulture); - } - } - /// /// Looks up a localized string similar to Biometrics. /// @@ -1660,6 +1669,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Created {0}. + /// + public static string CreatedX { + get { + return ResourceManager.GetString("CreatedX", resourceCulture); + } + } + /// /// Looks up a localized string similar to Creating account.... /// @@ -4660,6 +4678,24 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Passkey. + /// + public static string Passkey { + get { + return ResourceManager.GetString("Passkey", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Passkeys. + /// + public static string Passkeys { + get { + return ResourceManager.GetString("Passkeys", resourceCulture); + } + } + /// /// Looks up a localized string similar to Passphrase. /// @@ -7037,6 +7073,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to You cannot edit passkey application because it would invalidate the passkey. + /// + public static string YouCannotEditPasskeyApplicationBecauseItWouldInvalidateThePasskey { + get { + return ResourceManager.GetString("YouCannotEditPasskeyApplicationBecauseItWouldInvalidateThePasskey", resourceCulture); + } + } + /// /// Looks up a localized string similar to Your account has been permanently deleted. /// diff --git a/src/App/Resources/AppResources.resx b/src/App/Resources/AppResources.resx index 5e9c48694..93a2965a0 100644 --- a/src/App/Resources/AppResources.resx +++ b/src/App/Resources/AppResources.resx @@ -2616,4 +2616,20 @@ Do you want to switch to this account? Current master password + + Passkey + + + Passkeys + + + Created {0} + To state the date in which the cipher was created: Created 03/21/2023 + + + Application + + + You cannot edit passkey application because it would invalidate the passkey + diff --git a/src/App/Utilities/AppHelpers.cs b/src/App/Utilities/AppHelpers.cs index 48c0fb73a..49086a18b 100644 --- a/src/App/Utilities/AppHelpers.cs +++ b/src/App/Utilities/AppHelpers.cs @@ -78,6 +78,18 @@ namespace Bit.App.Utilities options.Add(AppResources.CopyNotes); } } + if (cipher.Type == Core.Enums.CipherType.Fido2Key) + { + if (!string.IsNullOrWhiteSpace(cipher.Fido2Key.UserName)) + { + options.Add(AppResources.CopyUsername); + } + if (cipher.Fido2Key.CanLaunch) + { + options.Add(AppResources.Launch); + } + } + var selection = await page.DisplayActionSheet(cipher.Name, AppResources.Cancel, null, options.ToArray()); if (await vaultTimeoutService.IsLockedAsync()) { @@ -96,7 +108,7 @@ namespace Bit.App.Utilities } else if (selection == AppResources.CopyUsername) { - await clipboardService.CopyTextAsync(cipher.Login.Username); + await clipboardService.CopyTextAsync(cipher.Type == CipherType.Login ? cipher.Login.Username : cipher.Fido2Key.UserName); platformUtilsService.ShowToastForCopiedValue(AppResources.Username); } else if (selection == AppResources.CopyPassword) @@ -121,9 +133,9 @@ namespace Bit.App.Utilities } } } - else if (selection == AppResources.Launch) + else if (selection == AppResources.Launch && cipher.CanLaunch) { - platformUtilsService.LaunchUri(cipher.Login.LaunchUri); + platformUtilsService.LaunchUri(cipher.LaunchUri); } else if (selection == AppResources.CopyNumber) { diff --git a/src/App/Utilities/IconGlyphExtensions.cs b/src/App/Utilities/IconGlyphExtensions.cs index d7c6c35ac..d3b33f9f4 100644 --- a/src/App/Utilities/IconGlyphExtensions.cs +++ b/src/App/Utilities/IconGlyphExtensions.cs @@ -8,25 +8,20 @@ namespace Bit.App.Utilities { public static string GetIcon(this CipherView cipher) { - string icon = null; switch (cipher.Type) { case CipherType.Login: - icon = GetLoginIconGlyph(cipher); - break; + return GetLoginIconGlyph(cipher); case CipherType.SecureNote: - icon = BitwardenIcons.StickyNote; - break; + return BitwardenIcons.StickyNote; case CipherType.Card: - icon = BitwardenIcons.CreditCard; - break; + return BitwardenIcons.CreditCard; case CipherType.Identity: - icon = BitwardenIcons.IdCard; - break; - default: - break; + return BitwardenIcons.IdCard; + case CipherType.Fido2Key: + return BitwardenIcons.Passkey; } - return icon; + return null; } static string GetLoginIconGlyph(CipherView cipher) diff --git a/src/App/Utilities/IconImageConverter.cs b/src/App/Utilities/IconImageConverter.cs index e81ec7959..87ad2391b 100644 --- a/src/App/Utilities/IconImageConverter.cs +++ b/src/App/Utilities/IconImageConverter.cs @@ -13,31 +13,29 @@ namespace Bit.App.Utilities public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { var cipher = value as CipherView; - return GetIcon(cipher); + return IconImageHelper.GetIconImage(cipher); } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotImplementedException(); } - - private string GetIcon(CipherView cipher) - { - string icon = null; - switch (cipher.Type) - { - case CipherType.Login: - icon = IconImageHelper.GetLoginIconImage(cipher); - break; - default: - break; - } - return icon; - } } public static class IconImageHelper { + public static string GetIconImage(CipherView cipher) + { + switch (cipher.Type) + { + case CipherType.Login: + return IconImageHelper.GetLoginIconImage(cipher); + case CipherType.Fido2Key: + return IconImageHelper.GetFido2KeyIconImage(cipher); + } + return null; + } + public static string GetLoginIconImage(CipherView cipher) { string image = null; @@ -67,6 +65,26 @@ namespace Bit.App.Utilities return image; } + public static string GetFido2KeyIconImage(CipherView cipher) + { + var hostnameUri = cipher.Fido2Key.LaunchUri; + if (!hostnameUri.Contains(".")) + { + return null; + } + + if (!hostnameUri.Contains("://")) + { + hostnameUri = string.Concat("https://", hostnameUri); + } + if (hostnameUri.StartsWith("http")) + { + return GetIconUrl(hostnameUri); + } + + return null; + } + private static string GetIconUrl(string hostnameUri) { IEnvironmentService _environmentService = ServiceContainer.Resolve("environmentService"); @@ -85,7 +103,6 @@ namespace Bit.App.Utilities } } return string.Format("{0}/{1}/icon.png", iconsUrl, hostname); - } } } diff --git a/src/Core/BitwardenIcons.cs b/src/Core/BitwardenIcons.cs index 2ca47edf5..d9a74e990 100644 --- a/src/Core/BitwardenIcons.cs +++ b/src/Core/BitwardenIcons.cs @@ -114,5 +114,6 @@ public const string ViewCellMenu = "\xe5d3"; public const string Device = "\xe986"; public const string Suitcase = "\xe98c"; + public const string Passkey = "\xe99f"; } } diff --git a/src/Core/Models/Data/CipherData.cs b/src/Core/Models/Data/CipherData.cs index 56181338f..f8bd1a648 100644 --- a/src/Core/Models/Data/CipherData.cs +++ b/src/Core/Models/Data/CipherData.cs @@ -21,6 +21,8 @@ namespace Bit.Core.Models.Data OrganizationUseTotp = response.OrganizationUseTotp; Favorite = response.Favorite; RevisionDate = response.RevisionDate; + CreationDate = response.CreationDate; + DeletedDate = response.DeletedDate; Type = response.Type; Name = response.Name; Notes = response.Notes; @@ -64,7 +66,6 @@ namespace Bit.Core.Models.Data Fields = response.Fields?.Select(f => new FieldData(f)).ToList(); Attachments = response.Attachments?.Select(a => new AttachmentData(a)).ToList(); PasswordHistory = response.PasswordHistory?.Select(ph => new PasswordHistoryData(ph)).ToList(); - DeletedDate = response.DeletedDate; } public string Id { get; set; } @@ -76,6 +77,8 @@ namespace Bit.Core.Models.Data public bool OrganizationUseTotp { get; set; } public bool Favorite { get; set; } public DateTime RevisionDate { get; set; } + public DateTime CreationDate { get; set; } + public DateTime? DeletedDate { get; set; } public Enums.CipherType Type { get; set; } public string Name { get; set; } public string Notes { get; set; } @@ -88,7 +91,6 @@ namespace Bit.Core.Models.Data public List Attachments { get; set; } public List PasswordHistory { get; set; } public List CollectionIds { get; set; } - public DateTime? DeletedDate { get; set; } public Enums.CipherRepromptType Reprompt { get; set; } } } diff --git a/src/Core/Models/Domain/Cipher.cs b/src/Core/Models/Domain/Cipher.cs index cfe1db60c..5715760c3 100644 --- a/src/Core/Models/Domain/Cipher.cs +++ b/src/Core/Models/Domain/Cipher.cs @@ -29,6 +29,7 @@ namespace Bit.Core.Models.Domain Edit = obj.Edit; ViewPassword = obj.ViewPassword; RevisionDate = obj.RevisionDate; + CreationDate = obj.CreationDate; CollectionIds = obj.CollectionIds != null ? new HashSet(obj.CollectionIds) : null; LocalData = localData; Reprompt = obj.Reprompt; @@ -71,6 +72,8 @@ namespace Bit.Core.Models.Domain public bool Edit { get; set; } public bool ViewPassword { get; set; } public DateTime RevisionDate { get; set; } + public DateTime CreationDate { get; set; } + public DateTime? DeletedDate { get; set; } public Dictionary LocalData { get; set; } public Login Login { get; set; } public Identity Identity { get; set; } @@ -81,7 +84,6 @@ namespace Bit.Core.Models.Domain public List Fields { get; set; } public List PasswordHistory { get; set; } public HashSet CollectionIds { get; set; } - public DateTime? DeletedDate { get; set; } public CipherRepromptType Reprompt { get; set; } public async Task DecryptAsync() @@ -174,6 +176,7 @@ namespace Bit.Core.Models.Domain OrganizationUseTotp = OrganizationUseTotp, Favorite = Favorite, RevisionDate = RevisionDate, + CreationDate = CreationDate, Type = Type, CollectionIds = CollectionIds.ToList(), DeletedDate = DeletedDate, diff --git a/src/Core/Models/Request/CipherRequest.cs b/src/Core/Models/Request/CipherRequest.cs index 82c029751..ddb2dbc03 100644 --- a/src/Core/Models/Request/CipherRequest.cs +++ b/src/Core/Models/Request/CipherRequest.cs @@ -73,6 +73,21 @@ namespace Bit.Core.Models.Request Type = cipher.SecureNote.Type }; break; + case CipherType.Fido2Key: + Fido2Key = new Fido2KeyApi + { + NonDiscoverableId = cipher.Fido2Key.NonDiscoverableId?.EncryptedString, + KeyType = cipher.Fido2Key.KeyType?.EncryptedString, + KeyAlgorithm = cipher.Fido2Key.KeyAlgorithm?.EncryptedString, + KeyCurve = cipher.Fido2Key.KeyCurve?.EncryptedString, + KeyValue = cipher.Fido2Key.KeyValue?.EncryptedString, + RpId = cipher.Fido2Key.RpId?.EncryptedString, + RpName = cipher.Fido2Key.RpName?.EncryptedString, + UserHandle = cipher.Fido2Key.UserHandle?.EncryptedString, + UserName = cipher.Fido2Key.UserName?.EncryptedString, + Counter = cipher.Fido2Key.Counter?.EncryptedString + }; + break; default: break; } @@ -118,6 +133,7 @@ namespace Bit.Core.Models.Request public SecureNoteApi SecureNote { get; set; } public CardApi Card { get; set; } public IdentityApi Identity { get; set; } + public Fido2KeyApi Fido2Key { get; set; } public List Fields { get; set; } public List PasswordHistory { get; set; } public Dictionary Attachments { get; set; } diff --git a/src/Core/Models/Response/CipherResponse.cs b/src/Core/Models/Response/CipherResponse.cs index 4abfcf4c0..5247c4522 100644 --- a/src/Core/Models/Response/CipherResponse.cs +++ b/src/Core/Models/Response/CipherResponse.cs @@ -29,5 +29,6 @@ namespace Bit.Core.Models.Response public List CollectionIds { get; set; } public DateTime? DeletedDate { get; set; } public CipherRepromptType Reprompt { get; set; } + public DateTime CreationDate { get; set; } } } diff --git a/src/Core/Models/View/CipherView.cs b/src/Core/Models/View/CipherView.cs index c4f6455cf..a29ebb38c 100644 --- a/src/Core/Models/View/CipherView.cs +++ b/src/Core/Models/View/CipherView.cs @@ -6,7 +6,7 @@ using Bit.Core.Models.Domain; namespace Bit.Core.Models.View { - public class CipherView : View + public class CipherView : View, ILaunchableView { public CipherView() { } @@ -23,6 +23,7 @@ namespace Bit.Core.Models.View LocalData = c.LocalData; CollectionIds = c.CollectionIds; RevisionDate = c.RevisionDate; + CreationDate = c.CreationDate; DeletedDate = c.DeletedDate; Reprompt = c.Reprompt; } @@ -48,6 +49,7 @@ namespace Bit.Core.Models.View public List PasswordHistory { get; set; } public HashSet CollectionIds { get; set; } public DateTime RevisionDate { get; set; } + public DateTime CreationDate { get; set; } public DateTime? DeletedDate { get; set; } public CipherRepromptType Reprompt { get; set; } @@ -112,5 +114,11 @@ namespace Bit.Core.Models.View { return LinkedFieldOptions.Find(lfo => lfo.Value == id).Key; } + + public string ComparableName => Name + Login?.Username + Fido2Key?.UserName; + + public bool CanLaunch => Login?.CanLaunch == true || Fido2Key?.CanLaunch == true; + + public string LaunchUri => Login?.LaunchUri ?? Fido2Key?.LaunchUri; } } diff --git a/src/Core/Models/View/Fido2KeyView.cs b/src/Core/Models/View/Fido2KeyView.cs index 06d8e4cef..857f7f622 100644 --- a/src/Core/Models/View/Fido2KeyView.cs +++ b/src/Core/Models/View/Fido2KeyView.cs @@ -1,9 +1,10 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Bit.Core.Enums; namespace Bit.Core.Models.View { - public class Fido2KeyView : ItemView + public class Fido2KeyView : ItemView, ILaunchableView { public string NonDiscoverableId { get; set; } public string KeyType { get; set; } = Constants.DefaultFido2KeyType; @@ -17,7 +18,8 @@ namespace Bit.Core.Models.View public string Counter { get; set; } public override string SubTitle => UserName; - public override List> LinkedFieldOptions => new List>(); + public bool CanLaunch => !string.IsNullOrEmpty(RpId); + public string LaunchUri => $"https://{RpId}"; } } diff --git a/src/Core/Models/View/ILaunchableView.cs b/src/Core/Models/View/ILaunchableView.cs new file mode 100644 index 000000000..2156d3140 --- /dev/null +++ b/src/Core/Models/View/ILaunchableView.cs @@ -0,0 +1,8 @@ +namespace Bit.Core.Models.View +{ + public interface ILaunchableView + { + bool CanLaunch { get; } + string LaunchUri { get; } + } +} diff --git a/src/Core/Models/View/LoginUriView.cs b/src/Core/Models/View/LoginUriView.cs index 62ca2dd39..44874ab1c 100644 --- a/src/Core/Models/View/LoginUriView.cs +++ b/src/Core/Models/View/LoginUriView.cs @@ -7,7 +7,7 @@ using Bit.Core.Utilities; namespace Bit.Core.Models.View { - public class LoginUriView : View + public class LoginUriView : View, ILaunchableView { private HashSet _canLaunchWhitelist = new HashSet { diff --git a/src/Core/Services/CipherService.cs b/src/Core/Services/CipherService.cs index e289a1ae6..5eb344c2f 100644 --- a/src/Core/Services/CipherService.cs +++ b/src/Core/Services/CipherService.cs @@ -176,6 +176,7 @@ namespace Bit.Core.Services OrganizationId = model.OrganizationId, Type = model.Type, CollectionIds = model.CollectionIds, + CreationDate = model.CreationDate, RevisionDate = model.RevisionDate, Reprompt = model.Reprompt }; @@ -1147,6 +1148,22 @@ namespace Bit.Core.Services "LicenseNumber" }, key); break; + case CipherType.Fido2Key: + cipher.Fido2Key = new Fido2Key(); + await EncryptObjPropertyAsync(model.Fido2Key, cipher.Fido2Key, new HashSet + { + nameof(Fido2Key.NonDiscoverableId), + nameof(Fido2Key.KeyType), + nameof(Fido2Key.KeyAlgorithm), + nameof(Fido2Key.KeyCurve), + nameof(Fido2Key.KeyValue), + nameof(Fido2Key.RpId), + nameof(Fido2Key.RpName), + nameof(Fido2Key.UserHandle), + nameof(Fido2Key.UserName), + nameof(Fido2Key.Counter) + }, key); + break; default: throw new Exception("Unknown cipher type."); } @@ -1229,8 +1246,8 @@ namespace Bit.Core.Services public int Compare(CipherView a, CipherView b) { - var aName = a?.Name; - var bName = b?.Name; + var aName = a?.ComparableName; + var bName = b?.ComparableName; if (aName == null && bName != null) { return -1; @@ -1243,19 +1260,6 @@ namespace Bit.Core.Services { return 0; } - var result = _i18nService.StringComparer.Compare(aName, bName); - if (result != 0 || a.Type != CipherType.Login || b.Type != CipherType.Login) - { - return result; - } - if (a.Login.Username != null) - { - aName += a.Login.Username; - } - if (b.Login.Username != null) - { - bName += b.Login.Username; - } return _i18nService.StringComparer.Compare(aName, bName); } } diff --git a/src/Core/Utilities/CipherTypeExtensions.cs b/src/Core/Utilities/CipherTypeExtensions.cs new file mode 100644 index 000000000..b306fc4ce --- /dev/null +++ b/src/Core/Utilities/CipherTypeExtensions.cs @@ -0,0 +1,14 @@ +using Bit.Core.Enums; + +namespace Bit.Core.Utilities +{ + public static class CipherTypeExtensions + { + public static bool IsEqualToOrCanSignIn(this CipherType type, CipherType type2) + { + return type == type2 + || + (type == CipherType.Login && type2 == CipherType.Fido2Key); + } + } +} diff --git a/src/iOS.Autofill/Resources/bwi-font.ttf b/src/iOS.Autofill/Resources/bwi-font.ttf index 7c7afd4cdb9d110404b67ba3e8cd89d95151c138..3c54d7041250872f3bc87f4a7f6cce615a969185 100644 GIT binary patch delta 5446 zcmaJl3vgRicK5u$o}{PGdwS2Z99bVrwycjOSwE9RQS3xG1QL@Eq{In!;>5c)51)bcBXWip)znaljm?eOQ3UPYRC9Q*ikGH2>lZ0OH&Bo ze#$EZ!V-?VrgrZ;aAt%4TO6kdVV>J{-}dF>r_cZE3Ifr~NMvO9_<LXp*9(u@ zWE6|MKmQ_2*t>{>pwuAfxaU&QQ;3ToTD#VL9!=>_A{c(=_a!}F+Mc-~U5xeXO;ZbQxdSPq~?A`!Gm01bkDj|cl; zyZD>G;b3C~ZTeozraaoxG9s!w2uM{!bqON#4@kj06p5gIEehr#j|cl;L!*8@&x@De zoliGv^KeF5f-u3 zxfoE5be+%BpSMid2N{U6XiLOH6n3mx>#04|I=eehOyQrv6a}H6F!dy?qg(SMv=HK^J*xGrDett%@*~FRi zl*M_~*{nHL2+@K0q3UEZqMd)5k(NxI;4ZL8lu zldOu_T>;U}aln>JEO4A#3=F22H&cUyspfEMuvB7Lwk=eeh%iiKq7-UdBYRjj zEPGsbhwJ++!vu|R^Ht{duwgQ?hY*ay1M`2y{w1oPB_hVx0IJ;=ErzkFWlz}C?<@DK z9x*CLi&0NG%#ig^TDR`0<{fVx{I@G#zIE{6l`1>J9Q_g>bInt5srk~ugUpYbKl>6t zXMR9K44I$hh-KczNIN1bAfh{l^JuMdsrbx0gPje50^Zh5jd!En=~qsMyi zIW{^v&4rdNdaLO58pcSmxO8c;IARzMyFH|7*+8JOh@I`}eq!_JRP&2O;`o*=Q&U^E z98V;!{*$6)UJ2D>E&SHnMU_Axt7##--O<|JBW&Ys+};{;fRGplj11(^rGkK7l{2z} zh_NG%K>QOg6qi7;U~N2CvObG}h5|!3vew6!rQS+^R7$aoj)+U~h`K0U{309=C_Soq zeG&8*txf|$%uD?fg**2H*bl9&Pjg%E%rqi7v>?}{1zX3kco6ns*nalSk%-<5U_5AnQCWf^ca*F&Zc^{3QEfgz|YiTjs z@&FM59pIyC(i>Ejcn%R%1yoGyD3>w#p}O#-qU!LZ(?Dk#1`j?iD0(atVUMoBrFp&I zf>2w=2s8Nt7owqC@B3t@4tPd`}{vrxez_>}5pv|o@C5&&&Hw>I-zjCM3 zM>cNUdiTbSN75M=!vJ(Rc)I`q-h?l)+RM8H|72Q}1l{K|L{U(kPSvis`eU(zOOex) zeo^7Qs~ZWu$BSzOS#Wd!FicbO4TL-)w;GG>s8&}Bilk*!)h}@ZBY9!LX%le^IJIcF6LyCNd^WGd2%O|s)r=-7!pdrON32U#!XEe@1DkJK zvnL!1h4(bp+_o9nOD;)KY^=xv$Vyh>8P=YZ%(wNJ!;Wy-kQA?1arbI^N2lMY_+f>u z*TDeq&L}p9LoQAiQqUcmz}Kkbw2PA05g3lehdUHQ!p#bHKBUPE$8ZwYJ{XA%wM$Mx za7yisp-5z~9iccUYg$W#jASSs!|~W~z~Pm|_5eC)jTi9R;7EKUygnk$P7F1P?+@WfohKqsOwbfmJ_a-AH7;W$^uquc7T zbIF|*zWC7K;K2cY$z(De&!i_SmBouImC1A_o<0)nijE{xnM^7<673463&+;3Jyu9R z9vLkbM=Ctr8kwKNGgl++q)NugL!?0tqed`!z~hr+ z73;<UznMe9RRbeM^@0iXIZzM<(WZF7DOli9OlpLbG$=xs$7FTew^h5!Mq>{ z9DDL4%c)Mu!Jnrn=Xs|n$h`RpJBtx8Fi-I2`pgK=6CAOBbaj@UH7}c=ftxwkB5%&h zPSwc@49rsIJf-^%vcxm;3tm*6`2F$Atu3@D!z-^eIGI%OjbC8W>8@ZR9B)>wHUrR}7prW}~D|%cHK7!`!xFN*QSe5U; ziH07_TU)WHM6VbcWv;*1lOUS;#I9vTIDlv5KMpF<@Pa+9*!9e-yL}ECvn;bCk!RdL z3^d9)c($5KR;$TW6(E>deQPGz5t5ynEIWA)k0t{W$2(h@qxBV#zgSC&M> zsUe2~!9GM- z6bN4vT4c<>YYDMu0@ zB;rL)cO{FsV^365`$8cqbtv4C(Wuy#Py{d|I>egHVG_o40}b@XM6e+;#7KI~T@qft z497Ypn}!vkU6X7Mj>Ck`ZZp5-=`{|4+XdT1O|~0$*&_%dhnc5Gw(EAgDw)r^-0;82 zV-PTC!y2XK61;VnqFbQTl>wW@7aFH;WPJ6B8yUm*kTG)@Ih*GeIJ+^kxsfyMq@1nD zylegsF_oXRioF=gV?`5-TUCijYp1fFFULw&YHsi%lzIlmD8XYn5? z!{iQ(M&CxavMQrizQkgtRG`rW^C#*BRu0vQNm0=#(_awtpVo!Bkm!mLA=-*RV$2Fl zG~DAF6-yxuLRJi7#cuj5^VF3}Sc1_+xewS4dN8Q_Iy+Am2Q{sp=_?ldGIdQ`UdoJw zBp1JT@A(=&?}Tts;X#9}6$fzr$=~ zm!th?{Y!3)@U1d|-ma1OjeU6ecgU@}PFuCuB}uhH>BWAIDcE^OV{;@Ie%6WcN*`10AyIxKHI(77qJCUhZu zEIQIP9J?vLsrS#56RAz9_tK{_^}bB@mh9DBPi|B0C;6TI=L+${!^L%_6Xl-r&nu|` zVc?d5Pj8xD>|T6g$(f}y)ziynYg09I`CUVQvhqUx%5cZL;r{T0M2^gSTN!%|~m$9GTd#X~PG1ymn{)PP4J;gS&3od|>q6E!(#4+j?b882{&O-1hADhqwR7 z#HEQ}@7TNJ+|FZ@W0S8Z7cIL>z$8UOk`teIg z?|tIAW07MY9M_MB*Bw7~{G%t^o*X{GoV@Ae-jf$Bo4v>?#7iwin6t~ delta 2102 zcmZ`)Yitx%6u#%)*_qj$*`3+4-CbyDcXr!ZLLao--7U7IQY1W@7DNMR1lrOED{P@s zgyL&gQ?NuOJSq!@kd&aoN|aCxhG0??f+P@rC{Y83fIpgugdYS5Q9AX^ZYxA!X71d3 z?m73KbH4kXnY)*`-%fG~0)!BWBncyBi(j4*QkwZB&YZ#4+}hdFb+h>9WkMhqC&h*O*-~a(1zrOd+pK}tWP#KO^ z)SPBvSQP>sHiLfg|(MqU=Oddie+dw%buApKp8;}2~!PuMv3|eHK zmpG0Ci^W)`1hWSmc8JlsCm}GmF!q}uK%8U1TR2=Pb4I`2fyaV(k{L8f{3MzVIraTy z$V&vutqAORgpnEuY7X#VY@Q;g5w}W=VcK@l>awvdl1sGO8AlnDtj0R$bdT`@^jotf zDO-e35uk_LIbcaufhJ*+B)M%iC(jEU&+|^3Z)53vrP*l>68VyDhtL;L1yzG zZuDxJm)>y|IyOso&AX2Y+~{J{5Ee;_A{pTmroeK|_m8rw$`=HY(s-jJ4oYH!;_)aA zrYa>$qdP$pCF3HBL*M>$HtW>3sj!g>#Zb*g;MbIq52cCyT1koKD=jsa{D0ts>a@l; zu6V4+uQFk9cI-B-XBz%jJXm(-XE=aZr=dikpE{=X!Q7l_({gfy%*@}VoCZ*;ckvKC1a_!hGd7@9<^?{Fx5I+7I`sq|N#nfcPp;paYk?pjr{%BVh4 z&3U5-=Xi^ weE0BswaID8J0HCD!5_Vj-o9=3w}1EHp}t>sICjiU!+U@^NESI2D1;^SUkLq{T>t<8 diff --git a/src/iOS.Extension/Resources/bwi-font.ttf b/src/iOS.Extension/Resources/bwi-font.ttf index 51281cb413f61e636e654e6e89b5539be398e927..3c54d7041250872f3bc87f4a7f6cce615a969185 100644 GIT binary patch delta 4816 zcmaJ^eQ;A(cE9I+>OFl~dQb0JmXP&f$(ChFmaQ-Ip$G$x3142oK!YJR0ql7BFoXs+ zfk+7(0u(R_EKE~|xU=bo?XtTwW!q#(oAq|4ly*AlPMiF(bZ44QnRKR`>7={Ogmlq! z&wbA@w3FuPJ>7fHJ@@?XJ?GqW&;8{)+<$$*osZQ%BLsqkkdg7RXQ!}i!uAm?ZR2~s zwfo}dXRl(rPM~#YV%ONth@+4v5dJy#7blRw{Wd%s2k6(iE>r?UA%bYmTJw2S zp&g2LH|K&=Dn7=cqR$8CdMEd1;F8PhGIp!-0=61(D8|^jmZ~3SiphcdvjbhIt{Yo+ zt?AzWJvPPu4kGxeRuZTN0flNMD47dMAY4%g=5s_a4pRg;g9ye!6X6_8A_lyH824up z1Ky+<#wcR2Rm3nxZ#;D=U)(5>>!_dYRP!Zs;+FPZfN)VXLZV%gWOx%53~!nWHg=$b zjUA|&AIbr=NHmHT37|o+?(tw9q#M8aI}V1UXwwfZoAPK&(}<|<5TH~Q)g_3`KcWQV zSTu_IH6<9wJRYorghu^p%p12qm`m5HbF=I$TO}&#Am1Qc3E?AAQysmL9<3EZK$RbZ zm`~M^Qd~l7LJE*4PHQpbMq1QV=OREg(s4ddZ;xrhEMy?YqAigRS=iA=wWD0_U@}UZ z^L3}Y+)*<|>xvVIQ>jYgs3?KURqmK09pzD{qBuv(9e@L{&U3HxcG0Ccjag(x39}t# zH;QOe8voEyW~(!leP&9OBynnnLd+=6M%Af6m^O@$6(^Gs?fmN;-yjgC%*MyKfyj+U zW3gJ}NZ;Jwuz$_gaaW1Bu>TX3`B_3HXip|E60 zp)jQD4!b?9s@;J=c>!DRXg{@eWTNp!GI?g(wuy;t+s-7D_y5V|%3Kc*#4Ubn?1FM2 z(5D^}G>qfs2>5nc_ETQ#rD7uK7%Ec1t=pfWpdb9=5TQDm%Br)Ib-I?Ef7{GpP z#lR%DeRKEW6{|Cuyd*QA>qT7;#N)%+?i`!z&TeXp2TU5raT2d-E4otMyL~>bFP5yR zffnDAWLuxl=bMbh5{b$7L@YLWNRU`gw>8;@+Ze{bG0roxqI7p3U%PgFYT#H9VE zB}4?FZ_O-l@WU5bPy+5-Is|QQwy9uzE#J_wpZ&5aojx%7pnoPN!mbxqIXByxS$G$Ni#<_pYiXwGJoC0<& z7;J^T;Xa?_wJ`!`^D9b5wYh{9mCCMoo8pRi;5!U#U9QF0a?s)TwI0R=-~M!*Z$9!2sWsaY+n^C{8y@&>X73 zSE=K)h&HbyFc?n^23@)hCo9i92Rn^iU zW7A#1!9;v8;PBeSmH;|vl@~B?|`&+Q}0bwRfU}Wx$NKXfJAPpKckxw+wwi zJ;U0fxso{Vg7frwPt4VHbRtSaM=FVy>*Od6+qn{MU8~B@rgoS4!VCTVNBj82b=D)h z(7o$ylbz+6eohudCx3;&1lx2o@ih|4= zzhdhc0RwX+q8p#-kvW1b_7CpY*}8Gt_zaqutCo19E;|(`D=<)};5?Q4Q-s7l@-tpk zoLK(w2i6oU$#DI8jgxtom*-*DWW??M`SOmDSfQIQ@oG|D`D!8{_U zifP{QNCPg&-wnB9k$HQXvFpN*_xc<(W|?M3As3py7pRqT@J6L8RjH)9DgdF(s@0iL zFf2P&S$6UqZcPSk9PgB6)hUOAq0H)4U)OJ3J215Fz=3r`2VhG;RRi=hR%Vl(6)!%O z&SaJ;KiX>4ZXb!{H>}S`!dXR)HFhtUcc@xh!U0bJ9nY$)*6YpTiNKq!k03j&b`ixwH<@2y1~82T$U7*v&D z5Z2-JlbzYd?QCb#S`4;uF=lK3|CvuDOlKT6*W~eNHW3eMIxAVk8GB-?k`0IHs>6|B zMy1O(7exRQqGPPV948S>H_$-u>yQ)T6-g9Ry~MBeHRI9=QoKn@5phH$^rh^PaiiXD_nj=4B&JdN8AoC(Nu^ zq`7-Z>nX*HWk;vizzb%U-5!VA##?=<0p{I-RG;ydzv7Qesstm7 zTW<{NZUshG!3g#@2C*MT{1pdw8*gDZ^DcH9Z;gMMeC3tomtJpP5WaMEb-BJ|-5(3^RtSLadd9kzR2aNW$*DRP}e)p^IP81|ThS^^*b?br` z*NS*ov=jikd7Giysc9no``2Exe($TUe^llCeE6>uI~gRKFdBUe-RfZ&QZ*Jcr96!$ zm_Jc3Fmos?CdEX(M1LX7f2;#@KG7AULd=RkV%!W%G~DAD#R@`Z3}VH0`YZ9&m5Lhm zF(Hu!cB2*wX};Fh=|aD%4rH>0LN+s?s!NNRp|H)(A3Qiy#pkEtU5jyBEx~&lC`#?I z&QzAS^O!lgLhen*KMP(*=XhUiS*yR(eHJ6l)D%XX@mpITtM*sZ@SlqIcxqq=z_x+Z zN?qAy;`y`?EQ=moZoyg5%MV7qr8e0k?wv-=3Z(OhhFd(D%_KV(QEV2|7MGAc528m7 z$K6g>q8TPA9Da@Suo(f%U9cdM1Q_XGItGvkAU`grU_LO_^=fuuYQ;?6p z@kQH%^71tw?y$Enf9!)ANS8-7c$}Az?W4=rHTac;)w$0(ybN8Q(cxLBURIkSmF=T) z*!O6PLsz0>e$=zO_i8?of3dK>c&^k@dcWM&C-gnh_vymPMNNy&Exxp5 zs&etMdUc{|EPZm>pRBkxaCa~`__r&MuKdx;PgYH=etr$N=G$xYYd=|cZ)oSnEgSD_ z`pM>=%|>m@ohP5zdSqnZwjJ9KZNEDzjQw&4_jLEuFFySrJ8$j$^{#`vuI@fPK05xB ziP2{g&wM(0`kU> zzi{{Hm1Du0Q2Y5yTV9%d`P1XOzy0AWmrfjgb>XX%uiiSj@6?sk(bIR%XlEkp&zwK= o$=T+!gXfs(h0_P8uVD-%VR)=!Xi(*HsUtqKMXd zXM+@*##o@7wT2X98>L@H+Gq$({iCraQvYb1Hc``}f0(p2{e!46tt@@ZK{c_?&CSf4 z_vX!ezxlnrbA`KpjyvLu{7eWW6GApL*R{3ctfJh3v)CNo(=_=@4z3|ez`=zY>l#u^ zZzd3McA`EQ!VPZJ1SpT9ydc!Fvt#r7j&YR738A6z_WHW^;Xn@o_ZsvOY^m#LC7&^s zz}zU>r){fiY4ja&`E}g*5Dz4`Zg1O37^)y?^xgBl(9>HENpN~N1YZ=n)21Ty28)tX zR6BS9Y#?CwYQ4Yzm=wu|0)Mce7~(xK&y|=0iI4)$TzW&X>PM-GQGqcYJEa^23cx6Y zctPOxK8j0h6gWoFB5r4*5Vk51acZhw%{b!(Hb&9Yv>U?{3<-9#Ie~|iPmF^+<8U-l z$_!JygkyS@syWH<)?zh10%VX)(HasUB;BiJKsID#LqK~Nc7AX+?EJi70Te?#4qXq~N<{0oM4o}LQ z-e_VrvyiTHBO2<%LRfg;H8*dP&OOU$zCVY9>-rfn6?4h!2Y zIYhISag;I1tZ%hvChAW^pV=u%P7&TifF5*lz@n-GHNqT8a#<{Po)~afGQ#!~A1Nfngn-8JWV1Y&Wx#)2VVNMu`-dtCJ~!HV7x9sbUEf_Br36X zBT<=qB++Rwcr(WsyAV(Fk;DPW@U=dFuvR3e*nL_{<|3t$vIr|uIs-JX0KArg%)n6d zjaOG;$U@Cxl0~mp(vbp|B+*PQFl5&pwAP0FHqVpL079~?_n`*$5VqNd>o0TQPMwO< zadu$3a1b2yX;ev`t9RoMNfEpf0gBfW-W`V#sTs(HRK=jprZmWO#IJYH>uHin6O%`VO*&G?6w!=GYysd{165VE1cFFj}9- z%R5b2?sf;C1zyeApWb^Y2bUwn^C5n21^9#cd9GAI-mE|_Pb<(MXeee3d7p>b(ad9y zIB@ONG>@CDbbC~#)0&nr+3fbPbx3Sc6J%AMyo!UMiBepa;JU1eJbnxIOnxLvw?xUM zYN|^TorZ-TwaWrrlLdXLAhXpEcDOwrH=S^#+u9|o$KB5aZgQPr2x}!pk#zrALtx3q z_?@h(@?}A!G+ZHxLy}moBql25hAJjXlZU}0O8OXz!(aVV;y8D3KD@w^<@GOrP@@#x zB@%}pS3-UZf8RSh$L^$INgRtU@<0UAVxM|owF%IS#_oIIPqWC8skSA*drJR$X4R^q)g5cj zJiDn(Eo&`1Ri0T_bs?lL|!`nioBt4W6q`pHNl#X zYcFjc*nF{mqM@YWOk;QBNK;|c$>t-WrqIZ%O+IT-5T@wuWu`_N%SqukGDY z)JEFA-PyHk$F8%x-MjC1^z9jYy>=hn_s1J2_7_JNMkn6f`{vC~TW9yddtK+>8tC3| VsPE9&+Z1sF{2ycIQq!T5{tI_ZaMAz( diff --git a/src/iOS/Resources/bwi-font.ttf b/src/iOS/Resources/bwi-font.ttf index 7c7afd4cdb9d110404b67ba3e8cd89d95151c138..3c54d7041250872f3bc87f4a7f6cce615a969185 100644 GIT binary patch delta 5446 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 delta 2102 zcmZ`)Yitx%6u#%)*_qj$*`3+4-CbyDcXr!ZLLao--7U7IQY1W@7DNMR1lrOED{P@s zgyL&gQ?NuOJSq!@kd&aoN|aCxhG0??f+P@rC{Y83fIpgugdYS5Q9AX^ZYxA!X71d3 z?m73KbH4kXnY)*`-%fG~0)!BWBncyBi(j4*QkwZB&YZ#4+}hdFb+h>9WkMhqC&h*O*-~a(1zrOd+pK}tWP#KO^ z)SPBvSQP>sHiLfg|(MqU=Oddie+dw%buApKp8;}2~!PuMv3|eHK zmpG0Ci^W)`1hWSmc8JlsCm}GmF!q}uK%8U1TR2=Pb4I`2fyaV(k{L8f{3MzVIraTy z$V&vutqAORgpnEuY7X#VY@Q;g5w}W=VcK@l>awvdl1sGO8AlnDtj0R$bdT`@^jotf zDO-e35uk_LIbcaufhJ*+B)M%iC(jEU&+|^3Z)53vrP*l>68VyDhtL;L1yzG zZuDxJm)>y|IyOso&AX2Y+~{J{5Ee;_A{pTmroeK|_m8rw$`=HY(s-jJ4oYH!;_)aA zrYa>$qdP$pCF3HBL*M>$HtW>3sj!g>#Zb*g;MbIq52cCyT1koKD=jsa{D0ts>a@l; zu6V4+uQFk9cI-B-XBz%jJXm(-XE=aZr=dikpE{=X!Q7l_({gfy%*@}VoCZ*;ckvKC1a_!hGd7@9<^?{Fx5I+7I`sq|N#nfcPp;paYk?pjr{%BVh4 z&3U5-=Xi^ weE0BswaID8J0HCD!5_Vj-o9=3w}1EHp}t>sICjiU!+U@^NESI2D1;^SUkLq{T>t<8