Skip to content

Commit

Permalink
SSO experimental stop storing MasterPwd
Browse files Browse the repository at this point in the history
  • Loading branch information
Timshel committed Feb 3, 2024
1 parent e94d0f9 commit 9dbe82c
Show file tree
Hide file tree
Showing 13 changed files with 110 additions and 52 deletions.
2 changes: 2 additions & 0 deletions .env.template
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,8 @@
# SSO_ORGANIZATIONS_SCOPE=groups
## Access token path to read groups
# SSO_ORGANIZATIONS_TOKEN_PATH=/groups
## Experimental (running this will make reverting complicated)
# SSO_EXPERIMENTAL_NO_MASTER_PWD=false

########################
### MFA/2FA settings ###
Expand Down
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,29 @@ Ex to build with latest: `--build-arg OIDC_WEB_RELEASE="https://github.com/Timsh

[Readme](test/oidc/README.md)

## Experimental version

### Stop storing Master Password hash

This allow to stop storing the Master password in the server database.
This is a work in progress and released for testing.
Once activated newly created account will no longer store a master password hash, making reverting to a standard VaultWarden instance troublesome.

#### To activate

- `SSO_EXPERIMENTAL_NO_MASTER_PWD`: Control the activation of the feature. Default `false`.

Additionnaly a new web build is available which stop sending the hash cf `experimental` in [Timshel/oidc_web_builds](https://github.com/Timshel/oidc_web_builds/releases)
You'll need to pass an env variable: `-e SSO_FRONTEND='experimental'` (cf [start.sh](docker/start.sh)).

#### To revert

You'll first need to run the server without the `experimental` front-end.
\
You can then go to `Account settings \ Security \ Keys` and trigger the `Change KDF`.
\
This endpoint is not modified and will save the new master password hash, every user will need to do this to restore a Master password in db.

## DB Migration

ATM The migrations add an independant table `sso_nonce` and a column `invited_by_email` to `users_organizations`.
Expand Down
1 change: 1 addition & 0 deletions docker/Dockerfile.alpine
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ ENV DEBIAN_FRONTEND=noninteractive \
# Get all version of the front-end
RUN curl -L "${OIDC_WEB_RELEASE}/oidc_button_web_vault.tar.gz" | tar -xz ; mv web-vault /web-vault_button
RUN curl -L "${OIDC_WEB_RELEASE}/oidc_override_web_vault.tar.gz" | tar -xz ; mv web-vault /web-vault_override
RUN curl -L "${OIDC_WEB_RELEASE}/oidc_experimental_web_vault.tar.gz" | tar -xz ; mv web-vault /web-vault_experimental

# Create CARGO_HOME folder and don't download rust docs
RUN mkdir -pv "${CARGO_HOME}" \
Expand Down
2 changes: 2 additions & 0 deletions docker/Dockerfile.debian
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ RUN xx-apt-get install -y \
# Get all version of the front-end
RUN curl -L "${OIDC_WEB_RELEASE}/oidc_button_web_vault.tar.gz" | tar -xz ; mv web-vault /web-vault_button
RUN curl -L "${OIDC_WEB_RELEASE}/oidc_override_web_vault.tar.gz" | tar -xz ; mv web-vault /web-vault_override
RUN curl -L "${OIDC_WEB_RELEASE}/oidc_experimental_web_vault.tar.gz" | tar -xz ; mv web-vault /web-vault_experimental

# Create CARGO_HOME folder and don't download rust docs
RUN mkdir -pv "${CARGO_HOME}" \
Expand Down Expand Up @@ -178,6 +179,7 @@ COPY docker/start.sh /start.sh

COPY --from=build /web-vault_button ./web-vault_button
COPY --from=build /web-vault_override ./web-vault_override
COPY --from=build /web-vault_experimental ./web-vault_experimental
COPY --from=build /app/target/final/vaultwarden .

HEALTHCHECK --interval=60s --timeout=10s CMD ["/healthcheck.sh"]
Expand Down
2 changes: 2 additions & 0 deletions docker/Dockerfile.j2
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ RUN xx-apt-get install -y \
# Get all version of the front-end
RUN curl -L "${OIDC_WEB_RELEASE}/oidc_button_web_vault.tar.gz" | tar -xz ; mv web-vault /web-vault_button
RUN curl -L "${OIDC_WEB_RELEASE}/oidc_override_web_vault.tar.gz" | tar -xz ; mv web-vault /web-vault_override
RUN curl -L "${OIDC_WEB_RELEASE}/oidc_experimental_web_vault.tar.gz" | tar -xz ; mv web-vault /web-vault_experimental

# Create CARGO_HOME folder and don't download rust docs
RUN mkdir -pv "${CARGO_HOME}" \
Expand Down Expand Up @@ -221,6 +222,7 @@ COPY docker/start.sh /start.sh

COPY --from=build /web-vault_button ./web-vault_button
COPY --from=build /web-vault_override ./web-vault_override
COPY --from=build /web-vault_experimental ./web-vault_experimental
COPY --from=build /app/target/final/vaultwarden .

HEALTHCHECK --interval=60s --timeout=10s CMD ["/healthcheck.sh"]
Expand Down
3 changes: 3 additions & 0 deletions docker/start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ rm -f /web-vault
if [ "$SSO_FRONTEND" = "override" ] ; then
echo "### Running web-vault frontend with SSO override ###"
ln -s /web-vault_override /web-vault
elif [ "$SSO_FRONTEND" = "experimental" ] ; then
echo "### Running web-vault frontend with SSO experimental ###"
ln -s /web-vault_experimental /web-vault
else
echo "### Running web-vault frontend with SSO button ###"
ln -s /web-vault_button /web-vault
Expand Down
99 changes: 63 additions & 36 deletions src/api/core/accounts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ pub struct SetPasswordData {
KdfParallelism: Option<i32>,
Key: String,
Keys: Option<KeysData>,
MasterPasswordHash: String,
MasterPasswordHash: Option<String>,
MasterPasswordHint: Option<String>,
#[allow(dead_code)]
OrgIdentifier: Option<String>,
Expand Down Expand Up @@ -201,14 +201,15 @@ pub async fn _register(data: JsonUpcase<RegisterData>, mut conn: DbConn) -> Json
user.client_kdf_memory = data.KdfMemory;
user.client_kdf_parallelism = data.KdfParallelism;

user.set_password(&data.MasterPasswordHash, Some(data.Key), true, None);
user.set_password(&data.MasterPasswordHash, true, None);
user.password_hint = password_hint;

// Add extra fields if present
if let Some(name) = data.Name {
user.name = name;
}

user.akey = data.Key;
if let Some(keys) = data.Keys {
user.private_key = Some(keys.EncryptedPrivateKey);
user.public_key = Some(keys.PublicKey);
Expand Down Expand Up @@ -238,10 +239,27 @@ async fn post_set_password(data: JsonUpcase<SetPasswordData>, headers: Headers,
let data: SetPasswordData = data.into_inner().data;
let mut user = headers.user;

// Check against the password hint setting here so if it fails, the user
// can retry without losing their invitation below.
let password_hint = clean_password_hint(&data.MasterPasswordHint);
enforce_password_hint_setting(&password_hint)?;
if !CONFIG.sso_enabled() || !CONFIG.sso_experimental_no_master_pwd() {
if let Some(password_hash) = data.MasterPasswordHash {
// Check against the password hint setting here so if it fails, the user
// can retry without losing their invitation below.
let password_hint = clean_password_hint(&data.MasterPasswordHint);
enforce_password_hint_setting(&password_hint)?;

// We need to allow revision-date to use the old security_timestamp
let routes = ["revision_date"];
let routes: Option<Vec<String>> = Some(routes.iter().map(ToString::to_string).collect());

user.set_password(&password_hash, false, routes);
user.password_hint = password_hint;

if CONFIG.mail_enabled() {
mail::send_set_password(&user.email.to_lowercase(), &user.name).await?;
}
} else {
err_code!("Missing password hash", Status::UnprocessableEntity.code)
}
}

if let Some(client_kdf_iter) = data.KdfIterations {
user.client_kdf_iter = client_kdf_iter;
Expand All @@ -251,25 +269,15 @@ async fn post_set_password(data: JsonUpcase<SetPasswordData>, headers: Headers,
user.client_kdf_type = client_kdf_type;
}

// We need to allow revision-date to use the old security_timestamp
let routes = ["revision_date"];
let routes: Option<Vec<String>> = Some(routes.iter().map(ToString::to_string).collect());

user.client_kdf_memory = data.KdfMemory;
user.client_kdf_parallelism = data.KdfParallelism;

user.set_password(&data.MasterPasswordHash, Some(data.Key), false, routes);
user.password_hint = password_hint;

user.akey = data.Key;
if let Some(keys) = data.Keys {
user.private_key = Some(keys.EncryptedPrivateKey);
user.public_key = Some(keys.PublicKey);
}

if CONFIG.mail_enabled() {
mail::send_set_password(&user.email.to_lowercase(), &user.name).await?;
}

user.save(&mut conn).await?;
Ok(Json(json!({
"Object": "set-password",
Expand Down Expand Up @@ -374,8 +382,8 @@ async fn post_keys(data: JsonUpcase<KeysData>, headers: Headers, mut conn: DbCon
#[derive(Deserialize)]
#[allow(non_snake_case)]
struct ChangePassData {
MasterPasswordHash: String,
NewMasterPasswordHash: String,
MasterPasswordHash: Option<String>,
NewMasterPasswordHash: Option<String>,
MasterPasswordHint: Option<String>,
Key: String,
}
Expand All @@ -390,22 +398,37 @@ async fn post_password(
let data: ChangePassData = data.into_inner().data;
let mut user = headers.user;

if !user.check_valid_password(&data.MasterPasswordHash) {
err!("Invalid password")
}

user.password_hint = clean_password_hint(&data.MasterPasswordHint);
enforce_password_hint_setting(&user.password_hint)?;

if !CONFIG.sso_enabled() || !CONFIG.sso_experimental_no_master_pwd() {
if let Some(password_hash) = data.MasterPasswordHash {
if !user.check_valid_password(&password_hash) {
err!("Invalid password")
}
} else {
err_code!("Missing password hash", Status::UnprocessableEntity.code)
}

if let Some(new_password_hash) = data.NewMasterPasswordHash {
user.set_password(
&new_password_hash,
true,
Some(vec![
String::from("post_rotatekey"),
String::from("get_contacts"),
String::from("get_public_keys"),
]),
);
} else {
err_code!("Missing password hash", Status::UnprocessableEntity.code)
}
}

log_user_event(EventType::UserChangedPassword as i32, &user.uuid, headers.device.atype, &headers.ip.ip, &mut conn)
.await;

user.set_password(
&data.NewMasterPasswordHash,
Some(data.Key),
true,
Some(vec![String::from("post_rotatekey"), String::from("get_contacts"), String::from("get_public_keys")]),
);
user.akey = data.Key;

let save_result = user.save(&mut conn).await;

Expand Down Expand Up @@ -469,7 +492,8 @@ async fn post_kdf(data: JsonUpcase<ChangeKdfData>, headers: Headers, mut conn: D
}
user.client_kdf_iter = data.KdfIterations;
user.client_kdf_type = data.Kdf;
user.set_password(&data.NewMasterPasswordHash, Some(data.Key), true, None);
user.akey = data.Key;
user.set_password(&data.NewMasterPasswordHash, true, None);
let save_result = user.save(&mut conn).await;

nt.send_logout(&user, Some(headers.device.uuid)).await;
Expand Down Expand Up @@ -687,7 +711,8 @@ async fn post_email(
user.email_new = None;
user.email_new_token = None;

user.set_password(&data.NewMasterPasswordHash, Some(data.Key), true, None);
user.akey = data.Key;
user.set_password(&data.NewMasterPasswordHash, true, None);

let save_result = user.save(&mut conn).await;

Expand Down Expand Up @@ -905,7 +930,7 @@ struct SecretVerificationRequest {
pub async fn kdf_upgrade(user: &mut User, pwd_hash: &str, conn: &mut DbConn) -> ApiResult<()> {
if user.password_iterations != CONFIG.password_iterations() {
user.password_iterations = CONFIG.password_iterations();
user.set_password(pwd_hash, None, false, None);
user.set_password(pwd_hash, false, None);

if let Err(e) = user.save(conn).await {
error!("Error updating user: {:#?}", e);
Expand All @@ -923,11 +948,13 @@ async fn verify_password(
let data: SecretVerificationRequest = data.into_inner().data;
let mut user = headers.user;

if !user.check_valid_password(&data.MasterPasswordHash) {
err!("Invalid password")
}
if !CONFIG.sso_enabled() || !CONFIG.sso_experimental_no_master_pwd() {
if !user.check_valid_password(&data.MasterPasswordHash) {
err!("Invalid password")
}

kdf_upgrade(&mut user, &data.MasterPasswordHash, &mut conn).await?;
kdf_upgrade(&mut user, &data.MasterPasswordHash, &mut conn).await?;
}

Ok(Json(json!({
"MasterPasswordPolicy": {}, // Required for SSO login with mobile apps
Expand Down
3 changes: 2 additions & 1 deletion src/api/core/emergency_access.rs
Original file line number Diff line number Diff line change
Expand Up @@ -687,7 +687,8 @@ async fn password_emergency_access(
};

// change grantor_user password
grantor_user.set_password(new_master_password_hash, Some(data.Key), true, None);
grantor_user.akey = data.Key;
grantor_user.set_password(new_master_password_hash, true, None);
grantor_user.save(&mut conn).await?;

// Disable TwoFactor providers since they will otherwise block logins
Expand Down
3 changes: 2 additions & 1 deletion src/api/core/organizations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2779,7 +2779,8 @@ async fn put_reset_password(
let reset_request = data.into_inner().data;

let mut user = user;
user.set_password(reset_request.NewMasterPasswordHash.as_str(), Some(reset_request.Key), true, None);
user.akey = reset_request.Key;
user.set_password(reset_request.NewMasterPasswordHash.as_str(), true, None);
user.save(&mut conn).await?;

nt.send_logout(&user, None).await;
Expand Down
4 changes: 2 additions & 2 deletions src/api/identity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ async fn _sso_login(

(user, device, new_device, None)
}
Some((mut user, device, new_device, twofactor_token)) if user.password_hash.is_empty() => {
Some((mut user, device, new_device, twofactor_token)) if user.public_key.is_none() => {
user.verified_at = Some(now);
if let Some(user_name) = user_infos.user_name {
user.name = user_name;
Expand Down Expand Up @@ -395,7 +395,7 @@ async fn authenticated_response(
"scope": auth_tokens.scope(),
"unofficialServer": true,
"UserDecryptionOptions": {
"HasMasterPassword": !user.password_hash.is_empty(),
"HasMasterPassword": user.public_key.is_some(),
"Object": "userDecryptionOptions"
},
});
Expand Down
2 changes: 1 addition & 1 deletion src/business/organization_logic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ pub async fn invite(
let mut user_org_status = UserOrgStatus::Invited;

// automatically accept existing users if mail is disabled
if !user.password_hash.is_empty() && !CONFIG.mail_enabled() {
if user.public_key.is_some() && !CONFIG.mail_enabled() {
user_org_status = UserOrgStatus::Accepted;
}

Expand Down
6 changes: 6 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,8 @@ make_config! {
sso_organizations_invite: bool, true, def, false;
/// Access token path to read Organization/Groups
sso_organizations_token_path: String, true, def, "/groups".to_string();
/// Deactivate saving the master password
sso_experimental_no_master_pwd: bool, true, def, false;
},

/// Yubikey settings
Expand Down Expand Up @@ -853,6 +855,10 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
err!("`SSO_CLIENT_ID`, `SSO_CLIENT_SECRET` and `SSO_AUTHORITY` must be set for SSO support")
}

if !cfg.sso_only && cfg.sso_experimental_no_master_pwd {
warn!("`sso_only` should be activated when running `sso_experimental_no_master_pwd`")
}

internal_sso_issuer_url(&cfg.sso_authority)?;
internal_sso_redirect_url(&cfg.sso_callback_path)?;
check_master_password_policy(&cfg.sso_master_password_policy)?;
Expand Down
12 changes: 1 addition & 11 deletions src/db/models/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,23 +165,13 @@ impl User {
/// These routes are able to use the previous stamp id for the next 2 minutes.
/// After these 2 minutes this stamp will expire.
///
pub fn set_password(
&mut self,
password: &str,
new_key: Option<String>,
reset_security_stamp: bool,
allow_next_route: Option<Vec<String>>,
) {
pub fn set_password(&mut self, password: &str, reset_security_stamp: bool, allow_next_route: Option<Vec<String>>) {
self.password_hash = crypto::hash_password(password.as_bytes(), &self.salt, self.password_iterations as u32);

if let Some(route) = allow_next_route {
self.set_stamp_exception(route);
}

if let Some(new_key) = new_key {
self.akey = new_key;
}

if reset_security_stamp {
self.reset_security_stamp()
}
Expand Down

0 comments on commit 9dbe82c

Please sign in to comment.