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">
<code_scheme name="Project" version="173">
<option name="RIGHT_MARGIN" value="160" />
<AndroidXmlCodeStyleSettings>
<option name="ARRANGEMENT_SETTINGS_MIGRATED_TO_191" value="true" />
</AndroidXmlCodeStyleSettings>
<JetCodeStyleSettings>
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
<value>

View File

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

View File

@ -49,12 +49,12 @@ script:
# Build Android test (assembleAndroidTest) (disabled for now)
# Code quality (lintGplayRelease lintFdroidRelease)
# Split into two steps because if a task contain Fdroid, PlayService will be disabled
- ./gradlew clean assembleGplayRelease lintGplayRelease --stacktrace
- ./gradlew clean assembleFdroidRelease lintFdroidRelease --stacktrace
# Done by Buildkite now: - ./gradlew clean assembleGplayRelease lintGplayRelease --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)
# - ./gradlew testGplayReleaseUnitTest --stacktrace
# 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
# 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)
===================================================

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.
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
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'
// Work
implementation "androidx.work:work-runtime-ktx:2.3.0"
implementation "androidx.work:work-runtime-ktx:2.3.3"
// FP
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.internal.crypto.model.CryptoDeviceInfo
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.KeyVerificationStart
import im.vector.matrix.android.internal.crypto.model.rest.toValue
@ -279,7 +278,7 @@ class SASTest : InstrumentedTest {
val startMessage = KeyVerificationStart(
fromDevice = bobSession.cryptoService().getMyDevice().deviceId,
method = VerificationMethod.SAS.toValue(),
transactionID = tid,
transactionId = tid,
keyAgreementProtocols = protocols,
hashes = hashes,
messageAuthenticationCodes = mac,
@ -350,16 +349,16 @@ class SASTest : InstrumentedTest {
val aliceVerificationService = aliceSession.cryptoService().verificationService()
val bobVerificationService = bobSession!!.cryptoService().verificationService()
var accepted: KeyVerificationAccept? = null
var startReq: KeyVerificationStart? = null
var accepted: ValidVerificationInfoAccept? = null
var startReq: ValidVerificationInfoStart.SasVerificationInfoStart? = null
val aliceAcceptedLatch = CountDownLatch(1)
val aliceListener = object : VerificationService.Listener {
override fun transactionUpdated(tx: VerificationTransaction) {
if ((tx as SASDefaultVerificationTransaction).state === VerificationTxState.OnAccepted) {
val at = tx as SASDefaultVerificationTransaction
accepted = at.accepted as? KeyVerificationAccept
startReq = at.startReq as? KeyVerificationStart
accepted = at.accepted
startReq = at.startReq
aliceAcceptedLatch.countDown()
}
}
@ -384,13 +383,13 @@ class SASTest : InstrumentedTest {
assertTrue("Should have receive a commitment", accepted!!.commitment?.trim()?.isEmpty() == false)
// check that agreement is valid
assertTrue("Agreed Protocol should be Valid", accepted!!.isValid())
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!!.messageAuthenticationCodes!!.contains(accepted!!.messageAuthenticationCode))
assertTrue("Agreed Protocol should be Valid", accepted != null)
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!!.messageAuthenticationCodes.contains(accepted!!.messageAuthenticationCode))
accepted!!.shortAuthenticationStrings?.forEach {
assertTrue("all agreed Short Code should be known by alice", startReq!!.shortAuthenticationStrings!!.contains(it))
accepted!!.shortAuthenticationStrings.forEach {
assertTrue("all agreed Short Code should be known by alice", startReq!!.shortAuthenticationStrings.contains(it))
}
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.TestConstants
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.junit.FixMethodOrder
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.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.util.Cancelable
interface PushRuleService {
/**
* Fetch the push rules from the server
*/
fun fetchPushRules(scope: String = RuleScope.GLOBAL)
// TODO get push rule set
fun getPushRules(scope: String = RuleScope.GLOBAL): List<PushRule>
// TODO update rule
fun getPushRules(scope: String = RuleScope.GLOBAL): RuleSet
fun updatePushRuleEnableStatus(kind: RuleKind, pushRule: PushRule, enabled: Boolean, 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 addPushRuleListener(listener: PushRuleListener)

View File

@ -20,6 +20,7 @@ import com.squareup.moshi.JsonClass
/**
* All push rulesets for a user.
* Ref: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-pushrules
*/
@JsonClass(generateAdapter = true)
internal data class GetPushRulesResponse(
@ -27,11 +28,11 @@ internal data class GetPushRulesResponse(
* Global rules, account level applying to all devices
*/
@Json(name = "global")
val global: Ruleset,
val global: RuleSet,
/**
* Device specific rules, apply only to current 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 timber.log.Timber
/**
* Ref: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-pushrules
*/
@JsonClass(generateAdapter = true)
data class PushCondition(
/**
* Required. The kind of condition to apply.
*/
@Json(name = "kind")
val kind: String,
/**
* Required for event_match conditions. The dot- separated field of the event to match.
*/
@Json(name = "key")
val key: String? = null,
/**
* Required for event_match conditions.
*/
@Json(name = "pattern")
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.
* 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? {

View File

@ -18,31 +18,158 @@ 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.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)
data class PushRule(
/**
* Required. The actions to perform when this rule is matched.
*/
@Json(name = "actions")
val actions: List<Any>,
/**
* Required. Whether this is a default rule, or has been set explicitly.
*/
@Json(name = "default")
val default: Boolean? = false,
/**
* Required. Whether the push rule is enabled or not.
*/
@Json(name = "enabled")
val enabled: Boolean,
/**
* 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
*/
@Json(name = "conditions")
val conditions: List<PushCondition>? = null,
/**
* The glob-style pattern to match against. Only applicable to content rules.
*/
@Json(name = "pattern")
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 queryUri: String,
val path: String,
val mimeType: String?,
private val mimeType: String?,
val type: Type
) : Parcelable {
@ -41,4 +41,6 @@ data class ContentAttachmentData(
AUDIO,
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
* 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.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_SHOW
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
* TODO We should not expose this whole object to the app. Create an interface
*/
data class PendingVerificationRequest(
val ageLocalTs: Long,
val isIncoming: Boolean = false,
val localID: String = UUID.randomUUID().toString(),
val localId: String = UUID.randomUUID().toString(),
val otherUserId: String,
val roomId: String?,
val transactionId: String? = null,
val requestInfo: VerificationInfoRequest? = null,
val readyInfo: VerificationInfoReady? = null,
val requestInfo: ValidVerificationInfoRequest? = null,
val readyInfo: ValidVerificationInfoReady? = null,
val cancelConclusion: CancelCode? = null,
val isSuccessful: Boolean = false,
val handledByOtherSession: Boolean = false,

View File

@ -1,11 +1,11 @@
/*
* Copyright 2019 New Vector Ltd
* 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
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@ -13,15 +13,11 @@
* 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.JsonClass
package im.vector.matrix.android.api.session.crypto.verification
@JsonClass(generateAdapter = true)
internal data class Ruleset(
val content: List<PushRule>? = null,
val override: List<PushRule>? = null,
val room: List<PushRule>? = null,
val sender: List<PushRule>? = null,
val underride: List<PushRule>? = null
data class ValidVerificationInfoReady(
val transactionId: String,
val fromDevice: String,
val methods: List<String>
)

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");
* you may not use this file except in compliance with the License.
@ -14,16 +14,11 @@
* 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
val supportedVerificationMethods =
listOf(
// RiotX supports SAS verification
VerificationMethod.SAS,
// RiotX is able to show QR codes
VerificationMethod.QR_CODE_SHOW,
// RiotX is able to scan QR codes
VerificationMethod.QR_CODE_SCAN
)
data class ValidVerificationInfoRequest(
val transactionId: String,
val fromDevice: String,
val methods: List<String>,
val timestamp: Long?
)

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

View File

@ -34,7 +34,11 @@ interface FileService {
/**
* 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.
*/
@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.
*/
@Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null
) : MessageEncryptedContent {
) : MessageWithAttachmentContent {
fun getMimeType(): String {
// 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
*/
interface MessageImageInfoContent : MessageEncryptedContent {
interface MessageImageInfoContent : MessageWithAttachmentContent {
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.internal.crypto.verification.VerificationInfoAccept
import im.vector.matrix.android.internal.crypto.verification.VerificationInfoAcceptFactory
import timber.log.Timber
@JsonClass(generateAdapter = true)
internal data class MessageVerificationAcceptContent(
@ -34,22 +33,9 @@ internal data class MessageVerificationAcceptContent(
@Json(name = "commitment") override var commitment: String? = null
) : VerificationInfoAccept {
override val transactionID: String?
override val transactionId: String?
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()
companion object : VerificationInfoAcceptFactory {

View File

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

View File

@ -25,12 +25,22 @@ import im.vector.matrix.android.internal.crypto.verification.VerificationInfo
@JsonClass(generateAdapter = true)
internal data class MessageVerificationDoneContent(
@Json(name = "m.relates_to") val relatesTo: RelationDefaultContent?
) : VerificationInfo {
) : VerificationInfo<ValidVerificationDone> {
override val transactionID: String?
override val transactionId: String?
get() = relatesTo?.eventId
override fun isValid() = transactionID?.isNotEmpty() == true
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.internal.crypto.verification.VerificationInfoKey
import im.vector.matrix.android.internal.crypto.verification.VerificationInfoKeyFactory
import timber.log.Timber
@JsonClass(generateAdapter = true)
internal data class MessageVerificationKeyContent(
@ -33,17 +32,9 @@ internal data class MessageVerificationKeyContent(
@Json(name = "m.relates_to") val relatesTo: RelationDefaultContent?
) : VerificationInfoKey {
override val transactionID: String?
override val transactionId: String?
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()
companion object : VerificationInfoKeyFactory {

View File

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

View File

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

View File

@ -33,18 +33,10 @@ data class MessageVerificationRequestContent(
@Json(name = "format") val format: String? = null,
@Json(name = "formatted_body") val formattedBody: String? = 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 {
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()
}

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.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.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.util.JsonCanonicalizer
import timber.log.Timber
@JsonClass(generateAdapter = true)
internal data class MessageVerificationStartContent(
@ -39,46 +34,12 @@ internal data class MessageVerificationStartContent(
@Json(name = "secret") override val sharedSecret: String?
) : VerificationInfoStart {
override fun toCanonicalJson(): String? {
override fun toCanonicalJson(): String {
return JsonCanonicalizer.getCanonicalJson(MessageVerificationStartContent::class.java, this)
}
override val transactionID: String?
override val transactionId: String?
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()
}

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.
*/
@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 MessageEncryptedContent : MessageContent {
interface MessageWithAttachmentContent : MessageContent {
/**
* 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
*/
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 ParsingError : SharedSecretStorageError("parsing Error")
object BadMac : SharedSecretStorageError("Bad mac")
object BadCipherText : SharedSecretStorageError("Bad cipher text")
data class OtherError(val reason: Throwable) : SharedSecretStorageError(reason.localizedMessage)
}

View File

@ -627,9 +627,7 @@ internal class DefaultCryptoService @Inject constructor(
*/
@Throws(MXCryptoError::class)
override fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
return runBlocking {
internalDecryptEvent(event, timeline)
}
return 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.
* @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
if (eventContent == null) {
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.
* @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.

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

View File

@ -38,7 +38,7 @@ internal class MXOlmDecryption(
private val userId: String)
: 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 {
Timber.e("## decryptEvent() : 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 ->
privateKeysInfo.master
?.fromBase64NoPadding()
?.fromBase64()
?.let { privateKeySeed ->
val pkSigning = OlmPkSigning()
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.masterKey()?.unpaddedBase64PublicKey) {
@ -93,7 +93,7 @@ internal class DefaultCrossSigningService @Inject constructor(
}
}
privateKeysInfo.user
?.fromBase64NoPadding()
?.fromBase64()
?.let { privateKeySeed ->
val pkSigning = OlmPkSigning()
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.userKey()?.unpaddedBase64PublicKey) {
@ -106,7 +106,7 @@ internal class DefaultCrossSigningService @Inject constructor(
}
}
privateKeysInfo.selfSigned
?.fromBase64NoPadding()
?.fromBase64()
?.let { privateKeySeed ->
val pkSigning = OlmPkSigning()
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.selfSigningKey()?.unpaddedBase64PublicKey) {
@ -307,7 +307,7 @@ internal class DefaultCrossSigningService @Inject constructor(
var userKeyIsTrusted = false
var selfSignedKeyIsTrusted = false
masterKeyPrivateKey?.fromBase64NoPadding()
masterKeyPrivateKey?.fromBase64()
?.let { privateKeySeed ->
val pkSigning = OlmPkSigning()
try {
@ -324,7 +324,7 @@ internal class DefaultCrossSigningService @Inject constructor(
}
}
uskKeyPrivateKey?.fromBase64NoPadding()
uskKeyPrivateKey?.fromBase64()
?.let { privateKeySeed ->
val pkSigning = OlmPkSigning()
try {
@ -341,7 +341,7 @@ internal class DefaultCrossSigningService @Inject constructor(
}
}
sskPrivateKey?.fromBase64NoPadding()
sskPrivateKey?.fromBase64()
?.let { privateKeySeed ->
val pkSigning = OlmPkSigning()
try {
@ -450,7 +450,7 @@ internal class DefaultCrossSigningService @Inject constructor(
// 1) check if I know the private key
val masterPrivateKey = cryptoStore.getCrossSigningPrivateKeys()
?.master
?.fromBase64NoPadding()
?.fromBase64()
var isMaterKeyTrusted = false
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.CryptoDeviceInfo
import im.vector.matrix.android.internal.util.JsonCanonicalizer
import timber.log.Timber
fun CryptoDeviceInfo.canonicalSignable(): String {
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)
}
fun String.fromBase64NoPadding(): ByteArray {
return Base64.decode(this, Base64.NO_PADDING or Base64.NO_WRAP)
fun String.fromBase64(): ByteArray {
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 im.vector.matrix.android.internal.crypto.verification.VerificationInfoAccept
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.
*/
@JsonClass(generateAdapter = true)
internal data class KeyVerificationAccept(
/**
* 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.
* Alices device should record this ID and use it in future messages in this transaction.
*/
@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
@ -67,19 +65,6 @@ internal data class KeyVerificationAccept(
override var commitment: String? = null
) : 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
companion object : VerificationInfoAcceptFactory {
@ -90,7 +75,7 @@ internal data class KeyVerificationAccept(
messageAuthenticationCode: String,
shortAuthenticationStrings: List<String>): VerificationInfoAccept {
return KeyVerificationAccept(
transactionID = tid,
transactionId = tid,
keyAgreementProtocol = keyAgreementProtocol,
hash = hash,
commitment = commitment,

View File

@ -29,7 +29,7 @@ internal data class KeyVerificationCancel(
* the transaction ID of the verification to cancel
*/
@Json(name = "transaction_id")
override val transactionID: String? = null,
override val transactionId: String? = null,
/**
* machine-readable reason for cancelling, see #CancelCode
@ -53,11 +53,4 @@ internal data class KeyVerificationCancel(
}
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)
internal data class KeyVerificationDone(
@Json(name = "transaction_id") override val transactionID: String? = null
@Json(name = "transaction_id") override val transactionId: String? = null
) : SendToDeviceObject, VerificationInfoDone {
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
*/
@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
@ -44,11 +44,4 @@ internal data class KeyVerificationKey(
}
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)
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 = "keys") override val keys: String? = null
) : SendToDeviceObject, VerificationInfoMac {
override fun isValid(): Boolean {
if (transactionID.isNullOrBlank() || keys.isNullOrBlank() || mac.isNullOrEmpty()) {
return false
}
return true
}
override fun toSendToDeviceObject(): SendToDeviceObject? = this
companion object : VerificationInfoMacFactory {

View File

@ -26,12 +26,8 @@ import im.vector.matrix.android.internal.crypto.verification.VerificationInfoRea
internal data class KeyVerificationReady(
@Json(name = "from_device") override val fromDevice: 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 {
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 = "methods") override val methods: List<String>,
@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 {
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.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.util.JsonCanonicalizer
import timber.log.Timber
/**
* Sent by Alice to initiate an interactive key verification.
@ -29,8 +26,8 @@ import timber.log.Timber
@JsonClass(generateAdapter = true)
internal data class KeyVerificationStart(
@Json(name = "from_device") override val fromDevice: String? = null,
override val method: String? = null,
@Json(name = "transaction_id") override val transactionID: String? = null,
@Json(name = "method") override val method: String? = null,
@Json(name = "transaction_id") override val transactionId: String? = null,
@Json(name = "key_agreement_protocols") override val keyAgreementProtocols: List<String>? = null,
@Json(name = "hashes") override val hashes: 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
) : SendToDeviceObject, VerificationInfoStart {
override fun toCanonicalJson(): String? {
override fun toCanonicalJson(): String {
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
}

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.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.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.keysbackup.generatePrivateKeyWithPassword
import im.vector.matrix.android.internal.crypto.keysbackup.util.computeRecoveryKey
@ -268,7 +268,7 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
val ivParameterSpec = IvParameterSpec(iv)
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec)
// secret are not that big, just do Final
val cipherBytes = cipher.doFinal(clearDataBase64.fromBase64NoPadding())
val cipherBytes = cipher.doFinal(clearDataBase64.fromBase64())
require(cipherBytes.isNotEmpty())
val macKeySpec = SecretKeySpec(macKey, "HmacSHA256")
@ -295,9 +295,9 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
val aesKey = pseudoRandomKey.copyOfRange(0, 32)
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")
@ -314,7 +314,7 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
val mac = Mac.getInstance("HmacSHA256").apply { init(macKeySpec) }
val digest = mac.doFinal(cipherRawBytes)
if (!cipherContent.mac?.fromBase64NoPadding()?.contentEquals(digest).orFalse()) {
if (!cipherContent.mac?.fromBase64()?.contentEquals(digest).orFalse()) {
throw SharedSecretStorageError.BadMac
} else {
// we are good

View File

@ -41,22 +41,22 @@ internal class DefaultSendVerificationMessageTask @Inject constructor(
override suspend fun execute(params: SendVerificationMessageTask.Params): String {
val event = handleEncryption(params)
val localID = event.eventId!!
val localId = event.eventId!!
try {
localEchoUpdater.updateSendState(localID, SendState.SENDING)
localEchoUpdater.updateSendState(localId, SendState.SENDING)
val executeRequest = executeRequest<SendResponse>(eventBus) {
apiCall = roomAPI.send(
localID,
localId,
roomId = event.roomId ?: "",
content = event.content,
eventType = event.type
)
}
localEchoUpdater.updateSendState(localID, SendState.SENT)
localEchoUpdater.updateSendState(localId, SendState.SENT)
return executeRequest.eventId
} catch (e: Throwable) {
localEchoUpdater.updateSendState(localID, SendState.UNDELIVERED)
localEchoUpdater.updateSendState(localId, SendState.UNDELIVERED)
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")
if (state != VerificationTxState.None) {
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,
// 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 agreedHash = startReq!!.hashes?.firstOrNull { KNOWN_HASHES.contains(it) }
val agreedMac = startReq!!.messageAuthenticationCodes?.firstOrNull { KNOWN_MACS.contains(it) }
val agreedShortCode = startReq!!.shortAuthenticationStrings?.filter { KNOWN_SHORT_CODES.contains(it) }
val agreedProtocol = startReq!!.keyAgreementProtocols.firstOrNull { KNOWN_AGREEMENT_PROTOCOLS.contains(it) }
val agreedHash = startReq!!.hashes.firstOrNull { KNOWN_HASHES.contains(it) }
val agreedMac = startReq!!.messageAuthenticationCodes.firstOrNull { KNOWN_MACS.contains(it) }
val agreedShortCode = startReq!!.shortAuthenticationStrings.filter { KNOWN_SHORT_CODES.contains(it) }
// 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,
@ -141,12 +141,12 @@ internal class DefaultIncomingSASDefaultVerificationTransaction(
}
private fun doAccept(accept: VerificationInfoAccept) {
this.accepted = accept
this.accepted = accept.asValidObject()
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,
// 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) ?: ""
// we need to send this to other device now
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")
cancel(CancelCode.UnexpectedMessage)
}
override fun onKeyVerificationKey(vKey: VerificationInfoKey) {
override fun onKeyVerificationKey(vKey: ValidVerificationInfoKey) {
Timber.v("## SAS received key for request id:$transactionId")
if (state != VerificationTxState.SendingAccept && state != VerificationTxState.Accepted) {
Timber.e("## SAS received key from invalid state $state")
@ -213,7 +213,7 @@ internal class DefaultIncomingSASDefaultVerificationTransaction(
state = VerificationTxState.ShortCodeReady
}
override fun onKeyVerificationMac(vKey: VerificationInfoMac) {
override fun onKeyVerificationMac(vMac: ValidVerificationInfoMac) {
Timber.v("## SAS I: received mac for request id:$transactionId")
// Check for state?
if (state != VerificationTxState.SendingKey
@ -226,12 +226,13 @@ internal class DefaultIncomingSASDefaultVerificationTransaction(
cancel(CancelCode.UnexpectedMessage)
return
}
theirMac = vKey
theirMac = vMac
// Do I have my Mac?
if (myMac != null) {
// I can check
verifyMacs()
verifyMacs(vMac)
}
// 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")
cancel(CancelCode.UnexpectedMessage)
}
@ -95,7 +95,7 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction(
KNOWN_SHORT_CODES
)
startReq = startMessage
startReq = startMessage.asValidObject() as? ValidVerificationInfoStart.SasVerificationInfoStart
state = VerificationTxState.SendingStart
sendToOther(
@ -118,7 +118,7 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction(
// fromDevice = session.sessionParams.credentials.deviceId ?: "",
// methods = listOf(KeyVerificationStart.VERIF_METHOD_SAS),
// timestamp = System.currentTimeMillis().toInt(),
// transactionID = transactionId
// transactionId = transactionId
// )
//
// 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")
if (state != VerificationTxState.Started) {
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)
|| !KNOWN_HASHES.contains(accept.hash)
|| !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")
cancel(CancelCode.UnknownMethod)
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")
if (state != VerificationTxState.SendingKey && state != VerificationTxState.KeySent) {
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.
// check commitment
val concat = vKey.key + startReq!!.toCanonicalJson()
val concat = vKey.key + startReq!!.canonicalJson
val otherCommitment = hashUsingAgreedHashMethod(concat) ?: ""
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")
if (state != VerificationTxState.OnKeyReceived
&& state != VerificationTxState.ShortCodeReady
@ -218,12 +218,12 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction(
return
}
theirMac = vKey
theirMac = vMac
// Do I have my Mac?
if (myMac != null) {
// I can check
verifyMacs()
verifyMacs(vMac)
}
// 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.crosssigning.CrossSigningService
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.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.VerificationService
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.MessageVerificationRequestContent
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.MyDeviceInfoHolder
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 kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import okhttp3.internal.toImmutableList
import timber.log.Timber
import java.util.UUID
import javax.inject.Inject
@ -105,7 +107,7 @@ internal class DefaultVerificationService @Inject constructor(
* Map [sender: [PendingVerificationRequest]]
* 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
fun onToDeviceEvent(event: Event) {
@ -268,9 +270,9 @@ internal class DefaultVerificationService @Inject constructor(
}
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
Timber.e("## SAS Received invalid key request")
return
@ -278,7 +280,7 @@ internal class DefaultVerificationService @Inject constructor(
val senderId = event.senderId ?: return
// We don't want to block here
val otherDeviceId = requestInfo.fromDevice ?: return
val otherDeviceId = validRequestInfo.fromDevice
GlobalScope.launch {
if (checkKeysAreDownloaded(senderId, otherDeviceId) == null) {
@ -287,19 +289,16 @@ internal class DefaultVerificationService @Inject constructor(
}
// Remember this request
val requestsForUser = pendingRequests[senderId]
?: ArrayList<PendingVerificationRequest>().also {
pendingRequests[event.senderId] = it
}
val requestsForUser = pendingRequests.getOrPut(senderId) { mutableListOf() }
val pendingVerificationRequest = PendingVerificationRequest(
ageLocalTs = event.ageLocalTs ?: System.currentTimeMillis(),
isIncoming = true,
otherUserId = senderId, // requestInfo.toUserId,
roomId = null,
transactionId = requestInfo.transactionID,
localID = requestInfo.transactionID!!,
requestInfo = requestInfo
transactionId = validRequestInfo.transactionId,
localId = validRequestInfo.transactionId,
requestInfo = validRequestInfo
)
requestsForUser.add(pendingVerificationRequest)
dispatchRequestAdded(pendingVerificationRequest)
@ -307,10 +306,13 @@ internal class DefaultVerificationService @Inject constructor(
suspend fun onRoomRequestReceived(event: Event) {
Timber.v("## SAS Verification request from ${event.senderId} in room ${event.roomId}")
val requestInfo = event.getClearContent().toModel<MessageVerificationRequestContent>()
?: return
val requestInfo = event.getClearContent().toModel<MessageVerificationRequestContent>() ?: return
val validRequestInfo = requestInfo
// copy the EventId to the transactionId
.copy(transactionId = event.eventId)
.asValidObject() ?: return
val senderId = event.senderId ?: return
val fromDevice = requestInfo.fromDevice ?: return
if (requestInfo.toUserId != userId) {
// 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
GlobalScope.launch {
if (checkKeysAreDownloaded(senderId, fromDevice) == null) {
Timber.e("## SAS Verification device $fromDevice is not known")
if (checkKeysAreDownloaded(senderId, validRequestInfo.fromDevice) == null) {
Timber.e("## SAS Verification device ${validRequestInfo.fromDevice} is not known")
}
}
// Remember this request
val requestsForUser = pendingRequests[senderId]
?: ArrayList<PendingVerificationRequest>().also {
pendingRequests[event.senderId] = it
}
val requestsForUser = pendingRequests.getOrPut(senderId) { mutableListOf() }
val pendingVerificationRequest = PendingVerificationRequest(
ageLocalTs = event.ageLocalTs ?: System.currentTimeMillis(),
@ -337,8 +336,8 @@ internal class DefaultVerificationService @Inject constructor(
otherUserId = senderId, // requestInfo.toUserId,
roomId = event.roomId,
transactionId = event.eventId,
localID = event.eventId!!,
requestInfo = requestInfo
localId = event.eventId!!,
requestInfo = validRequestInfo
)
requestsForUser.add(pendingVerificationRequest)
dispatchRequestAdded(pendingVerificationRequest)
@ -362,13 +361,15 @@ internal class DefaultVerificationService @Inject constructor(
relatesTo = event.content.toModel<MessageRelationContent>()?.relatesTo
)
val validStartReq = startReq?.asValidObject()
val otherUserId = event.senderId
if (startReq?.isValid()?.not() == true) {
if (validStartReq == null) {
Timber.e("## received invalid verification request")
if (startReq.transactionID != null) {
if (startReq?.transactionId != null) {
verificationTransportRoomMessageFactory.createTransport(event.roomId ?: "", null)
.cancelTransaction(
startReq.transactionID ?: "",
startReq.transactionId ?: "",
otherUserId!!,
startReq.fromDevice ?: event.getSenderKey()!!,
CancelCode.UnknownMethod
@ -377,14 +378,14 @@ internal class DefaultVerificationService @Inject constructor(
return
}
handleStart(otherUserId, startReq as VerificationInfoStart) {
handleStart(otherUserId, validStartReq) {
it.transport = verificationTransportRoomMessageFactory.createTransport(event.roomId ?: "", it)
}?.let {
verificationTransportRoomMessageFactory.createTransport(event.roomId ?: "", null)
.cancelTransaction(
startReq.transactionID ?: "",
validStartReq.transactionId,
otherUserId!!,
startReq.fromDevice ?: event.getSenderKey()!!,
validStartReq.fromDevice,
it
)
}
@ -392,16 +393,17 @@ internal class DefaultVerificationService @Inject constructor(
private suspend fun onStartRequestReceived(event: Event) {
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")
val otherUserId = event.senderId
if (!startReq.isValid()) {
val otherUserId = event.senderId!!
if (validStartReq == null) {
Timber.e("## SAS received invalid verification request")
if (startReq.transactionID != null) {
if (startReq?.transactionId != null) {
verificationTransportToDeviceFactory.createTransport(null).cancelTransaction(
startReq.transactionID,
otherUserId!!,
startReq.transactionId,
otherUserId,
startReq.fromDevice ?: event.getSenderKey()!!,
CancelCode.UnknownMethod
)
@ -409,13 +411,13 @@ internal class DefaultVerificationService @Inject constructor(
return
}
// Download device keys prior to everything
handleStart(otherUserId, startReq) {
handleStart(otherUserId, validStartReq) {
it.transport = verificationTransportToDeviceFactory.createTransport(it)
}?.let {
verificationTransportToDeviceFactory.createTransport(null).cancelTransaction(
startReq.transactionID ?: "",
otherUserId!!,
startReq.fromDevice ?: event.getSenderKey()!!,
validStartReq.transactionId,
otherUserId,
validStartReq.fromDevice,
it
)
}
@ -424,18 +426,20 @@ internal class DefaultVerificationService @Inject constructor(
/**
* 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? {
Timber.d("## SAS onStartRequestReceived ${startReq.transactionID}")
if (checkKeysAreDownloaded(otherUserId!!, startReq.fromDevice ?: "") != null) {
val tid = startReq.transactionID!!
private suspend fun handleStart(otherUserId: String?,
startReq: ValidVerificationInfoStart,
txConfigure: (DefaultVerificationTransaction) -> Unit): CancelCode? {
Timber.d("## SAS onStartRequestReceived ${startReq.transactionId}")
if (checkKeysAreDownloaded(otherUserId!!, startReq.fromDevice) != null) {
val tid = startReq.transactionId
val existing = getExistingTransaction(otherUserId, tid)
when (startReq.method) {
VERIFICATION_METHOD_SAS -> {
when (startReq) {
is ValidVerificationInfoStart.SasVerificationInfoStart -> {
when (existing) {
is SasVerificationTransaction -> {
// 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)
// Already cancelled, so return null
return null
@ -450,7 +454,7 @@ internal class DefaultVerificationService @Inject constructor(
?.also {
// Multiple keyshares between two devices:
// 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 {
it.cancel(CancelCode.UnexpectedMessage)
@ -462,12 +466,12 @@ internal class DefaultVerificationService @Inject constructor(
}
// 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
// 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
val autoAccept = getExistingVerificationRequest(otherUserId)?.any {
it.transactionId == startReq.transactionID
it.transactionId == startReq.transactionId
&& (it.requestInfo?.fromDevice == this.deviceId || it.readyInfo?.fromDevice == this.deviceId)
}
?: false
@ -479,27 +483,23 @@ internal class DefaultVerificationService @Inject constructor(
cryptoStore,
crossSigningService,
myDeviceInfoHolder.get().myDevice.fingerprint()!!,
startReq.transactionID!!,
startReq.transactionId,
otherUserId,
autoAccept).also { txConfigure(it) }
addTransaction(tx)
tx.acceptVerificationEvent(otherUserId, startReq)
tx.onVerificationStart(startReq)
return null
}
VERIFICATION_METHOD_RECIPROCATE -> {
is ValidVerificationInfoStart.ReciprocateVerificationInfoStart -> {
// Other user has scanned my QR code
if (existing is DefaultQrCodeVerificationTransaction) {
existing.onStartReceived(startReq)
return null
} else {
Timber.w("## SAS onStartRequestReceived - unexpected message ${startReq.transactionID}")
Timber.w("## SAS onStartRequestReceived - unexpected message ${startReq.transactionId}")
return CancelCode.UnexpectedMessage
}
}
else -> {
Timber.e("## SAS onStartRequestReceived - unknown method ${startReq.method}")
return CancelCode.UnknownMethod
}
}
} else {
return CancelCode.UnexpectedMessage
@ -529,24 +529,27 @@ internal class DefaultVerificationService @Inject constructor(
// relates_to is in clear in encrypted payload
relatesTo = event.content.toModel<MessageRelationContent>()?.relatesTo
)
if (cancelReq == null || cancelReq.isValid().not()) {
val validCancelReq = cancelReq?.asValidObject()
if (validCancelReq == null) {
// ignore
Timber.e("## SAS Received invalid key request")
// TODO should we cancel?
return
}
getExistingVerificationRequest(event.senderId ?: "", cancelReq.transactionID)?.let {
updatePendingRequest(it.copy(cancelConclusion = safeValueOf(cancelReq.code)))
getExistingVerificationRequest(event.senderId ?: "", validCancelReq.transactionId)?.let {
updatePendingRequest(it.copy(cancelConclusion = safeValueOf(validCancelReq.code)))
// Should we remove it from the list?
}
handleOnCancel(event.senderId!!, cancelReq)
handleOnCancel(event.senderId!!, validCancelReq)
}
private fun onCancelReceived(event: Event) {
Timber.v("## SAS onCancelReceived")
val cancelReq = event.getClearContent().toModel<KeyVerificationCancel>()!!
val cancelReq = event.getClearContent().toModel<KeyVerificationCancel>()?.asValidObject()
if (!cancelReq.isValid()) {
if (cancelReq == null) {
// ignore
Timber.e("## SAS Received invalid cancel request")
return
@ -556,11 +559,11 @@ internal class DefaultVerificationService @Inject constructor(
handleOnCancel(otherUserId, cancelReq)
}
private fun handleOnCancel(otherUserId: String, cancelReq: VerificationInfoCancel) {
Timber.v("## SAS onCancelReceived otherUser:$otherUserId reason:${cancelReq.reason}")
private fun handleOnCancel(otherUserId: String, cancelReq: ValidVerificationInfoCancel) {
Timber.v("## SAS onCancelReceived otherUser: $otherUserId reason: ${cancelReq.reason}")
val existingTransaction = getExistingTransaction(otherUserId, cancelReq.transactionID!!)
val existingRequest = getExistingVerificationRequest(otherUserId, cancelReq.transactionID!!)
val existingTransaction = getExistingTransaction(otherUserId, cancelReq.transactionId)
val existingRequest = getExistingVerificationRequest(otherUserId, cancelReq.transactionId)
if (existingRequest != null) {
// Mark this request as cancelled
@ -582,30 +585,28 @@ internal class DefaultVerificationService @Inject constructor(
relatesTo = event.content.toModel<MessageRelationContent>()?.relatesTo
)
?: return
handleAccept(accept, event.senderId!!)
val validAccept = accept.asValidObject() ?: return
handleAccept(validAccept, event.senderId!!)
}
private fun onAcceptReceived(event: 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!!)
}
private fun handleAccept(acceptReq: VerificationInfoAccept, senderId: String) {
if (!acceptReq.isValid()) {
// ignore
Timber.e("## SAS Received invalid accept request")
return
}
private fun handleAccept(acceptReq: ValidVerificationInfoAccept, senderId: String) {
val otherUserId = senderId
val existing = getExistingTransaction(otherUserId, acceptReq.transactionID!!)
val existing = getExistingTransaction(otherUserId, acceptReq.transactionId)
if (existing == null) {
Timber.e("## SAS Received invalid accept request")
return
}
if (existing is SASDefaultVerificationTransaction) {
existing.acceptVerificationEvent(otherUserId, acceptReq)
existing.onVerificationAccept(acceptReq)
} else {
// not other types now
}
@ -617,7 +618,8 @@ internal class DefaultVerificationService @Inject constructor(
// relates_to is in clear in encrypted payload
relatesTo = event.content.toModel<MessageRelationContent>()?.relatesTo
)
if (keyReq == null || keyReq.isValid().not()) {
?.asValidObject()
if (keyReq == null) {
// ignore
Timber.e("## SAS Received invalid key request")
// TODO should we cancel?
@ -627,9 +629,9 @@ internal class DefaultVerificationService @Inject constructor(
}
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
Timber.e("## SAS Received invalid key request")
return
@ -637,16 +639,16 @@ internal class DefaultVerificationService @Inject constructor(
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")
val otherUserId = event.senderId!!
val existing = getExistingTransaction(otherUserId, keyReq.transactionID!!)
val existing = getExistingTransaction(otherUserId, keyReq.transactionId)
if (existing == null) {
Timber.e("## SAS Received invalid key request")
return
}
if (existing is SASDefaultVerificationTransaction) {
existing.acceptVerificationEvent(otherUserId, keyReq)
existing.onKeyVerificationKey(keyReq)
} else {
// not other types now
}
@ -658,7 +660,8 @@ internal class DefaultVerificationService @Inject constructor(
// relates_to is in clear in encrypted payload
relatesTo = event.content.toModel<MessageRelationContent>()?.relatesTo
)
if (macReq == null || macReq.isValid().not() || event.senderId == null) {
?.asValidObject()
if (macReq == null || event.senderId == null) {
// ignore
Timber.e("## SAS Received invalid mac request")
// TODO should we cancel?
@ -673,13 +676,14 @@ internal class DefaultVerificationService @Inject constructor(
// relates_to is in clear in encrypted payload
relatesTo = event.content.toModel<MessageRelationContent>()?.relatesTo
)
if (readyReq == null || readyReq.isValid().not() || event.senderId == null) {
?.asValidObject()
if (readyReq == null || event.senderId == null) {
// ignore
Timber.e("## SAS Received invalid ready request")
// TODO should we cancel?
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")
// TODO cancel?
return
@ -691,15 +695,15 @@ internal class DefaultVerificationService @Inject constructor(
}
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
Timber.e("## SAS Received invalid ready request")
// TODO should we cancel?
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")
// TODO cancel?
return
@ -716,8 +720,9 @@ internal class DefaultVerificationService @Inject constructor(
// relates_to is in clear in encrypted payload
relatesTo = event.content.toModel<MessageRelationContent>()?.relatesTo
)
?.asValidObject()
if (doneReq == null || doneReq.isValid().not() || event.senderId == null) {
if (doneReq == null || event.senderId == null) {
// ignore
Timber.e("## SAS Received invalid Done request")
// TODO should we cancel?
@ -728,9 +733,9 @@ internal class DefaultVerificationService @Inject constructor(
}
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
Timber.e("## SAS Received invalid mac request")
return
@ -738,41 +743,41 @@ internal class DefaultVerificationService @Inject constructor(
handleMacReceived(event.senderId, macReq)
}
private fun handleMacReceived(senderId: String, macReq: VerificationInfoMac) {
private fun handleMacReceived(senderId: String, macReq: ValidVerificationInfoMac) {
Timber.v("## SAS Received $macReq")
val existing = getExistingTransaction(senderId, macReq.transactionID!!)
val existing = getExistingTransaction(senderId, macReq.transactionId)
if (existing == null) {
Timber.e("## SAS Received invalid Mac request")
return
}
if (existing is SASDefaultVerificationTransaction) {
existing.acceptVerificationEvent(senderId, macReq)
existing.onKeyVerificationMac(macReq)
} else {
// not other types known for now
}
}
private fun handleReadyReceived(senderId: String,
readyReq: VerificationInfoReady,
readyReq: ValidVerificationInfoReady,
transportCreator: (DefaultVerificationTransaction) -> VerificationTransport) {
val existingRequest = getExistingVerificationRequest(senderId)?.find { it.transactionId == readyReq.transactionID }
val existingRequest = getExistingVerificationRequest(senderId)?.find { it.transactionId == readyReq.transactionId }
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
}
val qrCodeData = readyReq.methods
// 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 {
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
val tx = DefaultQrCodeVerificationTransaction(
setDeviceVerificationAction,
readyReq.transactionID!!,
readyReq.transactionId,
senderId,
readyReq.fromDevice,
crossSigningService,
@ -886,10 +891,10 @@ internal class DefaultVerificationService @Inject constructor(
)
}
private fun handleDoneReceived(senderId: String, doneInfo: VerificationInfo) {
val existingRequest = getExistingVerificationRequest(senderId)?.find { it.transactionId == doneInfo.transactionID }
private fun handleDoneReceived(senderId: String, doneInfo: ValidVerificationDone) {
val existingRequest = getExistingVerificationRequest(senderId)?.find { it.transactionId == doneInfo.transactionId }
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
}
updatePendingRequest(existingRequest.copy(isSuccessful = true))
@ -975,15 +980,12 @@ internal class DefaultVerificationService @Inject constructor(
: PendingVerificationRequest {
Timber.i("## SAS Requesting verification to user: $otherUserId in room $roomId")
val requestsForUser = pendingRequests[otherUserId]
?: ArrayList<PendingVerificationRequest>().also {
pendingRequests[otherUserId] = it
}
val requestsForUser = pendingRequests.getOrPut(otherUserId) { mutableListOf() }
val transport = verificationTransportRoomMessageFactory.createTransport(roomId, null)
// Cancel existing pending requests?
requestsForUser.toImmutableList().forEach { existingRequest ->
requestsForUser.toList().forEach { existingRequest ->
existingRequest.transactionId?.let { tid ->
if (!existingRequest.isFinished) {
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(
ageLocalTs = System.currentTimeMillis(),
isIncoming = false,
roomId = roomId,
localID = localID,
localId = validLocalId,
otherUserId = otherUserId
)
@ -1019,7 +1021,7 @@ internal class DefaultVerificationService @Inject constructor(
}
.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
updatePendingRequest(verificationRequest.copy(
transactionId = syncedId,
@ -1039,15 +1041,12 @@ internal class DefaultVerificationService @Inject constructor(
Timber.i("## Requesting verification to user: $otherUserId with device list $otherDevices")
val targetDevices = otherDevices ?: cryptoService.getUserDevices(otherUserId).map { it.deviceId }
val requestsForUser = pendingRequests[otherUserId]
?: ArrayList<PendingVerificationRequest>().also {
pendingRequests[otherUserId] = it
}
val requestsForUser = pendingRequests.getOrPut(otherUserId) { mutableListOf() }
val transport = verificationTransportToDeviceFactory.createTransport(null)
// Cancel existing pending requests?
requestsForUser.toImmutableList().forEach { existingRequest ->
requestsForUser.toList().forEach { existingRequest ->
existingRequest.transactionId?.let { tid ->
if (!existingRequest.isFinished) {
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(
transactionId = localID,
transactionId = localId,
ageLocalTs = System.currentTimeMillis(),
isIncoming = false,
roomId = null,
localID = localID,
localId = localId,
otherUserId = otherUserId,
targetDevices = targetDevices
)
@ -1087,7 +1086,7 @@ internal class DefaultVerificationService @Inject constructor(
}
.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
updatePendingRequest(verificationRequest.copy(
// localId stays different
@ -1113,13 +1112,10 @@ internal class DefaultVerificationService @Inject constructor(
}
private fun updatePendingRequest(updated: PendingVerificationRequest) {
val requestsForUser = pendingRequests[updated.otherUserId]
?: ArrayList<PendingVerificationRequest>().also {
pendingRequests[updated.otherUserId] = it
}
val requestsForUser = pendingRequests.getOrPut(updated.otherUserId) { mutableListOf() }
val index = requestsForUser.indexOfFirst {
it.transactionId == updated.transactionId
|| it.transactionId == null && it.localID == updated.localID
|| it.transactionId == null && it.localId == updated.localId
}
if (index != -1) {
requestsForUser.removeAt(index)
@ -1186,7 +1182,7 @@ internal class DefaultVerificationService @Inject constructor(
CancelCode.User,
null // TODO handle error?
)
updatePendingRequest(existingRequest.copy(readyInfo = readyMsg))
updatePendingRequest(existingRequest.copy(readyInfo = readyMsg.asValidObject()))
return true
} else {
Timber.e("## SAS readyPendingVerificationInDMs Verification not found")
@ -1214,7 +1210,7 @@ internal class DefaultVerificationService @Inject constructor(
}
if (methods.isNullOrEmpty()) {
Timber.i("Cannot ready this request, no common methods found txId:$transactionId")
// TODO buttons should not be shown in this case?
// TODO buttons should not be shown in this case?
return false
}
// TODO this is not yet related to a transaction, maybe we should use another method like for cancel?
@ -1225,7 +1221,7 @@ internal class DefaultVerificationService @Inject constructor(
existingRequest.requestInfo?.fromDevice ?: "",
null // TODO handle error?
)
updatePendingRequest(existingRequest.copy(readyInfo = readyMsg))
updatePendingRequest(existingRequest.copy(readyInfo = readyMsg.asValidObject()))
return true
} else {
Timber.e("## SAS readyPendingVerification Verification not found")

View File

@ -15,12 +15,21 @@
*/
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.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
*/
internal abstract class DefaultVerificationTransaction(
private val setDeviceVerificationAction: SetDeviceVerificationAction,
private val crossSigningService: CrossSigningService,
private val userId: String,
override val transactionId: String,
override val otherUserId: String,
override var otherDeviceId: String? = null,
@ -42,5 +51,50 @@ internal abstract class DefaultVerificationTransaction(
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
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.verification.CancelCode
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.events.model.EventType
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.store.IMXCryptoStore
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.
*/
internal abstract class SASDefaultVerificationTransaction(
private val setDeviceVerificationAction: SetDeviceVerificationAction,
setDeviceVerificationAction: SetDeviceVerificationAction,
open val userId: String,
open val deviceId: String?,
private val cryptoStore: IMXCryptoStore,
private val crossSigningService: CrossSigningService,
crossSigningService: CrossSigningService,
private val deviceFingerprint: String,
transactionId: String,
otherUserId: String,
otherDeviceId: String?,
isIncoming: Boolean
) : DefaultVerificationTransaction(transactionId, otherUserId, otherDeviceId, isIncoming), SasVerificationTransaction {
) : DefaultVerificationTransaction(
setDeviceVerificationAction,
crossSigningService,
userId,
transactionId,
otherUserId,
otherDeviceId,
isIncoming),
SasVerificationTransaction {
companion object {
const val SAS_MAC_SHA256_LONGKDF = "hmac-sha256"
@ -89,15 +96,17 @@ internal abstract class SASDefaultVerificationTransaction(
private var olmSas: OlmSAS? = null
var startReq: VerificationInfoStart? = null
var accepted: VerificationInfoAccept? = null
var otherKey: String? = null
var shortCodeBytes: ByteArray? = null
// Visible for test
var startReq: ValidVerificationInfoStart.SasVerificationInfoStart? = null
// Visible for test
var accepted: ValidVerificationInfoAccept? = null
protected var otherKey: String? = null
protected var shortCodeBytes: ByteArray? = null
var myMac: VerificationInfoMac? = null
var theirMac: VerificationInfoMac? = null
protected var myMac: ValidVerificationInfoMac? = null
protected var theirMac: ValidVerificationInfoMac? = null
fun getSAS(): OlmSAS {
protected fun getSAS(): OlmSAS {
if (olmSas == null) olmSas = OlmSAS()
return olmSas!!
}
@ -177,7 +186,7 @@ internal abstract class SASDefaultVerificationTransaction(
}
val macMsg = transport.createMac(transactionId, keyMap, keyStrings)
myMac = macMsg
myMac = macMsg.asValidObject()
state = VerificationTxState.SendingMac
sendToOther(EventType.KEY_VERIFICATION_MAC, macMsg, VerificationTxState.MacSent, CancelCode.User) {
if (state == VerificationTxState.SendingMac) {
@ -187,9 +196,8 @@ internal abstract class SASDefaultVerificationTransaction(
}
// Do I already have their Mac?
if (theirMac != null) {
verifyMacs()
} // if not wait for it
theirMac?.let { verifyMacs(it) }
// if not wait for it
}
override fun shortCodeDoesNotMatch() {
@ -201,27 +209,15 @@ internal abstract class SASDefaultVerificationTransaction(
return transport is VerificationTransportToDevice
}
override fun acceptVerificationEvent(senderId: String, info: VerificationInfo) {
when (info) {
is VerificationInfoStart -> onVerificationStart(info)
is VerificationInfoAccept -> onVerificationAccept(info)
is VerificationInfoKey -> onKeyVerificationKey(info)
is VerificationInfoMac -> onKeyVerificationMac(info)
else -> {
// nop
}
}
}
abstract fun onVerificationStart(startReq: ValidVerificationInfoStart.SasVerificationInfoStart)
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() {
protected fun verifyMacs(theirMacSafe: ValidVerificationInfoMac) {
Timber.v("## SAS verifying macs for id:$transactionId")
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.
// 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.
val baseInfo = "MATRIX_KEY_VERIFICATION_MAC$otherUserId$otherDeviceId$userId$deviceId$transactionId"
val baseInfo = "MATRIX_KEY_VERIFICATION_MAC" +
otherUserId + otherDeviceId +
userId + deviceId +
transactionId
val commaSeparatedListOfKeyIds = theirMac!!.mac!!.keys.sorted().joinToString(",")
val commaSeparatedListOfKeyIds = theirMacSafe.mac.keys.sorted().joinToString(",")
val keyStrings = macUsingAgreedMethod(commaSeparatedListOfKeyIds, baseInfo + "KEY_IDS")
if (theirMac!!.keys != keyStrings) {
if (theirMacSafe.keys != keyStrings) {
// WRONG!
cancel(CancelCode.MismatchedKeys)
return
@ -250,7 +242,7 @@ internal abstract class SASDefaultVerificationTransaction(
val verifiedDevices = ArrayList<String>()
// cannot be empty because it has been validated
theirMac!!.mac!!.keys.forEach {
theirMacSafe.mac.keys.forEach {
val keyIDNoPrefix = it.withoutPrefix("ed25519:")
val otherDeviceKey = otherUserKnownDevices?.get(keyIDNoPrefix)?.fingerprint()
if (otherDeviceKey == null) {
@ -259,7 +251,7 @@ internal abstract class SASDefaultVerificationTransaction(
return@forEach
}
val mac = macUsingAgreedMethod(otherDeviceKey, baseInfo + it)
if (mac != theirMac?.mac?.get(it)) {
if (mac != theirMacSafe.mac[it]) {
// WRONG!
Timber.e("## SAS Verification: mac mismatch for $otherDeviceKey with id $keyIDNoPrefix")
cancel(CancelCode.MismatchedKeys)
@ -273,12 +265,12 @@ internal abstract class SASDefaultVerificationTransaction(
val otherCrossSigningMasterKeyPublic = otherMasterKey?.unpaddedBase64PublicKey
if (otherCrossSigningMasterKeyPublic != null) {
// Did the user signed his master key
theirMac!!.mac!!.keys.forEach {
theirMacSafe.mac.keys.forEach {
val keyIDNoPrefix = it.withoutPrefix("ed25519:")
if (keyIDNoPrefix == otherCrossSigningMasterKeyPublic) {
// Check the signature
val mac = macUsingAgreedMethod(otherCrossSigningMasterKeyPublic, baseInfo + it)
if (mac != theirMac?.mac?.get(it)) {
if (mac != theirMacSafe.mac.get(it)) {
// WRONG!
Timber.e("## SAS Verification: mac mismatch for MasterKey with id $keyIDNoPrefix")
cancel(CancelCode.MismatchedKeys)
@ -298,40 +290,9 @@ internal abstract class SASDefaultVerificationTransaction(
return
}
// If not me sign his MSK and upload the signature
if (otherMasterKeyIsVerified && 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, "## 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)
trust(otherMasterKeyIsVerified,
verifiedDevices,
eventuallyMarkMyMasterKeyAsTrusted = otherMasterKey?.trustLevel?.isVerified() == false)
}
override fun cancel() {
@ -343,11 +304,11 @@ internal abstract class SASDefaultVerificationTransaction(
transport.cancelTransaction(transactionId, otherUserId, otherDeviceId ?: "", code)
}
protected fun sendToOther(type: String,
keyToDevice: VerificationInfo,
nextState: VerificationTxState,
onErrorReason: CancelCode,
onDone: (() -> Unit)?) {
protected fun <T> sendToOther(type: String,
keyToDevice: VerificationInfo<T>,
nextState: VerificationTxState,
onErrorReason: CancelCode,
onDone: (() -> Unit)?) {
transport.sendToOther(type, keyToDevice, nextState, onErrorReason, onDone)
}
@ -369,11 +330,11 @@ internal abstract class SASDefaultVerificationTransaction(
}
override fun supportsEmoji(): Boolean {
return accepted?.shortAuthenticationStrings?.contains(SasMode.EMOJI) == true
return accepted?.shortAuthenticationStrings?.contains(SasMode.EMOJI).orFalse()
}
override fun supportsDecimal(): Boolean {
return accepted?.shortAuthenticationStrings?.contains(SasMode.DECIMAL) == true
return accepted?.shortAuthenticationStrings?.contains(SasMode.DECIMAL).orFalse()
}
protected fun hashUsingAgreedHashMethod(toHash: String): String? {
@ -386,7 +347,7 @@ internal abstract class SASDefaultVerificationTransaction(
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()) {
return getSAS().calculateMacLongKdf(message, info)
} 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
* 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 b1 = byteArray[1].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.internal.crypto.model.rest.SendToDeviceObject
interface VerificationInfo {
interface VerificationInfo<ValidObjectType> {
fun toEventContent(): Content? = null
fun toSendToDeviceObject(): SendToDeviceObject? = null
fun isValid(): Boolean
fun asValidObject(): ValidObjectType?
/**
* 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.
* Alices device should record this ID and use it in future messages in this transaction.
*/
val transactionID: String?
// TODO Refacto Put the relatesTo here or at least in Message sent in Room parent?
// val relatesTo: RelationDefaultContent?
val transactionId: String?
}

View File

@ -15,7 +15,7 @@
*/
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
*/
@ -41,6 +41,24 @@ internal interface VerificationInfoAccept : VerificationInfo {
* and the canonical JSON representation of the m.key.verification.start message.
*/
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 {
@ -52,3 +70,12 @@ internal interface VerificationInfoAcceptFactory {
messageAuthenticationCode: String,
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
internal interface VerificationInfoCancel : VerificationInfo {
internal interface VerificationInfoCancel : VerificationInfo<ValidVerificationInfoCancel> {
/**
* 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.
*/
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
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.
*/
internal interface VerificationInfoKey : VerificationInfo {
internal interface VerificationInfoKey : VerificationInfo<ValidVerificationInfoKey> {
/**
* The devices ephemeral public key, as an unpadded base64 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 {
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
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
*/
@ -28,8 +28,26 @@ internal interface VerificationInfoMac : VerificationInfo {
* give the MAC of the string ed25519:ABCDEFG,ed25519:HIJKLMN.
*/
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 {
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
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,
* 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.
*/
interface VerificationInfoReady : VerificationInfo {
internal interface VerificationInfoReady : VerificationInfo<ValidVerificationInfoReady> {
/**
* 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
*/
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 {

View File

@ -15,7 +15,9 @@
*/
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.
@ -33,4 +35,18 @@ interface VerificationInfoRequest : VerificationInfo {
* the message should be ignored by the receiver.
*/
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
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?
@ -57,5 +61,64 @@ internal interface VerificationInfoStart : VerificationInfo {
*/
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
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.VerificationTxState
@ -27,18 +28,18 @@ internal interface VerificationTransport {
/**
* Sends a message
*/
fun sendToOther(type: String,
verificationInfo: VerificationInfo,
nextState: VerificationTxState,
onErrorReason: CancelCode,
onDone: (() -> Unit)?)
fun <T> sendToOther(type: String,
verificationInfo: VerificationInfo<T>,
nextState: VerificationTxState,
onErrorReason: CancelCode,
onDone: (() -> Unit)?)
fun sendVerificationRequest(supportedMethods: List<String>,
localID: String,
localId: String,
otherUserId: String,
roomId: String?,
toDevices: List<String>?,
callback: (String?, VerificationInfoRequest?) -> Unit)
callback: (String?, ValidVerificationInfoRequest?) -> Unit)
fun cancelTransaction(transactionId: String,
otherUserId: String,
@ -64,7 +65,7 @@ internal interface VerificationTransport {
* Create start for SAS verification
*/
fun createStartForSas(fromDevice: String,
transactionID: String,
transactionId: String,
keyAgreementProtocols: List<String>,
hashes: List<String>,
messageAuthenticationCodes: List<String>,
@ -74,7 +75,7 @@ internal interface VerificationTransport {
* Create start for QR code verification
*/
fun createStartForQrCode(fromDevice: String,
transactionID: String,
transactionId: String,
sharedSecret: String): VerificationInfoStart
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.Operation
import androidx.work.WorkInfo
import com.zhuinden.monarchy.Monarchy
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.VerificationTxState
import im.vector.matrix.android.api.session.events.model.Content
@ -65,16 +65,15 @@ internal class VerificationTransportRoomMessage(
private val userId: String,
private val userDeviceId: String?,
private val roomId: String,
private val monarchy: Monarchy,
private val localEchoEventFactory: LocalEchoEventFactory,
private val tx: DefaultVerificationTransaction?
) : VerificationTransport {
override fun sendToOther(type: String,
verificationInfo: VerificationInfo,
nextState: VerificationTxState,
onErrorReason: CancelCode,
onDone: (() -> Unit)?) {
override fun <T> sendToOther(type: String,
verificationInfo: VerificationInfo<T>,
nextState: VerificationTxState,
onErrorReason: CancelCode,
onDone: (() -> Unit)?) {
Timber.d("## SAS sending msg type $type")
Timber.v("## SAS sending msg info $verificationInfo")
val event = createEventAndLocalEcho(
@ -138,26 +137,33 @@ internal class VerificationTransportRoomMessage(
}
override fun sendVerificationRequest(supportedMethods: List<String>,
localID: String,
localId: String,
otherUserId: String,
roomId: String?,
toDevices: List<String>?,
callback: (String?, VerificationInfoRequest?) -> Unit) {
callback: (String?, ValidVerificationInfoRequest?) -> Unit) {
Timber.d("## SAS sending verification request with supported methods: $supportedMethods")
// This transport requires a room
requireNotNull(roomId)
val validInfo = ValidVerificationInfoRequest(
transactionId = "",
fromDevice = userDeviceId ?: "",
methods = supportedMethods,
timestamp = System.currentTimeMillis()
)
val info = MessageVerificationRequestContent(
body = stringProvider.getString(R.string.key_verification_request_fallback_message, userId),
fromDevice = userDeviceId ?: "",
fromDevice = validInfo.fromDevice,
toUserId = otherUserId,
timestamp = System.currentTimeMillis(),
methods = supportedMethods
timestamp = validInfo.timestamp,
methods = validInfo.methods
)
val content = info.toContent()
val event = createEventAndLocalEcho(
localID,
localId,
EventType.MESSAGE,
roomId,
content
@ -192,8 +198,8 @@ internal class VerificationTransportRoomMessage(
?.let { wInfo ->
if (wInfo.outputData.getBoolean("failed", false)) {
callback(null, null)
} else if (wInfo.outputData.getString(localID) != null) {
callback(wInfo.outputData.getString(localID), info)
} else if (wInfo.outputData.getString(localId) != null) {
callback(wInfo.outputData.getString(localId), validInfo)
} else {
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 createStartForSas(fromDevice: String,
transactionID: String,
transactionId: String,
keyAgreementProtocols: List<String>,
hashes: List<String>,
messageAuthenticationCodes: List<String>,
@ -286,14 +292,14 @@ internal class VerificationTransportRoomMessage(
VERIFICATION_METHOD_SAS,
RelationDefaultContent(
type = RelationType.REFERENCE,
eventId = transactionID
eventId = transactionId
),
null
)
}
override fun createStartForQrCode(fromDevice: String,
transactionID: String,
transactionId: String,
sharedSecret: String): VerificationInfoStart {
return MessageVerificationStartContent(
fromDevice,
@ -304,7 +310,7 @@ internal class VerificationTransportRoomMessage(
VERIFICATION_METHOD_RECIPROCATE,
RelationDefaultContent(
type = RelationType.REFERENCE,
eventId = transactionID
eventId = transactionId
),
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(
roomId = roomId,
originServerTs = System.currentTimeMillis(),
senderId = userId,
eventId = localID,
eventId = localId,
type = type,
content = content,
unsignedData = UnsignedData(age = null, transactionId = localID)
unsignedData = UnsignedData(age = null, transactionId = localId)
).also {
localEchoEventFactory.createLocalEcho(it)
}
@ -347,7 +353,6 @@ internal class VerificationTransportRoomMessage(
internal class VerificationTransportRoomMessageFactory @Inject constructor(
private val workManagerProvider: WorkManagerProvider,
private val stringProvider: StringProvider,
private val monarchy: Monarchy,
@SessionId
private val sessionId: String,
@UserId
@ -357,6 +362,6 @@ internal class VerificationTransportRoomMessageFactory @Inject constructor(
private val localEchoEventFactory: LocalEchoEventFactory) {
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
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.VerificationTxState
import im.vector.matrix.android.api.session.events.model.EventType
@ -46,28 +47,34 @@ internal class VerificationTransportToDevice(
) : VerificationTransport {
override fun sendVerificationRequest(supportedMethods: List<String>,
localID: String,
localId: String,
otherUserId: String,
roomId: String?,
toDevices: List<String>?,
callback: (String?, VerificationInfoRequest?) -> Unit) {
callback: (String?, ValidVerificationInfoRequest?) -> Unit) {
Timber.d("## SAS sending verification request with supported methods: $supportedMethods")
val contentMap = MXUsersDevicesMap<Any>()
val keyReq = KeyVerificationRequest(
fromDevice = myDeviceId,
val validKeyReq = ValidVerificationInfoRequest(
transactionId = localId,
fromDevice = myDeviceId ?: "",
methods = supportedMethods,
timestamp = System.currentTimeMillis(),
transactionID = localID
timestamp = System.currentTimeMillis()
)
val keyReq = KeyVerificationRequest(
fromDevice = validKeyReq.fromDevice,
methods = validKeyReq.methods,
timestamp = validKeyReq.timestamp,
transactionId = validKeyReq.transactionId
)
toDevices?.forEach {
contentMap.setObject(otherUserId, it, keyReq)
}
sendToDeviceTask
.configureWith(SendToDeviceTask.Params(MessageType.MSGTYPE_VERIFICATION_REQUEST, contentMap, localID)) {
.configureWith(SendToDeviceTask.Params(MessageType.MSGTYPE_VERIFICATION_REQUEST, contentMap, localId)) {
this.callback = object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
Timber.v("## verification [$tx.transactionId] send toDevice request success")
callback.invoke(localID, keyReq)
callback.invoke(localId, validKeyReq)
}
override fun onFailure(failure: Throwable) {
@ -103,11 +110,11 @@ internal class VerificationTransportToDevice(
.executeBy(taskExecutor)
}
override fun sendToOther(type: String,
verificationInfo: VerificationInfo,
nextState: VerificationTxState,
onErrorReason: CancelCode,
onDone: (() -> Unit)?) {
override fun <T> sendToOther(type: String,
verificationInfo: VerificationInfo<T>,
nextState: VerificationTxState,
onErrorReason: CancelCode,
onDone: (() -> Unit)?) {
Timber.d("## SAS sending msg type $type")
Timber.v("## SAS sending msg info $verificationInfo")
val tx = tx ?: return
@ -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 createStartForSas(fromDevice: String,
transactionID: String,
transactionId: String,
keyAgreementProtocols: List<String>,
hashes: List<String>,
messageAuthenticationCodes: List<String>,
@ -205,7 +212,7 @@ internal class VerificationTransportToDevice(
return KeyVerificationStart(
fromDevice,
VERIFICATION_METHOD_SAS,
transactionID,
transactionId,
keyAgreementProtocols,
hashes,
messageAuthenticationCodes,
@ -214,12 +221,12 @@ internal class VerificationTransportToDevice(
}
override fun createStartForQrCode(fromDevice: String,
transactionID: String,
transactionId: String,
sharedSecret: String): VerificationInfoStart {
return KeyVerificationStart(
fromDevice,
VERIFICATION_METHOD_RECIPROCATE,
transactionID,
transactionId,
null,
null,
null,
@ -229,7 +236,7 @@ internal class VerificationTransportToDevice(
override fun createReady(tid: String, fromDevice: String, methods: List<String>): VerificationInfoReady {
return KeyVerificationReady(
transactionID = tid,
transactionId = tid,
fromDevice = fromDevice,
methods = methods
)

View File

@ -16,23 +16,22 @@
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.verification.CancelCode
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.events.model.EventType
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.verification.DefaultVerificationTransaction
import im.vector.matrix.android.internal.crypto.verification.VerificationInfo
import im.vector.matrix.android.internal.crypto.verification.VerificationInfoStart
import im.vector.matrix.android.internal.crypto.verification.ValidVerificationInfoStart
import im.vector.matrix.android.internal.util.exhaustive
import timber.log.Timber
internal class DefaultQrCodeVerificationTransaction(
private val setDeviceVerificationAction: SetDeviceVerificationAction,
setDeviceVerificationAction: SetDeviceVerificationAction,
override val transactionId: String,
override val otherUserId: String,
override var otherDeviceId: String?,
@ -43,7 +42,15 @@ internal class DefaultQrCodeVerificationTransaction(
val userId: String,
val deviceId: String,
override val isIncoming: Boolean
) : DefaultVerificationTransaction(transactionId, otherUserId, otherDeviceId, isIncoming), QrCodeVerificationTransaction {
) : DefaultVerificationTransaction(
setDeviceVerificationAction,
crossSigningService,
userId,
transactionId,
otherUserId,
otherDeviceId,
isIncoming),
QrCodeVerificationTransaction {
override val qrCodeText: String?
get() = qrCodeData?.toEncodedString()
@ -76,72 +83,95 @@ internal class DefaultQrCodeVerificationTransaction(
}
// check master key
val myMasterKey = crossSigningService.getUserCrossSigningKeys(userId)?.masterKey()?.unpaddedBase64PublicKey
var canTrustOtherUserMasterKey = false
// Check the other device view of my MSK
when (otherQrCodeData) {
is QrCodeData.VerifyingAnotherUser -> {
if (otherQrCodeData.otherUserMasterCrossSigningPublicKey
!= crossSigningService.getUserCrossSigningKeys(userId)?.masterKey()?.unpaddedBase64PublicKey) {
// key2 (aka otherUserMasterCrossSigningPublicKey) is what the one displaying the QR code (other user) think my MSK is.
// Let's check that it's correct
// If not -> Cancel
if (otherQrCodeData.otherUserMasterCrossSigningPublicKey != myMasterKey) {
Timber.d("## Verification QR: Invalid other master key ${otherQrCodeData.otherUserMasterCrossSigningPublicKey}")
cancel(CancelCode.MismatchedKeys)
return
} else Unit
}
is QrCodeData.SelfVerifyingMasterKeyTrusted -> {
if (otherQrCodeData.userMasterCrossSigningPublicKey
!= crossSigningService.getUserCrossSigningKeys(userId)?.masterKey()?.unpaddedBase64PublicKey) {
// key1 (aka userMasterCrossSigningPublicKey) is the session displaying the QR code view of our MSK.
// 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}")
cancel(CancelCode.MismatchedKeys)
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 -> {
if (otherQrCodeData.userMasterCrossSigningPublicKey
!= crossSigningService.getUserCrossSigningKeys(userId)?.masterKey()?.unpaddedBase64PublicKey) {
// key2 (aka userMasterCrossSigningPublicKey) is the session displaying the QR code view of our MSK.
// 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}")
cancel(CancelCode.MismatchedKeys)
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
val toVerifyDeviceIds = mutableListOf<String>()
var canTrustOtherUserMasterKey = false
// Check device key if available
// Let's now check the other user/device key material
when (otherQrCodeData) {
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
!= crossSigningService.getUserCrossSigningKeys(otherUserId)?.masterKey()?.unpaddedBase64PublicKey) {
Timber.d("## Verification QR: Invalid user master key ${otherQrCodeData.userMasterCrossSigningPublicKey}")
cancel(CancelCode.MismatchedKeys)
return
} else {
// It does so i should mark it as trusted
canTrustOtherUserMasterKey = true
Unit
}
}
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
!= cryptoStore.getUserDevice(userId, deviceId)?.fingerprint()) {
Timber.d("## Verification QR: Invalid other device key ${otherQrCodeData.otherDeviceKey}")
cancel(CancelCode.MismatchedKeys)
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 -> {
// 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
!= cryptoStore.getUserDevice(otherUserId, otherDeviceId ?: "")?.fingerprint()) {
Timber.d("## Verification QR: Invalid device key ${otherQrCodeData.deviceKey}")
cancel(CancelCode.MismatchedKeys)
return
} else {
toVerifyDeviceIds.add(otherQrCodeData.deviceKey)
// Yes it does -> i should trust it and sign then upload the signature
toVerifyDeviceIds.add(otherDeviceId ?: "")
Unit
}
}
}.exhaustive
if (!canTrustOtherUserMasterKey && toVerifyDeviceIds.isEmpty()) {
// Nothing to verify
// // Nothing to verify
cancel(CancelCode.MismatchedKeys)
return
}
@ -152,10 +182,12 @@ internal class DefaultQrCodeVerificationTransaction(
start(otherQrCodeData.sharedSecret)
// 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) {
Timber.e("## Verification QR: start verification from invalid state")
// should I cancel??
@ -177,9 +209,6 @@ internal class DefaultQrCodeVerificationTransaction(
)
}
override fun acceptVerificationEvent(senderId: String, info: VerificationInfo) {
}
override fun cancel() {
cancel(CancelCode.User)
}
@ -192,14 +221,14 @@ internal class DefaultQrCodeVerificationTransaction(
override fun isToDeviceTransport() = false
// 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) {
// Should not happen
cancel(CancelCode.UnexpectedMessage)
return
}
if (startReq.sharedSecret == qrCodeData.sharedSecret) {
if (startReq.sharedSecret.fromBase64Safe()?.contentEquals(qrCodeData.sharedSecret.fromBase64()) == true) {
// Ok, we can trust the other user
// We can only trust the master key in this case
// But first, ask the user for a confirmation
@ -211,7 +240,21 @@ internal class DefaultQrCodeVerificationTransaction(
}
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() {
@ -219,46 +262,4 @@ internal class DefaultQrCodeVerificationTransaction(
// At least remove the transaction...
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
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.extensions.toUnsignedInt
@ -52,15 +52,15 @@ fun QrCodeData.toEncodedString(): String {
}
// Keys
firstKey.fromBase64NoPadding().forEach {
firstKey.fromBase64().forEach {
result += it
}
secondKey.fromBase64NoPadding().forEach {
secondKey.fromBase64().forEach {
result += it
}
// Secret
sharedSecret.fromBase64NoPadding().forEach {
sharedSecret.fromBase64().forEach {
result += it
}
@ -94,11 +94,11 @@ fun String.toQrCodeData(): QrCodeData? {
val mode = byteArray[cursor].toInt()
cursor++
// Get transaction length
val bigEndian1 = byteArray[cursor].toUnsignedInt()
val bigEndian2 = byteArray[cursor + 1].toUnsignedInt()
// Get transaction length, Big Endian format
val msb = byteArray[cursor].toUnsignedInt()
val lsb = byteArray[cursor + 1].toUnsignedInt()
val transactionLength = bigEndian1 * 0x0100 + bigEndian2
val transactionLength = msb.shl(8) + lsb
cursor++
cursor++

View File

@ -25,3 +25,7 @@ annotation class SessionFilesDirectory
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
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 okhttp3.OkHttpClient
import org.matrix.olm.OlmManager
import java.io.File
@Component(modules = [MatrixModule::class, NetworkModule::class, AuthModule::class])
@MatrixScope
@ -52,6 +53,9 @@ internal interface MatrixComponent {
fun resources(): Resources
@CacheDirectory
fun cacheDir(): File
fun olmManager(): OlmManager
fun taskExecutor(): TaskExecutor

View File

@ -26,6 +26,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.android.asCoroutineDispatcher
import kotlinx.coroutines.asCoroutineDispatcher
import org.matrix.olm.OlmManager
import java.io.File
import java.util.concurrent.Executors
@Module
@ -49,6 +50,13 @@ internal object MatrixModule {
return context.resources
}
@JvmStatic
@Provides
@CacheDirectory
fun providesCacheDir(context: Context): File {
return context.cacheDir
}
@JvmStatic
@Provides
@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.internal.crypto.attachments.ElementToDecrypt
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.Unauthenticated
import im.vector.matrix.android.internal.extensions.foldToCallback
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.writeToFile
import kotlinx.coroutines.GlobalScope
@ -42,8 +42,10 @@ import java.io.IOException
import javax.inject.Inject
internal class DefaultFileService @Inject constructor(
@SessionCacheDirectory
@CacheDirectory
private val cacheDirectory: File,
@SessionCacheDirectory
private val sessionCacheDirectory: File,
private val contentUrlResolver: ContentUrlResolver,
@Unauthenticated
private val okHttpClient: OkHttpClient,
@ -62,60 +64,50 @@ internal class DefaultFileService @Inject constructor(
return GlobalScope.launch(coroutineDispatchers.main) {
withContext(coroutineDispatchers.io) {
Try {
val folder = getFolder(downloadMode, id)
val folder = File(sessionCacheDirectory, "MF")
if (!folder.exists()) {
folder.mkdirs()
}
File(folder, fileName)
}.flatMap { destFile ->
if (!destFile.exists() || downloadMode == FileService.DownloadMode.TO_EXPORT) {
Try {
val resolvedUrl = contentUrlResolver.resolveFullSize(url) ?: throw IllegalArgumentException("url is null")
if (!destFile.exists()) {
val resolvedUrl = contentUrlResolver.resolveFullSize(url) ?: return@flatMap Try.Failure(IllegalArgumentException("url is null"))
val request = Request.Builder()
.url(resolvedUrl)
.build()
val request = Request.Builder()
.url(resolvedUrl)
.build()
val response = okHttpClient.newCall(request).execute()
var inputStream = response.body?.byteStream()
Timber.v("Response size ${response.body?.contentLength()} - Stream available: ${inputStream?.available()}")
if (!response.isSuccessful
|| inputStream == null) {
throw IOException()
}
if (elementToDecrypt != null) {
Timber.v("## decrypt file")
inputStream = MXEncryptedAttachments.decryptAttachment(inputStream, elementToDecrypt)
?: throw IllegalStateException("Decryption error")
}
writeToFile(inputStream, destFile)
destFile
val response = okHttpClient.newCall(request).execute()
var inputStream = response.body?.byteStream()
Timber.v("Response size ${response.body?.contentLength()} - Stream available: ${inputStream?.available()}")
if (!response.isSuccessful || inputStream == null) {
return@flatMap Try.Failure(IOException())
}
} else {
Try.just(destFile)
if (elementToDecrypt != null) {
Timber.v("## decrypt file")
inputStream = MXEncryptedAttachments.decryptAttachment(inputStream, elementToDecrypt)
?: return@flatMap Try.Failure(IllegalStateException("Decryption error"))
}
writeToFile(inputStream, destFile)
}
Try.just(copyFile(destFile, downloadMode))
}
}
.foldToCallback(callback)
}.toCancelable()
}
private fun getFolder(downloadMode: FileService.DownloadMode, id: String): File {
private fun copyFile(file: File, downloadMode: FileService.DownloadMode): File {
return when (downloadMode) {
FileService.DownloadMode.FOR_INTERNAL_USE -> {
// Create dir tree (MF stands for Matrix File):
// <cache>/<sessionId>/MF/<md5(id)>/
val tmpFolderSession = File(cacheDirectory, "MF")
File(tmpFolderSession, id.md5())
}
FileService.DownloadMode.TO_EXPORT -> {
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
}
FileService.DownloadMode.TO_EXPORT ->
file.copyTo(File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), file.name), true)
FileService.DownloadMode.FOR_EXTERNAL_SHARE ->
file.copyTo(File(File(cacheDirectory, "ext_share"), file.name), true)
FileService.DownloadMode.FOR_INTERNAL_USE ->
file
}
.also { folder ->
if (!folder.exists()) {
folder.mkdirs()
}
}
}
}

View File

@ -20,6 +20,7 @@ import android.os.Handler
import android.os.Looper
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
import im.vector.matrix.android.internal.session.SessionScope
import timber.log.Timber
import javax.inject.Inject
@SessionScope
@ -33,7 +34,13 @@ internal class DefaultContentUploadStateTracker @Inject constructor() : ContentU
val listeners = listeners.getOrPut(key) { ArrayList() }
listeners.add(updateListener)
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) {
@ -79,7 +86,13 @@ internal class DefaultContentUploadStateTracker @Inject constructor() : ContentU
private fun updateState(key: String, state: ContentUploadStateTracker.State) {
states[key] = state
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,
val events: List<Event>,
val attachment: ContentAttachmentData,
val isRoomEncrypted: Boolean,
val isEncrypted: Boolean,
val compressBeforeSending: Boolean,
override val lastFailureMessage: String? = null
) : SessionWorkerParams
@ -90,9 +90,11 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
Timber.e(e)
notifyTracker(params) { contentUploadStateTracker.setFailure(it, e) }
return Result.success(
WorkerParamsFactory.toData(params.copy(
lastFailureMessage = e.localizedMessage
))
WorkerParamsFactory.toData(
params.copy(
lastFailureMessage = e.localizedMessage
)
)
)
}
.let { originalFile ->
@ -136,7 +138,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
}
try {
val contentUploadResponse = if (params.isRoomEncrypted) {
val contentUploadResponse = if (params.isEncrypted) {
Timber.v("Encrypt thumbnail")
notifyTracker(params) { contentUploadStateTracker.setEncryptingThumbnail(it) }
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
return try {
val contentUploadResponse = if (params.isRoomEncrypted) {
val contentUploadResponse = if (params.isEncrypted) {
Timber.v("Encrypt file")
notifyTracker(params) { contentUploadStateTracker.setEncrypting(it) }
val encryptionResult = MXEncryptedAttachments.encryptAttachment(FileInputStream(attachmentFile), attachment.mimeType)
val encryptionResult = MXEncryptedAttachments.encryptAttachment(FileInputStream(attachmentFile), attachment.getSafeMimeType())
uploadedFileEncryptedFileInfo = encryptionResult.encryptedFileInfo
fileUploader
.uploadByteArray(encryptionResult.encryptedByteArray, attachment.name, "application/octet-stream", progressListener)
} else {
fileUploader
.uploadFile(attachmentFile, attachment.name, attachment.mimeType, progressListener)
.uploadFile(attachmentFile, attachment.name, attachment.getSafeMimeType(), progressListener)
}
handleSuccess(params,
@ -226,7 +228,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
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))
}

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.getActions
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.util.Cancelable
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.GetPushRulesTask
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.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith
@ -42,6 +44,7 @@ internal class DefaultPushRuleService @Inject constructor(
private val getPushRulesTask: GetPushRulesTask,
private val updatePushRuleEnableStatusTask: UpdatePushRuleEnableStatusTask,
private val addPushRuleTask: AddPushRuleTask,
private val updatePushRuleActionsTask: UpdatePushRuleActionsTask,
private val removePushRuleTask: RemovePushRuleTask,
private val taskExecutor: TaskExecutor,
private val monarchy: Monarchy
@ -55,7 +58,7 @@ internal class DefaultPushRuleService @Inject constructor(
.executeBy(taskExecutor)
}
override fun getPushRules(scope: String): List<PushRule> {
override fun getPushRules(scope: String): RuleSet {
var contentRules: List<PushRule> = emptyList()
var overrideRules: 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 overrideRules + contentRules + roomRules + senderRules + underrideRules
return RuleSet(
content = contentRules,
override = overrideRules,
room = roomRules,
sender = senderRules,
underride = underrideRules
)
}
override fun updatePushRuleEnableStatus(kind: RuleKind, pushRule: PushRule, enabled: Boolean, callback: MatrixCallback<Unit>): Cancelable {
@ -111,6 +119,14 @@ internal class DefaultPushRuleService @Inject constructor(
.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 {
return removePushRuleTask
.configureWith(RemovePushRuleTask.Params(kind, pushRule)) {

View File

@ -40,24 +40,35 @@ import im.vector.matrix.android.internal.di.SerializeNulls
* }
* </code>
*/
@JsonClass(generateAdapter = true)
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")
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
@Json(name = "kind")
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")
val appId: String,
@ -88,15 +99,17 @@ internal data class JsonPusher(
/**
* 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")
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
// to any others with different user IDs.
// Otherwise, the homeserver must remove any other pushers with the same App ID and pushkey for different users.
// The default is false.
/**
* If true, the homeserver should add another pusher with the given pushkey and App ID in addition to any others
* with different user IDs. Otherwise, the homeserver must remove any other pushers with the same App ID and pushkey
* for different users.
* The default is false.
*/
@Json(name = "append")
val append: Boolean? = false
)

View File

@ -22,12 +22,14 @@ import com.squareup.moshi.JsonClass
internal data class JsonPusherData(
/**
* 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")
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")
val format: String? = null

View File

@ -47,6 +47,7 @@ internal interface PushRulesApi {
/**
* 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 ruleId the ruleId

View File

@ -72,6 +72,9 @@ internal abstract class PushersModule {
@Binds
abstract fun bindAddPushRuleTask(task: DefaultAddPushRuleTask): AddPushRuleTask
@Binds
abstract fun bindUpdatePushRuleActionTask(task: DefaultUpdatePushRuleActionsTask): UpdatePushRuleActionsTask
@Binds
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
* is matching an existing local echo.
*
* The transactionID is used as loc
* The transactionId is used as loc
*/
internal class LocalEchoEventFactory @Inject constructor(
@UserId private val userId: String,
@ -261,7 +261,7 @@ internal class LocalEchoEventFactory @Inject constructor(
msgType = MessageType.MSGTYPE_IMAGE,
body = attachment.name ?: "image",
info = ImageInfo(
mimeType = attachment.mimeType,
mimeType = attachment.getSafeMimeType(),
width = width?.toInt() ?: 0,
height = height?.toInt() ?: 0,
size = attachment.size.toInt()
@ -293,7 +293,7 @@ internal class LocalEchoEventFactory @Inject constructor(
msgType = MessageType.MSGTYPE_VIDEO,
body = attachment.name ?: "video",
videoInfo = VideoInfo(
mimeType = attachment.mimeType,
mimeType = attachment.getSafeMimeType(),
width = width,
height = height,
size = attachment.size,
@ -312,7 +312,7 @@ internal class LocalEchoEventFactory @Inject constructor(
msgType = MessageType.MSGTYPE_AUDIO,
body = attachment.name ?: "audio",
audioInfo = AudioInfo(
mimeType = attachment.mimeType?.takeIf { it.isNotBlank() } ?: "audio/mpeg",
mimeType = attachment.getSafeMimeType()?.takeIf { it.isNotBlank() } ?: "audio/mpeg",
size = attachment.size
),
url = attachment.path
@ -325,7 +325,7 @@ internal class LocalEchoEventFactory @Inject constructor(
msgType = MessageType.MSGTYPE_FILE,
body = attachment.name ?: "file",
info = FileInfo(
mimeType = attachment.mimeType?.takeIf { it.isNotBlank() }
mimeType = attachment.getSafeMimeType()?.takeIf { it.isNotBlank() }
?: "application/octet-stream",
size = attachment.size
),
@ -335,25 +335,25 @@ internal class LocalEchoEventFactory @Inject constructor(
}
private fun createEvent(roomId: String, content: Any? = null): Event {
val localID = LocalEcho.createLocalEchoId()
val localId = LocalEcho.createLocalEchoId()
return Event(
roomId = roomId,
originServerTs = dummyOriginServerTs(),
senderId = userId,
eventId = localID,
eventId = localId,
type = EventType.MESSAGE,
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 {
val localID = LocalEcho.createLocalEchoId()
val localId = LocalEcho.createLocalEchoId()
return Event(
roomId = roomId,
originServerTs = dummyOriginServerTs(),
senderId = userId,
eventId = localID,
eventId = localId,
type = EventType.MESSAGE,
content = MessageVerificationRequestContent(
body = stringProvider.getString(R.string.key_verification_request_fallback_message, userId),
@ -362,7 +362,7 @@ internal class LocalEchoEventFactory @Inject constructor(
timestamp = System.currentTimeMillis(),
methods = methods
).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 {
val localID = LocalEcho.createLocalEchoId()
val localId = LocalEcho.createLocalEchoId()
return Event(
roomId = roomId,
originServerTs = dummyOriginServerTs(),
senderId = userId,
eventId = localID,
eventId = localId,
type = EventType.REDACTION,
redacts = eventId,
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
} // nothing on initial sync
val rules = pushRuleService.getPushRules(RuleScope.GLOBAL)
val rules = pushRuleService.getPushRules(RuleScope.GLOBAL).getAllRules()
processEventForPushTask.execute(ProcessEventForPushTask.Params(roomsSyncResponse, rules))
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.versionMinor = 17
ext.versionMinor = 18
ext.versionPatch = 0
static def getGitTimestamp() {
@ -296,7 +296,7 @@ dependencies {
implementation 'com.airbnb.android:mvrx:1.3.0'
// Work
implementation "androidx.work:work-runtime-ktx:2.3.0-beta02"
implementation "androidx.work:work-runtime-ktx:2.3.3"
// Paging
implementation "androidx.paging:paging-runtime-ktx:2.1.1"

View File

@ -6,9 +6,15 @@
<issue id="MissingTranslation" severity="warning" />
<issue id="TypographyEllipsis" severity="error" />
<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 -->
<issue id="ButtonOrder" severity="error" />
<issue id="TextFields" severity="error" />
<!-- Layout -->
<issue id="UnknownIdInLayout" severity="error" />
@ -19,6 +25,7 @@
<issue id="InefficientWeight" severity="error" />
<issue id="DisableBaselineAlignment" severity="error" />
<issue id="ScrollViewSize" severity="error" />
<issue id="NegativeMargin" severity="error" />
<!-- RTL -->
<issue id="RtlEnabled" severity="error" />
@ -30,9 +37,22 @@
<issue id="SetTextI18n" severity="error" />
<issue id="ViewConstructor" severity="error" />
<issue id="UseValueOf" severity="error" />
<issue id="Recycle" severity="error" />
<issue id="KotlinPropertyAccess" severity="error" />
<!-- Ignore error from HtmlCompressor lib -->
<issue id="InvalidPackage">
<ignore path="**/htmlcompressor-1.4.jar"/>
<ignore path="**/htmlcompressor-1.4.jar" />
</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>

View File

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

View File

@ -57,7 +57,7 @@ object FcmHelper {
*
* @param activity the first launch Activity
*/
fun ensureFcmTokenIsRetrieved(activity: Activity, pushersManager: PushersManager) {
fun ensureFcmTokenIsRetrieved(activity: Activity, pushersManager: PushersManager, registerPusher: Boolean) {
// 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.features.settings.troubleshoot.NotificationTroubleshootTestManager
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.TestSystemSettings
import javax.inject.Inject
@ -28,7 +28,7 @@ import javax.inject.Inject
class NotificationTroubleshootTestManagerFactory @Inject constructor(private val testSystemSettings: TestSystemSettings,
private val testAccountSettings: TestAccountSettings,
private val testDeviceSettings: TestDeviceSettings,
private val testBingRulesSettings: TestBingRulesSettings,
private val testPushRulesSettings: TestPushRulesSettings,
private val testAutoStartBoot: TestAutoStartBoot,
private val testBackgroundRestrictions: TestBackgroundRestrictions) {
@ -37,7 +37,7 @@ class NotificationTroubleshootTestManagerFactory @Inject constructor(private val
mgr.addTest(testSystemSettings)
mgr.addTest(testAccountSettings)
mgr.addTest(testDeviceSettings)
mgr.addTest(testBingRulesSettings)
mgr.addTest(testPushRulesSettings)
mgr.addTest(testAutoStartBoot)
mgr.addTest(testBackgroundRestrictions)
return mgr

View File

@ -25,13 +25,13 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ProcessLifecycleOwner
import com.google.firebase.messaging.FirebaseMessagingService
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.events.model.Event
import im.vector.riotx.BuildConfig
import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder
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.features.badge.BadgeProxy
import im.vector.riotx.features.notifications.NotifiableEventResolver
@ -196,7 +196,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
description = "",
type = null,
timestamp = System.currentTimeMillis(),
soundName = BingRule.ACTION_VALUE_DEFAULT,
soundName = PushRule.ACTION_VALUE_DEFAULT,
isPushGatewayEvent = true
)
notificationDrawerManager.onNotifiableEventReceived(simpleNotifiableEvent)

View File

@ -68,7 +68,7 @@ object FcmHelper {
*
* @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))) {
// 'app should always check the device for a compatible Google Play services APK before accessing Google Play services features'
if (checkPlayServices(activity)) {
@ -76,7 +76,9 @@ object FcmHelper {
FirebaseInstanceId.getInstance().instanceId
.addOnSuccessListener(activity) { instanceIdResult ->
storeFcmToken(activity, instanceIdResult.token)
pushersManager.registerPusherWithFcmKey(instanceIdResult.token)
if (registerPusher) {
pushersManager.registerPusherWithFcmKey(instanceIdResult.token)
}
}
.addOnFailureListener(activity) { e -> Timber.e(e, "## ensureFcmTokenIsRetrieved() : failed") }
} catch (e: Throwable) {

View File

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

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