Browse Source

Add support for default credentials

Hari 7 years ago
parent
commit
9e6dfb644c

+ 1 - 0
src/main/AndroidManifest.xml

@@ -242,6 +242,7 @@
 
         <activity android:name=".ui.activity.PassCodeActivity" />
         <activity android:name=".ui.activity.FingerprintActivity"/>
+        <activity android:name=".ui.activity.RequestCredentialsActivity" />
         <activity android:name=".ui.activity.ConflictsResolveActivity"/>
         <activity android:name=".ui.activity.GenericExplanationActivity"/>
         <activity android:name=".ui.activity.ErrorsWhileCopyingHandlerActivity"/>

+ 17 - 0
src/main/java/com/owncloud/android/authentication/PassCodeManager.java

@@ -32,6 +32,8 @@ import com.owncloud.android.MainApp;
 import com.owncloud.android.ui.activity.FingerprintActivity;
 import com.owncloud.android.ui.activity.PassCodeActivity;
 import com.owncloud.android.ui.activity.Preferences;
+import com.owncloud.android.ui.activity.RequestCredentialsActivity;
+import com.owncloud.android.utils.DeviceCredentialUtils;
 
 import java.util.HashSet;
 import java.util.Set;
@@ -44,6 +46,7 @@ public class PassCodeManager {
         exemptOfPasscodeActivities = new HashSet<>();
         exemptOfPasscodeActivities.add(PassCodeActivity.class);
         exemptOfPasscodeActivities.add(FingerprintActivity.class);
+        sExemptOfPasscodeActivites.add(RequestCredentialsActivity.class);
         // other activities may be exempted, if needed
     }
 
@@ -91,6 +94,14 @@ public class PassCodeManager {
             activity.startActivity(i);
         }
 
+        if (!sExemptOfPasscodeActivites.contains(activity.getClass()) && Build.VERSION.SDK_INT >=
+                Build.VERSION_CODES.M && deviceCredentialsShouldBeRequested() &&
+                !DeviceCredentialUtils.tryEncrypt()) {
+            Intent i = new Intent(MainApp.getAppContext(), RequestCredentialsActivity.class);
+            i.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
+            activity.startActivity(i);
+        }
+
         visibleActivitiesCounter++;    // keep it AFTER passCodeShouldBeRequested was checked
     }
 
@@ -131,4 +142,10 @@ public class PassCodeManager {
         return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
                 appPrefs.getBoolean(Preferences.PREFERENCE_USE_FINGERPRINT, false);
     }
+
+    private boolean deviceCredentialsShouldBeRequested() {
+        SharedPreferences appPrefs = PreferenceManager
+                .getDefaultSharedPreferences(MainApp.getAppContext());
+        return (appPrefs.getBoolean(Preferences.PREFERENCE_USE_DEVICE_CREDENTIALS, false));
+    }
 }

+ 51 - 0
src/main/java/com/owncloud/android/ui/activity/Preferences.java

@@ -70,6 +70,7 @@ import com.owncloud.android.lib.common.ExternalLinkType;
 import com.owncloud.android.lib.common.OwnCloudAccount;
 import com.owncloud.android.lib.common.OwnCloudClientManagerFactory;
 import com.owncloud.android.lib.common.utils.Log_OC;
+import com.owncloud.android.utils.DeviceCredentialUtils;
 import com.owncloud.android.utils.DisplayUtils;
 import com.owncloud.android.utils.MimeTypeUtil;
 import com.owncloud.android.utils.ThemeUtils;
@@ -87,10 +88,14 @@ public class Preferences extends PreferenceActivity
     private static final String TAG = Preferences.class.getSimpleName();
 
     public final static String PREFERENCE_USE_FINGERPRINT = "use_fingerprint";
+
+    public final static String PREFERENCE_USE_DEVICE_CREDENTIALS= "use_device_credentials";
+
     public static final String PREFERENCE_EXPERT_MODE = "expert_mode";
 
     private static final int ACTION_REQUEST_PASSCODE = 5;
     private static final int ACTION_CONFIRM_PASSCODE = 6;
