From 1f0e6d05c8dc4ef85d0b2abddb77a8df3942a7fc Mon Sep 17 00:00:00 2001 From: Pierre Hubert Date: Wed, 20 Apr 2022 21:06:53 +0200 Subject: [PATCH 01/10] Generate & return webauthn registration challenge --- Cargo.lock | 97 +++++++++++++++++++++++ Cargo.toml | 6 +- src/controllers/two_factors_controller.rs | 39 +++++++++ src/data/crypto_wrapper.rs | 92 +++++++++++++++++++++ src/data/id_token.rs | 2 +- src/data/mod.rs | 4 +- src/data/totp_key.rs | 2 +- src/data/webauthn_manager.rs | 77 ++++++++++++++++++ src/main.rs | 6 ++ templates/settings/add_webauthn_page.html | 14 ++++ templates/settings/two_factors_page.html | 1 + 11 files changed, 336 insertions(+), 4 deletions(-) create mode 100644 src/data/crypto_wrapper.rs create mode 100644 src/data/webauthn_manager.rs create mode 100644 templates/settings/add_webauthn_page.html diff --git a/Cargo.lock b/Cargo.lock index a255b19..f4b8c16 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -424,10 +424,12 @@ dependencies = [ "actix", "actix-identity", "actix-web", + "aes-gcm", "askama", "base32", "base64", "bcrypt", + "bincode", "clap", "digest 0.10.3", "env_logger", @@ -444,8 +446,10 @@ dependencies = [ "serde_yaml", "sha2 0.10.2", "totp_rfc6238", + "url", "urlencoding", "uuid", + "webauthn-rs", ] [[package]] @@ -459,6 +463,15 @@ dependencies = [ "getrandom", ] +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bit_field" version = "0.10.1" @@ -995,6 +1008,21 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.0.1" @@ -1693,6 +1721,33 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +[[package]] +name = "openssl" +version = "0.10.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7ae222234c30df141154f159066c5093ff73b63204dcda7121eb082fc56a95" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-sys", +] + +[[package]] +name = "openssl-sys" +version = "0.9.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e46109c383602735fa0a2e48dd2b7c892b048e1bf69e5c3b1d804b7d9c203cb" +dependencies = [ + "autocfg 1.1.0", + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "os_str_bytes" version = "6.0.0" @@ -1831,6 +1886,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "pkg-config" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" + [[package]] name = "png" version = "0.17.5" @@ -2101,6 +2162,16 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde_cbor" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5" +dependencies = [ + "half", + "serde", +] + [[package]] name = "serde_derive" version = "1.0.136" @@ -2539,6 +2610,7 @@ dependencies = [ "idna", "matches", "percent-encoding", + "serde", ] [[package]] @@ -2562,6 +2634,12 @@ dependencies = [ "getrandom", ] +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.4" @@ -2644,6 +2722,25 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webauthn-rs" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90b266eccb4b32595876f5c73ea443b0516da0b1df72ca07bc08ed9ba7f96ec1" +dependencies = [ + "base64", + "nom", + "openssl", + "rand", + "serde", + "serde_cbor", + "serde_derive", + "serde_json", + "thiserror", + "tracing", + "url", +] + [[package]] name = "weezl" version = "0.1.5" diff --git a/Cargo.toml b/Cargo.toml index 3bd98c5..9ea7c0e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,4 +30,8 @@ sha2 = "0.10.2" lazy-regex = "2.3.0" totp_rfc6238 = "0.5.0" base32 = "0.4.0" -qrcode-generator = "4.1.4" \ No newline at end of file +qrcode-generator = "4.1.4" +webauthn-rs = "0.3.2" +url = "2.2.2" +aes-gcm = { version = "0.9.4", features = ["aes"] } +bincode = "1.3.3" \ No newline at end of file diff --git a/src/controllers/two_factors_controller.rs b/src/controllers/two_factors_controller.rs index 127185b..7228ad9 100644 --- a/src/controllers/two_factors_controller.rs +++ b/src/controllers/two_factors_controller.rs @@ -9,6 +9,7 @@ use crate::data::app_config::AppConfig; use crate::data::current_user::CurrentUser; use crate::data::totp_key::TotpKey; use crate::data::user::User; +use crate::data::webauthn_manager::WebAuthManagerReq; #[derive(Template)] #[template(path = "settings/two_factors_page.html")] @@ -26,6 +27,13 @@ struct AddTotpPage { secret_key: String, } +#[derive(Template)] +#[template(path = "settings/add_webauthn_page.html")] +struct AddWebauhtnPage { + _p: BaseSettingsPage, + opaque_state: String, + challenge_json: String, +} /// Manage two factors authentication methods route pub async fn two_factors_route(user: CurrentUser) -> impl Responder { @@ -69,4 +77,35 @@ pub async fn add_totp_factor_route(user: CurrentUser, app_conf: web::Data impl Responder { + let registration_request = match manager.start_register(&user) { + Ok(r) => r, + Err(e) => { + log::error!("Failed to request new key! {:?}", e); + return HttpResponse::InternalServerError().body("Failed to generate request for registration!"); + } + }; + + let challenge_json = match serde_json::to_string(®istration_request.creation_challenge) { + Ok(r) => r, + Err(e) => { + log::error!("Failed to serialize challenge! {:?}", e); + return HttpResponse::InternalServerError().body("Failed to serialize challenge!"); + } + }; + + HttpResponse::Ok() + .body(AddWebauhtnPage { + _p: BaseSettingsPage::get( + "New security key", + &user, + None, + None), + + opaque_state: registration_request.opaque_state, + challenge_json: urlencoding::encode(&challenge_json).to_string(), + }.render().unwrap()) } \ No newline at end of file diff --git a/src/data/crypto_wrapper.rs b/src/data/crypto_wrapper.rs new file mode 100644 index 0000000..f26ce28 --- /dev/null +++ b/src/data/crypto_wrapper.rs @@ -0,0 +1,92 @@ +use std::io::ErrorKind; + +use aes_gcm::{Aes256Gcm, Key, Nonce}; +use aes_gcm::aead::Aead; +use aes_gcm::NewAead; +use rand::Rng; +use serde::de::DeserializeOwned; +use serde::Serialize; + +use crate::utils::err::Res; + +const NONCE_LEN: usize = 12; +const KEY_LEN: usize = 32; + +pub struct CryptoWrapper { + key: Vec, +} + +impl CryptoWrapper { + /// Generate a new memory wrapper + pub fn new_random() -> Self { + Self { key: (0..KEY_LEN).map(|_| { rand::random::() }).collect() } + } + + /// Encrypt some data + pub fn encrypt(&self, data: &T) -> Res { + let aes_key = Aes256Gcm::new(Key::from_slice(&self.key)); + let nonce_bytes = rand::thread_rng().gen::<[u8; NONCE_LEN]>(); + + let serialized_data = bincode::serialize(data)?; + + let mut enc = aes_key.encrypt(Nonce::from_slice(&nonce_bytes), + serialized_data.as_slice()).unwrap(); + enc.extend_from_slice(&nonce_bytes); + + + Ok(base64::encode(enc)) + } + + /// Decrypt some data previously encrypted using the [`CryptoWrapper::encrypt`] method + pub fn decrypt(&self, input: &str) -> Res { + let bytes = base64::decode(input)?; + + if bytes.len() < NONCE_LEN { + return Err(Box::new(std::io::Error::new(ErrorKind::Other, + "Input string is smaller than nonce!"))); + } + + let (enc, nonce) = bytes.split_at(bytes.len() - NONCE_LEN); + assert_eq!(nonce.len(), NONCE_LEN); + + let aes_key = Aes256Gcm::new(Key::from_slice(&self.key)); + + let dec = match aes_key.decrypt(Nonce::from_slice(nonce), enc) { + Ok(d) => d, + Err(e) => { + log::error!("Failed to decrypt wrapped data! {:#?}", e); + return Err(Box::new(std::io::Error::new(ErrorKind::Other, + "Failed to decrypt wrapped data!"))); + } + }; + + Ok(bincode::deserialize(&dec)?) + } +} + +#[cfg(test)] +mod test { + use crate::data::crypto_wrapper::CryptoWrapper; + + #[derive(serde::Serialize, serde::Deserialize, Eq, PartialEq, Debug)] + struct Message(String); + + #[test] + fn encrypt_and_decrypt() { + let wrapper = CryptoWrapper::new_random(); + let msg = Message("Pierre was here".to_string()); + let enc = wrapper.encrypt(&msg).unwrap(); + let dec: Message = wrapper.decrypt(&enc).unwrap(); + + assert_eq!(dec, msg) + } + + #[test] + fn encrypt_and_decrypt_invalid() { + let wrapper_1 = CryptoWrapper::new_random(); + let wrapper_2 = CryptoWrapper::new_random(); + let msg = Message("Pierre was here".to_string()); + let enc = wrapper_1.encrypt(&msg).unwrap(); + wrapper_2.decrypt::(&enc).unwrap_err(); + } +} \ No newline at end of file diff --git a/src/data/id_token.rs b/src/data/id_token.rs index 67ac62b..8f0ec74 100644 --- a/src/data/id_token.rs +++ b/src/data/id_token.rs @@ -12,7 +12,7 @@ pub struct IdToken { /// REQUIRED. Audience(s) that this ID Token is intended for. It MUST contain the OAuth 2.0 client_id of the Relying Party as an audience value. It MAY also contain identifiers for other audiences. In the general case, the aud value is an array of case sensitive strings. In the common special case when there is one audience, the aud value MAY be a single case sensitive string. #[serde(rename = "aud")] pub audience: String, - /// REQUIRED. Expiration time on or after which the ID Token MUST NOT be accepted for processing. The processing of this parameter requires that the current date/time MUST be before the expiration date/time listed in the value. Implementers MAY provide for some small leeway, usually no more than a few minutes, to account for clock skew. Its value is a JSON number representing the number of seconds from 1970-01-01T0:0:0Z as measured in UTC until the date/time. See RFC 3339 [RFC3339] for details regarding date/times in general and UTC in particular. + /// REQUIRED. Expiration time on or after which the ID Token MUST NOT be accepted for processing. The processing of this parameter requires that the current date/time MUST be before the expiration date/time listed in the value. Implementers MAY provide for some small leeway, usually no more than a few minutes, to account for clock skew. Its value is a JSON number representing the number of seconds from 1970-01-01T0:0:0Z as measured in UTC until the date/time. See RFC 3339 for details regarding date/times in general and UTC in particular. #[serde(rename = "exp")] pub expiration_time: u64, /// REQUIRED. Time at which the JWT was issued. Its value is a JSON number representing the number of seconds from 1970-01-01T0:0:0Z as measured in UTC until the date/time. diff --git a/src/data/mod.rs b/src/data/mod.rs index b78ab80..37bb154 100644 --- a/src/data/mod.rs +++ b/src/data/mod.rs @@ -12,4 +12,6 @@ pub mod code_challenge; pub mod open_id_user_info; pub mod access_token; pub mod totp_key; -pub mod login_redirect; \ No newline at end of file +pub mod login_redirect; +pub mod webauthn_manager; +pub mod crypto_wrapper; \ No newline at end of file diff --git a/src/data/totp_key.rs b/src/data/totp_key.rs index 1131a4e..aac8697 100644 --- a/src/data/totp_key.rs +++ b/src/data/totp_key.rs @@ -34,7 +34,7 @@ impl TotpKey { /// Get QrCode URL for user /// - /// Based on https://github.com/google/google-authenticator/wiki/Key-Uri-Format + /// Based on pub fn url_for_user(&self, u: &User, conf: &AppConfig) -> String { format!( "otpauth://totp/{}:{}?secret={}&issuer={}&algorithm=SHA1&digits={}&period={}", diff --git a/src/data/webauthn_manager.rs b/src/data/webauthn_manager.rs new file mode 100644 index 0000000..2614160 --- /dev/null +++ b/src/data/webauthn_manager.rs @@ -0,0 +1,77 @@ +use std::sync::Arc; + +use actix_web::web; +use webauthn_rs::{RegistrationState, Webauthn, WebauthnConfig}; +use webauthn_rs::proto::CreationChallengeResponse; + +use crate::constants::APP_NAME; +use crate::data::app_config::AppConfig; +use crate::data::crypto_wrapper::CryptoWrapper; +use crate::data::user::{User, UserID}; +use crate::utils::err::Res; + +#[derive(Debug)] +struct WebAuthnAppConfig { + origin: url::Url, + relying_party_id: String, +} + +impl WebauthnConfig for WebAuthnAppConfig { + fn get_relying_party_name(&self) -> &str { + APP_NAME + } + + fn get_origin(&self) -> &url::Url { + &self.origin + } + + fn get_relying_party_id(&self) -> &str { + &self.relying_party_id + } +} + +pub struct RegisterKeyRequest { + pub opaque_state: String, + pub creation_challenge: CreationChallengeResponse, +} + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +struct RegisterKeyOpaqueData { + registration_state: RegistrationState, + user_id: UserID, +} + +pub type WebAuthManagerReq = web::Data>; + +pub struct WebAuthManager { + core: Webauthn, + crypto_wrapper: CryptoWrapper, +} + +impl WebAuthManager { + pub fn init(conf: &AppConfig) -> Self { + Self { + core: Webauthn::new(WebAuthnAppConfig { + origin: url::Url::parse(&conf.website_origin) + .expect("Failed to parse configuration origin!"), + relying_party_id: conf.domain_name().to_string(), + }), + crypto_wrapper: CryptoWrapper::new_random(), + } + } + + pub fn start_register(&self, user: &User) -> Res { + let (creation_challenge, registration_state) = self.core.generate_challenge_register( + &user.username, + true, + )?; + + Ok(RegisterKeyRequest { + opaque_state: self.crypto_wrapper.encrypt(&RegisterKeyOpaqueData { + registration_state, + user_id: user.uid.clone(), + })?, + creation_challenge, + }) + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 84ab1af..65468c8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use actix::Actor; use actix_identity::{CookieIdentityPolicy, IdentityService}; use actix_web::{App, get, HttpResponse, HttpServer, web}; @@ -17,6 +19,7 @@ use basic_oidc::data::client::ClientManager; use basic_oidc::data::entity_manager::EntityManager; use basic_oidc::data::jwt_signer::JWTSigner; use basic_oidc::data::user::{hash_password, User}; +use basic_oidc::data::webauthn_manager::WebAuthManager; use basic_oidc::middlewares::auth_middleware::AuthMiddleware; #[get("/health")] @@ -68,6 +71,7 @@ async fn main() -> std::io::Result<()> { let openid_sessions_actor = OpenIDSessionsActor::default().start(); let jwt_signer = JWTSigner::gen_from_memory() .expect("Failed to generate JWKS key"); + let webauthn_manager = Arc::new(WebAuthManager::init(&config)); log::info!("Server will listen on {}", config.listen_address); let listen_address = config.listen_address.to_string(); @@ -91,6 +95,7 @@ async fn main() -> std::io::Result<()> { .app_data(web::Data::new(config.clone())) .app_data(web::Data::new(clients)) .app_data(web::Data::new(jwt_signer.clone())) + .app_data(web::Data::new(webauthn_manager.clone())) .wrap(Logger::default()) .wrap(AuthMiddleware {}) @@ -124,6 +129,7 @@ async fn main() -> std::io::Result<()> { .route("/settings/change_password", web::post().to(settings_controller::change_password_route)) .route("/settings/two_factors", web::get().to(two_factors_controller::two_factors_route)) .route("/settings/two_factors/add_totp", web::get().to(two_factors_controller::add_totp_factor_route)) + .route("/settings/two_factors/add_webauthn", web::get().to(two_factors_controller::add_webauthn_factor_route)) // User API .route("/settings/api/two_factor/save_totp_factor", web::post().to(two_factor_api::save_totp_factor)) diff --git a/templates/settings/add_webauthn_page.html b/templates/settings/add_webauthn_page.html new file mode 100644 index 0000000..15e3ea8 --- /dev/null +++ b/templates/settings/add_webauthn_page.html @@ -0,0 +1,14 @@ +{% extends "base_settings_page.html" %} +{% block content %} +
+ +

In order, to continue, please insert your security key & approve the registration request.

+ + + +
+ +{% endblock content %} diff --git a/templates/settings/two_factors_page.html b/templates/settings/two_factors_page.html index 1b740fc..8bfa371 100644 --- a/templates/settings/two_factors_page.html +++ b/templates/settings/two_factors_page.html @@ -12,6 +12,7 @@

Add Authenticator App + Add Security Key

-- 2.45.2 From 49716a8bf57e0bde0c6edc48dd0ea642a029b1a7 Mon Sep 17 00:00:00 2001 From: Pierre Hubert Date: Thu, 21 Apr 2022 19:24:26 +0200 Subject: [PATCH 02/10] Register user security keys --- assets/js/base64_lib.js | 243 ++++++++++++++++++++++ src/controllers/two_factor_api.rs | 39 ++++ src/data/user.rs | 8 +- src/data/webauthn_manager.rs | 29 ++- src/main.rs | 1 + templates/settings/add_webauthn_page.html | 74 ++++++- 6 files changed, 387 insertions(+), 7 deletions(-) create mode 100644 assets/js/base64_lib.js diff --git a/assets/js/base64_lib.js b/assets/js/base64_lib.js new file mode 100644 index 0000000..588f88e --- /dev/null +++ b/assets/js/base64_lib.js @@ -0,0 +1,243 @@ +// From : https://gitlab.com/comunic/comunicconsole/-/raw/master/src/utils/Base64Lib.ts + +/* +MIT License + +Copyright (c) 2020 Egor Nepomnyaschih + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +/* +// This constant can also be computed with the following algorithm: +const base64abc = [], + A = "A".charCodeAt(0), + a = "a".charCodeAt(0), + n = "0".charCodeAt(0); +for (let i = 0; i < 26; ++i) { + base64abc.push(String.fromCharCode(A + i)); +} +for (let i = 0; i < 26; ++i) { + base64abc.push(String.fromCharCode(a + i)); +} +for (let i = 0; i < 10; ++i) { + base64abc.push(String.fromCharCode(n + i)); +} +base64abc.push("+"); +base64abc.push("/"); +*/ +const base64abc = [ + "A", + "B", + "C", + "D", + "E", + "F", + "G", + "H", + "I", + "J", + "K", + "L", + "M", + "N", + "O", + "P", + "Q", + "R", + "S", + "T", + "U", + "V", + "W", + "X", + "Y", + "Z", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "+", + "/", +]; + +/* +// This constant can also be computed with the following algorithm: +const l = 256, base64codes = new Uint8Array(l); +for (let i = 0; i < l; ++i) { + base64codes[i] = 255; // invalid character +} +base64abc.forEach((char, index) => { + base64codes[char.charCodeAt(0)] = index; +}); +base64codes["=".charCodeAt(0)] = 0; // ignored anyway, so we just need to prevent an error +*/ +const base64codes = [ + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 62, 255, + 255, 255, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 255, 255, 255, 0, 255, + 255, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, + 19, 20, 21, 22, 23, 24, 25, 255, 255, 255, 255, 255, 255, 26, 27, 28, 29, + 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, + 49, 50, 51, +]; + +function getBase64Code(charCode) { + if (charCode >= base64codes.length) { + throw new Error("Unable to parse base64 string."); + } + const code = base64codes[charCode]; + if (code === 255) { + throw new Error("Unable to parse base64 string."); + } + return code; +} + +function bytesToBase64(bytes) { + let result = "", + i, + l = bytes.length; + for (i = 2; i < l; i += 3) { + result += base64abc[bytes[i - 2] >> 2]; + result += base64abc[((bytes[i - 2] & 0x03) << 4) | (bytes[i - 1] >> 4)]; + result += base64abc[((bytes[i - 1] & 0x0f) << 2) | (bytes[i] >> 6)]; + result += base64abc[bytes[i] & 0x3f]; + } + if (i === l + 1) { + // 1 octet yet to write + result += base64abc[bytes[i - 2] >> 2]; + result += base64abc[(bytes[i - 2] & 0x03) << 4]; + result += "=="; + } + if (i === l) { + // 2 octets yet to write + result += base64abc[bytes[i - 2] >> 2]; + result += base64abc[((bytes[i - 2] & 0x03) << 4) | (bytes[i - 1] >> 4)]; + result += base64abc[(bytes[i - 1] & 0x0f) << 2]; + result += "="; + } + return result; +} + +function base64ToBytes(str) { + if (str.length % 4 !== 0) { + throw new Error("Unable to parse base64 string."); + } + const index = str.indexOf("="); + if (index !== -1 && index < str.length - 2) { + throw new Error("Unable to parse base64 string."); + } + let missingOctets = str.endsWith("==") ? 2 : str.endsWith("=") ? 1 : 0, + n = str.length, + result = new Uint8Array(3 * (n / 4)), + buffer; + for (let i = 0, j = 0; i < n; i += 4, j += 3) { + buffer = + (getBase64Code(str.charCodeAt(i)) << 18) | + (getBase64Code(str.charCodeAt(i + 1)) << 12) | + (getBase64Code(str.charCodeAt(i + 2)) << 6) | + getBase64Code(str.charCodeAt(i + 3)); + result[j] = buffer >> 16; + result[j + 1] = (buffer >> 8) & 0xff; + result[j + 2] = buffer & 0xff; + } + return result.subarray(0, result.length - missingOctets); +} + +function base64encode(str, encoder = new TextEncoder()) { + return bytesToBase64(encoder.encode(str)); +} + +function base64decode(str, decoder = new TextDecoder()) { + return decoder.decode(base64ToBytes(str)); +} + + + +// From: https://gitlab.com/comunic/comunicconsole/-/raw/master/src/utils/Base64Utils.ts + +/** + * Add padding to base64 string + * + * Based on : https://gist.github.com/catwell/3046205 + * + * @param input Input base64, without padding + */ +function base64AddPadding(input) { + const remainder = input.length % 4; + + if (remainder === 2) input += "=="; + else if (remainder === 3) input += "="; + + return input.replaceAll("-", "+").replaceAll("_", "/"); +} + +/** + * Turn a base64 string without padding into Uint8Array + * + * @param input Input base64 (without padding) string + */ +function base64NoPaddingToUint8Array(input) { + return Uint8Array.from(atob(base64AddPadding(input)), (c) => + c.charCodeAt(0) + ); +} + +/** + * Convert a buffer to a base64-encoded string + * + * @param buff Buffer to convert + */ +function ArrayBufferToBase64(buff) { + const arr = new Uint8Array(buff); + return bytesToBase64(arr); +} \ No newline at end of file diff --git a/src/controllers/two_factor_api.rs b/src/controllers/two_factor_api.rs index c8874b6..239f84f 100644 --- a/src/controllers/two_factor_api.rs +++ b/src/controllers/two_factor_api.rs @@ -1,12 +1,14 @@ use actix::Addr; use actix_web::{HttpResponse, Responder, web}; use uuid::Uuid; +use webauthn_rs::proto::RegisterPublicKeyCredential; use crate::actors::users_actor; use crate::actors::users_actor::UsersActor; use crate::data::current_user::CurrentUser; use crate::data::totp_key::TotpKey; use crate::data::user::{FactorID, TwoFactor, TwoFactorType, User}; +use crate::data::webauthn_manager::WebAuthManagerReq; #[derive(serde::Deserialize)] pub struct AddTOTPRequest { @@ -45,6 +47,43 @@ pub async fn save_totp_factor(user: CurrentUser, form: web::Json } } +#[derive(serde::Deserialize)] +pub struct AddWebauthnRequest { + opaque_state: String, + factor_name: String, + credential: RegisterPublicKeyCredential, +} + +pub async fn save_webauthn_factor(user: CurrentUser, form: web::Json, + users: web::Data>, + manager: WebAuthManagerReq) -> impl Responder { + let key = match manager.finish_registration( + &user, + &form.0.opaque_state, + form.0.credential, + ) { + Ok(k) => k, + Err(e) => { + log::error!("Failed to register security key! {:?}", e); + return HttpResponse::InternalServerError().body("Failed to register key!"); + } + }; + + let mut user = User::from(user); + user.add_factor(TwoFactor { + id: FactorID(Uuid::new_v4().to_string()), + name: form.0.factor_name, + kind: TwoFactorType::WEBAUTHN(key), + }); + let res = users.send(users_actor::UpdateUserRequest(user)).await.unwrap().0; + + if !res { + HttpResponse::InternalServerError().body("Failed to update user information!") + } else { + HttpResponse::Ok().body("Added new factor!") + } +} + #[derive(serde::Deserialize)] pub struct DeleteFactorRequest { id: FactorID, diff --git a/src/data/user.rs b/src/data/user.rs index 8ded161..9f3d983 100644 --- a/src/data/user.rs +++ b/src/data/user.rs @@ -2,6 +2,7 @@ use crate::data::client::ClientID; use crate::data::entity_manager::EntityManager; use crate::data::login_redirect::LoginRedirect; use crate::data::totp_key::TotpKey; +use crate::data::webauthn_manager::WebauthnPubKey; use crate::utils::err::Res; #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] @@ -13,7 +14,7 @@ pub struct FactorID(pub String); #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub enum TwoFactorType { TOTP(TotpKey), - _OTHER, + WEBAUTHN(WebauthnPubKey), } #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] @@ -27,7 +28,7 @@ impl TwoFactor { pub fn type_str(&self) -> &'static str { match self.kind { TwoFactorType::TOTP(_) => "Authenticator app", - _ => unimplemented!() + TwoFactorType::WEBAUTHN(_) => "Security key", } } @@ -35,7 +36,8 @@ impl TwoFactor { match self.kind { TwoFactorType::TOTP(_) => format!("/2fa_otp?id={}&redirect={}", self.id.0, redirect_uri.get_encoded()), - _ => unimplemented!() + TwoFactorType::WEBAUTHN(_) => format!("/2fa_webauthn?id={}&redirect={}", + self.id.0, redirect_uri.get_encoded()), } } } diff --git a/src/data/webauthn_manager.rs b/src/data/webauthn_manager.rs index 2614160..be44013 100644 --- a/src/data/webauthn_manager.rs +++ b/src/data/webauthn_manager.rs @@ -1,8 +1,9 @@ +use std::io::ErrorKind; use std::sync::Arc; use actix_web::web; use webauthn_rs::{RegistrationState, Webauthn, WebauthnConfig}; -use webauthn_rs::proto::CreationChallengeResponse; +use webauthn_rs::proto::{CreationChallengeResponse, Credential, RegisterPublicKeyCredential}; use crate::constants::APP_NAME; use crate::data::app_config::AppConfig; @@ -35,6 +36,11 @@ pub struct RegisterKeyRequest { pub creation_challenge: CreationChallengeResponse, } +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +pub struct WebauthnPubKey { + creds: Credential, +} + #[derive(Debug, serde::Serialize, serde::Deserialize)] struct RegisterKeyOpaqueData { registration_state: RegistrationState, @@ -54,7 +60,10 @@ impl WebAuthManager { core: Webauthn::new(WebAuthnAppConfig { origin: url::Url::parse(&conf.website_origin) .expect("Failed to parse configuration origin!"), - relying_party_id: conf.domain_name().to_string(), + relying_party_id: conf.domain_name().split_once(':') + .map(|s| s.0) + .unwrap_or(conf.domain_name()) + .to_string(), }), crypto_wrapper: CryptoWrapper::new_random(), } @@ -63,7 +72,7 @@ impl WebAuthManager { pub fn start_register(&self, user: &User) -> Res { let (creation_challenge, registration_state) = self.core.generate_challenge_register( &user.username, - true, + false, )?; Ok(RegisterKeyRequest { @@ -74,4 +83,18 @@ impl WebAuthManager { creation_challenge, }) } + + pub fn finish_registration(&self, user: &User, opaque_state: &str, + pub_cred: RegisterPublicKeyCredential) -> Res { + let state: RegisterKeyOpaqueData = self.crypto_wrapper.decrypt(opaque_state)?; + if state.user_id != user.uid { + return Err(Box::new( + std::io::Error::new(ErrorKind::Other, "Invalid user for pubkey!"))); + } + + let res = self.core + .register_credential(&pub_cred, &state.registration_state, |_| Ok(false))?; + + Ok(WebauthnPubKey { creds: res.0 }) + } } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 65468c8..1806614 100644 --- a/src/main.rs +++ b/src/main.rs @@ -133,6 +133,7 @@ async fn main() -> std::io::Result<()> { // User API .route("/settings/api/two_factor/save_totp_factor", web::post().to(two_factor_api::save_totp_factor)) + .route("/settings/api/two_factor/save_webauthn_factor", web::post().to(two_factor_api::save_webauthn_factor)) .route("/settings/api/two_factor/delete_factor", web::post().to(two_factor_api::delete_factor)) // Admin routes diff --git a/templates/settings/add_webauthn_page.html b/templates/settings/add_webauthn_page.html index 15e3ea8..56873fe 100644 --- a/templates/settings/add_webauthn_page.html +++ b/templates/settings/add_webauthn_page.html @@ -2,11 +2,83 @@ {% block content %}
-

In order, to continue, please insert your security key & approve the registration request.

+

In order to continue, please click on the "Start Enrollment" button, insert your security key and approve the + registration request.

+
+ + + Please give a name to your key to identify it more easily later. +
Please give a name to this security key
+
+ + + +
-- 2.45.2 From 0f2fe87b5de930f6ac8d34b7eb4a48a86cd08d95 Mon Sep 17 00:00:00 2001 From: Pierre Hubert Date: Thu, 21 Apr 2022 19:26:50 +0200 Subject: [PATCH 03/10] cargo clippy --- src/data/webauthn_manager.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/webauthn_manager.rs b/src/data/webauthn_manager.rs index be44013..69e320c 100644 --- a/src/data/webauthn_manager.rs +++ b/src/data/webauthn_manager.rs @@ -62,7 +62,7 @@ impl WebAuthManager { .expect("Failed to parse configuration origin!"), relying_party_id: conf.domain_name().split_once(':') .map(|s| s.0) - .unwrap_or(conf.domain_name()) + .unwrap_or_else(|| conf.domain_name()) .to_string(), }), crypto_wrapper: CryptoWrapper::new_random(), -- 2.45.2 From 0f17a8a35c961592e8898ba9420231871d7303ef Mon Sep 17 00:00:00 2001 From: Pierre Hubert Date: Thu, 21 Apr 2022 19:28:54 +0200 Subject: [PATCH 04/10] Better sentence meaning --- templates/login/choose_second_factor.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/login/choose_second_factor.html b/templates/login/choose_second_factor.html index c351656..384b291 100644 --- a/templates/login/choose_second_factor.html +++ b/templates/login/choose_second_factor.html @@ -2,7 +2,7 @@ {% block content %}
-

You need to validate a second factor to validate your login.

+

You need to validate a second factor to complete your login.

{% for factor in factors %}

-- 2.45.2 From f09a62f8dfce321cdb1069055ac086412e1f91ab Mon Sep 17 00:00:00 2001 From: Pierre Hubert Date: Thu, 21 Apr 2022 19:31:38 +0200 Subject: [PATCH 05/10] Disable "Start enrollment" button while processing credentials --- templates/settings/add_webauthn_page.html | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/templates/settings/add_webauthn_page.html b/templates/settings/add_webauthn_page.html index 56873fe..0838d58 100644 --- a/templates/settings/add_webauthn_page.html +++ b/templates/settings/add_webauthn_page.html @@ -15,7 +15,7 @@

+ style="margin-top: 20px;" id="submitButton" /> -- 2.45.2 From 1d69ea536f501866a5166943f28633fa043a1187 Mon Sep 17 00:00:00 2001 From: Pierre Hubert Date: Sat, 23 Apr 2022 18:56:14 +0200 Subject: [PATCH 06/10] Get auth challenge --- README.md | 1 + src/controllers/login_controller.rs | 74 +++++++++++++++++++++++++++++ src/data/webauthn_manager.rs | 40 +++++++++++++--- src/main.rs | 1 + templates/login/webauthn_input.html | 20 ++++++++ 5 files changed, 129 insertions(+), 7 deletions(-) create mode 100644 templates/login/webauthn_input.html diff --git a/README.md b/README.md index 86287da..d8265a3 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ Features : * [x] TOTP (authenticator app) * [ ] Using a security key * [ ] Fully responsive webui +* [ ] `robots.txt` file to prevent indexing ## Compiling You will need the Rust toolchain to compile this project. To build it for production, just run: diff --git a/src/controllers/login_controller.rs b/src/controllers/login_controller.rs index 4d9c718..0ccdac8 100644 --- a/src/controllers/login_controller.rs +++ b/src/controllers/login_controller.rs @@ -12,6 +12,7 @@ use crate::data::login_redirect::LoginRedirect; use crate::data::remote_ip::RemoteIP; use crate::data::session_identity::{SessionIdentity, SessionStatus}; use crate::data::user::{FactorID, TwoFactor, TwoFactorType, User}; +use crate::data::webauthn_manager::WebAuthManagerReq; struct BaseLoginPage<'a> { danger: Option, @@ -49,6 +50,15 @@ struct LoginWithOTPTemplate<'a> { factor: &'a TwoFactor, } +#[derive(Template)] +#[template(path = "login/webauthn_input.html")] +struct LoginWithWebauthnTemplate<'a> { + _p: BaseLoginPage<'a>, + factor: &'a TwoFactor, + opaque_state: String, + challenge_json: String, +} + #[derive(serde::Deserialize)] pub struct LoginRequestBody { @@ -327,4 +337,68 @@ pub async fn login_with_otp(id: Identity, query: web::Query, }, factor, }.render().unwrap()) +} + +#[derive(serde::Deserialize)] +pub struct LoginWithWebauthnQuery { + #[serde(default)] + redirect: LoginRedirect, + id: FactorID, +} + + +/// Login with Webauthn +pub async fn login_with_webauthn(id: Identity, query: web::Query, + manager: WebAuthManagerReq, + users: web::Data>) -> impl Responder { + if !SessionIdentity(&id).need_2fa_auth() { + return redirect_user_for_login(query.redirect.get()); + } + + let user: User = users.send(users_actor::GetUserRequest(SessionIdentity(&id).user_id())) + .await.unwrap().0.expect("Could not find user!"); + + let factor = match user.find_factor(&query.id) { + Some(f) => f, + None => return HttpResponse::Ok() + .body(FatalErrorPage { message: "Factor not found!" }.render().unwrap()) + }; + + let key = match &factor.kind { + TwoFactorType::WEBAUTHN(key) => key, + _ => { + return HttpResponse::Ok() + .body(FatalErrorPage { message: "Factor is not a Webauthn key!" }.render().unwrap()); + } + }; + + let challenge = match manager.start_authentication(&user.uid, key) { + Ok(c) => c, + Err(e) => { + log::error!("Failed to generate webauthn challenge! {:?}", e); + return HttpResponse::InternalServerError() + .body(FatalErrorPage { message: "Failed to generate webauthn challenge" }.render().unwrap()); + } + }; + + let challenge_json = match serde_json::to_string(&challenge.login_challenge) { + Ok(r) => r, + Err(e) => { + log::error!("Failed to serialize challenge! {:?}", e); + return HttpResponse::InternalServerError().body("Failed to serialize challenge!"); + } + }; + + HttpResponse::Ok().body(LoginWithWebauthnTemplate { + _p: BaseLoginPage { + danger: None, + success: None, + page_title: "Two-Factor Auth", + app_name: APP_NAME, + redirect_uri: &query.redirect, + }, + factor, + opaque_state: challenge.opaque_state, + challenge_json: urlencoding::encode(&challenge_json).to_string(), + }.render().unwrap()) } \ No newline at end of file diff --git a/src/data/webauthn_manager.rs b/src/data/webauthn_manager.rs index 69e320c..55f2c42 100644 --- a/src/data/webauthn_manager.rs +++ b/src/data/webauthn_manager.rs @@ -2,8 +2,8 @@ use std::io::ErrorKind; use std::sync::Arc; use actix_web::web; -use webauthn_rs::{RegistrationState, Webauthn, WebauthnConfig}; -use webauthn_rs::proto::{CreationChallengeResponse, Credential, RegisterPublicKeyCredential}; +use webauthn_rs::{AuthenticationState, RegistrationState, Webauthn, WebauthnConfig}; +use webauthn_rs::proto::{CreationChallengeResponse, Credential, RegisterPublicKeyCredential, RequestChallengeResponse}; use crate::constants::APP_NAME; use crate::data::app_config::AppConfig; @@ -31,22 +31,34 @@ impl WebauthnConfig for WebAuthnAppConfig { } } -pub struct RegisterKeyRequest { - pub opaque_state: String, - pub creation_challenge: CreationChallengeResponse, -} - #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct WebauthnPubKey { creds: Credential, } +pub struct RegisterKeyRequest { + pub opaque_state: String, + pub creation_challenge: CreationChallengeResponse, +} + #[derive(Debug, serde::Serialize, serde::Deserialize)] struct RegisterKeyOpaqueData { registration_state: RegistrationState, user_id: UserID, } +pub struct AuthRequest { + pub opaque_state: String, + pub login_challenge: RequestChallengeResponse, +} + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +struct AuthStateOpaqueData { + authentication_state: AuthenticationState, + user_id: UserID, +} + + pub type WebAuthManagerReq = web::Data>; pub struct WebAuthManager { @@ -97,4 +109,18 @@ impl WebAuthManager { Ok(WebauthnPubKey { creds: res.0 }) } + + pub fn start_authentication(&self, user_id: &UserID, key: &WebauthnPubKey) -> Res { + let (login_challenge, authentication_state) = self.core.generate_challenge_authenticate(vec![ + key.creds.clone() + ])?; + + Ok(AuthRequest { + opaque_state: self.crypto_wrapper.encrypt(&AuthStateOpaqueData { + authentication_state, + user_id: user_id.clone(), + })?, + login_challenge, + }) + } } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 1806614..8cb1004 100644 --- a/src/main.rs +++ b/src/main.rs @@ -119,6 +119,7 @@ async fn main() -> std::io::Result<()> { .route("/2fa_auth", web::get().to(login_controller::choose_2fa_method)) .route("/2fa_otp", web::get().to(login_controller::login_with_otp)) .route("/2fa_otp", web::post().to(login_controller::login_with_otp)) + .route("/2fa_webauthn", web::get().to(login_controller::login_with_webauthn)) // Logout page .route("/logout", web::get().to(login_controller::logout_route)) diff --git a/templates/login/webauthn_input.html b/templates/login/webauthn_input.html new file mode 100644 index 0000000..a70ad66 --- /dev/null +++ b/templates/login/webauthn_input.html @@ -0,0 +1,20 @@ +{% extends "base_login_page.html" %} +{% block content %} + + +
+

Please insert now your security key {{ factor.name }}, and accept authentication request.

+
+ + + + + + +{% endblock content %} \ No newline at end of file -- 2.45.2 From 05d3bee328a0ef502e2d5316739e57250a44f0a9 Mon Sep 17 00:00:00 2001 From: Pierre Hubert Date: Sat, 23 Apr 2022 19:20:59 +0200 Subject: [PATCH 07/10] Send authenticate request --- templates/login/webauthn_input.html | 70 +++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/templates/login/webauthn_input.html b/templates/login/webauthn_input.html index a70ad66..7b79b8f 100644 --- a/templates/login/webauthn_input.html +++ b/templates/login/webauthn_input.html @@ -1,20 +1,90 @@ {% extends "base_login_page.html" %} {% block content %} +

Please insert now your security key {{ factor.name }}, and accept authentication request.

+
+ +
+ {% endblock content %} \ No newline at end of file -- 2.45.2 From 9e345895ff03126ebc59d8d560df9edd4d0ca2e9 Mon Sep 17 00:00:00 2001 From: Pierre Hubert Date: Sat, 23 Apr 2022 20:17:49 +0200 Subject: [PATCH 08/10] Managed to authenticate user using Webauthn --- src/controllers/login_api.rs | 33 +++++++++++++++++++++++++++++++++ src/controllers/mod.rs | 1 + src/data/webauthn_manager.rs | 17 ++++++++++++++++- src/main.rs | 7 ++++--- 4 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 src/controllers/login_api.rs diff --git a/src/controllers/login_api.rs b/src/controllers/login_api.rs new file mode 100644 index 0000000..afa4355 --- /dev/null +++ b/src/controllers/login_api.rs @@ -0,0 +1,33 @@ +use actix_identity::Identity; +use actix_web::{HttpResponse, Responder, web}; +use webauthn_rs::proto::PublicKeyCredential; + +use crate::data::session_identity::{SessionIdentity, SessionStatus}; +use crate::data::webauthn_manager::WebAuthManagerReq; + +#[derive(serde::Deserialize)] +pub struct AuthWebauthnRequest { + opaque_state: String, + credential: PublicKeyCredential, +} + +pub async fn auth_webauthn(id: Identity, + req: web::Json, + manager: WebAuthManagerReq) -> impl Responder { + if !SessionIdentity(&id).need_2fa_auth() { + return HttpResponse::Unauthorized().json("No 2FA required!"); + } + + let user_id = SessionIdentity(&id).user_id(); + + match manager.finish_authentication(&user_id, &req.opaque_state, &req.credential) { + Ok(_) => { + SessionIdentity(&id).set_status(SessionStatus::SignedIn); + HttpResponse::Ok().body("You are authenticated!") + } + Err(e) => { + log::error!("Failed to authenticate user using webauthn! {:?}", e); + HttpResponse::InternalServerError().body("Failed to validate security key!") + } + } +} \ No newline at end of file diff --git a/src/controllers/mod.rs b/src/controllers/mod.rs index a94576d..24e865d 100644 --- a/src/controllers/mod.rs +++ b/src/controllers/mod.rs @@ -1,6 +1,7 @@ pub mod assets_controller; pub mod base_controller; pub mod login_controller; +pub mod login_api; pub mod settings_controller; pub mod admin_controller; pub mod admin_api; diff --git a/src/data/webauthn_manager.rs b/src/data/webauthn_manager.rs index 55f2c42..392e7d6 100644 --- a/src/data/webauthn_manager.rs +++ b/src/data/webauthn_manager.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use actix_web::web; use webauthn_rs::{AuthenticationState, RegistrationState, Webauthn, WebauthnConfig}; -use webauthn_rs::proto::{CreationChallengeResponse, Credential, RegisterPublicKeyCredential, RequestChallengeResponse}; +use webauthn_rs::proto::{CreationChallengeResponse, Credential, PublicKeyCredential, RegisterPublicKeyCredential, RequestChallengeResponse}; use crate::constants::APP_NAME; use crate::data::app_config::AppConfig; @@ -45,6 +45,7 @@ pub struct RegisterKeyRequest { struct RegisterKeyOpaqueData { registration_state: RegistrationState, user_id: UserID, + // TODO : add time } pub struct AuthRequest { @@ -56,6 +57,7 @@ pub struct AuthRequest { struct AuthStateOpaqueData { authentication_state: AuthenticationState, user_id: UserID, + // TODO : add time } @@ -123,4 +125,17 @@ impl WebAuthManager { login_challenge, }) } + + pub fn finish_authentication(&self, user_id: &UserID, opaque_state: &str, + pub_cred: &PublicKeyCredential) -> Res { + let state: AuthStateOpaqueData = self.crypto_wrapper.decrypt(opaque_state)?; + if &state.user_id != user_id { + return Err(Box::new( + std::io::Error::new(ErrorKind::Other, "Invalid user for pubkey!"))); + } + + self.core.authenticate_credential(pub_cred, &state.authentication_state)?; + + Ok(()) + } } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 8cb1004..829eb0e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -111,7 +111,8 @@ async fn main() -> std::io::Result<()> { // Assets serving .route("/assets/{path:.*}", web::get().to(assets_route)) - // Login page + // Login pages + .route("/logout", web::get().to(login_controller::logout_route)) .route("/login", web::get().to(login_controller::login_route)) .route("/login", web::post().to(login_controller::login_route)) .route("/reset_password", web::get().to(login_controller::reset_password_route)) @@ -121,8 +122,8 @@ async fn main() -> std::io::Result<()> { .route("/2fa_otp", web::post().to(login_controller::login_with_otp)) .route("/2fa_webauthn", web::get().to(login_controller::login_with_webauthn)) - // Logout page - .route("/logout", web::get().to(login_controller::logout_route)) + // Login api + .route("/login/api/auth_webauthn", web::post().to(login_api::auth_webauthn)) // Settings routes .route("/settings", web::get().to(settings_controller::account_settings_details_route)) -- 2.45.2 From 933c8ff024a397d0e48daede29aa2da96549d6fa Mon Sep 17 00:00:00 2001 From: Pierre Hubert Date: Sat, 23 Apr 2022 20:22:32 +0200 Subject: [PATCH 09/10] Add expiration to webauthn challenges --- src/constants.rs | 6 +++++- src/data/webauthn_manager.rs | 19 ++++++++++++++++--- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/constants.rs b/src/constants.rs index 563c983..f60cbba 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -56,4 +56,8 @@ pub const OPEN_ID_AUTHORIZATION_CODE_TIMEOUT: u64 = 300; pub const OPEN_ID_ACCESS_TOKEN_LEN: usize = 50; pub const OPEN_ID_ACCESS_TOKEN_TIMEOUT: u64 = 3600; pub const OPEN_ID_REFRESH_TOKEN_LEN: usize = 120; -pub const OPEN_ID_REFRESH_TOKEN_TIMEOUT: u64 = 360000; \ No newline at end of file +pub const OPEN_ID_REFRESH_TOKEN_TIMEOUT: u64 = 360000; + +/// Webauthn constants +pub const WEBAUTHN_REGISTER_CHALLENGE_EXPIRE: u64 = 3600; +pub const WEBAUTHN_LOGIN_CHALLENGE_EXPIRE: u64 = 3600; \ No newline at end of file diff --git a/src/data/webauthn_manager.rs b/src/data/webauthn_manager.rs index 392e7d6..8d88282 100644 --- a/src/data/webauthn_manager.rs +++ b/src/data/webauthn_manager.rs @@ -5,11 +5,12 @@ use actix_web::web; use webauthn_rs::{AuthenticationState, RegistrationState, Webauthn, WebauthnConfig}; use webauthn_rs::proto::{CreationChallengeResponse, Credential, PublicKeyCredential, RegisterPublicKeyCredential, RequestChallengeResponse}; -use crate::constants::APP_NAME; +use crate::constants::{APP_NAME, WEBAUTHN_LOGIN_CHALLENGE_EXPIRE, WEBAUTHN_REGISTER_CHALLENGE_EXPIRE}; use crate::data::app_config::AppConfig; use crate::data::crypto_wrapper::CryptoWrapper; use crate::data::user::{User, UserID}; use crate::utils::err::Res; +use crate::utils::time::time; #[derive(Debug)] struct WebAuthnAppConfig { @@ -45,7 +46,7 @@ pub struct RegisterKeyRequest { struct RegisterKeyOpaqueData { registration_state: RegistrationState, user_id: UserID, - // TODO : add time + expire: u64, } pub struct AuthRequest { @@ -57,7 +58,7 @@ pub struct AuthRequest { struct AuthStateOpaqueData { authentication_state: AuthenticationState, user_id: UserID, - // TODO : add time + expire: u64, } @@ -93,6 +94,7 @@ impl WebAuthManager { opaque_state: self.crypto_wrapper.encrypt(&RegisterKeyOpaqueData { registration_state, user_id: user.uid.clone(), + expire: time() + WEBAUTHN_REGISTER_CHALLENGE_EXPIRE, })?, creation_challenge, }) @@ -106,6 +108,11 @@ impl WebAuthManager { std::io::Error::new(ErrorKind::Other, "Invalid user for pubkey!"))); } + if state.expire < time() { + return Err(Box::new( + std::io::Error::new(ErrorKind::Other, "Challenge has expired!"))); + } + let res = self.core .register_credential(&pub_cred, &state.registration_state, |_| Ok(false))?; @@ -121,6 +128,7 @@ impl WebAuthManager { opaque_state: self.crypto_wrapper.encrypt(&AuthStateOpaqueData { authentication_state, user_id: user_id.clone(), + expire: time() + WEBAUTHN_LOGIN_CHALLENGE_EXPIRE, })?, login_challenge, }) @@ -134,6 +142,11 @@ impl WebAuthManager { std::io::Error::new(ErrorKind::Other, "Invalid user for pubkey!"))); } + if state.expire < time() { + return Err(Box::new( + std::io::Error::new(ErrorKind::Other, "Challenge has expired!"))); + } + self.core.authenticate_credential(pub_cred, &state.authentication_state)?; Ok(()) -- 2.45.2 From 822b78237aeac7548d8d5ef35024e24e5eababce Mon Sep 17 00:00:00 2001 From: Pierre Hubert Date: Sat, 23 Apr 2022 20:23:23 +0200 Subject: [PATCH 10/10] Finish implementation of Webauthn! --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d8265a3..e387800 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Features : * [x] Bruteforce protection * [ ] 2 factors authentication * [x] TOTP (authenticator app) - * [ ] Using a security key + * [x] Using a security key * [ ] Fully responsive webui * [ ] `robots.txt` file to prevent indexing -- 2.45.2