From d4bb3e03637a371fbc4bcf4b46b62d2beca757f7 Mon Sep 17 00:00:00 2001 From: Cobb Date: Sat, 13 Jun 2026 09:48:30 -0700 Subject: [PATCH] security: store device-issued random token from /pair (drop serial-derived token) Matches the adacam-api secure-pairing redesign: /pair now returns a random token during the device pairing window. Drop deriveApiToken (sha256 of the serial) + savePairing now takes (serial, token); pairDevice() persists the device-returned token and errors clearly if the pairing window is closed. NEEDS BUILD+DEVICE PAIRING TEST before merge to main. --- .../java/com/adamaps/varroa/data/Models.kt | 3 +++ .../adamaps/varroa/data/SettingsDataStore.kt | 19 ++++----------- .../varroa/viewmodel/SettingsViewModel.kt | 23 ++++++++++++------- 3 files changed, 22 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/com/adamaps/varroa/data/Models.kt b/app/src/main/java/com/adamaps/varroa/data/Models.kt index 18faff5..c50838b 100644 --- a/app/src/main/java/com/adamaps/varroa/data/Models.kt +++ b/app/src/main/java/com/adamaps/varroa/data/Models.kt @@ -115,6 +115,9 @@ 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 diff --git a/app/src/main/java/com/adamaps/varroa/data/SettingsDataStore.kt b/app/src/main/java/com/adamaps/varroa/data/SettingsDataStore.kt index 6d9adcc..e6bac59 100644 --- a/app/src/main/java/com/adamaps/varroa/data/SettingsDataStore.kt +++ b/app/src/main/java/com/adamaps/varroa/data/SettingsDataStore.kt @@ -10,7 +10,6 @@ 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 by preferencesDataStore(name = "varroa_settings") @@ -30,16 +29,6 @@ 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 { @@ -99,11 +88,11 @@ class SettingsDataStore(private val context: Context) { } /** - * Store pairing data after successful pairing with AdaCam. - * Derives and stores the API token from the serial. + * 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. */ - suspend fun savePairing(serial: String) { - val token = deriveApiToken(serial) + suspend fun savePairing(serial: String, token: String) { context.dataStore.edit { prefs -> prefs[KEY_DEVICE_SERIAL] = serial prefs[KEY_API_TOKEN] = token diff --git a/app/src/main/java/com/adamaps/varroa/viewmodel/SettingsViewModel.kt b/app/src/main/java/com/adamaps/varroa/viewmodel/SettingsViewModel.kt index 8e16e29..42de564 100644 --- a/app/src/main/java/com/adamaps/varroa/viewmodel/SettingsViewModel.kt +++ b/app/src/main/java/com/adamaps/varroa/viewmodel/SettingsViewModel.kt @@ -120,15 +120,22 @@ 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") - - // Store pairing data (derives token automatically) - store.savePairing(serial) - - // Update client with new token - val newSettings = store.settings.first() - client.apiToken = newSettings.apiToken - + + // Persist the RANDOM token the device handed us (not derived) + store.savePairing(serial, token) + + // Update client with the new token + client.apiToken = token + _pairingResult.value = "Paired successfully! Serial: $serial" // Fetch statuses now that we're paired