@ -1,4 +1,4 @@
use std ::collections ::HashSet ;
use std ::collections ::{ HashMap , HashSet } ;
use chrono ::Utc ;
use chrono ::Utc ;
use rocket ::{
use rocket ::{
@ -20,10 +20,10 @@ use crate::{
db ::{
db ::{
DbConn , DbPool ,
DbConn , DbPool ,
models ::{
models ::{
AuthRequest , AuthRequestId , Cipher , CipherId , Device , DeviceId , DeviceType , DeviceWithAuthRequest ,
AuthRequest , AuthRequestId , AuthRequestType , Cipher , CipherId , Device , DeviceId , DeviceType ,
EmergencyAccess , EmergencyAccessId , EventType , Folder , FolderId , Invitation , KeyId , Membership ,
DeviceWithAuthRequest , EmergencyAccess , EmergencyAccessId , EventType , Folder , FolderId , Invitation , KeyId ,
MembershipId , OrgPolicy , OrgPolicyType , Organization , OrganizationId , Send , SendId , User , UserId ,
Membership , Membership Id, OrgPolicy , OrgPolicyType , Organization , OrganizationId , Send , SendId , User ,
UserKdfType ,
UserId , User KdfType ,
} ,
} ,
} ,
} ,
mail ,
mail ,
@ -46,6 +46,7 @@ pub fn routes() -> Vec<rocket::Route> {
post_keys ,
post_keys ,
post_password ,
post_password ,
post_set_password ,
post_set_password ,
put_update_tde_offboarding_password ,
post_kdf ,
post_kdf ,
post_rotatekey ,
post_rotatekey ,
post_user_key ,
post_user_key ,
@ -70,8 +71,14 @@ pub fn routes() -> Vec<rocket::Route> {
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 ,
post_device_keys ,
post_device_retrieve_keys ,
post_devices_untrust ,
post_devices_lost_trust ,
get_tasks ,
get_tasks ,
post_auth_request ,
post_auth_request ,
post_admin_auth_request ,
get_auth_request ,
get_auth_request ,
put_auth_request ,
put_auth_request ,
get_auth_request_response ,
get_auth_request_response ,
@ -446,8 +453,11 @@ async fn post_set_password(data: Json<SetPasswordData>, headers: Headers, conn:
let data : SetPasswordData = data . into_inner ( ) ;
let data : SetPasswordData = data . into_inner ( ) ;
let mut user = headers . user ;
let mut user = headers . user ;
if user . private_key . is_some ( ) | | ! user . password_hash . is_empty ( ) {
// A trusted device account already has its key pair but no master password, and must still be
err ! ( "Account already initialized, cannot set password" )
// able to add one later, for instance once the server stops offering trusted device encryption.
// What this must never do is hand out a fresh master password for an account that has one.
if ! user . password_hash . is_empty ( ) {
err ! ( "Account already has a master password" )
}
}
// Check against the password hint setting here so if it fails,
// Check against the password hint setting here so if it fails,
@ -455,6 +465,19 @@ async fn post_set_password(data: Json<SetPasswordData>, headers: Headers, conn:
let password_hint = clean_password_hint ( data . master_password_hint . as_ref ( ) ) ;
let password_hint = clean_password_hint ( data . master_password_hint . as_ref ( ) ) ;
enforce_password_hint_setting ( password_hint . as_ref ( ) ) ? ;
enforce_password_hint_setting ( password_hint . as_ref ( ) ) ? ;
// Same reasoning as in `post_keys`: the existing ciphers are encrypted under the existing key
// pair, so an account that has one only gets a password, never new keys.
let keys = match ( data . keys , user . private_key . is_some ( ) | | user . public_key . is_some ( ) ) {
( Some ( keys ) , false ) = > Some ( keys ) ,
( Some ( keys ) , true )
if user . private_key . as_ref ( ) ! = Some ( & keys . encrypted_private_key )
| | user . public_key . as_ref ( ) ! = Some ( & keys . public_key ) = >
{
err ! ( "Account already initialized, cannot replace the account keys" )
}
_ = > None ,
} ;
set_kdf_data ( & mut user , & data . kdf ) ? ;
set_kdf_data ( & mut user , & data . kdf ) ? ;
user . set_password (
user . set_password (
@ -467,7 +490,7 @@ async fn post_set_password(data: Json<SetPasswordData>, headers: Headers, conn:
. await ? ;
. await ? ;
user . password_hint = password_hint ;
user . password_hint = password_hint ;
if let Some ( keys ) = data . keys {
if let Some ( keys ) = keys {
user . private_key = Some ( keys . encrypted_private_key ) ;
user . private_key = Some ( keys . encrypted_private_key ) ;
user . public_key = Some ( keys . public_key ) ;
user . public_key = Some ( keys . public_key ) ;
}
}
@ -502,6 +525,117 @@ async fn post_set_password(data: Json<SetPasswordData>, headers: Headers, conn:
} ) ) )
} ) ) )
}
}
#[ derive(Deserialize) ]
#[ serde(rename_all = " camelCase " ) ]
struct UpdateTdeOffboardingPasswordData {
// Older clients send the authentication hash and wrapped user key directly.
new_master_password_hash : Option < String > ,
key : Option < String > ,
// Current Bitwarden clients send the two independently derived password data objects.
authentication_data : Option < MasterPasswordAuthentication > ,
unlock_data : Option < MasterPasswordUnlock > ,
master_password_hint : Option < String > ,
}
/// Gives an account that unlocks with a trusted device the master password it needs once the server stops
/// offering trusted devices.
///
/// The endpoint the clients take when a login answered `IsTdeOffboarding`, see `trusted_device_option`.
/// Deliberately not `/accounts/set-password`: the account is fully set up by this point, so the only
/// thing added is a second way to unlock the user key it already has. The account key pair and the vault
/// are left as they are, and unlike `/accounts/keys` nothing here could replace them.
///
/// Upstream keys this on the organization having switched its SSO member decryption away from trusted
/// devices; Vaultwarden configures SSO for the whole server, so the same state is `SSO_ENABLED` without
/// `SSO_TRUSTED_DEVICE_ENCRYPTION`, which is exactly when a login starts answering `IsTdeOffboarding`.
/// https://github.com/bitwarden/server/blob/main/src/Core/Auth/UserFeatures/TdeOffboardingPassword/TdeOffboardingPasswordCommand.cs
#[ put( " /accounts/update-tde-offboarding-password " , data = " <data> " ) ]
async fn put_update_tde_offboarding_password (
data : Json < UpdateTdeOffboardingPasswordData > ,
headers : Headers ,
conn : DbConn ,
nt : Notify < '_ > ,
) -> EmptyResult {
let data = data . into_inner ( ) ;
let mut user = headers . user ;
// Adding a master password to an account that has one is changing it, which is `/accounts/password`
// and asks for the current one first. Without this an authenticated caller could replace the password
// of the account they are on, and a second offboarding call would overwrite the first one's password.
if ! user . password_hash . is_empty ( ) {
err ! ( "Account already has a master password" )
}
// The way out of trusted devices only exists while the server still takes SSO logins but no
// longer offers trusted devices. A server that still offers them has nothing to offboard from,
// and one without SSO never had the flow at all.
if ! CONFIG . sso_enabled ( ) | | CONFIG . sso_trusted_device_encryption ( ) {
err ! ( "Trusted device offboarding is not available on this server" )
}
// Bitwarden accepts either shape or both; prefer the modern pair when it is complete.
// Do not combine individual fields from different payload variants.
let ( password_hash , key ) =
match ( data . authentication_data , data . unlock_data , data . new_master_password_hash , data . key ) {
( Some ( authentication ) , Some ( unlock ) , _ , _ ) = > {
// The first master password establishes the KDF settings. It does not have to
// inherit the placeholder KDF of the former TDE-only account.
if authentication . kdf ! = unlock . kdf
| | authentication . salt ! = unlock . salt
| | authentication . salt ! = user . email . trim ( ) . to_lowercase ( )
{
err ! ( "Authentication and unlock password parameters do not agree" )
}
// Bitwarden validates the KDF ranges when an initial master password is set.
let kdf = & authentication . kdf ;
match kdf . kdf {
x if x = = UserKdfType ::Pbkdf2 as i32 = > {
if ! ( 600_000 . . = 2_000_000 ) . contains ( & kdf . kdf_iterations ) {
err ! ( "PBKDF2 KDF iterations must be between 600000 and 2000000" )
}
}
x if x = = UserKdfType ::Argon2id as i32 = > {
if ! ( 2 . . = 10 ) . contains ( & kdf . kdf_iterations )
| | ! kdf . kdf_memory . is_some_and ( | m | ( 15 . . = 1024 ) . contains ( & m ) )
| | ! kdf . kdf_parallelism . is_some_and ( | p | ( 1 . . = 16 ) . contains ( & p ) )
{
err ! ( "Invalid Argon2 KDF settings" )
}
}
_ = > err ! ( "Unsupported KDF type" ) ,
}
set_kdf_data ( & mut user , kdf ) ? ;
( authentication . hash , unlock . key )
}
( _ , _ , Some ( password_hash ) , Some ( key ) ) = > ( password_hash , key ) ,
_ = > err ! ( "Supply either legacy or modern master password data" ) ,
} ;
// An invalid wrapped user key would make the password unable to decrypt the vault.
if ! crate ::util ::is_valid_enc_string ( & key ) {
err ! ( "key is not a valid encrypted string" )
}
let password_hint = clean_password_hint ( data . master_password_hint . as_ref ( ) ) ;
enforce_password_hint_setting ( password_hint . as_ref ( ) ) ? ;
// TDE offboarding changes only the way the existing user key is wrapped.
user . set_password ( & password_hash , Some ( key ) , true , None , & conn ) . await ? ;
user . password_hint = password_hint ;
log_user_event ( EventType ::UserTdeOffboardingPasswordSet , & user . uuid , headers . device . atype , & headers . ip . ip , & conn )
. await ;
user . save ( & conn ) . await ? ;
// Upstream logs every session out at this point. The account unlocks a different way from now
// on, so the sessions that were opened against a trusted device do not carry over.
nt . send_logout ( & user , None , & conn ) . await ;
Ok ( ( ) )
}
#[ get( " /accounts/profile " ) ]
#[ get( " /accounts/profile " ) ]
async fn profile ( headers : Headers , conn : DbConn ) -> Json < Value > {
async fn profile ( headers : Headers , conn : DbConn ) -> Json < Value > {
Json ( headers . user . to_json ( & conn ) . await )
Json ( headers . user . to_json ( & conn ) . await )
@ -601,8 +735,22 @@ async fn post_keys(data: Json<KeysData>, headers: Headers, conn: DbConn) -> Json
let mut user = headers . user ;
let mut user = headers . user ;
// Replacing the key pair of an initialized account would make every existing cipher undecryptable,
// so only accept it while the account has none yet. The clients call this during account creation,
// including the trusted device flow, where a stale client state could otherwise send us here for an
// account that is already set up. Repeating the same keys stays allowed so a retry does not fail.
if user . private_key . is_some ( ) | | user . public_key . is_some ( ) {
if user . private_key . is_some ( ) | | user . public_key . is_some ( ) {
err ! ( "User has existing keypair" )
if user . private_key . as_ref ( ) ! = Some ( & data . encrypted_private_key )
| | user . public_key . as_ref ( ) ! = Some ( & data . public_key )
{
err ! ( "Account already initialized, cannot replace the account keys" )
}
return Ok ( Json ( json ! ( {
"privateKey" : user . private_key ,
"publicKey" : user . public_key ,
"object" :"keys"
} ) ) ) ;
}
}
user . private_key = Some ( data . encrypted_private_key ) ;
user . private_key = Some ( data . encrypted_private_key ) ;
@ -832,6 +980,16 @@ 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 > ,
/// The user key, re-wrapped for every device that unlocks the vault without a master password.
device_key_unlock_data : Vec < UpdateDeviceKeysData > ,
}
#[ derive(Deserialize) ]
#[ serde(rename_all = " camelCase " ) ]
struct UpdateDeviceKeysData {
device_id : DeviceId ,
encrypted_user_key : String ,
encrypted_public_key : String ,
}
}
#[ derive(Deserialize) ]
#[ derive(Deserialize) ]
@ -861,6 +1019,52 @@ struct RotateAccountData {
sends : Vec < SendData > ,
sends : Vec < SendData > ,
}
}
/// Works out what a key rotation has to write to the user's devices.
///
/// Returns the devices that keep their trust, each with the user key freshly wrapped for it; whatever
/// the user owns beyond that list ends up untrusted, so the caller can hand the result to
/// `Device::replace_trust` and be done in one transaction. Mirrors `DeviceRotationValidator` upstream,
/// which refuses a rotation that would quietly drop the trust of a device the user still relies on.
/// https://github.com/bitwarden/server/blob/main/src/Api/KeyManagement/Validators/DeviceRotationValidator.cs
fn validate_device_keydata (
updates : & [ UpdateDeviceKeysData ] ,
existing_devices : & [ Device ] ,
) -> ApiResult < Vec < ( DeviceId , String , String ) > > {
// Everything the client sent is checked before any of it is used, so a request that is
// malformed anywhere is refused as a whole rather than answered in part.
let mut listed : HashMap < & DeviceId , & UpdateDeviceKeysData > = HashMap ::with_capacity ( updates . len ( ) ) ;
for update in updates {
if listed . insert ( & update . device_id , update ) . is_some ( ) {
err ! ( "A device was listed more than once in the rotation" )
}
if ! existing_devices . iter ( ) . any ( | device | device . uuid = = update . device_id ) {
err ! ( format ! ( "Device {} does not belong to this user" , update . device_id ) )
}
validate_enc_strings ( & [
( "encryptedUserKey" , & update . encrypted_user_key ) ,
( "encryptedPublicKey" , & update . encrypted_public_key ) ,
] ) ? ;
}
// Walked over the devices that are trusted right now rather than over what was sent, because a
// rotation may only carry an existing trust over to the new user key. Trusting a device is a step of
// its own, `PUT /devices/<id>/keys`. An entry for anything else is passed over, as upstream does.
let mut rotated = Vec ::new ( ) ;
for device in existing_devices . iter ( ) . filter ( | device | device . is_trusted ( ) ) {
let Some ( update ) = listed . get ( & device . uuid ) else {
err ! ( "All existing trusted devices must be included in the rotation" )
} ;
rotated . push ( ( device . uuid . clone ( ) , update . encrypted_user_key . clone ( ) , update . encrypted_public_key . clone ( ) ) ) ;
}
Ok ( rotated )
}
fn validate_keydata (
fn validate_keydata (
data : & KeyData ,
data : & KeyData ,
existing_ciphers : & [ Cipher ] ,
existing_ciphers : & [ Cipher ] ,
@ -965,6 +1169,7 @@ async fn post_rotatekey(data: Json<KeyData>, headers: Headers, conn: DbConn, nt:
// We only rotate the reset password key if it is set.
// We only rotate the reset password key if it is set.
existing_memberships . retain ( | m | m . reset_password_key . is_some ( ) ) ;
existing_memberships . retain ( | m | m . reset_password_key . is_some ( ) ) ;
let mut existing_sends = Send ::find_by_user ( user_id , & conn ) . await ;
let mut existing_sends = Send ::find_by_user ( user_id , & conn ) . await ;
let existing_devices = Device ::find_by_user ( user_id , & conn ) . await ;
validate_keydata (
validate_keydata (
& data ,
& data ,
@ -976,6 +1181,8 @@ async fn post_rotatekey(data: Json<KeyData>, headers: Headers, conn: DbConn, nt:
& headers . user ,
& headers . user ,
) ? ;
) ? ;
let rotated_devices = validate_device_keydata ( & data . account_unlock_data . device_key_unlock_data , & existing_devices ) ? ;
// Update folder data
// Update folder data
for folder_data in data . account_data . folders {
for folder_data in data . account_data . folders {
// Skip `null` folder id entries.
// Skip `null` folder id entries.
@ -1041,6 +1248,11 @@ async fn post_rotatekey(data: Json<KeyData>, headers: Headers, conn: DbConn, nt:
}
}
}
}
// Every device holds the previous user key wrapped for itself, which unlocks nothing anymore. Settle
// that here rather than after the account itself: the ciphers have already been rewritten under the
// new user key, so a device holding the new one is the half that still works if what follows fails.
Device ::replace_trust ( & headers . user . uuid , rotated_devices , & conn ) . await ? ;
// Update user data
// Update user data
let mut user = headers . user ;
let mut user = headers . user ;
@ -1602,7 +1814,7 @@ async fn put_device_token(device_id: DeviceId, data: Json<PushToken>, headers: H
}
}
device . push_token = Some ( token ) ;
device . push_token = Some ( token ) ;
if let Err ( e ) = device . save ( true , & conn ) . await {
if let Err ( e ) = device . save_activity ( & conn ) . await {
err ! ( format ! ( "An error occurred while trying to save the device push token: {e}" ) ) ;
err ! ( format ! ( "An error occurred while trying to save the device push token: {e}" ) ) ;
}
}
@ -1640,6 +1852,127 @@ 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
}
}
// Trusted device encryption, see https://bitwarden.com/help/login-with-sso-trusted-devices/
// The three key blobs below are generated and encrypted by the client, the server only stores them and
// hands them back on the next login of that same device. It never learns the device key that unwraps
// `encrypted_private_key`, so a stored trust is worth nothing without the device itself.
// https://github.com/bitwarden/server/blob/main/src/Api/Controllers/DevicesController.cs
#[ derive(Debug, Deserialize) ]
#[ serde(rename_all = " camelCase " ) ]
struct TrustedDeviceKeysData {
encrypted_user_key : String ,
encrypted_public_key : String ,
encrypted_private_key : String ,
}
/// Refuses anything that does not even have the shape of an `EncString`.
///
/// The server cannot tell whether a blob decrypts, but storing something that certainly does not only
/// leaves a device that calls itself trusted and fails its owner at the next unlock. Upstream puts
/// `[EncryptedString]` on the same fields.
fn validate_enc_strings ( values : & [ ( & str , & str ) ] ) -> EmptyResult {
for ( name , value ) in values {
if ! crate ::util ::is_valid_enc_string ( value ) {
err ! ( format ! ( "{name} is not a valid encrypted string" ) )
}
}
Ok ( ( ) )
}
/// Marks a device of the current user as trusted.
///
/// Upstream keys this on the device identifier and does not require it to be the device the request was
/// authenticated with, so neither do we. The keys only ever unlock the vault on the device holding the
/// matching device key, so writing them for another of your own devices gains nothing.
#[ put( " /devices/<device_id>/keys " , data = " <data> " ) ]
async fn put_device_keys (
device_id : DeviceId ,
data : Json < TrustedDeviceKeysData > ,
headers : Headers ,
conn : DbConn ,
) -> JsonResult {
let data = data . into_inner ( ) ;
validate_enc_strings ( & [
( "encryptedUserKey" , & data . encrypted_user_key ) ,
( "encryptedPublicKey" , & data . encrypted_public_key ) ,
( "encryptedPrivateKey" , & data . encrypted_private_key ) ,
] ) ? ;
let Some ( mut device ) = Device ::find_by_uuid_and_user ( & device_id , & headers . user . uuid , & conn ) . await else {
err ! ( "No device found" )
} ;
device . encrypted_user_key = Some ( data . encrypted_user_key ) ;
device . encrypted_public_key = Some ( data . encrypted_public_key ) ;
device . encrypted_private_key = Some ( data . encrypted_private_key ) ;
device . save ( true , & conn ) . await ? ;
Ok ( Json ( device . to_json ( ) ) )
}
// Deprecated upstream in favour of the PUT variant, but still served for older clients
#[ post( " /devices/<device_id>/keys " , data = " <data> " ) ]
async fn post_device_keys (
device_id : DeviceId ,
data : Json < TrustedDeviceKeysData > ,
headers : Headers ,
conn : DbConn ,
) -> JsonResult {
put_device_keys ( device_id , data , headers , conn ) . await
}
/// The public half of a device's trust, needed by the clients to re-wrap the user key for every
/// trusted device during a key rotation.
#[ post( " /devices/<device_id>/retrieve-keys " ) ]
async fn post_device_retrieve_keys ( 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 {
err ! ( "No device found" )
} ;
Ok ( Json ( device . to_protected_json ( ) ) )
}
#[ derive(Debug, Deserialize) ]
#[ serde(rename_all = " camelCase " ) ]
struct UntrustDevicesData {
devices : Vec < DeviceId > ,
}
#[ post( " /devices/untrust " , data = " <data> " ) ]
async fn post_devices_untrust ( data : Json < UntrustDevicesData > , headers : Headers , conn : DbConn ) -> EmptyResult {
let data = data . into_inner ( ) ;
let owned : HashSet < DeviceId > =
Device ::find_by_user ( & headers . user . uuid , & conn ) . await . into_iter ( ) . map ( | device | device . uuid ) . collect ( ) ;
// Check that the user owns all of them first, so a single foreign id does not leave the request
// half applied.
if let Some ( unknown ) = data . devices . iter ( ) . find ( | device_id | ! owned . contains ( * device_id ) ) {
err ! ( format ! ( "Device {unknown} does not belong to this user" ) )
}
Device ::untrust_many ( & headers . user . uuid , data . devices , & conn ) . await
}
/// Reported by a client that still holds a device key but did not get any keys back from us.
///
/// Nothing is left to clean up, the device already counts as untrusted here. Upstream only writes a log
/// line as well, since this points at the client and the server having drifted apart.
#[ expect(clippy::needless_pass_by_value, reason = " Not beneficial for Headers " ) ]
#[ post( " /devices/lost-trust " ) ]
fn post_devices_lost_trust ( headers : Headers ) -> EmptyResult {
warn ! (
"Device {} ({}) of user {} still holds a device key, but has no trusted device keys on the server" ,
headers . device . uuid ,
DeviceType ::from_i32 ( headers . device . atype ) ,
headers . user . uuid
) ;
Ok ( ( ) )
}
#[ get( " /tasks " ) ]
#[ get( " /tasks " ) ]
fn get_tasks ( _client_headers : ClientHeaders ) -> JsonResult {
fn get_tasks ( _client_headers : ClientHeaders ) -> JsonResult {
Ok ( Json ( json ! ( {
Ok ( Json ( json ! ( {
@ -1655,9 +1988,57 @@ struct AuthRequestRequest {
device_identifier : DeviceId ,
device_identifier : DeviceId ,
email : String ,
email : String ,
public_key : String ,
public_key : String ,
// Not used for now
#[ serde(default, rename = " type " ) ]
// #[serde(alias = "type")]
atype : i32 ,
// _type: i32,
}
/// Upstream puts `[StringLength(25)]` on the access code, so no client sends more than that.
/// https://github.com/bitwarden/server/blob/main/src/Core/Auth/Models/Api/Request/AuthRequest/AuthRequestCreateRequestModel.cs
const MAX_ACCESS_CODE_LENGTH : usize = 25 ;
/// A base64 SPKI RSA-4096 public key is under a kilobyte; this leaves room for whatever comes next.
const MAX_REQUEST_PUBLIC_KEY_LENGTH : usize = 4096 ;
impl AuthRequestRequest {
/// Both of these end up stored, and the admin approval route stores a copy per organization the user
/// belongs to, so neither may be unbounded. The public key is handed to the answering client as
/// base64 to wrap a key against; one that is not base64 would break the page listing the requests.
fn validate ( & self ) -> EmptyResult {
if self . access_code . is_empty ( ) | | self . access_code . len ( ) > MAX_ACCESS_CODE_LENGTH {
err ! ( "Invalid access code" )
}
if self . public_key . is_empty ( )
| | self . public_key . len ( ) > MAX_REQUEST_PUBLIC_KEY_LENGTH
| | data_encoding ::BASE64 . decode ( self . public_key . as_bytes ( ) ) . is_err ( )
{
err ! ( "Invalid public key" )
}
Ok ( ( ) )
}
}
fn auth_request_json ( auth_request : & AuthRequest ) -> Value {
json ! ( {
"id" : auth_request . uuid ,
"publicKey" : auth_request . public_key ,
"type" : auth_request . atype ,
"requestDeviceType" : DeviceType ::from_i32 ( auth_request . device_type ) . to_string ( ) ,
// The clients read the raw enum value as well, to pick an icon for the asking device.
"requestDeviceTypeValue" : auth_request . device_type ,
"requestDeviceIdentifier" : auth_request . request_device_identifier ,
"requestIpAddress" : auth_request . request_ip ,
// Not recorded here, but the clients read it, so it is answered rather than missing.
"requestCountryName" : null ,
"key" : auth_request . enc_key ,
"masterPasswordHash" : auth_request . master_password_hash ,
"creationDate" : format_date ( & auth_request . creation_date ) ,
"responseDate" : auth_request . response_date . as_ref ( ) . map ( format_date ) ,
"requestApproved" : auth_request . approved . unwrap_or ( false ) ,
"origin" : CONFIG . domain_origin ( ) ,
"object" : "auth-request"
} )
}
}
#[ post( " /auth-requests " , data = " <data> " ) ]
#[ post( " /auth-requests " , data = " <data> " ) ]
@ -1671,6 +2052,14 @@ async fn post_auth_request(
let data = data . into_inner ( ) ;
let data = data . into_inner ( ) ;
// Asking an administrator for approval means telling them who is asking, so that one is only
// available to a caller who has already proven who they are. See `post_admin_auth_request`.
if AuthRequestType ::from_i32 ( data . atype ) = = Some ( AuthRequestType ::AdminApproval ) {
err ! ( "You must be authenticated to create a request of that type" )
}
data . validate ( ) ? ;
let Some ( user ) = User ::find_by_mail ( & data . email , & conn ) . await else {
let Some ( user ) = User ::find_by_mail ( & data . email , & conn ) . await else {
err ! ( "AuthRequest doesn't exist" , "User not found" )
err ! ( "AuthRequest doesn't exist" , "User not found" )
} ;
} ;
@ -1681,8 +2070,14 @@ async fn post_auth_request(
_ = > err ! ( "AuthRequest doesn't exist" , "Device verification failed" ) ,
_ = > err ! ( "AuthRequest doesn't exist" , "Device verification failed" ) ,
} ;
} ;
let Some ( atype ) = AuthRequestType ::from_i32 ( data . atype ) else {
err ! ( "Unknown auth request type" )
} ;
let auth_request = AuthRequest ::new (
let auth_request = AuthRequest ::new (
user . uuid . clone ( ) ,
user . uuid . clone ( ) ,
None ,
atype ,
data . device_identifier . clone ( ) ,
data . device_identifier . clone ( ) ,
client_headers . device_type ,
client_headers . device_type ,
client_headers . ip . ip . to_string ( ) ,
client_headers . ip . ip . to_string ( ) ,
@ -1702,19 +2097,109 @@ async fn post_auth_request(
)
)
. await ;
. await ;
Ok ( Json ( json ! ( {
Ok ( Json ( auth_request_json ( & auth_request ) ) )
"id" : auth_request . uuid ,
}
"publicKey" : auth_request . public_key ,
"requestDeviceType" : DeviceType ::from_i32 ( auth_request . device_type ) . to_string ( ) ,
/// Asks the administrators of every organization the user belongs to to let this device in.
"requestIpAddress" : auth_request . request_ip ,
///
"key" : null ,
/// The way out for someone who unlocks with trusted devices and has no other device left to ask. One
"masterPasswordHash" : null ,
/// request per organization, so whichever administrator gets there first can answer.
"creationDate" : format_date ( & auth_request . creation_date ) ,
/// https://github.com/bitwarden/server/blob/main/src/Api/Auth/Controllers/AuthRequestsController.cs
"responseDate" : null ,
#[ post( " /auth-requests/admin-request " , data = " <data> " ) ]
"requestApproved" : false ,
async fn post_admin_auth_request ( data : Json < AuthRequestRequest > , headers : Headers , conn : DbConn ) -> JsonResult {
"origin" : CONFIG . domain_origin ( ) ,
// Every call mails all administrators of every organization involved, so it is worth its own limit.
"object" : "auth-request"
crate ::ratelimit ::check_limit_unauthenticated ( & headers . ip . ip ) ? ;
} ) ) )
let data = data . into_inner ( ) ;
if AuthRequestType ::from_i32 ( data . atype ) ! = Some ( AuthRequestType ::AdminApproval ) {
err ! ( "Invalid auth request type, expected admin approval" )
}
if data . device_identifier ! = headers . device . uuid {
err ! ( "AuthRequest doesn't exist" , "Device verification failed" )
}
data . validate ( ) ? ;
// Only an organization that could actually answer is asked. Approving hands the member their own
// user key, which an administrator can only do with the key that enrolling into account recovery
// left them, so an organization without one has nothing to offer and needs none of the asker's data.
let memberships : Vec < Membership > = Membership ::find_by_user ( & headers . user . uuid , & conn )
. await
. into_iter ( )
. filter ( Membership ::can_use_admin_approval )
. collect ( ) ;
if memberships . is_empty ( ) {
err ! ( "User does not belong to any organization that could approve a device" )
}
log_user_event (
EventType ::UserRequestedDeviceApproval ,
& headers . user . uuid ,
headers . device . atype ,
& headers . ip . ip ,
& conn ,
)
. await ;
let mut first_request = None ;
for membership in memberships {
// Like Bitwarden, each approval attempt gets its own immutable creation time and ID.
// Reusing a request and refreshing its creation date would extend the 7-day window,
// and suppress a second administrator notification after a mail delivery failure.
let auth_request = AuthRequest ::new (
headers . user . uuid . clone ( ) ,
Some ( membership . org_uuid . clone ( ) ) ,
AuthRequestType ::AdminApproval ,
data . device_identifier . clone ( ) ,
headers . device . atype ,
headers . ip . ip . to_string ( ) ,
data . access_code . clone ( ) ,
data . public_key . clone ( ) ,
) ;
auth_request . save ( & conn ) . await ? ;
notify_device_approval_requested ( & headers . user , & membership . org_uuid , & conn ) . await ;
if first_request . is_none ( ) {
first_request = Some ( auth_request ) ;
}
}
// Guaranteed by the emptiness check above
let auth_request = first_request . expect ( "at least one organization" ) ;
Ok ( Json ( auth_request_json ( & auth_request ) ) )
}
/// Mails everyone in the organization who could answer the request. Failing to reach them must not
/// undo the request itself, so problems are logged rather than returned.
async fn notify_device_approval_requested ( user : & User , org_id : & OrganizationId , conn : & DbConn ) {
if ! CONFIG . mail_enabled ( ) {
return ;
}
let Some ( org ) = Organization ::find_by_uuid ( org_id , conn ) . await else {
return ;
} ;
// The same set that may answer the request, see `ManageResetPasswordHeaders`. Mailing anyone
// else would tell them who is asking for something they cannot do anything about.
let approvers = Membership ::find_confirmed_by_org ( org_id , conn )
. await
. into_iter ( )
. filter ( Membership ::can_manage_reset_password_now ) ;
for approver in approvers {
let Some ( admin ) = User ::find_by_uuid ( & approver . user_uuid , conn ) . await else {
continue ;
} ;
if let Err ( e ) =
mail ::send_device_approval_requested ( & admin . email , org_id , & org . name , & user . email , & user . name ) . await
{
error ! ( "Error sending device approval request email: {e:#?}" ) ;
}
}
}
}
#[ get( " /auth-requests/<auth_request_id> " ) ]
#[ get( " /auth-requests/<auth_request_id> " ) ]
@ -1724,21 +2209,13 @@ async fn get_auth_request(auth_request_id: AuthRequestId, headers: Headers, conn
err ! ( "AuthRequest doesn't exist" , "Record not found or user uuid does not match" )
err ! ( "AuthRequest doesn't exist" , "Record not found or user uuid does not match" )
} ;
} ;
let response_date_utc = auth_request . response_date . map ( | response_date | format_date ( & response_date ) ) ;
// The anonymous lookup refuses an expired request, and so does this one: the window an approval
// stays usable in should not depend on which of the two the client happens to poll.
if auth_request . is_expired ( ) {
err ! ( "AuthRequest doesn't exist" , "Request has expired" )
}
Ok ( Json ( json ! ( {
Ok ( Json ( auth_request_json ( & auth_request ) ) )
"id" : & auth_request_id ,
"publicKey" : auth_request . public_key ,
"requestDeviceType" : DeviceType ::from_i32 ( auth_request . device_type ) . to_string ( ) ,
"requestIpAddress" : auth_request . request_ip ,
"key" : auth_request . enc_key ,
"masterPasswordHash" : auth_request . master_password_hash ,
"creationDate" : format_date ( & auth_request . creation_date ) ,
"responseDate" : response_date_utc ,
"requestApproved" : auth_request . approved ,
"origin" : CONFIG . domain_origin ( ) ,
"object" :"auth-request"
} ) ) )
}
}
#[ derive(Debug, Deserialize) ]
#[ derive(Debug, Deserialize) ]
@ -1765,6 +2242,13 @@ async fn put_auth_request(
err ! ( "AuthRequest doesn't exist" , "Record not found or user uuid does not match" )
err ! ( "AuthRequest doesn't exist" , "Record not found or user uuid does not match" )
} ;
} ;
// A request addressed to an administrator is answered through the organization, where the
// permission to do so can actually be checked. Letting the asking user answer it here would
// make the whole detour pointless.
if auth_request . is_admin_approval ( ) {
err ! ( "AuthRequest doesn't exist" , "Admin approval requests are answered by the organization" )
}
if headers . device . uuid ! = data . device_identifier {
if headers . device . uuid ! = data . device_identifier {
err ! ( "AuthRequest doesn't exist" , "Device verification failed" )
err ! ( "AuthRequest doesn't exist" , "Device verification failed" )
}
}
@ -1773,8 +2257,26 @@ async fn put_auth_request(
err ! ( "An authentication request with the same device already exists" )
err ! ( "An authentication request with the same device already exists" )
}
}
if auth_request . is_expired ( ) {
err ! ( "AuthRequest doesn't exist" , "Request has expired" )
}
// Only the newest request of a device may be approved. Anyone can create a request for a known
// device, so without this an older one could still be sitting there when the user approves what
// their screen shows, and the answer would go to whoever left it. Same check as upstream.
if data . request_approved
& & AuthRequest ::find_by_user_and_requested_device (
& headers . user . uuid ,
& auth_request . request_device_identifier ,
& conn ,
)
. await
. is_none_or ( | newest | newest . uuid ! = auth_request . uuid )
{
err ! ( "This request is no longer valid. Make sure to approve the most recent request." )
}
let response_date = Utc ::now ( ) . naive_utc ( ) ;
let response_date = Utc ::now ( ) . naive_utc ( ) ;
let response_date_utc = format_date ( & response_date ) ;
if data . request_approved {
if data . request_approved {
auth_request . approved = Some ( data . request_approved ) ;
auth_request . approved = Some ( data . request_approved ) ;
@ -1785,7 +2287,7 @@ async fn put_auth_request(
auth_request . save ( & conn ) . await ? ;
auth_request . save ( & conn ) . await ? ;
ant . send_auth_response ( & auth_request . user_uuid , & auth_request . uuid ) . await ;
ant . send_auth_response ( & auth_request . user_uuid , & auth_request . uuid ) . await ;
nt . send_auth_response ( & auth_request . user_uuid , & auth_request . uuid , & headers . device , & conn ) . await ;
nt . send_auth_response ( & auth_request . user_uuid , & auth_request . uuid , Some ( & headers . device ) , & conn ) . await ;
log_user_event (
log_user_event (
EventType ::OrganizationUserApprovedAuthRequest ,
EventType ::OrganizationUserApprovedAuthRequest ,
@ -1808,19 +2310,7 @@ async fn put_auth_request(
. await ;
. await ;
}
}
Ok ( Json ( json ! ( {
Ok ( Json ( auth_request_json ( & auth_request ) ) )
"id" : & auth_request_id ,
"publicKey" : auth_request . public_key ,
"requestDeviceType" : DeviceType ::from_i32 ( auth_request . device_type ) . to_string ( ) ,
"requestIpAddress" : auth_request . request_ip ,
"key" : auth_request . enc_key ,
"masterPasswordHash" : auth_request . master_password_hash ,
"creationDate" : format_date ( & auth_request . creation_date ) ,
"responseDate" : response_date_utc ,
"requestApproved" : auth_request . approved ,
"origin" : CONFIG . domain_origin ( ) ,
"object" :"auth-request"
} ) ) )
}
}
#[ get( " /auth-requests/<auth_request_id>/response?<code> " ) ]
#[ get( " /auth-requests/<auth_request_id>/response?<code> " ) ]
@ -1843,21 +2333,11 @@ async fn get_auth_request_response(
err ! ( "AuthRequest doesn't exist" , "Invalid device, IP or code" )
err ! ( "AuthRequest doesn't exist" , "Invalid device, IP or code" )
}
}
let response_date_utc = auth_request . response_date . map ( | response_date | format_date ( & response_date ) ) ;
if auth_request . is_expired ( ) {
err ! ( "AuthRequest doesn't exist" , "Request has expired" )
}
Ok ( Json ( json ! ( {
Ok ( Json ( auth_request_json ( & auth_request ) ) )
"id" : & auth_request_id ,
"publicKey" : auth_request . public_key ,
"requestDeviceType" : DeviceType ::from_i32 ( auth_request . device_type ) . to_string ( ) ,
"requestIpAddress" : auth_request . request_ip ,
"key" : auth_request . enc_key ,
"masterPasswordHash" : auth_request . master_password_hash ,
"creationDate" : format_date ( & auth_request . creation_date ) ,
"responseDate" : response_date_utc ,
"requestApproved" : auth_request . approved ,
"origin" : CONFIG . domain_origin ( ) ,
"object" :"auth-request"
} ) ) )
}
}
// Now unused but not yet removed
// Now unused but not yet removed
@ -1874,7 +2354,8 @@ async fn get_auth_requests_pending(headers: Headers, conn: DbConn) -> JsonResult
Ok ( Json ( json ! ( {
Ok ( Json ( json ! ( {
"data" : auth_requests
"data" : auth_requests
. iter ( )
. iter ( )
. filter ( | request | request . approved . is_none ( ) )
// The same set a device answers for itself, see `find_by_user_and_requested_device`.
. filter ( | request | request . approved . is_none ( ) & & ! request . is_admin_approval ( ) & & ! request . is_expired ( ) )
. map ( | request | {
. map ( | request | {
let response_date_utc = request . response_date . map ( | response_date | format_date ( & response_date ) ) ;
let response_date_utc = request . response_date . map ( | response_date | format_date ( & response_date ) ) ;
@ -1910,6 +2391,100 @@ pub async fn purge_auth_requests(pool: DbPool) {
mod tests {
mod tests {
use super ::* ;
use super ::* ;
fn device ( id : & str , trusted : bool ) -> Device {
let mut device = Device ::new ( id . to_owned ( ) . into ( ) , String ::from ( "user" ) . into ( ) , String ::new ( ) , 9 ) ;
if trusted {
device . encrypted_user_key = Some ( String ::from ( "4.b2xkdXNlcmtleQ==" ) ) ;
device . encrypted_public_key = Some ( String ::from ( "2.aXY=|Y2lwaGVy|bWFj" ) ) ;
device . encrypted_private_key = Some ( String ::from ( "2.aXY=|Y2lwaGVy|bWFj" ) ) ;
}
device
}
/// A device with an incomplete set of trust keys cannot unlock the vault.
fn half_trusted ( id : & str ) -> Device {
let mut device = device ( id , true ) ;
device . encrypted_user_key = None ;
device . encrypted_public_key = None ;
device
}
fn update ( device_id : & str ) -> UpdateDeviceKeysData {
UpdateDeviceKeysData {
device_id : device_id . to_owned ( ) . into ( ) ,
encrypted_user_key : String ::from ( "4.bmV3dXNlcmtleQ==" ) ,
encrypted_public_key : String ::from ( "2.aXY=|bmV3|bWFj" ) ,
}
}
/// The ids and keys the rotation would write, so a test can say what it expects in one line.
fn rotated ( result : & [ ( DeviceId , String , String ) ] ) -> Vec < String > {
result . iter ( ) . map ( | ( device_id , user_key , _ ) | format ! ( "{device_id}={user_key}" ) ) . collect ( )
}
/// A rotation only carries an existing trust over to the new user key. Trusting a device is
/// `PUT /devices/<id>/keys`, taken by the device itself once it holds the device key these blobs are
/// wrapped for, so listing any other device gains it nothing and the write that follows clears it.
#[ test ]
fn only_the_devices_trusted_before_are_rotated ( ) {
let devices = [ device ( "a" , true ) , device ( "b" , true ) , device ( "c" , false ) , half_trusted ( "d" ) ] ;
let result = validate_device_keydata ( & [ update ( "a" ) , update ( "b" ) , update ( "c" ) , update ( "d" ) ] , & devices ) . unwrap ( ) ;
assert_eq ! ( rotated ( & result ) , [ "a=4.bmV3dXNlcmtleQ==" , "b=4.bmV3dXNlcmtleQ==" ] , "both re-wrapped" ) ;
// Neither of the others unlocks anything as it stands, so leaving them out loses no trust.
let result = validate_device_keydata ( & [ update ( "a" ) , update ( "b" ) ] , & devices ) . unwrap ( ) ;
assert_eq ! ( rotated ( & result ) , [ "a=4.bmV3dXNlcmtleQ==" , "b=4.bmV3dXNlcmtleQ==" ] ) ;
let devices = [ device ( "c" , false ) , half_trusted ( "d" ) ] ;
assert ! ( validate_device_keydata ( & [ ] , & devices ) . unwrap ( ) . is_empty ( ) ) ;
assert ! ( validate_device_keydata ( & [ update ( "c" ) , update ( "d" ) ] , & devices ) . unwrap ( ) . is_empty ( ) ) ;
}
#[ test ]
fn a_malformed_rotation_is_refused_as_a_whole ( ) {
let devices = [ device ( "a" , true ) , device ( "b" , true ) ] ;
let mut bad_user_key = update ( "a" ) ;
bad_user_key . encrypted_user_key = String ::from ( "not an enc string" ) ;
let mut bad_public_key = update ( "a" ) ;
bad_public_key . encrypted_public_key = String ::new ( ) ;
for ( updates , expected ) in [
// Silently dropping the trust of a device the user still relies on is not the server's call.
( vec ! [ update ( "a" ) ] , "All existing trusted devices must be included" ) ,
( vec ! [ update ( "a" ) , update ( "b" ) , update ( "stranger" ) ] , "does not belong to this user" ) ,
// Two entries for one device means one of the two keys is dropped without anyone noticing which.
( vec ! [ update ( "a" ) , update ( "a" ) , update ( "b" ) ] , "listed more than once" ) ,
( vec ! [ bad_user_key , update ( "b" ) ] , "encryptedUserKey" ) ,
( vec ! [ bad_public_key , update ( "b" ) ] , "encryptedPublicKey" ) ,
] {
let err = validate_device_keydata ( & updates , & devices ) . unwrap_err ( ) ;
assert ! ( format ! ( "{err}" ) . contains ( expected ) , "expected {expected:?}, got {err}" ) ;
}
}
#[ test ]
fn rotation_requires_device_key_unlock_data ( ) {
let mut unlock_data = serde_json ::json ! ( {
"emergencyAccessUnlockData" : [ ] ,
"masterPasswordUnlockData" : {
"kdfType" : 0 ,
"kdfIterations" : 600_000 ,
"email" : "user@example.com" ,
"masterKeyAuthenticationHash" : "hash" ,
"masterKeyEncryptedUserKey" : "key"
} ,
"organizationAccountRecoveryUnlockData" : [ ] ,
"deviceKeyUnlockData" : [ ]
} ) ;
let data : RotateAccountUnlockData = serde_json ::from_value ( unlock_data . clone ( ) ) . unwrap ( ) ;
assert ! ( data . device_key_unlock_data . is_empty ( ) ) ;
unlock_data . as_object_mut ( ) . unwrap ( ) . remove ( "deviceKeyUnlockData" ) ;
assert ! ( serde_json ::from_value ::< RotateAccountUnlockData > ( unlock_data ) . is_err ( ) ) ;
}
#[ test ]
#[ test ]
fn test_register_data_current_format ( ) {
fn test_register_data_current_format ( ) {
// Web and iOS since the master password authentication/unlock split
// Web and iOS since the master password authentication/unlock split