This commit is contained in:
Benoit Marty 2019-05-16 10:23:57 +02:00 committed by Benoit Marty
parent 6aae943e77
commit 1436667e7d
303 changed files with 25671 additions and 506 deletions

View file

@ -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'
}
}
}

View file

@ -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.

View file

@ -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()
}
}
}

View file

@ -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)
}

View file

@ -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()

View file

@ -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"
}
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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?
}

View file

@ -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."
}
}

View file

@ -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)
}
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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 cant 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
}

View file

@ -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)

View file

@ -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
}
}

View file

@ -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"
}

View file

@ -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
}
}

View file

@ -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)
}
}

View file

@ -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()
}

View file

@ -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
}

View file

@ -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
}
}
}

View file

@ -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,

View file

@ -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

View file

@ -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
}

View file

@ -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>
}

View file

@ -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>

View file

@ -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)
}
}

View file

@ -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"

View file

@ -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())
}
}
}

View file

@ -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

View file

@ -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
}
}

View file

@ -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()
}

View file

@ -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
}
}

View file

@ -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)
}
}
}

View file

@ -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)
}
}

View file

@ -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
)

View file

@ -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()
}
}

View file

@ -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
}
}

View file

@ -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()
)

View file

@ -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
*/

View file

@ -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)
}
}

View file

@ -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
}
}

View file

@ -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)
}
}

View file

@ -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
}
}
}
}

View file

@ -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)
}
}

View file

@ -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)
}

View file

@ -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>)
}

View file

@ -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
)

View file

@ -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")
}
})
}
}

View file

@ -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)
}
})
}
}

View file

@ -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
}
}

View file

@ -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"
}
}

View file

@ -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)
}
})
}
}

View file

@ -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>
}

View file

@ -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)
}

View file

@ -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)
}
}
}

View file

@ -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>
}

View file

@ -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()
)

View file

@ -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
}

View file

@ -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()
)

View file

@ -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
}

View file

@ -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)
}
}
}

View file

@ -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 = ""
}

View file

@ -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
)

View file

@ -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()

View file

@ -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)
}
}

View file

@ -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)!!
}
}

View file

@ -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()
)

View file

@ -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
)

View file

@ -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()

View file

@ -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()
)

View file

@ -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()

View file

@ -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)
}
}
}

View file

@ -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)
}
}
}

View file

@ -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)
}
}
}

View file

@ -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)
}
}
}

View file

@ -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)
}
}
}

View file

@ -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()
}
}
}

View file

@ -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)
}
}
}

View file

@ -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)
}
}
}

View file

@ -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)
}
}
}

View file

@ -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)
}
}
}

View file

@ -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)
}
}
}

View file

@ -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)
}
}
}

View file

@ -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)
}
}
}

View file

@ -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)
}
}
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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)

View file

@ -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
}
}

View file

@ -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
)

View file

@ -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;
}
}

View file

@ -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");
}
}
}

View file

@ -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
}
}

View file

@ -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