diff --git a/app/src/main/java/network/columba/app/service/MessageCollector.kt b/app/src/main/java/network/columba/app/service/MessageCollector.kt index 038467a68..8b1aac078 100644 --- a/app/src/main/java/network/columba/app/service/MessageCollector.kt +++ b/app/src/main/java/network/columba/app/service/MessageCollector.kt @@ -230,6 +230,12 @@ class MessageCollector deliveryMethod = receivedMessage.deliveryMethod, // Local reception time for sort ordering receivedAt = now, + // LXMF signature-verification state from the backend. + // false = sender identity was unknown to our RNS at + // receive time (potential forgery — see MessageEntity + // for full semantics); the UI renders an "unverified + // sender" warning on these. + signatureVerified = receivedMessage.signatureVerified, ) // Get peer name from cache, existing conversation, or use formatted hash diff --git a/app/src/main/java/network/columba/app/ui/components/UnverifiedSenderChip.kt b/app/src/main/java/network/columba/app/ui/components/UnverifiedSenderChip.kt new file mode 100644 index 000000000..cb31e3603 --- /dev/null +++ b/app/src/main/java/network/columba/app/ui/components/UnverifiedSenderChip.kt @@ -0,0 +1,62 @@ +package network.columba.app.ui.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Warning +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp + +/** + * Inline warning banner for messages whose LXMF signature could not be verified + * against a known sender identity (`UnverifiedReason.SOURCE_UNKNOWN`). Mirrors + * Sideband's "this message is likely to be fake" banner; rendered above a + * message bubble when `MessageUi.signatureVerified == false`. + * + * Threat model: the LXMF wire layer encrypts to the recipient's public key, so + * decryption alone does NOT prove the sender's identity — the signature does. + * When we don't yet hold the sender's identity (their announce hasn't reached + * us), the signature can't be checked, and the message could be a legitimate + * first-contact OR a forgery from anyone who generated a fresh identity hash. + * Rendered with `errorContainer` for the same warning affordance the app uses + * for failed states. + */ +@Composable +fun UnverifiedSenderChip(modifier: Modifier = Modifier) { + Surface( + modifier = modifier, + color = MaterialTheme.colorScheme.errorContainer, + shape = RoundedCornerShape(8.dp), + ) { + Row( + modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + Icon( + imageVector = Icons.Default.Warning, + // Decorative — the adjacent Text already conveys the meaning, so + // a contentDescription here would double-read in TalkBack. + contentDescription = null, + tint = MaterialTheme.colorScheme.onErrorContainer, + modifier = Modifier.size(14.dp), + ) + Text( + text = "Unverified sender — may be forged", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onErrorContainer, + fontWeight = FontWeight.Medium, + ) + } + } +} diff --git a/app/src/main/java/network/columba/app/ui/model/MessageMapper.kt b/app/src/main/java/network/columba/app/ui/model/MessageMapper.kt index b0c74d7ac..eb0883c47 100644 --- a/app/src/main/java/network/columba/app/ui/model/MessageMapper.kt +++ b/app/src/main/java/network/columba/app/ui/model/MessageMapper.kt @@ -79,6 +79,7 @@ fun Message.toMessageUi(): MessageUi { receivedSnr = receivedSnr, receivedAt = receivedAt, sentInterface = sentInterface, + signatureVerified = signatureVerified, ) } diff --git a/app/src/main/java/network/columba/app/ui/model/MessageUi.kt b/app/src/main/java/network/columba/app/ui/model/MessageUi.kt index 58158f12b..f138743de 100644 --- a/app/src/main/java/network/columba/app/ui/model/MessageUi.kt +++ b/app/src/main/java/network/columba/app/ui/model/MessageUi.kt @@ -129,6 +129,19 @@ data class MessageUi( * Null for received messages or messages sent before this feature was added. */ val sentInterface: String? = null, + /** + * Whether the LXMF signature was verified against the sender's known + * identity at receive time. + * - true = signature checked against a known source identity, valid. + * - false = the sender's signature could not be verified — could be a + * legitimate first-contact or a forgery; the UI MUST warn. + * Kotlin backend: SOURCE_UNKNOWN only (LXMF-kt drops + * SIGNATURE_INVALID at the router). Python backend: may also be + * a failed signature check. + * - null = sent messages (signed locally) or legacy rows; treated as + * "no warning" to preserve historical display. + */ + val signatureVerified: Boolean? = null, ) { /** * Whether this message should be displayed as a standalone media item without a bubble. diff --git a/app/src/main/java/network/columba/app/ui/screens/MessageDetailScreen.kt b/app/src/main/java/network/columba/app/ui/screens/MessageDetailScreen.kt index cd35850f5..8b462a99e 100644 --- a/app/src/main/java/network/columba/app/ui/screens/MessageDetailScreen.kt +++ b/app/src/main/java/network/columba/app/ui/screens/MessageDetailScreen.kt @@ -22,6 +22,8 @@ import androidx.compose.material.icons.filled.Error import androidx.compose.material.icons.filled.HourglassEmpty import androidx.compose.material.icons.filled.Hub import androidx.compose.material.icons.filled.Link +import androidx.compose.material.icons.filled.VerifiedUser +import androidx.compose.material.icons.filled.Warning import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.ExperimentalMaterial3Api @@ -194,6 +196,34 @@ fun MessageDetailScreen( } else { // Received message info: delivery method, hop count, receiving interface. + // Signature verification card. Only emitted for received + // messages where the flag is non-null. Null (sent messages, + // legacy rows) renders no card — we don't claim a + // verification state either way for those, matching the + // bubble behavior in MessagingScreen. + msg.signatureVerified?.let { verified -> + if (verified) { + MessageInfoCard( + icon = Icons.Default.VerifiedUser, + title = "Signature", + content = "Verified", + subtitle = "The sender's identity was confirmed against their announce.", + ) + } else { + MessageInfoCard( + icon = Icons.Default.Warning, + iconTint = MaterialTheme.colorScheme.error, + title = "Signature", + content = "Unverified", + subtitle = "The sender's identity was unknown to your node when this " + + "message arrived. It could be legitimate (their announce hasn't " + + "reached you yet) or a forgery from anyone who knows your address. " + + "Treat with caution.", + contentColor = MaterialTheme.colorScheme.error, + ) + } + } + // Delivery method card. Especially important for propagation-fetched // messages — hop count / interface / signal metrics are all null for // those, so without this card the detail screen renders empty below diff --git a/app/src/main/java/network/columba/app/ui/screens/MessagingScreen.kt b/app/src/main/java/network/columba/app/ui/screens/MessagingScreen.kt index ba82344ad..4eb355b34 100644 --- a/app/src/main/java/network/columba/app/ui/screens/MessagingScreen.kt +++ b/app/src/main/java/network/columba/app/ui/screens/MessagingScreen.kt @@ -177,6 +177,7 @@ import network.columba.app.ui.components.SelectableTextDialog import network.columba.app.ui.components.StarToggleButton import network.columba.app.ui.components.SwipeableMessageBubble import network.columba.app.ui.components.SyncStatusBottomSheet +import network.columba.app.ui.components.UnverifiedSenderChip import network.columba.app.ui.components.simpleVerticalScrollbar import network.columba.app.ui.model.CodecProfile import network.columba.app.ui.model.LocationSharingState @@ -1831,6 +1832,17 @@ fun MessageBubble( modifier = Modifier.fillMaxWidth(), horizontalAlignment = if (isFromMe) Alignment.End else Alignment.Start, ) { + // Unverified-sender warning — mirrors Sideband's "this message is + // likely to be fake" banner. Rendered above every bubble type (text, + // media-only, attachment, pending-file notification) because a forged + // message could carry any payload shape. Trigger is exactly + // `signatureVerified == false` (an unverified sender); null (sent, + // legacy) and true both skip the chip. See + // MessageEntity.signatureVerified for the full state table. + if (message.signatureVerified == false) { + UnverifiedSenderChip(modifier = Modifier.padding(bottom = 4.dp)) + } + // Handle pending file notifications (system messages for files arriving via relay) if (message.isPendingFileNotification) { if (message.isSuperseded) { diff --git a/data/src/main/java/network/columba/app/data/db/ColumbaDatabase.kt b/data/src/main/java/network/columba/app/data/db/ColumbaDatabase.kt index ba394bab1..b7a813468 100644 --- a/data/src/main/java/network/columba/app/data/db/ColumbaDatabase.kt +++ b/data/src/main/java/network/columba/app/data/db/ColumbaDatabase.kt @@ -51,7 +51,7 @@ import network.columba.app.data.db.entity.RmspServerEntity BlockedPeerEntity::class, InterfaceFirstSeenEntity::class, ], - version = 2, + version = 3, exportSchema = false, ) abstract class ColumbaDatabase : RoomDatabase() { @@ -108,6 +108,23 @@ abstract class ColumbaDatabase : RoomDatabase() { } } + /** + * v2 → v3: add nullable `messages.signatureVerified INTEGER` column. + * + * Surfaces LXMF signature-verification state per received message so + * the UI can warn on unverified senders. Pure additive `ALTER` — no + * data transform. Existing rows backfill to + * NULL, which the UI treats as "no warning": showing every legacy + * message as unverified would be inaccurate (most are from peers we + * already had on file) and alarming. + */ + val MIGRATION_2_3: Migration = + object : Migration(2, 3) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE messages ADD COLUMN signatureVerified INTEGER") + } + } + /** * Extract the `fields[16].reactions` blob out of a legacy * `fieldsJson`, returning `(newFieldsJson, reactionsJson)`. diff --git a/data/src/main/java/network/columba/app/data/db/entity/MessageEntity.kt b/data/src/main/java/network/columba/app/data/db/entity/MessageEntity.kt index 0c98eab83..acf057876 100644 --- a/data/src/main/java/network/columba/app/data/db/entity/MessageEntity.kt +++ b/data/src/main/java/network/columba/app/data/db/entity/MessageEntity.kt @@ -71,4 +71,17 @@ data class MessageEntity( val receivedAt: Long? = null, // Interface name through which message was sent (null for received messages or pre-feature messages) val sentInterface: String? = null, + // For received messages: whether the LXMF signature was verified against + // the sender's known identity. + // true = signature checked against a known source identity, valid. + // false = the sender's signature could not be verified — could be a + // forgery; the UI must warn. Kotlin backend: always + // SOURCE_UNKNOWN (we don't hold the sender's identity yet; + // LXMF-kt drops SIGNATURE_INVALID at the router). Python backend: + // may also be a failed signature check (python LXMF delivers + // those to the app). + // null = sent by us (signed locally — implicitly authentic) or legacy + // rows from before this column (migration backfills null). Treat + // as "no warning" to preserve historical display. + val signatureVerified: Boolean? = null, ) diff --git a/data/src/main/java/network/columba/app/data/di/DatabaseModule.kt b/data/src/main/java/network/columba/app/data/di/DatabaseModule.kt index dca4d151b..7b2752ecd 100644 --- a/data/src/main/java/network/columba/app/data/di/DatabaseModule.kt +++ b/data/src/main/java/network/columba/app/data/di/DatabaseModule.kt @@ -109,7 +109,7 @@ object DatabaseModule { context, ColumbaDatabase::class.java, DATABASE_NAME, - ).addMigrations(ColumbaDatabase.MIGRATION_1_2) + ).addMigrations(ColumbaDatabase.MIGRATION_1_2, ColumbaDatabase.MIGRATION_2_3) .fallbackToDestructiveMigration() .fallbackToDestructiveMigrationOnDowngrade() .enableMultiInstanceInvalidation() diff --git a/data/src/main/java/network/columba/app/data/repository/ConversationRepository.kt b/data/src/main/java/network/columba/app/data/repository/ConversationRepository.kt index 94a5e4de5..2879fe161 100644 --- a/data/src/main/java/network/columba/app/data/repository/ConversationRepository.kt +++ b/data/src/main/java/network/columba/app/data/repository/ConversationRepository.kt @@ -62,6 +62,11 @@ data class Message( val receivedAt: Long? = null, // Interface name through which message was sent (null for received messages or pre-feature messages) val sentInterface: String? = null, + // For received messages: whether the LXMF signature was verified against + // the sender's known identity. false = potential forgery (sender identity + // unknown to us at receive time); null = sent by us, legacy rows, or the + // Python backend. See MessageEntity.signatureVerified for full semantics. + val signatureVerified: Boolean? = null, // DB-local per-target-message reactions aggregation. Flat shape: // {"👍": ["sender_hex_1", ...], "❤️": [...]} // Never appears on the wire — the LXMF reaction wire format is @@ -313,29 +318,12 @@ class ConversationRepository val processedFieldsJson = extractLargeAttachments(message.id, message.fieldsJson) val messageEntity = - MessageEntity( - id = message.id, + buildMessageEntity( + message = message, conversationHash = peerHash, identityHash = identityHash, - content = sanitizedContent, // Store SANITIZED content - timestamp = message.timestamp, - isFromMe = message.isFromMe, - status = message.status, - isRead = message.isFromMe, // Our own messages are always "read" - fieldsJson = processedFieldsJson, // LXMF fields with large attachments extracted - deliveryMethod = message.deliveryMethod, - errorMessage = message.errorMessage, - replyToMessageId = message.replyToMessageId, // Reply reference - // Received-side routing / signal metadata. Previously these - // were dropped here, which meant the message-details screen - // never rendered RSSI/SNR/hopcount/receiving-interface even - // when the upstream producer populated them. - receivedHopCount = message.receivedHopCount, - receivedInterface = message.receivedInterface, - receivedRssi = message.receivedRssi, - receivedSnr = message.receivedSnr, - receivedAt = message.receivedAt, - sentInterface = message.sentInterface, + sanitizedContent = sanitizedContent, + processedFieldsJson = processedFieldsJson, ) messageDao.insertMessage(messageEntity) @@ -605,9 +593,46 @@ class ConversationRepository receivedInterface = receivedInterface, receivedAt = receivedAt, sentInterface = sentInterface, + signatureVerified = signatureVerified, reactionsJson = reactionsJson, ) + /** + * Build a [MessageEntity] for persistence. Extracted from [saveMessage] + * (which is at the detekt LongMethod budget) — the caller supplies the + * resolved conversation/identity hashes plus the already-sanitized + * content and attachment-extracted fieldsJson. Received-side routing / + * signal metadata and `signatureVerified` are carried straight through + * so the message-details screen and unverified-sender warning render. + */ + private fun buildMessageEntity( + message: Message, + conversationHash: String, + identityHash: String, + sanitizedContent: String, + processedFieldsJson: String?, + ) = MessageEntity( + id = message.id, + conversationHash = conversationHash, + identityHash = identityHash, + content = sanitizedContent, + timestamp = message.timestamp, + isFromMe = message.isFromMe, + status = message.status, + isRead = message.isFromMe, // Our own messages are always "read" + fieldsJson = processedFieldsJson, + deliveryMethod = message.deliveryMethod, + errorMessage = message.errorMessage, + replyToMessageId = message.replyToMessageId, + receivedHopCount = message.receivedHopCount, + receivedInterface = message.receivedInterface, + receivedRssi = message.receivedRssi, + receivedSnr = message.receivedSnr, + receivedAt = message.receivedAt, + sentInterface = message.sentInterface, + signatureVerified = message.signatureVerified, + ) + /** * Update the sent interface name for a message (active identity scoped). */ diff --git a/data/src/test/java/network/columba/app/data/db/Migration2To3Test.kt b/data/src/test/java/network/columba/app/data/db/Migration2To3Test.kt new file mode 100644 index 000000000..16db28115 --- /dev/null +++ b/data/src/test/java/network/columba/app/data/db/Migration2To3Test.kt @@ -0,0 +1,102 @@ +package network.columba.app.data.db + +import android.app.Application +import android.content.Context +import androidx.sqlite.db.SupportSQLiteDatabase +import androidx.sqlite.db.SupportSQLiteOpenHelper +import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory +import androidx.test.core.app.ApplicationProvider +import org.junit.After +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +/** + * Round-trip test for [ColumbaDatabase.MIGRATION_2_3] — the v2 → v3 step that + * adds the nullable `messages.signatureVerified INTEGER` column. + * + * Exercises the real production migration: stands up a minimal v2 `messages` + * table, seeds a pre-existing row, runs `MIGRATION_2_3.migrate(db)`, and asserts + * 1. the column did not exist before the migration, + * 2. it exists afterward, and + * 3. pre-existing rows backfill to NULL (the "no warning" state the UI relies + * on for legacy messages — not `false`, which would falsely flag every old + * message as a potential forgery). + * + * Room's full schema-match validation (column type/affinity vs the + * `MessageEntity` definition) is covered separately by the current-schema build + * in [RoomUpgradeValidationTest]; this test pins the migration SQL itself. + */ +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [34], application = Application::class) +class Migration2To3Test { + private lateinit var db: SupportSQLiteDatabase + + @Before + fun setup() { + val context = ApplicationProvider.getApplicationContext() + // In-memory (name = null) v2 `messages` table. Only the columns the + // migration cares about are needed — the ALTER adds its column + // regardless of the surrounding schema. + val config = + SupportSQLiteOpenHelper.Configuration + .builder(context) + .name(null) + .callback( + object : SupportSQLiteOpenHelper.Callback(2) { + override fun onCreate(db: SupportSQLiteDatabase) { + db.execSQL( + "CREATE TABLE messages (" + + "id TEXT NOT NULL, " + + "identityHash TEXT NOT NULL, " + + "content TEXT, " + + "PRIMARY KEY(id, identityHash))", + ) + } + + override fun onUpgrade( + db: SupportSQLiteDatabase, + oldVersion: Int, + newVersion: Int, + ) = Unit + }, + ).build() + db = FrameworkSQLiteOpenHelperFactory().create(config).writableDatabase + } + + @After + fun teardown() { + db.close() + } + + private fun columnExists(column: String): Boolean = + db.query("PRAGMA table_info(messages)").use { cursor -> + val nameIdx = cursor.getColumnIndexOrThrow("name") + generateSequence { if (cursor.moveToNext()) cursor.getString(nameIdx) else null } + .any { it == column } + } + + @Test + fun `adds nullable signatureVerified column and backfills existing rows to null`() { + db.execSQL( + "INSERT INTO messages (id, identityHash, content) VALUES ('m1', 'id1', 'legacy')", + ) + assertFalse("column must not exist before the migration", columnExists("signatureVerified")) + + ColumbaDatabase.MIGRATION_2_3.migrate(db) + + assertTrue("column must exist after the migration", columnExists("signatureVerified")) + + db.query("SELECT signatureVerified FROM messages WHERE id = 'm1'").use { cursor -> + assertTrue(cursor.moveToFirst()) + assertTrue( + "pre-existing rows must backfill to NULL, not false", + cursor.isNull(cursor.getColumnIndexOrThrow("signatureVerified")), + ) + } + } +} diff --git a/rns-api/src/main/java/network/columba/app/rns/api/model/ReceivedMessage.kt b/rns-api/src/main/java/network/columba/app/rns/api/model/ReceivedMessage.kt index a77b9f296..c6588bad1 100644 --- a/rns-api/src/main/java/network/columba/app/rns/api/model/ReceivedMessage.kt +++ b/rns-api/src/main/java/network/columba/app/rns/api/model/ReceivedMessage.kt @@ -33,4 +33,17 @@ data class ReceivedMessage( // Mirrors the string vocabulary already used for outbound on // `MessageEntity.deliveryMethod` and `MessageDetailScreen.getDeliveryMethodInfo`. val deliveryMethod: String? = null, + // Whether the LXMF signature was verified against the sender's known + // identity at receive time. Surfaces LXMF-kt's `LXMessage.signatureValidated` + // so the UI can warn on unverified senders. + // true = signature checked against a known source identity, valid. + // false = the sender's signature could not be verified — the message is + // forgeable/untrusted and the UI warns. On the Kotlin backend + // this is always SOURCE_UNKNOWN (we don't yet hold the sender's + // identity; LXMF-kt drops SIGNATURE_INVALID at the router). On + // the Python backend it may additionally be a failed signature + // check, since python LXMF's `lxmf_delivery` delivers those too. + // null = the backend couldn't determine it, or a pre-feature path. + // Treated as "no warning". + val signatureVerified: Boolean? = null, ) : Parcelable diff --git a/rns-backend-kt/src/main/kotlin/network/columba/app/rns/backend/kt/NativeRnsBackendImpl.kt b/rns-backend-kt/src/main/kotlin/network/columba/app/rns/backend/kt/NativeRnsBackendImpl.kt index da85e561a..990506899 100644 --- a/rns-backend-kt/src/main/kotlin/network/columba/app/rns/backend/kt/NativeRnsBackendImpl.kt +++ b/rns-backend-kt/src/main/kotlin/network/columba/app/rns/backend/kt/NativeRnsBackendImpl.kt @@ -423,7 +423,23 @@ class NativeRnsBackendImpl( displayName = config.displayName, ) + // Surface the LXMF signature-verification state to the UI. LXMF-kt's + // router has already dropped SIGNATURE_INVALID deliveries (tampered + // payloads claiming a known sender) before this callback fires, so an + // unverified message here is the SOURCE_UNKNOWN case: we don't yet + // hold the sender's identity, so the signature can't be checked — it + // could be a legitimate first-contact OR a forgery. We ingest it + // anyway (Sideband-style warn-and-ingest, so genuine first-contact + // isn't lost). `buildReceivedMessage` reads `message.signatureValidated` + // onto the ReceivedMessage so the UI can warn. router!!.registerDeliveryCallback { message -> + if (!message.signatureValidated) { + Log.w( + TAG, + "Unverified inbound message from ${message.sourceHash.toHex()} " + + "(${message.unverifiedReason}) — ingesting with warning per Sideband-style policy", + ) + } handleIncomingMessage(message) } @@ -1014,6 +1030,10 @@ class NativeRnsBackendImpl( receivedRssi = message.receivedRssi, receivedSnr = message.receivedSnr, deliveryMethod = nativeDeliveryMethodName(message.method), + // Read directly off the message — LXMF-kt sets `signatureValidated` + // false only for SOURCE_UNKNOWN here (SIGNATURE_INVALID is dropped + // at the router). Surfaced so the UI can warn on unverified senders. + signatureVerified = message.signatureValidated, ) } diff --git a/rns-backend-py/src/main/kotlin/network/columba/app/rns/backend/py/PythonEventBridge.kt b/rns-backend-py/src/main/kotlin/network/columba/app/rns/backend/py/PythonEventBridge.kt index 11d557a3b..447da3652 100644 --- a/rns-backend-py/src/main/kotlin/network/columba/app/rns/backend/py/PythonEventBridge.kt +++ b/rns-backend-py/src/main/kotlin/network/columba/app/rns/backend/py/PythonEventBridge.kt @@ -237,6 +237,18 @@ class PythonEventBridge { receivedRssi = payload.dictInt("rssi"), receivedSnr = payload.dictDouble("snr")?.toFloat(), deliveryMethod = lxmfMethodName(payload.dictInt("method")), + // LXMF signature-verification state, forwarded by + // `event_bridge.py` from python LXMF's `message.signature_validated`. + // false → the sender's signature could not be verified, so the + // UI warns. Note python LXMF's `lxmf_delivery` delivers BOTH + // SOURCE_UNKNOWN and SIGNATURE_INVALID to the callback (it never + // drops on signature), whereas the Kotlin backend's LXMF-kt + // router drops SIGNATURE_INVALID — so on this path a false may + // additionally mean "signature check failed", not only + // "unknown sender". Always present for delivered messages, so + // this is a definite true/false (never null) here, matching the + // Kotlin backend's `message.signatureValidated`. + signatureVerified = payload.dictBool("signature_validated"), ) // Side-channels always route — independent of the chat-emit diff --git a/rns-host/src/main/kotlin/network/columba/app/rns/host/di/ServiceDatabaseProvider.kt b/rns-host/src/main/kotlin/network/columba/app/rns/host/di/ServiceDatabaseProvider.kt index d7f8ccb14..28ac08d58 100644 --- a/rns-host/src/main/kotlin/network/columba/app/rns/host/di/ServiceDatabaseProvider.kt +++ b/rns-host/src/main/kotlin/network/columba/app/rns/host/di/ServiceDatabaseProvider.kt @@ -29,7 +29,7 @@ object ServiceDatabaseProvider { context.applicationContext, ColumbaDatabase::class.java, DatabaseModule.DATABASE_NAME, - ).addMigrations(ColumbaDatabase.MIGRATION_1_2) + ).addMigrations(ColumbaDatabase.MIGRATION_1_2, ColumbaDatabase.MIGRATION_2_3) .fallbackToDestructiveMigration() .fallbackToDestructiveMigrationOnDowngrade() .enableMultiInstanceInvalidation()