From 41c691f26c2765671b0ae158715c380f55eab7bc Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 22 Jan 2020 17:58:25 +0100 Subject: [PATCH] Create QrCodeData class and method to convert to URL and vice versa, with TUs --- .../api/permalinks/PermalinkFactory.kt | 14 +- .../crypto/verification/qrcode/Extensions.kt | 114 ++++++++++++++ .../crypto/verification/qrcode/QrCodeData.kt | 36 +++++ .../crypto/verification/qrcode/QrCodeTest.kt | 143 ++++++++++++++++++ 4 files changed, 305 insertions(+), 2 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/qrcode/Extensions.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/qrcode/QrCodeData.kt create mode 100644 matrix-sdk-android/src/test/java/im/vector/matrix/android/internal/crypto/verification/qrcode/QrCodeTest.kt diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/PermalinkFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/PermalinkFactory.kt index 1af77869ee..03c5149e6b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/PermalinkFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/permalinks/PermalinkFactory.kt @@ -19,7 +19,7 @@ package im.vector.matrix.android.api.permalinks 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 { @@ -84,7 +84,17 @@ object PermalinkFactory { * @param id the id to escape * @return the escaped id */ - private fun escape(id: String): String { + internal fun escape(id: String): String { 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", "/") + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/qrcode/Extensions.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/qrcode/Extensions.kt new file mode 100644 index 0000000000..a2fc5e688c --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/qrcode/Extensions.kt @@ -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: + *
+ * https://matrix.to/#/?
+ *     request=
+ *     &action=verify
+ *     &key_=...
+ *     &verification_algorithms=
+ *     &verification_key=
+ *     &other_user_key=
+ * 
+ */ +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 + ) +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/qrcode/QrCodeData.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/qrcode/QrCodeData.kt new file mode 100644 index 0000000000..9b97deb7ea --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/qrcode/QrCodeData.kt @@ -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_: 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, + // 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 +) diff --git a/matrix-sdk-android/src/test/java/im/vector/matrix/android/internal/crypto/verification/qrcode/QrCodeTest.kt b/matrix-sdk-android/src/test/java/im/vector/matrix/android/internal/crypto/verification/qrcode/QrCodeTest.kt new file mode 100644 index 0000000000..022dd76fb4 --- /dev/null +++ b/matrix-sdk-android/src/test/java/im/vector/matrix/android/internal/crypto/verification/qrcode/QrCodeTest.kt @@ -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() + } +}