[mob][photos] Added time delay for invalid attempts

This commit is contained in:
Aman Raj Singh Mourya
2024-06-29 14:19:47 +05:30
parent b4e0eb8491
commit 3c86a9bc06
10 changed files with 217 additions and 78 deletions

View File

@@ -212,7 +212,7 @@ Future<void> _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();

View File

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

View File

@@ -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<LockScreenOption> {
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<LockScreenOption> {
final bool result = await Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {
return const LockScreenOptionPassword();
return const LockScreenPassword();
},
),
);

View File

@@ -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<LockScreenOptionConfirmPassword> createState() =>
_LockScreenOptionConfirmPasswordState();
State<LockScreenConfirmPassword> createState() =>
_LockScreenConfirmPasswordState();
}
class _LockScreenOptionConfirmPasswordState
extends State<LockScreenOptionConfirmPassword> {
class _LockScreenConfirmPasswordState extends State<LockScreenConfirmPassword> {
/// _confirmPasswordController is disposed by the [TextInputWidget]
final _confirmPasswordController = TextEditingController(text: null);

View File

@@ -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<LockScreenOptionConfirmPin> createState() =>
_LockScreenOptionConfirmPinState();
State<LockScreenConfirmPin> createState() => _LockScreenConfirmPinState();
}
class _LockScreenOptionConfirmPinState
extends State<LockScreenOptionConfirmPin> {
class _LockScreenConfirmPinState extends State<LockScreenConfirmPin> {
final _confirmPinController = TextEditingController(text: null);
final LockscreenSetting _lockscreenSetting = LockscreenSetting.instance;

View File

@@ -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<LockScreenOptionPassword> createState() =>
_LockScreenOptionPasswordState();
State<LockScreenPassword> createState() => _LockScreenPasswordState();
}
class _LockScreenOptionPasswordState extends State<LockScreenOptionPassword> {
class _LockScreenPasswordState extends State<LockScreenPassword> {
/// _passwordController is disposed by the [TextInputWidget]
final _passwordController = TextEditingController(text: null);
final _focusNode = FocusNode();
final _isFormValid = ValueNotifier<bool>(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<LockScreenOptionPassword> {
"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<LockScreenOptionPassword> {
} else {
await Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) => LockScreenOptionConfirmPassword(
builder: (BuildContext context) => LockScreenConfirmPassword(
password: _passwordController.text,
),
),

View File

@@ -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<LockScreenOptionPin> createState() => _LockScreenOptionPinState();
State<LockScreenPin> createState() => _LockScreenPinState();
}
class _LockScreenOptionPinState extends State<LockScreenOptionPin> {
class _LockScreenPinState extends State<LockScreenPin> {
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<LockScreenOptionPin> {
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<void> _confirmPin(String code) async {
@@ -85,8 +106,7 @@ class _LockScreenOptionPinState extends State<LockScreenOptionPin> {
} else {
await Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) =>
LockScreenOptionConfirmPin(pin: code),
builder: (BuildContext context) => LockScreenConfirmPin(pin: code),
),
);
_pinController.clear();

View File

@@ -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<LockScreen> 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<LockScreen> 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<LockScreen> with WidgetsBindingObserver {
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
Future<void> 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<LockScreen> with WidgetsBindingObserver {
super.dispose();
}
Future<void> 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<void> _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");
}

View File

@@ -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<bool> requestAuthentication(BuildContext context, String reason) async {
Future<bool> requestAuthentication(
BuildContext context,
String reason, {
bool isLockscreenAuth = false,
}) async {
Logger("AuthUtil").info("Requesting authentication");
await LocalAuthentication().stopAuthentication();
@@ -16,7 +20,12 @@ Future<bool> 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,

View File

@@ -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<void> setLastInvalidAttemptTime(int time) async {
await _preferences.setInt(lastInvalidAttemptTime, time);
}
int getlastInvalidAttemptTime() {
return _preferences.getInt(lastInvalidAttemptTime) ?? 0;
}
int getInvalidAttemptCount() {
return _preferences.getInt(keyInvalidAttempts) ?? 0;
}
Future<void> setInvalidAttemptCount(int count) async {
await _preferences.setInt(keyInvalidAttempts, count);
}
static Uint8List generateSalt() {