@ -1,4 +1,4 @@
use std ::collections ::HashSet ;
use std ::collections ::{ HashMap , HashSet } ;
use chrono ::Utc ;
use chrono ::Utc ;
use rocket ::{
use rocket ::{
@ -66,10 +66,13 @@ pub fn routes() -> Vec<rocket::Route> {
get_known_device ,
get_known_device ,
get_all_devices ,
get_all_devices ,
get_device ,
get_device ,
post_device_lost_trust ,
post_device_token ,
post_device_token ,
post_devices_untrust ,
put_device_token ,
put_device_token ,
put_clear_device_token ,
put_clear_device_token ,
post_clear_device_token ,
post_clear_device_token ,
put_device_keys ,
get_tasks ,
get_tasks ,
post_auth_request ,
post_auth_request ,
get_auth_request ,
get_auth_request ,
@ -832,6 +835,9 @@ struct RotateAccountUnlockData {
emergency_access_unlock_data : Vec < UpdateEmergencyAccessData > ,
emergency_access_unlock_data : Vec < UpdateEmergencyAccessData > ,
master_password_unlock_data : MasterPasswordUnlockData ,
master_password_unlock_data : MasterPasswordUnlockData ,
organization_account_recovery_unlock_data : Vec < UpdateResetPasswordData > ,
organization_account_recovery_unlock_data : Vec < UpdateResetPasswordData > ,
#[ serde(default) ]
device_key_unlock_data : Vec < UpdateDeviceKeysData > ,
}
}
#[ derive(Deserialize) ]
#[ derive(Deserialize) ]
@ -846,6 +852,14 @@ struct MasterPasswordUnlockData {
master_key_encrypted_user_key : String ,
master_key_encrypted_user_key : String ,
}
}
#[ derive(Deserialize) ]
#[ serde(rename_all = " camelCase " ) ]
struct UpdateDeviceKeysData {
device_id : DeviceId ,
encrypted_user_key : String ,
encrypted_public_key : String ,
}
#[ derive(Deserialize) ]
#[ derive(Deserialize) ]
#[ serde(rename_all = " camelCase " ) ]
#[ serde(rename_all = " camelCase " ) ]
struct RotateAccountKeys {
struct RotateAccountKeys {
@ -861,9 +875,11 @@ struct RotateAccountData {
sends : Vec < SendData > ,
sends : Vec < SendData > ,
}
}
#[ allow(clippy::too_many_arguments) ]
fn validate_keydata (
fn validate_keydata (
data : & KeyData ,
data : & KeyData ,
existing_ciphers : & [ Cipher ] ,
existing_ciphers : & [ Cipher ] ,
existing_devices : & [ Device ] ,
existing_folders : & [ Folder ] ,
existing_folders : & [ Folder ] ,
existing_emergency_access : & [ EmergencyAccess ] ,
existing_emergency_access : & [ EmergencyAccess ] ,
existing_memberships : & [ Membership ] ,
existing_memberships : & [ Membership ] ,
@ -929,6 +945,15 @@ fn validate_keydata(
err ! ( "All existing reset password keys must be included in the rotation" )
err ! ( "All existing reset password keys must be included in the rotation" )
}
}
// Check that we're correctly rotating all the user's trusted device
let existing_trusted_device_ids =
existing_devices . iter ( ) . filter ( | d | d . is_trusted ( ) ) . map ( | c | & c . uuid ) . collect ::< HashSet < & DeviceId > > ( ) ;
let provided_device_ids =
data . account_unlock_data . device_key_unlock_data . iter ( ) . map ( | rp | & rp . device_id ) . collect ::< HashSet < & DeviceId > > ( ) ;
if ! provided_device_ids . is_superset ( & existing_trusted_device_ids ) {
err ! ( "All existing trusted device keys must be included in the rotation" )
}
// Check that we're correctly rotating all the user's sends
// Check that we're correctly rotating all the user's sends
let existing_send_ids = existing_sends . iter ( ) . map ( | s | & s . uuid ) . collect ::< HashSet < & SendId > > ( ) ;
let existing_send_ids = existing_sends . iter ( ) . map ( | s | & s . uuid ) . collect ::< HashSet < & SendId > > ( ) ;
let provided_send_ids = data . account_data . sends . iter ( ) . filter_map ( | s | s . id . as_ref ( ) ) . collect ::< HashSet < & SendId > > ( ) ;
let provided_send_ids = data . account_data . sends . iter ( ) . filter_map ( | s | s . id . as_ref ( ) ) . collect ::< HashSet < & SendId > > ( ) ;
@ -959,6 +984,7 @@ async fn post_rotatekey(data: Json<KeyData>, headers: Headers, conn: DbConn, nt:
// TODO: Ideally we'd do everything after this point in a single transaction.
// TODO: Ideally we'd do everything after this point in a single transaction.
let mut existing_ciphers = Cipher ::find_owned_by_user ( user_id , & conn ) . await ;
let mut existing_ciphers = Cipher ::find_owned_by_user ( user_id , & conn ) . await ;
let existing_devices = Device ::find_by_user ( user_id , & conn ) . await ;
let mut existing_folders = Folder ::find_by_user ( user_id , & conn ) . await ;
let mut existing_folders = Folder ::find_by_user ( user_id , & conn ) . await ;
let mut existing_emergency_access = EmergencyAccess ::find_all_confirmed_by_grantor_uuid ( user_id , & conn ) . await ;
let mut existing_emergency_access = EmergencyAccess ::find_all_confirmed_by_grantor_uuid ( user_id , & conn ) . await ;
let mut existing_memberships = Membership ::find_by_user ( user_id , & conn ) . await ;
let mut existing_memberships = Membership ::find_by_user ( user_id , & conn ) . await ;
@ -969,6 +995,7 @@ async fn post_rotatekey(data: Json<KeyData>, headers: Headers, conn: DbConn, nt:
validate_keydata (
validate_keydata (
& data ,
& data ,
& existing_ciphers ,
& existing_ciphers ,
& existing_devices ,
& existing_folders ,
& existing_folders ,
& existing_emergency_access ,
& existing_emergency_access ,
& existing_memberships ,
& existing_memberships ,
@ -1014,6 +1041,20 @@ async fn post_rotatekey(data: Json<KeyData>, headers: Headers, conn: DbConn, nt:
membership . save ( & conn ) . await ? ;
membership . save ( & conn ) . await ? ;
}
}
// Update device
let mut device_updates : HashMap < DeviceId , UpdateDeviceKeysData > =
data . account_unlock_data . device_key_unlock_data . into_iter ( ) . map ( | d | ( d . device_id . clone ( ) , d ) ) . collect ( ) ;
for mut device in existing_devices {
if let Some ( update ) = device_updates . remove ( & device . uuid ) {
device . encrypted_public_key = Some ( update . encrypted_public_key ) ;
device . encrypted_user_key = Some ( update . encrypted_user_key ) ;
device . save ( true , & conn ) . await ? ;
} else if device . is_trusted ( ) {
device . lost_trust ( ) ;
device . save ( true , & conn ) . await ? ;
}
}
// Update send data
// Update send data
for send_data in data . account_data . sends {
for send_data in data . account_data . sends {
let Some ( send ) = send_data . id . as_ref ( ) . and_then ( | id | existing_sends . iter_mut ( ) . find ( | s | & s . uuid = = id ) ) else {
let Some ( send ) = send_data . id . as_ref ( ) . and_then ( | id | existing_sends . iter_mut ( ) . find ( | s | & s . uuid = = id ) ) else {
@ -1565,6 +1606,52 @@ async fn get_all_devices(headers: Headers, conn: DbConn) -> JsonResult {
} ) ) )
} ) ) )
}
}
/// Reported by a client that still holds a device key but did not get any keys back from us.
#[ post( " /devices/lost-trust " ) ]
async fn post_device_lost_trust ( headers : Headers , conn : DbConn ) -> EmptyResult {
let mut device = headers . device ;
warn ! (
"Device {} ({}) of user {} still holds a device key, but has no trusted device keys on the server" ,
device . uuid ,
DeviceType ::from_i32 ( device . atype ) ,
headers . user . uuid
) ;
// Still ensure that it's the case.
if device . lost_trust ( ) {
device . save ( true , & conn ) . await ? ;
}
Ok ( ( ) )
}
#[ derive(Deserialize) ]
#[ serde(rename_all = " camelCase " ) ]
struct DevicesUntrusted {
devices : Vec < DeviceId > ,
}
#[ post( " /devices/untrust " , data = " <data> " ) ]
async fn post_devices_untrust ( headers : Headers , data : Json < DevicesUntrusted > , conn : DbConn ) -> EmptyResult {
let untrusted = data . into_inner ( ) ;
if ! untrusted . devices . is_empty ( ) {
for device_id in untrusted . devices {
warn ! ( "Cannot find user {} device {} to untrust" , device_id , headers . user . email ) ;
if let Some ( mut device ) = Device ::find_by_uuid_and_user ( & device_id , & headers . user . uuid , & conn ) . await {
if device . lost_trust ( ) {
device . save ( true , & conn ) . await ? ;
}
} else {
warn ! ( "Cannot find user {} device {} to untrust" , device_id , headers . user . email ) ;
}
}
}
Ok ( ( ) )
}
#[ get( " /devices/identifier/<device_id> " ) ]
#[ get( " /devices/identifier/<device_id> " ) ]
async fn get_device ( device_id : DeviceId , headers : Headers , conn : DbConn ) -> JsonResult {
async fn get_device ( device_id : DeviceId , headers : Headers , conn : DbConn ) -> JsonResult {
let Some ( device ) = Device ::find_by_uuid_and_user ( & device_id , & headers . user . uuid , & conn ) . await else {
let Some ( device ) = Device ::find_by_uuid_and_user ( & device_id , & headers . user . uuid , & conn ) . await else {
@ -1640,6 +1727,40 @@ async fn post_clear_device_token(device_id: DeviceId, ip: ClientIp, conn: DbConn
put_clear_device_token ( device_id , ip , conn ) . await
put_clear_device_token ( device_id , ip , conn ) . await
}
}
#[ derive(Debug, Deserialize) ]
#[ serde(rename_all = " camelCase " ) ]
struct DeviceKeysData {
#[ serde(alias = " EncryptedUserKey " ) ]
encrypted_user_key : String ,
#[ serde(alias = " EncryptedPublicKey " ) ]
encrypted_public_key : String ,
#[ serde(alias = " EncryptedPrivateKey " ) ]
encrypted_private_key : String ,
}
// https://github.com/bitwarden/server/blob/v2026.3.1/src/Api/Controllers/DevicesController.cs
#[ put( " /devices/<device_id>/keys " , data = " <data> " ) ]
async fn put_device_keys (
device_id : DeviceId ,
data : Json < DeviceKeysData > ,
headers : Headers ,
conn : DbConn ,
) -> JsonResult {
if headers . device . uuid ! = device_id {
err ! ( "No device found" ) ;
}
let mut device = headers . device ;
let data = data . into_inner ( ) ;
device . encrypted_user_key = Some ( data . encrypted_user_key ) . filter ( | k | ! k . is_empty ( ) ) ;
device . encrypted_public_key = Some ( data . encrypted_public_key ) . filter ( | k | ! k . is_empty ( ) ) ;
device . encrypted_private_key = Some ( data . encrypted_private_key ) . filter ( | k | ! k . is_empty ( ) ) ;
device . save ( true , & conn ) . await ? ;
Ok ( Json ( device . to_json ( ) ) )
}
#[ get( " /tasks " ) ]
#[ get( " /tasks " ) ]
fn get_tasks ( _client_headers : ClientHeaders ) -> JsonResult {
fn get_tasks ( _client_headers : ClientHeaders ) -> JsonResult {
Ok ( Json ( json ! ( {
Ok ( Json ( json ! ( {