crypto: Update to the latest rust-sdk version

This commit is contained in:
Damir Jelić 2021-11-17 13:22:15 +01:00
parent a3af73261c
commit 50cdbaf041
9 changed files with 105 additions and 57 deletions

View File

@ -47,7 +47,6 @@ import org.matrix.android.sdk.internal.session.sync.model.DeviceListResponse
import org.matrix.android.sdk.internal.session.sync.model.DeviceOneTimeKeysCountSyncResponse import org.matrix.android.sdk.internal.session.sync.model.DeviceOneTimeKeysCountSyncResponse
import org.matrix.android.sdk.internal.session.sync.model.ToDeviceSyncResponse import org.matrix.android.sdk.internal.session.sync.model.ToDeviceSyncResponse
import timber.log.Timber import timber.log.Timber
import uniffi.olm.BackupKey
import uniffi.olm.BackupKeys import uniffi.olm.BackupKeys
import uniffi.olm.CrossSigningKeyExport import uniffi.olm.CrossSigningKeyExport
import uniffi.olm.CrossSigningStatus import uniffi.olm.CrossSigningStatus
@ -56,6 +55,7 @@ import uniffi.olm.DecryptionException
import uniffi.olm.DeviceLists import uniffi.olm.DeviceLists
import uniffi.olm.KeyRequestPair import uniffi.olm.KeyRequestPair
import uniffi.olm.Logger import uniffi.olm.Logger
import uniffi.olm.MegolmV1BackupKey
import uniffi.olm.Request import uniffi.olm.Request
import uniffi.olm.RequestType import uniffi.olm.RequestType
import uniffi.olm.RoomKeyCounts import uniffi.olm.RoomKeyCounts
@ -790,10 +790,10 @@ internal class OlmMachine(
} }
@Throws(CryptoStoreException::class) @Throws(CryptoStoreException::class)
suspend fun enableBackup(key: String, version: String) { suspend fun enableBackupV1(key: String, version: String) {
return withContext(Dispatchers.Default) { return withContext(Dispatchers.Default) {
val backupKey = BackupKey(key, mapOf(), null) val backupKey = MegolmV1BackupKey(key, mapOf(), null, MXCRYPTO_ALGORITHM_MEGOLM_BACKUP)
inner.enableBackup(backupKey, version) inner.enableBackupV1(backupKey, version)
} }
} }

View File

