Use monotonic clock for vault timeout (#1175)

* Use monotonic clock for vault timeout

* free memory

* removed vault timeout timers and added crash logging to iOS clock hack
This commit is contained in:
Matt Portune 2020-12-14 15:29:30 -05:00 committed by GitHub
parent 3227daddaf
commit acf2e4360f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 98 additions and 96 deletions

View File

@ -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();
}

View File

@ -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)

View File

@ -43,5 +43,6 @@ namespace Bit.App.Abstractions
void OpenAccessibilityOverlayPermissionSettings();
void OpenAutofillSettings();
bool UsingDarkTheme();
long GetActiveTime();
}
}

View File

@ -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<int?>(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<string>(Constants.VaultTimeoutActionKey);
if (action == "logOut")

View File

@ -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<IStorageService>("storageService");
}
if (_deviceActionService == null)
{
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
}
}
private void SaveActivity()
{
SetStorageService();
_storageService.SaveAsync(Constants.LastActiveKey, DateTime.UtcNow);
SetServices();
_storageService.SaveAsync(Constants.LastActiveKey, _deviceActionService.GetActiveTime());
}
}
}

View File

@ -241,5 +241,10 @@ namespace Bit.App.Services
catch { }
return false;
}
public long GetActiveTime()
{
return _deviceActionService.GetActiveTime();
}
}
}

View File

@ -28,5 +28,6 @@ namespace Bit.Core.Abstractions
bool SupportsDuo();
Task<bool> SupportsBiometricAsync();
Task<bool> AuthenticateBiometricAsync(string text = null, string fallbackText = null, Action fallback = null);
long GetActiveTime();
}
}
}

View File

@ -90,13 +90,13 @@ namespace Bit.Core.Services
{
return;
}
var lastActive = await _storageService.GetAsync<DateTime?>(Constants.LastActiveKey);
var lastActive = await _storageService.GetAsync<long?>(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<string>(Constants.VaultTimeoutActionKey);

View File

@ -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)

View File

@ -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<T>(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<TimeVal>(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<T>(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;
}
}
}

View File

@ -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<string, int?, bool> data)
{
if (data.Item3)