use std::sync::Arc;
use as_variant::as_variant;
pub use matrix_sdk_base::crypto::types::qr_login::{
LoginQrCodeDecodeError, Msc4108IntentData, QrCodeData, QrCodeIntent, QrCodeIntentData,
};
use matrix_sdk_base::crypto::{SecretImportError, store::SecretsBundleExportError};
pub use oauth2::{
ConfigurationError, DeviceCodeErrorResponse, DeviceCodeErrorResponseType, HttpClientError,
RequestTokenError, StandardErrorResponse,
basic::{BasicErrorResponse, BasicRequestTokenError},
};
use ruma::api::error::ErrorKind;
use thiserror::Error;
use tokio::sync::Mutex;
use url::Url;
pub use vodozemac::ecies::{Error as EciesError, MessageDecodeError};
mod grant;
mod login;
mod messages;
mod rendezvous_channel;
mod secure_channel;
pub use self::{
grant::{GrantLoginProgress, GrantLoginWithGeneratedQrCode, GrantLoginWithScannedQrCode},
login::{LoginProgress, LoginWithGeneratedQrCode, LoginWithQrCode},
messages::{LoginFailureReason, LoginProtocolType, QrAuthMessage},
};
use super::CrossProcessRefreshLockError;
#[cfg(doc)]
use super::OAuth;
use crate::HttpError;
#[derive(Debug, Error)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Error), uniffi(flat_error))]
pub enum QRCodeLoginError {
#[error(transparent)]
OAuth(#[from] DeviceAuthorizationOAuthError),
#[error("The login failed, reason: {reason}")]
LoginFailure {
reason: LoginFailureReason,
homeserver: Option<Url>,
},
#[error("We have received an unexpected message, expected: {expected}, got {received:?}")]
UnexpectedMessage {
expected: &'static str,
received: Box<QrAuthMessage>,
},
#[error(transparent)]
SecureChannel(SecureChannelError),
#[error("The rendezvous session was not found and might have expired")]
NotFound,
#[error(transparent)]
CrossProcessRefreshLock(#[from] CrossProcessRefreshLockError),
#[error(transparent)]
UserIdDiscovery(HttpError),
#[error(transparent)]
SessionTokens(crate::Error),
#[error(transparent)]
DeviceKeyUpload(crate::Error),
#[error(transparent)]
SecretImport(#[from] SecretImportError),
#[error(transparent)]
ServerReset(crate::Error),
}
impl From<SecureChannelError> for QRCodeLoginError {
fn from(e: SecureChannelError) -> Self {
match e {
SecureChannelError::RendezvousChannel(ref http_error) => {
if let Some(ErrorKind::NotFound) = http_error.client_api_error_kind() {
return Self::NotFound;
}
Self::SecureChannel(e)
}
e => Self::SecureChannel(e),
}
}
}
#[derive(Debug, Error)]
pub enum QRCodeGrantLoginError {
#[error("Secrets backup not set up")]
MissingSecretsBackup(Option<SecretsBundleExportError>),
#[error("The check code was incorrect")]
InvalidCheckCode,
#[error("The rendezvous session was not found and might have expired")]
NotFound,
#[error("Auth handshake error: {0}")]
Unknown(String),
#[error("Unsupported protocol: {0}")]
UnsupportedProtocol(LoginProtocolType),
#[error("The requested device ID is already in use")]
DeviceIDAlreadyInUse,
#[error("The requested device was not returned by the homeserver")]
DeviceNotFound,
#[error(transparent)]
SecureChannel(SecureChannelError),
#[error("We have received an unexpected message, expected: {expected}, got {received:?}")]
UnexpectedMessage {
expected: &'static str,
received: Box<QrAuthMessage>,
},
#[error("The login failed, reason: {reason}")]
LoginFailure {
reason: LoginFailureReason,
},
}
impl From<SecureChannelError> for QRCodeGrantLoginError {
fn from(e: SecureChannelError) -> Self {
match e {
SecureChannelError::RendezvousChannel(ref http_error) => {
if let Some(ErrorKind::NotFound) = http_error.client_api_error_kind() {
return Self::NotFound;
}
Self::SecureChannel(e)
}
SecureChannelError::InvalidCheckCode => Self::InvalidCheckCode,
e => Self::SecureChannel(e),
}
}
}
impl From<SecretsBundleExportError> for QRCodeGrantLoginError {
fn from(e: SecretsBundleExportError) -> Self {
Self::MissingSecretsBackup(Some(e))
}
}
#[derive(Debug, Error)]
pub enum DeviceAuthorizationOAuthError {
#[error(transparent)]
OAuth(#[from] crate::authentication::oauth::OAuthError),
#[error("OAuth 2.0 server doesn't support the device authorization grant")]
NoDeviceAuthorizationEndpoint,
#[error(transparent)]
DeviceAuthorization(#[from] BasicRequestTokenError<HttpClientError<reqwest::Error>>),
#[error(transparent)]
RequestToken(
#[from] RequestTokenError<HttpClientError<reqwest::Error>, DeviceCodeErrorResponse>,
),
}
impl DeviceAuthorizationOAuthError {
pub fn as_request_token_error(&self) -> Option<&DeviceCodeErrorResponseType> {
let error = as_variant!(self, DeviceAuthorizationOAuthError::RequestToken)?;
let request_token_error = as_variant!(error, RequestTokenError::ServerResponse)?;
Some(request_token_error.error())
}
}
#[derive(Debug, Error)]
pub enum SecureChannelError {
#[error(transparent)]
Utf8(#[from] std::str::Utf8Error),
#[error(transparent)]
Ecies(#[from] EciesError),
#[error(transparent)]
MessageDecode(#[from] MessageDecodeError),
#[error(transparent)]
Json(#[from] serde_json::Error),
#[error(
"The secure channel setup has received an unexpected message, expected: {expected}, got {received}"
)]
SecureChannelMessage {
expected: &'static str,
received: String,
},
#[error("The secure channel could not have been established, the check code was invalid")]
InvalidCheckCode,
#[error("Error in the rendezvous channel: {0:?}")]
RendezvousChannel(#[from] HttpError),
#[error(
"The secure channel could not have been established, \
the two devices have the same login intent"
)]
InvalidIntent,
#[error(
"The secure channel could not have been established, \
the check code cannot be received"
)]
CannotReceiveCheckCode,
#[error("The QR code specifies an unsupported protocol version")]
UnsupportedQrCodeType,
}
#[derive(Clone, Debug)]
pub struct QrProgress {
pub check_code: u8,
}
#[derive(Clone, Debug)]
pub enum GeneratedQrProgress {
QrReady(QrCodeData),
QrScanned(CheckCodeSender),
}
pub type CheckCodeSender = CloneableSender<u8>;
impl CheckCodeSender {
pub async fn send(&self, check_code: u8) -> Result<(), SenderError> {
self.send_impl(check_code).await
}
}
#[derive(Clone, Copy, Debug)]
pub(crate) enum ContinuationMessage {
Confirm,
Cancel,
}
#[derive(Clone, Debug)]
pub struct ContinuationMessageSender(CloneableSender<ContinuationMessage>);
impl ContinuationMessageSender {
pub async fn confirm(&self) -> Result<(), SenderError> {
self.0.send_impl(ContinuationMessage::Confirm).await
}
pub async fn cancel(&self) -> Result<(), SenderError> {
self.0.send_impl(ContinuationMessage::Cancel).await
}
}
#[derive(Clone, Debug)]
pub struct CloneableSender<T> {
inner: Arc<Mutex<Option<tokio::sync::oneshot::Sender<T>>>>,
}
impl<T> CloneableSender<T> {
pub(crate) fn new(tx: tokio::sync::oneshot::Sender<T>) -> Self {
Self { inner: Arc::new(Mutex::new(Some(tx))) }
}
async fn send_impl(&self, message: T) -> Result<(), SenderError> {
match self.inner.lock().await.take() {
Some(tx) => tx.send(message).map_err(|_| SenderError::CannotSend),
None => Err(SenderError::AlreadySent),
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum SenderError {
#[error("message already sent.")]
AlreadySent,
#[error("message cannot be sent.")]
CannotSend,
}
#[cfg(all(test, not(target_family = "wasm")))]
mod tests {
use matrix_sdk_test::async_test;
use serde_json::json;
use wiremock::{
Mock, ResponseTemplate,
matchers::{method, path},
};
use crate::test_utils::mocks::MatrixMockServer;
#[async_test]
async fn test_msc_4388_rendezvous_server_supported() {
const URL: &str = "/_matrix/client/unstable/io.element.msc4388/rendezvous";
let server = MatrixMockServer::new().await;
let client = server.client_builder().logged_in_with_oauth().build().await;
{
let _discover_guard = server
.server()
.register_as_scoped(
Mock::given(method("GET"))
.and(path(URL))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"create_available": true,
})))
.expect(1),
)
.await;
let supported = client
.oauth()
.msc_4388_rendezvous_server_supported()
.await
.expect("We should be able to check if the rendezvous server is supported");
assert!(supported, "The rendezvous server should be supported");
}
{
let _discover_guard = server
.server()
.register_as_scoped(
Mock::given(method("GET"))
.and(path(URL))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"create_available": false,
})))
.expect(1),
)
.await;
let supported = client
.oauth()
.msc_4388_rendezvous_server_supported()
.await
.expect("We should be able to check if the rendezvous server is supported");
assert!(
!supported,
"The rendezvous server should not be supported, because create_available is false"
);
}
{
let _discover_guard = server
.server()
.register_as_scoped(
Mock::given(method("GET"))
.and(path(URL))
.respond_with(ResponseTemplate::new(404))
.expect(1),
)
.await;
let supported = client
.oauth()
.msc_4388_rendezvous_server_supported()
.await
.expect("We should be able to check if the rendezvous server is supported");
assert!(
!supported,
"The rendezvous server should not be supported if we receive a 404 response"
);
}
{
let _discover_guard = server
.server()
.register_as_scoped(
Mock::given(method("GET"))
.and(path(URL))
.respond_with(ResponseTemplate::new(403))
.expect(1),
)
.await;
let supported = client
.oauth()
.msc_4388_rendezvous_server_supported()
.await
.expect("We should be able to check if the rendezvous server is supported");
assert!(
!supported,
"The rendezvous server should not be supported if we receive a 403 response"
);
}
{
let _discover_guard = server
.server()
.register_as_scoped(
Mock::given(method("GET"))
.and(path(URL))
.respond_with(ResponseTemplate::new(500))
.expect(1),
)
.await;
client
.oauth()
.msc_4388_rendezvous_server_supported()
.await
.expect_err("We should return an error if the homeserver can't tell us if the endpoint is supported or not");
}
}
}