@ -125,7 +125,7 @@ internal class RustKeyBackupService @Inject constructor(
BackupRecoveryKey() BackupRecoveryKey()
} }
val publicKey = key.publicKey() val publicKey = key.megolmV1PublicKey()
val backupAuthData = SignalableMegolmBackupAuthData( val backupAuthData = SignalableMegolmBackupAuthData(
publicKey = publicKey.publicKey, publicKey = publicKey.publicKey,
privateKeySalt = publicKey.passphraseInfo?.privateKeySalt, privateKeySalt = publicKey.passphraseInfo?.privateKeySalt,
@ -144,7 +144,7 @@ internal class RustKeyBackupService @Inject constructor(
) )
MegolmBackupCreationInfo( MegolmBackupCreationInfo(
algorithm = MXCRYPTO_ALGORITHM_MEGOLM_BACKUP, algorithm = publicKey.backupAlgorithm,
authData = signedMegolmBackupAuthData, authData = signedMegolmBackupAuthData,
recoveryKey = key.toBase58() recoveryKey = key.toBase58()
) )
@ -264,7 +264,8 @@ internal class RustKeyBackupService @Inject constructor(
override fun backupAllGroupSessions(progressListener: ProgressListener?, override fun backupAllGroupSessions(progressListener: ProgressListener?,
callback: MatrixCallback<Unit>?) { callback: MatrixCallback<Unit>?) {
// This is only used in tests? // This is only used in tests? While it's fine have methods that are
// only used for tests, this one has a lot of logic that is nowhere else used.
TODO() TODO()
} }
@ -354,7 +355,7 @@ internal class RustKeyBackupService @Inject constructor(
// Check that the recovery key matches to the public key that we downloaded from the server. // Check that the recovery key matches to the public key that we downloaded from the server.
// If they match, we can trust the public key and enable backups since we have the private key. // If they match, we can trust the public key and enable backups since we have the private key.
private fun checkRecoveryKey(recoveryKey: BackupRecoveryKey, keysBackupData: KeysVersionResult) { private fun checkRecoveryKey(recoveryKey: BackupRecoveryKey, keysBackupData: KeysVersionResult) {
val backupKey = recoveryKey.publicKey() val backupKey = recoveryKey.megolmV1PublicKey()
val authData = getMegolmBackupAuthData(keysBackupData) val authData = getMegolmBackupAuthData(keysBackupData)
when { when {
@ -474,7 +475,7 @@ internal class RustKeyBackupService @Inject constructor(
if (ciphertext != null && mac != null && ephemeralKey != null) { if (ciphertext != null && mac != null && ephemeralKey != null) {
try { try {
val decrypted = key.decrypt(ephemeralKey, mac, ciphertext) val decrypted = key.decryptV1(ephemeralKey, mac, ciphertext)
val moshi = MoshiProvider.providesMoshi() val moshi = MoshiProvider.providesMoshi()
val adapter = moshi.adapter(MegolmSessionData::class.java) val adapter = moshi.adapter(MegolmSessionData::class.java)
@ -729,7 +730,7 @@ internal class RustKeyBackupService @Inject constructor(
} }
private fun isValidRecoveryKey(recoveryKey: BackupRecoveryKey, version: KeysVersionResult): Boolean { private fun isValidRecoveryKey(recoveryKey: BackupRecoveryKey, version: KeysVersionResult): Boolean {
val publicKey = recoveryKey.publicKey().publicKey val publicKey = recoveryKey.megolmV1PublicKey().publicKey
val authData = getMegolmBackupAuthData(version) ?: return false val authData = getMegolmBackupAuthData(version) ?: return false
return authData.publicKey == publicKey return authData.publicKey == publicKey
} }
@ -800,7 +801,7 @@ internal class RustKeyBackupService @Inject constructor(
if (retrievedMegolmBackupAuthData != null) { if (retrievedMegolmBackupAuthData != null) {
try { try {
olmMachine.enableBackup(retrievedMegolmBackupAuthData.publicKey, keysVersionResult.version) olmMachine.enableBackupV1(retrievedMegolmBackupAuthData.publicKey, keysVersionResult.version)
keysBackupVersion = keysVersionResult keysBackupVersion = keysVersionResult
} catch (e: OlmException) { } catch (e: OlmException) {
Timber.e(e, "OlmException") Timber.e(e, "OlmException")

View File

@ -29,11 +29,11 @@ features = ["lax_deserialize"]
[dependencies.matrix-sdk-common] [dependencies.matrix-sdk-common]
git = "https://github.com/matrix-org/matrix-rust-sdk/" git = "https://github.com/matrix-org/matrix-rust-sdk/"
rev = "1943840b82fd323455d9ca9ce27243d37a660569" rev = "2e4d5f25cba03bd26415b91defd6e762e8c31b63"
[dependencies.matrix-sdk-crypto] [dependencies.matrix-sdk-crypto]
git = "https://github.com/matrix-org/matrix-rust-sdk/" git = "https://github.com/matrix-org/matrix-rust-sdk/"
rev = "1943840b82fd323455d9ca9ce27243d37a660569" rev = "2e4d5f25cba03bd26415b91defd6e762e8c31b63"
features = ["sled_cryptostore", "qrcode", "backups_v1"] features = ["sled_cryptostore", "qrcode", "backups_v1"]
[dependencies.tokio] [dependencies.tokio]

View File

@ -5,7 +5,10 @@ use sha2::Sha512;
use std::{collections::HashMap, iter}; use std::{collections::HashMap, iter};
use thiserror::Error; use thiserror::Error;
use matrix_sdk_crypto::backups::{OlmPkDecryptionError, RecoveryKey}; use matrix_sdk_crypto::{
backups::{OlmPkDecryptionError, RecoveryKey},
store::CryptoStoreError as InnerStoreError,
};
/// The private part of the backup key, the one used for recovery. /// The private part of the backup key, the one used for recovery.
pub struct BackupRecoveryKey { pub struct BackupRecoveryKey {
@ -26,6 +29,9 @@ pub enum DecodeError {
/// An error happened while decoding the recovery key. /// An error happened while decoding the recovery key.
#[error(transparent)] #[error(transparent)]
Decode(#[from] matrix_sdk_crypto::backups::DecodeError), Decode(#[from] matrix_sdk_crypto::backups::DecodeError),
/// An error happened in the storage layer,
#[error(transparent)]
CryptoStore(#[from] InnerStoreError),
} }
/// Struct containing info about the way the backup key got derived from a /// Struct containing info about the way the backup key got derived from a
@ -39,13 +45,15 @@ pub struct PassphraseInfo {
} }
/// The public part of the backup key. /// The public part of the backup key.
pub struct BackupKey { pub struct MegolmV1BackupKey {
/// The actuall base64 encoded public key. /// The actuall base64 encoded public key.
pub public_key: String, pub public_key: String,
/// Signatures that have signed our backup key. /// Signatures that have signed our backup key.
pub signatures: HashMap<String, HashMap<String, String>>, pub signatures: HashMap<String, HashMap<String, String>>,
/// The passphrase info, if the key was derived from one. /// The passphrase info, if the key was derived from one.
pub passphrase_info: Option<PassphraseInfo>, pub passphrase_info: Option<PassphraseInfo>,
/// Get the full name of the backup algorithm this backup key supports.
pub backup_algorithm: String,
} }
impl BackupRecoveryKey { impl BackupRecoveryKey {
@ -107,8 +115,8 @@ impl BackupRecoveryKey {
} }
/// Get the public part of the backup key. /// Get the public part of the backup key.
pub fn public_key(&self) -> BackupKey { pub fn megolm_v1_public_key(&self) -> MegolmV1BackupKey {
let public_key = self.inner.public_key(); let public_key = self.inner.megolm_v1_public_key();
let signatures: HashMap<String, HashMap<String, String>> = public_key let signatures: HashMap<String, HashMap<String, String>> = public_key
.signatures() .signatures()
@ -121,10 +129,11 @@ impl BackupRecoveryKey {
}) })
.collect(); .collect();
BackupKey { MegolmV1BackupKey {
public_key: public_key.to_base64(), public_key: public_key.to_base64(),
signatures, signatures,
passphrase_info: self.passphrase_info.clone(), passphrase_info: self.passphrase_info.clone(),
backup_algorithm: public_key.backup_algorithm().to_owned(),
} }
} }
@ -140,14 +149,14 @@ impl BackupRecoveryKey {
/// Try to decrypt a message that was encrypted using the public part of the /// Try to decrypt a message that was encrypted using the public part of the
/// backup key. /// backup key.
pub fn decrypt( pub fn decrypt_v1(
&self, &self,
ephemeral_key: String, ephemeral_key: String,
mac: String, mac: String,
ciphertext: String, ciphertext: String,
) -> Result<String, PkDecryptionError> { ) -> Result<String, PkDecryptionError> {
self.inner self.inner
.decrypt(ephemeral_key, mac, ciphertext) .decrypt_v1(ephemeral_key, mac, ciphertext)
.map_err(|e| e.into()) .map_err(|e| e.into())
} }
} }

View File

@ -2,7 +2,7 @@
use matrix_sdk_crypto::{ use matrix_sdk_crypto::{
store::CryptoStoreError as InnerStoreError, KeyExportError, MegolmError, OlmError, store::CryptoStoreError as InnerStoreError, KeyExportError, MegolmError, OlmError,
SignatureError as InnerSignatureError, SecretImportError as RustSecretImportError, SecretImportError as RustSecretImportError, SignatureError as InnerSignatureError,
}; };
use ruma::identifiers::Error as RumaIdentifierError; use ruma::identifiers::Error as RumaIdentifierError;
@ -12,6 +12,8 @@ pub enum KeyImportError {
Export(#[from] KeyExportError), Export(#[from] KeyExportError),
#[error(transparent)] #[error(transparent)]
CryptoStore(#[from] InnerStoreError), CryptoStore(#[from] InnerStoreError),
#[error(transparent)]
Json(#[from] serde_json::Error),
} }
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]

View File

@ -18,7 +18,7 @@ mod users;
mod verification; mod verification;
pub use backup_recovery_key::{ pub use backup_recovery_key::{
BackupKey, BackupRecoveryKey, DecodeError, PassphraseInfo, PkDecryptionError, BackupRecoveryKey, DecodeError, MegolmV1BackupKey, PassphraseInfo, PkDecryptionError,
}; };
pub use device::Device; pub use device::Device;
pub use error::{ pub use error::{
@ -87,19 +87,19 @@ pub struct CrossSigningKeyExport {
pub user_signing_key: Option<String>, pub user_signing_key: Option<String>,
} }
/// TODO /// Struct holding the number of room keys we have.
pub struct RoomKeyCounts { pub struct RoomKeyCounts {
/// TODO /// The total number of room keys.
pub total: i64, pub total: i64,
/// TODO /// The number of backed up room keys.
pub backed_up: i64, pub backed_up: i64,
} }
/// TODO /// Backup keys and information we load from the store.
pub struct BackupKeys { pub struct BackupKeys {
/// TODO /// The recovery key as a base64 encoded string.
pub recovery_key: String, pub recovery_key: String,
/// TODO /// The version that is used with the recovery key.
pub backup_version: String, pub backup_version: String,
} }

View File

@ -32,7 +32,7 @@ use tokio::runtime::Runtime;
use matrix_sdk_common::{deserialized_responses::AlgorithmInfo, uuid::Uuid}; use matrix_sdk_common::{deserialized_responses::AlgorithmInfo, uuid::Uuid};
use matrix_sdk_crypto::{ use matrix_sdk_crypto::{
backups::{MegolmV1BackupKey, RecoveryKey}, backups::{MegolmV1BackupKey as RustBackupKey, RecoveryKey},
decrypt_key_export, encrypt_key_export, decrypt_key_export, encrypt_key_export,
matrix_qrcode::QrVerificationData, matrix_qrcode::QrVerificationData,
olm::ExportedRoomKey, olm::ExportedRoomKey,
@ -43,11 +43,11 @@ use matrix_sdk_crypto::{
use crate::{ use crate::{
error::{CryptoStoreError, DecryptionError, SecretImportError, SignatureError}, error::{CryptoStoreError, DecryptionError, SecretImportError, SignatureError},
responses::{response_from_string, OutgoingVerificationRequest, OwnedResponse}, responses::{response_from_string, OutgoingVerificationRequest, OwnedResponse},
BackupKey, BackupKeys, BootstrapCrossSigningResult, ConfirmVerificationResult, BackupKeys, BootstrapCrossSigningResult, ConfirmVerificationResult, CrossSigningKeyExport,
CrossSigningKeyExport, CrossSigningStatus, DecryptedEvent, Device, DeviceLists, KeyImportError, CrossSigningStatus, DecodeError, DecryptedEvent, Device, DeviceLists, KeyImportError,
KeysImportResult, ProgressListener, QrCode, Request, RequestType, RequestVerificationResult, KeysImportResult, MegolmV1BackupKey, ProgressListener, QrCode, Request, RequestType,
RoomKeyCounts, ScanResult, SignatureUploadRequest, StartSasResult, UserIdentity, Verification, RequestVerificationResult, RoomKeyCounts, ScanResult, SignatureUploadRequest, StartSasResult,
VerificationRequest, UserIdentity, Verification, VerificationRequest,
}; };
/// A high level state machine that handles E2EE for Matrix. /// A high level state machine that handles E2EE for Matrix.
@ -79,7 +79,7 @@ impl OlmMachine {
pub fn new(user_id: &str, device_id: &str, path: &str) -> Result<Self, CryptoStoreError> { pub fn new(user_id: &str, device_id: &str, path: &str) -> Result<Self, CryptoStoreError> {
let user_id = UserId::try_from(user_id)?; let user_id = UserId::try_from(user_id)?;
let device_id = device_id.into(); let device_id = device_id.into();
let runtime = Runtime::new().unwrap(); let runtime = Runtime::new().expect("Couldn't create a tokio runtime");
Ok(OlmMachine { Ok(OlmMachine {
inner: runtime.block_on(InnerMachine::new_with_default_store( inner: runtime.block_on(InnerMachine::new_with_default_store(
@ -499,7 +499,7 @@ impl OlmMachine {
let encrypted_content = self let encrypted_content = self
.runtime .runtime
.block_on(self.inner.encrypt(&room_id, content)) .block_on(self.inner.encrypt(&room_id, content))
.unwrap(); .expect("Encrypting an event produced an error");
Ok(serde_json::to_string(&encrypted_content)?) Ok(serde_json::to_string(&encrypted_content)?)
} }
@ -644,13 +644,24 @@ impl OlmMachine {
self.impor_keys_helper(keys, progress_listener) self.impor_keys_helper(keys, progress_listener)
} }
/// TODO /// Import room keys from the given serialized unencrypted key export.
///
/// This method is the same as [`OlmMachine::import_keys`] but the
/// decryption step is skipped and should be performed by the caller. This
/// may be useful for custom handling or for server-side key backups.
///
/// # Arguments
///
/// * `keys` - The serialized version of the unencrypted key export.
///
/// * `progress_listener` - A callback that can be used to introspect the
/// progress of the key import.
pub fn import_decrypted_keys( pub fn import_decrypted_keys(
&self, &self,
keys: &str, keys: &str,
progress_listener: Box<dyn ProgressListener>, progress_listener: Box<dyn ProgressListener>,
) -> Result<KeysImportResult, KeyImportError> { ) -> Result<KeysImportResult, KeyImportError> {
let keys: Vec<ExportedRoomKey> = serde_json::from_str(keys).unwrap(); let keys: Vec<ExportedRoomKey> = serde_json::from_str(keys)?;
self.impor_keys_helper(keys, progress_listener) self.impor_keys_helper(keys, progress_listener)
} }
@ -1263,30 +1274,47 @@ impl OlmMachine {
Ok(()) Ok(())
} }
/// TODO /// Activate the given backup key to be used with the given backup version.
pub fn enable_backup(&self, key: BackupKey, version: String) -> Result<(), CryptoStoreError> { ///
let backup_key = MegolmV1BackupKey::from_base64(&key.public_key).unwrap(); /// **Warning**: The caller needs to make sure that the given `BackupKey` is
/// trusted, otherwise we might be encrypting room keys that a malicious
/// party could decrypt.
///
/// The [`OlmMachine::verify_backup`] method can be used to so.
pub fn enable_backup_v1(
&self,
key: MegolmV1BackupKey,
version: String,
) -> Result<(), DecodeError> {
let backup_key = RustBackupKey::from_base64(&key.public_key)?;
backup_key.set_version(version); backup_key.set_version(version);
self.runtime self.runtime
.block_on(self.inner.backup_machine().enable_backup(backup_key))?; .block_on(self.inner.backup_machine().enable_backup_v1(backup_key))?;
Ok(()) Ok(())
} }
/// TODO /// Are we able to encrypt room keys.
///
/// This returns true if we have an active `BackupKey` and backup version
/// registered with the state machine.
pub fn backup_enabled(&self) -> bool { pub fn backup_enabled(&self) -> bool {
self.runtime.block_on(self.inner.backup_machine().enabled()) self.runtime.block_on(self.inner.backup_machine().enabled())
} }
/// TODO /// Disable and reset our backup state.
///
/// This will remove any pending backup request, remove the backup key and
/// reset the backup state of each room key we have.
pub fn disable_backup(&self) -> Result<(), CryptoStoreError> { pub fn disable_backup(&self) -> Result<(), CryptoStoreError> {
Ok(self Ok(self
.runtime .runtime
.block_on(self.inner.backup_machine().disable_backup())?) .block_on(self.inner.backup_machine().disable_backup())?)
} }
/// TODO /// Encrypt a batch of room keys and return a request that needs to be sent
/// out to backup the room keys.
pub fn backup_room_keys(&self) -> Result<Option<Request>, CryptoStoreError> { pub fn backup_room_keys(&self) -> Result<Option<Request>, CryptoStoreError> {
let request = self let request = self
.runtime .runtime
@ -1297,7 +1325,7 @@ impl OlmMachine {
Ok(request) Ok(request)
} }
/// TODO /// Get the number of backed up room keys and the total number of room keys.
pub fn room_key_counts(&self) -> Result<RoomKeyCounts, CryptoStoreError> { pub fn room_key_counts(&self) -> Result<RoomKeyCounts, CryptoStoreError> {
Ok(self Ok(self
.runtime .runtime
@ -1305,7 +1333,10 @@ impl OlmMachine {
.into()) .into())
} }
/// TODO /// Store the recovery key in the cryptostore.
///
/// This is useful if the client wants to support gossiping of the backup
/// key.
pub fn save_recovery_key( pub fn save_recovery_key(
&self, &self,
key: Option<String>, key: Option<String>,
@ -1321,7 +1352,7 @@ impl OlmMachine {
.block_on(self.inner.backup_machine().save_recovery_key(key, version))?) .block_on(self.inner.backup_machine().save_recovery_key(key, version))?)
} }
/// TODO /// Get the backup keys we have saved in our crypto store.
pub fn get_backup_keys(&self) -> Result<Option<BackupKeys>, CryptoStoreError> { pub fn get_backup_keys(&self) -> Result<Option<BackupKeys>, CryptoStoreError> {
Ok(self Ok(self
.runtime .runtime
@ -1330,7 +1361,8 @@ impl OlmMachine {
.ok()) .ok())
} }
/// TODO /// Sign the given message using our device key and if available cross
/// signing master key.
pub fn sign(&self, message: &str) -> HashMap<String, HashMap<String, String>> { pub fn sign(&self, message: &str) -> HashMap<String, HashMap<String, String>> {
self.runtime self.runtime
.block_on(self.inner.sign(message)) .block_on(self.inner.sign(message))
@ -1344,7 +1376,8 @@ impl OlmMachine {
.collect() .collect()
} }
/// TODO /// Check if the given backup has been verified by us or by another of our
/// devices that we trust.
pub fn verify_backup(&self, auth_data: &str) -> Result<bool, CryptoStoreError> { pub fn verify_backup(&self, auth_data: &str) -> Result<bool, CryptoStoreError> {
let auth_data = serde_json::from_str(auth_data)?; let auth_data = serde_json::from_str(auth_data)?;
Ok(self Ok(self

View File

@ -19,6 +19,7 @@ enum PkDecryptionError {
enum KeyImportError { enum KeyImportError {
"Export", "Export",
"CryptoStore", "CryptoStore",
"Json",
}; };
[Error] [Error]
@ -357,8 +358,8 @@ interface OlmMachine {
boolean is_identity_verified([ByRef] string user_id); boolean is_identity_verified([ByRef] string user_id);
record<DOMString, record<DOMString, string>> sign([ByRef] string message); record<DOMString, record<DOMString, string>> sign([ByRef] string message);
[Throws=CryptoStoreError] [Throws=DecodeError]
void enable_backup(BackupKey key, string version); void enable_backup_v1(MegolmV1BackupKey key, string version);
[Throws=CryptoStoreError] [Throws=CryptoStoreError]
void disable_backup(); void disable_backup();
[Throws=CryptoStoreError] [Throws=CryptoStoreError]
@ -379,10 +380,11 @@ dictionary PassphraseInfo {
i32 private_key_iterations; i32 private_key_iterations;
}; };
dictionary BackupKey { dictionary MegolmV1BackupKey {
string public_key; string public_key;
record<DOMString, record<DOMString, string>> signatures; record<DOMString, record<DOMString, string>> signatures;
PassphraseInfo? passphrase_info; PassphraseInfo? passphrase_info;
string backup_algorithm;
}; };
dictionary BackupKeys { dictionary BackupKeys {
@ -398,6 +400,7 @@ dictionary RoomKeyCounts {
[Error] [Error]
enum DecodeError { enum DecodeError {
"Decode", "Decode",
"CryptoStore",
}; };
interface BackupRecoveryKey { interface BackupRecoveryKey {
@ -412,7 +415,7 @@ interface BackupRecoveryKey {
constructor(string key); constructor(string key);
string to_base58(); string to_base58();
string to_base64(); string to_base64();
BackupKey public_key(); MegolmV1BackupKey megolm_v1_public_key();
[Throws=PkDecryptionError] [Throws=PkDecryptionError]
string decrypt(string ephemeral_key, string mac, string ciphertext); string decrypt_v1(string ephemeral_key, string mac, string ciphertext);
}; };

View File

@ -10,13 +10,13 @@ pub enum Verification {
/// The `m.sas.v1` verification flow. /// The `m.sas.v1` verification flow.
SasV1 { SasV1 {
#[allow(missing_docs)] #[allow(missing_docs)]
sas: Sas sas: Sas,
}, },
/// The `m.qr_code.scan.v1`, `m.qr_code.show.v1`, and `m.reciprocate.v1` /// The `m.qr_code.scan.v1`, `m.qr_code.show.v1`, and `m.reciprocate.v1`
/// verification flow. /// verification flow.
QrCodeV1 { QrCodeV1 {
#[allow(missing_docs)] #[allow(missing_docs)]
qrcode: QrCode qrcode: QrCode,
}, },
} }