CVE-2024-39846: Insufficient Information (NVD-CWE-noinfo)
value: NewPass before 1.2.0 stores passwords (rather than password hashes) directly, which makes it easier to obtain unauthorized access to sensitive information. NOTE: in each case, data at rest is encrypted, but is decrypted within process memory during use.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-06-29T21:15Z
Repository: https://github.com/6eero/NewPass
Language: Java
Files changed: app/src/main/java/com/gero/newpass/encryption/PasswordUtils.java, app/src/main/java/com/gero/newpass/view/activities/LoginActivity.java, app/src/main/java/com/gero/newpass/viewmodel/LoginViewModel.java
Commit date: 2024-05-31 22:17:46 +0200
Commit message: Feature: Store password hash in encrypted shared preferences

Previously, the database encryption password (used for login as well and created during the first launch) was directly stored encrypted in EncryptedSharedPreferences. With this commit, when a user inputs the password to create the user during the first launch, the password is hashed using PBKDF2WithHmacSHA256, and the resulting hash is stored in encrypted shared preferences.

Then, every time the user inputs the known password to login, the password is hashed using the same algorithm, the same number of iterations, and is confronted with the (hashed) password retrieved from the EncryptedSharedPreferences.

This improves security by ensuring that the actual password is not stored directly, but rather its hash, making it more difficult for unauthorized access to obtain sensitive information.

Diff: diff --git a/app/src/main/java/com/gero/newpass/encryption/PasswordUtils.java b/app/src/main/java/com/gero/newpass/encryption/PasswordUtils.java
new file mode 100644
index 0000000..5f2d71f
--- /dev/null
+++ b/app/src/main/java/com/gero/newpass/encryption/PasswordUtils.java
@@ -0,0 +1,61 @@
+package com.gero.newpass.encryption;
+
+import android.util.Base64;
+import android.util.Log;
+
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.security.spec.InvalidKeySpecException;
+import java.util.Arrays;
+
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.PBEKeySpec;
+
+public class PasswordUtils {
+
+    private static final int SALT_LENGTH = 16;   // in bytes (at least 16 bytes)
+    private static final int HASH_LENGTH = 256; // in bits
+    private static final int ITERATIONS = 10000;
+
+    public static String hashPassword(String password) throws NoSuchAlgorithmException, InvalidKeySpecException {
+
+        SecureRandom random = new SecureRandom();
+        byte[] salt = new byte[SALT_LENGTH];
+        random.nextBytes(salt);
+
+
+        PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), salt, ITERATIONS, HASH_LENGTH);
+        SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
+        byte[] hash = keyFactory.generateSecret(spec).getEncoded();
+
+        String saltBase64 = Base64.encodeToString(salt, Base64.NO_WRAP);
+        String hashBase64 = Base64.encodeToString(hash, Base64.NO_WRAP);
+
+        return saltBase64 + ":" + hashBase64;
+    }
+
+    public static boolean verifyPassword(String password, String storedPassword) throws NoSuchAlgorithmException, InvalidKeySpecException {
+
+        String[] parts = storedPassword.split(":");
+
+        if (parts.length != 2) {
+            throw new IllegalArgumentException("Stored password must have the format 'salt:hash'");
+        }
+
+        String saltBase64 = parts[0];
+        String hashBase64 = parts[1];
+
+        byte[] salt = Base64.decode(saltBase64, Base64.NO_WRAP);
+        byte[] hash = Base64.decode(hashBase64, Base64.NO_WRAP);
+
+        /*
+        The password provided by the user during the login attempt is hashed using the same algorithm,
+        the same number of iterations and the same salt retrieved from the database.
+         */
+        PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), salt, ITERATIONS, hash.length * 8);
+        SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
+        byte[] testHash = keyFactory.generateSecret(spec).getEncoded();
+
+        return java.util.Arrays.equals(hash, testHash);
+    }
+}
diff --git a/app/src/main/java/com/gero/newpass/view/activities/LoginActivity.java b/app/src/main/java/com/gero/newpass/view/activities/LoginActivity.java
index 1e6794f..638133a 100644
--- a/app/src/main/java/com/gero/newpass/view/activities/LoginActivity.java
+++ b/app/src/main/java/com/gero/newpass/view/activities/LoginActivity.java
@@ -37,10 +37,12 @@ import com.gero.newpass.utilities.AnimationsUtility;
 import com.gero.newpass.utilities.StringHelper;
 import com.gero.newpass.utilities.SystemBarColorHelper;
 import com.gero.newpass.utilities.VibrationHelper;
 import com.gero.newpass.viewmodel.LoginViewModel;
 
