From bfdd9c6bb49a0cd9c17c7e125a1992cb7eed5b0f Mon Sep 17 00:00:00 2001 From: 2witstudios <2witstudios@gmail.com> Date: Mon, 2 Feb 2026 12:03:39 -0600 Subject: [PATCH 1/2] feat(android): add Firebase and zero-trust secure storage for production - Add Firebase SDK (BoM 34.8.0, firebase-messaging) for push notifications - Create PageSpaceSecureStoragePlugin with AES-256-GCM encrypted storage - Register secure storage plugin in MainActivity - Update Gradle plugin to 8.9.1 and compileSdk/targetSdk to 36 - Update google-services plugin to 4.4.4 - Add google-services.json for Firebase project - Configure Google OAuth client ID in capacitor.config.ts Security: Uses Android Keystore-backed master key with hardware security module when available. Same JS interface as iOS PageSpaceKeychainPlugin. Co-Authored-By: Claude Opus 4.5 --- apps/android/android/app/build.gradle | 9 ++ apps/android/android/app/google-services.json | 29 ++++++ .../ai/pagespace/android/MainActivity.java | 9 +- .../android/PageSpaceSecureStoragePlugin.java | 91 +++++++++++++++++++ apps/android/android/build.gradle | 4 +- apps/android/android/variables.gradle | 4 +- apps/android/capacitor.config.ts | 2 +- 7 files changed, 142 insertions(+), 6 deletions(-) create mode 100644 apps/android/android/app/google-services.json create mode 100644 apps/android/android/app/src/main/java/ai/pagespace/android/PageSpaceSecureStoragePlugin.java diff --git a/apps/android/android/app/build.gradle b/apps/android/android/app/build.gradle index faba51dbe..bc8b89b61 100644 --- a/apps/android/android/app/build.gradle +++ b/apps/android/android/app/build.gradle @@ -40,6 +40,15 @@ dependencies { androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion" androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion" implementation project(':capacitor-cordova-android-plugins') + + // Firebase BoM - manages all Firebase versions + implementation platform('com.google.firebase:firebase-bom:34.8.0') + + // Firebase Cloud Messaging for push notifications + implementation 'com.google.firebase:firebase-messaging' + + // Security library for encrypted storage (zero-trust secure storage) + implementation 'androidx.security:security-crypto:1.1.0-alpha06' } apply from: 'capacitor.build.gradle' diff --git a/apps/android/android/app/google-services.json b/apps/android/android/app/google-services.json new file mode 100644 index 000000000..3f9cee445 --- /dev/null +++ b/apps/android/android/app/google-services.json @@ -0,0 +1,29 @@ +{ + "project_info": { + "project_number": "107236466397", + "project_id": "pagespace-f328e", + "storage_bucket": "pagespace-f328e.firebasestorage.app" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:107236466397:android:931c03aeb94c42dd904534", + "android_client_info": { + "package_name": "ai.pagespace.android" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "AIzaSyDsbaTral3mH28xMxesfO5FMIz3-ORzcLg" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/apps/android/android/app/src/main/java/ai/pagespace/android/MainActivity.java b/apps/android/android/app/src/main/java/ai/pagespace/android/MainActivity.java index c81327d38..29044b541 100644 --- a/apps/android/android/app/src/main/java/ai/pagespace/android/MainActivity.java +++ b/apps/android/android/app/src/main/java/ai/pagespace/android/MainActivity.java @@ -1,5 +1,12 @@ package ai.pagespace.android; +import android.os.Bundle; import com.getcapacitor.BridgeActivity; -public class MainActivity extends BridgeActivity {} +public class MainActivity extends BridgeActivity { + @Override + public void onCreate(Bundle savedInstanceState) { + registerPlugin(PageSpaceSecureStoragePlugin.class); + super.onCreate(savedInstanceState); + } +} diff --git a/apps/android/android/app/src/main/java/ai/pagespace/android/PageSpaceSecureStoragePlugin.java b/apps/android/android/app/src/main/java/ai/pagespace/android/PageSpaceSecureStoragePlugin.java new file mode 100644 index 000000000..281cc1143 --- /dev/null +++ b/apps/android/android/app/src/main/java/ai/pagespace/android/PageSpaceSecureStoragePlugin.java @@ -0,0 +1,91 @@ +package ai.pagespace.android; + +import android.content.Context; +import android.content.SharedPreferences; +import androidx.security.crypto.EncryptedSharedPreferences; +import androidx.security.crypto.MasterKey; +import com.getcapacitor.JSObject; +import com.getcapacitor.Plugin; +import com.getcapacitor.PluginCall; +import com.getcapacitor.PluginMethod; +import com.getcapacitor.annotation.CapacitorPlugin; + +/** + * Zero-trust secure storage plugin using Android EncryptedSharedPreferences. + * Provides the same JS interface as the iOS PageSpaceKeychainPlugin. + * + * Security properties: + * - AES-256-GCM encryption for values + * - AES-256-SIV encryption for keys + * - Android Keystore-backed master key + * - Hardware security module used when available + * - No cloud sync - data stays on device + */ +@CapacitorPlugin(name = "PageSpaceKeychain") +public class PageSpaceSecureStoragePlugin extends Plugin { + private SharedPreferences sharedPreferences; + private static final String PREFS_NAME = "ai.pagespace.secure"; + + @Override + public void load() { + try { + Context context = getContext(); + MasterKey masterKey = new MasterKey.Builder(context) + .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) + .build(); + + sharedPreferences = EncryptedSharedPreferences.create( + context, + PREFS_NAME, + masterKey, + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM + ); + } catch (Exception e) { + // Fallback to regular SharedPreferences if encryption fails + // This should rarely happen but prevents app crashes + sharedPreferences = getContext() + .getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + } + } + + @PluginMethod + public void get(PluginCall call) { + String key = call.getString("key"); + if (key == null) { + call.reject("Missing key"); + return; + } + String value = sharedPreferences.getString(key, null); + JSObject result = new JSObject(); + result.put("value", value); + call.resolve(result); + } + + @PluginMethod + public void set(PluginCall call) { + String key = call.getString("key"); + String value = call.getString("value"); + if (key == null || value == null) { + call.reject("Missing key or value"); + return; + } + sharedPreferences.edit().putString(key, value).apply(); + JSObject result = new JSObject(); + result.put("success", true); + call.resolve(result); + } + + @PluginMethod + public void remove(PluginCall call) { + String key = call.getString("key"); + if (key == null) { + call.reject("Missing key"); + return; + } + sharedPreferences.edit().remove(key).apply(); + JSObject result = new JSObject(); + result.put("success", true); + call.resolve(result); + } +} diff --git a/apps/android/android/build.gradle b/apps/android/android/build.gradle index f1b3b0e51..3bcc21fb1 100644 --- a/apps/android/android/build.gradle +++ b/apps/android/android/build.gradle @@ -7,8 +7,8 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:8.7.2' - classpath 'com.google.gms:google-services:4.4.2' + classpath 'com.android.tools.build:gradle:8.9.1' + classpath 'com.google.gms:google-services:4.4.4' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/apps/android/android/variables.gradle b/apps/android/android/variables.gradle index 2c8e4083f..821864dba 100644 --- a/apps/android/android/variables.gradle +++ b/apps/android/android/variables.gradle @@ -1,7 +1,7 @@ ext { minSdkVersion = 23 - compileSdkVersion = 35 - targetSdkVersion = 35 + compileSdkVersion = 36 + targetSdkVersion = 36 androidxActivityVersion = '1.9.2' androidxAppCompatVersion = '1.7.0' androidxCoordinatorLayoutVersion = '1.2.0' diff --git a/apps/android/capacitor.config.ts b/apps/android/capacitor.config.ts index b3d221571..1699d50af 100644 --- a/apps/android/capacitor.config.ts +++ b/apps/android/capacitor.config.ts @@ -33,7 +33,7 @@ const config: CapacitorConfig = { SocialLogin: { google: { // You'll need to add your Android client ID from Google Cloud Console - androidClientId: 'YOUR_ANDROID_CLIENT_ID.apps.googleusercontent.com', + androidClientId: '636969838408-s5s3ts6nubc6c29ur81o2ipf6tmu9gqq.apps.googleusercontent.com', }, }, PushNotifications: { From 671c117e47bc117fe6c0e745f90b620add7cb9bd Mon Sep 17 00:00:00 2001 From: 2witstudios <2witstudios@gmail.com> Date: Mon, 2 Feb 2026 12:31:02 -0600 Subject: [PATCH 2/2] fix(android): reject secure storage calls when encryption unavailable Instead of silently falling back to unencrypted SharedPreferences when EncryptedSharedPreferences fails to initialize (keystore corruption, device policy, missing secure hardware), now explicitly reject all plugin calls with an error message. This prevents silent plaintext credential storage that would defeat the zero-trust security contract. Co-Authored-By: Claude Opus 4.5 --- .../android/PageSpaceSecureStoragePlugin.java | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/apps/android/android/app/src/main/java/ai/pagespace/android/PageSpaceSecureStoragePlugin.java b/apps/android/android/app/src/main/java/ai/pagespace/android/PageSpaceSecureStoragePlugin.java index 281cc1143..9b7a863e3 100644 --- a/apps/android/android/app/src/main/java/ai/pagespace/android/PageSpaceSecureStoragePlugin.java +++ b/apps/android/android/app/src/main/java/ai/pagespace/android/PageSpaceSecureStoragePlugin.java @@ -24,6 +24,7 @@ @CapacitorPlugin(name = "PageSpaceKeychain") public class PageSpaceSecureStoragePlugin extends Plugin { private SharedPreferences sharedPreferences; + private String initializationError; private static final String PREFS_NAME = "ai.pagespace.secure"; @Override @@ -41,16 +42,24 @@ public void load() { EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM ); + initializationError = null; } catch (Exception e) { - // Fallback to regular SharedPreferences if encryption fails - // This should rarely happen but prevents app crashes - sharedPreferences = getContext() - .getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + sharedPreferences = null; + initializationError = "Secure storage unavailable: " + e.getMessage(); } } + private boolean rejectIfNotInitialized(PluginCall call) { + if (sharedPreferences == null) { + call.reject(initializationError != null ? initializationError : "Secure storage not initialized"); + return true; + } + return false; + } + @PluginMethod public void get(PluginCall call) { + if (rejectIfNotInitialized(call)) return; String key = call.getString("key"); if (key == null) { call.reject("Missing key"); @@ -64,6 +73,7 @@ public void get(PluginCall call) { @PluginMethod public void set(PluginCall call) { + if (rejectIfNotInitialized(call)) return; String key = call.getString("key"); String value = call.getString("value"); if (key == null || value == null) { @@ -78,6 +88,7 @@ public void set(PluginCall call) { @PluginMethod public void remove(PluginCall call) { + if (rejectIfNotInitialized(call)) return; String key = call.getString("key"); if (key == null) { call.reject("Missing key");