[mob][auth] Implemented Lock screen

This commit is contained in:
Aman Raj Singh Mourya
2024-07-04 16:55:33 +05:30
parent 8c79aedd19
commit 9982c73d5a
16 changed files with 1840 additions and 77 deletions

View File

@@ -13,6 +13,7 @@ import 'package:ente_auth/models/key_attributes.dart';
import 'package:ente_auth/models/key_gen_result.dart';
import 'package:ente_auth/models/private_key_attributes.dart';
import 'package:ente_auth/store/authenticator_db.dart';
import 'package:ente_auth/utils/lock_screen_settings.dart';
import 'package:ente_crypto_dart/ente_crypto_dart.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:logging/logging.dart';
@@ -469,7 +470,13 @@ class Configuration {
await _preferences.setBool(hasOptedForOfflineModeKey, true);
}
bool shouldShowLockScreen() {
Future<bool> shouldShowLockScreen() async {
final bool isPin = await LockScreenSettings.instance.isPinSet();
final bool isPass = await LockScreenSettings.instance.isPasswordSet();
return isPin || isPass || shouldShowSystemLockScreen();
}
bool shouldShowSystemLockScreen() {
if (_preferences.containsKey(keyShouldShowLockScreen)) {
return _preferences.getBool(keyShouldShowLockScreen)!;
} else {
@@ -477,7 +484,7 @@ class Configuration {
}
}
Future<void> setShouldShowLockScreen(bool value) {
Future<void> setSystemLockScreen(bool value) {
return _preferences.setBool(keyShouldShowLockScreen, value);
}

View File

@@ -22,6 +22,7 @@ import 'package:ente_auth/store/code_store.dart';
import 'package:ente_auth/ui/tools/app_lock.dart';
import 'package:ente_auth/ui/tools/lock_screen.dart';
import 'package:ente_auth/ui/utils/icon_utils.dart';
import 'package:ente_auth/utils/lock_screen_settings.dart';
import 'package:ente_auth/utils/platform_util.dart';
import 'package:ente_auth/utils/window_protocol_handler.dart';
import 'package:ente_crypto_dart/ente_crypto_dart.dart';
@@ -114,7 +115,7 @@ Future<void> _runInForeground() async {
AppLock(
builder: (args) => App(locale: locale),
lockScreen: const LockScreen(),
enabled: Configuration.instance.shouldShowLockScreen(),
enabled: await Configuration.instance.shouldShowLockScreen(),
locale: locale,
lightTheme: lightThemeData,
darkTheme: darkThemeData,
@@ -173,6 +174,7 @@ Future<void> _init(bool bool, {String? via}) async {
await NotificationService.instance.init();
await UpdateService.instance.init();
await IconUtils.instance.init();
await LockScreenSettings.instance.init();
}
Future<void> _setupPrivacyScreen() async {

View File

@@ -1,6 +1,8 @@
import 'dart:io';
import 'package:ente_auth/core/configuration.dart';
import 'package:ente_auth/ui/settings/lock_screen/lock_screen_password.dart';
import 'package:ente_auth/ui/settings/lock_screen/lock_screen_pin.dart';
import 'package:ente_auth/ui/tools/app_lock.dart';
import 'package:ente_auth/utils/auth_util.dart';
import 'package:ente_auth/utils/dialog_util.dart';
@@ -25,7 +27,7 @@ class LocalAuthenticationService {
AppLock.of(context)!.setEnabled(false);
final result = await requestAuthentication(context, infoMessage);
AppLock.of(context)!.setEnabled(
Configuration.instance.shouldShowLockScreen(),
await Configuration.instance.shouldShowLockScreen(),
);
if (!result) {
showToast(context, infoMessage);
@@ -37,6 +39,47 @@ class LocalAuthenticationService {
return true;
}
Future<bool> requestEnteAuthForLockScreen(
BuildContext context,
String? savedPin,
String? savedPassword, {
bool isOnOpeningApp = false,
}) async {
if (savedPassword != null) {
final result = await Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {
return LockScreenPassword(
isAuthenticating: true,
isOnOpeningApp: isOnOpeningApp,
authPass: savedPassword,
);
},
),
);
if (result) {
return true;
}
}
if (savedPin != null) {
final result = await Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {
return LockScreenPin(
isAuthenticating: true,
isOnOpeningApp: isOnOpeningApp,
authPin: savedPin,
);
},
),
);
if (result) {
return true;
}
}
return false;
}
Future<bool> requestLocalAuthForLockScreen(
BuildContext context,
bool shouldEnableLockScreen,
@@ -53,11 +96,11 @@ class LocalAuthenticationService {
if (result) {
AppLock.of(context)!.setEnabled(shouldEnableLockScreen);
await Configuration.instance
.setShouldShowLockScreen(shouldEnableLockScreen);
.setSystemLockScreen(shouldEnableLockScreen);
return true;
} else {
AppLock.of(context)!
.setEnabled(Configuration.instance.shouldShowLockScreen());
.setEnabled(await Configuration.instance.shouldShowLockScreen());
}
} else {
// ignore: unawaited_futures

View File

@@ -6,6 +6,7 @@ import 'package:ente_auth/ui/components/separators.dart';
import 'package:ente_auth/utils/debouncer.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:logging/logging.dart';
class TextInputWidget extends StatefulWidget {
final String? label;
@@ -58,6 +59,7 @@ class TextInputWidget extends StatefulWidget {
}
class _TextInputWidgetState extends State<TextInputWidget> {
final _logger = Logger("TextInputWidget");
ExecutionState executionState = ExecutionState.idle;
final _textController = TextEditingController();
final _debouncer = Debouncer(const Duration(milliseconds: 300));
@@ -66,7 +68,7 @@ class _TextInputWidgetState extends State<TextInputWidget> {
///This is to pass if the TextInputWidget is in a dialog and an error is
///thrown in executing onSubmit by passing it as arg in Navigator.pop()
Exception? _exception;
bool _incorrectPassword = false;
@override
void initState() {
widget.submitNotifier?.addListener(_onSubmit);
@@ -138,7 +140,11 @@ class _TextInputWidgetState extends State<TextInputWidget> {
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: colorScheme.strokeFaint),
borderSide: BorderSide(
color: _incorrectPassword
? const Color.fromRGBO(245, 42, 42, 1)
: colorScheme.strokeFaint,
),
borderRadius: BorderRadius.circular(8),
),
suffixIcon: Padding(
@@ -233,6 +239,10 @@ class _TextInputWidgetState extends State<TextInputWidget> {
executionState = ExecutionState.error;
_debouncer.cancelDebounce();
_exception = e as Exception;
if (e.toString().contains("Incorrect password")) {
_logger.warning("Incorrect password");
_surfaceWrongPasswordState();
}
if (!widget.popNavAfterSubmission) {
rethrow;
}
@@ -306,6 +316,20 @@ class _TextInputWidgetState extends State<TextInputWidget> {
void _popNavigatorStack(BuildContext context, {Exception? e}) {
Navigator.of(context).canPop() ? Navigator.of(context).pop(e) : null;
}
void _surfaceWrongPasswordState() {
setState(() {
_incorrectPassword = true;
HapticFeedback.vibrate();
Future.delayed(const Duration(seconds: 1), () {
if (mounted) {
setState(() {
_incorrectPassword = false;
});
}
});
});
}
}
//todo: Add clear and custom icon for suffic icon

View File

@@ -0,0 +1,196 @@
import "package:ente_auth/theme/ente_theme.dart";
import "package:flutter/material.dart";
class CustomPinKeypad extends StatelessWidget {
final TextEditingController controller;
const CustomPinKeypad({required this.controller, super.key});
@override
Widget build(BuildContext context) {
return SafeArea(
child: Container(
padding: const EdgeInsets.all(2),
color: getEnteColorScheme(context).strokeFainter,
child: Column(
children: [
Row(
children: [
_Button(
text: '',
number: '1',
onTap: () {
_onKeyTap('1');
},
),
_Button(
text: "ABC",
number: '2',
onTap: () {
_onKeyTap('2');
},
),
_Button(
text: "DEF",
number: '3',
onTap: () {
_onKeyTap('3');
},
),
],
),
Row(
children: [
_Button(
number: '4',
text: "GHI",
onTap: () {
_onKeyTap('4');
},
),
_Button(
number: '5',
text: 'JKL',
onTap: () {
_onKeyTap('5');
},
),
_Button(
number: '6',
text: 'MNO',
onTap: () {
_onKeyTap('6');
},
),
],
),
Row(
children: [
_Button(
number: '7',
text: 'PQRS',
onTap: () {
_onKeyTap('7');
},
),
_Button(
number: '8',
text: 'TUV',
onTap: () {
_onKeyTap('8');
},
),
_Button(
number: '9',
text: 'WXYZ',
onTap: () {
_onKeyTap('9');
},
),
],
),
Row(
children: [
const _Button(
number: '',
text: '',
muteButton: true,
onTap: null,
),
_Button(
number: '0',
text: '',
onTap: () {
_onKeyTap('0');
},
),
_Button(
number: '',
text: '',
icon: const Icon(Icons.backspace_outlined),
onTap: () {
_onBackspace();
},
),
],
),
],
),
),
);
}
void _onKeyTap(String number) {
controller.text += number;
return;
}
void _onBackspace() {
if (controller.text.isNotEmpty) {
controller.text =
controller.text.substring(0, controller.text.length - 1);
}
return;
}
}
class _Button extends StatelessWidget {
final String number;
final String text;
final VoidCallback? onTap;
final bool muteButton;
final Widget? icon;
const _Button({
required this.number,
required this.text,
this.muteButton = false,
required this.onTap,
this.icon,
});
@override
Widget build(BuildContext context) {
final colorScheme = getEnteColorScheme(context);
final textTheme = getEnteTextTheme(context);
return Expanded(
child: GestureDetector(
onTap: onTap,
child: Container(
margin: const EdgeInsets.all(4),
decoration: BoxDecoration(
shape: BoxShape.rectangle,
borderRadius: BorderRadius.circular(6),
color: muteButton
? colorScheme.fillFaintPressed
: icon == null
? colorScheme.backgroundElevated2
: null,
),
child: Center(
child: muteButton
? const SizedBox.shrink()
: icon != null
? Container(
child: icon,
)
: Container(
padding: const EdgeInsets.all(4),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
number,
style: textTheme.h3,
),
Text(
text,
style: textTheme.tinyBold,
),
],
),
),
),
),
),
);
}
}

View File

@@ -0,0 +1,185 @@
import "package:ente_auth/theme/ente_theme.dart";
import "package:ente_auth/ui/common/dynamic_fab.dart";
import "package:ente_auth/ui/components/buttons/icon_button_widget.dart";
import "package:ente_auth/ui/components/text_input_widget.dart";
import "package:ente_auth/utils/lock_screen_settings.dart";
import "package:flutter/material.dart";
import "package:flutter/services.dart";
class LockScreenConfirmPassword extends StatefulWidget {
const LockScreenConfirmPassword({
super.key,
required this.password,
});
final String password;
@override
State<LockScreenConfirmPassword> createState() =>
_LockScreenConfirmPasswordState();
}
class _LockScreenConfirmPasswordState extends State<LockScreenConfirmPassword> {
final _confirmPasswordController = TextEditingController(text: null);
final LockScreenSettings _lockscreenSetting = LockScreenSettings.instance;
final _focusNode = FocusNode();
final _isFormValid = ValueNotifier<bool>(false);
final _submitNotifier = ValueNotifier(false);
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async {
_focusNode.requestFocus();
});
}
@override
void dispose() {
_submitNotifier.dispose();
_focusNode.dispose();
_isFormValid.dispose();
_confirmPasswordController.dispose();
super.dispose();
}
Future<void> _confirmPasswordMatch() async {
if (widget.password == _confirmPasswordController.text) {
await _lockscreenSetting.setPassword(_confirmPasswordController.text);
Navigator.of(context).pop(true);
Navigator.of(context).pop(true);
return;
}
await HapticFeedback.vibrate();
throw Exception("Incorrect password");
}
@override
Widget build(BuildContext context) {
final colorTheme = getEnteColorScheme(context);
final textTheme = getEnteTextTheme(context);
final isKeypadOpen = MediaQuery.viewInsetsOf(context).bottom > 100;
FloatingActionButtonLocation? fabLocation() {
if (isKeypadOpen) {
return null;
} else {
return FloatingActionButtonLocation.centerFloat;
}
}
return Scaffold(
resizeToAvoidBottomInset: isKeypadOpen,
appBar: AppBar(
elevation: 0,
leading: IconButton(
onPressed: () {
FocusScope.of(context).unfocus();
Navigator.of(context).pop();
},
icon: Icon(
Icons.arrow_back,
color: colorTheme.textBase,
),
),
),
floatingActionButton: ValueListenableBuilder<bool>(
valueListenable: _isFormValid,
builder: (context, isFormValid, child) {
return DynamicFAB(
isKeypadOpen: isKeypadOpen,
buttonText: "Confirm",
isFormValid: isFormValid,
onPressedFunction: () async {
_submitNotifier.value = !_submitNotifier.value;
},
);
},
),
floatingActionButtonLocation: fabLocation(),
floatingActionButtonAnimator: NoScalingAnimation(),
body: SingleChildScrollView(
child: Center(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
height: 120,
width: 120,
child: Stack(
alignment: Alignment.center,
children: [
Container(
width: 82,
height: 82,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
colors: [
Colors.grey.shade500.withOpacity(0.2),
Colors.grey.shade50.withOpacity(0.1),
Colors.grey.shade400.withOpacity(0.2),
Colors.grey.shade300.withOpacity(0.4),
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: Padding(
padding: const EdgeInsets.all(1.0),
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: colorTheme.backgroundBase,
),
),
),
),
SizedBox(
height: 75,
width: 75,
child: CircularProgressIndicator(
backgroundColor: colorTheme.fillFaintPressed,
value: 1,
strokeWidth: 1.5,
),
),
IconButtonWidget(
icon: Icons.lock,
iconButtonType: IconButtonType.primary,
iconColor: colorTheme.textBase,
),
],
),
),
Text(
"Re-enter Password",
style: textTheme.bodyBold,
),
const Padding(padding: EdgeInsets.all(12)),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: TextInputWidget(
hintText: "Confirm Password",
autoFocus: true,
textCapitalization: TextCapitalization.none,
isPasswordInput: true,
shouldSurfaceExecutionStates: false,
onChange: (p0) {
_confirmPasswordController.text = p0;
_isFormValid.value =
_confirmPasswordController.text.isNotEmpty;
},
onSubmit: (p0) {
return _confirmPasswordMatch();
},
submitNotifier: _submitNotifier,
),
),
const Padding(padding: EdgeInsets.all(12)),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,206 @@
import "package:ente_auth/theme/ente_theme.dart";
import "package:ente_auth/ui/settings/lock_screen/custom_pin_keypad.dart";
import "package:ente_auth/utils/lock_screen_settings.dart";
import "package:flutter/material.dart";
import "package:flutter/services.dart";
import "package:pinput/pinput.dart";
class LockScreenConfirmPin extends StatefulWidget {
const LockScreenConfirmPin({super.key, required this.pin});
final String pin;
@override
State<LockScreenConfirmPin> createState() => _LockScreenConfirmPinState();
}
class _LockScreenConfirmPinState extends State<LockScreenConfirmPin> {
final _confirmPinController = TextEditingController(text: null);
bool isConfirmPinValid = false;
final LockScreenSettings _lockscreenSetting = LockScreenSettings.instance;
final _pinPutDecoration = PinTheme(
height: 48,
width: 48,
padding: const EdgeInsets.only(top: 6.0),
decoration: BoxDecoration(
border: Border.all(color: const Color.fromRGBO(45, 194, 98, 1.0)),
borderRadius: BorderRadius.circular(15.0),
),
);
@override
void dispose() {
super.dispose();
_confirmPinController.dispose();
}
Future<void> _confirmPinMatch() async {
if (widget.pin == _confirmPinController.text) {
await _lockscreenSetting.setPin(_confirmPinController.text);
Navigator.of(context).pop(true);
Navigator.of(context).pop(true);
return;
}
setState(() {
isConfirmPinValid = true;
});
await HapticFeedback.vibrate();
await Future.delayed(const Duration(milliseconds: 75));
_confirmPinController.clear();
setState(() {
isConfirmPinValid = false;
});
}
@override
Widget build(BuildContext context) {
final colorTheme = getEnteColorScheme(context);
final textTheme = getEnteTextTheme(context);
return Scaffold(
appBar: AppBar(
elevation: 0,
leading: IconButton(
onPressed: () {
Navigator.of(context).pop(false);
},
icon: Icon(
Icons.arrow_back,
color: colorTheme.textBase,
),
),
),
body: OrientationBuilder(
builder: (context, orientation) {
return orientation == Orientation.portrait
? _getBody(colorTheme, textTheme, isPortrait: true)
: SingleChildScrollView(
child: _getBody(colorTheme, textTheme, isPortrait: false),
);
},
),
);
}
Widget _getBody(colorTheme, textTheme, {required bool isPortrait}) {
return Center(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
height: 120,
width: 120,
child: Stack(
alignment: Alignment.center,
children: [
Container(
width: 82,
height: 82,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
colors: [
Colors.grey.shade500.withOpacity(0.2),
Colors.grey.shade50.withOpacity(0.1),
Colors.grey.shade400.withOpacity(0.2),
Colors.grey.shade300.withOpacity(0.4),
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: Padding(
padding: const EdgeInsets.all(1.0),
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: colorTheme.backgroundBase,
),
),
),
),
SizedBox(
height: 75,
width: 75,
child: ValueListenableBuilder(
valueListenable: _confirmPinController,
builder: (context, value, child) {
return TweenAnimationBuilder<double>(
tween: Tween<double>(
begin: 0,
end: _confirmPinController.text.length / 4,
),
curve: Curves.ease,
duration: const Duration(milliseconds: 250),
builder: (context, value, _) =>
CircularProgressIndicator(
backgroundColor: colorTheme.fillFaintPressed,
value: value,
color: colorTheme.primary400,
strokeWidth: 1.5,
),
);
},
),
),
Icon(
Icons.lock,
color: colorTheme.textBase,
size: 30,
),
],
),
),
Text(
"Re-enter PIN",
style: textTheme.bodyBold,
),
const Padding(padding: EdgeInsets.all(12)),
Pinput(
length: 4,
showCursor: false,
useNativeKeyboard: false,
controller: _confirmPinController,
defaultPinTheme: _pinPutDecoration,
submittedPinTheme: _pinPutDecoration.copyWith(
textStyle: textTheme.h3Bold,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10.0),
border: Border.all(
color: colorTheme.fillBase,
),
),
),
followingPinTheme: _pinPutDecoration.copyWith(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10.0),
border: Border.all(
color: colorTheme.fillMuted,
),
),
),
focusedPinTheme: _pinPutDecoration,
errorPinTheme: _pinPutDecoration.copyWith(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10.0),
border: Border.all(
color: colorTheme.warning400,
),
),
),
errorText: '',
obscureText: true,
obscuringCharacter: '*',
forceErrorState: isConfirmPinValid,
onCompleted: (value) async {
await _confirmPinMatch();
},
),
isPortrait
? const Spacer()
: const Padding(padding: EdgeInsets.all(12)),
CustomPinKeypad(controller: _confirmPinController),
],
),
);
}
}

