From 3c86a9bc0652b29dc9dde910181683ef0b960413 Mon Sep 17 00:00:00 2001 From: Aman Raj Singh Mourya Date: Sat, 29 Jun 2024 14:19:47 +0530 Subject: [PATCH] [mob][photos] Added time delay for invalid attempts --- mobile/lib/main.dart | 2 +- .../local_authentication_service.dart | 15 ++- .../lockscreen/lock_screen_option.dart | 8 +- ....dart => lockscreen_confirm_password.dart} | 11 +- ...m_pin.dart => lockscreen_confirm_pin.dart} | 10 +- ...password.dart => lockscreen_password.dart} | 35 +++-- ...en_option_pin.dart => lockscreen_pin.dart} | 56 +++++--- mobile/lib/ui/tools/lock_screen.dart | 121 ++++++++++++++---- mobile/lib/utils/auth_util.dart | 13 +- mobile/lib/utils/lockscreen_setting.dart | 24 +++- 10 files changed, 217 insertions(+), 78 deletions(-) rename mobile/lib/ui/settings/lockscreen/{lock_screen_option_confirm_password.dart => lockscreen_confirm_password.dart} (94%) rename mobile/lib/ui/settings/lockscreen/{lock_screen_option_confirm_pin.dart => lockscreen_confirm_pin.dart} (97%) rename mobile/lib/ui/settings/lockscreen/{lock_screen_option_password.dart => lockscreen_password.dart} (84%) rename mobile/lib/ui/settings/lockscreen/{lock_screen_option_pin.dart => lockscreen_pin.dart} (90%) diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 70a53224d9..ec49a844c5 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -212,7 +212,7 @@ Future _init(bool isBackground, {String via = ''}) async { CryptoUtil.init(); _logger.info("Lockscreen init"); - LockscreenSetting.instance.init(secureStorage); + LockscreenSetting.instance.init(secureStorage, preferences); _logger.info("Configuration init"); await Configuration.instance.init(); diff --git a/mobile/lib/services/local_authentication_service.dart b/mobile/lib/services/local_authentication_service.dart index 490b2b5dcc..73cc0e3701 100644 --- a/mobile/lib/services/local_authentication_service.dart +++ b/mobile/lib/services/local_authentication_service.dart @@ -3,8 +3,8 @@ import "dart:async"; import 'package:flutter/material.dart'; import 'package:local_auth/local_auth.dart'; import 'package:photos/core/configuration.dart'; -import "package:photos/ui/settings/lockscreen/lock_screen_option_password.dart"; -import "package:photos/ui/settings/lockscreen/lock_screen_option_pin.dart"; +import "package:photos/ui/settings/lockscreen/lockscreen_password.dart"; +import "package:photos/ui/settings/lockscreen/lockscreen_pin.dart"; import 'package:photos/ui/tools/app_lock.dart'; import 'package:photos/utils/auth_util.dart'; import 'package:photos/utils/dialog_util.dart'; @@ -38,14 +38,16 @@ class LocalAuthenticationService { Future requestEnteAuthForLockScreen( BuildContext context, String? savedPin, - String? savedPassword, - ) async { + String? savedPassword, { + bool isLockscreenAuth = false, + }) async { if (savedPassword != null) { final result = await Navigator.of(context).push( MaterialPageRoute( builder: (BuildContext context) { - return LockScreenOptionPassword( + return LockScreenPassword( isAuthenticating: true, + isLockscreenAuth: isLockscreenAuth, authPass: savedPassword, ); }, @@ -59,8 +61,9 @@ class LocalAuthenticationService { final result = await Navigator.of(context).push( MaterialPageRoute( builder: (BuildContext context) { - return LockScreenOptionPin( + return LockScreenPin( isAuthenticating: true, + isLockscreenAuth: isLockscreenAuth, authPin: savedPin, ); }, diff --git a/mobile/lib/ui/settings/lockscreen/lock_screen_option.dart b/mobile/lib/ui/settings/lockscreen/lock_screen_option.dart index d7d9c31f99..57e5529297 100644 --- a/mobile/lib/ui/settings/lockscreen/lock_screen_option.dart +++ b/mobile/lib/ui/settings/lockscreen/lock_screen_option.dart @@ -7,8 +7,8 @@ import "package:photos/ui/components/menu_item_widget/menu_item_widget.dart"; import "package:photos/ui/components/title_bar_title_widget.dart"; import "package:photos/ui/components/title_bar_widget.dart"; import "package:photos/ui/components/toggle_switch_widget.dart"; -import "package:photos/ui/settings/lockscreen/lock_screen_option_password.dart"; -import "package:photos/ui/settings/lockscreen/lock_screen_option_pin.dart"; +import "package:photos/ui/settings/lockscreen/lockscreen_password.dart"; +import "package:photos/ui/settings/lockscreen/lockscreen_pin.dart"; import "package:photos/ui/tools/app_lock.dart"; import "package:photos/utils/lockscreen_setting.dart"; @@ -53,7 +53,7 @@ class _LockScreenOptionState extends State { final bool result = await Navigator.of(context).push( MaterialPageRoute( builder: (BuildContext context) { - return const LockScreenOptionPin(); + return const LockScreenPin(); }, ), ); @@ -71,7 +71,7 @@ class _LockScreenOptionState extends State { final bool result = await Navigator.of(context).push( MaterialPageRoute( builder: (BuildContext context) { - return const LockScreenOptionPassword(); + return const LockScreenPassword(); }, ), ); diff --git a/mobile/lib/ui/settings/lockscreen/lock_screen_option_confirm_password.dart b/mobile/lib/ui/settings/lockscreen/lockscreen_confirm_password.dart similarity index 94% rename from mobile/lib/ui/settings/lockscreen/lock_screen_option_confirm_password.dart rename to mobile/lib/ui/settings/lockscreen/lockscreen_confirm_password.dart index 2cb22dac92..1ab2861c57 100644 --- a/mobile/lib/ui/settings/lockscreen/lock_screen_option_confirm_password.dart +++ b/mobile/lib/ui/settings/lockscreen/lockscreen_confirm_password.dart @@ -7,20 +7,19 @@ import "package:photos/ui/components/buttons/icon_button_widget.dart"; import "package:photos/ui/components/text_input_widget.dart"; import "package:photos/utils/lockscreen_setting.dart"; -class LockScreenOptionConfirmPassword extends StatefulWidget { - const LockScreenOptionConfirmPassword({ +class LockScreenConfirmPassword extends StatefulWidget { + const LockScreenConfirmPassword({ super.key, required this.password, }); final String password; @override - State createState() => - _LockScreenOptionConfirmPasswordState(); + State createState() => + _LockScreenConfirmPasswordState(); } -class _LockScreenOptionConfirmPasswordState - extends State { +class _LockScreenConfirmPasswordState extends State { /// _confirmPasswordController is disposed by the [TextInputWidget] final _confirmPasswordController = TextEditingController(text: null); diff --git a/mobile/lib/ui/settings/lockscreen/lock_screen_option_confirm_pin.dart b/mobile/lib/ui/settings/lockscreen/lockscreen_confirm_pin.dart similarity index 97% rename from mobile/lib/ui/settings/lockscreen/lock_screen_option_confirm_pin.dart rename to mobile/lib/ui/settings/lockscreen/lockscreen_confirm_pin.dart index 2f8e9a20a2..138fbd2808 100644 --- a/mobile/lib/ui/settings/lockscreen/lock_screen_option_confirm_pin.dart +++ b/mobile/lib/ui/settings/lockscreen/lockscreen_confirm_pin.dart @@ -7,16 +7,14 @@ import "package:photos/ui/components/buttons/icon_button_widget.dart"; import "package:photos/utils/lockscreen_setting.dart"; import "package:pinput/pinput.dart"; -class LockScreenOptionConfirmPin extends StatefulWidget { - const LockScreenOptionConfirmPin({super.key, required this.pin}); +class LockScreenConfirmPin extends StatefulWidget { + const LockScreenConfirmPin({super.key, required this.pin}); final String pin; @override - State createState() => - _LockScreenOptionConfirmPinState(); + State createState() => _LockScreenConfirmPinState(); } -class _LockScreenOptionConfirmPinState - extends State { +class _LockScreenConfirmPinState extends State { final _confirmPinController = TextEditingController(text: null); final LockscreenSetting _lockscreenSetting = LockscreenSetting.instance; diff --git a/mobile/lib/ui/settings/lockscreen/lock_screen_option_password.dart b/mobile/lib/ui/settings/lockscreen/lockscreen_password.dart similarity index 84% rename from mobile/lib/ui/settings/lockscreen/lock_screen_option_password.dart rename to mobile/lib/ui/settings/lockscreen/lockscreen_password.dart index 396db510ca..f9ca2f7d05 100644 --- a/mobile/lib/ui/settings/lockscreen/lock_screen_option_password.dart +++ b/mobile/lib/ui/settings/lockscreen/lockscreen_password.dart @@ -8,37 +8,41 @@ import "package:photos/theme/ente_theme.dart"; import "package:photos/ui/common/dynamic_fab.dart"; import "package:photos/ui/components/buttons/icon_button_widget.dart"; import "package:photos/ui/components/text_input_widget.dart"; -import "package:photos/ui/settings/lockscreen/lock_screen_option_confirm_password.dart"; +import "package:photos/ui/settings/lockscreen/lockscreen_confirm_password.dart"; import "package:photos/utils/crypto_util.dart"; import "package:photos/utils/lockscreen_setting.dart"; -class LockScreenOptionPassword extends StatefulWidget { - const LockScreenOptionPassword({ +class LockScreenPassword extends StatefulWidget { + const LockScreenPassword({ super.key, this.isAuthenticating = false, + this.isLockscreenAuth = false, this.authPass, }); - /// If [isAuthenticating] is true then we are authenticating the user + /// If [isLockscreenAuth] is true then we are authenticating the user at Lock screen + /// If [isAuthenticating] is true then we are authenticating the user at Setting screen final bool isAuthenticating; + final bool isLockscreenAuth; final String? authPass; @override - State createState() => - _LockScreenOptionPasswordState(); + State createState() => _LockScreenPasswordState(); } -class _LockScreenOptionPasswordState extends State { +class _LockScreenPasswordState extends State { /// _passwordController is disposed by the [TextInputWidget] final _passwordController = TextEditingController(text: null); final _focusNode = FocusNode(); final _isFormValid = ValueNotifier(false); final _submitNotifier = ValueNotifier(false); + int invalidAttemptsCount = 0; final LockscreenSetting _lockscreenSetting = LockscreenSetting.instance; - late String hashedPassword; + late String enteredHashedPassword; @override void initState() { super.initState(); + invalidAttemptsCount = _lockscreenSetting.getInvalidAttemptCount(); WidgetsBinding.instance.addPostFrameCallback((_) async { _focusNode.requestFocus(); }); @@ -61,11 +65,20 @@ class _LockScreenOptionPasswordState extends State { "memLimit": Sodium.cryptoPwhashMemlimitInteractive, }); - hashedPassword = base64Encode(hash); - if (widget.authPass == hashedPassword) { + enteredHashedPassword = base64Encode(hash); + if (widget.authPass == enteredHashedPassword) { + await _lockscreenSetting.setInvalidAttemptCount(0); Navigator.of(context).pop(true); return true; } else { + if (widget.isLockscreenAuth) { + invalidAttemptsCount++; + if (invalidAttemptsCount > 4) { + await _lockscreenSetting.setInvalidAttemptCount(invalidAttemptsCount); + Navigator.of(context).pop(false); + } + } + await HapticFeedback.vibrate(); throw Exception("Incorrect password"); } @@ -78,7 +91,7 @@ class _LockScreenOptionPasswordState extends State { } else { await Navigator.of(context).push( MaterialPageRoute( - builder: (BuildContext context) => LockScreenOptionConfirmPassword( + builder: (BuildContext context) => LockScreenConfirmPassword( password: _passwordController.text, ), ), diff --git a/mobile/lib/ui/settings/lockscreen/lock_screen_option_pin.dart b/mobile/lib/ui/settings/lockscreen/lockscreen_pin.dart similarity index 90% rename from mobile/lib/ui/settings/lockscreen/lock_screen_option_pin.dart rename to mobile/lib/ui/settings/lockscreen/lockscreen_pin.dart index e64e52f284..a64936ee89 100644 --- a/mobile/lib/ui/settings/lockscreen/lock_screen_option_pin.dart +++ b/mobile/lib/ui/settings/lockscreen/lockscreen_pin.dart @@ -7,31 +7,41 @@ import "package:photos/theme/colors.dart"; import "package:photos/theme/ente_theme.dart"; import "package:photos/theme/text_style.dart"; import "package:photos/ui/components/buttons/icon_button_widget.dart"; -import "package:photos/ui/settings/lockscreen/lock_screen_option_confirm_pin.dart"; +import "package:photos/ui/settings/lockscreen/lockscreen_confirm_pin.dart"; import "package:photos/utils/crypto_util.dart"; import "package:photos/utils/lockscreen_setting.dart"; import 'package:pinput/pinput.dart'; -class LockScreenOptionPin extends StatefulWidget { - const LockScreenOptionPin({ +class LockScreenPin extends StatefulWidget { + const LockScreenPin({ super.key, this.isAuthenticating = false, + this.isLockscreenAuth = false, this.authPin, }); - /// If [isAuthenticating] is true then we are authenticating the user + /// If [isLockscreenAuth] is true then we are authenticating the user at the Lock screen + /// If [isAuthenticating] is true then we are authenticating the user at the Setting screen final bool isAuthenticating; + final bool isLockscreenAuth; final String? authPin; @override - State createState() => _LockScreenOptionPinState(); + State createState() => _LockScreenPinState(); } -class _LockScreenOptionPinState extends State { +class _LockScreenPinState extends State { final _pinController = TextEditingController(text: null); final LockscreenSetting _lockscreenSetting = LockscreenSetting.instance; late String enteredHashedPin; bool isPinValid = false; + int invalidAttemptsCount = 0; + + @override + void initState() { + super.initState(); + invalidAttemptsCount = _lockscreenSetting.getInvalidAttemptCount(); + } @override void dispose() { @@ -63,19 +73,30 @@ class _LockScreenOptionPinState extends State { enteredHashedPin = base64Encode(hash); if (widget.authPin == enteredHashedPin) { + invalidAttemptsCount = 0; + await _lockscreenSetting.setInvalidAttemptCount(0); Navigator.of(context).pop(true); return true; + } else { + setState(() { + isPinValid = true; + }); + await HapticFeedback.vibrate(); + await Future.delayed(const Duration(milliseconds: 75)); + _pinController.clear(); + setState(() { + isPinValid = false; + }); + + if (widget.isLockscreenAuth) { + invalidAttemptsCount++; + if (invalidAttemptsCount > 4) { + await _lockscreenSetting.setInvalidAttemptCount(invalidAttemptsCount); + Navigator.of(context).pop(false); + } + } + return false; } - setState(() { - isPinValid = true; - }); - await HapticFeedback.vibrate(); - await Future.delayed(const Duration(milliseconds: 75)); - _pinController.clear(); - setState(() { - isPinValid = false; - }); - return false; } Future _confirmPin(String code) async { @@ -85,8 +106,7 @@ class _LockScreenOptionPinState extends State { } else { await Navigator.of(context).push( MaterialPageRoute( - builder: (BuildContext context) => - LockScreenOptionConfirmPin(pin: code), + builder: (BuildContext context) => LockScreenConfirmPin(pin: code), ), ); _pinController.clear(); diff --git a/mobile/lib/ui/tools/lock_screen.dart b/mobile/lib/ui/tools/lock_screen.dart index f5f0696a3c..91e2bbfd4d 100644 --- a/mobile/lib/ui/tools/lock_screen.dart +++ b/mobile/lib/ui/tools/lock_screen.dart @@ -1,4 +1,6 @@ +import "dart:async"; import "dart:io"; +import "dart:math"; import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; @@ -6,6 +8,7 @@ import "package:photos/l10n/l10n.dart"; import 'package:photos/ui/common/gradient_button.dart'; import 'package:photos/ui/tools/app_lock.dart'; import 'package:photos/utils/auth_util.dart'; +import "package:photos/utils/lockscreen_setting.dart"; class LockScreen extends StatefulWidget { const LockScreen({Key? key}) : super(key: key); @@ -20,11 +23,17 @@ class _LockScreenState extends State with WidgetsBindingObserver { bool _hasPlacedAppInBackground = false; bool _hasAuthenticationFailed = false; int? lastAuthenticatingTime; + bool isTimerRunning = false; + int lockedTime = 0; + int invalidAttemptCount = 0; + int remainingTime = 0; + final _lockscreenSetting = LockscreenSetting.instance; @override void initState() { _logger.info("initiatingState"); super.initState(); + invalidAttemptCount = _lockscreenSetting.getInvalidAttemptCount(); WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addPostFrameCallback((timeStamp) { if (isNonMobileIOSDevice()) { @@ -53,11 +62,14 @@ class _LockScreenState extends State with WidgetsBindingObserver { SizedBox( width: 180, child: GradientButton( - text: context.l10n.unlock, + text: isTimerRunning + ? formatTime(remainingTime) + : context.l10n.unlock, iconData: Icons.lock_open_outlined, onTap: () async { - // ignore: unawaited_futures - _showLockScreen(source: "tapUnlock"); + if (!isTimerRunning) { + await _showLockScreen(source: "tapUnlock"); + } }, ), ), @@ -78,32 +90,38 @@ class _LockScreenState extends State with WidgetsBindingObserver { } @override - void didChangeAppLifecycleState(AppLifecycleState state) { + Future didChangeAppLifecycleState(AppLifecycleState state) async { _logger.info(state.toString()); if (state == AppLifecycleState.resumed && !_isShowingLockScreen) { - // This is triggered either when the lock screen is dismissed or when - // the app is brought to foreground _hasPlacedAppInBackground = false; final bool didAuthInLast5Seconds = lastAuthenticatingTime != null && DateTime.now().millisecondsSinceEpoch - lastAuthenticatingTime! < 5000; + if (!_hasAuthenticationFailed && !didAuthInLast5Seconds) { - // Show the lock screen again only if the app is resuming from the - // background, and not when the lock screen was explicitly dismissed - Future.delayed( - Duration.zero, - () => _showLockScreen(source: "lifeCycle"), - ); + if (_lockscreenSetting.getlastInvalidAttemptTime() > + DateTime.now().millisecondsSinceEpoch && + !_isShowingLockScreen) { + final int time = (_lockscreenSetting.getlastInvalidAttemptTime() - + DateTime.now().millisecondsSinceEpoch) ~/ + 1000; + + Future.delayed( + Duration.zero, + () { + startLockTimer(time); + _showLockScreen(source: "lifeCycle"); + }, + ); + } } else { - _hasAuthenticationFailed = false; // Reset failure state + _hasAuthenticationFailed = false; } } else if (state == AppLifecycleState.paused || state == AppLifecycleState.inactive) { - // This is triggered either when the lock screen pops up or when - // the app is pushed to background if (!_isShowingLockScreen) { _hasPlacedAppInBackground = true; - _hasAuthenticationFailed = false; // reset failure state + _hasAuthenticationFailed = false; } } } @@ -115,24 +133,83 @@ class _LockScreenState extends State with WidgetsBindingObserver { super.dispose(); } + Future startLockTimer(int time) async { + if (isTimerRunning) { + return; + } + + setState(() { + isTimerRunning = true; + remainingTime = time; + }); + + while (remainingTime > 0) { + await Future.delayed(const Duration(seconds: 1)); + setState(() { + remainingTime--; + }); + } + + setState(() { + isTimerRunning = false; + }); + } + + String formatTime(int seconds) { + final int hours = seconds ~/ 3600; + final int minutes = (seconds % 3600) ~/ 60; + final int remainingSeconds = seconds % 60; + + if (hours > 0) { + return "$hours hr $minutes min"; + } else if (minutes > 0) { + return "$minutes min $remainingSeconds sec"; + } else { + return "$remainingSeconds sec"; + } + } + Future _showLockScreen({String source = ''}) async { final int id = DateTime.now().millisecondsSinceEpoch; _logger.info("Showing lock screen $source $id"); try { + if (id < _lockscreenSetting.getlastInvalidAttemptTime() && + !_isShowingLockScreen) { + final int time = + (_lockscreenSetting.getlastInvalidAttemptTime() - id) ~/ 1000; + + await startLockTimer(time); + } _isShowingLockScreen = true; - final result = await requestAuthentication( - context, - context.l10n.authToViewYourMemories, - ); + final result = isTimerRunning + ? false + : await requestAuthentication( + context, + context.l10n.authToViewYourMemories, + isLockscreenAuth: true, + ); _logger.finest("LockScreen Result $result $id"); _isShowingLockScreen = false; if (result) { lastAuthenticatingTime = DateTime.now().millisecondsSinceEpoch; AppLock.of(context)!.didUnlock(); + await _lockscreenSetting.setInvalidAttemptCount(0); + setState(() { + lockedTime = 15; + isTimerRunning = false; + }); } else { if (!_hasPlacedAppInBackground) { - // Treat this as a failure only if user did not explicitly - // put the app in background + if (_lockscreenSetting.getInvalidAttemptCount() > 4 && + invalidAttemptCount != + _lockscreenSetting.getInvalidAttemptCount()) { + invalidAttemptCount = _lockscreenSetting.getInvalidAttemptCount(); + lockedTime = pow(2, invalidAttemptCount - 5).toInt() * 30; + await _lockscreenSetting.setLastInvalidAttemptTime( + DateTime.now().millisecondsSinceEpoch + lockedTime * 1000, + ); + await startLockTimer(lockedTime); + } _hasAuthenticationFailed = true; _logger.info("Authentication failed"); } diff --git a/mobile/lib/utils/auth_util.dart b/mobile/lib/utils/auth_util.dart index b105376f7a..0fa14db811 100644 --- a/mobile/lib/utils/auth_util.dart +++ b/mobile/lib/utils/auth_util.dart @@ -7,7 +7,11 @@ import "package:photos/generated/l10n.dart"; import "package:photos/services/local_authentication_service.dart"; import "package:photos/utils/lockscreen_setting.dart"; -Future requestAuthentication(BuildContext context, String reason) async { +Future requestAuthentication( + BuildContext context, + String reason, { + bool isLockscreenAuth = false, +}) async { Logger("AuthUtil").info("Requesting authentication"); await LocalAuthentication().stopAuthentication(); @@ -16,7 +20,12 @@ Future requestAuthentication(BuildContext context, String reason) async { final String? savedPassword = await lockscreenSetting.getPassword(); if (savedPassword != null || savedPin != null) { return await LocalAuthenticationService.instance - .requestEnteAuthForLockScreen(context, savedPin, savedPassword); + .requestEnteAuthForLockScreen( + context, + savedPin, + savedPassword, + isLockscreenAuth: isLockscreenAuth, + ); } else { return await LocalAuthentication().authenticate( localizedReason: reason, diff --git a/mobile/lib/utils/lockscreen_setting.dart b/mobile/lib/utils/lockscreen_setting.dart index fc5e435700..988ac3624a 100644 --- a/mobile/lib/utils/lockscreen_setting.dart +++ b/mobile/lib/utils/lockscreen_setting.dart @@ -4,6 +4,7 @@ import "package:flutter/foundation.dart"; import "package:flutter_secure_storage/flutter_secure_storage.dart"; import "package:flutter_sodium/flutter_sodium.dart"; import "package:photos/utils/crypto_util.dart"; +import "package:shared_preferences/shared_preferences.dart"; class LockscreenSetting { LockscreenSetting._privateConstructor(); @@ -13,11 +14,30 @@ class LockscreenSetting { static const password = "user_pass"; static const pin = "user_pin"; static const saltKey = "user_salt"; - + static const keyInvalidAttempts = "invalid_attempts"; + static const lastInvalidAttemptTime = "last_invalid_attempt_time"; late FlutterSecureStorage _secureStorage; + late SharedPreferences _preferences; - void init(FlutterSecureStorage secureStorage) { + void init(FlutterSecureStorage secureStorage, SharedPreferences prefs) async { _secureStorage = secureStorage; + _preferences = prefs; + } + + Future setLastInvalidAttemptTime(int time) async { + await _preferences.setInt(lastInvalidAttemptTime, time); + } + + int getlastInvalidAttemptTime() { + return _preferences.getInt(lastInvalidAttemptTime) ?? 0; + } + + int getInvalidAttemptCount() { + return _preferences.getInt(keyInvalidAttempts) ?? 0; + } + + Future setInvalidAttemptCount(int count) async { + await _preferences.setInt(keyInvalidAttempts, count); } static Uint8List generateSalt() {