Generate random encryption key and persist in keyvault (#771)

This commit is contained in:
Leendert de Borst
2025-04-10 12:20:37 +02:00
parent 15b44e4575
commit 7e7d86be25

View File

@@ -2,10 +2,16 @@ 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 java.nio.ByteBuffer;
import java.security.KeyStore;
import java.security.SecureRandom;
import androidx.biometric.BiometricPrompt;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.FragmentActivity;
import org.json.JSONArray;
@@ -15,8 +21,12 @@ import org.json.JSONObject;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
@@ -24,169 +34,436 @@ public class SharedCredentialStore {
private static final String TAG = "SharedCredentialStore";
private static final String SHARED_PREFS_NAME = "net.aliasvault.credentials";
private static final String CREDENTIALS_KEY = "stored_credentials";
// Hardcoded encryption key (32 bytes for AES-256)
private static final byte[] ENCRYPTION_KEY = {
0x01, 0x23, 0x45, 0x67, (byte) 0x89, (byte) 0xAB, (byte) 0xCD, (byte) 0xEF,
0x01, 0x23, 0x45, 0x67, (byte) 0x89, (byte) 0xAB, (byte) 0xCD, (byte) 0xEF,
0x01, 0x23, 0x45, 0x67, (byte) 0x89, (byte) 0xAB, (byte) 0xCD, (byte) 0xEF,
0x01, 0x23, 0x45, 0x67, (byte) 0x89, (byte) 0xAB, (byte) 0xCD, (byte) 0xEF
};
private static final String KEYSTORE_ALIAS = "net.aliasvault.encryption_key";
private static final String ENCRYPTED_KEY_PREF = "encrypted_key";
private static SharedCredentialStore instance;
private final Context appContext;
// Cache for encryption key during the lifetime of this instance
private byte[] encryptionKey;
private final Executor executor;
// Interface for operations that need callbacks
public interface CryptoOperationCallback {
void onSuccess(String result);
void onError(Exception e);
}
private SharedCredentialStore(Context context) {
this.appContext = context.getApplicationContext();
this.executor = Executors.newSingleThreadExecutor();
}
public static synchronized SharedCredentialStore getInstance(Context context) {
if (instance == null) {
instance = new SharedCredentialStore(context);
}
return instance;
}
/**
* Get or create encryption key using biometric authentication
*/
public void getEncryptionKey(FragmentActivity activity, final CryptoOperationCallback callback) {
// If key is already in memory, use it
if (encryptionKey != null) {
Log.d(TAG, "Using cached encryption key");
callback.onSuccess("Key available");
return;
}
// Check if we have a stored key
SharedPreferences prefs = appContext.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE);
String encryptedKeyB64 = prefs.getString(ENCRYPTED_KEY_PREF, null);
if (encryptedKeyB64 == null) {
// No key exists, create a new one
createNewEncryptionKey(activity, callback);
} else {
// Key exists, retrieve it with biometric auth
retrieveEncryptionKey(activity, encryptedKeyB64, callback);
}
}
/**
* Create a new random encryption key and protect it with biometrics
*/
private void createNewEncryptionKey(FragmentActivity activity, final CryptoOperationCallback callback) {
try {
// Generate a random 32-byte key for AES-256
SecureRandom secureRandom = new SecureRandom();
byte[] randomKey = new byte[32];
secureRandom.nextBytes(randomKey);
// Cache the key
encryptionKey = randomKey;
// Store the key protected by biometric authentication
storeKeyWithBiometricProtection(activity, randomKey, callback);
} catch (Exception e) {
Log.e(TAG, "Error creating encryption key", e);
callback.onError(e);
}
}
/**
* Store the encryption key protected by biometric authentication
*/
private void storeKeyWithBiometricProtection(FragmentActivity activity, final byte[] keyToStore,
final CryptoOperationCallback callback) {
try {
// Set up KeyStore
KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);
// Create or get biometric key
if (!keyStore.containsAlias(KEYSTORE_ALIAS)) {
KeyGenerator keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
KeyGenParameterSpec keySpec = new KeyGenParameterSpec.Builder(
KEYSTORE_ALIAS,
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setUserAuthenticationRequired(true)
.build();
keyGenerator.init(keySpec);
keyGenerator.generateKey();
}
// Get the created key
final SecretKey secretKey = (SecretKey) keyStore.getKey(KEYSTORE_ALIAS, null);
// Create BiometricPrompt
BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder()
.setTitle("Remember AliasVault password")
.setSubtitle("Protect your AliasVault decryption key with your biometrics.")
.setNegativeButtonText("Cancel")
.build();
BiometricPrompt biometricPrompt = new BiometricPrompt(activity, executor,
new BiometricPrompt.AuthenticationCallback() {
@Override
public void onAuthenticationSucceeded(BiometricPrompt.AuthenticationResult result) {
try {
// Get the cipher from the result
Cipher cipher = result.getCryptoObject().getCipher();
// Encrypt the key
byte[] encryptedKey = cipher.doFinal(keyToStore);
byte[] iv = cipher.getIV();
// Combine IV and encrypted key
ByteBuffer byteBuffer = ByteBuffer.allocate(iv.length + encryptedKey.length);
byteBuffer.put(iv);
byteBuffer.put(encryptedKey);
byte[] combined = byteBuffer.array();
// Store encrypted key in SharedPreferences
String encryptedKeyB64 = Base64.encodeToString(combined, Base64.DEFAULT);
SharedPreferences prefs = appContext.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE);
prefs.edit().putString(ENCRYPTED_KEY_PREF, encryptedKeyB64).apply();
Log.d(TAG, "Encryption key stored successfully");
callback.onSuccess("Key stored successfully");
} catch (Exception e) {
Log.e(TAG, "Error storing encryption key", e);
callback.onError(e);
}
}
@Override
public void onAuthenticationError(int errorCode, CharSequence errString) {
Log.e(TAG, "Authentication error: " + errString);
callback.onError(new Exception("Authentication error: " + errString));
}
@Override
public void onAuthenticationFailed() {
Log.e(TAG, "Authentication failed");
}
});
// Initialize cipher for encryption
Cipher cipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/"
+ KeyProperties.BLOCK_MODE_GCM + "/"
+ KeyProperties.ENCRYPTION_PADDING_NONE);
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
// Show biometric prompt
biometricPrompt.authenticate(promptInfo, new BiometricPrompt.CryptoObject(cipher));
} catch (Exception e) {
Log.e(TAG, "Error in biometric key storage", e);
callback.onError(e);
}
}
/**
* Retrieve the encryption key using biometric authentication
*/
private void retrieveEncryptionKey(FragmentActivity activity, final String encryptedKeyB64,
final CryptoOperationCallback callback) {
try {
// Set up KeyStore
KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);
// Check if key exists
if (!keyStore.containsAlias(KEYSTORE_ALIAS)) {
Log.e(TAG, "Keystore key not found");
createNewEncryptionKey(activity, callback);
return;
}
// Get the key
final SecretKey secretKey = (SecretKey) keyStore.getKey(KEYSTORE_ALIAS, null);
// Create BiometricPrompt
BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder()
.setTitle("Unlock Vault")
.setSubtitle("Unlock your protected AliasVault contents")
.setNegativeButtonText("Cancel")
.build();
BiometricPrompt biometricPrompt = new BiometricPrompt(activity, executor,
new BiometricPrompt.AuthenticationCallback() {
@Override
public void onAuthenticationSucceeded(BiometricPrompt.AuthenticationResult result) {
try {
// Get the cipher from the result
Cipher cipher = result.getCryptoObject().getCipher();
// Decode combined data
byte[] combined = Base64.decode(encryptedKeyB64, Base64.DEFAULT);
// Extract IV and encrypted data
ByteBuffer byteBuffer = ByteBuffer.wrap(combined);
// GCM typically uses 12 bytes for IV
byte[] iv = new byte[12];
byteBuffer.get(iv);
// Get remaining bytes as ciphertext
byte[] encryptedBytes = new byte[byteBuffer.remaining()];
byteBuffer.get(encryptedBytes);
// Decrypt the key
byte[] decryptedKey = cipher.doFinal(encryptedBytes);
// Cache the key
encryptionKey = decryptedKey;
Log.d(TAG, "Encryption key retrieved successfully");
callback.onSuccess("Key retrieved successfully");
} catch (Exception e) {
Log.e(TAG, "Error retrieving encryption key", e);
callback.onError(e);
}
}
@Override
public void onAuthenticationError(int errorCode, CharSequence errString) {
Log.e(TAG, "Authentication error: " + errString);
callback.onError(new Exception("Authentication error: " + errString));
}
@Override
public void onAuthenticationFailed() {
Log.e(TAG, "Authentication failed");
}
});
// Initialize cipher for decryption with IV from stored encrypted key
byte[] combined = Base64.decode(encryptedKeyB64, Base64.DEFAULT);
ByteBuffer byteBuffer = ByteBuffer.wrap(combined);
byte[] iv = new byte[12];
byteBuffer.get(iv);
Cipher cipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/"
+ KeyProperties.BLOCK_MODE_GCM + "/"
+ KeyProperties.ENCRYPTION_PADDING_NONE);
GCMParameterSpec spec = new GCMParameterSpec(128, iv);
cipher.init(Cipher.DECRYPT_MODE, secretKey, spec);
// Show biometric prompt
biometricPrompt.authenticate(promptInfo, new BiometricPrompt.CryptoObject(cipher));
} catch (Exception e) {
Log.e(TAG, "Error in biometric key retrieval", e);
callback.onError(e);
}
}
/**
* Encrypts data using AES/GCM/NoPadding
*/
private String encryptData(String plaintext) throws Exception {
if (encryptionKey == null) {
throw new Exception("Encryption key not available");
}
// Create cipher
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
// Create secret key from hardcoded bytes
SecretKeySpec secretKeySpec = new SecretKeySpec(ENCRYPTION_KEY, "AES");
// Create secret key from retrieved bytes
SecretKeySpec secretKeySpec = new SecretKeySpec(encryptionKey, "AES");
// Initialize cipher for encryption
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);
// Get IV
byte[] iv = cipher.getIV();
// Encrypt data
byte[] encryptedBytes = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
// Combine IV and encrypted data
ByteBuffer byteBuffer = ByteBuffer.allocate(iv.length + encryptedBytes.length);
byteBuffer.put(iv);
byteBuffer.put(encryptedBytes);
byte[] combined = byteBuffer.array();
// Return Base64 encoded combined data
return Base64.encodeToString(combined, Base64.DEFAULT);
}
/**
* Decrypts data using AES/GCM/NoPadding
*/
private String decryptData(String encryptedData) throws Exception {
if (encryptionKey == null) {
throw new Exception("Encryption key not available");
}
// Decode combined data
byte[] combined = Base64.decode(encryptedData, Base64.DEFAULT);
// Extract IV and encrypted data
ByteBuffer byteBuffer = ByteBuffer.wrap(combined);
// GCM typically uses 12 bytes for IV
byte[] iv = new byte[12];
byteBuffer.get(iv);
// Get remaining bytes as ciphertext
byte[] encryptedBytes = new byte[byteBuffer.remaining()];
byteBuffer.get(encryptedBytes);
// Create cipher
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
// Create secret key from hardcoded bytes
SecretKeySpec secretKeySpec = new SecretKeySpec(ENCRYPTION_KEY, "AES");
// Create secret key from retrieved bytes
SecretKeySpec secretKeySpec = new SecretKeySpec(encryptionKey, "AES");
// Create GCM parameter spec with IV
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(128, iv);
// Initialize cipher for decryption
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, gcmParameterSpec);
// Decrypt data
byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
// Return decrypted string
return new String(decryptedBytes, StandardCharsets.UTF_8);
}
/**
* Save a credential to SharedPreferences with encryption
*/
public void saveCredential(FragmentActivity activity, final Credential credential,
final CryptoOperationCallback callback) {
try {
Log.d(TAG, "Saving credential for: " + credential.getService());
// Get current credentials from SharedPreferences
SharedPreferences prefs = appContext.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE);
String encryptedCredentialsJson = prefs.getString(CREDENTIALS_KEY, null);
List<Credential> credentials;
if (encryptedCredentialsJson != null) {
// Decrypt and parse existing credentials
String decryptedJson = decryptData(encryptedCredentialsJson);
credentials = parseCredentialsFromJson(decryptedJson);
} else {
// No existing credentials
credentials = new ArrayList<>();
// First ensure we have the encryption key
getEncryptionKey(activity, new CryptoOperationCallback() {
@Override
public void onSuccess(String result) {
try {
Log.d(TAG, "Saving credential for: " + credential.getService());
// Get current credentials from SharedPreferences
SharedPreferences prefs = appContext.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE);
String encryptedCredentialsJson = prefs.getString(CREDENTIALS_KEY, null);
List<Credential> credentials;
if (encryptedCredentialsJson != null) {
// Decrypt and parse existing credentials
String decryptedJson = decryptData(encryptedCredentialsJson);
credentials = parseCredentialsFromJson(decryptedJson);
} else {
// No existing credentials
credentials = new ArrayList<>();
}
// Add new credential
credentials.add(credential);
// Convert to JSON
String jsonData = credentialsToJson(credentials);
// Encrypt
String encryptedJson = encryptData(jsonData);
// Save encrypted data
prefs.edit().putString(CREDENTIALS_KEY, encryptedJson).apply();
callback.onSuccess("Credential saved successfully");
} catch (Exception e) {
Log.e(TAG, "Error saving credential", e);
callback.onError(e);
}
}
// Add new credential
credentials.add(credential);
// Convert to JSON
String jsonData = credentialsToJson(credentials);
// Encrypt
String encryptedJson = encryptData(jsonData);
// Save encrypted data
prefs.edit().putString(CREDENTIALS_KEY, encryptedJson).apply();
callback.onSuccess("Credential saved successfully");
} catch (Exception e) {
Log.e(TAG, "Error saving credential", e);
callback.onError(e);
}
@Override
public void onError(Exception e) {
Log.e(TAG, "Failed to get encryption key", e);
callback.onError(e);
}
});
}
/**
* Get all credentials from SharedPreferences with decryption
*/
public void getAllCredentials(FragmentActivity activity, final CryptoOperationCallback callback) {
try {
Log.d(TAG, "Retrieving credentials from SharedPreferences");
// Get encrypted credentials from SharedPreferences
SharedPreferences prefs = appContext.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE);
String encryptedCredentialsJson = prefs.getString(CREDENTIALS_KEY, null);
if (encryptedCredentialsJson == null) {
// No credentials found
callback.onSuccess(new JSONArray().toString());
return;
// First ensure we have the encryption key
getEncryptionKey(activity, new CryptoOperationCallback() {
@Override
public void onSuccess(String result) {
try {
Log.d(TAG, "Retrieving credentials from SharedPreferences");
// Get encrypted credentials from SharedPreferences
SharedPreferences prefs = appContext.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE);
String encryptedCredentialsJson = prefs.getString(CREDENTIALS_KEY, null);
if (encryptedCredentialsJson == null) {
// No credentials found
callback.onSuccess(new JSONArray().toString());
return;
}
// Decrypt credentials
String decryptedJson = decryptData(encryptedCredentialsJson);
callback.onSuccess(decryptedJson);
} catch (Exception e) {
Log.e(TAG, "Error retrieving credentials", e);
callback.onError(e);
}
}
// Decrypt credentials
String decryptedJson = decryptData(encryptedCredentialsJson);
callback.onSuccess(decryptedJson);
} catch (Exception e) {
Log.e(TAG, "Error retrieving credentials", e);
callback.onError(e);
}
@Override
public void onError(Exception e) {
Log.e(TAG, "Failed to get encryption key", e);
callback.onError(e);
}
});
}
/**
* Clear all credentials from SharedPreferences
*/
@@ -195,42 +472,59 @@ public class SharedCredentialStore {
SharedPreferences prefs = appContext.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE);
prefs.edit()
.remove(CREDENTIALS_KEY)
.remove(ENCRYPTED_KEY_PREF)
.apply();
// Clear the cached encryption key
encryptionKey = null;
// Remove the key from Android Keystore if it exists
try {
KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);
if (keyStore.containsAlias(KEYSTORE_ALIAS)) {
keyStore.deleteEntry(KEYSTORE_ALIAS);
Log.d(TAG, "Removed encryption key from Android Keystore");
}
} catch (Exception e) {
Log.e(TAG, "Error removing encryption key from Keystore", e);
}
}
private List<Credential> parseCredentialsFromJson(String json) throws JSONException {
List<Credential> credentials = new ArrayList<>();
if (json == null || json.isEmpty()) {
return credentials;
}
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();
}
}