M7: polish + E2E + docs (layout pass, theming, states, e2e driver, schema, setup.md, security)

This commit is contained in:
ARIA committed 2026-08-20 12:00:13 +02:00
1 parent 0cc8b7aafe
commit bf6bf7e8bd
26 files changed
+2225 -327

No files matched your search

@@ -1,16 +1,64 @@
package iris.platform
import android.content.Context
import android.content.SharedPreferences
import android.os.Build
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
import iris.data.SecureStore
import java.util.UUID
/**
* Android pairing storage. M1: SharedPreferences (dev). M5 moves the token
* to EncryptedSharedPreferences per docs/10 §10.2.
* Android pairing storage. M7: EncryptedSharedPreferences (MasterKey
* AES256_GCM) per docs/09 §9.7. The M1/M5 plain "iris" SharedPreferences
* values are migrated on first run (read old key, write encrypted, delete
* old key) so an upgrade never loses the pairing.
*/
class AndroidSecureStore(context: Context) : SecureStore {
private val prefs = context.applicationContext.getSharedPreferences("iris", Context.MODE_PRIVATE)
private val appContext = context.applicationContext
private val plainPrefs = appContext.getSharedPreferences(PLAIN_PREFS_NAME, Context.MODE_PRIVATE)
private val prefs: SharedPreferences = EncryptedSharedPreferences.create(
appContext,
SECURE_PREFS_NAME,
MasterKey.Builder(appContext)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build(),
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
)
init {
migratePlainValues()
}
/** One-time migration of the M1/M5 plain values into the encrypted store. */
private fun migratePlainValues() {
val editor = prefs.edit()
var migrated = false
for (key in listOf(KEY_URL, KEY_TOKEN, KEY_DEVICE_ID, KEY_FCM_TOKEN, KEY_NTFY_TOPIC, KEY_NTFY_SERVER)) {
val old = plainPrefs.getString(key, null)
if (old != null && !prefs.contains(key)) {
editor.putString(key, old)
migrated = true
}
}
val oldCursor = plainPrefs.getLong(KEY_SYNC_CURSOR, 0L)
if (oldCursor != 0L && !prefs.contains(KEY_SYNC_CURSOR)) {
editor.putLong(KEY_SYNC_CURSOR, oldCursor)
migrated = true
}
if (migrated) editor.apply()
// The plain store must not keep a copy of any value.
plainPrefs.edit()
.remove(KEY_URL)
.remove(KEY_TOKEN)
.remove(KEY_DEVICE_ID)
.remove(KEY_SYNC_CURSOR)
.remove(KEY_FCM_TOKEN)
.remove(KEY_NTFY_TOPIC)
.remove(KEY_NTFY_SERVER)
.apply()
}
override var serverUrl: String
get() = prefs.getString(KEY_URL, "").orEmpty()
@@ -59,6 +107,8 @@ class AndroidSecureStore(context: Context) : SecureStore {
}
private companion object {
const val PLAIN_PREFS_NAME = "iris"
const val SECURE_PREFS_NAME = "iris_secure"
const val KEY_URL = "server_url"
const val KEY_TOKEN = "token"
const val KEY_DEVICE_ID = "device_id"