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