Project

General

Profile

User Story #47 » 0002-feat-netcode-persist-mesh-CA-key-credential-offline-.patch

knight8241, 08/07/2026 18:32

View differences:

androidApp/src/main/java/com/aether/mofe/data/MeshRepository.kt
import com.aether.mofe.platform.band.BandPredicateRecord
import com.aether.mofe.platform.band.BroadcastBand
import com.aether.mofe.platform.band.SharedMeshState
import com.aether.mofe.platform.netcode.NetcodeTrust
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
......
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonObject
import kotlin.math.atan2
import kotlin.math.cos
import kotlin.math.hypot
......
runCatching {
ensureX25519()
loadMemberships()
// Restore the pinned mesh CA key (cache from last enrollment) so the netcode can be
// constructed offline after a restart — see NetcodeTrust.attach / MeshTrustMaterial.
appContext?.let { NetcodeTrust.attach(it.getSharedPreferences("aether_netcode_trust", Context.MODE_PRIVATE)) }
// A used UWB scope is dead, so a recycled link reopens on a FRESH scope
// with a NEW local address. Drop that peer's cached advertised address so
// the next tick re-mints it (see maybeOpenUwbSessions) and re-broadcasts
......
_activeMeshId.value = meshId
selfContract = selfContract.copy(meshId = meshId)
loadMeshKey(meshId) // restore a previously-adopted group key for this mesh
loadPersistedCredential(meshId) // restore this device's cached mesh-CA credential (offline netcode)
saveMemberships() // persist which mesh is active
rebuild()
}
......
selfControleeAddr = ""
}
// ── Netcode trust: cache this device's own mesh-CA credential (cache from last enrollment) ──
private fun credentialPrefs() =
appContext?.getSharedPreferences("aether_credentials", Context.MODE_PRIVATE)
/** Reload this device's persisted mesh-CA credential for [meshId] so the netcode has its
* DeviceIdentity offline after a restart. No-op if none is cached or one is already loaded. The
* MeshTrustMaterial readiness gate re-checks device/mesh/expiry before it's used to build a node. */
private fun loadPersistedCredential(meshId: String) {
if (_gatewayCredential.value != null || meshId.isBlank()) return
val raw = credentialPrefs()?.getString("cred_$meshId", null) ?: return
_gatewayCredential.value = runCatching { membershipJson.parseToJsonElement(raw).jsonObject }.getOrNull()
}
/** A1 pull complete: arm the uplink and flip the status chip to Registered. */
fun applyPulledCredential(credential: JsonObject, ownerLabel: String) {
_gatewayCredential.value = credential
_regState.value = MeshRegState.Registered(ownerLabel, canonicalMeshId ?: selfContract.meshId)
// Persist THIS device's credential (cache from last enrollment) so it's available offline after
// a restart — reloaded by setCanonicalMeshId → loadPersistedCredential.
val meshId = canonicalMeshId ?: selfContract.meshId
if (meshId.isNotBlank()) credentialPrefs()?.edit { putString("cred_$meshId", credential.toString()) }
_regState.value = MeshRegState.Registered(ownerLabel, meshId)
}
}
androidApp/src/main/java/com/aether/mofe/platform/netcode/NetcodeTrust.kt
package com.aether.mofe.platform.netcode
import android.content.SharedPreferences
import android.util.Base64
import android.util.Log
import com.aether.mofe.messaging.MeshCodec
......
*/
object NetcodeTrust {
private const val TAG = "NetcodeTrust"
private const val K_CA_KEY = "mesh_ca_pub_b64"
private const val K_CA_KID = "mesh_ca_key_id"
@Volatile private var pinnedCaPublicKey: ByteArray? = null
@Volatile private var pinnedKeyId: String? = null
/** Durable store for the CA pin — set once at startup so the pin survives a restart (see [attach]). */
@Volatile private var store: SharedPreferences? = null
/**
* Attach durable storage and RESTORE any previously pinned CA key — the "cache from last
* enrollment" reload. Call once at startup (e.g. from the repository init, which holds a Context)
* BEFORE the first offline netcode bring-up. Without this, the CA pin lives only in memory and a
* process restart while offline loses it, so `MeshNode.build` could never be constructed without
* the server. Idempotent: a later online [pinMeshCa] write-through refreshes the stored key.
*/
fun attach(prefs: SharedPreferences) {
store = prefs
if (pinnedCaPublicKey == null) {
val b64 = prefs.getString(K_CA_KEY, null) ?: return
applyPin(b64, prefs.getString(K_CA_KID, null))
}
}
/**
* Pin the mesh CA anchor fetched from the server. Idempotent; logs (does not silently accept) a
* changed key id, since a rotated CA key is a trust event the app should surface.
* Pin the mesh CA anchor fetched from the server, and write it through to durable storage so it is
* available offline after a restart. Idempotent; logs (does not silently accept) a changed key id,
* since a rotated CA key is a trust event the app should surface.
*/
fun pinMeshCa(meshCaPublicKeyB64: String, keyId: String?) {
val decoded = runCatching { Base64.decode(meshCaPublicKeyB64, Base64.DEFAULT) }.getOrNull() ?: return
if (!applyPin(meshCaPublicKeyB64, keyId)) return
store?.edit()?.putString(K_CA_KEY, meshCaPublicKeyB64)?.putString(K_CA_KID, keyId)?.apply()
}
/** Decode + set the in-memory pin (no persistence). Returns false if the key can't be decoded. */
private fun applyPin(meshCaPublicKeyB64: String, keyId: String?): Boolean {
val decoded = runCatching { Base64.decode(meshCaPublicKeyB64, Base64.DEFAULT) }.getOrNull() ?: return false
val prevId = pinnedKeyId
if (prevId != null && keyId != null && prevId != keyId) {
Log.w(TAG, "mesh CA key id changed ($prevId → $keyId) — CA rotated or endpoint switched")
}
pinnedCaPublicKey = decoded
pinnedKeyId = keyId
return true
}
/** The pinned Ed25519 mesh CA public key (raw bytes), or null until [pinMeshCa] has run. */
(1-1/4)