Merge pull request #1037 from vector-im/feature/crosssigning_qr
Migrate to binary QR code verification (#994)
This commit is contained in:
commit
b6372df676
@ -9,7 +9,7 @@ Features ✨:
|
||||
- Sending image: image are sent to rooms with a reduced size. It's still possible to send original image file (#1010)
|
||||
|
||||
Improvements 🙌:
|
||||
-
|
||||
- Migrate to binary QR code verification (#994)
|
||||
|
||||
Bugfix 🐛:
|
||||
- Account creation: wrongly hints that an email can be used to create an account (#941)
|
||||
|
@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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.verification.qrcode
|
||||
|
||||
fun hexToByteArray(hex: String): ByteArray {
|
||||
// Remove all spaces
|
||||
return hex.replace(" ", "")
|
||||
.let {
|
||||
if (it.length % 2 != 0) "0$it" else it
|
||||
}
|
||||
.let {
|
||||
ByteArray(it.length / 2)
|
||||
.apply {
|
||||
for (i in this.indices) {
|
||||
val index = i * 2
|
||||
val v = it.substring(index, index + 2).toInt(16)
|
||||
this[i] = v.toByte()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,249 @@
|
||||
/*
|
||||
* 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.verification.qrcode
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import im.vector.matrix.android.InstrumentedTest
|
||||
import org.amshove.kluent.shouldBeNull
|
||||
import org.amshove.kluent.shouldEqual
|
||||
import org.amshove.kluent.shouldEqualTo
|
||||
import org.amshove.kluent.shouldNotBeNull
|
||||
import org.junit.FixMethodOrder
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.MethodSorters
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@FixMethodOrder(MethodSorters.JVM)
|
||||
class QrCodeTest : InstrumentedTest {
|
||||
|
||||
private val qrCode1 = QrCodeData.VerifyingAnotherUser(
|
||||
transactionId = "MaTransaction",
|
||||
userMasterCrossSigningPublicKey = "ktEwcUP6su1xh+GuE+CYkQ3H6W/DIl+ybHFdaEOrolU",
|
||||
otherUserMasterCrossSigningPublicKey = "TXluZKTZLvSRWOTPlOqLq534bA+/K4zLFKSu9cGLQaU",
|
||||
sharedSecret = "MTIzNDU2Nzg"
|
||||
)
|
||||
|
||||
private val value1 = "MATRIX\u0002\u0000\u0000\u000DMaTransaction\u0092Ñ0qCú²íq\u0087á®\u0013à\u0098\u0091\u000DÇéoÃ\"_²lq]hC«¢UMynd¤Ù.ô\u0091XäÏ\u0094ê\u008B«\u009Døl\u000F¿+\u008CË\u0014¤®õÁ\u008BA¥12345678"
|
||||
|
||||
private val qrCode2 = QrCodeData.SelfVerifyingMasterKeyTrusted(
|
||||
transactionId = "MaTransaction",
|
||||
userMasterCrossSigningPublicKey = "ktEwcUP6su1xh+GuE+CYkQ3H6W/DIl+ybHFdaEOrolU",
|
||||
otherDeviceKey = "TXluZKTZLvSRWOTPlOqLq534bA+/K4zLFKSu9cGLQaU",
|
||||
sharedSecret = "MTIzNDU2Nzg"
|
||||
)
|
||||
|
||||
private val value2 = "MATRIX\u0002\u0001\u0000\u000DMaTransaction\u0092Ñ0qCú²íq\u0087á®\u0013à\u0098\u0091\u000DÇéoÃ\"_²lq]hC«¢UMynd¤Ù.ô\u0091XäÏ\u0094ê\u008B«\u009Døl\u000F¿+\u008CË\u0014¤®õÁ\u008BA¥12345678"
|
||||
|
||||
private val qrCode3 = QrCodeData.SelfVerifyingMasterKeyNotTrusted(
|
||||
transactionId = "MaTransaction",
|
||||
deviceKey = "TXluZKTZLvSRWOTPlOqLq534bA+/K4zLFKSu9cGLQaU",
|
||||
userMasterCrossSigningPublicKey = "ktEwcUP6su1xh+GuE+CYkQ3H6W/DIl+ybHFdaEOrolU",
|
||||
sharedSecret = "MTIzNDU2Nzg"
|
||||
)
|
||||
|
||||
private val value3 = "MATRIX\u0002\u0002\u0000\u000DMaTransactionMynd¤Ù.ô\u0091XäÏ\u0094ê\u008B«\u009Døl\u000F¿+\u008CË\u0014¤®õÁ\u008BA¥\u0092Ñ0qCú²íq\u0087á®\u0013à\u0098\u0091\u000DÇéoÃ\"_²lq]hC«¢U12345678"
|
||||
|
||||
private val sharedSecretByteArray = "12345678".toByteArray(Charsets.ISO_8859_1)
|
||||
|
||||
private val tlx_byteArray = hexToByteArray("4d 79 6e 64 a4 d9 2e f4 91 58 e4 cf 94 ea 8b ab 9d f8 6c 0f bf 2b 8c cb 14 a4 ae f5 c1 8b 41 a5")
|
||||
|
||||
private val kte_byteArray = hexToByteArray("92 d1 30 71 43 fa b2 ed 71 87 e1 ae 13 e0 98 91 0d c7 e9 6f c3 22 5f b2 6c 71 5d 68 43 ab a2 55")
|
||||
|
||||
@Test
|
||||
fun testEncoding1() {
|
||||
qrCode1.toEncodedString() shouldEqual value1
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testEncoding2() {
|
||||
qrCode2.toEncodedString() shouldEqual value2
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testEncoding3() {
|
||||
qrCode3.toEncodedString() shouldEqual value3
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSymmetry1() {
|
||||
qrCode1.toEncodedString().toQrCodeData() shouldEqual qrCode1
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSymmetry2() {
|
||||
qrCode2.toEncodedString().toQrCodeData() shouldEqual qrCode2
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSymmetry3() {
|
||||
qrCode3.toEncodedString().toQrCodeData() shouldEqual qrCode3
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCase1() {
|
||||
val url = qrCode1.toEncodedString()
|
||||
|
||||
val byteArray = url.toByteArray(Charsets.ISO_8859_1)
|
||||
checkHeader(byteArray)
|
||||
|
||||
// Mode
|
||||
byteArray[7] shouldEqualTo 0
|
||||
|
||||
checkSizeAndTransaction(byteArray)
|
||||
|
||||
compareArray(byteArray.copyOfRange(23, 23 + 32), kte_byteArray)
|
||||
compareArray(byteArray.copyOfRange(23 + 32, 23 + 64), tlx_byteArray)
|
||||
|
||||
compareArray(byteArray.copyOfRange(23 + 64, byteArray.size), sharedSecretByteArray)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCase2() {
|
||||
val url = qrCode2.toEncodedString()
|
||||
|
||||
val byteArray = url.toByteArray(Charsets.ISO_8859_1)
|
||||
checkHeader(byteArray)
|
||||
|
||||
// Mode
|
||||
byteArray[7] shouldEqualTo 1
|
||||
|
||||
checkSizeAndTransaction(byteArray)
|
||||
compareArray(byteArray.copyOfRange(23, 23 + 32), kte_byteArray)
|
||||
compareArray(byteArray.copyOfRange(23 + 32, 23 + 64), tlx_byteArray)
|
||||
|
||||
compareArray(byteArray.copyOfRange(23 + 64, byteArray.size), sharedSecretByteArray)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCase3() {
|
||||
val url = qrCode3.toEncodedString()
|
||||
|
||||
val byteArray = url.toByteArray(Charsets.ISO_8859_1)
|
||||
checkHeader(byteArray)
|
||||
|
||||
// Mode
|
||||
byteArray[7] shouldEqualTo 2
|
||||
|
||||
checkSizeAndTransaction(byteArray)
|
||||
compareArray(byteArray.copyOfRange(23, 23 + 32), tlx_byteArray)
|
||||
compareArray(byteArray.copyOfRange(23 + 32, 23 + 64), kte_byteArray)
|
||||
|
||||
compareArray(byteArray.copyOfRange(23 + 64, byteArray.size), sharedSecretByteArray)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLongTransactionId() {
|
||||
// Size on two bytes (2_000 = 0x07D0)
|
||||
val longTransactionId = "PatternId_".repeat(200)
|
||||
|
||||
val qrCode = qrCode1.copy(transactionId = longTransactionId)
|
||||
|
||||
val result = qrCode.toEncodedString()
|
||||
val expected = value1.replace("\u0000\u000DMaTransaction", "\u0007\u00D0$longTransactionId")
|
||||
|
||||
result shouldEqual expected
|
||||
|
||||
// Reverse operation
|
||||
expected.toQrCodeData() shouldEqual qrCode
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAnyTransactionId() {
|
||||
for (qty in 0 until 0x1FFF step 200) {
|
||||
val longTransactionId = "a".repeat(qty)
|
||||
|
||||
val qrCode = qrCode1.copy(transactionId = longTransactionId)
|
||||
|
||||
// Symmetric operation
|
||||
qrCode.toEncodedString().toQrCodeData() shouldEqual qrCode
|
||||
}
|
||||
}
|
||||
|
||||
// Error cases
|
||||
@Test
|
||||
fun testErrorHeader() {
|
||||
value1.replace("MATRIX", "MOTRIX").toQrCodeData().shouldBeNull()
|
||||
value1.replace("MATRIX", "MATRI").toQrCodeData().shouldBeNull()
|
||||
value1.replace("MATRIX", "").toQrCodeData().shouldBeNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testErrorVersion() {
|
||||
value1.replace("MATRIX\u0002", "MATRIX\u0000").toQrCodeData().shouldBeNull()
|
||||
value1.replace("MATRIX\u0002", "MATRIX\u0001").toQrCodeData().shouldBeNull()
|
||||
value1.replace("MATRIX\u0002", "MATRIX\u0003").toQrCodeData().shouldBeNull()
|
||||
value1.replace("MATRIX\u0002", "MATRIX").toQrCodeData().shouldBeNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testErrorSecretTooShort() {
|
||||
value1.replace("12345678", "1234567").toQrCodeData().shouldBeNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testErrorNoTransactionNoKeyNoSecret() {
|
||||
// But keep transaction length
|
||||
"MATRIX\u0002\u0000\u0000\u000D".toQrCodeData().shouldBeNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testErrorNoKeyNoSecret() {
|
||||
"MATRIX\u0002\u0000\u0000\u000DMaTransaction".toQrCodeData().shouldBeNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testErrorTransactionLengthTooShort() {
|
||||
// In this case, the secret will be longer, so this is not an error, but it will lead to keys mismatch
|
||||
value1.replace("\u000DMaTransaction", "\u000CMaTransaction").toQrCodeData().shouldNotBeNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testErrorTransactionLengthTooBig() {
|
||||
value1.replace("\u000DMaTransaction", "\u000EMaTransaction").toQrCodeData().shouldBeNull()
|
||||
}
|
||||
|
||||
private fun compareArray(actual: ByteArray, expected: ByteArray) {
|
||||
actual.size shouldEqual expected.size
|
||||
|
||||
for (i in actual.indices) {
|
||||
actual[i] shouldEqualTo expected[i]
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkHeader(byteArray: ByteArray) {
|
||||
// MATRIX
|
||||
byteArray[0] shouldEqualTo 'M'.toByte()
|
||||
byteArray[1] shouldEqualTo 'A'.toByte()
|
||||
byteArray[2] shouldEqualTo 'T'.toByte()
|
||||
byteArray[3] shouldEqualTo 'R'.toByte()
|
||||
byteArray[4] shouldEqualTo 'I'.toByte()
|
||||
byteArray[5] shouldEqualTo 'X'.toByte()
|
||||
|
||||
// Version
|
||||
byteArray[6] shouldEqualTo 2
|
||||
}
|
||||
|
||||
private fun checkSizeAndTransaction(byteArray: ByteArray) {
|
||||
// Size
|
||||
byteArray[8] shouldEqualTo 0
|
||||
byteArray[9] shouldEqualTo 13
|
||||
|
||||
// Transaction
|
||||
byteArray.copyOfRange(10, 10 + "MaTransaction".length).toString(Charsets.ISO_8859_1) shouldEqual "MaTransaction"
|
||||
}
|
||||
}
|
@ -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.
|
||||
@ -32,14 +32,14 @@ class SharedSecretTest : InstrumentedTest {
|
||||
@Test
|
||||
fun testSharedSecretLengthCase() {
|
||||
repeat(100) {
|
||||
generateSharedSecret().length shouldBe 43
|
||||
generateSharedSecretV2().length shouldBe 11
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSharedDiffCase() {
|
||||
val sharedSecret1 = generateSharedSecret()
|
||||
val sharedSecret2 = generateSharedSecret()
|
||||
val sharedSecret1 = generateSharedSecretV2()
|
||||
val sharedSecret2 = generateSharedSecretV2()
|
||||
|
||||
sharedSecret1 shouldNotBeEqualTo sharedSecret2
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ package im.vector.matrix.android.api.session.room.model
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import im.vector.matrix.android.internal.session.room.VerificationState
|
||||
|
||||
/**
|
||||
* Contains an aggregated summary info of the references.
|
||||
@ -26,6 +27,6 @@ import com.squareup.moshi.JsonClass
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ReferencesAggregatedContent(
|
||||
// Verification status info for m.key.verification.request msgType events
|
||||
@Json(name = "verif_sum") val verificationSummary: String
|
||||
@Json(name = "verif_sum") val verificationState: VerificationState
|
||||
// Add more fields for future summary info.
|
||||
)
|
||||
|
@ -66,7 +66,7 @@ import im.vector.matrix.android.internal.crypto.model.rest.toValue
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
import im.vector.matrix.android.internal.crypto.verification.qrcode.DefaultQrCodeVerificationTransaction
|
||||
import im.vector.matrix.android.internal.crypto.verification.qrcode.QrCodeData
|
||||
import im.vector.matrix.android.internal.crypto.verification.qrcode.generateSharedSecret
|
||||
import im.vector.matrix.android.internal.crypto.verification.qrcode.generateSharedSecretV2
|
||||
import im.vector.matrix.android.internal.di.DeviceId
|
||||
import im.vector.matrix.android.internal.di.UserId
|
||||
import im.vector.matrix.android.internal.session.SessionScope
|
||||
@ -796,17 +796,17 @@ internal class DefaultVerificationService @Inject constructor(
|
||||
|
||||
return when {
|
||||
userId != otherUserId ->
|
||||
createQrCodeDataForDistinctUser(requestId, otherUserId, otherDeviceId)
|
||||
createQrCodeDataForDistinctUser(requestId, otherUserId)
|
||||
crossSigningService.isCrossSigningVerified() ->
|
||||
// This is a self verification and I am the old device (Osborne2)
|
||||
createQrCodeDataForVerifiedDevice(requestId, otherDeviceId)
|
||||
else ->
|
||||
// This is a self verification and I am the new device (Dynabook)
|
||||
createQrCodeDataForUnVerifiedDevice(requestId, otherDeviceId)
|
||||
createQrCodeDataForUnVerifiedDevice(requestId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createQrCodeDataForDistinctUser(requestId: String, otherUserId: String, otherDeviceId: String?): QrCodeData? {
|
||||
private fun createQrCodeDataForDistinctUser(requestId: String, otherUserId: String): QrCodeData.VerifyingAnotherUser? {
|
||||
val myMasterKey = crossSigningService.getMyCrossSigningKeys()
|
||||
?.masterKey()
|
||||
?.unpaddedBase64PublicKey
|
||||
@ -823,39 +823,16 @@ internal class DefaultVerificationService @Inject constructor(
|
||||
return null
|
||||
}
|
||||
|
||||
val myDeviceId = deviceId
|
||||
?: run {
|
||||
Timber.w("## Unable to get my deviceId")
|
||||
return null
|
||||
}
|
||||
|
||||
val myDeviceKey = myDeviceInfoHolder.get().myDevice.fingerprint()
|
||||
?: run {
|
||||
Timber.w("## Unable to get my fingerprint")
|
||||
return null
|
||||
}
|
||||
|
||||
val otherDeviceKey = otherDeviceId
|
||||
?.let {
|
||||
cryptoStore.getUserDevice(userId, otherDeviceId)?.fingerprint()
|
||||
}
|
||||
|
||||
return QrCodeData(
|
||||
userId = userId,
|
||||
requestId = requestId,
|
||||
action = QrCodeData.ACTION_VERIFY,
|
||||
keys = hashMapOf(
|
||||
myMasterKey to myMasterKey,
|
||||
myDeviceId to myDeviceKey
|
||||
),
|
||||
sharedSecret = generateSharedSecret(),
|
||||
otherUserKey = otherUserMasterKey,
|
||||
otherDeviceKey = otherDeviceKey
|
||||
return QrCodeData.VerifyingAnotherUser(
|
||||
transactionId = requestId,
|
||||
userMasterCrossSigningPublicKey = myMasterKey,
|
||||
otherUserMasterCrossSigningPublicKey = otherUserMasterKey,
|
||||
sharedSecret = generateSharedSecretV2()
|
||||
)
|
||||
}
|
||||
|
||||
// Create a QR code to display on the old device (Osborne2)
|
||||
private fun createQrCodeDataForVerifiedDevice(requestId: String, otherDeviceId: String?): QrCodeData? {
|
||||
private fun createQrCodeDataForVerifiedDevice(requestId: String, otherDeviceId: String?): QrCodeData.SelfVerifyingMasterKeyTrusted? {
|
||||
val myMasterKey = crossSigningService.getMyCrossSigningKeys()
|
||||
?.masterKey()
|
||||
?.unpaddedBase64PublicKey
|
||||
@ -873,34 +850,16 @@ internal class DefaultVerificationService @Inject constructor(
|
||||
return null
|
||||
}
|
||||
|
||||
val myDeviceId = deviceId
|
||||
?: run {
|
||||
Timber.w("## Unable to get my deviceId")
|
||||
return null
|
||||
}
|
||||
|
||||
val myDeviceKey = myDeviceInfoHolder.get().myDevice.fingerprint()
|
||||
?: run {
|
||||
Timber.w("## Unable to get my fingerprint")
|
||||
return null
|
||||
}
|
||||
|
||||
return QrCodeData(
|
||||
userId = userId,
|
||||
requestId = requestId,
|
||||
action = QrCodeData.ACTION_VERIFY,
|
||||
keys = hashMapOf(
|
||||
myMasterKey to myMasterKey,
|
||||
myDeviceId to myDeviceKey
|
||||
),
|
||||
sharedSecret = generateSharedSecret(),
|
||||
otherUserKey = null,
|
||||
otherDeviceKey = otherDeviceKey
|
||||
return QrCodeData.SelfVerifyingMasterKeyTrusted(
|
||||
transactionId = requestId,
|
||||
userMasterCrossSigningPublicKey = myMasterKey,
|
||||
otherDeviceKey = otherDeviceKey,
|
||||
sharedSecret = generateSharedSecretV2()
|
||||
)
|
||||
}
|
||||
|
||||
// Create a QR code to display on the new device (Dynabook)
|
||||
private fun createQrCodeDataForUnVerifiedDevice(requestId: String, otherDeviceId: String?): QrCodeData? {
|
||||
private fun createQrCodeDataForUnVerifiedDevice(requestId: String): QrCodeData.SelfVerifyingMasterKeyNotTrusted? {
|
||||
val myMasterKey = crossSigningService.getMyCrossSigningKeys()
|
||||
?.masterKey()
|
||||
?.unpaddedBase64PublicKey
|
||||
@ -909,34 +868,17 @@ internal class DefaultVerificationService @Inject constructor(
|
||||
return null
|
||||
}
|
||||
|
||||
val myDeviceId = deviceId
|
||||
?: run {
|
||||
Timber.w("## Unable to get my deviceId")
|
||||
return null
|
||||
}
|
||||
|
||||
val myDeviceKey = myDeviceInfoHolder.get().myDevice.fingerprint()
|
||||
?: run {
|
||||
Timber.w("## Unable to get my fingerprint")
|
||||
return null
|
||||
}
|
||||
|
||||
val otherDeviceKey = otherDeviceId
|
||||
?.let {
|
||||
cryptoStore.getUserDevice(userId, otherDeviceId)?.fingerprint()
|
||||
}
|
||||
|
||||
return QrCodeData(
|
||||
userId = userId,
|
||||
requestId = requestId,
|
||||
action = QrCodeData.ACTION_VERIFY,
|
||||
keys = hashMapOf(
|
||||
// Note: no master key here
|
||||
myDeviceId to myDeviceKey
|
||||
),
|
||||
sharedSecret = generateSharedSecret(),
|
||||
otherUserKey = myMasterKey,
|
||||
otherDeviceKey = otherDeviceKey
|
||||
return QrCodeData.SelfVerifyingMasterKeyNotTrusted(
|
||||
transactionId = requestId,
|
||||
deviceKey = myDeviceKey,
|
||||
userMasterCrossSigningPublicKey = myMasterKey,
|
||||
sharedSecret = generateSharedSecretV2()
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -28,7 +28,7 @@ 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.util.withoutPrefix
|
||||
import im.vector.matrix.android.internal.util.exhaustive
|
||||
import timber.log.Timber
|
||||
|
||||
internal class DefaultQrCodeVerificationTransaction(
|
||||
@ -46,7 +46,7 @@ internal class DefaultQrCodeVerificationTransaction(
|
||||
) : DefaultVerificationTransaction(transactionId, otherUserId, otherDeviceId, isIncoming), QrCodeVerificationTransaction {
|
||||
|
||||
override val qrCodeText: String?
|
||||
get() = qrCodeData?.toUrl()
|
||||
get() = qrCodeData?.toEncodedString()
|
||||
|
||||
override var state: VerificationTxState = VerificationTxState.None
|
||||
set(newState) {
|
||||
@ -69,89 +69,76 @@ internal class DefaultQrCodeVerificationTransaction(
|
||||
}
|
||||
|
||||
// Perform some checks
|
||||
if (otherQrCodeData.action != QrCodeData.ACTION_VERIFY) {
|
||||
Timber.d("## Verification QR: Invalid action ${otherQrCodeData.action}")
|
||||
cancel(CancelCode.QrCodeInvalid)
|
||||
return
|
||||
}
|
||||
|
||||
if (otherQrCodeData.userId != otherUserId) {
|
||||
Timber.d("## Verification QR: Mismatched user ${otherQrCodeData.userId}")
|
||||
cancel(CancelCode.MismatchedUser)
|
||||
return
|
||||
}
|
||||
|
||||
if (otherQrCodeData.requestId != transactionId) {
|
||||
Timber.d("## Verification QR: Invalid transaction actual ${otherQrCodeData.requestId} expected:$transactionId")
|
||||
if (otherQrCodeData.transactionId != transactionId) {
|
||||
Timber.d("## Verification QR: Invalid transaction actual ${otherQrCodeData.transactionId} expected:$transactionId")
|
||||
cancel(CancelCode.QrCodeInvalid)
|
||||
return
|
||||
}
|
||||
|
||||
// check master key
|
||||
if (otherQrCodeData.userId != userId
|
||||
&& otherQrCodeData.otherUserKey == null) {
|
||||
// Verification with other user, other_user_key is mandatory in this case
|
||||
Timber.d("## Verification QR: Invalid, missing other_user_key")
|
||||
cancel(CancelCode.QrCodeInvalid)
|
||||
return
|
||||
}
|
||||
|
||||
if (otherQrCodeData.otherUserKey != null
|
||||
&& otherQrCodeData.otherUserKey != crossSigningService.getUserCrossSigningKeys(userId)?.masterKey()?.unpaddedBase64PublicKey) {
|
||||
Timber.d("## Verification QR: Invalid other master key ${otherQrCodeData.otherUserKey}")
|
||||
cancel(CancelCode.MismatchedKeys)
|
||||
return
|
||||
}
|
||||
|
||||
// Check device key if available
|
||||
if (otherQrCodeData.otherDeviceKey != null
|
||||
&& otherQrCodeData.otherDeviceKey != cryptoStore.getUserDevice(userId, deviceId)?.fingerprint()) {
|
||||
Timber.d("## Verification QR: Invalid other device key")
|
||||
cancel(CancelCode.MismatchedKeys)
|
||||
return
|
||||
}
|
||||
when (otherQrCodeData) {
|
||||
is QrCodeData.VerifyingAnotherUser -> {
|
||||
if (otherQrCodeData.otherUserMasterCrossSigningPublicKey
|
||||
!= crossSigningService.getUserCrossSigningKeys(userId)?.masterKey()?.unpaddedBase64PublicKey) {
|
||||
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) {
|
||||
Timber.d("## Verification QR: Invalid other master key ${otherQrCodeData.userMasterCrossSigningPublicKey}")
|
||||
cancel(CancelCode.MismatchedKeys)
|
||||
return
|
||||
} else Unit
|
||||
}
|
||||
is QrCodeData.SelfVerifyingMasterKeyNotTrusted -> {
|
||||
if (otherQrCodeData.userMasterCrossSigningPublicKey
|
||||
!= crossSigningService.getUserCrossSigningKeys(userId)?.masterKey()?.unpaddedBase64PublicKey) {
|
||||
Timber.d("## Verification QR: Invalid other master key ${otherQrCodeData.userMasterCrossSigningPublicKey}")
|
||||
cancel(CancelCode.MismatchedKeys)
|
||||
return
|
||||
} else Unit
|
||||
}
|
||||
}.exhaustive
|
||||
|
||||
val toVerifyDeviceIds = mutableListOf<String>()
|
||||
var canTrustOtherUserMasterKey = false
|
||||
|
||||
val otherDevices = cryptoStore.getUserDevices(otherUserId)
|
||||
otherQrCodeData.keys.keys.forEach { key ->
|
||||
Timber.w("## Verification QR: Checking key $key")
|
||||
|
||||
when (val keyNoPrefix = key.withoutPrefix("ed25519:")) {
|
||||
otherQrCodeData.keys[key] -> {
|
||||
// Maybe master key?
|
||||
if (otherQrCodeData.keys[key] == crossSigningService.getUserCrossSigningKeys(otherUserId)?.masterKey()?.unpaddedBase64PublicKey) {
|
||||
canTrustOtherUserMasterKey = true
|
||||
} else {
|
||||
cancel(CancelCode.MismatchedKeys)
|
||||
return
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
when (val otherDevice = otherDevices?.get(keyNoPrefix)) {
|
||||
null -> {
|
||||
// Unknown device, ignore
|
||||
}
|
||||
else -> {
|
||||
when (otherDevice.fingerprint()) {
|
||||
null -> {
|
||||
// Ignore
|
||||
}
|
||||
otherQrCodeData.keys[key] -> {
|
||||
// Store the deviceId to verify after
|
||||
toVerifyDeviceIds.add(key)
|
||||
}
|
||||
else -> {
|
||||
cancel(CancelCode.MismatchedKeys)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Check device key if available
|
||||
when (otherQrCodeData) {
|
||||
is QrCodeData.VerifyingAnotherUser -> {
|
||||
if (otherQrCodeData.userMasterCrossSigningPublicKey
|
||||
!= crossSigningService.getUserCrossSigningKeys(otherUserId)?.masterKey()?.unpaddedBase64PublicKey) {
|
||||
Timber.d("## Verification QR: Invalid user master key ${otherQrCodeData.userMasterCrossSigningPublicKey}")
|
||||
cancel(CancelCode.MismatchedKeys)
|
||||
return
|
||||
} else {
|
||||
canTrustOtherUserMasterKey = true
|
||||
Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
is QrCodeData.SelfVerifyingMasterKeyTrusted -> {
|
||||
if (otherQrCodeData.otherDeviceKey
|
||||
!= cryptoStore.getUserDevice(userId, deviceId)?.fingerprint()) {
|
||||
Timber.d("## Verification QR: Invalid other device key ${otherQrCodeData.otherDeviceKey}")
|
||||
cancel(CancelCode.MismatchedKeys)
|
||||
return
|
||||
} else Unit
|
||||
}
|
||||
is QrCodeData.SelfVerifyingMasterKeyNotTrusted -> {
|
||||
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)
|
||||
Unit
|
||||
}
|
||||
}
|
||||
}.exhaustive
|
||||
|
||||
if (!canTrustOtherUserMasterKey && toVerifyDeviceIds.isEmpty()) {
|
||||
// Nothing to verify
|
||||
@ -164,13 +151,6 @@ internal class DefaultQrCodeVerificationTransaction(
|
||||
// qrCodeData.sharedSecret will be used to send the start request
|
||||
start(otherQrCodeData.sharedSecret)
|
||||
|
||||
val safeOtherDeviceId = otherDeviceId
|
||||
if (!otherQrCodeData.otherDeviceKey.isNullOrBlank()
|
||||
&& safeOtherDeviceId != null) {
|
||||
// Locally verify the device
|
||||
toVerifyDeviceIds.add(safeOtherDeviceId)
|
||||
}
|
||||
|
||||
// Trust the other user
|
||||
trust(canTrustOtherUserMasterKey, toVerifyDeviceIds.distinct())
|
||||
}
|
||||
@ -264,8 +244,8 @@ internal class DefaultQrCodeVerificationTransaction(
|
||||
|
||||
// TODO what if the otherDevice is not in this list? and should we
|
||||
toVerifyDeviceIds.forEach {
|
||||
setDeviceVerified(otherUserId, it)
|
||||
}
|
||||
setDeviceVerified(otherUserId, it)
|
||||
}
|
||||
transport.done(transactionId)
|
||||
state = VerificationTxState.Verified
|
||||
}
|
||||
|
@ -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.
|
||||
@ -16,116 +16,112 @@
|
||||
|
||||
package im.vector.matrix.android.internal.crypto.verification.qrcode
|
||||
|
||||
import im.vector.matrix.android.api.MatrixPatterns
|
||||
import im.vector.matrix.android.api.permalinks.PermalinkFactory
|
||||
import java.net.URLDecoder
|
||||
import java.net.URLEncoder
|
||||
import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64NoPadding
|
||||
import im.vector.matrix.android.internal.crypto.crosssigning.toBase64NoPadding
|
||||
import im.vector.matrix.android.internal.extensions.toUnsignedInt
|
||||
|
||||
private const val ENCODING = "utf-8"
|
||||
// MATRIX
|
||||
private val prefix = "MATRIX".toByteArray(Charsets.ISO_8859_1)
|
||||
|
||||
/**
|
||||
* Generate an URL to generate a QR code of the form:
|
||||
* <pre>
|
||||
* https://matrix.to/#/<user-id>?
|
||||
* request=<event-id>
|
||||
* &action=verify
|
||||
* &key_<keyid>=<key-in-base64>...
|
||||
* &secret=<shared_secret>
|
||||
* &other_user_key=<master-key-in-base64>
|
||||
* &other_device_key=<device-key-in-base64>
|
||||
*
|
||||
* Example:
|
||||
* https://matrix.to/#/@user:matrix.org?
|
||||
* request=%24pBeIfm7REDACTEDSQJbgqvi-yYiwmPB8_H_W_O974
|
||||
* &action=verify
|
||||
* &key_VJEDVKUYTQ=DL7LWIw7Qp%2B4AREDACTEDOwy2BjygumSWAGfzaWY
|
||||
* &key_fsh%2FfQ08N3xvh4ySXsINB%2BJ2hREDACTEDVcVOG4qqo=fsh%2FfQ08N3xvh4ySXsINB%2BJ2hREDACTEDVcVOG4qqo
|
||||
* &secret=AjQqw51Fp6UBuPolZ2FAD5WnXc22ZhJG6iGslrVvIdw%3D
|
||||
* &other_user_key=WqSVLkBCS%2Fi5NqRREDACTEDRPxBIuqK8Usl6Y3big
|
||||
* &other_device_key=WqSVLkBREDACTEDBsfszdvsdBEvefqsdcsfBvsfcsFb
|
||||
* </pre>
|
||||
*/
|
||||
fun QrCodeData.toUrl(): String {
|
||||
return buildString {
|
||||
append(PermalinkFactory.createPermalink(userId))
|
||||
append("?request=")
|
||||
append(URLEncoder.encode(requestId, ENCODING))
|
||||
append("&action=")
|
||||
append(URLEncoder.encode(action, ENCODING))
|
||||
fun QrCodeData.toEncodedString(): String {
|
||||
var result = ByteArray(0)
|
||||
|
||||
for ((keyId, key) in keys) {
|
||||
append("&key_${URLEncoder.encode(keyId, ENCODING)}=")
|
||||
append(URLEncoder.encode(key, ENCODING))
|
||||
}
|
||||
|
||||
append("&secret=")
|
||||
append(URLEncoder.encode(sharedSecret, ENCODING))
|
||||
|
||||
if (!otherUserKey.isNullOrBlank()) {
|
||||
append("&other_user_key=")
|
||||
append(URLEncoder.encode(otherUserKey, ENCODING))
|
||||
}
|
||||
if (!otherDeviceKey.isNullOrBlank()) {
|
||||
append("&other_device_key=")
|
||||
append(URLEncoder.encode(otherDeviceKey, ENCODING))
|
||||
}
|
||||
// MATRIX
|
||||
for (i in prefix.indices) {
|
||||
result += prefix[i]
|
||||
}
|
||||
|
||||
// Version
|
||||
result += 2
|
||||
|
||||
// Mode
|
||||
result += when (this) {
|
||||
is QrCodeData.VerifyingAnotherUser -> 0
|
||||
is QrCodeData.SelfVerifyingMasterKeyTrusted -> 1
|
||||
is QrCodeData.SelfVerifyingMasterKeyNotTrusted -> 2
|
||||
}.toByte()
|
||||
|
||||
// TransactionId length
|
||||
val length = transactionId.length
|
||||
result += ((length and 0xFF00) shr 8).toByte()
|
||||
result += length.toByte()
|
||||
|
||||
// TransactionId
|
||||
transactionId.forEach {
|
||||
result += it.toByte()
|
||||
}
|
||||
|
||||
// Keys
|
||||
firstKey.fromBase64NoPadding().forEach {
|
||||
result += it
|
||||
}
|
||||
secondKey.fromBase64NoPadding().forEach {
|
||||
result += it
|
||||
}
|
||||
|
||||
// Secret
|
||||
sharedSecret.fromBase64NoPadding().forEach {
|
||||
result += it
|
||||
}
|
||||
|
||||
return result.toString(Charsets.ISO_8859_1)
|
||||
}
|
||||
|
||||
fun String.toQrCodeData(): QrCodeData? {
|
||||
if (!startsWith(PermalinkFactory.MATRIX_TO_URL_BASE)) {
|
||||
val byteArray = toByteArray(Charsets.ISO_8859_1)
|
||||
|
||||
// Size should be min 6 + 1 + 1 + 2 + ? + 32 + 32 + ? = 74 + transactionLength + secretLength
|
||||
|
||||
// Check header
|
||||
// MATRIX
|
||||
if (byteArray.size < 10) return null
|
||||
|
||||
for (i in prefix.indices) {
|
||||
if (byteArray[i] != prefix[i]) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
var cursor = prefix.size // 6
|
||||
|
||||
// Version
|
||||
if (byteArray[cursor] != 2.toByte()) {
|
||||
return null
|
||||
}
|
||||
cursor++
|
||||
|
||||
// Get mode
|
||||
val mode = byteArray[cursor].toInt()
|
||||
cursor++
|
||||
|
||||
// Get transaction length
|
||||
val bigEndian1 = byteArray[cursor].toUnsignedInt()
|
||||
val bigEndian2 = byteArray[cursor + 1].toUnsignedInt()
|
||||
|
||||
val transactionLength = bigEndian1 * 0x0100 + bigEndian2
|
||||
|
||||
cursor++
|
||||
cursor++
|
||||
|
||||
val secretLength = byteArray.size - 74 - transactionLength
|
||||
|
||||
// ensure the secret length is 8 bytes min
|
||||
if (secretLength < 8) {
|
||||
return null
|
||||
}
|
||||
|
||||
val fragment = substringAfter("#")
|
||||
if (fragment.isEmpty()) {
|
||||
return null
|
||||
val transactionId = byteArray.copyOfRange(cursor, cursor + transactionLength).toString(Charsets.ISO_8859_1)
|
||||
cursor += transactionLength
|
||||
val key1 = byteArray.copyOfRange(cursor, cursor + 32).toBase64NoPadding()
|
||||
cursor += 32
|
||||
val key2 = byteArray.copyOfRange(cursor, cursor + 32).toBase64NoPadding()
|
||||
cursor += 32
|
||||
val secret = byteArray.copyOfRange(cursor, byteArray.size).toBase64NoPadding()
|
||||
|
||||
return when (mode) {
|
||||
0 -> QrCodeData.VerifyingAnotherUser(transactionId, key1, key2, secret)
|
||||
1 -> QrCodeData.SelfVerifyingMasterKeyTrusted(transactionId, key1, key2, secret)
|
||||
2 -> QrCodeData.SelfVerifyingMasterKeyNotTrusted(transactionId, key1, key2, secret)
|
||||
else -> null
|
||||
}
|
||||
|
||||
val safeFragment = fragment.substringBefore("?")
|
||||
|
||||
// we are limiting to 2 params
|
||||
val params = safeFragment
|
||||
.split(MatrixPatterns.SEP_REGEX.toRegex())
|
||||
.filter { it.isNotEmpty() }
|
||||
|
||||
if (params.size != 1) {
|
||||
return null
|
||||
}
|
||||
|
||||
val userId = params.getOrNull(0)
|
||||
?.let { PermalinkFactory.unescape(it) }
|
||||
?.takeIf { MatrixPatterns.isUserId(it) } ?: return null
|
||||
|
||||
val urlParams = fragment.substringAfter("?")
|
||||
.split("&".toRegex())
|
||||
.filter { it.isNotEmpty() }
|
||||
|
||||
val keyValues = urlParams.map {
|
||||
(it.substringBefore("=") to it.substringAfter("=").let { value -> URLDecoder.decode(value, ENCODING) })
|
||||
}.toMap()
|
||||
|
||||
val action = keyValues["action"]?.takeIf { it.isNotBlank() } ?: return null
|
||||
|
||||
val requestEventId = keyValues["request"]?.takeIf { it.isNotBlank() } ?: return null
|
||||
val sharedSecret = keyValues["secret"]?.takeIf { it.isNotBlank() } ?: return null
|
||||
val otherUserKey = keyValues["other_user_key"]
|
||||
val otherDeviceKey = keyValues["other_device_key"]
|
||||
|
||||
val keys = keyValues.keys
|
||||
.filter { it.startsWith("key_") }
|
||||
.map {
|
||||
URLDecoder.decode(it.substringAfter("key_"), ENCODING) to (keyValues[it] ?: return null)
|
||||
}
|
||||
.toMap()
|
||||
|
||||
return QrCodeData(
|
||||
userId,
|
||||
requestEventId,
|
||||
action,
|
||||
keys,
|
||||
sharedSecret,
|
||||
otherUserKey,
|
||||
otherDeviceKey
|
||||
)
|
||||
}
|
||||
|
@ -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.
|
||||
@ -19,27 +19,84 @@ package im.vector.matrix.android.internal.crypto.verification.qrcode
|
||||
/**
|
||||
* Ref: https://github.com/uhoreg/matrix-doc/blob/qr_key_verification/proposals/1543-qr_code_key_verification.md#qr-code-format
|
||||
*/
|
||||
data class QrCodeData(
|
||||
val userId: String,
|
||||
// Request Id. Can be an arbitrary value. In DM, it will be the event ID of the associated verification request event.
|
||||
val requestId: String,
|
||||
// The action
|
||||
val action: String,
|
||||
// key_<key_id>: each key that the user wants verified will have an entry of this form, where the value is the key in unpadded base64.
|
||||
// The QR code should contain at least the user's master cross-signing key. In the case where a device does not have a cross-signing key
|
||||
// (as in the case where a user logs in to a new device, and is verifying against another device), thin the QR code should contain at
|
||||
// least the device's key.
|
||||
val keys: Map<String, String>,
|
||||
// random single-use shared secret in unpadded base64. It must be at least 256-bits long (43 characters when base64-encoded).
|
||||
val sharedSecret: String,
|
||||
// the other user's master cross-signing key, in unpadded base64. In other words, if Alice is displaying the QR code,
|
||||
// this would be the copy of Bob's master cross-signing key that Alice has.
|
||||
val otherUserKey: String?,
|
||||
// The other device's key, in unpadded base64
|
||||
// This is only needed when a user is verifying their own devices, where the other device has not yet been signed with the cross-signing key.
|
||||
val otherDeviceKey: String?
|
||||
sealed class QrCodeData(
|
||||
/**
|
||||
* the event ID or transaction_id of the associated verification
|
||||
*/
|
||||
open val transactionId: String,
|
||||
/**
|
||||
* First key (32 bytes, in base64 no padding)
|
||||
*/
|
||||
val firstKey: String,
|
||||
/**
|
||||
* Second key (32 bytes, in base64 no padding)
|
||||
*/
|
||||
val secondKey: String,
|
||||
/**
|
||||
* a random shared secret (in base64 no padding)
|
||||
*/
|
||||
open val sharedSecret: String
|
||||
) {
|
||||
companion object {
|
||||
const val ACTION_VERIFY = "verify"
|
||||
}
|
||||
/**
|
||||
* verifying another user with cross-signing
|
||||
* QR code verification mode: 0x00
|
||||
*/
|
||||
data class VerifyingAnotherUser(
|
||||
override val transactionId: String,
|
||||
/**
|
||||
* the user's own master cross-signing public key
|
||||
*/
|
||||
val userMasterCrossSigningPublicKey: String,
|
||||
/**
|
||||
* what the device thinks the other user's master cross-signing key is
|
||||
*/
|
||||
val otherUserMasterCrossSigningPublicKey: String,
|
||||
override val sharedSecret: String
|
||||
) : QrCodeData(
|
||||
transactionId,
|
||||
userMasterCrossSigningPublicKey,
|
||||
otherUserMasterCrossSigningPublicKey,
|
||||
sharedSecret)
|
||||
|
||||
/**
|
||||
* self-verifying in which the current device does trust the master key
|
||||
* QR code verification mode: 0x01
|
||||
*/
|
||||
data class SelfVerifyingMasterKeyTrusted(
|
||||
override val transactionId: String,
|
||||
/**
|
||||
* the user's own master cross-signing public key
|
||||
*/
|
||||
val userMasterCrossSigningPublicKey: String,
|
||||
/**
|
||||
* what the device thinks the other device's device key is
|
||||
*/
|
||||
val otherDeviceKey: String,
|
||||
override val sharedSecret: String
|
||||
) : QrCodeData(
|
||||
transactionId,
|
||||
userMasterCrossSigningPublicKey,
|
||||
otherDeviceKey,
|
||||
sharedSecret)
|
||||
|
||||
/**
|
||||
* self-verifying in which the current device does not yet trust the master key
|
||||
* QR code verification mode: 0x02
|
||||
*/
|
||||
data class SelfVerifyingMasterKeyNotTrusted(
|
||||
override val transactionId: String,
|
||||
/**
|
||||
* the current device's device key
|
||||
*/
|
||||
val deviceKey: String,
|
||||
/**
|
||||
* what the device thinks the user's master cross-signing key is
|
||||
*/
|
||||
val userMasterCrossSigningPublicKey: String,
|
||||
override val sharedSecret: String
|
||||
) : QrCodeData(
|
||||
transactionId,
|
||||
deviceKey,
|
||||
userMasterCrossSigningPublicKey,
|
||||
sharedSecret)
|
||||
}
|
||||
|
@ -19,11 +19,11 @@ package im.vector.matrix.android.internal.crypto.verification.qrcode
|
||||
import im.vector.matrix.android.internal.crypto.crosssigning.toBase64NoPadding
|
||||
import java.security.SecureRandom
|
||||
|
||||
fun generateSharedSecret(): String {
|
||||
fun generateSharedSecretV2(): String {
|
||||
val secureRandom = SecureRandom()
|
||||
|
||||
// 256 bits long
|
||||
val secretBytes = ByteArray(32)
|
||||
// 8 bytes long
|
||||
val secretBytes = ByteArray(8)
|
||||
secureRandom.nextBytes(secretBytes)
|
||||
return secretBytes.toBase64NoPadding()
|
||||
}
|
||||
|
@ -73,6 +73,21 @@ fun VerificationState.isCanceled(): Boolean {
|
||||
return this == VerificationState.CANCELED_BY_ME || this == VerificationState.CANCELED_BY_OTHER
|
||||
}
|
||||
|
||||
// State transition with control
|
||||
private fun VerificationState?.toState(newState: VerificationState): VerificationState {
|
||||
// Cancel is always prioritary ?
|
||||
// Eg id i found that mac or keys mismatch and send a cancel and the other send a done, i have to
|
||||
// consider as canceled
|
||||
if (newState.isCanceled()) {
|
||||
return newState
|
||||
}
|
||||
// never move out of cancel
|
||||
if (this?.isCanceled() == true) {
|
||||
return this
|
||||
}
|
||||
return newState
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by EventRelationAggregationUpdater, when new events that can affect relations are inserted in base.
|
||||
*/
|
||||
@ -550,38 +565,26 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor(
|
||||
} else {
|
||||
ContentMapper.map(verifSummary.content)?.toModel<ReferencesAggregatedContent>()
|
||||
var data = ContentMapper.map(verifSummary.content)?.toModel<ReferencesAggregatedContent>()
|
||||
?: ReferencesAggregatedContent(VerificationState.REQUEST.name)
|
||||
?: ReferencesAggregatedContent(VerificationState.REQUEST)
|
||||
// TODO ignore invalid messages? e.g a START after a CANCEL?
|
||||
// i.e. never change state if already canceled/done
|
||||
val currentState = VerificationState.values().firstOrNull { data.verificationSummary == it.name }
|
||||
val currentState = data.verificationState
|
||||
val newState = when (event.getClearType()) {
|
||||
EventType.KEY_VERIFICATION_START -> {
|
||||
updateVerificationState(currentState, VerificationState.WAITING)
|
||||
}
|
||||
EventType.KEY_VERIFICATION_ACCEPT -> {
|
||||
updateVerificationState(currentState, VerificationState.WAITING)
|
||||
}
|
||||
EventType.KEY_VERIFICATION_READY -> {
|
||||
updateVerificationState(currentState, VerificationState.WAITING)
|
||||
}
|
||||
EventType.KEY_VERIFICATION_KEY -> {
|
||||
updateVerificationState(currentState, VerificationState.WAITING)
|
||||
}
|
||||
EventType.KEY_VERIFICATION_MAC -> {
|
||||
updateVerificationState(currentState, VerificationState.WAITING)
|
||||
}
|
||||
EventType.KEY_VERIFICATION_CANCEL -> {
|
||||
updateVerificationState(currentState, if (event.senderId == userId) {
|
||||
VerificationState.CANCELED_BY_ME
|
||||
} else VerificationState.CANCELED_BY_OTHER)
|
||||
}
|
||||
EventType.KEY_VERIFICATION_DONE -> {
|
||||
updateVerificationState(currentState, VerificationState.DONE)
|
||||
}
|
||||
EventType.KEY_VERIFICATION_START,
|
||||
EventType.KEY_VERIFICATION_ACCEPT,
|
||||
EventType.KEY_VERIFICATION_READY,
|
||||
EventType.KEY_VERIFICATION_KEY,
|
||||
EventType.KEY_VERIFICATION_MAC -> currentState.toState(VerificationState.WAITING)
|
||||
EventType.KEY_VERIFICATION_CANCEL -> currentState.toState(if (event.senderId == userId) {
|
||||
VerificationState.CANCELED_BY_ME
|
||||
} else {
|
||||
VerificationState.CANCELED_BY_OTHER
|
||||
})
|
||||
EventType.KEY_VERIFICATION_DONE -> currentState.toState(VerificationState.DONE)
|
||||
else -> VerificationState.REQUEST
|
||||
}
|
||||
|
||||
data = data.copy(verificationSummary = newState.name)
|
||||
data = data.copy(verificationState = newState)
|
||||
verifSummary.content = ContentMapper.map(data.toContent())
|
||||
}
|
||||
|
||||
@ -592,18 +595,4 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor(
|
||||
verifSummary.sourceEvents.add(event.eventId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateVerificationState(oldState: VerificationState?, newState: VerificationState): VerificationState {
|
||||
// Cancel is always prioritary ?
|
||||
// Eg id i found that mac or keys mismatch and send a cancel and the other send a done, i have to
|
||||
// consider as canceled
|
||||
if (newState == VerificationState.CANCELED_BY_OTHER || newState == VerificationState.CANCELED_BY_ME) {
|
||||
return newState
|
||||
}
|
||||
// never move out of cancel
|
||||
if (oldState == VerificationState.CANCELED_BY_OTHER || oldState == VerificationState.CANCELED_BY_ME) {
|
||||
return oldState
|
||||
}
|
||||
return newState
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,20 @@
|
||||
/*
|
||||
* 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.util
|
||||
|
||||
// Trick to ensure that when block is exhaustive
|
||||
internal val <T> T.exhaustive: T get() = this
|
@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 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.verification.qrcode
|
||||
|
||||
import org.amshove.kluent.shouldEqualTo
|
||||
import org.junit.FixMethodOrder
|
||||
import org.junit.Test
|
||||
import org.junit.runners.MethodSorters
|
||||
|
||||
@FixMethodOrder(MethodSorters.JVM)
|
||||
class BinaryStringTest {
|
||||
|
||||
/**
|
||||
* I want to put bytes to a String, and vice versa
|
||||
*/
|
||||
@Test
|
||||
fun testNominalCase() {
|
||||
val byteArray = ByteArray(256)
|
||||
for (i in byteArray.indices) {
|
||||
byteArray[i] = i.toByte() // Random.nextInt(255).toByte()
|
||||
}
|
||||
|
||||
val str = byteArray.toString(Charsets.ISO_8859_1)
|
||||
|
||||
str.length shouldEqualTo 256
|
||||
|
||||
// Ok convert back to bytearray
|
||||
|
||||
val result = str.toByteArray(Charsets.ISO_8859_1)
|
||||
|
||||
result.size shouldEqualTo 256
|
||||
|
||||
for (i in 0..255) {
|
||||
result[i] shouldEqualTo i.toByte()
|
||||
result[i] shouldEqualTo byteArray[i]
|
||||
}
|
||||
}
|
||||
}
|
@ -1,246 +0,0 @@
|
||||
/*
|
||||
* Copyright 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.verification.qrcode
|
||||
|
||||
import org.amshove.kluent.shouldBe
|
||||
import org.amshove.kluent.shouldBeEqualTo
|
||||
import org.amshove.kluent.shouldBeNull
|
||||
import org.amshove.kluent.shouldNotBeNull
|
||||
import org.junit.FixMethodOrder
|
||||
import org.junit.Test
|
||||
import org.junit.runners.MethodSorters
|
||||
|
||||
@Suppress("SpellCheckingInspection")
|
||||
@FixMethodOrder(MethodSorters.JVM)
|
||||
class QrCodeTest {
|
||||
|
||||
private val basicQrCodeData = QrCodeData(
|
||||
userId = "@benoit:matrix.org",
|
||||
requestId = "\$azertyazerty",
|
||||
action = QrCodeData.ACTION_VERIFY,
|
||||
keys = mapOf(
|
||||
"1" to "abcdef",
|
||||
"2" to "ghijql"
|
||||
),
|
||||
sharedSecret = "sharedSecret",
|
||||
otherUserKey = "otherUserKey",
|
||||
otherDeviceKey = "otherDeviceKey"
|
||||
)
|
||||
|
||||
private val basicUrl = "https://matrix.to/#/@benoit:matrix.org" +
|
||||
"?request=%24azertyazerty" +
|
||||
"&action=verify" +
|
||||
"&key_1=abcdef" +
|
||||
"&key_2=ghijql" +
|
||||
"&secret=sharedSecret" +
|
||||
"&other_user_key=otherUserKey" +
|
||||
"&other_device_key=otherDeviceKey"
|
||||
|
||||
@Test
|
||||
fun testNominalCase() {
|
||||
val url = basicQrCodeData.toUrl()
|
||||
|
||||
url shouldBeEqualTo basicUrl
|
||||
|
||||
val decodedData = url.toQrCodeData()
|
||||
|
||||
decodedData.shouldNotBeNull()
|
||||
|
||||
decodedData.userId shouldBeEqualTo "@benoit:matrix.org"
|
||||
decodedData.requestId shouldBeEqualTo "\$azertyazerty"
|
||||
decodedData.keys["1"]?.shouldBeEqualTo("abcdef")
|
||||
decodedData.keys["2"]?.shouldBeEqualTo("ghijql")
|
||||
decodedData.sharedSecret shouldBeEqualTo "sharedSecret"
|
||||
decodedData.otherUserKey?.shouldBeEqualTo("otherUserKey")
|
||||
decodedData.otherDeviceKey?.shouldBeEqualTo("otherDeviceKey")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSlashCase() {
|
||||
val url = basicQrCodeData
|
||||
.copy(
|
||||
userId = "@benoit/foo:matrix.org",
|
||||
requestId = "\$azertyazerty/bar"
|
||||
)
|
||||
.toUrl()
|
||||
|
||||
url shouldBeEqualTo basicUrl
|
||||
.replace("@benoit", "@benoit%2Ffoo")
|
||||
.replace("azertyazerty", "azertyazerty%2Fbar")
|
||||
|
||||
val decodedData = url.toQrCodeData()
|
||||
|
||||
decodedData.shouldNotBeNull()
|
||||
|
||||
decodedData.userId shouldBeEqualTo "@benoit/foo:matrix.org"
|
||||
decodedData.requestId shouldBeEqualTo "\$azertyazerty/bar"
|
||||
decodedData.keys["1"]?.shouldBeEqualTo("abcdef")
|
||||
decodedData.keys["2"]?.shouldBeEqualTo("ghijql")
|
||||
decodedData.sharedSecret shouldBeEqualTo "sharedSecret"
|
||||
decodedData.otherUserKey!! shouldBeEqualTo "otherUserKey"
|
||||
decodedData.otherDeviceKey!! shouldBeEqualTo "otherDeviceKey"
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNoOtherUserKey() {
|
||||
val url = basicQrCodeData
|
||||
.copy(
|
||||
otherUserKey = null
|
||||
)
|
||||
.toUrl()
|
||||
|
||||
url shouldBeEqualTo basicUrl
|
||||
.replace("&other_user_key=otherUserKey", "")
|
||||
|
||||
val decodedData = url.toQrCodeData()
|
||||
|
||||
decodedData.shouldNotBeNull()
|
||||
|
||||
decodedData.userId shouldBeEqualTo "@benoit:matrix.org"
|
||||
decodedData.requestId shouldBeEqualTo "\$azertyazerty"
|
||||
decodedData.keys["1"]?.shouldBeEqualTo("abcdef")
|
||||
decodedData.keys["2"]?.shouldBeEqualTo("ghijql")
|
||||
decodedData.sharedSecret shouldBeEqualTo "sharedSecret"
|
||||
decodedData.otherUserKey shouldBe null
|
||||
decodedData.otherDeviceKey?.shouldBeEqualTo("otherDeviceKey")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNoOtherDeviceKey() {
|
||||
val url = basicQrCodeData
|
||||
.copy(
|
||||
otherDeviceKey = null
|
||||
)
|
||||
.toUrl()
|
||||
|
||||
url shouldBeEqualTo basicUrl
|
||||
.replace("&other_device_key=otherDeviceKey", "")
|
||||
|
||||
val decodedData = url.toQrCodeData()
|
||||
|
||||
decodedData.shouldNotBeNull()
|
||||
|
||||
decodedData.userId shouldBeEqualTo "@benoit:matrix.org"
|
||||
decodedData.requestId shouldBeEqualTo "\$azertyazerty"
|
||||
decodedData.keys["1"]?.shouldBeEqualTo("abcdef")
|
||||
decodedData.keys["2"]?.shouldBeEqualTo("ghijql")
|
||||
decodedData.sharedSecret shouldBeEqualTo "sharedSecret"
|
||||
decodedData.otherUserKey?.shouldBeEqualTo("otherUserKey")
|
||||
decodedData.otherDeviceKey shouldBe null
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testUrlCharInKeys() {
|
||||
val url = basicQrCodeData
|
||||
.copy(
|
||||
keys = mapOf(
|
||||
"/=" to "abcdef",
|
||||
"&?" to "ghijql"
|
||||
)
|
||||
)
|
||||
.toUrl()
|
||||
|
||||
url shouldBeEqualTo basicUrl
|
||||
.replace("key_1=abcdef", "key_%2F%3D=abcdef")
|
||||
.replace("key_2=ghijql", "key_%26%3F=ghijql")
|
||||
|
||||
val decodedData = url.toQrCodeData()
|
||||
|
||||
decodedData.shouldNotBeNull()
|
||||
|
||||
decodedData.keys["/="]?.shouldBeEqualTo("abcdef")
|
||||
decodedData.keys["&&"]?.shouldBeEqualTo("ghijql")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMissingActionCase() {
|
||||
basicUrl.replace("&action=verify", "")
|
||||
.toQrCodeData()
|
||||
.shouldBeNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testEmptyActionCase() {
|
||||
basicUrl.replace("&action=verify", "&action=")
|
||||
.toQrCodeData()
|
||||
.shouldBeNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOtherActionCase() {
|
||||
basicUrl.replace("&action=verify", "&action=confirm")
|
||||
.toQrCodeData()
|
||||
?.action
|
||||
?.shouldBeEqualTo("confirm")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMissingRequestId() {
|
||||
basicUrl.replace("request=%24azertyazerty", "")
|
||||
.toQrCodeData()
|
||||
.shouldBeNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testEmptyRequestId() {
|
||||
basicUrl.replace("request=%24azertyazerty", "request=")
|
||||
.toQrCodeData()
|
||||
.shouldBeNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMissingUserId() {
|
||||
basicUrl.replace("@benoit:matrix.org", "")
|
||||
.toQrCodeData()
|
||||
.shouldBeNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testBadUserId() {
|
||||
basicUrl.replace("@benoit:matrix.org", "@benoit")
|
||||
.toQrCodeData()
|
||||
.shouldBeNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMissingSecret() {
|
||||
basicUrl.replace("&secret=sharedSecret", "")
|
||||
.toQrCodeData()
|
||||
.shouldBeNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testEmptySecret() {
|
||||
basicUrl.replace("&secret=sharedSecret", "&secret=")
|
||||
.toQrCodeData()
|
||||
.shouldBeNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSelfSigning() {
|
||||
// request is not an eventId in this case
|
||||
val url = "https://matrix.to/#/@benoit0815:matrix.org" +
|
||||
"?request=local.4dff40e1-7bf1-4e80-81ed-c6090d43bf20" +
|
||||
"&action=verify" +
|
||||
"&key_utbSRFcFjFDYf0KcNv3FoBHFSbvUPXtCYutuOg6WQ%2Bs=utbSRFcFjFDYf0KcNv3FoBHFSbvUPXtCYutuOg6WQ%2Bs" +
|
||||
"&key_YSOXZVBXIZ=F0XWqgUePgwm5HMYG3yhBNneHmscrAxxlooLHjy8YQc" +
|
||||
"&secret=LYVcEQmfdorbJ3vbQnq7nbNZc%2BGmDxUen1rByV9hRM4" +
|
||||
"&other_device_key=eGoUqZqAroCYpjp7FLGIkTEzYHBFED4uUAfJ267gqQQ"
|
||||
|
||||
url.toQrCodeData()!!.requestId shouldBeEqualTo "local.4dff40e1-7bf1-4e80-81ed-c6090d43bf20"
|
||||
}
|
||||
}
|
@ -378,8 +378,11 @@ dependencies {
|
||||
|
||||
// TESTS
|
||||
testImplementation 'junit:junit:4.12'
|
||||
testImplementation 'org.amshove.kluent:kluent-android:1.44'
|
||||
|
||||
androidTestImplementation 'androidx.test:runner:1.2.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||
androidTestImplementation 'org.amshove.kluent:kluent-android:1.44'
|
||||
}
|
||||
|
||||
if (getGradle().getStartParameter().getTaskRequests().toString().contains("Gplay")) {
|
||||
|
@ -37,6 +37,7 @@ import im.vector.riotx.core.utils.toast
|
||||
import im.vector.riotx.features.debug.sas.DebugSasEmojiActivity
|
||||
import im.vector.riotx.features.qrcode.QrCodeScannerActivity
|
||||
import kotlinx.android.synthetic.debug.activity_debug_menu.*
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class DebugMenuActivity : VectorBaseActivity() {
|
||||
@ -50,8 +51,19 @@ class DebugMenuActivity : VectorBaseActivity() {
|
||||
injector.inject(this)
|
||||
}
|
||||
|
||||
private lateinit var buffer: ByteArray
|
||||
|
||||
override fun initUiAndData() {
|
||||
renderQrCode("https://www.example.org")
|
||||
// renderQrCode("https://www.example.org")
|
||||
|
||||
buffer = ByteArray(256)
|
||||
for (i in buffer.indices) {
|
||||
buffer[i] = i.toByte()
|
||||
}
|
||||
|
||||
val string = buffer.toString(Charsets.ISO_8859_1)
|
||||
|
||||
renderQrCode(string)
|
||||
}
|
||||
|
||||
private fun renderQrCode(text: String) {
|
||||
@ -194,7 +206,20 @@ class DebugMenuActivity : VectorBaseActivity() {
|
||||
toast("QrCode: " + QrCodeScannerActivity.getResultText(data) + " is QRCode: " + QrCodeScannerActivity.getResultIsQrCode(data))
|
||||
|
||||
// Also update the current QR Code (reverse operation)
|
||||
renderQrCode(QrCodeScannerActivity.getResultText(data) ?: "")
|
||||
// renderQrCode(QrCodeScannerActivity.getResultText(data) ?: "")
|
||||
val result = QrCodeScannerActivity.getResultText(data)!!
|
||||
|
||||
if (result.length != buffer.size) {
|
||||
Timber.e("Error, length are not the same")
|
||||
} else {
|
||||
// Convert to ByteArray
|
||||
val byteArrayResult = result.toByteArray(Charsets.ISO_8859_1)
|
||||
for (i in byteArrayResult.indices) {
|
||||
if (buffer[i] != byteArrayResult[i]) {
|
||||
Timber.e("Error for byte $i, expecting ${buffer[i]} and get ${byteArrayResult[i]}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -104,11 +104,9 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
|
||||
}
|
||||
.toList(),
|
||||
referencesInfoData = event.annotations?.referencesAggregatedSummary?.let { referencesAggregatedSummary ->
|
||||
val stateStr = referencesAggregatedSummary.content.toModel<ReferencesAggregatedContent>()?.verificationSummary
|
||||
ReferencesInfoData(
|
||||
VerificationState.values().firstOrNull { stateStr == it.name }
|
||||
?: VerificationState.REQUEST
|
||||
)
|
||||
val verificationState = referencesAggregatedSummary.content.toModel<ReferencesAggregatedContent>()?.verificationState
|
||||
?: VerificationState.REQUEST
|
||||
ReferencesInfoData(verificationState)
|
||||
},
|
||||
sentByMe = event.root.senderId == session.myUserId
|
||||
)
|
||||
|
@ -31,6 +31,7 @@ import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.matrix.android.api.session.crypto.sas.VerificationService
|
||||
import im.vector.matrix.android.internal.session.room.VerificationState
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.extensions.exhaustive
|
||||
import im.vector.riotx.core.resources.ColorProvider
|
||||
import im.vector.riotx.core.utils.DebouncedClickListener
|
||||
import im.vector.riotx.features.home.AvatarRenderer
|
||||
@ -102,12 +103,7 @@ abstract class VerificationRequestItem : AbsBaseMessageItem<VerificationRequestI
|
||||
}
|
||||
holder.statusTextView.isVisible = true
|
||||
}
|
||||
else -> {
|
||||
holder.buttonBar.isVisible = false
|
||||
holder.statusTextView.text = null
|
||||
holder.statusTextView.isVisible = false
|
||||
}
|
||||
}
|
||||
}.exhaustive
|
||||
|
||||
// Always hide buttons if request is too old
|
||||
if (!VerificationService.isValidRequest(attributes.informationData.ageLocalTS)) {
|
||||
|
@ -22,6 +22,7 @@ import android.os.Bundle
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import com.google.zxing.Result
|
||||
import com.google.zxing.ResultMetadataType
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.di.ScreenComponent
|
||||
import im.vector.riotx.core.extensions.replaceFragment
|
||||
@ -43,15 +44,33 @@ class QrCodeScannerActivity : VectorBaseActivity() {
|
||||
}
|
||||
|
||||
fun setResultAndFinish(result: Result?) {
|
||||
result?.let {
|
||||
if (result != null) {
|
||||
val rawBytes = getRawBytes(result)
|
||||
val rawBytesStr = rawBytes?.toString(Charsets.ISO_8859_1)
|
||||
|
||||
setResult(RESULT_OK, Intent().apply {
|
||||
putExtra(EXTRA_OUT_TEXT, it.text)
|
||||
putExtra(EXTRA_OUT_IS_QR_CODE, it.barcodeFormat == BarcodeFormat.QR_CODE)
|
||||
putExtra(EXTRA_OUT_TEXT, rawBytesStr ?: result.text)
|
||||
putExtra(EXTRA_OUT_IS_QR_CODE, result.barcodeFormat == BarcodeFormat.QR_CODE)
|
||||
})
|
||||
}
|
||||
finish()
|
||||
}
|
||||
|
||||
// Copied from https://github.com/markusfisch/BinaryEye/blob/
|
||||
// 9d57889b810dcaa1a91d7278fc45c262afba1284/app/src/main/kotlin/de/markusfisch/android/binaryeye/activity/CameraActivity.kt#L434
|
||||
private fun getRawBytes(result: Result): ByteArray? {
|
||||
val metadata = result.resultMetadata ?: return null
|
||||
val segments = metadata[ResultMetadataType.BYTE_SEGMENTS] ?: return null
|
||||
var bytes = ByteArray(0)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
for (seg in segments as Iterable<ByteArray>) {
|
||||
bytes += seg
|
||||
}
|
||||
// byte segments can never be shorter than the text.
|
||||
// Zxing cuts off content prefixes like "WIFI:"
|
||||
return if (bytes.size >= result.text.length) bytes else null
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val EXTRA_OUT_TEXT = "EXTRA_OUT_TEXT"
|
||||
private const val EXTRA_OUT_IS_QR_CODE = "EXTRA_OUT_IS_QR_CODE"
|
||||
|
Loading…
x
Reference in New Issue
Block a user