vw_small

Hardened fork of Vaultwarden (https://github.com/dani-garcia/vaultwarden) with fewer features.
git clone https://git.philomathiclife.com/repos/vw_small
Log | Files | Refs | README

accounts.rs (23054B)


      1 use crate::{
      2     api::{EmptyResult, JsonResult, PasswordOrOtpData},
      3     auth::{decode_delete, ClientHeaders, Headers},
      4     db::{
      5         models::{Cipher, Device, Folder, User, UserKdfType, UserOrganization},
      6         DbConn,
      7     },
      8     error::Error,
      9     util::NumberOrString,
     10 };
     11 use rocket::serde::json::Json;
     12 use rocket::{
     13     http::Status,
     14     request::{FromRequest, Outcome, Request},
     15 };
     16 use serde_json::Value;
     17 
     18 pub fn routes() -> Vec<rocket::Route> {
     19     routes![
     20         api_key,
     21         delete_account,
     22         get_auth_request,
     23         get_auth_request_response,
     24         get_auth_requests,
     25         get_known_device,
     26         get_public_keys,
     27         password_hint,
     28         post_auth_request,
     29         post_clear_device_token,
     30         post_delete_account,
     31         post_delete_recover,
     32         post_delete_recover_token,
     33         post_device_token,
     34         post_email,
     35         post_email_token,
     36         post_kdf,
     37         post_keys,
     38         post_password,
     39         post_profile,
     40         post_rotatekey,
     41         post_sstamp,
     42         post_verify_email,
     43         post_verify_email_token,
     44         prelogin,
     45         profile,
     46         put_auth_request,
     47         put_avatar,
     48         put_clear_device_token,
     49         put_device_token,
     50         put_profile,
     51         register,
     52         revision_date,
     53         rotate_api_key,
     54         verify_password,
     55     ]
     56 }
     57 
     58 #[derive(Debug, Deserialize)]
     59 #[serde(rename_all = "camelCase")]
     60 pub struct RegisterData {
     61     #[allow(dead_code)]
     62     email: String,
     63     #[allow(dead_code)]
     64     kdf: Option<i32>,
     65     #[allow(dead_code)]
     66     kdf_iterations: Option<i32>,
     67     #[allow(dead_code)]
     68     kdf_memory: Option<i32>,
     69     #[allow(dead_code)]
     70     kdf_parallelism: Option<i32>,
     71     #[allow(dead_code)]
     72     key: String,
     73     #[allow(dead_code)]
     74     keys: Option<KeysData>,
     75     #[allow(dead_code)]
     76     master_password_hash: String,
     77     #[allow(dead_code)]
     78     master_password_hint: Option<String>,
     79     #[allow(dead_code)]
     80     name: Option<String>,
     81     #[allow(dead_code)]
     82     token: Option<String>,
     83     #[allow(dead_code)]
     84     organization_user_id: Option<String>,
     85 }
     86 
     87 #[derive(Debug, Deserialize)]
     88 #[serde(rename_all = "camelCase")]
     89 struct KeysData {
     90     encrypted_private_key: String,
     91     public_key: String,
     92 }
     93 
     94 /// Trims whitespace from password hints, and converts blank password hints to `None`.
     95 fn clean_password_hint(password_hint: &Option<String>) -> Option<String> {
     96     password_hint.as_ref().and_then(|h| match h.trim() {
     97         "" => None,
     98         ht => Some(ht.to_owned()),
     99     })
    100 }
    101 
    102 fn enforce_password_hint_setting(password_hint: &Option<String>) -> EmptyResult {
    103     if password_hint.is_some() {
    104         err!("Password hints have been disabled by the administrator. Remove the hint and try again.");
    105     }
    106     Ok(())
    107 }
    108 
    109 #[allow(unused_variables, clippy::needless_pass_by_value)]
    110 #[post("/accounts/register", data = "<data>")]
    111 fn register(data: Json<RegisterData>) -> Error {
    112     const MSG: &str = "Registration is permanently disabled.";
    113     Error::new(MSG, MSG)
    114 }
    115 #[get("/accounts/profile")]
    116 async fn profile(headers: Headers, conn: DbConn) -> Json<Value> {
    117     Json(headers.user.to_json(&conn).await)
    118 }
    119 
    120 #[derive(Deserialize)]
    121 #[serde(rename_all = "camelCase")]
    122 struct ProfileData {
    123     // Culture: String, // Ignored, always use en-US
    124     // MasterPasswordHint: Option<String>, // Ignored, has been moved to ChangePassData
    125     name: String,
    126 }
    127 
    128 #[put("/accounts/profile", data = "<data>")]
    129 async fn put_profile(data: Json<ProfileData>, headers: Headers, conn: DbConn) -> JsonResult {
    130     post_profile(data, headers, conn).await
    131 }
    132 
    133 #[post("/accounts/profile", data = "<data>")]
    134 async fn post_profile(data: Json<ProfileData>, headers: Headers, conn: DbConn) -> JsonResult {
    135     let prof_data: ProfileData = data.into_inner();
    136     // Check if the length of the username exceeds 50 characters (Same is Upstream Bitwarden)
    137     // This also prevents issues with very long usernames causing to large JWT's. See #2419
    138     if prof_data.name.len() > 50 {
    139         err!("The field Name must be a string with a maximum length of 50.");
    140     }
    141     let mut user = headers.user;
    142     user.name = prof_data.name;
    143     user.save(&conn).await?;
    144     Ok(Json(user.to_json(&conn).await))
    145 }
    146 
    147 #[derive(Deserialize)]
    148 #[serde(rename_all = "camelCase")]
    149 struct AvatarData {
    150     avatar_color: Option<String>,
    151 }
    152 
    153 #[put("/accounts/avatar", data = "<data>")]
    154 async fn put_avatar(data: Json<AvatarData>, headers: Headers, conn: DbConn) -> JsonResult {
    155     let av_data: AvatarData = data.into_inner();
    156     // It looks like it only supports the 6 hex color format.
    157     // If you try to add the short value it will not show that color.
    158     // Check and force 7 chars, including the #.
    159     if let Some(ref color) = av_data.avatar_color {
    160         if color.len() != 7 {
    161             err!(
    162                 "The field AvatarColor must be a HTML/Hex color code with a length of 7 characters"
    163             )
    164         }
    165     }
    166     let mut user = headers.user;
    167     user.avatar_color = av_data.avatar_color;
    168     user.save(&conn).await?;
    169     Ok(Json(user.to_json(&conn).await))
    170 }
    171 
    172 #[get("/users/<uuid>/public-key")]
    173 async fn get_public_keys(uuid: &str, _headers: Headers, conn: DbConn) -> JsonResult {
    174     let Some(user) = User::find_by_uuid(uuid, &conn).await else {
    175         err!("User doesn't exist")
    176     };
    177     Ok(Json(json!({
    178         "userId": user.uuid,
    179         "publicKey": user.public_key,
    180         "object":"userKey"
    181     })))
    182 }
    183 
    184 #[post("/accounts/keys", data = "<data>")]
    185 async fn post_keys(data: Json<KeysData>, headers: Headers, conn: DbConn) -> JsonResult {
    186     let key_data: KeysData = data.into_inner();
    187     let mut user = headers.user;
    188     user.private_key = Some(key_data.encrypted_private_key);
    189     user.public_key = Some(key_data.public_key);
    190     user.save(&conn).await?;
    191     Ok(Json(json!({
    192         "privateKey": user.private_key,
    193         "publicKey": user.public_key,
    194         "object":"keys"
    195     })))
    196 }
    197 
    198 #[derive(Deserialize)]
    199 #[serde(rename_all = "camelCase")]
    200 struct ChangePassData {
    201     master_password_hash: String,
    202     new_master_password_hash: String,
    203     master_password_hint: Option<String>,
    204     key: String,
    205 }
    206 
    207 #[post("/accounts/password", data = "<data>")]
    208 async fn post_password(data: Json<ChangePassData>, headers: Headers, conn: DbConn) -> EmptyResult {
    209     let pass_data: ChangePassData = data.into_inner();
    210     let mut user = headers.user;
    211     if !user.check_valid_password(&pass_data.master_password_hash) {
    212         err!("Invalid password")
    213     }
    214     user.password_hint = clean_password_hint(&pass_data.master_password_hint);
    215     enforce_password_hint_setting(&user.password_hint)?;
    216     user.set_password(
    217         &pass_data.new_master_password_hash,
    218         Some(pass_data.key),
    219         true,
    220         Some(vec![
    221             String::from("post_rotatekey"),
    222             String::from("get_contacts"),
    223             String::from("get_public_keys"),
    224         ]),
    225     );
    226     user.save(&conn).await
    227 }
    228 
    229 #[derive(Deserialize)]
    230 #[serde(rename_all = "camelCase")]
    231 struct ChangeKdfData {
    232     kdf: i32,
    233     kdf_iterations: u32,
    234     kdf_memory: Option<u32>,
    235     kdf_parallelism: Option<u32>,
    236     master_password_hash: String,
    237     new_master_password_hash: String,
    238     key: String,
    239 }
    240 
    241 #[post("/accounts/kdf", data = "<data>")]
    242 async fn post_kdf(data: Json<ChangeKdfData>, headers: Headers, conn: DbConn) -> EmptyResult {
    243     let kdf_data: ChangeKdfData = data.into_inner();
    244     let mut user = headers.user;
    245     if !user.check_valid_password(&kdf_data.master_password_hash) {
    246         err!("Invalid password")
    247     }
    248     if kdf_data.kdf == i32::from(UserKdfType::Pbkdf2) && kdf_data.kdf_iterations < 100_000u32 {
    249         err!("PBKDF2 KDF iterations must be at least 100000.")
    250     }
    251     if kdf_data.kdf == i32::from(UserKdfType::Argon2id) {
    252         if kdf_data.kdf_iterations < 1u32 {
    253             err!("Argon2 KDF iterations must be at least 1.")
    254         }
    255         if let Some(m) = kdf_data.kdf_memory {
    256             if !(15u32..=1024u32).contains(&m) {
    257                 err!("Argon2 memory must be between 15 MB and 1024 MB.")
    258             }
    259             user.set_client_kdf_memory(kdf_data.kdf_memory);
    260         } else {
    261             err!("Argon2 memory parameter is required.")
    262         }
    263         if let Some(p) = kdf_data.kdf_parallelism {
    264             if !(1u32..=16u32).contains(&p) {
    265                 err!("Argon2 parallelism must be between 1 and 16.")
    266             }
    267             user.set_client_kdf_parallelism(kdf_data.kdf_parallelism);
    268         } else {
    269             err!("Argon2 parallelism parameter is required.")
    270         }
    271     } else {
    272         user.set_client_kdf_memory(None);
    273         user.set_client_kdf_parallelism(None);
    274     }
    275     user.set_client_kdf_iter(kdf_data.kdf_iterations);
    276     user.client_kdf_type = kdf_data.kdf;
    277     user.set_password(
    278         &kdf_data.new_master_password_hash,
    279         Some(kdf_data.key),
    280         true,
    281         None,
    282     );
    283     user.save(&conn).await
    284 }
    285 
    286 #[derive(Deserialize)]
    287 #[serde(rename_all = "camelCase")]
    288 struct UpdateFolderData {
    289     id: Option<String>,
    290     name: String,
    291 }
    292 
    293 #[derive(Deserialize)]
    294 #[serde(rename_all = "camelCase")]
    295 struct UpdateResetPasswordData {
    296     organization_id: String,
    297     reset_password_key: String,
    298 }
    299 
    300 use super::ciphers::CipherData;
    301 
    302 #[derive(Deserialize)]
    303 #[serde(rename_all = "camelCase")]
    304 struct KeyData {
    305     ciphers: Vec<CipherData>,
    306     folders: Vec<UpdateFolderData>,
    307     reset_password_keys: Vec<UpdateResetPasswordData>,
    308     key: String,
    309     master_password_hash: String,
    310     private_key: String,
    311 }
    312 
    313 #[post("/accounts/key", data = "<data>")]
    314 async fn post_rotatekey(data: Json<KeyData>, headers: Headers, conn: DbConn) -> EmptyResult {
    315     let key_data: KeyData = data.into_inner();
    316     if !headers
    317         .user
    318         .check_valid_password(&key_data.master_password_hash)
    319     {
    320         err!("Invalid password")
    321     }
    322     // Validate the import before continuing
    323     // Bitwarden does not process the import if there is one item invalid.
    324     // Since we check for the size of the encrypted note length, we need to do that here to pre-validate it.
    325     // TODO: See if we can optimize the whole cipher adding/importing and prevent duplicate code and checks.
    326     Cipher::validate_cipher_data(&key_data.ciphers)?;
    327     let user_uuid = &headers.user.uuid;
    328     // Update folder data
    329     for folder_data in key_data.folders {
    330         if let Some(folder_id) = folder_data.id {
    331             let Some(mut saved_folder) = Folder::find_by_uuid(&folder_id, &conn).await else {
    332                 err!("Folder doesn't exist")
    333             };
    334             if &saved_folder.user_uuid != user_uuid {
    335                 err!("The folder is not owned by the user")
    336             }
    337 
    338             saved_folder.name = folder_data.name;
    339             saved_folder.save(&conn).await?;
    340         }
    341     }
    342     for reset_password_data in key_data.reset_password_keys {
    343         let Some(mut user_org) = UserOrganization::find_by_user_and_org(
    344             user_uuid,
    345             &reset_password_data.organization_id,
    346             &conn,
    347         )
    348         .await
    349         else {
    350             err!("Reset password doesn't exist")
    351         };
    352         user_org.reset_password_key = Some(reset_password_data.reset_password_key);
    353         user_org.save(&conn).await?;
    354     }
    355     // Update cipher data
    356     use super::ciphers::update_cipher_from_data;
    357     for cipher_data in key_data.ciphers {
    358         if cipher_data.organization_id.is_none() {
    359             let Some(mut saved_cipher) =
    360                 Cipher::find_by_uuid(cipher_data.id.as_ref().unwrap(), &conn).await
    361             else {
    362                 err!("Cipher doesn't exist")
    363             };
    364             if saved_cipher.user_uuid.as_ref().unwrap() != user_uuid {
    365                 err!("The cipher is not owned by the user")
    366             }
    367             update_cipher_from_data(&mut saved_cipher, cipher_data, &headers, None, &conn, true)
    368                 .await?;
    369         }
    370     }
    371 
    372     // Update user data
    373     let mut user = headers.user;
    374     user.akey = key_data.key;
    375     user.private_key = Some(key_data.private_key);
    376     user.reset_security_stamp();
    377     user.save(&conn).await
    378 }
    379 
    380 #[post("/accounts/security-stamp", data = "<data>")]
    381 async fn post_sstamp(data: Json<PasswordOrOtpData>, headers: Headers, conn: DbConn) -> EmptyResult {
    382     let otp_data: PasswordOrOtpData = data.into_inner();
    383     let mut user = headers.user;
    384     otp_data.validate(&user)?;
    385     Device::delete_all_by_user(&user.uuid, &conn).await?;
    386     user.reset_security_stamp();
    387     user.save(&conn).await
    388 }
    389 
    390 #[derive(Deserialize)]
    391 #[serde(rename_all = "camelCase")]
    392 struct EmailTokenData {
    393     #[allow(dead_code)]
    394     master_password_hash: String,
    395     #[allow(dead_code)]
    396     new_email: String,
    397 }
    398 
    399 #[allow(unused_variables, clippy::needless_pass_by_value)]
    400 #[post("/accounts/email-token", data = "<data>")]
    401 fn post_email_token(data: Json<EmailTokenData>, _headers: Headers) -> Error {
    402     const MSG: &str = "E-mail change is not allowed.";
    403     Error::new(MSG, MSG)
    404 }
    405 
    406 #[derive(Deserialize)]
    407 #[serde(rename_all = "camelCase")]
    408 struct ChangeEmailData {
    409     #[allow(dead_code)]
    410     master_password_hash: String,
    411     #[allow(dead_code)]
    412     new_email: String,
    413     #[allow(dead_code)]
    414     key: String,
    415     #[allow(dead_code)]
    416     new_master_password_hash: String,
    417     #[allow(dead_code)]
    418     token: NumberOrString,
    419 }
    420 
    421 #[allow(unused_variables, clippy::needless_pass_by_value)]
    422 #[post("/accounts/email", data = "<data>")]
    423 fn post_email(data: Json<ChangeEmailData>, _headers: Headers) -> Error {
    424     const MSG: &str = "E-mail change is not allowed.";
    425     Error::new(MSG, MSG)
    426 }
    427 
    428 #[allow(clippy::needless_pass_by_value)]
    429 #[post("/accounts/verify-email")]
    430 fn post_verify_email(_headers: Headers) -> Error {
    431     const MSG: &str = "E-mail is disabled.";
    432     Error::new(MSG, MSG)
    433 }
    434 
    435 #[derive(Deserialize)]
    436 #[serde(rename_all = "camelCase")]
    437 struct VerifyEmailTokenData {
    438     #[allow(dead_code)]
    439     user_id: String,
    440     #[allow(dead_code)]
    441     token: String,
    442 }
    443 
    444 #[allow(unused_variables, clippy::needless_pass_by_value)]
    445 #[post("/accounts/verify-email-token", data = "<data>")]
    446 fn post_verify_email_token(data: Json<VerifyEmailTokenData>) -> Error {
    447     const MSG: &str = "E-mail is disabled.";
    448     Error::new(MSG, MSG)
    449 }
    450 
    451 #[derive(Deserialize)]
    452 #[serde(rename_all = "camelCase")]
    453 struct DeleteRecoverData {
    454     #[allow(dead_code)]
    455     email: String,
    456 }
    457 
    458 #[allow(unused_variables, clippy::needless_pass_by_value)]
    459 #[post("/accounts/delete-recover", data = "<data>")]
    460 fn post_delete_recover(data: Json<DeleteRecoverData>) -> Error {
    461     const MSG: &str = "Account deletion is disabled with at this endpoint.";
    462     Error::new(MSG, MSG)
    463 }
    464 
    465 #[derive(Deserialize)]
    466 #[serde(rename_all = "camelCase")]
    467 struct DeleteRecoverTokenData {
    468     user_id: String,
    469     token: String,
    470 }
    471 
    472 #[post("/accounts/delete-recover-token", data = "<data>")]
    473 async fn post_delete_recover_token(
    474     data: Json<DeleteRecoverTokenData>,
    475     conn: DbConn,
    476 ) -> EmptyResult {
    477     let token_data: DeleteRecoverTokenData = data.into_inner();
    478     let Some(user) = User::find_by_uuid(&token_data.user_id, &conn).await else {
    479         err!("User doesn't exist")
    480     };
    481     let Ok(claims) = decode_delete(&token_data.token) else {
    482         err!("Invalid claim")
    483     };
    484     if claims.sub != user.uuid {
    485         err!("Invalid claim");
    486     }
    487     user.delete(&conn).await
    488 }
    489 
    490 #[post("/accounts/delete", data = "<data>")]
    491 async fn post_delete_account(
    492     data: Json<PasswordOrOtpData>,
    493     headers: Headers,
    494     conn: DbConn,
    495 ) -> EmptyResult {
    496     delete_account(data, headers, conn).await
    497 }
    498 
    499 #[delete("/accounts", data = "<data>")]
    500 async fn delete_account(
    501     data: Json<PasswordOrOtpData>,
    502     headers: Headers,
    503     conn: DbConn,
    504 ) -> EmptyResult {
    505     let otp_data: PasswordOrOtpData = data.into_inner();
    506     let user = headers.user;
    507     otp_data.validate(&user)?;
    508     user.delete(&conn).await
    509 }
    510 #[allow(clippy::needless_pass_by_value)]
    511 #[get("/accounts/revision-date")]
    512 fn revision_date(headers: Headers) -> Json<Value> {
    513     Json(json!(headers.user.updated_at.and_utc().timestamp_millis()))
    514 }
    515 
    516 #[derive(Deserialize)]
    517 #[serde(rename_all = "camelCase")]
    518 struct PasswordHintData {
    519     #[allow(dead_code)]
    520     email: String,
    521 }
    522 
    523 #[allow(unused_variables, clippy::needless_pass_by_value)]
    524 #[post("/accounts/password-hint", data = "<data>")]
    525 fn password_hint(data: Json<PasswordHintData>) -> Error {
    526     const MSG: &str = "Password hints are disabled.";
    527     Error::new(MSG, MSG)
    528 }
    529 
    530 #[derive(Deserialize)]
    531 #[serde(rename_all = "camelCase")]
    532 pub struct PreloginData {
    533     email: String,
    534 }
    535 
    536 #[post("/accounts/prelogin", data = "<data>")]
    537 async fn prelogin(data: Json<PreloginData>, conn: DbConn) -> Json<Value> {
    538     _prelogin(data, conn).await
    539 }
    540 
    541 pub async fn _prelogin(data: Json<PreloginData>, conn: DbConn) -> Json<Value> {
    542     let login_data: PreloginData = data.into_inner();
    543     let (kdf_type, kdf_iter, kdf_mem, kdf_para) =
    544         match User::find_by_mail(&login_data.email, &conn).await {
    545             Some(user) => (
    546                 user.client_kdf_type,
    547                 user.client_kdf_iter(),
    548                 user.client_kdf_memory(),
    549                 user.client_kdf_parallelism(),
    550             ),
    551             None => (
    552                 User::client_kdf_type_default(),
    553                 User::CLIENT_KDF_ITER_DEFAULT,
    554                 None,
    555                 None,
    556             ),
    557         };
    558     let result = json!({
    559         "kdf": kdf_type,
    560         "kdfIterations": kdf_iter,
    561         "kdfMemory": kdf_mem,
    562         "kdfParallelism": kdf_para,
    563     });
    564     Json(result)
    565 }
    566 
    567 // https://github.com/bitwarden/server/blob/master/src/Api/Models/Request/Accounts/SecretVerificationRequestModel.cs
    568 #[derive(Deserialize)]
    569 #[serde(rename_all = "camelCase")]
    570 struct SecretVerificationRequest {
    571     master_password_hash: String,
    572 }
    573 
    574 #[post("/accounts/verify-password", data = "<data>")]
    575 fn verify_password(data: Json<SecretVerificationRequest>, headers: Headers) -> EmptyResult {
    576     let req: SecretVerificationRequest = data.into_inner();
    577     let user = headers.user;
    578     if !user.check_valid_password(&req.master_password_hash) {
    579         err!("Invalid password")
    580     }
    581     Ok(())
    582 }
    583 
    584 const API_DISABLED_MSG: &str = "API access is disabled.";
    585 #[allow(unused_variables, clippy::needless_pass_by_value)]
    586 #[post("/accounts/api-key", data = "<data>")]
    587 fn api_key(data: Json<PasswordOrOtpData>, _headers: Headers) -> Error {
    588     Error::new(API_DISABLED_MSG, API_DISABLED_MSG)
    589 }
    590 
    591 #[allow(unused_variables, clippy::needless_pass_by_value)]
    592 #[post("/accounts/rotate-api-key", data = "<data>")]
    593 fn rotate_api_key(data: Json<PasswordOrOtpData>, _headers: Headers) -> Error {
    594     Error::new(API_DISABLED_MSG, API_DISABLED_MSG)
    595 }
    596 
    597 #[get("/devices/knowndevice")]
    598 async fn get_known_device(device: KnownDevice, conn: DbConn) -> JsonResult {
    599     let mut result = false;
    600     if let Some(user) = User::find_by_mail(&device.email, &conn).await {
    601         result = Device::find_by_uuid_and_user(&device.uuid, &user.uuid, &conn)
    602             .await
    603             .is_some();
    604     }
    605     Ok(Json(json!(result)))
    606 }
    607 
    608 struct KnownDevice {
    609     email: String,
    610     uuid: String,
    611 }
    612 
    613 #[rocket::async_trait]
    614 impl<'r> FromRequest<'r> for KnownDevice {
    615     type Error = &'static str;
    616     async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
    617         let email = if let Some(email_b64) = request.headers().get_one("X-Request-Email") {
    618             let Ok(email_bytes) = data_encoding::BASE64URL_NOPAD.decode(email_b64.as_bytes())
    619             else {
    620                 return Outcome::Error((
    621                     Status::BadRequest,
    622                     "X-Request-Email value failed to decode as base64url",
    623                 ));
    624             };
    625             match String::from_utf8(email_bytes) {
    626                 Ok(email) => email,
    627                 Err(_) => {
    628                     return Outcome::Error((
    629                         Status::BadRequest,
    630                         "X-Request-Email value failed to decode as UTF-8",
    631                     ))
    632                 }
    633             }
    634         } else {
    635             return Outcome::Error((Status::BadRequest, "X-Request-Email value is required"));
    636         };
    637 
    638         let uuid = if let Some(uuid) = request.headers().get_one("X-Device-Identifier") {
    639             uuid.to_owned()
    640         } else {
    641             return Outcome::Error((Status::BadRequest, "X-Device-Identifier value is required"));
    642         };
    643         Outcome::Success(Self { email, uuid })
    644     }
    645 }
    646 
    647 #[derive(Deserialize)]
    648 #[serde(rename_all = "camelCase")]
    649 struct PushToken {
    650     #[allow(dead_code)]
    651     push_token: String,
    652 }
    653 
    654 #[allow(unused_variables, clippy::needless_pass_by_value)]
    655 #[post("/devices/identifier/<uuid>/token", data = "<data>")]
    656 fn post_device_token(uuid: &str, data: Json<PushToken>, _headers: Headers) {}
    657 #[allow(unused_variables, clippy::needless_pass_by_value)]
    658 #[put("/devices/identifier/<uuid>/token", data = "<data>")]
    659 fn put_device_token(uuid: &str, data: Json<PushToken>, _headers: Headers) {}
    660 #[allow(unused_variables)]
    661 #[put("/devices/identifier/<uuid>/clear-token")]
    662 const fn put_clear_device_token(uuid: &str) {}
    663 
    664 // On upstream server, both PUT and POST are declared. Implementing the POST method in case it would be useful somewhere
    665 #[allow(unused_variables)]
    666 #[post("/devices/identifier/<uuid>/clear-token")]
    667 const fn post_clear_device_token(uuid: &str) {}
    668 
    669 #[derive(Deserialize)]
    670 #[serde(rename_all = "camelCase")]
    671 struct AuthRequestRequest {
    672     #[allow(dead_code)]
    673     access_code: String,
    674     #[allow(dead_code)]
    675     device_identifier: String,
    676     #[allow(dead_code)]
    677     email: String,
    678     #[allow(dead_code)]
    679     public_key: String,
    680     #[serde(alias = "type")]
    681     _type: i32,
    682 }
    683 const AUTH_DISABLED_MSG: &str = "Auth requests and log in via device are disabled.";
    684 #[allow(unused_variables, clippy::needless_pass_by_value)]
    685 #[post("/auth-requests", data = "<data>")]
    686 fn post_auth_request(data: Json<AuthRequestRequest>, _headers: ClientHeaders) -> Error {
    687     Error::new(AUTH_DISABLED_MSG, AUTH_DISABLED_MSG)
    688 }
    689 
    690 #[allow(unused_variables)]
    691 #[get("/auth-requests/<uuid>")]
    692 fn get_auth_request(uuid: &str) -> Error {
    693     Error::new(AUTH_DISABLED_MSG, AUTH_DISABLED_MSG)
    694 }
    695 
    696 #[derive(Deserialize)]
    697 #[serde(rename_all = "camelCase")]
    698 struct AuthResponseRequest {
    699     #[allow(dead_code)]
    700     device_identifier: String,
    701     #[allow(dead_code)]
    702     key: String,
    703     #[allow(dead_code)]
    704     master_password_hash: Option<String>,
    705     #[allow(dead_code)]
    706     request_approved: bool,
    707 }
    708 
    709 #[allow(unused_variables, clippy::needless_pass_by_value)]
    710 #[put("/auth-requests/<uuid>", data = "<data>")]
    711 fn put_auth_request(uuid: &str, data: Json<AuthResponseRequest>) -> Error {
    712     Error::new(AUTH_DISABLED_MSG, AUTH_DISABLED_MSG)
    713 }
    714 
    715 #[allow(unused_variables)]
    716 #[get("/auth-requests/<uuid>/response?<code>")]
    717 fn get_auth_request_response(uuid: &str, code: &str) -> Error {
    718     Error::new(AUTH_DISABLED_MSG, AUTH_DISABLED_MSG)
    719 }
    720 
    721 #[allow(unused_variables, clippy::needless_pass_by_value)]
    722 #[get("/auth-requests")]
    723 fn get_auth_requests(headers: Headers) -> Json<Value> {
    724     Json(json!({
    725         "data": Vec::<Value>::new(),
    726         "continuationToken": null,
    727         "object": "list"
    728     }))
    729 }