1
0
Fork 0
mirror of https://github.com/dani-garcia/vaultwarden.git synced 2025-07-17 09:41:07 +00:00

implement unecrypted passkey login and cleanup code a bit mor

This commit is contained in:
zUnixorn 2025-06-04 15:27:31 +02:00
parent a4b480dc9f
commit 22a3571c46
No known key found for this signature in database
GPG key ID: 0BE3A9CAE3E8D0DA
7 changed files with 90 additions and 94 deletions

View file

@ -4,8 +4,8 @@ CREATE TABLE web_authn_credentials (
name TEXT NOT NULL, name TEXT NOT NULL,
credential TEXT NOT NULL, credential TEXT NOT NULL,
supports_prf BOOLEAN NOT NULL, supports_prf BOOLEAN NOT NULL,
encrypted_user_key TEXT NOT NULL, encrypted_user_key TEXT,
encrypted_public_key TEXT NOT NULL, encrypted_public_key TEXT,
encrypted_private_key TEXT NOT NULL, encrypted_private_key TEXT,
FOREIGN KEY(user_uuid) REFERENCES users(uuid) FOREIGN KEY(user_uuid) REFERENCES users(uuid)
); );

View file

@ -204,6 +204,7 @@ async fn post_api_webauthn_delete(data: Json<PasswordOrOtpData>, uuid: String, h
Ok(Status::Ok) Ok(Status::Ok)
} }
// TODO replace this with something else
static WEBAUTHN_STATES: OnceLock<Mutex<HashMap<UserId, RegistrationState>>> = OnceLock::new(); static WEBAUTHN_STATES: OnceLock<Mutex<HashMap<UserId, RegistrationState>>> = OnceLock::new();
#[post("/webauthn/attestation-options", data = "<data>")] #[post("/webauthn/attestation-options", data = "<data>")]
@ -213,7 +214,7 @@ async fn post_api_webauthn_attestation_options(data: Json<PasswordOrOtpData>, he
data.validate(&user, false, &mut conn).await?; data.validate(&user, false, &mut conn).await?;
// C# does this check as well // TODO C# does this check as well, should there be an option in the admin panel to disable passkey login?
// await ValidateIfUserCanUsePasskeyLogin(user.Id); // await ValidateIfUserCanUsePasskeyLogin(user.Id);
// TODO add existing keys here when the table exists // TODO add existing keys here when the table exists
@ -240,71 +241,41 @@ async fn post_api_webauthn_attestation_options(data: Json<PasswordOrOtpData>, he
let mut options = serde_json::to_value(challenge.public_key)?; let mut options = serde_json::to_value(challenge.public_key)?;
options["status"] = "ok".into(); options["status"] = "ok".into();
options["errorMessage"] = "".into(); options["errorMessage"] = "".into();
// TODO does this need to be set?
// TODO test if the client actually expects this field to exist
options["extensions"] = Value::Object(serde_json::Map::new()); options["extensions"] = Value::Object(serde_json::Map::new());
// TODO make this nicer Ok(Json(json!({
let mut webauthn_credential_create_options = Value::Object(serde_json::Map::new()); "options": options,
webauthn_credential_create_options["options"] = options; "object": "webauthnCredentialCreateOptions"
webauthn_credential_create_options["object"] = "webauthnCredentialCreateOptions".into(); })))
// TODO this hopefully shouldn't be needed
// webauthn_credential_create_options["token"] = "atoken".into();
Ok(Json(webauthn_credential_create_options))
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
// TODO remove allow dead_code
#[allow(dead_code)]
struct WebAuthnLoginCredentialCreateRequest { struct WebAuthnLoginCredentialCreateRequest {
device_response: RegisterPublicKeyCredentialCopy, device_response: RegisterPublicKeyCredentialCopy,
name: String, name: String,
// TODO this is hopefully not needed
// token: String,
supports_prf: bool, supports_prf: bool,
encrypted_user_key: String, encrypted_user_key: Option<String>,
encrypted_public_key: String, encrypted_public_key: Option<String>,
encrypted_private_key: String, encrypted_private_key: Option<String>,
} }
#[post("/webauthn", data = "<data>")] #[post("/webauthn", data = "<data>")]
async fn post_api_webauthn(data: Json<WebAuthnLoginCredentialCreateRequest>, headers: Headers, mut conn: DbConn) -> ApiResult<Status> { async fn post_api_webauthn(data: Json<WebAuthnLoginCredentialCreateRequest>, headers: Headers, mut conn: DbConn) -> ApiResult<Status> {
// this check await ValidateIfUserCanUsePasskeyLogin(user.Id); again
let data: WebAuthnLoginCredentialCreateRequest = data.into_inner(); let data: WebAuthnLoginCredentialCreateRequest = data.into_inner();
// let data: WebAuthnLoginCredentialCreateRequest = serde_json::from_str(&data)?;
let user = headers.user; let user = headers.user;
// TODO Retrieve and delete the saved challenge state here
// Verify the credentials with the saved state // Verify the credentials with the saved state
let (credential, _data) = { let (credential, _data) = {
let mut states = WEBAUTHN_STATES.get().unwrap().lock().unwrap(); let mut states = WEBAUTHN_STATES.get().unwrap().lock().unwrap();
let state = states.remove(&user.uuid).unwrap(); let state = states.remove(&user.uuid).unwrap();
// TODO make the closure check if the credential already exists
WebauthnConfig::load(true).register_credential(&data.device_response.into(), &state, |_| Ok(false))? WebauthnConfig::load(true).register_credential(&data.device_response.into(), &state, |_| Ok(false))?
}; };
// TODO add existing keys here when the table exists
// let mut registrations: Vec<_> = get_webauthn_registrations(&user.uuid, &mut conn).await?.1;
// // TODO: Check for repeated ID's
// registrations.push(WebauthnRegistration {
// id: data.id.into_i32()?,
// name: data.name,
// migrated: false,
//
// credential,
// });
// let registrations = Vec::new();
// TODO Save the registration
// TwoFactor::new(user.uuid.clone(), TwoFactorType::Webauthn, serde_json::to_string(&registrations)?)
// .save(&mut conn)
// .await?;
WebAuthnCredential::new( WebAuthnCredential::new(
user.uuid, user.uuid,
data.name, data.name,
@ -326,10 +297,10 @@ async fn get_api_webauthn(headers: Headers, mut conn: DbConn) -> Json<Value> {
.await .await
.into_iter() .into_iter()
.map(|wac| { .map(|wac| {
// TODO generate prfStatus from GetPrfStatus() in C#
json!({ json!({
"id": wac.uuid, "id": wac.uuid,
"name": wac.name, "name": wac.name,
// TODO generate prfStatus like GetPrfStatus() does in the C# implementation
"prfStatus": 0, "prfStatus": 0,
"encryptedUserKey": wac.encrypted_user_key, "encryptedUserKey": wac.encrypted_user_key,
"encryptedPublicKey": wac.encrypted_public_key, "encryptedPublicKey": wac.encrypted_public_key,

View file

@ -146,7 +146,6 @@ async fn get_webauthn(data: Json<PasswordOrOtpData>, headers: Headers, mut conn:
}))) })))
} }
// TODO Creation call
#[post("/two-factor/get-webauthn-challenge", data = "<data>")] #[post("/two-factor/get-webauthn-challenge", data = "<data>")]
async fn generate_webauthn_challenge(data: Json<PasswordOrOtpData>, headers: Headers, mut conn: DbConn) -> JsonResult { async fn generate_webauthn_challenge(data: Json<PasswordOrOtpData>, headers: Headers, mut conn: DbConn) -> JsonResult {
let data: PasswordOrOtpData = data.into_inner(); let data: PasswordOrOtpData = data.into_inner();
@ -261,7 +260,6 @@ impl From<PublicKeyCredentialCopy> for PublicKeyCredential {
} }
} }
// TODO Confirmation call
#[post("/two-factor/webauthn", data = "<data>")] #[post("/two-factor/webauthn", data = "<data>")]
async fn activate_webauthn(data: Json<EnableWebauthnData>, headers: Headers, mut conn: DbConn) -> JsonResult { async fn activate_webauthn(data: Json<EnableWebauthnData>, headers: Headers, mut conn: DbConn) -> JsonResult {
let data: EnableWebauthnData = data.into_inner(); let data: EnableWebauthnData = data.into_inner();

View file

@ -125,7 +125,7 @@ pub struct PublicKeyCredentialCopy {
pub raw_id: Base64UrlSafeData, pub raw_id: Base64UrlSafeData,
pub response: AuthenticatorAssertionResponseRawCopy, pub response: AuthenticatorAssertionResponseRawCopy,
pub r#type: String, pub r#type: String,
// TODO think about what to do with this field, currently this is ignored in the conversion // This field is unused and discarded when converted to PublicKeyCredential
pub extensions: Option<Value>, pub extensions: Option<Value>,
} }
@ -199,43 +199,56 @@ async fn _webauthn_login(
let web_authn_credentials = WebAuthnCredential::find_all_by_user(&user.uuid, conn).await; let web_authn_credentials = WebAuthnCredential::find_all_by_user(&user.uuid, conn).await;
let credentials = web_authn_credentials let parsed_credentials = web_authn_credentials
.iter() .iter()
.map(|c| { .map(|c| {
serde_json::from_str(&c.credential) serde_json::from_str(&c.credential)
}).collect::<Result<Vec<Credential>, _>>()?; }).collect::<Result<Vec<Credential>, _>>()?;
let web_authn_credential = { let pairs = web_authn_credentials.into_iter()
.zip(parsed_credentials.clone())
.collect::<Vec<_>>();
let authenticator_data;
let (web_authn_credential, mut credential) = {
let token = data.token.as_ref().unwrap(); let token = data.token.as_ref().unwrap();
let mut states = WEBAUTHN_AUTHENTICATION_STATES.get().unwrap().lock().unwrap(); let mut states = WEBAUTHN_AUTHENTICATION_STATES.get().unwrap().lock().unwrap();
let mut state = states.remove(token).unwrap(); let mut state = states.remove(token).unwrap();
let resp = device_response.into(); let resp = device_response.into();
state.set_allowed_credentials(credentials); state.set_allowed_credentials(parsed_credentials);
// TODO update respective credential in database let credential_id;
let (credential_id, auth_data) = WebauthnConfig::load(true)
.authenticate_credential(&resp, &state)?; if let Ok((cred_id, auth_data)) = WebauthnConfig::load(true)
.authenticate_credential(&resp, &state) {
if !auth_data.user_verified { credential_id = cred_id;
// TODO throw an error here authenticator_data = auth_data;
panic!() } else {
} err!(
"Passkey authentication Failed.",
web_authn_credentials.into_iter()
.find(|c| &serde_json::from_str::<Credential>(&c.credential).unwrap().cred_id == credential_id)
.unwrap()
/* TODO return this error on failure
err!(
"Username or password is incorrect. Try again",
format!("IP: {}. Username: {username}.", ip.ip), format!("IP: {}. Username: {username}.", ip.ip),
ErrorEvent { ErrorEvent {
event: EventType::UserFailedLogIn, event: EventType::UserFailedLogIn,
} }
) )
*/ }
// TODO should this check be done? Since we need to trust the client here anyway ...
// if !auth_data.user_verified { some_error }
pairs.into_iter()
.find(|(_, c)| &c.cred_id == credential_id)
.unwrap()
}; };
// update the counter
credential.counter = authenticator_data.counter;
WebAuthnCredential::update_credential_by_uuid(
&web_authn_credential.uuid,
serde_json::to_string(&credential)?,
conn
).await?;
let now = Utc::now().naive_utc(); let now = Utc::now().naive_utc();
@ -273,7 +286,7 @@ async fn _webauthn_login(
let (mut device, new_device) = get_device(&data, conn, &user).await; let (mut device, new_device) = get_device(&data, conn, &user).await;
// TODO is this needed with passkeys? // TODO is this wanted with passkeys?
if CONFIG.mail_enabled() && new_device { if CONFIG.mail_enabled() && new_device {
if let Err(e) = mail::send_new_device_logged_in(&user.email, &ip.ip.to_string(), &now, &device).await { if let Err(e) = mail::send_new_device_logged_in(&user.email, &ip.ip.to_string(), &now, &device).await {
error!("Error sending new device email: {e:#?}"); error!("Error sending new device email: {e:#?}");
@ -335,7 +348,7 @@ async fn _webauthn_login(
json!({"Object": "masterPasswordPolicy"}) json!({"Object": "masterPasswordPolicy"})
}; };
let result = json!({ let mut result = json!({
"access_token": access_token, "access_token": access_token,
"expires_in": expires_in, "expires_in": expires_in,
"token_type": "Bearer", "token_type": "Bearer",
@ -354,13 +367,16 @@ async fn _webauthn_login(
"scope": scope, "scope": scope,
"UserDecryptionOptions": { "UserDecryptionOptions": {
"HasMasterPassword": !user.password_hash.is_empty(), "HasMasterPassword": !user.password_hash.is_empty(),
"WebAuthnPrfOption": {
"EncryptedPrivateKey": web_authn_credential.encrypted_private_key,
"EncryptedUserKey": web_authn_credential.encrypted_user_key,
},
"Object": "userDecryptionOptions" "Object": "userDecryptionOptions"
}, },
}); });
if web_authn_credential.encrypted_private_key.is_some() && web_authn_credential.encrypted_user_key.is_some() {
result["UserDecryptionOptions"]["WebAuthnPrfOption"] = json!({
"EncryptedPrivateKey": web_authn_credential.encrypted_private_key,
"EncryptedUserKey": web_authn_credential.encrypted_user_key,
})
}
info!("User {username} logged in successfully. IP: {}", ip.ip); info!("User {username} logged in successfully. IP: {}", ip.ip);
Ok(Json(result)) Ok(Json(result))
@ -963,6 +979,7 @@ async fn identity_register(data: Json<RegisterData>, conn: DbConn) -> JsonResult
_register(data, false, conn).await _register(data, false, conn).await
} }
// TODO this should be removed and either use something similar to what bitwarden employs or something else
static WEBAUTHN_AUTHENTICATION_STATES: OnceLock<Mutex<HashMap<String, AuthenticationState>>> = OnceLock::new(); static WEBAUTHN_AUTHENTICATION_STATES: OnceLock<Mutex<HashMap<String, AuthenticationState>>> = OnceLock::new();
#[get("/accounts/webauthn/assertion-options")] #[get("/accounts/webauthn/assertion-options")]
@ -972,8 +989,7 @@ fn get_web_authn_assertion_options() -> JsonResult {
Vec::new(), Vec::new(),
None, None,
)?; )?;
// TODO this needs to be solved in another way to avoid DoS
let t = util::get_uuid(); let t = util::get_uuid();
WEBAUTHN_AUTHENTICATION_STATES.get_or_init(|| Mutex::new(HashMap::new())).lock().unwrap().insert(t.clone(), state); WEBAUTHN_AUTHENTICATION_STATES.get_or_init(|| Mutex::new(HashMap::new())).lock().unwrap().insert(t.clone(), state);
@ -1052,7 +1068,7 @@ async fn register_finish(data: Json<RegisterData>, conn: DbConn) -> JsonResult {
struct ConnectData { struct ConnectData {
#[field(name = uncased("grant_type"))] #[field(name = uncased("grant_type"))]
#[field(name = uncased("granttype"))] #[field(name = uncased("granttype"))]
grant_type: String, // refresh_token, password, client_credentials (API key) grant_type: String, // refresh_token, password, client_credentials (API key), webauthn
// Needed for grant_type="refresh_token" // Needed for grant_type="refresh_token"
#[field(name = uncased("refresh_token"))] #[field(name = uncased("refresh_token"))]
@ -1100,10 +1116,10 @@ struct ConnectData {
#[field(name = uncased("authrequest"))] #[field(name = uncased("authrequest"))]
auth_request: Option<AuthRequestId>, auth_request: Option<AuthRequestId>,
// Needed for "login with passkey" // Needed for grant_type = "webauthn"
#[field(name = uncased("deviceresponse"))] #[field(name = uncased("deviceresponse"))]
device_response: Option<String>, device_response: Option<String>,
// TODO this may be removed again if implemented correctly // TODO this may be removed when `WEBAUTHN_AUTHENTICATION_STATES` is removed
#[field(name = uncased("token"))] #[field(name = uncased("token"))]
token: Option<String>, token: Option<String>,
} }

View file

@ -476,5 +476,5 @@ impl Invitation {
)] )]
#[deref(forward)] #[deref(forward)]
#[from(forward)] #[from(forward)]
// TODO create a way to construct this // TODO this also shouldn't be public
pub struct UserId(pub String); pub struct UserId(pub String);

View file

@ -16,9 +16,9 @@ db_object! {
pub name: String, pub name: String,
pub credential: String, pub credential: String,
pub supports_prf: bool, pub supports_prf: bool,
pub encrypted_user_key: String, pub encrypted_user_key: Option<String>,
pub encrypted_public_key: String, pub encrypted_public_key: Option<String>,
pub encrypted_private_key: String, pub encrypted_private_key: Option<String>,
} }
} }
@ -28,9 +28,9 @@ impl WebAuthnCredential {
name: String, name: String,
credential: String, credential: String,
supports_prf: bool, supports_prf: bool,
encrypted_user_key: String, encrypted_user_key: Option<String>,
encrypted_public_key: String, encrypted_public_key: Option<String>,
encrypted_private_key: String, encrypted_private_key: Option<String>,
) -> Self { ) -> Self {
Self { Self {
uuid: WebAuthnCredentialId(crate::util::get_uuid()), uuid: WebAuthnCredentialId(crate::util::get_uuid()),
@ -78,6 +78,16 @@ impl WebAuthnCredential {
).execute(conn).map_res("Error removing web_authn_credential for user") ).execute(conn).map_res("Error removing web_authn_credential for user")
}} }}
} }
pub async fn update_credential_by_uuid(uuid: &WebAuthnCredentialId, credential: String, conn: &mut DbConn) -> EmptyResult {
db_run! { conn: {
diesel::update(web_authn_credentials::table
.filter(web_authn_credentials::uuid.eq(uuid))
).set(web_authn_credentials::credential.eq(credential))
.execute(conn)
.map_res("Error updating credential for web_authn_credential")
}}
}
} }
#[derive( #[derive(
@ -96,4 +106,5 @@ impl WebAuthnCredential {
Deserialize, Deserialize,
UuidFromParam, UuidFromParam,
)] )]
// TODO this probably shouldn't need to be public
pub struct WebAuthnCredentialId(pub String); pub struct WebAuthnCredentialId(pub String);

View file

@ -327,9 +327,9 @@ table! {
name -> Text, name -> Text,
credential -> Text, credential -> Text,
supports_prf -> Bool, supports_prf -> Bool,
encrypted_user_key -> Text, encrypted_user_key -> Nullable<Text>,
encrypted_public_key -> Text, encrypted_public_key -> Nullable<Text>,
encrypted_private_key -> Text, encrypted_private_key -> Nullable<Text>,
} }
} }