+    private static final int ACTION_CONFIRM_DEVICE_CREDENTIALS = 7;
 
     private static final int ACTION_REQUEST_CODE_DAVDROID_SETUP = 10;
 
@@ -503,12 +508,15 @@ public class Preferences extends PreferenceActivity
 
         boolean fPassCodeEnabled = getResources().getBoolean(R.bool.passcode_enabled);
         boolean fPrintEnabled = getResources().getBoolean(R.bool.fingerprint_enabled);
+        boolean fDeviceCredentialsEnabled = getResources().getBoolean(R.bool.device_credentials);
         boolean fShowHiddenFilesEnabled = getResources().getBoolean(R.bool.show_hidden_files_enabled);
 
         setupPasscodePreference(preferenceCategoryDetails, fPassCodeEnabled);
 
         setupFingerprintPreference(preferenceCategoryDetails, fPrintEnabled);
 
+        setupDeviceCredentialsPreference(preferenceCategoryDetails, fDeviceCredentialsEnabled);
+
         setupHiddenFilesPreference(preferenceCategoryDetails, fShowHiddenFilesEnabled);
 
         setupExpertModePreference(preferenceCategoryDetails);
@@ -618,6 +626,39 @@ public class Preferences extends PreferenceActivity
         }
     }
 
+    private void setupDeviceCredentialsPreference(PreferenceCategory preferenceCategoryDetails,
+                                                  boolean fDeviceCredentialsEnabled) {
+        SwitchPreference useDeviceCredentials = (SwitchPreference) findPreference(PREFERENCE_USE_DEVICE_CREDENTIALS);
+        if (useDeviceCredentials != null && fDeviceCredentialsEnabled && Build.VERSION.SDK_INT >=
+                Build.VERSION_CODES.M) {
+            if (!DeviceCredentialUtils.areCredentialsAvailable(getApplicationContext())) {
+                DisplayUtils.showSnackMessage(this, R.string.prefs_device_credentials_not_setup);
+                useDeviceCredentials.setChecked(false);
+            } else {
+                useDeviceCredentials.setOnPreferenceChangeListener(
+                        new OnPreferenceChangeListener() {
+                    @Override
+                    public boolean onPreferenceChange(Preference preference, Object newValue) {
+                        Boolean incoming = (Boolean) newValue;
+                        if (incoming) {
+                            SharedPreferences appPrefs = PreferenceManager
+                                    .getDefaultSharedPreferences(getApplicationContext());
+                            SharedPreferences.Editor editor = appPrefs.edit();
+                            editor.putBoolean(PREFERENCE_USE_DEVICE_CREDENTIALS, true).apply();
+                            return true;
+                        } else {
+                            Intent i = new Intent(getApplicationContext(), RequestCredentialsActivity.class);
+                            startActivityForResult(i, ACTION_CONFIRM_DEVICE_CREDENTIALS);
+                            return false;
+                        }
+                    }
+                });
+            }
+        } else {
+            preferenceCategoryDetails.removePreference(useDeviceCredentials);
+        }
+    }
+
     private void setupPasscodePreference(PreferenceCategory preferenceCategoryDetails, boolean fPassCodeEnabled) {
         pCode = (SwitchPreference) findPreference(PassCodeActivity.PREFERENCE_SET_PASSCODE);
         if (pCode != null && fPassCodeEnabled) {
@@ -869,11 +910,21 @@ public class Preferences extends PreferenceActivity
             }
         } else if (requestCode == ACTION_REQUEST_CODE_DAVDROID_SETUP && resultCode == RESULT_OK) {
             DisplayUtils.showSnackMessage(this, R.string.prefs_calendar_contacts_sync_setup_successful);
+        } else if (requestCode == ACTION_CONFIRM_DEVICE_CREDENTIALS && resultCode == RESULT_OK) {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
+                    data.getBooleanExtra(RequestCredentialsActivity.KEY_CHECK_RESULT, false)) {
+                SharedPreferences.Editor appPrefs = PreferenceManager
+                        .getDefaultSharedPreferences(getApplicationContext()).edit();
+                appPrefs.putBoolean(PREFERENCE_USE_DEVICE_CREDENTIALS, false).apply();
+
+                DisplayUtils.showSnackMessage(this, R.string.credentials_disabled);
+            }
         }
     }
 
     @NonNull
     @Override
