mirror of
https://github.com/dani-garcia/vaultwarden.wiki.git
synced 2026-11-13 00:08:36 +02:00
* Implement V2 key rotation support * Clear user key ids left stale by key rotations * Refuse key ids that aren't 32 lowercase hex characters, like upstream * Save the signature key pair before the v2 account state * Gate v2 logins on upstream's v2 encryption check * Document the v2 encryption feature flags * Remove the v2 upgrade feature flags With force-upgrade-v2-encryption on, the clients loop on any v1 account that stays v1 on a vault route: the migration check runs fullSync(false), which writes lastSync, which triggers the scheduler again. No server response stops it, so don't serve the flag until the clients fix it. pm-30144-sdk-key-rotation and pm-31050-no-logout-key-upgrade-rotation only serve that upgrade, so they go too. * Move the v2 upgrade token migration to its own version Diesel identifies a migration by the date in its folder name alone, and silently drops one of two folders that share it. The Custom Roles migration uses the same date. * Refuse rotations from a replaced user key, like upstream Both rotation endpoints accept the optional oldUserKeyId. When it and the stored key id are both set and differ, the rotation is refused with a 409 asking the client to sync, before the account keys are checked or anything is written. Also correct the comment on saving the signature key pair first: by then the vault data is already re-encrypted, so a failure there doesn't keep the account usable. * Update the v2 client minimum in .env.template and the key pair comments V2 accounts need 2026.10.1, or 2026.10.2 for the desktop app, so the per-flag versions are only enough to create one. The comments on saving the signature key pair now cover the rotation of an account that is already v2.
348 lines
12 KiB
Rust
348 lines
12 KiB
Rust
use std::{
|
|
sync::LazyLock,
|
|
time::{Duration, Instant},
|
|
};
|
|
|
|
use reqwest::{
|
|
Method,
|
|
header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE},
|
|
};
|
|
use serde_json::Value;
|
|
use tokio::sync::RwLock;
|
|
|
|
use crate::{
|
|
CONFIG,
|
|
api::{ApiResult, EmptyResult, LogOutReason, UpdateType},
|
|
db::{
|
|
DbConn,
|
|
models::{AuthRequestId, Cipher, Device, Folder, PushId, Send, User, UserId},
|
|
},
|
|
http_client::make_http_request,
|
|
util::{format_date, get_uuid},
|
|
};
|
|
|
|
#[derive(Deserialize)]
|
|
struct AuthPushToken {
|
|
access_token: String,
|
|
expires_in: i32,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct LocalAuthPushToken {
|
|
access_token: String,
|
|
valid_until: Instant,
|
|
}
|
|
|
|
async fn get_auth_api_token() -> ApiResult<String> {
|
|
static API_TOKEN: LazyLock<RwLock<LocalAuthPushToken>> = LazyLock::new(|| {
|
|
RwLock::new(LocalAuthPushToken {
|
|
access_token: String::new(),
|
|
valid_until: Instant::now(),
|
|
})
|
|
});
|
|
let api_token = API_TOKEN.read().await;
|
|
|
|
if api_token.valid_until.saturating_duration_since(Instant::now()).as_secs() > 0 {
|
|
debug!("Auth Push token still valid, no need for a new one");
|
|
return Ok(api_token.access_token.clone());
|
|
}
|
|
drop(api_token); // Drop the read lock now
|
|
|
|
let installation_id = CONFIG.push_installation_id();
|
|
let client_id = format!("installation.{installation_id}");
|
|
let client_secret = CONFIG.push_installation_key();
|
|
|
|
let params = [
|
|
("grant_type", "client_credentials"),
|
|
("scope", "api.push"),
|
|
("client_id", &client_id),
|
|
("client_secret", &client_secret),
|
|
];
|
|
|
|
let res = match make_http_request(Method::POST, &format!("{}/connect/token", CONFIG.push_identity_uri()))?
|
|
.form(¶ms)
|
|
.send()
|
|
.await
|
|
{
|
|
Ok(r) => r,
|
|
Err(e) => err!(format!("Error getting push token from bitwarden server: {e}")),
|
|
};
|
|
|
|
let json_pushtoken = match res.json::<AuthPushToken>().await {
|
|
Ok(r) => r,
|
|
Err(e) => err!(format!("Unexpected push token received from bitwarden server: {e}")),
|
|
};
|
|
|
|
let mut api_token = API_TOKEN.write().await;
|
|
// Token valid for half the specified time
|
|
let half_expires_in = u64::from((json_pushtoken.expires_in / 2).max(0).cast_unsigned());
|
|
api_token.valid_until = Instant::now().checked_add(Duration::from_secs(half_expires_in)).unwrap();
|
|
|
|
api_token.access_token = json_pushtoken.access_token;
|
|
|
|
debug!("Token still valid for {}", api_token.valid_until.saturating_duration_since(Instant::now()).as_secs());
|
|
Ok(api_token.access_token.clone())
|
|
}
|
|
|
|
pub async fn register_push_device(device: &mut Device, conn: &DbConn) -> EmptyResult {
|
|
if !CONFIG.push_enabled() || !device.is_push_device() {
|
|
return Ok(());
|
|
}
|
|
|
|
if device.push_token.is_none() {
|
|
warn!("Skipping the registration of the device {:?} because the push_token field is empty.", device.uuid);
|
|
warn!("To get rid of this message you need to logout, clear the app data and login again on the device.");
|
|
return Ok(());
|
|
}
|
|
|
|
debug!("Registering Device {:?}", device.push_uuid);
|
|
|
|
// Generate a random push_uuid so if it doesn't already have one
|
|
if device.push_uuid.is_none() {
|
|
device.push_uuid = Some(PushId(get_uuid()));
|
|
}
|
|
|
|
//Needed to register a device for push to bitwarden :
|
|
let data = json!({
|
|
"deviceId": device.push_uuid, // Unique UUID per user/device
|
|
"pushToken": device.push_token,
|
|
"userId": device.user_uuid,
|
|
"type": device.atype,
|
|
"identifier": device.uuid, // Unique UUID of the device/app, determined by the device/app it self currently registering
|
|
// "organizationIds:" [] // TODO: This is not yet implemented by Vaultwarden!
|
|
"installationId": CONFIG.push_installation_id(),
|
|
});
|
|
|
|
let auth_api_token = get_auth_api_token().await?;
|
|
let auth_header = format!("Bearer {auth_api_token}");
|
|
|
|
if let Err(e) = make_http_request(Method::POST, &(CONFIG.push_relay_uri() + "/push/register"))?
|
|
.header(CONTENT_TYPE, "application/json")
|
|
.header(ACCEPT, "application/json")
|
|
.header(AUTHORIZATION, auth_header)
|
|
.json(&data)
|
|
.send()
|
|
.await?
|
|
.error_for_status()
|
|
{
|
|
err!(format!("An error occurred while proceeding registration of a device: {e}"));
|
|
}
|
|
|
|
if let Err(e) = device.save(true, conn).await {
|
|
err!(format!("An error occurred while trying to save the (registered) device push uuid: {e}"));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn unregister_push_device(push_id: Option<&PushId>) -> EmptyResult {
|
|
if !CONFIG.push_enabled() || push_id.is_none() {
|
|
return Ok(());
|
|
}
|
|
let data = json!({
|
|
"id": push_id.as_ref().unwrap(),
|
|
});
|
|
|
|
let auth_api_token = get_auth_api_token().await?;
|
|
let auth_header = format!("Bearer {auth_api_token}");
|
|
|
|
if let Err(e) = make_http_request(Method::POST, &(CONFIG.push_relay_uri() + "/push/delete"))?
|
|
.header(CONTENT_TYPE, "application/json")
|
|
.header(ACCEPT, "application/json")
|
|
.header(AUTHORIZATION, auth_header)
|
|
.json(&data)
|
|
.send()
|
|
.await?
|
|
.error_for_status()
|
|
{
|
|
err!(format!("An error occurred during device unregistration: {e}"));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn push_cipher_update(ut: UpdateType, cipher: &Cipher, device: &Device, conn: &DbConn) {
|
|
// We shouldn't send a push notification on cipher update if the cipher belongs to an organization, this isn't implemented in the upstream server too.
|
|
if cipher.organization_uuid.is_some() {
|
|
return;
|
|
}
|
|
let Some(user_id) = &cipher.user_uuid else {
|
|
debug!("Cipher has no uuid");
|
|
return;
|
|
};
|
|
|
|
if Device::check_user_has_push_device(user_id, conn).await {
|
|
send_to_push_relay(json!({
|
|
"userId": user_id,
|
|
"organizationId": null,
|
|
"deviceId": device.push_uuid, // Should be the records unique uuid of the acting device (unique uuid per user/device)
|
|
"identifier": device.uuid, // Should be the acting device id (aka uuid per device/app)
|
|
"type": ut as i32,
|
|
"payload": {
|
|
"id": cipher.uuid,
|
|
"userId": cipher.user_uuid,
|
|
"organizationId": null,
|
|
"collectionIds": null,
|
|
"revisionDate": format_date(&cipher.updated_at)
|
|
},
|
|
"clientType": null,
|
|
"installationId": null
|
|
}))
|
|
.await;
|
|
}
|
|
}
|
|
|
|
pub async fn push_logout(user: &User, acting_device: Option<&Device>, reason: Option<LogOutReason>, conn: &DbConn) {
|
|
if Device::check_user_has_push_device(&user.uuid, conn).await {
|
|
push_logout_to_relay(user, acting_device, reason);
|
|
}
|
|
}
|
|
|
|
// Sends the logout without checking for push devices first.
|
|
// Needed after deleting a user, when the devices are already gone from the database.
|
|
pub fn push_logout_to_relay(user: &User, acting_device: Option<&Device>, reason: Option<LogOutReason>) {
|
|
tokio::task::spawn(send_to_push_relay(json!({
|
|
"userId": user.uuid,
|
|
"organizationId": (),
|
|
"deviceId": acting_device.and_then(|d| d.push_uuid.as_ref()),
|
|
"identifier": acting_device.map(|d| &d.uuid),
|
|
"type": UpdateType::LogOut as i32,
|
|
"payload": {
|
|
"userId": user.uuid,
|
|
"date": format_date(&user.updated_at),
|
|
"reason": reason.map(|r| r as i32),
|
|
},
|
|
"clientType": null,
|
|
"installationId": null
|
|
})));
|
|
}
|
|
|
|
pub async fn push_user_update(ut: UpdateType, user: &User, push_uuid: Option<&PushId>, conn: &DbConn) {
|
|
if Device::check_user_has_push_device(&user.uuid, conn).await {
|
|
tokio::task::spawn(send_to_push_relay(json!({
|
|
"userId": user.uuid,
|
|
"organizationId": null,
|
|
"deviceId": push_uuid,
|
|
"identifier": null,
|
|
"type": ut as i32,
|
|
"payload": {
|
|
"userId": user.uuid,
|
|
"date": format_date(&user.updated_at)
|
|
},
|
|
"clientType": null,
|
|
"installationId": null
|
|
})));
|
|
}
|
|
}
|
|
|
|
pub async fn push_folder_update(ut: UpdateType, folder: &Folder, device: &Device, conn: &DbConn) {
|
|
if Device::check_user_has_push_device(&folder.user_uuid, conn).await {
|
|
tokio::task::spawn(send_to_push_relay(json!({
|
|
"userId": folder.user_uuid,
|
|
"organizationId": null,
|
|
"deviceId": device.push_uuid, // Should be the records unique uuid of the acting device (unique uuid per user/device)
|
|
"identifier": device.uuid, // Should be the acting device id (aka uuid per device/app)
|
|
"type": ut as i32,
|
|
"payload": {
|
|
"id": folder.uuid,
|
|
"userId": folder.user_uuid,
|
|
"revisionDate": format_date(&folder.updated_at)
|
|
},
|
|
"clientType": null,
|
|
"installationId": null
|
|
})));
|
|
}
|
|
}
|
|
|
|
pub async fn push_send_update(ut: UpdateType, send: &Send, device: &Device, conn: &DbConn) {
|
|
if let Some(s) = &send.user_uuid
|
|
&& Device::check_user_has_push_device(s, conn).await
|
|
{
|
|
tokio::task::spawn(send_to_push_relay(json!({
|
|
"userId": send.user_uuid,
|
|
"organizationId": null,
|
|
"deviceId": device.push_uuid, // Should be the records unique uuid of the acting device (unique uuid per user/device)
|
|
"identifier": device.uuid, // Should be the acting device id (aka uuid per device/app)
|
|
"type": ut as i32,
|
|
"payload": {
|
|
"id": send.uuid,
|
|
"userId": send.user_uuid,
|
|
"revisionDate": format_date(&send.revision_date)
|
|
},
|
|
"clientType": null,
|
|
"installationId": null
|
|
})));
|
|
}
|
|
}
|
|
|
|
async fn send_to_push_relay(notification_data: Value) {
|
|
if !CONFIG.push_enabled() {
|
|
return;
|
|
}
|
|
|
|
let auth_api_token = match get_auth_api_token().await {
|
|
Ok(s) => s,
|
|
Err(e) => {
|
|
debug!("Could not get the auth push token: {e}");
|
|
return;
|
|
}
|
|
};
|
|
|
|
let auth_header = format!("Bearer {auth_api_token}");
|
|
|
|
let req = match make_http_request(Method::POST, &(CONFIG.push_relay_uri() + "/push/send")) {
|
|
Ok(r) => r,
|
|
Err(e) => {
|
|
error!("An error occurred while sending a send update to the push relay: {e}");
|
|
return;
|
|
}
|
|
};
|
|
|
|
if let Err(e) = req
|
|
.header(ACCEPT, "application/json")
|
|
.header(CONTENT_TYPE, "application/json")
|
|
.header(AUTHORIZATION, &auth_header)
|
|
.json(¬ification_data)
|
|
.send()
|
|
.await
|
|
{
|
|
error!("An error occurred while sending a send update to the push relay: {e}");
|
|
}
|
|
}
|
|
|
|
pub async fn push_auth_request(user_id: &UserId, auth_request_id: &str, device: &Device, conn: &DbConn) {
|
|
if Device::check_user_has_push_device(user_id, conn).await {
|
|
tokio::task::spawn(send_to_push_relay(json!({
|
|
"userId": user_id,
|
|
"organizationId": null,
|
|
"deviceId": device.push_uuid, // Should be the records unique uuid of the acting device (unique uuid per user/device)
|
|
"identifier": device.uuid, // Should be the acting device id (aka uuid per device/app)
|
|
"type": UpdateType::AuthRequest as i32,
|
|
"payload": {
|
|
"userId": user_id,
|
|
"id": auth_request_id,
|
|
},
|
|
"clientType": null,
|
|
"installationId": null
|
|
})));
|
|
}
|
|
}
|
|
|
|
pub async fn push_auth_response(user_id: &UserId, auth_request_id: &AuthRequestId, device: &Device, conn: &DbConn) {
|
|
if Device::check_user_has_push_device(user_id, conn).await {
|
|
tokio::task::spawn(send_to_push_relay(json!({
|
|
"userId": user_id,
|
|
"organizationId": null,
|
|
"deviceId": device.push_uuid, // Should be the records unique uuid of the acting device (unique uuid per user/device)
|
|
"identifier": device.uuid, // Should be the acting device id (aka uuid per device/app)
|
|
"type": UpdateType::AuthRequestResponse as i32,
|
|
"payload": {
|
|
"userId": user_id,
|
|
"id": auth_request_id,
|
|
},
|
|
"clientType": null,
|
|
"installationId": null
|
|
})));
|
|
}
|
|
}
|