Make android app buildable (#771)
14
.vscode/tasks.json
vendored
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
```
|
||||
@@ -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";
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.USE_BIOMETRIC"/>
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<Credential> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
|
||||
List<NativeModule> modules = new ArrayList<>();
|
||||
modules.add(new CredentialManagerModule(reactContext));
|
||||
return modules;
|
||||
}
|
||||
}
|
||||
@@ -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<Credential> 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<Credential> 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<Credential> parseCredentialsFromJson(String json) throws JSONException {
|
||||
List<Credential> 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<Credential> 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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<Credential> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
|
||||
List<NativeModule> modules = new ArrayList<>();
|
||||
modules.add(new CredentialManagerModule(reactContext));
|
||||
return modules;
|
||||
}
|
||||
}
|
||||
@@ -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<Credential> 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<Credential> 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<Credential> parseCredentialsFromJson(String json) throws JSONException {
|
||||
List<Credential> 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<Credential> 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();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/iconBackground"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -1,5 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/iconBackground"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 8.9 KiB |
|
Before Width: | Height: | Size: 9.9 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 12 KiB |