Эх сурвалжийг харах

Merge pull request #7997 from nextcloud/pinBruteForce

Pin prompt: add brute force: delay from 1 to 10s when wrong input
Andy Scherzinger 4 жил өмнө
parent
commit
6c9a639bd6

+ 6 - 0
src/main/java/com/nextcloud/client/preferences/AppPreferences.java

@@ -355,4 +355,10 @@ public interface AppPreferences {
     boolean isPowerCheckDisabled();
 
     void setPowerCheckDisabled(boolean value);
+
+    void increasePinWrongAttempts();
+
+    void resetPinWrongAttempts();
+
+    int pinBruteForceDelay();
 }

+ 23 - 0
src/main/java/com/nextcloud/client/preferences/AppPreferencesImpl.java

@@ -39,6 +39,7 @@ import java.util.Set;
 import java.util.concurrent.CopyOnWriteArraySet;
 
 import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
 
 import static com.owncloud.android.ui.fragment.OCFileListFragment.FOLDER_LAYOUT_LIST;
 
@@ -88,6 +89,7 @@ public final class AppPreferencesImpl implements AppPreferences {
     private static final String PREF__MIGRATED_USER_ID = "migrated_user_id";
     private static final String PREF__PHOTO_SEARCH_TIMESTAMP = "photo_search_timestamp";
     private static final String PREF__POWER_CHECK_DISABLED = "power_check_disabled";
+    private static final String PREF__PIN_BRUTE_FORCE_COUNT = "pin_brute_force_count";
 
     private final Context context;
     private final SharedPreferences preferences;
@@ -641,4 +643,25 @@ public final class AppPreferencesImpl implements AppPreferences {
     public void setPowerCheckDisabled(boolean value) {
         preferences.edit().putBoolean(PREF__POWER_CHECK_DISABLED, value).apply();
     }
+
+    public void increasePinWrongAttempts() {
+        int count = preferences.getInt(PREF__PIN_BRUTE_FORCE_COUNT, 0);
+        preferences.edit().putInt(PREF__PIN_BRUTE_FORCE_COUNT, count + 1).apply();
+    }
+
+    @Override
+    public void resetPinWrongAttempts() {
+        preferences.edit().putInt(PREF__PIN_BRUTE_FORCE_COUNT, 0).apply();
+    }
+
+    public int pinBruteForceDelay() {
+        int count = preferences.getInt(PREF__PIN_BRUTE_FORCE_COUNT, 0);
+
+        return computeBruteForceDelay(count);
+    }
+
+    @VisibleForTesting
+    public int computeBruteForceDelay(int count) {
+        return (int) Math.min(count / 3d, 10);
+    }
 }

+ 41 - 1
src/main/java/com/owncloud/android/ui/activity/PassCodeActivity.java

@@ -123,6 +123,8 @@ public class PassCodeActivity extends AppCompatActivity implements Injectable {
             binding.explanation.setVisibility(View.INVISIBLE);
             setCancelButtonEnabled(false);      // no option to cancel
 
+            showDelay();
+
         } else if (ACTION_REQUEST_WITH_RESULT.equals(getIntent().getAction())) {
             if (savedInstanceState != null) {
                 confirmingPassCode = savedInstanceState.getBoolean(PassCodeActivity.KEY_CONFIRMING_PASSCODE);
@@ -235,18 +237,22 @@ public class PassCodeActivity extends AppCompatActivity implements Injectable {
     private void processFullPassCode() {
         if (ACTION_CHECK.equals(getIntent().getAction())) {
             if (checkPassCode()) {
+                preferences.resetPinWrongAttempts();
+
                 /// pass code accepted in request, user is allowed to access the app
                 AppPreferencesImpl.fromContext(this).setLockTimestamp(SystemClock.elapsedRealtime());
                 hideSoftKeyboard();
                 finish();
 
             }  else {
+                preferences.increasePinWrongAttempts();
+
                 showErrorAndRestart(R.string.pass_code_wrong, R.string.pass_code_enter_pass_code, View.INVISIBLE);
             }
 
         } else if (ACTION_CHECK_WITH_RESULT.equals(getIntent().getAction())) {
             if (checkPassCode()) {
-                AppPreferencesImpl.fromContext(this).setLockTimestamp(SystemClock.elapsedRealtime());
+                preferences.setLockTimestamp(SystemClock.elapsedRealtime());
                 Intent resultIntent = new Intent();
                 resultIntent.putExtra(KEY_CHECK_RESULT, true);
                 setResult(RESULT_OK, resultIntent);
@@ -292,6 +298,8 @@ public class PassCodeActivity extends AppCompatActivity implements Injectable {
         binding.header.setText(headerMessage);                          // TODO check if really needed
         binding.explanation.setVisibility(explanationVisibility); // TODO check if really needed
         clearBoxes();
+
+        showDelay();
     }
 
 
@@ -385,6 +393,38 @@ public class PassCodeActivity extends AppCompatActivity implements Injectable {
         finish();
     }
 
+    private void showDelay() {
+        int delay = preferences.pinBruteForceDelay();
+
+        if (delay > 0) {
+            binding.explanation.setText(R.string.brute_force_delay);
+            binding.explanation.setVisibility(View.VISIBLE);
+            binding.txt0.setEnabled(false);
+            binding.txt1.setEnabled(false);
+            binding.txt2.setEnabled(false);
+            binding.txt3.setEnabled(false);
+
+            new Thread(new Runnable() {
+                @Override
+                public void run() {
+                    try {
+                        Thread.sleep(delay * 1000);
+
+                        runOnUiThread(() -> {
+                            binding.explanation.setVisibility(View.INVISIBLE);
+                            binding.txt0.setEnabled(true);
+                            binding.txt1.setEnabled(true);
+                            binding.txt2.setEnabled(true);
+                            binding.txt3.setEnabled(true);
+                        });
+                    } catch (InterruptedException e) {
+                        Log_OC.e(this, "Could not delay password input prompt");
+                    }
+                }
+            }).start();
+        }
+    }
+
 
     @Override
     public void onSaveInstanceState(@NonNull Bundle outState) {

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

@@ -953,4 +953,5 @@
     <string name="dialog_close">Close</string>
     <string name="direct_login_failed">Login via direct link failed!</string>
     <string name="login_url_helper_text">The link to your %1$s web interface when you open it in the browser.</string>
+    <string name="brute_force_delay">Delayed due to too many wrong attempts</string>
 </resources>

+ 14 - 0
src/test/java/com/nextcloud/client/preferences/TestAppPreferences.java

@@ -13,6 +13,7 @@ import org.mockito.InOrder;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import static org.junit.Assert.assertEquals;
 import static org.mockito.Mockito.anyString;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.inOrder;
@@ -157,5 +158,18 @@ public class TestAppPreferences {
             inOrder.verify(editor).remove("prefs_instant_behaviour");
             inOrder.verify(editor).apply();
         }
+
+        @Test
+        public void testBruteForceDelay() {
+            assertEquals(0, appPreferences.computeBruteForceDelay(0));
+            assertEquals(0, appPreferences.computeBruteForceDelay(2));
+            assertEquals(1, appPreferences.computeBruteForceDelay(3));
+            assertEquals(1, appPreferences.computeBruteForceDelay(5));
+            assertEquals(2, appPreferences.computeBruteForceDelay(6));
+            assertEquals(3, appPreferences.computeBruteForceDelay(11));
+            assertEquals(8, appPreferences.computeBruteForceDelay(25));
+            assertEquals(10, appPreferences.computeBruteForceDelay(50));
+            assertEquals(10, appPreferences.computeBruteForceDelay(100));
+        }
     }
 }