diff --git a/src/Android/Assets/bwi-font.ttf b/src/Android/Assets/bwi-font.ttf index 7c7afd4cd..3c54d7041 100644 Binary files a/src/Android/Assets/bwi-font.ttf and b/src/Android/Assets/bwi-font.ttf differ 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 7c7afd4cd..3c54d7041 100644 Binary files a/src/iOS.Autofill/Resources/bwi-font.ttf and b/src/iOS.Autofill/Resources/bwi-font.ttf differ diff --git a/src/iOS.Extension/Resources/bwi-font.ttf b/src/iOS.Extension/Resources/bwi-font.ttf index 51281cb41..3c54d7041 100644 Binary files a/src/iOS.Extension/Resources/bwi-font.ttf and b/src/iOS.Extension/Resources/bwi-font.ttf differ diff --git a/src/iOS/Resources/bwi-font.ttf b/src/iOS/Resources/bwi-font.ttf index 7c7afd4cd..3c54d7041 100644 Binary files a/src/iOS/Resources/bwi-font.ttf and b/src/iOS/Resources/bwi-font.ttf differ