using Bit.App.Abstractions; using Bit.App.Models; using Bit.App.Resources; using Bit.Core.Abstractions; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.View; using Bit.Core.Utilities; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Xamarin.Forms; namespace Bit.App.Pages { public class AddEditPageViewModel : BaseViewModel { private readonly IDeviceActionService _deviceActionService; private readonly ICipherService _cipherService; private readonly IFolderService _folderService; private readonly ICollectionService _collectionService; private readonly IUserService _userService; private readonly IPlatformUtilsService _platformUtilsService; private readonly IAuditService _auditService; private readonly IMessagingService _messagingService; private CipherView _cipher; private bool _showPassword; private bool _showCardCode; private int _typeSelectedIndex; private int _cardBrandSelectedIndex; private int _cardExpMonthSelectedIndex; private int _identityTitleSelectedIndex; private int _folderSelectedIndex; private int _ownershipSelectedIndex; private bool _hasCollections; private List _writeableCollections; private string[] _additionalCipherProperties = new string[] { nameof(IsLogin), nameof(IsIdentity), nameof(IsCard), nameof(IsSecureNote), nameof(ShowUris), nameof(ShowAttachments), nameof(ShowCollections), }; private List> _matchDetectionOptions = new List> { new KeyValuePair(null, AppResources.Default), new KeyValuePair(UriMatchType.Domain, AppResources.BaseDomain), new KeyValuePair(UriMatchType.Host, AppResources.Host), new KeyValuePair(UriMatchType.StartsWith, AppResources.StartsWith), new KeyValuePair(UriMatchType.RegularExpression, AppResources.RegEx), new KeyValuePair(UriMatchType.Exact, AppResources.Exact), new KeyValuePair(UriMatchType.Never, AppResources.Never) }; private List> _fieldTypeOptions = new List> { new KeyValuePair(FieldType.Text, AppResources.FieldTypeText), new KeyValuePair(FieldType.Hidden, AppResources.FieldTypeHidden), new KeyValuePair(FieldType.Boolean, AppResources.FieldTypeBoolean) }; public AddEditPageViewModel() { _deviceActionService = ServiceContainer.Resolve("deviceActionService"); _cipherService = ServiceContainer.Resolve("cipherService"); _folderService = ServiceContainer.Resolve("folderService"); _userService = ServiceContainer.Resolve("userService"); _platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); _auditService = ServiceContainer.Resolve("auditService"); _messagingService = ServiceContainer.Resolve("messagingService"); _collectionService = ServiceContainer.Resolve("collectionService"); GeneratePasswordCommand = new Command(GeneratePassword); TogglePasswordCommand = new Command(TogglePassword); ToggleCardCodeCommand = new Command(ToggleCardCode); CheckPasswordCommand = new Command(CheckPasswordAsync); UriOptionsCommand = new Command(UriOptions); FieldOptionsCommand = new Command(FieldOptions); Uris = new ExtendedObservableCollection(); Fields = new ExtendedObservableCollection(); Collections = new ExtendedObservableCollection(); TypeOptions = new List> { new KeyValuePair(AppResources.TypeLogin, CipherType.Login), new KeyValuePair(AppResources.TypeCard, CipherType.Card), new KeyValuePair(AppResources.TypeIdentity, CipherType.Identity), new KeyValuePair(AppResources.TypeSecureNote, CipherType.SecureNote), }; CardBrandOptions = new List> { new KeyValuePair($"-- {AppResources.Select} --", null), new KeyValuePair("Visa", "Visa"), new KeyValuePair("Mastercard", "Mastercard"), new KeyValuePair("American Express", "Amex"), new KeyValuePair("Discover", "Discover"), new KeyValuePair("Diners Club", "Diners Club"), new KeyValuePair("JCB", "JCB"), new KeyValuePair("Maestro", "Maestro"), new KeyValuePair("UnionPay", "UnionPay"), new KeyValuePair(AppResources.Other, "Other") }; CardExpMonthOptions = new List> { new KeyValuePair($"-- {AppResources.Select} --", null), new KeyValuePair($"01 - {AppResources.January}", "1"), new KeyValuePair($"02 - {AppResources.February}", "2"), new KeyValuePair($"03 - {AppResources.March}", "3"), new KeyValuePair($"04 - {AppResources.April}", "4"), new KeyValuePair($"05 - {AppResources.May}", "5"), new KeyValuePair($"06 - {AppResources.June}", "6"), new KeyValuePair($"07 - {AppResources.July}", "7"), new KeyValuePair($"08 - {AppResources.August}", "8"), new KeyValuePair($"09 - {AppResources.September}", "9"), new KeyValuePair($"10 - {AppResources.October}", "10"), new KeyValuePair($"11 - {AppResources.November}", "11"), new KeyValuePair($"12 - {AppResources.December}", "12") }; IdentityTitleOptions = new List> { new KeyValuePair($"-- {AppResources.Select} --", null), new KeyValuePair(AppResources.Mr, AppResources.Mr), new KeyValuePair(AppResources.Mrs, AppResources.Mrs), new KeyValuePair(AppResources.Ms, AppResources.Ms), new KeyValuePair(AppResources.Dr, AppResources.Dr), }; FolderOptions = new List>(); OwnershipOptions = new List>(); } public Command GeneratePasswordCommand { get; set; } public Command TogglePasswordCommand { get; set; } public Command ToggleCardCodeCommand { get; set; } public Command CheckPasswordCommand { get; set; } public Command UriOptionsCommand { get; set; } public Command FieldOptionsCommand { get; set; } public string CipherId { get; set; } public string OrganizationId { get; set; } public string FolderId { get; set; } public CipherType? Type { get; set; } public HashSet CollectionIds { get; set; } public string DefaultName { get; set; } public string DefaultUri { get; set; } public List> TypeOptions { get; set; } public List> CardBrandOptions { get; set; } public List> CardExpMonthOptions { get; set; } public List> IdentityTitleOptions { get; set; } public List> FolderOptions { get; set; } public List> OwnershipOptions { get; set; } public ExtendedObservableCollection Uris { get; set; } public ExtendedObservableCollection Fields { get; set; } public ExtendedObservableCollection Collections { get; set; } public int TypeSelectedIndex { get => _typeSelectedIndex; set { if(SetProperty(ref _typeSelectedIndex, value)) { TypeChanged(); } } } public int CardBrandSelectedIndex { get => _cardBrandSelectedIndex; set { if(SetProperty(ref _cardBrandSelectedIndex, value)) { CardBrandChanged(); } } } public int CardExpMonthSelectedIndex { get => _cardExpMonthSelectedIndex; set { if(SetProperty(ref _cardExpMonthSelectedIndex, value)) { CardExpMonthChanged(); } } } public int IdentityTitleSelectedIndex { get => _identityTitleSelectedIndex; set { if(SetProperty(ref _identityTitleSelectedIndex, value)) { IdentityTitleChanged(); } } } public int FolderSelectedIndex { get => _folderSelectedIndex; set { if(SetProperty(ref _folderSelectedIndex, value)) { FolderChanged(); } } } public int OwnershipSelectedIndex { get => _ownershipSelectedIndex; set { if(SetProperty(ref _ownershipSelectedIndex, value)) { OrganizationChanged(); } } } public CipherView Cipher { get => _cipher; set => SetProperty(ref _cipher, value, additionalPropertyNames: _additionalCipherProperties); } public bool ShowPassword { get => _showPassword; set => SetProperty(ref _showPassword, value, additionalPropertyNames: new string[] { nameof(ShowPasswordIcon) }); } public bool ShowCardCode { get => _showCardCode; set => SetProperty(ref _showCardCode, value, additionalPropertyNames: new string[] { nameof(ShowCardCodeIcon) }); } public bool HasCollections { get => _hasCollections; set => SetProperty(ref _hasCollections, value, additionalPropertyNames: new string[] { nameof(ShowCollections) }); } public bool ShowCollections => !EditMode && Cipher.OrganizationId != null; public bool EditMode => !string.IsNullOrWhiteSpace(CipherId); public bool IsLogin => Cipher?.Type == CipherType.Login; public bool IsIdentity => Cipher?.Type == CipherType.Identity; public bool IsCard => Cipher?.Type == CipherType.Card; public bool IsSecureNote => Cipher?.Type == CipherType.SecureNote; public bool ShowUris => IsLogin && Cipher.Login.HasUris; public bool ShowAttachments => Cipher.HasAttachments; public string ShowPasswordIcon => ShowPassword ? "" : ""; public string ShowCardCodeIcon => ShowCardCode ? "" : ""; public void Init() { PageTitle = EditMode ? AppResources.EditItem : AppResources.AddItem; } public async Task LoadAsync(AppOptions appOptions = null) { var myEmail = await _userService.GetEmailAsync(); OwnershipOptions.Add(new KeyValuePair(myEmail, null)); var orgs = await _userService.GetAllOrganizationAsync(); foreach(var org in orgs.OrderBy(o => o.Name)) { if(org.Enabled && org.Status == OrganizationUserStatusType.Confirmed) { OwnershipOptions.Add(new KeyValuePair(org.Name, org.Id)); } } var allCollections = await _collectionService.GetAllDecryptedAsync(); _writeableCollections = allCollections.Where(c => !c.ReadOnly).ToList(); if(CollectionIds?.Any() ?? false) { var colId = CollectionIds.First(); var collection = _writeableCollections.FirstOrDefault(c => c.Id == colId); OrganizationId = collection?.OrganizationId; } var folders = await _folderService.GetAllDecryptedAsync(); FolderOptions = folders.Select(f => new KeyValuePair(f.Name, f.Id)).ToList(); if(Cipher == null) { if(EditMode) { var cipher = await _cipherService.GetAsync(CipherId); if(cipher == null) { return false; } Cipher = await cipher.DecryptAsync(); } else { Cipher = new CipherView { Name = DefaultName, OrganizationId = OrganizationId, FolderId = FolderId, Type = Type.GetValueOrDefault(CipherType.Login), Login = new LoginView(), Card = new CardView(), Identity = new IdentityView(), SecureNote = new SecureNoteView() }; Cipher.Login.Uris = new List { new LoginUriView { Uri = DefaultUri } }; Cipher.SecureNote.Type = SecureNoteType.Generic; if(appOptions != null) { Cipher.Type = appOptions.SaveType.GetValueOrDefault(Cipher.Type); Cipher.Login.Username = appOptions.SaveUsername; Cipher.Login.Password = appOptions.SavePassword; Cipher.Card.Code = appOptions.SaveCardCode; if(int.TryParse(appOptions.SaveCardExpMonth, out int month) && month <= 12 && month >= 1) { Cipher.Card.ExpMonth = month.ToString(); } Cipher.Card.ExpYear = appOptions.SaveCardExpYear; Cipher.Card.CardholderName = appOptions.SaveCardName; Cipher.Card.Number = appOptions.SaveCardNumber; } } TypeSelectedIndex = TypeOptions.FindIndex(k => k.Value == Cipher.Type); FolderSelectedIndex = string.IsNullOrWhiteSpace(Cipher.FolderId) ? FolderOptions.Count - 1 : FolderOptions.FindIndex(k => k.Value == Cipher.FolderId); CardBrandSelectedIndex = string.IsNullOrWhiteSpace(Cipher.Card?.Brand) ? 0 : CardBrandOptions.FindIndex(k => k.Value == Cipher.Card.Brand); CardExpMonthSelectedIndex = string.IsNullOrWhiteSpace(Cipher.Card?.ExpMonth) ? 0 : CardExpMonthOptions.FindIndex(k => k.Value == Cipher.Card.ExpMonth); IdentityTitleSelectedIndex = string.IsNullOrWhiteSpace(Cipher.Identity?.Title) ? 0 : IdentityTitleOptions.FindIndex(k => k.Value == Cipher.Identity.Title); OwnershipSelectedIndex = string.IsNullOrWhiteSpace(Cipher.OrganizationId) ? 0 : OwnershipOptions.FindIndex(k => k.Value == Cipher.OrganizationId); if(!EditMode && (CollectionIds?.Any() ?? false)) { foreach(var col in Collections) { col.Checked = CollectionIds.Contains(col.Collection.Id); } } if(Cipher.Login?.Uris != null) { Uris.ResetWithRange(Cipher.Login.Uris); } if(Cipher.Fields != null) { Fields.ResetWithRange(Cipher.Fields?.Select(f => new AddEditPageFieldViewModel(f))); } } return true; } public async Task SubmitAsync() { if(Cipher == null) { return false; } if(Xamarin.Essentials.Connectivity.NetworkAccess == Xamarin.Essentials.NetworkAccess.None) { await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage, AppResources.InternetConnectionRequiredTitle); return false; } if(string.IsNullOrWhiteSpace(Cipher.Name)) { await Page.DisplayAlert(AppResources.AnErrorHasOccurred, string.Format(AppResources.ValidationFieldRequired, AppResources.Name), AppResources.Ok); return false; } Cipher.Fields = Fields != null && Fields.Any() ? Fields.Select(f => f.Field).ToList() : null; if(Cipher.Login != null) { Cipher.Login.Uris = Uris?.ToList(); if(!EditMode && Cipher.Type == CipherType.Login && (Cipher.Login.Uris?.Count ?? 0) == 1 && string.IsNullOrWhiteSpace(Cipher.Login.Uris.First().Uri)) { Cipher.Login.Uris = null; } } if(!EditMode && Cipher.OrganizationId != null) { if(!Collections?.Any(c => c.Checked) ?? true) { await Page.DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.SelectOneCollection, AppResources.Ok); return false; } Cipher.CollectionIds = Collections.Any() ? new HashSet(Collections.Where(c => c.Checked).Select(c => c.Collection.Id)) : null; } var cipher = await _cipherService.EncryptAsync(Cipher); if(cipher == null) { return false; } try { await _deviceActionService.ShowLoadingAsync(AppResources.Saving); await _cipherService.SaveWithServerAsync(cipher); Cipher.Id = cipher.Id; await _deviceActionService.HideLoadingAsync(); _platformUtilsService.ShowToast("success", null, EditMode ? AppResources.ItemUpdated : AppResources.NewItemCreated); _messagingService.Send(EditMode ? "editedCipher" : "addedCipher"); if((Page as AddEditPage).FromAutofillFramework) { // Close and go back to app _deviceActionService.CloseAutofill(); } else { await Page.Navigation.PopModalAsync(); } return true; } catch(ApiException e) { await _deviceActionService.HideLoadingAsync(); await Page.DisplayAlert(AppResources.AnErrorHasOccurred, e.Error.GetSingleMessage(), AppResources.Ok); } return false; } public async Task DeleteAsync() { if(Xamarin.Essentials.Connectivity.NetworkAccess == Xamarin.Essentials.NetworkAccess.None) { await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage, AppResources.InternetConnectionRequiredTitle); return false; } var confirmed = await _platformUtilsService.ShowDialogAsync(AppResources.DoYouReallyWantToDelete, null, AppResources.Yes, AppResources.Cancel); if(!confirmed) { return false; } try { await _deviceActionService.ShowLoadingAsync(AppResources.Deleting); await _cipherService.DeleteWithServerAsync(Cipher.Id); await _deviceActionService.HideLoadingAsync(); _platformUtilsService.ShowToast("success", null, AppResources.ItemDeleted); _messagingService.Send("deletedCipher"); return true; } catch(ApiException e) { await _deviceActionService.HideLoadingAsync(); await Page.DisplayAlert(AppResources.AnErrorHasOccurred, e.Error.GetSingleMessage(), AppResources.Ok); } return false; } public async void GeneratePassword() { if(!string.IsNullOrWhiteSpace(Cipher?.Login?.Password)) { var confirmed = await _platformUtilsService.ShowDialogAsync(AppResources.PasswordOverrideAlert, null, AppResources.Yes, AppResources.No); if(!confirmed) { return; } } var page = new GeneratorPage(false, async (password) => { Cipher.Login.Password = password; TriggerCipherChanged(); await Page.Navigation.PopModalAsync(); }); await Page.Navigation.PushModalAsync(new NavigationPage(page)); } public async void UriOptions(LoginUriView uri) { if(!(Page as AddEditPage).DoOnce()) { return; } var selection = await Page.DisplayActionSheet(AppResources.Options, AppResources.Cancel, null, AppResources.MatchDetection, AppResources.Remove); if(selection == AppResources.Remove) { Uris.Remove(uri); } else if(selection == AppResources.MatchDetection) { var options = _matchDetectionOptions.Select(o => o.Key == uri.Match ? $"✓ {o.Value}" : o.Value); var matchSelection = await Page.DisplayActionSheet(AppResources.URIMatchDetection, AppResources.Cancel, null, options.ToArray()); if(matchSelection != null && matchSelection != AppResources.Cancel) { var matchSelectionClean = matchSelection.Replace("✓ ", string.Empty); uri.Match = _matchDetectionOptions.FirstOrDefault(o => o.Value == matchSelectionClean).Key; } } } public void AddUri() { if(Cipher.Type != CipherType.Login) { return; } if(Uris == null) { Uris = new ExtendedObservableCollection(); } Uris.Add(new LoginUriView()); } public async void FieldOptions(AddEditPageFieldViewModel field) { if(!(Page as AddEditPage).DoOnce()) { return; } var selection = await Page.DisplayActionSheet(AppResources.Options, AppResources.Cancel, null, AppResources.Edit, AppResources.MoveUp, AppResources.MoveDown, AppResources.Remove); if(selection == AppResources.Remove) { Fields.Remove(field); } else if(selection == AppResources.Edit) { var name = await _deviceActionService.DisplayPromptAync(AppResources.CustomFieldName, null, field.Field.Name); field.Field.Name = name ?? field.Field.Name; field.TriggerFieldChanged(); } else if(selection == AppResources.MoveUp) { var currentIndex = Fields.IndexOf(field); if(currentIndex > 0) { Fields.Move(currentIndex, currentIndex - 1); } } else if(selection == AppResources.MoveDown) { var currentIndex = Fields.IndexOf(field); if(currentIndex < Fields.Count - 1) { Fields.Move(currentIndex, currentIndex + 1); } } } public async void AddField() { var typeSelection = await Page.DisplayActionSheet(AppResources.SelectTypeField, AppResources.Cancel, null, _fieldTypeOptions.Select(f => f.Value).ToArray()); if(typeSelection != null && typeSelection != AppResources.Cancel) { var name = await _deviceActionService.DisplayPromptAync(AppResources.CustomFieldName); if(name == null) { return; } if(Fields == null) { Fields = new ExtendedObservableCollection(); } var type = _fieldTypeOptions.FirstOrDefault(f => f.Value == typeSelection).Key; Fields.Add(new AddEditPageFieldViewModel(new FieldView { Type = type, Name = string.IsNullOrWhiteSpace(name) ? null : name })); } } public void TogglePassword() { ShowPassword = !ShowPassword; } public void ToggleCardCode() { ShowCardCode = !ShowCardCode; } public async Task UpdateTotpKeyAsync(string key) { if(Cipher?.Login != null) { if(!string.IsNullOrWhiteSpace(key)) { Cipher.Login.Totp = key; TriggerCipherChanged(); _platformUtilsService.ShowToast("info", null, AppResources.AuthenticatorKeyAdded); } else { await _platformUtilsService.ShowDialogAsync(AppResources.AuthenticatorKeyReadError); } } } private void TypeChanged() { if(Cipher != null && TypeSelectedIndex > -1) { Cipher.Type = TypeOptions[TypeSelectedIndex].Value; TriggerCipherChanged(); } } private void CardBrandChanged() { if(Cipher?.Card != null && CardBrandSelectedIndex > -1) { Cipher.Card.Brand = CardBrandOptions[CardBrandSelectedIndex].Value; } } private void CardExpMonthChanged() { if(Cipher?.Card != null && CardExpMonthSelectedIndex > -1) { Cipher.Card.ExpMonth = CardExpMonthOptions[CardExpMonthSelectedIndex].Value; } } private void IdentityTitleChanged() { if(Cipher?.Identity != null && IdentityTitleSelectedIndex > -1) { Cipher.Identity.Title = IdentityTitleOptions[IdentityTitleSelectedIndex].Value; } } private void FolderChanged() { if(Cipher != null && FolderSelectedIndex > -1) { Cipher.FolderId = FolderOptions[FolderSelectedIndex].Value; } } private void OrganizationChanged() { if(Cipher != null && OwnershipSelectedIndex > -1) { Cipher.OrganizationId = OwnershipOptions[OwnershipSelectedIndex].Value; TriggerCipherChanged(); } if(Cipher.OrganizationId != null) { var cols = _writeableCollections.Where(c => c.OrganizationId == Cipher.OrganizationId) .Select(c => new CollectionViewModel { Collection = c }).ToList(); Collections.ResetWithRange(cols); } else { Collections.ResetWithRange(new List()); } HasCollections = Collections.Any(); } private void TriggerCipherChanged() { TriggerPropertyChanged(nameof(Cipher), _additionalCipherProperties); } private async void CheckPasswordAsync() { if(!(Page as BaseContentPage).DoOnce()) { return; } if(string.IsNullOrWhiteSpace(Cipher.Login?.Password)) { return; } await _deviceActionService.ShowLoadingAsync(AppResources.CheckingPassword); var matches = await _auditService.PasswordLeakedAsync(Cipher.Login.Password); await _deviceActionService.HideLoadingAsync(); if(matches > 0) { await _platformUtilsService.ShowDialogAsync(string.Format(AppResources.PasswordExposed, matches.ToString("N0"))); } else { await _platformUtilsService.ShowDialogAsync(AppResources.PasswordSafe); } } } public class AddEditPageFieldViewModel : ExtendedViewModel { private FieldView _field; private bool _showHiddenValue; private bool _booleanValue; private string[] _additionalFieldProperties = new string[] { nameof(IsBooleanType), nameof(IsHiddenType), nameof(IsTextType), }; public AddEditPageFieldViewModel(FieldView field) { Field = field; ToggleHiddenValueCommand = new Command(ToggleHiddenValue); BooleanValue = IsBooleanType && field.Value == "true"; } public FieldView Field { get => _field; set => SetProperty(ref _field, value, additionalPropertyNames: _additionalFieldProperties); } public bool ShowHiddenValue { get => _showHiddenValue; set => SetProperty(ref _showHiddenValue, value, additionalPropertyNames: new string[] { nameof(ShowHiddenValueIcon) }); } public bool BooleanValue { get => _booleanValue; set { SetProperty(ref _booleanValue, value); if(IsBooleanType) { Field.Value = value ? "true" : "false"; } } } public Command ToggleHiddenValueCommand { get; set; } public string ShowHiddenValueIcon => _showHiddenValue ? "" : ""; public bool IsTextType => _field.Type == FieldType.Text; public bool IsBooleanType => _field.Type == FieldType.Boolean; public bool IsHiddenType => _field.Type == FieldType.Hidden; public void ToggleHiddenValue() { ShowHiddenValue = !ShowHiddenValue; } public void TriggerFieldChanged() { TriggerPropertyChanged(nameof(Field), _additionalFieldProperties); } } }