feat: desktop (7124ed710acf33d895faa4730a04b87f9c5cac24)

This commit is contained in:
Prateek Sunal 2024-03-05 14:33:39 +05:30
parent ef553d9401
commit b86729050a
166 changed files with 3120 additions and 2641 deletions

6
auth/.gitignore vendored
View file

@ -15,6 +15,11 @@
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
.dart_tool/
@ -32,3 +37,4 @@ lib/generated_plugin_registrant.dart
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
android/key.properties
dist/

View file

@ -32,7 +32,7 @@ if (keystorePropertiesFile.exists()) {
}
android {
compileSdkVersion 33
compileSdkVersion 34
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
@ -46,7 +46,7 @@ android {
defaultConfig {
applicationId "io.ente.auth"
minSdkVersion 20
minSdkVersion 21
targetSdkVersion 33
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
@ -56,7 +56,7 @@ android {
signingConfigs {
release {
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : file(System.getenv("SIGNING_KEY_PATH"))
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : System.getenv("SIGNING_KEY_PATH") ? file(System.getenv("SIGNING_KEY_PATH")) : null
keyAlias keystoreProperties['keyAlias'] ? keystoreProperties['keyAlias'] : System.getenv("SIGNING_KEY_ALIAS")
keyPassword keystoreProperties['keyPassword'] ? keystoreProperties['keyPassword'] : System.getenv("SIGNING_KEY_PASSWORD")
storePassword keystoreProperties['storePassword'] ? keystoreProperties['storePassword'] : System.getenv("SIGNING_STORE_PASSWORD")
@ -109,6 +109,7 @@ dependencies {
implementation 'io.sentry:sentry-android:2.0.0'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'com.android.support:multidex:1.0.3'
implementation 'com.google.guava:guava:28.2-android'
implementation 'com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.1.1'

View file

@ -0,0 +1,25 @@
output: dist/
releases:
- name: dev
jobs:
- name: release-dev-linux-zip
package:
platform: linux
target: zip
- name: release-dev-linux-deb
package:
platform: linux
target: deb
- name: release-dev-linux-appimage
package:
platform: linux
target: appimage
- name: release-dev-windows-exe
package:
platform: windows
target: exe
- name: release-dev-macos-dmg
package:
platform: macos
target: dmg

View file

@ -1,7 +1,9 @@
PODS:
- connectivity (0.0.1):
- app_links (0.0.1):
- Flutter
- Reachability
- connectivity_plus (0.0.1):
- Flutter
- ReachabilitySwift
- device_info_plus (0.0.1):
- Flutter
- DKImagePickerController/Core (4.3.4):
@ -45,27 +47,24 @@ PODS:
- Flutter (1.0.0)
- flutter_email_sender (0.0.1):
- Flutter
- flutter_inappwebview (0.0.1):
- flutter_inappwebview_ios (0.0.1):
- Flutter
- flutter_inappwebview/Core (= 0.0.1)
- flutter_inappwebview_ios/Core (= 0.0.1)
- OrderedSet (~> 5.0)
- flutter_inappwebview/Core (0.0.1):
- flutter_inappwebview_ios/Core (0.0.1):
- Flutter
- OrderedSet (~> 5.0)
- flutter_local_authentication (1.2.0):
- Flutter
- flutter_local_notifications (0.0.1):
- Flutter
- flutter_native_splash (0.0.1):
- Flutter
- flutter_secure_storage (6.0.0):
- Flutter
- flutter_sodium (0.0.1):
- Flutter
- fluttertoast (0.0.2):
- Flutter
- Toast
- FMDB (2.7.5):
- FMDB/standard (= 2.7.5)
- FMDB/standard (2.7.5)
- local_auth_ios (0.0.1):
- Flutter
- move_to_background (0.0.1):
@ -84,45 +83,63 @@ PODS:
- qr_code_scanner (0.2.0):
- Flutter
- MTBBarcodeScanner
- Reachability (3.2)
- SDWebImage (5.17.0):
- SDWebImage/Core (= 5.17.0)
- SDWebImage/Core (5.17.0)
- Sentry/HybridSDK (8.9.1):
- SentryPrivate (= 8.9.1)
- ReachabilitySwift (5.0.0)
- SDWebImage (5.18.10):
- SDWebImage/Core (= 5.18.10)
- SDWebImage/Core (5.18.10)
- Sentry/HybridSDK (8.19.0):
- SentryPrivate (= 8.19.0)
- sentry_flutter (0.0.1):
- Flutter
- FlutterMacOS
- Sentry/HybridSDK (= 8.9.1)
- SentryPrivate (8.9.1)
- Sentry/HybridSDK (= 8.19.0)
- SentryPrivate (8.19.0)
- share_plus (0.0.1):
- Flutter
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- smart_auth (0.0.1):
- Flutter
- sodium_libs (2.2.0):
- Flutter
- sqflite (0.0.3):
- Flutter
- FMDB (>= 2.7.5)
- SwiftyGif (5.4.4)
- Toast (4.0.0)
- uni_links (0.0.1):
- FlutterMacOS
- sqlite3 (3.45.1):
- sqlite3/common (= 3.45.1)
- sqlite3/common (3.45.1)
- sqlite3/fts5 (3.45.1):
- sqlite3/common
- sqlite3/perf-threadsafe (3.45.1):
- sqlite3/common
- sqlite3/rtree (3.45.1):
- sqlite3/common
- sqlite3_flutter_libs (0.0.1):
- Flutter
- sqlite3 (~> 3.45.1)
- sqlite3/fts5
- sqlite3/perf-threadsafe
- sqlite3/rtree
- SwiftyGif (5.4.4)
- Toast (4.1.0)
- url_launcher_ios (0.0.1):
- Flutter
DEPENDENCIES:
- connectivity (from `.symlinks/plugins/connectivity/ios`)
- app_links (from `.symlinks/plugins/app_links/ios`)
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- file_picker (from `.symlinks/plugins/file_picker/ios`)
- file_saver (from `.symlinks/plugins/file_saver/ios`)
- fk_user_agent (from `.symlinks/plugins/fk_user_agent/ios`)
- Flutter (from `Flutter`)
- flutter_email_sender (from `.symlinks/plugins/flutter_email_sender/ios`)
- flutter_inappwebview (from `.symlinks/plugins/flutter_inappwebview/ios`)
- flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`)
- flutter_local_authentication (from `.symlinks/plugins/flutter_local_authentication/ios`)
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
- flutter_sodium (from `.symlinks/plugins/flutter_sodium/ios`)
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
- local_auth_ios (from `.symlinks/plugins/local_auth_ios/ios`)
- move_to_background (from `.symlinks/plugins/move_to_background/ios`)
@ -134,27 +151,31 @@ DEPENDENCIES:
- sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite (from `.symlinks/plugins/sqflite/ios`)
- uni_links (from `.symlinks/plugins/uni_links/ios`)
- smart_auth (from `.symlinks/plugins/smart_auth/ios`)
- sodium_libs (from `.symlinks/plugins/sodium_libs/ios`)
- sqflite (from `.symlinks/plugins/sqflite/darwin`)
- sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
SPEC REPOS:
trunk:
- DKImagePickerController
- DKPhotoGallery
- FMDB
- MTBBarcodeScanner
- OrderedSet
- Reachability
- ReachabilitySwift
- SDWebImage
- Sentry
- SentryPrivate
- sqlite3
- SwiftyGif
- Toast
EXTERNAL SOURCES:
connectivity:
:path: ".symlinks/plugins/connectivity/ios"
app_links:
:path: ".symlinks/plugins/app_links/ios"
connectivity_plus:
:path: ".symlinks/plugins/connectivity_plus/ios"
device_info_plus:
:path: ".symlinks/plugins/device_info_plus/ios"
file_picker:
@ -167,16 +188,16 @@ EXTERNAL SOURCES:
:path: Flutter
flutter_email_sender:
:path: ".symlinks/plugins/flutter_email_sender/ios"
flutter_inappwebview:
:path: ".symlinks/plugins/flutter_inappwebview/ios"
flutter_inappwebview_ios:
:path: ".symlinks/plugins/flutter_inappwebview_ios/ios"
flutter_local_authentication:
:path: ".symlinks/plugins/flutter_local_authentication/ios"
flutter_local_notifications:
:path: ".symlinks/plugins/flutter_local_notifications/ios"
flutter_native_splash:
:path: ".symlinks/plugins/flutter_native_splash/ios"
flutter_secure_storage:
:path: ".symlinks/plugins/flutter_secure_storage/ios"
flutter_sodium:
:path: ".symlinks/plugins/flutter_sodium/ios"
fluttertoast:
:path: ".symlinks/plugins/fluttertoast/ios"
local_auth_ios:
@ -199,51 +220,58 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
smart_auth:
:path: ".symlinks/plugins/smart_auth/ios"
sodium_libs:
:path: ".symlinks/plugins/sodium_libs/ios"
sqflite:
:path: ".symlinks/plugins/sqflite/ios"
uni_links:
:path: ".symlinks/plugins/uni_links/ios"
:path: ".symlinks/plugins/sqflite/darwin"
sqlite3_flutter_libs:
:path: ".symlinks/plugins/sqlite3_flutter_libs/ios"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
SPEC CHECKSUMS:
connectivity: c4130b2985d4ef6fd26f9702e886bd5260681467
device_info_plus: e5c5da33f982a436e103237c0c85f9031142abed
app_links: 5ef33d0d295a89d9d16bb81b0e3b0d5f70d6c875
connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d
device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac
DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179
file_picker: ce3938a0df3cc1ef404671531facef740d03f920
file_picker: 15fd9539e4eb735dc54bae8c0534a7a9511a03de
file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
fk_user_agent: 1f47ec39291e8372b1d692b50084b0d54103c545
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_email_sender: 02d7443217d8c41483223627972bfdc09f74276b
flutter_inappwebview: acd4fc0f012cefd09015000c241137d82f01ba62
flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743
flutter_inappwebview_ios: 97215cf7d4677db55df76782dbd2930c5e1c1ea0
flutter_local_authentication: 1172a4dd88f6306dadce067454e2c4caf07977bb
flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086
flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be
flutter_sodium: c84426b4de738514b5b66cfdeb8a06634e72fe0b
fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
local_auth_ios: c6cf091ded637a88f24f86a8875d8b0f526e2605
local_auth_ios: 1ba1475238daa33a6ffa2a29242558437be435ac
move_to_background: 39a5b79b26d577b0372cbe8a8c55e7aa9fcd3a2d
MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb
open_filex: 6e26e659846ec990262224a12ef1c528bb4edbe4
OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c
package_info_plus: fd030dabf36271f146f1f3beacd48f564b0f17f7
path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
privacy_screen: 1a131c052ceb3c3659934b003b0d397c2381a24e
qr_code_scanner: bb67d64904c3b9658ada8c402e8b4d406d5d796e
Reachability: 33e18b67625424e47b6cde6d202dce689ad7af96
SDWebImage: 750adf017a315a280c60fde706ab1e552a3ae4e9
Sentry: e3203780941722a1fcfee99e351de14244c7f806
sentry_flutter: 8f0ffd53088e6a4d50c095852c5cad9e4405025c
SentryPrivate: 5e3683390f66611fc7c6215e27645873adb55d13
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
SDWebImage: fc8f2d48bbfd72ef39d70e981bd24a3f3be53fec
Sentry: 1ebcaef678a27c8ac515f974cb5425dd1bbdec2f
sentry_flutter: ecdfbedee55337205561cfa782ee02d31ec83e1f
SentryPrivate: 765c9b4ebe9ac1a5fcdc067c5a1cfbf3f10e1677
share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5
shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126
sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a
shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695
smart_auth: 4bedbc118723912d0e45a07e8ab34039c19e04f2
sodium_libs: 0486eb2c3172ce494406367d4b379042444b769d
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
sqlite3: 73b7fc691fdc43277614250e04d183740cb15078
sqlite3_flutter_libs: af0e8fe9bce48abddd1ffdbbf839db0302d72d80
SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
uni_links: d97da20c7701486ba192624d99bffaaffcfc298a
url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4
Toast: ec33c32b8688982cecc6348adeae667c1b9938da
url_launcher_ios: bbd758c6e7f9fd7b5b1d4cde34d2b95fcce5e812
PODFILE CHECKSUM: b4e3a7eabb03395b66e81fc061789f61526ee6bb

View file

@ -1,5 +1,6 @@
import UIKit
import Flutter
import UIKit
import app_links
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
@ -8,6 +9,15 @@ import Flutter
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
super.application(application, didFinishLaunchingWithOptions: launchOptions)
if let url = AppLinks.shared.getLink(launchOptions: launchOptions) {
AppLinks.shared.handleLink(url: url)
}
return false
// return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}

View file

@ -77,5 +77,9 @@
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
<key>UIFileSharingEnabled</key>
<true/>
</dict>
</plist>

View file

@ -20,7 +20,7 @@ import 'package:flutter_localizations/flutter_localizations.dart';
class App extends StatefulWidget {
final Locale locale;
const App({Key? key, this.locale = const Locale("en")}) : super(key: key);
const App({super.key, this.locale = const Locale("en")});
static void setLocale(BuildContext context, Locale newLocale) {
_AppState state = context.findAncestorStateOfType<_AppState>()!;

View file

@ -12,12 +12,12 @@ import 'package:ente_auth/models/key_attributes.dart';
import 'package:ente_auth/models/key_gen_result.dart';
import 'package:ente_auth/models/private_key_attributes.dart';
import 'package:ente_auth/store/authenticator_db.dart';
import 'package:ente_auth/utils/crypto_util.dart';
import 'package:ente_crypto_dart/ente_crypto_dart.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:flutter_sodium/flutter_sodium.dart';
import 'package:logging/logging.dart';
import 'package:path_provider/path_provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
import 'package:tuple/tuple.dart';
class Configuration {
@ -70,9 +70,10 @@ class Configuration {
Future<void> init() async {
_preferences = await SharedPreferences.getInstance();
sqfliteFfiInit();
_secureStorage = const FlutterSecureStorage();
_documentsDirectory = (await getApplicationDocumentsDirectory()).path;
_tempDirectory = _documentsDirectory + "/temp/";
_tempDirectory = "$_documentsDirectory/temp/";
final tempDirectory = io.Directory(_tempDirectory);
try {
final currentTime = DateTime.now().microsecondsSinceEpoch;
@ -160,7 +161,7 @@ class Configuration {
// decrypt the master key
final kekSalt = CryptoUtil.getSaltToDeriveKey();
final derivedKeyResult = await CryptoUtil.deriveSensitiveKey(
utf8.encode(password) as Uint8List,
utf8.encode(password),
kekSalt,
);
final loginKey = await CryptoUtil.deriveLoginKey(derivedKeyResult.key);
@ -170,28 +171,28 @@ class Configuration {
CryptoUtil.encryptSync(masterKey, derivedKeyResult.key);
// Generate a public-private keypair and encrypt the latter
final keyPair = await CryptoUtil.generateKeyPair();
final keyPair = CryptoUtil.generateKeyPair();
final encryptedSecretKeyData =
CryptoUtil.encryptSync(keyPair.sk, masterKey);
CryptoUtil.encryptSync(keyPair.secretKey.extractBytes(), masterKey);
final attributes = KeyAttributes(
Sodium.bin2base64(kekSalt),
Sodium.bin2base64(encryptedKeyData.encryptedData!),
Sodium.bin2base64(encryptedKeyData.nonce!),
Sodium.bin2base64(keyPair.pk),
Sodium.bin2base64(encryptedSecretKeyData.encryptedData!),
Sodium.bin2base64(encryptedSecretKeyData.nonce!),
CryptoUtil.bin2base64(kekSalt),
CryptoUtil.bin2base64(encryptedKeyData.encryptedData!),
CryptoUtil.bin2base64(encryptedKeyData.nonce!),
CryptoUtil.bin2base64(keyPair.publicKey),
CryptoUtil.bin2base64(encryptedSecretKeyData.encryptedData!),
CryptoUtil.bin2base64(encryptedSecretKeyData.nonce!),
derivedKeyResult.memLimit,
derivedKeyResult.opsLimit,
Sodium.bin2base64(encryptedMasterKey.encryptedData!),
Sodium.bin2base64(encryptedMasterKey.nonce!),
Sodium.bin2base64(encryptedRecoveryKey.encryptedData!),
Sodium.bin2base64(encryptedRecoveryKey.nonce!),
CryptoUtil.bin2base64(encryptedMasterKey.encryptedData!),
CryptoUtil.bin2base64(encryptedMasterKey.nonce!),
CryptoUtil.bin2base64(encryptedRecoveryKey.encryptedData!),
CryptoUtil.bin2base64(encryptedRecoveryKey.nonce!),
);
final privateAttributes = PrivateKeyAttributes(
Sodium.bin2base64(masterKey),
Sodium.bin2hex(recoveryKey),
Sodium.bin2base64(keyPair.sk),
CryptoUtil.bin2base64(masterKey),
CryptoUtil.bin2hex(recoveryKey),
CryptoUtil.bin2base64(keyPair.secretKey.extractBytes()),
);
return KeyGenResult(attributes, privateAttributes, loginKey);
}
@ -206,7 +207,7 @@ class Configuration {
// decrypt the master key
final kekSalt = CryptoUtil.getSaltToDeriveKey();
final derivedKeyResult = await CryptoUtil.deriveSensitiveKey(
utf8.encode(password) as Uint8List,
utf8.encode(password),
kekSalt,
);
final loginKey = await CryptoUtil.deriveLoginKey(derivedKeyResult.key);
@ -218,9 +219,9 @@ class Configuration {
final existingAttributes = getKeyAttributes();
final updatedAttributes = existingAttributes!.copyWith(
kekSalt: Sodium.bin2base64(kekSalt),
encryptedKey: Sodium.bin2base64(encryptedKeyData.encryptedData!),
keyDecryptionNonce: Sodium.bin2base64(encryptedKeyData.nonce!),
kekSalt: CryptoUtil.bin2base64(kekSalt),
encryptedKey: CryptoUtil.bin2base64(encryptedKeyData.encryptedData!),
keyDecryptionNonce: CryptoUtil.bin2base64(encryptedKeyData.nonce!),
memLimit: derivedKeyResult.memLimit,
opsLimit: derivedKeyResult.opsLimit,
);
@ -238,8 +239,8 @@ class Configuration {
}) async {
_logger.info('Start decryptAndSaveSecrets');
keyEncryptionKey ??= await CryptoUtil.deriveKey(
utf8.encode(password) as Uint8List,
Sodium.base642bin(attributes.kekSalt),
utf8.encode(password),
CryptoUtil.base642bin(attributes.kekSalt),
attributes.memLimit,
attributes.opsLimit,
);
@ -248,31 +249,31 @@ class Configuration {
Uint8List key;
try {
key = CryptoUtil.decryptSync(
Sodium.base642bin(attributes.encryptedKey),
CryptoUtil.base642bin(attributes.encryptedKey),
keyEncryptionKey,
Sodium.base642bin(attributes.keyDecryptionNonce),
CryptoUtil.base642bin(attributes.keyDecryptionNonce),
);
} catch (e) {
_logger.severe('master-key failed, incorrect password?', e);
throw Exception("Incorrect password");
}
_logger.info("master-key done");
await setKey(Sodium.bin2base64(key));
await setKey(CryptoUtil.bin2base64(key));
final secretKey = CryptoUtil.decryptSync(
Sodium.base642bin(attributes.encryptedSecretKey),
CryptoUtil.base642bin(attributes.encryptedSecretKey),
key,
Sodium.base642bin(attributes.secretKeyDecryptionNonce),
CryptoUtil.base642bin(attributes.secretKeyDecryptionNonce),
);
_logger.info("secret-key done");
await setSecretKey(Sodium.bin2base64(secretKey));
await setSecretKey(CryptoUtil.bin2base64(secretKey));
final token = CryptoUtil.openSealSync(
Sodium.base642bin(getEncryptedToken()!),
Sodium.base642bin(attributes.publicKey),
CryptoUtil.base642bin(getEncryptedToken()!),
CryptoUtil.base642bin(attributes.publicKey),
secretKey,
);
_logger.info('appToken done');
await setToken(
Sodium.bin2base64(token, variant: Sodium.base64VariantUrlsafe),
CryptoUtil.bin2base64(token, urlSafe: true),
);
return keyEncryptionKey;
}
@ -291,28 +292,28 @@ class Configuration {
Uint8List masterKey;
try {
masterKey = await CryptoUtil.decrypt(
Sodium.base642bin(attributes!.masterKeyEncryptedWithRecoveryKey),
Sodium.hex2bin(recoveryKey),
Sodium.base642bin(attributes.masterKeyDecryptionNonce),
CryptoUtil.base642bin(attributes!.masterKeyEncryptedWithRecoveryKey),
CryptoUtil.hex2bin(recoveryKey),
CryptoUtil.base642bin(attributes.masterKeyDecryptionNonce),
);
} catch (e) {
_logger.severe(e);
rethrow;
}
await setKey(Sodium.bin2base64(masterKey));
await setKey(CryptoUtil.bin2base64(masterKey));
final secretKey = CryptoUtil.decryptSync(
Sodium.base642bin(attributes.encryptedSecretKey),
CryptoUtil.base642bin(attributes.encryptedSecretKey),
masterKey,
Sodium.base642bin(attributes.secretKeyDecryptionNonce),
CryptoUtil.base642bin(attributes.secretKeyDecryptionNonce),
);
await setSecretKey(Sodium.bin2base64(secretKey));
await setSecretKey(CryptoUtil.bin2base64(secretKey));
final token = CryptoUtil.openSealSync(
Sodium.base642bin(getEncryptedToken()!),
Sodium.base642bin(attributes.publicKey),
CryptoUtil.base642bin(getEncryptedToken()!),
CryptoUtil.base642bin(attributes.publicKey),
secretKey,
);
await setToken(
Sodium.bin2base64(token, variant: Sodium.base64VariantUrlsafe),
CryptoUtil.bin2base64(token, urlSafe: true),
);
}
@ -400,27 +401,31 @@ class Configuration {
}
Uint8List? getKey() {
return _key == null ? null : Sodium.base642bin(_key!);
return _key == null ? null : CryptoUtil.base642bin(_key!);
}
Uint8List? getSecretKey() {
return _secretKey == null ? null : Sodium.base642bin(_secretKey!);
return _secretKey == null ? null : CryptoUtil.base642bin(_secretKey!);
}
Uint8List? getAuthSecretKey() {
return _authSecretKey == null ? null : Sodium.base642bin(_authSecretKey!);
return _authSecretKey == null
? null
: CryptoUtil.base642bin(_authSecretKey!);
}
Uint8List? getOfflineSecretKey() {
return _offlineAuthKey == null ? null : Sodium.base642bin(_offlineAuthKey!);
return _offlineAuthKey == null
? null
: CryptoUtil.base642bin(_offlineAuthKey!);
}
Uint8List getRecoveryKey() {
final keyAttributes = getKeyAttributes()!;
return CryptoUtil.decryptSync(
Sodium.base642bin(keyAttributes.recoveryKeyEncryptedWithMasterKey),
CryptoUtil.base642bin(keyAttributes.recoveryKeyEncryptedWithMasterKey),
getKey()!,
Sodium.base642bin(keyAttributes.recoveryKeyDecryptionNonce),
CryptoUtil.base642bin(keyAttributes.recoveryKeyDecryptionNonce),
);
}
@ -447,7 +452,7 @@ class Configuration {
iOptions: _secureStorageOptionsIOS,
);
} else {
_offlineAuthKey = Sodium.bin2base64(CryptoUtil.generateKey());
_offlineAuthKey = CryptoUtil.bin2base64(CryptoUtil.generateKey());
await _secureStorage.write(
key: offlineAuthSecretKey,
value: _offlineAuthKey,

View file

@ -7,7 +7,7 @@ const String sentryDSN =
"https://ed4ddd6309b847ba8849935e26e9b648@sentry.ente.io/9";
const String sentryTunnel = "https://sentry-reporter.ente.io";
const String roadmapURL = "https://roadmap.ente.io";
const String githubDiscussionsUrl = "https://github.com/ente-io/ente/discussions";
const String githubIssuesUrl = "https://github.com/ente-io/auth/issues?q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc";
const int microSecondsInDay = 86400000000;
const int android11SDKINT = 30;
const int galleryLoadStartTime = -8000000000000000; // Wednesday, March 6, 1748

View file

@ -1,9 +1,9 @@
class InvalidFileError extends ArgumentError {
InvalidFileError(String message) : super(message);
InvalidFileError(String super.message);
}
class InvalidFileUploadState extends AssertionError {
InvalidFileUploadState(String message) : super(message);
InvalidFileUploadState(String super.message);
}
class SubscriptionAlreadyClaimedError extends Error {}
@ -30,19 +30,15 @@ class UnauthorizedError extends Error {}
class RequestCancelledError extends Error {}
class InvalidSyncStatusError extends AssertionError {
InvalidSyncStatusError(String message) : super(message);
InvalidSyncStatusError(String super.message);
}
class UnauthorizedEditError extends AssertionError {}
class InvalidStateError extends AssertionError {
InvalidStateError(String message) : super(message);
InvalidStateError(String super.message);
}
class KeyDerivationError extends Error {}
class LoginKeyDerivationError extends Error {}
class SrpSetupNotCompleteError extends Error {}
class AuthenticatorKeyNotFound extends Error {}

View file

@ -235,14 +235,14 @@ class SuperLogging {
extraLines = null;
}
final str = (config.prefix) + " " + rec.toPrettyString(extraLines);
final str = "${config.prefix} ${rec.toPrettyString(extraLines)}";
// write to stdout
printLog(str);
// push to log queue
if (fileIsEnabled) {
fileQueueEntries.add(str + '\n');
fileQueueEntries.add('$str\n');
if (fileQueueEntries.length == 1) {
flushQueue();
}
@ -275,7 +275,7 @@ class SuperLogging {
static var logChunkSize = 800;
static void printLog(String text) {
text.chunked(logChunkSize).forEach(print);
text.chunked(logChunkSize).forEach(debugPrint);
}
/// A queue to be consumed by [setupSentry].
@ -354,7 +354,7 @@ class SuperLogging {
final date = config.dateFmt!.parse(basename(file.path));
dates[file as File] = date;
files.add(file);
} on FormatException {}
} on Exception catch (_) {}
}
final nowTime = DateTime.now();
@ -374,7 +374,7 @@ class SuperLogging {
"deleting log file ${file.path}",
);
await file.delete();
} catch (ignore) {}
} on Exception catch (_) {}
}
}

View file

@ -3,9 +3,10 @@ import 'dart:io';
import 'package:dio/dio.dart';
import 'package:ente_auth/core/configuration.dart';
import 'package:ente_auth/core/constants.dart';
import 'package:ente_auth/utils/package_info_util.dart';
import 'package:ente_auth/utils/platform_util.dart';
import 'package:fk_user_agent/fk_user_agent.dart';
import 'package:flutter/foundation.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:uuid/uuid.dart';
@ -21,16 +22,22 @@ class Network {
late Dio _enteDio;
Future<void> init() async {
await FkUserAgent.init();
final packageInfo = await PackageInfo.fromPlatform();
if (PlatformUtil.isMobile()) await FkUserAgent.init();
final packageInfo = await PackageInfoUtil().getPackageInfo();
final version = PackageInfoUtil().getVersion(packageInfo);
final packageName = PackageInfoUtil().getPackageName(packageInfo);
final preferences = await SharedPreferences.getInstance();
_dio = Dio(
BaseOptions(
connectTimeout: kConnectTimeout,
connectTimeout: Duration(milliseconds: kConnectTimeout),
headers: {
HttpHeaders.userAgentHeader: FkUserAgent.userAgent,
'X-Client-Version': packageInfo.version,
'X-Client-Package': packageInfo.packageName,
HttpHeaders.userAgentHeader: PlatformUtil.isMobile()
? FkUserAgent.userAgent
: Platform.operatingSystem,
'X-Client-Version': version,
'X-Client-Package': packageName,
},
),
);
@ -38,11 +45,14 @@ class Network {
_enteDio = Dio(
BaseOptions(
baseUrl: apiEndpoint,
connectTimeout: kConnectTimeout,
connectTimeout: Duration(milliseconds: kConnectTimeout),
headers: {
HttpHeaders.userAgentHeader: FkUserAgent.userAgent,
'X-Client-Version': packageInfo.version,
'X-Client-Package': packageInfo.packageName,
if (PlatformUtil.isMobile())
HttpHeaders.userAgentHeader: FkUserAgent.userAgent
else
HttpHeaders.userAgentHeader: Platform.operatingSystem,
'X-Client-Version': version,
'X-Client-Package': packageName,
},
),
);

View file

@ -11,6 +11,7 @@ final lightThemeData = ThemeData(
iconTheme: const IconThemeData(color: Colors.black),
primaryIconTheme:
const IconThemeData(color: Colors.red, opacity: 1.0, size: 50.0),
buttonTheme: const ButtonThemeData(),
outlinedButtonTheme: buildOutlinedButtonThemeData(
bgDisabled: const Color.fromRGBO(158, 158, 158, 1),
bgEnabled: const Color.fromRGBO(0, 0, 0, 1),
@ -72,24 +73,42 @@ final lightThemeData = ThemeData(
? const Color.fromRGBO(255, 255, 255, 1)
: const Color.fromRGBO(0, 0, 0, 1);
}),
), radioTheme: RadioThemeData(
fillColor: MaterialStateProperty.resolveWith<Color?>((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) { return null; }
if (states.contains(MaterialState.selected)) { return const Color.fromRGBO(102, 187, 106, 1); }
),
radioTheme: RadioThemeData(
fillColor:
MaterialStateProperty.resolveWith<Color?>((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return null;
}
if (states.contains(MaterialState.selected)) {
return const Color.fromRGBO(102, 187, 106, 1);
}
return null;
}),
), switchTheme: SwitchThemeData(
thumbColor: MaterialStateProperty.resolveWith<Color?>((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) { return null; }
if (states.contains(MaterialState.selected)) { return const Color.fromRGBO(102, 187, 106, 1); }
),
switchTheme: SwitchThemeData(
thumbColor:
MaterialStateProperty.resolveWith<Color?>((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return null;
}
if (states.contains(MaterialState.selected)) {
return const Color.fromRGBO(102, 187, 106, 1);
}
return null;
}),
trackColor: MaterialStateProperty.resolveWith<Color?>((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) { return null; }
if (states.contains(MaterialState.selected)) { return const Color.fromRGBO(102, 187, 106, 1); }
trackColor:
MaterialStateProperty.resolveWith<Color?>((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return null;
}
if (states.contains(MaterialState.selected)) {
return const Color.fromRGBO(102, 187, 106, 1);
}
return null;
}),
), colorScheme: const ColorScheme.light(
),
colorScheme: const ColorScheme.light(
primary: Colors.black,
secondary: Color.fromARGB(255, 163, 163, 163),
).copyWith(background: const Color.fromRGBO(255, 255, 255, 1)),
@ -105,6 +124,7 @@ final darkThemeData = ThemeData(
hintColor: const Color.fromRGBO(158, 158, 158, 1),
buttonTheme: const ButtonThemeData().copyWith(
buttonColor: const Color.fromRGBO(45, 194, 98, 1.0),
height: 56,
),
textTheme: _buildTextTheme(const Color.fromRGBO(255, 255, 255, 1)),
outlinedButtonTheme: buildOutlinedButtonThemeData(
@ -164,24 +184,43 @@ final darkThemeData = ThemeData(
return const Color.fromRGBO(158, 158, 158, 1);
}
}),
), radioTheme: RadioThemeData(
fillColor: MaterialStateProperty.resolveWith<Color?>((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) { return null; }
if (states.contains(MaterialState.selected)) { return const Color.fromRGBO(102, 187, 106, 1); }
),
radioTheme: RadioThemeData(
fillColor:
MaterialStateProperty.resolveWith<Color?>((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return null;
}
if (states.contains(MaterialState.selected)) {
return const Color.fromRGBO(102, 187, 106, 1);
}
return null;
}),
), switchTheme: SwitchThemeData(
thumbColor: MaterialStateProperty.resolveWith<Color?>((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) { return null; }
if (states.contains(MaterialState.selected)) { return const Color.fromRGBO(102, 187, 106, 1); }
),
switchTheme: SwitchThemeData(
thumbColor:
MaterialStateProperty.resolveWith<Color?>((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return null;
}
if (states.contains(MaterialState.selected)) {
return const Color.fromRGBO(102, 187, 106, 1);
}
return null;
}),
trackColor: MaterialStateProperty.resolveWith<Color?>((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) { return null; }
if (states.contains(MaterialState.selected)) { return const Color.fromRGBO(102, 187, 106, 1); }
trackColor:
MaterialStateProperty.resolveWith<Color?>((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return null;
}
if (states.contains(MaterialState.selected)) {
return const Color.fromRGBO(102, 187, 106, 1);
}
return null;
}),
), colorScheme: const ColorScheme.dark(primary: Colors.white).copyWith(background: const Color.fromRGBO(0, 0, 0, 1)),
),
colorScheme: const ColorScheme.dark(primary: Colors.white)
.copyWith(background: const Color.fromRGBO(0, 0, 0, 1)),
);
TextTheme _buildTextTheme(Color textColor) {
@ -400,6 +439,7 @@ OutlinedButtonThemeData buildOutlinedButtonThemeData({
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
fixedSize: const Size.fromHeight(56),
alignment: Alignment.center,
padding: const EdgeInsets.fromLTRB(50, 16, 50, 16),
textStyle: const TextStyle(
@ -436,7 +476,9 @@ ElevatedButtonThemeData buildElevatedButtonThemeData({
}) {
return ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
foregroundColor: onPrimary, backgroundColor: primary, elevation: elevation,
foregroundColor: onPrimary,
backgroundColor: primary,
elevation: elevation,
alignment: Alignment.center,
textStyle: const TextStyle(
fontWeight: FontWeight.w600,

View file

@ -10,12 +10,12 @@ class AuthenticatorGateway {
late String _basedEndpoint;
AuthenticatorGateway(this._dio, this._config) {
_basedEndpoint = _config.getHttpEndpoint() + "/authenticator";
_basedEndpoint = "${_config.getHttpEndpoint()}/authenticator";
}
Future<void> createKey(String encKey, String header) async {
await _dio.post(
_basedEndpoint + "/key",
"$_basedEndpoint/key",
data: {
"encryptedKey": encKey,
"header": header,
@ -31,7 +31,7 @@ class AuthenticatorGateway {
Future<AuthKey> getKey() async {
try {
final response = await _dio.get(
_basedEndpoint + "/key",
"$_basedEndpoint/key",
options: Options(
headers: {
"X-Auth-Token": _config.getToken(),
@ -39,7 +39,7 @@ class AuthenticatorGateway {
),
);
return AuthKey.fromMap(response.data);
} on DioError catch (e) {
} on DioException catch (e) {
if (e.response != null && (e.response!.statusCode ?? 0) == 404) {
throw AuthenticatorKeyNotFound();
} else {
@ -52,7 +52,7 @@ class AuthenticatorGateway {
Future<AuthEntity> createEntity(String encryptedData, String header) async {
final response = await _dio.post(
_basedEndpoint + "/entity",
"$_basedEndpoint/entity",
data: {
"encryptedData": encryptedData,
"header": header,
@ -72,7 +72,7 @@ class AuthenticatorGateway {
String header,
) async {
await _dio.put(
_basedEndpoint + "/entity",
"$_basedEndpoint/entity",
data: {
"id": id,
"encryptedData": encryptedData,
@ -90,7 +90,7 @@ class AuthenticatorGateway {
String id,
) async {
await _dio.delete(
_basedEndpoint + "/entity",
"$_basedEndpoint/entity",
queryParameters: {
"id": id,
},
@ -105,7 +105,7 @@ class AuthenticatorGateway {
Future<List<AuthEntity>> getDiff(int sinceTime, {int limit = 500}) async {
try {
final response = await _dio.get(
_basedEndpoint + "/entity/diff",
"$_basedEndpoint/entity/diff",
queryParameters: {
"sinceTime": sinceTime,
"limit": limit,
@ -124,7 +124,7 @@ class AuthenticatorGateway {
}
return authEntities;
} catch (e) {
if (e is DioError && e.response?.statusCode == 401) {
if (e is DioException && e.response?.statusCode == 401) {
throw UnauthorizedError();
} else {
rethrow;

View file

@ -59,7 +59,7 @@
}
},
"contactSupport": "Contact support",
"rateUsOnStore" : "Rate us on {storeName}",
"rateUsOnStore": "Rate us on {storeName}",
"blog": "Blog",
"merchandise": "Merchandise",
"verifyPassword": "Verify password",
@ -133,7 +133,6 @@
"faq_q_5": "How can I enable FaceID lock in ente Auth",
"faq_a_5": "You can enable FaceID lock under Settings → Security → Lockscreen.",
"somethingWentWrongMessage": "Something went wrong, please try again",
"leaveFamily": "Leave family",
"leaveFamilyMessage": "Are you sure that you want to leave the family plan?",
"inFamilyPlanMessage": "You are on a family plan!",
@ -198,6 +197,10 @@
"recoveryKeySaveDescription": "We don't store this key, please save this 24 word key in a safe place.",
"doThisLater": "Do this later",
"saveKey": "Save key",
"save": "Save",
"send": "Send",
"saveOrSendDescription": "Do you want to save this to your storage (Downloads folder by default) or send it to other apps?",
"saveOnlyDescription": "Do you want to save this to your storage (Downloads folder by default)?",
"back": "Back",
"createAccount": "Create account",
"passwordStrength": "Password strength: {passwordStrengthValue}",
@ -337,7 +340,7 @@
"offlineModeWarning": "You have chosen to proceed without backups. Please take manual backups to make sure your codes are safe.",
"showLargeIcons": "Show large icons",
"shouldHideCode": "Hide codes",
"doubleTapToViewHiddenCode" : "You can double tap on an entry to view code",
"doubleTapToViewHiddenCode": "You can double tap on an entry to view code",
"focusOnSearchBar": "Focus search on app start",
"confirmUpdatingkey": "Are you sure you want to update the secret key?",
"minimizeAppOnCopy": "Minimize app on copy",
@ -405,5 +408,6 @@
"signOutOtherDevices": "Sign out other devices",
"doNotSignOut": "Do not sign out",
"hearUsWhereTitle": "How did you hear about Ente? (optional)",
"hearUsExplanation": "We don't track app installs. It'd help if you told us where you found us!"
"hearUsExplanation": "We don't track app installs. It'd help if you told us where you found us!",
"recoveryKeySaved": "Recovery key saved in Downloads folder!"
}

View file

@ -1,5 +1,6 @@
import 'dart:io';
import 'package:adaptive_theme/adaptive_theme.dart';
import 'package:computer/computer.dart';
import "package:ente_auth/app/view/app.dart";
import 'package:ente_auth/core/configuration.dart';
import 'package:ente_auth/core/constants.dart';
@ -18,7 +19,9 @@ import 'package:ente_auth/store/code_store.dart';
import 'package:ente_auth/ui/tools/app_lock.dart';
import 'package:ente_auth/ui/tools/lock_screen.dart';
import 'package:ente_auth/ui/utils/icon_utils.dart';
import 'package:ente_auth/utils/crypto_util.dart';
import 'package:ente_auth/utils/platform_util.dart';
import 'package:ente_auth/utils/window_protocol_handler.dart';
import 'package:ente_crypto_dart/ente_crypto_dart.dart';
import 'package:flutter/foundation.dart';
import "package:flutter/material.dart";
import 'package:flutter/scheduler.dart';
@ -26,14 +29,27 @@ import 'package:flutter_displaymode/flutter_displaymode.dart';
import 'package:logging/logging.dart';
import 'package:path_provider/path_provider.dart';
import 'package:privacy_screen/privacy_screen.dart';
import 'package:window_manager/window_manager.dart';
final _logger = Logger("main");
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await windowManager.ensureInitialized();
if (PlatformUtil.isDesktop()) {
WindowOptions windowOptions = const WindowOptions(
size: Size(450, 800),
);
windowManager.waitUntilReadyToShow(windowOptions, () async {
await windowManager.show();
await windowManager.focus();
});
}
await _runInForeground();
await _setupPrivacyScreen();
FlutterDisplayMode.setHighRefreshRate();
if (Platform.isAndroid) FlutterDisplayMode.setHighRefreshRate();
}
Future<void> _runInForeground() async {
@ -65,10 +81,14 @@ ThemeMode _themeMode(AdaptiveThemeMode? savedThemeMode) {
}
Future _runWithLogs(Function() function, {String prefix = ""}) async {
String dir = "";
try {
dir = "${(await getApplicationSupportDirectory()).path}/logs";
} catch (_) {}
await SuperLogging.main(
LogConfig(
body: function,
logDirPath: (await getApplicationSupportDirectory()).path + "/logs",
logDirPath: dir,
maxLogFiles: 5,
sentryDsn: sentryDSN,
enableInDebugMode: true,
@ -77,10 +97,19 @@ Future _runWithLogs(Function() function, {String prefix = ""}) async {
);
}
void _registerWindowsProtocol() {
const kWindowsScheme = 'ente';
// Register our protocol only on Windows platform
if (!kIsWeb && Platform.isWindows) {
WindowsProtocolHandler()
.register(kWindowsScheme, executable: null, arguments: null);
}
}
Future<void> _init(bool bool, {String? via}) async {
// Start workers asynchronously. No need to wait for them to start
Computer.shared().turnOn(workersCount: 4, verbose: kDebugMode);
CryptoUtil.init();
_registerWindowsProtocol();
await initCryptoUtil();
await PreferenceService.instance.init();
await CodeStore.instance.init();
await Configuration.instance.init();
@ -95,6 +124,7 @@ Future<void> _init(bool bool, {String? via}) async {
}
Future<void> _setupPrivacyScreen() async {
if (!PlatformUtil.isMobile()) return;
final brightness =
SchedulerBinding.instance.platformDispatcher.platformBrightness;
bool isInDarkMode = brightness == Brightness.dark;

View file

@ -57,14 +57,7 @@ class Code {
updatedAlgo,
updatedType,
updatedCounter,
"otpauth://${updatedType.name}/" +
updateIssuer +
":" +
updateAccount +
"?algorithm=${updatedAlgo.name}&digits=$updatedDigits&issuer=" +
updateIssuer +
"&period=$updatePeriod&secret=" +
updatedSecret + (updatedType == Type.hotp ? "&counter=$updatedCounter" : ""),
"otpauth://${updatedType.name}/$updateIssuer:$updateAccount?algorithm=${updatedAlgo.name}&digits=$updatedDigits&issuer=$updateIssuer&period=$updatePeriod&secret=$updatedSecret${updatedType == Type.hotp ? "&counter=$updatedCounter" : ""}",
generatedID: generatedID,
);
}
@ -83,14 +76,7 @@ class Code {
Algorithm.sha1,
Type.totp,
0,
"otpauth://totp/" +
issuer +
":" +
account +
"?algorithm=SHA1&digits=6&issuer=" +
issuer +
"&period=30&secret=" +
secret,
"otpauth://totp/$issuer:$account?algorithm=SHA1&digits=6&issuer=$issuer&period=30&secret=$secret",
);
}

View file

@ -1,9 +0,0 @@
import 'dart:typed_data';
class DerivedKeyResult {
final Uint8List key;
final int memLimit;
final int opsLimit;
DerivedKeyResult(this.key, this.memLimit, this.opsLimit);
}

View file

@ -1,15 +0,0 @@
import 'dart:typed_data';
class EncryptionResult {
final Uint8List? encryptedData;
final Uint8List? key;
final Uint8List? header;
final Uint8List? nonce;
EncryptionResult({
this.encryptedData,
this.key,
this.header,
this.nonce,
});
}

View file

@ -7,26 +7,18 @@ 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';
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';
import "package:flutter/material.dart";
import 'package:local_auth/local_auth.dart';
class OnboardingPage extends StatefulWidget {
const OnboardingPage({Key? key}) : super(key: key);
const OnboardingPage({super.key});
@override
State<OnboardingPage> createState() => _OnboardingPageState();
@ -56,11 +48,16 @@ class _OnboardingPageState extends State<OnboardingPage> {
final l10n = context.l10n;
return Scaffold(
body: SafeArea(
child: Center(
child: SingleChildScrollView(
child: Center(
child: ConstrainedBox(
constraints:
const BoxConstraints.tightFor(height: 800, width: 450),
child: Padding(
padding:
const EdgeInsets.symmetric(vertical: 40.0, horizontal: 40),
padding: const EdgeInsets.symmetric(
vertical: 40.0,
horizontal: 40,
),
child: Column(
children: [
Column(
@ -112,25 +109,31 @@ class _OnboardingPageState extends State<OnboardingPage> {
Text(
l10n.onBoardingBody,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.titleLarge!.copyWith(
style:
Theme.of(context).textTheme.titleLarge!.copyWith(
color: Colors.white38,
// color: Theme.of(context)
// .colorScheme
// .mutedTextColor,
),
),
],
),
const SizedBox(height: 100),
// TODO: Remove After Stable
// Container(
// width: double.infinity,
// padding: const EdgeInsets.symmetric(horizontal: 20),
// child: GradientButton(
// onTap: _navigateToSignUpPage,
// text: l10n.newUser,
// ),
// ),
// const SizedBox(height: 24),
Container(
height: 56,
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 20),
child: GradientButton(
onTap: _navigateToSignUpPage,
text: l10n.newUser,
),
),
const SizedBox(height: 4),
Container(
width: double.infinity,
padding: const EdgeInsets.fromLTRB(20, 12, 20, 0),
padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
child: Hero(
tag: "log_in",
child: ElevatedButton(
@ -148,89 +151,96 @@ class _OnboardingPageState extends State<OnboardingPage> {
),
),
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,
),
),
),
),
),
// TODO: Remove After Stable
// 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,
// ),
// ),
// ),
// ),
// ),
],
),
),
),
),
),
);
}
Future<void> _optForOfflineMode() async {
bool canCheckBio = await LocalAuthentication().canCheckBiometrics;
if (!canCheckBio) {
showToast(
context,
"Sorry, biometric authentication is not supported on this device.",
);
return;
}
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() {
Widget page;
if (Configuration.instance.getEncryptedToken() == null) {
page = const EmailEntryPage();
} else {
// No key
if (Configuration.instance.getKeyAttributes() == null) {
// Never had a key
page = const PasswordEntryPage(
mode: PasswordEntryMode.set,
);
} else if (Configuration.instance.getKey() == null) {
// Yet to decrypt the key
page = const PasswordReentryPage();
} else {
// All is well, user just has not subscribed
page = const HomePage();
}
}
Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {
return page;
},
),
);
}
// TODO: Remove After Stable
// Future<void> _optForOfflineMode() async {
// final canContinue = Platform.isMacOS || Platform.isLinux
// ? true
// : await LocalAuthentication().canCheckBiometrics;
// if (!canContinue) {
// showToast(
// context,
// "Sorry, biometric authentication is not supported on this device.",
// );
// return;
// }
// 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() {
// Widget page;
// if (Configuration.instance.getEncryptedToken() == null) {
// page = const EmailEntryPage();
// } else {
// // No key
// if (Configuration.instance.getKeyAttributes() == null) {
// // Never had a key
// page = const PasswordEntryPage(
// mode: PasswordEntryMode.set,
// );
// } else if (Configuration.instance.getKey() == null) {
// // Yet to decrypt the key
// page = const PasswordReentryPage();
// } else {
// // All is well, user just has not subscribed
// page = const HomePage();
// }
// }
// Navigator.of(context).push(
// MaterialPageRoute(
// builder: (BuildContext context) {
// return page;
// },
// ),
// );
// }
void _navigateToSignInPage() {
Widget page;

View file

@ -9,7 +9,7 @@ import "package:flutter/material.dart";
class SetupEnterSecretKeyPage extends StatefulWidget {
final Code? code;
SetupEnterSecretKeyPage({this.code, Key? key}) : super(key: key);
SetupEnterSecretKeyPage({this.code, super.key});
@override
State<SetupEnterSecretKeyPage> createState() =>
@ -32,7 +32,7 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
widget.code != null ? safeDecode(widget.code!.account).trim() : null,
);
_secretController = TextEditingController(
text: widget.code != null ? widget.code!.secret : null,
text: widget.code?.secret,
);
_secretKeyObscured = widget.code != null;
super.initState();
@ -45,8 +45,8 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
appBar: AppBar(
title: Text(l10n.importAccountPageTitle),
),
body: SafeArea(
child: Center(
body: Center(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 40.0, horizontal: 40),
child: Column(

View file

@ -1,4 +1,3 @@
import 'dart:math';
import "package:ente_auth/l10n/l10n.dart";
@ -10,7 +9,7 @@ import 'package:qr_flutter/qr_flutter.dart';
class ViewQrPage extends StatelessWidget {
final Code? code;
ViewQrPage({this.code, Key? key}) : super(key: key);
ViewQrPage({this.code, super.key});
@override
Widget build(BuildContext context) {
@ -22,15 +21,22 @@ class ViewQrPage extends StatelessWidget {
appBar: AppBar(
title: Text(l10n.qrCode),
),
body: SafeArea(
child: Center(
body: Center(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 40.0, horizontal: 40),
child: Column(
children: [
QrImage(
QrImageView(
data: code!.rawData,
foregroundColor: Theme.of(context).colorScheme.onBackground,
eyeStyle: QrEyeStyle(
eyeShape: QrEyeShape.square,
color: Theme.of(context).colorScheme.onBackground,
),
dataModuleStyle: QrDataModuleStyle(
dataModuleShape: QrDataModuleShape.square,
color: Theme.of(context).colorScheme.onBackground,
),
version: QrVersions.auto,
size: qrSize,
),

View file

@ -16,9 +16,8 @@ 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:ente_crypto_dart/ente_crypto_dart.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';
@ -26,6 +25,7 @@ enum AccountMode {
online,
offline,
}
extension on AccountMode {
bool get isOnline => this == AccountMode.online;
bool get isOffline => this == AccountMode.offline;
@ -75,10 +75,10 @@ class AuthenticatorService {
final key = await getOrCreateAuthDataKey(mode);
for (LocalAuthEntity e in result) {
try {
final decryptedValue = await CryptoUtil.decryptChaCha(
Sodium.base642bin(e.encryptedData),
final decryptedValue = await CryptoUtil.decryptData(
CryptoUtil.base642bin(e.encryptedData),
key,
Sodium.base642bin(e.header),
CryptoUtil.base642bin(e.header),
);
final hasSynced = !(e.id == null || e.shouldSync);
entities.add(
@ -101,12 +101,13 @@ class AuthenticatorService {
AccountMode accountMode,
) async {
var key = await getOrCreateAuthDataKey(accountMode);
final encryptedKeyData = await CryptoUtil.encryptChaCha(
utf8.encode(plainText) as Uint8List,
final encryptedKeyData = await CryptoUtil.encryptData(
utf8.encode(plainText),
key,
);
String encryptedData = Sodium.bin2base64(encryptedKeyData.encryptedData!);
String header = Sodium.bin2base64(encryptedKeyData.header!);
String encryptedData =
CryptoUtil.bin2base64(encryptedKeyData.encryptedData!);
String header = CryptoUtil.bin2base64(encryptedKeyData.header!);
final insertedID = accountMode.isOnline
? await _db.insert(encryptedData, header)
: await _offlineDb.insert(encryptedData, header);
@ -123,12 +124,13 @@ class AuthenticatorService {
AccountMode accountMode,
) async {
var key = await getOrCreateAuthDataKey(accountMode);
final encryptedKeyData = await CryptoUtil.encryptChaCha(
utf8.encode(plainText) as Uint8List,
final encryptedKeyData = await CryptoUtil.encryptData(
utf8.encode(plainText),
key,
);
String encryptedData = Sodium.bin2base64(encryptedKeyData.encryptedData!);
String header = Sodium.bin2base64(encryptedKeyData.header!);
String encryptedData =
CryptoUtil.bin2base64(encryptedKeyData.encryptedData!);
String header = CryptoUtil.bin2base64(encryptedKeyData.header!);
final int affectedRows = accountMode.isOnline
? await _db.updateEntry(generatedID, encryptedData, header)
: await _offlineDb.updateEntry(generatedID, encryptedData, header);
@ -154,7 +156,7 @@ class AuthenticatorService {
} else {
debugPrint("Skipping delete since account mode is offline");
}
if(accountMode.isOnline) {
if (accountMode.isOnline) {
await _db.deleteByIDs(generatedIDs: [genID]);
} else {
await _offlineDb.deleteByIDs(generatedIDs: [genID]);
@ -163,7 +165,7 @@ class AuthenticatorService {
Future<bool> onlineSync() async {
try {
if(getAccountMode().isOffline) {
if (getAccountMode().isOffline) {
debugPrint("Skipping sync since account mode is offline");
return false;
}
@ -191,25 +193,25 @@ class AuthenticatorService {
Future<void> _remoteToLocalSync() async {
_logger.info('Initiating remote to local sync');
final int lastSyncTime = _prefs.getInt(_lastEntitySyncTime) ?? 0;
_logger.info("Current sync is " + lastSyncTime.toString());
_logger.info("Current sync is $lastSyncTime");
const int fetchLimit = 500;
final List<AuthEntity> result =
await _gateway.getDiff(lastSyncTime, limit: fetchLimit);
_logger.info(result.length.toString() + " entries fetched from remote");
_logger.info("${result.length} entries fetched from remote");
if (result.isEmpty) {
return;
}
final maxSyncTime = result.map((e) => e.updatedAt).reduce(max);
List<String> deletedIDs =
result.where((element) => element.isDeleted).map((e) => e.id).toList();
_logger.info(deletedIDs.length.toString() + " entries deleted");
_logger.info("${deletedIDs.length} entries deleted");
result.removeWhere((element) => element.isDeleted);
await _db.insertOrReplace(result);
if (deletedIDs.isNotEmpty) {
await _db.deleteByIDs(ids: deletedIDs);
}
_prefs.setInt(_lastEntitySyncTime, maxSyncTime);
_logger.info("Setting synctime to " + maxSyncTime.toString());
_logger.info("Setting synctime to $maxSyncTime");
if (result.length == fetchLimit) {
_logger.info("Diff limit reached, pulling again");
await _remoteToLocalSync();
@ -223,7 +225,7 @@ class AuthenticatorService {
.where((element) => element.shouldSync || element.id == null)
.toList();
_logger.info(
pendingUpdate.length.toString() + " entries to be updated at remote",
"${pendingUpdate.length} entries to be updated at remote",
);
for (LocalAuthEntity entity in pendingUpdate) {
if (entity.id == null) {
@ -253,7 +255,7 @@ class AuthenticatorService {
}
Future<Uint8List> getOrCreateAuthDataKey(AccountMode mode) async {
if(mode.isOffline) {
if (mode.isOffline) {
return _config.getOfflineSecretKey()!;
}
if (_config.getAuthSecretKey() != null) {
@ -262,21 +264,21 @@ class AuthenticatorService {
try {
final AuthKey response = await _gateway.getKey();
final authKey = CryptoUtil.decryptSync(
Sodium.base642bin(response.encryptedKey),
CryptoUtil.base642bin(response.encryptedKey),
_config.getKey()!,
Sodium.base642bin(response.header),
CryptoUtil.base642bin(response.header),
);
await _config.setAuthSecretKey(Sodium.bin2base64(authKey));
await _config.setAuthSecretKey(CryptoUtil.bin2base64(authKey));
return authKey;
} on AuthenticatorKeyNotFound catch (e) {
_logger.info("AuthenticatorKeyNotFound generating key ${e.stackTrace}");
final key = CryptoUtil.generateKey();
final encryptedKeyData = CryptoUtil.encryptSync(key, _config.getKey()!);
await _gateway.createKey(
Sodium.bin2base64(encryptedKeyData.encryptedData!),
Sodium.bin2base64(encryptedKeyData.nonce!),
CryptoUtil.bin2base64(encryptedKeyData.encryptedData!),
CryptoUtil.bin2base64(encryptedKeyData.nonce!),
);
await _config.setAuthSecretKey(Sodium.bin2base64(key));
await _config.setAuthSecretKey(CryptoUtil.bin2base64(key));
return key;
} catch (e, s) {
_logger.severe("Failed to getOrCreateAuthDataKey", e, s);

View file

@ -28,6 +28,8 @@ class BillingService {
final _dio = Network.instance.getDio();
final _config = Configuration.instance;
bool _isOnSubscriptionPage = false;
Subscription? _cachedSubscription;
Future<BillingPlans>? _future;
@ -50,7 +52,7 @@ class BillingService {
Future<Response<dynamic>> _fetchPrivateBillingPlans() {
return _dio.get(
_config.getHttpEndpoint() + "/billing/user-plans/",
"${_config.getHttpEndpoint()}/billing/user-plans/",
options: Options(
headers: {
"X-Auth-Token": _config.getToken(),
@ -60,7 +62,7 @@ class BillingService {
}
Future<Response<dynamic>> _fetchPublicBillingPlans() {
return _dio.get(_config.getHttpEndpoint() + "/billing/plans/v2");
return _dio.get("${_config.getHttpEndpoint()}/billing/plans/v2");
}
Future<Subscription> verifySubscription(
@ -70,7 +72,7 @@ class BillingService {
}) async {
try {
final response = await _dio.post(
_config.getHttpEndpoint() + "/billing/verify-subscription",
"${_config.getHttpEndpoint()}/billing/verify-subscription",
data: {
"paymentProvider": paymentProvider ??
(Platform.isAndroid ? "playstore" : "appstore"),
@ -84,7 +86,7 @@ class BillingService {
),
);
return Subscription.fromMap(response.data["subscription"]);
} on DioError catch (e) {
} on DioException catch (e) {
if (e.response != null && e.response!.statusCode == 409) {
throw SubscriptionAlreadyClaimedError();
} else {
@ -100,7 +102,7 @@ class BillingService {
if (_cachedSubscription == null) {
try {
final response = await _dio.get(
_config.getHttpEndpoint() + "/billing/subscription",
"${_config.getHttpEndpoint()}/billing/subscription",
options: Options(
headers: {
"X-Auth-Token": _config.getToken(),
@ -109,7 +111,7 @@ class BillingService {
);
_cachedSubscription =
Subscription.fromMap(response.data["subscription"]);
} on DioError catch (e, s) {
} on DioException catch (e, s) {
_logger.severe(e, s);
rethrow;
}
@ -120,7 +122,7 @@ class BillingService {
Future<Subscription> cancelStripeSubscription() async {
try {
final response = await _dio.post(
_config.getHttpEndpoint() + "/billing/stripe/cancel-subscription",
"${_config.getHttpEndpoint()}/billing/stripe/cancel-subscription",
options: Options(
headers: {
"X-Auth-Token": _config.getToken(),
@ -129,7 +131,7 @@ class BillingService {
);
final subscription = Subscription.fromMap(response.data["subscription"]);
return subscription;
} on DioError catch (e, s) {
} on DioException catch (e, s) {
_logger.severe(e, s);
rethrow;
}
@ -138,7 +140,7 @@ class BillingService {
Future<Subscription> activateStripeSubscription() async {
try {
final response = await _dio.post(
_config.getHttpEndpoint() + "/billing/stripe/activate-subscription",
"${_config.getHttpEndpoint()}/billing/stripe/activate-subscription",
options: Options(
headers: {
"X-Auth-Token": _config.getToken(),
@ -147,7 +149,7 @@ class BillingService {
);
final subscription = Subscription.fromMap(response.data["subscription"]);
return subscription;
} on DioError catch (e, s) {
} on DioException catch (e, s) {
_logger.severe(e, s);
rethrow;
}
@ -158,7 +160,7 @@ class BillingService {
}) async {
try {
final response = await _dio.get(
_config.getHttpEndpoint() + "/billing/stripe/customer-portal",
"${_config.getHttpEndpoint()}/billing/stripe/customer-portal",
queryParameters: {
"redirectURL": kWebPaymentRedirectUrl,
},
@ -169,9 +171,13 @@ class BillingService {
),
);
return response.data["url"];
} on DioError catch (e, s) {
} on DioException catch (e, s) {
_logger.severe(e, s);
rethrow;
}
}
void setIsOnSubscriptionPage(bool isOnSubscriptionPage) {
_isOnSubscriptionPage = isOnSubscriptionPage;
}
}

View file

@ -1,15 +1,21 @@
import 'dart:io';
import 'package:ente_auth/core/configuration.dart';
import 'package:ente_auth/ui/tools/app_lock.dart';
import 'package:ente_auth/utils/auth_util.dart';
import 'package:ente_auth/utils/dialog_util.dart';
import 'package:ente_auth/utils/toast_util.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_local_authentication/flutter_local_authentication.dart';
import 'package:local_auth/local_auth.dart';
import 'package:logging/logging.dart';
class LocalAuthenticationService {
LocalAuthenticationService._privateConstructor();
static final LocalAuthenticationService instance =
LocalAuthenticationService._privateConstructor();
final logger = Logger((LocalAuthenticationService).toString());
Future<bool> requestLocalAuthentication(
BuildContext context,
@ -38,7 +44,7 @@ class LocalAuthenticationService {
String errorDialogContent, [
String errorDialogTitle = "",
]) async {
if (await LocalAuthentication().isDeviceSupported()) {
if (await _isLocalAuthSupportedOnDevice()) {
AppLock.of(context)!.disable();
final result = await requestAuthentication(
context,
@ -64,6 +70,12 @@ class LocalAuthenticationService {
}
Future<bool> _isLocalAuthSupportedOnDevice() async {
return await LocalAuthentication().isDeviceSupported();
try {
return Platform.isMacOS || Platform.isLinux
? await FlutterLocalAuthentication().canAuthenticate()
: await LocalAuthentication().isDeviceSupported();
} on MissingPluginException {
return false;
}
}
}

View file

@ -27,7 +27,7 @@ class NotificationService {
_flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>();
if (implementation != null) {
implementation.requestPermission();
implementation.requestNotificationsPermission();
}
}

View file

@ -96,7 +96,7 @@ class UserRemoteFlagService {
queryParams["defaultValue"] = defaultValue;
}
final response = await _dio.get(
_config.getHttpEndpoint() + "/remote-store",
"${_config.getHttpEndpoint()}/remote-store",
queryParameters: queryParams,
options: Options(
headers: {
@ -119,7 +119,7 @@ class UserRemoteFlagService {
Future<void> _updateKeyValue(String key, String value) async {
try {
final response = await _dio.post(
_config.getHttpEndpoint() + "/remote-store/update",
"${_config.getHttpEndpoint()}/remote-store/update",
data: {
"key": key,
"value": value,

View file

@ -28,9 +28,9 @@ import 'package:ente_auth/ui/common/progress_dialog.dart';
import 'package:ente_auth/ui/home_page.dart';
import 'package:ente_auth/ui/two_factor_authentication_page.dart';
import 'package:ente_auth/ui/two_factor_recovery_page.dart';
import 'package:ente_auth/utils/crypto_util.dart';
import 'package:ente_auth/utils/dialog_util.dart';
import 'package:ente_auth/utils/toast_util.dart';
import 'package:ente_crypto_dart/ente_crypto_dart.dart';
import "package:flutter/foundation.dart";
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
@ -78,7 +78,7 @@ class UserService {
await dialog.show();
try {
final response = await _dio.post(
_config.getHttpEndpoint() + "/users/ott",
"${_config.getHttpEndpoint()}/users/ott",
data: {"email": email, "purpose": isChangeEmail ? "change" : ""},
);
await dialog.hide();
@ -100,7 +100,7 @@ class UserService {
return;
}
unawaited(showGenericErrorDialog(context: context));
} on DioError catch (e) {
} on DioException catch (e) {
await dialog.hide();
_logger.info(e);
if (e.response != null && e.response!.statusCode == 403) {
@ -127,7 +127,7 @@ class UserService {
String type = "SubCancellation",
}) async {
await _dio.post(
_config.getHttpEndpoint() + "/anonymous/feedback",
"${_config.getHttpEndpoint()}/anonymous/feedback",
data: {"feedback": feedback, "type": "type"},
);
}
@ -171,7 +171,7 @@ class UserService {
try {
final response = await _enteDio.get("/users/sessions");
return Sessions.fromMap(response.data);
} on DioError catch (e) {
} on DioException catch (e) {
_logger.info(e);
rethrow;
}
@ -185,7 +185,7 @@ class UserService {
"token": token,
},
);
} on DioError catch (e) {
} on DioException catch (e) {
_logger.info(e);
rethrow;
}
@ -194,7 +194,7 @@ class UserService {
Future<void> leaveFamilyPlan() async {
try {
await _enteDio.delete("/family/leave");
} on DioError catch (e) {
} on DioException catch (e) {
_logger.warning('failed to leave family plan', e);
rethrow;
}
@ -276,11 +276,11 @@ class UserService {
"ott": ott,
};
if (!_config.isLoggedIn()) {
verifyData["source"] = 'auth:' + _getRefSource();
verifyData["source"] = 'auth:${_getRefSource()}';
}
try {
final response = await _dio.post(
_config.getHttpEndpoint() + "/users/verify-email",
"${_config.getHttpEndpoint()}/users/verify-email",
data: verifyData,
);
await dialog.hide();
@ -315,7 +315,7 @@ class UserService {
// should never reach here
throw Exception("unexpected response during email verification");
}
} on DioError catch (e) {
} on DioException catch (e) {
_logger.info(e);
await dialog.hide();
if (e.response != null && e.response!.statusCode == 410) {
@ -376,7 +376,7 @@ class UserService {
context.l10n.oops,
context.l10n.verificationFailedPleaseTryAgain,
);
} on DioError catch (e) {
} on DioException catch (e) {
await dialog.hide();
if (e.response != null && e.response!.statusCode == 403) {
showErrorDialog(
@ -423,7 +423,7 @@ class UserService {
Future<SrpAttributes> getSrpAttributes(String email) async {
try {
final response = await _dio.get(
_config.getHttpEndpoint() + "/users/srp/attributes",
"${_config.getHttpEndpoint()}/users/srp/attributes",
queryParameters: {
"email": email,
},
@ -433,7 +433,7 @@ class UserService {
} else {
throw Exception("get-srp-attributes action failed");
}
} on DioError catch (e) {
} on DioException catch (e) {
if (e.response != null && e.response!.statusCode == 404) {
throw SrpSetupNotCompleteError();
}
@ -486,10 +486,9 @@ class UserService {
// ignore: need to calculate secret to get M1, unused_local_variable
final clientS = client.calculateSecret(serverB);
final clientM = client.calculateClientEvidenceMessage();
// ignore: unused_local_variable
late Response srpCompleteResponse;
late Response _;
if (setKeysRequest == null) {
srpCompleteResponse = await _enteDio.post(
_ = await _enteDio.post(
"/users/srp/complete",
data: {
'setupID': setupSRPResponse.setupID,
@ -497,7 +496,7 @@ class UserService {
},
);
} else {
srpCompleteResponse = await _enteDio.post(
_ = await _enteDio.post(
"/users/srp/update",
data: {
'setupID': setupSRPResponse.setupID,
@ -536,7 +535,7 @@ class UserService {
late Uint8List keyEncryptionKey;
_logger.finest('Start deriving key');
keyEncryptionKey = await CryptoUtil.deriveKey(
utf8.encode(userPassword) as Uint8List,
utf8.encode(userPassword),
CryptoUtil.base642bin(srpAttributes.kekSalt),
srpAttributes.memLimit,
srpAttributes.opsLimit,
@ -559,7 +558,7 @@ class UserService {
final A = client.generateClientCredentials(salt, identity, password);
final createSessionResponse = await _dio.post(
_config.getHttpEndpoint() + "/users/srp/create-session",
"${_config.getHttpEndpoint()}/users/srp/create-session",
data: {
"srpUserID": srpAttributes.srpUserID,
"srpA": base64Encode(SRP6Util.encodeBigInt(A!)),
@ -573,7 +572,7 @@ class UserService {
final clientS = client.calculateSecret(serverB);
final clientM = client.calculateClientEvidenceMessage();
final response = await _dio.post(
_config.getHttpEndpoint() + "/users/srp/verify-session",
"${_config.getHttpEndpoint()}/users/srp/verify-session",
data: {
"sessionID": sessionID,
"srpUserID": srpAttributes.srpUserID,
@ -667,7 +666,7 @@ class UserService {
await dialog.show();
try {
final response = await _dio.post(
_config.getHttpEndpoint() + "/users/two-factor/verify",
"${_config.getHttpEndpoint()}/users/two-factor/verify",
data: {
"sessionID": sessionID,
"code": code,
@ -686,7 +685,7 @@ class UserService {
(route) => route.isFirst,
);
}
} on DioError catch (e) {
} on DioException catch (e) {
await dialog.hide();
_logger.severe(e);
if (e.response != null && e.response!.statusCode == 404) {
@ -722,7 +721,7 @@ class UserService {
await dialog.show();
try {
final response = await _dio.get(
_config.getHttpEndpoint() + "/users/two-factor/recover",
"${_config.getHttpEndpoint()}/users/two-factor/recover",
queryParameters: {
"sessionID": sessionID,
},
@ -741,7 +740,7 @@ class UserService {
(route) => route.isFirst,
);
}
} on DioError catch (e) {
} on DioException catch (e) {
_logger.severe(e);
if (e.response != null && e.response!.statusCode == 404) {
showToast(context, context.l10n.sessionExpired);
@ -809,7 +808,7 @@ class UserService {
}
try {
final response = await _dio.post(
_config.getHttpEndpoint() + "/users/two-factor/remove",
"${_config.getHttpEndpoint()}/users/two-factor/remove",
data: {
"sessionID": sessionID,
"secret": secret,
@ -830,7 +829,7 @@ class UserService {
(route) => route.isFirst,
);
}
} on DioError catch (e) {
} on DioException catch (e) {
_logger.severe(e);
if (e.response != null && e.response!.statusCode == 404) {
showToast(context, "Session expired");

View file

@ -3,10 +3,12 @@ import 'dart:io';
import 'package:ente_auth/models/authenticator/auth_entity.dart';
import 'package:ente_auth/models/authenticator/local_auth_entity.dart';
import 'package:ente_auth/utils/directory_utils.dart';
import 'package:flutter/foundation.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:sqflite/sqflite.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
class AuthenticatorDB {
static const _databaseName = "ente.authenticator.db";
@ -25,6 +27,16 @@ class AuthenticatorDB {
}
Future<Database> _initDatabase() async {
if (Platform.isWindows || Platform.isLinux) {
var databaseFactory = databaseFactoryFfi;
return await databaseFactory.openDatabase(
await DirectoryUtils.getDatabasePath(_databaseName),
options: OpenDatabaseOptions(
version: _databaseVersion,
onCreate: _onCreate,
),
);
}
final Directory documentsDirectory =
await getApplicationDocumentsDirectory();
final String path = join(documentsDirectory.path, _databaseName);
@ -166,7 +178,7 @@ class AuthenticatorDB {
batch.delete(entityTable, where: whereID, whereArgs: [id]);
}
}
await batch.commit();
final _ = await batch.commit();
debugPrint("Done");
}

View file

@ -3,10 +3,11 @@ import 'dart:io';
import 'package:ente_auth/models/authenticator/auth_entity.dart';
import 'package:ente_auth/models/authenticator/local_auth_entity.dart';
import 'package:ente_auth/utils/directory_utils.dart';
import 'package:flutter/foundation.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:sqflite/sqflite.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
class OfflineAuthenticatorDB {
static const _databaseName = "ente.offline_authenticator.db";
@ -15,7 +16,8 @@ class OfflineAuthenticatorDB {
static const entityTable = 'entities';
OfflineAuthenticatorDB._privateConstructor();
static final OfflineAuthenticatorDB instance = OfflineAuthenticatorDB._privateConstructor();
static final OfflineAuthenticatorDB instance =
OfflineAuthenticatorDB._privateConstructor();
static Future<Database>? _dbFuture;
@ -25,6 +27,16 @@ class OfflineAuthenticatorDB {
}
Future<Database> _initDatabase() async {
if (Platform.isWindows || Platform.isLinux) {
var databaseFactory = databaseFactoryFfi;
return await databaseFactory.openDatabase(
await DirectoryUtils.getDatabasePath(_databaseName),
options: OpenDatabaseOptions(
version: _databaseVersion,
onCreate: _onCreate,
),
);
}
final Directory documentsDirectory =
await getApplicationDocumentsDirectory();
final String path = join(documentsDirectory.path, _databaseName);
@ -151,7 +163,7 @@ class OfflineAuthenticatorDB {
batch.delete(entityTable, where: whereID, whereArgs: [id]);
}
}
await batch.commit();
final _ = await batch.commit();
debugPrint("Done");
}

View file

@ -3,7 +3,6 @@ import 'package:shared_preferences/shared_preferences.dart';
class UserStore {
UserStore._privateConstructor();
// ignore: unused_field
late SharedPreferences _preferences;
static final UserStore instance = UserStore._privateConstructor();

View file

@ -204,6 +204,7 @@ const Color _warning700 = Color.fromRGBO(234, 63, 63, 1);
const Color _warning500 = Color.fromRGBO(255, 101, 101, 1);
const Color _warning800 = Color(0xFFF53434);
const Color warning500 = Color.fromRGBO(255, 101, 101, 1);
// ignore: unused_element
const Color _warning400 = Color.fromRGBO(255, 111, 111, 1);
const Color _caution500 = Color.fromRGBO(255, 194, 71, 1);

View file

@ -5,7 +5,7 @@ import 'package:ente_auth/utils/email_util.dart';
import 'package:flutter/material.dart';
class ChangeEmailDialog extends StatefulWidget {
const ChangeEmailDialog({Key? key}) : super(key: key);
const ChangeEmailDialog({super.key});
@override
State<ChangeEmailDialog> createState() => _ChangeEmailDialogState();

View file

@ -7,15 +7,15 @@ import 'package:ente_auth/services/local_authentication_service.dart';
import 'package:ente_auth/services/user_service.dart';
import 'package:ente_auth/ui/common/dialogs.dart';
import 'package:ente_auth/ui/common/gradient_button.dart';
import 'package:ente_auth/utils/crypto_util.dart';
import 'package:ente_auth/utils/email_util.dart';
import 'package:ente_auth/utils/platform_util.dart';
import 'package:ente_crypto_dart/ente_crypto_dart.dart';
import 'package:flutter/material.dart';
import 'package:flutter_sodium/flutter_sodium.dart';
class DeleteAccountPage extends StatelessWidget {
const DeleteAccountPage({
Key? key,
}) : super(key: key);
super.key,
});
@override
Widget build(BuildContext context) {
@ -150,6 +150,8 @@ class DeleteAccountPage extends StatelessWidget {
l10n.initiateAccountDeleteTitle,
);
await PlatformUtil.refocusWindows();
if (hasAuthenticated) {
final choice = await showChoiceDialogOld(
context,
@ -164,8 +166,10 @@ class DeleteAccountPage extends StatelessWidget {
return;
}
final decryptChallenge = CryptoUtil.openSealSync(
Sodium.base642bin(response.encryptedChallenge),
Sodium.base642bin(Configuration.instance.getKeyAttributes()!.publicKey),
CryptoUtil.base642bin(response.encryptedChallenge),
CryptoUtil.base642bin(
Configuration.instance.getKeyAttributes()!.publicKey,
),
Configuration.instance.getSecretKey()!,
);
final challengeResponseStr = utf8.decode(decryptChallenge);

View file

@ -5,7 +5,7 @@ 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/common/dynamic_fab.dart';
import 'package:ente_auth/ui/common/web_page.dart';
import 'package:ente_auth/utils/platform_util.dart';
import 'package:ente_auth/utils/toast_util.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@ -14,7 +14,7 @@ import 'package:step_progress_indicator/step_progress_indicator.dart';
import "package:styled_text/styled_text.dart";
class EmailEntryPage extends StatefulWidget {
const EmailEntryPage({Key? key}) : super(key: key);
const EmailEntryPage({super.key});
@override
State<EmailEntryPage> createState() => _EmailEntryPageState();
@ -427,15 +427,10 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
tags: {
'u-terms': StyledTextActionTag(
(String? text, Map<String?, String?> attrs) =>
Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {
return WebPage(
PlatformUtil.openWebView(
context,
context.l10n.termsOfServicesTitle,
"https://ente.io/terms",
);
},
),
),
style: const TextStyle(
decoration: TextDecoration.underline,
@ -443,15 +438,10 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
),
'u-policy': StyledTextActionTag(
(String? text, Map<String?, String?> attrs) =>
Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {
return WebPage(
PlatformUtil.openWebView(
context,
context.l10n.privacyPolicyTitle,
"https://ente.io/privacy",
);
},
),
),
style: const TextStyle(
decoration: TextDecoration.underline,
@ -494,15 +484,10 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
tags: {
'underline': StyledTextActionTag(
(String? text, Map<String?, String?> attrs) =>
Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {
return WebPage(
PlatformUtil.openWebView(
context,
context.l10n.encryption,
"https://ente.io/architecture",
);
},
),
),
style: const TextStyle(
decoration: TextDecoration.underline,

View file

@ -6,13 +6,13 @@ import 'package:ente_auth/models/api/user/srp.dart';
import 'package:ente_auth/services/user_service.dart';
import 'package:ente_auth/ui/account/login_pwd_verification_page.dart';
import 'package:ente_auth/ui/common/dynamic_fab.dart';
import 'package:ente_auth/ui/common/web_page.dart';
import 'package:ente_auth/utils/platform_util.dart';
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import "package:styled_text/styled_text.dart";
class LoginPage extends StatefulWidget {
const LoginPage({Key? key}) : super(key: key);
const LoginPage({super.key});
@override
State<LoginPage> createState() => _LoginPageState();
@ -25,6 +25,36 @@ class _LoginPageState extends State<LoginPage> {
Color? _emailInputFieldColor;
final Logger _logger = Logger('_LoginPageState');
Future<void> onPressed() async {
UserService.instance.setEmail(_email!);
Configuration.instance.resetVolatilePassword();
SrpAttributes? attr;
bool isEmailVerificationEnabled = true;
try {
attr = await UserService.instance.getSrpAttributes(_email!);
isEmailVerificationEnabled = attr.isEmailMFAEnabled;
} catch (e) {
if (e is! SrpSetupNotCompleteError) {
_logger.severe('Error getting SRP attributes', e);
}
}
if (attr != null && !isEmailVerificationEnabled) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {
return LoginPasswordVerificationPage(
srpAttributes: attr!,
);
},
),
);
} else {
await UserService.instance
.sendOtt(context, _email!, isCreateAccountScreen: false);
}
FocusScope.of(context).unfocus();
}
@override
void initState() {
_email = _config.getEmail();
@ -60,35 +90,7 @@ class _LoginPageState extends State<LoginPage> {
isKeypadOpen: isKeypadOpen,
isFormValid: _emailIsValid,
buttonText: context.l10n.logInLabel,
onPressedFunction: () async {
UserService.instance.setEmail(_email!);
Configuration.instance.resetVolatilePassword();
SrpAttributes? attr;
bool isEmailVerificationEnabled = true;
try {
attr = await UserService.instance.getSrpAttributes(_email!);
isEmailVerificationEnabled = attr.isEmailMFAEnabled;
} catch (e) {
if (e is! SrpSetupNotCompleteError) {
_logger.severe('Error getting SRP attributes', e);
}
}
if (attr != null && !isEmailVerificationEnabled) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {
return LoginPasswordVerificationPage(
srpAttributes: attr!,
);
},
),
);
} else {
await UserService.instance
.sendOtt(context, _email!, isCreateAccountScreen: false);
}
FocusScope.of(context).unfocus();
},
onPressedFunction: onPressed,
),
floatingActionButtonLocation: fabLocation(),
floatingActionButtonAnimator: NoScalingAnimation(),
@ -115,6 +117,8 @@ class _LoginPageState extends State<LoginPage> {
padding: const EdgeInsets.fromLTRB(20, 24, 20, 0),
child: TextFormField(
autofillHints: const [AutofillHints.email],
onFieldSubmitted:
_emailIsValid ? (value) => onPressed() : null,
decoration: InputDecoration(
fillColor: _emailInputFieldColor,
filled: true,
@ -178,15 +182,10 @@ class _LoginPageState extends State<LoginPage> {
tags: {
'u-terms': StyledTextActionTag(
(String? text, Map<String?, String?> attrs) =>
Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {
return WebPage(
PlatformUtil.openWebView(
context,
context.l10n.termsOfServicesTitle,
"https://ente.io/terms",
);
},
),
),
style: const TextStyle(
decoration: TextDecoration.underline,
@ -194,15 +193,10 @@ class _LoginPageState extends State<LoginPage> {
),
'u-policy': StyledTextActionTag(
(String? text, Map<String?, String?> attrs) =>
Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {
return WebPage(
PlatformUtil.openWebView(
context,
context.l10n.privacyPolicyTitle,
"https://ente.io/privacy",
);
},
),
),
style: const TextStyle(
decoration: TextDecoration.underline,

View file

@ -1,6 +1,5 @@
import "package:dio/dio.dart";
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/models/api/user/srp.dart";
import "package:ente_auth/services/user_service.dart";
@ -9,6 +8,7 @@ import 'package:ente_auth/ui/common/dynamic_fab.dart';
import "package:ente_auth/ui/components/buttons/button_widget.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";
@ -19,8 +19,7 @@ import "package:logging/logging.dart";
// volatile password.
class LoginPasswordVerificationPage extends StatefulWidget {
final SrpAttributes srpAttributes;
const LoginPasswordVerificationPage({Key? key, required this.srpAttributes})
: super(key: key);
const LoginPasswordVerificationPage({super.key, required this.srpAttributes});
@override
State<LoginPasswordVerificationPage> createState() =>
@ -36,6 +35,11 @@ class _LoginPasswordVerificationPageState
bool _passwordInFocus = false;
bool _passwordVisible = false;
Future<void> onPressed() async {
FocusScope.of(context).unfocus();
await verifyPassword(context, _passwordController.text);
}
@override
void initState() {
super.initState();
@ -77,10 +81,7 @@ class _LoginPasswordVerificationPageState
isKeypadOpen: isKeypadOpen,
isFormValid: _passwordController.text.isNotEmpty,
buttonText: context.l10n.logInLabel,
onPressedFunction: () async {
FocusScope.of(context).unfocus();
await verifyPassword(context, _passwordController.text);
},
onPressedFunction: onPressed,
),
floatingActionButtonLocation: fabLocation(),
floatingActionButtonAnimator: NoScalingAnimation(),
@ -101,7 +102,7 @@ class _LoginPasswordVerificationPageState
password,
dialog,
);
} on DioError catch (e, s) {
} on DioException catch (e, s) {
await dialog.hide();
if (e.response != null && e.response!.statusCode == 401) {
_logger.severe('server reject, failed verify SRP login', e, s);
@ -112,7 +113,7 @@ class _LoginPasswordVerificationPageState
);
} else {
_logger.severe('API failure during SRP login', e, s);
if (e.type == DioErrorType.other) {
if (e.type == DioExceptionType.unknown) {
await _showContactSupportDialog(
context,
context.l10n.noInternetConnection,
@ -229,6 +230,9 @@ class _LoginPasswordVerificationPageState
Padding(
padding: const EdgeInsets.fromLTRB(20, 24, 20, 0),
child: TextFormField(
onFieldSubmitted: _passwordController.text.isNotEmpty
? (_) => onPressed()
: null,
key: const ValueKey("passwordInputField"),
autofillHints: const [AutofillHints.password],
decoration: InputDecoration(

View file

@ -17,8 +17,8 @@ class OTTVerificationPage extends StatefulWidget {
this.isChangeEmail = false,
this.isCreateAccountScreen = false,
this.isResetPasswordScreen = false,
Key? key,
}) : super(key: key);
super.key,
});
@override
State<OTTVerificationPage> createState() => _OTTVerificationPageState();
@ -27,6 +27,23 @@ class OTTVerificationPage extends StatefulWidget {
class _OTTVerificationPageState extends State<OTTVerificationPage> {
final _verificationCodeController = TextEditingController();
Future<void> onPressed() async {
if (widget.isChangeEmail) {
await UserService.instance.changeEmail(
context,
widget.email,
_verificationCodeController.text,
);
} else {
await UserService.instance.verifyEmail(
context,
_verificationCodeController.text,
isResettingPasswordScreen: widget.isResetPasswordScreen,
);
}
FocusScope.of(context).unfocus();
}
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
@ -68,22 +85,9 @@ class _OTTVerificationPageState extends State<OTTVerificationPage> {
body: _getBody(),
floatingActionButton: DynamicFAB(
isKeypadOpen: isKeypadOpen,
isFormValid: !(_verificationCodeController.text.isEmpty),
isFormValid: _verificationCodeController.text.isNotEmpty,
buttonText: l10n.verify,
onPressedFunction: () {
if (widget.isChangeEmail) {
UserService.instance.changeEmail(
context,
widget.email,
_verificationCodeController.text,
);
} else {
UserService.instance
.verifyEmail(context, _verificationCodeController.text,
isResettingPasswordScreen: widget.isResetPasswordScreen,);
}
FocusScope.of(context).unfocus();
},
onPressedFunction: onPressed,
),
floatingActionButtonLocation: fabLocation(),
floatingActionButtonAnimator: NoScalingAnimation(),
@ -160,6 +164,9 @@ class _OTTVerificationPageState extends State<OTTVerificationPage> {
padding: const EdgeInsets.fromLTRB(20, 16, 20, 16),
child: TextFormField(
style: Theme.of(context).textTheme.titleMedium,
onFieldSubmitted: _verificationCodeController.text.isNotEmpty
? (_) => onPressed()
: null,
decoration: InputDecoration(
filled: true,
hintText: l10n.tapToEnterCode,

View file

@ -4,11 +4,11 @@ import 'package:ente_auth/models/key_gen_result.dart';
import 'package:ente_auth/services/user_service.dart';
import 'package:ente_auth/ui/account/recovery_key_page.dart';
import 'package:ente_auth/ui/common/dynamic_fab.dart';
import 'package:ente_auth/ui/common/web_page.dart';
import 'package:ente_auth/ui/components/models/button_type.dart';
import 'package:ente_auth/ui/home_page.dart';
import 'package:ente_auth/utils/dialog_util.dart';
import 'package:ente_auth/utils/navigation_util.dart';
import 'package:ente_auth/utils/platform_util.dart';
import 'package:ente_auth/utils/toast_util.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@ -25,7 +25,7 @@ enum PasswordEntryMode {
class PasswordEntryPage extends StatefulWidget {
final PasswordEntryMode mode;
const PasswordEntryPage({required this.mode, Key? key}) : super(key: key);
const PasswordEntryPage({required this.mode, super.key});
@override
State<PasswordEntryPage> createState() => _PasswordEntryPageState();
@ -343,17 +343,12 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {
return WebPage(
PlatformUtil.openWebView(
context,
context.l10n.howItWorks,
"https://ente.io/architecture",
);
},
),
);
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: RichText(

View file

@ -9,14 +9,14 @@ 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/crypto_util.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({Key? key}) : super(key: key);
const PasswordReentryPage({super.key});
@override
State<PasswordReentryPage> createState() => _PasswordReentryPageState();
@ -260,8 +260,8 @@ class _PasswordReentryPageState extends State<PasswordReentryPage> {
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Wrap(
alignment: WrapAlignment.spaceBetween,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
GestureDetector(
behavior: HitTestBehavior.opaque,
@ -274,15 +274,19 @@ class _PasswordReentryPageState extends State<PasswordReentryPage> {
),
);
},
child: Center(
child: Text(
context.l10n.forgotPassword,
style:
Theme.of(context).textTheme.titleMedium!.copyWith(
style: Theme.of(context)
.textTheme
.titleMedium!
.copyWith(
fontSize: 14,
decoration: TextDecoration.underline,
),
),
),
),
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () async {
@ -296,15 +300,19 @@ class _PasswordReentryPageState extends State<PasswordReentryPage> {
Navigator.of(context)
.popUntil((route) => route.isFirst);
},
child: Center(
child: Text(
context.l10n.changeEmail,
style:
Theme.of(context).textTheme.titleMedium!.copyWith(
style: Theme.of(context)
.textTheme
.titleMedium!
.copyWith(
fontSize: 14,
decoration: TextDecoration.underline,
),
),
),
),
],
),
),

View file

@ -1,3 +1,4 @@
import 'dart:convert';
import 'dart:io' as io;
import 'package:bip39/bip39.dart' as bip39;
@ -7,7 +8,10 @@ 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';
@ -27,7 +31,7 @@ class RecoveryKeyPage extends StatefulWidget {
const RecoveryKeyPage(
this.recoveryKey,
this.doneText, {
Key? key,
super.key,
this.showAppBar,
this.onDone,
this.isDismissible,
@ -35,7 +39,7 @@ class RecoveryKeyPage extends StatefulWidget {
this.text,
this.subText,
this.showProgressBar = false,
}) : super(key: key);
});
@override
State<RecoveryKeyPage> createState() => _RecoveryKeyPageState();
@ -44,7 +48,7 @@ class RecoveryKeyPage extends StatefulWidget {
class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
bool _hasTriedToSave = false;
final _recoveryKeyFile = io.File(
Configuration.instance.getTempDirectory() + "ente-recovery-key.txt",
"${Configuration.instance.getTempDirectory()}ente-recovery-key.txt",
);
@override
@ -61,6 +65,21 @@ class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
? 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(
@ -113,65 +132,76 @@ class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
style: Theme.of(context).textTheme.titleMedium,
),
const Padding(padding: EdgeInsets.only(top: 24)),
DottedBorder(
color: const Color.fromARGB(255, 105, 17, 127),
//color of dotted/dash line
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,
//thickness of dash/dots
color: const Color(0xFF6B6B6B),
dashPattern: const [6, 6],
radius: const Radius.circular(8),
//dash patterns, 10 is dash width, 6 is space width
child: SizedBox(
//inner container
// height: 120, //height of inner container
width: double
.infinity, //width to 100% match to parent container.
// ignore: prefer_const_literals_to_create_immutables
child: Column(
width: double.infinity,
child: Stack(
children: [
GestureDetector(
onTap: () async {
await Clipboard.setData(
ClipboardData(text: recoveryKey),
);
showShortToast(
context,
context.l10n.recoveryKeyCopiedToClipboard,
);
setState(() {
_hasTriedToSave = true;
});
},
child: Container(
decoration: BoxDecoration(
border: Border.all(
color: const Color.fromRGBO(
49,
155,
86,
.2,
),
),
borderRadius: const BorderRadius.all(
Radius.circular(2),
),
color: Theme.of(context)
.colorScheme
.recoveryKeyBoxColor,
),
Column(
children: [
Builder(
builder: (context) {
final content = Container(
padding: const EdgeInsets.all(20),
width: double.infinity,
child: Text(
recoveryKey,
style:
Theme.of(context).textTheme.bodyLarge,
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(
@ -193,7 +223,7 @@ class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
),
),
],
), // columnEnds
),
),
),
);
@ -207,13 +237,16 @@ class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
final List<Widget> childrens = [];
if (!_hasTriedToSave) {
childrens.add(
ElevatedButton(
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));
}
@ -221,31 +254,67 @@ class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
childrens.add(
GradientButton(
onTap: () async {
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(
ElevatedButton(
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);
// ignore: deprecated_member_use
await Share.shareFiles([_recoveryKeyFile.path]);
Future.delayed(const Duration(milliseconds: 500), () {
if (mounted) {
@ -264,3 +333,24 @@ class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
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,8 +1,7 @@
import 'dart:ui';
import 'package:ente_auth/core/configuration.dart';
import 'package:ente_auth/l10n/l10n.dart';
import 'package:ente_auth/ui/account/password_entry_page.dart';
import 'package:ente_auth/ui/common/dynamic_fab.dart';
import 'package:ente_auth/utils/dialog_util.dart';
@ -10,7 +9,7 @@ import 'package:ente_auth/utils/toast_util.dart';
import 'package:flutter/material.dart';
class RecoveryPage extends StatefulWidget {
const RecoveryPage({Key? key}) : super(key: key);
const RecoveryPage({super.key});
@override
State<RecoveryPage> createState() => _RecoveryPageState();
@ -19,6 +18,36 @@ class RecoveryPage extends StatefulWidget {
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!");
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (BuildContext context) {
return const PopScope(
canPop: false,
child: PasswordEntryPage(
mode: PasswordEntryMode.reset,
),
);
},
),
);
} catch (e) {
await dialog.hide();
String errMessage = 'The recovery key you entered is incorrect';
if (e is AssertionError) {
errMessage = '$errMessage : ${e.message}';
}
showErrorDialog(context, "Incorrect recovery key", errMessage);
}
}
@override
Widget build(BuildContext context) {
final isKeypadOpen = MediaQuery.of(context).viewInsets.bottom > 100;
@ -46,35 +75,7 @@ class _RecoveryPageState extends State<RecoveryPage> {
isKeypadOpen: isKeypadOpen,
isFormValid: _recoveryKey.text.isNotEmpty,
buttonText: 'Recover',
onPressedFunction: () 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!");
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (BuildContext context) {
return WillPopScope(
onWillPop: () async => false,
child: const PasswordEntryPage(
mode: PasswordEntryMode.reset,
),
);
},
),
);
} catch (e) {
await dialog.hide();
String errMessage = 'The recovery key you entered is incorrect';
if (e is AssertionError) {
errMessage = '$errMessage : ${e.message}';
}
showErrorDialog(context, "Incorrect recovery key", errMessage);
}
},
onPressedFunction: onPressed,
),
floatingActionButtonLocation: fabLocation(),
floatingActionButtonAnimator: NoScalingAnimation(),
@ -87,7 +88,7 @@ class _RecoveryPageState extends State<RecoveryPage> {
padding:
const EdgeInsets.symmetric(vertical: 30, horizontal: 20),
child: Text(
'Forgot password',
context.l10n.forgotPassword,
style: Theme.of(context).textTheme.headlineMedium,
),
),
@ -139,8 +140,10 @@ class _RecoveryPageState extends State<RecoveryPage> {
child: Center(
child: Text(
"No recovery key?",
style:
Theme.of(context).textTheme.titleMedium!.copyWith(
style: Theme.of(context)
.textTheme
.titleMedium!
.copyWith(
fontSize: 14,
decoration: TextDecoration.underline,
),

View file

@ -5,10 +5,9 @@ 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/crypto_util.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:flutter_sodium/flutter_sodium.dart";
import "package:logging/logging.dart";
typedef OnPasswordVerifiedFn = Future<void> Function(Uint8List bytes);
@ -17,8 +16,11 @@ class RequestPasswordVerificationPage extends StatefulWidget {
final OnPasswordVerifiedFn onPasswordVerified;
final Function? onPasswordError;
const RequestPasswordVerificationPage(
{super.key, required this.onPasswordVerified, this.onPasswordError,});
const RequestPasswordVerificationPage({
super.key,
required this.onPasswordVerified,
this.onPasswordError,
});
@override
State<RequestPasswordVerificationPage> createState() =>
@ -82,15 +84,15 @@ class _RequestPasswordVerificationPageState
try {
final attributes = Configuration.instance.getKeyAttributes()!;
final Uint8List keyEncryptionKey = await CryptoUtil.deriveKey(
utf8.encode(_passwordController.text) as Uint8List,
Sodium.base642bin(attributes.kekSalt),
utf8.encode(_passwordController.text),
CryptoUtil.base642bin(attributes.kekSalt),
attributes.memLimit,
attributes.opsLimit,
);
CryptoUtil.decryptSync(
Sodium.base642bin(attributes.encryptedKey),
CryptoUtil.base642bin(attributes.encryptedKey),
keyEncryptionKey,
Sodium.base642bin(attributes.keyDecryptionNonce),
CryptoUtil.base642bin(attributes.keyDecryptionNonce),
);
dialog.show();
// pop

View file

@ -11,7 +11,7 @@ import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
class SessionsPage extends StatefulWidget {
const SessionsPage({Key? key}) : super(key: key);
const SessionsPage({super.key});
@override
State<SessionsPage> createState() => _SessionsPageState();

View file

@ -12,12 +12,13 @@ import 'package:ente_auth/ui/common/gradient_button.dart';
import 'package:ente_auth/ui/components/buttons/button_widget.dart';
import 'package:ente_auth/utils/dialog_util.dart';
import 'package:ente_auth/utils/navigation_util.dart';
import 'package:ente_auth/utils/platform_util.dart';
import 'package:ente_crypto_dart/ente_crypto_dart.dart';
import 'package:flutter/material.dart';
import 'package:flutter_sodium/flutter_sodium.dart';
import 'package:logging/logging.dart';
class VerifyRecoveryPage extends StatefulWidget {
const VerifyRecoveryPage({Key? key}) : super(key: key);
const VerifyRecoveryPage({super.key});
@override
State<VerifyRecoveryPage> createState() => _VerifyRecoveryPageState();
@ -34,14 +35,14 @@ class _VerifyRecoveryPageState extends State<VerifyRecoveryPage> {
try {
final String inputKey = _recoveryKey.text.trim();
final String recoveryKey =
Sodium.bin2hex(Configuration.instance.getRecoveryKey());
CryptoUtil.bin2hex(Configuration.instance.getRecoveryKey());
final String recoveryKeyWords = bip39.entropyToMnemonic(recoveryKey);
if (inputKey == recoveryKey || inputKey == recoveryKeyWords) {
try {
await UserRemoteFlagService.instance.markRecoveryVerificationAsDone();
} catch (e) {
await dialog.hide();
if (e is DioError && e.type == DioErrorType.other) {
if (e is DioException && e.type == DioExceptionType.unknown) {
await showErrorDialog(
context,
"No internet connection",
@ -88,10 +89,13 @@ class _VerifyRecoveryPageState extends State<VerifyRecoveryPage> {
context,
"Please authenticate to view your recovery key",
);
await PlatformUtil.refocusWindows();
if (hasAuthenticated) {
String recoveryKey;
try {
recoveryKey = Sodium.bin2hex(Configuration.instance.getRecoveryKey());
recoveryKey =
CryptoUtil.bin2hex(Configuration.instance.getRecoveryKey());
routeToPage(
context,
RecoveryKeyPage(

View file

@ -6,12 +6,12 @@ class CodeTimerProgress extends StatefulWidget {
final int period;
CodeTimerProgress({
Key? key,
super.key,
required this.period,
}) : super(key: key);
});
@override
_CodeTimerProgressState createState() => _CodeTimerProgressState();
State createState() => _CodeTimerProgressState();
}
class _CodeTimerProgressState extends State<CodeTimerProgress>

View file

@ -14,6 +14,7 @@ import 'package:ente_auth/store/code_store.dart';
import 'package:ente_auth/ui/code_timer_progress.dart';
import 'package:ente_auth/ui/utils/icon_utils.dart';
import 'package:ente_auth/utils/dialog_util.dart';
import 'package:ente_auth/utils/platform_util.dart';
import 'package:ente_auth/utils/toast_util.dart';
import 'package:ente_auth/utils/totp_util.dart';
import 'package:flutter/material.dart';
@ -24,7 +25,7 @@ import 'package:move_to_background/move_to_background.dart';
class CodeWidget extends StatefulWidget {
final Code code;
const CodeWidget(this.code, {Key? key}) : super(key: key);
const CodeWidget(this.code, {super.key});
@override
State<CodeWidget> createState() => _CodeWidgetState();
@ -372,9 +373,10 @@ class _CodeWidgetState extends State<CodeWidget> {
}
Future<void> _onEditPressed(_) async {
bool _isAuthSuccessful = await LocalAuthenticationService.instance
bool isAuthSuccessful = await LocalAuthenticationService.instance
.requestLocalAuthentication(context, context.l10n.editCodeAuthMessage);
if (!_isAuthSuccessful) {
await PlatformUtil.refocusWindows();
if (!isAuthSuccessful) {
return;
}
final Code? code = await Navigator.of(context).push(
@ -390,9 +392,10 @@ class _CodeWidgetState extends State<CodeWidget> {
}
Future<void> _onShowQrPressed(_) async {
bool _isAuthSuccessful = await LocalAuthenticationService.instance
bool isAuthSuccessful = await LocalAuthenticationService.instance
.requestLocalAuthentication(context, context.l10n.showQRAuthMessage);
if (!_isAuthSuccessful) {
await PlatformUtil.refocusWindows();
if (!isAuthSuccessful) {
return;
}
// ignore: unused_local_variable
@ -406,14 +409,15 @@ class _CodeWidgetState extends State<CodeWidget> {
}
void _onDeletePressed(_) async {
bool _isAuthSuccessful =
bool isAuthSuccessful =
await LocalAuthenticationService.instance.requestLocalAuthentication(
context,
context.l10n.deleteCodeAuthMessage,
);
if (!_isAuthSuccessful) {
if (!isAuthSuccessful) {
return;
}
FocusScope.of(context).requestFocus();
final l10n = context.l10n;
await showChoiceActionSheet(
context,
@ -450,7 +454,7 @@ class _CodeWidgetState extends State<CodeWidget> {
code = code.replaceAll(RegExp(r'\d'), '');
}
if (code.length == 6) {
return code.substring(0, 3) + " " + code.substring(3, 6);
return "${code.substring(0, 3)} ${code.substring(3, 6)}";
}
return code;
}

View file

@ -5,8 +5,7 @@ import 'package:flutter/material.dart';
class BottomShadowWidget extends StatelessWidget {
final double offsetDy;
final Color? shadowColor;
const BottomShadowWidget({this.offsetDy = 28, this.shadowColor, Key? key})
: super(key: key);
const BottomShadowWidget({this.offsetDy = 28, this.shadowColor, super.key});
@override
Widget build(BuildContext context) {

View file

@ -1,17 +1,15 @@
import 'package:flutter/material.dart';
class DividerWithPadding extends StatelessWidget {
final double left, top, right, bottom, thinckness;
const DividerWithPadding({
Key? key,
super.key,
this.left = 0,
this.top = 0,
this.right = 0,
this.bottom = 0,
this.thinckness = 0.5,
}) : super(key: key);
});
@override
Widget build(BuildContext context) {

View file

@ -1,3 +1,5 @@
import 'dart:math' as math;
import 'package:ente_auth/ente_theme_data.dart';
@ -10,12 +12,12 @@ class DynamicFAB extends StatelessWidget {
final Function? onPressedFunction;
const DynamicFAB({
Key? key,
super.key,
this.isKeypadOpen,
this.buttonText,
this.isFormValid,
this.onPressedFunction,
}) : super(key: key);
});
@override
Widget build(BuildContext context) {
@ -60,10 +62,10 @@ class DynamicFAB extends StatelessWidget {
} else {
return Container(
width: double.infinity,
height: 56,
padding: const EdgeInsets.symmetric(horizontal: 20),
child: OutlinedButton(
onPressed:
isFormValid! ? onPressedFunction as void Function()? : null,
onPressed: isFormValid! ? onPressedFunction as void Function()? : null,
child: Text(buttonText!),
),
);

View file

@ -1,5 +1,3 @@
import 'package:flutter/material.dart';
class GradientButton extends StatelessWidget {
@ -15,17 +13,21 @@ class GradientButton extends StatelessWidget {
// padding between the text and icon
final double paddingValue;
// used when two icons are in row
final bool reversedGradient;
const GradientButton({
Key? key,
super.key,
this.linearGradientColors = const [
Color.fromARGB(255, 133, 44, 210),
Color.fromARGB(255, 187, 26, 93),
],
this.reversedGradient = false,
this.onTap,
this.text = '',
this.iconData,
this.paddingValue = 0.0,
}) : super(key: key);
});
@override
Widget build(BuildContext context) {
@ -71,7 +73,9 @@ class GradientButton extends StatelessWidget {
gradient: LinearGradient(
begin: const Alignment(0.1, -0.9),
end: const Alignment(-0.6, 0.9),
colors: linearGradientColors,
colors: reversedGradient
? linearGradientColors.reversed.toList()
: linearGradientColors,
),
borderRadius: BorderRadius.circular(8),
),

View file

@ -1,12 +1,10 @@
import 'package:ente_auth/ente_theme_data.dart';
import 'package:flutter/material.dart';
class LinearProgressDialog extends StatefulWidget {
final String message;
const LinearProgressDialog(this.message, {Key? key}) : super(key: key);
const LinearProgressDialog(this.message, {super.key});
@override
LinearProgressDialogState createState() => LinearProgressDialogState();
@ -29,8 +27,8 @@ class LinearProgressDialogState extends State<LinearProgressDialog> {
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async => false,
return PopScope(
canPop: false,
child: AlertDialog(
title: Text(
widget.message,

View file

@ -11,8 +11,8 @@ class EnteLoadingWidget extends StatelessWidget {
this.size = 14,
this.padding = 5,
this.alignment = Alignment.center,
Key? key,
}) : super(key: key);
super.key,
});
@override
Widget build(BuildContext context) {

View file

@ -152,8 +152,8 @@ class ProgressDialog {
barrierColor: _barrierColor,
builder: (BuildContext context) {
_dismissingContext = context;
return WillPopScope(
onWillPop: () async => _barrierDismissible,
return PopScope(
canPop: _barrierDismissible,
child: Dialog(
backgroundColor: _backgroundColor,
insetAnimationCurve: _insetAnimCurve,

View file

@ -8,8 +8,7 @@ class RenameDialog extends StatefulWidget {
final String type;
final int maxLength;
const RenameDialog(this.name, this.type, {Key? key, this.maxLength = 100})
: super(key: key);
const RenameDialog(this.name, this.type, {super.key, this.maxLength = 100});
@override
State<RenameDialog> createState() => _RenameDialogState();

View file

@ -6,7 +6,7 @@ class WebPage extends StatefulWidget {
final String title;
final String url;
const WebPage(this.title, this.url, {Key? key}) : super(key: key);
const WebPage(this.title, this.url, {super.key});
@override
State<WebPage> createState() => _WebPageState();
@ -28,9 +28,9 @@ class _WebPageState extends State<WebPage> {
),
backgroundColor: Colors.black,
body: InAppWebView(
initialUrlRequest: URLRequest(url: Uri.parse(widget.url)),
initialOptions: InAppWebViewGroupOptions(
crossPlatform: InAppWebViewOptions(transparentBackground: true),
initialUrlRequest: URLRequest(url: WebUri(widget.url)),
initialSettings: InAppWebViewSettings(
transparentBackground: true,
),
onLoadStop: (c, url) {
setState(() {

View file

@ -57,7 +57,7 @@ class ButtonWidget extends StatelessWidget {
final ValueNotifier<String>? progressStatus;
const ButtonWidget({
Key? key,
super.key,
required this.buttonType,
this.buttonSize = ButtonSize.large,
this.icon,
@ -71,7 +71,7 @@ class ButtonWidget extends StatelessWidget {
this.shouldSurfaceExecutionStates = true,
this.progressStatus,
this.shouldShowSuccessConfirmation = false,
}) : super(key: key);
});
@override
Widget build(BuildContext context) {
@ -155,7 +155,7 @@ class ButtonChildWidget extends StatefulWidget {
final bool shouldShowSuccessConfirmation;
const ButtonChildWidget({
Key? key,
super.key,
required this.buttonStyle,
required this.buttonType,
required this.isDisabled,
@ -168,7 +168,7 @@ class ButtonChildWidget extends StatefulWidget {
this.labelText,
this.icon,
this.buttonAction,
}) : super(key: key);
});
@override
State<ButtonChildWidget> createState() => _ButtonChildWidgetState();

View file

@ -17,7 +17,7 @@ class IconButtonWidget extends StatefulWidget {
final Color? pressedColor;
final Color? iconColor;
const IconButtonWidget({
Key? key,
super.key,
required this.icon,
required this.iconButtonType,
this.disableGestureDetector = false,
@ -25,7 +25,7 @@ class IconButtonWidget extends StatefulWidget {
this.defaultColor,
this.pressedColor,
this.iconColor,
}) : super(key: key);
});
@override
State<IconButtonWidget> createState() => _IconButtonWidgetState();

View file

@ -13,8 +13,8 @@ class CaptionedTextWidget extends StatelessWidget {
this.textStyle,
this.makeTextBold = false,
this.textColor,
Key? key,
}) : super(key: key);
super.key,
});
@override
Widget build(BuildContext context) {

View file

@ -14,12 +14,12 @@ class DividerWidget extends StatelessWidget {
final bool divColorHasBlur;
final EdgeInsets? padding;
const DividerWidget({
Key? key,
super.key,
required this.dividerType,
this.bgColor = Colors.transparent,
this.divColorHasBlur = true,
this.padding,
}) : super(key: key);
});
@override
Widget build(BuildContext context) {

View file

@ -13,8 +13,8 @@ class ExpandableMenuItemWidget extends StatefulWidget {
required this.title,
required this.selectionOptionsWidget,
required this.leadingIcon,
Key? key,
}) : super(key: key);
super.key,
});
@override
State<ExpandableMenuItemWidget> createState() =>

View file

@ -1,13 +1,10 @@
import 'dart:ui';
import 'package:ente_auth/core/event_bus.dart';
import 'package:ente_auth/events/opened_settings_event.dart';
import 'package:flutter/material.dart';
class HomeHeaderWidget extends StatefulWidget {
final Widget centerWidget;
const HomeHeaderWidget({required this.centerWidget, Key? key})
: super(key: key);
const HomeHeaderWidget({required this.centerWidget, super.key});
@override
State<HomeHeaderWidget> createState() => _HomeHeaderWidgetState();
@ -16,7 +13,7 @@ class HomeHeaderWidget extends StatefulWidget {
class _HomeHeaderWidgetState extends State<HomeHeaderWidget> {
@override
Widget build(BuildContext context) {
final hasNotch = window.viewPadding.top > 65;
final hasNotch = View.of(context).viewPadding.top > 65;
return Padding(
padding: EdgeInsets.fromLTRB(4, hasNotch ? 4 : 8, 4, 4),
child: Row(

View file

@ -12,7 +12,7 @@ class TrailingWidget extends StatefulWidget {
final double trailingExtraMargin;
final bool showExecutionStates;
const TrailingWidget({
Key? key,
super.key,
required this.executionStateNotifier,
this.trailingIcon,
this.trailingIconColor,
@ -20,7 +20,7 @@ class TrailingWidget extends StatefulWidget {
required this.trailingIconIsMuted,
required this.trailingExtraMargin,
required this.showExecutionStates,
}) : super(key: key);
});
@override
State<TrailingWidget> createState() => _TrailingWidgetState();
}
@ -101,11 +101,11 @@ class ExpansionTrailingIcon extends StatelessWidget {
final IconData? trailingIcon;
final Color? trailingIconColor;
const ExpansionTrailingIcon({
Key? key,
super.key,
required this.isExpanded,
this.trailingIcon,
this.trailingIconColor,
}) : super(key: key);
});
@override
Widget build(BuildContext context) {
@ -138,12 +138,12 @@ class LeadingWidget extends StatelessWidget {
// leadIconSize deafult value is 20.
final double leadingIconSize;
const LeadingWidget({
Key? key,
super.key,
required this.leadingIconSize,
this.leadingIcon,
this.leadingIconColor,
this.leadingIconWidget,
}) : super(key: key);
});
@override
Widget build(BuildContext context) {

View file

@ -86,8 +86,8 @@ class MenuItemWidget extends StatefulWidget {
this.showOnlyLoadingState = false,
this.surfaceExecutionStates = true,
this.alwaysShowSuccessState = false,
Key? key,
}) : super(key: key);
super.key,
});
@override
State<MenuItemWidget> createState() => _MenuItemWidgetState();

View file

@ -3,8 +3,7 @@ import 'package:flutter/material.dart';
class MenuSectionDescriptionWidget extends StatelessWidget {
final String content;
const MenuSectionDescriptionWidget({Key? key, required this.content})
: super(key: key);
const MenuSectionDescriptionWidget({super.key, required this.content});
@override
Widget build(BuildContext context) {

View file

@ -21,14 +21,14 @@ class NotificationWidget extends StatelessWidget {
final NotificationType type;
const NotificationWidget({
Key? key,
super.key,
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) {

View file

@ -6,11 +6,11 @@ class TitleBarTitleWidget extends StatelessWidget {
final bool isTitleH2;
final IconData? icon;
const TitleBarTitleWidget({
Key? key,
super.key,
this.title,
this.isTitleH2 = false,
this.icon,
}) : super(key: key);
});
@override
Widget build(BuildContext context) {

View file

@ -14,7 +14,7 @@ class TitleBarWidget extends StatelessWidget {
final bool isOnTopOfScreen;
final Color? backgroundColor;
const TitleBarWidget({
Key? key,
super.key,
this.leading,
this.title,
this.caption,
@ -25,7 +25,7 @@ class TitleBarWidget extends StatelessWidget {
this.isFlexibleSpaceDisabled = false,
this.isOnTopOfScreen = true,
this.backgroundColor,
}) : super(key: key);
});
@override
Widget build(BuildContext context) {

View file

@ -13,8 +13,8 @@ class ToggleSwitchWidget extends StatefulWidget {
const ToggleSwitchWidget({
required this.value,
required this.onChanged,
Key? key,
}) : super(key: key);
super.key,
});
@override
State<ToggleSwitchWidget> createState() => _ToggleSwitchWidgetState();

View file

@ -21,10 +21,15 @@ class CoachMarkWidget extends StatelessWidget {
children: [
Expanded(
child: Container(
width: double.infinity,
color: Theme.of(context).colorScheme.background.withOpacity(0.1),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 8, sigmaY: 8),
child: Column(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
@ -32,15 +37,14 @@ class CoachMarkWidget extends StatelessWidget {
size: 42,
),
const SizedBox(
height: 12,
height: 24,
),
Text(
l10n.swipeHint,
style: Theme.of(context).textTheme.titleLarge,
textAlign: TextAlign.center,
),
const SizedBox(
height: 16,
height: 36,
),
SizedBox(
width: 160,
@ -55,6 +59,8 @@ class CoachMarkWidget extends StatelessWidget {
),
],
),
],
),
),
),
),

View file

@ -3,6 +3,7 @@ import 'package:ente_auth/theme/ente_theme.dart';
import 'package:ente_auth/ui/settings/data/import_page.dart';
import 'package:ente_auth/ui/settings/faq.dart';
import 'package:ente_auth/utils/navigation_util.dart';
import 'package:ente_auth/utils/platform_util.dart';
import 'package:flutter/material.dart';
class HomeEmptyStateWidget extends StatelessWidget {
@ -10,16 +11,18 @@ class HomeEmptyStateWidget extends StatelessWidget {
final VoidCallback? onManuallySetupTap;
const HomeEmptyStateWidget({
Key? key,
super.key,
required this.onScanTap,
required this.onManuallySetupTap,
}) : super(key: key);
});
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
return SingleChildScrollView(
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints.tightFor(height: 800, width: 450),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 40.0, horizontal: 40),
child: Column(
@ -38,6 +41,7 @@ class HomeEmptyStateWidget extends StatelessWidget {
style: Theme.of(context).textTheme.headlineMedium,
),
const SizedBox(height: 64),
if (PlatformUtil.isMobile())
SizedBox(
width: 400,
child: OutlinedButton(
@ -56,7 +60,7 @@ class HomeEmptyStateWidget extends StatelessWidget {
const SizedBox(height: 54),
InkWell(
onTap: () {
routeToPage(context, ImportCodePage());
routeToPage(context, const ImportCodePage());
},
child: Text(
l10n.importCodes,
@ -93,6 +97,7 @@ class HomeEmptyStateWidget extends StatelessWidget {
),
),
),
),
);
}
}

View file

@ -6,8 +6,8 @@ class SpeedDialLabelWidget extends StatelessWidget {
const SpeedDialLabelWidget(
this.label, {
Key? key,
}) : super(key: key);
super.key,
});
@override
Widget build(BuildContext context) {

View file

@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:io';
import 'package:app_links/app_links.dart';
import 'package:ente_auth/core/configuration.dart';
import 'package:ente_auth/core/event_bus.dart';
import 'package:ente_auth/ente_theme_data.dart';
@ -22,28 +23,32 @@ import 'package:ente_auth/ui/home/speed_dial_label_widget.dart';
import 'package:ente_auth/ui/scanner_page.dart';
import 'package:ente_auth/ui/settings_page.dart';
import 'package:ente_auth/utils/dialog_util.dart';
import 'package:ente_auth/utils/platform_util.dart';
import 'package:ente_auth/utils/totp_util.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_speed_dial/flutter_speed_dial.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:logging/logging.dart';
import 'package:move_to_background/move_to_background.dart';
import 'package:uni_links/uni_links.dart';
class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
static final _settingsPage = SettingsPage(
late final _settingsPage = SettingsPage(
emailNotifier: UserService.instance.emailValueNotifier,
scaffoldKey: scaffoldKey,
);
bool _hasLoaded = false;
bool _isSettingsOpen = false;
final Logger _logger = Logger("HomePage");
final scaffoldKey = GlobalKey<ScaffoldState>();
final TextEditingController _textController = TextEditingController();
bool _showSearchBox = false;
@ -144,28 +149,25 @@ class _HomePageState extends State<HomePage> {
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
return WillPopScope(
onWillPop: () async {
return PopScope(
onPopInvoked: (_) async {
if (_isSettingsOpen) {
Navigator.pop(context);
return false;
scaffoldKey.currentState!.closeDrawer();
return;
} else if (!Platform.isAndroid) {
Navigator.of(context).pop();
return;
}
if (Platform.isAndroid) {
MoveToBackground.moveTaskToBack();
return false;
} else {
return true;
}
},
canPop: false,
child: Scaffold(
key: scaffoldKey,
drawerEnableOpenDragGesture: !Platform.isAndroid,
drawer: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 428),
child: Drawer(
width: double.infinity,
drawer: Drawer(
width: 428,
child: _settingsPage,
),
),
onDrawerChanged: (isOpened) => _isSettingsOpen = isOpened,
body: SafeArea(
bottom: false,
@ -232,10 +234,13 @@ class _HomePageState extends State<HomePage> {
onManuallySetupTap: _redirectToManualEntryPage,
);
} else {
final list = ListView.builder(
final list = AlignedGridView.count(
crossAxisCount: (MediaQuery.sizeOf(context).width ~/ 400)
.clamp(1, double.infinity)
.toInt(),
itemBuilder: ((context, index) {
try {
return CodeWidget(_filteredCodes[index]);
return ClipRect(child: CodeWidget(_filteredCodes[index]));
} catch (e) {
return const Text("Failed");
}
@ -289,8 +294,10 @@ class _HomePageState extends State<HomePage> {
Future<bool> _initDeepLinks() async {
// Platform messages may fail, so we use a try/catch PlatformException.
final appLinks = AppLinks();
try {
final String? initialLink = await getInitialLink();
String? initialLink;
initialLink = await appLinks.getInitialAppLinkString();
// Parse the link and warn the user, if it is not correct,
// but keep in mind it could be `null`.
if (initialLink != null) {
@ -306,14 +313,16 @@ class _HomePageState extends State<HomePage> {
}
// Attach a listener to the stream
linkStream.listen(
(String? link) {
if (!kIsWeb && !Platform.isLinux) {
appLinks.stringLinkStream.listen(
(link) {
_handleDeeplink(context, link);
},
onError: (err) {
_logger.severe(err);
},
);
}
return false;
}
@ -342,6 +351,14 @@ class _HomePageState extends State<HomePage> {
}
Widget _getFab() {
if (PlatformUtil.isDesktop()) {
return FloatingActionButton(
onPressed: () => _redirectToManualEntryPage(),
child: const Icon(Icons.add),
elevation: 8.0,
shape: const CircleBorder(),
);
}
return SpeedDial(
icon: Icons.add,
activeIcon: Icons.close,

View file

@ -6,8 +6,8 @@ class LinearProgressWidget extends StatelessWidget {
const LinearProgressWidget({
required this.color,
required this.fractionOfStorage,
Key? key,
}) : super(key: key);
super.key,
});
@override
Widget build(BuildContext context) {

View file

@ -9,7 +9,7 @@ import 'package:flutter/material.dart';
import 'package:qr_code_scanner/qr_code_scanner.dart';
class ScannerGoogleAuthPage extends StatefulWidget {
const ScannerGoogleAuthPage({Key? key}) : super(key: key);
const ScannerGoogleAuthPage({super.key});
@override
State<ScannerGoogleAuthPage> createState() => ScannerGoogleAuthPageState();
@ -85,7 +85,7 @@ class ScannerGoogleAuthPageState extends State<ScannerGoogleAuthPage> {
} catch (e) {
controller.dispose();
Navigator.of(context).pop();
showToast(context, "Error " + e.toString());
showToast(context, "Error $e");
}
});
}

View file

@ -6,7 +6,7 @@ import 'package:flutter/material.dart';
import 'package:qr_code_scanner/qr_code_scanner.dart';
class ScannerPage extends StatefulWidget {
const ScannerPage({Key? key}) : super(key: key);
const ScannerPage({super.key});
@override
State<ScannerPage> createState() => ScannerPageState();

View file

@ -1,19 +1,19 @@
import 'package:ente_auth/l10n/l10n.dart';
import 'package:ente_auth/services/update_service.dart';
import 'package:ente_auth/theme/ente_theme.dart';
import 'package:ente_auth/ui/common/web_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';
import 'package:ente_auth/ui/settings/app_update_dialog.dart';
import 'package:ente_auth/ui/settings/common_settings.dart';
import 'package:ente_auth/utils/dialog_util.dart';
import 'package:ente_auth/utils/platform_util.dart';
import 'package:ente_auth/utils/toast_util.dart';
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
class AboutSectionWidget extends StatelessWidget {
const AboutSectionWidget({Key? key}) : super(key: key);
const AboutSectionWidget({super.key});
@override
Widget build(BuildContext context) {
@ -36,7 +36,7 @@ class AboutSectionWidget extends StatelessWidget {
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () async {
launchUrl(Uri.parse("https://github.com/ente-io/ente"));
launchUrl(Uri.parse("https://github.com/ente-io/auth"));
},
),
sectionOptionSpacing,
@ -102,8 +102,8 @@ class AboutMenuItemWidget extends StatelessWidget {
required this.title,
required this.url,
this.webPageTitle,
Key? key,
}) : super(key: key);
super.key,
});
@override
Widget build(BuildContext context) {
@ -115,12 +115,10 @@ class AboutMenuItemWidget extends StatelessWidget {
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () async {
Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {
return WebPage(webPageTitle ?? title, url);
},
),
await PlatformUtil.openWebView(
context,
webPageTitle ?? title,
url,
);
},
);

View file

@ -1,22 +1,20 @@
import 'package:ente_auth/l10n/l10n.dart';
import 'package:ente_auth/services/local_authentication_service.dart';
import 'package:ente_auth/services/user_service.dart';
import 'package:ente_auth/theme/ente_theme.dart';
import 'package:ente_auth/ui/account/change_email_dialog.dart';
import 'package:ente_auth/ui/account/delete_account_page.dart';
import 'package:ente_auth/ui/account/password_entry_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';
import 'package:ente_auth/ui/settings/common_settings.dart';
import 'package:ente_auth/utils/dialog_util.dart';
import 'package:ente_auth/utils/navigation_util.dart';
import 'package:ente_auth/utils/platform_util.dart';
import 'package:flutter/material.dart';
class AccountSectionWidget extends StatelessWidget {
AccountSectionWidget({Key? key}) : super(key: key);
AccountSectionWidget({super.key});
@override
Widget build(BuildContext context) {
@ -46,6 +44,7 @@ class AccountSectionWidget extends StatelessWidget {
context,
l10n.authToChangeYourEmail,
);
await PlatformUtil.refocusWindows();
if (hasAuthenticated) {
showDialog(
context: context,
@ -59,33 +58,34 @@ class AccountSectionWidget extends StatelessWidget {
},
),
sectionOptionSpacing,
MenuItemWidget(
captionedTextWidget: CaptionedTextWidget(
title: l10n.changePassword,
),
pressedColor: getEnteColorScheme(context).fillFaint,
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () async {
final hasAuthenticated = await LocalAuthenticationService.instance
.requestLocalAuthentication(
context,
l10n.authToChangeYourPassword,
);
if (hasAuthenticated) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {
return const PasswordEntryPage(
mode: PasswordEntryMode.update,
);
},
),
);
}
},
),
sectionOptionSpacing,
// TODO: Remove After Stable
// MenuItemWidget(
// captionedTextWidget: CaptionedTextWidget(
// title: l10n.changePassword,
// ),
// pressedColor: getEnteColorScheme(context).fillFaint,
// trailingIcon: Icons.chevron_right_outlined,
// trailingIconIsMuted: true,
// onTap: () async {
// final hasAuthenticated = await LocalAuthenticationService.instance
// .requestLocalAuthentication(
// context,
// l10n.authToChangeYourPassword,
// );
// if (hasAuthenticated) {
// Navigator.of(context).push(
// MaterialPageRoute(
// builder: (BuildContext context) {
// return const PasswordEntryPage(
// mode: PasswordEntryMode.update,
// );
// },
// ),
// );
// }
// },
// ),
// sectionOptionSpacing,
MenuItemWidget(
captionedTextWidget: CaptionedTextWidget(
title: context.l10n.logout,
@ -115,6 +115,7 @@ class AccountSectionWidget extends StatelessWidget {
children: children,
);
}
void _onLogoutTapped(BuildContext context) {
showChoiceActionSheet(
context,

View file

@ -14,7 +14,7 @@ import 'package:url_launcher/url_launcher_string.dart';
class AppUpdateDialog extends StatefulWidget {
final LatestVersionInfo? latestVersionInfo;
const AppUpdateDialog(this.latestVersionInfo, {Key? key}) : super(key: key);
const AppUpdateDialog(this.latestVersionInfo, {super.key});
@override
State<AppUpdateDialog> createState() => _AppUpdateDialogState();
@ -30,7 +30,7 @@ class _AppUpdateDialogState extends State<AppUpdateDialog> {
Padding(
padding: const EdgeInsets.fromLTRB(8, 4, 0, 4),
child: Text(
"- " + log,
"- $log",
style: Theme.of(context).textTheme.bodySmall!.copyWith(
fontSize: 14,
),
@ -87,8 +87,8 @@ class _AppUpdateDialogState extends State<AppUpdateDialog> {
);
final shouldForceUpdate =
UpdateService.instance.shouldForceUpdate(widget.latestVersionInfo);
return WillPopScope(
onWillPop: () async => !shouldForceUpdate,
return PopScope(
canPop: !shouldForceUpdate,
child: AlertDialog(
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@ -118,7 +118,7 @@ class _AppUpdateDialogState extends State<AppUpdateDialog> {
class ApkDownloaderDialog extends StatefulWidget {
final LatestVersionInfo? versionInfo;
const ApkDownloaderDialog(this.versionInfo, {Key? key}) : super(key: key);
const ApkDownloaderDialog(this.versionInfo, {super.key});
@override
State<ApkDownloaderDialog> createState() => _ApkDownloaderDialogState();
@ -131,17 +131,15 @@ class _ApkDownloaderDialogState extends State<ApkDownloaderDialog> {
@override
void initState() {
super.initState();
_saveUrl = Configuration.instance.getTempDirectory() +
"ente-" +
widget.versionInfo!.name! +
".apk";
_saveUrl =
"${Configuration.instance.getTempDirectory()}ente-${widget.versionInfo!.name!}.apk";
_downloadApk();
}
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async => false,
return PopScope(
canPop: false,
child: AlertDialog(
title: const Text(
"Downloading...",

View file

@ -4,8 +4,8 @@ import 'package:package_info_plus/package_info_plus.dart';
class AppVersionWidget extends StatefulWidget {
const AppVersionWidget({
Key? key,
}) : super(key: key);
super.key,
});
@override
State<AppVersionWidget> createState() => _AppVersionWidgetState();
@ -48,7 +48,7 @@ class _AppVersionWidgetState extends State<AppVersionWidget> {
return Padding(
padding: const EdgeInsets.all(20),
child: Text(
"Version: " + snapshot.data!,
"Version: ${snapshot.data!}",
style: Theme.of(context).textTheme.bodySmall,
),
);

View file

@ -11,7 +11,7 @@ import 'package:ente_auth/utils/navigation_util.dart';
import 'package:flutter/material.dart';
class DangerSectionWidget extends StatelessWidget {
const DangerSectionWidget({Key? key}) : super(key: key);
const DangerSectionWidget({super.key});
@override
Widget build(BuildContext context) {

View file

@ -1,4 +1,3 @@
import 'package:ente_auth/l10n/l10n.dart';
import 'package:ente_auth/theme/ente_theme.dart';
import 'package:ente_auth/ui/components/captioned_text_widget.dart';
@ -11,7 +10,9 @@ import 'package:ente_auth/utils/navigation_util.dart';
import 'package:flutter/material.dart';
class DataSectionWidget extends StatelessWidget {
DataSectionWidget({Key? key}) : super(key: key);
// final _logger = Logger("AccountSectionWidget");
DataSectionWidget({super.key});
@override
Widget build(BuildContext context) {
@ -36,7 +37,7 @@ class DataSectionWidget extends StatelessWidget {
trailingIcon: Icons.chevron_right_outlined,
trailingIconIsMuted: true,
onTap: () async {
routeToPage(context, ImportCodePage());
routeToPage(context, const ImportCodePage());
},
),
sectionOptionSpacing,

View file

@ -9,14 +9,14 @@ import 'package:ente_auth/store/code_store.dart';
import 'package:ente_auth/ui/components/buttons/button_widget.dart';
import 'package:ente_auth/ui/components/dialog_widget.dart';
import 'package:ente_auth/ui/components/models/button_type.dart';
import 'package:ente_auth/utils/crypto_util.dart';
import 'package:ente_auth/utils/dialog_util.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:ente_crypto_dart/ente_crypto_dart.dart';
import 'package:file_saver/file_saver.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_sodium/flutter_sodium.dart';
import 'package:intl/intl.dart';
import 'package:logging/logging.dart';
import 'package:share_plus/share_plus.dart';
@ -76,17 +76,17 @@ Future<void> _requestForEncryptionPassword(
try {
final kekSalt = CryptoUtil.getSaltToDeriveKey();
final derivedKeyResult = await CryptoUtil.deriveSensitiveKey(
utf8.encode(password) as Uint8List,
utf8.encode(password),
kekSalt,
);
String exportPlainText = await _getAuthDataForExport();
// Encrypt the key with this derived key
final encResult = await CryptoUtil.encryptChaCha(
utf8.encode(exportPlainText) as Uint8List,
final encResult = await CryptoUtil.encryptData(
utf8.encode(exportPlainText),
derivedKeyResult.key,
);
final encContent = Sodium.bin2base64(encResult.encryptedData!);
final encNonce = Sodium.bin2base64(encResult.header!);
final encContent = CryptoUtil.bin2base64(encResult.encryptedData!);
final encNonce = CryptoUtil.bin2base64(encResult.header!);
final EnteAuthExport data = EnteAuthExport(
version: 1,
encryptedData: encContent,
@ -94,11 +94,11 @@ Future<void> _requestForEncryptionPassword(
kdfParams: KDFParams(
memLimit: derivedKeyResult.memLimit,
opsLimit: derivedKeyResult.opsLimit,
salt: Sodium.bin2base64(kekSalt),
salt: CryptoUtil.bin2base64(kekSalt),
),
);
// get json value of data
_exportCodes(context, jsonEncode(data.toJson()));
await _exportCodes(context, jsonEncode(data.toJson()));
} catch (e, s) {
Logger("ExportWidget").severe(e, s);
showToast(context, "Error while exporting codes.");
@ -126,46 +126,55 @@ Future<void> _showExportWarningDialog(BuildContext context) async {
Future<void> _exportCodes(BuildContext context, String fileContent) async {
DateTime now = DateTime.now().toUtc();
String formattedDate = DateFormat('yyyy-MM-dd').format(now);
String exportFileName = 'ente-auth-codes-$formattedDate.txt';
final _codeFile = File(
Configuration.instance.getTempDirectory() + exportFileName,
);
String exportFileName = 'ente-auth-codes-$formattedDate';
String exportFileExtension = 'txt';
final hasAuthenticated = await LocalAuthenticationService.instance
.requestLocalAuthentication(context, context.l10n.authToExportCodes);
await PlatformUtil.refocusWindows();
if (!hasAuthenticated) {
return;
}
if (_codeFile.existsSync()) {
await _codeFile.delete();
}
_codeFile.writeAsStringSync(fileContent);
final Size size = MediaQuery.of(context).size;
if (Platform.isAndroid) {
await FileSaver.instance.saveAs(
name: exportFileName,
filePath: _codeFile.path,
mimeType: MimeType.text,
ext: 'txt',
Future.delayed(
const Duration(milliseconds: 1200),
() async => await shareDialog(
context,
context.l10n.exportCodes,
saveAction: () async {
await PlatformUtil.shareFile(
exportFileName,
exportFileExtension,
CryptoUtil.strToBin(fileContent),
MimeType.text,
);
} else {
},
sendAction: () async {
final codeFile = File(
"${Configuration.instance.getTempDirectory()}$exportFileName.$exportFileExtension",
);
if (codeFile.existsSync()) {
await codeFile.delete();
}
codeFile.writeAsStringSync(fileContent);
final Size size = MediaQuery.of(context).size;
await Share.shareFiles(
[_codeFile.path],
[codeFile.path],
sharePositionOrigin: Rect.fromLTWH(0, 0, size.width, size.height / 2),
);
}
Future.delayed(const Duration(seconds: 30), () async {
if (_codeFile.existsSync()) {
_codeFile.deleteSync();
if (codeFile.existsSync()) {
codeFile.deleteSync();
}
});
},
),
);
}
Future<String> _getAuthDataForExport() async {
final codes = await CodeStore.instance.getAllCodes();
String data = "";
for (final code in codes) {
data += code.rawData + "\n";
data += "${code.rawData}\n";
}
return data;
}

View file

@ -91,7 +91,7 @@ Future<int?> _processAegisExportFile(
final jsonString = await file.readAsString();
final decodedJson = jsonDecode(jsonString);
final isEncrypted = decodedJson['header']['slots'] != null;
var aegisDB;
Map? aegisDB;
if (isEncrypted) {
String? password;
try {
@ -127,7 +127,7 @@ Future<int?> _processAegisExportFile(
aegisDB = decodedJson['db'];
}
final parsedCodes = [];
for (var item in aegisDB['entries']) {
for (var item in aegisDB?['entries']) {
var kind = item['type'];
var account = item['name'];
var issuer = item['issuer'];

View file

@ -11,14 +11,13 @@ import 'package:ente_auth/ui/components/buttons/button_widget.dart';
import 'package:ente_auth/ui/components/dialog_widget.dart';
import 'package:ente_auth/ui/components/models/button_type.dart';
import 'package:ente_auth/ui/settings/data/import/import_success.dart';
import 'package:ente_auth/utils/crypto_util.dart';
import 'package:ente_auth/utils/dialog_util.dart';
import 'package:ente_auth/utils/toast_util.dart';
import 'package:ente_crypto_dart/ente_crypto_dart.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_sodium/flutter_sodium.dart';
import 'package:logging/logging.dart';
Future<void> showEncryptedImportInstruction(BuildContext context) async {
@ -80,21 +79,21 @@ Future<void> _decryptExportData(
try {
await progressDialog.show();
final derivedKey = await CryptoUtil.deriveKey(
utf8.encode(password) as Uint8List,
Sodium.base642bin(enteAuthExport.kdfParams.salt),
utf8.encode(password),
CryptoUtil.base642bin(enteAuthExport.kdfParams.salt),
enteAuthExport.kdfParams.memLimit,
enteAuthExport.kdfParams.opsLimit,
);
Uint8List? decryptedContent;
// Encrypt the key with this derived key
try {
decryptedContent = await CryptoUtil.decryptChaCha(
Sodium.base642bin(enteAuthExport.encryptedData),
decryptedContent = await CryptoUtil.decryptData(
CryptoUtil.base642bin(enteAuthExport.encryptedData),
derivedKey,
Sodium.base642bin(enteAuthExport.encryptionNonce),
CryptoUtil.base642bin(enteAuthExport.encryptionNonce),
);
} catch (e,s) {
Logger("encryptedImport").warning('failed to decrypt',e,s);
} catch (e, s) {
Logger("encryptedImport").warning('failed to decrypt', e, s);
showToast(context, l10n.incorrectPasswordTitle);
isPasswordIncorrect = true;
}

View file

@ -1,6 +1,5 @@
import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
import 'package:base32/base32.dart';
import 'package:ente_auth/l10n/l10n.dart';
import 'package:ente_auth/models/code.dart';
@ -12,6 +11,8 @@ import 'package:ente_auth/ui/components/dialog_widget.dart';
import 'package:ente_auth/ui/components/models/button_type.dart';
import 'package:ente_auth/ui/scanner_gauth_page.dart';
import 'package:ente_auth/ui/settings/data/import/import_success.dart';
import 'package:ente_auth/utils/platform_util.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
@ -24,6 +25,7 @@ Future<void> showGoogleAuthInstruction(BuildContext context) async {
title: l10n.importFromApp("Google Authenticator"),
body: l10n.importGoogleAuthGuide,
buttons: [
if (PlatformUtil.isMobile())
ButtonWidget(
buttonType: ButtonType.primary,
labelText: l10n.scanAQrCode,

View file

@ -1,4 +1,3 @@
import 'package:ente_auth/ui/settings/data/import/2fas_import.dart';
import 'package:ente_auth/ui/settings/data/import/aegis_import.dart';
import 'package:ente_auth/ui/settings/data/import/bitwarden_import.dart';
import 'package:ente_auth/ui/settings/data/import/encrypted_ente_import.dart';
@ -6,6 +5,7 @@ import 'package:ente_auth/ui/settings/data/import/google_auth_import.dart';
import 'package:ente_auth/ui/settings/data/import/lastpass_import.dart';
import 'package:ente_auth/ui/settings/data/import/plain_text_import.dart';
import 'package:ente_auth/ui/settings/data/import/raivo_plain_text_import.dart';
import 'package:ente_auth/ui/settings/data/import/two_fas_import.dart';
import 'package:ente_auth/ui/settings/data/import_page.dart';
import 'package:flutter/cupertino.dart';

View file

@ -11,9 +11,9 @@ Future<void> importSuccessDialog(BuildContext context, int count) async {
firstButtonLabel: context.l10n.ok,
firstButtonOnTap: () async {
Navigator.of(context).pop();
if(Navigator.of(context).canPop()) {
Navigator.of(context).pop();
}
// if(Navigator.of(context).canPop()) {
// Navigator.of(context).pop();
// }
},
firstButtonType: ButtonType.primary,
);

View file

@ -48,10 +48,8 @@ class PlainTextImport extends StatelessWidget {
],
);
}
}
Future<void> showImportInstructionDialog(BuildContext context) async {
final l10n = context.l10n;
final AlertDialog alert = AlertDialog(
@ -94,7 +92,6 @@ Future<void> showImportInstructionDialog(BuildContext context) async {
);
}
Future<void> _pickImportFile(BuildContext context) async {
final l10n = context.l10n;
FilePickerResult? result = await FilePicker.platform.pickFiles();

View file

@ -170,8 +170,8 @@ Future<int?> _process2FasExportFile(
}
String decrypt2FasVault(dynamic data, {required String password}) {
int ITERATION_COUNT = 10000;
int KEY_SIZE = 256;
int iterationCount = 10000;
int keySize = 256;
final String encryptedServices = data["servicesEncrypted"];
var split = encryptedServices.split(":");
final encryptedData = base64.decode(split[0]);
@ -181,11 +181,11 @@ String decrypt2FasVault(dynamic data, {required String password}) {
final pbkdf2 = PBKDF2KeyDerivator(HMac(SHA256Digest(), 64));
final params = Pbkdf2Parameters(
salt,
ITERATION_COUNT,
KEY_SIZE ~/ 8,
iterationCount,
keySize ~/ 8,
);
pbkdf2.init(params);
Uint8List key = Uint8List(KEY_SIZE ~/ 8);
Uint8List key = Uint8List(keySize ~/ 8);
pbkdf2.deriveKey(Uint8List.fromList(utf8.encode(password)), 0, key, 0);
final decrypted = decrypt(key, iv, encryptedData);
final utf8Decode = utf8.decode(decrypted);

View file

@ -21,7 +21,9 @@ enum ImportType {
}
class ImportCodePage extends StatelessWidget {
final List<ImportType> importOptions = [
const ImportCodePage({super.key});
static const List<ImportType> importOptions = [
ImportType.plainText,
ImportType.encrypted,
ImportType.twoFas,
@ -32,8 +34,6 @@ class ImportCodePage extends StatelessWidget {
ImportType.lastpass,
];
ImportCodePage({super.key});
String getTitle(BuildContext context, ImportType type) {
switch (type) {
case ImportType.plainText:

View file

@ -3,9 +3,9 @@ import 'package:ente_auth/l10n/l10n.dart';
import 'package:ente_auth/ui/settings/common_settings.dart';
import 'package:ente_auth/ui/settings/settings_section_title.dart';
import 'package:ente_auth/ui/settings/settings_text_item.dart';
import 'package:ente_crypto_dart/ente_crypto_dart.dart';
import 'package:expandable/expandable.dart';
import 'package:flutter/material.dart';
import 'package:flutter_sodium/flutter_sodium.dart';
class DebugSectionWidget extends StatelessWidget {
const DebugSectionWidget({super.key});
@ -51,7 +51,7 @@ class DebugSectionWidget extends StatelessWidget {
"Key",
style: TextStyle(fontWeight: FontWeight.bold),
),
Text(Sodium.bin2base64(Configuration.instance.getKey()!)),
Text(CryptoUtil.bin2base64(Configuration.instance.getKey()!)),
const Padding(padding: EdgeInsets.all(12)),
const Text(
"Encrypted Key",

View file

@ -8,8 +8,8 @@ import 'package:flutter/material.dart';
class FAQQuestionsWidget extends StatelessWidget {
const FAQQuestionsWidget({
Key? key,
}) : super(key: key);
super.key,
});
@override
Widget build(BuildContext context) {
@ -64,9 +64,9 @@ class FAQQuestionsWidget extends StatelessWidget {
class FaqWidget extends StatelessWidget {
const FaqWidget({
Key? key,
super.key,
required this.faq,
}) : super(key: key);
});
final FaqItem? faq;

View file

@ -17,7 +17,7 @@ import 'package:ente_auth/utils/toast_util.dart';
import 'package:flutter/material.dart';
class AdvancedSectionWidget extends StatefulWidget {
const AdvancedSectionWidget({Key? key}) : super(key: key);
const AdvancedSectionWidget({super.key});
@override
State<AdvancedSectionWidget> createState() => _AdvancedSectionWidgetState();

View file

@ -18,8 +18,8 @@ class LanguageSelectorPage extends StatelessWidget {
this.supportedLocales,
this.onLocaleChanged,
this.currentLocale, {
Key? key,
}) : super(key: key);
super.key,
});
@override
Widget build(BuildContext context) {
@ -79,8 +79,8 @@ class ItemsWidget extends StatefulWidget {
this.supportedLocales,
this.onLocaleChanged,
this.currentLocale, {
Key? key,
}) : super(key: key);
super.key,
});
@override
State<ItemsWidget> createState() => _ItemsWidgetState();

View file

@ -4,8 +4,8 @@ import 'package:url_launcher/url_launcher.dart';
class MadeWithLoveWidget extends StatelessWidget {
const MadeWithLoveWidget({
Key? key,
}) : super(key: key);
super.key,
});
@override
Widget build(BuildContext context) {

View file

@ -15,15 +15,15 @@ import 'package:ente_auth/ui/components/expandable_menu_item_widget.dart';
import 'package:ente_auth/ui/components/menu_item_widget.dart';
import 'package:ente_auth/ui/components/toggle_switch_widget.dart';
import 'package:ente_auth/ui/settings/common_settings.dart';
import 'package:ente_auth/utils/crypto_util.dart';
import 'package:ente_auth/utils/dialog_util.dart';
import 'package:ente_auth/utils/navigation_util.dart';
import 'package:ente_auth/utils/platform_util.dart';
import 'package:ente_auth/utils/toast_util.dart';
import 'package:ente_crypto_dart/ente_crypto_dart.dart';
import 'package:flutter/material.dart';
import 'package:flutter_sodium/flutter_sodium.dart';
class SecuritySectionWidget extends StatefulWidget {
const SecuritySectionWidget({Key? key}) : super(key: key);
const SecuritySectionWidget({super.key});
@override
State<SecuritySectionWidget> createState() => _SecuritySectionWidgetState();
@ -78,11 +78,12 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
context,
l10n.authToViewYourRecoveryKey,
);
await PlatformUtil.refocusWindows();
if (hasAuthenticated) {
String recoveryKey;
try {
recoveryKey =
Sodium.bin2hex(Configuration.instance.getRecoveryKey());
CryptoUtil.bin2hex(Configuration.instance.getRecoveryKey());
} catch (e) {
showGenericErrorDialog(context: context);
return;
@ -113,6 +114,7 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
);
final isEmailMFAEnabled =
UserService.instance.hasEmailMFAEnabled();
await PlatformUtil.refocusWindows();
if (hasAuthenticated) {
await updateEmailMFA(!isEmailMFAEnabled);
if (mounted) {
@ -136,6 +138,7 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
context,
context.l10n.authToViewYourActiveSessions,
);
await PlatformUtil.refocusWindows();
if (hasAuthenticated) {
Navigator.of(context).push(
MaterialPageRoute(
@ -167,6 +170,7 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
context.l10n.lockScreenEnablePreSteps,
);
if (hasAuthenticated) {
FocusScope.of(context).requestFocus();
setState(() {});
}
},

View file

@ -8,9 +8,9 @@ class SettingsSectionTitle extends StatelessWidget {
const SettingsSectionTitle(
this.title, {
Key? key,
super.key,
this.color,
}) : super(key: key);
});
@override
Widget build(BuildContext context) {

View file

@ -8,10 +8,10 @@ class SettingsTextItem extends StatelessWidget {
final String text;
final IconData icon;
const SettingsTextItem({
Key? key,
super.key,
required this.text,
required this.icon,
}) : super(key: key);
});
@override
Widget build(BuildContext context) {

View file

@ -11,7 +11,7 @@ import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher_string.dart';
class SocialSectionWidget extends StatelessWidget {
const SocialSectionWidget({Key? key}) : super(key: key);
const SocialSectionWidget({super.key});
@override
Widget build(BuildContext context) {
@ -67,9 +67,9 @@ class SocialsMenuItemWidget extends StatelessWidget {
const SocialsMenuItemWidget(
this.text,
this.url, {
Key? key,
super.key,
this.launchInExternalApp = true,
}) : super(key: key);
});
@override
Widget build(BuildContext context) {

Some files were not shown because too many files have changed in this diff Show more