diff --git a/Cargo.lock b/Cargo.lock index d950e9da..6f711007 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1076,6 +1076,7 @@ dependencies = [ "loole", "lru-cache", "rand 0.8.5", + "recaptcha-verify", "regex", "reqwest", "ruma", @@ -3751,6 +3752,17 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "recaptcha-verify" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e3be7b2e46e24637ac96b0c9f70070f188652018573f36f4e511dcad09738a" +dependencies = [ + "reqwest", + "serde", + "serde_json", +] + [[package]] name = "redox_syscall" version = "0.5.13" diff --git a/conduwuit-example.toml b/conduwuit-example.toml index b7456237..bdc2f570 100644 --- a/conduwuit-example.toml +++ b/conduwuit-example.toml @@ -441,6 +441,26 @@ # #registration_token_file = +# The public site key for reCaptcha. If this is provided, reCaptcha +# becomes required during registration. If both captcha *and* +# registration token are enabled, both will be required during +# registration. +# +# IMPORTANT: "Verify the origin of reCAPTCHA solutions" **MUST** BE +# DISABLED IF YOU WANT THE CAPTCHA TO WORK IN 3RD PARTY CLIENTS, OR +# CLIENTS HOSTED ON DOMAINS OTHER THAN YOUR OWN! +# +# Registration must be enabled (`allow_registration` must be true) for +# this to have any effect. +# +#recaptcha_site_key = + +# The private site key for reCaptcha. +# If this is omitted, captcha registration will not work, +# even if `recaptcha_site_key` is set. +# +#recaptcha_private_site_key = + # Controls whether encrypted rooms and events are allowed. # #allow_encryption = true diff --git a/src/api/client/account.rs b/src/api/client/account.rs index 12801e7d..11414abf 100644 --- a/src/api/client/account.rs +++ b/src/api/client/account.rs @@ -291,19 +291,34 @@ pub(crate) async fn register_route( } // UIAA - let mut uiaainfo; - let skip_auth = if services.globals.registration_token.is_some() { + let mut uiaainfo = UiaaInfo { + flows: Vec::new(), + completed: Vec::new(), + params: Box::default(), + session: None, + auth_error: None, + }; + let mut skip_auth = body.appservice_info.is_some(); + if services.globals.registration_token.is_some() { // Registration token required - uiaainfo = UiaaInfo { - flows: vec![AuthFlow { - stages: vec![AuthType::RegistrationToken], - }], - completed: Vec::new(), - params: Box::default(), - session: None, - auth_error: None, - }; - body.appservice_info.is_some() + uiaainfo.flows.push(AuthFlow { + stages: vec![AuthType::RegistrationToken], + }); + } + if services.config.recaptcha_private_site_key.is_some() { + if let Some(pubkey) = &services.config.recaptcha_site_key { + // ReCaptcha required + uiaainfo + .flows + .push(AuthFlow { stages: vec![AuthType::ReCaptcha] }); + uiaainfo.params = serde_json::value::to_raw_value(&serde_json::json!({ + "m.login.recaptcha": { + "public_key": pubkey, + }, + })) + .expect("Failed to serialize recaptcha params"); + skip_auth = skip_auth || is_guest; + } } else { // No registration token necessary, but clients must still go through the flow uiaainfo = UiaaInfo { @@ -313,8 +328,8 @@ pub(crate) async fn register_route( session: None, auth_error: None, }; - body.appservice_info.is_some() || is_guest - }; + skip_auth = skip_auth || is_guest; + } if !skip_auth { match &body.auth { diff --git a/src/core/config/check.rs b/src/core/config/check.rs index 3dc45e2f..6710a91a 100644 --- a/src/core/config/check.rs +++ b/src/core/config/check.rs @@ -180,19 +180,28 @@ pub fn check(config: &Config) -> Result { } } + if config.recaptcha_site_key.is_some() && config.recaptcha_private_site_key.is_none() { + return Err!(Config( + "recaptcha_private_site_key", + "reCAPTCHA private site key is required when reCAPTCHA site key is set." + )); + } + if config.allow_registration && !config.yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse && config.registration_token.is_none() && config.registration_token_file.is_none() + && config.recaptcha_site_key.is_none() { return Err!(Config( "registration_token", - "!! You have `allow_registration` enabled without a token configured in your config \ - which means you are allowing ANYONE to register on your conduwuit instance without \ - any 2nd-step (e.g. registration token). If this is not the intended behaviour, \ - please set a registration token. For security and safety reasons, conduwuit will \ - shut down. If you are extra sure this is the desired behaviour you want, please \ - set the following config option to true: + "!! You have `allow_registration` enabled without a token or captcha configured \ + which means you are allowing ANYONE to register on your continuwuity instance \ + without any 2nd-step (e.g. registration token, captcha), which is FREQUENTLY \ + abused by malicious actors. If this is not the intended behaviour, please set a \ + registration token. For security and safety reasons, continuwuity will shut down. \ + If you are extra sure this is the desired behaviour you want, please set the \ + following config option to true: `yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse`" )); } diff --git a/src/core/config/mod.rs b/src/core/config/mod.rs index 282caf79..d93acd9b 100644 --- a/src/core/config/mod.rs +++ b/src/core/config/mod.rs @@ -556,6 +556,24 @@ pub struct Config { /// example: "/etc/continuwuity/.reg_token" pub registration_token_file: Option, + /// The public site key for reCaptcha. If this is provided, reCaptcha + /// becomes required during registration. If both captcha *and* + /// registration token are enabled, both will be required during + /// registration. + /// + /// IMPORTANT: "Verify the origin of reCAPTCHA solutions" **MUST** BE + /// DISABLED IF YOU WANT THE CAPTCHA TO WORK IN 3RD PARTY CLIENTS, OR + /// CLIENTS HOSTED ON DOMAINS OTHER THAN YOUR OWN! + /// + /// Registration must be enabled (`allow_registration` must be true) for + /// this to have any effect. + pub recaptcha_site_key: Option, + + /// The private site key for reCaptcha. + /// If this is omitted, captcha registration will not work, + /// even if `recaptcha_site_key` is set. + pub recaptcha_private_site_key: Option, + /// Controls whether encrypted rooms and events are allowed. #[serde(default = "true_fn")] pub allow_encryption: bool, diff --git a/src/service/Cargo.toml b/src/service/Cargo.toml index 8b0d1405..fdebd1d7 100644 --- a/src/service/Cargo.toml +++ b/src/service/Cargo.toml @@ -111,6 +111,7 @@ webpage.workspace = true webpage.optional = true blurhash.workspace = true blurhash.optional = true +recaptcha-verify = { version = "0.1.5", default-features = false } [lints] workspace = true diff --git a/src/service/uiaa/mod.rs b/src/service/uiaa/mod.rs index 7803c736..7735c87f 100644 --- a/src/service/uiaa/mod.rs +++ b/src/service/uiaa/mod.rs @@ -177,6 +177,34 @@ pub async fn try_auth( // Password was correct! Let's add it to `completed` uiaainfo.completed.push(AuthType::Password); }, + | AuthData::ReCaptcha(r) => { + if self.services.config.recaptcha_private_site_key.is_none() { + return Err!(Request(Forbidden("ReCaptcha is not configured."))); + } + match recaptcha_verify::verify( + self.services + .config + .recaptcha_private_site_key + .as_ref() + .unwrap(), + r.response.as_str(), + None, + ) + .await + { + | Ok(()) => { + uiaainfo.completed.push(AuthType::ReCaptcha); + }, + | Err(e) => { + error!("ReCaptcha verification failed: {e:?}"); + uiaainfo.auth_error = Some(ruma::api::client::error::StandardErrorBody { + kind: ErrorKind::forbidden(), + message: "ReCaptcha verification failed.".to_owned(), + }); + return Ok((false, uiaainfo)); + }, + } + }, | AuthData::RegistrationToken(t) => { let tokens = self.read_tokens().await?; if tokens.contains(t.token.trim()) {