Browse Source

Implement parts of screen lock functionality

Signed-off-by: Mario Danic <mario@lovelyhq.com>
Mario Danic 6 years ago
parent
commit
67bea68a1d

+ 58 - 1
app/src/main/java/com/nextcloud/talk/activities/BaseActivity.java

@@ -21,14 +21,20 @@
 package com.nextcloud.talk.activities;
 
 import android.annotation.SuppressLint;
+import android.app.KeyguardManager;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build;
 import android.os.Bundle;
 import android.util.Log;
 import android.view.WindowManager;
 import android.webkit.SslErrorHandler;
 
+import androidx.annotation.RequiresApi;
 import com.nextcloud.talk.R;
 import com.nextcloud.talk.application.NextcloudTalkApplication;
 import com.nextcloud.talk.events.CertificateEvent;
+import com.nextcloud.talk.utils.SecurityUtils;
 import com.nextcloud.talk.utils.preferences.AppPreferences;
 import com.nextcloud.talk.utils.ssl.MagicTrustManager;
 import com.yarolegovich.lovelydialog.LovelyStandardDialog;
@@ -51,6 +57,7 @@ import autodagger.AutoInjector;
 @AutoInjector(NextcloudTalkApplication.class)
 public class BaseActivity extends AppCompatActivity {
     private static final String TAG = "BaseActivity";
+    private static final int REQUEST_CODE_CONFIRM_DEVICE_CREDENTIALS = 112;
 
     @Inject
     EventBus eventBus;
@@ -58,12 +65,62 @@ public class BaseActivity extends AppCompatActivity {
     @Inject
     AppPreferences appPreferences;
 
+    private KeyguardManager keyguardManager;
+
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         NextcloudTalkApplication.getSharedApplication().getComponentApplication().inject(this);
         super.onCreate(savedInstanceState);
+        if (appPreferences.getIsScreenLocked()) {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+                SecurityUtils.createKey();
+            }
+        }
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
         if (appPreferences.getIsScreenSecured()) {
-            getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE);
+            getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
+        } else {
+            getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SECURE);
+        }
+
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            checkIfWeAreSecure();
+        }
+    }
+
+    @RequiresApi(api = Build.VERSION_CODES.M)
+    private void checkIfWeAreSecure() {
+        keyguardManager = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
+        if (keyguardManager != null && keyguardManager.isKeyguardSecure() && appPreferences.getIsScreenLocked()) {
+            if (!SecurityUtils.checkIfWeAreAuthenticated()) {
+                showAuthenticationScreen();
+            }
+        }
+    }
+
+    private void showAuthenticationScreen() {
+        Intent intent = keyguardManager.createConfirmDeviceCredentialIntent(null, null);
+        if (intent != null) {
+            startActivityForResult(intent, REQUEST_CODE_CONFIRM_DEVICE_CREDENTIALS);
+        }
+    }
+
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+        if (requestCode == REQUEST_CODE_CONFIRM_DEVICE_CREDENTIALS) {
+            if (resultCode == RESULT_OK) {
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+                    if (SecurityUtils.checkIfWeAreAuthenticated()) {
+                        // all went well
+                    }
+                }
+            } else {
+                // we didnt auth
+            }
         }
     }
 

+ 55 - 31
app/src/main/java/com/nextcloud/talk/controllers/SettingsController.java

@@ -22,6 +22,8 @@ package com.nextcloud.talk.controllers;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
+import android.app.KeyguardManager;
+import android.content.Context;
 import android.content.Intent;
 import android.net.Uri;
 import android.os.Build;
@@ -36,7 +38,13 @@ import android.view.WindowManager;
 import android.widget.Checkable;
 import android.widget.ImageView;
 import android.widget.TextView;
-
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.view.ViewCompat;
+import androidx.work.OneTimeWorkRequest;
+import androidx.work.WorkManager;
+import autodagger.AutoInjector;
+import butterknife.BindView;
 import com.bluelinelabs.conductor.RouterTransaction;
 import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler;
 import com.bluelinelabs.conductor.changehandler.VerticalChangeHandler;
@@ -61,37 +69,18 @@ import com.nextcloud.talk.utils.glide.GlideApp;
 import com.nextcloud.talk.utils.preferences.AppPreferences;
 import com.nextcloud.talk.utils.preferences.MagicUserInputModule;
 import com.nextcloud.talk.utils.singletons.ApplicationWideMessageHolder;
