feat: Added support for passkey vault unlock feature

Co-authored-by: Keng <39216134+kengzzzz@users.noreply.github.com>
This commit is contained in:
Raphaël Roumezin 2026-06-30 11:07:00 +02:00 committed by Raphael Roumezin
parent 3b669264bb
commit 9e4aa5efe7
No known key found for this signature in database
GPG Key ID: 91D7A94FE35B7922
6 changed files with 60 additions and 16 deletions

View File

@ -12,9 +12,11 @@ use serde_json::Value;
use crate::{
CONFIG,
api::{self, EmptyResult, JsonResult, Notify, PasswordOrOtpData, UpdateType, core::log_event},
auth::ClientVersion,
auth::{Headers, OrgIdGuard, OwnerHeaders},
api::{
self, EmptyResult, JsonResult, Notify, PasswordOrOtpData, UpdateType,
core::{log_event, webauthn_prf_option},
},
auth::{ClientVersion, Headers, OrgIdGuard, OwnerHeaders},
config::PathType,
crypto,
db::{
@ -22,7 +24,7 @@ use crate::{
models::{
Archive, Attachment, AttachmentId, Cipher, CipherId, Collection, CollectionCipher, CollectionGroup,
CollectionId, CollectionUser, EventType, Favorite, Folder, FolderCipher, FolderId, Group, Membership,
MembershipType, OrgPolicy, OrgPolicyType, OrganizationId, RepromptType, Send, UserId,
MembershipType, OrgPolicy, OrgPolicyType, OrganizationId, RepromptType, Send, UserId, WebauthnCredential,
},
},
util::{NumberOrString, deser_opt_nonempty_str, save_temp_file},
@ -188,6 +190,12 @@ async fn sync(data: SyncData, headers: Headers, client_version: Option<ClientVer
Value::Null
};
let webauthn_prf_options = WebauthnCredential::find_all_by_user(&headers.user.uuid, &conn)
.await
.iter()
.filter_map(|wac| webauthn_prf_option(wac, false))
.collect();
Ok(Json(json!({
"profile": user_json,
"folders": folders_json,
@ -196,13 +204,21 @@ async fn sync(data: SyncData, headers: Headers, client_version: Option<ClientVer
"ciphers": ciphers_json,
"domains": domains_json,
"sends": sends_json,
"userDecryption": {
"masterPasswordUnlock": master_password_unlock,
},
"userDecryption": sync_user_decryption(&master_password_unlock, webauthn_prf_options),
"object": "sync"
})))
}
fn sync_user_decryption(master_password_unlock: &Value, webauthn_prf_options: Vec<Value>) -> Value {
let mut user_decryption = json!({
"masterPasswordUnlock": master_password_unlock,
});
if !webauthn_prf_options.is_empty() {
user_decryption["webAuthnPrfOptions"] = webauthn_prf_options.into();
}
user_decryption
}
#[get("/ciphers")]
async fn get_ciphers(headers: Headers, conn: DbConn) -> JsonResult {
let ciphers = Cipher::find_by_user_visible(&headers.user.uuid, &conn).await;

View File

@ -15,7 +15,7 @@ pub use ciphers::{CipherData, CipherSyncData, CipherSyncType, purge_trashed_ciph
pub use emergency_access::{emergency_notification_reminder_job, emergency_request_timeout_job};
pub use events::{event_cleanup_job, log_event, log_user_event};
pub use sends::purge_sends;
pub use webauthn::WEBAUTHN_PASSWORDLESS;
pub use webauthn::{WEBAUTHN_PASSWORDLESS, webauthn_prf_option};
use reqwest::Method;
use rocket::{Catcher, Route, serde::json::Json, serde::json::Value};
@ -211,6 +211,11 @@ fn config() -> Json<Value> {
&FeatureFlagFilter::ValidOnly,
);
feature_states.insert("pm-19148-innovation-archive".to_owned(), true);
if CONFIG.passkey_login_allowed() {
feature_states.insert("pm-2035-passkey-unlock".to_owned(), true);
} else {
feature_states.remove("pm-2035-passkey-unlock");
}
Json(json!({
// Note: The clients use this version to handle backwards compatibility concerns

View File

@ -14,7 +14,7 @@ use crate::{
auth::Headers,
db::{
DbConn,
models::{TwoFactor, TwoFactorType, WebauthnCredential, WebauthnCredentialId},
models::{TwoFactor, TwoFactorType, WebauthnCredential, WebauthnCredentialId, WebauthnCredentialPrfStatus},
},
};
@ -44,6 +44,30 @@ pub fn routes() -> Vec<rocket::Route> {
routes![get_webauthn, post_webauthn, post_webauthn_attestation_options, post_webauthn_delete]
}
pub fn webauthn_prf_option(wac: &WebauthnCredential, pascal_case: bool) -> Option<Value> {
if !matches!(wac.get_prf_status(), WebauthnCredentialPrfStatus::Enabled) {
return None;
}
let passkey: Passkey = serde_json::from_str(&wac.credential).ok()?;
if pascal_case {
Some(json!({
"CredentialId": passkey.cred_id().to_owned(),
"Transports": [],
"EncryptedPrivateKey": wac.encrypted_private_key.as_ref()?,
"EncryptedUserKey": wac.encrypted_user_key.as_ref()?,
}))
} else {
Some(json!({
"credentialId": passkey.cred_id().to_owned(),
"transports": [],
"encryptedPrivateKey": wac.encrypted_private_key.as_ref()?,
"encryptedUserKey": wac.encrypted_user_key.as_ref()?,
}))
}
}
#[get("/webauthn")]
async fn get_webauthn(headers: Headers, conn: DbConn) -> JsonResult {
if !CONFIG.passkey_login_allowed() {

View File

@ -23,6 +23,7 @@ use crate::{
authenticator, duo, duo_oidc, email, enforce_2fa_policy, is_twofactor_provider_usable, webauthn,
yubikey,
},
webauthn_prf_option,
},
master_password_policy,
push::register_push_device,
@ -633,13 +634,10 @@ async fn webauthn_login(data: ConnectData, user_id: &mut Option<UserId>, conn: &
let mut result = authenticated_response(&user, &mut device, auth_tokens, None, conn, ip).await?;
// Add WebAuthnPrfOption if the credential has encrypted keys (PRF-based decryption)
if matched_wac.encrypted_private_key.is_some() && matched_wac.encrypted_user_key.is_some() {
// Add WebAuthnPrfOption if the credential has enabled PRF-based decryption.
if let Some(prf_option) = webauthn_prf_option(matched_wac, true) {
let Json(ref mut val) = result;
val["UserDecryptionOptions"]["WebAuthnPrfOption"] = json!({
"EncryptedPrivateKey": matched_wac.encrypted_private_key,
"EncryptedUserKey": matched_wac.encrypted_user_key,
});
val["UserDecryptionOptions"]["WebAuthnPrfOption"] = prf_option;
}
Ok(result)

View File

@ -1406,6 +1406,7 @@ pub const SUPPORTED_FEATURE_FLAGS: &[&str] = &[
"ssh-agent-v2",
// Key Management Team
"ssh-key-vault-item",
"pm-2035-passkey-unlock",
"pm-25373-windows-biometrics-v2",
// Mobile Team
"anon-addy-self-host-alias",

View File

@ -44,4 +44,4 @@ pub use self::two_factor::{TwoFactor, TwoFactorType};
pub use self::two_factor_duo_context::TwoFactorDuoContext;
pub use self::two_factor_incomplete::TwoFactorIncomplete;
pub use self::user::{Invitation, SsoUser, User, UserId, UserKdfType, UserStampException};
pub use self::webauthn_credential::{WebauthnCredential, WebauthnCredentialId};
pub use self::webauthn_credential::{WebauthnCredential, WebauthnCredentialId, WebauthnCredentialPrfStatus};