Remove old password reentry, recovery key, recovery, and password verification pages; refactor imports and update references in settings and home page.

This commit is contained in:
AmanRajSinghMourya
2025-08-01 11:58:22 +05:30
parent 6f94d91afb
commit f914263b2f
14 changed files with 24 additions and 2218 deletions

View File

@@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:io';
import 'package:adaptive_theme/adaptive_theme.dart';
import 'package:ente_accounts/services/user_service.dart';
import 'package:ente_auth/core/configuration.dart';
import 'package:ente_auth/ente_theme_data.dart';
import "package:ente_auth/l10n/l10n.dart";
@@ -9,7 +10,6 @@ import 'package:ente_auth/locale.dart';
import "package:ente_auth/onboarding/view/onboarding_page.dart";
import 'package:ente_auth/services/authenticator_service.dart';
import 'package:ente_auth/services/update_service.dart';
import 'package:ente_auth/services/user_service.dart';
import 'package:ente_auth/services/window_listener_service.dart';
import 'package:ente_auth/ui/home_page.dart';
import 'package:ente_auth/ui/settings/app_update_dialog.dart';

View File

@@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:io';
import 'package:adaptive_theme/adaptive_theme.dart';
import 'package:ente_accounts/services/user_service.dart';
import "package:ente_auth/app/view/app.dart";
import 'package:ente_auth/core/configuration.dart';
import 'package:ente_auth/core/constants.dart';
@@ -13,11 +14,11 @@ import 'package:ente_auth/services/billing_service.dart';
import 'package:ente_auth/services/notification_service.dart';
import 'package:ente_auth/services/preference_service.dart';
import 'package:ente_auth/services/update_service.dart';
import 'package:ente_auth/services/user_service.dart';
import 'package:ente_auth/services/window_listener_service.dart';
import 'package:ente_auth/store/authenticator_db.dart';
import 'package:ente_auth/store/code_display_store.dart';
import 'package:ente_auth/store/code_store.dart';
import 'package:ente_auth/ui/home_page.dart';
import 'package:ente_auth/ui/utils/icon_utils.dart';
import 'package:ente_auth/utils/directory_utils.dart';
import 'package:ente_auth/utils/platform_util.dart';
@@ -162,7 +163,7 @@ Future<void> _init(bool bool, {String? via}) async {
await CodeDisplayStore.instance.init();
await Configuration.instance.init([AuthenticatorDB.instance]);
await Network.instance.init(Configuration.instance);
await UserService.instance.init();
await UserService.instance.init(Configuration.instance, const HomePage());
await AuthenticatorService.instance.init();
await BillingService.instance.init();
await NotificationService.instance.init();

View File

@@ -4,6 +4,7 @@ import 'dart:io';
import 'package:ente_accounts/pages/email_entry_page.dart';
import 'package:ente_accounts/pages/login_page.dart';
import 'package:ente_accounts/pages/password_entry_page.dart';
import 'package:ente_accounts/pages/password_reentry_page.dart';
import 'package:ente_auth/app/view/app.dart';
import 'package:ente_auth/core/configuration.dart';
import 'package:ente_auth/ente_theme_data.dart';
@@ -12,7 +13,6 @@ import "package:ente_auth/l10n/l10n.dart";
import 'package:ente_auth/locale.dart';
import 'package:ente_auth/theme/text_style.dart';
import 'package:ente_auth/ui/account/logout_dialog.dart';
import 'package:ente_auth/ui/account/password_reentry_page.dart';
import 'package:ente_auth/ui/common/gradient_button.dart';
import 'package:ente_auth/ui/components/buttons/button_widget.dart';
import 'package:ente_auth/ui/components/models/button_result.dart';
@@ -272,7 +272,10 @@ class _OnboardingPageState extends State<OnboardingPage> {
);
} else if (Configuration.instance.getKey() == null) {
// Yet to decrypt the key
page = const PasswordReentryPage();
page = PasswordReentryPage(
Configuration.instance,
const HomePage(),
);
} else {
// All is well, user just has not subscribed
page = const HomePage();
@@ -302,7 +305,10 @@ class _OnboardingPageState extends State<OnboardingPage> {
);
} else if (Configuration.instance.getKey() == null) {
// Yet to decrypt the key
page = const PasswordReentryPage();
page = PasswordReentryPage(
Configuration.instance,
const HomePage(),
);
} else {
// All is well, user just has not subscribed
// page = getSubscriptionPage(isOnBoarding: true);

View File

@@ -1,57 +0,0 @@
import 'package:ente_auth/core/constants.dart';
import 'package:ente_auth/utils/dialog_util.dart';
import 'package:ente_network/network.dart';
import 'package:flutter/widgets.dart';
import 'package:logging/logging.dart';
import 'package:url_launcher/url_launcher_string.dart';
class PasskeyService {
PasskeyService._privateConstructor();
static final PasskeyService instance = PasskeyService._privateConstructor();
final _enteDio = Network.instance.enteDio;
Future<String> getAccountsUrl() async {
final response = await _enteDio.get(
"/users/accounts-token",
);
final accountsUrl = response.data!["accountsUrl"] ?? kAccountsUrl;
final jwtToken = response.data!["accountsToken"] as String;
return "$accountsUrl/passkeys?token=$jwtToken";
}
Future<bool> isPasskeyRecoveryEnabled() async {
final response = await _enteDio.get(
"/users/two-factor/recovery-status",
);
return response.data!["isPasskeyRecoveryEnabled"] as bool;
}
Future<void> configurePasskeyRecovery(
String secret,
String userEncryptedSecret,
String userSecretNonce,
) async {
await _enteDio.post(
"/users/two-factor/passkeys/configure-recovery",
data: {
"secret": secret,
"userSecretCipher": userEncryptedSecret,
"userSecretNonce": userSecretNonce,
},
);
}
Future<void> openPasskeyPage(BuildContext context) async {
try {
final url = await getAccountsUrl();
await launchUrlString(
url,
mode: LaunchMode.externalApplication,
);
} catch (e) {
Logger('PasskeyService').severe("failed to open passkey page", e);
showGenericErrorDialog(context: context, error: e).ignore();
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,326 +0,0 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:ente_auth/core/configuration.dart';
import 'package:ente_auth/core/errors.dart';
import 'package:ente_auth/l10n/l10n.dart';
import 'package:ente_auth/services/user_service.dart';
import 'package:ente_auth/ui/account/recovery_page.dart';
import 'package:ente_auth/ui/common/dynamic_fab.dart';
import 'package:ente_auth/ui/components/buttons/button_widget.dart';
import 'package:ente_auth/ui/home_page.dart';
import 'package:ente_auth/utils/dialog_util.dart';
import 'package:ente_auth/utils/email_util.dart';
import 'package:ente_crypto_dart/ente_crypto_dart.dart';
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
class PasswordReentryPage extends StatefulWidget {
const PasswordReentryPage({super.key});
@override
State<PasswordReentryPage> createState() => _PasswordReentryPageState();
}
class _PasswordReentryPageState extends State<PasswordReentryPage> {
final _logger = Logger((_PasswordReentryPageState).toString());
final _passwordController = TextEditingController();
final FocusNode _passwordFocusNode = FocusNode();
String? email;
bool _passwordInFocus = false;
bool _passwordVisible = false;
String? _volatilePassword;
@override
void initState() {
super.initState();
email = Configuration.instance.getEmail();
_volatilePassword = Configuration.instance.getVolatilePassword();
if (_volatilePassword != null) {
_passwordController.text = _volatilePassword!;
Future.delayed(
Duration.zero,
() => verifyPassword(_volatilePassword!, usingVolatilePassword: true),
);
}
_passwordFocusNode.addListener(() {
setState(() {
_passwordInFocus = _passwordFocusNode.hasFocus;
});
});
}
@override
Widget build(BuildContext context) {
final isKeypadOpen = MediaQuery.of(context).viewInsets.bottom > 100;
FloatingActionButtonLocation? fabLocation() {
if (isKeypadOpen) {
return null;
} else {
return FloatingActionButtonLocation.centerFloat;
}
}
return Scaffold(
resizeToAvoidBottomInset: isKeypadOpen,
appBar: AppBar(
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back),
color: Theme.of(context).iconTheme.color,
onPressed: () {
Navigator.of(context).pop();
},
),
),
body: _getBody(),
floatingActionButton: DynamicFAB(
key: const ValueKey("verifyPasswordButton"),
isKeypadOpen: isKeypadOpen,
isFormValid: _passwordController.text.isNotEmpty,
buttonText: context.l10n.verifyPassword,
onPressedFunction: () async {
FocusScope.of(context).unfocus();
await verifyPassword(_passwordController.text);
},
),
floatingActionButtonLocation: fabLocation(),
floatingActionButtonAnimator: NoScalingAnimation(),
);
}
Future<void> verifyPassword(
String password, {
bool usingVolatilePassword = false,
}) async {
FocusScope.of(context).unfocus();
final dialog = createProgressDialog(context, context.l10n.pleaseWait);
await dialog.show();
if (usingVolatilePassword) {
_logger.info("Using volatile password");
}
try {
final kek = await Configuration.instance.decryptSecretsAndGetKeyEncKey(
password,
Configuration.instance.getKeyAttributes()!,
);
_registerSRPForExistingUsers(kek).ignore();
} on KeyDerivationError catch (e, s) {
_logger.severe("Password verification failed", e, s);
await dialog.hide();
final dialogChoice = await showChoiceDialog(
context,
title: context.l10n.recreatePasswordTitle,
body: context.l10n.recreatePasswordBody,
firstButtonLabel: context.l10n.useRecoveryKey,
);
if (dialogChoice!.action == ButtonAction.first) {
// ignore: unawaited_futures
Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {
return const RecoveryPage();
},
),
);
}
return;
} catch (e, s) {
_logger.severe("Password verification failed", e, s);
await dialog.hide();
final dialogChoice = await showChoiceDialog(
context,
title: context.l10n.incorrectPasswordTitle,
body: context.l10n.pleaseTryAgain,
firstButtonLabel: context.l10n.contactSupport,
secondButtonLabel: context.l10n.ok,
);
if (dialogChoice!.action == ButtonAction.first) {
await sendLogs(
context,
context.l10n.contactSupport,
postShare: () {},
);
}
return;
}
Configuration.instance.resetVolatilePassword();
await dialog.hide();
unawaited(
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(
builder: (BuildContext context) {
return const HomePage();
},
),
(route) => false,
),
);
}
Future<void> _registerSRPForExistingUsers(Uint8List key) async {
bool shouldSetupSRP = false;
try {
// ignore: unused_local_variable
final attr = await UserService.instance.getSrpAttributes(email!);
} on SrpSetupNotCompleteError {
shouldSetupSRP = true;
} catch (e, s) {
_logger.severe("error while fetching attr", e, s);
}
if (shouldSetupSRP) {
try {
final Uint8List loginKey = await CryptoUtil.deriveLoginKey(key);
await UserService.instance.registerOrUpdateSrp(loginKey);
} catch (e, s) {
_logger.severe("error while setting up srp for existing users", e, s);
}
}
}
Widget _getBody() {
return Column(
children: [
Expanded(
child: AutofillGroup(
child: ListView(
children: [
Padding(
padding:
const EdgeInsets.symmetric(vertical: 30, horizontal: 20),
child: Text(
context.l10n.welcomeBack,
style: Theme.of(context).textTheme.headlineMedium,
),
),
Visibility(
// hidden textForm for suggesting auto-fill service for saving
// password
visible: false,
child: TextFormField(
autofillHints: const [
AutofillHints.email,
],
autocorrect: false,
keyboardType: TextInputType.emailAddress,
initialValue: email,
textInputAction: TextInputAction.next,
),
),
Padding(
padding: const EdgeInsets.fromLTRB(20, 24, 20, 0),
child: TextFormField(
key: const ValueKey("passwordInputField"),
autofillHints: const [AutofillHints.password],
decoration: InputDecoration(
hintText: context.l10n.enterYourPasswordHint,
filled: true,
contentPadding: const EdgeInsets.all(20),
border: UnderlineInputBorder(
borderSide: BorderSide.none,
borderRadius: BorderRadius.circular(6),
),
suffixIcon: _passwordInFocus
? IconButton(
icon: Icon(
_passwordVisible
? Icons.visibility
: Icons.visibility_off,
color: Theme.of(context).iconTheme.color,
size: 20,
),
onPressed: () {
setState(() {
_passwordVisible = !_passwordVisible;
});
},
)
: null,
),
style: const TextStyle(
fontSize: 14,
),
controller: _passwordController,
autofocus: true,
autocorrect: false,
obscureText: !_passwordVisible,
keyboardType: TextInputType.visiblePassword,
focusNode: _passwordFocusNode,
onChanged: (_) {
setState(() {});
},
),
),
const Padding(
padding: EdgeInsets.symmetric(vertical: 18),
child: Divider(
thickness: 1,
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {
return const RecoveryPage();
},
),
);
},
child: Center(
child: Text(
context.l10n.forgotPassword,
style: Theme.of(context)
.textTheme
.titleMedium!
.copyWith(
fontSize: 14,
decoration: TextDecoration.underline,
),
),
),
),
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () async {
final dialog = createProgressDialog(
context,
context.l10n.pleaseWait,
);
await dialog.show();
await Configuration.instance.logout();
await dialog.hide();
Navigator.of(context)
.popUntil((route) => route.isFirst);
},
child: Center(
child: Text(
context.l10n.changeEmail,
style: Theme.of(context)
.textTheme
.titleMedium!
.copyWith(
fontSize: 14,
decoration: TextDecoration.underline,
),
),
),
),
],
),
),
],
),
),
),
],
);
}
}

