diff --git a/.vscode/tasks.json b/.vscode/tasks.json
index 636aa3210..2ac634ecf 100644
--- a/.vscode/tasks.json
+++ b/.vscode/tasks.json
@@ -125,6 +125,20 @@
"options": {
"cwd": "${workspaceFolder}/mobile-app"
}
+ },
+ {
+ "label": "Run Android App",
+ "type": "shell",
+ "command": "npx",
+ "args": ["expo", "run:android"],
+ "problemMatcher": [],
+ "group": {
+ "kind": "build",
+ "isDefault": true
+ },
+ "options": {
+ "cwd": "${workspaceFolder}/mobile-app"
+ }
}
]
}
diff --git a/docs/misc/dev/mobile-app-development.md b/docs/misc/dev/mobile-app-development.md
index 9ed233df7..2a176c9bc 100644
--- a/docs/misc/dev/mobile-app-development.md
+++ b/docs/misc/dev/mobile-app-development.md
@@ -20,3 +20,28 @@ Run iOS on Simulator:
```
npx expo run:ios
```
+
+Install OpenJDK for Android dev:
+
+```
+brew install openjdk@17
+
+# Add to path
+sudo ln -sfn /opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk /Library/Java/JavaVirtualMachines/openjdk-17.jdk
+
+# Test if Java works on CLI
+java --version
+```
+
+Make sure NDK is installed:
+
+1. Open Android Studio.
+2. Go to: `Preferences > Appearance & Behavior > System Settings > Android SDK > SDK Tools tab`.
+3. Check NDK (Side by side).
+4. Click Apply or OK to install it.
+
+If getting `node` errors in Android studio, close and re-open Android Studio from CLI via:
+
+```
+open -a "Android Studio"
+```
\ No newline at end of file
diff --git a/mobile-app/android/app/build.gradle b/mobile-app/android/app/build.gradle
index 744697d24..ef0ac0e34 100644
--- a/mobile-app/android/app/build.gradle
+++ b/mobile-app/android/app/build.gradle
@@ -150,6 +150,9 @@ dependencies {
// The version of react-native is set by the React Native Gradle Plugin
implementation("com.facebook.react:react-android")
+ // Add biometric dependency for credential management
+ implementation("androidx.biometric:biometric:1.1.0")
+
def isGifEnabled = (findProperty('expo.gif.enabled') ?: "") == "true";
def isWebpEnabled = (findProperty('expo.webp.enabled') ?: "") == "true";
def isWebpAnimatedEnabled = (findProperty('expo.webp.animated') ?: "") == "true";
diff --git a/mobile-app/android/app/src/main/AndroidManifest.xml b/mobile-app/android/app/src/main/AndroidManifest.xml
index 4b9278649..1a6f3049c 100644
--- a/mobile-app/android/app/src/main/AndroidManifest.xml
+++ b/mobile-app/android/app/src/main/AndroidManifest.xml
@@ -4,6 +4,7 @@
+
diff --git a/mobile-app/android/app/src/main/java/com/aliasvault/Credential.java b/mobile-app/android/app/src/main/java/com/aliasvault/Credential.java
new file mode 100644
index 000000000..d94807c21
--- /dev/null
+++ b/mobile-app/android/app/src/main/java/com/aliasvault/Credential.java
@@ -0,0 +1,25 @@
+package com.aliasvault;
+
+public class Credential {
+ private String username;
+ private String password;
+ private String service;
+
+ public Credential(String username, String password, String service) {
+ this.username = username;
+ this.password = password;
+ this.service = service;
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public String getService() {
+ return service;
+ }
+}
\ No newline at end of file
diff --git a/mobile-app/android/app/src/main/java/com/aliasvault/CredentialManagerModule.java b/mobile-app/android/app/src/main/java/com/aliasvault/CredentialManagerModule.java
new file mode 100644
index 000000000..7f760e093
--- /dev/null
+++ b/mobile-app/android/app/src/main/java/com/aliasvault/CredentialManagerModule.java
@@ -0,0 +1,75 @@
+package com.aliasvault;
+
+import android.util.Log;
+
+import com.facebook.react.bridge.Arguments;
+import com.facebook.react.bridge.Promise;
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.bridge.ReactContextBaseJavaModule;
+import com.facebook.react.bridge.ReactMethod;
+import com.facebook.react.bridge.WritableArray;
+import com.facebook.react.bridge.WritableMap;
+
+import java.util.List;
+
+public class CredentialManagerModule extends ReactContextBaseJavaModule {
+ private static final String TAG = "CredentialManagerModule";
+ private final ReactApplicationContext reactContext;
+
+ public CredentialManagerModule(ReactApplicationContext reactContext) {
+ super(reactContext);
+ this.reactContext = reactContext;
+ }
+
+ @Override
+ public String getName() {
+ return "CredentialManager";
+ }
+
+ @ReactMethod
+ public void addCredential(String username, String password, String service, Promise promise) {
+ try {
+ SharedCredentialStore store = SharedCredentialStore.getInstance(reactContext);
+ Credential credential = new Credential(username, password, service);
+ store.addCredential(credential);
+ promise.resolve(true);
+ } catch (Exception e) {
+ Log.e(TAG, "Error adding credential", e);
+ promise.reject("ERR_ADD_CREDENTIAL", "Failed to add credential: " + e.getMessage(), e);
+ }
+ }
+
+ @ReactMethod
+ public void getCredentials(Promise promise) {
+ try {
+ SharedCredentialStore store = SharedCredentialStore.getInstance(reactContext);
+ List credentials = store.getAllCredentials();
+
+ WritableArray credentialsArray = Arguments.createArray();
+ for (Credential credential : credentials) {
+ WritableMap credentialMap = Arguments.createMap();
+ credentialMap.putString("username", credential.getUsername());
+ credentialMap.putString("password", credential.getPassword());
+ credentialMap.putString("service", credential.getService());
+ credentialsArray.pushMap(credentialMap);
+ }
+
+ promise.resolve(credentialsArray);
+ } catch (Exception e) {
+ Log.e(TAG, "Error getting credentials", e);
+ promise.reject("ERR_GET_CREDENTIALS", "Failed to get credentials: " + e.getMessage(), e);
+ }
+ }
+
+ @ReactMethod
+ public void clearCredentials(Promise promise) {
+ try {
+ SharedCredentialStore store = SharedCredentialStore.getInstance(reactContext);
+ store.clearAllCredentials();
+ promise.resolve(true);
+ } catch (Exception e) {
+ Log.e(TAG, "Error clearing credentials", e);
+ promise.reject("ERR_CLEAR_CREDENTIALS", "Failed to clear credentials: " + e.getMessage(), e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/mobile-app/android/app/src/main/java/com/aliasvault/CredentialManagerPackage.java b/mobile-app/android/app/src/main/java/com/aliasvault/CredentialManagerPackage.java
new file mode 100644
index 000000000..3e772a4ce
--- /dev/null
+++ b/mobile-app/android/app/src/main/java/com/aliasvault/CredentialManagerPackage.java
@@ -0,0 +1,24 @@
+package com.aliasvault;
+
+import com.facebook.react.ReactPackage;
+import com.facebook.react.bridge.NativeModule;
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.uimanager.ViewManager;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class CredentialManagerPackage implements ReactPackage {
+ @Override
+ public List createViewManagers(ReactApplicationContext reactContext) {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public List createNativeModules(ReactApplicationContext reactContext) {
+ List modules = new ArrayList<>();
+ modules.add(new CredentialManagerModule(reactContext));
+ return modules;
+ }
+}
\ No newline at end of file
diff --git a/mobile-app/android/app/src/main/java/com/aliasvault/SharedCredentialStore.java b/mobile-app/android/app/src/main/java/com/aliasvault/SharedCredentialStore.java
new file mode 100644
index 000000000..a8dbad910
--- /dev/null
+++ b/mobile-app/android/app/src/main/java/com/aliasvault/SharedCredentialStore.java
@@ -0,0 +1,220 @@
+package com.aliasvault;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.security.keystore.KeyGenParameterSpec;
+import android.security.keystore.KeyProperties;
+import android.util.Base64;
+import android.util.Log;
+
+import androidx.biometric.BiometricPrompt;
+import androidx.fragment.app.FragmentActivity;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+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.UnrecoverableEntryException;
+import java.security.cert.CertificateException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.KeyGenerator;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.GCMParameterSpec;
+
+public class SharedCredentialStore {
+ private static final String TAG = "SharedCredentialStore";
+ private static final String ANDROID_KEYSTORE = "AndroidKeyStore";
+ private static final String ENCRYPTION_KEY_ALIAS = "aliasvault_encryption_key";
+ private static final String SHARED_PREFS_NAME = "net.aliasvault.autofill";
+ private static final String CREDENTIALS_KEY = "storedCredentials";
+ private static final String IV_SUFFIX = "_iv";
+
+ private static SharedCredentialStore instance;
+ private final Context appContext;
+ private SecretKey cachedEncryptionKey;
+
+ private SharedCredentialStore(Context context) {
+ this.appContext = context.getApplicationContext();
+ }
+
+ public static synchronized SharedCredentialStore getInstance(Context context) {
+ if (instance == null) {
+ instance = new SharedCredentialStore(context);
+ }
+ return instance;
+ }
+
+ private SecretKey getOrCreateEncryptionKey() throws Exception {
+ if (cachedEncryptionKey != null) {
+ return cachedEncryptionKey;
+ }
+
+ try {
+ KeyStore keyStore = KeyStore.getInstance(ANDROID_KEYSTORE);
+ keyStore.load(null);
+
+ // Check if the key exists
+ if (keyStore.containsAlias(ENCRYPTION_KEY_ALIAS)) {
+ // Key exists, retrieve it
+ KeyStore.SecretKeyEntry secretKeyEntry = (KeyStore.SecretKeyEntry) keyStore.getEntry(
+ ENCRYPTION_KEY_ALIAS, null);
+ cachedEncryptionKey = secretKeyEntry.getSecretKey();
+ return cachedEncryptionKey;
+ } else {
+ // Key doesn't exist, create it
+ KeyGenerator keyGenerator = KeyGenerator.getInstance(
+ KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEYSTORE);
+
+ KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec.Builder(
+ ENCRYPTION_KEY_ALIAS,
+ KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
+ .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
+ .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
+ .setKeySize(256)
+ .setUserAuthenticationRequired(true)
+ .build();
+
+ keyGenerator.init(keyGenParameterSpec);
+ cachedEncryptionKey = keyGenerator.generateKey();
+ return cachedEncryptionKey;
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Error getting or creating encryption key", e);
+ throw e;
+ }
+ }
+
+ private Cipher getCipher() throws NoSuchPaddingException, NoSuchAlgorithmException {
+ return Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/"
+ + KeyProperties.BLOCK_MODE_GCM + "/"
+ + KeyProperties.ENCRYPTION_PADDING_NONE);
+ }
+
+ private String encrypt(String data) throws Exception {
+ SecretKey key = getOrCreateEncryptionKey();
+ Cipher cipher = getCipher();
+ cipher.init(Cipher.ENCRYPT_MODE, key);
+
+ byte[] iv = cipher.getIV();
+ byte[] encryptedBytes = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
+
+ // Store IV in SharedPreferences
+ SharedPreferences prefs = appContext.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE);
+ prefs.edit().putString(CREDENTIALS_KEY + IV_SUFFIX, Base64.encodeToString(iv, Base64.DEFAULT)).apply();
+
+ return Base64.encodeToString(encryptedBytes, Base64.DEFAULT);
+ }
+
+ private String decrypt(String encryptedData) throws Exception {
+ SecretKey key = getOrCreateEncryptionKey();
+ Cipher cipher = getCipher();
+
+ // Get IV from SharedPreferences
+ SharedPreferences prefs = appContext.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE);
+ String ivString = prefs.getString(CREDENTIALS_KEY + IV_SUFFIX, null);
+ if (ivString == null) {
+ throw new Exception("IV not found for decryption");
+ }
+
+ byte[] iv = Base64.decode(ivString, Base64.DEFAULT);
+ byte[] encryptedBytes = Base64.decode(encryptedData, Base64.DEFAULT);
+
+ GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(128, iv);
+ cipher.init(Cipher.DECRYPT_MODE, key, gcmParameterSpec);
+
+ byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
+ return new String(decryptedBytes, StandardCharsets.UTF_8);
+ }
+
+ public List getAllCredentials() throws Exception {
+ SharedPreferences prefs = appContext.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE);
+ String encryptedData = prefs.getString(CREDENTIALS_KEY, null);
+
+ if (encryptedData == null) {
+ return new ArrayList<>();
+ }
+
+ String decryptedData = decrypt(encryptedData);
+ return parseCredentialsFromJson(decryptedData);
+ }
+
+ public void addCredential(Credential credential) throws Exception {
+ List credentials = getAllCredentials();
+ credentials.add(credential);
+
+ String jsonData = credentialsToJson(credentials);
+ String encryptedData = encrypt(jsonData);
+
+ SharedPreferences prefs = appContext.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE);
+ prefs.edit().putString(CREDENTIALS_KEY, encryptedData).apply();
+ }
+
+ public void clearAllCredentials() {
+ SharedPreferences prefs = appContext.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE);
+ prefs.edit()
+ .remove(CREDENTIALS_KEY)
+ .remove(CREDENTIALS_KEY + IV_SUFFIX)
+ .apply();
+
+ try {
+ KeyStore keyStore = KeyStore.getInstance(ANDROID_KEYSTORE);
+ keyStore.load(null);
+ if (keyStore.containsAlias(ENCRYPTION_KEY_ALIAS)) {
+ keyStore.deleteEntry(ENCRYPTION_KEY_ALIAS);
+ }
+ cachedEncryptionKey = null;
+ } catch (Exception e) {
+ Log.e(TAG, "Error clearing encryption key", e);
+ }
+ }
+
+ public void clearCache() {
+ cachedEncryptionKey = null;
+ }
+
+ private List parseCredentialsFromJson(String json) throws JSONException {
+ List credentials = new ArrayList<>();
+ JSONArray jsonArray = new JSONArray(json);
+
+ for (int i = 0; i < jsonArray.length(); i++) {
+ JSONObject jsonObject = jsonArray.getJSONObject(i);
+ String username = jsonObject.getString("username");
+ String password = jsonObject.getString("password");
+ String service = jsonObject.getString("service");
+
+ credentials.add(new Credential(username, password, service));
+ }
+
+ return credentials;
+ }
+
+ private String credentialsToJson(List credentials) throws JSONException {
+ JSONArray jsonArray = new JSONArray();
+
+ for (Credential credential : credentials) {
+ JSONObject jsonObject = new JSONObject();
+ jsonObject.put("username", credential.getUsername());
+ jsonObject.put("password", credential.getPassword());
+ jsonObject.put("service", credential.getService());
+
+ jsonArray.put(jsonObject);
+ }
+
+ return jsonArray.toString();
+ }
+}
\ No newline at end of file
diff --git a/mobile-app/android/app/src/main/java/net/aliasvault/app/MainApplication.kt b/mobile-app/android/app/src/main/java/net/aliasvault/app/MainApplication.kt
index c65aded2c..279a0c70f 100644
--- a/mobile-app/android/app/src/main/java/net/aliasvault/app/MainApplication.kt
+++ b/mobile-app/android/app/src/main/java/net/aliasvault/app/MainApplication.kt
@@ -12,6 +12,7 @@ import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
import com.facebook.react.defaults.DefaultReactNativeHost
import com.facebook.react.soloader.OpenSourceMergedSoMapping
import com.facebook.soloader.SoLoader
+import net.aliasvault.app.credentialmanager.CredentialManagerPackage
import expo.modules.ApplicationLifecycleDispatcher
import expo.modules.ReactNativeHostWrapper
@@ -25,6 +26,7 @@ class MainApplication : Application(), ReactApplication {
val packages = PackageList(this).packages
// Packages that cannot be autolinked yet can be added manually here, for example:
// packages.add(new MyReactNativePackage());
+ packages.add(CredentialManagerPackage())
return packages
}
diff --git a/mobile-app/android/app/src/main/java/net/aliasvault/app/credentialmanager/Credential.java b/mobile-app/android/app/src/main/java/net/aliasvault/app/credentialmanager/Credential.java
new file mode 100644
index 000000000..a7dd69525
--- /dev/null
+++ b/mobile-app/android/app/src/main/java/net/aliasvault/app/credentialmanager/Credential.java
@@ -0,0 +1,25 @@
+package net.aliasvault.app.credentialmanager;
+
+public class Credential {
+ private String username;
+ private String password;
+ private String service;
+
+ public Credential(String username, String password, String service) {
+ this.username = username;
+ this.password = password;
+ this.service = service;
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public String getService() {
+ return service;
+ }
+}
\ No newline at end of file
diff --git a/mobile-app/android/app/src/main/java/net/aliasvault/app/credentialmanager/CredentialManagerModule.java b/mobile-app/android/app/src/main/java/net/aliasvault/app/credentialmanager/CredentialManagerModule.java
new file mode 100644
index 000000000..f49de1408
--- /dev/null
+++ b/mobile-app/android/app/src/main/java/net/aliasvault/app/credentialmanager/CredentialManagerModule.java
@@ -0,0 +1,75 @@
+package net.aliasvault.app.credentialmanager;
+
+import android.util.Log;
+
+import com.facebook.react.bridge.Arguments;
+import com.facebook.react.bridge.Promise;
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.bridge.ReactContextBaseJavaModule;
+import com.facebook.react.bridge.ReactMethod;
+import com.facebook.react.bridge.WritableArray;
+import com.facebook.react.bridge.WritableMap;
+
+import java.util.List;
+
+public class CredentialManagerModule extends ReactContextBaseJavaModule {
+ private static final String TAG = "CredentialManagerModule";
+ private final ReactApplicationContext reactContext;
+
+ public CredentialManagerModule(ReactApplicationContext reactContext) {
+ super(reactContext);
+ this.reactContext = reactContext;
+ }
+
+ @Override
+ public String getName() {
+ return "CredentialManager";
+ }
+
+ @ReactMethod
+ public void addCredential(String username, String password, String service, Promise promise) {
+ try {
+ SharedCredentialStore store = SharedCredentialStore.getInstance(reactContext);
+ Credential credential = new Credential(username, password, service);
+ store.addCredential(credential);
+ promise.resolve(true);
+ } catch (Exception e) {
+ Log.e(TAG, "Error adding credential", e);
+ promise.reject("ERR_ADD_CREDENTIAL", "Failed to add credential: " + e.getMessage(), e);
+ }
+ }
+
+ @ReactMethod
+ public void getCredentials(Promise promise) {
+ try {
+ SharedCredentialStore store = SharedCredentialStore.getInstance(reactContext);
+ List credentials = store.getAllCredentials();
+
+ WritableArray credentialsArray = Arguments.createArray();
+ for (Credential credential : credentials) {
+ WritableMap credentialMap = Arguments.createMap();
+ credentialMap.putString("username", credential.getUsername());
+ credentialMap.putString("password", credential.getPassword());
+ credentialMap.putString("service", credential.getService());
+ credentialsArray.pushMap(credentialMap);
+ }
+
+ promise.resolve(credentialsArray);
+ } catch (Exception e) {
+ Log.e(TAG, "Error getting credentials", e);
+ promise.reject("ERR_GET_CREDENTIALS", "Failed to get credentials: " + e.getMessage(), e);
+ }
+ }
+
+ @ReactMethod
+ public void clearCredentials(Promise promise) {
+ try {
+ SharedCredentialStore store = SharedCredentialStore.getInstance(reactContext);
+ store.clearAllCredentials();
+ promise.resolve(true);
+ } catch (Exception e) {
+ Log.e(TAG, "Error clearing credentials", e);
+ promise.reject("ERR_CLEAR_CREDENTIALS", "Failed to clear credentials: " + e.getMessage(), e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/mobile-app/android/app/src/main/java/net/aliasvault/app/credentialmanager/CredentialManagerPackage.java b/mobile-app/android/app/src/main/java/net/aliasvault/app/credentialmanager/CredentialManagerPackage.java
new file mode 100644
index 000000000..f6dd53fad
--- /dev/null
+++ b/mobile-app/android/app/src/main/java/net/aliasvault/app/credentialmanager/CredentialManagerPackage.java
@@ -0,0 +1,24 @@
+package net.aliasvault.app.credentialmanager;
+
+import com.facebook.react.ReactPackage;
+import com.facebook.react.bridge.NativeModule;
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.uimanager.ViewManager;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class CredentialManagerPackage implements ReactPackage {
+ @Override
+ public List createViewManagers(ReactApplicationContext reactContext) {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public List createNativeModules(ReactApplicationContext reactContext) {
+ List modules = new ArrayList<>();
+ modules.add(new CredentialManagerModule(reactContext));
+ return modules;
+ }
+}
\ No newline at end of file
diff --git a/mobile-app/android/app/src/main/java/net/aliasvault/app/credentialmanager/SharedCredentialStore.java b/mobile-app/android/app/src/main/java/net/aliasvault/app/credentialmanager/SharedCredentialStore.java
new file mode 100644
index 000000000..35cc3623a
--- /dev/null
+++ b/mobile-app/android/app/src/main/java/net/aliasvault/app/credentialmanager/SharedCredentialStore.java
@@ -0,0 +1,205 @@
+package net.aliasvault.app.credentialmanager;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.security.keystore.KeyGenParameterSpec;
+import android.security.keystore.KeyProperties;
+import android.util.Base64;
+import android.util.Log;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.nio.charset.StandardCharsets;
+import java.security.KeyStore;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.crypto.Cipher;
+import javax.crypto.KeyGenerator;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.GCMParameterSpec;
+
+public class SharedCredentialStore {
+ private static final String TAG = "SharedCredentialStore";
+ private static final String ANDROID_KEYSTORE = "AndroidKeyStore";
+ private static final String ENCRYPTION_KEY_ALIAS = "aliasvault_encryption_key";
+ private static final String SHARED_PREFS_NAME = "net.aliasvault.autofill";
+ private static final String CREDENTIALS_KEY = "storedCredentials";
+ private static final String IV_SUFFIX = "_iv";
+
+ private static SharedCredentialStore instance;
+ private final Context appContext;
+ private SecretKey cachedEncryptionKey;
+
+ private SharedCredentialStore(Context context) {
+ this.appContext = context.getApplicationContext();
+ }
+
+ public static synchronized SharedCredentialStore getInstance(Context context) {
+ if (instance == null) {
+ instance = new SharedCredentialStore(context);
+ }
+ return instance;
+ }
+
+ private SecretKey getOrCreateEncryptionKey() throws Exception {
+ if (cachedEncryptionKey != null) {
+ return cachedEncryptionKey;
+ }
+
+ try {
+ KeyStore keyStore = KeyStore.getInstance(ANDROID_KEYSTORE);
+ keyStore.load(null);
+
+ // Check if the key exists
+ if (keyStore.containsAlias(ENCRYPTION_KEY_ALIAS)) {
+ // Key exists, retrieve it
+ KeyStore.SecretKeyEntry secretKeyEntry = (KeyStore.SecretKeyEntry) keyStore.getEntry(
+ ENCRYPTION_KEY_ALIAS, null);
+ cachedEncryptionKey = secretKeyEntry.getSecretKey();
+ return cachedEncryptionKey;
+ } else {
+ // Key doesn't exist, create it
+ KeyGenerator keyGenerator = KeyGenerator.getInstance(
+ KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEYSTORE);
+
+ KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec.Builder(
+ ENCRYPTION_KEY_ALIAS,
+ KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
+ .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
+ .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
+ .setKeySize(256)
+ .setUserAuthenticationRequired(true)
+ .build();
+
+ keyGenerator.init(keyGenParameterSpec);
+ cachedEncryptionKey = keyGenerator.generateKey();
+ return cachedEncryptionKey;
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Error getting or creating encryption key", e);
+ throw e;
+ }
+ }
+
+ private Cipher getCipher() throws Exception {
+ return Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/"
+ + KeyProperties.BLOCK_MODE_GCM + "/"
+ + KeyProperties.ENCRYPTION_PADDING_NONE);
+ }
+
+ private String encrypt(String data) throws Exception {
+ SecretKey key = getOrCreateEncryptionKey();
+ Cipher cipher = getCipher();
+ cipher.init(Cipher.ENCRYPT_MODE, key);
+
+ byte[] iv = cipher.getIV();
+ byte[] encryptedBytes = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
+
+ // Store IV in SharedPreferences
+ SharedPreferences prefs = appContext.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE);
+ prefs.edit().putString(CREDENTIALS_KEY + IV_SUFFIX, Base64.encodeToString(iv, Base64.DEFAULT)).apply();
+
+ return Base64.encodeToString(encryptedBytes, Base64.DEFAULT);
+ }
+
+ private String decrypt(String encryptedData) throws Exception {
+ SecretKey key = getOrCreateEncryptionKey();
+ Cipher cipher = getCipher();
+
+ // Get IV from SharedPreferences
+ SharedPreferences prefs = appContext.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE);
+ String ivString = prefs.getString(CREDENTIALS_KEY + IV_SUFFIX, null);
+ if (ivString == null) {
+ throw new Exception("IV not found for decryption");
+ }
+
+ byte[] iv = Base64.decode(ivString, Base64.DEFAULT);
+ byte[] encryptedBytes = Base64.decode(encryptedData, Base64.DEFAULT);
+
+ GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(128, iv);
+ cipher.init(Cipher.DECRYPT_MODE, key, gcmParameterSpec);
+
+ byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
+ return new String(decryptedBytes, StandardCharsets.UTF_8);
+ }
+
+ public List getAllCredentials() throws Exception {
+ SharedPreferences prefs = appContext.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE);
+ String encryptedData = prefs.getString(CREDENTIALS_KEY, null);
+
+ if (encryptedData == null) {
+ return new ArrayList<>();
+ }
+
+ String decryptedData = decrypt(encryptedData);
+ return parseCredentialsFromJson(decryptedData);
+ }
+
+ public void addCredential(Credential credential) throws Exception {
+ List credentials = getAllCredentials();
+ credentials.add(credential);
+
+ String jsonData = credentialsToJson(credentials);
+ String encryptedData = encrypt(jsonData);
+
+ SharedPreferences prefs = appContext.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE);
+ prefs.edit().putString(CREDENTIALS_KEY, encryptedData).apply();
+ }
+
+ public void clearAllCredentials() {
+ SharedPreferences prefs = appContext.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE);
+ prefs.edit()
+ .remove(CREDENTIALS_KEY)
+ .remove(CREDENTIALS_KEY + IV_SUFFIX)
+ .apply();
+
+ try {
+ KeyStore keyStore = KeyStore.getInstance(ANDROID_KEYSTORE);
+ keyStore.load(null);
+ if (keyStore.containsAlias(ENCRYPTION_KEY_ALIAS)) {
+ keyStore.deleteEntry(ENCRYPTION_KEY_ALIAS);
+ }
+ cachedEncryptionKey = null;
+ } catch (Exception e) {
+ Log.e(TAG, "Error clearing encryption key", e);
+ }
+ }
+
+ public void clearCache() {
+ cachedEncryptionKey = null;
+ }
+
+ private List parseCredentialsFromJson(String json) throws JSONException {
+ List credentials = new ArrayList<>();
+ JSONArray jsonArray = new JSONArray(json);
+
+ for (int i = 0; i < jsonArray.length(); i++) {
+ JSONObject jsonObject = jsonArray.getJSONObject(i);
+ String username = jsonObject.getString("username");
+ String password = jsonObject.getString("password");
+ String service = jsonObject.getString("service");
+
+ credentials.add(new Credential(username, password, service));
+ }
+
+ return credentials;
+ }
+
+ private String credentialsToJson(List credentials) throws JSONException {
+ JSONArray jsonArray = new JSONArray();
+
+ for (Credential credential : credentials) {
+ JSONObject jsonObject = new JSONObject();
+ jsonObject.put("username", credential.getUsername());
+ jsonObject.put("password", credential.getPassword());
+ jsonObject.put("service", credential.getService());
+
+ jsonArray.put(jsonObject);
+ }
+
+ return jsonArray.toString();
+ }
+}
\ No newline at end of file
diff --git a/mobile-app/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/mobile-app/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
index 3941bea9b..3c7a95729 100644
--- a/mobile-app/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
+++ b/mobile-app/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -1,5 +1,4 @@
-
\ No newline at end of file
diff --git a/mobile-app/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/mobile-app/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
index 3941bea9b..3c7a95729 100644
--- a/mobile-app/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
+++ b/mobile-app/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -1,5 +1,4 @@
-
\ No newline at end of file
diff --git a/mobile-app/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/mobile-app/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp
deleted file mode 100644
index 7fae0ccbc..000000000
Binary files a/mobile-app/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp and /dev/null differ
diff --git a/mobile-app/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp b/mobile-app/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp
deleted file mode 100644
index ac03dbf69..000000000
Binary files a/mobile-app/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp and /dev/null differ
diff --git a/mobile-app/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/mobile-app/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
deleted file mode 100644
index afa0a4ef4..000000000
Binary files a/mobile-app/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/mobile-app/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/mobile-app/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp
deleted file mode 100644
index 78aaf4541..000000000
Binary files a/mobile-app/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp and /dev/null differ
diff --git a/mobile-app/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp b/mobile-app/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp
deleted file mode 100644
index e1173a94d..000000000
Binary files a/mobile-app/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp and /dev/null differ
diff --git a/mobile-app/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/mobile-app/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
deleted file mode 100644
index c4f6e101e..000000000
Binary files a/mobile-app/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/mobile-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/mobile-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
deleted file mode 100644
index 7a0f085fa..000000000
Binary files a/mobile-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and /dev/null differ
diff --git a/mobile-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp b/mobile-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp
deleted file mode 100644
index ff086fdc3..000000000
Binary files a/mobile-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp and /dev/null differ
diff --git a/mobile-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/mobile-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
deleted file mode 100644
index 6c2d40bf5..000000000
Binary files a/mobile-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/mobile-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/mobile-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
deleted file mode 100644
index 730e3fa55..000000000
Binary files a/mobile-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and /dev/null differ
diff --git a/mobile-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/mobile-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp
deleted file mode 100644
index f7f1d0690..000000000
Binary files a/mobile-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp and /dev/null differ
diff --git a/mobile-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/mobile-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
deleted file mode 100644
index 345261586..000000000
Binary files a/mobile-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and /dev/null differ
diff --git a/mobile-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/mobile-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
deleted file mode 100644
index b11a322ab..000000000
Binary files a/mobile-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and /dev/null differ
diff --git a/mobile-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp b/mobile-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp
deleted file mode 100644
index 49a464ee3..000000000
Binary files a/mobile-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp and /dev/null differ
diff --git a/mobile-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/mobile-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
deleted file mode 100644
index b51fd15c2..000000000
Binary files a/mobile-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and /dev/null differ