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,38 +64,90 @@ class NodeClient() { ...@@ -59,38 +64,90 @@ 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 {
val keyGen = KeyPairGenerator.getInstance("RSA") // Try to load existing keys from SharedPreferences
keyGen.initialize(2048) val savedPublicKey = prefs?.getString("device_public_key", null)
val keyPair = keyGen.generateKeyPair() val savedPrivateKey = prefs?.getString("device_private_key", null)
val publicKey = keyPair.public if (savedPublicKey != null && savedPrivateKey != null) {
val privateKey = keyPair.private // Load existing keys
devicePrivateKeyObject = privateKey devicePublicKey = savedPublicKey
devicePrivateKey = savedPrivateKey
// Convert to PEM format
devicePublicKey = "-----BEGIN PUBLIC KEY-----\n" + // Reconstruct PrivateKey object from PEM
Base64.getEncoder().encodeToString(publicKey.encoded).chunked(64).joinToString("\n") + val keyFactory = KeyFactory.getInstance("RSA")
"\n-----END PUBLIC KEY-----" val privateKeyBytes = Base64.getDecoder().decode(
savedPrivateKey
devicePrivateKey = "-----BEGIN PRIVATE KEY-----\n" + .replace("-----BEGIN PRIVATE KEY-----", "")
Base64.getEncoder().encodeToString(privateKey.encoded).chunked(64).joinToString("\n") + .replace("-----END PRIVATE KEY-----", "")
"\n-----END PRIVATE KEY-----" .replace("\n", "")
)
// Generate device ID from public key hash val privateKeySpec = PKCS8EncodedKeySpec(privateKeyBytes)
val digest = MessageDigest.getInstance("SHA-256") devicePrivateKeyObject = keyFactory.generatePrivate(privateKeySpec)
val hash = digest.digest(publicKey.encoded)
deviceId = hash.joinToString("") { "%02x".format(it) } // Derive deviceId from public key
val publicKeyBytes = Base64.getDecoder().decode(
Log.d(TAG, "Device identity generated: deviceId=$deviceId") 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) { } 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