-import com.yarolegovich.mp.MaterialChoicePreference;
-import com.yarolegovich.mp.MaterialEditTextPreference;
-import com.yarolegovich.mp.MaterialPreferenceCategory;
-import com.yarolegovich.mp.MaterialPreferenceScreen;
-import com.yarolegovich.mp.MaterialStandardPreference;
-import com.yarolegovich.mp.MaterialSwitchPreference;
-
+import com.yarolegovich.mp.*;
+import io.reactivex.android.schedulers.AndroidSchedulers;
+import io.reactivex.disposables.Disposable;
+import io.reactivex.schedulers.Schedulers;
 import net.orange_box.storebox.listeners.OnPreferenceValueChangedListener;
-
 import org.greenrobot.eventbus.EventBus;
 
+import javax.inject.Inject;
 import java.io.IOException;
 import java.net.URI;
 import java.net.URISyntaxException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Objects;
-
-import javax.inject.Inject;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.core.view.ViewCompat;
-import androidx.work.OneTimeWorkRequest;
-import androidx.work.WorkManager;
-import autodagger.AutoInjector;
-import butterknife.BindView;
-import io.reactivex.android.schedulers.AndroidSchedulers;
-import io.reactivex.disposables.Disposable;
-import io.reactivex.schedulers.Schedulers;
+import java.util.*;
 
 @AutoInjector(NextcloudTalkApplication.class)
 public class SettingsController extends BaseController {
@@ -164,6 +153,9 @@ public class SettingsController extends BaseController {
     @BindView(R.id.settings_link_previews)
     MaterialSwitchPreference linkPreviewsSwitchPreference;
 
+    @BindView(R.id.settings_screen_lock)
+    MaterialSwitchPreference screenLockSwitchPreference;
+
     @BindView(R.id.message_text)
     TextView messageText;
 
@@ -179,6 +171,9 @@ public class SettingsController extends BaseController {
     @Inject
     UserUtils userUtils;
 
+    @Inject
+    Context context;
+
     private UserEntity currentUser;
     private String credentials;
 
@@ -235,9 +230,18 @@ public class SettingsController extends BaseController {
             shouldVibrateSwitchPreference.setVisibility(View.GONE);
         }
 
-         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
             incognitoKeyboardSwitchPreference.setVisibility(View.GONE);
-         }
+        }
+
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
+            screenLockSwitchPreference.setVisibility(View.GONE);
+        } else {
+            screenLockSwitchPreference.setSummary(String.format(Locale.getDefault(),
+                    getResources().getString(R.string.nc_settings_screen_lock_desc),
+                    getResources().getString(R.string.nc_app_name)));
+        }
+
 
         if (!TextUtils.isEmpty(getResources().getString(R.string.nc_privacy_url))) {
             privacyButton.addPreferenceClickListener(view12 -> {
@@ -343,15 +347,34 @@ public class SettingsController extends BaseController {
         }
 
         if (shouldVibrateSwitchPreference.getVisibility() == View.VISIBLE) {
-            ((Checkable)shouldVibrateSwitchPreference.findViewById(R.id.mp_checkable)).setChecked(appPreferences.getShouldVibrateSetting());
+            ((Checkable) shouldVibrateSwitchPreference.findViewById(R.id.mp_checkable)).setChecked(appPreferences.getShouldVibrateSetting());
         }
 
-        ((Checkable)screenSecuritySwitchPreference.findViewById(R.id.mp_checkable)).setChecked(appPreferences.getIsScreenSecured());
+        ((Checkable) screenSecuritySwitchPreference.findViewById(R.id.mp_checkable)).setChecked(appPreferences.getIsScreenSecured());
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
             ((Checkable) incognitoKeyboardSwitchPreference.findViewById(R.id.mp_checkable)).setChecked(appPreferences.getIsKeyboardIncognito());
         }
-        ((Checkable)linkPreviewsSwitchPreference.findViewById(R.id.mp_checkable)).setChecked(appPreferences.getAreLinkPreviewsAllowed());
 
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            ((Checkable) incognitoKeyboardSwitchPreference.findViewById(R.id.mp_checkable)).setChecked(appPreferences.getIsKeyboardIncognito());
+        }
+
+        ((Checkable) linkPreviewsSwitchPreference.findViewById(R.id.mp_checkable)).setChecked(appPreferences.getAreLinkPreviewsAllowed());
+
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            KeyguardManager keyguardManager = (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE);
+
+            if (keyguardManager.isDeviceSecure()) {
+                screenLockSwitchPreference.setEnabled(true);
+                ((Checkable) screenLockSwitchPreference.findViewById(R.id.mp_checkable)).setChecked(appPreferences.getIsScreenLocked());
+                screenLockSwitchPreference.setAlpha(1.0f);
+            } else {
+                screenLockSwitchPreference.setEnabled(false);
+                appPreferences.setScreenLock(false);
+                ((Checkable) screenLockSwitchPreference.findViewById(R.id.mp_checkable)).setChecked(false);
+                screenLockSwitchPreference.setAlpha(0.38f);
+            }
+        }
 
         String ringtoneName = "";
         RingtoneSettings ringtoneSettings;