View File

@@ -0,0 +1,224 @@
import "package:ente_auth/core/configuration.dart";
import "package:ente_auth/theme/ente_theme.dart";
import "package:ente_auth/ui/components/captioned_text_widget.dart";
import "package:ente_auth/ui/components/divider_widget.dart";
import "package:ente_auth/ui/components/menu_item_widget.dart";
import "package:ente_auth/ui/components/title_bar_title_widget.dart";
import "package:ente_auth/ui/components/title_bar_widget.dart";
import "package:ente_auth/ui/components/toggle_switch_widget.dart";
import "package:ente_auth/ui/settings/lock_screen/lock_screen_password.dart";
import "package:ente_auth/ui/settings/lock_screen/lock_screen_pin.dart";
import "package:ente_auth/ui/tools/app_lock.dart";
import "package:ente_auth/utils/lock_screen_settings.dart";
import "package:flutter/material.dart";
class LockScreenOptions extends StatefulWidget {
const LockScreenOptions({super.key});
@override
State<LockScreenOptions> createState() => _LockScreenOptionsState();
}
class _LockScreenOptionsState extends State<LockScreenOptions> {
final Configuration _configuration = Configuration.instance;
final LockScreenSettings _lockscreenSetting = LockScreenSettings.instance;
late bool appLock;
bool isPinEnabled = false;
bool isPasswordEnabled = false;
@override
void initState() {
super.initState();
_initializeSettings();
appLock = isPinEnabled ||
isPasswordEnabled ||
_configuration.shouldShowSystemLockScreen();
}
Future<void> _initializeSettings() async {
final bool passwordEnabled = await _lockscreenSetting.isPasswordSet();
final bool pinEnabled = await _lockscreenSetting.isPinSet();
setState(() {
isPasswordEnabled = passwordEnabled;
isPinEnabled = pinEnabled;
});
}
Future<void> _deviceLock() async {
await _lockscreenSetting.removePinAndPassword();
await _initializeSettings();
}
Future<void> _pinLock() async {
final bool result = await Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {
return const LockScreenPin();
},
),
);
setState(() {
_initializeSettings();
if (result) {
appLock = isPinEnabled ||
isPasswordEnabled ||
_configuration.shouldShowSystemLockScreen();
}
});
}
Future<void> _passwordLock() async {
final bool result = await Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {
return const LockScreenPassword();
},
),
);
setState(() {
_initializeSettings();
if (result) {
appLock = isPinEnabled ||
isPasswordEnabled ||
_configuration.shouldShowSystemLockScreen();
}
});
}
Future<void> _onToggleSwitch() async {
AppLock.of(context)!.setEnabled(!appLock);
await _configuration.setSystemLockScreen(!appLock);
await _lockscreenSetting.removePinAndPassword();
setState(() {
_initializeSettings();
appLock = !appLock;
});
}
@override
Widget build(BuildContext context) {
final colorTheme = getEnteColorScheme(context);
final textTheme = getEnteTextTheme(context);
return Scaffold(
body: CustomScrollView(
primary: false,
slivers: <Widget>[
const TitleBarWidget(
flexibleSpaceTitle: TitleBarTitleWidget(
title: 'App lock',
),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Column(
children: [
MenuItemWidget(
captionedTextWidget: const CaptionedTextWidget(
title: 'App lock',
),
alignCaptionedTextToLeft: true,
singleBorderRadius: 8,
menuItemColor: colorTheme.fillFaint,
trailingWidget: ToggleSwitchWidget(
value: () => appLock,
onChanged: () => _onToggleSwitch(),
),
),
!appLock
? Padding(
padding: const EdgeInsets.only(
top: 14,
left: 14,
right: 12,
),
child: Text(
'Choose between your device\'s default lock screen and a custom lock screen with a PIN or password.',
style: textTheme.miniFaint,
textAlign: TextAlign.left,
),
)
: const SizedBox(),
const Padding(
padding: EdgeInsets.only(top: 24),
),
],
),
appLock
? Column(
children: [
MenuItemWidget(
captionedTextWidget:
const CaptionedTextWidget(
title: "Device lock",
),
alignCaptionedTextToLeft: true,
isTopBorderRadiusRemoved: false,
isBottomBorderRadiusRemoved: true,
menuItemColor: colorTheme.fillFaint,
trailingIcon:
!(isPasswordEnabled || isPinEnabled)
? Icons.check
: null,
trailingIconColor: colorTheme.textBase,
onTap: () => _deviceLock(),
),
DividerWidget(
dividerType: DividerType.menuNoIcon,
bgColor: colorTheme.fillFaint,
),
MenuItemWidget(
captionedTextWidget:
const CaptionedTextWidget(
title: "Pin lock",
),
alignCaptionedTextToLeft: true,
isTopBorderRadiusRemoved: true,
isBottomBorderRadiusRemoved: true,
menuItemColor: colorTheme.fillFaint,
trailingIcon:
isPinEnabled ? Icons.check : null,
trailingIconColor: colorTheme.textBase,
onTap: () => _pinLock(),
),
DividerWidget(
dividerType: DividerType.menuNoIcon,
bgColor: colorTheme.fillFaint,
),
MenuItemWidget(
captionedTextWidget:
const CaptionedTextWidget(
title: "Password",
),
alignCaptionedTextToLeft: true,
isTopBorderRadiusRemoved: true,
isBottomBorderRadiusRemoved: false,
menuItemColor: colorTheme.fillFaint,
trailingIcon:
isPasswordEnabled ? Icons.check : null,
trailingIconColor: colorTheme.textBase,
onTap: () => _passwordLock(),
),
],
)
: Container(),
],
),
),
);
},
childCount: 1,
),
),
],
),
);
}
}

