From 067298fa10f79e86f32fb4713086e7f878a91c4c Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 16 May 2026 15:41:06 +0100 Subject: [PATCH] fix: preserve android legacy auth fallback --- .../openclaw/app/gateway/DeviceAuthStore.kt | 97 ++++++++++++++++--- .../app/gateway/DeviceAuthStoreTest.kt | 42 ++++++++ 2 files changed, 126 insertions(+), 13 deletions(-) diff --git a/apps/android/app/src/main/java/ai/openclaw/app/gateway/DeviceAuthStore.kt b/apps/android/app/src/main/java/ai/openclaw/app/gateway/DeviceAuthStore.kt index e473dcf2780..be2b8a3d074 100644 --- a/apps/android/app/src/main/java/ai/openclaw/app/gateway/DeviceAuthStore.kt +++ b/apps/android/app/src/main/java/ai/openclaw/app/gateway/DeviceAuthStore.kt @@ -48,13 +48,79 @@ interface DeviceAuthTokenStore { ) } -class DeviceAuthStore( +internal interface DeviceAuthStateStore { + fun readDeviceAuthToken( + deviceId: String, + role: String, + ): OpenClawSQLiteDeviceAuthTokenRow? + + fun readLatestDeviceAuthDeviceId(): String? + + fun upsertDeviceAuthToken(row: OpenClawSQLiteDeviceAuthTokenRow) + + fun deleteDeviceAuthToken( + deviceId: String, + role: String, + ) + + fun deleteAllDeviceAuthTokens() +} + +private class OpenClawSQLiteDeviceAuthStateStore( + private val store: OpenClawSQLiteStateStore, +) : DeviceAuthStateStore { + override fun readDeviceAuthToken( + deviceId: String, + role: String, + ): OpenClawSQLiteDeviceAuthTokenRow? = store.readDeviceAuthToken(deviceId, role) + + override fun readLatestDeviceAuthDeviceId(): String? = store.readLatestDeviceAuthDeviceId() + + override fun upsertDeviceAuthToken(row: OpenClawSQLiteDeviceAuthTokenRow) { + store.upsertDeviceAuthToken(row) + } + + override fun deleteDeviceAuthToken( + deviceId: String, + role: String, + ) { + store.deleteDeviceAuthToken(deviceId, role) + } + + override fun deleteAllDeviceAuthTokens() { + store.deleteAllDeviceAuthTokens() + } +} + +class DeviceAuthStore private constructor( private val context: Context, private val legacyPrefsOverride: SecurePrefs? = null, + private val stateStore: DeviceAuthStateStore, ) : DeviceAuthTokenStore { + constructor( + context: Context, + legacyPrefsOverride: SecurePrefs? = null, + ) : this( + context = context, + legacyPrefsOverride = legacyPrefsOverride, + stateStore = OpenClawSQLiteDeviceAuthStateStore(OpenClawSQLiteStateStore(context)), + ) + + internal companion object { + fun createForTesting( + context: Context, + legacyPrefsOverride: SecurePrefs? = null, + stateStoreOverride: DeviceAuthStateStore, + ): DeviceAuthStore = + DeviceAuthStore( + context = context, + legacyPrefsOverride = legacyPrefsOverride, + stateStore = stateStoreOverride, + ) + } + private val json = Json { ignoreUnknownKeys = true } private val legacyPrefs by lazy { legacyPrefsOverride ?: SecurePrefs(context) } - private val stateStore = OpenClawSQLiteStateStore(context) override fun loadEntry( deviceId: String, @@ -150,17 +216,22 @@ class DeviceAuthStore( scopes = normalizeScopes(metadata?.scopes ?: emptyList()), updatedAtMs = metadata?.updatedAtMs?.takeIf { it > 0L } ?: System.currentTimeMillis(), ) - stateStore.upsertDeviceAuthToken( - OpenClawSQLiteDeviceAuthTokenRow( - deviceId = normalizedDevice, - role = normalizedRole, - token = sqliteSecurePrefsTokenMarker, - scopesJson = json.encodeToString(entry.scopes), - updatedAtMs = entry.updatedAtMs, - ), - ) - legacyPrefs.putString(tokenKey(normalizedDevice, normalizedRole), entry.token) - removeLegacyMetadata(normalizedDevice, normalizedRole) + val migrated = + runCatching { + stateStore.upsertDeviceAuthToken( + OpenClawSQLiteDeviceAuthTokenRow( + deviceId = normalizedDevice, + role = normalizedRole, + token = sqliteSecurePrefsTokenMarker, + scopesJson = json.encodeToString(entry.scopes), + updatedAtMs = entry.updatedAtMs, + ), + ) + }.isSuccess + if (migrated) { + legacyPrefs.putString(tokenKey(normalizedDevice, normalizedRole), entry.token) + removeLegacyMetadata(normalizedDevice, normalizedRole) + } return entry } diff --git a/apps/android/app/src/test/java/ai/openclaw/app/gateway/DeviceAuthStoreTest.kt b/apps/android/app/src/test/java/ai/openclaw/app/gateway/DeviceAuthStoreTest.kt index 86795c35b68..75669cc9954 100644 --- a/apps/android/app/src/test/java/ai/openclaw/app/gateway/DeviceAuthStoreTest.kt +++ b/apps/android/app/src/test/java/ai/openclaw/app/gateway/DeviceAuthStoreTest.kt @@ -83,6 +83,28 @@ class DeviceAuthStoreTest { ) } + @Test + fun loadEntryReturnsLegacySecurePrefsTokenWhenSQLiteMigrationFails() { + val app = RuntimeEnvironment.getApplication() + val prefs = legacyPrefs(app) + val metadata = """{"scopes":["operator.read"],"updatedAtMs":1700000000000}""" + prefs.putString("gateway.deviceToken.device-1.operator", " operator-token ") + prefs.putString("gateway.deviceTokenMeta.device-1.operator", metadata) + + val entry = + DeviceAuthStore.createForTesting( + context = app, + legacyPrefsOverride = prefs, + stateStoreOverride = ThrowingDeviceAuthStateStore(), + ).loadEntry("device-1", "operator") + + assertEquals("operator-token", entry?.token) + assertEquals(listOf("operator.read"), entry?.scopes) + assertEquals(1700000000000L, entry?.updatedAtMs) + assertEquals(" operator-token ", prefs.getString("gateway.deviceToken.device-1.operator")) + assertEquals(metadata, prefs.getString("gateway.deviceTokenMeta.device-1.operator")) + } + @Test fun loadEntryMovesPlaintextSqliteTokenBackToSecurePrefs() { val app = RuntimeEnvironment.getApplication() @@ -132,4 +154,24 @@ class DeviceAuthStoreTest { prefs.edit().clear().commit() return SecurePrefs(context, securePrefsOverride = prefs) } + + private class ThrowingDeviceAuthStateStore : DeviceAuthStateStore { + override fun readDeviceAuthToken( + deviceId: String, + role: String, + ): OpenClawSQLiteDeviceAuthTokenRow? = null + + override fun readLatestDeviceAuthDeviceId(): String? = null + + override fun upsertDeviceAuthToken(row: OpenClawSQLiteDeviceAuthTokenRow) { + error("sqlite unavailable") + } + + override fun deleteDeviceAuthToken( + deviceId: String, + role: String, + ) = Unit + + override fun deleteAllDeviceAuthTokens() = Unit + } }