diff --git a/CHANGES.rst b/CHANGES.rst index 14867d7f4..31cda151d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -7,6 +7,8 @@ Features: Improvements: - Any Account data element, even if the type is not known is persisted. - The crypto store is now implemented using a Realm database. The existing file store will be migrated at first usage (#398) + - Upgrade olm-sdk.aar from version 2.3.0 to version 3.0.0 + - Implement the backup of the room keys in the KeysBackup class (vector-im/riot-android#2642) Bugfix: - Room members who left are listed with the actual members (vector-im/riot-android#2744) @@ -16,6 +18,7 @@ Bugfix: API Change: - new API in CallSoundsManager to allow client to play the specified Ringtone (vector-im/riot-android#827) - IMXStore.storeAccountData() has been renamed to IMXStore.storeRoomAccountData() + - MXCrypto: importRoomKeys methods now return number of imported keys and number of total keys in the Callback. Translations: - diff --git a/matrix-sdk/libs/olm-sdk.aar b/matrix-sdk/libs/olm-sdk.aar index 66be8a65a..552650a7d 100644 Binary files a/matrix-sdk/libs/olm-sdk.aar and b/matrix-sdk/libs/olm-sdk.aar differ diff --git a/matrix-sdk/src/androidTest/java/org/matrix/androidsdk/common/CommonTestHelper.java b/matrix-sdk/src/androidTest/java/org/matrix/androidsdk/common/CommonTestHelper.java index 3ee7d76be..24d52ab14 100644 --- a/matrix-sdk/src/androidTest/java/org/matrix/androidsdk/common/CommonTestHelper.java +++ b/matrix-sdk/src/androidTest/java/org/matrix/androidsdk/common/CommonTestHelper.java @@ -281,7 +281,6 @@ public void onSuccess(Credentials credentials) { IMXStore store = new MXFileStore(hs, false, context); MXDataHandler dataHandler = new MXDataHandler(store, credentials); - // TODO Use sessionTestParam parameter when other PR will be merged dataHandler.setLazyLoadingEnabled(sessionTestParams.getWithLazyLoading()); MXSession mxSession = new MXSession.Builder(hs, dataHandler, context) @@ -373,8 +372,8 @@ public void clearAllSessions(List sessions) { } /** - * Clone a session - * // TODO Use this method where it should be (after merge of keys backup) + * Clone a session. + * It simulate that the user launches again the application with the same Credentials, contrary to login which will create a new DeviceId * * @param from the session to clone * @return the duplicated session @@ -383,10 +382,11 @@ public void clearAllSessions(List sessions) { public MXSession createNewSession(@NonNull MXSession from, SessionTestParams sessionTestParams) throws InterruptedException { final Context context = InstrumentationRegistry.getContext(); - Credentials aliceCredentials = from.getCredentials(); - HomeServerConnectionConfig hs = createHomeServerConfig(aliceCredentials); + Credentials credentials = from.getCredentials(); + HomeServerConnectionConfig hs = createHomeServerConfig(credentials); MXFileStore store = new MXFileStore(hs, false, context); - MXDataHandler dataHandler = new MXDataHandler(store, aliceCredentials); + MXDataHandler dataHandler = new MXDataHandler(store, credentials); + dataHandler.setLazyLoadingEnabled(sessionTestParams.getWithLazyLoading()); store.setDataHandler(dataHandler); MXSession session2 = new MXSession.Builder(hs, dataHandler, context) .withLegacyCryptoStore(sessionTestParams.getWithLegacyCryptoStore()) diff --git a/matrix-sdk/src/androidTest/java/org/matrix/androidsdk/common/CryptoTestHelper.kt b/matrix-sdk/src/androidTest/java/org/matrix/androidsdk/common/CryptoTestHelper.kt index 8cbb72334..66f385cc1 100644 --- a/matrix-sdk/src/androidTest/java/org/matrix/androidsdk/common/CryptoTestHelper.kt +++ b/matrix-sdk/src/androidTest/java/org/matrix/androidsdk/common/CryptoTestHelper.kt @@ -18,12 +18,12 @@ package org.matrix.androidsdk.common import android.os.SystemClock import android.text.TextUtils -import org.junit.Assert -import org.junit.Assert.assertNotNull -import org.junit.Assert.assertTrue +import org.junit.Assert.* import org.matrix.androidsdk.MXSession import org.matrix.androidsdk.crypto.MXCRYPTO_ALGORITHM_MEGOLM -import org.matrix.androidsdk.crypto.MXCryptoAlgorithms +import org.matrix.androidsdk.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP +import org.matrix.androidsdk.crypto.keysbackup.MegolmBackupAuthData +import org.matrix.androidsdk.crypto.keysbackup.MegolmBackupCreationInfo import org.matrix.androidsdk.data.RoomState import org.matrix.androidsdk.listeners.MXEventListener import org.matrix.androidsdk.rest.model.Event @@ -36,10 +36,10 @@ import java.util.concurrent.CountDownLatch /** * Synchronously enable crypto for the session and fail if it does not work */ -fun MXSession.enableCrypto() { - val cryptoLatch = CountDownLatch(1) - enableCrypto(true, TestApiCallback(cryptoLatch)) - cryptoLatch.await() +fun MXSession.enableCrypto(testHelper: CommonTestHelper) { + val latch = CountDownLatch(1) + enableCrypto(true, TestApiCallback(latch)) + testHelper.await(latch) } @@ -80,7 +80,7 @@ class CryptoTestHelper(val mTestHelper: CommonTestHelper) { } }) mTestHelper.await(lock0) - Assert.assertTrue(results.containsKey("enableCrypto")) + assertTrue(results.containsKey("enableCrypto")) var roomId: String? = null val lock1 = CountDownLatch(1) @@ -93,7 +93,7 @@ class CryptoTestHelper(val mTestHelper: CommonTestHelper) { }) mTestHelper.await(lock1) - Assert.assertNotNull(roomId) + assertNotNull(roomId) val room = aliceSession.dataHandler.getRoom(roomId) @@ -105,7 +105,7 @@ class CryptoTestHelper(val mTestHelper: CommonTestHelper) { } }) mTestHelper.await(lock2) - Assert.assertTrue(results.containsKey("enableEncryptionWithAlgorithm")) + assertTrue(results.containsKey("enableEncryptionWithAlgorithm")) return CryptoTestData(aliceSession, roomId!!) } @@ -158,7 +158,7 @@ class CryptoTestHelper(val mTestHelper: CommonTestHelper) { mTestHelper.await(lock1) - Assert.assertTrue(statuses.containsKey("invite") && statuses.containsKey("onNewRoom")) + assertTrue(statuses.containsKey("invite") && statuses.containsKey("onNewRoom")) bobSession.dataHandler.removeListener(bobEventListener) @@ -183,11 +183,11 @@ class CryptoTestHelper(val mTestHelper: CommonTestHelper) { mTestHelper.await(lock2) // Ensure bob can send messages to the room - val roomFromBobPOV = bobSession.getDataHandler().getRoom(aliceRoomId) - assertNotNull(roomFromBobPOV.getState().getPowerLevels()) - assertTrue(roomFromBobPOV.getState().getPowerLevels().maySendMessage(bobSession.getMyUserId())) + val roomFromBobPOV = bobSession.dataHandler.getRoom(aliceRoomId) + assertNotNull(roomFromBobPOV.state.powerLevels) + assertTrue(roomFromBobPOV.state.powerLevels.maySendMessage(bobSession.myUserId)) - Assert.assertTrue(statuses.toString() + "", statuses.containsKey("AliceJoin")) + assertTrue(statuses.toString() + "", statuses.containsKey("AliceJoin")) bobSession.dataHandler.removeListener(bobEventListener) @@ -241,7 +241,7 @@ class CryptoTestHelper(val mTestHelper: CommonTestHelper) { mTestHelper.await(lock1) - Assert.assertTrue(statuses.containsKey("invite") && statuses.containsKey("onNewRoom")) + assertTrue(statuses.containsKey("invite") && statuses.containsKey("onNewRoom")) samSession.dataHandler.removeListener(samEventListener) @@ -255,7 +255,7 @@ class CryptoTestHelper(val mTestHelper: CommonTestHelper) { }) mTestHelper.await(lock2) - Assert.assertTrue(statuses.containsKey("joinRoom")) + assertTrue(statuses.containsKey("joinRoom")) // wait the initial sync SystemClock.sleep(1000) @@ -308,8 +308,8 @@ class CryptoTestHelper(val mTestHelper: CommonTestHelper) { // Alice sends a message roomFromAlicePOV.sendEvent(buildTextEvent(messagesFromAlice[0], aliceSession, aliceRoomId), TestApiCallback(lock, true)) mTestHelper.await(lock) - Assert.assertTrue(results.containsKey("onToDeviceEvent")) - Assert.assertEquals(1, messagesReceivedByBobCount) + assertTrue(results.containsKey("onToDeviceEvent")) + assertEquals(1, messagesReceivedByBobCount) // Bob send a message lock = CountDownLatch(1) @@ -317,7 +317,7 @@ class CryptoTestHelper(val mTestHelper: CommonTestHelper) { // android does not echo the messages sent from itself messagesReceivedByBobCount++ mTestHelper.await(lock) - Assert.assertEquals(2, messagesReceivedByBobCount) + assertEquals(2, messagesReceivedByBobCount) // Bob send a message lock = CountDownLatch(1) @@ -325,7 +325,7 @@ class CryptoTestHelper(val mTestHelper: CommonTestHelper) { // android does not echo the messages sent from itself messagesReceivedByBobCount++ mTestHelper.await(lock) - Assert.assertEquals(3, messagesReceivedByBobCount) + assertEquals(3, messagesReceivedByBobCount) // Bob send a message lock = CountDownLatch(1) @@ -333,41 +333,59 @@ class CryptoTestHelper(val mTestHelper: CommonTestHelper) { // android does not echo the messages sent from itself messagesReceivedByBobCount++ mTestHelper.await(lock) - Assert.assertEquals(4, messagesReceivedByBobCount) + assertEquals(4, messagesReceivedByBobCount) // Alice sends a message lock = CountDownLatch(2) roomFromAlicePOV.sendEvent(buildTextEvent(messagesFromAlice[1], aliceSession, aliceRoomId), TestApiCallback(lock, true)) mTestHelper.await(lock) - Assert.assertEquals(5, messagesReceivedByBobCount) + assertEquals(5, messagesReceivedByBobCount) return cryptoTestData } fun checkEncryptedEvent(event: Event, roomId: String, clearMessage: String, senderSession: MXSession) { - Assert.assertEquals(Event.EVENT_TYPE_MESSAGE_ENCRYPTED, event.wireType) - Assert.assertNotNull(event.wireContent) + assertEquals(Event.EVENT_TYPE_MESSAGE_ENCRYPTED, event.wireType) + assertNotNull(event.wireContent) val eventWireContent = event.wireContent.asJsonObject - Assert.assertNotNull(eventWireContent) + assertNotNull(eventWireContent) - Assert.assertNull(eventWireContent.get("body")) - Assert.assertEquals(MXCRYPTO_ALGORITHM_MEGOLM, eventWireContent.get("algorithm").asString) + assertNull(eventWireContent.get("body")) + assertEquals(MXCRYPTO_ALGORITHM_MEGOLM, eventWireContent.get("algorithm").asString) - Assert.assertNotNull(eventWireContent.get("ciphertext")) - Assert.assertNotNull(eventWireContent.get("session_id")) - Assert.assertNotNull(eventWireContent.get("sender_key")) + assertNotNull(eventWireContent.get("ciphertext")) + assertNotNull(eventWireContent.get("session_id")) + assertNotNull(eventWireContent.get("sender_key")) - Assert.assertEquals(senderSession.credentials.deviceId, eventWireContent.get("device_id").asString) + assertEquals(senderSession.credentials.deviceId, eventWireContent.get("device_id").asString) - Assert.assertNotNull(event.eventId) - Assert.assertEquals(roomId, event.roomId) - Assert.assertEquals(Event.EVENT_TYPE_MESSAGE, event.getType()) - Assert.assertTrue(event.getAge() < 10000) + assertNotNull(event.eventId) + assertEquals(roomId, event.roomId) + assertEquals(Event.EVENT_TYPE_MESSAGE, event.getType()) + assertTrue(event.getAge() < 10000) val eventContent = event.contentAsJsonObject - Assert.assertNotNull(eventContent) - Assert.assertEquals(clearMessage, eventContent!!.get("body").asString) - Assert.assertEquals(senderSession.myUserId, event.sender) + assertNotNull(eventContent) + assertEquals(clearMessage, eventContent!!.get("body").asString) + assertEquals(senderSession.myUserId, event.sender) + } + + fun createFakeMegolmBackupAuthData(): MegolmBackupAuthData { + return MegolmBackupAuthData( + publicKey = "abcdefg", + signatures = HashMap().apply { + this["something"] = HashMap().apply { + this["ed25519:something"] = "hijklmnop" + } + } + ) + } + + fun createFakeMegolmBackupCreationInfo(): MegolmBackupCreationInfo { + return MegolmBackupCreationInfo().apply { + algorithm = MXCRYPTO_ALGORITHM_MEGOLM_BACKUP + authData = createFakeMegolmBackupAuthData() + } } } diff --git a/matrix-sdk/src/androidTest/java/org/matrix/androidsdk/common/SessionTestParams.kt b/matrix-sdk/src/androidTest/java/org/matrix/androidsdk/common/SessionTestParams.kt index caa9faa02..d2a146c5b 100644 --- a/matrix-sdk/src/androidTest/java/org/matrix/androidsdk/common/SessionTestParams.kt +++ b/matrix-sdk/src/androidTest/java/org/matrix/androidsdk/common/SessionTestParams.kt @@ -16,7 +16,7 @@ package org.matrix.androidsdk.common -class SessionTestParams @JvmOverloads constructor(val withInitialSync: Boolean = false, - val withCryptoEnabled: Boolean = false, - val withLazyLoading: Boolean = false, - val withLegacyCryptoStore: Boolean = true) \ No newline at end of file +data class SessionTestParams @JvmOverloads constructor(val withInitialSync: Boolean = false, + val withCryptoEnabled: Boolean = false, + val withLazyLoading: Boolean = true, + val withLegacyCryptoStore: Boolean = false) \ No newline at end of file diff --git a/matrix-sdk/src/androidTest/java/org/matrix/androidsdk/common/TestApiCallback.kt b/matrix-sdk/src/androidTest/java/org/matrix/androidsdk/common/TestApiCallback.kt index a561a8dc7..1b9e01d3c 100644 --- a/matrix-sdk/src/androidTest/java/org/matrix/androidsdk/common/TestApiCallback.kt +++ b/matrix-sdk/src/androidTest/java/org/matrix/androidsdk/common/TestApiCallback.kt @@ -17,12 +17,10 @@ package org.matrix.androidsdk.common import android.support.annotation.CallSuper -import junit.framework.Assert - +import org.junit.Assert.fail import org.matrix.androidsdk.rest.callback.ApiCallback import org.matrix.androidsdk.rest.model.MatrixError import org.matrix.androidsdk.util.Log - import java.util.concurrent.CountDownLatch /** @@ -43,7 +41,7 @@ open class TestApiCallback @JvmOverloads constructor(private val countDownLat Log.e("TestApiCallback", e.message, e) if (onlySuccessful) { - Assert.fail("onNetworkError " + e.localizedMessage) + fail("onNetworkError " + e.localizedMessage) } countDownLatch.countDown() @@ -54,7 +52,7 @@ open class TestApiCallback @JvmOverloads constructor(private val countDownLat Log.e("TestApiCallback", e.message + " " + e.errcode) if (onlySuccessful) { - Assert.fail("onMatrixError " + e.localizedMessage) + fail("onMatrixError " + e.localizedMessage) } countDownLatch.countDown() @@ -65,7 +63,7 @@ open class TestApiCallback @JvmOverloads constructor(private val countDownLat Log.e("TestApiCallback", e.message, e) if (onlySuccessful) { - Assert.fail("onUnexpectedError " + e.localizedMessage) + fail("onUnexpectedError " + e.localizedMessage) } countDownLatch.countDown() diff --git a/matrix-sdk/src/androidTest/java/org/matrix/androidsdk/common/TestAssertUtil.kt b/matrix-sdk/src/androidTest/java/org/matrix/androidsdk/common/TestAssertUtil.kt new file mode 100644 index 000000000..6f764d455 --- /dev/null +++ b/matrix-sdk/src/androidTest/java/org/matrix/androidsdk/common/TestAssertUtil.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2018 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 org.matrix.androidsdk.common + +import org.junit.Assert.* + +/** + * Compare two lists and their content + */ +fun assertListEquals(list1: List?, list2: List?) { + if (list1 == null) { + assertNull(list2) + } else { + assertNotNull(list2) + + assertEquals("List sizes must match", list1.size, list2!!.size) + + for (i in list1.indices) { + assertEquals("Elements at index $i are not equal", list1[i], list2[i]) + } + } +} + +/** + * Compare two maps and their content + */ +fun assertDictEquals(dict1: Map?, dict2: Map?) { + if (dict1 == null) { + assertNull(dict2) + } else { + assertNotNull(dict2) + + assertEquals("Map sizes must match", dict1.size, dict2!!.size) + + for (i in dict1.keys) { + assertEquals("Values for key $i are not equal", dict1[i], dict2[i]) + } + } +} diff --git a/matrix-sdk/src/androidTest/java/org/matrix/androidsdk/common/Triple.java b/matrix-sdk/src/androidTest/java/org/matrix/androidsdk/common/Triple.java deleted file mode 100644 index c97449248..000000000 --- a/matrix-sdk/src/androidTest/java/org/matrix/androidsdk/common/Triple.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 2018 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 org.matrix.androidsdk.common; - -import android.util.Pair; - -public class Triple { - public A first; - public B second; - public C third; - - /** - * Constructor for a Triple. - * - * @param first the first object in the Triple - * @param second the second object in the Triple - * @param third the third object in the Triple - */ - public Triple(A first, B second, C third) { - this.first = first; - this.second = second; - this.third = third; - } - - /** - * Constructor from a Pair and another element - * - * @param pair - * @param third - */ - public Triple(Pair pair, C third) { - this(pair.first, pair.second, third); - } -} diff --git a/matrix-sdk/src/androidTest/java/org/matrix/androidsdk/crypto/CryptoStoreMigrationTest.kt b/matrix-sdk/src/androidTest/java/org/matrix/androidsdk/crypto/CryptoStoreMigrationTest.kt index daaf960ae..7d3db0173 100644 --- a/matrix-sdk/src/androidTest/java/org/matrix/androidsdk/crypto/CryptoStoreMigrationTest.kt +++ b/matrix-sdk/src/androidTest/java/org/matrix/androidsdk/crypto/CryptoStoreMigrationTest.kt @@ -18,13 +18,10 @@ package org.matrix.androidsdk.crypto import android.support.test.InstrumentationRegistry import android.text.TextUtils -import android.util.Pair -import org.junit.Assert import org.junit.Assert.* import org.junit.FixMethodOrder import org.junit.Test import org.junit.runners.MethodSorters -import org.matrix.androidsdk.MXSession import org.matrix.androidsdk.common.* import org.matrix.androidsdk.crypto.data.MXDeviceInfo import org.matrix.androidsdk.data.RoomState @@ -34,11 +31,7 @@ import org.matrix.androidsdk.data.cryptostore.db.RealmCryptoStore import org.matrix.androidsdk.data.timeline.EventTimeline import org.matrix.androidsdk.listeners.MXEventListener import org.matrix.androidsdk.rest.model.Event -import org.matrix.androidsdk.rest.model.MatrixError -import org.matrix.androidsdk.rest.model.RoomMember import org.matrix.androidsdk.rest.model.crypto.RoomKeyRequestBody -import org.matrix.androidsdk.rest.model.message.Message -import org.matrix.androidsdk.util.JsonUtils import org.matrix.androidsdk.util.Log import org.matrix.olm.OlmAccount import org.matrix.olm.OlmSession @@ -271,8 +264,8 @@ class CryptoStoreMigrationTest { val roomFromBobPOV = bobSession!!.dataHandler.getRoom(aliceRoomId) val roomFromAlicePOV = aliceSession.dataHandler.getRoom(aliceRoomId) - Assert.assertTrue(roomFromBobPOV.isEncrypted) - Assert.assertTrue(roomFromAlicePOV.isEncrypted) + assertTrue(roomFromBobPOV.isEncrypted) + assertTrue(roomFromAlicePOV.isEncrypted) aliceSession.crypto!!.setWarnOnUnknownDevices(false) @@ -294,7 +287,7 @@ class CryptoStoreMigrationTest { roomFromAlicePOV.sendEvent(mCryptoTestHelper.buildTextEvent(messageFromAlice, aliceSession, aliceRoomId), TestApiCallback(lock)) mTestHelper.await(lock) - Assert.assertTrue(results.containsKey("onLiveEvent")) + assertTrue(results.containsKey("onLiveEvent")) // Close alice and bob session aliceSession.crypto!!.close() @@ -321,7 +314,7 @@ class CryptoStoreMigrationTest { assertFalse(bobSession2.crypto!!.cryptoStore!!.inboundGroupSessions!!.isEmpty()) val roomFromBobPOV2 = bobSession2.dataHandler.getRoom(aliceRoomId) - Assert.assertTrue(roomFromBobPOV2.isEncrypted) + assertTrue(roomFromBobPOV2.isEncrypted) val lock2 = CountDownLatch(1) @@ -341,7 +334,7 @@ class CryptoStoreMigrationTest { roomFromBobPOV2.timeline.backPaginate(1, null) mTestHelper.await(lock2) - Assert.assertTrue(results.containsKey("onLiveEvent2")) + assertTrue(results.containsKey("onLiveEvent2")) cryptoTestData.clear(context) aliceSession2.clear(context) diff --git a/matrix-sdk/src/androidTest/java/org/matrix/androidsdk/crypto/CryptoTest.java b/matrix-sdk/src/androidTest/java/org/matrix/androidsdk/crypto/CryptoTest.java index cac8e1c71..0ebb55919 100644 --- a/matrix-sdk/src/androidTest/java/org/matrix/androidsdk/crypto/CryptoTest.java +++ b/matrix-sdk/src/androidTest/java/org/matrix/androidsdk/crypto/CryptoTest.java @@ -41,6 +41,7 @@ import org.matrix.androidsdk.common.CryptoTestHelper; import org.matrix.androidsdk.common.TestApiCallback; import org.matrix.androidsdk.common.TestConstants; +import org.matrix.androidsdk.crypto.data.ImportRoomKeysResult; import org.matrix.androidsdk.crypto.data.MXDeviceInfo; import org.matrix.androidsdk.crypto.data.MXOlmSessionResult; import org.matrix.androidsdk.crypto.data.MXUsersDevicesMap; @@ -141,15 +142,7 @@ public void onSuccess(Void info) { Assert.assertNotNull(myUserDevices); Assert.assertEquals(1, myUserDevices.size()); - final Credentials bobCredentials = bobSession.getCredentials(); - - HomeServerConnectionConfig hs = mTestHelper.createHomeServerConfig(bobCredentials); - - IMXStore store = new MXFileStore(hs, false, context); - - MXSession bobSession2 = new MXSession.Builder(hs, new MXDataHandler(store, bobCredentials), context) - .withLegacyCryptoStore(mCryptoTestHelper.USE_LEGACY_CRYPTO_STORE) - .build(); + MXSession bobSession2 = mTestHelper.createNewSession(bobSession, mCryptoTestHelper.getDefaultSessionParams()); final CountDownLatch lock1 = new CountDownLatch(1); MXStoreListener listener = new MXStoreListener() { @@ -276,7 +269,7 @@ public void onSuccess(MXUsersDevicesMap info) { // Continue testing other methods Assert.assertNotNull(bobSession.getCrypto().deviceWithIdentityKey(aliceSession.getCrypto().getOlmDevice().getDeviceCurve25519Key(), - aliceSession.getMyUserId(), CryptoConstantsKt.MXCRYPTO_ALGORITHM_OLM)); + CryptoConstantsKt.MXCRYPTO_ALGORITHM_OLM)); Assert.assertTrue(aliceDeviceFromBobPOV.isUnknown()); CountDownLatch lock3a = new CountDownLatch(1); @@ -318,15 +311,7 @@ public void onSuccess(Void info) { Assert.assertTrue(aliceDeviceFromBobPOV.isBlocked()); - Credentials bobCredentials = bobSession.getCredentials(); - - HomeServerConnectionConfig hs = mTestHelper.createHomeServerConfig(bobCredentials); - - IMXStore store = new MXFileStore(hs, false, context); - - MXSession bobSession2 = new MXSession.Builder(hs, new MXDataHandler(store, bobCredentials), context) - .withLegacyCryptoStore(mCryptoTestHelper.USE_LEGACY_CRYPTO_STORE) - .build(); + MXSession bobSession2 = mTestHelper.createNewSession(bobSession, mCryptoTestHelper.getDefaultSessionParams()); final CountDownLatch lock4 = new CountDownLatch(1); @@ -382,7 +367,7 @@ public void onCryptoSyncComplete() { MXDeviceInfo aliceDeviceFromBobPOV2 = bobSession2.getCrypto() .deviceWithIdentityKey(aliceSession.getCrypto().getOlmDevice().getDeviceCurve25519Key(), - aliceSession.getMyUserId(), CryptoConstantsKt.MXCRYPTO_ALGORITHM_OLM); + CryptoConstantsKt.MXCRYPTO_ALGORITHM_OLM); Assert.assertNotNull(aliceDeviceFromBobPOV2); Assert.assertEquals(aliceDeviceFromBobPOV2.fingerprint(), aliceSession.getCrypto().getOlmDevice().getDeviceEd25519Key()); @@ -403,7 +388,7 @@ public void onSuccess(MXUsersDevicesMap info) { Assert.assertTrue(results.containsKey("downloadKeys2")); MXDeviceInfo aliceDeviceFromBobPOV3 = bobSession2.getCrypto().deviceWithIdentityKey(aliceSession.getCrypto().getOlmDevice().getDeviceCurve25519Key(), - aliceSession.getMyUserId(), CryptoConstantsKt.MXCRYPTO_ALGORITHM_OLM); + CryptoConstantsKt.MXCRYPTO_ALGORITHM_OLM); Assert.assertNotNull(aliceDeviceFromBobPOV3); Assert.assertEquals(aliceDeviceFromBobPOV3.fingerprint(), aliceSession.getCrypto().getOlmDevice().getDeviceEd25519Key()); @@ -488,15 +473,7 @@ public void onSuccess(MXUsersDevicesMap info) { Assert.assertNotNull(sessionWithAliceDevice.mSessionId); Assert.assertEquals("AliceDevice", sessionWithAliceDevice.mDevice.deviceId); - Credentials bobCredentials = bobSession.getCredentials(); - - HomeServerConnectionConfig hs = mTestHelper.createHomeServerConfig(bobCredentials); - - IMXStore store = new MXFileStore(hs, false, context); - - MXSession bobSession2 = new MXSession.Builder(hs, new MXDataHandler(store, bobCredentials), context) - .withLegacyCryptoStore(mCryptoTestHelper.USE_LEGACY_CRYPTO_STORE) - .build(); + MXSession bobSession2 = mTestHelper.createNewSession(bobSession, mCryptoTestHelper.getDefaultSessionParams()); final CountDownLatch lock5 = new CountDownLatch(1); @@ -799,7 +776,8 @@ public void test08_testAliceAndBobInAEncryptedRoom2() throws Exception { @Override public void onLiveEvent(Event event, RoomState roomState) { if (TextUtils.equals(event.getType(), Event.EVENT_TYPE_MESSAGE) && !TextUtils.equals(event.getSender(), bobSession.getMyUserId())) { - mCryptoTestHelper.checkEncryptedEvent(event, aliceRoomId, mCryptoTestHelper.getMessagesFromAlice().get(nbReceivedMessagesFromAlice[0]), aliceSession); + mCryptoTestHelper.checkEncryptedEvent(event, aliceRoomId, + mCryptoTestHelper.getMessagesFromAlice().get(nbReceivedMessagesFromAlice[0]), aliceSession); nbReceivedMessagesFromAlice[0]++; list.get(list.size() - 1).countDown(); @@ -811,7 +789,8 @@ public void onLiveEvent(Event event, RoomState roomState) { @Override public void onLiveEvent(Event event, RoomState roomState) { if (TextUtils.equals(event.getType(), Event.EVENT_TYPE_MESSAGE) && !TextUtils.equals(event.getSender(), aliceSession.getMyUserId())) { - mCryptoTestHelper.checkEncryptedEvent(event, aliceRoomId, mCryptoTestHelper.getMessagesFromBob().get(nbReceivedMessagesFromBob[0]), bobSession); + mCryptoTestHelper.checkEncryptedEvent(event, aliceRoomId, + mCryptoTestHelper.getMessagesFromBob().get(nbReceivedMessagesFromBob[0]), bobSession); nbReceivedMessagesFromBob[0]++; list.get(list.size() - 1).countDown(); @@ -840,28 +819,33 @@ public void onToDeviceEvent(Event event) { } }); - roomFromAlicePOV.sendEvent(mCryptoTestHelper.buildTextEvent(mCryptoTestHelper.getMessagesFromAlice().get(nbReceivedMessagesFromAlice[0]), aliceSession, aliceRoomId), callback); + roomFromAlicePOV.sendEvent(mCryptoTestHelper.buildTextEvent( + mCryptoTestHelper.getMessagesFromAlice().get(nbReceivedMessagesFromAlice[0]), aliceSession, aliceRoomId), callback); mTestHelper.await(list.get(list.size() - 1)); Assert.assertTrue(results.containsKey("onToDeviceEvent")); Assert.assertEquals(1, nbReceivedMessagesFromAlice[0]); list.add(new CountDownLatch(1)); - roomFromBobPOV.sendEvent(mCryptoTestHelper.buildTextEvent(mCryptoTestHelper.getMessagesFromBob().get(nbReceivedMessagesFromBob[0]), bobSession, aliceRoomId), callback); + roomFromBobPOV.sendEvent(mCryptoTestHelper.buildTextEvent( + mCryptoTestHelper.getMessagesFromBob().get(nbReceivedMessagesFromBob[0]), bobSession, aliceRoomId), callback); mTestHelper.await(list.get(list.size() - 1)); Assert.assertEquals(1, nbReceivedMessagesFromBob[0]); list.add(new CountDownLatch(1)); - roomFromBobPOV.sendEvent(mCryptoTestHelper.buildTextEvent(mCryptoTestHelper.getMessagesFromBob().get(nbReceivedMessagesFromBob[0]), bobSession, aliceRoomId), callback); + roomFromBobPOV.sendEvent(mCryptoTestHelper.buildTextEvent( + mCryptoTestHelper.getMessagesFromBob().get(nbReceivedMessagesFromBob[0]), bobSession, aliceRoomId), callback); mTestHelper.await(list.get(list.size() - 1)); Assert.assertEquals(2, nbReceivedMessagesFromBob[0]); list.add(new CountDownLatch(1)); - roomFromBobPOV.sendEvent(mCryptoTestHelper.buildTextEvent(mCryptoTestHelper.getMessagesFromBob().get(nbReceivedMessagesFromBob[0]), bobSession, aliceRoomId), callback); + roomFromBobPOV.sendEvent(mCryptoTestHelper.buildTextEvent( + mCryptoTestHelper.getMessagesFromBob().get(nbReceivedMessagesFromBob[0]), bobSession, aliceRoomId), callback); mTestHelper.await(list.get(list.size() - 1)); Assert.assertEquals(3, nbReceivedMessagesFromBob[0]); list.add(new CountDownLatch(1)); - roomFromAlicePOV.sendEvent(mCryptoTestHelper.buildTextEvent(mCryptoTestHelper.getMessagesFromAlice().get(nbReceivedMessagesFromAlice[0]), aliceSession, aliceRoomId), callback); + roomFromAlicePOV.sendEvent(mCryptoTestHelper.buildTextEvent( + mCryptoTestHelper.getMessagesFromAlice().get(nbReceivedMessagesFromAlice[0]), aliceSession, aliceRoomId), callback); mTestHelper.await(list.get(list.size() - 1)); Assert.assertEquals(2, nbReceivedMessagesFromAlice[0]); @@ -894,7 +878,7 @@ public void test09_testAliceInAEncryptedRoomAfterInitialSync() throws Exception final CountDownLatch lock1 = new CountDownLatch(1); final MXSession aliceSession2 = new MXSession.Builder(hs, new MXDataHandler(store, aliceCredentials), context) - .withLegacyCryptoStore(mCryptoTestHelper.USE_LEGACY_CRYPTO_STORE) + .withLegacyCryptoStore(mCryptoTestHelper.getUSE_LEGACY_CRYPTO_STORE()) .build(); MXStoreListener listener = new MXStoreListener() { @@ -1029,7 +1013,7 @@ public void onSuccess(Void info) { IMXStore store = new MXFileStore(hs, false, context); MXSession aliceSession2 = new MXSession.Builder(hs, new MXDataHandler(store, aliceCredentials2), context) - .withLegacyCryptoStore(mCryptoTestHelper.USE_LEGACY_CRYPTO_STORE) + .withLegacyCryptoStore(mCryptoTestHelper.getUSE_LEGACY_CRYPTO_STORE()) .build(); aliceSession2.enableCryptoWhenStarting(); @@ -1123,7 +1107,7 @@ public void test11_testAliceAndBobInAEncryptedRoomBackPaginationFromMemoryStore( final CountDownLatch lock1 = new CountDownLatch(2); MXSession bobSession2 = new MXSession.Builder(hs, new MXDataHandler(store, bobCredentials), context) - .withLegacyCryptoStore(mCryptoTestHelper.USE_LEGACY_CRYPTO_STORE) + .withLegacyCryptoStore(mCryptoTestHelper.getUSE_LEGACY_CRYPTO_STORE()) .build(); MXEventListener eventListener = new MXEventListener() { @@ -2442,7 +2426,7 @@ public void onSuccess(byte[] info) { IMXStore store = new MXFileStore(hs, false, context); MXSession aliceSession2 = new MXSession.Builder(hs, new MXDataHandler(store, aliceCredentials2), context) - .withLegacyCryptoStore(mCryptoTestHelper.USE_LEGACY_CRYPTO_STORE) + .withLegacyCryptoStore(mCryptoTestHelper.getUSE_LEGACY_CRYPTO_STORE()) .build(); aliceSession2.enableCryptoWhenStarting(); @@ -3297,7 +3281,8 @@ public void onLiveEvent(Event event, RoomState roomState) { if (TextUtils.equals(event.getType(), Event.EVENT_TYPE_MESSAGE) && !TextUtils.equals(event.getSender(), bobSession.getMyUserId())) { bobReceivedEvents.add(event); - mCryptoTestHelper.checkEncryptedEvent(event, aliceRoomId, mCryptoTestHelper.getMessagesFromAlice().get(nbReceivedMessagesFromAlice[0]), aliceSession); + mCryptoTestHelper.checkEncryptedEvent(event, aliceRoomId, + mCryptoTestHelper.getMessagesFromAlice().get(nbReceivedMessagesFromAlice[0]), aliceSession); nbReceivedMessagesFromAlice[0]++; list.get(list.size() - 1).countDown(); @@ -3353,7 +3338,8 @@ public void onToDeviceEvent(Event event) { }); // Alice sends a first event - roomFromAlicePOV.sendEvent(mCryptoTestHelper.buildTextEvent(mCryptoTestHelper.getMessagesFromAlice().get(nbReceivedMessagesFromAlice[0]), aliceSession, aliceRoomId), callback); + roomFromAlicePOV.sendEvent(mCryptoTestHelper.buildTextEvent(mCryptoTestHelper.getMessagesFromAlice().get(nbReceivedMessagesFromAlice[0]), + aliceSession, aliceRoomId), callback); mTestHelper.await(list.get(list.size() - 1)); Assert.assertTrue(results.containsKey("onToDeviceEvent")); Assert.assertEquals(1, nbReceivedMessagesFromAlice[0]); @@ -3362,7 +3348,8 @@ public void onToDeviceEvent(Event event) { Assert.assertTrue(roomFromBobPOV.canReplyTo(bobReceivedEvents.get(0))); list.add(new CountDownLatch(1)); - roomFromBobPOV.sendTextMessage(mCryptoTestHelper.getMessagesFromBob().get(nbReceivedMessagesFromBob[0]), null, Message.MSGTYPE_TEXT, bobReceivedEvents.get(0), null); + roomFromBobPOV.sendTextMessage(mCryptoTestHelper.getMessagesFromBob().get(nbReceivedMessagesFromBob[0]), + null, Message.MSGTYPE_TEXT, bobReceivedEvents.get(0), null); mTestHelper.await(list.get(list.size() - 1)); Assert.assertEquals(1, nbReceivedMessagesFromBob[0]); diff --git a/matrix-sdk/src/androidTest/java/org/matrix/androidsdk/crypto/KeysBackupTest.kt b/matrix-sdk/src/androidTest/java/org/matrix/androidsdk/crypto/KeysBackupTest.kt new file mode 100644 index 000000000..2988d8fdd --- /dev/null +++ b/matrix-sdk/src/androidTest/java/org/matrix/androidsdk/crypto/KeysBackupTest.kt @@ -0,0 +1,744 @@ +/* + * Copyright 2018 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 org.matrix.androidsdk.crypto + +import android.support.test.InstrumentationRegistry +import android.support.test.runner.AndroidJUnit4 +import org.junit.Assert.* +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.matrix.androidsdk.common.* +import org.matrix.androidsdk.crypto.data.ImportRoomKeysResult +import org.matrix.androidsdk.crypto.data.MXDeviceInfo +import org.matrix.androidsdk.crypto.keysbackup.KeyBackupVersionTrust +import org.matrix.androidsdk.crypto.keysbackup.KeysBackup +import org.matrix.androidsdk.crypto.keysbackup.KeysBackupStateManager +import org.matrix.androidsdk.crypto.keysbackup.MegolmBackupCreationInfo +import org.matrix.androidsdk.rest.callback.SuccessCallback +import org.matrix.androidsdk.rest.callback.SuccessErrorCallback +import org.matrix.androidsdk.rest.model.keys.CreateKeysBackupVersionBody +import org.matrix.androidsdk.rest.model.keys.KeysVersion +import org.matrix.androidsdk.rest.model.keys.KeysVersionResult +import org.matrix.androidsdk.util.JsonUtils +import java.util.concurrent.CountDownLatch + +@RunWith(AndroidJUnit4::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class KeysBackupTest { + + private val mTestHelper = CommonTestHelper() + private val mCryptoTestHelper = CryptoTestHelper(mTestHelper) + + private val defaultSessionParams = SessionTestParams( + withInitialSync = false, + withCryptoEnabled = true, + withLazyLoading = true, + withLegacyCryptoStore = false) + private val defaultSessionParamsWithInitialSync = SessionTestParams( + withInitialSync = true, + withCryptoEnabled = true, + withLazyLoading = true, + withLegacyCryptoStore = false) + + /** + * - From doE2ETestWithAliceAndBobInARoomWithEncryptedMessages, we should have no backed up keys + * - Check backup keys after having marked one as backed up + * - Reset keys backup markers + */ + @Test + fun roomKeysTest_testBackupStore_ok() { + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages(true) + + val store = cryptoTestData.firstSession.crypto!!.cryptoStore + + // From doE2ETestWithAliceAndBobInARoomWithEncryptedMessages, we should have no backed up keys + val sessions = store.inboundGroupSessionsToBackup(100) + val sessionsCount = sessions.size + + assertFalse(sessions.isEmpty()) + assertEquals(sessionsCount, store.inboundGroupSessionsCount(false)) + assertEquals(0, store.inboundGroupSessionsCount(true)) + + // - Check backup keys after having marked one as backed up + val session = sessions[0] + + store.markBackupDoneForInboundGroupSessionWithId(session.mSession.sessionIdentifier(), session.mSenderKey) + + assertEquals(sessionsCount, store.inboundGroupSessionsCount(false)) + assertEquals(1, store.inboundGroupSessionsCount(true)) + + val sessions2 = store.inboundGroupSessionsToBackup(100) + assertEquals(sessionsCount - 1, sessions2.size) + + // - Reset keys backup markers + store.resetBackupMarkers() + + val sessions3 = store.inboundGroupSessionsToBackup(100) + assertEquals(sessionsCount, sessions3.size) + assertEquals(sessionsCount, store.inboundGroupSessionsCount(false)) + assertEquals(0, store.inboundGroupSessionsCount(true)) + } + + /** + * Check that prepareKeysBackupVersion returns valid data + */ + @Test + fun prepareKeysBackupVersionTest() { + val bobSession = mTestHelper.createAccount(TestConstants.USER_BOB, defaultSessionParams) + + bobSession.enableCrypto(mTestHelper) + + assertNotNull(bobSession.crypto) + assertNotNull(bobSession.crypto!!.keysBackup) + + val keysBackup = bobSession.crypto!!.keysBackup + + assertFalse(keysBackup.isEnabled) + + val latch = CountDownLatch(1) + + keysBackup.prepareKeysBackupVersion(object : SuccessErrorCallback { + override fun onSuccess(info: MegolmBackupCreationInfo?) { + assertNotNull(info) + + assertEquals(MXCRYPTO_ALGORITHM_MEGOLM_BACKUP, info!!.algorithm) + assertNotNull(info.authData) + assertNotNull(info.authData!!.publicKey) + assertNotNull(info.authData!!.signatures) + assertNotNull(info.recoveryKey) + + latch.countDown() + } + + override fun onUnexpectedError(e: Exception?) { + fail(e?.localizedMessage) + + latch.countDown() + } + }) + latch.await() + + bobSession.clear(InstrumentationRegistry.getContext()) + } + + /** + * Test creating a keys backup version and check that createKeyBackupVersion() returns valid data + */ + @Test + fun createKeyBackupVersionTest() { + val bobSession = mTestHelper.createAccount(TestConstants.USER_BOB, defaultSessionParams) + bobSession.enableCrypto(mTestHelper) + + val keysBackup = bobSession.crypto!!.keysBackup + + assertFalse(keysBackup.isEnabled) + + var megolmBackupCreationInfo: MegolmBackupCreationInfo? = null + val latch = CountDownLatch(1) + keysBackup.prepareKeysBackupVersion(object : SuccessErrorCallback { + + override fun onSuccess(info: MegolmBackupCreationInfo) { + megolmBackupCreationInfo = info + + latch.countDown() + } + + override fun onUnexpectedError(e: Exception) { + fail(e.localizedMessage) + + latch.countDown() + } + }) + latch.await() + + assertNotNull(megolmBackupCreationInfo) + + assertFalse(keysBackup.isEnabled) + + val latch2 = CountDownLatch(1) + + // Create the version + keysBackup.createKeyBackupVersion(megolmBackupCreationInfo!!, object : TestApiCallback(latch2) { + override fun onSuccess(info: KeysVersion) { + assertNotNull(info) + assertNotNull(info.version) + + super.onSuccess(info) + } + }) + mTestHelper.await(latch2) + + // Backup must be enable now + assertTrue(keysBackup.isEnabled) + + bobSession.clear(InstrumentationRegistry.getContext()) + } + + /** + * - Check that createKeyBackupVersion() launches the backup + * - Check the backup completes + */ + @Test + fun backupAfterCreateKeyBackupVersionTest() { + val context = InstrumentationRegistry.getContext() + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages(true) + + val cryptoStore = cryptoTestData.firstSession.crypto!!.cryptoStore + val keysBackup = cryptoTestData.firstSession.crypto!!.keysBackup + + val latch = CountDownLatch(1) + var counter = 0 + + assertEquals(2, cryptoStore.inboundGroupSessionsCount(false)) + assertEquals(0, cryptoStore.inboundGroupSessionsCount(true)) + + keysBackup.addListener(object : KeysBackupStateManager.KeysBackupStateListener { + override fun onStateChange(newState: KeysBackupStateManager.KeysBackupState) { + // Check the several backup state changes + when (counter) { + 0 -> assertEquals(KeysBackupStateManager.KeysBackupState.ReadyToBackUp, newState) + 1 -> assertEquals(KeysBackupStateManager.KeysBackupState.WillBackUp, newState) + 2 -> assertEquals(KeysBackupStateManager.KeysBackupState.BackingUp, newState) + 3 -> { + assertEquals(KeysBackupStateManager.KeysBackupState.ReadyToBackUp, newState) + + // Last state + // Remove itself from the list of listeners + keysBackup.removeListener(this) + + latch.countDown() + } + } + counter++ + } + }) + + prepareAndCreateKeyBackupData(keysBackup) + + mTestHelper.await(latch) + + val nbOfKeys = cryptoStore.inboundGroupSessionsCount(false) + val backedUpKeys = cryptoStore.inboundGroupSessionsCount(true) + + assertEquals(2, nbOfKeys) + assertEquals("All keys must have been marked as backed up", nbOfKeys, backedUpKeys) + + cryptoTestData.clear(context) + } + + + /** + * Check that backupAllGroupSessions() returns valid data + */ + @Test + fun backupAllGroupSessionsTest() { + val context = InstrumentationRegistry.getContext() + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages(true) + + val cryptoStore = cryptoTestData.firstSession.crypto!!.cryptoStore + val keysBackup = cryptoTestData.firstSession.crypto!!.keysBackup + + prepareAndCreateKeyBackupData(keysBackup) + + // Check that backupAllGroupSessions returns valid data + val nbOfKeys = cryptoStore.inboundGroupSessionsCount(false) + + assertEquals(2, nbOfKeys) + + val latch = CountDownLatch(1) + + var lastBackedUpKeysProgress = 0 + + keysBackup.backupAllGroupSessions(object : KeysBackup.BackupProgressListener { + override fun onProgress(backedUp: Int, total: Int) { + assertEquals(nbOfKeys, total) + lastBackedUpKeysProgress = backedUp + } + + }, TestApiCallback(latch)) + + mTestHelper.await(latch) + assertEquals(nbOfKeys, lastBackedUpKeysProgress) + + val backedUpKeys = cryptoStore.inboundGroupSessionsCount(true) + + assertEquals("All keys must have been marked as backed up", nbOfKeys, backedUpKeys) + + cryptoTestData.clear(context) + } + + /** + * Check encryption and decryption of megolm keys in the backup. + * - Pick a megolm key + * - Check [MXKeyBackup encryptGroupSession] returns stg + * - Check [MXKeyBackup pkDecryptionFromRecoveryKey] is able to create a OLMPkDecryption + * - Check [MXKeyBackup decryptKeyBackupData] returns stg + * - Compare the decrypted megolm key with the original one + */ + @Test + fun testEncryptAndDecryptKeyBackupData() { + val context = InstrumentationRegistry.getContext() + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages(true) + + val cryptoStore = cryptoTestData.firstSession.crypto!!.cryptoStore + val keysBackup = cryptoTestData.firstSession.crypto!!.keysBackup + + // - Pick a megolm key + val session = cryptoStore.inboundGroupSessionsToBackup(1)[0] + + val keyBackupCreationInfo = prepareAndCreateKeyBackupData(keysBackup).megolmBackupCreationInfo + + // - Check encryptGroupSession() returns stg + val keyBackupData = keysBackup.encryptGroupSession(session) + assertNotNull(keyBackupData) + assertNotNull(keyBackupData.sessionData) + + // - Check pkDecryptionFromRecoveryKey() is able to create a OlmPkDecryption + val decryption = keysBackup.pkDecryptionFromRecoveryKey(keyBackupCreationInfo.recoveryKey) + assertNotNull(decryption) + // - Check decryptKeyBackupData() returns stg + val sessionData = keysBackup.decryptKeyBackupData(keyBackupData, session.mSession.sessionIdentifier(), cryptoTestData.roomId, decryption!!) + assertNotNull(sessionData) + // - Compare the decrypted megolm key with the original one + assertKeysEquals(session.exportKeys(), sessionData) + + cryptoTestData.clear(context) + } + + /** + * - Do an e2e backup to the homeserver + * - Log Alice on a new device + * - Restore the e2e backup from the homeserver + * - Imported keys number must be correct + * - The new device must have the same count of megolm keys + * - Alice must have the same keys on both devices + */ + @Test + fun restoreKeyBackupTest() { + val context = InstrumentationRegistry.getContext() + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages(true) + + val cryptoStore = cryptoTestData.firstSession.crypto!!.cryptoStore + val keysBackup = cryptoTestData.firstSession.crypto!!.keysBackup + + val aliceKeys1 = cryptoStore.inboundGroupSessionsToBackup(100) + + // - Do an e2e backup to the homeserver + val prepareKeyBackupDataResult = prepareAndCreateKeyBackupData(keysBackup) + + val latch = CountDownLatch(1) + var lastBackup = 0 + var lastTotal = 0 + keysBackup.backupAllGroupSessions(object : KeysBackup.BackupProgressListener { + override fun onProgress(backedUp: Int, total: Int) { + lastBackup = backedUp + lastTotal = total + } + }, TestApiCallback(latch)) + mTestHelper.await(latch) + + assertEquals(2, lastBackup) + assertEquals(2, lastTotal) + + // - Log Alice on a new device + val aliceSession2 = mTestHelper.logIntoAccount(cryptoTestData.firstSession.myUserId, defaultSessionParamsWithInitialSync) + + // Test check: aliceSession2 has no keys at login + assertEquals(0, aliceSession2.crypto!!.cryptoStore.inboundGroupSessionsCount(false)) + + // - Restore the e2e backup from the homeserver + val latch2 = CountDownLatch(1) + var importRoomKeysResult: ImportRoomKeysResult? = null + aliceSession2.crypto!!.keysBackup.restoreKeyBackup(prepareKeyBackupDataResult.version, + prepareKeyBackupDataResult.megolmBackupCreationInfo.recoveryKey, + null, + null, + object : TestApiCallback(latch2) { + override fun onSuccess(info: ImportRoomKeysResult) { + importRoomKeysResult = info + super.onSuccess(info) + } + } + ) + mTestHelper.await(latch2) + + // - Imported keys number must be correct + assertEquals(aliceKeys1.size, importRoomKeysResult!!.totalNumberOfKeys) + assertEquals(importRoomKeysResult!!.totalNumberOfKeys, importRoomKeysResult!!.successfullyNumberOfImportedKeys) + // - The new device must have the same count of megolm keys + assertEquals(aliceKeys1.size, aliceSession2.crypto!!.cryptoStore.inboundGroupSessionsCount(false)) + // - Alice must have the same keys on both devices + for (aliceKey1 in aliceKeys1) { + val aliceKey2 = aliceSession2.crypto!! + .cryptoStore.getInboundGroupSession(aliceKey1.mSession.sessionIdentifier(), aliceKey1.mSenderKey) + assertNotNull(aliceKey2) + assertKeysEquals(aliceKey1.exportKeys(), aliceKey2!!.exportKeys()) + } + + cryptoTestData.clear(context) + } + + /** + * - Create a backup version + * - Check the returned MXKeyBackupVersion is trusted + */ + @Test + fun testIsKeyBackupTrusted() { + // - Create a backup version + val context = InstrumentationRegistry.getContext() + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages(true) + + val keysBackup = cryptoTestData.firstSession.crypto!!.keysBackup + + // - Do an e2e backup to the homeserver + prepareAndCreateKeyBackupData(keysBackup) + + // Get key backup version from the home server + var keysVersionResult: KeysVersionResult? = null + val lock = CountDownLatch(1) + keysBackup.getCurrentVersion(object : TestApiCallback(lock) { + override fun onSuccess(info: KeysVersionResult?) { + keysVersionResult = info + super.onSuccess(info) + } + }) + mTestHelper.await(lock) + + assertNotNull(keysVersionResult) + + // - Check the returned KeyBackupVersion is trusted + val latch = CountDownLatch(1) + var keyBackupVersionTrust: KeyBackupVersionTrust? = null + keysBackup.isKeyBackupTrusted(keysVersionResult!!, SuccessCallback { info -> + keyBackupVersionTrust = info + + latch.countDown() + }) + mTestHelper.await(latch) + + assertNotNull(keyBackupVersionTrust) + assertTrue(keyBackupVersionTrust!!.usable) + assertEquals(1, keyBackupVersionTrust!!.signatures.size) + + val signature = keyBackupVersionTrust!!.signatures[0] + assertTrue(signature.valid) + assertNotNull(signature.device) + assertEquals(signature.device!!.deviceId, cryptoTestData.firstSession.credentials.deviceId) + + cryptoTestData.clear(context) + } + + /** + * Check backup starts automatically if there is an existing and compatible backup + * version on the homeserver. + * - Create a backup version + * - Restart alice session + * -> The new alice session must back up to the same version + */ + @Test + fun testCheckAndStartKeyBackupWhenRestartingAMatrixSession() { + // - Create a backup version + val context = InstrumentationRegistry.getContext() + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages(true) + + val keysBackup = cryptoTestData.firstSession.crypto!!.keysBackup + + assertFalse(keysBackup.isEnabled) + + val keyBackupCreationInfo = prepareAndCreateKeyBackupData(keysBackup) + + assertTrue(keysBackup.isEnabled) + + // - Restart alice session + // - Log Alice on a new device + val aliceSession2 = mTestHelper.logIntoAccount(cryptoTestData.firstSession.myUserId, defaultSessionParamsWithInitialSync) + + cryptoTestData.clear(context) + + val keysBackup2 = aliceSession2.crypto!!.keysBackup + + // -> The new alice session must back up to the same version + val latch = CountDownLatch(1) + var count = 0 + keysBackup2.addListener(object : KeysBackupStateManager.KeysBackupStateListener { + override fun onStateChange(newState: KeysBackupStateManager.KeysBackupState) { + // Check the backup completes + if (keysBackup.state == KeysBackupStateManager.KeysBackupState.ReadyToBackUp) { + count++ + + if (count == 2) { + // Remove itself from the list of listeners + keysBackup.removeListener(this) + + latch.countDown() + } + } + } + }) + mTestHelper.await(latch) + + assertEquals(keyBackupCreationInfo.version, keysBackup2.currentBackupVersion) + + aliceSession2.clear(context) + } + + /** + * Check WrongBackUpVersion state + * + * - Make alice back up her keys to her homeserver + * - Create a new backup with fake data on the homeserver + * - Make alice back up all her keys again + * -> That must fail and her backup state must be disabled + */ + @Test + fun testBackupWhenAnotherBackupWasCreated() { + // - Create a backup version + val context = InstrumentationRegistry.getContext() + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages(true) + + val keysBackup = cryptoTestData.firstSession.crypto!!.keysBackup + + assertFalse(keysBackup.isEnabled) + + // Wait for keys backup to be finished + val latch0 = CountDownLatch(1) + var count = 0 + keysBackup.addListener(object : KeysBackupStateManager.KeysBackupStateListener { + override fun onStateChange(newState: KeysBackupStateManager.KeysBackupState) { + // Check the backup completes + if (keysBackup.state == KeysBackupStateManager.KeysBackupState.ReadyToBackUp) { + count++ + + if (count == 2) { + // Remove itself from the list of listeners + keysBackup.removeListener(this) + + latch0.countDown() + } + } + } + }) + + // - Make alice back up her keys to her homeserver + prepareAndCreateKeyBackupData(keysBackup) + + assertTrue(keysBackup.isEnabled) + + mTestHelper.await(latch0) + + // - Create a new backup with fake data on the homeserver, directly using the rest client + val latch = CountDownLatch(1) + + val megolmBackupCreationInfo = mCryptoTestHelper.createFakeMegolmBackupCreationInfo() + val createKeysBackupVersionBody = CreateKeysBackupVersionBody() + createKeysBackupVersionBody.algorithm = megolmBackupCreationInfo.algorithm + createKeysBackupVersionBody.authData = JsonUtils.getBasicGson().toJsonTree(megolmBackupCreationInfo.authData) + cryptoTestData.firstSession.roomKeysRestClient.createKeysBackupVersion(createKeysBackupVersionBody, TestApiCallback(latch)) + mTestHelper.await(latch) + + // Reset the store backup status for keys + cryptoTestData.firstSession.crypto!!.cryptoStore.resetBackupMarkers() + + // - Make alice back up all her keys again + val latch2 = CountDownLatch(1) + keysBackup.backupAllGroupSessions(object : KeysBackup.BackupProgressListener { + override fun onProgress(backedUp: Int, total: Int) { + } + + }, TestApiCallback(latch2, false)) + mTestHelper.await(latch2) + + // -> That must fail and her backup state must be disabled + assertEquals(KeysBackupStateManager.KeysBackupState.WrongBackUpVersion, keysBackup.state) + assertFalse(keysBackup.isEnabled) + + cryptoTestData.clear(context) + } + + /** + * - Do an e2e backup to the homeserver + * - Log Alice on a new device + * - Post a message to have a new megolm session + * - Try to backup all + * -> It must fail + * - Validate the old device from the new one + * -> Backup should automatically enable on the new device + * -> It must use the same backup version + * - Try to backup all again + * -> It must success + */ + @Test + fun testBackupAfterVerifyingADevice() { + // - Create a backup version + val context = InstrumentationRegistry.getContext() + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages(true) + + val keysBackup = cryptoTestData.firstSession.crypto!!.keysBackup + + // - Make alice back up her keys to her homeserver + prepareAndCreateKeyBackupData(keysBackup) + + // Wait for keys backup to finish by asking again to backup keys. + val latch = CountDownLatch(1) + keysBackup.backupAllGroupSessions(object : KeysBackup.BackupProgressListener { + override fun onProgress(backedUp: Int, total: Int) { + + } + }, TestApiCallback(latch)) + mTestHelper.await(latch) + + val oldDeviceId = cryptoTestData.firstSession.credentials.deviceId + val oldKeyBackupVersion = keysBackup.currentBackupVersion + val aliceUserId = cryptoTestData.firstSession.myUserId + + // Close first Alice session, else they will share the same Crypto store and the test fails. + cryptoTestData.firstSession.clear(context) + + // - Log Alice on a new device + val aliceSession2 = mTestHelper.logIntoAccount(aliceUserId, defaultSessionParamsWithInitialSync) + + // - Post a message to have a new megolm session + aliceSession2.crypto!!.setWarnOnUnknownDevices(false) + + val room2 = aliceSession2.dataHandler.getRoom(cryptoTestData.roomId) + + mTestHelper.sendTextMessage(room2, "New key", 1) + + // - Try to backup all in aliceSession2, it must fail + val keysBackup2 = aliceSession2.crypto!!.keysBackup + + var isSuccessful = false + val latch2 = CountDownLatch(1) + keysBackup2.backupAllGroupSessions(object : KeysBackup.BackupProgressListener { + override fun onProgress(backedUp: Int, total: Int) { + } + + }, object : TestApiCallback(latch2, false) { + override fun onSuccess(info: Void?) { + isSuccessful = true + super.onSuccess(info) + } + }) + mTestHelper.await(latch2) + + assertFalse(isSuccessful) + assertFalse(keysBackup2.isEnabled) + + // - Validate the old device from the new one + val latch3 = CountDownLatch(1) + aliceSession2.crypto!!.setDeviceVerification(MXDeviceInfo.DEVICE_VERIFICATION_VERIFIED, oldDeviceId, aliceSession2.myUserId, TestApiCallback(latch3)) + mTestHelper.await(latch3) + + // -> Backup should automatically enable on the new device + val latch4 = CountDownLatch(1) + keysBackup2.addListener(object : KeysBackupStateManager.KeysBackupStateListener { + override fun onStateChange(newState: KeysBackupStateManager.KeysBackupState) { + // Check the backup completes + if (keysBackup2.state == KeysBackupStateManager.KeysBackupState.ReadyToBackUp) { + // Remove itself from the list of listeners + keysBackup2.removeListener(this) + + latch4.countDown() + } + } + }) + mTestHelper.await(latch4) + + // -> It must use the same backup version + assertEquals(oldKeyBackupVersion, aliceSession2.crypto!!.keysBackup.currentBackupVersion) + + val latch5 = CountDownLatch(1) + aliceSession2.crypto!!.keysBackup.backupAllGroupSessions(null, TestApiCallback(latch5)) + mTestHelper.await(latch5) + + // -> It must success + assertTrue(aliceSession2.crypto!!.keysBackup.isEnabled) + + aliceSession2.clear(context) + cryptoTestData.clear(context) + } + + /* ========================================================================================== + * Private + * ========================================================================================== */ + + private data class PrepareKeyBackupDataResult(val megolmBackupCreationInfo: MegolmBackupCreationInfo, + val version: String) + + private fun prepareAndCreateKeyBackupData(keysBackup: KeysBackup): PrepareKeyBackupDataResult { + var megolmBackupCreationInfo: MegolmBackupCreationInfo? = null + val latch = CountDownLatch(1) + keysBackup.prepareKeysBackupVersion(object : SuccessErrorCallback { + + override fun onSuccess(info: MegolmBackupCreationInfo) { + megolmBackupCreationInfo = info + + latch.countDown() + } + + override fun onUnexpectedError(e: Exception) { + fail(e.localizedMessage) + + latch.countDown() + } + }) + mTestHelper.await(latch) + + assertNotNull(megolmBackupCreationInfo) + + assertFalse(keysBackup.isEnabled) + + val latch2 = CountDownLatch(1) + + // Create the version + var version: String? = null + keysBackup.createKeyBackupVersion(megolmBackupCreationInfo!!, object : TestApiCallback(latch2) { + override fun onSuccess(info: KeysVersion) { + assertNotNull(info) + assertNotNull(info.version) + + version = info.version + + // Backup must be enable now + assertTrue(keysBackup.isEnabled) + + super.onSuccess(info) + } + }) + mTestHelper.await(latch2) + + return PrepareKeyBackupDataResult(megolmBackupCreationInfo!!, version!!) + } + + private fun assertKeysEquals(keys1: MegolmSessionData?, keys2: MegolmSessionData?) { + assertNotNull(keys1) + assertNotNull(keys2) + + assertEquals(keys1?.algorithm, keys2?.algorithm) + assertEquals(keys1?.room_id, keys2?.room_id) + // No need to compare the shortcut + // assertEquals(keys1?.sender_claimed_ed25519_key, keys2?.sender_claimed_ed25519_key) + assertEquals(keys1?.sender_key, keys2?.sender_key) + assertEquals(keys1?.session_id, keys2?.session_id) + assertEquals(keys1?.session_key, keys2?.session_key) + + assertListEquals(keys1?.forwardingCurve25519KeyChain, keys2?.forwardingCurve25519KeyChain) + assertDictEquals(keys1?.sender_claimed_keys, keys2?.sender_claimed_keys) + } +} \ No newline at end of file diff --git a/matrix-sdk/src/androidTest/java/org/matrix/androidsdk/lazyloading/RoomNameTest.java b/matrix-sdk/src/androidTest/java/org/matrix/androidsdk/lazyloading/RoomNameTest.java index 4ad38cb17..7e3ce91fc 100644 --- a/matrix-sdk/src/androidTest/java/org/matrix/androidsdk/lazyloading/RoomNameTest.java +++ b/matrix-sdk/src/androidTest/java/org/matrix/androidsdk/lazyloading/RoomNameTest.java @@ -18,14 +18,12 @@ import android.support.test.InstrumentationRegistry; -import junit.framework.Assert; - +import org.junit.Assert; import org.junit.BeforeClass; import org.junit.FixMethodOrder; import org.junit.Test; import org.junit.runners.MethodSorters; import org.matrix.androidsdk.MXSession; -import org.matrix.androidsdk.RestClient; import org.matrix.androidsdk.common.CommonTestHelper; import java.util.Arrays; diff --git a/matrix-sdk/src/androidTest/java/org/matrix/androidsdk/roomkeys/RoomKeysRestClientTest.kt b/matrix-sdk/src/androidTest/java/org/matrix/androidsdk/roomkeys/RoomKeysRestClientTest.kt new file mode 100644 index 000000000..2cc865a2a --- /dev/null +++ b/matrix-sdk/src/androidTest/java/org/matrix/androidsdk/roomkeys/RoomKeysRestClientTest.kt @@ -0,0 +1,313 @@ +/* + * Copyright 2018 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 org.matrix.androidsdk.roomkeys + +import android.support.test.InstrumentationRegistry +import android.support.test.runner.AndroidJUnit4 +import org.junit.Assert.* +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.matrix.androidsdk.common.* +import org.matrix.androidsdk.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP +import org.matrix.androidsdk.crypto.keysbackup.MegolmBackupAuthData +import org.matrix.androidsdk.rest.model.MatrixError +import org.matrix.androidsdk.rest.model.keys.* +import org.matrix.androidsdk.util.JsonUtils +import org.matrix.androidsdk.util.Log +import java.util.* +import java.util.concurrent.CountDownLatch + +@RunWith(AndroidJUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class RoomKeysRestClientTest { + + private val mTestHelper = CommonTestHelper() + private val mCryptoTestHelper = CryptoTestHelper(mTestHelper) + + @Test + fun roomKeysTest_getKeysBackupVersion_noBackup() { + Log.e(LOG_TAG, "RoomKeysTest_getVersion") + + val context = InstrumentationRegistry.getContext() + val bobSession = mTestHelper.createAccount(TestConstants.USER_BOB, SessionTestParams()) + + val lock = CountDownLatch(2) + bobSession.roomKeysRestClient + .getKeysBackupLastVersion(object : TestApiCallback(lock, false) { + override fun onMatrixError(e: MatrixError) { + // Error is NOT_FOUND + assertEquals(MatrixError.NOT_FOUND, e.errcode) + super.onMatrixError(e) + } + }) + + bobSession.roomKeysRestClient + .getKeysBackupVersion("1", object : TestApiCallback(lock, false) { + override fun onMatrixError(e: MatrixError) { + assertEquals(MatrixError.NOT_FOUND, e.errcode) + super.onMatrixError(e) + } + }) + mTestHelper.await(lock) + + bobSession.clear(context) + } + + /** + * - Create a backup version on the server + * - Get the current version from the server + * -> Check they match + */ + @Test + fun roomKeysTest_createVersionAndRetrieveIt_ok() { + Log.e(LOG_TAG, "RoomKeysTest_createVersion_ok") + + val context = InstrumentationRegistry.getContext() + val bobSession = mTestHelper.createAccount(TestConstants.USER_BOB, SessionTestParams()) + + val megolmBackupAuthData = mCryptoTestHelper.createFakeMegolmBackupAuthData() + + val createKeysBackupVersionBody = CreateKeysBackupVersionBody() + createKeysBackupVersionBody.algorithm = MXCRYPTO_ALGORITHM_MEGOLM_BACKUP + createKeysBackupVersionBody.authData = JsonUtils.getBasicGson().toJsonTree(megolmBackupAuthData) + + var keysVersion: KeysVersion? = null + var lock = CountDownLatch(1) + bobSession.roomKeysRestClient + .createKeysBackupVersion(createKeysBackupVersionBody, object : TestApiCallback(lock) { + override fun onSuccess(info: KeysVersion) { + keysVersion = info + super.onSuccess(info) + } + }) + mTestHelper.await(lock) + + assertNotNull(keysVersion) + + val version = keysVersion!!.version + assertNotNull(version) + + var keysVersionResult: KeysVersionResult? = null + var keysVersionResultLast: KeysVersionResult? = null + lock = CountDownLatch(2) + // Retrieve the last version by specifying it and check we get the same content + bobSession.roomKeysRestClient + .getKeysBackupVersion(version!!, object : TestApiCallback(lock) { + override fun onSuccess(info: KeysVersionResult) { + keysVersionResult = info + super.onSuccess(info) + } + }) + // Retrieve the last version without specifying it and check we get the same content + bobSession.roomKeysRestClient + .getKeysBackupLastVersion(object : TestApiCallback(lock) { + override fun onSuccess(info: KeysVersionResult) { + keysVersionResultLast = info + super.onSuccess(info) + } + }) + mTestHelper.await(lock) + + // Check that all the fields are the same + compareVersion(version, createKeysBackupVersionBody, megolmBackupAuthData, keysVersionResult) + compareVersion(version, createKeysBackupVersionBody, megolmBackupAuthData, keysVersionResultLast) + + bobSession.clear(context) + } + + private fun compareVersion(version: String, + createKeysBackupVersionBody: CreateKeysBackupVersionBody, + megolmBackupAuthData: MegolmBackupAuthData, + keysVersionResult: KeysVersionResult?) { + assertNotNull(keysVersionResult) + assertEquals(version, keysVersionResult!!.version) + assertEquals(createKeysBackupVersionBody.algorithm, keysVersionResult.algorithm) + + val retrievedMegolmBackupAuthData = keysVersionResult.getAuthDataAsMegolmBackupAuthData() + + assertEquals(megolmBackupAuthData.publicKey, retrievedMegolmBackupAuthData.publicKey) + assertEquals(megolmBackupAuthData.signatures, retrievedMegolmBackupAuthData.signatures) + } + + /** + * - Create a backup version on the server + * - Make a backup + * - Get the backup back + * -> Check they match + */ + @Test + fun roomKeysTest_createVersionCreateBackupAndRetrieveIt_ok() { + Log.e(LOG_TAG, "RoomKeysTest_createVersion_ok") + + val context = InstrumentationRegistry.getContext() + val bobSession = mTestHelper.createAccount(TestConstants.USER_BOB, SessionTestParams()) + + val megolmBackupAuthData = mCryptoTestHelper.createFakeMegolmBackupAuthData() + + val createKeysBackupVersionBody = CreateKeysBackupVersionBody() + createKeysBackupVersionBody.algorithm = MXCRYPTO_ALGORITHM_MEGOLM_BACKUP + createKeysBackupVersionBody.authData = JsonUtils.getGson(false).toJsonTree(megolmBackupAuthData) + + var keysVersion: KeysVersion? = null + var lock = CountDownLatch(1) + bobSession.roomKeysRestClient + .createKeysBackupVersion(createKeysBackupVersionBody, object : TestApiCallback(lock) { + override fun onSuccess(info: KeysVersion) { + keysVersion = info + super.onSuccess(info) + } + }) + mTestHelper.await(lock) + + assertNotNull(keysVersion) + val version = keysVersion!!.version + assertNotNull(version) + + // Make a backup + val keys = HashMap() + keys["key"] = "value" + + val keyBackupData = KeyBackupData() + keyBackupData.firstMessageIndex = 1 + keyBackupData.forwardedCount = 2 + keyBackupData.isVerified = true + keyBackupData.sessionData = JsonUtils.getGson(false).toJsonTree(keys) + + val roomId = "!aRoomId:matrix.org" + val sessionId = "ASession" + + lock = CountDownLatch(1) + // Send the backup + bobSession.roomKeysRestClient + .sendKeyBackup(roomId, sessionId, version!!, keyBackupData, TestApiCallback(lock)) + mTestHelper.await(lock) + + // Get the backup back + var keysBackupDataResult: KeysBackupData? = null + lock = CountDownLatch(1) + // Retrieve the version and check we get the same content + bobSession.roomKeysRestClient + .getKeysBackup(version, object : TestApiCallback(lock) { + override fun onSuccess(info: KeysBackupData) { + keysBackupDataResult = info + super.onSuccess(info) + } + }) + mTestHelper.await(lock) + + // Check that all the fields are the same + assertNotNull(keysBackupDataResult) + val retrievedKeyBackupData = keysBackupDataResult!!.roomIdToRoomKeysBackupData[roomId]!!.sessionIdToKeyBackupData[sessionId]!! + + assertEquals(keyBackupData.firstMessageIndex, retrievedKeyBackupData.firstMessageIndex) + assertEquals(keyBackupData.forwardedCount, retrievedKeyBackupData.forwardedCount) + assertEquals(keyBackupData.isVerified, retrievedKeyBackupData.isVerified) + assertEquals(keyBackupData.sessionData, retrievedKeyBackupData.sessionData) + + bobSession.clear(context) + } + + /** + * - Create a backup version on the server + * - Make a backup + * - Delete it + * - Get the backup back + * -> Check it is now empty + */ + @Test + fun roomKeysTest_createVersionCreateBackupDeleteBackupAndRetrieveIt_ok() { + Log.e(LOG_TAG, "RoomKeysTest_createVersion_ok") + + val context = InstrumentationRegistry.getContext() + val bobSession = mTestHelper.createAccount(TestConstants.USER_BOB, SessionTestParams()) + + val megolmBackupAuthData = mCryptoTestHelper.createFakeMegolmBackupAuthData() + + val createKeysBackupVersionBody = CreateKeysBackupVersionBody() + createKeysBackupVersionBody.algorithm = MXCRYPTO_ALGORITHM_MEGOLM_BACKUP + createKeysBackupVersionBody.authData = JsonUtils.getGson(false).toJsonTree(megolmBackupAuthData) + + var keysVersion: KeysVersion? = null + var lock = CountDownLatch(1) + bobSession.roomKeysRestClient + .createKeysBackupVersion(createKeysBackupVersionBody, object : TestApiCallback(lock) { + override fun onSuccess(info: KeysVersion) { + keysVersion = info + super.onSuccess(info) + } + }) + mTestHelper.await(lock) + + assertNotNull(keysVersion) + val version = keysVersion!!.version + assertNotNull(version) + + // Make a backup + val keys = HashMap() + keys["key"] = "value" + + val keyBackupData = KeyBackupData() + keyBackupData.firstMessageIndex = 1 + keyBackupData.forwardedCount = 2 + keyBackupData.isVerified = true + keyBackupData.sessionData = JsonUtils.getGson(false).toJsonTree(keys) + + val roomId = "!aRoomId:matrix.org" + val sessionId = "ASession" + + // Send the backup + lock = CountDownLatch(1) + bobSession.roomKeysRestClient + .sendKeyBackup(roomId, sessionId, version!!, keyBackupData, TestApiCallback(lock)) + mTestHelper.await(lock) + + // Delete the backup + lock = CountDownLatch(1) + bobSession.roomKeysRestClient + .deleteKeyBackup(roomId, sessionId, version, TestApiCallback(lock)) + mTestHelper.await(lock) + + // Get the backup back + var keysBackupDataResult: KeysBackupData? = null + lock = CountDownLatch(1) + // Retrieve the version and check it is empty + bobSession.roomKeysRestClient + .getKeysBackup(version, object : TestApiCallback(lock) { + override fun onSuccess(info: KeysBackupData) { + keysBackupDataResult = info + super.onSuccess(info) + } + }) + mTestHelper.await(lock) + + // Check that all the fields are the same + assertNotNull(keysBackupDataResult) + assertTrue(keysBackupDataResult!!.roomIdToRoomKeysBackupData.isEmpty()) + + bobSession.clear(context) + } + + /* ========================================================================================== + * Private + * ========================================================================================== */ + + companion object { + private val LOG_TAG = RoomKeysRestClientTest::class.java.simpleName + } +} diff --git a/matrix-sdk/src/main/java/org/matrix/androidsdk/MXSession.java b/matrix-sdk/src/main/java/org/matrix/androidsdk/MXSession.java index 91d387dda..eacace252 100644 --- a/matrix-sdk/src/main/java/org/matrix/androidsdk/MXSession.java +++ b/matrix-sdk/src/main/java/org/matrix/androidsdk/MXSession.java @@ -65,6 +65,7 @@ import org.matrix.androidsdk.rest.client.ProfileRestClient; import org.matrix.androidsdk.rest.client.PushRulesRestClient; import org.matrix.androidsdk.rest.client.PushersRestClient; +import org.matrix.androidsdk.rest.client.RoomKeysRestClient; import org.matrix.androidsdk.rest.client.RoomsRestClient; import org.matrix.androidsdk.rest.client.ThirdPidRestClient; import org.matrix.androidsdk.rest.model.CreateRoomParams; @@ -144,6 +145,7 @@ public class MXSession { private final GroupsRestClient mGroupsRestClient; private final MediaScanRestClient mMediaScanRestClient; private final FilterRestClient mFilterRestClient; + private final RoomKeysRestClient mRoomKeysRestClient; private ApiFailureCallback mFailureCallback; @@ -231,6 +233,7 @@ private MXSession(HomeServerConnectionConfig hsConfig, mGroupsRestClient = new GroupsRestClient(hsConfig); mMediaScanRestClient = new MediaScanRestClient(hsConfig); mFilterRestClient = new FilterRestClient(hsConfig); + mRoomKeysRestClient = new RoomKeysRestClient(hsConfig); } /** @@ -482,6 +485,16 @@ public FilterRestClient getFilterRestClient() { return mFilterRestClient; } + /** + * Get the API client for requests to the Room Keys API. + * + * @return the Room Keys API client + */ + public RoomKeysRestClient getRoomKeysRestClient() { + checkIfAlive(); + return mRoomKeysRestClient; + } + /** * Refresh the presence info of a dedicated user. * diff --git a/matrix-sdk/src/main/java/org/matrix/androidsdk/RestClient.java b/matrix-sdk/src/main/java/org/matrix/androidsdk/RestClient.java index a0885c4df..e5214a529 100644 --- a/matrix-sdk/src/main/java/org/matrix/androidsdk/RestClient.java +++ b/matrix-sdk/src/main/java/org/matrix/androidsdk/RestClient.java @@ -105,6 +105,10 @@ public enum EndPointServer { // http client private OkHttpClient mOkHttpClient = new OkHttpClient(); + public RestClient(HomeServerConnectionConfig hsConfig, Class type, String uriPrefix) { + this(hsConfig, type, uriPrefix, JsonUtils.getKotlinGson(), EndPointServer.HOME_SERVER); + } + public RestClient(HomeServerConnectionConfig hsConfig, Class type, String uriPrefix, boolean withNullSerialization) { this(hsConfig, type, uriPrefix, withNullSerialization, EndPointServer.HOME_SERVER); } @@ -132,8 +136,13 @@ public RestClient(HomeServerConnectionConfig hsConfig, Class type, String uri * @param endPointServer tell which server is used to define the base url */ public RestClient(HomeServerConnectionConfig hsConfig, Class type, String uriPrefix, boolean withNullSerialization, EndPointServer endPointServer) { + this(hsConfig, type, uriPrefix, JsonUtils.getGson(withNullSerialization), endPointServer); + } + + // Private constructor with Gson instance as a parameter + private RestClient(HomeServerConnectionConfig hsConfig, Class type, String uriPrefix, Gson _gson, EndPointServer endPointServer) { // The JSON -> object mapper - gson = JsonUtils.getGson(withNullSerialization); + gson = _gson; mHsConfig = hsConfig; mCredentials = hsConfig.getCredentials(); diff --git a/matrix-sdk/src/main/java/org/matrix/androidsdk/crypto/MXCrypto.java b/matrix-sdk/src/main/java/org/matrix/androidsdk/crypto/MXCrypto.java index e9a703a16..faf8220d6 100755 --- a/matrix-sdk/src/main/java/org/matrix/androidsdk/crypto/MXCrypto.java +++ b/matrix-sdk/src/main/java/org/matrix/androidsdk/crypto/MXCrypto.java @@ -32,12 +32,14 @@ import org.matrix.androidsdk.MXSession; import org.matrix.androidsdk.crypto.algorithms.IMXDecrypting; import org.matrix.androidsdk.crypto.algorithms.IMXEncrypting; +import org.matrix.androidsdk.crypto.data.ImportRoomKeysResult; import org.matrix.androidsdk.crypto.data.MXDeviceInfo; import org.matrix.androidsdk.crypto.data.MXEncryptEventContentResult; import org.matrix.androidsdk.crypto.data.MXKey; import org.matrix.androidsdk.crypto.data.MXOlmInboundGroupSession2; import org.matrix.androidsdk.crypto.data.MXOlmSessionResult; import org.matrix.androidsdk.crypto.data.MXUsersDevicesMap; +import org.matrix.androidsdk.crypto.keysbackup.KeysBackup; import org.matrix.androidsdk.data.Room; import org.matrix.androidsdk.data.RoomState; import org.matrix.androidsdk.data.cryptostore.IMXCryptoStore; @@ -106,7 +108,8 @@ public class MXCrypto { // Our device keys private MXDeviceInfo mMyDevice; - // The libolm wrapper. + // The libolm wrapper. Null if the Crypto has been closed + @Nullable private MXOlmDevice mOlmDevice; private Map> mLastPublishedOneTimeKeys; @@ -185,6 +188,9 @@ public void onLiveEvent(Event event, RoomState roomState) { // Set of parameters used to configure/customize the end-to-end crypto. private MXCryptoConfig mCryptoConfig; + // The key backup manager. + private final KeysBackup mKeysBackup; + /** * Constructor * @@ -192,7 +198,9 @@ public void onLiveEvent(Event event, RoomState roomState) { * @param cryptoStore the crypto store * @param cryptoConfig the optional set of parameters used to configure the e2e encryption. */ - public MXCrypto(MXSession matrixSession, IMXCryptoStore cryptoStore, @Nullable MXCryptoConfig cryptoConfig) { + public MXCrypto(@NonNull MXSession matrixSession, + @NonNull IMXCryptoStore cryptoStore, + @Nullable MXCryptoConfig cryptoConfig) { mSession = matrixSession; mCryptoStore = cryptoStore; @@ -275,6 +283,8 @@ public MXCrypto(MXSession matrixSession, IMXCryptoStore cryptoStore, @Nullable M mOutgoingRoomKeyRequestManager = new MXOutgoingRoomKeyRequestManager(mSession, this); mReceivedRoomKeyRequests.addAll(mCryptoStore.getPendingIncomingRoomKeyRequests()); + + mKeysBackup = new KeysBackup(this, mSession); } /** @@ -298,7 +308,7 @@ public Handler getEncryptingThreadHandler() { /** * @return the decrypting thread handler */ - private Handler getDecryptingThreadHandler() { + public Handler getDecryptingThreadHandler() { // mDecryptingHandlerThread was not yet ready if (null == mDecryptingHandler) { mDecryptingHandler = new Handler(mDecryptingHandlerThread.getLooper()); @@ -465,6 +475,8 @@ public void run() { mOutgoingRoomKeyRequestManager.start(); + mKeysBackup.checkAndStartKeyBackup(); + synchronized (mInitializationCallbacks) { for (ApiCallback callback : mInitializationCallbacks) { final ApiCallback fCallback = callback; @@ -560,10 +572,12 @@ public void run() { mOlmDevice = null; } - mMyDevice = null; + // Do not reset My Device + // mMyDevice = null; mCryptoStore.close(); - mCryptoStore = null; + // Do not reset Crypto store + // mCryptoStore = null; if (null != mEncryptingHandlerThread) { mEncryptingHandlerThread.quit(); @@ -587,12 +601,20 @@ public void run() { } /** - * @return the olmdevice instance + * @return the olmdevice instance, or null if the Crypto is closed. */ + @Nullable public MXOlmDevice getOlmDevice() { return mOlmDevice; } + /** + * @return the KeysBackup instance + */ + public KeysBackup getKeysBackup() { + return mKeysBackup; + } + /** * A sync response has been received * @@ -665,12 +687,12 @@ private void updateOneTimeKeyCount(int currentCount) { /** * Find a device by curve25519 identity key * - * @param userId the owner of the device. * @param algorithm the encryption algorithm. * @param senderKey the curve25519 key to match. - * @return the device info. + * @return the device info, or null if not found / unsupported algorithm / crypto released */ - public MXDeviceInfo deviceWithIdentityKey(final String senderKey, final String userId, final String algorithm) { + @Nullable + public MXDeviceInfo deviceWithIdentityKey(final String senderKey, final String algorithm) { if (!hasBeenReleased()) { if (!TextUtils.equals(algorithm, CryptoConstantsKt.MXCRYPTO_ALGORITHM_MEGOLM) && !TextUtils.equals(algorithm, CryptoConstantsKt.MXCRYPTO_ALGORITHM_OLM)) { @@ -678,44 +700,11 @@ public MXDeviceInfo deviceWithIdentityKey(final String senderKey, final String u return null; } - if (!TextUtils.isEmpty(userId)) { - final List result = new ArrayList<>(); - final CountDownLatch lock = new CountDownLatch(1); - - getDecryptingThreadHandler().post(new Runnable() { - @Override - public void run() { - List devices = getUserDevices(userId); - - if (null != devices) { - for (MXDeviceInfo device : devices) { - Set keys = device.keys.keySet(); - - for (String keyId : keys) { - if (keyId.startsWith("curve25519:")) { - if (TextUtils.equals(senderKey, device.keys.get(keyId))) { - result.add(device); - } - } - } - } - } - - lock.countDown(); - } - }); - - try { - lock.await(); - } catch (Exception e) { - Log.e(LOG_TAG, "## deviceWithIdentityKey() : failed " + e.getMessage(), e); - } - - return (result.size() > 0) ? result.get(0) : null; - } + // Find in the crypto store + return getCryptoStore().deviceWithIdentityKey(senderKey); } - // Doesn't match a known device + // The store is released return null; } @@ -851,6 +840,13 @@ public void run() { if (device.mVerified != verificationStatus) { device.mVerified = verificationStatus; mCryptoStore.storeUserDevice(userId, device); + + if (userId.equals(mSession.getMyUserId())) { + // If one of the user's own devices is being marked as verified / unverified, + // check the key backup status, since whether or not we use this depends on + // whether it has a signature from a verified device + mKeysBackup.checkAndStartKeyBackup(); + } } if (null != callback) { @@ -1515,6 +1511,25 @@ public Map encryptMessage(Map payloadFields, Lis return res; } + /** + * Sign Object + * // TODO Also use this method internally + * + * @param str + * @return + */ + public Map signObject(String str) { + Map result = new HashMap<>(); + + Map content = new HashMap<>(); + + content.put("ed25519:" + mMyDevice.deviceId, mOlmDevice.signMessage(str)); + + result.put(mMyDevice.userId, content); + + return result; + } + /** * Handle the 'toDevice' event * @@ -1683,7 +1698,7 @@ public void run() { } }; - // if the device is is verified already, share the keys + // if the device is verified already, share the keys MXDeviceInfo device = mCryptoStore.getUserDevice(deviceId, userId); if (null != device) { @@ -2275,15 +2290,15 @@ public void run() { return; } - List> exportedSessions = new ArrayList<>(); + List exportedSessions = new ArrayList<>(); List inboundGroupSessions = mCryptoStore.getInboundGroupSessions(); for (MXOlmInboundGroupSession2 session : inboundGroupSessions) { - Map map = session.exportKeys(); + MegolmSessionData megolmSessionData = session.exportKeys(); - if (null != map) { - exportedSessions.add(map); + if (null != megolmSessionData) { + exportedSessions.add(megolmSessionData); } } @@ -2314,10 +2329,14 @@ public void run() { * @param password the password * @param callback the asynchronous callback. */ - public void importRoomKeys(final byte[] roomKeysAsArray, final String password, final ApiCallback callback) { + public void importRoomKeys(final byte[] roomKeysAsArray, + final String password, + final ApiCallback callback) { getDecryptingThreadHandler().post(new Runnable() { @Override public void run() { + Log.d(LOG_TAG, "## importRoomKeys starts"); + long t0 = System.currentTimeMillis(); String roomKeys; @@ -2333,14 +2352,14 @@ public void run() { return; } - List> importedSessions; + List importedSessions; long t1 = System.currentTimeMillis(); - Log.d(LOG_TAG, "## importRoomKeys starts"); + Log.d(LOG_TAG, "## importRoomKeys : decryptMegolmKeyFile done in " + (t1 - t0) + " ms"); try { - importedSessions = JsonUtils.getGson(false).fromJson(roomKeys, new TypeToken>>() { + importedSessions = JsonUtils.getGson(false).fromJson(roomKeys, new TypeToken>() { }.getType()); } catch (final Exception e) { Log.e(LOG_TAG, "## importRoomKeys failed " + e.getMessage(), e); @@ -2355,21 +2374,55 @@ public void run() { long t2 = System.currentTimeMillis(); - Log.d(LOG_TAG, "## importRoomKeys retrieve " + importedSessions.size() + "sessions in " + (t1 - t0) + " ms"); + Log.d(LOG_TAG, "## importRoomKeys : JSON parsing " + (t2 - t1) + " ms"); + + importMegolmSessionsData(importedSessions, true, callback); + } + }); + } + + /** + * Import a list of megolm session keys. + * + * @param megolmSessionsData megolm sessions. + * @param backUpKeys true to back up them to the homeserver. + * @param callback + */ + public void importMegolmSessionsData(final List megolmSessionsData, + final boolean backUpKeys, + final ApiCallback callback) { + getDecryptingThreadHandler().post(new Runnable() { + @Override + public void run() { + long t0 = System.currentTimeMillis(); + + final int totalNumbersOfKeys = megolmSessionsData.size(); + int totalNumbersOfImportedKeys = 0; + - for (int index = 0; index < importedSessions.size(); index++) { - Map map = importedSessions.get(index); + for (int index = 0; index < megolmSessionsData.size(); index++) { + MegolmSessionData megolmSessionData = megolmSessionsData.get(index); - MXOlmInboundGroupSession2 session = mOlmDevice.importInboundGroupSession(map); + MXOlmInboundGroupSession2 session = mOlmDevice.importInboundGroupSession(megolmSessionData); if ((null != session) && mRoomDecryptors.containsKey(session.mRoomId)) { - IMXDecrypting decrypting = mRoomDecryptors.get(session.mRoomId).get(map.get("algorithm")); + IMXDecrypting decrypting = mRoomDecryptors.get(session.mRoomId).get(megolmSessionData.algorithm); if (null != decrypting) { try { String sessionId = session.mSession.sessionIdentifier(); Log.d(LOG_TAG, "## importRoomKeys retrieve mSenderKey " + session.mSenderKey + " sessionId " + sessionId); + totalNumbersOfImportedKeys++; + + // Do not back up the key if it comes from a backup recovery + if (backUpKeys) { + mKeysBackup.maybeSendKeyBackup(); + } else { + mCryptoStore.markBackupDoneForInboundGroupSessionWithId(sessionId, session.mSenderKey); + } + + // Have another go at decrypting events sent with this session decrypting.onNewSession(session.mSenderKey, sessionId); } catch (Exception e) { Log.e(LOG_TAG, "## importRoomKeys() : onNewSession failed " + e.getMessage(), e); @@ -2378,17 +2431,16 @@ public void run() { } } - long t3 = System.currentTimeMillis(); + long t1 = System.currentTimeMillis(); + + Log.d(LOG_TAG, "## importMegolmSessionsData : sessions import " + (t1 - t0) + " ms (" + megolmSessionsData.size() + " sessions)"); - Log.d(LOG_TAG, "## importRoomKeys : done in " + (t3 - t0) + " ms (" + importedSessions.size() + " sessions)"); - Log.d(LOG_TAG, "## importRoomKeys : decryptMegolmKeyFile done in " + (t1 - t0) + " ms"); - Log.d(LOG_TAG, "## importRoomKeys : JSON parsing " + (t2 - t1) + " ms"); - Log.d(LOG_TAG, "## importRoomKeys : sessions import " + (t3 - t2) + " ms"); + final int finalTotalNumbersOfImportedKeys = totalNumbersOfImportedKeys; getUIHandler().post(new Runnable() { @Override public void run() { - callback.onSuccess(null); + callback.onSuccess(new ImportRoomKeysResult(totalNumbersOfKeys, finalTotalNumbersOfImportedKeys)); } }); } @@ -2774,4 +2826,17 @@ private void onRoomKeyRequestCancellation(IncomingRoomKeyRequestCancellation req } } } + + /* ========================================================================================== + * DEBUG INFO + * ========================================================================================== */ + + @Override + public String toString() { + if (mMyDevice != null) { + return mMyDevice.userId + " (" + mMyDevice.deviceId + ")"; + } + + return super.toString(); + } } \ No newline at end of file diff --git a/matrix-sdk/src/main/java/org/matrix/androidsdk/crypto/MXOlmDevice.java b/matrix-sdk/src/main/java/org/matrix/androidsdk/crypto/MXOlmDevice.java index 63b18c7b5..595e94dea 100755 --- a/matrix-sdk/src/main/java/org/matrix/androidsdk/crypto/MXOlmDevice.java +++ b/matrix-sdk/src/main/java/org/matrix/androidsdk/crypto/MXOlmDevice.java @@ -162,7 +162,7 @@ public String getDeviceEd25519Key() { * @param message the message to be signed. * @return the base64-encoded signature. */ - private String signMessage(String message) { + public String signMessage(String message) { try { return mOlmAccount.signMessage(message); } catch (Exception e) { @@ -583,13 +583,14 @@ public boolean addInboundGroupSession(String sessionId, /** * Import an inbound group session to the session store. * - * @param exportedSessionMap the exported session map + * @param megolmSessionData the megolm session data * @return the imported session if the operation succeeds. */ - public MXOlmInboundGroupSession2 importInboundGroupSession(Map exportedSessionMap) { - String sessionId = (String) exportedSessionMap.get("session_id"); - String senderKey = (String) exportedSessionMap.get("sender_key"); - String roomId = (String) exportedSessionMap.get("room_id"); + @Nullable + public MXOlmInboundGroupSession2 importInboundGroupSession(MegolmSessionData megolmSessionData) { + String sessionId = megolmSessionData.session_id; + String senderKey = megolmSessionData.sender_key; + String roomId = megolmSessionData.room_id; if (null != getInboundGroupSession(sessionId, senderKey, roomId)) { // If we already have this session, consider updating it @@ -602,7 +603,7 @@ public MXOlmInboundGroupSession2 importInboundGroupSession(Map e MXOlmInboundGroupSession2 session = null; try { - session = new MXOlmInboundGroupSession2(exportedSessionMap); + session = new MXOlmInboundGroupSession2(megolmSessionData); } catch (Exception e) { Log.e(LOG_TAG, "## importInboundGroupSession() : Update for megolm session " + senderKey + "/" + sessionId, e); } @@ -740,14 +741,14 @@ public void resetReplayAttackCheckInTimeline(String timeline) { /** * Verify an ed25519 signature on a JSON object. * - * @param key the ed25519 key. - * @param JSONDictinary the JSON object which was signed. - * @param signature the base64-encoded signature to be checked. + * @param key the ed25519 key. + * @param JSONDictionary the JSON object which was signed. + * @param signature the base64-encoded signature to be checked. * @throws Exception the exception */ - public void verifySignature(String key, Map JSONDictinary, String signature) throws Exception { + public void verifySignature(String key, Map JSONDictionary, String signature) throws Exception { // Check signature on the canonical version of the JSON - mOlmUtility.verifyEd25519Signature(signature, key, JsonUtils.getCanonicalizedJsonString(JSONDictinary)); + mOlmUtility.verifyEd25519Signature(signature, key, JsonUtils.getCanonicalizedJsonString(JSONDictionary)); } /** diff --git a/matrix-sdk/src/main/java/org/matrix/androidsdk/crypto/MegolmSessionData.java b/matrix-sdk/src/main/java/org/matrix/androidsdk/crypto/MegolmSessionData.java new file mode 100644 index 000000000..53e746c40 --- /dev/null +++ b/matrix-sdk/src/main/java/org/matrix/androidsdk/crypto/MegolmSessionData.java @@ -0,0 +1,64 @@ +/* + * Copyright 2018 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 org.matrix.androidsdk.crypto; + +import java.util.List; +import java.util.Map; + +/** + * The type of object we use for importing and exporting megolm session data. + */ +public class MegolmSessionData { + /** + * The algorithm used. + */ + public String algorithm; + + /** + * Unique id for the session. + */ + public String session_id; + + /** + * Sender's Curve25519 device key. + */ + public String sender_key; + + /** + * Room this session is used in. + */ + public String room_id; + + /** + * Base64'ed key data. + */ + public String session_key; + + /** + * Other keys the sender claims. + */ + public Map sender_claimed_keys; + + // This is a shortcut for sender_claimed_keys.get("ed25519") + // Keep it for compatibility reason. + public String sender_claimed_ed25519_key; + + /** + * Devices which forwarded this session to us (normally empty). + */ + public List forwardingCurve25519KeyChain; +} diff --git a/matrix-sdk/src/main/java/org/matrix/androidsdk/crypto/algorithms/megolm/MXMegolmDecryption.java b/matrix-sdk/src/main/java/org/matrix/androidsdk/crypto/algorithms/megolm/MXMegolmDecryption.java index 90e8d453c..7343ad94f 100644 --- a/matrix-sdk/src/main/java/org/matrix/androidsdk/crypto/algorithms/megolm/MXMegolmDecryption.java +++ b/matrix-sdk/src/main/java/org/matrix/androidsdk/crypto/algorithms/megolm/MXMegolmDecryption.java @@ -290,6 +290,8 @@ public void onRoomKeyEvent(Event roomKeyEvent) { mOlmDevice.addInboundGroupSession(sessionId, sessionKey, roomId, senderKey, forwarding_curve25519_key_chain, keysClaimed, exportFormat); + mSession.getCrypto().getKeysBackup().maybeSendKeyBackup(); + Map content = new HashMap<>(); content.put("algorithm", roomKeyContent.algorithm); content.put("room_id", roomKeyContent.room_id); diff --git a/matrix-sdk/src/main/java/org/matrix/androidsdk/crypto/algorithms/megolm/MXMegolmEncryption.java b/matrix-sdk/src/main/java/org/matrix/androidsdk/crypto/algorithms/megolm/MXMegolmEncryption.java index a5323db47..c42fc41ac 100644 --- a/matrix-sdk/src/main/java/org/matrix/androidsdk/crypto/algorithms/megolm/MXMegolmEncryption.java +++ b/matrix-sdk/src/main/java/org/matrix/androidsdk/crypto/algorithms/megolm/MXMegolmEncryption.java @@ -237,6 +237,8 @@ private MXOutboundSessionInfo prepareNewSessionInRoom() { olmDevice.addInboundGroupSession(sessionId, olmDevice.getSessionKey(sessionId), mRoomId, olmDevice.getDeviceCurve25519Key(), new ArrayList(), keysClaimedMap, false); + mCrypto.getKeysBackup().maybeSendKeyBackup(); + return new MXOutboundSessionInfo(sessionId); } diff --git a/matrix-sdk/src/main/java/org/matrix/androidsdk/crypto/data/ImportRoomKeysResult.kt b/matrix-sdk/src/main/java/org/matrix/androidsdk/crypto/data/ImportRoomKeysResult.kt new file mode 100644 index 000000000..e72b06dfa --- /dev/null +++ b/matrix-sdk/src/main/java/org/matrix/androidsdk/crypto/data/ImportRoomKeysResult.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2018 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 org.matrix.androidsdk.crypto.data + +data class ImportRoomKeysResult(val totalNumberOfKeys: Int, + val successfullyNumberOfImportedKeys: Int) diff --git a/matrix-sdk/src/main/java/org/matrix/androidsdk/crypto/data/MXOlmInboundGroupSession2.java b/matrix-sdk/src/main/java/org/matrix/androidsdk/crypto/data/MXOlmInboundGroupSession2.java index c1238be71..97c78853c 100755 --- a/matrix-sdk/src/main/java/org/matrix/androidsdk/crypto/data/MXOlmInboundGroupSession2.java +++ b/matrix-sdk/src/main/java/org/matrix/androidsdk/crypto/data/MXOlmInboundGroupSession2.java @@ -17,15 +17,16 @@ package org.matrix.androidsdk.crypto.data; +import android.support.annotation.Nullable; import android.text.TextUtils; import org.matrix.androidsdk.crypto.CryptoConstantsKt; +import org.matrix.androidsdk.crypto.MegolmSessionData; import org.matrix.androidsdk.util.Log; import org.matrix.olm.OlmInboundGroupSession; import java.io.Serializable; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; import java.util.Map; @@ -89,20 +90,20 @@ public MXOlmInboundGroupSession2(String sessionKey, boolean isImported) { /** * Create a new instance from the provided keys map. * - * @param map the map + * @param megolmSessionData the megolm session data * @throws Exception if the data are invalid */ - public MXOlmInboundGroupSession2(Map map) throws Exception { + public MXOlmInboundGroupSession2(MegolmSessionData megolmSessionData) throws Exception { try { - mSession = OlmInboundGroupSession.importSession((String) map.get("session_key")); + mSession = OlmInboundGroupSession.importSession(megolmSessionData.session_key); - if (!TextUtils.equals(mSession.sessionIdentifier(), (String) map.get("session_id"))) { + if (!TextUtils.equals(mSession.sessionIdentifier(), megolmSessionData.session_id)) { throw new Exception("Mismatched group session Id"); } - mSenderKey = (String) map.get("sender_key"); - mKeysClaimed = (Map) map.get("sender_claimed_keys"); - mRoomId = (String) map.get("room_id"); + mSenderKey = megolmSessionData.sender_key; + mKeysClaimed = megolmSessionData.sender_claimed_keys; + mRoomId = megolmSessionData.room_id; } catch (Exception e) { throw new Exception(e.getMessage()); } @@ -111,30 +112,31 @@ public MXOlmInboundGroupSession2(Map map) throws Exception { /** * Export the inbound group session keys * - * @return the inbound group session as map if the operation succeeds + * @return the inbound group session as MegolmSessionData if the operation succeeds */ - public Map exportKeys() { - Map map = new HashMap<>(); + @Nullable + public MegolmSessionData exportKeys() { + MegolmSessionData megolmSessionData = new MegolmSessionData(); try { if (null == mForwardingCurve25519KeyChain) { mForwardingCurve25519KeyChain = new ArrayList<>(); } - map.put("sender_claimed_ed25519_key", mKeysClaimed.get("ed25519")); - map.put("forwardingCurve25519KeyChain", mForwardingCurve25519KeyChain); - map.put("sender_key", mSenderKey); - map.put("sender_claimed_keys", mKeysClaimed); - map.put("room_id", mRoomId); - map.put("session_id", mSession.sessionIdentifier()); - map.put("session_key", mSession.export(mSession.getFirstKnownIndex())); - map.put("algorithm", CryptoConstantsKt.MXCRYPTO_ALGORITHM_MEGOLM); + megolmSessionData.sender_claimed_ed25519_key = mKeysClaimed.get("ed25519"); + megolmSessionData.forwardingCurve25519KeyChain = new ArrayList<>(mForwardingCurve25519KeyChain); + megolmSessionData.sender_key = mSenderKey; + megolmSessionData.sender_claimed_keys = mKeysClaimed; + megolmSessionData.room_id = mRoomId; + megolmSessionData.session_id = mSession.sessionIdentifier(); + megolmSessionData.session_key = mSession.export(mSession.getFirstKnownIndex()); + megolmSessionData.algorithm = CryptoConstantsKt.MXCRYPTO_ALGORITHM_MEGOLM; } catch (Exception e) { - map = null; + megolmSessionData = null; Log.e(LOG_TAG, "## export() : senderKey " + mSenderKey + " failed " + e.getMessage(), e); } - return map; + return megolmSessionData; } /** diff --git a/matrix-sdk/src/main/java/org/matrix/androidsdk/crypto/keysbackup/KeyBackupVersionTrust.kt b/matrix-sdk/src/main/java/org/matrix/androidsdk/crypto/keysbackup/KeyBackupVersionTrust.kt new file mode 100644 index 000000000..9a4346327 --- /dev/null +++ b/matrix-sdk/src/main/java/org/matrix/androidsdk/crypto/keysbackup/KeyBackupVersionTrust.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2018 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 org.matrix.androidsdk.crypto.keysbackup + +/** + * Data model for response to [KeysBackup.isKeyBackupTrusted()]. + */ +class KeyBackupVersionTrust { + /** + * Flag to indicate if the backup is trusted. + * true if there is a signature that is valid & from a trusted device. + */ + var usable = false + + /** + * Signatures found in the backup version. + */ + var signatures: MutableList = ArrayList() +} \ No newline at end of file diff --git a/matrix-sdk/src/androidTest/java/org/matrix/androidsdk/common/SessionAndRoomId.java b/matrix-sdk/src/main/java/org/matrix/androidsdk/crypto/keysbackup/KeyBackupVersionTrustSignature.kt similarity index 60% rename from matrix-sdk/src/androidTest/java/org/matrix/androidsdk/common/SessionAndRoomId.java rename to matrix-sdk/src/main/java/org/matrix/androidsdk/crypto/keysbackup/KeyBackupVersionTrustSignature.kt index 2a833e34f..6d907c802 100644 --- a/matrix-sdk/src/androidTest/java/org/matrix/androidsdk/common/SessionAndRoomId.java +++ b/matrix-sdk/src/main/java/org/matrix/androidsdk/crypto/keysbackup/KeyBackupVersionTrustSignature.kt @@ -14,20 +14,23 @@ * limitations under the License. */ -package org.matrix.androidsdk.common; +package org.matrix.androidsdk.crypto.keysbackup -import android.util.Pair; +import org.matrix.androidsdk.crypto.data.MXDeviceInfo -import org.matrix.androidsdk.MXSession; +/** + * A signature in a the `KeyBackupVersionTrust` object. + */ +class KeyBackupVersionTrustSignature { -public class SessionAndRoomId extends Pair { /** - * Constructor for a Pair. - * - * @param first the first object in the Pair - * @param second the second object in the pair + * The device that signed the backup version. */ - public SessionAndRoomId(MXSession first, String second) { - super(first, second); - } + var device: MXDeviceInfo? = null + + /** + *Flag to indicate the signature from this device is valid. + */ + var valid = false + } diff --git a/matrix-sdk/src/main/java/org/matrix/androidsdk/crypto/keysbackup/KeysBackup.kt b/matrix-sdk/src/main/java/org/matrix/androidsdk/crypto/keysbackup/KeysBackup.kt new file mode 100644 index 000000000..e12628097 --- /dev/null +++ b/matrix-sdk/src/main/java/org/matrix/androidsdk/crypto/keysbackup/KeysBackup.kt @@ -0,0 +1,843 @@ +/* + * Copyright 2018 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 org.matrix.androidsdk.crypto.keysbackup + +import android.support.annotation.VisibleForTesting +import org.matrix.androidsdk.MXSession +import org.matrix.androidsdk.crypto.MXCRYPTO_ALGORITHM_MEGOLM +import org.matrix.androidsdk.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP +import org.matrix.androidsdk.crypto.MXCrypto +import org.matrix.androidsdk.crypto.MegolmSessionData +import org.matrix.androidsdk.crypto.data.ImportRoomKeysResult +import org.matrix.androidsdk.crypto.data.MXDeviceInfo +import org.matrix.androidsdk.crypto.data.MXOlmInboundGroupSession2 +import org.matrix.androidsdk.crypto.util.computeRecoveryKey +import org.matrix.androidsdk.crypto.util.extractCurveKeyFromRecoveryKey +import org.matrix.androidsdk.rest.callback.ApiCallback +import org.matrix.androidsdk.rest.callback.SimpleApiCallback +import org.matrix.androidsdk.rest.callback.SuccessCallback +import org.matrix.androidsdk.rest.callback.SuccessErrorCallback +import org.matrix.androidsdk.rest.client.RoomKeysRestClient +import org.matrix.androidsdk.rest.model.MatrixError +import org.matrix.androidsdk.rest.model.keys.* +import org.matrix.androidsdk.util.JsonUtils +import org.matrix.androidsdk.util.Log +import org.matrix.olm.OlmException +import org.matrix.olm.OlmPkDecryption +import org.matrix.olm.OlmPkEncryption +import org.matrix.olm.OlmPkMessage +import java.util.* + +/** + * A KeysBackup class instance manage incremental backup of e2e keys (megolm keys) + * to the user's homeserver. + */ +class KeysBackup(private val mCrypto: MXCrypto, session: MXSession) { + + private val mRoomKeysRestClient = session.roomKeysRestClient + + private val mKeysBackupStateManager = KeysBackupStateManager() + + // The backup version being used. + private var mKeysBackupVersion: KeysVersionResult? = null + + // The backup key being used. + private var mBackupKey: OlmPkEncryption? = null + + private val mRandom = Random() + + private var backupAllGroupSessionsCallback: ApiCallback? = null + + private var mKeysBackupStateListener: KeysBackupStateManager.KeysBackupStateListener? = null + + val isEnabled: Boolean + get() = mKeysBackupStateManager.isEnabled + + val state: KeysBackupStateManager.KeysBackupState + get() = mKeysBackupStateManager.state + + val currentBackupVersion: String? + get() = mKeysBackupVersion?.version + + fun addListener(listener: KeysBackupStateManager.KeysBackupStateListener) { + mKeysBackupStateManager.addListener(listener) + } + + fun removeListener(listener: KeysBackupStateManager.KeysBackupStateListener) { + mKeysBackupStateManager.removeListener(listener) + } + + /** + * Set up the data required to create a new backup version. + * The backup version will not be created and enabled until [createKeyBackupVersion] + * is called. + * The returned [MegolmBackupCreationInfo] object has a `recoveryKey` member with + * the user-facing recovery key string. + * + * @param callback Asynchronous callback + */ + fun prepareKeysBackupVersion(callback: SuccessErrorCallback) { + mCrypto.decryptingThreadHandler.post { + try { + val olmPkDecryption = OlmPkDecryption() + val publicKey = olmPkDecryption.generateKey() + val signatures = mapOf("public_key" to publicKey) + val megolmBackupAuthData = MegolmBackupAuthData( + publicKey = publicKey, + signatures = mCrypto.signObject(JsonUtils.getCanonicalizedJsonString(signatures)) + ) + + val megolmBackupCreationInfo = MegolmBackupCreationInfo() + megolmBackupCreationInfo.algorithm = MXCRYPTO_ALGORITHM_MEGOLM_BACKUP + megolmBackupCreationInfo.authData = megolmBackupAuthData + megolmBackupCreationInfo.recoveryKey = computeRecoveryKey(olmPkDecryption.privateKey()) + + mCrypto.uiHandler.post { callback.onSuccess(megolmBackupCreationInfo) } + } catch (e: OlmException) { + Log.e(LOG_TAG, "OlmException: ", e) + + mCrypto.uiHandler.post { callback.onUnexpectedError(e) } + } + } + } + + /** + * Create a new key backup version and enable it, using the information return from [prepareKeysBackupVersion]. + * + * @param keyBackupCreationInfo the info object from [prepareKeysBackupVersion]. + * @param callback Asynchronous callback + */ + fun createKeyBackupVersion(keyBackupCreationInfo: MegolmBackupCreationInfo, + callback: ApiCallback) { + val createKeysBackupVersionBody = CreateKeysBackupVersionBody() + createKeysBackupVersionBody.algorithm = keyBackupCreationInfo.algorithm + createKeysBackupVersionBody.authData = JsonUtils.getBasicGson().toJsonTree(keyBackupCreationInfo.authData) + + mRoomKeysRestClient.createKeysBackupVersion(createKeysBackupVersionBody, object : SimpleApiCallback(callback) { + override fun onSuccess(info: KeysVersion) { + // Reset backup markers. + mCrypto.cryptoStore.resetBackupMarkers() + + val keyBackupVersion = KeysVersionResult() + keyBackupVersion.algorithm = createKeysBackupVersionBody.algorithm + keyBackupVersion.authData = createKeysBackupVersionBody.authData + keyBackupVersion.version = info.version + + enableKeyBackup(keyBackupVersion) + + callback.onSuccess(info) + } + }) + } + + /** + * Delete a key backup version. + * If we are backing up to this version. Backup will be stopped. + * + * @param version the backup version to delete. + * @param callback Asynchronous callback + */ + fun deleteKeyBackupVersion(version: String, callback: ApiCallback) { + mCrypto.decryptingThreadHandler.post { + // If we're currently backing up to this backup... stop. + // (We start using it automatically in createKeyBackupVersion so this is symmetrical). + if (mKeysBackupVersion != null && version == mKeysBackupVersion!!.version) { + disableKeyBackup() + mKeysBackupStateManager.state = KeysBackupStateManager.KeysBackupState.Unknown + } + + mRoomKeysRestClient.deleteKeysBackup(version, callback) + } + } + + /** + * Start to back up keys immediately. + * + * @param progress the callback to follow the progress + * @param callback the main callback + */ + fun backupAllGroupSessions(progress: BackupProgressListener?, + callback: ApiCallback?) { + // Get a status right now + getBackupProgress(object : BackupProgressListener { + override fun onProgress(backedUp: Int, total: Int) { + // Reset previous listeners if any + resetBackupAllGroupSessionsListeners() + Log.d(LOG_TAG, "backupAllGroupSessions: backupProgress: $backedUp/$total") + progress?.onProgress(backedUp, total) + + if (backedUp == total) { + Log.d(LOG_TAG, "backupAllGroupSessions: complete") + callback?.onSuccess(null) + return + } + + backupAllGroupSessionsCallback = callback + + // Listen to `state` change to determine when to call onBackupProgress and onComplete + mKeysBackupStateListener = object : KeysBackupStateManager.KeysBackupStateListener { + override fun onStateChange(newState: KeysBackupStateManager.KeysBackupState) { + getBackupProgress(object : BackupProgressListener { + override fun onProgress(backedUp: Int, total: Int) { + progress?.onProgress(backedUp, total) + + // If backup is finished, notify the main listener + if (state === KeysBackupStateManager.KeysBackupState.ReadyToBackUp) { + backupAllGroupSessionsCallback?.onSuccess(null) + resetBackupAllGroupSessionsListeners() + } + } + }) + } + } + + mKeysBackupStateManager.addListener(mKeysBackupStateListener!!) + + sendKeyBackup() + } + }) + } + + /** + * Check trust on a key backup version. + * + * @param keyBackupVersion the backup version to check. + * @param callback block called when the operations completes. + */ + fun isKeyBackupTrusted(keyBackupVersion: KeysVersionResult, + callback: SuccessCallback) { + mCrypto.decryptingThreadHandler.post { + val myUserId = mCrypto.myDevice.userId + + val keyBackupVersionTrust = KeyBackupVersionTrust() + val authData = keyBackupVersion.getAuthDataAsMegolmBackupAuthData() + + if (keyBackupVersion.algorithm == null + || authData == null + || authData.publicKey.isEmpty() + || authData.signatures?.isEmpty() == true) { + Log.d(LOG_TAG, "isKeyBackupTrusted: Key backup is absent or missing required data") + mCrypto.uiHandler.post { callback.onSuccess(keyBackupVersionTrust) } + return@post + } + + val mySigs: Map = authData.signatures!![myUserId] as Map + if (mySigs.isEmpty()) { + Log.d(LOG_TAG, "isKeyBackupTrusted: Ignoring key backup because it lacks any signatures from this user") + mCrypto.uiHandler.post { callback.onSuccess(keyBackupVersionTrust) } + return@post + } + + for (keyId in mySigs.keys) { + // XXX: is this how we're supposed to get the device id? + var deviceId: String? = null + val components = keyId.split(":") + if (components.size == 2) { + deviceId = components[1] + } + + var device: MXDeviceInfo? = null + if (deviceId != null) { + device = mCrypto.cryptoStore.getUserDevice(deviceId, myUserId) + } + if (device == null) { + Log.d(LOG_TAG, "isKeyBackupTrusted: Ignoring signature from unknown key $deviceId") + continue + } + + var isSignatureValid = false + mCrypto.olmDevice?.let { + try { + it.verifySignature(device.fingerprint(), authData.signalableJSONDictionary(), mySigs[keyId] as String) + isSignatureValid = true + } catch (e: OlmException) { + Log.d(LOG_TAG, "isKeyBackupTrusted: Bad signature from device " + device.deviceId + " " + e.localizedMessage) + } + } + + if (isSignatureValid && device.isVerified) { + keyBackupVersionTrust.usable = true + } + + val signature = KeyBackupVersionTrustSignature() + signature.device = device + signature.valid = isSignatureValid + keyBackupVersionTrust.signatures.add(signature) + } + + mCrypto.uiHandler.post { callback.onSuccess(keyBackupVersionTrust) } + } + } + + private fun resetBackupAllGroupSessionsListeners() { + backupAllGroupSessionsCallback = null + + mKeysBackupStateListener?.let { + mKeysBackupStateManager.removeListener(it) + } + + mKeysBackupStateListener = null + } + + interface BackupProgressListener { + fun onProgress(backedUp: Int, total: Int) + } + + private fun getBackupProgress(listener: BackupProgressListener) { + mCrypto.decryptingThreadHandler.post { + val backedUpKeys = mCrypto.cryptoStore.inboundGroupSessionsCount(true) + val total = mCrypto.cryptoStore.inboundGroupSessionsCount(false) + + mCrypto.uiHandler.post { listener.onProgress(backedUpKeys, total) } + } + } + + /** + * Restore a backup from a given backup version stored on the homeserver. + * + * @param version the backup version to restore from. + * @param recoveryKey the recovery key to decrypt the retrieved backup. + * @param roomId the id of the room to get backup data from. + * @param sessionId the id of the session to restore. + * @param callback Callback. It provides the number of found keys and the number of successfully imported keys. + */ + fun restoreKeyBackup(version: String, + recoveryKey: String, + roomId: String?, + sessionId: String?, + callback: ApiCallback?) { + Log.d(LOG_TAG, "restoreKeyBackup: From backup version: $version") + + mCrypto.decryptingThreadHandler.post(Runnable { + // Get a PK decryption instance + val decryption = pkDecryptionFromRecoveryKey(recoveryKey) + if (decryption == null) { + Log.e(LOG_TAG, "restoreKeyBackup: Invalid recovery key. Error") + if (callback != null) { + mCrypto.uiHandler.post { callback.onUnexpectedError(Exception("Invalid recovery key")) } + } + return@Runnable + } + + // Get backup from the homeserver + keyBackupForSession(sessionId, roomId, version, object : ApiCallback { + override fun onUnexpectedError(e: Exception) { + if (callback != null) { + mCrypto.uiHandler.post { callback.onUnexpectedError(e) } + } + } + + override fun onNetworkError(e: Exception) { + if (callback != null) { + mCrypto.uiHandler.post { callback.onNetworkError(e) } + } + } + + override fun onMatrixError(e: MatrixError) { + if (callback != null) { + mCrypto.uiHandler.post { callback.onMatrixError(e) } + } + } + + override fun onSuccess(keysBackupData: KeysBackupData) { + val sessionsData = ArrayList() + // Restore that data + for (roomIdLoop in keysBackupData.roomIdToRoomKeysBackupData.keys) { + for (sessionIdLoop in keysBackupData.roomIdToRoomKeysBackupData[roomIdLoop]!!.sessionIdToKeyBackupData.keys) { + val keyBackupData = keysBackupData.roomIdToRoomKeysBackupData[roomIdLoop]!!.sessionIdToKeyBackupData[sessionIdLoop]!! + + val sessionData = decryptKeyBackupData(keyBackupData, sessionIdLoop, roomIdLoop, decryption) + + sessionData?.let { + sessionsData.add(it) + } + } + } + Log.d(LOG_TAG, "restoreKeyBackup: Got " + sessionsData.size + " keys from the backup store on the homeserver") + // Do not trigger a backup for them if they come from the backup version we are using + val backUp = version != mKeysBackupVersion?.version + if (backUp) { + Log.d(LOG_TAG, "restoreKeyBackup: Those keys will be backed up to backup version: " + mKeysBackupVersion?.version) + } + + // Import them into the crypto store + mCrypto.importMegolmSessionsData(sessionsData, backUp, callback) + } + }) + }) + } + + /** + * Same method as [RoomKeysRestClient.getRoomKeyBackup] except that it accepts nullable + * parameters and always returns a KeysBackupData object through the Callback + */ + private fun keyBackupForSession(sessionId: String?, + roomId: String?, + version: String, + callback: ApiCallback) { + if (roomId != null && sessionId != null) { + // Get key for the room and for the session + mRoomKeysRestClient.getRoomKeyBackup(roomId, sessionId, version, object : SimpleApiCallback(callback) { + override fun onSuccess(info: KeyBackupData) { + // Convert to KeysBackupData + val keysBackupData = KeysBackupData() + keysBackupData.roomIdToRoomKeysBackupData = HashMap() + val roomKeysBackupData = RoomKeysBackupData() + roomKeysBackupData.sessionIdToKeyBackupData = HashMap() + roomKeysBackupData.sessionIdToKeyBackupData[sessionId] = info + keysBackupData.roomIdToRoomKeysBackupData[roomId] = roomKeysBackupData + + callback.onSuccess(keysBackupData) + } + }) + } else if (roomId != null) { + // Get all keys for the room + mRoomKeysRestClient.getRoomKeysBackup(roomId, version, object : SimpleApiCallback(callback) { + override fun onSuccess(info: RoomKeysBackupData) { + // Convert to KeysBackupData + val keysBackupData = KeysBackupData() + keysBackupData.roomIdToRoomKeysBackupData = HashMap() + keysBackupData.roomIdToRoomKeysBackupData[roomId] = info + + callback.onSuccess(keysBackupData) + } + }) + } else { + // Get all keys + mRoomKeysRestClient.getKeysBackup(version, callback) + } + } + + @VisibleForTesting + fun pkDecryptionFromRecoveryKey(recoveryKey: String): OlmPkDecryption? { + // Extract the primary key + val privateKey = extractCurveKeyFromRecoveryKey(recoveryKey) + + // Built the PK decryption with it + var decryption: OlmPkDecryption? = null + if (privateKey != null) { + try { + decryption = OlmPkDecryption() + decryption.setPrivateKey(privateKey) + } catch (e: OlmException) { + Log.e(LOG_TAG, "OlmException", e) + } + + } + + return decryption + } + + /** + * Do a backup if there are new keys, with a delay + */ + fun maybeSendKeyBackup() { + when (state) { + KeysBackupStateManager.KeysBackupState.Unknown -> { + // If not already done, check for a valid backup version on the homeserver. + // If one, maybeSendKeyBackup will be called again. + checkAndStartKeyBackup() + } + KeysBackupStateManager.KeysBackupState.ReadyToBackUp -> { + mKeysBackupStateManager.state = KeysBackupStateManager.KeysBackupState.WillBackUp + + // Wait between 0 and 10 seconds, to avoid backup requests from + // different clients hitting the server all at the same time when a + // new key is sent + val delayInMs = mRandom.nextInt(KEY_BACKUP_WAITING_TIME_TO_SEND_KEY_BACKUP_MILLIS).toLong() + + mCrypto.decryptingThreadHandler.postDelayed({ sendKeyBackup() }, delayInMs) + } + else -> { + Log.d(LOG_TAG, "maybeSendKeyBackup: Skip it because state: $state") + } + } + } + + /** + * Retrieve the current version of the backup from the home server + * + * It can be different than mKeysBackupVersion. + * @param callback + */ + fun getCurrentVersion(callback: ApiCallback) { + mRoomKeysRestClient.getKeysBackupLastVersion(object : SimpleApiCallback(callback) { + override fun onSuccess(info: KeysVersionResult) { + callback.onSuccess(info) + } + + override fun onMatrixError(e: MatrixError) { + // Workaround because the homeserver currently returns M_NOT_FOUND when there is no key backup + if (e.errcode == MatrixError.NOT_FOUND) { + callback.onSuccess(null) + } else { + // Transmit the error + callback.onMatrixError(e) + } + } + }) + } + + /** + * Check the server for an active key backup. + * + * If one is present and has a valid signature from one of the user's verified + * devices, start backing up to it. + */ + fun checkAndStartKeyBackup() { + if (isEnabled) { + Log.w(LOG_TAG, "checkAndStartKeyBackup: invalid state: $state") + + return + } + + mKeysBackupStateManager.state = KeysBackupStateManager.KeysBackupState.CheckingBackUpOnHomeserver + + getCurrentVersion(object : ApiCallback { + override fun onSuccess(keyBackupVersion: KeysVersionResult?) { + if (keyBackupVersion == null) { + Log.d(LOG_TAG, "checkAndStartKeyBackup: Found no key backup version on the homeserver") + disableKeyBackup() + } else { + isKeyBackupTrusted(keyBackupVersion, SuccessCallback { trustInfo -> + mKeysBackupStateManager.state = KeysBackupStateManager.KeysBackupState.Disabled + + if (trustInfo.usable) { + Log.d(LOG_TAG, "checkAndStartKeyBackup: Found usable key backup. version: " + keyBackupVersion.version) + when { + mKeysBackupVersion == null -> { + // Check the version we used at the previous app run + val versionInStore = mCrypto.cryptoStore.keyBackupVersion + if (versionInStore != null && versionInStore != keyBackupVersion.version) { + Log.d(LOG_TAG, " -> clean the previously used version $versionInStore") + disableKeyBackup() + } + + Log.d(LOG_TAG, " -> enabling key backups") + enableKeyBackup(keyBackupVersion) + } + mKeysBackupVersion!!.version.equals(keyBackupVersion.version) -> { + Log.d(LOG_TAG, " -> same backup version (" + keyBackupVersion.version + "). Keep using it") + } + else -> { + Log.d(LOG_TAG, " -> disable the current version (" + mKeysBackupVersion!!.version + + ") and enabling the new one: " + keyBackupVersion.version) + disableKeyBackup() + enableKeyBackup(keyBackupVersion) + } + } + } else { + Log.d(LOG_TAG, "checkAndStartKeyBackup: No usable key backup. version: " + keyBackupVersion.version) + if (mKeysBackupVersion == null) { + Log.d(LOG_TAG, " -> not enabling key backup") + } else { + Log.d(LOG_TAG, " -> disabling key backup") + disableKeyBackup() + } + } + }) + } + } + + override fun onUnexpectedError(e: Exception?) { + Log.e(LOG_TAG, "checkAndStartKeyBackup: Failed to get current version", e) + mKeysBackupStateManager.state = KeysBackupStateManager.KeysBackupState.Unknown + } + + override fun onNetworkError(e: Exception?) { + Log.e(LOG_TAG, "checkAndStartKeyBackup: Failed to get current version", e) + mKeysBackupStateManager.state = KeysBackupStateManager.KeysBackupState.Unknown + } + + override fun onMatrixError(e: MatrixError?) { + Log.e(LOG_TAG, "checkAndStartKeyBackup: Failed to get current version " + e?.localizedMessage) + mKeysBackupStateManager.state = KeysBackupStateManager.KeysBackupState.Unknown + } + }) + } + + /* ========================================================================================== + * Private + * ========================================================================================== */ + + /** + * Enable backing up of keys. + * + * @param keysVersionResult backup information object as returned by [getCurrentVersion]. + */ + private fun enableKeyBackup(keysVersionResult: KeysVersionResult) { + if (keysVersionResult.authData != null) { + val retrievedMegolmBackupAuthData = keysVersionResult.getAuthDataAsMegolmBackupAuthData() + + if (retrievedMegolmBackupAuthData != null) { + mKeysBackupVersion = keysVersionResult + mCrypto.cryptoStore.keyBackupVersion = keysVersionResult.version + + try { + mBackupKey = OlmPkEncryption().apply { + setRecipientKey(retrievedMegolmBackupAuthData.publicKey) + } + } catch (e: OlmException) { + Log.e(LOG_TAG, "OlmException", e) + return + } + + mKeysBackupStateManager.state = KeysBackupStateManager.KeysBackupState.ReadyToBackUp + + maybeSendKeyBackup() + } else { + Log.e(LOG_TAG, "Invalid authentication data") + } + } else { + Log.e(LOG_TAG, "Invalid authentication data") + } + } + + /** + * Disable backing up of keys. + */ + private fun disableKeyBackup() { + resetBackupAllGroupSessionsListeners() + + mKeysBackupVersion = null + mCrypto.cryptoStore.keyBackupVersion = null + mBackupKey = null + mKeysBackupStateManager.state = KeysBackupStateManager.KeysBackupState.Disabled + + // Reset backup markers + mCrypto.cryptoStore.resetBackupMarkers() + } + + /** + * Send a chunk of keys to backup + */ + private fun sendKeyBackup() { + Log.d(LOG_TAG, "sendKeyBackup") + + // Sanity check + if (!isEnabled || mBackupKey == null || mKeysBackupVersion == null) { + Log.d(LOG_TAG, "sendKeyBackup: Invalid configuration") + backupAllGroupSessionsCallback?.onUnexpectedError(IllegalStateException("Invalid configuration")) + resetBackupAllGroupSessionsListeners() + + return + } + + if (state === KeysBackupStateManager.KeysBackupState.BackingUp) { + // Do nothing if we are already backing up + Log.d(LOG_TAG, "sendKeyBackup: Invalid state: $state") + return + } + + // Get a chunk of keys to backup + val sessions = mCrypto.cryptoStore.inboundGroupSessionsToBackup(KEY_BACKUP_SEND_KEYS_MAX_COUNT) + + Log.d(LOG_TAG, "sendKeyBackup: 1 - " + sessions.size + " sessions to back up") + + if (sessions.isEmpty()) { + // Backup is up to date + mKeysBackupStateManager.state = KeysBackupStateManager.KeysBackupState.ReadyToBackUp + + backupAllGroupSessionsCallback?.onSuccess(null) + resetBackupAllGroupSessionsListeners() + return + } + + mKeysBackupStateManager.state = KeysBackupStateManager.KeysBackupState.BackingUp + + Log.d(LOG_TAG, "sendKeyBackup: 2 - Encrypting keys") + + // Gather data to send to the homeserver + // roomId -> sessionId -> MXKeyBackupData + val keysBackupData = KeysBackupData() + keysBackupData.roomIdToRoomKeysBackupData = HashMap() + + for (session in sessions) { + val keyBackupData = encryptGroupSession(session) + if (keysBackupData.roomIdToRoomKeysBackupData[session.mRoomId] == null) { + val roomKeysBackupData = RoomKeysBackupData() + roomKeysBackupData.sessionIdToKeyBackupData = HashMap() + keysBackupData.roomIdToRoomKeysBackupData[session.mRoomId] = roomKeysBackupData + } + + try { + keysBackupData.roomIdToRoomKeysBackupData[session.mRoomId]!!.sessionIdToKeyBackupData[session.mSession.sessionIdentifier()] = keyBackupData + } catch (e: OlmException) { + Log.e(LOG_TAG, "OlmException", e) + } + } + + Log.d(LOG_TAG, "sendKeyBackup: 4 - Sending request") + + // Make the request + mRoomKeysRestClient.sendKeysBackup(mKeysBackupVersion!!.version!!, keysBackupData, object : ApiCallback { + override fun onNetworkError(e: Exception) { + backupAllGroupSessionsCallback?.onNetworkError(e) + resetBackupAllGroupSessionsListeners() + + onError() + } + + private fun onError() { + Log.e(LOG_TAG, "sendKeyBackup: sendKeysBackup failed.") + + // Retry a bit later + mKeysBackupStateManager.state = KeysBackupStateManager.KeysBackupState.ReadyToBackUp + maybeSendKeyBackup() + } + + override fun onMatrixError(e: MatrixError) { + Log.e(LOG_TAG, "sendKeyBackup: sendKeysBackup failed. Error: " + e.localizedMessage) + + if (e.errcode == MatrixError.WRONG_ROOM_KEYS_VERSION) { + mKeysBackupStateManager.state = KeysBackupStateManager.KeysBackupState.WrongBackUpVersion + backupAllGroupSessionsCallback?.onMatrixError(e) + resetBackupAllGroupSessionsListeners() + disableKeyBackup() + + // disableKeyBackup() set state to Disable, so ensure it is WrongBackUpVersion + mKeysBackupStateManager.state = KeysBackupStateManager.KeysBackupState.WrongBackUpVersion + } else { + // Come back to the ready state so that we will retry on the next received key + mKeysBackupStateManager.state = KeysBackupStateManager.KeysBackupState.ReadyToBackUp + } + } + + override fun onUnexpectedError(e: Exception) { + backupAllGroupSessionsCallback?.onUnexpectedError(e) + resetBackupAllGroupSessionsListeners() + + onError() + } + + override fun onSuccess(info: Void?) { + Log.d(LOG_TAG, "sendKeyBackup: 5a - Request complete") + + // Mark keys as backed up + for (session in sessions) { + try { + mCrypto.cryptoStore.markBackupDoneForInboundGroupSessionWithId(session.mSession.sessionIdentifier(), session.mSenderKey) + } catch (e: OlmException) { + Log.e(LOG_TAG, "OlmException", e) + } + } + + if (sessions.size < KEY_BACKUP_SEND_KEYS_MAX_COUNT) { + Log.d(LOG_TAG, "sendKeyBackup: All keys have been backed up") + // Note: Changing state will trigger the call to backupAllGroupSessionsCallback.onSuccess() + mKeysBackupStateManager.state = KeysBackupStateManager.KeysBackupState.ReadyToBackUp + } else { + Log.d(LOG_TAG, "sendKeyBackup: Continue to back up keys") + mKeysBackupStateManager.state = KeysBackupStateManager.KeysBackupState.WillBackUp + + sendKeyBackup() + } + } + }) + } + + @VisibleForTesting + fun encryptGroupSession(session: MXOlmInboundGroupSession2): KeyBackupData { + // Gather information for each key + val device = mCrypto.deviceWithIdentityKey(session.mSenderKey, MXCRYPTO_ALGORITHM_MEGOLM) + + // Build the m.megolm_backup.v1.curve25519-aes-sha2 data as defined at + // https://github.com/uhoreg/matrix-doc/blob/e2e_backup/proposals/1219-storing-megolm-keys-serverside.md#mmegolm_backupv1curve25519-aes-sha2-key-format + val sessionData = session.exportKeys() + val sessionBackupData = mapOf( + "algorithm" to sessionData!!.algorithm, + "sender_key" to sessionData.sender_key, + "sender_claimed_keys" to sessionData.sender_claimed_keys, + "forwarding_curve25519_key_chain" to (sessionData.forwardingCurve25519KeyChain ?: ArrayList()), + "session_key" to sessionData.session_key) + + var encryptedSessionBackupData: OlmPkMessage? = null + try { + encryptedSessionBackupData = mBackupKey?.encrypt(JsonUtils.getGson(false).toJson(sessionBackupData)) + } catch (e: OlmException) { + Log.e(LOG_TAG, "OlmException", e) + } + + // Build backup data for that key + val keyBackupData = KeyBackupData() + try { + keyBackupData.firstMessageIndex = session.mSession.firstKnownIndex + } catch (e: OlmException) { + Log.e(LOG_TAG, "OlmException", e) + } + + keyBackupData.forwardedCount = session.mForwardingCurve25519KeyChain.size + keyBackupData.isVerified = device?.isVerified == true + + val data = mapOf( + "ciphertext" to encryptedSessionBackupData!!.mCipherText, + "mac" to encryptedSessionBackupData.mMac, + "ephemeral" to encryptedSessionBackupData.mEphemeralKey) + + keyBackupData.sessionData = JsonUtils.getGson(false).toJsonTree(data) + + return keyBackupData + } + + @VisibleForTesting + fun decryptKeyBackupData(keyBackupData: KeyBackupData, sessionId: String, roomId: String, decryption: OlmPkDecryption): MegolmSessionData? { + var sessionBackupData: MegolmSessionData? = null + + val jsonObject = keyBackupData.sessionData?.asJsonObject + + val ciphertext = jsonObject?.get("ciphertext")?.asString + val mac = jsonObject?.get("mac")?.asString + val ephemeralKey = jsonObject?.get("ephemeral")?.asString + + if (ciphertext != null && mac != null && ephemeralKey != null) { + val encrypted = OlmPkMessage() + encrypted.mCipherText = ciphertext + encrypted.mMac = mac + encrypted.mEphemeralKey = ephemeralKey + + try { + val decrypted = decryption.decrypt(encrypted) + sessionBackupData = JsonUtils.toClass(decrypted, MegolmSessionData::class.java) + } catch (e: OlmException) { + Log.e(LOG_TAG, "OlmException", e) + } + + if (sessionBackupData != null) { + sessionBackupData.session_id = sessionId + sessionBackupData.room_id = roomId + } + } + + return sessionBackupData + } + + companion object { + private val LOG_TAG = KeysBackup::class.java.simpleName + + // Maximum delay in ms in {@link maybeSendKeyBackup} + private const val KEY_BACKUP_WAITING_TIME_TO_SEND_KEY_BACKUP_MILLIS = 10000 + + // Maximum number of keys to send at a time to the homeserver. + private const val KEY_BACKUP_SEND_KEYS_MAX_COUNT = 100 + } + + /* ========================================================================================== + * DEBUG INFO + * ========================================================================================== */ + + override fun toString() = "KeysBackup for $mCrypto" +} diff --git a/matrix-sdk/src/main/java/org/matrix/androidsdk/crypto/keysbackup/KeysBackupStateManager.kt b/matrix-sdk/src/main/java/org/matrix/androidsdk/crypto/keysbackup/KeysBackupStateManager.kt new file mode 100644 index 000000000..b9778e282 --- /dev/null +++ b/matrix-sdk/src/main/java/org/matrix/androidsdk/crypto/keysbackup/KeysBackupStateManager.kt @@ -0,0 +1,114 @@ +/* + * Copyright 2018 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 org.matrix.androidsdk.crypto.keysbackup + +import org.matrix.androidsdk.util.Log +import java.util.* + +class KeysBackupStateManager { + + private val mListeners = ArrayList() + + // Backup state + var state = KeysBackupState.Unknown + set(newState) { + Log.d("KeysBackup", "setState: $field -> $newState") + + field = newState + + // Notify listeners about the state change + synchronized(mListeners) { + mListeners.forEach { + it.onStateChange(state) + } + } + } + + val isEnabled: Boolean + get() = state == KeysBackupStateManager.KeysBackupState.ReadyToBackUp + || state == KeysBackupStateManager.KeysBackupState.BackingUp + || state == KeysBackupStateManager.KeysBackupState.WillBackUp + + /** + * E2e keys backup states. + * + *
+     *                               |
+     *                               V        deleteKeyBackupVersion (on current backup)
+     *  +---------------------->  UNKNOWN  <-------------
+     *  |                            |
+     *  |                            | checkAndStartKeyBackup (at startup or on new verified device or a new detected backup)
+     *  |                            V
+     *  |                     CHECKING BACKUP
+     *  |                            |
+     *  |    Network error           |
+     *  +<----------+----------------+-------> DISABLED <----------------------+
+     *  |           |                |            |                            |
+     *  |           |                |            | createKeyBackupVersion     |
+     *  |           V                |            V                            |
+     *  +<---  WRONG VERSION         |         ENABLING                        |
+     *              ^                |            |                            |
+     *              |                V       ok   |     error                  |
+     *              |     +------> READY <--------+----------------------------+
+     *              |     |          |
+     *              |     |          | on new key
+     *              |     |          V
+     *              |     |     WILL BACK UP (waiting a random duration)
+     *              |     |          |
+     *              |     |          |
+     *              |     | ok       V
+     *              |     +----- BACKING UP
+     *              |                |
+     *              |      Error     |
+     *              +<---------------+
+     * 
+ */ + enum class KeysBackupState { + // Need to check the current backup version on the homeserver + Unknown, + // Checking if backup is enabled on home server + CheckingBackUpOnHomeserver, + // Backup has been stopped because a new backup version has been detected on the homeserver + WrongBackUpVersion, + // Backup from this device is not enabled + Disabled, + // Backup is being enabled: the backup version is being created on the homeserver + Enabling, + // Backup is enabled and ready to send backup to the homeserver + ReadyToBackUp, + // Backup is going to be send to the homeserver + WillBackUp, + // Backup is being sent to the homeserver + BackingUp + } + + interface KeysBackupStateListener { + fun onStateChange(newState: KeysBackupState) + } + + fun addListener(listener: KeysBackupStateListener) { + synchronized(mListeners) { + mListeners.add(listener) + } + } + + fun removeListener(listener: KeysBackupStateListener) { + synchronized(mListeners) { + mListeners.remove(listener) + } + } +} diff --git a/matrix-sdk/src/main/java/org/matrix/androidsdk/crypto/keysbackup/MegolmBackupAuthData.kt b/matrix-sdk/src/main/java/org/matrix/androidsdk/crypto/keysbackup/MegolmBackupAuthData.kt new file mode 100644 index 000000000..203434ac9 --- /dev/null +++ b/matrix-sdk/src/main/java/org/matrix/androidsdk/crypto/keysbackup/MegolmBackupAuthData.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2018 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 org.matrix.androidsdk.crypto.keysbackup + +import com.google.gson.annotations.SerializedName + +/** + * Data model for [org.matrix.androidsdk.rest.model.keys.KeysAlgorithmAndData.authData] in case + * of [org.matrix.androidsdk.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP]. + */ +data class MegolmBackupAuthData( + /** + * The curve25519 public key used to encrypt the backups. + */ + @SerializedName("public_key") + val publicKey: String = "", + + /** + * Signatures of the public key. + */ + val signatures: MutableMap? = null +) { + /** + * Same as the parent [MXJSONModel JSONDictionary] but return only + * data that must be signed. + */ + fun signalableJSONDictionary(): Map = mapOf("public_key" to publicKey) +} diff --git a/matrix-sdk/src/main/java/org/matrix/androidsdk/crypto/keysbackup/MegolmBackupCreationInfo.kt b/matrix-sdk/src/main/java/org/matrix/androidsdk/crypto/keysbackup/MegolmBackupCreationInfo.kt new file mode 100644 index 000000000..58258802e --- /dev/null +++ b/matrix-sdk/src/main/java/org/matrix/androidsdk/crypto/keysbackup/MegolmBackupCreationInfo.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2018 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 org.matrix.androidsdk.crypto.keysbackup + +/** + * Data retrieved from Olm library. algorithm and authData will be send to the homeserver, and recoveryKey will be displayed to the user + */ +class MegolmBackupCreationInfo { + + /** + * The algorithm used for storing backups [org.matrix.androidsdk.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP]. + */ + var algorithm: String = "" + + /** + * Authentication data. + */ + var authData: MegolmBackupAuthData? = null + + /** + * The Base58 recovery key. + */ + var recoveryKey: String = "" + +} diff --git a/matrix-sdk/src/main/java/org/matrix/androidsdk/crypto/util/Base58.kt b/matrix-sdk/src/main/java/org/matrix/androidsdk/crypto/util/Base58.kt new file mode 100644 index 000000000..19dd046b9 --- /dev/null +++ b/matrix-sdk/src/main/java/org/matrix/androidsdk/crypto/util/Base58.kt @@ -0,0 +1,85 @@ +/** + * Copyright 2011 Google Inc. + * Copyright 2018 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 org.matrix.androidsdk.crypto.util + +import java.math.BigInteger + +/** + * Ref: https://github.com/bitcoin-labs/bitcoin-mobile-android/blob/master/src/bitcoinj/java/com/google/bitcoin/core/Base58.java + * + * + * A custom form of base58 is used to encode BitCoin addresses. Note that this is not the same base58 as used by + * Flickr, which you may see reference to around the internet. + * + * Satoshi says: why base-58 instead of standard base-64 encoding? + * + * * Don't want 0OIl characters that look the same in some fonts and + * could be used to create visually identical looking account numbers. + * * A string with non-alphanumeric characters is not as easily accepted as an account number. + * * E-mail usually won't line-break if there's no punctuation to break at. + * * Doubleclicking selects the whole number as one word if it's all alphanumeric. + * + */ +private const val ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" +private val BASE = BigInteger.valueOf(58) + +/** + * Encode a byte array to a human readable string with base58 chars + */ +fun base58encode(input: ByteArray): String { + var bi = BigInteger(1, input) + val s = StringBuffer() + while (bi >= BASE) { + val mod = bi.mod(BASE) + s.insert(0, ALPHABET[mod.toInt()]) + bi = bi.subtract(mod).divide(BASE) + } + s.insert(0, ALPHABET[bi.toInt()]) + // Convert leading zeros too. + for (anInput in input) { + if (anInput.toInt() == 0) + s.insert(0, ALPHABET[0]) + else + break + } + return s.toString() +} + +/** + * Decode a base58 String to a byte array + */ +fun base58decode(input: String): ByteArray { + var result = decodeToBigInteger(input).toByteArray() + + // Remove the first leading zero if any + if (result[0] == 0.toByte()) { + result = result.copyOfRange(1, result.size) + } + + return result +} + +private fun decodeToBigInteger(input: String): BigInteger { + var bi = BigInteger.valueOf(0) + // Work backwards through the string. + for (i in input.length - 1 downTo 0) { + val alphaIndex = ALPHABET.indexOf(input[i]) + bi = bi.add(BigInteger.valueOf(alphaIndex.toLong()).multiply(BASE.pow(input.length - 1 - i))) + } + return bi +} diff --git a/matrix-sdk/src/main/java/org/matrix/androidsdk/crypto/util/RecoveryKey.kt b/matrix-sdk/src/main/java/org/matrix/androidsdk/crypto/util/RecoveryKey.kt new file mode 100644 index 000000000..5f3b29062 --- /dev/null +++ b/matrix-sdk/src/main/java/org/matrix/androidsdk/crypto/util/RecoveryKey.kt @@ -0,0 +1,122 @@ +/* + * Copyright 2018 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 org.matrix.androidsdk.crypto.util + +import org.matrix.androidsdk.extensions.split4 +import kotlin.experimental.xor + +/** + * See https://github.com/uhoreg/matrix-doc/blob/e2e_backup/proposals/1219-storing-megolm-keys-serverside.md + */ + +private const val CHAR_0 = 0x8B.toByte() +private const val CHAR_1 = 0x01.toByte() + +private const val RECOVERY_KEY_LENGTH = 2 + 32 + 1 + +/** + * Tell if the format of the recovery key is correct + * + * @param recoveryKey + * @return true if the format of the recovery key is correct + */ +fun isValidRecoveryKey(recoveryKey: String?): Boolean { + return extractCurveKeyFromRecoveryKey(recoveryKey) != null +} + +/** + * Copute recovery key from curve25519 key + * + * @param curve25519Key + * @return the recovery key + */ +fun computeRecoveryKey(curve25519Key: ByteArray): String { + // Append header and parity + val data = ByteArray(curve25519Key.size + 3) + + // Header + data[0] = CHAR_0 + data[1] = CHAR_1 + + // Copy key and compute parity + var parity: Byte = CHAR_0 xor CHAR_1 + + for (i in curve25519Key.indices) { + data[i + 2] = curve25519Key[i] + parity = parity xor curve25519Key[i] + } + + // Parity + data[curve25519Key.size + 2] = parity + + val b58Encoded = base58encode(data) + + // Add white space every 4 chars + return b58Encoded.split4() +} + +/** + * Please call [.isValidRecoveryKey] and ensure it returns true before calling this method + * + * @param recoveryKey the recovery key + * @return curveKey, or null in case of error + */ +fun extractCurveKeyFromRecoveryKey(recoveryKey: String?): ByteArray? { + if (recoveryKey == null) { + return null + } + + // Remove any space + val spaceFreeRecoveryKey = recoveryKey.replace(" ".toRegex(), "") + + val b58DecodedKey = base58decode(spaceFreeRecoveryKey) + + // Check length + if (b58DecodedKey.size != RECOVERY_KEY_LENGTH) { + return null + } + + // Check first byte + if (b58DecodedKey[0] != CHAR_0) { + return null + } + + // Check second byte + if (b58DecodedKey[1] != CHAR_1) { + return null + } + + // Check parity + var parity: Byte = 0 + + for (i in 0 until RECOVERY_KEY_LENGTH) { + parity = parity xor b58DecodedKey[i] + } + + if (parity != 0.toByte()) { + return null + } + + // Remove header and parity bytes + val result = ByteArray(b58DecodedKey.size - 3) + + for (i in 2 until b58DecodedKey.size - 1) { + result[i - 2] = b58DecodedKey[i] + } + + return result +} diff --git a/matrix-sdk/src/main/java/org/matrix/androidsdk/data/cryptostore/IMXCryptoStore.java b/matrix-sdk/src/main/java/org/matrix/androidsdk/data/cryptostore/IMXCryptoStore.java index d30ae3be1..57a485bb5 100644 --- a/matrix-sdk/src/main/java/org/matrix/androidsdk/data/cryptostore/IMXCryptoStore.java +++ b/matrix-sdk/src/main/java/org/matrix/androidsdk/data/cryptostore/IMXCryptoStore.java @@ -98,7 +98,7 @@ public interface IMXCryptoStore { /** * Store a device for a user. * - * @param userId The user's id. + * @param userId the user's id. * @param device the device to store. */ void storeUserDevice(String userId, MXDeviceInfo device); @@ -106,12 +106,21 @@ public interface IMXCryptoStore { /** * Retrieve a device for a user. * - * @param deviceId The device id. - * @param userId The user's id. - * @return A map from device id to 'MXDevice' object for the device. + * @param deviceId the device id. + * @param userId the user's id. + * @return the device */ MXDeviceInfo getUserDevice(String deviceId, String userId); + /** + * Retrieve a device by its identity key. + * + * @param identityKey the device identity key (`MXDeviceInfo.identityKey`) + * @return the device or null if not found + */ + @Nullable + MXDeviceInfo deviceWithIdentityKey(String identityKey); + /** * Store the known devices for a user. * @@ -205,6 +214,39 @@ public interface IMXCryptoStore { */ void removeInboundGroupSession(String sessionId, String senderKey); + /* ========================================================================================== + * Keys backup + * ========================================================================================== */ + + /** + * Mark all inbound group sessions as not backed up. + */ + void resetBackupMarkers(); + + /** + * Mark an inbound group session as backed up on the user homeserver. + * + * @param sessionId the session identifier. + * @param senderKey the base64-encoded curve25519 key of the sender. + */ + void markBackupDoneForInboundGroupSessionWithId(String sessionId, String senderKey); + + /** + * Retrieve inbound group sessions that are not yet backed up. + * + * @param limit the maximum number of sessions to return. + * @return an array of non backed up inbound group sessions. + */ + List inboundGroupSessionsToBackup(int limit); + + /** + * Number of stored inbound group sessions. + * + * @param onlyBackedUp if true, count only session marked as backed up. + * @return a count. + */ + int inboundGroupSessionsCount(boolean onlyBackedUp); + /** * Set the global override for whether the client should ever send encrypted * messages to unverified devices. @@ -234,6 +276,19 @@ public interface IMXCryptoStore { */ List getRoomsListBlacklistUnverifiedDevices(); + /** + * Set the current keys backup version + * + * @param keyBackupVersion the keys backup version or null to delete it + */ + void setKeyBackupVersion(@Nullable String keyBackupVersion); + + /** + * Get the current keys backup version + */ + @Nullable + String getKeyBackupVersion(); + /** * @return the devices statuses map (userId -> tracking status) */ diff --git a/matrix-sdk/src/main/java/org/matrix/androidsdk/data/cryptostore/MXFileCryptoStore.java b/matrix-sdk/src/main/java/org/matrix/androidsdk/data/cryptostore/MXFileCryptoStore.java index 664b0df54..3109ec65d 100644 --- a/matrix-sdk/src/main/java/org/matrix/androidsdk/data/cryptostore/MXFileCryptoStore.java +++ b/matrix-sdk/src/main/java/org/matrix/androidsdk/data/cryptostore/MXFileCryptoStore.java @@ -54,7 +54,10 @@ /** * the crypto data store + * + * @deprecated use RealmCryptoStore now. The MXFileCryptoStore does not support Keys Backup. */ +@Deprecated public class MXFileCryptoStore implements IMXCryptoStore { private static final String LOG_TAG = MXFileCryptoStore.class.getSimpleName(); @@ -572,6 +575,13 @@ public MXDeviceInfo getUserDevice(String deviceId, String userId) { return deviceInfo; } + @Override + @Nullable + public MXDeviceInfo deviceWithIdentityKey(String identityKey) { + // No op + return null; + } + @Override public void storeUserDevices(String userId, Map devices) { if (!mIsReady) { @@ -931,6 +941,27 @@ public List getInboundGroupSessions() { return inboundGroupSessions; } + @Override + public void resetBackupMarkers() { + // No op + } + + @Override + public void markBackupDoneForInboundGroupSessionWithId(String sessionId, String senderKey) { + // No op + } + + @Override + public List inboundGroupSessionsToBackup(int limit) { + // No op + return new ArrayList<>(); + } + + @Override + public int inboundGroupSessionsCount(boolean onlyBackedUp) { + return 0; + } + @Override public void close() { // release JNI objects @@ -1007,6 +1038,18 @@ public List getRoomsListBlacklistUnverifiedDevices() { } } + @Override + public void setKeyBackupVersion(@Nullable String keyBackupVersion) { + // No op + } + + @Nullable + @Override + public String getKeyBackupVersion() { + // No op + return null; + } + /** * save the outgoing room key requests. */ diff --git a/matrix-sdk/src/main/java/org/matrix/androidsdk/data/cryptostore/db/RealmCryptoStore.kt b/matrix-sdk/src/main/java/org/matrix/androidsdk/data/cryptostore/db/RealmCryptoStore.kt index 5d8105f9f..2eab15983 100644 --- a/matrix-sdk/src/main/java/org/matrix/androidsdk/data/cryptostore/db/RealmCryptoStore.kt +++ b/matrix-sdk/src/main/java/org/matrix/androidsdk/data/cryptostore/db/RealmCryptoStore.kt @@ -36,6 +36,7 @@ import org.matrix.olm.OlmAccount import org.matrix.olm.OlmException import org.matrix.olm.OlmSession import java.io.File +import kotlin.collections.set // enableFileEncryption is used to migrate the previous store class RealmCryptoStore(private val enableFileEncryption: Boolean = false) : IMXCryptoStore { @@ -203,6 +204,19 @@ class RealmCryptoStore(private val enableFileEncryption: Boolean = false) : IMXC ?.getDeviceInfo() } + override fun deviceWithIdentityKey(identityKey: String?): MXDeviceInfo? { + if (identityKey == null) { + return null + } + + return doRealmQueryAndCopy(realmConfiguration) { + it.where() + .equalTo(DeviceInfoEntityFields.IDENTITY_KEY, identityKey) + .findFirst() + } + ?.getDeviceInfo() + } + override fun storeUserDevices(userId: String?, devices: MutableMap?) { if (userId == null) { return @@ -411,6 +425,67 @@ class RealmCryptoStore(private val enableFileEncryption: Boolean = false) : IMXC } } + /* ========================================================================================== + * Keys backup + * ========================================================================================== */ + + override fun getKeyBackupVersion(): String? { + return doRealmQueryAndCopy(realmConfiguration) { + it.where().findFirst() + }?.backupVersion + } + + override fun setKeyBackupVersion(keyBackupVersion: String?) { + doRealmTransaction(realmConfiguration) { + it.where().findFirst()?.backupVersion = keyBackupVersion + } + } + + override fun resetBackupMarkers() { + doRealmTransaction(realmConfiguration) { + it.where() + .findAll() + .map { inboundGroupSession -> + inboundGroupSession.backedUp = false + } + } + } + + override fun markBackupDoneForInboundGroupSessionWithId(sessionId: String, senderKey: String) { + val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionId, senderKey) + + doRealmTransaction(realmConfiguration) { + it.where() + .equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key) + .findFirst() + ?.backedUp = true + } + } + + override fun inboundGroupSessionsToBackup(limit: Int): List { + return doRealmQueryAndCopyList(realmConfiguration) { + it.where() + .equalTo(OlmInboundGroupSessionEntityFields.BACKED_UP, false) + .limit(limit.toLong()) + .findAll() + }.mapNotNull { inboundGroupSession -> + inboundGroupSession.getInboundGroupSession() + } + } + + override fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int { + return doWithRealm(realmConfiguration) { + it.where() + .apply { + if (onlyBackedUp) { + equalTo(OlmInboundGroupSessionEntityFields.BACKED_UP, true) + } + } + .count() + .toInt() + } + } + override fun setGlobalBlacklistUnverifiedDevices(block: Boolean) { doRealmTransaction(realmConfiguration) { it.where().findFirst()?.globalBlacklistUnverifiedDevices = block diff --git a/matrix-sdk/src/main/java/org/matrix/androidsdk/data/cryptostore/db/RealmCryptoStoreMigration.kt b/matrix-sdk/src/main/java/org/matrix/androidsdk/data/cryptostore/db/RealmCryptoStoreMigration.kt index b0c2c1842..a750a25d4 100644 --- a/matrix-sdk/src/main/java/org/matrix/androidsdk/data/cryptostore/db/RealmCryptoStoreMigration.kt +++ b/matrix-sdk/src/main/java/org/matrix/androidsdk/data/cryptostore/db/RealmCryptoStoreMigration.kt @@ -18,12 +18,14 @@ package org.matrix.androidsdk.data.cryptostore.db import io.realm.DynamicRealm import io.realm.RealmMigration +import org.matrix.androidsdk.util.Log internal object RealmCryptoStoreMigration : RealmMigration { + const val LOG_TAG = "RealmCryptoStoreMigration" const val CRYPTO_STORE_SCHEMA_VERSION = 0L override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { + Log.d(LOG_TAG, "Migrating Realm Crypto from $oldVersion to $newVersion") } - } diff --git a/matrix-sdk/src/main/java/org/matrix/androidsdk/data/cryptostore/db/model/CryptoMetadataEntity.kt b/matrix-sdk/src/main/java/org/matrix/androidsdk/data/cryptostore/db/model/CryptoMetadataEntity.kt index d9a379272..4689403c9 100644 --- a/matrix-sdk/src/main/java/org/matrix/androidsdk/data/cryptostore/db/model/CryptoMetadataEntity.kt +++ b/matrix-sdk/src/main/java/org/matrix/androidsdk/data/cryptostore/db/model/CryptoMetadataEntity.kt @@ -33,7 +33,7 @@ internal open class CryptoMetadataEntity( var deviceSyncToken: String? = null, // Settings for blacklisting unverified devices. var globalBlacklistUnverifiedDevices: Boolean = false, - // The keys backup version currently used. + // The keys backup version currently used. Null means no backup. var backupVersion: String? = null ) : RealmObject() { diff --git a/matrix-sdk/src/main/java/org/matrix/androidsdk/data/cryptostore/db/model/OlmInboundGroupSessionEntity.kt b/matrix-sdk/src/main/java/org/matrix/androidsdk/data/cryptostore/db/model/OlmInboundGroupSessionEntity.kt index 2be5f50d7..dabdfbf1f 100644 --- a/matrix-sdk/src/main/java/org/matrix/androidsdk/data/cryptostore/db/model/OlmInboundGroupSessionEntity.kt +++ b/matrix-sdk/src/main/java/org/matrix/androidsdk/data/cryptostore/db/model/OlmInboundGroupSessionEntity.kt @@ -31,7 +31,7 @@ open class OlmInboundGroupSessionEntity( var senderKey: String? = null, // olmInboundGroupSessionData contains Json var olmInboundGroupSessionData: String? = null, - // Indicate if the key has been backed up to the homeserver (for future use) + // Indicate if the key has been backed up to the homeserver var backedUp: Boolean = false) : RealmObject() { diff --git a/matrix-sdk/src/main/java/org/matrix/androidsdk/extensions/StringExtensions.kt b/matrix-sdk/src/main/java/org/matrix/androidsdk/extensions/StringExtensions.kt new file mode 100644 index 000000000..c49684f2c --- /dev/null +++ b/matrix-sdk/src/main/java/org/matrix/androidsdk/extensions/StringExtensions.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2018 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 org.matrix.androidsdk.extensions + +/** + * Split a String in group of 4 chars, separated with space + */ +fun String.split4() = chunked(4).joinToString(separator = " ") diff --git a/matrix-sdk/src/main/java/org/matrix/androidsdk/rest/api/RoomKeysApi.kt b/matrix-sdk/src/main/java/org/matrix/androidsdk/rest/api/RoomKeysApi.kt new file mode 100644 index 000000000..468e12b31 --- /dev/null +++ b/matrix-sdk/src/main/java/org/matrix/androidsdk/rest/api/RoomKeysApi.kt @@ -0,0 +1,135 @@ +/* + * Copyright 2018 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 org.matrix.androidsdk.rest.api + +import org.matrix.androidsdk.rest.model.keys.* +import retrofit2.Call +import retrofit2.http.* + +/** + * Ref: https://github.com/uhoreg/matrix-doc/blob/e2e_backup/proposals/1219-storing-megolm-keys-serverside.md + */ +interface RoomKeysApi { + + /* ========================================================================================== + * Backup versions management + * ========================================================================================== */ + + /** + * Create a new keys backup version. + */ + @POST("room_keys/version") + fun createKeysBackupVersion(@Body createKeysBackupVersionBody: CreateKeysBackupVersionBody): Call + + /** + * Get information about the last version. + */ + @GET("room_keys/version") + fun getKeysBackupLastVersion(): Call + + /** + * Get information about the given version. + */ + @GET("room_keys/version/{version}") + fun getKeysBackupVersion(@Path("version") version: String): Call + + /* ========================================================================================== + * Storing keys + * ========================================================================================== */ + + /** + * Store the key for the given session in the given room, using the given backup version. + * + * + * If the server already has a backup in the backup version for the given session and room, then it will + * keep the "better" one. To determine which one is "better", key backups are compared first by the is_verified + * flag (true is better than false), then by the first_message_index (a lower number is better), and finally by + * forwarded_count (a lower number is better). + */ + @PUT("room_keys/keys/{roomId}/{sessionId}") + fun storeRoomSessionData(@Path("roomId") roomId: String, + @Path("sessionId") sessionId: String, + @Query("version") version: String, + @Body keyBackupData: KeyBackupData): Call + + + /** + * Store several keys for the given room, using the given backup version. + */ + @PUT("room_keys/keys/{roomId}") + fun storeRoomSessionsData(@Path("roomId") roomId: String, + @Query("version") version: String, + @Body roomKeysBackupData: RoomKeysBackupData): Call + + /** + * Store several keys, using the given backup version. + */ + @PUT("room_keys/keys") + fun storeSessionsData(@Query("version") version: String, + @Body keysBackupData: KeysBackupData): Call + + /* ========================================================================================== + * Retrieving keys + * ========================================================================================== */ + + /** + * Retrieve the key for the given session in the given room from the backup. + */ + @GET("room_keys/keys/{roomId}/{sessionId}") + fun getRoomSessionData(@Path("roomId") roomId: String, + @Path("sessionId") sessionId: String, + @Query("version") version: String): Call + + /** + * Retrieve all the keys for the given room from the backup. + */ + @GET("room_keys/keys/{roomId}") + fun getRoomSessionsData(@Path("roomId") roomId: String, + @Query("version") version: String): Call + + /** + * Retrieve all the keys from the backup. + */ + @GET("room_keys/keys") + fun getSessionsData(@Query("version") version: String): Call + + + /* ========================================================================================== + * Deleting keys + * ========================================================================================== */ + + /** + * Deletes keys from the backup. + */ + @DELETE("room_keys/keys/{roomId}/{sessionId}") + fun deleteRoomSessionData(@Path("roomId") roomId: String, + @Path("sessionId") sessionId: String, + @Query("version") version: String): Call + + /** + * Deletes keys from the backup. + */ + @DELETE("room_keys/keys/{roomId}") + fun deleteRoomSessionsData(@Path("roomId") roomId: String, + @Query("version") version: String): Call + + /** + * Deletes keys from the backup. + */ + @DELETE("room_keys/keys") + fun deleteSessionsData(@Query("version") version: String): Call +} diff --git a/matrix-sdk/src/main/java/org/matrix/androidsdk/rest/client/RoomKeysRestClient.kt b/matrix-sdk/src/main/java/org/matrix/androidsdk/rest/client/RoomKeysRestClient.kt new file mode 100644 index 000000000..08da51923 --- /dev/null +++ b/matrix-sdk/src/main/java/org/matrix/androidsdk/rest/client/RoomKeysRestClient.kt @@ -0,0 +1,174 @@ +/* + * Copyright 2018 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 org.matrix.androidsdk.rest.client + +import org.matrix.androidsdk.HomeServerConnectionConfig +import org.matrix.androidsdk.RestClient +import org.matrix.androidsdk.rest.api.RoomKeysApi +import org.matrix.androidsdk.rest.callback.ApiCallback +import org.matrix.androidsdk.rest.callback.RestAdapterCallback +import org.matrix.androidsdk.rest.model.keys.* + +/** + * Class used to make requests to the RoomKeys API. + */ +class RoomKeysRestClient(hsConfig: HomeServerConnectionConfig) : + RestClient(hsConfig, RoomKeysApi::class.java, RestClient.URI_API_PREFIX_PATH_R0) { + + /** + * Get the key backup last version + * If not supported by the server, an error is returned: {"errcode":"M_NOT_FOUND","error":"No backup found"} + * + * @param callback the callback + */ + fun getKeysBackupLastVersion(callback: ApiCallback) { + val description = "getKeysBackupVersion" + + mApi.getKeysBackupLastVersion() + .enqueue(RestAdapterCallback(description, null, callback, null)) + } + + /** + * Get a key backup specific version + * If not supported by the server, an error is returned: {"errcode":"M_NOT_FOUND","error":"No backup found"} + * + * @param version version + * @param callback the callback + */ + fun getKeysBackupVersion(version: String, + callback: ApiCallback) { + val description = "getKeysBackupVersion" + + mApi.getKeysBackupVersion(version) + .enqueue(RestAdapterCallback(description, null, callback, null)) + } + + /** + * Create a keys backup version + * + * @param createKeysBackupVersionBody the body + * @param callback the callback + */ + fun createKeysBackupVersion(createKeysBackupVersionBody: CreateKeysBackupVersionBody, callback: ApiCallback) { + val description = "createKeysBackupVersion" + + mApi.createKeysBackupVersion(createKeysBackupVersionBody) + .enqueue(RestAdapterCallback(description, null, callback, null)) + } + + /** + * Send room session data for the given room, session, and version + * + * @param roomId the room id + * @param sessionId the session id + * @param version the version of the backup + * @param keyBackupData the data to send + * @param callback the callback + */ + fun sendKeyBackup(roomId: String, + sessionId: String, + version: String, + keyBackupData: KeyBackupData, + callback: ApiCallback) { + val description = "sendKeyBackup" + + mApi.storeRoomSessionData(roomId, sessionId, version, keyBackupData) + .enqueue(RestAdapterCallback(description, null, callback, null)) + } + + /** + * Send room session data + * + * @param version the version of the backup + * @param keysBackupData the data to send + * @param callback the callback + */ + fun sendKeysBackup(version: String, + keysBackupData: KeysBackupData, + callback: ApiCallback) { + val description = "sendKeysBackup" + + mApi.storeSessionsData(version, keysBackupData) + .enqueue(RestAdapterCallback(description, null, callback, null)) + } + + /** + * Retrieve the key for the given session in the given room from the backup. + * + * @param roomId the room id + * @param sessionId the session id + * @param version the version of the backup, or empty String to retrieve the last version + * @param callback the callback + */ + fun getRoomKeyBackup(roomId: String, sessionId: String, version: String, callback: ApiCallback) { + val description = "getKeyBackup" + + mApi.getRoomSessionData(roomId, sessionId, version) + .enqueue(RestAdapterCallback(description, null, callback, null)) + } + + /** + * Retrieve all the keys for the given room from the backup. + * + * @param roomId the room id + * @param version the version of the backup, or empty String to retrieve the last version + * @param callback the callback + */ + fun getRoomKeysBackup(roomId: String, version: String, callback: ApiCallback) { + val description = "getRoomKeysBackup" + + mApi.getRoomSessionsData(roomId, version) + .enqueue(RestAdapterCallback(description, null, callback, null)) + } + + /** + * Retrieve the complete sessions data for the given backup version + * + * @param version the version of the backup, or empty String to retrieve the last version + * @param callback the callback + */ + fun getKeysBackup(version: String, callback: ApiCallback) { + val description = "getKeyBackup" + + mApi.getSessionsData(version) + .enqueue(RestAdapterCallback(description, null, callback, null)) + } + + /** + * @param roomId + * @param sessionId + * @param version + * @param callback + */ + fun deleteKeyBackup(roomId: String, sessionId: String, version: String, callback: ApiCallback) { + val description = "deleteKeyBackup" + + mApi.deleteRoomSessionData(roomId, sessionId, version) + .enqueue(RestAdapterCallback(description, null, callback, null)) + } + + /** + * @param version + * @param callback + */ + fun deleteKeysBackup(version: String, callback: ApiCallback) { + val description = "deleteKeyBackup" + + mApi.deleteSessionsData(version) + .enqueue(RestAdapterCallback(description, null, callback, null)) + } +} diff --git a/matrix-sdk/src/main/java/org/matrix/androidsdk/rest/model/MatrixError.java b/matrix-sdk/src/main/java/org/matrix/androidsdk/rest/model/MatrixError.java index 9217b3440..3303f73b9 100644 --- a/matrix-sdk/src/main/java/org/matrix/androidsdk/rest/model/MatrixError.java +++ b/matrix-sdk/src/main/java/org/matrix/androidsdk/rest/model/MatrixError.java @@ -55,6 +55,7 @@ public class MatrixError implements java.io.Serializable { public static final String TOO_LARGE = "M_TOO_LARGE"; public static final String M_CONSENT_NOT_GIVEN = "M_CONSENT_NOT_GIVEN"; public static final String RESOURCE_LIMIT_EXCEEDED = "M_RESOURCE_LIMIT_EXCEEDED"; + public static final String WRONG_ROOM_KEYS_VERSION = "M_WRONG_ROOM_KEYS_VERSION"; // custom ones public static final String NOT_SUPPORTED = "M_NOT_SUPPORTED"; diff --git a/matrix-sdk/src/main/java/org/matrix/androidsdk/rest/model/keys/CreateKeysBackupVersionBody.kt b/matrix-sdk/src/main/java/org/matrix/androidsdk/rest/model/keys/CreateKeysBackupVersionBody.kt new file mode 100644 index 000000000..886406e51 --- /dev/null +++ b/matrix-sdk/src/main/java/org/matrix/androidsdk/rest/model/keys/CreateKeysBackupVersionBody.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2018 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 org.matrix.androidsdk.rest.model.keys + +class CreateKeysBackupVersionBody : KeysAlgorithmAndData() diff --git a/matrix-sdk/src/main/java/org/matrix/androidsdk/rest/model/keys/KeyBackupData.kt b/matrix-sdk/src/main/java/org/matrix/androidsdk/rest/model/keys/KeyBackupData.kt new file mode 100644 index 000000000..fd7f74d9f --- /dev/null +++ b/matrix-sdk/src/main/java/org/matrix/androidsdk/rest/model/keys/KeyBackupData.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2018 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 org.matrix.androidsdk.rest.model.keys + +import com.google.gson.JsonElement +import com.google.gson.annotations.SerializedName + +/** + * Backup data for one key. + */ +class KeyBackupData { + + /** + * Required. The index of the first message in the session that the key can decrypt. + */ + @SerializedName("first_message_index") + var firstMessageIndex: Long = 0 + + /** + * Required. The number of times this key has been forwarded. + */ + @SerializedName("forwarded_count") + var forwardedCount: Int = 0 + + /** + * Whether the device backing up the key has verified the device that the key is from. + */ + @SerializedName("is_verified") + var isVerified: Boolean = false + + /** + * Algorithm-dependent data. + */ + @SerializedName("session_data") + var sessionData: JsonElement? = null +} diff --git a/matrix-sdk/src/main/java/org/matrix/androidsdk/rest/model/keys/KeysAlgorithmAndData.kt b/matrix-sdk/src/main/java/org/matrix/androidsdk/rest/model/keys/KeysAlgorithmAndData.kt new file mode 100644 index 000000000..52c1d2bfa --- /dev/null +++ b/matrix-sdk/src/main/java/org/matrix/androidsdk/rest/model/keys/KeysAlgorithmAndData.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2018 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 org.matrix.androidsdk.rest.model.keys + +import com.google.gson.JsonElement +import com.google.gson.annotations.SerializedName +import org.matrix.androidsdk.crypto.keysbackup.MegolmBackupAuthData +import org.matrix.androidsdk.util.JsonUtils + +/** + *
+ *     Example:
+ *
+ *     {
+ *         "algorithm": "m.megolm_backup.v1.curve25519-aes-sha2",
+ *         "auth_data": {
+ *             "public_key": "abcdefg",
+ *             "signatures": {
+ *                 "something": {
+ *                     "ed25519:something": "hijklmnop"
+ *                 }
+ *             }
+ *         }
+ *     }
+ * 
+ */ +open class KeysAlgorithmAndData { + + /** + * The algorithm used for storing backups. Currently, only "m.megolm_backup.v1.curve25519-aes-sha2" is defined + */ + var algorithm: String? = null + + /** + * algorithm-dependent data, for "m.megolm_backup.v1.curve25519-aes-sha2" see [org.matrix.androidsdk.crypto.keysbackup.MegolmBackupAuthData] + */ + @SerializedName("auth_data") + var authData: JsonElement? = null + + /** + * Facility method to convert authData to a MegolmBackupAuthData object + */ + fun getAuthDataAsMegolmBackupAuthData() = JsonUtils.getBasicGson() + .fromJson(authData, MegolmBackupAuthData::class.java) +} diff --git a/matrix-sdk/src/main/java/org/matrix/androidsdk/rest/model/keys/KeysBackupData.kt b/matrix-sdk/src/main/java/org/matrix/androidsdk/rest/model/keys/KeysBackupData.kt new file mode 100644 index 000000000..2e18ce49e --- /dev/null +++ b/matrix-sdk/src/main/java/org/matrix/androidsdk/rest/model/keys/KeysBackupData.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2018 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 org.matrix.androidsdk.rest.model.keys + +import com.google.gson.annotations.SerializedName + +/** + * Backup data for several keys in several rooms. + */ +class KeysBackupData { + + // the keys are the room IDs, and the values are RoomKeysBackupData + @SerializedName("rooms") + var roomIdToRoomKeysBackupData: MutableMap = HashMap() + +} diff --git a/matrix-sdk/src/main/java/org/matrix/androidsdk/rest/model/keys/KeysVersion.kt b/matrix-sdk/src/main/java/org/matrix/androidsdk/rest/model/keys/KeysVersion.kt new file mode 100644 index 000000000..5c0d47858 --- /dev/null +++ b/matrix-sdk/src/main/java/org/matrix/androidsdk/rest/model/keys/KeysVersion.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2018 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 org.matrix.androidsdk.rest.model.keys + +class KeysVersion { + + // the keys backup version + var version: String? = null +} diff --git a/matrix-sdk/src/main/java/org/matrix/androidsdk/rest/model/keys/KeysVersionResult.kt b/matrix-sdk/src/main/java/org/matrix/androidsdk/rest/model/keys/KeysVersionResult.kt new file mode 100644 index 000000000..9e2ddd18f --- /dev/null +++ b/matrix-sdk/src/main/java/org/matrix/androidsdk/rest/model/keys/KeysVersionResult.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2018 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 org.matrix.androidsdk.rest.model.keys + +class KeysVersionResult : KeysAlgorithmAndData() { + + // the backup version + var version: String? = null +} diff --git a/matrix-sdk/src/main/java/org/matrix/androidsdk/rest/model/keys/RoomKeysBackupData.kt b/matrix-sdk/src/main/java/org/matrix/androidsdk/rest/model/keys/RoomKeysBackupData.kt new file mode 100644 index 000000000..80bcb28b4 --- /dev/null +++ b/matrix-sdk/src/main/java/org/matrix/androidsdk/rest/model/keys/RoomKeysBackupData.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2018 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 org.matrix.androidsdk.rest.model.keys + +import com.google.gson.annotations.SerializedName + +/** + * Backup data for several keys within a room. + */ +class RoomKeysBackupData { + + // the keys are the session IDs, and the values are KeyBackupData + @SerializedName("sessions") + var sessionIdToKeyBackupData: MutableMap = HashMap() +} diff --git a/matrix-sdk/src/main/java/org/matrix/androidsdk/util/JsonUtils.java b/matrix-sdk/src/main/java/org/matrix/androidsdk/util/JsonUtils.java index 74a25e121..fff2e13e3 100644 --- a/matrix-sdk/src/main/java/org/matrix/androidsdk/util/JsonUtils.java +++ b/matrix-sdk/src/main/java/org/matrix/androidsdk/util/JsonUtils.java @@ -72,6 +72,11 @@ public class JsonUtils { private static final Gson basicGson = new Gson(); + private static final Gson kotlinGson = new GsonBuilder() + .registerTypeAdapter(boolean.class, new BooleanDeserializer(false)) + .registerTypeAdapter(Boolean.class, new BooleanDeserializer(true)) + .create(); + private static final Gson gson = new GsonBuilder() .setFieldNamingStrategy(new MatrixFieldNamingStrategy()) .excludeFieldsWithModifiers(Modifier.PRIVATE, Modifier.STATIC) @@ -112,6 +117,15 @@ public static Gson getBasicGson() { return basicGson; } + /** + * Provides the JSON parser for Kotlin. + * + * @return the kotlin JSON parser + */ + public static Gson getKotlinGson() { + return kotlinGson; + } + /** * Provides the JSON parser. * diff --git a/matrix-sdk/src/test/java/org/matrix/androidsdk/crypto/util/Base58Test.kt b/matrix-sdk/src/test/java/org/matrix/androidsdk/crypto/util/Base58Test.kt new file mode 100644 index 000000000..e62f23f1d --- /dev/null +++ b/matrix-sdk/src/test/java/org/matrix/androidsdk/crypto/util/Base58Test.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2018 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 org.matrix.androidsdk.crypto.util + + +import org.junit.Assert +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runners.MethodSorters + +@FixMethodOrder(MethodSorters.JVM) +class Base58Test { + + @Test + fun encode() { + // Example comes from https://github.com/keis/base58 + Assert.assertEquals("StV1DL6CwTryKyV", base58encode("hello world".toByteArray())) + } + + @Test + fun decode() { + // Example comes from https://github.com/keis/base58 + Assert.assertArrayEquals("hello world".toByteArray(), base58decode("StV1DL6CwTryKyV")); + } + + @Test + fun encode_curve25519() { + // Encode a 32 bytes key + Assert.assertEquals("4F85ZySpwyY6FuH7mQYyyr5b8nV9zFRBLj92AJa37sMr", + base58encode(("0123456789" + "0123456789" + "0123456789" + "01").toByteArray())) + } + + @Test + fun decode_curve25519() { + Assert.assertArrayEquals(("0123456789" + "0123456789" + "0123456789" + "01").toByteArray(), + base58decode("4F85ZySpwyY6FuH7mQYyyr5b8nV9zFRBLj92AJa37sMr")) + } +} \ No newline at end of file diff --git a/matrix-sdk/src/test/java/org/matrix/androidsdk/crypto/util/RecoveryKeyTest.kt b/matrix-sdk/src/test/java/org/matrix/androidsdk/crypto/util/RecoveryKeyTest.kt new file mode 100644 index 000000000..bd491c922 --- /dev/null +++ b/matrix-sdk/src/test/java/org/matrix/androidsdk/crypto/util/RecoveryKeyTest.kt @@ -0,0 +1,76 @@ +/* + * Copyright 2018 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 org.matrix.androidsdk.crypto.util + +import org.junit.Assert +import org.junit.Test + +class RecoveryKeyTest { + private val curve25519Key = byteArrayOf( + 0x77.toByte(), 0x07.toByte(), 0x6D.toByte(), 0x0A.toByte(), 0x73.toByte(), 0x18.toByte(), 0xA5.toByte(), 0x7D.toByte(), + 0x3C.toByte(), 0x16.toByte(), 0xC1.toByte(), 0x72.toByte(), 0x51.toByte(), 0xB2.toByte(), 0x66.toByte(), 0x45.toByte(), + 0xDF.toByte(), 0x4C.toByte(), 0x2F.toByte(), 0x87.toByte(), 0xEB.toByte(), 0xC0.toByte(), 0x99.toByte(), 0x2A.toByte(), + 0xB1.toByte(), 0x77.toByte(), 0xFB.toByte(), 0xA5.toByte(), 0x1D.toByte(), 0xB9.toByte(), 0x2C.toByte(), 0x2A.toByte()) + + @Test + fun isValidRecoveryKey_valid_true() { + Assert.assertTrue(isValidRecoveryKey("EsTcLW2KPGiFwKEA3As5g5c4BXwkqeeJZJV8Q9fugUMNUE4d")) + + // Space should be ignored + Assert.assertTrue(isValidRecoveryKey("EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d")) + } + + @Test + fun isValidRecoveryKey_null_false() { + Assert.assertFalse(isValidRecoveryKey(null)) + } + + @Test + fun isValidRecoveryKey_empty_false() { + Assert.assertFalse(isValidRecoveryKey("")) + } + + @Test + fun isValidRecoveryKey_wrong_size_false() { + Assert.assertFalse(isValidRecoveryKey("abc")) + } + + @Test + fun isValidRecoveryKey_bad_first_byte_false() { + Assert.assertFalse(isValidRecoveryKey("FsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d")) + } + + @Test + fun isValidRecoveryKey_bad_second_byte_false() { + Assert.assertFalse(isValidRecoveryKey("EqTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d")) + } + + @Test + fun isValidRecoveryKey_bad_parity_false() { + Assert.assertFalse(isValidRecoveryKey("EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4e")) + } + + @Test + fun computeRecoveryKey_ok() { + Assert.assertEquals("EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d", computeRecoveryKey(curve25519Key)) + } + + @Test + fun extractCurveKeyFromRecoveryKey_ok() { + Assert.assertArrayEquals(curve25519Key, extractCurveKeyFromRecoveryKey("EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d")) + } +} \ No newline at end of file diff --git a/matrix-sdk/src/test/java/org/matrix/androidsdk/data/cryptostore/db/HelperTest.kt b/matrix-sdk/src/test/java/org/matrix/androidsdk/data/cryptostore/db/HelperTest.kt index ef114ef31..da3afd435 100644 --- a/matrix-sdk/src/test/java/org/matrix/androidsdk/data/cryptostore/db/HelperTest.kt +++ b/matrix-sdk/src/test/java/org/matrix/androidsdk/data/cryptostore/db/HelperTest.kt @@ -16,7 +16,7 @@ package org.matrix.androidsdk.data.cryptostore.db -import junit.framework.Assert.assertEquals +import org.junit.Assert.assertEquals import org.junit.Test class HelperTest {