Merge pull request #2621 from vector-im/feature/bca/crypto_store_outbound

Persist outbound group session
This commit is contained in:
Benoit Marty 2021-01-14 10:44:08 +01:00 committed by GitHub
commit 1ffc3a4f53
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 348 additions and 58 deletions

View File

@ -7,6 +7,7 @@ Features ✨:
Improvements 🙌:
- Add System theme option and set as default (#904, #2387)
- Store megolm outbound session to improve send time of first message after app launch.
- Warn user when they are leaving a not public room (#1460)
- Option to disable emoji keyboard (#2563)

View File

@ -50,6 +50,8 @@ import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.MethodSorters
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import java.util.concurrent.CountDownLatch
@RunWith(AndroidJUnit4::class)
@ -296,4 +298,77 @@ class KeyShareTests : InstrumentedTest {
mTestHelper.signOutAndClose(aliceSession1)
mTestHelper.signOutAndClose(aliceSession2)
}
@Test
fun test_ImproperKeyShareBug() {
val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
mTestHelper.doSync<Unit> {
aliceSession.cryptoService().crossSigningService()
.initializeCrossSigning(UserPasswordAuth(
user = aliceSession.myUserId,
password = TestConstants.PASSWORD
), it)
}
// Create an encrypted room and send a couple of messages
val roomId = mTestHelper.doSync<String> {
aliceSession.createRoom(
CreateRoomParams().apply {
visibility = RoomDirectoryVisibility.PRIVATE
enableEncryption()
},
it
)
}
val roomAlicePov = aliceSession.getRoom(roomId)
assertNotNull(roomAlicePov)
Thread.sleep(1_000)
assertTrue(roomAlicePov?.isEncrypted() == true)
val secondEventId = mTestHelper.sendTextMessage(roomAlicePov!!, "Message", 3)[1].eventId
// Create bob session
val bobSession = mTestHelper.createAccount(TestConstants.USER_BOB, SessionTestParams(true))
mTestHelper.doSync<Unit> {
bobSession.cryptoService().crossSigningService()
.initializeCrossSigning(UserPasswordAuth(
user = bobSession.myUserId,
password = TestConstants.PASSWORD
), it)
}
// Let alice invite bob
mTestHelper.doSync<Unit> {
roomAlicePov.invite(bobSession.myUserId, null, it)
}
mTestHelper.doSync<Unit> {
bobSession.joinRoom(roomAlicePov.roomId, null, emptyList(), it)
}
// we want to discard alice outbound session
aliceSession.cryptoService().discardOutboundSession(roomAlicePov.roomId)
// and now resend a new message to reset index to 0
mTestHelper.sendTextMessage(roomAlicePov, "After", 1)
val roomRoomBobPov = aliceSession.getRoom(roomId)
val beforeJoin = roomRoomBobPov!!.getTimeLineEvent(secondEventId)
var dRes = tryOrNull { bobSession.cryptoService().decryptEvent(beforeJoin!!.root, "") }
assert(dRes == null)
// Try to re-ask the keys
bobSession.cryptoService().reRequestRoomKeyForEvent(beforeJoin!!.root)
Thread.sleep(3_000)
// With the bug the first session would have improperly reshare that key :/
dRes = tryOrNull { bobSession.cryptoService().decryptEvent(beforeJoin.root, "") }
Log.d("#TEST", "KS: sgould not decrypt that ${beforeJoin.root.getClearContent().toModel<MessageContent>()?.body}")
assert(dRes?.clearEvent == null)
}
}

View File

@ -32,6 +32,7 @@ import org.matrix.android.sdk.internal.crypto.crosssigning.toBase64NoPadding
import org.matrix.android.sdk.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey
import org.matrix.android.sdk.internal.crypto.model.rest.GossipingDefaultContent
import org.matrix.android.sdk.internal.crypto.model.rest.GossipingToDeviceObject
import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.di.SessionId
import org.matrix.android.sdk.internal.session.SessionScope
@ -206,34 +207,7 @@ internal class IncomingGossipingRequestManager @Inject constructor(
Timber.v("## CRYPTO | GOSSIP processIncomingRoomKeyRequest from $userId:$deviceId for $roomId / ${body.sessionId} id ${request.requestId}")
if (credentials.userId != userId) {
Timber.w("## CRYPTO | GOSSIP processReceivedGossipingRequests() : room key request from other user")
val senderKey = body.senderKey ?: return Unit
.also { Timber.w("missing senderKey") }
.also { cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) }
val sessionId = body.sessionId ?: return Unit
.also { Timber.w("missing sessionId") }
.also { cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) }
if (alg != MXCRYPTO_ALGORITHM_MEGOLM) {
return Unit
.also { Timber.w("Only megolm is accepted here") }
.also { cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) }
}
val roomEncryptor = roomEncryptorsStore.get(roomId) ?: return Unit
.also { Timber.w("no room Encryptor") }
.also { cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) }
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
val isSuccess = roomEncryptor.reshareKey(sessionId, userId, deviceId, senderKey)
if (isSuccess) {
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTED)
} else {
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.UNABLE_TO_PROCESS)
}
}
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.RE_REQUESTED)
handleKeyRequestFromOtherUser(body, request, alg, roomId, userId, deviceId)
return
}
// TODO: should we queue up requests we don't yet have keys for, in case they turn up later?
@ -291,6 +265,42 @@ internal class IncomingGossipingRequestManager @Inject constructor(
onRoomKeyRequest(request)
}
private fun handleKeyRequestFromOtherUser(body: RoomKeyRequestBody,
request: IncomingRoomKeyRequest,
alg: String,
roomId: String,
userId: String,
deviceId: String) {
Timber.w("## CRYPTO | GOSSIP processReceivedGossipingRequests() : room key request from other user")
val senderKey = body.senderKey ?: return Unit
.also { Timber.w("missing senderKey") }
.also { cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) }
val sessionId = body.sessionId ?: return Unit
.also { Timber.w("missing sessionId") }
.also { cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) }
if (alg != MXCRYPTO_ALGORITHM_MEGOLM) {
return Unit
.also { Timber.w("Only megolm is accepted here") }
.also { cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) }
}
val roomEncryptor = roomEncryptorsStore.get(roomId) ?: return Unit
.also { Timber.w("no room Encryptor") }
.also { cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED) }
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
val isSuccess = roomEncryptor.reshareKey(sessionId, userId, deviceId, senderKey)
if (isSuccess) {
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.ACCEPTED)
} else {
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.UNABLE_TO_PROCESS)
}
}
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.RE_REQUESTED)
}
private fun processIncomingSecretShareRequest(request: IncomingSecretShareRequest) {
val secretName = request.secretName ?: return Unit.also {
cryptoStore.updateGossipingRequestState(request, GossipingRequestState.REJECTED)

View File

@ -19,6 +19,8 @@ package org.matrix.android.sdk.internal.crypto
import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.util.JSON_DICT_PARAMETERIZED_TYPE
import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXOutboundSessionInfo
import org.matrix.android.sdk.internal.crypto.algorithms.megolm.SharedWithHelper
import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2
import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper
@ -46,7 +48,7 @@ internal class MXOlmDevice @Inject constructor(
*/
private val store: IMXCryptoStore,
private val inboundGroupSessionStore: InboundGroupSessionStore
) {
) {
/**
* @return the Curve25519 key for the account.
@ -63,11 +65,15 @@ internal class MXOlmDevice @Inject constructor(
// The OLM lib utility instance.
private var olmUtility: OlmUtility? = null
private data class GroupSessionCacheItem(
val groupId: String,
val groupSession: OlmOutboundGroupSession
)
// The outbound group session.
// They are not stored in 'store' to avoid to remember to which devices we sent the session key.
// Plus, in cryptography, it is good to refresh sessions from time to time.
// The key is the session id, the value the outbound group session.
private val outboundGroupSessionStore: MutableMap<String, OlmOutboundGroupSession> = HashMap()
// Caches active outbound session to avoid to sync with DB before read
// The key is the session id, the value the <roomID,outbound group session>.
private val outboundGroupSessionCache: MutableMap<String, GroupSessionCacheItem> = HashMap()
// Store a set of decrypted message indexes for each group session.
// This partially mitigates a replay attack where a MITM resends a group
@ -135,6 +141,10 @@ internal class MXOlmDevice @Inject constructor(
*/
fun release() {
olmUtility?.releaseUtility()
outboundGroupSessionCache.values.forEach {
it.groupSession.releaseSession()
}
outboundGroupSessionCache.clear()
}
/**
@ -406,11 +416,12 @@ internal class MXOlmDevice @Inject constructor(
*
* @return the session id for the outbound session.
*/
fun createOutboundGroupSession(): String? {
fun createOutboundGroupSessionForRoom(roomId: String): String? {
var session: OlmOutboundGroupSession? = null
try {
session = OlmOutboundGroupSession()
outboundGroupSessionStore[session.sessionIdentifier()] = session
outboundGroupSessionCache[session.sessionIdentifier()] = GroupSessionCacheItem(roomId, session)
store.storeCurrentOutboundGroupSessionForRoom(roomId, session)
return session.sessionIdentifier()
} catch (e: Exception) {
Timber.e(e, "createOutboundGroupSession")
@ -421,6 +432,39 @@ internal class MXOlmDevice @Inject constructor(
return null
}
fun storeOutboundGroupSessionForRoom(roomId: String, sessionId: String) {
outboundGroupSessionCache[sessionId]?.let {
store.storeCurrentOutboundGroupSessionForRoom(roomId, it.groupSession)
}
}
fun restoreOutboundGroupSessionForRoom(roomId: String): MXOutboundSessionInfo? {
val restoredOutboundGroupSession = store.getCurrentOutboundGroupSessionForRoom(roomId)
if (restoredOutboundGroupSession != null) {
val sessionId = restoredOutboundGroupSession.outboundGroupSession.sessionIdentifier()
// cache it
outboundGroupSessionCache[sessionId] = GroupSessionCacheItem(roomId, restoredOutboundGroupSession.outboundGroupSession)
return MXOutboundSessionInfo(
sessionId = sessionId,
sharedWithHelper = SharedWithHelper(roomId, sessionId, store),
restoredOutboundGroupSession.creationTime
)
}
return null
}
fun discardOutboundGroupSessionForRoom(roomId: String) {
val toDiscard = outboundGroupSessionCache.filter {
it.value.groupId == roomId
}
toDiscard.forEach { (sessionId, cacheItem) ->
cacheItem.groupSession.releaseSession()
outboundGroupSessionCache.remove(sessionId)
}
store.storeCurrentOutboundGroupSessionForRoom(roomId, null)
}
/**
* Get the current session key of an outbound group session.
*
@ -430,7 +474,7 @@ internal class MXOlmDevice @Inject constructor(
fun getSessionKey(sessionId: String): String? {
if (sessionId.isNotEmpty()) {
try {
return outboundGroupSessionStore[sessionId]!!.sessionKey()
return outboundGroupSessionCache[sessionId]!!.groupSession.sessionKey()
} catch (e: Exception) {
Timber.e(e, "## getSessionKey() : failed")
}
@ -446,7 +490,7 @@ internal class MXOlmDevice @Inject constructor(
*/
fun getMessageIndex(sessionId: String): Int {
return if (sessionId.isNotEmpty()) {
outboundGroupSessionStore[sessionId]!!.messageIndex()
outboundGroupSessionCache[sessionId]!!.groupSession.messageIndex()
} else 0
}
@ -460,7 +504,7 @@ internal class MXOlmDevice @Inject constructor(
fun encryptGroupMessage(sessionId: String, payloadString: String): String? {
if (sessionId.isNotEmpty() && payloadString.isNotEmpty()) {
try {
return outboundGroupSessionStore[sessionId]!!.encryptMessage(payloadString)
return outboundGroupSessionCache[sessionId]!!.groupSession.encryptMessage(payloadString)
} catch (e: Exception) {
Timber.e(e, "## encryptGroupMessage() : failed")
}
@ -747,7 +791,7 @@ internal class MXOlmDevice @Inject constructor(
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_SENDER_KEY, MXCryptoError.ERROR_MISSING_PROPERTY_REASON)
}
val session = inboundGroupSessionStore.getInboundGroupSession(sessionId, senderKey)
val session = inboundGroupSessionStore.getInboundGroupSession(sessionId, senderKey)
if (session != null) {
// Check that the room id matches the original one for the session. This stops

View File

@ -68,6 +68,10 @@ internal class MXMegolmEncryption(
// case outboundSession.shareOperation will be non-null.)
private var outboundSession: MXOutboundSessionInfo? = null
init {
// restore existing outbound session if any
outboundSession = olmDevice.restoreOutboundGroupSessionForRoom(roomId)
}
// Default rotation periods
// TODO: Make it configurable via parameters
// Session rotation periods
@ -86,6 +90,9 @@ internal class MXMegolmEncryption(
return encryptContent(outboundSession, eventType, eventContent)
.also {
notifyWithheldForSession(devices.withHeldDevices, outboundSession)
// annoyingly we have to serialize again the saved outbound session to store message index :/
// if not we would see duplicate message index errors
olmDevice.storeOutboundGroupSessionForRoom(roomId, outboundSession.sessionId)
}
}
@ -107,6 +114,7 @@ internal class MXMegolmEncryption(
override fun discardSessionKey() {
outboundSession = null
olmDevice.discardOutboundGroupSessionForRoom(roomId)
}
/**
@ -116,7 +124,7 @@ internal class MXMegolmEncryption(
*/
private fun prepareNewSessionInRoom(): MXOutboundSessionInfo {
Timber.v("## CRYPTO | prepareNewSessionInRoom() ")
val sessionId = olmDevice.createOutboundGroupSession()
val sessionId = olmDevice.createOutboundGroupSessionForRoom(roomId)
val keysClaimedMap = HashMap<String, String>()
keysClaimedMap["ed25519"] = olmDevice.deviceEd25519Key!!
@ -152,7 +160,7 @@ internal class MXMegolmEncryption(
val deviceIds = devicesInRoom.getUserDeviceIds(userId)
for (deviceId in deviceIds!!) {
val deviceInfo = devicesInRoom.getObject(userId, deviceId)
if (deviceInfo != null && !cryptoStore.wasSessionSharedWithUser(roomId, safeSession.sessionId, userId, deviceId).found) {
if (deviceInfo != null && !cryptoStore.getSharedSessionInfo(roomId, safeSession.sessionId, userId, deviceId).found) {
val devices = shareMap.getOrPut(userId) { ArrayList() }
devices.add(deviceInfo)
}
@ -401,11 +409,18 @@ internal class MXMegolmEncryption(
.also { Timber.w("## Crypto reshareKey: Device not found") }
// Get the chain index of the key we previously sent this device
val chainIndex = outboundSession?.sharedWithHelper?.wasSharedWith(userId, deviceId) ?: return false
val wasSessionSharedWithUser = cryptoStore.getSharedSessionInfo(roomId, sessionId, userId, deviceId)
if (!wasSessionSharedWithUser.found) {
// This session was never shared with this user
// Send a room key with held
notifyKeyWithHeld(listOf(UserDevice(userId, deviceId)), sessionId, senderKey, WithHeldCode.UNAUTHORISED)
Timber.w("## Crypto reshareKey: ERROR : Never shared megolm with this device")
return false
}
// if found chain index should not be null
val chainIndex = wasSessionSharedWithUser.chainIndex ?: return false
.also {
// Send a room key with held
notifyKeyWithHeld(listOf(UserDevice(userId, deviceId)), sessionId, senderKey, WithHeldCode.UNAUTHORISED)
Timber.w("## Crypto reshareKey: ERROR : Never share megolm with this device")
Timber.w("## Crypto reshareKey: Null chain index")
}
val devicesByUser = mapOf(userId to listOf(deviceInfo))

View File

@ -23,9 +23,9 @@ import timber.log.Timber
internal class MXOutboundSessionInfo(
// The id of the session
val sessionId: String,
val sharedWithHelper: SharedWithHelper) {
// When the session was created
private val creationTime = System.currentTimeMillis()
val sharedWithHelper: SharedWithHelper,
// When the session was created
private val creationTime: Long = System.currentTimeMillis()) {
// Number of times this session has been used
var useCount: Int = 0

View File

@ -28,10 +28,6 @@ internal class SharedWithHelper(
return cryptoStore.getSharedWithInfo(roomId, sessionId)
}
fun wasSharedWith(userId: String, deviceId: String): Int? {
return cryptoStore.wasSessionSharedWithUser(roomId, sessionId, userId, deviceId).chainIndex
}
fun markedSessionAsShared(userId: String, deviceId: String, chainIndex: Int) {
cryptoStore.markedSessionAsShared(roomId, sessionId, userId, deviceId, chainIndex)
}

View File

@ -0,0 +1,24 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto.model
import org.matrix.olm.OlmOutboundGroupSession
data class OutboundGroupSessionWrapper(
val outboundGroupSession: OlmOutboundGroupSession,
val creationTime: Long
)

View File

@ -33,11 +33,13 @@ import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2
import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper
import org.matrix.android.sdk.internal.crypto.model.OutboundGroupSessionWrapper
import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyWithHeldContent
import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody
import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity
import org.matrix.olm.OlmAccount
import org.matrix.olm.OlmOutboundGroupSession
/**
* the crypto data store
@ -293,6 +295,16 @@ internal interface IMXCryptoStore {
*/
fun getInboundGroupSession(sessionId: String, senderKey: String): OlmInboundGroupSessionWrapper2?
/**
* Get the current outbound group session for this encrypted room
*/
fun getCurrentOutboundGroupSessionForRoom(roomId: String): OutboundGroupSessionWrapper?
/**
* Get the current outbound group session for this encrypted room
*/
fun storeCurrentOutboundGroupSessionForRoom(roomId: String, outboundGroupSession: OlmOutboundGroupSession?)
/**
* Remove an inbound group session
*
@ -439,7 +451,15 @@ internal interface IMXCryptoStore {
fun getWithHeldMegolmSession(roomId: String, sessionId: String): RoomKeyWithHeldContent?
fun markedSessionAsShared(roomId: String?, sessionId: String, userId: String, deviceId: String, chainIndex: Int)
fun wasSessionSharedWithUser(roomId: String?, sessionId: String, userId: String, deviceId: String): SharedSessionResult
/**
* Query for information on this session sharing history.
* @return SharedSessionResult
* if found is true then this session was initialy shared with that user|device,
* in this case chainIndex is not nullindicates the ratchet position.
* In found is false, chainIndex is null
*/
fun getSharedSessionInfo(roomId: String?, sessionId: String, userId: String, deviceId: String): SharedSessionResult
data class SharedSessionResult(val found: Boolean, val chainIndex: Int?)
fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap<Int>

View File

@ -47,6 +47,7 @@ import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2
import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper
import org.matrix.android.sdk.internal.crypto.model.OutboundGroupSessionWrapper
import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyWithHeldContent
import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody
@ -73,6 +74,7 @@ import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSess
import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSessionEntityFields
import org.matrix.android.sdk.internal.crypto.store.db.model.OlmSessionEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.OlmSessionEntityFields
import org.matrix.android.sdk.internal.crypto.store.db.model.OutboundGroupSessionInfoEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingGossipingRequestEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingGossipingRequestEntityFields
import org.matrix.android.sdk.internal.crypto.store.db.model.SharedSessionEntity
@ -95,6 +97,7 @@ import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.olm.OlmAccount
import org.matrix.olm.OlmException
import org.matrix.olm.OlmOutboundGroupSession
import timber.log.Timber
import javax.inject.Inject
import kotlin.collections.set
@ -756,6 +759,42 @@ internal class RealmCryptoStore @Inject constructor(
return inboundGroupSessionToRelease[key]
}
override fun getCurrentOutboundGroupSessionForRoom(roomId: String): OutboundGroupSessionWrapper? {
return doWithRealm(realmConfiguration) { realm ->
realm.where<CryptoRoomEntity>()
.equalTo(CryptoRoomEntityFields.ROOM_ID, roomId)
.findFirst()?.outboundSessionInfo?.let { entity ->
entity.getOutboundGroupSession()?.let {
OutboundGroupSessionWrapper(
it,
entity.creationTime ?: 0
)
}
}
}
}
override fun storeCurrentOutboundGroupSessionForRoom(roomId: String, outboundGroupSession: OlmOutboundGroupSession?) {
// we can do this async, as it's just for restoring on next launch
// the olmdevice is caching the active instance
// this is called for each sent message (so not high frequency), thus we can use basic realm async without
// risk of reaching max async operation limit?
doRealmTransactionAsync(realmConfiguration) { realm ->
CryptoRoomEntity.getById(realm, roomId)?.let { entity ->
// we should delete existing outbound session info if any
entity.outboundSessionInfo?.deleteFromRealm()
if (outboundGroupSession != null) {
val info = realm.createObject(OutboundGroupSessionInfoEntity::class.java).apply {
creationTime = System.currentTimeMillis()
putOutboundGroupSession(outboundGroupSession)
}
entity.outboundSessionInfo = info
}
}
}
}
/**
* Note: the result will be only use to export all the keys and not to use the OlmInboundGroupSessionWrapper2,
* so there is no need to use or update `inboundGroupSessionToRelease` for native memory management
@ -1645,7 +1684,7 @@ internal class RealmCryptoStore @Inject constructor(
}
}
override fun wasSessionSharedWithUser(roomId: String?, sessionId: String, userId: String, deviceId: String): IMXCryptoStore.SharedSessionResult {
override fun getSharedSessionInfo(roomId: String?, sessionId: String, userId: String, deviceId: String): IMXCryptoStore.SharedSessionResult {
return doWithRealm(realmConfiguration) { realm ->
SharedSessionEntity.get(realm, roomId, sessionId, userId, deviceId)?.let {
IMXCryptoStore.SharedSessionResult(true, it.chainIndex)

View File

@ -44,6 +44,7 @@ import org.matrix.android.sdk.internal.di.SerializeNulls
import io.realm.DynamicRealm
import io.realm.RealmMigration
import io.realm.RealmObjectSchema
import org.matrix.android.sdk.internal.crypto.store.db.model.OutboundGroupSessionInfoEntityFields
import org.matrix.androidsdk.crypto.data.MXOlmInboundGroupSession2
import timber.log.Timber
import javax.inject.Inject
@ -55,7 +56,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
// 0, 1, 2: legacy Riot-Android
// 3: migrate to RiotX schema
// 4, 5, 6, 7, 8, 9: migrations from RiotX (which was previously 1, 2, 3, 4, 5, 6)
const val CRYPTO_STORE_SCHEMA_VERSION = 11L
const val CRYPTO_STORE_SCHEMA_VERSION = 12L
}
private fun RealmObjectSchema.addFieldIfNotExists(fieldName: String, fieldType: Class<*>): RealmObjectSchema {
@ -93,6 +94,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
if (oldVersion <= 8) migrateTo9(realm)
if (oldVersion <= 9) migrateTo10(realm)
if (oldVersion <= 10) migrateTo11(realm)
if (oldVersion <= 11) migrateTo12(realm)
}
private fun migrateTo1Legacy(realm: DynamicRealm) {
@ -483,4 +485,16 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
realm.schema.get("CryptoMetadataEntity")
?.addField(CryptoMetadataEntityFields.DEVICE_KEYS_SENT_TO_SERVER, Boolean::class.java)
}
// Version 12L added outbound group session persistence
private fun migrateTo12(realm: DynamicRealm) {
Timber.d("Step 11 -> 12")
val outboundEntitySchema = realm.schema.create("OutboundGroupSessionInfoEntity")
.addField(OutboundGroupSessionInfoEntityFields.SERIALIZED_OUTBOUND_SESSION_DATA, String::class.java)
.addField(OutboundGroupSessionInfoEntityFields.CREATION_TIME, Long::class.java)
.setNullable(OutboundGroupSessionInfoEntityFields.CREATION_TIME, true)
realm.schema.get("CryptoRoomEntity")
?.addRealmObjectField(CryptoRoomEntityFields.OUTBOUND_SESSION_INFO.`$`, outboundEntitySchema)
}
}

View File

@ -33,6 +33,7 @@ import org.matrix.android.sdk.internal.crypto.store.db.model.TrustLevelEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntity
import org.matrix.android.sdk.internal.crypto.store.db.model.WithHeldSessionEntity
import io.realm.annotations.RealmModule
import org.matrix.android.sdk.internal.crypto.store.db.model.OutboundGroupSessionInfoEntity
/**
* Realm module for Crypto store classes
@ -54,6 +55,7 @@ import io.realm.annotations.RealmModule
OutgoingGossipingRequestEntity::class,
MyDeviceLastSeenInfoEntity::class,
WithHeldSessionEntity::class,
SharedSessionEntity::class
SharedSessionEntity::class,
OutboundGroupSessionInfoEntity::class
])
internal class RealmCryptoStoreModule

View File

@ -23,7 +23,12 @@ internal open class CryptoRoomEntity(
@PrimaryKey var roomId: String? = null,
var algorithm: String? = null,
var shouldEncryptForInvitedMembers: Boolean? = null,
var blacklistUnverifiedDevices: Boolean = false)
var blacklistUnverifiedDevices: Boolean = false,
// Store the current outbound session for this room,
// to avoid re-create and re-share at each startup (if rotation not needed..)
// This is specific to megolm but not sure how to model it better
var outboundSessionInfo: OutboundGroupSessionInfoEntity? = null
)
: RealmObject() {
companion object

View File

@ -0,0 +1,44 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto.store.db.model
import io.realm.RealmObject
import org.matrix.android.sdk.internal.crypto.store.db.deserializeFromRealm
import org.matrix.android.sdk.internal.crypto.store.db.serializeForRealm
import org.matrix.olm.OlmOutboundGroupSession
import timber.log.Timber
internal open class OutboundGroupSessionInfoEntity(
var serializedOutboundSessionData: String? = null,
var creationTime: Long? = null
) : RealmObject() {
fun getOutboundGroupSession(): OlmOutboundGroupSession? {
return try {
deserializeFromRealm(serializedOutboundSessionData)
} catch (failure: Throwable) {
Timber.e(failure, "## getOutboundGroupSession() Deserialization failure")
return null
}
}
fun putOutboundGroupSession(olmOutboundGroupSession: OlmOutboundGroupSession?) {
serializedOutboundSessionData = serializeForRealm(olmOutboundGroupSession)
}
companion object
}

View File

@ -305,7 +305,8 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
) {
add(EventSharedAction.UseKeyBackup)
}
if (session.cryptoService().getCryptoDeviceInfo(session.myUserId).size > 1) {
if (session.cryptoService().getCryptoDeviceInfo(session.myUserId).size > 1
|| timelineEvent.senderInfo.userId != session.myUserId) {
add(EventSharedAction.ReRequestKey(timelineEvent.eventId))
}
}