@@ -633,6 +656,7 @@ public class SettingsController extends BaseController {
             }
         }
     }
+
     private class ProxyCredentialsChangeListener implements OnPreferenceValueChangedListener<Boolean> {
 
         @Override

+ 93 - 0
app/src/main/java/com/nextcloud/talk/utils/SecurityUtils.java

@@ -0,0 +1,93 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Mario Danic
+ * Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.utils;
+
+import android.os.Build;
+import android.security.keystore.KeyGenParameterSpec;
+import android.security.keystore.KeyPermanentlyInvalidatedException;
+import android.security.keystore.KeyProperties;
+import android.security.keystore.UserNotAuthenticatedException;
+import android.util.Log;
+import androidx.annotation.RequiresApi;
+
+import javax.crypto.*;
+import java.io.IOException;
+import java.security.*;
+import java.security.cert.CertificateException;
+
+public class SecurityUtils {
+    private static final String TAG = "SecurityUtils";
+    private static final String CREDENTIALS_KEY = "KEY_CREDENTIALS";
+    private static final byte[] SECRET_BYTE_ARRAY = new byte[]{1, 2, 3, 4, 5, 6};
+
+    private static final int AUTHENTICATION_DURATION_SECONDS = 10;
+
+    @RequiresApi(api = Build.VERSION_CODES.M)
+    public static boolean checkIfWeAreAuthenticated() {
+        try {
+            KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
+            keyStore.load(null);
+            SecretKey secretKey = (SecretKey) keyStore.getKey(CREDENTIALS_KEY, null);
+            Cipher cipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/" + KeyProperties.BLOCK_MODE_CBC + "/" + KeyProperties.ENCRYPTION_PADDING_PKCS7);
+
+            // Try encrypting something, it will only work if the user authenticated within
+            // the last AUTHENTICATION_DURATION_SECONDS seconds.
+            cipher.init(Cipher.ENCRYPT_MODE, secretKey);
+            cipher.doFinal(SECRET_BYTE_ARRAY);
+
+            // If the user has recently authenticated, we will reach here
+            return true;
+        } catch (UserNotAuthenticatedException e) {
+            // User is not authenticated, let's authenticate with device credentials.
+            return false;
+        } catch (KeyPermanentlyInvalidatedException e) {
+            return false;
+        } catch (BadPaddingException | IllegalBlockSizeException | KeyStoreException |
+                CertificateException | UnrecoverableKeyException | IOException
+                | NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException e) {
+            return false;
+        }
+    }
+
+     @RequiresApi(api = Build.VERSION_CODES.M)
+     public static void createKey() {
+        try {
+            KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
+            keyStore.load(null);
+            KeyGenerator keyGenerator = KeyGenerator.getInstance(
+                    KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
+
+            keyGenerator.init(new KeyGenParameterSpec.Builder(CREDENTIALS_KEY,
+                    KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
+                    .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
+                    .setUserAuthenticationRequired(true)
+                    // Require that the user has unlocked in the last 30 seconds
+                    .setUserAuthenticationValidityDurationSeconds(AUTHENTICATION_DURATION_SECONDS)
+                    .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
+                    .build());
+            keyGenerator.generateKey();
+        } catch (NoSuchAlgorithmException | NoSuchProviderException
+                | InvalidAlgorithmParameterException | KeyStoreException
+                | CertificateException | IOException e) {
+            Log.e(TAG, "Failed to create a symetric key");
+        }
+    }
+}

+ 8 - 0
app/src/main/res/layout/controller_settings.xml

@@ -149,6 +149,14 @@
         apc:mpc_title="@string/nc_settings_privacy"
         apc:mpc_title_color="@color/colorPrimary">
 
+        <com.yarolegovich.mp.MaterialSwitchPreference
+            android:id="@+id/settings_screen_lock"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            apc:mp_default_value="@bool/value_false"
+            apc:mp_key="@string/nc_settings_screen_lock_key"
+            apc:mp_title="@string/nc_settings_screen_lock_title" />
+
         <com.yarolegovich.mp.MaterialSwitchPreference
             android:id="@+id/settings_screen_security"
             android:layout_width="match_parent"