diff --git a/build.gradle b/build.gradle index c54c0f59..8c9840e9 100644 --- a/build.gradle +++ b/build.gradle @@ -3,6 +3,7 @@ buildscript { ext.kotlin_version = '1.3.21' ext.koin_version = '1.0.2' + // TODO ext.koin_version = '2.0.0-GA' repositories { google() jcenter() @@ -11,7 +12,7 @@ buildscript { } } dependencies { - classpath 'com.android.tools.build:gradle:3.3.2' + classpath 'com.android.tools.build:gradle:3.4.0' classpath 'com.google.gms:google-services:4.2.0' classpath "com.airbnb.okreplay:gradle-plugin:1.4.0" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" @@ -29,6 +30,10 @@ allprojects { maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' } google() jcenter() + // For Olm SDK + maven { + url 'https://jitpack.io' + } } } diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index c04907ab..4922a78e 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -48,7 +48,7 @@ android { buildConfigField "boolean", "LOG_PRIVATE_DATA", "false" // Set to BODY instead of NONE to enable logging - buildConfigField "okhttp3.logging.HttpLoggingInterceptor.Level", "OKHTTP_LOGGING_LEVEL", "okhttp3.logging.HttpLoggingInterceptor.Level.NONE" + buildConfigField "okhttp3.logging.HttpLoggingInterceptor.Level", "OKHTTP_LOGGING_LEVEL", "okhttp3.logging.HttpLoggingInterceptor.Level.BODY" } release { @@ -126,6 +126,9 @@ dependencies { implementation "io.arrow-kt:arrow-effects-instances:$arrow_version" implementation "io.arrow-kt:arrow-integration-retrofit-adapter:$arrow_version" + // olm lib is now hosted by jitpack: https://jitpack.io/#org.matrix.gitlab.matrix-org/olm + implementation 'org.matrix.gitlab.matrix-org:olm:3.1.2' + // DI implementation "org.koin:koin-core:$koin_version" implementation "org.koin:koin-core-ext:$koin_version" diff --git a/matrix-sdk-android/libs/olm-sdk.aar b/matrix-sdk-android/libs/olm-sdk.aar deleted file mode 100644 index 66be8a65..00000000 Binary files a/matrix-sdk-android/libs/olm-sdk.aar and /dev/null differ diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/comparators/DatedObjectComparators.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/comparators/DatedObjectComparators.kt new file mode 100644 index 00000000..aad33ab7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/comparators/DatedObjectComparators.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.comparators + +import im.vector.matrix.android.api.interfaces.DatedObject +import java.util.* + +object DatedObjectComparators { + + /** + * Comparator to sort DatedObjects from the oldest to the latest. + */ + val ascComparator by lazy { + Comparator { datedObject1, datedObject2 -> + (datedObject1.date - datedObject2.date).toInt() + } + } + + /** + * Comparator to sort DatedObjects from the latest to the oldest. + */ + val descComparator by lazy { + Comparator { datedObject1, datedObject2 -> + (datedObject2.date - datedObject1.date).toInt() + } + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/extensions/MatrixSdkExtensions.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/extensions/MatrixSdkExtensions.kt new file mode 100644 index 00000000..e2e53904 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/extensions/MatrixSdkExtensions.kt @@ -0,0 +1,35 @@ +/* + * 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 im.vector.matrix.android.api.extensions + +import im.vector.matrix.android.api.comparators.DatedObjectComparators +import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo +import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo +import java.util.* + +/* ========================================================================================== + * MXDeviceInfo + * ========================================================================================== */ + +fun MXDeviceInfo.getFingerprintHumanReadable() = fingerprint() + ?.chunked(4) + ?.joinToString(separator = " ") + + +fun List.sortByLastSeen() { + Collections.sort(this, DatedObjectComparators.descComparator) +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Failure.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Failure.kt index 94e3776e..8431239a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Failure.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Failure.kt @@ -16,6 +16,7 @@ package im.vector.matrix.android.api.failure +import im.vector.matrix.android.api.session.crypto.MXCryptoError import java.io.IOException /** @@ -31,6 +32,7 @@ sealed class Failure(cause: Throwable? = null) : Throwable(cause = cause) { data class Unknown(val throwable: Throwable? = null) : Failure(throwable) data class NetworkConnection(val ioException: IOException? = null) : Failure(ioException) data class ServerError(val error: MatrixError) : Failure(RuntimeException(error.toString())) + data class CryptoError(val error: MXCryptoError) : Failure(RuntimeException(error.toString())) abstract class FeatureFailure : Failure() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/MatrixError.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/MatrixError.kt index 92cba3b8..1e87cfc1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/MatrixError.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/MatrixError.kt @@ -54,5 +54,6 @@ data class MatrixError( const val TOO_LARGE = "M_TOO_LARGE" const val M_CONSENT_NOT_GIVEN = "M_CONSENT_NOT_GIVEN" const val RESOURCE_LIMIT_EXCEEDED = "M_RESOURCE_LIMIT_EXCEEDED" + const val WRONG_ROOM_KEYS_VERSION = "M_WRONG_ROOM_KEYS_VERSION" } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/interfaces/DatedObject.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/interfaces/DatedObject.kt new file mode 100644 index 00000000..cd735506 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/interfaces/DatedObject.kt @@ -0,0 +1,25 @@ +/* + * 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 im.vector.matrix.android.api.interfaces + +/** + * Can be implemented by any object containing a timestamp. + * This interface can be use to sort such object + */ +interface DatedObject { + val date: Long +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/listeners/ProgressListener.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/listeners/ProgressListener.kt new file mode 100644 index 00000000..854f0e9f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/listeners/ProgressListener.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.listeners + +/** + * Interface to send a progress info + */ +interface ProgressListener { + /** + * @param progress from 0 to total by contract + * @param total + */ + fun onProgress(progress: Int, total: Int) +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/listeners/StepProgressListener.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/listeners/StepProgressListener.kt new file mode 100644 index 00000000..af5b815c --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/listeners/StepProgressListener.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.listeners + +/** + * Interface to send a progress info + */ +interface StepProgressListener { + + sealed class Step { + data class ComputingKey(val progress: Int, val total: Int) : Step() + object DownloadingKey : Step() + data class ImportingKey(val progress: Int, val total: Int) : Step() + } + + /** + * @param step The current step, containing progress data if available. Else you should consider progress as indeterminate + */ + fun onStepProgress(step: Step) +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt index 86c8a86f..df85b2f0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/CryptoService.kt @@ -16,8 +16,107 @@ package im.vector.matrix.android.api.session.crypto +import android.content.Context +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.listeners.ProgressListener +import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupService +import im.vector.matrix.android.api.session.crypto.keyshare.RoomKeysRequestListener +import im.vector.matrix.android.api.session.crypto.sas.SasVerificationService +import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult +import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult +import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo +import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody +import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse + interface CryptoService { - // Not supported for the moment - fun isCryptoEnabled() = false + fun setDeviceName(deviceId: String, deviceName: String, callback: MatrixCallback) + + fun deleteDevice(deviceId: String, accountPassword: String, callback: MatrixCallback) + + fun getCryptoVersion(context: Context, longFormat: Boolean): String + + fun isCryptoEnabled(): Boolean + + fun getSasVerificationService(): SasVerificationService + + fun getKeysBackupService(): KeysBackupService + + fun isRoomBlacklistUnverifiedDevices(roomId: String, callback: MatrixCallback?) + + fun setWarnOnUnknownDevices(warn: Boolean) + + fun setDeviceVerification(verificationStatus: Int, deviceId: String, userId: String, callback: MatrixCallback) + + fun getUserDevices(userId: String): MutableList + + fun setDevicesKnown(devices: List, callback: MatrixCallback?) + + fun deviceWithIdentityKey(senderKey: String, algorithm: String): MXDeviceInfo? + + fun getMyDevice(): MXDeviceInfo + + fun getGlobalBlacklistUnverifiedDevices(callback: MatrixCallback?) + + fun setGlobalBlacklistUnverifiedDevices(block: Boolean, callback: MatrixCallback?) + + fun setRoomUnBlacklistUnverifiedDevices(roomId: String, callback: MatrixCallback) + + fun getDeviceTrackingStatus(userId: String): Int + + fun importRoomKeys(roomKeysAsArray: ByteArray, password: String, progressListener: ProgressListener?, callback: MatrixCallback) + + fun exportRoomKeys(password: String, callback: MatrixCallback) + + fun setRoomBlacklistUnverifiedDevices(roomId: String, callback: MatrixCallback) + + fun getDeviceInfo(userId: String, deviceId: String?, callback: MatrixCallback) + + // TODO move elsewhere + fun reRequestRoomKeyForEvent(event: Event) + + fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody) + + fun addRoomKeysRequestListener(listener: RoomKeysRequestListener) + + fun getDevicesList(callback: MatrixCallback) + + fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int + + /* + fun start(isInitialSync: Boolean, aCallback: MatrixCallback?) + + fun isStarted(): Boolean + + fun isStarting(): Boolean + + fun close() + + fun encryptEventContent(eventContent: Content, + eventType: String, + room: Room, + callback: MatrixCallback) + + fun onToDeviceEvent(event: Event) + + fun onSyncCompleted(syncResponse: SyncResponse, fromToken: String?, isCatchingUp: Boolean) + + fun getOlmDevice(): MXOlmDevice? + + fun checkUnknownDevices(userIds: List, callback: MatrixCallback) + + fun warnOnUnknownDevices(): Boolean + + @Throws(MXDecryptionException::class) + fun decryptEvent(event: Event, timelineId: String?): MXEventDecryptionResult? + + fun resetReplayAttackCheckInTimeline(timelineId: String) + + + @VisibleForTesting + fun ensureOlmSessionsForUsers(users: List, callback: MatrixCallback>) + */ + + fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult? } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/MXCryptoError.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/MXCryptoError.kt new file mode 100644 index 00000000..3e4f7028 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/MXCryptoError.kt @@ -0,0 +1,131 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2017 Vector Creations Ltd + * 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 im.vector.matrix.android.api.session.crypto + +import android.text.TextUtils + +/** + * Represents a crypto error response. + */ +class MXCryptoError(var code: String, + var message: String) { + + /** + * Describe the error with more details + */ + private var mDetailedErrorDescription: String? = null + + /** + * Data exception. + * Some exceptions provide some data to describe the exception + */ + var mExceptionData: Any? = null + + /** + * @return true if the current error is an olm one. + */ + val isOlmError: Boolean + get() = TextUtils.equals(OLM_ERROR_CODE, code) + + + /** + * @return the detailed error description + */ + val detailedErrorDescription: String? + get() = if (TextUtils.isEmpty(mDetailedErrorDescription)) { + message + } else mDetailedErrorDescription + + /** + * Create a crypto error + * + * @param code the error code (see XX_ERROR_CODE) + * @param shortErrorDescription the short error description + * @param detailedErrorDescription the detailed error description + */ + constructor(code: String, shortErrorDescription: String, detailedErrorDescription: String?) : this(code, shortErrorDescription) { + mDetailedErrorDescription = detailedErrorDescription + } + + /** + * Create a crypto error + * + * @param code the error code (see XX_ERROR_CODE) + * @param shortErrorDescription the short error description + * @param detailedErrorDescription the detailed error description + * @param exceptionData the exception data + */ + constructor(code: String, shortErrorDescription: String, detailedErrorDescription: String?, exceptionData: Any) : this(code, shortErrorDescription) { + mDetailedErrorDescription = detailedErrorDescription + mExceptionData = exceptionData + } + + companion object { + + /** + * Error codes + */ + const val ENCRYPTING_NOT_ENABLED_ERROR_CODE = "ENCRYPTING_NOT_ENABLED" + const val UNABLE_TO_ENCRYPT_ERROR_CODE = "UNABLE_TO_ENCRYPT" + const val UNABLE_TO_DECRYPT_ERROR_CODE = "UNABLE_TO_DECRYPT" + const val UNKNOWN_INBOUND_SESSION_ID_ERROR_CODE = "UNKNOWN_INBOUND_SESSION_ID" + const val INBOUND_SESSION_MISMATCH_ROOM_ID_ERROR_CODE = "INBOUND_SESSION_MISMATCH_ROOM_ID" + const val MISSING_FIELDS_ERROR_CODE = "MISSING_FIELDS" + const val MISSING_CIPHER_TEXT_ERROR_CODE = "MISSING_CIPHER_TEXT" + const val NOT_INCLUDE_IN_RECIPIENTS_ERROR_CODE = "NOT_INCLUDE_IN_RECIPIENTS" + const val BAD_RECIPIENT_ERROR_CODE = "BAD_RECIPIENT" + const val BAD_RECIPIENT_KEY_ERROR_CODE = "BAD_RECIPIENT_KEY" + const val FORWARDED_MESSAGE_ERROR_CODE = "FORWARDED_MESSAGE" + const val BAD_ROOM_ERROR_CODE = "BAD_ROOM" + const val BAD_ENCRYPTED_MESSAGE_ERROR_CODE = "BAD_ENCRYPTED_MESSAGE" + const val DUPLICATED_MESSAGE_INDEX_ERROR_CODE = "DUPLICATED_MESSAGE_INDEX" + const val MISSING_PROPERTY_ERROR_CODE = "MISSING_PROPERTY" + const val OLM_ERROR_CODE = "OLM_ERROR_CODE" + const val UNKNOWN_DEVICES_CODE = "UNKNOWN_DEVICES_CODE" + + /** + * short error reasons + */ + const val UNABLE_TO_DECRYPT = "Unable to decrypt" + const val UNABLE_TO_ENCRYPT = "Unable to encrypt" + + /** + * Detailed error reasons + */ + const val ENCRYPTING_NOT_ENABLED_REASON = "Encryption not enabled" + const val UNABLE_TO_ENCRYPT_REASON = "Unable to encrypt %s" + const val UNABLE_TO_DECRYPT_REASON = "Unable to decrypt %1\$s. Algorithm: %2\$s" + const val OLM_REASON = "OLM error: %1\$s" + const val DETAILLED_OLM_REASON = "Unable to decrypt %1\$s. OLM error: %2\$s" + const val UNKNOWN_INBOUND_SESSION_ID_REASON = "Unknown inbound session id" + const val INBOUND_SESSION_MISMATCH_ROOM_ID_REASON = "Mismatched room_id for inbound group session (expected %1\$s, was %2\$s)" + const val MISSING_FIELDS_REASON = "Missing fields in input" + const val MISSING_CIPHER_TEXT_REASON = "Missing ciphertext" + const val NOT_INCLUDED_IN_RECIPIENT_REASON = "Not included in recipients" + const val BAD_RECIPIENT_REASON = "Message was intended for %1\$s" + const val BAD_RECIPIENT_KEY_REASON = "Message not intended for this device" + const val FORWARDED_MESSAGE_REASON = "Message forwarded from %1\$s" + const val BAD_ROOM_REASON = "Message intended for room %1\$s" + const val BAD_ENCRYPTED_MESSAGE_REASON = "Bad Encrypted Message" + const val DUPLICATE_MESSAGE_INDEX_REASON = "Duplicate message index, possible replay attack %1\$s" + const val ERROR_MISSING_PROPERTY_REASON = "No '%1\$s' property. Cannot prevent unknown-key attack" + const val UNKNOWN_DEVICES_REASON = "This room contains unknown devices which have not been verified.\n" + "We strongly recommend you verify them before continuing." + const val NO_MORE_ALGORITHM_REASON = "Room was previously configured to use encryption, but is no longer." + " Perhaps the homeserver is hiding the configuration event." + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/keysbackup/KeysBackupService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/keysbackup/KeysBackupService.kt new file mode 100644 index 00000000..85a96b5f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/keysbackup/KeysBackupService.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.session.crypto.keysbackup + +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.listeners.ProgressListener +import im.vector.matrix.android.api.listeners.StepProgressListener +import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult +import im.vector.matrix.android.internal.crypto.keysbackup.model.KeysBackupVersionTrust +import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo +import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersion +import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult + +// TODO Add doc from implementation +interface KeysBackupService { + fun getCurrentVersion(callback: MatrixCallback) + fun createKeysBackupVersion(keysBackupCreationInfo: MegolmBackupCreationInfo, callback: MatrixCallback) + fun getTotalNumbersOfKeys(): Int + fun getTotalNumbersOfBackedUpKeys(): Int + fun backupAllGroupSessions(progressListener: ProgressListener?, callback: MatrixCallback?) + fun getKeysBackupTrust(keysBackupVersion: KeysVersionResult, callback: MatrixCallback) + fun getBackupProgress(progressListener: ProgressListener) + fun maybeBackupKeys() + fun getVersion(version: String, callback: MatrixCallback) + fun forceUsingLastVersion(callback: MatrixCallback) + fun checkAndStartKeysBackup() + fun addListener(listener: KeysBackupStateListener) + fun removeListener(listener: KeysBackupStateListener) + + fun prepareKeysBackupVersion(password: String?, progressListener: ProgressListener?, callback: MatrixCallback) + fun deleteBackup(version: String, callback: MatrixCallback?) + fun canRestoreKeys(): Boolean + fun trustKeysBackupVersion(keysBackupVersion: KeysVersionResult, trust: Boolean, callback: MatrixCallback) + fun trustKeysBackupVersionWithRecoveryKey(keysBackupVersion: KeysVersionResult, recoveryKey: String, callback: MatrixCallback) + fun trustKeysBackupVersionWithPassphrase(keysBackupVersion: KeysVersionResult, password: String, callback: MatrixCallback) + fun restoreKeysWithRecoveryKey(keysVersionResult: KeysVersionResult, recoveryKey: String, roomId: String?, sessionId: String?, stepProgressListener: StepProgressListener?, callback: MatrixCallback) + fun restoreKeyBackupWithPassword(keysBackupVersion: KeysVersionResult, password: String, roomId: String?, sessionId: String?, stepProgressListener: StepProgressListener?, callback: MatrixCallback) + + val mKeysBackupVersion: KeysVersionResult? + val currentBackupVersion: String? + val isEnabled: Boolean + val isStucked: Boolean + val state: KeysBackupState + + interface KeysBackupStateListener { + fun onStateChange(newState: KeysBackupState) + } + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/keysbackup/KeysBackupState.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/keysbackup/KeysBackupState.kt new file mode 100644 index 00000000..a1bd29e7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/keysbackup/KeysBackupState.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.session.crypto.keysbackup + +/** + * E2e keys backup states. + * + *
+ *                               |
+ *                               V        deleteKeyBackupVersion (on current backup)
+ *  +---------------------->  UNKNOWN  <-------------
+ *  |                            |
+ *  |                            | checkAndStartKeysBackup (at startup or on new verified device or a new detected backup)
+ *  |                            V
+ *  |                     CHECKING BACKUP
+ *  |                            |
+ *  |    Network error           |
+ *  +<----------+----------------+-------> DISABLED <----------------------+
+ *  |           |                |            |                            |
+ *  |           |                |            | createKeysBackupVersion    |
+ *  |           V                |            V                            |
+ *  +<---  WRONG VERSION         |         ENABLING                        |
+ *      |       ^                |            |                            |
+ *      |       |                V       ok   |     error                  |
+ *      |       |     +------> READY <--------+----------------------------+
+ *      V       |     |          |
+ * NOT TRUSTED  |     |          | 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, + // There is a backup available on the homeserver but it is not trusted. + // It is not trusted because the signature is invalid or the device that created it is not verified + // Use [KeysBackup.getKeysBackupTrust()] to get trust details. + // Consequently, the backup from this device is not enabled. + NotTrusted, + // 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, + // e2e keys are going to be sent to the homeserver + WillBackUp, + // e2e keys are being sent to the homeserver + BackingUp +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/keyshare/RoomKeysRequestListener.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/keyshare/RoomKeysRequestListener.kt new file mode 100644 index 00000000..5bce27e1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/keyshare/RoomKeysRequestListener.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 im.vector.matrix.android.api.session.crypto.keyshare + +import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest +import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequestCancellation + +/** + * Room keys events listener + */ +interface RoomKeysRequestListener { + /** + * An room key request has been received. + * + * @param request the request + */ + fun onRoomKeyRequest(request: IncomingRoomKeyRequest) + + /** + * A room key request cancellation has been received. + * + * @param request the cancellation request + */ + fun onRoomKeyRequestCancellation(request: IncomingRoomKeyRequestCancellation) +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/CancelCode.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/CancelCode.kt new file mode 100644 index 00000000..d999d06f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/CancelCode.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.api.session.crypto.sas + +enum class CancelCode(val value: String, val humanReadable: String) { + User("m.user", "the user cancelled the verification"), + Timeout("m.timeout", "the verification process timed out"), + UnknownTransaction("m.unknown_transaction", "the device does not know about that transaction"), + UnknownMethod("m.unknown_method", "the device can’t agree on a key agreement, hash, MAC, or SAS method"), + MismatchedCommitment("m.mismatched_commitment", "the hash commitment did not match"), + MismatchedSas("m.mismatched_sas", "the SAS did not match"), + UnexpectedMessage("m.unexpected_message", "the device received an unexpected message"), + InvalidMessage("m.invalid_message", "an invalid message was received"), + MismatchedKeys("m.key_mismatch", "Key mismatch"), + UserMismatchError("m.user_error", "User mismatch") +} + +fun safeValueOf(code: String?): CancelCode { + return CancelCode.values().firstOrNull { code == it.value } ?: CancelCode.User +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/EmojiRepresentation.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/EmojiRepresentation.kt new file mode 100644 index 00000000..0bcb96a9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/EmojiRepresentation.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.session.crypto.sas + +import androidx.annotation.StringRes + +data class EmojiRepresentation(val emoji: String, + @StringRes val nameResId: Int) \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/IncomingSasVerificationTransaction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/IncomingSasVerificationTransaction.kt new file mode 100644 index 00000000..791c63dc --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/IncomingSasVerificationTransaction.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.session.crypto.sas + +interface IncomingSasVerificationTransaction { + val uxState: UxState + + fun performAccept() + + enum class UxState { + UNKNOWN, + SHOW_ACCEPT, + WAIT_FOR_KEY_AGREEMENT, + SHOW_SAS, + WAIT_FOR_VERIFICATION, + VERIFIED, + CANCELLED_BY_ME, + CANCELLED_BY_OTHER + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/Mode.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/Mode.kt new file mode 100644 index 00000000..da72e98b --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/Mode.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.session.crypto.sas + +object SasMode { + const val DECIMAL = "decimal" + const val EMOJI = "emoji" +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/OutgoingSasVerificationRequest.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/OutgoingSasVerificationRequest.kt new file mode 100644 index 00000000..6aeed555 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/OutgoingSasVerificationRequest.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.session.crypto.sas + +interface OutgoingSasVerificationRequest { + val uxState: UxState + + enum class UxState { + UNKNOWN, + WAIT_FOR_START, + WAIT_FOR_KEY_AGREEMENT, + SHOW_SAS, + WAIT_FOR_VERIFICATION, + VERIFIED, + CANCELLED_BY_ME, + CANCELLED_BY_OTHER + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/SasVerificationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/SasVerificationService.kt new file mode 100644 index 00000000..a11c69cd --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/SasVerificationService.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.session.crypto.sas + +interface SasVerificationService { + fun addListener(listener: SasVerificationListener) + + fun removeListener(listener: SasVerificationListener) + + fun markedLocallyAsManuallyVerified(userId: String, deviceID: String) + + fun getExistingTransaction(otherUser: String, tid: String): SasVerificationTransaction? + + fun beginKeyVerificationSAS(userId: String, deviceID: String): String? + + fun beginKeyVerification(method: String, userId: String, deviceID: String): String? + + // fun transactionUpdated(tx: SasVerificationTransaction) + + interface SasVerificationListener { + fun transactionCreated(tx: SasVerificationTransaction) + fun transactionUpdated(tx: SasVerificationTransaction) + fun markedAsManuallyVerified(userId: String, deviceId: String) + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/SasVerificationTransaction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/SasVerificationTransaction.kt new file mode 100644 index 00000000..dc489cf6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/SasVerificationTransaction.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.session.crypto.sas + +interface SasVerificationTransaction { + val state: SasVerificationTxState + + val cancelledReason: CancelCode? + + val transactionId: String + + val otherUserId: String + + var otherDeviceId: String? + + val isIncoming: Boolean + + fun supportsEmoji(): Boolean + + fun supportsDecimal(): Boolean + + fun getEmojiCodeRepresentation(): List + + fun getDecimalCodeRepresentation(): String + + /** + * User wants to cancel the transaction + */ + fun cancel() + + /** + * To be called by the client when the user has verified that + * both short codes do match + */ + fun userHasVerifiedShortCode() +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/SasVerificationTxState.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/SasVerificationTxState.kt new file mode 100644 index 00000000..60ce8f0c --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/SasVerificationTxState.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.session.crypto.sas + +enum class SasVerificationTxState { + None, + // I have started a verification request + SendingStart, + Started, + // Other user/device sent me a request + OnStarted, + // I have accepted a request started by the other user/device + SendingAccept, + Accepted, + // My request has been accepted by the other user/device + OnAccepted, + // I have sent my public key + SendingKey, + KeySent, + // The other user/device has sent me his public key + OnKeyReceived, + // Short code is ready to be displayed + ShortCodeReady, + // I have compared the code and manually said that they match + ShortCodeAccepted, + + SendingMac, + MacSent, + Verifying, + Verified, + + //Global: The verification has been cancelled (by me or other), see cancelReason for details + Cancelled, + OnCancelled +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt index 7c6ca39b..cb04fa45 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt @@ -16,13 +16,20 @@ package im.vector.matrix.android.api.session.events.model +import android.text.TextUtils import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import com.squareup.moshi.Types +import im.vector.matrix.android.api.session.crypto.MXCryptoError +import im.vector.matrix.android.api.util.JsonDict +import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult import im.vector.matrix.android.internal.di.MoshiProvider +import timber.log.Timber import java.lang.reflect.ParameterizedType +import java.util.ArrayList +import java.util.HashMap -typealias Content = Map +typealias Content = JsonDict /** * This methods is a facility method to map a json content to a model. @@ -77,4 +84,148 @@ data class Event( companion object { internal val CONTENT_TYPE: ParameterizedType = Types.newParameterizedType(Map::class.java, String::class.java, Any::class.java) } + + //============================================================================================================== + // Crypto + //============================================================================================================== + + /** + * For encrypted events, the plaintext payload for the event. + * This is a small MXEvent instance with typically value for `type` and 'content' fields. + */ + @Transient + private var mClearEvent: Event? = null + + /** + * Curve25519 key which we believe belongs to the sender of the event. + * See `senderKey` property. + */ + @Transient + private var mSenderCurve25519Key: String? = null + + /** + * Ed25519 key which the sender of this event (for olm) or the creator of the megolm session (for megolm) claims to own. + * See `claimedEd25519Key` property. + */ + @Transient + private var mClaimedEd25519Key: String? = null + + /** + * Curve25519 keys of devices involved in telling us about the senderCurve25519Key and claimedEd25519Key. + * See `forwardingCurve25519KeyChain` property. + */ + @Transient + private var mForwardingCurve25519KeyChain: List = ArrayList() + + /** + * Decryption error + */ + @Transient + private var mCryptoError: MXCryptoError? = null + + /** + * @return true if this event is encrypted. + */ + fun isEncrypted(): Boolean { + return TextUtils.equals(type, EventType.ENCRYPTED) + } + + /** + * Update the clear data on this event. + * This is used after decrypting an event; it should not be used by applications. + * It fires kMXEventDidDecryptNotification. + * + * @param decryptionResult the decryption result, including the plaintext and some key info. + */ + fun setClearData(decryptionResult: MXEventDecryptionResult?) { + mClearEvent = null + + if (null != decryptionResult) { + if (null != decryptionResult!!.mClearEvent) { + mClearEvent = decryptionResult!!.mClearEvent + } + + if (null != mClearEvent) { + mClearEvent!!.mSenderCurve25519Key = decryptionResult!!.mSenderCurve25519Key + mClearEvent!!.mClaimedEd25519Key = decryptionResult!!.mClaimedEd25519Key + + if (null != decryptionResult!!.mForwardingCurve25519KeyChain) { + mClearEvent!!.mForwardingCurve25519KeyChain = decryptionResult!!.mForwardingCurve25519KeyChain + } else { + mClearEvent!!.mForwardingCurve25519KeyChain = ArrayList() + } + + try { + // Add "m.relates_to" data from e2e event to the unencrypted event + // TODO + //if (getWireContent().getAsJsonObject().has("m.relates_to")) { + // mClearEvent!!.getContentAsJsonObject() + // .add("m.relates_to", getWireContent().getAsJsonObject().get("m.relates_to")) + //} + } catch (e: Exception) { + Timber.e(e,"Unable to restore 'm.relates_to' the clear event") + } + + } + + mCryptoError = null + } + } + + /** + * @return The curve25519 key that sent this event. + */ + fun getSenderKey(): String? { + return if (null != mClearEvent) { + mClearEvent!!.mSenderCurve25519Key + } else { + mSenderCurve25519Key + } + } + + /** + * @return The additional keys the sender of this encrypted event claims to possess. + */ + fun getKeysClaimed(): Map { + val res = HashMap() + + val claimedEd25519Key = if (null != mClearEvent) mClearEvent!!.mClaimedEd25519Key else mClaimedEd25519Key + + if (null != claimedEd25519Key) { + res["ed25519"] = claimedEd25519Key + } + + return res + } + + /** + * @return the event type + */ + fun getClearType(): String { + return if (null != mClearEvent) { + mClearEvent!!.type + } else { + type + } + } + + /** + * @return the linked crypto error + */ + fun getCryptoError(): MXCryptoError? { + return mCryptoError + } + + /** + * Update the linked crypto error + * + * @param error the new crypto error. + */ + fun setCryptoError(error: MXCryptoError?) { + mCryptoError = error + if (null != error) { + mClearEvent = null + } + } + } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt index 4315fd59..b867063c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EventType.kt @@ -36,8 +36,6 @@ object EventType { const val FULLY_READ = "m.fully_read" const val PLUMBING = "m.room.plumbing" const val BOT_OPTIONS = "m.room.bot.options" - const val KEY_REQUEST = "m.room_key_request" - const val FORWARDED_ROOM_KEY = "m.forwarded_room_key" const val PREVIEW_URLS = "org.matrix.room.preview_urls" // State Events @@ -65,6 +63,17 @@ object EventType { const val CALL_ANSWER = "m.call.answer" const val CALL_HANGUP = "m.call.hangup" + // Key share events + const val ROOM_KEY_REQUEST = "m.room_key_request" + const val FORWARDED_ROOM_KEY = "m.forwarded_room_key" + + // Interactive key verification + const val KEY_VERIFICATION_START = "m.key.verification.start" + const val KEY_VERIFICATION_ACCEPT = "m.key.verification.accept" + const val KEY_VERIFICATION_KEY = "m.key.verification.key" + const val KEY_VERIFICATION_MAC = "m.key.verification.mac" + const val KEY_VERIFICATION_CANCEL = "m.key.verification.cancel" + private val STATE_EVENTS = listOf( STATE_ROOM_NAME, STATE_ROOM_TOPIC, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt index c2e69d33..9cb4b846 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt @@ -17,6 +17,7 @@ package im.vector.matrix.android.api.session.room import androidx.lifecycle.LiveData +import im.vector.matrix.android.api.session.room.crypto.RoomCryptoService import im.vector.matrix.android.api.session.room.members.RoomMembersService import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.read.ReadService @@ -27,7 +28,12 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineService /** * This interface defines methods to interact within a room. */ -interface Room : TimelineService, SendService, ReadService, RoomMembersService, StateService { +interface Room : TimelineService, + SendService, + ReadService, + RoomMembersService, + StateService, + RoomCryptoService { /** * The roomId of this room diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/crypto/RoomCryptoService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/crypto/RoomCryptoService.kt new file mode 100644 index 00000000..52a139cf --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/crypto/RoomCryptoService.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.session.room.crypto + +import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM + +interface RoomCryptoService { + + // TODO + fun isEncrypted(): Boolean = false + + // TODO + fun encryptionAlgorithm(): String? = MXCRYPTO_ALGORITHM_MEGOLM + + // TODO + fun shouldEncryptForInvitedMembers(): Boolean = false +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/RoomMembersService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/RoomMembersService.kt index 930afd7a..e04cebea 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/RoomMembersService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/members/RoomMembersService.kt @@ -54,4 +54,18 @@ interface RoomMembersService { */ fun invite(userId: String, callback: MatrixCallback) + /** + * Return all the roomMembers ids which are joined or invited to the room + * + * @return a roomMember id list of joined or invited members. + */ + fun getActiveRoomMemberIds(): List + + /** + * Return all the roomMembers ids which are joined to the room + * + * @return a roomMember id list of joined members. + */ + fun getJoinedRoomMemberIds(): List + } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/Types.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/Types.kt new file mode 100644 index 00000000..601b3cb0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/util/Types.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.util + +typealias JsonDict = Map diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoAsyncHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoAsyncHelper.kt new file mode 100644 index 00000000..42e97d30 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoAsyncHelper.kt @@ -0,0 +1,64 @@ +/* + * + * * Copyright 2019 New Vector Ltd + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package im.vector.matrix.android.internal.crypto + +import android.os.Handler +import android.os.HandlerThread +import android.os.Looper + +private const val THREAD_ENCRYPT_NAME = "Crypto_Encrypt_Thread" +private const val THREAD_DECRYPT_NAME = "Crypto_Decrypt_Thread" + +internal object CryptoAsyncHelper { + + private var uiHandler: Handler? = null + private var decryptBackgroundHandler: Handler? = null + private var encryptBackgroundHandler: Handler? = null + + fun getUiHandler(): Handler { + return uiHandler + ?: Handler(Looper.getMainLooper()) + .also { uiHandler = it } + } + + fun getDecryptBackgroundHandler(): Handler { + return decryptBackgroundHandler + ?: createDecryptBackgroundHandler() + .also { decryptBackgroundHandler = it } + } + + fun getEncryptBackgroundHandler(): Handler { + return encryptBackgroundHandler + ?: createEncryptBackgroundHandler() + .also { encryptBackgroundHandler = it } + } + + private fun createDecryptBackgroundHandler(): Handler { + val handlerThread = HandlerThread(THREAD_DECRYPT_NAME) + handlerThread.start() + return Handler(handlerThread.looper) + } + + private fun createEncryptBackgroundHandler(): Handler { + val handlerThread = HandlerThread(THREAD_ENCRYPT_NAME) + handlerThread.start() + return Handler(handlerThread.looper) + } + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoConstants.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoConstants.kt new file mode 100644 index 00000000..c045019c --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoConstants.kt @@ -0,0 +1,32 @@ +/* + * 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 im.vector.matrix.android.internal.crypto + +/** + * Matrix algorithm value for olm. + */ +const val MXCRYPTO_ALGORITHM_OLM = "m.olm.v1.curve25519-aes-sha2" + +/** + * Matrix algorithm value for megolm. + */ +const val MXCRYPTO_ALGORITHM_MEGOLM = "m.megolm.v1.aes-sha2" + +/** + * Matrix algorithm value for megolm keys backup. + */ +const val MXCRYPTO_ALGORITHM_MEGOLM_BACKUP = "m.megolm_backup.v1.curve25519-aes-sha2" diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoManager.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoManager.kt new file mode 100755 index 00000000..ca437da0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoManager.kt @@ -0,0 +1,2102 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2017 Vector Creations Ltd + * 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 im.vector.matrix.android.internal.crypto + +import android.content.Context +import android.os.Handler +import android.os.HandlerThread +import android.os.Looper +import android.text.TextUtils +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.auth.data.Credentials +import im.vector.matrix.android.api.failure.Failure +import im.vector.matrix.android.api.listeners.ProgressListener +import im.vector.matrix.android.api.session.crypto.CryptoService +import im.vector.matrix.android.api.session.crypto.MXCryptoError +import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupService +import im.vector.matrix.android.api.session.crypto.keyshare.RoomKeysRequestListener +import im.vector.matrix.android.api.session.crypto.sas.SasVerificationService +import im.vector.matrix.android.api.session.events.model.Content +import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.room.Room +import im.vector.matrix.android.api.session.room.RoomService +import im.vector.matrix.android.api.session.room.model.Membership +import im.vector.matrix.android.internal.crypto.algorithms.IMXEncrypting +import im.vector.matrix.android.internal.crypto.keysbackup.KeysBackup +import im.vector.matrix.android.internal.crypto.model.* +import im.vector.matrix.android.internal.crypto.model.event.RoomKeyContent +import im.vector.matrix.android.internal.crypto.model.rest.* +import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore +import im.vector.matrix.android.internal.crypto.tasks.* +import im.vector.matrix.android.internal.crypto.verification.DefaultSasVerificationService +import im.vector.matrix.android.internal.di.MoshiProvider +import im.vector.matrix.android.internal.session.sync.model.SyncResponse +import im.vector.matrix.android.internal.task.TaskExecutor +import im.vector.matrix.android.internal.task.configureWith +import im.vector.matrix.android.internal.util.convertToUTF8 +import org.matrix.olm.OlmAccount +import org.matrix.olm.OlmManager +import timber.log.Timber +import java.util.* +import java.util.concurrent.CountDownLatch + +/** + * A `MXCrypto` class instance manages the end-to-end crypto for a MXSession instance. + * + * + * Messages posted by the user are automatically redirected to MXCrypto in order to be encrypted + * before sending. + * In the other hand, received events goes through MXCrypto for decrypting. + * MXCrypto maintains all necessary keys and their sharing with other devices required for the crypto. + * Specially, it tracks all room membership changes events in order to do keys updates. + */ +internal class CryptoManager( + // The credentials, + private val mCredentials: Credentials, + // the crypto store + private val mCryptoStore: IMXCryptoStore, + // Olm device + private val mOlmDevice: MXOlmDevice, + cryptoConfig: MXCryptoConfig?, + // Device list manager + private val deviceListManager: DeviceListManager, + // The key backup service. + private val mKeysBackup: KeysBackup, + // + private val roomDecryptorProvider: RoomDecryptorProvider, + // The SAS verification service. + private val mSasVerificationService: DefaultSasVerificationService, + // + private val mIncomingRoomKeyRequestManager: IncomingRoomKeyRequestManager, + // + private val mOutgoingRoomKeyRequestManager: MXOutgoingRoomKeyRequestManager, + // Room service + private val mRoomService: RoomService, + // Olm Manager + private val mOlmManager: OlmManager, + // Tasks + private val mClaimOneTimeKeysForUsersDeviceTask: ClaimOneTimeKeysForUsersDeviceTask, + private val mDeleteDeviceTask: DeleteDeviceTask, + private val mGetDevicesTask: GetDevicesTask, + private val mGetKeyChangesTask: GetKeyChangesTask, + private val mSendToDeviceTask: SendToDeviceTask, + private val mSetDeviceNameTask: SetDeviceNameTask, + private val mUploadKeysTask: UploadKeysTask, + // TaskExecutor + private val mTaskExecutor: TaskExecutor +) : KeysBackup.KeysBackupCryptoListener, + DefaultSasVerificationService.SasCryptoListener, + DeviceListManager.DeviceListCryptoListener, + CryptoService { + + // MXEncrypting instance for each room. + private val mRoomEncryptors: MutableMap + + // Our device keys + /** + * @return my device info + */ + private val myDevice: MXDeviceInfo + + private var mLastPublishedOneTimeKeys: Map>? = null + + // the encryption is starting + private var mIsStarting: Boolean = false + + // tell if the crypto is started + private var mIsStarted: Boolean = false + + // the crypto background threads + private var mEncryptingHandlerThread: HandlerThread? = null + private var mEncryptingHandler: Handler? = null + + private var mDecryptingHandlerThread: HandlerThread? = null + private var mDecryptingHandler: Handler? = null + + // the UI thread + private val mUIHandler: Handler + + private var mOneTimeKeyCount: Int? = null + + // TODO + //private val mNetworkListener = object : IMXNetworkEventListener { + // override fun onNetworkConnectionUpdate(isConnected: Boolean) { + // if (isConnected && !isStarted()) { + // Timber.d("Start MXCrypto because a network connection has been retrieved ") + // start(false, null) + // } + // } + //} + + fun onLiveEvent(roomId: String, event: Event) { + if (event.type == EventType.ENCRYPTION) { + onCryptoEvent(roomId, event) + } else if (event.type == EventType.STATE_ROOM_MEMBER) { + onRoomMembershipEvent(roomId, event) + } + } + + // initialization callbacks + private val mInitializationCallbacks = ArrayList>() + + // Warn the user if some new devices are detected while encrypting a message. + private var mWarnOnUnknownDevices = true + + // tell if there is a OTK check in progress + private var mOneTimeKeyCheckInProgress = false + + // last OTK check timestamp + private var mLastOneTimeKeyCheck: Long = 0 + + // Set of parameters used to configure/customize the end-to-end crypto. + private var mCryptoConfig: MXCryptoConfig? = null + + /** + * @return the encrypting thread handler + */ + // mEncryptingHandlerThread was not yet ready + // fail to get the handler + // might happen if the thread is not yet ready + val encryptingThreadHandler: Handler + get() { + if (null == mEncryptingHandler) { + mEncryptingHandler = Handler(mEncryptingHandlerThread!!.looper) + } + return if (null == mEncryptingHandler) { + mUIHandler + } else mEncryptingHandler!! + } + + /** + * Tells whether the client should ever send encrypted messages to unverified devices. + * The default value is false. + * This function must be called in the getEncryptingThreadHandler() thread. + * + * @return true to unilaterally blacklist all unverified devices. + */ + val globalBlacklistUnverifiedDevices: Boolean + get() = mCryptoStore.getGlobalBlacklistUnverifiedDevices() + + init { + if (null != cryptoConfig) { + mCryptoConfig = cryptoConfig + } else { + // Consider the default configuration value + mCryptoConfig = MXCryptoConfig() + } + + mRoomEncryptors = HashMap() + + var deviceId = mCredentials.deviceId + // deviceId should always be defined + val refreshDevicesList = !TextUtils.isEmpty(deviceId) + + if (TextUtils.isEmpty(deviceId)) { + // use the stored one + deviceId = this.mCryptoStore.getDeviceId() + + // Should not happen anymore + TODO() + //mSession.setDeviceId(deviceId) + } + + if (TextUtils.isEmpty(deviceId)) { + deviceId = UUID.randomUUID().toString() + // Should not happen anymore + TODO() + //mSession.setDeviceId(deviceId) + Timber.d("Warning: No device id in MXCredentials. An id was created. Think of storing it") + this.mCryptoStore.storeDeviceId(deviceId) + } + + myDevice = MXDeviceInfo(deviceId!!, mCredentials.userId) + + val keys = HashMap() + + if (!TextUtils.isEmpty(mOlmDevice.deviceEd25519Key)) { + keys["ed25519:" + mCredentials.deviceId] = mOlmDevice.deviceEd25519Key!! + } + + if (!TextUtils.isEmpty(mOlmDevice.deviceCurve25519Key)) { + keys["curve25519:" + mCredentials.deviceId] = mOlmDevice.deviceCurve25519Key!! + } + + myDevice.keys = keys + + myDevice.algorithms = MXCryptoAlgorithms.supportedAlgorithms() + myDevice.mVerified = MXDeviceInfo.DEVICE_VERIFICATION_VERIFIED + + // Add our own deviceinfo to the store + val endToEndDevicesForUser = this.mCryptoStore.getUserDevices(mCredentials.userId) + + val myDevices: MutableMap + + if (null != endToEndDevicesForUser) { + myDevices = HashMap(endToEndDevicesForUser) + } else { + myDevices = HashMap() + } + + myDevices[myDevice.deviceId] = myDevice + + this.mCryptoStore.storeUserDevices(mCredentials.userId, myDevices) + + mEncryptingHandlerThread = HandlerThread("MXCrypto_encrypting_" + mCredentials.userId, Thread.MIN_PRIORITY) + mEncryptingHandlerThread!!.start() + + mDecryptingHandlerThread = HandlerThread("MXCrypto_decrypting_" + mCredentials.userId, Thread.MIN_PRIORITY) + mDecryptingHandlerThread!!.start() + + mUIHandler = Handler(Looper.getMainLooper()) + + if (refreshDevicesList) { + // ensure to have the up-to-date devices list + // got some issues when upgrading from Riot < 0.6.4 + deviceListManager.handleDeviceListsChanges(listOf(mCredentials.userId), null) + } + + mOutgoingRoomKeyRequestManager.setWorkingHandler(encryptingThreadHandler) + mIncomingRoomKeyRequestManager.setEncryptingThreadHandler(encryptingThreadHandler) + + mKeysBackup.setCryptoInternalListener(this) + mSasVerificationService.setCryptoInternalListener(this) + deviceListManager.setCryptoInternalListener(this) + } + + override fun setDeviceName(deviceId: String, deviceName: String, callback: MatrixCallback) { + mSetDeviceNameTask + .configureWith(SetDeviceNameTask.Params(deviceId, deviceName)) + .dispatchTo(callback) + .executeBy(mTaskExecutor) + } + + override fun deleteDevice(deviceId: String, accountPassword: String, callback: MatrixCallback) { + mDeleteDeviceTask + .configureWith(DeleteDeviceTask.Params(deviceId, accountPassword)) + .dispatchTo(callback) + .executeBy(mTaskExecutor) + } + + override fun getCryptoVersion(context: Context, longFormat: Boolean): String { + return if (longFormat) mOlmManager.getDetailedVersion(context) else mOlmManager.version + } + + override fun getMyDevice(): MXDeviceInfo { + return myDevice + } + + override fun getDevicesList(callback: MatrixCallback) { + mGetDevicesTask + .configureWith(Unit) + .dispatchTo(callback) + .executeBy(mTaskExecutor) + } + + /** + * @return the decrypting thread handler + */ + fun getDecryptingThreadHandler(): Handler { + // mDecryptingHandlerThread was not yet ready + if (null == mDecryptingHandler) { + mDecryptingHandler = Handler(mDecryptingHandlerThread!!.looper) + } + + // fail to get the handler + // might happen if the thread is not yet ready + return if (null == mDecryptingHandler) { + mUIHandler + } else mDecryptingHandler!! + } + + override fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int { + return mCryptoStore.inboundGroupSessionsCount(onlyBackedUp) + } + + /** + * @return true if this instance has been released + */ + override fun hasBeenReleased(): Boolean { + return null == mOlmDevice + } + + /** + * Provides the tracking status + * + * @param userId the user id + * @return the tracking status + */ + override fun getDeviceTrackingStatus(userId: String): Int { + return mCryptoStore.getDeviceTrackingStatus(userId, DeviceListManager.TRACKING_STATUS_NOT_TRACKED) + } + + /** + * Tell if the MXCrypto is started + * + * @return true if the crypto is started + */ + fun isStarted(): Boolean { + return mIsStarted + } + + /** + * Tells if the MXCrypto is starting. + * + * @return true if the crypto is starting + */ + fun isStarting(): Boolean { + return mIsStarting + } + + /** + * Start the crypto module. + * Device keys will be uploaded, then one time keys if there are not enough on the homeserver + * and, then, if this is the first time, this new device will be announced to all other users + * devices. + * + * @param isInitialSync true if it starts from an initial sync + * @param aCallback the asynchronous callback + */ + fun start(isInitialSync: Boolean, aCallback: MatrixCallback?) { + synchronized(mInitializationCallbacks) { + if (null != aCallback && mInitializationCallbacks.indexOf(aCallback) < 0) { + mInitializationCallbacks.add(aCallback) + } + } + + if (mIsStarting) { + return + } + + // do not start if there is not network connection + // TODO + //if (null != mNetworkConnectivityReceiver && !mNetworkConnectivityReceiver!!.isConnected()) { + // // wait that a valid network connection is retrieved + // mNetworkConnectivityReceiver!!.removeEventListener(mNetworkListener) + // mNetworkConnectivityReceiver!!.addEventListener(mNetworkListener) + // return + //} + + mIsStarting = true + + // Open the store + mCryptoStore.open() + + encryptingThreadHandler.post { + uploadDeviceKeys(object : MatrixCallback { + private fun onError() { + mUIHandler.postDelayed({ + if (!isStarted()) { + mIsStarting = false + start(isInitialSync, null) + } + }, 1000) + } + + override fun onSuccess(data: KeysUploadResponse) { + encryptingThreadHandler.post { + if (!hasBeenReleased()) { + Timber.d("###########################################################") + Timber.d("uploadDeviceKeys done for " + mCredentials.userId) + Timber.d(" - device id : " + mCredentials.deviceId) + Timber.d(" - ed25519 : " + mOlmDevice.deviceEd25519Key) + Timber.d(" - curve25519 : " + mOlmDevice.deviceCurve25519Key) + Timber.d(" - oneTimeKeys: " + mLastPublishedOneTimeKeys) + Timber.d("") + + encryptingThreadHandler.post { + maybeUploadOneTimeKeys(object : MatrixCallback { + override fun onSuccess(data: Unit) { + encryptingThreadHandler.post { + // TODO + //if (null != mNetworkConnectivityReceiver) { + // mNetworkConnectivityReceiver!!.removeEventListener(mNetworkListener) + //} + + mIsStarting = false + mIsStarted = true + + mOutgoingRoomKeyRequestManager.start() + + mKeysBackup.checkAndStartKeysBackup() + + synchronized(mInitializationCallbacks) { + for (callback in mInitializationCallbacks) { + mUIHandler.post { callback.onSuccess(Unit) } + } + mInitializationCallbacks.clear() + } + + if (isInitialSync) { + encryptingThreadHandler.post { + // refresh the devices list for each known room members + deviceListManager.invalidateAllDeviceLists() + deviceListManager.refreshOutdatedDeviceLists() + } + } else { + encryptingThreadHandler.post { + mIncomingRoomKeyRequestManager.processReceivedRoomKeyRequests() + + } + } + } + } + + override fun onFailure(failure: Throwable) { + Timber.e(failure, "## start failed") + onError() + } + }) + } + } + } + } + + override fun onFailure(failure: Throwable) { + Timber.e(failure, "## start failed") + onError() + } + }) + } + } + + /** + * Close the crypto + */ + fun close() { + if (null != mEncryptingHandlerThread) { + encryptingThreadHandler.post { + mOlmDevice.release() + + // Do not reset My Device + // mMyDevice = null; + + mCryptoStore.close() + // Do not reset Crypto store + // mCryptoStore = null; + + if (null != mEncryptingHandlerThread) { + mEncryptingHandlerThread!!.quit() + mEncryptingHandlerThread = null + } + + mOutgoingRoomKeyRequestManager.stop() + } + + getDecryptingThreadHandler().post { + if (null != mDecryptingHandlerThread) { + mDecryptingHandlerThread!!.quit() + mDecryptingHandlerThread = null + } + } + } + } + + override fun isCryptoEnabled(): Boolean { + // TODO Check that this test is correct + return mOlmDevice != null + } + + /** + * @return the Keys backup Service + */ + override fun getKeysBackupService(): KeysBackupService { + return mKeysBackup + } + + /** + * @return the SasVerificationService + */ + override fun getSasVerificationService(): SasVerificationService { + return mSasVerificationService + } + + /** + * A sync response has been received + * + * @param syncResponse the syncResponse + * @param fromToken the start sync token + * @param isCatchingUp true if there is a catch-up in progress. + */ + fun onSyncCompleted(syncResponse: SyncResponse, fromToken: String?, isCatchingUp: Boolean) { + encryptingThreadHandler.post { + if (null != syncResponse.deviceLists) { + deviceListManager.handleDeviceListsChanges(syncResponse.deviceLists.changed, syncResponse.deviceLists.left) + } + + if (null != syncResponse.deviceOneTimeKeysCount) { + val currentCount = syncResponse.deviceOneTimeKeysCount.signedCurve25519 ?: 0 + updateOneTimeKeyCount(currentCount) + } + + if (isStarted()) { + // Make sure we process to-device messages before generating new one-time-keys #2782 + deviceListManager.refreshOutdatedDeviceLists() + } + + if (!isCatchingUp && isStarted()) { + maybeUploadOneTimeKeys() + + mIncomingRoomKeyRequestManager.processReceivedRoomKeyRequests() + } + } + } + + /** + * Get the stored device keys for a user. + * + * @param userId the user to list keys for. + * @param callback the asynchronous callback + */ + fun getUserDevices(userId: String, callback: MatrixCallback>?) { + encryptingThreadHandler.post { + val list = getUserDevices(userId) + + if (null != callback) { + mUIHandler.post { callback.onSuccess(list) } + } + } + } + + /** + * Stores the current one_time_key count which will be handled later (in a call of + * _onSyncCompleted). The count is e.g. coming from a /sync response. + * + * @param currentCount the new count + */ + private fun updateOneTimeKeyCount(currentCount: Int) { + mOneTimeKeyCount = currentCount + } + + /** + * Find a device by curve25519 identity key + * + * @param senderKey the curve25519 key to match. + * @param algorithm the encryption algorithm. + * @return the device info, or null if not found / unsupported algorithm / crypto released + */ + override fun deviceWithIdentityKey(senderKey: String, algorithm: String): MXDeviceInfo? { + return if (!hasBeenReleased()) { + if (!TextUtils.equals(algorithm, MXCRYPTO_ALGORITHM_MEGOLM) && !TextUtils.equals(algorithm, MXCRYPTO_ALGORITHM_OLM)) { + // We only deal in olm keys + null + } else mCryptoStore.deviceWithIdentityKey(senderKey) + + // Find in the crypto store + } else null + + // The store is released + } + + /** + * Provides the device information for a device id and a user Id + * + * @param userId the user id + * @param deviceId the device id + * @param callback the asynchronous callback + */ + override fun getDeviceInfo(userId: String, deviceId: String?, callback: MatrixCallback) { + getDecryptingThreadHandler().post { + val di: MXDeviceInfo? + + if (!TextUtils.isEmpty(userId) && !TextUtils.isEmpty(deviceId)) { + di = mCryptoStore.getUserDevice(deviceId!!, userId) + } else { + di = null + } + + mUIHandler.post { callback.onSuccess(di) } + } + } + + /** + * Set the devices as known + * + * @param devices the devices. Note that the mVerified member of the devices in this list will not be updated by this method. + * @param callback the asynchronous callback + */ + override fun setDevicesKnown(devices: List, callback: MatrixCallback?) { + if (hasBeenReleased()) { + return + } + encryptingThreadHandler.post { + // build a devices map + val devicesIdListByUserId = HashMap>() + + for (di in devices) { + var deviceIdsList: MutableList? = devicesIdListByUserId[di.userId]?.toMutableList() + + if (null == deviceIdsList) { + deviceIdsList = ArrayList() + devicesIdListByUserId[di.userId] = deviceIdsList + } + deviceIdsList.add(di.deviceId) + } + + val userIds = devicesIdListByUserId.keys + + for (userId in userIds) { + val storedDeviceIDs = mCryptoStore.getUserDevices(userId) + + // sanity checks + if (null != storedDeviceIDs) { + var isUpdated = false + val deviceIds = devicesIdListByUserId[userId] + + for (deviceId in deviceIds!!) { + val device = storedDeviceIDs[deviceId] + + // assume if the device is either verified or blocked + // it means that the device is known + if (null != device && device.isUnknown) { + device.mVerified = MXDeviceInfo.DEVICE_VERIFICATION_UNVERIFIED + isUpdated = true + } + } + + if (isUpdated) { + mCryptoStore.storeUserDevices(userId, storedDeviceIDs) + } + } + } + + if (null != callback) { + mUIHandler.post { callback.onSuccess(Unit) } + } + } + } + + /** + * Update the blocked/verified state of the given device. + * + * @param verificationStatus the new verification status + * @param deviceId the unique identifier for the device. + * @param userId the owner of the device + * @param callback the asynchronous callback + */ + override fun setDeviceVerification(verificationStatus: Int, deviceId: String, userId: String, callback: MatrixCallback) { + if (hasBeenReleased()) { + return + } + + encryptingThreadHandler.post(Runnable { + val device = mCryptoStore.getUserDevice(deviceId, userId) + + // Sanity check + if (null == device) { + Timber.e("## setDeviceVerification() : Unknown device $userId:$deviceId") + mUIHandler.post { callback.onSuccess(Unit) } + return@Runnable + } + + if (device.mVerified != verificationStatus) { + device.mVerified = verificationStatus + mCryptoStore.storeUserDevice(userId, device) + + if (userId == mCredentials.userId) { + // 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.checkAndStartKeysBackup() + } + } + + mUIHandler.post { callback.onSuccess(Unit) } + }) + } + + /** + * Configure a room to use encryption. + * This method must be called in getEncryptingThreadHandler + * + * @param roomId the room id to enable encryption in. + * @param algorithm the encryption config for the room. + * @param inhibitDeviceQuery true to suppress device list query for users in the room (for now) + * @param membersId list of members to start tracking their devices + * @return true if the operation succeeds. + */ + private fun setEncryptionInRoom(roomId: String, algorithm: String?, inhibitDeviceQuery: Boolean, membersId: List): Boolean { + if (hasBeenReleased()) { + return false + } + + // If we already have encryption in this room, we should ignore this event + // (for now at least. Maybe we should alert the user somehow?) + val existingAlgorithm = mCryptoStore.getRoomAlgorithm(roomId) + + if (!TextUtils.isEmpty(existingAlgorithm) && !TextUtils.equals(existingAlgorithm, algorithm)) { + Timber.e("## setEncryptionInRoom() : Ignoring m.room.encryption event which requests a change of config in $roomId") + return false + } + + val encryptingClass = MXCryptoAlgorithms.encryptorClassForAlgorithm(algorithm) + + if (null == encryptingClass) { + Timber.e("## setEncryptionInRoom() : Unable to encrypt with " + algorithm!!) + return false + } + + mCryptoStore.storeRoomAlgorithm(roomId, algorithm!!) + + val alg: IMXEncrypting + + try { + val ctor = encryptingClass.constructors[0] + alg = ctor.newInstance() as IMXEncrypting + } catch (e: Exception) { + Timber.e(e, "## setEncryptionInRoom() : fail to load the class") + return false + } + + alg.initWithMatrixSession(this, + mOlmDevice, + deviceListManager, + mCredentials, + mSendToDeviceTask, + mTaskExecutor, + roomId) + + synchronized(mRoomEncryptors) { + mRoomEncryptors.put(roomId, alg) + } + + // if encryption was not previously enabled in this room, we will have been + // ignoring new device events for these users so far. We may well have + // up-to-date lists for some users, for instance if we were sharing other + // e2e rooms with them, so there is room for optimisation here, but for now + // we just invalidate everyone in the room. + if (null == existingAlgorithm) { + Timber.d("Enabling encryption in $roomId for the first time; invalidating device lists for all users therein") + + val userIds = ArrayList(membersId) + + deviceListManager.startTrackingDeviceList(userIds) + + if (!inhibitDeviceQuery) { + deviceListManager.refreshOutdatedDeviceLists() + } + } + + return true + } + + /** + * Tells if a room is encrypted + * + * @param roomId the room id + * @return true if the room is encrypted + */ + fun isRoomEncrypted(roomId: String?): Boolean { + var res = false + + if (null != roomId) { + synchronized(mRoomEncryptors) { + res = mRoomEncryptors.containsKey(roomId) + + if (!res) { + val room = mRoomService.getRoom(roomId) + + if (null != room) { + res = room.isEncrypted() + } + } + } + } + + return res + } + + /** + * @return the stored device keys for a user. + */ + override fun getUserDevices(userId: String): MutableList { + val map = mCryptoStore.getUserDevices(userId) + return if (null != map) ArrayList(map.values) else ArrayList() + } + + /** + * Try to make sure we have established olm sessions for the given users. + * It must be called in getEncryptingThreadHandler() thread. + * The callback is called in the UI thread. + * + * @param users a list of user ids. + * @param callback the asynchronous callback + */ + fun ensureOlmSessionsForUsers(users: List, callback: MatrixCallback>) { + Timber.d("## ensureOlmSessionsForUsers() : ensureOlmSessionsForUsers $users") + + val devicesByUser = HashMap>() + + for (userId in users) { + devicesByUser[userId] = ArrayList() + + val devices = getUserDevices(userId) + + for (device in devices) { + val key = device.identityKey() + + if (TextUtils.equals(key, mOlmDevice.deviceCurve25519Key)) { + // Don't bother setting up session to ourself + continue + } + + if (device.isVerified) { + // Don't bother setting up sessions with blocked users + continue + } + + devicesByUser[userId]!!.add(device) + } + } + + ensureOlmSessionsForDevices(devicesByUser, callback) + } + + /** + * Try to make sure we have established olm sessions for the given devices. + * It must be called in getCryptoHandler() thread. + * The callback is called in the UI thread. + * + * @param devicesByUser a map from userid to list of devices. + * @param callback the asynchronous callback + */ + fun ensureOlmSessionsForDevices(devicesByUser: Map>, + callback: MatrixCallback>?) { + val devicesWithoutSession = ArrayList() + + val results = MXUsersDevicesMap() + + val userIds = devicesByUser.keys + + for (userId in userIds) { + val deviceInfos = devicesByUser[userId] + + for (deviceInfo in deviceInfos!!) { + val deviceId = deviceInfo.deviceId + val key = deviceInfo.identityKey() + + val sessionId = mOlmDevice.getSessionId(key!!) + + if (TextUtils.isEmpty(sessionId)) { + devicesWithoutSession.add(deviceInfo) + } + + val olmSessionResult = MXOlmSessionResult(deviceInfo, sessionId) + results.setObject(olmSessionResult, userId, deviceId) + } + } + + if (devicesWithoutSession.size == 0) { + if (null != callback) { + mUIHandler.post { callback.onSuccess(results) } + } + return + } + + // Prepare the request for claiming one-time keys + val usersDevicesToClaim = MXUsersDevicesMap() + + val oneTimeKeyAlgorithm = MXKey.KEY_SIGNED_CURVE_25519_TYPE + + for (device in devicesWithoutSession) { + usersDevicesToClaim.setObject(oneTimeKeyAlgorithm, device.userId, device.deviceId) + } + + // TODO: this has a race condition - if we try to send another message + // while we are claiming a key, we will end up claiming two and setting up + // two sessions. + // + // That should eventually resolve itself, but it's poor form. + + Timber.d("## claimOneTimeKeysForUsersDevices() : $usersDevicesToClaim") + + mClaimOneTimeKeysForUsersDeviceTask + .configureWith(ClaimOneTimeKeysForUsersDeviceTask.Params(usersDevicesToClaim)) + .dispatchTo(object : MatrixCallback> { + override fun onSuccess(data: MXUsersDevicesMap) { + encryptingThreadHandler.post { + try { + Timber.d("## claimOneTimeKeysForUsersDevices() : keysClaimResponse.oneTimeKeys: $data") + + for (userId in userIds) { + val deviceInfos = devicesByUser[userId] + + for (deviceInfo in deviceInfos!!) { + + var oneTimeKey: MXKey? = null + + val deviceIds = data.getUserDeviceIds(userId) + + if (null != deviceIds) { + for (deviceId in deviceIds) { + val olmSessionResult = results.getObject(deviceId, userId) + + if (null != olmSessionResult!!.mSessionId) { + // We already have a result for this device + continue + } + + val key = data.getObject(deviceId, userId) + + if (TextUtils.equals(key!!.type, oneTimeKeyAlgorithm)) { + oneTimeKey = key + } + + if (null == oneTimeKey) { + Timber.d("## ensureOlmSessionsForDevices() : No one-time keys " + oneTimeKeyAlgorithm + + " for device " + userId + " : " + deviceId) + continue + } + + // Update the result for this device in results + olmSessionResult.mSessionId = verifyKeyAndStartSession(oneTimeKey, userId, deviceInfo) + } + } + } + } + } catch (e: Exception) { + Timber.e(e, "## ensureOlmSessionsForDevices() " + e.message) + } + + if (!hasBeenReleased()) { + if (null != callback) { + mUIHandler.post { callback.onSuccess(results) } + } + } + } + } + + override fun onFailure(failure: Throwable) { + Timber.e(failure, "## ensureOlmSessionsForUsers(): claimOneTimeKeysForUsersDevices request failed") + + callback?.onFailure(failure) + } + }) + .executeBy(mTaskExecutor) + } + + private fun verifyKeyAndStartSession(oneTimeKey: MXKey, userId: String, deviceInfo: MXDeviceInfo): String? { + var sessionId: String? = null + + val deviceId = deviceInfo.deviceId + val signKeyId = "ed25519:$deviceId" + val signature = oneTimeKey.signatureForUserId(userId, signKeyId) + + if (!TextUtils.isEmpty(signature) && !TextUtils.isEmpty(deviceInfo.fingerprint())) { + var isVerified = false + var errorMessage: String? = null + + try { + mOlmDevice.verifySignature(deviceInfo.fingerprint()!!, oneTimeKey.signalableJSONDictionary(), signature) + isVerified = true + } catch (e: Exception) { + errorMessage = e.message + } + + // Check one-time key signature + if (isVerified) { + sessionId = mOlmDevice.createOutboundSession(deviceInfo.identityKey()!!, oneTimeKey.value) + + if (!TextUtils.isEmpty(sessionId)) { + Timber.d("## verifyKeyAndStartSession() : Started new sessionid " + sessionId + + " for device " + deviceInfo + "(theirOneTimeKey: " + oneTimeKey.value + ")") + } else { + // Possibly a bad key + Timber.e("## verifyKeyAndStartSession() : Error starting session with device $userId:$deviceId") + } + } else { + Timber.e("## verifyKeyAndStartSession() : Unable to verify signature on one-time key for device " + userId + + ":" + deviceId + " Error " + errorMessage) + } + } + + return sessionId + } + + + /** + * Encrypt an event content according to the configuration of the room. + * + * @param eventContent the content of the event. + * @param eventType the type of the event. + * @param room the room the event will be sent. + * @param callback the asynchronous callback + */ + fun encryptEventContent(eventContent: Content, + eventType: String, + room: Room, + callback: MatrixCallback) { + // wait that the crypto is really started + if (!isStarted()) { + Timber.d("## encryptEventContent() : wait after e2e init") + + start(false, object : MatrixCallback { + override fun onSuccess(data: Unit) { + encryptEventContent(eventContent, eventType, room, callback) + } + + override fun onFailure(failure: Throwable) { + Timber.e(failure, "## encryptEventContent() : onNetworkError while waiting to start e2e") + + callback.onFailure(failure) + } + }) + + return + } + + // Check whether the event content must be encrypted for the invited members. + val encryptForInvitedMembers = mCryptoConfig!!.mEnableEncryptionForInvitedMembers && room.shouldEncryptForInvitedMembers() + + val userIds = if (encryptForInvitedMembers) { + room.getActiveRoomMemberIds() + } else { + room.getJoinedRoomMemberIds() + } + + // just as you are sending a secret message? + + encryptingThreadHandler.post { + var alg: IMXEncrypting? + + synchronized(mRoomEncryptors) { + alg = mRoomEncryptors[room.roomId] + } + + if (null == alg) { + val algorithm = room.encryptionAlgorithm() + + if (null != algorithm) { + if (setEncryptionInRoom(room.roomId, algorithm, false, userIds)) { + synchronized(mRoomEncryptors) { + alg = mRoomEncryptors[room.roomId] + } + } + } + } + + if (null != alg) { + val t0 = System.currentTimeMillis() + Timber.d("## encryptEventContent() starts") + + alg!!.encryptEventContent(eventContent, eventType, userIds, object : MatrixCallback { + override fun onSuccess(data: Content) { + Timber.d("## encryptEventContent() : succeeds after " + (System.currentTimeMillis() - t0) + " ms") + + callback.onSuccess(MXEncryptEventContentResult(data, EventType.ENCRYPTED)) + } + + override fun onFailure(failure: Throwable) { + callback.onFailure(failure) + } + }) + } else { + val algorithm = room.encryptionAlgorithm() + val reason = String.format(MXCryptoError.UNABLE_TO_ENCRYPT_REASON, + algorithm ?: MXCryptoError.NO_MORE_ALGORITHM_REASON) + Timber.e("## encryptEventContent() : $reason") + + mUIHandler.post { + callback.onFailure(Failure.CryptoError(MXCryptoError(MXCryptoError.UNABLE_TO_ENCRYPT_ERROR_CODE, + MXCryptoError.UNABLE_TO_ENCRYPT, reason))) + } + } + } + } + + /** + * Decrypt an event + * + * @param event the raw event. + * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack. + * @return the MXEventDecryptionResult data, or null in case of error + */ + @Throws(MXDecryptionException::class) + override fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult? { + val eventContent = event.content //wireEventContent? + + if (null == eventContent) { + Timber.e("## decryptEvent : empty event content") + return null + } + + val results = ArrayList() + val lock = CountDownLatch(1) + val exceptions = ArrayList() + + getDecryptingThreadHandler().post { + var result: MXEventDecryptionResult? = null + val alg = roomDecryptorProvider.getRoomDecryptor(event.roomId, eventContent["algorithm"] as String) + + if (null == alg) { + val reason = String.format(MXCryptoError.UNABLE_TO_DECRYPT_REASON, event.eventId, eventContent["algorithm"] as String) + Timber.e("## decryptEvent() : $reason") + exceptions.add(MXDecryptionException(MXCryptoError(MXCryptoError.UNABLE_TO_DECRYPT_ERROR_CODE, + MXCryptoError.UNABLE_TO_DECRYPT, reason))) + } else { + try { + result = alg.decryptEvent(event, timeline) + } catch (decryptionException: MXDecryptionException) { + exceptions.add(decryptionException) + } + + if (null != result) { + results.add(result) + } + } + lock.countDown() + } + + try { + lock.await() + } catch (e: Exception) { + Timber.e(e, "## decryptEvent() : failed") + } + + if (!exceptions.isEmpty()) { + throw exceptions[0] + } + + return if (!results.isEmpty()) { + results[0] + } else null + + } + + /** + * Reset replay attack data for the given timeline. + * + * @param timelineId the timeline id + */ + fun resetReplayAttackCheckInTimeline(timelineId: String) { + getDecryptingThreadHandler().post { mOlmDevice.resetReplayAttackCheckInTimeline(timelineId) } + } + + /** + * Encrypt an event payload for a list of devices. + * This method must be called from the getCryptoHandler() thread. + * + * @param payloadFields fields to include in the encrypted payload. + * @param deviceInfos list of device infos to encrypt for. + * @return the content for an m.room.encrypted event. + */ + fun encryptMessage(payloadFields: Map, deviceInfos: List): EncryptedMessage { + if (hasBeenReleased()) { + // Empty object + return EncryptedMessage() + } + + val deviceInfoParticipantKey = HashMap() + val participantKeys = ArrayList() + + for (di in deviceInfos) { + participantKeys.add(di.identityKey()!!) + deviceInfoParticipantKey[di.identityKey()!!] = di + } + + val payloadJson = HashMap(payloadFields) + + payloadJson["sender"] = mCredentials.userId + payloadJson["sender_device"] = mCredentials.deviceId + + // Include the Ed25519 key so that the recipient knows what + // device this message came from. + // We don't need to include the curve25519 key since the + // recipient will already know this from the olm headers. + // When combined with the device keys retrieved from the + // homeserver signed by the ed25519 key this proves that + // the curve25519 key and the ed25519 key are owned by + // the same device. + val keysMap = HashMap() + keysMap["ed25519"] = mOlmDevice.deviceEd25519Key!! + payloadJson["keys"] = keysMap + + val ciphertext = HashMap() + + for (deviceKey in participantKeys) { + val sessionId = mOlmDevice.getSessionId(deviceKey) + + if (!TextUtils.isEmpty(sessionId)) { + Timber.d("Using sessionid $sessionId for device $deviceKey") + val deviceInfo = deviceInfoParticipantKey[deviceKey] + + payloadJson["recipient"] = deviceInfo!!.userId + + val recipientsKeysMap = HashMap() + recipientsKeysMap["ed25519"] = deviceInfo.fingerprint()!! + payloadJson["recipient_keys"] = recipientsKeysMap + + // FIXME We have to canonicalize the JSON + //JsonUtility.canonicalize(JsonUtility.getGson(false).toJsonTree(payloadJson)).toString() + + val payloadString = convertToUTF8(MoshiProvider.getCanonicalJson(Map::class.java, payloadJson)) + ciphertext[deviceKey] = mOlmDevice.encryptMessage(deviceKey, sessionId!!, payloadString!!)!! + } + } + + val res = EncryptedMessage() + + res.algorithm = MXCRYPTO_ALGORITHM_OLM + res.senderKey = mOlmDevice.deviceCurve25519Key + res.cipherText = ciphertext + + return res + } + + /** + * Sign Object + * + * Example: + *
+     *     {
+     *         "[MY_USER_ID]": {
+     *             "ed25519:[MY_DEVICE_ID]": "sign(str)"
+     *         }
+     *     }
+     * 
+ * + * @param strToSign the String to sign and to include in the Map + * @return a Map (see example) + */ + override fun signObject(strToSign: String): Map> { + val result = HashMap>() + + val content = HashMap() + + content["ed25519:" + myDevice.deviceId] = mOlmDevice.signMessage(strToSign)!! + + result[myDevice.userId] = content + + return result + } + + /** + * Handle the 'toDevice' event + * + * @param event the event + */ + fun onToDeviceEvent(event: Event) { + mSasVerificationService.onToDeviceEvent(event) + + if (TextUtils.equals(event.type, EventType.ROOM_KEY) || TextUtils.equals(event.type, EventType.FORWARDED_ROOM_KEY)) { + getDecryptingThreadHandler().post { onRoomKeyEvent(event) } + } else if (TextUtils.equals(event.type, EventType.ROOM_KEY_REQUEST)) { + encryptingThreadHandler.post { + mIncomingRoomKeyRequestManager.onRoomKeyRequestEvent(event) + } + } + } + + /** + * Handle a key event. + * This method must be called on getDecryptingThreadHandler() thread. + * + * @param event the key event. + */ + private fun onRoomKeyEvent(event: Event?) { + // sanity check + if (null == event) { + Timber.e("## onRoomKeyEvent() : null event") + return + } + + val roomKeyContent = event.content.toModel()!! + + if (TextUtils.isEmpty(roomKeyContent.roomId) || TextUtils.isEmpty(roomKeyContent.algorithm)) { + Timber.e("## onRoomKeyEvent() : missing fields") + return + } + + val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(this, roomKeyContent.roomId, roomKeyContent.algorithm) + + if (null == alg) { + Timber.e("## onRoomKeyEvent() : Unable to handle keys for " + roomKeyContent.algorithm!!) + return + } + + alg.onRoomKeyEvent(event) + } + + /** + * Handle an m.room.encryption event. + * + * @param event the encryption event. + */ + fun onCryptoEvent(roomId: String, event: Event) { + val eventContent = event.content // wireEventContent + + val room = mRoomService.getRoom(roomId)!! + + // Check whether the event content must be encrypted for the invited members. + val encryptForInvitedMembers = mCryptoConfig!!.mEnableEncryptionForInvitedMembers && room.shouldEncryptForInvitedMembers() + + val userIds = if (encryptForInvitedMembers) { + room.getActiveRoomMemberIds() + } else { + room.getJoinedRoomMemberIds() + } + + encryptingThreadHandler.post { setEncryptionInRoom(roomId, eventContent!!["algorithm"] as String, true, userIds) } + } + + /** + * Handle a change in the membership state of a member of a room. + * + * @param event the membership event causing the change + */ + private fun onRoomMembershipEvent(roomId: String, event: Event) { + val alg: IMXEncrypting? + + synchronized(mRoomEncryptors) { + alg = mRoomEncryptors[roomId] + } + + if (null == alg) { + // No encrypting in this room + return + } + + val userId = event.stateKey!! + val room = mRoomService.getRoom(roomId) + + val roomMember = room?.getRoomMember(userId) + + if (null != roomMember) { + val membership = roomMember.membership + + encryptingThreadHandler.post { + if (membership == Membership.JOIN) { + // make sure we are tracking the deviceList for this user. + deviceListManager.startTrackingDeviceList(Arrays.asList(userId)) + } else if (membership == Membership.INVITE + && room.shouldEncryptForInvitedMembers() + && mCryptoConfig!!.mEnableEncryptionForInvitedMembers) { + // track the deviceList for this invited user. + // Caution: there's a big edge case here in that federated servers do not + // know what other servers are in the room at the time they've been invited. + // They therefore will not send device updates if a user logs in whilst + // their state is invite. + deviceListManager.startTrackingDeviceList(Arrays.asList(userId)) + } + } + } + } + + /** + * Upload my user's device keys. + * This method must called on getEncryptingThreadHandler() thread. + * The callback will called on UI thread. + * + * @param callback the asynchronous callback + */ + private fun uploadDeviceKeys(callback: MatrixCallback) { + // Prepare the device keys data to send + // Sign it + val canonicalJson = MoshiProvider.getCanonicalJson(Map::class.java, myDevice.signalableJSONDictionary()) + + myDevice.signatures = signObject(canonicalJson) + + // For now, we set the device id explicitly, as we may not be using the + // same one as used in login. + mUploadKeysTask + .configureWith(UploadKeysTask.Params(myDevice.toDeviceKeys(), null, myDevice.deviceId)) + .dispatchTo(callback) + .executeBy(mTaskExecutor) + } + + /** + * OTK upload loop + * + * @param keyCount the number of key to generate + * @param keyLimit the limit + * @param callback the asynchronous callback + */ + private fun uploadLoop(keyCount: Int, keyLimit: Int, callback: MatrixCallback) { + if (keyLimit <= keyCount) { + // If we don't need to generate any more keys then we are done. + mUIHandler.post { callback.onSuccess(Unit) } + return + } + + val keysThisLoop = Math.min(keyLimit - keyCount, ONE_TIME_KEY_GENERATION_MAX_NUMBER) + + mOlmDevice.generateOneTimeKeys(keysThisLoop) + + uploadOneTimeKeys(object : MatrixCallback { + override fun onSuccess(data: KeysUploadResponse) { + encryptingThreadHandler.post { + if (data.hasOneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE)) { + uploadLoop(data.oneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE), keyLimit, callback) + } else { + Timber.e("## uploadLoop() : response for uploading keys does not contain one_time_key_counts.signed_curve25519") + mUIHandler.post { + callback.onFailure( + Exception("response for uploading keys does not contain one_time_key_counts.signed_curve25519")) + } + } + } + } + + override fun onFailure(failure: Throwable) { + callback.onFailure(failure) + } + }) + } + + /** + * Check if the OTK must be uploaded. + * + * @param callback the asynchronous callback + */ + private fun maybeUploadOneTimeKeys(callback: MatrixCallback? = null) { + if (mOneTimeKeyCheckInProgress) { + mUIHandler.post { + callback?.onSuccess(Unit) + } + return + } + + if (System.currentTimeMillis() - mLastOneTimeKeyCheck < ONE_TIME_KEY_UPLOAD_PERIOD) { + // we've done a key upload recently. + mUIHandler.post { + callback?.onSuccess(Unit) + } + return + } + + mLastOneTimeKeyCheck = System.currentTimeMillis() + + mOneTimeKeyCheckInProgress = true + + // We then check how many keys we can store in the Account object. + val maxOneTimeKeys = mOlmDevice.getMaxNumberOfOneTimeKeys() + + // Try to keep at most half that number on the server. This leaves the + // rest of the slots free to hold keys that have been claimed from the + // server but we haven't recevied a message for. + // If we run out of slots when generating new keys then olm will + // discard the oldest private keys first. This will eventually clean + // out stale private keys that won't receive a message. + val keyLimit = Math.floor(maxOneTimeKeys / 2.0).toInt() + + if (null != mOneTimeKeyCount) { + uploadOTK(mOneTimeKeyCount!!, keyLimit, callback) + } else { + // ask the server how many keys we have + mUploadKeysTask + .configureWith(UploadKeysTask.Params(null, null, myDevice.deviceId)) + .dispatchTo(object : MatrixCallback { + + override fun onSuccess(data: KeysUploadResponse) { + encryptingThreadHandler.post { + if (!hasBeenReleased()) { + // We need to keep a pool of one time public keys on the server so that + // other devices can start conversations with us. But we can only store + // a finite number of private keys in the olm Account object. + // To complicate things further then can be a delay between a device + // claiming a public one time key from the server and it sending us a + // message. We need to keep the corresponding private key locally until + // we receive the message. + // But that message might never arrive leaving us stuck with duff + // private keys clogging up our local storage. + // So we need some kind of enginering compromise to balance all of + // these factors. + val keyCount = data.oneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE) + uploadOTK(keyCount, keyLimit, callback) + } + } + } + + override fun onFailure(failure: Throwable) { + Timber.e(failure, "## uploadKeys() : failed") + + mOneTimeKeyCount = null + mOneTimeKeyCheckInProgress = false + + mUIHandler.post { + callback?.onFailure(failure) + } + } + }) + .executeBy(mTaskExecutor) + } + } + + /** + * Upload some the OTKs. + * + * @param keyCount the key count + * @param keyLimit the limit + * @param callback the asynchronous callback + */ + private fun uploadOTK(keyCount: Int, keyLimit: Int, callback: MatrixCallback?) { + uploadLoop(keyCount, keyLimit, object : MatrixCallback { + private fun uploadKeysDone(errorMessage: String?) { + if (null != errorMessage) { + Timber.e("## maybeUploadOneTimeKeys() : failed $errorMessage") + } + mOneTimeKeyCount = null + mOneTimeKeyCheckInProgress = false + } + + override fun onSuccess(data: Unit) { + Timber.d("## maybeUploadOneTimeKeys() : succeeded") + uploadKeysDone(null) + + mUIHandler.post { + callback?.onSuccess(Unit) + } + } + + override fun onFailure(failure: Throwable) { + uploadKeysDone(failure.message) + + mUIHandler.post { + callback?.onFailure(failure) + } + } + }) + + } + + /** + * Upload my user's one time keys. + * This method must called on getEncryptingThreadHandler() thread. + * The callback will called on UI thread. + * + * @param callback the asynchronous callback + */ + private fun uploadOneTimeKeys(callback: MatrixCallback?) { + val oneTimeKeys = mOlmDevice.getOneTimeKeys() + val oneTimeJson = HashMap() + + val curve25519Map = oneTimeKeys!![OlmAccount.JSON_KEY_ONE_TIME_KEY] + + if (null != curve25519Map) { + for (key_id in curve25519Map.keys) { + val k = HashMap() + k["key"] = curve25519Map[key_id]!! + + // the key is also signed + val canonicalJson = MoshiProvider.getCanonicalJson(Map::class.java, k) + + k["signatures"] = signObject(canonicalJson) + + oneTimeJson["signed_curve25519:$key_id"] = k + } + } + + // For now, we set the device id explicitly, as we may not be using the + // same one as used in login. + mUploadKeysTask + .configureWith(UploadKeysTask.Params(null, oneTimeJson, myDevice.deviceId)) + .dispatchTo(object : MatrixCallback { + override fun onSuccess(data: KeysUploadResponse) { + encryptingThreadHandler.post { + if (!hasBeenReleased()) { + mLastPublishedOneTimeKeys = oneTimeKeys + mOlmDevice.markKeysAsPublished() + + if (null != callback) { + mUIHandler.post { callback.onSuccess(data) } + } + } + } + } + + override fun onFailure(failure: Throwable) { + if (null != callback) { + mUIHandler.post { callback.onFailure(failure) } + } + + } + }) + .executeBy(mTaskExecutor) + } + + + /** + * Export the crypto keys + * + * @param password the password + * @param callback the exported keys + */ + override fun exportRoomKeys(password: String, callback: MatrixCallback) { + exportRoomKeys(password, MXMegolmExportEncryption.DEFAULT_ITERATION_COUNT, callback) + } + + /** + * Export the crypto keys + * + * @param password the password + * @param anIterationCount the encryption iteration count (0 means no encryption) + * @param callback the exported keys + */ + fun exportRoomKeys(password: String, anIterationCount: Int, callback: MatrixCallback) { + val iterationCount = Math.max(0, anIterationCount) + + getDecryptingThreadHandler().post(Runnable { + val exportedSessions = ArrayList() + + val inboundGroupSessions = mCryptoStore.getInboundGroupSessions() + + for (session in inboundGroupSessions) { + val megolmSessionData = session.exportKeys() + + if (null != megolmSessionData) { + exportedSessions.add(megolmSessionData) + } + } + + val encryptedRoomKeys: ByteArray + + try { + val moshi = MoshiProvider.providesMoshi() + val adapter = moshi.adapter(List::class.java) + + encryptedRoomKeys = MXMegolmExportEncryption + .encryptMegolmKeyFile(adapter.toJson(exportedSessions), password, iterationCount) + } catch (e: Exception) { + callback.onFailure(e) + return@Runnable + } + + mUIHandler.post { callback.onSuccess(encryptedRoomKeys) } + }) + } + + /** + * Import the room keys + * + * @param roomKeysAsArray the room keys as array. + * @param password the password + * @param progressListener the progress listener + * @param callback the asynchronous callback. + */ + override fun importRoomKeys(roomKeysAsArray: ByteArray, + password: String, + progressListener: ProgressListener?, + callback: MatrixCallback) { + getDecryptingThreadHandler().post(Runnable { + Timber.d("## importRoomKeys starts") + + val t0 = System.currentTimeMillis() + val roomKeys: String + + try { + roomKeys = MXMegolmExportEncryption.decryptMegolmKeyFile(roomKeysAsArray, password) + } catch (e: Exception) { + mUIHandler.post { callback.onFailure(e) } + return@Runnable + } + + val importedSessions: List + + val t1 = System.currentTimeMillis() + + Timber.d("## importRoomKeys : decryptMegolmKeyFile done in " + (t1 - t0) + " ms") + + try { + val moshi = MoshiProvider.providesMoshi() + val adapter = moshi.adapter(List::class.java) + val list = adapter.fromJson(roomKeys) + importedSessions = list as List + } catch (e: Exception) { + Timber.e(e, "## importRoomKeys failed") + mUIHandler.post { callback.onFailure(e) } + return@Runnable + } + + val t2 = System.currentTimeMillis() + + Timber.d("## importRoomKeys : JSON parsing " + (t2 - t1) + " ms") + + importMegolmSessionsData(importedSessions, true, progressListener, callback) + }) + } + + /** + * Import a list of megolm session keys. + * + * @param megolmSessionsData megolm sessions. + * @param backUpKeys true to back up them to the homeserver. + * @param progressListener the progress listener + * @param callback + */ + override fun importMegolmSessionsData(megolmSessionsData: List, + backUpKeys: Boolean, + progressListener: ProgressListener?, + callback: MatrixCallback) { + getDecryptingThreadHandler().post { + val t0 = System.currentTimeMillis() + + val totalNumbersOfKeys = megolmSessionsData.size + var cpt = 0 + var lastProgress = 0 + var totalNumbersOfImportedKeys = 0 + + if (progressListener != null) { + mUIHandler.post { progressListener.onProgress(0, 100) } + } + + val sessions = mOlmDevice.importInboundGroupSessions(megolmSessionsData) + + for (megolmSessionData in megolmSessionsData) { + cpt++ + + + val decrypting = roomDecryptorProvider.getRoomDecryptor(megolmSessionData.roomId, megolmSessionData.algorithm) + + if (null != decrypting) { + try { + val sessionId = megolmSessionData.sessionId + Timber.d("## importRoomKeys retrieve mSenderKey " + megolmSessionData.senderKey + " sessionId " + sessionId) + + totalNumbersOfImportedKeys++ + + // cancel any outstanding room key requests for this session + val roomKeyRequestBody = RoomKeyRequestBody() + + roomKeyRequestBody.algorithm = megolmSessionData.algorithm + roomKeyRequestBody.roomId = megolmSessionData.roomId + roomKeyRequestBody.senderKey = megolmSessionData.senderKey + roomKeyRequestBody.sessionId = megolmSessionData.sessionId + + cancelRoomKeyRequest(roomKeyRequestBody) + + // Have another go at decrypting events sent with this session + decrypting.onNewSession(megolmSessionData.senderKey!!, sessionId!!) + } catch (e: Exception) { + Timber.e(e, "## importRoomKeys() : onNewSession failed") + } + } + + if (progressListener != null) { + val progress = 100 * cpt / totalNumbersOfKeys + + if (lastProgress != progress) { + lastProgress = progress + + mUIHandler.post { progressListener.onProgress(progress, 100) } + } + } + } + + // Do not back up the key if it comes from a backup recovery + if (backUpKeys) { + mKeysBackup.maybeBackupKeys() + } else { + mCryptoStore.markBackupDoneForInboundGroupSessions(sessions) + } + + val t1 = System.currentTimeMillis() + + Timber.d("## importMegolmSessionsData : sessions import " + (t1 - t0) + " ms (" + megolmSessionsData.size + " sessions)") + + val finalTotalNumbersOfImportedKeys = totalNumbersOfImportedKeys + + mUIHandler.post { callback.onSuccess(ImportRoomKeysResult(totalNumbersOfKeys, finalTotalNumbersOfImportedKeys)) } + } + } + + /** + * Tells if the encryption must fail if some unknown devices are detected. + * + * @return true to warn when some unknown devices are detected. + */ + fun warnOnUnknownDevices(): Boolean { + return mWarnOnUnknownDevices + } + + /** + * Update the warn status when some unknown devices are detected. + * + * @param warn true to warn when some unknown devices are detected. + */ + override fun setWarnOnUnknownDevices(warn: Boolean) { + mWarnOnUnknownDevices = warn + } + + /** + * Check if the user ids list have some unknown devices. + * A success means there is no unknown devices. + * If there are some unknown devices, a MXCryptoError.UNKNOWN_DEVICES_CODE exception is triggered. + * + * @param userIds the user ids list + * @param callback the asynchronous callback. + */ + fun checkUnknownDevices(userIds: List, callback: MatrixCallback) { + // force the refresh to ensure that the devices list is up-to-date + deviceListManager.downloadKeys(userIds, true, object : MatrixCallback> { + override fun onSuccess(data: MXUsersDevicesMap) { + val unknownDevices = getUnknownDevices(data) + + if (unknownDevices.map.size == 0) { + callback.onSuccess(Unit) + } else { + // trigger an an unknown devices exception + callback.onFailure( + Failure.CryptoError(MXCryptoError(MXCryptoError.UNKNOWN_DEVICES_CODE, + MXCryptoError.UNABLE_TO_ENCRYPT, MXCryptoError.UNKNOWN_DEVICES_REASON, unknownDevices))) + } + } + + override fun onFailure(failure: Throwable) { + callback.onFailure(failure) + } + }) + } + + /** + * Set the global override for whether the client should ever send encrypted + * messages to unverified devices. + * If false, it can still be overridden per-room. + * If true, it overrides the per-room settings. + * + * @param block true to unilaterally blacklist all + * @param callback the asynchronous callback. + */ + override fun setGlobalBlacklistUnverifiedDevices(block: Boolean, callback: MatrixCallback?) { + encryptingThreadHandler.post { + mCryptoStore.setGlobalBlacklistUnverifiedDevices(block) + mUIHandler.post { + callback?.onSuccess(Unit) + } + } + } + + /** + * Tells whether the client should ever send encrypted messages to unverified devices. + * The default value is false. + * messages to unverified devices. + * + * @param callback the asynchronous callback + */ + override fun getGlobalBlacklistUnverifiedDevices(callback: MatrixCallback?) { + encryptingThreadHandler.post { + if (null != callback) { + val status = globalBlacklistUnverifiedDevices + + mUIHandler.post { callback.onSuccess(status) } + } + } + } + + /** + * Tells whether the client should encrypt messages only for the verified devices + * in this room. + * The default value is false. + * This function must be called in the getEncryptingThreadHandler() thread. + * + * @param roomId the room id + * @return true if the client should encrypt messages only for the verified devices. + */ + fun isRoomBlacklistUnverifiedDevices(roomId: String?): Boolean { + return if (null != roomId) { + mCryptoStore.getRoomsListBlacklistUnverifiedDevices().contains(roomId) + } else { + false + } + } + + /** + * Tells whether the client should encrypt messages only for the verified devices + * in this room. + * The default value is false. + * This function must be called in the getEncryptingThreadHandler() thread. + * + * @param roomId the room id + * @param callback the asynchronous callback + */ + override fun isRoomBlacklistUnverifiedDevices(roomId: String, callback: MatrixCallback?) { + encryptingThreadHandler.post { + val status = isRoomBlacklistUnverifiedDevices(roomId) + + mUIHandler.post { + callback?.onSuccess(status) + } + } + } + + /** + * Manages the room black-listing for unverified devices. + * + * @param roomId the room id + * @param add true to add the room id to the list, false to remove it. + * @param callback the asynchronous callback + */ + private fun setRoomBlacklistUnverifiedDevices(roomId: String, add: Boolean, callback: MatrixCallback?) { + val room = mRoomService.getRoom(roomId) + + // sanity check + if (null == room) { + mUIHandler.post { callback!!.onSuccess(Unit) } + + return + } + + encryptingThreadHandler.post { + val roomIds = mCryptoStore.getRoomsListBlacklistUnverifiedDevices().toMutableList() + + if (add) { + if (!roomIds.contains(roomId)) { + roomIds.add(roomId) + } + } else { + roomIds.remove(roomId) + } + + mCryptoStore.setRoomsListBlacklistUnverifiedDevices(roomIds) + + mUIHandler.post { + callback?.onSuccess(Unit) + } + } + } + + + /** + * Add this room to the ones which don't encrypt messages to unverified devices. + * + * @param roomId the room id + * @param callback the asynchronous callback + */ + override fun setRoomBlacklistUnverifiedDevices(roomId: String, callback: MatrixCallback) { + setRoomBlacklistUnverifiedDevices(roomId, true, callback) + } + + /** + * Remove this room to the ones which don't encrypt messages to unverified devices. + * + * @param roomId the room id + * @param callback the asynchronous callback + */ + override fun setRoomUnBlacklistUnverifiedDevices(roomId: String, callback: MatrixCallback) { + setRoomBlacklistUnverifiedDevices(roomId, false, callback) + } + + /** + * Send a request for some room keys, if we have not already done so. + * + * @param requestBody requestBody + * @param recipients recipients + */ + fun requestRoomKey(requestBody: RoomKeyRequestBody, recipients: List>) { + encryptingThreadHandler.post { mOutgoingRoomKeyRequestManager.sendRoomKeyRequest(requestBody, recipients) } + } + + /** + * Cancel any earlier room key request + * + * @param requestBody requestBody + */ + override fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody) { + encryptingThreadHandler.post { mOutgoingRoomKeyRequestManager.cancelRoomKeyRequest(requestBody) } + } + + /** + * Re request the encryption keys required to decrypt an event. + * + * @param event the event to decrypt again. + */ + override fun reRequestRoomKeyForEvent(event: Event) { + val wireContent = event.content!! // Wireeventcontent? + + val algorithm = wireContent["algorithm"].toString() + val senderKey = wireContent["sender_key"].toString() + val sessionId = wireContent["session_id"].toString() + + encryptingThreadHandler.post { + val requestBody = RoomKeyRequestBody() + + requestBody.roomId = event.roomId + requestBody.algorithm = algorithm + requestBody.senderKey = senderKey + requestBody.sessionId = sessionId + + mOutgoingRoomKeyRequestManager.resendRoomKeyRequest(requestBody) + } + } + + /** + * Add a RoomKeysRequestListener listener. + * + * @param listener listener + */ + override fun addRoomKeysRequestListener(listener: RoomKeysRequestListener) { + mIncomingRoomKeyRequestManager.addRoomKeysRequestListener(listener) + } + + /** + * Add a RoomKeysRequestListener listener. + * + * @param listener listener + */ + fun removeRoomKeysRequestListener(listener: RoomKeysRequestListener) { + mIncomingRoomKeyRequestManager.removeRoomKeysRequestListener(listener) + } + + /* ========================================================================================== + * DEBUG INFO + * ========================================================================================== */ + + override fun toString(): String { + return myDevice.userId + " (" + myDevice.deviceId + ")" + + } + + companion object { + // max number of keys to upload at once + // Creating keys can be an expensive operation so we limit the + // number we generate in one go to avoid blocking the application + // for too long. + private const val ONE_TIME_KEY_GENERATION_MAX_NUMBER = 5 + + // frequency with which to check & upload one-time keys + private const val ONE_TIME_KEY_UPLOAD_PERIOD = (60 * 1000).toLong() // one minute + + /** + * Provides the list of unknown devices + * + * @param devicesInRoom the devices map + * @return the unknown devices map + */ + fun getUnknownDevices(devicesInRoom: MXUsersDevicesMap): MXUsersDevicesMap { + val unknownDevices = MXUsersDevicesMap() + + val userIds = devicesInRoom.userIds + for (userId in userIds) { + val deviceIds = devicesInRoom.getUserDeviceIds(userId) + for (deviceId in deviceIds!!) { + val deviceInfo = devicesInRoom.getObject(deviceId, userId) + + if (deviceInfo!!.isUnknown) { + unknownDevices.setObject(deviceInfo, userId, deviceId) + } + } + } + + return unknownDevices + } + } +} +/** + * Check if the OTK must be uploaded. + */ \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt new file mode 100644 index 00000000..7dfb47a8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt @@ -0,0 +1,240 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto + +import android.content.Context +import im.vector.matrix.android.api.auth.data.Credentials +import im.vector.matrix.android.api.session.crypto.CryptoService +import im.vector.matrix.android.internal.crypto.api.CryptoApi +import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore +import im.vector.matrix.android.internal.crypto.store.db.RealmCryptoStore +import im.vector.matrix.android.internal.crypto.store.db.RealmCryptoStoreMigration +import im.vector.matrix.android.internal.crypto.store.db.RealmCryptoStoreModule +import im.vector.matrix.android.internal.crypto.store.db.hash +import im.vector.matrix.android.internal.crypto.keysbackup.KeysBackup +import im.vector.matrix.android.internal.crypto.keysbackup.api.RoomKeysApi +import im.vector.matrix.android.internal.crypto.keysbackup.tasks.* +import im.vector.matrix.android.internal.crypto.tasks.* +import im.vector.matrix.android.internal.crypto.verification.DefaultSasVerificationService +import im.vector.matrix.android.internal.session.DefaultSession +import io.realm.RealmConfiguration +import org.koin.dsl.module.module +import org.matrix.olm.OlmManager +import retrofit2.Retrofit +import java.io.File + +internal class CryptoModule { + + val definition = module(override = true) { + + /* ========================================================================================== + * Crypto Main + * ========================================================================================== */ + + // Realm configuration, named to avoid clash with main cache realm configuration + scope(DefaultSession.SCOPE, name = "CryptoRealmConfiguration") { + val context: Context = get() + + val credentials: Credentials = get() + + RealmConfiguration.Builder() + .directory(File(context.filesDir, credentials.userId.hash())) + .name("crypto_store.realm") + .modules(RealmCryptoStoreModule()) + .schemaVersion(RealmCryptoStoreMigration.CRYPTO_STORE_SCHEMA_VERSION) + .migration(RealmCryptoStoreMigration) + .build() + } + + // CryptoStore + scope(DefaultSession.SCOPE) { + RealmCryptoStore(false /* TODO*/, + get("CryptoRealmConfiguration"), + get()) as IMXCryptoStore + } + + scope(DefaultSession.SCOPE) { + val retrofit: Retrofit = get() + retrofit.create(CryptoApi::class.java) + } + + // CryptoService + scope(DefaultSession.SCOPE) { + DefaultCryptoService(get()) as CryptoService + } + + // + scope(DefaultSession.SCOPE) { + MXOutgoingRoomKeyRequestManager(get(), get(), get()) + } + + scope(DefaultSession.SCOPE) { + IncomingRoomKeyRequestManager(get(), get(), get()) + } + + scope(DefaultSession.SCOPE) { + RoomDecryptorProvider(get(), get(), get(), get(), get()) + } + + scope(DefaultSession.SCOPE) { + // Ensure OlmManager is loaded first + get() + + MXOlmDevice(get()) + } + + // CryptoManager + scope(DefaultSession.SCOPE) { + CryptoManager( + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + get(), + // Tasks + get(), get(), get(), get(), get(), get(), get(), + // Task executor + get() + ) + } + + // Olm manager + single { + // load the crypto libs. + OlmManager() + } + + + // Crypto config + scope(DefaultSession.SCOPE) { + MXCryptoConfig() + } + + // Device list + scope(DefaultSession.SCOPE) { + DeviceListManager(get(), get(), get(), get(), get(), get()) + } + + // Crypto tasks + scope(DefaultSession.SCOPE) { + DefaultClaimOneTimeKeysForUsersDevice(get()) as ClaimOneTimeKeysForUsersDeviceTask + } + scope(DefaultSession.SCOPE) { + DefaultDeleteDeviceTask(get()) as DeleteDeviceTask + } + scope(DefaultSession.SCOPE) { + DefaultDownloadKeysForUsers(get()) as DownloadKeysForUsersTask + } + scope(DefaultSession.SCOPE) { + DefaultGetDevicesTask(get()) as GetDevicesTask + } + scope(DefaultSession.SCOPE) { + DefaultGetKeyChangesTask(get()) as GetKeyChangesTask + } + scope(DefaultSession.SCOPE) { + DefaultSendToDeviceTask(get()) as SendToDeviceTask + } + scope(DefaultSession.SCOPE) { + DefaultSetDeviceNameTask(get()) as SetDeviceNameTask + } + scope(DefaultSession.SCOPE) { + DefaultUploadKeysTask(get()) as UploadKeysTask + } + + /* ========================================================================================== + * Keys backup + * ========================================================================================== */ + + scope(DefaultSession.SCOPE) { + val retrofit: Retrofit = get() + retrofit.create(RoomKeysApi::class.java) + } + + scope(DefaultSession.SCOPE) { + KeysBackup( + // Credentials + get(), + // CryptoStore + get(), + get(), + // Task + get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), + // Task executor + get()) + } + + // Key backup tasks + scope(DefaultSession.SCOPE) { + DefaultCreateKeysBackupVersionTask(get()) as CreateKeysBackupVersionTask + } + scope(DefaultSession.SCOPE) { + DefaultDeleteBackupTask(get()) as DeleteBackupTask + } + scope(DefaultSession.SCOPE) { + DefaultDeleteRoomSessionDataTask(get()) as DeleteRoomSessionDataTask + } + scope(DefaultSession.SCOPE) { + DefaultDeleteRoomSessionsDataTask(get()) as DeleteRoomSessionsDataTask + } + scope(DefaultSession.SCOPE) { + DefaultDeleteSessionsDataTask(get()) as DeleteSessionsDataTask + } + scope(DefaultSession.SCOPE) { + DefaultGetKeysBackupLastVersionTask(get()) as GetKeysBackupLastVersionTask + } + scope(DefaultSession.SCOPE) { + DefaultGetKeysBackupVersionTask(get()) as GetKeysBackupVersionTask + } + scope(DefaultSession.SCOPE) { + DefaultGetRoomSessionDataTask(get()) as GetRoomSessionDataTask + } + scope(DefaultSession.SCOPE) { + DefaultGetRoomSessionsDataTask(get()) as GetRoomSessionsDataTask + } + scope(DefaultSession.SCOPE) { + DefaultGetSessionsDataTask(get()) as GetSessionsDataTask + } + scope(DefaultSession.SCOPE) { + DefaultStoreRoomSessionDataTask(get()) as StoreRoomSessionDataTask + } + scope(DefaultSession.SCOPE) { + DefaultStoreRoomSessionsDataTask(get()) as StoreRoomSessionsDataTask + } + scope(DefaultSession.SCOPE) { + DefaultStoreSessionsDataTask(get()) as StoreSessionsDataTask + } + scope(DefaultSession.SCOPE) { + DefaultUpdateKeysBackupVersionTask(get()) as UpdateKeysBackupVersionTask + } + + /* ========================================================================================== + * SAS Verification + * ========================================================================================== */ + + scope(DefaultSession.SCOPE) { + DefaultSasVerificationService(get(), get(), get(), get(), get()) + } + + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt new file mode 100644 index 00000000..00ecc525 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DefaultCryptoService.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto + +import im.vector.matrix.android.api.session.crypto.CryptoService + +internal class DefaultCryptoService(val cryptoManager: CryptoManager) + : CryptoService by cryptoManager \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DeviceListManager.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DeviceListManager.kt new file mode 100755 index 00000000..e6201217 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/DeviceListManager.kt @@ -0,0 +1,747 @@ +/* + * Copyright 2017 Vector Creations Ltd + * 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 im.vector.matrix.android.internal.crypto + +import android.text.TextUtils +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.MatrixPatterns +import im.vector.matrix.android.api.auth.data.Credentials +import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo +import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap +import im.vector.matrix.android.internal.crypto.model.rest.KeysQueryResponse +import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore +import im.vector.matrix.android.internal.crypto.tasks.DownloadKeysForUsersTask +import im.vector.matrix.android.internal.session.sync.SyncTokenStore +import im.vector.matrix.android.internal.task.TaskExecutor +import im.vector.matrix.android.internal.task.configureWith +import timber.log.Timber +import java.util.* + +// Legacy name: MXDeviceList +internal class DeviceListManager(private val mCryptoStore: IMXCryptoStore, + private val mOlmDevice: MXOlmDevice, + private val mSyncTokenStore: SyncTokenStore, + private val mCredentials: Credentials, + private val mDownloadKeysForUsersTask: DownloadKeysForUsersTask, + private val mTaskExecutor: TaskExecutor) { + + // keys in progress + private val mUserKeyDownloadsInProgress = HashSet() + + // HS not ready for retry + private val mNotReadyToRetryHS = HashSet() + + // indexed by UserId + private val mPendingDownloadKeysRequestToken = HashMap() + + // pending queues list + private val mDownloadKeysQueues = ArrayList() + + // tells if there is a download keys request in progress + private var mIsDownloadingKeys = false + + // Internal listener + private lateinit var mCryptoListener: DeviceListCryptoListener + + /** + * Creator + * + * @param userIds the user ids list + * @param callback the asynchronous callback + */ + internal inner class DownloadKeysPromise(userIds: List, + val mCallback: MatrixCallback>?) { + // list of remain pending device keys + val mPendingUserIdsList: MutableList + + // the unfiltered user ids list + val mUserIdsList: List + + init { + mPendingUserIdsList = ArrayList(userIds) + mUserIdsList = ArrayList(userIds) + } + } + + init { + var isUpdated = false + + val deviceTrackingStatuses = mCryptoStore.getDeviceTrackingStatuses().toMutableMap() + for (userId in deviceTrackingStatuses.keys) { + val status = deviceTrackingStatuses[userId]!! + + if (TRACKING_STATUS_DOWNLOAD_IN_PROGRESS == status || TRACKING_STATUS_UNREACHABLE_SERVER == status) { + // if a download was in progress when we got shut down, it isn't any more. + deviceTrackingStatuses.put(userId, TRACKING_STATUS_PENDING_DOWNLOAD) + isUpdated = true + } + } + + if (isUpdated) { + mCryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses) + } + } + + /** + * Tells if the key downloads should be tried + * + * @param userId the userId + * @return true if the keys download can be retrieved + */ + private fun canRetryKeysDownload(userId: String): Boolean { + var res = false + + if (!TextUtils.isEmpty(userId) && userId.contains(":")) { + try { + synchronized(mNotReadyToRetryHS) { + res = !mNotReadyToRetryHS.contains(userId.substring(userId.lastIndexOf(":") + 1)) + } + } catch (e: Exception) { + Timber.e(e, "## canRetryKeysDownload() failed") + } + + } + + return res + } + + /** + * Add a download keys promise + * + * @param userIds the user ids list + * @param callback the asynchronous callback + * @return the filtered user ids list i.e the one which require a remote request + */ + private fun addDownloadKeysPromise(userIds: MutableList?, callback: MatrixCallback>?): MutableList? { + if (null != userIds) { + val filteredUserIds = ArrayList() + val invalidUserIds = ArrayList() + + for (userId in userIds) { + if (MatrixPatterns.isUserId(userId)) { + filteredUserIds.add(userId) + } else { + Timber.e("## userId " + userId + "is not a valid user id") + invalidUserIds.add(userId) + } + } + + synchronized(mUserKeyDownloadsInProgress) { + filteredUserIds.removeAll(mUserKeyDownloadsInProgress) + mUserKeyDownloadsInProgress.addAll(userIds) + // got some email addresses instead of matrix ids + mUserKeyDownloadsInProgress.removeAll(invalidUserIds) + userIds.removeAll(invalidUserIds) + } + + mDownloadKeysQueues.add(DownloadKeysPromise(userIds, callback)) + + return filteredUserIds + } else { + return null + } + } + + /** + * Clear the unavailable server lists + */ + private fun clearUnavailableServersList() { + synchronized(mNotReadyToRetryHS) { + mNotReadyToRetryHS.clear() + } + } + + /** + * Mark the cached device list for the given user outdated + * flag the given user for device-list tracking, if they are not already. + * + * @param userIds the user ids list + */ + fun startTrackingDeviceList(userIds: List?) { + if (null != userIds) { + var isUpdated = false + val deviceTrackingStatuses = mCryptoStore.getDeviceTrackingStatuses().toMutableMap() + + for (userId in userIds) { + if (!deviceTrackingStatuses.containsKey(userId) || TRACKING_STATUS_NOT_TRACKED == deviceTrackingStatuses[userId]) { + Timber.d("## startTrackingDeviceList() : Now tracking device list for $userId") + deviceTrackingStatuses.put(userId, TRACKING_STATUS_PENDING_DOWNLOAD) + isUpdated = true + } + } + + if (isUpdated) { + mCryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses) + } + } + } + + /** + * Update the devices list statuses + * + * @param changed the user ids list which have new devices + * @param left the user ids list which left a room + */ + fun handleDeviceListsChanges(changed: List?, left: List?) { + var isUpdated = false + val deviceTrackingStatuses = mCryptoStore.getDeviceTrackingStatuses().toMutableMap() + + if (changed?.isNotEmpty() == true) { + clearUnavailableServersList() + + for (userId in changed) { + if (deviceTrackingStatuses.containsKey(userId)) { + Timber.d("## invalidateUserDeviceList() : Marking device list outdated for $userId") + deviceTrackingStatuses.put(userId, TRACKING_STATUS_PENDING_DOWNLOAD) + isUpdated = true + } + } + } + + if (left?.isNotEmpty() == true) { + clearUnavailableServersList() + + for (userId in left) { + if (deviceTrackingStatuses.containsKey(userId)) { + Timber.d("## invalidateUserDeviceList() : No longer tracking device list for $userId") + deviceTrackingStatuses.put(userId, TRACKING_STATUS_NOT_TRACKED) + isUpdated = true + } + } + } + + if (isUpdated) { + mCryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses) + } + } + + /** + * This will flag each user whose devices we are tracking as in need of an + * + update + */ + fun invalidateAllDeviceLists() { + handleDeviceListsChanges(ArrayList(mCryptoStore.getDeviceTrackingStatuses().keys), null) + } + + /** + * The keys download failed + * + * @param userIds the user ids list + */ + private fun onKeysDownloadFailed(userIds: List?) { + if (null != userIds) { + synchronized(mUserKeyDownloadsInProgress) { + val deviceTrackingStatuses = mCryptoStore.getDeviceTrackingStatuses().toMutableMap() + + for (userId in userIds) { + mUserKeyDownloadsInProgress.remove(userId) + deviceTrackingStatuses.put(userId, TRACKING_STATUS_PENDING_DOWNLOAD) + } + + mCryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses) + } + } + + mIsDownloadingKeys = false + } + + /** + * The keys download succeeded. + * + * @param userIds the userIds list + * @param failures the failure map. + */ + private fun onKeysDownloadSucceed(userIds: List?, failures: Map>?) { + if (null != failures) { + val keys = failures.keys + + for (k in keys) { + val value = failures[k] + + if (value!!.containsKey("status")) { + val statusCodeAsVoid = value["status"] + var statusCode = 0 + + if (statusCodeAsVoid is Double) { + statusCode = statusCodeAsVoid.toInt() + } else if (statusCodeAsVoid is Int) { + statusCode = statusCodeAsVoid.toInt() + } + + if (statusCode == 503) { + synchronized(mNotReadyToRetryHS) { + mNotReadyToRetryHS.add(k) + } + } + } + } + } + + val deviceTrackingStatuses = mCryptoStore.getDeviceTrackingStatuses().toMutableMap() + + if (null != userIds) { + if (mDownloadKeysQueues.size > 0) { + val promisesToRemove = ArrayList() + + for (promise in mDownloadKeysQueues) { + promise.mPendingUserIdsList.removeAll(userIds) + + if (promise.mPendingUserIdsList.size == 0) { + // private members + val usersDevicesInfoMap = MXUsersDevicesMap() + + for (userId in promise.mUserIdsList) { + val devices = mCryptoStore.getUserDevices(userId) + if (null == devices) { + if (canRetryKeysDownload(userId)) { + deviceTrackingStatuses.put(userId, TRACKING_STATUS_PENDING_DOWNLOAD) + Timber.e("failed to retry the devices of $userId : retry later") + } else { + if (deviceTrackingStatuses.containsKey(userId) && TRACKING_STATUS_DOWNLOAD_IN_PROGRESS == deviceTrackingStatuses[userId]) { + deviceTrackingStatuses.put(userId, TRACKING_STATUS_UNREACHABLE_SERVER) + Timber.e("failed to retry the devices of $userId : the HS is not available") + } + } + } else { + if (deviceTrackingStatuses.containsKey(userId) && TRACKING_STATUS_DOWNLOAD_IN_PROGRESS == deviceTrackingStatuses[userId]) { + // we didn't get any new invalidations since this download started: + // this user's device list is now up to date. + deviceTrackingStatuses.put(userId, TRACKING_STATUS_UP_TO_DATE) + Timber.d("Device list for $userId now up to date") + } + + // And the response result + usersDevicesInfoMap.setObjects(devices, userId) + } + } + + if (!mCryptoListener.hasBeenReleased()) { + val callback = promise.mCallback + + if (null != callback) { + CryptoAsyncHelper.getUiHandler().post { callback.onSuccess(usersDevicesInfoMap) } + } + } + promisesToRemove.add(promise) + } + } + mDownloadKeysQueues.removeAll(promisesToRemove) + } + + for (userId in userIds) { + mUserKeyDownloadsInProgress.remove(userId) + } + + mCryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses) + } + + mIsDownloadingKeys = false + } + + /** + * Download the device keys for a list of users and stores the keys in the MXStore. + * It must be called in getEncryptingThreadHandler() thread. + * The callback is called in the UI thread. + * + * @param userIds The users to fetch. + * @param forceDownload Always download the keys even if cached. + * @param callback the asynchronous callback + */ + fun downloadKeys(userIds: List?, forceDownload: Boolean, callback: MatrixCallback>?) { + Timber.d("## downloadKeys() : forceDownload $forceDownload : $userIds") + + // Map from userid -> deviceid -> DeviceInfo + val stored = MXUsersDevicesMap() + + // List of user ids we need to download keys for + val downloadUsers = ArrayList() + + if (null != userIds) { + if (forceDownload) { + downloadUsers.addAll(userIds) + } else { + for (userId in userIds) { + val status = mCryptoStore.getDeviceTrackingStatus(userId, TRACKING_STATUS_NOT_TRACKED) + + // downloading keys ->the keys download won't be triggered twice but the callback requires the dedicated keys + // not yet retrieved + if (mUserKeyDownloadsInProgress.contains(userId) || TRACKING_STATUS_UP_TO_DATE != status && TRACKING_STATUS_UNREACHABLE_SERVER != status) { + downloadUsers.add(userId) + } else { + val devices = mCryptoStore.getUserDevices(userId) + + // should always be true + if (null != devices) { + stored.setObjects(devices, userId) + } else { + downloadUsers.add(userId) + } + } + } + } + } + + if (0 == downloadUsers.size) { + Timber.d("## downloadKeys() : no new user device") + + if (null != callback) { + CryptoAsyncHelper.getUiHandler().post { callback.onSuccess(stored) } + } + } else { + Timber.d("## downloadKeys() : starts") + val t0 = System.currentTimeMillis() + + doKeyDownloadForUsers(downloadUsers, object : MatrixCallback> { + override fun onSuccess(data: MXUsersDevicesMap) { + Timber.d("## downloadKeys() : doKeyDownloadForUsers succeeds after " + (System.currentTimeMillis() - t0) + " ms") + + data.addEntriesFromMap(stored) + + callback?.onSuccess(data) + } + + override fun onFailure(failure: Throwable) { + Timber.e(failure, "## downloadKeys() : doKeyDownloadForUsers onFailure") + callback?.onFailure(failure) + } + }) + } + } + + /** + * Download the devices keys for a set of users. + * It must be called in getEncryptingThreadHandler() thread. + * The callback is called in the UI thread. + * + * @param downloadUsers the user ids list + * @param callback the asynchronous callback + */ + private fun doKeyDownloadForUsers(downloadUsers: MutableList, callback: MatrixCallback>?) { + Timber.d("## doKeyDownloadForUsers() : doKeyDownloadForUsers $downloadUsers") + + // get the user ids which did not already trigger a keys download + val filteredUsers = addDownloadKeysPromise(downloadUsers, callback) + + // if there is no new keys request + if (0 == filteredUsers!!.size) { + // trigger nothing + return + } + + // sanity check + //if (null == mxSession.dataHandler || null == mxSession.dataHandler.store) { + // return + //} + + mIsDownloadingKeys = true + + // track the race condition while sending requests + // we defines a tag for each request + // and test if the response is the latest request one + val downloadToken = filteredUsers.hashCode().toString() + " " + System.currentTimeMillis() + + for (userId in filteredUsers) { + mPendingDownloadKeysRequestToken[userId] = downloadToken + } + + mDownloadKeysForUsersTask + .configureWith(DownloadKeysForUsersTask.Params(filteredUsers, mSyncTokenStore.getLastToken())) + .dispatchTo(object : MatrixCallback { + override fun onSuccess(data: KeysQueryResponse) { + CryptoAsyncHelper.getEncryptBackgroundHandler().post { + Timber.d("## doKeyDownloadForUsers() : Got keys for " + filteredUsers.size + " users") + val userIdsList = ArrayList(filteredUsers) + + for (userId in userIdsList) { + // test if the response is the latest request one + if (!TextUtils.equals(mPendingDownloadKeysRequestToken[userId], downloadToken)) { + Timber.e("## doKeyDownloadForUsers() : Another update in the queue for " + + userId + " not marking up-to-date") + filteredUsers.remove(userId) + } else { + val devices = data.deviceKeys!![userId] + + Timber.d("## doKeyDownloadForUsers() : Got keys for $userId : $devices") + + if (null != devices) { + val mutableDevices = HashMap(devices) + val deviceIds = ArrayList(mutableDevices.keys) + + for (deviceId in deviceIds) { + // the user has been logged out + // TODO + //if (null == cryptoStore) { + // break + //} + + // Get the potential previously store device keys for this device + val previouslyStoredDeviceKeys = mCryptoStore.getUserDevice(deviceId, userId) + val deviceInfo = mutableDevices[deviceId] + + // in some race conditions (like unit tests) + // the self device must be seen as verified + if (TextUtils.equals(deviceInfo!!.deviceId, mCredentials.deviceId) && TextUtils.equals(userId, mCredentials.userId)) { + deviceInfo.mVerified = MXDeviceInfo.DEVICE_VERIFICATION_VERIFIED + } + + // Validate received keys + if (!validateDeviceKeys(deviceInfo, userId, deviceId, previouslyStoredDeviceKeys)) { + // New device keys are not valid. Do not store them + mutableDevices.remove(deviceId) + + if (null != previouslyStoredDeviceKeys) { + // But keep old validated ones if any + mutableDevices[deviceId] = previouslyStoredDeviceKeys + } + } else if (null != previouslyStoredDeviceKeys) { + // The verified status is not sync'ed with hs. + // This is a client side information, valid only for this client. + // So, transfer its previous value + mutableDevices[deviceId]!!.mVerified = previouslyStoredDeviceKeys.mVerified + } + } + + // Update the store + // Note that devices which aren't in the response will be removed from the stores + mCryptoStore.storeUserDevices(userId, mutableDevices) + } + + // the response is the latest request one + mPendingDownloadKeysRequestToken.remove(userId) + } + } + + onKeysDownloadSucceed(filteredUsers, data.failures) + } + } + + private fun onFailed() { + CryptoAsyncHelper.getEncryptBackgroundHandler().post { + val userIdsList = ArrayList(filteredUsers) + + // test if the response is the latest request one + for (userId in userIdsList) { + if (!TextUtils.equals(mPendingDownloadKeysRequestToken[userId], downloadToken)) { + Timber.e("## doKeyDownloadForUsers() : Another update in the queue for $userId not marking up-to-date") + filteredUsers.remove(userId) + } else { + // the response is the latest request one + mPendingDownloadKeysRequestToken.remove(userId) + } + } + + onKeysDownloadFailed(filteredUsers) + } + } + + override fun onFailure(failure: Throwable) { + Timber.e(failure, "##doKeyDownloadForUsers() : onNetworkError") + + onFailed() + + callback?.onFailure(failure) + } + }) + .executeBy(mTaskExecutor) + } + + /** + * Validate device keys. + * This method must called on getEncryptingThreadHandler() thread. + * + * @param deviceKeys the device keys to validate. + * @param userId the id of the user of the device. + * @param deviceId the id of the device. + * @param previouslyStoredDeviceKeys the device keys we received before for this device + * @return true if succeeds + */ + private fun validateDeviceKeys(deviceKeys: MXDeviceInfo?, userId: String, deviceId: String, previouslyStoredDeviceKeys: MXDeviceInfo?): Boolean { + if (null == deviceKeys) { + Timber.e("## validateDeviceKeys() : deviceKeys is null from $userId:$deviceId") + return false + } + + if (null == deviceKeys.keys) { + Timber.e("## validateDeviceKeys() : deviceKeys.keys is null from $userId:$deviceId") + return false + } + + if (null == deviceKeys.signatures) { + Timber.e("## validateDeviceKeys() : deviceKeys.signatures is null from $userId:$deviceId") + return false + } + + // Check that the user_id and device_id in the received deviceKeys are correct + if (!TextUtils.equals(deviceKeys.userId, userId)) { + Timber.e("## validateDeviceKeys() : Mismatched user_id " + deviceKeys.userId + " from " + userId + ":" + deviceId) + return false + } + + if (!TextUtils.equals(deviceKeys.deviceId, deviceId)) { + Timber.e("## validateDeviceKeys() : Mismatched device_id " + deviceKeys.deviceId + " from " + userId + ":" + deviceId) + return false + } + + val signKeyId = "ed25519:" + deviceKeys.deviceId + val signKey = deviceKeys.keys!![signKeyId] + + if (null == signKey) { + Timber.e("## validateDeviceKeys() : Device " + userId + ":" + deviceKeys.deviceId + " has no ed25519 key") + return false + } + + val signatureMap = deviceKeys.signatures!![userId] + + if (null == signatureMap) { + Timber.e("## validateDeviceKeys() : Device " + userId + ":" + deviceKeys.deviceId + " has no map for " + userId) + return false + } + + val signature = signatureMap[signKeyId] + + if (null == signature) { + Timber.e("## validateDeviceKeys() : Device " + userId + ":" + deviceKeys.deviceId + " is not signed") + return false + } + + var isVerified = false + var errorMessage: String? = null + + try { + mOlmDevice.verifySignature(signKey, deviceKeys.signalableJSONDictionary(), signature) + isVerified = true + } catch (e: Exception) { + errorMessage = e.message + } + + if (!isVerified) { + Timber.e("## validateDeviceKeys() : Unable to verify signature on device " + userId + ":" + + deviceKeys.deviceId + " with error " + errorMessage) + return false + } + + if (null != previouslyStoredDeviceKeys) { + if (!TextUtils.equals(previouslyStoredDeviceKeys.fingerprint(), signKey)) { + // This should only happen if the list has been MITMed; we are + // best off sticking with the original keys. + // + // Should we warn the user about it somehow? + Timber.e("## validateDeviceKeys() : WARNING:Ed25519 key for device " + userId + ":" + + deviceKeys.deviceId + " has changed : " + + previouslyStoredDeviceKeys.fingerprint() + " -> " + signKey) + + Timber.e("## validateDeviceKeys() : $previouslyStoredDeviceKeys -> $deviceKeys") + Timber.e("## validateDeviceKeys() : " + previouslyStoredDeviceKeys.keys + " -> " + deviceKeys.keys) + + return false + } + } + + return true + } + + /** + * Start device queries for any users who sent us an m.new_device recently + * This method must be called on getEncryptingThreadHandler() thread. + */ + fun refreshOutdatedDeviceLists() { + val users = ArrayList() + + val deviceTrackingStatuses = mCryptoStore.getDeviceTrackingStatuses().toMutableMap() + + for (userId in deviceTrackingStatuses.keys) { + if (TRACKING_STATUS_PENDING_DOWNLOAD == deviceTrackingStatuses[userId]) { + users.add(userId) + } + } + + if (users.size == 0) { + return + } + + if (mIsDownloadingKeys) { + // request already in progress - do nothing. (We will automatically + // make another request if there are more users with outdated + // device lists when the current request completes). + return + } + + // update the statuses + for (userId in users) { + val status = deviceTrackingStatuses[userId] + + if (null != status && TRACKING_STATUS_PENDING_DOWNLOAD == status) { + deviceTrackingStatuses.put(userId, TRACKING_STATUS_DOWNLOAD_IN_PROGRESS) + } + } + + mCryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses) + + doKeyDownloadForUsers(users, object : MatrixCallback> { + override fun onSuccess(data: MXUsersDevicesMap) { + CryptoAsyncHelper.getEncryptBackgroundHandler().post { Timber.d("## refreshOutdatedDeviceLists() : done") } + } + + override fun onFailure(failure: Throwable) { + Timber.e(failure, "## refreshOutdatedDeviceLists() : ERROR updating device keys for users $users") + } + }) + } + + fun setCryptoInternalListener(listener: DeviceListCryptoListener) { + mCryptoListener = listener + } + + + interface DeviceListCryptoListener { + fun hasBeenReleased(): Boolean + } + + companion object { + + /** + * State transition diagram for DeviceList.deviceTrackingStatus + *
+         *
+         *                                   |
+         *        stopTrackingDeviceList     V
+         *      +---------------------> NOT_TRACKED
+         *      |                            |
+         *      +<--------------------+      | startTrackingDeviceList
+         *      |                     |      V
+         *      |   +-------------> PENDING_DOWNLOAD <--------------------+-+
+         *      |   |                      ^ |                            | |
+         *      |   | restart     download | |  start download            | | invalidateUserDeviceList
+         *      |   | client        failed | |                            | |
+         *      |   |                      | V                            | |
+         *      |   +------------ DOWNLOAD_IN_PROGRESS -------------------+ |
+         *      |                    |       |                              |
+         *      +<-------------------+       |  download successful         |
+         *      ^                            V                              |
+         *      +----------------------- UP_TO_DATE ------------------------+
+         *
+         * 
+ */ + + const val TRACKING_STATUS_NOT_TRACKED = -1 + const val TRACKING_STATUS_PENDING_DOWNLOAD = 1 + const val TRACKING_STATUS_DOWNLOAD_IN_PROGRESS = 2 + const val TRACKING_STATUS_UP_TO_DATE = 3 + const val TRACKING_STATUS_UNREACHABLE_SERVER = 4 + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequest.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequest.kt new file mode 100755 index 00000000..63394349 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequest.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2016 OpenMarket Ltd + * 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 im.vector.matrix.android.internal.crypto + + +import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody +import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyShareRequest + +/** + * IncomingRoomKeyRequest class defines the incoming room keys request. + */ +open class IncomingRoomKeyRequest { + /** + * The user id + */ + var mUserId: String? = null + + /** + * The device id + */ + var mDeviceId: String? = null + + /** + * The request id + */ + var mRequestId: String? = null + + /** + * The request body + */ + var mRequestBody: RoomKeyRequestBody? = null + + /** + * The runnable to call to accept to share the keys + */ + @Transient + var mShare: Runnable? = null + + /** + * The runnable to call to ignore the key share request. + */ + @Transient + var mIgnore: Runnable? = null + + /** + * Constructor + * + * @param event the event + */ + constructor(event: Event) { + mUserId = event.sender + + val roomKeyShareRequest = event.content.toModel()!! + mDeviceId = roomKeyShareRequest.requestingDeviceId + mRequestId = roomKeyShareRequest.requestId + mRequestBody = if (null != roomKeyShareRequest.body) roomKeyShareRequest.body else RoomKeyRequestBody() + } + + /** + * Constructor for object creation from crypto store + */ + constructor() +} + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequestCancellation.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequestCancellation.kt new file mode 100755 index 00000000..765bc5cd --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequestCancellation.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2016 OpenMarket Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto + +import im.vector.matrix.android.api.session.events.model.Event + +/** + * IncomingRoomKeyRequestCancellation describes the incoming room key cancellation. + */ +class IncomingRoomKeyRequestCancellation(event: Event) : IncomingRoomKeyRequest(event) { + + init { + mRequestBody = null + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequestManager.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequestManager.kt new file mode 100644 index 00000000..d70e103b --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/IncomingRoomKeyRequestManager.kt @@ -0,0 +1,235 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto + +import android.os.Handler +import android.text.TextUtils +import im.vector.matrix.android.api.auth.data.Credentials +import im.vector.matrix.android.api.session.crypto.keyshare.RoomKeysRequestListener +import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore +import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyShare +import timber.log.Timber +import java.util.* + +internal class IncomingRoomKeyRequestManager( + val mCredentials: Credentials, + val mCryptoStore: IMXCryptoStore, + val mRoomDecryptorProvider: RoomDecryptorProvider) { + + + // list of IncomingRoomKeyRequests/IncomingRoomKeyRequestCancellations + // we received in the current sync. + private val mReceivedRoomKeyRequests = ArrayList() + private val mReceivedRoomKeyRequestCancellations = ArrayList() + + // the listeners + val mRoomKeysRequestListeners: MutableSet = HashSet() + + init { + mReceivedRoomKeyRequests.addAll(mCryptoStore.getPendingIncomingRoomKeyRequests()) + } + + /** + * Called when we get an m.room_key_request event + * This method must be called on getEncryptingThreadHandler() thread. + * + * @param event the announcement event. + */ + fun onRoomKeyRequestEvent(event: Event) { + val roomKeyShare = event.content.toModel()!! + + if (null != roomKeyShare.action) { + when (roomKeyShare.action) { + RoomKeyShare.ACTION_SHARE_REQUEST -> synchronized(mReceivedRoomKeyRequests) { + mReceivedRoomKeyRequests.add(IncomingRoomKeyRequest(event)) + } + RoomKeyShare.ACTION_SHARE_CANCELLATION -> synchronized(mReceivedRoomKeyRequestCancellations) { + mReceivedRoomKeyRequestCancellations.add(IncomingRoomKeyRequestCancellation(event)) + } + else -> Timber.e("## onRoomKeyRequestEvent() : unsupported action " + roomKeyShare.action!!) + } + } + } + + private lateinit var encryptingThreadHandler: Handler + + fun setEncryptingThreadHandler(encryptingThreadHandler: Handler) { + this.encryptingThreadHandler = encryptingThreadHandler + } + + /** + * Process any m.room_key_request events which were queued up during the + * current sync. + */ + fun processReceivedRoomKeyRequests() { + var receivedRoomKeyRequests: List? = null + + synchronized(mReceivedRoomKeyRequests) { + if (!mReceivedRoomKeyRequests.isEmpty()) { + receivedRoomKeyRequests = ArrayList(mReceivedRoomKeyRequests) + mReceivedRoomKeyRequests.clear() + } + } + + if (null != receivedRoomKeyRequests) { + for (request in receivedRoomKeyRequests!!) { + val userId = request.mUserId!! + val deviceId = request.mDeviceId + val body = request.mRequestBody + val roomId = body!!.roomId + val alg = body.algorithm + + Timber.d("m.room_key_request from " + userId + ":" + deviceId + " for " + roomId + " / " + body.sessionId + " id " + request.mRequestId) + + if (!TextUtils.equals(mCredentials.userId, userId)) { + // TODO: determine if we sent this device the keys already: in + Timber.e("## processReceivedRoomKeyRequests() : Ignoring room key request from other user for now") + return + } + + // todo: should we queue up requests we don't yet have keys for, + // in case they turn up later? + + // if we don't have a decryptor for this room/alg, we don't have + // the keys for the requested events, and can drop the requests. + + val decryptor = mRoomDecryptorProvider.getRoomDecryptor(roomId, alg) + + if (null == decryptor) { + Timber.e("## processReceivedRoomKeyRequests() : room key request for unknown $alg in room $roomId") + continue + } + + if (!decryptor.hasKeysForKeyRequest(request)) { + Timber.e("## processReceivedRoomKeyRequests() : room key request for unknown session " + body.sessionId!!) + mCryptoStore.deleteIncomingRoomKeyRequest(request) + continue + } + + if (TextUtils.equals(deviceId, mCredentials.deviceId) && TextUtils.equals(mCredentials.userId, userId)) { + Timber.d("## processReceivedRoomKeyRequests() : oneself device - ignored") + mCryptoStore.deleteIncomingRoomKeyRequest(request) + continue + } + + request.mShare = Runnable { + encryptingThreadHandler.post { + decryptor.shareKeysWithDevice(request) + mCryptoStore.deleteIncomingRoomKeyRequest(request) + } + } + + request.mIgnore = Runnable { encryptingThreadHandler.post { mCryptoStore.deleteIncomingRoomKeyRequest(request) } } + + // if the device is verified already, share the keys + val device = mCryptoStore.getUserDevice(deviceId!!, userId) + + if (null != device) { + if (device.isVerified) { + Timber.d("## processReceivedRoomKeyRequests() : device is already verified: sharing keys") + mCryptoStore.deleteIncomingRoomKeyRequest(request) + request.mShare!!.run() + continue + } + + if (device.isBlocked) { + Timber.d("## processReceivedRoomKeyRequests() : device is blocked -> ignored") + mCryptoStore.deleteIncomingRoomKeyRequest(request) + continue + } + } + + mCryptoStore.storeIncomingRoomKeyRequest(request) + onRoomKeyRequest(request) + } + } + + var receivedRoomKeyRequestCancellations: List? = null + + synchronized(mReceivedRoomKeyRequestCancellations) { + if (!mReceivedRoomKeyRequestCancellations.isEmpty()) { + receivedRoomKeyRequestCancellations = mReceivedRoomKeyRequestCancellations.toList() + mReceivedRoomKeyRequestCancellations.clear() + } + } + + if (null != receivedRoomKeyRequestCancellations) { + for (request in receivedRoomKeyRequestCancellations!!) { + Timber.d("## ## processReceivedRoomKeyRequests() : m.room_key_request cancellation for " + request.mUserId + + ":" + request.mDeviceId + " id " + request.mRequestId) + + // we should probably only notify the app of cancellations we told it + // about, but we don't currently have a record of that, so we just pass + // everything through. + onRoomKeyRequestCancellation(request) + mCryptoStore.deleteIncomingRoomKeyRequest(request) + } + } + } + + /** + * Dispatch onRoomKeyRequest + * + * @param request the request + */ + private fun onRoomKeyRequest(request: IncomingRoomKeyRequest) { + synchronized(mRoomKeysRequestListeners) { + for (listener in mRoomKeysRequestListeners) { + try { + listener.onRoomKeyRequest(request) + } catch (e: Exception) { + Timber.e(e, "## onRoomKeyRequest() failed") + } + + } + } + } + + + /** + * A room key request cancellation has been received. + * + * @param request the cancellation request + */ + private fun onRoomKeyRequestCancellation(request: IncomingRoomKeyRequestCancellation) { + synchronized(mRoomKeysRequestListeners) { + for (listener in mRoomKeysRequestListeners) { + try { + listener.onRoomKeyRequestCancellation(request) + } catch (e: Exception) { + Timber.e(e, "## onRoomKeyRequestCancellation() failed") + } + + } + } + } + + fun addRoomKeysRequestListener(listener: RoomKeysRequestListener) { + synchronized(mRoomKeysRequestListeners) { + mRoomKeysRequestListeners.add(listener) + } + } + + fun removeRoomKeysRequestListener(listener: RoomKeysRequestListener) { + synchronized(mRoomKeysRequestListeners) { + mRoomKeysRequestListeners.remove(listener) + } + } + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXCryptoAlgorithms.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXCryptoAlgorithms.kt new file mode 100755 index 00000000..292839bd --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXCryptoAlgorithms.kt @@ -0,0 +1,98 @@ +/* + * Copyright 2016 OpenMarket Ltd + * 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 im.vector.matrix.android.internal.crypto + +import android.text.TextUtils +import im.vector.matrix.android.internal.crypto.algorithms.IMXDecrypting +import im.vector.matrix.android.internal.crypto.algorithms.IMXEncrypting +import timber.log.Timber +import java.util.* + +internal object MXCryptoAlgorithms { + + // encryptors map + private val mEncryptors: MutableMap> + + // decryptors map + private val mDecryptors: MutableMap> + + init { + mEncryptors = HashMap() + try { + mEncryptors[MXCRYPTO_ALGORITHM_MEGOLM] = Class.forName("im.vector.matrix.android.internal.crypto.algorithms.megolm.MXMegolmEncryption") as Class + } catch (e: Exception) { + Timber.e("## MXCryptoAlgorithms() : fails to add MXCRYPTO_ALGORITHM_MEGOLM") + } + + try { + mEncryptors[MXCRYPTO_ALGORITHM_OLM] = Class.forName("im.vector.matrix.android.internal.crypto.algorithms.olm.MXOlmEncryption") as Class + } catch (e: Exception) { + Timber.e("## MXCryptoAlgorithms() : fails to add MXCRYPTO_ALGORITHM_OLM") + } + + mDecryptors = HashMap() + try { + mDecryptors[MXCRYPTO_ALGORITHM_MEGOLM] = Class.forName("im.vector.matrix.android.internal.crypto.algorithms.megolm.MXMegolmDecryption") as Class + } catch (e: Exception) { + Timber.e("## MXCryptoAlgorithms() : fails to add MXCRYPTO_ALGORITHM_MEGOLM") + } + + try { + mDecryptors[MXCRYPTO_ALGORITHM_OLM] = Class.forName("im.vector.matrix.android.internal.crypto.algorithms.olm.MXOlmDecryption") as Class + } catch (e: Exception) { + Timber.e("## MXCryptoAlgorithms() : fails to add MXCRYPTO_ALGORITHM_OLM") + } + + } + + /** + * Get the class implementing encryption for the provided algorithm. + * + * @param algorithm the algorithm tag. + * @return A class implementing 'IMXEncrypting'. + */ + fun encryptorClassForAlgorithm(algorithm: String?): Class? { + return if (!TextUtils.isEmpty(algorithm)) { + mEncryptors[algorithm] + } else { + null + } + } + + /** + * Get the class implementing decryption for the provided algorithm. + * + * @param algorithm the algorithm tag. + * @return A class implementing 'IMXDecrypting'. + */ + + fun decryptorClassForAlgorithm(algorithm: String?): Class? { + return if (!TextUtils.isEmpty(algorithm)) { + mDecryptors[algorithm] + } else { + null + } + } + + /** + * @return The list of registered algorithms. + */ + fun supportedAlgorithms(): List { + return ArrayList(mEncryptors.keys) + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXCryptoConfig.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXCryptoConfig.kt new file mode 100644 index 00000000..5194928a --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXCryptoConfig.kt @@ -0,0 +1,27 @@ +/* + * 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 im.vector.matrix.android.internal.crypto + +/** + * Class to define the parameters used to customize or configure the end-to-end crypto. + */ +data class MXCryptoConfig( + // Tell whether the encryption of the event content is enabled for the invited members. + // By default, we encrypt messages only for the joined members. + // The encryption for the invited members will be blocked if the history visibility is "joined". + var mEnableEncryptionForInvitedMembers: Boolean = false +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXDecryptionException.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXDecryptionException.kt new file mode 100644 index 00000000..b0d91c21 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXDecryptionException.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2017 Vector Creations Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto + +import im.vector.matrix.android.api.session.crypto.MXCryptoError + +/** + * This class represents a decryption exception + */ +class MXDecryptionException +( + /** + * the linked crypto error + */ + val cryptoError: MXCryptoError? +) : Exception() { + + override val message: String? + get() = cryptoError?.message ?: super.message + + override fun getLocalizedMessage(): String { + return cryptoError?.message ?: super.getLocalizedMessage() + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXEncryptedAttachments.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXEncryptedAttachments.kt new file mode 100755 index 00000000..9d37990b --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXEncryptedAttachments.kt @@ -0,0 +1,269 @@ +/* + * Copyright 2016 OpenMarket Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto + +import android.text.TextUtils +import android.util.Base64 +import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo +import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileKey +import timber.log.Timber +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.InputStream +import java.security.MessageDigest +import java.security.SecureRandom +import java.util.* +import javax.crypto.Cipher +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec + +object MXEncryptedAttachments { + private const val CRYPTO_BUFFER_SIZE = 32 * 1024 + private const val CIPHER_ALGORITHM = "AES/CTR/NoPadding" + private const val SECRET_KEY_SPEC_ALGORITHM = "AES" + private const val MESSAGE_DIGEST_ALGORITHM = "SHA-256" + + /** + * Define the result of an encryption file + */ + data class EncryptionResult( + var mEncryptedFileInfo: EncryptedFileInfo, + var mEncryptedStream: InputStream + ) + + /*** + * Encrypt an attachment stream. + * @param attachmentStream the attachment stream + * @param mimetype the mime type + * @return the encryption file info + */ + fun encryptAttachment(attachmentStream: InputStream, mimetype: String): EncryptionResult? { + val t0 = System.currentTimeMillis() + val secureRandom = SecureRandom() + + // generate a random iv key + // Half of the IV is random, the lower order bits are zeroed + // such that the counter never wraps. + // See https://github.com/matrix-org/matrix-ios-kit/blob/3dc0d8e46b4deb6669ed44f72ad79be56471354c/MatrixKit/Models/Room/MXEncryptedAttachments.m#L75 + val initVectorBytes = ByteArray(16) + Arrays.fill(initVectorBytes, 0.toByte()) + + val ivRandomPart = ByteArray(8) + secureRandom.nextBytes(ivRandomPart) + + System.arraycopy(ivRandomPart, 0, initVectorBytes, 0, ivRandomPart.size) + + val key = ByteArray(32) + secureRandom.nextBytes(key) + + val outStream = ByteArrayOutputStream() + + try { + val encryptCipher = Cipher.getInstance(CIPHER_ALGORITHM) + val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM) + val ivParameterSpec = IvParameterSpec(initVectorBytes) + encryptCipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec) + + val messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM) + + val data = ByteArray(CRYPTO_BUFFER_SIZE) + var read: Int + var encodedBytes: ByteArray + + read = attachmentStream.read(data) + while (read != -1) { + encodedBytes = encryptCipher.update(data, 0, read) + messageDigest.update(encodedBytes, 0, encodedBytes.size) + outStream.write(encodedBytes) + read = attachmentStream.read(data) + } + + // encrypt the latest chunk + encodedBytes = encryptCipher.doFinal() + messageDigest.update(encodedBytes, 0, encodedBytes.size) + outStream.write(encodedBytes) + + val result = EncryptionResult( + mEncryptedFileInfo = EncryptedFileInfo( + url = null, + mimetype = mimetype, + key = EncryptedFileKey( + alg = "A256CTR", + ext = true, + key_ops = listOf("encrypt", "decrypt"), + kty = "oct", + k = base64ToBase64Url(Base64.encodeToString(key, Base64.DEFAULT))!! + ), + iv = Base64.encodeToString(initVectorBytes, Base64.DEFAULT).replace("\n", "").replace("=", ""), + hashes = mapOf("sha256" to base64ToUnpaddedBase64(Base64.encodeToString(messageDigest.digest(), Base64.DEFAULT))!!), + v = "v2" + ), + mEncryptedStream = ByteArrayInputStream(outStream.toByteArray()) + ) + + outStream.close() + + Timber.d("Encrypt in " + (System.currentTimeMillis() - t0) + " ms") + return result + } catch (oom: OutOfMemoryError) { + Timber.e(oom, "## encryptAttachment failed " + oom.message) + } catch (e: Exception) { + Timber.e(e, "## encryptAttachment failed " + e.message) + } + + try { + outStream.close() + } catch (e: Exception) { + Timber.e(e, "## encryptAttachment() : fail to close outStream") + } + + return null + } + + /** + * Decrypt an attachment + * + * @param attachmentStream the attachment stream + * @param encryptedFileInfo the encryption file info + * @return the decrypted attachment stream + */ + fun decryptAttachment(attachmentStream: InputStream?, encryptedFileInfo: EncryptedFileInfo?): InputStream? { + // sanity checks + if (null == attachmentStream || null == encryptedFileInfo) { + Timber.e("## decryptAttachment() : null parameters") + return null + } + + if (TextUtils.isEmpty(encryptedFileInfo.iv) + || null == encryptedFileInfo.key + || null == encryptedFileInfo.hashes + || !encryptedFileInfo.hashes.containsKey("sha256")) { + Timber.e("## decryptAttachment() : some fields are not defined") + return null + } + + if (!TextUtils.equals(encryptedFileInfo.key!!.alg, "A256CTR") + || !TextUtils.equals(encryptedFileInfo.key!!.kty, "oct") + || TextUtils.isEmpty(encryptedFileInfo.key!!.k)) { + Timber.e("## decryptAttachment() : invalid key fields") + return null + } + + // detect if there is no data to decrypt + try { + if (0 == attachmentStream.available()) { + return ByteArrayInputStream(ByteArray(0)) + } + } catch (e: Exception) { + Timber.e(e, "Fail to retrieve the file size") + } + + val t0 = System.currentTimeMillis() + + val outStream = ByteArrayOutputStream() + + try { + val key = Base64.decode(base64UrlToBase64(encryptedFileInfo.key!!.k), Base64.DEFAULT) + val initVectorBytes = Base64.decode(encryptedFileInfo.iv, Base64.DEFAULT) + + val decryptCipher = Cipher.getInstance(CIPHER_ALGORITHM) + val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM) + val ivParameterSpec = IvParameterSpec(initVectorBytes) + decryptCipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec) + + val messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM) + + var read: Int + val data = ByteArray(CRYPTO_BUFFER_SIZE) + var decodedBytes: ByteArray + + read = attachmentStream.read(data) + while (read != -1) { + messageDigest.update(data, 0, read) + decodedBytes = decryptCipher.update(data, 0, read) + outStream.write(decodedBytes) + read = attachmentStream.read(data) + } + + // decrypt the last chunk + decodedBytes = decryptCipher.doFinal() + outStream.write(decodedBytes) + + val currentDigestValue = base64ToUnpaddedBase64(Base64.encodeToString(messageDigest.digest(), Base64.DEFAULT)) + + if (!TextUtils.equals(encryptedFileInfo.hashes["sha256"], currentDigestValue)) { + Timber.e("## decryptAttachment() : Digest value mismatch") + outStream.close() + return null + } + + val decryptedStream = ByteArrayInputStream(outStream.toByteArray()) + outStream.close() + + Timber.d("Decrypt in " + (System.currentTimeMillis() - t0) + " ms") + + return decryptedStream + } catch (oom: OutOfMemoryError) { + Timber.e(oom, "## decryptAttachment() : failed " + oom.message) + } catch (e: Exception) { + Timber.e(e, "## decryptAttachment() : failed " + e.message) + } + + try { + outStream.close() + } catch (closeException: Exception) { + Timber.e(closeException, "## decryptAttachment() : fail to close the file") + } + + return null + } + + /** + * Base64 URL conversion methods + */ + + private fun base64UrlToBase64(base64Url: String?): String? { + var result = base64Url + if (null != result) { + result = result.replace("-".toRegex(), "+") + result = result.replace("_".toRegex(), "/") + } + + return result + } + + private fun base64ToBase64Url(base64: String?): String? { + var result = base64 + if (null != result) { + result = result.replace("\n".toRegex(), "") + result = result.replace("\\+".toRegex(), "-") + result = result.replace("/".toRegex(), "_") + result = result.replace("=".toRegex(), "") + } + return result + } + + private fun base64ToUnpaddedBase64(base64: String?): String? { + var result = base64 + if (null != result) { + result = result.replace("\n".toRegex(), "") + result = result.replace("=".toRegex(), "") + } + + return result + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXEventDecryptionResult.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXEventDecryptionResult.kt new file mode 100644 index 00000000..f8b5f6b6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXEventDecryptionResult.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2017 Vector Creations Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto + +import im.vector.matrix.android.api.session.events.model.Event +import java.util.* + +/** + * The result of a (successful) call to decryptEvent. + */ +data class MXEventDecryptionResult( + + /** + * The plaintext payload for the event (typically containing "type" and "content" fields). + */ + var mClearEvent: Event? = null, + + /** + * Key owned by the sender of this event. + * See MXEvent.senderKey. + */ + var mSenderCurve25519Key: String? = null, + + /** + * Ed25519 key claimed by the sender of this event. + * See MXEvent.claimedEd25519Key. + */ + var mClaimedEd25519Key: String? = null, + + /** + * List of curve25519 keys involved in telling us about the senderCurve25519Key and + * claimedEd25519Key. See MXEvent.forwardingCurve25519KeyChain. + */ + var mForwardingCurve25519KeyChain: List = ArrayList() +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXMegolmExportEncryption.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXMegolmExportEncryption.kt new file mode 100755 index 00000000..68aa5c67 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXMegolmExportEncryption.kt @@ -0,0 +1,373 @@ +/* + * Copyright 2017 OpenMarket Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto + +import android.text.TextUtils +import android.util.Base64 + +import java.io.ByteArrayOutputStream +import java.security.SecureRandom +import java.util.Arrays + +import javax.crypto.Cipher +import javax.crypto.Mac +import javax.crypto.SecretKey +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec + +import timber.log.Timber +import java.nio.charset.Charset +import kotlin.experimental.and +import kotlin.experimental.xor + +/** + * Utility class to import/export the crypto data + */ +object MXMegolmExportEncryption { + private const val HEADER_LINE = "-----BEGIN MEGOLM SESSION DATA-----" + private const val TRAILER_LINE = "-----END MEGOLM SESSION DATA-----" + // we split into lines before base64ing, because encodeBase64 doesn't deal + // terribly well with large arrays. + private const val LINE_LENGTH = 72 * 4 / 3 + + // default iteration count to export the e2e keys + const val DEFAULT_ITERATION_COUNT = 500000 + + /** + * Convert a signed byte to a int value + * + * @param bVal the byte value to convert + * @return the matched int value + */ + private fun byteToInt(bVal: Byte): Int { + return (bVal and 0xFF.toByte()).toInt() + } + + /** + * Extract the AES key from the deriveKeys result. + * + * @param keyBits the deriveKeys result. + * @return the AES key + */ + private fun getAesKey(keyBits: ByteArray): ByteArray { + return Arrays.copyOfRange(keyBits, 0, 32) + } + + /** + * Extract the Hmac key from the deriveKeys result. + * + * @param keyBits the deriveKeys result. + * @return the Hmac key. + */ + private fun getHmacKey(keyBits: ByteArray): ByteArray { + return Arrays.copyOfRange(keyBits, 32, keyBits.size) + } + + /** + * Decrypt a megolm key file + * + * @param data the data to decrypt + * @param password the password. + * @return the decrypted output. + * @throws Exception the failure reason + */ + @Throws(Exception::class) + fun decryptMegolmKeyFile(data: ByteArray, password: String): String { + val body = unpackMegolmKeyFile(data) + + // check we have a version byte + if (null == body || body.size == 0) { + Timber.e("## decryptMegolmKeyFile() : Invalid file: too short") + throw Exception("Invalid file: too short") + } + + val version = body[0] + if (version.toInt() != 1) { + Timber.e("## decryptMegolmKeyFile() : Invalid file: too short") + throw Exception("Unsupported version") + } + + val ciphertextLength = body.size - (1 + 16 + 16 + 4 + 32) + if (ciphertextLength < 0) { + throw Exception("Invalid file: too short") + } + + if (TextUtils.isEmpty(password)) { + throw Exception("Empty password is not supported") + } + + val salt = Arrays.copyOfRange(body, 1, 1 + 16) + val iv = Arrays.copyOfRange(body, 17, 17 + 16) + val iterations = byteToInt(body[33]) shl 24 or (byteToInt(body[34]) shl 16) or (byteToInt(body[35]) shl 8) or byteToInt(body[36]) + val ciphertext = Arrays.copyOfRange(body, 37, 37 + ciphertextLength) + val hmac = Arrays.copyOfRange(body, body.size - 32, body.size) + + val deriveKey = deriveKeys(salt, iterations, password) + + val toVerify = Arrays.copyOfRange(body, 0, body.size - 32) + + val macKey = SecretKeySpec(getHmacKey(deriveKey), "HmacSHA256") + val mac = Mac.getInstance("HmacSHA256") + mac.init(macKey) + val digest = mac.doFinal(toVerify) + + if (!Arrays.equals(hmac, digest)) { + Timber.e("## decryptMegolmKeyFile() : Authentication check failed: incorrect password?") + throw Exception("Authentication check failed: incorrect password?") + } + + val decryptCipher = Cipher.getInstance("AES/CTR/NoPadding") + + val secretKeySpec = SecretKeySpec(getAesKey(deriveKey), "AES") + val ivParameterSpec = IvParameterSpec(iv) + decryptCipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec) + + val outStream = ByteArrayOutputStream() + outStream.write(decryptCipher.update(ciphertext)) + outStream.write(decryptCipher.doFinal()) + + val decodedString = String(outStream.toByteArray(), Charset.defaultCharset()) + outStream.close() + + return decodedString + } + + /** + * Encrypt a string into the megolm export format. + * + * @param data the data to encrypt. + * @param password the password + * @param kdf_rounds the iteration count + * @return the encrypted data + * @throws Exception the failure reason + */ + @Throws(Exception::class) + @JvmOverloads + fun encryptMegolmKeyFile(data: String, password: String, kdf_rounds: Int = DEFAULT_ITERATION_COUNT): ByteArray { + if (TextUtils.isEmpty(password)) { + throw Exception("Empty password is not supported") + } + + val secureRandom = SecureRandom() + + val salt = ByteArray(16) + secureRandom.nextBytes(salt) + + val iv = ByteArray(16) + secureRandom.nextBytes(iv) + + // clear bit 63 of the salt to stop us hitting the 64-bit counter boundary + // (which would mean we wouldn't be able to decrypt on Android). The loss + // of a single bit of salt is a price we have to pay. + iv[9] = iv[9] and 0x7f + + val deriveKey = deriveKeys(salt, kdf_rounds, password) + + val decryptCipher = Cipher.getInstance("AES/CTR/NoPadding") + + val secretKeySpec = SecretKeySpec(getAesKey(deriveKey), "AES") + val ivParameterSpec = IvParameterSpec(iv) + decryptCipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec) + + val outStream = ByteArrayOutputStream() + outStream.write(decryptCipher.update(data.toByteArray(charset("UTF-8")))) + outStream.write(decryptCipher.doFinal()) + + val cipherArray = outStream.toByteArray() + val bodyLength = 1 + salt.size + iv.size + 4 + cipherArray.size + 32 + + val resultBuffer = ByteArray(bodyLength) + var idx = 0 + resultBuffer[idx++] = 1 // version + + System.arraycopy(salt, 0, resultBuffer, idx, salt.size) + idx += salt.size + + System.arraycopy(iv, 0, resultBuffer, idx, iv.size) + idx += iv.size + + resultBuffer[idx++] = (kdf_rounds shr 24 and 0xff).toByte() + resultBuffer[idx++] = (kdf_rounds shr 16 and 0xff).toByte() + resultBuffer[idx++] = (kdf_rounds shr 8 and 0xff).toByte() + resultBuffer[idx++] = (kdf_rounds and 0xff).toByte() + + System.arraycopy(cipherArray, 0, resultBuffer, idx, cipherArray.size) + idx += cipherArray.size + + val toSign = Arrays.copyOfRange(resultBuffer, 0, idx) + + val macKey = SecretKeySpec(getHmacKey(deriveKey), "HmacSHA256") + val mac = Mac.getInstance("HmacSHA256") + mac.init(macKey) + val digest = mac.doFinal(toSign) + System.arraycopy(digest, 0, resultBuffer, idx, digest.size) + + return packMegolmKeyFile(resultBuffer) + } + + /** + * Unbase64 an ascii-armoured megolm key file + * Strips the header and trailer lines, and unbase64s the content + * + * @param data the input data + * @return unbase64ed content + */ + @Throws(Exception::class) + private fun unpackMegolmKeyFile(data: ByteArray): ByteArray? { + val fileStr = String(data, Charset.defaultCharset()) + + // look for the start line + var lineStart = 0 + + while (true) { + val lineEnd = fileStr.indexOf('\n', lineStart) + + if (lineEnd < 0) { + Timber.e("## unpackMegolmKeyFile() : Header line not found") + throw Exception("Header line not found") + } + + val line = fileStr.substring(lineStart, lineEnd).trim { it <= ' ' } + + // start the next line after the newline + lineStart = lineEnd + 1 + + if (TextUtils.equals(line, HEADER_LINE)) { + break + } + } + + val dataStart = lineStart + + // look for the end line + while (true) { + val lineEnd = fileStr.indexOf('\n', lineStart) + val line: String + + if (lineEnd < 0) { + line = fileStr.substring(lineStart).trim { it <= ' ' } + } else { + line = fileStr.substring(lineStart, lineEnd).trim { it <= ' ' } + } + + if (TextUtils.equals(line, TRAILER_LINE)) { + break + } + + if (lineEnd < 0) { + Timber.e("## unpackMegolmKeyFile() : Trailer line not found") + throw Exception("Trailer line not found") + } + + // start the next line after the newline + lineStart = lineEnd + 1 + } + + val dataEnd = lineStart + + // Receiving side + return Base64.decode(fileStr.substring(dataStart, dataEnd), Base64.DEFAULT) + } + + /** + * Pack the megolm data. + * + * @param data the data to pack. + * @return the packed data + * @throws Exception the failure reason. + */ + @Throws(Exception::class) + private fun packMegolmKeyFile(data: ByteArray): ByteArray { + val nLines = (data.size + LINE_LENGTH - 1) / LINE_LENGTH + + val outStream = ByteArrayOutputStream() + outStream.write(HEADER_LINE.toByteArray()) + + var o = 0 + + for (i in 1..nLines) { + outStream.write("\n".toByteArray()) + + val len = Math.min(LINE_LENGTH, data.size - o) + outStream.write(Base64.encode(data, o, len, Base64.DEFAULT)) + o += LINE_LENGTH + } + + outStream.write("\n".toByteArray()) + outStream.write(TRAILER_LINE.toByteArray()) + outStream.write("\n".toByteArray()) + + return outStream.toByteArray() + } + + /** + * Derive the AES and HMAC-SHA-256 keys for the file + * + * @param salt salt for pbkdf + * @param iterations number of pbkdf iterations + * @param password password + * @return the derived keys + */ + @Throws(Exception::class) + private fun deriveKeys(salt: ByteArray, iterations: Int, password: String): ByteArray { + val t0 = System.currentTimeMillis() + + // based on https://en.wikipedia.org/wiki/PBKDF2 algorithm + // it is simpler than the generic algorithm because the expected key length is equal to the mac key length. + // noticed as dklen/hlen + val prf = Mac.getInstance("HmacSHA512") + prf.init(SecretKeySpec(password.toByteArray(charset("UTF-8")), "HmacSHA512")) + + // 512 bits key length + val key = ByteArray(64) + val Uc = ByteArray(64) + + // U1 = PRF(Password, Salt || INT_32_BE(i)) + prf.update(salt) + val int32BE = ByteArray(4) + Arrays.fill(int32BE, 0.toByte()) + int32BE[3] = 1.toByte() + prf.update(int32BE) + prf.doFinal(Uc, 0) + + // copy to the key + System.arraycopy(Uc, 0, key, 0, Uc.size) + + for (index in 2..iterations) { + // Uc = PRF(Password, Uc-1) + prf.update(Uc) + prf.doFinal(Uc, 0) + + // F(Password, Salt, c, i) = U1 ^ U2 ^ ... ^ Uc + for (byteIndex in Uc.indices) { + key[byteIndex] = key[byteIndex] xor Uc[byteIndex] + } + } + + Timber.d("## deriveKeys() : " + iterations + " in " + (System.currentTimeMillis() - t0) + " ms") + + return key + } +} +/** + * Encrypt a string into the megolm export format. + * + * @param data the data to encrypt. + * @param password the password + * @return the encrypted data + * @throws Exception the failure reason + */ \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXOlmDevice.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXOlmDevice.kt new file mode 100755 index 00000000..4c26afca --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXOlmDevice.kt @@ -0,0 +1,814 @@ +/* + * Copyright 2016 OpenMarket Ltd + * 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 im.vector.matrix.android.internal.crypto + +import android.text.TextUtils +import im.vector.matrix.android.api.session.crypto.MXCryptoError +import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.internal.crypto.algorithms.MXDecryptionResult +import im.vector.matrix.android.internal.crypto.model.MXOlmInboundGroupSession2 +import im.vector.matrix.android.internal.crypto.model.MXOlmSession +import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore +import im.vector.matrix.android.internal.di.MoshiProvider +import im.vector.matrix.android.internal.util.convertFromUTF8 +import im.vector.matrix.android.internal.util.convertToUTF8 +import org.matrix.olm.* +import timber.log.Timber +import java.net.URLEncoder +import java.util.* + +// The libolm wrapper. +internal class MXOlmDevice( + /** + * The store where crypto data is saved. + */ + private val mStore: IMXCryptoStore) { + + /** + * @return the Curve25519 key for the account. + */ + var deviceCurve25519Key: String? = null + private set + + /** + * @return the Ed25519 key for the account. + */ + var deviceEd25519Key: String? = null + private set + + // The OLMKit account instance. + private var mOlmAccount: OlmAccount? = null + + // The OLMKit utility instance. + private var mOlmUtility: OlmUtility? = null + + // The outbound group session. + // They are not stored in 'store' to avoid to remember to which devices we sent the session key. + // Plus, in cryptography, it is good to refresh sessions from time to time. + // The key is the session id, the value the outbound group session. + private val mOutboundGroupSessionStore: MutableMap + + // Store a set of decrypted message indexes for each group session. + // This partially mitigates a replay attack where a MITM resends a group + // message into the room. + // + // The Matrix SDK exposes events through MXEventTimelines. A developer can open several + // timelines from a same room so that a message can be decrypted several times but from + // a different timeline. + // So, store these message indexes per timeline id. + // + // The first level keys are timeline ids. + // The second level keys are strings of form "||" + // Values are true. + private val mInboundGroupSessionMessageIndexes: MutableMap> + + /** + * inboundGroupSessionWithId error + */ + private var mInboundGroupSessionWithIdError: MXCryptoError? = null + + init { + // Retrieve the account from the store + mOlmAccount = mStore.getAccount() + + if (null == mOlmAccount) { + Timber.d("MXOlmDevice : create a new olm account") + // Else, create it + try { + mOlmAccount = OlmAccount() + mStore.storeAccount(mOlmAccount!!) + } catch (e: Exception) { + Timber.e(e, "MXOlmDevice : cannot initialize mOlmAccount") + } + + } else { + Timber.d("MXOlmDevice : use an existing account") + } + + try { + mOlmUtility = OlmUtility() + } catch (e: Exception) { + Timber.e(e, "## MXOlmDevice : OlmUtility failed with error") + mOlmUtility = null + } + + mOutboundGroupSessionStore = HashMap() + + try { + deviceCurve25519Key = mOlmAccount!!.identityKeys()[OlmAccount.JSON_KEY_IDENTITY_KEY] + } catch (e: Exception) { + Timber.e(e, "## MXOlmDevice : cannot find " + OlmAccount.JSON_KEY_IDENTITY_KEY + " with error") + } + + try { + deviceEd25519Key = mOlmAccount!!.identityKeys()[OlmAccount.JSON_KEY_FINGER_PRINT_KEY] + } catch (e: Exception) { + Timber.e(e, "## MXOlmDevice : cannot find " + OlmAccount.JSON_KEY_FINGER_PRINT_KEY + " with error") + } + + mInboundGroupSessionMessageIndexes = HashMap() + } + + /** + * @return The current (unused, unpublished) one-time keys for this account. + */ + fun getOneTimeKeys(): Map>? { + try { + return mOlmAccount!!.oneTimeKeys() + } catch (e: Exception) { + Timber.e(e, "## getOneTimeKeys() : failed") + } + + return null + } + + /** + * @return The maximum number of one-time keys the olm account can store. + */ + fun getMaxNumberOfOneTimeKeys(): Long { + return mOlmAccount?.maxOneTimeKeys() ?: -1 + } + + /** + * Release the instance + */ + fun release() { + if (null != mOlmAccount) { + mOlmAccount!!.releaseAccount() + } + } + + /** + * Signs a message with the ed25519 key for this account. + * + * @param message the message to be signed. + * @return the base64-encoded signature. + */ + fun signMessage(message: String): String? { + try { + return mOlmAccount!!.signMessage(message) + } catch (e: Exception) { + Timber.e(e, "## signMessage() : failed") + } + + return null + } + + /** + * Marks all of the one-time keys as published. + */ + fun markKeysAsPublished() { + try { + mOlmAccount!!.markOneTimeKeysAsPublished() + mStore.storeAccount(mOlmAccount!!) + } catch (e: Exception) { + Timber.e(e, "## markKeysAsPublished() : failed") + } + } + + /** + * Generate some new one-time keys + * + * @param numKeys number of keys to generate + */ + fun generateOneTimeKeys(numKeys: Int) { + try { + mOlmAccount!!.generateOneTimeKeys(numKeys) + mStore.storeAccount(mOlmAccount!!) + } catch (e: Exception) { + Timber.e(e, "## generateOneTimeKeys() : failed") + } + + } + + /** + * Generate a new outbound session. + * The new session will be stored in the MXStore. + * + * @param theirIdentityKey the remote user's Curve25519 identity key + * @param theirOneTimeKey the remote user's one-time Curve25519 key + * @return the session id for the outbound session. + */ + fun createOutboundSession(theirIdentityKey: String, theirOneTimeKey: String): String? { + Timber.d("## createOutboundSession() ; theirIdentityKey $theirIdentityKey theirOneTimeKey $theirOneTimeKey") + var olmSession: OlmSession? = null + + try { + olmSession = OlmSession() + olmSession.initOutboundSession(mOlmAccount!!, theirIdentityKey, theirOneTimeKey) + + val mxOlmSession = MXOlmSession(olmSession, 0) + + // Pretend we've received a message at this point, otherwise + // if we try to send a message to the device, it won't use + // this session + mxOlmSession.onMessageReceived() + + mStore.storeSession(mxOlmSession, theirIdentityKey) + + val sessionIdentifier = olmSession.sessionIdentifier() + + Timber.d("## createOutboundSession() ; olmSession.sessionIdentifier: $sessionIdentifier") + return sessionIdentifier + + } catch (e: Exception) { + Timber.e(e, "## createOutboundSession() failed") + + olmSession?.releaseSession() + } + + return null + } + + /** + * Generate a new inbound session, given an incoming message. + * + * @param theirDeviceIdentityKey the remote user's Curve25519 identity key. + * @param messageType the message_type field from the received message (must be 0). + * @param ciphertext base64-encoded body from the received message. + * @return {{payload: string, session_id: string}} decrypted payload, and session id of new session. + */ + fun createInboundSession(theirDeviceIdentityKey: String, messageType: Int, ciphertext: String): Map? { + + Timber.d("## createInboundSession() : theirIdentityKey: $theirDeviceIdentityKey") + + var olmSession: OlmSession? = null + + try { + try { + olmSession = OlmSession() + olmSession.initInboundSessionFrom(mOlmAccount!!, theirDeviceIdentityKey, ciphertext) + } catch (e: Exception) { + Timber.e(e, "## createInboundSession() : the session creation failed") + return null + } + + Timber.d("## createInboundSession() : sessionId: " + olmSession.sessionIdentifier()) + + try { + mOlmAccount!!.removeOneTimeKeys(olmSession) + mStore.storeAccount(mOlmAccount!!) + } catch (e: Exception) { + Timber.e(e, "## createInboundSession() : removeOneTimeKeys failed") + } + + Timber.d("## createInboundSession() : ciphertext: $ciphertext") + try { + Timber.d("## createInboundSession() :ciphertext: SHA256:" + mOlmUtility!!.sha256(URLEncoder.encode(ciphertext, "utf-8"))) + } catch (e: Exception) { + Timber.e(e, "## createInboundSession() :ciphertext: cannot encode ciphertext") + } + + val olmMessage = OlmMessage() + olmMessage.mCipherText = ciphertext + olmMessage.mType = messageType.toLong() + + var payloadString: String? = null + + try { + payloadString = olmSession.decryptMessage(olmMessage) + + val mxOlmSession = MXOlmSession(olmSession, 0) + // This counts as a received message: set last received message time to now + mxOlmSession.onMessageReceived() + + mStore.storeSession(mxOlmSession, theirDeviceIdentityKey) + } catch (e: Exception) { + Timber.e(e, "## createInboundSession() : decryptMessage failed") + } + + val res = HashMap() + + if (!TextUtils.isEmpty(payloadString)) { + res["payload"] = payloadString!! + } + + val sessionIdentifier = olmSession.sessionIdentifier() + + if (!TextUtils.isEmpty(sessionIdentifier)) { + res["session_id"] = sessionIdentifier + } + + return res + } catch (e: Exception) { + Timber.e(e, "## createInboundSession() : OlmSession creation failed") + + olmSession?.releaseSession() + } + + return null + } + + /** + * Get a list of known session IDs for the given device. + * + * @param theirDeviceIdentityKey the Curve25519 identity key for the remote device. + * @return a list of known session ids for the device. + */ + fun getSessionIds(theirDeviceIdentityKey: String): Set? { + return mStore.getDeviceSessionIds(theirDeviceIdentityKey) + } + + /** + * Get the right olm session id for encrypting messages to the given identity key. + * + * @param theirDeviceIdentityKey the Curve25519 identity key for the remote device. + * @return the session id, or null if no established session. + */ + fun getSessionId(theirDeviceIdentityKey: String): String? { + return mStore.getLastUsedSessionId(theirDeviceIdentityKey) + } + + /** + * Encrypt an outgoing message using an existing session. + * + * @param theirDeviceIdentityKey the Curve25519 identity key for the remote device. + * @param sessionId the id of the active session + * @param payloadString the payload to be encrypted and sent + * @return the cipher text + */ + fun encryptMessage(theirDeviceIdentityKey: String, sessionId: String, payloadString: String): Map? { + var res: MutableMap? = null + val olmMessage: OlmMessage + val mxOlmSession = getSessionForDevice(theirDeviceIdentityKey, sessionId) + + if (mxOlmSession != null) { + try { + Timber.d("## encryptMessage() : olmSession.sessionIdentifier: $sessionId") + //Timber.d("## encryptMessage() : payloadString: " + payloadString); + + olmMessage = mxOlmSession.olmSession.encryptMessage(payloadString) + mStore.storeSession(mxOlmSession, theirDeviceIdentityKey) + res = HashMap() + + res["body"] = olmMessage.mCipherText + res["type"] = olmMessage.mType + } catch (e: Exception) { + Timber.e(e, "## encryptMessage() : failed " + e.message) + } + + } + + return res + } + + /** + * Decrypt an incoming message using an existing session. + * + * @param ciphertext the base64-encoded body from the received message. + * @param messageType message_type field from the received message. + * @param theirDeviceIdentityKey the Curve25519 identity key for the remote device. + * @param sessionId the id of the active session. + * @return the decrypted payload. + */ + fun decryptMessage(ciphertext: String, messageType: Int, sessionId: String, theirDeviceIdentityKey: String): String? { + var payloadString: String? = null + + val mxOlmSession = getSessionForDevice(theirDeviceIdentityKey, sessionId) + + if (null != mxOlmSession) { + val olmMessage = OlmMessage() + olmMessage.mCipherText = ciphertext + olmMessage.mType = messageType.toLong() + + try { + payloadString = mxOlmSession.olmSession.decryptMessage(olmMessage) + mxOlmSession.onMessageReceived() + mStore.storeSession(mxOlmSession, theirDeviceIdentityKey) + } catch (e: Exception) { + Timber.e(e, "## decryptMessage() : decryptMessage failed " + e.message) + } + + } + + return payloadString + } + + /** + * Determine if an incoming messages is a prekey message matching an existing session. + * + * @param theirDeviceIdentityKey the Curve25519 identity key for the remote device. + * @param sessionId the id of the active session. + * @param messageType message_type field from the received message. + * @param ciphertext the base64-encoded body from the received message. + * @return YES if the received message is a prekey message which matchesthe given session. + */ + fun matchesSession(theirDeviceIdentityKey: String, sessionId: String, messageType: Int, ciphertext: String): Boolean { + if (messageType != 0) { + return false + } + + val mxOlmSession = getSessionForDevice(theirDeviceIdentityKey, sessionId) + return null != mxOlmSession && mxOlmSession.olmSession.matchesInboundSession(ciphertext) + } + + + // Outbound group session + + /** + * Generate a new outbound group session. + * + * @return the session id for the outbound session. + */ + fun createOutboundGroupSession(): String? { + var session: OlmOutboundGroupSession? = null + try { + session = OlmOutboundGroupSession() + mOutboundGroupSessionStore[session.sessionIdentifier()] = session + return session.sessionIdentifier() + } catch (e: Exception) { + Timber.e(e, "createOutboundGroupSession " + e.message) + + session?.releaseSession() + } + + return null + } + + /** + * Get the current session key of an outbound group session. + * + * @param sessionId the id of the outbound group session. + * @return the base64-encoded secret key. + */ + fun getSessionKey(sessionId: String): String? { + if (!TextUtils.isEmpty(sessionId)) { + try { + return mOutboundGroupSessionStore[sessionId]!!.sessionKey() + } catch (e: Exception) { + Timber.e(e, "## getSessionKey() : failed " + e.message) + } + + } + return null + } + + /** + * Get the current message index of an outbound group session. + * + * @param sessionId the id of the outbound group session. + * @return the current chain index. + */ + fun getMessageIndex(sessionId: String): Int { + return if (!TextUtils.isEmpty(sessionId)) { + mOutboundGroupSessionStore[sessionId]!!.messageIndex() + } else 0 + } + + /** + * Encrypt an outgoing message with an outbound group session. + * + * @param sessionId the id of the outbound group session. + * @param payloadString the payload to be encrypted and sent. + * @return ciphertext + */ + fun encryptGroupMessage(sessionId: String, payloadString: String): String? { + if (!TextUtils.isEmpty(sessionId) && !TextUtils.isEmpty(payloadString)) { + try { + return mOutboundGroupSessionStore[sessionId]!!.encryptMessage(payloadString) + } catch (e: Exception) { + Timber.e(e, "## encryptGroupMessage() : failed " + e.message) + } + + } + return null + } + + // Inbound group session + + /** + * Add an inbound group session to the session store. + * + * @param sessionId the session identifier. + * @param sessionKey base64-encoded secret key. + * @param roomId the id of the room in which this session will be used. + * @param senderKey the base64-encoded curve25519 key of the sender. + * @param forwardingCurve25519KeyChain Devices involved in forwarding this session to us. + * @param keysClaimed Other keys the sender claims. + * @param exportFormat true if the megolm keys are in export format + * @return true if the operation succeeds. + */ + fun addInboundGroupSession(sessionId: String, + sessionKey: String, + roomId: String, + senderKey: String, + forwardingCurve25519KeyChain: List, + keysClaimed: Map, + exportFormat: Boolean): Boolean { + val existingInboundSession = getInboundGroupSession(sessionId, senderKey, roomId) + val session = MXOlmInboundGroupSession2(sessionKey, exportFormat) + + if (null != existingInboundSession) { + // If we already have this session, consider updating it + Timber.e("## addInboundGroupSession() : Update for megolm session $senderKey/$sessionId") + + val existingFirstKnown = existingInboundSession.firstKnownIndex!! + val newKnownFirstIndex = session.firstKnownIndex!! + + //If our existing session is better we keep it + if (newKnownFirstIndex != null && existingFirstKnown <= newKnownFirstIndex) { + if (session.mSession != null) { + session.mSession!!.releaseSession() + } + return false + } + } + + // sanity check + if (null == session.mSession) { + Timber.e("## addInboundGroupSession : invalid session") + return false + } + + try { + if (!TextUtils.equals(session.mSession!!.sessionIdentifier(), sessionId)) { + Timber.e("## addInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey") + session.mSession!!.releaseSession() + return false + } + } catch (e: Exception) { + session.mSession!!.releaseSession() + Timber.e(e, "## addInboundGroupSession : sessionIdentifier() failed") + return false + } + + session.mSenderKey = senderKey + session.mRoomId = roomId + session.mKeysClaimed = keysClaimed + session.mForwardingCurve25519KeyChain = forwardingCurve25519KeyChain + + mStore.storeInboundGroupSessions(listOf(session)) + + return true + } + + /** + * Import an inbound group sessions to the session store. + * + * @param megolmSessionsData the megolm sessions data + * @return the successfully imported sessions. + */ + fun importInboundGroupSessions(megolmSessionsData: List): List { + val sessions = ArrayList(megolmSessionsData.size) + + for (megolmSessionData in megolmSessionsData) { + + val sessionId = megolmSessionData.sessionId + val senderKey = megolmSessionData.senderKey + val roomId = megolmSessionData.roomId + + var session: MXOlmInboundGroupSession2? = null + + try { + session = MXOlmInboundGroupSession2(megolmSessionData) + } catch (e: Exception) { + Timber.e(e, "## importInboundGroupSession() : Update for megolm session $senderKey/$sessionId") + } + + // sanity check + if (null == session || null == session.mSession) { + Timber.e("## importInboundGroupSession : invalid session") + continue + } + + try { + if (!TextUtils.equals(session.mSession!!.sessionIdentifier(), sessionId)) { + Timber.e("## importInboundGroupSession : ERROR: Mismatched group session ID from senderKey: " + senderKey!!) + if (session.mSession != null) session.mSession!!.releaseSession() + continue + } + } catch (e: Exception) { + Timber.e(e, "## importInboundGroupSession : sessionIdentifier() failed") + session.mSession!!.releaseSession() + continue + } + + val existingOlmSession = getInboundGroupSession(sessionId, senderKey, roomId) + if (null != existingOlmSession) { + // If we already have this session, consider updating it + Timber.e("## importInboundGroupSession() : Update for megolm session $senderKey/$sessionId") + + // For now we just ignore updates. TODO: implement something here + if (existingOlmSession.firstKnownIndex!! <= session.firstKnownIndex!!) { + //Ignore this, keep existing + session.mSession!!.releaseSession() + continue + } + } + + sessions.add(session) + } + + mStore.storeInboundGroupSessions(sessions) + + return sessions + } + + /** + * Remove an inbound group session + * + * @param sessionId the session identifier. + * @param sessionKey base64-encoded secret key. + */ + fun removeInboundGroupSession(sessionId: String?, sessionKey: String?) { + if (null != sessionId && null != sessionKey) { + mStore.removeInboundGroupSession(sessionId, sessionKey) + } + } + + /** + * Decrypt a received message with an inbound group session. + * + * @param body the base64-encoded body of the encrypted message. + * @param roomId the room in which the message was received. + * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack. + * @param sessionId the session identifier. + * @param senderKey the base64-encoded curve25519 key of the sender. + * @return the decrypting result. Nil if the sessionId is unknown. + */ + @Throws(MXDecryptionException::class) + fun decryptGroupMessage(body: String, + roomId: String, + timeline: String?, + sessionId: String, + senderKey: String): MXDecryptionResult? { + val result = MXDecryptionResult() + val session = getInboundGroupSession(sessionId, senderKey, roomId) + + if (null != session) { + // Check that the room id matches the original one for the session. This stops + // the HS pretending a message was targeting a different room. + if (TextUtils.equals(roomId, session.mRoomId)) { + var errorMessage = "" + var decryptResult: OlmInboundGroupSession.DecryptMessageResult? = null + try { + decryptResult = session.mSession!!.decryptMessage(body) + } catch (e: Exception) { + Timber.e(e, "## decryptGroupMessage () : decryptMessage failed") + errorMessage = e.message ?: "" + } + + if (null != decryptResult) { + if (null != timeline) { + if (!mInboundGroupSessionMessageIndexes.containsKey(timeline)) { + mInboundGroupSessionMessageIndexes[timeline] = HashMap() + } + + val messageIndexKey = senderKey + "|" + sessionId + "|" + decryptResult.mIndex + + if (null != mInboundGroupSessionMessageIndexes[timeline]!![messageIndexKey]) { + val reason = String.format(MXCryptoError.DUPLICATE_MESSAGE_INDEX_REASON, decryptResult.mIndex) + Timber.e("## decryptGroupMessage() : $reason") + throw MXDecryptionException(MXCryptoError(MXCryptoError.DUPLICATED_MESSAGE_INDEX_ERROR_CODE, + MXCryptoError.UNABLE_TO_DECRYPT, reason)) + } + + mInboundGroupSessionMessageIndexes[timeline]!!.put(messageIndexKey, true) + } + + mStore.storeInboundGroupSessions(listOf(session)) + try { + val moshi = MoshiProvider.providesMoshi() + val adapter = moshi.adapter(Map::class.java) + result.mPayload = adapter.fromJson(convertFromUTF8(decryptResult.mDecryptedMessage)) as Event? + } catch (e: Exception) { + Timber.e(e, "## decryptGroupMessage() : RLEncoder.encode failed " + e.message) + return null + } + + if (null == result.mPayload) { + Timber.e("## decryptGroupMessage() : fails to parse the payload") + return null + } + + result.mKeysClaimed = session.mKeysClaimed + result.mSenderKey = senderKey + result.mForwardingCurve25519KeyChain = session.mForwardingCurve25519KeyChain + } else { + Timber.e("## decryptGroupMessage() : failed to decode the message") + throw MXDecryptionException(MXCryptoError(MXCryptoError.OLM_ERROR_CODE, errorMessage, null)) + } + } else { + val reason = String.format(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_REASON, roomId, session.mRoomId) + Timber.e("## decryptGroupMessage() : $reason") + throw MXDecryptionException(MXCryptoError(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_ERROR_CODE, + MXCryptoError.UNABLE_TO_DECRYPT, reason)) + } + } else { + Timber.e("## decryptGroupMessage() : Cannot retrieve inbound group session $sessionId") + throw MXDecryptionException(mInboundGroupSessionWithIdError) + } + + return result + } + + /** + * Reset replay attack data for the given timeline. + * + * @param timeline the id of the timeline. + */ + fun resetReplayAttackCheckInTimeline(timeline: String?) { + if (null != timeline) { + mInboundGroupSessionMessageIndexes.remove(timeline) + } + } + + // Utilities + + /** + * Verify an ed25519 signature on a JSON object. + * + * @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 + */ + @Throws(Exception::class) + fun verifySignature(key: String, jsonDictionary: Map, signature: String) { + // Check signature on the canonical version of the JSON + mOlmUtility!!.verifyEd25519Signature(signature, key, MoshiProvider.getCanonicalJson>(Map::class.java, jsonDictionary)) + } + + /** + * Calculate the SHA-256 hash of the input and encodes it as base64. + * + * @param message the message to hash. + * @return the base64-encoded hash value. + */ + fun sha256(message: String): String { + return mOlmUtility!!.sha256(convertToUTF8(message)) + } + + /** + * Search an OlmSession + * + * @param theirDeviceIdentityKey the device key + * @param sessionId the session Id + * @return the olm session + */ + private fun getSessionForDevice(theirDeviceIdentityKey: String, sessionId: String): MXOlmSession? { + // sanity check + return if (!TextUtils.isEmpty(theirDeviceIdentityKey) && !TextUtils.isEmpty(sessionId)) { + mStore.getDeviceSession(sessionId, theirDeviceIdentityKey) + } else null + + } + + /** + * Extract an InboundGroupSession from the session store and do some check. + * mInboundGroupSessionWithIdError describes the failure reason. + * + * @param roomId the room where the session is used. + * @param sessionId the session identifier. + * @param senderKey the base64-encoded curve25519 key of the sender. + * @return the inbound group session. + */ + fun getInboundGroupSession(sessionId: String?, senderKey: String?, roomId: String?): MXOlmInboundGroupSession2? { + mInboundGroupSessionWithIdError = null + + val session = mStore.getInboundGroupSession(sessionId!!, senderKey!!) + + if (null != session) { + // Check that the room id matches the original one for the session. This stops + // the HS pretending a message was targeting a different room. + if (!TextUtils.equals(roomId, session.mRoomId)) { + val errorDescription = String.format(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_REASON, roomId, session.mRoomId) + Timber.e("## getInboundGroupSession() : $errorDescription") + mInboundGroupSessionWithIdError = MXCryptoError(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_ERROR_CODE, + MXCryptoError.UNABLE_TO_DECRYPT, errorDescription) + } + } else { + Timber.e("## getInboundGroupSession() : Cannot retrieve inbound group session $sessionId") + mInboundGroupSessionWithIdError = MXCryptoError(MXCryptoError.UNKNOWN_INBOUND_SESSION_ID_ERROR_CODE, + MXCryptoError.UNKNOWN_INBOUND_SESSION_ID_REASON, null) + } + return session + } + + /** + * Determine if we have the keys for a given megolm session. + * + * @param roomId room in which the message was received + * @param senderKey base64-encoded curve25519 key of the sender + * @param sessionId session identifier + * @return true if the unbound session keys are known. + */ + fun hasInboundSessionKeys(roomId: String, senderKey: String, sessionId: String): Boolean { + return null != getInboundGroupSession(sessionId, senderKey, roomId) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXOutgoingRoomKeyRequestManager.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXOutgoingRoomKeyRequestManager.kt new file mode 100755 index 00000000..c314fcba --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXOutgoingRoomKeyRequestManager.kt @@ -0,0 +1,312 @@ +/* + * Copyright 2016 OpenMarket Ltd + * 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 im.vector.matrix.android.internal.crypto + +import android.os.Handler +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore +import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap +import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody +import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyShareCancellation +import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyShareRequest +import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask +import im.vector.matrix.android.internal.task.TaskExecutor +import im.vector.matrix.android.internal.task.configureWith +import timber.log.Timber +import java.util.* + +internal class MXOutgoingRoomKeyRequestManager( + private val mCryptoStore: IMXCryptoStore, + private val mSendToDeviceTask: SendToDeviceTask, + private val mTaskExecutor: TaskExecutor) { + + // working handler (should not be the UI thread) + private lateinit var mWorkingHandler: Handler + + // running + var mClientRunning: Boolean = false + + // transaction counter + private var mTxnCtr: Int = 0 + + // sanity check to ensure that we don't end up with two concurrent runs + // of mSendOutgoingRoomKeyRequestsTimer + private var mSendOutgoingRoomKeyRequestsRunning: Boolean = false + + fun setWorkingHandler(encryptingThreadHandler: Handler) { + mWorkingHandler = encryptingThreadHandler + } + + /** + * Called when the client is started. Sets background processes running. + */ + fun start() { + mClientRunning = true + startTimer() + } + + /** + * Called when the client is stopped. Stops any running background processes. + */ + fun stop() { + mClientRunning = false + } + + /** + * Make up a new transaction id + * + * @return {string} a new, unique, transaction id + */ + private fun makeTxnId(): String { + return "m" + System.currentTimeMillis() + "." + mTxnCtr++ + } + + /** + * Send off a room key request, if we haven't already done so. + * + * + * The `requestBody` is compared (with a deep-equality check) against + * previous queued or sent requests and if it matches, no change is made. + * Otherwise, a request is added to the pending list, and a job is started + * in the background to send it. + * + * @param requestBody requestBody + * @param recipients recipients + */ + fun sendRoomKeyRequest(requestBody: RoomKeyRequestBody?, recipients: List>) { + mWorkingHandler.post { + val req = mCryptoStore.getOrAddOutgoingRoomKeyRequest( + OutgoingRoomKeyRequest(requestBody, recipients, makeTxnId(), OutgoingRoomKeyRequest.RequestState.UNSENT)) + + + if (req!!.mState === OutgoingRoomKeyRequest.RequestState.UNSENT) { + startTimer() + } + } + } + + /** + * Cancel room key requests, if any match the given details + * + * @param requestBody requestBody + */ + fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody) { + cancelRoomKeyRequest(requestBody, false) + } + + /** + * Cancel room key requests, if any match the given details, and resend + * + * @param requestBody requestBody + */ + fun resendRoomKeyRequest(requestBody: RoomKeyRequestBody) { + cancelRoomKeyRequest(requestBody, true) + } + + /** + * Cancel room key requests, if any match the given details, and resend + * + * @param requestBody requestBody + * @param andResend true to resend the key request + */ + private fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody, andResend: Boolean) { + val req = mCryptoStore.getOutgoingRoomKeyRequest(requestBody) + ?: // no request was made for this key + return + + Timber.d("cancelRoomKeyRequest: requestId: " + req.mRequestId + " state: " + req.mState + " andResend: " + andResend) + + if (req.mState === OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING || req.mState === OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING_AND_WILL_RESEND) { + // nothing to do here + } else if (req.mState === OutgoingRoomKeyRequest.RequestState.UNSENT || req.mState === OutgoingRoomKeyRequest.RequestState.FAILED) { + Timber.d("## cancelRoomKeyRequest() : deleting unnecessary room key request for $requestBody") + mCryptoStore.deleteOutgoingRoomKeyRequest(req.mRequestId) + } else if (req.mState === OutgoingRoomKeyRequest.RequestState.SENT) { + if (andResend) { + req.mState = OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING_AND_WILL_RESEND + } else { + req.mState = OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING + } + req.mCancellationTxnId = makeTxnId() + mCryptoStore.updateOutgoingRoomKeyRequest(req) + sendOutgoingRoomKeyRequestCancellation(req) + } + } + + + /** + * Start the background timer to send queued requests, if the timer isn't already running. + */ + private fun startTimer() { + mWorkingHandler.post(Runnable { + if (mSendOutgoingRoomKeyRequestsRunning) { + return@Runnable + } + + mWorkingHandler.postDelayed(Runnable { + if (mSendOutgoingRoomKeyRequestsRunning) { + Timber.d("## startTimer() : RoomKeyRequestSend already in progress!") + return@Runnable + } + + mSendOutgoingRoomKeyRequestsRunning = true + sendOutgoingRoomKeyRequests() + }, SEND_KEY_REQUESTS_DELAY_MS.toLong()) + }) + } + + // look for and send any queued requests. Runs itself recursively until + // there are no more requests, or there is an error (in which case, the + // timer will be restarted before the promise resolves). + private fun sendOutgoingRoomKeyRequests() { + if (!mClientRunning) { + mSendOutgoingRoomKeyRequestsRunning = false + return + } + + Timber.d("## sendOutgoingRoomKeyRequests() : Looking for queued outgoing room key requests") + val outgoingRoomKeyRequest = mCryptoStore.getOutgoingRoomKeyRequestByState( + HashSet(Arrays.asList(OutgoingRoomKeyRequest.RequestState.UNSENT, + OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING, + OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING_AND_WILL_RESEND))) + + if (null == outgoingRoomKeyRequest) { + Timber.e("## sendOutgoingRoomKeyRequests() : No more outgoing room key requests") + mSendOutgoingRoomKeyRequestsRunning = false + return + } + + if (OutgoingRoomKeyRequest.RequestState.UNSENT === outgoingRoomKeyRequest.mState) { + sendOutgoingRoomKeyRequest(outgoingRoomKeyRequest) + } else { + sendOutgoingRoomKeyRequestCancellation(outgoingRoomKeyRequest) + } + } + + /** + * Send the outgoing key request. + * + * @param request the request + */ + private fun sendOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest) { + Timber.d("## sendOutgoingRoomKeyRequest() : Requesting keys " + request.mRequestBody + + " from " + request.mRecipients + " id " + request.mRequestId) + + val requestMessage = RoomKeyShareRequest() + requestMessage.requestingDeviceId = mCryptoStore.getDeviceId() + requestMessage.requestId = request.mRequestId + requestMessage.body = request.mRequestBody + + sendMessageToDevices(requestMessage, request.mRecipients, request.mRequestId, object : MatrixCallback { + private fun onDone(state: OutgoingRoomKeyRequest.RequestState) { + mWorkingHandler.post { + if (request.mState !== OutgoingRoomKeyRequest.RequestState.UNSENT) { + Timber.d("## sendOutgoingRoomKeyRequest() : Cannot update room key request from UNSENT as it was already updated to " + request.mState) + } else { + request.mState = state + mCryptoStore.updateOutgoingRoomKeyRequest(request) + } + + mSendOutgoingRoomKeyRequestsRunning = false + startTimer() + } + } + + override fun onSuccess(data: Unit) { + Timber.d("## sendOutgoingRoomKeyRequest succeed") + onDone(OutgoingRoomKeyRequest.RequestState.SENT) + } + + override fun onFailure(failure: Throwable) { + Timber.e("## sendOutgoingRoomKeyRequest failed") + onDone(OutgoingRoomKeyRequest.RequestState.FAILED) + } + }) + } + + /** + * Given a OutgoingRoomKeyRequest, cancel it and delete the request record + * + * @param request the request + */ + private fun sendOutgoingRoomKeyRequestCancellation(request: OutgoingRoomKeyRequest) { + Timber.d("## sendOutgoingRoomKeyRequestCancellation() : Sending cancellation for key request for " + request.mRequestBody + + " to " + request.mRecipients + + " cancellation id " + request.mCancellationTxnId) + + val roomKeyShareCancellation = RoomKeyShareCancellation() + roomKeyShareCancellation.requestingDeviceId = mCryptoStore.getDeviceId() + roomKeyShareCancellation.requestId = request.mCancellationTxnId + + sendMessageToDevices(roomKeyShareCancellation, request.mRecipients, request.mCancellationTxnId, object : MatrixCallback { + private fun onDone() { + mWorkingHandler.post { + mCryptoStore.deleteOutgoingRoomKeyRequest(request.mRequestId) + mSendOutgoingRoomKeyRequestsRunning = false + startTimer() + } + } + + + override fun onSuccess(data: Unit) { + Timber.d("## sendOutgoingRoomKeyRequestCancellation() : done") + val resend = request.mState === OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING_AND_WILL_RESEND + + onDone() + + // Resend the request with a new ID + if (resend) { + sendRoomKeyRequest(request.mRequestBody, request.mRecipients) + } + } + + override fun onFailure(failure: Throwable) { + Timber.e("## sendOutgoingRoomKeyRequestCancellation failed") + onDone() + } + }) + } + + /** + * Send a SendToDeviceObject to a list of recipients + * + * @param message the message + * @param recipients the recipients. + * @param transactionId the transaction id + * @param callback the asynchronous callback. + */ + private fun sendMessageToDevices(message: Any, + recipients: List>, + transactionId: String?, + callback: MatrixCallback) { + val contentMap = MXUsersDevicesMap() + + for (recipient in recipients) { + contentMap.setObject(message, recipient["userId"], recipient["deviceId"]) + } + + mSendToDeviceTask.configureWith(SendToDeviceTask.Params(EventType.ROOM_KEY_REQUEST, contentMap, transactionId)) + .dispatchTo(callback) + .executeBy(mTaskExecutor) + } + + companion object { + private const val SEND_KEY_REQUESTS_DELAY_MS = 500 + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MegolmSessionData.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MegolmSessionData.kt new file mode 100644 index 00000000..04fb5eb4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MegolmSessionData.kt @@ -0,0 +1,80 @@ +/* + * 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 im.vector.matrix.android.internal.crypto + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.internal.di.MoshiProvider + +/** + * The type of object we use for importing and exporting megolm session data. + */ +@JsonClass(generateAdapter = true) +data class MegolmSessionData( + /** + * The algorithm used. + */ + @Json(name = "algorithm") + var algorithm: String? = null, + + /** + * Unique id for the session. + */ + @Json(name = "session_id") + var sessionId: String? = null, + + /** + * Sender's Curve25519 device key. + */ + @Json(name = "sender_key") + var senderKey: String? = null, + + /** + * Room this session is used in. + */ + @Json(name = "room_id") + var roomId: String? = null, + + /** + * Base64'ed key data. + */ + @Json(name = "session_key") + var sessionKey: String? = null, + + /** + * Other keys the sender claims. + */ + @Json(name = "sender_claimed_keys") + var senderClaimedKeys: Map? = null, + + // This is a shortcut for sender_claimed_keys.get("ed25519") + // Keep it for compatibility reason. + @Json(name = "sender_claimed_ed25519_key") + var senderClaimedEd25519Key: String? = null, + + /** + * Devices which forwarded this session to us (normally empty). + */ + @Json(name = "forwarding_curve25519_key_chain") + var forwardingCurve25519KeyChain: List? = null +) { + + fun toJsonString(): String { + return MoshiProvider.providesMoshi().adapter(MegolmSessionData::class.java).toJson(this) + } +} + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingRoomKeyRequest.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingRoomKeyRequest.kt new file mode 100755 index 00000000..a439fddd --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/OutgoingRoomKeyRequest.kt @@ -0,0 +1,119 @@ +/* + * Copyright 2016 OpenMarket Ltd + * 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 im.vector.matrix.android.internal.crypto + +import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody + +/** + * Represents an outgoing room key request + */ +class OutgoingRoomKeyRequest( + // RequestBody + var mRequestBody: RoomKeyRequestBody?, // list of recipients for the request + var mRecipients: List>, // Unique id for this request. Used for both + // an id within the request for later pairing with a cancellation, and for + // the transaction id when sending the to_device messages to our local + var mRequestId: String, // current state of this request + var mState: RequestState) { + + // transaction id for the cancellation, if any + var mCancellationTxnId: String? = null + + /** + * Used only for log. + * + * @return the room id. + */ + val roomId: String? + get() = if (null != mRequestBody) { + mRequestBody!!.roomId + } else null + + /** + * Used only for log. + * + * @return the session id + */ + val sessionId: String? + get() = if (null != mRequestBody) { + mRequestBody!!.sessionId + } else null + + /** + * possible states for a room key request + * + * + * The state machine looks like: + *
+     *
+     *      |
+     *      V
+     *    UNSENT  -----------------------------+
+     *      |                                  |
+     *      | (send successful)                | (cancellation requested)
+     *      V                                  |
+     *     SENT                                |
+     *      |--------------------------------  |  --------------+
+     *      |                                  |                |
+     *      |                                  |                | (cancellation requested with intent
+     *      |                                  |                | to resend a new request)
+     *      | (cancellation requested)         |                |
+     *      V                                  |                V
+     *  CANCELLATION_PENDING                   | CANCELLATION_PENDING_AND_WILL_RESEND
+     *      |                                  |                |
+     *      | (cancellation sent)              |                | (cancellation sent. Create new request
+     *      |                                  |                |  in the UNSENT state)
+     *      V                                  |                |
+     *  (deleted)  <---------------------------+----------------+
+     *  
+ */ + + enum class RequestState { + /** + * request not yet sent + */ + UNSENT, + /** + * request sent, awaiting reply + */ + SENT, + /** + * reply received, cancellation not yet sent + */ + CANCELLATION_PENDING, + /** + * Cancellation not yet sent, once sent, a new request will be done + */ + CANCELLATION_PENDING_AND_WILL_RESEND, + /** + * sending failed + */ + FAILED; + + companion object { + fun from(state: Int) = when (state) { + 0 -> UNSENT + 1 -> SENT + 2 -> CANCELLATION_PENDING + 3 -> CANCELLATION_PENDING_AND_WILL_RESEND + else /*4*/ -> FAILED + } + } + } +} + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/RoomDecryptorProvider.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/RoomDecryptorProvider.kt new file mode 100644 index 00000000..04a0f596 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/RoomDecryptorProvider.kt @@ -0,0 +1,109 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto + +import android.text.TextUtils +import im.vector.matrix.android.api.auth.data.Credentials +import im.vector.matrix.android.internal.crypto.algorithms.IMXDecrypting +import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask +import im.vector.matrix.android.internal.task.TaskExecutor +import timber.log.Timber +import java.util.* + +internal class RoomDecryptorProvider( + val mCredentials: Credentials, + val olmDevice: MXOlmDevice, + val deviceListManager: DeviceListManager, + val mSendToDeviceTask: SendToDeviceTask, + val mTaskExecutor: TaskExecutor +) { + + // A map from algorithm to MXDecrypting instance, for each room + private val mRoomDecryptors: MutableMap>/* room id */ = HashMap() + + /** + * Get a decryptor for a given room and algorithm. + * If we already have a decryptor for the given room and algorithm, return + * it. Otherwise try to instantiate it. + * + * @param roomId the room id + * @param algorithm the crypto algorithm + * @return the decryptor + * TODO do not provide cryptoManager? + */ + fun getOrCreateRoomDecryptor(cryptoManager: CryptoManager, roomId: String?, algorithm: String?): IMXDecrypting? { + // sanity check + if (TextUtils.isEmpty(algorithm)) { + Timber.e("## getRoomDecryptor() : null algorithm") + return null + } + + if (null == mRoomDecryptors) { + Timber.e("## getRoomDecryptor() : null mRoomDecryptors") + return null + } + + var alg: IMXDecrypting? = null + + if (!TextUtils.isEmpty(roomId)) { + synchronized(mRoomDecryptors) { + if (!mRoomDecryptors.containsKey(roomId)) { + mRoomDecryptors[roomId!!] = HashMap() + } + + alg = mRoomDecryptors[roomId]!![algorithm] + } + + if (null != alg) { + return alg + } + } + + val decryptingClass = MXCryptoAlgorithms.decryptorClassForAlgorithm(algorithm) + + if (null != decryptingClass) { + try { + val ctor = decryptingClass.constructors[0] + alg = ctor.newInstance() as IMXDecrypting + + if (null != alg) { + alg!!.initWithMatrixSession(mCredentials, cryptoManager, olmDevice, deviceListManager, mSendToDeviceTask, mTaskExecutor) + + if (!TextUtils.isEmpty(roomId)) { + synchronized(mRoomDecryptors) { + mRoomDecryptors[roomId]!!.put(algorithm!!, alg!!) + } + } + } + } catch (e: Exception) { + Timber.e(e, "## getRoomDecryptor() : fail to load the class") + return null + } + + } + + return alg + } + + fun getRoomDecryptor(roomId: String?, algorithm: String?): IMXDecrypting? { + if (roomId == null || algorithm == null) { + return null + } + + return mRoomDecryptors[roomId]?.get(algorithm) + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/IMXDecrypting.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/IMXDecrypting.kt new file mode 100644 index 00000000..a8711a1b --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/IMXDecrypting.kt @@ -0,0 +1,84 @@ +/* + * Copyright 2015 OpenMarket Ltd + * 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 im.vector.matrix.android.internal.crypto.algorithms + +import im.vector.matrix.android.api.auth.data.Credentials +import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.internal.crypto.* +import im.vector.matrix.android.internal.crypto.CryptoManager +import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask +import im.vector.matrix.android.internal.task.TaskExecutor + +/** + * An interface for decrypting data + */ +internal interface IMXDecrypting { + + /** + * Init the object fields + * + * @param matrixSession the session + */ + fun initWithMatrixSession(credentials: Credentials, + crypto: CryptoManager, + olmDevice: MXOlmDevice, + deviceListManager: DeviceListManager, + sendToDeviceTask: SendToDeviceTask, + taskExecutor: TaskExecutor) + + /** + * Decrypt an event + * + * @param event the raw event. + * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack. + * @return the decryption information, or null in case of error + * @throws MXDecryptionException the decryption failure reason + */ + @Throws(MXDecryptionException::class) + fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult? + + /** + * Handle a key event. + * + * @param event the key event. + */ + fun onRoomKeyEvent(event: Event) + + /** + * Check if the some messages can be decrypted with a new session + * + * @param senderKey the session sender key + * @param sessionId the session id + */ + fun onNewSession(senderKey: String, sessionId: String) + + /** + * Determine if we have the keys necessary to respond to a room key request + * + * @param request keyRequest + * @return true if we have the keys and could (theoretically) share + */ + fun hasKeysForKeyRequest(request: IncomingRoomKeyRequest): Boolean + + /** + * Send the response to a room key request. + * + * @param request keyRequest + */ + fun shareKeysWithDevice(request: IncomingRoomKeyRequest) +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/IMXEncrypting.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/IMXEncrypting.kt new file mode 100644 index 00000000..dafec264 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/IMXEncrypting.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2015 OpenMarket Ltd + * Copyright 2017 Vector Creations Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto.algorithms + +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.auth.data.Credentials +import im.vector.matrix.android.api.session.events.model.Content +import im.vector.matrix.android.internal.crypto.DeviceListManager +import im.vector.matrix.android.internal.crypto.MXOlmDevice +import im.vector.matrix.android.internal.crypto.CryptoManager +import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask +import im.vector.matrix.android.internal.task.TaskExecutor + +/** + * An interface for encrypting data + */ +internal interface IMXEncrypting { + + /** + * Init + * + * @param matrixSession the related 'MXSession'. + * @param roomId the id of the room we will be sending to. + */ + fun initWithMatrixSession(crypto: CryptoManager, + olmDevice: MXOlmDevice, + deviceListManager: DeviceListManager, + credentials: Credentials, + sendToDeviceTask: SendToDeviceTask, + taskExecutor: TaskExecutor, + roomId: String) + + /** + * Encrypt an event content according to the configuration of the room. + * + * @param eventContent the content of the event. + * @param eventType the type of the event. + * @param userIds the room members the event will be sent to. + * @param callback the asynchronous callback + */ + fun encryptEventContent(eventContent: Content, eventType: String, userIds: List, callback: MatrixCallback) +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/MXDecryptionResult.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/MXDecryptionResult.kt new file mode 100755 index 00000000..186d7459 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/MXDecryptionResult.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2016 OpenMarket Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto.algorithms + +import im.vector.matrix.android.api.session.events.model.Event + +/** + * This class represents the decryption result. + */ +data class MXDecryptionResult( + /** + * The decrypted payload (with properties 'type', 'content') + */ + var mPayload: Event? = null, + + /** + * keys that the sender of the event claims ownership of: + * map from key type to base64-encoded key. + */ + var mKeysClaimed: Map? = null, + + /** + * The curve25519 key that the sender of the event is known to have ownership of. + */ + var mSenderKey: String? = null, + + /** + * Devices which forwarded this session to us (normally empty). + */ + var mForwardingCurve25519KeyChain: List? = null +) \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt new file mode 100644 index 00000000..92cf5786 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt @@ -0,0 +1,422 @@ +/* + * Copyright 2016 OpenMarket Ltd + * 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 im.vector.matrix.android.internal.crypto.algorithms.megolm + +import android.text.TextUtils +import androidx.annotation.Keep +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.auth.data.Credentials +import im.vector.matrix.android.api.session.crypto.MXCryptoError +import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.internal.crypto.* +import im.vector.matrix.android.internal.crypto.algorithms.IMXDecrypting +import im.vector.matrix.android.internal.crypto.algorithms.MXDecryptionResult +import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore +import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo +import im.vector.matrix.android.internal.crypto.model.MXOlmSessionResult +import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap +import im.vector.matrix.android.internal.crypto.CryptoManager +import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent +import im.vector.matrix.android.internal.crypto.model.rest.ForwardedRoomKeyContent +import im.vector.matrix.android.internal.crypto.model.event.RoomKeyContent +import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody +import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask +import im.vector.matrix.android.internal.task.TaskExecutor +import im.vector.matrix.android.internal.task.configureWith +import timber.log.Timber +import java.util.* + +@Keep +internal class MXMegolmDecryption : IMXDecrypting { + /** + * The olm device interface + */ + + // the matrix credentials + private lateinit var mCredentials: Credentials + + private lateinit var mCrypto: CryptoManager + private lateinit var mOlmDevice: MXOlmDevice + private lateinit var mDeviceListManager: DeviceListManager + private lateinit var mCryptoStore: IMXCryptoStore + private lateinit var mSendToDeviceTask: SendToDeviceTask + private lateinit var mTaskExecutor: TaskExecutor + + /** + * Events which we couldn't decrypt due to unknown sessions / indexes: map from + * senderKey|sessionId to timelines to list of MatrixEvents. + */ + private var mPendingEvents: MutableMap>>? = null/* senderKey|sessionId */ + + /** + * Init the object fields + */ + override fun initWithMatrixSession(credentials: Credentials, + crypto: CryptoManager, + olmDevice: MXOlmDevice, + deviceListManager: DeviceListManager, + sendToDeviceTask: SendToDeviceTask, + taskExecutor: TaskExecutor) { + mCredentials = credentials + mDeviceListManager = deviceListManager + mSendToDeviceTask = sendToDeviceTask + mTaskExecutor = taskExecutor + mOlmDevice = olmDevice + mPendingEvents = HashMap() + } + + @Throws(MXDecryptionException::class) + override fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult? { + return decryptEvent(event, timeline, true) + } + + @Throws(MXDecryptionException::class) + private fun decryptEvent(event: Event?, timeline: String, requestKeysOnFail: Boolean): MXEventDecryptionResult? { + // sanity check + if (null == event) { + Timber.e("## decryptEvent() : null event") + return null + } + + val encryptedEventContent = event.content.toModel()!! + + if (TextUtils.isEmpty(encryptedEventContent.senderKey) || TextUtils.isEmpty(encryptedEventContent.sessionId) || TextUtils.isEmpty(encryptedEventContent.ciphertext)) { + throw MXDecryptionException(MXCryptoError(MXCryptoError.MISSING_FIELDS_ERROR_CODE, + MXCryptoError.UNABLE_TO_DECRYPT, MXCryptoError.MISSING_FIELDS_REASON)) + } + + var eventDecryptionResult: MXEventDecryptionResult? = null + var cryptoError: MXCryptoError? = null + var decryptGroupMessageResult: MXDecryptionResult? = null + + try { + decryptGroupMessageResult = mOlmDevice.decryptGroupMessage(encryptedEventContent.ciphertext!!, event.roomId!!, timeline, encryptedEventContent.sessionId!!, encryptedEventContent.senderKey!!) + } catch (e: MXDecryptionException) { + cryptoError = e.cryptoError + } + + // the decryption succeeds + if (null != decryptGroupMessageResult && null != decryptGroupMessageResult.mPayload && null == cryptoError) { + eventDecryptionResult = MXEventDecryptionResult() + + eventDecryptionResult.mClearEvent = decryptGroupMessageResult.mPayload + eventDecryptionResult.mSenderCurve25519Key = decryptGroupMessageResult.mSenderKey + + if (null != decryptGroupMessageResult.mKeysClaimed) { + eventDecryptionResult.mClaimedEd25519Key = decryptGroupMessageResult.mKeysClaimed!!["ed25519"] + } + + eventDecryptionResult.mForwardingCurve25519KeyChain = decryptGroupMessageResult.mForwardingCurve25519KeyChain!! + } else if (null != cryptoError) { + if (cryptoError.isOlmError) { + if (TextUtils.equals("UNKNOWN_MESSAGE_INDEX", cryptoError.message)) { + addEventToPendingList(event, timeline) + + if (requestKeysOnFail) { + requestKeysForEvent(event) + } + } + + val reason = String.format(MXCryptoError.OLM_REASON, cryptoError.message) + val detailedReason = String.format(MXCryptoError.DETAILLED_OLM_REASON, encryptedEventContent.ciphertext, cryptoError.message) + + throw MXDecryptionException(MXCryptoError( + MXCryptoError.OLM_ERROR_CODE, + reason, + detailedReason)) + } else if (TextUtils.equals(cryptoError.code, MXCryptoError.UNKNOWN_INBOUND_SESSION_ID_ERROR_CODE)) { + addEventToPendingList(event, timeline) + if (requestKeysOnFail) { + requestKeysForEvent(event) + } + } + + throw MXDecryptionException(cryptoError) + } + + return eventDecryptionResult + } + + /** + * Helper for the real decryptEvent and for _retryDecryption. If + * requestKeysOnFail is true, we'll send an m.room_key_request when we fail + * to decrypt the event due to missing megolm keys. + * + * @param event the event + */ + private fun requestKeysForEvent(event: Event) { + val sender = event.sender!! + val encryptedEventContent = event.content.toModel()!! + + val recipients = ArrayList>() + + val selfMap = HashMap() + selfMap["userId"] = mCredentials.userId + selfMap["deviceId"] = "*" + recipients.add(selfMap) + + if (!TextUtils.equals(sender, mCredentials.userId)) { + val senderMap = HashMap() + senderMap["userId"] = sender + senderMap["deviceId"] = encryptedEventContent.deviceId!! + recipients.add(senderMap) + } + + val requestBody = RoomKeyRequestBody() + + requestBody.roomId = event.roomId + requestBody.algorithm = encryptedEventContent.algorithm + requestBody.senderKey = encryptedEventContent.senderKey + requestBody.sessionId = encryptedEventContent.sessionId + + mCrypto.requestRoomKey(requestBody, recipients) + } + + /** + * Add an event to the list of those we couldn't decrypt the first time we + * saw them. + * + * @param event the event to try to decrypt later + * @param timelineId the timeline identifier + */ + private fun addEventToPendingList(event: Event, timelineId: String) { + var timelineId = timelineId + val encryptedEventContent = event.content.toModel()!! + + val k = "${encryptedEventContent.senderKey}|${encryptedEventContent.sessionId}" + + // avoid undefined timelineId + if (TextUtils.isEmpty(timelineId)) { + timelineId = "" + } + + if (!mPendingEvents!!.containsKey(k)) { + mPendingEvents!![k] = HashMap() + } + + if (!mPendingEvents!![k]!!.containsKey(timelineId)) { + mPendingEvents!![k]!!.put(timelineId, ArrayList()) + } + + if (mPendingEvents!![k]!![timelineId]!!.indexOf(event) < 0) { + Timber.d("## addEventToPendingList() : add Event " + event.eventId + " in room id " + event.roomId) + mPendingEvents!![k]!![timelineId]!!.add(event) + } + } + + /** + * Handle a key event. + * + * @param roomKeyEvent the key event. + */ + override fun onRoomKeyEvent(event: Event) { + var exportFormat = false + val roomKeyContent = event.content.toModel()!! + + var senderKey: String? = event.getSenderKey() + var keysClaimed: MutableMap = HashMap() + var forwarding_curve25519_key_chain: MutableList? = null + + if (TextUtils.isEmpty(roomKeyContent.roomId) || TextUtils.isEmpty(roomKeyContent.sessionId) || TextUtils.isEmpty(roomKeyContent.sessionKey)) { + Timber.e("## onRoomKeyEvent() : Key event is missing fields") + return + } + + if (TextUtils.equals(event.type, EventType.FORWARDED_ROOM_KEY)) { + Timber.d("## onRoomKeyEvent(), forward adding key : roomId " + roomKeyContent.roomId + " sessionId " + roomKeyContent.sessionId + + " sessionKey " + roomKeyContent.sessionKey) // from " + event); + val forwardedRoomKeyContent = event.content.toModel()!! + + if (null == forwardedRoomKeyContent.forwardingCurve25519KeyChain) { + forwarding_curve25519_key_chain = ArrayList() + } else { + forwarding_curve25519_key_chain = ArrayList(forwardedRoomKeyContent.forwardingCurve25519KeyChain!!) + } + + forwarding_curve25519_key_chain.add(senderKey!!) + + exportFormat = true + senderKey = forwardedRoomKeyContent.senderKey + if (null == senderKey) { + Timber.e("## onRoomKeyEvent() : forwarded_room_key event is missing sender_key field") + return + } + + if (null == forwardedRoomKeyContent.senderClaimedEd25519Key) { + Timber.e("## forwarded_room_key_event is missing sender_claimed_ed25519_key field") + return + } + + keysClaimed["ed25519"] = forwardedRoomKeyContent.senderClaimedEd25519Key!! + } else { + Timber.d("## onRoomKeyEvent(), Adding key : roomId " + roomKeyContent.roomId + " sessionId " + roomKeyContent.sessionId + + " sessionKey " + roomKeyContent.sessionKey) // from " + event); + + if (null == senderKey) { + Timber.e("## onRoomKeyEvent() : key event has no sender key (not encrypted?)") + return + } + + // inherit the claimed ed25519 key from the setup message + keysClaimed = event.getKeysClaimed().toMutableMap() + } + + val added = mOlmDevice.addInboundGroupSession(roomKeyContent.sessionId!!, roomKeyContent.sessionKey!!, roomKeyContent.roomId!!, senderKey, forwarding_curve25519_key_chain!!, keysClaimed, exportFormat) + + if (added) { + mCrypto.getKeysBackupService().maybeBackupKeys() + + val content = RoomKeyRequestBody() + + content.algorithm = roomKeyContent.algorithm + content.roomId = roomKeyContent.roomId + content.sessionId = roomKeyContent.sessionId + content.senderKey = senderKey + + mCrypto.cancelRoomKeyRequest(content) + + onNewSession(senderKey, roomKeyContent.sessionId!!) + } + } + + /** + * Check if the some messages can be decrypted with a new session + * + * @param senderKey the session sender key + * @param sessionId the session id + */ + override fun onNewSession(senderKey: String, sessionId: String) { + val k = "$senderKey|$sessionId" + + val pending = mPendingEvents!![k] + + if (null != pending) { + // Have another go at decrypting events sent with this session. + mPendingEvents!!.remove(k) + + val timelineIds = pending.keys + + for (timelineId in timelineIds) { + val events = pending[timelineId] + + for (event in events!!) { + var result: MXEventDecryptionResult? = null + + try { + result = decryptEvent(event, timelineId) + } catch (e: MXDecryptionException) { + Timber.e(e, "## onNewSession() : Still can't decrypt " + event.eventId + ". Error") + event.setCryptoError(e.cryptoError) + } + + if (null != result) { + val fResut = result + CryptoAsyncHelper.getUiHandler().post { + event.setClearData(fResut) + TODO() + //mSession!!.onEventDecrypted(event) + } + Timber.d("## onNewSession() : successful re-decryption of " + event.eventId) + } + } + } + } + } + + override fun hasKeysForKeyRequest(request: IncomingRoomKeyRequest): Boolean { + return (null != request + && null != request.mRequestBody + && mOlmDevice.hasInboundSessionKeys(request.mRequestBody!!.roomId!!, request.mRequestBody!!.senderKey!!, request.mRequestBody!!.sessionId!!)) + } + + override fun shareKeysWithDevice(request: IncomingRoomKeyRequest) { + // sanity checks + if (null == request || null == request.mRequestBody) { + return + } + + val userId = request.mUserId!! + + mDeviceListManager.downloadKeys(listOf(userId), false, object : MatrixCallback> { + override fun onSuccess(data: MXUsersDevicesMap) { + val deviceId = request.mDeviceId + val deviceInfo = mCryptoStore.getUserDevice(deviceId!!, userId) + + if (null != deviceInfo) { + val body = request.mRequestBody + + val devicesByUser = HashMap>() + devicesByUser[userId] = ArrayList(Arrays.asList(deviceInfo)) + + mCrypto.ensureOlmSessionsForDevices(devicesByUser, object : MatrixCallback> { + override fun onSuccess(map: MXUsersDevicesMap) { + val olmSessionResult = map.getObject(deviceId, userId) + + if (null == olmSessionResult || null == olmSessionResult.mSessionId) { + // no session with this device, probably because there + // were no one-time keys. + // + // ensureOlmSessionsForUsers has already done the logging, + // so just skip it. + return + } + + Timber.d("## shareKeysWithDevice() : sharing keys for session " + body!!.senderKey + "|" + body.sessionId + + " with device " + userId + ":" + deviceId) + + val inboundGroupSession = mOlmDevice.getInboundGroupSession(body.sessionId, body.senderKey, body.roomId) + + val payloadJson = HashMap() + payloadJson["type"] = EventType.FORWARDED_ROOM_KEY + payloadJson["content"] = inboundGroupSession!!.exportKeys()!! + + val encodedPayload = mCrypto.encryptMessage(payloadJson, Arrays.asList(deviceInfo)) + val sendToDeviceMap = MXUsersDevicesMap() + sendToDeviceMap.setObject(encodedPayload, userId, deviceId) + + Timber.d("## shareKeysWithDevice() : sending to $userId:$deviceId") + mSendToDeviceTask.configureWith(SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)) + .dispatchTo(object : MatrixCallback { + override fun onSuccess(data: Unit) { + Timber.d("## shareKeysWithDevice() : sent to $userId:$deviceId") + } + + override fun onFailure(failure: Throwable) { + Timber.e(failure, "## shareKeysWithDevice() : sendToDevice $userId:$deviceId failed") + } + }) + .executeBy(mTaskExecutor) + } + + override fun onFailure(failure: Throwable) { + Timber.e(failure, "## shareKeysWithDevice() : ensureOlmSessionsForDevices $userId:$deviceId failed") + } + }) + } else { + Timber.e("## shareKeysWithDevice() : ensureOlmSessionsForDevices $userId:$deviceId not found") + } + } + + override fun onFailure(failure: Throwable) { + Timber.e(failure, "## shareKeysWithDevice() : downloadKeys $userId failed") + } + }) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt new file mode 100644 index 00000000..d49f9c87 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt @@ -0,0 +1,548 @@ +/* + * Copyright 2015 OpenMarket Ltd + * Copyright 2017 Vector Creations Ltd + * 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 im.vector.matrix.android.internal.crypto.algorithms.megolm + +import android.text.TextUtils +import androidx.annotation.Keep +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.auth.data.Credentials +import im.vector.matrix.android.api.failure.Failure +import im.vector.matrix.android.api.session.crypto.MXCryptoError +import im.vector.matrix.android.api.session.events.model.Content +import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.api.session.events.model.toContent +import im.vector.matrix.android.internal.crypto.CryptoAsyncHelper +import im.vector.matrix.android.internal.crypto.DeviceListManager +import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM +import im.vector.matrix.android.internal.crypto.MXOlmDevice +import im.vector.matrix.android.internal.crypto.algorithms.IMXEncrypting +import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo +import im.vector.matrix.android.internal.crypto.model.MXOlmSessionResult +import im.vector.matrix.android.internal.crypto.model.MXQueuedEncryption +import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap +import im.vector.matrix.android.internal.crypto.CryptoManager +import im.vector.matrix.android.internal.crypto.keysbackup.KeysBackup +import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask +import im.vector.matrix.android.internal.di.MoshiProvider +import im.vector.matrix.android.internal.task.TaskExecutor +import im.vector.matrix.android.internal.task.configureWith +import im.vector.matrix.android.internal.util.convertToUTF8 +import timber.log.Timber +import java.util.* + +@Keep +internal class MXMegolmEncryption : IMXEncrypting { + + private lateinit var mCrypto: CryptoManager + private lateinit var olmDevice: MXOlmDevice + private lateinit var mDeviceListManager: DeviceListManager + + private lateinit var mKeysBackup: KeysBackup + private lateinit var mCredentials: Credentials + private lateinit var mSendToDeviceTask: SendToDeviceTask + private lateinit var mTaskExecutor: TaskExecutor + + // The id of the room we will be sending to. + private var mRoomId: String? = null + + private var mDeviceId: String? = null + + // OutboundSessionInfo. Null if we haven't yet started setting one up. Note + // that even if this is non-null, it may not be ready for use (in which + // case outboundSession.shareOperation will be non-null.) + private var mOutboundSession: MXOutboundSessionInfo? = null + + // true when there is an HTTP operation in progress + private var mShareOperationIsProgress: Boolean = false + + private val mPendingEncryptions = ArrayList() + + // Session rotation periods + private var mSessionRotationPeriodMsgs: Int = 0 + private var mSessionRotationPeriodMs: Int = 0 + + /** + * @return a snapshot of the pending encryptions + */ + private val pendingEncryptions: List + get() { + val list = ArrayList() + + synchronized(mPendingEncryptions) { + list.addAll(mPendingEncryptions) + } + + return list + } + + override fun initWithMatrixSession(crypto: CryptoManager, + olmDevice: MXOlmDevice, + deviceListManager: DeviceListManager, + credentials: Credentials, + sendToDeviceTask: SendToDeviceTask, + taskExecutor: TaskExecutor, + roomId: String) { + mCrypto = crypto + this.olmDevice = olmDevice + mDeviceListManager = deviceListManager + mCredentials = credentials + mSendToDeviceTask = sendToDeviceTask + mTaskExecutor = taskExecutor + + mRoomId = roomId + mDeviceId = mCredentials.deviceId + + // Default rotation periods + // TODO: Make it configurable via parameters + mSessionRotationPeriodMsgs = 100 + mSessionRotationPeriodMs = 7 * 24 * 3600 * 1000 + } + + override fun encryptEventContent(eventContent: Content, + eventType: String, + userIds: List, + callback: MatrixCallback) { + // Queue the encryption request + // It will be processed when everything is set up + val queuedEncryption = MXQueuedEncryption() + + queuedEncryption.mEventContent = eventContent + queuedEncryption.mEventType = eventType + queuedEncryption.mApiCallback = callback + + synchronized(mPendingEncryptions) { + mPendingEncryptions.add(queuedEncryption) + } + + val t0 = System.currentTimeMillis() + Timber.d("## encryptEventContent () starts") + + getDevicesInRoom(userIds, object : MatrixCallback> { + + /** + * A network error has been received while encrypting + * @param failure the exception + */ + private fun dispatchFailure(failure: Throwable) { + Timber.e(failure, "## encryptEventContent() : failure") + val queuedEncryptions = pendingEncryptions + + for (queuedEncryption in queuedEncryptions) { + queuedEncryption.mApiCallback?.onFailure(failure) + } + + synchronized(mPendingEncryptions) { + mPendingEncryptions.removeAll(queuedEncryptions) + } + } + + override fun onSuccess(devicesInRoom: MXUsersDevicesMap) { + ensureOutboundSession(devicesInRoom, object : MatrixCallback { + override fun onSuccess(data: MXOutboundSessionInfo) { + mCrypto!!.encryptingThreadHandler.post { + Timber.d("## encryptEventContent () processPendingEncryptions after " + (System.currentTimeMillis() - t0) + "ms") + processPendingEncryptions(data) + } + } + + override fun onFailure(failure: Throwable) { + dispatchFailure(failure) + } + }) + } + + override fun onFailure(failure: Throwable) { + dispatchFailure(failure) + } + }) + } + + /** + * Prepare a new session. + * + * @return the session description + */ + private fun prepareNewSessionInRoom(): MXOutboundSessionInfo { + val sessionId = olmDevice!!.createOutboundGroupSession() + + val keysClaimedMap = HashMap() + keysClaimedMap["ed25519"] = olmDevice.deviceEd25519Key!! + + olmDevice.addInboundGroupSession(sessionId!!, olmDevice.getSessionKey(sessionId)!!, mRoomId!!, olmDevice.deviceCurve25519Key!!, + ArrayList(), keysClaimedMap, false) + + mKeysBackup.maybeBackupKeys() + + return MXOutboundSessionInfo(sessionId) + } + + /** + * Ensure the outbound session + * + * @param devicesInRoom the devices list + * @param callback the asynchronous callback. + */ + private fun ensureOutboundSession(devicesInRoom: MXUsersDevicesMap, callback: MatrixCallback?) { + var session = mOutboundSession + + if (null == session + // Need to make a brand new session? + || session.needsRotation(mSessionRotationPeriodMsgs, mSessionRotationPeriodMs) + // Determine if we have shared with anyone we shouldn't have + || session.sharedWithTooManyDevices(devicesInRoom)) { + session = prepareNewSessionInRoom() + mOutboundSession = session + } + + if (mShareOperationIsProgress) { + Timber.d("## ensureOutboundSessionInRoom() : already in progress") + // Key share already in progress + return + } + + val fSession = session + + val shareMap = HashMap>()/* userId */ + + val userIds = devicesInRoom.userIds + + for (userId in userIds) { + val deviceIds = devicesInRoom.getUserDeviceIds(userId) + + for (deviceId in deviceIds!!) { + val deviceInfo = devicesInRoom.getObject(deviceId, userId) + + if (null == fSession.mSharedWithDevices.getObject(deviceId, userId)) { + if (!shareMap.containsKey(userId)) { + shareMap[userId] = ArrayList() + } + + shareMap[userId]!!.add(deviceInfo) + } + } + } + + shareKey(fSession, shareMap, object : MatrixCallback { + override fun onSuccess(data: Unit) { + mShareOperationIsProgress = false + callback?.onSuccess(fSession) + } + + override fun onFailure(failure: Throwable) { + Timber.e("## ensureOutboundSessionInRoom() : shareKey onFailure") + + callback?.onFailure(failure) + mShareOperationIsProgress = false + } + }) + + } + + /** + * Share the device key to a list of users + * + * @param session the session info + * @param devicesByUsers the devices map + * @param callback the asynchronous callback + */ + private fun shareKey(session: MXOutboundSessionInfo, + devicesByUsers: MutableMap>, + callback: MatrixCallback?) { + // nothing to send, the task is done + if (0 == devicesByUsers.size) { + Timber.d("## shareKey() : nothing more to do") + + if (null != callback) { + CryptoAsyncHelper.getUiHandler().post { callback.onSuccess(Unit) } + } + + return + } + + // reduce the map size to avoid request timeout when there are too many devices (Users size * devices per user) + val subMap = HashMap>() + + val userIds = ArrayList() + var devicesCount = 0 + + for (userId in devicesByUsers.keys) { + val devicesList = devicesByUsers[userId] + + userIds.add(userId) + subMap[userId] = devicesList!! + + devicesCount += devicesList.size + + if (devicesCount > 100) { + break + } + } + + Timber.d("## shareKey() ; userId $userIds") + shareUserDevicesKey(session, subMap, object : MatrixCallback { + override fun onSuccess(data: Unit) { + mCrypto!!.encryptingThreadHandler.post { + for (userId in userIds) { + devicesByUsers.remove(userId) + } + shareKey(session, devicesByUsers, callback) + } + } + + override fun onFailure(failure: Throwable) { + Timber.e(failure, "## shareKey() ; userIds " + userIds + " failed") + callback?.onFailure(failure) + } + }) + } + + /** + * Share the device keys of a an user + * + * @param session the session info + * @param devicesByUser the devices map + * @param callback the asynchronous callback + */ + private fun shareUserDevicesKey(session: MXOutboundSessionInfo, + devicesByUser: Map>, + callback: MatrixCallback?) { + val sessionKey = olmDevice.getSessionKey(session.mSessionId) + val chainIndex = olmDevice.getMessageIndex(session.mSessionId) + + val submap = HashMap() + submap["algorithm"] = MXCRYPTO_ALGORITHM_MEGOLM + submap["room_id"] = mRoomId!! + submap["session_id"] = session.mSessionId + submap["session_key"] = sessionKey!! + submap["chain_index"] = chainIndex + + val payload = HashMap() + payload["type"] = EventType.ROOM_KEY + payload["content"] = submap + + val t0 = System.currentTimeMillis() + Timber.d("## shareUserDevicesKey() : starts") + + mCrypto!!.ensureOlmSessionsForDevices(devicesByUser, object : MatrixCallback> { + override fun onSuccess(results: MXUsersDevicesMap) { + mCrypto!!.encryptingThreadHandler.post { + Timber.d("## shareUserDevicesKey() : ensureOlmSessionsForDevices succeeds after " + + (System.currentTimeMillis() - t0) + " ms") + val contentMap = MXUsersDevicesMap() + + var haveTargets = false + val userIds = results.userIds + + for (userId in userIds) { + val devicesToShareWith = devicesByUser[userId] + + for ((deviceID) in devicesToShareWith!!) { + + val sessionResult = results.getObject(deviceID, userId) + + if (null == sessionResult || null == sessionResult.mSessionId) { + // no session with this device, probably because there + // were no one-time keys. + // + // we could send them a to_device message anyway, as a + // signal that they have missed out on the key sharing + // message because of the lack of keys, but there's not + // much point in that really; it will mostly serve to clog + // up to_device inboxes. + // + // ensureOlmSessionsForUsers has already done the logging, + // so just skip it. + continue + } + + Timber.d("## shareUserDevicesKey() : Sharing keys with device $userId:$deviceID") + //noinspection ArraysAsListWithZeroOrOneArgument,ArraysAsListWithZeroOrOneArgument + contentMap.setObject(mCrypto!!.encryptMessage(payload, Arrays.asList(sessionResult.mDevice)), userId, deviceID) + haveTargets = true + } + } + + if (haveTargets && !mCrypto!!.hasBeenReleased()) { + val t0 = System.currentTimeMillis() + Timber.d("## shareUserDevicesKey() : has target") + + mSendToDeviceTask.configureWith(SendToDeviceTask.Params(EventType.ENCRYPTED, contentMap)) + .dispatchTo(object : MatrixCallback { + override fun onSuccess(data: Unit) { + mCrypto!!.encryptingThreadHandler.post { + Timber.d("## shareUserDevicesKey() : sendToDevice succeeds after " + + (System.currentTimeMillis() - t0) + " ms") + + // Add the devices we have shared with to session.sharedWithDevices. + // we deliberately iterate over devicesByUser (ie, the devices we + // attempted to share with) rather than the contentMap (those we did + // share with), because we don't want to try to claim a one-time-key + // for dead devices on every message. + for (userId in devicesByUser.keys) { + val devicesToShareWith = devicesByUser[userId] + + for ((deviceId) in devicesToShareWith!!) { + session.mSharedWithDevices.setObject(chainIndex, userId, deviceId) + } + } + + CryptoAsyncHelper.getUiHandler().post { + callback?.onSuccess(Unit) + } + } + } + + override fun onFailure(failure: Throwable) { + Timber.e(failure, "## shareUserDevicesKey() : sendToDevice") + + callback?.onFailure(failure) + } + }) + .executeBy(mTaskExecutor) + } else { + Timber.d("## shareUserDevicesKey() : no need to sharekey") + + if (null != callback) { + CryptoAsyncHelper.getUiHandler().post { callback.onSuccess(Unit) } + } + } + } + } + + override fun onFailure(failure: Throwable) { + Timber.e(failure, "## shareUserDevicesKey() : ensureOlmSessionsForDevices failed") + + callback?.onFailure(failure) + } + }) + } + + /** + * process the pending encryptions + */ + private fun processPendingEncryptions(session: MXOutboundSessionInfo?) { + if (null != session) { + val queuedEncryptions = pendingEncryptions + + // Everything is in place, encrypt all pending events + for (queuedEncryption in queuedEncryptions) { + val payloadJson = HashMap() + + payloadJson["room_id"] = mRoomId!! + payloadJson["type"] = queuedEncryption.mEventType!! + payloadJson["content"] = queuedEncryption.mEventContent!! + + // Get canonical Json from + val content = payloadJson.toContent()!! + + val payloadString = convertToUTF8(MoshiProvider.getCanonicalJson(Map::class.java, content)) + val ciphertext = olmDevice.encryptGroupMessage(session.mSessionId, payloadString!!) + + val map = HashMap() + map["algorithm"] = MXCRYPTO_ALGORITHM_MEGOLM + map["sender_key"] = olmDevice.deviceCurve25519Key!! + map["ciphertext"] = ciphertext!! + map["session_id"] = session.mSessionId + + // Include our device ID so that recipients can send us a + // m.new_device message if they don't have our session key. + map["device_id"] = mDeviceId!! + + CryptoAsyncHelper.getUiHandler().post { queuedEncryption.mApiCallback?.onSuccess(map.toContent()!!) } + + session.mUseCount++ + } + + synchronized(mPendingEncryptions) { + mPendingEncryptions.removeAll(queuedEncryptions) + } + } + } + + /** + * Get the list of devices which can encrypt data to. + * This method must be called in getDecryptingThreadHandler() thread. + * + * @param userIds the user ids whose devices must be checked. + * @param callback the asynchronous callback + */ + private fun getDevicesInRoom(userIds: List, callback: MatrixCallback>) { + // We are happy to use a cached version here: we assume that if we already + // have a list of the user's devices, then we already share an e2e room + // with them, which means that they will have announced any new devices via + // an m.new_device. + mDeviceListManager.downloadKeys(userIds, false, object : MatrixCallback> { + override fun onSuccess(devices: MXUsersDevicesMap) { + mCrypto!!.encryptingThreadHandler.post { + val encryptToVerifiedDevicesOnly = mCrypto!!.globalBlacklistUnverifiedDevices || mCrypto!!.isRoomBlacklistUnverifiedDevices(mRoomId) + + val devicesInRoom = MXUsersDevicesMap() + val unknownDevices = MXUsersDevicesMap() + + val userIds = devices.userIds + + for (userId in userIds) { + val deviceIds = devices.getUserDeviceIds(userId) + + for (deviceId in deviceIds!!) { + val deviceInfo = devices.getObject(deviceId, userId) + + if (mCrypto!!.warnOnUnknownDevices() && deviceInfo!!.isUnknown) { + // The device is not yet known by the user + unknownDevices.setObject(deviceInfo, userId, deviceId) + continue + } + + if (deviceInfo!!.isBlocked) { + // Remove any blocked devices + continue + } + + if (!deviceInfo.isVerified && encryptToVerifiedDevicesOnly) { + continue + } + + if (TextUtils.equals(deviceInfo.identityKey(), olmDevice.deviceCurve25519Key)) { + // Don't bother sending to ourself + continue + } + + devicesInRoom.setObject(deviceInfo, userId, deviceId) + } + } + + CryptoAsyncHelper.getUiHandler().post { + // Check if any of these devices are not yet known to the user. + // if so, warn the user so they can verify or ignore. + if (0 != unknownDevices.map.size) { + callback.onFailure(Failure.CryptoError(MXCryptoError(MXCryptoError.UNKNOWN_DEVICES_CODE, + MXCryptoError.UNABLE_TO_ENCRYPT, MXCryptoError.UNKNOWN_DEVICES_REASON, unknownDevices))) + } else { + callback.onSuccess(devicesInRoom) + } + } + } + } + + override fun onFailure(failure: Throwable) { + callback.onFailure(failure) + } + }) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXOutboundSessionInfo.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXOutboundSessionInfo.kt new file mode 100644 index 00000000..b78b3444 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/megolm/MXOutboundSessionInfo.kt @@ -0,0 +1,76 @@ +/* + * Copyright 2015 OpenMarket Ltd + * Copyright 2017 Vector Creations Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto.algorithms.megolm + +import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo +import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap +import timber.log.Timber + +class MXOutboundSessionInfo( + // The id of the session + val mSessionId: String) { + // When the session was created + private val mCreationTime = System.currentTimeMillis() + + // Number of times this session has been used + var mUseCount: Int = 0 + + // Devices with which we have shared the session key + // userId -> {deviceId -> msgindex} + val mSharedWithDevices: MXUsersDevicesMap = MXUsersDevicesMap() + + fun needsRotation(rotationPeriodMsgs: Int, rotationPeriodMs: Int): Boolean { + var needsRotation = false + val sessionLifetime = System.currentTimeMillis() - mCreationTime + + if (mUseCount >= rotationPeriodMsgs || sessionLifetime >= rotationPeriodMs) { + Timber.d("## needsRotation() : Rotating megolm session after " + mUseCount + ", " + sessionLifetime + "ms") + needsRotation = true + } + + return needsRotation + } + + /** + * Determine if this session has been shared with devices which it shouldn't have been. + * + * @param devicesInRoom the devices map + * @return true if we have shared the session with devices which aren't in devicesInRoom. + */ + fun sharedWithTooManyDevices(devicesInRoom: MXUsersDevicesMap): Boolean { + val userIds = mSharedWithDevices.userIds + + for (userId in userIds) { + if (null == devicesInRoom.getUserDeviceIds(userId)) { + Timber.d("## sharedWithTooManyDevices() : Starting new session because we shared with $userId") + return true + } + + val deviceIds = mSharedWithDevices.getUserDeviceIds(userId) + + for (deviceId in deviceIds!!) { + if (null == devicesInRoom.getObject(deviceId, userId)) { + Timber.d("## sharedWithTooManyDevices() : Starting new session because we shared with $userId:$deviceId") + return true + } + } + } + + return false + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/olm/MXOlmDecryption.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/olm/MXOlmDecryption.kt new file mode 100644 index 00000000..8a9a2c25 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/olm/MXOlmDecryption.kt @@ -0,0 +1,276 @@ +/* + * Copyright 2015 OpenMarket Ltd + * 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 im.vector.matrix.android.internal.crypto.algorithms.olm + +import android.text.TextUtils +import androidx.annotation.Keep +import im.vector.matrix.android.api.auth.data.Credentials +import im.vector.matrix.android.api.session.crypto.MXCryptoError +import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.internal.crypto.* +import im.vector.matrix.android.internal.crypto.algorithms.IMXDecrypting +import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore +import im.vector.matrix.android.internal.crypto.CryptoManager +import im.vector.matrix.android.internal.crypto.model.event.OlmEventContent +import im.vector.matrix.android.internal.crypto.model.event.OlmPayloadContent +import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask +import im.vector.matrix.android.internal.task.TaskExecutor +import im.vector.matrix.android.internal.util.convertFromUTF8 +import timber.log.Timber +import java.util.* + +/** + * An interface for encrypting data + */ +@Keep +internal class MXOlmDecryption : IMXDecrypting { + + // The olm device interface + private var mOlmDevice: MXOlmDevice? = null + + // the matrix credentials + private lateinit var mCredentials: Credentials + + private lateinit var mCrypto: CryptoManager + private lateinit var mCryptoStore: IMXCryptoStore + private lateinit var mSendToDeviceTask: SendToDeviceTask + private lateinit var mTaskExecutor: TaskExecutor + + override fun initWithMatrixSession(credentials: Credentials, + crypto: CryptoManager, + olmDevice: MXOlmDevice, + deviceListManager: DeviceListManager, + sendToDeviceTask: SendToDeviceTask, + taskExecutor: TaskExecutor) { + mCredentials = credentials + mCrypto = crypto + mSendToDeviceTask = sendToDeviceTask + mTaskExecutor = taskExecutor + mOlmDevice = olmDevice + } + + @Throws(MXDecryptionException::class) + override fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult? { + // sanity check + if (null == event) { + Timber.e("## decryptEvent() : null event") + return null + } + + val olmEventContent = event.content.toModel()!! + + if (null == olmEventContent.ciphertext) { + Timber.e("## decryptEvent() : missing cipher text") + throw MXDecryptionException(MXCryptoError(MXCryptoError.MISSING_CIPHER_TEXT_ERROR_CODE, + MXCryptoError.UNABLE_TO_DECRYPT, MXCryptoError.MISSING_CIPHER_TEXT_REASON)) + } + + if (!olmEventContent.ciphertext!!.containsKey(mOlmDevice!!.deviceCurve25519Key)) { + Timber.e("## decryptEvent() : our device " + mOlmDevice!!.deviceCurve25519Key + + " is not included in recipients. Event") + throw MXDecryptionException(MXCryptoError(MXCryptoError.NOT_INCLUDE_IN_RECIPIENTS_ERROR_CODE, + MXCryptoError.UNABLE_TO_DECRYPT, MXCryptoError.NOT_INCLUDED_IN_RECIPIENT_REASON)) + } + + // The message for myUser + val message = olmEventContent.ciphertext!![mOlmDevice!!.deviceCurve25519Key] as Map + val payloadString = decryptMessage(message, olmEventContent.senderKey) + + if (null == payloadString) { + Timber.e("## decryptEvent() Failed to decrypt Olm event (id= " + event.eventId + " ) from " + olmEventContent.senderKey) + throw MXDecryptionException(MXCryptoError(MXCryptoError.BAD_ENCRYPTED_MESSAGE_ERROR_CODE, + MXCryptoError.UNABLE_TO_DECRYPT, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON)) + } + + val payload = convertFromUTF8(payloadString) + + if (null == payload) { + Timber.e("## decryptEvent failed : null payload") + throw MXDecryptionException(MXCryptoError(MXCryptoError.UNABLE_TO_DECRYPT_ERROR_CODE, + MXCryptoError.UNABLE_TO_DECRYPT, MXCryptoError.MISSING_CIPHER_TEXT_REASON)) + } + + val olmPayloadContent = OlmPayloadContent.fromJsonString(payload) + + if (TextUtils.isEmpty(olmPayloadContent.recipient)) { + val reason = String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "recipient") + Timber.e("## decryptEvent() : $reason") + throw MXDecryptionException(MXCryptoError(MXCryptoError.MISSING_PROPERTY_ERROR_CODE, + MXCryptoError.UNABLE_TO_DECRYPT, reason)) + } + + if (!TextUtils.equals(olmPayloadContent.recipient, mCredentials.userId)) { + Timber.e("## decryptEvent() : Event " + event.eventId + ": Intended recipient " + olmPayloadContent.recipient + + " does not match our id " + mCredentials.userId) + throw MXDecryptionException(MXCryptoError(MXCryptoError.BAD_RECIPIENT_ERROR_CODE, + MXCryptoError.UNABLE_TO_DECRYPT, String.format(MXCryptoError.BAD_RECIPIENT_REASON, olmPayloadContent.recipient))) + } + + if (null == olmPayloadContent.recipient_keys) { + Timber.e("## decryptEvent() : Olm event (id=" + event.eventId + + ") contains no " + "'recipient_keys' property; cannot prevent unknown-key attack") + throw MXDecryptionException(MXCryptoError(MXCryptoError.MISSING_PROPERTY_ERROR_CODE, + MXCryptoError.UNABLE_TO_DECRYPT, String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "recipient_keys"))) + } + + val ed25519 = olmPayloadContent.recipient_keys!!.get("ed25519") + + if (!TextUtils.equals(ed25519, mOlmDevice!!.deviceEd25519Key)) { + Timber.e("## decryptEvent() : Event " + event.eventId + ": Intended recipient ed25519 key " + ed25519 + " did not match ours") + throw MXDecryptionException(MXCryptoError(MXCryptoError.BAD_RECIPIENT_KEY_ERROR_CODE, + MXCryptoError.UNABLE_TO_DECRYPT, MXCryptoError.BAD_RECIPIENT_KEY_REASON)) + } + + if (TextUtils.isEmpty(olmPayloadContent.sender)) { + Timber.e("## decryptEvent() : Olm event (id=" + event.eventId + + ") contains no 'sender' property; cannot prevent unknown-key attack") + throw MXDecryptionException(MXCryptoError(MXCryptoError.MISSING_PROPERTY_ERROR_CODE, + MXCryptoError.UNABLE_TO_DECRYPT, String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "sender"))) + } + + if (!TextUtils.equals(olmPayloadContent.sender, event.sender)) { + Timber.e("Event " + event.eventId + ": original sender " + olmPayloadContent.sender + + " does not match reported sender " + event.sender) + throw MXDecryptionException(MXCryptoError(MXCryptoError.FORWARDED_MESSAGE_ERROR_CODE, + MXCryptoError.UNABLE_TO_DECRYPT, String.format(MXCryptoError.FORWARDED_MESSAGE_REASON, olmPayloadContent.sender))) + } + + if (!TextUtils.equals(olmPayloadContent.room_id, event.roomId)) { + Timber.e("## decryptEvent() : Event " + event.eventId + ": original room " + olmPayloadContent.room_id + + " does not match reported room " + event.roomId) + throw MXDecryptionException(MXCryptoError(MXCryptoError.BAD_ROOM_ERROR_CODE, + MXCryptoError.UNABLE_TO_DECRYPT, String.format(MXCryptoError.BAD_ROOM_REASON, olmPayloadContent.room_id))) + } + + if (null == olmPayloadContent.keys) { + Timber.e("## decryptEvent failed : null keys") + throw MXDecryptionException(MXCryptoError(MXCryptoError.UNABLE_TO_DECRYPT_ERROR_CODE, + MXCryptoError.UNABLE_TO_DECRYPT, MXCryptoError.MISSING_CIPHER_TEXT_REASON)) + } + + val result = MXEventDecryptionResult() + // TODO result.mClearEvent = payload + result.mSenderCurve25519Key = olmEventContent.senderKey + result.mClaimedEd25519Key = olmPayloadContent.keys!!.get("ed25519") + + return result + } + + override fun onRoomKeyEvent(event: Event) { + // No impact for olm + } + + override fun onNewSession(senderKey: String, sessionId: String) { + // No impact for olm + } + + override fun hasKeysForKeyRequest(request: IncomingRoomKeyRequest): Boolean { + return false + } + + override fun shareKeysWithDevice(request: IncomingRoomKeyRequest) {} + + /** + * Attempt to decrypt an Olm message. + * + * @param theirDeviceIdentityKey the Curve25519 identity key of the sender. + * @param message message object, with 'type' and 'body' fields. + * @return payload, if decrypted successfully. + */ + private fun decryptMessage(message: Map, theirDeviceIdentityKey: String?): String? { + val sessionIdsSet = mOlmDevice!!.getSessionIds(theirDeviceIdentityKey!!) + + val sessionIds: List + + if (null == sessionIdsSet) { + sessionIds = ArrayList() + } else { + sessionIds = ArrayList(sessionIdsSet) + } + + val messageBody = message["body"] as String? + var messageType: Int? = null + + val typeAsVoid = message["type"] + + if (null != typeAsVoid) { + if (typeAsVoid is Double) { + messageType = typeAsVoid.toInt() + } else if (typeAsVoid is Int) { + messageType = typeAsVoid + } else if (typeAsVoid is Long) { + messageType = typeAsVoid.toInt() + } + } + + if (null == messageBody || null == messageType) { + return null + } + + // Try each session in turn + // decryptionErrors = {}; + for (sessionId in sessionIds) { + val payload = mOlmDevice!!.decryptMessage(messageBody, messageType, sessionId, theirDeviceIdentityKey) + + if (null != payload) { + Timber.d("## decryptMessage() : Decrypted Olm message from $theirDeviceIdentityKey with session $sessionId") + return payload + } else { + val foundSession = mOlmDevice!!.matchesSession(theirDeviceIdentityKey, sessionId, messageType, messageBody) + + if (foundSession) { + // Decryption failed, but it was a prekey message matching this + // session, so it should have worked. + Timber.e("## decryptMessage() : Error decrypting prekey message with existing session id $sessionId:TODO") + return null + } + } + } + + if (messageType != 0) { + // not a prekey message, so it should have matched an existing session, but it + // didn't work. + + if (sessionIds.size == 0) { + Timber.e("## decryptMessage() : No existing sessions") + } else { + Timber.e("## decryptMessage() : Error decrypting non-prekey message with existing sessions") + } + + return null + } + + // prekey message which doesn't match any existing sessions: make a new + // session. + val res = mOlmDevice!!.createInboundSession(theirDeviceIdentityKey, messageType, messageBody) + + if (null == res) { + Timber.e("## decryptMessage() : Error decrypting non-prekey message with existing sessions") + return null + } + + Timber.d("## decryptMessage() : Created new inbound Olm session get id " + res["session_id"] + " with " + theirDeviceIdentityKey) + + return res["payload"] + } + + companion object { + private val LOG_TAG = "MXOlmDecryption" + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/olm/MXOlmEncryption.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/olm/MXOlmEncryption.kt new file mode 100644 index 00000000..1242956b --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/algorithms/olm/MXOlmEncryption.kt @@ -0,0 +1,135 @@ +/* + * Copyright 2015 OpenMarket Ltd + * Copyright 2017 Vector Creations Ltd + * 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 im.vector.matrix.android.internal.crypto.algorithms.olm + +import android.text.TextUtils +import androidx.annotation.Keep +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.auth.data.Credentials +import im.vector.matrix.android.api.session.events.model.Content +import im.vector.matrix.android.api.session.events.model.toContent +import im.vector.matrix.android.internal.crypto.DeviceListManager +import im.vector.matrix.android.internal.crypto.MXOlmDevice +import im.vector.matrix.android.internal.crypto.algorithms.IMXEncrypting +import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo +import im.vector.matrix.android.internal.crypto.model.MXOlmSessionResult +import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap +import im.vector.matrix.android.internal.crypto.CryptoManager +import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask +import im.vector.matrix.android.internal.task.TaskExecutor +import java.util.* + +@Keep +internal class MXOlmEncryption : IMXEncrypting { + private lateinit var mCrypto: CryptoManager + private lateinit var mOlmDevice: MXOlmDevice + private lateinit var mDeviceListManager: DeviceListManager + + private lateinit var mCredentials: Credentials + private lateinit var mSendToDeviceTask: SendToDeviceTask + private lateinit var mTaskExecutor: TaskExecutor + + private lateinit var mRoomId: String + + override fun initWithMatrixSession(crypto: CryptoManager, + olmDevice: MXOlmDevice, + deviceListManager: DeviceListManager, + credentials: Credentials, + sendToDeviceTask: SendToDeviceTask, + taskExecutor: TaskExecutor, + roomId: String) { + mCrypto = crypto + mOlmDevice = olmDevice + mDeviceListManager = deviceListManager + + mRoomId = roomId + } + + override fun encryptEventContent(eventContent: Content, + eventType: String, + userIds: List, + callback: MatrixCallback) { + // pick the list of recipients based on the membership list. + // + // TODO: there is a race condition here! What if a new user turns up + ensureSession(userIds, object : MatrixCallback { + override fun onSuccess(info: Unit) { + val deviceInfos = ArrayList() + + for (userId in userIds) { + val devices = mCrypto.getUserDevices(userId) + + if (null != devices) { + for (device in devices) { + val key = device.identityKey() + + if (TextUtils.equals(key, mOlmDevice.deviceCurve25519Key)) { + // Don't bother setting up session to ourself + continue + } + + if (device.isBlocked) { + // Don't bother setting up sessions with blocked users + continue + } + + deviceInfos.add(device) + } + } + } + + val messageMap = HashMap() + messageMap["room_id"] = mRoomId!! + messageMap["type"] = eventType + messageMap["content"] = eventContent + + mCrypto!!.encryptMessage(messageMap, deviceInfos) + + callback.onSuccess(messageMap.toContent()!!) + } + }) + } + + /** + * Ensure that the session + * + * @param users the user ids list + * @param callback the asynchronous callback + */ + private fun ensureSession(users: List, callback: MatrixCallback?) { + mDeviceListManager.downloadKeys(users, false, object : MatrixCallback> { + + override fun onSuccess(data: MXUsersDevicesMap) { + mCrypto!!.ensureOlmSessionsForUsers(users, object : MatrixCallback> { + override fun onSuccess(data: MXUsersDevicesMap) { + callback?.onSuccess(Unit) + } + + override fun onFailure(failure: Throwable) { + callback?.onFailure(failure) + } + }) + } + + override fun onFailure(failure: Throwable) { + callback?.onFailure(failure) + } + }) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/api/CryptoApi.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/api/CryptoApi.kt new file mode 100644 index 00000000..23e5b466 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/api/CryptoApi.kt @@ -0,0 +1,113 @@ +/* + * Copyright 2014 OpenMarket Ltd + * Copyright 2017 Vector Creations Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.internal.crypto.api + + +import im.vector.matrix.android.internal.crypto.model.rest.* +import im.vector.matrix.android.internal.crypto.model.rest.KeysUploadBody +import im.vector.matrix.android.internal.crypto.model.rest.SendToDeviceBody +import im.vector.matrix.android.internal.network.NetworkConstants +import retrofit2.Call +import retrofit2.http.* + +internal interface CryptoApi { + + /** + * Get the devices list + * Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#get-matrix-client-r0-devices + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "devices") + fun getDevices(): Call + + /** + * Upload device and/or one-time keys. + * Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-keys-upload + * + * @param params the params. + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "keys/upload") + fun uploadKeys(@Body body: KeysUploadBody): Call + + /** + * Upload device and/or one-time keys. + * Doc: not documented + * + * @param deviceId the deviceId + * @param params the params. + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "keys/upload/{deviceId}") + fun uploadKeys(@Path("deviceId") deviceId: String, @Body body: KeysUploadBody): Call + + /** + * Download device keys. + * Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-keys-query + * + * @param params the params. + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "keys/query") + fun downloadKeysForUsers(@Body params: KeysQueryBody): Call + + /** + * Claim one-time keys. + * Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-keys-claim + * + * @param params the params. + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "keys/claim") + fun claimOneTimeKeysForUsersDevices(@Body body: KeysClaimBody): Call + + /** + * Send an event to a specific list of devices + * Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#put-matrix-client-r0-sendtodevice-eventtype-txnid + * + * @param eventType the type of event to send + * @param transactionId the transaction ID for this event + * @param body the body + */ + @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "sendToDevice/{eventType}/{txnId}") + fun sendToDevice(@Path("eventType") eventType: String, @Path("txnId") transactionId: String, @Body body: SendToDeviceBody): Call + + /** + * Delete a device. + * Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#delete-matrix-client-r0-devices-deviceid + * + * @param deviceId the device id + * @param params the deletion parameters + */ + @HTTP(path = NetworkConstants.URI_API_PREFIX_PATH_R0 + "devices/{device_id}", method = "DELETE", hasBody = true) + fun deleteDevice(@Path("device_id") deviceId: String, @Body params: DeleteDeviceParams): Call + + /** + * Update the device information. + * Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#put-matrix-client-r0-devices-deviceid + * + * @param deviceId the device id + * @param params the params + */ + @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "devices/{device_id}") + fun updateDeviceInfo(@Path("device_id") deviceId: String, @Body params: UpdateDeviceInfoBody): Call + + /** + * Get the update devices list from two sync token. + * Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#get-matrix-client-r0-keys-changes + * + * @param oldToken the start token. + * @param newToken the up-to token. + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "keys/changes") + fun getKeyChanges(@Query("from") oldToken: String, @Query("to") newToken: String): Call +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackup.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackup.kt new file mode 100644 index 00000000..6fce19bf --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackup.kt @@ -0,0 +1,1503 @@ +/* + * 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 im.vector.matrix.android.internal.crypto.keysbackup + +import android.os.Handler +import android.os.Looper +import androidx.annotation.UiThread +import androidx.annotation.VisibleForTesting +import androidx.annotation.WorkerThread +import arrow.core.Try +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.auth.data.Credentials +import im.vector.matrix.android.api.failure.Failure +import im.vector.matrix.android.api.failure.MatrixError +import im.vector.matrix.android.api.listeners.ProgressListener +import im.vector.matrix.android.api.listeners.StepProgressListener +import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupService +import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState +import im.vector.matrix.android.internal.crypto.* +import im.vector.matrix.android.internal.crypto.keysbackup.model.KeysBackupVersionTrust +import im.vector.matrix.android.internal.crypto.keysbackup.model.KeysBackupVersionTrustSignature +import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupAuthData +import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo +import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.* +import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore +import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity +import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult +import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo +import im.vector.matrix.android.internal.crypto.model.MXOlmInboundGroupSession2 +import im.vector.matrix.android.internal.crypto.keysbackup.tasks.* +import im.vector.matrix.android.internal.crypto.keysbackup.util.computeRecoveryKey +import im.vector.matrix.android.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey +import im.vector.matrix.android.internal.di.MoshiProvider +import im.vector.matrix.android.internal.task.Task +import im.vector.matrix.android.internal.task.TaskExecutor +import im.vector.matrix.android.internal.task.TaskThread +import im.vector.matrix.android.internal.task.configureWith +import org.matrix.olm.OlmException +import org.matrix.olm.OlmPkDecryption +import org.matrix.olm.OlmPkEncryption +import org.matrix.olm.OlmPkMessage +import timber.log.Timber +import java.security.InvalidParameterException +import java.util.* +import kotlin.collections.HashMap + +/** + * A KeysBackup class instance manage incremental backup of e2e keys (megolm keys) + * to the user's homeserver. + */ +internal class KeysBackup( + private val mCredentials: Credentials, + private val mCryptoStore: IMXCryptoStore, + private val mOlmDevice: MXOlmDevice, + // Tasks + private val mCreateKeysBackupVersionTask: CreateKeysBackupVersionTask, + private val mDeleteBackupTask: DeleteBackupTask, + private val mDeleteRoomSessionDataTask: DeleteRoomSessionDataTask, + private val mDeleteRoomSessionsDataTask: DeleteRoomSessionsDataTask, + private val mDeleteSessionDataTask: DeleteSessionsDataTask, + private val mGetKeysBackupLastVersionTask: GetKeysBackupLastVersionTask, + private val mGetKeysBackupVersionTask: GetKeysBackupVersionTask, + private val mGetRoomSessionDataTask: GetRoomSessionDataTask, + private val mGetRoomSessionsDataTask: GetRoomSessionsDataTask, + private val mGetSessionsDataTask: GetSessionsDataTask, + private val mStoreRoomSessionDataTask: StoreRoomSessionDataTask, + private val mStoreSessionsDataTask: StoreRoomSessionsDataTask, + private val mStoreSessionDataTask: StoreSessionsDataTask, + private val mUpdateKeysBackupVersionTask: UpdateKeysBackupVersionTask, + // Task executor + private val mTaskExecutor: TaskExecutor +) : KeysBackupService { + + private val mUIHandler = Handler(Looper.getMainLooper()) + + private val mKeysBackupStateManager = KeysBackupStateManager(mUIHandler) + + // The backup version + override var mKeysBackupVersion: KeysVersionResult? = null + private set + + // The backup key being used. + private var mBackupKey: OlmPkEncryption? = null + + private val mRandom = Random() + + private var backupAllGroupSessionsCallback: MatrixCallback? = null + + private var mKeysBackupStateListener: KeysBackupService.KeysBackupStateListener? = null + + override val isEnabled: Boolean + get() = mKeysBackupStateManager.isEnabled + + override val isStucked: Boolean + get() = mKeysBackupStateManager.isStucked + + override val state: KeysBackupState + get() = mKeysBackupStateManager.state + + override val currentBackupVersion: String? + get() = mKeysBackupVersion?.version + + // Internal listener + private lateinit var mKeysBackupCryptoListener: KeysBackupCryptoListener + + override fun addListener(listener: KeysBackupService.KeysBackupStateListener) { + mKeysBackupStateManager.addListener(listener) + } + + override fun removeListener(listener: KeysBackupService.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 [createKeysBackupVersion] + * is called. + * The returned [MegolmBackupCreationInfo] object has a `recoveryKey` member with + * the user-facing recovery key string. + * + * @param password an optional passphrase string that can be entered by the user + * when restoring the backup as an alternative to entering the recovery key. + * @param progressListener a progress listener, as generating private key from password may take a while + * @param callback Asynchronous callback + */ + override fun prepareKeysBackupVersion(password: String?, + progressListener: ProgressListener?, + callback: MatrixCallback) { + CryptoAsyncHelper.getDecryptBackgroundHandler().post { + try { + val olmPkDecryption = OlmPkDecryption() + val megolmBackupAuthData = MegolmBackupAuthData() + + if (password != null) { + // Generate a private key from the password + val backgroundProgressListener = if (progressListener == null) { + null + } else { + object : ProgressListener { + override fun onProgress(progress: Int, total: Int) { + mUIHandler.post { + try { + progressListener.onProgress(progress, total) + } catch (e: Exception) { + Timber.e(e, "prepareKeysBackupVersion: onProgress failure") + } + } + } + } + } + + val generatePrivateKeyResult = generatePrivateKeyWithPassword(password, backgroundProgressListener) + megolmBackupAuthData.publicKey = olmPkDecryption.setPrivateKey(generatePrivateKeyResult.privateKey) + megolmBackupAuthData.privateKeySalt = generatePrivateKeyResult.salt + megolmBackupAuthData.privateKeyIterations = generatePrivateKeyResult.iterations + } else { + val publicKey = olmPkDecryption.generateKey() + + megolmBackupAuthData.publicKey = publicKey + } + + val canonicalJson = MoshiProvider.getCanonicalJson(Map::class.java, megolmBackupAuthData.signalableJSONDictionary()) + + megolmBackupAuthData.signatures = mKeysBackupCryptoListener.signObject(canonicalJson) + + + val megolmBackupCreationInfo = MegolmBackupCreationInfo() + megolmBackupCreationInfo.algorithm = MXCRYPTO_ALGORITHM_MEGOLM_BACKUP + megolmBackupCreationInfo.authData = megolmBackupAuthData + megolmBackupCreationInfo.recoveryKey = computeRecoveryKey(olmPkDecryption.privateKey()) + + mUIHandler.post { callback.onSuccess(megolmBackupCreationInfo) } + } catch (e: OlmException) { + Timber.e(e, "OlmException") + + mUIHandler.post { callback.onFailure(e) } + } + } + } + + /** + * Create a new keys backup version and enable it, using the information return from [prepareKeysBackupVersion]. + * + * @param keysBackupCreationInfo the info object from [prepareKeysBackupVersion]. + * @param callback Asynchronous callback + */ + override fun createKeysBackupVersion(keysBackupCreationInfo: MegolmBackupCreationInfo, + callback: MatrixCallback) { + val createKeysBackupVersionBody = CreateKeysBackupVersionBody() + createKeysBackupVersionBody.algorithm = keysBackupCreationInfo.algorithm + createKeysBackupVersionBody.authData = MoshiProvider.providesMoshi().adapter(Map::class.java) + .fromJson(keysBackupCreationInfo.authData?.toJsonString()) as Map? + + mKeysBackupStateManager.state = KeysBackupState.Enabling + + mCreateKeysBackupVersionTask + .configureWith(createKeysBackupVersionBody) + .dispatchTo(object : MatrixCallback { + override fun onSuccess(info: KeysVersion) { + // Reset backup markers. + mCryptoStore.resetBackupMarkers() + + val keyBackupVersion = KeysVersionResult() + keyBackupVersion.algorithm = createKeysBackupVersionBody.algorithm + keyBackupVersion.authData = createKeysBackupVersionBody.authData + keyBackupVersion.version = info.version + + // We can consider that the server does not have keys yet + keyBackupVersion.count = 0 + keyBackupVersion.hash = null + + enableKeysBackup(keyBackupVersion) + + callback.onSuccess(info) + } + + override fun onFailure(failure: Throwable) { + mKeysBackupStateManager.state = KeysBackupState.Disabled + callback.onFailure(failure) + } + }) + .executeBy(mTaskExecutor) + } + + /** + * Delete a keys backup version. It will delete all backed up keys on the server, and the backup itself. + * If we are backing up to this version. Backup will be stopped. + * + * @param version the backup version to delete. + * @param callback Asynchronous callback + */ + override fun deleteBackup(version: String, callback: MatrixCallback?) { + CryptoAsyncHelper.getDecryptBackgroundHandler().post { + // If we're currently backing up to this backup... stop. + // (We start using it automatically in createKeysBackupVersion so this is symmetrical). + if (mKeysBackupVersion != null && version == mKeysBackupVersion!!.version) { + resetKeysBackupData() + mKeysBackupVersion = null + mKeysBackupStateManager.state = KeysBackupState.Unknown + } + + mDeleteBackupTask.configureWith(DeleteBackupTask.Params(version)) + .dispatchTo(object : MatrixCallback { + private fun eventuallyRestartBackup() { + // Do not stay in KeysBackupState.Unknown but check what is available on the homeserver + if (state == KeysBackupState.Unknown) { + checkAndStartKeysBackup() + } + } + + override fun onSuccess(data: Unit) { + eventuallyRestartBackup() + + mUIHandler.post { callback?.onSuccess(Unit) } + } + + override fun onFailure(failure: Throwable) { + eventuallyRestartBackup() + + mUIHandler.post { callback?.onFailure(failure) } + } + }) + .executeBy(mTaskExecutor) + } + } + + /** + * Ask if the backup on the server contains keys that we may do not have locally. + * This should be called when entering in the state READY_TO_BACKUP + */ + override fun canRestoreKeys(): Boolean { + // Server contains more keys than locally + val totalNumberOfKeysLocally = getTotalNumbersOfKeys() + + val keysBackupData = mCryptoStore.getKeysBackupData() + + val totalNumberOfKeysServer = keysBackupData?.backupLastServerNumberOfKeys ?: -1 + val hashServer = keysBackupData?.backupLastServerHash + + return when { + totalNumberOfKeysLocally < totalNumberOfKeysServer -> { + // Server contains more keys than this device + true + } + totalNumberOfKeysLocally == totalNumberOfKeysServer -> { + // Same number, compare hash? + // TODO We have not found any algorithm to determine if a restore is recommended here. Return false for the moment + false + } + else -> false + } + } + + /** + * Facility method to get the total number of locally stored keys + */ + override fun getTotalNumbersOfKeys(): Int { + return mCryptoStore.inboundGroupSessionsCount(false) + + } + + /** + * Facility method to get the number of backed up keys + */ + override fun getTotalNumbersOfBackedUpKeys(): Int { + return mCryptoStore.inboundGroupSessionsCount(true) + } + + /** + * Start to back up keys immediately. + * + * @param progressListener the callback to follow the progress + * @param callback the main callback + */ + override fun backupAllGroupSessions(progressListener: ProgressListener?, + callback: MatrixCallback?) { + // Get a status right now + getBackupProgress(object : ProgressListener { + override fun onProgress(progress: Int, total: Int) { + // Reset previous listeners if any + resetBackupAllGroupSessionsListeners() + Timber.d("backupAllGroupSessions: backupProgress: $progress/$total") + try { + progressListener?.onProgress(progress, total) + } catch (e: Exception) { + Timber.e(e, "backupAllGroupSessions: onProgress failure") + } + + if (progress == total) { + Timber.d("backupAllGroupSessions: complete") + callback?.onSuccess(Unit) + return + } + + backupAllGroupSessionsCallback = callback + + // Listen to `state` change to determine when to call onBackupProgress and onComplete + mKeysBackupStateListener = object : KeysBackupService.KeysBackupStateListener { + override fun onStateChange(newState: KeysBackupState) { + getBackupProgress(object : ProgressListener { + override fun onProgress(progress: Int, total: Int) { + try { + progressListener?.onProgress(progress, total) + } catch (e: Exception) { + Timber.e(e, "backupAllGroupSessions: onProgress failure 2") + } + + // If backup is finished, notify the main listener + if (state === KeysBackupState.ReadyToBackUp) { + backupAllGroupSessionsCallback?.onSuccess(Unit) + resetBackupAllGroupSessionsListeners() + } + } + }) + } + } + + mKeysBackupStateManager.addListener(mKeysBackupStateListener!!) + + backupKeys() + } + }) + } + + /** + * Check trust on a key backup version. + * + * @param keysBackupVersion the backup version to check. + * @param callback block called when the operations completes. + */ + override fun getKeysBackupTrust(keysBackupVersion: KeysVersionResult, + callback: MatrixCallback) { + // TODO Validate with François that this is correct + object : Task { + override fun execute(params: KeysVersionResult): Try { + return Try { + getKeysBackupTrustBg(params) + } + } + } + .configureWith(keysBackupVersion) + .dispatchTo(callback) + .executeOn(TaskThread.COMPUTATION) + .executeBy(mTaskExecutor) + } + + /** + * Check trust on a key backup version. + * This has to be called on background thread. + * + * @param keysBackupVersion the backup version to check. + * @return a KeysBackupVersionTrust object + */ + @WorkerThread + private fun getKeysBackupTrustBg(keysBackupVersion: KeysVersionResult): KeysBackupVersionTrust { + val myUserId = mCredentials.userId + + val keysBackupVersionTrust = KeysBackupVersionTrust() + val authData = keysBackupVersion.getAuthDataAsMegolmBackupAuthData() + + if (keysBackupVersion.algorithm == null + || authData == null + || authData.publicKey.isEmpty() + || authData.signatures.isNullOrEmpty()) { + Timber.d("getKeysBackupTrust: Key backup is absent or missing required data") + return keysBackupVersionTrust + } + + val mySigs: Map = authData.signatures!![myUserId] as Map + if (mySigs.isEmpty()) { + Timber.d("getKeysBackupTrust: Ignoring key backup because it lacks any signatures from this user") + return keysBackupVersionTrust + } + + 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 = mCryptoStore.getUserDevice(deviceId, myUserId) + + var isSignatureValid = false + + if (device == null) { + Timber.d("getKeysBackupTrust: Signature from unknown device $deviceId") + } else { + try { + mOlmDevice.verifySignature(device.fingerprint()!!, authData.signalableJSONDictionary(), mySigs[keyId] as String) + isSignatureValid = true + } catch (e: OlmException) { + Timber.d("getKeysBackupTrust: Bad signature from device " + device.deviceId + " " + e.localizedMessage) + } + + if (isSignatureValid && device.isVerified) { + keysBackupVersionTrust.usable = true + } + } + + val signature = KeysBackupVersionTrustSignature() + signature.device = device + signature.valid = isSignatureValid + signature.deviceId = deviceId + keysBackupVersionTrust.signatures.add(signature) + } + } + + return keysBackupVersionTrust + } + + /** + * Set trust on a keys backup version. + * It adds (or removes) the signature of the current device to the authentication part of the keys backup version. + * + * @param keysBackupVersion the backup version to check. + * @param trust the trust to set to the keys backup. + * @param callback block called when the operations completes. + */ + override fun trustKeysBackupVersion(keysBackupVersion: KeysVersionResult, + trust: Boolean, + callback: MatrixCallback) { + Timber.d("trustKeyBackupVersion: $trust, version ${keysBackupVersion.version}") + + CryptoAsyncHelper.getDecryptBackgroundHandler().post { + val myUserId = mCredentials.userId + + // Get auth data to update it + val authData = getMegolmBackupAuthData(keysBackupVersion) + + if (authData == null) { + Timber.w("trustKeyBackupVersion:trust: Key backup is missing required data") + + mUIHandler.post { + callback.onFailure(IllegalArgumentException("Missing element")) + } + + return@post + } + + // Get current signatures, or create an empty set + val myUserSignatures = (authData.signatures!![myUserId]?.toMutableMap() ?: HashMap()) + + if (trust) { + // Add current device signature + val canonicalJson = MoshiProvider.getCanonicalJson(Map::class.java, authData.signalableJSONDictionary()) + + val deviceSignatures = mKeysBackupCryptoListener.signObject(canonicalJson) + + deviceSignatures[myUserId]?.forEach { entry -> + myUserSignatures[entry.key] = entry.value + } + } else { + // Remove current device signature + myUserSignatures.remove("ed25519:${mCredentials.deviceId}") + } + + // Create an updated version of KeysVersionResult + val updateKeysBackupVersionBody = UpdateKeysBackupVersionBody(keysBackupVersion.version!!) + + updateKeysBackupVersionBody.algorithm = keysBackupVersion.algorithm + + val newMegolmBackupAuthData = authData.copy() + + val newSignatures = newMegolmBackupAuthData.signatures!!.toMutableMap() + newSignatures[myUserId] = myUserSignatures + + newMegolmBackupAuthData.signatures = newSignatures + + val moshi = MoshiProvider.providesMoshi() + val adapter = moshi.adapter(Map::class.java) + + + updateKeysBackupVersionBody.authData = adapter.fromJson(newMegolmBackupAuthData.toJsonString()) as Map? + + // And send it to the homeserver + mUpdateKeysBackupVersionTask + .configureWith(UpdateKeysBackupVersionTask.Params(keysBackupVersion.version!!, updateKeysBackupVersionBody)) + .dispatchTo(object : MatrixCallback { + override fun onSuccess(data: Unit) { + // Relaunch the state machine on this updated backup version + val newKeysBackupVersion = KeysVersionResult() + + newKeysBackupVersion.version = keysBackupVersion.version + newKeysBackupVersion.algorithm = keysBackupVersion.algorithm + newKeysBackupVersion.count = keysBackupVersion.count + newKeysBackupVersion.hash = keysBackupVersion.hash + newKeysBackupVersion.authData = updateKeysBackupVersionBody.authData + + checkAndStartWithKeysBackupVersion(newKeysBackupVersion) + + mUIHandler.post { + callback.onSuccess(data) + } + } + + override fun onFailure(failure: Throwable) { + mUIHandler.post { + callback.onFailure(failure) + } + } + }) + .executeBy(mTaskExecutor) + } + } + + /** + * Set trust on a keys backup version. + * + * @param keysBackupVersion the backup version to check. + * @param recoveryKey the recovery key to challenge with the key backup public key. + * @param callback block called when the operations completes. + */ + override fun trustKeysBackupVersionWithRecoveryKey(keysBackupVersion: KeysVersionResult, + recoveryKey: String, + callback: MatrixCallback) { + Timber.d("trustKeysBackupVersionWithRecoveryKey: version ${keysBackupVersion.version}") + + CryptoAsyncHelper.getDecryptBackgroundHandler().post { + if (!isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysBackupVersion)) { + Timber.w("trustKeyBackupVersionWithRecoveryKey: Invalid recovery key.") + + mUIHandler.post { + callback.onFailure(IllegalArgumentException("Invalid recovery key or password")) + } + return@post + } + + trustKeysBackupVersion(keysBackupVersion, true, callback) + } + } + + /** + * Set trust on a keys backup version. + * + * @param keysBackupVersion the backup version to check. + * @param password the pass phrase to challenge with the keyBackupVersion public key. + * @param callback block called when the operations completes. + */ + override fun trustKeysBackupVersionWithPassphrase(keysBackupVersion: KeysVersionResult, + password: String, + callback: MatrixCallback) { + Timber.d("trustKeysBackupVersionWithPassphrase: version ${keysBackupVersion.version}") + + CryptoAsyncHelper.getDecryptBackgroundHandler().post { + val recoveryKey = recoveryKeyFromPassword(password, keysBackupVersion, null) + + if (recoveryKey == null) { + Timber.w("trustKeysBackupVersionWithPassphrase: Key backup is missing required data") + + mUIHandler.post { + callback.onFailure(IllegalArgumentException("Missing element")) + } + + return@post + } + + // Check trust using the recovery key + trustKeysBackupVersionWithRecoveryKey(keysBackupVersion, recoveryKey, callback) + } + } + + /** + * Get public key from a Recovery key + * + * @param recoveryKey the recovery key + * @return the corresponding public key, from Olm + */ + @WorkerThread + private fun pkPublicKeyFromRecoveryKey(recoveryKey: String): String? { + // Extract the primary key + val privateKey = extractCurveKeyFromRecoveryKey(recoveryKey) + + if (privateKey == null) { + Timber.w("pkPublicKeyFromRecoveryKey: private key is null") + + return null + } + + // Built the PK decryption with it + val pkPublicKey: String + + try { + val decryption = OlmPkDecryption() + pkPublicKey = decryption.setPrivateKey(privateKey) + } catch (e: OlmException) { + return null + } + + return pkPublicKey + } + + private fun resetBackupAllGroupSessionsListeners() { + backupAllGroupSessionsCallback = null + + mKeysBackupStateListener?.let { + mKeysBackupStateManager.removeListener(it) + } + + mKeysBackupStateListener = null + } + + /** + * Return the current progress of the backup + */ + override fun getBackupProgress(progressListener: ProgressListener) { + CryptoAsyncHelper.getDecryptBackgroundHandler().post { + val backedUpKeys = mCryptoStore.inboundGroupSessionsCount(true) + val total = mCryptoStore.inboundGroupSessionsCount(false) + + mUIHandler.post { progressListener.onProgress(backedUpKeys, total) } + } + } + + /** + * Restore a backup with a recovery key from a given backup version stored on the homeserver. + * + * @param keysVersionResult 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 stepProgressListener the step progress listener + * @param callback Callback. It provides the number of found keys and the number of successfully imported keys. + */ + override fun restoreKeysWithRecoveryKey(keysVersionResult: KeysVersionResult, + recoveryKey: String, + roomId: String?, + sessionId: String?, + stepProgressListener: StepProgressListener?, + callback: MatrixCallback) { + Timber.d("restoreKeysWithRecoveryKey: From backup version: ${keysVersionResult.version}") + + CryptoAsyncHelper.getDecryptBackgroundHandler().post(Runnable { + // Check if the recovery is valid before going any further + if (!isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysVersionResult)) { + Timber.e("restoreKeysWithRecoveryKey: Invalid recovery key for this keys version") + mUIHandler.post { callback.onFailure(InvalidParameterException("Invalid recovery key")) } + return@Runnable + } + + // Get a PK decryption instance + val decryption = pkDecryptionFromRecoveryKey(recoveryKey) + if (decryption == null) { + // This should not happen anymore + Timber.e("restoreKeysWithRecoveryKey: Invalid recovery key. Error") + mUIHandler.post { callback.onFailure(InvalidParameterException("Invalid recovery key")) } + return@Runnable + } + + if (stepProgressListener != null) { + mUIHandler.post { stepProgressListener.onStepProgress(StepProgressListener.Step.DownloadingKey) } + } + + // Get backed up keys from the homeserver + getKeys(sessionId, roomId, keysVersionResult.version!!, object : MatrixCallback { + override fun onSuccess(data: KeysBackupData) { + val sessionsData = ArrayList() + // Restore that data + var sessionsFromHsCount = 0 + for (roomIdLoop in data.roomIdToRoomKeysBackupData.keys) { + for (sessionIdLoop in data.roomIdToRoomKeysBackupData[roomIdLoop]!!.sessionIdToKeyBackupData.keys) { + sessionsFromHsCount++ + + val keyBackupData = data.roomIdToRoomKeysBackupData[roomIdLoop]!!.sessionIdToKeyBackupData[sessionIdLoop]!! + + val sessionData = decryptKeyBackupData(keyBackupData, sessionIdLoop, roomIdLoop, decryption) + + sessionData?.let { + sessionsData.add(it) + } + } + } + Timber.d("restoreKeysWithRecoveryKey: Decrypted " + sessionsData.size + " keys out of " + + sessionsFromHsCount + " 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 = keysVersionResult.version != mKeysBackupVersion?.version + if (backUp) { + Timber.d("restoreKeysWithRecoveryKey: Those keys will be backed up to backup version: " + mKeysBackupVersion?.version) + } + + // Import them into the crypto store + val progressListener = if (stepProgressListener != null) { + object : ProgressListener { + override fun onProgress(progress: Int, total: Int) { + // Note: no need to post to UI thread, importMegolmSessionsData() will do it + stepProgressListener.onStepProgress(StepProgressListener.Step.ImportingKey(progress, total)) + } + } + } else { + null + } + + mKeysBackupCryptoListener.importMegolmSessionsData(sessionsData, backUp, progressListener, callback) + } + + override fun onFailure(failure: Throwable) { + mUIHandler.post { callback.onFailure(failure) } + } + }) + }) + } + + /** + * Restore a backup with a password from a given backup version stored on the homeserver. + * + * @param keysBackupVersion the backup version to restore from. + * @param password the password 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 stepProgressListener the step progress listener + * @param callback Callback. It provides the number of found keys and the number of successfully imported keys. + */ + override fun restoreKeyBackupWithPassword(keysBackupVersion: KeysVersionResult, + password: String, + roomId: String?, + sessionId: String?, + stepProgressListener: StepProgressListener?, + callback: MatrixCallback) { + Timber.d("[MXKeyBackup] restoreKeyBackup with password: From backup version: ${keysBackupVersion.version}") + + CryptoAsyncHelper.getDecryptBackgroundHandler().post { + val progressListener = if (stepProgressListener != null) { + object : ProgressListener { + override fun onProgress(progress: Int, total: Int) { + mUIHandler.post { + stepProgressListener.onStepProgress(StepProgressListener.Step.ComputingKey(progress, total)) + } + } + } + } else { + null + } + + val recoveryKey = recoveryKeyFromPassword(password, keysBackupVersion, progressListener) + + if (recoveryKey == null) { + mUIHandler.post { + Timber.d("backupKeys: Invalid configuration") + callback.onFailure(IllegalStateException("Invalid configuration")) + } + + return@post + } + + restoreKeysWithRecoveryKey(keysBackupVersion, recoveryKey, roomId, sessionId, stepProgressListener, callback) + } + } + + /** + * Same method as [RoomKeysRestClient.getRoomKey] except that it accepts nullable + * parameters and always returns a KeysBackupData object through the Callback + */ + private fun getKeys(sessionId: String?, + roomId: String?, + version: String, + callback: MatrixCallback) { + if (roomId != null && sessionId != null) { + // Get key for the room and for the session + mGetRoomSessionDataTask + .configureWith(GetRoomSessionDataTask.Params(roomId, sessionId, version)) + .dispatchTo(object : MatrixCallback { + override fun onSuccess(data: KeyBackupData) { + // Convert to KeysBackupData + val keysBackupData = KeysBackupData() + keysBackupData.roomIdToRoomKeysBackupData = HashMap() + val roomKeysBackupData = RoomKeysBackupData() + roomKeysBackupData.sessionIdToKeyBackupData = HashMap() + roomKeysBackupData.sessionIdToKeyBackupData[sessionId] = data + keysBackupData.roomIdToRoomKeysBackupData[roomId] = roomKeysBackupData + + callback.onSuccess(keysBackupData) + } + + override fun onFailure(failure: Throwable) { + callback.onFailure(failure) + } + }) + .executeBy(mTaskExecutor) + } else if (roomId != null) { + // Get all keys for the room + mGetRoomSessionsDataTask + .configureWith(GetRoomSessionsDataTask.Params(roomId, version)) + .dispatchTo(object : MatrixCallback { + override fun onSuccess(data: RoomKeysBackupData) { + // Convert to KeysBackupData + val keysBackupData = KeysBackupData() + keysBackupData.roomIdToRoomKeysBackupData = HashMap() + keysBackupData.roomIdToRoomKeysBackupData[roomId] = data + + callback.onSuccess(keysBackupData) + } + + override fun onFailure(failure: Throwable) { + callback.onFailure(failure) + } + }) + .executeBy(mTaskExecutor) + } else { + // Get all keys + mGetSessionsDataTask + .configureWith(GetSessionsDataTask.Params(version)) + .dispatchTo(callback) + .executeBy(mTaskExecutor) + } + } + + @VisibleForTesting + @WorkerThread + 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) { + Timber.e(e, "OlmException") + } + + } + + return decryption + } + + /** + * Do a backup if there are new keys, with a delay + */ + override fun maybeBackupKeys() { + when { + isStucked -> { + // If not already done, or in error case, check for a valid backup version on the homeserver. + // If there is one, maybeBackupKeys will be called again. + checkAndStartKeysBackup() + } + state == KeysBackupState.ReadyToBackUp -> { + mKeysBackupStateManager.state = 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() + + mUIHandler.postDelayed({ backupKeys() }, delayInMs) + } + else -> { + Timber.d("maybeBackupKeys: Skip it because state: $state") + } + } + } + + /** + * Get information about a backup version defined on the homeserver. + * + * It can be different than mKeysBackupVersion. + * @param version the backup version + * @param callback + */ + override fun getVersion(version: String, + callback: MatrixCallback) { + mGetKeysBackupVersionTask + .configureWith(version) + .dispatchTo(object : MatrixCallback { + override fun onSuccess(data: KeysVersionResult) { + callback.onSuccess(data) + } + + override fun onFailure(failure: Throwable) { + if (failure is Failure.ServerError + && failure.error.code == MatrixError.NOT_FOUND) { + // Workaround because the homeserver currently returns M_NOT_FOUND when there is no key backup + callback.onSuccess(null) + } else { + // Transmit the error + callback.onFailure(failure) + } + } + }) + .executeBy(mTaskExecutor) + } + + /** + * Retrieve the current version of the backup from the home server + * + * It can be different than mKeysBackupVersion. + * @param callback onSuccess(null) will be called if there is no backup on the server + */ + override fun getCurrentVersion(callback: MatrixCallback) { + mGetKeysBackupLastVersionTask + .configureWith(Unit) + .dispatchTo(object : MatrixCallback { + override fun onSuccess(data: KeysVersionResult) { + callback.onSuccess(data) + } + + override fun onFailure(failure: Throwable) { + if (failure is Failure.ServerError + && failure.error.code == MatrixError.NOT_FOUND) { + // Workaround because the homeserver currently returns M_NOT_FOUND when there is no key backup + callback.onSuccess(null) + } else { + // Transmit the error + callback.onFailure(failure) + } + } + }) + .executeBy(mTaskExecutor) + } + + /** + * This method fetches the last backup version on the server, then compare to the currently backup version use. + * If versions are not the same, the current backup is deleted (on server or locally), then the backup may be started again, using the last version. + * + * @param callback true if backup is already using the last version, and false if it is not the case + */ + override fun forceUsingLastVersion(callback: MatrixCallback) { + getCurrentVersion(object : MatrixCallback { + override fun onSuccess(data: KeysVersionResult?) { + val localBackupVersion = mKeysBackupVersion?.version + val serverBackupVersion = data?.version + + if (serverBackupVersion == null) { + if (localBackupVersion == null) { + // No backup on the server, and backup is not active + callback.onSuccess(true) + } else { + // No backup on the server, and we are currently backing up, so stop backing up + callback.onSuccess(false) + resetKeysBackupData() + mKeysBackupVersion = null + mKeysBackupStateManager.state = KeysBackupState.Disabled + } + } else { + if (localBackupVersion == null) { + // backup on the server, and backup is not active + callback.onSuccess(false) + // Do a check + checkAndStartWithKeysBackupVersion(data) + } else { + // Backup on the server, and we are currently backing up, compare version + if (localBackupVersion == serverBackupVersion) { + // We are already using the last version of the backup + callback.onSuccess(true) + } else { + // We are not using the last version, so delete the current version we are using on the server + callback.onSuccess(false) + + // This will automatically check for the last version then + deleteBackup(localBackupVersion, null) + } + } + } + } + + override fun onFailure(failure: Throwable) { + callback.onFailure(failure) + } + }) + } + + /** + * 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. + */ + override fun checkAndStartKeysBackup() { + if (!isStucked) { + // Try to start or restart the backup only if it is in unknown or bad state + Timber.w("checkAndStartKeysBackup: invalid state: $state") + + return + } + + mKeysBackupVersion = null + mKeysBackupStateManager.state = KeysBackupState.CheckingBackUpOnHomeserver + + getCurrentVersion(object : MatrixCallback { + override fun onSuccess(data: KeysVersionResult?) { + checkAndStartWithKeysBackupVersion(data) + } + + override fun onFailure(failure: Throwable) { + Timber.e(failure, "checkAndStartKeysBackup: Failed to get current version") + mKeysBackupStateManager.state = KeysBackupState.Unknown + } + }) + } + + private fun checkAndStartWithKeysBackupVersion(keyBackupVersion: KeysVersionResult?) { + Timber.d("checkAndStartWithKeyBackupVersion: ${keyBackupVersion?.version}") + + mKeysBackupVersion = keyBackupVersion + + if (keyBackupVersion == null) { + Timber.d("checkAndStartWithKeysBackupVersion: Found no key backup version on the homeserver") + resetKeysBackupData() + mKeysBackupStateManager.state = KeysBackupState.Disabled + } else { + getKeysBackupTrust(keyBackupVersion, object : MatrixCallback { + + override fun onSuccess(data: KeysBackupVersionTrust) { + val versionInStore = mCryptoStore.getKeyBackupVersion() + + if (data.usable) { + Timber.d("checkAndStartWithKeysBackupVersion: Found usable key backup. version: " + keyBackupVersion.version) + // Check the version we used at the previous app run + if (versionInStore != null && versionInStore != keyBackupVersion.version) { + Timber.d(" -> clean the previously used version $versionInStore") + resetKeysBackupData() + } + + Timber.d(" -> enabling key backups") + enableKeysBackup(keyBackupVersion) + } else { + Timber.d("checkAndStartWithKeysBackupVersion: No usable key backup. version: " + keyBackupVersion.version) + if (versionInStore != null) { + Timber.d(" -> disabling key backup") + resetKeysBackupData() + } + + mKeysBackupStateManager.state = KeysBackupState.NotTrusted + } + } + + override fun onFailure(failure: Throwable) { + // Cannot happen + } + + }) + } + } + + /* ========================================================================================== + * Private + * ========================================================================================== */ + + /** + * Extract MegolmBackupAuthData data from a backup version. + * + * @param keysBackupData the key backup data + * + * @return the authentication if found and valid, null in other case + */ + private fun getMegolmBackupAuthData(keysBackupData: KeysVersionResult): MegolmBackupAuthData? { + if (keysBackupData.version.isNullOrBlank() + || keysBackupData.algorithm != MXCRYPTO_ALGORITHM_MEGOLM_BACKUP + || keysBackupData.authData == null) { + return null + } + + val authData = keysBackupData.getAuthDataAsMegolmBackupAuthData() + + if (authData.signatures == null + || authData.publicKey.isBlank()) { + return null + } + + return authData + } + + /** + * Compute the recovery key from a password and key backup version. + * + * @param password the password. + * @param keysBackupData the backup and its auth data. + * + * @return the recovery key if successful, null in other cases + */ + @WorkerThread + private fun recoveryKeyFromPassword(password: String, keysBackupData: KeysVersionResult, progressListener: ProgressListener?): String? { + val authData = getMegolmBackupAuthData(keysBackupData) + + if (authData == null) { + Timber.w("recoveryKeyFromPassword: invalid parameter") + return null + } + + if (authData.privateKeySalt.isNullOrBlank() + || authData.privateKeyIterations == null) { + Timber.w("recoveryKeyFromPassword: Salt and/or iterations not found in key backup auth data") + + return null + } + + // Extract the recovery key from the passphrase + val data = retrievePrivateKeyWithPassword(password, authData.privateKeySalt!!, authData.privateKeyIterations!!, progressListener) + + return computeRecoveryKey(data) + } + + /** + * Check if a recovery key matches key backup authentication data. + * + * @param recoveryKey the recovery key to challenge. + * @param keysBackupData the backup and its auth data. + * + * @return true if successful. + */ + @WorkerThread + private fun isValidRecoveryKeyForKeysBackupVersion(recoveryKey: String, keysBackupData: KeysVersionResult): Boolean { + // Build PK decryption instance with the recovery key + val publicKey = pkPublicKeyFromRecoveryKey(recoveryKey) + + if (publicKey == null) { + Timber.w("isValidRecoveryKeyForKeysBackupVersion: public key is null") + + return false + } + + val authData = getMegolmBackupAuthData(keysBackupData) + + if (authData == null) { + Timber.w("isValidRecoveryKeyForKeysBackupVersion: Key backup is missing required data") + + return false + } + + // Compare both + if (publicKey != authData.publicKey) { + Timber.w("isValidRecoveryKeyForKeysBackupVersion: Public keys mismatch") + + return false + } + + // Public keys match! + return true + } + + /** + * Enable backing up of keys. + * This method will update the state and will start sending keys in nominal case + * + * @param keysVersionResult backup information object as returned by [getCurrentVersion]. + */ + private fun enableKeysBackup(keysVersionResult: KeysVersionResult) { + if (keysVersionResult.authData != null) { + val retrievedMegolmBackupAuthData = keysVersionResult.getAuthDataAsMegolmBackupAuthData() + + if (retrievedMegolmBackupAuthData != null) { + mKeysBackupVersion = keysVersionResult + mCryptoStore.setKeyBackupVersion(keysVersionResult.version) + + onServerDataRetrieved(keysVersionResult.count, keysVersionResult.hash) + + try { + mBackupKey = OlmPkEncryption().apply { + setRecipientKey(retrievedMegolmBackupAuthData.publicKey) + } + } catch (e: OlmException) { + Timber.e(e, "OlmException") + mKeysBackupStateManager.state = KeysBackupState.Disabled + return + } + + mKeysBackupStateManager.state = KeysBackupState.ReadyToBackUp + + maybeBackupKeys() + } else { + Timber.e("Invalid authentication data") + mKeysBackupStateManager.state = KeysBackupState.Disabled + } + } else { + Timber.e("Invalid authentication data") + mKeysBackupStateManager.state = KeysBackupState.Disabled + } + } + + /** + * Update the DB with data fetch from the server + */ + private fun onServerDataRetrieved(count: Int?, hash: String?) { + mCryptoStore.setKeysBackupData(KeysBackupDataEntity() + .apply { + backupLastServerNumberOfKeys = count + backupLastServerHash = hash + } + ) + } + + /** + * Reset all local key backup data. + * + * Note: This method does not update the state + */ + private fun resetKeysBackupData() { + resetBackupAllGroupSessionsListeners() + + mCryptoStore.setKeyBackupVersion(null) + mCryptoStore.setKeysBackupData(null) + mBackupKey = null + + // Reset backup markers + mCryptoStore.resetBackupMarkers() + } + + /** + * Send a chunk of keys to backup + */ + @UiThread + private fun backupKeys() { + Timber.d("backupKeys") + + // Sanity check, as this method can be called after a delay, the state may have change during the delay + if (!isEnabled || mBackupKey == null || mKeysBackupVersion == null) { + Timber.d("backupKeys: Invalid configuration") + backupAllGroupSessionsCallback?.onFailure(IllegalStateException("Invalid configuration")) + resetBackupAllGroupSessionsListeners() + + return + } + + if (state === KeysBackupState.BackingUp) { + // Do nothing if we are already backing up + Timber.d("backupKeys: Invalid state: $state") + return + } + + // Get a chunk of keys to backup + val sessions = mCryptoStore.inboundGroupSessionsToBackup(KEY_BACKUP_SEND_KEYS_MAX_COUNT) + + Timber.d("backupKeys: 1 - " + sessions.size + " sessions to back up") + + if (sessions.isEmpty()) { + // Backup is up to date + mKeysBackupStateManager.state = KeysBackupState.ReadyToBackUp + + backupAllGroupSessionsCallback?.onSuccess(Unit) + resetBackupAllGroupSessionsListeners() + return + } + + mKeysBackupStateManager.state = KeysBackupState.BackingUp + + CryptoAsyncHelper.getEncryptBackgroundHandler().post { + Timber.d("backupKeys: 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) { + Timber.e(e, "OlmException") + } + } + + Timber.d("backupKeys: 4 - Sending request") + + // Make the request + mStoreSessionDataTask + .configureWith(StoreSessionsDataTask.Params(mKeysBackupVersion!!.version!!, keysBackupData)) + .dispatchTo(object : MatrixCallback { + override fun onSuccess(data: BackupKeysResult) { + mUIHandler.post { + Timber.d("backupKeys: 5a - Request complete") + + // Mark keys as backed up + mCryptoStore.markBackupDoneForInboundGroupSessions(sessions) + + if (sessions.size < KEY_BACKUP_SEND_KEYS_MAX_COUNT) { + Timber.d("backupKeys: All keys have been backed up") + onServerDataRetrieved(data.count, data.hash) + + // Note: Changing state will trigger the call to backupAllGroupSessionsCallback.onSuccess() + mKeysBackupStateManager.state = KeysBackupState.ReadyToBackUp + } else { + Timber.d("backupKeys: Continue to back up keys") + mKeysBackupStateManager.state = KeysBackupState.WillBackUp + + backupKeys() + } + } + } + + override fun onFailure(failure: Throwable) { + if (failure is Failure.ServerError) { + mUIHandler.post { + Timber.e(failure, "backupKeys: backupKeys failed.") + + when (failure.error.code) { + MatrixError.NOT_FOUND, + MatrixError.WRONG_ROOM_KEYS_VERSION -> { + // Backup has been deleted on the server, or we are not using the last backup version + mKeysBackupStateManager.state = KeysBackupState.WrongBackUpVersion + backupAllGroupSessionsCallback?.onFailure(failure) + resetBackupAllGroupSessionsListeners() + resetKeysBackupData() + mKeysBackupVersion = null + + // Do not stay in KeysBackupState.WrongBackUpVersion but check what is available on the homeserver + checkAndStartKeysBackup() + } + else -> // Come back to the ready state so that we will retry on the next received key + mKeysBackupStateManager.state = KeysBackupState.ReadyToBackUp + } + } + } else { + mUIHandler.post { + backupAllGroupSessionsCallback?.onFailure(failure) + resetBackupAllGroupSessionsListeners() + + Timber.e("backupKeys: backupKeys failed.") + + // Retry a bit later + mKeysBackupStateManager.state = KeysBackupState.ReadyToBackUp + maybeBackupKeys() + } + } + } + }) + .executeBy(mTaskExecutor) + } + } + + @VisibleForTesting + @WorkerThread + fun encryptGroupSession(session: MXOlmInboundGroupSession2): KeyBackupData { + // Gather information for each key + val device = mKeysBackupCryptoListener.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.senderKey, + "sender_claimed_keys" to sessionData.senderClaimedKeys, + "forwarding_curve25519_key_chain" to (sessionData.forwardingCurve25519KeyChain ?: ArrayList()), + "session_key" to sessionData.sessionKey) + + var encryptedSessionBackupData: OlmPkMessage? = null + + val moshi = MoshiProvider.providesMoshi() + val adapter = moshi.adapter(Map::class.java) + + try { + val json = adapter.toJson(sessionBackupData) + + encryptedSessionBackupData = mBackupKey?.encrypt(json) + } catch (e: OlmException) { + Timber.e(e, "OlmException") + } + + // Build backup data for that key + val keyBackupData = KeyBackupData() + try { + keyBackupData.firstMessageIndex = session.mSession!!.firstKnownIndex + } catch (e: OlmException) { + Timber.e(e, "OlmException") + } + + 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 = data + + return keyBackupData + } + + @VisibleForTesting + @WorkerThread + fun decryptKeyBackupData(keyBackupData: KeyBackupData, sessionId: String, roomId: String, decryption: OlmPkDecryption): MegolmSessionData? { + var sessionBackupData: MegolmSessionData? = null + + val jsonObject = keyBackupData.sessionData + + val ciphertext = jsonObject?.get("ciphertext")?.toString() + val mac = jsonObject?.get("mac")?.toString() + val ephemeralKey = jsonObject?.get("ephemeral")?.toString() + + 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) + + val moshi = MoshiProvider.providesMoshi() + val adapter = moshi.adapter(MegolmSessionData::class.java) + + sessionBackupData = adapter.fromJson(decrypted) + } catch (e: OlmException) { + Timber.e(e, "OlmException") + } + + if (sessionBackupData != null) { + sessionBackupData.sessionId = sessionId + sessionBackupData.roomId = roomId + } + } + + return sessionBackupData + } + + companion object { + // Maximum delay in ms in {@link maybeBackupKeys} + 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 + } + + + fun setCryptoInternalListener(listener: KeysBackupCryptoListener) { + mKeysBackupCryptoListener = listener + } + + interface KeysBackupCryptoListener { + fun signObject(strToSign: String): Map> + + fun importMegolmSessionsData(megolmSessionsData: List, + backUpKeys: Boolean, + progressListener: ProgressListener?, + callback: MatrixCallback) + + fun deviceWithIdentityKey(senderKey: String, algorithm: String): MXDeviceInfo? + } + + + /* ========================================================================================== + * DEBUG INFO + * ========================================================================================== */ + + override fun toString() = "KeysBackup for ${mCredentials.userId}" +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupPassword.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupPassword.kt new file mode 100644 index 00000000..ae4aa609 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupPassword.kt @@ -0,0 +1,153 @@ +/* + * Copyright 2019 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. + */ + +/** + * Utility to compute a backup private key from a password and vice-versa. + */ +package im.vector.matrix.android.internal.crypto.keysbackup + +import androidx.annotation.WorkerThread +import im.vector.matrix.android.api.listeners.ProgressListener +import timber.log.Timber +import java.util.* +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec +import kotlin.experimental.xor + + +private const val SALT_LENGTH = 32 +private const val DEFAULT_ITERATION = 500_000 + +data class GeneratePrivateKeyResult( + // The private key + val privateKey: ByteArray, + // the salt used to generate the private key + val salt: String, + // number of key derivations done on the generated private key. + val iterations: Int) + +/** + * Compute a private key from a password. + * + * @param password the password to use. + * + * @return a {privateKey, salt, iterations} tuple. + */ +@WorkerThread +fun generatePrivateKeyWithPassword(password: String, progressListener: ProgressListener?): GeneratePrivateKeyResult { + val salt = generateSalt() + val iterations = DEFAULT_ITERATION + val privateKey = deriveKey(password, salt, iterations, progressListener) + + return GeneratePrivateKeyResult(privateKey, salt, iterations) +} + +/** + * Retrieve a private key from {password, salt, iterations} + * + * @param password the password used to generated the private key. + * @param salt the salt. + * @param iterations number of key derivations. + * @param progressListener the progress listener + * + * @return a private key. + */ +@WorkerThread +fun retrievePrivateKeyWithPassword(password: String, + salt: String, + iterations: Int, + progressListener: ProgressListener? = null): ByteArray { + return deriveKey(password, salt, iterations, progressListener) +} + +/** + * Compute a private key by deriving a password and a salt strings. + * + * @param password the password. + * @param salt the salt. + * @param iterations number of derivations. + * @param progressListener a listener to follow progress. + * + * @return a private key. + */ +@WorkerThread +private fun deriveKey(password: String, + salt: String, + iterations: Int, + progressListener: ProgressListener?): ByteArray { + // Note: copied and adapted from MXMegolmExportEncryption + val t0 = System.currentTimeMillis() + + // based on https://en.wikipedia.org/wiki/PBKDF2 algorithm + // it is simpler than the generic algorithm because the expected key length is equal to the mac key length. + // noticed as dklen/hlen + + // dklen = 256 + // hlen = 512 + val prf = Mac.getInstance("HmacSHA512") + + prf.init(SecretKeySpec(password.toByteArray(), "HmacSHA512")) + + // 256 bits key length + val dk = ByteArray(32) + val uc = ByteArray(64) + + // U1 = PRF(Password, Salt || INT_32_BE(i)) with i goes from 1 to dklen/hlen + prf.update(salt.toByteArray()) + val int32BE = byteArrayOf(0, 0, 0, 1) + prf.update(int32BE) + prf.doFinal(uc, 0) + + // copy to the key + System.arraycopy(uc, 0, dk, 0, dk.size) + + var lastProgress = -1 + + for (index in 2..iterations) { + // Uc = PRF(Password, Uc-1) + prf.update(uc) + prf.doFinal(uc, 0) + + // F(Password, Salt, c, i) = U1 ^ U2 ^ ... ^ Uc + for (byteIndex in dk.indices) { + dk[byteIndex] = dk[byteIndex] xor uc[byteIndex] + } + + val progress = (index + 1) * 100 / iterations + if (progress != lastProgress) { + lastProgress = progress + progressListener?.onProgress(lastProgress, 100) + } + } + + Timber.d("KeysBackupPassword", "## deriveKeys() : " + iterations + " in " + (System.currentTimeMillis() - t0) + " ms") + + return dk +} + +/** + * Generate a 32 chars salt + */ +private fun generateSalt(): String { + var salt = "" + + do { + salt += UUID.randomUUID().toString() + } while (salt.length < SALT_LENGTH) + + + return salt.substring(0, SALT_LENGTH) +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupStateManager.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupStateManager.kt new file mode 100644 index 00000000..fe765712 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/KeysBackupStateManager.kt @@ -0,0 +1,70 @@ +/* + * 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 im.vector.matrix.android.internal.crypto.keysbackup + +import android.os.Handler +import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupService +import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState +import timber.log.Timber +import java.util.* + +internal class KeysBackupStateManager(val uiHandler: Handler) { + + private val mListeners = ArrayList() + + // Backup state + var state = KeysBackupState.Unknown + set(newState) { + Timber.d("KeysBackup", "setState: $field -> $newState") + + field = newState + + // Notify listeners about the state change, on the ui thread + uiHandler.post { + synchronized(mListeners) { + mListeners.forEach { + // Use newState because state may have already changed again + it.onStateChange(newState) + } + } + } + } + + val isEnabled: Boolean + get() = state == KeysBackupState.ReadyToBackUp + || state == KeysBackupState.WillBackUp + || state == KeysBackupState.BackingUp + + // True if unknown or bad state + val isStucked: Boolean + get() = state == KeysBackupState.Unknown + || state == KeysBackupState.Disabled + || state == KeysBackupState.WrongBackUpVersion + || state == KeysBackupState.NotTrusted + + fun addListener(listener: KeysBackupService.KeysBackupStateListener) { + synchronized(mListeners) { + mListeners.add(listener) + } + } + + fun removeListener(listener: KeysBackupService.KeysBackupStateListener) { + synchronized(mListeners) { + mListeners.remove(listener) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/api/RoomKeysApi.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/api/RoomKeysApi.kt new file mode 100644 index 00000000..73b6112f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/api/RoomKeysApi.kt @@ -0,0 +1,180 @@ +/* + * 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 im.vector.matrix.android.internal.crypto.keysbackup.api + +import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.* +import im.vector.matrix.android.internal.network.NetworkConstants +import retrofit2.Call +import retrofit2.http.* + +/** + * Ref: https://github.com/uhoreg/matrix-doc/blob/e2e_backup/proposals/1219-storing-megolm-keys-serverside.md + */ +internal interface RoomKeysApi { + + /* ========================================================================================== + * Backup versions management + * ========================================================================================== */ + + /** + * Create a new keys backup version. + * @param createKeysBackupVersionBody the body + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "room_keys/version") + fun createKeysBackupVersion(@Body createKeysBackupVersionBody: CreateKeysBackupVersionBody): Call + + /** + * Get the key backup last version + * If not supported by the server, an error is returned: {"errcode":"M_NOT_FOUND","error":"No backup found"} + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "room_keys/version") + fun getKeysBackupLastVersion(): Call + + /** + * Get information about the given version. + * If not supported by the server, an error is returned: {"errcode":"M_NOT_FOUND","error":"No backup found"} + * + * @param version version + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "room_keys/version/{version}") + fun getKeysBackupVersion(@Path("version") version: String): Call + + /** + * Update information about the given version. + * @param version version + * @param updateKeysBackupVersionBody the body + */ + @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "room_keys/version/{version}") + fun updateKeysBackupVersion(@Path("version") version: String, + @Body keysBackupVersionBody: UpdateKeysBackupVersionBody): 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). + * + * @param roomId the room id + * @param sessionId the session id + * @param version the version of the backup + * @param keyBackupData the data to send + */ + @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "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. + * + * @param roomId the room id + * @param version the version of the backup + * @param roomKeysBackupData the data to send + */ + @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "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. + * + * @param version the version of the backup + * @param keysBackupData the data to send + */ + @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "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. + * + * @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 + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "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. + * + * @param roomId the room id + * @param version the version of the backup, or empty String to retrieve the last version + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "room_keys/keys/{roomId}") + fun getRoomSessionsData(@Path("roomId") roomId: String, + @Query("version") version: String): Call + + /** + * Retrieve all the keys from the backup. + * + * @param version the version of the backup, or empty String to retrieve the last version + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "room_keys/keys") + fun getSessionsData(@Query("version") version: String): Call + + + /* ========================================================================================== + * Deleting keys + * ========================================================================================== */ + + /** + * Deletes keys from the backup. + */ + @DELETE(NetworkConstants.URI_API_PREFIX_PATH_R0 + "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(NetworkConstants.URI_API_PREFIX_PATH_R0 + "room_keys/keys/{roomId}") + fun deleteRoomSessionsData(@Path("roomId") roomId: String, + @Query("version") version: String): Call + + /** + * Deletes keys from the backup. + */ + @DELETE(NetworkConstants.URI_API_PREFIX_PATH_R0 + "room_keys/keys") + fun deleteSessionsData(@Query("version") version: String): Call + + /* ========================================================================================== + * Deleting backup + * ========================================================================================== */ + + /** + * Deletes a backup. + */ + @DELETE(NetworkConstants.URI_API_PREFIX_PATH_R0 + "room_keys/version/{version}") + fun deleteBackup(@Path("version") version: String): Call +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/KeyBackupVersionTrust.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/KeyBackupVersionTrust.kt new file mode 100644 index 00000000..77531d28 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/KeyBackupVersionTrust.kt @@ -0,0 +1,36 @@ +/* + * 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 im.vector.matrix.android.internal.crypto.keysbackup.model + +import com.squareup.moshi.JsonClass + +/** + * Data model for response to [KeysBackup.isKeyBackupTrusted()]. + */ +@JsonClass(generateAdapter = true) +data 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: Boolean = false, + + /** + * Signatures found in the backup version. + */ + var signatures: MutableList = ArrayList() +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/KeyBackupVersionTrustSignature.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/KeyBackupVersionTrustSignature.kt new file mode 100644 index 00000000..fce77b33 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/KeyBackupVersionTrustSignature.kt @@ -0,0 +1,36 @@ +/* + * 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 im.vector.matrix.android.internal.crypto.keysbackup.model + +import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo + +/** + * A signature in a the `KeyBackupVersionTrust` object. + */ +class KeyBackupVersionTrustSignature { + + /** + * The device that signed the backup version. + */ + var device: MXDeviceInfo? = null + + /** + *Flag to indicate the signature from this device is valid. + */ + var valid = false + +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/KeysBackupVersionTrust.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/KeysBackupVersionTrust.kt new file mode 100644 index 00000000..ed765d14 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/KeysBackupVersionTrust.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 im.vector.matrix.android.internal.crypto.keysbackup.model + +/** + * Data model for response to [KeysBackup.getKeysBackupTrust()]. + */ +data class KeysBackupVersionTrust( + /** + * Flag to indicate if the backup is trusted. + * true if there is a signature that is valid & from a trusted device. + */ + var usable: Boolean = false, + + /** + * Signatures found in the backup version. + */ + var signatures: MutableList = ArrayList() +) \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/KeysBackupVersionTrustSignature.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/KeysBackupVersionTrustSignature.kt new file mode 100644 index 00000000..afa6b779 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/KeysBackupVersionTrustSignature.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 im.vector.matrix.android.internal.crypto.keysbackup.model + +import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo + +/** + * A signature in a `KeysBackupVersionTrust` object. + */ +class KeysBackupVersionTrustSignature { + + /** + * The id of the device that signed the backup version. + */ + var deviceId: String? = null + + /** + * The device that signed the backup version. + * Can be null if the device is not known. + */ + var device: MXDeviceInfo? = null + + /** + * Flag to indicate the signature from this device is valid. + */ + var valid = false + +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/MegolmBackupAuthData.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/MegolmBackupAuthData.kt new file mode 100644 index 00000000..442b1f08 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/MegolmBackupAuthData.kt @@ -0,0 +1,75 @@ +/* + * 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 im.vector.matrix.android.internal.crypto.keysbackup.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.internal.di.MoshiProvider + +/** + * Data model for [org.matrix.androidsdk.rest.model.keys.KeysAlgorithmAndData.authData] in case + * of [org.matrix.androidsdk.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP]. + */ +@JsonClass(generateAdapter = true) +data class MegolmBackupAuthData( + /** + * The curve25519 public key used to encrypt the backups. + */ + @Json(name = "public_key") + var publicKey: String = "", + + /** + * In case of a backup created from a password, the salt associated with the backup + * private key. + */ + @Json(name = "private_key_salt") + var privateKeySalt: String? = null, + + /** + * In case of a backup created from a password, the number of key derivations. + */ + @Json(name = "private_key_iterations") + var privateKeyIterations: Int? = null, + + /** + * Signatures of the public key. + * userId -> (deviceSignKeyId -> signature) + */ + var signatures: Map>? = null +) { + + fun toJsonString(): String { + return MoshiProvider.providesMoshi() + .adapter(MegolmBackupAuthData::class.java) + .toJson(this) + } + + /** + * Same as the parent [MXJSONModel JSONDictionary] but return only + * data that must be signed. + */ + fun signalableJSONDictionary(): Map = HashMap().apply { + put("public_key", publicKey) + + privateKeySalt?.let { + put("private_key_salt", it) + } + privateKeyIterations?.let { + put("private_key_iterations", it) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/MegolmBackupCreationInfo.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/MegolmBackupCreationInfo.kt new file mode 100644 index 00000000..2be7806b --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/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 im.vector.matrix.android.internal.crypto.keysbackup.model + +/** + * 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-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/BackupKeysResult.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/BackupKeysResult.kt new file mode 100644 index 00000000..e2abc381 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/BackupKeysResult.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto.keysbackup.model.rest + +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class BackupKeysResult( + + // The hash value which is an opaque string representing stored keys in the backup + var hash: String? = null, + + // The number of keys stored in the backup. + var count: Int? = null + +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/CreateKeysBackupVersionBody.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/CreateKeysBackupVersionBody.kt new file mode 100644 index 00000000..5efbc6d0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/CreateKeysBackupVersionBody.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 im.vector.matrix.android.internal.crypto.keysbackup.model.rest + +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +class CreateKeysBackupVersionBody : KeysAlgorithmAndData() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/KeyBackupData.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/KeyBackupData.kt new file mode 100644 index 00000000..f172d45f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/KeyBackupData.kt @@ -0,0 +1,56 @@ +/* + * 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 im.vector.matrix.android.internal.crypto.keysbackup.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.internal.di.MoshiProvider + +/** + * Backup data for one key. + */ +@JsonClass(generateAdapter = true) +data class KeyBackupData( + /** + * Required. The index of the first message in the session that the key can decrypt. + */ + @Json(name = "first_message_index") + var firstMessageIndex: Long = 0, + + /** + * Required. The number of times this key has been forwarded. + */ + @Json(name = "forwarded_count") + var forwardedCount: Int = 0, + + /** + * Whether the device backing up the key has verified the device that the key is from. + */ + @Json(name = "is_verified") + var isVerified: Boolean = false, + + /** + * Algorithm-dependent data. + */ + @Json(name = "session_data") + var sessionData: Map? = null +) { + + fun toJsonString(): String { + return MoshiProvider.providesMoshi().adapter(KeyBackupData::class.java).toJson(this) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/KeysAlgorithmAndData.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/KeysAlgorithmAndData.kt new file mode 100644 index 00000000..b03dd498 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/KeysAlgorithmAndData.kt @@ -0,0 +1,62 @@ +/* + * 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 im.vector.matrix.android.internal.crypto.keysbackup.model.rest + +import com.squareup.moshi.Json +import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupAuthData +import im.vector.matrix.android.internal.di.MoshiProvider + +/** + *
+ *     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 + */ + @Json(name = "algorithm") + var algorithm: String? = null + + /** + * algorithm-dependent data, for "m.megolm_backup.v1.curve25519-aes-sha2" see [im.vector.matrix.android.internal.crypto.keysbackup.MegolmBackupAuthData] + */ + @Json(name = "auth_data") + var authData: Map? = null + + /** + * Facility method to convert authData to a MegolmBackupAuthData object + */ + fun getAuthDataAsMegolmBackupAuthData(): MegolmBackupAuthData { + return MoshiProvider.providesMoshi() + .adapter(MegolmBackupAuthData::class.java) + .fromJsonValue(authData)!! + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/KeysBackupData.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/KeysBackupData.kt new file mode 100644 index 00000000..2f4165d8 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/KeysBackupData.kt @@ -0,0 +1,32 @@ +/* + * 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 im.vector.matrix.android.internal.crypto.keysbackup.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Backup data for several keys in several rooms. + */ +@JsonClass(generateAdapter = true) +data class KeysBackupData( + + // the keys are the room IDs, and the values are RoomKeysBackupData + @Json(name = "rooms") + var roomIdToRoomKeysBackupData: MutableMap = HashMap() + +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/KeysVersion.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/KeysVersion.kt new file mode 100644 index 00000000..4e33c17b --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/KeysVersion.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 im.vector.matrix.android.internal.crypto.keysbackup.model.rest + +data class KeysVersion( + // the keys backup version + var version: String? = null +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/KeysVersionResult.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/KeysVersionResult.kt new file mode 100644 index 00000000..bff8f138 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/KeysVersionResult.kt @@ -0,0 +1,31 @@ +/* + * 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 im.vector.matrix.android.internal.crypto.keysbackup.model.rest + +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class KeysVersionResult( + // the backup version + var version: String? = null, + + // The hash value which is an opaque string representing stored keys in the backup + var hash: String? = null, + + // The number of keys stored in the backup. + var count: Int? = null +) : KeysAlgorithmAndData() \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/RoomKeysBackupData.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/RoomKeysBackupData.kt new file mode 100644 index 00000000..5d69f635 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/RoomKeysBackupData.kt @@ -0,0 +1,31 @@ +/* + * 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 im.vector.matrix.android.internal.crypto.keysbackup.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Backup data for several keys within a room. + */ +@JsonClass(generateAdapter = true) +data class RoomKeysBackupData( + + // the keys are the session IDs, and the values are KeyBackupData + @Json(name = "sessions") + var sessionIdToKeyBackupData: MutableMap = HashMap() +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/UpdateKeysBackupVersionBody.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/UpdateKeysBackupVersionBody.kt new file mode 100644 index 00000000..e2d628c4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/UpdateKeysBackupVersionBody.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto.keysbackup.model.rest + +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class UpdateKeysBackupVersionBody( + // the backup version, mandatory + val version: String +) : KeysAlgorithmAndData() \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/CreateKeysBackupVersionTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/CreateKeysBackupVersionTask.kt new file mode 100644 index 00000000..2053f56e --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/CreateKeysBackupVersionTask.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto.keysbackup.tasks + +import arrow.core.Try +import im.vector.matrix.android.internal.crypto.keysbackup.api.RoomKeysApi +import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.CreateKeysBackupVersionBody +import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersion +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.task.Task + +internal interface CreateKeysBackupVersionTask : Task + + +internal class DefaultCreateKeysBackupVersionTask(private val roomKeysApi: RoomKeysApi) + : CreateKeysBackupVersionTask { + + + override fun execute(params: CreateKeysBackupVersionBody): Try { + return executeRequest { + apiCall = roomKeysApi.createKeysBackupVersion(params) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/DeleteBackupTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/DeleteBackupTask.kt new file mode 100644 index 00000000..472d8846 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/DeleteBackupTask.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto.keysbackup.tasks + +import arrow.core.Try +import im.vector.matrix.android.internal.crypto.keysbackup.api.RoomKeysApi +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.task.Task + + +internal interface DeleteBackupTask : Task { + data class Params( + val version: String + ) +} + + +internal class DefaultDeleteBackupTask(private val roomKeysApi: RoomKeysApi) + : DeleteBackupTask { + + override fun execute(params: DeleteBackupTask.Params): Try { + return executeRequest { + apiCall = roomKeysApi.deleteBackup( + params.version) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/DeleteRoomSessionDataTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/DeleteRoomSessionDataTask.kt new file mode 100644 index 00000000..01c59e8f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/DeleteRoomSessionDataTask.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto.keysbackup.tasks + +import arrow.core.Try +import im.vector.matrix.android.internal.crypto.keysbackup.api.RoomKeysApi +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.task.Task + +internal interface DeleteRoomSessionDataTask : Task { + data class Params( + val roomId: String, + val sessionId: String, + val version: String + ) +} + + +internal class DefaultDeleteRoomSessionDataTask(private val roomKeysApi: RoomKeysApi) + : DeleteRoomSessionDataTask { + + override fun execute(params: DeleteRoomSessionDataTask.Params): Try { + return executeRequest { + apiCall = roomKeysApi.deleteRoomSessionData( + params.roomId, + params.sessionId, + params.version) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/DeleteRoomSessionsDataTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/DeleteRoomSessionsDataTask.kt new file mode 100644 index 00000000..79061f49 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/DeleteRoomSessionsDataTask.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto.keysbackup.tasks + +import arrow.core.Try +import im.vector.matrix.android.internal.crypto.keysbackup.api.RoomKeysApi +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.task.Task + +internal interface DeleteRoomSessionsDataTask : Task { + data class Params( + val roomId: String, + val version: String + ) +} + + +internal class DefaultDeleteRoomSessionsDataTask(private val roomKeysApi: RoomKeysApi) + : DeleteRoomSessionsDataTask { + + override fun execute(params: DeleteRoomSessionsDataTask.Params): Try { + return executeRequest { + apiCall = roomKeysApi.deleteRoomSessionsData( + params.roomId, + params.version) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/DeleteSessionsDataTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/DeleteSessionsDataTask.kt new file mode 100644 index 00000000..8fbd9957 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/DeleteSessionsDataTask.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto.keysbackup.tasks + +import arrow.core.Try +import im.vector.matrix.android.internal.crypto.keysbackup.api.RoomKeysApi +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.task.Task + +internal interface DeleteSessionsDataTask : Task { + data class Params( + val version: String + ) +} + + +internal class DefaultDeleteSessionsDataTask(private val roomKeysApi: RoomKeysApi) + : DeleteSessionsDataTask { + + override fun execute(params: DeleteSessionsDataTask.Params): Try { + return executeRequest { + apiCall = roomKeysApi.deleteSessionsData( + params.version) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/GetKeysBackupLastVersionTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/GetKeysBackupLastVersionTask.kt new file mode 100644 index 00000000..ff09a939 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/GetKeysBackupLastVersionTask.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto.keysbackup.tasks + +import arrow.core.Try +import im.vector.matrix.android.internal.crypto.keysbackup.api.RoomKeysApi +import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.task.Task + +internal interface GetKeysBackupLastVersionTask : Task + + +internal class DefaultGetKeysBackupLastVersionTask(private val roomKeysApi: RoomKeysApi) + : GetKeysBackupLastVersionTask { + + + override fun execute(params: Unit): Try { + return executeRequest { + apiCall = roomKeysApi.getKeysBackupLastVersion() + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/GetKeysBackupVersionTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/GetKeysBackupVersionTask.kt new file mode 100644 index 00000000..becdf7a4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/GetKeysBackupVersionTask.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto.keysbackup.tasks + +import arrow.core.Try +import im.vector.matrix.android.internal.crypto.keysbackup.api.RoomKeysApi +import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.task.Task + +internal interface GetKeysBackupVersionTask : Task + + +internal class DefaultGetKeysBackupVersionTask(private val roomKeysApi: RoomKeysApi) + : GetKeysBackupVersionTask { + + + override fun execute(params: String): Try { + return executeRequest { + apiCall = roomKeysApi.getKeysBackupVersion(params) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/GetRoomSessionDataTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/GetRoomSessionDataTask.kt new file mode 100644 index 00000000..ef28efd4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/GetRoomSessionDataTask.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto.keysbackup.tasks + +import arrow.core.Try +import im.vector.matrix.android.internal.crypto.keysbackup.api.RoomKeysApi +import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeyBackupData +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.task.Task + +internal interface GetRoomSessionDataTask : Task { + data class Params( + val roomId: String, + val sessionId: String, + val version: String + ) +} + + +internal class DefaultGetRoomSessionDataTask(private val roomKeysApi: RoomKeysApi) + : GetRoomSessionDataTask { + + override fun execute(params: GetRoomSessionDataTask.Params): Try { + return executeRequest { + apiCall = roomKeysApi.getRoomSessionData( + params.roomId, + params.sessionId, + params.version) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/GetRoomSessionsDataTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/GetRoomSessionsDataTask.kt new file mode 100644 index 00000000..7858c687 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/GetRoomSessionsDataTask.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto.keysbackup.tasks + +import arrow.core.Try +import im.vector.matrix.android.internal.crypto.keysbackup.api.RoomKeysApi +import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.RoomKeysBackupData +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.task.Task + + +internal interface GetRoomSessionsDataTask : Task { + data class Params( + val roomId: String, + val version: String + ) +} + + +internal class DefaultGetRoomSessionsDataTask(private val roomKeysApi: RoomKeysApi) + : GetRoomSessionsDataTask { + + override fun execute(params: GetRoomSessionsDataTask.Params): Try { + return executeRequest { + apiCall = roomKeysApi.getRoomSessionsData( + params.roomId, + params.version) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/GetSessionsDataTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/GetSessionsDataTask.kt new file mode 100644 index 00000000..67b18788 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/GetSessionsDataTask.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto.keysbackup.tasks + +import arrow.core.Try +import im.vector.matrix.android.internal.crypto.keysbackup.api.RoomKeysApi +import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysBackupData +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.task.Task + +internal interface GetSessionsDataTask : Task { + data class Params( + val version: String + ) +} + + +internal class DefaultGetSessionsDataTask(private val roomKeysApi: RoomKeysApi) + : GetSessionsDataTask { + + override fun execute(params: GetSessionsDataTask.Params): Try { + return executeRequest { + apiCall = roomKeysApi.getSessionsData( + params.version) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/StoreRoomSessionDataTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/StoreRoomSessionDataTask.kt new file mode 100644 index 00000000..1c81f691 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/StoreRoomSessionDataTask.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto.keysbackup.tasks + +import arrow.core.Try +import im.vector.matrix.android.internal.crypto.keysbackup.api.RoomKeysApi +import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeyBackupData +import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.BackupKeysResult +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.task.Task + +internal interface StoreRoomSessionDataTask : Task { + data class Params( + val roomId: String, + val sessionId: String, + val version: String, + val keyBackupData: KeyBackupData + ) +} + + +internal class DefaultStoreRoomSessionDataTask(private val roomKeysApi: RoomKeysApi) + : StoreRoomSessionDataTask { + + override fun execute(params: StoreRoomSessionDataTask.Params): Try { + return executeRequest { + apiCall = roomKeysApi.storeRoomSessionData( + params.roomId, + params.sessionId, + params.version, + params.keyBackupData) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/StoreRoomSessionsDataTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/StoreRoomSessionsDataTask.kt new file mode 100644 index 00000000..09b9c0b0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/StoreRoomSessionsDataTask.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto.keysbackup.tasks + +import arrow.core.Try +import im.vector.matrix.android.internal.crypto.keysbackup.api.RoomKeysApi +import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.RoomKeysBackupData +import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.BackupKeysResult +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.task.Task + +internal interface StoreRoomSessionsDataTask : Task { + data class Params( + val roomId: String, + val version: String, + val roomKeysBackupData: RoomKeysBackupData + ) +} + + +internal class DefaultStoreRoomSessionsDataTask(private val roomKeysApi: RoomKeysApi) + : StoreRoomSessionsDataTask { + + override fun execute(params: StoreRoomSessionsDataTask.Params): Try { + return executeRequest { + apiCall = roomKeysApi.storeRoomSessionsData( + params.roomId, + params.version, + params.roomKeysBackupData) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/StoreSessionsDataTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/StoreSessionsDataTask.kt new file mode 100644 index 00000000..736bfab0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/StoreSessionsDataTask.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto.keysbackup.tasks + +import arrow.core.Try +import im.vector.matrix.android.internal.crypto.keysbackup.api.RoomKeysApi +import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysBackupData +import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.BackupKeysResult +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.task.Task + +internal interface StoreSessionsDataTask : Task { + data class Params( + val version: String, + val keysBackupData: KeysBackupData + ) +} + + +internal class DefaultStoreSessionsDataTask(private val roomKeysApi: RoomKeysApi) + : StoreSessionsDataTask { + + override fun execute(params: StoreSessionsDataTask.Params): Try { + return executeRequest { + apiCall = roomKeysApi.storeSessionsData( + params.version, + params.keysBackupData) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/UpdateKeysBackupVersionTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/UpdateKeysBackupVersionTask.kt new file mode 100644 index 00000000..ffede6ab --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/tasks/UpdateKeysBackupVersionTask.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto.keysbackup.tasks + +import arrow.core.Try +import im.vector.matrix.android.internal.crypto.keysbackup.api.RoomKeysApi +import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.UpdateKeysBackupVersionBody +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.task.Task + +internal interface UpdateKeysBackupVersionTask : Task { + data class Params( + val version: String, + val keysBackupVersionBody: UpdateKeysBackupVersionBody + ) +} + + +internal class DefaultUpdateKeysBackupVersionTask(private val roomKeysApi: RoomKeysApi) + : UpdateKeysBackupVersionTask { + + + override fun execute(params: UpdateKeysBackupVersionTask.Params): Try { + return executeRequest { + apiCall = roomKeysApi.updateKeysBackupVersion(params.version, params.keysBackupVersionBody) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/util/Base58.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/util/Base58.kt new file mode 100644 index 00000000..d842fb26 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/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 im.vector.matrix.android.internal.crypto.keysbackup.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-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/util/RecoveryKey.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/util/RecoveryKey.kt new file mode 100644 index 00000000..86ab5cfb --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/util/RecoveryKey.kt @@ -0,0 +1,119 @@ +/* + * 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 im.vector.matrix.android.internal.crypto.keysbackup.util + +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 +} + +/** + * Compute 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 + + // Do not add white space every 4 chars, it's up to the presenter to do it + return base58encode(data) +} + +/** + * 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("""\s""".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-android/src/main/java/im/vector/matrix/android/internal/crypto/model/ImportRoomKeysResult.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/ImportRoomKeysResult.kt new file mode 100644 index 00000000..03def499 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/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 im.vector.matrix.android.internal.crypto.model + +data class ImportRoomKeysResult(val totalNumberOfKeys: Int, + val successfullyNumberOfImportedKeys: Int) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/MXDeviceInfo.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/MXDeviceInfo.kt new file mode 100755 index 00000000..28bcba61 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/MXDeviceInfo.kt @@ -0,0 +1,189 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2017 Vector Creations Ltd + * 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 im.vector.matrix.android.internal.crypto.model + +import android.text.TextUtils +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.util.JsonDict +import im.vector.matrix.android.internal.crypto.model.rest.DeviceKeys +import java.io.Serializable +import java.util.* + +@JsonClass(generateAdapter = true) +data class MXDeviceInfo( + + /** + * The id of this device. + */ + @Json(name = "device_id") + var deviceId: String, + + /** + * the user id + */ + @Json(name = "user_id") + var userId: String, + + /** + * The list of algorithms supported by this device. + */ + @Json(name = "algorithms") + var algorithms: List? = null, + + /** + * A map from ":" to "". + */ + @Json(name = "keys") + var keys: Map? = null, + + /** + * The signature of this MXDeviceInfo. + * A map from "" to a map from ":" to "" + */ + @Json(name = "signatures") + var signatures: Map>? = null, + + /* + * Additional data from the home server. + */ + @Json(name = "unsigned") + var unsigned: JsonDict? = null, + + /** + * Verification state of this device. + */ + var mVerified: Int = DEVICE_VERIFICATION_UNKNOWN +) : Serializable { + /** + * Tells if the device is unknown + * + * @return true if the device is unknown + */ + val isUnknown: Boolean + get() = mVerified == DEVICE_VERIFICATION_UNKNOWN + + /** + * Tells if the device is verified. + * + * @return true if the device is verified + */ + val isVerified: Boolean + get() = mVerified == DEVICE_VERIFICATION_VERIFIED + + /** + * Tells if the device is unverified. + * + * @return true if the device is unverified + */ + val isUnverified: Boolean + get() = mVerified == DEVICE_VERIFICATION_UNVERIFIED + + /** + * Tells if the device is blocked. + * + * @return true if the device is blocked + */ + val isBlocked: Boolean + get() = mVerified == DEVICE_VERIFICATION_BLOCKED + + /** + * @return the fingerprint + */ + fun fingerprint(): String? { + return if (null != keys && !TextUtils.isEmpty(deviceId)) { + keys!!["ed25519:$deviceId"] + } else null + + } + + /** + * @return the identity key + */ + fun identityKey(): String? { + return if (null != keys && !TextUtils.isEmpty(deviceId)) { + keys!!["curve25519:$deviceId"] + } else null + + } + + /** + * @return the display name + */ + fun displayName(): String? { + return if (null != unsigned) { + unsigned!!["device_display_name"] as String? + } else null + + } + + /** + * @return the signed data map + */ + fun signalableJSONDictionary(): Map { + val map = HashMap() + + map["device_id"] = deviceId + + if (null != userId) { + map["user_id"] = userId!! + } + + if (null != algorithms) { + map["algorithms"] = algorithms!! + } + + if (null != keys) { + map["keys"] = keys!! + } + + return map + } + + /** + * @return a dictionary of the parameters + */ + fun toDeviceKeys(): DeviceKeys { + return DeviceKeys( + userId = userId, + deviceId = deviceId, + algorithms = algorithms!!, + keys = keys!!, + signatures = signatures!! + ) + } + + override fun toString(): String { + return "MXDeviceInfo $userId:$deviceId" + } + + companion object { + // This device is a new device and the user was not warned it has been added. + const val DEVICE_VERIFICATION_UNKNOWN = -1 + + // The user has not yet verified this device. + const val DEVICE_VERIFICATION_UNVERIFIED = 0 + + // The user has verified this device. + const val DEVICE_VERIFICATION_VERIFIED = 1 + + // The user has blocked this device. + const val DEVICE_VERIFICATION_BLOCKED = 2 + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/MXEncryptEventContentResult.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/MXEncryptEventContentResult.kt new file mode 100755 index 00000000..7c0addf3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/MXEncryptEventContentResult.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2016 OpenMarket Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto.model + +import im.vector.matrix.android.api.session.events.model.Content + +data class MXEncryptEventContentResult( + /** + * The event content + */ + val mEventContent: Content, + /** + * the event type + */ + val mEventType: String +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/MXKey.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/MXKey.java new file mode 100755 index 00000000..1a96f22c --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/MXKey.java @@ -0,0 +1,139 @@ +/* + * Copyright 2016 OpenMarket Ltd + * 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 im.vector.matrix.android.internal.crypto.model; + +import android.text.TextUtils; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import timber.log.Timber; + +public class MXKey implements Serializable { + /** + * Key types. + */ + public static final String KEY_CURVE_25519_TYPE = "curve25519"; + public static final String KEY_SIGNED_CURVE_25519_TYPE = "signed_curve25519"; + //public static final String KEY_ED_25519_TYPE = "ed25519"; + + /** + * The type of the key. + */ + public String type; + + /** + * The id of the key. + */ + public String keyId; + + /** + * The key. + */ + public String value; + + /** + * signature user Id to [deviceid][signature] + */ + public Map> signatures; + + /** + * Default constructor + */ + public MXKey() { + } + + /** + * Convert a map to a MXKey + * + * @param map the map to convert + */ + public MXKey(Map> map) { + if ((null != map) && (map.size() > 0)) { + List mapKeys = new ArrayList<>(map.keySet()); + + String firstEntry = mapKeys.get(0); + setKeyFullId(firstEntry); + + Map params = map.get(firstEntry); + value = (String) params.get("key"); + signatures = (Map>) params.get("signatures"); + } + } + + /** + * @return the key full id + */ + public String getKeyFullId() { + return type + ":" + keyId; + } + + /** + * Update the key fields with a key full id + * + * @param keyFullId the key full id + */ + private void setKeyFullId(String keyFullId) { + if (!TextUtils.isEmpty(keyFullId)) { + try { + String[] components = keyFullId.split(":"); + + if (components.length == 2) { + type = components[0]; + keyId = components[1]; + } + } catch (Exception e) { + Timber.e(e, "## setKeyFullId() failed"); + } + } + } + + /** + * @return the signed data map + */ + public Map signalableJSONDictionary() { + Map map = new HashMap<>(); + + if (null != value) { + map.put("key", value); + } + + return map; + } + + /** + * Returns a signature for an user Id and a signkey + * + * @param userId the user id + * @param signkey the sign key + * @return the signature + */ + public String signatureForUserId(String userId, String signkey) { + // sanity checks + if (!TextUtils.isEmpty(userId) && !TextUtils.isEmpty(signkey)) { + if ((null != signatures) && signatures.containsKey(userId)) { + return signatures.get(userId).get(signkey); + } + } + + return null; + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/MXOlmInboundGroupSession.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/MXOlmInboundGroupSession.java new file mode 100755 index 00000000..8d6f99e5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/MXOlmInboundGroupSession.java @@ -0,0 +1,59 @@ +/* + * Copyright 2016 OpenMarket Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto.model; + +import org.matrix.olm.OlmInboundGroupSession; + +import java.io.Serializable; +import java.util.Map; + +import timber.log.Timber; + + +/** + * This class adds more context to a OLMInboundGroupSession object. + * This allows additional checks. The class implements NSCoding so that the context can be stored. + */ +public class MXOlmInboundGroupSession implements Serializable { + // + private static final String LOG_TAG = "OlmInboundGroupSession"; + + // The associated olm inbound group session. + public OlmInboundGroupSession mSession; + + // The room in which this session is used. + public String mRoomId; + + // The base64-encoded curve25519 key of the sender. + public String mSenderKey; + + // Other keys the sender claims. + public Map mKeysClaimed; + + /** + * Constructor + * + * @param sessionKey the session key + */ + public MXOlmInboundGroupSession(String sessionKey) { + try { + mSession = new OlmInboundGroupSession(sessionKey); + } catch (Exception e) { + Timber.e(e, "Cannot create"); + } + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/MXOlmInboundGroupSession2.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/MXOlmInboundGroupSession2.kt new file mode 100755 index 00000000..ab6f5669 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/MXOlmInboundGroupSession2.kt @@ -0,0 +1,166 @@ +/* + * Copyright 2016 OpenMarket Ltd + * 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 im.vector.matrix.android.internal.crypto.model + +import android.text.TextUtils +import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM +import im.vector.matrix.android.internal.crypto.MegolmSessionData +import org.matrix.olm.OlmInboundGroupSession +import timber.log.Timber +import java.util.* + +/** + * This class adds more context to a OLMInboundGroupSession object. + * This allows additional checks. The class implements NSCoding so that the context can be stored. + */ +class MXOlmInboundGroupSession2 { + + // The associated olm inbound group session. + var mSession: OlmInboundGroupSession? = null + + // The room in which this session is used. + var mRoomId: String? = null + + // The base64-encoded curve25519 key of the sender. + var mSenderKey: String? = null + + // Other keys the sender claims. + var mKeysClaimed: Map? = null + + // Devices which forwarded this session to us (normally empty). + var mForwardingCurve25519KeyChain: List? = ArrayList() + + /** + * @return the first known message index + */ + val firstKnownIndex: Long? + get() { + if (null != mSession) { + try { + return mSession!!.firstKnownIndex + } catch (e: Exception) { + Timber.e(e, "## getFirstKnownIndex() : getFirstKnownIndex failed") + } + + } + + return null + } + + /** + * Constructor + * + * @param prevFormatSession the previous session format + */ + constructor(prevFormatSession: MXOlmInboundGroupSession) { + mSession = prevFormatSession.mSession + mRoomId = prevFormatSession.mRoomId + mSenderKey = prevFormatSession.mSenderKey + mKeysClaimed = prevFormatSession.mKeysClaimed + } + + /** + * Constructor + * + * @param sessionKey the session key + * @param isImported true if it is an imported session key + */ + constructor(sessionKey: String, isImported: Boolean) { + try { + if (!isImported) { + mSession = OlmInboundGroupSession(sessionKey) + } else { + mSession = OlmInboundGroupSession.importSession(sessionKey) + } + } catch (e: Exception) { + Timber.e(e, "Cannot create") + } + + } + + /** + * Create a new instance from the provided keys map. + * + * @param megolmSessionData the megolm session data + * @throws Exception if the data are invalid + */ + @Throws(Exception::class) + constructor(megolmSessionData: MegolmSessionData) { + try { + mSession = OlmInboundGroupSession.importSession(megolmSessionData.sessionKey!!) + + if (!TextUtils.equals(mSession!!.sessionIdentifier(), megolmSessionData.sessionId)) { + throw Exception("Mismatched group session Id") + } + + mSenderKey = megolmSessionData.senderKey + mKeysClaimed = megolmSessionData.senderClaimedKeys + mRoomId = megolmSessionData.roomId + } catch (e: Exception) { + throw Exception(e.message) + } + } + + /** + * Export the inbound group session keys + * + * @return the inbound group session as MegolmSessionData if the operation succeeds + */ + fun exportKeys(): MegolmSessionData? { + var megolmSessionData: MegolmSessionData? = MegolmSessionData() + + try { + if (null == mForwardingCurve25519KeyChain) { + mForwardingCurve25519KeyChain = ArrayList() + } + + megolmSessionData!!.senderClaimedEd25519Key = mKeysClaimed!!["ed25519"] + megolmSessionData.forwardingCurve25519KeyChain = ArrayList(mForwardingCurve25519KeyChain!!) + megolmSessionData.senderKey = mSenderKey + megolmSessionData.senderClaimedKeys = mKeysClaimed + megolmSessionData.roomId = mRoomId + megolmSessionData.sessionId = mSession!!.sessionIdentifier() + megolmSessionData.sessionKey = mSession!!.export(mSession!!.firstKnownIndex) + megolmSessionData.algorithm = MXCRYPTO_ALGORITHM_MEGOLM + } catch (e: Exception) { + megolmSessionData = null + Timber.e(e, "## export() : senderKey " + mSenderKey + " failed") + } + + return megolmSessionData + } + + /** + * Export the session for a message index. + * + * @param messageIndex the message index + * @return the exported data + */ + fun exportSession(messageIndex: Long): String? { + if (null != mSession) { + try { + return mSession!!.export(messageIndex) + } catch (e: Exception) { + Timber.e(e, "## exportSession() : export failed") + } + + } + + return null + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/MXOlmSession.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/MXOlmSession.kt new file mode 100644 index 00000000..c9af9c2c --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/MXOlmSession.kt @@ -0,0 +1,36 @@ +/* + * 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 im.vector.matrix.android.internal.crypto.model + +import org.matrix.olm.OlmSession + +/** + * Encapsulate a OlmSession and a last received message Timestamp + */ +data class MXOlmSession( + // The associated olm session. + val olmSession: OlmSession, + // Timestamp at which the session last received a message. + var lastReceivedMessageTs: Long = 0) { + + /** + * Notify that a message has been received on this olm session so that it updates `lastReceivedMessageTs` + */ + fun onMessageReceived() { + lastReceivedMessageTs = System.currentTimeMillis() + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/MXOlmSessionResult.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/MXOlmSessionResult.java new file mode 100755 index 00000000..1beb65c4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/MXOlmSessionResult.java @@ -0,0 +1,43 @@ +/* + * Copyright 2016 OpenMarket Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto.model; + +import java.io.Serializable; + +public class MXOlmSessionResult implements Serializable { + /** + * the device + */ + public final MXDeviceInfo mDevice; + + /** + * Base64 olm session id. + * null if no session could be established. + */ + public String mSessionId; + + /** + * Constructor + * + * @param device the device + * @param sessionId the olm session id + */ + public MXOlmSessionResult(MXDeviceInfo device, String sessionId) { + mDevice = device; + mSessionId = sessionId; + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/MXQueuedEncryption.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/MXQueuedEncryption.kt new file mode 100755 index 00000000..b41f3b24 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/MXQueuedEncryption.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2016 OpenMarket Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto.model + +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.session.events.model.Content + +class MXQueuedEncryption { + + /** + * The data to encrypt. + */ + var mEventContent: Content? = null + var mEventType: String? = null + + /** + * the asynchronous callback + */ + var mApiCallback: MatrixCallback? = null +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/MXUsersDevicesMap.java b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/MXUsersDevicesMap.java new file mode 100755 index 00000000..49d46e6c --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/MXUsersDevicesMap.java @@ -0,0 +1,186 @@ +/* + * Copyright 2016 OpenMarket Ltd + * 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 im.vector.matrix.android.internal.crypto.model; + +import android.text.TextUtils; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class MXUsersDevicesMap implements Serializable { + + // The device keys as returned by the homeserver: a map of a map (userId -> deviceId -> Object). + private final Map> mMap = new HashMap<>(); + + /** + * @return the inner map + */ + public Map> getMap() { + return mMap; + } + + /** + * Default constructor constructor + */ + public MXUsersDevicesMap() { + } + + /** + * The constructor + * + * @param map the map + */ + public MXUsersDevicesMap(Map> map) { + if (null != map) { + Set keys = map.keySet(); + + for (String key : keys) { + mMap.put(key, new HashMap<>(map.get(key))); + } + } + } + + /** + * @return a deep copy + */ + public MXUsersDevicesMap deepCopy() { + MXUsersDevicesMap copy = new MXUsersDevicesMap<>(); + + Set keys = mMap.keySet(); + + for (String key : keys) { + copy.mMap.put(key, new HashMap<>(mMap.get(key))); + } + + return copy; + } + + /** + * @return the user Ids + */ + public List getUserIds() { + return new ArrayList<>(mMap.keySet()); + } + + /** + * Provides the device ids list for an user id + * + * @param userId the user id + * @return the device ids list + */ + public List getUserDeviceIds(String userId) { + if (!TextUtils.isEmpty(userId) && mMap.containsKey(userId)) { + return new ArrayList<>(mMap.get(userId).keySet()); + } + + return null; + } + + /** + * Provides the object for a device id and an user Id + * + * @param deviceId the device id + * @param userId the object id + * @return the object + */ + public E getObject(String deviceId, String userId) { + if (!TextUtils.isEmpty(userId) && mMap.containsKey(userId) && !TextUtils.isEmpty(deviceId)) { + return mMap.get(userId).get(deviceId); + } + + return null; + } + + /** + * Set an object for a dedicated user Id and device Id + * + * @param object the object to set + * @param userId the user Id + * @param deviceId the device id + */ + public void setObject(E object, String userId, String deviceId) { + if ((null != object) && !TextUtils.isEmpty(userId) && !TextUtils.isEmpty(deviceId)) { + Map subMap = mMap.get(userId); + + if (null == subMap) { + subMap = new HashMap<>(); + mMap.put(userId, subMap); + } + + subMap.put(deviceId, object); + } + } + + /** + * Defines the objects map for an user Id + * + * @param objectsPerDevices the objects maps + * @param userId the user id + */ + public void setObjects(Map objectsPerDevices, String userId) { + if (!TextUtils.isEmpty(userId)) { + if (null == objectsPerDevices) { + mMap.remove(userId); + } else { + mMap.put(userId, new HashMap<>(objectsPerDevices)); + } + } + } + + /** + * Removes objects for a dedicated user + * + * @param userId the user id. + */ + public void removeUserObjects(String userId) { + if (!TextUtils.isEmpty(userId)) { + mMap.remove(userId); + } + } + + /** + * Clear the internal dictionary + */ + public void removeAllObjects() { + mMap.clear(); + } + + /** + * Add entries from another MXUsersDevicesMap + * + * @param other the other one + */ + public void addEntriesFromMap(MXUsersDevicesMap other) { + if (null != other) { + mMap.putAll(other.getMap()); + } + } + + @Override + public String toString() { + if (null != mMap) { + return "MXUsersDevicesMap " + mMap.toString(); + } else { + return "MXDeviceInfo : null map"; + } + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/event/EncryptedEventContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/event/EncryptedEventContent.kt new file mode 100644 index 00000000..fc574eb1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/event/EncryptedEventContent.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2016 OpenMarket Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.internal.crypto.model.event + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class representing an encrypted event content + */ +@JsonClass(generateAdapter = true) +data class EncryptedEventContent( + + /** + * the used algorithm + */ + @Json(name = "algorithm") + var algorithm: String? = null, + + /** + * The encrypted event + */ + @Json(name = "ciphertext") + var ciphertext: String? = null, + + /** + * The device id + */ + @Json(name = "device_id") + var deviceId: String? = null, + + /** + * the sender key + */ + @Json(name = "sender_key") + var senderKey: String? = null, + + /** + * The session id + */ + @Json(name = "session_id") + var sessionId: String? = null +) \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/event/EncryptionEventContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/event/EncryptionEventContent.kt new file mode 100644 index 00000000..22df41e9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/event/EncryptionEventContent.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2016 OpenMarket Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.internal.crypto.model.event + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class representing an encrypted event content + */ +@JsonClass(generateAdapter = true) +data class EncryptionEventContent( + + /** + * Required. The encryption algorithm to be used to encrypt messages sent in this room. Must be 'm.megolm.v1.aes-sha2'. + */ + @Json(name = "algorithm") + var algorithm: String, + + /** + * How long the session should be used before changing it. 604800000 (a week) is the recommended default. + */ + @Json(name = "rotation_period_ms") + var rotationPeriodMs: Long? = null, + + /** + * How many messages should be sent before changing the session. 100 is the recommended default. + */ + @Json(name = "rotation_period_msgs") + var rotationPeriodMsgs: Long? = null +) \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/event/NewDeviceContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/event/NewDeviceContent.kt new file mode 100644 index 00000000..a6777a4f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/event/NewDeviceContent.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2016 OpenMarket Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.internal.crypto.model.event + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class NewDeviceContent( + + // the device id + @Json(name = "device_id") + var deviceId: String? = null, + + // the room ids list + @Json(name = "rooms") + var rooms: List? = null +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/event/OlmEventContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/event/OlmEventContent.kt new file mode 100644 index 00000000..7ac0b075 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/event/OlmEventContent.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2016 OpenMarket Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.internal.crypto.model.event + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class representing an encrypted event content + */ +@JsonClass(generateAdapter = true) +data class OlmEventContent( + /** + * + */ + @Json(name = "ciphertext") + var ciphertext: Map? = null, + + /** + * the sender key + */ + @Json(name = "sender_key") + var senderKey: String? = null +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/event/OlmPayloadContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/event/OlmPayloadContent.kt new file mode 100644 index 00000000..e97ff8f6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/event/OlmPayloadContent.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2016 OpenMarket Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.internal.crypto.model.event + +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.internal.di.MoshiProvider + +/** + * Class representing the OLM payload content + */ +@JsonClass(generateAdapter = true) +data class OlmPayloadContent( + /** + * The room id + */ + var room_id: String? = null, + + /** + * The sender + */ + var sender: String? = null, + + /** + * The recipient + */ + var recipient: String? = null, + + /** + * the recipient keys + */ + var recipient_keys: Map? = null, + + /** + * The keys + */ + var keys: Map? = null +) { + fun toJsonString(): String { + return MoshiProvider.providesMoshi().adapter(OlmPayloadContent::class.java).toJson(this) + } + + companion object { + fun fromJsonString(str: String): OlmPayloadContent { + return MoshiProvider.providesMoshi().adapter(OlmPayloadContent::class.java).fromJson(str)!! + } + } +} + + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/event/RoomKeyContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/event/RoomKeyContent.kt new file mode 100644 index 00000000..cafcb14d --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/event/RoomKeyContent.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2016 OpenMarket Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.internal.crypto.model.event + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class representing an sharekey content + */ +@JsonClass(generateAdapter = true) +data class RoomKeyContent( + + @Json(name = "algorithm") + var algorithm: String? = null, + + @Json(name = "room_id") + var roomId: String? = null, + + @Json(name = "session_id") + var sessionId: String? = null, + + @Json(name = "session_key") + var sessionKey: String? = null, + + // should be a Long but it is sometimes a double + @Json(name = "chain_index") + var chainIndex: Any? = null +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/DeleteDeviceAuth.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/DeleteDeviceAuth.kt new file mode 100644 index 00000000..f50fbaba --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/DeleteDeviceAuth.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2016 OpenMarket Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * This class provides the + */ +@JsonClass(generateAdapter = true) +data class DeleteDeviceAuth( + + // device device session id + @Json(name = "session") + var session: String? = null, + + // registration information + @Json(name = "type") + var type: String? = null, + + var user: String? = null, + var password: String? = null +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/DeleteDeviceParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/DeleteDeviceParams.kt new file mode 100644 index 00000000..ba3ed903 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/DeleteDeviceParams.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2016 OpenMarket Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.internal.crypto.model.rest + +import com.squareup.moshi.JsonClass + +/** + * This class provides the parameter to delete a device + */ +@JsonClass(generateAdapter = true) +data class DeleteDeviceParams( + var auth: DeleteDeviceAuth? = null +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/DeviceInfo.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/DeviceInfo.kt new file mode 100644 index 00000000..c43e1287 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/DeviceInfo.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2014 OpenMarket Ltd + * Copyright 2017 Vector Creations Ltd + * 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 im.vector.matrix.android.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.interfaces.DatedObject + +/** + * This class describes the device information + */ +@JsonClass(generateAdapter = true) +data class DeviceInfo( + /** + * The owner user id (not documented and useless but the homeserver sent it. You should not need it) + */ + @Json(name = "user_id") + var user_id: String? = null, + + /** + * The device id + */ + @Json(name = "device_id") + var deviceId: String? = null, + + /** + * The device display name + */ + @Json(name = "display_name") + var displayName: String? = null, + + /** + * The last time this device has been seen. + */ + @Json(name = "last_seen_ts") + var lastSeenTs: Long = 0, + + /** + * The last ip address + */ + @Json(name = "last_seen_ip") + var lastSeenIp: String? = null +) : DatedObject { + + override val date: Long + get() = lastSeenTs + +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/DeviceKeys.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/DeviceKeys.kt new file mode 100644 index 00000000..3486bfda --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/DeviceKeys.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.util.JsonDict + +@JsonClass(generateAdapter = true) +data class DeviceKeys( + @Json(name = "user_id") + val userId: String, + + @Json(name = "device_id") + val deviceId: String, + + @Json(name = "algorithms") + val algorithms: List, + + @Json(name = "keys") + val keys: Map, + + @Json(name = "signatures") + val signatures: JsonDict +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/DevicesListResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/DevicesListResponse.kt new file mode 100644 index 00000000..9b50b486 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/DevicesListResponse.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2014 OpenMarket Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * This class describes the response to https://matrix.org/docs/spec/client_server/r0.4.0.html#get-matrix-client-r0-devices + */ +@JsonClass(generateAdapter = true) +data class DevicesListResponse( + @Json(name = "devices") + var devices: List? = null +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/EncryptedBodyFileInfo.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/EncryptedBodyFileInfo.kt new file mode 100644 index 00000000..a83001fe --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/EncryptedBodyFileInfo.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 im.vector.matrix.android.internal.crypto.model.rest + +import org.matrix.olm.OlmPkMessage + +/** + * Build from a OlmPkMessage object + * + * @param olmPkMessage OlmPkMessage + */ +class EncryptedBodyFileInfo(olmPkMessage: OlmPkMessage) { + var ciphertext = olmPkMessage.mCipherText + var mac = olmPkMessage.mMac + var ephemeral = olmPkMessage.mEphemeralKey +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/EncryptedFileInfo.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/EncryptedFileInfo.kt new file mode 100644 index 00000000..3e0d912f --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/EncryptedFileInfo.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2016 OpenMarket Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.internal.crypto.model.rest + +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class EncryptedFileInfo( + var url: String? = null, + var mimetype: String, + var key: EncryptedFileKey? = null, + var iv: String, + var hashes: Map, + var v: String? = null +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/EncryptedFileKey.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/EncryptedFileKey.kt new file mode 100644 index 00000000..433a3619 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/EncryptedFileKey.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2016 OpenMarket Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.internal.crypto.model.rest + +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class EncryptedFileKey( + var alg: String, + var ext: Boolean? = null, + var key_ops: List, + var kty: String, + var k: String +) + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/EncryptedMessage.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/EncryptedMessage.kt new file mode 100644 index 00000000..bd070e31 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/EncryptedMessage.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto.model.rest + + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class EncryptedMessage( + var algorithm: String? = null, + + @Json(name = "sender_key") + var senderKey: String? = null, + + @Json(name = "ciphertext") + var cipherText: Map? = null +) : SendToDeviceObject diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/ForwardedRoomKeyContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/ForwardedRoomKeyContent.kt new file mode 100644 index 00000000..a135c2a7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/ForwardedRoomKeyContent.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2016 OpenMarket Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class representing the forward room key request body content + */ +@JsonClass(generateAdapter = true) +data class ForwardedRoomKeyContent( + + @Json(name = "algorithm") + var algorithm: String? = null, + + @Json(name = "room_id") + var roomId: String? = null, + + @Json(name = "sender_key") + var senderKey: String? = null, + + @Json(name = "session_id") + var sessionId: String? = null, + + @Json(name = "session_key") + var sessionKey: String? = null, + + @Json(name = "forwarding_curve25519_key_chain") + var forwardingCurve25519KeyChain: List? = null, + + @Json(name = "sender_claimed_ed25519_key") + var senderClaimedEd25519Key: String? = null +) \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyChangesResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyChangesResponse.kt new file mode 100644 index 00000000..5a48f547 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyChangesResponse.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2017 Vector Creations Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * This class describes the key changes response + */ +@JsonClass(generateAdapter = true) +data class KeyChangesResponse( + // list of user ids which have new devices + @Json(name = "changed") + var changed: List? = null, + + // List of user ids who are no more tracked. + @Json(name = "left") + var left: List? = null +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationAccept.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationAccept.kt new file mode 100644 index 00000000..7be6f204 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationAccept.kt @@ -0,0 +1,96 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import timber.log.Timber + +/** + * Sent by Bob to accept a verification from a previously sent m.key.verification.start message. + */ +@JsonClass(generateAdapter = true) +data class KeyVerificationAccept( + + /** + * string to identify the transaction. + * This string must be unique for the pair of users performing verification for the duration that the transaction is valid. + * Alice’s device should record this ID and use it in future messages in this transaction. + */ + @Json(name = "transaction_id") + var transactionID: String? = null, + + /** + * The key agreement protocol that Bob’s device has selected to use, out of the list proposed by Alice’s device + */ + @Json(name = "key_agreement_protocol") + var keyAgreementProtocol: String? = null, + + /** + * The hash algorithm that Bob’s device has selected to use, out of the list proposed by Alice’s device + */ + var hash: String? = null, + + /** + * The message authentication code that Bob’s device has selected to use, out of the list proposed by Alice’s device + */ + @Json(name = "message_authentication_code") + var messageAuthenticationCode: String? = null, + + /** + * An array of short authentication string methods that Bob’s client (and Bob) understands. Must be a subset of the list proposed by Alice’s device + */ + @Json(name = "short_authentication_string") + var shortAuthenticationStrings: List? = null, + + /** + * The hash (encoded as unpadded base64) of the concatenation of the device’s ephemeral public key (QB, encoded as unpadded base64) + * and the canonical JSON representation of the m.key.verification.start message. + */ + var commitment: String? = null +) : SendToDeviceObject { + + fun isValid(): Boolean { + if (transactionID.isNullOrBlank() + || keyAgreementProtocol.isNullOrBlank() + || hash.isNullOrBlank() + || commitment.isNullOrBlank() + || messageAuthenticationCode.isNullOrBlank() + || shortAuthenticationStrings.isNullOrEmpty()) { + Timber.e("## received invalid verification request") + return false + } + return true + } + + companion object { + fun create(tid: String, + keyAgreementProtocol: String, + hash: String, + commitment: String, + messageAuthenticationCode: String, + shortAuthenticationStrings: List): KeyVerificationAccept { + return KeyVerificationAccept().apply { + this.transactionID = tid + this.keyAgreementProtocol = keyAgreementProtocol + this.hash = hash + this.commitment = commitment + this.messageAuthenticationCode = messageAuthenticationCode + this.shortAuthenticationStrings = shortAuthenticationStrings + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationCancel.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationCancel.kt new file mode 100644 index 00000000..1d4a9b34 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationCancel.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.session.crypto.sas.CancelCode + +/** + * To device event sent by either party to cancel a key verification. + */ +@JsonClass(generateAdapter = true) +data class KeyVerificationCancel( + /** + * the transaction ID of the verification to cancel + */ + @Json(name = "transaction_id") + var transactionID: String? = null, + + /** + * machine-readable reason for cancelling, see #CancelCode + */ + var code: String? = null, + + /** + * human-readable reason for cancelling. This should only be used if the receiving client does not understand the code given. + */ + var reason: String? = null +) : SendToDeviceObject { + + companion object { + fun create(tid: String, cancelCode: CancelCode): KeyVerificationCancel { + return KeyVerificationCancel().apply { + this.transactionID = tid + this.code = cancelCode.value + this.reason = cancelCode.humanReadable + } + } + } + + fun isValid(): Boolean { + if (transactionID.isNullOrBlank() || code.isNullOrBlank()) { + return false + } + return true + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationKey.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationKey.kt new file mode 100644 index 00000000..b5e33232 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationKey.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + + +/** + * Sent by both devices to send their ephemeral Curve25519 public key to the other device. + */ +@JsonClass(generateAdapter = true) +data class KeyVerificationKey( + /** + * the ID of the transaction that the message is part of + */ + @Json(name = "transaction_id") + @JvmField + var transactionID: String? = null, + + /** + * The device’s ephemeral public key, as an unpadded base64 string + */ + @JvmField + var key: String? = null + +) : SendToDeviceObject { + + companion object { + fun create(tid: String, key: String): KeyVerificationKey { + return KeyVerificationKey().apply { + this.transactionID = tid + this.key = key + } + } + } + + fun isValid(): Boolean { + if (transactionID.isNullOrBlank() || key.isNullOrBlank()) { + return false + } + return true + } + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationMac.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationMac.kt new file mode 100644 index 00000000..19dbbe5d --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationMac.kt @@ -0,0 +1,66 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Sent by both devices to send the MAC of their device key to the other device. + */ +@JsonClass(generateAdapter = true) +data class KeyVerificationMac( + /** + * the ID of the transaction that the message is part of + */ + @Json(name = "transaction_id") + var transactionID: String? = null, + + /** + * A map of key ID to the MAC of the key, as an unpadded base64 string, calculated using the MAC key + */ + @JvmField + var mac: Map? = null, + + /** + * The MAC of the comma-separated, sorted list of key IDs given in the mac property, + * as an unpadded base64 string, calculated using the MAC key. + * For example, if the mac property gives MACs for the keys ed25519:ABCDEFG and ed25519:HIJKLMN, then this property will + * give the MAC of the string “ed25519:ABCDEFG,ed25519:HIJKLMN”. + */ + @JvmField + var keys: String? = null + +) : SendToDeviceObject { + + fun isValid(): Boolean { + if (transactionID.isNullOrBlank() || keys.isNullOrBlank() || mac.isNullOrEmpty()) { + return false + } + return true + } + + companion object { + fun create(tid: String, mac: Map, keys: String): KeyVerificationMac { + return KeyVerificationMac().apply { + this.transactionID = tid + this.mac = mac + this.keys = keys + } + } + } + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationStart.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationStart.kt new file mode 100644 index 00000000..3c621ec6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeyVerificationStart.kt @@ -0,0 +1,97 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.internal.crypto.model.rest + +import com.squareup.moshi.Json +import im.vector.matrix.android.api.session.crypto.sas.SasMode +import im.vector.matrix.android.internal.crypto.verification.SASVerificationTransaction +import timber.log.Timber + +/** + * Sent by Alice to initiate an interactive key verification. + */ +class KeyVerificationStart : SendToDeviceObject { + + /** + * Alice’s device ID + */ + @Json(name = "from_device") + var fromDevice: String? = null + + var method: String? = null + + /** + * String to identify the transaction. + * This string must be unique for the pair of users performing verification for the duration that the transaction is valid. + * Alice’s device should record this ID and use it in future messages in this transaction. + */ + @Json(name = "transaction_id") + var transactionID: String? = null + + /** + * An array of key agreement protocols that Alice’s client understands. + * Must include “curve25519”. + * Other methods may be defined in the future + */ + @Json(name = "key_agreement_protocols") + var keyAgreementProtocols: List? = null + + /** + * An array of hashes that Alice’s client understands. + * Must include “sha256”. Other methods may be defined in the future. + */ + var hashes: List? = null + + /** + * An array of message authentication codes that Alice’s client understands. + * Must include “hkdf-hmac-sha256”. + * Other methods may be defined in the future. + */ + @Json(name = "message_authentication_codes") + var messageAuthenticationCodes: List? = null + + /** + * An array of short authentication string methods that Alice’s client (and Alice) understands. + * Must include “decimal”. + * This document also describes the “emoji” method. + * Other methods may be defined in the future + */ + @Json(name = "short_authentication_string") + var shortAuthenticationStrings: List? = null + + + companion object { + const val VERIF_METHOD_SAS = "m.sas.v1" + } + + fun isValid(): Boolean { + if (transactionID.isNullOrBlank() + || fromDevice.isNullOrBlank() + || method != VERIF_METHOD_SAS + || keyAgreementProtocols.isNullOrEmpty() + || hashes.isNullOrEmpty() + || hashes?.contains("sha256") == false + || messageAuthenticationCodes.isNullOrEmpty() + || (messageAuthenticationCodes?.contains(SASVerificationTransaction.SAS_MAC_SHA256) == false + && messageAuthenticationCodes?.contains(SASVerificationTransaction.SAS_MAC_SHA256_LONGKDF) == false) + || shortAuthenticationStrings.isNullOrEmpty() + || shortAuthenticationStrings?.contains(SasMode.DECIMAL) == false) { + Timber.e("## received invalid verification request") + return false + } + return true + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysClaimBody.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysClaimBody.kt new file mode 100644 index 00000000..f92c5afd --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysClaimBody.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2016 OpenMarket Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * This class represents the response to /keys/query request made by claimOneTimeKeysForUsersDevices. + */ +@JsonClass(generateAdapter = true) +data class KeysClaimBody( + + @Json(name = "timeout") + var timeout: Int? = null, + + /** + * Required. The keys to be claimed. A map from user ID, to a map from device ID to algorithm name. + */ + @Json(name = "one_time_keys") + var oneTimeKeys: Map> +) + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysClaimResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysClaimResponse.kt new file mode 100644 index 00000000..c1fbfb1c --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysClaimResponse.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2016 OpenMarket Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * This class represents the response to /keys/query request made by claimOneTimeKeysForUsersDevices. + */ +@JsonClass(generateAdapter = true) +data class KeysClaimResponse( + + /** + * The requested keys ordered by device by user. + * TODO Type does not match spec, should be Map + */ + @Json(name = "one_time_keys") + var oneTimeKeys: Map>>>? = null +) + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysQueryBody.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysQueryBody.kt new file mode 100644 index 00000000..769a28f7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysQueryBody.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2017 Vector Creations Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * This class represents the body to /keys/query + */ +@JsonClass(generateAdapter = true) +data class KeysQueryBody( + + /** + * The time (in milliseconds) to wait when downloading keys from remote servers. 10 seconds is the recommended default. + */ + @Json(name = "timeout") + var timeout: Int? = null, + + /** + * Required. The keys to be downloaded. + * A map from user ID, to a list of device IDs, or to an empty list to indicate all devices for the corresponding user. + */ + @Json(name = "device_keys") + val deviceKeys: Map, + + /** + * If the client is fetching keys as a result of a device update received in a sync request, this should be the 'since' token + * of that sync request, or any later sync token. This allows the server to ensure its response contains the keys advertised + * by the notification in that sync. + */ + @Json(name = "token") + var token: String? = null + +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysQueryResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysQueryResponse.kt new file mode 100644 index 00000000..d13bf995 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysQueryResponse.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2017 Vector Creations Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo + +/** + * This class represents the response to /keys/query request made by downloadKeysForUsers + */ +@JsonClass(generateAdapter = true) +data class KeysQueryResponse( + /** + * The device keys per devices per users. + * Map from userId to map from deviceId to MXDeviceInfo + * TODO Use MXUsersDevicesMap? + */ + @Json(name = "device_keys") + var deviceKeys: Map>? = null, + + /** + * The failures sorted by homeservers. TODO Bad comment ? + * TODO Use MXUsersDevicesMap? + */ + var failures: Map>? = null +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysUploadBody.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysUploadBody.kt new file mode 100644 index 00000000..f67cbd47 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysUploadBody.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.util.JsonDict + +@JsonClass(generateAdapter = true) +data class KeysUploadBody( + @Json(name = "device_keys") + var deviceKeys: DeviceKeys? = null, + + @Json(name = "one_time_keys") + var oneTimeKeys: JsonDict? = null +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysUploadResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysUploadResponse.kt new file mode 100644 index 00000000..72e148ba --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysUploadResponse.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2016 OpenMarket Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * This class represents the response to /keys/upload request made by uploadKeys. + */ +@JsonClass(generateAdapter = true) +data class KeysUploadResponse( + + /** + * The count per algorithm as returned by the home server: a map (algorithm to count). + */ + @Json(name = "one_time_key_counts") + var oneTimeKeyCounts: Map? = null + +) { + /** + * Helper methods to extract information from 'oneTimeKeyCounts' + * + * @param algorithm the expected algorithm + * @return the time key counts + */ + fun oneTimeKeyCountsForAlgorithm(algorithm: String): Int { + return oneTimeKeyCounts?.get(algorithm) ?: 0 + } + + /** + * Tells if there is a oneTimeKeys for a dedicated algorithm. + * + * @param algorithm the algorithm + * @return true if it is found + */ + fun hasOneTimeKeyCountsForAlgorithm(algorithm: String): Boolean { + return oneTimeKeyCounts?.containsKey(algorithm) == true + } +} + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyRequestBody.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyRequestBody.kt new file mode 100644 index 00000000..4dc0d1d0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyRequestBody.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Class representing an room key request body content + */ +@JsonClass(generateAdapter = true) +data class RoomKeyRequestBody( + var algorithm: String? = null, + + @Json(name = "room_id") + var roomId: String? = null, + + @Json(name = "sender_key") + var senderKey: String? = null, + + @Json(name = "session_id") + var sessionId: String? = null +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyShare.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyShare.kt new file mode 100644 index 00000000..5d7d49f5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyShare.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.internal.crypto.model.rest + +import com.squareup.moshi.Json + +/** + * Parent class representing an room key action request + * Note: this class cannot be abstract because of [org.matrix.androidsdk.core.JsonUtils.toRoomKeyShare] + */ +open class RoomKeyShare : SendToDeviceObject { + + var action: String? = null + + @Json(name = "requesting_device_id") + var requestingDeviceId: String? = null + + @Json(name = "request_id") + var requestId: String? = null + + companion object { + const val ACTION_SHARE_REQUEST = "request" + const val ACTION_SHARE_CANCELLATION = "request_cancellation" + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyShareCancellation.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyShareCancellation.kt new file mode 100644 index 00000000..d763c554 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyShareCancellation.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.internal.crypto.model.rest + +import com.squareup.moshi.JsonClass + +/** + * Class representing an room key request cancellation content + */ +@JsonClass(generateAdapter = true) +class RoomKeyShareCancellation : RoomKeyShare() { + init { + action = ACTION_SHARE_CANCELLATION + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyShareRequest.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyShareRequest.kt new file mode 100644 index 00000000..43743258 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/RoomKeyShareRequest.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2016 OpenMarket Ltd + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.internal.crypto.model.rest + +import com.squareup.moshi.JsonClass + +/** + * Class representing an room key request content + */ +@JsonClass(generateAdapter = true) +class RoomKeyShareRequest : RoomKeyShare() { + + var body: RoomKeyRequestBody? = null + + init { + action = ACTION_SHARE_REQUEST + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/SendToDeviceBody.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/SendToDeviceBody.kt new file mode 100644 index 00000000..cf1166df --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/SendToDeviceBody.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto.model.rest + +class SendToDeviceBody { + + // `Any` should implement SendToDeviceObject, but we cannot use interface here because of Gson serialization + /** + * The messages to send. A map from user ID, to a map from device ID to message body. + * The device ID may also be *, meaning all known devices for the user. + */ + var messages: Map>? = null +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/SendToDeviceObject.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/SendToDeviceObject.kt new file mode 100644 index 00000000..e69debbd --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/SendToDeviceObject.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto.model.rest + +interface SendToDeviceObject diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/UpdateDeviceInfoBody.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/UpdateDeviceInfoBody.kt new file mode 100644 index 00000000..3a7da69d --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/UpdateDeviceInfoBody.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2016 OpenMarket Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto.model.rest + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class UpdateDeviceInfoBody( + + /** + * The new display name for this device. If not given, the display name is unchanged. + */ + @Json(name = "display_name") + var displayName: String? = null + +) + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/IMXCryptoStore.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/IMXCryptoStore.kt new file mode 100644 index 00000000..d08ba7d9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/IMXCryptoStore.kt @@ -0,0 +1,375 @@ +/* + * Copyright 2016 OpenMarket Ltd + * 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 im.vector.matrix.android.internal.crypto.store + +import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest +import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest +import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity +import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo +import im.vector.matrix.android.internal.crypto.model.MXOlmInboundGroupSession2 +import im.vector.matrix.android.internal.crypto.model.MXOlmSession +import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody +import org.matrix.olm.OlmAccount + +/** + * the crypto data store + */ +internal interface IMXCryptoStore { + + /** + * @return the device id + */ + fun getDeviceId(): String + + /** + * @return the olm account + */ + fun getAccount(): OlmAccount? + + /** + * Retrieve the known inbound group sessions. + * + * @return the list of all known group sessions, to export them. + */ + fun getInboundGroupSessions(): List + + /** + * @return true to unilaterally blacklist all unverified devices. + */ + fun getGlobalBlacklistUnverifiedDevices(): Boolean + + /** + * Set the global override for whether the client should ever send encrypted + * messages to unverified devices. + * If false, it can still be overridden per-room. + * If true, it overrides the per-room settings. + * + * @param block true to unilaterally blacklist all + */ + fun setGlobalBlacklistUnverifiedDevices(block: Boolean) + + /** + * Provides the rooms ids list in which the messages are not encrypted for the unverified devices. + * + * @return the room Ids list + */ + fun getRoomsListBlacklistUnverifiedDevices(): List + + /** + * Updates the rooms ids list in which the messages are not encrypted for the unverified devices. + * + * @param roomIds the room ids list + */ + fun setRoomsListBlacklistUnverifiedDevices(roomIds: List) + + /** + * Get the current keys backup version + */ + fun getKeyBackupVersion(): String? + + /** + * Set the current keys backup version + * + * @param keyBackupVersion the keys backup version or null to delete it + */ + fun setKeyBackupVersion(keyBackupVersion: String?) + + /** + * Get the current keys backup local data + */ + fun getKeysBackupData(): KeysBackupDataEntity? + + /** + * Set the keys backup local data + * + * @param keysBackupData the keys backup local data, or null to erase data + */ + fun setKeysBackupData(keysBackupData: KeysBackupDataEntity?) + + /** + * @return the devices statuses map (userId -> tracking status) + */ + fun getDeviceTrackingStatuses(): Map + + /** + * @return the pending IncomingRoomKeyRequest requests + */ + fun getPendingIncomingRoomKeyRequests(): List + + /** + * Indicate if the store contains data for the passed account. + * + * @return true means that the user enabled the crypto in a previous session + */ + fun hasData(): Boolean + + /** + * Delete the crypto store for the passed credentials. + */ + fun deleteStore() + + /** + * open any existing crypto store + */ + fun open() + + /** + * Close the store + */ + fun close() + + /** + * Store the device id. + * + * @param deviceId the device id + */ + fun storeDeviceId(deviceId: String) + + /** + * Store the end to end account for the logged-in user. + * + * @param account the account to save + */ + fun storeAccount(account: OlmAccount) + + /** + * Store a device for a user. + * + * @param userId the user's id. + * @param device the device to store. + */ + fun storeUserDevice(userId: String?, device: MXDeviceInfo?) + + /** + * Retrieve a device for a user. + * + * @param deviceId the device id. + * @param userId the user's id. + * @return the device + */ + fun getUserDevice(deviceId: String, userId: String): MXDeviceInfo? + + /** + * Retrieve a device by its identity key. + * + * @param identityKey the device identity key (`MXDeviceInfo.identityKey`) + * @return the device or null if not found + */ + fun deviceWithIdentityKey(identityKey: String): MXDeviceInfo? + + /** + * Store the known devices for a user. + * + * @param userId The user's id. + * @param devices A map from device id to 'MXDevice' object for the device. + */ + fun storeUserDevices(userId: String, devices: Map) + + /** + * Retrieve the known devices for a user. + * + * @param userId The user's id. + * @return The devices map if some devices are known, else null + */ + fun getUserDevices(userId: String): Map? + + /** + * Store the crypto algorithm for a room. + * + * @param roomId the id of the room. + * @param algorithm the algorithm. + */ + fun storeRoomAlgorithm(roomId: String, algorithm: String) + + /** + * Provides the algorithm used in a dedicated room. + * + * @param roomId the room id + * @return the algorithm, null is the room is not encrypted + */ + fun getRoomAlgorithm(roomId: String): String? + + /** + * Store a session between the logged-in user and another device. + * + * @param session the end-to-end session. + * @param deviceKey the public key of the other device. + */ + fun storeSession(session: MXOlmSession, deviceKey: String) + + /** + * Retrieve the end-to-end session ids between the logged-in user and another + * device. + * + * @param deviceKey the public key of the other device. + * @return A set of sessionId, or null if device is not known + */ + fun getDeviceSessionIds(deviceKey: String): Set? + + /** + * Retrieve an end-to-end session between the logged-in user and another + * device. + * + * @param sessionId the session Id. + * @param deviceKey the public key of the other device. + * @return The Base64 end-to-end session, or null if not found + */ + fun getDeviceSession(sessionId: String?, deviceKey: String?): MXOlmSession? + + /** + * Retrieve the last used sessionId, regarding `lastReceivedMessageTs`, or null if no session exist + * + * @param deviceKey the public key of the other device. + * @return last used sessionId, or null if not found + */ + fun getLastUsedSessionId(deviceKey: String): String? + + /** + * Store inbound group sessions. + * + * @param sessions the inbound group sessions to store. + */ + fun storeInboundGroupSessions(sessions: List) + + /** + * Retrieve an inbound group session. + * + * @param sessionId the session identifier. + * @param senderKey the base64-encoded curve25519 key of the sender. + * @return an inbound group session. + */ + fun getInboundGroupSession(sessionId: String, senderKey: String): MXOlmInboundGroupSession2? + + /** + * Remove an inbound group session + * + * @param sessionId the session identifier. + * @param senderKey the base64-encoded curve25519 key of the sender. + */ + fun removeInboundGroupSession(sessionId: String, senderKey: String) + + /* ========================================================================================== + * Keys backup + * ========================================================================================== */ + + /** + * Mark all inbound group sessions as not backed up. + */ + fun resetBackupMarkers() + + /** + * Mark inbound group sessions as backed up on the user homeserver. + * + * @param sessions the sessions + */ + fun markBackupDoneForInboundGroupSessions(sessions: List) + + /** + * 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. + */ + fun inboundGroupSessionsToBackup(limit: Int): List + + /** + * Number of stored inbound group sessions. + * + * @param onlyBackedUp if true, count only session marked as backed up. + * @return a count. + */ + fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int + + /** + * Save the device statuses + * + * @param deviceTrackingStatuses the device tracking statuses + */ + fun saveDeviceTrackingStatuses(deviceTrackingStatuses: Map) + + /** + * Get the tracking status of a specified userId devices. + * + * @param userId the user id + * @param defaultValue the default value + * @return the tracking status + */ + fun getDeviceTrackingStatus(userId: String, defaultValue: Int): Int + + /** + * Look for an existing outgoing room key request, and if none is found, return null + * + * @param requestBody the request body + * @return an OutgoingRoomKeyRequest instance or null + */ + fun getOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody): OutgoingRoomKeyRequest? + + /** + * Look for an existing outgoing room key request, and if none is found, add a new one. + * + * @param request the request + * @return either the same instance as passed in, or the existing one. + */ + fun getOrAddOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest): OutgoingRoomKeyRequest? + + /** + * Look for room key requests by state. + * + * @param states the states + * @return an OutgoingRoomKeyRequest or null + */ + fun getOutgoingRoomKeyRequestByState(states: Set): OutgoingRoomKeyRequest? + + /** + * Update an existing outgoing request. + * + * @param request the request + */ + fun updateOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest) + + /** + * Delete an outgoing room key request. + * + * @param transactionId the transaction id. + */ + fun deleteOutgoingRoomKeyRequest(transactionId: String) + + /** + * Store an incomingRoomKeyRequest instance + * + * @param incomingRoomKeyRequest the incoming key request + */ + fun storeIncomingRoomKeyRequest(incomingRoomKeyRequest: IncomingRoomKeyRequest?) + + /** + * Delete an incomingRoomKeyRequest instance + * + * @param incomingRoomKeyRequest the incoming key request + */ + fun deleteIncomingRoomKeyRequest(incomingRoomKeyRequest: IncomingRoomKeyRequest) + + /** + * Search an IncomingRoomKeyRequest + * + * @param userId the user id + * @param deviceId the device id + * @param requestId the request id + * @return an IncomingRoomKeyRequest if it exists, else null + */ + fun getIncomingRoomKeyRequest(userId: String, deviceId: String, requestId: String): IncomingRoomKeyRequest? +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/Helper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/Helper.kt new file mode 100644 index 00000000..cbf941f1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/Helper.kt @@ -0,0 +1,127 @@ +/* + * 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 im.vector.matrix.android.internal.crypto.store.db + +import android.util.Base64 +import im.vector.matrix.android.internal.util.CompatUtil +import io.realm.Realm +import io.realm.RealmConfiguration +import io.realm.RealmObject +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.ObjectInputStream +import java.io.ObjectOutputStream +import java.security.MessageDigest +import java.util.zip.GZIPInputStream + + +/** + * Compute a Hash of a String, using md5 algorithm + */ +fun String.hash() = try { + val digest = MessageDigest.getInstance("md5") + digest.update(toByteArray()) + val bytes = digest.digest() + val sb = StringBuilder() + for (i in bytes.indices) { + sb.append(String.format("%02X", bytes[i])) + } + sb.toString().toLowerCase() +} catch (exc: Exception) { + // Should not happen, but just in case + hashCode().toString() +} + +/** + * Get realm, invoke the action, close realm, and return the result of the action + */ +fun doWithRealm(realmConfiguration: RealmConfiguration, action: (Realm) -> T): T { + val realm = Realm.getInstance(realmConfiguration) + val result = action.invoke(realm) + realm.close() + return result +} + +/** + * Get realm, do the query, copy from realm, close realm, and return the copied result + */ +fun doRealmQueryAndCopy(realmConfiguration: RealmConfiguration, action: (Realm) -> T?): T? { + val realm = Realm.getInstance(realmConfiguration) + val result = action.invoke(realm) + val copiedResult = result?.let { realm.copyFromRealm(result) } + realm.close() + return copiedResult +} + +/** + * Get realm, do the list query, copy from realm, close realm, and return the copied result + */ +fun doRealmQueryAndCopyList(realmConfiguration: RealmConfiguration, action: (Realm) -> Iterable): Iterable { + val realm = Realm.getInstance(realmConfiguration) + val result = action.invoke(realm) + val copiedResult = realm.copyFromRealm(result) + realm.close() + return copiedResult +} + +/** + * Get realm instance, invoke the action in a transaction and close realm + */ +fun doRealmTransaction(realmConfiguration: RealmConfiguration, action: (Realm) -> Unit) { + val realm = Realm.getInstance(realmConfiguration) + realm.executeTransaction { action.invoke(it) } + realm.close() +} + +/** + * Serialize any Serializable object, zip it and convert to Base64 String + */ +fun serializeForRealm(o: Any?): String? { + if (o == null) { + return null + } + + val baos = ByteArrayOutputStream() + val gzis = CompatUtil.createGzipOutputStream(baos) + val out = ObjectOutputStream(gzis) + + out.writeObject(o) + out.close() + + return Base64.encodeToString(baos.toByteArray(), Base64.DEFAULT) +} + +/** + * Do the opposite of serializeForRealm. + */ +fun deserializeFromRealm(string: String?): T? { + if (string == null) { + return null + } + + val decodedB64 = Base64.decode(string.toByteArray(), Base64.DEFAULT) + + val bais = ByteArrayInputStream(decodedB64) + val gzis = GZIPInputStream(bais) + val ois = ObjectInputStream(gzis) + + val result = ois.readObject() as T + + ois.close() + + return result +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStore.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStore.kt new file mode 100644 index 00000000..c37ab767 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStore.kt @@ -0,0 +1,708 @@ +/* + * 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 im.vector.matrix.android.internal.crypto.store.db + +import android.text.TextUtils +import im.vector.matrix.android.api.auth.data.Credentials +import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest +import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest +import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore +import im.vector.matrix.android.internal.crypto.store.db.model.* +import im.vector.matrix.android.internal.crypto.store.db.query.delete +import im.vector.matrix.android.internal.crypto.store.db.query.getById +import im.vector.matrix.android.internal.crypto.store.db.query.getOrCreate +import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo +import im.vector.matrix.android.internal.crypto.model.MXOlmInboundGroupSession2 +import im.vector.matrix.android.internal.crypto.model.MXOlmSession +import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody +import io.realm.RealmConfiguration +import io.realm.Sort +import io.realm.kotlin.where +import org.matrix.olm.OlmAccount +import org.matrix.olm.OlmException +import timber.log.Timber +import kotlin.collections.set + +// enableFileEncryption is used to migrate the previous store +internal class RealmCryptoStore(private val enableFileEncryption: Boolean = false, + private val realmConfiguration: RealmConfiguration, + private val credentials: Credentials) : IMXCryptoStore { + + /* ========================================================================================== + * Memory cache, to correctly release JNI objects + * ========================================================================================== */ + + // The olm account + private var olmAccount: OlmAccount? = null + + // Cache for OlmSession, to release them properly + private val olmSessionsToRelease = HashMap() + + // Cache for InboundGroupSession, to release them properly + private val inboundGroupSessionToRelease = HashMap() + + /* ========================================================================================== + * Other data + * ========================================================================================== */ + + override fun hasData(): Boolean { + return doWithRealm(realmConfiguration) { + !it.isEmpty + // Check if there is a MetaData object + && it.where().count() > 0 + } + } + + override fun deleteStore() { + doRealmTransaction(realmConfiguration) { + it.deleteAll() + } + } + + override fun open() { + // Ensure CryptoMetadataEntity is inserted in DB + doWithRealm(realmConfiguration) { realm -> + var currentMetadata = realm.where().findFirst() + + var deleteAll = false + + if (currentMetadata != null) { + // Check credentials + // The device id may not have been provided in credentials. + // Check it only if provided, else trust the stored one. + if (!TextUtils.equals(currentMetadata.userId, credentials.userId) + || (credentials.deviceId != null && !TextUtils.equals(credentials.deviceId, currentMetadata.deviceId))) { + Timber.w("## open() : Credentials do not match, close this store and delete data") + deleteAll = true + currentMetadata = null + } + } + + if (currentMetadata == null) { + realm.executeTransaction { + if (deleteAll) { + it.deleteAll() + } + + // Metadata not found, or database cleaned, create it + it.createObject(CryptoMetadataEntity::class.java, credentials.userId).apply { + deviceId = credentials.deviceId + } + } + } + } + } + + override fun close() { + olmSessionsToRelease.forEach { + it.value.olmSession.releaseSession() + } + olmSessionsToRelease.clear() + + inboundGroupSessionToRelease.forEach { + it.value.mSession?.releaseSession() + } + inboundGroupSessionToRelease.clear() + + olmAccount?.releaseAccount() + } + + override fun storeDeviceId(deviceId: String) { + doRealmTransaction(realmConfiguration) { + it.where().findFirst()?.deviceId = deviceId + } + } + + override fun getDeviceId(): String { + return doRealmQueryAndCopy(realmConfiguration) { + it.where().findFirst() + }?.deviceId ?: "" + } + + override fun storeAccount(account: OlmAccount) { + olmAccount = account + + doRealmTransaction(realmConfiguration) { + it.where().findFirst()?.putOlmAccount(account) + } + } + + override fun getAccount(): OlmAccount? { + if (olmAccount == null) { + olmAccount = doRealmQueryAndCopy(realmConfiguration) { it.where().findFirst() }?.getOlmAccount() + } + + return olmAccount + } + + override fun storeUserDevice(userId: String?, deviceInfo: MXDeviceInfo?) { + if (userId == null || deviceInfo == null) { + return + } + + doRealmTransaction(realmConfiguration) { + val user = UserEntity.getOrCreate(it, userId) + + // Create device info + val deviceInfoEntity = DeviceInfoEntity.getOrCreate(it, userId, deviceInfo.deviceId).apply { + deviceId = deviceInfo.deviceId + identityKey = deviceInfo.identityKey() + putDeviceInfo(deviceInfo) + } + + if (!user.devices.contains(deviceInfoEntity)) { + user.devices.add(deviceInfoEntity) + } + } + } + + override fun getUserDevice(deviceId: String, userId: String): MXDeviceInfo? { + return doRealmQueryAndCopy(realmConfiguration) { + it.where() + .equalTo(DeviceInfoEntityFields.PRIMARY_KEY, DeviceInfoEntity.createPrimaryKey(userId, deviceId)) + .findFirst() + } + ?.getDeviceInfo() + } + + override fun deviceWithIdentityKey(identityKey: String): MXDeviceInfo? { + return doRealmQueryAndCopy(realmConfiguration) { + it.where() + .equalTo(DeviceInfoEntityFields.IDENTITY_KEY, identityKey) + .findFirst() + } + ?.getDeviceInfo() + } + + override fun storeUserDevices(userId: String, devices: Map) { + if (userId == null) { + return + } + doRealmTransaction(realmConfiguration) { realm -> + if (devices == null) { + // Remove the user + UserEntity.delete(realm, userId) + } else { + UserEntity.getOrCreate(realm, userId) + .let { u -> + // Add the devices + // Ensure all other devices are deleted + u.devices.deleteAllFromRealm() + + u.devices.addAll( + devices.map { + DeviceInfoEntity.getOrCreate(realm, userId, it.value.deviceId).apply { + deviceId = it.value.deviceId + identityKey = it.value.identityKey() + putDeviceInfo(it.value) + } + } + ) + } + } + } + } + + override fun getUserDevices(userId: String): Map? { + return doRealmQueryAndCopy(realmConfiguration) { + it.where() + .equalTo(UserEntityFields.USER_ID, userId) + .findFirst() + } + ?.devices + ?.mapNotNull { it.getDeviceInfo() } + ?.associateBy { it.deviceId } + } + + override fun storeRoomAlgorithm(roomId: String, algorithm: String) { + doRealmTransaction(realmConfiguration) { + CryptoRoomEntity.getOrCreate(it, roomId).algorithm = algorithm + } + } + + override fun getRoomAlgorithm(roomId: String): String? { + return doRealmQueryAndCopy(realmConfiguration) { + CryptoRoomEntity.getById(it, roomId) + } + ?.algorithm + } + + override fun storeSession(session: MXOlmSession, deviceKey: String) { + var sessionIdentifier: String? = null + + try { + sessionIdentifier = session.olmSession.sessionIdentifier() + } catch (e: OlmException) { + Timber.e(e, "## storeSession() : sessionIdentifier failed " + e.message) + } + + if (sessionIdentifier != null) { + val key = OlmSessionEntity.createPrimaryKey(sessionIdentifier, deviceKey) + + // Release memory of previously known session, if it is not the same one + if (olmSessionsToRelease[key]?.olmSession != session.olmSession) { + olmSessionsToRelease[key]?.olmSession?.releaseSession() + } + + olmSessionsToRelease[key] = session + + doRealmTransaction(realmConfiguration) { + val realmOlmSession = OlmSessionEntity().apply { + primaryKey = key + sessionId = sessionIdentifier + this.deviceKey = deviceKey + putOlmSession(session.olmSession) + lastReceivedMessageTs = session.lastReceivedMessageTs + } + + it.insertOrUpdate(realmOlmSession) + } + } + } + + override fun getDeviceSession(sessionId: String?, deviceKey: String?): MXOlmSession? { + if (sessionId == null || deviceKey == null) { + return null + } + + val key = OlmSessionEntity.createPrimaryKey(sessionId, deviceKey) + + // If not in cache (or not found), try to read it from realm + if (olmSessionsToRelease[key] == null) { + doRealmQueryAndCopy(realmConfiguration) { + it.where() + .equalTo(OlmSessionEntityFields.PRIMARY_KEY, key) + .findFirst() + } + ?.let { + val olmSession = it.getOlmSession() + if (olmSession != null && it.sessionId != null) { + olmSessionsToRelease[key] = MXOlmSession(olmSession, it.lastReceivedMessageTs) + } + } + } + + return olmSessionsToRelease[key] + } + + override fun getLastUsedSessionId(deviceKey: String): String? { + return doRealmQueryAndCopy(realmConfiguration) { + it.where() + .equalTo(OlmSessionEntityFields.DEVICE_KEY, deviceKey) + .sort(OlmSessionEntityFields.LAST_RECEIVED_MESSAGE_TS, Sort.DESCENDING) + .findFirst() + } + ?.sessionId + } + + override fun getDeviceSessionIds(deviceKey: String): MutableSet { + return doRealmQueryAndCopyList(realmConfiguration) { + it.where() + .equalTo(OlmSessionEntityFields.DEVICE_KEY, deviceKey) + .findAll() + } + .mapNotNull { + it.sessionId + } + .toMutableSet() + } + + override fun storeInboundGroupSessions(sessions: List) { + if (sessions.isEmpty()) { + return + } + + doRealmTransaction(realmConfiguration) { + sessions.forEach { session -> + var sessionIdentifier: String? = null + + try { + sessionIdentifier = session.mSession?.sessionIdentifier() + } catch (e: OlmException) { + Timber.e(e, "## storeInboundGroupSession() : sessionIdentifier failed " + e.message) + } + + if (sessionIdentifier != null) { + val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionIdentifier, session.mSenderKey) + + // Release memory of previously known session, if it is not the same one + if (inboundGroupSessionToRelease[key] != session) { + inboundGroupSessionToRelease[key]?.mSession?.releaseSession() + } + + inboundGroupSessionToRelease[key] = session + + val realmOlmInboundGroupSession = OlmInboundGroupSessionEntity().apply { + primaryKey = key + sessionId = sessionIdentifier + senderKey = session.mSenderKey + putInboundGroupSession(session) + } + + it.insertOrUpdate(realmOlmInboundGroupSession) + } + } + } + } + + override fun getInboundGroupSession(sessionId: String, senderKey: String): MXOlmInboundGroupSession2? { + val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionId, senderKey) + + // If not in cache (or not found), try to read it from realm + if (inboundGroupSessionToRelease[key] == null) { + doRealmQueryAndCopy(realmConfiguration) { + it.where() + .equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key) + .findFirst() + } + ?.getInboundGroupSession() + ?.let { + inboundGroupSessionToRelease[key] = it + } + } + + return inboundGroupSessionToRelease[key] + } + + /** + * Note: the result will be only use to export all the keys and not to use the MXOlmInboundGroupSession2, + * so there is no need to use or update `inboundGroupSessionToRelease` for native memory management + */ + override fun getInboundGroupSessions(): MutableList { + return doRealmQueryAndCopyList(realmConfiguration) { + it.where() + .findAll() + } + .mapNotNull { + it.getInboundGroupSession() + } + .toMutableList() + } + + override fun removeInboundGroupSession(sessionId: String, senderKey: String) { + val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionId, senderKey) + + // Release memory of previously known session + inboundGroupSessionToRelease[key]?.mSession?.releaseSession() + inboundGroupSessionToRelease.remove(key) + + doRealmTransaction(realmConfiguration) { + it.where() + .equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key) + .findAll() + .deleteAllFromRealm() + } + } + + /* ========================================================================================== + * 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 getKeysBackupData(): KeysBackupDataEntity? { + return doRealmQueryAndCopy(realmConfiguration) { + it.where().findFirst() + } + } + + override fun setKeysBackupData(keysBackupData: KeysBackupDataEntity?) { + doRealmTransaction(realmConfiguration) { + if (keysBackupData == null) { + // Clear the table + it.where() + .findAll() + .deleteAllFromRealm() + } else { + // Only one object + it.copyToRealmOrUpdate(keysBackupData) + } + } + } + + override fun resetBackupMarkers() { + doRealmTransaction(realmConfiguration) { + it.where() + .findAll() + .map { inboundGroupSession -> + inboundGroupSession.backedUp = false + } + } + } + + override fun markBackupDoneForInboundGroupSessions(sessions: List) { + if (sessions.isEmpty()) { + return + } + + doRealmTransaction(realmConfiguration) { + sessions.forEach { session -> + try { + val key = OlmInboundGroupSessionEntity.createPrimaryKey(session.mSession?.sessionIdentifier(), session.mSenderKey) + + it.where() + .equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key) + .findFirst() + ?.backedUp = true + } catch (e: OlmException) { + Timber.e(e, "OlmException") + } + } + } + } + + 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 + } + } + + override fun getGlobalBlacklistUnverifiedDevices(): Boolean { + return doRealmQueryAndCopy(realmConfiguration) { + it.where().findFirst() + }?.globalBlacklistUnverifiedDevices + ?: false + } + + override fun setRoomsListBlacklistUnverifiedDevices(roomIds: List) { + doRealmTransaction(realmConfiguration) { + // Reset all + it.where() + .findAll() + .forEach { room -> + room.blacklistUnverifiedDevices = false + } + + // Enable those in the list + it.where() + .`in`(CryptoRoomEntityFields.ROOM_ID, roomIds.toTypedArray()) + .findAll() + .forEach { room -> + room.blacklistUnverifiedDevices = true + } + } + } + + override fun getRoomsListBlacklistUnverifiedDevices(): MutableList { + return doRealmQueryAndCopyList(realmConfiguration) { + it.where() + .equalTo(CryptoRoomEntityFields.BLACKLIST_UNVERIFIED_DEVICES, true) + .findAll() + } + .mapNotNull { + it.roomId + } + .toMutableList() + } + + override fun getDeviceTrackingStatuses(): MutableMap { + return doRealmQueryAndCopyList(realmConfiguration) { + it.where() + .findAll() + } + .associateBy { + it.userId!! + } + .mapValues { + it.value.deviceTrackingStatus + } + .toMutableMap() + } + + override fun saveDeviceTrackingStatuses(deviceTrackingStatuses: Map) { + doRealmTransaction(realmConfiguration) { + deviceTrackingStatuses + .map { entry -> + UserEntity.getOrCreate(it, entry.key) + .deviceTrackingStatus = entry.value + } + } + } + + override fun getDeviceTrackingStatus(userId: String, defaultValue: Int): Int { + return doRealmQueryAndCopy(realmConfiguration) { + it.where() + .equalTo(UserEntityFields.USER_ID, userId) + .findFirst() + } + ?.deviceTrackingStatus + ?: defaultValue + } + + override fun getOutgoingRoomKeyRequest(requestBody: RoomKeyRequestBody): OutgoingRoomKeyRequest? { + return doRealmQueryAndCopy(realmConfiguration) { + it.where() + .equalTo(OutgoingRoomKeyRequestEntityFields.REQUEST_BODY_ALGORITHM, requestBody.algorithm) + .equalTo(OutgoingRoomKeyRequestEntityFields.REQUEST_BODY_ROOM_ID, requestBody.roomId) + .equalTo(OutgoingRoomKeyRequestEntityFields.REQUEST_BODY_SENDER_KEY, requestBody.senderKey) + .equalTo(OutgoingRoomKeyRequestEntityFields.REQUEST_BODY_SESSION_ID, requestBody.sessionId) + .findFirst() + } + ?.toOutgoingRoomKeyRequest() + } + + override fun getOrAddOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest): OutgoingRoomKeyRequest? { + if (request.mRequestBody == null) { + return null + } + + val existingOne = getOutgoingRoomKeyRequest(request.mRequestBody!!) + + if (existingOne != null) { + return existingOne + } + + // Insert the request and return the one passed in parameter + doRealmTransaction(realmConfiguration) { + it.createObject(OutgoingRoomKeyRequestEntity::class.java, request.mRequestId).apply { + putRequestBody(request.mRequestBody) + putRecipients(request.mRecipients) + cancellationTxnId = request.mCancellationTxnId + state = request.mState.ordinal + } + } + + return request + } + + override fun getOutgoingRoomKeyRequestByState(states: Set): OutgoingRoomKeyRequest? { + return doRealmQueryAndCopy(realmConfiguration) { + it.where() + .`in`(OutgoingRoomKeyRequestEntityFields.STATE, states.map { it.ordinal }.toTypedArray()) + .findFirst() + } + ?.toOutgoingRoomKeyRequest() + } + + override fun updateOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest) { + doRealmTransaction(realmConfiguration) { + val obj = OutgoingRoomKeyRequestEntity().apply { + requestId = request.mRequestId + cancellationTxnId = request.mCancellationTxnId + state = request.mState.ordinal + putRecipients(request.mRecipients) + putRequestBody(request.mRequestBody) + } + + it.insertOrUpdate(obj) + } + } + + override fun deleteOutgoingRoomKeyRequest(transactionId: String) { + doRealmTransaction(realmConfiguration) { + it.where() + .equalTo(OutgoingRoomKeyRequestEntityFields.REQUEST_ID, transactionId) + .findFirst() + ?.deleteFromRealm() + } + } + + override fun storeIncomingRoomKeyRequest(incomingRoomKeyRequest: IncomingRoomKeyRequest?) { + if (incomingRoomKeyRequest == null) { + return + } + + doRealmTransaction(realmConfiguration) { + // Delete any previous store request with the same parameters + it.where() + .equalTo(IncomingRoomKeyRequestEntityFields.USER_ID, incomingRoomKeyRequest.mUserId) + .equalTo(IncomingRoomKeyRequestEntityFields.DEVICE_ID, incomingRoomKeyRequest.mDeviceId) + .equalTo(IncomingRoomKeyRequestEntityFields.REQUEST_ID, incomingRoomKeyRequest.mRequestId) + .findAll() + .deleteAllFromRealm() + + // Then store it + it.createObject(IncomingRoomKeyRequestEntity::class.java).apply { + userId = incomingRoomKeyRequest.mUserId + deviceId = incomingRoomKeyRequest.mDeviceId + requestId = incomingRoomKeyRequest.mRequestId + putRequestBody(incomingRoomKeyRequest.mRequestBody) + } + } + } + + override fun deleteIncomingRoomKeyRequest(incomingRoomKeyRequest: IncomingRoomKeyRequest) { + doRealmTransaction(realmConfiguration) { + it.where() + .equalTo(IncomingRoomKeyRequestEntityFields.USER_ID, incomingRoomKeyRequest.mUserId) + .equalTo(IncomingRoomKeyRequestEntityFields.DEVICE_ID, incomingRoomKeyRequest.mDeviceId) + .equalTo(IncomingRoomKeyRequestEntityFields.REQUEST_ID, incomingRoomKeyRequest.mRequestId) + .findAll() + .deleteAllFromRealm() + } + } + + override fun getIncomingRoomKeyRequest(userId: String, deviceId: String, requestId: String): IncomingRoomKeyRequest? { + return doRealmQueryAndCopy(realmConfiguration) { + it.where() + .equalTo(IncomingRoomKeyRequestEntityFields.USER_ID, userId) + .equalTo(IncomingRoomKeyRequestEntityFields.DEVICE_ID, deviceId) + .equalTo(IncomingRoomKeyRequestEntityFields.REQUEST_ID, requestId) + .findFirst() + } + ?.toIncomingRoomKeyRequest() + } + + override fun getPendingIncomingRoomKeyRequests(): MutableList { + return doRealmQueryAndCopyList(realmConfiguration) { + it.where() + .findAll() + } + .map { + it.toIncomingRoomKeyRequest() + } + .toMutableList() + } + + companion object { + private const val LOG_TAG = "RealmCryptoStore" + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreMigration.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreMigration.kt new file mode 100644 index 00000000..00af9dab --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreMigration.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 im.vector.matrix.android.internal.crypto.store.db + +import io.realm.DynamicRealm +import io.realm.RealmMigration +import timber.log.Timber + +internal object RealmCryptoStoreMigration : RealmMigration { + + const val CRYPTO_STORE_SCHEMA_VERSION = 0L + + override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { + Timber.d("Migrating Realm Crypto from $oldVersion to $newVersion") + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreModule.kt new file mode 100644 index 00000000..96d69b7b --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/RealmCryptoStoreModule.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto.store.db + +import io.realm.annotations.RealmModule +import im.vector.matrix.android.internal.crypto.store.db.model.* + +/** + * Realm module for Crypto store classes + */ +@RealmModule(library = true, + classes = [ + CryptoMetadataEntity::class, + CryptoRoomEntity::class, + DeviceInfoEntity::class, + IncomingRoomKeyRequestEntity::class, + KeysBackupDataEntity::class, + OlmInboundGroupSessionEntity::class, + OlmSessionEntity::class, + OutgoingRoomKeyRequestEntity::class, + UserEntity::class + ]) +internal class RealmCryptoStoreModule \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/CryptoMetadataEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/CryptoMetadataEntity.kt new file mode 100644 index 00000000..19cc06fa --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/CryptoMetadataEntity.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 im.vector.matrix.android.internal.crypto.store.db.model + +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey +import im.vector.matrix.android.internal.crypto.store.db.deserializeFromRealm +import im.vector.matrix.android.internal.crypto.store.db.serializeForRealm +import org.matrix.olm.OlmAccount + +internal open class CryptoMetadataEntity( + // The current user id. + @PrimaryKey var userId: String? = null, + // The current device id. + var deviceId: String? = null, + // Serialized OlmAccount + var olmAccountData: String? = null, + // The sync token corresponding to the device list. // TODO? + var deviceSyncToken: String? = null, + // Settings for blacklisting unverified devices. + var globalBlacklistUnverifiedDevices: Boolean = false, + // The keys backup version currently used. Null means no backup. + var backupVersion: String? = null +) : RealmObject() { + + + // Deserialize data + fun getOlmAccount(): OlmAccount? { + return deserializeFromRealm(olmAccountData) + } + + // Serialize data + fun putOlmAccount(olmAccount: OlmAccount?) { + olmAccountData = serializeForRealm(olmAccount) + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/CryptoRoomEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/CryptoRoomEntity.kt new file mode 100644 index 00000000..09394051 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/CryptoRoomEntity.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 im.vector.matrix.android.internal.crypto.store.db.model + +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey + +internal open class CryptoRoomEntity( + @PrimaryKey var roomId: String? = null, + var algorithm: String? = null, + var blacklistUnverifiedDevices: Boolean = false) + : RealmObject() { + + companion object + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/DeviceInfoEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/DeviceInfoEntity.kt new file mode 100644 index 00000000..d690073a --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/DeviceInfoEntity.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 im.vector.matrix.android.internal.crypto.store.db.model + +import im.vector.matrix.android.internal.crypto.store.db.deserializeFromRealm +import im.vector.matrix.android.internal.crypto.store.db.serializeForRealm +import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo +import io.realm.RealmObject +import io.realm.RealmResults +import io.realm.annotations.LinkingObjects +import io.realm.annotations.PrimaryKey + +internal fun DeviceInfoEntity.Companion.createPrimaryKey(userId: String, deviceId: String) = "$userId|$deviceId" + +// deviceInfoData contains serialized data +internal open class DeviceInfoEntity(@PrimaryKey var primaryKey: String = "", + var deviceId: String? = null, + var identityKey: String? = null, + var deviceInfoData: String? = null) + : RealmObject() { + + // Deserialize data + fun getDeviceInfo(): MXDeviceInfo? { + return deserializeFromRealm(deviceInfoData) + } + + // Serialize data + fun putDeviceInfo(deviceInfo: MXDeviceInfo?) { + deviceInfoData = serializeForRealm(deviceInfo) + } + + @LinkingObjects("devices") + val users: RealmResults? = null + + companion object +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/IncomingRoomKeyRequestEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/IncomingRoomKeyRequestEntity.kt new file mode 100644 index 00000000..45a0c115 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/IncomingRoomKeyRequestEntity.kt @@ -0,0 +1,56 @@ +/* + * 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 im.vector.matrix.android.internal.crypto.store.db.model + +import io.realm.RealmObject +import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest +import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody + +internal open class IncomingRoomKeyRequestEntity( + var requestId: String? = null, + var userId: String? = null, + var deviceId: String? = null, + // RoomKeyRequestBody fields + var requestBodyAlgorithm: String? = null, + var requestBodyRoomId: String? = null, + var requestBodySenderKey: String? = null, + var requestBodySessionId: String? = null +) : RealmObject() { + + fun toIncomingRoomKeyRequest(): IncomingRoomKeyRequest { + return IncomingRoomKeyRequest().apply { + mRequestId = requestId + mUserId = userId + mDeviceId = deviceId + mRequestBody = RoomKeyRequestBody().apply { + algorithm = requestBodyAlgorithm + roomId = requestBodyRoomId + senderKey = requestBodySenderKey + sessionId = requestBodySessionId + } + } + } + + fun putRequestBody(requestBody: RoomKeyRequestBody?) { + requestBody?.let { + requestBodyAlgorithm = it.algorithm + requestBodyRoomId = it.roomId + requestBodySenderKey = it.senderKey + requestBodySessionId = it.sessionId + } + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/KeysBackupDataEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/KeysBackupDataEntity.kt new file mode 100644 index 00000000..53ebf146 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/KeysBackupDataEntity.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto.store.db.model + +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey + +internal open class KeysBackupDataEntity( + // Primary key to update this object. There is only one object, so it's a constant, please do not set it + @PrimaryKey + var primaryKey: Int = 0, + // The last known hash of the backed up keys on the server + var backupLastServerHash: String? = null, + // The last known number of backed up keys on the server + var backupLastServerNumberOfKeys: Int? = null +) : RealmObject() \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/OlmInboundGroupSessionEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/OlmInboundGroupSessionEntity.kt new file mode 100644 index 00000000..53747a04 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/OlmInboundGroupSessionEntity.kt @@ -0,0 +1,47 @@ +/* + * 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 im.vector.matrix.android.internal.crypto.store.db.model + +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey +import im.vector.matrix.android.internal.crypto.store.db.deserializeFromRealm +import im.vector.matrix.android.internal.crypto.store.db.serializeForRealm +import im.vector.matrix.android.internal.crypto.model.MXOlmInboundGroupSession2 + +internal fun OlmInboundGroupSessionEntity.Companion.createPrimaryKey(sessionId: String?, senderKey: String?) = "$sessionId|$senderKey" + +internal open class OlmInboundGroupSessionEntity( + // Combined value to build a primary key + @PrimaryKey var primaryKey: String? = null, + var sessionId: String? = null, + var senderKey: String? = null, + // olmInboundGroupSessionData contains Json + var olmInboundGroupSessionData: String? = null, + // Indicate if the key has been backed up to the homeserver + var backedUp: Boolean = false) + : RealmObject() { + + fun getInboundGroupSession(): MXOlmInboundGroupSession2? { + return deserializeFromRealm(olmInboundGroupSessionData) + } + + fun putInboundGroupSession(mxOlmInboundGroupSession2: MXOlmInboundGroupSession2?) { + olmInboundGroupSessionData = serializeForRealm(mxOlmInboundGroupSession2) + } + + companion object +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/OlmSessionEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/OlmSessionEntity.kt new file mode 100644 index 00000000..4425cf33 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/OlmSessionEntity.kt @@ -0,0 +1,44 @@ +/* + * 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 im.vector.matrix.android.internal.crypto.store.db.model + +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey +import im.vector.matrix.android.internal.crypto.store.db.deserializeFromRealm +import im.vector.matrix.android.internal.crypto.store.db.serializeForRealm +import org.matrix.olm.OlmSession + +internal fun OlmSessionEntity.Companion.createPrimaryKey(sessionId: String, deviceKey: String) = "$sessionId|$deviceKey" + +// olmSessionData is a serialized OlmSession +internal open class OlmSessionEntity(@PrimaryKey var primaryKey: String = "", + var sessionId: String? = null, + var deviceKey: String? = null, + var olmSessionData: String? = null, + var lastReceivedMessageTs: Long = 0) + : RealmObject() { + + fun getOlmSession(): OlmSession? { + return deserializeFromRealm(olmSessionData) + } + + fun putOlmSession(olmSession: OlmSession?) { + olmSessionData = serializeForRealm(olmSession) + } + + companion object +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/OutgoingRoomKeyRequestEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/OutgoingRoomKeyRequestEntity.kt new file mode 100644 index 00000000..f4cc732e --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/OutgoingRoomKeyRequestEntity.kt @@ -0,0 +1,77 @@ +/* + * 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 im.vector.matrix.android.internal.crypto.store.db.model + +import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequest +import im.vector.matrix.android.internal.crypto.store.db.deserializeFromRealm +import im.vector.matrix.android.internal.crypto.store.db.serializeForRealm +import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey + +internal open class OutgoingRoomKeyRequestEntity( + @PrimaryKey var requestId: String? = null, + var cancellationTxnId: String? = null, + // Serialized Json + var recipientsData: String? = null, + // RoomKeyRequestBody fields + var requestBodyAlgorithm: String? = null, + var requestBodyRoomId: String? = null, + var requestBodySenderKey: String? = null, + var requestBodySessionId: String? = null, + // State + var state: Int = 0 +) : RealmObject() { + + /** + * Convert to OutgoingRoomKeyRequest + */ + fun toOutgoingRoomKeyRequest(): OutgoingRoomKeyRequest { + return OutgoingRoomKeyRequest( + RoomKeyRequestBody().apply { + algorithm = requestBodyAlgorithm + roomId = requestBodyRoomId + senderKey = requestBodySenderKey + sessionId = requestBodySessionId + }, + getRecipients()!!, + requestId!!, + OutgoingRoomKeyRequest.RequestState.from(state) + ).apply { + this.mCancellationTxnId = cancellationTxnId + } + } + + private fun getRecipients(): List>? { + return deserializeFromRealm(recipientsData) + } + + fun putRecipients(recipients: List>?) { + recipientsData = serializeForRealm(recipients) + } + + fun putRequestBody(requestBody: RoomKeyRequestBody?) { + requestBody?.let { + requestBodyAlgorithm = it.algorithm + requestBodyRoomId = it.roomId + requestBodySenderKey = it.senderKey + requestBodySessionId = it.sessionId + } + } +} + + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/UserEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/UserEntity.kt new file mode 100644 index 00000000..27cd7fe2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/UserEntity.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 im.vector.matrix.android.internal.crypto.store.db.model + +import io.realm.RealmList +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey + +internal open class UserEntity(@PrimaryKey var userId: String? = null, + var devices: RealmList = RealmList(), + var deviceTrackingStatus: Int = 0) + : RealmObject() { + + companion object +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/query/CryptoRoomEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/query/CryptoRoomEntityQueries.kt new file mode 100644 index 00000000..d7ab87a4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/query/CryptoRoomEntityQueries.kt @@ -0,0 +1,41 @@ +/* + * 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 im.vector.matrix.android.internal.crypto.store.db.query + +import im.vector.matrix.android.internal.crypto.store.db.model.CryptoRoomEntity +import im.vector.matrix.android.internal.crypto.store.db.model.CryptoRoomEntityFields +import io.realm.Realm +import io.realm.kotlin.where + +/** + * Get or create a room + */ +internal fun CryptoRoomEntity.Companion.getOrCreate(realm: Realm, roomId: String): CryptoRoomEntity { + return getById(realm, roomId) + ?: let { + realm.createObject(CryptoRoomEntity::class.java, roomId) + } +} + +/** + * Get a room + */ +internal fun CryptoRoomEntity.Companion.getById(realm: Realm, roomId: String): CryptoRoomEntity? { + return realm.where() + .equalTo(CryptoRoomEntityFields.ROOM_ID, roomId) + .findFirst() +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/query/DeviceInfoEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/query/DeviceInfoEntityQueries.kt new file mode 100644 index 00000000..2aea3cd2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/query/DeviceInfoEntityQueries.kt @@ -0,0 +1,37 @@ +/* + * 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 im.vector.matrix.android.internal.crypto.store.db.query + +import io.realm.Realm +import io.realm.kotlin.where +import im.vector.matrix.android.internal.crypto.store.db.model.DeviceInfoEntity +import im.vector.matrix.android.internal.crypto.store.db.model.DeviceInfoEntityFields +import im.vector.matrix.android.internal.crypto.store.db.model.createPrimaryKey + +/** + * Get or create a device info + */ +internal fun DeviceInfoEntity.Companion.getOrCreate(realm: Realm, userId: String, deviceId: String): DeviceInfoEntity { + return realm.where() + .equalTo(DeviceInfoEntityFields.PRIMARY_KEY, DeviceInfoEntity.createPrimaryKey(userId, deviceId)) + .findFirst() + ?: let { + realm.createObject(DeviceInfoEntity::class.java, DeviceInfoEntity.createPrimaryKey(userId, deviceId)).apply { + this.deviceId = deviceId + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/query/UserEntitiesQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/query/UserEntitiesQueries.kt new file mode 100644 index 00000000..dd5278ce --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/query/UserEntitiesQueries.kt @@ -0,0 +1,45 @@ +/* + * 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 im.vector.matrix.android.internal.crypto.store.db.query + +import io.realm.Realm +import io.realm.kotlin.where +import im.vector.matrix.android.internal.crypto.store.db.model.UserEntity +import im.vector.matrix.android.internal.crypto.store.db.model.UserEntityFields + +/** + * Get or create a user + */ +internal fun UserEntity.Companion.getOrCreate(realm: Realm, userId: String): UserEntity { + return realm.where() + .equalTo(UserEntityFields.USER_ID, userId) + .findFirst() + ?: let { + realm.createObject(UserEntity::class.java, userId) + } +} + +/** + * Delete a user + */ +internal fun UserEntity.Companion.delete(realm: Realm, userId: String) { + realm.where() + .equalTo(UserEntityFields.USER_ID, userId) + .findFirst() + ?.deleteFromRealm() +} + diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/ClaimOneTimeKeysForUsersDeviceTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/ClaimOneTimeKeysForUsersDeviceTask.kt new file mode 100644 index 00000000..06e979b4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/ClaimOneTimeKeysForUsersDeviceTask.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto.tasks + +import arrow.core.Try +import im.vector.matrix.android.internal.crypto.api.CryptoApi +import im.vector.matrix.android.internal.crypto.model.MXKey +import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap +import im.vector.matrix.android.internal.crypto.model.rest.KeysClaimBody +import im.vector.matrix.android.internal.crypto.model.rest.KeysClaimResponse +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.task.Task +import timber.log.Timber +import java.util.* + +internal interface ClaimOneTimeKeysForUsersDeviceTask : Task> { + data class Params( + // a list of users, devices and key types to retrieve keys for. + val usersDevicesKeyTypesMap: MXUsersDevicesMap + ) +} + +internal class DefaultClaimOneTimeKeysForUsersDevice(private val cryptoApi: CryptoApi) + : ClaimOneTimeKeysForUsersDeviceTask { + + override fun execute(params: ClaimOneTimeKeysForUsersDeviceTask.Params): Try> { + val body = KeysClaimBody(oneTimeKeys = params.usersDevicesKeyTypesMap.map) + + return executeRequest { + apiCall = cryptoApi.claimOneTimeKeysForUsersDevices(body) + }.flatMap { keysClaimResponse -> + Try { + val map = HashMap>() + + if (null != keysClaimResponse.oneTimeKeys) { + for (userId in keysClaimResponse.oneTimeKeys!!.keys) { + val mapByUserId = keysClaimResponse.oneTimeKeys!![userId] + + val keysMap = HashMap() + + for (deviceId in mapByUserId!!.keys) { + try { + keysMap[deviceId] = MXKey(mapByUserId[deviceId]) + } catch (e: Exception) { + Timber.e(e, "## claimOneTimeKeysForUsersDevices : fail to create a MXKey ") + } + + } + + if (keysMap.size != 0) { + map[userId] = keysMap + } + } + } + + MXUsersDevicesMap(map) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DeleteDeviceTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DeleteDeviceTask.kt new file mode 100644 index 00000000..4630bd93 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DeleteDeviceTask.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto.tasks + +import arrow.core.Try +import im.vector.matrix.android.internal.crypto.api.CryptoApi +import im.vector.matrix.android.internal.crypto.model.rest.DeleteDeviceParams +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.task.Task + +internal interface DeleteDeviceTask : Task { + data class Params( + val deviceId: String, + val accountPassword: String + ) +} + +internal class DefaultDeleteDeviceTask(private val cryptoApi: CryptoApi) + : DeleteDeviceTask { + + override fun execute(params: DeleteDeviceTask.Params): Try { + return executeRequest { + apiCall = cryptoApi.deleteDevice(params.deviceId, + DeleteDeviceParams()) + } + + // TODO Recover error, see legacy code MXSession.deleteDevice() + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DownloadKeysForUsersTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DownloadKeysForUsersTask.kt new file mode 100644 index 00000000..22c552b4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/DownloadKeysForUsersTask.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto.tasks + +import android.text.TextUtils +import arrow.core.Try +import im.vector.matrix.android.internal.crypto.api.CryptoApi +import im.vector.matrix.android.internal.crypto.model.rest.KeysQueryBody +import im.vector.matrix.android.internal.crypto.model.rest.KeysQueryResponse +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.task.Task +import java.util.* + +internal interface DownloadKeysForUsersTask : Task { + data class Params( + // the list of users to get keys for. + val userIds: List?, + // the up-to token + val token: String?) +} + +internal class DefaultDownloadKeysForUsers(private val cryptoApi: CryptoApi) + : DownloadKeysForUsersTask { + + override fun execute(params: DownloadKeysForUsersTask.Params): Try { + val downloadQuery = HashMap>() + + if (null != params.userIds) { + for (userId in params.userIds) { + downloadQuery[userId] = HashMap() + } + } + + val body = KeysQueryBody( + deviceKeys = downloadQuery + ) + + if (!TextUtils.isEmpty(params.token)) { + body.token = params.token + } + + return executeRequest { + apiCall = cryptoApi.downloadKeysForUsers(body) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/GetDevicesTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/GetDevicesTask.kt new file mode 100644 index 00000000..b7c706d2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/GetDevicesTask.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto.tasks + +import arrow.core.Try +import im.vector.matrix.android.internal.crypto.api.CryptoApi +import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.task.Task + +internal interface GetDevicesTask : Task + +internal class DefaultGetDevicesTask(private val cryptoApi: CryptoApi) + : GetDevicesTask { + + override fun execute(params: Unit): Try { + return executeRequest { + apiCall = cryptoApi.getDevices() + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/GetKeyChangesTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/GetKeyChangesTask.kt new file mode 100644 index 00000000..e4f9f7ae --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/GetKeyChangesTask.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto.tasks + +import arrow.core.Try +import im.vector.matrix.android.internal.crypto.api.CryptoApi +import im.vector.matrix.android.internal.crypto.model.rest.KeyChangesResponse +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.task.Task + +internal interface GetKeyChangesTask : Task { + data class Params( + // the start token. + val from: String, + // the up-to token. + val to: String + ) +} + +internal class DefaultGetKeyChangesTask(private val cryptoApi: CryptoApi) + : GetKeyChangesTask { + + override fun execute(params: GetKeyChangesTask.Params): Try { + return executeRequest { + apiCall = cryptoApi.getKeyChanges(params.from, + params.to) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/SendToDeviceTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/SendToDeviceTask.kt new file mode 100644 index 00000000..c9a99f8a --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/SendToDeviceTask.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto.tasks + +import arrow.core.Try +import im.vector.matrix.android.internal.crypto.api.CryptoApi +import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap +import im.vector.matrix.android.internal.crypto.model.rest.SendToDeviceBody +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.task.Task +import java.util.* + +internal interface SendToDeviceTask : Task { + data class Params( + // the type of event to send + val eventType: String, + // the content to send. Map from user_id to device_id to content dictionary. + val contentMap: MXUsersDevicesMap, + // the transactionId + val transactionId: String? = null + ) +} + +internal class DefaultSendToDeviceTask(private val cryptoApi: CryptoApi) + : SendToDeviceTask { + + override fun execute(params: SendToDeviceTask.Params): Try { + val sendToDeviceBody = SendToDeviceBody() + sendToDeviceBody.messages = params.contentMap.map + + return executeRequest { + apiCall = cryptoApi.sendToDevice( + params.eventType, + params.transactionId ?: Random().nextInt(Integer.MAX_VALUE).toString(), + sendToDeviceBody + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/SetDeviceNameTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/SetDeviceNameTask.kt new file mode 100644 index 00000000..aba5b5c7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/SetDeviceNameTask.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto.tasks + +import android.text.TextUtils +import arrow.core.Try +import im.vector.matrix.android.internal.crypto.api.CryptoApi +import im.vector.matrix.android.internal.crypto.model.rest.UpdateDeviceInfoBody +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.task.Task + +internal interface SetDeviceNameTask : Task { + data class Params( + // the device id + val deviceId: String, + // the device name + val deviceName: String + ) +} + +internal class DefaultSetDeviceNameTask(private val cryptoApi: CryptoApi) + : SetDeviceNameTask { + + override fun execute(params: SetDeviceNameTask.Params): Try { + val body = UpdateDeviceInfoBody( + displayName = if (TextUtils.isEmpty(params.deviceName)) "" else params.deviceName + ) + + return executeRequest { + apiCall = cryptoApi.updateDeviceInfo(params.deviceId, body) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/UploadKeysTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/UploadKeysTask.kt new file mode 100644 index 00000000..e985032a --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/UploadKeysTask.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto.tasks + +import arrow.core.Try +import im.vector.matrix.android.api.util.JsonDict +import im.vector.matrix.android.internal.crypto.api.CryptoApi +import im.vector.matrix.android.internal.crypto.model.rest.KeysUploadResponse +import im.vector.matrix.android.internal.crypto.model.rest.DeviceKeys +import im.vector.matrix.android.internal.crypto.model.rest.KeysUploadBody +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.task.Task +import im.vector.matrix.android.internal.util.convertToUTF8 + +internal interface UploadKeysTask : Task { + data class Params( + // the device keys to send. + val deviceKeys: DeviceKeys?, + // the one-time keys to send. + val oneTimeKeys: JsonDict?, + // the explicit device_id to use for upload (default is to use the same as that used during auth). + val deviceId: String) +} + +internal class DefaultUploadKeysTask(private val cryptoApi: CryptoApi) + : UploadKeysTask { + + override fun execute(params: UploadKeysTask.Params): Try { + val encodedDeviceId = convertToUTF8(params.deviceId) + + val body = KeysUploadBody() + + if (null != params.deviceKeys) { + body.deviceKeys = params.deviceKeys + } + + if (null != params.oneTimeKeys) { + body.oneTimeKeys = params.oneTimeKeys + } + + return executeRequest { + if (encodedDeviceId.isNullOrBlank()) { + apiCall = cryptoApi.uploadKeys(body) + } else { + apiCall = cryptoApi.uploadKeys(encodedDeviceId, body) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultSasVerificationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultSasVerificationService.kt new file mode 100644 index 00000000..441d7a41 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultSasVerificationService.kt @@ -0,0 +1,446 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.crypto.verification + +import android.os.Handler +import android.os.Looper +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.auth.data.Credentials +import im.vector.matrix.android.api.session.crypto.sas.CancelCode +import im.vector.matrix.android.api.session.crypto.sas.SasVerificationService +import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState +import im.vector.matrix.android.api.session.crypto.sas.safeValueOf +import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.internal.crypto.CryptoAsyncHelper +import im.vector.matrix.android.internal.crypto.DeviceListManager +import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore +import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo +import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap +import im.vector.matrix.android.internal.crypto.model.rest.* +import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask +import im.vector.matrix.android.internal.task.TaskExecutor +import im.vector.matrix.android.internal.task.configureWith +import timber.log.Timber +import java.util.* +import kotlin.collections.HashMap + +/** + * Manages all current verifications transactions with short codes. + * Short codes interactive verification is a more user friendly way of verifying devices + * that is still maintaining a good level of security (alternative to the 43-character strings compare method). + */ +internal class DefaultSasVerificationService(private val mCredentials: Credentials, + private val mCryptoStore: IMXCryptoStore, + private val deviceListManager: DeviceListManager, + private val mSendToDeviceTask: SendToDeviceTask, + private val mTaskExecutor: TaskExecutor) + : VerificationTransaction.Listener, SasVerificationService { + + private val uiHandler = Handler(Looper.getMainLooper()) + + // map [sender : [transaction]] + private val txMap = HashMap>() + + // Event received from the sync + fun onToDeviceEvent(event: Event) { + CryptoAsyncHelper.getDecryptBackgroundHandler().post { + when (event.type) { + EventType.KEY_VERIFICATION_START -> { + onStartRequestReceived(event) + } + EventType.KEY_VERIFICATION_CANCEL -> { + onCancelReceived(event) + } + EventType.KEY_VERIFICATION_ACCEPT -> { + onAcceptReceived(event) + } + EventType.KEY_VERIFICATION_KEY -> { + onKeyReceived(event) + } + EventType.KEY_VERIFICATION_MAC -> { + onMacReceived(event) + } + else -> { + //ignore + } + } + } + } + + // Internal listener + private lateinit var mCryptoListener: SasCryptoListener + + + private var listeners = ArrayList() + + override fun addListener(listener: SasVerificationService.SasVerificationListener) { + uiHandler.post { + if (!listeners.contains(listener)) { + listeners.add(listener) + } + } + } + + override fun removeListener(listener: SasVerificationService.SasVerificationListener) { + uiHandler.post { + listeners.remove(listener) + } + } + + private fun dispatchTxAdded(tx: VerificationTransaction) { + uiHandler.post { + listeners.forEach { + try { + it.transactionCreated(tx) + } catch (e: Throwable) { + Timber.e(e, "## Error while notifying listeners") + } + + } + } + } + + private fun dispatchTxUpdated(tx: VerificationTransaction) { + uiHandler.post { + listeners.forEach { + try { + it.transactionUpdated(tx) + } catch (e: Throwable) { + Timber.e(e, "## Error while notifying listeners") + } + } + } + } + + override fun markedLocallyAsManuallyVerified(userId: String, deviceID: String) { + mCryptoListener.setDeviceVerification(MXDeviceInfo.DEVICE_VERIFICATION_VERIFIED, + deviceID, + userId, + object : MatrixCallback { + override fun onSuccess(data: Unit) { + uiHandler.post { + listeners.forEach { + try { + it.markedAsManuallyVerified(userId, deviceID) + } catch (e: Throwable) { + Timber.e(e, "## Error while notifying listeners") + } + } + } + } + + override fun onFailure(failure: Throwable) { + Timber.e(failure, "## Manual verification failed in state") + } + }) + } + + private fun onStartRequestReceived(event: Event) { + val startReq = event.content.toModel()!! + + val otherUserId = event.sender + if (!startReq.isValid()) { + Timber.e("## received invalid verification request") + if (startReq.transactionID != null) { + cancelTransaction( + startReq.transactionID!!, + otherUserId!!, + startReq?.fromDevice ?: event.getSenderKey()!!, + CancelCode.UnknownMethod + ) + } + return + } + //Download device keys prior to everything + checkKeysAreDownloaded( + otherUserId!!, + startReq, + success = { + Timber.d("## SAS onStartRequestReceived ${startReq.transactionID!!}") + val tid = startReq.transactionID!! + val existing = getExistingTransaction(otherUserId, tid) + val existingTxs = getExistingTransactionsForUser(otherUserId) + if (existing != null) { + //should cancel both! + Timber.d("## SAS onStartRequestReceived - Request exist with same if ${startReq.transactionID!!}") + existing.cancel(CancelCode.UnexpectedMessage) + cancelTransaction(tid, otherUserId, startReq.fromDevice!!, CancelCode.UnexpectedMessage) + } else if (existingTxs?.isEmpty() == false) { + Timber.d("## SAS onStartRequestReceived - There is already a transaction with this user ${startReq.transactionID!!}") + //Multiple keyshares between two devices: any two devices may only have at most one key verification in flight at a time. + existingTxs.forEach { + it.cancel(CancelCode.UnexpectedMessage) + } + cancelTransaction(tid, otherUserId, startReq.fromDevice!!, CancelCode.UnexpectedMessage) + } else { + //Ok we can create + if (KeyVerificationStart.VERIF_METHOD_SAS == startReq.method) { + Timber.d("## SAS onStartRequestReceived - request accepted ${startReq.transactionID!!}") + val tx = IncomingSASVerificationTransaction( + this, + mCredentials, + mCryptoStore, + mSendToDeviceTask, + mTaskExecutor, + mCryptoListener.getMyDevice().fingerprint()!!, + startReq.transactionID!!, + otherUserId) + addTransaction(tx) + tx.acceptToDeviceEvent(otherUserId, startReq) + } else { + Timber.e("## SAS onStartRequestReceived - unknown method ${startReq.method}") + cancelTransaction(tid, otherUserId, startReq.fromDevice + ?: event.getSenderKey()!!, CancelCode.UnknownMethod) + } + } + }, + error = { + cancelTransaction(startReq.transactionID!!, otherUserId, startReq.fromDevice!!, CancelCode.UnexpectedMessage) + }) + } + + private fun checkKeysAreDownloaded(otherUserId: String, + startReq: KeyVerificationStart, + success: (MXUsersDevicesMap) -> Unit, + error: () -> Unit) { + deviceListManager.downloadKeys(listOf(otherUserId), true, object : MatrixCallback> { + override fun onFailure(failure: Throwable) { + CryptoAsyncHelper.getDecryptBackgroundHandler().post { + error() + } + } + + override fun onSuccess(info: MXUsersDevicesMap) { + CryptoAsyncHelper.getDecryptBackgroundHandler().post { + if (info.getUserDeviceIds(otherUserId).contains(startReq.fromDevice)) { + success(info) + } else { + error() + } + } + } + }) + } + + private fun onCancelReceived(event: Event) { + Timber.d("## SAS onCancelReceived") + val cancelReq = event.content.toModel()!! + + if (!cancelReq.isValid()) { + //ignore + Timber.e("## Received invalid accept request") + return + } + val otherUserId = event.sender!! + + Timber.d("## SAS onCancelReceived otherUser:$otherUserId reason:${cancelReq.reason}") + val existing = getExistingTransaction(otherUserId, cancelReq.transactionID!!) + if (existing == null) { + Timber.e("## Received invalid cancel request") + return + } + if (existing is SASVerificationTransaction) { + existing.cancelledReason = safeValueOf(cancelReq.code) + existing.state = SasVerificationTxState.OnCancelled + } + } + + private fun onAcceptReceived(event: Event) { + val acceptReq = event.content.toModel()!! + + if (!acceptReq.isValid()) { + //ignore + Timber.e("## Received invalid accept request") + return + } + val otherUserId = event.sender!! + val existing = getExistingTransaction(otherUserId, acceptReq.transactionID!!) + if (existing == null) { + Timber.e("## Received invalid accept request") + return + + } + + if (existing is SASVerificationTransaction) { + existing.acceptToDeviceEvent(otherUserId, acceptReq) + } else { + //not other types now + } + } + + + private fun onKeyReceived(event: Event) { + val keyReq = event.content.toModel()!! + + if (!keyReq.isValid()) { + //ignore + Timber.e("## Received invalid key request") + return + } + val otherUserId = event.sender!! + val existing = getExistingTransaction(otherUserId, keyReq.transactionID!!) + if (existing == null) { + Timber.e("## Received invalid accept request") + return + } + if (existing is SASVerificationTransaction) { + existing.acceptToDeviceEvent(otherUserId, keyReq) + } else { + //not other types now + } + } + + private fun onMacReceived(event: Event) { + val macReq = event.content.toModel()!! + + if (!macReq.isValid()) { + //ignore + Timber.e("## Received invalid key request") + return + } + val otherUserId = event.sender!! + val existing = getExistingTransaction(otherUserId, macReq.transactionID!!) + if (existing == null) { + Timber.e("## Received invalid accept request") + return + } + if (existing is SASVerificationTransaction) { + existing.acceptToDeviceEvent(otherUserId, macReq) + } else { + //not other types known for now + } + } + + override fun getExistingTransaction(otherUser: String, tid: String): VerificationTransaction? { + synchronized(lock = txMap) { + return txMap[otherUser]?.get(tid) + } + } + + private fun getExistingTransactionsForUser(otherUser: String): Collection? { + synchronized(txMap) { + return txMap[otherUser]?.values + } + } + + private fun removeTransaction(otherUser: String, tid: String) { + synchronized(txMap) { + txMap[otherUser]?.remove(tid)?.removeListener(this) + } + } + + private fun addTransaction(tx: VerificationTransaction) { + tx.otherUserId.let { otherUserId -> + synchronized(txMap) { + if (txMap[otherUserId] == null) { + txMap[otherUserId] = HashMap() + } + txMap[otherUserId]?.set(tx.transactionId, tx) + dispatchTxAdded(tx) + tx.addListener(this) + } + } + } + + override fun beginKeyVerificationSAS(userId: String, deviceID: String): String? { + return beginKeyVerification(KeyVerificationStart.VERIF_METHOD_SAS, userId, deviceID) + } + + override fun beginKeyVerification(method: String, userId: String, deviceID: String): String? { + val txID = createUniqueIDForTransaction(userId, deviceID) + //should check if already one (and cancel it) + if (KeyVerificationStart.VERIF_METHOD_SAS == method) { + val tx = OutgoingSASVerificationRequest( + this, + mCredentials, + mCryptoStore, + mSendToDeviceTask, + mTaskExecutor, + mCryptoListener.getMyDevice().fingerprint()!!, + txID, + userId, + deviceID) + addTransaction(tx) + CryptoAsyncHelper.getDecryptBackgroundHandler().post { + tx.start() + } + return txID + } else { + throw IllegalArgumentException("Unknown verification method") + } + } + + /** + * This string must be unique for the pair of users performing verification for the duration that the transaction is valid + */ + private fun createUniqueIDForTransaction(userId: String, deviceID: String): String { + val buff = StringBuffer() + buff + .append(mCredentials.userId).append("|") + .append(mCredentials.deviceId).append("|") + .append(userId).append("|") + .append(deviceID).append("|") + .append(UUID.randomUUID().toString()) + return buff.toString() + } + + + override fun transactionUpdated(tx: VerificationTransaction) { + dispatchTxUpdated(tx) + if (tx is SASVerificationTransaction + && (tx.state == SasVerificationTxState.Cancelled + || tx.state == SasVerificationTxState.OnCancelled + || tx.state == SasVerificationTxState.Verified) + ) { + //remove + this.removeTransaction(tx.otherUserId, tx.transactionId) + } + } + + fun cancelTransaction(transactionId: String, userId: String, userDevice: String, code: CancelCode) { + val cancelMessage = KeyVerificationCancel.create(transactionId, code) + val contentMap = MXUsersDevicesMap() + contentMap.setObject(cancelMessage, userId, userDevice) + + mSendToDeviceTask.configureWith(SendToDeviceTask.Params(EventType.KEY_VERIFICATION_CANCEL, contentMap, transactionId)) + .dispatchTo(object : MatrixCallback { + override fun onSuccess(data: Unit) { + Timber.d("## SAS verification [$transactionId] canceled for reason ${code.value}") + } + + override fun onFailure(failure: Throwable) { + Timber.e(failure, "## SAS verification [$transactionId] failed to cancel.") + } + }) + .executeBy(mTaskExecutor) + } + + fun setCryptoInternalListener(listener: SasCryptoListener) { + mCryptoListener = listener + } + + fun setDeviceVerification(verificationStatus: Int, deviceId: String, userId: String, callback: MatrixCallback) { + mCryptoListener.setDeviceVerification(verificationStatus, deviceId, userId, callback) + } + + interface SasCryptoListener { + fun setDeviceVerification(verificationStatus: Int, deviceId: String, userId: String, callback: MatrixCallback) + fun getMyDevice(): MXDeviceInfo + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/IncomingSASVerificationTransaction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/IncomingSASVerificationTransaction.kt new file mode 100644 index 00000000..e9bc6788 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/IncomingSASVerificationTransaction.kt @@ -0,0 +1,242 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.internal.crypto.verification + +import android.util.Base64 +import im.vector.matrix.android.api.auth.data.Credentials +import im.vector.matrix.android.api.session.crypto.sas.CancelCode +import im.vector.matrix.android.api.session.crypto.sas.IncomingSasVerificationTransaction +import im.vector.matrix.android.api.session.crypto.sas.SasMode +import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState +import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.internal.crypto.CryptoAsyncHelper +import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore +import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationAccept +import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationKey +import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationMac +import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationStart +import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask +import im.vector.matrix.android.internal.di.MoshiProvider +import im.vector.matrix.android.internal.task.TaskExecutor +import timber.log.Timber + +internal class IncomingSASVerificationTransaction( + private val mSasVerificationService: DefaultSasVerificationService, + private val mCredentials: Credentials, + private val mCryptoStore: IMXCryptoStore, + private val mSendToDeviceTask: SendToDeviceTask, + private val mTaskExecutor: TaskExecutor, transactionId: String, + deviceFingerprint: String, + otherUserID: String) + : SASVerificationTransaction( + mSasVerificationService, + mCredentials, + mCryptoStore, + mSendToDeviceTask, + mTaskExecutor, + deviceFingerprint, + transactionId, + otherUserID, + null, + true), + IncomingSasVerificationTransaction { + + override val uxState: IncomingSasVerificationTransaction.UxState + get() { + return when (state) { + SasVerificationTxState.OnStarted -> IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT + SasVerificationTxState.SendingAccept, + SasVerificationTxState.Accepted, + SasVerificationTxState.OnKeyReceived, + SasVerificationTxState.SendingKey, + SasVerificationTxState.KeySent -> IncomingSasVerificationTransaction.UxState.WAIT_FOR_KEY_AGREEMENT + SasVerificationTxState.ShortCodeReady -> IncomingSasVerificationTransaction.UxState.SHOW_SAS + SasVerificationTxState.ShortCodeAccepted, + SasVerificationTxState.SendingMac, + SasVerificationTxState.MacSent, + SasVerificationTxState.Verifying -> IncomingSasVerificationTransaction.UxState.WAIT_FOR_VERIFICATION + SasVerificationTxState.Verified -> IncomingSasVerificationTransaction.UxState.VERIFIED + SasVerificationTxState.Cancelled -> IncomingSasVerificationTransaction.UxState.CANCELLED_BY_ME + SasVerificationTxState.OnCancelled -> IncomingSasVerificationTransaction.UxState.CANCELLED_BY_OTHER + else -> IncomingSasVerificationTransaction.UxState.UNKNOWN + } + } + + override fun onVerificationStart(startReq: KeyVerificationStart) { + Timber.d("## SAS received verification request from state $state") + if (state != SasVerificationTxState.None) { + Timber.e("## received verification request from invalid state") + //should I cancel?? + throw IllegalStateException("Interactive Key verification already started") + } + this.startReq = startReq + state = SasVerificationTxState.OnStarted + this.otherDeviceId = startReq.fromDevice + + } + + + override fun performAccept() { + if (state != SasVerificationTxState.OnStarted) { + Timber.e("## Cannot perform accept from state $state") + return + } + + // Select a key agreement protocol, a hash algorithm, a message authentication code, + // and short authentication string methods out of the lists given in requester's message. + val agreedProtocol = startReq!!.keyAgreementProtocols?.firstOrNull { KNOWN_AGREEMENT_PROTOCOLS.contains(it) } + val agreedHash = startReq!!.hashes?.firstOrNull { KNOWN_HASHES.contains(it) } + val agreedMac = startReq!!.messageAuthenticationCodes?.firstOrNull { KNOWN_MACS.contains(it) } + val agreedShortCode = startReq!!.shortAuthenticationStrings?.filter { KNOWN_SHORT_CODES.contains(it) } + + //No common key sharing/hashing/hmac/SAS methods. + //If a device is unable to complete the verification because the devices are unable to find a common key sharing, + // hashing, hmac, or SAS method, then it should send a m.key.verification.cancel message + if (listOf(agreedProtocol, agreedHash, agreedMac).any { it.isNullOrBlank() } + || agreedShortCode.isNullOrEmpty()) { + //Failed to find agreement + Timber.e("## Failed to find agreement ") + cancel(CancelCode.UnknownMethod) + return + } + + //Bob’s device ensures that it has a copy of Alice’s device key. + val mxDeviceInfo = mCryptoStore.getUserDevice(this.otherUserId, otherDeviceId!!) + + if (mxDeviceInfo?.fingerprint() == null) { + Timber.e("## Failed to find device key ") + //TODO force download keys!! + //would be probably better to download the keys + //for now I cancel + CryptoAsyncHelper.getDecryptBackgroundHandler().post { + cancel(CancelCode.User) + } + } else { + // val otherKey = info.identityKey() + //need to jump back to correct thread + val accept = KeyVerificationAccept.create( + tid = transactionId, + keyAgreementProtocol = agreedProtocol!!, + hash = agreedHash!!, + messageAuthenticationCode = agreedMac!!, + shortAuthenticationStrings = agreedShortCode, + commitment = Base64.encodeToString("temporary commitment".toByteArray(), Base64.DEFAULT) + ) + CryptoAsyncHelper.getDecryptBackgroundHandler().post { + doAccept(accept) + } + } + } + + + private fun doAccept(accept: KeyVerificationAccept) { + this.accepted = accept + Timber.d("## SAS accept request id:$transactionId") + + //The hash commitment is the hash (using the selected hash algorithm) of the unpadded base64 representation of QB, + // concatenated with the canonical JSON representation of the content of the m.key.verification.start message + val concat = getSAS().publicKey + MoshiProvider.getCanonicalJson(KeyVerificationStart::class.java, startReq!!) + accept.commitment = hashUsingAgreedHashMethod(concat) ?: "" + //we need to send this to other device now + state = SasVerificationTxState.SendingAccept + sendToOther(EventType.KEY_VERIFICATION_ACCEPT, accept, SasVerificationTxState.Accepted, CancelCode.User) { + if (state == SasVerificationTxState.SendingAccept) { + //It is possible that we receive the next event before this one :/, in this case we should keep state + state = SasVerificationTxState.Accepted + } + } + } + + + override fun onVerificationAccept(accept: KeyVerificationAccept) { + Timber.d("## SAS invalid message for incoming request id:$transactionId") + cancel(CancelCode.UnexpectedMessage) + } + + override fun onKeyVerificationKey(userId: String, vKey: KeyVerificationKey) { + Timber.d("## SAS received key for request id:$transactionId") + if (state != SasVerificationTxState.SendingAccept && state != SasVerificationTxState.Accepted) { + Timber.e("## received key from invalid state $state") + cancel(CancelCode.UnexpectedMessage) + return + } + + otherKey = vKey.key + // Upon receipt of the m.key.verification.key message from Alice’s device, + // Bob’s device replies with a to_device message with type set to m.key.verification.key, + // sending Bob’s public key QB + val pubKey = getSAS().publicKey + + val keyToDevice = KeyVerificationKey.create(transactionId, pubKey) + //we need to send this to other device now + state = SasVerificationTxState.SendingKey + this.sendToOther(EventType.KEY_VERIFICATION_KEY, keyToDevice, SasVerificationTxState.KeySent, CancelCode.User) { + if (state == SasVerificationTxState.SendingKey) { + //It is possible that we receive the next event before this one :/, in this case we should keep state + state = SasVerificationTxState.KeySent + } + } + + // Alice’s and Bob’s devices perform an Elliptic-curve Diffie-Hellman + // (calculate the point (x,y)=dAQB=dBQA and use x as the result of the ECDH), + // using the result as the shared secret. + + getSAS().setTheirPublicKey(otherKey) + //(Note: In all of the following HKDF is as defined in RFC 5869, and uses the previously agreed-on hash function as the hash function, + // the shared secret as the input keying material, no salt, and with the input parameter set to the concatenation of: + // - the string “MATRIX_KEY_VERIFICATION_SAS”, + // - the Matrix ID of the user who sent the m.key.verification.start message, + // - the device ID of the device that sent the m.key.verification.start message, + // - the Matrix ID of the user who sent the m.key.verification.accept message, + // - he device ID of the device that sent the m.key.verification.accept message + // - the transaction ID. + val sasInfo = "MATRIX_KEY_VERIFICATION_SAS" + + "$otherUserId$otherDeviceId" + + "${mCredentials.userId}${mCredentials.deviceId}" + + transactionId + //decimal: generate five bytes by using HKDF. + //emoji: generate six bytes by using HKDF. + shortCodeBytes = getSAS().generateShortCode(sasInfo, 6) + + Timber.e("************ BOB CODE ${getDecimalCodeRepresentation(shortCodeBytes!!)}") + Timber.e("************ BOB EMOJI CODE ${getShortCodeRepresentation(SasMode.EMOJI)}") + + state = SasVerificationTxState.ShortCodeReady + } + + override fun onKeyVerificationMac(vKey: KeyVerificationMac) { + Timber.d("## SAS received mac for request id:$transactionId") + //Check for state? + if (state != SasVerificationTxState.SendingKey + && state != SasVerificationTxState.KeySent + && state != SasVerificationTxState.ShortCodeReady + && state != SasVerificationTxState.ShortCodeAccepted + && state != SasVerificationTxState.SendingMac + && state != SasVerificationTxState.MacSent) { + Timber.e("## received key from invalid state $state") + cancel(CancelCode.UnexpectedMessage) + return + } + theirMac = vKey + + //Do I have my Mac? + if (myMac != null) { + //I can check + verifyMacs() + } + //Wait for ShortCode Accepted + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/OutgoingSASVerificationRequest.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/OutgoingSASVerificationRequest.kt new file mode 100644 index 00000000..e2cc5d3a --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/OutgoingSASVerificationRequest.kt @@ -0,0 +1,216 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.internal.crypto.verification + +import im.vector.matrix.android.api.auth.data.Credentials +import im.vector.matrix.android.api.session.crypto.sas.CancelCode +import im.vector.matrix.android.api.session.crypto.sas.OutgoingSasVerificationRequest +import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState +import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore +import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap +import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationAccept +import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationKey +import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationMac +import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationStart +import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask +import im.vector.matrix.android.internal.di.MoshiProvider +import im.vector.matrix.android.internal.task.TaskExecutor +import timber.log.Timber + +internal class OutgoingSASVerificationRequest( + private val mSasVerificationService: DefaultSasVerificationService, + private val mCredentials: Credentials, + private val mCryptoStore: IMXCryptoStore, + private val mSendToDeviceTask: SendToDeviceTask, + private val mTaskExecutor: TaskExecutor, + deviceFingerprint: String, + transactionId: String, + otherUserId: String, + otherDeviceId: String) + : SASVerificationTransaction( + mSasVerificationService, + mCredentials, + mCryptoStore, + mSendToDeviceTask, + mTaskExecutor, + deviceFingerprint, + transactionId, + otherUserId, + otherDeviceId, + isIncoming = false), + OutgoingSasVerificationRequest { + + + override val uxState: OutgoingSasVerificationRequest.UxState + get() { + return when (state) { + SasVerificationTxState.None -> OutgoingSasVerificationRequest.UxState.WAIT_FOR_START + SasVerificationTxState.SendingStart, + SasVerificationTxState.Started, + SasVerificationTxState.OnAccepted, + SasVerificationTxState.SendingKey, + SasVerificationTxState.KeySent, + SasVerificationTxState.OnKeyReceived -> OutgoingSasVerificationRequest.UxState.WAIT_FOR_KEY_AGREEMENT + SasVerificationTxState.ShortCodeReady -> OutgoingSasVerificationRequest.UxState.SHOW_SAS + SasVerificationTxState.ShortCodeAccepted, + SasVerificationTxState.SendingMac, + SasVerificationTxState.MacSent, + SasVerificationTxState.Verifying -> OutgoingSasVerificationRequest.UxState.WAIT_FOR_VERIFICATION + SasVerificationTxState.Verified -> OutgoingSasVerificationRequest.UxState.VERIFIED + SasVerificationTxState.OnCancelled -> OutgoingSasVerificationRequest.UxState.CANCELLED_BY_ME + SasVerificationTxState.Cancelled -> OutgoingSasVerificationRequest.UxState.CANCELLED_BY_OTHER + else -> OutgoingSasVerificationRequest.UxState.UNKNOWN + } + } + + override fun onVerificationStart(startReq: KeyVerificationStart) { + Timber.e("## onVerificationStart - unexpected id:$transactionId") + cancel(CancelCode.UnexpectedMessage) + } + + fun start() { + + if (state != SasVerificationTxState.None) { + Timber.e("## start verification from invalid state") + //should I cancel?? + throw IllegalStateException("Interactive Key verification already started") + } + + val startMessage = KeyVerificationStart() + startMessage.fromDevice = mCredentials.deviceId + startMessage.method = KeyVerificationStart.VERIF_METHOD_SAS + startMessage.transactionID = transactionId + startMessage.keyAgreementProtocols = KNOWN_AGREEMENT_PROTOCOLS + startMessage.hashes = KNOWN_HASHES + startMessage.messageAuthenticationCodes = KNOWN_MACS + startMessage.shortAuthenticationStrings = KNOWN_SHORT_CODES + + startReq = startMessage + val contentMap = MXUsersDevicesMap() + contentMap.setObject(startMessage, otherUserId, otherDeviceId) + state = SasVerificationTxState.SendingStart + + sendToOther( + EventType.KEY_VERIFICATION_START, + startMessage, + SasVerificationTxState.Started, + CancelCode.User, + null + ) + } + + override fun onVerificationAccept(accept: KeyVerificationAccept) { + Timber.d("## onVerificationAccept id:$transactionId") + if (state != SasVerificationTxState.Started) { + Timber.e("## received accept request from invalid state $state") + cancel(CancelCode.UnexpectedMessage) + return + } + //Check that the agreement is correct + if (!KNOWN_AGREEMENT_PROTOCOLS.contains(accept.keyAgreementProtocol) + || !KNOWN_HASHES.contains(accept.hash) + || !KNOWN_MACS.contains(accept.messageAuthenticationCode) + || accept.shortAuthenticationStrings!!.intersect(KNOWN_SHORT_CODES).isEmpty()) { + Timber.e("## received accept request from invalid state") + cancel(CancelCode.UnknownMethod) + return + } + + //Upon receipt of the m.key.verification.accept message from Bob’s device, + // Alice’s device stores the commitment value for later use. + accepted = accept + state = SasVerificationTxState.OnAccepted + + // Alice’s device creates an ephemeral Curve25519 key pair (dA,QA), + // and replies with a to_device message with type set to “m.key.verification.key”, sending Alice’s public key QA + val pubKey = getSAS().publicKey + + val keyToDevice = KeyVerificationKey.create(transactionId, pubKey) + //we need to send this to other device now + state = SasVerificationTxState.SendingKey + sendToOther(EventType.KEY_VERIFICATION_KEY, keyToDevice, SasVerificationTxState.KeySent, CancelCode.User) { + //It is possible that we receive the next event before this one :/, in this case we should keep state + if (state == SasVerificationTxState.SendingKey) { + state = SasVerificationTxState.KeySent + } + } + } + + override fun onKeyVerificationKey(userId: String, vKey: KeyVerificationKey) { + Timber.d("## onKeyVerificationKey id:$transactionId") + if (state != SasVerificationTxState.SendingKey && state != SasVerificationTxState.KeySent) { + Timber.e("## received key from invalid state $state") + cancel(CancelCode.UnexpectedMessage) + return + } + + otherKey = vKey.key + // Upon receipt of the m.key.verification.key message from Bob’s device, + // Alice’s device checks that the commitment property from the Bob’s m.key.verification.accept + // message is the same as the expected value based on the value of the key property received + // in Bob’s m.key.verification.key and the content of Alice’s m.key.verification.start message. + + //check commitment + val concat = vKey.key + MoshiProvider.getCanonicalJson(KeyVerificationStart::class.java, startReq!!) + val otherCommitment = hashUsingAgreedHashMethod(concat) ?: "" + + if (accepted!!.commitment.equals(otherCommitment)) { + getSAS().setTheirPublicKey(otherKey) + //(Note: In all of the following HKDF is as defined in RFC 5869, and uses the previously agreed-on hash function as the hash function, + // the shared secret as the input keying material, no salt, and with the input parameter set to the concatenation of: + // - the string “MATRIX_KEY_VERIFICATION_SAS”, + // - the Matrix ID of the user who sent the m.key.verification.start message, + // - the device ID of the device that sent the m.key.verification.start message, + // - the Matrix ID of the user who sent the m.key.verification.accept message, + // - he device ID of the device that sent the m.key.verification.accept message + // - the transaction ID. + val sasInfo = "MATRIX_KEY_VERIFICATION_SAS" + + "${mCredentials.userId}${mCredentials.deviceId}" + + "$otherUserId$otherDeviceId" + + transactionId + //decimal: generate five bytes by using HKDF. + //emoji: generate six bytes by using HKDF. + shortCodeBytes = getSAS().generateShortCode(sasInfo, 6) + state = SasVerificationTxState.ShortCodeReady + } else { + //bad commitement + cancel(CancelCode.MismatchedCommitment) + } + } + + override fun onKeyVerificationMac(vKey: KeyVerificationMac) { + Timber.d("## onKeyVerificationMac id:$transactionId") + if (state != SasVerificationTxState.OnKeyReceived + && state != SasVerificationTxState.ShortCodeReady + && state != SasVerificationTxState.ShortCodeAccepted + && state != SasVerificationTxState.SendingMac + && state != SasVerificationTxState.MacSent) { + Timber.e("## received key from invalid state $state") + cancel(CancelCode.UnexpectedMessage) + return + } + + theirMac = vKey + + //Do I have my Mac? + if (myMac != null) { + //I can check + verifyMacs() + } + //Wait for ShortCode Accepted + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SASVerificationTransaction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SASVerificationTransaction.kt new file mode 100644 index 00000000..583029a1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/SASVerificationTransaction.kt @@ -0,0 +1,420 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.internal.crypto.verification + +import android.os.Build +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.auth.data.Credentials +import im.vector.matrix.android.api.session.crypto.sas.CancelCode +import im.vector.matrix.android.api.session.crypto.sas.EmojiRepresentation +import im.vector.matrix.android.api.session.crypto.sas.SasMode +import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState +import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.internal.crypto.CryptoAsyncHelper +import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore +import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo +import im.vector.matrix.android.internal.crypto.model.MXKey +import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap +import im.vector.matrix.android.internal.crypto.model.rest.* +import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask +import im.vector.matrix.android.internal.task.TaskExecutor +import im.vector.matrix.android.internal.task.configureWith +import org.matrix.olm.OlmSAS +import org.matrix.olm.OlmUtility +import timber.log.Timber +import kotlin.properties.Delegates + +/** + * Represents an ongoing short code interactive key verification between two devices. + */ +internal abstract class SASVerificationTransaction( + private val mSasVerificationService: DefaultSasVerificationService, + private val mCredentials: Credentials, + private val mCryptoStore: IMXCryptoStore, + private val mSendToDeviceTask: SendToDeviceTask, + private val mTaskExecutor: TaskExecutor, + private val deviceFingerprint: String, + transactionId: String, + otherUserId: String, + otherDevice: String?, + isIncoming: Boolean) : + VerificationTransaction(transactionId, otherUserId, otherDevice, isIncoming) { + + companion object { + const val SAS_MAC_SHA256_LONGKDF = "hmac-sha256" + const val SAS_MAC_SHA256 = "hkdf-hmac-sha256" + + //ordered by preferred order + val KNOWN_AGREEMENT_PROTOCOLS = listOf(MXKey.KEY_CURVE_25519_TYPE) + //ordered by preferred order + val KNOWN_HASHES = listOf("sha256") + //ordered by preferred order + val KNOWN_MACS = listOf(SAS_MAC_SHA256, SAS_MAC_SHA256_LONGKDF) + + //older devices have limited support of emoji, so reply with decimal + val KNOWN_SHORT_CODES = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + listOf(SasMode.EMOJI, SasMode.DECIMAL) + else + listOf(SasMode.DECIMAL) + + } + + override var state by Delegates.observable(SasVerificationTxState.None) { _, _, new -> + // println("$property has changed from $old to $new") + listeners.forEach { + try { + it.transactionUpdated(this) + } catch (e: Throwable) { + Timber.e(e, "## Error while notifying listeners") + } + } + if (new == SasVerificationTxState.Cancelled + || new == SasVerificationTxState.OnCancelled + || new == SasVerificationTxState.Verified) { + releaseSAS() + } + } + + override var cancelledReason: CancelCode? = null + + private var olmSas: OlmSAS? = null + + var startReq: KeyVerificationStart? = null + var accepted: KeyVerificationAccept? = null + var otherKey: String? = null + var shortCodeBytes: ByteArray? = null + + var myMac: KeyVerificationMac? = null + var theirMac: KeyVerificationMac? = null + + fun getSAS(): OlmSAS { + if (olmSas == null) olmSas = OlmSAS() + return olmSas!! + } + + //To override finalize(), all you need to do is simply declare it, without using the override keyword: + protected fun finalize() { + releaseSAS() + } + + private fun releaseSAS() { + // finalization logic + olmSas?.releaseSas() + olmSas = null + } + + + /** + * To be called by the client when the user has verified that + * both short codes do match + */ + override fun userHasVerifiedShortCode() { + Timber.d("## SAS short code verified by user for id:$transactionId") + if (state != SasVerificationTxState.ShortCodeReady) { + //ignore and cancel? + Timber.e("## Accepted short code from invalid state $state") + cancel(CancelCode.UnexpectedMessage) + return + } + + state = SasVerificationTxState.ShortCodeAccepted + //Alice and Bob’ devices calculate the HMAC of their own device keys and a comma-separated, + // sorted list of the key IDs that they wish the other user to verify, + //the shared secret as the input keying material, no salt, and with the input parameter set to the concatenation of: + // - the string “MATRIX_KEY_VERIFICATION_MAC”, + // - the Matrix ID of the user whose key is being MAC-ed, + // - the device ID of the device sending the MAC, + // - the Matrix ID of the other user, + // - the device ID of the device receiving the MAC, + // - the transaction ID, and + // - the key ID of the key being MAC-ed, or the string “KEY_IDS” if the item being MAC-ed is the list of key IDs. + + val baseInfo = "MATRIX_KEY_VERIFICATION_MAC" + + mCredentials.userId + mCredentials.deviceId + + otherUserId + otherDeviceId + + transactionId + + val keyId = "ed25519:${mCredentials.deviceId}" + val macString = macUsingAgreedMethod(deviceFingerprint, baseInfo + keyId) + val keyStrings = macUsingAgreedMethod(keyId, baseInfo + "KEY_IDS") + + if (macString.isNullOrBlank() || keyStrings.isNullOrBlank()) { + //Should not happen + Timber.e("## SAS verification [$transactionId] failed to send KeyMac, empty key hashes.") + cancel(CancelCode.UnexpectedMessage) + return + } + + val macMsg = KeyVerificationMac.create(transactionId, mapOf(keyId to macString), keyStrings) + myMac = macMsg + state = SasVerificationTxState.SendingMac + sendToOther(EventType.KEY_VERIFICATION_MAC, macMsg, SasVerificationTxState.MacSent, CancelCode.User) { + if (state == SasVerificationTxState.SendingMac) { + //It is possible that we receive the next event before this one :/, in this case we should keep state + state = SasVerificationTxState.MacSent + } + } + + //Do I already have their Mac? + if (theirMac != null) { + verifyMacs() + } //if not wait for it + + } + + override fun acceptToDeviceEvent(senderId: String, event: SendToDeviceObject) { + when (event) { + is KeyVerificationStart -> onVerificationStart(event) + is KeyVerificationAccept -> onVerificationAccept(event) + is KeyVerificationKey -> onKeyVerificationKey(senderId, event) + is KeyVerificationMac -> onKeyVerificationMac(event) + else -> { + //nop + } + } + } + + abstract fun onVerificationStart(startReq: KeyVerificationStart) + + abstract fun onVerificationAccept(accept: KeyVerificationAccept) + + abstract fun onKeyVerificationKey(userId: String, vKey: KeyVerificationKey) + + abstract fun onKeyVerificationMac(vKey: KeyVerificationMac) + + protected fun verifyMacs() { + Timber.d("## SAS verifying macs for id:$transactionId") + state = SasVerificationTxState.Verifying + + //Keys have been downloaded earlier in process + val otherUserKnownDevices = mCryptoStore.getUserDevices(otherUserId) + + // Bob’s device calculates the HMAC (as above) of its copies of Alice’s keys given in the message (as identified by their key ID), + // as well as the HMAC of the comma-separated, sorted list of the key IDs given in the message. + // Bob’s device compares these with the HMAC values given in the m.key.verification.mac message. + // If everything matches, then consider Alice’s device keys as verified. + + val baseInfo = "MATRIX_KEY_VERIFICATION_MAC" + + otherUserId + otherDeviceId + + mCredentials.userId + mCredentials.deviceId + + transactionId + + val commaSeparatedListOfKeyIds = theirMac!!.mac!!.keys.sorted().joinToString(",") + + val keyStrings = macUsingAgreedMethod(commaSeparatedListOfKeyIds, baseInfo + "KEY_IDS") + if (theirMac!!.keys != keyStrings) { + //WRONG! + cancel(CancelCode.MismatchedKeys) + return + } + //cannot be empty because it has been validated + theirMac!!.mac!!.keys.forEach { + val keyIDNoPrefix = if (it.startsWith("ed25519:")) it.substring("ed25519:".length) else it + val otherDeviceKey = otherUserKnownDevices?.get(keyIDNoPrefix)?.fingerprint() + if (otherDeviceKey == null) { + cancel(CancelCode.MismatchedKeys) + return + } + val mac = macUsingAgreedMethod(otherDeviceKey, baseInfo + it) + if (mac != theirMac?.mac?.get(it)) { + //WRONG! + cancel(CancelCode.MismatchedKeys) + return + } + } + + setDeviceVerified( + otherDeviceId ?: "", + otherUserId, + success = { + state = SasVerificationTxState.Verified + }, + error = { + //mmm what to do?, looks like this is never called + } + ) + } + + private fun setDeviceVerified(deviceId: String, userId: String, success: () -> Unit, error: () -> Unit) { + mSasVerificationService.setDeviceVerification(MXDeviceInfo.DEVICE_VERIFICATION_VERIFIED, + deviceId, + userId, + object : MatrixCallback { + + override fun onSuccess(data: Unit) { + //We good + Timber.d("## SAS verification complete and device status updated for id:$transactionId") + success() + } + + override fun onFailure(failure: Throwable) { + Timber.e(failure, "## SAS verification [$transactionId] failed in state : $state") + error() + } + }) + } + + override fun cancel() { + cancel(CancelCode.User) + } + + override fun cancel(code: CancelCode) { + cancelledReason = code + state = SasVerificationTxState.Cancelled + mSasVerificationService.cancelTransaction( + transactionId, + otherUserId, + otherDeviceId ?: "", + code) + } + + protected fun sendToOther(type: String, + keyToDevice: Any, + nextState: SasVerificationTxState, + onErrorReason: CancelCode, + onDone: (() -> Unit)?) { + val contentMap = MXUsersDevicesMap() + contentMap.setObject(keyToDevice, otherUserId, otherDeviceId) + + mSendToDeviceTask.configureWith(SendToDeviceTask.Params(type, contentMap, transactionId)) + .dispatchTo(object : MatrixCallback { + override fun onSuccess(data: Unit) { + Timber.d("## SAS verification [$transactionId] toDevice type '$type' success.") + CryptoAsyncHelper.getDecryptBackgroundHandler().post { + if (onDone != null) { + onDone() + } else { + state = nextState + } + } + } + + override fun onFailure(failure: Throwable) { + Timber.e("## SAS verification [$transactionId] failed to send toDevice in state : $state") + + CryptoAsyncHelper.getDecryptBackgroundHandler().post { + cancel(onErrorReason) + } + } + }) + .executeBy(mTaskExecutor) + } + + fun getShortCodeRepresentation(shortAuthenticationStringMode: String): String? { + if (shortCodeBytes == null) { + return null + } + when (shortAuthenticationStringMode) { + SasMode.DECIMAL -> { + if (shortCodeBytes!!.size < 5) return null + return getDecimalCodeRepresentation(shortCodeBytes!!) + } + SasMode.EMOJI -> { + if (shortCodeBytes!!.size < 6) return null + return getEmojiCodeRepresentation(shortCodeBytes!!).joinToString(" ") { it.emoji } + } + else -> return null + } + } + + override fun supportsEmoji(): Boolean { + return accepted?.shortAuthenticationStrings?.contains(SasMode.EMOJI) == true + } + + override fun supportsDecimal(): Boolean { + return accepted?.shortAuthenticationStrings?.contains(SasMode.DECIMAL) == true + } + + protected fun hashUsingAgreedHashMethod(toHash: String): String? { + if ("sha256".toLowerCase() == accepted?.hash?.toLowerCase()) { + val olmUtil = OlmUtility() + val hashBytes = olmUtil.sha256(toHash) + olmUtil.releaseUtility() + return hashBytes + } + return null + } + + protected fun macUsingAgreedMethod(message: String, info: String): String? { + if (SAS_MAC_SHA256_LONGKDF.toLowerCase() == accepted?.messageAuthenticationCode?.toLowerCase()) { + return getSAS().calculateMacLongKdf(message, info) + } else if (SAS_MAC_SHA256.toLowerCase() == accepted?.messageAuthenticationCode?.toLowerCase()) { + return getSAS().calculateMac(message, info) + } + return null + } + + override fun getDecimalCodeRepresentation(): String { + return getDecimalCodeRepresentation(shortCodeBytes!!) + } + + /** + * decimal: generate five bytes by using HKDF. + * Take the first 13 bits and convert it to a decimal number (which will be a number between 0 and 8191 inclusive), + * and add 1000 (resulting in a number between 1000 and 9191 inclusive). + * Do the same with the second 13 bits, and the third 13 bits, giving three 4-digit numbers. + * In other words, if the five bytes are B0, B1, B2, B3, and B4, then the first number is (B0 << 5 | B1 >> 3) + 1000, + * the second number is ((B1 & 0x7) << 10 | B2 << 2 | B3 >> 6) + 1000, and the third number is ((B3 & 0x3f) << 7 | B4 >> 1) + 1000. + * (This method of converting 13 bits at a time is used to avoid requiring 32-bit clients to do big-number arithmetic, + * and adding 1000 to the number avoids having clients to worry about properly zero-padding the number when displaying to the user.) + * The three 4-digit numbers are displayed to the user either with dashes (or another appropriate separator) separating the three numbers, + * or with the three numbers on separate lines. + */ + fun getDecimalCodeRepresentation(byteArray: ByteArray): String { + val b0 = byteArray[0].toInt().and(0xff) //need unsigned byte + val b1 = byteArray[1].toInt().and(0xff) //need unsigned byte + val b2 = byteArray[2].toInt().and(0xff) //need unsigned byte + val b3 = byteArray[3].toInt().and(0xff) //need unsigned byte + val b4 = byteArray[4].toInt().and(0xff) //need unsigned byte + //(B0 << 5 | B1 >> 3) + 1000 + val first = (b0.shl(5) or b1.shr(3)) + 1000 + //((B1 & 0x7) << 10 | B2 << 2 | B3 >> 6) + 1000 + val second = ((b1 and 0x7).shl(10) or b2.shl(2) or b3.shr(6)) + 1000 + //((B3 & 0x3f) << 7 | B4 >> 1) + 1000 + val third = ((b3 and 0x3f).shl(7) or b4.shr(1)) + 1000 + return "$first $second $third" + } + + override fun getEmojiCodeRepresentation(): List { + return getEmojiCodeRepresentation(shortCodeBytes!!) + } + + /** + * emoji: generate six bytes by using HKDF. + * Split the first 42 bits into 7 groups of 6 bits, as one would do when creating a base64 encoding. + * For each group of 6 bits, look up the emoji from Appendix A corresponding + * to that number 7 emoji are selected from a list of 64 emoji (see Appendix A) + */ + fun getEmojiCodeRepresentation(byteArray: ByteArray): List { + val b0 = byteArray[0].toInt().and(0xff) + val b1 = byteArray[1].toInt().and(0xff) + val b2 = byteArray[2].toInt().and(0xff) + val b3 = byteArray[3].toInt().and(0xff) + val b4 = byteArray[4].toInt().and(0xff) + val b5 = byteArray[5].toInt().and(0xff) + return listOf( + getEmojiForCode((b0 and 0xFC).shr(2)), + getEmojiForCode((b0 and 0x3).shl(4) or (b1 and 0xF0).shr(4)), + getEmojiForCode((b1 and 0xF).shl(2) or (b2 and 0xC0).shr(6)), + getEmojiForCode((b2 and 0x3F)), + getEmojiForCode((b3 and 0xFC).shr(2)), + getEmojiForCode((b3 and 0x3).shl(4) or (b4 and 0xF0).shr(4)), + getEmojiForCode((b4 and 0xF).shl(2) or (b5 and 0xC0).shr(6)) + ) + } + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationEmoji.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationEmoji.kt new file mode 100644 index 00000000..0e814083 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationEmoji.kt @@ -0,0 +1,88 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.internal.crypto.verification + +import im.vector.matrix.android.R +import im.vector.matrix.android.api.session.crypto.sas.EmojiRepresentation + +internal fun getEmojiForCode(code: Int): EmojiRepresentation { + return when (code % 64) { + 0 -> EmojiRepresentation("🐶", R.string.verification_emoji_dog) + 1 -> EmojiRepresentation("🐱", R.string.verification_emoji_cat) + 2 -> EmojiRepresentation("🦁", R.string.verification_emoji_lion) + 3 -> EmojiRepresentation("🐎", R.string.verification_emoji_horse) + 4 -> EmojiRepresentation("🦄", R.string.verification_emoji_unicorn) + 5 -> EmojiRepresentation("🐷", R.string.verification_emoji_pig) + 6 -> EmojiRepresentation("🐘", R.string.verification_emoji_elephant) + 7 -> EmojiRepresentation("🐰", R.string.verification_emoji_rabbit) + 8 -> EmojiRepresentation("🐼", R.string.verification_emoji_panda) + 9 -> EmojiRepresentation("🐓", R.string.verification_emoji_rooster) + 10 -> EmojiRepresentation("🐧", R.string.verification_emoji_penguin) + 11 -> EmojiRepresentation("🐢", R.string.verification_emoji_turtle) + 12 -> EmojiRepresentation("🐟", R.string.verification_emoji_fish) + 13 -> EmojiRepresentation("🐙", R.string.verification_emoji_octopus) + 14 -> EmojiRepresentation("🦋", R.string.verification_emoji_butterfly) + 15 -> EmojiRepresentation("🌷", R.string.verification_emoji_flower) + 16 -> EmojiRepresentation("🌳", R.string.verification_emoji_tree) + 17 -> EmojiRepresentation("🌵", R.string.verification_emoji_cactus) + 18 -> EmojiRepresentation("🍄", R.string.verification_emoji_mushroom) + 19 -> EmojiRepresentation("🌏", R.string.verification_emoji_globe) + 20 -> EmojiRepresentation("🌙", R.string.verification_emoji_moon) + 21 -> EmojiRepresentation("☁️", R.string.verification_emoji_cloud) + 22 -> EmojiRepresentation("🔥", R.string.verification_emoji_fire) + 23 -> EmojiRepresentation("🍌", R.string.verification_emoji_banana) + 24 -> EmojiRepresentation("🍎", R.string.verification_emoji_apple) + 25 -> EmojiRepresentation("🍓", R.string.verification_emoji_strawberry) + 26 -> EmojiRepresentation("🌽", R.string.verification_emoji_corn) + 27 -> EmojiRepresentation("🍕", R.string.verification_emoji_pizza) + 28 -> EmojiRepresentation("🎂", R.string.verification_emoji_cake) + 29 -> EmojiRepresentation("❤️", R.string.verification_emoji_heart) + 30 -> EmojiRepresentation("😀", R.string.verification_emoji_smiley) + 31 -> EmojiRepresentation("🤖", R.string.verification_emoji_robot) + 32 -> EmojiRepresentation("🎩", R.string.verification_emoji_hat) + 33 -> EmojiRepresentation("👓", R.string.verification_emoji_glasses) + 34 -> EmojiRepresentation("🔧", R.string.verification_emoji_wrench) + 35 -> EmojiRepresentation("🎅", R.string.verification_emoji_santa) + 36 -> EmojiRepresentation("👍", R.string.verification_emoji_thumbsup) + 37 -> EmojiRepresentation("☂️", R.string.verification_emoji_umbrella) + 38 -> EmojiRepresentation("⌛", R.string.verification_emoji_hourglass) + 39 -> EmojiRepresentation("⏰", R.string.verification_emoji_clock) + 40 -> EmojiRepresentation("🎁", R.string.verification_emoji_gift) + 41 -> EmojiRepresentation("💡", R.string.verification_emoji_lightbulb) + 42 -> EmojiRepresentation("📕", R.string.verification_emoji_book) + 43 -> EmojiRepresentation("✏️", R.string.verification_emoji_pencil) + 44 -> EmojiRepresentation("📎", R.string.verification_emoji_paperclip) + 45 -> EmojiRepresentation("✂️", R.string.verification_emoji_scissors) + 46 -> EmojiRepresentation("🔒", R.string.verification_emoji_lock) + 47 -> EmojiRepresentation("🔑", R.string.verification_emoji_key) + 48 -> EmojiRepresentation("🔨", R.string.verification_emoji_hammer) + 49 -> EmojiRepresentation("☎️", R.string.verification_emoji_telephone) + 50 -> EmojiRepresentation("🏁", R.string.verification_emoji_flag) + 51 -> EmojiRepresentation("🚂", R.string.verification_emoji_train) + 52 -> EmojiRepresentation("🚲", R.string.verification_emoji_bicycle) + 53 -> EmojiRepresentation("✈️", R.string.verification_emoji_airplane) + 54 -> EmojiRepresentation("🚀", R.string.verification_emoji_rocket) + 55 -> EmojiRepresentation("🏆", R.string.verification_emoji_trophy) + 56 -> EmojiRepresentation("⚽", R.string.verification_emoji_ball) + 57 -> EmojiRepresentation("🎸", R.string.verification_emoji_guitar) + 58 -> EmojiRepresentation("🎺", R.string.verification_emoji_trumpet) + 59 -> EmojiRepresentation("🔔", R.string.verification_emoji_bell) + 60 -> EmojiRepresentation("⚓", R.string.verification_emoji_anchor) + 61 -> EmojiRepresentation("🎧", R.string.verification_emoji_headphone) + 62 -> EmojiRepresentation("📁", R.string.verification_emoji_folder) + /* 63 */ else -> EmojiRepresentation("📌", R.string.verification_emoji_pin) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransaction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransaction.kt new file mode 100644 index 00000000..be3f4c78 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransaction.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.matrix.android.internal.crypto.verification + +import im.vector.matrix.android.api.session.crypto.sas.CancelCode +import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTransaction +import im.vector.matrix.android.internal.crypto.model.rest.SendToDeviceObject + +/** + * Generic interactive key verification transaction + */ +internal abstract class VerificationTransaction( + override val transactionId: String, + override val otherUserId: String, + override var otherDeviceId: String? = null, + override val isIncoming: Boolean) : SasVerificationTransaction { + + interface Listener { + fun transactionUpdated(tx: VerificationTransaction) + } + + protected var listeners = ArrayList() + + fun addListener(listener: Listener) { + if (!listeners.contains(listener)) listeners.add(listener) + } + + fun removeListener(listener: Listener) { + listeners.remove(listener) + } + + abstract fun acceptToDeviceEvent(senderId: String, event: SendToDeviceObject) + + abstract fun cancel(code: CancelCode) +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MoshiProvider.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MoshiProvider.kt index becc0cfe..eb2af1ad 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MoshiProvider.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MoshiProvider.kt @@ -17,17 +17,7 @@ package im.vector.matrix.android.internal.di import com.squareup.moshi.Moshi -import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent -import im.vector.matrix.android.api.session.room.model.message.MessageContent -import im.vector.matrix.android.api.session.room.model.message.MessageDefaultContent -import im.vector.matrix.android.api.session.room.model.message.MessageEmoteContent -import im.vector.matrix.android.api.session.room.model.message.MessageFileContent -import im.vector.matrix.android.api.session.room.model.message.MessageImageContent -import im.vector.matrix.android.api.session.room.model.message.MessageLocationContent -import im.vector.matrix.android.api.session.room.model.message.MessageNoticeContent -import im.vector.matrix.android.api.session.room.model.message.MessageTextContent -import im.vector.matrix.android.api.session.room.model.message.MessageType -import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent +import im.vector.matrix.android.api.session.room.model.message.* import im.vector.matrix.android.internal.network.parsing.RuntimeJsonAdapterFactory import im.vector.matrix.android.internal.network.parsing.UriMoshiAdapter import im.vector.matrix.android.internal.session.sync.model.UserAccountData @@ -39,17 +29,17 @@ object MoshiProvider { private val moshi: Moshi = Moshi.Builder() .add(UriMoshiAdapter()) .add(RuntimeJsonAdapterFactory.of(UserAccountData::class.java, "type", UserAccountDataFallback::class.java) - .registerSubtype(UserAccountDataDirectMessages::class.java, UserAccountData.TYPE_DIRECT_MESSAGES) + .registerSubtype(UserAccountDataDirectMessages::class.java, UserAccountData.TYPE_DIRECT_MESSAGES) ) .add(RuntimeJsonAdapterFactory.of(MessageContent::class.java, "msgtype", MessageDefaultContent::class.java) - .registerSubtype(MessageTextContent::class.java, MessageType.MSGTYPE_TEXT) - .registerSubtype(MessageNoticeContent::class.java, MessageType.MSGTYPE_NOTICE) - .registerSubtype(MessageEmoteContent::class.java, MessageType.MSGTYPE_EMOTE) - .registerSubtype(MessageAudioContent::class.java, MessageType.MSGTYPE_AUDIO) - .registerSubtype(MessageImageContent::class.java, MessageType.MSGTYPE_IMAGE) - .registerSubtype(MessageVideoContent::class.java, MessageType.MSGTYPE_VIDEO) - .registerSubtype(MessageLocationContent::class.java, MessageType.MSGTYPE_LOCATION) - .registerSubtype(MessageFileContent::class.java, MessageType.MSGTYPE_FILE) + .registerSubtype(MessageTextContent::class.java, MessageType.MSGTYPE_TEXT) + .registerSubtype(MessageNoticeContent::class.java, MessageType.MSGTYPE_NOTICE) + .registerSubtype(MessageEmoteContent::class.java, MessageType.MSGTYPE_EMOTE) + .registerSubtype(MessageAudioContent::class.java, MessageType.MSGTYPE_AUDIO) + .registerSubtype(MessageImageContent::class.java, MessageType.MSGTYPE_IMAGE) + .registerSubtype(MessageVideoContent::class.java, MessageType.MSGTYPE_VIDEO) + .registerSubtype(MessageLocationContent::class.java, MessageType.MSGTYPE_LOCATION) + .registerSubtype(MessageFileContent::class.java, MessageType.MSGTYPE_FILE) ) .build() @@ -57,4 +47,11 @@ object MoshiProvider { return moshi } + fun getCanonicalJson(type: Class, o: T): String { + val adadpter = moshi.adapter(type) + + // FIXME It is not canonical... + return adadpter.toJson(o) + } + } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt index 42e5d8ed..3ebc200a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/DefaultSession.kt @@ -16,16 +16,22 @@ package im.vector.matrix.android.internal.session +import android.content.Context import android.os.Looper import androidx.annotation.MainThread import androidx.lifecycle.LiveData import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.auth.data.SessionParams +import im.vector.matrix.android.api.listeners.ProgressListener import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.cache.CacheService import im.vector.matrix.android.api.session.content.ContentUploadStateTracker import im.vector.matrix.android.api.session.content.ContentUrlResolver +import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupService +import im.vector.matrix.android.api.session.crypto.keyshare.RoomKeysRequestListener +import im.vector.matrix.android.api.session.crypto.sas.SasVerificationService +import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.group.Group import im.vector.matrix.android.api.session.group.GroupService import im.vector.matrix.android.api.session.group.model.GroupSummary @@ -38,6 +44,13 @@ import im.vector.matrix.android.api.session.sync.FilterService import im.vector.matrix.android.api.session.user.UserService import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.android.api.util.MatrixCallbackDelegate +import im.vector.matrix.android.internal.crypto.CryptoModule +import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult +import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult +import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo +import im.vector.matrix.android.internal.crypto.CryptoManager +import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody +import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse import im.vector.matrix.android.internal.database.LiveEntityObserver import im.vector.matrix.android.internal.di.MatrixKoinComponent import im.vector.matrix.android.internal.di.MatrixKoinHolder @@ -53,7 +66,6 @@ import org.koin.standalone.inject internal class DefaultSession(override val sessionParams: SessionParams) : Session, MatrixKoinComponent { - companion object { const val SCOPE: String = "session" } @@ -69,6 +81,7 @@ internal class DefaultSession(override val sessionParams: SessionParams) : Sessi private val filterService by inject() private val cacheService by inject() private val signOutService by inject() + private val cryptoService by inject() private val syncThread by inject() private val contentUrlResolver by inject() private val contentUploadProgressTracker by inject() @@ -86,11 +99,20 @@ internal class DefaultSession(override val sessionParams: SessionParams) : Sessi val signOutModule = SignOutModule().definition val userModule = UserModule().definition val contentModule = ContentModule().definition - MatrixKoinHolder.instance.loadModules(listOf(sessionModule, syncModule, roomModule, groupModule, userModule, signOutModule, contentModule)) + val cryptoModule = CryptoModule().definition + MatrixKoinHolder.instance.loadModules(listOf(sessionModule, + syncModule, + roomModule, + groupModule, + userModule, + signOutModule, + contentModule, + cryptoModule)) scope = getKoin().getOrCreateScope(SCOPE) if (!monarchy.isMonarchyThreadOpen) { monarchy.openManually() } + cryptoService.start(false, null) liveEntityUpdaters.forEach { it.start() } } @@ -111,6 +133,7 @@ internal class DefaultSession(override val sessionParams: SessionParams) : Sessi assertMainThread() assert(isOpen) liveEntityUpdaters.forEach { it.dispose() } + cryptoService.close() if (monarchy.isMonarchyThreadOpen) { monarchy.closeManually() } @@ -208,6 +231,116 @@ internal class DefaultSession(override val sessionParams: SessionParams) : Sessi return userService.getUser(userId) } + // CRYPTO SERVICE + + override fun setDeviceName(deviceId: String, deviceName: String, callback: MatrixCallback) { + cryptoService.setDeviceName(deviceId, deviceName, callback) + } + + override fun deleteDevice(deviceId: String, accountPassword: String, callback: MatrixCallback) { + cryptoService.deleteDevice(deviceId, accountPassword, callback) + } + + override fun getCryptoVersion(context: Context, longFormat: Boolean): String { + return cryptoService.getCryptoVersion(context, longFormat) + } + + override fun isCryptoEnabled(): Boolean { + return cryptoService.isCryptoEnabled() + } + + override fun getSasVerificationService(): SasVerificationService { + return cryptoService.getSasVerificationService() + } + + override fun getKeysBackupService(): KeysBackupService { + return cryptoService.getKeysBackupService() + } + + override fun isRoomBlacklistUnverifiedDevices(roomId: String, callback: MatrixCallback?) { + cryptoService.isRoomBlacklistUnverifiedDevices(roomId, callback) + } + + override fun setWarnOnUnknownDevices(warn: Boolean) { + cryptoService.setWarnOnUnknownDevices(warn) + } + + override fun setDeviceVerification(verificationStatus: Int, deviceId: String, userId: String, callback: MatrixCallback) { + cryptoService.setDeviceVerification(verificationStatus, deviceId, userId, callback) + } + + override fun getUserDevices(userId: String): MutableList { + return cryptoService.getUserDevices(userId) + } + + override fun setDevicesKnown(devices: List, callback: MatrixCallback?) { + cryptoService.setDevicesKnown(devices, callback) + } + + override fun deviceWithIdentityKey(senderKey: String, algorithm: String): MXDeviceInfo? { + return cryptoService.deviceWithIdentityKey(senderKey, algorithm) + } + + override fun getMyDevice(): MXDeviceInfo { + return cryptoService.getMyDevice() + } + + override fun getDevicesList(callback: MatrixCallback) { + cryptoService.getDevicesList(callback) + } + + override fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int { + return cryptoService.inboundGroupSessionsCount(onlyBackedUp) + } + + override fun getGlobalBlacklistUnverifiedDevices(callback: MatrixCallback?) { + cryptoService.getGlobalBlacklistUnverifiedDevices(callback) + } + + override fun setGlobalBlacklistUnverifiedDevices(block: Boolean, callback: MatrixCallback?) { + cryptoService.setGlobalBlacklistUnverifiedDevices(block, callback) + } + + override fun setRoomUnBlacklistUnverifiedDevices(roomId: String, callback: MatrixCallback) { + cryptoService.setRoomUnBlacklistUnverifiedDevices(roomId, callback) + } + + override fun getDeviceTrackingStatus(userId: String): Int { + return cryptoService.getDeviceTrackingStatus(userId) + } + + override fun importRoomKeys(roomKeysAsArray: ByteArray, password: String, progressListener: ProgressListener?, callback: MatrixCallback) { + cryptoService.importRoomKeys(roomKeysAsArray, password, progressListener, callback) + } + + override fun exportRoomKeys(password: String, callback: MatrixCallback) { + cryptoService.exportRoomKeys(password, callback) + } + + override fun setRoomBlacklistUnverifiedDevices(roomId: String, callback: MatrixCallback) { + cryptoService.setRoomBlacklistUnverifiedDevices(roomId, callback) + } + + override fun getDeviceInfo(userId: String, deviceId: String?, callback: MatrixCallback) { + cryptoService.getDeviceInfo(userId, deviceId, callback) + } + + override fun reRequestRoomKeyForEvent(event: Event) { + cryptoService.reRequestRoomKeyForEvent(event) + } + + override fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody) { + cryptoService.cancelRoomKeyRequest(requestBody) + } + + override fun addRoomKeysRequestListener(listener: RoomKeysRequestListener) { + cryptoService.addRoomKeysRequestListener(listener) + } + + override fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult? { + return cryptoService.decryptEvent(event, timeline) + } + // Private methods ***************************************************************************** private fun assertMainThread() { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt index accb91ec..d877bd3f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt @@ -60,7 +60,7 @@ internal class SessionModule(private val sessionParams: SessionParams) { sessionParams.credentials } - scope(DefaultSession.SCOPE) { + scope(DefaultSession.SCOPE, name = "SessionRealmConfiguration") { val context = get() val childPath = sessionParams.credentials.userId.md5() val directory = File(context.filesDir, childPath) @@ -75,7 +75,7 @@ internal class SessionModule(private val sessionParams: SessionParams) { scope(DefaultSession.SCOPE) { Monarchy.Builder() - .setRealmConfiguration(get()) + .setRealmConfiguration(get("SessionRealmConfiguration")) .build() } @@ -119,7 +119,7 @@ internal class SessionModule(private val sessionParams: SessionParams) { } scope(DefaultSession.SCOPE) { - RealmClearCacheTask(get()) as ClearCacheTask + RealmClearCacheTask(get("SessionRealmConfiguration")) as ClearCacheTask } scope(DefaultSession.SCOPE) { @@ -131,7 +131,7 @@ internal class SessionModule(private val sessionParams: SessionParams) { } scope(DefaultSession.SCOPE) { - DefaultFilterRepository(get()) as FilterRepository + DefaultFilterRepository(get("SessionRealmConfiguration")) as FilterRepository } scope(DefaultSession.SCOPE) { @@ -142,7 +142,7 @@ internal class SessionModule(private val sessionParams: SessionParams) { DefaultFilterService(get(), get(), get()) as FilterService } - scope(DefaultSession.SCOPE) { + scope(DefaultSession.SCOPE) { val retrofit: Retrofit = get() retrofit.create(FilterApi::class.java) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt index 973b7381..192bbac0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt @@ -75,7 +75,7 @@ class RoomModule { } scope(DefaultSession.SCOPE) { - DefaultCreateRoomTask(get(), get()) as CreateRoomTask + DefaultCreateRoomTask(get(), get("SessionRealmConfiguration")) as CreateRoomTask } scope(DefaultSession.SCOPE) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/DefaultRoomMembersService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/DefaultRoomMembersService.kt index a007a1eb..1e1c898a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/DefaultRoomMembersService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/DefaultRoomMembersService.kt @@ -68,4 +68,26 @@ internal class DefaultRoomMembersService(private val roomId: String, .dispatchTo(callback) .executeBy(taskExecutor) } + + override fun getActiveRoomMemberIds(): List { + return getRoomMemberIdsFiltered { it.membership == Membership.JOIN || it.membership == Membership.INVITE } + } + + override fun getJoinedRoomMemberIds(): List { + return getRoomMemberIdsFiltered { it.membership == Membership.JOIN } + } + + /* ========================================================================================== + * Private + * ========================================================================================== */ + + private fun getRoomMemberIdsFiltered(predicate: (RoomMember) -> Boolean): List { + return monarchy.fetchAllCopiedSync { RoomMembers(it, roomId).queryRoomMembersEvent() } + .map { it.asDomain() } + .associateBy { it.stateKey!! } + .mapValues { it.value.content.toModel()!! } + .filterValues { predicate(it) } + .keys + .toList() + } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomMembers.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomMembers.kt index 40266cb7..98d33f12 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomMembers.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/members/RoomMembers.kt @@ -49,7 +49,7 @@ internal class RoomMembers(private val realm: Realm, } fun isUniqueDisplayName(displayName: String?): Boolean { - if(displayName.isNullOrEmpty()){ + if (displayName.isNullOrEmpty()) { return true } return EventEntity @@ -83,12 +83,12 @@ internal class RoomMembers(private val realm: Realm, fun getNumberOfJoinedMembers(): Int { return roomSummary?.joinedMembersCount - ?: getLoaded().filterValues { it.membership == Membership.JOIN }.size + ?: getLoaded().filterValues { it.membership == Membership.JOIN }.size } fun getNumberOfInvitedMembers(): Int { return roomSummary?.invitedMembersCount - ?: getLoaded().filterValues { it.membership == Membership.INVITE }.size + ?: getLoaded().filterValues { it.membership == Membership.INVITE }.size } fun getNumberOfMembers(): Int { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/CryptoSyncHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/CryptoSyncHandler.kt new file mode 100644 index 00000000..992ad747 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/CryptoSyncHandler.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.session.sync + +import im.vector.matrix.android.internal.crypto.CryptoManager +import im.vector.matrix.android.internal.session.sync.model.SyncResponse +import im.vector.matrix.android.internal.session.sync.model.ToDeviceSyncResponse + + +internal class CryptoSyncHandler(private val crypto: CryptoManager) { + + fun handleToDevice(toDevice: ToDeviceSyncResponse) { + toDevice.events?.forEach { + crypto.onToDeviceEvent(it) + } + + } + + fun onSyncCompleted(syncResponse: SyncResponse, fromToken: String?, catchingUp: Boolean) { + crypto.onSyncCompleted(syncResponse, fromToken, catchingUp) + } + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt index 24dff339..3ed252d5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/RoomSyncHandler.kt @@ -22,6 +22,7 @@ import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.MyMembership import im.vector.matrix.android.api.session.room.model.tag.RoomTagContent +import im.vector.matrix.android.internal.crypto.CryptoManager import im.vector.matrix.android.internal.database.helper.addAll import im.vector.matrix.android.internal.database.helper.addOrUpdate import im.vector.matrix.android.internal.database.helper.addStateEvents @@ -33,11 +34,7 @@ import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoo import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection -import im.vector.matrix.android.internal.session.sync.model.InvitedRoomSync -import im.vector.matrix.android.internal.session.sync.model.RoomSync -import im.vector.matrix.android.internal.session.sync.model.RoomSyncAccountData -import im.vector.matrix.android.internal.session.sync.model.RoomSyncEphemeral -import im.vector.matrix.android.internal.session.sync.model.RoomsSyncResponse +import im.vector.matrix.android.internal.session.sync.model.* import io.realm.Realm import io.realm.kotlin.createObject import timber.log.Timber @@ -45,7 +42,8 @@ import timber.log.Timber internal class RoomSyncHandler(private val monarchy: Monarchy, private val readReceiptHandler: ReadReceiptHandler, private val roomSummaryUpdater: RoomSummaryUpdater, - private val roomTagHandler: RoomTagHandler) { + private val roomTagHandler: RoomTagHandler, + private val mCrypto: CryptoManager) { sealed class HandlingStrategy { data class JOINED(val data: Map) : HandlingStrategy() @@ -65,9 +63,9 @@ internal class RoomSyncHandler(private val monarchy: Monarchy, private fun handleRoomSync(realm: Realm, handlingStrategy: HandlingStrategy) { val rooms = when (handlingStrategy) { - is HandlingStrategy.JOINED -> handlingStrategy.data.map { handleJoinedRoom(realm, it.key, it.value) } + is HandlingStrategy.JOINED -> handlingStrategy.data.map { handleJoinedRoom(realm, it.key, it.value) } is HandlingStrategy.INVITED -> handlingStrategy.data.map { handleInvitedRoom(realm, it.key, it.value) } - is HandlingStrategy.LEFT -> handlingStrategy.data.map { handleLeftRoom(it.key, it.value) } + is HandlingStrategy.LEFT -> handlingStrategy.data.map { handleLeftRoom(it.key, it.value) } } realm.insertOrUpdate(rooms) } @@ -79,7 +77,7 @@ internal class RoomSyncHandler(private val monarchy: Monarchy, Timber.v("Handle join sync for room $roomId") val roomEntity = RoomEntity.where(realm, roomId).findFirst() - ?: realm.createObject(roomId) + ?: realm.createObject(roomId) if (roomEntity.membership == MyMembership.INVITED) { roomEntity.chunks.deleteAllFromRealm() @@ -110,6 +108,11 @@ internal class RoomSyncHandler(private val monarchy: Monarchy, ) roomEntity.addOrUpdate(chunkEntity) + // Give info to crypto module + roomSync.timeline.events.forEach { + mCrypto.onLiveEvent(roomId, it) + } + // Try to remove local echo val transactionIds = roomSync.timeline.events.mapNotNull { it.unsignedData?.transactionId } transactionIds.forEach { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncModule.kt index d0f60405..d954667c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncModule.kt @@ -40,19 +40,23 @@ internal class SyncModule { } scope(DefaultSession.SCOPE) { - RoomSyncHandler(get(), get(), get(), get()) + RoomSyncHandler(get(), get(), get(), get(), get()) } scope(DefaultSession.SCOPE) { GroupSyncHandler(get()) } + scope(DefaultSession.SCOPE) { + CryptoSyncHandler(get()) + } + scope(DefaultSession.SCOPE) { UserAccountDataSyncHandler(get()) } scope(DefaultSession.SCOPE) { - SyncResponseHandler(get(), get(), get()) + SyncResponseHandler(get(), get(), get(), get()) } scope(DefaultSession.SCOPE) { @@ -60,7 +64,7 @@ internal class SyncModule { } scope(DefaultSession.SCOPE) { - SyncTokenStore(get()) + SyncTokenStore(get("SessionRealmConfiguration")) } scope(DefaultSession.SCOPE) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncResponseHandler.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncResponseHandler.kt index d9859526..011e2ddb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncResponseHandler.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncResponseHandler.kt @@ -23,12 +23,18 @@ import kotlin.system.measureTimeMillis internal class SyncResponseHandler(private val roomSyncHandler: RoomSyncHandler, private val userAccountDataSyncHandler: UserAccountDataSyncHandler, - private val groupSyncHandler: GroupSyncHandler) { + private val groupSyncHandler: GroupSyncHandler, + private val cryptoSyncHandler: CryptoSyncHandler) { fun handleResponse(syncResponse: SyncResponse, fromToken: String?, isCatchingUp: Boolean): Try { return Try { Timber.v("Start handling sync") val measure = measureTimeMillis { + // Handle the to device events before the room ones + // to ensure to decrypt them properly + if (syncResponse.toDevice != null) { + cryptoSyncHandler.handleToDevice(syncResponse.toDevice) + } if (syncResponse.rooms != null) { roomSyncHandler.handle(syncResponse.rooms) } @@ -38,6 +44,8 @@ internal class SyncResponseHandler(private val roomSyncHandler: RoomSyncHandler, if (syncResponse.accountData != null) { userAccountDataSyncHandler.handle(syncResponse.accountData) } + + cryptoSyncHandler.onSyncCompleted(syncResponse, fromToken, isCatchingUp) } Timber.v("Finish handling sync in $measure ms") syncResponse diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/DeviceOneTimeKeysCountSyncResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/DeviceOneTimeKeysCountSyncResponse.kt index ed0be6eb..c1d9e0a1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/DeviceOneTimeKeysCountSyncResponse.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/model/DeviceOneTimeKeysCountSyncResponse.kt @@ -22,5 +22,4 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) internal data class DeviceOneTimeKeysCountSyncResponse( @Json(name = "signed_curve25519") val signedCurve25519: Int? = null - ) \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/CompatUtil.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/CompatUtil.kt new file mode 100644 index 00000000..76797ac7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/CompatUtil.kt @@ -0,0 +1,325 @@ +/* + * 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 im.vector.matrix.android.internal.util + +import android.content.Context +import android.content.SharedPreferences +import android.os.Build +import android.preference.PreferenceManager +import android.security.KeyPairGeneratorSpec +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyProperties +import android.util.Base64 +import androidx.annotation.RequiresApi +import timber.log.Timber +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import java.math.BigInteger +import java.security.* +import java.security.cert.CertificateException +import java.security.spec.AlgorithmParameterSpec +import java.security.spec.RSAKeyGenParameterSpec +import java.util.* +import java.util.zip.GZIPOutputStream +import javax.crypto.* +import javax.crypto.spec.GCMParameterSpec +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec +import javax.security.auth.x500.X500Principal + +object CompatUtil { + private val TAG = CompatUtil::class.java.simpleName + private const val ANDROID_KEY_STORE_PROVIDER = "AndroidKeyStore" + private const val AES_GCM_CIPHER_TYPE = "AES/GCM/NoPadding" + private const val AES_GCM_KEY_SIZE_IN_BITS = 128 + private const val AES_GCM_IV_LENGTH = 12 + private const val AES_LOCAL_PROTECTION_KEY_ALIAS = "aes_local_protection" + + private const val RSA_WRAP_LOCAL_PROTECTION_KEY_ALIAS = "rsa_wrap_local_protection" + private const val RSA_WRAP_CIPHER_TYPE = "RSA/NONE/PKCS1Padding" + private const val AES_WRAPPED_PROTECTION_KEY_SHARED_PREFERENCE = "aes_wrapped_local_protection" + + private const val SHARED_KEY_ANDROID_VERSION_WHEN_KEY_HAS_BEEN_GENERATED = "android_version_when_key_has_been_generated" + + private var sSecretKeyAndVersion: SecretKeyAndVersion? = null + private var sPrng: SecureRandom? = null + + /** + * Returns the unique SecureRandom instance shared for all local storage encryption operations. + */ + private val prng: SecureRandom + get() { + if (sPrng == null) { + sPrng = SecureRandom() + } + + return sPrng!! + } + + /** + * Create a GZIPOutputStream instance + * Special treatment on KitKat device, force the syncFlush param to false + * Before Kitkat, this param does not exist and after Kitkat it is set to false by default + * + * @param outputStream the output stream + */ + @Throws(IOException::class) + fun createGzipOutputStream(outputStream: OutputStream): GZIPOutputStream { + return if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) { + GZIPOutputStream(outputStream, false) + } else { + GZIPOutputStream(outputStream) + } + } + + /** + * Returns the AES key used for local storage encryption/decryption with AES/GCM. + * The key is created if it does not exist already in the keystore. + * From Marshmallow, this key is generated and operated directly from the android keystore. + * From KitKat and before Marshmallow, this key is stored in the application shared preferences + * wrapped by a RSA key generated and operated directly from the android keystore. + * + * @param context the context holding the application shared preferences + */ + @RequiresApi(Build.VERSION_CODES.KITKAT) + @Synchronized + @Throws(KeyStoreException::class, + CertificateException::class, + NoSuchAlgorithmException::class, + IOException::class, + NoSuchProviderException::class, + InvalidAlgorithmParameterException::class, + NoSuchPaddingException::class, + InvalidKeyException::class, + IllegalBlockSizeException::class, + UnrecoverableKeyException::class) + private fun getAesGcmLocalProtectionKey(context: Context): SecretKeyAndVersion { + if (sSecretKeyAndVersion == null) { + val keyStore = KeyStore.getInstance(ANDROID_KEY_STORE_PROVIDER) + keyStore.load(null) + + Timber.i(TAG, "Loading local protection key") + + var key: SecretKey? + + val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) + // Get the version of Android when the key has been generated, default to the current version of the system. In this case, the + // key will be generated + val androidVersionWhenTheKeyHasBeenGenerated = sharedPreferences.getInt(SHARED_KEY_ANDROID_VERSION_WHEN_KEY_HAS_BEEN_GENERATED, Build.VERSION.SDK_INT) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (keyStore.containsAlias(AES_LOCAL_PROTECTION_KEY_ALIAS)) { + Timber.i(TAG, "AES local protection key found in keystore") + key = keyStore.getKey(AES_LOCAL_PROTECTION_KEY_ALIAS, null) as SecretKey + } else { + // Check if a key has been created on version < M (in case of OS upgrade) + key = readKeyApiL(sharedPreferences, keyStore) + + if (key == null) { + Timber.i(TAG, "Generating AES key with keystore") + val generator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE_PROVIDER) + generator.init( + KeyGenParameterSpec.Builder(AES_LOCAL_PROTECTION_KEY_ALIAS, + KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT) + .setBlockModes(KeyProperties.BLOCK_MODE_GCM) + .setKeySize(AES_GCM_KEY_SIZE_IN_BITS) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) + .build()) + key = generator.generateKey() + + sharedPreferences.edit() + .putInt(SHARED_KEY_ANDROID_VERSION_WHEN_KEY_HAS_BEEN_GENERATED, Build.VERSION.SDK_INT) + .apply() + } + } + } else { + key = readKeyApiL(sharedPreferences, keyStore) + + if (key == null) { + Timber.i(TAG, "Generating RSA key pair with keystore") + val generator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, ANDROID_KEY_STORE_PROVIDER) + val start = Calendar.getInstance() + val end = Calendar.getInstance() + end.add(Calendar.YEAR, 10) + + generator.initialize( + KeyPairGeneratorSpec.Builder(context) + .setAlgorithmParameterSpec(RSAKeyGenParameterSpec(2048, RSAKeyGenParameterSpec.F4)) + .setAlias(RSA_WRAP_LOCAL_PROTECTION_KEY_ALIAS) + .setSubject(X500Principal("CN=matrix-android-sdk")) + .setStartDate(start.time) + .setEndDate(end.time) + .setSerialNumber(BigInteger.ONE) + .build()) + val keyPair = generator.generateKeyPair() + + Timber.i(TAG, "Generating wrapped AES key") + + val aesKeyRaw = ByteArray(AES_GCM_KEY_SIZE_IN_BITS / java.lang.Byte.SIZE) + prng.nextBytes(aesKeyRaw) + key = SecretKeySpec(aesKeyRaw, "AES") + + val cipher = Cipher.getInstance(RSA_WRAP_CIPHER_TYPE) + cipher.init(Cipher.WRAP_MODE, keyPair.public) + val wrappedAesKey = cipher.wrap(key) + + sharedPreferences.edit() + .putString(AES_WRAPPED_PROTECTION_KEY_SHARED_PREFERENCE, Base64.encodeToString(wrappedAesKey, 0)) + .putInt(SHARED_KEY_ANDROID_VERSION_WHEN_KEY_HAS_BEEN_GENERATED, Build.VERSION.SDK_INT) + .apply() + } + } + + sSecretKeyAndVersion = SecretKeyAndVersion(key!!, androidVersionWhenTheKeyHasBeenGenerated) + } + + return sSecretKeyAndVersion!! + } + + /** + * Read the key, which may have been stored when the OS was < M + * + * @param sharedPreferences shared pref + * @param keyStore key store + * @return the key if it exists or null + */ + @Throws(KeyStoreException::class, + NoSuchPaddingException::class, + NoSuchAlgorithmException::class, + InvalidKeyException::class, + UnrecoverableKeyException::class) + private fun readKeyApiL(sharedPreferences: SharedPreferences, keyStore: KeyStore): SecretKey? { + val wrappedAesKeyString = sharedPreferences.getString(AES_WRAPPED_PROTECTION_KEY_SHARED_PREFERENCE, null) + if (wrappedAesKeyString != null && keyStore.containsAlias(RSA_WRAP_LOCAL_PROTECTION_KEY_ALIAS)) { + Timber.i(TAG, "RSA + wrapped AES local protection keys found in keystore") + val privateKey = keyStore.getKey(RSA_WRAP_LOCAL_PROTECTION_KEY_ALIAS, null) as PrivateKey + val wrappedAesKey = Base64.decode(wrappedAesKeyString, 0) + val cipher = Cipher.getInstance(RSA_WRAP_CIPHER_TYPE) + cipher.init(Cipher.UNWRAP_MODE, privateKey) + return cipher.unwrap(wrappedAesKey, "AES", Cipher.SECRET_KEY) as SecretKey + } + + // Key does not exist + return null + } + + /** + * Create a CipherOutputStream instance. + * Before Kitkat, this method will return out as local storage encryption is not implemented for + * devices before KitKat. + * + * @param out the output stream + * @param context the context holding the application shared preferences + */ + @Throws(IOException::class, + CertificateException::class, + NoSuchAlgorithmException::class, + UnrecoverableKeyException::class, + InvalidKeyException::class, + InvalidAlgorithmParameterException::class, + NoSuchPaddingException::class, + NoSuchProviderException::class, + KeyStoreException::class, + IllegalBlockSizeException::class) + fun createCipherOutputStream(out: OutputStream, context: Context): OutputStream? { + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + return out + } + + val keyAndVersion = getAesGcmLocalProtectionKey(context) + if (keyAndVersion == null || keyAndVersion.secretKey == null) { + throw KeyStoreException() + } + + val cipher = Cipher.getInstance(AES_GCM_CIPHER_TYPE) + val iv: ByteArray + + if (keyAndVersion.androidVersionWhenTheKeyHasBeenGenerated >= Build.VERSION_CODES.M) { + cipher.init(Cipher.ENCRYPT_MODE, keyAndVersion.secretKey) + iv = cipher.iv + } else { + iv = ByteArray(AES_GCM_IV_LENGTH) + prng.nextBytes(iv) + cipher.init(Cipher.ENCRYPT_MODE, keyAndVersion.secretKey, IvParameterSpec(iv)) + } + + if (iv.size != AES_GCM_IV_LENGTH) { + Timber.e(TAG, "Invalid IV length " + iv.size) + return null + } + + out.write(iv.size) + out.write(iv) + + return CipherOutputStream(out, cipher) + } + + /** + * Create a CipherInputStream instance. + * Before Kitkat, this method will return `in` because local storage encryption is not implemented for devices before KitKat. + * Warning, if `in` is not an encrypted stream, it's up to the caller to close and reopen `in`, because the stream has been read. + * + * @param in the input stream + * @param context the context holding the application shared preferences + * @return in, or the created InputStream, or null if the InputStream `in` does not contain encrypted data + */ + @Throws(NoSuchPaddingException::class, + NoSuchAlgorithmException::class, + CertificateException::class, + InvalidKeyException::class, + KeyStoreException::class, + UnrecoverableKeyException::class, + IllegalBlockSizeException::class, + NoSuchProviderException::class, + InvalidAlgorithmParameterException::class, + IOException::class) + fun createCipherInputStream(`in`: InputStream, context: Context): InputStream? { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + return `in` + } + + val iv_len = `in`.read() + if (iv_len != AES_GCM_IV_LENGTH) { + Timber.e(TAG, "Invalid IV length $iv_len") + return null + } + + val iv = ByteArray(AES_GCM_IV_LENGTH) + `in`.read(iv) + + val cipher = Cipher.getInstance(AES_GCM_CIPHER_TYPE) + + val keyAndVersion = getAesGcmLocalProtectionKey(context) + if (keyAndVersion == null || keyAndVersion.secretKey == null) { + throw KeyStoreException() + } + + val spec: AlgorithmParameterSpec + + if (keyAndVersion.androidVersionWhenTheKeyHasBeenGenerated >= Build.VERSION_CODES.M) { + spec = GCMParameterSpec(AES_GCM_KEY_SIZE_IN_BITS, iv) + } else { + spec = IvParameterSpec(iv) + } + + cipher.init(Cipher.DECRYPT_MODE, keyAndVersion.secretKey, spec) + + return CipherInputStream(`in`, cipher) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/SecretKeyAndVersion.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/SecretKeyAndVersion.kt new file mode 100644 index 00000000..6accb520 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/SecretKeyAndVersion.kt @@ -0,0 +1,32 @@ +/* + * 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 im.vector.matrix.android.internal.util + +import javax.crypto.SecretKey + +/** + * Tuple which contains the secret key and the version of Android when the key has been generated + */ +internal data class SecretKeyAndVersion( + /** + * the key + */ + val secretKey: SecretKey, + /** + * The android version when the key has been generated + */ + val androidVersionWhenTheKeyHasBeenGenerated: Int) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/StringUtils.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/StringUtils.kt new file mode 100644 index 00000000..4e7f0c7a --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/StringUtils.kt @@ -0,0 +1,63 @@ +/* + * 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 im.vector.matrix.android.internal.util + +import timber.log.Timber + +/** + * Convert a string to an UTF8 String + * + * @param s the string to convert + * @return the utf-8 string + */ +fun convertToUTF8(s: String): String? { + var out: String? = s + + if (null != out) { + try { + val bytes = out.toByteArray(charset("UTF-8")) + out = String(bytes) + } catch (e: Exception) { + Timber.e(e, "## convertToUTF8() failed") + } + + } + + return out +} + +/** + * Convert a string from an UTF8 String + * + * @param s the string to convert + * @return the utf-16 string + */ +fun convertFromUTF8(s: String): String? { + var out: String? = s + + if (null != out) { + try { + val bytes = out.toByteArray() + out = String(bytes, charset("UTF-8")) + } catch (e: Exception) { + Timber.e(e, "## convertFromUTF8() failed") + } + + } + + return out +} diff --git a/matrix-sdk-android/src/main/res/values-bg/strings.xml b/matrix-sdk-android/src/main/res/values-bg/strings.xml index 16f17103..897be232 100644 --- a/matrix-sdk-android/src/main/res/values-bg/strings.xml +++ b/matrix-sdk-android/src/main/res/values-bg/strings.xml @@ -81,4 +81,8 @@ Празна стая + Премахнато съобщение + Съобщение премахнато от %1$s + Премахнато съобщение [причина: %1$s] + Съобщение премахнато от %1$s [причина: %2$s] diff --git a/matrix-sdk-android/src/main/res/values-eo/strings.xml b/matrix-sdk-android/src/main/res/values-eo/strings.xml index a24f0973..c88d96d6 100644 --- a/matrix-sdk-android/src/main/res/values-eo/strings.xml +++ b/matrix-sdk-android/src/main/res/values-eo/strings.xml @@ -14,9 +14,9 @@ %1$s forbaris %2$s %1$s malinvitis %2$s %1$s ŝanĝis sian profilbildon -** Ne eblas malĉifri: %s ** + ** Ne eblas malĉifri: %s ** La aparato de la sendanto ne sendis al ni la ŝlosilojn por tiu mesaĝo. Respondanta al - + diff --git a/matrix-sdk-android/src/main/res/values-eu/strings.xml b/matrix-sdk-android/src/main/res/values-eu/strings.xml index c123936a..4dfab9f4 100644 --- a/matrix-sdk-android/src/main/res/values-eu/strings.xml +++ b/matrix-sdk-android/src/main/res/values-eu/strings.xml @@ -81,4 +81,8 @@ + Mezua kendu da + %1$s erabiltzaileak mezua kendu du + Mezua kendu da [arrazoia: %1$s] + %1$s erabiltzaileak mezua kendu du [arrazoia: %2$s] diff --git a/matrix-sdk-android/src/main/res/values-fi/strings.xml b/matrix-sdk-android/src/main/res/values-fi/strings.xml index cee8489c..d76293b2 100644 --- a/matrix-sdk-android/src/main/res/values-fi/strings.xml +++ b/matrix-sdk-android/src/main/res/values-fi/strings.xml @@ -3,7 +3,7 @@ %1$s lähetti kuvan. %s:n kutsu - %1$s kutsui %2$s + %1$s kutsui käyttäjän %2$s %1$s kutsui sinut %1$s liittyi %1$s poistui @@ -14,25 +14,25 @@ %1$s veti takaisin kutsun käyttäjälle %2$s %1$s vaihtoi profiilikuvaa %1$s asetti näyttönimekseen %2$s - %1$s muutti näyttönimensä %2$s -> %3$s + %1$s muutti näyttönimensä nimestä %2$s nimeen %3$s %1$s poisti näyttönimensä (%2$s) %1$s muutti aiheeksi %2$s %1$s muutti huoneen nimeksi %2$s %s soitti videopuhelun. - %s soitti puhelun. + %s soitti äänipuhelun. %s vastasi puheluun. %s lopetti puhelun. %1$s muutti tulevan huonehistorian näkyväksi käyttäjälle %2$s - kaikki jäsenet, heidän kutsuistaan asti. - kaikki jäsenet, heidän liittymisestään asti. + kaikki huoneen jäsenet, heidän kutsumisestaan asti. + kaikki huoneen jäsenet, heidän liittymisestään asti. kaikki huoneen jäsenet. kaikki. tuntematon (%s). %1$s otti käyttöön osapuolten välisen salauksen (%2$s) - %1$s lähetti VoIP konferenssi-pyynnön - VoIP konferenssi alkoi - VoIP konferenssi loppui + %1$s lähetti VoIP-konferenssipyynnön + VoIP-konferenssi alkoi + VoIP-konferenssi päättyi (profiilikuva muuttui myös) %1$s poisti huoneen nimen @@ -48,7 +48,7 @@ Kuvan lataaminen epäonnistui Verkkovirhe - Matrix virhe + Matrix-virhe Tällä hetkellä ei ole mahdollista liittyä uudelleen tyhjään huoneeseen. @@ -68,7 +68,7 @@ Tyhjä huone -%1$s lähetti tarran. + %1$s lähetti tarran. Vastauksena käyttäjälle @@ -78,8 +78,12 @@ lähetti tiedoston. - %1$s ja yksi muu - %1$s ja %2$d muuta - + %1$s ja yksi muu + %1$s ja %2$d muuta + - + Viesti poistettu + %1$s poisti viestin + Viesti poistettu [syy: %1$s] + %1$s poisti viestin [syy: %2$s] + diff --git a/matrix-sdk-android/src/main/res/values-fr/strings.xml b/matrix-sdk-android/src/main/res/values-fr/strings.xml index f6a00e36..c54e1b7c 100644 --- a/matrix-sdk-android/src/main/res/values-fr/strings.xml +++ b/matrix-sdk-android/src/main/res/values-fr/strings.xml @@ -81,4 +81,8 @@ + Message supprimé + Message supprimé par %1$s + Message supprimé [motif : %1$s] + Message supprimé par %1$s [motif : %2$s] diff --git a/matrix-sdk-android/src/main/res/values-hu/strings.xml b/matrix-sdk-android/src/main/res/values-hu/strings.xml index a235f131..5e4881be 100644 --- a/matrix-sdk-android/src/main/res/values-hu/strings.xml +++ b/matrix-sdk-android/src/main/res/values-hu/strings.xml @@ -80,4 +80,8 @@ + Üzenet eltávolítva + Üzenetet eltávolította: %1$s + Üzenet eltávolítva [ok: %1$s] + Üzenetet eltávolította: %1$s [ok: %2$s] diff --git a/matrix-sdk-android/src/main/res/values-it/strings.xml b/matrix-sdk-android/src/main/res/values-it/strings.xml index df881bfc..bfa3d559 100644 --- a/matrix-sdk-android/src/main/res/values-it/strings.xml +++ b/matrix-sdk-android/src/main/res/values-it/strings.xml @@ -81,4 +81,8 @@ + Messaggio rimosso + Messaggio rimosso da %1$s + Messaggio rimosso [motivo: %1$s] + Messaggio rimosso da %1$s [motivo: %2$s] diff --git a/matrix-sdk-android/src/main/res/values-nl/strings.xml b/matrix-sdk-android/src/main/res/values-nl/strings.xml index 81357f94..cd04670c 100644 --- a/matrix-sdk-android/src/main/res/values-nl/strings.xml +++ b/matrix-sdk-android/src/main/res/values-nl/strings.xml @@ -90,4 +90,8 @@ + Bericht verwijderd + Bericht verwijderd door %1$s + Bericht verwijderd [reden: %1$s] + Bericht verwijderd door %1$s [reden: %2$s] diff --git a/matrix-sdk-android/src/main/res/values-pl/strings.xml b/matrix-sdk-android/src/main/res/values-pl/strings.xml index ec8774b9..1bb8aef3 100644 --- a/matrix-sdk-android/src/main/res/values-pl/strings.xml +++ b/matrix-sdk-android/src/main/res/values-pl/strings.xml @@ -53,7 +53,7 @@ %1$s i jeden inny %1$s i kilku innych %1$s i %2$d innych - + ** Nie można odszyfrować: %s ** diff --git a/matrix-sdk-android/src/main/res/values-sk/strings.xml b/matrix-sdk-android/src/main/res/values-sk/strings.xml index b1a0a4f8..b7e9bb41 100644 --- a/matrix-sdk-android/src/main/res/values-sk/strings.xml +++ b/matrix-sdk-android/src/main/res/values-sk/strings.xml @@ -78,7 +78,7 @@ %1$s a 1 ďalší %1$s a %2$d ďalší %1$s a %2$d ďalších - + diff --git a/matrix-sdk-android/src/main/res/values-sq/strings.xml b/matrix-sdk-android/src/main/res/values-sq/strings.xml index 31c1a408..0203f3fa 100644 --- a/matrix-sdk-android/src/main/res/values-sq/strings.xml +++ b/matrix-sdk-android/src/main/res/values-sq/strings.xml @@ -80,4 +80,8 @@ %1$s dhe %2$d të tjerë + Mesazhi u hoq + Mesazhi u hoq nga %1$s + Mesazh i hequr [arsye: %1$s] + Mesazh i hequr nga %1$s [arsye: %2$s] diff --git a/matrix-sdk-android/src/main/res/values-zh-rCN/strings.xml b/matrix-sdk-android/src/main/res/values-zh-rCN/strings.xml index 563fdded..b92c72e5 100644 --- a/matrix-sdk-android/src/main/res/values-zh-rCN/strings.xml +++ b/matrix-sdk-android/src/main/res/values-zh-rCN/strings.xml @@ -78,4 +78,8 @@ %1$s 与其他 %2$d 位 + 消息已被移除 + 消息已被 %1$s 移除 + 消息已被移除 [原因: %1$s] + 消息已被 %1$s 移除 [原因: %2$s] diff --git a/matrix-sdk-android/src/main/res/values-zh-rTW/strings.xml b/matrix-sdk-android/src/main/res/values-zh-rTW/strings.xml index 3ab114b6..5b3d5a7e 100644 --- a/matrix-sdk-android/src/main/res/values-zh-rTW/strings.xml +++ b/matrix-sdk-android/src/main/res/values-zh-rTW/strings.xml @@ -79,4 +79,8 @@ + 訊息已移除 + 訊息已被 %1$s 移除 + 訊息已移除 [理由:%1$s] + 訊息已被 %1$s 移除 [理由:%2$s] diff --git a/matrix-sdk-android/src/main/res/values/strings.xml b/matrix-sdk-android/src/main/res/values/strings.xml index 2172c095..1414c723 100644 --- a/matrix-sdk-android/src/main/res/values/strings.xml +++ b/matrix-sdk-android/src/main/res/values/strings.xml @@ -98,4 +98,134 @@ Empty room + + + Dog + + Cat + + Lion + + Horse + + Unicorn + + Pig + + Elephant + + Rabbit + + Panda + + Rooster + + Penguin + + Turtle + + Fish + + Octopus + + Butterfly + + Flower + + Tree + + Cactus + + Mushroom + + Globe + + Moon + + Cloud + + Fire + + Banana + + Apple + + Strawberry + + Corn + + Pizza + + Cake + + Heart + + Smiley + + Robot + + Hat + + Glasses + + Wrench + + Santa + + Thumbs Up + + Umbrella + + Hourglass + + Clock + + Gift + + Light Bulb + + Book + + Pencil + + Paperclip + + Scissors + + Lock + + Key + + Hammer + + Telephone + + Flag + + Train + + Bicycle + + Airplane + + Rocket + + Trophy + + Ball + + Guitar + + Trumpet + + Bell + + Anchor + + Headphone + + Folder + + Pin + diff --git a/matrix-sdk-android/src/test/java/ModuleTest.kt b/matrix-sdk-android/src/test/java/ModuleTest.kt new file mode 100644 index 00000000..11735fd3 --- /dev/null +++ b/matrix-sdk-android/src/test/java/ModuleTest.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2019 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. + */ + +// TODO When upgrading to koin 2.0 +/* +class ModuleTest : KoinTest { + + @Test + fun checkModules() { + startKoin { + listOf(CryptoModule().definition) + }.checkModules() + } +} + */ \ No newline at end of file diff --git a/vector/build.gradle b/vector/build.gradle index e75bbd32..5e18183a 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -174,6 +174,12 @@ dependencies { implementation "ru.noties.markwon:core:$markwon_version" implementation "ru.noties.markwon:html:$markwon_version" + // Passphrase strength helper + implementation 'com.nulab-inc:zxcvbn:1.2.5' + + //Alerter + implementation 'com.tapadoo.android:alerter:3.0.2' + implementation 'com.otaliastudios:autocomplete:1.1.0' // Butterknife diff --git a/vector/src/gplay/java/im/vector/riotredesign/push/fcm/troubleshoot/TestTokenRegistration.kt b/vector/src/gplay/java/im/vector/riotredesign/push/fcm/troubleshoot/TestTokenRegistration.kt index 7a8c319e..912597ec 100644 --- a/vector/src/gplay/java/im/vector/riotredesign/push/fcm/troubleshoot/TestTokenRegistration.kt +++ b/vector/src/gplay/java/im/vector/riotredesign/push/fcm/troubleshoot/TestTokenRegistration.kt @@ -27,7 +27,7 @@ class TestTokenRegistration(val fragment: Fragment) : TroubleshootTest(R.string. override fun perform() { /* TODO - Matrix.getInstance(VectorApp.getInstance().baseContext).pushManager.forceSessionsRegistration(object : ApiCallback { + Matrix.getInstance(VectorApp.getInstance().baseContext).pushManager.forceSessionsRegistration(object : MatrixCallback { override fun onSuccess(info: Void?) { description = fragment.getString(R.string.settings_troubleshoot_test_token_registration_success) status = TestStatus.SUCCESS diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 2860cf9a..a65ef719 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -38,6 +38,18 @@ android:label="@string/title_activity_settings" android:windowSoftInputMode="adjustResize" /> + + + + () + + override fun initUiAndData() { + configureToolbar() + waitingView = findViewById(R.id.waiting_view) + } + + /** + * Displays a progress indicator with a message to the user. + * Blocks user interactions. + */ + fun updateWaitingView(data: WaitingViewData?) { + data?.let { + waitingStatusText.text = data.message + + if (data.progress != null && data.progressTotal != null) { + waitingHorizontalProgress.isIndeterminate = false + waitingHorizontalProgress.progress = data.progress + waitingHorizontalProgress.max = data.progressTotal + waitingHorizontalProgress.isVisible = true + waitingCircularProgress.isVisible = false + } else if (data.isIndeterminate) { + waitingHorizontalProgress.isIndeterminate = true + waitingHorizontalProgress.isVisible = true + waitingCircularProgress.isVisible = false + } else { + waitingHorizontalProgress.isVisible = false + waitingCircularProgress.isVisible = true + } + + showWaitingView() + } ?: run { + hideWaitingView() + } + } + + override fun showWaitingView() { + hideKeyboard() + waitingStatusText.isGone = waitingStatusText.text.isNullOrBlank() + super.showWaitingView() + } + + override fun hideWaitingView() { + waitingStatusText.text = null + waitingStatusText.isGone = true + waitingHorizontalProgress.progress = 0 + waitingHorizontalProgress.isVisible = false + super.hideWaitingView() + } + + override fun onBackPressed() { + if (waitingView!!.isVisible) { + // ignore + return + } + super.onBackPressed() + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/core/platform/VectorBaseActivity.kt b/vector/src/main/java/im/vector/riotredesign/core/platform/VectorBaseActivity.kt index 758b78be..0424528e 100644 --- a/vector/src/main/java/im/vector/riotredesign/core/platform/VectorBaseActivity.kt +++ b/vector/src/main/java/im/vector/riotredesign/core/platform/VectorBaseActivity.kt @@ -24,6 +24,7 @@ import android.view.View import androidx.annotation.* import androidx.appcompat.widget.Toolbar import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.core.view.isVisible import butterknife.BindView import butterknife.ButterKnife import butterknife.Unbinder @@ -254,6 +255,39 @@ abstract class VectorBaseActivity : BaseMvRxActivity() { } } + //============================================================================================== + // Handle loading view (also called waiting view or spinner view) + //============================================================================================== + + var waitingView: View? = null + set(value) { + field = value + + // Ensure this view is clickable to catch UI events + value?.isClickable = true + } + + /** + * Tells if the waiting view is currently displayed + * + * @return true if the waiting view is displayed + */ + fun isWaitingViewVisible() = waitingView?.isVisible == true + + /** + * Show the waiting view + */ + open fun showWaitingView() { + waitingView?.isVisible = true + } + + /** + * Hide the waiting view + */ + open fun hideWaitingView() { + waitingView?.isVisible = false + } + /* ========================================================================================== * OPEN METHODS * ========================================================================================== */ diff --git a/vector/src/main/java/im/vector/riotredesign/core/platform/WaitingViewData.kt b/vector/src/main/java/im/vector/riotredesign/core/platform/WaitingViewData.kt new file mode 100644 index 00000000..00191007 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/core/platform/WaitingViewData.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotredesign.core.platform + +/** + * Model to display a Waiting View + */ +data class WaitingViewData( + val message: String, + val progress: Int? = null, + val progressTotal: Int? = null, + val isIndeterminate: Boolean = false +) diff --git a/vector/src/main/java/im/vector/riotredesign/core/preference/VectorPreference.kt b/vector/src/main/java/im/vector/riotredesign/core/preference/VectorPreference.kt index 6b46a633..5ba29341 100755 --- a/vector/src/main/java/im/vector/riotredesign/core/preference/VectorPreference.kt +++ b/vector/src/main/java/im/vector/riotredesign/core/preference/VectorPreference.kt @@ -133,7 +133,7 @@ open class VectorPreference : Preference { } } catch (e: Exception) { - Timber.e(LOG_TAG, "onBindView " + e.message, e) + Timber.e("onBindView " + e.message, e) } super.onBindViewHolder(holder) diff --git a/vector/src/main/java/im/vector/riotredesign/core/resources/ResourceUtils.kt b/vector/src/main/java/im/vector/riotredesign/core/resources/ResourceUtils.kt new file mode 100644 index 00000000..1f1fbd9b --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/core/resources/ResourceUtils.kt @@ -0,0 +1,98 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotredesign.core.resources + +import android.content.Context +import android.net.Uri +import android.text.TextUtils +import android.webkit.MimeTypeMap +import im.vector.riotredesign.core.utils.getFileExtension +import timber.log.Timber +import java.io.InputStream + +/** + * Mime types + */ +const val MIME_TYPE_JPEG = "image/jpeg" +const val MIME_TYPE_JPG = "image/jpg" +const val MIME_TYPE_IMAGE_ALL = "image/*" +const val MIME_TYPE_ALL_CONTENT = "*/*" + +data class Resource( + var mContentStream: InputStream? = null, + var mMimeType: String? = null +) { + /** + * Close the content stream. + */ + fun close() { + try { + mMimeType = null + + mContentStream?.close() + mContentStream = null + } catch (e: Exception) { + Timber.e(e, "Resource.close failed") + } + + } + + /** + * Tells if the opened resource is a jpeg one. + * + * @return true if the opened resource is a jpeg one. + */ + fun isJpegResource(): Boolean { + return MIME_TYPE_JPEG == mMimeType || MIME_TYPE_JPG == mMimeType + } +} + +/** + * Get a resource stream and metadata about it given its URI returned from onActivityResult. + * + * @param context the context. + * @param uri the URI + * @param mimetype the mimetype + * @return a [Resource] encapsulating the opened resource stream and associated metadata + * or `null` if opening the resource stream failed. + */ +fun openResource(context: Context, uri: Uri, mimetype: String?): Resource? { + var mimetype = mimetype + try { + // if the mime type is not provided, try to find it out + if (TextUtils.isEmpty(mimetype)) { + mimetype = context.contentResolver.getType(uri) + + // try to find the mimetype from the filename + if (null == mimetype) { + val extension = getFileExtension(uri.toString()) + if (extension != null) { + mimetype = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) + } + } + } + + return Resource( + context.contentResolver.openInputStream(uri), + mimetype) + + } catch (e: Exception) { + Timber.e(e, "Failed to open resource input stream") + } + + return null +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/core/ui/list/GenericItemViewHolder.kt b/vector/src/main/java/im/vector/riotredesign/core/ui/list/GenericItemViewHolder.kt new file mode 100644 index 00000000..ca8f1c10 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/core/ui/list/GenericItemViewHolder.kt @@ -0,0 +1,104 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.riotredesign.core.ui.list + +import android.view.View +import android.widget.Button +import android.widget.ImageView +import android.widget.ProgressBar +import android.widget.TextView +import androidx.annotation.LayoutRes +import androidx.core.view.isVisible +import androidx.recyclerview.widget.RecyclerView +import butterknife.BindView +import butterknife.ButterKnife +import im.vector.riotredesign.R + +/** + * View Holder for generic list items. + * Displays an item with a title, and optional description. + * Can display an accessory on the right, that can be an image or an indeterminate progress. + * If provided with an action, will display a button at the bottom of the list item. + */ +class GenericItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + + companion object { + @LayoutRes + const val resId = R.layout.item_generic_list + } + + @BindView(R.id.item_generic_title_text) + lateinit var titleText: TextView + + @BindView(R.id.item_generic_description_text) + lateinit var descriptionText: TextView + + @BindView(R.id.item_generic_accessory_image) + lateinit var accessoryImage: ImageView + + @BindView(R.id.item_generic_progress_bar) + lateinit var progressBar: ProgressBar + + @BindView(R.id.item_generic_action_button) + lateinit var actionButton: Button + + init { + ButterKnife.bind(this, itemView) + } + + fun bind(item: GenericRecyclerViewItem) { + titleText.text = item.title + + when (item.style) { + GenericRecyclerViewItem.STYLE.BIG_TEXT -> titleText.textSize = 18f + GenericRecyclerViewItem.STYLE.NORMAL_TEXT -> titleText.textSize = 14f + } + + item.description?.let { + descriptionText.isVisible = true + descriptionText.text = it + } ?: run { descriptionText.isVisible = false } + + if (item.hasIndeterminateProcess) { + progressBar.isVisible = true + accessoryImage.isVisible = false + } else { + progressBar.isVisible = false + if (item.endIconResourceId != -1) { + accessoryImage.setImageResource(item.endIconResourceId) + accessoryImage.isVisible = true + } else { + accessoryImage.isVisible = false + } + } + + val buttonAction = item.buttonAction + + if (buttonAction == null) { + actionButton.isVisible = false + } else { + actionButton.text = buttonAction.title + actionButton.setOnClickListener { + buttonAction.perform?.run() + } + actionButton.isVisible = true + } + + itemView?.setOnClickListener { + item.itemClickAction?.perform?.run() + } + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/core/ui/list/GenericRecyclerViewItem.kt b/vector/src/main/java/im/vector/riotredesign/core/ui/list/GenericRecyclerViewItem.kt new file mode 100644 index 00000000..7b936ddd --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/core/ui/list/GenericRecyclerViewItem.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.riotredesign.core.ui.list + +import androidx.annotation.DrawableRes + + +/** + * A generic list item. + * Displays an item with a title, and optional description. + * Can display an accessory on the right, that can be an image or an indeterminate progress. + * If provided with an action, will display a button at the bottom of the list item. + */ +class GenericRecyclerViewItem(val title: String, + var description: String? = null, + val style: STYLE = STYLE.NORMAL_TEXT) { + + enum class STYLE { + BIG_TEXT, + NORMAL_TEXT + } + + @DrawableRes + var endIconResourceId: Int = -1 + + var hasIndeterminateProcess = false + + var buttonAction: Action? = null + + var itemClickAction: Action? = null + + class Action(var title: String) { + var perform: Runnable? = null + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/core/ui/views/KeysBackupBanner.kt b/vector/src/main/java/im/vector/riotredesign/core/ui/views/KeysBackupBanner.kt new file mode 100755 index 00000000..5cc88971 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/core/ui/views/KeysBackupBanner.kt @@ -0,0 +1,293 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotredesign.core.ui.views + +import android.content.Context +import android.preference.PreferenceManager +import android.util.AttributeSet +import android.view.View +import android.view.ViewGroup +import android.widget.AbsListView +import android.widget.TextView +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.content.edit +import androidx.core.view.isVisible +import androidx.transition.TransitionManager +import butterknife.BindView +import butterknife.ButterKnife +import butterknife.OnClick +import im.vector.riotredesign.R +import timber.log.Timber + +/** + * The view used in VectorHomeActivity to show some information about the keys backup state + * It does have a unique render method + */ +class KeysBackupBanner @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : ConstraintLayout(context, attrs, defStyleAttr), View.OnClickListener { + + @BindView(R.id.view_keys_backup_banner_text_1) + lateinit var textView1: TextView + + @BindView(R.id.view_keys_backup_banner_text_2) + lateinit var textView2: TextView + + @BindView(R.id.view_keys_backup_banner_close_group) + lateinit var close: View + + @BindView(R.id.view_keys_backup_banner_loading) + lateinit var loading: View + + var delegate: Delegate? = null + private var state: State = State.Initial + + private var scrollState = AbsListView.OnScrollListener.SCROLL_STATE_IDLE + set(value) { + field = value + + val pendingV = pendingVisibility + + if (pendingV != null) { + pendingVisibility = null + visibility = pendingV + } + } + + private var pendingVisibility: Int? = null + + init { + setupView() + } + + /** + * This methods is responsible for rendering the view according to the newState + * + * @param newState the newState representing the view + */ + fun render(newState: State, force: Boolean = false) { + if (newState == state && !force) { + Timber.d("State unchanged") + return + } + Timber.d("Rendering $newState") + + state = newState + + hideAll() + + when (newState) { + State.Initial -> renderInitial() + State.Hidden -> renderHidden() + is State.Setup -> renderSetup(newState.numberOfKeys) + is State.Recover -> renderRecover(newState.version) + is State.Update -> renderUpdate(newState.version) + State.BackingUp -> renderBackingUp() + } + } + + override fun setVisibility(visibility: Int) { + if (scrollState != AbsListView.OnScrollListener.SCROLL_STATE_IDLE) { + // Wait for scroll state to be idle + pendingVisibility = visibility + return + } + + if (visibility != getVisibility()) { + // Schedule animation + val parent = parent as ViewGroup + TransitionManager.beginDelayedTransition(parent) + } + + super.setVisibility(visibility) + } + + override fun onClick(v: View?) { + when (state) { + is State.Setup -> { + delegate?.setupKeysBackup() + } + is State.Recover -> { + delegate?.recoverKeysBackup() + } + } + } + + @OnClick(R.id.view_keys_backup_banner_close) + internal fun onCloseClicked() { + state.let { + when (it) { + is State.Setup -> { + PreferenceManager.getDefaultSharedPreferences(context).edit { + putBoolean(BANNER_SETUP_DO_NOT_SHOW_AGAIN, true) + } + } + is State.Recover -> { + PreferenceManager.getDefaultSharedPreferences(context).edit { + putString(BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION, it.version) + } + } + is State.Update -> { + PreferenceManager.getDefaultSharedPreferences(context).edit { + putString(BANNER_UPDATE_DO_NOT_SHOW_FOR_VERSION, it.version) + } + } + else -> { + // Should not happen, close button is not displayed in other cases + } + } + } + + // Force refresh + render(state, true) + } + + // PRIVATE METHODS ***************************************************************************************************************************************** + + private fun setupView() { + inflate(context, R.layout.view_keys_backup_banner, this) + ButterKnife.bind(this) + + setOnClickListener(this) + } + + private fun renderInitial() { + isVisible = false + } + + private fun renderHidden() { + isVisible = false + } + + private fun renderSetup(nbOfKeys: Int) { + if (nbOfKeys == 0 + || PreferenceManager.getDefaultSharedPreferences(context).getBoolean(BANNER_SETUP_DO_NOT_SHOW_AGAIN, false)) { + // Do not display the setup banner if there is no keys to backup, or if the user has already closed it + isVisible = false + } else { + isVisible = true + + textView1.setText(R.string.keys_backup_banner_setup_line1) + textView2.isVisible = true + textView2.setText(R.string.keys_backup_banner_setup_line2) + close.isVisible = true + } + } + + private fun renderRecover(version: String) { + if (version == PreferenceManager.getDefaultSharedPreferences(context).getString(BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION, null)) { + isVisible = false + } else { + isVisible = true + + textView1.setText(R.string.keys_backup_banner_recover_line1) + textView2.isVisible = true + textView2.setText(R.string.keys_backup_banner_recover_line2) + close.isVisible = true + } + } + + private fun renderUpdate(version: String) { + if (version == PreferenceManager.getDefaultSharedPreferences(context).getString(BANNER_UPDATE_DO_NOT_SHOW_FOR_VERSION, null)) { + isVisible = false + } else { + isVisible = true + + textView1.setText(R.string.keys_backup_banner_update_line1) + textView2.isVisible = true + textView2.setText(R.string.keys_backup_banner_update_line2) + close.isVisible = true + } + } + + private fun renderBackingUp() { + isVisible = true + + textView1.setText(R.string.keys_backup_banner_in_progress) + loading.isVisible = true + } + + /** + * Hide all views that are not visible in all state + */ + private fun hideAll() { + textView2.isVisible = false + close.isVisible = false + loading.isVisible = false + } + + /** + * The state representing the view + * It can take one state at a time + */ + sealed class State { + // Not yet rendered + object Initial : State() + + // View will be Gone + object Hidden : State() + + // Keys backup is not setup, numberOfKeys is the number of locally stored keys + data class Setup(val numberOfKeys: Int) : State() + + // Keys backup can be recovered, with version from the server + data class Recover(val version: String) : State() + + // Keys backup can be updated + data class Update(val version: String) : State() + + // Keys are backing up + object BackingUp : State() + } + + /** + * An interface to delegate some actions to another object + */ + interface Delegate { + fun setupKeysBackup() + fun recoverKeysBackup() + } + + companion object { + /** + * Preference key for setup. Value is a boolean. + */ + private const val BANNER_SETUP_DO_NOT_SHOW_AGAIN = "BANNER_SETUP_DO_NOT_SHOW_AGAIN" + + /** + * Preference key for recover. Value is a backup version (String). + */ + private const val BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION = "BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION" + + /** + * Preference key for update. Value is a backup version (String). + */ + private const val BANNER_UPDATE_DO_NOT_SHOW_FOR_VERSION = "BANNER_UPDATE_DO_NOT_SHOW_FOR_VERSION" + + /** + * Inform the banner that a Recover has been done for this version, so do not show the Recover banner for this version + */ + fun onRecoverDoneForVersion(context: Context, version: String) { + PreferenceManager.getDefaultSharedPreferences(context).edit { + putString(BANNER_RECOVER_DO_NOT_SHOW_FOR_VERSION, version) + } + } + } +} + diff --git a/vector/src/main/java/im/vector/riotredesign/core/ui/views/PasswordStrengthBar.kt b/vector/src/main/java/im/vector/riotredesign/core/ui/views/PasswordStrengthBar.kt new file mode 100644 index 00000000..1d53593d --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/core/ui/views/PasswordStrengthBar.kt @@ -0,0 +1,119 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.riotredesign.core.ui.views + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.View +import android.widget.LinearLayout +import androidx.annotation.IntRange +import butterknife.BindColor +import butterknife.BindView +import butterknife.ButterKnife +import im.vector.riotredesign.R + +/** + * A password strength bar custom widget + * Strength is an Integer + * -> 0 No strength + * -> 1 Weak + * -> 2 Fair + * -> 3 Good + * -> 4 Strong + */ +class PasswordStrengthBar @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0) + : LinearLayout(context, attrs, defStyleAttr) { + + @BindView(R.id.password_strength_bar_1) + lateinit var bar1: View + + @BindView(R.id.password_strength_bar_2) + lateinit var bar2: View + + @BindView(R.id.password_strength_bar_3) + lateinit var bar3: View + + @BindView(R.id.password_strength_bar_4) + lateinit var bar4: View + + + @BindColor(R.color.password_strength_bar_undefined) + @JvmField + var colorBackground: Int = 0 + @BindColor(R.color.password_strength_bar_weak) + @JvmField + var colorWeak: Int = 0 + @BindColor(R.color.password_strength_bar_low) + @JvmField + var colorLow: Int = 0 + @BindColor(R.color.password_strength_bar_ok) + @JvmField + var colorOk: Int = 0 + @BindColor(R.color.password_strength_bar_strong) + @JvmField + var colorStrong: Int = 0 + + @IntRange(from = 0, to = 4) + var strength = 0 + set(newValue) { + field = newValue.coerceIn(0, 4) + + when (newValue) { + 0 -> { + bar1.setBackgroundColor(colorBackground) + bar2.setBackgroundColor(colorBackground) + bar3.setBackgroundColor(colorBackground) + bar4.setBackgroundColor(colorBackground) + } + 1 -> { + bar1.setBackgroundColor(colorWeak) + bar2.setBackgroundColor(colorBackground) + bar3.setBackgroundColor(colorBackground) + bar4.setBackgroundColor(colorBackground) + } + 2 -> { + bar1.setBackgroundColor(colorLow) + bar2.setBackgroundColor(colorLow) + bar3.setBackgroundColor(colorBackground) + bar4.setBackgroundColor(colorBackground) + } + 3 -> { + bar1.setBackgroundColor(colorOk) + bar2.setBackgroundColor(colorOk) + bar3.setBackgroundColor(colorOk) + bar4.setBackgroundColor(colorBackground) + } + 4 -> { + bar1.setBackgroundColor(colorStrong) + bar2.setBackgroundColor(colorStrong) + bar3.setBackgroundColor(colorStrong) + bar4.setBackgroundColor(colorStrong) + } + } + } + + init { + LayoutInflater.from(context) + .inflate(R.layout.view_password_strength_bar, this, true) + orientation = HORIZONTAL + ButterKnife.bind(this) + strength = 0 + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/core/utils/FileUtils.kt b/vector/src/main/java/im/vector/riotredesign/core/utils/FileUtils.kt index c0a75970..e1fae0df 100644 --- a/vector/src/main/java/im/vector/riotredesign/core/utils/FileUtils.kt +++ b/vector/src/main/java/im/vector/riotredesign/core/utils/FileUtils.kt @@ -17,6 +17,7 @@ package im.vector.riotredesign.core.utils import android.content.Context +import android.text.TextUtils import timber.log.Timber import java.io.File @@ -87,3 +88,46 @@ private fun recursiveActionOnFile(file: File, action: ActionOnFile): Boolean { return action.invoke(file) } + +/** + * Get the file extension of a fileUri or a filename + * + * @param fileUri the fileUri (can be a simple filename) + * @return the file extension, in lower case, or null is extension is not available or empty + */ +fun getFileExtension(fileUri: String): String? { + var reducedStr = fileUri + + if (!TextUtils.isEmpty(reducedStr)) { + // Remove fragment + val fragment = fileUri.lastIndexOf('#') + if (fragment > 0) { + reducedStr = fileUri.substring(0, fragment) + } + + // Remove query + val query = reducedStr.lastIndexOf('?') + if (query > 0) { + reducedStr = reducedStr.substring(0, query) + } + + // Remove path + val filenamePos = reducedStr.lastIndexOf('/') + val filename = if (0 <= filenamePos) reducedStr.substring(filenamePos + 1) else reducedStr + + // Contrary to method MimeTypeMap.getFileExtensionFromUrl, we do not check the pattern + // See https://stackoverflow.com/questions/14320527/android-should-i-use-mimetypemap-getfileextensionfromurl-bugs + if (!filename.isEmpty()) { + val dotPos = filename.lastIndexOf('.') + if (0 <= dotPos) { + val ext = filename.substring(dotPos + 1) + + if (ext.isNotBlank()) { + return ext.toLowerCase() + } + } + } + } + + return null +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/core/utils/PermissionsTools.kt b/vector/src/main/java/im/vector/riotredesign/core/utils/PermissionsTools.kt index f9438d61..96202766 100644 --- a/vector/src/main/java/im/vector/riotredesign/core/utils/PermissionsTools.kt +++ b/vector/src/main/java/im/vector/riotredesign/core/utils/PermissionsTools.kt @@ -269,9 +269,10 @@ private fun checkPermissions(permissionsToBeGrantedBitMap: Int, val permissionsArrayToBeGranted = permissionsListToBeGranted.toTypedArray() // for android < M, we use a custom dialog to request the contacts book access. - /* if (permissionsListToBeGranted.contains(Manifest.permission.READ_CONTACTS) && Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + TODO() + /* AlertDialog.Builder(activity) .setIcon(android.R.drawable.ic_dialog_info) .setTitle(R.string.permissions_rationale_popup_title) @@ -293,13 +294,13 @@ private fun checkPermissions(permissionsToBeGrantedBitMap: Int, } } .show() + */ } else { fragment?.requestPermissions(permissionsArrayToBeGranted, requestCode) ?: run { ActivityCompat.requestPermissions(activity, permissionsArrayToBeGranted, requestCode) } } - */ } else { // permissions were granted, start now. isPermissionGranted = true diff --git a/vector/src/main/java/im/vector/riotredesign/features/crypto/keysbackup/restore/KeysBackupRestoreActivity.kt b/vector/src/main/java/im/vector/riotredesign/features/crypto/keysbackup/restore/KeysBackupRestoreActivity.kt new file mode 100644 index 00000000..20fb0bd8 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/crypto/keysbackup/restore/KeysBackupRestoreActivity.kt @@ -0,0 +1,113 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.riotredesign.features.crypto.keysbackup.restore + +import android.app.Activity +import android.content.Context +import android.content.Intent +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.FragmentManager +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProviders +import im.vector.fragments.keysbackup.restore.KeysBackupRestoreFromPassphraseFragment +import im.vector.fragments.keysbackup.restore.KeysBackupRestoreSharedViewModel +import im.vector.riotredesign.R +import im.vector.riotredesign.core.platform.SimpleFragmentActivity + +class KeysBackupRestoreActivity : SimpleFragmentActivity() { + + companion object { + + fun intent(context: Context): Intent { + return Intent(context, KeysBackupRestoreActivity::class.java) + } + } + + override fun getTitleRes() = R.string.title_activity_keys_backup_restore + + private lateinit var viewModel: KeysBackupRestoreSharedViewModel + + override fun initUiAndData() { + super.initUiAndData() + viewModel = ViewModelProviders.of(this).get(KeysBackupRestoreSharedViewModel::class.java) + viewModel.initSession(mSession) + + viewModel.keyVersionResult.observe(this, Observer { keyVersion -> + + if (keyVersion != null && supportFragmentManager.fragments.isEmpty()) { + val isBackupCreatedFromPassphrase = keyVersion.getAuthDataAsMegolmBackupAuthData().privateKeySalt != null + if (isBackupCreatedFromPassphrase) { + supportFragmentManager.beginTransaction() + .replace(R.id.container, KeysBackupRestoreFromPassphraseFragment.newInstance()) + .commitNow() + } else { + supportFragmentManager.beginTransaction() + .replace(R.id.container, KeysBackupRestoreFromKeyFragment.newInstance()) + .commitNow() + } + } + }) + + viewModel.keyVersionResultError.observe(this, Observer { uxStateEvent -> + uxStateEvent?.getContentIfNotHandled()?.let { + AlertDialog.Builder(this) + .setTitle(R.string.unknown_error) + .setMessage(it) + .setCancelable(false) + .setPositiveButton(R.string.ok) { _, _ -> + //nop + finish() + } + .show() + } + }) + + if (viewModel.keyVersionResult.value == null) { + //We need to fetch from API + viewModel.getLatestVersion(this) + } + + viewModel.navigateEvent.observe(this, Observer { uxStateEvent -> + when (uxStateEvent?.getContentIfNotHandled()) { + KeysBackupRestoreSharedViewModel.NAVIGATE_TO_RECOVER_WITH_KEY -> { + supportFragmentManager.beginTransaction() + .replace(R.id.container, KeysBackupRestoreFromKeyFragment.newInstance()) + .addToBackStack(null) + .commit() + } + KeysBackupRestoreSharedViewModel.NAVIGATE_TO_SUCCESS -> { + supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) + supportFragmentManager.beginTransaction() + .replace(R.id.container, KeysBackupRestoreSuccessFragment.newInstance()) + .commit() + } + } + }) + + viewModel.loadingEvent.observe(this, Observer { + updateWaitingView(it) + }) + + viewModel.importRoomKeysFinishWithResult.observe(this, Observer { + it?.getContentIfNotHandled()?.let { + //set data? + setResult(Activity.RESULT_OK) + finish() + } + }) + } + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/crypto/keysbackup/restore/KeysBackupRestoreFromKeyFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/crypto/keysbackup/restore/KeysBackupRestoreFromKeyFragment.kt new file mode 100644 index 00000000..31736e33 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/crypto/keysbackup/restore/KeysBackupRestoreFromKeyFragment.kt @@ -0,0 +1,122 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.riotredesign.features.crypto.keysbackup.restore + +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import android.text.Editable +import android.view.inputmethod.EditorInfo +import android.widget.EditText +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProviders +import butterknife.BindView +import butterknife.OnClick +import butterknife.OnTextChanged +import com.google.android.material.textfield.TextInputLayout +import im.vector.fragments.keysbackup.restore.KeysBackupRestoreSharedViewModel +import im.vector.riotredesign.R +import im.vector.riotredesign.core.platform.VectorBaseFragment +import im.vector.riotredesign.core.utils.startImportTextFromFileIntent +import timber.log.Timber + +class KeysBackupRestoreFromKeyFragment : VectorBaseFragment() { + + companion object { + fun newInstance() = KeysBackupRestoreFromKeyFragment() + + private const val REQUEST_TEXT_FILE_GET = 1 + } + + override fun getLayoutResId() = R.layout.fragment_keys_backup_restore_from_key + + private lateinit var viewModel: KeysBackupRestoreFromKeyViewModel + private lateinit var sharedViewModel: KeysBackupRestoreSharedViewModel + + @BindView(R.id.keys_backup_key_enter_til) + lateinit var mKeyInputLayout: TextInputLayout + @BindView(R.id.keys_restore_key_enter_edittext) + lateinit var mKeyTextEdit: EditText + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + viewModel = ViewModelProviders.of(this).get(KeysBackupRestoreFromKeyViewModel::class.java) + sharedViewModel = activity?.run { + ViewModelProviders.of(this).get(KeysBackupRestoreSharedViewModel::class.java) + } ?: throw Exception("Invalid Activity") + + mKeyTextEdit.setText(viewModel.recoveryCode.value) + mKeyTextEdit.setOnEditorActionListener { _, actionId, _ -> + if (actionId == EditorInfo.IME_ACTION_DONE) { + onRestoreFromKey() + return@setOnEditorActionListener true + } + return@setOnEditorActionListener false + } + + mKeyInputLayout.error = viewModel.recoveryCodeErrorText.value + viewModel.recoveryCodeErrorText.observe(this, Observer { newValue -> + mKeyInputLayout.error = newValue + }) + + } + + @OnTextChanged(R.id.keys_restore_key_enter_edittext) + fun onRestoreKeyTextEditChange(s: Editable?) { + s?.toString()?.let { + viewModel.updateCode(it) + } + } + + @OnClick(R.id.keys_restore_button) + fun onRestoreFromKey() { + val value = viewModel.recoveryCode.value + if (value.isNullOrBlank()) { + viewModel.recoveryCodeErrorText.value = context?.getString(R.string.keys_backup_recovery_code_empty_error_message) + } else { + viewModel.recoverKeys(context!!, sharedViewModel) + } + } + + @OnClick(R.id.keys_backup_import) + fun onImport() { + startImportTextFromFileIntent(this, REQUEST_TEXT_FILE_GET) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (requestCode == REQUEST_TEXT_FILE_GET && resultCode == Activity.RESULT_OK) { + val dataURI = data?.data + if (dataURI != null) { + try { + activity + ?.contentResolver + ?.openInputStream(dataURI) + ?.bufferedReader() + ?.use { it.readText() } + ?.let { + mKeyTextEdit.setText(it) + mKeyTextEdit.setSelection(it.length) + } + } catch (e: Exception) { + Timber.e(e, "Failed to read recovery kay from text") + } + } + return + } + super.onActivityResult(requestCode, resultCode, data) + } + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/crypto/keysbackup/restore/KeysBackupRestoreFromKeyViewModel.kt b/vector/src/main/java/im/vector/riotredesign/features/crypto/keysbackup/restore/KeysBackupRestoreFromKeyViewModel.kt new file mode 100644 index 00000000..358b7c6f --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/crypto/keysbackup/restore/KeysBackupRestoreFromKeyViewModel.kt @@ -0,0 +1,113 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.riotredesign.features.crypto.keysbackup.restore + +import android.content.Context +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import im.vector.fragments.keysbackup.restore.KeysBackupRestoreSharedViewModel +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.listeners.StepProgressListener +import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupService +import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult +import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult +import im.vector.riotredesign.R +import im.vector.riotredesign.core.platform.WaitingViewData +import im.vector.riotredesign.core.ui.views.KeysBackupBanner +import timber.log.Timber + +class KeysBackupRestoreFromKeyViewModel : ViewModel() { + + var recoveryCode: MutableLiveData = MutableLiveData() + var recoveryCodeErrorText: MutableLiveData = MutableLiveData() + + init { + recoveryCode.value = null + recoveryCodeErrorText.value = null + } + + //========= Actions ========= + fun updateCode(newValue: String) { + recoveryCode.value = newValue + recoveryCodeErrorText.value = null + } + + fun recoverKeys(context: Context, sharedViewModel: KeysBackupRestoreSharedViewModel) { + val session = sharedViewModel.session + val keysBackup = session.getKeysBackupService() + + recoveryCodeErrorText.value = null + val recoveryKey = recoveryCode.value!! + + val keysVersionResult = sharedViewModel.keyVersionResult.value!! + + keysBackup.restoreKeysWithRecoveryKey(keysVersionResult, + recoveryKey, + null, + session.sessionParams.credentials.userId, + object : StepProgressListener { + override fun onStepProgress(step: StepProgressListener.Step) { + when (step) { + is StepProgressListener.Step.DownloadingKey -> { + sharedViewModel.loadingEvent.value = WaitingViewData(context.getString(R.string.keys_backup_restoring_waiting_message) + + "\n" + context.getString(R.string.keys_backup_restoring_downloading_backup_waiting_message), + isIndeterminate = true) + } + is StepProgressListener.Step.ImportingKey -> { + // Progress 0 can take a while, display an indeterminate progress in this case + if (step.progress == 0) { + sharedViewModel.loadingEvent.value = WaitingViewData(context.getString(R.string.keys_backup_restoring_waiting_message) + + "\n" + context.getString(R.string.keys_backup_restoring_importing_keys_waiting_message), + isIndeterminate = true) + + } else { + sharedViewModel.loadingEvent.value = WaitingViewData(context.getString(R.string.keys_backup_restoring_waiting_message) + + "\n" + context.getString(R.string.keys_backup_restoring_importing_keys_waiting_message), + step.progress, + step.total) + } + } + } + } + }, + object : MatrixCallback { + override fun onSuccess(info: ImportRoomKeysResult) { + sharedViewModel.loadingEvent.value = null + sharedViewModel.didRecoverSucceed(info) + + KeysBackupBanner.onRecoverDoneForVersion(context, keysVersionResult.version!!) + trustOnDecrypt(keysBackup, keysVersionResult) + } + + override fun onFailure(failure: Throwable) { + sharedViewModel.loadingEvent.value = null + recoveryCodeErrorText.value = context.getString(R.string.keys_backup_recovery_code_error_decrypt) + Timber.e(failure, "## onUnexpectedError") + } + }) + } + + private fun trustOnDecrypt(keysBackup: KeysBackupService, keysVersionResult: KeysVersionResult) { + keysBackup.trustKeysBackupVersion(keysVersionResult, true, + object : MatrixCallback { + + override fun onSuccess(data: Unit) { + Timber.d("##### trustKeysBackupVersion onSuccess") + } + + }) + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/crypto/keysbackup/restore/KeysBackupRestoreFromPassphraseFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/crypto/keysbackup/restore/KeysBackupRestoreFromPassphraseFragment.kt new file mode 100644 index 00000000..41d0a4e5 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/crypto/keysbackup/restore/KeysBackupRestoreFromPassphraseFragment.kt @@ -0,0 +1,135 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.fragments.keysbackup.restore + +import android.content.Context +import android.os.Bundle +import android.text.Editable +import android.text.SpannableString +import android.text.style.ClickableSpan +import android.view.View +import android.view.inputmethod.EditorInfo +import android.widget.EditText +import android.widget.ImageView +import android.widget.TextView +import androidx.core.text.set +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProviders +import butterknife.BindView +import butterknife.OnClick +import butterknife.OnTextChanged +import com.google.android.material.textfield.TextInputLayout +import im.vector.riotredesign.R +import im.vector.riotredesign.core.extensions.showPassword +import im.vector.riotredesign.core.platform.VectorBaseFragment +import im.vector.riotredesign.features.crypto.keysbackup.restore.KeysBackupRestoreFromPassphraseViewModel + +class KeysBackupRestoreFromPassphraseFragment : VectorBaseFragment() { + + override fun getLayoutResId() = R.layout.fragment_keys_backup_restore_from_passphrase + + private lateinit var viewModel: KeysBackupRestoreFromPassphraseViewModel + private lateinit var sharedViewModel: KeysBackupRestoreSharedViewModel + + @BindView(R.id.keys_backup_passphrase_enter_til) + lateinit var mPassphraseInputLayout: TextInputLayout + + @BindView(R.id.keys_backup_passphrase_enter_edittext) + lateinit var mPassphraseTextEdit: EditText + + @BindView(R.id.keys_backup_view_show_password) + lateinit var mPassphraseReveal: ImageView + + @BindView(R.id.keys_backup_passphrase_help_with_link) + lateinit var helperTextWithLink: TextView + + @OnClick(R.id.keys_backup_view_show_password) + fun toggleVisibilityMode() { + viewModel.showPasswordMode.value = !(viewModel.showPasswordMode.value ?: false) + } + + companion object { + fun newInstance() = KeysBackupRestoreFromPassphraseFragment() + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + viewModel = ViewModelProviders.of(this).get(KeysBackupRestoreFromPassphraseViewModel::class.java) + sharedViewModel = activity?.run { + ViewModelProviders.of(this).get(KeysBackupRestoreSharedViewModel::class.java) + } ?: throw Exception("Invalid Activity") + + + viewModel.passphraseErrorText.observe(this, Observer { newValue -> + mPassphraseInputLayout.error = newValue + }) + + helperTextWithLink.text = spannableStringForHelperText(context!!) + + viewModel.showPasswordMode.observe(this, Observer { + val shouldBeVisible = it ?: false + mPassphraseTextEdit.showPassword(shouldBeVisible) + mPassphraseReveal.setImageResource(if (shouldBeVisible) R.drawable.ic_eye_closed_black else R.drawable.ic_eye_black) + }) + + mPassphraseTextEdit.setOnEditorActionListener { _, actionId, _ -> + if (actionId == EditorInfo.IME_ACTION_DONE) { + onRestoreBackup() + return@setOnEditorActionListener true + } + return@setOnEditorActionListener false + } + + } + + private fun spannableStringForHelperText(context: Context): SpannableString { + val clickableText = context.getString(R.string.keys_backup_restore_use_recovery_key) + val helperText = context.getString(R.string.keys_backup_restore_with_passphrase_helper_with_link, clickableText) + + val spanString = SpannableString(helperText) + + // used just to have default link representation + val clickableSpan = object : ClickableSpan() { + override fun onClick(widget: View?) {} + } + val start = helperText.indexOf(clickableText) + val end = start + clickableText.length + spanString[start, end] = clickableSpan + return spanString + } + + @OnTextChanged(R.id.keys_backup_passphrase_enter_edittext) + fun onPassphraseTextEditChange(s: Editable?) { + s?.toString()?.let { viewModel.updatePassphrase(it) } + } + + @OnClick(R.id.keys_backup_passphrase_help_with_link) + fun onUseRecoveryKey() { + sharedViewModel.moveToRecoverWithKey() + } + + @OnClick(R.id.keys_backup_restore_with_passphrase_submit) + fun onRestoreBackup() { + val value = viewModel.passphrase.value + if (value.isNullOrBlank()) { + viewModel.passphraseErrorText.value = context?.getString(R.string.passphrase_empty_error_message) + } else { + viewModel.recoverKeys(context!!, sharedViewModel) + } + } + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/crypto/keysbackup/restore/KeysBackupRestoreFromPassphraseViewModel.kt b/vector/src/main/java/im/vector/riotredesign/features/crypto/keysbackup/restore/KeysBackupRestoreFromPassphraseViewModel.kt new file mode 100644 index 00000000..f58ce7c8 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/crypto/keysbackup/restore/KeysBackupRestoreFromPassphraseViewModel.kt @@ -0,0 +1,120 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.riotredesign.features.crypto.keysbackup.restore + +import android.content.Context +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import im.vector.fragments.keysbackup.restore.KeysBackupRestoreSharedViewModel +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.listeners.StepProgressListener +import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupService +import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult +import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult +import im.vector.riotredesign.R +import im.vector.riotredesign.core.platform.WaitingViewData +import im.vector.riotredesign.core.ui.views.KeysBackupBanner +import timber.log.Timber + +class KeysBackupRestoreFromPassphraseViewModel : ViewModel() { + + var passphrase: MutableLiveData = MutableLiveData() + var passphraseErrorText: MutableLiveData = MutableLiveData() + var showPasswordMode: MutableLiveData = MutableLiveData() + + init { + passphrase.value = null + passphraseErrorText.value = null + showPasswordMode.value = false + } + + //========= Actions ========= + + fun updatePassphrase(newValue: String) { + passphrase.value = newValue + passphraseErrorText.value = null + } + + fun recoverKeys(context: Context, sharedViewModel: KeysBackupRestoreSharedViewModel) { + val keysBackup = sharedViewModel.session.getKeysBackupService() + + passphraseErrorText.value = null + + val keysVersionResult = sharedViewModel.keyVersionResult.value!! + + keysBackup.restoreKeyBackupWithPassword(keysVersionResult, + passphrase.value!!, + null, + sharedViewModel.session.sessionParams.credentials.userId, + object : StepProgressListener { + override fun onStepProgress(step: StepProgressListener.Step) { + when (step) { + is StepProgressListener.Step.ComputingKey -> { + sharedViewModel.loadingEvent.value = WaitingViewData(context.getString(R.string.keys_backup_restoring_waiting_message) + + "\n" + context.getString(R.string.keys_backup_restoring_computing_key_waiting_message), + step.progress, + step.total) + } + is StepProgressListener.Step.DownloadingKey -> { + sharedViewModel.loadingEvent.value = WaitingViewData(context.getString(R.string.keys_backup_restoring_waiting_message) + + "\n" + context.getString(R.string.keys_backup_restoring_downloading_backup_waiting_message), + isIndeterminate = true) + } + is StepProgressListener.Step.ImportingKey -> { + // Progress 0 can take a while, display an indeterminate progress in this case + if (step.progress == 0) { + sharedViewModel.loadingEvent.value = WaitingViewData(context.getString(R.string.keys_backup_restoring_waiting_message) + + "\n" + context.getString(R.string.keys_backup_restoring_importing_keys_waiting_message), + isIndeterminate = true) + + } else { + sharedViewModel.loadingEvent.value = WaitingViewData(context.getString(R.string.keys_backup_restoring_waiting_message) + + "\n" + context.getString(R.string.keys_backup_restoring_importing_keys_waiting_message), + step.progress, + step.total) + } + } + } + } + }, + object : MatrixCallback { + override fun onSuccess(data: ImportRoomKeysResult) { + sharedViewModel.loadingEvent.value = null + sharedViewModel.didRecoverSucceed(data) + + KeysBackupBanner.onRecoverDoneForVersion(context, keysVersionResult.version!!) + trustOnDecrypt(keysBackup, keysVersionResult) + } + + override fun onFailure(failure: Throwable) { + sharedViewModel.loadingEvent.value = null + passphraseErrorText.value = context.getString(R.string.keys_backup_passphrase_error_decrypt) + Timber.e(failure, "## onUnexpectedError") + } + }) + } + + private fun trustOnDecrypt(keysBackup: KeysBackupService, keysVersionResult: KeysVersionResult) { + keysBackup.trustKeysBackupVersion(keysVersionResult, true, + object : MatrixCallback { + + override fun onSuccess(data: Unit) { + Timber.d("##### trustKeysBackupVersion onSuccess") + } + + }) + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/crypto/keysbackup/restore/KeysBackupRestoreSharedViewModel.kt b/vector/src/main/java/im/vector/riotredesign/features/crypto/keysbackup/restore/KeysBackupRestoreSharedViewModel.kt new file mode 100644 index 00000000..3db0191e --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/crypto/keysbackup/restore/KeysBackupRestoreSharedViewModel.kt @@ -0,0 +1,102 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.fragments.keysbackup.restore + +import android.content.Context +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult +import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult +import im.vector.riotredesign.R +import im.vector.riotredesign.core.platform.WaitingViewData +import im.vector.riotredesign.core.utils.LiveEvent + +class KeysBackupRestoreSharedViewModel : ViewModel() { + + companion object { + const val NAVIGATE_TO_RECOVER_WITH_KEY = "NAVIGATE_TO_RECOVER_WITH_KEY" + const val NAVIGATE_TO_SUCCESS = "NAVIGATE_TO_SUCCESS" + } + + lateinit var session: Session + + var keyVersionResult: MutableLiveData = MutableLiveData() + + private var _keyVersionResultError: MutableLiveData> = MutableLiveData() + val keyVersionResultError: LiveData> + get() = _keyVersionResultError + + + private var _navigateEvent: MutableLiveData> = MutableLiveData() + val navigateEvent: LiveData> + get() = _navigateEvent + + var loadingEvent: MutableLiveData = MutableLiveData() + + + var importKeyResult: ImportRoomKeysResult? = null + var importRoomKeysFinishWithResult: MutableLiveData> = MutableLiveData() + + + init { + keyVersionResult.value = null + _keyVersionResultError.value = null + loadingEvent.value = null + } + + fun initSession(session: Session) { + this.session = session + } + + + fun getLatestVersion(context: Context) { + val keysBackup = session.getKeysBackupService() + + loadingEvent.value = WaitingViewData(context.getString(R.string.keys_backup_restore_is_getting_backup_version)) + + keysBackup.getCurrentVersion(object : MatrixCallback { + override fun onSuccess(data: KeysVersionResult?) { + loadingEvent.value = null + if (data?.version.isNullOrBlank()) { + //should not happen + _keyVersionResultError.value = LiveEvent(context.getString(R.string.keys_backup_get_version_error, "")) + } else { + keyVersionResult.value = data + } + } + + override fun onFailure(failure: Throwable) { + loadingEvent.value = null + _keyVersionResultError.value = LiveEvent(context.getString(R.string.keys_backup_get_version_error, failure.localizedMessage)) + + // TODO For network error + // _keyVersionResultError.value = LiveEvent(context.getString(R.string.network_error_please_check_and_retry)) + } + }) + } + + fun moveToRecoverWithKey() { + _navigateEvent.value = LiveEvent(NAVIGATE_TO_RECOVER_WITH_KEY) + } + + fun didRecoverSucceed(result: ImportRoomKeysResult) { + importKeyResult = result + _navigateEvent.value = LiveEvent(NAVIGATE_TO_SUCCESS) + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/crypto/keysbackup/restore/KeysBackupRestoreSuccessFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/crypto/keysbackup/restore/KeysBackupRestoreSuccessFragment.kt new file mode 100644 index 00000000..106ed764 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/crypto/keysbackup/restore/KeysBackupRestoreSuccessFragment.kt @@ -0,0 +1,66 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.riotredesign.features.crypto.keysbackup.restore + +import android.os.Bundle +import android.widget.TextView +import androidx.lifecycle.ViewModelProviders +import butterknife.BindView +import butterknife.OnClick +import im.vector.fragments.keysbackup.restore.KeysBackupRestoreSharedViewModel +import im.vector.riotredesign.R +import im.vector.riotredesign.core.platform.VectorBaseFragment +import im.vector.riotredesign.core.utils.LiveEvent + +class KeysBackupRestoreSuccessFragment : VectorBaseFragment() { + + override fun getLayoutResId() = R.layout.fragment_keys_backup_restore_success + + @BindView(R.id.keys_backup_restore_success) + lateinit var mSuccessText: TextView + @BindView(R.id.keys_backup_restore_success_info) + lateinit var mSuccessDetailsText: TextView + + + private lateinit var sharedViewModel: KeysBackupRestoreSharedViewModel + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + sharedViewModel = activity?.run { + ViewModelProviders.of(this).get(KeysBackupRestoreSharedViewModel::class.java) + } ?: throw Exception("Invalid Activity") + + sharedViewModel.importKeyResult?.let { + val part1 = resources.getQuantityString(R.plurals.keys_backup_restore_success_description_part1, + it.totalNumberOfKeys, it.totalNumberOfKeys) + val part2 = resources.getQuantityString(R.plurals.keys_backup_restore_success_description_part2, + it.successfullyNumberOfImportedKeys, it.successfullyNumberOfImportedKeys) + mSuccessDetailsText.text = String.format("%s\n%s", part1, part2) + } + + //We don't put emoji in string xml as it will crash on old devices + mSuccessText.text = context?.getString(R.string.keys_backup_restore_success_title, "🎉") + } + + @OnClick(R.id.keys_backup_setup_done_button) + fun onDone() { + sharedViewModel.importRoomKeysFinishWithResult.value = LiveEvent(sharedViewModel.importKeyResult!!) + } + + companion object { + fun newInstance() = KeysBackupRestoreSuccessFragment() + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/crypto/keysbackup/settings/KeysBackupManageActivity.kt b/vector/src/main/java/im/vector/riotredesign/features/crypto/keysbackup/settings/KeysBackupManageActivity.kt new file mode 100644 index 00000000..e313064f --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/crypto/keysbackup/settings/KeysBackupManageActivity.kt @@ -0,0 +1,76 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.riotredesign.features.crypto.keysbackup.settings + +import android.content.Context +import android.content.Intent +import androidx.appcompat.app.AlertDialog +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProviders +import im.vector.fragments.keysbackup.settings.KeysBackupSettingsFragment +import im.vector.matrix.android.api.MatrixCallback +import im.vector.riotredesign.R +import im.vector.riotredesign.core.platform.SimpleFragmentActivity + + +class KeysBackupManageActivity : SimpleFragmentActivity() { + + companion object { + + fun intent(context: Context): Intent { + val intent = Intent(context, KeysBackupManageActivity::class.java) + return intent + } + } + + override fun getTitleRes() = R.string.encryption_message_recovery + + + private lateinit var viewModel: KeysBackupSettingsViewModel + + override fun initUiAndData() { + super.initUiAndData() + viewModel = ViewModelProviders.of(this).get(KeysBackupSettingsViewModel::class.java) + viewModel.initSession(mSession) + + + if (supportFragmentManager.fragments.isEmpty()) { + supportFragmentManager.beginTransaction() + .replace(R.id.container, KeysBackupSettingsFragment.newInstance()) + .commitNow() + + mSession.getKeysBackupService() + .forceUsingLastVersion(object : MatrixCallback {}) + } + + viewModel.loadingEvent.observe(this, Observer { + updateWaitingView(it) + }) + + + viewModel.apiResultError.observe(this, Observer { uxStateEvent -> + uxStateEvent?.getContentIfNotHandled()?.let { + AlertDialog.Builder(this) + .setTitle(R.string.unknown_error) + .setMessage(it) + .setCancelable(false) + .setPositiveButton(R.string.ok, null) + .show() + } + }) + + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/crypto/keysbackup/settings/KeysBackupSettingsFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/crypto/keysbackup/settings/KeysBackupSettingsFragment.kt new file mode 100644 index 00000000..72dc66c2 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/crypto/keysbackup/settings/KeysBackupSettingsFragment.kt @@ -0,0 +1,130 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.fragments.keysbackup.settings + +import android.os.Bundle +import android.view.View +import androidx.appcompat.app.AlertDialog +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProviders +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import butterknife.BindView +import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState +import im.vector.riotredesign.R +import im.vector.riotredesign.core.platform.VectorBaseFragment +import im.vector.riotredesign.core.platform.WaitingViewData +import im.vector.riotredesign.features.crypto.keysbackup.restore.KeysBackupRestoreActivity +import im.vector.riotredesign.features.crypto.keysbackup.settings.KeysBackupSettingsViewModel +import im.vector.riotredesign.features.crypto.keysbackup.setup.KeysBackupSetupActivity + +class KeysBackupSettingsFragment : VectorBaseFragment(), + KeysBackupSettingsRecyclerViewAdapter.AdapterListener { + + + companion object { + fun newInstance() = KeysBackupSettingsFragment() + } + + override fun getLayoutResId() = R.layout.fragment_keys_backup_settings + + private lateinit var viewModel: KeysBackupSettingsViewModel + + @BindView(R.id.keys_backup_settings_recycler_view) + lateinit var recyclerView: RecyclerView + + private var recyclerViewAdapter: KeysBackupSettingsRecyclerViewAdapter? = null + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val layoutManager = LinearLayoutManager(context) + recyclerView.layoutManager = layoutManager + + recyclerViewAdapter = KeysBackupSettingsRecyclerViewAdapter(activity!!) + recyclerView.adapter = recyclerViewAdapter + recyclerViewAdapter?.adapterListener = this + + + viewModel = activity?.run { + ViewModelProviders.of(this).get(KeysBackupSettingsViewModel::class.java) + } ?: throw Exception("Invalid Activity") + + + viewModel.keyBackupState.observe(this, Observer { keysBackupState -> + if (keysBackupState == null) { + //Cannot happen? + viewModel.keyVersionTrust.value = null + } else { + when (keysBackupState) { + KeysBackupState.Unknown, + KeysBackupState.CheckingBackUpOnHomeserver -> { + viewModel.loadingEvent.value = WaitingViewData("") + } + else -> { + viewModel.loadingEvent.value = null + //All this cases will be manage by looking at the backup trust object + viewModel.session?.getKeysBackupService()?.mKeysBackupVersion?.let { + viewModel.getKeysBackupTrust(it) + } ?: run { + viewModel.keyVersionTrust.value = null + } + } + } + } + + // Update the adapter for each state change + viewModel.session?.let { session -> + recyclerViewAdapter?.updateWithTrust(session, viewModel.keyVersionTrust.value) + } + }) + + viewModel.keyVersionTrust.observe(this, Observer { + viewModel.session?.let { session -> + recyclerViewAdapter?.updateWithTrust(session, it) + } + }) + + } + + override fun didSelectSetupMessageRecovery() { + context?.let { + startActivity(KeysBackupSetupActivity.intent(it, false)) + } + } + + override fun didSelectRestoreMessageRecovery() { + context?.let { + startActivity(KeysBackupRestoreActivity.intent(it)) + } + } + + override fun didSelectDeleteSetupMessageRecovery() { + activity?.let { + AlertDialog.Builder(it) + .setTitle(R.string.keys_backup_settings_delete_confirm_title) + .setMessage(R.string.keys_backup_settings_delete_confirm_message) + .setCancelable(false) + .setPositiveButton(R.string.keys_backup_settings_delete_confirm_title) { _, _ -> + viewModel.deleteCurrentBackup(it) + } + .setNegativeButton(R.string.cancel, null) + .setCancelable(true) + .show() + } + } + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/crypto/keysbackup/settings/KeysBackupSettingsRecyclerViewAdapter.kt b/vector/src/main/java/im/vector/riotredesign/features/crypto/keysbackup/settings/KeysBackupSettingsRecyclerViewAdapter.kt new file mode 100644 index 00000000..aed605b0 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/crypto/keysbackup/settings/KeysBackupSettingsRecyclerViewAdapter.kt @@ -0,0 +1,233 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.fragments.keysbackup.settings + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import androidx.core.view.isVisible +import androidx.recyclerview.widget.RecyclerView +import butterknife.BindView +import butterknife.ButterKnife +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState +import im.vector.matrix.android.internal.crypto.keysbackup.model.KeysBackupVersionTrust +import im.vector.riotredesign.R +import im.vector.riotredesign.core.ui.list.GenericItemViewHolder +import im.vector.riotredesign.core.ui.list.GenericRecyclerViewItem + +class KeysBackupSettingsRecyclerViewAdapter(val context: Context) : RecyclerView.Adapter() { + + val inflater: LayoutInflater = LayoutInflater.from(context) + + private var infoList: List = ArrayList() + + private var isBackupAlreadySetup = false + + var adapterListener: AdapterListener? = null + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + return when (viewType) { + GenericItemViewHolder.resId -> GenericItemViewHolder(inflater.inflate(viewType, parent, false)) + else -> FooterViewHolder(inflater.inflate(viewType, parent, false)) + } + } + + override fun getItemViewType(position: Int): Int { + return if (position < infoList.size) { + GenericItemViewHolder.resId + } else { + R.layout.item_keys_backup_settings_button_footer + } + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + if (holder is GenericItemViewHolder) { + holder.bind(infoList[position]) + } else if (holder is FooterViewHolder) { + if (isBackupAlreadySetup) { + holder.button1.setText(R.string.keys_backup_settings_restore_backup_button) + holder.button1.isVisible = true + holder.button1.setOnClickListener { + adapterListener?.didSelectRestoreMessageRecovery() + } + + holder.button2.setText(R.string.keys_backup_settings_delete_backup_button) + holder.button2.isVisible = true + holder.button2.setOnClickListener { + adapterListener?.didSelectDeleteSetupMessageRecovery() + } + } else { + holder.button1.setText(R.string.keys_backup_setup) + holder.button1.isVisible = true + holder.button1.setOnClickListener { + adapterListener?.didSelectSetupMessageRecovery() + } + + holder.button2.isVisible = false + } + } + } + + override fun getItemCount(): Int { + return infoList.size + 1 /*footer*/ + } + + + fun updateWithTrust(session: Session, keyBackupVersionTrust: KeysBackupVersionTrust?) { + val keyBackupState = session.getKeysBackupService().state + val keyVersionResult = session.getKeysBackupService().mKeysBackupVersion + + val infos = ArrayList() + var itemSummary: GenericRecyclerViewItem? = null + + when (keyBackupState) { + KeysBackupState.Unknown, + KeysBackupState.CheckingBackUpOnHomeserver -> { + //In this cases recycler view is hidden any way + //so do nothing + } + KeysBackupState.Disabled -> { + itemSummary = GenericRecyclerViewItem(context.getString(R.string.keys_backup_settings_status_not_setup), + style = GenericRecyclerViewItem.STYLE.BIG_TEXT) + + isBackupAlreadySetup = false + } + KeysBackupState.WrongBackUpVersion, + KeysBackupState.NotTrusted, + KeysBackupState.Enabling -> { + itemSummary = GenericRecyclerViewItem(context.getString(R.string.keys_backup_settings_status_ko), + style = GenericRecyclerViewItem.STYLE.BIG_TEXT).apply { + description = keyBackupState.toString() + endIconResourceId = R.drawable.unit_test_ko + } + + isBackupAlreadySetup = true + } + KeysBackupState.ReadyToBackUp -> { + itemSummary = GenericRecyclerViewItem(context.getString(R.string.keys_backup_settings_status_ok), + style = GenericRecyclerViewItem.STYLE.BIG_TEXT).apply { + endIconResourceId = R.drawable.unit_test_ok + description = context.getString(R.string.keys_backup_info_keys_all_backup_up) + } + + isBackupAlreadySetup = true + } + KeysBackupState.WillBackUp, + KeysBackupState.BackingUp -> { + itemSummary = GenericRecyclerViewItem(context.getString(R.string.keys_backup_settings_status_ok), + style = GenericRecyclerViewItem.STYLE.BIG_TEXT).apply { + hasIndeterminateProcess = true + + val totalKeys = session.inboundGroupSessionsCount(false) + ?: 0 + val backedUpKeys = session.inboundGroupSessionsCount(true) + ?: 0 + + val remainingKeysToBackup = totalKeys - backedUpKeys + + description = context.resources.getQuantityString(R.plurals.keys_backup_info_keys_backing_up, remainingKeysToBackup, remainingKeysToBackup) + } + + isBackupAlreadySetup = true + } + } + + itemSummary?.let { + infos.add(it) + } + + if (keyBackupVersionTrust != null) { + + if (!keyBackupVersionTrust.usable) { + itemSummary?.description = context.getString(R.string.keys_backup_settings_untrusted_backup) + } + + //Add infos + infos.add(GenericRecyclerViewItem(context.getString(R.string.keys_backup_info_title_version), keyVersionResult?.version + ?: "")) + infos.add(GenericRecyclerViewItem(context.getString(R.string.keys_backup_info_title_algorithm), keyVersionResult?.algorithm + ?: "")) + + keyBackupVersionTrust.signatures.forEach { + val signatureInfo = GenericRecyclerViewItem(context.getString(R.string.keys_backup_info_title_signature)) + val isDeviceKnown = it.device != null + val isDeviceVerified = it.device?.isVerified ?: false + val isSignatureValid = it.valid + val deviceId: String = it.deviceId ?: "" + + if (!isDeviceKnown) { + signatureInfo.description = context.getString(R.string.keys_backup_settings_signature_from_unknown_device, deviceId) + signatureInfo.endIconResourceId = R.drawable.e2e_warning + } else { + if (isSignatureValid) { + if (session.sessionParams.credentials.deviceId == it.deviceId) { + signatureInfo.description = context.getString(R.string.keys_backup_settings_valid_signature_from_this_device) + signatureInfo.endIconResourceId = R.drawable.e2e_verified + } else { + if (isDeviceVerified) { + signatureInfo.description = context.getString(R.string.keys_backup_settings_valid_signature_from_verified_device, deviceId) + signatureInfo.endIconResourceId = R.drawable.e2e_verified + } else { + signatureInfo.description = context.getString(R.string.keys_backup_settings_valid_signature_from_unverified_device, deviceId) + signatureInfo.endIconResourceId = R.drawable.e2e_warning + } + } + } else { + //Invalid signature + signatureInfo.endIconResourceId = R.drawable.e2e_warning + if (isDeviceVerified) { + signatureInfo.description = context.getString(R.string.keys_backup_settings_invalid_signature_from_verified_device, deviceId) + } else { + signatureInfo.description = context.getString(R.string.keys_backup_settings_invalid_signature_from_unverified_device, deviceId) + } + } + } + + infos.add(signatureInfo) + } //end for each + } + + infoList = infos + + notifyDataSetChanged() + } + + class FooterViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + init { + ButterKnife.bind(this, itemView) + } + + @BindView(R.id.keys_backup_settings_footer_button1) + lateinit var button1: Button + + @BindView(R.id.keys_backup_settings_footer_button2) + lateinit var button2: Button + + fun bind() { + + } + } + + interface AdapterListener { + fun didSelectSetupMessageRecovery() + fun didSelectRestoreMessageRecovery() + fun didSelectDeleteSetupMessageRecovery() + } + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/crypto/keysbackup/settings/KeysBackupSettingsViewModel.kt b/vector/src/main/java/im/vector/riotredesign/features/crypto/keysbackup/settings/KeysBackupSettingsViewModel.kt new file mode 100644 index 00000000..b8e17977 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/crypto/keysbackup/settings/KeysBackupSettingsViewModel.kt @@ -0,0 +1,91 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.riotredesign.features.crypto.keysbackup.settings + +import android.content.Context +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupService +import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState +import im.vector.matrix.android.internal.crypto.keysbackup.model.KeysBackupVersionTrust +import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult +import im.vector.riotredesign.R +import im.vector.riotredesign.core.platform.WaitingViewData +import im.vector.riotredesign.core.utils.LiveEvent + + +class KeysBackupSettingsViewModel : ViewModel(), + KeysBackupService.KeysBackupStateListener { + + var session: Session? = null + + var keyVersionTrust: MutableLiveData = MutableLiveData() + var keyBackupState: MutableLiveData = MutableLiveData() + + private var _apiResultError: MutableLiveData> = MutableLiveData() + val apiResultError: LiveData> + get() = _apiResultError + + var loadingEvent: MutableLiveData = MutableLiveData() + + fun initSession(session: Session) { + keyBackupState.value = session.getKeysBackupService().state + if (this.session == null) { + this.session = session + session.getKeysBackupService().addListener(this) + } + } + + fun getKeysBackupTrust(versionResult: KeysVersionResult) { + val keysBackup = session?.getKeysBackupService() + keysBackup?.getKeysBackupTrust(versionResult, object : MatrixCallback { + override fun onSuccess(data: KeysBackupVersionTrust) { + keyVersionTrust.value = data + } + }) + } + + override fun onCleared() { + super.onCleared() + session?.getKeysBackupService()?.removeListener(this) + } + + override fun onStateChange(newState: KeysBackupState) { + keyBackupState.value = newState + } + + fun deleteCurrentBackup(context: Context) { + session?.getKeysBackupService()?.run { + loadingEvent.value = WaitingViewData(context.getString(R.string.keys_backup_settings_deleting_backup)) + if (currentBackupVersion != null) { + deleteBackup(currentBackupVersion!!, object : MatrixCallback { + override fun onSuccess(info: Unit) { + //mmmm if state is stil unknown/checking.. + loadingEvent.value = null + } + + override fun onFailure(failure: Throwable) { + loadingEvent.value = null + _apiResultError.value = LiveEvent(context.getString(R.string.keys_backup_get_version_error, failure.localizedMessage)) + } + }) + } + } + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt b/vector/src/main/java/im/vector/riotredesign/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt new file mode 100644 index 00000000..e3d1de92 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt @@ -0,0 +1,206 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.riotredesign.features.crypto.keysbackup.setup + +import android.content.Context +import android.content.Intent +import androidx.appcompat.app.AlertDialog +import androidx.core.view.isVisible +import androidx.fragment.app.FragmentManager +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProviders +import im.vector.fragments.keysbackup.setup.KeysBackupSetupSharedViewModel +import im.vector.riotredesign.R +import im.vector.riotredesign.core.dialogs.ExportKeysDialog +import im.vector.riotredesign.core.platform.SimpleFragmentActivity + +class KeysBackupSetupActivity : SimpleFragmentActivity() { + + override fun getTitleRes() = R.string.title_activity_keys_backup_setup + + private lateinit var viewModel: KeysBackupSetupSharedViewModel + + override fun initUiAndData() { + super.initUiAndData() + if (isFirstCreation()) { + supportFragmentManager.beginTransaction() + .replace(R.id.container, KeysBackupSetupStep1Fragment.newInstance()) + .commitNow() + } + + viewModel = ViewModelProviders.of(this).get(KeysBackupSetupSharedViewModel::class.java) + viewModel.showManualExport.value = intent.getBooleanExtra(EXTRA_SHOW_MANUAL_EXPORT, false) + viewModel.initSession(mSession) + + + viewModel.isCreatingBackupVersion.observe(this, Observer { + val isCreating = it ?: false + if (isCreating) { + showWaitingView() + } else { + hideWaitingView() + } + }) + + viewModel.loadingStatus.observe(this, Observer { + it?.let { + updateWaitingView(it) + } + }) + + viewModel.navigateEvent.observe(this, Observer { uxStateEvent -> + when (uxStateEvent?.getContentIfNotHandled()) { + KeysBackupSetupSharedViewModel.NAVIGATE_TO_STEP_2 -> { + supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) + supportFragmentManager.beginTransaction() + .replace(R.id.container, KeysBackupSetupStep2Fragment.newInstance()) + .commit() + } + KeysBackupSetupSharedViewModel.NAVIGATE_TO_STEP_3 -> { + supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) + supportFragmentManager.beginTransaction() + .replace(R.id.container, KeysBackupSetupStep3Fragment.newInstance()) + .commit() + } + KeysBackupSetupSharedViewModel.NAVIGATE_FINISH -> { + val resultIntent = Intent() + viewModel.keysVersion.value?.version?.let { + resultIntent.putExtra(KEYS_VERSION, it) + } + setResult(RESULT_OK, resultIntent) + finish() + } + KeysBackupSetupSharedViewModel.NAVIGATE_MANUAL_EXPORT -> { + exportKeysManually() + } + } + }) + + + viewModel.prepareRecoverFailError.observe(this, Observer { error -> + if (error != null) { + AlertDialog.Builder(this) + .setTitle(R.string.unknown_error) + .setMessage(error.localizedMessage) + .setPositiveButton(R.string.ok) { _, _ -> + //nop + viewModel.prepareRecoverFailError.value = null + } + .show() + } + }) + + viewModel.creatingBackupError.observe(this, Observer { error -> + if (error != null) { + AlertDialog.Builder(this) + .setTitle(R.string.unexpected_error) + .setMessage(error.localizedMessage) + .setPositiveButton(R.string.ok) { _, _ -> + //nop + viewModel.creatingBackupError.value = null + } + .show() + } + }) + } + + fun exportKeysManually() { + ExportKeysDialog().show(this, object : ExportKeysDialog.ExportKeyDialogListener { + override fun onPassphrase(passphrase: String) { + notImplemented() + /* + showWaitingView() + + CommonActivityUtils.exportKeys(mSession, passphrase, object : SimpleApiCallback(this@KeysBackupSetupActivity) { + override fun onSuccess(filename: String) { + hideWaitingView() + + AlertDialog.Builder(this@KeysBackupSetupActivity) + .setMessage(getString(R.string.encryption_export_saved_as, filename)) + .setCancelable(false) + .setPositiveButton(R.string.ok) { dialog, which -> + val resultIntent = Intent() + resultIntent.putExtra(MANUAL_EXPORT, true) + setResult(RESULT_OK, resultIntent) + finish() + } + .show() + } + + override fun onNetworkError(e: Exception) { + super.onNetworkError(e) + hideWaitingView() + } + + override fun onMatrixError(e: MatrixError) { + super.onMatrixError(e) + hideWaitingView() + } + + override fun onUnexpectedError(e: Exception) { + super.onUnexpectedError(e) + hideWaitingView() + } + }) + */ + } + }) + + + } + + + override fun onBackPressed() { + if (viewModel.shouldPromptOnBack) { + if (waitingView?.isVisible == true) { + return + } + AlertDialog.Builder(this) + .setTitle(R.string.keys_backup_setup_skip_title) + .setMessage(R.string.keys_backup_setup_skip_msg) + .setNegativeButton(R.string.stay, null) + .setPositiveButton(R.string.abort) { _, _ -> + finish() + } + .show() + } else { + super.onBackPressed() + } + } + +// I think this code is useful, but it violates the code quality rules +// override fun onOptionsItemSelected(item: MenuItem): Boolean { +// if (item.itemId == android .R. id. home) { +// onBackPressed() +// return true +// } +// +// return super.onOptionsItemSelected(item) +// } + + + companion object { + const val KEYS_VERSION = "KEYS_VERSION" + const val MANUAL_EXPORT = "MANUAL_EXPORT" + const val EXTRA_SHOW_MANUAL_EXPORT = "SHOW_MANUAL_EXPORT" + + fun intent(context: Context, showManualExport: Boolean): Intent { + val intent = Intent(context, KeysBackupSetupActivity::class.java) + intent.putExtra(EXTRA_SHOW_MANUAL_EXPORT, showManualExport) + return intent + } + } +} diff --git a/vector/src/main/java/im/vector/riotredesign/features/crypto/keysbackup/setup/KeysBackupSetupSharedViewModel.kt b/vector/src/main/java/im/vector/riotredesign/features/crypto/keysbackup/setup/KeysBackupSetupSharedViewModel.kt new file mode 100644 index 00000000..c9d11d16 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/crypto/keysbackup/setup/KeysBackupSetupSharedViewModel.kt @@ -0,0 +1,176 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.fragments.keysbackup.setup + +import android.content.Context +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.nulabinc.zxcvbn.Strength +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.listeners.ProgressListener +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupService +import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo +import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersion +import im.vector.riotredesign.R +import im.vector.riotredesign.core.platform.WaitingViewData +import im.vector.riotredesign.core.utils.LiveEvent +import timber.log.Timber + +/** + * The shared view model between all fragments. + */ +class KeysBackupSetupSharedViewModel : ViewModel() { + + companion object { + const val NAVIGATE_TO_STEP_2 = "NAVIGATE_TO_STEP_2" + const val NAVIGATE_TO_STEP_3 = "NAVIGATE_TO_STEP_3" + const val NAVIGATE_FINISH = "NAVIGATE_FINISH" + const val NAVIGATE_MANUAL_EXPORT = "NAVIGATE_MANUAL_EXPORT" + } + + lateinit var session: Session + + var showManualExport: MutableLiveData = MutableLiveData() + + var navigateEvent: MutableLiveData> = MutableLiveData() + var shouldPromptOnBack = true + + // Step 2 + var passphrase: MutableLiveData = MutableLiveData() + var passphraseError: MutableLiveData = MutableLiveData() + + var confirmPassphrase: MutableLiveData = MutableLiveData() + var confirmPassphraseError: MutableLiveData = MutableLiveData() + + var passwordStrength: MutableLiveData = MutableLiveData() + var showPasswordMode: MutableLiveData = MutableLiveData() + + // Step 3 + // Var to ignore events from previous request(s) to generate a recovery key + private var currentRequestId: MutableLiveData = MutableLiveData() + var recoveryKey: MutableLiveData = MutableLiveData() + var prepareRecoverFailError: MutableLiveData = MutableLiveData() + var megolmBackupCreationInfo: MegolmBackupCreationInfo? = null + var copyHasBeenMade = false + var isCreatingBackupVersion: MutableLiveData = MutableLiveData() + var creatingBackupError: MutableLiveData = MutableLiveData() + var keysVersion: MutableLiveData = MutableLiveData() + + + var loadingStatus: MutableLiveData = MutableLiveData() + + init { + showPasswordMode.value = false + recoveryKey.value = null + isCreatingBackupVersion.value = false + prepareRecoverFailError.value = null + creatingBackupError.value = null + loadingStatus.value = null + } + + fun initSession(session: Session) { + this.session = session + } + + fun prepareRecoveryKey(context: Context, withPassphrase: String?) { + // Update requestId + currentRequestId.value = System.currentTimeMillis() + isCreatingBackupVersion.value = true + + // Ensure passphrase is hidden during the process + showPasswordMode.value = false + + recoveryKey.value = null + prepareRecoverFailError.value = null + session.let { mxSession -> + val requestedId = currentRequestId.value!! + + mxSession.getKeysBackupService().prepareKeysBackupVersion(withPassphrase, + object : ProgressListener { + override fun onProgress(progress: Int, total: Int) { + if (requestedId != currentRequestId.value) { + //this is an old request, we can't cancel but we can ignore + return + } + + loadingStatus.value = WaitingViewData(context.getString(R.string.keys_backup_setup_step3_generating_key_status), + progress, + total) + } + }, + object : MatrixCallback { + override fun onSuccess(data: MegolmBackupCreationInfo) { + if (requestedId != currentRequestId.value) { + //this is an old request, we can't cancel but we can ignore + return + } + recoveryKey.value = data.recoveryKey + megolmBackupCreationInfo = data + copyHasBeenMade = false + + val keyBackup = session?.getKeysBackupService() + if (keyBackup != null) { + createKeysBackup(context, keyBackup) + } else { + loadingStatus.value = null + + isCreatingBackupVersion.value = false + prepareRecoverFailError.value = Exception() + } + } + + override fun onFailure(failure: Throwable) { + if (requestedId != currentRequestId.value) { + //this is an old request, we can't cancel but we can ignore + return + } + + loadingStatus.value = null + + isCreatingBackupVersion.value = false + prepareRecoverFailError.value = failure ?: Exception() + } + }) + } + } + + private fun createKeysBackup(context: Context, keysBackup: KeysBackupService) { + loadingStatus.value = WaitingViewData(context.getString(R.string.keys_backup_setup_creating_backup), isIndeterminate = true) + + creatingBackupError.value = null + keysBackup.createKeysBackupVersion(megolmBackupCreationInfo!!, object : MatrixCallback { + + override fun onSuccess(data: KeysVersion) { + loadingStatus.value = null + + isCreatingBackupVersion.value = false + keysVersion.value = data + navigateEvent.value = LiveEvent(NAVIGATE_TO_STEP_3) + } + + override fun onFailure(failure: Throwable) { + Timber.e(failure, "## createKeyBackupVersion") + loadingStatus.value = null + + isCreatingBackupVersion.value = false + creatingBackupError.value = failure + } + }) + } + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/crypto/keysbackup/setup/KeysBackupSetupStep1Fragment.kt b/vector/src/main/java/im/vector/riotredesign/features/crypto/keysbackup/setup/KeysBackupSetupStep1Fragment.kt new file mode 100644 index 00000000..0fccd510 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/crypto/keysbackup/setup/KeysBackupSetupStep1Fragment.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotredesign.features.crypto.keysbackup.setup + +import android.os.Bundle +import android.view.View +import android.widget.Button +import android.widget.TextView +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProviders +import butterknife.BindView +import butterknife.OnClick +import im.vector.fragments.keysbackup.setup.KeysBackupSetupSharedViewModel +import im.vector.riotredesign.R +import im.vector.riotredesign.core.platform.VectorBaseFragment +import im.vector.riotredesign.core.utils.LiveEvent + +class KeysBackupSetupStep1Fragment : VectorBaseFragment() { + + companion object { + fun newInstance() = KeysBackupSetupStep1Fragment() + } + + override fun getLayoutResId() = R.layout.fragment_keys_backup_setup_step1 + + private lateinit var viewModel: KeysBackupSetupSharedViewModel + + @BindView(R.id.keys_backup_setup_step1_advanced) + lateinit var advancedOptionText: TextView + + + @BindView(R.id.keys_backup_setup_step1_manualExport) + lateinit var manualExportButton: Button + + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + viewModel = activity?.run { + ViewModelProviders.of(this).get(KeysBackupSetupSharedViewModel::class.java) + } ?: throw Exception("Invalid Activity") + + viewModel.showManualExport.observe(this, Observer { + val showOption = it ?: false + //Can't use isVisible because the kotlin compiler will crash with Back-end (JVM) Internal error: wrong code generated + advancedOptionText.visibility = if (showOption) View.VISIBLE else View.GONE + manualExportButton.visibility = if (showOption) View.VISIBLE else View.GONE + }) + + } + + @OnClick(R.id.keys_backup_setup_step1_button) + fun onButtonClick() { + viewModel.navigateEvent.value = LiveEvent(KeysBackupSetupSharedViewModel.NAVIGATE_TO_STEP_2) + } + + @OnClick(R.id.keys_backup_setup_step1_manualExport) + fun onManualExportClick() { + viewModel.navigateEvent.value = LiveEvent(KeysBackupSetupSharedViewModel.NAVIGATE_MANUAL_EXPORT) + } +} diff --git a/vector/src/main/java/im/vector/riotredesign/features/crypto/keysbackup/setup/KeysBackupSetupStep2Fragment.kt b/vector/src/main/java/im/vector/riotredesign/features/crypto/keysbackup/setup/KeysBackupSetupStep2Fragment.kt new file mode 100644 index 00000000..47ade276 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/crypto/keysbackup/setup/KeysBackupSetupStep2Fragment.kt @@ -0,0 +1,212 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.riotredesign.features.crypto.keysbackup.setup + +import android.os.AsyncTask +import android.os.Bundle +import android.text.TextUtils +import android.view.ViewGroup +import android.view.inputmethod.EditorInfo +import android.widget.EditText +import android.widget.ImageView +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProviders +import androidx.transition.TransitionManager +import butterknife.BindView +import butterknife.OnClick +import butterknife.OnTextChanged +import com.google.android.material.textfield.TextInputLayout +import com.nulabinc.zxcvbn.Zxcvbn +import im.vector.fragments.keysbackup.setup.KeysBackupSetupSharedViewModel +import im.vector.riotredesign.R +import im.vector.riotredesign.core.extensions.showPassword +import im.vector.riotredesign.core.platform.VectorBaseFragment +import im.vector.riotredesign.core.ui.views.PasswordStrengthBar +import im.vector.riotredesign.features.settings.VectorLocale + + +class KeysBackupSetupStep2Fragment : VectorBaseFragment() { + + override fun getLayoutResId() = R.layout.fragment_keys_backup_setup_step2 + + @BindView(R.id.keys_backup_root) + lateinit var rootGroup: ViewGroup + + @BindView(R.id.keys_backup_passphrase_enter_edittext) + lateinit var mPassphraseTextEdit: EditText + + @BindView(R.id.keys_backup_passphrase_enter_til) + lateinit var mPassphraseInputLayout: TextInputLayout + + @BindView(R.id.keys_backup_view_show_password) + lateinit var mPassphraseReveal: ImageView + + @BindView(R.id.keys_backup_passphrase_confirm_edittext) + lateinit var mPassphraseConfirmTextEdit: EditText + + @BindView(R.id.keys_backup_passphrase_confirm_til) + lateinit var mPassphraseConfirmInputLayout: TextInputLayout + + @BindView(R.id.keys_backup_passphrase_security_progress) + lateinit var mPassphraseProgressLevel: PasswordStrengthBar + + private val zxcvbn = Zxcvbn() + + @OnTextChanged(R.id.keys_backup_passphrase_enter_edittext) + fun onPassphraseChanged() { + viewModel.passphrase.value = mPassphraseTextEdit.text.toString() + viewModel.confirmPassphraseError.value = null + } + + @OnTextChanged(R.id.keys_backup_passphrase_confirm_edittext) + fun onConfirmPassphraseChanged() { + viewModel.confirmPassphrase.value = mPassphraseConfirmTextEdit.text.toString() + } + + private lateinit var viewModel: KeysBackupSetupSharedViewModel + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + viewModel = activity?.run { + ViewModelProviders.of(this).get(KeysBackupSetupSharedViewModel::class.java) + } ?: throw Exception("Invalid Activity") + + viewModel.shouldPromptOnBack = true + bindViewToViewModel() + } + + /* ========================================================================================== + * MENU + * ========================================================================================== */ + + private fun bindViewToViewModel() { + viewModel.passwordStrength.observe(this, Observer { strength -> + if (strength == null) { + mPassphraseProgressLevel.strength = 0 + mPassphraseInputLayout.error = null + } else { + val score = strength.score + mPassphraseProgressLevel.strength = score + + if (score in 1..3) { + val warning = strength.feedback?.getWarning(VectorLocale.applicationLocale) + if (warning != null) { + mPassphraseInputLayout.error = warning + } + + val suggestions = strength.feedback?.getSuggestions(VectorLocale.applicationLocale) + if (suggestions != null) { + mPassphraseInputLayout.error = suggestions.firstOrNull() + } + + } else { + mPassphraseInputLayout.error = null + } + + } + }) + + viewModel.passphrase.observe(this, Observer { newValue -> + if (TextUtils.isEmpty(newValue)) { + viewModel.passwordStrength.value = null + } else { + AsyncTask.execute { + val strength = zxcvbn.measure(newValue) + activity?.runOnUiThread { + viewModel.passwordStrength.value = strength + } + } + } + + }) + + mPassphraseTextEdit.setText(viewModel.passphrase.value) + + viewModel.passphraseError.observe(this, Observer { + TransitionManager.beginDelayedTransition(rootGroup) + mPassphraseInputLayout.error = it + }) + + mPassphraseConfirmTextEdit.setText(viewModel.confirmPassphrase.value) + + viewModel.showPasswordMode.observe(this, Observer { + val shouldBeVisible = it ?: false + mPassphraseTextEdit.showPassword(shouldBeVisible) + mPassphraseConfirmTextEdit.showPassword(shouldBeVisible) + mPassphraseReveal.setImageResource(if (shouldBeVisible) R.drawable.ic_eye_closed_black else R.drawable.ic_eye_black) + }) + + viewModel.confirmPassphraseError.observe(this, Observer { + TransitionManager.beginDelayedTransition(rootGroup) + mPassphraseConfirmInputLayout.error = it + }) + + mPassphraseConfirmTextEdit.setOnEditorActionListener { _, actionId, _ -> + if (actionId == EditorInfo.IME_ACTION_DONE) { + doNext() + return@setOnEditorActionListener true + } + return@setOnEditorActionListener false + } + + } + + @OnClick(R.id.keys_backup_view_show_password) + fun toggleVisibilityMode() { + viewModel.showPasswordMode.value = !(viewModel.showPasswordMode.value ?: false) + } + + @OnClick(R.id.keys_backup_setup_step2_button) + fun doNext() { + when { + TextUtils.isEmpty(viewModel.passphrase.value) -> { + viewModel.passphraseError.value = context?.getString(R.string.passphrase_empty_error_message) + } + viewModel.passphrase.value != viewModel.confirmPassphrase.value -> { + viewModel.confirmPassphraseError.value = context?.getString(R.string.passphrase_passphrase_does_not_match) + } + viewModel.passwordStrength.value?.score ?: 0 < 4 -> { + viewModel.passphraseError.value = context?.getString(R.string.passphrase_passphrase_too_weak) + } + else -> { + viewModel.megolmBackupCreationInfo = null + + viewModel.prepareRecoveryKey(activity!!, viewModel.passphrase.value) + } + } + } + + @OnClick(R.id.keys_backup_setup_step2_skip_button) + fun skipPassphrase() { + when { + TextUtils.isEmpty(viewModel.passphrase.value) -> { + // Generate a recovery key for the user + viewModel.megolmBackupCreationInfo = null + + viewModel.prepareRecoveryKey(activity!!, null) + } + else -> { + // User has entered a passphrase but want to skip this step. + viewModel.passphraseError.value = context?.getString(R.string.keys_backup_passphrase_not_empty_error_message) + } + } + } + + companion object { + fun newInstance() = KeysBackupSetupStep2Fragment() + } +} diff --git a/vector/src/main/java/im/vector/riotredesign/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt b/vector/src/main/java/im/vector/riotredesign/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt new file mode 100644 index 00000000..d2739d16 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/crypto/keysbackup/setup/KeysBackupSetupStep3Fragment.kt @@ -0,0 +1,184 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.riotredesign.features.crypto.keysbackup.setup + +import android.os.Bundle +import android.view.View +import android.widget.Button +import android.widget.TextView +import android.widget.Toast +import androidx.core.view.isVisible +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProviders +import butterknife.BindView +import butterknife.OnClick +import com.google.android.material.bottomsheet.BottomSheetDialog +import im.vector.fragments.keysbackup.setup.KeysBackupSetupSharedViewModel +import im.vector.riotredesign.R +import im.vector.riotredesign.core.platform.VectorBaseFragment +import im.vector.riotredesign.core.utils.* +import java.io.ByteArrayInputStream + +class KeysBackupSetupStep3Fragment : VectorBaseFragment() { + + override fun getLayoutResId() = R.layout.fragment_keys_backup_setup_step3 + + @BindView(R.id.keys_backup_setup_step3_button) + lateinit var mFinishButton: Button + + @BindView(R.id.keys_backup_recovery_key_text) + lateinit var mRecoveryKeyTextView: TextView + + @BindView(R.id.keys_backup_setup_step3_line2_text) + lateinit var mRecoveryKeyLabel2TextView: TextView + + companion object { + fun newInstance() = KeysBackupSetupStep3Fragment() + } + + private lateinit var viewModel: KeysBackupSetupSharedViewModel + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + viewModel = activity?.run { + ViewModelProviders.of(this).get(KeysBackupSetupSharedViewModel::class.java) + } ?: throw Exception("Invalid Activity") + + + viewModel.shouldPromptOnBack = false + + viewModel.passphrase.observe(this, Observer { + if (it.isNullOrBlank()) { + //Recovery was generated, so show key and options to save + mRecoveryKeyLabel2TextView.text = getString(R.string.keys_backup_setup_step3_text_line2_no_passphrase) + mFinishButton.text = getString(R.string.keys_backup_setup_step3_button_title_no_passphrase) + + mRecoveryKeyTextView.text = viewModel.recoveryKey.value!! + .replace(" ", "") + .chunked(16) + .joinToString("\n") { + it + .chunked(4) + .joinToString(" ") + } + mRecoveryKeyTextView.isVisible = true + + } else { + mRecoveryKeyLabel2TextView.text = getString(R.string.keys_backup_setup_step3_text_line2) + mFinishButton.text = getString(R.string.keys_backup_setup_step3_button_title) + mRecoveryKeyTextView.isVisible = false + } + }) + + } + + @OnClick(R.id.keys_backup_setup_step3_button) + fun onFinishButtonClicked() { + if (viewModel.megolmBackupCreationInfo == null) { + //nothing + } else { + if (viewModel.passphrase.value.isNullOrBlank() && !viewModel.copyHasBeenMade) { + Toast.makeText(context, R.string.keys_backup_setup_step3_please_make_copy, Toast.LENGTH_LONG).show() + } else { + viewModel.navigateEvent.value = LiveEvent(KeysBackupSetupSharedViewModel.NAVIGATE_FINISH) + } + } + } + + @OnClick(R.id.keys_backup_setup_step3_copy_button) + fun onCopyButtonClicked() { + val dialog = BottomSheetDialog(activity!!) + dialog.setContentView(R.layout.bottom_sheet_save_recovery_key) + dialog.setCanceledOnTouchOutside(true) + val recoveryKey = viewModel.recoveryKey.value!! + + if (viewModel.passphrase.value.isNullOrBlank()) { + dialog.findViewById(R.id.keys_backup_recovery_key_text)?.isVisible = false + } else { + dialog.findViewById(R.id.keys_backup_recovery_key_text)?.let { + it.isVisible = true + it.text = recoveryKey.replace(" ", "") + .chunked(16) + .joinToString("\n") { + it + .chunked(4) + .joinToString(" ") + } + } + } + + dialog.findViewById(R.id.keys_backup_setup_save)?.setOnClickListener { + if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES, this, PERMISSION_REQUEST_CODE_EXPORT_KEYS)) { + exportRecoveryKeyToFile(recoveryKey) + } + dialog.dismiss() + } + + dialog.findViewById(R.id.keys_backup_setup_share)?.setOnClickListener { + startSharePlainTextIntent(this, + context?.getString(R.string.keys_backup_setup_step3_share_intent_chooser_title), + recoveryKey, + context?.getString(R.string.recovery_key)) + viewModel.copyHasBeenMade = true + dialog.dismiss() + } + + dialog.show() + } + + @OnClick(R.id.keys_backup_recovery_key_text) + fun onRecoveryKeyClicked() { + viewModel.recoveryKey.value?.let { + viewModel.copyHasBeenMade = true + + copyToClipboard(activity!!, it) + } + } + + fun exportRecoveryKeyToFile(it: String) { + val stream = ByteArrayInputStream(it.toByteArray()) + + TODO() + /* + val url = viewModel.session.mediaCache.saveMedia(stream, "recovery-key" + System.currentTimeMillis() + ".txt", "text/plain") + stream.close() + CommonActivityUtils.saveMediaIntoDownloads(context, + File(Uri.parse(url).path!!), "recovery-key.txt", "text/plain", object : SimpleApiCallback() { + override fun onSuccess(path: String) { + context?.let { + AlertDialog.Builder(it) + .setMessage(getString(R.string.recovery_key_export_saved_as_warning, path)) + .setCancelable(false) + .setPositiveButton(R.string.ok, null) + .show() + } + + viewModel.copyHasBeenMade = true + } + }) + */ + } + + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + if (allGranted(grantResults)) { + if (requestCode == PERMISSION_REQUEST_CODE_EXPORT_KEYS) { + viewModel.recoveryKey.value?.let { + exportRecoveryKeyToFile(it) + } + } + } + } +} diff --git a/vector/src/main/java/im/vector/riotredesign/features/crypto/verification/IncomingVerificationRequestHandler.kt b/vector/src/main/java/im/vector/riotredesign/features/crypto/verification/IncomingVerificationRequestHandler.kt new file mode 100644 index 00000000..a3ca69ce --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/crypto/verification/IncomingVerificationRequestHandler.kt @@ -0,0 +1,99 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.riotredesign.features.crypto.verification + +import android.content.Context +import im.vector.matrix.android.api.Matrix +import im.vector.matrix.android.api.auth.data.Credentials +import im.vector.matrix.android.api.session.crypto.sas.SasVerificationService +import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTransaction +import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState +import im.vector.riotredesign.R +import im.vector.riotredesign.features.popup.PopupAlertManager + +/** + * Listens to the VerificationManager and add a new notification when an incoming request is detected. + */ +class IncomingVerificationRequestHandler(val context: Context, + private val credentials: Credentials, + verificationService: SasVerificationService) : SasVerificationService.SasVerificationListener { + + init { + verificationService.addListener(this) + } + + override fun transactionCreated(tx: SasVerificationTransaction) {} + + override fun transactionUpdated(tx: SasVerificationTransaction) { + when (tx.state) { + SasVerificationTxState.OnStarted -> { + //Add a notification for every incoming request + val session = Matrix.getInstance().currentSession!! + val name = session.getUser(tx.otherUserId)?.displayName + ?: tx.otherUserId + + val alert = PopupAlertManager.VectorAlert( + "kvr_${tx.transactionId}", + context.getString(R.string.sas_incoming_request_notif_title), + context.getString(R.string.sas_incoming_request_notif_content, name), + R.drawable.shield + ).apply { + contentAction = Runnable { + val intent = SASVerificationActivity.incomingIntent(context, + credentials.userId, + tx.otherUserId, + tx.transactionId) + weakCurrentActivity?.get()?.startActivity(intent) + } + dismissedAction = Runnable { + tx.cancel() + } + addButton( + context.getString(R.string.ignore), + Runnable { + tx.cancel() + } + ) + addButton( + context.getString(R.string.action_open), + Runnable { + val intent = SASVerificationActivity.incomingIntent(context, + credentials.userId, + tx.otherUserId, + tx.transactionId) + weakCurrentActivity?.get()?.startActivity(intent) + } + ) + //10mn expiration + expirationTimestamp = System.currentTimeMillis() + (10 * 60 * 1000L) + + } + PopupAlertManager.postVectorAlert(alert) + } + SasVerificationTxState.Cancelled, + SasVerificationTxState.OnCancelled, + SasVerificationTxState.Verified -> { + //cancel related notification + PopupAlertManager.cancelAlert("kvr_${tx.transactionId}") + } + else -> Unit + } + } + + override fun markedAsManuallyVerified(userId: String, deviceId: String) { + + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/crypto/verification/SASVerificationActivity.kt b/vector/src/main/java/im/vector/riotredesign/features/crypto/verification/SASVerificationActivity.kt new file mode 100644 index 00000000..4db3315a --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/crypto/verification/SASVerificationActivity.kt @@ -0,0 +1,245 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.riotredesign.features.crypto.verification + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.view.MenuItem +import androidx.appcompat.app.AlertDialog +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProviders +import im.vector.matrix.android.api.session.crypto.sas.IncomingSasVerificationTransaction +import im.vector.matrix.android.api.session.crypto.sas.OutgoingSasVerificationRequest +import im.vector.matrix.android.api.session.crypto.sas.CancelCode +import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState +import im.vector.riotredesign.R +import im.vector.riotredesign.core.platform.SimpleFragmentActivity +import im.vector.riotredesign.core.platform.WaitingViewData + +class SASVerificationActivity : SimpleFragmentActivity() { + + + companion object { + + private const val EXTRA_MATRIX_ID = "EXTRA_MATRIX_ID" + private const val EXTRA_TRANSACTION_ID = "EXTRA_TRANSACTION_ID" + private const val EXTRA_OTHER_USER_ID = "EXTRA_OTHER_USER_ID" + private const val EXTRA_OTHER_DEVICE_ID = "EXTRA_OTHER_DEVICE_ID" + private const val EXTRA_IS_INCOMING = "EXTRA_IS_INCOMING" + + /* ========================================================================================== + * INPUT + * ========================================================================================== */ + + fun incomingIntent(context: Context, matrixID: String, otherUserId: String, transactionID: String): Intent { + val intent = Intent(context, SASVerificationActivity::class.java) + intent.putExtra(EXTRA_MATRIX_ID, matrixID) + intent.putExtra(EXTRA_TRANSACTION_ID, transactionID) + intent.putExtra(EXTRA_OTHER_USER_ID, otherUserId) + intent.putExtra(EXTRA_IS_INCOMING, true) + return intent + } + + fun outgoingIntent(context: Context, matrixID: String, otherUserId: String, otherDeviceId: String): Intent { + val intent = Intent(context, SASVerificationActivity::class.java) + intent.putExtra(EXTRA_MATRIX_ID, matrixID) + intent.putExtra(EXTRA_OTHER_DEVICE_ID, otherDeviceId) + intent.putExtra(EXTRA_OTHER_USER_ID, otherUserId) + intent.putExtra(EXTRA_IS_INCOMING, false) + return intent + } + + /* ========================================================================================== + * OUTPUT + * ========================================================================================== */ + + fun getOtherUserId(intent: Intent?): String? { + return intent?.getStringExtra(EXTRA_OTHER_USER_ID) + } + + fun getOtherDeviceId(intent: Intent?): String? { + return intent?.getStringExtra(EXTRA_OTHER_DEVICE_ID) + } + } + + override fun getTitleRes() = R.string.title_activity_verify_device + + + private lateinit var viewModel: SasVerificationViewModel + + override fun initUiAndData() { + super.initUiAndData() + viewModel = ViewModelProviders.of(this).get(SasVerificationViewModel::class.java) + val transactionID: String? = intent.getStringExtra(EXTRA_TRANSACTION_ID) + + if (isFirstCreation()) { + val isIncoming = intent.getBooleanExtra(EXTRA_IS_INCOMING, false) + if (isIncoming) { + //incoming always have a transaction id + viewModel.initIncoming(mSession, intent.getStringExtra(EXTRA_OTHER_USER_ID), transactionID) + } else { + viewModel.initOutgoing(mSession, intent.getStringExtra(EXTRA_OTHER_USER_ID), intent.getStringExtra(EXTRA_OTHER_DEVICE_ID)) + } + + if (isIncoming) { + val incoming = viewModel.transaction as IncomingSasVerificationTransaction + when (incoming.uxState) { + IncomingSasVerificationTransaction.UxState.UNKNOWN, + IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT, + IncomingSasVerificationTransaction.UxState.WAIT_FOR_KEY_AGREEMENT -> { + supportActionBar?.setTitle(R.string.sas_incoming_request_title) + supportFragmentManager.beginTransaction() + .setCustomAnimations(R.anim.no_anim, R.anim.exit_fade_out) + .replace(R.id.container, SASVerificationIncomingFragment.newInstance()) + .commitNow() + } + IncomingSasVerificationTransaction.UxState.WAIT_FOR_VERIFICATION, + IncomingSasVerificationTransaction.UxState.SHOW_SAS -> { + supportFragmentManager.beginTransaction() + .setCustomAnimations(R.anim.no_anim, R.anim.exit_fade_out) + .replace(R.id.container, SASVerificationShortCodeFragment.newInstance()) + .commitNow() + } + IncomingSasVerificationTransaction.UxState.VERIFIED -> { + supportFragmentManager.beginTransaction() + .setCustomAnimations(R.anim.no_anim, R.anim.exit_fade_out) + .replace(R.id.container, SASVerificationVerifiedFragment.newInstance()) + .commitNow() + } + IncomingSasVerificationTransaction.UxState.CANCELLED_BY_ME, + IncomingSasVerificationTransaction.UxState.CANCELLED_BY_OTHER -> { + viewModel.navigateCancel() + } + } + } else { + val outgoing = viewModel.transaction as? OutgoingSasVerificationRequest + //transaction can be null, as not yet created + when (outgoing?.uxState) { + null, + OutgoingSasVerificationRequest.UxState.UNKNOWN, + OutgoingSasVerificationRequest.UxState.WAIT_FOR_START, + OutgoingSasVerificationRequest.UxState.WAIT_FOR_KEY_AGREEMENT -> { + supportFragmentManager.beginTransaction() + .setCustomAnimations(R.anim.no_anim, R.anim.exit_fade_out) + .replace(R.id.container, SASVerificationStartFragment.newInstance()) + .commitNow() + } + OutgoingSasVerificationRequest.UxState.SHOW_SAS, + OutgoingSasVerificationRequest.UxState.WAIT_FOR_VERIFICATION -> { + supportFragmentManager.beginTransaction() + .setCustomAnimations(R.anim.no_anim, R.anim.exit_fade_out) + .replace(R.id.container, SASVerificationShortCodeFragment.newInstance()) + .commitNow() + } + OutgoingSasVerificationRequest.UxState.VERIFIED -> { + supportFragmentManager.beginTransaction() + .setCustomAnimations(R.anim.no_anim, R.anim.exit_fade_out) + .replace(R.id.container, SASVerificationVerifiedFragment.newInstance()) + .commitNow() + } + OutgoingSasVerificationRequest.UxState.CANCELLED_BY_ME, + OutgoingSasVerificationRequest.UxState.CANCELLED_BY_OTHER -> { + viewModel.navigateCancel() + } + } + } + } + + viewModel.navigateEvent.observe(this, Observer { uxStateEvent -> + when (uxStateEvent?.getContentIfNotHandled()) { + SasVerificationViewModel.NAVIGATE_FINISH -> { + finish() + } + SasVerificationViewModel.NAVIGATE_FINISH_SUCCESS -> { + val dataResult = Intent() + dataResult.putExtra(EXTRA_OTHER_DEVICE_ID, viewModel.otherDeviceId) + dataResult.putExtra(EXTRA_OTHER_USER_ID, viewModel.otherUserId) + setResult(Activity.RESULT_OK, dataResult) + finish() + } + SasVerificationViewModel.NAVIGATE_SAS_DISPLAY -> { + supportFragmentManager.beginTransaction() + .setCustomAnimations(R.anim.enter_from_right, R.anim.exit_fade_out) + .replace(R.id.container, SASVerificationShortCodeFragment.newInstance()) + .commitNow() + } + SasVerificationViewModel.NAVIGATE_SUCCESS -> { + supportFragmentManager.beginTransaction() + .setCustomAnimations(R.anim.enter_from_right, R.anim.exit_fade_out) + .replace(R.id.container, SASVerificationVerifiedFragment.newInstance()) + .commitNow() + } + SasVerificationViewModel.NAVIGATE_CANCELLED -> { + val isCancelledByMe = viewModel.transaction?.state == SasVerificationTxState.Cancelled + val humanReadableReason = when (viewModel.transaction?.cancelledReason) { + CancelCode.User -> getString(R.string.sas_error_m_user) + CancelCode.Timeout -> getString(R.string.sas_error_m_timeout) + CancelCode.UnknownTransaction -> getString(R.string.sas_error_m_unknown_transaction) + CancelCode.UnknownMethod -> getString(R.string.sas_error_m_unknown_method) + CancelCode.MismatchedCommitment -> getString(R.string.sas_error_m_mismatched_commitment) + CancelCode.MismatchedSas -> getString(R.string.sas_error_m_mismatched_sas) + CancelCode.UnexpectedMessage -> getString(R.string.sas_error_m_unexpected_message) + CancelCode.InvalidMessage -> getString(R.string.sas_error_m_invalid_message) + CancelCode.MismatchedKeys -> getString(R.string.sas_error_m_key_mismatch) + // Use user error + CancelCode.UserMismatchError -> getString(R.string.sas_error_m_user_error) + null -> getString(R.string.sas_error_unknown) + } + val message = + if (isCancelledByMe) getString(R.string.sas_cancelled_by_me, humanReadableReason) + else getString(R.string.sas_cancelled_by_other, humanReadableReason) + //Show a dialog + if (!this.isFinishing) { + AlertDialog.Builder(this) + .setTitle(R.string.sas_cancelled_dialog_title) + .setMessage(message) + .setCancelable(false) + .setPositiveButton(R.string.ok) { _, _ -> + //nop + finish() + } + .show() + } + } + } + }) + + viewModel.loadingLiveEvent.observe(this, Observer { + if (it == null) { + hideWaitingView() + } else { + val status = if (it == -1) "" else getString(it) + updateWaitingView(WaitingViewData(status, isIndeterminate = true)) + } + }) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == android.R.id.home) { + //we want to cancel the transaction + viewModel.cancelTransaction() + } + + return super.onOptionsItemSelected(item) + } + + + override fun onBackPressed() { + //we want to cancel the transaction + viewModel.cancelTransaction() + } +} diff --git a/vector/src/main/java/im/vector/riotredesign/features/crypto/verification/SASVerificationIncomingFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/crypto/verification/SASVerificationIncomingFragment.kt new file mode 100644 index 00000000..9b479eb9 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/crypto/verification/SASVerificationIncomingFragment.kt @@ -0,0 +1,99 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.riotredesign.features.crypto.verification + +import android.os.Bundle +import android.widget.ImageView +import android.widget.TextView +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProviders +import butterknife.BindView +import butterknife.OnClick +import im.vector.matrix.android.api.session.crypto.sas.IncomingSasVerificationTransaction +import im.vector.riotredesign.R +import im.vector.riotredesign.core.platform.VectorBaseFragment +import im.vector.riotredesign.features.home.AvatarRenderer + +class SASVerificationIncomingFragment : VectorBaseFragment() { + + companion object { + fun newInstance() = SASVerificationIncomingFragment() + } + + @BindView(R.id.sas_incoming_request_user_display_name) + lateinit var otherUserDisplayNameTextView: TextView + + @BindView(R.id.sas_incoming_request_user_id) + lateinit var otherUserIdTextView: TextView + + @BindView(R.id.sas_incoming_request_user_device) + lateinit var otherDeviceTextView: TextView + + @BindView(R.id.sas_incoming_request_user_avatar) + lateinit var avatarImageView: ImageView + + override fun getLayoutResId() = R.layout.fragment_sas_verification_incoming_request + + private lateinit var viewModel: SasVerificationViewModel + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + viewModel = activity?.run { + ViewModelProviders.of(this).get(SasVerificationViewModel::class.java) + } ?: throw Exception("Invalid Activity") + + otherUserDisplayNameTextView.text = viewModel.otherUser?.displayName ?: viewModel.otherUserId + otherUserIdTextView.text = viewModel.otherUserId + otherDeviceTextView.text = viewModel.otherDeviceId + + viewModel.otherUser?.let { + AvatarRenderer.render(it, avatarImageView) + } + + viewModel.transactionState.observe(this, Observer { + val uxState = (viewModel.transaction as? IncomingSasVerificationTransaction)?.uxState + when (uxState) { + IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT -> { + viewModel.loadingLiveEvent.value = null + } + IncomingSasVerificationTransaction.UxState.WAIT_FOR_KEY_AGREEMENT -> { + viewModel.loadingLiveEvent.value = R.string.sas_waiting_for_partner + } + IncomingSasVerificationTransaction.UxState.SHOW_SAS -> { + viewModel.shortCodeReady() + } + IncomingSasVerificationTransaction.UxState.CANCELLED_BY_ME, + IncomingSasVerificationTransaction.UxState.CANCELLED_BY_OTHER -> { + viewModel.loadingLiveEvent.value = null + viewModel.navigateCancel() + } + else -> Unit + } + }) + + } + + @OnClick(R.id.sas_request_continue_button) + fun didAccept() { + viewModel.acceptTransaction() + } + + @OnClick(R.id.sas_request_cancel_button) + fun didCancel() { + viewModel.cancelTransaction() + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/crypto/verification/SASVerificationShortCodeFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/crypto/verification/SASVerificationShortCodeFragment.kt new file mode 100644 index 00000000..b0363fae --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/crypto/verification/SASVerificationShortCodeFragment.kt @@ -0,0 +1,183 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.riotredesign.features.crypto.verification + +import android.os.Bundle +import android.view.ViewGroup +import android.widget.TextView +import androidx.core.view.isInvisible +import androidx.core.view.isVisible +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProviders +import butterknife.BindView +import butterknife.OnClick +import im.vector.matrix.android.api.session.crypto.sas.IncomingSasVerificationTransaction +import im.vector.matrix.android.api.session.crypto.sas.OutgoingSasVerificationRequest +import im.vector.riotredesign.R +import im.vector.riotredesign.core.platform.VectorBaseFragment + +class SASVerificationShortCodeFragment : VectorBaseFragment() { + + private lateinit var viewModel: SasVerificationViewModel + + companion object { + fun newInstance() = SASVerificationShortCodeFragment() + } + + + @BindView(R.id.sas_decimal_code) + lateinit var decimalTextView: TextView + + @BindView(R.id.sas_emoji_description) + lateinit var descriptionTextView: TextView + + @BindView(R.id.sas_emoji_grid) + lateinit var emojiGrid: ViewGroup + + + @BindView(R.id.emoji0) + lateinit var emoji0View: ViewGroup + @BindView(R.id.emoji1) + lateinit var emoji1View: ViewGroup + @BindView(R.id.emoji2) + lateinit var emoji2View: ViewGroup + @BindView(R.id.emoji3) + lateinit var emoji3View: ViewGroup + @BindView(R.id.emoji4) + lateinit var emoji4View: ViewGroup + @BindView(R.id.emoji5) + lateinit var emoji5View: ViewGroup + @BindView(R.id.emoji6) + lateinit var emoji6View: ViewGroup + + + override fun getLayoutResId() = R.layout.fragment_sas_verification_display_code + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + viewModel = activity?.run { + ViewModelProviders.of(this).get(SasVerificationViewModel::class.java) + } ?: throw Exception("Invalid Activity") + + + viewModel.transaction?.let { + if (it.supportsEmoji()) { + val emojicodes = it.getEmojiCodeRepresentation() + emojicodes.forEachIndexed { index, emojiRepresentation -> + when (index) { + 0 -> { + emoji0View.findViewById(R.id.item_emoji_tv).text = emojiRepresentation.emoji + emoji0View.findViewById(R.id.item_emoji_name_tv).setText(emojiRepresentation.nameResId) + } + 1 -> { + emoji1View.findViewById(R.id.item_emoji_tv).text = emojiRepresentation.emoji + emoji1View.findViewById(R.id.item_emoji_name_tv).setText(emojiRepresentation.nameResId) + } + 2 -> { + emoji2View.findViewById(R.id.item_emoji_tv).text = emojiRepresentation.emoji + emoji2View.findViewById(R.id.item_emoji_name_tv).setText(emojiRepresentation.nameResId) + } + 3 -> { + emoji3View.findViewById(R.id.item_emoji_tv).text = emojiRepresentation.emoji + emoji3View.findViewById(R.id.item_emoji_name_tv)?.setText(emojiRepresentation.nameResId) + } + 4 -> { + emoji4View.findViewById(R.id.item_emoji_tv).text = emojiRepresentation.emoji + emoji4View.findViewById(R.id.item_emoji_name_tv).setText(emojiRepresentation.nameResId) + } + 5 -> { + emoji5View.findViewById(R.id.item_emoji_tv).text = emojiRepresentation.emoji + emoji5View.findViewById(R.id.item_emoji_name_tv).setText(emojiRepresentation.nameResId) + } + 6 -> { + emoji6View.findViewById(R.id.item_emoji_tv).text = emojiRepresentation.emoji + emoji6View.findViewById(R.id.item_emoji_name_tv).setText(emojiRepresentation.nameResId) + } + } + } + } + + //decimal is at least supported + decimalTextView.text = it.getDecimalCodeRepresentation() + + + if (it.supportsEmoji()) { + descriptionTextView.text = getString(R.string.sas_emoji_description) + decimalTextView.isVisible = false + emojiGrid.isVisible = true + } else { + descriptionTextView.text = getString(R.string.sas_decimal_description) + decimalTextView.isVisible = true + emojiGrid.isInvisible = true + } + } + + + viewModel.transactionState.observe(this, Observer { + if (viewModel.transaction is IncomingSasVerificationTransaction) { + val uxState = (viewModel.transaction as IncomingSasVerificationTransaction).uxState + when (uxState) { + IncomingSasVerificationTransaction.UxState.SHOW_SAS -> { + viewModel.loadingLiveEvent.value = null + } + IncomingSasVerificationTransaction.UxState.VERIFIED -> { + viewModel.loadingLiveEvent.value = null + viewModel.deviceIsVerified() + } + IncomingSasVerificationTransaction.UxState.CANCELLED_BY_ME, + IncomingSasVerificationTransaction.UxState.CANCELLED_BY_OTHER -> { + viewModel.loadingLiveEvent.value = null + viewModel.navigateCancel() + } + else -> { + viewModel.loadingLiveEvent.value = R.string.sas_waiting_for_partner + } + } + + } else if (viewModel.transaction is OutgoingSasVerificationRequest) { + val uxState = (viewModel.transaction as OutgoingSasVerificationRequest).uxState + when (uxState) { + OutgoingSasVerificationRequest.UxState.SHOW_SAS -> { + viewModel.loadingLiveEvent.value = null + } + OutgoingSasVerificationRequest.UxState.VERIFIED -> { + viewModel.loadingLiveEvent.value = null + viewModel.deviceIsVerified() + } + OutgoingSasVerificationRequest.UxState.CANCELLED_BY_ME, + OutgoingSasVerificationRequest.UxState.CANCELLED_BY_OTHER -> { + viewModel.loadingLiveEvent.value = null + viewModel.navigateCancel() + } + else -> { + viewModel.loadingLiveEvent.value = R.string.sas_waiting_for_partner + } + } + } + }) + } + + + @OnClick(R.id.sas_request_continue_button) + fun didAccept() { + viewModel.confirmEmojiSame() + } + + @OnClick(R.id.sas_request_cancel_button) + fun didCancel() { + viewModel.cancelTransaction() + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/crypto/verification/SASVerificationStartFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/crypto/verification/SASVerificationStartFragment.kt new file mode 100644 index 00000000..e41d0c73 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/crypto/verification/SASVerificationStartFragment.kt @@ -0,0 +1,133 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.riotredesign.features.crypto.verification + +import android.os.Bundle +import android.view.ViewGroup +import android.widget.Button +import android.widget.ProgressBar +import android.widget.TextView +import androidx.core.view.isInvisible +import androidx.core.view.isVisible +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProviders +import androidx.transition.TransitionManager +import butterknife.BindView +import butterknife.OnClick +import im.vector.matrix.android.api.session.crypto.sas.OutgoingSasVerificationRequest +import im.vector.riotredesign.R +import im.vector.riotredesign.core.platform.VectorBaseActivity +import im.vector.riotredesign.core.platform.VectorBaseFragment + +class SASVerificationStartFragment : VectorBaseFragment() { + + companion object { + fun newInstance() = SASVerificationStartFragment() + } + + override fun getLayoutResId() = R.layout.fragment_sas_verification_start + + private lateinit var viewModel: SasVerificationViewModel + + + @BindView(R.id.rootLayout) + lateinit var rootLayout: ViewGroup + + @BindView(R.id.sas_start_button) + lateinit var startButton: Button + + @BindView(R.id.sas_start_button_loading) + lateinit var startButtonLoading: ProgressBar + + @BindView(R.id.sas_verifying_keys) + lateinit var loadingText: TextView + + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + viewModel = activity?.run { + ViewModelProviders.of(this).get(SasVerificationViewModel::class.java) + } ?: throw Exception("Invalid Activity") + + viewModel.transactionState.observe(this, Observer { + val uxState = (viewModel.transaction as? OutgoingSasVerificationRequest)?.uxState + when (uxState) { + OutgoingSasVerificationRequest.UxState.WAIT_FOR_KEY_AGREEMENT -> { + //display loading + TransitionManager.beginDelayedTransition(this.rootLayout) + this.loadingText.isVisible = true + this.startButton.isInvisible = true + this.startButtonLoading.isVisible = true + this.startButtonLoading.animate() + + } + OutgoingSasVerificationRequest.UxState.SHOW_SAS -> { + viewModel.shortCodeReady() + } + OutgoingSasVerificationRequest.UxState.CANCELLED_BY_ME, + OutgoingSasVerificationRequest.UxState.CANCELLED_BY_OTHER -> { + viewModel.navigateCancel() + } + else -> { + TransitionManager.beginDelayedTransition(this.rootLayout) + this.loadingText.isVisible = false + this.startButton.isVisible = true + this.startButtonLoading.isVisible = false + } + } + }) + + } + + @OnClick(R.id.sas_start_button) + fun doStart() { + viewModel.beginSasKeyVerification() + } + + @OnClick(R.id.sas_legacy_verification) + fun doLegacy() { + (requireActivity() as VectorBaseActivity).notImplemented() + + /* + viewModel.session.crypto?.getDeviceInfo(viewModel.otherUserId ?: "", viewModel.otherDeviceId + ?: "", object : SimpleApiCallback() { + override fun onSuccess(info: MXDeviceInfo?) { + info?.let { + + CommonActivityUtils.displayDeviceVerificationDialogLegacy(it, it.userId, viewModel.session, activity, object : YesNoListener { + override fun yes() { + viewModel.manuallyVerified() + } + + override fun no() { + + } + }) + } + } + }) + */ + } + + @OnClick(R.id.sas_cancel_button) + fun doCancel() { + // Transaction may be started, or not + viewModel.cancelTransaction() + } + + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/crypto/verification/SASVerificationVerifiedFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/crypto/verification/SASVerificationVerifiedFragment.kt new file mode 100644 index 00000000..a14dc3ce --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/crypto/verification/SASVerificationVerifiedFragment.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.riotredesign.features.crypto.verification + +import android.os.Bundle +import androidx.lifecycle.ViewModelProviders +import butterknife.OnClick +import im.vector.riotredesign.R +import im.vector.riotredesign.core.platform.VectorBaseFragment + +class SASVerificationVerifiedFragment : VectorBaseFragment() { + + override fun getLayoutResId() = R.layout.fragment_sas_verification_verified + + companion object { + fun newInstance() = SASVerificationVerifiedFragment() + } + + private lateinit var viewModel: SasVerificationViewModel + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + viewModel = activity?.run { + ViewModelProviders.of(this).get(SasVerificationViewModel::class.java) + } ?: throw Exception("Invalid Activity") + + } + + @OnClick(R.id.sas_verification_verified_done_button) + fun onDone() { + viewModel.finishSuccess() + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/crypto/verification/SasVerificationViewModel.kt b/vector/src/main/java/im/vector/riotredesign/features/crypto/verification/SasVerificationViewModel.kt new file mode 100644 index 00000000..7f06bba8 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/crypto/verification/SasVerificationViewModel.kt @@ -0,0 +1,160 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.riotredesign.features.crypto.verification + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.crypto.sas.IncomingSasVerificationTransaction +import im.vector.matrix.android.api.session.crypto.sas.SasVerificationService +import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTransaction +import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState +import im.vector.matrix.android.api.session.user.model.User +import im.vector.riotredesign.core.utils.LiveEvent + + +class SasVerificationViewModel : ViewModel(), + SasVerificationService.SasVerificationListener { + + + companion object { + const val NAVIGATE_FINISH = "NAVIGATE_FINISH" + const val NAVIGATE_FINISH_SUCCESS = "NAVIGATE_FINISH_SUCCESS" + const val NAVIGATE_SAS_DISPLAY = "NAVIGATE_SAS_DISPLAY" + const val NAVIGATE_SUCCESS = "NAVIGATE_SUCCESS" + const val NAVIGATE_CANCELLED = "NAVIGATE_CANCELLED" + } + + lateinit var sasVerificationService: SasVerificationService + + var otherUserId: String? = null + var otherDeviceId: String? = null + var otherUser: User? = null + var transaction: SasVerificationTransaction? = null + + + var transactionState: MutableLiveData = MutableLiveData() + + init { + //Force a first observe + transactionState.value = null + } + + private var _navigateEvent: MutableLiveData> = MutableLiveData() + val navigateEvent: LiveData> + get() = _navigateEvent + + + var loadingLiveEvent: MutableLiveData = MutableLiveData() + + var transactionID: String? = null + set(value) { + if (value != null) { + transaction = sasVerificationService.getExistingTransaction(otherUserId!!, value) + transactionState.value = transaction?.state + otherDeviceId = transaction?.otherDeviceId + } + field = value + } + + + fun initIncoming(session: Session, otherUserId: String, transactionID: String?) { + this.sasVerificationService = session.getSasVerificationService() + this.otherUserId = otherUserId + this.transactionID = transactionID + this.sasVerificationService.addListener(this) + this.otherUser = session.getUser(otherUserId) + if (transactionID == null || transaction == null) { + //sanity, this transaction is not known anymore + _navigateEvent.value = LiveEvent(NAVIGATE_FINISH) + } + } + + fun initOutgoing(session: Session, otherUserId: String, otherDeviceId: String) { + this.sasVerificationService = session.getSasVerificationService() + this.otherUserId = otherUserId + this.otherDeviceId = otherDeviceId + this.sasVerificationService.addListener(this) + this.otherUser = session.getUser(otherUserId) + } + + fun beginSasKeyVerification() { + val verificationSAS = sasVerificationService.beginKeyVerificationSAS(otherUserId!!, otherDeviceId!!) + this.transactionID = verificationSAS + } + + + override fun transactionCreated(tx: SasVerificationTransaction) { + + } + + override fun transactionUpdated(tx: SasVerificationTransaction) { + if (transactionID == tx.transactionId) { + transactionState.value = tx.state + } + } + + override fun markedAsManuallyVerified(userId: String, deviceId: String) { + + } + + fun cancelTransaction() { + transaction?.cancel() + _navigateEvent.value = LiveEvent(NAVIGATE_FINISH) + } + + fun finishSuccess() { + _navigateEvent.value = LiveEvent(NAVIGATE_FINISH_SUCCESS) + } + + fun manuallyVerified() { + if (otherUserId != null && otherDeviceId != null) { + sasVerificationService.markedLocallyAsManuallyVerified(otherUserId!!, otherDeviceId!!) + } + _navigateEvent.value = LiveEvent(NAVIGATE_FINISH_SUCCESS) + } + + + fun acceptTransaction() { + (transaction as? IncomingSasVerificationTransaction)?.performAccept() + } + + fun confirmEmojiSame() { + transaction?.userHasVerifiedShortCode() + } + + fun shortCodeReady() { + loadingLiveEvent.value = null + _navigateEvent.value = LiveEvent(NAVIGATE_SAS_DISPLAY) + } + + fun deviceIsVerified() { + loadingLiveEvent.value = null + _navigateEvent.value = LiveEvent(NAVIGATE_SUCCESS) + } + + fun navigateCancel() { + _navigateEvent.value = LiveEvent(NAVIGATE_CANCELLED) + } + + override fun onCleared() { + super.onCleared() + sasVerificationService.removeListener(this) + } + + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/AvatarRenderer.kt b/vector/src/main/java/im/vector/riotredesign/features/home/AvatarRenderer.kt index 012bdf83..f7305439 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/AvatarRenderer.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/AvatarRenderer.kt @@ -31,6 +31,7 @@ import im.vector.matrix.android.api.Matrix import im.vector.matrix.android.api.MatrixPatterns import im.vector.matrix.android.api.session.content.ContentUrlResolver import im.vector.matrix.android.api.session.room.model.RoomSummary +import im.vector.matrix.android.api.session.user.model.User import im.vector.riotredesign.R import im.vector.riotredesign.core.glide.GlideApp import im.vector.riotredesign.core.glide.GlideRequest @@ -55,6 +56,11 @@ object AvatarRenderer { render(roomSummary.avatarUrl, roomSummary.roomId, roomSummary.displayName, imageView) } + @UiThread + fun render(user: User, imageView: ImageView) { + render(imageView.context, GlideApp.with(imageView), user.avatarUrl, user.userId, user.displayName, DrawableImageViewTarget(imageView)) + } + @UiThread fun render(avatarUrl: String?, identifier: String, name: String?, imageView: ImageView) { render(imageView.context, GlideApp.with(imageView), avatarUrl, identifier, name, DrawableImageViewTarget(imageView)) diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt b/vector/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt index 98666053..cffd20f1 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/HomeModule.kt @@ -66,6 +66,8 @@ class HomeModule { roomMemberItemFactory = RoomMemberItemFactory(get()), roomHistoryVisibilityItemFactory = RoomHistoryVisibilityItemFactory(get()), callItemFactory = CallItemFactory(get()), + encryptionItemFactory = EncryptionItemFactory(get()), + encryptedItemFactory = EncryptedItemFactory(get(), get(), messageItemFactory), defaultItemFactory = DefaultItemFactory() ) TimelineEventController(timelineDateFormatter, timelineItemFactory, timelineMediaSizeProvider) diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt new file mode 100644 index 00000000..25bb8d74 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt @@ -0,0 +1,84 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotredesign.features.home.room.detail.timeline.factory + +import android.graphics.Typeface +import android.text.Spannable +import android.text.SpannableString +import android.text.style.StyleSpan +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.crypto.MXCryptoError +import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.api.session.room.timeline.TimelineEvent +import im.vector.matrix.android.internal.crypto.MXDecryptionException +import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult +import im.vector.riotredesign.R +import im.vector.riotredesign.core.epoxy.VectorEpoxyModel +import im.vector.riotredesign.core.resources.StringProvider +import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController +import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem_ + +class EncryptedItemFactory( + private val session: Session, + private val stringProvider: StringProvider, + private val messageItemFactory: MessageItemFactory) { + + fun create(timelineEvent: TimelineEvent, + nextEvent: TimelineEvent?, + callback: TimelineEventController.Callback?): VectorEpoxyModel<*>? { + + return when { + EventType.ENCRYPTED == timelineEvent.root.type -> { + val decrypted: MXEventDecryptionResult? + try { + decrypted = session.decryptEvent(timelineEvent.root, "TODO") + } catch (e: MXDecryptionException) { + val errorDescription = + if (e.cryptoError?.code == MXCryptoError.UNKNOWN_INBOUND_SESSION_ID_ERROR_CODE) { + stringProvider.getString(R.string.notice_crypto_error_unkwown_inbound_session_id) + } else { + e.localizedMessage + } + + val message = stringProvider.getString(R.string.notice_crypto_unable_to_decrypt, errorDescription) + + val spannableStr = SpannableString(message) + spannableStr.setSpan(StyleSpan(Typeface.ITALIC), 0, message.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + // TODO This is not correct format for error, change it + return NoticeItem_() + .noticeText(spannableStr) + .avatarUrl(timelineEvent.senderAvatar) + .memberName(timelineEvent.senderName) + } + + if (decrypted == null) { + return null + } + + if (decrypted.mClearEvent == null) { + return null + } + + val decryptedTimelineEvent = timelineEvent.copy(root = decrypted.mClearEvent!!) + + // Success + return messageItemFactory.create(decryptedTimelineEvent, nextEvent, callback) + } + else -> null + } + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt new file mode 100644 index 00000000..55ab6527 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotredesign.features.home.room.detail.timeline.factory + +import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.room.timeline.TimelineEvent +import im.vector.matrix.android.internal.crypto.model.event.EncryptionEventContent +import im.vector.riotredesign.R +import im.vector.riotredesign.core.resources.StringProvider +import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem +import im.vector.riotredesign.features.home.room.detail.timeline.item.NoticeItem_ + +class EncryptionItemFactory(private val stringProvider: StringProvider) { + + fun create(event: TimelineEvent): NoticeItem? { + val text = buildNoticeText(event.root, event.senderName) ?: return null + return NoticeItem_() + .noticeText(text) + .avatarUrl(event.senderAvatar) + .memberName(event.senderName) + } + + private fun buildNoticeText(event: Event, senderName: String?): CharSequence? { + return when { + EventType.ENCRYPTION == event.type -> { + val content = event.content.toModel() ?: return null + stringProvider.getString(R.string.notice_end_to_end, senderName, content.algorithm) + } + else -> null + } + + } + + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/TimelineItemFactory.kt index d5354434..21759e20 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/TimelineItemFactory.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/TimelineItemFactory.kt @@ -28,6 +28,8 @@ class TimelineItemFactory(private val messageItemFactory: MessageItemFactory, private val roomMemberItemFactory: RoomMemberItemFactory, private val roomHistoryVisibilityItemFactory: RoomHistoryVisibilityItemFactory, private val callItemFactory: CallItemFactory, + private val encryptionItemFactory: EncryptionItemFactory, + private val encryptedItemFactory: EncryptedItemFactory, private val defaultItemFactory: DefaultItemFactory) { fun create(event: TimelineEvent, @@ -46,8 +48,10 @@ class TimelineItemFactory(private val messageItemFactory: MessageItemFactory, EventType.CALL_HANGUP, EventType.CALL_ANSWER -> callItemFactory.create(event) - EventType.ENCRYPTED, - EventType.ENCRYPTION, + EventType.ENCRYPTION -> encryptionItemFactory.create(event) + + EventType.ENCRYPTED -> encryptedItemFactory.create(event, nextEvent, callback) + EventType.STATE_ROOM_THIRD_PARTY_INVITE, EventType.STICKER, EventType.STATE_ROOM_CREATE -> defaultItemFactory.create(event) diff --git a/vector/src/main/java/im/vector/riotredesign/features/lifecycle/VectorActivityLifecycleCallbacks.kt b/vector/src/main/java/im/vector/riotredesign/features/lifecycle/VectorActivityLifecycleCallbacks.kt new file mode 100644 index 00000000..9a2a5e1e --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/lifecycle/VectorActivityLifecycleCallbacks.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotredesign.features.lifecycle + +import android.app.Activity +import android.app.Application +import android.os.Bundle +import im.vector.riotredesign.features.popup.PopupAlertManager + +class VectorActivityLifecycleCallbacks : Application.ActivityLifecycleCallbacks { + override fun onActivityPaused(activity: Activity) { + } + + override fun onActivityResumed(activity: Activity) { + PopupAlertManager.onNewActivityDisplayed(activity) + } + + override fun onActivityStarted(activity: Activity) { + } + + override fun onActivityDestroyed(activity: Activity) { + } + + override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) { + } + + override fun onActivityStopped(activity: Activity) { + } + + override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { + } + +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/notifications/NotificationBroadcastReceiver.kt b/vector/src/main/java/im/vector/riotredesign/features/notifications/NotificationBroadcastReceiver.kt index d7f7aadd..b3464b52 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/notifications/NotificationBroadcastReceiver.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/notifications/NotificationBroadcastReceiver.kt @@ -61,7 +61,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver(), KoinComponent { Matrix.getInstance(context)?.defaultSession?.let { session -> session.dataHandler ?.getRoom(roomId) - ?.markAllAsRead(object : SimpleApiCallback() { + ?.markAllAsRead(object : SimpleApiCallback() { override fun onSuccess(void: Void?) { // Ignore } @@ -100,7 +100,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver(), KoinComponent { val event = Event(mxMessage, session.credentials.userId, roomId) room.storeOutgoingEvent(event) - room.sendEvent(event, object : ApiCallback { + room.sendEvent(event, object : MatrixCallback { override fun onSuccess(info: Void?) { Timber.d("Send message : onSuccess ") val notifiableMessageEvent = NotifiableMessageEvent( diff --git a/vector/src/main/java/im/vector/riotredesign/features/popup/PopupAlertManager.kt b/vector/src/main/java/im/vector/riotredesign/features/popup/PopupAlertManager.kt new file mode 100644 index 00000000..f658b496 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/popup/PopupAlertManager.kt @@ -0,0 +1,229 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.riotredesign.features.popup + +import android.app.Activity +import android.os.Handler +import android.os.Looper +import android.view.View +import androidx.annotation.ColorRes +import androidx.annotation.DrawableRes +import com.tapadoo.alerter.Alerter +import com.tapadoo.alerter.OnHideAlertListener +import im.vector.riotredesign.R +import im.vector.riotredesign.features.crypto.verification.SASVerificationActivity +import timber.log.Timber +import java.lang.ref.WeakReference + +/** + * Responsible of displaying important popup alerts on top of the screen. + * Alerts are stacked and will be displayed sequentially + */ +object PopupAlertManager { + + private var weakCurrentActivity: WeakReference? = null + private var currentAlerter: VectorAlert? = null + + private val alertFiFo = ArrayList() + + fun postVectorAlert(alert: VectorAlert) { + synchronized(alertFiFo) { + alertFiFo.add(alert) + } + displayNextIfPossible() + } + + fun cancelAlert(uid: String) { + synchronized(alertFiFo) { + alertFiFo.listIterator().apply { + while (this.hasNext()) { + val next = this.next() + if (next.uid == uid) { + this.remove() + } + } + } + } + + //it could also be the current one + if (currentAlerter?.uid == uid) { + Alerter.hide() + currentIsDismissed() + } + } + + + fun onNewActivityDisplayed(activity: Activity) { + //we want to remove existing popup on previous activity and display it on new one + if (currentAlerter != null) { + weakCurrentActivity?.get()?.let { + Alerter.clearCurrent(it) + } + } + + if (shouldIgnoreActivity(activity)) { + return + } + + weakCurrentActivity = WeakReference(activity) + + if (currentAlerter != null) { + if (currentAlerter!!.expirationTimestamp != null && System.currentTimeMillis() > currentAlerter!!.expirationTimestamp!!) { + //this alert has expired, remove it + //perform dismiss + try { + currentAlerter?.dismissedAction?.run() + } catch (e: Exception) { + Timber.e("## failed to perform action") + } + currentAlerter = null + Handler(Looper.getMainLooper()).postDelayed({ + displayNextIfPossible() + }, 2000) + } else { + showAlert(currentAlerter!!, activity, animate = false) + } + } else { + Handler(Looper.getMainLooper()).postDelayed({ + displayNextIfPossible() + }, 2000) + } + } + + private fun shouldIgnoreActivity(activity: Activity) = activity is SASVerificationActivity + + + private fun displayNextIfPossible() { + val currentActivity = weakCurrentActivity?.get() + if (Alerter.isShowing || currentActivity == null) { + //will retry later + return + } + val next: VectorAlert? + synchronized(alertFiFo) { + next = alertFiFo.firstOrNull() + if (next != null) alertFiFo.remove(next) + } + currentAlerter = next + next?.let { + val currentTime = System.currentTimeMillis() + if (next.expirationTimestamp != null && currentTime > next.expirationTimestamp!!) { + //skip + try { + next.dismissedAction?.run() + } catch (e: java.lang.Exception) { + Timber.e("## failed to perform action") + } + displayNextIfPossible() + } else { + showAlert(it, currentActivity) + } + } + } + + private fun showAlert(alert: VectorAlert, activity: Activity, animate: Boolean = true) { + alert.weakCurrentActivity = WeakReference(activity) + Alerter.create(activity) + .setTitle(alert.title) + .setText(alert.description) + .apply { + + if (!animate) { + setEnterAnimation(R.anim.anim_alerter_no_anim) + } + + alert.iconId?.let { + setIcon(it) + } + alert.actions.forEach { action -> + addButton(action.title, R.style.AlerterButton, View.OnClickListener { + if (action.autoClose) { + currentIsDismissed() + Alerter.hide() + } + try { + action.action.run() + } catch (e: java.lang.Exception) { + Timber.e("## failed to perform action") + } + + }) + } + setOnClickListener(View.OnClickListener { _ -> + alert.contentAction?.let { + currentIsDismissed() + Alerter.hide() + try { + it.run() + } catch (e: java.lang.Exception) { + Timber.e("## failed to perform action") + } + } + }) + + } + .setOnHideListener(OnHideAlertListener { + //called when dismissed on swipe + try { + alert.dismissedAction?.run() + } catch (e: java.lang.Exception) { + Timber.e("## failed to perform action") + } + currentIsDismissed() + }) + .enableSwipeToDismiss() + .enableInfiniteDuration(true) + .setBackgroundColorRes(alert.colorRes ?: R.color.notification_accent_color) + .show() + } + + private fun currentIsDismissed() { + //current alert has been hidden + currentAlerter = null + Handler(Looper.getMainLooper()).postDelayed({ + displayNextIfPossible() + }, 500) + } + + /** + * Dataclass to describe an important alert with actions. + */ + data class VectorAlert(val uid: String, + val title: String, + val description: String, + @DrawableRes val iconId: Int?) { + + data class Button(val title: String, val action: Runnable, val autoClose: Boolean) + + //will be set by manager, and accessible by actions at runtime + var weakCurrentActivity: WeakReference? = null + + val actions = ArrayList