mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2025-03-16 03:08:58 +03:00
commit
567c1fd7a5
427 changed files with 26807 additions and 908 deletions
|
@ -3,6 +3,7 @@
|
|||
buildscript {
|
||||
ext.kotlin_version = '1.3.21'
|
||||
ext.koin_version = '1.0.2'
|
||||
// TODO ext.koin_version = '2.0.0-GA'
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
|
@ -29,6 +30,10 @@ allprojects {
|
|||
maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' }
|
||||
google()
|
||||
jcenter()
|
||||
// For Olm SDK
|
||||
maven {
|
||||
url 'https://jitpack.io'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -128,6 +128,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.
|
@ -19,4 +19,4 @@ package im.vector.matrix.android
|
|||
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
|
||||
internal val testCoroutineDispatchers = MatrixCoroutineDispatchers(Main, Main, Main)
|
||||
internal val testCoroutineDispatchers = MatrixCoroutineDispatchers(Main, Main, Main, Main)
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.crypto
|
||||
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
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.RealmCryptoStoreModule
|
||||
import io.realm.RealmConfiguration
|
||||
import java.util.*
|
||||
|
||||
internal class CryptoStoreHelper {
|
||||
|
||||
fun createStore(): IMXCryptoStore {
|
||||
return RealmCryptoStore(
|
||||
realmConfiguration = RealmConfiguration.Builder()
|
||||
.name("test.realm")
|
||||
.modules(RealmCryptoStoreModule())
|
||||
.build(),
|
||||
credentials = createCredential())
|
||||
}
|
||||
|
||||
fun createCredential() = Credentials(
|
||||
userId = "userId_" + Random().nextInt(),
|
||||
homeServer = "http://matrix.org",
|
||||
accessToken = "access_token",
|
||||
refreshToken = null,
|
||||
deviceId = "deviceId_sample"
|
||||
)
|
||||
}
|
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
* 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.OlmSessionWrapper
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Test
|
||||
import org.matrix.olm.OlmAccount
|
||||
import org.matrix.olm.OlmManager
|
||||
import org.matrix.olm.OlmSession
|
||||
|
||||
private const val DUMMY_DEVICE_KEY = "DeviceKey"
|
||||
|
||||
class CryptoStoreTest {
|
||||
|
||||
private val cryptoStoreHelper = CryptoStoreHelper()
|
||||
|
||||
@Test
|
||||
fun test_metadata_realm_ok() {
|
||||
val cryptoStore: IMXCryptoStore = cryptoStoreHelper.createStore()
|
||||
|
||||
assertFalse(cryptoStore.hasData())
|
||||
|
||||
cryptoStore.open()
|
||||
|
||||
assertEquals("deviceId_sample", cryptoStore.getDeviceId())
|
||||
|
||||
assertTrue(cryptoStore.hasData())
|
||||
|
||||
// Cleanup
|
||||
cryptoStore.close()
|
||||
cryptoStore.deleteStore()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test_lastSessionUsed() {
|
||||
// Ensure Olm is initialized
|
||||
OlmManager()
|
||||
|
||||
val cryptoStore: IMXCryptoStore = cryptoStoreHelper.createStore()
|
||||
|
||||
assertNull(cryptoStore.getLastUsedSessionId(DUMMY_DEVICE_KEY))
|
||||
|
||||
val olmAccount1 = OlmAccount().apply {
|
||||
generateOneTimeKeys(1)
|
||||
}
|
||||
|
||||
val olmSession1 = OlmSession().apply {
|
||||
initOutboundSession(olmAccount1,
|
||||
olmAccount1.identityKeys()[OlmAccount.JSON_KEY_IDENTITY_KEY],
|
||||
olmAccount1.oneTimeKeys()[OlmAccount.JSON_KEY_ONE_TIME_KEY]?.values?.first())
|
||||
}
|
||||
|
||||
val sessionId1 = olmSession1.sessionIdentifier()
|
||||
val olmSessionWrapper1 = OlmSessionWrapper(olmSession1)
|
||||
|
||||
cryptoStore.storeSession(olmSessionWrapper1, DUMMY_DEVICE_KEY)
|
||||
|
||||
assertEquals(sessionId1, cryptoStore.getLastUsedSessionId(DUMMY_DEVICE_KEY))
|
||||
|
||||
val olmAccount2 = OlmAccount().apply {
|
||||
generateOneTimeKeys(1)
|
||||
}
|
||||
|
||||
val olmSession2 = OlmSession().apply {
|
||||
initOutboundSession(olmAccount2,
|
||||
olmAccount2.identityKeys()[OlmAccount.JSON_KEY_IDENTITY_KEY],
|
||||
olmAccount2.oneTimeKeys()[OlmAccount.JSON_KEY_ONE_TIME_KEY]?.values?.first())
|
||||
}
|
||||
|
||||
val sessionId2 = olmSession2.sessionIdentifier()
|
||||
val olmSessionWrapper2 = OlmSessionWrapper(olmSession2)
|
||||
|
||||
cryptoStore.storeSession(olmSessionWrapper2, DUMMY_DEVICE_KEY)
|
||||
|
||||
// Ensure sessionIds are distinct
|
||||
assertNotEquals(sessionId1, sessionId2)
|
||||
|
||||
// Note: we cannot be sure what will be the result of getLastUsedSessionId() here
|
||||
|
||||
olmSessionWrapper2.onMessageReceived()
|
||||
cryptoStore.storeSession(olmSessionWrapper2, DUMMY_DEVICE_KEY)
|
||||
|
||||
// sessionId2 is returned now
|
||||
assertEquals(sessionId2, cryptoStore.getLastUsedSessionId(DUMMY_DEVICE_KEY))
|
||||
|
||||
Thread.sleep(2)
|
||||
|
||||
olmSessionWrapper1.onMessageReceived()
|
||||
cryptoStore.storeSession(olmSessionWrapper1, DUMMY_DEVICE_KEY)
|
||||
|
||||
// sessionId1 is returned now
|
||||
assertEquals(sessionId1, cryptoStore.getLastUsedSessionId(DUMMY_DEVICE_KEY))
|
||||
|
||||
// Cleanup
|
||||
olmSession1.releaseSession()
|
||||
olmSession2.releaseSession()
|
||||
|
||||
olmAccount1.releaseAccount()
|
||||
olmAccount2.releaseAccount()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,154 @@
|
|||
/*
|
||||
* 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.util
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import im.vector.matrix.android.InstrumentedTest
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
internal class JsonCanonicalizerTest : InstrumentedTest {
|
||||
|
||||
@Test
|
||||
fun identityTest() {
|
||||
listOf(
|
||||
"{}",
|
||||
"""{"a":true}""",
|
||||
"""{"a":false}""",
|
||||
"""{"a":1}""",
|
||||
"""{"a":1.2}""",
|
||||
"""{"a":null}""",
|
||||
"""{"a":[]}""",
|
||||
"""{"a":["b":"c"]}""",
|
||||
"""{"a":["c":"b","d":"e"]}""",
|
||||
"""{"a":["d":"b","c":"e"]}"""
|
||||
).forEach {
|
||||
assertEquals(it,
|
||||
JsonCanonicalizer.canonicalize(it))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun reorderTest() {
|
||||
assertEquals("""{"a":true,"b":false}""",
|
||||
JsonCanonicalizer.canonicalize("""{"b":false,"a":true}"""))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun realSampleTest() {
|
||||
assertEquals("""{"algorithms":["m.megolm.v1.aes-sha2","m.olm.v1.curve25519-aes-sha2"],"device_id":"VSCUNFSOUI","keys":{"curve25519:VSCUNFSOUI":"utyOjnhiQ73qNhi9HlN0OgWIowe5gthTS8r0r9TcJ3o","ed25519:VSCUNFSOUI":"qNhEt+Yggaajet0hX/FjTRLfySgs65ldYyomm7PIx6U"},"user_id":"@benoitx:matrix.org"}""",
|
||||
JsonCanonicalizer.canonicalize("""{"algorithms":["m.megolm.v1.aes-sha2","m.olm.v1.curve25519-aes-sha2"],"device_id":"VSCUNFSOUI","user_id":"@benoitx:matrix.org","keys":{"curve25519:VSCUNFSOUI":"utyOjnhiQ73qNhi9HlN0OgWIowe5gthTS8r0r9TcJ3o","ed25519:VSCUNFSOUI":"qNhEt+Yggaajet0hX/FjTRLfySgs65ldYyomm7PIx6U"}}"""))
|
||||
}
|
||||
|
||||
/* ==========================================================================================
|
||||
* Test from https://matrix.org/docs/spec/appendices.html#examples
|
||||
* ========================================================================================== */
|
||||
|
||||
@Test
|
||||
fun matrixOrg001Test() {
|
||||
assertEquals("""{}""",
|
||||
JsonCanonicalizer.canonicalize("""{}"""))
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun matrixOrg002Test() {
|
||||
assertEquals("""{"one":1,"two":"Two"}""",
|
||||
JsonCanonicalizer.canonicalize("""{
|
||||
"one": 1,
|
||||
"two": "Two"
|
||||
}"""))
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun matrixOrg003Test() {
|
||||
assertEquals("""{"a":"1","b":"2"}""",
|
||||
JsonCanonicalizer.canonicalize("""{
|
||||
"b": "2",
|
||||
"a": "1"
|
||||
}"""))
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun matrixOrg004Test() {
|
||||
assertEquals("""{"a":"1","b":"2"}""",
|
||||
JsonCanonicalizer.canonicalize("""{"b":"2","a":"1"}"""))
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun matrixOrg005Test() {
|
||||
assertEquals("""{"auth":{"mxid":"@john.doe:example.com","profile":{"display_name":"John Doe","three_pids":[{"address":"john.doe@example.org","medium":"email"},{"address":"123456789","medium":"msisdn"}]},"success":true}}""",
|
||||
JsonCanonicalizer.canonicalize("""{
|
||||
"auth": {
|
||||
"success": true,
|
||||
"mxid": "@john.doe:example.com",
|
||||
"profile": {
|
||||
"display_name": "John Doe",
|
||||
"three_pids": [
|
||||
{
|
||||
"medium": "email",
|
||||
"address": "john.doe@example.org"
|
||||
},
|
||||
{
|
||||
"medium": "msisdn",
|
||||
"address": "123456789"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}"""))
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun matrixOrg006Test() {
|
||||
assertEquals("""{"a":"日本語"}""",
|
||||
JsonCanonicalizer.canonicalize("""{
|
||||
"a": "日本語"
|
||||
}"""))
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun matrixOrg007Test() {
|
||||
assertEquals("""{"日":1,"本":2}""",
|
||||
JsonCanonicalizer.canonicalize("""{
|
||||
"本": 2,
|
||||
"日": 1
|
||||
}"""))
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun matrixOrg008Test() {
|
||||
assertEquals("""{"a":"日"}""",
|
||||
JsonCanonicalizer.canonicalize("{\"a\": \"\u65E5\"}"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun matrixOrg009Test() {
|
||||
assertEquals("""{"a":null}""",
|
||||
JsonCanonicalizer.canonicalize("""{
|
||||
"a": null
|
||||
}"""))
|
||||
}
|
||||
}
|
|
@ -25,7 +25,7 @@ import kotlin.random.Random
|
|||
|
||||
internal class FakeGetContextOfEventTask(private val tokenChunkEventPersistor: TokenChunkEventPersistor) : GetContextOfEventTask {
|
||||
|
||||
override fun execute(params: GetContextOfEventTask.Params): Try<TokenChunkEventPersistor.Result> {
|
||||
override suspend fun execute(params: GetContextOfEventTask.Params): Try<TokenChunkEventPersistor.Result> {
|
||||
val fakeEvents = RoomDataHelper.createFakeListOfEvents(30)
|
||||
val tokenChunkEvent = FakeTokenChunkEvent(
|
||||
Random.nextLong(System.currentTimeMillis()).toString(),
|
||||
|
|
|
@ -23,7 +23,7 @@ import kotlin.random.Random
|
|||
|
||||
internal class FakePaginationTask(private val tokenChunkEventPersistor: TokenChunkEventPersistor) : PaginationTask {
|
||||
|
||||
override fun execute(params: PaginationTask.Params): Try<TokenChunkEventPersistor.Result> {
|
||||
override suspend fun execute(params: PaginationTask.Params): Try<TokenChunkEventPersistor.Result> {
|
||||
val fakeEvents = RoomDataHelper.createFakeListOfEvents(30)
|
||||
val tokenChunkEvent = FakeTokenChunkEvent(params.from, Random.nextLong(System.currentTimeMillis()).toString(), fakeEvents)
|
||||
return tokenChunkEventPersistor.insertInDb(tokenChunkEvent, params.roomId, params.direction)
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.api.comparators
|
||||
|
||||
import im.vector.matrix.android.api.interfaces.DatedObject
|
||||
import java.util.*
|
||||
|
||||
object DatedObjectComparators {
|
||||
|
||||
/**
|
||||
* Comparator to sort DatedObjects from the oldest to the latest.
|
||||
*/
|
||||
val ascComparator by lazy {
|
||||
Comparator<DatedObject> { datedObject1, datedObject2 ->
|
||||
(datedObject1.date - datedObject2.date).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Comparator to sort DatedObjects from the latest to the oldest.
|
||||
*/
|
||||
val descComparator by lazy {
|
||||
Comparator<DatedObject> { datedObject1, datedObject2 ->
|
||||
(datedObject2.date - datedObject1.date).toInt()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.api.extensions
|
||||
|
||||
import im.vector.matrix.android.api.comparators.DatedObjectComparators
|
||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
||||
import java.util.*
|
||||
|
||||
/* ==========================================================================================
|
||||
* MXDeviceInfo
|
||||
* ========================================================================================== */
|
||||
|
||||
fun MXDeviceInfo.getFingerprintHumanReadable() = fingerprint()
|
||||
?.chunked(4)
|
||||
?.joinToString(separator = " ")
|
||||
|
||||
|
||||
fun List<DeviceInfo>.sortByLastSeen() {
|
||||
Collections.sort(this, DatedObjectComparators.descComparator)
|
||||
}
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package im.vector.matrix.android.api.failure
|
||||
|
||||
import im.vector.matrix.android.api.session.crypto.MXCryptoError
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
|
@ -31,6 +32,7 @@ sealed class Failure(cause: Throwable? = null) : Throwable(cause = cause) {
|
|||
data class Unknown(val throwable: Throwable? = null) : Failure(throwable)
|
||||
data class NetworkConnection(val ioException: IOException? = null) : Failure(ioException)
|
||||
data class ServerError(val error: MatrixError) : Failure(RuntimeException(error.toString()))
|
||||
data class CryptoError(val error: MXCryptoError) : Failure(RuntimeException(error.toString()))
|
||||
|
||||
abstract class FeatureFailure : Failure()
|
||||
|
||||
|
|
|
@ -54,5 +54,6 @@ data class MatrixError(
|
|||
const val TOO_LARGE = "M_TOO_LARGE"
|
||||
const val M_CONSENT_NOT_GIVEN = "M_CONSENT_NOT_GIVEN"
|
||||
const val RESOURCE_LIMIT_EXCEEDED = "M_RESOURCE_LIMIT_EXCEEDED"
|
||||
const val WRONG_ROOM_KEYS_VERSION = "M_WRONG_ROOM_KEYS_VERSION"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.api.interfaces
|
||||
|
||||
/**
|
||||
* Can be implemented by any object containing a timestamp.
|
||||
* This interface can be use to sort such object
|
||||
*/
|
||||
interface DatedObject {
|
||||
val date: Long
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.api.listeners
|
||||
|
||||
/**
|
||||
* Interface to send a progress info
|
||||
*/
|
||||
interface ProgressListener {
|
||||
/**
|
||||
* @param progress from 0 to total by contract
|
||||
* @param total
|
||||
*/
|
||||
fun onProgress(progress: Int, total: Int)
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.api.listeners
|
||||
|
||||
/**
|
||||
* Interface to send a progress info
|
||||
*/
|
||||
interface StepProgressListener {
|
||||
|
||||
sealed class Step {
|
||||
data class ComputingKey(val progress: Int, val total: Int) : Step()
|
||||
object DownloadingKey : Step()
|
||||
data class ImportingKey(val progress: Int, val total: Int) : Step()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param step The current step, containing progress data if available. Else you should consider progress as indeterminate
|
||||
*/
|
||||
fun onStepProgress(step: Step)
|
||||
}
|
|
@ -16,8 +16,93 @@
|
|||
|
||||
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.Content
|
||||
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.MXEncryptEventContentResult
|
||||
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
||||
|
||||
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?): Boolean
|
||||
|
||||
fun setWarnOnUnknownDevices(warn: Boolean)
|
||||
|
||||
fun setDeviceVerification(verificationStatus: Int, deviceId: String, userId: String)
|
||||
|
||||
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(): Boolean
|
||||
|
||||
fun setGlobalBlacklistUnverifiedDevices(block: Boolean)
|
||||
|
||||
fun setRoomUnBlacklistUnverifiedDevices(roomId: String)
|
||||
|
||||
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)
|
||||
|
||||
fun getDeviceInfo(userId: String, deviceId: String?): MXDeviceInfo?
|
||||
|
||||
fun reRequestRoomKeyForEvent(event: Event)
|
||||
|
||||
fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody)
|
||||
|
||||
fun addRoomKeysRequestListener(listener: RoomKeysRequestListener)
|
||||
|
||||
fun getDevicesList(callback: MatrixCallback<DevicesListResponse>)
|
||||
|
||||
fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int
|
||||
|
||||
fun isRoomEncrypted(roomId: String): Boolean
|
||||
|
||||
fun encryptEventContent(eventContent: Content,
|
||||
eventType: String,
|
||||
roomId: String,
|
||||
callback: MatrixCallback<MXEncryptEventContentResult>)
|
||||
|
||||
fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult?
|
||||
|
||||
fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback<MXEventDecryptionResult?>)
|
||||
|
||||
fun getEncryptionAlgorithm(roomId: String): String?
|
||||
|
||||
fun shouldEncryptForInvitedMembers(roomId: String): Boolean
|
||||
|
||||
fun downloadKeys(userIds: List<String>, forceDownload: Boolean, callback: MatrixCallback<MXUsersDevicesMap<MXDeviceInfo>>)
|
||||
|
||||
fun clearCryptoCache(callback: MatrixCallback<Unit>)
|
||||
|
||||
}
|
|
@ -0,0 +1,132 @@
|
|||
/*
|
||||
* 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() = 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"
|
||||
const val UNKNOWN_MESSAGE_INDEX = "UNKNOWN_MESSAGE_INDEX"
|
||||
|
||||
/**
|
||||
* short error reasons
|
||||
*/
|
||||
const val UNABLE_TO_DECRYPT = "Unable to decrypt"
|
||||
const val UNABLE_TO_ENCRYPT = "Unable to encrypt"
|
||||
|
||||
/**
|
||||
* Detailed error reasons
|
||||
*/
|
||||
const val ENCRYPTING_NOT_ENABLED_REASON = "Encryption not enabled"
|
||||
const val UNABLE_TO_ENCRYPT_REASON = "Unable to encrypt %s"
|
||||
const val UNABLE_TO_DECRYPT_REASON = "Unable to decrypt %1\$s. Algorithm: %2\$s"
|
||||
const val OLM_REASON = "OLM error: %1\$s"
|
||||
const val DETAILLED_OLM_REASON = "Unable to decrypt %1\$s. OLM error: %2\$s"
|
||||
const val UNKNOWN_INBOUND_SESSION_ID_REASON = "Unknown inbound session id"
|
||||
const val INBOUND_SESSION_MISMATCH_ROOM_ID_REASON = "Mismatched room_id for inbound group session (expected %1\$s, was %2\$s)"
|
||||
const val MISSING_FIELDS_REASON = "Missing fields in input"
|
||||
const val MISSING_CIPHER_TEXT_REASON = "Missing ciphertext"
|
||||
const val NOT_INCLUDED_IN_RECIPIENT_REASON = "Not included in recipients"
|
||||
const val BAD_RECIPIENT_REASON = "Message was intended for %1\$s"
|
||||
const val BAD_RECIPIENT_KEY_REASON = "Message not intended for this device"
|
||||
const val FORWARDED_MESSAGE_REASON = "Message forwarded from %1\$s"
|
||||
const val BAD_ROOM_REASON = "Message intended for room %1\$s"
|
||||
const val BAD_ENCRYPTED_MESSAGE_REASON = "Bad Encrypted Message"
|
||||
const val DUPLICATE_MESSAGE_INDEX_REASON = "Duplicate message index, possible replay attack %1\$s"
|
||||
const val ERROR_MISSING_PROPERTY_REASON = "No '%1\$s' property. Cannot prevent unknown-key attack"
|
||||
const val UNKNOWN_DEVICES_REASON = "This room contains unknown devices which have not been verified.\n" + "We strongly recommend you verify them before continuing."
|
||||
const val NO_MORE_ALGORITHM_REASON = "Room was previously configured to use encryption, but is no longer." + " Perhaps the homeserver is hiding the configuration event."
|
||||
}
|
||||
}
|
|
@ -0,0 +1,192 @@
|
|||
/*
|
||||
* 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.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
|
||||
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
|
||||
|
||||
interface KeysBackupService {
|
||||
/**
|
||||
* Retrieve the current version of the backup from the home server
|
||||
*
|
||||
* It can be different than keysBackupVersion.
|
||||
* @param callback onSuccess(null) will be called if there is no backup on the server
|
||||
*/
|
||||
fun getCurrentVersion(callback: MatrixCallback<KeysVersionResult?>)
|
||||
|
||||
/**
|
||||
* Create a new keys backup version and enable it, using the information return from [prepareKeysBackupVersion].
|
||||
*
|
||||
* @param keysBackupCreationInfo the info object from [prepareKeysBackupVersion].
|
||||
* @param callback Asynchronous callback
|
||||
*/
|
||||
fun createKeysBackupVersion(keysBackupCreationInfo: MegolmBackupCreationInfo, callback: MatrixCallback<KeysVersion>)
|
||||
|
||||
/**
|
||||
* Facility method to get the total number of locally stored keys
|
||||
*/
|
||||
fun getTotalNumbersOfKeys(): Int
|
||||
|
||||
/**
|
||||
* Facility method to get the number of backed up keys
|
||||
*/
|
||||
fun getTotalNumbersOfBackedUpKeys(): Int
|
||||
|
||||
/**
|
||||
* Start to back up keys immediately.
|
||||
*
|
||||
* @param progressListener the callback to follow the progress
|
||||
* @param callback the main callback
|
||||
*/
|
||||
fun backupAllGroupSessions(progressListener: ProgressListener?, callback: MatrixCallback<Unit>?)
|
||||
|
||||
/**
|
||||
* Check trust on a key backup version.
|
||||
*
|
||||
* @param keysBackupVersion the backup version to check.
|
||||
* @param callback block called when the operations completes.
|
||||
*/
|
||||
fun getKeysBackupTrust(keysBackupVersion: KeysVersionResult, callback: MatrixCallback<KeysBackupVersionTrust>)
|
||||
|
||||
/**
|
||||
* Return the current progress of the backup
|
||||
*/
|
||||
fun getBackupProgress(progressListener: ProgressListener)
|
||||
|
||||
/**
|
||||
* Get information about a backup version defined on the homeserver.
|
||||
*
|
||||
* It can be different than keysBackupVersion.
|
||||
* @param version the backup version
|
||||
* @param callback
|
||||
*/
|
||||
fun getVersion(version: String, callback: MatrixCallback<KeysVersionResult?>)
|
||||
|
||||
/**
|
||||
* This method fetches the last backup version on the server, then compare to the currently backup version use.
|
||||
* If versions are not the same, the current backup is deleted (on server or locally), then the backup may be started again, using the last version.
|
||||
*
|
||||
* @param callback true if backup is already using the last version, and false if it is not the case
|
||||
*/
|
||||
fun forceUsingLastVersion(callback: MatrixCallback<Boolean>)
|
||||
|
||||
/**
|
||||
* Check the server for an active key backup.
|
||||
*
|
||||
* If one is present and has a valid signature from one of the user's verified
|
||||
* devices, start backing up to it.
|
||||
*/
|
||||
fun checkAndStartKeysBackup()
|
||||
|
||||
fun addListener(listener: KeysBackupStateListener)
|
||||
|
||||
fun removeListener(listener: KeysBackupStateListener)
|
||||
|
||||
/**
|
||||
* Set up the data required to create a new backup version.
|
||||
* The backup version will not be created and enabled until [createKeysBackupVersion]
|
||||
* is called.
|
||||
* The returned [MegolmBackupCreationInfo] object has a `recoveryKey` member with
|
||||
* the user-facing recovery key string.
|
||||
*
|
||||
* @param password an optional passphrase string that can be entered by the user
|
||||
* when restoring the backup as an alternative to entering the recovery key.
|
||||
* @param progressListener a progress listener, as generating private key from password may take a while
|
||||
* @param callback Asynchronous callback
|
||||
*/
|
||||
fun prepareKeysBackupVersion(password: String?, progressListener: ProgressListener?, callback: MatrixCallback<MegolmBackupCreationInfo>)
|
||||
|
||||
/**
|
||||
* Delete a keys backup version. It will delete all backed up keys on the server, and the backup itself.
|
||||
* If we are backing up to this version. Backup will be stopped.
|
||||
*
|
||||
* @param version the backup version to delete.
|
||||
* @param callback Asynchronous callback
|
||||
*/
|
||||
fun deleteBackup(version: String, callback: MatrixCallback<Unit>?)
|
||||
|
||||
/**
|
||||
* Ask if the backup on the server contains keys that we may do not have locally.
|
||||
* This should be called when entering in the state READY_TO_BACKUP
|
||||
*/
|
||||
fun canRestoreKeys(): Boolean
|
||||
|
||||
/**
|
||||
* Set trust on a keys backup version.
|
||||
* It adds (or removes) the signature of the current device to the authentication part of the keys backup version.
|
||||
*
|
||||
* @param keysBackupVersion the backup version to check.
|
||||
* @param trust the trust to set to the keys backup.
|
||||
* @param callback block called when the operations completes.
|
||||
*/
|
||||
fun trustKeysBackupVersion(keysBackupVersion: KeysVersionResult, trust: Boolean, callback: MatrixCallback<Unit>)
|
||||
|
||||
/**
|
||||
* Set trust on a keys backup version.
|
||||
*
|
||||
* @param keysBackupVersion the backup version to check.
|
||||
* @param recoveryKey the recovery key to challenge with the key backup public key.
|
||||
* @param callback block called when the operations completes.
|
||||
*/
|
||||
fun trustKeysBackupVersionWithRecoveryKey(keysBackupVersion: KeysVersionResult, recoveryKey: String, callback: MatrixCallback<Unit>)
|
||||
|
||||
/**
|
||||
* Set trust on a keys backup version.
|
||||
*
|
||||
* @param keysBackupVersion the backup version to check.
|
||||
* @param password the pass phrase to challenge with the keyBackupVersion public key.
|
||||
* @param callback block called when the operations completes.
|
||||
*/
|
||||
fun trustKeysBackupVersionWithPassphrase(keysBackupVersion: KeysVersionResult, password: String, callback: MatrixCallback<Unit>)
|
||||
|
||||
/**
|
||||
* Restore a backup with a recovery key from a given backup version stored on the homeserver.
|
||||
*
|
||||
* @param keysVersionResult the backup version to restore from.
|
||||
* @param recoveryKey the recovery key to decrypt the retrieved backup.
|
||||
* @param roomId the id of the room to get backup data from.
|
||||
* @param sessionId the id of the session to restore.
|
||||
* @param stepProgressListener the step progress listener
|
||||
* @param callback Callback. It provides the number of found keys and the number of successfully imported keys.
|
||||
*/
|
||||
fun restoreKeysWithRecoveryKey(keysVersionResult: KeysVersionResult, recoveryKey: String, roomId: String?, sessionId: String?, stepProgressListener: StepProgressListener?, callback: MatrixCallback<ImportRoomKeysResult>)
|
||||
|
||||
/**
|
||||
* Restore a backup with a password from a given backup version stored on the homeserver.
|
||||
*
|
||||
* @param keysBackupVersion the backup version to restore from.
|
||||
* @param password the password to decrypt the retrieved backup.
|
||||
* @param roomId the id of the room to get backup data from.
|
||||
* @param sessionId the id of the session to restore.
|
||||
* @param stepProgressListener the step progress listener
|
||||
* @param callback Callback. It provides the number of found keys and the number of successfully imported keys.
|
||||
*/
|
||||
fun restoreKeyBackupWithPassword(keysBackupVersion: KeysVersionResult, password: String, roomId: String?, sessionId: String?, stepProgressListener: StepProgressListener?, callback: MatrixCallback<ImportRoomKeysResult>)
|
||||
|
||||
val keysBackupVersion: KeysVersionResult?
|
||||
val currentBackupVersion: String?
|
||||
val isEnabled: Boolean
|
||||
val isStucked: Boolean
|
||||
val state: KeysBackupState
|
||||
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.api.session.crypto.keysbackup
|
||||
|
||||
/**
|
||||
* E2e keys backup states.
|
||||
*
|
||||
* <pre>
|
||||
* |
|
||||
* V deleteKeyBackupVersion (on current backup)
|
||||
* +----------------------> UNKNOWN <-------------
|
||||
* | |
|
||||
* | | checkAndStartKeysBackup (at startup or on new verified device or a new detected backup)
|
||||
* | V
|
||||
* | CHECKING BACKUP
|
||||
* | |
|
||||
* | Network error |
|
||||
* +<----------+----------------+-------> DISABLED <----------------------+
|
||||
* | | | | |
|
||||
* | | | | createKeysBackupVersion |
|
||||
* | V | V |
|
||||
* +<--- WRONG VERSION | ENABLING |
|
||||
* | ^ | | |
|
||||
* | | V ok | error |
|
||||
* | | +------> READY <--------+----------------------------+
|
||||
* V | | |
|
||||
* NOT TRUSTED | | | on new key
|
||||
* | | V
|
||||
* | | WILL BACK UP (waiting a random duration)
|
||||
* | | |
|
||||
* | | |
|
||||
* | | ok V
|
||||
* | +----- BACKING UP
|
||||
* | |
|
||||
* | Error |
|
||||
* +<---------------+
|
||||
* </pre>
|
||||
*/
|
||||
enum class KeysBackupState {
|
||||
// Need to check the current backup version on the homeserver
|
||||
Unknown,
|
||||
// Checking if backup is enabled on home server
|
||||
CheckingBackUpOnHomeserver,
|
||||
// Backup has been stopped because a new backup version has been detected on the homeserver
|
||||
WrongBackUpVersion,
|
||||
// Backup from this device is not enabled
|
||||
Disabled,
|
||||
// There is a backup available on the homeserver but it is not trusted.
|
||||
// It is not trusted because the signature is invalid or the device that created it is not verified
|
||||
// Use [KeysBackup.getKeysBackupTrust()] to get trust details.
|
||||
// Consequently, the backup from this device is not enabled.
|
||||
NotTrusted,
|
||||
// Backup is being enabled: the backup version is being created on the homeserver
|
||||
Enabling,
|
||||
// Backup is enabled and ready to send backup to the homeserver
|
||||
ReadyToBackUp,
|
||||
// e2e keys are going to be sent to the homeserver
|
||||
WillBackUp,
|
||||
// e2e keys are being sent to the homeserver
|
||||
BackingUp
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* 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
|
||||
|
||||
interface KeysBackupStateListener {
|
||||
|
||||
/**
|
||||
* The keys backup state has changed
|
||||
* @param newState the new state
|
||||
*/
|
||||
fun onStateChange(newState: KeysBackupState)
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.api.session.crypto.keyshare
|
||||
|
||||
import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest
|
||||
import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequestCancellation
|
||||
|
||||
/**
|
||||
* Room keys events listener
|
||||
*/
|
||||
interface RoomKeysRequestListener {
|
||||
/**
|
||||
* An room key request has been received.
|
||||
*
|
||||
* @param request the request
|
||||
*/
|
||||
fun onRoomKeyRequest(request: IncomingRoomKeyRequest)
|
||||
|
||||
/**
|
||||
* A room key request cancellation has been received.
|
||||
*
|
||||
* @param request the cancellation request
|
||||
*/
|
||||
fun onRoomKeyRequestCancellation(request: IncomingRoomKeyRequestCancellation)
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package im.vector.matrix.android.api.session.crypto.sas
|
||||
|
||||
enum class CancelCode(val value: String, val humanReadable: String) {
|
||||
User("m.user", "the user cancelled the verification"),
|
||||
Timeout("m.timeout", "the verification process timed out"),
|
||||
UnknownTransaction("m.unknown_transaction", "the device does not know about that transaction"),
|
||||
UnknownMethod("m.unknown_method", "the device can’t agree on a key agreement, hash, MAC, or SAS method"),
|
||||
MismatchedCommitment("m.mismatched_commitment", "the hash commitment did not match"),
|
||||
MismatchedSas("m.mismatched_sas", "the SAS did not match"),
|
||||
UnexpectedMessage("m.unexpected_message", "the device received an unexpected message"),
|
||||
InvalidMessage("m.invalid_message", "an invalid message was received"),
|
||||
MismatchedKeys("m.key_mismatch", "Key mismatch"),
|
||||
UserMismatchError("m.user_error", "User mismatch")
|
||||
}
|
||||
|
||||
fun safeValueOf(code: String?): CancelCode {
|
||||
return CancelCode.values().firstOrNull { code == it.value } ?: CancelCode.User
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.api.session.crypto.sas
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
|
||||
data class EmojiRepresentation(val emoji: String,
|
||||
@StringRes val nameResId: Int)
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.api.session.crypto.sas
|
||||
|
||||
interface IncomingSasVerificationTransaction {
|
||||
val uxState: UxState
|
||||
|
||||
fun performAccept()
|
||||
|
||||
enum class UxState {
|
||||
UNKNOWN,
|
||||
SHOW_ACCEPT,
|
||||
WAIT_FOR_KEY_AGREEMENT,
|
||||
SHOW_SAS,
|
||||
WAIT_FOR_VERIFICATION,
|
||||
VERIFIED,
|
||||
CANCELLED_BY_ME,
|
||||
CANCELLED_BY_OTHER
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.api.session.crypto.sas
|
||||
|
||||
object SasMode {
|
||||
const val DECIMAL = "decimal"
|
||||
const val EMOJI = "emoji"
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.api.session.crypto.sas
|
||||
|
||||
interface OutgoingSasVerificationRequest {
|
||||
val uxState: UxState
|
||||
|
||||
enum class UxState {
|
||||
UNKNOWN,
|
||||
WAIT_FOR_START,
|
||||
WAIT_FOR_KEY_AGREEMENT,
|
||||
SHOW_SAS,
|
||||
WAIT_FOR_VERIFICATION,
|
||||
VERIFIED,
|
||||
CANCELLED_BY_ME,
|
||||
CANCELLED_BY_OTHER
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.api.session.crypto.sas
|
||||
|
||||
interface SasVerificationService {
|
||||
fun addListener(listener: SasVerificationListener)
|
||||
|
||||
fun removeListener(listener: SasVerificationListener)
|
||||
|
||||
fun markedLocallyAsManuallyVerified(userId: String, deviceID: String)
|
||||
|
||||
fun getExistingTransaction(otherUser: String, tid: String): SasVerificationTransaction?
|
||||
|
||||
fun beginKeyVerificationSAS(userId: String, deviceID: String): String?
|
||||
|
||||
fun beginKeyVerification(method: String, userId: String, deviceID: String): String?
|
||||
|
||||
// fun transactionUpdated(tx: SasVerificationTransaction)
|
||||
|
||||
interface SasVerificationListener {
|
||||
fun transactionCreated(tx: SasVerificationTransaction)
|
||||
fun transactionUpdated(tx: SasVerificationTransaction)
|
||||
fun markedAsManuallyVerified(userId: String, deviceId: String)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.api.session.crypto.sas
|
||||
|
||||
interface SasVerificationTransaction {
|
||||
val state: SasVerificationTxState
|
||||
|
||||
val cancelledReason: CancelCode?
|
||||
|
||||
val transactionId: String
|
||||
|
||||
val otherUserId: String
|
||||
|
||||
var otherDeviceId: String?
|
||||
|
||||
val isIncoming: Boolean
|
||||
|
||||
fun supportsEmoji(): Boolean
|
||||
|
||||
fun supportsDecimal(): Boolean
|
||||
|
||||
fun getEmojiCodeRepresentation(): List<EmojiRepresentation>
|
||||
|
||||
fun getDecimalCodeRepresentation(): String
|
||||
|
||||
/**
|
||||
* User wants to cancel the transaction
|
||||
*/
|
||||
fun cancel()
|
||||
|
||||
/**
|
||||
* To be called by the client when the user has verified that
|
||||
* both short codes do match
|
||||
*/
|
||||
fun userHasVerifiedShortCode()
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.api.session.crypto.sas
|
||||
|
||||
enum class SasVerificationTxState {
|
||||
None,
|
||||
// I have started a verification request
|
||||
SendingStart,
|
||||
Started,
|
||||
// Other user/device sent me a request
|
||||
OnStarted,
|
||||
// I have accepted a request started by the other user/device
|
||||
SendingAccept,
|
||||
Accepted,
|
||||
// My request has been accepted by the other user/device
|
||||
OnAccepted,
|
||||
// I have sent my public key
|
||||
SendingKey,
|
||||
KeySent,
|
||||
// The other user/device has sent me his public key
|
||||
OnKeyReceived,
|
||||
// Short code is ready to be displayed
|
||||
ShortCodeReady,
|
||||
// I have compared the code and manually said that they match
|
||||
ShortCodeAccepted,
|
||||
|
||||
SendingMac,
|
||||
MacSent,
|
||||
Verifying,
|
||||
Verified,
|
||||
|
||||
//Global: The verification has been cancelled (by me or other), see cancelReason for details
|
||||
Cancelled,
|
||||
OnCancelled
|
||||
}
|
|
@ -16,14 +16,17 @@
|
|||
|
||||
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.*
|
||||
|
||||
typealias Content = Map<String, @JvmSuppressWildcards Any>
|
||||
typealias Content = JsonDict
|
||||
|
||||
/**
|
||||
* This methods is a facility method to map a json content to a model.
|
||||
|
@ -81,10 +84,146 @@ data class Event(
|
|||
* @return true if event is state event.
|
||||
*/
|
||||
fun isStateEvent(): Boolean {
|
||||
return EventType.isStateEvent(type)
|
||||
return EventType.isStateEvent(getClearType())
|
||||
}
|
||||
|
||||
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
|
||||
var mClearEvent: Event? = null
|
||||
private set
|
||||
|
||||
/**
|
||||
* 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
|
||||
var mCryptoError: MXCryptoError? = null
|
||||
private set
|
||||
|
||||
/**
|
||||
* @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.
|
||||
*
|
||||
* @param decryptionResult the decryption result, including the plaintext and some key info.
|
||||
*/
|
||||
internal fun setClearData(decryptionResult: MXEventDecryptionResult?) {
|
||||
mClearEvent = null
|
||||
if (decryptionResult != null) {
|
||||
if (decryptionResult.clearEvent != null) {
|
||||
val adapter = MoshiProvider.providesMoshi().adapter(Event::class.java)
|
||||
mClearEvent = adapter.fromJsonValue(decryptionResult.clearEvent)
|
||||
|
||||
}
|
||||
mClearEvent?.apply {
|
||||
mSenderCurve25519Key = decryptionResult.senderCurve25519Key
|
||||
mClaimedEd25519Key = decryptionResult.claimedEd25519Key
|
||||
mForwardingCurve25519KeyChain = decryptionResult.forwardingCurve25519KeyChain
|
||||
try {
|
||||
// Add "m.relates_to" data from e2e event to the unencrypted event
|
||||
// TODO
|
||||
//if (getWireContent().getAsJsonObject().has("m.relates_to")) {
|
||||
// clearEvent!!.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 mClearEvent?.type ?: type
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the event content
|
||||
*/
|
||||
fun getClearContent(): Content? {
|
||||
return mClearEvent?.content ?: content
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the linked crypto error
|
||||
*/
|
||||
fun getCryptoError(): MXCryptoError? {
|
||||
return mCryptoError
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the linked crypto error
|
||||
*
|
||||
* @param error the new crypto error.
|
||||
*/
|
||||
fun setCryptoError(error: MXCryptoError?) {
|
||||
mCryptoError = error
|
||||
if (null != error) {
|
||||
mClearEvent = null
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -36,8 +36,6 @@ object EventType {
|
|||
const val FULLY_READ = "m.fully_read"
|
||||
const val PLUMBING = "m.room.plumbing"
|
||||
const val BOT_OPTIONS = "m.room.bot.options"
|
||||
const val KEY_REQUEST = "m.room_key_request"
|
||||
const val FORWARDED_ROOM_KEY = "m.forwarded_room_key"
|
||||
const val PREVIEW_URLS = "org.matrix.room.preview_urls"
|
||||
|
||||
// State Events
|
||||
|
@ -65,11 +63,20 @@ 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"
|
||||
|
||||
// Relation Events
|
||||
|
||||
const val REACTION = "m.reaction"
|
||||
|
||||
|
||||
private val STATE_EVENTS = listOf(
|
||||
STATE_ROOM_NAME,
|
||||
STATE_ROOM_TOPIC,
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
package im.vector.matrix.android.api.session.room
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import im.vector.matrix.android.api.session.room.crypto.RoomCryptoService
|
||||
import im.vector.matrix.android.api.session.room.members.MembershipService
|
||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||
import im.vector.matrix.android.api.session.room.model.relation.RelationService
|
||||
|
@ -28,7 +29,14 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineService
|
|||
/**
|
||||
* This interface defines methods to interact within a room.
|
||||
*/
|
||||
interface Room : TimelineService, SendService, ReadService, MembershipService, StateService , RelationService{
|
||||
interface Room :
|
||||
TimelineService,
|
||||
SendService,
|
||||
ReadService,
|
||||
MembershipService,
|
||||
StateService,
|
||||
RelationService,
|
||||
RoomCryptoService {
|
||||
|
||||
/**
|
||||
* The roomId of this room
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* 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
|
||||
|
||||
interface RoomCryptoService {
|
||||
|
||||
fun isEncrypted(): Boolean
|
||||
|
||||
fun encryptionAlgorithm(): String?
|
||||
|
||||
fun shouldEncryptForInvitedMembers(): Boolean
|
||||
}
|
|
@ -22,5 +22,5 @@ enum class RoomHistoryVisibility {
|
|||
@Json(name = "shared") SHARED,
|
||||
@Json(name = "invited") INVITED,
|
||||
@Json(name = "joined") JOINED,
|
||||
@Json(name = "word_readable") WORLD_READABLE
|
||||
@Json(name = "world_readable") WORLD_READABLE
|
||||
}
|
|
@ -23,5 +23,5 @@ import com.squareup.moshi.JsonClass
|
|||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class RoomHistoryVisibilityContent(
|
||||
@Json(name = "history_visibility") val historyVisibility: RoomHistoryVisibility
|
||||
@Json(name = "history_visibility") val historyVisibility: RoomHistoryVisibility? = null
|
||||
)
|
|
@ -149,7 +149,7 @@ class CreateRoomParams {
|
|||
if (initialStates != null && !initialStates!!.isEmpty()) {
|
||||
val newInitialStates = ArrayList<Event>()
|
||||
for (event in initialStates!!) {
|
||||
if (event.type != EventType.STATE_HISTORY_VISIBILITY) {
|
||||
if (event.getClearType() != EventType.STATE_HISTORY_VISIBILITY) {
|
||||
newInitialStates.add(event)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,10 +26,19 @@ enum class SendState {
|
|||
SENDING,
|
||||
// the event has been sent
|
||||
SENT,
|
||||
SYNCED;
|
||||
// the event has been received from server
|
||||
SYNCED,
|
||||
// The event failed to be sent
|
||||
UNDELIVERED,
|
||||
// the event failed to be sent because some unknown devices have been found while encrypting it
|
||||
FAILED_UNKNOWN_DEVICES;
|
||||
|
||||
fun isSent(): Boolean {
|
||||
return this == SENT || this == SYNCED
|
||||
}
|
||||
|
||||
fun hasFailed(): Boolean {
|
||||
return this == UNDELIVERED || this == FAILED_UNKNOWN_DEVICES
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -64,6 +64,6 @@ data class TimelineEvent(
|
|||
}
|
||||
|
||||
fun isEncrypted() : Boolean {
|
||||
return EventType.ENCRYPTED == root.type
|
||||
return EventType.ENCRYPTED == root.getClearType()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.api.util
|
||||
|
||||
import arrow.core.*
|
||||
|
||||
inline fun <A> TryOf<A>.onError(f: (Throwable) -> Unit): Try<A> = fix()
|
||||
.fold(
|
||||
{
|
||||
f(it)
|
||||
Failure(it)
|
||||
},
|
||||
{ Success(it) }
|
||||
)
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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
|
||||
|
||||
import com.squareup.moshi.Types
|
||||
import java.lang.reflect.ParameterizedType
|
||||
|
||||
typealias JsonDict = Map<String, @JvmSuppressWildcards Any>
|
||||
|
||||
internal val JSON_DICT_PARAMETERIZED_TYPE: ParameterizedType = Types.newParameterizedType(Map::class.java, String::class.java, Any::class.java)
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
*
|
||||
* * 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_CRYPTO_NAME = "Crypto_Thread"
|
||||
|
||||
// TODO Remove and replace by Task
|
||||
internal object CryptoAsyncHelper {
|
||||
|
||||
private var uiHandler: Handler? = null
|
||||
private var cryptoBackgroundHandler: Handler? = null
|
||||
|
||||
fun getUiHandler(): Handler {
|
||||
return uiHandler
|
||||
?: Handler(Looper.getMainLooper())
|
||||
.also { uiHandler = it }
|
||||
}
|
||||
|
||||
|
||||
fun getDecryptBackgroundHandler(): Handler {
|
||||
return getCryptoBackgroundHandler()
|
||||
}
|
||||
|
||||
fun getEncryptBackgroundHandler(): Handler {
|
||||
return getCryptoBackgroundHandler()
|
||||
}
|
||||
|
||||
private fun getCryptoBackgroundHandler(): Handler {
|
||||
return cryptoBackgroundHandler
|
||||
?: createCryptoBackgroundHandler()
|
||||
.also { cryptoBackgroundHandler = it }
|
||||
}
|
||||
|
||||
private fun createCryptoBackgroundHandler(): Handler {
|
||||
val handlerThread = HandlerThread(THREAD_CRYPTO_NAME)
|
||||
handlerThread.start()
|
||||
return Handler(handlerThread.looper)
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.crypto
|
||||
|
||||
/**
|
||||
* Matrix algorithm value for olm.
|
||||
*/
|
||||
const val MXCRYPTO_ALGORITHM_OLM = "m.olm.v1.curve25519-aes-sha2"
|
||||
|
||||
/**
|
||||
* Matrix algorithm value for megolm.
|
||||
*/
|
||||
const val MXCRYPTO_ALGORITHM_MEGOLM = "m.megolm.v1.aes-sha2"
|
||||
|
||||
/**
|
||||
* Matrix algorithm value for megolm keys backup.
|
||||
*/
|
||||
const val MXCRYPTO_ALGORITHM_MEGOLM_BACKUP = "m.megolm_backup.v1.curve25519-aes-sha2"
|
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,332 @@
|
|||
/*
|
||||
* 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.actions.*
|
||||
import im.vector.matrix.android.internal.crypto.algorithms.megolm.MXMegolmDecryptionFactory
|
||||
import im.vector.matrix.android.internal.crypto.algorithms.megolm.MXMegolmEncryptionFactory
|
||||
import im.vector.matrix.android.internal.crypto.algorithms.olm.MXOlmDecryptionFactory
|
||||
import im.vector.matrix.android.internal.crypto.algorithms.olm.MXOlmEncryptionFactory
|
||||
import im.vector.matrix.android.internal.crypto.api.CryptoApi
|
||||
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.repository.WarnOnUnknownDeviceRepository
|
||||
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.tasks.*
|
||||
import im.vector.matrix.android.internal.crypto.verification.DefaultSasVerificationService
|
||||
import im.vector.matrix.android.internal.session.DefaultSession
|
||||
import im.vector.matrix.android.internal.session.cache.ClearCacheTask
|
||||
import im.vector.matrix.android.internal.session.cache.RealmClearCacheTask
|
||||
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) {
|
||||
get<CryptoManager>() as CryptoService
|
||||
}
|
||||
|
||||
//
|
||||
scope(DefaultSession.SCOPE) {
|
||||
OutgoingRoomKeyRequestManager(get(), get(), get())
|
||||
}
|
||||
|
||||
scope(DefaultSession.SCOPE) {
|
||||
IncomingRoomKeyRequestManager(get(), get(), get())
|
||||
}
|
||||
|
||||
scope(DefaultSession.SCOPE) {
|
||||
RoomDecryptorProvider(get(), get())
|
||||
}
|
||||
|
||||
scope(DefaultSession.SCOPE) {
|
||||
// Ensure OlmManager is loaded first
|
||||
get<OlmManager>()
|
||||
MXOlmDevice(get())
|
||||
}
|
||||
|
||||
// ObjectSigner
|
||||
scope(DefaultSession.SCOPE) {
|
||||
ObjectSigner(get(), get())
|
||||
}
|
||||
|
||||
// OneTimeKeysUploader
|
||||
scope(DefaultSession.SCOPE) {
|
||||
OneTimeKeysUploader(get(), get(), get(), get())
|
||||
}
|
||||
|
||||
// Actions
|
||||
scope(DefaultSession.SCOPE) {
|
||||
SetDeviceVerificationAction(get(), get(), get())
|
||||
}
|
||||
|
||||
// Device info
|
||||
scope(DefaultSession.SCOPE) {
|
||||
MyDeviceInfoHolder(get(), get(), get())
|
||||
}
|
||||
|
||||
scope(DefaultSession.SCOPE) {
|
||||
EnsureOlmSessionsForDevicesAction(get(), get())
|
||||
}
|
||||
|
||||
scope(DefaultSession.SCOPE) {
|
||||
EnsureOlmSessionsForUsersAction(get(), get(), get())
|
||||
}
|
||||
|
||||
scope(DefaultSession.SCOPE) {
|
||||
MegolmSessionDataImporter(get(), get(), get(), get())
|
||||
}
|
||||
|
||||
scope(DefaultSession.SCOPE) {
|
||||
MessageEncrypter(get(), get())
|
||||
}
|
||||
|
||||
|
||||
scope(DefaultSession.SCOPE) {
|
||||
WarnOnUnknownDeviceRepository()
|
||||
}
|
||||
|
||||
// Factories
|
||||
scope(DefaultSession.SCOPE) {
|
||||
MXMegolmDecryptionFactory(
|
||||
get(), get(), get(), get(), get(), get(), get(), get(), get()
|
||||
)
|
||||
}
|
||||
|
||||
scope(DefaultSession.SCOPE) {
|
||||
MXMegolmEncryptionFactory(
|
||||
get(), get(), get(), get(), get(), get(), get(), get(), get(), get()
|
||||
)
|
||||
}
|
||||
|
||||
scope(DefaultSession.SCOPE) {
|
||||
MXOlmDecryptionFactory(
|
||||
get(), get()
|
||||
)
|
||||
}
|
||||
|
||||
scope(DefaultSession.SCOPE) {
|
||||
MXOlmEncryptionFactory(
|
||||
get(), get(), get(), get(), get(), get()
|
||||
)
|
||||
}
|
||||
|
||||
// CryptoManager
|
||||
scope(DefaultSession.SCOPE) {
|
||||
CryptoManager(
|
||||
credentials = get(),
|
||||
myDeviceInfoHolder = get(),
|
||||
cryptoStore = get(),
|
||||
olmDevice = get(),
|
||||
cryptoConfig = get(),
|
||||
deviceListManager = get(),
|
||||
keysBackup = get(),
|
||||
objectSigner = get(),
|
||||
oneTimeKeysUploader = get(),
|
||||
roomDecryptorProvider = get(),
|
||||
sasVerificationService = get(),
|
||||
incomingRoomKeyRequestManager = get(),
|
||||
outgoingRoomKeyRequestManager = get(),
|
||||
olmManager = get(),
|
||||
setDeviceVerificationAction = get(),
|
||||
megolmSessionDataImporter = get(),
|
||||
warnOnUnknownDevicesRepository = get(),
|
||||
megolmEncryptionFactory = get(),
|
||||
olmEncryptionFactory = get(),
|
||||
deleteDeviceTask = get(),
|
||||
// Tasks
|
||||
getDevicesTask = get(),
|
||||
setDeviceNameTask = get(),
|
||||
uploadKeysTask = get(),
|
||||
loadRoomMembersTask = get(),
|
||||
clearCryptoDataTask = get("ClearTaskCryptoCache"),
|
||||
monarchy = get(),
|
||||
coroutineDispatchers = get(),
|
||||
taskExecutor = 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())
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
scope(DefaultSession.SCOPE, name = "ClearTaskCryptoCache") {
|
||||
RealmClearCacheTask(get("CryptoRealmConfiguration")) as ClearCacheTask
|
||||
}
|
||||
|
||||
/* ==========================================================================================
|
||||
* Keys backup
|
||||
* ========================================================================================== */
|
||||
|
||||
scope(DefaultSession.SCOPE) {
|
||||
val retrofit: Retrofit = get()
|
||||
retrofit.create(RoomKeysApi::class.java)
|
||||
}
|
||||
|
||||
scope(DefaultSession.SCOPE) {
|
||||
KeysBackup(
|
||||
// Credentials
|
||||
get(),
|
||||
// CryptoStore
|
||||
get(),
|
||||
get(),
|
||||
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(), get(), get(), get())
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,508 @@
|
|||
/*
|
||||
* 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 arrow.core.Try
|
||||
import im.vector.matrix.android.api.MatrixPatterns
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.api.util.onError
|
||||
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.store.IMXCryptoStore
|
||||
import im.vector.matrix.android.internal.crypto.tasks.DownloadKeysForUsersTask
|
||||
import im.vector.matrix.android.internal.session.sync.SyncTokenStore
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
|
||||
// Legacy name: MXDeviceList
|
||||
internal class DeviceListManager(private val cryptoStore: IMXCryptoStore,
|
||||
private val olmDevice: MXOlmDevice,
|
||||
private val syncTokenStore: SyncTokenStore,
|
||||
private val credentials: Credentials,
|
||||
private val downloadKeysForUsersTask: DownloadKeysForUsersTask) {
|
||||
|
||||
// HS not ready for retry
|
||||
private val notReadyToRetryHS = HashSet<String>()
|
||||
|
||||
init {
|
||||
var isUpdated = false
|
||||
val deviceTrackingStatuses = cryptoStore.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[userId] = TRACKING_STATUS_PENDING_DOWNLOAD
|
||||
isUpdated = true
|
||||
}
|
||||
}
|
||||
if (isUpdated) {
|
||||
cryptoStore.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(notReadyToRetryHS) {
|
||||
res = !notReadyToRetryHS.contains(userId.substring(userId.lastIndexOf(":") + 1))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## canRetryKeysDownload() failed")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the unavailable server lists
|
||||
*/
|
||||
private fun clearUnavailableServersList() {
|
||||
synchronized(notReadyToRetryHS) {
|
||||
notReadyToRetryHS.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 = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
|
||||
|
||||
for (userId in userIds) {
|
||||
if (!deviceTrackingStatuses.containsKey(userId) || TRACKING_STATUS_NOT_TRACKED == deviceTrackingStatuses[userId]) {
|
||||
Timber.v("## startTrackingDeviceList() : Now tracking device list for $userId")
|
||||
deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD
|
||||
isUpdated = true
|
||||
}
|
||||
}
|
||||
|
||||
if (isUpdated) {
|
||||
cryptoStore.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 = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
|
||||
|
||||
if (changed?.isNotEmpty() == true) {
|
||||
for (userId in changed) {
|
||||
if (deviceTrackingStatuses.containsKey(userId)) {
|
||||
Timber.v("## invalidateUserDeviceList() : Marking device list outdated for $userId")
|
||||
deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD
|
||||
isUpdated = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (left?.isNotEmpty() == true) {
|
||||
for (userId in left) {
|
||||
if (deviceTrackingStatuses.containsKey(userId)) {
|
||||
Timber.v("## invalidateUserDeviceList() : No longer tracking device list for $userId")
|
||||
deviceTrackingStatuses[userId] = TRACKING_STATUS_NOT_TRACKED
|
||||
isUpdated = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isUpdated) {
|
||||
cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This will flag each user whose devices we are tracking as in need of an
|
||||
* + update
|
||||
*/
|
||||
fun invalidateAllDeviceLists() {
|
||||
handleDeviceListsChanges(ArrayList(cryptoStore.getDeviceTrackingStatuses().keys), null)
|
||||
}
|
||||
|
||||
/**
|
||||
* The keys download failed
|
||||
*
|
||||
* @param userIds the user ids list
|
||||
*/
|
||||
private fun onKeysDownloadFailed(userIds: List<String>) {
|
||||
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
|
||||
for (userId in userIds) {
|
||||
deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD
|
||||
}
|
||||
cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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>>?): MXUsersDevicesMap<MXDeviceInfo> {
|
||||
if (failures != null) {
|
||||
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(notReadyToRetryHS) {
|
||||
notReadyToRetryHS.add(k)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
|
||||
val usersDevicesInfoMap = MXUsersDevicesMap<MXDeviceInfo>()
|
||||
for (userId in userIds) {
|
||||
val devices = cryptoStore.getUserDevices(userId)
|
||||
if (null == devices) {
|
||||
if (canRetryKeysDownload(userId)) {
|
||||
deviceTrackingStatuses[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[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[userId] = TRACKING_STATUS_UP_TO_DATE
|
||||
Timber.v("Device list for $userId now up to date")
|
||||
}
|
||||
// And the response result
|
||||
usersDevicesInfoMap.setObjects(devices, userId)
|
||||
}
|
||||
}
|
||||
cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses)
|
||||
return usersDevicesInfoMap
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
suspend fun downloadKeys(userIds: List<String>?, forceDownload: Boolean): Try<MXUsersDevicesMap<MXDeviceInfo>> {
|
||||
Timber.v("## 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 = cryptoStore.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 (TRACKING_STATUS_UP_TO_DATE != status && TRACKING_STATUS_UNREACHABLE_SERVER != status) {
|
||||
downloadUsers.add(userId)
|
||||
} else {
|
||||
val devices = cryptoStore.getUserDevices(userId)
|
||||
// should always be true
|
||||
if (devices != null) {
|
||||
stored.setObjects(devices, userId)
|
||||
} else {
|
||||
downloadUsers.add(userId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return if (downloadUsers.isEmpty()) {
|
||||
Timber.v("## downloadKeys() : no new user device")
|
||||
Try.just(stored)
|
||||
} else {
|
||||
Timber.v("## downloadKeys() : starts")
|
||||
val t0 = System.currentTimeMillis()
|
||||
doKeyDownloadForUsers(downloadUsers)
|
||||
.map {
|
||||
Timber.v("## downloadKeys() : doKeyDownloadForUsers succeeds after " + (System.currentTimeMillis() - t0) + " ms")
|
||||
it.addEntriesFromMap(stored)
|
||||
it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download the devices keys for a set of users.
|
||||
*
|
||||
* @param downloadUsers the user ids list
|
||||
*/
|
||||
private suspend fun doKeyDownloadForUsers(downloadUsers: MutableList<String>): Try<MXUsersDevicesMap<MXDeviceInfo>> {
|
||||
Timber.v("## doKeyDownloadForUsers() : doKeyDownloadForUsers $downloadUsers")
|
||||
// get the user ids which did not already trigger a keys download
|
||||
val filteredUsers = downloadUsers.filter { MatrixPatterns.isUserId(it) }
|
||||
if (filteredUsers.isEmpty()) {
|
||||
// trigger nothing
|
||||
return Try.just(MXUsersDevicesMap())
|
||||
}
|
||||
val params = DownloadKeysForUsersTask.Params(filteredUsers, syncTokenStore.getLastToken())
|
||||
return downloadKeysForUsersTask.execute(params)
|
||||
.map { response ->
|
||||
Timber.v("## doKeyDownloadForUsers() : Got keys for " + filteredUsers.size + " users")
|
||||
for (userId in filteredUsers) {
|
||||
val devices = response.deviceKeys?.get(userId)
|
||||
Timber.v("## doKeyDownloadForUsers() : Got keys for $userId : $devices")
|
||||
if (devices != null) {
|
||||
val mutableDevices = HashMap(devices)
|
||||
val deviceIds = ArrayList(mutableDevices.keys)
|
||||
for (deviceId in deviceIds) {
|
||||
// Get the potential previously store device keys for this device
|
||||
val previouslyStoredDeviceKeys = cryptoStore.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, credentials.deviceId) && TextUtils.equals(userId, credentials.userId)) {
|
||||
deviceInfo.verified = 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]!!.verified = previouslyStoredDeviceKeys.verified
|
||||
}
|
||||
}
|
||||
// Update the store
|
||||
// Note that devices which aren't in the response will be removed from the stores
|
||||
cryptoStore.storeUserDevices(userId, mutableDevices)
|
||||
}
|
||||
}
|
||||
onKeysDownloadSucceed(filteredUsers, response.failures)
|
||||
}
|
||||
.onError {
|
||||
Timber.e(it, "##doKeyDownloadForUsers(): error")
|
||||
onKeysDownloadFailed(filteredUsers)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
olmDevice.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.
|
||||
*/
|
||||
suspend fun refreshOutdatedDeviceLists() {
|
||||
val users = ArrayList<String>()
|
||||
|
||||
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
|
||||
|
||||
for (userId in deviceTrackingStatuses.keys) {
|
||||
if (TRACKING_STATUS_PENDING_DOWNLOAD == deviceTrackingStatuses[userId]) {
|
||||
users.add(userId)
|
||||
}
|
||||
}
|
||||
|
||||
if (users.size == 0) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses)
|
||||
doKeyDownloadForUsers(users)
|
||||
.fold(
|
||||
{
|
||||
Timber.e(it, "## refreshOutdatedDeviceLists() : ERROR updating device keys for users $users")
|
||||
},
|
||||
{
|
||||
Timber.v("## refreshOutdatedDeviceLists() : done")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* State transition diagram for DeviceList.deviceTrackingStatus
|
||||
* <pre>
|
||||
*
|
||||
* |
|
||||
* stopTrackingDeviceList V
|
||||
* +---------------------> NOT_TRACKED
|
||||
* | |
|
||||
* +<--------------------+ | startTrackingDeviceList
|
||||
* | | V
|
||||
* | +-------------> PENDING_DOWNLOAD <--------------------+-+
|
||||
* | | ^ | | |
|
||||
* | | restart download | | start download | | invalidateUserDeviceList
|
||||
* | | client failed | | | |
|
||||
* | | | V | |
|
||||
* | +------------ DOWNLOAD_IN_PROGRESS -------------------+ |
|
||||
* | | | |
|
||||
* +<-------------------+ | download successful |
|
||||
* ^ V |
|
||||
* +----------------------- UP_TO_DATE ------------------------+
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
|
||||
const val TRACKING_STATUS_NOT_TRACKED = -1
|
||||
const val TRACKING_STATUS_PENDING_DOWNLOAD = 1
|
||||
const val TRACKING_STATUS_DOWNLOAD_IN_PROGRESS = 2
|
||||
const val TRACKING_STATUS_UP_TO_DATE = 3
|
||||
const val TRACKING_STATUS_UNREACHABLE_SERVER = 4
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* 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 userId: String? = null
|
||||
|
||||
/**
|
||||
* The device id
|
||||
*/
|
||||
var deviceId: String? = null
|
||||
|
||||
/**
|
||||
* The request id
|
||||
*/
|
||||
var requestId: String? = null
|
||||
|
||||
/**
|
||||
* The request body
|
||||
*/
|
||||
var requestBody: RoomKeyRequestBody? = null
|
||||
|
||||
/**
|
||||
* The runnable to call to accept to share the keys
|
||||
*/
|
||||
@Transient
|
||||
var share: Runnable? = null
|
||||
|
||||
/**
|
||||
* The runnable to call to ignore the key share request.
|
||||
*/
|
||||
@Transient
|
||||
var ignore: Runnable? = null
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param event the event
|
||||
*/
|
||||
constructor(event: Event) {
|
||||
userId = event.sender
|
||||
val roomKeyShareRequest = event.getClearContent().toModel<RoomKeyShareRequest>()!!
|
||||
deviceId = roomKeyShareRequest.requestingDeviceId
|
||||
requestId = roomKeyShareRequest.requestId
|
||||
requestBody = if (null != roomKeyShareRequest.body) roomKeyShareRequest.body else RoomKeyRequestBody()
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for object creation from crypto store
|
||||
*/
|
||||
constructor()
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright 2016 OpenMarket Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.crypto
|
||||
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
|
||||
/**
|
||||
* IncomingRoomKeyRequestCancellation describes the incoming room key cancellation.
|
||||
*/
|
||||
class IncomingRoomKeyRequestCancellation(event: Event) : IncomingRoomKeyRequest(event) {
|
||||
|
||||
init {
|
||||
requestBody = null
|
||||
}
|
||||
}
|
|
@ -0,0 +1,202 @@
|
|||
/*
|
||||
* 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.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.model.rest.RoomKeyShare
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
internal class IncomingRoomKeyRequestManager(
|
||||
private val credentials: Credentials,
|
||||
private val cryptoStore: IMXCryptoStore,
|
||||
private val roomDecryptorProvider: RoomDecryptorProvider) {
|
||||
|
||||
|
||||
// list of IncomingRoomKeyRequests/IncomingRoomKeyRequestCancellations
|
||||
// we received in the current sync.
|
||||
private val receivedRoomKeyRequests = ArrayList<IncomingRoomKeyRequest>()
|
||||
private val receivedRoomKeyRequestCancellations = ArrayList<IncomingRoomKeyRequestCancellation>()
|
||||
|
||||
// the listeners
|
||||
private val roomKeysRequestListeners: MutableSet<RoomKeysRequestListener> = HashSet()
|
||||
|
||||
init {
|
||||
receivedRoomKeyRequests.addAll(cryptoStore.getPendingIncomingRoomKeyRequests())
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when we get an m.room_key_request event
|
||||
* It must be called on CryptoThread
|
||||
*
|
||||
* @param event the announcement event.
|
||||
*/
|
||||
suspend fun onRoomKeyRequestEvent(event: Event) {
|
||||
val roomKeyShare = event.getClearContent().toModel<RoomKeyShare>()
|
||||
when (roomKeyShare?.action) {
|
||||
RoomKeyShare.ACTION_SHARE_REQUEST -> receivedRoomKeyRequests.add(IncomingRoomKeyRequest(event))
|
||||
RoomKeyShare.ACTION_SHARE_CANCELLATION -> receivedRoomKeyRequestCancellations.add(IncomingRoomKeyRequestCancellation(event))
|
||||
else -> Timber.e("## onRoomKeyRequestEvent() : unsupported action " + roomKeyShare?.action)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process any m.room_key_request events which were queued up during the
|
||||
* current sync.
|
||||
* It must be called on CryptoThread
|
||||
*/
|
||||
fun processReceivedRoomKeyRequests() {
|
||||
val roomKeyRequestsToProcess = ArrayList(receivedRoomKeyRequests)
|
||||
receivedRoomKeyRequests.clear()
|
||||
for (request in roomKeyRequestsToProcess) {
|
||||
val userId = request.userId
|
||||
val deviceId = request.deviceId
|
||||
val body = request.requestBody
|
||||
val roomId = body!!.roomId
|
||||
val alg = body.algorithm
|
||||
|
||||
Timber.v("m.room_key_request from " + userId + ":" + deviceId + " for " + roomId + " / " + body.sessionId + " id " + request.requestId)
|
||||
if (userId == null || credentials.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 = roomDecryptorProvider.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!!)
|
||||
cryptoStore.deleteIncomingRoomKeyRequest(request)
|
||||
continue
|
||||
}
|
||||
|
||||
if (TextUtils.equals(deviceId, credentials.deviceId) && TextUtils.equals(credentials.userId, userId)) {
|
||||
Timber.v("## processReceivedRoomKeyRequests() : oneself device - ignored")
|
||||
cryptoStore.deleteIncomingRoomKeyRequest(request)
|
||||
continue
|
||||
}
|
||||
request.share = Runnable {
|
||||
decryptor.shareKeysWithDevice(request)
|
||||
cryptoStore.deleteIncomingRoomKeyRequest(request)
|
||||
}
|
||||
request.ignore = Runnable {
|
||||
cryptoStore.deleteIncomingRoomKeyRequest(request)
|
||||
}
|
||||
// if the device is verified already, share the keys
|
||||
val device = cryptoStore.getUserDevice(deviceId!!, userId)
|
||||
if (device != null) {
|
||||
if (device.isVerified) {
|
||||
Timber.v("## processReceivedRoomKeyRequests() : device is already verified: sharing keys")
|
||||
cryptoStore.deleteIncomingRoomKeyRequest(request)
|
||||
request.share?.run()
|
||||
continue
|
||||
}
|
||||
|
||||
if (device.isBlocked) {
|
||||
Timber.v("## processReceivedRoomKeyRequests() : device is blocked -> ignored")
|
||||
cryptoStore.deleteIncomingRoomKeyRequest(request)
|
||||
continue
|
||||
}
|
||||
}
|
||||
cryptoStore.storeIncomingRoomKeyRequest(request)
|
||||
onRoomKeyRequest(request)
|
||||
}
|
||||
|
||||
var receivedRoomKeyRequestCancellations: List<IncomingRoomKeyRequestCancellation>? = null
|
||||
|
||||
synchronized(this.receivedRoomKeyRequestCancellations) {
|
||||
if (!this.receivedRoomKeyRequestCancellations.isEmpty()) {
|
||||
receivedRoomKeyRequestCancellations = this.receivedRoomKeyRequestCancellations.toList()
|
||||
this.receivedRoomKeyRequestCancellations.clear()
|
||||
}
|
||||
}
|
||||
|
||||
if (null != receivedRoomKeyRequestCancellations) {
|
||||
for (request in receivedRoomKeyRequestCancellations!!) {
|
||||
Timber.v("## ## processReceivedRoomKeyRequests() : m.room_key_request cancellation for " + request.userId
|
||||
+ ":" + request.deviceId + " id " + request.requestId)
|
||||
|
||||
// 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)
|
||||
cryptoStore.deleteIncomingRoomKeyRequest(request)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch onRoomKeyRequest
|
||||
*
|
||||
* @param request the request
|
||||
*/
|
||||
private fun onRoomKeyRequest(request: IncomingRoomKeyRequest) {
|
||||
synchronized(roomKeysRequestListeners) {
|
||||
for (listener in roomKeysRequestListeners) {
|
||||
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(roomKeysRequestListeners) {
|
||||
for (listener in roomKeysRequestListeners) {
|
||||
try {
|
||||
listener.onRoomKeyRequestCancellation(request)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## onRoomKeyRequestCancellation() failed")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun addRoomKeysRequestListener(listener: RoomKeysRequestListener) {
|
||||
synchronized(roomKeysRequestListeners) {
|
||||
roomKeysRequestListeners.add(listener)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeRoomKeysRequestListener(listener: RoomKeysRequestListener) {
|
||||
synchronized(roomKeysRequestListeners) {
|
||||
roomKeysRequestListeners.remove(listener)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* 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
|
||||
|
||||
// TODO Update comment
|
||||
internal object MXCryptoAlgorithms {
|
||||
|
||||
/**
|
||||
* Get the class implementing encryption for the provided algorithm.
|
||||
*
|
||||
* @param algorithm the algorithm tag.
|
||||
* @return A class implementing 'IMXEncrypting'.
|
||||
*/
|
||||
fun hasEncryptorClassForAlgorithm(algorithm: String?): Boolean {
|
||||
return when (algorithm) {
|
||||
MXCRYPTO_ALGORITHM_MEGOLM,
|
||||
MXCRYPTO_ALGORITHM_OLM -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the class implementing decryption for the provided algorithm.
|
||||
*
|
||||
* @param algorithm the algorithm tag.
|
||||
* @return A class implementing 'IMXDecrypting'.
|
||||
*/
|
||||
|
||||
fun hasDecryptorClassForAlgorithm(algorithm: String?): Boolean {
|
||||
return when (algorithm) {
|
||||
MXCRYPTO_ALGORITHM_MEGOLM,
|
||||
MXCRYPTO_ALGORITHM_OLM -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The list of registered algorithms.
|
||||
*/
|
||||
fun supportedAlgorithms(): List<String> {
|
||||
return listOf(MXCRYPTO_ALGORITHM_MEGOLM, MXCRYPTO_ALGORITHM_OLM)
|
||||
}
|
||||
}
|
|
@ -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 enableEncryptionForInvitedMembers: Boolean = false
|
||||
)
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright 2016 OpenMarket Ltd
|
||||
* Copyright 2017 Vector Creations Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.crypto
|
||||
|
||||
import im.vector.matrix.android.api.session.crypto.MXCryptoError
|
||||
|
||||
/**
|
||||
* This class represents a decryption exception
|
||||
*/
|
||||
class MXDecryptionException
|
||||
(
|
||||
/**
|
||||
* the linked crypto error
|
||||
*/
|
||||
val cryptoError: MXCryptoError?
|
||||
) : Exception() {
|
||||
|
||||
override val message: String?
|
||||
get() = cryptoError?.message ?: super.message
|
||||
|
||||
override fun getLocalizedMessage(): String {
|
||||
return cryptoError?.message ?: super.getLocalizedMessage()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,269 @@
|
|||
/*
|
||||
* Copyright 2016 OpenMarket Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.crypto
|
||||
|
||||
import android.text.TextUtils
|
||||
import android.util.Base64
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileKey
|
||||
import timber.log.Timber
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.InputStream
|
||||
import java.security.MessageDigest
|
||||
import java.security.SecureRandom
|
||||
import java.util.*
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
object MXEncryptedAttachments {
|
||||
private const val CRYPTO_BUFFER_SIZE = 32 * 1024
|
||||
private const val CIPHER_ALGORITHM = "AES/CTR/NoPadding"
|
||||
private const val SECRET_KEY_SPEC_ALGORITHM = "AES"
|
||||
private const val MESSAGE_DIGEST_ALGORITHM = "SHA-256"
|
||||
|
||||
/**
|
||||
* Define the result of an encryption file
|
||||
*/
|
||||
data class EncryptionResult(
|
||||
var encryptedFileInfo: EncryptedFileInfo,
|
||||
var encryptedStream: 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(
|
||||
encryptedFileInfo = 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"
|
||||
),
|
||||
encryptedStream = ByteArrayInputStream(outStream.toByteArray())
|
||||
)
|
||||
|
||||
outStream.close()
|
||||
|
||||
Timber.v("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.v("Decrypt in " + (System.currentTimeMillis() - t0) + " ms")
|
||||
|
||||
return decryptedStream
|
||||
} catch (oom: OutOfMemoryError) {
|
||||
Timber.e(oom, "## decryptAttachment() : failed " + oom.message)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## decryptAttachment() : failed " + e.message)
|
||||
}
|
||||
|
||||
try {
|
||||
outStream.close()
|
||||
} catch (closeException: Exception) {
|
||||
Timber.e(closeException, "## decryptAttachment() : fail to close the file")
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Base64 URL conversion methods
|
||||
*/
|
||||
|
||||
private fun base64UrlToBase64(base64Url: String?): String? {
|
||||
var result = base64Url
|
||||
if (null != result) {
|
||||
result = result.replace("-".toRegex(), "+")
|
||||
result = result.replace("_".toRegex(), "/")
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private fun base64ToBase64Url(base64: String?): String? {
|
||||
var result = base64
|
||||
if (null != result) {
|
||||
result = result.replace("\n".toRegex(), "")
|
||||
result = result.replace("\\+".toRegex(), "-")
|
||||
result = result.replace("/".toRegex(), "_")
|
||||
result = result.replace("=".toRegex(), "")
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun base64ToUnpaddedBase64(base64: String?): String? {
|
||||
var result = base64
|
||||
if (null != result) {
|
||||
result = result.replace("\n".toRegex(), "")
|
||||
result = result.replace("=".toRegex(), "")
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* Copyright 2016 OpenMarket Ltd
|
||||
* Copyright 2017 Vector Creations Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.crypto
|
||||
|
||||
import im.vector.matrix.android.api.util.JsonDict
|
||||
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 clearEvent: JsonDict? = null,
|
||||
|
||||
/**
|
||||
* Key owned by the sender of this event.
|
||||
* See MXEvent.senderKey.
|
||||
*/
|
||||
var senderCurve25519Key: String? = null,
|
||||
|
||||
/**
|
||||
* Ed25519 key claimed by the sender of this event.
|
||||
* See MXEvent.claimedEd25519Key.
|
||||
*/
|
||||
var claimedEd25519Key: String? = null,
|
||||
|
||||
/**
|
||||
* List of curve25519 keys involved in telling us about the senderCurve25519Key and
|
||||
* claimedEd25519Key. See MXEvent.forwardingCurve25519KeyChain.
|
||||
*/
|
||||
var forwardingCurve25519KeyChain: List<String> = ArrayList()
|
||||
)
|
|
@ -0,0 +1,361 @@
|
|||
/*
|
||||
* 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 timber.log.Timber
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.nio.charset.Charset
|
||||
import java.security.SecureRandom
|
||||
import java.util.*
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.Mac
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
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.v("## deriveKeys() : " + iterations + " in " + (System.currentTimeMillis() - t0) + " ms")
|
||||
|
||||
return key
|
||||
}
|
||||
}
|
|
@ -0,0 +1,810 @@
|
|||
/*
|
||||
* 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.util.JSON_DICT_PARAMETERIZED_TYPE
|
||||
import im.vector.matrix.android.api.util.JsonDict
|
||||
import im.vector.matrix.android.internal.crypto.algorithms.MXDecryptionResult
|
||||
import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper
|
||||
import im.vector.matrix.android.internal.crypto.model.OlmSessionWrapper
|
||||
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 store: 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 OLM lib account instance.
|
||||
private var olmAccount: OlmAccount? = null
|
||||
|
||||
// The OLM lib utility instance.
|
||||
private var olmUtility: 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 outboundGroupSessionStore: MutableMap<String, OlmOutboundGroupSession> = HashMap()
|
||||
|
||||
// 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 inboundGroupSessionMessageIndexes: MutableMap<String, MutableMap<String, Boolean>> = HashMap()
|
||||
|
||||
/**
|
||||
* inboundGroupSessionWithId error
|
||||
*/
|
||||
private var inboundGroupSessionWithIdError: MXCryptoError? = null
|
||||
|
||||
init {
|
||||
// Retrieve the account from the store
|
||||
olmAccount = store.getAccount()
|
||||
|
||||
if (null == olmAccount) {
|
||||
Timber.v("MXOlmDevice : create a new olm account")
|
||||
// Else, create it
|
||||
try {
|
||||
olmAccount = OlmAccount()
|
||||
store.storeAccount(olmAccount!!)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "MXOlmDevice : cannot initialize olmAccount")
|
||||
}
|
||||
|
||||
} else {
|
||||
Timber.v("MXOlmDevice : use an existing account")
|
||||
}
|
||||
|
||||
try {
|
||||
olmUtility = OlmUtility()
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## MXOlmDevice : OlmUtility failed with error")
|
||||
olmUtility = null
|
||||
}
|
||||
|
||||
try {
|
||||
deviceCurve25519Key = olmAccount!!.identityKeys()[OlmAccount.JSON_KEY_IDENTITY_KEY]
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## MXOlmDevice : cannot find " + OlmAccount.JSON_KEY_IDENTITY_KEY + " with error")
|
||||
}
|
||||
|
||||
try {
|
||||
deviceEd25519Key = olmAccount!!.identityKeys()[OlmAccount.JSON_KEY_FINGER_PRINT_KEY]
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## MXOlmDevice : cannot find " + OlmAccount.JSON_KEY_FINGER_PRINT_KEY + " with error")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The current (unused, unpublished) one-time keys for this account.
|
||||
*/
|
||||
fun getOneTimeKeys(): Map<String, Map<String, String>>? {
|
||||
try {
|
||||
return olmAccount!!.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 olmAccount?.maxOneTimeKeys() ?: -1
|
||||
}
|
||||
|
||||
/**
|
||||
* Release the instance
|
||||
*/
|
||||
fun release() {
|
||||
olmAccount?.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 olmAccount!!.signMessage(message)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## signMessage() : failed")
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks all of the one-time keys as published.
|
||||
*/
|
||||
fun markKeysAsPublished() {
|
||||
try {
|
||||
olmAccount!!.markOneTimeKeysAsPublished()
|
||||
store.storeAccount(olmAccount!!)
|
||||
} 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 {
|
||||
olmAccount!!.generateOneTimeKeys(numKeys)
|
||||
store.storeAccount(olmAccount!!)
|
||||
} 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.v("## createOutboundSession() ; theirIdentityKey $theirIdentityKey theirOneTimeKey $theirOneTimeKey")
|
||||
var olmSession: OlmSession? = null
|
||||
|
||||
try {
|
||||
olmSession = OlmSession()
|
||||
olmSession.initOutboundSession(olmAccount!!, theirIdentityKey, theirOneTimeKey)
|
||||
|
||||
val olmSessionWrapper = OlmSessionWrapper(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
|
||||
olmSessionWrapper.onMessageReceived()
|
||||
|
||||
store.storeSession(olmSessionWrapper, theirIdentityKey)
|
||||
|
||||
val sessionIdentifier = olmSession.sessionIdentifier()
|
||||
|
||||
Timber.v("## 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.v("## createInboundSession() : theirIdentityKey: $theirDeviceIdentityKey")
|
||||
|
||||
var olmSession: OlmSession? = null
|
||||
|
||||
try {
|
||||
try {
|
||||
olmSession = OlmSession()
|
||||
olmSession.initInboundSessionFrom(olmAccount!!, theirDeviceIdentityKey, ciphertext)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## createInboundSession() : the session creation failed")
|
||||
return null
|
||||
}
|
||||
|
||||
Timber.v("## createInboundSession() : sessionId: " + olmSession.sessionIdentifier())
|
||||
|
||||
try {
|
||||
olmAccount!!.removeOneTimeKeys(olmSession)
|
||||
store.storeAccount(olmAccount!!)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## createInboundSession() : removeOneTimeKeys failed")
|
||||
}
|
||||
|
||||
Timber.v("## createInboundSession() : ciphertext: $ciphertext")
|
||||
try {
|
||||
val sha256 = olmUtility!!.sha256(URLEncoder.encode(ciphertext, "utf-8"))
|
||||
Timber.v("## createInboundSession() :ciphertext: SHA256:" + sha256)
|
||||
} 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 olmSessionWrapper = OlmSessionWrapper(olmSession, 0)
|
||||
// This counts as a received message: set last received message time to now
|
||||
olmSessionWrapper.onMessageReceived()
|
||||
|
||||
store.storeSession(olmSessionWrapper, 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 store.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 store.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 olmSessionWrapper = getSessionForDevice(theirDeviceIdentityKey, sessionId)
|
||||
|
||||
if (olmSessionWrapper != null) {
|
||||
try {
|
||||
Timber.v("## encryptMessage() : olmSession.sessionIdentifier: $sessionId")
|
||||
//Timber.v("## encryptMessage() : payloadString: " + payloadString);
|
||||
|
||||
olmMessage = olmSessionWrapper.olmSession.encryptMessage(payloadString)
|
||||
store.storeSession(olmSessionWrapper, 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 olmSessionWrapper = getSessionForDevice(theirDeviceIdentityKey, sessionId)
|
||||
|
||||
if (null != olmSessionWrapper) {
|
||||
val olmMessage = OlmMessage()
|
||||
olmMessage.mCipherText = ciphertext
|
||||
olmMessage.mType = messageType.toLong()
|
||||
|
||||
try {
|
||||
payloadString = olmSessionWrapper.olmSession.decryptMessage(olmMessage)
|
||||
olmSessionWrapper.onMessageReceived()
|
||||
store.storeSession(olmSessionWrapper, 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 olmSessionWrapper = getSessionForDevice(theirDeviceIdentityKey, sessionId)
|
||||
return null != olmSessionWrapper && olmSessionWrapper.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()
|
||||
outboundGroupSessionStore[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 outboundGroupSessionStore[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)) {
|
||||
outboundGroupSessionStore[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 outboundGroupSessionStore[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 = OlmInboundGroupSessionWrapper(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.olmInboundGroupSession != null) {
|
||||
session.olmInboundGroupSession!!.releaseSession()
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// sanity check
|
||||
if (null == session.olmInboundGroupSession) {
|
||||
Timber.e("## addInboundGroupSession : invalid session")
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
if (!TextUtils.equals(session.olmInboundGroupSession!!.sessionIdentifier(), sessionId)) {
|
||||
Timber.e("## addInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey")
|
||||
session.olmInboundGroupSession!!.releaseSession()
|
||||
return false
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
session.olmInboundGroupSession!!.releaseSession()
|
||||
Timber.e(e, "## addInboundGroupSession : sessionIdentifier() failed")
|
||||
return false
|
||||
}
|
||||
|
||||
session.senderKey = senderKey
|
||||
session.roomId = roomId
|
||||
session.keysClaimed = keysClaimed
|
||||
session.forwardingCurve25519KeyChain = forwardingCurve25519KeyChain
|
||||
|
||||
store.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<OlmInboundGroupSessionWrapper> {
|
||||
val sessions = ArrayList<OlmInboundGroupSessionWrapper>(megolmSessionsData.size)
|
||||
|
||||
for (megolmSessionData in megolmSessionsData) {
|
||||
|
||||
val sessionId = megolmSessionData.sessionId
|
||||
val senderKey = megolmSessionData.senderKey
|
||||
val roomId = megolmSessionData.roomId
|
||||
|
||||
var session: OlmInboundGroupSessionWrapper? = null
|
||||
|
||||
try {
|
||||
session = OlmInboundGroupSessionWrapper(megolmSessionData)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## importInboundGroupSession() : Update for megolm session $senderKey/$sessionId")
|
||||
}
|
||||
|
||||
// sanity check
|
||||
if (null == session || null == session.olmInboundGroupSession) {
|
||||
Timber.e("## importInboundGroupSession : invalid session")
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
if (!TextUtils.equals(session.olmInboundGroupSession!!.sessionIdentifier(), sessionId)) {
|
||||
Timber.e("## importInboundGroupSession : ERROR: Mismatched group session ID from senderKey: " + senderKey!!)
|
||||
if (session.olmInboundGroupSession != null) session.olmInboundGroupSession!!.releaseSession()
|
||||
continue
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## importInboundGroupSession : sessionIdentifier() failed")
|
||||
session.olmInboundGroupSession!!.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.olmInboundGroupSession!!.releaseSession()
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
sessions.add(session)
|
||||
}
|
||||
|
||||
store.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) {
|
||||
store.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.roomId)) {
|
||||
var errorMessage = ""
|
||||
var decryptResult: OlmInboundGroupSession.DecryptMessageResult? = null
|
||||
try {
|
||||
decryptResult = session.olmInboundGroupSession!!.decryptMessage(body)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## decryptGroupMessage () : decryptMessage failed")
|
||||
errorMessage = e.message ?: ""
|
||||
}
|
||||
|
||||
if (null != decryptResult) {
|
||||
if (null != timeline) {
|
||||
if (!inboundGroupSessionMessageIndexes.containsKey(timeline)) {
|
||||
inboundGroupSessionMessageIndexes[timeline] = HashMap()
|
||||
}
|
||||
|
||||
val messageIndexKey = senderKey + "|" + sessionId + "|" + decryptResult.mIndex
|
||||
|
||||
if (null != inboundGroupSessionMessageIndexes[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))
|
||||
}
|
||||
|
||||
inboundGroupSessionMessageIndexes[timeline]!!.put(messageIndexKey, true)
|
||||
}
|
||||
|
||||
store.storeInboundGroupSessions(listOf(session))
|
||||
try {
|
||||
val adapter = MoshiProvider.providesMoshi().adapter<JsonDict>(JSON_DICT_PARAMETERIZED_TYPE)
|
||||
val payloadString = convertFromUTF8(decryptResult.mDecryptedMessage)
|
||||
val payload = adapter.fromJson(payloadString)
|
||||
result.payload = payload
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## decryptGroupMessage() : RLEncoder.encode failed " + e.message)
|
||||
return null
|
||||
}
|
||||
|
||||
if (null == result.payload) {
|
||||
Timber.e("## decryptGroupMessage() : fails to parse the payload")
|
||||
return null
|
||||
}
|
||||
|
||||
result.keysClaimed = session.keysClaimed
|
||||
result.senderKey = senderKey
|
||||
result.forwardingCurve25519KeyChain = session.forwardingCurve25519KeyChain
|
||||
} 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.roomId)
|
||||
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(inboundGroupSessionWithIdError)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset replay attack data for the given timeline.
|
||||
*
|
||||
* @param timeline the id of the timeline.
|
||||
*/
|
||||
fun resetReplayAttackCheckInTimeline(timeline: String?) {
|
||||
if (null != timeline) {
|
||||
inboundGroupSessionMessageIndexes.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
|
||||
olmUtility!!.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 olmUtility!!.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): OlmSessionWrapper? {
|
||||
// sanity check
|
||||
return if (!TextUtils.isEmpty(theirDeviceIdentityKey) && !TextUtils.isEmpty(sessionId)) {
|
||||
store.getDeviceSession(sessionId, theirDeviceIdentityKey)
|
||||
} else null
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract an InboundGroupSession from the session store and do some check.
|
||||
* inboundGroupSessionWithIdError 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?): OlmInboundGroupSessionWrapper? {
|
||||
inboundGroupSessionWithIdError = null
|
||||
|
||||
val session = store.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.roomId)) {
|
||||
val errorDescription = String.format(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_REASON, roomId, session.roomId)
|
||||
Timber.e("## getInboundGroupSession() : $errorDescription")
|
||||
inboundGroupSessionWithIdError = MXCryptoError(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_ERROR_CODE,
|
||||
MXCryptoError.UNABLE_TO_DECRYPT, errorDescription)
|
||||
}
|
||||
} else {
|
||||
Timber.e("## getInboundGroupSession() : Cannot retrieve inbound group session $sessionId")
|
||||
inboundGroupSessionWithIdError = MXCryptoError(MXCryptoError.UNKNOWN_INBOUND_SESSION_ID_ERROR_CODE,
|
||||
MXCryptoError.UNKNOWN_INBOUND_SESSION_ID_REASON, null)
|
||||
}
|
||||
return session
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if we have the keys for a given megolm session.
|
||||
*
|
||||
* @param roomId room in which the message was received
|
||||
* @param senderKey base64-encoded curve25519 key of the sender
|
||||
* @param sessionId session identifier
|
||||
* @return true if the unbound session keys are known.
|
||||
*/
|
||||
fun hasInboundSessionKeys(roomId: String, senderKey: String, sessionId: String): Boolean {
|
||||
return null != getInboundGroupSession(sessionId, senderKey, roomId)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* 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
|
||||
|
||||
/**
|
||||
* 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
|
||||
)
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* 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.model.MXDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
import java.util.*
|
||||
|
||||
internal class MyDeviceInfoHolder(
|
||||
// The credentials,
|
||||
credentials: Credentials,
|
||||
// the crypto store
|
||||
cryptoStore: IMXCryptoStore,
|
||||
// Olm device
|
||||
olmDevice: MXOlmDevice
|
||||
) {
|
||||
// Our device keys
|
||||
/**
|
||||
* my device info
|
||||
*/
|
||||
val myDevice: MXDeviceInfo = MXDeviceInfo(credentials.deviceId!!, credentials.userId)
|
||||
|
||||
init {
|
||||
val keys = HashMap<String, String>()
|
||||
|
||||
if (!TextUtils.isEmpty(olmDevice.deviceEd25519Key)) {
|
||||
keys["ed25519:" + credentials.deviceId] = olmDevice.deviceEd25519Key!!
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(olmDevice.deviceCurve25519Key)) {
|
||||
keys["curve25519:" + credentials.deviceId] = olmDevice.deviceCurve25519Key!!
|
||||
}
|
||||
|
||||
myDevice.keys = keys
|
||||
|
||||
myDevice.algorithms = MXCryptoAlgorithms.supportedAlgorithms()
|
||||
myDevice.verified = MXDeviceInfo.DEVICE_VERIFICATION_VERIFIED
|
||||
|
||||
// Add our own deviceinfo to the store
|
||||
val endToEndDevicesForUser = cryptoStore.getUserDevices(credentials.userId)
|
||||
|
||||
val myDevices: MutableMap<String, MXDeviceInfo>
|
||||
|
||||
if (null != endToEndDevicesForUser) {
|
||||
myDevices = HashMap(endToEndDevicesForUser)
|
||||
} else {
|
||||
myDevices = HashMap()
|
||||
}
|
||||
|
||||
myDevices[myDevice.deviceId] = myDevice
|
||||
|
||||
cryptoStore.storeUserDevices(credentials.userId, myDevices)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* 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.auth.data.Credentials
|
||||
import java.util.*
|
||||
|
||||
internal class ObjectSigner(private val credentials: Credentials,
|
||||
private val olmDevice: MXOlmDevice) {
|
||||
|
||||
/**
|
||||
* Sign Object
|
||||
*
|
||||
* Example:
|
||||
* <pre>
|
||||
* {
|
||||
* "[MY_USER_ID]": {
|
||||
* "ed25519:[MY_DEVICE_ID]": "sign(str)"
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* @param strToSign the String to sign and to include in the Map
|
||||
* @return a Map (see example)
|
||||
*/
|
||||
fun signObject(strToSign: String): Map<String, Map<String, String>> {
|
||||
val result = HashMap<String, Map<String, String>>()
|
||||
|
||||
val content = HashMap<String, String>()
|
||||
|
||||
content["ed25519:" + credentials.deviceId] = olmDevice.signMessage(strToSign)!!
|
||||
|
||||
result[credentials.userId] = content
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,188 @@
|
|||
/*
|
||||
* 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 arrow.core.Try
|
||||
import arrow.instances.`try`.applicativeError.handleError
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.internal.crypto.model.MXKey
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.KeysUploadResponse
|
||||
import im.vector.matrix.android.internal.crypto.tasks.UploadKeysTask
|
||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||
import org.matrix.olm.OlmAccount
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
|
||||
internal class OneTimeKeysUploader(
|
||||
private val credentials: Credentials,
|
||||
private val olmDevice: MXOlmDevice,
|
||||
private val objectSigner: ObjectSigner,
|
||||
private val uploadKeysTask: UploadKeysTask
|
||||
) {
|
||||
// tell if there is a OTK check in progress
|
||||
private var oneTimeKeyCheckInProgress = false
|
||||
|
||||
// last OTK check timestamp
|
||||
private var lastOneTimeKeyCheck: Long = 0
|
||||
private var oneTimeKeyCount: Int? = null
|
||||
|
||||
private var lastPublishedOneTimeKeys: Map<String, Map<String, String>>? = null
|
||||
|
||||
/**
|
||||
* Stores the current one_time_key count which will be handled later (in a call of
|
||||
* _onSyncCompleted). The count is e.g. coming from a /sync response.
|
||||
*
|
||||
* @param currentCount the new count
|
||||
*/
|
||||
fun updateOneTimeKeyCount(currentCount: Int) {
|
||||
oneTimeKeyCount = currentCount
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if the OTK must be uploaded.
|
||||
*/
|
||||
suspend fun maybeUploadOneTimeKeys(): Try<Unit> {
|
||||
if (oneTimeKeyCheckInProgress) {
|
||||
return Try.just(Unit)
|
||||
}
|
||||
if (System.currentTimeMillis() - lastOneTimeKeyCheck < ONE_TIME_KEY_UPLOAD_PERIOD) {
|
||||
// we've done a key upload recently.
|
||||
return Try.just(Unit)
|
||||
}
|
||||
|
||||
lastOneTimeKeyCheck = System.currentTimeMillis()
|
||||
oneTimeKeyCheckInProgress = true
|
||||
|
||||
// We then check how many keys we can store in the Account object.
|
||||
val maxOneTimeKeys = olmDevice.getMaxNumberOfOneTimeKeys()
|
||||
|
||||
// Try to keep at most half that number on the server. This leaves the
|
||||
// rest of the slots free to hold keys that have been claimed from the
|
||||
// server but we haven't received a message for.
|
||||
// If we run out of slots when generating new keys then olm will
|
||||
// discard the oldest private keys first. This will eventually clean
|
||||
// out stale private keys that won't receive a message.
|
||||
val keyLimit = Math.floor(maxOneTimeKeys / 2.0).toInt()
|
||||
val result = if (oneTimeKeyCount != null) {
|
||||
uploadOTK(oneTimeKeyCount!!, keyLimit)
|
||||
} else {
|
||||
// ask the server how many keys we have
|
||||
val uploadKeysParams = UploadKeysTask.Params(null, null, credentials.deviceId!!)
|
||||
uploadKeysTask.execute(uploadKeysParams)
|
||||
.flatMap {
|
||||
// We need to keep a pool of one time public keys on the server so that
|
||||
// other devices can start conversations with us. But we can only store
|
||||
// a finite number of private keys in the olm Account object.
|
||||
// To complicate things further then can be a delay between a device
|
||||
// claiming a public one time key from the server and it sending us a
|
||||
// message. We need to keep the corresponding private key locally until
|
||||
// we receive the message.
|
||||
// But that message might never arrive leaving us stuck with duff
|
||||
// private keys clogging up our local storage.
|
||||
// So we need some kind of engineering compromise to balance all of
|
||||
// these factors.
|
||||
// TODO Why we do not set oneTimeKeyCount here?
|
||||
// TODO This is not needed anymore, see https://github.com/matrix-org/matrix-js-sdk/pull/493 (TODO on iOS also)
|
||||
val keyCount = it.oneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE)
|
||||
uploadOTK(keyCount, keyLimit)
|
||||
}
|
||||
}
|
||||
return result
|
||||
.map {
|
||||
Timber.v("## uploadKeys() : success")
|
||||
oneTimeKeyCount = null
|
||||
oneTimeKeyCheckInProgress = false
|
||||
}
|
||||
.handleError {
|
||||
Timber.e(it, "## uploadKeys() : failed")
|
||||
oneTimeKeyCount = null
|
||||
oneTimeKeyCheckInProgress = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload some the OTKs.
|
||||
*
|
||||
* @param keyCount the key count
|
||||
* @param keyLimit the limit
|
||||
*/
|
||||
private suspend fun uploadOTK(keyCount: Int, keyLimit: Int): Try<Unit> {
|
||||
if (keyLimit <= keyCount) {
|
||||
// If we don't need to generate any more keys then we are done.
|
||||
return Try.just(Unit)
|
||||
}
|
||||
|
||||
val keysThisLoop = Math.min(keyLimit - keyCount, ONE_TIME_KEY_GENERATION_MAX_NUMBER)
|
||||
olmDevice.generateOneTimeKeys(keysThisLoop)
|
||||
return uploadOneTimeKeys()
|
||||
.flatMap {
|
||||
if (it.hasOneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE)) {
|
||||
uploadOTK(it.oneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE), keyLimit)
|
||||
} else {
|
||||
Timber.e("## uploadLoop() : response for uploading keys does not contain one_time_key_counts.signed_curve25519")
|
||||
Try.raise(Exception("response for uploading keys does not contain one_time_key_counts.signed_curve25519"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload my user's one time keys.
|
||||
*/
|
||||
private suspend fun uploadOneTimeKeys(): Try<KeysUploadResponse> {
|
||||
val oneTimeKeys = olmDevice.getOneTimeKeys()
|
||||
val oneTimeJson = HashMap<String, Any>()
|
||||
|
||||
val curve25519Map = oneTimeKeys!![OlmAccount.JSON_KEY_ONE_TIME_KEY]
|
||||
|
||||
if (null != curve25519Map) {
|
||||
for (key_id in curve25519Map.keys) {
|
||||
val k = HashMap<String, Any>()
|
||||
k["key"] = curve25519Map.getValue(key_id)
|
||||
|
||||
// the key is also signed
|
||||
val canonicalJson = MoshiProvider.getCanonicalJson(Map::class.java, k)
|
||||
|
||||
k["signatures"] = objectSigner.signObject(canonicalJson)
|
||||
|
||||
oneTimeJson["signed_curve25519:$key_id"] = k
|
||||
}
|
||||
}
|
||||
|
||||
// For now, we set the device id explicitly, as we may not be using the
|
||||
// same one as used in login.
|
||||
val uploadParams = UploadKeysTask.Params(null, oneTimeJson, credentials.deviceId!!)
|
||||
return uploadKeysTask
|
||||
.execute(uploadParams)
|
||||
.map {
|
||||
lastPublishedOneTimeKeys = oneTimeKeys
|
||||
olmDevice.markKeysAsPublished()
|
||||
it
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
// max number of keys to upload at once
|
||||
// Creating keys can be an expensive operation so we limit the
|
||||
// number we generate in one go to avoid blocking the application
|
||||
// for too long.
|
||||
private const val ONE_TIME_KEY_GENERATION_MAX_NUMBER = 5
|
||||
|
||||
// frequency with which to check & upload one-time keys
|
||||
private const val ONE_TIME_KEY_UPLOAD_PERIOD = (60 * 1000).toLong() // one minute
|
||||
}
|
||||
}
|
|
@ -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 requestBody: RoomKeyRequestBody?, // list of recipients for the request
|
||||
var recipients: 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 requestId: String, // current state of this request
|
||||
var state: RequestState) {
|
||||
|
||||
// transaction id for the cancellation, if any
|
||||
var cancellationTxnId: String? = null
|
||||
|
||||
/**
|
||||
* Used only for log.
|
||||
*
|
||||
* @return the room id.
|
||||
*/
|
||||
val roomId: String?
|
||||
get() = if (null != requestBody) {
|
||||
requestBody!!.roomId
|
||||
} else null
|
||||
|
||||
/**
|
||||
* Used only for log.
|
||||
*
|
||||
* @return the session id
|
||||
*/
|
||||
val sessionId: String?
|
||||
get() = if (null != requestBody) {
|
||||
requestBody!!.sessionId
|
||||
} else null
|
||||
|
||||
/**
|
||||
* possible states for a room key request
|
||||
*
|
||||
*
|
||||
* The state machine looks like:
|
||||
* <pre>
|
||||
*
|
||||
* |
|
||||
* V
|
||||
* UNSENT -----------------------------+
|
||||
* | |
|
||||
* | (send successful) | (cancellation requested)
|
||||
* V |
|
||||
* SENT |
|
||||
* |-------------------------------- | --------------+
|
||||
* | | |
|
||||
* | | | (cancellation requested with intent
|
||||
* | | | to resend a new request)
|
||||
* | (cancellation requested) | |
|
||||
* V | V
|
||||
* CANCELLATION_PENDING | CANCELLATION_PENDING_AND_WILL_RESEND
|
||||
* | | |
|
||||
* | (cancellation sent) | | (cancellation sent. Create new request
|
||||
* | | | in the UNSENT state)
|
||||
* V | |
|
||||
* (deleted) <---------------------------+----------------+
|
||||
* </pre>
|
||||
*/
|
||||
|
||||
enum class RequestState {
|
||||
/**
|
||||
* request not yet sent
|
||||
*/
|
||||
UNSENT,
|
||||
/**
|
||||
* request sent, awaiting reply
|
||||
*/
|
||||
SENT,
|
||||
/**
|
||||
* reply received, cancellation not yet sent
|
||||
*/
|
||||
CANCELLATION_PENDING,
|
||||
/**
|
||||
* Cancellation not yet sent, once sent, a new request will be done
|
||||
*/
|
||||
CANCELLATION_PENDING_AND_WILL_RESEND,
|
||||
/**
|
||||
* sending failed
|
||||
*/
|
||||
FAILED;
|
||||
|
||||
companion object {
|
||||
fun from(state: Int) = when (state) {
|
||||
0 -> UNSENT
|
||||
1 -> SENT
|
||||
2 -> CANCELLATION_PENDING
|
||||
3 -> CANCELLATION_PENDING_AND_WILL_RESEND
|
||||
else /*4*/ -> FAILED
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,296 @@
|
|||
/*
|
||||
* 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.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.store.IMXCryptoStore
|
||||
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 OutgoingRoomKeyRequestManager(
|
||||
private val cryptoStore: IMXCryptoStore,
|
||||
private val sendToDeviceTask: SendToDeviceTask,
|
||||
private val taskExecutor: TaskExecutor) {
|
||||
|
||||
// running
|
||||
private var isClientRunning: Boolean = false
|
||||
|
||||
// transaction counter
|
||||
private var txnCtr: Int = 0
|
||||
|
||||
// sanity check to ensure that we don't end up with two concurrent runs
|
||||
// of sendOutgoingRoomKeyRequestsTimer
|
||||
private var sendOutgoingRoomKeyRequestsRunning: Boolean = false
|
||||
|
||||
/**
|
||||
* Called when the client is started. Sets background processes running.
|
||||
*/
|
||||
fun start() {
|
||||
isClientRunning = true
|
||||
startTimer()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the client is stopped. Stops any running background processes.
|
||||
*/
|
||||
fun stop() {
|
||||
isClientRunning = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Make up a new transaction id
|
||||
*
|
||||
* @return {string} a new, unique, transaction id
|
||||
*/
|
||||
private fun makeTxnId(): String {
|
||||
return "m" + System.currentTimeMillis() + "." + txnCtr++
|
||||
}
|
||||
|
||||
/**
|
||||
* 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>>) {
|
||||
val req = cryptoStore.getOrAddOutgoingRoomKeyRequest(
|
||||
OutgoingRoomKeyRequest(requestBody, recipients, makeTxnId(), OutgoingRoomKeyRequest.RequestState.UNSENT))
|
||||
|
||||
|
||||
if (req?.state == 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 = cryptoStore.getOutgoingRoomKeyRequest(requestBody)
|
||||
?: // no request was made for this key
|
||||
return
|
||||
|
||||
Timber.v("cancelRoomKeyRequest: requestId: " + req.requestId + " state: " + req.state + " andResend: " + andResend)
|
||||
|
||||
if (req.state === OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING || req.state === OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING_AND_WILL_RESEND) {
|
||||
// nothing to do here
|
||||
} else if (req.state === OutgoingRoomKeyRequest.RequestState.UNSENT || req.state === OutgoingRoomKeyRequest.RequestState.FAILED) {
|
||||
Timber.v("## cancelRoomKeyRequest() : deleting unnecessary room key request for $requestBody")
|
||||
cryptoStore.deleteOutgoingRoomKeyRequest(req.requestId)
|
||||
} else if (req.state === OutgoingRoomKeyRequest.RequestState.SENT) {
|
||||
if (andResend) {
|
||||
req.state = OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING_AND_WILL_RESEND
|
||||
} else {
|
||||
req.state = OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING
|
||||
}
|
||||
req.cancellationTxnId = makeTxnId()
|
||||
cryptoStore.updateOutgoingRoomKeyRequest(req)
|
||||
sendOutgoingRoomKeyRequestCancellation(req)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Start the background timer to send queued requests, if the timer isn't already running.
|
||||
*/
|
||||
private fun startTimer() {
|
||||
if (sendOutgoingRoomKeyRequestsRunning) {
|
||||
return
|
||||
}
|
||||
Handler().postDelayed(Runnable {
|
||||
if (sendOutgoingRoomKeyRequestsRunning) {
|
||||
Timber.v("## startTimer() : RoomKeyRequestSend already in progress!")
|
||||
return@Runnable
|
||||
}
|
||||
|
||||
sendOutgoingRoomKeyRequestsRunning = 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 (!isClientRunning) {
|
||||
sendOutgoingRoomKeyRequestsRunning = false
|
||||
return
|
||||
}
|
||||
|
||||
Timber.v("## sendOutgoingRoomKeyRequests() : Looking for queued outgoing room key requests")
|
||||
val outgoingRoomKeyRequest = cryptoStore.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")
|
||||
sendOutgoingRoomKeyRequestsRunning = false
|
||||
return
|
||||
}
|
||||
|
||||
if (OutgoingRoomKeyRequest.RequestState.UNSENT === outgoingRoomKeyRequest.state) {
|
||||
sendOutgoingRoomKeyRequest(outgoingRoomKeyRequest)
|
||||
} else {
|
||||
sendOutgoingRoomKeyRequestCancellation(outgoingRoomKeyRequest)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the outgoing key request.
|
||||
*
|
||||
* @param request the request
|
||||
*/
|
||||
private fun sendOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest) {
|
||||
Timber.v("## sendOutgoingRoomKeyRequest() : Requesting keys " + request.requestBody
|
||||
+ " from " + request.recipients + " id " + request.requestId)
|
||||
|
||||
val requestMessage = RoomKeyShareRequest()
|
||||
requestMessage.requestingDeviceId = cryptoStore.getDeviceId()
|
||||
requestMessage.requestId = request.requestId
|
||||
requestMessage.body = request.requestBody
|
||||
|
||||
sendMessageToDevices(requestMessage, request.recipients, request.requestId, object : MatrixCallback<Unit> {
|
||||
private fun onDone(state: OutgoingRoomKeyRequest.RequestState) {
|
||||
if (request.state !== OutgoingRoomKeyRequest.RequestState.UNSENT) {
|
||||
Timber.v("## sendOutgoingRoomKeyRequest() : Cannot update room key request from UNSENT as it was already updated to " + request.state)
|
||||
} else {
|
||||
request.state = state
|
||||
cryptoStore.updateOutgoingRoomKeyRequest(request)
|
||||
}
|
||||
|
||||
sendOutgoingRoomKeyRequestsRunning = false
|
||||
startTimer()
|
||||
}
|
||||
|
||||
override fun onSuccess(data: Unit) {
|
||||
Timber.v("## 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.v("## sendOutgoingRoomKeyRequestCancellation() : Sending cancellation for key request for " + request.requestBody
|
||||
+ " to " + request.recipients
|
||||
+ " cancellation id " + request.cancellationTxnId)
|
||||
|
||||
val roomKeyShareCancellation = RoomKeyShareCancellation()
|
||||
roomKeyShareCancellation.requestingDeviceId = cryptoStore.getDeviceId()
|
||||
roomKeyShareCancellation.requestId = request.cancellationTxnId
|
||||
|
||||
sendMessageToDevices(roomKeyShareCancellation, request.recipients, request.cancellationTxnId, object : MatrixCallback<Unit> {
|
||||
private fun onDone() {
|
||||
cryptoStore.deleteOutgoingRoomKeyRequest(request.requestId)
|
||||
sendOutgoingRoomKeyRequestsRunning = false
|
||||
startTimer()
|
||||
}
|
||||
|
||||
|
||||
override fun onSuccess(data: Unit) {
|
||||
Timber.v("## sendOutgoingRoomKeyRequestCancellation() : done")
|
||||
val resend = request.state === OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING_AND_WILL_RESEND
|
||||
|
||||
onDone()
|
||||
|
||||
// Resend the request with a new ID
|
||||
if (resend) {
|
||||
sendRoomKeyRequest(request.requestBody, request.recipients)
|
||||
}
|
||||
}
|
||||
|
||||
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"]) // TODO Change this two hard coded key to something better
|
||||
}
|
||||
|
||||
sendToDeviceTask.configureWith(SendToDeviceTask.Params(EventType.ROOM_KEY_REQUEST, contentMap, transactionId))
|
||||
.dispatchTo(callback)
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val SEND_KEY_REQUESTS_DELAY_MS = 500
|
||||
}
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* 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.internal.crypto.algorithms.IMXDecrypting
|
||||
import im.vector.matrix.android.internal.crypto.algorithms.megolm.MXMegolmDecryptionFactory
|
||||
import im.vector.matrix.android.internal.crypto.algorithms.olm.MXOlmDecryptionFactory
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
|
||||
internal class RoomDecryptorProvider(
|
||||
private val olmDecryptionFactory: MXOlmDecryptionFactory,
|
||||
private val megolmDecryptionFactory: MXMegolmDecryptionFactory
|
||||
) {
|
||||
|
||||
// A map from algorithm to MXDecrypting instance, for each room
|
||||
private val roomDecryptors: MutableMap<String /* room id */, MutableMap<String /* algorithm */, IMXDecrypting>> = 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 Create another method for the case of roomId is null
|
||||
*/
|
||||
fun getOrCreateRoomDecryptor(roomId: String?, algorithm: String?): IMXDecrypting? {
|
||||
// sanity check
|
||||
if (algorithm.isNullOrEmpty()) {
|
||||
Timber.e("## getRoomDecryptor() : null algorithm")
|
||||
return null
|
||||
}
|
||||
if(roomId != null && roomId.isNotEmpty()) {
|
||||
synchronized(roomDecryptors) {
|
||||
if (!roomDecryptors.containsKey(roomId)) {
|
||||
roomDecryptors[roomId] = HashMap()
|
||||
}
|
||||
val alg = roomDecryptors[roomId]?.get(algorithm)
|
||||
if (alg != null) {
|
||||
return alg
|
||||
}
|
||||
}
|
||||
}
|
||||
val decryptingClass = MXCryptoAlgorithms.hasDecryptorClassForAlgorithm(algorithm)
|
||||
if (decryptingClass) {
|
||||
val alg = when (algorithm) {
|
||||
MXCRYPTO_ALGORITHM_MEGOLM -> megolmDecryptionFactory.create()
|
||||
else -> olmDecryptionFactory.create()
|
||||
}
|
||||
if (roomId != null && !TextUtils.isEmpty(roomId)) {
|
||||
synchronized(roomDecryptors) {
|
||||
roomDecryptors[roomId]?.put(algorithm, alg)
|
||||
}
|
||||
}
|
||||
return alg
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun getRoomDecryptor(roomId: String?, algorithm: String?): IMXDecrypting? {
|
||||
if (roomId == null || algorithm == null) {
|
||||
return null
|
||||
}
|
||||
return roomDecryptors[roomId]?.get(algorithm)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,154 @@
|
|||
/*
|
||||
* 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.actions
|
||||
|
||||
import android.text.TextUtils
|
||||
import arrow.core.Try
|
||||
import im.vector.matrix.android.internal.crypto.MXOlmDevice
|
||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.MXKey
|
||||
import im.vector.matrix.android.internal.crypto.model.MXOlmSessionResult
|
||||
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
||||
import im.vector.matrix.android.internal.crypto.tasks.ClaimOneTimeKeysForUsersDeviceTask
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
|
||||
internal class EnsureOlmSessionsForDevicesAction(private val olmDevice: MXOlmDevice,
|
||||
private val oneTimeKeysForUsersDeviceTask: ClaimOneTimeKeysForUsersDeviceTask) {
|
||||
|
||||
|
||||
suspend fun handle(devicesByUser: Map<String, List<MXDeviceInfo>>): Try<MXUsersDevicesMap<MXOlmSessionResult>> {
|
||||
val devicesWithoutSession = ArrayList<MXDeviceInfo>()
|
||||
|
||||
val results = MXUsersDevicesMap<MXOlmSessionResult>()
|
||||
|
||||
val userIds = devicesByUser.keys
|
||||
|
||||
for (userId in userIds) {
|
||||
val deviceInfos = devicesByUser[userId]
|
||||
|
||||
for (deviceInfo in deviceInfos!!) {
|
||||
val deviceId = deviceInfo.deviceId
|
||||
val key = deviceInfo.identityKey()
|
||||
|
||||
val sessionId = olmDevice.getSessionId(key!!)
|
||||
|
||||
if (TextUtils.isEmpty(sessionId)) {
|
||||
devicesWithoutSession.add(deviceInfo)
|
||||
}
|
||||
|
||||
val olmSessionResult = MXOlmSessionResult(deviceInfo, sessionId)
|
||||
results.setObject(olmSessionResult, userId, deviceId)
|
||||
}
|
||||
}
|
||||
|
||||
if (devicesWithoutSession.size == 0) {
|
||||
return Try.just(results)
|
||||
}
|
||||
|
||||
// Prepare the request for claiming one-time keys
|
||||
val usersDevicesToClaim = MXUsersDevicesMap<String>()
|
||||
|
||||
val oneTimeKeyAlgorithm = MXKey.KEY_SIGNED_CURVE_25519_TYPE
|
||||
|
||||
for (device in devicesWithoutSession) {
|
||||
usersDevicesToClaim.setObject(oneTimeKeyAlgorithm, device.userId, device.deviceId)
|
||||
}
|
||||
|
||||
// TODO: this has a race condition - if we try to send another message
|
||||
// while we are claiming a key, we will end up claiming two and setting up
|
||||
// two sessions.
|
||||
//
|
||||
// That should eventually resolve itself, but it's poor form.
|
||||
|
||||
Timber.v("## claimOneTimeKeysForUsersDevices() : $usersDevicesToClaim")
|
||||
|
||||
val claimParams = ClaimOneTimeKeysForUsersDeviceTask.Params(usersDevicesToClaim)
|
||||
return oneTimeKeysForUsersDeviceTask
|
||||
.execute(claimParams)
|
||||
.map {
|
||||
Timber.v("## claimOneTimeKeysForUsersDevices() : keysClaimResponse.oneTimeKeys: $it")
|
||||
for (userId in userIds) {
|
||||
val deviceInfos = devicesByUser[userId]
|
||||
for (deviceInfo in deviceInfos!!) {
|
||||
var oneTimeKey: MXKey? = null
|
||||
val deviceIds = it.getUserDeviceIds(userId)
|
||||
if (null != deviceIds) {
|
||||
for (deviceId in deviceIds) {
|
||||
val olmSessionResult = results.getObject(deviceId, userId)
|
||||
if (olmSessionResult!!.sessionId != null) {
|
||||
// We already have a result for this device
|
||||
continue
|
||||
}
|
||||
val key = it.getObject(deviceId, userId)
|
||||
if (key?.type == oneTimeKeyAlgorithm) {
|
||||
oneTimeKey = key
|
||||
}
|
||||
if (oneTimeKey == null) {
|
||||
Timber.v("## ensureOlmSessionsForDevices() : No one-time keys " + oneTimeKeyAlgorithm
|
||||
+ " for device " + userId + " : " + deviceId)
|
||||
continue
|
||||
}
|
||||
// Update the result for this device in results
|
||||
olmSessionResult.sessionId = verifyKeyAndStartSession(oneTimeKey, userId, deviceInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
results
|
||||
}
|
||||
}
|
||||
|
||||
private fun verifyKeyAndStartSession(oneTimeKey: MXKey, userId: String, deviceInfo: MXDeviceInfo): String? {
|
||||
var sessionId: String? = null
|
||||
|
||||
val deviceId = deviceInfo.deviceId
|
||||
val signKeyId = "ed25519:$deviceId"
|
||||
val signature = oneTimeKey.signatureForUserId(userId, signKeyId)
|
||||
|
||||
if (!TextUtils.isEmpty(signature) && !TextUtils.isEmpty(deviceInfo.fingerprint())) {
|
||||
var isVerified = false
|
||||
var errorMessage: String? = null
|
||||
|
||||
try {
|
||||
olmDevice.verifySignature(deviceInfo.fingerprint()!!, oneTimeKey.signalableJSONDictionary(), signature)
|
||||
isVerified = true
|
||||
} catch (e: Exception) {
|
||||
errorMessage = e.message
|
||||
}
|
||||
|
||||
// Check one-time key signature
|
||||
if (isVerified) {
|
||||
sessionId = olmDevice.createOutboundSession(deviceInfo.identityKey()!!, oneTimeKey.value)
|
||||
|
||||
if (!TextUtils.isEmpty(sessionId)) {
|
||||
Timber.v("## verifyKeyAndStartSession() : Started new sessionid " + sessionId
|
||||
+ " for device " + deviceInfo + "(theirOneTimeKey: " + oneTimeKey.value + ")")
|
||||
} else {
|
||||
// Possibly a bad key
|
||||
Timber.e("## verifyKeyAndStartSession() : Error starting session with device $userId:$deviceId")
|
||||
}
|
||||
} else {
|
||||
Timber.e("## verifyKeyAndStartSession() : Unable to verify signature on one-time key for device " + userId
|
||||
+ ":" + deviceId + " Error " + errorMessage)
|
||||
}
|
||||
}
|
||||
|
||||
return sessionId
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* 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.actions
|
||||
|
||||
import android.text.TextUtils
|
||||
import arrow.core.Try
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.internal.crypto.MXOlmDevice
|
||||
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.store.IMXCryptoStore
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
|
||||
internal class EnsureOlmSessionsForUsersAction(private val olmDevice: MXOlmDevice,
|
||||
private val cryptoStore: IMXCryptoStore,
|
||||
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction) {
|
||||
|
||||
/**
|
||||
* Try to make sure we have established olm sessions for the given users.
|
||||
* @param users a list of user ids.
|
||||
*/
|
||||
suspend fun handle(users: List<String>) : Try<MXUsersDevicesMap<MXOlmSessionResult>> {
|
||||
Timber.v("## ensureOlmSessionsForUsers() : ensureOlmSessionsForUsers $users")
|
||||
val devicesByUser = HashMap<String /* userId */, MutableList<MXDeviceInfo>>()
|
||||
|
||||
for (userId in users) {
|
||||
devicesByUser[userId] = ArrayList()
|
||||
|
||||
val devices = cryptoStore.getUserDevices(userId)?.values ?: emptyList()
|
||||
|
||||
for (device in devices) {
|
||||
val key = device.identityKey()
|
||||
|
||||
if (TextUtils.equals(key, olmDevice.deviceCurve25519Key)) {
|
||||
// Don't bother setting up session to ourself
|
||||
continue
|
||||
}
|
||||
|
||||
if (device.isVerified) {
|
||||
// Don't bother setting up sessions with blocked users
|
||||
continue
|
||||
}
|
||||
|
||||
devicesByUser[userId]!!.add(device)
|
||||
}
|
||||
}
|
||||
return ensureOlmSessionsForDevicesAction.handle(devicesByUser)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.crypto.actions
|
||||
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.listeners.ProgressListener
|
||||
import im.vector.matrix.android.internal.crypto.CryptoAsyncHelper
|
||||
import im.vector.matrix.android.internal.crypto.MXOlmDevice
|
||||
import im.vector.matrix.android.internal.crypto.MegolmSessionData
|
||||
import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequestManager
|
||||
import im.vector.matrix.android.internal.crypto.RoomDecryptorProvider
|
||||
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
import timber.log.Timber
|
||||
|
||||
internal class MegolmSessionDataImporter(private val olmDevice: MXOlmDevice,
|
||||
private val roomDecryptorProvider: RoomDecryptorProvider,
|
||||
private val outgoingRoomKeyRequestManager: OutgoingRoomKeyRequestManager,
|
||||
private val cryptoStore: IMXCryptoStore) {
|
||||
|
||||
/**
|
||||
* Import a list of megolm session keys.
|
||||
*
|
||||
* @param megolmSessionsData megolm sessions.
|
||||
* @param backUpKeys true to back up them to the homeserver.
|
||||
* @param progressListener the progress listener
|
||||
* @param callback
|
||||
*/
|
||||
fun handle(megolmSessionsData: List<MegolmSessionData>,
|
||||
fromBackup: Boolean,
|
||||
progressListener: ProgressListener?,
|
||||
callback: MatrixCallback<ImportRoomKeysResult>) {
|
||||
val t0 = System.currentTimeMillis()
|
||||
|
||||
val totalNumbersOfKeys = megolmSessionsData.size
|
||||
var cpt = 0
|
||||
var lastProgress = 0
|
||||
var totalNumbersOfImportedKeys = 0
|
||||
|
||||
if (progressListener != null) {
|
||||
CryptoAsyncHelper.getUiHandler().post {
|
||||
progressListener.onProgress(0, 100)
|
||||
}
|
||||
}
|
||||
val olmInboundGroupSessionWrappers = olmDevice.importInboundGroupSessions(megolmSessionsData)
|
||||
|
||||
for (megolmSessionData in megolmSessionsData) {
|
||||
cpt++
|
||||
|
||||
|
||||
val decrypting = roomDecryptorProvider.getOrCreateRoomDecryptor(megolmSessionData.roomId, megolmSessionData.algorithm)
|
||||
|
||||
if (null != decrypting) {
|
||||
try {
|
||||
val sessionId = megolmSessionData.sessionId
|
||||
Timber.v("## importRoomKeys retrieve senderKey " + megolmSessionData.senderKey + " sessionId " + sessionId)
|
||||
|
||||
totalNumbersOfImportedKeys++
|
||||
|
||||
// cancel any outstanding room key requests for this session
|
||||
val roomKeyRequestBody = RoomKeyRequestBody()
|
||||
|
||||
roomKeyRequestBody.algorithm = megolmSessionData.algorithm
|
||||
roomKeyRequestBody.roomId = megolmSessionData.roomId
|
||||
roomKeyRequestBody.senderKey = megolmSessionData.senderKey
|
||||
roomKeyRequestBody.sessionId = megolmSessionData.sessionId
|
||||
|
||||
outgoingRoomKeyRequestManager.cancelRoomKeyRequest(roomKeyRequestBody)
|
||||
|
||||
// Have another go at decrypting events sent with this session
|
||||
decrypting.onNewSession(megolmSessionData.senderKey!!, sessionId!!)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## importRoomKeys() : onNewSession failed")
|
||||
}
|
||||
}
|
||||
|
||||
if (progressListener != null) {
|
||||
CryptoAsyncHelper.getUiHandler().post {
|
||||
val progress = 100 * cpt / totalNumbersOfKeys
|
||||
|
||||
if (lastProgress != progress) {
|
||||
lastProgress = progress
|
||||
|
||||
progressListener.onProgress(progress, 100)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Do not back up the key if it comes from a backup recovery
|
||||
if (fromBackup) {
|
||||
cryptoStore.markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers)
|
||||
}
|
||||
|
||||
val t1 = System.currentTimeMillis()
|
||||
|
||||
Timber.v("## importMegolmSessionsData : sessions import " + (t1 - t0) + " ms (" + megolmSessionsData.size + " sessions)")
|
||||
|
||||
val finalTotalNumbersOfImportedKeys = totalNumbersOfImportedKeys
|
||||
|
||||
CryptoAsyncHelper.getUiHandler().post {
|
||||
callback.onSuccess(ImportRoomKeysResult(totalNumbersOfKeys, finalTotalNumbersOfImportedKeys))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.crypto.actions
|
||||
|
||||
import android.text.TextUtils
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_OLM
|
||||
import im.vector.matrix.android.internal.crypto.MXOlmDevice
|
||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.EncryptedMessage
|
||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||
import im.vector.matrix.android.internal.util.convertToUTF8
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
|
||||
internal class MessageEncrypter(private val credentials: Credentials,
|
||||
private val olmDevice: MXOlmDevice) {
|
||||
|
||||
/**
|
||||
* Encrypt an event payload for a list of devices.
|
||||
* This method must be called from the getCryptoHandler() thread.
|
||||
*
|
||||
* @param payloadFields fields to include in the encrypted payload.
|
||||
* @param deviceInfos list of device infos to encrypt for.
|
||||
* @return the content for an m.room.encrypted event.
|
||||
*/
|
||||
fun encryptMessage(payloadFields: Map<String, Any>, deviceInfos: List<MXDeviceInfo>): EncryptedMessage {
|
||||
val deviceInfoParticipantKey = HashMap<String, MXDeviceInfo>()
|
||||
val participantKeys = ArrayList<String>()
|
||||
|
||||
for (di in deviceInfos) {
|
||||
participantKeys.add(di.identityKey()!!)
|
||||
deviceInfoParticipantKey[di.identityKey()!!] = di
|
||||
}
|
||||
|
||||
val payloadJson = HashMap(payloadFields)
|
||||
|
||||
payloadJson["sender"] = credentials.userId
|
||||
payloadJson["sender_device"] = credentials.deviceId
|
||||
|
||||
// Include the Ed25519 key so that the recipient knows what
|
||||
// device this message came from.
|
||||
// We don't need to include the curve25519 key since the
|
||||
// recipient will already know this from the olm headers.
|
||||
// When combined with the device keys retrieved from the
|
||||
// homeserver signed by the ed25519 key this proves that
|
||||
// the curve25519 key and the ed25519 key are owned by
|
||||
// the same device.
|
||||
val keysMap = HashMap<String, String>()
|
||||
keysMap["ed25519"] = olmDevice.deviceEd25519Key!!
|
||||
payloadJson["keys"] = keysMap
|
||||
|
||||
val ciphertext = HashMap<String, Any>()
|
||||
|
||||
for (deviceKey in participantKeys) {
|
||||
val sessionId = olmDevice.getSessionId(deviceKey)
|
||||
|
||||
if (!TextUtils.isEmpty(sessionId)) {
|
||||
Timber.v("Using sessionid $sessionId for device $deviceKey")
|
||||
val deviceInfo = deviceInfoParticipantKey[deviceKey]
|
||||
|
||||
payloadJson["recipient"] = deviceInfo!!.userId
|
||||
|
||||
val recipientsKeysMap = HashMap<String, String>()
|
||||
recipientsKeysMap["ed25519"] = deviceInfo.fingerprint()!!
|
||||
payloadJson["recipient_keys"] = recipientsKeysMap
|
||||
|
||||
// FIXME We have to canonicalize the JSON
|
||||
//JsonUtility.canonicalize(JsonUtility.getGson(false).toJsonTree(payloadJson)).toString()
|
||||
|
||||
val payloadString = convertToUTF8(MoshiProvider.getCanonicalJson(Map::class.java, payloadJson))
|
||||
ciphertext[deviceKey] = olmDevice.encryptMessage(deviceKey, sessionId!!, payloadString!!)!!
|
||||
}
|
||||
}
|
||||
|
||||
val res = EncryptedMessage()
|
||||
|
||||
res.algorithm = MXCRYPTO_ALGORITHM_OLM
|
||||
res.senderKey = olmDevice.deviceCurve25519Key
|
||||
res.cipherText = ciphertext
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
}
|
|
@ -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.internal.crypto.actions
|
||||
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.KeysBackup
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
import timber.log.Timber
|
||||
|
||||
internal class SetDeviceVerificationAction(private val cryptoStore: IMXCryptoStore,
|
||||
private val credentials: Credentials,
|
||||
private val keysBackup: KeysBackup) {
|
||||
|
||||
fun handle(verificationStatus: Int, deviceId: String, userId: String) {
|
||||
val device = cryptoStore.getUserDevice(deviceId, userId)
|
||||
|
||||
// Sanity check
|
||||
if (null == device) {
|
||||
Timber.w("## setDeviceVerification() : Unknown device $userId:$deviceId")
|
||||
return
|
||||
}
|
||||
|
||||
if (device.verified != verificationStatus) {
|
||||
device.verified = verificationStatus
|
||||
cryptoStore.storeUserDevice(userId, device)
|
||||
|
||||
if (userId == credentials.userId) {
|
||||
// If one of the user's own devices is being marked as verified / unverified,
|
||||
// check the key backup status, since whether or not we use this depends on
|
||||
// whether it has a signature from a verified device
|
||||
keysBackup.checkAndStartKeysBackup()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* 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.session.events.model.Event
|
||||
import im.vector.matrix.android.internal.crypto.IncomingRoomKeyRequest
|
||||
import im.vector.matrix.android.internal.crypto.MXDecryptionException
|
||||
import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.KeysBackup
|
||||
|
||||
/**
|
||||
* An interface for decrypting data
|
||||
*/
|
||||
internal interface IMXDecrypting {
|
||||
|
||||
/**
|
||||
* 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)
|
||||
suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult?
|
||||
|
||||
/**
|
||||
* Handle a key event.
|
||||
*
|
||||
* @param event the key event.
|
||||
*/
|
||||
fun onRoomKeyEvent(event: Event, keysBackup: KeysBackup) {}
|
||||
|
||||
/**
|
||||
* 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 = false
|
||||
|
||||
/**
|
||||
* Send the response to a room key request.
|
||||
*
|
||||
* @param request keyRequest
|
||||
*/
|
||||
fun shareKeysWithDevice(request: IncomingRoomKeyRequest) {}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* 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 arrow.core.Try
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.events.model.Content
|
||||
|
||||
/**
|
||||
* An interface for encrypting data
|
||||
*/
|
||||
internal interface IMXEncrypting {
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @return the encrypted content wrapped by [Try]
|
||||
*/
|
||||
suspend fun encryptEventContent(eventContent: Content, eventType: String, userIds: List<String>): Try<Content>
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright 2016 OpenMarket Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.crypto.algorithms
|
||||
|
||||
import im.vector.matrix.android.api.util.JsonDict
|
||||
|
||||
/**
|
||||
* This class represents the decryption result.
|
||||
*/
|
||||
data class MXDecryptionResult(
|
||||
/**
|
||||
* The decrypted payload (with properties 'type', 'content')
|
||||
*/
|
||||
var payload: JsonDict? = null,
|
||||
|
||||
/**
|
||||
* keys that the sender of the event claims ownership of:
|
||||
* map from key type to base64-encoded key.
|
||||
*/
|
||||
var keysClaimed: Map<String, String>? = null,
|
||||
|
||||
/**
|
||||
* The curve25519 key that the sender of the event is known to have ownership of.
|
||||
*/
|
||||
var senderKey: String? = null,
|
||||
|
||||
/**
|
||||
* Devices which forwarded this session to us (normally empty).
|
||||
*/
|
||||
var forwardingCurve25519KeyChain: List<String>? = null
|
||||
)
|
|
@ -0,0 +1,357 @@
|
|||
/*
|
||||
* 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 arrow.core.Try
|
||||
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.actions.EnsureOlmSessionsForDevicesAction
|
||||
import im.vector.matrix.android.internal.crypto.actions.MessageEncrypter
|
||||
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.keysbackup.KeysBackup
|
||||
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.event.EncryptedEventContent
|
||||
import im.vector.matrix.android.internal.crypto.model.event.RoomKeyContent
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.ForwardedRoomKeyContent
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
|
||||
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
|
||||
internal class MXMegolmDecryption(private val credentials: Credentials,
|
||||
private val olmDevice: MXOlmDevice,
|
||||
private val deviceListManager: DeviceListManager,
|
||||
private val outgoingRoomKeyRequestManager: OutgoingRoomKeyRequestManager,
|
||||
private val messageEncrypter: MessageEncrypter,
|
||||
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
|
||||
private val cryptoStore: IMXCryptoStore,
|
||||
private val sendToDeviceTask: SendToDeviceTask,
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers)
|
||||
: IMXDecrypting {
|
||||
|
||||
/**
|
||||
* Events which we couldn't decrypt due to unknown sessions / indexes: map from
|
||||
* senderKey|sessionId to timelines to list of MatrixEvents.
|
||||
*/
|
||||
private var pendingEvents: MutableMap<String /* senderKey|sessionId */, MutableMap<String /* timelineId */, MutableList<Event>>> = HashMap()
|
||||
|
||||
@Throws(MXDecryptionException::class)
|
||||
override suspend 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? {
|
||||
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 = olmDevice.decryptGroupMessage(encryptedEventContent.ciphertext!!, event.roomId!!, timeline, encryptedEventContent.sessionId!!, encryptedEventContent.senderKey!!)
|
||||
} catch (e: MXDecryptionException) {
|
||||
cryptoError = e.cryptoError
|
||||
}
|
||||
// the decryption succeeds
|
||||
if (decryptGroupMessageResult?.payload != null && cryptoError == null) {
|
||||
eventDecryptionResult = MXEventDecryptionResult()
|
||||
|
||||
eventDecryptionResult.clearEvent = decryptGroupMessageResult.payload
|
||||
eventDecryptionResult.senderCurve25519Key = decryptGroupMessageResult.senderKey
|
||||
|
||||
if (null != decryptGroupMessageResult.keysClaimed) {
|
||||
eventDecryptionResult.claimedEd25519Key = decryptGroupMessageResult.keysClaimed!!["ed25519"]
|
||||
}
|
||||
|
||||
eventDecryptionResult.forwardingCurve25519KeyChain = decryptGroupMessageResult.forwardingCurve25519KeyChain!!
|
||||
} else if (cryptoError != null) {
|
||||
if (cryptoError.isOlmError) {
|
||||
if (MXCryptoError.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"] = credentials.userId // TODO Replace this hard coded keys (see OutgoingRoomKeyRequestManager)
|
||||
selfMap["deviceId"] = "*"
|
||||
recipients.add(selfMap)
|
||||
|
||||
if (!TextUtils.equals(sender, credentials.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
|
||||
|
||||
outgoingRoomKeyRequestManager.sendRoomKeyRequest(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) {
|
||||
val encryptedEventContent = event.content.toModel<EncryptedEventContent>() ?: return
|
||||
val pendingEventsKey = "${encryptedEventContent.senderKey}|${encryptedEventContent.sessionId}"
|
||||
|
||||
if (!pendingEvents.containsKey(pendingEventsKey)) {
|
||||
pendingEvents[pendingEventsKey] = HashMap()
|
||||
}
|
||||
|
||||
if (!pendingEvents[pendingEventsKey]!!.containsKey(timelineId)) {
|
||||
pendingEvents[pendingEventsKey]!![timelineId] = ArrayList()
|
||||
}
|
||||
|
||||
if (pendingEvents[pendingEventsKey]!![timelineId]!!.indexOf(event) < 0) {
|
||||
Timber.v("## addEventToPendingList() : add Event " + event.eventId + " in room id " + event.roomId)
|
||||
pendingEvents[pendingEventsKey]!![timelineId]!!.add(event)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a key event.
|
||||
*
|
||||
* @param event the key event.
|
||||
*/
|
||||
override fun onRoomKeyEvent(event: Event, keysBackup: KeysBackup) {
|
||||
var exportFormat = false
|
||||
val roomKeyContent = event.getClearContent().toModel<RoomKeyContent>() ?: return
|
||||
|
||||
var senderKey: String? = event.getSenderKey()
|
||||
var keysClaimed: MutableMap<String, String> = HashMap()
|
||||
var forwardingCurve25519KeyChain: MutableList<String> = ArrayList()
|
||||
|
||||
if (TextUtils.isEmpty(roomKeyContent.roomId) || TextUtils.isEmpty(roomKeyContent.sessionId) || TextUtils.isEmpty(roomKeyContent.sessionKey)) {
|
||||
Timber.e("## onRoomKeyEvent() : Key event is missing fields")
|
||||
return
|
||||
}
|
||||
if (event.getClearType() == EventType.FORWARDED_ROOM_KEY) {
|
||||
Timber.v("## onRoomKeyEvent(), forward adding key : roomId " + roomKeyContent.roomId + " sessionId " + roomKeyContent.sessionId
|
||||
+ " sessionKey " + roomKeyContent.sessionKey) // from " + event);
|
||||
val forwardedRoomKeyContent = event.getClearContent().toModel<ForwardedRoomKeyContent>() ?: return
|
||||
forwardingCurve25519KeyChain = if (forwardedRoomKeyContent.forwardingCurve25519KeyChain == null) {
|
||||
ArrayList()
|
||||
} else {
|
||||
ArrayList(forwardedRoomKeyContent.forwardingCurve25519KeyChain)
|
||||
}
|
||||
forwardingCurve25519KeyChain.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.v("## 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 = olmDevice.addInboundGroupSession(roomKeyContent.sessionId!!, roomKeyContent.sessionKey!!, roomKeyContent.roomId!!, senderKey, forwardingCurve25519KeyChain, keysClaimed, exportFormat)
|
||||
|
||||
if (added) {
|
||||
keysBackup.maybeBackupKeys()
|
||||
|
||||
val content = RoomKeyRequestBody()
|
||||
|
||||
content.algorithm = roomKeyContent.algorithm
|
||||
content.roomId = roomKeyContent.roomId
|
||||
content.sessionId = roomKeyContent.sessionId
|
||||
content.senderKey = senderKey
|
||||
|
||||
outgoingRoomKeyRequestManager.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) {
|
||||
//TODO see how to handle this
|
||||
Timber.v("ON NEW SESSION $sessionId - $senderKey")
|
||||
/*val k = "$senderKey|$sessionId"
|
||||
|
||||
val pending = pendingEvents[k]
|
||||
|
||||
if (null != pending) {
|
||||
// Have another go at decrypting events sent with this session.
|
||||
pendingEvents.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)
|
||||
//mSession!!.onEventDecrypted(event)
|
||||
}
|
||||
Timber.v("## onNewSession() : successful re-decryption of " + event.eventId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
override fun hasKeysForKeyRequest(request: IncomingRoomKeyRequest): Boolean {
|
||||
return (null != request.requestBody
|
||||
&& olmDevice.hasInboundSessionKeys(request.requestBody!!.roomId!!, request.requestBody!!.senderKey!!, request.requestBody!!.sessionId!!))
|
||||
}
|
||||
|
||||
override fun shareKeysWithDevice(request: IncomingRoomKeyRequest) {
|
||||
// sanity checks
|
||||
if (request.requestBody == null) {
|
||||
return
|
||||
}
|
||||
val userId = request.userId!!
|
||||
CoroutineScope(coroutineDispatchers.crypto).launch {
|
||||
deviceListManager
|
||||
.downloadKeys(listOf(userId), false)
|
||||
.flatMap {
|
||||
val deviceId = request.deviceId
|
||||
val deviceInfo = cryptoStore.getUserDevice(deviceId!!, userId)
|
||||
if (deviceInfo == null) {
|
||||
throw RuntimeException()
|
||||
} else {
|
||||
val devicesByUser = HashMap<String, List<MXDeviceInfo>>()
|
||||
devicesByUser[userId] = ArrayList(Arrays.asList(deviceInfo))
|
||||
ensureOlmSessionsForDevicesAction
|
||||
.handle(devicesByUser)
|
||||
.flatMap {
|
||||
val body = request.requestBody
|
||||
val olmSessionResult = it.getObject(deviceId, userId)
|
||||
if (olmSessionResult?.sessionId == null) {
|
||||
// no session with this device, probably because there
|
||||
// were no one-time keys.
|
||||
Try.just(Unit)
|
||||
}
|
||||
Timber.v("## shareKeysWithDevice() : sharing keys for session " + body!!.senderKey + "|" + body.sessionId
|
||||
+ " with device " + userId + ":" + deviceId)
|
||||
val inboundGroupSession = olmDevice.getInboundGroupSession(body.sessionId, body.senderKey, body.roomId)
|
||||
|
||||
val payloadJson = HashMap<String, Any>()
|
||||
payloadJson["type"] = EventType.FORWARDED_ROOM_KEY
|
||||
payloadJson["content"] = inboundGroupSession!!.exportKeys()!!
|
||||
|
||||
val encodedPayload = messageEncrypter.encryptMessage(payloadJson, Arrays.asList(deviceInfo))
|
||||
val sendToDeviceMap = MXUsersDevicesMap<Any>()
|
||||
sendToDeviceMap.setObject(encodedPayload, userId, deviceId)
|
||||
Timber.v("## shareKeysWithDevice() : sending to $userId:$deviceId")
|
||||
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
|
||||
sendToDeviceTask.execute(sendToDeviceParams)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.crypto.algorithms.megolm
|
||||
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.internal.crypto.DeviceListManager
|
||||
import im.vector.matrix.android.internal.crypto.MXOlmDevice
|
||||
import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequestManager
|
||||
import im.vector.matrix.android.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
|
||||
import im.vector.matrix.android.internal.crypto.actions.MessageEncrypter
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
|
||||
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||
|
||||
internal class MXMegolmDecryptionFactory(private val credentials: Credentials,
|
||||
private val olmDevice: MXOlmDevice,
|
||||
private val deviceListManager: DeviceListManager,
|
||||
private val outgoingRoomKeyRequestManager: OutgoingRoomKeyRequestManager,
|
||||
private val messageEncrypter: MessageEncrypter,
|
||||
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
|
||||
private val cryptoStore: IMXCryptoStore,
|
||||
private val sendToDeviceTask: SendToDeviceTask,
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers) {
|
||||
|
||||
fun create(): MXMegolmDecryption {
|
||||
return MXMegolmDecryption(
|
||||
credentials,
|
||||
olmDevice,
|
||||
deviceListManager,
|
||||
outgoingRoomKeyRequestManager,
|
||||
messageEncrypter,
|
||||
ensureOlmSessionsForDevicesAction,
|
||||
cryptoStore,
|
||||
sendToDeviceTask,
|
||||
coroutineDispatchers)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,337 @@
|
|||
/*
|
||||
* 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 arrow.core.Try
|
||||
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.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.actions.EnsureOlmSessionsForDevicesAction
|
||||
import im.vector.matrix.android.internal.crypto.actions.MessageEncrypter
|
||||
import im.vector.matrix.android.internal.crypto.algorithms.IMXEncrypting
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.KeysBackup
|
||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
||||
import im.vector.matrix.android.internal.crypto.repository.WarnOnUnknownDeviceRepository
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
|
||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||
import im.vector.matrix.android.internal.util.convertToUTF8
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
|
||||
internal class MXMegolmEncryption(
|
||||
// The id of the room we will be sending to.
|
||||
private var roomId: String,
|
||||
private val olmDevice: MXOlmDevice,
|
||||
private val keysBackup: KeysBackup,
|
||||
private val cryptoStore: IMXCryptoStore,
|
||||
private val deviceListManager: DeviceListManager,
|
||||
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
|
||||
private val credentials: Credentials,
|
||||
private val sendToDeviceTask: SendToDeviceTask,
|
||||
private val messageEncrypter: MessageEncrypter,
|
||||
private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository
|
||||
) : IMXEncrypting {
|
||||
|
||||
|
||||
// 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 outboundSession: MXOutboundSessionInfo? = null
|
||||
|
||||
// Default rotation periods
|
||||
// TODO: Make it configurable via parameters
|
||||
// Session rotation periods
|
||||
private var sessionRotationPeriodMsgs: Int = 100
|
||||
private var sessionRotationPeriodMs: Int = 7 * 24 * 3600 * 1000
|
||||
|
||||
override suspend fun encryptEventContent(eventContent: Content,
|
||||
eventType: String,
|
||||
userIds: List<String>): Try<Content> {
|
||||
return getDevicesInRoom(userIds)
|
||||
.flatMap { ensureOutboundSession(it) }
|
||||
.flatMap {
|
||||
encryptContent(it, eventType, eventContent)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)!!, roomId, olmDevice.deviceCurve25519Key!!,
|
||||
ArrayList(), keysClaimedMap, false)
|
||||
|
||||
keysBackup.maybeBackupKeys()
|
||||
|
||||
return MXOutboundSessionInfo(sessionId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the outbound session
|
||||
*
|
||||
* @param devicesInRoom the devices list
|
||||
*/
|
||||
private suspend fun ensureOutboundSession(devicesInRoom: MXUsersDevicesMap<MXDeviceInfo>): Try<MXOutboundSessionInfo> {
|
||||
var session = outboundSession
|
||||
if (session == null
|
||||
// Need to make a brand new session?
|
||||
|| session.needsRotation(sessionRotationPeriodMsgs, sessionRotationPeriodMs)
|
||||
// Determine if we have shared with anyone we shouldn't have
|
||||
|| session.sharedWithTooManyDevices(devicesInRoom)) {
|
||||
session = prepareNewSessionInRoom()
|
||||
outboundSession = session
|
||||
}
|
||||
val safeSession = 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 == safeSession.sharedWithDevices.getObject(deviceId, userId)) {
|
||||
if (!shareMap.containsKey(userId)) {
|
||||
shareMap[userId] = ArrayList()
|
||||
}
|
||||
shareMap[userId]!!.add(deviceInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
return shareKey(safeSession, shareMap).map { safeSession!! }
|
||||
}
|
||||
|
||||
/**
|
||||
* Share the device key to a list of users
|
||||
*
|
||||
* @param session the session info
|
||||
* @param devicesByUsers the devices map
|
||||
*/
|
||||
private suspend fun shareKey(session: MXOutboundSessionInfo,
|
||||
devicesByUsers: Map<String, List<MXDeviceInfo>>): Try<Unit> {
|
||||
// nothing to send, the task is done
|
||||
if (devicesByUsers.isEmpty()) {
|
||||
Timber.v("## shareKey() : nothing more to do")
|
||||
return Try.just(Unit)
|
||||
}
|
||||
// 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) {
|
||||
devicesByUsers[userId]?.let {
|
||||
userIds.add(userId)
|
||||
subMap[userId] = it
|
||||
devicesCount += it.size
|
||||
}
|
||||
if (devicesCount > 100) {
|
||||
break
|
||||
}
|
||||
}
|
||||
Timber.v("## shareKey() ; userId $userIds")
|
||||
return shareUserDevicesKey(session, subMap)
|
||||
.flatMap {
|
||||
val remainingDevices = devicesByUsers.filterKeys { userIds.contains(it).not() }
|
||||
shareKey(session, remainingDevices)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Share the device keys of a an user
|
||||
*
|
||||
* @param session the session info
|
||||
* @param devicesByUser the devices map
|
||||
* @param callback the asynchronous callback
|
||||
*/
|
||||
private suspend fun shareUserDevicesKey(session: MXOutboundSessionInfo,
|
||||
devicesByUser: Map<String, List<MXDeviceInfo>>): Try<Unit> {
|
||||
val sessionKey = olmDevice.getSessionKey(session.sessionId)
|
||||
val chainIndex = olmDevice.getMessageIndex(session.sessionId)
|
||||
|
||||
val submap = HashMap<String, Any>()
|
||||
submap["algorithm"] = MXCRYPTO_ALGORITHM_MEGOLM
|
||||
submap["room_id"] = roomId
|
||||
submap["session_id"] = session.sessionId
|
||||
submap["session_key"] = sessionKey!!
|
||||
submap["chain_index"] = chainIndex
|
||||
|
||||
val payload = HashMap<String, Any>()
|
||||
payload["type"] = EventType.ROOM_KEY
|
||||
payload["content"] = submap
|
||||
|
||||
var t0 = System.currentTimeMillis()
|
||||
Timber.v("## shareUserDevicesKey() : starts")
|
||||
|
||||
return ensureOlmSessionsForDevicesAction.handle(devicesByUser)
|
||||
.flatMap {
|
||||
Timber.v("## shareUserDevicesKey() : ensureOlmSessionsForDevices succeeds after "
|
||||
+ (System.currentTimeMillis() - t0) + " ms")
|
||||
val contentMap = MXUsersDevicesMap<Any>()
|
||||
var haveTargets = false
|
||||
val userIds = it.userIds
|
||||
for (userId in userIds) {
|
||||
val devicesToShareWith = devicesByUser[userId]
|
||||
for ((deviceID) in devicesToShareWith!!) {
|
||||
val sessionResult = it.getObject(deviceID, userId)
|
||||
if (sessionResult?.sessionId == null) {
|
||||
// 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.v("## shareUserDevicesKey() : Sharing keys with device $userId:$deviceID")
|
||||
//noinspection ArraysAsListWithZeroOrOneArgument,ArraysAsListWithZeroOrOneArgument
|
||||
contentMap.setObject(messageEncrypter.encryptMessage(payload, Arrays.asList(sessionResult.deviceInfo)), userId, deviceID)
|
||||
haveTargets = true
|
||||
}
|
||||
}
|
||||
if (haveTargets) {
|
||||
t0 = System.currentTimeMillis()
|
||||
Timber.v("## shareUserDevicesKey() : has target")
|
||||
val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, contentMap)
|
||||
sendToDeviceTask.execute(sendToDeviceParams)
|
||||
.map {
|
||||
Timber.v("## 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.sharedWithDevices.setObject(chainIndex, userId, deviceId)
|
||||
}
|
||||
}
|
||||
Unit
|
||||
}
|
||||
} else {
|
||||
Timber.v("## shareUserDevicesKey() : no need to sharekey")
|
||||
Try.just(Unit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* process the pending encryptions
|
||||
*/
|
||||
private suspend fun encryptContent(session: MXOutboundSessionInfo, eventType: String, eventContent: Content) = Try<Content> {
|
||||
// Everything is in place, encrypt all pending events
|
||||
val payloadJson = HashMap<String, Any>()
|
||||
payloadJson["room_id"] = roomId
|
||||
payloadJson["type"] = eventType
|
||||
payloadJson["content"] = eventContent
|
||||
|
||||
// Get canonical Json from
|
||||
|
||||
val payloadString = convertToUTF8(MoshiProvider.getCanonicalJson(Map::class.java, payloadJson))
|
||||
val ciphertext = olmDevice.encryptGroupMessage(session.sessionId, payloadString!!)
|
||||
|
||||
val map = HashMap<String, Any>()
|
||||
map["algorithm"] = MXCRYPTO_ALGORITHM_MEGOLM
|
||||
map["sender_key"] = olmDevice.deviceCurve25519Key!!
|
||||
map["ciphertext"] = ciphertext!!
|
||||
map["session_id"] = session.sessionId
|
||||
|
||||
// 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"] = credentials.deviceId!!
|
||||
session.useCount++
|
||||
map
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 suspend fun getDevicesInRoom(userIds: List<String>): Try<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.
|
||||
return deviceListManager
|
||||
.downloadKeys(userIds, false)
|
||||
.flatMap {
|
||||
val encryptToVerifiedDevicesOnly = cryptoStore.getGlobalBlacklistUnverifiedDevices()
|
||||
|| cryptoStore.getRoomsListBlacklistUnverifiedDevices().contains(roomId)
|
||||
|
||||
val devicesInRoom = MXUsersDevicesMap<MXDeviceInfo>()
|
||||
val unknownDevices = MXUsersDevicesMap<MXDeviceInfo>()
|
||||
|
||||
for (userId in it.userIds) {
|
||||
val deviceIds = it.getUserDeviceIds(userId) ?: continue
|
||||
for (deviceId in deviceIds) {
|
||||
val deviceInfo = it.getObject(deviceId, userId) ?: continue
|
||||
if (warnOnUnknownDevicesRepository.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)
|
||||
}
|
||||
}
|
||||
if (unknownDevices.isEmpty) {
|
||||
Try.just(devicesInRoom)
|
||||
} else {
|
||||
val cryptoError = MXCryptoError(MXCryptoError.UNKNOWN_DEVICES_CODE,
|
||||
MXCryptoError.UNABLE_TO_ENCRYPT, MXCryptoError.UNKNOWN_DEVICES_REASON, unknownDevices)
|
||||
Try.Failure(Failure.CryptoError(cryptoError))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* 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.algorithms.megolm
|
||||
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.internal.crypto.DeviceListManager
|
||||
import im.vector.matrix.android.internal.crypto.MXOlmDevice
|
||||
import im.vector.matrix.android.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
|
||||
import im.vector.matrix.android.internal.crypto.actions.MessageEncrypter
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.KeysBackup
|
||||
import im.vector.matrix.android.internal.crypto.repository.WarnOnUnknownDeviceRepository
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
|
||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
|
||||
internal class MXMegolmEncryptionFactory(
|
||||
private val olmDevice: MXOlmDevice,
|
||||
private val keysBackup: KeysBackup,
|
||||
private val cryptoStore: IMXCryptoStore,
|
||||
private val deviceListManager: DeviceListManager,
|
||||
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
|
||||
private val credentials: Credentials,
|
||||
private val sendToDeviceTask: SendToDeviceTask,
|
||||
// FIXME Why taskExecutor is not used?
|
||||
private val taskExecutor: TaskExecutor,
|
||||
private val messageEncrypter: MessageEncrypter,
|
||||
private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository) {
|
||||
|
||||
fun create(roomId: String): MXMegolmEncryption {
|
||||
return MXMegolmEncryption(
|
||||
roomId,
|
||||
olmDevice,
|
||||
keysBackup,
|
||||
cryptoStore,
|
||||
deviceListManager,
|
||||
ensureOlmSessionsForDevicesAction,
|
||||
credentials,
|
||||
sendToDeviceTask,
|
||||
messageEncrypter,
|
||||
warnOnUnknownDevicesRepository)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
||||
internal class MXOutboundSessionInfo(
|
||||
// The id of the session
|
||||
val sessionId: String) {
|
||||
// When the session was created
|
||||
private val creationTime = System.currentTimeMillis()
|
||||
|
||||
// Number of times this session has been used
|
||||
var useCount: Int = 0
|
||||
|
||||
// Devices with which we have shared the session key
|
||||
// userId -> {deviceId -> msgindex}
|
||||
val sharedWithDevices: MXUsersDevicesMap<Int> = MXUsersDevicesMap()
|
||||
|
||||
fun needsRotation(rotationPeriodMsgs: Int, rotationPeriodMs: Int): Boolean {
|
||||
var needsRotation = false
|
||||
val sessionLifetime = System.currentTimeMillis() - creationTime
|
||||
|
||||
if (useCount >= rotationPeriodMsgs || sessionLifetime >= rotationPeriodMs) {
|
||||
Timber.v("## needsRotation() : Rotating megolm session after " + useCount + ", " + 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 = sharedWithDevices.userIds
|
||||
|
||||
for (userId in userIds) {
|
||||
if (null == devicesInRoom.getUserDeviceIds(userId)) {
|
||||
Timber.v("## sharedWithTooManyDevices() : Starting new session because we shared with $userId")
|
||||
return true
|
||||
}
|
||||
|
||||
val deviceIds = sharedWithDevices.getUserDeviceIds(userId)
|
||||
|
||||
for (deviceId in deviceIds!!) {
|
||||
if (null == devicesInRoom.getObject(deviceId, userId)) {
|
||||
Timber.v("## sharedWithTooManyDevices() : Starting new session because we shared with $userId:$deviceId")
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
|
@ -0,0 +1,235 @@
|
|||
/*
|
||||
* 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
|
||||
import im.vector.matrix.android.api.util.JSON_DICT_PARAMETERIZED_TYPE
|
||||
import im.vector.matrix.android.api.util.JsonDict
|
||||
import im.vector.matrix.android.internal.crypto.MXDecryptionException
|
||||
import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult
|
||||
import im.vector.matrix.android.internal.crypto.MXOlmDevice
|
||||
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
|
||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||
import im.vector.matrix.android.internal.util.convertFromUTF8
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
|
||||
internal class MXOlmDecryption(
|
||||
// The olm device interface
|
||||
private val olmDevice: MXOlmDevice,
|
||||
// the matrix credentials
|
||||
private val credentials: Credentials)
|
||||
: IMXDecrypting {
|
||||
|
||||
@Throws(MXDecryptionException::class)
|
||||
override suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult? {
|
||||
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(olmDevice.deviceCurve25519Key)) {
|
||||
Timber.e("## decryptEvent() : our device " + olmDevice.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!![olmDevice.deviceCurve25519Key] as JsonDict
|
||||
val decryptedPayload = decryptMessage(message, olmEventContent.senderKey!!)
|
||||
|
||||
if (decryptedPayload == 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 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)
|
||||
|
||||
if (payload == null) {
|
||||
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(payloadString)
|
||||
|
||||
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, credentials.userId)) {
|
||||
Timber.e("## decryptEvent() : Event " + event.eventId + ": Intended recipient " + olmPayloadContent.recipient
|
||||
+ " does not match our id " + credentials.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, olmDevice.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()
|
||||
result.clearEvent = payload
|
||||
result.senderCurve25519Key = olmEventContent.senderKey
|
||||
result.claimedEd25519Key = olmPayloadContent.keys!!.get("ed25519")
|
||||
|
||||
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.
|
||||
*/
|
||||
private fun decryptMessage(message: JsonDict, theirDeviceIdentityKey: String): String? {
|
||||
val sessionIdsSet = olmDevice.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 = olmDevice.decryptMessage(messageBody, messageType, sessionId, theirDeviceIdentityKey)
|
||||
|
||||
if (null != payload) {
|
||||
Timber.v("## decryptMessage() : Decrypted Olm message from $theirDeviceIdentityKey with session $sessionId")
|
||||
return payload
|
||||
} else {
|
||||
val foundSession = olmDevice.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 = olmDevice.createInboundSession(theirDeviceIdentityKey, messageType, messageBody)
|
||||
|
||||
if (null == res) {
|
||||
Timber.e("## decryptMessage() : Error decrypting non-prekey message with existing sessions")
|
||||
return null
|
||||
}
|
||||
|
||||
Timber.v("## decryptMessage() : Created new inbound Olm session get id " + res["session_id"] + " with " + theirDeviceIdentityKey)
|
||||
|
||||
return res["payload"]
|
||||
}
|
||||
}
|
|
@ -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.algorithms.olm
|
||||
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.internal.crypto.MXOlmDevice
|
||||
|
||||
internal class MXOlmDecryptionFactory(private val olmDevice: MXOlmDevice,
|
||||
private val credentials: Credentials) {
|
||||
|
||||
fun create(): MXOlmDecryption {
|
||||
return MXOlmDecryption(
|
||||
olmDevice,
|
||||
credentials)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* 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 arrow.core.Try
|
||||
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.actions.EnsureOlmSessionsForUsersAction
|
||||
import im.vector.matrix.android.internal.crypto.actions.MessageEncrypter
|
||||
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.store.IMXCryptoStore
|
||||
import java.util.*
|
||||
|
||||
internal class MXOlmEncryption(
|
||||
private var roomId: String,
|
||||
private val olmDevice: MXOlmDevice,
|
||||
private val cryptoStore: IMXCryptoStore,
|
||||
private val messageEncrypter: MessageEncrypter,
|
||||
private val deviceListManager: DeviceListManager,
|
||||
private val ensureOlmSessionsForUsersAction: EnsureOlmSessionsForUsersAction)
|
||||
: IMXEncrypting {
|
||||
|
||||
override suspend fun encryptEventContent(eventContent: Content, eventType: String, userIds: List<String>): Try<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
|
||||
return ensureSession(userIds)
|
||||
.map {
|
||||
val deviceInfos = ArrayList<MXDeviceInfo>()
|
||||
for (userId in userIds) {
|
||||
val devices = cryptoStore.getUserDevices(userId)?.values ?: emptyList()
|
||||
for (device in devices) {
|
||||
val key = device.identityKey()
|
||||
if (TextUtils.equals(key, olmDevice.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"] = roomId
|
||||
messageMap["type"] = eventType
|
||||
messageMap["content"] = eventContent
|
||||
|
||||
messageEncrypter.encryptMessage(messageMap, deviceInfos)
|
||||
messageMap.toContent()!!
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Ensure that the session
|
||||
*
|
||||
* @param users the user ids list
|
||||
* @param callback the asynchronous callback
|
||||
*/
|
||||
private suspend fun ensureSession(users: List<String>): Try<Unit> {
|
||||
return deviceListManager
|
||||
.downloadKeys(users, false)
|
||||
.flatMap { ensureOlmSessionsForUsersAction.handle(users) }
|
||||
.map { Unit }
|
||||
|
||||
}
|
||||
}
|
|
@ -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.algorithms.olm
|
||||
|
||||
import im.vector.matrix.android.internal.crypto.DeviceListManager
|
||||
import im.vector.matrix.android.internal.crypto.MXOlmDevice
|
||||
import im.vector.matrix.android.internal.crypto.actions.EnsureOlmSessionsForUsersAction
|
||||
import im.vector.matrix.android.internal.crypto.actions.MessageEncrypter
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||
|
||||
internal class MXOlmEncryptionFactory(private val olmDevice: MXOlmDevice,
|
||||
private val cryptoStore: IMXCryptoStore,
|
||||
private val messageEncrypter: MessageEncrypter,
|
||||
private val deviceListManager: DeviceListManager,
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||
private val ensureOlmSessionsForUsersAction: EnsureOlmSessionsForUsersAction) {
|
||||
|
||||
fun create(roomId: String): MXOlmEncryption {
|
||||
return MXOlmEncryption(
|
||||
roomId,
|
||||
olmDevice,
|
||||
cryptoStore,
|
||||
messageEncrypter,
|
||||
deviceListManager,
|
||||
ensureOlmSessionsForUsersAction)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
/*
|
||||
* Copyright 2014 OpenMarket Ltd
|
||||
* Copyright 2017 Vector Creations Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package im.vector.matrix.android.internal.crypto.api
|
||||
|
||||
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.*
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.KeysUploadBody
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.SendToDeviceBody
|
||||
import im.vector.matrix.android.internal.network.NetworkConstants
|
||||
import retrofit2.Call
|
||||
import retrofit2.http.*
|
||||
|
||||
internal interface CryptoApi {
|
||||
|
||||
/**
|
||||
* Get the devices list
|
||||
* Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#get-matrix-client-r0-devices
|
||||
*/
|
||||
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "devices")
|
||||
fun getDevices(): Call<DevicesListResponse>
|
||||
|
||||
/**
|
||||
* Upload device and/or one-time keys.
|
||||
* Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-keys-upload
|
||||
*
|
||||
* @param params the params.
|
||||
*/
|
||||
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "keys/upload")
|
||||
fun uploadKeys(@Body body: KeysUploadBody): Call<KeysUploadResponse>
|
||||
|
||||
/**
|
||||
* Upload device and/or one-time keys.
|
||||
* Doc: not documented
|
||||
*
|
||||
* @param deviceId the deviceId
|
||||
* @param params the params.
|
||||
*/
|
||||
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "keys/upload/{deviceId}")
|
||||
fun uploadKeys(@Path("deviceId") deviceId: String, @Body body: KeysUploadBody): Call<KeysUploadResponse>
|
||||
|
||||
/**
|
||||
* Download device keys.
|
||||
* Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-keys-query
|
||||
*
|
||||
* @param params the params.
|
||||
*/
|
||||
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "keys/query")
|
||||
fun downloadKeysForUsers(@Body params: KeysQueryBody): Call<KeysQueryResponse>
|
||||
|
||||
/**
|
||||
* Claim one-time keys.
|
||||
* Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-keys-claim
|
||||
*
|
||||
* @param params the params.
|
||||
*/
|
||||
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "keys/claim")
|
||||
fun claimOneTimeKeysForUsersDevices(@Body body: KeysClaimBody): Call<KeysClaimResponse>
|
||||
|
||||
/**
|
||||
* Send an event to a specific list of devices
|
||||
* Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#put-matrix-client-r0-sendtodevice-eventtype-txnid
|
||||
*
|
||||
* @param eventType the type of event to send
|
||||
* @param transactionId the transaction ID for this event
|
||||
* @param body the body
|
||||
*/
|
||||
@PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "sendToDevice/{eventType}/{txnId}")
|
||||
fun sendToDevice(@Path("eventType") eventType: String, @Path("txnId") transactionId: String, @Body body: SendToDeviceBody): Call<Unit>
|
||||
|
||||
/**
|
||||
* Delete a device.
|
||||
* Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#delete-matrix-client-r0-devices-deviceid
|
||||
*
|
||||
* @param deviceId the device id
|
||||
* @param params the deletion parameters
|
||||
*/
|
||||
@HTTP(path = NetworkConstants.URI_API_PREFIX_PATH_R0 + "devices/{device_id}", method = "DELETE", hasBody = true)
|
||||
fun deleteDevice(@Path("device_id") deviceId: String, @Body params: DeleteDeviceParams): Call<Unit>
|
||||
|
||||
/**
|
||||
* Update the device information.
|
||||
* Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#put-matrix-client-r0-devices-deviceid
|
||||
*
|
||||
* @param deviceId the device id
|
||||
* @param params the params
|
||||
*/
|
||||
@PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "devices/{device_id}")
|
||||
fun updateDeviceInfo(@Path("device_id") deviceId: String, @Body params: UpdateDeviceInfoBody): Call<Unit>
|
||||
|
||||
/**
|
||||
* Get the update devices list from two sync token.
|
||||
* Doc: https://matrix.org/docs/spec/client_server/r0.4.0.html#get-matrix-client-r0-keys-changes
|
||||
*
|
||||
* @param oldToken the start token.
|
||||
* @param newToken the up-to token.
|
||||
*/
|
||||
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "keys/changes")
|
||||
fun getKeyChanges(@Query("from") oldToken: String, @Query("to") newToken: String): Call<KeyChangesResponse>
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,153 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Utility to compute a backup private key from a password and vice-versa.
|
||||
*/
|
||||
package im.vector.matrix.android.internal.crypto.keysbackup
|
||||
|
||||
import androidx.annotation.WorkerThread
|
||||
import im.vector.matrix.android.api.listeners.ProgressListener
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
import javax.crypto.Mac
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
import kotlin.experimental.xor
|
||||
|
||||
|
||||
private const val SALT_LENGTH = 32
|
||||
private const val DEFAULT_ITERATION = 500_000
|
||||
|
||||
data class GeneratePrivateKeyResult(
|
||||
// The private key
|
||||
val privateKey: ByteArray,
|
||||
// the salt used to generate the private key
|
||||
val salt: String,
|
||||
// number of key derivations done on the generated private key.
|
||||
val iterations: Int)
|
||||
|
||||
/**
|
||||
* Compute a private key from a password.
|
||||
*
|
||||
* @param password the password to use.
|
||||
*
|
||||
* @return a {privateKey, salt, iterations} tuple.
|
||||
*/
|
||||
@WorkerThread
|
||||
fun generatePrivateKeyWithPassword(password: String, progressListener: ProgressListener?): GeneratePrivateKeyResult {
|
||||
val salt = generateSalt()
|
||||
val iterations = DEFAULT_ITERATION
|
||||
val privateKey = deriveKey(password, salt, iterations, progressListener)
|
||||
|
||||
return GeneratePrivateKeyResult(privateKey, salt, iterations)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a private key from {password, salt, iterations}
|
||||
*
|
||||
* @param password the password used to generated the private key.
|
||||
* @param salt the salt.
|
||||
* @param iterations number of key derivations.
|
||||
* @param progressListener the progress listener
|
||||
*
|
||||
* @return a private key.
|
||||
*/
|
||||
@WorkerThread
|
||||
fun retrievePrivateKeyWithPassword(password: String,
|
||||
salt: String,
|
||||
iterations: Int,
|
||||
progressListener: ProgressListener? = null): ByteArray {
|
||||
return deriveKey(password, salt, iterations, progressListener)
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute a private key by deriving a password and a salt strings.
|
||||
*
|
||||
* @param password the password.
|
||||
* @param salt the salt.
|
||||
* @param iterations number of derivations.
|
||||
* @param progressListener a listener to follow progress.
|
||||
*
|
||||
* @return a private key.
|
||||
*/
|
||||
@WorkerThread
|
||||
private fun deriveKey(password: String,
|
||||
salt: String,
|
||||
iterations: Int,
|
||||
progressListener: ProgressListener?): ByteArray {
|
||||
// Note: copied and adapted from MXMegolmExportEncryption
|
||||
val t0 = System.currentTimeMillis()
|
||||
|
||||
// based on https://en.wikipedia.org/wiki/PBKDF2 algorithm
|
||||
// it is simpler than the generic algorithm because the expected key length is equal to the mac key length.
|
||||
// noticed as dklen/hlen
|
||||
|
||||
// dklen = 256
|
||||
// hlen = 512
|
||||
val prf = Mac.getInstance("HmacSHA512")
|
||||
|
||||
prf.init(SecretKeySpec(password.toByteArray(), "HmacSHA512"))
|
||||
|
||||
// 256 bits key length
|
||||
val dk = ByteArray(32)
|
||||
val uc = ByteArray(64)
|
||||
|
||||
// U1 = PRF(Password, Salt || INT_32_BE(i)) with i goes from 1 to dklen/hlen
|
||||
prf.update(salt.toByteArray())
|
||||
val int32BE = byteArrayOf(0, 0, 0, 1)
|
||||
prf.update(int32BE)
|
||||
prf.doFinal(uc, 0)
|
||||
|
||||
// copy to the key
|
||||
System.arraycopy(uc, 0, dk, 0, dk.size)
|
||||
|
||||
var lastProgress = -1
|
||||
|
||||
for (index in 2..iterations) {
|
||||
// Uc = PRF(Password, Uc-1)
|
||||
prf.update(uc)
|
||||
prf.doFinal(uc, 0)
|
||||
|
||||
// F(Password, Salt, c, i) = U1 ^ U2 ^ ... ^ Uc
|
||||
for (byteIndex in dk.indices) {
|
||||
dk[byteIndex] = dk[byteIndex] xor uc[byteIndex]
|
||||
}
|
||||
|
||||
val progress = (index + 1) * 100 / iterations
|
||||
if (progress != lastProgress) {
|
||||
lastProgress = progress
|
||||
progressListener?.onProgress(lastProgress, 100)
|
||||
}
|
||||
}
|
||||
|
||||
Timber.v("KeysBackupPassword", "## deriveKeys() : " + iterations + " in " + (System.currentTimeMillis() - t0) + " ms")
|
||||
|
||||
return dk
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a 32 chars salt
|
||||
*/
|
||||
private fun generateSalt(): String {
|
||||
var salt = ""
|
||||
|
||||
do {
|
||||
salt += UUID.randomUUID().toString()
|
||||
} while (salt.length < SALT_LENGTH)
|
||||
|
||||
|
||||
return salt.substring(0, SALT_LENGTH)
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.crypto.keysbackup
|
||||
|
||||
import android.os.Handler
|
||||
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState
|
||||
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupStateListener
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
|
||||
internal class KeysBackupStateManager(private val uiHandler: Handler) {
|
||||
|
||||
private val listeners = ArrayList<KeysBackupStateListener>()
|
||||
|
||||
// Backup state
|
||||
var state = KeysBackupState.Unknown
|
||||
set(newState) {
|
||||
Timber.v("KeysBackup: setState: $field -> $newState")
|
||||
|
||||
field = newState
|
||||
|
||||
// Notify listeners about the state change, on the ui thread
|
||||
uiHandler.post {
|
||||
synchronized(listeners) {
|
||||
listeners.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: KeysBackupStateListener) {
|
||||
synchronized(listeners) {
|
||||
listeners.add(listener)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeListener(listener: KeysBackupStateListener) {
|
||||
synchronized(listeners) {
|
||||
listeners.remove(listener)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,180 @@
|
|||
/*
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.crypto.keysbackup.api
|
||||
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.*
|
||||
import im.vector.matrix.android.internal.network.NetworkConstants
|
||||
import retrofit2.Call
|
||||
import retrofit2.http.*
|
||||
|
||||
/**
|
||||
* Ref: https://github.com/uhoreg/matrix-doc/blob/e2e_backup/proposals/1219-storing-megolm-keys-serverside.md
|
||||
*/
|
||||
internal interface RoomKeysApi {
|
||||
|
||||
/* ==========================================================================================
|
||||
* Backup versions management
|
||||
* ========================================================================================== */
|
||||
|
||||
/**
|
||||
* Create a new keys backup version.
|
||||
* @param createKeysBackupVersionBody the body
|
||||
*/
|
||||
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "room_keys/version")
|
||||
fun createKeysBackupVersion(@Body createKeysBackupVersionBody: CreateKeysBackupVersionBody): Call<KeysVersion>
|
||||
|
||||
/**
|
||||
* Get the key backup last version
|
||||
* If not supported by the server, an error is returned: {"errcode":"M_NOT_FOUND","error":"No backup found"}
|
||||
*/
|
||||
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "room_keys/version")
|
||||
fun getKeysBackupLastVersion(): Call<KeysVersionResult>
|
||||
|
||||
/**
|
||||
* Get information about the given version.
|
||||
* If not supported by the server, an error is returned: {"errcode":"M_NOT_FOUND","error":"No backup found"}
|
||||
*
|
||||
* @param version version
|
||||
*/
|
||||
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "room_keys/version/{version}")
|
||||
fun getKeysBackupVersion(@Path("version") version: String): Call<KeysVersionResult>
|
||||
|
||||
/**
|
||||
* Update information about the given version.
|
||||
* @param version version
|
||||
* @param updateKeysBackupVersionBody the body
|
||||
*/
|
||||
@PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "room_keys/version/{version}")
|
||||
fun updateKeysBackupVersion(@Path("version") version: String,
|
||||
@Body keysBackupVersionBody: UpdateKeysBackupVersionBody): Call<Unit>
|
||||
|
||||
/* ==========================================================================================
|
||||
* Storing keys
|
||||
* ========================================================================================== */
|
||||
|
||||
/**
|
||||
* Store the key for the given session in the given room, using the given backup version.
|
||||
*
|
||||
*
|
||||
* If the server already has a backup in the backup version for the given session and room, then it will
|
||||
* keep the "better" one. To determine which one is "better", key backups are compared first by the is_verified
|
||||
* flag (true is better than false), then by the first_message_index (a lower number is better), and finally by
|
||||
* forwarded_count (a lower number is better).
|
||||
*
|
||||
* @param roomId the room id
|
||||
* @param sessionId the session id
|
||||
* @param version the version of the backup
|
||||
* @param keyBackupData the data to send
|
||||
*/
|
||||
@PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "room_keys/keys/{roomId}/{sessionId}")
|
||||
fun storeRoomSessionData(@Path("roomId") roomId: String,
|
||||
@Path("sessionId") sessionId: String,
|
||||
@Query("version") version: String,
|
||||
@Body keyBackupData: KeyBackupData): Call<BackupKeysResult>
|
||||
|
||||
/**
|
||||
* Store several keys for the given room, using the given backup version.
|
||||
*
|
||||
* @param roomId the room id
|
||||
* @param version the version of the backup
|
||||
* @param roomKeysBackupData the data to send
|
||||
*/
|
||||
@PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "room_keys/keys/{roomId}")
|
||||
fun storeRoomSessionsData(@Path("roomId") roomId: String,
|
||||
@Query("version") version: String,
|
||||
@Body roomKeysBackupData: RoomKeysBackupData): Call<BackupKeysResult>
|
||||
|
||||
/**
|
||||
* Store several keys, using the given backup version.
|
||||
*
|
||||
* @param version the version of the backup
|
||||
* @param keysBackupData the data to send
|
||||
*/
|
||||
@PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "room_keys/keys")
|
||||
fun storeSessionsData(@Query("version") version: String,
|
||||
@Body keysBackupData: KeysBackupData): Call<BackupKeysResult>
|
||||
|
||||
/* ==========================================================================================
|
||||
* Retrieving keys
|
||||
* ========================================================================================== */
|
||||
|
||||
/**
|
||||
* Retrieve the key for the given session in the given room from the backup.
|
||||
*
|
||||
* @param roomId the room id
|
||||
* @param sessionId the session id
|
||||
* @param version the version of the backup, or empty String to retrieve the last version
|
||||
*/
|
||||
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "room_keys/keys/{roomId}/{sessionId}")
|
||||
fun getRoomSessionData(@Path("roomId") roomId: String,
|
||||
@Path("sessionId") sessionId: String,
|
||||
@Query("version") version: String): Call<KeyBackupData>
|
||||
|
||||
/**
|
||||
* Retrieve all the keys for the given room from the backup.
|
||||
*
|
||||
* @param roomId the room id
|
||||
* @param version the version of the backup, or empty String to retrieve the last version
|
||||
*/
|
||||
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "room_keys/keys/{roomId}")
|
||||
fun getRoomSessionsData(@Path("roomId") roomId: String,
|
||||
@Query("version") version: String): Call<RoomKeysBackupData>
|
||||
|
||||
/**
|
||||
* Retrieve all the keys from the backup.
|
||||
*
|
||||
* @param version the version of the backup, or empty String to retrieve the last version
|
||||
*/
|
||||
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "room_keys/keys")
|
||||
fun getSessionsData(@Query("version") version: String): Call<KeysBackupData>
|
||||
|
||||
|
||||
/* ==========================================================================================
|
||||
* Deleting keys
|
||||
* ========================================================================================== */
|
||||
|
||||
/**
|
||||
* Deletes keys from the backup.
|
||||
*/
|
||||
@DELETE(NetworkConstants.URI_API_PREFIX_PATH_R0 + "room_keys/keys/{roomId}/{sessionId}")
|
||||
fun deleteRoomSessionData(@Path("roomId") roomId: String,
|
||||
@Path("sessionId") sessionId: String,
|
||||
@Query("version") version: String): Call<Unit>
|
||||
|
||||
/**
|
||||
* Deletes keys from the backup.
|
||||
*/
|
||||
@DELETE(NetworkConstants.URI_API_PREFIX_PATH_R0 + "room_keys/keys/{roomId}")
|
||||
fun deleteRoomSessionsData(@Path("roomId") roomId: String,
|
||||
@Query("version") version: String): Call<Unit>
|
||||
|
||||
/**
|
||||
* Deletes keys from the backup.
|
||||
*/
|
||||
@DELETE(NetworkConstants.URI_API_PREFIX_PATH_R0 + "room_keys/keys")
|
||||
fun deleteSessionsData(@Query("version") version: String): Call<Unit>
|
||||
|
||||
/* ==========================================================================================
|
||||
* Deleting backup
|
||||
* ========================================================================================== */
|
||||
|
||||
/**
|
||||
* Deletes a backup.
|
||||
*/
|
||||
@DELETE(NetworkConstants.URI_API_PREFIX_PATH_R0 + "room_keys/version/{version}")
|
||||
fun deleteBackup(@Path("version") version: String): Call<Unit>
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.crypto.keysbackup.model
|
||||
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Data model for response to [KeysBackup.isKeyBackupTrusted()].
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class KeyBackupVersionTrust(
|
||||
/**
|
||||
* Flag to indicate if the backup is trusted.
|
||||
* true if there is a signature that is valid & from a trusted device.
|
||||
*/
|
||||
var usable: Boolean = false,
|
||||
|
||||
/**
|
||||
* Signatures found in the backup version.
|
||||
*/
|
||||
var signatures: MutableList<KeyBackupVersionTrustSignature> = ArrayList()
|
||||
)
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.crypto.keysbackup.model
|
||||
|
||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||
|
||||
/**
|
||||
* A signature in a the `KeyBackupVersionTrust` object.
|
||||
*/
|
||||
class KeyBackupVersionTrustSignature {
|
||||
|
||||
/**
|
||||
* The device that signed the backup version.
|
||||
*/
|
||||
var device: MXDeviceInfo? = null
|
||||
|
||||
/**
|
||||
*Flag to indicate the signature from this device is valid.
|
||||
*/
|
||||
var valid = false
|
||||
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.crypto.keysbackup.model
|
||||
|
||||
/**
|
||||
* Data model for response to [KeysBackup.getKeysBackupTrust()].
|
||||
*/
|
||||
data class KeysBackupVersionTrust(
|
||||
/**
|
||||
* Flag to indicate if the backup is trusted.
|
||||
* true if there is a signature that is valid & from a trusted device.
|
||||
*/
|
||||
var usable: Boolean = false,
|
||||
|
||||
/**
|
||||
* Signatures found in the backup version.
|
||||
*/
|
||||
var signatures: MutableList<KeysBackupVersionTrustSignature> = ArrayList()
|
||||
)
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.crypto.keysbackup.model
|
||||
|
||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||
|
||||
/**
|
||||
* A signature in a `KeysBackupVersionTrust` object.
|
||||
*/
|
||||
class KeysBackupVersionTrustSignature {
|
||||
|
||||
/**
|
||||
* The id of the device that signed the backup version.
|
||||
*/
|
||||
var deviceId: String? = null
|
||||
|
||||
/**
|
||||
* The device that signed the backup version.
|
||||
* Can be null if the device is not known.
|
||||
*/
|
||||
var device: MXDeviceInfo? = null
|
||||
|
||||
/**
|
||||
* Flag to indicate the signature from this device is valid.
|
||||
*/
|
||||
var valid = false
|
||||
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.crypto.keysbackup.model
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||
|
||||
/**
|
||||
* Data model for [org.matrix.androidsdk.rest.model.keys.KeysAlgorithmAndData.authData] in case
|
||||
* of [org.matrix.androidsdk.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP].
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class MegolmBackupAuthData(
|
||||
/**
|
||||
* The curve25519 public key used to encrypt the backups.
|
||||
*/
|
||||
@Json(name = "public_key")
|
||||
var publicKey: String = "",
|
||||
|
||||
/**
|
||||
* In case of a backup created from a password, the salt associated with the backup
|
||||
* private key.
|
||||
*/
|
||||
@Json(name = "private_key_salt")
|
||||
var privateKeySalt: String? = null,
|
||||
|
||||
/**
|
||||
* In case of a backup created from a password, the number of key derivations.
|
||||
*/
|
||||
@Json(name = "private_key_iterations")
|
||||
var privateKeyIterations: Int? = null,
|
||||
|
||||
/**
|
||||
* Signatures of the public key.
|
||||
* userId -> (deviceSignKeyId -> signature)
|
||||
*/
|
||||
var signatures: Map<String, Map<String, String>>? = null
|
||||
) {
|
||||
|
||||
fun toJsonString(): String {
|
||||
return MoshiProvider.providesMoshi()
|
||||
.adapter(MegolmBackupAuthData::class.java)
|
||||
.toJson(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as the parent [MXJSONModel JSONDictionary] but return only
|
||||
* data that must be signed.
|
||||
*/
|
||||
fun signalableJSONDictionary(): Map<String, Any> = HashMap<String, Any>().apply {
|
||||
put("public_key", publicKey)
|
||||
|
||||
privateKeySalt?.let {
|
||||
put("private_key_salt", it)
|
||||
}
|
||||
privateKeyIterations?.let {
|
||||
put("private_key_iterations", it)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.crypto.keysbackup.model
|
||||
|
||||
/**
|
||||
* Data retrieved from Olm library. algorithm and authData will be send to the homeserver, and recoveryKey will be displayed to the user
|
||||
*/
|
||||
class MegolmBackupCreationInfo {
|
||||
|
||||
/**
|
||||
* The algorithm used for storing backups [org.matrix.androidsdk.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP].
|
||||
*/
|
||||
var algorithm: String = ""
|
||||
|
||||
/**
|
||||
* Authentication data.
|
||||
*/
|
||||
var authData: MegolmBackupAuthData? = null
|
||||
|
||||
/**
|
||||
* The Base58 recovery key.
|
||||
*/
|
||||
var recoveryKey: String = ""
|
||||
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.crypto.keysbackup.model.rest
|
||||
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class BackupKeysResult(
|
||||
|
||||
// The hash value which is an opaque string representing stored keys in the backup
|
||||
var hash: String? = null,
|
||||
|
||||
// The number of keys stored in the backup.
|
||||
var count: Int? = null
|
||||
|
||||
)
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.crypto.keysbackup.model.rest
|
||||
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
class CreateKeysBackupVersionBody : KeysAlgorithmAndData()
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.crypto.keysbackup.model.rest
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||
|
||||
/**
|
||||
* Backup data for one key.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class KeyBackupData(
|
||||
/**
|
||||
* Required. The index of the first message in the session that the key can decrypt.
|
||||
*/
|
||||
@Json(name = "first_message_index")
|
||||
var firstMessageIndex: Long = 0,
|
||||
|
||||
/**
|
||||
* Required. The number of times this key has been forwarded.
|
||||
*/
|
||||
@Json(name = "forwarded_count")
|
||||
var forwardedCount: Int = 0,
|
||||
|
||||
/**
|
||||
* Whether the device backing up the key has verified the device that the key is from.
|
||||
*/
|
||||
@Json(name = "is_verified")
|
||||
var isVerified: Boolean = false,
|
||||
|
||||
/**
|
||||
* Algorithm-dependent data.
|
||||
*/
|
||||
@Json(name = "session_data")
|
||||
var sessionData: Map<String, Any>? = null
|
||||
) {
|
||||
|
||||
fun toJsonString(): String {
|
||||
return MoshiProvider.providesMoshi().adapter(KeyBackupData::class.java).toJson(this)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.crypto.keysbackup.model.rest
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupAuthData
|
||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* Example:
|
||||
*
|
||||
* {
|
||||
* "algorithm": "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||
* "auth_data": {
|
||||
* "public_key": "abcdefg",
|
||||
* "signatures": {
|
||||
* "something": {
|
||||
* "ed25519:something": "hijklmnop"
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
*/
|
||||
open class KeysAlgorithmAndData {
|
||||
|
||||
/**
|
||||
* The algorithm used for storing backups. Currently, only "m.megolm_backup.v1.curve25519-aes-sha2" is defined
|
||||
*/
|
||||
@Json(name = "algorithm")
|
||||
var algorithm: String? = null
|
||||
|
||||
/**
|
||||
* algorithm-dependent data, for "m.megolm_backup.v1.curve25519-aes-sha2" see [im.vector.matrix.android.internal.crypto.keysbackup.MegolmBackupAuthData]
|
||||
*/
|
||||
@Json(name = "auth_data")
|
||||
var authData: Map<String, Any>? = null
|
||||
|
||||
/**
|
||||
* Facility method to convert authData to a MegolmBackupAuthData object
|
||||
*/
|
||||
fun getAuthDataAsMegolmBackupAuthData(): MegolmBackupAuthData {
|
||||
return MoshiProvider.providesMoshi()
|
||||
.adapter(MegolmBackupAuthData::class.java)
|
||||
.fromJsonValue(authData)!!
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.crypto.keysbackup.model.rest
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Backup data for several keys in several rooms.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class KeysBackupData(
|
||||
|
||||
// the keys are the room IDs, and the values are RoomKeysBackupData
|
||||
@Json(name = "rooms")
|
||||
var roomIdToRoomKeysBackupData: MutableMap<String, RoomKeysBackupData> = HashMap()
|
||||
|
||||
)
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.crypto.keysbackup.model.rest
|
||||
|
||||
data class KeysVersion(
|
||||
// the keys backup version
|
||||
var version: String? = null
|
||||
)
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.crypto.keysbackup.model.rest
|
||||
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class KeysVersionResult(
|
||||
// the backup version
|
||||
var version: String? = null,
|
||||
|
||||
// The hash value which is an opaque string representing stored keys in the backup
|
||||
var hash: String? = null,
|
||||
|
||||
// The number of keys stored in the backup.
|
||||
var count: Int? = null
|
||||
) : KeysAlgorithmAndData()
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright 2018 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.crypto.keysbackup.model.rest
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Backup data for several keys within a room.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class RoomKeysBackupData(
|
||||
|
||||
// the keys are the session IDs, and the values are KeyBackupData
|
||||
@Json(name = "sessions")
|
||||
var sessionIdToKeyBackupData: MutableMap<String, KeyBackupData> = HashMap()
|
||||
)
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.crypto.keysbackup.model.rest
|
||||
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class UpdateKeysBackupVersionBody(
|
||||
// the backup version, mandatory
|
||||
val version: String
|
||||
) : KeysAlgorithmAndData()
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue