add PIN lock after exceeding failure count

This commit is contained in:
Matthias Oesterheld
2024-10-16 15:58:37 +02:00
parent c592726bd4
commit e21ab79d5d
2 changed files with 98 additions and 2 deletions

View File

@ -120,6 +120,7 @@ Note: Only when you use the `tap_action` field do you also need to include the `
In order to provide at least some kind of additional security, users with touch devices can now choose to use a PIN confirmation when using the `tap_action` field. Please be aware that the dashboard configuration is hosted on your Home assistant server without authentication, making the PIN publicly available for everyone accessing your configuration. In order to provide at least some kind of additional security, users with touch devices can now choose to use a PIN confirmation when using the `tap_action` field. Please be aware that the dashboard configuration is hosted on your Home assistant server without authentication, making the PIN publicly available for everyone accessing your configuration.
The PIN can be a string of arbitrary length consisting only of the digits 1-4. The PIN can be a string of arbitrary length consisting only of the digits 1-4.
The user has 5 attempts to provide a valid PIN within 2 minutes. If too many failures have been detected in this time, the PIN dialog will be locked for 10 minutes.
```json ```json
{ {

View File

@ -1,8 +1,29 @@
//-----------------------------------------------------------------------------------
//
// Distributed under MIT Licence
// See https://github.com/house-of-abbey/GarminHomeAssistant/blob/main/LICENSE.
//
//-----------------------------------------------------------------------------------
//
// GarminHomeAssistant is a Garmin IQ application written in Monkey C and routinely
// tested on a Venu 2 device. The source code is provided at:
// https://github.com/house-of-abbey/GarminHomeAssistant.
//
// P A Abbey & J D Abbey & Someone0nEarth, 31 October 2023
//
//
// Description:
//
// Pin Confirmation dialog and logic.
//
//-----------------------------------------------------------------------------------
import Toybox.Graphics; import Toybox.Graphics;
import Toybox.Lang; import Toybox.Lang;
import Toybox.WatchUi; import Toybox.WatchUi;
import Toybox.Timer; import Toybox.Timer;
import Toybox.Attention; import Toybox.Attention;
import Toybox.Time;
class PinDigit extends WatchUi.Selectable { class PinDigit extends WatchUi.Selectable {
@ -82,23 +103,32 @@ class HomeAssistantPinConfirmationDelegate extends WatchUi.BehaviorDelegate {
private var mConfirmMethod as Method(state as Lang.Boolean) as Void; private var mConfirmMethod as Method(state as Lang.Boolean) as Void;
private var mTimer as Timer.Timer or Null; private var mTimer as Timer.Timer or Null;
private var mState as Lang.Boolean; private var mState as Lang.Boolean;
private var mFailures as PinFailures;
function initialize(callback as Method(state as Lang.Boolean) as Void, state as Lang.Boolean, pin as String) { function initialize(callback as Method(state as Lang.Boolean) as Void, state as Lang.Boolean, pin as String) {
BehaviorDelegate.initialize(); BehaviorDelegate.initialize();
mFailures = new PinFailures();
if (mFailures.isLocked()) {
WatchUi.showToast("PIN input locked for " + mFailures.getLockedUntilSeconds() + " seconds", {});
}
mPin = pin.toCharArray(); mPin = pin.toCharArray();
mCurrentIndex = 0; mCurrentIndex = 0;
mConfirmMethod = callback; mConfirmMethod = callback;
mState = state; mState = state;
resetTimer(); resetTimer();
} }
function onSelectable(event as SelectableEvent) as Boolean { function onSelectable(event as SelectableEvent) as Boolean {
if (mFailures.isLocked()) {
goBack();
}
var instance = event.getInstance(); var instance = event.getInstance();
if (instance instanceof PinDigit && event.getPreviousState() == :stateSelected) { if (instance instanceof PinDigit && event.getPreviousState() == :stateSelected) {
var currentDigit = getTranscodedCurrentDigit(); var currentDigit = getTranscodedCurrentDigit();
if (currentDigit != null && currentDigit == instance.getDigit()) { if (currentDigit != null && currentDigit == instance.getDigit()) {
// System.println("Pin digit " + (mCurrentIndex+1) + " matches"); // System.println("Pin digit " + (mCurrentIndex+1) + " matches");
if (mCurrentIndex == mPin.size()-1) { if (mCurrentIndex == mPin.size()-1) {
mFailures.reset();
getApp().getQuitTimer().reset(); getApp().getQuitTimer().reset();
if (mTimer != null) { if (mTimer != null) {
mTimer.stop(); mTimer.stop();
@ -111,7 +141,6 @@ class HomeAssistantPinConfirmationDelegate extends WatchUi.BehaviorDelegate {
} }
} else { } else {
// System.println("Pin digit " + (mCurrentIndex+1) + " doesn't match"); // System.println("Pin digit " + (mCurrentIndex+1) + " doesn't match");
// TODO: add maxFailures counter & protection
error(); error();
} }
} }
@ -151,6 +180,7 @@ class HomeAssistantPinConfirmationDelegate extends WatchUi.BehaviorDelegate {
} }
function error() as Void { function error() as Void {
mFailures.addFailure();
if (Attention has :vibrate && Settings.getVibrate()) { if (Attention has :vibrate && Settings.getVibrate()) {
Attention.vibrate([ Attention.vibrate([
new Attention.VibeProfile(100, 100), new Attention.VibeProfile(100, 100),
@ -165,4 +195,69 @@ class HomeAssistantPinConfirmationDelegate extends WatchUi.BehaviorDelegate {
goBack(); goBack();
} }
}
class PinFailures {
const MAX_FAILURES as Number = 5; // maximum number of failed pin confirmation attemps allwed in ...
const MAX_FAILURE_MINUTES as Number = 2; // ... this number of minutes before pin confirmation is locked for ...
const LOCK_TIME_MINUTES as Number = 10; // ... this number of minutes
const STORAGE_KEY_FAILURES as String = "pin_failures";
const STORAGE_KEY_LOCKED as String = "pin_locked";
private var mFailures as Array<Number>;
private var mLockedUntil as Number or Null;
function initialize() {
// System.println("Initializing PIN failures from storage");
var failures = Application.Storage.getValue(PinFailures.STORAGE_KEY_FAILURES);
mFailures = (failures == null) ? [] : failures;
mLockedUntil = Application.Storage.getValue(PinFailures.STORAGE_KEY_LOCKED);
}
function addFailure() {
mFailures.add(Time.now().value());
// System.println(mFailures.size() + " PIN confirmation failures recorded");
if (mFailures.size() >= MAX_FAILURES) {
// System.println("Too many failures detected");
var oldestFailureOutdate = new Time.Moment(mFailures[0]).add(new Time.Duration(MAX_FAILURE_MINUTES * 60));
// System.println("Oldest failure: " + oldestFailureOutdate.value() + " Now:" + Time.now().value());
if (new Time.Moment(Time.now().value()).greaterThan(oldestFailureOutdate)) {
// System.println("Pruning oldest outdated failure");
mFailures = mFailures.slice(1, null);
} else {
mFailures = [];
mLockedUntil = Time.now().add(new Time.Duration(LOCK_TIME_MINUTES * Gregorian.SECONDS_PER_MINUTE)).value();
Application.Storage.setValue(STORAGE_KEY_LOCKED, mLockedUntil);
// System.println("Locked until " + mLockedUntil);
}
}
Application.Storage.setValue(STORAGE_KEY_FAILURES, mFailures);
}
function reset() {
// System.println("Resetting failures");
mFailures = [];
mLockedUntil = null;
Application.Storage.deleteValue(STORAGE_KEY_FAILURES);
Application.Storage.deleteValue(STORAGE_KEY_LOCKED);
}
function getLockedUntilSeconds() as Number {
return new Time.Moment(mLockedUntil).subtract(Time.now()).value();
}
function isLocked() as Boolean {
if (mLockedUntil == null) {
return false;
}
var isLocked = new Time.Moment(Time.now().value()).lessThan(new Time.Moment(mLockedUntil));
if (!isLocked) {
mLockedUntil = null;
Application.Storage.deleteValue(STORAGE_KEY_LOCKED);
}
return isLocked;
}
} }