Neeraj Gupta hace 1 año
padre
commit
b10ee19a50

+ 2 - 1
analysis_options.yaml

@@ -48,4 +48,5 @@ analyzer:
     avoid_renaming_method_parameters: ignore # incorrect warnings for `equals` overrides
 
   exclude:
-    - thirdparty/**
+    - thirdparty/**
+    - flutter/**

+ 7 - 7
ios/Podfile.lock

@@ -94,7 +94,7 @@ PODS:
   - shared_preferences_foundation (0.0.1):
     - Flutter
     - FlutterMacOS
-  - sqflite (0.0.2):
+  - sqflite (0.0.3):
     - Flutter
     - FMDB (>= 2.7.5)
   - SwiftyGif (5.4.4)
@@ -207,7 +207,7 @@ SPEC CHECKSUMS:
   flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
   flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be
   flutter_sodium: c84426b4de738514b5b66cfdeb8a06634e72fe0b
-  fluttertoast: eb263d302cc92e04176c053d2385237e9f43fad0
+  fluttertoast: fafc4fa4d01a6a9e4f772ecd190ffa525e9e2d9c
   FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
   local_auth_ios: c6cf091ded637a88f24f86a8875d8b0f526e2605
   move_to_background: 39a5b79b26d577b0372cbe8a8c55e7aa9fcd3a2d
@@ -215,19 +215,19 @@ SPEC CHECKSUMS:
   open_filex: 6e26e659846ec990262224a12ef1c528bb4edbe4
   OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c
   package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e
-  path_provider_foundation: 37748e03f12783f9de2cb2c4eadfaa25fe6d4852
+  path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
   qr_code_scanner: bb67d64904c3b9658ada8c402e8b4d406d5d796e
   Reachability: 33e18b67625424e47b6cde6d202dce689ad7af96
   SDWebImage: 9bec4c5cdd9579e1f57104735ee0c37df274d593
   Sentry: 4c9babff9034785067c896fd580b1f7de44da020
-  sentry_flutter: b10ae7a5ddcbc7f04648eeb2672b5747230172f1
+  sentry_flutter: 1346a880b24c0240807b53b10cf50ddad40f504e
   share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68
-  shared_preferences_foundation: 297b3ebca31b34ec92be11acd7fb0ba932c822ca
-  sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
+  shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126
+  sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a
   SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f
   Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
   uni_links: d97da20c7701486ba192624d99bffaaffcfc298a
-  url_launcher_ios: ae1517e5e344f5544fb090b079e11f399dfbe4d2
+  url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4
 
 PODFILE CHECKSUM: b4e3a7eabb03395b66e81fc061789f61526ee6bb
 

+ 22 - 18
lib/core/configuration.dart

@@ -4,7 +4,6 @@ import 'dart:typed_data';
 
 import 'package:bip39/bip39.dart' as bip39;
 import 'package:ente_auth/core/constants.dart';
-import 'package:ente_auth/core/errors.dart';
 import 'package:ente_auth/core/event_bus.dart';
 import 'package:ente_auth/events/signed_in_event.dart';
 import 'package:ente_auth/events/signed_out_event.dart';
@@ -18,6 +17,7 @@ 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:tuple/tuple.dart';
 
 class Configuration {
   Configuration._privateConstructor();
@@ -146,6 +146,7 @@ class Configuration {
       utf8.encode(password) as Uint8List,
       kekSalt,
     );
+    final loginKey = await CryptoUtil.deriveLoginKey(derivedKeyResult.key);
 
     // Encrypt the key with this derived key
     final encryptedKeyData =
@@ -175,10 +176,11 @@ class Configuration {
       Sodium.bin2hex(recoveryKey),
       Sodium.bin2base64(keyPair.sk),
     );
-    return KeyGenResult(attributes, privateAttributes);
+    return KeyGenResult(attributes, privateAttributes, loginKey);
   }
 
-  Future<KeyAttributes> updatePassword(String password) async {
+
+  Future<Tuple2<KeyAttributes, Uint8List>> getAttributesForNewPassword(String password) async {
     // Get master key
     final masterKey = getKey();
 
@@ -189,6 +191,7 @@ class Configuration {
       utf8.encode(password) as Uint8List,
       kekSalt,
     );
+    final loginKey = await CryptoUtil.deriveLoginKey(derivedKeyResult.key);
 
     // Encrypt the key with this derived key
     final encryptedKeyData =
@@ -196,42 +199,38 @@ class Configuration {
 
     final existingAttributes = getKeyAttributes();
 
-    return existingAttributes!.copyWith(
+    final updatedAttributes = existingAttributes!.copyWith(
       kekSalt: Sodium.bin2base64(kekSalt),
       encryptedKey: Sodium.bin2base64(encryptedKeyData.encryptedData!),
       keyDecryptionNonce: Sodium.bin2base64(encryptedKeyData.nonce!),
       memLimit: derivedKeyResult.memLimit,
       opsLimit: derivedKeyResult.opsLimit,
     );
+    return Tuple2(updatedAttributes, loginKey);
   }
 
-  Future<void> decryptAndSaveSecrets(
+  // decryptSecretsAndGetLoginKey decrypts the master key and recovery key
+  // with the given password and save them in local secure storage.
+  // This method also returns the keyEncKey that can be used for performing
+  // SRP setup for existing users.
+  Future<Uint8List> decryptSecretsAndGetKeyEncKey(
     String password,
     KeyAttributes attributes,
   ) async {
     _logger.info('Start decryptAndSaveSecrets');
-    // validatePreVerificationStateCheck(
-    //   attributes,
-    //   password,
-    //   getEncryptedToken(),
-    // );
-    _logger.info('state validation done');
-    final kek = await CryptoUtil.deriveKey(
+    final keyEncryptionKey = await CryptoUtil.deriveKey(
       utf8.encode(password) as Uint8List,
       Sodium.base642bin(attributes.kekSalt),
       attributes.memLimit,
       attributes.opsLimit,
-    ).onError((e, s) {
-      _logger.severe('deriveKey failed', e, s);
-      throw KeyDerivationError();
-    });
+    );
 
     _logger.info('user-key done');
     Uint8List key;
     try {
       key = CryptoUtil.decryptSync(
         Sodium.base642bin(attributes.encryptedKey),
-        kek,
+        keyEncryptionKey,
         Sodium.base642bin(attributes.keyDecryptionNonce),
       );
     } catch (e) {
@@ -256,6 +255,7 @@ class Configuration {
     await setToken(
       Sodium.bin2base64(token, variant: Sodium.base64VariantUrlsafe),
     );
+    return keyEncryptionKey;
   }
 
   Future<void> recover(String recoveryKey) async {
@@ -306,6 +306,10 @@ class Configuration {
     return _cachedToken;
   }
 
+  bool isLoggedIn() {
+    return getToken() != null;
+  }
+
   Future<void> setToken(String token) async {
     _cachedToken = token;
     await _preferences.setString(tokenKey, token);
@@ -413,7 +417,7 @@ class Configuration {
     final keyAttributes = getKeyAttributes()!;
     return CryptoUtil.decryptSync(
       Sodium.base642bin(keyAttributes.recoveryKeyEncryptedWithMasterKey),
-      getKey(),
+      getKey()!,
       Sodium.base642bin(keyAttributes.recoveryKeyDecryptionNonce),
     );
   }

+ 2 - 0
lib/core/errors.dart

@@ -41,4 +41,6 @@ class InvalidStateError extends AssertionError {
 
 class KeyDerivationError extends Error {}
 
+class SrpSetupNotCompleteError extends Error {}
+
 class AuthenticatorKeyNotFound extends Error {}

+ 48 - 0
lib/core/network.dart

@@ -1,18 +1,29 @@
 import 'dart:io';
 
 import 'package:dio/dio.dart';
+import 'package:ente_auth/core/configuration.dart';
+import 'package:ente_auth/core/constants.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';
 
 int kConnectTimeout = 15000;
 
 class Network {
+  // apiEndpoint points to the Ente server's API endpoint
+  static const apiEndpoint = String.fromEnvironment(
+    "endpoint",
+    defaultValue: kDefaultProductionEndpoint,
+  );
   late Dio _dio;
+  late Dio _enteDio;
 
   Future<void> init() async {
     await FkUserAgent.init();
     final packageInfo = await PackageInfo.fromPlatform();
+    final preferences = await SharedPreferences.getInstance();
     _dio = Dio(
       BaseOptions(
         connectTimeout: kConnectTimeout,
@@ -24,6 +35,18 @@ class Network {
       ),
     );
     _dio.interceptors.add(RequestIdInterceptor());
+    _enteDio = Dio(
+      BaseOptions(
+        baseUrl: apiEndpoint,
+        connectTimeout: kConnectTimeout,
+        headers: {
+          HttpHeaders.userAgentHeader: FkUserAgent.userAgent,
+          'X-Client-Version': packageInfo.version,
+          'X-Client-Package': packageInfo.packageName,
+        },
+      ),
+    );
+    _enteDio.interceptors.add(EnteRequestInterceptor(preferences, apiEndpoint));
   }
 
   Network._privateConstructor();
@@ -31,6 +54,7 @@ class Network {
   static Network instance = Network._privateConstructor();
 
   Dio getDio() => _dio;
+  Dio get enteDio => _enteDio;
 }
 
 class RequestIdInterceptor extends Interceptor {
@@ -41,3 +65,27 @@ class RequestIdInterceptor extends Interceptor {
     return super.onRequest(options, handler);
   }
 }
+
+class EnteRequestInterceptor extends Interceptor {
+  final SharedPreferences _preferences;
+  final String enteEndpoint;
+
+  EnteRequestInterceptor(this._preferences, this.enteEndpoint);
+
+  @override
+  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
+    if (kDebugMode) {
+      assert(
+      options.baseUrl == enteEndpoint,
+      "interceptor should only be used for API endpoint",
+      );
+    }
+    // ignore: prefer_const_constructors
+    options.headers.putIfAbsent("x-request-id", () => Uuid().v4().toString());
+    final String? tokenValue = _preferences.getString(Configuration.tokenKey);
+    if (tokenValue != null) {
+      options.headers.putIfAbsent("X-Auth-Token", () => tokenValue);
+    }
+    return super.onRequest(options, handler);
+  }
+}

+ 14 - 1
lib/l10n/arb/app_en.arb

@@ -277,5 +277,18 @@
   "thisWillLogYouOutOfTheFollowingDevice": "This will log you out of the following device:",
   "terminateSession": "Terminate session?",
   "terminate": "Terminate",
-  "thisDevice": "This device"
+  "thisDevice": "This device",
+  "toResetVerifyEmail": "To reset your password, please verify your email first.",
+  "thisEmailIsAlreadyInUse": "This email is already in use",
+  "verificationFailedPleaseTryAgain": "Verification failed, please try again",
+  "yourVerificationCodeHasExpired": "Your verification code has expired",
+  "incorrectCode": "Incorrect code",
+  "sorryTheCodeYouveEnteredIsIncorrect": "Sorry, the code you've entered is incorrect",
+  "emailChangedTo": "Email changed to {newEmail}",
+  "authenticationFailedPleaseTryAgain": "Authentication failed, please try again",
+  "authenticationSuccessful": "Authentication successful!",
+  "twofactorAuthenticationSuccessfullyReset": "Two-factor authentication successfully reset",
+  "incorrectRecoveryKey": "Incorrect recovery key",
+  "theRecoveryKeyYouEnteredIsIncorrect": "The recovery key you entered is incorrect",
+  "enterPassword": "Enter password"
 }

+ 4 - 0
lib/main.dart

@@ -1,4 +1,5 @@
 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';
@@ -17,6 +18,7 @@ import 'package:ente_auth/store/code_store.dart';
 import 'package:ente_auth/ui/tools/app_lock.dart';
 import 'package:ente_auth/ui/tools/lock_screen.dart';
 import 'package:ente_auth/utils/crypto_util.dart';
+import 'package:flutter/foundation.dart';
 import "package:flutter/material.dart";
 import 'package:logging/logging.dart';
 import 'package:path_provider/path_provider.dart';
@@ -70,6 +72,8 @@ Future _runWithLogs(Function() function, {String prefix = ""}) async {
 }
 
 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();
   await PreferenceService.instance.init();
   await CodeStore.instance.init();

+ 132 - 0
lib/models/api/user/srp.dart

@@ -0,0 +1,132 @@
+class SetupSRPRequest {
+  final String srpUserID;
+  final String srpSalt;
+  final String srpVerifier;
+  final String srpA;
+  final bool isUpdate;
+
+  SetupSRPRequest({
+    required this.srpUserID,
+    required this.srpSalt,
+    required this.srpVerifier,
+    required this.srpA,
+    required this.isUpdate,
+  });
+
+  Map<String, dynamic> toMap() {
+    return {
+      'srpUserID': srpUserID.toString(),
+      'srpSalt': srpSalt,
+      'srpVerifier': srpVerifier,
+      'srpA': srpA,
+      'isUpdate': isUpdate,
+    };
+  }
+
+  factory SetupSRPRequest.fromJson(Map<String, dynamic> json) {
+    return SetupSRPRequest(
+      srpUserID: json['srpUserID'],
+      srpSalt: json['srpSalt'],
+      srpVerifier: json['srpVerifier'],
+      srpA: json['srpA'],
+      isUpdate: json['isUpdate'],
+    );
+  }
+}
+
+class SetupSRPResponse {
+  final String setupID;
+  final String srpB;
+
+  SetupSRPResponse({
+    required this.setupID,
+    required this.srpB,
+  });
+
+  Map<String, dynamic> toMap() {
+    return {
+      'setupID': setupID.toString(),
+      'srpB': srpB,
+    };
+  }
+
+  factory SetupSRPResponse.fromJson(Map<String, dynamic> json) {
+    return SetupSRPResponse(
+      setupID: json['setupID'],
+      srpB: json['srpB'],
+    );
+  }
+}
+
+class CompleteSRPSetupRequest {
+  final String setupID;
+  final String srpM1;
+
+  CompleteSRPSetupRequest({
+    required this.setupID,
+    required this.srpM1,
+  });
+
+  Map<String, dynamic> toMap() {
+    return {
+      'setupID': setupID.toString(),
+      'srpM1': srpM1,
+    };
+  }
+
+  factory CompleteSRPSetupRequest.fromJson(Map<String, dynamic> json) {
+    return CompleteSRPSetupRequest(
+      setupID: json['setupID'],
+      srpM1: json['srpM1'],
+    );
+  }
+}
+class SrpAttributes {
+  final String srpUserID;
+  final String srpSalt;
+  final int memLimit;
+  final int opsLimit;
+  final String kekSalt;
+
+  SrpAttributes({
+    required this.srpUserID,
+    required this.srpSalt,
+    required this.memLimit,
+    required this.opsLimit,
+    required this.kekSalt,
+  });
+
+  factory SrpAttributes.fromMap(Map<String, dynamic> map) {
+    return SrpAttributes(
+      srpUserID: map['attributes']['srpUserID'],
+      srpSalt: map['attributes']['srpSalt'],
+      memLimit: map['attributes']['memLimit'],
+      opsLimit: map['attributes']['opsLimit'],
+      kekSalt: map['attributes']['kekSalt'],
+    );
+  }
+}
+
+class CompleteSRPSetupResponse {
+  final String setupID;
+  final String srpM2;
+
+  CompleteSRPSetupResponse({
+    required this.setupID,
+    required this.srpM2,
+  });
+
+  Map<String, dynamic> toMap() {
+    return {
+      'setupID': setupID,
+      'srpM2': srpM2,
+    };
+  }
+
+  factory CompleteSRPSetupResponse.fromJson(Map<String, dynamic> json) {
+    return CompleteSRPSetupResponse(
+      setupID: json['setupID'],
+      srpM2: json['srpM2'],
+    );
+  }
+}

+ 4 - 1
lib/models/key_gen_result.dart

@@ -1,9 +1,12 @@
+import 'dart:typed_data';
+
 import 'package:ente_auth/models/key_attributes.dart';
 import 'package:ente_auth/models/private_key_attributes.dart';
 
 class KeyGenResult {
   final KeyAttributes keyAttributes;
   final PrivateKeyAttributes privateKeyAttributes;
+  final Uint8List loginKey;
 
-  KeyGenResult(this.keyAttributes, this.privateKeyAttributes);
+  KeyGenResult(this.keyAttributes, this.privateKeyAttributes, this.loginKey);
 }

+ 2 - 2
lib/onboarding/view/onboarding_page.dart

@@ -163,7 +163,7 @@ class _OnboardingPageState extends State<OnboardingPage> {
       // No key
       if (Configuration.instance.getKeyAttributes() == null) {
         // Never had a key
-        page = const PasswordEntryPage();
+        page = const PasswordEntryPage(mode: PasswordEntryMode.set,);
       } else if (Configuration.instance.getKey() == null) {
         // Yet to decrypt the key
         page = const PasswordReentryPage();
@@ -189,7 +189,7 @@ class _OnboardingPageState extends State<OnboardingPage> {
       // No key
       if (Configuration.instance.getKeyAttributes() == null) {
         // Never had a key
-        page = const PasswordEntryPage();
+        page = const PasswordEntryPage(mode: PasswordEntryMode.set,);
       } else if (Configuration.instance.getKey() == null) {
         // Yet to decrypt the key
         page = const PasswordReentryPage();

+ 1 - 1
lib/services/authenticator_service.dart

@@ -217,7 +217,7 @@ class AuthenticatorService {
       final AuthKey response = await _gateway.getKey();
       final authKey = CryptoUtil.decryptSync(
         Sodium.base642bin(response.encryptedKey),
-        _config.getKey(),
+        _config.getKey()!,
         Sodium.base642bin(response.header),
       );
       await _config.setAuthSecretKey(Sodium.bin2base64(authKey));

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 444 - 279
lib/services/user_service.dart


+ 90 - 4
lib/theme/text_style.dart

@@ -5,6 +5,18 @@ const FontWeight _regularWeight = FontWeight.w500;
 const FontWeight _boldWeight = FontWeight.w600;
 const String _fontFamily = 'Inter';
 
+const TextStyle brandStyleSmall = TextStyle(
+  fontWeight: FontWeight.bold,
+  fontFamily: 'Montserrat',
+  fontSize: 21,
+);
+
+const TextStyle brandStyleMedium = TextStyle(
+  fontWeight: FontWeight.bold,
+  fontFamily: 'Montserrat',
+  fontSize: 24,
+);
+
 const TextStyle h1 = TextStyle(
   fontSize: 48,
   height: 48 / 28,
@@ -31,7 +43,7 @@ const TextStyle large = TextStyle(
 );
 const TextStyle body = TextStyle(
   fontSize: 16,
-  height: 19.4 / 16.0,
+  height: 20 / 16.0,
   fontWeight: _regularWeight,
   fontFamily: _fontFamily,
 );
@@ -71,6 +83,29 @@ class EnteTextTheme {
   final TextStyle miniBold;
   final TextStyle tiny;
   final TextStyle tinyBold;
+  final TextStyle brandSmall;
+  final TextStyle brandMedium;
+
+  // textMuted variants
+  final TextStyle h1Muted;
+  final TextStyle h2Muted;
+  final TextStyle h3Muted;
+  final TextStyle largeMuted;
+  final TextStyle bodyMuted;
+  final TextStyle smallMuted;
+  final TextStyle miniMuted;
+  final TextStyle miniBoldMuted;
+  final TextStyle tinyMuted;
+
+  // textFaint variants
+  final TextStyle h1Faint;
+  final TextStyle h2Faint;
+  final TextStyle h3Faint;
+  final TextStyle largeFaint;
+  final TextStyle bodyFaint;
+  final TextStyle smallFaint;
+  final TextStyle miniFaint;
+  final TextStyle tinyFaint;
 
   const EnteTextTheme({
     required this.h1,
@@ -89,13 +124,45 @@ class EnteTextTheme {
     required this.miniBold,
     required this.tiny,
     required this.tinyBold,
+    required this.brandSmall,
+    required this.brandMedium,
+    required this.h1Muted,
+    required this.h2Muted,
+    required this.h3Muted,
+    required this.largeMuted,
+    required this.bodyMuted,
+    required this.smallMuted,
+    required this.miniMuted,
+    required this.miniBoldMuted,
+    required this.tinyMuted,
+    required this.h1Faint,
+    required this.h2Faint,
+    required this.h3Faint,
+    required this.largeFaint,
+    required this.bodyFaint,
+    required this.smallFaint,
+    required this.miniFaint,
+    required this.tinyFaint,
   });
 }
 
-EnteTextTheme lightTextTheme = _buildEnteTextStyle(textBaseLight);
-EnteTextTheme darkTextTheme = _buildEnteTextStyle(textBaseDark);
+EnteTextTheme lightTextTheme = _buildEnteTextStyle(
+  textBaseLight,
+  textMutedLight,
+  textFaintLight,
+);
+
+EnteTextTheme darkTextTheme = _buildEnteTextStyle(
+  textBaseDark,
+  textMutedDark,
+  textFaintDark,
+);
 
-EnteTextTheme _buildEnteTextStyle(Color color) {
+EnteTextTheme _buildEnteTextStyle(
+    Color color,
+    Color textMuted,
+    Color textFaint,
+    ) {
   return EnteTextTheme(
     h1: h1.copyWith(color: color),
     h1Bold: h1.copyWith(color: color, fontWeight: _boldWeight),
@@ -113,5 +180,24 @@ EnteTextTheme _buildEnteTextStyle(Color color) {
     miniBold: mini.copyWith(color: color, fontWeight: _boldWeight),
     tiny: tiny.copyWith(color: color),
     tinyBold: tiny.copyWith(color: color, fontWeight: _boldWeight),
+    brandSmall: brandStyleSmall.copyWith(color: color),
+    brandMedium: brandStyleMedium.copyWith(color: color),
+    h1Muted: h1.copyWith(color: textMuted),
+    h2Muted: h2.copyWith(color: textMuted),
+    h3Muted: h3.copyWith(color: textMuted),
+    largeMuted: large.copyWith(color: textMuted),
+    bodyMuted: body.copyWith(color: textMuted),
+    smallMuted: small.copyWith(color: textMuted),
+    miniMuted: mini.copyWith(color: textMuted),
+    miniBoldMuted: mini.copyWith(color: textMuted, fontWeight: _boldWeight),
+    tinyMuted: tiny.copyWith(color: textMuted),
+    h1Faint: h1.copyWith(color: textFaint),
+    h2Faint: h2.copyWith(color: textFaint),
+    h3Faint: h3.copyWith(color: textFaint),
+    largeFaint: large.copyWith(color: textFaint),
+    bodyFaint: body.copyWith(color: textFaint),
+    smallFaint: small.copyWith(color: textFaint),
+    miniFaint: mini.copyWith(color: textFaint),
+    tinyFaint: tiny.copyWith(color: textFaint),
   );
 }

+ 24 - 3
lib/ui/account/login_page.dart

@@ -1,10 +1,13 @@
 import 'package:email_validator/email_validator.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/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:flutter/material.dart';
+import 'package:logging/logging.dart';
 import "package:styled_text/styled_text.dart";
 
 class LoginPage extends StatefulWidget {
@@ -19,6 +22,7 @@ class _LoginPageState extends State<LoginPage> {
   bool _emailIsValid = false;
   String? _email;
   Color? _emailInputFieldColor;
+  final Logger _logger = Logger('_LoginPageState');
 
   @override
   void initState() {
@@ -55,10 +59,27 @@ class _LoginPageState extends State<LoginPage> {
         isKeypadOpen: isKeypadOpen,
         isFormValid: _emailIsValid,
         buttonText: context.l10n.logInLabel,
-        onPressedFunction: () {
+        onPressedFunction: () async {
           UserService.instance.setEmail(_email!);
-          UserService.instance
-              .sendOtt(context, _email!, isCreateAccountScreen: false);
+          try {
+            final attr = await UserService.instance.getSrpAttributes(_email!);
+            Navigator.of(context).push(
+              MaterialPageRoute(
+                builder: (BuildContext context) {
+                  return LoginPasswordVerificationPage(
+                    srpAttributes: attr,
+                  );
+                },
+              ),
+            );
+          }
+          catch (e) {
+            if(e is! SrpSetupNotCompleteError) {
+              _logger.warning('Error getting SRP attributes', e);
+            }
+            await UserService.instance
+                .sendOtt(context, _email!, isCreateAccountScreen: false);
+          }
           FocusScope.of(context).unfocus();
         },
       ),

+ 232 - 0
lib/ui/account/login_pwd_verification_page.dart

@@ -0,0 +1,232 @@
+
+import 'package:ente_auth/core/configuration.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";
+import "package:ente_auth/theme/ente_theme.dart";
+import 'package:ente_auth/ui/common/dynamic_fab.dart';
+import "package:ente_auth/utils/dialog_util.dart";
+import 'package:flutter/material.dart';
+import "package:logging/logging.dart";
+
+// LoginPasswordVerificationPage is a page that allows the user to enter their password to verify their identity.
+// If the password is correct, then the user is either directed to
+// PasswordReentryPage (if the user has not yet set up 2FA) or TwoFactorAuthenticationPage (if the user has set up 2FA).
+// In the PasswordReentryPage, the password is auto-filled based on the
+// volatile password.
+class LoginPasswordVerificationPage extends StatefulWidget {
+  final SrpAttributes srpAttributes;
+  const LoginPasswordVerificationPage({Key? key, required this.srpAttributes}) : super(key: key);
+
+  @override
+  State<LoginPasswordVerificationPage> createState() => _LoginPasswordVerificationPageState();
+}
+
+class _LoginPasswordVerificationPageState extends
+State<LoginPasswordVerificationPage> {
+  final _logger = Logger((_LoginPasswordVerificationPageState).toString());
+  final _passwordController = TextEditingController();
+  final FocusNode _passwordFocusNode = FocusNode();
+  String? email;
+  bool _passwordInFocus = false;
+  bool _passwordVisible = false;
+
+  @override
+  void initState() {
+    super.initState();
+    email = Configuration.instance.getEmail();
+    _passwordFocusNode.addListener(() {
+      setState(() {
+        _passwordInFocus = _passwordFocusNode.hasFocus;
+      });
+    });
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    final isKeypadOpen = MediaQuery.of(context).viewInsets.bottom > 100;
+
+    FloatingActionButtonLocation? fabLocation() {
+      if (isKeypadOpen) {
+        return null;
+      } else {
+        return FloatingActionButtonLocation.centerFloat;
+      }
+    }
+
+    return Scaffold(
+      resizeToAvoidBottomInset: isKeypadOpen,
+      appBar: AppBar(
+        elevation: 0,
+        leading: IconButton(
+          icon: const Icon(Icons.arrow_back),
+          color: Theme.of(context).iconTheme.color,
+          onPressed: () {
+            Navigator.of(context).pop();
+          },
+        ),
+      ),
+      body: _getBody(),
+      floatingActionButton: DynamicFAB(
+        key: const ValueKey("verifyPasswordButton"),
+        isKeypadOpen: isKeypadOpen,
+        isFormValid: _passwordController.text.isNotEmpty,
+        buttonText: context.l10n.logInLabel,
+        onPressedFunction: () async {
+          FocusScope.of(context).unfocus();
+          await UserService.instance.verifyEmailViaPassword(context, widget
+              .srpAttributes,
+              _passwordController.text,);
+        },
+      ),
+      floatingActionButtonLocation: fabLocation(),
+      floatingActionButtonAnimator: NoScalingAnimation(),
+    );
+  }
+
+  Widget _getBody() {
+    return Column(
+      children: [
+        Expanded(
+          child: AutofillGroup(
+            child: ListView(
+              children: [
+                Padding(
+                  padding:
+                  const EdgeInsets.only(top: 30, left: 20, right: 20),
+                  child: Text(
+                    context.l10n.enterPassword,
+                    style: Theme.of(context).textTheme.headlineMedium,
+                  ),
+                ),
+                Padding(
+                  padding: const EdgeInsets.only(bottom: 30, left: 22, right:
+                  20,),
+                  child: Text(email ?? '', style: getEnteTextTheme(context).smallMuted,),
+                ),
+                Visibility(
+                  // hidden textForm for suggesting auto-fill service for saving
+                  // password
+                  visible: false,
+                  child: TextFormField(
+                    autofillHints: const [
+                      AutofillHints.email,
+                    ],
+                    autocorrect: false,
+                    keyboardType: TextInputType.emailAddress,
+                    initialValue: email,
+                    textInputAction: TextInputAction.next,
+                  ),
+                ),
+                Padding(
+                  padding: const EdgeInsets.fromLTRB(20, 24, 20, 0),
+                  child: TextFormField(
+                    key: const ValueKey("passwordInputField"),
+                    autofillHints: const [AutofillHints.password],
+                    decoration: InputDecoration(
+                      hintText: context.l10n.enterYourPasswordHint,
+                      filled: true,
+                      contentPadding: const EdgeInsets.all(20),
+                      border: UnderlineInputBorder(
+                        borderSide: BorderSide.none,
+                        borderRadius: BorderRadius.circular(6),
+                      ),
+                      suffixIcon: _passwordInFocus
+                          ? IconButton(
+                        icon: Icon(
+                          _passwordVisible
+                              ? Icons.visibility
+                              : Icons.visibility_off,
+                          color: Theme.of(context).iconTheme.color,
+                          size: 20,
+                        ),
+                        onPressed: () {
+                          setState(() {
+                            _passwordVisible = !_passwordVisible;
+                          });
+                        },
+                      )
+                          : null,
+                    ),
+                    style: const TextStyle(
+                      fontSize: 14,
+                    ),
+                    controller: _passwordController,
+                    autofocus: true,
+                    autocorrect: false,
+                    obscureText: !_passwordVisible,
+                    keyboardType: TextInputType.visiblePassword,
+                    focusNode: _passwordFocusNode,
+                    onChanged: (_) {
+                      setState(() {});
+                    },
+                  ),
+                ),
+                const Padding(
+                  padding: EdgeInsets.symmetric(vertical: 18),
+                  child: Divider(
+                    thickness: 1,
+                  ),
+                ),
+                Padding(
+                  padding: const EdgeInsets.symmetric(horizontal: 20),
+                  child: Row(
+                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                    children: [
+                      GestureDetector(
+                        behavior: HitTestBehavior.opaque,
+                        onTap: () async {
+                          await UserService.instance
+                              .sendOtt(context, email!,
+                              isResetPasswordScreen: true,);
+                        },
+                        child: Center(
+                          child: Text(
+                            context.l10n.forgotPassword,
+                            style: Theme.of(context)
+                                .textTheme
+                                .titleMedium!
+                                .copyWith(
+                              fontSize: 14,
+                              decoration: TextDecoration.underline,
+                            ),
+                          ),
+                        ),
+                      ),
+                      GestureDetector(
+                        behavior: HitTestBehavior.opaque,
+                        onTap: () async {
+                          final dialog = createProgressDialog(
+                            context,
+                            context.l10n.pleaseWait,
+                          );
+                          await dialog.show();
+                          await Configuration.instance.logout();
+                          await dialog.hide();
+                          Navigator.of(context)
+                              .popUntil((route) => route.isFirst);
+                        },
+                        child: Center(
+                          child: Text(
+                            context.l10n.changeEmail,
+                            style: Theme.of(context)
+                                .textTheme
+                                .titleMedium!
+                                .copyWith(
+                              fontSize: 14,
+                              decoration: TextDecoration.underline,
+                            ),
+                          ),
+                        ),
+                      ),
+                    ],
+                  ),
+                )
+              ],
+            ),
+          ),
+        ),
+      ],
+    );
+  }
+}

+ 21 - 8
lib/ui/account/ott_verification_page.dart

@@ -10,11 +10,13 @@ class OTTVerificationPage extends StatefulWidget {
   final String email;
   final bool isChangeEmail;
   final bool isCreateAccountScreen;
+  final bool isResetPasswordScreen;
 
   const OTTVerificationPage(
     this.email, {
     this.isChangeEmail = false,
     this.isCreateAccountScreen = false,
+    this.isResetPasswordScreen = false,
     Key? key,
   }) : super(key: key);
 
@@ -78,7 +80,8 @@ class _OTTVerificationPageState extends State<OTTVerificationPage> {
             );
           } else {
             UserService.instance
-                .verifyEmail(context, _verificationCodeController.text);
+                .verifyEmail(context, _verificationCodeController.text,
+              isResettingPasswordScreen: widget.isResetPasswordScreen,);
           }
           FocusScope.of(context).unfocus();
         },
@@ -129,13 +132,21 @@ class _OTTVerificationPageState extends State<OTTVerificationPage> {
                             },
                           ),
                         ),
-                        Text(
-                          l10n.checkInboxAndSpamFolder,
-                          style: Theme.of(context)
-                              .textTheme
-                              .subtitle1!
-                              .copyWith(fontSize: 14),
-                        ),
+                        widget.isResetPasswordScreen
+                            ? Text(
+                                l10n.toResetVerifyEmail,
+                                style: Theme.of(context)
+                                    .textTheme
+                                    .titleMedium!
+                                    .copyWith(fontSize: 14),
+                              )
+                            : Text(
+                                l10n.checkInboxAndSpamFolder,
+                                style: Theme.of(context)
+                                    .textTheme
+                                    .subtitle1!
+                                    .copyWith(fontSize: 14),
+                              ),
                       ],
                     ),
                   ),
@@ -182,6 +193,8 @@ class _OTTVerificationPageState extends State<OTTVerificationPage> {
                         context,
                         widget.email,
                         isCreateAccountScreen: widget.isCreateAccountScreen,
+                        isChangeEmail: widget.isChangeEmail,
+                        isResetPasswordScreen: widget.isResetPasswordScreen,
                       );
                     },
                     child: Text(

+ 7 - 5
lib/ui/account/password_entry_page.dart

@@ -1,5 +1,6 @@
 import 'package:ente_auth/core/configuration.dart';
 import 'package:ente_auth/l10n/l10n.dart';
+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';
@@ -23,7 +24,7 @@ enum PasswordEntryMode {
 class PasswordEntryPage extends StatefulWidget {
   final PasswordEntryMode mode;
 
-  const PasswordEntryPage({this.mode = PasswordEntryMode.set, Key? key})
+  const PasswordEntryPage({required this.mode, Key? key})
       : super(key: key);
 
   @override
@@ -377,9 +378,9 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
         createProgressDialog(context, context.l10n.generatingEncryptionKeys);
     await dialog.show();
     try {
-      final keyAttributes = await Configuration.instance
-          .updatePassword(_passwordController1.text);
-      await UserService.instance.updateKeyAttributes(keyAttributes);
+      final result = await Configuration.instance
+          .getAttributesForNewPassword(_passwordController1.text);
+      await UserService.instance.updateKeyAttributes(result.item1, result.item2);
       await dialog.hide();
       showShortToast(context, context.l10n.passwordChangedSuccessfully);
       Navigator.of(context).pop();
@@ -399,7 +400,7 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
         createProgressDialog(context, l10n.generatingEncryptionKeysTitle);
     await dialog.show();
     try {
-      final result = await Configuration.instance.generateKey(password);
+      final KeyGenResult result = await Configuration.instance.generateKey(password);
       Configuration.instance.setVolatilePassword(null);
       await dialog.hide();
       onDone() async {
@@ -408,6 +409,7 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
         try {
           await UserService.instance.setAttributes(result);
           await dialog.hide();
+          Configuration.instance.setVolatilePassword(null);
           Navigator.of(context).pushAndRemoveUntil(
             MaterialPageRoute(
               builder: (BuildContext context) {

+ 128 - 82
lib/ui/account/password_reentry_page.dart

@@ -1,12 +1,15 @@
 import 'dart:async';
+import 'dart:typed_data';
 
 import 'package:ente_auth/core/configuration.dart';
 import 'package:ente_auth/core/errors.dart';
 import 'package:ente_auth/l10n/l10n.dart';
+import 'package:ente_auth/services/user_service.dart';
 import 'package:ente_auth/ui/account/recovery_page.dart';
 import 'package:ente_auth/ui/common/dynamic_fab.dart';
 import 'package:ente_auth/ui/components/buttons/button_widget.dart';
 import 'package:ente_auth/ui/home_page.dart';
+import 'package:ente_auth/utils/crypto_util.dart';
 import 'package:ente_auth/utils/dialog_util.dart';
 import 'package:ente_auth/utils/email_util.dart';
 import 'package:flutter/material.dart';
@@ -26,11 +29,20 @@ class _PasswordReentryPageState extends State<PasswordReentryPage> {
   String? email;
   bool _passwordInFocus = false;
   bool _passwordVisible = false;
+  String? _volatilePassword;
 
   @override
   void initState() {
     super.initState();
     email = Configuration.instance.getEmail();
+    _volatilePassword = Configuration.instance.getVolatilePassword();
+    if (_volatilePassword != null) {
+      _passwordController.text = _volatilePassword!;
+      Future.delayed(
+        Duration.zero,
+            () => verifyPassword(_volatilePassword!),
+      );
+    }
     _passwordFocusNode.addListener(() {
       setState(() {
         _passwordInFocus = _passwordFocusNode.hasFocus;
@@ -64,68 +76,13 @@ class _PasswordReentryPageState extends State<PasswordReentryPage> {
       ),
       body: _getBody(),
       floatingActionButton: DynamicFAB(
+        key: const ValueKey("verifyPasswordButton"),
         isKeypadOpen: isKeypadOpen,
         isFormValid: _passwordController.text.isNotEmpty,
         buttonText: context.l10n.verifyPassword,
         onPressedFunction: () async {
           FocusScope.of(context).unfocus();
-          final dialog = createProgressDialog(context, context.l10n.pleaseWait);
-          await dialog.show();
-          try {
-            await Configuration.instance.decryptAndSaveSecrets(
-              _passwordController.text,
-              Configuration.instance.getKeyAttributes()!,
-            );
-          } on KeyDerivationError catch (e, s) {
-            _logger.severe("Password verification failed", e, s);
-            await dialog.hide();
-            final dialogChoice = await showChoiceDialog(
-              context,
-              title: context.l10n.recreatePasswordTitle,
-              body: context.l10n.recreatePasswordBody,
-              firstButtonLabel: context.l10n.useRecoveryKey,
-            );
-            if (dialogChoice!.action == ButtonAction.first) {
-              Navigator.of(context).push(
-                MaterialPageRoute(
-                  builder: (BuildContext context) {
-                    return const RecoveryPage();
-                  },
-                ),
-              );
-            }
-            return;
-          } catch (e, s) {
-            _logger.severe("Password verification failed", e, s);
-            await dialog.hide();
-            final dialogChoice = await showChoiceDialog(
-              context,
-              title: context.l10n.incorrectPasswordTitle,
-              body: context.l10n.pleaseTryAgain,
-              firstButtonLabel: context.l10n.contactSupport,
-              secondButtonLabel: context.l10n.ok,
-            );
-            if (dialogChoice!.action == ButtonAction.first) {
-              await sendLogs(
-                context,
-                context.l10n.contactSupport,
-                "support@ente.io",
-                postShare: () {},
-              );
-            }
-            return;
-          }
-          await dialog.hide();
-          unawaited(
-            Navigator.of(context).pushAndRemoveUntil(
-              MaterialPageRoute(
-                builder: (BuildContext context) {
-                  return const HomePage();
-                },
-              ),
-              (route) => false,
-            ),
-          );
+          await verifyPassword(_passwordController.text);
         },
       ),
       floatingActionButtonLocation: fabLocation(),
@@ -133,6 +90,90 @@ class _PasswordReentryPageState extends State<PasswordReentryPage> {
     );
   }
 
+  Future<void> verifyPassword(String password) async {
+    FocusScope.of(context).unfocus();
+    final dialog =
+    createProgressDialog(context, context.l10n.pleaseWait);
+    await dialog.show();
+    try {
+      final kek = await Configuration.instance.decryptSecretsAndGetKeyEncKey(
+        password,
+        Configuration.instance.getKeyAttributes()!,
+      );
+      _registerSRPForExistingUsers(kek).ignore();
+    } on KeyDerivationError catch (e, s) {
+      _logger.severe("Password verification failed", e, s);
+      await dialog.hide();
+      final dialogChoice = await showChoiceDialog(
+        context,
+        title: context.l10n.recreatePasswordTitle,
+        body: context.l10n.recreatePasswordBody,
+        firstButtonLabel: context.l10n.useRecoveryKey,
+      );
+      if (dialogChoice!.action == ButtonAction.first) {
+        Navigator.of(context).push(
+          MaterialPageRoute(
+            builder: (BuildContext context) {
+              return const RecoveryPage();
+            },
+          ),
+        );
+      }
+      return;
+    } catch (e, s) {
+      _logger.severe("Password verification failed", e, s);
+      await dialog.hide();
+      final dialogChoice = await showChoiceDialog(
+        context,
+        title: context.l10n.incorrectPasswordTitle,
+        body: context.l10n.pleaseTryAgain,
+        firstButtonLabel: context.l10n.contactSupport,
+        secondButtonLabel: context.l10n.ok,
+      );
+      if (dialogChoice!.action == ButtonAction.first) {
+        await sendLogs(
+          context,
+          context.l10n.contactSupport,
+          "support@ente.io",
+          postShare: () {},
+        );
+      }
+      return;
+    }
+    await dialog.hide();
+    Configuration.instance.setVolatilePassword(null);
+    unawaited(
+      Navigator.of(context).pushAndRemoveUntil(
+        MaterialPageRoute(
+          builder: (BuildContext context) {
+            return const HomePage();
+          },
+        ),
+            (route) => false,
+      ),
+    );
+  }
+
+  Future<void> _registerSRPForExistingUsers(Uint8List key) async {
+    bool shouldSetupSRP = false;
+    try {
+      // ignore: unused_local_variable
+      final attr = await UserService.instance.getSrpAttributes(email!);
+    } on SrpSetupNotCompleteError {
+      shouldSetupSRP = true;
+    } catch (e, s) {
+      _logger.severe("error while fetching attr", e, s);
+    }
+    if (shouldSetupSRP) {
+      try {
+        final Uint8List loginKey = await CryptoUtil.deriveLoginKey(key);
+        await UserService.instance.registerOrUpdateSrp(loginKey);
+      } catch (e, s) {
+        _logger.severe("error while setting up srp for existing users", e, s);
+      }
+    }
+  }
+
   Widget _getBody() {
     return Column(
       children: [
@@ -142,10 +183,10 @@ class _PasswordReentryPageState extends State<PasswordReentryPage> {
               children: [
                 Padding(
                   padding:
-                      const EdgeInsets.symmetric(vertical: 30, horizontal: 20),
+                  const EdgeInsets.symmetric(vertical: 30, horizontal: 20),
                   child: Text(
                     context.l10n.welcomeBack,
-                    style: Theme.of(context).textTheme.headline4,
+                    style: Theme.of(context).textTheme.headlineMedium,
                   ),
                 ),
                 Visibility(
@@ -165,6 +206,7 @@ class _PasswordReentryPageState extends State<PasswordReentryPage> {
                 Padding(
                   padding: const EdgeInsets.fromLTRB(20, 24, 20, 0),
                   child: TextFormField(
+                    key: const ValueKey("passwordInputField"),
                     autofillHints: const [AutofillHints.password],
                     decoration: InputDecoration(
                       hintText: context.l10n.enterYourPasswordHint,
@@ -176,19 +218,19 @@ class _PasswordReentryPageState extends State<PasswordReentryPage> {
                       ),
                       suffixIcon: _passwordInFocus
                           ? IconButton(
-                              icon: Icon(
-                                _passwordVisible
-                                    ? Icons.visibility
-                                    : Icons.visibility_off,
-                                color: Theme.of(context).iconTheme.color,
-                                size: 20,
-                              ),
-                              onPressed: () {
-                                setState(() {
-                                  _passwordVisible = !_passwordVisible;
-                                });
-                              },
-                            )
+                        icon: Icon(
+                          _passwordVisible
+                              ? Icons.visibility
+                              : Icons.visibility_off,
+                          color: Theme.of(context).iconTheme.color,
+                          size: 20,
+                        ),
+                        onPressed: () {
+                          setState(() {
+                            _passwordVisible = !_passwordVisible;
+                          });
+                        },
+                      )
                           : null,
                     ),
                     style: const TextStyle(
@@ -230,11 +272,13 @@ class _PasswordReentryPageState extends State<PasswordReentryPage> {
                         child: Center(
                           child: Text(
                             context.l10n.forgotPassword,
-                            style:
-                                Theme.of(context).textTheme.subtitle1!.copyWith(
-                                      fontSize: 14,
-                                      decoration: TextDecoration.underline,
-                                    ),
+                            style: Theme.of(context)
+                                .textTheme
+                                .titleMedium!
+                                .copyWith(
+                              fontSize: 14,
+                              decoration: TextDecoration.underline,
+                            ),
                           ),
                         ),
                       ),
@@ -254,11 +298,13 @@ class _PasswordReentryPageState extends State<PasswordReentryPage> {
                         child: Center(
                           child: Text(
                             context.l10n.changeEmail,
-                            style:
-                                Theme.of(context).textTheme.subtitle1!.copyWith(
-                                      fontSize: 14,
-                                      decoration: TextDecoration.underline,
-                                    ),
+                            style: Theme.of(context)
+                                .textTheme
+                                .titleMedium!
+                                .copyWith(
+                              fontSize: 14,
+                              decoration: TextDecoration.underline,
+                            ),
                           ),
                         ),
                       ),

+ 212 - 77
lib/utils/crypto_util.dart

@@ -1,7 +1,9 @@
+import 'dart:convert';
 import 'dart:io' as io;
 import 'dart:typed_data';
 
 import 'package:computer/computer.dart';
+import 'package:ente_auth/core/errors.dart';
 import 'package:ente_auth/models/derived_key_result.dart';
 import 'package:ente_auth/models/encryption_result.dart';
 import 'package:ente_auth/utils/device_info.dart';
@@ -11,6 +13,10 @@ import 'package:logging/logging.dart';
 const int encryptionChunkSize = 4 * 1024 * 1024;
 final int decryptionChunkSize =
     encryptionChunkSize + Sodium.cryptoSecretstreamXchacha20poly1305Abytes;
+const int hashChunkSize = 4 * 1024 * 1024;
+const int loginSubKeyLen = 32;
+const int loginSubKeyId = 1;
+const String loginSubKeyContext = "loginctx";
 
 Uint8List cryptoSecretboxEasy(Map<String, dynamic> args) {
   return Sodium.cryptoSecretboxEasy(args["source"], args["nonce"], args["key"]);
@@ -31,35 +37,47 @@ Uint8List cryptoPwHash(Map<String, dynamic> args) {
     args["salt"],
     args["opsLimit"],
     args["memLimit"],
-    Sodium.cryptoPwhashAlgDefault,
+    Sodium.cryptoPwhashAlgArgon2id13,
   );
 }
 
-Uint8List cryptoGenericHash(Map<String, dynamic> args) {
+Uint8List cryptoKdfDeriveFromKey(
+    Map<String, dynamic> args,
+    ) {
+  return Sodium.cryptoKdfDeriveFromKey(
+    args["subkeyLen"],
+    args["subkeyId"],
+    args["context"],
+    args["key"],
+  );
+}
+
+// Returns the hash for a given file, chunking it in batches of hashChunkSize
+Future<Uint8List> cryptoGenericHash(Map<String, dynamic> args) async {
   final sourceFile = io.File(args["sourceFilePath"]);
-  final sourceFileLength = sourceFile.lengthSync();
+  final sourceFileLength = await sourceFile.length();
   final inputFile = sourceFile.openSync(mode: io.FileMode.read);
   final state =
-      Sodium.cryptoGenerichashInit(null, Sodium.cryptoGenerichashBytesMax);
+  Sodium.cryptoGenerichashInit(null, Sodium.cryptoGenerichashBytesMax);
   var bytesRead = 0;
   bool isDone = false;
   while (!isDone) {
-    var chunkSize = encryptionChunkSize;
+    var chunkSize = hashChunkSize;
     if (bytesRead + chunkSize >= sourceFileLength) {
       chunkSize = sourceFileLength - bytesRead;
       isDone = true;
     }
-    final buffer = inputFile.readSync(chunkSize);
+    final buffer = await inputFile.read(chunkSize);
     bytesRead += chunkSize;
     Sodium.cryptoGenerichashUpdate(state, buffer);
   }
-  inputFile.closeSync();
+  await inputFile.close();
   return Sodium.cryptoGenerichashFinal(state, Sodium.cryptoGenerichashBytesMax);
 }
 
 EncryptionResult chachaEncryptData(Map<String, dynamic> args) {
   final initPushResult =
-      Sodium.cryptoSecretstreamXchacha20poly1305InitPush(args["key"]);
+  Sodium.cryptoSecretstreamXchacha20poly1305InitPush(args["key"]);
   final encryptedData = Sodium.cryptoSecretstreamXchacha20poly1305Push(
     initPushResult.state,
     args["source"],
@@ -72,6 +90,7 @@ EncryptionResult chachaEncryptData(Map<String, dynamic> args) {
   );
 }
 
+// Encrypts a given file, in chunks of encryptionChunkSize
 Future<EncryptionResult> chachaEncryptFile(Map<String, dynamic> args) async {
   final encryptionStartTime = DateTime.now().millisecondsSinceEpoch;
   final logger = Logger("ChaChaEncrypt");
@@ -83,7 +102,7 @@ Future<EncryptionResult> chachaEncryptFile(Map<String, dynamic> args) async {
   final inputFile = sourceFile.openSync(mode: io.FileMode.read);
   final key = args["key"] ?? Sodium.cryptoSecretstreamXchacha20poly1305Keygen();
   final initPushResult =
-      Sodium.cryptoSecretstreamXchacha20poly1305InitPush(key);
+  Sodium.cryptoSecretstreamXchacha20poly1305InitPush(key);
   var bytesRead = 0;
   var tag = Sodium.cryptoSecretstreamXchacha20poly1305TagMessage;
   while (tag != Sodium.cryptoSecretstreamXchacha20poly1305TagFinal) {
@@ -92,7 +111,7 @@ Future<EncryptionResult> chachaEncryptFile(Map<String, dynamic> args) async {
       chunkSize = sourceFileLength - bytesRead;
       tag = Sodium.cryptoSecretstreamXchacha20poly1305TagFinal;
     }
-    final buffer = inputFile.readSync(chunkSize);
+    final buffer = await inputFile.read(chunkSize);
     bytesRead += chunkSize;
     final encryptedData = Sodium.cryptoSecretstreamXchacha20poly1305Push(
       initPushResult.state,
@@ -102,7 +121,7 @@ Future<EncryptionResult> chachaEncryptFile(Map<String, dynamic> args) async {
     );
     await destinationFile.writeAsBytes(encryptedData, mode: io.FileMode.append);
   }
-  inputFile.closeSync();
+  await inputFile.close();
 
   logger.info(
     "Encryption time: " +
@@ -134,10 +153,10 @@ Future<void> chachaDecryptFile(Map<String, dynamic> args) async {
     if (bytesRead + chunkSize >= sourceFileLength) {
       chunkSize = sourceFileLength - bytesRead;
     }
-    final buffer = inputFile.readSync(chunkSize);
+    final buffer = await inputFile.read(chunkSize);
     bytesRead += chunkSize;
     final pullResult =
-        Sodium.cryptoSecretstreamXchacha20poly1305Pull(pullState, buffer, null);
+    Sodium.cryptoSecretstreamXchacha20poly1305Pull(pullState, buffer, null);
     await destinationFile.writeAsBytes(pullResult.m, mode: io.FileMode.append);
     tag = pullResult.tag;
   }
@@ -164,13 +183,43 @@ Uint8List chachaDecryptData(Map<String, dynamic> args) {
 }
 
 class CryptoUtil {
-  static final Computer _computer = Computer();
+  // Note: workers are turned on during app startup.
+  static final Computer _computer = Computer.shared();
 
   static init() {
-    _computer.turnOn(workersCount: 4);
-    // Sodium.init();
+    Sodium.init();
+  }
+
+  static Uint8List base642bin(String b64, {
+    String? ignore,
+    int variant = Sodium.base64VariantOriginal,
+  }) {
+    return Sodium.base642bin(b64, ignore: ignore, variant: variant);
+  }
+
+  static String bin2base64(Uint8List bin, {
+    bool urlSafe = false,
+  }) {
+    return Sodium.bin2base64(
+      bin,
+      variant:
+      urlSafe ? Sodium.base64VariantUrlsafe : Sodium.base64VariantOriginal,
+    );
+  }
+
+  static String bin2hex(Uint8List bin) {
+    return Sodium.bin2hex(bin);
+  }
+
+  static Uint8List hex2bin(String hex) {
+    return Sodium.hex2bin(hex);
   }
 
+  // Encrypts the given source, with the given key and a randomly generated
+  // nonce, using XSalsa20 (w Poly1305 MAC).
+  // This function runs on the same thread as the caller, so should be used only
+  // for small amounts of data where thread switching can result in a degraded
+  // user experience
   static EncryptionResult encryptSync(Uint8List source, Uint8List key) {
     final nonce = Sodium.randombytesBuf(Sodium.cryptoSecretboxNoncebytes);
 
@@ -186,24 +235,30 @@ class CryptoUtil {
     );
   }
 
-  static Future<Uint8List> decrypt(
-    Uint8List cipher,
-    Uint8List key,
-    Uint8List nonce,
-  ) async {
+  // Decrypts the given cipher, with the given key and nonce using XSalsa20
+  // (w Poly1305 MAC).
+  static Future<Uint8List> decrypt(Uint8List cipher,
+      Uint8List key,
+      Uint8List nonce,) async {
     final args = <String, dynamic>{};
     args["cipher"] = cipher;
     args["nonce"] = nonce;
     args["key"] = key;
-    return _computer.compute(cryptoSecretboxOpenEasy, param: args);
+    return _computer.compute(
+      cryptoSecretboxOpenEasy,
+      param: args,
+      taskName: "decrypt",
+    );
   }
 
-  static Uint8List decryptSync(
-    Uint8List cipher,
-    Uint8List? key,
-    Uint8List nonce,
-  ) {
-    assert(key != null, "key can not be null");
+  // Decrypts the given cipher, with the given key and nonce using XSalsa20
+  // (w Poly1305 MAC).
+  // This function runs on the same thread as the caller, so should be used only
+  // for small amounts of data where thread switching can result in a degraded
+  // user experience
+  static Uint8List decryptSync(Uint8List cipher,
+      Uint8List key,
+      Uint8List nonce,) {
     final args = <String, dynamic>{};
     args["cipher"] = cipher;
     args["nonce"] = nonce;
@@ -211,94 +266,130 @@ class CryptoUtil {
     return cryptoSecretboxOpenEasy(args);
   }
 
-  static Future<EncryptionResult> encryptChaCha(
-    Uint8List source,
-    Uint8List key,
-  ) async {
+  // Encrypts the given source, with the given key and a randomly generated
+  // nonce, using XChaCha20 (w Poly1305 MAC).
+  // This function runs on the isolate pool held by `_computer`.
+  // TODO: Remove "ChaCha", an implementation detail from the function name
+  static Future<EncryptionResult> encryptChaCha(Uint8List source,
+      Uint8List key,) async {
     final args = <String, dynamic>{};
     args["source"] = source;
     args["key"] = key;
-    return _computer.compute(chachaEncryptData, param: args);
+    return _computer.compute(
+      chachaEncryptData,
+      param: args,
+      taskName: "encryptChaCha",
+    );
   }
 
-  static Future<Uint8List> decryptChaCha(
-    Uint8List source,
-    Uint8List key,
-    Uint8List header,
-  ) async {
+  // Decrypts the given source, with the given key and header using XChaCha20
+  // (w Poly1305 MAC).
+  // TODO: Remove "ChaCha", an implementation detail from the function name
+  static Future<Uint8List> decryptChaCha(Uint8List source,
+      Uint8List key,
+      Uint8List header,) async {
     final args = <String, dynamic>{};
     args["source"] = source;
     args["key"] = key;
     args["header"] = header;
-    return _computer.compute(chachaDecryptData, param: args);
+    return _computer.compute(
+      chachaDecryptData,
+      param: args,
+      taskName: "decryptChaCha",
+    );
   }
 
+  // Encrypts the file at sourceFilePath, with the key (if provided) and a
+  // randomly generated nonce using XChaCha20 (w Poly1305 MAC), and writes it
+  // to the destinationFilePath.
+  // If a key is not provided, one is generated and returned.
   static Future<EncryptionResult> encryptFile(
-    String sourceFilePath,
-    String destinationFilePath, {
-    Uint8List? key,
-  }) {
+      String sourceFilePath,
+      String destinationFilePath, {
+        Uint8List? key,
+      }) {
     final args = <String, dynamic>{};
     args["sourceFilePath"] = sourceFilePath;
     args["destinationFilePath"] = destinationFilePath;
     args["key"] = key;
-    return _computer.compute(chachaEncryptFile, param: args);
+    return _computer.compute(
+      chachaEncryptFile,
+      param: args,
+      taskName: "encryptFile",
+    );
   }
 
+  // Decrypts the file at sourceFilePath, with the given key and header using
+  // XChaCha20 (w Poly1305 MAC), and writes it to the destinationFilePath.
   static Future<void> decryptFile(
-    String sourceFilePath,
-    String destinationFilePath,
-    Uint8List header,
-    Uint8List key,
-  ) {
+      String sourceFilePath,
+      String destinationFilePath,
+      Uint8List header,
+      Uint8List key,) {
     final args = <String, dynamic>{};
     args["sourceFilePath"] = sourceFilePath;
     args["destinationFilePath"] = destinationFilePath;
     args["header"] = header;
     args["key"] = key;
-    return _computer.compute(chachaDecryptFile, param: args);
+    return _computer.compute(
+      chachaDecryptFile,
+      param: args,
+      taskName: "decryptFile",
+    );
   }
 
+  // Generates and returns a 256-bit key.
   static Uint8List generateKey() {
     return Sodium.cryptoSecretboxKeygen();
   }
 
+  // Generates and returns a random byte buffer of length
+  // crypto_pwhash_SALTBYTES (16)
   static Uint8List getSaltToDeriveKey() {
     return Sodium.randombytesBuf(Sodium.cryptoPwhashSaltbytes);
   }
 
+  // Generates and returns a secret key and the corresponding public key.
   static Future<KeyPair> generateKeyPair() async {
     return Sodium.cryptoBoxKeypair();
   }
 
+  // Decrypts the input using the given publicKey-secretKey pair
   static Uint8List openSealSync(
-    Uint8List input,
-    Uint8List publicKey,
-    Uint8List secretKey,
-  ) {
+      Uint8List input,
+      Uint8List publicKey,
+      Uint8List secretKey,
+      ) {
     return Sodium.cryptoBoxSealOpen(input, publicKey, secretKey);
   }
 
+  // Encrypts the input using the given publicKey
   static Uint8List sealSync(Uint8List input, Uint8List publicKey) {
     return Sodium.cryptoBoxSeal(input, publicKey);
   }
 
+  // Derives a key for a given password and salt using Argon2id, v1.3.
+  // The function first attempts to derive a key with both memLimit and opsLimit
+  // set to their Sensitive variants.
+  // If this fails, say on a device with insufficient RAM, we retry by halving
+  // the memLimit and doubling the opsLimit, while ensuring that we stay within
+  // the min and max limits for both parameters.
+  // At all points, we ensure that the product of these two variables (the area
+  // under the graph that determines the amount of work required) is a constant.
   static Future<DerivedKeyResult> deriveSensitiveKey(
-    Uint8List password,
-    Uint8List salt,
-  ) async {
+      Uint8List password,
+      Uint8List salt,
+      ) async {
     final logger = Logger("pwhash");
-    // Default with 1 GB mem and 4 ops limit
     int memLimit = Sodium.cryptoPwhashMemlimitSensitive;
     int opsLimit = Sodium.cryptoPwhashOpslimitSensitive;
-
     if (await isLowSpecDevice()) {
       logger.info("low spec device detected");
       // When sensitive memLimit (1 GB) is used, on low spec device the OS might
-      // kill the app with OOM. To avoid that, start with 256 MB and 
+      // kill the app with OOM. To avoid that, start with 256 MB and
       // corresponding ops limit (16).
       // This ensures that the product of these two variables
-      // (the area under the graph that determines the amount of work required) 
+      // (the area under the graph that determines the amount of work required)
       // stays the same
       // SODIUM_CRYPTO_PWHASH_MEMLIMIT_SENSITIVE: 1073741824
       // SODIUM_CRYPTO_PWHASH_MEMLIMIT_MODERATE: 268435456
@@ -315,11 +406,8 @@ class CryptoUtil {
         key = await deriveKey(password, salt, memLimit, opsLimit);
         return DerivedKeyResult(key, memLimit, opsLimit);
       } catch (e, s) {
-        logger.severe(
-          "failed to derive memLimit: $memLimit and opsLimit: $opsLimit",
-          e,
-          s,
-        );
+        logger.warning(
+          "failed to deriveKey mem: $memLimit, ops: $opsLimit", e, s,);
       }
       memLimit = (memLimit / 2).round();
       opsLimit = opsLimit * 2;
@@ -327,29 +415,76 @@ class CryptoUtil {
     throw UnsupportedError("Cannot perform this operation on this device");
   }
 
+  // Derives a key for the given password and salt, using Argon2id, v1.3
+  // with memory and ops limit hardcoded to their Interactive variants
+  // NOTE: This is only used while setting passwords for shared links, as an
+  // extra layer of authentication (atop the access token and collection key).
+  // More details @ https://ente.io/blog/building-shareable-links/
+  static Future<DerivedKeyResult> deriveInteractiveKey(
+      Uint8List password,
+      Uint8List salt,
+      ) async {
+    final int memLimit = Sodium.cryptoPwhashMemlimitInteractive;
+    final int opsLimit = Sodium.cryptoPwhashOpslimitInteractive;
+    final key = await deriveKey(password, salt, memLimit, opsLimit);
+    return DerivedKeyResult(key, memLimit, opsLimit);
+  }
+
+  // Derives a key for a given password, salt, memLimit and opsLimit using
+  // Argon2id, v1.3.
   static Future<Uint8List> deriveKey(
-    Uint8List password,
-    Uint8List salt,
-    int memLimit,
-    int opsLimit,
-  ) {
-    return _computer.compute(
-      cryptoPwHash,
+      Uint8List password,
+      Uint8List salt,
+      int memLimit,
+      int opsLimit,
+      ) {
+    try {
+      return _computer.compute(
+        cryptoPwHash,
+        param: {
+          "password": password,
+          "salt": salt,
+          "memLimit": memLimit,
+          "opsLimit": opsLimit,
+        },
+        taskName: "deriveKey",
+      );
+    } catch(e,s) {
+      final String errMessage = 'failed to deriveKey memLimit: $memLimit and '
+          'opsLimit: $opsLimit';
+      Logger("CryptoUtilDeriveKey").warning(errMessage, e, s);
+      throw KeyDerivationError();
+    }
+  }
+
+  // derives a Login key as subKey from the given key by applying KDF
+  // (Key Derivation Function) with the `loginSubKeyId` and
+  // `loginSubKeyLen` and `loginSubKeyContext` as context
+  static Future<Uint8List> deriveLoginKey(
+      Uint8List key,
+      ) async {
+    final Uint8List derivedKey = await  _computer.compute(
+      cryptoKdfDeriveFromKey,
       param: {
-        "password": password,
-        "salt": salt,
-        "memLimit": memLimit,
-        "opsLimit": opsLimit,
+        "key": key,
+        "subkeyId": loginSubKeyId,
+        "subkeyLen": loginSubKeyLen,
+        "context": utf8.encode(loginSubKeyContext),
       },
+      taskName: "deriveLoginKey",
     );
+    // return the first 16 bytes of the derived key
+    return derivedKey.sublist(0, 16);
   }
 
+  // Computes and returns the hash of the source file
   static Future<Uint8List> getHash(io.File source) {
     return _computer.compute(
       cryptoGenericHash,
       param: {
         "sourceFilePath": source.path,
       },
+      taskName: "fileHash",
     );
   }
 }

+ 1 - 1
macos/Podfile

@@ -1,4 +1,4 @@
-platform :osx, '10.11'
+platform :osx, '10.14'
 
 # CocoaPods analytics sends network stats synchronously affecting flutter build latency.
 ENV['COCOAPODS_DISABLE_STATS'] = 'true'

+ 41 - 16
macos/Podfile.lock

@@ -2,7 +2,11 @@ PODS:
   - connectivity_macos (0.0.1):
     - FlutterMacOS
     - Reachability
-  - flutter_secure_storage_macos (3.3.1):
+  - device_info_plus (0.0.1):
+    - FlutterMacOS
+  - flutter_local_notifications (0.0.1):
+    - FlutterMacOS
+  - flutter_secure_storage_macos (6.1.1):
     - FlutterMacOS
   - FlutterMacOS (1.0.0)
   - FMDB (2.7.5):
@@ -10,12 +14,19 @@ PODS:
   - FMDB/standard (2.7.5)
   - package_info_plus_macos (0.0.1):
     - FlutterMacOS
-  - path_provider_macos (0.0.1):
+  - path_provider_foundation (0.0.1):
+    - Flutter
     - FlutterMacOS
   - Reachability (3.2)
+  - Sentry/HybridSDK (7.31.5)
+  - sentry_flutter (0.0.1):
+    - Flutter
+    - FlutterMacOS
+    - Sentry/HybridSDK (= 7.31.5)
   - share_plus_macos (0.0.1):
     - FlutterMacOS
-  - shared_preferences_macos (0.0.1):
+  - shared_preferences_foundation (0.0.1):
+    - Flutter
     - FlutterMacOS
   - sqflite (0.0.2):
     - FlutterMacOS
@@ -25,12 +36,15 @@ PODS:
 
 DEPENDENCIES:
   - connectivity_macos (from `Flutter/ephemeral/.symlinks/plugins/connectivity_macos/macos`)
+  - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`)
+  - flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos`)
   - flutter_secure_storage_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos`)
   - FlutterMacOS (from `Flutter/ephemeral`)
   - package_info_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus_macos/macos`)
-  - path_provider_macos (from `Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos`)
+  - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/macos`)
+  - sentry_flutter (from `Flutter/ephemeral/.symlinks/plugins/sentry_flutter/macos`)
   - share_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/share_plus_macos/macos`)
-  - shared_preferences_macos (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_macos/macos`)
+  - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/macos`)
   - sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/macos`)
   - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
 
@@ -38,22 +52,29 @@ SPEC REPOS:
   trunk:
     - FMDB
     - Reachability
+    - Sentry
 
 EXTERNAL SOURCES:
   connectivity_macos:
     :path: Flutter/ephemeral/.symlinks/plugins/connectivity_macos/macos
+  device_info_plus:
+    :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos
+  flutter_local_notifications:
+    :path: Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos
   flutter_secure_storage_macos:
     :path: Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos
   FlutterMacOS:
     :path: Flutter/ephemeral
   package_info_plus_macos:
     :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus_macos/macos
-  path_provider_macos:
-    :path: Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos
+  path_provider_foundation:
+    :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/macos
+  sentry_flutter:
+    :path: Flutter/ephemeral/.symlinks/plugins/sentry_flutter/macos
   share_plus_macos:
     :path: Flutter/ephemeral/.symlinks/plugins/share_plus_macos/macos
-  shared_preferences_macos:
-    :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_macos/macos
+  shared_preferences_foundation:
+    :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/macos
   sqflite:
     :path: Flutter/ephemeral/.symlinks/plugins/sqflite/macos
   url_launcher_macos:
@@ -61,17 +82,21 @@ EXTERNAL SOURCES:
 
 SPEC CHECKSUMS:
   connectivity_macos: 5dae6ee11d320fac7c05f0d08bd08fc32b5514d9
-  flutter_secure_storage_macos: 6ceee8fbc7f484553ad17f79361b556259df89aa
-  FlutterMacOS: ae6af50a8ea7d6103d888583d46bd8328a7e9811
+  device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f
+  flutter_local_notifications: 3805ca215b2fb7f397d78b66db91f6a747af52e4
+  flutter_secure_storage_macos: d56e2d218c1130b262bef8b4a7d64f88d7f9c9ea
+  FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
   FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
   package_info_plus_macos: f010621b07802a241d96d01876d6705f15e77c1c
-  path_provider_macos: 3c0c3b4b0d4a76d2bf989a913c2de869c5641a19
+  path_provider_foundation: 37748e03f12783f9de2cb2c4eadfaa25fe6d4852
   Reachability: 33e18b67625424e47b6cde6d202dce689ad7af96
+  Sentry: 4c9babff9034785067c896fd580b1f7de44da020
+  sentry_flutter: b10ae7a5ddcbc7f04648eeb2672b5747230172f1
   share_plus_macos: 853ee48e7dce06b633998ca0735d482dd671ade4
-  shared_preferences_macos: a64dc611287ed6cbe28fd1297898db1336975727
+  shared_preferences_foundation: 297b3ebca31b34ec92be11acd7fb0ba932c822ca
   sqflite: a5789cceda41d54d23f31d6de539d65bb14100ea
-  url_launcher_macos: 597e05b8e514239626bcf4a850fcf9ef5c856ec3
+  url_launcher_macos: c04e4fa86382d4f94f6b38f14625708be3ae52e2
 
-PODFILE CHECKSUM: 6eac6b3292e5142cfc23bdeb71848a40ec51c14c
+PODFILE CHECKSUM: 353c8bcc5d5b0994e508d035b5431cfe18c1dea7
 
-COCOAPODS: 1.11.3
+COCOAPODS: 1.12.1

+ 5 - 4
macos/Runner.xcodeproj/project.pbxproj

@@ -3,7 +3,7 @@
 	archiveVersion = 1;
 	classes = {
 	};
-	objectVersion = 51;
+	objectVersion = 54;
 	objects = {
 
 /* Begin PBXAggregateTarget section */
