SSSS service + test

This commit is contained in:
Valere 2020-02-12 11:28:13 +01:00 committed by Valere
parent bf06b57bad
commit 108ebea84e
22 changed files with 1043 additions and 70 deletions

View File

@ -31,7 +31,7 @@ import im.vector.matrix.android.api.util.JsonDict
import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.api.util.toOptional
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent
import io.reactivex.Observable
import io.reactivex.Single
@ -123,7 +123,7 @@ class RxSession(private val session: Session) {
}
}
fun liveAccountData(filter: List<String>): Observable<List<UserAccountData>> {
fun liveAccountData(filter: List<String>): Observable<List<UserAccountDataEvent>> {
return session.getLiveAccountData(filter).asObservable()
.startWithCallable {
session.getAccountData(filter)

View File

@ -0,0 +1,272 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* 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 im.vector.matrix.android.internal.crypto.ssss
import android.util.Base64
import androidx.lifecycle.Observer
import androidx.test.ext.junit.runners.AndroidJUnit4
import im.vector.matrix.android.InstrumentedTest
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.securestorage.Curve25519AesSha2KeySpec
import im.vector.matrix.android.api.session.securestorage.EncryptedSecretContent
import im.vector.matrix.android.api.session.securestorage.KeySigner
import im.vector.matrix.android.api.session.securestorage.SSSSKeyCreationInfo
import im.vector.matrix.android.api.session.securestorage.SecretStorageKeyContent
import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.common.CommonTestHelper
import im.vector.matrix.android.common.CryptoTestHelper
import im.vector.matrix.android.common.SessionTestParams
import im.vector.matrix.android.common.TestConstants
import im.vector.matrix.android.common.TestMatrixCallback
import im.vector.matrix.android.internal.crypto.secrets.DefaultSharedSecureStorage
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.junit.Assert
import org.junit.Assert.fail
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.MethodSorters
import java.util.concurrent.CountDownLatch
@RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
class QuadSTests : InstrumentedTest {
private val mTestHelper = CommonTestHelper(context())
private val mCryptoTestHelper = CryptoTestHelper(mTestHelper)
@Test
fun test_Generate4SKey() {
val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
val aliceLatch = CountDownLatch(1)
val quadS = aliceSession.sharedSecretStorageService
val emptyKeySigner = object : KeySigner {
override fun sign(canonicalJson: String): Map<String, Map<String, String>>? {
return null
}
}
var recoveryKey: String? = null
val TEST_KEY_ID = "my.test.Key"
quadS.generateKey(TEST_KEY_ID, "Test Key", emptyKeySigner,
object : MatrixCallback<SSSSKeyCreationInfo> {
override fun onSuccess(data: SSSSKeyCreationInfo) {
recoveryKey = data.recoveryKey
aliceLatch.countDown()
}
override fun onFailure(failure: Throwable) {
Assert.fail("onFailure " + failure.localizedMessage)
aliceLatch.countDown()
}
})
mTestHelper.await(aliceLatch)
// Assert Account data is updated
val accountDataLock = CountDownLatch(1)
var accountData: UserAccountDataEvent? = null
val liveAccountData = runBlocking(Dispatchers.Main) {
aliceSession.getLiveAccountData("m.secret_storage.key.$TEST_KEY_ID")
}
val accountDataObserver = Observer<Optional<UserAccountDataEvent>?> { t ->
if (t?.getOrNull()?.type == "m.secret_storage.key.$TEST_KEY_ID") {
accountData = t.getOrNull()
accountDataLock.countDown()
}
}
GlobalScope.launch(Dispatchers.Main) { liveAccountData.observeForever(accountDataObserver) }
mTestHelper.await(accountDataLock)
Assert.assertNotNull("Key should be stored in account data", accountData)
val parsed = SecretStorageKeyContent.fromJson(accountData!!.content)
Assert.assertNotNull("Key Content cannot be parsed", parsed)
Assert.assertEquals("Unexpected Algorithm", DefaultSharedSecureStorage.ALGORITHM_CURVE25519_AES_SHA2, parsed!!.algorithm)
Assert.assertEquals("Unexpected key name", "Test Key", parsed.name)
Assert.assertNull("Key was not generated from passphrase", parsed.passphrase)
Assert.assertNotNull("Pubkey should be defined", parsed.publicKey)
val privateKeySpec = Curve25519AesSha2KeySpec.fromRecoveryKey(recoveryKey!!)
DefaultSharedSecureStorage.withOlmDecryption { olmPkDecryption ->
val pubKey = olmPkDecryption.setPrivateKey(privateKeySpec!!.privateKey)
Assert.assertEquals("Unexpected Public Key", pubKey, parsed.publicKey)
}
// Set as default key
quadS.setDefaultKey(TEST_KEY_ID, object : MatrixCallback<Unit> {})
var defaultKeyAccountData: UserAccountDataEvent? = null
val defaultDataLock = CountDownLatch(1)
val liveDefAccountData = runBlocking(Dispatchers.Main) {
aliceSession.getLiveAccountData(DefaultSharedSecureStorage.DEFAULT_KEY_ID)
}
val accountDefDataObserver = Observer<Optional<UserAccountDataEvent>?> { t ->
if (t?.getOrNull()?.type == DefaultSharedSecureStorage.DEFAULT_KEY_ID) {
defaultKeyAccountData = t.getOrNull()!!
defaultDataLock.countDown()
}
}
GlobalScope.launch(Dispatchers.Main) { liveDefAccountData.observeForever(accountDefDataObserver) }
mTestHelper.await(defaultDataLock)
Assert.assertNotNull(defaultKeyAccountData?.content)
Assert.assertEquals("Unexpected default key ${defaultKeyAccountData?.content}", TEST_KEY_ID, defaultKeyAccountData?.content?.get("key"))
mTestHelper.signout(aliceSession)
}
@Test
fun test_StoreSecret() {
val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
val keyId = "My.Key"
val info = generatedSecret(aliceSession, keyId, true)
// Store a secret
val storeCountDownLatch = CountDownLatch(1)
val clearSecret = Base64.encodeToString("42".toByteArray(), Base64.NO_PADDING or Base64.NO_WRAP)
aliceSession.sharedSecretStorageService.storeSecret(
"secret.of.life",
clearSecret,
null, // default key
TestMatrixCallback(storeCountDownLatch)
)
val secretAccountData = assertAccountData(aliceSession,"secret.of.life" )
val encryptedContent = secretAccountData.content.get("encrypted") as? Map<*,*>
Assert.assertNotNull("Element should be encrypted", encryptedContent)
Assert.assertNotNull("Secret should be encrypted with default key", encryptedContent?.get(keyId))
val secret = EncryptedSecretContent.fromJson(encryptedContent?.get(keyId))
Assert.assertNotNull(secret?.ciphertext)
Assert.assertNotNull(secret?.mac)
Assert.assertNotNull(secret?.ephemeral)
// Try to decrypt??
val keySpec = Curve25519AesSha2KeySpec.fromRecoveryKey(info.recoveryKey)
var decryptedSecret: String? = null
val decryptCountDownLatch = CountDownLatch(1)
aliceSession.sharedSecretStorageService.getSecret("secret.of.life" ,
null, //default key
keySpec!!,
null,
object : MatrixCallback<String> {
override fun onFailure(failure: Throwable) {
fail("Fail to decrypt -> " +failure.localizedMessage)
decryptCountDownLatch.countDown()
}
override fun onSuccess(data: String) {
decryptedSecret = data
decryptCountDownLatch.countDown()
}
}
)
mTestHelper.await(decryptCountDownLatch)
Assert.assertEquals("Secret mismatch", clearSecret, decryptedSecret)
mTestHelper.signout(aliceSession)
}
private fun assertAccountData(session: Session, type: String): UserAccountDataEvent {
val accountDataLock = CountDownLatch(1)
var accountData: UserAccountDataEvent? = null
val liveAccountData = runBlocking(Dispatchers.Main) {
session.getLiveAccountData(type)
}
val accountDataObserver = Observer<Optional<UserAccountDataEvent>?> { t ->
if (t?.getOrNull()?.type == type) {
accountData = t.getOrNull()
accountDataLock.countDown()
}
}
GlobalScope.launch(Dispatchers.Main) { liveAccountData.observeForever(accountDataObserver) }
mTestHelper.await(accountDataLock)
Assert.assertNotNull("Account Data type:$type should be found", accountData)
return accountData!!
}
private fun generatedSecret(session: Session, keyId: String, asDefault: Boolean = true): SSSSKeyCreationInfo {
val quadS = session.sharedSecretStorageService
val emptyKeySigner = object : KeySigner {
override fun sign(canonicalJson: String): Map<String, Map<String, String>>? {
return null
}
}
var creationInfo: SSSSKeyCreationInfo? = null
val generateLatch = CountDownLatch(1)
quadS.generateKey(keyId, keyId, emptyKeySigner,
object : MatrixCallback<SSSSKeyCreationInfo> {
override fun onSuccess(data: SSSSKeyCreationInfo) {
creationInfo = data
generateLatch.countDown()
}
override fun onFailure(failure: Throwable) {
Assert.fail("onFailure " + failure.localizedMessage)
generateLatch.countDown()
}
})
mTestHelper.await(generateLatch)
Assert.assertNotNull(creationInfo)
assertAccountData(session, "m.secret_storage.key.$keyId")
if (asDefault) {
val setDefaultLatch = CountDownLatch(1)
quadS.setDefaultKey(keyId, TestMatrixCallback(setDefaultLatch))
mTestHelper.await(setDefaultLatch)
assertAccountData(session, DefaultSharedSecureStorage.DEFAULT_KEY_ID)
}
return creationInfo!!
}
}

View File

@ -34,6 +34,7 @@ import im.vector.matrix.android.api.session.pushers.PushersService
import im.vector.matrix.android.api.session.room.RoomDirectoryService
import im.vector.matrix.android.api.session.room.RoomService
import im.vector.matrix.android.api.session.securestorage.SecureStorageService
import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageService
import im.vector.matrix.android.api.session.signout.SignOutService
import im.vector.matrix.android.api.session.sync.FilterService
import im.vector.matrix.android.api.session.sync.SyncState
@ -161,4 +162,6 @@ interface Session :
*/
fun onGlobalError(globalError: GlobalError)
}
val sharedSecretStorageService: SharedSecretStorageService
}

View File

@ -19,17 +19,17 @@ package im.vector.matrix.android.api.session.accountdata
import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent
interface AccountDataService {
fun getAccountData(type: String): UserAccountData?
fun getAccountData(type: String): UserAccountDataEvent?
fun getLiveAccountData(type: String): LiveData<Optional<UserAccountData>>
fun getLiveAccountData(type: String): LiveData<Optional<UserAccountDataEvent>>
fun getAccountData(filterType: List<String>): List<UserAccountData>
fun getAccountData(filterType: List<String>): List<UserAccountDataEvent>
fun getLiveAccountData(filterType: List<String>): LiveData<List<UserAccountData>>
fun getLiveAccountData(filterType: List<String>): LiveData<List<UserAccountDataEvent>>
fun updateAccountData(type: String, data: Any, callback: MatrixCallback<Unit>? = null)
}

View File

@ -0,0 +1,47 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* 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 im.vector.matrix.android.api.session.securestorage
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.internal.di.MoshiProvider
/**
* The account_data will have an encrypted property that is a map from key ID to an object.
* The algorithm from the m.secret_storage.key.[key ID] data for the given key defines how the other properties are interpreted,
* though it's expected that most encryption schemes would have ciphertext and mac properties,
* where the ciphertext property is the unpadded base64-encoded ciphertext, and the mac is used to ensure the integrity of the data.
*/
@JsonClass(generateAdapter = true)
data class EncryptedSecretContent(
/** unpadded base64-encoded ciphertext */
@Json(name = "ciphertext") val ciphertext: String? = null,
@Json(name = "mac") val mac: String? = null,
@Json(name = "ephemeral") val ephemeral: String? = null
) {
companion object {
/**
* Facility method to convert from object which must be comprised of maps, lists,
* strings, numbers, booleans and nulls.
*/
fun fromJson(obj: Any?): EncryptedSecretContent? {
return MoshiProvider.providesMoshi()
.adapter(EncryptedSecretContent::class.java)
.fromJsonValue(obj)
}
}
}

View File

@ -0,0 +1,24 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* 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 im.vector.matrix.android.api.session.securestorage
sealed class KeyInfoResult {
data class Success(val keyInfo: KeyInfo) : KeyInfoResult()
data class Error(val error: SharedSecretStorageError) : KeyInfoResult()
fun isSuccess(): Boolean = this is Success
}

View File

@ -0,0 +1,21 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* 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 im.vector.matrix.android.api.session.securestorage
interface KeySigner {
fun sign(canonicalJson: String): Map<String, Map<String, String>>?
}

View File

@ -0,0 +1,23 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* 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 im.vector.matrix.android.api.session.securestorage
data class SSSSKeyCreationInfo (
val keyId: String = "",
var content: SecretStorageKeyContent?,
val recoveryKey: String = ""
)

View File

@ -0,0 +1,67 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* 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 im.vector.matrix.android.api.session.securestorage
import im.vector.matrix.android.api.listeners.ProgressListener
import im.vector.matrix.android.internal.crypto.keysbackup.deriveKey
import im.vector.matrix.android.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey
/** Tag class */
interface SSSSKeySpec
data class Curve25519AesSha2KeySpec(
val privateKey: ByteArray
) : SSSSKeySpec {
companion object {
fun fromPassphrase(passphrase: String, salt: String, iterations: Int, progressListener: ProgressListener?): Curve25519AesSha2KeySpec {
return Curve25519AesSha2KeySpec(
privateKey = deriveKey(
passphrase,
salt,
iterations,
progressListener
)
)
}
fun fromRecoveryKey(recoveryKey: String): Curve25519AesSha2KeySpec? {
return extractCurveKeyFromRecoveryKey(recoveryKey)?.let {
Curve25519AesSha2KeySpec(
privateKey = it
)
}
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Curve25519AesSha2KeySpec
if (!privateKey.contentEquals(other.privateKey)) return false
return true
}
override fun hashCode(): Int {
return privateKey.contentHashCode()
}
}

View File

@ -0,0 +1,101 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* 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 im.vector.matrix.android.api.session.securestorage
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.matrix.android.internal.util.JsonCanonicalizer
/**
*
* The contents of the account data for the key will include an algorithm property, which indicates the encryption algorithm used, as well as a name property,
* which is a human-readable name.
* The contents will be signed as signed JSON using the user's master cross-signing key. Other properties depend on the encryption algorithm.
*
*
* "content": {
* "algorithm": "m.secret_storage.v1.curve25519-aes-sha2",
* "passphrase": {
* "algorithm": "m.pbkdf2",
* "iterations": 500000,
* "salt": "IrswcMWnYieBALCAOMBw9k93xSzlc2su"
* },
* "pubkey": "qql1q3IvBbwMU97zLnyh9HYW5x/zqTy5eoK1n+9fm1Y",
* "signatures": {
* "@valere35:matrix.org": {
* "ed25519:nOUQYiH9L8uKp5JajqiQyv+Loa3+lsdil7UBverz/Ko": "QtePmwfUL7+SHYRJT/HaTgF7gUFog1E/wtUCt0qc5aB8N+Sz5iCOvQ0KtaFHQ5SJzsBlYH8k7ejoBc0RcnU7BA"
* }
* }
* }
*/
data class KeyInfo(
val id: String,
val content: SecretStorageKeyContent
)
@JsonClass(generateAdapter = true)
data class SecretStorageKeyContent(
/** Currently support m.secret_storage.v1.curve25519-aes-sha2 */
@Json(name = "algorithm") val algorithm: String? = null,
@Json(name = "name") val name: String? = null,
@Json(name = "passphrase") val passphrase: SSSSPassphrase? = null,
@Json(name = "pubkey") val publicKey: String? = null,
@Json(name = "signatures")
var signatures: Map<String, Map<String, String>>? = null
) {
private fun signalableJSONDictionary(): Map<String, Any> {
val map = HashMap<String, Any>()
algorithm?.let { map["algorithm"] = it }
name?.let { map["name"] = it }
publicKey?.let { map["pubkey"] = it }
passphrase?.let { ssspp ->
map["passphrase"] = mapOf(
"algorithm" to ssspp.algorithm,
"iterations" to ssspp.salt,
"salt" to ssspp.salt
)
}
return map
}
fun canonicalSignable(): String {
return JsonCanonicalizer.getCanonicalJson(Map::class.java, signalableJSONDictionary())
}
companion object {
/**
* Facility method to convert from object which must be comprised of maps, lists,
* strings, numbers, booleans and nulls.
*/
fun fromJson(obj: Any?): SecretStorageKeyContent? {
return MoshiProvider.providesMoshi()
.adapter(SecretStorageKeyContent::class.java)
.fromJsonValue(obj)
}
}
}
@JsonClass(generateAdapter = true)
data class SSSSPassphrase(
@Json(name = "algorithm") val algorithm: String?,
@Json(name = "iterations") val iterations: Int,
@Json(name = "salt") val salt: String?
)

View File

@ -0,0 +1,30 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* 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 im.vector.matrix.android.api.session.securestorage
sealed class SharedSecretStorageError(message: String?) : Throwable(message) {
data class UnknownSecret(val secretName: String) : SharedSecretStorageError("Unknown Secret $secretName")
data class UnknownKey(val keyId: String) : SharedSecretStorageError("Unknown key $keyId")
data class UnknownAlgorithm(val keyId: String) : SharedSecretStorageError("Unknown algorithm $keyId")
data class UnsupportedAlgorithm(val algorithm: String) : SharedSecretStorageError("Unknown algorithm $algorithm")
data class SecretNotEncrypted(val secretName: String) : SharedSecretStorageError("Missing content for secret $secretName")
data class SecretNotEncryptedWithKey(val secretName: String, val keyId: String) : SharedSecretStorageError("Missing content for secret $secretName with key $keyId")
object BadKeyFormat : SharedSecretStorageError("Bad Key Format")
object ParsingError : SharedSecretStorageError("parsing Error")
data class OtherError(val reason: Throwable) : SharedSecretStorageError(reason.localizedMessage)
}

View File

@ -17,6 +17,7 @@
package im.vector.matrix.android.api.session.securestorage
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.listeners.ProgressListener
/**
* Some features may require clients to store encrypted data on the server so that it can be shared securely between clients.
@ -39,7 +40,29 @@ interface SharedSecretStorageService {
*
* @return {string} the ID of the key
*/
fun addKey(algorithm: String, opts: Map<String, Any>, keyId: String, callback: MatrixCallback<String>)
fun generateKey(keyId: String,
keyName: String,
keySigner: KeySigner,
callback: MatrixCallback<SSSSKeyCreationInfo>)
fun generateKeyWithPassphrase(keyId: String,
keyName: String,
passphrase: String,
keySigner: KeySigner,
progressListener: ProgressListener?,
callback: MatrixCallback<SSSSKeyCreationInfo>)
fun getKey(keyId: String): KeyInfoResult
/**
* A key can be marked as the "default" key by setting the user's account_data with event type m.secret_storage.default_key
* to an object that has the ID of the key as its key property.
* The default key will be used to encrypt all secrets that the user would expect to be available on all their clients.
* Unless the user specifies otherwise, clients will try to use the default key to decrypt secrets.
*/
fun getDefaultKey(): KeyInfoResult
fun setDefaultKey(keyId: String, callback: MatrixCallback<Unit>)
/**
* Check whether we have a key with a given ID.
@ -51,23 +74,29 @@ interface SharedSecretStorageService {
/**
* Store an encrypted secret on the server
* Clients MUST ensure that the key is trusted before using it to encrypt secrets.
*
* @param name The name of the secret
* @param secret The secret contents.
* @param keys The IDs of the keys to use to encrypt the secret or null to use the default key.
* @param keys The list of (ID,privateKey) of the keys to use to encrypt the secret.
*/
fun storeSecret(name: String, secretBase64: String, keys: List<String>?, callback: MatrixCallback<Unit>)
/**
* Use this call to determine which SSSSKeySpec to use for requesting secret
*/
fun getAlgorithmsForSecret(name: String): List<KeyInfoResult>
/**
* Get an encrypted secret from the shared storage
*
* @param name The name of the secret
* @param keyId The id of the key that should be used to decrypt
* @param keyId The id of the key that should be used to decrypt (null for default key)
* @param privateKey the passphrase/secret
*
* @return The decrypted value
*/
fun getSecret(name: String, keyId: String, privateKey: String) : String
@Throws
fun getSecret(name: String, keyId: String?, secretKey: SSSSKeySpec, callback: MatrixCallback<String>)
}

View File

@ -40,8 +40,28 @@ import im.vector.matrix.android.internal.crypto.keysbackup.model.KeysBackupVersi
import im.vector.matrix.android.internal.crypto.keysbackup.model.KeysBackupVersionTrustSignature
import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupAuthData
import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.*
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.*
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.BackupKeysResult
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.CreateKeysBackupVersionBody
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeyBackupData
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysBackupData
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersion
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.RoomKeysBackupData
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.UpdateKeysBackupVersionBody
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.CreateKeysBackupVersionTask
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.DeleteBackupTask
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.DeleteRoomSessionDataTask
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.DeleteRoomSessionsDataTask
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.DeleteSessionsDataTask
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.GetKeysBackupLastVersionTask
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.GetKeysBackupVersionTask
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.GetRoomSessionDataTask
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.GetRoomSessionsDataTask
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.GetSessionsDataTask
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.StoreRoomSessionDataTask
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.StoreRoomSessionsDataTask
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.StoreSessionsDataTask
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.UpdateKeysBackupVersionTask
import im.vector.matrix.android.internal.crypto.keysbackup.util.computeRecoveryKey
import im.vector.matrix.android.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult

View File

@ -83,10 +83,10 @@ fun retrievePrivateKeyWithPassword(password: String,
* @return a private key.
*/
@WorkerThread
private fun deriveKey(password: String,
salt: String,
iterations: Int,
progressListener: ProgressListener?): ByteArray {
fun deriveKey(password: String,
salt: String,
iterations: Int,
progressListener: ProgressListener?): ByteArray {
// Note: copied and adapted from MXMegolmExportEncryption
val t0 = System.currentTimeMillis()

View File

@ -0,0 +1,366 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* 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 im.vector.matrix.android.internal.crypto.secrets
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.listeners.ProgressListener
import im.vector.matrix.android.api.session.accountdata.AccountDataService
import im.vector.matrix.android.api.session.securestorage.Curve25519AesSha2KeySpec
import im.vector.matrix.android.api.session.securestorage.EncryptedSecretContent
import im.vector.matrix.android.api.session.securestorage.KeyInfo
import im.vector.matrix.android.api.session.securestorage.KeyInfoResult
import im.vector.matrix.android.api.session.securestorage.KeySigner
import im.vector.matrix.android.api.session.securestorage.SSSSKeyCreationInfo
import im.vector.matrix.android.api.session.securestorage.SSSSKeySpec
import im.vector.matrix.android.api.session.securestorage.SSSSPassphrase
import im.vector.matrix.android.api.session.securestorage.SecretStorageKeyContent
import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageError
import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageService
import im.vector.matrix.android.internal.crypto.keysbackup.generatePrivateKeyWithPassword
import im.vector.matrix.android.internal.crypto.keysbackup.util.computeRecoveryKey
import im.vector.matrix.android.internal.extensions.foldToCallback
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.matrix.olm.OlmPkDecryption
import org.matrix.olm.OlmPkEncryption
import org.matrix.olm.OlmPkMessage
import javax.inject.Inject
internal class DefaultSharedSecureStorage @Inject constructor(
private val accountDataService: AccountDataService,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val cryptoCoroutineScope: CoroutineScope
) : SharedSecretStorageService {
override fun generateKey(keyId: String,
keyName: String,
keySigner: KeySigner,
callback: MatrixCallback<SSSSKeyCreationInfo>) {
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
val pkDecryption = OlmPkDecryption()
val pubKey: String
val privateKey: ByteArray
try {
pubKey = pkDecryption.generateKey()
privateKey = pkDecryption.privateKey()
} catch (failure: Throwable) {
return@launch Unit.also {
callback.onFailure(failure)
}
} finally {
pkDecryption.releaseDecryption()
}
val storageKeyContent = SecretStorageKeyContent(
name = keyName,
algorithm = ALGORITHM_CURVE25519_AES_SHA2,
passphrase = null,
publicKey = pubKey
)
val signedContent = keySigner.sign(storageKeyContent.canonicalSignable())?.let {
storageKeyContent.copy(
signatures = it
)
} ?: storageKeyContent
accountDataService.updateAccountData(
"$KEY_ID_BASE.$keyId",
signedContent,
object : MatrixCallback<Unit> {
override fun onFailure(failure: Throwable) {
callback.onFailure(failure)
}
override fun onSuccess(data: Unit) {
callback.onSuccess(SSSSKeyCreationInfo(
keyId = keyId,
content = storageKeyContent,
recoveryKey = computeRecoveryKey(privateKey)
))
}
}
)
}
}
override fun generateKeyWithPassphrase(keyId: String,
keyName: String,
passphrase: String,
keySigner: KeySigner,
progressListener: ProgressListener?,
callback: MatrixCallback<SSSSKeyCreationInfo>) {
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
val privatePart = generatePrivateKeyWithPassword(passphrase, progressListener)
val pkDecryption = OlmPkDecryption()
val pubKey: String
try {
pubKey = pkDecryption.setPrivateKey(privatePart.privateKey)
} catch (failure: Throwable) {
return@launch Unit.also {
callback.onFailure(failure)
}
} finally {
pkDecryption.releaseDecryption()
}
val storageKeyContent = SecretStorageKeyContent(
algorithm = ALGORITHM_CURVE25519_AES_SHA2,
passphrase = SSSSPassphrase(algorithm = "m.pbkdf2", iterations = privatePart.iterations, salt = privatePart.salt),
publicKey = pubKey
)
val signedContent = keySigner.sign(storageKeyContent.canonicalSignable())?.let {
storageKeyContent.copy(
signatures = it
)
} ?: storageKeyContent
accountDataService.updateAccountData(
"$KEY_ID_BASE.$keyId",
signedContent,
object : MatrixCallback<Unit> {
override fun onFailure(failure: Throwable) {
callback.onFailure(failure)
}
override fun onSuccess(data: Unit) {
callback.onSuccess(SSSSKeyCreationInfo(
keyId = keyId,
content = storageKeyContent,
recoveryKey = computeRecoveryKey(privatePart.privateKey)
))
}
}
)
}
}
override fun hasKey(keyId: String): Boolean {
return accountDataService.getAccountData("$KEY_ID_BASE.$keyId") != null
}
override fun getKey(keyId: String): KeyInfoResult {
val accountData = accountDataService.getAccountData("$KEY_ID_BASE.$keyId")
?: return KeyInfoResult.Error(SharedSecretStorageError.UnknownKey(keyId))
return SecretStorageKeyContent.fromJson(accountData.content)?.let {
KeyInfoResult.Success(
KeyInfo(id = keyId, content = it)
)
} ?: KeyInfoResult.Error(SharedSecretStorageError.UnknownAlgorithm(keyId))
}
override fun setDefaultKey(keyId: String, callback: MatrixCallback<Unit>) {
val existingKey = getKey(keyId)
if (existingKey is KeyInfoResult.Success) {
accountDataService.updateAccountData(DEFAULT_KEY_ID,
mapOf("key" to keyId),
callback
)
} else {
callback.onFailure(SharedSecretStorageError.UnknownKey(keyId))
}
}
override fun getDefaultKey(): KeyInfoResult {
val accountData = accountDataService.getAccountData(DEFAULT_KEY_ID)
?: return KeyInfoResult.Error(SharedSecretStorageError.UnknownKey(DEFAULT_KEY_ID))
val keyId = accountData.content["key"] as? String
?: return KeyInfoResult.Error(SharedSecretStorageError.UnknownKey(DEFAULT_KEY_ID))
return getKey(keyId)
}
override fun storeSecret(name: String, secretBase64: String, keys: List<String>?, callback: MatrixCallback<Unit>) {
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
val encryptedContents = HashMap<String, EncryptedSecretContent>()
try {
if (keys == null || keys.isEmpty()) {
//use default key
val key = getDefaultKey()
when (key) {
is KeyInfoResult.Success -> {
if (key.keyInfo.content.algorithm == ALGORITHM_CURVE25519_AES_SHA2) {
withOlmEncryption { olmEncrypt ->
olmEncrypt.setRecipientKey(key.keyInfo.content.publicKey)
val encryptedResult = olmEncrypt.encrypt(secretBase64)
encryptedContents[key.keyInfo.id] = EncryptedSecretContent(
ciphertext = encryptedResult.mCipherText,
ephemeral = encryptedResult.mEphemeralKey,
mac = encryptedResult.mMac
)
}
} else {
// Unknown algorithm
callback.onFailure(SharedSecretStorageError.UnknownAlgorithm(key.keyInfo.content.algorithm ?: ""))
return@launch
}
}
is KeyInfoResult.Error -> {
callback.onFailure(key.error)
return@launch
}
}
} else {
keys.forEach {
val keyId = it
// encrypt the content
val key = getKey(keyId)
when (key) {
is KeyInfoResult.Success -> {
if (key.keyInfo.content.algorithm == ALGORITHM_CURVE25519_AES_SHA2) {
withOlmEncryption { olmEncrypt ->
olmEncrypt.setRecipientKey(key.keyInfo.content.publicKey)
val encryptedResult = olmEncrypt.encrypt(secretBase64)
encryptedContents[keyId] = EncryptedSecretContent(
ciphertext = encryptedResult.mCipherText,
ephemeral = encryptedResult.mEphemeralKey,
mac = encryptedResult.mMac
)
}
} else {
// Unknown algorithm
callback.onFailure(SharedSecretStorageError.UnknownAlgorithm(key.keyInfo.content.algorithm ?: ""))
return@launch
}
}
is KeyInfoResult.Error -> {
callback.onFailure(key.error)
return@launch
}
}
}
}
accountDataService.updateAccountData(
type = name,
data = mapOf(
"encrypted" to encryptedContents
),
callback = callback
)
} catch (failure: Throwable) {
callback.onFailure(failure)
}
}
// Add default key
}
override fun getAlgorithmsForSecret(name: String): List<KeyInfoResult> {
val accountData = accountDataService.getAccountData(name)
?: return listOf(KeyInfoResult.Error(SharedSecretStorageError.UnknownSecret(name)))
val encryptedContent = accountData.content[ENCRYPTED] as? Map<*, *>
?: return listOf(KeyInfoResult.Error(SharedSecretStorageError.SecretNotEncrypted(name)))
val results = ArrayList<KeyInfoResult>()
encryptedContent.keys.forEach {
(it as? String)?.let { keyId ->
results.add(getKey(keyId))
}
}
return results
}
override fun getSecret(name: String, keyId: String?, secretKey: SSSSKeySpec, callback: MatrixCallback<String>) {
val accountData = accountDataService.getAccountData(name) ?: return Unit.also {
callback.onFailure(SharedSecretStorageError.UnknownSecret(name))
}
val encryptedContent = accountData.content[ENCRYPTED] as? Map<*, *> ?: return Unit.also {
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 {
callback.onFailure(SharedSecretStorageError.SecretNotEncryptedWithKey(name, key.keyInfo.id))
}
val secretContent = EncryptedSecretContent.fromJson(encryptedForKey)
?: return Unit.also {
callback.onFailure(SharedSecretStorageError.ParsingError)
}
val algorithm = key.keyInfo.content
if (ALGORITHM_CURVE25519_AES_SHA2 == algorithm.algorithm) {
val keySpec = secretKey as? Curve25519AesSha2KeySpec ?: return Unit.also {
callback.onFailure(SharedSecretStorageError.BadKeyFormat)
}
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
kotlin.runCatching {
// decryt from recovery key
val keyBytes = keySpec.privateKey
val decryption = OlmPkDecryption()
try {
decryption.setPrivateKey(keyBytes)
decryption.decrypt(OlmPkMessage().apply {
mCipherText = secretContent.ciphertext
mEphemeralKey = secretContent.ephemeral
mMac = secretContent.mac
})
} catch (failure: Throwable) {
throw failure
} finally {
decryption.releaseDecryption()
}
}.foldToCallback(callback)
}
} else {
callback.onFailure(SharedSecretStorageError.UnsupportedAlgorithm(algorithm.algorithm ?: ""))
}
}
companion object {
const val KEY_ID_BASE = "m.secret_storage.key"
const val ENCRYPTED = "encrypted"
const val DEFAULT_KEY_ID = "m.secret_storage.default_key"
const val ALGORITHM_CURVE25519_AES_SHA2 = "m.secret_storage.v1.curve25519-aes-sha2"
fun withOlmEncryption(block: (OlmPkEncryption) -> Unit) {
val olmPkEncryption = OlmPkEncryption()
try {
block(olmPkEncryption)
} catch (failure: Throwable) {
throw failure
} finally {
olmPkEncryption.releaseEncryption()
}
}
fun withOlmDecryption(block: (OlmPkDecryption) -> Unit) {
val olmPkDecryption = OlmPkDecryption()
try {
block(olmPkDecryption)
} catch (failure: Throwable) {
throw failure
} finally {
olmPkDecryption.releaseDecryption()
}
}
}
}

View File

@ -1,39 +0,0 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* 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 im.vector.matrix.android.internal.crypto.secrets
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageService
internal class DefaultSharedSecureStorage : SharedSecretStorageService {
override fun addKey(algorithm: String, opts: Map<String, Any>, keyId: String, callback: MatrixCallback<String>) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun hasKey(keyId: String): Boolean {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun storeSecret(name: String, secretBase64: String, keys: List<String>?, callback: MatrixCallback<Unit>) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun getSecret(name: String, keyId: String, privateKey: String): String {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
}

View File

@ -23,7 +23,7 @@ import im.vector.matrix.android.internal.network.parsing.UriMoshiAdapter
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataBreadcrumbs
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataDirectMessages
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataFallback
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataIgnoredUsers
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataPushRules
@ -31,7 +31,7 @@ object MoshiProvider {
private val moshi: Moshi = Moshi.Builder()
.add(UriMoshiAdapter())
.add(RuntimeJsonAdapterFactory.of(UserAccountData::class.java, "type", UserAccountDataFallback::class.java)
.add(RuntimeJsonAdapterFactory.of(UserAccountData::class.java, "type", UserAccountDataEvent::class.java)
.registerSubtype(UserAccountDataDirectMessages::class.java, UserAccountData.TYPE_DIRECT_MESSAGES)
.registerSubtype(UserAccountDataIgnoredUsers::class.java, UserAccountData.TYPE_IGNORED_USER_LIST)
.registerSubtype(UserAccountDataPushRules::class.java, UserAccountData.TYPE_PUSH_RULES)

View File

@ -38,6 +38,7 @@ import im.vector.matrix.android.api.session.pushers.PushersService
import im.vector.matrix.android.api.session.room.RoomDirectoryService
import im.vector.matrix.android.api.session.room.RoomService
import im.vector.matrix.android.api.session.securestorage.SecureStorageService
import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageService
import im.vector.matrix.android.api.session.signout.SignOutService
import im.vector.matrix.android.api.session.sync.FilterService
import im.vector.matrix.android.api.session.sync.SyncState
@ -93,6 +94,7 @@ internal class DefaultSession @Inject constructor(
private val initialSyncProgressService: Lazy<InitialSyncProgressService>,
private val homeServerCapabilitiesService: Lazy<HomeServerCapabilitiesService>,
private val accountDataService: Lazy<AccountDataService>,
private val _sharedSecretStorageService: Lazy<SharedSecretStorageService>,
private val shieldTrustUpdater: ShieldTrustUpdater)
: Session,
RoomService by roomService.get(),
@ -111,6 +113,9 @@ internal class DefaultSession @Inject constructor(
ProfileService by profileService.get(),
AccountDataService by accountDataService.get() {
override val sharedSecretStorageService: SharedSecretStorageService
get() = _sharedSecretStorageService.get()
private var isOpen = false
private var syncThread: SyncThread? = null

View File

@ -35,6 +35,8 @@ import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.accountdata.AccountDataService
import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilitiesService
import im.vector.matrix.android.api.session.securestorage.SecureStorageService
import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageService
import im.vector.matrix.android.internal.crypto.secrets.DefaultSharedSecureStorage
import im.vector.matrix.android.internal.crypto.verification.VerificationMessageLiveObserver
import im.vector.matrix.android.internal.database.LiveEntityObserver
import im.vector.matrix.android.internal.database.SessionRealmConfigurationFactory
@ -268,4 +270,7 @@ internal abstract class SessionModule {
@Binds
abstract fun bindAccountDataServiceService(accountDataService: DefaultAccountDataService): AccountDataService
@Binds
abstract fun bindSharedSecuredSecretStorageService(service: DefaultSharedSecureStorage): SharedSecretStorageService
}

View File

@ -20,7 +20,7 @@ import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class UserAccountDataFallback(
data class UserAccountDataEvent(
@Json(name = "type") override val type: String,
@Json(name = "content") val content: Map<String, Any>
) : UserAccountData()

View File

@ -28,8 +28,7 @@ import im.vector.matrix.android.internal.database.model.UserAccountDataEntity
import im.vector.matrix.android.internal.database.model.UserAccountDataEntityFields
import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.matrix.android.internal.di.SessionId
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataFallback
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith
import javax.inject.Inject
@ -44,17 +43,17 @@ internal class DefaultAccountDataService @Inject constructor(
private val moshi = MoshiProvider.providesMoshi()
private val adapter = moshi.adapter<Map<String, Any>>(JSON_DICT_PARAMETERIZED_TYPE)
override fun getAccountData(type: String): UserAccountData? {
override fun getAccountData(type: String): UserAccountDataEvent? {
return getAccountData(listOf(type)).firstOrNull()
}
override fun getLiveAccountData(type: String): LiveData<Optional<UserAccountData>> {
override fun getLiveAccountData(type: String): LiveData<Optional<UserAccountDataEvent>> {
return Transformations.map(getLiveAccountData(listOf(type))) {
it.firstOrNull()?.toOptional()
}
}
override fun getAccountData(filterType: List<String>): List<UserAccountData> {
override fun getAccountData(filterType: List<String>): List<UserAccountDataEvent> {
return monarchy.fetchAllCopiedSync { realm ->
realm.where(UserAccountDataEntity::class.java)
.apply {
@ -64,7 +63,7 @@ internal class DefaultAccountDataService @Inject constructor(
}
}?.mapNotNull { entity ->
entity.type?.let { type ->
UserAccountDataFallback(
UserAccountDataEvent(
type = type,
content = entity.contentStr?.let { adapter.fromJson(it) } ?: emptyMap()
)
@ -72,7 +71,7 @@ internal class DefaultAccountDataService @Inject constructor(
} ?: emptyList()
}
override fun getLiveAccountData(filterType: List<String>): LiveData<List<UserAccountData>> {
override fun getLiveAccountData(filterType: List<String>): LiveData<List<UserAccountDataEvent>> {
return monarchy.findAllMappedWithChanges({ realm ->
realm.where(UserAccountDataEntity::class.java)
.apply {
@ -81,7 +80,7 @@ internal class DefaultAccountDataService @Inject constructor(
}
}
}, { entity ->
UserAccountDataFallback(
UserAccountDataEvent(
type = entity.type ?: "",
content = entity.contentStr?.let { adapter.fromJson(it) } ?: emptyMap()
)

View File

@ -22,7 +22,7 @@ import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataFallback
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent
import im.vector.riotx.R
import im.vector.riotx.core.extensions.configureWith
import im.vector.riotx.core.platform.VectorBaseActivity
@ -55,9 +55,9 @@ class AccountDataFragment @Inject constructor(
}
override fun didTap(data: UserAccountData) {
val fb = data as? UserAccountDataFallback ?: return
val fb = data as? UserAccountDataEvent ?: return
val jsonString = MoshiProvider.providesMoshi()
.adapter(UserAccountDataFallback::class.java)
.adapter(UserAccountDataEvent::class.java)
.toJson(fb)
JsonViewerBottomSheetDialog.newInstance(jsonString)
.show(childFragmentManager, "JSON_VIEWER")