mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2024-11-24 02:15:46 +03:00
Merge branch 'develop' into feature/attachment_process
This commit is contained in:
commit
13d3aa9ff1
57 changed files with 1897 additions and 85 deletions
36
CHANGES.md
36
CHANGES.md
|
@ -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:
|
||||||
-
|
-
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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!!
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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>>?
|
||||||
|
}
|
|
@ -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?
|
||||||
|
)
|
|
@ -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)
|
||||||
|
}
|
|
@ -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>)
|
||||||
|
}
|
|
@ -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 = ""
|
||||||
|
)
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"
|
||||||
|
|
|
@ -637,9 +637,9 @@ 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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -83,10 +83,10 @@ 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 {
|
||||||
// Note: copied and adapted from MXMegolmExportEncryption
|
// Note: copied and adapted from MXMegolmExportEncryption
|
||||||
val t0 = System.currentTimeMillis()
|
val t0 = System.currentTimeMillis()
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
// Didn't want to break too much thing, so i re-serialize to jsonString before reparsing
|
||||||
is UserAccountDataBreadcrumbs -> handleBreadcrumbs(realm, it)
|
// TODO would be better to have a mapper?
|
||||||
is UserAccountDataFallback -> Timber.d("Receive account data of unhandled type ${it.type}")
|
val toJson = MoshiProvider.providesMoshi().adapter(Event::class.java).toJson(it)
|
||||||
else -> error("Missing code here!")
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO Store all account data, app can be interested of it
|
|
||||||
// accountData?.list?.forEach {
|
|
||||||
// it.toString()
|
|
||||||
// MoshiProvider.providesMoshi()
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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()
|
|
@ -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()
|
||||||
)
|
)
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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(
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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) :
|
||||||
|
|
|
@ -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)) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -1918,7 +1918,7 @@ Si vous n’avez 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 n’a pas l’air d’être valide. Veuillez le vérifier</string>
|
<string name="login_msisdn_error_other">Le numéro de téléphone n’a pas l’air d’être valide. Veuillez le vérifier</string>
|
||||||
|
|
||||||
<string name="login_signup_to">S’inscrire sur %1$s</string>
|
<string name="login_signup_to">S’inscrire sur %1$s</string>
|
||||||
<string name="login_signup_username_hint">Nom d’utilisateur ou e-mail</string>
|
<string name="login_signin_username_hint">Nom d’utilisateur 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 d’utilisateur est déjà pris</string>
|
<string name="login_signup_error_user_in_use">Ce nom d’utilisateur est déjà pris</string>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -1871,7 +1871,7 @@ Që të garantoni se s’ju 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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 -->
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue