Vishnu Mohandas 4 лет назад
Родитель
Сommit
d445e21e01

+ 42 - 22
lib/core/configuration.dart

@@ -27,29 +27,32 @@ import 'package:photos/utils/crypto_util.dart';
 
 class Configuration {
   Configuration._privateConstructor();
+
   static final Configuration instance = Configuration._privateConstructor();
-  static final _logger = Logger("Configuration");
-  final kTempFolderDeletionTimeBuffer = Duration(days: 1).inMicroseconds;
 
-  static const userIDKey = "user_id";
   static const emailKey = "email";
-  static const nameKey = "name";
-  static const tokenKey = "token";
   static const foldersToBackUpKey = "folders_to_back_up";
-  static const keyKey = "key";
-  static const secretKeyKey = "secret_key";
   static const keyAttributesKey = "key_attributes";
+  static const keyKey = "key";
   static const keyShouldBackupOverMobileData = "should_backup_over_mobile_data";
-  static const keyShouldShowLockScreen = "should_show_lock_screen";
   static const keyShouldHideFromRecents = "should_hide_from_recents";
+  static const keyShouldShowLockScreen = "should_show_lock_screen";
   static const lastTempFolderClearTimeKey = "last_temp_folder_clear_time";
+  static const nameKey = "name";
+  static const secretKeyKey = "secret_key";
+  static const tokenKey = "token";
+  static const userIDKey = "user_id";
+
+  final kTempFolderDeletionTimeBuffer = Duration(days: 1).inMicroseconds;
+
+  static final _logger = Logger("Configuration");
 
-  SharedPreferences _preferences;
-  FlutterSecureStorage _secureStorage;
-  String _key;
   String _cachedToken;
-  String _secretKey;
   String _documentsDirectory;
+  String _key;
+  SharedPreferences _preferences;
+  String _secretKey;
+  FlutterSecureStorage _secureStorage;
   String _tempDirectory;
 
   Future<void> init() async {
@@ -109,14 +112,14 @@ class Configuration {
 
   Future<KeyGenResult> generateKey(String password) async {
     // Create a master key
-    final key = CryptoUtil.generateKey();
+    final masterKey = CryptoUtil.generateKey();
 
     // Create a recovery key
     final recoveryKey = CryptoUtil.generateKey();
 
     // Encrypt master key and recovery key with each other
-    final encryptedMasterKey = CryptoUtil.encryptSync(key, recoveryKey);
-    final encryptedRecoveryKey = CryptoUtil.encryptSync(recoveryKey, key);
+    final encryptedMasterKey = CryptoUtil.encryptSync(masterKey, recoveryKey);
+    final encryptedRecoveryKey = CryptoUtil.encryptSync(recoveryKey, masterKey);
 
     // Derive a key from the password that will be used to encrypt and
     // decrypt the master key
@@ -131,11 +134,12 @@ class Configuration {
     );
 
     // Encrypt the key with this derived key
-    final encryptedKeyData = CryptoUtil.encryptSync(key, kek);
+    final encryptedKeyData = CryptoUtil.encryptSync(masterKey, kek);
 
     // Generate a public-private keypair and encrypt the latter
     final keyPair = await CryptoUtil.generateKeyPair();
-    final encryptedSecretKeyData = CryptoUtil.encryptSync(keyPair.sk, key);
+    final encryptedSecretKeyData =
+        CryptoUtil.encryptSync(keyPair.sk, masterKey);
 
     final attributes = KeyAttributes(
       Sodium.bin2base64(kekSalt),
@@ -151,14 +155,14 @@ class Configuration {
       Sodium.bin2base64(encryptedRecoveryKey.encryptedData),
       Sodium.bin2base64(encryptedRecoveryKey.nonce),
     );
-    final privateAttributes = PrivateKeyAttributes(Sodium.bin2base64(key),
+    final privateAttributes = PrivateKeyAttributes(Sodium.bin2base64(masterKey),
         Sodium.bin2hex(recoveryKey), Sodium.bin2base64(keyPair.sk));
     return KeyGenResult(attributes, privateAttributes);
   }
 
   Future<KeyAttributes> updatePassword(String password) async {
     // Get master key
-    final key = getKey();
+    final masterKey = getKey();
 
     // Derive a key from the password that will be used to encrypt and
     // decrypt the master key
@@ -173,7 +177,7 @@ class Configuration {
     );
 
     // Encrypt the key with this derived key
-    final encryptedKeyData = CryptoUtil.encryptSync(key, kek);
+    final encryptedKeyData = CryptoUtil.encryptSync(masterKey, kek);
 
     // User's public-private key pairs stay untouched since the master key
     // has not changed
@@ -184,8 +188,9 @@ class Configuration {
       final recoveryKey = CryptoUtil.generateKey();
 
       // Encrypt master key and recovery key with each other
-      final encryptedMasterKey = CryptoUtil.encryptSync(key, recoveryKey);
-      final encryptedRecoveryKey = CryptoUtil.encryptSync(recoveryKey, key);
+      final encryptedMasterKey = CryptoUtil.encryptSync(masterKey, recoveryKey);
+      final encryptedRecoveryKey =
+          CryptoUtil.encryptSync(recoveryKey, masterKey);
       return existingAttributes.copyWith(
         kekSalt: Sodium.bin2base64(kekSalt),
         encryptedKey: Sodium.bin2base64(encryptedKeyData.encryptedData),
@@ -234,6 +239,21 @@ class Configuration {
     await setSecretKey(Sodium.bin2base64(secretKey));
   }
 
+  Future<void> recover(String recoveryKey) async {
+    final keyAttributes = getKeyAttributes();
+    var masterKey;
+    try {
+      masterKey = await CryptoUtil.decrypt(
+          Sodium.base642bin(keyAttributes.masterKeyEncryptedWithRecoveryKey),
+          Sodium.hex2bin(recoveryKey),
+          Sodium.base642bin(keyAttributes.masterKeyDecryptionNonce));
+    } catch (e) {
+      _logger.severe(e);
+      throw e;
+    }
+    await setKey(Sodium.bin2base64(masterKey));
+  }
+
   String getHttpEndpoint() {
     if (kDebugMode) {
       return "http://192.168.1.111:8080";

+ 5 - 17
lib/services/user_service.dart

@@ -121,7 +121,7 @@ class UserService {
     }
   }
 
-  Future<void> setupAttributes(KeyGenResult result) async {
+  Future<void> setAttributes(KeyGenResult result) async {
     try {
       final name = _config.getName();
       await _dio.put(
@@ -145,12 +145,8 @@ class UserService {
     }
   }
 
-  Future<void> updateKeyAttributes(
-      BuildContext context, KeyGenResult result) async {
-    final dialog = createProgressDialog(context, "please wait...");
-    await dialog.show();
+  Future<void> updateKeyAttributes(KeyAttributes keyAttributes) async {
     try {
-      final keyAttributes = result.keyAttributes;
       final setKeyRequest = SetKeysRequest(
         kekSalt: keyAttributes.kekSalt,
         encryptedKey: keyAttributes.encryptedKey,
@@ -158,7 +154,7 @@ class UserService {
         memLimit: keyAttributes.memLimit,
         opsLimit: keyAttributes.opsLimit,
       );
-      final response = await _dio.put(
+      await _dio.put(
         _config.getHttpEndpoint() + "/users/keys",
         data: setKeyRequest.toMap(),
         options: Options(
@@ -167,18 +163,10 @@ class UserService {
           },
         ),
       );
-      await dialog.hide();
-      if (response != null && response.statusCode == 200) {
-        await _config.setKeyAttributes(keyAttributes);
-        showToast("password changed successfully");
-        Navigator.of(context).pop();
-      } else {
-        showGenericErrorDialog(context);
-      }
+      await _config.setKeyAttributes(keyAttributes);
     } catch (e) {
-      await dialog.hide();
       _logger.severe(e);
-      showGenericErrorDialog(context);
+      throw e;
     }
   }
 

+ 64 - 38
lib/ui/password_entry_page.dart

@@ -12,11 +12,19 @@ import 'package:photos/ui/recovery_key_dialog.dart';
 import 'package:photos/ui/subscription_page.dart';
 import 'package:photos/ui/web_page.dart';
 import 'package:photos/utils/dialog_util.dart';
+import 'package:photos/utils/toast_util.dart';
+
+enum PasswordEntryMode {
+  set,
+  update,
+  reset,
+}
 
 class PasswordEntryPage extends StatefulWidget {
-  final bool isUpdatePassword;
+  final PasswordEntryMode mode;
 
-  PasswordEntryPage({this.isUpdatePassword = false, Key key}) : super(key: key);
+  PasswordEntryPage({this.mode = PasswordEntryMode.set, Key key})
+      : super(key: key);
 
   @override
   _PasswordEntryPageState createState() => _PasswordEntryPageState();
@@ -31,18 +39,22 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
 
   @override
   Widget build(BuildContext context) {
+    String title = "set password";
+    if (widget.mode == PasswordEntryMode.update) {
+      title = "change password";
+    } else if (widget.mode == PasswordEntryMode.reset) {
+      title = "reset password";
+    }
     return Scaffold(
       appBar: AppBar(
-        title: Text(widget.isUpdatePassword
-            ? "change password"
-            : "encryption password"),
+        title: Text(title),
       ),
-      body: _getBody(),
+      body: _getBody(title),
       resizeToAvoidBottomInset: false,
     );
   }
 
-  Widget _getBody() {
+  Widget _getBody(String buttonText) {
     return Column(
       children: [
         FlutterPasswordStrength(
@@ -60,7 +72,7 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
                 Padding(padding: EdgeInsets.all(12)),
                 Text(
                   "enter a" +
-                      (widget.isUpdatePassword ? " new " : " ") +
+                      (widget.mode != PasswordEntryMode.set ? " new " : " ") +
                       "password we can use to encrypt your data",
                   textAlign: TextAlign.center,
                   style: TextStyle(
@@ -122,25 +134,11 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
                   height: 64,
                   padding: EdgeInsets.fromLTRB(40, 0, 40, 0),
                   child: button(
-                    widget.isUpdatePassword
-                        ? "change password"
-                        : "set password",
+                    buttonText,
                     fontSize: 18,
                     onPressed: _passwordController1.text.isNotEmpty &&
                             _passwordController2.text.isNotEmpty
-                        ? () {
-                            if (_passwordController1.text !=
-                                _passwordController2.text) {
-                              showErrorDialog(context, "uhm...",
-                                  "the passwords you entered don't match");
-                            } else if (_passwordStrength <
-                                kPasswordStrengthThreshold) {
-                              showErrorDialog(context, "weak password",
-                                  "the password you have chosen is too simple, please choose another one");
-                            } else {
-                              _showRecoveryCodeDialog();
-                            }
-                          }
+                        ? _onButtonPress
                         : null,
                   ),
                 ),
@@ -177,6 +175,38 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
     );
   }
 
+  void _onButtonPress() {
+    if (_passwordController1.text != _passwordController2.text) {
+      showErrorDialog(
+          context, "uhm...", "the passwords you entered don't match");
+    } else if (_passwordStrength < kPasswordStrengthThreshold) {
+      showErrorDialog(context, "weak password",
+          "the password you have chosen is too simple, please choose another one");
+    } else {
+      if (widget.mode == PasswordEntryMode.set) {
+        _showRecoveryCodeDialog();
+      } else {
+        _updatePassword();
+      }
+    }
+  }
+
+  void _updatePassword() async {
+    final dialog = createProgressDialog(context, "please wait...");
+    await dialog.show();
+    try {
+      final keyAttributes = await Configuration.instance
+          .updatePassword(_passwordController1.text);
+      await UserService.instance.updateKeyAttributes(keyAttributes);
+      await dialog.hide();
+      showToast("password changed successfully");
+      Navigator.of(context).pop();
+    } catch (e) {
+      await dialog.hide();
+      showGenericErrorDialog(context);
+    }
+  }
+
   Future<void> _showRecoveryCodeDialog() async {
     final dialog =
         createProgressDialog(context, "generating encryption keys...");
@@ -189,20 +219,16 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
         final dialog = createProgressDialog(context, "please wait...");
         await dialog.show();
         try {
-          if (widget.isUpdatePassword) {
-            UserService.instance.updateKeyAttributes(context, result);
-          } else {
-            await UserService.instance.setupAttributes(result);
-            await dialog.hide();
-            Navigator.of(context).pushAndRemoveUntil(
-              MaterialPageRoute(
-                builder: (BuildContext context) {
-                  return SubscriptionPage(isOnboarding: true);
-                },
-              ),
-              (route) => route.isFirst,
-            );
-          }
+          await UserService.instance.setAttributes(result);
+          await dialog.hide();
+          Navigator.of(context).pushAndRemoveUntil(
+            MaterialPageRoute(
+              builder: (BuildContext context) {
+                return SubscriptionPage(isOnboarding: true);
+              },
+            ),
+            (route) => route.isFirst,
+          );
         } catch (e, s) {
           Logger("PEP").severe(e, s);
           await dialog.hide();

+ 102 - 2
lib/ui/recovery_page.dart

@@ -1,8 +1,23 @@
+import 'dart:ui';
+
 import 'package:flutter/material.dart';
+import 'package:logging/logging.dart';
+import 'package:photos/core/configuration.dart';
+import 'package:photos/ui/common_elements.dart';
+import 'package:photos/ui/password_entry_page.dart';
+import 'package:photos/utils/dialog_util.dart';
+import 'package:photos/utils/toast_util.dart';
 
-class RecoveryPage extends StatelessWidget {
+class RecoveryPage extends StatefulWidget {
   const RecoveryPage({Key key}) : super(key: key);
 
+  @override
+  _RecoveryPageState createState() => _RecoveryPageState();
+}
+
+class _RecoveryPageState extends State<RecoveryPage> {
+  final _recoveryKey = TextEditingController();
+
   @override
   Widget build(BuildContext context) {
     return Scaffold(
@@ -15,8 +30,93 @@ class RecoveryPage extends StatelessWidget {
         ),
       ),
       body: Column(
+        crossAxisAlignment: CrossAxisAlignment.stretch,
+        mainAxisAlignment: MainAxisAlignment.center,
+        mainAxisSize: MainAxisSize.max,
         children: [
-          Text("please enter your recovery key to recover your data"),
+          Padding(
+            padding: const EdgeInsets.fromLTRB(60, 0, 60, 0),
+            child: TextFormField(
+              decoration: InputDecoration(
+                hintText: "enter your recovery key",
+                contentPadding: EdgeInsets.all(20),
+              ),
+              style: TextStyle(
+                fontSize: 14,
+                fontFeatures: [FontFeature.tabularFigures()],
+              ),
+              controller: _recoveryKey,
+              autofocus: false,
+              autocorrect: false,
+              keyboardType: TextInputType.multiline,
+              maxLines: null,
+              onChanged: (_) {
+                setState(() {});
+              },
+            ),
+          ),
+          Padding(padding: EdgeInsets.all(12)),
+          Container(
+            padding: const EdgeInsets.fromLTRB(80, 0, 80, 0),
+            width: double.infinity,
+            height: 64,
+            child: button(
+              "recover",
+              fontSize: 18,
+              onPressed: _recoveryKey.text.isNotEmpty
+                  ? () async {
+                      final dialog =
+                          createProgressDialog(context, "decrypting...");
+                      await dialog.show();
+                      try {
+                        await Configuration.instance
+                            .recover(_recoveryKey.text.trim());
+                        await dialog.hide();
+                        showToast("recovery successful!");
+                        Navigator.of(context).pushReplacement(
+                          MaterialPageRoute(
+                            builder: (BuildContext context) {
+                              return WillPopScope(
+                                onWillPop: () async => false,
+                                child: PasswordEntryPage(
+                                  mode: PasswordEntryMode.reset,
+                                ),
+                              );
+                            },
+                          ),
+                        );
+                      } catch (e) {
+                        await dialog.hide();
+                        showErrorDialog(context, "incorrect recovery key",
+                            "the recovery key you entered is incorrect");
+                      }
+                    }
+                  : null,
+            ),
+          ),
+          GestureDetector(
+            behavior: HitTestBehavior.translucent,
+            onTap: () {
+              showErrorDialog(
+                context,
+                "sorry",
+                "due to the nature of our end-to-end encryption protocol, your data cannot be decrypted without your password or recovery key",
+              );
+            },
+            child: Container(
+              padding: EdgeInsets.all(40),
+              child: Center(
+                child: Text(
+                  "no recovery key?",
+                  style: TextStyle(
+                    decoration: TextDecoration.underline,
+                    fontSize: 12,
+                    color: Colors.white.withOpacity(0.9),
+                  ),
+                ),
+              ),
+            ),
+          ),
         ],
       ),
     );

+ 1 - 1
lib/ui/settings_page.dart

@@ -302,7 +302,7 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
                 MaterialPageRoute(
                   builder: (BuildContext context) {
                     return PasswordEntryPage(
-                      isUpdatePassword: true,
+                      mode: PasswordEntryMode.update,
                     );
                   },
                 ),