2019-05-16 08:23:57 +00:00
|
|
|
/*
|
|
|
|
* 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 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
|
2019-05-26 17:21:45 +00:00
|
|
|
import im.vector.matrix.android.api.util.JSON_DICT_PARAMETERIZED_TYPE
|
|
|
|
import im.vector.matrix.android.api.util.JsonDict
|
2019-05-20 08:30:33 +00:00
|
|
|
import im.vector.matrix.android.internal.crypto.MXDecryptionException
|
|
|
|
import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult
|
|
|
|
import im.vector.matrix.android.internal.crypto.MXOlmDevice
|
2019-05-16 08:23:57 +00:00
|
|
|
import im.vector.matrix.android.internal.crypto.algorithms.IMXDecrypting
|
|
|
|
import im.vector.matrix.android.internal.crypto.model.event.OlmEventContent
|
|
|
|
import im.vector.matrix.android.internal.crypto.model.event.OlmPayloadContent
|
2019-05-26 17:21:45 +00:00
|
|
|
import im.vector.matrix.android.internal.di.MoshiProvider
|
2019-05-16 08:23:57 +00:00
|
|
|
import im.vector.matrix.android.internal.util.convertFromUTF8
|
|
|
|
import timber.log.Timber
|
|
|
|
import java.util.*
|
|
|
|
|
2019-05-20 08:30:33 +00:00
|
|
|
internal class MXOlmDecryption(
|
|
|
|
// The olm device interface
|
2019-06-11 11:32:09 +00:00
|
|
|
private val olmDevice: MXOlmDevice,
|
2019-05-20 08:30:33 +00:00
|
|
|
// the matrix credentials
|
2019-06-11 11:32:09 +00:00
|
|
|
private val credentials: Credentials)
|
2019-05-20 08:30:33 +00:00
|
|
|
: IMXDecrypting {
|
2019-05-16 08:23:57 +00:00
|
|
|
|
|
|
|
@Throws(MXDecryptionException::class)
|
2019-07-03 14:48:53 +00:00
|
|
|
override suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
|
2019-05-16 08:23:57 +00:00
|
|
|
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))
|
|
|
|
}
|
|
|
|
|
2019-06-11 11:32:09 +00:00
|
|
|
if (!olmEventContent.ciphertext!!.containsKey(olmDevice.deviceCurve25519Key)) {
|
|
|
|
Timber.e("## decryptEvent() : our device " + olmDevice.deviceCurve25519Key
|
2019-05-16 08:23:57 +00:00
|
|
|
+ " 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
|
2019-06-11 11:32:09 +00:00
|
|
|
val message = olmEventContent.ciphertext!![olmDevice.deviceCurve25519Key] as JsonDict
|
2019-05-26 17:21:45 +00:00
|
|
|
val decryptedPayload = decryptMessage(message, olmEventContent.senderKey!!)
|
2019-05-16 08:23:57 +00:00
|
|
|
|
2019-05-26 17:21:45 +00:00
|
|
|
if (decryptedPayload == null) {
|
2019-05-16 08:23:57 +00:00
|
|
|
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))
|
|
|
|
}
|
2019-05-26 17:21:45 +00:00
|
|
|
val payloadString = convertFromUTF8(decryptedPayload)
|
|
|
|
if (payloadString == null) {
|
|
|
|
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 adapter = MoshiProvider.providesMoshi().adapter<JsonDict>(JSON_DICT_PARAMETERIZED_TYPE)
|
|
|
|
val payload = adapter.fromJson(payloadString)
|
2019-05-16 08:23:57 +00:00
|
|
|
|
2019-05-26 17:21:45 +00:00
|
|
|
if (payload == null) {
|
2019-05-16 08:23:57 +00:00
|
|
|
Timber.e("## decryptEvent failed : null payload")
|
|
|
|
throw MXDecryptionException(MXCryptoError(MXCryptoError.UNABLE_TO_DECRYPT_ERROR_CODE,
|
|
|
|
MXCryptoError.UNABLE_TO_DECRYPT, MXCryptoError.MISSING_CIPHER_TEXT_REASON))
|
|
|
|
}
|
|
|
|
|
2019-05-26 17:21:45 +00:00
|
|
|
val olmPayloadContent = OlmPayloadContent.fromJsonString(payloadString)
|
2019-05-16 08:23:57 +00:00
|
|
|
|
|
|
|
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))
|
|
|
|
}
|
|
|
|
|
2019-06-11 11:32:09 +00:00
|
|
|
if (!TextUtils.equals(olmPayloadContent.recipient, credentials.userId)) {
|
2019-05-16 08:23:57 +00:00
|
|
|
Timber.e("## decryptEvent() : Event " + event.eventId + ": Intended recipient " + olmPayloadContent.recipient
|
2019-06-11 11:32:09 +00:00
|
|
|
+ " does not match our id " + credentials.userId)
|
2019-05-16 08:23:57 +00:00
|
|
|
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")
|
|
|
|
|
2019-06-11 11:32:09 +00:00
|
|
|
if (!TextUtils.equals(ed25519, olmDevice.deviceEd25519Key)) {
|
2019-05-16 08:23:57 +00:00
|
|
|
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")))
|
|
|
|
}
|
|
|
|
|
2019-06-18 13:58:19 +00:00
|
|
|
if (!TextUtils.equals(olmPayloadContent.sender, event.senderId)) {
|
2019-05-16 08:23:57 +00:00
|
|
|
Timber.e("Event " + event.eventId + ": original sender " + olmPayloadContent.sender
|
2019-06-18 13:58:19 +00:00
|
|
|
+ " does not match reported sender " + event.senderId)
|
2019-05-16 08:23:57 +00:00
|
|
|
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()
|
2019-06-03 16:39:37 +00:00
|
|
|
result.clearEvent = payload
|
|
|
|
result.senderCurve25519Key = olmEventContent.senderKey
|
|
|
|
result.claimedEd25519Key = olmPayloadContent.keys!!.get("ed25519")
|
2019-05-16 08:23:57 +00:00
|
|
|
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*/
|
2019-05-26 17:21:45 +00:00
|
|
|
private fun decryptMessage(message: JsonDict, theirDeviceIdentityKey: String): String? {
|
2019-06-11 11:32:09 +00:00
|
|
|
val sessionIdsSet = olmDevice.getSessionIds(theirDeviceIdentityKey)
|
2019-05-16 08:23:57 +00:00
|
|
|
|
|
|
|
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) {
|
2019-06-11 11:32:09 +00:00
|
|
|
val payload = olmDevice.decryptMessage(messageBody, messageType, sessionId, theirDeviceIdentityKey)
|
2019-05-16 08:23:57 +00:00
|
|
|
|
|
|
|
if (null != payload) {
|
2019-05-20 15:13:08 +00:00
|
|
|
Timber.v("## decryptMessage() : Decrypted Olm message from $theirDeviceIdentityKey with session $sessionId")
|
2019-05-16 08:23:57 +00:00
|
|
|
return payload
|
|
|
|
} else {
|
2019-06-11 11:32:09 +00:00
|
|
|
val foundSession = olmDevice.matchesSession(theirDeviceIdentityKey, sessionId, messageType, messageBody)
|
2019-05-16 08:23:57 +00:00
|
|
|
|
|
|
|
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.
|
2019-06-11 11:32:09 +00:00
|
|
|
val res = olmDevice.createInboundSession(theirDeviceIdentityKey, messageType, messageBody)
|
2019-05-16 08:23:57 +00:00
|
|
|
|
|
|
|
if (null == res) {
|
|
|
|
Timber.e("## decryptMessage() : Error decrypting non-prekey message with existing sessions")
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
|
2019-05-20 15:13:08 +00:00
|
|
|
Timber.v("## decryptMessage() : Created new inbound Olm session get id " + res["session_id"] + " with " + theirDeviceIdentityKey)
|
2019-05-16 08:23:57 +00:00
|
|
|
|
|
|
|
return res["payload"]
|
|
|
|
}
|
|
|
|
}
|