Create QrCodeData class and method to convert to URL and vice versa, with TUs
This commit is contained in:
parent
81337d1624
commit
41c691f26c
@ -19,7 +19,7 @@ package im.vector.matrix.android.api.permalinks
|
|||||||
import im.vector.matrix.android.api.session.events.model.Event
|
import im.vector.matrix.android.api.session.events.model.Event
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Useful methods to create Matrix permalink.
|
* Useful methods to create Matrix permalink (matrix.to links).
|
||||||
*/
|
*/
|
||||||
object PermalinkFactory {
|
object PermalinkFactory {
|
||||||
|
|
||||||
@ -84,7 +84,17 @@ object PermalinkFactory {
|
|||||||
* @param id the id to escape
|
* @param id the id to escape
|
||||||
* @return the escaped id
|
* @return the escaped id
|
||||||
*/
|
*/
|
||||||
private fun escape(id: String): String {
|
internal fun escape(id: String): String {
|
||||||
return id.replace("/", "%2F")
|
return id.replace("/", "%2F")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unescape '/' in id
|
||||||
|
*
|
||||||
|
* @param id the id to escape
|
||||||
|
* @return the escaped id
|
||||||
|
*/
|
||||||
|
internal fun unescape(id: String): String {
|
||||||
|
return id.replace("%2F", "/")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,114 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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>...
|
||||||
|
* &verification_algorithms=<algorithm>
|
||||||
|
* &verification_key=<random-key-in-base64>
|
||||||
|
* &other_user_key=<master-key-in-base64>
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
fun QrCodeData.toUrl(): String {
|
||||||
|
return buildString {
|
||||||
|
append(PermalinkFactory.createPermalink(userId))
|
||||||
|
append("?request=")
|
||||||
|
append(PermalinkFactory.escape(requestId))
|
||||||
|
append("&action=verify")
|
||||||
|
|
||||||
|
for ((keyId, key) in keys) {
|
||||||
|
append("&key_$keyId=")
|
||||||
|
append(PermalinkFactory.escape(key))
|
||||||
|
}
|
||||||
|
|
||||||
|
append("&verification_algorithms=")
|
||||||
|
append(PermalinkFactory.escape(verificationAlgorithms))
|
||||||
|
append("&verification_key=")
|
||||||
|
append(PermalinkFactory.escape(verificationKey))
|
||||||
|
append("&other_user_key=")
|
||||||
|
append(PermalinkFactory.escape(otherUserKey))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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("="))
|
||||||
|
}.toMap()
|
||||||
|
|
||||||
|
if (keyValues["action"] != "verify") {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val requestId = keyValues["request"]
|
||||||
|
?.let { PermalinkFactory.unescape(it) }
|
||||||
|
?.takeIf { MatrixPatterns.isEventId(it) } ?: return null
|
||||||
|
val verificationAlgorithms = keyValues["verification_algorithms"] ?: return null
|
||||||
|
val verificationKey = keyValues["verification_key"] ?: return null
|
||||||
|
val otherUserKey = keyValues["other_user_key"] ?: return null
|
||||||
|
|
||||||
|
val keys = keyValues.keys
|
||||||
|
.filter { it.startsWith("key_") }
|
||||||
|
.map {
|
||||||
|
it.substringAfter("key_") to (keyValues[it] ?: return null)
|
||||||
|
}
|
||||||
|
.toMap()
|
||||||
|
|
||||||
|
return QrCodeData(
|
||||||
|
userId,
|
||||||
|
requestId,
|
||||||
|
keys,
|
||||||
|
verificationAlgorithms,
|
||||||
|
verificationKey,
|
||||||
|
otherUserKey
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
data class QrCodeData(
|
||||||
|
val userId: String,
|
||||||
|
// the event ID of the associated verification request event.
|
||||||
|
val requestId: 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.
|
||||||
|
val keys: Map<String, String>,
|
||||||
|
// algorithm
|
||||||
|
val verificationAlgorithms: String,
|
||||||
|
// random single-use shared secret in unpadded base64. It must be at least 256-bits long (43 characters when base64-encoded).
|
||||||
|
val verificationKey: 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
|
||||||
|
)
|
@ -0,0 +1,143 @@
|
|||||||
|
/*
|
||||||
|
* 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.shouldBeEqualTo
|
||||||
|
import org.amshove.kluent.shouldBeNull
|
||||||
|
import org.amshove.kluent.shouldNotBeNull
|
||||||
|
import org.junit.FixMethodOrder
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runners.MethodSorters
|
||||||
|
|
||||||
|
@FixMethodOrder(MethodSorters.JVM)
|
||||||
|
class QrCodeTest {
|
||||||
|
|
||||||
|
private val basicQrCodeData = QrCodeData(
|
||||||
|
userId = "@benoit:matrix.org",
|
||||||
|
requestId = "\$azertyazerty",
|
||||||
|
keys = mapOf(
|
||||||
|
"1" to "abcdef",
|
||||||
|
"2" to "ghijql"
|
||||||
|
),
|
||||||
|
verificationAlgorithms = "verificationAlgorithm",
|
||||||
|
verificationKey = "verificationKey",
|
||||||
|
otherUserKey = "otherUserKey"
|
||||||
|
)
|
||||||
|
|
||||||
|
private val basicUrl = "https://matrix.to/#/@benoit:matrix.org?request=\$azertyazerty&action=verify&key_1=abcdef&key_2=ghijql&verification_algorithms=verificationAlgorithm&verification_key=verificationKey&other_user_key=otherUserKey"
|
||||||
|
|
||||||
|
@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.verificationAlgorithms shouldBeEqualTo "verificationAlgorithm"
|
||||||
|
decodedData.verificationKey shouldBeEqualTo "verificationKey"
|
||||||
|
decodedData.otherUserKey shouldBeEqualTo "otherUserKey"
|
||||||
|
}
|
||||||
|
|
||||||
|
@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.verificationAlgorithms shouldBeEqualTo "verificationAlgorithm"
|
||||||
|
decodedData.verificationKey shouldBeEqualTo "verificationKey"
|
||||||
|
decodedData.otherUserKey shouldBeEqualTo "otherUserKey"
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testMissingActionCase() {
|
||||||
|
basicUrl.replace("&action=verify", "")
|
||||||
|
.toQrCodeData()
|
||||||
|
.shouldBeNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testBadActionCase() {
|
||||||
|
basicUrl.replace("&action=verify", "&action=confirm")
|
||||||
|
.toQrCodeData()
|
||||||
|
.shouldBeNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testBadRequestId() {
|
||||||
|
basicUrl.replace("\$azertyazerty", "@azertyazerty")
|
||||||
|
.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 testMissingVerificationAlgorithm() {
|
||||||
|
basicUrl.replace("&verification_algorithms=verificationAlgorithm", "")
|
||||||
|
.toQrCodeData()
|
||||||
|
.shouldBeNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testMissingVerificationKey() {
|
||||||
|
basicUrl.replace("&verification_key=verificationKey", "")
|
||||||
|
.toQrCodeData()
|
||||||
|
.shouldBeNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testMissingOtherUserKey() {
|
||||||
|
basicUrl.replace("&other_user_key=otherUserKey", "")
|
||||||
|
.toQrCodeData()
|
||||||
|
.shouldBeNull()
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user