Compare commits

..

No commits in common. "feat/secure-pairing" and "main" have entirely different histories.

3 changed files with 23 additions and 22 deletions

View file

@ -115,9 +115,6 @@ data class BeePlugin(
data class PairResponse(
@SerializedName("serial") val serial: String,
// Random device API token, returned once during the pairing window. Nullable
// so an old/closed-window device (no token field / 403) is handled gracefully.
@SerializedName("token") val token: String? = null,
@SerializedName("version") val version: String,
@SerializedName("ap_ip") val apIp: String,
@SerializedName("api_port") val apiPort: Int

View file

@ -10,6 +10,7 @@ import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import java.security.MessageDigest
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "varroa_settings")
@ -29,6 +30,16 @@ data class VarroaSettings(
val isPaired: Boolean = false
)
/**
* Derive the API token from the device serial.
* Token = first 32 chars of SHA-256("adacam-api-{serial}-token")
*/
fun deriveApiToken(serial: String): String {
val input = "adacam-api-$serial-token"
val bytes = MessageDigest.getInstance("SHA-256").digest(input.toByteArray())
return bytes.joinToString("") { "%02x".format(it) }.substring(0, 32)
}
class SettingsDataStore(private val context: Context) {
companion object {
@ -88,11 +99,11 @@ class SettingsDataStore(private val context: Context) {
}
/**
* Store pairing data after a successful /pair. The device returns a RANDOM
* token (not derived from the serial) during its one-shot pairing window;
* persist the serial + that token.
* Store pairing data after successful pairing with AdaCam.
* Derives and stores the API token from the serial.
*/
suspend fun savePairing(serial: String, token: String) {
suspend fun savePairing(serial: String) {
val token = deriveApiToken(serial)
context.dataStore.edit { prefs ->
prefs[KEY_DEVICE_SERIAL] = serial
prefs[KEY_API_TOKEN] = token

View file

@ -120,21 +120,14 @@ class SettingsViewModel(app: Application) : AndroidViewModel(app) {
when (val result = client.pair()) {
is ApiResult.Success -> {
val serial = result.data.serial
val token = result.data.token
if (token.isNullOrBlank()) {
// No token = pairing window closed on the device (or old build).
Log.w(TAG, "Pairing returned no token (window closed?)")
_pairingResult.value =
"Pairing window closed. Run 'adacam-pair' on the Bee, then pair again."
return@launch
}
Log.i(TAG, "Pairing successful: serial=$serial")
// Persist the RANDOM token the device handed us (not derived)
store.savePairing(serial, token)
// Store pairing data (derives token automatically)
store.savePairing(serial)
// Update client with the new token
client.apiToken = token
// Update client with new token
val newSettings = store.settings.first()
client.apiToken = newSettings.apiToken
_pairingResult.value = "Paired successfully! Serial: $serial"