View File

@@ -0,0 +1,231 @@
import "package:ente_auth/theme/ente_theme.dart";
import "package:ente_auth/ui/common/dynamic_fab.dart";
import "package:ente_auth/ui/components/buttons/icon_button_widget.dart";
import "package:ente_auth/ui/components/text_input_widget.dart";
import "package:ente_auth/ui/settings/lock_screen/lock_screen_confirm_password.dart";
import "package:ente_auth/ui/settings/lock_screen/lock_screen_options.dart";
import "package:ente_auth/utils/lock_screen_settings.dart";
import "package:flutter/material.dart";
import "package:flutter/services.dart";
class LockScreenPassword extends StatefulWidget {
const LockScreenPassword({
super.key,
this.isAuthenticating = false,
this.isOnOpeningApp = false,
this.authPass,
});
//Is false when setting a new password
final bool isAuthenticating;
final bool isOnOpeningApp;
final String? authPass;
@override
State<LockScreenPassword> createState() => _LockScreenPasswordState();
}
class _LockScreenPasswordState extends State<LockScreenPassword> {
final _passwordController = TextEditingController(text: null);
final _focusNode = FocusNode();
final _isFormValid = ValueNotifier<bool>(false);
final _submitNotifier = ValueNotifier(false);
int invalidAttemptsCount = 0;
final _lockscreenSetting = LockScreenSettings.instance;
@override
void initState() {
super.initState();
invalidAttemptsCount = _lockscreenSetting.getInvalidAttemptCount();
WidgetsBinding.instance.addPostFrameCallback((_) async {
_focusNode.requestFocus();
});
}
@override
void dispose() {
super.dispose();
_submitNotifier.dispose();
_focusNode.dispose();
_isFormValid.dispose();
_passwordController.dispose();
}
@override
Widget build(BuildContext context) {
final colorTheme = getEnteColorScheme(context);
final textTheme = getEnteTextTheme(context);
final isKeypadOpen = MediaQuery.viewInsetsOf(context).bottom > 100;
FloatingActionButtonLocation? fabLocation() {
if (isKeypadOpen) {
return null;
} else {
return FloatingActionButtonLocation.centerFloat;
}
}
return Scaffold(
resizeToAvoidBottomInset: isKeypadOpen,
appBar: AppBar(
elevation: 0,
leading: IconButton(
onPressed: () {
FocusScope.of(context).unfocus();
Navigator.of(context).pop(false);
},
icon: Icon(
Icons.arrow_back,
color: colorTheme.textBase,
),
),
),
floatingActionButton: ValueListenableBuilder<bool>(
valueListenable: _isFormValid,
builder: (context, isFormValid, child) {
return DynamicFAB(
isKeypadOpen: isKeypadOpen,
buttonText: "Next",
isFormValid: isFormValid,
onPressedFunction: () async {
_submitNotifier.value = !_submitNotifier.value;
},
);
},
),
floatingActionButtonLocation: fabLocation(),
floatingActionButtonAnimator: NoScalingAnimation(),
body: SingleChildScrollView(
child: Center(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
height: 120,
width: 120,
child: Stack(
alignment: Alignment.center,
children: [
Container(
width: 82,
height: 82,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
colors: [
Colors.grey.shade500.withOpacity(0.2),
Colors.grey.shade50.withOpacity(0.1),
Colors.grey.shade400.withOpacity(0.2),
Colors.grey.shade300.withOpacity(0.4),
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: Padding(
padding: const EdgeInsets.all(1.0),
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: colorTheme.backgroundBase,
),
),
),
),
SizedBox(
height: 75,
width: 75,
child: CircularProgressIndicator(
backgroundColor: colorTheme.fillFaintPressed,
value: 1,
strokeWidth: 1.5,
),
),
IconButtonWidget(
icon: Icons.lock,
iconButtonType: IconButtonType.primary,
iconColor: colorTheme.textBase,
),
],
),
),
Text(
widget.isAuthenticating ? "Enter Password" : "Set new Password",
textAlign: TextAlign.center,
style: textTheme.bodyBold,
),
const Padding(padding: EdgeInsets.all(12)),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: TextInputWidget(
hintText: "Password",
autoFocus: true,
textCapitalization: TextCapitalization.none,
isPasswordInput: true,
shouldSurfaceExecutionStates: false,
onChange: (p0) {
_passwordController.text = p0;
_isFormValid.value = _passwordController.text.isNotEmpty;
},
onSubmit: (p0) {
return _confirmPassword();
},
submitNotifier: _submitNotifier,
),
),
const Padding(padding: EdgeInsets.all(12)),
],
),
),
),
);
}
Future<bool> _confirmPasswordAuth(String inputtedPassword) async {
// final Uint8List? salt = await _lockscreenSetting.getSalt();
// final hash = cryptoPwHash({
// "password": utf8.encode(inputtedPassword),
// "salt": salt,
// "opsLimit": Sodium.cryptoPwhashOpslimitInteractive,
// "memLimit": Sodium.cryptoPwhashMemlimitInteractive,
// });
if (widget.authPass == inputtedPassword) {
await _lockscreenSetting.setInvalidAttemptCount(0);
widget.isOnOpeningApp
? Navigator.of(context).pop(true)
: Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (context) => const LockScreenOptions(),
),
);
return true;
} else {
if (widget.isOnOpeningApp) {
invalidAttemptsCount++;
if (invalidAttemptsCount > 4) {
await _lockscreenSetting.setInvalidAttemptCount(invalidAttemptsCount);
Navigator.of(context).pop(false);
}
}
await HapticFeedback.vibrate();
throw Exception("Incorrect password");
}
}
Future<void> _confirmPassword() async {
if (widget.isAuthenticating) {
await _confirmPasswordAuth(_passwordController.text);
return;
} else {
await Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) => LockScreenConfirmPassword(
password: _passwordController.text,
),
),
);
_passwordController.clear();
}
}
}

