Merge branch 'develop' into feature/attachment_process

This commit is contained in:
Benoit Marty 2020-02-17 13:48:21 +01:00 committed by GitHub
commit 13d3aa9ff1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
57 changed files with 1897 additions and 85 deletions

View file

@ -1,7 +1,8 @@
Changes in RiotX 0.16.0 (2020-XX-XX) Changes in RiotX 0.17.0 (2020-XX-XX)
=================================================== ===================================================
Features ✨: Features ✨:
- Secured Shared Storage Support (#984, #936)
- Polls and Bot Buttons (MSC 2192 matrix-org/matrix-doc#2192) - Polls and Bot Buttons (MSC 2192 matrix-org/matrix-doc#2192)
- It's now possible to select several rooms (with a possible mix of clear/encrypted rooms) when sharing elements to RiotX (#1010) - It's now possible to select several rooms (with a possible mix of clear/encrypted rooms) when sharing elements to RiotX (#1010)
- Media preview: media are previewed before being sent to a room (#1010) - Media preview: media are previewed before being sent to a room (#1010)
@ -9,19 +10,16 @@ Features ✨:
- Sending image: image are sent to rooms with a reduced size. It's still possible to send original image file (#1010) - Sending image: image are sent to rooms with a reduced size. It's still possible to send original image file (#1010)
Improvements 🙌: Improvements 🙌:
- Show confirmation dialog before deleting a message (#967) -
- Open room member profile from reactions list and read receipts list (#875)
Bugfix 🐛: Bugfix 🐛:
- Fix crash by removing all notifications after clearing cache (#878) - Account creation: wrongly hints that an email can be used to create an account (#941)
- Fix issue with verification when other client declares it can only show QR code (#988)
Translations 🗣: Translations 🗣:
- -
SDK API changes 🔞: SDK API changes ⚠️:
- Javadoc improved for PushersService -
- PushersService.pushers() has been renamed to PushersService.getPushers()
Build 🧱: Build 🧱:
- -
@ -29,6 +27,25 @@ Build 🧱:
Other changes: Other changes:
- -
Changes in RiotX 0.16.0 (2020-02-14)
===================================================
Features ✨:
- Polls and Bot Buttons (MSC 2192 matrix-org/matrix-doc#2192)
Improvements 🙌:
- Show confirmation dialog before deleting a message (#967, #1003)
- Open room member profile from reactions list and read receipts list (#875)
Bugfix 🐛:
- Fix crash by removing all notifications after clearing cache (#878)
- Fix issue with verification when other client declares it can only show QR code (#988)
- Fix too errors in the code (1941862499c9ec5268cc80882512ced379cafcfd, a250a895fe0a4acf08c671e03434edcd29ccd84f)
SDK API changes ⚠️:
- Javadoc improved for PushersService
- PushersService.pushers() has been renamed to PushersService.getPushers()
Changes in RiotX 0.15.0 (2020-02-10) Changes in RiotX 0.15.0 (2020-02-10)
=================================================== ===================================================
@ -400,7 +417,7 @@ Bugfix 🐛:
Translations 🗣: Translations 🗣:
- -
SDK API changes 🔞: SDK API changes ⚠️:
- -
Build 🧱: Build 🧱:
@ -408,4 +425,3 @@ Build 🧱:
Other changes: Other changes:
- -

View file

@ -36,6 +36,8 @@ allprojects {
includeGroupByRegex "com\\.github\\.Zhuinden" includeGroupByRegex "com\\.github\\.Zhuinden"
// And ucrop // And ucrop
includeGroupByRegex "com\\.github\\.yalantis" includeGroupByRegex "com\\.github\\.yalantis"
// JsonViewer
includeGroupByRegex 'com\\.github\\.BillCarsonFr'
} }
} }
maven { maven {

View file

@ -31,6 +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.Optional
import im.vector.matrix.android.api.util.toOptional import im.vector.matrix.android.api.util.toOptional
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.Single import io.reactivex.Single
@ -121,6 +122,13 @@ class RxSession(private val session: Session) {
session.getCrossSigningService().getUserCrossSigningKeys(userId).toOptional() session.getCrossSigningService().getUserCrossSigningKeys(userId).toOptional()
} }
} }
fun liveAccountData(filter: List<String>): Observable<List<UserAccountDataEvent>> {
return session.getLiveAccountDataEvents(filter).asObservable()
.startWithCallable {
session.getAccountDataEvents(filter)
}
}
} }
fun Session.rx(): RxSession { fun Session.rx(): RxSession {

View file

@ -0,0 +1,438 @@
/*
* 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.SSSS_ALGORITHM_CURVE25519_AES_SHA2
import im.vector.matrix.android.internal.crypto.crosssigning.toBase64NoPadding
import im.vector.matrix.android.internal.crypto.secrets.DefaultSharedSecretStorageService
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.getLiveAccountDataEvent("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", SSSS_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!!)
DefaultSharedSecretStorageService.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.getLiveAccountDataEvent(DefaultSharedSecretStorageService.DEFAULT_KEY_ID)
}
val accountDefDataObserver = Observer<Optional<UserAccountDataEvent>?> { t ->
if (t?.getOrNull()?.type == DefaultSharedSecretStorageService.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!!,
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)
}
@Test
fun test_SetDefaultLocalEcho() {
val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
val quadS = aliceSession.sharedSecretStorageService
val emptyKeySigner = object : KeySigner {
override fun sign(canonicalJson: String): Map<String, Map<String, String>>? {
return null
}
}
val TEST_KEY_ID = "my.test.Key"
val countDownLatch = CountDownLatch(1)
quadS.generateKey(TEST_KEY_ID, "Test Key", emptyKeySigner,
TestMatrixCallback(countDownLatch))
mTestHelper.await(countDownLatch)
// Test that we don't need to wait for an account data sync to access directly the keyid from DB
val defaultLatch = CountDownLatch(1)
quadS.setDefaultKey(TEST_KEY_ID, TestMatrixCallback(defaultLatch))
mTestHelper.await(defaultLatch)
mTestHelper.signout(aliceSession)
}
@Test
fun test_StoreSecretWithMultipleKey() {
val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
val keyId1 = "Key.1"
val key1Info = generatedSecret(aliceSession, keyId1, true)
val keyId2 = "Key2"
val key2Info = generatedSecret(aliceSession, keyId2, true)
val mySecretText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit"
val storeLatch = CountDownLatch(1)
aliceSession.sharedSecretStorageService.storeSecret(
"my.secret",
mySecretText.toByteArray().toBase64NoPadding(),
listOf(keyId1, keyId2),
TestMatrixCallback(storeLatch)
)
mTestHelper.await(storeLatch)
val accountDataEvent = aliceSession.getAccountDataEvent("my.secret")
val encryptedContent = accountDataEvent?.content?.get("encrypted") as? Map<*, *>
Assert.assertEquals("Content should contains two encryptions", 2, encryptedContent?.keys?.size ?: 0)
Assert.assertNotNull(encryptedContent?.get(keyId1))
Assert.assertNotNull(encryptedContent?.get(keyId2))
// Assert that can decrypt with both keys
val decryptCountDownLatch = CountDownLatch(2)
aliceSession.sharedSecretStorageService.getSecret("my.secret",
keyId1,
Curve25519AesSha2KeySpec.fromRecoveryKey(key1Info.recoveryKey)!!,
TestMatrixCallback(decryptCountDownLatch)
)
aliceSession.sharedSecretStorageService.getSecret("my.secret",
keyId2,
Curve25519AesSha2KeySpec.fromRecoveryKey(key2Info.recoveryKey)!!,
TestMatrixCallback(decryptCountDownLatch)
)
mTestHelper.await(decryptCountDownLatch)
mTestHelper.signout(aliceSession)
}
@Test
fun test_GetSecretWithBadPassphrase() {
val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
val keyId1 = "Key.1"
val passphrase = "The good pass phrase"
val key1Info = generatedSecretFromPassphrase(aliceSession, passphrase, keyId1, true)
val mySecretText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit"
val storeLatch = CountDownLatch(1)
aliceSession.sharedSecretStorageService.storeSecret(
"my.secret",
mySecretText.toByteArray().toBase64NoPadding(),
listOf(keyId1),
TestMatrixCallback(storeLatch)
)
mTestHelper.await(storeLatch)
val decryptCountDownLatch = CountDownLatch(2)
aliceSession.sharedSecretStorageService.getSecret("my.secret",
keyId1,
Curve25519AesSha2KeySpec.fromPassphrase(
"A bad passphrase",
key1Info.content?.passphrase?.salt ?: "",
key1Info.content?.passphrase?.iterations ?: 0,
null),
object : MatrixCallback<String> {
override fun onSuccess(data: String) {
decryptCountDownLatch.countDown()
fail("Should not be able to decrypt")
}
override fun onFailure(failure: Throwable) {
Assert.assertTrue(true)
decryptCountDownLatch.countDown()
}
}
)
// Now try with correct key
aliceSession.sharedSecretStorageService.getSecret("my.secret",
keyId1,
Curve25519AesSha2KeySpec.fromPassphrase(
passphrase,
key1Info.content?.passphrase?.salt ?: "",
key1Info.content?.passphrase?.iterations ?: 0,
null),
TestMatrixCallback(decryptCountDownLatch)
)
mTestHelper.await(decryptCountDownLatch)
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.getLiveAccountDataEvent(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, DefaultSharedSecretStorageService.DEFAULT_KEY_ID)
}
return creationInfo!!
}
private fun generatedSecretFromPassphrase(session: Session, passphrase: String, 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.generateKeyWithPassphrase(keyId, keyId,
passphrase,
emptyKeySigner,
null,
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, DefaultSharedSecretStorageService.DEFAULT_KEY_ID)
}
return creationInfo!!
}
}

View file

@ -21,6 +21,7 @@ import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.failure.GlobalError import im.vector.matrix.android.api.failure.GlobalError
import im.vector.matrix.android.api.pushrules.PushRuleService import im.vector.matrix.android.api.pushrules.PushRuleService
import im.vector.matrix.android.api.session.accountdata.AccountDataService
import im.vector.matrix.android.api.session.cache.CacheService import im.vector.matrix.android.api.session.cache.CacheService
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
import im.vector.matrix.android.api.session.content.ContentUrlResolver import im.vector.matrix.android.api.session.content.ContentUrlResolver
@ -33,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.RoomDirectoryService
import im.vector.matrix.android.api.session.room.RoomService 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.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.signout.SignOutService
import im.vector.matrix.android.api.session.sync.FilterService import im.vector.matrix.android.api.session.sync.FilterService
import im.vector.matrix.android.api.session.sync.SyncState import im.vector.matrix.android.api.session.sync.SyncState
@ -57,7 +59,8 @@ interface Session :
PushersService, PushersService,
InitialSyncProgressService, InitialSyncProgressService,
HomeServerCapabilitiesService, HomeServerCapabilitiesService,
SecureStorageService { SecureStorageService,
AccountDataService {
/** /**
* The params associated to the session * The params associated to the session
@ -159,4 +162,6 @@ interface Session :
*/ */
fun onGlobalError(globalError: GlobalError) fun onGlobalError(globalError: GlobalError)
} }
val sharedSecretStorageService: SharedSecretStorageService
} }

View file

@ -0,0 +1,36 @@
/*
* 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.accountdata
import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.events.model.Content
import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent
interface AccountDataService {
fun getAccountDataEvent(type: String): UserAccountDataEvent?
fun getLiveAccountDataEvent(type: String): LiveData<Optional<UserAccountDataEvent>>
fun getAccountDataEvents(filterType: List<String>): List<UserAccountDataEvent>
fun getLiveAccountDataEvents(filterType: List<String>): LiveData<List<UserAccountDataEvent>>
fun updateAccountData(type: String, content: Content, callback: MatrixCallback<Unit>? = null)
}

View file

@ -157,6 +157,11 @@ data class Event(
*/ */
fun isRedacted() = unsignedData?.redactedEvent != null fun isRedacted() = unsignedData?.redactedEvent != null
/**
* Tells if the event is redacted by the user himself.
*/
fun isRedactedBySameUser() = senderId == unsignedData?.redactedEvent?.senderId
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (javaClass != other?.javaClass) return false if (javaClass != other?.javaClass) return false

View file

@ -66,6 +66,9 @@ object EventType {
const val ROOM_KEY_REQUEST = "m.room_key_request" const val ROOM_KEY_REQUEST = "m.room_key_request"
const val FORWARDED_ROOM_KEY = "m.forwarded_room_key" const val FORWARDED_ROOM_KEY = "m.forwarded_room_key"
const val REQUEST_SECRET = "m.secret.request"
const val SEND_SECRET = "m.secret.send"
// Interactive key verification // Interactive key verification
const val KEY_VERIFICATION_START = "m.key.verification.start" const val KEY_VERIFICATION_START = "m.key.verification.start"
const val KEY_VERIFICATION_ACCEPT = "m.key.verification.accept" const val KEY_VERIFICATION_ACCEPT = "m.key.verification.accept"

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,100 @@
/*
* 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,32 @@
/*
* 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

@ -0,0 +1,112 @@
/*
* Copyright 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.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.
* Clients may also wish to securely send such data directly to each other.
* For example, key backups (MSC1219) can store the decryption key for the backups on the server, or cross-signing (MSC1756) can store the signing keys.
*
* https://github.com/matrix-org/matrix-doc/pull/1946
*
*/
interface SharedSecretStorageService {
/**
* Generates a SSSS key for encrypting secrets.
* Use the SsssKeyCreationInfo object returned by the callback to get more information about the created key (recovery key ...)
*
* @param keyId the ID of the key
* @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 callback Get key creation info
*/
fun generateKey(keyId: String,
keyName: String,
keySigner: KeySigner,
callback: MatrixCallback<SsssKeyCreationInfo>)
/**
* Generates a SSSS key using the given passphrase.
* Use the SsssKeyCreationInfo object returned by the callback to get more information about the created key (recovery key, salt, iteration ...)
*
* @param keyId the ID of the key
* @param keyName human readable key name
* @param passphrase The passphrase used to generate the key
* @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 callback Get key creation info
*/
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.
*
* @param keyId The ID of the key to check
* @return Whether we have the key.
*/
fun hasKey(keyId: String): Boolean
/**
* 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 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 (null for default key)
* @param secretKey the secret key to use (@see #Curve25519AesSha2KeySpec)
*
*/
@Throws
fun getSecret(name: String, keyId: String?, secretKey: SSSSKeySpec, callback: MatrixCallback<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,66 @@
/*
* 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

@ -31,6 +31,11 @@ const val MXCRYPTO_ALGORITHM_MEGOLM = "m.megolm.v1.aes-sha2"
*/ */
const val MXCRYPTO_ALGORITHM_MEGOLM_BACKUP = "m.megolm_backup.v1.curve25519-aes-sha2" const val MXCRYPTO_ALGORITHM_MEGOLM_BACKUP = "m.megolm_backup.v1.curve25519-aes-sha2"
/**
* Secured Shared Storage algorithm constant
*/
const val SSSS_ALGORITHM_CURVE25519_AES_SHA2 = "m.secret_storage.v1.curve25519-aes-sha2"
// TODO Refacto: use this constants everywhere // TODO Refacto: use this constants everywhere
const val ed25519 = "ed25519" const val ed25519 = "ed25519"
const val curve25519 = "curve25519" const val curve25519 = "curve25519"

View file

@ -637,11 +637,11 @@ internal class DefaultCrossSigningService @Inject constructor(
// In this case it will change my MSK trust, and should then re-trigger a check of all other user trust // In this case it will change my MSK trust, and should then re-trigger a check of all other user trust
setUserKeysAsTrusted(otherUserId, checkSelfTrust().isVerified()) setUserKeysAsTrusted(otherUserId, checkSelfTrust().isVerified())
} }
}
eventBus.post(CryptoToSessionUserTrustChange(userIds)) eventBus.post(CryptoToSessionUserTrustChange(userIds))
} }
} }
}
private fun setUserKeysAsTrusted(otherUserId: String, trusted: Boolean) { private fun setUserKeysAsTrusted(otherUserId: String, trusted: Boolean) {
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {

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.KeysBackupVersionTrustSignature
import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupAuthData 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.MegolmBackupCreationInfo
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.* import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.BackupKeysResult
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.* 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.computeRecoveryKey
import im.vector.matrix.android.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey import im.vector.matrix.android.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult

View file

@ -83,7 +83,7 @@ fun retrievePrivateKeyWithPassword(password: String,
* @return a private key. * @return a private key.
*/ */
@WorkerThread @WorkerThread
private fun deriveKey(password: String, fun deriveKey(password: String,
salt: String, salt: String,
iterations: Int, iterations: Int,
progressListener: ProgressListener?): ByteArray { progressListener: ProgressListener?): ByteArray {

View file

@ -0,0 +1,358 @@
/*
* 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.events.model.toContent
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.SSSS_ALGORITHM_CURVE25519_AES_SHA2
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 DefaultSharedSecretStorageService @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 = SSSS_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.toContent(),
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 = SSSS_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.toContent(),
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.getAccountDataEvent("$KEY_ID_BASE.$keyId") != null
}
override fun getKey(keyId: String): KeyInfoResult {
val accountData = accountDataService.getAccountDataEvent("$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.getAccountDataEvent(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 == SSSS_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 == SSSS_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,
content = 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.getAccountDataEvent(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.getAccountDataEvent(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 (SSSS_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"
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

@ -53,6 +53,7 @@ import io.realm.annotations.RealmModule
DraftEntity::class, DraftEntity::class,
HomeServerCapabilitiesEntity::class, HomeServerCapabilitiesEntity::class,
RoomMemberSummaryEntity::class, RoomMemberSummaryEntity::class,
CurrentStateEventEntity::class CurrentStateEventEntity::class,
UserAccountDataEntity::class
]) ])
internal class SessionRealmModule internal class SessionRealmModule

View file

@ -0,0 +1,34 @@
/*
* 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.database.model
import io.realm.RealmObject
import io.realm.annotations.Index
/**
* Clients can store custom config data for their account on their HomeServer.
* This account data will be synced between different devices and can persist across installations on a particular device.
* Users may only view the account data for their own account.
* The account_data may be either global or scoped to a particular rooms.
*/
internal open class UserAccountDataEntity(
@Index var type: String? = null,
var contentStr: String? = null
) : RealmObject() {
companion object
}

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.UserAccountData
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataBreadcrumbs 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.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.UserAccountDataIgnoredUsers
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataPushRules import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataPushRules
@ -31,7 +31,7 @@ object MoshiProvider {
private val moshi: Moshi = Moshi.Builder() private val moshi: Moshi = Moshi.Builder()
.add(UriMoshiAdapter()) .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(UserAccountDataDirectMessages::class.java, UserAccountData.TYPE_DIRECT_MESSAGES)
.registerSubtype(UserAccountDataIgnoredUsers::class.java, UserAccountData.TYPE_IGNORED_USER_LIST) .registerSubtype(UserAccountDataIgnoredUsers::class.java, UserAccountData.TYPE_IGNORED_USER_LIST)
.registerSubtype(UserAccountDataPushRules::class.java, UserAccountData.TYPE_PUSH_RULES) .registerSubtype(UserAccountDataPushRules::class.java, UserAccountData.TYPE_PUSH_RULES)

View file

@ -25,6 +25,7 @@ import im.vector.matrix.android.api.failure.GlobalError
import im.vector.matrix.android.api.pushrules.PushRuleService import im.vector.matrix.android.api.pushrules.PushRuleService
import im.vector.matrix.android.api.session.InitialSyncProgressService import im.vector.matrix.android.api.session.InitialSyncProgressService
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.accountdata.AccountDataService
import im.vector.matrix.android.api.session.cache.CacheService import im.vector.matrix.android.api.session.cache.CacheService
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
import im.vector.matrix.android.api.session.content.ContentUrlResolver import im.vector.matrix.android.api.session.content.ContentUrlResolver
@ -37,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.RoomDirectoryService
import im.vector.matrix.android.api.session.room.RoomService 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.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.signout.SignOutService
import im.vector.matrix.android.api.session.sync.FilterService import im.vector.matrix.android.api.session.sync.FilterService
import im.vector.matrix.android.api.session.sync.SyncState import im.vector.matrix.android.api.session.sync.SyncState
@ -91,6 +93,8 @@ internal class DefaultSession @Inject constructor(
private val contentUploadProgressTracker: ContentUploadStateTracker, private val contentUploadProgressTracker: ContentUploadStateTracker,
private val initialSyncProgressService: Lazy<InitialSyncProgressService>, private val initialSyncProgressService: Lazy<InitialSyncProgressService>,
private val homeServerCapabilitiesService: Lazy<HomeServerCapabilitiesService>, private val homeServerCapabilitiesService: Lazy<HomeServerCapabilitiesService>,
private val accountDataService: Lazy<AccountDataService>,
private val _sharedSecretStorageService: Lazy<SharedSecretStorageService>,
private val shieldTrustUpdater: ShieldTrustUpdater) private val shieldTrustUpdater: ShieldTrustUpdater)
: Session, : Session,
RoomService by roomService.get(), RoomService by roomService.get(),
@ -106,7 +110,11 @@ internal class DefaultSession @Inject constructor(
InitialSyncProgressService by initialSyncProgressService.get(), InitialSyncProgressService by initialSyncProgressService.get(),
SecureStorageService by secureStorageService.get(), SecureStorageService by secureStorageService.get(),
HomeServerCapabilitiesService by homeServerCapabilitiesService.get(), HomeServerCapabilitiesService by homeServerCapabilitiesService.get(),
ProfileService by profileService.get() { ProfileService by profileService.get(),
AccountDataService by accountDataService.get() {
override val sharedSecretStorageService: SharedSecretStorageService
get() = _sharedSecretStorageService.get()
private var isOpen = false private var isOpen = false

View file

@ -32,8 +32,11 @@ import im.vector.matrix.android.api.auth.data.sessionId
import im.vector.matrix.android.api.crypto.MXCryptoConfig import im.vector.matrix.android.api.crypto.MXCryptoConfig
import im.vector.matrix.android.api.session.InitialSyncProgressService import im.vector.matrix.android.api.session.InitialSyncProgressService
import im.vector.matrix.android.api.session.Session 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.homeserver.HomeServerCapabilitiesService
import im.vector.matrix.android.api.session.securestorage.SecureStorageService 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.DefaultSharedSecretStorageService
import im.vector.matrix.android.internal.crypto.verification.VerificationMessageLiveObserver import im.vector.matrix.android.internal.crypto.verification.VerificationMessageLiveObserver
import im.vector.matrix.android.internal.database.LiveEntityObserver import im.vector.matrix.android.internal.database.LiveEntityObserver
import im.vector.matrix.android.internal.database.SessionRealmConfigurationFactory import im.vector.matrix.android.internal.database.SessionRealmConfigurationFactory
@ -61,6 +64,7 @@ import im.vector.matrix.android.internal.session.room.create.RoomCreateEventLive
import im.vector.matrix.android.internal.session.room.prune.EventsPruner import im.vector.matrix.android.internal.session.room.prune.EventsPruner
import im.vector.matrix.android.internal.session.room.tombstone.RoomTombstoneEventLiveObserver import im.vector.matrix.android.internal.session.room.tombstone.RoomTombstoneEventLiveObserver
import im.vector.matrix.android.internal.session.securestorage.DefaultSecureStorageService import im.vector.matrix.android.internal.session.securestorage.DefaultSecureStorageService
import im.vector.matrix.android.internal.session.user.accountdata.DefaultAccountDataService
import im.vector.matrix.android.internal.util.md5 import im.vector.matrix.android.internal.util.md5
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@ -263,4 +267,10 @@ internal abstract class SessionModule {
@Binds @Binds
abstract fun bindHomeServerCapabilitiesService(homeServerCapabilitiesService: DefaultHomeServerCapabilitiesService): HomeServerCapabilitiesService abstract fun bindHomeServerCapabilitiesService(homeServerCapabilitiesService: DefaultHomeServerCapabilitiesService): HomeServerCapabilitiesService
@Binds
abstract fun bindAccountDataService(accountDataService: DefaultAccountDataService): AccountDataService
@Binds
abstract fun bindSharedSecretStorageService(service: DefaultSharedSecretStorageService): SharedSecretStorageService
} }

View file

@ -19,9 +19,12 @@ package im.vector.matrix.android.internal.session.sync
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.pushrules.RuleScope import im.vector.matrix.android.api.pushrules.RuleScope
import im.vector.matrix.android.api.pushrules.RuleSetKey import im.vector.matrix.android.api.pushrules.RuleSetKey
import im.vector.matrix.android.api.session.events.model.Content
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.RoomMemberContent import im.vector.matrix.android.api.session.room.model.RoomMemberContent
import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.internal.database.mapper.ContentMapper
import im.vector.matrix.android.internal.database.mapper.PushRulesMapper import im.vector.matrix.android.internal.database.mapper.PushRulesMapper
import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.BreadcrumbsEntity import im.vector.matrix.android.internal.database.model.BreadcrumbsEntity
@ -29,15 +32,18 @@ import im.vector.matrix.android.internal.database.model.IgnoredUserEntity
import im.vector.matrix.android.internal.database.model.PushRulesEntity import im.vector.matrix.android.internal.database.model.PushRulesEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields
import im.vector.matrix.android.internal.database.model.UserAccountDataEntity
import im.vector.matrix.android.internal.database.model.UserAccountDataEntityFields
import im.vector.matrix.android.internal.database.query.getDirectRooms import im.vector.matrix.android.internal.database.query.getDirectRooms
import im.vector.matrix.android.internal.database.query.getOrCreate import im.vector.matrix.android.internal.database.query.getOrCreate
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.session.room.membership.RoomMemberHelper import im.vector.matrix.android.internal.session.room.membership.RoomMemberHelper
import im.vector.matrix.android.internal.session.sync.model.InvitedRoomSync import im.vector.matrix.android.internal.session.sync.model.InvitedRoomSync
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.UserAccountDataBreadcrumbs
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataDirectMessages 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.UserAccountDataIgnoredUsers import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataIgnoredUsers
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataPushRules import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataPushRules
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataSync import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataSync
@ -45,6 +51,7 @@ import im.vector.matrix.android.internal.session.user.accountdata.DirectChatsHel
import im.vector.matrix.android.internal.session.user.accountdata.UpdateUserAccountDataTask import im.vector.matrix.android.internal.session.user.accountdata.UpdateUserAccountDataTask
import io.realm.Realm import io.realm.Realm
import io.realm.RealmList import io.realm.RealmList
import io.realm.kotlin.where
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -56,21 +63,23 @@ internal class UserAccountDataSyncHandler @Inject constructor(
fun handle(realm: Realm, accountData: UserAccountDataSync?) { fun handle(realm: Realm, accountData: UserAccountDataSync?) {
accountData?.list?.forEach { accountData?.list?.forEach {
when (it) { // Generic handling, just save in base
is UserAccountDataDirectMessages -> handleDirectChatRooms(realm, it) handleGenericAccountData(realm, it.type, it.content)
is UserAccountDataPushRules -> handlePushRules(realm, it)
is UserAccountDataIgnoredUsers -> handleIgnoredUsers(realm, it)
is UserAccountDataBreadcrumbs -> handleBreadcrumbs(realm, it)
is UserAccountDataFallback -> Timber.d("Receive account data of unhandled type ${it.type}")
else -> error("Missing code here!")
}
}
// TODO Store all account data, app can be interested of it // Didn't want to break too much thing, so i re-serialize to jsonString before reparsing
// accountData?.list?.forEach { // TODO would be better to have a mapper?
// it.toString() val toJson = MoshiProvider.providesMoshi().adapter(Event::class.java).toJson(it)
// MoshiProvider.providesMoshi() val model = toJson?.let { json ->
// } MoshiProvider.providesMoshi().adapter(UserAccountData::class.java).fromJson(json)
}
// Specific parsing
when (model) {
is UserAccountDataDirectMessages -> handleDirectChatRooms(realm, model)
is UserAccountDataPushRules -> handlePushRules(realm, model)
is UserAccountDataIgnoredUsers -> handleIgnoredUsers(realm, model)
is UserAccountDataBreadcrumbs -> handleBreadcrumbs(realm, model)
}
}
} }
// If we get some direct chat invites, we synchronize the user account data including those. // If we get some direct chat invites, we synchronize the user account data including those.
@ -200,4 +209,19 @@ internal class UserAccountDataSyncHandler @Inject constructor(
?.breadcrumbsIndex = index ?.breadcrumbsIndex = index
} }
} }
fun handleGenericAccountData(realm: Realm, type: String, content: Content?) {
val existing = realm.where<UserAccountDataEntity>()
.equalTo(UserAccountDataEntityFields.TYPE, type)
.findFirst()
if (existing != null) {
// Update current value
existing.contentStr = ContentMapper.map(content)
} else {
realm.createObject(UserAccountDataEntity::class.java).let { accountDataEntity ->
accountDataEntity.type = type
accountDataEntity.contentStr = ContentMapper.map(content)
}
}
}
} }

View file

@ -18,7 +18,7 @@ package im.vector.matrix.android.internal.session.sync.model.accountdata
import com.squareup.moshi.Json import com.squareup.moshi.Json
internal abstract class UserAccountData { abstract class UserAccountData {
@Json(name = "type") abstract val type: String @Json(name = "type") abstract val type: String

View file

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

View file

@ -18,8 +18,9 @@ package im.vector.matrix.android.internal.session.sync.model.accountdata
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.session.events.model.Event
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class UserAccountDataSync( internal data class UserAccountDataSync(
@Json(name = "events") val list: List<UserAccountData> = emptyList() @Json(name = "events") val list: List<Event> = emptyList()
) )

View file

@ -0,0 +1,114 @@
/*
* 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.session.user.accountdata
import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.accountdata.AccountDataService
import im.vector.matrix.android.api.session.events.model.Content
import im.vector.matrix.android.api.util.JSON_DICT_PARAMETERIZED_TYPE
import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.api.util.toOptional
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.UserAccountDataSyncHandler
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
internal class DefaultAccountDataService @Inject constructor(
private val monarchy: Monarchy,
@SessionId private val sessionId: String,
private val updateUserAccountDataTask: UpdateUserAccountDataTask,
private val userAccountDataSyncHandler: UserAccountDataSyncHandler,
private val taskExecutor: TaskExecutor
) : AccountDataService {
private val moshi = MoshiProvider.providesMoshi()
private val adapter = moshi.adapter<Map<String, Any>>(JSON_DICT_PARAMETERIZED_TYPE)
override fun getAccountDataEvent(type: String): UserAccountDataEvent? {
return getAccountDataEvents(listOf(type)).firstOrNull()
}
override fun getLiveAccountDataEvent(type: String): LiveData<Optional<UserAccountDataEvent>> {
return Transformations.map(getLiveAccountDataEvents(listOf(type))) {
it.firstOrNull()?.toOptional()
}
}
override fun getAccountDataEvents(filterType: List<String>): List<UserAccountDataEvent> {
return monarchy.fetchAllCopiedSync { realm ->
realm.where(UserAccountDataEntity::class.java)
.apply {
if (filterType.isNotEmpty()) {
`in`(UserAccountDataEntityFields.TYPE, filterType.toTypedArray())
}
}
}?.mapNotNull { entity ->
entity.type?.let { type ->
UserAccountDataEvent(
type = type,
content = entity.contentStr?.let { adapter.fromJson(it) } ?: emptyMap()
)
}
} ?: emptyList()
}
override fun getLiveAccountDataEvents(filterType: List<String>): LiveData<List<UserAccountDataEvent>> {
return monarchy.findAllMappedWithChanges({ realm ->
realm.where(UserAccountDataEntity::class.java)
.apply {
if (filterType.isNotEmpty()) {
`in`(UserAccountDataEntityFields.TYPE, filterType.toTypedArray())
}
}
}, { entity ->
UserAccountDataEvent(
type = entity.type ?: "",
content = entity.contentStr?.let { adapter.fromJson(it) } ?: emptyMap()
)
})
}
override fun updateAccountData(type: String, content: Content, callback: MatrixCallback<Unit>?) {
updateUserAccountDataTask.configureWith(UpdateUserAccountDataTask.AnyParams(
type = type,
any = content
)) {
this.retryCount = 5
this.callback = object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
monarchy.runTransactionSync { realm ->
userAccountDataSyncHandler.handleGenericAccountData(realm, type, content)
}
callback?.onSuccess(data)
}
override fun onFailure(failure: Throwable) {
callback?.onFailure(failure)
}
}
}
.executeBy(taskExecutor)
}
}

View file

@ -49,6 +49,14 @@ internal interface UpdateUserAccountDataTask : Task<UpdateUserAccountDataTask.Pa
return breadcrumbsContent return breadcrumbsContent
} }
} }
data class AnyParams(override val type: String,
private val any: Any
) : Params {
override fun getData(): Any {
return any
}
}
} }
internal class DefaultUpdateUserAccountDataTask @Inject constructor( internal class DefaultUpdateUserAccountDataTask @Inject constructor(

View file

@ -15,7 +15,7 @@ androidExtensions {
} }
ext.versionMajor = 0 ext.versionMajor = 0
ext.versionMinor = 16 ext.versionMinor = 17
ext.versionPatch = 0 ext.versionPatch = 0
static def getGitTimestamp() { static def getGitTimestamp() {
@ -369,6 +369,8 @@ dependencies {
implementation "androidx.emoji:emoji-appcompat:1.0.0" implementation "androidx.emoji:emoji-appcompat:1.0.0"
implementation 'com.github.BillCarsonFr:JsonViewer:0.4'
// QR-code // QR-code
// Stick to 3.3.3 because of https://github.com/zxing/zxing/issues/1170 // Stick to 3.3.3 because of https://github.com/zxing/zxing/issues/1170
implementation 'com.google.zxing:core:3.3.3' implementation 'com.google.zxing:core:3.3.3'

View file

@ -22,6 +22,7 @@
padding: 4px; padding: 4px;
} }
</style> </style>
</head> </head>
<body> <body>
@ -384,6 +385,9 @@ SOFTWARE.
<br/> <br/>
Copyright 2017, Yalantis Copyright 2017, Yalantis
</li> </li>
<li>
<b>BillCarsonFr/JsonViewer</b>
</li>
</ul> </ul>
<pre> <pre>
Apache License Apache License

View file

@ -73,6 +73,7 @@ import im.vector.riotx.features.settings.VectorSettingsPreferencesFragment
import im.vector.riotx.features.settings.VectorSettingsSecurityPrivacyFragment import im.vector.riotx.features.settings.VectorSettingsSecurityPrivacyFragment
import im.vector.riotx.features.settings.crosssigning.CrossSigningSettingsFragment import im.vector.riotx.features.settings.crosssigning.CrossSigningSettingsFragment
import im.vector.riotx.features.settings.devices.VectorSettingsDevicesFragment import im.vector.riotx.features.settings.devices.VectorSettingsDevicesFragment
import im.vector.riotx.features.settings.devtools.AccountDataFragment
import im.vector.riotx.features.settings.ignored.VectorSettingsIgnoredUsersFragment import im.vector.riotx.features.settings.ignored.VectorSettingsIgnoredUsersFragment
import im.vector.riotx.features.settings.push.PushGatewaysFragment import im.vector.riotx.features.settings.push.PushGatewaysFragment
import im.vector.riotx.features.share.IncomingShareFragment import im.vector.riotx.features.share.IncomingShareFragment
@ -360,4 +361,9 @@ interface FragmentModule {
@IntoMap @IntoMap
@FragmentKey(IncomingShareFragment::class) @FragmentKey(IncomingShareFragment::class)
fun bindIncomingShareFragment(fragment: IncomingShareFragment): Fragment fun bindIncomingShareFragment(fragment: IncomingShareFragment): Fragment
@Binds
@IntoMap
@FragmentKey(AccountDataFragment::class)
fun bindAccountDataFragment(fragment: AccountDataFragment): Fragment
} }

View file

@ -18,6 +18,8 @@ package im.vector.riotx.core.utils
import androidx.annotation.ColorRes import androidx.annotation.ColorRes
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.resources.ColorProvider
import org.billcarsonfr.jsonviewer.JSonViewerStyleProvider
import kotlin.math.abs import kotlin.math.abs
@ColorRes @ColorRes
@ -37,3 +39,14 @@ fun getColorFromUserId(userId: String?): Int {
else -> R.color.riotx_username_1 else -> R.color.riotx_username_1
} }
} }
fun jsonViewerStyler(colorProvider: ColorProvider): JSonViewerStyleProvider {
return JSonViewerStyleProvider(
keyColor = colorProvider.getColor(R.color.riotx_accent),
secondaryColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary),
stringColor = colorProvider.getColorFromAttribute(R.attr.vctr_notice_text_color),
baseColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_primary),
booleanColor = colorProvider.getColorFromAttribute(R.attr.vctr_notice_text_color),
numberColor = colorProvider.getColorFromAttribute(R.attr.vctr_notice_text_color)
)
}

View file

@ -27,12 +27,10 @@ import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.text.Spannable import android.text.Spannable
import android.view.HapticFeedbackConstants import android.view.HapticFeedbackConstants
import android.view.LayoutInflater
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.Window import android.view.Window
import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
@ -96,6 +94,7 @@ import im.vector.riotx.core.extensions.showKeyboard
import im.vector.riotx.core.files.addEntryToDownloadManager import im.vector.riotx.core.files.addEntryToDownloadManager
import im.vector.riotx.core.glide.GlideApp import im.vector.riotx.core.glide.GlideApp
import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.core.ui.views.JumpToReadMarkerView import im.vector.riotx.core.ui.views.JumpToReadMarkerView
import im.vector.riotx.core.ui.views.NotificationAreaView import im.vector.riotx.core.ui.views.NotificationAreaView
import im.vector.riotx.core.utils.Debouncer import im.vector.riotx.core.utils.Debouncer
@ -110,6 +109,7 @@ import im.vector.riotx.core.utils.checkPermissions
import im.vector.riotx.core.utils.copyToClipboard import im.vector.riotx.core.utils.copyToClipboard
import im.vector.riotx.core.utils.createUIHandler import im.vector.riotx.core.utils.createUIHandler
import im.vector.riotx.core.utils.getColorFromUserId import im.vector.riotx.core.utils.getColorFromUserId
import im.vector.riotx.core.utils.jsonViewerStyler
import im.vector.riotx.core.utils.openUrlInExternalBrowser import im.vector.riotx.core.utils.openUrlInExternalBrowser
import im.vector.riotx.core.utils.shareMedia import im.vector.riotx.core.utils.shareMedia
import im.vector.riotx.core.utils.toast import im.vector.riotx.core.utils.toast
@ -157,6 +157,7 @@ import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.fragment_room_detail.* import kotlinx.android.synthetic.main.fragment_room_detail.*
import kotlinx.android.synthetic.main.merge_composer_layout.view.* import kotlinx.android.synthetic.main.merge_composer_layout.view.*
import kotlinx.android.synthetic.main.merge_overlay_waiting_view.* import kotlinx.android.synthetic.main.merge_overlay_waiting_view.*
import org.billcarsonfr.jsonviewer.JSonViewerDialog
import org.commonmark.parser.Parser import org.commonmark.parser.Parser
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
@ -181,8 +182,8 @@ class RoomDetailFragment @Inject constructor(
private val notificationDrawerManager: NotificationDrawerManager, private val notificationDrawerManager: NotificationDrawerManager,
val roomDetailViewModelFactory: RoomDetailViewModel.Factory, val roomDetailViewModelFactory: RoomDetailViewModel.Factory,
private val eventHtmlRenderer: EventHtmlRenderer, private val eventHtmlRenderer: EventHtmlRenderer,
private val vectorPreferences: VectorPreferences private val vectorPreferences: VectorPreferences,
) : private val colorProvider: ColorProvider) :
VectorBaseFragment(), VectorBaseFragment(),
TimelineEventController.Callback, TimelineEventController.Callback,
VectorInviteView.Callback, VectorInviteView.Callback,
@ -801,12 +802,15 @@ class RoomDetailFragment @Inject constructor(
.show() .show()
} }
private fun promptReasonToRedactEvent(eventId: String) { private fun promptConfirmationToRedactEvent(action: EventSharedAction.Redact) {
val layout = requireActivity().layoutInflater.inflate(R.layout.dialog_delete_event, null) val layout = requireActivity().layoutInflater.inflate(R.layout.dialog_delete_event, null)
val reasonCheckBox = layout.findViewById<MaterialCheckBox>(R.id.deleteEventReasonCheck) val reasonCheckBox = layout.findViewById<MaterialCheckBox>(R.id.deleteEventReasonCheck)
val reasonTextInputLayout = layout.findViewById<TextInputLayout>(R.id.deleteEventReasonTextInputLayout) val reasonTextInputLayout = layout.findViewById<TextInputLayout>(R.id.deleteEventReasonTextInputLayout)
val reasonInput = layout.findViewById<TextInputEditText>(R.id.deleteEventReasonInput) val reasonInput = layout.findViewById<TextInputEditText>(R.id.deleteEventReasonInput)
reasonCheckBox.isVisible = action.askForReason
reasonTextInputLayout.isVisible = action.askForReason
reasonCheckBox.setOnCheckedChangeListener { _, isChecked -> reasonTextInputLayout.isEnabled = isChecked } reasonCheckBox.setOnCheckedChangeListener { _, isChecked -> reasonTextInputLayout.isEnabled = isChecked }
AlertDialog.Builder(requireActivity()) AlertDialog.Builder(requireActivity())
@ -814,9 +818,10 @@ class RoomDetailFragment @Inject constructor(
.setView(layout) .setView(layout)
.setPositiveButton(R.string.remove) { _, _ -> .setPositiveButton(R.string.remove) { _, _ ->
val reason = reasonInput.text.toString() val reason = reasonInput.text.toString()
.takeIf { reasonCheckBox.isChecked } .takeIf { action.askForReason }
?.takeIf { reasonCheckBox.isChecked }
?.takeIf { it.isNotBlank() } ?.takeIf { it.isNotBlank() }
roomDetailViewModel.handle(RoomDetailAction.RedactAction(eventId, reason)) roomDetailViewModel.handle(RoomDetailAction.RedactAction(action.eventId, reason))
} }
.setNegativeButton(R.string.cancel, null) .setNegativeButton(R.string.cancel, null)
.show() .show()
@ -1134,7 +1139,7 @@ class RoomDetailFragment @Inject constructor(
showSnackWithMessage(getString(R.string.copied_to_clipboard), Snackbar.LENGTH_SHORT) showSnackWithMessage(getString(R.string.copied_to_clipboard), Snackbar.LENGTH_SHORT)
} }
is EventSharedAction.Redact -> { is EventSharedAction.Redact -> {
promptReasonToRedactEvent(action.eventId) promptConfirmationToRedactEvent(action)
} }
is EventSharedAction.Share -> { is EventSharedAction.Share -> {
// TODO current data communication is too limited // TODO current data communication is too limited
@ -1168,26 +1173,18 @@ class RoomDetailFragment @Inject constructor(
onEditedDecorationClicked(action.messageInformationData) onEditedDecorationClicked(action.messageInformationData)
} }
is EventSharedAction.ViewSource -> { is EventSharedAction.ViewSource -> {
val view = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_event_content, null) JSonViewerDialog.newInstance(
view.findViewById<TextView>(R.id.event_content_text_view)?.let { action.content,
it.text = action.content -1,
} jsonViewerStyler(colorProvider)
).show(childFragmentManager, "JSON_VIEWER")
AlertDialog.Builder(requireActivity())
.setView(view)
.setPositiveButton(R.string.ok, null)
.show()
} }
is EventSharedAction.ViewDecryptedSource -> { is EventSharedAction.ViewDecryptedSource -> {
val view = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_event_content, null) JSonViewerDialog.newInstance(
view.findViewById<TextView>(R.id.event_content_text_view)?.let { action.content,
it.text = action.content -1,
} jsonViewerStyler(colorProvider)
).show(childFragmentManager, "JSON_VIEWER")
AlertDialog.Builder(requireActivity())
.setView(view)
.setPositiveButton(R.string.ok, null)
.show()
} }
is EventSharedAction.QuickReact -> { is EventSharedAction.QuickReact -> {
// eventId,ClickedOn,Add // eventId,ClickedOn,Add

View file

@ -55,7 +55,7 @@ sealed class EventSharedAction(@StringRes val titleRes: Int,
data class Remove(val eventId: String) : data class Remove(val eventId: String) :
EventSharedAction(R.string.remove, R.drawable.ic_trash, true) EventSharedAction(R.string.remove, R.drawable.ic_trash, true)
data class Redact(val eventId: String) : data class Redact(val eventId: String, val askForReason: Boolean) :
EventSharedAction(R.string.message_action_item_redact, R.drawable.ic_delete, true) EventSharedAction(R.string.message_action_item_redact, R.drawable.ic_delete, true)
data class Cancel(val eventId: String) : data class Cancel(val eventId: String) :

View file

@ -168,6 +168,10 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
} }
private fun computeMessageBody(timelineEvent: TimelineEvent): CharSequence { private fun computeMessageBody(timelineEvent: TimelineEvent): CharSequence {
if (timelineEvent.root.isRedacted()) {
return getRedactionReason(timelineEvent)
}
return when (timelineEvent.root.getClearType()) { return when (timelineEvent.root.getClearType()) {
EventType.MESSAGE, EventType.MESSAGE,
EventType.STICKER -> { EventType.STICKER -> {
@ -200,6 +204,31 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
} ?: "" } ?: ""
} }
private fun getRedactionReason(timelineEvent: TimelineEvent): String {
return (timelineEvent
.root
.unsignedData
?.redactedEvent
?.content
?.get("reason") as? String)
?.takeIf { it.isNotBlank() }
.let { reason ->
if (reason == null) {
if (timelineEvent.root.isRedactedBySameUser()) {
stringProvider.getString(R.string.event_redacted_by_user_reason)
} else {
stringProvider.getString(R.string.event_redacted_by_admin_reason)
}
} else {
if (timelineEvent.root.isRedactedBySameUser()) {
stringProvider.getString(R.string.event_redacted_by_user_reason_with_reason, reason)
} else {
stringProvider.getString(R.string.event_redacted_by_admin_reason_with_reason, reason)
}
}
}
}
private fun actionsForEvent(timelineEvent: TimelineEvent): List<EventSharedAction> { private fun actionsForEvent(timelineEvent: TimelineEvent): List<EventSharedAction> {
val messageContent: MessageContent? = timelineEvent.annotations?.editSummary?.aggregatedContent.toModel() val messageContent: MessageContent? = timelineEvent.annotations?.editSummary?.aggregatedContent.toModel()
?: timelineEvent.root.getClearContent().toModel() ?: timelineEvent.root.getClearContent().toModel()
@ -227,7 +256,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
} }
if (canRedact(timelineEvent, session.myUserId)) { if (canRedact(timelineEvent, session.myUserId)) {
add(EventSharedAction.Redact(eventId)) add(EventSharedAction.Redact(eventId, askForReason = informationData.senderId != session.myUserId))
} }
if (canCopy(msgType)) { if (canCopy(msgType)) {

View file

@ -40,7 +40,7 @@ import javax.inject.Inject
/** /**
* In this screen, in signin mode: * In this screen, in signin mode:
* - the user is asked for login and password to sign in to a homeserver. * - the user is asked for login (or email) and password to sign in to a homeserver.
* - He also can reset his password * - He also can reset his password
* In signup mode: * In signup mode:
* - the user is asked for login and password * - the user is asked for login and password
@ -97,6 +97,12 @@ class LoginFragment @Inject constructor() : AbstractLoginFragment() {
SignMode.SignIn -> R.string.login_connect_to SignMode.SignIn -> R.string.login_connect_to
} }
loginFieldTil.hint = getString(when (state.signMode) {
SignMode.Unknown -> error("developer error")
SignMode.SignUp -> R.string.login_signup_username_hint
SignMode.SignIn -> R.string.login_signin_username_hint
})
when (state.serverType) { when (state.serverType) {
ServerType.MatrixOrg -> { ServerType.MatrixOrg -> {
loginServerIcon.isVisible = true loginServerIcon.isVisible = true

View file

@ -0,0 +1,79 @@
/*
* 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.riotx.features.settings.devtools
import android.view.View
import com.airbnb.epoxy.TypedEpoxyController
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.loadingItem
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.core.ui.list.genericFooterItem
import im.vector.riotx.core.ui.list.genericItemWithValue
import im.vector.riotx.core.utils.DebouncedClickListener
import javax.inject.Inject
class AccountDataEpoxyController @Inject constructor(
private val stringProvider: StringProvider
) : TypedEpoxyController<AccountDataViewState>() {
interface InteractionListener {
fun didTap(data: UserAccountData)
}
var interactionListener: InteractionListener? = null
override fun buildModels(data: AccountDataViewState?) {
if (data == null) return
when (data.accountData) {
is Loading -> {
loadingItem {
id("loading")
loadingText(stringProvider.getString(R.string.loading))
}
}
is Fail -> {
genericFooterItem {
id("fail")
text(data.accountData.error.localizedMessage)
}
}
is Success -> {
val dataList = data.accountData.invoke()
if (dataList.isEmpty()) {
genericFooterItem {
id("noResults")
text(stringProvider.getString(R.string.no_result_placeholder))
}
} else {
dataList.forEach { accountData ->
genericItemWithValue {
id(accountData.type)
title(accountData.type)
itemClickAction(DebouncedClickListener(View.OnClickListener {
interactionListener?.didTap(accountData)
}))
}
}
}
}
}
}
}

View file

@ -0,0 +1,79 @@
/*
* 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.riotx.features.settings.devtools
import android.os.Bundle
import android.view.View
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.UserAccountDataEvent
import im.vector.riotx.R
import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.extensions.configureWith
import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.core.utils.jsonViewerStyler
import kotlinx.android.synthetic.main.fragment_generic_recycler.*
import org.billcarsonfr.jsonviewer.JSonViewerDialog
import javax.inject.Inject
class AccountDataFragment @Inject constructor(
val viewModelFactory: AccountDataViewModel.Factory,
private val epoxyController: AccountDataEpoxyController,
private val colorProvider: ColorProvider
) : VectorBaseFragment(), AccountDataEpoxyController.InteractionListener {
override fun getLayoutResId() = R.layout.fragment_generic_recycler
private val viewModel: AccountDataViewModel by fragmentViewModel(AccountDataViewModel::class)
override fun onResume() {
super.onResume()
(activity as? VectorBaseActivity)?.supportActionBar?.setTitle(R.string.settings_account_data)
}
override fun invalidate() = withState(viewModel) { state ->
epoxyController.setData(state)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
recyclerView.configureWith(epoxyController, showDivider = true)
epoxyController.interactionListener = this
}
override fun onDestroyView() {
super.onDestroyView()
recyclerView.cleanup()
epoxyController.interactionListener = null
}
override fun didTap(data: UserAccountData) {
val fb = data as? UserAccountDataEvent ?: return
val jsonString = MoshiProvider.providesMoshi()
.adapter(UserAccountDataEvent::class.java)
.toJson(fb)
JSonViewerDialog.newInstance(
jsonString,
-1, // open All
jsonViewerStyler(colorProvider)
).show(childFragmentManager, "JSON_VIEWER")
}
}

View file

@ -0,0 +1,64 @@
/*
* 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.riotx.features.settings.devtools
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData
import im.vector.matrix.rx.rx
import im.vector.riotx.core.platform.EmptyAction
import im.vector.riotx.core.platform.EmptyViewEvents
import im.vector.riotx.core.platform.VectorViewModel
data class AccountDataViewState(
val accountData: Async<List<UserAccountData>> = Uninitialized
) : MvRxState
class AccountDataViewModel @AssistedInject constructor(@Assisted initialState: AccountDataViewState,
private val session: Session)
: VectorViewModel<AccountDataViewState, EmptyAction, EmptyViewEvents>(initialState) {
init {
session.rx().liveAccountData(emptyList())
.execute {
copy(accountData = it)
}
}
override fun handle(action: EmptyAction) {}
@AssistedInject.Factory
interface Factory {
fun create(initialState: AccountDataViewState): AccountDataViewModel
}
companion object : MvRxViewModelFactory<AccountDataViewModel, AccountDataViewState> {
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: AccountDataViewState): AccountDataViewModel? {
val fragment: AccountDataFragment = (viewModelContext as FragmentViewModelContext).fragment()
return fragment.viewModelFactory.create(state)
}
}
}

View file

@ -43,8 +43,7 @@
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:id="@+id/deleteEventReasonInput" android:id="@+id/deleteEventReasonInput"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content" />
android:text="@string/event_redacted_by_user_reason" />
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>

View file

@ -50,8 +50,8 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="32dp" android:layout_marginTop="32dp"
android:hint="@string/login_signup_username_hint" app:errorEnabled="true"
app:errorEnabled="true"> tools:hint="@string/login_signin_username_hint">
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:id="@+id/loginField" android:id="@+id/loginField"

View file

@ -1907,7 +1907,7 @@
<string name="login_msisdn_error_other">Телефонния номер изглежда невалиден. Моля проверете го</string> <string name="login_msisdn_error_other">Телефонния номер изглежда невалиден. Моля проверете го</string>
<string name="login_signup_to">Регистрация в %1$s</string> <string name="login_signup_to">Регистрация в %1$s</string>
<string name="login_signup_username_hint">Потребителско име или имейл</string> <string name="login_signin_username_hint">Потребителско име или имейл</string>
<string name="login_signup_password_hint">Парола</string> <string name="login_signup_password_hint">Парола</string>
<string name="login_signup_submit">Напред</string> <string name="login_signup_submit">Напред</string>
<string name="login_signup_error_user_in_use">Това потребителско име е заето</string> <string name="login_signup_error_user_in_use">Това потребителско име е заето</string>

View file

@ -1941,7 +1941,7 @@ Wenn du diese neue Wiederherstellungsmethode nicht eingerichtet hast, kann ein A
<string name="login_msisdn_confirm_notice">Wir haben einen Code an %1$s gesendet. Gib diesen unten ein um dich zu verifizieren.</string> <string name="login_msisdn_confirm_notice">Wir haben einen Code an %1$s gesendet. Gib diesen unten ein um dich zu verifizieren.</string>
<string name="login_msisdn_confirm_hint">Code eingeben</string> <string name="login_msisdn_confirm_hint">Code eingeben</string>
<string name="login_msisdn_confirm_send_again">Erneut senden</string> <string name="login_msisdn_confirm_send_again">Erneut senden</string>
<string name="login_signup_username_hint">Benutzername oder E-Mail-Adresse</string> <string name="login_signin_username_hint">Benutzername oder E-Mail-Adresse</string>
<string name="login_signup_password_hint">Passwort</string> <string name="login_signup_password_hint">Passwort</string>
<string name="login_signup_error_user_in_use">Dieser Benutzername ist bereits belegt</string> <string name="login_signup_error_user_in_use">Dieser Benutzername ist bereits belegt</string>
<string name="login_signup_cancel_confirmation_content">Dein Benutzerkonto ist noch nicht erstellt. <string name="login_signup_cancel_confirmation_content">Dein Benutzerkonto ist noch nicht erstellt.

View file

@ -1909,7 +1909,7 @@ Abisua: Fitxategi hau ezabatu daiteke aplikazioa desinstalatzen bada.</string>
<string name="login_msisdn_error_other">Telefono zenbakia baliogabea dirudi. Egiaztatu ezazu</string> <string name="login_msisdn_error_other">Telefono zenbakia baliogabea dirudi. Egiaztatu ezazu</string>
<string name="login_signup_to">Erregistratu %1$s zerbitzarian</string> <string name="login_signup_to">Erregistratu %1$s zerbitzarian</string>
<string name="login_signup_username_hint">Erabiltzaile-izena edo e-maila</string> <string name="login_signin_username_hint">Erabiltzaile-izena edo e-maila</string>
<string name="login_signup_password_hint">Pasahitza</string> <string name="login_signup_password_hint">Pasahitza</string>
<string name="login_signup_submit">Hurrengoa</string> <string name="login_signup_submit">Hurrengoa</string>
<string name="login_signup_error_user_in_use">Erabiltzaile-izen hori hartuta dago</string> <string name="login_signup_error_user_in_use">Erabiltzaile-izen hori hartuta dago</string>

View file

@ -1962,7 +1962,7 @@ Jotta et menetä mitään, automaattiset päivitykset kannattaa pitää käytös
<string name="login_msisdn_error_other">Puhelinnumero vaikuttaa epäkelvolta. Tarkista numero</string> <string name="login_msisdn_error_other">Puhelinnumero vaikuttaa epäkelvolta. Tarkista numero</string>
<string name="login_signup_to">Rekisteröidy palvelimelle %1$s</string> <string name="login_signup_to">Rekisteröidy palvelimelle %1$s</string>
<string name="login_signup_username_hint">Käyttäjätunnus tai sähköpostiosoite</string> <string name="login_signin_username_hint">Käyttäjätunnus tai sähköpostiosoite</string>
<string name="login_signup_password_hint">Salasana</string> <string name="login_signup_password_hint">Salasana</string>
<string name="login_signup_submit">Seuraava</string> <string name="login_signup_submit">Seuraava</string>
<string name="login_signup_error_user_in_use">Käyttäjätunnus on varattu</string> <string name="login_signup_error_user_in_use">Käyttäjätunnus on varattu</string>

View file

@ -1918,7 +1918,7 @@ Si vous navez pas configuré de nouvelle méthode de récupération, un attaq
<string name="login_msisdn_error_other">Le numéro de téléphone na pas lair dêtre valide. Veuillez le vérifier</string> <string name="login_msisdn_error_other">Le numéro de téléphone na pas lair dêtre valide. Veuillez le vérifier</string>
<string name="login_signup_to">Sinscrire sur %1$s</string> <string name="login_signup_to">Sinscrire sur %1$s</string>
<string name="login_signup_username_hint">Nom dutilisateur ou e-mail</string> <string name="login_signin_username_hint">Nom dutilisateur ou e-mail</string>
<string name="login_signup_password_hint">Mot de passe</string> <string name="login_signup_password_hint">Mot de passe</string>
<string name="login_signup_submit">Suivant</string> <string name="login_signup_submit">Suivant</string>
<string name="login_signup_error_user_in_use">Ce nom dutilisateur est déjà pris</string> <string name="login_signup_error_user_in_use">Ce nom dutilisateur est déjà pris</string>

View file

@ -1913,7 +1913,7 @@ Ha nem te állítottad be a visszaállítási metódust, akkor egy támadó pró
<string name="login_msisdn_error_other">A telefonszám érvénytelennek látszik. Kérlek ellenőrizd</string> <string name="login_msisdn_error_other">A telefonszám érvénytelennek látszik. Kérlek ellenőrizd</string>
<string name="login_signup_to">Bejelentkezés ide: %1$s</string> <string name="login_signup_to">Bejelentkezés ide: %1$s</string>
<string name="login_signup_username_hint">Felhasználónév vagy e-mail</string> <string name="login_signin_username_hint">Felhasználónév vagy e-mail</string>
<string name="login_signup_password_hint">Jelszó</string> <string name="login_signup_password_hint">Jelszó</string>
<string name="login_signup_submit">Következő</string> <string name="login_signup_submit">Következő</string>
<string name="login_signup_error_user_in_use">A felhasználónév már használatban van</string> <string name="login_signup_error_user_in_use">A felhasználónév már használatban van</string>

View file

@ -1963,7 +1963,7 @@
<string name="login_msisdn_error_other">Il numero di telefono non sembra valido. Ricontrollalo</string> <string name="login_msisdn_error_other">Il numero di telefono non sembra valido. Ricontrollalo</string>
<string name="login_signup_to">Registrati su %1$s</string> <string name="login_signup_to">Registrati su %1$s</string>
<string name="login_signup_username_hint">Nome utente o email</string> <string name="login_signin_username_hint">Nome utente o email</string>
<string name="login_signup_password_hint">Password</string> <string name="login_signup_password_hint">Password</string>
<string name="login_signup_submit">Avanti</string> <string name="login_signup_submit">Avanti</string>
<string name="login_signup_error_user_in_use">Quel nome utente esiste già</string> <string name="login_signup_error_user_in_use">Quel nome utente esiste già</string>

View file

@ -1871,7 +1871,7 @@ Që të garantoni se sju shpëton gjë, thjesht mbajeni të aktivizuar mekani
<string name="login_msisdn_error_other">Numri i telefonit duket se është i vlefshëm. Ju lutemi, kontrollojeni</string> <string name="login_msisdn_error_other">Numri i telefonit duket se është i vlefshëm. Ju lutemi, kontrollojeni</string>
<string name="login_signup_to">Regjistrohuni te %1$s</string> <string name="login_signup_to">Regjistrohuni te %1$s</string>
<string name="login_signup_username_hint">Emër përdoruesi ose email</string> <string name="login_signin_username_hint">Emër përdoruesi ose email</string>
<string name="login_signup_password_hint">Fjalëkalim</string> <string name="login_signup_password_hint">Fjalëkalim</string>
<string name="login_signup_submit">Pasuesi</string> <string name="login_signup_submit">Pasuesi</string>
<string name="login_signup_error_user_in_use">Ai emër përdoruesi është i zënë</string> <string name="login_signup_error_user_in_use">Ai emër përdoruesi është i zënë</string>

View file

@ -1866,7 +1866,7 @@ Matrix 中的消息可見度類似于電子郵件。我們忘記您的郵件意
<string name="login_msisdn_error_other">電話號碼似乎無效。請檢查</string> <string name="login_msisdn_error_other">電話號碼似乎無效。請檢查</string>
<string name="login_signup_to">註冊至 %1$s</string> <string name="login_signup_to">註冊至 %1$s</string>
<string name="login_signup_username_hint">使用者名稱或電子郵件</string> <string name="login_signin_username_hint">使用者名稱或電子郵件</string>
<string name="login_signup_password_hint">密碼</string> <string name="login_signup_password_hint">密碼</string>
<string name="login_signup_submit">下一個</string> <string name="login_signup_submit">下一個</string>
<string name="login_signup_error_user_in_use">使用者名稱已被使用</string> <string name="login_signup_error_user_in_use">使用者名稱已被使用</string>

View file

@ -1912,7 +1912,7 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming
<!-- Replaced string is the homeserver url --> <!-- Replaced string is the homeserver url -->
<string name="login_signup_to">Sign up to %1$s</string> <string name="login_signup_to">Sign up to %1$s</string>
<string name="login_signup_username_hint">Username or email</string> <string name="login_signin_username_hint">Username or email</string>
<string name="login_signup_password_hint">Password</string> <string name="login_signup_password_hint">Password</string>
<string name="login_signup_submit">Next</string> <string name="login_signup_submit">Next</string>
<string name="login_signup_error_user_in_use">That username is taken</string> <string name="login_signup_error_user_in_use">That username is taken</string>

View file

@ -6,6 +6,8 @@
<!-- Sections has been created to avoid merge conflict. Let's see if it's better --> <!-- Sections has been created to avoid merge conflict. Let's see if it's better -->
<!-- BEGIN Strings added by Valere --> <!-- BEGIN Strings added by Valere -->
<string name="settings_dev_tools">Dev Tools</string>
<string name="settings_account_data">Account Data</string>
<plurals name="poll_info"> <plurals name="poll_info">
<item quantity="zero">%d vote</item> <item quantity="zero">%d vote</item>
<item quantity="other">%d votes</item> <item quantity="other">%d votes</item>
@ -26,9 +28,9 @@
<item quantity="one">Send image with the original size</item> <item quantity="one">Send image with the original size</item>
<item quantity="other">Send images with the original size</item> <item quantity="other">Send images with the original size</item>
</plurals> </plurals>
<string name="login_signup_username_hint">Username</string>
<!-- END Strings added by Benoit --> <!-- END Strings added by Benoit -->
<!-- BEGIN Strings added by Benoit --> <!-- BEGIN Strings added by Benoit -->
<!-- END Strings added by Benoit --> <!-- END Strings added by Benoit -->
@ -38,6 +40,9 @@
<string name="delete_event_dialog_content">Are you sure you wish to remove (delete) this event? Note that if you delete a room name or topic change, it could undo the change.</string> <string name="delete_event_dialog_content">Are you sure you wish to remove (delete) this event? Note that if you delete a room name or topic change, it could undo the change.</string>
<string name="delete_event_dialog_reason_checkbox">Include a reason</string> <string name="delete_event_dialog_reason_checkbox">Include a reason</string>
<string name="delete_event_dialog_reason_hint">Reason for redacting</string> <string name="delete_event_dialog_reason_hint">Reason for redacting</string>
<string name="event_redacted_by_user_reason_with_reason">Event deleted by user, reason: %1$s</string>
<string name="event_redacted_by_admin_reason_with_reason">Event moderated by room admin, reason: %1$s</string>
<!-- END Strings added by Onuray --> <!-- END Strings added by Onuray -->

View file

@ -63,6 +63,14 @@
android:persistent="false" android:persistent="false"
android:title="@string/settings_push_rules" android:title="@string/settings_push_rules"
app:fragment="im.vector.riotx.features.settings.push.PushRulesFragment" /> app:fragment="im.vector.riotx.features.settings.push.PushRulesFragment" />
</im.vector.riotx.core.preference.VectorPreferenceCategory>
<im.vector.riotx.core.preference.VectorPreferenceCategory android:title="@string/settings_dev_tools">
<im.vector.riotx.core.preference.VectorPreference
android:persistent="false"
android:title="@string/settings_account_data"
app:fragment="im.vector.riotx.features.settings.devtools.AccountDataFragment" />
</im.vector.riotx.core.preference.VectorPreferenceCategory> </im.vector.riotx.core.preference.VectorPreferenceCategory>