mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2024-11-24 18:36:21 +03:00
Crypto
This commit is contained in:
parent
6aae943e77
commit
1436667e7d
303 changed files with 25671 additions and 506 deletions
|
@ -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'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
Binary file not shown.
|
@ -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<DatedObject> { datedObject1, datedObject2 ->
|
||||
(datedObject1.date - datedObject2.date).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Comparator to sort DatedObjects from the latest to the oldest.
|
||||
*/
|
||||
val descComparator by lazy {
|
||||
Comparator<DatedObject> { datedObject1, datedObject2 ->
|
||||
(datedObject2.date - datedObject1.date).toInt()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<DeviceInfo>.sortByLastSeen() {
|
||||
Collections.sort(this, DatedObjectComparators.descComparator)
|
||||
}
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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<Unit>)
|
||||
|
||||
fun deleteDevice(deviceId: String, accountPassword: String, callback: MatrixCallback<Unit>)
|
||||
|
||||
fun getCryptoVersion(context: Context, longFormat: Boolean): String
|
||||
|
||||
fun isCryptoEnabled(): Boolean
|
||||
|
||||
fun getSasVerificationService(): SasVerificationService
|
||||
|
||||
fun getKeysBackupService(): KeysBackupService
|
||||
|
||||
fun isRoomBlacklistUnverifiedDevices(roomId: String, callback: MatrixCallback<Boolean>?)
|
||||
|
||||
fun setWarnOnUnknownDevices(warn: Boolean)
|
||||
|
||||
fun setDeviceVerification(verificationStatus: Int, deviceId: String, userId: String, callback: MatrixCallback<Unit>)
|
||||
|
||||
fun getUserDevices(userId: String): MutableList<MXDeviceInfo>
|
||||
|
||||
fun setDevicesKnown(devices: List<MXDeviceInfo>, callback: MatrixCallback<Unit>?)
|
||||
|
||||
fun deviceWithIdentityKey(senderKey: String, algorithm: String): MXDeviceInfo?
|
||||
|
||||
fun getMyDevice(): MXDeviceInfo
|
||||
|
||||
fun getGlobalBlacklistUnverifiedDevices(callback: MatrixCallback<Boolean>?)
|
||||
|
||||
fun setGlobalBlacklistUnverifiedDevices(block: Boolean, callback: MatrixCallback<Unit>?)
|
||||
|
||||
fun setRoomUnBlacklistUnverifiedDevices(roomId: String, callback: MatrixCallback<Unit>)
|
||||
|
||||
fun getDeviceTrackingStatus(userId: String): Int
|
||||
|
||||
fun importRoomKeys(roomKeysAsArray: ByteArray, password: String, progressListener: ProgressListener?, callback: MatrixCallback<ImportRoomKeysResult>)
|
||||
|
||||
fun exportRoomKeys(password: String, callback: MatrixCallback<ByteArray>)
|
||||
|
||||
fun setRoomBlacklistUnverifiedDevices(roomId: String, callback: MatrixCallback<Unit>)
|
||||
|
||||
fun getDeviceInfo(userId: String, deviceId: String?, callback: MatrixCallback<MXDeviceInfo?>)
|
||||
|
||||
// TODO move elsewhere
|
||||
fun reRequestRoomKeyForEvent(event: Event)
|
||||
|
||||
fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody)
|
||||
|
||||
fun addRoomKeysRequestListener(listener: RoomKeysRequestListener)
|
||||
|
||||
fun getDevicesList(callback: MatrixCallback<DevicesListResponse>)
|
||||
|
||||
fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int
|
||||
|
||||
/*
|
||||
fun start(isInitialSync: Boolean, aCallback: MatrixCallback<Unit>?)
|
||||
|
||||
fun isStarted(): Boolean
|
||||
|
||||
fun isStarting(): Boolean
|
||||
|
||||
fun close()
|
||||
|
||||
fun encryptEventContent(eventContent: Content,
|
||||
eventType: String,
|
||||
room: Room,
|
||||
callback: MatrixCallback<MXEncryptEventContentResult>)
|
||||
|
||||
fun onToDeviceEvent(event: Event)
|
||||
|
||||
fun onSyncCompleted(syncResponse: SyncResponse, fromToken: String?, isCatchingUp: Boolean)
|
||||
|
||||
fun getOlmDevice(): MXOlmDevice?
|
||||
|
||||
fun checkUnknownDevices(userIds: List<String>, callback: MatrixCallback<Unit>)
|
||||
|
||||
fun warnOnUnknownDevices(): Boolean
|
||||
|
||||
@Throws(MXDecryptionException::class)
|
||||
fun decryptEvent(event: Event, timelineId: String?): MXEventDecryptionResult?
|
||||
|
||||
fun resetReplayAttackCheckInTimeline(timelineId: String)
|
||||
|
||||
|
||||
@VisibleForTesting
|
||||
fun ensureOlmSessionsForUsers(users: List<String>, callback: MatrixCallback<MXUsersDevicesMap<MXOlmSessionResult>>)
|
||||
*/
|
||||
|
||||
fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult?
|
||||
}
|
|
@ -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."
|
||||
}
|
||||
}
|
|
@ -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<KeysVersionResult?>)
|
||||
fun createKeysBackupVersion(keysBackupCreationInfo: MegolmBackupCreationInfo, callback: MatrixCallback<KeysVersion>)
|
||||
fun getTotalNumbersOfKeys(): Int
|
||||
fun getTotalNumbersOfBackedUpKeys(): Int
|
||||
fun backupAllGroupSessions(progressListener: ProgressListener?, callback: MatrixCallback<Unit>?)
|
||||
fun getKeysBackupTrust(keysBackupVersion: KeysVersionResult, callback: MatrixCallback<KeysBackupVersionTrust>)
|
||||
fun getBackupProgress(progressListener: ProgressListener)
|
||||
fun maybeBackupKeys()
|
||||
fun getVersion(version: String, callback: MatrixCallback<KeysVersionResult?>)
|
||||
fun forceUsingLastVersion(callback: MatrixCallback<Boolean>)
|
||||
fun checkAndStartKeysBackup()
|
||||
fun addListener(listener: KeysBackupStateListener)
|
||||
fun removeListener(listener: KeysBackupStateListener)
|
||||
|
||||
fun prepareKeysBackupVersion(password: String?, progressListener: ProgressListener?, callback: MatrixCallback<MegolmBackupCreationInfo>)
|
||||
fun deleteBackup(version: String, callback: MatrixCallback<Unit>?)
|
||||
fun canRestoreKeys(): Boolean
|
||||
fun trustKeysBackupVersion(keysBackupVersion: KeysVersionResult, trust: Boolean, callback: MatrixCallback<Unit>)
|
||||
fun trustKeysBackupVersionWithRecoveryKey(keysBackupVersion: KeysVersionResult, recoveryKey: String, callback: MatrixCallback<Unit>)
|
||||
fun trustKeysBackupVersionWithPassphrase(keysBackupVersion: KeysVersionResult, password: String, callback: MatrixCallback<Unit>)
|
||||
fun restoreKeysWithRecoveryKey(keysVersionResult: KeysVersionResult, recoveryKey: String, roomId: String?, sessionId: String?, stepProgressListener: StepProgressListener?, callback: MatrixCallback<ImportRoomKeysResult>)
|
||||
fun restoreKeyBackupWithPassword(keysBackupVersion: KeysVersionResult, password: String, roomId: String?, sessionId: String?, stepProgressListener: StepProgressListener?, callback: MatrixCallback<ImportRoomKeysResult>)
|
||||
|
||||
val mKeysBackupVersion: KeysVersionResult?
|
||||
val currentBackupVersion: String?
|
||||
val isEnabled: Boolean
|
||||
val isStucked: Boolean
|
||||
val state: KeysBackupState
|
||||
|
||||
interface KeysBackupStateListener {
|
||||
fun onStateChange(newState: KeysBackupState)
|
||||
}
|
||||
|
||||
}
|
|
@ -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.
|
||||
*
|
||||
* <pre>
|
||||
* |
|
||||
* 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 |
|
||||
* +<---------------+
|
||||
* </pre>
|
||||
*/
|
||||
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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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<EmojiRepresentation>
|
||||
|
||||
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()
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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<String, @JvmSuppressWildcards Any>
|
||||
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<String> = 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<String, String> {
|
||||
val res = HashMap<String, String>()
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -54,4 +54,18 @@ interface RoomMembersService {
|
|||
*/
|
||||
fun invite(userId: String, callback: MatrixCallback<Unit>)
|
||||
|
||||
/**
|
||||
* 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<String>
|
||||
|
||||
/**
|
||||
* Return all the roomMembers ids which are joined to the room
|
||||
*
|
||||
* @return a roomMember id list of joined members.
|
||||
*/
|
||||
fun getJoinedRoomMemberIds(): List<String>
|
||||
|
||||
}
|
|
@ -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<String, @JvmSuppressWildcards Any>
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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"
|
File diff suppressed because it is too large
Load diff
|
@ -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<OlmManager>()
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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<String>()
|
||||
|
||||
// HS not ready for retry
|
||||
private val mNotReadyToRetryHS = HashSet<String>()
|
||||
|
||||
// indexed by UserId
|
||||
private val mPendingDownloadKeysRequestToken = HashMap<String, String>()
|
||||
|
||||
// pending queues list
|
||||
private val mDownloadKeysQueues = ArrayList<DownloadKeysPromise>()
|
||||
|
||||
// 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<String>,
|
||||
val mCallback: MatrixCallback<MXUsersDevicesMap<MXDeviceInfo>>?) {
|
||||
// list of remain pending device keys
|
||||
val mPendingUserIdsList: MutableList<String>
|
||||
|
||||
// the unfiltered user ids list
|
||||
val mUserIdsList: List<String>
|
||||
|
||||
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<String>?, callback: MatrixCallback<MXUsersDevicesMap<MXDeviceInfo>>?): MutableList<String>? {
|
||||
if (null != userIds) {
|
||||
val filteredUserIds = ArrayList<String>()
|
||||
val invalidUserIds = ArrayList<String>()
|
||||
|
||||
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<String>?) {
|
||||
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<String>?, left: List<String>?) {
|
||||
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<String>?) {
|
||||
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<String>?, failures: Map<String, Map<String, Any>>?) {
|
||||
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<DownloadKeysPromise>()
|
||||
|
||||
for (promise in mDownloadKeysQueues) {
|
||||
promise.mPendingUserIdsList.removeAll(userIds)
|
||||
|
||||
if (promise.mPendingUserIdsList.size == 0) {
|
||||
// private members
|
||||
val usersDevicesInfoMap = MXUsersDevicesMap<MXDeviceInfo>()
|
||||
|
||||
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<String>?, forceDownload: Boolean, callback: MatrixCallback<MXUsersDevicesMap<MXDeviceInfo>>?) {
|
||||
Timber.d("## downloadKeys() : forceDownload $forceDownload : $userIds")
|
||||
|
||||
// Map from userid -> deviceid -> DeviceInfo
|
||||
val stored = MXUsersDevicesMap<MXDeviceInfo>()
|
||||
|
||||
// List of user ids we need to download keys for
|
||||
val downloadUsers = ArrayList<String>()
|
||||
|
||||
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<MXUsersDevicesMap<MXDeviceInfo>> {
|
||||
override fun onSuccess(data: MXUsersDevicesMap<MXDeviceInfo>) {
|
||||
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<String>, callback: MatrixCallback<MXUsersDevicesMap<MXDeviceInfo>>?) {
|
||||
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<KeysQueryResponse> {
|
||||
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<String>()
|
||||
|
||||
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<MXUsersDevicesMap<MXDeviceInfo>> {
|
||||
override fun onSuccess(data: MXUsersDevicesMap<MXDeviceInfo>) {
|
||||
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
|
||||
* <pre>
|
||||
*
|
||||
* |
|
||||
* 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 ------------------------+
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
|
||||
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
|
||||
}
|
||||
}
|
|
@ -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<RoomKeyShareRequest>()!!
|
||||
mDeviceId = roomKeyShareRequest.requestingDeviceId
|
||||
mRequestId = roomKeyShareRequest.requestId
|
||||
mRequestBody = if (null != roomKeyShareRequest.body) roomKeyShareRequest.body else RoomKeyRequestBody()
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for object creation from crypto store
|
||||
*/
|
||||
constructor()
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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<IncomingRoomKeyRequest>()
|
||||
private val mReceivedRoomKeyRequestCancellations = ArrayList<IncomingRoomKeyRequestCancellation>()
|
||||
|
||||
// the listeners
|
||||
val mRoomKeysRequestListeners: MutableSet<RoomKeysRequestListener> = HashSet<RoomKeysRequestListener>()
|
||||
|
||||
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<RoomKeyShare>()!!
|
||||
|
||||
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<IncomingRoomKeyRequest>? = 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<IncomingRoomKeyRequestCancellation>? = 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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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<String, Class<IMXEncrypting>>
|
||||
|
||||
// decryptors map
|
||||
private val mDecryptors: MutableMap<String, Class<IMXDecrypting>>
|
||||
|
||||
init {
|
||||
mEncryptors = HashMap()
|
||||
try {
|
||||
mEncryptors[MXCRYPTO_ALGORITHM_MEGOLM] = Class.forName("im.vector.matrix.android.internal.crypto.algorithms.megolm.MXMegolmEncryption") as Class<IMXEncrypting>
|
||||
} 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<IMXEncrypting>
|
||||
} 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<IMXDecrypting>
|
||||
} 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<IMXDecrypting>
|
||||
} 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<IMXEncrypting>? {
|
||||
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<IMXDecrypting>? {
|
||||
return if (!TextUtils.isEmpty(algorithm)) {
|
||||
mDecryptors[algorithm]
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The list of registered algorithms.
|
||||
*/
|
||||
fun supportedAlgorithms(): List<String> {
|
||||
return ArrayList(mEncryptors.keys)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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<String> = ArrayList()
|
||||
)
|
|
@ -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
|
||||
*/
|
|
@ -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<String, OlmOutboundGroupSession>
|
||||
|
||||
// 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 "<senderKey>|<session_id>|<message_index>"
|
||||
// Values are true.
|
||||
private val mInboundGroupSessionMessageIndexes: MutableMap<String, MutableMap<String, Boolean>>
|
||||
|
||||
/**
|
||||
* 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<String, Map<String, String>>? {
|
||||
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<String, String>? {
|
||||
|
||||
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<String, String>()
|
||||
|
||||
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<String>? {
|
||||
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<String, Any>? {
|
||||
var res: MutableMap<String, Any>? = 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<String>,
|
||||
keysClaimed: Map<String, String>,
|
||||
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<MegolmSessionData>): List<MXOlmInboundGroupSession2> {
|
||||
val sessions = ArrayList<MXOlmInboundGroupSession2>(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<String, Any>, signature: String) {
|
||||
// Check signature on the canonical version of the JSON
|
||||
mOlmUtility!!.verifyEd25519Signature(signature, key, MoshiProvider.getCanonicalJson<Map<*, *>>(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)
|
||||
}
|
||||
}
|
|
@ -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<Map<String, String>>) {
|
||||
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<OutgoingRoomKeyRequest.RequestState>(Arrays.asList<OutgoingRoomKeyRequest.RequestState>(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<Unit> {
|
||||
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<Unit> {
|
||||
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<Map<String, String>>,
|
||||
transactionId: String?,
|
||||
callback: MatrixCallback<Unit>) {
|
||||
val contentMap = MXUsersDevicesMap<Any>()
|
||||
|
||||
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
|
||||
}
|
||||
}
|
|
@ -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<String, String>? = 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<String>? = null
|
||||
) {
|
||||
|
||||
fun toJsonString(): String {
|
||||
return MoshiProvider.providesMoshi().adapter(MegolmSessionData::class.java).toJson(this)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Map<String, String>>, // 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:
|
||||
* <pre>
|
||||
*
|
||||
* |
|
||||
* 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) <---------------------------+----------------+
|
||||
* </pre>
|
||||
*/
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String, MutableMap<String /* algorithm */, IMXDecrypting>>/* 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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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<String>, callback: MatrixCallback<Content>)
|
||||
}
|
|
@ -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<String, String>? = 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<String>? = null
|
||||
)
|
|
@ -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<String, MutableMap<String /* timelineId */, MutableList<Event>>>? = 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<EncryptedEventContent>()!!
|
||||
|
||||
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<EncryptedEventContent>()!!
|
||||
|
||||
val recipients = ArrayList<Map<String, String>>()
|
||||
|
||||
val selfMap = HashMap<String, String>()
|
||||
selfMap["userId"] = mCredentials.userId
|
||||
selfMap["deviceId"] = "*"
|
||||
recipients.add(selfMap)
|
||||
|
||||
if (!TextUtils.equals(sender, mCredentials.userId)) {
|
||||
val senderMap = HashMap<String, String>()
|
||||
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<EncryptedEventContent>()!!
|
||||
|
||||
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<Event>())
|
||||
}
|
||||
|
||||
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<RoomKeyContent>()!!
|
||||
|
||||
var senderKey: String? = event.getSenderKey()
|
||||
var keysClaimed: MutableMap<String, String> = HashMap()
|
||||
var forwarding_curve25519_key_chain: MutableList<String>? = 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<ForwardedRoomKeyContent>()!!
|
||||
|
||||
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<MXUsersDevicesMap<MXDeviceInfo>> {
|
||||
override fun onSuccess(data: MXUsersDevicesMap<MXDeviceInfo>) {
|
||||
val deviceId = request.mDeviceId
|
||||
val deviceInfo = mCryptoStore.getUserDevice(deviceId!!, userId)
|
||||
|
||||
if (null != deviceInfo) {
|
||||
val body = request.mRequestBody
|
||||
|
||||
val devicesByUser = HashMap<String, List<MXDeviceInfo>>()
|
||||
devicesByUser[userId] = ArrayList(Arrays.asList(deviceInfo))
|
||||
|
||||
mCrypto.ensureOlmSessionsForDevices(devicesByUser, object : MatrixCallback<MXUsersDevicesMap<MXOlmSessionResult>> {
|
||||
override fun onSuccess(map: MXUsersDevicesMap<MXOlmSessionResult>) {
|
||||
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<String, Any>()
|
||||
payloadJson["type"] = EventType.FORWARDED_ROOM_KEY
|
||||
payloadJson["content"] = inboundGroupSession!!.exportKeys()!!
|
||||
|
||||
val encodedPayload = mCrypto.encryptMessage(payloadJson, Arrays.asList(deviceInfo))
|
||||
val sendToDeviceMap = MXUsersDevicesMap<Any>()
|
||||
sendToDeviceMap.setObject(encodedPayload, userId, deviceId)
|
||||
|
||||
Timber.d("## shareKeysWithDevice() : sending to $userId:$deviceId")
|
||||
mSendToDeviceTask.configureWith(SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap))
|
||||
.dispatchTo(object : MatrixCallback<Unit> {
|
||||
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")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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<MXQueuedEncryption>()
|
||||
|
||||
// Session rotation periods
|
||||
private var mSessionRotationPeriodMsgs: Int = 0
|
||||
private var mSessionRotationPeriodMs: Int = 0
|
||||
|
||||
/**
|
||||
* @return a snapshot of the pending encryptions
|
||||
*/
|
||||
private val pendingEncryptions: List<MXQueuedEncryption>
|
||||
get() {
|
||||
val list = ArrayList<MXQueuedEncryption>()
|
||||
|
||||
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<String>,
|
||||
callback: MatrixCallback<Content>) {
|
||||
// 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<MXUsersDevicesMap<MXDeviceInfo>> {
|
||||
|
||||
/**
|
||||
* 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<MXDeviceInfo>) {
|
||||
ensureOutboundSession(devicesInRoom, object : MatrixCallback<MXOutboundSessionInfo> {
|
||||
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<String, String>()
|
||||
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<MXDeviceInfo>, callback: MatrixCallback<MXOutboundSessionInfo>?) {
|
||||
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<String, MutableList<MXDeviceInfo>>()/* 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<Unit> {
|
||||
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<String, MutableList<MXDeviceInfo>>,
|
||||
callback: MatrixCallback<Unit>?) {
|
||||
// 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<String, List<MXDeviceInfo>>()
|
||||
|
||||
val userIds = ArrayList<String>()
|
||||
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<Unit> {
|
||||
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<String, List<MXDeviceInfo>>,
|
||||
callback: MatrixCallback<Unit>?) {
|
||||
val sessionKey = olmDevice.getSessionKey(session.mSessionId)
|
||||
val chainIndex = olmDevice.getMessageIndex(session.mSessionId)
|
||||
|
||||
val submap = HashMap<String, Any>()
|
||||
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<String, Any>()
|
||||
payload["type"] = EventType.ROOM_KEY
|
||||
payload["content"] = submap
|
||||
|
||||
val t0 = System.currentTimeMillis()
|
||||
Timber.d("## shareUserDevicesKey() : starts")
|
||||
|
||||
mCrypto!!.ensureOlmSessionsForDevices(devicesByUser, object : MatrixCallback<MXUsersDevicesMap<MXOlmSessionResult>> {
|
||||
override fun onSuccess(results: MXUsersDevicesMap<MXOlmSessionResult>) {
|
||||
mCrypto!!.encryptingThreadHandler.post {
|
||||
Timber.d("## shareUserDevicesKey() : ensureOlmSessionsForDevices succeeds after "
|
||||
+ (System.currentTimeMillis() - t0) + " ms")
|
||||
val contentMap = MXUsersDevicesMap<Any>()
|
||||
|
||||
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<Unit> {
|
||||
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<String, Any>()
|
||||
|
||||
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<String, Any>()
|
||||
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<String>, callback: MatrixCallback<MXUsersDevicesMap<MXDeviceInfo>>) {
|
||||
// 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<MXUsersDevicesMap<MXDeviceInfo>> {
|
||||
override fun onSuccess(devices: MXUsersDevicesMap<MXDeviceInfo>) {
|
||||
mCrypto!!.encryptingThreadHandler.post {
|
||||
val encryptToVerifiedDevicesOnly = mCrypto!!.globalBlacklistUnverifiedDevices || mCrypto!!.isRoomBlacklistUnverifiedDevices(mRoomId)
|
||||
|
||||
val devicesInRoom = MXUsersDevicesMap<MXDeviceInfo>()
|
||||
val unknownDevices = MXUsersDevicesMap<MXDeviceInfo>()
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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<Int> = 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<MXDeviceInfo>): 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
|
||||
}
|
||||
}
|
|
@ -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<OlmEventContent>()!!
|
||||
|
||||
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<String, Any>
|
||||
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<String, Any>, theirDeviceIdentityKey: String?): String? {
|
||||
val sessionIdsSet = mOlmDevice!!.getSessionIds(theirDeviceIdentityKey!!)
|
||||
|
||||
val sessionIds: List<String>
|
||||
|
||||
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"
|
||||
}
|
||||
}
|
|
@ -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<String>,
|
||||
callback: MatrixCallback<Content>) {
|
||||
// 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<Unit> {
|
||||
override fun onSuccess(info: Unit) {
|
||||
val deviceInfos = ArrayList<MXDeviceInfo>()
|
||||
|
||||
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<String, Any>()
|
||||
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<String>, callback: MatrixCallback<Unit>?) {
|
||||
mDeviceListManager.downloadKeys(users, false, object : MatrixCallback<MXUsersDevicesMap<MXDeviceInfo>> {
|
||||
|
||||
override fun onSuccess(data: MXUsersDevicesMap<MXDeviceInfo>) {
|
||||
mCrypto!!.ensureOlmSessionsForUsers(users, object : MatrixCallback<MXUsersDevicesMap<MXOlmSessionResult>> {
|
||||
override fun onSuccess(data: MXUsersDevicesMap<MXOlmSessionResult>) {
|
||||
callback?.onSuccess(Unit)
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
callback?.onFailure(failure)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
callback?.onFailure(failure)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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<DevicesListResponse>
|
||||
|
||||
/**
|
||||
* 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<KeysUploadResponse>
|
||||
|
||||
/**
|
||||
* 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<KeysUploadResponse>
|
||||
|
||||
/**
|
||||
* 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<KeysQueryResponse>
|
||||
|
||||
/**
|
||||
* 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<KeysClaimResponse>
|
||||
|
||||
/**
|
||||
* 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<Unit>
|
||||
|
||||
/**
|
||||
* 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<Unit>
|
||||
|
||||
/**
|
||||
* 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<Unit>
|
||||
|
||||
/**
|
||||
* 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<KeyChangesResponse>
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -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)
|
||||
}
|
|
@ -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<KeysBackupService.KeysBackupStateListener>()
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<KeysVersion>
|
||||
|
||||
/**
|
||||
* 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<KeysVersionResult>
|
||||
|
||||
/**
|
||||
* 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<KeysVersionResult>
|
||||
|
||||
/**
|
||||
* 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<Unit>
|
||||
|
||||
/* ==========================================================================================
|
||||
* 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<BackupKeysResult>
|
||||
|
||||
/**
|
||||
* 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<BackupKeysResult>
|
||||
|
||||
/**
|
||||
* 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<BackupKeysResult>
|
||||
|
||||
/* ==========================================================================================
|
||||
* 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<KeyBackupData>
|
||||
|
||||
/**
|
||||
* 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<RoomKeysBackupData>
|
||||
|
||||
/**
|
||||
* 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<KeysBackupData>
|
||||
|
||||
|
||||
/* ==========================================================================================
|
||||
* 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<Unit>
|
||||
|
||||
/**
|
||||
* 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<Unit>
|
||||
|
||||
/**
|
||||
* Deletes keys from the backup.
|
||||
*/
|
||||
@DELETE(NetworkConstants.URI_API_PREFIX_PATH_R0 + "room_keys/keys")
|
||||
fun deleteSessionsData(@Query("version") version: String): Call<Unit>
|
||||
|
||||
/* ==========================================================================================
|
||||
* Deleting backup
|
||||
* ========================================================================================== */
|
||||
|
||||
/**
|
||||
* Deletes a backup.
|
||||
*/
|
||||
@DELETE(NetworkConstants.URI_API_PREFIX_PATH_R0 + "room_keys/version/{version}")
|
||||
fun deleteBackup(@Path("version") version: String): Call<Unit>
|
||||
}
|
|
@ -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<KeyBackupVersionTrustSignature> = ArrayList()
|
||||
)
|
|
@ -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
|
||||
|
||||
}
|
|
@ -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<KeysBackupVersionTrustSignature> = ArrayList()
|
||||
)
|
|
@ -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
|
||||
|
||||
}
|
|
@ -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<String, Map<String, String>>? = 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<String, Any> = HashMap<String, Any>().apply {
|
||||
put("public_key", publicKey)
|
||||
|
||||
privateKeySalt?.let {
|
||||
put("private_key_salt", it)
|
||||
}
|
||||
privateKeyIterations?.let {
|
||||
put("private_key_iterations", it)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 = ""
|
||||
|
||||
}
|
|
@ -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
|
||||
|
||||
)
|
|
@ -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()
|
|
@ -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<String, Any>? = null
|
||||
) {
|
||||
|
||||
fun toJsonString(): String {
|
||||
return MoshiProvider.providesMoshi().adapter(KeyBackupData::class.java).toJson(this)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* Example:
|
||||
*
|
||||
* {
|
||||
* "algorithm": "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||
* "auth_data": {
|
||||
* "public_key": "abcdefg",
|
||||
* "signatures": {
|
||||
* "something": {
|
||||
* "ed25519:something": "hijklmnop"
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
*/
|
||||
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<String, Any>? = null
|
||||
|
||||
/**
|
||||
* Facility method to convert authData to a MegolmBackupAuthData object
|
||||
*/
|
||||
fun getAuthDataAsMegolmBackupAuthData(): MegolmBackupAuthData {
|
||||
return MoshiProvider.providesMoshi()
|
||||
.adapter(MegolmBackupAuthData::class.java)
|
||||
.fromJsonValue(authData)!!
|
||||
}
|
||||
}
|
|
@ -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<String, RoomKeysBackupData> = HashMap()
|
||||
|
||||
)
|
|
@ -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
|
||||
)
|
|
@ -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()
|
|
@ -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<String, KeyBackupData> = HashMap()
|
||||
)
|
|
@ -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()
|
|
@ -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<CreateKeysBackupVersionBody, KeysVersion>
|
||||
|
||||
|
||||
internal class DefaultCreateKeysBackupVersionTask(private val roomKeysApi: RoomKeysApi)
|
||||
: CreateKeysBackupVersionTask {
|
||||
|
||||
|
||||
override fun execute(params: CreateKeysBackupVersionBody): Try<KeysVersion> {
|
||||
return executeRequest {
|
||||
apiCall = roomKeysApi.createKeysBackupVersion(params)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<DeleteBackupTask.Params, Unit> {
|
||||
data class Params(
|
||||
val version: String
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
internal class DefaultDeleteBackupTask(private val roomKeysApi: RoomKeysApi)
|
||||
: DeleteBackupTask {
|
||||
|
||||
override fun execute(params: DeleteBackupTask.Params): Try<Unit> {
|
||||
return executeRequest {
|
||||
apiCall = roomKeysApi.deleteBackup(
|
||||
params.version)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<DeleteRoomSessionDataTask.Params, Unit> {
|
||||
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<Unit> {
|
||||
return executeRequest {
|
||||
apiCall = roomKeysApi.deleteRoomSessionData(
|
||||
params.roomId,
|
||||
params.sessionId,
|
||||
params.version)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<DeleteRoomSessionsDataTask.Params, Unit> {
|
||||
data class Params(
|
||||
val roomId: String,
|
||||
val version: String
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
internal class DefaultDeleteRoomSessionsDataTask(private val roomKeysApi: RoomKeysApi)
|
||||
: DeleteRoomSessionsDataTask {
|
||||
|
||||
override fun execute(params: DeleteRoomSessionsDataTask.Params): Try<Unit> {
|
||||
return executeRequest {
|
||||
apiCall = roomKeysApi.deleteRoomSessionsData(
|
||||
params.roomId,
|
||||
params.version)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<DeleteSessionsDataTask.Params, Unit> {
|
||||
data class Params(
|
||||
val version: String
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
internal class DefaultDeleteSessionsDataTask(private val roomKeysApi: RoomKeysApi)
|
||||
: DeleteSessionsDataTask {
|
||||
|
||||
override fun execute(params: DeleteSessionsDataTask.Params): Try<Unit> {
|
||||
return executeRequest {
|
||||
apiCall = roomKeysApi.deleteSessionsData(
|
||||
params.version)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<Unit, KeysVersionResult>
|
||||
|
||||
|
||||
internal class DefaultGetKeysBackupLastVersionTask(private val roomKeysApi: RoomKeysApi)
|
||||
: GetKeysBackupLastVersionTask {
|
||||
|
||||
|
||||
override fun execute(params: Unit): Try<KeysVersionResult> {
|
||||
return executeRequest {
|
||||
apiCall = roomKeysApi.getKeysBackupLastVersion()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<String, KeysVersionResult>
|
||||
|
||||
|
||||
internal class DefaultGetKeysBackupVersionTask(private val roomKeysApi: RoomKeysApi)
|
||||
: GetKeysBackupVersionTask {
|
||||
|
||||
|
||||
override fun execute(params: String): Try<KeysVersionResult> {
|
||||
return executeRequest {
|
||||
apiCall = roomKeysApi.getKeysBackupVersion(params)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<GetRoomSessionDataTask.Params, KeyBackupData> {
|
||||
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<KeyBackupData> {
|
||||
return executeRequest {
|
||||
apiCall = roomKeysApi.getRoomSessionData(
|
||||
params.roomId,
|
||||
params.sessionId,
|
||||
params.version)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<GetRoomSessionsDataTask.Params, RoomKeysBackupData> {
|
||||
data class Params(
|
||||
val roomId: String,
|
||||
val version: String
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
internal class DefaultGetRoomSessionsDataTask(private val roomKeysApi: RoomKeysApi)
|
||||
: GetRoomSessionsDataTask {
|
||||
|
||||
override fun execute(params: GetRoomSessionsDataTask.Params): Try<RoomKeysBackupData> {
|
||||
return executeRequest {
|
||||
apiCall = roomKeysApi.getRoomSessionsData(
|
||||
params.roomId,
|
||||
params.version)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<GetSessionsDataTask.Params, KeysBackupData> {
|
||||
data class Params(
|
||||
val version: String
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
internal class DefaultGetSessionsDataTask(private val roomKeysApi: RoomKeysApi)
|
||||
: GetSessionsDataTask {
|
||||
|
||||
override fun execute(params: GetSessionsDataTask.Params): Try<KeysBackupData> {
|
||||
return executeRequest {
|
||||
apiCall = roomKeysApi.getSessionsData(
|
||||
params.version)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<StoreRoomSessionDataTask.Params, BackupKeysResult> {
|
||||
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<BackupKeysResult> {
|
||||
return executeRequest {
|
||||
apiCall = roomKeysApi.storeRoomSessionData(
|
||||
params.roomId,
|
||||
params.sessionId,
|
||||
params.version,
|
||||
params.keyBackupData)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<StoreRoomSessionsDataTask.Params, BackupKeysResult> {
|
||||
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<BackupKeysResult> {
|
||||
return executeRequest {
|
||||
apiCall = roomKeysApi.storeRoomSessionsData(
|
||||
params.roomId,
|
||||
params.version,
|
||||
params.roomKeysBackupData)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<StoreSessionsDataTask.Params, BackupKeysResult> {
|
||||
data class Params(
|
||||
val version: String,
|
||||
val keysBackupData: KeysBackupData
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
internal class DefaultStoreSessionsDataTask(private val roomKeysApi: RoomKeysApi)
|
||||
: StoreSessionsDataTask {
|
||||
|
||||
override fun execute(params: StoreSessionsDataTask.Params): Try<BackupKeysResult> {
|
||||
return executeRequest {
|
||||
apiCall = roomKeysApi.storeSessionsData(
|
||||
params.version,
|
||||
params.keysBackupData)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<UpdateKeysBackupVersionTask.Params, Unit> {
|
||||
data class Params(
|
||||
val version: String,
|
||||
val keysBackupVersionBody: UpdateKeysBackupVersionBody
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
internal class DefaultUpdateKeysBackupVersionTask(private val roomKeysApi: RoomKeysApi)
|
||||
: UpdateKeysBackupVersionTask {
|
||||
|
||||
|
||||
override fun execute(params: UpdateKeysBackupVersionTask.Params): Try<Unit> {
|
||||
return executeRequest {
|
||||
apiCall = roomKeysApi.updateKeysBackupVersion(params.version, params.keysBackupVersionBody)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
|
@ -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<String>? = null,
|
||||
|
||||
/**
|
||||
* A map from "<key type>:<deviceId>" to "<base64-encoded key>".
|
||||
*/
|
||||
@Json(name = "keys")
|
||||
var keys: Map<String, String>? = null,
|
||||
|
||||
/**
|
||||
* The signature of this MXDeviceInfo.
|
||||
* A map from "<userId>" to a map from "<key type>:<deviceId>" to "<signature>"
|
||||
*/
|
||||
@Json(name = "signatures")
|
||||
var signatures: Map<String, Map<String, String>>? = 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<String, Any> {
|
||||
val map = HashMap<String, Any>()
|
||||
|
||||
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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
|
@ -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<String, Map<String, String>> signatures;
|
||||
|
||||
/**
|
||||
* Default constructor
|
||||
*/
|
||||
public MXKey() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a map to a MXKey
|
||||
*
|
||||
* @param map the map to convert
|
||||
*/
|
||||
public MXKey(Map<String, Map<String, Object>> map) {
|
||||
if ((null != map) && (map.size() > 0)) {
|
||||
List<String> mapKeys = new ArrayList<>(map.keySet());
|
||||
|
||||
String firstEntry = mapKeys.get(0);
|
||||
setKeyFullId(firstEntry);
|
||||
|
||||
Map<String, Object> params = map.get(firstEntry);
|
||||
value = (String) params.get("key");
|
||||
signatures = (Map<String, Map<String, String>>) 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<String, Object> signalableJSONDictionary() {
|
||||
Map<String, Object> 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;
|
||||
}
|
||||
}
|
|
@ -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<String, String> mKeysClaimed;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param sessionKey the session key
|
||||
*/
|
||||
public MXOlmInboundGroupSession(String sessionKey) {
|
||||
try {
|
||||
mSession = new OlmInboundGroupSession(sessionKey);
|
||||
} catch (Exception e) {
|
||||
Timber.e(e, "Cannot create");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<String, String>? = null
|
||||
|
||||
// Devices which forwarded this session to us (normally empty).
|
||||
var mForwardingCurve25519KeyChain: List<String>? = 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
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue