Files
vaultwarden/src/crypto.rs
T
Mathijs van Veluw d297e274a3 Several SSO Fixes (#7163)
* Ensure SSO token is only usable on the same client

This commit adds an extra check via cookies to ensure the same browser/client is used to request and provide the SSO token.
Previously it would be able to provide a custom link which attackers could use to steal data.
While an attacker would still need the Master Password to be able to decrypt or execute specific actions, they were able to fetch encrypted data.

Solved with some help of Claude Code.

Signed-off-by: BlackDex <black.dex@gmail.com>

* Check email-verified on SSO login/create

This commit prevents possible account takeover via SSO which doesn't check/validate or provide validated status of the email.
It was checked at other locations, but was skipped here.

Signed-off-by: BlackDex <black.dex@gmail.com>

* Prevent data disclosure via SSO endpoints

This commit prevents some data disclosure and user enumeration by only returning the fake SSO identifier.
Since we do not check the identifier anywhere useful, returning the fake one is just fine.

During an invite to an org, that link contains the correct UUID and will be used for the master password requirements.
For anything else, server admins should set the `SSO_MASTER_PASSWORD_POLICY` env variable.

Signed-off-by: BlackDex <black.dex@gmail.com>

* Adjust admin layout to fix issues when SSO is enabled

Signed-off-by: BlackDex <black.dex@gmail.com>

---------

Signed-off-by: BlackDex <black.dex@gmail.com>
2026-04-29 22:25:36 +02:00

123 lines
3.5 KiB
Rust

//
// PBKDF2 derivation
//
use std::num::NonZeroU32;
use data_encoding::{Encoding, HEXLOWER};
use ring::{digest, hmac, pbkdf2};
const DIGEST_ALG: pbkdf2::Algorithm = pbkdf2::PBKDF2_HMAC_SHA256;
const OUTPUT_LEN: usize = digest::SHA256_OUTPUT_LEN;
pub fn hash_password(secret: &[u8], salt: &[u8], iterations: u32) -> Vec<u8> {
let mut out = vec![0u8; OUTPUT_LEN]; // Initialize array with zeros
let iterations = NonZeroU32::new(iterations).expect("Iterations can't be zero");
pbkdf2::derive(DIGEST_ALG, iterations, salt, secret, &mut out);
out
}
pub fn verify_password_hash(secret: &[u8], salt: &[u8], previous: &[u8], iterations: u32) -> bool {
let iterations = NonZeroU32::new(iterations).expect("Iterations can't be zero");
pbkdf2::verify(DIGEST_ALG, iterations, salt, secret, previous).is_ok()
}
//
// HMAC
//
pub fn hmac_sign(key: &str, data: &str) -> String {
let key = hmac::Key::new(hmac::HMAC_SHA1_FOR_LEGACY_USE_ONLY, key.as_bytes());
let signature = hmac::sign(&key, data.as_bytes());
HEXLOWER.encode(signature.as_ref())
}
//
// Random values
//
/// Return an array holding `N` random bytes.
pub fn get_random_bytes<const N: usize>() -> [u8; N] {
use ring::rand::{SecureRandom, SystemRandom};
let mut array = [0; N];
SystemRandom::new().fill(&mut array).expect("Error generating random values");
array
}
/// Encode random bytes using the provided function.
pub fn encode_random_bytes<const N: usize>(e: &Encoding) -> String {
e.encode(&get_random_bytes::<N>())
}
/// Generates a random string over a specified alphabet.
pub fn get_random_string(alphabet: &[u8], num_chars: usize) -> String {
// Ref: https://rust-lang-nursery.github.io/rust-cookbook/algorithms/randomness.html
use rand::RngExt;
let mut rng = rand::rng();
(0..num_chars)
.map(|_| {
let i = rng.random_range(0..alphabet.len());
char::from(alphabet[i])
})
.collect()
}
/// Generates a random numeric string.
pub fn get_random_string_numeric(num_chars: usize) -> String {
const ALPHABET: &[u8] = b"0123456789";
get_random_string(ALPHABET, num_chars)
}
/// Generates a random alphanumeric string.
pub fn get_random_string_alphanum(num_chars: usize) -> String {
const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ\
abcdefghijklmnopqrstuvwxyz\
0123456789";
get_random_string(ALPHABET, num_chars)
}
pub fn generate_id<const N: usize>() -> String {
encode_random_bytes::<N>(&HEXLOWER)
}
pub fn generate_send_file_id() -> String {
// Send File IDs are globally scoped, so make them longer to avoid collisions.
generate_id::<32>() // 256 bits
}
use crate::db::models::AttachmentId;
pub fn generate_attachment_id() -> AttachmentId {
// Attachment IDs are scoped to a cipher, so they can be smaller.
AttachmentId(generate_id::<10>()) // 80 bits
}
/// Generates a numeric token for email-based verifications.
pub fn generate_email_token(token_size: u8) -> String {
get_random_string_numeric(token_size as usize)
}
/// Generates a personal API key.
/// Upstream uses 30 chars, which is ~178 bits of entropy.
pub fn generate_api_key() -> String {
get_random_string_alphanum(30)
}
//
// Constant time compare
//
pub fn ct_eq<T: AsRef<[u8]>, U: AsRef<[u8]>>(a: T, b: U) -> bool {
use subtle::ConstantTimeEq;
a.as_ref().ct_eq(b.as_ref()).into()
}
//
// SHA256
//
pub fn sha256_hex(data: &[u8]) -> String {
HEXLOWER.encode(digest::digest(&digest::SHA256, data).as_ref())
}