diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/ssss/QuadSTests.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/ssss/QuadSTests.kt index c84716c4d9..af15738cc8 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/ssss/QuadSTests.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/ssss/QuadSTests.kt @@ -33,6 +33,7 @@ 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.crosssigning.toBase64NoPadding import im.vector.matrix.android.internal.crypto.secrets.DefaultSharedSecureStorage import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent import kotlinx.coroutines.Dispatchers @@ -164,9 +165,9 @@ class QuadSTests : InstrumentedTest { TestMatrixCallback(storeCountDownLatch) ) - val secretAccountData = assertAccountData(aliceSession,"secret.of.life" ) + val secretAccountData = assertAccountData(aliceSession, "secret.of.life") - val encryptedContent = secretAccountData.content.get("encrypted") as? Map<*,*> + 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)) @@ -182,12 +183,12 @@ class QuadSTests : InstrumentedTest { var decryptedSecret: String? = null val decryptCountDownLatch = CountDownLatch(1) - aliceSession.sharedSecretStorageService.getSecret("secret.of.life" , - null, //default key + aliceSession.sharedSecretStorageService.getSecret("secret.of.life", + null, //default key keySpec!!, object : MatrixCallback { override fun onFailure(failure: Throwable) { - fail("Fail to decrypt -> " +failure.localizedMessage) + fail("Fail to decrypt -> " + failure.localizedMessage) decryptCountDownLatch.countDown() } @@ -202,7 +203,136 @@ class QuadSTests : InstrumentedTest { 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>? { + 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.getAccountData("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 { + 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 { @@ -268,4 +398,50 @@ class QuadSTests : InstrumentedTest { 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>? { + return null + } + } + + var creationInfo: SSSSKeyCreationInfo? = null + + val generateLatch = CountDownLatch(1) + + quadS.generateKeyWithPassphrase(keyId, keyId, + passphrase, + emptyKeySigner, + null, + object : MatrixCallback { + + override fun onSuccess(data: SSSSKeyCreationInfo) { + creationInfo = data + generateLatch.countDown() + } + + override fun onFailure(failure: Throwable) { + Assert.fail("onFailure " + failure.localizedMessage) + generateLatch.countDown() + } + }) + + mTestHelper.await(generateLatch) + + Assert.assertNotNull(creationInfo) + + assertAccountData(session, "m.secret_storage.key.$keyId") + if (asDefault) { + val setDefaultLatch = CountDownLatch(1) + quadS.setDefaultKey(keyId, TestMatrixCallback(setDefaultLatch)) + mTestHelper.await(setDefaultLatch) + assertAccountData(session, DefaultSharedSecureStorage.DEFAULT_KEY_ID) + } + + return creationInfo!! + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/accountdata/AccountDataService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/accountdata/AccountDataService.kt index a832921dc7..f4ffdce71c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/accountdata/AccountDataService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/accountdata/AccountDataService.kt @@ -18,6 +18,7 @@ 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 @@ -31,5 +32,5 @@ interface AccountDataService { fun getLiveAccountData(filterType: List): LiveData> - fun updateAccountData(type: String, data: Any, callback: MatrixCallback? = null) + fun updateAccountData(type: String, content: Content, callback: MatrixCallback? = null) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/secrets/DefaultSharedSecretStorage.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/secrets/DefaultSharedSecretStorage.kt index a4cf02aeae..2c6d6702eb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/secrets/DefaultSharedSecretStorage.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/secrets/DefaultSharedSecretStorage.kt @@ -19,6 +19,7 @@ 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 @@ -82,7 +83,7 @@ internal class DefaultSharedSecureStorage @Inject constructor( accountDataService.updateAccountData( "$KEY_ID_BASE.$keyId", - signedContent, + signedContent.toContent(), object : MatrixCallback { override fun onFailure(failure: Throwable) { callback.onFailure(failure) @@ -136,7 +137,7 @@ internal class DefaultSharedSecureStorage @Inject constructor( accountDataService.updateAccountData( "$KEY_ID_BASE.$keyId", - signedContent, + signedContent.toContent(), object : MatrixCallback { override fun onFailure(failure: Throwable) { callback.onFailure(failure) @@ -254,7 +255,7 @@ internal class DefaultSharedSecureStorage @Inject constructor( accountDataService.updateAccountData( type = name, - data = mapOf( + content = mapOf( "encrypted" to encryptedContents ), callback = callback diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/UserAccountDataSyncHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/UserAccountDataSyncHandler.kt index fc4f73630b..541d11cfc9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/UserAccountDataSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/UserAccountDataSyncHandler.kt @@ -210,7 +210,7 @@ internal class UserAccountDataSyncHandler @Inject constructor( } } - private fun handleGenericAccountData(realm: Realm, type: String, content: Content?) { + fun handleGenericAccountData(realm: Realm, type: String, content: Content?) { val existing = realm.where().equalTo(UserAccountDataEntityFields.TYPE, type) .findFirst() if (existing != null) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/DefaultAccountDataService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/DefaultAccountDataService.kt index 0bb57f0dae..ee9e1d2550 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/DefaultAccountDataService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/DefaultAccountDataService.kt @@ -21,6 +21,7 @@ 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 @@ -28,6 +29,7 @@ import im.vector.matrix.android.internal.database.model.UserAccountDataEntity import im.vector.matrix.android.internal.database.model.UserAccountDataEntityFields import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.matrix.android.internal.di.SessionId +import im.vector.matrix.android.internal.session.sync.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 @@ -37,6 +39,7 @@ 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 { @@ -87,13 +90,24 @@ internal class DefaultAccountDataService @Inject constructor( }) } - override fun updateAccountData(type: String, data: Any, callback: MatrixCallback?) { + override fun updateAccountData(type: String, content: Content, callback: MatrixCallback?) { updateUserAccountDataTask.configureWith(UpdateUserAccountDataTask.AnyParams( type = type, - any = data + any = content )) { this.retryCount = 5 - callback?.let { this.callback = it } + this.callback = object : MatrixCallback { + 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) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/UpdateUserAccountDataTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/UpdateUserAccountDataTask.kt index beb3a0fcc0..9f8a851ee2 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/UpdateUserAccountDataTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/accountdata/UpdateUserAccountDataTask.kt @@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.session.user.accountdata import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.session.sync.UserAccountDataSyncHandler import im.vector.matrix.android.internal.session.sync.model.accountdata.BreadcrumbsContent import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData import im.vector.matrix.android.internal.task.Task