Browse Source

feat: Added support for passkey vault unlock feature

Co-authored-by: Keng <39216134+kengzzzz@users.noreply.github.com>
pull/7370/head
Raphaël Roumezin 3 weeks ago
committed by Raphael Roumezin
parent
commit
9e4aa5efe7
No known key found for this signature in database GPG Key ID: 91D7A94FE35B7922
  1. 30
      src/api/core/ciphers.rs
  2. 7
      src/api/core/mod.rs
  3. 26
      src/api/core/webauthn.rs
  4. 10
      src/api/identity.rs
  5. 1
      src/config.rs
  6. 2
      src/db/models/mod.rs

30
src/api/core/ciphers.rs

@ -12,9 +12,11 @@ use serde_json::Value;
use crate::{ use crate::{
CONFIG, CONFIG,
api::{self, EmptyResult, JsonResult, Notify, PasswordOrOtpData, UpdateType, core::log_event}, api::{
auth::ClientVersion, self, EmptyResult, JsonResult, Notify, PasswordOrOtpData, UpdateType,
auth::{Headers, OrgIdGuard, OwnerHeaders}, core::{log_event, webauthn_prf_option},
},
auth::{ClientVersion, Headers, OrgIdGuard, OwnerHeaders},
config::PathType, config::PathType,
crypto, crypto,
db::{ db::{
@ -22,7 +24,7 @@ use crate::{
models::{ models::{
Archive, Attachment, AttachmentId, Cipher, CipherId, Collection, CollectionCipher, CollectionGroup, Archive, Attachment, AttachmentId, Cipher, CipherId, Collection, CollectionCipher, CollectionGroup,
CollectionId, CollectionUser, EventType, Favorite, Folder, FolderCipher, FolderId, Group, Membership, 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}, 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 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!({ Ok(Json(json!({
"profile": user_json, "profile": user_json,
"folders": folders_json, "folders": folders_json,
@ -196,13 +204,21 @@ async fn sync(data: SyncData, headers: Headers, client_version: Option<ClientVer
"ciphers": ciphers_json, "ciphers": ciphers_json,
"domains": domains_json, "domains": domains_json,
"sends": sends_json, "sends": sends_json,
"userDecryption": { "userDecryption": sync_user_decryption(&master_password_unlock, webauthn_prf_options),
"masterPasswordUnlock": master_password_unlock,
},
"object": "sync" "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")] #[get("/ciphers")]
async fn get_ciphers(headers: Headers, conn: DbConn) -> JsonResult { async fn get_ciphers(headers: Headers, conn: DbConn) -> JsonResult {
let ciphers = Cipher::find_by_user_visible(&headers.user.uuid, &conn).await; let ciphers = Cipher::find_by_user_visible(&headers.user.uuid, &conn).await;

7
src/api/core/mod.rs

@ -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 emergency_access::{emergency_notification_reminder_job, emergency_request_timeout_job};
pub use events::{event_cleanup_job, log_event, log_user_event}; pub use events::{event_cleanup_job, log_event, log_user_event};
pub use sends::purge_sends; pub use sends::purge_sends;
pub use webauthn::WEBAUTHN_PASSWORDLESS; pub use webauthn::{WEBAUTHN_PASSWORDLESS, webauthn_prf_option};
use reqwest::Method; use reqwest::Method;
use rocket::{Catcher, Route, serde::json::Json, serde::json::Value}; use rocket::{Catcher, Route, serde::json::Json, serde::json::Value};
@ -211,6 +211,11 @@ fn config() -> Json<Value> {
&FeatureFlagFilter::ValidOnly, &FeatureFlagFilter::ValidOnly,
); );
feature_states.insert("pm-19148-innovation-archive".to_owned(), true); 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!({ Json(json!({
// Note: The clients use this version to handle backwards compatibility concerns // Note: The clients use this version to handle backwards compatibility concerns

26
src/api/core/webauthn.rs

@ -14,7 +14,7 @@ use crate::{
auth::Headers, auth::Headers,
db::{ db::{
DbConn, 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] 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")] #[get("/webauthn")]
async fn get_webauthn(headers: Headers, conn: DbConn) -> JsonResult { async fn get_webauthn(headers: Headers, conn: DbConn) -> JsonResult {
if !CONFIG.passkey_login_allowed() { if !CONFIG.passkey_login_allowed() {

10
src/api/identity.rs

@ -23,6 +23,7 @@ use crate::{
authenticator, duo, duo_oidc, email, enforce_2fa_policy, is_twofactor_provider_usable, webauthn, authenticator, duo, duo_oidc, email, enforce_2fa_policy, is_twofactor_provider_usable, webauthn,
yubikey, yubikey,
}, },
webauthn_prf_option,
}, },
master_password_policy, master_password_policy,
push::register_push_device, 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?; 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) // Add WebAuthnPrfOption if the credential has enabled PRF-based decryption.
if matched_wac.encrypted_private_key.is_some() && matched_wac.encrypted_user_key.is_some() { if let Some(prf_option) = webauthn_prf_option(matched_wac, true) {
let Json(ref mut val) = result; let Json(ref mut val) = result;
val["UserDecryptionOptions"]["WebAuthnPrfOption"] = json!({ val["UserDecryptionOptions"]["WebAuthnPrfOption"] = prf_option;
"EncryptedPrivateKey": matched_wac.encrypted_private_key,
"EncryptedUserKey": matched_wac.encrypted_user_key,
});
} }
Ok(result) Ok(result)

1
src/config.rs

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

2
src/db/models/mod.rs

@ -44,4 +44,4 @@ pub use self::two_factor::{TwoFactor, TwoFactorType};
pub use self::two_factor_duo_context::TwoFactorDuoContext; pub use self::two_factor_duo_context::TwoFactorDuoContext;
pub use self::two_factor_incomplete::TwoFactorIncomplete; pub use self::two_factor_incomplete::TwoFactorIncomplete;
pub use self::user::{Invitation, SsoUser, User, UserId, UserKdfType, UserStampException}; 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};

Loading…
Cancel
Save