@@ -256,6 +256,7 @@
 /* Begin PBXShellScriptBuildPhase section */
 		3399D490228B24CF009A79C7 /* ShellScript */ = {
 			isa = PBXShellScriptBuildPhase;
+			alwaysOutOfDate = 1;
 			buildActionMask = 2147483647;
 			files = (
 			);
@@ -404,7 +405,7 @@
 				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
 				GCC_WARN_UNUSED_FUNCTION = YES;
 				GCC_WARN_UNUSED_VARIABLE = YES;
-				MACOSX_DEPLOYMENT_TARGET = 10.11;
+				MACOSX_DEPLOYMENT_TARGET = 10.14;
 				MTL_ENABLE_DEBUG_INFO = NO;
 				SDKROOT = macosx;
 				SWIFT_COMPILATION_MODE = wholemodule;
@@ -483,7 +484,7 @@
 				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
 				GCC_WARN_UNUSED_FUNCTION = YES;
 				GCC_WARN_UNUSED_VARIABLE = YES;
-				MACOSX_DEPLOYMENT_TARGET = 10.11;
+				MACOSX_DEPLOYMENT_TARGET = 10.14;
 				MTL_ENABLE_DEBUG_INFO = YES;
 				ONLY_ACTIVE_ARCH = YES;
 				SDKROOT = macosx;
@@ -530,7 +531,7 @@
 				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
 				GCC_WARN_UNUSED_FUNCTION = YES;
 				GCC_WARN_UNUSED_VARIABLE = YES;
-				MACOSX_DEPLOYMENT_TARGET = 10.11;
+				MACOSX_DEPLOYMENT_TARGET = 10.14;
 				MTL_ENABLE_DEBUG_INFO = NO;
 				SDKROOT = macosx;
 				SWIFT_COMPILATION_MODE = wholemodule;

+ 170 - 161
pubspec.lock

@@ -5,42 +5,42 @@ packages:
     dependency: transitive
     description:
       name: _fe_analyzer_shared
-      sha256: "4897882604d919befd350648c7f91926a9d5de99e67b455bf0917cc2362f4bb8"
+      sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a
       url: "https://pub.dev"
     source: hosted
-    version: "47.0.0"
+    version: "61.0.0"
   adaptive_theme:
     dependency: "direct main"
     description:
       name: adaptive_theme
-      sha256: "84af26cfc68220df3cd35d9d94cb8953e7182ef560e13d8efb87f32bf1e588fc"
+      sha256: "3568bb526d4823c7bb35f9ce3604af15e04cc0e9cc4f257da3604fe6b48d74ae"
       url: "https://pub.dev"
     source: hosted
-    version: "3.1.1"
+    version: "3.2.1"
   analyzer:
     dependency: transitive
     description:
       name: analyzer
-      sha256: "690e335554a8385bc9d787117d9eb52c0c03ee207a607e593de3c9d71b1cfe80"
+      sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562
       url: "https://pub.dev"
     source: hosted
-    version: "4.7.0"
+    version: "5.13.0"
   archive:
     dependency: transitive
     description:
       name: archive
-      sha256: d6347d54a2d8028e0437e3c099f66fdb8ae02c4720c1e7534c9f24c10351f85d
+      sha256: "0c8368c9b3f0abbc193b9d6133649a614204b528982bebc7026372d61677ce3a"
       url: "https://pub.dev"
     source: hosted
-    version: "3.3.6"
+    version: "3.3.7"
   args:
     dependency: transitive
     description:
       name: args
-      sha256: "139d809800a412ebb26a3892da228b2d0ba36f0ef5d9a82166e5e52ec8d61611"
+      sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596
       url: "https://pub.dev"
     source: hosted
-    version: "2.3.2"
+    version: "2.4.2"
   async:
     dependency: transitive
     description:
@@ -69,18 +69,18 @@ packages:
     dependency: "direct main"
     description:
       name: bloc
-      sha256: bd4f8027bfa60d96c8046dec5ce74c463b2c918dce1b0d36593575995344534a
+      sha256: "3820f15f502372d979121de1f6b97bfcf1630ebff8fe1d52fb2b0bfa49be5b49"
       url: "https://pub.dev"
     source: hosted
-    version: "8.1.0"
+    version: "8.1.2"
   bloc_test:
     dependency: "direct dev"
     description:
       name: bloc_test
-      sha256: "622b97678bf8c06a94f4c26a89ee9ebf7319bf775383dee2233e86e1f94ee28d"
+      sha256: "43d5b2f3d09ba768d6b611151bdf20ca141ffb46e795eb9550a58c9c2f4eae3f"
       url: "https://pub.dev"
     source: hosted
-    version: "9.1.0"
+    version: "9.1.3"
   boolean_selector:
     dependency: transitive
     description:
@@ -117,10 +117,10 @@ packages:
     dependency: transitive
     description:
       name: build_resolvers
-      sha256: "687cf90a3951affac1bd5f9ecb5e3e90b60487f3d9cdc359bb310f8876bb02a6"
+      sha256: "6c4dd11d05d056e76320b828a1db0fc01ccd376922526f8e9d6c796a5adbac20"
       url: "https://pub.dev"
     source: hosted
-    version: "2.0.10"
+    version: "2.2.1"
   build_runner:
     dependency: "direct dev"
     description:
@@ -133,10 +133,10 @@ packages:
     dependency: transitive
     description:
       name: build_runner_core
-      sha256: "14febe0f5bac5ae474117a36099b4de6f1dbc52df6c5e55534b3da9591bf4292"
+      sha256: "0671ad4162ed510b70d0eb4ad6354c249f8429cab4ae7a4cec86bbc2886eb76e"
       url: "https://pub.dev"
     source: hosted
-    version: "7.2.7"
+    version: "7.2.7+1"
   built_collection:
     dependency: transitive
     description:
@@ -149,10 +149,10 @@ packages:
     dependency: transitive
     description:
       name: built_value
-      sha256: "169565c8ad06adb760c3645bf71f00bff161b00002cace266cad42c5d22a7725"
+      sha256: "598a2a682e2a7a90f08ba39c0aaa9374c5112340f0a2e275f61b59389543d166"
       url: "https://pub.dev"
     source: hosted
-    version: "8.4.3"
+    version: "8.6.1"
   characters:
     dependency: transitive
     description:
@@ -165,10 +165,10 @@ packages:
     dependency: transitive
     description:
       name: checked_yaml
-      sha256: "3d1505d91afa809d177efd4eed5bb0eb65805097a1463abdd2add076effae311"
+      sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff
       url: "https://pub.dev"
     source: hosted
-    version: "2.0.2"
+    version: "2.0.3"
   clipboard:
     dependency: "direct main"
     description:
@@ -189,10 +189,10 @@ packages:
     dependency: transitive
     description:
       name: code_builder
-      sha256: "0d43dd1288fd145de1ecc9a3948ad4a6d5a82f0a14c4fdd0892260787d975cbe"
+      sha256: "4ad01d6e56db961d29661561effde45e519939fdaeb46c351275b182eac70189"
       url: "https://pub.dev"
     source: hosted
-    version: "4.4.0"
+    version: "4.5.0"
   collection:
     dependency: "direct main"
     description:
@@ -204,11 +204,12 @@ packages:
   computer:
     dependency: "direct main"
     description:
-      name: computer
-      sha256: "3df9f1ef497aaf69e066b00f4441726eb28037dc33e97b5d56393967f92c5fe8"
-      url: "https://pub.dev"
-    source: hosted
-    version: "2.0.0"
+      path: "."
+      ref: HEAD
+      resolved-ref: "82e365fed8a1a76f6eea0220de98389eed7b0445"
+      url: "https://github.com/ente-io/computer.git"
+    source: git
+    version: "3.2.1"
   confetti:
     dependency: "direct main"
     description:
@@ -261,10 +262,10 @@ packages:
     dependency: transitive
     description:
       name: coverage
-      sha256: "961c4aebd27917269b1896382c7cb1b1ba81629ba669ba09c27a7e5710ec9040"
+      sha256: "2fb815080e44a09b85e0f2ca8a820b15053982b2e714b59267719e8a9ff17097"
       url: "https://pub.dev"
     source: hosted
-    version: "1.6.2"
+    version: "1.6.3"
   cross_file:
     dependency: transitive
     description:
@@ -277,18 +278,18 @@ packages:
     dependency: transitive
     description:
       name: crypto
-      sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67
+      sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
       url: "https://pub.dev"
     source: hosted
-    version: "3.0.2"
+    version: "3.0.3"
   csslib:
     dependency: transitive
     description:
       name: csslib
-      sha256: b36c7f7e24c0bdf1bf9a3da461c837d1de64b9f8beb190c9011d8c72a3dfd745
+      sha256: "706b5707578e0c1b4b7550f64078f0a0f19dec3f50a178ffae7006b0a9ca58fb"
       url: "https://pub.dev"
     source: hosted
-    version: "0.17.2"
+    version: "1.0.0"
   cupertino_icons:
     dependency: "direct main"
     description:
@@ -301,10 +302,10 @@ packages:
     dependency: transitive
     description:
       name: dart_style
-      sha256: "7a03456c3490394c8e7665890333e91ae8a49be43542b616e414449ac358acd4"
+      sha256: "1efa911ca7086affd35f463ca2fc1799584fb6aa89883cf0af8e3664d6a02d55"
       url: "https://pub.dev"
     source: hosted
-    version: "2.2.4"
+    version: "2.3.2"
   dbus:
     dependency: transitive
     description:
@@ -317,10 +318,10 @@ packages:
     dependency: "direct main"
     description:
       name: device_info_plus
-      sha256: "7ff671ed0a6356fa8f2e1ae7d3558d3fb7b6a41e24455e4f8df75b811fb8e4ab"
+      sha256: f52ab3b76b36ede4d135aab80194df8925b553686f0fa12226b4e2d658e45903
       url: "https://pub.dev"
     source: hosted
-    version: "8.0.0"
+    version: "8.2.2"
   device_info_plus_platform_interface:
     dependency: transitive
     description:
@@ -397,10 +398,10 @@ packages:
     dependency: transitive
     description:
       name: ffi
-      sha256: a38574032c5f1dd06c4aee541789906c12ccaab8ba01446e800d9c5b79c4a978
+      sha256: ed5337a5660c506388a9f012be0288fb38b49020ce2b45fe1f8b8323fe429f99
       url: "https://pub.dev"
     source: hosted
-    version: "2.0.1"
+    version: "2.0.2"
   file:
     dependency: transitive
     description:
@@ -413,18 +414,18 @@ packages:
     dependency: "direct main"
     description:
       name: file_picker
-      sha256: d090ae03df98b0247b82e5928f44d1b959867049d18d73635e2e0bc3f49542b9
+      sha256: b85eb92b175767fdaa0c543bf3b0d1f610fe966412ea72845fe5ba7801e763ff
       url: "https://pub.dev"
     source: hosted
-    version: "5.2.5"
+    version: "5.2.10"
   fixnum:
     dependency: transitive
     description:
       name: fixnum
-      sha256: "04be3e934c52e082558cc9ee21f42f5c1cd7a1262f4c63cd0357c08d5bba81ec"
+      sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1"
       url: "https://pub.dev"
     source: hosted
-    version: "1.0.1"
+    version: "1.1.0"
   fk_user_agent:
     dependency: "direct main"
     description:
@@ -442,10 +443,10 @@ packages:
     dependency: "direct main"
     description:
       name: flutter_bloc
-      sha256: "890c51c8007f0182360e523518a0c732efb89876cb4669307af7efada5b55557"
+      sha256: e74efb89ee6945bcbce74a5b3a5a3376b088e5f21f55c263fc38cbdc6237faae
       url: "https://pub.dev"
     source: hosted
-    version: "8.1.1"
+    version: "8.1.3"
   flutter_email_sender:
     dependency: "direct main"
     description:
@@ -511,10 +512,10 @@ packages:
     dependency: transitive
     description:
       name: flutter_plugin_android_lifecycle
-      sha256: "60fc7b78455b94e6de2333d2f95196d32cf5c22f4b0b0520a628804cb463503b"
+      sha256: "950e77c2bbe1692bc0874fc7fb491b96a4dc340457f4ea1641443d0a6c1ea360"
       url: "https://pub.dev"
     source: hosted
-    version: "2.0.7"
+    version: "2.0.15"
   flutter_secure_storage:
     dependency: "direct main"
     description:
@@ -602,10 +603,10 @@ packages:
     dependency: "direct main"
     description:
       name: fluttertoast
-      sha256: "7cc92eabe01e3f1babe1571c5560b135dfc762a34e41e9056881e2196b178ec1"
+      sha256: "474f7d506230897a3cd28c965ec21c5328ae5605fc9c400cd330e9e9d6ac175c"
       url: "https://pub.dev"
     source: hosted
-    version: "8.1.2"
+    version: "8.2.2"
   frontend_server_client:
     dependency: transitive
     description:
@@ -618,10 +619,10 @@ packages:
     dependency: transitive
     description:
       name: glob
-      sha256: "4515b5b6ddb505ebdd242a5f2cc5d22d3d6a80013789debfbda7777f47ea308c"
+      sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63"
       url: "https://pub.dev"
     source: hosted
-    version: "2.1.1"
+    version: "2.1.2"
   google_nav_bar:
     dependency: "direct main"
     description:
@@ -634,10 +635,10 @@ packages:
     dependency: transitive
     description:
       name: graphs
-      sha256: f9e130f3259f52d26f0cfc0e964513796dafed572fa52e45d2f8d6ca14db39b2
+      sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19
       url: "https://pub.dev"
     source: hosted
-    version: "2.2.0"
+    version: "2.3.1"
   hex:
     dependency: transitive
     description:
@@ -650,18 +651,18 @@ packages:
     dependency: transitive
     description:
       name: html
-      sha256: d9793e10dbe0e6c364f4c59bf3e01fb33a9b2a674bc7a1081693dba0614b6269
+      sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a"
       url: "https://pub.dev"
     source: hosted
-    version: "0.15.1"
+    version: "0.15.4"
   http:
     dependency: "direct main"
     description:
       name: http
-      sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482"
+      sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2"
       url: "https://pub.dev"
     source: hosted
-    version: "0.13.5"
+    version: "0.13.6"
   http_multi_server:
     dependency: transitive
     description:
@@ -714,18 +715,18 @@ packages:
     dependency: "direct main"
     description:
       name: json_annotation
-      sha256: "3520fa844009431b5d4491a5a778603520cdc399ab3406332dcc50f93547258c"
+      sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467
       url: "https://pub.dev"
     source: hosted
-    version: "4.7.0"
+    version: "4.8.1"
   json_serializable:
     dependency: "direct dev"
     description:
       name: json_serializable
-      sha256: f3c2c18a7889580f71926f30c1937727c8c7d4f3a435f8f5e8b0ddd25253ef5d
+      sha256: "43793352f90efa5d8b251893a63d767b2f7c833120e3cc02adad55eefec04dc7"
       url: "https://pub.dev"
     source: hosted
-    version: "6.5.4"
+    version: "6.6.2"
   lints:
     dependency: "direct dev"
     description:
@@ -778,10 +779,10 @@ packages:
     dependency: "direct main"
     description:
       name: logging
-      sha256: "04094f2eb032cbb06c6f6e8d3607edcfcb0455e2bb6cbc010cb01171dcb64e6d"
+      sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340"
       url: "https://pub.dev"
     source: hosted
-    version: "1.1.1"
+    version: "1.2.0"
   matcher:
     dependency: transitive
     description:
@@ -850,10 +851,10 @@ packages:
     dependency: transitive
     description:
       name: node_preamble
-      sha256: "8ebdbaa3b96d5285d068f80772390d27c21e1fa10fb2df6627b1b9415043608d"
+      sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db"
       url: "https://pub.dev"
     source: hosted
-    version: "2.0.1"
+    version: "2.0.2"
   open_filex:
     dependency: "direct main"
     description:
@@ -866,10 +867,10 @@ packages:
     dependency: "direct main"
     description:
       name: otp
-      sha256: "337fddb07d10df56dc57f3f8a3e1a7e7abe7463cd85e4efc567c4d2bc94d4ed6"
+      sha256: fcb7f21e30c4cd80a0a982c27a9b75151cc1fe3d8f7ee680673c090171b1ad55
       url: "https://pub.dev"
     source: hosted
-    version: "3.1.3"
+    version: "3.1.4"
   package_config:
     dependency: transitive
     description:
@@ -962,50 +963,50 @@ packages:
     dependency: "direct main"
     description:
       name: path_provider
-      sha256: dcea5feb97d8abf90cab9e9030b497fb7c3cbf26b7a1fe9e3ef7dcb0a1ddec95
+      sha256: "3087813781ab814e4157b172f1a11c46be20179fcc9bea043e0fba36bc0acaa2"
       url: "https://pub.dev"
     source: hosted
-    version: "2.0.12"
+    version: "2.0.15"
   path_provider_android:
     dependency: transitive
     description:
       name: path_provider_android
-      sha256: a776c088d671b27f6e3aa8881d64b87b3e80201c64e8869b811325de7a76c15e
+      sha256: "2cec049d282c7f13c594b4a73976b0b4f2d7a1838a6dd5aaf7bd9719196bee86"
       url: "https://pub.dev"
     source: hosted
-    version: "2.0.22"
+    version: "2.0.27"
   path_provider_foundation:
     dependency: transitive
     description:
       name: path_provider_foundation
-      sha256: "62a68e7e1c6c459f9289859e2fae58290c981ce21d1697faf54910fe1faa4c74"
+      sha256: "916731ccbdce44d545414dd9961f26ba5fbaa74bcbb55237d8e65a623a8c7297"
       url: "https://pub.dev"
     source: hosted
-    version: "2.1.1"
+    version: "2.2.4"
   path_provider_linux:
     dependency: transitive
     description:
       name: path_provider_linux
-      sha256: ab0987bf95bc591da42dffb38c77398fc43309f0b9b894dcc5d6f40c4b26c379
+      sha256: ffbb8cc9ed2c9ec0e4b7a541e56fd79b138e8f47d2fb86815f15358a349b3b57
       url: "https://pub.dev"
     source: hosted
-    version: "2.1.7"
+    version: "2.1.11"
   path_provider_platform_interface:
     dependency: transitive
     description:
       name: path_provider_platform_interface
-      sha256: f0abc8ebd7253741f05488b4813d936b4d07c6bae3e86148a09e342ee4b08e76
+      sha256: "57585299a729335f1298b43245842678cb9f43a6310351b18fb577d6e33165ec"
       url: "https://pub.dev"
     source: hosted
-    version: "2.0.5"
+    version: "2.0.6"
   path_provider_windows:
     dependency: transitive
     description:
       name: path_provider_windows
-      sha256: bcabbe399d4042b8ee687e17548d5d3f527255253b4a639f5f8d2094a9c2b45c
+      sha256: "1cb68ba4cd3a795033de62ba1b7b4564dace301f952de6bfb3cd91b202b6ee96"
       url: "https://pub.dev"
     source: hosted
-    version: "2.1.3"
+    version: "2.1.7"
   petitparser:
     dependency: transitive
     description:
@@ -1034,18 +1035,18 @@ packages:
     dependency: transitive
     description:
       name: plugin_platform_interface
-      sha256: dbf0f707c78beedc9200146ad3cb0ab4d5da13c246336987be6940f026500d3a
+      sha256: "6a2128648c854906c53fa8e33986fc0247a1116122f9534dd20e3ab9e16a32bc"
       url: "https://pub.dev"
     source: hosted
-    version: "2.1.3"
+    version: "2.1.4"
   pointycastle:
-    dependency: transitive
+    dependency: "direct main"
     description:
       name: pointycastle
-      sha256: db7306cf0249f838d1a24af52b5a5887c5bf7f31d8bb4e827d071dc0939ad346
+      sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c"
       url: "https://pub.dev"
     source: hosted
-    version: "3.6.2"
+    version: "3.7.3"
   pool:
     dependency: transitive
     description:
@@ -1074,18 +1075,18 @@ packages:
     dependency: transitive
     description:
       name: pub_semver
-      sha256: "307de764d305289ff24ad257ad5c5793ce56d04947599ad68b3baa124105fc17"
+      sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c"
       url: "https://pub.dev"
     source: hosted
-    version: "2.1.3"
+    version: "2.1.4"
   pubspec_parse:
     dependency: transitive
     description:
       name: pubspec_parse
-      sha256: "75f6614d6dde2dc68948dffbaa4fe5dae32cd700eb9fb763fe11dfb45a3c4d0a"
+      sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367
       url: "https://pub.dev"
     source: hosted
-    version: "1.2.1"
+    version: "1.2.3"
   qr_code_scanner:
     dependency: "direct main"
     description:
@@ -1098,18 +1099,18 @@ packages:
     dependency: "direct main"
     description:
       name: sentry
-      sha256: c64db3444237ff747c5a68f5214897bcb078de248785d0d816e3c55ab94dd71d
+      sha256: a1529c545fcbc899e5dcc7c94ff1c6ad0c334dfc99a3cda366b1da98af7c5678
       url: "https://pub.dev"
     source: hosted
-    version: "6.19.0"
+    version: "6.22.0"
   sentry_flutter:
     dependency: "direct main"
     description:
       name: sentry_flutter
-      sha256: a76cf5180d571535fb8fc3bf10358ab385d78134fcf652d0e03ba7741525ab09
+      sha256: cab07e99a8f27af94f399cabceaff6968011660505b30a0e2286728a81bc476c
       url: "https://pub.dev"
     source: hosted
-    version: "6.19.0"
+    version: "6.22.0"
   share_plus:
     dependency: "direct main"
     description:
@@ -1138,10 +1139,10 @@ packages:
     dependency: transitive
     description:
       name: share_plus_platform_interface
-      sha256: "82ddd4ab9260c295e6e39612d4ff00390b9a7a21f1bb1da771e2f232d80ab8a1"
+      sha256: "0c6e61471bd71b04a138b8b588fa388e66d8b005e6f2deda63371c5c505a0981"
       url: "https://pub.dev"
     source: hosted
-    version: "3.2.0"
+    version: "3.2.1"
   share_plus_web:
     dependency: transitive
     description:
@@ -1162,90 +1163,90 @@ packages:
     dependency: "direct main"
     description:
       name: shared_preferences
-      sha256: "5949029e70abe87f75cfe59d17bf5c397619c4b74a099b10116baeb34786fad9"
+      sha256: "0344316c947ffeb3a529eac929e1978fcd37c26be4e8468628bac399365a3ca1"
       url: "https://pub.dev"
     source: hosted
-    version: "2.0.17"
+    version: "2.2.0"
   shared_preferences_android:
     dependency: transitive
     description:
       name: shared_preferences_android
-      sha256: "955e9736a12ba776bdd261cf030232b30eadfcd9c79b32a3250dd4a494e8c8f7"
+      sha256: fe8401ec5b6dcd739a0fe9588802069e608c3fdbfd3c3c93e546cf2f90438076
       url: "https://pub.dev"
     source: hosted
-    version: "2.0.15"
+    version: "2.2.0"
   shared_preferences_foundation:
     dependency: transitive
     description:
       name: shared_preferences_foundation
-      sha256: "2b55c18636a4edc529fa5cd44c03d3f3100c00513f518c5127c951978efcccd0"
+      sha256: f39696b83e844923b642ce9dd4bd31736c17e697f6731a5adf445b1274cf3cd4
       url: "https://pub.dev"
     source: hosted
-    version: "2.1.3"
+    version: "2.3.2"
   shared_preferences_linux:
     dependency: transitive
     description:
       name: shared_preferences_linux
-      sha256: f8ea038aa6da37090093974ebdcf4397010605fd2ff65c37a66f9d28394cb874
+      sha256: "71d6806d1449b0a9d4e85e0c7a917771e672a3d5dc61149cc9fac871115018e1"
       url: "https://pub.dev"
     source: hosted
-    version: "2.1.3"
+    version: "2.3.0"
   shared_preferences_platform_interface:
     dependency: transitive
     description:
       name: shared_preferences_platform_interface
-      sha256: da9431745ede5ece47bc26d5d73a9d3c6936ef6945c101a5aca46f62e52c1cf3
+      sha256: "23b052f17a25b90ff2b61aad4cc962154da76fb62848a9ce088efe30d7c50ab1"
       url: "https://pub.dev"
     source: hosted
-    version: "2.1.0"
+    version: "2.3.0"
   shared_preferences_web:
     dependency: transitive
     description:
       name: shared_preferences_web
-      sha256: a4b5bc37fe1b368bbc81f953197d55e12f49d0296e7e412dfe2d2d77d6929958
+      sha256: "7347b194fb0bbeb4058e6a4e87ee70350b6b2b90f8ac5f8bd5b3a01548f6d33a"
       url: "https://pub.dev"
     source: hosted
-    version: "2.0.4"
+    version: "2.2.0"
   shared_preferences_windows:
     dependency: transitive
     description:
       name: shared_preferences_windows
-      sha256: "5eaf05ae77658d3521d0e993ede1af962d4b326cd2153d312df716dc250f00c9"
+      sha256: f95e6a43162bce43c9c3405f3eb6f39e5b5d11f65fab19196cf8225e2777624d
       url: "https://pub.dev"
     source: hosted
-    version: "2.1.3"
+    version: "2.3.0"
   shelf:
     dependency: transitive
     description:
       name: shelf
-      sha256: c24a96135a2ccd62c64b69315a14adc5c3419df63b4d7c05832a346fdb73682c
+      sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4
       url: "https://pub.dev"
     source: hosted
-    version: "1.4.0"
+    version: "1.4.1"
   shelf_packages_handler:
     dependency: transitive
     description:
       name: shelf_packages_handler
-      sha256: aef74dc9195746a384843102142ab65b6a4735bb3beea791e63527b88cc83306
+      sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e"
       url: "https://pub.dev"
     source: hosted
-    version: "3.0.1"
+    version: "3.0.2"
   shelf_static:
     dependency: transitive
     description:
       name: shelf_static
-      sha256: e792b76b96a36d4a41b819da593aff4bdd413576b3ba6150df5d8d9996d2e74c
+      sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e
       url: "https://pub.dev"
     source: hosted
-    version: "1.1.1"
+    version: "1.1.2"
   shelf_web_socket:
     dependency: transitive
     description:
       name: shelf_web_socket
-      sha256: a988c0e8d8ffbdb8a28aa7ec8e449c260f3deb808781fe1284d22c5bba7156e8
+      sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1"
       url: "https://pub.dev"
     source: hosted
-    version: "1.0.3"
+    version: "1.0.4"
   sky_engine:
     dependency: transitive
     description: flutter
@@ -1255,18 +1256,18 @@ packages:
     dependency: transitive
     description:
       name: source_gen
-      sha256: "2d79738b6bbf38a43920e2b8d189e9a3ce6cc201f4b8fc76be5e4fe377b1c38d"
+      sha256: "373f96cf5a8744bc9816c1ff41cf5391bbdbe3d7a96fe98c622b6738a8a7bd33"
       url: "https://pub.dev"
     source: hosted
-    version: "1.2.6"
+    version: "1.3.2"
   source_helper:
     dependency: transitive
     description:
       name: source_helper
-      sha256: "3b67aade1d52416149c633ba1bb36df44d97c6b51830c2198e934e3fca87ca1f"
+      sha256: "6adebc0006c37dd63fe05bca0a929b99f06402fc95aa35bf36d67f5c06de01fd"
       url: "https://pub.dev"
     source: hosted
-    version: "1.3.3"
+    version: "1.3.4"
   source_map_stack_trace:
     dependency: transitive
     description:
@@ -1279,10 +1280,10 @@ packages:
     dependency: transitive
     description:
       name: source_maps
-      sha256: "490098075234dcedb83c5d949b4c93dad5e6b7702748de000be2b57b8e6b2427"
+      sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703"
       url: "https://pub.dev"
     source: hosted
-    version: "0.10.11"
+    version: "0.10.12"
   source_span:
     dependency: transitive
     description:
@@ -1295,18 +1296,18 @@ packages:
     dependency: "direct main"
     description:
       name: sqflite
-      sha256: "78324387dc81df14f78df06019175a86a2ee0437624166c382e145d0a7fd9a4f"
+      sha256: b4d6710e1200e96845747e37338ea8a819a12b51689a3bcf31eff0003b37a0b9
       url: "https://pub.dev"
     source: hosted
-    version: "2.2.4+1"
+    version: "2.2.8+4"
   sqflite_common:
     dependency: transitive
     description:
       name: sqflite_common
-      sha256: bfd6973aaeeb93475bc0d875ac9aefddf7965ef22ce09790eb963992ffc5183f
+      sha256: "8f7603f3f8f126740bc55c4ca2d1027aab4b74a1267a3e31ce51fe40e3b65b8f"
       url: "https://pub.dev"
     source: hosted
-    version: "2.4.2+2"
+    version: "2.4.5+1"
   stack_trace:
     dependency: transitive
     description:
@@ -1359,10 +1360,10 @@ packages:
     dependency: transitive
     description:
       name: synchronized
-      sha256: "33b31b6beb98100bf9add464a36a8dd03eb10c7a8cf15aeec535e9b054aaf04b"
+      sha256: "5fcbd27688af6082f5abd611af56ee575342c30e87541d0245f7ff99faa02c60"
       url: "https://pub.dev"
     source: hosted
-    version: "3.0.1"
+    version: "3.1.0"
   term_glyph:
     dependency: transitive
     description:
@@ -1399,10 +1400,10 @@ packages:
     dependency: transitive
     description:
       name: timezone
-      sha256: "24c8fcdd49a805d95777a39064862133ff816ebfffe0ceff110fb5960e557964"
+      sha256: "1cfd8ddc2d1cfd836bc93e67b9be88c3adaeca6f40a00ca999104c30693cdca0"
       url: "https://pub.dev"
     source: hosted
-    version: "0.9.1"
+    version: "0.9.2"
   timing:
     dependency: transitive
     description:
@@ -1411,14 +1412,22 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.0.1"
+  tuple:
+    dependency: "direct main"
+    description:
+      name: tuple
+      sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.0.2"
   typed_data:
     dependency: transitive
     description:
       name: typed_data
-      sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5"
+      sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c
       url: "https://pub.dev"
     source: hosted
-    version: "1.3.1"
+    version: "1.3.2"
   uni_links:
     dependency: "direct main"
     description:
@@ -1447,74 +1456,74 @@ packages:
     dependency: transitive
     description:
       name: universal_io
-      sha256: "79f78ddad839ee3aae3ec7c01eb4575faf0d5c860f8e5223bc9f9c17f7f03cef"
+      sha256: "06866290206d196064fd61df4c7aea1ffe9a4e7c4ccaa8fcded42dd41948005d"
       url: "https://pub.dev"
     source: hosted
-    version: "2.0.4"
+    version: "2.2.0"
   url_launcher:
     dependency: "direct main"
     description:
       name: url_launcher
-      sha256: "698fa0b4392effdc73e9e184403b627362eb5fbf904483ac9defbb1c2191d809"
+      sha256: eb1e00ab44303d50dd487aab67ebc575456c146c6af44422f9c13889984c00f3
       url: "https://pub.dev"
     source: hosted
-    version: "6.1.8"
+    version: "6.1.11"
   url_launcher_android:
     dependency: transitive
     description:
       name: url_launcher_android
-      sha256: "3e2f6dfd2c7d9cd123296cab8ef66cfc2c1a13f5845f42c7a0f365690a8a7dd1"
+      sha256: "15f5acbf0dce90146a0f5a2c4a002b1814a6303c4c5c075aa2623b2d16156f03"
       url: "https://pub.dev"
     source: hosted
-    version: "6.0.23"
+    version: "6.0.36"
   url_launcher_ios:
     dependency: transitive
     description:
       name: url_launcher_ios
-      sha256: bb328b24d3bccc20bdf1024a0990ac4f869d57663660de9c936fb8c043edefe3
+      sha256: "9af7ea73259886b92199f9e42c116072f05ff9bea2dcb339ab935dfc957392c2"
       url: "https://pub.dev"
     source: hosted
-    version: "6.0.18"
+    version: "6.1.4"
   url_launcher_linux:
     dependency: transitive
     description:
       name: url_launcher_linux
-      sha256: "318c42cba924e18180c029be69caf0a1a710191b9ec49bb42b5998fdcccee3cc"
+      sha256: "207f4ddda99b95b4d4868320a352d374b0b7e05eefad95a4a26f57da413443f5"
       url: "https://pub.dev"
     source: hosted
-    version: "3.0.2"
+    version: "3.0.5"
   url_launcher_macos:
     dependency: transitive
     description:
       name: url_launcher_macos
-      sha256: "41988b55570df53b3dd2a7fc90c76756a963de6a8c5f8e113330cb35992e2094"
+      sha256: "1c4fdc0bfea61a70792ce97157e5cc17260f61abbe4f39354513f39ec6fd73b1"
       url: "https://pub.dev"
     source: hosted
-    version: "3.0.2"
+    version: "3.0.6"
   url_launcher_platform_interface:
     dependency: transitive
     description:
       name: url_launcher_platform_interface
-      sha256: "4eae912628763eb48fc214522e58e942fd16ce195407dbf45638239523c759a6"
+      sha256: bfdfa402f1f3298637d71ca8ecfe840b4696698213d5346e9d12d4ab647ee2ea
       url: "https://pub.dev"
     source: hosted
-    version: "2.1.1"
+    version: "2.1.3"
   url_launcher_web:
     dependency: transitive
     description:
       name: url_launcher_web
-      sha256: "44d79408ce9f07052095ef1f9a693c258d6373dc3944249374e30eff7219ccb0"
+      sha256: cc26720eefe98c1b71d85f9dc7ef0cada5132617046369d9dc296b3ecaa5cbb4
       url: "https://pub.dev"
     source: hosted
-    version: "2.0.14"
+    version: "2.0.18"
   url_launcher_windows:
     dependency: transitive
     description:
       name: url_launcher_windows
-      sha256: b6217370f8eb1fd85c8890c539f5a639a01ab209a36db82c921ebeacefc7a615
+      sha256: "7967065dd2b5fccc18c653b97958fdf839c5478c28e767c61ee879f4e7882422"
       url: "https://pub.dev"
     source: hosted
-    version: "3.0.3"
+    version: "3.0.7"
   uuid:
     dependency: "direct main"
     description:
@@ -1551,10 +1560,10 @@ packages:
     dependency: transitive
     description:
       name: web_socket_channel
-      sha256: ca49c0bc209c687b887f30527fb6a9d80040b072cc2990f34b9bec3e7663101b
+      sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b
       url: "https://pub.dev"
     source: hosted
-    version: "2.3.0"
+    version: "2.4.0"
   webkit_inspection_protocol:
     dependency: transitive
     description:
@@ -1567,10 +1576,10 @@ packages:
     dependency: transitive
     description:
       name: win32
-      sha256: c9ebe7ee4ab0c2194e65d3a07d8c54c5d00bb001b76081c4a04cdb8448b59e46
+      sha256: a6f0236dbda0f63aa9a25ad1ff9a9d8a4eaaa5012da0dc59d21afdb1dc361ca4
       url: "https://pub.dev"
     source: hosted
-    version: "3.1.3"
+    version: "3.1.4"
   xdg_directories:
     dependency: transitive
     description:
@@ -1583,10 +1592,10 @@ packages:
     dependency: transitive
     description:
       name: xml
-      sha256: ac0e3f4bf00ba2708c33fbabbbe766300e509f8c82dbd4ab6525039813f7e2fb
+      sha256: "979ee37d622dec6365e2efa4d906c37470995871fe9ae080d967e192d88286b5"
       url: "https://pub.dev"
     source: hosted
-    version: "6.1.0"
+    version: "6.2.2"
   xmlstream:
     dependency: transitive
     description:
@@ -1599,10 +1608,10 @@ packages:
     dependency: transitive
     description:
       name: yaml
-      sha256: "23812a9b125b48d4007117254bca50abb6c712352927eece9e155207b1db2370"
+      sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5"
       url: "https://pub.dev"
     source: hosted
-    version: "3.1.1"
+    version: "3.1.2"
 sdks:
-  dart: ">=2.18.0 <3.0.0"
+  dart: ">=2.19.0 <3.0.0"
   flutter: ">=3.7.0"

+ 4 - 1
pubspec.yaml

@@ -12,7 +12,8 @@ dependencies:
   bloc: ^8.0.3 #done
   clipboard: ^0.1.3
   collection: # dart
-  computer: ^2.0.0
+  computer:
+    git: "https://github.com/ente-io/computer.git"
   confetti: ^0.7.0
   connectivity: ^3.0.3
   cupertino_icons: ^1.0.0
@@ -56,6 +57,7 @@ dependencies:
   password_strength: ^0.2.0
   path_provider: ^2.0.11
   pinput: ^1.2.2
+  pointycastle: ^3.7.3
   qr_code_scanner: ^1.0.1
   sentry: ^6.12.1
   sentry_flutter: ^6.12.1
@@ -64,6 +66,7 @@ dependencies:
   sqflite: ^2.1.0
   step_progress_indicator: ^1.0.2
   styled_text: ^7.0.0
+  tuple: ^2.0.0
   uni_links: ^0.5.1
   url_launcher: ^6.1.5
   uuid: ^3.0.4

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio