diff --git a/samples/CameraAccessAndroid/app/build.gradle.kts b/samples/CameraAccessAndroid/app/build.gradle.kts index c20937c8..a0810fda 100644 --- a/samples/CameraAccessAndroid/app/build.gradle.kts +++ b/samples/CameraAccessAndroid/app/build.gradle.kts @@ -76,6 +76,7 @@ dependencies { implementation(libs.datastore.preferences) implementation(libs.gson) implementation(libs.lifecycle.process) + implementation(libs.security.crypto) androidTestImplementation(libs.androidx.ui.test.junit4) androidTestImplementation(libs.androidx.test.uiautomator) androidTestImplementation(libs.androidx.test.rules) diff --git a/samples/CameraAccessAndroid/app/src/main/java/com/meta/wearable/dat/externalsampleapps/cameraaccess/settings/SettingsManager.kt b/samples/CameraAccessAndroid/app/src/main/java/com/meta/wearable/dat/externalsampleapps/cameraaccess/settings/SettingsManager.kt index dd8d2d26..a0895c37 100644 --- a/samples/CameraAccessAndroid/app/src/main/java/com/meta/wearable/dat/externalsampleapps/cameraaccess/settings/SettingsManager.kt +++ b/samples/CameraAccessAndroid/app/src/main/java/com/meta/wearable/dat/externalsampleapps/cameraaccess/settings/SettingsManager.kt @@ -2,21 +2,69 @@ package com.meta.wearable.dat.externalsampleapps.cameraaccess.settings import android.content.Context import android.content.SharedPreferences +import android.util.Log +import androidx.security.crypto.EncryptedSharedPreferences +import androidx.security.crypto.MasterKey import com.meta.wearable.dat.externalsampleapps.cameraaccess.Secrets object SettingsManager { + private const val TAG = "SettingsManager" private const val PREFS_NAME = "visionclaw_settings" + private const val SECURE_PREFS_NAME = "visionclaw_secure" + // Non-sensitive settings (host, port, toggles) private lateinit var prefs: SharedPreferences + // Secrets (API keys, tokens) — encrypted at rest + private lateinit var securePrefs: SharedPreferences fun init(context: Context) { prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + + val masterKey = MasterKey.Builder(context) + .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) + .build() + securePrefs = EncryptedSharedPreferences.create( + context, + SECURE_PREFS_NAME, + masterKey, + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM + ) + + migrateSecretsFromPlainPrefs() } + /** One-time migration: move secrets from plain SharedPreferences to EncryptedSharedPreferences. */ + private fun migrateSecretsFromPlainPrefs() { + if (prefs.getBoolean("secrets_migrated", false)) return + + val secretKeys = listOf("geminiAPIKey", "openClawHookToken", "openClawGatewayToken") + for (key in secretKeys) { + val value = prefs.getString(key, null) + if (value != null) { + securePrefs.edit().putString(key, value).apply() + prefs.edit().remove(key).apply() + Log.d(TAG, "Migrated $key to encrypted storage") + } + } + + prefs.edit().putBoolean("secrets_migrated", true).apply() + } + + // Secrets — stored in EncryptedSharedPreferences var geminiAPIKey: String - get() = prefs.getString("geminiAPIKey", null) ?: Secrets.geminiAPIKey - set(value) = prefs.edit().putString("geminiAPIKey", value).apply() + get() = securePrefs.getString("geminiAPIKey", null) ?: Secrets.geminiAPIKey + set(value) = securePrefs.edit().putString("geminiAPIKey", value).apply() + + var openClawHookToken: String + get() = securePrefs.getString("openClawHookToken", null) ?: Secrets.openClawHookToken + set(value) = securePrefs.edit().putString("openClawHookToken", value).apply() + var openClawGatewayToken: String + get() = securePrefs.getString("openClawGatewayToken", null) ?: Secrets.openClawGatewayToken + set(value) = securePrefs.edit().putString("openClawGatewayToken", value).apply() + + // Non-sensitive settings — plain SharedPreferences var geminiSystemPrompt: String get() = prefs.getString("geminiSystemPrompt", null) ?: DEFAULT_SYSTEM_PROMPT set(value) = prefs.edit().putString("geminiSystemPrompt", value).apply() @@ -32,14 +80,6 @@ object SettingsManager { } set(value) = prefs.edit().putInt("openClawPort", value).apply() - var openClawHookToken: String - get() = prefs.getString("openClawHookToken", null) ?: Secrets.openClawHookToken - set(value) = prefs.edit().putString("openClawHookToken", value).apply() - - var openClawGatewayToken: String - get() = prefs.getString("openClawGatewayToken", null) ?: Secrets.openClawGatewayToken - set(value) = prefs.edit().putString("openClawGatewayToken", value).apply() - var webrtcSignalingURL: String get() = prefs.getString("webrtcSignalingURL", null) ?: Secrets.webrtcSignalingURL set(value) = prefs.edit().putString("webrtcSignalingURL", value).apply() @@ -53,6 +93,7 @@ object SettingsManager { set(value) = prefs.edit().putBoolean("proactiveNotificationsEnabled", value).apply() fun resetAll() { + securePrefs.edit().clear().apply() prefs.edit().clear().apply() } diff --git a/samples/CameraAccessAndroid/gradle/libs.versions.toml b/samples/CameraAccessAndroid/gradle/libs.versions.toml index 3f496017..ca65674a 100644 --- a/samples/CameraAccessAndroid/gradle/libs.versions.toml +++ b/samples/CameraAccessAndroid/gradle/libs.versions.toml @@ -16,6 +16,7 @@ camerax = "1.4.1" datastore = "1.1.1" gson = "2.11.0" lifecycleProcess = "2.8.7" +securityCrypto = "1.1.0-alpha06" [libraries] androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } @@ -41,6 +42,7 @@ camerax-view = { group = "androidx.camera", name = "camera-view", version.ref = datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" } gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" } lifecycle-process = { group = "androidx.lifecycle", name = "lifecycle-process", version.ref = "lifecycleProcess" } +security-crypto = { group = "androidx.security", name = "security-crypto", version.ref = "securityCrypto" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" }