package org.meshbay.client.keys import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters import org.bouncycastle.crypto.signers.Ed25519Signer import org.json.JSONObject import java.security.SecureRandom /** * The device's key for signing in to the hub (E3): generated, held and used * here, never handed to the page, which asks for a signature over * `meshbay:user_auth::` — the bytes `POST /v1/users/auth` * verifies. Not a per-node identity: nothing here correlates a person across * operators (main.js `device:*`). */ class DeviceKey(private val store: Secrets, private val now: () -> Long = { System.currentTimeMillis() / 1000 }) { private fun current(): Ed25519PrivateKeyParameters? { val stored = store.read().optString(SecretStore.DEVICE_KEY, "") return if (stored.isEmpty()) null else Kdf.edFromPkcs8(Kdf.unb64(stored)) } private fun publicOf(k: Ed25519PrivateKeyParameters) = Kdf.b64(k.generatePublicKey().encoded) fun ensure(): String { current()?.let { return publicOf(it) } val k = Ed25519PrivateKeyParameters(SecureRandom()) store.update { it.put(SecretStore.DEVICE_KEY, Kdf.b64(Kdf.edToPkcs8(k))) } return publicOf(k) } fun publicKey(): String? = current()?.let { publicOf(it) } fun sign(username: String): JSONObject? { val k = current() ?: return null val ts = now() // The username is inside the signature, so one collected for another // account is not usable. val message = "meshbay:user_auth:$username:$ts".toByteArray(Charsets.UTF_8) val s = Ed25519Signer().apply { init(true, k); update(message, 0, message.size) } return JSONObject().put("timestamp", ts).put("signature", Kdf.b64(s.generateSignature())) } fun forget(): Boolean { store.update { it.remove(SecretStore.DEVICE_KEY) } return true } }