View File

@@ -1,355 +0,0 @@
import 'dart:convert';
import 'dart:io' as io;
import 'package:bip39/bip39.dart' as bip39;
import 'package:dotted_border/dotted_border.dart';
import 'package:ente_auth/core/configuration.dart';
import 'package:ente_auth/core/constants.dart';
import 'package:ente_auth/ente_theme_data.dart';
import 'package:ente_auth/l10n/l10n.dart';
import 'package:ente_auth/ui/common/gradient_button.dart';
import 'package:ente_auth/utils/platform_util.dart';
import 'package:ente_auth/utils/share_utils.dart';
import 'package:ente_auth/utils/toast_util.dart';
import 'package:file_saver/file_saver.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:share_plus/share_plus.dart';
import 'package:step_progress_indicator/step_progress_indicator.dart';
class RecoveryKeyPage extends StatefulWidget {
final bool? showAppBar;
final String recoveryKey;
final String doneText;
final Function()? onDone;
final bool? isDismissible;
final String? title;
final String? text;
final String? subText;
final bool showProgressBar;
const RecoveryKeyPage(
this.recoveryKey,
this.doneText, {
super.key,
this.showAppBar,
this.onDone,
this.isDismissible,
this.title,
this.text,
this.subText,
this.showProgressBar = false,
});
@override
State<RecoveryKeyPage> createState() => _RecoveryKeyPageState();
}
class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
bool _hasTriedToSave = false;
final _recoveryKeyFile = io.File(
"${Configuration.instance.getTempDirectory()}ente-recovery-key.txt",
);
@override
Widget build(BuildContext context) {
final String recoveryKey = bip39.entropyToMnemonic(widget.recoveryKey);
if (recoveryKey.split(' ').length != mnemonicKeyWordCount) {
throw AssertionError(
'recovery code should have $mnemonicKeyWordCount words',
);
}
final double topPadding = widget.showAppBar!
? 40
: widget.showProgressBar
? 32
: 120;
Future<void> copy() async {
await Clipboard.setData(
ClipboardData(
text: recoveryKey,
),
);
showShortToast(
context,
context.l10n.recoveryKeyCopiedToClipboard,
);
setState(() {
_hasTriedToSave = true;
});
}
return Scaffold(
appBar: widget.showProgressBar
? AppBar(
automaticallyImplyLeading: false,
elevation: 0,
title: Hero(
tag: "recovery_key",
child: StepProgressIndicator(
totalSteps: 4,
currentStep: 3,
selectedColor: Theme.of(context).colorScheme.alternativeColor,
roundedEdges: const Radius.circular(10),
unselectedColor:
Theme.of(context).colorScheme.stepProgressUnselectedColor,
),
),
)
: widget.showAppBar!
? AppBar(
elevation: 0,
title: Text(widget.title ?? context.l10n.recoveryKey),
)
: null,
body: Padding(
padding: EdgeInsets.fromLTRB(20, topPadding, 20, 20),
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
minWidth: constraints.maxWidth,
minHeight: constraints.maxHeight,
),
child: IntrinsicHeight(
child: Column(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
widget.showAppBar!
? const SizedBox.shrink()
: Text(
widget.title ?? context.l10n.recoveryKey,
style: Theme.of(context).textTheme.headlineMedium,
),
Padding(
padding: EdgeInsets.all(widget.showAppBar! ? 0 : 12),
),
Text(
widget.text ?? context.l10n.recoveryKeyOnForgotPassword,
style: Theme.of(context).textTheme.titleMedium,
),
const Padding(padding: EdgeInsets.only(top: 24)),
Container(
padding: const EdgeInsets.all(1),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
gradient: const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color(0x8E9610D6),
Color(0x8E9F4FC6),
],
stops: [0.0, 0.9753],
),
),
child: DottedBorder(
padding: EdgeInsets.zero,
borderType: BorderType.RRect,
strokeWidth: 1,
color: const Color(0xFF6B6B6B),
dashPattern: const [6, 6],
radius: const Radius.circular(8),
child: SizedBox(
width: double.infinity,
child: Stack(
children: [
Column(
children: [
Builder(
builder: (context) {
final content = Container(
padding: const EdgeInsets.all(20),
width: double.infinity,
child: Text(
recoveryKey,
textAlign: TextAlign.justify,
style: Theme.of(context)
.textTheme
.bodyLarge,
),
);
if (PlatformUtil.isMobile()) {
return GestureDetector(
onTap: () async => await copy(),
child: content,
);
} else {
return SelectableRegion(
focusNode: FocusNode(),
selectionControls:
PlatformUtil.selectionControls,
child: content,
);
}
},
),
],
),
Positioned(
right: 0,
top: 0,
child: PlatformCopy(
onPressed: copy,
),
),
],
),
),
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 20),
child: Text(
widget.subText ??
context.l10n.recoveryKeySaveDescription,
style: Theme.of(context).textTheme.bodyLarge,
),
),
Expanded(
child: Container(
alignment: Alignment.bottomCenter,
width: double.infinity,
padding: const EdgeInsets.fromLTRB(10, 10, 10, 42),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: _saveOptions(context, recoveryKey),
),
),
),
],
),
),
),
);
},
),
),
);
}
List<Widget> _saveOptions(BuildContext context, String recoveryKey) {
final List<Widget> childrens = [];
if (!_hasTriedToSave) {
childrens.add(
SizedBox(
height: 56,
child: ElevatedButton(
style: Theme.of(context).colorScheme.optionalActionButtonStyle,
onPressed: () async {
await _saveKeys();
},
child: Text(context.l10n.doThisLater),
),
),
);
childrens.add(const SizedBox(height: 10));
}
childrens.add(
GradientButton(
onTap: () async {
await shareDialog(
context,
context.l10n.recoveryKey,
saveAction: () async {
await _saveRecoveryKey(recoveryKey);
},
sendAction: () async {
await _shareRecoveryKey(recoveryKey);
},
);
},
text: context.l10n.saveKey,
),
);
if (_hasTriedToSave) {
childrens.add(const SizedBox(height: 10));
childrens.add(
SizedBox(
height: 56,
child: ElevatedButton(
child: Text(widget.doneText),
onPressed: () async {
await _saveKeys();
},
),
),
);
}
childrens.add(const SizedBox(height: 12));
return childrens;
}
Future _saveRecoveryKey(String recoveryKey) async {
final bytes = utf8.encode(recoveryKey);
final time = DateTime.now().millisecondsSinceEpoch;
await PlatformUtil.shareFile(
"ente_recovery_key_$time",
"txt",
bytes,
MimeType.text,
);
if (mounted) {
showToast(
context,
context.l10n.recoveryKeySaved,
);
setState(() {
_hasTriedToSave = true;
});
}
}
Future _shareRecoveryKey(String recoveryKey) async {
if (_recoveryKeyFile.existsSync()) {
await _recoveryKeyFile.delete();
}
_recoveryKeyFile.writeAsStringSync(recoveryKey);
await Share.shareXFiles([XFile(_recoveryKeyFile.path)]);
Future.delayed(const Duration(milliseconds: 500), () {
if (mounted) {
setState(() {
_hasTriedToSave = true;
});
}
});
}
Future<void> _saveKeys() async {
Navigator.of(context).pop();
if (_recoveryKeyFile.existsSync()) {
await _recoveryKeyFile.delete();
}
widget.onDone!();
}
}
class PlatformCopy extends StatelessWidget {
const PlatformCopy({
super.key,
required this.onPressed,
});
final void Function() onPressed;
@override
Widget build(BuildContext context) {
return IconButton(
onPressed: () => onPressed(),
visualDensity: VisualDensity.compact,
icon: const Icon(
Icons.copy,
size: 16,
),
);
}
}

