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.builtins.ListSerializer11import kotlinx.serialization.json.Json12import javax.inject.Inject13import javax.inject.Singleton141516171819202122232425262728293031323334353637@Singleton38class PairedDeviceStore @Inject constructor(39 @ApplicationContext context: Context,40) : PairedDeviceRegistry {41 private val masterKey = MasterKey.Builder(context)42 .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)43 .build()4445 private val prefs = EncryptedSharedPreferences.create(46 context,47 PREFS_FILE,48 masterKey,49 EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,50 EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,51 )5253 private val listSerializer = ListSerializer(PairedDevice.serializer())5455 override fun getAll(): List<PairedDevice> {56 val raw = prefs.getString(KEY_DEVICES, null) ?: return emptyList()57 return runCatching { json.decodeFromString(listSerializer, raw) }.getOrDefault(emptyList())58 }5960 override fun get(id: String): PairedDevice? = getAll().firstOrNull { it.deviceId == id }6162 override fun getByPubkey(pubkeyHex: String): PairedDevice? =63 getAll().firstOrNull {64 it.longTermPublicKeyHex?.let { key -> CryptoCompare.hex(key, pubkeyHex) } == true65 }6667 68 override fun upsert(device: PairedDevice) {69 val next = getAll().filterNot { it.deviceId == device.deviceId } + device70 persist(next)71 }7273 fun remove(id: String) {74 persist(getAll().filterNot { it.deviceId == id })75 }7677 fun clear() {78 prefs.edit().remove(KEY_DEVICES).apply()79 }8081 82838485 fun updateLastSeen(id: String, ip: String? = null) {86 val current = get(id) ?: return87 if (ip == null) return88 upsert(current.copy(lastKnownIp = ip))89 }9091 fun updateLastSync(id: String, at: Long = System.currentTimeMillis()) {92 val current = get(id) ?: return93 upsert(current.copy(lastSyncedAt = at))94 }9596 979899100 fun setRelayUrlForAll(relayUrl: String) {101 persist(withRelayUrl(getAll(), relayUrl))102 }103104 105106107108109 fun observeAll(): Flow<List<PairedDevice>> = callbackFlow {110 trySend(getAll())111 val listener = android.content.SharedPreferences.OnSharedPreferenceChangeListener { _, key ->112 if (key == KEY_DEVICES || key == null) trySend(getAll())113 }114 prefs.registerOnSharedPreferenceChangeListener(listener)115 awaitClose { prefs.unregisterOnSharedPreferenceChangeListener(listener) }116 }117118 private fun persist(devices: List<PairedDevice>) {119 prefs.edit()120 .putString(KEY_DEVICES, json.encodeToString(listSerializer, devices))121 .apply()122 }123124 private companion object {125 const val PREFS_FILE = "peach_paired_devices"126 const val KEY_DEVICES = "paired_devices"127 val json = Json { encodeDefaults = true; ignoreUnknownKeys = true }128 }129}130131internal fun withRelayUrl(132 devices: List<PairedDevice>,133 relayUrl: String,134): List<PairedDevice> = devices.map { it.copy(relayUrl = relayUrl) }