Merge pull request #3079 from Dominaezzz/suspend_functions_9

Convert SharedSecretStorageService and AccountDataService to suspend functions
This commit is contained in:
Benoit Marty 2021-03-31 09:28:20 +02:00 committed by GitHub
commit 79bee63515
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 229 additions and 360 deletions

View file

@ -19,7 +19,6 @@ package org.matrix.android.sdk.internal.crypto.ssss
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import org.matrix.android.sdk.InstrumentedTest import org.matrix.android.sdk.InstrumentedTest
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.securestorage.EncryptedSecretContent import org.matrix.android.sdk.api.session.securestorage.EncryptedSecretContent
import org.matrix.android.sdk.api.session.securestorage.KeySigner import org.matrix.android.sdk.api.session.securestorage.KeySigner
@ -31,7 +30,6 @@ import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.common.CommonTestHelper import org.matrix.android.sdk.common.CommonTestHelper
import org.matrix.android.sdk.common.SessionTestParams import org.matrix.android.sdk.common.SessionTestParams
import org.matrix.android.sdk.common.TestConstants import org.matrix.android.sdk.common.TestConstants
import org.matrix.android.sdk.common.TestMatrixCallback
import org.matrix.android.sdk.internal.crypto.SSSS_ALGORITHM_AES_HMAC_SHA2 import org.matrix.android.sdk.internal.crypto.SSSS_ALGORITHM_AES_HMAC_SHA2
import org.matrix.android.sdk.internal.crypto.crosssigning.toBase64NoPadding import org.matrix.android.sdk.internal.crypto.crosssigning.toBase64NoPadding
import org.matrix.android.sdk.internal.crypto.secrets.DefaultSharedSecretStorageService import org.matrix.android.sdk.internal.crypto.secrets.DefaultSharedSecretStorageService
@ -40,7 +38,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.amshove.kluent.shouldBe
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull import org.junit.Assert.assertNull
@ -70,8 +67,8 @@ class QuadSTests : InstrumentedTest {
val TEST_KEY_ID = "my.test.Key" val TEST_KEY_ID = "my.test.Key"
mTestHelper.doSync<SsssKeyCreationInfo> { mTestHelper.runBlockingTest {
quadS.generateKey(TEST_KEY_ID, null, "Test Key", emptyKeySigner, it) quadS.generateKey(TEST_KEY_ID, null, "Test Key", emptyKeySigner)
} }
// Assert Account data is updated // Assert Account data is updated
@ -99,7 +96,9 @@ class QuadSTests : InstrumentedTest {
assertNull("Key was not generated from passphrase", parsed.passphrase) assertNull("Key was not generated from passphrase", parsed.passphrase)
// Set as default key // Set as default key
quadS.setDefaultKey(TEST_KEY_ID, object : MatrixCallback<Unit> {}) GlobalScope.launch {
quadS.setDefaultKey(TEST_KEY_ID)
}
var defaultKeyAccountData: UserAccountDataEvent? = null var defaultKeyAccountData: UserAccountDataEvent? = null
val defaultDataLock = CountDownLatch(1) val defaultDataLock = CountDownLatch(1)
@ -133,12 +132,11 @@ class QuadSTests : InstrumentedTest {
// Store a secret // Store a secret
val clearSecret = "42".toByteArray().toBase64NoPadding() val clearSecret = "42".toByteArray().toBase64NoPadding()
mTestHelper.doSync<Unit> { mTestHelper.runBlockingTest {
aliceSession.sharedSecretStorageService.storeSecret( aliceSession.sharedSecretStorageService.storeSecret(
"secret.of.life", "secret.of.life",
clearSecret, clearSecret,
listOf(SharedSecretStorageService.KeyRef(null, keySpec)), // default key listOf(SharedSecretStorageService.KeyRef(null, keySpec)) // default key
it
) )
} }
@ -155,12 +153,11 @@ class QuadSTests : InstrumentedTest {
// Try to decrypt?? // Try to decrypt??
val decryptedSecret = mTestHelper.doSync<String> { val decryptedSecret = mTestHelper.runBlockingTest {
aliceSession.sharedSecretStorageService.getSecret( aliceSession.sharedSecretStorageService.getSecret(
"secret.of.life", "secret.of.life",
null, // default key null, // default key
keySpec!!, keySpec!!
it
) )
} }
@ -176,13 +173,13 @@ class QuadSTests : InstrumentedTest {
val TEST_KEY_ID = "my.test.Key" val TEST_KEY_ID = "my.test.Key"
mTestHelper.doSync<SsssKeyCreationInfo> { mTestHelper.runBlockingTest {
quadS.generateKey(TEST_KEY_ID, null, "Test Key", emptyKeySigner, it) quadS.generateKey(TEST_KEY_ID, null, "Test Key", emptyKeySigner)
} }
// Test that we don't need to wait for an account data sync to access directly the keyid from DB // Test that we don't need to wait for an account data sync to access directly the keyid from DB
mTestHelper.doSync<Unit> { mTestHelper.runBlockingTest {
quadS.setDefaultKey(TEST_KEY_ID, it) quadS.setDefaultKey(TEST_KEY_ID)
} }
mTestHelper.signOutAndClose(aliceSession) mTestHelper.signOutAndClose(aliceSession)
@ -198,15 +195,14 @@ class QuadSTests : InstrumentedTest {
val mySecretText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit" val mySecretText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit"
mTestHelper.doSync<Unit> { mTestHelper.runBlockingTest {
aliceSession.sharedSecretStorageService.storeSecret( aliceSession.sharedSecretStorageService.storeSecret(
"my.secret", "my.secret",
mySecretText.toByteArray().toBase64NoPadding(), mySecretText.toByteArray().toBase64NoPadding(),
listOf( listOf(
SharedSecretStorageService.KeyRef(keyId1, RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey)), SharedSecretStorageService.KeyRef(keyId1, RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey)),
SharedSecretStorageService.KeyRef(keyId2, RawBytesKeySpec.fromRecoveryKey(key2Info.recoveryKey)) SharedSecretStorageService.KeyRef(keyId2, RawBytesKeySpec.fromRecoveryKey(key2Info.recoveryKey))
), )
it
) )
} }
@ -219,19 +215,17 @@ class QuadSTests : InstrumentedTest {
assertNotNull(encryptedContent?.get(keyId2)) assertNotNull(encryptedContent?.get(keyId2))
// Assert that can decrypt with both keys // Assert that can decrypt with both keys
mTestHelper.doSync<String> { mTestHelper.runBlockingTest {
aliceSession.sharedSecretStorageService.getSecret("my.secret", aliceSession.sharedSecretStorageService.getSecret("my.secret",
keyId1, keyId1,
RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey)!!, RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey)!!
it
) )
} }
mTestHelper.doSync<String> { mTestHelper.runBlockingTest {
aliceSession.sharedSecretStorageService.getSecret("my.secret", aliceSession.sharedSecretStorageService.getSecret("my.secret",
keyId2, keyId2,
RawBytesKeySpec.fromRecoveryKey(key2Info.recoveryKey)!!, RawBytesKeySpec.fromRecoveryKey(key2Info.recoveryKey)!!
it
) )
} }
@ -247,50 +241,34 @@ class QuadSTests : InstrumentedTest {
val mySecretText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit" val mySecretText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit"
mTestHelper.doSync<Unit> { mTestHelper.runBlockingTest {
aliceSession.sharedSecretStorageService.storeSecret( aliceSession.sharedSecretStorageService.storeSecret(
"my.secret", "my.secret",
mySecretText.toByteArray().toBase64NoPadding(), mySecretText.toByteArray().toBase64NoPadding(),
listOf(SharedSecretStorageService.KeyRef(keyId1, RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey))), listOf(SharedSecretStorageService.KeyRef(keyId1, RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey)))
it
) )
} }
val decryptCountDownLatch = CountDownLatch(1) mTestHelper.runBlockingTest {
var error = false
aliceSession.sharedSecretStorageService.getSecret("my.secret", aliceSession.sharedSecretStorageService.getSecret("my.secret",
keyId1, keyId1,
RawBytesKeySpec.fromPassphrase( RawBytesKeySpec.fromPassphrase(
"A bad passphrase", "A bad passphrase",
key1Info.content?.passphrase?.salt ?: "", key1Info.content?.passphrase?.salt ?: "",
key1Info.content?.passphrase?.iterations ?: 0, key1Info.content?.passphrase?.iterations ?: 0,
null), null)
object : MatrixCallback<String> {
override fun onSuccess(data: String) {
decryptCountDownLatch.countDown()
}
override fun onFailure(failure: Throwable) {
error = true
decryptCountDownLatch.countDown()
}
}
) )
}
mTestHelper.await(decryptCountDownLatch)
error shouldBe true
// Now try with correct key // Now try with correct key
mTestHelper.doSync<String> { mTestHelper.runBlockingTest {
aliceSession.sharedSecretStorageService.getSecret("my.secret", aliceSession.sharedSecretStorageService.getSecret("my.secret",
keyId1, keyId1,
RawBytesKeySpec.fromPassphrase( RawBytesKeySpec.fromPassphrase(
passphrase, passphrase,
key1Info.content?.passphrase?.salt ?: "", key1Info.content?.passphrase?.salt ?: "",
key1Info.content?.passphrase?.iterations ?: 0, key1Info.content?.passphrase?.iterations ?: 0,
null), null)
it
) )
} }
@ -321,15 +299,15 @@ class QuadSTests : InstrumentedTest {
private fun generatedSecret(session: Session, keyId: String, asDefault: Boolean = true): SsssKeyCreationInfo { private fun generatedSecret(session: Session, keyId: String, asDefault: Boolean = true): SsssKeyCreationInfo {
val quadS = session.sharedSecretStorageService val quadS = session.sharedSecretStorageService
val creationInfo = mTestHelper.doSync<SsssKeyCreationInfo> { val creationInfo = mTestHelper.runBlockingTest {
quadS.generateKey(keyId, null, keyId, emptyKeySigner, it) quadS.generateKey(keyId, null, keyId, emptyKeySigner)
} }
assertAccountData(session, "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$keyId") assertAccountData(session, "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$keyId")
if (asDefault) { if (asDefault) {
mTestHelper.doSync<Unit> { mTestHelper.runBlockingTest {
quadS.setDefaultKey(keyId, it) quadS.setDefaultKey(keyId)
} }
assertAccountData(session, DefaultSharedSecretStorageService.DEFAULT_KEY_ID) assertAccountData(session, DefaultSharedSecretStorageService.DEFAULT_KEY_ID)
} }
@ -340,21 +318,20 @@ class QuadSTests : InstrumentedTest {
private fun generatedSecretFromPassphrase(session: Session, passphrase: String, keyId: String, asDefault: Boolean = true): SsssKeyCreationInfo { private fun generatedSecretFromPassphrase(session: Session, passphrase: String, keyId: String, asDefault: Boolean = true): SsssKeyCreationInfo {
val quadS = session.sharedSecretStorageService val quadS = session.sharedSecretStorageService
val creationInfo = mTestHelper.doSync<SsssKeyCreationInfo> { val creationInfo = mTestHelper.runBlockingTest {
quadS.generateKeyWithPassphrase( quadS.generateKeyWithPassphrase(
keyId, keyId,
keyId, keyId,
passphrase, passphrase,
emptyKeySigner, emptyKeySigner,
null, null)
it)
} }
assertAccountData(session, "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$keyId") assertAccountData(session, "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$keyId")
if (asDefault) { if (asDefault) {
val setDefaultLatch = CountDownLatch(1) mTestHelper.runBlockingTest {
quadS.setDefaultKey(keyId, TestMatrixCallback(setDefaultLatch)) quadS.setDefaultKey(keyId)
mTestHelper.await(setDefaultLatch) }
assertAccountData(session, DefaultSharedSecretStorageService.DEFAULT_KEY_ID) assertAccountData(session, DefaultSharedSecretStorageService.DEFAULT_KEY_ID)
} }

View file

@ -17,9 +17,7 @@
package org.matrix.android.sdk.api.session.accountdata package org.matrix.android.sdk.api.session.accountdata
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.events.model.Content
import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.Optional
interface AccountDataService { interface AccountDataService {
@ -48,5 +46,5 @@ interface AccountDataService {
/** /**
* Update the account data with the provided type and the provided account data content * Update the account data with the provided type and the provided account data content
*/ */
fun updateAccountData(type: String, content: Content, callback: MatrixCallback<Unit>? = null): Cancelable suspend fun updateAccountData(type: String, content: Content)
} }

View file

@ -16,7 +16,6 @@
package org.matrix.android.sdk.api.session.securestorage package org.matrix.android.sdk.api.session.securestorage
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.listeners.ProgressListener import org.matrix.android.sdk.api.listeners.ProgressListener
import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
@ -43,13 +42,12 @@ interface SharedSecretStorageService {
* @param keyName a human readable name * @param keyName a human readable name
* @param keySigner Used to add a signature to the key (client should check key signature before storing secret) * @param keySigner Used to add a signature to the key (client should check key signature before storing secret)
* *
* @param callback Get key creation info * @return key creation info
*/ */
fun generateKey(keyId: String, suspend fun generateKey(keyId: String,
key: SsssKeySpec?, key: SsssKeySpec?,
keyName: String, keyName: String,
keySigner: KeySigner?, keySigner: KeySigner?): SsssKeyCreationInfo
callback: MatrixCallback<SsssKeyCreationInfo>)
/** /**
* Generates a SSSS key using the given passphrase. * Generates a SSSS key using the given passphrase.
@ -61,14 +59,13 @@ interface SharedSecretStorageService {
* @param keySigner Used to add a signature to the key (client should check key signature before retrieving secret) * @param keySigner Used to add a signature to the key (client should check key signature before retrieving secret)
* @param progressListener The derivation of the passphrase may take long depending on the device, use this to report progress * @param progressListener The derivation of the passphrase may take long depending on the device, use this to report progress
* *
* @param callback Get key creation info * @return key creation info
*/ */
fun generateKeyWithPassphrase(keyId: String, suspend fun generateKeyWithPassphrase(keyId: String,
keyName: String, keyName: String,
passphrase: String, passphrase: String,
keySigner: KeySigner, keySigner: KeySigner,
progressListener: ProgressListener?, progressListener: ProgressListener?): SsssKeyCreationInfo
callback: MatrixCallback<SsssKeyCreationInfo>)
fun getKey(keyId: String): KeyInfoResult fun getKey(keyId: String): KeyInfoResult
@ -80,7 +77,7 @@ interface SharedSecretStorageService {
*/ */
fun getDefaultKey(): KeyInfoResult fun getDefaultKey(): KeyInfoResult
fun setDefaultKey(keyId: String, callback: MatrixCallback<Unit>) suspend fun setDefaultKey(keyId: String)
/** /**
* Check whether we have a key with a given ID. * Check whether we have a key with a given ID.
@ -98,7 +95,7 @@ interface SharedSecretStorageService {
* @param secret The secret contents. * @param secret The secret contents.
* @param keys The list of (ID,privateKey) of the keys to use to encrypt the secret. * @param keys The list of (ID,privateKey) of the keys to use to encrypt the secret.
*/ */
fun storeSecret(name: String, secretBase64: String, keys: List<KeyRef>, callback: MatrixCallback<Unit>) suspend fun storeSecret(name: String, secretBase64: String, keys: List<KeyRef>)
/** /**
* Use this call to determine which SSSSKeySpec to use for requesting secret * Use this call to determine which SSSSKeySpec to use for requesting secret
@ -113,7 +110,7 @@ interface SharedSecretStorageService {
* @param secretKey the secret key to use (@see #RawBytesKeySpec) * @param secretKey the secret key to use (@see #RawBytesKeySpec)
* *
*/ */
fun getSecret(name: String, keyId: String?, secretKey: SsssKeySpec, callback: MatrixCallback<String>) suspend fun getSecret(name: String, keyId: String?, secretKey: SsssKeySpec): String
/** /**
* Return true if SSSS is configured * Return true if SSSS is configured

View file

@ -16,7 +16,6 @@
package org.matrix.android.sdk.internal.crypto.secrets package org.matrix.android.sdk.internal.crypto.secrets
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.listeners.ProgressListener import org.matrix.android.sdk.api.listeners.ProgressListener
import org.matrix.android.sdk.api.session.accountdata.AccountDataService import org.matrix.android.sdk.api.session.accountdata.AccountDataService
@ -43,10 +42,9 @@ import org.matrix.android.sdk.internal.crypto.keysbackup.util.computeRecoveryKey
import org.matrix.android.sdk.internal.crypto.tools.HkdfSha256 import org.matrix.android.sdk.internal.crypto.tools.HkdfSha256
import org.matrix.android.sdk.internal.crypto.tools.withOlmDecryption import org.matrix.android.sdk.internal.crypto.tools.withOlmDecryption
import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.extensions.foldToCallback
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.withContext
import org.matrix.olm.OlmPkMessage import org.matrix.olm.OlmPkMessage
import java.security.SecureRandom import java.security.SecureRandom
import javax.crypto.Cipher import javax.crypto.Cipher
@ -64,21 +62,15 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
private val cryptoCoroutineScope: CoroutineScope private val cryptoCoroutineScope: CoroutineScope
) : SharedSecretStorageService { ) : SharedSecretStorageService {
override fun generateKey(keyId: String, override suspend fun generateKey(keyId: String,
key: SsssKeySpec?, key: SsssKeySpec?,
keyName: String, keyName: String,
keySigner: KeySigner?, keySigner: KeySigner?): SsssKeyCreationInfo {
callback: MatrixCallback<SsssKeyCreationInfo>) { return withContext(cryptoCoroutineScope.coroutineContext + coroutineDispatchers.main) {
cryptoCoroutineScope.launch(coroutineDispatchers.main) { val bytes = (key as? RawBytesKeySpec)?.privateKey
val bytes = try {
(key as? RawBytesKeySpec)?.privateKey
?: ByteArray(32).also { ?: ByteArray(32).also {
SecureRandom().nextBytes(it) SecureRandom().nextBytes(it)
} }
} catch (failure: Throwable) {
callback.onFailure(failure)
return@launch
}
val storageKeyContent = SecretStorageKeyContent( val storageKeyContent = SecretStorageKeyContent(
name = keyName, name = keyName,
@ -92,34 +84,22 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
) )
} ?: storageKeyContent } ?: storageKeyContent
accountDataService.updateAccountData( accountDataService.updateAccountData("$KEY_ID_BASE.$keyId", signedContent.toContent())
"$KEY_ID_BASE.$keyId", SsssKeyCreationInfo(
signedContent.toContent(),
object : MatrixCallback<Unit> {
override fun onFailure(failure: Throwable) {
callback.onFailure(failure)
}
override fun onSuccess(data: Unit) {
callback.onSuccess(SsssKeyCreationInfo(
keyId = keyId, keyId = keyId,
content = storageKeyContent, content = storageKeyContent,
recoveryKey = computeRecoveryKey(bytes), recoveryKey = computeRecoveryKey(bytes),
keySpec = RawBytesKeySpec(bytes) keySpec = RawBytesKeySpec(bytes)
))
}
}
) )
} }
} }
override fun generateKeyWithPassphrase(keyId: String, override suspend fun generateKeyWithPassphrase(keyId: String,
keyName: String, keyName: String,
passphrase: String, passphrase: String,
keySigner: KeySigner, keySigner: KeySigner,
progressListener: ProgressListener?, progressListener: ProgressListener?): SsssKeyCreationInfo {
callback: MatrixCallback<SsssKeyCreationInfo>) { return withContext(cryptoCoroutineScope.coroutineContext + coroutineDispatchers.main) {
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
val privatePart = generatePrivateKeyWithPassword(passphrase, progressListener) val privatePart = generatePrivateKeyWithPassword(passphrase, progressListener)
val storageKeyContent = SecretStorageKeyContent( val storageKeyContent = SecretStorageKeyContent(
@ -135,21 +115,13 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
accountDataService.updateAccountData( accountDataService.updateAccountData(
"$KEY_ID_BASE.$keyId", "$KEY_ID_BASE.$keyId",
signedContent.toContent(), signedContent.toContent()
object : MatrixCallback<Unit> { )
override fun onFailure(failure: Throwable) { SsssKeyCreationInfo(
callback.onFailure(failure)
}
override fun onSuccess(data: Unit) {
callback.onSuccess(SsssKeyCreationInfo(
keyId = keyId, keyId = keyId,
content = storageKeyContent, content = storageKeyContent,
recoveryKey = computeRecoveryKey(privatePart.privateKey), recoveryKey = computeRecoveryKey(privatePart.privateKey),
keySpec = RawBytesKeySpec(privatePart.privateKey) keySpec = RawBytesKeySpec(privatePart.privateKey)
))
}
}
) )
} }
} }
@ -168,15 +140,12 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
} ?: KeyInfoResult.Error(SharedSecretStorageError.UnknownAlgorithm(keyId)) } ?: KeyInfoResult.Error(SharedSecretStorageError.UnknownAlgorithm(keyId))
} }
override fun setDefaultKey(keyId: String, callback: MatrixCallback<Unit>) { override suspend fun setDefaultKey(keyId: String) {
val existingKey = getKey(keyId) val existingKey = getKey(keyId)
if (existingKey is KeyInfoResult.Success) { if (existingKey is KeyInfoResult.Success) {
accountDataService.updateAccountData(DEFAULT_KEY_ID, accountDataService.updateAccountData(DEFAULT_KEY_ID, mapOf("key" to keyId))
mapOf("key" to keyId),
callback
)
} else { } else {
callback.onFailure(SharedSecretStorageError.UnknownKey(keyId)) throw SharedSecretStorageError.UnknownKey(keyId)
} }
} }
@ -188,10 +157,9 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
return getKey(keyId) return getKey(keyId)
} }
override fun storeSecret(name: String, secretBase64: String, keys: List<SharedSecretStorageService.KeyRef>, callback: MatrixCallback<Unit>) { override suspend fun storeSecret(name: String, secretBase64: String, keys: List<SharedSecretStorageService.KeyRef>) {
cryptoCoroutineScope.launch(coroutineDispatchers.main) { withContext(cryptoCoroutineScope.coroutineContext + coroutineDispatchers.main) {
val encryptedContents = HashMap<String, EncryptedSecretContent>() val encryptedContents = HashMap<String, EncryptedSecretContent>()
try {
keys.forEach { keys.forEach {
val keyId = it.keyId val keyId = it.keyId
// encrypt the content // encrypt the content
@ -203,27 +171,17 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
} }
} else { } else {
// Unknown algorithm // Unknown algorithm
callback.onFailure(SharedSecretStorageError.UnknownAlgorithm(key.keyInfo.content.algorithm ?: "")) throw SharedSecretStorageError.UnknownAlgorithm(key.keyInfo.content.algorithm ?: "")
return@launch
} }
} }
is KeyInfoResult.Error -> { is KeyInfoResult.Error -> throw key.error
callback.onFailure(key.error)
return@launch
}
} }
} }
accountDataService.updateAccountData( accountDataService.updateAccountData(
type = name, type = name,
content = mapOf( content = mapOf("encrypted" to encryptedContents)
"encrypted" to encryptedContents
),
callback = callback
) )
} catch (failure: Throwable) {
callback.onFailure(failure)
}
} }
} }
@ -344,33 +302,21 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
return results return results
} }
override fun getSecret(name: String, keyId: String?, secretKey: SsssKeySpec, callback: MatrixCallback<String>) { override suspend fun getSecret(name: String, keyId: String?, secretKey: SsssKeySpec): String {
val accountData = accountDataService.getAccountDataEvent(name) ?: return Unit.also { val accountData = accountDataService.getAccountDataEvent(name) ?: throw SharedSecretStorageError.UnknownSecret(name)
callback.onFailure(SharedSecretStorageError.UnknownSecret(name)) val encryptedContent = accountData.content[ENCRYPTED] as? Map<*, *> ?: throw SharedSecretStorageError.SecretNotEncrypted(name)
} val key = keyId?.let { getKey(it) } as? KeyInfoResult.Success ?: getDefaultKey() as? KeyInfoResult.Success
val encryptedContent = accountData.content[ENCRYPTED] as? Map<*, *> ?: return Unit.also { ?: throw SharedSecretStorageError.UnknownKey(name)
callback.onFailure(SharedSecretStorageError.SecretNotEncrypted(name))
}
val key = keyId?.let { getKey(it) } as? KeyInfoResult.Success ?: getDefaultKey() as? KeyInfoResult.Success ?: return Unit.also {
callback.onFailure(SharedSecretStorageError.UnknownKey(name))
}
val encryptedForKey = encryptedContent[key.keyInfo.id] ?: return Unit.also { val encryptedForKey = encryptedContent[key.keyInfo.id] ?: throw SharedSecretStorageError.SecretNotEncryptedWithKey(name, key.keyInfo.id)
callback.onFailure(SharedSecretStorageError.SecretNotEncryptedWithKey(name, key.keyInfo.id))
}
val secretContent = EncryptedSecretContent.fromJson(encryptedForKey) val secretContent = EncryptedSecretContent.fromJson(encryptedForKey)
?: return Unit.also { ?: throw SharedSecretStorageError.ParsingError
callback.onFailure(SharedSecretStorageError.ParsingError)
}
val algorithm = key.keyInfo.content val algorithm = key.keyInfo.content
if (SSSS_ALGORITHM_CURVE25519_AES_SHA2 == algorithm.algorithm) { if (SSSS_ALGORITHM_CURVE25519_AES_SHA2 == algorithm.algorithm) {
val keySpec = secretKey as? RawBytesKeySpec ?: return Unit.also { val keySpec = secretKey as? RawBytesKeySpec ?: throw SharedSecretStorageError.BadKeyFormat
callback.onFailure(SharedSecretStorageError.BadKeyFormat) return withContext(cryptoCoroutineScope.coroutineContext + coroutineDispatchers.main) {
}
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
runCatching {
// decrypt from recovery key // decrypt from recovery key
withOlmDecryption { olmPkDecryption -> withOlmDecryption { olmPkDecryption ->
olmPkDecryption.setPrivateKey(keySpec.privateKey) olmPkDecryption.setPrivateKey(keySpec.privateKey)
@ -382,19 +328,14 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
} }
) )
} }
}.foldToCallback(callback)
} }
} else if (SSSS_ALGORITHM_AES_HMAC_SHA2 == algorithm.algorithm) { } else if (SSSS_ALGORITHM_AES_HMAC_SHA2 == algorithm.algorithm) {
val keySpec = secretKey as? RawBytesKeySpec ?: return Unit.also { val keySpec = secretKey as? RawBytesKeySpec ?: throw SharedSecretStorageError.BadKeyFormat
callback.onFailure(SharedSecretStorageError.BadKeyFormat) return withContext(cryptoCoroutineScope.coroutineContext + coroutineDispatchers.main) {
}
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
runCatching {
decryptAesHmacSha2(keySpec, name, secretContent) decryptAesHmacSha2(keySpec, name, secretContent)
}.foldToCallback(callback)
} }
} else { } else {
callback.onFailure(SharedSecretStorageError.UnsupportedAlgorithm(algorithm.algorithm ?: "")) throw SharedSecretStorageError.UnsupportedAlgorithm(algorithm.algorithm ?: "")
} }
} }

View file

@ -18,16 +18,15 @@ package org.matrix.android.sdk.internal.session.user.accountdata
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.session.accountdata.AccountDataService import org.matrix.android.sdk.api.session.accountdata.AccountDataService
import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.events.model.Content
import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.session.sync.UserAccountDataSyncHandler import org.matrix.android.sdk.internal.session.sync.UserAccountDataSyncHandler
import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent
import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.task.configureWith import org.matrix.android.sdk.internal.task.configureWith
import org.matrix.android.sdk.internal.util.awaitCallback
import javax.inject.Inject import javax.inject.Inject
internal class DefaultAccountDataService @Inject constructor( internal class DefaultAccountDataService @Inject constructor(
@ -54,26 +53,18 @@ internal class DefaultAccountDataService @Inject constructor(
return accountDataDataSource.getLiveAccountDataEvents(types) return accountDataDataSource.getLiveAccountDataEvents(types)
} }
override fun updateAccountData(type: String, content: Content, callback: MatrixCallback<Unit>?): Cancelable { override suspend fun updateAccountData(type: String, content: Content) {
return updateUserAccountDataTask.configureWith(UpdateUserAccountDataTask.AnyParams( val params = UpdateUserAccountDataTask.AnyParams(type = type, any = content)
type = type, awaitCallback<Unit> { callback ->
any = content updateUserAccountDataTask.configureWith(params) {
)) { this.retryCount = 5 // TODO: Need to refactor retrying out into a helper method.
this.retryCount = 5 this.callback = callback
this.callback = object : MatrixCallback<Unit> { }
override fun onSuccess(data: Unit) { .executeBy(taskExecutor)
}
// TODO Move that to the task (but it created a circular dependencies...) // TODO Move that to the task (but it created a circular dependencies...)
monarchy.runTransactionSync { realm -> monarchy.runTransactionSync { realm ->
userAccountDataSyncHandler.handleGenericAccountData(realm, type, content) userAccountDataSyncHandler.handleGenericAccountData(realm, type, content)
} }
callback?.onSuccess(data)
}
override fun onFailure(failure: Throwable) {
callback?.onFailure(failure)
}
}
}
.executeBy(taskExecutor)
} }
} }

View file

@ -43,7 +43,6 @@ import org.matrix.android.sdk.api.session.securestorage.IntegrityResult
import org.matrix.android.sdk.api.session.securestorage.KeyInfoResult import org.matrix.android.sdk.api.session.securestorage.KeyInfoResult
import org.matrix.android.sdk.api.session.securestorage.RawBytesKeySpec import org.matrix.android.sdk.api.session.securestorage.RawBytesKeySpec
import org.matrix.android.sdk.internal.crypto.crosssigning.toBase64NoPadding import org.matrix.android.sdk.internal.crypto.crosssigning.toBase64NoPadding
import org.matrix.android.sdk.internal.util.awaitCallback
import org.matrix.android.sdk.rx.rx import org.matrix.android.sdk.rx.rx
import timber.log.Timber import timber.log.Timber
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
@ -220,13 +219,10 @@ class SharedSecureStorageViewModel @AssistedInject constructor(
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
args.requestedSecrets.forEach { args.requestedSecrets.forEach {
if (session.getAccountDataEvent(it) != null) { if (session.getAccountDataEvent(it) != null) {
val res = awaitCallback<String> { callback -> val res = session.sharedSecretStorageService.getSecret(
session.sharedSecretStorageService.getSecret(
name = it, name = it,
keyId = keyInfo.id, keyId = keyInfo.id,
secretKey = keySpec, secretKey = keySpec)
callback = callback)
}
decryptedSecretMap[it] = res decryptedSecretMap[it] = res
} else { } else {
Timber.w("## Cannot find secret $it in SSSS, skip") Timber.w("## Cannot find secret $it in SSSS, skip")
@ -292,13 +288,10 @@ class SharedSecureStorageViewModel @AssistedInject constructor(
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
args.requestedSecrets.forEach { args.requestedSecrets.forEach {
if (session.getAccountDataEvent(it) != null) { if (session.getAccountDataEvent(it) != null) {
val res = awaitCallback<String> { callback -> val res = session.sharedSecretStorageService.getSecret(
session.sharedSecretStorageService.getSecret(
name = it, name = it,
keyId = keyInfo.id, keyId = keyInfo.id,
secretKey = keySpec, secretKey = keySpec)
callback = callback)
}
decryptedSecretMap[it] = res decryptedSecretMap[it] = res
} else { } else {
Timber.w("## Cannot find secret $it in SSSS, skip") Timber.w("## Cannot find secret $it in SSSS, skip")

View file

@ -97,7 +97,6 @@ class BackupToQuadSMigrationTask @Inject constructor(
when { when {
params.passphrase?.isNotEmpty() == true -> { params.passphrase?.isNotEmpty() == true -> {
reportProgress(params, R.string.bootstrap_progress_generating_ssss) reportProgress(params, R.string.bootstrap_progress_generating_ssss)
awaitCallback {
quadS.generateKeyWithPassphrase( quadS.generateKeyWithPassphrase(
UUID.randomUUID().toString(), UUID.randomUUID().toString(),
"ssss_key", "ssss_key",
@ -112,23 +111,18 @@ class BackupToQuadSMigrationTask @Inject constructor(
"$progress/$total") "$progress/$total")
)) ))
} }
},
it
)
} }
)
} }
params.recoveryKey != null -> { params.recoveryKey != null -> {
reportProgress(params, R.string.bootstrap_progress_generating_ssss_recovery) reportProgress(params, R.string.bootstrap_progress_generating_ssss_recovery)
awaitCallback {
quadS.generateKey( quadS.generateKey(
UUID.randomUUID().toString(), UUID.randomUUID().toString(),
extractCurveKeyFromRecoveryKey(params.recoveryKey)?.let { RawBytesKeySpec(it) }, extractCurveKeyFromRecoveryKey(params.recoveryKey)?.let { RawBytesKeySpec(it) },
"ssss_key", "ssss_key",
EmptyKeySigner(), EmptyKeySigner()
it
) )
} }
}
else -> { else -> {
return Result.IllegalParams return Result.IllegalParams
} }
@ -137,14 +131,11 @@ class BackupToQuadSMigrationTask @Inject constructor(
// Ok, so now we have migrated the old keybackup secret as the quadS key // Ok, so now we have migrated the old keybackup secret as the quadS key
// Now we need to store the keybackup key in SSSS in a compatible way // Now we need to store the keybackup key in SSSS in a compatible way
reportProgress(params, R.string.bootstrap_progress_storing_in_sss) reportProgress(params, R.string.bootstrap_progress_storing_in_sss)
awaitCallback<Unit> {
quadS.storeSecret( quadS.storeSecret(
KEYBACKUP_SECRET_SSSS_NAME, KEYBACKUP_SECRET_SSSS_NAME,
curveKey.toBase64NoPadding(), curveKey.toBase64NoPadding(),
listOf(SharedSecretStorageService.KeyRef(info.keyId, info.keySpec)), listOf(SharedSecretStorageService.KeyRef(info.keyId, info.keySpec))
it
) )
}
// save for gossiping // save for gossiping
keysBackupService.saveBackupRecoveryKey(recoveryKey, version.version) keysBackupService.saveBackupRecoveryKey(recoveryKey, version.version)

View file

@ -126,26 +126,22 @@ class BootstrapCrossSigningTask @Inject constructor(
Timber.d("## BootstrapCrossSigningTask: Creating 4S key with pass: ${params.passphrase != null}") Timber.d("## BootstrapCrossSigningTask: Creating 4S key with pass: ${params.passphrase != null}")
try { try {
keyInfo = awaitCallback { keyInfo = params.passphrase?.let { passphrase ->
params.passphrase?.let { passphrase ->
ssssService.generateKeyWithPassphrase( ssssService.generateKeyWithPassphrase(
UUID.randomUUID().toString(), UUID.randomUUID().toString(),
"ssss_key", "ssss_key",
passphrase, passphrase,
EmptyKeySigner(), EmptyKeySigner(),
null, null
it
) )
} ?: run { } ?: run {
ssssService.generateKey( ssssService.generateKey(
UUID.randomUUID().toString(), UUID.randomUUID().toString(),
params.keySpec, params.keySpec,
"ssss_key", "ssss_key",
EmptyKeySigner(), EmptyKeySigner()
it
) )
} }
}
} catch (failure: Failure) { } catch (failure: Failure) {
Timber.e("## BootstrapCrossSigningTask: Creating 4S - Failed to generate key <${failure.localizedMessage}>") Timber.e("## BootstrapCrossSigningTask: Creating 4S - Failed to generate key <${failure.localizedMessage}>")
return BootstrapResult.FailedToCreateSSSSKey(failure) return BootstrapResult.FailedToCreateSSSSKey(failure)
@ -159,9 +155,7 @@ class BootstrapCrossSigningTask @Inject constructor(
Timber.d("## BootstrapCrossSigningTask: Creating 4S - Set default key") Timber.d("## BootstrapCrossSigningTask: Creating 4S - Set default key")
try { try {
awaitCallback<Unit> { ssssService.setDefaultKey(keyInfo.keyId)
ssssService.setDefaultKey(keyInfo.keyId, it)
}
} catch (failure: Failure) { } catch (failure: Failure) {
// Maybe we could just ignore this error? // Maybe we could just ignore this error?
Timber.e("## BootstrapCrossSigningTask: Creating 4S - Set default key error <${failure.localizedMessage}>") Timber.e("## BootstrapCrossSigningTask: Creating 4S - Set default key error <${failure.localizedMessage}>")
@ -183,13 +177,11 @@ class BootstrapCrossSigningTask @Inject constructor(
) )
) )
Timber.d("## BootstrapCrossSigningTask: Creating 4S - Storing MSK...") Timber.d("## BootstrapCrossSigningTask: Creating 4S - Storing MSK...")
awaitCallback<Unit> {
ssssService.storeSecret( ssssService.storeSecret(
MASTER_KEY_SSSS_NAME, MASTER_KEY_SSSS_NAME,
mskPrivateKey, mskPrivateKey,
listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec)), it listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec))
) )
}
params.progressListener?.onProgress( params.progressListener?.onProgress(
WaitingViewData( WaitingViewData(
stringProvider.getString(R.string.bootstrap_crosssigning_progress_save_usk), stringProvider.getString(R.string.bootstrap_crosssigning_progress_save_usk),
@ -197,27 +189,22 @@ class BootstrapCrossSigningTask @Inject constructor(
) )
) )
Timber.d("## BootstrapCrossSigningTask: Creating 4S - Storing USK...") Timber.d("## BootstrapCrossSigningTask: Creating 4S - Storing USK...")
awaitCallback<Unit> {
ssssService.storeSecret( ssssService.storeSecret(
USER_SIGNING_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME,
uskPrivateKey, uskPrivateKey,
listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec)), listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec))
it
) )
}
params.progressListener?.onProgress( params.progressListener?.onProgress(
WaitingViewData( WaitingViewData(
stringProvider.getString(R.string.bootstrap_crosssigning_progress_save_ssk), isIndeterminate = true stringProvider.getString(R.string.bootstrap_crosssigning_progress_save_ssk), isIndeterminate = true
) )
) )
Timber.d("## BootstrapCrossSigningTask: Creating 4S - Storing SSK...") Timber.d("## BootstrapCrossSigningTask: Creating 4S - Storing SSK...")
awaitCallback<Unit> {
ssssService.storeSecret( ssssService.storeSecret(
SELF_SIGNING_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME,
sskPrivateKey, sskPrivateKey,
listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec)), it listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec))
) )
}
} catch (failure: Failure) { } catch (failure: Failure) {
Timber.e("## BootstrapCrossSigningTask: Creating 4S - Failed to store keys <${failure.localizedMessage}>") Timber.e("## BootstrapCrossSigningTask: Creating 4S - Failed to store keys <${failure.localizedMessage}>")
// Maybe we could just ignore this error? // Maybe we could just ignore this error?
@ -265,15 +252,13 @@ class BootstrapCrossSigningTask @Inject constructor(
Timber.d("## BootstrapCrossSigningTask: Creating 4S - Save megolm backup key for gossiping") Timber.d("## BootstrapCrossSigningTask: Creating 4S - Save megolm backup key for gossiping")
session.cryptoService().keysBackupService().saveBackupRecoveryKey(creationInfo.recoveryKey, version = version.version) session.cryptoService().keysBackupService().saveBackupRecoveryKey(creationInfo.recoveryKey, version = version.version)
awaitCallback<Unit> {
extractCurveKeyFromRecoveryKey(creationInfo.recoveryKey)?.toBase64NoPadding()?.let { secret -> extractCurveKeyFromRecoveryKey(creationInfo.recoveryKey)?.toBase64NoPadding()?.let { secret ->
ssssService.storeSecret( ssssService.storeSecret(
KEYBACKUP_SECRET_SSSS_NAME, KEYBACKUP_SECRET_SSSS_NAME,
secret, secret,
listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec)), it listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec))
) )
} }
}
} else { } else {
Timber.d("## BootstrapCrossSigningTask: Creating 4S - Existing megolm backup found") Timber.d("## BootstrapCrossSigningTask: Creating 4S - Existing megolm backup found")
// ensure we store existing backup secret if we have it! // ensure we store existing backup secret if we have it!
@ -284,15 +269,13 @@ class BootstrapCrossSigningTask @Inject constructor(
} }
if (isValid) { if (isValid) {
Timber.d("## BootstrapCrossSigningTask: Creating 4S - Megolm key valid and known") Timber.d("## BootstrapCrossSigningTask: Creating 4S - Megolm key valid and known")
awaitCallback<Unit> {
extractCurveKeyFromRecoveryKey(knownMegolmSecret!!.recoveryKey)?.toBase64NoPadding()?.let { secret -> extractCurveKeyFromRecoveryKey(knownMegolmSecret!!.recoveryKey)?.toBase64NoPadding()?.let { secret ->
ssssService.storeSecret( ssssService.storeSecret(
KEYBACKUP_SECRET_SSSS_NAME, KEYBACKUP_SECRET_SSSS_NAME,
secret, secret,
listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec)), it listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec))
) )
} }
}
} else { } else {
Timber.d("## BootstrapCrossSigningTask: Creating 4S - Megolm key is unknown by this session") Timber.d("## BootstrapCrossSigningTask: Creating 4S - Megolm key is unknown by this session")
} }

View file

@ -32,7 +32,6 @@ import im.vector.app.core.platform.VectorViewModel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent
import org.matrix.android.sdk.internal.util.awaitCallback
import org.matrix.android.sdk.rx.rx import org.matrix.android.sdk.rx.rx
data class AccountDataViewState( data class AccountDataViewState(
@ -58,9 +57,7 @@ class AccountDataViewModel @AssistedInject constructor(@Assisted initialState: A
private fun handleDeleteAccountData(action: AccountDataAction.DeleteAccountData) { private fun handleDeleteAccountData(action: AccountDataAction.DeleteAccountData) {
viewModelScope.launch { viewModelScope.launch {
awaitCallback { session.updateAccountData(action.type, emptyMap())
session.updateAccountData(action.type, emptyMap(), it)
}
} }
} }

View file

@ -283,11 +283,12 @@ class WidgetPostAPIHandler @AssistedInject constructor(@Assisted private val roo
"type" to "m.widget" "type" to "m.widget"
) )
) )
launchWidgetAPIAction(widgetPostAPIMediator, eventData) {
session.updateAccountData( session.updateAccountData(
type = UserAccountDataTypes.TYPE_WIDGETS, type = UserAccountDataTypes.TYPE_WIDGETS,
content = addUserWidgetBody, content = addUserWidgetBody
callback = createWidgetAPICallback(widgetPostAPIMediator, eventData)
) )
}
} else { } else {
launchWidgetAPIAction(widgetPostAPIMediator, eventData) { launchWidgetAPIAction(widgetPostAPIMediator, eventData) {
session.widgetService().createRoomWidget( session.widgetService().createRoomWidget(