+import java.security.NoSuchAlgorithmException;
+import java.security.spec.InvalidKeySpecException;
 import java.util.Locale;
 
 public class LoginActivity extends AppCompatActivity {
 
     private EditText passwordEntry;
@@ -71,15 +73,15 @@ public class LoginActivity extends AppCompatActivity {
         loginViewModel.getLoginMessageLiveData().observe(this, message -> {
             // Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
         });
 
         loginViewModel.getLoginSuccessLiveData().observe(this, success -> {
-            String savedPasswordSharedPreferences = encryptedSharedPreferences.getString("password", "");
+            String hashedPassword = encryptedSharedPreferences.getString("password", "");
 
             if (success) {
                 Intent intent = new Intent(LoginActivity.this, MainViewActivity.class);
-                StringHelper.setSharedString(savedPasswordSharedPreferences);
+                StringHelper.setSharedString(hashedPassword);
                 startActivity(intent);
                 finish();
             } else {
                 AnimationsUtility.errorAnimation(buttonRegisterOrUnlock, textViewRegisterOrUnlock);
             }
@@ -88,12 +90,12 @@ public class LoginActivity extends AppCompatActivity {
         encryptedSharedPreferences = EncryptionHelper.getEncryptedSharedPreferences(getApplicationContext());
 
         //Determining whether to set dark or light mode based on shared preferences
         SharedPreferencesHelper.toggleDarkLightModeUI(this);
 
-        String password = encryptedSharedPreferences.getString("password", "");
-        Boolean isPasswordEmpty = password.isEmpty();
+        String hashedPassword = encryptedSharedPreferences.getString("password", "");
+        Boolean isPasswordEmpty = hashedPassword.isEmpty();
 
         if (!isPasswordEmpty) {
             textViewRegisterOrUnlock.setText(getString(R.string.unlock_newpass_button_text));
             welcomeTextView.setText(getString(R.string.welcome_back_newpass_text));
 
@@ -177,11 +179,15 @@ public class LoginActivity extends AppCompatActivity {
     private void registerUser() {
         Log.d("LOGIN_VM", "First launch");
 
         buttonRegisterOrUnlock.setOnClickListener(v -> {
             String passwordInput = passwordEntry.getText().toString();
-            loginViewModel.createUser(passwordInput, encryptedSharedPreferences);
+            try {
+                loginViewModel.createUser(passwordInput, encryptedSharedPreferences);
+            } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
+                throw new RuntimeException(e);
+            }
             VibrationHelper.vibrate(v, VibrationHelper.VibrationType.Strong);
         });
     }
 
     private void hideUI(boolean bool) {
@@ -211,11 +217,15 @@ public class LoginActivity extends AppCompatActivity {
                 case MotionEvent.ACTION_DOWN:
                     VibrationHelper.vibrate(v, VibrationHelper.VibrationType.Weak);
                     return true;
                 case MotionEvent.ACTION_UP:
                     v.performClick();
-                    loginViewModel.loginUserWithPassword(passwordInput, encryptedSharedPreferences);
+                    try {
+                        loginViewModel.loginUserWithPassword(passwordInput, encryptedSharedPreferences);
+                    } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
+                        throw new RuntimeException(e);
+                    }
                     VibrationHelper.vibrate(v, VibrationHelper.VibrationType.Strong);
                     return true;
             }
             return false;
         });
diff --git a/app/src/main/java/com/gero/newpass/viewmodel/LoginViewModel.java b/app/src/main/java/com/gero/newpass/viewmodel/LoginViewModel.java
index bcd5ae0..f5ae9d6 100644
--- a/app/src/main/java/com/gero/newpass/viewmodel/LoginViewModel.java
+++ b/app/src/main/java/com/gero/newpass/viewmodel/LoginViewModel.java
@@ -13,12 +13,15 @@ import androidx.lifecycle.LiveData;
 import androidx.lifecycle.MutableLiveData;
 import androidx.lifecycle.ViewModel;
 import androidx.security.crypto.EncryptedSharedPreferences;
 
 import com.gero.newpass.R;
+import com.gero.newpass.encryption.PasswordUtils;
 import com.gero.newpass.repository.ResourceRepository;
 
+import java.security.NoSuchAlgorithmException;
+import java.security.spec.InvalidKeySpecException;
 import java.util.concurrent.Executor;
 
 public class LoginViewModel extends ViewModel {
 
     private final MutableLiveData<String> loginMessageLiveData = new MutableLiveData<>();
@@ -38,15 +41,15 @@ public class LoginViewModel extends ViewModel {
         return loginSuccessLiveData;
     }
 
 
 
-    public void loginUserWithPassword(String password, EncryptedSharedPreferences sharedPreferences) {
+    public void loginUserWithPassword(String password, EncryptedSharedPreferences sharedPreferences) throws NoSuchAlgorithmException, InvalidKeySpecException {
 
-        String savedPasswordSharedPreferences = sharedPreferences.getString("password", "");
+        String hashedPassword = sharedPreferences.getString("password", "");
 
-        if (savedPasswordSharedPreferences.equals(password)) {
+        if (PasswordUtils.verifyPassword(password, hashedPassword)) {
             loginSuccessLiveData.setValue(true);
             loginMessageLiveData.setValue(resourceRepository.getString(R.string.login_done));
         } else {
             loginSuccessLiveData.setValue(false);
             loginMessageLiveData.setValue(resourceRepository.getString(R.string.access_denied));
@@ -86,15 +89,17 @@ public class LoginViewModel extends ViewModel {
 
         biometricPrompt.authenticate(promptInfo);
     }
 
 
-    public void createUser(String password, EncryptedSharedPreferences sharedPreferences) {
+    public void createUser(String password, EncryptedSharedPreferences sharedPreferences) throws NoSuchAlgorithmException, InvalidKeySpecException {
 
         if (password.length() >= 4) {
             SharedPreferences.Editor editor = sharedPreferences.edit();
-            editor.putString("password", password);
+
+            String hashedPassword = PasswordUtils.hashPassword(password);
+            editor.putString("password", hashedPassword);
             editor.apply();
 
             loginSuccessLiveData.setValue(true);
             loginMessageLiveData.setValue(resourceRepository.getString(R.string.user_created_successfully));
         } else {

Vulnerable code:             String savedPasswordSharedPreferences = encryptedSharedPreferences.getString("password", "");
                StringHelper.setSharedString(savedPasswordSharedPreferences);
        String password = encryptedSharedPreferences.getString("password", "");
        Boolean isPasswordEmpty = password.isEmpty();
            loginViewModel.createUser(passwordInput, encryptedSharedPreferences);
                    loginViewModel.loginUserWithPassword(passwordInput, encryptedSharedPreferences);
    public void loginUserWithPassword(String password, EncryptedSharedPreferences sharedPreferences) {
        String savedPasswordSharedPreferences = sharedPreferences.getString("password", "");
        if (savedPasswordSharedPreferences.equals(password)) {
    public void createUser(String password, EncryptedSharedPreferences sharedPreferences) {
            editor.putString("password", password);
Fixed code: package com.gero.newpass.encryption;

import android.util.Base64;
import android.util.Log;

import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.util.Arrays;

import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;

public class PasswordUtils {

    private static final int SALT_LENGTH = 16;   // in bytes (at least 16 bytes)
    private static final int HASH_LENGTH = 256; // in bits
    private static final int ITERATIONS = 10000;

    public static String hashPassword(String password) throws NoSuchAlgorithmException, InvalidKeySpecException {

        SecureRandom random = new SecureRandom();
        byte[] salt = new byte[SALT_LENGTH];
        random.nextBytes(salt);


        PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), salt, ITERATIONS, HASH_LENGTH);
        SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
        byte[] hash = keyFactory.generateSecret(spec).getEncoded();

        String saltBase64 = Base64.encodeToString(salt, Base64.NO_WRAP);
        String hashBase64 = Base64.encodeToString(hash, Base64.NO_WRAP);

        return saltBase64 + ":" + hashBase64;
    }

    public static boolean verifyPassword(String password, String storedPassword) throws NoSuchAlgorithmException, InvalidKeySpecException {

        String[] parts = storedPassword.split(":");

        if (parts.length != 2) {
            throw new IllegalArgumentException("Stored password must have the format 'salt:hash'");
        }

        String saltBase64 = parts[0];
        String hashBase64 = parts[1];

        byte[] salt = Base64.decode(saltBase64, Base64.NO_WRAP);
        byte[] hash = Base64.decode(hashBase64, Base64.NO_WRAP);

        /*
        The password provided by the user during the login attempt is hashed using the same algorithm,
        the same number of iterations and the same salt retrieved from the database.
         */
        PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), salt, ITERATIONS, hash.length * 8);
        SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
        byte[] testHash = keyFactory.generateSecret(spec).getEncoded();

        return java.util.Arrays.equals(hash, testHash);
    }
}
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
            String hashedPassword = encryptedSharedPreferences.getString("password", "");
                StringHelper.setSharedString(hashedPassword);
        String hashedPassword = encryptedSharedPreferences.getString("password", "");
        Boolean isPasswordEmpty = hashedPassword.isEmpty();
            try {
                loginViewModel.createUser(passwordInput, encryptedSharedPreferences);
            } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
                throw new RuntimeException(e);
            }
                    try {
                        loginViewModel.loginUserWithPassword(passwordInput, encryptedSharedPreferences);
                    } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
                        throw new RuntimeException(e);
                    }
import com.gero.newpass.encryption.PasswordUtils;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
    public void loginUserWithPassword(String password, EncryptedSharedPreferences sharedPreferences) throws NoSuchAlgorithmException, InvalidKeySpecException {
        String hashedPassword = sharedPreferences.getString("password", "");
        if (PasswordUtils.verifyPassword(password, hashedPassword)) {
    public void createUser(String password, EncryptedSharedPreferences sharedPreferences) throws NoSuchAlgorithmException, InvalidKeySpecException {

            String hashedPassword = PasswordUtils.hashPassword(password);
            editor.putString("password", hashedPassword);