View File

@@ -0,0 +1,269 @@
import "package:ente_auth/theme/colors.dart";
import "package:ente_auth/theme/ente_theme.dart";
import "package:ente_auth/theme/text_style.dart";
import "package:ente_auth/ui/settings/lock_screen/custom_pin_keypad.dart";
import "package:ente_auth/ui/settings/lock_screen/lock_screen_confirm_pin.dart";
import "package:ente_auth/ui/settings/lock_screen/lock_screen_options.dart";
import "package:ente_auth/utils/lock_screen_settings.dart";
import "package:flutter/material.dart";
import "package:flutter/services.dart";
import 'package:pinput/pinput.dart';
class LockScreenPin extends StatefulWidget {
const LockScreenPin({
super.key,
this.isAuthenticating = false,
this.isOnOpeningApp = false,
this.authPin,
});
//Is false when setting a new password
final bool isAuthenticating;
final bool isOnOpeningApp;
final String? authPin;
@override
State<LockScreenPin> createState() => _LockScreenPinState();
}
class _LockScreenPinState extends State<LockScreenPin> {
final _pinController = TextEditingController(text: null);
final LockScreenSettings _lockscreenSetting = LockScreenSettings.instance;
bool isPinValid = false;
int invalidAttemptsCount = 0;
@override
void initState() {
super.initState();
invalidAttemptsCount = _lockscreenSetting.getInvalidAttemptCount();
}
@override
void dispose() {
super.dispose();
_pinController.dispose();
}
Future<bool> confirmPinAuth(String inputtedPin) async {
// final Uint8List? salt = await _lockscreenSetting.getSalt();
// final hash = cryptoPwHash({
// "password": utf8.encode(code),
// "salt": salt,
// "opsLimit": Sodium.cryptoPwhashOpslimitInteractive,
// "memLimit": Sodium.cryptoPwhashMemlimitInteractive,
// });
// final String hashedPin = base64Encode(hash);
if (widget.authPin == inputtedPin) {
invalidAttemptsCount = 0;
await _lockscreenSetting.setInvalidAttemptCount(0);
widget.isOnOpeningApp
? Navigator.of(context).pop(true)
: Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (context) => const LockScreenOptions(),
),
);
return true;
} else {
setState(() {
isPinValid = true;
});
await HapticFeedback.vibrate();
await Future.delayed(const Duration(milliseconds: 75));
_pinController.clear();
setState(() {
isPinValid = false;
});
if (widget.isOnOpeningApp) {
invalidAttemptsCount++;
if (invalidAttemptsCount > 4) {
await _lockscreenSetting.setInvalidAttemptCount(invalidAttemptsCount);
Navigator.of(context).pop(false);
}
}
return false;
}
}
Future<void> _confirmPin(String inputtedPin) async {
if (widget.isAuthenticating) {
await confirmPinAuth(inputtedPin);
return;
} else {
await Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) =>
LockScreenConfirmPin(pin: inputtedPin),
),
);
_pinController.clear();
}
}
final _pinPutDecoration = PinTheme(
height: 48,
width: 48,
padding: const EdgeInsets.only(top: 6.0),
decoration: BoxDecoration(
border: Border.all(color: const Color.fromRGBO(45, 194, 98, 1.0)),
borderRadius: BorderRadius.circular(15.0),
),
);
@override
Widget build(BuildContext context) {
final colorTheme = getEnteColorScheme(context);
final textTheme = getEnteTextTheme(context);
return Scaffold(
appBar: AppBar(
elevation: 0,
leading: IconButton(
onPressed: () {
Navigator.of(context).pop(false);
},
icon: Icon(
Icons.arrow_back,
color: colorTheme.textBase,
),
),
),
body: OrientationBuilder(
builder: (context, orientation) {
return orientation == Orientation.portrait
? _getBody(colorTheme, textTheme, isPortrait: true)
: SingleChildScrollView(
child: _getBody(colorTheme, textTheme, isPortrait: false),
);
},
),
);
}
Widget _getBody(
EnteColorScheme colorTheme,
EnteTextTheme textTheme, {
required bool isPortrait,
}) {
return Center(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
height: 120,
width: 120,
child: Stack(
alignment: Alignment.center,
children: [
Container(
width: 82,
height: 82,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
colors: [
Colors.grey.shade500.withOpacity(0.2),
Colors.grey.shade50.withOpacity(0.1),
Colors.grey.shade400.withOpacity(0.2),
Colors.grey.shade300.withOpacity(0.4),
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: Padding(
padding: const EdgeInsets.all(1.0),
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: colorTheme.backgroundBase,
),
),
),
),
SizedBox(
height: 75,
width: 75,
child: ValueListenableBuilder(
valueListenable: _pinController,
builder: (context, value, child) {
return TweenAnimationBuilder<double>(
tween: Tween<double>(
begin: 0,
end: _pinController.text.length / 4,
),
curve: Curves.ease,
duration: const Duration(milliseconds: 250),
builder: (context, value, _) =>
CircularProgressIndicator(
backgroundColor: colorTheme.fillFaintPressed,
value: value,
color: colorTheme.primary400,
strokeWidth: 1.5,
),
);
},
),
),
Icon(
Icons.lock,
color: colorTheme.textBase,
size: 30,
),
],
),
),
Text(
widget.isAuthenticating ? "Enter PIN" : "Set new PIN",
style: textTheme.bodyBold,
),
const Padding(padding: EdgeInsets.all(12)),
Pinput(
length: 4,
showCursor: false,
useNativeKeyboard: false,
controller: _pinController,
defaultPinTheme: _pinPutDecoration,
submittedPinTheme: _pinPutDecoration.copyWith(
textStyle: textTheme.h3Bold,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10.0),
border: Border.all(
color: colorTheme.fillBase,
),
),
),
followingPinTheme: _pinPutDecoration.copyWith(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10.0),
border: Border.all(
color: colorTheme.fillMuted,
),
),
),
focusedPinTheme: _pinPutDecoration,
errorPinTheme: _pinPutDecoration.copyWith(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10.0),
border: Border.all(
color: colorTheme.warning400,
),
),
),
forceErrorState: isPinValid,
obscureText: true,
obscuringCharacter: '*',
errorText: '',
onCompleted: (value) async {
await _confirmPin(_pinController.text);
},
),
isPortrait
? const Spacer()
: const Padding(padding: EdgeInsets.all(12)),
CustomPinKeypad(controller: _pinController),
],
),
);
}
}

