security: store device-issued random token from /pair (drop serial-derived token)
All checks were successful
gitleaks / scan (push) Successful in 22s

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.
This commit is contained in:
Cobb 2026-06-13 09:48:30 -07:00
parent 55a47d3817
commit d4bb3e0363
3 changed files with 22 additions and 23 deletions

View file

@ -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

View file

@ -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<Preferences> 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

View file

@ -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