Merge branch 'release/0.18.0'

This commit is contained in:
Valere 2020-03-11 13:41:49 +01:00
commit dec591517c
319 changed files with 2067 additions and 1727 deletions

View file

@ -1,95 +0,0 @@
# Use Docker file from https://hub.docker.com/r/runmymind/docker-android-sdk
# Last docker plugin version can be found here:
# https://github.com/buildkite-plugins/docker-buildkite-plugin/releases
# We propagate the environment to the container (sse https://github.com/buildkite-plugins/docker-buildkite-plugin#propagate-environment-optional-boolean)
steps:
- label: "Compile and run Unit tests"
agents:
# We use a medium sized instance instead of the normal small ones because
# gradle build can be memory hungry
queue: "medium"
commands:
- "./gradlew clean test --stacktrace"
plugins:
- docker#v3.1.0:
image: "runmymind/docker-android-sdk"
propagate-environment: true
- label: "Compile Android tests"
agents:
# We use a medium sized instance instead of the normal small ones because
# gradle build can be memory hungry
queue: "medium"
commands:
- "./gradlew clean assembleAndroidTest --stacktrace"
plugins:
- docker#v3.1.0:
image: "runmymind/docker-android-sdk"
propagate-environment: true
- label: "Assemble GPlay Debug version"
agents:
# We use a xlarge sized instance instead of the normal small ones because
# gradle build can be memory hungry
queue: "xlarge"
commands:
- "./gradlew clean lintGplayRelease assembleGplayDebug --stacktrace"
artifact_paths:
- "vector/build/outputs/apk/gplay/debug/*.apk"
branches: "!master"
plugins:
- docker#v3.1.0:
image: "runmymind/docker-android-sdk"
propagate-environment: true
- label: "Assemble FDroid Debug version"
agents:
# We use a xlarge sized instance instead of the normal small ones because
# gradle build can be memory hungry
queue: "xlarge"
commands:
- "./gradlew clean lintFdroidRelease assembleFdroidDebug --stacktrace"
artifact_paths:
- "vector/build/outputs/apk/fdroid/debug/*.apk"
branches: "!master"
plugins:
- docker#v3.1.0:
image: "runmymind/docker-android-sdk"
propagate-environment: true
- label: "Build Google Play unsigned APK"
agents:
# We use a xlarge sized instance instead of the normal small ones because
# gradle build can be memory hungry
queue: "xlarge"
commands:
- "./gradlew clean assembleGplayRelease --stacktrace"
artifact_paths:
- "vector/build/outputs/apk/gplay/release/*.apk"
branches: "master"
plugins:
- docker#v3.1.0:
image: "runmymind/docker-android-sdk"
propagate-environment: true
# Code quality
- label: "Code quality"
command:
- "./tools/check/check_code_quality.sh"
- label: "ktlint"
command:
- "curl -sSLO https://github.com/pinterest/ktlint/releases/download/0.34.2/ktlint && chmod a+x ktlint"
- "./ktlint --android --experimental -v"
plugins:
- docker#v3.1.0:
image: "openjdk"
# Check that indonesians files are identical.
# Due to Android issue, the resource folder must be values-in/, and Weblate export data into values-id/.
# If this step fails, it means that Weblate has updated the file in value-id/ so to fix it, copy the file to values-in/
- label: "Indonesian"
command:
- "diff ./vector/src/main/res/values-id/strings.xml ./vector/src/main/res/values-in/strings.xml"

View file

@ -1,9 +1,6 @@
<component name="ProjectCodeStyleConfiguration"> <component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173"> <code_scheme name="Project" version="173">
<option name="RIGHT_MARGIN" value="160" /> <option name="RIGHT_MARGIN" value="160" />
<AndroidXmlCodeStyleSettings>
<option name="ARRANGEMENT_SETTINGS_MIGRATED_TO_191" value="true" />
</AndroidXmlCodeStyleSettings>
<JetCodeStyleSettings> <JetCodeStyleSettings>
<option name="PACKAGES_TO_USE_STAR_IMPORTS"> <option name="PACKAGES_TO_USE_STAR_IMPORTS">
<value> <value>

View file

@ -19,6 +19,7 @@
<w>msisdn</w> <w>msisdn</w>
<w>pbkdf</w> <w>pbkdf</w>
<w>pkcs</w> <w>pkcs</w>
<w>riotx</w>
<w>signin</w> <w>signin</w>
<w>signout</w> <w>signout</w>
<w>signup</w> <w>signup</w>

View file

@ -49,12 +49,12 @@ script:
# Build Android test (assembleAndroidTest) (disabled for now) # Build Android test (assembleAndroidTest) (disabled for now)
# Code quality (lintGplayRelease lintFdroidRelease) # Code quality (lintGplayRelease lintFdroidRelease)
# Split into two steps because if a task contain Fdroid, PlayService will be disabled # Split into two steps because if a task contain Fdroid, PlayService will be disabled
- ./gradlew clean assembleGplayRelease lintGplayRelease --stacktrace # Done by Buildkite now: - ./gradlew clean assembleGplayRelease lintGplayRelease --stacktrace
- ./gradlew clean assembleFdroidRelease lintFdroidRelease --stacktrace # Done by Buildkite now: - ./gradlew clean assembleFdroidRelease lintFdroidRelease --stacktrace
# Run unitary test (Disable for now, see https://travis-ci.org/vector-im/riot-android/builds/502504370) # Run unitary test (Disable for now, see https://travis-ci.org/vector-im/riot-android/builds/502504370)
# - ./gradlew testGplayReleaseUnitTest --stacktrace # - ./gradlew testGplayReleaseUnitTest --stacktrace
# Other code quality check # Other code quality check
- ./tools/check/check_code_quality.sh # Done by Buildkite now: - ./tools/check/check_code_quality.sh
- ./tools/travis/check_pr.sh - ./tools/travis/check_pr.sh
# Check that indonesians file are identical. Due to Android issue, the resource folder must be value-in/, and Weblate export data into value-id/. # Check that indonesians file are identical. Due to Android issue, the resource folder must be value-in/, and Weblate export data into value-id/.
- diff ./vector/src/main/res/values-id/strings.xml ./vector/src/main/res/values-in/strings.xml # Done by Buildkite now: - diff ./vector/src/main/res/values-id/strings.xml ./vector/src/main/res/values-in/strings.xml

View file