View File

@@ -1,164 +0,0 @@
import 'package:ente_accounts/pages/password_entry_page.dart';
import 'package:ente_auth/core/configuration.dart';
import 'package:ente_auth/l10n/l10n.dart';
import 'package:ente_auth/ui/common/dynamic_fab.dart';
import 'package:ente_auth/ui/home_page.dart';
import 'package:ente_auth/utils/dialog_util.dart';
import 'package:ente_auth/utils/toast_util.dart';
import 'package:flutter/material.dart';
class RecoveryPage extends StatefulWidget {
const RecoveryPage({super.key});
@override
State<RecoveryPage> createState() => _RecoveryPageState();
}
class _RecoveryPageState extends State<RecoveryPage> {
final _recoveryKey = TextEditingController();
Future<void> onPressed() async {
FocusScope.of(context).unfocus();
final dialog = createProgressDialog(context, "Decrypting...");
await dialog.show();
try {
await Configuration.instance.recover(_recoveryKey.text.trim());
await dialog.hide();
showToast(context, "Recovery successful!");
await Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (BuildContext context) {
return PopScope(
canPop: false,
child: PasswordEntryPage(
Configuration.instance,
PasswordEntryMode.reset,
const HomePage(),
),
);
},
),
);
} catch (e) {
await dialog.hide();
String errMessage = 'The recovery key you entered is incorrect';
if (e is AssertionError) {
errMessage = '$errMessage : ${e.message}';
}
await showErrorDialog(context, "Incorrect recovery key", errMessage);
}
}
@override
Widget build(BuildContext context) {
final isKeypadOpen = MediaQuery.of(context).viewInsets.bottom > 100;
FloatingActionButtonLocation? fabLocation() {
if (isKeypadOpen) {
return null;
} else {
return FloatingActionButtonLocation.centerFloat;
}
}
return Scaffold(
resizeToAvoidBottomInset: isKeypadOpen,
appBar: AppBar(
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back),
color: Theme.of(context).iconTheme.color,
onPressed: () {
Navigator.of(context).pop();
},
),
),
floatingActionButton: DynamicFAB(
isKeypadOpen: isKeypadOpen,
isFormValid: _recoveryKey.text.isNotEmpty,
buttonText: 'Recover',
onPressedFunction: onPressed,
),
floatingActionButtonLocation: fabLocation(),
floatingActionButtonAnimator: NoScalingAnimation(),
body: Column(
children: [
Expanded(
child: ListView(
children: [
Padding(
padding:
const EdgeInsets.symmetric(vertical: 30, horizontal: 20),
child: Text(
context.l10n.forgotPassword,
style: Theme.of(context).textTheme.headlineMedium,
),
),
Padding(
padding: const EdgeInsets.fromLTRB(20, 24, 20, 0),
child: TextFormField(
decoration: InputDecoration(
filled: true,
hintText: "Enter your recovery key",
contentPadding: const EdgeInsets.all(20),
border: UnderlineInputBorder(
borderSide: BorderSide.none,
borderRadius: BorderRadius.circular(6),
),
),
style: const TextStyle(
fontSize: 14,
fontFeatures: [FontFeature.tabularFigures()],
),
controller: _recoveryKey,
autofocus: false,
autocorrect: false,
keyboardType: TextInputType.multiline,
maxLines: null,
onChanged: (_) {
setState(() {});
},
),
),
const Padding(
padding: EdgeInsets.symmetric(vertical: 18),
child: Divider(
thickness: 1,
),
),
Row(
children: [
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
showErrorDialog(
context,
"Sorry",
"Due to the nature of our end-to-end encryption protocol, your data cannot be decrypted without your password or recovery key",
);
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Center(
child: Text(
context.l10n.noRecoveryKeyTitle,
style: Theme.of(context)
.textTheme
.titleMedium!
.copyWith(
fontSize: 14,
decoration: TextDecoration.underline,
),
),
),
),
),
],
),
],
),
),
],
),
);
}
}

