diff --git a/src/Android/MainActivity.cs b/src/Android/MainActivity.cs index e2e9bb407..8987e9e1a 100644 --- a/src/Android/MainActivity.cs +++ b/src/Android/MainActivity.cs @@ -38,7 +38,6 @@ namespace Bit.Droid private IAppIdService _appIdService; private IStorageService _storageService; private IEventService _eventService; - private PendingIntent _vaultTimeoutAlarmPendingIntent; private PendingIntent _clearClipboardPendingIntent; private PendingIntent _eventUploadPendingIntent; private AppOptions _appOptions; @@ -51,9 +50,6 @@ namespace Bit.Droid var eventUploadIntent = new Intent(this, typeof(EventUploadReceiver)); _eventUploadPendingIntent = PendingIntent.GetBroadcast(this, 0, eventUploadIntent, PendingIntentFlags.UpdateCurrent); - var alarmIntent = new Intent(this, typeof(LockAlarmReceiver)); - _vaultTimeoutAlarmPendingIntent = PendingIntent.GetBroadcast(this, 0, alarmIntent, - PendingIntentFlags.UpdateCurrent); var clearClipboardIntent = new Intent(this, typeof(ClearClipboardAlarmReceiver)); _clearClipboardPendingIntent = PendingIntent.GetBroadcast(this, 0, clearClipboardIntent, PendingIntentFlags.UpdateCurrent); @@ -91,20 +87,7 @@ namespace Bit.Droid _broadcasterService.Subscribe(_activityKey, (message) => { - if (message.Command == "scheduleVaultTimeoutTimer") - { - var alarmManager = GetSystemService(AlarmService) as AlarmManager; - var vaultTimeoutMinutes = (int)message.Data; - var vaultTimeoutMs = vaultTimeoutMinutes * 60000; - var triggerMs = Java.Lang.JavaSystem.CurrentTimeMillis() + vaultTimeoutMs + 10; - alarmManager.Set(AlarmType.RtcWakeup, triggerMs, _vaultTimeoutAlarmPendingIntent); - } - else if (message.Command == "cancelVaultTimeoutTimer") - { - var alarmManager = GetSystemService(AlarmService) as AlarmManager; - alarmManager.Cancel(_vaultTimeoutAlarmPendingIntent); - } - else if (message.Command == "startEventTimer") + if (message.Command == "startEventTimer") { StartEventAlarm(); } diff --git a/src/Android/Services/DeviceActionService.cs b/src/Android/Services/DeviceActionService.cs index 898be8c40..2c106e454 100644 --- a/src/Android/Services/DeviceActionService.cs +++ b/src/Android/Services/DeviceActionService.cs @@ -752,6 +752,15 @@ namespace Bit.Droid.Services return false; } + public long GetActiveTime() + { + // Returns milliseconds since the system was booted, and includes deep sleep. This clock is guaranteed to + // be monotonic, and continues to tick even when the CPU is in power saving modes, so is the recommend + // basis for general purpose interval timing. + // ref: https://developer.android.com/reference/android/os/SystemClock#elapsedRealtime() + return SystemClock.ElapsedRealtime() / 1000; + } + private bool DeleteDir(Java.IO.File dir) { if (dir != null && dir.IsDirectory) diff --git a/src/App/Abstractions/IDeviceActionService.cs b/src/App/Abstractions/IDeviceActionService.cs index 2d1d20067..c1795685f 100644 --- a/src/App/Abstractions/IDeviceActionService.cs +++ b/src/App/Abstractions/IDeviceActionService.cs @@ -43,5 +43,6 @@ namespace Bit.App.Abstractions void OpenAccessibilityOverlayPermissionSettings(); void OpenAutofillSettings(); bool UsingDarkTheme(); + long GetActiveTime(); } } diff --git a/src/App/App.xaml.cs b/src/App/App.xaml.cs index a2e7fc191..d4d8c7b81 100644 --- a/src/App/App.xaml.cs +++ b/src/App/App.xaml.cs @@ -185,7 +185,7 @@ namespace Bit.App var isLocked = await _vaultTimeoutService.IsLockedAsync(); if (!isLocked) { - await _storageService.SaveAsync(Constants.LastActiveKey, DateTime.UtcNow); + await _storageService.SaveAsync(Constants.LastActiveKey, _deviceActionService.GetActiveTime()); } SetTabsPageFromAutofill(isLocked); await SleptAsync(); @@ -210,7 +210,7 @@ namespace Bit.App private async void ResumedAsync() { - _messagingService.Send("cancelVaultTimeoutTimer"); + await _vaultTimeoutService.CheckVaultTimeoutAsync(); _messagingService.Send("startEventTimer"); await ClearCacheIfNeededAsync(); Prime(); @@ -302,11 +302,7 @@ namespace Bit.App vaultTimeout = await _storageService.GetAsync(Constants.VaultTimeoutKey); } vaultTimeout = vaultTimeout.GetValueOrDefault(-1); - if (vaultTimeout > 0) - { - _messagingService.Send("scheduleVaultTimeoutTimer", vaultTimeout.Value); - } - else if (vaultTimeout == 0) + if (vaultTimeout == 0) { var action = await _storageService.GetAsync(Constants.VaultTimeoutActionKey); if (action == "logOut") diff --git a/src/App/Pages/BaseContentPage.cs b/src/App/Pages/BaseContentPage.cs index 4d7d8b803..1dcce333e 100644 --- a/src/App/Pages/BaseContentPage.cs +++ b/src/App/Pages/BaseContentPage.cs @@ -3,6 +3,7 @@ using Bit.Core.Abstractions; using Bit.Core.Utilities; using System; using System.Threading.Tasks; +using Bit.App.Abstractions; using Xamarin.Forms; using Xamarin.Forms.PlatformConfiguration; using Xamarin.Forms.PlatformConfiguration.iOSSpecific; @@ -12,6 +13,7 @@ namespace Bit.App.Pages public class BaseContentPage : ContentPage { private IStorageService _storageService; + private IDeviceActionService _deviceActionService; protected int ShowModalAnimationDelay = 400; protected int ShowPageAnimationDelay = 100; @@ -101,18 +103,22 @@ namespace Bit.App.Pages }); } - private void SetStorageService() + private void SetServices() { if (_storageService == null) { _storageService = ServiceContainer.Resolve("storageService"); } + if (_deviceActionService == null) + { + _deviceActionService = ServiceContainer.Resolve("deviceActionService"); + } } private void SaveActivity() { - SetStorageService(); - _storageService.SaveAsync(Constants.LastActiveKey, DateTime.UtcNow); + SetServices(); + _storageService.SaveAsync(Constants.LastActiveKey, _deviceActionService.GetActiveTime()); } } } diff --git a/src/App/Services/MobilePlatformUtilsService.cs b/src/App/Services/MobilePlatformUtilsService.cs index 577713a46..9b7ce2add 100644 --- a/src/App/Services/MobilePlatformUtilsService.cs +++ b/src/App/Services/MobilePlatformUtilsService.cs @@ -241,5 +241,10 @@ namespace Bit.App.Services catch { } return false; } + + public long GetActiveTime() + { + return _deviceActionService.GetActiveTime(); + } } } diff --git a/src/Core/Abstractions/IPlatformUtilsService.cs b/src/Core/Abstractions/IPlatformUtilsService.cs index 5ee188135..5469dfe2e 100644 --- a/src/Core/Abstractions/IPlatformUtilsService.cs +++ b/src/Core/Abstractions/IPlatformUtilsService.cs @@ -28,5 +28,6 @@ namespace Bit.Core.Abstractions bool SupportsDuo(); Task SupportsBiometricAsync(); Task AuthenticateBiometricAsync(string text = null, string fallbackText = null, Action fallback = null); + long GetActiveTime(); } -} \ No newline at end of file +} diff --git a/src/Core/Services/VaultTimeoutService.cs b/src/Core/Services/VaultTimeoutService.cs index ce369210a..1e1847897 100644 --- a/src/Core/Services/VaultTimeoutService.cs +++ b/src/Core/Services/VaultTimeoutService.cs @@ -90,13 +90,13 @@ namespace Bit.Core.Services { return; } - var lastActive = await _storageService.GetAsync(Constants.LastActiveKey); + var lastActive = await _storageService.GetAsync(Constants.LastActiveKey); if (lastActive == null) { return; } - var diff = DateTime.UtcNow - lastActive.Value; - if (diff.TotalSeconds >= vaultTimeout.Value) + var diff = _platformUtilsService.GetActiveTime() - lastActive; + if (diff >= vaultTimeout * 60) { // Pivot based on saved action var action = await _storageService.GetAsync(Constants.VaultTimeoutActionKey); diff --git a/src/iOS.Core/Services/DeviceActionService.cs b/src/iOS.Core/Services/DeviceActionService.cs index a1ed3bcb9..fa06cf2d5 100644 --- a/src/iOS.Core/Services/DeviceActionService.cs +++ b/src/iOS.Core/Services/DeviceActionService.cs @@ -430,6 +430,13 @@ namespace Bit.iOS.Core.Services return false; } + public long GetActiveTime() + { + // Fall back to UnixTimeSeconds in case this approach stops working. We'll lose clock-change protection but + // the lock functionality will continue to work. + return iOSHelpers.GetSystemUpTimeSeconds() ?? DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + } + private void ImagePicker_FinishedPickingMedia(object sender, UIImagePickerMediaPickedEventArgs e) { if (sender is UIImagePickerController picker) diff --git a/src/iOS.Core/Utilities/iOSHelpers.cs b/src/iOS.Core/Utilities/iOSHelpers.cs index c13ad95c9..ab6a8c36d 100644 --- a/src/iOS.Core/Utilities/iOSHelpers.cs +++ b/src/iOS.Core/Utilities/iOSHelpers.cs @@ -1,4 +1,7 @@ -using Bit.App.Utilities; +using System; +using System.Runtime.InteropServices; +using Bit.App.Utilities; +using Microsoft.AppCenter.Crashes; using UIKit; using Xamarin.Forms; using Xamarin.Forms.Platform.iOS; @@ -7,7 +10,52 @@ namespace Bit.iOS.Core.Utilities { public static class iOSHelpers { - public static System.nfloat? GetAccessibleFont(double size) + [DllImport(ObjCRuntime.Constants.SystemLibrary)] + internal static extern int sysctlbyname([MarshalAs(UnmanagedType.LPStr)] string property, IntPtr output, + IntPtr oldLen, IntPtr newp, uint newLen); + + // Returns the difference between when the system was booted and now in seconds, resulting in a duration that + // includes sleep time. + // ref: https://forums.xamarin.com/discussion/20006/access-to-sysctl-h + // ref: https://github.com/XLabs/Xamarin-Forms-Labs/blob/master/src/Platform/XLabs.Platform.iOS/Device/AppleDevice.cs + public static long? GetSystemUpTimeSeconds() + { + long? uptime = null; + IntPtr pLen = default, pStr = default; + try + { + var property = "kern.boottime"; + pLen = Marshal.AllocHGlobal(sizeof(int)); + sysctlbyname(property, IntPtr.Zero, pLen, IntPtr.Zero, 0); + var length = Marshal.ReadInt32(pLen); + pStr = Marshal.AllocHGlobal(length); + sysctlbyname(property, pStr, pLen, IntPtr.Zero, 0); + var timeVal = Marshal.PtrToStructure(pStr); + var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + if (timeVal.sec > 0 && now > 0) + { + uptime = now - timeVal.sec; + } + } + catch (Exception e) + { + Crashes.TrackError(e); + } + finally + { + if (pLen != default) + { + Marshal.FreeHGlobal(pLen); + } + if (pStr != default) + { + Marshal.FreeHGlobal(pStr); + } + } + return uptime; + } + + public static nfloat? GetAccessibleFont(double size) { var pointSize = UIFontDescriptor.PreferredBody.PointSize; if (size == Device.GetNamedSize(NamedSize.Large, typeof(T))) @@ -60,5 +108,11 @@ namespace Bit.iOS.Core.Utilities control, NSLayoutAttribute.Bottom, 1, 10f), }); } + + private struct TimeVal + { + public long sec; + public long usec; + } } } diff --git a/src/iOS/AppDelegate.cs b/src/iOS/AppDelegate.cs index ab3d39af4..66464ae53 100644 --- a/src/iOS/AppDelegate.cs +++ b/src/iOS/AppDelegate.cs @@ -4,7 +4,6 @@ using System.Threading.Tasks; using AuthenticationServices; using Bit.App.Abstractions; using Bit.App.Pages; -using Bit.App.Resources; using Bit.App.Services; using Bit.App.Utilities; using Bit.Core; @@ -29,8 +28,6 @@ namespace Bit.iOS private Core.NFCReaderDelegate _nfcDelegate = null; private NSTimer _clipboardTimer = null; private nint _clipboardBackgroundTaskId; - private NSTimer _vaultTimeoutTimer = null; - private nint _lockBackgroundTaskId; private NSTimer _eventTimer = null; private nint _eventBackgroundTaskId; @@ -59,15 +56,7 @@ namespace Bit.iOS _broadcasterService.Subscribe(nameof(AppDelegate), async (message) => { - if (message.Command == "scheduleVaultTimeoutTimer") - { - VaultTimeoutTimer((int)message.Data); - } - else if (message.Command == "cancelVaultTimeoutTimer") - { - CancelVaultTimeoutTimer(); - } - else if (message.Command == "startEventTimer") + if (message.Command == "startEventTimer") { StartEventTimer(); } @@ -212,7 +201,7 @@ namespace Bit.iOS UIApplication.SharedApplication.KeyWindow.BringSubviewToFront(view); UIApplication.SharedApplication.KeyWindow.EndEditing(true); UIApplication.SharedApplication.SetStatusBarHidden(true, false); - _storageService.SaveAsync(Constants.LastActiveKey, DateTime.UtcNow); + _storageService.SaveAsync(Constants.LastActiveKey, _deviceActionService.GetActiveTime()); _messagingService.Send("slept"); base.DidEnterBackground(uiApplication); } @@ -322,55 +311,6 @@ namespace Bit.iOS "pushNotificationService", iosPushNotificationService); } - private void VaultTimeoutTimer(int vaultTimeoutMinutes) - { - if (_lockBackgroundTaskId > 0) - { - UIApplication.SharedApplication.EndBackgroundTask(_lockBackgroundTaskId); - _lockBackgroundTaskId = 0; - } - _lockBackgroundTaskId = UIApplication.SharedApplication.BeginBackgroundTask(() => - { - UIApplication.SharedApplication.EndBackgroundTask(_lockBackgroundTaskId); - _lockBackgroundTaskId = 0; - }); - var vaultTimeoutMs = vaultTimeoutMinutes * 60000; - _vaultTimeoutTimer?.Invalidate(); - _vaultTimeoutTimer?.Dispose(); - _vaultTimeoutTimer = null; - var vaultTimeoutMsSpan = TimeSpan.FromMilliseconds(vaultTimeoutMs + 10); - Device.BeginInvokeOnMainThread(() => - { - _vaultTimeoutTimer = NSTimer.CreateScheduledTimer(vaultTimeoutMsSpan, timer => - { - Device.BeginInvokeOnMainThread(() => - { - _vaultTimeoutService.CheckVaultTimeoutAsync(); - _vaultTimeoutTimer?.Invalidate(); - _vaultTimeoutTimer?.Dispose(); - _vaultTimeoutTimer = null; - if (_lockBackgroundTaskId > 0) - { - UIApplication.SharedApplication.EndBackgroundTask(_lockBackgroundTaskId); - _lockBackgroundTaskId = 0; - } - }); - }); - }); - } - - private void CancelVaultTimeoutTimer() - { - _vaultTimeoutTimer?.Invalidate(); - _vaultTimeoutTimer?.Dispose(); - _vaultTimeoutTimer = null; - if (_lockBackgroundTaskId > 0) - { - UIApplication.SharedApplication.EndBackgroundTask(_lockBackgroundTaskId); - _lockBackgroundTaskId = 0; - } - } - private async Task ClearClipboardTimerAsync(Tuple data) { if (data.Item3)