@ -1,3 +1,31 @@
Changes in RiotX 0.18.0 (2020-03-11)
===================================================
Improvements 🙌:
- Share image and other media from e2e rooms (#677)
- Add support for `/plain` command (#12)
- Detect spaces in password if user fail to login (#1038)
- FTUE: do not display a different color when encrypting message when not in developer mode.
- Open room member profile from avatar of the room member state event (#935)
- Restore the push rules configuration in the settings
Bugfix 🐛:
- Fix crash on attachment preview screen (#1088)
- "Share" option is not appearing in encrypted rooms for images (#1031)
- Set "image/jpeg" as MIME type of images instead of "image/jpg" (#1075)
- Self verification via QR code is failing (#1130)
SDK API changes ⚠️:
- PushRuleService.getPushRules() now returns a RuleSet. Use getAllRules() on this object to get all the rules.
Build 🧱:
- Upgrade ktlint to version 0.36.0
- Pipeline file for Buildkite is now hosted on another Github repository: https://github.com/matrix-org/pipelines/blob/master/riotx-android/pipeline.yml
Other changes:
- Restore availability to Chromebooks (#932)
- Add a [documentation](./docs/integration_tests.md) to run integration tests
Changes in RiotX 0.17.0 (2020-02-27) Changes in RiotX 0.17.0 (2020-02-27)
=================================================== ===================================================

View file

@ -82,6 +82,8 @@ Make sure the following commands execute without any error:
RiotX is currently supported on Android KitKat (API 19+): please test your change on an Android device (or Android emulator) running with API 19. Many issues can happen (including crashes) on older devices. RiotX is currently supported on Android KitKat (API 19+): please test your change on an Android device (or Android emulator) running with API 19. Many issues can happen (including crashes) on older devices.
Also, if possible, please test your change on a real device. Testing on Android emulator may not be sufficient. Also, if possible, please test your change on a real device. Testing on Android emulator may not be sufficient.
You should consider adding Unit tests with your PR, and also integration tests (AndroidTest). Please refer to [this document](./docs/integration_tests.md) to install and run the integration test environment.
### Internationalisation ### Internationalisation
When adding new string resources, please only add new entries in file `value/strings.xml`. Translations will be added later by the community of translators with a specific tool named [Weblate](https://translate.riot.im/projects/riot-android/). When adding new string resources, please only add new entries in file `value/strings.xml`. Translations will be added later by the community of translators with a specific tool named [Weblate](https://translate.riot.im/projects/riot-android/).

97
docs/integration_tests.md Normal file
View file

@ -0,0 +1,97 @@
# Integration tests
Integration tests are useful to ensure that the code works well for any use cases.
They can also be used as sample on how to use the Matrix SDK.
In a ideal world, every API of the SDK should be covered by integration tests. For the moment, we have test mainly for the Crypto part, which is the tricky part. But it covers quite a lot of features: accounts creation, login to existing account, send encrypted messages, keys backup, verification, etc.
The Matrix SDK is able to open multiple sessions, for the same user, of for different users. This way we can test communication between several sessions on a single device.
## Pre requirements
Integration tests need a homeserver running on localhost.
The documentation describes what we do to have one, using [Synapse](https://github.com/matrix-org/synapse/), which is the Matrix reference homeserver.
## Install and run Synapse
Steps:
- Install virtualenv
```bash
python3 -m pip install virtualenv
```
- Clone Synapse repository
```bash
git clone -b develop https://github.com/matrix-org/synapse.git
```
or
```bash
git clone -b develop git@github.com:matrix-org/synapse.git
```
You should have the develop branch cloned by default.
- Run synapse, from the Synapse folder you just cloned
```bash
virtualenv -p python3 env
source env/bin/activate
pip install -e .
demo/start.sh --no-rate-limit
```
Alternatively, to install the latest Synapse release package (and not a cloned branch) you can run the following instead of `pip install -e .`:
```bash
pip install matrix-synapse
```
You should now have 3 running federated Synapse instances 🎉, at http://127.0.0.1:8080/, http://127.0.0.1:8081/ and http://127.0.0.1:8082/, which should display a "It Works! Synapse is running" message.
## Run the test
It's recommended to run tests using an Android Emulator and not a real device. First reason for that is that the tests will use http://10.0.2.2:8080 to connect to Synapse, which run locally on your machine.
You can run all the tests in the `androidTest` folders.
## Stop Synapse
To stop Synapse, you can run the following commands:
```bash
./demo/stop.sh
```
And you can deactivate the virtualenv:
```bash
deactivate
```
## Troubleshoot
You'll need python3 to be able to run synapse
### Android Emulator does cannot reach the homeserver
Try on the Emulator browser to open "http://10.0.2.2:8080". You should see the "Synapse is running" message.
### virtualenv command fails
You can try using
```bash
python3 -m venv env
```
or
```bash
python3 -m virtualenv env
```
instead of
```bash
virtualenv -p python3 env
```

View file

@ -126,7 +126,7 @@ dependencies {
kapt 'dk.ilios:realmfieldnameshelper:1.1.1' kapt 'dk.ilios:realmfieldnameshelper:1.1.1'
// Work // Work
implementation "androidx.work:work-runtime-ktx:2.3.0" implementation "androidx.work:work-runtime-ktx:2.3.3"
// FP // FP
implementation "io.arrow-kt:arrow-core:$arrow_version" implementation "io.arrow-kt:arrow-core:$arrow_version"

View file

@ -0,0 +1,38 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.crypto.crosssigning
import org.amshove.kluent.shouldBeNull
import org.amshove.kluent.shouldBeTrue
import org.junit.Test
@Suppress("SpellCheckingInspection")
class ExtensionsKtTest {
@Test
fun testComparingBase64StringWithOrWithoutPadding() {
// Without padding
"NMJyumnhMic".fromBase64().contentEquals("NMJyumnhMic".fromBase64()).shouldBeTrue()
// With padding
"NMJyumnhMic".fromBase64().contentEquals("NMJyumnhMic=".fromBase64()).shouldBeTrue()
}
@Test
fun testBadBase64() {
"===".fromBase64Safe().shouldBeNull()
}
}

View file

@ -33,7 +33,6 @@ import im.vector.matrix.android.common.CommonTestHelper
import im.vector.matrix.android.common.CryptoTestHelper import im.vector.matrix.android.common.CryptoTestHelper
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationAccept
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationCancel import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationCancel
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationStart import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationStart
import im.vector.matrix.android.internal.crypto.model.rest.toValue import im.vector.matrix.android.internal.crypto.model.rest.toValue
@ -279,7 +278,7 @@ class SASTest : InstrumentedTest {
val startMessage = KeyVerificationStart( val startMessage = KeyVerificationStart(
fromDevice = bobSession.cryptoService().getMyDevice().deviceId, fromDevice = bobSession.cryptoService().getMyDevice().deviceId,
method = VerificationMethod.SAS.toValue(), method = VerificationMethod.SAS.toValue(),
transactionID = tid, transactionId = tid,
keyAgreementProtocols = protocols, keyAgreementProtocols = protocols,
hashes = hashes, hashes = hashes,
messageAuthenticationCodes = mac, messageAuthenticationCodes = mac,
@ -350,16 +349,16 @@ class SASTest : InstrumentedTest {
val aliceVerificationService = aliceSession.cryptoService().verificationService() val aliceVerificationService = aliceSession.cryptoService().verificationService()
val bobVerificationService = bobSession!!.cryptoService().verificationService() val bobVerificationService = bobSession!!.cryptoService().verificationService()
var accepted: KeyVerificationAccept? = null var accepted: ValidVerificationInfoAccept? = null
var startReq: KeyVerificationStart? = null var startReq: ValidVerificationInfoStart.SasVerificationInfoStart? = null
val aliceAcceptedLatch = CountDownLatch(1) val aliceAcceptedLatch = CountDownLatch(1)
val aliceListener = object : VerificationService.Listener { val aliceListener = object : VerificationService.Listener {
override fun transactionUpdated(tx: VerificationTransaction) { override fun transactionUpdated(tx: VerificationTransaction) {
if ((tx as SASDefaultVerificationTransaction).state === VerificationTxState.OnAccepted) { if ((tx as SASDefaultVerificationTransaction).state === VerificationTxState.OnAccepted) {
val at = tx as SASDefaultVerificationTransaction val at = tx as SASDefaultVerificationTransaction
accepted = at.accepted as? KeyVerificationAccept accepted = at.accepted
startReq = at.startReq as? KeyVerificationStart startReq = at.startReq
aliceAcceptedLatch.countDown() aliceAcceptedLatch.countDown()
} }
} }
@ -384,13 +383,13 @@ class SASTest : InstrumentedTest {
assertTrue("Should have receive a commitment", accepted!!.commitment?.trim()?.isEmpty() == false) assertTrue("Should have receive a commitment", accepted!!.commitment?.trim()?.isEmpty() == false)
// check that agreement is valid // check that agreement is valid
assertTrue("Agreed Protocol should be Valid", accepted!!.isValid()) assertTrue("Agreed Protocol should be Valid", accepted != null)
assertTrue("Agreed Protocol should be known by alice", startReq!!.keyAgreementProtocols!!.contains(accepted!!.keyAgreementProtocol)) assertTrue("Agreed Protocol should be known by alice", startReq!!.keyAgreementProtocols.contains(accepted!!.keyAgreementProtocol))
assertTrue("Hash should be known by alice", startReq!!.hashes!!.contains(accepted!!.hash)) assertTrue("Hash should be known by alice", startReq!!.hashes.contains(accepted!!.hash))
assertTrue("Hash should be known by alice", startReq!!.messageAuthenticationCodes!!.contains(accepted!!.messageAuthenticationCode)) assertTrue("Hash should be known by alice", startReq!!.messageAuthenticationCodes.contains(accepted!!.messageAuthenticationCode))
accepted!!.shortAuthenticationStrings?.forEach { accepted!!.shortAuthenticationStrings.forEach {
assertTrue("all agreed Short Code should be known by alice", startReq!!.shortAuthenticationStrings!!.contains(it)) assertTrue("all agreed Short Code should be known by alice", startReq!!.shortAuthenticationStrings.contains(it))
} }
cryptoTestData.cleanUp(mTestHelper) cryptoTestData.cleanUp(mTestHelper)

View file

@ -24,7 +24,7 @@ import im.vector.matrix.android.common.CommonTestHelper
import im.vector.matrix.android.common.CryptoTestHelper import im.vector.matrix.android.common.CryptoTestHelper
import im.vector.matrix.android.common.TestConstants import im.vector.matrix.android.common.TestConstants
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
import im.vector.matrix.android.internal.crypto.verification.PendingVerificationRequest import im.vector.matrix.android.api.session.crypto.verification.PendingVerificationRequest
import org.amshove.kluent.shouldBe import org.amshove.kluent.shouldBe
import org.junit.FixMethodOrder import org.junit.FixMethodOrder
import org.junit.Test import org.junit.Test

View file

@ -17,25 +17,24 @@ package im.vector.matrix.android.api.pushrules
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.pushrules.rest.PushRule import im.vector.matrix.android.api.pushrules.rest.PushRule
import im.vector.matrix.android.api.pushrules.rest.RuleSet
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.Cancelable
interface PushRuleService { interface PushRuleService {
/** /**
* Fetch the push rules from the server * Fetch the push rules from the server
*/ */
fun fetchPushRules(scope: String = RuleScope.GLOBAL) fun fetchPushRules(scope: String = RuleScope.GLOBAL)
// TODO get push rule set fun getPushRules(scope: String = RuleScope.GLOBAL): RuleSet
fun getPushRules(scope: String = RuleScope.GLOBAL): List<PushRule>
// TODO update rule
fun updatePushRuleEnableStatus(kind: RuleKind, pushRule: PushRule, enabled: Boolean, callback: MatrixCallback<Unit>): Cancelable fun updatePushRuleEnableStatus(kind: RuleKind, pushRule: PushRule, enabled: Boolean, callback: MatrixCallback<Unit>): Cancelable
fun addPushRule(kind: RuleKind, pushRule: PushRule, callback: MatrixCallback<Unit>): Cancelable fun addPushRule(kind: RuleKind, pushRule: PushRule, callback: MatrixCallback<Unit>): Cancelable
fun updatePushRuleActions(kind: RuleKind, oldPushRule: PushRule, newPushRule: PushRule, callback: MatrixCallback<Unit>): Cancelable
fun removePushRule(kind: RuleKind, pushRule: PushRule, callback: MatrixCallback<Unit>): Cancelable fun removePushRule(kind: RuleKind, pushRule: PushRule, callback: MatrixCallback<Unit>): Cancelable
fun addPushRuleListener(listener: PushRuleListener) fun addPushRuleListener(listener: PushRuleListener)

View file

@ -20,6 +20,7 @@ import com.squareup.moshi.JsonClass
/** /**
* All push rulesets for a user. * All push rulesets for a user.
* Ref: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-pushrules
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class GetPushRulesResponse( internal data class GetPushRulesResponse(
@ -27,11 +28,11 @@ internal data class GetPushRulesResponse(
* Global rules, account level applying to all devices * Global rules, account level applying to all devices
*/ */
@Json(name = "global") @Json(name = "global")
val global: Ruleset, val global: RuleSet,
/** /**
* Device specific rules, apply only to current device * Device specific rules, apply only to current device
*/ */
@Json(name = "device") @Json(name = "device")
val device: Ruleset? = null val device: RuleSet? = null
) )

View file

@ -24,21 +24,27 @@ import im.vector.matrix.android.api.pushrules.RoomMemberCountCondition
import im.vector.matrix.android.api.pushrules.SenderNotificationPermissionCondition import im.vector.matrix.android.api.pushrules.SenderNotificationPermissionCondition
import timber.log.Timber import timber.log.Timber
/**
* Ref: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-pushrules
*/
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class PushCondition( data class PushCondition(
/** /**
* Required. The kind of condition to apply. * Required. The kind of condition to apply.
*/ */
@Json(name = "kind")
val kind: String, val kind: String,
/** /**
* Required for event_match conditions. The dot- separated field of the event to match. * Required for event_match conditions. The dot- separated field of the event to match.
*/ */
@Json(name = "key")
val key: String? = null, val key: String? = null,
/** /**
* Required for event_match conditions. * Required for event_match conditions.
*/ */
@Json(name = "pattern")
val pattern: String? = null, val pattern: String? = null,
/** /**
@ -47,7 +53,8 @@ data class PushCondition(
* A prefix of < matches rooms where the member count is strictly less than the given number and so forth. * A prefix of < matches rooms where the member count is strictly less than the given number and so forth.
* If no prefix is present, this parameter defaults to ==. * If no prefix is present, this parameter defaults to ==.
*/ */
@Json(name = "is") val iz: String? = null @Json(name = "is")
val iz: String? = null
) { ) {
fun asExecutableCondition(): Condition? { fun asExecutableCondition(): Condition? {

View file

@ -18,31 +18,158 @@ package im.vector.matrix.android.api.pushrules.rest
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.pushrules.Action
import im.vector.matrix.android.api.pushrules.getActions
import im.vector.matrix.android.api.pushrules.toJson
/**
* Ref: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-pushrules
*/
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class PushRule( data class PushRule(
/** /**
* Required. The actions to perform when this rule is matched. * Required. The actions to perform when this rule is matched.
*/ */
@Json(name = "actions")
val actions: List<Any>, val actions: List<Any>,
/** /**
* Required. Whether this is a default rule, or has been set explicitly. * Required. Whether this is a default rule, or has been set explicitly.
*/ */
@Json(name = "default")
val default: Boolean? = false, val default: Boolean? = false,
/** /**
* Required. Whether the push rule is enabled or not. * Required. Whether the push rule is enabled or not.
*/ */
@Json(name = "enabled")
val enabled: Boolean, val enabled: Boolean,
/** /**
* Required. The ID of this rule. * Required. The ID of this rule.
*/ */
@Json(name = "rule_id") val ruleId: String, @Json(name = "rule_id")
val ruleId: String,
/** /**
* The conditions that must hold true for an event in order for a rule to be applied to an event * The conditions that must hold true for an event in order for a rule to be applied to an event
*/ */
@Json(name = "conditions")
val conditions: List<PushCondition>? = null, val conditions: List<PushCondition>? = null,
/** /**
* The glob-style pattern to match against. Only applicable to content rules. * The glob-style pattern to match against. Only applicable to content rules.
*/ */
@Json(name = "pattern")
val pattern: String? = null val pattern: String? = null
) {
/**
* Add the default notification sound.
*/
fun setNotificationSound(): PushRule {
return setNotificationSound(ACTION_VALUE_DEFAULT)
}
fun getNotificationSound(): String? {
return (getActions().firstOrNull { it is Action.Sound } as? Action.Sound)?.sound
}
/**
* Set the notification sound
*
* @param sound notification sound
*/
fun setNotificationSound(sound: String): PushRule {
return copy(
actions = (getActions().filter { it !is Action.Sound } + Action.Sound(sound)).toJson()
) )
}
/**
* Remove the notification sound
*/
fun removeNotificationSound(): PushRule {
return copy(
actions = getActions().filter { it !is Action.Sound }.toJson()
)
}
/**
* Set the highlight status.
*
* @param highlight the highlight status
*/
fun setHighlight(highlight: Boolean): PushRule {
return copy(
actions = (getActions().filter { it !is Action.Highlight } + Action.Highlight(highlight)).toJson()
)
}
/**
* Set the notification status.
*
* @param notify true to notify
*/
fun setNotify(notify: Boolean): PushRule {
val mutableActions = actions.toMutableList()
mutableActions.remove(ACTION_DONT_NOTIFY)
mutableActions.remove(ACTION_NOTIFY)
if (notify) {
mutableActions.add(ACTION_NOTIFY)
} else {
mutableActions.add(ACTION_DONT_NOTIFY)
}
return copy(actions = mutableActions)
}
/**
* Return true if the rule should highlight the event.
*
* @return true if the rule should play sound
*/
fun shouldNotify() = actions.contains(ACTION_NOTIFY)
/**
* Return true if the rule should not highlight the event.
*
* @return true if the rule should not play sound
*/
fun shouldNotNotify() = actions.contains(ACTION_DONT_NOTIFY)
companion object {
/* ==========================================================================================
* Rule id
* ========================================================================================== */
const val RULE_ID_DISABLE_ALL = ".m.rule.master"
const val RULE_ID_CONTAIN_USER_NAME = ".m.rule.contains_user_name"
const val RULE_ID_CONTAIN_DISPLAY_NAME = ".m.rule.contains_display_name"
const val RULE_ID_ONE_TO_ONE_ROOM = ".m.rule.room_one_to_one"
const val RULE_ID_INVITE_ME = ".m.rule.invite_for_me"
const val RULE_ID_PEOPLE_JOIN_LEAVE = ".m.rule.member_event"
const val RULE_ID_CALL = ".m.rule.call"
const val RULE_ID_SUPPRESS_BOTS_NOTIFICATIONS = ".m.rule.suppress_notices"
const val RULE_ID_ALL_OTHER_MESSAGES_ROOMS = ".m.rule.message"
const val RULE_ID_AT_ROOMS = ".m.rule.roomnotif"
const val RULE_ID_TOMBSTONE = ".m.rule.tombstone"
const val RULE_ID_E2E_ONE_TO_ONE_ROOM = ".m.rule.encrypted_room_one_to_one"
const val RULE_ID_E2E_GROUP = ".m.rule.encrypted"
const val RULE_ID_REACTION = ".m.rule.reaction"
const val RULE_ID_FALLBACK = ".m.rule.fallback"
/* ==========================================================================================
* Actions
* ========================================================================================== */
const val ACTION_NOTIFY = "notify"
const val ACTION_DONT_NOTIFY = "dont_notify"
const val ACTION_COALESCE = "coalesce"
const val ACTION_SET_TWEAK_SOUND_VALUE = "sound"
const val ACTION_SET_TWEAK_HIGHLIGHT_VALUE = "highlight"
const val ACTION_PARAMETER_SET_TWEAK = "set_tweak"
const val ACTION_PARAMETER_VALUE = "value"
const val ACTION_VALUE_DEFAULT = "default"
const val ACTION_VALUE_RING = "ring"
}
}

View file

@ -0,0 +1,81 @@
/*
* 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.pushrules.rest
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.pushrules.RuleSetKey
/**
* Ref: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-pushrules
*/
@JsonClass(generateAdapter = true)
data class RuleSet(
@Json(name = "content")
val content: List<PushRule>? = null,
@Json(name = "override")
val override: List<PushRule>? = null,
@Json(name = "room")
val room: List<PushRule>? = null,
@Json(name = "sender")
val sender: List<PushRule>? = null,
@Json(name = "underride")
val underride: List<PushRule>? = null
) {
fun getAllRules(): List<PushRule> {
// Ref. for the order: https://matrix.org/docs/spec/client_server/latest#push-rules
return override.orEmpty() + content.orEmpty() + room.orEmpty() + sender.orEmpty() + underride.orEmpty()
}
/**
* Find a rule from its ruleID.
*
* @param ruleId a RULE_ID_XX value
* @return the matched bing rule or null it doesn't exist.
*/
fun findDefaultRule(ruleId: String?): PushRuleAndKind? {
var result: PushRuleAndKind? = null
// sanity check
if (null != ruleId) {
if (PushRule.RULE_ID_CONTAIN_USER_NAME == ruleId) {
result = findRule(content, ruleId)?.let { PushRuleAndKind(it, RuleSetKey.CONTENT) }
} else {
// assume that the ruleId is unique.
result = findRule(override, ruleId)?.let { PushRuleAndKind(it, RuleSetKey.OVERRIDE) }
if (null == result) {
result = findRule(underride, ruleId)?.let { PushRuleAndKind(it, RuleSetKey.UNDERRIDE) }
}
}
}
return result
}
/**
* Find a rule from its rule Id.
*
* @param rules the rules list.
* @param ruleId the rule Id.
* @return the bing rule if it exists, else null.
*/
private fun findRule(rules: List<PushRule>?, ruleId: String): PushRule? {
return rules?.firstOrNull { it.ruleId == ruleId }
}
}
data class PushRuleAndKind(
val pushRule: PushRule,
val kind: RuleSetKey
)

View file

@ -31,7 +31,7 @@ data class ContentAttachmentData(
val name: String? = null, val name: String? = null,
val queryUri: String, val queryUri: String,
val path: String, val path: String,
val mimeType: String?, private val mimeType: String?,
val type: Type val type: Type
) : Parcelable { ) : Parcelable {
@ -41,4 +41,6 @@ data class ContentAttachmentData(
AUDIO, AUDIO,
VIDEO VIDEO
} }
fun getSafeMimeType() = if (mimeType == "image/jpg") "image/jpeg" else mimeType
} }

View file

@ -13,10 +13,9 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package im.vector.matrix.android.internal.crypto.verification package im.vector.matrix.android.api.session.crypto.verification
import im.vector.matrix.android.api.extensions.orFalse import im.vector.matrix.android.api.extensions.orFalse
import im.vector.matrix.android.api.session.crypto.verification.CancelCode
import im.vector.matrix.android.internal.crypto.model.rest.VERIFICATION_METHOD_QR_CODE_SCAN import im.vector.matrix.android.internal.crypto.model.rest.VERIFICATION_METHOD_QR_CODE_SCAN
import im.vector.matrix.android.internal.crypto.model.rest.VERIFICATION_METHOD_QR_CODE_SHOW import im.vector.matrix.android.internal.crypto.model.rest.VERIFICATION_METHOD_QR_CODE_SHOW
import im.vector.matrix.android.internal.crypto.model.rest.VERIFICATION_METHOD_SAS import im.vector.matrix.android.internal.crypto.model.rest.VERIFICATION_METHOD_SAS
@ -24,17 +23,16 @@ import java.util.UUID
/** /**
* Stores current pending verification requests * Stores current pending verification requests
* TODO We should not expose this whole object to the app. Create an interface
*/ */
data class PendingVerificationRequest( data class PendingVerificationRequest(
val ageLocalTs: Long, val ageLocalTs: Long,
val isIncoming: Boolean = false, val isIncoming: Boolean = false,
val localID: String = UUID.randomUUID().toString(), val localId: String = UUID.randomUUID().toString(),
val otherUserId: String, val otherUserId: String,
val roomId: String?, val roomId: String?,
val transactionId: String? = null, val transactionId: String? = null,
val requestInfo: VerificationInfoRequest? = null, val requestInfo: ValidVerificationInfoRequest? = null,
val readyInfo: VerificationInfoReady? = null, val readyInfo: ValidVerificationInfoReady? = null,
val cancelConclusion: CancelCode? = null, val cancelConclusion: CancelCode? = null,
val isSuccessful: Boolean = false, val isSuccessful: Boolean = false,
val handledByOtherSession: Boolean = false, val handledByOtherSession: Boolean = false,

View file

@ -1,5 +1,5 @@
/* /*
* Copyright 2019 New Vector Ltd * Copyright (c) 2020 New Vector Ltd
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -13,15 +13,11 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package im.vector.matrix.android.api.pushrules.rest
import com.squareup.moshi.JsonClass package im.vector.matrix.android.api.session.crypto.verification
@JsonClass(generateAdapter = true) data class ValidVerificationInfoReady(
internal data class Ruleset( val transactionId: String,
val content: List<PushRule>? = null, val fromDevice: String,
val override: List<PushRule>? = null, val methods: List<String>
val room: List<PushRule>? = null,
val sender: List<PushRule>? = null,
val underride: List<PushRule>? = null
) )

View file

@ -1,5 +1,5 @@
/* /*
* Copyright 2020 New Vector Ltd * Copyright (c) 2020 New Vector Ltd
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,16 +14,11 @@
* limitations under the License. * limitations under the License.
*/ */
package im.vector.riotx.features.crypto.verification package im.vector.matrix.android.api.session.crypto.verification
import im.vector.matrix.android.api.session.crypto.verification.VerificationMethod data class ValidVerificationInfoRequest(
val transactionId: String,
val supportedVerificationMethods = val fromDevice: String,
listOf( val methods: List<String>,
// RiotX supports SAS verification val timestamp: Long?
VerificationMethod.SAS,
// RiotX is able to show QR codes
VerificationMethod.QR_CODE_SHOW,
// RiotX is able to scan QR codes
VerificationMethod.QR_CODE_SCAN
) )

View file

@ -18,7 +18,6 @@ package im.vector.matrix.android.api.session.crypto.verification
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.events.model.LocalEcho import im.vector.matrix.android.api.session.events.model.LocalEcho
import im.vector.matrix.android.internal.crypto.verification.PendingVerificationRequest
/** /**
* https://matrix.org/docs/spec/client_server/r0.5.0#key-verification-framework * https://matrix.org/docs/spec/client_server/r0.5.0#key-verification-framework

View file

@ -34,7 +34,11 @@ interface FileService {
/** /**
* Download file in cache * Download file in cache
*/ */
FOR_INTERNAL_USE FOR_INTERNAL_USE,
/**
* Download file in file provider path
*/
FOR_EXTERNAL_SHARE
} }
/** /**

View file

@ -51,4 +51,4 @@ data class MessageAudioContent(
* Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption. * Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption.
*/ */
@Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null @Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null
) : MessageEncryptedContent ) : MessageWithAttachmentContent

View file

@ -57,7 +57,7 @@ data class MessageFileContent(
* Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption. * Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption.
*/ */
@Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null @Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null
) : MessageEncryptedContent { ) : MessageWithAttachmentContent {
fun getMimeType(): String { fun getMimeType(): String {
// Mimetype default to plain text, should not be used // Mimetype default to plain text, should not be used

View file

@ -20,6 +20,6 @@ package im.vector.matrix.android.api.session.room.model.message
/** /**
* A content with image information * A content with image information
*/ */
interface MessageImageInfoContent : MessageEncryptedContent { interface MessageImageInfoContent : MessageWithAttachmentContent {
val info: ImageInfo? val info: ImageInfo?
} }

View file

@ -22,7 +22,6 @@ import im.vector.matrix.android.api.session.events.model.toContent
import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent
import im.vector.matrix.android.internal.crypto.verification.VerificationInfoAccept import im.vector.matrix.android.internal.crypto.verification.VerificationInfoAccept
import im.vector.matrix.android.internal.crypto.verification.VerificationInfoAcceptFactory import im.vector.matrix.android.internal.crypto.verification.VerificationInfoAcceptFactory
import timber.log.Timber
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class MessageVerificationAcceptContent( internal data class MessageVerificationAcceptContent(
@ -34,22 +33,9 @@ internal data class MessageVerificationAcceptContent(
@Json(name = "commitment") override var commitment: String? = null @Json(name = "commitment") override var commitment: String? = null
) : VerificationInfoAccept { ) : VerificationInfoAccept {
override val transactionID: String? override val transactionId: String?
get() = relatesTo?.eventId get() = relatesTo?.eventId
override fun isValid(): Boolean {
if (transactionID.isNullOrBlank()
|| keyAgreementProtocol.isNullOrBlank()
|| hash.isNullOrBlank()
|| commitment.isNullOrBlank()
|| messageAuthenticationCode.isNullOrBlank()
|| shortAuthenticationStrings.isNullOrEmpty()) {
Timber.e("## received invalid verification request")
return false
}
return true
}
override fun toEventContent() = toContent() override fun toEventContent() = toContent()
companion object : VerificationInfoAcceptFactory { companion object : VerificationInfoAcceptFactory {

View file

@ -28,21 +28,13 @@ data class MessageVerificationCancelContent(
@Json(name = "code") override val code: String? = null, @Json(name = "code") override val code: String? = null,
@Json(name = "reason") override val reason: String? = null, @Json(name = "reason") override val reason: String? = null,
@Json(name = "m.relates_to") val relatesTo: RelationDefaultContent? @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent?
) : VerificationInfoCancel { ) : VerificationInfoCancel {
override val transactionID: String? override val transactionId: String?
get() = relatesTo?.eventId get() = relatesTo?.eventId
override fun toEventContent() = toContent() override fun toEventContent() = toContent()
override fun isValid(): Boolean {
if (transactionID.isNullOrBlank() || code.isNullOrBlank()) {
return false
}
return true
}
companion object { companion object {
fun create(transactionId: String, reason: CancelCode): MessageVerificationCancelContent { fun create(transactionId: String, reason: CancelCode): MessageVerificationCancelContent {
return MessageVerificationCancelContent( return MessageVerificationCancelContent(

View file

@ -25,12 +25,22 @@ import im.vector.matrix.android.internal.crypto.verification.VerificationInfo
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class MessageVerificationDoneContent( internal data class MessageVerificationDoneContent(
@Json(name = "m.relates_to") val relatesTo: RelationDefaultContent? @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent?
) : VerificationInfo { ) : VerificationInfo<ValidVerificationDone> {
override val transactionID: String? override val transactionId: String?
get() = relatesTo?.eventId get() = relatesTo?.eventId
override fun isValid() = transactionID?.isNotEmpty() == true
override fun toEventContent(): Content? = toContent() override fun toEventContent(): Content? = toContent()
override fun asValidObject(): ValidVerificationDone? {
val validTransactionId = transactionId?.takeIf { it.isNotEmpty() } ?: return null
return ValidVerificationDone(
validTransactionId
)
} }
}
internal data class ValidVerificationDone(
val transactionId: String
)

View file

@ -22,7 +22,6 @@ import im.vector.matrix.android.api.session.events.model.toContent
import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent
import im.vector.matrix.android.internal.crypto.verification.VerificationInfoKey import im.vector.matrix.android.internal.crypto.verification.VerificationInfoKey
import im.vector.matrix.android.internal.crypto.verification.VerificationInfoKeyFactory import im.vector.matrix.android.internal.crypto.verification.VerificationInfoKeyFactory
import timber.log.Timber
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class MessageVerificationKeyContent( internal data class MessageVerificationKeyContent(
@ -33,17 +32,9 @@ internal data class MessageVerificationKeyContent(
@Json(name = "m.relates_to") val relatesTo: RelationDefaultContent? @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent?
) : VerificationInfoKey { ) : VerificationInfoKey {
override val transactionID: String? override val transactionId: String?
get() = relatesTo?.eventId get() = relatesTo?.eventId
override fun isValid(): Boolean {
if (transactionID.isNullOrBlank() || key.isNullOrBlank()) {
Timber.e("## received invalid verification request")
return false
}
return true
}
override fun toEventContent() = toContent() override fun toEventContent() = toContent()
companion object : VerificationInfoKeyFactory { companion object : VerificationInfoKeyFactory {

View file

@ -30,18 +30,11 @@ internal data class MessageVerificationMacContent(
@Json(name = "m.relates_to") val relatesTo: RelationDefaultContent? @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent?
) : VerificationInfoMac { ) : VerificationInfoMac {
override val transactionID: String? override val transactionId: String?
get() = relatesTo?.eventId get() = relatesTo?.eventId
override fun toEventContent() = toContent() override fun toEventContent() = toContent()
override fun isValid(): Boolean {
if (transactionID.isNullOrBlank() || keys.isNullOrBlank() || mac.isNullOrEmpty()) {
return false
}
return true
}
companion object : VerificationInfoMacFactory { companion object : VerificationInfoMacFactory {
override fun create(tid: String, mac: Map<String, String>, keys: String): VerificationInfoMac { override fun create(tid: String, mac: Map<String, String>, keys: String): VerificationInfoMac {
return MessageVerificationMacContent( return MessageVerificationMacContent(

View file

@ -30,18 +30,11 @@ internal data class MessageVerificationReadyContent(
@Json(name = "m.relates_to") val relatesTo: RelationDefaultContent? @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent?
) : VerificationInfoReady { ) : VerificationInfoReady {
override val transactionID: String? override val transactionId: String?
get() = relatesTo?.eventId get() = relatesTo?.eventId
override fun toEventContent() = toContent() override fun toEventContent() = toContent()
override fun isValid(): Boolean {
if (transactionID.isNullOrBlank() || methods.isNullOrEmpty() || fromDevice.isNullOrEmpty()) {
return false
}
return true
}
companion object : MessageVerificationReadyFactory { companion object : MessageVerificationReadyFactory {
override fun create(tid: String, methods: List<String>, fromDevice: String): VerificationInfoReady { override fun create(tid: String, methods: List<String>, fromDevice: String): VerificationInfoReady {
return MessageVerificationReadyContent( return MessageVerificationReadyContent(

View file

@ -33,18 +33,10 @@ data class MessageVerificationRequestContent(
@Json(name = "format") val format: String? = null, @Json(name = "format") val format: String? = null,
@Json(name = "formatted_body") val formattedBody: String? = null, @Json(name = "formatted_body") val formattedBody: String? = null,
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
@Json(name = "m.new_content") override val newContent: Content? = null @Json(name = "m.new_content") override val newContent: Content? = null,
// Not parsed, but set after, using the eventId
override val transactionId: String? = null
) : MessageContent, VerificationInfoRequest { ) : MessageContent, VerificationInfoRequest {
override fun isValid(): Boolean {
if (transactionID.isNullOrBlank() || methods.isNullOrEmpty() || fromDevice.isNullOrEmpty()) {
return false
}
return true
}
override val transactionID: String?
get() = relatesTo?.eventId
override fun toEventContent() = toContent() override fun toEventContent() = toContent()
} }

View file

@ -17,15 +17,10 @@ package im.vector.matrix.android.api.session.room.model.message
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.session.crypto.verification.SasMode
import im.vector.matrix.android.api.session.events.model.toContent import im.vector.matrix.android.api.session.events.model.toContent
import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent
import im.vector.matrix.android.internal.crypto.model.rest.VERIFICATION_METHOD_RECIPROCATE
import im.vector.matrix.android.internal.crypto.model.rest.VERIFICATION_METHOD_SAS
import im.vector.matrix.android.internal.crypto.verification.SASDefaultVerificationTransaction
import im.vector.matrix.android.internal.crypto.verification.VerificationInfoStart import im.vector.matrix.android.internal.crypto.verification.VerificationInfoStart
import im.vector.matrix.android.internal.util.JsonCanonicalizer import im.vector.matrix.android.internal.util.JsonCanonicalizer
import timber.log.Timber
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class MessageVerificationStartContent( internal data class MessageVerificationStartContent(
@ -39,46 +34,12 @@ internal data class MessageVerificationStartContent(
@Json(name = "secret") override val sharedSecret: String? @Json(name = "secret") override val sharedSecret: String?
) : VerificationInfoStart { ) : VerificationInfoStart {
override fun toCanonicalJson(): String? { override fun toCanonicalJson(): String {
return JsonCanonicalizer.getCanonicalJson(MessageVerificationStartContent::class.java, this) return JsonCanonicalizer.getCanonicalJson(MessageVerificationStartContent::class.java, this)
} }
override val transactionID: String? override val transactionId: String?
get() = relatesTo?.eventId get() = relatesTo?.eventId
// TODO Move those method to the interface?
override fun isValid(): Boolean {
if (transactionID.isNullOrBlank()
|| fromDevice.isNullOrBlank()
|| (method == VERIFICATION_METHOD_SAS && !isValidSas())
|| (method == VERIFICATION_METHOD_RECIPROCATE && !isValidReciprocate())) {
Timber.e("## received invalid verification request")
return false
}
return true
}
private fun isValidSas(): Boolean {
if (keyAgreementProtocols.isNullOrEmpty()
|| hashes.isNullOrEmpty()
|| !hashes.contains("sha256") || messageAuthenticationCodes.isNullOrEmpty()
|| (!messageAuthenticationCodes.contains(SASDefaultVerificationTransaction.SAS_MAC_SHA256)
&& !messageAuthenticationCodes.contains(SASDefaultVerificationTransaction.SAS_MAC_SHA256_LONGKDF))
|| shortAuthenticationStrings.isNullOrEmpty()
|| !shortAuthenticationStrings.contains(SasMode.DECIMAL)) {
return false
}
return true
}
private fun isValidReciprocate(): Boolean {
if (sharedSecret.isNullOrBlank()) {
return false
}
return true
}
override fun toEventContent() = toContent() override fun toEventContent() = toContent()
} }

View file

@ -51,4 +51,4 @@ data class MessageVideoContent(
* Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption. * Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption.
*/ */
@Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null @Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null
) : MessageEncryptedContent ) : MessageWithAttachmentContent

View file

@ -21,7 +21,7 @@ import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo
/** /**
* Interface for message which can contains an encrypted file * Interface for message which can contains an encrypted file
*/ */
interface MessageEncryptedContent : MessageContent { interface MessageWithAttachmentContent : MessageContent {
/** /**
* Required if the file is unencrypted. The URL (typically MXC URI) to the image. * Required if the file is unencrypted. The URL (typically MXC URI) to the image.
*/ */
@ -36,4 +36,4 @@ interface MessageEncryptedContent : MessageContent {
/** /**
* Get the url of the encrypted file or of the file * Get the url of the encrypted file or of the file
*/ */
fun MessageEncryptedContent.getFileUrl() = encryptedFileInfo?.url ?: url fun MessageWithAttachmentContent.getFileUrl() = encryptedFileInfo?.url ?: url

View file

@ -28,5 +28,7 @@ sealed class SharedSecretStorageError(message: String?) : Throwable(message) {
object BadKeyFormat : SharedSecretStorageError("Bad Key Format") object BadKeyFormat : SharedSecretStorageError("Bad Key Format")
object ParsingError : SharedSecretStorageError("parsing Error") object ParsingError : SharedSecretStorageError("parsing Error")
object BadMac : SharedSecretStorageError("Bad mac") object BadMac : SharedSecretStorageError("Bad mac")
object BadCipherText : SharedSecretStorageError("Bad cipher text")
data class OtherError(val reason: Throwable) : SharedSecretStorageError(reason.localizedMessage) data class OtherError(val reason: Throwable) : SharedSecretStorageError(reason.localizedMessage)
} }

View file

@ -627,9 +627,7 @@ internal class DefaultCryptoService @Inject constructor(
*/ */
@Throws(MXCryptoError::class) @Throws(MXCryptoError::class)
override fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult { override fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
return runBlocking { return internalDecryptEvent(event, timeline)
internalDecryptEvent(event, timeline)
}
} }
/** /**
@ -657,7 +655,7 @@ internal class DefaultCryptoService @Inject constructor(
* @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack. * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
* @return the MXEventDecryptionResult data, or null in case of error * @return the MXEventDecryptionResult data, or null in case of error
*/ */
private suspend fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult { private fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
val eventContent = event.content val eventContent = event.content
if (eventContent == null) { if (eventContent == null) {
Timber.e("## decryptEvent : empty event content") Timber.e("## decryptEvent : empty event content")

View file

@ -34,7 +34,7 @@ internal interface IMXDecrypting {
* @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack. * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
* @return the decryption information, or an error * @return the decryption information, or an error
*/ */
suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult
/** /**
* Handle a key event. * Handle a key event.

View file

@ -63,7 +63,7 @@ internal class MXMegolmDecryption(private val userId: String,
*/ */
private var pendingEvents: MutableMap<String /* senderKey|sessionId */, MutableMap<String /* timelineId */, MutableList<Event>>> = HashMap() private var pendingEvents: MutableMap<String /* senderKey|sessionId */, MutableMap<String /* timelineId */, MutableList<Event>>> = HashMap()
override suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult { override fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
// If cross signing is enabled, we don't send request until the keys are trusted // If cross signing is enabled, we don't send request until the keys are trusted
// There could be a race effect here when xsigning is enabled, we should ensure that keys was downloaded once // There could be a race effect here when xsigning is enabled, we should ensure that keys was downloaded once
val requestOnFail = cryptoStore.getMyCrossSigningInfo()?.isTrusted() == true val requestOnFail = cryptoStore.getMyCrossSigningInfo()?.isTrusted() == true

View file

@ -38,7 +38,7 @@ internal class MXOlmDecryption(
private val userId: String) private val userId: String)
: IMXDecrypting { : IMXDecrypting {
override suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult { override fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
val olmEventContent = event.content.toModel<OlmEventContent>() ?: run { val olmEventContent = event.content.toModel<OlmEventContent>() ?: run {
Timber.e("## decryptEvent() : bad event format") Timber.e("## decryptEvent() : bad event format")
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_EVENT_FORMAT, throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_EVENT_FORMAT,

View file

@ -80,7 +80,7 @@ internal class DefaultCrossSigningService @Inject constructor(
cryptoStore.getCrossSigningPrivateKeys()?.let { privateKeysInfo -> cryptoStore.getCrossSigningPrivateKeys()?.let { privateKeysInfo ->
privateKeysInfo.master privateKeysInfo.master
?.fromBase64NoPadding() ?.fromBase64()
?.let { privateKeySeed -> ?.let { privateKeySeed ->
val pkSigning = OlmPkSigning() val pkSigning = OlmPkSigning()
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.masterKey()?.unpaddedBase64PublicKey) { if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.masterKey()?.unpaddedBase64PublicKey) {
@ -93,7 +93,7 @@ internal class DefaultCrossSigningService @Inject constructor(
} }
} }
privateKeysInfo.user privateKeysInfo.user
?.fromBase64NoPadding() ?.fromBase64()
?.let { privateKeySeed -> ?.let { privateKeySeed ->
val pkSigning = OlmPkSigning() val pkSigning = OlmPkSigning()
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.userKey()?.unpaddedBase64PublicKey) { if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.userKey()?.unpaddedBase64PublicKey) {
@ -106,7 +106,7 @@ internal class DefaultCrossSigningService @Inject constructor(
} }
} }
privateKeysInfo.selfSigned privateKeysInfo.selfSigned
?.fromBase64NoPadding() ?.fromBase64()
?.let { privateKeySeed -> ?.let { privateKeySeed ->
val pkSigning = OlmPkSigning() val pkSigning = OlmPkSigning()
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.selfSigningKey()?.unpaddedBase64PublicKey) { if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.selfSigningKey()?.unpaddedBase64PublicKey) {
@ -307,7 +307,7 @@ internal class DefaultCrossSigningService @Inject constructor(
var userKeyIsTrusted = false var userKeyIsTrusted = false
var selfSignedKeyIsTrusted = false var selfSignedKeyIsTrusted = false
masterKeyPrivateKey?.fromBase64NoPadding() masterKeyPrivateKey?.fromBase64()
?.let { privateKeySeed -> ?.let { privateKeySeed ->
val pkSigning = OlmPkSigning() val pkSigning = OlmPkSigning()
try { try {
@ -324,7 +324,7 @@ internal class DefaultCrossSigningService @Inject constructor(
} }
} }
uskKeyPrivateKey?.fromBase64NoPadding() uskKeyPrivateKey?.fromBase64()
?.let { privateKeySeed -> ?.let { privateKeySeed ->
val pkSigning = OlmPkSigning() val pkSigning = OlmPkSigning()
try { try {
@ -341,7 +341,7 @@ internal class DefaultCrossSigningService @Inject constructor(
} }
} }
sskPrivateKey?.fromBase64NoPadding() sskPrivateKey?.fromBase64()
?.let { privateKeySeed -> ?.let { privateKeySeed ->
val pkSigning = OlmPkSigning() val pkSigning = OlmPkSigning()
try { try {
@ -450,7 +450,7 @@ internal class DefaultCrossSigningService @Inject constructor(
// 1) check if I know the private key // 1) check if I know the private key
val masterPrivateKey = cryptoStore.getCrossSigningPrivateKeys() val masterPrivateKey = cryptoStore.getCrossSigningPrivateKeys()
?.master ?.master
?.fromBase64NoPadding() ?.fromBase64()
var isMaterKeyTrusted = false var isMaterKeyTrusted = false
if (myMasterKey.trustLevel?.locallyVerified == true) { if (myMasterKey.trustLevel?.locallyVerified == true) {

View file

@ -19,6 +19,7 @@ import android.util.Base64
import im.vector.matrix.android.internal.crypto.model.CryptoCrossSigningKey import im.vector.matrix.android.internal.crypto.model.CryptoCrossSigningKey
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.matrix.android.internal.util.JsonCanonicalizer import im.vector.matrix.android.internal.util.JsonCanonicalizer
import timber.log.Timber
fun CryptoDeviceInfo.canonicalSignable(): String { fun CryptoDeviceInfo.canonicalSignable(): String {
return JsonCanonicalizer.getCanonicalJson(Map::class.java, signalableJSONDictionary()) return JsonCanonicalizer.getCanonicalJson(Map::class.java, signalableJSONDictionary())
@ -32,6 +33,18 @@ fun ByteArray.toBase64NoPadding(): String {
return Base64.encodeToString(this, Base64.NO_PADDING or Base64.NO_WRAP) return Base64.encodeToString(this, Base64.NO_PADDING or Base64.NO_WRAP)
} }
fun String.fromBase64NoPadding(): ByteArray { fun String.fromBase64(): ByteArray {
return Base64.decode(this, Base64.NO_PADDING or Base64.NO_WRAP) return Base64.decode(this, Base64.DEFAULT)
}
/**
* Decode the base 64. Return null in case of bad format. Should be used when parsing received data from external source
*/
fun String.fromBase64Safe(): ByteArray? {
return try {
Base64.decode(this, Base64.DEFAULT)
} catch (throwable: Throwable) {
Timber.e(throwable, "Unable to decode base64 string")
null
}
} }

View file

@ -19,21 +19,19 @@ import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import im.vector.matrix.android.internal.crypto.verification.VerificationInfoAccept import im.vector.matrix.android.internal.crypto.verification.VerificationInfoAccept
import im.vector.matrix.android.internal.crypto.verification.VerificationInfoAcceptFactory import im.vector.matrix.android.internal.crypto.verification.VerificationInfoAcceptFactory
import timber.log.Timber
/** /**
* Sent by Bob to accept a verification from a previously sent m.key.verification.start message. * Sent by Bob to accept a verification from a previously sent m.key.verification.start message.
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class KeyVerificationAccept( internal data class KeyVerificationAccept(
/** /**
* string to identify the transaction. * string to identify the transaction.
* This string must be unique for the pair of users performing verification for the duration that the transaction is valid. * This string must be unique for the pair of users performing verification for the duration that the transaction is valid.
* Alices device should record this ID and use it in future messages in this transaction. * Alices device should record this ID and use it in future messages in this transaction.
*/ */
@Json(name = "transaction_id") @Json(name = "transaction_id")
override val transactionID: String? = null, override val transactionId: String? = null,
/** /**
* The key agreement protocol that Bobs device has selected to use, out of the list proposed by Alices device * The key agreement protocol that Bobs device has selected to use, out of the list proposed by Alices device
@ -67,19 +65,6 @@ internal data class KeyVerificationAccept(
override var commitment: String? = null override var commitment: String? = null
) : SendToDeviceObject, VerificationInfoAccept { ) : SendToDeviceObject, VerificationInfoAccept {
override fun isValid(): Boolean {
if (transactionID.isNullOrBlank()
|| keyAgreementProtocol.isNullOrBlank()
|| hash.isNullOrBlank()
|| commitment.isNullOrBlank()
|| messageAuthenticationCode.isNullOrBlank()
|| shortAuthenticationStrings.isNullOrEmpty()) {
Timber.e("## received invalid verification request")
return false
}
return true
}
override fun toSendToDeviceObject() = this override fun toSendToDeviceObject() = this
companion object : VerificationInfoAcceptFactory { companion object : VerificationInfoAcceptFactory {
@ -90,7 +75,7 @@ internal data class KeyVerificationAccept(
messageAuthenticationCode: String, messageAuthenticationCode: String,
shortAuthenticationStrings: List<String>): VerificationInfoAccept { shortAuthenticationStrings: List<String>): VerificationInfoAccept {
return KeyVerificationAccept( return KeyVerificationAccept(
transactionID = tid, transactionId = tid,
keyAgreementProtocol = keyAgreementProtocol, keyAgreementProtocol = keyAgreementProtocol,
hash = hash, hash = hash,
commitment = commitment, commitment = commitment,

View file

@ -29,7 +29,7 @@ internal data class KeyVerificationCancel(
* the transaction ID of the verification to cancel * the transaction ID of the verification to cancel
*/ */
@Json(name = "transaction_id") @Json(name = "transaction_id")
override val transactionID: String? = null, override val transactionId: String? = null,
/** /**
* machine-readable reason for cancelling, see #CancelCode * machine-readable reason for cancelling, see #CancelCode
@ -53,11 +53,4 @@ internal data class KeyVerificationCancel(
} }
override fun toSendToDeviceObject() = this override fun toSendToDeviceObject() = this
override fun isValid(): Boolean {
if (transactionID.isNullOrBlank() || code.isNullOrBlank()) {
return false
}
return true
}
} }

View file

@ -24,15 +24,8 @@ import im.vector.matrix.android.internal.crypto.verification.VerificationInfoDon
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class KeyVerificationDone( internal data class KeyVerificationDone(
@Json(name = "transaction_id") override val transactionID: String? = null @Json(name = "transaction_id") override val transactionId: String? = null
) : SendToDeviceObject, VerificationInfoDone { ) : SendToDeviceObject, VerificationInfoDone {
override fun toSendToDeviceObject() = this override fun toSendToDeviceObject() = this
override fun isValid(): Boolean {
if (transactionID.isNullOrBlank()) {
return false
}
return true
}
} }

View file

@ -28,7 +28,7 @@ internal data class KeyVerificationKey(
/** /**
* the ID of the transaction that the message is part of * the ID of the transaction that the message is part of
*/ */
@Json(name = "transaction_id") override val transactionID: String? = null, @Json(name = "transaction_id") override val transactionId: String? = null,
/** /**
* The devices ephemeral public key, as an unpadded base64 string * The devices ephemeral public key, as an unpadded base64 string
@ -44,11 +44,4 @@ internal data class KeyVerificationKey(
} }
override fun toSendToDeviceObject() = this override fun toSendToDeviceObject() = this
override fun isValid(): Boolean {
if (transactionID.isNullOrBlank() || key.isNullOrBlank()) {
return false
}
return true
}
} }

View file

@ -25,19 +25,12 @@ import im.vector.matrix.android.internal.crypto.verification.VerificationInfoMac
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class KeyVerificationMac( internal data class KeyVerificationMac(
@Json(name = "transaction_id") override val transactionID: String? = null, @Json(name = "transaction_id") override val transactionId: String? = null,
@Json(name = "mac") override val mac: Map<String, String>? = null, @Json(name = "mac") override val mac: Map<String, String>? = null,
@Json(name = "keys") override val keys: String? = null @Json(name = "keys") override val keys: String? = null
) : SendToDeviceObject, VerificationInfoMac { ) : SendToDeviceObject, VerificationInfoMac {
override fun isValid(): Boolean {
if (transactionID.isNullOrBlank() || keys.isNullOrBlank() || mac.isNullOrEmpty()) {
return false
}
return true
}
override fun toSendToDeviceObject(): SendToDeviceObject? = this override fun toSendToDeviceObject(): SendToDeviceObject? = this
companion object : VerificationInfoMacFactory { companion object : VerificationInfoMacFactory {

View file

@ -26,12 +26,8 @@ import im.vector.matrix.android.internal.crypto.verification.VerificationInfoRea
internal data class KeyVerificationReady( internal data class KeyVerificationReady(
@Json(name = "from_device") override val fromDevice: String?, @Json(name = "from_device") override val fromDevice: String?,
@Json(name = "methods") override val methods: List<String>?, @Json(name = "methods") override val methods: List<String>?,
@Json(name = "transaction_id") override val transactionID: String? = null @Json(name = "transaction_id") override val transactionId: String? = null
) : SendToDeviceObject, VerificationInfoReady { ) : SendToDeviceObject, VerificationInfoReady {
override fun toSendToDeviceObject() = this override fun toSendToDeviceObject() = this
override fun isValid(): Boolean {
return !transactionID.isNullOrBlank() && !fromDevice.isNullOrBlank() && !methods.isNullOrEmpty()
}
} }

View file

@ -27,16 +27,8 @@ internal data class KeyVerificationRequest(
@Json(name = "from_device") override val fromDevice: String?, @Json(name = "from_device") override val fromDevice: String?,
@Json(name = "methods") override val methods: List<String>, @Json(name = "methods") override val methods: List<String>,
@Json(name = "timestamp") override val timestamp: Long?, @Json(name = "timestamp") override val timestamp: Long?,
@Json(name = "transaction_id") override val transactionID: String? = null @Json(name = "transaction_id") override val transactionId: String? = null
) : SendToDeviceObject, VerificationInfoRequest { ) : SendToDeviceObject, VerificationInfoRequest {
override fun toSendToDeviceObject() = this override fun toSendToDeviceObject() = this
override fun isValid(): Boolean {
if (transactionID.isNullOrBlank() || methods.isNullOrEmpty() || fromDevice.isNullOrEmpty()) {
return false
}
return true
}
} }

View file

@ -17,11 +17,8 @@ package im.vector.matrix.android.internal.crypto.model.rest
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.session.crypto.verification.SasMode
import im.vector.matrix.android.internal.crypto.verification.SASDefaultVerificationTransaction
import im.vector.matrix.android.internal.crypto.verification.VerificationInfoStart import im.vector.matrix.android.internal.crypto.verification.VerificationInfoStart
import im.vector.matrix.android.internal.util.JsonCanonicalizer import im.vector.matrix.android.internal.util.JsonCanonicalizer
import timber.log.Timber
/** /**
* Sent by Alice to initiate an interactive key verification. * Sent by Alice to initiate an interactive key verification.
@ -29,8 +26,8 @@ import timber.log.Timber
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class KeyVerificationStart( internal data class KeyVerificationStart(
@Json(name = "from_device") override val fromDevice: String? = null, @Json(name = "from_device") override val fromDevice: String? = null,
override val method: String? = null, @Json(name = "method") override val method: String? = null,
@Json(name = "transaction_id") override val transactionID: String? = null, @Json(name = "transaction_id") override val transactionId: String? = null,
@Json(name = "key_agreement_protocols") override val keyAgreementProtocols: List<String>? = null, @Json(name = "key_agreement_protocols") override val keyAgreementProtocols: List<String>? = null,
@Json(name = "hashes") override val hashes: List<String>? = null, @Json(name = "hashes") override val hashes: List<String>? = null,
@Json(name = "message_authentication_codes") override val messageAuthenticationCodes: List<String>? = null, @Json(name = "message_authentication_codes") override val messageAuthenticationCodes: List<String>? = null,
@ -39,43 +36,9 @@ internal data class KeyVerificationStart(
@Json(name = "secret") override val sharedSecret: String? = null @Json(name = "secret") override val sharedSecret: String? = null
) : SendToDeviceObject, VerificationInfoStart { ) : SendToDeviceObject, VerificationInfoStart {
override fun toCanonicalJson(): String? { override fun toCanonicalJson(): String {
return JsonCanonicalizer.getCanonicalJson(KeyVerificationStart::class.java, this) return JsonCanonicalizer.getCanonicalJson(KeyVerificationStart::class.java, this)
} }
// TODO Move those method to the interface?
override fun isValid(): Boolean {
if (transactionID.isNullOrBlank()
|| fromDevice.isNullOrBlank()
|| (method == VERIFICATION_METHOD_SAS && !isValidSas())
|| (method == VERIFICATION_METHOD_RECIPROCATE && !isValidReciprocate())) {
Timber.e("## received invalid verification request")
return false
}
return true
}
private fun isValidSas(): Boolean {
if (keyAgreementProtocols.isNullOrEmpty()
|| hashes.isNullOrEmpty()
|| !hashes.contains("sha256") || messageAuthenticationCodes.isNullOrEmpty()
|| (!messageAuthenticationCodes.contains(SASDefaultVerificationTransaction.SAS_MAC_SHA256)
&& !messageAuthenticationCodes.contains(SASDefaultVerificationTransaction.SAS_MAC_SHA256_LONGKDF))
|| shortAuthenticationStrings.isNullOrEmpty()
|| !shortAuthenticationStrings.contains(SasMode.DECIMAL)) {
return false
}
return true
}
private fun isValidReciprocate(): Boolean {
if (sharedSecret.isNullOrBlank()) {
return false
}
return true
}
override fun toSendToDeviceObject() = this override fun toSendToDeviceObject() = this
} }

View file

@ -35,7 +35,7 @@ import im.vector.matrix.android.api.session.securestorage.SsssKeySpec
import im.vector.matrix.android.api.session.securestorage.SsssPassphrase import im.vector.matrix.android.api.session.securestorage.SsssPassphrase
import im.vector.matrix.android.internal.crypto.SSSS_ALGORITHM_AES_HMAC_SHA2 import im.vector.matrix.android.internal.crypto.SSSS_ALGORITHM_AES_HMAC_SHA2
import im.vector.matrix.android.internal.crypto.SSSS_ALGORITHM_CURVE25519_AES_SHA2 import im.vector.matrix.android.internal.crypto.SSSS_ALGORITHM_CURVE25519_AES_SHA2
import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64NoPadding import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64
import im.vector.matrix.android.internal.crypto.crosssigning.toBase64NoPadding import im.vector.matrix.android.internal.crypto.crosssigning.toBase64NoPadding
import im.vector.matrix.android.internal.crypto.keysbackup.generatePrivateKeyWithPassword import im.vector.matrix.android.internal.crypto.keysbackup.generatePrivateKeyWithPassword
import im.vector.matrix.android.internal.crypto.keysbackup.util.computeRecoveryKey import im.vector.matrix.android.internal.crypto.keysbackup.util.computeRecoveryKey
@ -268,7 +268,7 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
val ivParameterSpec = IvParameterSpec(iv) val ivParameterSpec = IvParameterSpec(iv)
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec) cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec)
// secret are not that big, just do Final // secret are not that big, just do Final
val cipherBytes = cipher.doFinal(clearDataBase64.fromBase64NoPadding()) val cipherBytes = cipher.doFinal(clearDataBase64.fromBase64())
require(cipherBytes.isNotEmpty()) require(cipherBytes.isNotEmpty())
val macKeySpec = SecretKeySpec(macKey, "HmacSHA256") val macKeySpec = SecretKeySpec(macKey, "HmacSHA256")
@ -295,9 +295,9 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
val aesKey = pseudoRandomKey.copyOfRange(0, 32) val aesKey = pseudoRandomKey.copyOfRange(0, 32)
val macKey = pseudoRandomKey.copyOfRange(32, 64) val macKey = pseudoRandomKey.copyOfRange(32, 64)
val iv = cipherContent.initializationVector?.fromBase64NoPadding() ?: ByteArray(16) val iv = cipherContent.initializationVector?.fromBase64() ?: ByteArray(16)
val cipherRawBytes = cipherContent.ciphertext!!.fromBase64NoPadding() val cipherRawBytes = cipherContent.ciphertext?.fromBase64() ?: throw SharedSecretStorageError.BadCipherText
val cipher = Cipher.getInstance("AES/CTR/NoPadding") val cipher = Cipher.getInstance("AES/CTR/NoPadding")
@ -314,7 +314,7 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
val mac = Mac.getInstance("HmacSHA256").apply { init(macKeySpec) } val mac = Mac.getInstance("HmacSHA256").apply { init(macKeySpec) }
val digest = mac.doFinal(cipherRawBytes) val digest = mac.doFinal(cipherRawBytes)
if (!cipherContent.mac?.fromBase64NoPadding()?.contentEquals(digest).orFalse()) { if (!cipherContent.mac?.fromBase64()?.contentEquals(digest).orFalse()) {
throw SharedSecretStorageError.BadMac throw SharedSecretStorageError.BadMac
} else { } else {
// we are good // we are good

View file

@ -41,22 +41,22 @@ internal class DefaultSendVerificationMessageTask @Inject constructor(
override suspend fun execute(params: SendVerificationMessageTask.Params): String { override suspend fun execute(params: SendVerificationMessageTask.Params): String {
val event = handleEncryption(params) val event = handleEncryption(params)
val localID = event.eventId!! val localId = event.eventId!!
try { try {
localEchoUpdater.updateSendState(localID, SendState.SENDING) localEchoUpdater.updateSendState(localId, SendState.SENDING)
val executeRequest = executeRequest<SendResponse>(eventBus) { val executeRequest = executeRequest<SendResponse>(eventBus) {
apiCall = roomAPI.send( apiCall = roomAPI.send(
localID, localId,
roomId = event.roomId ?: "", roomId = event.roomId ?: "",
content = event.content, content = event.content,
eventType = event.type eventType = event.type
) )
} }
localEchoUpdater.updateSendState(localID, SendState.SENT) localEchoUpdater.updateSendState(localId, SendState.SENT)
return executeRequest.eventId return executeRequest.eventId
} catch (e: Throwable) { } catch (e: Throwable) {
localEchoUpdater.updateSendState(localID, SendState.UNDELIVERED) localEchoUpdater.updateSendState(localId, SendState.UNDELIVERED)
throw e throw e
} }
} }

View file

@ -76,7 +76,7 @@ internal class DefaultIncomingSASDefaultVerificationTransaction(
} }
} }
override fun onVerificationStart(startReq: VerificationInfoStart) { override fun onVerificationStart(startReq: ValidVerificationInfoStart.SasVerificationInfoStart) {
Timber.v("## SAS I: received verification request from state $state") Timber.v("## SAS I: received verification request from state $state")
if (state != VerificationTxState.None) { if (state != VerificationTxState.None) {
Timber.e("## SAS I: received verification request from invalid state") Timber.e("## SAS I: received verification request from invalid state")
@ -100,10 +100,10 @@ internal class DefaultIncomingSASDefaultVerificationTransaction(
// Select a key agreement protocol, a hash algorithm, a message authentication code, // Select a key agreement protocol, a hash algorithm, a message authentication code,
// and short authentication string methods out of the lists given in requester's message. // and short authentication string methods out of the lists given in requester's message.
val agreedProtocol = startReq!!.keyAgreementProtocols?.firstOrNull { KNOWN_AGREEMENT_PROTOCOLS.contains(it) } val agreedProtocol = startReq!!.keyAgreementProtocols.firstOrNull { KNOWN_AGREEMENT_PROTOCOLS.contains(it) }
val agreedHash = startReq!!.hashes?.firstOrNull { KNOWN_HASHES.contains(it) } val agreedHash = startReq!!.hashes.firstOrNull { KNOWN_HASHES.contains(it) }
val agreedMac = startReq!!.messageAuthenticationCodes?.firstOrNull { KNOWN_MACS.contains(it) } val agreedMac = startReq!!.messageAuthenticationCodes.firstOrNull { KNOWN_MACS.contains(it) }
val agreedShortCode = startReq!!.shortAuthenticationStrings?.filter { KNOWN_SHORT_CODES.contains(it) } val agreedShortCode = startReq!!.shortAuthenticationStrings.filter { KNOWN_SHORT_CODES.contains(it) }
// No common key sharing/hashing/hmac/SAS methods. // No common key sharing/hashing/hmac/SAS methods.
// If a device is unable to complete the verification because the devices are unable to find a common key sharing, // If a device is unable to complete the verification because the devices are unable to find a common key sharing,
@ -141,12 +141,12 @@ internal class DefaultIncomingSASDefaultVerificationTransaction(
} }
private fun doAccept(accept: VerificationInfoAccept) { private fun doAccept(accept: VerificationInfoAccept) {
this.accepted = accept this.accepted = accept.asValidObject()
Timber.v("## SAS incoming accept request id:$transactionId") Timber.v("## SAS incoming accept request id:$transactionId")
// The hash commitment is the hash (using the selected hash algorithm) of the unpadded base64 representation of QB, // The hash commitment is the hash (using the selected hash algorithm) of the unpadded base64 representation of QB,
// concatenated with the canonical JSON representation of the content of the m.key.verification.start message // concatenated with the canonical JSON representation of the content of the m.key.verification.start message
val concat = getSAS().publicKey + startReq!!.toCanonicalJson() val concat = getSAS().publicKey + startReq!!.canonicalJson
accept.commitment = hashUsingAgreedHashMethod(concat) ?: "" accept.commitment = hashUsingAgreedHashMethod(concat) ?: ""
// we need to send this to other device now // we need to send this to other device now
state = VerificationTxState.SendingAccept state = VerificationTxState.SendingAccept
@ -158,12 +158,12 @@ internal class DefaultIncomingSASDefaultVerificationTransaction(
} }
} }
override fun onVerificationAccept(accept: VerificationInfoAccept) { override fun onVerificationAccept(accept: ValidVerificationInfoAccept) {
Timber.v("## SAS invalid message for incoming request id:$transactionId") Timber.v("## SAS invalid message for incoming request id:$transactionId")
cancel(CancelCode.UnexpectedMessage) cancel(CancelCode.UnexpectedMessage)
} }
override fun onKeyVerificationKey(vKey: VerificationInfoKey) { override fun onKeyVerificationKey(vKey: ValidVerificationInfoKey) {
Timber.v("## SAS received key for request id:$transactionId") Timber.v("## SAS received key for request id:$transactionId")
if (state != VerificationTxState.SendingAccept && state != VerificationTxState.Accepted) { if (state != VerificationTxState.SendingAccept && state != VerificationTxState.Accepted) {
Timber.e("## SAS received key from invalid state $state") Timber.e("## SAS received key from invalid state $state")
@ -213,7 +213,7 @@ internal class DefaultIncomingSASDefaultVerificationTransaction(
state = VerificationTxState.ShortCodeReady state = VerificationTxState.ShortCodeReady
} }
override fun onKeyVerificationMac(vKey: VerificationInfoMac) { override fun onKeyVerificationMac(vMac: ValidVerificationInfoMac) {
Timber.v("## SAS I: received mac for request id:$transactionId") Timber.v("## SAS I: received mac for request id:$transactionId")
// Check for state? // Check for state?
if (state != VerificationTxState.SendingKey if (state != VerificationTxState.SendingKey
@ -226,12 +226,13 @@ internal class DefaultIncomingSASDefaultVerificationTransaction(
cancel(CancelCode.UnexpectedMessage) cancel(CancelCode.UnexpectedMessage)
return return
} }
theirMac = vKey
theirMac = vMac
// Do I have my Mac? // Do I have my Mac?
if (myMac != null) { if (myMac != null) {
// I can check // I can check
verifyMacs() verifyMacs(vMac)
} }
// Wait for ShortCode Accepted // Wait for ShortCode Accepted
} }

View file

@ -74,7 +74,7 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction(
} }
} }
override fun onVerificationStart(startReq: VerificationInfoStart) { override fun onVerificationStart(startReq: ValidVerificationInfoStart.SasVerificationInfoStart) {
Timber.e("## SAS O: onVerificationStart - unexpected id:$transactionId") Timber.e("## SAS O: onVerificationStart - unexpected id:$transactionId")
cancel(CancelCode.UnexpectedMessage) cancel(CancelCode.UnexpectedMessage)
} }
@ -95,7 +95,7 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction(
KNOWN_SHORT_CODES KNOWN_SHORT_CODES
) )
startReq = startMessage startReq = startMessage.asValidObject() as? ValidVerificationInfoStart.SasVerificationInfoStart
state = VerificationTxState.SendingStart state = VerificationTxState.SendingStart
sendToOther( sendToOther(
@ -118,7 +118,7 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction(
// fromDevice = session.sessionParams.credentials.deviceId ?: "", // fromDevice = session.sessionParams.credentials.deviceId ?: "",
// methods = listOf(KeyVerificationStart.VERIF_METHOD_SAS), // methods = listOf(KeyVerificationStart.VERIF_METHOD_SAS),
// timestamp = System.currentTimeMillis().toInt(), // timestamp = System.currentTimeMillis().toInt(),
// transactionID = transactionId // transactionId = transactionId
// ) // )
// //
// sendToOther( // sendToOther(
@ -130,7 +130,7 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction(
// ) // )
// } // }
override fun onVerificationAccept(accept: VerificationInfoAccept) { override fun onVerificationAccept(accept: ValidVerificationInfoAccept) {
Timber.v("## SAS O: onVerificationAccept id:$transactionId") Timber.v("## SAS O: onVerificationAccept id:$transactionId")
if (state != VerificationTxState.Started) { if (state != VerificationTxState.Started) {
Timber.e("## SAS O: received accept request from invalid state $state") Timber.e("## SAS O: received accept request from invalid state $state")
@ -141,7 +141,7 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction(
if (!KNOWN_AGREEMENT_PROTOCOLS.contains(accept.keyAgreementProtocol) if (!KNOWN_AGREEMENT_PROTOCOLS.contains(accept.keyAgreementProtocol)
|| !KNOWN_HASHES.contains(accept.hash) || !KNOWN_HASHES.contains(accept.hash)
|| !KNOWN_MACS.contains(accept.messageAuthenticationCode) || !KNOWN_MACS.contains(accept.messageAuthenticationCode)
|| accept.shortAuthenticationStrings!!.intersect(KNOWN_SHORT_CODES).isEmpty()) { || accept.shortAuthenticationStrings.intersect(KNOWN_SHORT_CODES).isEmpty()) {
Timber.e("## SAS O: received accept request from invalid state") Timber.e("## SAS O: received accept request from invalid state")
cancel(CancelCode.UnknownMethod) cancel(CancelCode.UnknownMethod)
return return
@ -167,7 +167,7 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction(
} }
} }
override fun onKeyVerificationKey(vKey: VerificationInfoKey) { override fun onKeyVerificationKey(vKey: ValidVerificationInfoKey) {
Timber.v("## SAS O: onKeyVerificationKey id:$transactionId") Timber.v("## SAS O: onKeyVerificationKey id:$transactionId")
if (state != VerificationTxState.SendingKey && state != VerificationTxState.KeySent) { if (state != VerificationTxState.SendingKey && state != VerificationTxState.KeySent) {
Timber.e("## received key from invalid state $state") Timber.e("## received key from invalid state $state")
@ -182,7 +182,7 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction(
// in Bobs m.key.verification.key and the content of Alices m.key.verification.start message. // in Bobs m.key.verification.key and the content of Alices m.key.verification.start message.
// check commitment // check commitment
val concat = vKey.key + startReq!!.toCanonicalJson() val concat = vKey.key + startReq!!.canonicalJson
val otherCommitment = hashUsingAgreedHashMethod(concat) ?: "" val otherCommitment = hashUsingAgreedHashMethod(concat) ?: ""
if (accepted!!.commitment.equals(otherCommitment)) { if (accepted!!.commitment.equals(otherCommitment)) {
@ -206,7 +206,7 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction(
} }
} }
override fun onKeyVerificationMac(vKey: VerificationInfoMac) { override fun onKeyVerificationMac(vMac: ValidVerificationInfoMac) {
Timber.v("## SAS O: onKeyVerificationMac id:$transactionId") Timber.v("## SAS O: onKeyVerificationMac id:$transactionId")
if (state != VerificationTxState.OnKeyReceived if (state != VerificationTxState.OnKeyReceived
&& state != VerificationTxState.ShortCodeReady && state != VerificationTxState.ShortCodeReady
@ -218,12 +218,12 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction(
return return
} }
theirMac = vKey theirMac = vMac
// Do I have my Mac? // Do I have my Mac?
if (myMac != null) { if (myMac != null) {
// I can check // I can check
verifyMacs() verifyMacs(vMac)
} }
// Wait for ShortCode Accepted // Wait for ShortCode Accepted
} }

View file

@ -23,8 +23,10 @@ import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.crypto.CryptoService
import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService
import im.vector.matrix.android.api.session.crypto.verification.CancelCode import im.vector.matrix.android.api.session.crypto.verification.CancelCode
import im.vector.matrix.android.api.session.crypto.verification.PendingVerificationRequest
import im.vector.matrix.android.api.session.crypto.verification.QrCodeVerificationTransaction import im.vector.matrix.android.api.session.crypto.verification.QrCodeVerificationTransaction
import im.vector.matrix.android.api.session.crypto.verification.SasVerificationTransaction import im.vector.matrix.android.api.session.crypto.verification.SasVerificationTransaction
import im.vector.matrix.android.api.session.crypto.verification.ValidVerificationInfoReady
import im.vector.matrix.android.api.session.crypto.verification.VerificationMethod import im.vector.matrix.android.api.session.crypto.verification.VerificationMethod
import im.vector.matrix.android.api.session.crypto.verification.VerificationService import im.vector.matrix.android.api.session.crypto.verification.VerificationService
import im.vector.matrix.android.api.session.crypto.verification.VerificationTransaction import im.vector.matrix.android.api.session.crypto.verification.VerificationTransaction
@ -45,6 +47,7 @@ import im.vector.matrix.android.api.session.room.model.message.MessageVerificati
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationReadyContent import im.vector.matrix.android.api.session.room.model.message.MessageVerificationReadyContent
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationStartContent import im.vector.matrix.android.api.session.room.model.message.MessageVerificationStartContent
import im.vector.matrix.android.api.session.room.model.message.ValidVerificationDone
import im.vector.matrix.android.internal.crypto.DeviceListManager import im.vector.matrix.android.internal.crypto.DeviceListManager
import im.vector.matrix.android.internal.crypto.MyDeviceInfoHolder import im.vector.matrix.android.internal.crypto.MyDeviceInfoHolder
import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction
@ -73,7 +76,6 @@ import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import okhttp3.internal.toImmutableList
import timber.log.Timber import timber.log.Timber
import java.util.UUID import java.util.UUID
import javax.inject.Inject import javax.inject.Inject
@ -105,7 +107,7 @@ internal class DefaultVerificationService @Inject constructor(
* Map [sender: [PendingVerificationRequest]] * Map [sender: [PendingVerificationRequest]]
* For now we keep all requests (even terminated ones) during the lifetime of the app. * For now we keep all requests (even terminated ones) during the lifetime of the app.
*/ */
private val pendingRequests = HashMap<String, ArrayList<PendingVerificationRequest>>() private val pendingRequests = HashMap<String, MutableList<PendingVerificationRequest>>()
// Event received from the sync // Event received from the sync
fun onToDeviceEvent(event: Event) { fun onToDeviceEvent(event: Event) {
@ -268,9 +270,9 @@ internal class DefaultVerificationService @Inject constructor(
} }
private fun onRequestReceived(event: Event) { private fun onRequestReceived(event: Event) {
val requestInfo = event.getClearContent().toModel<KeyVerificationRequest>()!! val validRequestInfo = event.getClearContent().toModel<KeyVerificationRequest>()?.asValidObject()
if (!requestInfo.isValid()) { if (validRequestInfo == null) {
// ignore // ignore
Timber.e("## SAS Received invalid key request") Timber.e("## SAS Received invalid key request")
return return
@ -278,7 +280,7 @@ internal class DefaultVerificationService @Inject constructor(
val senderId = event.senderId ?: return val senderId = event.senderId ?: return
// We don't want to block here // We don't want to block here
val otherDeviceId = requestInfo.fromDevice ?: return val otherDeviceId = validRequestInfo.fromDevice
GlobalScope.launch { GlobalScope.launch {
if (checkKeysAreDownloaded(senderId, otherDeviceId) == null) { if (checkKeysAreDownloaded(senderId, otherDeviceId) == null) {
@ -287,19 +289,16 @@ internal class DefaultVerificationService @Inject constructor(
} }
// Remember this request // Remember this request
val requestsForUser = pendingRequests[senderId] val requestsForUser = pendingRequests.getOrPut(senderId) { mutableListOf() }
?: ArrayList<PendingVerificationRequest>().also {
pendingRequests[event.senderId] = it
}
val pendingVerificationRequest = PendingVerificationRequest( val pendingVerificationRequest = PendingVerificationRequest(
ageLocalTs = event.ageLocalTs ?: System.currentTimeMillis(), ageLocalTs = event.ageLocalTs ?: System.currentTimeMillis(),
isIncoming = true, isIncoming = true,
otherUserId = senderId, // requestInfo.toUserId, otherUserId = senderId, // requestInfo.toUserId,
roomId = null, roomId = null,
transactionId = requestInfo.transactionID, transactionId = validRequestInfo.transactionId,
localID = requestInfo.transactionID!!, localId = validRequestInfo.transactionId,
requestInfo = requestInfo requestInfo = validRequestInfo
) )
requestsForUser.add(pendingVerificationRequest) requestsForUser.add(pendingVerificationRequest)
dispatchRequestAdded(pendingVerificationRequest) dispatchRequestAdded(pendingVerificationRequest)
@ -307,10 +306,13 @@ internal class DefaultVerificationService @Inject constructor(
suspend fun onRoomRequestReceived(event: Event) { suspend fun onRoomRequestReceived(event: Event) {
Timber.v("## SAS Verification request from ${event.senderId} in room ${event.roomId}") Timber.v("## SAS Verification request from ${event.senderId} in room ${event.roomId}")
val requestInfo = event.getClearContent().toModel<MessageVerificationRequestContent>() val requestInfo = event.getClearContent().toModel<MessageVerificationRequestContent>() ?: return
?: return val validRequestInfo = requestInfo
// copy the EventId to the transactionId
.copy(transactionId = event.eventId)
.asValidObject() ?: return
val senderId = event.senderId ?: return val senderId = event.senderId ?: return
val fromDevice = requestInfo.fromDevice ?: return
if (requestInfo.toUserId != userId) { if (requestInfo.toUserId != userId) {
// I should ignore this, it's not for me // I should ignore this, it's not for me
@ -320,16 +322,13 @@ internal class DefaultVerificationService @Inject constructor(
// We don't want to block here // We don't want to block here
GlobalScope.launch { GlobalScope.launch {
if (checkKeysAreDownloaded(senderId, fromDevice) == null) { if (checkKeysAreDownloaded(senderId, validRequestInfo.fromDevice) == null) {
Timber.e("## SAS Verification device $fromDevice is not known") Timber.e("## SAS Verification device ${validRequestInfo.fromDevice} is not known")
} }
} }
// Remember this request // Remember this request
val requestsForUser = pendingRequests[senderId] val requestsForUser = pendingRequests.getOrPut(senderId) { mutableListOf() }
?: ArrayList<PendingVerificationRequest>().also {
pendingRequests[event.senderId] = it
}
val pendingVerificationRequest = PendingVerificationRequest( val pendingVerificationRequest = PendingVerificationRequest(
ageLocalTs = event.ageLocalTs ?: System.currentTimeMillis(), ageLocalTs = event.ageLocalTs ?: System.currentTimeMillis(),
@ -337,8 +336,8 @@ internal class DefaultVerificationService @Inject constructor(
otherUserId = senderId, // requestInfo.toUserId, otherUserId = senderId, // requestInfo.toUserId,
roomId = event.roomId, roomId = event.roomId,
transactionId = event.eventId, transactionId = event.eventId,
localID = event.eventId!!, localId = event.eventId!!,
requestInfo = requestInfo requestInfo = validRequestInfo
) )
requestsForUser.add(pendingVerificationRequest) requestsForUser.add(pendingVerificationRequest)
dispatchRequestAdded(pendingVerificationRequest) dispatchRequestAdded(pendingVerificationRequest)
@ -362,13 +361,15 @@ internal class DefaultVerificationService @Inject constructor(
relatesTo = event.content.toModel<MessageRelationContent>()?.relatesTo relatesTo = event.content.toModel<MessageRelationContent>()?.relatesTo
) )
val validStartReq = startReq?.asValidObject()
val otherUserId = event.senderId val otherUserId = event.senderId
if (startReq?.isValid()?.not() == true) { if (validStartReq == null) {
Timber.e("## received invalid verification request") Timber.e("## received invalid verification request")
if (startReq.transactionID != null) { if (startReq?.transactionId != null) {
verificationTransportRoomMessageFactory.createTransport(event.roomId ?: "", null) verificationTransportRoomMessageFactory.createTransport(event.roomId ?: "", null)
.cancelTransaction( .cancelTransaction(
startReq.transactionID ?: "", startReq.transactionId ?: "",
otherUserId!!, otherUserId!!,
startReq.fromDevice ?: event.getSenderKey()!!, startReq.fromDevice ?: event.getSenderKey()!!,
CancelCode.UnknownMethod CancelCode.UnknownMethod
@ -377,14 +378,14 @@ internal class DefaultVerificationService @Inject constructor(
return return
} }
handleStart(otherUserId, startReq as VerificationInfoStart) { handleStart(otherUserId, validStartReq) {
it.transport = verificationTransportRoomMessageFactory.createTransport(event.roomId ?: "", it) it.transport = verificationTransportRoomMessageFactory.createTransport(event.roomId ?: "", it)
}?.let { }?.let {
verificationTransportRoomMessageFactory.createTransport(event.roomId ?: "", null) verificationTransportRoomMessageFactory.createTransport(event.roomId ?: "", null)
.cancelTransaction( .cancelTransaction(
startReq.transactionID ?: "", validStartReq.transactionId,
otherUserId!!, otherUserId!!,
startReq.fromDevice ?: event.getSenderKey()!!, validStartReq.fromDevice,
it it
) )
} }
@ -392,16 +393,17 @@ internal class DefaultVerificationService @Inject constructor(
private suspend fun onStartRequestReceived(event: Event) { private suspend fun onStartRequestReceived(event: Event) {
Timber.e("## SAS received Start request ${event.eventId}") Timber.e("## SAS received Start request ${event.eventId}")
val startReq = event.getClearContent().toModel<KeyVerificationStart>()!! val startReq = event.getClearContent().toModel<KeyVerificationStart>()
val validStartReq = startReq?.asValidObject()
Timber.v("## SAS received Start request $startReq") Timber.v("## SAS received Start request $startReq")
val otherUserId = event.senderId val otherUserId = event.senderId!!
if (!startReq.isValid()) { if (validStartReq == null) {
Timber.e("## SAS received invalid verification request") Timber.e("## SAS received invalid verification request")
if (startReq.transactionID != null) { if (startReq?.transactionId != null) {
verificationTransportToDeviceFactory.createTransport(null).cancelTransaction( verificationTransportToDeviceFactory.createTransport(null).cancelTransaction(
startReq.transactionID, startReq.transactionId,
otherUserId!!, otherUserId,
startReq.fromDevice ?: event.getSenderKey()!!, startReq.fromDevice ?: event.getSenderKey()!!,
CancelCode.UnknownMethod CancelCode.UnknownMethod
) )
@ -409,13 +411,13 @@ internal class DefaultVerificationService @Inject constructor(
return return
} }
// Download device keys prior to everything // Download device keys prior to everything
handleStart(otherUserId, startReq) { handleStart(otherUserId, validStartReq) {
it.transport = verificationTransportToDeviceFactory.createTransport(it) it.transport = verificationTransportToDeviceFactory.createTransport(it)
}?.let { }?.let {
verificationTransportToDeviceFactory.createTransport(null).cancelTransaction( verificationTransportToDeviceFactory.createTransport(null).cancelTransaction(
startReq.transactionID ?: "", validStartReq.transactionId,
otherUserId!!, otherUserId,
startReq.fromDevice ?: event.getSenderKey()!!, validStartReq.fromDevice,
it it
) )
} }
@ -424,18 +426,20 @@ internal class DefaultVerificationService @Inject constructor(
/** /**
* Return a CancelCode to make the caller cancel the verification. Else return null * Return a CancelCode to make the caller cancel the verification. Else return null
*/ */
private suspend fun handleStart(otherUserId: String?, startReq: VerificationInfoStart, txConfigure: (DefaultVerificationTransaction) -> Unit): CancelCode? { private suspend fun handleStart(otherUserId: String?,
Timber.d("## SAS onStartRequestReceived ${startReq.transactionID}") startReq: ValidVerificationInfoStart,
if (checkKeysAreDownloaded(otherUserId!!, startReq.fromDevice ?: "") != null) { txConfigure: (DefaultVerificationTransaction) -> Unit): CancelCode? {
val tid = startReq.transactionID!! Timber.d("## SAS onStartRequestReceived ${startReq.transactionId}")
if (checkKeysAreDownloaded(otherUserId!!, startReq.fromDevice) != null) {
val tid = startReq.transactionId
val existing = getExistingTransaction(otherUserId, tid) val existing = getExistingTransaction(otherUserId, tid)
when (startReq.method) { when (startReq) {
VERIFICATION_METHOD_SAS -> { is ValidVerificationInfoStart.SasVerificationInfoStart -> {
when (existing) { when (existing) {
is SasVerificationTransaction -> { is SasVerificationTransaction -> {
// should cancel both! // should cancel both!
Timber.v("## SAS onStartRequestReceived - Request exist with same id ${startReq.transactionID}") Timber.v("## SAS onStartRequestReceived - Request exist with same id ${startReq.transactionId}")
existing.cancel(CancelCode.UnexpectedMessage) existing.cancel(CancelCode.UnexpectedMessage)
// Already cancelled, so return null // Already cancelled, so return null
return null return null
@ -450,7 +454,7 @@ internal class DefaultVerificationService @Inject constructor(
?.also { ?.also {
// Multiple keyshares between two devices: // Multiple keyshares between two devices:
// any two devices may only have at most one key verification in flight at a time. // any two devices may only have at most one key verification in flight at a time.
Timber.v("## SAS onStartRequestReceived - Already a transaction with this user ${startReq.transactionID}") Timber.v("## SAS onStartRequestReceived - Already a transaction with this user ${startReq.transactionId}")
} }
?.forEach { ?.forEach {
it.cancel(CancelCode.UnexpectedMessage) it.cancel(CancelCode.UnexpectedMessage)
@ -462,12 +466,12 @@ internal class DefaultVerificationService @Inject constructor(
} }
// Ok we can create a SAS transaction // Ok we can create a SAS transaction
Timber.v("## SAS onStartRequestReceived - request accepted ${startReq.transactionID!!}") Timber.v("## SAS onStartRequestReceived - request accepted ${startReq.transactionId}")
// If there is a corresponding request, we can auto accept // If there is a corresponding request, we can auto accept
// as we are the one requesting in first place (or we accepted the request) // as we are the one requesting in first place (or we accepted the request)
// I need to check if the pending request was related to this device also // I need to check if the pending request was related to this device also
val autoAccept = getExistingVerificationRequest(otherUserId)?.any { val autoAccept = getExistingVerificationRequest(otherUserId)?.any {
it.transactionId == startReq.transactionID it.transactionId == startReq.transactionId
&& (it.requestInfo?.fromDevice == this.deviceId || it.readyInfo?.fromDevice == this.deviceId) && (it.requestInfo?.fromDevice == this.deviceId || it.readyInfo?.fromDevice == this.deviceId)
} }
?: false ?: false
@ -479,27 +483,23 @@ internal class DefaultVerificationService @Inject constructor(
cryptoStore, cryptoStore,
crossSigningService, crossSigningService,
myDeviceInfoHolder.get().myDevice.fingerprint()!!, myDeviceInfoHolder.get().myDevice.fingerprint()!!,
startReq.transactionID!!, startReq.transactionId,
otherUserId, otherUserId,
autoAccept).also { txConfigure(it) } autoAccept).also { txConfigure(it) }
addTransaction(tx) addTransaction(tx)
tx.acceptVerificationEvent(otherUserId, startReq) tx.onVerificationStart(startReq)
return null return null
} }
VERIFICATION_METHOD_RECIPROCATE -> { is ValidVerificationInfoStart.ReciprocateVerificationInfoStart -> {
// Other user has scanned my QR code // Other user has scanned my QR code
if (existing is DefaultQrCodeVerificationTransaction) { if (existing is DefaultQrCodeVerificationTransaction) {
existing.onStartReceived(startReq) existing.onStartReceived(startReq)
return null return null
} else { } else {
Timber.w("## SAS onStartRequestReceived - unexpected message ${startReq.transactionID}") Timber.w("## SAS onStartRequestReceived - unexpected message ${startReq.transactionId}")
return CancelCode.UnexpectedMessage return CancelCode.UnexpectedMessage
} }
} }
else -> {
Timber.e("## SAS onStartRequestReceived - unknown method ${startReq.method}")
return CancelCode.UnknownMethod
}
} }
} else { } else {
return CancelCode.UnexpectedMessage return CancelCode.UnexpectedMessage
@ -529,24 +529,27 @@ internal class DefaultVerificationService @Inject constructor(
// relates_to is in clear in encrypted payload // relates_to is in clear in encrypted payload
relatesTo = event.content.toModel<MessageRelationContent>()?.relatesTo relatesTo = event.content.toModel<MessageRelationContent>()?.relatesTo
) )
if (cancelReq == null || cancelReq.isValid().not()) {
val validCancelReq = cancelReq?.asValidObject()
if (validCancelReq == null) {
// ignore // ignore
Timber.e("## SAS Received invalid key request") Timber.e("## SAS Received invalid key request")
// TODO should we cancel? // TODO should we cancel?
return return
} }
getExistingVerificationRequest(event.senderId ?: "", cancelReq.transactionID)?.let { getExistingVerificationRequest(event.senderId ?: "", validCancelReq.transactionId)?.let {
updatePendingRequest(it.copy(cancelConclusion = safeValueOf(cancelReq.code))) updatePendingRequest(it.copy(cancelConclusion = safeValueOf(validCancelReq.code)))
// Should we remove it from the list? // Should we remove it from the list?
} }
handleOnCancel(event.senderId!!, cancelReq) handleOnCancel(event.senderId!!, validCancelReq)
} }
private fun onCancelReceived(event: Event) { private fun onCancelReceived(event: Event) {
Timber.v("## SAS onCancelReceived") Timber.v("## SAS onCancelReceived")
val cancelReq = event.getClearContent().toModel<KeyVerificationCancel>()!! val cancelReq = event.getClearContent().toModel<KeyVerificationCancel>()?.asValidObject()
if (!cancelReq.isValid()) { if (cancelReq == null) {
// ignore // ignore
Timber.e("## SAS Received invalid cancel request") Timber.e("## SAS Received invalid cancel request")
return return
@ -556,11 +559,11 @@ internal class DefaultVerificationService @Inject constructor(
handleOnCancel(otherUserId, cancelReq) handleOnCancel(otherUserId, cancelReq)
} }
private fun handleOnCancel(otherUserId: String, cancelReq: VerificationInfoCancel) { private fun handleOnCancel(otherUserId: String, cancelReq: ValidVerificationInfoCancel) {
Timber.v("## SAS onCancelReceived otherUser: $otherUserId reason: ${cancelReq.reason}") Timber.v("## SAS onCancelReceived otherUser: $otherUserId reason: ${cancelReq.reason}")
val existingTransaction = getExistingTransaction(otherUserId, cancelReq.transactionID!!) val existingTransaction = getExistingTransaction(otherUserId, cancelReq.transactionId)
val existingRequest = getExistingVerificationRequest(otherUserId, cancelReq.transactionID!!) val existingRequest = getExistingVerificationRequest(otherUserId, cancelReq.transactionId)
if (existingRequest != null) { if (existingRequest != null) {
// Mark this request as cancelled // Mark this request as cancelled
@ -582,30 +585,28 @@ internal class DefaultVerificationService @Inject constructor(
relatesTo = event.content.toModel<MessageRelationContent>()?.relatesTo relatesTo = event.content.toModel<MessageRelationContent>()?.relatesTo
) )
?: return ?: return
handleAccept(accept, event.senderId!!)
val validAccept = accept.asValidObject() ?: return
handleAccept(validAccept, event.senderId!!)
} }
private fun onAcceptReceived(event: Event) { private fun onAcceptReceived(event: Event) {
Timber.d("## SAS Received Accept $event") Timber.d("## SAS Received Accept $event")
val acceptReq = event.getClearContent().toModel<KeyVerificationAccept>() ?: return val acceptReq = event.getClearContent().toModel<KeyVerificationAccept>()?.asValidObject() ?: return
handleAccept(acceptReq, event.senderId!!) handleAccept(acceptReq, event.senderId!!)
} }
private fun handleAccept(acceptReq: VerificationInfoAccept, senderId: String) { private fun handleAccept(acceptReq: ValidVerificationInfoAccept, senderId: String) {
if (!acceptReq.isValid()) {
// ignore
Timber.e("## SAS Received invalid accept request")
return
}
val otherUserId = senderId val otherUserId = senderId
val existing = getExistingTransaction(otherUserId, acceptReq.transactionID!!) val existing = getExistingTransaction(otherUserId, acceptReq.transactionId)
if (existing == null) { if (existing == null) {
Timber.e("## SAS Received invalid accept request") Timber.e("## SAS Received invalid accept request")
return return
} }
if (existing is SASDefaultVerificationTransaction) { if (existing is SASDefaultVerificationTransaction) {
existing.acceptVerificationEvent(otherUserId, acceptReq) existing.onVerificationAccept(acceptReq)
} else { } else {
// not other types now // not other types now
} }
@ -617,7 +618,8 @@ internal class DefaultVerificationService @Inject constructor(
// relates_to is in clear in encrypted payload // relates_to is in clear in encrypted payload
relatesTo = event.content.toModel<MessageRelationContent>()?.relatesTo relatesTo = event.content.toModel<MessageRelationContent>()?.relatesTo
) )
if (keyReq == null || keyReq.isValid().not()) { ?.asValidObject()
if (keyReq == null) {
// ignore // ignore
Timber.e("## SAS Received invalid key request") Timber.e("## SAS Received invalid key request")
// TODO should we cancel? // TODO should we cancel?
@ -627,9 +629,9 @@ internal class DefaultVerificationService @Inject constructor(
} }
private fun onKeyReceived(event: Event) { private fun onKeyReceived(event: Event) {
val keyReq = event.getClearContent().toModel<KeyVerificationKey>()!! val keyReq = event.getClearContent().toModel<KeyVerificationKey>()?.asValidObject()
if (!keyReq.isValid()) { if (keyReq == null) {
// ignore // ignore
Timber.e("## SAS Received invalid key request") Timber.e("## SAS Received invalid key request")
return return
@ -637,16 +639,16 @@ internal class DefaultVerificationService @Inject constructor(
handleKeyReceived(event, keyReq) handleKeyReceived(event, keyReq)
} }
private fun handleKeyReceived(event: Event, keyReq: VerificationInfoKey) { private fun handleKeyReceived(event: Event, keyReq: ValidVerificationInfoKey) {
Timber.d("## SAS Received Key from ${event.senderId} with info $keyReq") Timber.d("## SAS Received Key from ${event.senderId} with info $keyReq")
val otherUserId = event.senderId!! val otherUserId = event.senderId!!
val existing = getExistingTransaction(otherUserId, keyReq.transactionID!!) val existing = getExistingTransaction(otherUserId, keyReq.transactionId)
if (existing == null) { if (existing == null) {
Timber.e("## SAS Received invalid key request") Timber.e("## SAS Received invalid key request")
return return
} }
if (existing is SASDefaultVerificationTransaction) { if (existing is SASDefaultVerificationTransaction) {
existing.acceptVerificationEvent(otherUserId, keyReq) existing.onKeyVerificationKey(keyReq)
} else { } else {
// not other types now // not other types now
} }
@ -658,7 +660,8 @@ internal class DefaultVerificationService @Inject constructor(
// relates_to is in clear in encrypted payload // relates_to is in clear in encrypted payload
relatesTo = event.content.toModel<MessageRelationContent>()?.relatesTo relatesTo = event.content.toModel<MessageRelationContent>()?.relatesTo
) )
if (macReq == null || macReq.isValid().not() || event.senderId == null) { ?.asValidObject()
if (macReq == null || event.senderId == null) {
// ignore // ignore
Timber.e("## SAS Received invalid mac request") Timber.e("## SAS Received invalid mac request")
// TODO should we cancel? // TODO should we cancel?
@ -673,13 +676,14 @@ internal class DefaultVerificationService @Inject constructor(
// relates_to is in clear in encrypted payload // relates_to is in clear in encrypted payload
relatesTo = event.content.toModel<MessageRelationContent>()?.relatesTo relatesTo = event.content.toModel<MessageRelationContent>()?.relatesTo
) )
if (readyReq == null || readyReq.isValid().not() || event.senderId == null) { ?.asValidObject()
if (readyReq == null || event.senderId == null) {
// ignore // ignore
Timber.e("## SAS Received invalid ready request") Timber.e("## SAS Received invalid ready request")
// TODO should we cancel? // TODO should we cancel?
return return
} }
if (checkKeysAreDownloaded(event.senderId, readyReq.fromDevice ?: "") == null) { if (checkKeysAreDownloaded(event.senderId, readyReq.fromDevice) == null) {
Timber.e("## SAS Verification device ${readyReq.fromDevice} is not known") Timber.e("## SAS Verification device ${readyReq.fromDevice} is not known")
// TODO cancel? // TODO cancel?
return return
@ -691,15 +695,15 @@ internal class DefaultVerificationService @Inject constructor(
} }
private suspend fun onReadyReceived(event: Event) { private suspend fun onReadyReceived(event: Event) {
val readyReq = event.getClearContent().toModel<KeyVerificationReady>() val readyReq = event.getClearContent().toModel<KeyVerificationReady>()?.asValidObject()
if (readyReq == null || readyReq.isValid().not() || event.senderId == null) { if (readyReq == null || event.senderId == null) {
// ignore // ignore
Timber.e("## SAS Received invalid ready request") Timber.e("## SAS Received invalid ready request")
// TODO should we cancel? // TODO should we cancel?
return return
} }
if (checkKeysAreDownloaded(event.senderId, readyReq.fromDevice ?: "") == null) { if (checkKeysAreDownloaded(event.senderId, readyReq.fromDevice) == null) {
Timber.e("## SAS Verification device ${readyReq.fromDevice} is not known") Timber.e("## SAS Verification device ${readyReq.fromDevice} is not known")
// TODO cancel? // TODO cancel?
return return
@ -716,8 +720,9 @@ internal class DefaultVerificationService @Inject constructor(
// relates_to is in clear in encrypted payload // relates_to is in clear in encrypted payload
relatesTo = event.content.toModel<MessageRelationContent>()?.relatesTo relatesTo = event.content.toModel<MessageRelationContent>()?.relatesTo
) )
?.asValidObject()
if (doneReq == null || doneReq.isValid().not() || event.senderId == null) { if (doneReq == null || event.senderId == null) {
// ignore // ignore
Timber.e("## SAS Received invalid Done request") Timber.e("## SAS Received invalid Done request")
// TODO should we cancel? // TODO should we cancel?
@ -728,9 +733,9 @@ internal class DefaultVerificationService @Inject constructor(
} }
private fun onMacReceived(event: Event) { private fun onMacReceived(event: Event) {
val macReq = event.getClearContent().toModel<KeyVerificationMac>()!! val macReq = event.getClearContent().toModel<KeyVerificationMac>()?.asValidObject()
if (!macReq.isValid() || event.senderId == null) { if (macReq == null || event.senderId == null) {
// ignore // ignore
Timber.e("## SAS Received invalid mac request") Timber.e("## SAS Received invalid mac request")
return return
@ -738,41 +743,41 @@ internal class DefaultVerificationService @Inject constructor(
handleMacReceived(event.senderId, macReq) handleMacReceived(event.senderId, macReq)
} }
private fun handleMacReceived(senderId: String, macReq: VerificationInfoMac) { private fun handleMacReceived(senderId: String, macReq: ValidVerificationInfoMac) {
Timber.v("## SAS Received $macReq") Timber.v("## SAS Received $macReq")
val existing = getExistingTransaction(senderId, macReq.transactionID!!) val existing = getExistingTransaction(senderId, macReq.transactionId)
if (existing == null) { if (existing == null) {
Timber.e("## SAS Received invalid Mac request") Timber.e("## SAS Received invalid Mac request")
return return
} }
if (existing is SASDefaultVerificationTransaction) { if (existing is SASDefaultVerificationTransaction) {
existing.acceptVerificationEvent(senderId, macReq) existing.onKeyVerificationMac(macReq)
} else { } else {
// not other types known for now // not other types known for now
} }
} }
private fun handleReadyReceived(senderId: String, private fun handleReadyReceived(senderId: String,
readyReq: VerificationInfoReady, readyReq: ValidVerificationInfoReady,
transportCreator: (DefaultVerificationTransaction) -> VerificationTransport) { transportCreator: (DefaultVerificationTransaction) -> VerificationTransport) {
val existingRequest = getExistingVerificationRequest(senderId)?.find { it.transactionId == readyReq.transactionID } val existingRequest = getExistingVerificationRequest(senderId)?.find { it.transactionId == readyReq.transactionId }
if (existingRequest == null) { if (existingRequest == null) {
Timber.e("## SAS Received Ready for unknown request txId:${readyReq.transactionID} fromDevice ${readyReq.fromDevice}") Timber.e("## SAS Received Ready for unknown request txId:${readyReq.transactionId} fromDevice ${readyReq.fromDevice}")
return return
} }
val qrCodeData = readyReq.methods val qrCodeData = readyReq.methods
// Check if other user is able to scan QR code // Check if other user is able to scan QR code
?.takeIf { it.contains(VERIFICATION_METHOD_QR_CODE_SCAN) } .takeIf { it.contains(VERIFICATION_METHOD_QR_CODE_SCAN) }
?.let { ?.let {
createQrCodeData(existingRequest.transactionId, existingRequest.otherUserId, readyReq.fromDevice) createQrCodeData(existingRequest.transactionId, existingRequest.otherUserId, readyReq.fromDevice)
} }
if (readyReq.methods.orEmpty().contains(VERIFICATION_METHOD_RECIPROCATE)) { if (readyReq.methods.contains(VERIFICATION_METHOD_RECIPROCATE)) {
// Create the pending transaction // Create the pending transaction
val tx = DefaultQrCodeVerificationTransaction( val tx = DefaultQrCodeVerificationTransaction(
setDeviceVerificationAction, setDeviceVerificationAction,
readyReq.transactionID!!, readyReq.transactionId,
senderId, senderId,
readyReq.fromDevice, readyReq.fromDevice,
crossSigningService, crossSigningService,
@ -886,10 +891,10 @@ internal class DefaultVerificationService @Inject constructor(
) )
} }
private fun handleDoneReceived(senderId: String, doneInfo: VerificationInfo) { private fun handleDoneReceived(senderId: String, doneInfo: ValidVerificationDone) {
val existingRequest = getExistingVerificationRequest(senderId)?.find { it.transactionId == doneInfo.transactionID } val existingRequest = getExistingVerificationRequest(senderId)?.find { it.transactionId == doneInfo.transactionId }
if (existingRequest == null) { if (existingRequest == null) {
Timber.e("## SAS Received Done for unknown request txId:${doneInfo.transactionID}") Timber.e("## SAS Received Done for unknown request txId:${doneInfo.transactionId}")
return return
} }
updatePendingRequest(existingRequest.copy(isSuccessful = true)) updatePendingRequest(existingRequest.copy(isSuccessful = true))
@ -975,15 +980,12 @@ internal class DefaultVerificationService @Inject constructor(
: PendingVerificationRequest { : PendingVerificationRequest {
Timber.i("## SAS Requesting verification to user: $otherUserId in room $roomId") Timber.i("## SAS Requesting verification to user: $otherUserId in room $roomId")
val requestsForUser = pendingRequests[otherUserId] val requestsForUser = pendingRequests.getOrPut(otherUserId) { mutableListOf() }
?: ArrayList<PendingVerificationRequest>().also {
pendingRequests[otherUserId] = it
}
val transport = verificationTransportRoomMessageFactory.createTransport(roomId, null) val transport = verificationTransportRoomMessageFactory.createTransport(roomId, null)
// Cancel existing pending requests? // Cancel existing pending requests?
requestsForUser.toImmutableList().forEach { existingRequest -> requestsForUser.toList().forEach { existingRequest ->
existingRequest.transactionId?.let { tid -> existingRequest.transactionId?.let { tid ->
if (!existingRequest.isFinished) { if (!existingRequest.isFinished) {
Timber.d("## SAS, cancelling pending requests to start a new one") Timber.d("## SAS, cancelling pending requests to start a new one")
@ -993,13 +995,13 @@ internal class DefaultVerificationService @Inject constructor(
} }
} }
val localID = localId ?: LocalEcho.createLocalEchoId() val validLocalId = localId ?: LocalEcho.createLocalEchoId()
val verificationRequest = PendingVerificationRequest( val verificationRequest = PendingVerificationRequest(
ageLocalTs = System.currentTimeMillis(), ageLocalTs = System.currentTimeMillis(),
isIncoming = false, isIncoming = false,
roomId = roomId, roomId = roomId,
localID = localID, localId = validLocalId,
otherUserId = otherUserId otherUserId = otherUserId
) )
@ -1019,7 +1021,7 @@ internal class DefaultVerificationService @Inject constructor(
} }
.distinct() .distinct()
transport.sendVerificationRequest(methodValues, localID, otherUserId, roomId, null) { syncedId, info -> transport.sendVerificationRequest(methodValues, validLocalId, otherUserId, roomId, null) { syncedId, info ->
// We need to update with the syncedID // We need to update with the syncedID
updatePendingRequest(verificationRequest.copy( updatePendingRequest(verificationRequest.copy(
transactionId = syncedId, transactionId = syncedId,
@ -1039,15 +1041,12 @@ internal class DefaultVerificationService @Inject constructor(
Timber.i("## Requesting verification to user: $otherUserId with device list $otherDevices") Timber.i("## Requesting verification to user: $otherUserId with device list $otherDevices")
val targetDevices = otherDevices ?: cryptoService.getUserDevices(otherUserId).map { it.deviceId } val targetDevices = otherDevices ?: cryptoService.getUserDevices(otherUserId).map { it.deviceId }
val requestsForUser = pendingRequests[otherUserId] val requestsForUser = pendingRequests.getOrPut(otherUserId) { mutableListOf() }
?: ArrayList<PendingVerificationRequest>().also {
pendingRequests[otherUserId] = it
}
val transport = verificationTransportToDeviceFactory.createTransport(null) val transport = verificationTransportToDeviceFactory.createTransport(null)
// Cancel existing pending requests? // Cancel existing pending requests?
requestsForUser.toImmutableList().forEach { existingRequest -> requestsForUser.toList().forEach { existingRequest ->
existingRequest.transactionId?.let { tid -> existingRequest.transactionId?.let { tid ->
if (!existingRequest.isFinished) { if (!existingRequest.isFinished) {
Timber.d("## SAS, cancelling pending requests to start a new one") Timber.d("## SAS, cancelling pending requests to start a new one")
@ -1059,14 +1058,14 @@ internal class DefaultVerificationService @Inject constructor(
} }
} }
val localID = LocalEcho.createLocalEchoId() val localId = LocalEcho.createLocalEchoId()
val verificationRequest = PendingVerificationRequest( val verificationRequest = PendingVerificationRequest(
transactionId = localID, transactionId = localId,
ageLocalTs = System.currentTimeMillis(), ageLocalTs = System.currentTimeMillis(),
isIncoming = false, isIncoming = false,
roomId = null, roomId = null,
localID = localID, localId = localId,
otherUserId = otherUserId, otherUserId = otherUserId,
targetDevices = targetDevices targetDevices = targetDevices
) )
@ -1087,7 +1086,7 @@ internal class DefaultVerificationService @Inject constructor(
} }
.distinct() .distinct()
transport.sendVerificationRequest(methodValues, localID, otherUserId, null, targetDevices) { _, info -> transport.sendVerificationRequest(methodValues, localId, otherUserId, null, targetDevices) { _, info ->
// Nothing special to do in to device mode // Nothing special to do in to device mode
updatePendingRequest(verificationRequest.copy( updatePendingRequest(verificationRequest.copy(
// localId stays different // localId stays different
@ -1113,13 +1112,10 @@ internal class DefaultVerificationService @Inject constructor(
} }
private fun updatePendingRequest(updated: PendingVerificationRequest) { private fun updatePendingRequest(updated: PendingVerificationRequest) {
val requestsForUser = pendingRequests[updated.otherUserId] val requestsForUser = pendingRequests.getOrPut(updated.otherUserId) { mutableListOf() }
?: ArrayList<PendingVerificationRequest>().also {
pendingRequests[updated.otherUserId] = it
}
val index = requestsForUser.indexOfFirst { val index = requestsForUser.indexOfFirst {
it.transactionId == updated.transactionId it.transactionId == updated.transactionId
|| it.transactionId == null && it.localID == updated.localID || it.transactionId == null && it.localId == updated.localId
} }
if (index != -1) { if (index != -1) {
requestsForUser.removeAt(index) requestsForUser.removeAt(index)
@ -1186,7 +1182,7 @@ internal class DefaultVerificationService @Inject constructor(
CancelCode.User, CancelCode.User,
null // TODO handle error? null // TODO handle error?
) )
updatePendingRequest(existingRequest.copy(readyInfo = readyMsg)) updatePendingRequest(existingRequest.copy(readyInfo = readyMsg.asValidObject()))
return true return true
} else { } else {
Timber.e("## SAS readyPendingVerificationInDMs Verification not found") Timber.e("## SAS readyPendingVerificationInDMs Verification not found")
@ -1225,7 +1221,7 @@ internal class DefaultVerificationService @Inject constructor(
existingRequest.requestInfo?.fromDevice ?: "", existingRequest.requestInfo?.fromDevice ?: "",
null // TODO handle error? null // TODO handle error?
) )
updatePendingRequest(existingRequest.copy(readyInfo = readyMsg)) updatePendingRequest(existingRequest.copy(readyInfo = readyMsg.asValidObject()))
return true return true
} else { } else {
Timber.e("## SAS readyPendingVerification Verification not found") Timber.e("## SAS readyPendingVerification Verification not found")

View file

@ -15,12 +15,21 @@
*/ */
package im.vector.matrix.android.internal.crypto.verification package im.vector.matrix.android.internal.crypto.verification
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService
import im.vector.matrix.android.api.session.crypto.verification.VerificationTransaction import im.vector.matrix.android.api.session.crypto.verification.VerificationTransaction
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
import timber.log.Timber
/** /**
* Generic interactive key verification transaction * Generic interactive key verification transaction
*/ */
internal abstract class DefaultVerificationTransaction( internal abstract class DefaultVerificationTransaction(
private val setDeviceVerificationAction: SetDeviceVerificationAction,
private val crossSigningService: CrossSigningService,
private val userId: String,
override val transactionId: String, override val transactionId: String,
override val otherUserId: String, override val otherUserId: String,
override var otherDeviceId: String? = null, override var otherDeviceId: String? = null,
@ -42,5 +51,50 @@ internal abstract class DefaultVerificationTransaction(
listeners.remove(listener) listeners.remove(listener)
} }
abstract fun acceptVerificationEvent(senderId: String, info: VerificationInfo) protected fun trust(canTrustOtherUserMasterKey: Boolean,
toVerifyDeviceIds: List<String>,
eventuallyMarkMyMasterKeyAsTrusted: Boolean) {
// If not me sign his MSK and upload the signature
if (canTrustOtherUserMasterKey) {
// we should trust this master key
// And check verification MSK -> SSK?
if (otherUserId != userId) {
crossSigningService.trustUser(otherUserId, object : MatrixCallback<Unit> {
override fun onFailure(failure: Throwable) {
Timber.e(failure, "## Verification: Failed to trust User $otherUserId")
}
})
} else {
// Notice other master key is mine because other is me
if (eventuallyMarkMyMasterKeyAsTrusted) {
// Mark my keys as trusted locally
crossSigningService.markMyMasterKeyAsTrusted()
}
}
}
if (otherUserId == userId) {
// If me it's reasonable to sign and upload the device signature
// Notice that i might not have the private keys, so may not be able to do it
crossSigningService.trustDevice(otherDeviceId!!, object : MatrixCallback<Unit> {
override fun onFailure(failure: Throwable) {
Timber.w("## Verification: Failed to sign new device $otherDeviceId, ${failure.localizedMessage}")
}
})
}
// TODO what if the otherDevice is not in this list? and should we
toVerifyDeviceIds.forEach {
setDeviceVerified(otherUserId, it)
}
transport.done(transactionId)
state = VerificationTxState.Verified
}
private fun setDeviceVerified(userId: String, deviceId: String) {
// TODO should not override cross sign status
setDeviceVerificationAction.handle(DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true),
userId,
deviceId)
}
} }

View file

@ -16,7 +16,7 @@
package im.vector.matrix.android.internal.crypto.verification package im.vector.matrix.android.internal.crypto.verification
import android.os.Build import android.os.Build
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.extensions.orFalse
import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService
import im.vector.matrix.android.api.session.crypto.verification.CancelCode import im.vector.matrix.android.api.session.crypto.verification.CancelCode
import im.vector.matrix.android.api.session.crypto.verification.EmojiRepresentation import im.vector.matrix.android.api.session.crypto.verification.EmojiRepresentation
@ -25,7 +25,6 @@ import im.vector.matrix.android.api.session.crypto.verification.SasVerificationT
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel
import im.vector.matrix.android.internal.crypto.model.MXKey import im.vector.matrix.android.internal.crypto.model.MXKey
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.extensions.toUnsignedInt import im.vector.matrix.android.internal.extensions.toUnsignedInt
@ -38,17 +37,25 @@ import timber.log.Timber
* Represents an ongoing short code interactive key verification between two devices. * Represents an ongoing short code interactive key verification between two devices.
*/ */
internal abstract class SASDefaultVerificationTransaction( internal abstract class SASDefaultVerificationTransaction(
private val setDeviceVerificationAction: SetDeviceVerificationAction, setDeviceVerificationAction: SetDeviceVerificationAction,
open val userId: String, open val userId: String,
open val deviceId: String?, open val deviceId: String?,
private val cryptoStore: IMXCryptoStore, private val cryptoStore: IMXCryptoStore,
private val crossSigningService: CrossSigningService, crossSigningService: CrossSigningService,
private val deviceFingerprint: String, private val deviceFingerprint: String,
transactionId: String, transactionId: String,
otherUserId: String, otherUserId: String,
otherDeviceId: String?, otherDeviceId: String?,
isIncoming: Boolean isIncoming: Boolean
) : DefaultVerificationTransaction(transactionId, otherUserId, otherDeviceId, isIncoming), SasVerificationTransaction { ) : DefaultVerificationTransaction(
setDeviceVerificationAction,
crossSigningService,
userId,
transactionId,
otherUserId,
otherDeviceId,
isIncoming),
SasVerificationTransaction {
companion object { companion object {
const val SAS_MAC_SHA256_LONGKDF = "hmac-sha256" const val SAS_MAC_SHA256_LONGKDF = "hmac-sha256"
@ -89,15 +96,17 @@ internal abstract class SASDefaultVerificationTransaction(
private var olmSas: OlmSAS? = null private var olmSas: OlmSAS? = null
var startReq: VerificationInfoStart? = null // Visible for test
var accepted: VerificationInfoAccept? = null var startReq: ValidVerificationInfoStart.SasVerificationInfoStart? = null
var otherKey: String? = null // Visible for test
var shortCodeBytes: ByteArray? = null var accepted: ValidVerificationInfoAccept? = null
protected var otherKey: String? = null
protected var shortCodeBytes: ByteArray? = null
var myMac: VerificationInfoMac? = null protected var myMac: ValidVerificationInfoMac? = null
var theirMac: VerificationInfoMac? = null protected var theirMac: ValidVerificationInfoMac? = null
fun getSAS(): OlmSAS { protected fun getSAS(): OlmSAS {
if (olmSas == null) olmSas = OlmSAS() if (olmSas == null) olmSas = OlmSAS()
return olmSas!! return olmSas!!
} }
@ -177,7 +186,7 @@ internal abstract class SASDefaultVerificationTransaction(
} }
val macMsg = transport.createMac(transactionId, keyMap, keyStrings) val macMsg = transport.createMac(transactionId, keyMap, keyStrings)
myMac = macMsg myMac = macMsg.asValidObject()
state = VerificationTxState.SendingMac state = VerificationTxState.SendingMac
sendToOther(EventType.KEY_VERIFICATION_MAC, macMsg, VerificationTxState.MacSent, CancelCode.User) { sendToOther(EventType.KEY_VERIFICATION_MAC, macMsg, VerificationTxState.MacSent, CancelCode.User) {
if (state == VerificationTxState.SendingMac) { if (state == VerificationTxState.SendingMac) {
@ -187,9 +196,8 @@ internal abstract class SASDefaultVerificationTransaction(
} }
// Do I already have their Mac? // Do I already have their Mac?
if (theirMac != null) { theirMac?.let { verifyMacs(it) }
verifyMacs() // if not wait for it
} // if not wait for it
} }
override fun shortCodeDoesNotMatch() { override fun shortCodeDoesNotMatch() {
@ -201,27 +209,15 @@ internal abstract class SASDefaultVerificationTransaction(
return transport is VerificationTransportToDevice return transport is VerificationTransportToDevice
} }
override fun acceptVerificationEvent(senderId: String, info: VerificationInfo) { abstract fun onVerificationStart(startReq: ValidVerificationInfoStart.SasVerificationInfoStart)
when (info) {
is VerificationInfoStart -> onVerificationStart(info)
is VerificationInfoAccept -> onVerificationAccept(info)
is VerificationInfoKey -> onKeyVerificationKey(info)
is VerificationInfoMac -> onKeyVerificationMac(info)
else -> {
// nop
}
}
}
abstract fun onVerificationStart(startReq: VerificationInfoStart) abstract fun onVerificationAccept(accept: ValidVerificationInfoAccept)
abstract fun onVerificationAccept(accept: VerificationInfoAccept) abstract fun onKeyVerificationKey(vKey: ValidVerificationInfoKey)
abstract fun onKeyVerificationKey(vKey: VerificationInfoKey) abstract fun onKeyVerificationMac(vMac: ValidVerificationInfoMac)
abstract fun onKeyVerificationMac(vKey: VerificationInfoMac) protected fun verifyMacs(theirMacSafe: ValidVerificationInfoMac) {
protected fun verifyMacs() {
Timber.v("## SAS verifying macs for id:$transactionId") Timber.v("## SAS verifying macs for id:$transactionId")
state = VerificationTxState.Verifying state = VerificationTxState.Verifying
@ -232,16 +228,12 @@ internal abstract class SASDefaultVerificationTransaction(
// as well as the HMAC of the comma-separated, sorted list of the key IDs given in the message. // as well as the HMAC of the comma-separated, sorted list of the key IDs given in the message.
// Bobs device compares these with the HMAC values given in the m.key.verification.mac message. // Bobs device compares these with the HMAC values given in the m.key.verification.mac message.
// If everything matches, then consider Alices device keys as verified. // If everything matches, then consider Alices device keys as verified.
val baseInfo = "MATRIX_KEY_VERIFICATION_MAC$otherUserId$otherDeviceId$userId$deviceId$transactionId"
val baseInfo = "MATRIX_KEY_VERIFICATION_MAC" + val commaSeparatedListOfKeyIds = theirMacSafe.mac.keys.sorted().joinToString(",")
otherUserId + otherDeviceId +
userId + deviceId +
transactionId
val commaSeparatedListOfKeyIds = theirMac!!.mac!!.keys.sorted().joinToString(",")
val keyStrings = macUsingAgreedMethod(commaSeparatedListOfKeyIds, baseInfo + "KEY_IDS") val keyStrings = macUsingAgreedMethod(commaSeparatedListOfKeyIds, baseInfo + "KEY_IDS")
if (theirMac!!.keys != keyStrings) { if (theirMacSafe.keys != keyStrings) {
// WRONG! // WRONG!
cancel(CancelCode.MismatchedKeys) cancel(CancelCode.MismatchedKeys)
return return
@ -250,7 +242,7 @@ internal abstract class SASDefaultVerificationTransaction(
val verifiedDevices = ArrayList<String>() val verifiedDevices = ArrayList<String>()
// cannot be empty because it has been validated // cannot be empty because it has been validated
theirMac!!.mac!!.keys.forEach { theirMacSafe.mac.keys.forEach {
val keyIDNoPrefix = it.withoutPrefix("ed25519:") val keyIDNoPrefix = it.withoutPrefix("ed25519:")
val otherDeviceKey = otherUserKnownDevices?.get(keyIDNoPrefix)?.fingerprint() val otherDeviceKey = otherUserKnownDevices?.get(keyIDNoPrefix)?.fingerprint()
if (otherDeviceKey == null) { if (otherDeviceKey == null) {
@ -259,7 +251,7 @@ internal abstract class SASDefaultVerificationTransaction(
return@forEach return@forEach
} }
val mac = macUsingAgreedMethod(otherDeviceKey, baseInfo + it) val mac = macUsingAgreedMethod(otherDeviceKey, baseInfo + it)
if (mac != theirMac?.mac?.get(it)) { if (mac != theirMacSafe.mac[it]) {
// WRONG! // WRONG!
Timber.e("## SAS Verification: mac mismatch for $otherDeviceKey with id $keyIDNoPrefix") Timber.e("## SAS Verification: mac mismatch for $otherDeviceKey with id $keyIDNoPrefix")
cancel(CancelCode.MismatchedKeys) cancel(CancelCode.MismatchedKeys)
@ -273,12 +265,12 @@ internal abstract class SASDefaultVerificationTransaction(
val otherCrossSigningMasterKeyPublic = otherMasterKey?.unpaddedBase64PublicKey val otherCrossSigningMasterKeyPublic = otherMasterKey?.unpaddedBase64PublicKey
if (otherCrossSigningMasterKeyPublic != null) { if (otherCrossSigningMasterKeyPublic != null) {
// Did the user signed his master key // Did the user signed his master key
theirMac!!.mac!!.keys.forEach { theirMacSafe.mac.keys.forEach {
val keyIDNoPrefix = it.withoutPrefix("ed25519:") val keyIDNoPrefix = it.withoutPrefix("ed25519:")
if (keyIDNoPrefix == otherCrossSigningMasterKeyPublic) { if (keyIDNoPrefix == otherCrossSigningMasterKeyPublic) {
// Check the signature // Check the signature
val mac = macUsingAgreedMethod(otherCrossSigningMasterKeyPublic, baseInfo + it) val mac = macUsingAgreedMethod(otherCrossSigningMasterKeyPublic, baseInfo + it)
if (mac != theirMac?.mac?.get(it)) { if (mac != theirMacSafe.mac.get(it)) {
// WRONG! // WRONG!
Timber.e("## SAS Verification: mac mismatch for MasterKey with id $keyIDNoPrefix") Timber.e("## SAS Verification: mac mismatch for MasterKey with id $keyIDNoPrefix")
cancel(CancelCode.MismatchedKeys) cancel(CancelCode.MismatchedKeys)
@ -298,40 +290,9 @@ internal abstract class SASDefaultVerificationTransaction(
return return
} }
// If not me sign his MSK and upload the signature trust(otherMasterKeyIsVerified,
if (otherMasterKeyIsVerified && otherUserId != userId) { verifiedDevices,
// we should trust this master key eventuallyMarkMyMasterKeyAsTrusted = otherMasterKey?.trustLevel?.isVerified() == false)
// And check verification MSK -> SSK?
crossSigningService.trustUser(otherUserId, object : MatrixCallback<Unit> {
override fun onFailure(failure: Throwable) {
Timber.e(failure, "## SAS Verification: Failed to trust User $otherUserId")
}
})
}
if (otherUserId == userId) {
// If me it's reasonable to sign and upload the device signature
// Notice that i might not have the private keys, so may not be able to do it
crossSigningService.trustDevice(otherDeviceId!!, object : MatrixCallback<Unit> {
override fun onFailure(failure: Throwable) {
Timber.w(failure, "## SAS Verification: Failed to sign new device $otherDeviceId")
}
})
}
// TODO what if the otherDevice is not in this list? and should we
verifiedDevices.forEach {
setDeviceVerified(otherUserId, it)
}
transport.done(transactionId)
state = VerificationTxState.Verified
}
private fun setDeviceVerified(userId: String, deviceId: String) {
// TODO should not override cross sign status
setDeviceVerificationAction.handle(DeviceTrustLevel(false, true),
userId,
deviceId)
} }
override fun cancel() { override fun cancel() {
@ -343,8 +304,8 @@ internal abstract class SASDefaultVerificationTransaction(
transport.cancelTransaction(transactionId, otherUserId, otherDeviceId ?: "", code) transport.cancelTransaction(transactionId, otherUserId, otherDeviceId ?: "", code)
} }
protected fun sendToOther(type: String, protected fun <T> sendToOther(type: String,
keyToDevice: VerificationInfo, keyToDevice: VerificationInfo<T>,
nextState: VerificationTxState, nextState: VerificationTxState,
onErrorReason: CancelCode, onErrorReason: CancelCode,
onDone: (() -> Unit)?) { onDone: (() -> Unit)?) {
@ -369,11 +330,11 @@ internal abstract class SASDefaultVerificationTransaction(
} }
override fun supportsEmoji(): Boolean { override fun supportsEmoji(): Boolean {
return accepted?.shortAuthenticationStrings?.contains(SasMode.EMOJI) == true return accepted?.shortAuthenticationStrings?.contains(SasMode.EMOJI).orFalse()
} }
override fun supportsDecimal(): Boolean { override fun supportsDecimal(): Boolean {
return accepted?.shortAuthenticationStrings?.contains(SasMode.DECIMAL) == true return accepted?.shortAuthenticationStrings?.contains(SasMode.DECIMAL).orFalse()
} }
protected fun hashUsingAgreedHashMethod(toHash: String): String? { protected fun hashUsingAgreedHashMethod(toHash: String): String? {
@ -386,7 +347,7 @@ internal abstract class SASDefaultVerificationTransaction(
return null return null
} }
protected fun macUsingAgreedMethod(message: String, info: String): String? { private fun macUsingAgreedMethod(message: String, info: String): String? {
if (SAS_MAC_SHA256_LONGKDF.toLowerCase() == accepted?.messageAuthenticationCode?.toLowerCase()) { if (SAS_MAC_SHA256_LONGKDF.toLowerCase() == accepted?.messageAuthenticationCode?.toLowerCase()) {
return getSAS().calculateMacLongKdf(message, info) return getSAS().calculateMacLongKdf(message, info)
} else if (SAS_MAC_SHA256.toLowerCase() == accepted?.messageAuthenticationCode?.toLowerCase()) { } else if (SAS_MAC_SHA256.toLowerCase() == accepted?.messageAuthenticationCode?.toLowerCase()) {
@ -436,7 +397,7 @@ internal abstract class SASDefaultVerificationTransaction(
* For each group of 6 bits, look up the emoji from Appendix A corresponding * For each group of 6 bits, look up the emoji from Appendix A corresponding
* to that number 7 emoji are selected from a list of 64 emoji (see Appendix A) * to that number 7 emoji are selected from a list of 64 emoji (see Appendix A)
*/ */
fun getEmojiCodeRepresentation(byteArray: ByteArray): List<EmojiRepresentation> { private fun getEmojiCodeRepresentation(byteArray: ByteArray): List<EmojiRepresentation> {
val b0 = byteArray[0].toUnsignedInt() val b0 = byteArray[0].toUnsignedInt()
val b1 = byteArray[1].toUnsignedInt() val b1 = byteArray[1].toUnsignedInt()
val b2 = byteArray[2].toUnsignedInt() val b2 = byteArray[2].toUnsignedInt()

View file

@ -18,18 +18,16 @@ package im.vector.matrix.android.internal.crypto.verification
import im.vector.matrix.android.api.session.events.model.Content import im.vector.matrix.android.api.session.events.model.Content
import im.vector.matrix.android.internal.crypto.model.rest.SendToDeviceObject import im.vector.matrix.android.internal.crypto.model.rest.SendToDeviceObject
interface VerificationInfo { interface VerificationInfo<ValidObjectType> {
fun toEventContent(): Content? = null fun toEventContent(): Content? = null
fun toSendToDeviceObject(): SendToDeviceObject? = null fun toSendToDeviceObject(): SendToDeviceObject? = null
fun isValid(): Boolean
fun asValidObject(): ValidObjectType?
/** /**
* String to identify the transaction. * String to identify the transaction.
* This string must be unique for the pair of users performing verification for the duration that the transaction is valid. * This string must be unique for the pair of users performing verification for the duration that the transaction is valid.
* Alices device should record this ID and use it in future messages in this transaction. * Alices device should record this ID and use it in future messages in this transaction.
*/ */
val transactionID: String? val transactionId: String?
// TODO Refacto Put the relatesTo here or at least in Message sent in Room parent?
// val relatesTo: RelationDefaultContent?
} }

View file

@ -15,7 +15,7 @@
*/ */
package im.vector.matrix.android.internal.crypto.verification package im.vector.matrix.android.internal.crypto.verification
internal interface VerificationInfoAccept : VerificationInfo { internal interface VerificationInfoAccept : VerificationInfo<ValidVerificationInfoAccept> {
/** /**
* The key agreement protocol that Bobs device has selected to use, out of the list proposed by Alices device * The key agreement protocol that Bobs device has selected to use, out of the list proposed by Alices device
*/ */
@ -41,6 +41,24 @@ internal interface VerificationInfoAccept : VerificationInfo {
* and the canonical JSON representation of the m.key.verification.start message. * and the canonical JSON representation of the m.key.verification.start message.
*/ */
var commitment: String? var commitment: String?
override fun asValidObject(): ValidVerificationInfoAccept? {
val validTransactionId = transactionId?.takeIf { it.isNotEmpty() } ?: return null
val validKeyAgreementProtocol = keyAgreementProtocol?.takeIf { it.isNotEmpty() } ?: return null
val validHash = hash?.takeIf { it.isNotEmpty() } ?: return null
val validMessageAuthenticationCode = messageAuthenticationCode?.takeIf { it.isNotEmpty() } ?: return null
val validShortAuthenticationStrings = shortAuthenticationStrings?.takeIf { it.isNotEmpty() } ?: return null
val validCommitment = commitment?.takeIf { it.isNotEmpty() } ?: return null
return ValidVerificationInfoAccept(
validTransactionId,
validKeyAgreementProtocol,
validHash,
validMessageAuthenticationCode,
validShortAuthenticationStrings,
validCommitment
)
}
} }
internal interface VerificationInfoAcceptFactory { internal interface VerificationInfoAcceptFactory {
@ -52,3 +70,12 @@ internal interface VerificationInfoAcceptFactory {
messageAuthenticationCode: String, messageAuthenticationCode: String,
shortAuthenticationStrings: List<String>): VerificationInfoAccept shortAuthenticationStrings: List<String>): VerificationInfoAccept
} }
internal data class ValidVerificationInfoAccept(
val transactionId: String,
val keyAgreementProtocol: String,
val hash: String,
val messageAuthenticationCode: String,
val shortAuthenticationStrings: List<String>,
var commitment: String?
)

View file

@ -15,7 +15,7 @@
*/ */
package im.vector.matrix.android.internal.crypto.verification package im.vector.matrix.android.internal.crypto.verification
internal interface VerificationInfoCancel : VerificationInfo { internal interface VerificationInfoCancel : VerificationInfo<ValidVerificationInfoCancel> {
/** /**
* machine-readable reason for cancelling, see [CancelCode] * machine-readable reason for cancelling, see [CancelCode]
*/ */
@ -25,4 +25,21 @@ internal interface VerificationInfoCancel : VerificationInfo {
* human-readable reason for cancelling. This should only be used if the receiving client does not understand the code given. * human-readable reason for cancelling. This should only be used if the receiving client does not understand the code given.
*/ */
val reason: String? val reason: String?
override fun asValidObject(): ValidVerificationInfoCancel? {
val validTransactionId = transactionId?.takeIf { it.isNotEmpty() } ?: return null
val validCode = code?.takeIf { it.isNotEmpty() } ?: return null
return ValidVerificationInfoCancel(
validTransactionId,
validCode,
reason
)
} }
}
internal data class ValidVerificationInfoCancel(
val transactionId: String,
val code: String,
val reason: String?
)

View file

@ -15,4 +15,14 @@
*/ */
package im.vector.matrix.android.internal.crypto.verification package im.vector.matrix.android.internal.crypto.verification
interface VerificationInfoDone : VerificationInfo internal interface VerificationInfoDone : VerificationInfo<ValidVerificationInfoDone> {
override fun asValidObject(): ValidVerificationInfoDone? {
if (transactionId.isNullOrEmpty()) {
return null
}
return ValidVerificationInfoDone
}
}
internal object ValidVerificationInfoDone

View file

@ -18,13 +18,28 @@ package im.vector.matrix.android.internal.crypto.verification
/** /**
* Sent by both devices to send their ephemeral Curve25519 public key to the other device. * Sent by both devices to send their ephemeral Curve25519 public key to the other device.
*/ */
internal interface VerificationInfoKey : VerificationInfo { internal interface VerificationInfoKey : VerificationInfo<ValidVerificationInfoKey> {
/** /**
* The devices ephemeral public key, as an unpadded base64 string * The devices ephemeral public key, as an unpadded base64 string
*/ */
val key: String? val key: String?
override fun asValidObject(): ValidVerificationInfoKey? {
val validTransactionId = transactionId?.takeIf { it.isNotEmpty() } ?: return null
val validKey = key?.takeIf { it.isNotEmpty() } ?: return null
return ValidVerificationInfoKey(
validTransactionId,
validKey
)
}
} }
internal interface VerificationInfoKeyFactory { internal interface VerificationInfoKeyFactory {
fun create(tid: String, pubKey: String): VerificationInfoKey fun create(tid: String, pubKey: String): VerificationInfoKey
} }
internal data class ValidVerificationInfoKey(
val transactionId: String,
val key: String
)

View file

@ -15,7 +15,7 @@
*/ */
package im.vector.matrix.android.internal.crypto.verification package im.vector.matrix.android.internal.crypto.verification
internal interface VerificationInfoMac : VerificationInfo { internal interface VerificationInfoMac : VerificationInfo<ValidVerificationInfoMac> {
/** /**
* A map of key ID to the MAC of the key, as an unpadded base64 string, calculated using the MAC key * A map of key ID to the MAC of the key, as an unpadded base64 string, calculated using the MAC key
*/ */
@ -28,8 +28,26 @@ internal interface VerificationInfoMac : VerificationInfo {
* give the MAC of the string ed25519:ABCDEFG,ed25519:HIJKLMN. * give the MAC of the string ed25519:ABCDEFG,ed25519:HIJKLMN.
*/ */
val keys: String? val keys: String?
override fun asValidObject(): ValidVerificationInfoMac? {
val validTransactionId = transactionId?.takeIf { it.isNotEmpty() } ?: return null
val validMac = mac?.takeIf { it.isNotEmpty() } ?: return null
val validKeys = keys?.takeIf { it.isNotEmpty() } ?: return null
return ValidVerificationInfoMac(
validTransactionId,
validMac,
validKeys
)
}
} }
internal interface VerificationInfoMacFactory { internal interface VerificationInfoMacFactory {
fun create(tid: String, mac: Map<String, String>, keys: String): VerificationInfoMac fun create(tid: String, mac: Map<String, String>, keys: String): VerificationInfoMac
} }
internal data class ValidVerificationInfoMac(
val transactionId: String,
val mac: Map<String, String>,
val keys: String
)

View file

@ -15,6 +15,8 @@
*/ */
package im.vector.matrix.android.internal.crypto.verification package im.vector.matrix.android.internal.crypto.verification
import im.vector.matrix.android.api.session.crypto.verification.ValidVerificationInfoReady
/** /**
* A new event type is added to the key verification framework: m.key.verification.ready, * A new event type is added to the key verification framework: m.key.verification.ready,
* which may be sent by the target of the m.key.verification.request message, upon receipt of the m.key.verification.request event. * which may be sent by the target of the m.key.verification.request message, upon receipt of the m.key.verification.request event.
@ -23,7 +25,7 @@ package im.vector.matrix.android.internal.crypto.verification
* with a m.key.verification.start event instead. * with a m.key.verification.start event instead.
*/ */
interface VerificationInfoReady : VerificationInfo { internal interface VerificationInfoReady : VerificationInfo<ValidVerificationInfoReady> {
/** /**
* The ID of the device that sent the m.key.verification.ready message * The ID of the device that sent the m.key.verification.ready message
*/ */
@ -33,6 +35,18 @@ interface VerificationInfoReady : VerificationInfo {
* An array of verification methods that the device supports * An array of verification methods that the device supports
*/ */
val methods: List<String>? val methods: List<String>?
override fun asValidObject(): ValidVerificationInfoReady? {
val validTransactionId = transactionId?.takeIf { it.isNotEmpty() } ?: return null
val validFromDevice = fromDevice?.takeIf { it.isNotEmpty() } ?: return null
val validMethods = methods?.takeIf { it.isNotEmpty() } ?: return null
return ValidVerificationInfoReady(
validTransactionId,
validFromDevice,
validMethods
)
}
} }
internal interface MessageVerificationReadyFactory { internal interface MessageVerificationReadyFactory {

View file

@ -15,7 +15,9 @@
*/ */
package im.vector.matrix.android.internal.crypto.verification package im.vector.matrix.android.internal.crypto.verification
interface VerificationInfoRequest : VerificationInfo { import im.vector.matrix.android.api.session.crypto.verification.ValidVerificationInfoRequest
internal interface VerificationInfoRequest : VerificationInfo<ValidVerificationInfoRequest> {
/** /**
* Required. The device ID which is initiating the request. * Required. The device ID which is initiating the request.
@ -33,4 +35,18 @@ interface VerificationInfoRequest : VerificationInfo {
* the message should be ignored by the receiver. * the message should be ignored by the receiver.
*/ */
val timestamp: Long? val timestamp: Long?
override fun asValidObject(): ValidVerificationInfoRequest? {
// FIXME No check on Timestamp?
val validTransactionId = transactionId?.takeIf { it.isNotEmpty() } ?: return null
val validFromDevice = fromDevice?.takeIf { it.isNotEmpty() } ?: return null
val validMethods = methods?.takeIf { it.isNotEmpty() } ?: return null
return ValidVerificationInfoRequest(
validTransactionId,
validFromDevice,
validMethods,
timestamp
)
}
} }

View file

@ -15,7 +15,11 @@
*/ */
package im.vector.matrix.android.internal.crypto.verification package im.vector.matrix.android.internal.crypto.verification
internal interface VerificationInfoStart : VerificationInfo { import im.vector.matrix.android.api.session.crypto.verification.SasMode
import im.vector.matrix.android.internal.crypto.model.rest.VERIFICATION_METHOD_RECIPROCATE
import im.vector.matrix.android.internal.crypto.model.rest.VERIFICATION_METHOD_SAS
internal interface VerificationInfoStart : VerificationInfo<ValidVerificationInfoStart> {
val method: String? val method: String?
@ -57,5 +61,64 @@ internal interface VerificationInfoStart : VerificationInfo {
*/ */
val sharedSecret: String? val sharedSecret: String?
fun toCanonicalJson(): String? fun toCanonicalJson(): String
override fun asValidObject(): ValidVerificationInfoStart? {
val validTransactionId = transactionId?.takeIf { it.isNotEmpty() } ?: return null
val validFromDevice = fromDevice?.takeIf { it.isNotEmpty() } ?: return null
return when (method) {
VERIFICATION_METHOD_SAS -> {
val validKeyAgreementProtocols = keyAgreementProtocols?.takeIf { it.isNotEmpty() } ?: return null
val validHashes = hashes?.takeIf { it.contains("sha256") } ?: return null
val validMessageAuthenticationCodes = messageAuthenticationCodes
?.takeIf {
it.contains(SASDefaultVerificationTransaction.SAS_MAC_SHA256)
|| it.contains(SASDefaultVerificationTransaction.SAS_MAC_SHA256_LONGKDF)
}
?: return null
val validShortAuthenticationStrings = shortAuthenticationStrings?.takeIf { it.contains(SasMode.DECIMAL) } ?: return null
ValidVerificationInfoStart.SasVerificationInfoStart(
validTransactionId,
validFromDevice,
validKeyAgreementProtocols,
validHashes,
validMessageAuthenticationCodes,
validShortAuthenticationStrings,
canonicalJson = toCanonicalJson()
)
}
VERIFICATION_METHOD_RECIPROCATE -> {
val validSharedSecret = sharedSecret?.takeIf { it.isNotEmpty() } ?: return null
ValidVerificationInfoStart.ReciprocateVerificationInfoStart(
validTransactionId,
validFromDevice,
validSharedSecret
)
}
else -> null
}
}
}
sealed class ValidVerificationInfoStart(
open val transactionId: String,
open val fromDevice: String) {
data class SasVerificationInfoStart(
override val transactionId: String,
override val fromDevice: String,
val keyAgreementProtocols: List<String>,
val hashes: List<String>,
val messageAuthenticationCodes: List<String>,
val shortAuthenticationStrings: List<String>,
val canonicalJson: String
) : ValidVerificationInfoStart(transactionId, fromDevice)
data class ReciprocateVerificationInfoStart(
override val transactionId: String,
override val fromDevice: String,
val sharedSecret: String
) : ValidVerificationInfoStart(transactionId, fromDevice)
} }

View file

@ -15,6 +15,7 @@
*/ */
package im.vector.matrix.android.internal.crypto.verification package im.vector.matrix.android.internal.crypto.verification
import im.vector.matrix.android.api.session.crypto.verification.ValidVerificationInfoRequest
import im.vector.matrix.android.api.session.crypto.verification.CancelCode import im.vector.matrix.android.api.session.crypto.verification.CancelCode
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
@ -27,18 +28,18 @@ internal interface VerificationTransport {
/** /**
* Sends a message * Sends a message
*/ */
fun sendToOther(type: String, fun <T> sendToOther(type: String,
verificationInfo: VerificationInfo, verificationInfo: VerificationInfo<T>,
nextState: VerificationTxState, nextState: VerificationTxState,
onErrorReason: CancelCode, onErrorReason: CancelCode,
onDone: (() -> Unit)?) onDone: (() -> Unit)?)
fun sendVerificationRequest(supportedMethods: List<String>, fun sendVerificationRequest(supportedMethods: List<String>,
localID: String, localId: String,
otherUserId: String, otherUserId: String,
roomId: String?, roomId: String?,
toDevices: List<String>?, toDevices: List<String>?,
callback: (String?, VerificationInfoRequest?) -> Unit) callback: (String?, ValidVerificationInfoRequest?) -> Unit)
fun cancelTransaction(transactionId: String, fun cancelTransaction(transactionId: String,
otherUserId: String, otherUserId: String,
@ -64,7 +65,7 @@ internal interface VerificationTransport {
* Create start for SAS verification * Create start for SAS verification
*/ */
fun createStartForSas(fromDevice: String, fun createStartForSas(fromDevice: String,
transactionID: String, transactionId: String,
keyAgreementProtocols: List<String>, keyAgreementProtocols: List<String>,
hashes: List<String>, hashes: List<String>,
messageAuthenticationCodes: List<String>, messageAuthenticationCodes: List<String>,
@ -74,7 +75,7 @@ internal interface VerificationTransport {
* Create start for QR code verification * Create start for QR code verification
*/ */
fun createStartForQrCode(fromDevice: String, fun createStartForQrCode(fromDevice: String,
transactionID: String, transactionId: String,
sharedSecret: String): VerificationInfoStart sharedSecret: String): VerificationInfoStart
fun createMac(tid: String, mac: Map<String, String>, keys: String): VerificationInfoMac fun createMac(tid: String, mac: Map<String, String>, keys: String): VerificationInfoMac

View file

@ -21,8 +21,8 @@ import androidx.work.Data
import androidx.work.ExistingWorkPolicy import androidx.work.ExistingWorkPolicy
import androidx.work.Operation import androidx.work.Operation
import androidx.work.WorkInfo import androidx.work.WorkInfo
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.R import im.vector.matrix.android.R
import im.vector.matrix.android.api.session.crypto.verification.ValidVerificationInfoRequest
import im.vector.matrix.android.api.session.crypto.verification.CancelCode import im.vector.matrix.android.api.session.crypto.verification.CancelCode
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
import im.vector.matrix.android.api.session.events.model.Content import im.vector.matrix.android.api.session.events.model.Content
@ -65,13 +65,12 @@ internal class VerificationTransportRoomMessage(
private val userId: String, private val userId: String,
private val userDeviceId: String?, private val userDeviceId: String?,
private val roomId: String, private val roomId: String,
private val monarchy: Monarchy,
private val localEchoEventFactory: LocalEchoEventFactory, private val localEchoEventFactory: LocalEchoEventFactory,
private val tx: DefaultVerificationTransaction? private val tx: DefaultVerificationTransaction?
) : VerificationTransport { ) : VerificationTransport {
override fun sendToOther(type: String, override fun <T> sendToOther(type: String,
verificationInfo: VerificationInfo, verificationInfo: VerificationInfo<T>,
nextState: VerificationTxState, nextState: VerificationTxState,
onErrorReason: CancelCode, onErrorReason: CancelCode,
onDone: (() -> Unit)?) { onDone: (() -> Unit)?) {
@ -138,26 +137,33 @@ internal class VerificationTransportRoomMessage(
} }
override fun sendVerificationRequest(supportedMethods: List<String>, override fun sendVerificationRequest(supportedMethods: List<String>,
localID: String, localId: String,
otherUserId: String, otherUserId: String,
roomId: String?, roomId: String?,
toDevices: List<String>?, toDevices: List<String>?,
callback: (String?, VerificationInfoRequest?) -> Unit) { callback: (String?, ValidVerificationInfoRequest?) -> Unit) {
Timber.d("## SAS sending verification request with supported methods: $supportedMethods") Timber.d("## SAS sending verification request with supported methods: $supportedMethods")
// This transport requires a room // This transport requires a room
requireNotNull(roomId) requireNotNull(roomId)
val validInfo = ValidVerificationInfoRequest(
transactionId = "",
fromDevice = userDeviceId ?: "",
methods = supportedMethods,
timestamp = System.currentTimeMillis()
)
val info = MessageVerificationRequestContent( val info = MessageVerificationRequestContent(
body = stringProvider.getString(R.string.key_verification_request_fallback_message, userId), body = stringProvider.getString(R.string.key_verification_request_fallback_message, userId),
fromDevice = userDeviceId ?: "", fromDevice = validInfo.fromDevice,
toUserId = otherUserId, toUserId = otherUserId,
timestamp = System.currentTimeMillis(), timestamp = validInfo.timestamp,
methods = supportedMethods methods = validInfo.methods
) )
val content = info.toContent() val content = info.toContent()
val event = createEventAndLocalEcho( val event = createEventAndLocalEcho(
localID, localId,
EventType.MESSAGE, EventType.MESSAGE,
roomId, roomId,
content content
@ -192,8 +198,8 @@ internal class VerificationTransportRoomMessage(
?.let { wInfo -> ?.let { wInfo ->
if (wInfo.outputData.getBoolean("failed", false)) { if (wInfo.outputData.getBoolean("failed", false)) {
callback(null, null) callback(null, null)
} else if (wInfo.outputData.getString(localID) != null) { } else if (wInfo.outputData.getString(localId) != null) {
callback(wInfo.outputData.getString(localID), info) callback(wInfo.outputData.getString(localId), validInfo)
} else { } else {
callback(null, null) callback(null, null)
} }
@ -272,7 +278,7 @@ internal class VerificationTransportRoomMessage(
override fun createMac(tid: String, mac: Map<String, String>, keys: String) = MessageVerificationMacContent.create(tid, mac, keys) override fun createMac(tid: String, mac: Map<String, String>, keys: String) = MessageVerificationMacContent.create(tid, mac, keys)
override fun createStartForSas(fromDevice: String, override fun createStartForSas(fromDevice: String,
transactionID: String, transactionId: String,
keyAgreementProtocols: List<String>, keyAgreementProtocols: List<String>,
hashes: List<String>, hashes: List<String>,
messageAuthenticationCodes: List<String>, messageAuthenticationCodes: List<String>,
@ -286,14 +292,14 @@ internal class VerificationTransportRoomMessage(
VERIFICATION_METHOD_SAS, VERIFICATION_METHOD_SAS,
RelationDefaultContent( RelationDefaultContent(
type = RelationType.REFERENCE, type = RelationType.REFERENCE,
eventId = transactionID eventId = transactionId
), ),
null null
) )
} }
override fun createStartForQrCode(fromDevice: String, override fun createStartForQrCode(fromDevice: String,
transactionID: String, transactionId: String,
sharedSecret: String): VerificationInfoStart { sharedSecret: String): VerificationInfoStart {
return MessageVerificationStartContent( return MessageVerificationStartContent(
fromDevice, fromDevice,
@ -304,7 +310,7 @@ internal class VerificationTransportRoomMessage(
VERIFICATION_METHOD_RECIPROCATE, VERIFICATION_METHOD_RECIPROCATE,
RelationDefaultContent( RelationDefaultContent(
type = RelationType.REFERENCE, type = RelationType.REFERENCE,
eventId = transactionID eventId = transactionId
), ),
sharedSecret sharedSecret
) )
@ -321,15 +327,15 @@ internal class VerificationTransportRoomMessage(
) )
} }
private fun createEventAndLocalEcho(localID: String = LocalEcho.createLocalEchoId(), type: String, roomId: String, content: Content): Event { private fun createEventAndLocalEcho(localId: String = LocalEcho.createLocalEchoId(), type: String, roomId: String, content: Content): Event {
return Event( return Event(
roomId = roomId, roomId = roomId,
originServerTs = System.currentTimeMillis(), originServerTs = System.currentTimeMillis(),
senderId = userId, senderId = userId,
eventId = localID, eventId = localId,
type = type, type = type,
content = content, content = content,
unsignedData = UnsignedData(age = null, transactionId = localID) unsignedData = UnsignedData(age = null, transactionId = localId)
).also { ).also {
localEchoEventFactory.createLocalEcho(it) localEchoEventFactory.createLocalEcho(it)
} }
@ -347,7 +353,6 @@ internal class VerificationTransportRoomMessage(
internal class VerificationTransportRoomMessageFactory @Inject constructor( internal class VerificationTransportRoomMessageFactory @Inject constructor(
private val workManagerProvider: WorkManagerProvider, private val workManagerProvider: WorkManagerProvider,
private val stringProvider: StringProvider, private val stringProvider: StringProvider,
private val monarchy: Monarchy,
@SessionId @SessionId
private val sessionId: String, private val sessionId: String,
@UserId @UserId
@ -357,6 +362,6 @@ internal class VerificationTransportRoomMessageFactory @Inject constructor(
private val localEchoEventFactory: LocalEchoEventFactory) { private val localEchoEventFactory: LocalEchoEventFactory) {
fun createTransport(roomId: String, tx: DefaultVerificationTransaction?): VerificationTransportRoomMessage { fun createTransport(roomId: String, tx: DefaultVerificationTransaction?): VerificationTransportRoomMessage {
return VerificationTransportRoomMessage(workManagerProvider, stringProvider, sessionId, userId, deviceId, roomId, monarchy, localEchoEventFactory, tx) return VerificationTransportRoomMessage(workManagerProvider, stringProvider, sessionId, userId, deviceId, roomId, localEchoEventFactory, tx)
} }
} }

View file

@ -16,6 +16,7 @@
package im.vector.matrix.android.internal.crypto.verification package im.vector.matrix.android.internal.crypto.verification
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.crypto.verification.ValidVerificationInfoRequest
import im.vector.matrix.android.api.session.crypto.verification.CancelCode import im.vector.matrix.android.api.session.crypto.verification.CancelCode
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.EventType
@ -46,28 +47,34 @@ internal class VerificationTransportToDevice(
) : VerificationTransport { ) : VerificationTransport {
override fun sendVerificationRequest(supportedMethods: List<String>, override fun sendVerificationRequest(supportedMethods: List<String>,
localID: String, localId: String,
otherUserId: String, otherUserId: String,
roomId: String?, roomId: String?,
toDevices: List<String>?, toDevices: List<String>?,
callback: (String?, VerificationInfoRequest?) -> Unit) { callback: (String?, ValidVerificationInfoRequest?) -> Unit) {
Timber.d("## SAS sending verification request with supported methods: $supportedMethods") Timber.d("## SAS sending verification request with supported methods: $supportedMethods")
val contentMap = MXUsersDevicesMap<Any>() val contentMap = MXUsersDevicesMap<Any>()
val keyReq = KeyVerificationRequest( val validKeyReq = ValidVerificationInfoRequest(
fromDevice = myDeviceId, transactionId = localId,
fromDevice = myDeviceId ?: "",
methods = supportedMethods, methods = supportedMethods,
timestamp = System.currentTimeMillis(), timestamp = System.currentTimeMillis()
transactionID = localID )
val keyReq = KeyVerificationRequest(
fromDevice = validKeyReq.fromDevice,
methods = validKeyReq.methods,
timestamp = validKeyReq.timestamp,
transactionId = validKeyReq.transactionId
) )
toDevices?.forEach { toDevices?.forEach {
contentMap.setObject(otherUserId, it, keyReq) contentMap.setObject(otherUserId, it, keyReq)
} }
sendToDeviceTask sendToDeviceTask
.configureWith(SendToDeviceTask.Params(MessageType.MSGTYPE_VERIFICATION_REQUEST, contentMap, localID)) { .configureWith(SendToDeviceTask.Params(MessageType.MSGTYPE_VERIFICATION_REQUEST, contentMap, localId)) {
this.callback = object : MatrixCallback<Unit> { this.callback = object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) { override fun onSuccess(data: Unit) {
Timber.v("## verification [$tx.transactionId] send toDevice request success") Timber.v("## verification [$tx.transactionId] send toDevice request success")
callback.invoke(localID, keyReq) callback.invoke(localId, validKeyReq)
} }
override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {
@ -103,8 +110,8 @@ internal class VerificationTransportToDevice(
.executeBy(taskExecutor) .executeBy(taskExecutor)
} }
override fun sendToOther(type: String, override fun <T> sendToOther(type: String,
verificationInfo: VerificationInfo, verificationInfo: VerificationInfo<T>,
nextState: VerificationTxState, nextState: VerificationTxState,
onErrorReason: CancelCode, onErrorReason: CancelCode,
onDone: (() -> Unit)?) { onDone: (() -> Unit)?) {
@ -197,7 +204,7 @@ internal class VerificationTransportToDevice(
override fun createMac(tid: String, mac: Map<String, String>, keys: String) = KeyVerificationMac.create(tid, mac, keys) override fun createMac(tid: String, mac: Map<String, String>, keys: String) = KeyVerificationMac.create(tid, mac, keys)
override fun createStartForSas(fromDevice: String, override fun createStartForSas(fromDevice: String,
transactionID: String, transactionId: String,
keyAgreementProtocols: List<String>, keyAgreementProtocols: List<String>,
hashes: List<String>, hashes: List<String>,
messageAuthenticationCodes: List<String>, messageAuthenticationCodes: List<String>,
@ -205,7 +212,7 @@ internal class VerificationTransportToDevice(
return KeyVerificationStart( return KeyVerificationStart(
fromDevice, fromDevice,
VERIFICATION_METHOD_SAS, VERIFICATION_METHOD_SAS,
transactionID, transactionId,
keyAgreementProtocols, keyAgreementProtocols,
hashes, hashes,
messageAuthenticationCodes, messageAuthenticationCodes,
@ -214,12 +221,12 @@ internal class VerificationTransportToDevice(
} }
override fun createStartForQrCode(fromDevice: String, override fun createStartForQrCode(fromDevice: String,
transactionID: String, transactionId: String,
sharedSecret: String): VerificationInfoStart { sharedSecret: String): VerificationInfoStart {
return KeyVerificationStart( return KeyVerificationStart(
fromDevice, fromDevice,
VERIFICATION_METHOD_RECIPROCATE, VERIFICATION_METHOD_RECIPROCATE,
transactionID, transactionId,
null, null,
null, null,
null, null,
@ -229,7 +236,7 @@ internal class VerificationTransportToDevice(
override fun createReady(tid: String, fromDevice: String, methods: List<String>): VerificationInfoReady { override fun createReady(tid: String, fromDevice: String, methods: List<String>): VerificationInfoReady {
return KeyVerificationReady( return KeyVerificationReady(
transactionID = tid, transactionId = tid,
fromDevice = fromDevice, fromDevice = fromDevice,
methods = methods methods = methods
) )

View file

@ -16,23 +16,22 @@
package im.vector.matrix.android.internal.crypto.verification.qrcode package im.vector.matrix.android.internal.crypto.verification.qrcode
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService
import im.vector.matrix.android.api.session.crypto.verification.CancelCode import im.vector.matrix.android.api.session.crypto.verification.CancelCode
import im.vector.matrix.android.api.session.crypto.verification.QrCodeVerificationTransaction import im.vector.matrix.android.api.session.crypto.verification.QrCodeVerificationTransaction
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustLevel import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64
import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64Safe
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.crypto.verification.DefaultVerificationTransaction import im.vector.matrix.android.internal.crypto.verification.DefaultVerificationTransaction
import im.vector.matrix.android.internal.crypto.verification.VerificationInfo import im.vector.matrix.android.internal.crypto.verification.ValidVerificationInfoStart
import im.vector.matrix.android.internal.crypto.verification.VerificationInfoStart
import im.vector.matrix.android.internal.util.exhaustive import im.vector.matrix.android.internal.util.exhaustive
import timber.log.Timber import timber.log.Timber
internal class DefaultQrCodeVerificationTransaction( internal class DefaultQrCodeVerificationTransaction(
private val setDeviceVerificationAction: SetDeviceVerificationAction, setDeviceVerificationAction: SetDeviceVerificationAction,
override val transactionId: String, override val transactionId: String,
override val otherUserId: String, override val otherUserId: String,
override var otherDeviceId: String?, override var otherDeviceId: String?,
@ -43,7 +42,15 @@ internal class DefaultQrCodeVerificationTransaction(
val userId: String, val userId: String,
val deviceId: String, val deviceId: String,
override val isIncoming: Boolean override val isIncoming: Boolean
) : DefaultVerificationTransaction(transactionId, otherUserId, otherDeviceId, isIncoming), QrCodeVerificationTransaction { ) : DefaultVerificationTransaction(
setDeviceVerificationAction,
crossSigningService,
userId,
transactionId,
otherUserId,
otherDeviceId,
isIncoming),
QrCodeVerificationTransaction {
override val qrCodeText: String? override val qrCodeText: String?
get() = qrCodeData?.toEncodedString() get() = qrCodeData?.toEncodedString()
@ -76,72 +83,95 @@ internal class DefaultQrCodeVerificationTransaction(
} }
// check master key // check master key
val myMasterKey = crossSigningService.getUserCrossSigningKeys(userId)?.masterKey()?.unpaddedBase64PublicKey
var canTrustOtherUserMasterKey = false
// Check the other device view of my MSK
when (otherQrCodeData) { when (otherQrCodeData) {
is QrCodeData.VerifyingAnotherUser -> { is QrCodeData.VerifyingAnotherUser -> {
if (otherQrCodeData.otherUserMasterCrossSigningPublicKey // key2 (aka otherUserMasterCrossSigningPublicKey) is what the one displaying the QR code (other user) think my MSK is.
!= crossSigningService.getUserCrossSigningKeys(userId)?.masterKey()?.unpaddedBase64PublicKey) { // Let's check that it's correct
// If not -> Cancel
if (otherQrCodeData.otherUserMasterCrossSigningPublicKey != myMasterKey) {
Timber.d("## Verification QR: Invalid other master key ${otherQrCodeData.otherUserMasterCrossSigningPublicKey}") Timber.d("## Verification QR: Invalid other master key ${otherQrCodeData.otherUserMasterCrossSigningPublicKey}")
cancel(CancelCode.MismatchedKeys) cancel(CancelCode.MismatchedKeys)
return return
} else Unit } else Unit
} }
is QrCodeData.SelfVerifyingMasterKeyTrusted -> { is QrCodeData.SelfVerifyingMasterKeyTrusted -> {
if (otherQrCodeData.userMasterCrossSigningPublicKey // key1 (aka userMasterCrossSigningPublicKey) is the session displaying the QR code view of our MSK.
!= crossSigningService.getUserCrossSigningKeys(userId)?.masterKey()?.unpaddedBase64PublicKey) { // Let's check that I see the same MSK
// If not -> Cancel
if (otherQrCodeData.userMasterCrossSigningPublicKey != myMasterKey) {
Timber.d("## Verification QR: Invalid other master key ${otherQrCodeData.userMasterCrossSigningPublicKey}") Timber.d("## Verification QR: Invalid other master key ${otherQrCodeData.userMasterCrossSigningPublicKey}")
cancel(CancelCode.MismatchedKeys) cancel(CancelCode.MismatchedKeys)
return return
} else Unit } else {
// I can trust the MSK then (i see the same one, and other session tell me it's trusted by him)
canTrustOtherUserMasterKey = true
}
} }
is QrCodeData.SelfVerifyingMasterKeyNotTrusted -> { is QrCodeData.SelfVerifyingMasterKeyNotTrusted -> {
if (otherQrCodeData.userMasterCrossSigningPublicKey // key2 (aka userMasterCrossSigningPublicKey) is the session displaying the QR code view of our MSK.
!= crossSigningService.getUserCrossSigningKeys(userId)?.masterKey()?.unpaddedBase64PublicKey) { // Let's check that it's the good one
// If not -> Cancel
if (otherQrCodeData.userMasterCrossSigningPublicKey != myMasterKey) {
Timber.d("## Verification QR: Invalid other master key ${otherQrCodeData.userMasterCrossSigningPublicKey}") Timber.d("## Verification QR: Invalid other master key ${otherQrCodeData.userMasterCrossSigningPublicKey}")
cancel(CancelCode.MismatchedKeys) cancel(CancelCode.MismatchedKeys)
return return
} else Unit } else {
// Nothing special here, we will send a reciprocate start event, and then the other session will trust it's view of the MSK
}
} }
}.exhaustive }.exhaustive
val toVerifyDeviceIds = mutableListOf<String>() val toVerifyDeviceIds = mutableListOf<String>()
var canTrustOtherUserMasterKey = false
// Check device key if available // Let's now check the other user/device key material
when (otherQrCodeData) { when (otherQrCodeData) {
is QrCodeData.VerifyingAnotherUser -> { is QrCodeData.VerifyingAnotherUser -> {
// key1(aka userMasterCrossSigningPublicKey) is the MSK of the one displaying the QR code (i.e other user)
// Let's check that it matches what I think it should be
if (otherQrCodeData.userMasterCrossSigningPublicKey if (otherQrCodeData.userMasterCrossSigningPublicKey
!= crossSigningService.getUserCrossSigningKeys(otherUserId)?.masterKey()?.unpaddedBase64PublicKey) { != crossSigningService.getUserCrossSigningKeys(otherUserId)?.masterKey()?.unpaddedBase64PublicKey) {
Timber.d("## Verification QR: Invalid user master key ${otherQrCodeData.userMasterCrossSigningPublicKey}") Timber.d("## Verification QR: Invalid user master key ${otherQrCodeData.userMasterCrossSigningPublicKey}")
cancel(CancelCode.MismatchedKeys) cancel(CancelCode.MismatchedKeys)
return return
} else { } else {
// It does so i should mark it as trusted
canTrustOtherUserMasterKey = true canTrustOtherUserMasterKey = true
Unit Unit
} }
} }
is QrCodeData.SelfVerifyingMasterKeyTrusted -> { is QrCodeData.SelfVerifyingMasterKeyTrusted -> {
// key2 (aka otherDeviceKey) is my current device key in POV of the one displaying the QR code (i.e other device)
// Let's check that it's correct
if (otherQrCodeData.otherDeviceKey if (otherQrCodeData.otherDeviceKey
!= cryptoStore.getUserDevice(userId, deviceId)?.fingerprint()) { != cryptoStore.getUserDevice(userId, deviceId)?.fingerprint()) {
Timber.d("## Verification QR: Invalid other device key ${otherQrCodeData.otherDeviceKey}") Timber.d("## Verification QR: Invalid other device key ${otherQrCodeData.otherDeviceKey}")
cancel(CancelCode.MismatchedKeys) cancel(CancelCode.MismatchedKeys)
return return
} else Unit } else Unit // Nothing special here, we will send a reciprocate start event, and then the other session will trust my device
// and thus allow me to request SSSS secret
} }
is QrCodeData.SelfVerifyingMasterKeyNotTrusted -> { is QrCodeData.SelfVerifyingMasterKeyNotTrusted -> {
// key1 (aka otherDeviceKey) is the device key of the one displaying the QR code (i.e other device)
// Let's check that it matches what I have locally
if (otherQrCodeData.deviceKey if (otherQrCodeData.deviceKey
!= cryptoStore.getUserDevice(otherUserId, otherDeviceId ?: "")?.fingerprint()) { != cryptoStore.getUserDevice(otherUserId, otherDeviceId ?: "")?.fingerprint()) {
Timber.d("## Verification QR: Invalid device key ${otherQrCodeData.deviceKey}") Timber.d("## Verification QR: Invalid device key ${otherQrCodeData.deviceKey}")
cancel(CancelCode.MismatchedKeys) cancel(CancelCode.MismatchedKeys)
return return
} else { } else {
toVerifyDeviceIds.add(otherQrCodeData.deviceKey) // Yes it does -> i should trust it and sign then upload the signature
toVerifyDeviceIds.add(otherDeviceId ?: "")
Unit Unit
} }
} }
}.exhaustive }.exhaustive
if (!canTrustOtherUserMasterKey && toVerifyDeviceIds.isEmpty()) { if (!canTrustOtherUserMasterKey && toVerifyDeviceIds.isEmpty()) {
// Nothing to verify // // Nothing to verify
cancel(CancelCode.MismatchedKeys) cancel(CancelCode.MismatchedKeys)
return return
} }
@ -152,10 +182,12 @@ internal class DefaultQrCodeVerificationTransaction(
start(otherQrCodeData.sharedSecret) start(otherQrCodeData.sharedSecret)
// Trust the other user // Trust the other user
trust(canTrustOtherUserMasterKey, toVerifyDeviceIds.distinct()) trust(canTrustOtherUserMasterKey,
toVerifyDeviceIds.distinct(),
eventuallyMarkMyMasterKeyAsTrusted = true)
} }
fun start(remoteSecret: String) { private fun start(remoteSecret: String) {
if (state != VerificationTxState.None) { if (state != VerificationTxState.None) {
Timber.e("## Verification QR: start verification from invalid state") Timber.e("## Verification QR: start verification from invalid state")
// should I cancel?? // should I cancel??
@ -177,9 +209,6 @@ internal class DefaultQrCodeVerificationTransaction(
) )
} }
override fun acceptVerificationEvent(senderId: String, info: VerificationInfo) {
}
override fun cancel() { override fun cancel() {
cancel(CancelCode.User) cancel(CancelCode.User)
} }
@ -192,14 +221,14 @@ internal class DefaultQrCodeVerificationTransaction(
override fun isToDeviceTransport() = false override fun isToDeviceTransport() = false
// Other user has scanned our QR code. check that the secret matched, so we can trust him // Other user has scanned our QR code. check that the secret matched, so we can trust him
fun onStartReceived(startReq: VerificationInfoStart) { fun onStartReceived(startReq: ValidVerificationInfoStart.ReciprocateVerificationInfoStart) {
if (qrCodeData == null) { if (qrCodeData == null) {
// Should not happen // Should not happen
cancel(CancelCode.UnexpectedMessage) cancel(CancelCode.UnexpectedMessage)
return return
} }
if (startReq.sharedSecret == qrCodeData.sharedSecret) { if (startReq.sharedSecret.fromBase64Safe()?.contentEquals(qrCodeData.sharedSecret.fromBase64()) == true) {
// Ok, we can trust the other user // Ok, we can trust the other user
// We can only trust the master key in this case // We can only trust the master key in this case
// But first, ask the user for a confirmation // But first, ask the user for a confirmation
@ -211,7 +240,21 @@ internal class DefaultQrCodeVerificationTransaction(
} }
override fun otherUserScannedMyQrCode() { override fun otherUserScannedMyQrCode() {
trust(true, emptyList()) when (qrCodeData) {
is QrCodeData.VerifyingAnotherUser -> {
// Alice telling Bob that the code was scanned successfully is sufficient for Bob to trust Alice's key,
trust(true, emptyList(), false)
}
is QrCodeData.SelfVerifyingMasterKeyTrusted -> {
// I now know that I have the correct device key for other session,
// and can sign it with the self-signing key and upload the signature
trust(false, listOf(otherDeviceId ?: ""), false)
}
is QrCodeData.SelfVerifyingMasterKeyNotTrusted -> {
// I now know that i can trust my MSK
trust(true, emptyList(), true)
}
}
} }
override fun otherUserDidNotScannedMyQrCode() { override fun otherUserDidNotScannedMyQrCode() {
@ -219,46 +262,4 @@ internal class DefaultQrCodeVerificationTransaction(
// At least remove the transaction... // At least remove the transaction...
state = VerificationTxState.Cancelled(CancelCode.MismatchedKeys, true) state = VerificationTxState.Cancelled(CancelCode.MismatchedKeys, true)
} }
private fun trust(canTrustOtherUserMasterKey: Boolean, toVerifyDeviceIds: List<String>) {
// If not me sign his MSK and upload the signature
if (canTrustOtherUserMasterKey) {
if (otherUserId != userId) {
// we should trust this master key
// And check verification MSK -> SSK?
crossSigningService.trustUser(otherUserId, object : MatrixCallback<Unit> {
override fun onFailure(failure: Throwable) {
Timber.e(failure, "## QR Verification: Failed to trust User $otherUserId")
}
})
} else {
// Mark my keys as trusted locally
crossSigningService.markMyMasterKeyAsTrusted()
}
}
if (otherUserId == userId) {
// If me it's reasonable to sign and upload the device signature
// Notice that i might not have the private keys, so may not be able to do it
crossSigningService.trustDevice(otherDeviceId!!, object : MatrixCallback<Unit> {
override fun onFailure(failure: Throwable) {
Timber.w(failure, "## QR Verification: Failed to sign new device $otherDeviceId")
}
})
}
// TODO what if the otherDevice is not in this list? and should we
toVerifyDeviceIds.forEach {
setDeviceVerified(otherUserId, it)
}
transport.done(transactionId)
state = VerificationTxState.Verified
}
private fun setDeviceVerified(userId: String, deviceId: String) {
// TODO should not override cross sign status
setDeviceVerificationAction.handle(DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true),
userId,
deviceId)
}
} }

View file

@ -16,7 +16,7 @@
package im.vector.matrix.android.internal.crypto.verification.qrcode package im.vector.matrix.android.internal.crypto.verification.qrcode
import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64NoPadding import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64
import im.vector.matrix.android.internal.crypto.crosssigning.toBase64NoPadding import im.vector.matrix.android.internal.crypto.crosssigning.toBase64NoPadding
import im.vector.matrix.android.internal.extensions.toUnsignedInt import im.vector.matrix.android.internal.extensions.toUnsignedInt
@ -52,15 +52,15 @@ fun QrCodeData.toEncodedString(): String {
} }
// Keys // Keys
firstKey.fromBase64NoPadding().forEach { firstKey.fromBase64().forEach {
result += it result += it
} }
secondKey.fromBase64NoPadding().forEach { secondKey.fromBase64().forEach {
result += it result += it
} }
// Secret // Secret
sharedSecret.fromBase64NoPadding().forEach { sharedSecret.fromBase64().forEach {
result += it result += it
} }
@ -94,11 +94,11 @@ fun String.toQrCodeData(): QrCodeData? {
val mode = byteArray[cursor].toInt() val mode = byteArray[cursor].toInt()
cursor++ cursor++
// Get transaction length // Get transaction length, Big Endian format
val bigEndian1 = byteArray[cursor].toUnsignedInt() val msb = byteArray[cursor].toUnsignedInt()
val bigEndian2 = byteArray[cursor + 1].toUnsignedInt() val lsb = byteArray[cursor + 1].toUnsignedInt()
val transactionLength = bigEndian1 * 0x0100 + bigEndian2 val transactionLength = msb.shl(8) + lsb
cursor++ cursor++
cursor++ cursor++

View file

@ -25,3 +25,7 @@ annotation class SessionFilesDirectory
@Qualifier @Qualifier
@Retention(AnnotationRetention.RUNTIME) @Retention(AnnotationRetention.RUNTIME)
annotation class SessionCacheDirectory annotation class SessionCacheDirectory
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class CacheDirectory

View file

@ -32,6 +32,7 @@ import im.vector.matrix.android.internal.util.BackgroundDetectionObserver
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import org.matrix.olm.OlmManager import org.matrix.olm.OlmManager
import java.io.File
@Component(modules = [MatrixModule::class, NetworkModule::class, AuthModule::class]) @Component(modules = [MatrixModule::class, NetworkModule::class, AuthModule::class])
@MatrixScope @MatrixScope
@ -52,6 +53,9 @@ internal interface MatrixComponent {
fun resources(): Resources fun resources(): Resources
@CacheDirectory
fun cacheDir(): File
fun olmManager(): OlmManager fun olmManager(): OlmManager
fun taskExecutor(): TaskExecutor fun taskExecutor(): TaskExecutor

View file

@ -26,6 +26,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.android.asCoroutineDispatcher import kotlinx.coroutines.android.asCoroutineDispatcher
import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.asCoroutineDispatcher
import org.matrix.olm.OlmManager import org.matrix.olm.OlmManager
import java.io.File
import java.util.concurrent.Executors import java.util.concurrent.Executors
@Module @Module
@ -49,6 +50,13 @@ internal object MatrixModule {
return context.resources return context.resources
} }
@JvmStatic
@Provides
@CacheDirectory
fun providesCacheDir(context: Context): File {
return context.cacheDir
}
@JvmStatic @JvmStatic
@Provides @Provides
@MatrixScope @MatrixScope

View file

@ -24,11 +24,11 @@ import im.vector.matrix.android.api.session.file.FileService
import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt
import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments
import im.vector.matrix.android.internal.di.CacheDirectory
import im.vector.matrix.android.internal.di.SessionCacheDirectory import im.vector.matrix.android.internal.di.SessionCacheDirectory
import im.vector.matrix.android.internal.di.Unauthenticated import im.vector.matrix.android.internal.di.Unauthenticated
import im.vector.matrix.android.internal.extensions.foldToCallback import im.vector.matrix.android.internal.extensions.foldToCallback
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import im.vector.matrix.android.internal.util.md5
import im.vector.matrix.android.internal.util.toCancelable import im.vector.matrix.android.internal.util.toCancelable
import im.vector.matrix.android.internal.util.writeToFile import im.vector.matrix.android.internal.util.writeToFile
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
@ -42,8 +42,10 @@ import java.io.IOException
import javax.inject.Inject import javax.inject.Inject
internal class DefaultFileService @Inject constructor( internal class DefaultFileService @Inject constructor(
@SessionCacheDirectory @CacheDirectory
private val cacheDirectory: File, private val cacheDirectory: File,
@SessionCacheDirectory
private val sessionCacheDirectory: File,
private val contentUrlResolver: ContentUrlResolver, private val contentUrlResolver: ContentUrlResolver,
@Unauthenticated @Unauthenticated
private val okHttpClient: OkHttpClient, private val okHttpClient: OkHttpClient,
@ -62,13 +64,14 @@ internal class DefaultFileService @Inject constructor(
return GlobalScope.launch(coroutineDispatchers.main) { return GlobalScope.launch(coroutineDispatchers.main) {
withContext(coroutineDispatchers.io) { withContext(coroutineDispatchers.io) {
Try { Try {
val folder = getFolder(downloadMode, id) val folder = File(sessionCacheDirectory, "MF")
if (!folder.exists()) {
folder.mkdirs()
}
File(folder, fileName) File(folder, fileName)
}.flatMap { destFile -> }.flatMap { destFile ->
if (!destFile.exists() || downloadMode == FileService.DownloadMode.TO_EXPORT) { if (!destFile.exists()) {
Try { val resolvedUrl = contentUrlResolver.resolveFullSize(url) ?: return@flatMap Try.Failure(IllegalArgumentException("url is null"))
val resolvedUrl = contentUrlResolver.resolveFullSize(url) ?: throw IllegalArgumentException("url is null")
val request = Request.Builder() val request = Request.Builder()
.url(resolvedUrl) .url(resolvedUrl)
@ -77,45 +80,34 @@ internal class DefaultFileService @Inject constructor(
val response = okHttpClient.newCall(request).execute() val response = okHttpClient.newCall(request).execute()
var inputStream = response.body?.byteStream() var inputStream = response.body?.byteStream()
Timber.v("Response size ${response.body?.contentLength()} - Stream available: ${inputStream?.available()}") Timber.v("Response size ${response.body?.contentLength()} - Stream available: ${inputStream?.available()}")
if (!response.isSuccessful if (!response.isSuccessful || inputStream == null) {
|| inputStream == null) { return@flatMap Try.Failure(IOException())
throw IOException()
} }
if (elementToDecrypt != null) { if (elementToDecrypt != null) {
Timber.v("## decrypt file") Timber.v("## decrypt file")
inputStream = MXEncryptedAttachments.decryptAttachment(inputStream, elementToDecrypt) inputStream = MXEncryptedAttachments.decryptAttachment(inputStream, elementToDecrypt)
?: throw IllegalStateException("Decryption error") ?: return@flatMap Try.Failure(IllegalStateException("Decryption error"))
} }
writeToFile(inputStream, destFile) writeToFile(inputStream, destFile)
destFile
}
} else {
Try.just(destFile)
} }
Try.just(copyFile(destFile, downloadMode))
} }
} }
.foldToCallback(callback) .foldToCallback(callback)
}.toCancelable() }.toCancelable()
} }
private fun getFolder(downloadMode: FileService.DownloadMode, id: String): File { private fun copyFile(file: File, downloadMode: FileService.DownloadMode): File {
return when (downloadMode) { return when (downloadMode) {
FileService.DownloadMode.FOR_INTERNAL_USE -> { FileService.DownloadMode.TO_EXPORT ->
// Create dir tree (MF stands for Matrix File): file.copyTo(File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), file.name), true)
// <cache>/<sessionId>/MF/<md5(id)>/ FileService.DownloadMode.FOR_EXTERNAL_SHARE ->
val tmpFolderSession = File(cacheDirectory, "MF") file.copyTo(File(File(cacheDirectory, "ext_share"), file.name), true)
File(tmpFolderSession, id.md5()) FileService.DownloadMode.FOR_INTERNAL_USE ->
} file
FileService.DownloadMode.TO_EXPORT -> {
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
}
}
.also { folder ->
if (!folder.exists()) {
folder.mkdirs()
}
} }
} }
} }

View file

@ -20,6 +20,7 @@ import android.os.Handler
import android.os.Looper import android.os.Looper
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.session.SessionScope
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@SessionScope @SessionScope
@ -33,7 +34,13 @@ internal class DefaultContentUploadStateTracker @Inject constructor() : ContentU
val listeners = listeners.getOrPut(key) { ArrayList() } val listeners = listeners.getOrPut(key) { ArrayList() }
listeners.add(updateListener) listeners.add(updateListener)
val currentState = states[key] ?: ContentUploadStateTracker.State.Idle val currentState = states[key] ?: ContentUploadStateTracker.State.Idle
mainHandler.post { updateListener.onUpdate(currentState) } mainHandler.post {
try {
updateListener.onUpdate(currentState)
} catch (e: Exception) {
Timber.e(e, "## ContentUploadStateTracker.onUpdate() failed")
}
}
} }
override fun untrack(key: String, updateListener: ContentUploadStateTracker.UpdateListener) { override fun untrack(key: String, updateListener: ContentUploadStateTracker.UpdateListener) {
@ -79,7 +86,13 @@ internal class DefaultContentUploadStateTracker @Inject constructor() : ContentU
private fun updateState(key: String, state: ContentUploadStateTracker.State) { private fun updateState(key: String, state: ContentUploadStateTracker.State) {
states[key] = state states[key] = state
mainHandler.post { mainHandler.post {
listeners[key]?.forEach { it.onUpdate(state) } listeners[key]?.forEach {
try {
it.onUpdate(state)
} catch (e: Exception) {
Timber.e(e, "## ContentUploadStateTracker.onUpdate() failed")
}
}
} }
} }
} }

View file

@ -58,7 +58,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
override val sessionId: String, override val sessionId: String,
val events: List<Event>, val events: List<Event>,
val attachment: ContentAttachmentData, val attachment: ContentAttachmentData,
val isRoomEncrypted: Boolean, val isEncrypted: Boolean,
val compressBeforeSending: Boolean, val compressBeforeSending: Boolean,
override val lastFailureMessage: String? = null override val lastFailureMessage: String? = null
) : SessionWorkerParams ) : SessionWorkerParams
@ -90,9 +90,11 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
Timber.e(e) Timber.e(e)
notifyTracker(params) { contentUploadStateTracker.setFailure(it, e) } notifyTracker(params) { contentUploadStateTracker.setFailure(it, e) }
return Result.success( return Result.success(
WorkerParamsFactory.toData(params.copy( WorkerParamsFactory.toData(
params.copy(
lastFailureMessage = e.localizedMessage lastFailureMessage = e.localizedMessage
)) )
)
) )
} }
.let { originalFile -> .let { originalFile ->
@ -136,7 +138,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
} }
try { try {
val contentUploadResponse = if (params.isRoomEncrypted) { val contentUploadResponse = if (params.isEncrypted) {
Timber.v("Encrypt thumbnail") Timber.v("Encrypt thumbnail")
notifyTracker(params) { contentUploadStateTracker.setEncryptingThumbnail(it) } notifyTracker(params) { contentUploadStateTracker.setEncryptingThumbnail(it) }
val encryptionResult = MXEncryptedAttachments.encryptAttachment(ByteArrayInputStream(thumbnailData.bytes), thumbnailData.mimeType) val encryptionResult = MXEncryptedAttachments.encryptAttachment(ByteArrayInputStream(thumbnailData.bytes), thumbnailData.mimeType)
@ -174,18 +176,18 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
var uploadedFileEncryptedFileInfo: EncryptedFileInfo? = null var uploadedFileEncryptedFileInfo: EncryptedFileInfo? = null
return try { return try {
val contentUploadResponse = if (params.isRoomEncrypted) { val contentUploadResponse = if (params.isEncrypted) {
Timber.v("Encrypt file") Timber.v("Encrypt file")
notifyTracker(params) { contentUploadStateTracker.setEncrypting(it) } notifyTracker(params) { contentUploadStateTracker.setEncrypting(it) }
val encryptionResult = MXEncryptedAttachments.encryptAttachment(FileInputStream(attachmentFile), attachment.mimeType) val encryptionResult = MXEncryptedAttachments.encryptAttachment(FileInputStream(attachmentFile), attachment.getSafeMimeType())
uploadedFileEncryptedFileInfo = encryptionResult.encryptedFileInfo uploadedFileEncryptedFileInfo = encryptionResult.encryptedFileInfo
fileUploader fileUploader
.uploadByteArray(encryptionResult.encryptedByteArray, attachment.name, "application/octet-stream", progressListener) .uploadByteArray(encryptionResult.encryptedByteArray, attachment.name, "application/octet-stream", progressListener)
} else { } else {
fileUploader fileUploader
.uploadFile(attachmentFile, attachment.name, attachment.mimeType, progressListener) .uploadFile(attachmentFile, attachment.name, attachment.getSafeMimeType(), progressListener)
} }
handleSuccess(params, handleSuccess(params,
@ -226,7 +228,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
updateEvent(it, attachmentUrl, encryptedFileInfo, thumbnailUrl, thumbnailEncryptedFileInfo, newImageAttributes) updateEvent(it, attachmentUrl, encryptedFileInfo, thumbnailUrl, thumbnailEncryptedFileInfo, newImageAttributes)
} }
val sendParams = MultipleEventSendingDispatcherWorker.Params(params.sessionId, updatedEvents, params.isRoomEncrypted) val sendParams = MultipleEventSendingDispatcherWorker.Params(params.sessionId, updatedEvents, params.isEncrypted)
return Result.success(WorkerParamsFactory.toData(sendParams)) return Result.success(WorkerParamsFactory.toData(sendParams))
} }

View file

@ -22,6 +22,7 @@ import im.vector.matrix.android.api.pushrules.RuleKind
import im.vector.matrix.android.api.pushrules.RuleSetKey import im.vector.matrix.android.api.pushrules.RuleSetKey
import im.vector.matrix.android.api.pushrules.getActions import im.vector.matrix.android.api.pushrules.getActions
import im.vector.matrix.android.api.pushrules.rest.PushRule import im.vector.matrix.android.api.pushrules.rest.PushRule
import im.vector.matrix.android.api.pushrules.rest.RuleSet
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.database.mapper.PushRulesMapper import im.vector.matrix.android.internal.database.mapper.PushRulesMapper
@ -31,6 +32,7 @@ import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.session.pushers.AddPushRuleTask import im.vector.matrix.android.internal.session.pushers.AddPushRuleTask
import im.vector.matrix.android.internal.session.pushers.GetPushRulesTask import im.vector.matrix.android.internal.session.pushers.GetPushRulesTask
import im.vector.matrix.android.internal.session.pushers.RemovePushRuleTask import im.vector.matrix.android.internal.session.pushers.RemovePushRuleTask
import im.vector.matrix.android.internal.session.pushers.UpdatePushRuleActionsTask
import im.vector.matrix.android.internal.session.pushers.UpdatePushRuleEnableStatusTask import im.vector.matrix.android.internal.session.pushers.UpdatePushRuleEnableStatusTask
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.task.configureWith
@ -42,6 +44,7 @@ internal class DefaultPushRuleService @Inject constructor(
private val getPushRulesTask: GetPushRulesTask, private val getPushRulesTask: GetPushRulesTask,
private val updatePushRuleEnableStatusTask: UpdatePushRuleEnableStatusTask, private val updatePushRuleEnableStatusTask: UpdatePushRuleEnableStatusTask,
private val addPushRuleTask: AddPushRuleTask, private val addPushRuleTask: AddPushRuleTask,
private val updatePushRuleActionsTask: UpdatePushRuleActionsTask,
private val removePushRuleTask: RemovePushRuleTask, private val removePushRuleTask: RemovePushRuleTask,
private val taskExecutor: TaskExecutor, private val taskExecutor: TaskExecutor,
private val monarchy: Monarchy private val monarchy: Monarchy
@ -55,7 +58,7 @@ internal class DefaultPushRuleService @Inject constructor(
.executeBy(taskExecutor) .executeBy(taskExecutor)
} }
override fun getPushRules(scope: String): List<PushRule> { override fun getPushRules(scope: String): RuleSet {
var contentRules: List<PushRule> = emptyList() var contentRules: List<PushRule> = emptyList()
var overrideRules: List<PushRule> = emptyList() var overrideRules: List<PushRule> = emptyList()
var roomRules: List<PushRule> = emptyList() var roomRules: List<PushRule> = emptyList()
@ -90,8 +93,13 @@ internal class DefaultPushRuleService @Inject constructor(
} }
} }
// Ref. for the order: https://matrix.org/docs/spec/client_server/latest#push-rules return RuleSet(
return overrideRules + contentRules + roomRules + senderRules + underrideRules content = contentRules,
override = overrideRules,
room = roomRules,
sender = senderRules,
underride = underrideRules
)
} }
override fun updatePushRuleEnableStatus(kind: RuleKind, pushRule: PushRule, enabled: Boolean, callback: MatrixCallback<Unit>): Cancelable { override fun updatePushRuleEnableStatus(kind: RuleKind, pushRule: PushRule, enabled: Boolean, callback: MatrixCallback<Unit>): Cancelable {
@ -111,6 +119,14 @@ internal class DefaultPushRuleService @Inject constructor(
.executeBy(taskExecutor) .executeBy(taskExecutor)
} }
override fun updatePushRuleActions(kind: RuleKind, oldPushRule: PushRule, newPushRule: PushRule, callback: MatrixCallback<Unit>): Cancelable {
return updatePushRuleActionsTask
.configureWith(UpdatePushRuleActionsTask.Params(kind, oldPushRule, newPushRule)) {
this.callback = callback
}
.executeBy(taskExecutor)
}
override fun removePushRule(kind: RuleKind, pushRule: PushRule, callback: MatrixCallback<Unit>): Cancelable { override fun removePushRule(kind: RuleKind, pushRule: PushRule, callback: MatrixCallback<Unit>): Cancelable {
return removePushRuleTask return removePushRuleTask
.configureWith(RemovePushRuleTask.Params(kind, pushRule)) { .configureWith(RemovePushRuleTask.Params(kind, pushRule)) {

View file

@ -40,24 +40,35 @@ import im.vector.matrix.android.internal.di.SerializeNulls
* } * }
* </code> * </code>
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class JsonPusher( internal data class JsonPusher(
/** /**
* Required. This is a unique identifier for this pusher. See /set for more detail. Max length, 512 bytes. * Required. This is a unique identifier for this pusher. The value you should use for this is the routing or
* destination address information for the notification, for example, the APNS token for APNS or the
* Registration ID for GCM. If your notification client has no such concept, use any unique identifier.
* Max length, 512 bytes.
*
* If the kind is "email", this is the email address to send notifications to.
*/ */
@Json(name = "pushkey") @Json(name = "pushkey")
val pushKey: String, val pushKey: String,
/** /**
* Required. The kind of pusher. "http" is a pusher that sends HTTP pokes. * Required. The kind of pusher to configure.
* "http" makes a pusher that sends HTTP pokes.
* "email" makes a pusher that emails the user with unread notifications.
* null deletes the pusher.
*/ */
@SerializeNulls @SerializeNulls
@Json(name = "kind") @Json(name = "kind")
val kind: String?, val kind: String?,
/** /**
* Required. This is a reverse-DNS style identifier for the application. Max length, 64 chars. * Required. This is a reverse-DNS style identifier for the application. It is recommended that this end
* with the platform, such that different platform versions get different app identifiers.
* Max length, 64 chars.
*
* If the kind is "email", this is "m.email".
*/ */
@Json(name = "app_id") @Json(name = "app_id")
val appId: String, val appId: String,
@ -88,15 +99,17 @@ internal data class JsonPusher(
/** /**
* Required. A dictionary of information for the pusher implementation itself. * Required. A dictionary of information for the pusher implementation itself.
* If kind is http, this should contain url which is the URL to use to send notifications to.
*/ */
@Json(name = "data") @Json(name = "data")
val data: JsonPusherData? = null, val data: JsonPusherData? = null,
// Only used to update add Pusher (body of api request) /**
// Used If true, the homeserver should add another pusher with the given pushkey and App ID in addition * If true, the homeserver should add another pusher with the given pushkey and App ID in addition to any others
// to any others with different user IDs. * with different user IDs. Otherwise, the homeserver must remove any other pushers with the same App ID and pushkey
// Otherwise, the homeserver must remove any other pushers with the same App ID and pushkey for different users. * for different users.
// The default is false. * The default is false.
*/
@Json(name = "append") @Json(name = "append")
val append: Boolean? = false val append: Boolean? = false
) )

View file

@ -22,12 +22,14 @@ import com.squareup.moshi.JsonClass
internal data class JsonPusherData( internal data class JsonPusherData(
/** /**
* Required if kind is http. The URL to use to send notifications to. * Required if kind is http. The URL to use to send notifications to.
* MUST be an HTTPS URL with a path of /_matrix/push/v1/notify.
*/ */
@Json(name = "url") @Json(name = "url")
val url: String? = null, val url: String? = null,
/** /**
* The format to use when sending notifications to the Push Gateway. * The format to send notifications in to Push Gateways if the kind is http.
* Currently the only format available is 'event_id_only'.
*/ */
@Json(name = "format") @Json(name = "format")
val format: String? = null val format: String? = null

View file

@ -47,6 +47,7 @@ internal interface PushRulesApi {
/** /**
* Update the ruleID action * Update the ruleID action
* Ref: https://matrix.org/docs/spec/client_server/latest#put-matrix-client-r0-pushrules-scope-kind-ruleid-actions
* *
* @param kind the notification kind (sender, room...) * @param kind the notification kind (sender, room...)
* @param ruleId the ruleId * @param ruleId the ruleId

View file

@ -72,6 +72,9 @@ internal abstract class PushersModule {
@Binds @Binds
abstract fun bindAddPushRuleTask(task: DefaultAddPushRuleTask): AddPushRuleTask abstract fun bindAddPushRuleTask(task: DefaultAddPushRuleTask): AddPushRuleTask
@Binds
abstract fun bindUpdatePushRuleActionTask(task: DefaultUpdatePushRuleActionsTask): UpdatePushRuleActionsTask
@Binds @Binds
abstract fun bindRemovePushRuleTask(task: DefaultRemovePushRuleTask): RemovePushRuleTask abstract fun bindRemovePushRuleTask(task: DefaultRemovePushRuleTask): RemovePushRuleTask

View file

@ -0,0 +1,55 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.session.pushers
import im.vector.matrix.android.api.pushrules.RuleKind
import im.vector.matrix.android.api.pushrules.rest.PushRule
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.task.Task
import org.greenrobot.eventbus.EventBus
import javax.inject.Inject
internal interface UpdatePushRuleActionsTask : Task<UpdatePushRuleActionsTask.Params, Unit> {
data class Params(
val kind: RuleKind,
val oldPushRule: PushRule,
val newPushRule: PushRule
)
}
internal class DefaultUpdatePushRuleActionsTask @Inject constructor(
private val pushRulesApi: PushRulesApi,
private val eventBus: EventBus
) : UpdatePushRuleActionsTask {
override suspend fun execute(params: UpdatePushRuleActionsTask.Params) {
if (params.oldPushRule.enabled != params.newPushRule.enabled) {
// First change enabled state
executeRequest<Unit>(eventBus) {
apiCall = pushRulesApi.updateEnableRuleStatus(params.kind.value, params.newPushRule.ruleId, params.newPushRule.enabled)
}
}
if (params.newPushRule.enabled) {
// Also ensure the actions are up to date
val body = mapOf("actions" to params.newPushRule.actions)
executeRequest<Unit>(eventBus) {
apiCall = pushRulesApi.updateRuleActions(params.kind.value, params.newPushRule.ruleId, body)
}
}
}
}

View file

@ -71,7 +71,7 @@ import javax.inject.Inject
* (the transaction ID), this id is used when receiving an event from a sync to check if this event * (the transaction ID), this id is used when receiving an event from a sync to check if this event
* is matching an existing local echo. * is matching an existing local echo.
* *
* The transactionID is used as loc * The transactionId is used as loc
*/ */
internal class LocalEchoEventFactory @Inject constructor( internal class LocalEchoEventFactory @Inject constructor(
@UserId private val userId: String, @UserId private val userId: String,
@ -261,7 +261,7 @@ internal class LocalEchoEventFactory @Inject constructor(
msgType = MessageType.MSGTYPE_IMAGE, msgType = MessageType.MSGTYPE_IMAGE,
body = attachment.name ?: "image", body = attachment.name ?: "image",
info = ImageInfo( info = ImageInfo(
mimeType = attachment.mimeType, mimeType = attachment.getSafeMimeType(),
width = width?.toInt() ?: 0, width = width?.toInt() ?: 0,
height = height?.toInt() ?: 0, height = height?.toInt() ?: 0,
size = attachment.size.toInt() size = attachment.size.toInt()
@ -293,7 +293,7 @@ internal class LocalEchoEventFactory @Inject constructor(
msgType = MessageType.MSGTYPE_VIDEO, msgType = MessageType.MSGTYPE_VIDEO,
body = attachment.name ?: "video", body = attachment.name ?: "video",
videoInfo = VideoInfo( videoInfo = VideoInfo(
mimeType = attachment.mimeType, mimeType = attachment.getSafeMimeType(),
width = width, width = width,
height = height, height = height,
size = attachment.size, size = attachment.size,
@ -312,7 +312,7 @@ internal class LocalEchoEventFactory @Inject constructor(
msgType = MessageType.MSGTYPE_AUDIO, msgType = MessageType.MSGTYPE_AUDIO,
body = attachment.name ?: "audio", body = attachment.name ?: "audio",
audioInfo = AudioInfo( audioInfo = AudioInfo(
mimeType = attachment.mimeType?.takeIf { it.isNotBlank() } ?: "audio/mpeg", mimeType = attachment.getSafeMimeType()?.takeIf { it.isNotBlank() } ?: "audio/mpeg",
size = attachment.size size = attachment.size
), ),
url = attachment.path url = attachment.path
@ -325,7 +325,7 @@ internal class LocalEchoEventFactory @Inject constructor(
msgType = MessageType.MSGTYPE_FILE, msgType = MessageType.MSGTYPE_FILE,
body = attachment.name ?: "file", body = attachment.name ?: "file",
info = FileInfo( info = FileInfo(
mimeType = attachment.mimeType?.takeIf { it.isNotBlank() } mimeType = attachment.getSafeMimeType()?.takeIf { it.isNotBlank() }
?: "application/octet-stream", ?: "application/octet-stream",
size = attachment.size size = attachment.size
), ),
@ -335,25 +335,25 @@ internal class LocalEchoEventFactory @Inject constructor(
} }
private fun createEvent(roomId: String, content: Any? = null): Event { private fun createEvent(roomId: String, content: Any? = null): Event {
val localID = LocalEcho.createLocalEchoId() val localId = LocalEcho.createLocalEchoId()
return Event( return Event(
roomId = roomId, roomId = roomId,
originServerTs = dummyOriginServerTs(), originServerTs = dummyOriginServerTs(),
senderId = userId, senderId = userId,
eventId = localID, eventId = localId,
type = EventType.MESSAGE, type = EventType.MESSAGE,
content = content.toContent(), content = content.toContent(),
unsignedData = UnsignedData(age = null, transactionId = localID) unsignedData = UnsignedData(age = null, transactionId = localId)
) )
} }
fun createVerificationRequest(roomId: String, fromDevice: String, toUserId: String, methods: List<String>): Event { fun createVerificationRequest(roomId: String, fromDevice: String, toUserId: String, methods: List<String>): Event {
val localID = LocalEcho.createLocalEchoId() val localId = LocalEcho.createLocalEchoId()
return Event( return Event(
roomId = roomId, roomId = roomId,
originServerTs = dummyOriginServerTs(), originServerTs = dummyOriginServerTs(),
senderId = userId, senderId = userId,
eventId = localID, eventId = localId,
type = EventType.MESSAGE, type = EventType.MESSAGE,
content = MessageVerificationRequestContent( content = MessageVerificationRequestContent(
body = stringProvider.getString(R.string.key_verification_request_fallback_message, userId), body = stringProvider.getString(R.string.key_verification_request_fallback_message, userId),
@ -362,7 +362,7 @@ internal class LocalEchoEventFactory @Inject constructor(
timestamp = System.currentTimeMillis(), timestamp = System.currentTimeMillis(),
methods = methods methods = methods
).toContent(), ).toContent(),
unsignedData = UnsignedData(age = null, transactionId = localID) unsignedData = UnsignedData(age = null, transactionId = localId)
) )
} }
@ -469,16 +469,16 @@ internal class LocalEchoEventFactory @Inject constructor(
} }
*/ */
fun createRedactEvent(roomId: String, eventId: String, reason: String?): Event { fun createRedactEvent(roomId: String, eventId: String, reason: String?): Event {
val localID = LocalEcho.createLocalEchoId() val localId = LocalEcho.createLocalEchoId()
return Event( return Event(
roomId = roomId, roomId = roomId,
originServerTs = dummyOriginServerTs(), originServerTs = dummyOriginServerTs(),
senderId = userId, senderId = userId,
eventId = localID, eventId = localId,
type = EventType.REDACTION, type = EventType.REDACTION,
redacts = eventId, redacts = eventId,
content = reason?.let { mapOf("reason" to it).toContent() }, content = reason?.let { mapOf("reason" to it).toContent() },
unsignedData = UnsignedData(age = null, transactionId = localID) unsignedData = UnsignedData(age = null, transactionId = localId)
) )
} }

View file

@ -120,7 +120,7 @@ internal class SyncResponseHandler @Inject constructor(private val monarchy: Mon
return return
} // nothing on initial sync } // nothing on initial sync
val rules = pushRuleService.getPushRules(RuleScope.GLOBAL) val rules = pushRuleService.getPushRules(RuleScope.GLOBAL).getAllRules()
processEventForPushTask.execute(ProcessEventForPushTask.Params(roomsSyncResponse, rules)) processEventForPushTask.execute(ProcessEventForPushTask.Params(roomsSyncResponse, rules))
Timber.v("[PushRules] <-- Push task scheduled") Timber.v("[PushRules] <-- Push task scheduled")
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 510 B

View file

@ -1,31 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingTop="10dp"
android:paddingBottom="10dp">
<ImageView
android:id="@+id/imageView_icon_and_text"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="center_vertical"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:src="@drawable/matrix_user" />
<TextView
android:id="@+id/textView_icon_and_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:singleLine="true"
android:textColor="@android:color/white"
tools:text="A text here" />
</LinearLayout>

View file

@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/listView_icon_and_text"/>
</LinearLayout>

View file

@ -1,22 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:background="#248801">
<org.matrix.androidsdk.view.AutoScrollDownListView
android:id="@+id/listView_messages"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:background="#190986"
android:cacheColorHint="@android:color/transparent"
android:childDivider="@android:color/transparent"
android:divider="#ffffff"
android:dividerHeight="0dp"
android:listSelector="@android:color/transparent"
android:transcriptMode="normal"
tools:layout_height="120dp" />
</FrameLayout>

View file

@ -1,3 +0,0 @@
<resources>
</resources>

View file

@ -0,0 +1,3 @@
#!/usr/bin/env bash
adb shell dumpsys jobscheduler im.vector.riotx.debug

View file

@ -0,0 +1,3 @@
#!/usr/bin/env bash
adb shell dumpsys jobscheduler im.vector.riotx

View file

@ -15,7 +15,7 @@ androidExtensions {
} }
ext.versionMajor = 0 ext.versionMajor = 0
ext.versionMinor = 17 ext.versionMinor = 18
ext.versionPatch = 0 ext.versionPatch = 0
static def getGitTimestamp() { static def getGitTimestamp() {
@ -296,7 +296,7 @@ dependencies {
implementation 'com.airbnb.android:mvrx:1.3.0' implementation 'com.airbnb.android:mvrx:1.3.0'
// Work // Work
implementation "androidx.work:work-runtime-ktx:2.3.0-beta02" implementation "androidx.work:work-runtime-ktx:2.3.3"
// Paging // Paging
implementation "androidx.paging:paging-runtime-ktx:2.1.1" implementation "androidx.paging:paging-runtime-ktx:2.1.1"

View file

@ -6,9 +6,15 @@
<issue id="MissingTranslation" severity="warning" /> <issue id="MissingTranslation" severity="warning" />
<issue id="TypographyEllipsis" severity="error" /> <issue id="TypographyEllipsis" severity="error" />
<issue id="ImpliedQuantity" severity="warning" /> <issue id="ImpliedQuantity" severity="warning" />
<issue id="IconXmlAndPng" severity="error" />
<issue id="IconDipSize" severity="error" />
<issue id="IconDuplicatesConfig" severity="error" />
<issue id="IconDuplicates" severity="error" />
<issue id="IconExpectedSize" severity="error" />
<!-- UX --> <!-- UX -->
<issue id="ButtonOrder" severity="error" /> <issue id="ButtonOrder" severity="error" />
<issue id="TextFields" severity="error" />
<!-- Layout --> <!-- Layout -->
<issue id="UnknownIdInLayout" severity="error" /> <issue id="UnknownIdInLayout" severity="error" />
@ -19,6 +25,7 @@
<issue id="InefficientWeight" severity="error" /> <issue id="InefficientWeight" severity="error" />
<issue id="DisableBaselineAlignment" severity="error" /> <issue id="DisableBaselineAlignment" severity="error" />
<issue id="ScrollViewSize" severity="error" /> <issue id="ScrollViewSize" severity="error" />
<issue id="NegativeMargin" severity="error" />
<!-- RTL --> <!-- RTL -->
<issue id="RtlEnabled" severity="error" /> <issue id="RtlEnabled" severity="error" />
@ -30,9 +37,22 @@
<issue id="SetTextI18n" severity="error" /> <issue id="SetTextI18n" severity="error" />
<issue id="ViewConstructor" severity="error" /> <issue id="ViewConstructor" severity="error" />
<issue id="UseValueOf" severity="error" /> <issue id="UseValueOf" severity="error" />
<issue id="Recycle" severity="error" />
<issue id="KotlinPropertyAccess" severity="error" />
<!-- Ignore error from HtmlCompressor lib --> <!-- Ignore error from HtmlCompressor lib -->
<issue id="InvalidPackage"> <issue id="InvalidPackage">
<ignore path="**/htmlcompressor-1.4.jar" /> <ignore path="**/htmlcompressor-1.4.jar" />
</issue> </issue>
<!-- Manifest -->
<issue id="PermissionImpliesUnsupportedChromeOsHardware" severity="error" />
<!-- Timber -->
<!-- This rule is failing on CI because it's marked as unknwown rule id :/-->
<!-- <issue id="BinaryOperationInTimber" severity="error" />-->
<!-- Wording -->
<!-- TODO When strings are imported from Weblate, move this to error -->
<issue id="Typos" severity="warning" />
</lint> </lint>

View file

@ -114,7 +114,7 @@ class DebugMenuActivity : VectorBaseActivity() {
.setContentText("Content") .setContentText("Content")
// No effect because it's a group summary notif // No effect because it's a group summary notif
.setNumber(33) .setNumber(33)
.setSmallIcon(R.drawable.logo_transparent) .setSmallIcon(R.drawable.ic_status_bar)
// This provocate the badge issue: no badge for group notification // This provocate the badge issue: no badge for group notification
.setGroup("GroupKey") .setGroup("GroupKey")
.setGroupSummary(true) .setGroupSummary(true)
@ -147,7 +147,7 @@ class DebugMenuActivity : VectorBaseActivity() {
// For shortcut on long press on launcher icon // For shortcut on long press on launcher icon
.setBadgeIconType(NotificationCompat.BADGE_ICON_NONE) .setBadgeIconType(NotificationCompat.BADGE_ICON_NONE)
.setStyle(messagingStyle1) .setStyle(messagingStyle1)
.setSmallIcon(R.drawable.logo_transparent) .setSmallIcon(R.drawable.ic_status_bar)
.setGroup("GroupKey") .setGroup("GroupKey")
.build() .build()
) )
@ -159,7 +159,7 @@ class DebugMenuActivity : VectorBaseActivity() {
.setContentTitle("Title 2") .setContentTitle("Title 2")
.setContentText("Content 2") .setContentText("Content 2")
.setStyle(messagingStyle2) .setStyle(messagingStyle2)
.setSmallIcon(R.drawable.logo_transparent) .setSmallIcon(R.drawable.ic_status_bar)
.setGroup("GroupKey") .setGroup("GroupKey")
.build() .build()
) )

View file

@ -57,7 +57,7 @@ object FcmHelper {
* *
* @param activity the first launch Activity * @param activity the first launch Activity
*/ */
fun ensureFcmTokenIsRetrieved(activity: Activity, pushersManager: PushersManager) { fun ensureFcmTokenIsRetrieved(activity: Activity, pushersManager: PushersManager, registerPusher: Boolean) {
// No op // No op
} }

View file

@ -20,7 +20,7 @@ import im.vector.riotx.fdroid.features.settings.troubleshoot.TestAutoStartBoot
import im.vector.riotx.fdroid.features.settings.troubleshoot.TestBackgroundRestrictions import im.vector.riotx.fdroid.features.settings.troubleshoot.TestBackgroundRestrictions
import im.vector.riotx.features.settings.troubleshoot.NotificationTroubleshootTestManager import im.vector.riotx.features.settings.troubleshoot.NotificationTroubleshootTestManager
import im.vector.riotx.features.settings.troubleshoot.TestAccountSettings import im.vector.riotx.features.settings.troubleshoot.TestAccountSettings
import im.vector.riotx.features.settings.troubleshoot.TestBingRulesSettings import im.vector.riotx.features.settings.troubleshoot.TestPushRulesSettings
import im.vector.riotx.features.settings.troubleshoot.TestDeviceSettings import im.vector.riotx.features.settings.troubleshoot.TestDeviceSettings
import im.vector.riotx.features.settings.troubleshoot.TestSystemSettings import im.vector.riotx.features.settings.troubleshoot.TestSystemSettings
import javax.inject.Inject import javax.inject.Inject
@ -28,7 +28,7 @@ import javax.inject.Inject
class NotificationTroubleshootTestManagerFactory @Inject constructor(private val testSystemSettings: TestSystemSettings, class NotificationTroubleshootTestManagerFactory @Inject constructor(private val testSystemSettings: TestSystemSettings,
private val testAccountSettings: TestAccountSettings, private val testAccountSettings: TestAccountSettings,
private val testDeviceSettings: TestDeviceSettings, private val testDeviceSettings: TestDeviceSettings,
private val testBingRulesSettings: TestBingRulesSettings, private val testPushRulesSettings: TestPushRulesSettings,
private val testAutoStartBoot: TestAutoStartBoot, private val testAutoStartBoot: TestAutoStartBoot,
private val testBackgroundRestrictions: TestBackgroundRestrictions) { private val testBackgroundRestrictions: TestBackgroundRestrictions) {
@ -37,7 +37,7 @@ class NotificationTroubleshootTestManagerFactory @Inject constructor(private val
mgr.addTest(testSystemSettings) mgr.addTest(testSystemSettings)
mgr.addTest(testAccountSettings) mgr.addTest(testAccountSettings)
mgr.addTest(testDeviceSettings) mgr.addTest(testDeviceSettings)
mgr.addTest(testBingRulesSettings) mgr.addTest(testPushRulesSettings)
mgr.addTest(testAutoStartBoot) mgr.addTest(testAutoStartBoot)
mgr.addTest(testBackgroundRestrictions) mgr.addTest(testBackgroundRestrictions)
return mgr return mgr

View file

@ -25,13 +25,13 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ProcessLifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner
import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage import com.google.firebase.messaging.RemoteMessage
import im.vector.matrix.android.api.pushrules.rest.PushRule
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
import im.vector.riotx.BuildConfig import im.vector.riotx.BuildConfig
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.extensions.vectorComponent import im.vector.riotx.core.extensions.vectorComponent
import im.vector.riotx.core.preference.BingRule
import im.vector.riotx.core.pushers.PushersManager import im.vector.riotx.core.pushers.PushersManager
import im.vector.riotx.features.badge.BadgeProxy import im.vector.riotx.features.badge.BadgeProxy
import im.vector.riotx.features.notifications.NotifiableEventResolver import im.vector.riotx.features.notifications.NotifiableEventResolver
@ -196,7 +196,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
description = "", description = "",
type = null, type = null,
timestamp = System.currentTimeMillis(), timestamp = System.currentTimeMillis(),
soundName = BingRule.ACTION_VALUE_DEFAULT, soundName = PushRule.ACTION_VALUE_DEFAULT,
isPushGatewayEvent = true isPushGatewayEvent = true
) )
notificationDrawerManager.onNotifiableEventReceived(simpleNotifiableEvent) notificationDrawerManager.onNotifiableEventReceived(simpleNotifiableEvent)

View file

@ -68,7 +68,7 @@ object FcmHelper {
* *
* @param activity the first launch Activity * @param activity the first launch Activity
*/ */
fun ensureFcmTokenIsRetrieved(activity: Activity, pushersManager: PushersManager) { fun ensureFcmTokenIsRetrieved(activity: Activity, pushersManager: PushersManager, registerPusher: Boolean) {
// if (TextUtils.isEmpty(getFcmToken(activity))) { // if (TextUtils.isEmpty(getFcmToken(activity))) {
// 'app should always check the device for a compatible Google Play services APK before accessing Google Play services features' // 'app should always check the device for a compatible Google Play services APK before accessing Google Play services features'
if (checkPlayServices(activity)) { if (checkPlayServices(activity)) {
@ -76,8 +76,10 @@ object FcmHelper {
FirebaseInstanceId.getInstance().instanceId FirebaseInstanceId.getInstance().instanceId
.addOnSuccessListener(activity) { instanceIdResult -> .addOnSuccessListener(activity) { instanceIdResult ->
storeFcmToken(activity, instanceIdResult.token) storeFcmToken(activity, instanceIdResult.token)
if (registerPusher) {
pushersManager.registerPusherWithFcmKey(instanceIdResult.token) pushersManager.registerPusherWithFcmKey(instanceIdResult.token)
} }
}
.addOnFailureListener(activity) { e -> Timber.e(e, "## ensureFcmTokenIsRetrieved() : failed") } .addOnFailureListener(activity) { e -> Timber.e(e, "## ensureFcmTokenIsRetrieved() : failed") }
} catch (e: Throwable) { } catch (e: Throwable) {
Timber.e(e, "## ensureFcmTokenIsRetrieved() : failed") Timber.e(e, "## ensureFcmTokenIsRetrieved() : failed")

View file

@ -18,8 +18,8 @@ package im.vector.riotx.push.fcm
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import im.vector.riotx.features.settings.troubleshoot.NotificationTroubleshootTestManager import im.vector.riotx.features.settings.troubleshoot.NotificationTroubleshootTestManager
import im.vector.riotx.features.settings.troubleshoot.TestAccountSettings import im.vector.riotx.features.settings.troubleshoot.TestAccountSettings
import im.vector.riotx.features.settings.troubleshoot.TestBingRulesSettings
import im.vector.riotx.features.settings.troubleshoot.TestDeviceSettings import im.vector.riotx.features.settings.troubleshoot.TestDeviceSettings
import im.vector.riotx.features.settings.troubleshoot.TestPushRulesSettings
import im.vector.riotx.features.settings.troubleshoot.TestSystemSettings import im.vector.riotx.features.settings.troubleshoot.TestSystemSettings
import im.vector.riotx.gplay.features.settings.troubleshoot.TestFirebaseToken import im.vector.riotx.gplay.features.settings.troubleshoot.TestFirebaseToken
import im.vector.riotx.gplay.features.settings.troubleshoot.TestPlayServices import im.vector.riotx.gplay.features.settings.troubleshoot.TestPlayServices
@ -29,7 +29,7 @@ import javax.inject.Inject
class NotificationTroubleshootTestManagerFactory @Inject constructor(private val testSystemSettings: TestSystemSettings, class NotificationTroubleshootTestManagerFactory @Inject constructor(private val testSystemSettings: TestSystemSettings,
private val testAccountSettings: TestAccountSettings, private val testAccountSettings: TestAccountSettings,
private val testDeviceSettings: TestDeviceSettings, private val testDeviceSettings: TestDeviceSettings,
private val testBingRulesSettings: TestBingRulesSettings, private val testBingRulesSettings: TestPushRulesSettings,
private val testPlayServices: TestPlayServices, private val testPlayServices: TestPlayServices,
private val testFirebaseToken: TestFirebaseToken, private val testFirebaseToken: TestFirebaseToken,
private val testTokenRegistration: TestTokenRegistration) { private val testTokenRegistration: TestTokenRegistration) {

Some files were not shown because too many files have changed in this diff Show more