mirror of
https://github.com/bitwarden/mobile
synced 2025-12-05 23:53:33 +00:00
* [EC-426] Add watchOS PoC app (#2054) * EC-426 Added watchOS app, configured iOS.csproj to bundle the output of XCode build into the Xamarin iOS app and added some custom logic to use WCSession to communicate between the iOS and the watchOS apps * EC-426 Removed Info.plist from iOS.Core project given that it's not needed * [EC-426] Added new encrypted watch app profiles * EC-426 added configuration for building watchApp and bundle it up on the iOS one * EC-426 Fix build for watchOS * EC-426 Fix build for watchOS applied shell bash * EC-426 Fix build for watchOS echo * EC-426 Fix build for watchOS simplify * EC-426 Fix build for watchOS added workspace path * EC-426 Changed code sign identity of watchOS project to Apple Distribution * EC-426 added manual code sign style and specified the provisioning profile for the targets on the watch xcode project * EC-426 updated path to watchOS on release on iOS.csproj and disabled android and f-.droid * EC-426 fix build * EC-426 fix path and check listing of directory of watchOS output just in case * EC-426 Fix Apple Watch build to list the folder recursively just in case we need to change the path for the watch bundle * EC-426 TEMP Change texts on input on login and lock to show that the app is for the Watch PoC testing * EC-426 Fix WatchApp build path * EC-426 Added WatchOS AppIcons * EC-426 added gitignore for XCode project removed files supposed to be ignored * EC-426 Cleaned the code a bit to avoid misbehavior * EC-426 Code cleanup Co-authored-by: Joseph Flinn <joseph.s.flinn@gmail.com> * [EC-585] Added data, encryption and some helpers and structure to the Watch app (#2164) * [EC-585] Added foundation classes on the watch to handle CoreData and some fixes on the communication of the ciphers, also some helper classes to store in keychain and encrypt data * EC-585 Added keychain helper, encryption helpers and added data storage using CoreData configuring it appropiately. View and ViewModel are here only to test that the fetching/saving works but it's not the actual UI of the watch app. Also removed all the places where the automatic file signature was added by XCode * EC-585 Fixed CipherServiceMock to implement protocol * EC-585 Fixed DeviceActionService duplicated services * [EC-614] Apple Watch MVP Cipher list UI (#2175) * [EC-585] Added foundation classes on the watch to handle CoreData and some fixes on the communication of the ciphers, also some helper classes to store in keychain and encrypt data * EC-585 Added keychain helper, encryption helpers and added data storage using CoreData configuring it appropiately. View and ViewModel are here only to test that the fetching/saving works but it's not the actual UI of the watch app. Also removed all the places where the automatic file signature was added by XCode * EC-585 Fixed CipherServiceMock to implement protocol * EC-585 Fixed DeviceActionService duplicated services * EC-614 Implemented watch ciphers list UI * [EC-615] Apple Watch MVP Cipher details UI (#2192) * [EC-585] Added foundation classes on the watch to handle CoreData and some fixes on the communication of the ciphers, also some helper classes to store in keychain and encrypt data * EC-585 Added keychain helper, encryption helpers and added data storage using CoreData configuring it appropiately. View and ViewModel are here only to test that the fetching/saving works but it's not the actual UI of the watch app. Also removed all the places where the automatic file signature was added by XCode * EC-585 Fixed CipherServiceMock to implement protocol * EC-585 Fixed DeviceActionService duplicated services * EC-614 Implemented watch ciphers list UI * EC-615 Added cipher details UI to watch and also implemented logic and helpers to generate the TOTPs * EC-615 Added value transformer to login uris on the cipher entity * EC-617 Added state view on watch app and some state helpers and wired it on the CipherListView. Also added some images (#2195) * [EC-581] Implement Apple Watch MVP Sync (#2206) * EC-581 Implemented sync iPhone -> watchOS, fix some issues with the watch database and sync flows for login/locks/multiple accounts * EC-581 Added watch sync on unlocking and need setup state when no user is synced and the session is not active * EC-581 Removed unused method * EC-581 Fix format * EC-759 Added avatar row on cipher list header to display avatar icon and email (#2213) * [EC-786] Apple Watch MVP Sync fixes (#2214) * EC-786 Commented things that are not going to be included on the MVP and fixed issue on the dictionary sent on the applicationContext to have a changing key based on time * EC-786 Commented need unlock state * EC-579 Added logic for Connect To Watch on iOS settings and moved it to the correct place. Also improved the synchronization and watch session activation logic (#2218) * EC-616 Added search header for ciphers and polished the code (#2226) Co-authored-by: Federico Maccaroni <fedemkr@gmail.com> Co-authored-by: Joseph Flinn <joseph.s.flinn@gmail.com>
690 lines
27 KiB
C#
690 lines
27 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using System.Windows.Input;
|
|
using Bit.App.Abstractions;
|
|
using Bit.App.Lists.ItemViewModels.CustomFields;
|
|
using Bit.App.Resources;
|
|
using Bit.App.Utilities;
|
|
using Bit.Core;
|
|
using Bit.Core.Abstractions;
|
|
using Bit.Core.Enums;
|
|
using Bit.Core.Exceptions;
|
|
using Bit.Core.Models.View;
|
|
using Bit.Core.Utilities;
|
|
using Xamarin.CommunityToolkit.ObjectModel;
|
|
using Xamarin.Forms;
|
|
|
|
namespace Bit.App.Pages
|
|
{
|
|
public class CipherDetailsPageViewModel : BaseCipherViewModel, IPasswordPromptable
|
|
{
|
|
private readonly ICipherService _cipherService;
|
|
private readonly IStateService _stateService;
|
|
private readonly IAuditService _auditService;
|
|
private readonly ITotpService _totpService;
|
|
private readonly IMessagingService _messagingService;
|
|
private readonly IEventService _eventService;
|
|
private readonly IPasswordRepromptService _passwordRepromptService;
|
|
private readonly ILocalizeService _localizeService;
|
|
private readonly ICustomFieldItemFactory _customFieldItemFactory;
|
|
private readonly IClipboardService _clipboardService;
|
|
private readonly IWatchDeviceService _watchDeviceService;
|
|
|
|
private List<ICustomFieldItemViewModel> _fields;
|
|
private bool _canAccessPremium;
|
|
private bool _showPassword;
|
|
private bool _showCardNumber;
|
|
private bool _showCardCode;
|
|
private string _totpCode;
|
|
private string _totpCodeFormatted;
|
|
private string _totpSec;
|
|
private double _totpInterval = Constants.TotpDefaultTimer;
|
|
private bool _totpLow;
|
|
private string _previousCipherId;
|
|
private byte[] _attachmentData;
|
|
private string _attachmentFilename;
|
|
private bool _passwordReprompted;
|
|
private TotpHelper _totpTickHelper;
|
|
private CancellationTokenSource _totpTickCancellationToken;
|
|
private Task _totpTickTask;
|
|
|
|
public CipherDetailsPageViewModel()
|
|
{
|
|
_cipherService = ServiceContainer.Resolve<ICipherService>("cipherService");
|
|
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
|
_auditService = ServiceContainer.Resolve<IAuditService>("auditService");
|
|
_totpService = ServiceContainer.Resolve<ITotpService>("totpService");
|
|
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
|
_eventService = ServiceContainer.Resolve<IEventService>("eventService");
|
|
_passwordRepromptService = ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService");
|
|
_localizeService = ServiceContainer.Resolve<ILocalizeService>("localizeService");
|
|
_customFieldItemFactory = ServiceContainer.Resolve<ICustomFieldItemFactory>("customFieldItemFactory");
|
|
_clipboardService = ServiceContainer.Resolve<IClipboardService>("clipboardService");
|
|
_watchDeviceService = ServiceContainer.Resolve<IWatchDeviceService>();
|
|
|
|
CopyCommand = new AsyncCommand<string>((id) => CopyAsync(id, null), onException: ex => _logger.Exception(ex), allowsMultipleExecutions: false);
|
|
CopyUriCommand = new AsyncCommand<LoginUriView>(uriView => CopyAsync("LoginUri", uriView.Uri), onException: ex => _logger.Exception(ex), allowsMultipleExecutions: false);
|
|
CopyFieldCommand = new AsyncCommand<FieldView>(field => CopyAsync(field.Type == FieldType.Hidden ? "H_FieldValue" : "FieldValue", field.Value), onException: ex => _logger.Exception(ex), allowsMultipleExecutions: false);
|
|
LaunchUriCommand = new Command<LoginUriView>(LaunchUri);
|
|
TogglePasswordCommand = new Command(TogglePassword);
|
|
ToggleCardNumberCommand = new Command(ToggleCardNumber);
|
|
ToggleCardCodeCommand = new Command(ToggleCardCode);
|
|
DownloadAttachmentCommand = new AsyncCommand<AttachmentView>(DownloadAttachmentAsync, allowsMultipleExecutions: false);
|
|
|
|
PageTitle = AppResources.ViewItem;
|
|
}
|
|
|
|
public ICommand CopyCommand { get; set; }
|
|
public ICommand CopyUriCommand { get; set; }
|
|
public ICommand CopyFieldCommand { get; set; }
|
|
public Command LaunchUriCommand { get; set; }
|
|
public Command TogglePasswordCommand { get; set; }
|
|
public Command ToggleCardNumberCommand { get; set; }
|
|
public Command ToggleCardCodeCommand { get; set; }
|
|
public AsyncCommand<AttachmentView> DownloadAttachmentCommand { get; set; }
|
|
public string CipherId { get; set; }
|
|
protected override string[] AdditionalPropertiesToRaiseOnCipherChanged => new string[]
|
|
{
|
|
nameof(IsLogin),
|
|
nameof(IsIdentity),
|
|
nameof(IsCard),
|
|
nameof(IsSecureNote),
|
|
nameof(ShowUris),
|
|
nameof(ShowAttachments),
|
|
nameof(ShowTotp),
|
|
nameof(ColoredPassword),
|
|
nameof(UpdatedText),
|
|
nameof(PasswordUpdatedText),
|
|
nameof(PasswordHistoryText),
|
|
nameof(ShowIdentityAddress),
|
|
nameof(IsDeleted),
|
|
nameof(CanEdit),
|
|
nameof(ShowUpgradePremiumTotpText)
|
|
};
|
|
public List<ICustomFieldItemViewModel> Fields
|
|
{
|
|
get => _fields;
|
|
set => SetProperty(ref _fields, value);
|
|
}
|
|
public bool CanAccessPremium
|
|
{
|
|
get => _canAccessPremium;
|
|
set => SetProperty(ref _canAccessPremium, value);
|
|
}
|
|
public bool ShowPassword
|
|
{
|
|
get => _showPassword;
|
|
set => SetProperty(ref _showPassword, value,
|
|
additionalPropertyNames: new string[]
|
|
{
|
|
nameof(ShowPasswordIcon),
|
|
nameof(PasswordVisibilityAccessibilityText)
|
|
});
|
|
}
|
|
public bool ShowCardNumber
|
|
{
|
|
get => _showCardNumber;
|
|
set => SetProperty(ref _showCardNumber, value,
|
|
additionalPropertyNames: new string[]
|
|
{
|
|
nameof(ShowCardNumberIcon)
|
|
});
|
|
}
|
|
public bool ShowCardCode
|
|
{
|
|
get => _showCardCode;
|
|
set => SetProperty(ref _showCardCode, value,
|
|
additionalPropertyNames: new string[]
|
|
{
|
|
nameof(ShowCardCodeIcon)
|
|
});
|
|
}
|
|
public bool IsLogin => Cipher?.Type == Core.Enums.CipherType.Login;
|
|
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 FormattedString ColoredPassword => GeneratedValueFormatter.Format(Cipher.Login.Password);
|
|
public FormattedString UpdatedText
|
|
{
|
|
get
|
|
{
|
|
var fs = new FormattedString();
|
|
fs.Spans.Add(new Span
|
|
{
|
|
Text = string.Format("{0}:", AppResources.DateUpdated),
|
|
FontAttributes = FontAttributes.Bold
|
|
});
|
|
fs.Spans.Add(new Span
|
|
{
|
|
Text = string.Format(" {0} {1}",
|
|
_localizeService.GetLocaleShortDate(Cipher.RevisionDate.ToLocalTime()),
|
|
_localizeService.GetLocaleShortTime(Cipher.RevisionDate.ToLocalTime()))
|
|
});
|
|
return fs;
|
|
}
|
|
}
|
|
public FormattedString PasswordUpdatedText
|
|
{
|
|
get
|
|
{
|
|
var fs = new FormattedString();
|
|
fs.Spans.Add(new Span
|
|
{
|
|
Text = string.Format("{0}:", AppResources.DatePasswordUpdated),
|
|
FontAttributes = FontAttributes.Bold
|
|
});
|
|
fs.Spans.Add(new Span
|
|
{
|
|
Text = string.Format(" {0} {1}",
|
|
_localizeService.GetLocaleShortDate(Cipher.PasswordRevisionDisplayDate?.ToLocalTime()),
|
|
_localizeService.GetLocaleShortTime(Cipher.PasswordRevisionDisplayDate?.ToLocalTime()))
|
|
});
|
|
return fs;
|
|
}
|
|
}
|
|
public FormattedString PasswordHistoryText
|
|
{
|
|
get
|
|
{
|
|
var fs = new FormattedString();
|
|
fs.Spans.Add(new Span
|
|
{
|
|
Text = string.Format("{0}:", AppResources.PasswordHistory),
|
|
FontAttributes = FontAttributes.Bold
|
|
});
|
|
fs.Spans.Add(new Span
|
|
{
|
|
Text = string.Format(" {0}", Cipher.PasswordHistory.Count.ToString()),
|
|
TextColor = ThemeManager.GetResourceColor("PrimaryColor")
|
|
});
|
|
return fs;
|
|
}
|
|
}
|
|
|
|
public bool ShowUpgradePremiumTotpText => !CanAccessPremium && !Cipher.OrganizationUseTotp && ShowTotp;
|
|
public bool ShowUris => IsLogin && Cipher.Login.HasUris;
|
|
public bool ShowIdentityAddress => IsIdentity && (
|
|
!string.IsNullOrWhiteSpace(Cipher.Identity.Address1) ||
|
|
!string.IsNullOrWhiteSpace(Cipher.Identity.City) ||
|
|
!string.IsNullOrWhiteSpace(Cipher.Identity.Country));
|
|
public bool ShowAttachments => Cipher.HasAttachments && (CanAccessPremium || Cipher.OrganizationId != null);
|
|
public bool ShowTotp => IsLogin && !string.IsNullOrWhiteSpace(Cipher.Login.Totp);
|
|
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
|
|
public string ShowCardNumberIcon => ShowCardNumber ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
|
|
public string ShowCardCodeIcon => ShowCardCode ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
|
|
public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow;
|
|
public string TotpCodeFormatted
|
|
{
|
|
get => ShowUpgradePremiumTotpText ? string.Empty : _totpCodeFormatted;
|
|
set => SetProperty(ref _totpCodeFormatted, value,
|
|
additionalPropertyNames: new string[]
|
|
{
|
|
nameof(ShowTotp)
|
|
});
|
|
}
|
|
public string TotpSec
|
|
{
|
|
get => _totpSec;
|
|
set => SetProperty(ref _totpSec, value,
|
|
additionalPropertyNames: new string[]
|
|
{
|
|
nameof(TotpProgress)
|
|
});
|
|
}
|
|
public bool TotpLow
|
|
{
|
|
get => _totpLow;
|
|
set
|
|
{
|
|
SetProperty(ref _totpLow, value);
|
|
Page.Resources["textTotp"] = ThemeManager.Resources()[value ? "text-danger" : "text-default"];
|
|
}
|
|
}
|
|
public double TotpProgress => string.IsNullOrEmpty(TotpSec) ? 0 : double.Parse(TotpSec) * 100 / _totpInterval;
|
|
public bool IsDeleted => Cipher.IsDeleted;
|
|
public bool CanEdit => !Cipher.IsDeleted;
|
|
|
|
public async Task<bool> LoadAsync(Action finishedLoadingAction = null)
|
|
{
|
|
var cipher = await _cipherService.GetAsync(CipherId);
|
|
if (cipher == null)
|
|
{
|
|
finishedLoadingAction?.Invoke();
|
|
return false;
|
|
}
|
|
Cipher = await cipher.DecryptAsync();
|
|
CanAccessPremium = await _stateService.CanAccessPremiumAsync();
|
|
|
|
Fields = Cipher.Fields?
|
|
.Select(f => _customFieldItemFactory.CreateCustomFieldItem(f, false, Cipher, this, CopyFieldCommand, null))
|
|
.ToList();
|
|
|
|
if (Cipher.Type == Core.Enums.CipherType.Login && !string.IsNullOrWhiteSpace(Cipher.Login.Totp) &&
|
|
(Cipher.OrganizationUseTotp || CanAccessPremium))
|
|
{
|
|
_totpTickHelper = new TotpHelper(Cipher);
|
|
_totpTickCancellationToken?.Cancel();
|
|
_totpInterval = _totpTickHelper.Interval;
|
|
_totpTickCancellationToken = new CancellationTokenSource();
|
|
_totpTickTask = new TimerTask(_logger, StartCiphersTotpTick, _totpTickCancellationToken).RunPeriodic();
|
|
}
|
|
if (_previousCipherId != CipherId)
|
|
{
|
|
var task = _eventService.CollectAsync(Core.Enums.EventType.Cipher_ClientViewed, CipherId);
|
|
}
|
|
_previousCipherId = CipherId;
|
|
finishedLoadingAction?.Invoke();
|
|
return true;
|
|
}
|
|
|
|
private async void StartCiphersTotpTick()
|
|
{
|
|
try
|
|
{
|
|
await _totpTickHelper.GenerateNewTotpValues();
|
|
TotpSec = _totpTickHelper.TotpSec;
|
|
TotpCodeFormatted = _totpTickHelper.TotpCodeFormatted;
|
|
_totpInterval = _totpTickHelper.Interval;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.Exception(ex);
|
|
}
|
|
}
|
|
|
|
public async Task StopCiphersTotpTick()
|
|
{
|
|
_totpTickCancellationToken?.Cancel();
|
|
if (_totpTickTask != null)
|
|
{
|
|
await _totpTickTask;
|
|
}
|
|
}
|
|
|
|
public async void TogglePassword()
|
|
{
|
|
if (!await PromptPasswordAsync())
|
|
{
|
|
return;
|
|
}
|
|
|
|
ShowPassword = !ShowPassword;
|
|
if (ShowPassword)
|
|
{
|
|
var task = _eventService.CollectAsync(Core.Enums.EventType.Cipher_ClientToggledPasswordVisible, CipherId);
|
|
}
|
|
}
|
|
|
|
public async void ToggleCardNumber()
|
|
{
|
|
if (!await PromptPasswordAsync())
|
|
{
|
|
return;
|
|
}
|
|
ShowCardNumber = !ShowCardNumber;
|
|
if (ShowCardNumber)
|
|
{
|
|
var task = _eventService.CollectAsync(
|
|
Core.Enums.EventType.Cipher_ClientToggledCardNumberVisible, CipherId);
|
|
}
|
|
}
|
|
|
|
public async void ToggleCardCode()
|
|
{
|
|
if (!await PromptPasswordAsync())
|
|
{
|
|
return;
|
|
}
|
|
ShowCardCode = !ShowCardCode;
|
|
if (ShowCardCode)
|
|
{
|
|
var task = _eventService.CollectAsync(
|
|
Core.Enums.EventType.Cipher_ClientToggledCardCodeVisible, CipherId);
|
|
}
|
|
}
|
|
|
|
public async Task<bool> DeleteAsync()
|
|
{
|
|
if (Xamarin.Essentials.Connectivity.NetworkAccess == Xamarin.Essentials.NetworkAccess.None)
|
|
{
|
|
await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage,
|
|
AppResources.InternetConnectionRequiredTitle);
|
|
return false;
|
|
}
|
|
var confirmed = await _platformUtilsService.ShowDialogAsync(
|
|
Cipher.IsDeleted ? AppResources.DoYouReallyWantToPermanentlyDeleteCipher : AppResources.DoYouReallyWantToSoftDeleteCipher,
|
|
null, AppResources.Yes, AppResources.Cancel);
|
|
if (!confirmed)
|
|
{
|
|
return false;
|
|
}
|
|
try
|
|
{
|
|
await _deviceActionService.ShowLoadingAsync(Cipher.IsDeleted ? AppResources.Deleting : AppResources.SoftDeleting);
|
|
if (Cipher.IsDeleted)
|
|
{
|
|
await _cipherService.DeleteWithServerAsync(Cipher.Id);
|
|
}
|
|
else
|
|
{
|
|
await _cipherService.SoftDeleteWithServerAsync(Cipher.Id);
|
|
}
|
|
await _deviceActionService.HideLoadingAsync();
|
|
|
|
_watchDeviceService.SyncDataToWatchAsync().FireAndForget();
|
|
|
|
_platformUtilsService.ShowToast("success", null,
|
|
Cipher.IsDeleted ? AppResources.ItemDeleted : AppResources.ItemSoftDeleted);
|
|
_messagingService.Send(Cipher.IsDeleted ? "deletedCipher" : "softDeletedCipher", Cipher);
|
|
return true;
|
|
}
|
|
catch (ApiException e)
|
|
{
|
|
await _deviceActionService.HideLoadingAsync();
|
|
if (e?.Error != null)
|
|
{
|
|
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
|
AppResources.AnErrorHasOccurred);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public async Task<bool> RestoreAsync()
|
|
{
|
|
if (!IsDeleted)
|
|
{
|
|
return false;
|
|
}
|
|
if (Xamarin.Essentials.Connectivity.NetworkAccess == Xamarin.Essentials.NetworkAccess.None)
|
|
{
|
|
await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage,
|
|
AppResources.InternetConnectionRequiredTitle);
|
|
return false;
|
|
}
|
|
var confirmed = await _platformUtilsService.ShowDialogAsync(AppResources.DoYouReallyWantToRestoreCipher,
|
|
null, AppResources.Yes, AppResources.Cancel);
|
|
if (!confirmed)
|
|
{
|
|
return false;
|
|
}
|
|
try
|
|
{
|
|
await _deviceActionService.ShowLoadingAsync(AppResources.Restoring);
|
|
await _cipherService.RestoreWithServerAsync(Cipher.Id);
|
|
await _deviceActionService.HideLoadingAsync();
|
|
_platformUtilsService.ShowToast("success", null, AppResources.ItemRestored);
|
|
_messagingService.Send("restoredCipher", Cipher);
|
|
return true;
|
|
}
|
|
catch (ApiException e)
|
|
{
|
|
await _deviceActionService.HideLoadingAsync();
|
|
if (e?.Error != null)
|
|
{
|
|
await _platformUtilsService.ShowDialogAsync(e.Error.GetSingleMessage(),
|
|
AppResources.AnErrorHasOccurred);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private async Task TotpUpdateCodeAsync()
|
|
{
|
|
if (Cipher == null || Cipher.Type != Core.Enums.CipherType.Login || Cipher.Login.Totp == null)
|
|
{
|
|
return;
|
|
}
|
|
_totpCode = await _totpService.GetCodeAsync(Cipher.Login.Totp);
|
|
if (_totpCode != null)
|
|
{
|
|
if (_totpCode.Length > 4)
|
|
{
|
|
var half = (int)Math.Floor(_totpCode.Length / 2M);
|
|
TotpCodeFormatted = string.Format("{0} {1}", _totpCode.Substring(0, half),
|
|
_totpCode.Substring(half));
|
|
}
|
|
else
|
|
{
|
|
TotpCodeFormatted = _totpCode;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
TotpCodeFormatted = null;
|
|
}
|
|
}
|
|
|
|
private async Task TotpTickAsync(int intervalSeconds)
|
|
{
|
|
var epoc = CoreHelpers.EpocUtcNow() / 1000;
|
|
var mod = epoc % intervalSeconds;
|
|
var totpSec = intervalSeconds - mod;
|
|
TotpSec = totpSec.ToString();
|
|
TotpLow = totpSec < 7;
|
|
if (mod == 0)
|
|
{
|
|
await TotpUpdateCodeAsync();
|
|
}
|
|
}
|
|
|
|
private async Task DownloadAttachmentAsync(AttachmentView attachment)
|
|
{
|
|
try
|
|
{
|
|
if (Xamarin.Essentials.Connectivity.NetworkAccess == Xamarin.Essentials.NetworkAccess.None)
|
|
{
|
|
await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage,
|
|
AppResources.InternetConnectionRequiredTitle);
|
|
return;
|
|
}
|
|
if (Cipher.OrganizationId == null && !CanAccessPremium)
|
|
{
|
|
await _platformUtilsService.ShowDialogAsync(AppResources.PremiumRequired);
|
|
return;
|
|
}
|
|
if (attachment.FileSize >= 10485760) // 10 MB
|
|
{
|
|
var confirmed = await _platformUtilsService.ShowDialogAsync(
|
|
string.Format(AppResources.AttachmentLargeWarning, attachment.SizeName), null,
|
|
AppResources.Yes, AppResources.No);
|
|
if (!confirmed)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
var canOpenFile = true;
|
|
if (!_fileService.CanOpenFile(attachment.FileName))
|
|
{
|
|
if (Device.RuntimePlatform == Device.iOS)
|
|
{
|
|
// iOS is currently hardcoded to always return CanOpenFile == true, but should it ever return false
|
|
// for any reason we want to be sure to catch it here.
|
|
await _platformUtilsService.ShowDialogAsync(AppResources.UnableToOpenFile);
|
|
return;
|
|
}
|
|
|
|
canOpenFile = false;
|
|
}
|
|
|
|
if (!await PromptPasswordAsync())
|
|
{
|
|
return;
|
|
}
|
|
|
|
await _deviceActionService.ShowLoadingAsync(AppResources.Downloading);
|
|
var data = await _cipherService.DownloadAndDecryptAttachmentAsync(Cipher.Id, attachment, Cipher.OrganizationId);
|
|
await _deviceActionService.HideLoadingAsync();
|
|
if (data == null)
|
|
{
|
|
await _platformUtilsService.ShowDialogAsync(AppResources.UnableToDownloadFile);
|
|
return;
|
|
}
|
|
|
|
if (Device.RuntimePlatform == Device.Android)
|
|
{
|
|
if (canOpenFile)
|
|
{
|
|
// We can open this attachment directly, so give the user the option to open or save
|
|
PromptOpenOrSave(data, attachment);
|
|
}
|
|
else
|
|
{
|
|
// We can't open this attachment so go directly to save
|
|
SaveAttachment(data, attachment);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
OpenAttachment(data, attachment);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.Exception(ex);
|
|
await _deviceActionService.HideLoadingAsync();
|
|
await _platformUtilsService.ShowDialogAsync(AppResources.AnErrorHasOccurred);
|
|
}
|
|
}
|
|
|
|
public async void PromptOpenOrSave(byte[] data, AttachmentView attachment)
|
|
{
|
|
var selection = await Page.DisplayActionSheet(attachment.FileName, AppResources.Cancel, null,
|
|
AppResources.Open, AppResources.Save);
|
|
if (selection == AppResources.Open)
|
|
{
|
|
OpenAttachment(data, attachment);
|
|
}
|
|
else if (selection == AppResources.Save)
|
|
{
|
|
SaveAttachment(data, attachment);
|
|
}
|
|
}
|
|
|
|
public async void OpenAttachment(byte[] data, AttachmentView attachment)
|
|
{
|
|
if (!_fileService.OpenFile(data, attachment.Id, attachment.FileName))
|
|
{
|
|
await _platformUtilsService.ShowDialogAsync(AppResources.UnableToOpenFile);
|
|
return;
|
|
}
|
|
}
|
|
|
|
public async void SaveAttachment(byte[] data, AttachmentView attachment)
|
|
{
|
|
_attachmentData = data;
|
|
_attachmentFilename = attachment.FileName;
|
|
if (!_fileService.SaveFile(_attachmentData, null, _attachmentFilename, null))
|
|
{
|
|
ClearAttachmentData();
|
|
await _platformUtilsService.ShowDialogAsync(AppResources.UnableToSaveAttachment);
|
|
}
|
|
}
|
|
|
|
public async void SaveFileSelected(string contentUri, string filename)
|
|
{
|
|
if (_fileService.SaveFile(_attachmentData, null, filename ?? _attachmentFilename, contentUri))
|
|
{
|
|
ClearAttachmentData();
|
|
_platformUtilsService.ShowToast("success", null, AppResources.SaveAttachmentSuccess);
|
|
return;
|
|
}
|
|
|
|
ClearAttachmentData();
|
|
await _platformUtilsService.ShowDialogAsync(AppResources.UnableToSaveAttachment);
|
|
}
|
|
|
|
private void ClearAttachmentData()
|
|
{
|
|
_attachmentData = null;
|
|
_attachmentFilename = null;
|
|
}
|
|
|
|
private async Task CopyAsync(string id, string text = null)
|
|
{
|
|
if (_passwordRepromptService.ProtectedFields.Contains(id) && !await PromptPasswordAsync())
|
|
{
|
|
return;
|
|
}
|
|
|
|
string name = null;
|
|
if (id == "LoginUsername")
|
|
{
|
|
text = Cipher.Login.Username;
|
|
name = AppResources.Username;
|
|
}
|
|
else if (id == "LoginPassword")
|
|
{
|
|
text = Cipher.Login.Password;
|
|
name = AppResources.Password;
|
|
}
|
|
else if (id == "LoginTotp")
|
|
{
|
|
text = TotpCodeFormatted.Replace(" ", string.Empty);
|
|
name = AppResources.VerificationCodeTotp;
|
|
}
|
|
else if (id == "LoginUri")
|
|
{
|
|
name = AppResources.URI;
|
|
}
|
|
else if (id == "FieldValue" || id == "H_FieldValue")
|
|
{
|
|
name = AppResources.Value;
|
|
}
|
|
else if (id == "CardNumber")
|
|
{
|
|
text = Cipher.Card.Number;
|
|
name = AppResources.Number;
|
|
}
|
|
else if (id == "CardCode")
|
|
{
|
|
text = Cipher.Card.Code;
|
|
name = AppResources.SecurityCode;
|
|
}
|
|
|
|
if (text != null)
|
|
{
|
|
await _clipboardService.CopyTextAsync(text);
|
|
if (!string.IsNullOrWhiteSpace(name))
|
|
{
|
|
_platformUtilsService.ShowToastForCopiedValue(name);
|
|
}
|
|
if (id == "LoginPassword")
|
|
{
|
|
await _eventService.CollectAsync(Core.Enums.EventType.Cipher_ClientCopiedPassword, CipherId);
|
|
}
|
|
else if (id == "CardCode")
|
|
{
|
|
await _eventService.CollectAsync(Core.Enums.EventType.Cipher_ClientCopiedCardCode, CipherId);
|
|
}
|
|
else if (id == "H_FieldValue")
|
|
{
|
|
await _eventService.CollectAsync(Core.Enums.EventType.Cipher_ClientCopiedHiddenField, CipherId);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void LaunchUri(LoginUriView uri)
|
|
{
|
|
if (uri.CanLaunch && (Page as BaseContentPage).DoOnce())
|
|
{
|
|
_platformUtilsService.LaunchUri(uri.LaunchUri);
|
|
}
|
|
}
|
|
|
|
public async Task<bool> PromptPasswordAsync()
|
|
{
|
|
if (Cipher.Reprompt == CipherRepromptType.None || _passwordReprompted)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return _passwordReprompted = await _passwordRepromptService.ShowPasswordPromptAsync();
|
|
}
|
|
}
|
|
}
|