mirror of
https://github.com/dani-garcia/vaultwarden.git
synced 2026-10-27 08:30:13 +00:00
Align API behaviour with upstream and return errors on malformed input (#7810)
- Attachments: reject a declared v2 attachment size above the upload limit, keep display sizes within the largest unit, and make download links valid for 1 minute like upstream. - Ciphers: moving a personal cipher into an organization needs collection write access or full access, like upstream. - Events: /events/collect only accepts the client event types upstream accepts. - Client IP: for list headers like X-Forwarded-For, use the rightmost address that isn't in IP_HEADER_TRUSTED_PROXIES, reading every occurrence. - Prelogin: unknown emails get stable KDF settings from upstream's list. - Login with device: an auth request can be used for one login, like upstream. - Directory Connector import: never revokes, restores or removes Owners, like upstream. - SSO: check the PKCE verifier again when the code exchange is reused during 2FA, and accept the same redirect URIs as upstream (desktop localhost callback for AppImage and dev builds, CLI on ports 8065-8070). - Identity: send the merged master password policy in the 2FA response and use PascalCase policy keys in identity responses, like upstream. Organization API key login no longer needs device fields. Recovery code login normalises the code, uses upstream's error message, logs a failed 2FA event and sends a "two-step login recovered" email. - Add GET /api/accounts/keys. - Return errors instead of panicking on malformed event dates, cipher data, key rotation cipher ids, bulk confirm entries and YubiKey OTPs.
This commit is contained in:
1 parent
1f99d9d276
commit
895ec2b1e7
24 files changed
+381
-154
No files matched your search
@@ -42,6 +42,7 @@ pub fn routes() -> Vec<rocket::Route> {
|
||||
post_profile,
|
||||
put_avatar,
|
||||
get_public_keys,
|
||||
get_keys,
|
||||
post_keys,
|
||||
post_password,
|
||||
post_set_password,
|
||||
@@ -568,6 +569,24 @@ async fn get_public_keys(user_id: UserId, _headers: Headers, conn: DbConn) -> Js
|
||||
})))
|
||||
}
|
||||
|
||||
#[get("/accounts/keys")]
|
||||
fn get_keys(headers: Headers) -> JsonResult {
|
||||
let user = headers.user;
|
||||
|
||||
// The SDK reads a 404 as the user having no key pair yet
|
||||
if user.private_key.is_none() || user.public_key.is_none() {
|
||||
err_code!("User has no key pair", Status::NotFound.code)
|
||||
}
|
||||
|
||||
Ok(Json(json!({
|
||||
"key": (!user.akey.is_empty()).then_some(&user.akey),
|
||||
"publicKey": user.public_key,
|
||||
"privateKey": user.private_key,
|
||||
"accountKeys": user.account_keys_json(),
|
||||
"object": "keys",
|
||||
})))
|
||||
}
|
||||
|
||||
#[post("/accounts/keys", data = "<data>")]
|
||||
async fn post_keys(data: Json<KeysData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||
let data: KeysData = data.into_inner();
|
||||
@@ -994,7 +1013,8 @@ async fn post_rotatekey(data: Json<KeyData>, headers: Headers, conn: DbConn, nt:
|
||||
// Update cipher data
|
||||
for cipher_data in data.account_data.ciphers {
|
||||
if cipher_data.organization_id.is_none() {
|
||||
let Some(saved_cipher) = existing_ciphers.iter_mut().find(|c| &c.uuid == cipher_data.id.as_ref().unwrap())
|
||||
let Some(saved_cipher) =
|
||||
cipher_data.id.as_ref().and_then(|id| existing_ciphers.iter_mut().find(|c| &c.uuid == id))
|
||||
else {
|
||||
err!("Cipher doesn't exist")
|
||||
};
|
||||
@@ -1360,14 +1380,33 @@ pub struct PreloginData {
|
||||
email: String,
|
||||
}
|
||||
|
||||
/// Like upstream, an unknown email gets KDF settings picked from a list of common ones by a keyed hash of the email,
|
||||
/// so they stay the same between requests. The salt stays null, as for existing users.
|
||||
fn unknown_email_kdf(email: &str) -> (i32, i32, Option<i32>, Option<i32>) {
|
||||
const PBKDF2: i32 = UserKdfType::Pbkdf2 as i32;
|
||||
const ARGON2ID: i32 = UserKdfType::Argon2id as i32;
|
||||
// Same list as upstream, the default is in it twice to give it more weight
|
||||
const KDF_SETTINGS: [(i32, i32, Option<i32>, Option<i32>); 6] = [
|
||||
(User::CLIENT_KDF_TYPE_DEFAULT, User::CLIENT_KDF_ITER_DEFAULT, None, None),
|
||||
(User::CLIENT_KDF_TYPE_DEFAULT, User::CLIENT_KDF_ITER_DEFAULT, None, None),
|
||||
(PBKDF2, 100_000, None, None),
|
||||
(PBKDF2, 5_000, None, None),
|
||||
(ARGON2ID, 3, Some(64), Some(4)),
|
||||
(ARGON2ID, 6, Some(32), Some(4)),
|
||||
];
|
||||
KDF_SETTINGS[crate::auth::prelogin_kdf_index(email, KDF_SETTINGS.len())]
|
||||
}
|
||||
|
||||
pub async fn prelogin(data: Json<PreloginData>, ip: ClientIp, conn: DbConn) -> JsonResult {
|
||||
crate::ratelimit::check_limit_unauthenticated(&ip.ip)?;
|
||||
|
||||
let data: PreloginData = data.into_inner();
|
||||
// Normalized once, so the lookup and the settings for an unknown email use the same value
|
||||
let email = data.email.trim().to_lowercase();
|
||||
|
||||
let (kdf_type, kdf_iter, kdf_mem, kdf_para) = match User::find_by_mail(&data.email, &conn).await {
|
||||
let (kdf_type, kdf_iter, kdf_mem, kdf_para) = match User::find_by_mail(&email, &conn).await {
|
||||
Some(user) => (user.client_kdf_type, user.client_kdf_iter, user.client_kdf_memory, user.client_kdf_parallelism),
|
||||
None => (User::CLIENT_KDF_TYPE_DEFAULT, User::CLIENT_KDF_ITER_DEFAULT, None, None),
|
||||
None => unknown_email_kdf(&email),
|
||||
};
|
||||
|
||||
Ok(Json(json!({
|
||||
|
||||
Reference in new issue
Block a user