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 clear() {70 persist(emptyList())71 }7273 fun observeAll(): Flow<List<PendingVouchedDevice>> = callbackFlow {74 trySend(getAll())75 val listener = android.content.SharedPreferences.OnSharedPreferenceChangeListener { _, key ->76 if (key == KEY_PENDING || key == null) trySend(getAll())77 }78 prefs.registerOnSharedPreferenceChangeListener(listener)79 awaitClose { prefs.unregisterOnSharedPreferenceChangeListener(listener) }80 }8182 private fun persist(devices: List<PendingVouchedDevice>) {83 prefs.edit()84 .putString(KEY_PENDING, json.encodeToString(listSerializer, devices))85 .apply()86 }8788 private companion object {89 const val PREFS_FILE = "peach_pending_vouched_devices"90 const val KEY_PENDING = "pending_vouched_devices"91 const val MAX_PENDING = 5092 val json = Json { encodeDefaults = true; ignoreUnknownKeys = true }93 }94}