From 0a05fc917bcc08ec7262cf83ebc0976178054cbb Mon Sep 17 00:00:00 2001 From: vishnukvmd Date: Wed, 16 Aug 2023 13:26:35 +0530 Subject: [PATCH 01/23] Update Podfile --- macos/Podfile.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index f73488907..678bce8da 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -88,14 +88,14 @@ SPEC CHECKSUMS: FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a package_info_plus_macos: f010621b07802a241d96d01876d6705f15e77c1c - path_provider_foundation: 37748e03f12783f9de2cb2c4eadfaa25fe6d4852 + path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 Reachability: 33e18b67625424e47b6cde6d202dce689ad7af96 Sentry: 4c9babff9034785067c896fd580b1f7de44da020 - sentry_flutter: b10ae7a5ddcbc7f04648eeb2672b5747230172f1 + sentry_flutter: 1346a880b24c0240807b53b10cf50ddad40f504e share_plus_macos: 853ee48e7dce06b633998ca0735d482dd671ade4 - shared_preferences_foundation: 297b3ebca31b34ec92be11acd7fb0ba932c822ca + shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 sqflite: a5789cceda41d54d23f31d6de539d65bb14100ea - url_launcher_macos: c04e4fa86382d4f94f6b38f14625708be3ae52e2 + url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95 PODFILE CHECKSUM: 353c8bcc5d5b0994e508d035b5431cfe18c1dea7 From 52e9567a12a17d65d25cfbd9d943b82e9f43f1da Mon Sep 17 00:00:00 2001 From: vishnukvmd Date: Wed, 16 Aug 2023 13:27:13 +0530 Subject: [PATCH 02/23] Persist user preference for offline mode --- lib/core/configuration.dart | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/lib/core/configuration.dart b/lib/core/configuration.dart index 9857e0b44..d7d1b21d4 100644 --- a/lib/core/configuration.dart +++ b/lib/core/configuration.dart @@ -38,6 +38,7 @@ class Configuration { static const encryptedTokenKey = "encrypted_token"; static const userIDKey = "user_id"; static const hasMigratedSecureStorageKey = "has_migrated_secure_storage"; + static const hasOptedForOfflineModeKey = "has_opted_for_offline_mode"; final kTempFolderDeletionTimeBuffer = const Duration(days: 1).inMicroseconds; @@ -179,8 +180,9 @@ class Configuration { return KeyGenResult(attributes, privateAttributes, loginKey); } - - Future> getAttributesForNewPassword(String password) async { + Future> getAttributesForNewPassword( + String password, + ) async { // Get master key final masterKey = getKey(); @@ -215,18 +217,16 @@ class Configuration { // SRP setup for existing users. Future decryptSecretsAndGetKeyEncKey( String password, - KeyAttributes attributes, - { + KeyAttributes attributes, { Uint8List? keyEncryptionKey, - } - ) async { + }) async { _logger.info('Start decryptAndSaveSecrets'); keyEncryptionKey ??= await CryptoUtil.deriveKey( - utf8.encode(password) as Uint8List, - Sodium.base642bin(attributes.kekSalt), - attributes.memLimit, - attributes.opsLimit, - ); + utf8.encode(password) as Uint8List, + Sodium.base642bin(attributes.kekSalt), + attributes.memLimit, + attributes.opsLimit, + ); _logger.info('user-key done'); Uint8List key; @@ -446,6 +446,14 @@ class Configuration { return getToken() != null && _key != null; } + bool hasOptedForOfflineMode() { + return _preferences.getBool(hasOptedForOfflineModeKey) ?? false; + } + + Future optForOfflineMode() { + return _preferences.setBool(hasOptedForOfflineModeKey, true); + } + bool shouldShowLockScreen() { if (_preferences.containsKey(keyShouldShowLockScreen)) { return _preferences.getBool(keyShouldShowLockScreen)!; From 34a39a2a86aec6764b3e290bbaf85352c45f6f15 Mon Sep 17 00:00:00 2001 From: vishnukvmd Date: Wed, 16 Aug 2023 13:27:30 +0530 Subject: [PATCH 03/23] Redirect to home page if user has opted for offline mode --- lib/app/view/app.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/app/view/app.dart b/lib/app/view/app.dart index 2f8e9f7ab..b723e54b9 100644 --- a/lib/app/view/app.dart +++ b/lib/app/view/app.dart @@ -1,4 +1,3 @@ - import 'dart:async'; import 'dart:io'; @@ -129,7 +128,8 @@ class _AppState extends State { Map get _getRoutes { return { - "/": (context) => Configuration.instance.hasConfiguredAccount() + "/": (context) => Configuration.instance.hasConfiguredAccount() || + Configuration.instance.hasOptedForOfflineMode() ? const HomePage() : const OnboardingPage(), }; From 54817cc10024c2184d6e6198da8837becac3c103 Mon Sep 17 00:00:00 2001 From: vishnukvmd Date: Wed, 16 Aug 2023 13:27:55 +0530 Subject: [PATCH 04/23] Add an option in the onboarding page to opt in for offline mode --- lib/ente_theme_data.dart | 2 +- lib/l10n/arb/app_en.arb | 5 +-- lib/onboarding/view/onboarding_page.dart | 43 +++++++++++++++++++++--- 3 files changed, 43 insertions(+), 7 deletions(-) diff --git a/lib/ente_theme_data.dart b/lib/ente_theme_data.dart index 7837e0360..176e61131 100644 --- a/lib/ente_theme_data.dart +++ b/lib/ente_theme_data.dart @@ -344,7 +344,7 @@ extension CustomColorScheme on ColorScheme { ? const Color.fromRGBO(245, 245, 245, 1.0) : const Color.fromRGBO(30, 30, 30, 1.0); - Color get searchResultsCountTextColor => brightness == Brightness.light + Color get mutedTextColor => brightness == Brightness.light ? const Color.fromRGBO(80, 80, 80, 1) : const Color.fromRGBO(150, 150, 150, 1); diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 44b5e36b6..0ac9314c6 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -5,7 +5,7 @@ "@counterAppBarTitle": { "description": "Text shown in the AppBar of the Counter Page" }, - "onBoardingBody": "Secure your 2FA codes", + "onBoardingBody": "Securely backup your 2FA codes", "onBoardingGetStarted": "Get Started", "setupFirstAccount": "Setup your first account", "importScanQrCode": "Scan a QR Code", @@ -315,5 +315,6 @@ "encrypted": "Encrypted", "plainText": "Plain text", "passwordToEncryptExport": "Password to encrypt export", - "export": "Export" + "export": "Export", + "useOffline": "Use without backups" } diff --git a/lib/onboarding/view/onboarding_page.dart b/lib/onboarding/view/onboarding_page.dart index ccb04268f..4b245d67a 100644 --- a/lib/onboarding/view/onboarding_page.dart +++ b/lib/onboarding/view/onboarding_page.dart @@ -7,6 +7,7 @@ import 'package:ente_auth/ente_theme_data.dart'; import 'package:ente_auth/events/trigger_logout_event.dart'; 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/email_entry_page.dart'; import 'package:ente_auth/ui/account/login_page.dart'; import 'package:ente_auth/ui/account/logout_dialog.dart'; @@ -111,7 +112,9 @@ class _OnboardingPageState extends State { textAlign: TextAlign.center, style: Theme.of(context).textTheme.headline6!.copyWith( - color: Colors.white38, + color: Theme.of(context) + .colorScheme + .mutedTextColor, ), ), ], @@ -128,7 +131,7 @@ class _OnboardingPageState extends State { const SizedBox(height: 4), Container( width: double.infinity, - padding: const EdgeInsets.fromLTRB(20, 12, 20, 28), + padding: const EdgeInsets.fromLTRB(20, 12, 20, 0), child: Hero( tag: "log_in", child: ElevatedButton( @@ -145,6 +148,23 @@ class _OnboardingPageState extends State { ), ), ), + const SizedBox(height: 4), + Container( + width: double.infinity, + padding: const EdgeInsets.only(top: 20, bottom: 20), + child: GestureDetector( + onTap: _optForOfflineMode, + child: Center( + child: Text( + l10n.useOffline, + style: body.copyWith( + color: + Theme.of(context).colorScheme.mutedTextColor, + ), + ), + ), + ), + ), ], ), ), @@ -155,6 +175,17 @@ class _OnboardingPageState extends State { ); } + Future _optForOfflineMode() async { + await Configuration.instance.optForOfflineMode(); + Navigator.of(context).push( + MaterialPageRoute( + builder: (BuildContext context) { + return const HomePage(); + }, + ), + ); + } + void _navigateToSignUpPage() { Widget page; if (Configuration.instance.getEncryptedToken() == null) { @@ -163,7 +194,9 @@ class _OnboardingPageState extends State { // No key if (Configuration.instance.getKeyAttributes() == null) { // Never had a key - page = const PasswordEntryPage(mode: PasswordEntryMode.set,); + page = const PasswordEntryPage( + mode: PasswordEntryMode.set, + ); } else if (Configuration.instance.getKey() == null) { // Yet to decrypt the key page = const PasswordReentryPage(); @@ -189,7 +222,9 @@ class _OnboardingPageState extends State { // No key if (Configuration.instance.getKeyAttributes() == null) { // Never had a key - page = const PasswordEntryPage(mode: PasswordEntryMode.set,); + page = const PasswordEntryPage( + mode: PasswordEntryMode.set, + ); } else if (Configuration.instance.getKey() == null) { // Yet to decrypt the key page = const PasswordReentryPage(); From 7d8a85b86118d07f5c4e65e398903a1d2fa8fc62 Mon Sep 17 00:00:00 2001 From: vishnukvmd Date: Wed, 16 Aug 2023 13:28:09 +0530 Subject: [PATCH 05/23] Modify Settings page to work for offline mode --- lib/ui/settings/security_section_widget.dart | 201 ++++++++++--------- lib/ui/settings/support_dev_widget.dart | 88 ++++---- lib/ui/settings_page.dart | 58 +++--- 3 files changed, 187 insertions(+), 160 deletions(-) diff --git a/lib/ui/settings/security_section_widget.dart b/lib/ui/settings/security_section_widget.dart index 2ea3695f9..b228ef512 100644 --- a/lib/ui/settings/security_section_widget.dart +++ b/lib/ui/settings/security_section_widget.dart @@ -31,9 +31,11 @@ class SecuritySectionWidget extends StatefulWidget { class _SecuritySectionWidgetState extends State { final _config = Configuration.instance; + late bool _hasLoggedIn; @override void initState() { + _hasLoggedIn = _config.hasConfiguredAccount(); super.initState(); } @@ -53,49 +55,103 @@ class _SecuritySectionWidgetState extends State { } Widget _getSectionOptions(BuildContext context) { - final bool? canDisableMFA = UserService.instance.canDisableEmailMFA(); - if (canDisableMFA == null) { - // We don't know if the user can disable MFA yet, so we fetch the info - UserService.instance.getUserDetailsV2().ignore(); - } final l10n = context.l10n; final List children = []; - children.addAll([ - sectionOptionSpacing, - MenuItemWidget( - captionedTextWidget: CaptionedTextWidget( - title: l10n.recoveryKey, - ), - pressedColor: getEnteColorScheme(context).fillFaint, - trailingIcon: Icons.chevron_right_outlined, - trailingIconIsMuted: true, - onTap: () async { - final hasAuthenticated = await LocalAuthenticationService.instance - .requestLocalAuthentication( - context, - l10n.authToViewYourRecoveryKey, - ); - if (hasAuthenticated) { - String recoveryKey; - try { - recoveryKey = - Sodium.bin2hex(Configuration.instance.getRecoveryKey()); - } catch (e) { - showGenericErrorDialog(context: context); - return; - } - routeToPage( + if (_hasLoggedIn) { + final bool? canDisableMFA = UserService.instance.canDisableEmailMFA(); + if (canDisableMFA == null) { + // We don't know if the user can disable MFA yet, so we fetch the info + UserService.instance.getUserDetailsV2().ignore(); + } + children.addAll([ + sectionOptionSpacing, + MenuItemWidget( + captionedTextWidget: CaptionedTextWidget( + title: l10n.recoveryKey, + ), + pressedColor: getEnteColorScheme(context).fillFaint, + trailingIcon: Icons.chevron_right_outlined, + trailingIconIsMuted: true, + onTap: () async { + final hasAuthenticated = await LocalAuthenticationService.instance + .requestLocalAuthentication( context, - RecoveryKeyPage( - recoveryKey, - l10n.ok, - showAppBar: true, - onDone: () {}, - ), + l10n.authToViewYourRecoveryKey, ); - } - }, - ), + if (hasAuthenticated) { + String recoveryKey; + try { + recoveryKey = + Sodium.bin2hex(Configuration.instance.getRecoveryKey()); + } catch (e) { + showGenericErrorDialog(context: context); + return; + } + routeToPage( + context, + RecoveryKeyPage( + recoveryKey, + l10n.ok, + showAppBar: true, + onDone: () {}, + ), + ); + } + }, + ), + MenuItemWidget( + captionedTextWidget: CaptionedTextWidget( + title: l10n.emailVerificationToggle, + ), + trailingWidget: ToggleSwitchWidget( + value: () => UserService.instance.hasEmailMFAEnabled(), + onChanged: () async { + final hasAuthenticated = await LocalAuthenticationService.instance + .requestLocalAuthentication( + context, + l10n.authToChangeEmailVerificationSetting, + ); + final isEmailMFAEnabled = + UserService.instance.hasEmailMFAEnabled(); + if (hasAuthenticated) { + await updateEmailMFA(!isEmailMFAEnabled); + if (mounted) { + setState(() {}); + } + } + }, + ), + ), + sectionOptionSpacing, + MenuItemWidget( + captionedTextWidget: CaptionedTextWidget( + title: context.l10n.viewActiveSessions, + ), + pressedColor: getEnteColorScheme(context).fillFaint, + trailingIcon: Icons.chevron_right_outlined, + trailingIconIsMuted: true, + onTap: () async { + final hasAuthenticated = await LocalAuthenticationService.instance + .requestLocalAuthentication( + context, + context.l10n.authToViewYourActiveSessions, + ); + if (hasAuthenticated) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (BuildContext context) { + return const SessionsPage(); + }, + ), + ); + } + }, + ), + ]); + } else { + children.add(sectionOptionSpacing); + } + children.addAll([ MenuItemWidget( captionedTextWidget: CaptionedTextWidget( title: l10n.lockscreen, @@ -117,54 +173,6 @@ class _SecuritySectionWidgetState extends State { ), ), sectionOptionSpacing, - MenuItemWidget( - captionedTextWidget: CaptionedTextWidget( - title: l10n.emailVerificationToggle, - ), - trailingWidget: ToggleSwitchWidget( - value: () => UserService.instance.hasEmailMFAEnabled(), - onChanged: () async { - final hasAuthenticated = await LocalAuthenticationService.instance - .requestLocalAuthentication( - context, - l10n.authToChangeEmailVerificationSetting, - ); - final isEmailMFAEnabled = UserService.instance.hasEmailMFAEnabled(); - if (hasAuthenticated) { - await updateEmailMFA(!isEmailMFAEnabled); - if (mounted) { - setState(() {}); - } - } - }, - ), - ), - sectionOptionSpacing, - MenuItemWidget( - captionedTextWidget: CaptionedTextWidget( - title: context.l10n.viewActiveSessions, - ), - pressedColor: getEnteColorScheme(context).fillFaint, - trailingIcon: Icons.chevron_right_outlined, - trailingIconIsMuted: true, - onTap: () async { - final hasAuthenticated = await LocalAuthenticationService.instance - .requestLocalAuthentication( - context, - context.l10n.authToViewYourActiveSessions, - ); - if (hasAuthenticated) { - Navigator.of(context).push( - MaterialPageRoute( - builder: (BuildContext context) { - return const SessionsPage(); - }, - ), - ); - } - }, - ), - sectionOptionSpacing, ]); return Column( children: children, @@ -173,12 +181,19 @@ class _SecuritySectionWidgetState extends State { Future updateEmailMFA(bool isEnabled) async { try { - final UserDetails details = await UserService.instance.getUserDetailsV2(memoryCount: false); - if(details.profileData?.canDisableEmailMFA == false) { - await routeToPage(context, RequestPasswordVerificationPage(onPasswordVerified: (Uint8List keyEncryptionKey) async { - final Uint8List loginKey = await CryptoUtil.deriveLoginKey(keyEncryptionKey); - await UserService.instance.registerOrUpdateSrp(loginKey); - },),); + final UserDetails details = + await UserService.instance.getUserDetailsV2(memoryCount: false); + if (details.profileData?.canDisableEmailMFA == false) { + await routeToPage( + context, + RequestPasswordVerificationPage( + onPasswordVerified: (Uint8List keyEncryptionKey) async { + final Uint8List loginKey = + await CryptoUtil.deriveLoginKey(keyEncryptionKey); + await UserService.instance.registerOrUpdateSrp(loginKey); + }, + ), + ); } await UserService.instance.updateEmailMFA(isEnabled); } catch (e) { diff --git a/lib/ui/settings/support_dev_widget.dart b/lib/ui/settings/support_dev_widget.dart index a5be4da45..20429b2f4 100644 --- a/lib/ui/settings/support_dev_widget.dart +++ b/lib/ui/settings/support_dev_widget.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:ente_auth/core/configuration.dart'; import 'package:ente_auth/l10n/l10n.dart'; import 'package:ente_auth/models/subscription.dart'; import 'package:ente_auth/services/billing_service.dart'; @@ -18,50 +19,53 @@ class SupportDevWidget extends StatelessWidget { final l10n = context.l10n; // fetch - return FutureBuilder( - future: BillingService.instance.getSubscription(), - builder: (context, snapshot) { - if (snapshot.hasData) { - final subscription = snapshot.data; - if (subscription != null && subscription.productID == "free") { - return GestureDetector( - onTap: () { - launchUrl(Uri.parse("https://ente.io")); - }, - child: Padding( - padding: - const EdgeInsets.symmetric(vertical: 12.0, horizontal: 6), - child: Column( - children: [ - StyledText( - text: l10n.supportDevs, - tags: { - 'bold-green': StyledTextTag( - style: TextStyle( - fontWeight: FontWeight.bold, - color: getEnteColorScheme(context).primaryGreen, - ), - ), - }, - ), - const Padding(padding: EdgeInsets.all(6)), - Platform.isAndroid - ? Text( - l10n.supportDiscount, - textAlign: TextAlign.center, - style: const TextStyle( - color: Colors.grey, + if (Configuration.instance.hasConfiguredAccount()) { + return FutureBuilder( + future: BillingService.instance.getSubscription(), + builder: (context, snapshot) { + if (snapshot.hasData) { + final subscription = snapshot.data; + if (subscription != null && subscription.productID == "free") { + return GestureDetector( + onTap: () { + launchUrl(Uri.parse("https://ente.io")); + }, + child: Padding( + padding: + const EdgeInsets.symmetric(vertical: 12.0, horizontal: 6), + child: Column( + children: [ + StyledText( + text: l10n.supportDevs, + tags: { + 'bold-green': StyledTextTag( + style: TextStyle( + fontWeight: FontWeight.bold, + color: getEnteColorScheme(context).primaryGreen, ), - ) - : const SizedBox.shrink(), - ], + ), + }, + ), + const Padding(padding: EdgeInsets.all(6)), + Platform.isAndroid + ? Text( + l10n.supportDiscount, + textAlign: TextAlign.center, + style: const TextStyle( + color: Colors.grey, + ), + ) + : const SizedBox.shrink(), + ], + ), ), - ), - ); + ); + } } - } - return const SizedBox.shrink(); - }, - ); + return const SizedBox.shrink(); + }, + ); + } + return const SizedBox.shrink(); } } diff --git a/lib/ui/settings_page.dart b/lib/ui/settings_page.dart index 5bcb0b34b..6151b695e 100644 --- a/lib/ui/settings_page.dart +++ b/lib/ui/settings_page.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:ente_auth/core/configuration.dart'; import 'package:ente_auth/services/user_service.dart'; import 'package:ente_auth/theme/colors.dart'; import 'package:ente_auth/theme/ente_theme.dart'; @@ -18,12 +19,15 @@ import 'package:flutter/material.dart'; class SettingsPage extends StatelessWidget { final ValueNotifier emailNotifier; - const SettingsPage({Key? key, required this.emailNotifier}) : super(key: key); + final _hasLoggedIn = Configuration.instance.hasConfiguredAccount(); + SettingsPage({Key? key, required this.emailNotifier}) : super(key: key); @override Widget build(BuildContext context) { - UserService.instance.getUserDetailsV2().ignore(); + if (_hasLoggedIn) { + UserService.instance.getUserDetailsV2().ignore(); + } final enteColorScheme = getEnteColorScheme(context); return Scaffold( body: Container( @@ -35,33 +39,37 @@ class SettingsPage extends StatelessWidget { Widget _getBody(BuildContext context, EnteColorScheme colorScheme) { final enteTextTheme = getEnteTextTheme(context); + const sectionSpacing = SizedBox(height: 8); final List contents = []; - contents.add( - Container( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: Align( - alignment: Alignment.centerLeft, - child: AnimatedBuilder( - // [AnimatedBuilder] accepts any [Listenable] subtype. - animation: emailNotifier, - builder: (BuildContext context, Widget? child) { - return Text( - emailNotifier.value!, - style: enteTextTheme.body.copyWith( - color: colorScheme.textMuted, - overflow: TextOverflow.ellipsis, - ), - ); - }, + if (_hasLoggedIn) { + contents.add( + Container( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Align( + alignment: Alignment.centerLeft, + child: AnimatedBuilder( + // [AnimatedBuilder] accepts any [Listenable] subtype. + animation: emailNotifier, + builder: (BuildContext context, Widget? child) { + return Text( + emailNotifier.value!, + style: enteTextTheme.body.copyWith( + color: colorScheme.textMuted, + overflow: TextOverflow.ellipsis, + ), + ); + }, + ), ), ), - ), - ); - const sectionSpacing = SizedBox(height: 8); - contents.add(const SizedBox(height: 12)); + ); + contents.addAll([ + const SizedBox(height: 12), + AccountSectionWidget(), + sectionSpacing, + ]); + } contents.addAll([ - AccountSectionWidget(), - sectionSpacing, DataSectionWidget(), sectionSpacing, const SecuritySectionWidget(), From 945d6d6728cf5e54f096eefe7b71bac94bf64ad1 Mon Sep 17 00:00:00 2001 From: vishnukvmd Date: Wed, 16 Aug 2023 14:00:19 +0530 Subject: [PATCH 06/23] Generate and save localKey --- lib/core/configuration.dart | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/lib/core/configuration.dart b/lib/core/configuration.dart index d7d1b21d4..b2cd7a276 100644 --- a/lib/core/configuration.dart +++ b/lib/core/configuration.dart @@ -30,6 +30,7 @@ class Configuration { static const emailKey = "email"; static const keyAttributesKey = "key_attributes"; static const keyKey = "key"; + static const localKeyKey = "local_key"; static const keyShouldShowLockScreen = "should_show_lock_screen"; static const lastTempFolderClearTimeKey = "last_temp_folder_clear_time"; static const secretKeyKey = "secret_key"; @@ -53,6 +54,7 @@ class Configuration { late FlutterSecureStorage _secureStorage; late String _tempDirectory; late String _thumbnailCacheDirectory; + late Uint8List _localKey; // 6th July 22: Remove this after 3 months. Hopefully, active users // will migrate to newer version of the app, where shared media is stored @@ -116,6 +118,7 @@ class Configuration { } await _migrateSecurityStorageToFirstUnlock(); } + await _setupLocalKey(); } Future logout({bool autoLogout = false}) async { @@ -300,6 +303,10 @@ class Configuration { ); } + Uint8List getLocalKey() { + return _localKey; + } + String getHttpEndpoint() { return endpoint; } @@ -474,6 +481,30 @@ class Configuration { return _volatilePassword; } + + Future _setupLocalKey() async { + final localKey = await _secureStorage.read( + key: localKeyKey, + iOptions: _secureStorageOptionsIOS, + ); + + if (localKey == null) { + await _generateLocalKey(); + return _setupLocalKey(); + } else { + _localKey = Sodium.base642bin(localKey); + } + } + + Future _generateLocalKey() async { + final key = CryptoUtil.generateKey(); + await _secureStorage.write( + key: localKeyKey, + value: Sodium.bin2base64(key), + iOptions: _secureStorageOptionsIOS, + ); + } + Future _migrateSecurityStorageToFirstUnlock() async { final hasMigratedSecureStorageToFirstUnlock = _preferences.getBool(hasMigratedSecureStorageKey) ?? false; From 3b710e9274919fa7671516142cef97c7083df08c Mon Sep 17 00:00:00 2001 From: vishnukvmd Date: Sun, 20 Aug 2023 21:49:35 +0530 Subject: [PATCH 07/23] Revert "Generate and save localKey" This reverts commit 945d6d6728cf5e54f096eefe7b71bac94bf64ad1. --- lib/core/configuration.dart | 31 ------------------------------- 1 file changed, 31 deletions(-) diff --git a/lib/core/configuration.dart b/lib/core/configuration.dart index b2cd7a276..d7d1b21d4 100644 --- a/lib/core/configuration.dart +++ b/lib/core/configuration.dart @@ -30,7 +30,6 @@ class Configuration { static const emailKey = "email"; static const keyAttributesKey = "key_attributes"; static const keyKey = "key"; - static const localKeyKey = "local_key"; static const keyShouldShowLockScreen = "should_show_lock_screen"; static const lastTempFolderClearTimeKey = "last_temp_folder_clear_time"; static const secretKeyKey = "secret_key"; @@ -54,7 +53,6 @@ class Configuration { late FlutterSecureStorage _secureStorage; late String _tempDirectory; late String _thumbnailCacheDirectory; - late Uint8List _localKey; // 6th July 22: Remove this after 3 months. Hopefully, active users // will migrate to newer version of the app, where shared media is stored @@ -118,7 +116,6 @@ class Configuration { } await _migrateSecurityStorageToFirstUnlock(); } - await _setupLocalKey(); } Future logout({bool autoLogout = false}) async { @@ -303,10 +300,6 @@ class Configuration { ); } - Uint8List getLocalKey() { - return _localKey; - } - String getHttpEndpoint() { return endpoint; } @@ -481,30 +474,6 @@ class Configuration { return _volatilePassword; } - - Future _setupLocalKey() async { - final localKey = await _secureStorage.read( - key: localKeyKey, - iOptions: _secureStorageOptionsIOS, - ); - - if (localKey == null) { - await _generateLocalKey(); - return _setupLocalKey(); - } else { - _localKey = Sodium.base642bin(localKey); - } - } - - Future _generateLocalKey() async { - final key = CryptoUtil.generateKey(); - await _secureStorage.write( - key: localKeyKey, - value: Sodium.bin2base64(key), - iOptions: _secureStorageOptionsIOS, - ); - } - Future _migrateSecurityStorageToFirstUnlock() async { final hasMigratedSecureStorageToFirstUnlock = _preferences.getBool(hasMigratedSecureStorageKey) ?? false; From 05a36b051dafd8e50d7520a9acb5aaca83e3e3af Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Thu, 31 Aug 2023 09:33:03 +0530 Subject: [PATCH 08/23] Clean up configuration --- lib/core/configuration.dart | 110 +++++++----------------------------- 1 file changed, 19 insertions(+), 91 deletions(-) diff --git a/lib/core/configuration.dart b/lib/core/configuration.dart index d7d1b21d4..d54da4d44 100644 --- a/lib/core/configuration.dart +++ b/lib/core/configuration.dart @@ -29,9 +29,10 @@ class Configuration { ); static const emailKey = "email"; static const keyAttributesKey = "key_attributes"; - static const keyKey = "key"; + static const keyShouldShowLockScreen = "should_show_lock_screen"; static const lastTempFolderClearTimeKey = "last_temp_folder_clear_time"; + static const keyKey = "key"; static const secretKeyKey = "secret_key"; static const authSecretKeyKey = "auth_secret_key"; static const tokenKey = "token"; @@ -46,28 +47,19 @@ class Configuration { String? _cachedToken; late String _documentsDirectory; - String? _key; late SharedPreferences _preferences; + String? _key; String? _secretKey; String? _authSecretKey; late FlutterSecureStorage _secureStorage; late String _tempDirectory; - late String _thumbnailCacheDirectory; - // 6th July 22: Remove this after 3 months. Hopefully, active users - // will migrate to newer version of the app, where shared media is stored - // on appSupport directory which OS won't clean up automatically - late String _sharedTempMediaDirectory; - - late String _sharedDocumentsMediaDirectory; String? _volatilePassword; final _secureStorageOptionsIOS = const IOSOptions( accessibility: KeychainAccessibility.first_unlock_this_device, ); - // const IOSOptions(accessibility: IOSAccessibility.first_unlock); - Future init() async { _preferences = await SharedPreferences.getInstance(); _secureStorage = const FlutterSecureStorage(); @@ -89,13 +81,6 @@ class Configuration { _logger.warning(e); } tempDirectory.createSync(recursive: true); - final tempDirectoryPath = (await getTemporaryDirectory()).path; - _thumbnailCacheDirectory = tempDirectoryPath + "/thumbnail-cache"; - io.Directory(_thumbnailCacheDirectory).createSync(recursive: true); - _sharedTempMediaDirectory = tempDirectoryPath + "/ente-shared-media"; - io.Directory(_sharedTempMediaDirectory).createSync(recursive: true); - _sharedDocumentsMediaDirectory = _documentsDirectory + "/ente-shared-media"; - io.Directory(_sharedDocumentsMediaDirectory).createSync(recursive: true); if (!_preferences.containsKey(tokenKey)) { await _secureStorage.deleteAll(iOptions: _secureStorageOptionsIOS); } else { @@ -114,7 +99,6 @@ class Configuration { if (_key == null) { await logout(autoLogout: true); } - await _migrateSecurityStorageToFirstUnlock(); } } @@ -356,52 +340,31 @@ class Configuration { } } - Future setKey(String? key) async { + Future setKey(String key) async { _key = key; - if (key == null) { - await _secureStorage.delete( - key: keyKey, - iOptions: _secureStorageOptionsIOS, - ); - } else { - await _secureStorage.write( - key: keyKey, - value: key, - iOptions: _secureStorageOptionsIOS, - ); - } + await _secureStorage.write( + key: keyKey, + value: key, + iOptions: _secureStorageOptionsIOS, + ); } Future setSecretKey(String? secretKey) async { _secretKey = secretKey; - if (secretKey == null) { - await _secureStorage.delete( - key: secretKeyKey, - iOptions: _secureStorageOptionsIOS, - ); - } else { - await _secureStorage.write( - key: secretKeyKey, - value: secretKey, - iOptions: _secureStorageOptionsIOS, - ); - } + await _secureStorage.write( + key: secretKeyKey, + value: secretKey, + iOptions: _secureStorageOptionsIOS, + ); } Future setAuthSecretKey(String? authSecretKey) async { _authSecretKey = authSecretKey; - if (authSecretKey == null) { - await _secureStorage.delete( - key: authSecretKeyKey, - iOptions: _secureStorageOptionsIOS, - ); - } else { - await _secureStorage.write( - key: authSecretKeyKey, - value: authSecretKey, - iOptions: _secureStorageOptionsIOS, - ); - } + await _secureStorage.write( + key: authSecretKeyKey, + value: authSecretKey, + iOptions: _secureStorageOptionsIOS, + ); } Uint8List? getKey() { @@ -430,18 +393,6 @@ class Configuration { return _tempDirectory; } - String getThumbnailCacheDirectory() { - return _thumbnailCacheDirectory; - } - - String getOldSharedMediaCacheDirectory() { - return _sharedTempMediaDirectory; - } - - String getSharedMediaDirectory() { - return _sharedDocumentsMediaDirectory; - } - bool hasConfiguredAccount() { return getToken() != null && _key != null; } @@ -473,27 +424,4 @@ class Configuration { String? getVolatilePassword() { return _volatilePassword; } - - Future _migrateSecurityStorageToFirstUnlock() async { - final hasMigratedSecureStorageToFirstUnlock = - _preferences.getBool(hasMigratedSecureStorageKey) ?? false; - if (!hasMigratedSecureStorageToFirstUnlock && - _key != null && - _secretKey != null) { - await _secureStorage.write( - key: keyKey, - value: _key, - iOptions: _secureStorageOptionsIOS, - ); - await _secureStorage.write( - key: secretKeyKey, - value: _secretKey, - iOptions: _secureStorageOptionsIOS, - ); - await _preferences.setBool( - hasMigratedSecureStorageKey, - true, - ); - } - } } From 117d397d770bbf386e9b898d94869a23d8cac15e Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Mon, 4 Sep 2023 10:49:28 +0530 Subject: [PATCH 09/23] Add offline db --- lib/store/offline_authenticator_db.dart | 170 ++++++++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 lib/store/offline_authenticator_db.dart diff --git a/lib/store/offline_authenticator_db.dart b/lib/store/offline_authenticator_db.dart new file mode 100644 index 000000000..569acd396 --- /dev/null +++ b/lib/store/offline_authenticator_db.dart @@ -0,0 +1,170 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:ente_auth/models/authenticator/auth_entity.dart'; +import 'package:ente_auth/models/authenticator/local_auth_entity.dart'; +import 'package:flutter/foundation.dart'; +import 'package:path/path.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:sqflite/sqflite.dart'; + +class OfflineAuthenticatorDB { + static const _databaseName = "ente.offline_authenticator.db"; + static const _databaseVersion = 1; + + static const entityTable = 'entities'; + + OfflineAuthenticatorDB._privateConstructor(); + static final OfflineAuthenticatorDB instance = OfflineAuthenticatorDB._privateConstructor(); + + static Future? _dbFuture; + + Future get database async { + _dbFuture ??= _initDatabase(); + return _dbFuture!; + } + + Future _initDatabase() async { + final Directory documentsDirectory = + await getApplicationDocumentsDirectory(); + final String path = join(documentsDirectory.path, _databaseName); + debugPrint(path); + return await openDatabase( + path, + version: _databaseVersion, + onCreate: _onCreate, + ); + } + + Future _onCreate(Database db, int version) async { + await db.execute( + ''' + CREATE TABLE $entityTable ( + _generatedID INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + id TEXT, + encryptedData TEXT NOT NULL, + header TEXT NOT NULL, + createdAt INTEGER NOT NULL, + updatedAt INTEGER NOT NULL, + shouldSync INTEGER DEFAULT 0, + UNIQUE(id) + ); + ''', + ); + } + + Future insert(String encData, String header) async { + final db = await instance.database; + final int timeInMicroSeconds = DateTime.now().microsecondsSinceEpoch; + final insertedID = await db.insert( + entityTable, + { + "encryptedData": encData, + "header": header, + "shouldSync": 1, + "createdAt": timeInMicroSeconds, + "updatedAt": timeInMicroSeconds, + }, + ); + return insertedID; + } + + Future updateEntry( + int generatedID, + String encData, + String header, + ) async { + final db = await instance.database; + final int timeInMicroSeconds = DateTime.now().microsecondsSinceEpoch; + int affectedRows = await db.update( + entityTable, + { + "encryptedData": encData, + "header": header, + "shouldSync": 1, + "updatedAt": timeInMicroSeconds, + }, + where: '_generatedID = ?', + whereArgs: [generatedID], + ); + return affectedRows; + } + + Future insertOrReplace(List authEntities) async { + final db = await instance.database; + final batch = db.batch(); + for (AuthEntity authEntity in authEntities) { + final insertRow = authEntity.toMap(); + insertRow.remove('isDeleted'); + insertRow.putIfAbsent('shouldSync', () => 0); + batch.insert( + entityTable, + insertRow, + conflictAlgorithm: ConflictAlgorithm.replace, + ); + } + await batch.commit(noResult: true); + } + + Future updateLocalEntity(LocalAuthEntity localAuthEntity) async { + final db = await instance.database; + await db.update( + entityTable, + localAuthEntity.toMap(), + where: '_generatedID = ?', + whereArgs: [localAuthEntity.generatedID], + conflictAlgorithm: ConflictAlgorithm.replace, + ); + } + + Future getEntryByID(int genID) async { + final db = await instance.database; + final rows = await db + .query(entityTable, where: '_generatedID = ?', whereArgs: [genID]); + final listOfAuthEntities = _convertRows(rows); + if (listOfAuthEntities.isEmpty) { + return null; + } else { + return listOfAuthEntities.first; + } + } + + Future> getAll() async { + final db = await instance.database; + final rows = await db.rawQuery("SELECT * from $entityTable"); + return _convertRows(rows); + } + +// deleteByID will prefer generated id if both ids are passed during deletion + Future deleteByIDs({List? generatedIDs, List? ids}) async { + final db = await instance.database; + final batch = db.batch(); + const whereGenID = '_generatedID = ?'; + const whereID = 'id = ?'; + if (generatedIDs != null) { + for (int genId in generatedIDs) { + batch.delete(entityTable, where: whereGenID, whereArgs: [genId]); + } + } + if (ids != null) { + for (String id in ids) { + batch.delete(entityTable, where: whereID, whereArgs: [id]); + } + } + final result = await batch.commit(); + debugPrint("Done"); + } + + Future clearTable() async { + final db = await instance.database; + await db.delete(entityTable); + } + + List _convertRows(List> rows) { + final keys = []; + for (final row in rows) { + keys.add(LocalAuthEntity.fromMap(row)); + } + return keys; + } +} From 2ff8963c527b0c129e687ba545dae8fd5475f420 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Mon, 4 Sep 2023 10:54:14 +0530 Subject: [PATCH 10/23] Refactor to clear online mode keys --- lib/core/configuration.dart | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/lib/core/configuration.dart b/lib/core/configuration.dart index d54da4d44..18763f718 100644 --- a/lib/core/configuration.dart +++ b/lib/core/configuration.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'dart:io' as io; import 'dart:typed_data'; @@ -40,6 +41,7 @@ class Configuration { static const userIDKey = "user_id"; static const hasMigratedSecureStorageKey = "has_migrated_secure_storage"; static const hasOptedForOfflineModeKey = "has_opted_for_offline_mode"; + final List onlineSecureKeys = [keyKey, secretKeyKey, authSecretKeyKey]; final kTempFolderDeletionTimeBuffer = const Duration(days: 1).inMicroseconds; @@ -81,8 +83,19 @@ class Configuration { _logger.warning(e); } tempDirectory.createSync(recursive: true); + await _initOnlineAccount(); + } + + Future _initOnlineAccount() async { if (!_preferences.containsKey(tokenKey)) { - await _secureStorage.deleteAll(iOptions: _secureStorageOptionsIOS); + for (final key in onlineSecureKeys) { + unawaited( + _secureStorage.delete( + key: key, + iOptions: _secureStorageOptionsIOS, + ), + ); + } } else { _key = await _secureStorage.read( key: keyKey, @@ -104,7 +117,12 @@ class Configuration { Future logout({bool autoLogout = false}) async { await _preferences.clear(); - await _secureStorage.deleteAll(iOptions: _secureStorageOptionsIOS); + for (String key in onlineSecureKeys) { + await _secureStorage.delete( + key: key, + iOptions: _secureStorageOptionsIOS, + ); + } await AuthenticatorDB.instance.clearTable(); _key = null; _cachedToken = null; From a02cfef1057375fdb61bccf2fb242d7680d8f282 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Mon, 4 Sep 2023 16:01:59 +0530 Subject: [PATCH 11/23] Add basic support for offline mode --- lib/core/configuration.dart | 32 +++++++- lib/onboarding/view/onboarding_page.dart | 1 + lib/services/authenticator_service.dart | 81 ++++++++++++++----- lib/store/code_store.dart | 18 +++-- lib/ui/settings/data/import/aegis_import.dart | 2 +- .../data/import/encrypted_ente_import.dart | 5 +- .../data/import/google_auth_import.dart | 2 +- .../data/import/plain_text_import.dart | 2 +- .../data/import/raivo_plain_text_import.dart | 2 +- lib/ui/settings_page.dart | 4 +- 10 files changed, 114 insertions(+), 35 deletions(-) diff --git a/lib/core/configuration.dart b/lib/core/configuration.dart index 18763f718..5d0e09a33 100644 --- a/lib/core/configuration.dart +++ b/lib/core/configuration.dart @@ -36,12 +36,17 @@ class Configuration { static const keyKey = "key"; static const secretKeyKey = "secret_key"; static const authSecretKeyKey = "auth_secret_key"; + static const offlineAuthSecretKey = "offline_auth_secret_key"; static const tokenKey = "token"; static const encryptedTokenKey = "encrypted_token"; static const userIDKey = "user_id"; static const hasMigratedSecureStorageKey = "has_migrated_secure_storage"; - static const hasOptedForOfflineModeKey = "has_opted_for_offline_mode"; - final List onlineSecureKeys = [keyKey, secretKeyKey, authSecretKeyKey]; + static const hasOptedForOfflineModeKey = "has_opted_for_offline_mode2"; + final List onlineSecureKeys = [ + keyKey, + secretKeyKey, + authSecretKeyKey + ]; final kTempFolderDeletionTimeBuffer = const Duration(days: 1).inMicroseconds; @@ -53,6 +58,7 @@ class Configuration { String? _key; String? _secretKey; String? _authSecretKey; + String? _offlineAuthKey; late FlutterSecureStorage _secureStorage; late String _tempDirectory; @@ -84,6 +90,14 @@ class Configuration { } tempDirectory.createSync(recursive: true); await _initOnlineAccount(); + await _initOfflineAccount(); + } + + Future _initOfflineAccount() async { + _offlineAuthKey = await _secureStorage.read( + key: offlineAuthSecretKey, + iOptions: _secureStorageOptionsIOS, + ); } Future _initOnlineAccount() async { @@ -397,6 +411,10 @@ class Configuration { return _authSecretKey == null ? null : Sodium.base642bin(_authSecretKey!); } + Uint8List? getOfflineSecretKey() { + return _offlineAuthKey == null ? null : Sodium.base642bin(_offlineAuthKey!); + } + Uint8List getRecoveryKey() { final keyAttributes = getKeyAttributes()!; return CryptoUtil.decryptSync( @@ -419,8 +437,14 @@ class Configuration { return _preferences.getBool(hasOptedForOfflineModeKey) ?? false; } - Future optForOfflineMode() { - return _preferences.setBool(hasOptedForOfflineModeKey, true); + Future optForOfflineMode() async { + _offlineAuthKey = Sodium.bin2base64(CryptoUtil.generateKey()); + await _secureStorage.write( + key: offlineAuthSecretKey, + value: _offlineAuthKey, + iOptions: _secureStorageOptionsIOS, + ); + await _preferences.setBool(hasOptedForOfflineModeKey, true); } bool shouldShowLockScreen() { diff --git a/lib/onboarding/view/onboarding_page.dart b/lib/onboarding/view/onboarding_page.dart index 7d8db274a..8f4c57cd2 100644 --- a/lib/onboarding/view/onboarding_page.dart +++ b/lib/onboarding/view/onboarding_page.dart @@ -177,6 +177,7 @@ class _OnboardingPageState extends State { } Future _optForOfflineMode() async { + await Configuration.instance.optForOfflineMode(); Navigator.of(context).push( MaterialPageRoute( diff --git a/lib/services/authenticator_service.dart b/lib/services/authenticator_service.dart index 846638d3e..eb450d29a 100644 --- a/lib/services/authenticator_service.dart +++ b/lib/services/authenticator_service.dart @@ -15,18 +15,29 @@ import 'package:ente_auth/models/authenticator/auth_key.dart'; import 'package:ente_auth/models/authenticator/entity_result.dart'; import 'package:ente_auth/models/authenticator/local_auth_entity.dart'; import 'package:ente_auth/store/authenticator_db.dart'; +import 'package:ente_auth/store/offline_authenticator_db.dart'; import 'package:ente_auth/utils/crypto_util.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_sodium/flutter_sodium.dart'; import 'package:logging/logging.dart'; import 'package:shared_preferences/shared_preferences.dart'; +enum AccountMode { + online, + offline, +} +extension on AccountMode { + bool get isOnline => this == AccountMode.online; + bool get isOffline => this == AccountMode.offline; +} + class AuthenticatorService { final _logger = Logger((AuthenticatorService).toString()); final _config = Configuration.instance; late SharedPreferences _prefs; late AuthenticatorGateway _gateway; late AuthenticatorDB _db; + late OfflineAuthenticatorDB _offlineDb; final String _lastEntitySyncTime = "lastEntitySyncTime"; AuthenticatorService._privateConstructor(); @@ -34,25 +45,34 @@ class AuthenticatorService { static final AuthenticatorService instance = AuthenticatorService._privateConstructor(); + AccountMode getAccountMode() { + return Configuration.instance.hasOptedForOfflineMode() && + !Configuration.instance.hasConfiguredAccount() + ? AccountMode.offline + : AccountMode.online; + } + Future init() async { _prefs = await SharedPreferences.getInstance(); _db = AuthenticatorDB.instance; + _offlineDb = OfflineAuthenticatorDB.instance; _gateway = AuthenticatorGateway(Network.instance.getDio(), _config); if (Configuration.instance.hasConfiguredAccount()) { - unawaited(sync()); + unawaited(onlineSync()); } Bus.instance.on().listen((event) { - unawaited(sync()); + unawaited(onlineSync()); }); } - Future> getEntities() async { - final List result = await _db.getAll(); + Future> getEntities(AccountMode mode) async { + final List result = + mode.isOnline ? await _db.getAll() : await _offlineDb.getAll(); final List entities = []; if (result.isEmpty) { return entities; } - final key = await getOrCreateAuthDataKey(); + final key = await getOrCreateAuthDataKey(mode); for (LocalAuthEntity e in result) { try { final decryptedValue = await CryptoUtil.decryptChaCha( @@ -75,17 +95,23 @@ class AuthenticatorService { return entities; } - Future addEntry(String plainText, bool shouldSync) async { - var key = await getOrCreateAuthDataKey(); + Future addEntry( + String plainText, + bool shouldSync, + AccountMode accountMode, + ) async { + var key = await getOrCreateAuthDataKey(accountMode); final encryptedKeyData = await CryptoUtil.encryptChaCha( utf8.encode(plainText) as Uint8List, key, ); String encryptedData = Sodium.bin2base64(encryptedKeyData.encryptedData!); String header = Sodium.bin2base64(encryptedKeyData.header!); - final insertedID = await _db.insert(encryptedData, header); + final insertedID = accountMode.isOnline + ? await _db.insert(encryptedData, header) + : await _offlineDb.insert(encryptedData, header); if (shouldSync) { - unawaited(sync()); + unawaited(onlineSync()); } return insertedID; } @@ -94,39 +120,53 @@ class AuthenticatorService { int generatedID, String plainText, bool shouldSync, + AccountMode accountMode, ) async { - var key = await getOrCreateAuthDataKey(); + var key = await getOrCreateAuthDataKey(accountMode); final encryptedKeyData = await CryptoUtil.encryptChaCha( utf8.encode(plainText) as Uint8List, key, ); String encryptedData = Sodium.bin2base64(encryptedKeyData.encryptedData!); String header = Sodium.bin2base64(encryptedKeyData.header!); - final int affectedRows = - await _db.updateEntry(generatedID, encryptedData, header); + final int affectedRows = accountMode.isOnline + ? await _db.updateEntry(generatedID, encryptedData, header) + : await _offlineDb.updateEntry(generatedID, encryptedData, header); assert( affectedRows == 1, "updateEntry should have updated exactly one row", ); if (shouldSync) { - unawaited(sync()); + unawaited(onlineSync()); } } - Future deleteEntry(int genID) async { - LocalAuthEntity? result = await _db.getEntryByID(genID); + Future deleteEntry(int genID, AccountMode accountMode) async { + LocalAuthEntity? result = accountMode.isOnline + ? await _db.getEntryByID(genID) + : await _offlineDb.getEntryByID(genID); if (result == null) { _logger.info("No entry found for given id"); return; } - if (result.id != null) { + if (result.id != null && accountMode.isOnline) { await _gateway.deleteEntity(result.id!); + } else { + debugPrint("Skipping delete since account mode is offline"); + } + if(accountMode.isOnline) { + await _db.deleteByIDs(generatedIDs: [genID]); + } else { + await _offlineDb.deleteByIDs(generatedIDs: [genID]); } - await _db.deleteByIDs(generatedIDs: [genID]); } - Future sync() async { + Future onlineSync() async { try { + if(getAccountMode().isOffline) { + debugPrint("Skipping sync since account mode is offline"); + return; + } _logger.info("Sync"); await _remoteToLocalSync(); _logger.info("remote fetch completed"); @@ -209,7 +249,10 @@ class AuthenticatorService { } } - Future getOrCreateAuthDataKey() async { + Future getOrCreateAuthDataKey(AccountMode mode) async { + if(mode.isOffline) { + return _config.getOfflineSecretKey()!; + } if (_config.getAuthSecretKey() != null) { return _config.getAuthSecretKey()!; } diff --git a/lib/store/code_store.dart b/lib/store/code_store.dart index 41585f542..04e7a2e91 100644 --- a/lib/store/code_store.dart +++ b/lib/store/code_store.dart @@ -20,9 +20,12 @@ class CodeStore { _authenticatorService = AuthenticatorService.instance; } - Future> getAllCodes() async { + + + Future> getAllCodes({AccountMode? accountMode}) async { + final mode = accountMode ?? _authenticatorService.getAccountMode(); final List entities = - await _authenticatorService.getEntities(); + await _authenticatorService.getEntities(mode); final List codes = []; for (final entity in entities) { final decodeJson = jsonDecode(entity.rawData); @@ -46,8 +49,10 @@ class CodeStore { Future addCode( Code code, { bool shouldSync = true, + AccountMode? accountMode, }) async { - final codes = await getAllCodes(); + final mode = accountMode ?? _authenticatorService.getAccountMode(); + final codes = await getAllCodes(accountMode: mode); bool isExistingCode = false; for (final existingCode in codes) { if (existingCode == code) { @@ -63,18 +68,21 @@ class CodeStore { code.generatedID!, jsonEncode(code.rawData), shouldSync, + mode, ); } else { code.generatedID = await _authenticatorService.addEntry( jsonEncode(code.rawData), shouldSync, + mode, ); } Bus.instance.fire(CodesUpdatedEvent()); } - Future removeCode(Code code) async { - await _authenticatorService.deleteEntry(code.generatedID!); + Future removeCode(Code code, {AccountMode? accountMode}) async { + final mode = accountMode ?? _authenticatorService.getAccountMode(); + await _authenticatorService.deleteEntry(code.generatedID!, mode); Bus.instance.fire(CodesUpdatedEvent()); } } diff --git a/lib/ui/settings/data/import/aegis_import.dart b/lib/ui/settings/data/import/aegis_import.dart index 053f40f5a..5042d618f 100644 --- a/lib/ui/settings/data/import/aegis_import.dart +++ b/lib/ui/settings/data/import/aegis_import.dart @@ -156,7 +156,7 @@ Future _processAegisExportFile( for (final code in parsedCodes) { await CodeStore.instance.addCode(code, shouldSync: false); } - unawaited(AuthenticatorService.instance.sync()); + unawaited(AuthenticatorService.instance.onlineSync()); int count = parsedCodes.length; return count; } diff --git a/lib/ui/settings/data/import/encrypted_ente_import.dart b/lib/ui/settings/data/import/encrypted_ente_import.dart index 986d3f577..15f869206 100644 --- a/lib/ui/settings/data/import/encrypted_ente_import.dart +++ b/lib/ui/settings/data/import/encrypted_ente_import.dart @@ -93,7 +93,8 @@ Future _decryptExportData( derivedKey, Sodium.base642bin(enteAuthExport.encryptionNonce), ); - } catch (e) { + } catch (e,s) { + Logger("encryptedImport").warning('failed to decrypt',e,s); showToast(context, l10n.incorrectPasswordTitle); isPasswordIncorrect = true; } @@ -118,7 +119,7 @@ Future _decryptExportData( for (final code in parsedCodes) { await CodeStore.instance.addCode(code, shouldSync: false); } - unawaited(AuthenticatorService.instance.sync()); + unawaited(AuthenticatorService.instance.onlineSync()); importedCodeCount = parsedCodes.length; await progressDialog.hide(); } catch (e, s) { diff --git a/lib/ui/settings/data/import/google_auth_import.dart b/lib/ui/settings/data/import/google_auth_import.dart index 6c1604273..d883f365c 100644 --- a/lib/ui/settings/data/import/google_auth_import.dart +++ b/lib/ui/settings/data/import/google_auth_import.dart @@ -55,7 +55,7 @@ Future showGoogleAuthInstruction(BuildContext context) async { for (final code in codes) { await CodeStore.instance.addCode(code, shouldSync: false); } - unawaited(AuthenticatorService.instance.sync()); + unawaited(AuthenticatorService.instance.onlineSync()); importSuccessDialog(context, codes.length); } } diff --git a/lib/ui/settings/data/import/plain_text_import.dart b/lib/ui/settings/data/import/plain_text_import.dart index 3a0ff0b14..a8e64bb5f 100644 --- a/lib/ui/settings/data/import/plain_text_import.dart +++ b/lib/ui/settings/data/import/plain_text_import.dart @@ -121,7 +121,7 @@ Future _pickImportFile(BuildContext context) async { for (final code in parsedCodes) { await CodeStore.instance.addCode(code, shouldSync: false); } - unawaited(AuthenticatorService.instance.sync()); + unawaited(AuthenticatorService.instance.onlineSync()); await progressDialog.hide(); await importSuccessDialog(context, parsedCodes.length); } catch (e) { diff --git a/lib/ui/settings/data/import/raivo_plain_text_import.dart b/lib/ui/settings/data/import/raivo_plain_text_import.dart index ab5d01882..48fc74888 100644 --- a/lib/ui/settings/data/import/raivo_plain_text_import.dart +++ b/lib/ui/settings/data/import/raivo_plain_text_import.dart @@ -111,7 +111,7 @@ Future _processRaivoExportFile(BuildContext context,String path) async { for (final code in parsedCodes) { await CodeStore.instance.addCode(code, shouldSync: false); } - unawaited(AuthenticatorService.instance.sync()); + unawaited(AuthenticatorService.instance.onlineSync()); int count = parsedCodes.length; return count; } diff --git a/lib/ui/settings_page.dart b/lib/ui/settings_page.dart index 6151b695e..584af45b9 100644 --- a/lib/ui/settings_page.dart +++ b/lib/ui/settings_page.dart @@ -19,12 +19,13 @@ import 'package:flutter/material.dart'; class SettingsPage extends StatelessWidget { final ValueNotifier emailNotifier; - final _hasLoggedIn = Configuration.instance.hasConfiguredAccount(); + SettingsPage({Key? key, required this.emailNotifier}) : super(key: key); @override Widget build(BuildContext context) { + final _hasLoggedIn = Configuration.instance.hasConfiguredAccount(); if (_hasLoggedIn) { UserService.instance.getUserDetailsV2().ignore(); } @@ -38,6 +39,7 @@ class SettingsPage extends StatelessWidget { } Widget _getBody(BuildContext context, EnteColorScheme colorScheme) { + final _hasLoggedIn = Configuration.instance.hasConfiguredAccount(); final enteTextTheme = getEnteTextTheme(context); const sectionSpacing = SizedBox(height: 8); final List contents = []; From 535109d08fd0171319bb38eaf1b181c060a780c7 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Mon, 4 Sep 2023 16:13:19 +0530 Subject: [PATCH 12/23] Hide unsync icon for offline mode --- lib/ui/code_widget.dart | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/ui/code_widget.dart b/lib/ui/code_widget.dart index e733b231c..73e0cf03c 100644 --- a/lib/ui/code_widget.dart +++ b/lib/ui/code_widget.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:clipboard/clipboard.dart'; +import 'package:ente_auth/core/configuration.dart'; import 'package:ente_auth/ente_theme_data.dart'; import 'package:ente_auth/l10n/l10n.dart'; import 'package:ente_auth/models/code.dart'; @@ -31,6 +32,7 @@ class _CodeWidgetState extends State { final ValueNotifier _nextCode = ValueNotifier(""); final Logger logger = Logger("_CodeWidgetState"); bool _isInitialized = false; + late bool hasConfiguredAccount; @override void initState() { @@ -46,6 +48,7 @@ class _CodeWidgetState extends State { } } }); + hasConfiguredAccount = Configuration.instance.hasConfiguredAccount(); } @override @@ -174,8 +177,8 @@ class _CodeWidgetState extends State { Row( mainAxisAlignment: MainAxisAlignment.end, children: [ - widget.code.hasSynced != null && - widget.code.hasSynced! + (widget.code.hasSynced != null && + widget.code.hasSynced!) || !hasConfiguredAccount ? Container() : const Icon( Icons.sync_disabled, From fb0fa73c0330e3f6baac74fc4450d6729d8f9528 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Mon, 4 Sep 2023 16:22:50 +0530 Subject: [PATCH 13/23] Use Sizebox.shrink() --- lib/ui/code_widget.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ui/code_widget.dart b/lib/ui/code_widget.dart index 73e0cf03c..393670f4a 100644 --- a/lib/ui/code_widget.dart +++ b/lib/ui/code_widget.dart @@ -179,7 +179,7 @@ class _CodeWidgetState extends State { children: [ (widget.code.hasSynced != null && widget.code.hasSynced!) || !hasConfiguredAccount - ? Container() + ? const SizedBox.shrink() : const Icon( Icons.sync_disabled, size: 20, From 523f216b618bc80f7b5e5d0ee3d9c002e36e2dc0 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Mon, 4 Sep 2023 16:34:06 +0530 Subject: [PATCH 14/23] Update key --- lib/core/configuration.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/core/configuration.dart b/lib/core/configuration.dart index 5d0e09a33..e2701765e 100644 --- a/lib/core/configuration.dart +++ b/lib/core/configuration.dart @@ -41,7 +41,7 @@ class Configuration { static const encryptedTokenKey = "encrypted_token"; static const userIDKey = "user_id"; static const hasMigratedSecureStorageKey = "has_migrated_secure_storage"; - static const hasOptedForOfflineModeKey = "has_opted_for_offline_mode2"; + static const hasOptedForOfflineModeKey = "has_opted_for_offline_mode"; final List onlineSecureKeys = [ keyKey, secretKeyKey, From 9c0aea66ec7400a522f7acd8c4bf8363907ed1db Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Mon, 4 Sep 2023 16:45:08 +0530 Subject: [PATCH 15/23] Check for existing key before creating new --- lib/core/configuration.dart | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/lib/core/configuration.dart b/lib/core/configuration.dart index e2701765e..a39052e65 100644 --- a/lib/core/configuration.dart +++ b/lib/core/configuration.dart @@ -438,12 +438,22 @@ class Configuration { } Future optForOfflineMode() async { - _offlineAuthKey = Sodium.bin2base64(CryptoUtil.generateKey()); - await _secureStorage.write( + if ((await _secureStorage.containsKey( key: offlineAuthSecretKey, - value: _offlineAuthKey, iOptions: _secureStorageOptionsIOS, - ); + ))) { + _offlineAuthKey = await _secureStorage.read( + key: offlineAuthSecretKey, + iOptions: _secureStorageOptionsIOS, + ); + } else { + _offlineAuthKey = Sodium.bin2base64(CryptoUtil.generateKey()); + await _secureStorage.write( + key: offlineAuthSecretKey, + value: _offlineAuthKey, + iOptions: _secureStorageOptionsIOS, + ); + } await _preferences.setBool(hasOptedForOfflineModeKey, true); } From 5e327a7d65cbb0381da7a0f16799130bd590a72a Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Mon, 4 Sep 2023 16:51:07 +0530 Subject: [PATCH 16/23] Show error toast if device doesn't support biometrics --- lib/onboarding/view/onboarding_page.dart | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/onboarding/view/onboarding_page.dart b/lib/onboarding/view/onboarding_page.dart index 8f4c57cd2..d1649aa0f 100644 --- a/lib/onboarding/view/onboarding_page.dart +++ b/lib/onboarding/view/onboarding_page.dart @@ -7,6 +7,7 @@ import 'package:ente_auth/ente_theme_data.dart'; import 'package:ente_auth/events/trigger_logout_event.dart'; import "package:ente_auth/l10n/l10n.dart"; import 'package:ente_auth/locale.dart'; +import 'package:ente_auth/services/local_authentication_service.dart'; import 'package:ente_auth/theme/text_style.dart'; import 'package:ente_auth/ui/account/email_entry_page.dart'; import 'package:ente_auth/ui/account/login_page.dart'; @@ -17,8 +18,10 @@ import 'package:ente_auth/ui/common/gradient_button.dart'; import 'package:ente_auth/ui/home_page.dart'; import 'package:ente_auth/ui/settings/language_picker.dart'; import 'package:ente_auth/utils/navigation_util.dart'; +import 'package:ente_auth/utils/toast_util.dart'; import 'package:flutter/foundation.dart'; import "package:flutter/material.dart"; +import 'package:local_auth/local_auth.dart'; class OnboardingPage extends StatefulWidget { const OnboardingPage({Key? key}) : super(key: key); @@ -177,7 +180,11 @@ class _OnboardingPageState extends State { } Future _optForOfflineMode() async { - + bool canCheckBio = await LocalAuthentication().canCheckBiometrics; + if(!canCheckBio) { + showToast(context, "Sorry, biometric authentication is not supported on this device."); + return; + } await Configuration.instance.optForOfflineMode(); Navigator.of(context).push( MaterialPageRoute( From a7818fc6d4f89dc119e3d62ba57b3ee34fe6189b Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Mon, 4 Sep 2023 16:58:13 +0530 Subject: [PATCH 17/23] Add notification component --- .../notification_warning_widget.dart | 145 ++++++++++++------ 1 file changed, 95 insertions(+), 50 deletions(-) diff --git a/lib/ui/components/notification_warning_widget.dart b/lib/ui/components/notification_warning_widget.dart index dc15fd5d0..a799ffcd3 100644 --- a/lib/ui/components/notification_warning_widget.dart +++ b/lib/ui/components/notification_warning_widget.dart @@ -1,75 +1,120 @@ -import 'package:ente_auth/ente_theme_data.dart'; +import "package:ente_auth/ente_theme_data.dart"; 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/components/buttons/icon_button_widget.dart'; import 'package:flutter/material.dart'; -class NotificationWarningWidget extends StatelessWidget { - final IconData warningIcon; +// CreateNotificationType enum +enum NotificationType { + warning, + banner, + notice, +} + +class NotificationWidget extends StatelessWidget { + final IconData startIcon; final IconData actionIcon; final String text; + final String? subText; final GestureTapCallback onTap; + final NotificationType type; - const NotificationWarningWidget({ + const NotificationWidget({ Key? key, - required this.warningIcon, + required this.startIcon, required this.actionIcon, required this.text, required this.onTap, + this.subText, + this.type = NotificationType.warning, }) : super(key: key); @override Widget build(BuildContext context) { + EnteColorScheme colorScheme = getEnteColorScheme(context); + EnteTextTheme textTheme = getEnteTextTheme(context); + TextStyle mainTextStyle = darkTextTheme.bodyBold; + TextStyle subTextStyle = darkTextTheme.miniMuted; + LinearGradient? backgroundGradient; + Color? backgroundColor; + EnteColorScheme strokeColorScheme = darkScheme; + List? boxShadow; + switch (type) { + case NotificationType.warning: + backgroundColor = warning500; + break; + case NotificationType.banner: + colorScheme = getEnteColorScheme(context); + textTheme = getEnteTextTheme(context); + backgroundColor = colorScheme.backgroundElevated2; + mainTextStyle = textTheme.bodyBold; + subTextStyle = textTheme.miniMuted; + strokeColorScheme = colorScheme; + boxShadow = [ + BoxShadow(color: Colors.black.withOpacity(0.25), blurRadius: 1), + ]; + break; + + case NotificationType.notice: + backgroundColor = colorScheme.backgroundElevated2; + mainTextStyle = textTheme.bodyBold; + subTextStyle = textTheme.miniMuted; + strokeColorScheme = colorScheme; + boxShadow = Theme.of(context).colorScheme.enteTheme.shadowMenu; + break; + } return Center( child: GestureDetector( onTap: onTap, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12), - child: Container( - decoration: BoxDecoration( - borderRadius: const BorderRadius.all( - Radius.circular(8), - ), - boxShadow: Theme.of(context).colorScheme.enteTheme.shadowMenu, - color: warning500, + child: Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all( + Radius.circular(8), ), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Icon( - warningIcon, - size: 36, - color: Colors.white, - ), - const SizedBox(width: 12), - Flexible( - child: Text( - text, - style: darkTextTheme.bodyBold, - textAlign: TextAlign.left, - ), - ), - const SizedBox(width: 12), - ClipOval( - child: Material( - color: fillFaintDark, - child: InkWell( - splashColor: Colors.red, // Splash color - onTap: onTap, - child: SizedBox( - width: 40, - height: 40, - child: Icon( - actionIcon, - color: Colors.white, - ), - ), + boxShadow: boxShadow, + color: backgroundColor, + gradient: backgroundGradient, + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Icon( + startIcon, + size: 36, + color: strokeColorScheme.strokeBase, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + text, + style: mainTextStyle, + textAlign: TextAlign.left, ), - ), + subText != null + ? Text( + subText!, + style: subTextStyle, + ) + : const SizedBox.shrink(), + ], ), - ], - ), + ), + const SizedBox(width: 12), + IconButtonWidget( + icon: actionIcon, + iconButtonType: IconButtonType.rounded, + iconColor: strokeColorScheme.strokeBase, + defaultColor: strokeColorScheme.fillFaint, + pressedColor: strokeColorScheme.fillMuted, + onTap: onTap, + ), + ], ), ), ), From 326653054d170b749a7707cdd0103d30a06b9490 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Mon, 4 Sep 2023 17:46:58 +0530 Subject: [PATCH 18/23] Add option to sign in to backup --- lib/l10n/arb/app_en.arb | 3 ++- lib/ui/settings_page.dart | 22 +++++++++++++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 0095d2ab6..7633f0fe8 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -321,5 +321,6 @@ "plainText": "Plain text", "passwordToEncryptExport": "Password to encrypt export", "export": "Export", - "useOffline": "Use without backups" + "useOffline": "Use without backups", + "signInToBackup": "Sign in to backup your codes" } diff --git a/lib/ui/settings_page.dart b/lib/ui/settings_page.dart index 584af45b9..36e9fab8c 100644 --- a/lib/ui/settings_page.dart +++ b/lib/ui/settings_page.dart @@ -1,9 +1,12 @@ import 'dart:io'; 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/theme/colors.dart'; import 'package:ente_auth/theme/ente_theme.dart'; +import 'package:ente_auth/ui/components/notification_warning_widget.dart'; import 'package:ente_auth/ui/settings/about_section_widget.dart'; import 'package:ente_auth/ui/settings/account_section_widget.dart'; import 'package:ente_auth/ui/settings/app_version_widget.dart'; @@ -14,13 +17,13 @@ import 'package:ente_auth/ui/settings/support_dev_widget.dart'; import 'package:ente_auth/ui/settings/support_section_widget.dart'; import 'package:ente_auth/ui/settings/theme_switch_widget.dart'; import 'package:ente_auth/ui/settings/title_bar_widget.dart'; +import 'package:ente_auth/utils/navigation_util.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; class SettingsPage extends StatelessWidget { final ValueNotifier emailNotifier; - SettingsPage({Key? key, required this.emailNotifier}) : super(key: key); @override @@ -70,6 +73,23 @@ class SettingsPage extends StatelessWidget { AccountSectionWidget(), sectionSpacing, ]); + } else { + contents.addAll([ + NotificationWidget( + startIcon: Icons.account_box_outlined, + actionIcon: Icons.arrow_forward, + text: context.l10n.signInToBackup, + type: NotificationType.notice, + onTap: () async => { + await routeToPage( + context, + const OnboardingPage(), + ), + }, + ), + sectionSpacing, + sectionSpacing, + ]); } contents.addAll([ DataSectionWidget(), From 2e3b6b27de6532b7e53d193307082c229e0ea452 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Mon, 4 Sep 2023 17:51:09 +0530 Subject: [PATCH 19/23] Add support for importing offline codes --- lib/store/code_store.dart | 39 ++++++++++++++++++++++++++++++++++++--- lib/ui/home_page.dart | 4 ++++ 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/lib/store/code_store.dart b/lib/store/code_store.dart index 04e7a2e91..fbc2d9d6f 100644 --- a/lib/store/code_store.dart +++ b/lib/store/code_store.dart @@ -1,11 +1,14 @@ import 'dart:convert'; +import 'dart:typed_data'; import 'package:collection/collection.dart'; +import 'package:ente_auth/core/configuration.dart'; import 'package:ente_auth/core/event_bus.dart'; import 'package:ente_auth/events/codes_updated_event.dart'; import 'package:ente_auth/models/authenticator/entity_result.dart'; import 'package:ente_auth/models/code.dart'; import 'package:ente_auth/services/authenticator_service.dart'; +import 'package:ente_auth/store/offline_authenticator_db.dart'; import 'package:logging/logging.dart'; class CodeStore { @@ -20,8 +23,6 @@ class CodeStore { _authenticatorService = AuthenticatorService.instance; } - - Future> getAllCodes({AccountMode? accountMode}) async { final mode = accountMode ?? _authenticatorService.getAccountMode(); final List entities = @@ -51,7 +52,7 @@ class CodeStore { bool shouldSync = true, AccountMode? accountMode, }) async { - final mode = accountMode ?? _authenticatorService.getAccountMode(); + final mode = accountMode ?? _authenticatorService.getAccountMode(); final codes = await getAllCodes(accountMode: mode); bool isExistingCode = false; for (final existingCode in codes) { @@ -85,4 +86,36 @@ class CodeStore { await _authenticatorService.deleteEntry(code.generatedID!, mode); Bus.instance.fire(CodesUpdatedEvent()); } + + Future importOfflineCodes() async { + try { + Configuration config = Configuration.instance; + // Account isn't configured yet, so we can't import offline codes + if (!config.hasConfiguredAccount()) { + return; + } + // Never opted for offline mode, so we can't import offline codes + if (!config.hasOptedForOfflineMode()) { + return; + } + Uint8List? hasOfflineKey = config.getOfflineSecretKey(); + if (hasOfflineKey == null) { + // No offline key, so we can't import offline codes + return; + } + List offlineCodes = + await CodeStore.instance.getAllCodes(accountMode: AccountMode.offline); + for (Code eachCode in offlineCodes) { + await CodeStore.instance.addCode( + eachCode, + accountMode: AccountMode.online, + shouldSync: false, + ); + } + OfflineAuthenticatorDB.instance.clearTable(); + AuthenticatorService.instance.onlineSync().ignore(); + } catch (e, s) { + _logger.severe("error while importing offline codes", e, s); + } + } } diff --git a/lib/ui/home_page.dart b/lib/ui/home_page.dart index 3e9c64e9a..c6f3c73c9 100644 --- a/lib/ui/home_page.dart +++ b/lib/ui/home_page.dart @@ -65,6 +65,10 @@ class _HomePageState extends State { await autoLogoutAlert(context); }); _initDeepLinks(); + Future.delayed( + const Duration(seconds: 0), + () => CodeStore.instance.importOfflineCodes(), + ); } From a594d1c9623bab836c95e9772ef73e3ae9ee1936 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Mon, 4 Sep 2023 18:52:38 +0530 Subject: [PATCH 20/23] Show support dev banner in offline mode --- lib/ui/settings/support_dev_widget.dart | 75 +++++++++++++------------ 1 file changed, 40 insertions(+), 35 deletions(-) diff --git a/lib/ui/settings/support_dev_widget.dart b/lib/ui/settings/support_dev_widget.dart index 20429b2f4..bd9082068 100644 --- a/lib/ui/settings/support_dev_widget.dart +++ b/lib/ui/settings/support_dev_widget.dart @@ -26,46 +26,51 @@ class SupportDevWidget extends StatelessWidget { if (snapshot.hasData) { final subscription = snapshot.data; if (subscription != null && subscription.productID == "free") { - return GestureDetector( - onTap: () { - launchUrl(Uri.parse("https://ente.io")); - }, - child: Padding( - padding: - const EdgeInsets.symmetric(vertical: 12.0, horizontal: 6), - child: Column( - children: [ - StyledText( - text: l10n.supportDevs, - tags: { - 'bold-green': StyledTextTag( - style: TextStyle( - fontWeight: FontWeight.bold, - color: getEnteColorScheme(context).primaryGreen, - ), - ), - }, - ), - const Padding(padding: EdgeInsets.all(6)), - Platform.isAndroid - ? Text( - l10n.supportDiscount, - textAlign: TextAlign.center, - style: const TextStyle( - color: Colors.grey, - ), - ) - : const SizedBox.shrink(), - ], - ), - ), - ); + return buildWidget(l10n, context); } } return const SizedBox.shrink(); }, ); + } else { + return buildWidget(l10n, context); } - return const SizedBox.shrink(); + } + + GestureDetector buildWidget(AppLocalizations l10n, BuildContext context) { + return GestureDetector( + onTap: () { + launchUrl(Uri.parse("https://ente.io")); + }, + child: Padding( + padding: + const EdgeInsets.symmetric(vertical: 12.0, horizontal: 6), + child: Column( + children: [ + StyledText( + text: l10n.supportDevs, + tags: { + 'bold-green': StyledTextTag( + style: TextStyle( + fontWeight: FontWeight.bold, + color: getEnteColorScheme(context).primaryGreen, + ), + ), + }, + ), + const Padding(padding: EdgeInsets.all(6)), + Platform.isAndroid + ? Text( + l10n.supportDiscount, + textAlign: TextAlign.center, + style: const TextStyle( + color: Colors.grey, + ), + ) + : const SizedBox.shrink(), + ], + ), + ), + ); } } From 4893753b19e73f0b503c99b0fd6e14ece9919487 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Mon, 4 Sep 2023 19:10:54 +0530 Subject: [PATCH 21/23] Add warning before export --- lib/l10n/arb/app_en.arb | 4 ++- lib/ui/settings/data/data_section_widget.dart | 2 +- lib/ui/settings_page.dart | 28 ++++++++++++++++--- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 7633f0fe8..0789c227a 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -322,5 +322,7 @@ "passwordToEncryptExport": "Password to encrypt export", "export": "Export", "useOffline": "Use without backups", - "signInToBackup": "Sign in to backup your codes" + "signInToBackup": "Sign in to backup your codes", + "singIn": "Sign in", + "sigInBackupReminder": "Please export your codes to ensure that you have a backup you can restore from." } diff --git a/lib/ui/settings/data/data_section_widget.dart b/lib/ui/settings/data/data_section_widget.dart index a067bc024..bf494c99c 100644 --- a/lib/ui/settings/data/data_section_widget.dart +++ b/lib/ui/settings/data/data_section_widget.dart @@ -51,7 +51,7 @@ class DataSectionWidget extends StatelessWidget { trailingIcon: Icons.chevron_right_outlined, trailingIconIsMuted: true, onTap: () async { - handleExportClick(context); + await handleExportClick(context); }, ), sectionOptionSpacing, diff --git a/lib/ui/settings_page.dart b/lib/ui/settings_page.dart index 36e9fab8c..ab5083da0 100644 --- a/lib/ui/settings_page.dart +++ b/lib/ui/settings_page.dart @@ -6,21 +6,26 @@ import 'package:ente_auth/onboarding/view/onboarding_page.dart'; import 'package:ente_auth/services/user_service.dart'; import 'package:ente_auth/theme/colors.dart'; import 'package:ente_auth/theme/ente_theme.dart'; +import 'package:ente_auth/ui/components/buttons/button_widget.dart'; +import 'package:ente_auth/ui/components/models/button_result.dart'; import 'package:ente_auth/ui/components/notification_warning_widget.dart'; import 'package:ente_auth/ui/settings/about_section_widget.dart'; import 'package:ente_auth/ui/settings/account_section_widget.dart'; import 'package:ente_auth/ui/settings/app_version_widget.dart'; import 'package:ente_auth/ui/settings/data/data_section_widget.dart'; +import 'package:ente_auth/ui/settings/data/export_widget.dart'; import 'package:ente_auth/ui/settings/security_section_widget.dart'; import 'package:ente_auth/ui/settings/social_section_widget.dart'; import 'package:ente_auth/ui/settings/support_dev_widget.dart'; import 'package:ente_auth/ui/settings/support_section_widget.dart'; import 'package:ente_auth/ui/settings/theme_switch_widget.dart'; import 'package:ente_auth/ui/settings/title_bar_widget.dart'; +import 'package:ente_auth/utils/dialog_util.dart'; import 'package:ente_auth/utils/navigation_util.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; + class SettingsPage extends StatelessWidget { final ValueNotifier emailNotifier; @@ -80,11 +85,26 @@ class SettingsPage extends StatelessWidget { actionIcon: Icons.arrow_forward, text: context.l10n.signInToBackup, type: NotificationType.notice, - onTap: () async => { - await routeToPage( + onTap: () async { + ButtonResult? result = await showChoiceActionSheet( context, - const OnboardingPage(), - ), + title: context.l10n.warning, + body: context.l10n.sigInBackupReminder, + secondButtonLabel: context.l10n.singIn, + secondButtonAction: ButtonAction.second, + firstButtonLabel: context.l10n.exportCodes, + ); + if (result == null) return; + if (result.action == ButtonAction.first) { + await handleExportClick(context); + } else { + if (result.action == ButtonAction.second) { + await routeToPage( + context, + const OnboardingPage(), + ); + } + } }, ), sectionSpacing, From 0d0c89900a438dd9c324628907c6c9033e18e2b4 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Mon, 4 Sep 2023 19:22:26 +0530 Subject: [PATCH 22/23] Show copoun code for iOS --- lib/ui/settings/support_dev_widget.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/ui/settings/support_dev_widget.dart b/lib/ui/settings/support_dev_widget.dart index bd9082068..d0edd4e70 100644 --- a/lib/ui/settings/support_dev_widget.dart +++ b/lib/ui/settings/support_dev_widget.dart @@ -49,6 +49,7 @@ class SupportDevWidget extends StatelessWidget { children: [ StyledText( text: l10n.supportDevs, + style: getEnteTextTheme(context).large, tags: { 'bold-green': StyledTextTag( style: TextStyle( @@ -59,15 +60,13 @@ class SupportDevWidget extends StatelessWidget { }, ), const Padding(padding: EdgeInsets.all(6)), - Platform.isAndroid - ? Text( + Text( l10n.supportDiscount, textAlign: TextAlign.center, style: const TextStyle( color: Colors.grey, ), ) - : const SizedBox.shrink(), ], ), ), From 57930b2dd8e36a5aeb16fc2b7f1d940a295cf74c Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Mon, 4 Sep 2023 19:30:55 +0530 Subject: [PATCH 23/23] Add warning when user selects without backup option --- lib/l10n/arb/app_en.arb | 3 ++- lib/onboarding/view/onboarding_page.dart | 34 +++++++++++++++++------- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 0789c227a..6614a3221 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -324,5 +324,6 @@ "useOffline": "Use without backups", "signInToBackup": "Sign in to backup your codes", "singIn": "Sign in", - "sigInBackupReminder": "Please export your codes to ensure that you have a backup you can restore from." + "sigInBackupReminder": "Please export your codes to ensure that you have a backup you can restore from.", + "offlineModeWarning": "You have chosen to proceed without backups. Please take manual backups to make sure your codes are safe." } diff --git a/lib/onboarding/view/onboarding_page.dart b/lib/onboarding/view/onboarding_page.dart index d1649aa0f..5d4e7e3e3 100644 --- a/lib/onboarding/view/onboarding_page.dart +++ b/lib/onboarding/view/onboarding_page.dart @@ -7,7 +7,6 @@ import 'package:ente_auth/ente_theme_data.dart'; import 'package:ente_auth/events/trigger_logout_event.dart'; import "package:ente_auth/l10n/l10n.dart"; import 'package:ente_auth/locale.dart'; -import 'package:ente_auth/services/local_authentication_service.dart'; import 'package:ente_auth/theme/text_style.dart'; import 'package:ente_auth/ui/account/email_entry_page.dart'; import 'package:ente_auth/ui/account/login_page.dart'; @@ -15,8 +14,11 @@ import 'package:ente_auth/ui/account/logout_dialog.dart'; import 'package:ente_auth/ui/account/password_entry_page.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'; import 'package:ente_auth/ui/home_page.dart'; import 'package:ente_auth/ui/settings/language_picker.dart'; +import 'package:ente_auth/utils/dialog_util.dart'; import 'package:ente_auth/utils/navigation_util.dart'; import 'package:ente_auth/utils/toast_util.dart'; import 'package:flutter/foundation.dart'; @@ -185,14 +187,28 @@ class _OnboardingPageState extends State { showToast(context, "Sorry, biometric authentication is not supported on this device."); return; } - await Configuration.instance.optForOfflineMode(); - Navigator.of(context).push( - MaterialPageRoute( - builder: (BuildContext context) { - return const HomePage(); - }, - ), - ); + final bool hasOptedBefore = Configuration.instance.hasOptedForOfflineMode(); + ButtonResult? result; + if(!hasOptedBefore) { + result = await showChoiceActionSheet( + context, + title: context.l10n.warning, + body: context.l10n.offlineModeWarning, + secondButtonLabel: context.l10n.cancel, + firstButtonLabel: context.l10n.ok, + ); + } + if (hasOptedBefore || result?.action == ButtonAction.first) { + await Configuration.instance.optForOfflineMode(); + Navigator.of(context).push( + MaterialPageRoute( + builder: (BuildContext context) { + return const HomePage(); + }, + ), + ); + } + } void _navigateToSignUpPage() {