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 {
composeOptions {
kotlinCompilerExtensionVersion = "1.5.5"
kotlinCompilerOptions {
freeCompilerArgs.addAll("-P", "plugin:androidx.compose.compiler.plugins.kotlin:suppressKotlinVersionCompatibilityCheck=1.9.20")
}
}
packaging {
......
package com.nexlab.openclaw.node.data.remote
import android.content.Context
import android.content.SharedPreferences
import android.util.Log
import com.nexlab.openclaw.node.domain.model.NodeCapabilities
import com.nexlab.openclaw.node.domain.model.NodeCommand
......@@ -11,10 +13,13 @@ import kotlinx.coroutines.flow.receiveAsFlow
import okhttp3.*
import org.json.JSONArray
import org.json.JSONObject
import java.security.KeyFactory
import java.security.KeyPairGenerator
import java.security.MessageDigest
import java.security.PrivateKey
import java.security.Signature
import java.security.spec.PKCS8EncodedKeySpec
import java.security.spec.X509EncodedKeySpec
import java.util.Base64
import java.util.concurrent.TimeUnit
import javax.inject.Inject
......@@ -59,38 +64,90 @@ class NodeClient() {
private var challengeTimestamp: Long = 0
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 {
// Generate device identity on initialization
// Note: setContext should be called after creation to enable persistence
generateDeviceIdentity()
}
private fun generateDeviceIdentity() {
try {
val keyGen = KeyPairGenerator.getInstance("RSA")
keyGen.initialize(2048)
val keyPair = keyGen.generateKeyPair()
val publicKey = keyPair.public
val privateKey = keyPair.private
devicePrivateKeyObject = privateKey
// Convert to PEM format
devicePublicKey = "-----BEGIN PUBLIC KEY-----\n" +
Base64.getEncoder().encodeToString(publicKey.encoded).chunked(64).joinToString("\n") +
"\n-----END PUBLIC KEY-----"
devicePrivateKey = "-----BEGIN PRIVATE KEY-----\n" +
Base64.getEncoder().encodeToString(privateKey.encoded).chunked(64).joinToString("\n") +
"\n-----END PRIVATE KEY-----"
// Generate device ID from public key hash
val digest = MessageDigest.getInstance("SHA-256")
val hash = digest.digest(publicKey.encoded)
deviceId = hash.joinToString("") { "%02x".format(it) }
Log.d(TAG, "Device identity generated: deviceId=$deviceId")
// 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")
keyGen.initialize(2048)
val keyPair = keyGen.generateKeyPair()
val publicKey = keyPair.public
val privateKey = keyPair.private
devicePrivateKeyObject = privateKey
// Convert to PEM format
devicePublicKey = "-----BEGIN PUBLIC KEY-----\n" +
Base64.getEncoder().encodeToString(publicKey.encoded).chunked(64).joinToString("\n") +
"\n-----END PUBLIC KEY-----"
devicePrivateKey = "-----BEGIN PRIVATE KEY-----\n" +
Base64.getEncoder().encodeToString(privateKey.encoded).chunked(64).joinToString("\n") +
"\n-----END PRIVATE KEY-----"
// Generate device ID from public key hash
val digest = MessageDigest.getInstance("SHA-256")
val hash = digest.digest(publicKey.encoded)
deviceId = hash.joinToString("") { "%02x".format(it) }
// 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) {
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
deviceId = "device-${System.currentTimeMillis()}"
}
......@@ -238,7 +295,7 @@ class NodeClient() {
put("minProtocol", 3)
put("maxProtocol", 3)
put("client", JSONObject().apply {
put("id", "node-host")
put("id", nodeId.ifBlank { "android-node" })
put("displayName", nodeId)
put("version", "1.0.0")
put("platform", "android")
......
......@@ -78,7 +78,7 @@ class SettingsRepository(
val nodeId: Flow<String> = context.dataStore.data.map { preferences ->
val prefsValue = getSharedPrefString("node_id")
if (prefsValue.isNotEmpty()) prefsValue
else preferences[NODE_ID] ?: "android"
else preferences[NODE_ID] ?: "android-node"
}
val autoConnect: Flow<Boolean> = context.dataStore.data.map { preferences ->
......
......@@ -45,6 +45,9 @@ class NodeService : Service() {
override fun onCreate() {
super.onCreate()
// Initialize NodeClient with context for device identity persistence
nodeClient.setContext(this)
// NodeClient is initialized directly above
// Debug: Write to file
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