mirror of
https://github.com/dani-garcia/vaultwarden.wiki.git
synced 2026-07-29 10:35:05 +03:00
Compare commits
3
Commits
843c063649
...
7161f612a1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7161f612a1 | ||
|
|
5ee908517f | ||
|
|
55577fa4eb |
+9
-1
@@ -80,8 +80,16 @@
|
|||||||
## Timeout when acquiring database connection
|
## Timeout when acquiring database connection
|
||||||
# DATABASE_TIMEOUT=30
|
# DATABASE_TIMEOUT=30
|
||||||
|
|
||||||
|
## Database idle timeout
|
||||||
|
## Timeout in seconds before idle connections to the database are closed.
|
||||||
|
# DATABASE_IDLE_TIMEOUT=600
|
||||||
|
|
||||||
|
## Database min connections
|
||||||
|
## Define the minimum size of the connection pool used for connecting to the database.
|
||||||
|
# DATABASE_MIN_CONNS=2
|
||||||
|
|
||||||
## Database max connections
|
## Database max connections
|
||||||
## Define the size of the connection pool used for connecting to the database.
|
## Define the maximum size of the connection pool used for connecting to the database.
|
||||||
# DATABASE_MAX_CONNS=10
|
# DATABASE_MAX_CONNS=10
|
||||||
|
|
||||||
## Database connection initialization
|
## Database connection initialization
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ concurrency:
|
|||||||
jobs:
|
jobs:
|
||||||
docker-build:
|
docker-build:
|
||||||
name: Build Vaultwarden containers
|
name: Build Vaultwarden containers
|
||||||
|
if: ${{ github.repository == 'dani-garcia/vaultwarden' }}
|
||||||
permissions:
|
permissions:
|
||||||
packages: write
|
packages: write
|
||||||
contents: read
|
contents: read
|
||||||
|
|||||||
+1
-2
@@ -126,8 +126,7 @@ yubico = { package = "yubico_ng", version = "0.14.1", features = ["online-tokio"
|
|||||||
# WebAuthn libraries
|
# WebAuthn libraries
|
||||||
# danger-allow-state-serialisation is needed to save the state in the db
|
# danger-allow-state-serialisation is needed to save the state in the db
|
||||||
# danger-credential-internals is needed to support U2F to Webauthn migration
|
# danger-credential-internals is needed to support U2F to Webauthn migration
|
||||||
# danger-user-presence-only-security-keys is needed to disable UV
|
webauthn-rs = { version = "0.5.2", features = ["danger-allow-state-serialisation", "danger-credential-internals"] }
|
||||||
webauthn-rs = { version = "0.5.2", features = ["danger-allow-state-serialisation", "danger-credential-internals", "danger-user-presence-only-security-keys"] }
|
|
||||||
webauthn-rs-proto = "0.5.2"
|
webauthn-rs-proto = "0.5.2"
|
||||||
webauthn-rs-core = "0.5.2"
|
webauthn-rs-core = "0.5.2"
|
||||||
|
|
||||||
|
|||||||
@@ -21,12 +21,12 @@ use std::sync::{Arc, LazyLock};
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use webauthn_rs::prelude::{Base64UrlSafeData, SecurityKey, SecurityKeyAuthentication, SecurityKeyRegistration};
|
use webauthn_rs::prelude::{Base64UrlSafeData, Credential, Passkey, PasskeyAuthentication, PasskeyRegistration};
|
||||||
use webauthn_rs::{Webauthn, WebauthnBuilder};
|
use webauthn_rs::{Webauthn, WebauthnBuilder};
|
||||||
use webauthn_rs_proto::{
|
use webauthn_rs_proto::{
|
||||||
AuthenticationExtensionsClientOutputs, AuthenticatorAssertionResponseRaw, AuthenticatorAttestationResponseRaw,
|
AuthenticationExtensionsClientOutputs, AuthenticatorAssertionResponseRaw, AuthenticatorAttestationResponseRaw,
|
||||||
PublicKeyCredential, RegisterPublicKeyCredential, RegistrationExtensionsClientOutputs,
|
PublicKeyCredential, RegisterPublicKeyCredential, RegistrationExtensionsClientOutputs,
|
||||||
RequestAuthenticationExtensions,
|
RequestAuthenticationExtensions, UserVerificationPolicy,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub static WEBAUTHN_2FA_CONFIG: LazyLock<Arc<Webauthn>> = LazyLock::new(|| {
|
pub static WEBAUTHN_2FA_CONFIG: LazyLock<Arc<Webauthn>> = LazyLock::new(|| {
|
||||||
@@ -38,8 +38,7 @@ pub static WEBAUTHN_2FA_CONFIG: LazyLock<Arc<Webauthn>> = LazyLock::new(|| {
|
|||||||
let webauthn = WebauthnBuilder::new(&rp_id, &rp_origin)
|
let webauthn = WebauthnBuilder::new(&rp_id, &rp_origin)
|
||||||
.expect("Creating WebauthnBuilder failed")
|
.expect("Creating WebauthnBuilder failed")
|
||||||
.rp_name(&domain)
|
.rp_name(&domain)
|
||||||
.timeout(Duration::from_millis(60000))
|
.timeout(Duration::from_millis(60000));
|
||||||
.danger_set_user_presence_only_security_keys(true);
|
|
||||||
|
|
||||||
Arc::new(webauthn.build().expect("Building Webauthn failed"))
|
Arc::new(webauthn.build().expect("Building Webauthn failed"))
|
||||||
});
|
});
|
||||||
@@ -78,7 +77,7 @@ pub struct WebauthnRegistration {
|
|||||||
pub name: String,
|
pub name: String,
|
||||||
pub migrated: bool,
|
pub migrated: bool,
|
||||||
|
|
||||||
pub credential: SecurityKey,
|
pub credential: Passkey,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WebauthnRegistration {
|
impl WebauthnRegistration {
|
||||||
@@ -89,6 +88,24 @@ impl WebauthnRegistration {
|
|||||||
"migrated": self.migrated,
|
"migrated": self.migrated,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_backup_eligible(&mut self, backup_eligible: bool, backup_state: bool) -> bool {
|
||||||
|
let mut changed = false;
|
||||||
|
let mut cred: Credential = self.credential.clone().into();
|
||||||
|
|
||||||
|
if cred.backup_state != backup_state {
|
||||||
|
cred.backup_state = backup_state;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if backup_eligible && !cred.backup_eligible {
|
||||||
|
cred.backup_eligible = true;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.credential = cred.into();
|
||||||
|
changed
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/two-factor/get-webauthn", data = "<data>")]
|
#[post("/two-factor/get-webauthn", data = "<data>")]
|
||||||
@@ -131,18 +148,27 @@ async fn generate_webauthn_challenge(
|
|||||||
.map(|r| r.credential.cred_id().to_owned()) // We return the credentialIds to the clients to avoid double registering
|
.map(|r| r.credential.cred_id().to_owned()) // We return the credentialIds to the clients to avoid double registering
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let (challenge, state) = webauthn.start_securitykey_registration(
|
let (mut challenge, state) = webauthn.start_passkey_registration(
|
||||||
Uuid::from_str(&user.uuid).expect("Failed to parse UUID"), // Should never fail
|
Uuid::from_str(&user.uuid).expect("Failed to parse UUID"), // Should never fail
|
||||||
&user.email,
|
&user.email,
|
||||||
&user.name,
|
&user.name,
|
||||||
Some(registrations),
|
Some(registrations),
|
||||||
None,
|
|
||||||
None,
|
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
let mut state = serde_json::to_value(&state)?;
|
||||||
|
state["rs"]["policy"] = Value::String("discouraged".to_string());
|
||||||
|
state["rs"]["extensions"].as_object_mut().unwrap().clear();
|
||||||
|
|
||||||
let type_ = TwoFactorType::WebauthnRegisterChallenge;
|
let type_ = TwoFactorType::WebauthnRegisterChallenge;
|
||||||
TwoFactor::new(user.uuid.clone(), type_, serde_json::to_string(&state)?).save(&mut conn).await?;
|
TwoFactor::new(user.uuid.clone(), type_, serde_json::to_string(&state)?).save(&mut conn).await?;
|
||||||
|
|
||||||
|
// Because for this flow we abuse the passkeys as 2FA, and use it more like a securitykey
|
||||||
|
// we need to modify some of the default settings defined by `start_passkey_registration()`.
|
||||||
|
challenge.public_key.extensions = None;
|
||||||
|
if let Some(asc) = challenge.public_key.authenticator_selection.as_mut() {
|
||||||
|
asc.user_verification = UserVerificationPolicy::Discouraged_DO_NOT_USE;
|
||||||
|
}
|
||||||
|
|
||||||
let mut challenge_value = serde_json::to_value(challenge.public_key)?;
|
let mut challenge_value = serde_json::to_value(challenge.public_key)?;
|
||||||
challenge_value["status"] = "ok".into();
|
challenge_value["status"] = "ok".into();
|
||||||
challenge_value["errorMessage"] = "".into();
|
challenge_value["errorMessage"] = "".into();
|
||||||
@@ -253,7 +279,7 @@ async fn activate_webauthn(
|
|||||||
let type_ = TwoFactorType::WebauthnRegisterChallenge as i32;
|
let type_ = TwoFactorType::WebauthnRegisterChallenge as i32;
|
||||||
let state = match TwoFactor::find_by_user_and_type(&user.uuid, type_, &mut conn).await {
|
let state = match TwoFactor::find_by_user_and_type(&user.uuid, type_, &mut conn).await {
|
||||||
Some(tf) => {
|
Some(tf) => {
|
||||||
let state: SecurityKeyRegistration = serde_json::from_str(&tf.data)?;
|
let state: PasskeyRegistration = serde_json::from_str(&tf.data)?;
|
||||||
tf.delete(&mut conn).await?;
|
tf.delete(&mut conn).await?;
|
||||||
state
|
state
|
||||||
}
|
}
|
||||||
@@ -261,7 +287,7 @@ async fn activate_webauthn(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Verify the credentials with the saved state
|
// Verify the credentials with the saved state
|
||||||
let credential = webauthn.finish_securitykey_registration(&data.device_response.into(), &state)?;
|
let credential = webauthn.finish_passkey_registration(&data.device_response.into(), &state)?;
|
||||||
|
|
||||||
let mut registrations: Vec<_> = get_webauthn_registrations(&user.uuid, &mut conn).await?.1;
|
let mut registrations: Vec<_> = get_webauthn_registrations(&user.uuid, &mut conn).await?.1;
|
||||||
// TODO: Check for repeated ID's
|
// TODO: Check for repeated ID's
|
||||||
@@ -372,21 +398,25 @@ pub async fn generate_webauthn_login(
|
|||||||
conn: &mut DbConn,
|
conn: &mut DbConn,
|
||||||
) -> JsonResult {
|
) -> JsonResult {
|
||||||
// Load saved credentials
|
// Load saved credentials
|
||||||
let creds: Vec<_> = get_webauthn_registrations(user_id, conn).await?.1.into_iter().map(|r| r.credential).collect();
|
let creds: Vec<Passkey> =
|
||||||
|
get_webauthn_registrations(user_id, conn).await?.1.into_iter().map(|r| r.credential).collect();
|
||||||
|
|
||||||
if creds.is_empty() {
|
if creds.is_empty() {
|
||||||
err!("No Webauthn devices registered")
|
err!("No Webauthn devices registered")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate a challenge based on the credentials
|
// Generate a challenge based on the credentials
|
||||||
let (mut response, state) = webauthn.start_securitykey_authentication(&creds)?;
|
let (mut response, state) = webauthn.start_passkey_authentication(&creds)?;
|
||||||
|
|
||||||
// Modify to discourage user verification
|
// Modify to discourage user verification
|
||||||
let mut state = serde_json::to_value(&state)?;
|
let mut state = serde_json::to_value(&state)?;
|
||||||
|
state["ast"]["policy"] = Value::String("discouraged".to_string());
|
||||||
|
|
||||||
// Add appid, this is only needed for U2F compatibility, so maybe it can be removed as well
|
// Add appid, this is only needed for U2F compatibility, so maybe it can be removed as well
|
||||||
let app_id = format!("{}/app-id.json", &CONFIG.domain());
|
let app_id = format!("{}/app-id.json", &CONFIG.domain());
|
||||||
state["ast"]["appid"] = Value::String(app_id.clone());
|
state["ast"]["appid"] = Value::String(app_id.clone());
|
||||||
|
|
||||||
|
response.public_key.user_verification = UserVerificationPolicy::Discouraged_DO_NOT_USE;
|
||||||
response
|
response
|
||||||
.public_key
|
.public_key
|
||||||
.extensions
|
.extensions
|
||||||
@@ -413,9 +443,9 @@ pub async fn validate_webauthn_login(
|
|||||||
conn: &mut DbConn,
|
conn: &mut DbConn,
|
||||||
) -> EmptyResult {
|
) -> EmptyResult {
|
||||||
let type_ = TwoFactorType::WebauthnLoginChallenge as i32;
|
let type_ = TwoFactorType::WebauthnLoginChallenge as i32;
|
||||||
let state = match TwoFactor::find_by_user_and_type(user_id, type_, conn).await {
|
let mut state = match TwoFactor::find_by_user_and_type(user_id, type_, conn).await {
|
||||||
Some(tf) => {
|
Some(tf) => {
|
||||||
let state: SecurityKeyAuthentication = serde_json::from_str(&tf.data)?;
|
let state: PasskeyAuthentication = serde_json::from_str(&tf.data)?;
|
||||||
tf.delete(conn).await?;
|
tf.delete(conn).await?;
|
||||||
state
|
state
|
||||||
}
|
}
|
||||||
@@ -432,7 +462,12 @@ pub async fn validate_webauthn_login(
|
|||||||
|
|
||||||
let mut registrations = get_webauthn_registrations(user_id, conn).await?.1;
|
let mut registrations = get_webauthn_registrations(user_id, conn).await?.1;
|
||||||
|
|
||||||
let authentication_result = webauthn.finish_securitykey_authentication(&rsp, &state)?;
|
// We need to check for and update the backup_eligible flag when needed.
|
||||||
|
// Vaultwarden did not have knowledge of this flag prior to migrating to webauthn-rs v0.5.x
|
||||||
|
// Because of this we check the flag at runtime and update the registrations and state when needed
|
||||||
|
check_and_update_backup_eligible(user_id, &rsp, &mut registrations, &mut state, conn).await?;
|
||||||
|
|
||||||
|
let authentication_result = webauthn.finish_passkey_authentication(&rsp, &state)?;
|
||||||
|
|
||||||
for reg in &mut registrations {
|
for reg in &mut registrations {
|
||||||
if ct_eq(reg.credential.cred_id(), authentication_result.cred_id()) {
|
if ct_eq(reg.credential.cred_id(), authentication_result.cred_id()) {
|
||||||
@@ -454,3 +489,66 @@ pub async fn validate_webauthn_login(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn check_and_update_backup_eligible(
|
||||||
|
user_id: &UserId,
|
||||||
|
rsp: &PublicKeyCredential,
|
||||||
|
registrations: &mut Vec<WebauthnRegistration>,
|
||||||
|
state: &mut PasskeyAuthentication,
|
||||||
|
conn: &mut DbConn,
|
||||||
|
) -> EmptyResult {
|
||||||
|
// The feature flags from the response
|
||||||
|
// For details see: https://www.w3.org/TR/webauthn-3/#sctn-authenticator-data
|
||||||
|
const FLAG_BACKUP_ELIGIBLE: u8 = 0b0000_1000;
|
||||||
|
const FLAG_BACKUP_STATE: u8 = 0b0001_0000;
|
||||||
|
|
||||||
|
if let Some(bits) = rsp.response.authenticator_data.get(32) {
|
||||||
|
let backup_eligible = 0 != (bits & FLAG_BACKUP_ELIGIBLE);
|
||||||
|
let backup_state = 0 != (bits & FLAG_BACKUP_STATE);
|
||||||
|
|
||||||
|
// If the current key is backup eligible, then we probably need to update one of the keys already stored in the database
|
||||||
|
// This is needed because Vaultwarden didn't store this information when using the previous version of webauthn-rs since it was a new addition to the protocol
|
||||||
|
// Because we store multiple keys in one json string, we need to fetch the correct key first, and update its information before we let it verify
|
||||||
|
if backup_eligible {
|
||||||
|
let rsp_id = rsp.raw_id.as_slice();
|
||||||
|
for reg in &mut *registrations {
|
||||||
|
if ct_eq(reg.credential.cred_id().as_slice(), rsp_id) {
|
||||||
|
// Try to update the key, and if needed also update the database, before the actual state check is done
|
||||||
|
if reg.set_backup_eligible(backup_eligible, backup_state) {
|
||||||
|
TwoFactor::new(
|
||||||
|
user_id.clone(),
|
||||||
|
TwoFactorType::Webauthn,
|
||||||
|
serde_json::to_string(®istrations)?,
|
||||||
|
)
|
||||||
|
.save(conn)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// We also need to adjust the current state which holds the challenge used to start the authentication verification
|
||||||
|
// Because Vaultwarden supports multiple keys, we need to loop through the deserialized state and check which key to update
|
||||||
|
let mut raw_state = serde_json::to_value(&state)?;
|
||||||
|
if let Some(credentials) = raw_state
|
||||||
|
.get_mut("ast")
|
||||||
|
.and_then(|v| v.get_mut("credentials"))
|
||||||
|
.and_then(|v| v.as_array_mut())
|
||||||
|
{
|
||||||
|
for cred in credentials.iter_mut() {
|
||||||
|
if cred.get("cred_id").is_some_and(|v| {
|
||||||
|
// Deserialize to a [u8] so it can be compared using `ct_eq` with the `rsp_id`
|
||||||
|
let cred_id_slice: Base64UrlSafeData = serde_json::from_value(v.clone()).unwrap();
|
||||||
|
ct_eq(cred_id_slice, rsp_id)
|
||||||
|
}) {
|
||||||
|
cred["backup_eligible"] = Value::Bool(backup_eligible);
|
||||||
|
cred["backup_state"] = Value::Bool(backup_state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*state = serde_json::from_value(raw_state)?;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
@@ -834,6 +834,14 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
|
|||||||
err!(format!("`DATABASE_MAX_CONNS` contains an invalid value. Ensure it is between 1 and {limit}.",));
|
err!(format!("`DATABASE_MAX_CONNS` contains an invalid value. Ensure it is between 1 and {limit}.",));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cfg.database_min_conns < 1 || cfg.database_min_conns > limit {
|
||||||
|
err!(format!("`DATABASE_MIN_CONNS` contains an invalid value. Ensure it is between 1 and {limit}.",));
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.database_min_conns > cfg.database_max_conns {
|
||||||
|
err!(format!("`DATABASE_MIN_CONNS` must be smaller than or equal to `DATABASE_MAX_CONNS`.",));
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(log_file) = &cfg.log_file {
|
if let Some(log_file) = &cfg.log_file {
|
||||||
if std::fs::OpenOptions::new().append(true).create(true).open(log_file).is_err() {
|
if std::fs::OpenOptions::new().append(true).create(true).open(log_file).is_err() {
|
||||||
err!("Unable to write to log file", log_file);
|
err!("Unable to write to log file", log_file);
|
||||||
|
|||||||
Reference in New Issue
Block a user