1package com.peachpasswords.sync23import android.content.Context4import android.content.SharedPreferences5import androidx.security.crypto.EncryptedSharedPreferences6import androidx.security.crypto.MasterKey7import dagger.hilt.android.EntryPointAccessors8import dagger.hilt.android.qualifiers.ApplicationContext9import dagger.hilt.EntryPoint10import dagger.hilt.InstallIn11import dagger.hilt.components.SingletonComponent12import javax.inject.Inject13import javax.inject.Singleton141516171819202122232425@Singleton26class SyncGenerationStore @Inject constructor(27 @ApplicationContext private val context: Context,28 private val devices: PairedDeviceStore,29 private val pendingDevices: PendingVouchedDeviceStore,30 private val lockedSyncSpool: LockedSyncSpoolStore,31) : SyncGenerationPort {3233 private val generationPrefs =34 context.getSharedPreferences(GENERATION_PREFS_FILE, Context.MODE_PRIVATE)3536 override fun readBlob(): String? = generationPrefs.getString(KEY_GENERATION, null)3738 override fun writeBlob(blob: String): Boolean =39 generationPrefs.edit().putString(KEY_GENERATION, blob).commit() && readBlob() == blob4041 override fun hasPairedDevices(): Boolean = devices.getAll().isNotEmpty()4243 override fun hasRevocations(): Boolean = devices.listRevocations().isNotEmpty()4445 override fun hasIssuerSigningKeys(): Boolean = devices.hasIssuerSigningKeys()4647 override fun hasPendingVouchedDevices(): Boolean = pendingDevices.getAll().isNotEmpty()4849 50515253545556575859 override fun archiveLegacyState(generation: Int): String? = runCatching {60 val archive = encryptedPrefs(context, ARCHIVE_PREFS_FILE)61 val prefix = "g$generation:"62 val expected = buildMap<String, Any?> {63 put("${prefix}meta", """{"schemaVersion":1,"generation":$generation,"archivedAt":${System.currentTimeMillis()}}""")64 for ((key, value) in devices.snapshot()) put("$prefix$ARCHIVE_DEVICES_PREFIX$key", value)65 for ((key, value) in pendingDevices.snapshot()) put("$prefix$ARCHIVE_PENDING_PREFIX$key", value)66 }67 val edit = archive.edit()68 for ((key, value) in expected) {69 when (value) {70 is String -> edit.putString(key, value)71 is Set<*> -> edit.putStringSet(key, value.filterIsInstance<String>().toSet())72 }73 }74 check(edit.commit()) { "generation_archive_persist_failed" }75 check(expected.all { (key, value) -> archiveMatches(archive, key, value) }) {76 "generation_archive_verify_failed"77 }78 ARCHIVE_PREFS_FILE79 }.getOrNull()8081 private fun archiveMatches(archive: SharedPreferences, key: String, expected: Any?): Boolean =82 when (expected) {83 is String -> archive.getString(key, null) == expected84 is Set<*> -> archive.getStringSet(key, null) == expected.filterIsInstance<String>().toSet()85 else -> false86 }8788 override fun clearLegacyTrustState(): Boolean = runCatching {89 PeachSyncService.stop(context)90 lockedSyncSpool.clear()91 devices.clear()92 pendingDevices.clear()93 devices.getAll().isEmpty() && devices.listRevocations().isEmpty() &&94 !devices.hasIssuerSigningKeys() && pendingDevices.getAll().isEmpty()95 }.getOrDefault(false)9697 9899100101102103104 override fun ensureV2Identity(): Boolean = runCatching {105 loadOrCreateLocalIdentity(context)106 true107 }.recoverCatching {108 check(!hasLegacyTrustState()) { "identity_mint_over_live_trust" }109 rotateDeviceId(context)110 true111 }.getOrDefault(false)112113 override fun localDeviceId(): String? =114 runCatching { loadOrCreateLocalIdentity(context).deviceId }.getOrNull()115116 117 fun clear() {118 generationPrefs.edit().clear().commit()119 }120121 private companion object {122 const val GENERATION_PREFS_FILE = "peach_sync_generation_v1"123 const val ARCHIVE_PREFS_FILE = "peach_sync_archive_v1"124 const val KEY_GENERATION = "sync_generation"125 const val ARCHIVE_DEVICES_PREFIX = "paired:"126 const val ARCHIVE_PENDING_PREFIX = "pending:"127 }128}129130131132133134135internal fun syncGenerationStore(context: Context): SyncGenerationStore =136 EntryPointAccessors137 .fromApplication(context.applicationContext, SyncGenerationEntryPoint::class.java)138 .syncGenerationStore()139140@EntryPoint141@InstallIn(SingletonComponent::class)142internal interface SyncGenerationEntryPoint {143 fun syncGenerationStore(): SyncGenerationStore144}145146private fun encryptedPrefs(context: Context, name: String): SharedPreferences =147 EncryptedSharedPreferences.create(148 context,149 name,150 MasterKey.Builder(context).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build(),151 EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,152 EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,153 )