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

commit 0dca43f496f1998dec0e9c68f38966beb671bd81
parent a6bc308abfadeab7f0523a4ab96fb0b42cdb71b7
Author: Zack Newman <zack@philomathiclife.com>
Date:   Thu,  3 Apr 2025 13:44:10 -0600

update webauthn, msrv, rust 2024, fix lints

Diffstat:
MCargo.toml | 36++++++++++++++++++------------------
Msrc/api/core/accounts.rs | 20+++++++++++---------
Msrc/api/core/ciphers.rs | 18+++++++++++-------
Msrc/api/core/organizations.rs | 30+++++++++++++++++++-----------
Msrc/api/core/two_factor/email.rs | 28++++++++++++++--------------
Msrc/api/core/two_factor/protected_actions.rs | 5++---
Msrc/api/core/two_factor/webauthn.rs | 66+++++++++++++++++++++++++++++++++---------------------------------
Msrc/api/identity.rs | 53+++++++++++++++++++++++++++--------------------------
Msrc/auth.rs | 38++++++++++++++++++++------------------
Msrc/config.rs | 12+++++++-----
Msrc/crypto.rs | 10+---------
Msrc/db/mod.rs | 19++++++++++---------
Msrc/db/models/cipher.rs | 9++++++---
Msrc/db/models/collection.rs | 2+-
Msrc/db/models/device.rs | 4++--
Msrc/db/models/favorite.rs | 2+-
Msrc/db/models/folder.rs | 2+-
Msrc/db/models/org_policy.rs | 6+++---
Msrc/db/models/organization.rs | 2+-
Msrc/db/models/two_factor.rs | 59+++++++++++++++++++++++++++++++++++------------------------
Msrc/db/models/user.rs | 23+++++------------------
Msrc/error.rs | 11++---------
Msrc/main.rs | 24+++++++++++++-----------
Msrc/priv_sep.rs | 2+-
24 files changed, 244 insertions(+), 237 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml @@ -3,39 +3,39 @@ authors = ["Daniel GarcĂ­a <dani-garcia@users.noreply.github.com>", "Zack Newman categories = ["api-bindings", "web-programming::http-server"] description = "Hardened fork of Vaultwarden with fewer features and pledge(2) and unveil(2) support." documentation = "https://github.com/dani-garcia/vaultwarden/wiki" -edition = "2021" +edition = "2024" keywords = ["password", "vaultwarden"] license = "AGPL-3.0-only" name = "vw_small" publish = false readme = "README.md" repository = "https://git.philomathiclife.com/repos/vw_small/" -rust-version = "1.82.0" -version = "3.0.0" +rust-version = "1.86.0" +version = "3.1.0" [target.'cfg(target_os = "openbsd")'.dependencies] priv_sep = { version = "2.1.0", default-features = false, features = ["openbsd"], optional = true } [dependencies] -chrono = { version = "0.4.38", default-features = false, features = ["serde"] } -data-encoding = { version = "2.6.0", default-features = false } -diesel = { version = "2.2.6", default-features = false, features = ["32-column-tables", "chrono", "r2d2", "sqlite"] } -jsonwebtoken = { version = "9.3.0", default-features = false, features = ["use_pem"] } -libsqlite3-sys = { version = "0.30.1", default-features = false, features = ["bundled"] } -openssl = { version = "0.10.68", default-features = false } +chrono = { version = "0.4.40", default-features = false, features = ["serde"] } +data-encoding = { version = "2.8.0", default-features = false } +diesel = { version = "2.2.8", default-features = false, features = ["32-column-tables", "chrono", "r2d2", "sqlite"] } +jsonwebtoken = { version = "9.3.1", default-features = false, features = ["use_pem"] } +libsqlite3-sys = { version = "0.31.0", default-features = false, features = ["bundled"] } +openssl = { version = "0.10.71", default-features = false } paste = { version = "1.0.15", default-features = false } -rand = { version = "0.8.5", default-features = false, features = ["small_rng"] } -ring = { version = "0.17.8", default-features = false } +rand = { version = "0.9.0", default-features = false, features = ["small_rng"] } +ring = { version = "0.17.14", default-features = false } rocket = { version = "0.5.1", default-features = false, features = ["json", "tls"] } -semver = { version = "1.0.23", default-features = false } -serde = { version = "1.0.215", default-features = false } -serde_json = { version = "1.0.133", default-features = false } -tokio = { version = "1.42.0", default-features = false } -toml = { version = "0.8.19", default-features = false, features = ["parse"] } +semver = { version = "1.0.26", default-features = false } +serde = { version = "1.0.219", default-features = false } +serde_json = { version = "1.0.140", default-features = false } +tokio = { version = "1.44.1", default-features = false } +toml = { version = "0.8.20", default-features = false, features = ["parse"] } totp-lite = { version = "2.0.1", default-features = false } url = { version = "2.5.4", default-features = false } -uuid = { version = "1.11.0", default-features = false, features = ["v4"] } -webauthn_rp = { version = "0.2.0", features = ["bin", "custom", "serde_relaxed"] } +uuid = { version = "1.16.0", default-features = false, features = ["v4"] } +webauthn_rp = { version = "0.3.0", features = ["bin", "custom", "serde_relaxed"] } [features] priv_sep = ["dep:priv_sep"] diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs @@ -1,9 +1,9 @@ use crate::{ api::{EmptyResult, JsonResult, PasswordOrOtpData}, - auth::{decode_delete, ClientHeaders, Headers}, + auth::{ClientHeaders, Headers, decode_delete}, db::{ - models::{Cipher, Device, Folder, User, UserKdfType, UserOrganization}, DbConn, + models::{Cipher, Device, Folder, User, UserKdfType, UserOrganization}, }, error::Error, util::NumberOrString, @@ -92,16 +92,18 @@ struct KeysData { } /// Trims whitespace from password hints, and converts blank password hints to `None`. -fn clean_password_hint(password_hint: &Option<String>) -> Option<String> { - password_hint.as_ref().and_then(|h| match h.trim() { +fn clean_password_hint(password_hint: Option<&str>) -> Option<String> { + password_hint.and_then(|h| match h.trim() { "" => None, ht => Some(ht.to_owned()), }) } -fn enforce_password_hint_setting(password_hint: &Option<String>) -> EmptyResult { +fn enforce_password_hint_setting(password_hint: Option<&str>) -> EmptyResult { if password_hint.is_some() { - err!("Password hints have been disabled by the administrator. Remove the hint and try again."); + err!( + "Password hints have been disabled by the administrator. Remove the hint and try again." + ); } Ok(()) } @@ -211,8 +213,8 @@ async fn post_password(data: Json<ChangePassData>, headers: Headers, conn: DbCon if !user.check_valid_password(&pass_data.master_password_hash) { err!("Invalid password") } - user.password_hint = clean_password_hint(&pass_data.master_password_hint); - enforce_password_hint_setting(&user.password_hint)?; + user.password_hint = clean_password_hint(pass_data.master_password_hint.as_deref()); + enforce_password_hint_setting(user.password_hint.as_deref())?; user.set_password( &pass_data.new_master_password_hash, Some(pass_data.key), @@ -628,7 +630,7 @@ impl<'r> FromRequest<'r> for KnownDevice { return Outcome::Error(( Status::BadRequest, "X-Request-Email value failed to decode as UTF-8", - )) + )); } } } else { diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs @@ -3,21 +3,21 @@ use crate::{ api::{self, EmptyResult, JsonResult, PasswordOrOtpData}, auth::Headers, db::{ + DbConn, models::{ Cipher, Collection, CollectionCipher, CollectionUser, Favorite, Folder, FolderCipher, OrgPolicy, OrgPolicyType, UserOrgType, UserOrganization, }, - DbConn, }, error::Error, util::NumberOrString, }; use chrono::{NaiveDateTime, Utc}; use rocket::{ + Route, form::{Form, FromForm}, fs::TempFile, serde::json::Json, - Route, }; use serde_json::Value; use std::collections::{HashMap, HashSet}; @@ -333,7 +333,9 @@ async fn enforce_personal_ownership_policy( let user_uuid = &headers.user.uuid; let policy_type = OrgPolicyType::PersonalOwnership; if OrgPolicy::is_applicable_to_user(user_uuid, policy_type, None, conn).await { - err!("Due to an Enterprise Policy, you are restricted from saving items to your personal vault.") + err!( + "Due to an Enterprise Policy, you are restricted from saving items to your personal vault." + ) } } Ok(()) @@ -356,7 +358,9 @@ pub async fn update_cipher_from_data( // ISO 8601 format Err(err) => warn!("Error parsing LastKnownRevisionDate '{}': {}", dt, err), Ok(dt) if cipher.updated_at.signed_duration_since(dt).num_seconds() > 1 => { - err!("The client copy of this cipher is out of date. Resync the client and try again.") + err!( + "The client copy of this cipher is out of date. Resync the client and try again." + ) } Ok(_) => (), } @@ -419,7 +423,7 @@ pub async fn update_cipher_from_data( .for_each(|ref mut f| { f.as_object_mut().unwrap().remove("Response"); }); - }; + } json_data } let type_data_opt = match data.r#type { @@ -887,7 +891,7 @@ async fn share_cipher_by_uuid( } } } - }; + } update_cipher_from_data( &mut cipher, data.cipher, @@ -1263,7 +1267,7 @@ async fn _delete_multiple_ciphers( for uuid in data.ids { if let error @ Err(_) = _delete_cipher_by_uuid(&uuid, &headers, &conn, soft_delete).await { return error; - }; + } } Ok(()) } diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs @@ -1,22 +1,22 @@ use crate::{ api::{ - core::{CipherSyncData, CipherSyncType}, EmptyResult, JsonResult, PasswordOrOtpData, + core::{CipherSyncData, CipherSyncType}, }, auth::{self, AdminHeaders, Headers, ManagerHeaders, ManagerHeadersLoose, OwnerHeaders}, db::{ + DbConn, models::{ Cipher, Collection, CollectionCipher, CollectionUser, OrgPolicy, OrgPolicyErr, OrgPolicyType, Organization, TwoFactorType, User, UserOrgStatus, UserOrgType, UserOrganization, }, - DbConn, }, error::Error, util::{self, NumberOrString}, }; use core::convert; -use rocket::{http::Status, serde::json::Json, Route}; +use rocket::{Route, http::Status, serde::json::Json}; use serde_json::Value; use std::collections::{HashMap, HashSet}; @@ -926,10 +926,14 @@ async fn edit_user( match OrgPolicy::is_user_allowed(&user_to_edit.user_uuid, org_id, true, &conn).await { Ok(()) => {} Err(OrgPolicyErr::TwoFactorMissing) => { - err!("You cannot modify this user to this type because it has no two-step login method activated"); + err!( + "You cannot modify this user to this type because it has no two-step login method activated" + ); } Err(OrgPolicyErr::SingleOrgEnforced) => { - err!("You cannot modify this user to this type because it is a member of an organization which forbids it"); + err!( + "You cannot modify this user to this type because it is a member of an organization which forbids it" + ); } } } @@ -1078,8 +1082,8 @@ async fn bulk_public_keys( })) } -use super::ciphers::update_cipher_from_data; use super::ciphers::CipherData; +use super::ciphers::update_cipher_from_data; #[derive(Deserialize)] #[serde(rename_all = "camelCase")] @@ -1225,7 +1229,7 @@ async fn post_bulk_collections( CollectionCipher::save(&cipher.uuid, collection, &conn).await?; } } - }; + } } Ok(()) @@ -1446,7 +1450,7 @@ async fn import( Some(user_org) if user_org.atype >= UserOrgType::Admin => { /* Okay, nothing to do */ } Some(_) => err!("User has insufficient permissions to use Directory Connector"), None => err!("User not part of organization"), - }; + } for user_data in &data.users { if user_data.deleted { // If user is marked for deletion and it exists, delete it @@ -1676,10 +1680,14 @@ async fn _restore_organization_user( match OrgPolicy::is_user_allowed(&user_org.user_uuid, org_id, false, conn).await { Ok(()) => {} Err(OrgPolicyErr::TwoFactorMissing) => { - err!("You cannot restore this user because it has no two-step login method activated"); + err!( + "You cannot restore this user because it has no two-step login method activated" + ); } Err(OrgPolicyErr::SingleOrgEnforced) => { - err!("You cannot restore this user because it is a member of an organization which forbids it"); + err!( + "You cannot restore this user because it is a member of an organization which forbids it" + ); } } } @@ -1946,7 +1954,7 @@ async fn get_org_export(org_id: &str, headers: AdminHeaders, conn: DbConn) -> Js // Therefore, we will check for any version smaller then v2023.1.0 and return a different response. // If we can't determine the version, we will use the latest default v2023.1.0 and higher. // https://github.com/bitwarden/server/blob/9ca93381ce416454734418c3a9f99ab49747f1b6/src/Api/Controllers/OrganizationExportController.cs#L44 - let use_list_response_model = headers.client_version.map_or(false, |client_version| { + let use_list_response_model = headers.client_version.is_some_and(|client_version| { let ver_match = VersionReq::parse("<2023.1.0").unwrap(); let client_version = Version::parse(&client_version).unwrap(); ver_match.matches(&client_version) diff --git a/src/api/core/two_factor/email.rs b/src/api/core/two_factor/email.rs @@ -1,5 +1,5 @@ use crate::{api::PasswordOrOtpData, auth::Headers, error::Error}; -use rocket::{serde::json::Json, Route}; +use rocket::{Route, serde::json::Json}; pub fn routes() -> Vec<Route> { routes![email, get_email, send_email, send_email_login] @@ -8,21 +8,21 @@ pub fn routes() -> Vec<Route> { #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct SendEmailLoginData { - #[allow(dead_code)] + #[expect(dead_code, reason = "upstream")] #[serde(alias = "Email")] email: String, - #[allow(dead_code)] + #[expect(dead_code, reason = "upstream")] #[serde(alias = "MasterPasswordHash")] master_password_hash: String, } const EMAIL_DISABLED_MSG: &str = "E-mail is disabled."; -#[allow(unused_variables, clippy::needless_pass_by_value)] +#[expect(unused_variables, clippy::needless_pass_by_value, reason = "upstream")] #[post("/two-factor/send-email-login", data = "<data>")] fn send_email_login(data: Json<SendEmailLoginData>) -> Error { Error::new(EMAIL_DISABLED_MSG, EMAIL_DISABLED_MSG) } -#[allow(unused_variables, clippy::needless_pass_by_value)] +#[expect(unused_variables, clippy::needless_pass_by_value, reason = "upstream")] #[post("/two-factor/get-email", data = "<data>")] fn get_email(data: Json<PasswordOrOtpData>, _headers: Headers) -> Error { Error::new(EMAIL_DISABLED_MSG, EMAIL_DISABLED_MSG) @@ -31,15 +31,15 @@ fn get_email(data: Json<PasswordOrOtpData>, _headers: Headers) -> Error { #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct SendEmailData { - #[allow(dead_code)] + #[expect(dead_code, reason = "upstream")] email: String, - #[allow(dead_code)] + #[expect(dead_code, reason = "upstream")] master_password_hash: Option<String>, - #[allow(dead_code)] + #[expect(dead_code, reason = "upstream")] otp: Option<String>, } -#[allow(unused_variables, clippy::needless_pass_by_value)] +#[expect(unused_variables, clippy::needless_pass_by_value, reason = "upstream")] #[post("/two-factor/send-email", data = "<data>")] fn send_email(data: Json<SendEmailData>, _headers: Headers) -> Error { Error::new(EMAIL_DISABLED_MSG, EMAIL_DISABLED_MSG) @@ -48,16 +48,16 @@ fn send_email(data: Json<SendEmailData>, _headers: Headers) -> Error { #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct EmailData { - #[allow(dead_code)] + #[expect(dead_code, reason = "upstream")] email: String, - #[allow(dead_code)] + #[expect(dead_code, reason = "upstream")] token: String, - #[allow(dead_code)] + #[expect(dead_code, reason = "upstream")] master_password_hash: Option<String>, - #[allow(dead_code)] + #[expect(dead_code, reason = "upstream")] otp: Option<String>, } -#[allow(unused_variables, clippy::needless_pass_by_value)] +#[expect(unused_variables, clippy::needless_pass_by_value, reason = "upstream")] #[put("/two-factor/email", data = "<data>")] fn email(data: Json<EmailData>, _headers: Headers) -> Error { Error::new(EMAIL_DISABLED_MSG, EMAIL_DISABLED_MSG) diff --git a/src/api/core/two_factor/protected_actions.rs b/src/api/core/two_factor/protected_actions.rs @@ -1,11 +1,10 @@ use crate::{auth::Headers, error::Error}; -use rocket::{serde::json::Json, Route}; +use rocket::{Route, serde::json::Json}; pub fn routes() -> Vec<Route> { routes![request_otp, verify_otp] } const DEVICE_LOG_IN_MSG: &str = "Log in via device is disabled."; -#[allow(clippy::needless_pass_by_value)] #[post("/accounts/request-otp")] fn request_otp(_headers: Headers) -> Error { Error::new(DEVICE_LOG_IN_MSG, DEVICE_LOG_IN_MSG) @@ -17,7 +16,7 @@ struct ProtectedActionVerify { otp: String, } -#[allow(unused_variables, clippy::needless_pass_by_value)] +#[expect(unused, clippy::needless_pass_by_value, reason = "upstream")] #[post("/accounts/verify-otp", data = "<data>")] fn verify_otp(data: Json<ProtectedActionVerify>, _headers: Headers) -> Error { Error::new(DEVICE_LOG_IN_MSG, DEVICE_LOG_IN_MSG) diff --git a/src/api/core/two_factor/webauthn.rs b/src/api/core/two_factor/webauthn.rs @@ -3,32 +3,32 @@ use crate::{ auth::{self, Headers}, config, db::{ - models::{WebAuthn, WebAuthnInfo}, DbConn, + models::{WebAuthn, WebAuthnInfo}, }, error::Error, }; use data_encoding::{BASE64, BASE64URL_NOPAD}; -use rocket::serde::json::Json; use rocket::Route; +use rocket::serde::json::Json; use serde::de::{Deserialize, Deserializer, Error as SerdeErr, MapAccess, Unexpected, Visitor}; use std::fmt::{self, Formatter}; use uuid::Uuid; use webauthn_rp::{ + AggErr, request::{ - auth::PublicKeyCredentialRequestOptions, + InsertResult, + auth::NonDiscoverableCredentialRequestOptions, register::{ Nickname, PublicKeyCredentialCreationOptions, PublicKeyCredentialUserEntity, - UserHandle, Username, + UserHandle16, Username, }, - InsertResult, }, response::{ - auth::Authentication, - register::{AuthenticatorAttestation, Registration}, AuthTransports, AuthenticatorAttachment, CollectedClientData, CredentialId, + auth::NonDiscoverableAuthentication16, + register::{AuthenticatorAttestation, Registration}, }, - AggErr, }; pub fn routes() -> Vec<Route> { @@ -65,13 +65,13 @@ async fn generate_webauthn_challenge( let uuid = Uuid::parse_str(user.uuid.as_str()) .map_err(|e| Error::from(e.to_string()))? .into_bytes(); - let id = UserHandle::try_from(uuid.as_slice()).map_err(AggErr::UserHandle)?; + let id = UserHandle16::from(uuid); let display_name = Some(Nickname::try_from(user.name.as_str()).map_err(AggErr::Nickname)?); let (server, client) = PublicKeyCredentialCreationOptions::second_factor( &config::get_config().rp_id, PublicKeyCredentialUserEntity { name, - id, + id: &id, display_name, }, WebAuthn::get_registered_creds(user.uuid.as_str(), &conn).await?, @@ -98,7 +98,7 @@ impl<'e> Deserialize<'e> for Type { D: Deserializer<'e>, { struct TypeVisitor; - impl<'f> Visitor<'f> for TypeVisitor { + impl Visitor<'_> for TypeVisitor { type Value = Type; fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { formatter.write_str("public-key") @@ -146,7 +146,7 @@ impl<'de> Deserialize<'de> for RegistrationWrapper { D: Deserializer<'e>, { struct FieldVisitor; - impl<'f> Visitor<'f> for FieldVisitor { + impl Visitor<'_> for FieldVisitor { type Value = Field; fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { write!( @@ -178,7 +178,7 @@ impl<'de> Deserialize<'de> for RegistrationWrapper { D: Deserializer<'e>, { struct RawIdVisitor; - impl<'f> Visitor<'f> for RawIdVisitor { + impl Visitor<'_> for RawIdVisitor { type Value = RawId; fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { formatter.write_str("RawId") @@ -230,14 +230,17 @@ impl<'de> Deserialize<'de> for RegistrationWrapper { D: Deserializer<'g>, { struct AuthFieldVisitor; - impl<'h> Visitor<'h> for AuthFieldVisitor { + impl Visitor<'_> for AuthFieldVisitor { type Value = AuthField; fn expecting( &self, formatter: &mut Formatter<'_>, ) -> fmt::Result { - write!(formatter, "'{ATTESTATION_OBJECT}' or '{CLIENT_DATA_JSON}'") + write!( + formatter, + "'{ATTESTATION_OBJECT}' or '{CLIENT_DATA_JSON}'" + ) } fn visit_str<E>(self, v: &str) -> Result<Self::Value, E> where @@ -264,7 +267,7 @@ impl<'de> Deserialize<'de> for RegistrationWrapper { D: Deserializer<'g>, { struct Base64Visitor; - impl<'h> Visitor<'h> for Base64Visitor { + impl Visitor<'_> for Base64Visitor { type Value = Base64; fn expecting( &self, @@ -444,10 +447,6 @@ async fn activate_webauthn( otp: None, } .validate(&user)?; - let uuid = Uuid::parse_str(user.uuid.as_str()) - .map_err(|e| Error::from(e.to_string()))? - .into_bytes(); - let user_handle = UserHandle::try_from(uuid.as_slice()).map_err(AggErr::UserHandle)?; let cred = auth::get_reg_ceremonies() .take( &CollectedClientData::from_client_data_json_relaxed::<true>( @@ -459,7 +458,6 @@ async fn activate_webauthn( .ok_or_else(|| Error::from(String::from("missing registration challenge")))? .verify( &config::get_config().rp_id, - user_handle, &data.device_response.0, auth::get_reg_options(), ) @@ -511,7 +509,7 @@ async fn delete_webauthn(data: Json<DeleteU2FData>, headers: Headers, conn: DbCo pub async fn generate_webauthn_login(user_uuid: &str, conn: &DbConn) -> JsonResult { let creds = WebAuthn::get_allowed_creds(user_uuid, conn).await?; let (server, client) = - PublicKeyCredentialRequestOptions::second_factor(&config::get_config().rp_id, creds) + NonDiscoverableCredentialRequestOptions::second_factor(&config::get_config().rp_id, creds) .map_err(AggErr::SecondFactor)? .start_ceremony() .map_err(AggErr::RequestOptions)?; @@ -523,7 +521,7 @@ pub async fn generate_webauthn_login(user_uuid: &str, conn: &DbConn) -> JsonResu InsertResult::Duplicate => Err(Error::from(String::from("duplicate webauthn challenge"))), } } -struct AuthenticationWrapper(Authentication); +struct AuthenticationWrapper(NonDiscoverableAuthentication16); impl<'de> Deserialize<'de> for AuthenticationWrapper { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where @@ -552,7 +550,7 @@ impl<'de> Deserialize<'de> for AuthenticationWrapper { D: Deserializer<'e>, { struct FieldVisitor; - impl<'f> Visitor<'f> for FieldVisitor { + impl Visitor<'_> for FieldVisitor { type Value = Field; fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { write!( @@ -600,7 +598,7 @@ impl<'de> Deserialize<'de> for AuthenticationWrapper { D: Deserializer<'g>, { struct FieldVisitor; - impl<'h> Visitor<'h> for FieldVisitor { + impl Visitor<'_> for FieldVisitor { type Value = Field; fn expecting( &self, @@ -700,11 +698,13 @@ impl<'de> Deserialize<'de> for AuthenticationWrapper { .and_then(|resp| { if extensions { if typ { - Ok(AuthenticationWrapper(Authentication::new( - r, - resp, - AuthenticatorAttachment::None, - ))) + Ok(AuthenticationWrapper( + NonDiscoverableAuthentication16::new( + r, + resp, + AuthenticatorAttachment::None, + ), + )) } else { Err(SerdeErr::missing_field(TYPE)) } @@ -740,8 +740,8 @@ pub async fn validate_webauthn_login( let uuid = Uuid::parse_str(user_uuid) .map_err(|e| Error::from(e.to_string()))? .into_bytes(); - let user_handle = UserHandle::try_from(uuid.as_slice()).map_err(AggErr::UserHandle)?; - let mut cred = WebAuthn::get_credential(auth.raw_id(), user_uuid, user_handle, conn) + let user_handle = UserHandle16::from(uuid); + let mut cred = WebAuthn::get_credential(auth.raw_id(), user_uuid, &user_handle, conn) .await? .ok_or_else(|| Error::from(String::from("credential does not exist")))?; if auth::get_auth_ceremonies() @@ -757,7 +757,7 @@ pub async fn validate_webauthn_login( &config::get_config().rp_id, &auth, &mut cred, - &auth::get_auth_options(), + auth::get_auth_options(), ) .map_err(AggErr::AuthCeremony)? { diff --git a/src/api/identity.rs b/src/api/identity.rs @@ -1,21 +1,21 @@ use crate::{ api::{ - core::accounts::{PreloginData, RegisterData, _prelogin}, ApiResult, EmptyResult, JsonResult, + core::accounts::{_prelogin, PreloginData, RegisterData}, }, auth::{ClientHeaders, ClientIp}, config, db::{ - models::{Device, OrgPolicy, OrgPolicyType, TwoFactorType, User}, DbConn, + models::{Device, OrgPolicy, OrgPolicyType, TwoFactorType, User}, }, - error::{Error, MapResult}, + error::{Error, MapResult as _}, util, }; use rocket::serde::json::Json; use rocket::{ - form::{Form, FromForm}, Route, + form::{Form, FromForm}, }; use serde_json::Value; @@ -29,17 +29,20 @@ async fn login(data: Form<ConnectData>, client_header: ClientHeaders, conn: DbCo let mut user_uuid: Option<String> = None; let login_result = match data.grant_type.as_ref() { "refresh_token" => { - _check_is_some(&data.refresh_token, "refresh_token cannot be blank")?; + _check_is_some(data.refresh_token.as_ref(), "refresh_token cannot be blank")?; _refresh_login(data, &conn).await } "password" => { - _check_is_some(&data.client_id, "client_id cannot be blank")?; - _check_is_some(&data.password, "password cannot be blank")?; - _check_is_some(&data.scope, "scope cannot be blank")?; - _check_is_some(&data.username, "username cannot be blank")?; - _check_is_some(&data.device_identifier, "device_identifier cannot be blank")?; - _check_is_some(&data.device_name, "device_name cannot be blank")?; - _check_is_some(&data.device_type, "device_type cannot be blank")?; + _check_is_some(data.client_id.as_ref(), "client_id cannot be blank")?; + _check_is_some(data.password.as_ref(), "password cannot be blank")?; + _check_is_some(data.scope.as_ref(), "scope cannot be blank")?; + _check_is_some(data.username.as_ref(), "username cannot be blank")?; + _check_is_some( + data.device_identifier.as_ref(), + "device_identifier cannot be blank", + )?; + _check_is_some(data.device_name.as_ref(), "device_name cannot be blank")?; + _check_is_some(data.device_type.as_ref(), "device_type cannot be blank")?; _password_login(data, &mut user_uuid, &conn, &client_header.ip).await } t => err!("Invalid type", t), @@ -252,23 +255,21 @@ async fn twofactor_auth( let tf_type = data .two_factor_provider .map(|prov| { - TwoFactorType::try_from(prov) - .map_err(Error::from) - .and_then(|tf| { - if matches!(tf, TwoFactorType::WebAuthn) { - if authn { - Ok(tf) - } else { - const MSG: &str = "no webauthn registrations"; - Err(Error::new(MSG, MSG)) - } - } else if totp_token.is_some() { + TwoFactorType::try_from(prov).and_then(|tf| { + if matches!(tf, TwoFactorType::WebAuthn) { + if authn { Ok(tf) } else { - const MSG: &str = "no totp registrations"; + const MSG: &str = "no webauthn registrations"; Err(Error::new(MSG, MSG)) } - }) + } else if totp_token.is_some() { + Ok(tf) + } else { + const MSG: &str = "no totp registrations"; + Err(Error::new(MSG, MSG)) + } + }) }) .transpose()? .unwrap_or({ @@ -394,7 +395,7 @@ struct ConnectData { auth_request: Option<String>, } -fn _check_is_some<T>(value: &Option<T>, msg: &str) -> EmptyResult { +fn _check_is_some<T>(value: Option<&T>, msg: &str) -> EmptyResult { if value.is_none() { err!(msg) } diff --git a/src/auth.rs b/src/auth.rs @@ -3,20 +3,20 @@ use crate::{ error::Error, }; use chrono::{TimeDelta, Utc}; -use jsonwebtoken::{self, errors::ErrorKind, Algorithm, DecodingKey, EncodingKey, Header}; +use jsonwebtoken::{self, Algorithm, DecodingKey, EncodingKey, Header, errors::ErrorKind}; use openssl::pkey::{Id, PKey}; use serde::de::DeserializeOwned; use serde::ser::Serialize; use std::fs::File; -use std::io::{Read, Write}; +use std::io::{Read as _, Write as _}; use std::sync::{Arc, Mutex, MutexGuard, OnceLock}; use webauthn_rp::request::{ + BackupReq, BuildIdentityHasher, FixedCapHashSet, auth::{ - AuthenticationServerState, AuthenticationVerificationOptions, - AuthenticatorAttachmentEnforcement, SignatureCounterEnforcement, + AuthenticationVerificationOptions, AuthenticatorAttachmentEnforcement, + NonDiscoverableAuthenticationServerState, SignatureCounterEnforcement, }, register::{RegistrationServerState, RegistrationVerificationOptions}, - BackupReq, BuildIdentityHasher, FixedCapHashSet, }; static ALLOWED_ORIGINS: OnceLock<[&str; 1]> = OnceLock::new(); #[inline] @@ -33,7 +33,7 @@ fn get_allowed_origins() -> &'static [&'static str] { .as_slice() } static REG_CEREMONIES: OnceLock< - Arc<Mutex<FixedCapHashSet<RegistrationServerState, BuildIdentityHasher>>>, + Arc<Mutex<FixedCapHashSet<RegistrationServerState<16>, BuildIdentityHasher>>>, > = OnceLock::new(); #[inline] fn init_reg_ceremonies() { @@ -42,8 +42,8 @@ fn init_reg_ceremonies() { .expect("REG_CEREMONIES must only be initialized once"); } #[inline] -pub fn get_reg_ceremonies( -) -> MutexGuard<'static, FixedCapHashSet<RegistrationServerState, BuildIdentityHasher>> { +pub fn get_reg_ceremonies() +-> MutexGuard<'static, FixedCapHashSet<RegistrationServerState<16>, BuildIdentityHasher>> { REG_CEREMONIES .get() .expect("REG_CEREMONIES must be initialized in main") @@ -66,14 +66,14 @@ fn init_reg_options() { .expect("REG_OPTIONS must only be initialized once"); } #[inline] -pub fn get_reg_options( -) -> &'static RegistrationVerificationOptions<'static, 'static, &'static str, &'static str> { +pub fn get_reg_options() +-> &'static RegistrationVerificationOptions<'static, 'static, &'static str, &'static str> { REG_OPTIONS .get() .expect("REG_OPTIONS must be initialized in main") } static AUTH_CEREMONIES: OnceLock< - Arc<Mutex<FixedCapHashSet<AuthenticationServerState, BuildIdentityHasher>>>, + Arc<Mutex<FixedCapHashSet<NonDiscoverableAuthenticationServerState, BuildIdentityHasher>>>, > = OnceLock::new(); #[inline] fn init_auth_ceremonies() { @@ -82,8 +82,10 @@ fn init_auth_ceremonies() { .expect("AUTH_CEREMONIES must only be initialized once"); } #[inline] -pub fn get_auth_ceremonies( -) -> MutexGuard<'static, FixedCapHashSet<AuthenticationServerState, BuildIdentityHasher>> { +pub fn get_auth_ceremonies() -> MutexGuard< + 'static, + FixedCapHashSet<NonDiscoverableAuthenticationServerState, BuildIdentityHasher>, +> { AUTH_CEREMONIES .get() .expect("AUTH_CEREMONIES must be initialized in main") @@ -108,8 +110,8 @@ fn init_auth_options() { .expect("AUTH_OPTIONS must only be initialized once"); } #[inline] -pub fn get_auth_options( -) -> &'static AuthenticationVerificationOptions<'static, 'static, &'static str, &'static str> { +pub fn get_auth_options() +-> &'static AuthenticationVerificationOptions<'static, 'static, &'static str, &'static str> { AUTH_OPTIONS .get() .expect("AUTH_OPTIONS must be initialized in main") @@ -374,10 +376,10 @@ pub struct BasicJwtClaims { pub sub: String, } use crate::db::{ + DbConn, models::{ Collection, Device, User, UserOrgStatus, UserOrgType, UserOrganization, UserStampException, }, - DbConn, }; use rocket::{ outcome::try_outcome, @@ -547,8 +549,8 @@ impl<'r> FromRequest<'r> for OrgHeaders { user } else { err_handler!( - "The current user isn't confirmed member of the organization" - ) + "The current user isn't confirmed member of the organization" + ) } } None => { diff --git a/src/config.rs b/src/config.rs @@ -1,8 +1,10 @@ -use core::fmt::{self, Display, Formatter}; -use core::num::NonZeroU8; -use core::str; +use core::{ + fmt::{self, Display, Formatter}, + num::{NonZeroU8, NonZeroUsize}, + str, +}; use rocket::config::{CipherSuite, LogLevel, TlsConfig}; -use rocket::data::{Limits, ToByteUnit}; +use rocket::data::{Limits, ToByteUnit as _}; use std::error; use std::fs; use std::io::Error; @@ -133,7 +135,7 @@ impl Config { ..Default::default() }; if let Some(count) = config_file.workers { - rocket.workers = usize::from(count.get()); + rocket.workers = NonZeroUsize::from(count).get(); } let domain = AsciiDomain::try_from(config_file.domain).map_err(|_e| ConfigErr::BadDomain)?; diff --git a/src/crypto.rs b/src/crypto.rs @@ -22,7 +22,7 @@ pub fn verify_password_hash(secret: &[u8], salt: &[u8], previous: &[u8], iterati /// Return an array holding `N` random bytes. pub fn get_random_bytes<const N: usize>() -> [u8; N] { - use ring::rand::{SecureRandom, SystemRandom}; + use ring::rand::{SecureRandom as _, SystemRandom}; let mut array = [0; N]; SystemRandom::new() .fill(&mut array) @@ -34,11 +34,3 @@ pub fn get_random_bytes<const N: usize>() -> [u8; N] { pub fn encode_random_bytes<const N: usize>(e: &Encoding) -> String { e.encode(&get_random_bytes::<N>()) } - -// -// Constant time compare -// -pub fn ct_eq<T: AsRef<[u8]>, U: AsRef<[u8]>>(a: T, b: U) -> bool { - use ring::constant_time::verify_slices_are_equal; - verify_slices_are_equal(a.as_ref(), b.as_ref()).is_ok() -} diff --git a/src/db/mod.rs b/src/db/mod.rs @@ -1,16 +1,17 @@ use crate::{ config::{self, Config}, - error::{Error, MapResult}, + error::{Error, MapResult as _}, }; +use core::num::{NonZeroU32, NonZeroUsize}; use diesel::{ - connection::SimpleConnection, - r2d2::{self, ConnectionManager, CustomizeConnection, Pool, PooledConnection}, SqliteConnection, + connection::SimpleConnection as _, + r2d2::{self, ConnectionManager, CustomizeConnection, Pool, PooledConnection}, }; use rocket::{ + Request, http::Status, request::{FromRequest, Outcome}, - Request, }; use std::{panic, sync::Arc, time::Duration}; use tokio::{ @@ -83,7 +84,7 @@ impl DbPool { let url = Config::DATABASE_URL; let manager = ConnectionManager::new(url); let pool = Pool::builder() - .max_size(u32::from(config::get_config().database_max_conns.get())) + .max_size(NonZeroU32::from(config::get_config().database_max_conns).get()) .connection_timeout(Duration::from_secs(u64::from( config::get_config().database_timeout, ))) @@ -92,9 +93,9 @@ impl DbPool { .map_res("Failed to create pool")?; Ok(Self { pool: Some(pool), - semaphore: Arc::new(Semaphore::new(usize::from( - config::get_config().database_max_conns.get(), - ))), + semaphore: Arc::new(Semaphore::new( + NonZeroUsize::from(config::get_config().database_max_conns).get(), + )), }) } // Get a connection from the pool @@ -127,7 +128,7 @@ macro_rules! db_run { use diesel::prelude::*; use tokio::task; #[allow(unused)] - use $crate::db::FromDb; + use $crate::db::FromDb as _; let mut con = $conn.conn.clone().lock_owned().await; paste::paste! { #[allow(unused)] use $crate::db::schema::{self, *}; diff --git a/src/db/models/cipher.rs b/src/db/models/cipher.rs @@ -108,7 +108,7 @@ impl Cipher { } use crate::api::EmptyResult; use crate::db::DbConn; -use crate::error::MapResult; +use crate::error::MapResult as _; /// Database methods impl Cipher { pub async fn to_json( @@ -329,7 +329,7 @@ impl Cipher { } } } - }; + } user_uuids } @@ -421,7 +421,10 @@ impl Cipher { } /// Returns whether this cipher is owned by an org in which the user has full access. - #[allow(clippy::else_if_without_else)] + #[expect( + clippy::else_if_without_else, + reason = "don't want a blank else branch" + )] async fn is_in_full_access_org( &self, user_uuid: &str, diff --git a/src/db/models/collection.rs b/src/db/models/collection.rs @@ -151,7 +151,7 @@ impl Collection { use crate::api::EmptyResult; use crate::db::DbConn; -use crate::error::MapResult; +use crate::error::MapResult as _; /// Database methods impl Collection { diff --git a/src/db/models/device.rs b/src/db/models/device.rs @@ -63,7 +63,7 @@ impl Device { // let orguser: Vec<_> = orgs.iter().filter(|o| o.atype == 2).map(|o| o.org_uuid.clone()).collect(); // let orgmanager: Vec<_> = orgs.iter().filter(|o| o.atype == 3).map(|o| o.org_uuid.clone()).collect(); // Create the JWT claims struct, to send to the client - use crate::auth::{self, encode_jwt, LoginJwtClaims}; + use crate::auth::{self, LoginJwtClaims, encode_jwt}; let claims = LoginJwtClaims { nbf: time_now.timestamp(), exp: time_now @@ -101,7 +101,7 @@ impl Device { use crate::api::EmptyResult; use crate::db::DbConn; -use crate::error::MapResult; +use crate::error::MapResult as _; /// Database methods impl Device { diff --git a/src/db/models/favorite.rs b/src/db/models/favorite.rs @@ -1,7 +1,7 @@ use super::User; use crate::api::EmptyResult; use crate::db::DbConn; -use crate::error::MapResult; +use crate::error::MapResult as _; db_object! { #[derive(Insertable)] #[diesel(table_name = favorites)] diff --git a/src/db/models/folder.rs b/src/db/models/folder.rs @@ -58,7 +58,7 @@ impl FolderCipher { use crate::api::EmptyResult; use crate::db::DbConn; -use crate::error::MapResult; +use crate::error::MapResult as _; /// Database methods impl Folder { diff --git a/src/db/models/org_policy.rs b/src/db/models/org_policy.rs @@ -1,8 +1,8 @@ use super::{UserOrgStatus, UserOrgType, UserOrganization}; use crate::api::EmptyResult; -use crate::db::models::TwoFactorType; use crate::db::DbConn; -use crate::error::{Error, MapResult}; +use crate::db::models::TwoFactorType; +use crate::error::{Error, MapResult as _}; use crate::util; use diesel::result::{self, DatabaseErrorKind}; use serde::Deserialize; @@ -253,7 +253,7 @@ impl OrgPolicy { return Err(OrgPolicyErr::TwoFactorMissing); } _ => {} - }; + } } // Enforce Single Organization Policy of other organizations user is a member of diff --git a/src/db/models/organization.rs b/src/db/models/organization.rs @@ -295,7 +295,7 @@ impl UserOrganization { use crate::api::EmptyResult; use crate::db::DbConn; -use crate::error::MapResult; +use crate::error::MapResult as _; /// Database methods impl Organization { diff --git a/src/db/models/two_factor.rs b/src/db/models/two_factor.rs @@ -1,27 +1,28 @@ use crate::{ api::EmptyResult, db::{ - schema::{totp, webauthn}, DbConn, FromDb, + schema::{totp, webauthn}, }, error::Error, }; -use serde::ser::{Serialize, SerializeStruct, Serializer}; +use serde::ser::{Serialize, SerializeStruct as _, Serializer}; use tokio::task; use webauthn_rp::{ - bin::{Decode, Encode}, + AggErr, AuthenticatedCredential, RegisteredCredential, + bin::{Decode as _, Encode as _}, request::{ - auth::AllowedCredentials, register::UserHandle, Credentials, PublicKeyCredentialDescriptor, + Credentials, PublicKeyCredentialDescriptor, auth::AllowedCredentials, + register::UserHandle16, }, response::{ + AuthTransports, CredentialId, register::{ AuthenticatorExtensionOutputStaticState, CompressedP256PubKey, CompressedP384PubKey, CompressedPubKey, CredentialProtectionPolicy, DynamicState, Ed25519PubKey, RsaPubKey, StaticState, UncompressedPubKey, }, - AuthTransports, CredentialId, }, - AggErr, AuthenticatedCredential, RegisteredCredential, }; db_object! { /// Exactly one of the following is true: @@ -64,8 +65,9 @@ db_object! { } } impl WebAuthn { + #[allow(clippy::as_conversions, clippy::cast_possible_wrap)] pub fn new( - cred: &RegisteredCredential<'_, '_>, + cred: &RegisteredCredential<'_, 16>, user_uuid: String, id: i64, name: String, @@ -203,7 +205,7 @@ impl Totp { impl TwoFactorType { #[allow(clippy::clone_on_ref_ptr, clippy::shadow_unrelated)] pub async fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> EmptyResult { - use diesel::prelude::{Connection, ExpressionMethods, RunQueryDsl}; + use diesel::prelude::{Connection as _, ExpressionMethods as _, RunQueryDsl as _}; use diesel::result; let mut con_res = conn.conn.clone().lock_owned().await; let con = con_res.as_mut().expect("unable to get a pooled connection"); @@ -224,7 +226,7 @@ impl TwoFactorType { } #[allow(clippy::clone_on_ref_ptr)] pub async fn delete_by_user(self, user_uuid: &str, conn: &DbConn) -> EmptyResult { - use diesel::prelude::{ExpressionMethods, RunQueryDsl}; + use diesel::prelude::{ExpressionMethods as _, RunQueryDsl as _}; use diesel::result; let mut con_res = conn.conn.clone().lock_owned().await; let con = con_res.as_mut().expect("unable to get a pooled connection"); @@ -243,7 +245,9 @@ impl TwoFactorType { } #[allow(clippy::clone_on_ref_ptr, clippy::shadow_unrelated)] pub async fn has_twofactor(user_uuid: &str, conn: &DbConn) -> Result<bool, Error> { - use diesel::prelude::{Connection, ExpressionMethods, QueryDsl, RunQueryDsl}; + use diesel::prelude::{ + Connection as _, ExpressionMethods as _, QueryDsl as _, RunQueryDsl as _, + }; use diesel::result; let mut con_res = conn.conn.clone().lock_owned().await; let con = con_res.as_mut().expect("unable to get a pooled connection"); @@ -276,9 +280,11 @@ impl TwoFactorType { user_uuid: &str, conn: &DbConn, ) -> Result<(bool, Option<String>), Error> { - use diesel::prelude::{Connection, ExpressionMethods, QueryDsl, RunQueryDsl}; + use diesel::OptionalExtension as _; + use diesel::prelude::{ + Connection as _, ExpressionMethods as _, QueryDsl as _, RunQueryDsl as _, + }; use diesel::result; - use diesel::OptionalExtension; let mut con_res = conn.conn.clone().lock_owned().await; let con = con_res.as_mut().expect("unable to get a pooled connection"); task::block_in_place(move || { @@ -304,7 +310,7 @@ impl TwoFactorType { impl WebAuthnInfo { #[allow(clippy::clone_on_ref_ptr)] pub async fn get_all_by_user(user_uuid: &str, conn: &DbConn) -> Result<Vec<Self>, Error> { - use diesel::prelude::{ExpressionMethods, QueryDsl, RunQueryDsl}; + use diesel::prelude::{ExpressionMethods as _, QueryDsl as _, RunQueryDsl as _}; use diesel::result; let mut con_res = conn.conn.clone().lock_owned().await; let con = con_res.as_mut().expect("unable to get a pooled connection"); @@ -325,7 +331,7 @@ impl WebAuthn { T: Credentials, PublicKeyCredentialDescriptor<Vec<u8>>: Into<T::Credential>, { - use diesel::prelude::{ExpressionMethods, QueryDsl, RunQueryDsl}; + use diesel::prelude::{ExpressionMethods as _, QueryDsl as _, RunQueryDsl as _}; use diesel::result; let mut con_res = conn.conn.clone().lock_owned().await; let con = con_res.as_mut().expect("unable to get a pooled connection"); @@ -366,7 +372,7 @@ impl WebAuthn { #[allow(clippy::clone_on_ref_ptr)] pub async fn insert(self, conn: &DbConn) -> EmptyResult { use __sqlite_model::WebAuthnDb; - use diesel::prelude::RunQueryDsl; + use diesel::prelude::RunQueryDsl as _; use diesel::result; let mut con_res = conn.conn.clone().lock_owned().await; let con = con_res.as_mut().expect("unable to get a pooled connection"); @@ -392,7 +398,7 @@ impl WebAuthn { dynamic_state: DynamicState, conn: &DbConn, ) -> EmptyResult { - use diesel::prelude::{ExpressionMethods, RunQueryDsl}; + use diesel::prelude::{ExpressionMethods as _, RunQueryDsl as _}; use diesel::result; let mut con_res = conn.conn.clone().lock_owned().await; let con = con_res.as_mut().expect("unable to get a pooled connection"); @@ -413,25 +419,30 @@ impl WebAuthn { }) }) } - #[allow(clippy::clone_on_ref_ptr)] + #[allow( + clippy::as_conversions, + clippy::cast_sign_loss, + clippy::clone_on_ref_ptr + )] pub async fn get_credential<'a, 'b>( credential_id: CredentialId<&'a [u8]>, user_uuid: &str, - user_handle: UserHandle<&'b [u8]>, + user_handle: &'b UserHandle16, conn: &DbConn, ) -> Result< Option< AuthenticatedCredential< 'a, 'b, + 16, CompressedPubKey<[u8; 32], [u8; 32], [u8; 48], Vec<u8>>, >, >, Error, > { - use diesel::prelude::{ExpressionMethods, QueryDsl, RunQueryDsl}; + use diesel::OptionalExtension as _; + use diesel::prelude::{ExpressionMethods as _, QueryDsl as _, RunQueryDsl as _}; use diesel::result; - use diesel::OptionalExtension; let mut con_res = conn.conn.clone().lock_owned().await; let con = con_res.as_mut().expect("unable to get a pooled connection"); task::block_in_place(move || { @@ -553,7 +564,7 @@ impl WebAuthn { id: i64, conn: &DbConn, ) -> EmptyResult { - use diesel::prelude::{ExpressionMethods, RunQueryDsl}; + use diesel::prelude::{ExpressionMethods as _, RunQueryDsl as _}; use diesel::result; let mut con_res = conn.conn.clone().lock_owned().await; let con = con_res.as_mut().expect("unable to get a pooled connection"); @@ -580,7 +591,7 @@ impl Totp { #[allow(clippy::clone_on_ref_ptr, clippy::shadow_unrelated)] pub async fn replace(self, conn: &DbConn) -> EmptyResult { use __sqlite_model::TotpDb; - use diesel::prelude::{ExpressionMethods, RunQueryDsl}; + use diesel::prelude::{ExpressionMethods as _, RunQueryDsl as _}; use diesel::result; let mut con_res = conn.conn.clone().lock_owned().await; let con = con_res.as_mut().expect("unable to get a pooled connection"); @@ -614,9 +625,9 @@ impl Totp { #[allow(clippy::clone_on_ref_ptr)] pub async fn find_by_user(user_uuid: &str, conn: &DbConn) -> Result<Option<Self>, Error> { use __sqlite_model::TotpDb; - use diesel::prelude::{ExpressionMethods, QueryDsl, RunQueryDsl}; + use diesel::OptionalExtension as _; + use diesel::prelude::{ExpressionMethods as _, QueryDsl as _, RunQueryDsl as _}; use diesel::result; - use diesel::OptionalExtension; let mut con_res = conn.conn.clone().lock_owned().await; let con = con_res.as_mut().expect("unable to get a pooled connection"); task::block_in_place(move || { diff --git a/src/db/models/user.rs b/src/db/models/user.rs @@ -168,17 +168,6 @@ impl User { .expect("underflow converting password iterations into a u32"), ) } - - pub fn check_valid_recovery_code(&self, recovery_code: &str) -> bool { - self.totp_recover.as_ref().map_or(false, |totp_recover| { - crypto::ct_eq(recovery_code, totp_recover.to_lowercase()) - }) - } - - pub fn check_valid_api_key(&self, key: &str) -> bool { - matches!(self.api_key, Some(ref api_key) if crypto::ct_eq(api_key, key)) - } - /// Set the password hash generated /// And resets the security_stamp. Based upon the allow_next_route the security_stamp will be different. /// @@ -187,9 +176,8 @@ impl User { /// * `password` - A str which contains a hashed version of the users master password. /// * `new_key` - A String which contains the new aKey value of the users master password. /// * `allow_next_route` - A Option<Vec<String>> with the function names of the next allowed (rocket) routes. - /// These routes are able to use the previous stamp id for the next 2 minutes. - /// After these 2 minutes this stamp will expire. - /// + /// These routes are able to use the previous stamp id for the next 2 minutes. + /// After these 2 minutes this stamp will expire. pub fn set_password( &mut self, password: &str, @@ -222,9 +210,8 @@ impl User { /// /// # Arguments /// * `route_exception` - A Vec<String> with the function names of the next allowed (rocket) routes. - /// These routes are able to use the previous stamp id for the next 2 minutes. - /// After these 2 minutes this stamp will expire. - /// + /// These routes are able to use the previous stamp id for the next 2 minutes. + /// After these 2 minutes this stamp will expire. pub fn set_stamp_exception(&mut self, route_exception: Vec<String>) { let stamp_exception = UserStampException { routes: route_exception, @@ -251,7 +238,7 @@ impl User { use super::{Cipher, Device, Favorite, Folder, TwoFactorType, UserOrgType, UserOrganization}; use crate::api::EmptyResult; use crate::db::DbConn; -use crate::error::MapResult; +use crate::error::MapResult as _; /// Database methods impl User { diff --git a/src/error.rs b/src/error.rs @@ -4,7 +4,6 @@ use core::error::Error as StdError; use core::fmt::{self, Debug, Display, Formatter}; -#[expect(edition_2024_expr_fragment_specifier, reason = "false positive")] macro_rules! make_error { ( $( $name:ident ( $ty:ty ): $src_fn:expr, $usr_msg_fun:expr ),+ $(,)? ) => { const BAD_REQUEST: u16 = 400; @@ -36,9 +35,9 @@ macro_rules! make_error { } use core::any::Any; use core::convert::Infallible; +use diesel::ConnectionError as DieselConErr; use diesel::r2d2::PoolError as R2d2Err; use diesel::result::Error as DieselErr; -use diesel::ConnectionError as DieselConErr; use jsonwebtoken::errors::Error as JwtErr; use openssl::error::ErrorStack as SSLErr; #[cfg(all(feature = "priv_sep", target_os = "openbsd"))] @@ -223,7 +222,7 @@ use rocket::request::Request; use rocket::response::{self, Responder, Response}; use std::io::Cursor; -impl<'r> Responder<'r, 'static> for Error { +impl Responder<'_, 'static> for Error { fn respond_to(self, _: &Request<'_>) -> response::Result<'static> { let code = Status::from_code(self.error_code).unwrap_or(Status::BadRequest); let body = self.to_string(); @@ -238,7 +237,6 @@ impl<'r> Responder<'r, 'static> for Error { // // Error return macros // -#[expect(edition_2024_expr_fragment_specifier, reason = "false positive")] #[macro_export] macro_rules! err { ($msg:expr) => {{ @@ -255,7 +253,6 @@ macro_rules! err { }}; } -#[expect(edition_2024_expr_fragment_specifier, reason = "false positive")] #[macro_export] macro_rules! err_silent { ($msg:expr) => {{ @@ -266,7 +263,6 @@ macro_rules! err_silent { }}; } -#[expect(edition_2024_expr_fragment_specifier, reason = "false positive")] #[macro_export] macro_rules! err_code { ($msg:expr, $err_code:expr) => {{ @@ -277,7 +273,6 @@ macro_rules! err_code { }}; } -#[expect(edition_2024_expr_fragment_specifier, reason = "false positive")] #[macro_export] macro_rules! err_discard { ($msg:expr, $data:expr) => {{ @@ -290,7 +285,6 @@ macro_rules! err_discard { }}; } -#[expect(edition_2024_expr_fragment_specifier, reason = "false positive")] #[macro_export] macro_rules! err_json { ($expr:expr, $log_value:expr) => {{ @@ -301,7 +295,6 @@ macro_rules! err_json { }}; } -#[expect(edition_2024_expr_fragment_specifier, reason = "false positive")] #[macro_export] macro_rules! err_handler { ($expr:expr) => {{ diff --git a/src/main.rs b/src/main.rs @@ -22,7 +22,10 @@ clippy::style, clippy::suspicious )] -#![allow( +#![expect( + clippy::allow_attributes, + clippy::allow_attributes_without_reason, + clippy::arbitrary_source_item_ordering, clippy::blanket_clippy_restriction_lints, clippy::doc_markdown, clippy::expect_used, @@ -35,18 +38,15 @@ clippy::missing_errors_doc, clippy::missing_trait_methods, clippy::mod_module_files, - clippy::module_name_repetitions, clippy::multiple_crate_versions, clippy::multiple_inherent_impl, - clippy::multiple_unsafe_ops_per_block, - clippy::no_effect_underscore_binding, clippy::panic, - clippy::panic_in_result_fn, clippy::partial_pub_fields, clippy::pub_use, clippy::question_mark_used, clippy::redundant_type_annotations, clippy::ref_patterns, + clippy::return_and_then, clippy::shadow_reuse, clippy::significant_drop_in_scrutinee, clippy::significant_drop_tightening, @@ -59,7 +59,9 @@ clippy::unseparated_literal_suffix, clippy::unwrap_in_result, clippy::unwrap_used, - clippy::used_underscore_binding + clippy::used_underscore_binding, + clippy::used_underscore_items, + reason = "noisy, opinionated, and likely doesn't prevent bugs or improve APIs" )] // The recursion_limit is mainly triggered by the json!() macro. // The more key/value pairs there are the more recursion occurs. @@ -84,6 +86,7 @@ mod db; mod priv_sep; mod util; use config::Config; +use core::num::NonZeroU32; pub use error::{Error, MapResult}; use std::env; use std::{path::Path, process}; @@ -132,14 +135,13 @@ fn static_init() { }); auth::init_values(); } - -#[allow(clippy::exit)] +#[expect(clippy::exit, reason = "upstream")] fn check_data_folder() { if !Path::new(Config::DATA_FOLDER).is_dir() { process::exit(1); } } -#[allow(clippy::exit)] +#[expect(clippy::exit, reason = "upstream")] fn check_web_vault() { if config::get_config().web_vault_enabled && !Path::new(Config::WEB_VAULT_FOLDER) @@ -149,11 +151,11 @@ fn check_web_vault() { process::exit(1); } } -#[allow(clippy::exit)] +#[expect(clippy::exit, reason = "upstream")] async fn create_db_pool() -> db::DbPool { (util::retry_db( db::DbPool::from_config, - u32::from(config::get_config().db_connection_retries.get()), + NonZeroU32::from(config::get_config().db_connection_retries).get(), ) .await) .unwrap_or_else(|_| { diff --git a/src/priv_sep.rs b/src/priv_sep.rs @@ -50,7 +50,7 @@ pub fn pledge_away_unveil(promises: &mut Promises) -> Result<(), Error> { #[allow(clippy::unnecessary_wraps)] #[cfg(not(all(feature = "priv_sep", target_os = "openbsd")))] #[inline] -pub fn pledge_away_unveil(_: &mut Zst) -> Result<(), Infallible> { +pub const fn pledge_away_unveil(_: &mut Zst) -> Result<(), Infallible> { Ok(()) } /// Calls `unveil` on `path` with `Permissions::READ`.