View File

@@ -15,12 +15,15 @@ import 'package:ente_auth/ui/components/expandable_menu_item_widget.dart';
import 'package:ente_auth/ui/components/menu_item_widget.dart';
import 'package:ente_auth/ui/components/toggle_switch_widget.dart';
import 'package:ente_auth/ui/settings/common_settings.dart';
import 'package:ente_auth/ui/settings/lock_screen/lock_screen_options.dart';
import 'package:ente_auth/utils/auth_util.dart';
import 'package:ente_auth/utils/dialog_util.dart';
import 'package:ente_auth/utils/navigation_util.dart';
import 'package:ente_auth/utils/platform_util.dart';
import 'package:ente_auth/utils/toast_util.dart';
import 'package:ente_crypto_dart/ente_crypto_dart.dart';
import 'package:flutter/material.dart';
import 'package:local_auth/local_auth.dart';
import 'package:logging/logging.dart';
class SecuritySectionWidget extends StatefulWidget {
@@ -134,25 +137,34 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
}
children.addAll([
MenuItemWidget(
captionedTextWidget: CaptionedTextWidget(
title: l10n.lockscreen,
captionedTextWidget: const CaptionedTextWidget(
title: "App lock",
),
trailingWidget: ToggleSwitchWidget(
value: () => _config.shouldShowLockScreen(),
onChanged: () async {
final hasAuthenticated = await LocalAuthenticationService.instance
.requestLocalAuthForLockScreen(
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () async {
if (await LocalAuthentication().isDeviceSupported()) {
final bool result = await requestAuthentication(
context,
!_config.shouldShowLockScreen(),
context.l10n.authToChangeLockscreenSetting,
context.l10n.lockScreenEnablePreSteps,
"Please authenticate to change lockscreen setting",
);
if (hasAuthenticated) {
FocusScope.of(context).requestFocus();
setState(() {});
if (result) {
await Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {
return const LockScreenOptions();
},
),
);
}
},
),
} else {
await showErrorDialog(
context,
"No system lock found",
"To enable app lock, please setup device passcode or screen lock in your system settings.",
);
}
},
),
sectionOptionSpacing,
]);

View File

@@ -1,10 +1,16 @@
import 'dart:io';
import 'dart:math';
import 'package:ente_auth/core/configuration.dart';
import 'package:ente_auth/l10n/l10n.dart';
import 'package:ente_auth/ui/common/gradient_button.dart';
import 'package:ente_auth/theme/ente_theme.dart';
import 'package:ente_auth/ui/tools/app_lock.dart';
import 'package:ente_auth/utils/auth_util.dart';
import 'package:ente_auth/utils/dialog_util.dart';
import 'package:ente_auth/utils/lock_screen_settings.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:logging/logging.dart';
class LockScreen extends StatefulWidget {
@@ -20,11 +26,17 @@ class _LockScreenState extends State<LockScreen> with WidgetsBindingObserver {
bool _hasPlacedAppInBackground = false;
bool _hasAuthenticationFailed = false;
int? lastAuthenticatingTime;
bool isTimerRunning = false;
int lockedTimeInSeconds = 0;
int invalidAttemptCount = 0;
int remainingTimeInSeconds = 0;
final _lockscreenSetting = LockScreenSettings.instance;
late Brightness _platformBrightness;
@override
void initState() {
_logger.info("initiatingState");
super.initState();
invalidAttemptCount = _lockscreenSetting.getInvalidAttemptCount();
WidgetsBinding.instance.addObserver(this);
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
if (isNonMobileIOSDevice()) {
@@ -33,37 +45,133 @@ class _LockScreenState extends State<LockScreen> with WidgetsBindingObserver {
}
_showLockScreen(source: "postFrameInit");
});
_platformBrightness =
SchedulerBinding.instance.platformDispatcher.platformBrightness;
}
@override
Widget build(BuildContext context) {
final colorTheme = getEnteColorScheme(context);
final textTheme = getEnteTextTheme(context);
return Scaffold(
body: Center(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Stack(
alignment: Alignment.center,
body: GestureDetector(
onTap: () {
isTimerRunning ? null : _showLockScreen(source: "tap");
},
child: Container(
decoration: BoxDecoration(
image: DecorationImage(
opacity: _platformBrightness == Brightness.light ? 0.08 : 0.12,
image: const ExactAssetImage(
'assets/loading_photos_background.png',
),
fit: BoxFit.cover,
),
),
child: Center(
child: Column(
children: [
Opacity(
opacity: 0.2,
child: Image.asset('assets/loading_photos_background.png'),
),
const Spacer(),
SizedBox(
width: 180,
child: GradientButton(
text: context.l10n.unlock,
iconData: Icons.lock_open_outlined,
onTap: () async {
// ignore: unawaited_futures
_showLockScreen(source: "tapUnlock");
},
height: 120,
width: 120,
child: Stack(
alignment: Alignment.center,
children: [
Container(
width: 82,
height: 82,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
colors: [
Colors.grey.shade500.withOpacity(0.2),
Colors.grey.shade50.withOpacity(0.1),
Colors.grey.shade400.withOpacity(0.2),
Colors.grey.shade300.withOpacity(0.4),
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: Padding(
padding: const EdgeInsets.all(1.0),
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: colorTheme.backgroundBase,
),
),
),
),
SizedBox(
height: 75,
width: 75,
child: TweenAnimationBuilder<double>(
tween: Tween<double>(
begin: 0,
end: _getFractionOfTimeElapsed(),
),
duration: const Duration(seconds: 1),
builder: (context, value, _) =>
CircularProgressIndicator(
backgroundColor: colorTheme.fillFaintPressed,
value: value,
color: colorTheme.primary400,
strokeWidth: 1.5,
),
),
),
Icon(
Icons.lock,
size: 30,
color: colorTheme.textBase,
),
],
),
),
const Spacer(),
isTimerRunning
? Stack(
alignment: Alignment.center,
children: [
Text(
"Too many incorrect attempts",
style: textTheme.small,
)
.animate(
delay: const Duration(milliseconds: 2000),
)
.fadeOut(
duration: 400.ms,
curve: Curves.easeInOutCirc,
),
Text(
_formatTime(remainingTimeInSeconds),
style: textTheme.small,
)
.animate(
delay: const Duration(milliseconds: 2250),
)
.fadeIn(
duration: 400.ms,
curve: Curves.easeInOutCirc,
),
],
)
: GestureDetector(
onTap: () => _showLockScreen(source: "tap"),
child: Text(
"Tap to unlock",
style: textTheme.small,
),
),
const Padding(
padding: EdgeInsets.only(bottom: 24),
),
],
),
],
),
),
),
);
@@ -90,10 +198,17 @@ class _LockScreenState extends State<LockScreen> with WidgetsBindingObserver {
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
}
@@ -115,24 +230,119 @@ class _LockScreenState extends State<LockScreen> with WidgetsBindingObserver {
super.dispose();
}
Future<void> startLockTimer(int timeInSeconds) async {
if (isTimerRunning) {
return;
}
setState(() {
isTimerRunning = true;
remainingTimeInSeconds = timeInSeconds;
});
while (remainingTimeInSeconds > 0) {
await Future.delayed(const Duration(seconds: 1));
setState(() {
remainingTimeInSeconds--;
});
}
setState(() {
isTimerRunning = false;
});
}
double _getFractionOfTimeElapsed() {
final int totalLockedTime =
lockedTimeInSeconds = pow(2, invalidAttemptCount - 5).toInt() * 30;
if (remainingTimeInSeconds == 0) return 1;
return 1 - remainingTimeInSeconds / totalLockedTime;
}
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}h ${minutes}m";
} else if (minutes > 0) {
return "${minutes}m ${remainingSeconds}s";
} else {
return "${remainingSeconds}s";
}
}
Future<void> _autoLogoutOnMaxInvalidAttempts() async {
_logger.info("Auto logout on max invalid attempts");
await _lockscreenSetting.setInvalidAttemptCount(0);
await showErrorDialog(
context,
"Too many incorrect attempts",
"Please login again",
isDismissable: false,
);
Navigator.of(context, rootNavigator: true).pop('dialog');
Navigator.of(context).popUntil((route) => route.isFirst);
final dialog = createProgressDialog(context, "Logging out ...");
await dialog.show();
await Configuration.instance.logout();
await dialog.hide();
}
Future<void> _showLockScreen({String source = ''}) async {
final int id = DateTime.now().millisecondsSinceEpoch;
_logger.info("Showing lock screen $source $id");
final int currentTimestamp = DateTime.now().millisecondsSinceEpoch;
_logger.info("Showing lock screen $source $currentTimestamp");
try {
if (currentTimestamp < _lockscreenSetting.getlastInvalidAttemptTime() &&
!_isShowingLockScreen) {
final int remainingTime =
(_lockscreenSetting.getlastInvalidAttemptTime() -
currentTimestamp) ~/
1000;
await startLockTimer(remainingTime);
}
_isShowingLockScreen = true;
final result = await requestAuthentication(
context,
context.l10n.authToViewSecrets,
);
_logger.finest("LockScreen Result $result $id");
final result = isTimerRunning
? false
: await requestAuthentication(
context,
context.l10n.authToViewSecrets,
isOpeningApp: true,
);
_logger.finest("LockScreen Result $result $currentTimestamp");
_isShowingLockScreen = false;
if (result) {
lastAuthenticatingTime = DateTime.now().millisecondsSinceEpoch;
AppLock.of(context)!.didUnlock();
await _lockscreenSetting.setInvalidAttemptCount(0);
setState(() {
lockedTimeInSeconds = 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();
if (invalidAttemptCount > 9) {
await _autoLogoutOnMaxInvalidAttempts();
return;
}
lockedTimeInSeconds = pow(2, invalidAttemptCount - 5).toInt() * 30;
await _lockscreenSetting.setLastInvalidAttemptTime(
DateTime.now().millisecondsSinceEpoch +
lockedTimeInSeconds * 1000,
);
await startLockTimer(lockedTimeInSeconds);
}
_hasAuthenticationFailed = true;
_logger.info("Authentication failed");
}

View File

@@ -4,7 +4,7 @@ import 'package:ente_auth/services/user_service.dart';
import 'package:ente_auth/ui/lifecycle_event_handler.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:pinput/pin_put/pin_put.dart';
import 'package:pinput/pinput.dart';
class TwoFactorAuthenticationPage extends StatefulWidget {
final String sessionID;
@@ -19,9 +19,13 @@ class TwoFactorAuthenticationPage extends StatefulWidget {
class _TwoFactorAuthenticationPageState
extends State<TwoFactorAuthenticationPage> {
final _pinController = TextEditingController();
final _pinPutDecoration = BoxDecoration(
border: Border.all(color: const Color.fromRGBO(45, 194, 98, 1.0)),
borderRadius: BorderRadius.circular(15.0),
final _pinPutDecoration = PinTheme(
height: 45,
width: 45,
decoration: BoxDecoration(
border: Border.all(color: const Color.fromRGBO(45, 194, 98, 1.0)),
borderRadius: BorderRadius.circular(15.0),
),
);
String _code = "";
late LifecycleEventHandler _lifecycleEventHandler;
@@ -79,9 +83,9 @@ class _TwoFactorAuthenticationPageState
const Padding(padding: EdgeInsets.all(32)),
Padding(
padding: const EdgeInsets.fromLTRB(40, 0, 40, 0),
child: PinPut(
fieldsCount: 6,
onSubmit: (String code) {
child: Pinput(
length: 6,
onCompleted: (String code) {
_verifyTwoFactorCode(code);
},
onChanged: (String pin) {
@@ -90,21 +94,34 @@ class _TwoFactorAuthenticationPageState
});
},
controller: _pinController,
submittedFieldDecoration: _pinPutDecoration.copyWith(
borderRadius: BorderRadius.circular(20.0),
),
selectedFieldDecoration: _pinPutDecoration,
followingFieldDecoration: _pinPutDecoration.copyWith(
borderRadius: BorderRadius.circular(5.0),
border: Border.all(
color: const Color.fromRGBO(45, 194, 98, 0.5),
submittedPinTheme: _pinPutDecoration.copyWith(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20.0),
border: Border.all(
color: const Color.fromRGBO(45, 194, 98, 0.5),
),
),
),
inputDecoration: const InputDecoration(
focusedBorder: InputBorder.none,
border: InputBorder.none,
counterText: '',
// submittedFieldDecoration: _pinPutDecoration.copyWith(
// borderRadius: BorderRadius.circular(20.0),
// ),
// selectedFieldDecoration: _pinPutDecoration,
defaultPinTheme: _pinPutDecoration,
followingPinTheme: _pinPutDecoration.copyWith(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10.0),
border: Border.all(
color: const Color.fromRGBO(45, 194, 98, 0.5),
),
),
),
// followingFieldDecoration: _pinPutDecoration.copyWith(
// borderRadius: BorderRadius.circular(5.0),
// border: Border.all(
// color: const Color.fromRGBO(45, 194, 98, 0.5),
// ),
// ),
autofocus: true,
),
),

View File

@@ -1,6 +1,8 @@
import 'dart:io';
import 'package:ente_auth/l10n/l10n.dart';
import 'package:ente_auth/services/local_authentication_service.dart';
import 'package:ente_auth/utils/lock_screen_settings.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter_local_authentication/flutter_local_authentication.dart';
import 'package:local_auth/local_auth.dart';
@@ -8,8 +10,24 @@ import 'package:local_auth_android/local_auth_android.dart';
import 'package:local_auth_darwin/types/auth_messages_ios.dart';
import 'package:logging/logging.dart';
Future<bool> requestAuthentication(BuildContext context, String reason) async {
Future<bool> requestAuthentication(
BuildContext context,
String reason, {
bool isOpeningApp = false,
}) async {
Logger("AuthUtil").info("Requesting authentication");
final String? savedPin = await LockScreenSettings.instance.getPin();
final String? savedPassword = await LockScreenSettings.instance.getPassword();
if (savedPassword != null || savedPin != null) {
return await LocalAuthenticationService.instance
.requestEnteAuthForLockScreen(
context,
savedPin,
savedPassword,
isOnOpeningApp: isOpeningApp,
);
}
if (Platform.isMacOS || Platform.isLinux) {
return await FlutterLocalAuthentication().authenticate();
} else {

View File

@@ -0,0 +1,118 @@
import "package:shared_preferences/shared_preferences.dart";
class LockScreenSettings {
LockScreenSettings._privateConstructor();
static final LockScreenSettings instance =
LockScreenSettings._privateConstructor();
static const password = "ls_password";
static const pin = "ls_pin";
static const saltKey = "ls_salt";
static const keyInvalidAttempts = "ls_invalid_attempts";
static const lastInvalidAttemptTime = "ls_last_invalid_attempt_time";
late SharedPreferences _preferences;
Future<void> init() async {
_preferences = await SharedPreferences.getInstance();
}
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() {
// return Sodium.randombytesBuf(Sodium.cryptoPwhashSaltbytes);
// }
Future<void> setPin(String userPin) async {
//await _secureStorage.delete(key: saltKey);
await _preferences.setString(pin, userPin);
await _preferences.remove(password);
// final salt = _generateSalt();
// final hash = cryptoPwHash({
// "password": utf8.encode(userPin),
// "salt": salt,
// "opsLimit": Sodium.cryptoPwhashOpslimitInteractive,
// "memLimit": Sodium.cryptoPwhashMemlimitInteractive,
// });
// final String saltPin = base64Encode(salt);
// final String hashedPin = base64Encode(hash);
// await _secureStorage.write(key: saltKey, value: saltPin);
// await _secureStorage.write(key: pin, value: hashedPin);
// await _secureStorage.delete(key: password);
return;
}
// Future<Uint8List?> getSalt() async {
// final String? salt = await _secureStorage.read(key: saltKey);
// if (salt == null) return null;
// return base64Decode(salt);
// }
Future<String?> getPin() async {
return _preferences.getString(pin);
// return _secureStorage.read(key: pin);
}
Future<void> setPassword(String pass) async {
await _preferences.setString(password, pass);
await _preferences.remove(pin);
// await _secureStorage.delete(key: saltKey);
// final salt = _generateSalt();
// final hash = cryptoPwHash({
// "password": utf8.encode(pass),
// "salt": salt,
// "opsLimit": Sodium.cryptoPwhashOpslimitInteractive,
// "memLimit": Sodium.cryptoPwhashMemlimitInteractive,
// });
// final String saltPassword = base64Encode(salt);
// final String hashPassword = base64Encode(hash);
// await _secureStorage.write(key: saltKey, value: saltPassword);
// await _secureStorage.write(key: password, value: hashPassword);
// await _secureStorage.delete(key: pin);
return;
}
Future<String?> getPassword() async {
return _preferences.getString(password);
// return _secureStorage.read(key: password);
}
Future<void> removePinAndPassword() async {
await _preferences.remove(pin);
await _preferences.remove(password);
// await _secureStorage.delete(key: saltKey);
// await _secureStorage.delete(key: pin);
// await _secureStorage.delete(key: password);
}
Future<bool> isPinSet() async {
return _preferences.containsKey(pin);
// return await _secureStorage.containsKey(key: pin);
}
Future<bool> isPasswordSet() async {
return _preferences.containsKey(password);
// return await _secureStorage.containsKey(key: password);
}
}

View File

@@ -41,6 +41,7 @@ dependencies:
fk_user_agent: ^2.1.0
flutter:
sdk: flutter
flutter_animate: ^4.1.0
flutter_bloc: ^8.0.1
flutter_context_menu: ^0.1.3
flutter_displaymode: ^0.6.0
@@ -77,7 +78,7 @@ dependencies:
password_strength: ^0.2.0
path: ^1.8.3
path_provider: ^2.0.11
pinput: ^1.2.2
pinput: ^2.0.2
pointycastle: ^3.7.3
privacy_screen: ^0.0.6
protobuf: ^3.0.0