View File

@@ -1,219 +0,0 @@
import "dart:convert";
import "dart:typed_data";
import 'package:ente_auth/core/configuration.dart';
import "package:ente_auth/l10n/l10n.dart";
import "package:ente_auth/theme/ente_theme.dart";
import 'package:ente_auth/ui/common/dynamic_fab.dart';
import "package:ente_auth/utils/dialog_util.dart";
import 'package:ente_crypto_dart/ente_crypto_dart.dart';
import 'package:flutter/material.dart';
import "package:logging/logging.dart";
typedef OnPasswordVerifiedFn = Future<void> Function(Uint8List bytes);
class RequestPasswordVerificationPage extends StatefulWidget {
final OnPasswordVerifiedFn onPasswordVerified;
final Function? onPasswordError;
const RequestPasswordVerificationPage({
super.key,
required this.onPasswordVerified,
this.onPasswordError,
});
@override
State<RequestPasswordVerificationPage> createState() =>
_RequestPasswordVerificationPageState();
}
class _RequestPasswordVerificationPageState
extends State<RequestPasswordVerificationPage> {
final _logger = Logger((_RequestPasswordVerificationPageState).toString());
final _passwordController = TextEditingController();
final FocusNode _passwordFocusNode = FocusNode();
String? email;
bool _passwordInFocus = false;
bool _passwordVisible = false;
@override
void initState() {
super.initState();
email = Configuration.instance.getEmail();
_passwordFocusNode.addListener(() {
setState(() {
_passwordInFocus = _passwordFocusNode.hasFocus;
});
});
}
@override
Widget build(BuildContext context) {
final isKeypadOpen = MediaQuery.of(context).viewInsets.bottom > 100;
FloatingActionButtonLocation? fabLocation() {
if (isKeypadOpen) {
return null;
} else {
return FloatingActionButtonLocation.centerFloat;
}
}
return Scaffold(
resizeToAvoidBottomInset: isKeypadOpen,
appBar: AppBar(
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back),
color: Theme.of(context).iconTheme.color,
onPressed: () {
Navigator.of(context).pop();
},
),
),
body: _getBody(),
floatingActionButton: DynamicFAB(
key: const ValueKey("verifyPasswordButton"),
isKeypadOpen: isKeypadOpen,
isFormValid: _passwordController.text.isNotEmpty,
buttonText: context.l10n.verifyPassword,
onPressedFunction: () async {
FocusScope.of(context).unfocus();
final dialog = createProgressDialog(context, context.l10n.pleaseWait);
await dialog.show();
try {
final attributes = Configuration.instance.getKeyAttributes()!;
final Uint8List keyEncryptionKey = await CryptoUtil.deriveKey(
utf8.encode(_passwordController.text),
CryptoUtil.base642bin(attributes.kekSalt),
attributes.memLimit,
attributes.opsLimit,
);
CryptoUtil.decryptSync(
CryptoUtil.base642bin(attributes.encryptedKey),
keyEncryptionKey,
CryptoUtil.base642bin(attributes.keyDecryptionNonce),
);
await dialog.show();
// pop
await widget.onPasswordVerified(keyEncryptionKey);
await dialog.hide();
Navigator.of(context).pop(true);
} catch (e, s) {
_logger.severe("Error while verifying password", e, s);
await dialog.hide();
if (widget.onPasswordError != null) {
widget.onPasswordError!();
} else {
// ignore: unawaited_futures
showErrorDialog(
context,
context.l10n.incorrectPasswordTitle,
context.l10n.pleaseTryAgain,
);
}
}
},
),
floatingActionButtonLocation: fabLocation(),
floatingActionButtonAnimator: NoScalingAnimation(),
);
}
Widget _getBody() {
return Column(
children: [
Expanded(
child: AutofillGroup(
child: ListView(
children: [
Padding(
padding: const EdgeInsets.only(top: 30, left: 20, right: 20),
child: Text(
context.l10n.enterPassword,
style: Theme.of(context).textTheme.headlineMedium,
),
),
Padding(
padding: const EdgeInsets.only(
bottom: 30,
left: 22,
right: 20,
),
child: Text(
email ?? '',
style: getEnteTextTheme(context).smallMuted,
),
),
Visibility(
// hidden textForm for suggesting auto-fill service for saving
// password
visible: false,
child: TextFormField(
autofillHints: const [
AutofillHints.email,
],
autocorrect: false,
keyboardType: TextInputType.emailAddress,
initialValue: email,
textInputAction: TextInputAction.next,
),
),
Padding(
padding: const EdgeInsets.fromLTRB(20, 24, 20, 0),
child: TextFormField(
key: const ValueKey("passwordInputField"),
autofillHints: const [AutofillHints.password],
decoration: InputDecoration(
hintText: context.l10n.enterYourPasswordHint,
filled: true,
contentPadding: const EdgeInsets.all(20),
border: UnderlineInputBorder(
borderSide: BorderSide.none,
borderRadius: BorderRadius.circular(6),
),
suffixIcon: _passwordInFocus
? IconButton(
icon: Icon(
_passwordVisible
? Icons.visibility
: Icons.visibility_off,
color: Theme.of(context).iconTheme.color,
size: 20,
),
onPressed: () {
setState(() {
_passwordVisible = !_passwordVisible;
});
},
)
: null,
),
style: const TextStyle(
fontSize: 14,
),
controller: _passwordController,
autofocus: true,
autocorrect: false,
obscureText: !_passwordVisible,
keyboardType: TextInputType.visiblePassword,
focusNode: _passwordFocusNode,
onChanged: (_) {
setState(() {});
},
),
),
const Padding(
padding: EdgeInsets.symmetric(vertical: 18),
child: Divider(
thickness: 1,
),
),
],
),
),
),
],
);
}
}