+    @NonNull
     public MenuInflater getMenuInflater() {
         return getDelegate().getMenuInflater();
     }

+ 65 - 0
src/main/java/com/owncloud/android/ui/activity/RequestCredentialsActivity.java

@@ -0,0 +1,65 @@
+package com.owncloud.android.ui.activity;
+
+import android.app.Activity;
+import android.app.KeyguardManager;
+import android.content.Context;
+import android.content.Intent;
+import android.support.annotation.RequiresApi;
+import android.widget.Toast;
+
+import com.owncloud.android.R;
+import com.owncloud.android.lib.common.utils.Log_OC;
+import com.owncloud.android.utils.AnalyticsUtils;
+import com.owncloud.android.utils.DeviceCredentialUtils;
+
+/**
+ * Dummy activity that is used to handle the device's default authentication workflow.
+ */
+@RequiresApi(value = 23)
+public class RequestCredentialsActivity extends Activity {
+
+    private static final String TAG = RequestCredentialsActivity.class.getSimpleName();
+    private static final String SCREEN_NAME = "Device credentials";
+
+    public final static String KEY_CHECK_RESULT = "KEY_CHECK_RESULT";
+    private static final int REQUEST_CODE_CONFIRM_DEVICE_CREDENTIALS = 1;
+
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+        if (requestCode == REQUEST_CODE_CONFIRM_DEVICE_CREDENTIALS) {
+            if (resultCode == Activity.RESULT_OK && DeviceCredentialUtils.tryEncrypt()) {
+                finishWithResult(true);
+            } else {
+                Toast.makeText(this, R.string.default_credentials_wrong, Toast.LENGTH_SHORT).show();
+                requestCredentials();
+            }
+        }
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+        AnalyticsUtils.setCurrentScreenName(this, SCREEN_NAME, TAG);
+        DeviceCredentialUtils.createKey();
+        requestCredentials();
+    }
+
+    private void requestCredentials() {
+        KeyguardManager keyguardManager = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
+        if (keyguardManager != null) {
+            Intent i = keyguardManager.createConfirmDeviceCredentialIntent(null, null);
+            startActivityForResult(i, REQUEST_CODE_CONFIRM_DEVICE_CREDENTIALS);
+        } else {
+            Log_OC.e(TAG, "Keyguard manager is null");
+            finishWithResult(false);
+        }
+    }
+
+    private void finishWithResult(boolean success) {
+        Intent resultIntent = new Intent();
+        resultIntent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
+        resultIntent.putExtra(KEY_CHECK_RESULT, success);
+        setResult(Activity.RESULT_OK, resultIntent);
+        finish();
+    }
+}

+ 111 - 0
src/main/java/com/owncloud/android/utils/DeviceCredentialUtils.java

