Cleanup QRCode v1

This commit is contained in:
Benoit Marty 2020-02-19 18:05:47 +01:00
parent 859b9e4f8e
commit f81eb298cb
6 changed files with 4 additions and 533 deletions

View file

@ -1,46 +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 androidx.test.ext.junit.runners.AndroidJUnit4
import im.vector.matrix.android.InstrumentedTest
import org.amshove.kluent.shouldBe
import org.amshove.kluent.shouldNotBeEqualTo
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 SharedSecretTest : InstrumentedTest {
@Test
fun testSharedSecretLengthCase() {
repeat(100) {
generateSharedSecret().length shouldBe 43
}
}
@Test
fun testSharedDiffCase() {
val sharedSecret1 = generateSharedSecret()
val sharedSecret2 = generateSharedSecret()
sharedSecret1 shouldNotBeEqualTo sharedSecret2
}
}

View file

@ -66,7 +66,6 @@ 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.store.IMXCryptoStore
import im.vector.matrix.android.internal.crypto.verification.qrcode.DefaultQrCodeVerificationTransaction import im.vector.matrix.android.internal.crypto.verification.qrcode.DefaultQrCodeVerificationTransaction
import im.vector.matrix.android.internal.crypto.verification.qrcode.QrCodeDataV2 import im.vector.matrix.android.internal.crypto.verification.qrcode.QrCodeDataV2
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.crypto.verification.qrcode.generateSharedSecretV2
import im.vector.matrix.android.internal.di.DeviceId import im.vector.matrix.android.internal.di.DeviceId
import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.di.UserId
@ -797,17 +796,17 @@ internal class DefaultVerificationService @Inject constructor(
return when { return when {
userId != otherUserId -> userId != otherUserId ->
createQrCodeDataForDistinctUser(requestId, otherUserId /*, otherDeviceId*/) createQrCodeDataForDistinctUser(requestId, otherUserId)
crossSigningService.isCrossSigningVerified() -> crossSigningService.isCrossSigningVerified() ->
// This is a self verification and I am the old device (Osborne2) // This is a self verification and I am the old device (Osborne2)
createQrCodeDataForVerifiedDevice(requestId, otherDeviceId) createQrCodeDataForVerifiedDevice(requestId, otherDeviceId)
else -> else ->
// This is a self verification and I am the new device (Dynabook) // 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?*/): QrCodeDataV2.VerifyingAnotherUser? { private fun createQrCodeDataForDistinctUser(requestId: String, otherUserId: String): QrCodeDataV2.VerifyingAnotherUser? {
val myMasterKey = crossSigningService.getMyCrossSigningKeys() val myMasterKey = crossSigningService.getMyCrossSigningKeys()
?.masterKey() ?.masterKey()
?.unpaddedBase64PublicKey ?.unpaddedBase64PublicKey
@ -824,25 +823,6 @@ internal class DefaultVerificationService @Inject constructor(
return null return null
} }
/* TODO Cleanup
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 QrCodeDataV2.VerifyingAnotherUser( return QrCodeDataV2.VerifyingAnotherUser(
transactionId = requestId, transactionId = requestId,
userMasterCrossSigningPublicKey = myMasterKey, userMasterCrossSigningPublicKey = myMasterKey,
@ -870,20 +850,6 @@ internal class DefaultVerificationService @Inject constructor(
return null return null
} }
/* TODO Cleanup
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 QrCodeDataV2.SelfVerifyingMasterKeyTrusted( return QrCodeDataV2.SelfVerifyingMasterKeyTrusted(
transactionId = requestId, transactionId = requestId,
userMasterCrossSigningPublicKey = myMasterKey, userMasterCrossSigningPublicKey = myMasterKey,
@ -893,7 +859,7 @@ internal class DefaultVerificationService @Inject constructor(
} }
// Create a QR code to display on the new device (Dynabook) // Create a QR code to display on the new device (Dynabook)
private fun createQrCodeDataForUnVerifiedDevice(requestId: String/*, otherDeviceId: String?*/): QrCodeDataV2.SelfVerifyingMasterKeyNotTrusted? { private fun createQrCodeDataForUnVerifiedDevice(requestId: String): QrCodeDataV2.SelfVerifyingMasterKeyNotTrusted? {
val myMasterKey = crossSigningService.getMyCrossSigningKeys() val myMasterKey = crossSigningService.getMyCrossSigningKeys()
?.masterKey() ?.masterKey()
?.unpaddedBase64PublicKey ?.unpaddedBase64PublicKey
@ -902,27 +868,12 @@ internal class DefaultVerificationService @Inject constructor(
return null return null
} }
/* TODO Cleanup
val myDeviceId = deviceId
?: run {
Timber.w("## Unable to get my deviceId")
return null
}
*/
val myDeviceKey = myDeviceInfoHolder.get().myDevice.fingerprint() val myDeviceKey = myDeviceInfoHolder.get().myDevice.fingerprint()
?: run { ?: run {
Timber.w("## Unable to get my fingerprint") Timber.w("## Unable to get my fingerprint")
return null return null
} }
/* TODO Cleanup
val otherDeviceKey = otherDeviceId
?.let {
cryptoStore.getUserDevice(userId, otherDeviceId)?.fingerprint()
}
*/
return QrCodeDataV2.SelfVerifyingMasterKeyNotTrusted( return QrCodeDataV2.SelfVerifyingMasterKeyNotTrusted(
transactionId = requestId, transactionId = requestId,
deviceKey = myDeviceKey, deviceKey = myDeviceKey,

View file

@ -1,133 +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 im.vector.matrix.android.api.MatrixPatterns
import im.vector.matrix.android.api.permalinks.PermalinkFactory
import java.net.URLDecoder
import java.net.URLEncoder
private const val ENCODING = "utf-8"
/**
* 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>
*/
// @Deprecated(message = "Use QrCodeDataV2")
fun QrCodeData.toUrl(): String {
return buildString {
append(PermalinkFactory.createPermalink(userId))
append("?request=")
append(URLEncoder.encode(requestId, ENCODING))
append("&action=")
append(URLEncoder.encode(action, ENCODING))
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))
}
}
}
// @Deprecated(message = "Use QrCodeDataV2")
fun String.toQrCodeData(): QrCodeData? {
if (!startsWith(PermalinkFactory.MATRIX_TO_URL_BASE)) {
return null
}
val fragment = substringAfter("#")
if (fragment.isEmpty()) {
return 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
)
}

View file

@ -1,46 +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
/**
* Ref: https://github.com/uhoreg/matrix-doc/blob/qr_key_verification/proposals/1543-qr_code_key_verification.md#qr-code-format
*/
//@Deprecated(message = "Use QrCodeDataV2")
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?
) {
companion object {
const val ACTION_VERIFY = "verify"
}
}

View file

@ -19,15 +19,6 @@ package im.vector.matrix.android.internal.crypto.verification.qrcode
import im.vector.matrix.android.internal.crypto.crosssigning.toBase64NoPadding import im.vector.matrix.android.internal.crypto.crosssigning.toBase64NoPadding
import java.security.SecureRandom import java.security.SecureRandom
fun generateSharedSecret(): String {
val secureRandom = SecureRandom()
// 256 bits long
val secretBytes = ByteArray(32)
secureRandom.nextBytes(secretBytes)
return secretBytes.toBase64NoPadding()
}
fun generateSharedSecretV2(): String { fun generateSharedSecretV2(): String {
val secureRandom = SecureRandom() val secureRandom = SecureRandom()

View file

@ -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"
}
}