View File

@@ -3,6 +3,7 @@ import 'dart:io';
import 'package:app_links/app_links.dart';
import 'package:collection/collection.dart';
import 'package:ente_accounts/services/user_service.dart';
import 'package:ente_auth/core/configuration.dart';
import 'package:ente_auth/ente_theme_data.dart';
import 'package:ente_auth/events/codes_updated_event.dart';
@@ -14,7 +15,6 @@ import 'package:ente_auth/onboarding/model/tag_enums.dart';
import 'package:ente_auth/onboarding/view/common/tag_chip.dart';
import 'package:ente_auth/onboarding/view/setup_enter_secret_key_page.dart';
import 'package:ente_auth/services/preference_service.dart';
import 'package:ente_auth/services/user_service.dart';
import 'package:ente_auth/store/code_display_store.dart';
import 'package:ente_auth/store/code_store.dart';
import 'package:ente_auth/theme/ente_theme.dart';

View File

@@ -1,11 +1,11 @@
import 'package:ente_accounts/pages/change_email_dialog.dart';
import 'package:ente_accounts/pages/delete_account_page.dart';
import 'package:ente_accounts/pages/password_entry_page.dart';
import 'package:ente_accounts/pages/recovery_key_page.dart';
import 'package:ente_accounts/services/user_service.dart';
import 'package:ente_auth/core/configuration.dart';
import 'package:ente_auth/l10n/l10n.dart';
import 'package:ente_auth/services/user_service.dart';
import 'package:ente_auth/theme/ente_theme.dart';
import 'package:ente_auth/ui/account/recovery_key_page.dart';
import 'package:ente_auth/ui/components/captioned_text_widget.dart';
import 'package:ente_auth/ui/components/expandable_menu_item_widget.dart';
import 'package:ente_auth/ui/components/menu_item_widget.dart';
@@ -124,6 +124,7 @@ class AccountSectionWidget extends StatelessWidget {
routeToPage(
context,
RecoveryKeyPage(
Configuration.instance,
recoveryKey,
l10n.ok,
showAppBar: true,

View File

@@ -1,10 +1,10 @@
import 'dart:io';
import 'package:ente_accounts/models/user_details.dart';
import 'package:ente_accounts/services/user_service.dart';
import 'package:ente_auth/core/configuration.dart';
import 'package:ente_auth/l10n/l10n.dart';
import 'package:ente_auth/l10n/l10n.dart';
import 'package:ente_auth/services/preference_service.dart';
import 'package:ente_auth/services/user_service.dart';
import 'package:ente_auth/ui/components/banner_widget.dart';
import 'package:flutter/material.dart';

View File

@@ -2,13 +2,13 @@ import 'dart:async';
import 'dart:typed_data';
import 'package:ente_accounts/models/user_details.dart';
import 'package:ente_accounts/pages/request_pwd_verification_page.dart';
import 'package:ente_accounts/pages/sessions_page.dart';
import 'package:ente_accounts/services/passkey_service.dart';
import 'package:ente_accounts/services/user_service.dart';
import 'package:ente_auth/core/configuration.dart';
import 'package:ente_auth/l10n/l10n.dart';
import 'package:ente_auth/services/passkey_service.dart';
import 'package:ente_auth/services/user_service.dart';
import 'package:ente_auth/theme/ente_theme.dart';
import 'package:ente_auth/ui/account/request_pwd_verification_page.dart';
import 'package:ente_auth/ui/components/buttons/button_widget.dart';
import 'package:ente_auth/ui/components/captioned_text_widget.dart';
import 'package:ente_auth/ui/components/expandable_menu_item_widget.dart';
@@ -243,6 +243,7 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
await routeToPage(
context,
RequestPasswordVerificationPage(
Configuration.instance,
onPasswordVerified: (Uint8List keyEncryptionKey) async {
final Uint8List loginKey =
await CryptoUtil.deriveLoginKey(keyEncryptionKey);

View File

@@ -1,9 +1,9 @@
import 'dart:io';
import 'package:ente_accounts/services/user_service.dart';
import 'package:ente_auth/core/configuration.dart';
import 'package:ente_auth/l10n/l10n.dart';
import 'package:ente_auth/onboarding/view/onboarding_page.dart';
import 'package:ente_auth/services/user_service.dart';
import 'package:ente_auth/store/code_store.dart';
import 'package:ente_auth/theme/colors.dart';
import 'package:ente_auth/theme/ente_theme.dart';