package com.peachpasswords.sync import android.content.Context import androidx.security.crypto.EncryptedSharedPreferences import androidx.security.crypto.MasterKey import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow import kotlinx.serialization.Serializable import kotlinx.serialization.builtins.ListSerializer import kotlinx.serialization.json.Json import javax.inject.Inject import javax.inject.Singleton @Serializable data class PendingVouchedDevice( val deviceId: String, val deviceName: String, val deviceType: String, val longTermPublicKeyHex: String, val signingPublicKeyHex: String, val wsPort: Int? = null, val pairedAt: Long, val receivedAt: Long, val vouchedByDeviceId: String, ) interface PendingVouchedRegistry { fun getAll(): List fun upsert(device: PendingVouchedDevice) fun remove(id: String) } @Singleton class PendingVouchedDeviceStore @Inject constructor( @ApplicationContext context: Context, ) : PendingVouchedRegistry { private val masterKey = MasterKey.Builder(context) .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) .build() private val prefs = EncryptedSharedPreferences.create( context, PREFS_FILE, masterKey, EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM, ) private val listSerializer = ListSerializer(PendingVouchedDevice.serializer()) override fun getAll(): List { val raw = prefs.getString(KEY_PENDING, null) ?: return emptyList() return runCatching { json.decodeFromString(listSerializer, raw) }.getOrDefault(emptyList()) } fun get(id: String): PendingVouchedDevice? = getAll().firstOrNull { it.deviceId == id } override fun upsert(device: PendingVouchedDevice) { val next = (getAll().filterNot { it.deviceId == device.deviceId } + device).takeLast(MAX_PENDING) persist(next) } override fun remove(id: String) { persist(getAll().filterNot { it.deviceId == id }) } fun clear() { persist(emptyList()) } fun observeAll(): Flow> = callbackFlow { trySend(getAll()) val listener = android.content.SharedPreferences.OnSharedPreferenceChangeListener { _, key -> if (key == KEY_PENDING || key == null) trySend(getAll()) } prefs.registerOnSharedPreferenceChangeListener(listener) awaitClose { prefs.unregisterOnSharedPreferenceChangeListener(listener) } } private fun persist(devices: List) { prefs.edit() .putString(KEY_PENDING, json.encodeToString(listSerializer, devices)) .apply() } private companion object { const val PREFS_FILE = "peach_pending_vouched_devices" const val KEY_PENDING = "pending_vouched_devices" const val MAX_PENDING = 50 val json = Json { encodeDefaults = true; ignoreUnknownKeys = true } } }