Commit a05576c8 authored by Lisa (AI Assistant)'s avatar Lisa (AI Assistant)

Fix: Persist device identity as cryptographic key pair

- NodeClient now persists RSA key pair to SharedPreferences
- Device ID derived from SHA-256 hash of public key
- Stable identity across app restarts
- SettingsRepository nodeId is now just display name
parent a8199e8b
...@@ -48,6 +48,9 @@ android { ...@@ -48,6 +48,9 @@ android {
composeOptions { composeOptions {
kotlinCompilerExtensionVersion = "1.5.5" kotlinCompilerExtensionVersion = "1.5.5"
kotlinCompilerOptions {
freeCompilerArgs.addAll("-P", "plugin:androidx.compose.compiler.plugins.kotlin:suppressKotlinVersionCompatibilityCheck=1.9.20")
}
} }
packaging { packaging {
......
package com.nexlab.openclaw.node.data.remote package com.nexlab.openclaw.node.data.remote
import android.content.Context
import android.content.SharedPreferences
import android.util.Log import android.util.Log
import com.nexlab.openclaw.node.domain.model.NodeCapabilities import com.nexlab.openclaw.node.domain.model.NodeCapabilities
import com.nexlab.openclaw.node.domain.model.NodeCommand import com.nexlab.openclaw.node.domain.model.NodeCommand
...@@ -11,10 +13,13 @@ import kotlinx.coroutines.flow.receiveAsFlow ...@@ -11,10 +13,13 @@ import kotlinx.coroutines.flow.receiveAsFlow
import okhttp3.* import okhttp3.*
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONObject import org.json.JSONObject
import java.security.KeyFactory
import java.security.KeyPairGenerator import java.security.KeyPairGenerator
import java.security.MessageDigest import java.security.MessageDigest
import java.security.PrivateKey import java.security.PrivateKey
import java.security.Signature import java.security.Signature
import java.security.spec.PKCS8EncodedKeySpec
import java.security.spec.X509EncodedKeySpec
import java.util.Base64 import java.util.Base64
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
...@@ -59,13 +64,58 @@ class NodeClient() { ...@@ -59,13 +64,58 @@ class NodeClient() {
private var challengeTimestamp: Long = 0 private var challengeTimestamp: Long = 0
private var hasReceivedChallenge = false private var hasReceivedChallenge = false
// SharedPreferences for device identity persistence
private var prefs: SharedPreferences? = null
fun setContext(context: Context) {
prefs = context.getSharedPreferences("openclaw_device_identity", Context.MODE_PRIVATE)
// Regenerate identity if needed (will load existing if available)
generateDeviceIdentity()
}
init { init {
// Generate device identity on initialization // Generate device identity on initialization
// Note: setContext should be called after creation to enable persistence
generateDeviceIdentity() generateDeviceIdentity()
} }
private fun generateDeviceIdentity() { private fun generateDeviceIdentity() {
try { try {
// Try to load existing keys from SharedPreferences
val savedPublicKey = prefs?.getString("device_public_key", null)
val savedPrivateKey = prefs?.getString("device_private_key", null)
if (savedPublicKey != null && savedPrivateKey != null) {
// Load existing keys
devicePublicKey = savedPublicKey
devicePrivateKey = savedPrivateKey
// Reconstruct PrivateKey object from PEM
val keyFactory = KeyFactory.getInstance("RSA")
val privateKeyBytes = Base64.getDecoder().decode(
savedPrivateKey
.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replace("\n", "")
)
val privateKeySpec = PKCS8EncodedKeySpec(privateKeyBytes)
devicePrivateKeyObject = keyFactory.generatePrivate(privateKeySpec)
// Derive deviceId from public key
val publicKeyBytes = Base64.getDecoder().decode(
savedPublicKey
.replace("-----BEGIN PUBLIC KEY-----", "")
.replace("-----END PUBLIC KEY-----", "")
.replace("\n", "")
)
val publicKey = keyFactory.generatePublic(X509EncodedKeySpec(publicKeyBytes))
val digest = MessageDigest.getInstance("SHA-256")
val hash = digest.digest(publicKey.encoded)
deviceId = hash.joinToString("") { "%02x".format(it) }
Log.d(TAG, "Loaded existing device identity: deviceId=$deviceId")
} else {
// Generate new key pair
val keyGen = KeyPairGenerator.getInstance("RSA") val keyGen = KeyPairGenerator.getInstance("RSA")
keyGen.initialize(2048) keyGen.initialize(2048)
val keyPair = keyGen.generateKeyPair() val keyPair = keyGen.generateKeyPair()
...@@ -88,9 +138,16 @@ class NodeClient() { ...@@ -88,9 +138,16 @@ class NodeClient() {
val hash = digest.digest(publicKey.encoded) val hash = digest.digest(publicKey.encoded)
deviceId = hash.joinToString("") { "%02x".format(it) } deviceId = hash.joinToString("") { "%02x".format(it) }
Log.d(TAG, "Device identity generated: deviceId=$deviceId") // Persist keys for future use
prefs?.edit()
?.putString("device_public_key", devicePublicKey)
?.putString("device_private_key", devicePrivateKey)
?.apply()
Log.d(TAG, "Generated new device identity: deviceId=$deviceId")
}
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Failed to generate device identity: ${e.message}") Log.e(TAG, "Failed to generate/load device identity: ${e.message}")
// Generate a random device ID as fallback // Generate a random device ID as fallback
deviceId = "device-${System.currentTimeMillis()}" deviceId = "device-${System.currentTimeMillis()}"
} }
...@@ -238,7 +295,7 @@ class NodeClient() { ...@@ -238,7 +295,7 @@ class NodeClient() {
put("minProtocol", 3) put("minProtocol", 3)
put("maxProtocol", 3) put("maxProtocol", 3)
put("client", JSONObject().apply { put("client", JSONObject().apply {
put("id", "node-host") put("id", nodeId.ifBlank { "android-node" })
put("displayName", nodeId) put("displayName", nodeId)
put("version", "1.0.0") put("version", "1.0.0")
put("platform", "android") put("platform", "android")
......
...@@ -78,7 +78,7 @@ class SettingsRepository( ...@@ -78,7 +78,7 @@ class SettingsRepository(
val nodeId: Flow<String> = context.dataStore.data.map { preferences -> val nodeId: Flow<String> = context.dataStore.data.map { preferences ->
val prefsValue = getSharedPrefString("node_id") val prefsValue = getSharedPrefString("node_id")
if (prefsValue.isNotEmpty()) prefsValue if (prefsValue.isNotEmpty()) prefsValue
else preferences[NODE_ID] ?: "android" else preferences[NODE_ID] ?: "android-node"
} }
val autoConnect: Flow<Boolean> = context.dataStore.data.map { preferences -> val autoConnect: Flow<Boolean> = context.dataStore.data.map { preferences ->
......
...@@ -45,6 +45,9 @@ class NodeService : Service() { ...@@ -45,6 +45,9 @@ class NodeService : Service() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
// Initialize NodeClient with context for device identity persistence
nodeClient.setContext(this)
// NodeClient is initialized directly above // NodeClient is initialized directly above
// Debug: Write to file // Debug: Write to file
try { try {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment