PendingVouchedDeviceStore.kt

3.1 KB · raw

1package com.peachpasswords.sync23import android.content.Context4import androidx.security.crypto.EncryptedSharedPreferences5import androidx.security.crypto.MasterKey6import dagger.hilt.android.qualifiers.ApplicationContext7import kotlinx.coroutines.channels.awaitClose8import kotlinx.coroutines.flow.Flow9import kotlinx.coroutines.flow.callbackFlow10import kotlinx.serialization.Serializable11import kotlinx.serialization.builtins.ListSerializer12import kotlinx.serialization.json.Json13import javax.inject.Inject14import javax.inject.Singleton1516@Serializable17data class PendingVouchedDevice(18    val deviceId: String,19    val deviceName: String,20    val deviceType: String,21    val longTermPublicKeyHex: String,22    val signingPublicKeyHex: String,23    val wsPort: Int? = null,24    val pairedAt: Long,25    val receivedAt: Long,26    val vouchedByDeviceId: String,27)2829interface PendingVouchedRegistry {30    fun getAll(): List<PendingVouchedDevice>31    fun upsert(device: PendingVouchedDevice)32    fun remove(id: String)33}3435@Singleton36class PendingVouchedDeviceStore @Inject constructor(37    @ApplicationContext context: Context,38) : PendingVouchedRegistry {39    private val masterKey = MasterKey.Builder(context)40        .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)41        .build()4243    private val prefs = EncryptedSharedPreferences.create(44        context,45        PREFS_FILE,46        masterKey,47        EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,48        EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,49    )5051    private val listSerializer = ListSerializer(PendingVouchedDevice.serializer())5253    override fun getAll(): List<PendingVouchedDevice> {54        val raw = prefs.getString(KEY_PENDING, null) ?: return emptyList()55        return runCatching { json.decodeFromString(listSerializer, raw) }.getOrDefault(emptyList())56    }5758    fun get(id: String): PendingVouchedDevice? = getAll().firstOrNull { it.deviceId == id }5960    override fun upsert(device: PendingVouchedDevice) {61        val next = (getAll().filterNot { it.deviceId == device.deviceId } + device).takeLast(MAX_PENDING)62        persist(next)63    }6465    override fun remove(id: String) {66        persist(getAll().filterNot { it.deviceId == id })67    }6869    fun observeAll(): Flow<List<PendingVouchedDevice>> = callbackFlow {70        trySend(getAll())71        val listener = android.content.SharedPreferences.OnSharedPreferenceChangeListener { _, key ->72            if (key == KEY_PENDING || key == null) trySend(getAll())73        }74        prefs.registerOnSharedPreferenceChangeListener(listener)75        awaitClose { prefs.unregisterOnSharedPreferenceChangeListener(listener) }76    }7778    private fun persist(devices: List<PendingVouchedDevice>) {79        prefs.edit()80            .putString(KEY_PENDING, json.encodeToString(listSerializer, devices))81            .apply()82    }8384    private companion object {85        const val PREFS_FILE = "peach_pending_vouched_devices"86        const val KEY_PENDING = "pending_vouched_devices"87        const val MAX_PENDING = 5088        val json = Json { encodeDefaults = true; ignoreUnknownKeys = true }89    }90}