@@ -0,0 +1,111 @@
+package com.owncloud.android.utils;
+
+import android.app.KeyguardManager;
+import android.content.Context;
+import android.security.keystore.KeyGenParameterSpec;
+import android.security.keystore.KeyProperties;
+import android.support.annotation.RequiresApi;
+
+import com.owncloud.android.lib.common.utils.Log_OC;
+
+import java.io.IOException;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.CertificateException;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.KeyGenerator;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+
+/**
+ * Utility class with methods for handling device credentials.
+ */
+@RequiresApi(value = 23)
+public class DeviceCredentialUtils {
+
+    private static final String TAG = DeviceCredentialUtils.class.getSimpleName();
+
+    /** Alias for our key in the Android Key Store. */
+    private static final String KEY_NAME = "Nextcloud";
+    private static final byte[] SECRET_BYTE_ARRAY = new byte[] {1, 2, 3, 4, 5, 6};
+
+    private static final int AUTHENTICATION_DURATION_SECONDS = 30;
+
+    private static final String ANDROID_KEY_STORE = "AndroidKeyStore";
+
+    public static boolean areCredentialsAvailable(Context context) {
+        KeyguardManager keyguardManager = (KeyguardManager) context.getSystemService(
+                Context.KEYGUARD_SERVICE);
+        if (keyguardManager != null) {
+            return keyguardManager.isKeyguardSecure();
+        } else {
+            Log_OC.e(TAG, "Keyguard manager is null");
+            return false;
+        }
+    }
+
+    /**
+     * Creates a symmetric key in the Android Key Store which can only be used after the user has
+     * authenticated with device credentials within the last X seconds.
+     */
+    public static void createKey() {
+        // Generate a key to decrypt payment credentials, tokens, etc.
+        try {
+            KeyStore keyStore = KeyStore.getInstance(ANDROID_KEY_STORE);
+            keyStore.load(null);
+            KeyGenerator keyGenerator = KeyGenerator.getInstance(
+                    KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE);
+
+            // Set the alias of the entry in Android KeyStore where the key will appear
+            // and the constrains (purposes) in the constructor of the Builder
+            keyGenerator.init(new KeyGenParameterSpec.Builder(KEY_NAME,
+                    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_OC.e(TAG, "Exception: " + e.getMessage());
+        }
+    }
+
+    /**
+     * Tries to encrypt some data with the generated key in {@link #createKey} which
+     * only works if the user has just authenticated via device credentials.
+     */
+    public static boolean tryEncrypt() {
+        try {
+            KeyStore keyStore = KeyStore.getInstance(ANDROID_KEY_STORE);
+            keyStore.load(null);
+            SecretKey secretKey = (SecretKey) keyStore.getKey(KEY_NAME, 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, you will reach here.
+            return true;
+        } catch (BadPaddingException | IllegalBlockSizeException | KeyStoreException |
+                CertificateException | UnrecoverableKeyException | IOException
+                | NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException e) {
+            return false;
+        }
+    }
+}

+ 1 - 0
src/main/res/values/setup.xml

@@ -91,6 +91,7 @@
     <!-- Help, imprint and feedback, and other things -->
     <bool name="fingerprint_enabled">true</bool>
     <bool name="passcode_enabled">true</bool>
+    <bool name="device_credentials">true</bool>
     <bool name="show_hidden_files_enabled">true</bool>
     <bool name="davdroid_integration_enabled">true</bool>
     <bool name="help_enabled">true</bool>

+ 4 - 0
src/main/res/values/strings.xml

@@ -42,6 +42,8 @@
     <string name="prefs_passcode">Passcode lock</string>
     <string name="prefs_fingerprint">Fingerprint lock</string>
     <string name="prefs_fingerprint_notsetup">No fingerprints have been set up.</string>
+    <string name="prefs_use_device_credentials">Lock using device credentials</string>
+    <string name="prefs_device_credentials_not_setup">No device credentials have been set up.</string>
     <string name="prefs_expert_mode">Expert mode</string>
     <string name="prefs_show_hidden_files">Show hidden files</string>
     <string name="prefs_log_delete_history_button">Delete history</string>
@@ -200,6 +202,8 @@
     <string name="foreign_files_remote_text">Remote: %1$s</string>
     <string name="upload_query_move_foreign_files">Insufficient space prevents copying the selected files into the %1$s folder. Would you like to move them there instead?</string>
     <string name="pass_code_enter_pass_code">Please enter your passcode</string>
+    <string name="default_credentials_wrong">Incorrect credentials</string>
+    <string name="credentials_disabled">Credentials disabled</string>
     
     <string name="pass_code_configure_your_pass_code">Enter your passcode</string>
     <string name="pass_code_configure_your_pass_code_explanation">The passcode will be requested every time the app is started</string>

+ 3 - 0
src/main/res/xml/preferences.xml

@@ -45,6 +45,9 @@
 		<com.owncloud.android.ui.ThemeableSwitchPreference
 			android:title="@string/prefs_fingerprint"
 			android:key="use_fingerprint"/>
+		<com.owncloud.android.ui.ThemeableSwitchPreference
+			android:title="@string/prefs_use_device_credentials"
+			android:key="use_device_credentials"/>
 		<com.owncloud.android.ui.ThemeableSwitchPreference
 			android:title="@string/prefs_show_hidden_files"
 			android:key="show_hidden_files"/>