forked from trashmodern/vaultwarden
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 38aad4f7be | |||
| 20d9e885bf |
Generated
+109
-78
File diff suppressed because it is too large
Load Diff
+11
-11
@@ -53,7 +53,7 @@ once_cell = "1.20.2"
|
||||
# Numerical libraries
|
||||
num-traits = "0.2.19"
|
||||
num-derive = "0.4.2"
|
||||
bigdecimal = "0.4.5"
|
||||
bigdecimal = "0.4.6"
|
||||
|
||||
# Web framework
|
||||
rocket = { version = "0.5.1", features = ["tls", "json"], default-features = false }
|
||||
@@ -67,10 +67,10 @@ dashmap = "6.1.0"
|
||||
|
||||
# Async futures
|
||||
futures = "0.3.31"
|
||||
tokio = { version = "1.41.0", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal", "net"] }
|
||||
tokio = { version = "1.41.1", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal", "net"] }
|
||||
|
||||
# A generic serialization/deserialization framework
|
||||
serde = { version = "1.0.213", features = ["derive"] }
|
||||
serde = { version = "1.0.214", features = ["derive"] }
|
||||
serde_json = "1.0.132"
|
||||
|
||||
# A safe, extensible ORM and Query builder
|
||||
@@ -112,7 +112,7 @@ yubico = { version = "0.11.0", features = ["online-tokio"], default-features = f
|
||||
webauthn-rs = "0.3.2"
|
||||
|
||||
# Handling of URL's for WebAuthn and favicons
|
||||
url = "2.5.2"
|
||||
url = "2.5.3"
|
||||
|
||||
# Email libraries
|
||||
lettre = { version = "0.11.10", features = ["smtp-transport", "sendmail-transport", "builder", "serde", "tokio1-native-tls", "hostname", "tracing", "tokio1"], default-features = false }
|
||||
@@ -120,24 +120,24 @@ percent-encoding = "2.3.1" # URL encoding library used for URL's in the emails
|
||||
email_address = "0.2.9"
|
||||
|
||||
# HTML Template library
|
||||
handlebars = { version = "6.1.0", features = ["dir_source"] }
|
||||
handlebars = { version = "6.2.0", features = ["dir_source"] }
|
||||
|
||||
# HTTP client (Used for favicons, version check, DUO and HIBP API)
|
||||
reqwest = { version = "0.12.8", features = ["native-tls-alpn", "stream", "json", "gzip", "brotli", "socks", "cookies"] }
|
||||
reqwest = { version = "0.12.9", features = ["native-tls-alpn", "stream", "json", "gzip", "brotli", "socks", "cookies"] }
|
||||
hickory-resolver = "0.24.1"
|
||||
|
||||
# Favicon extraction libraries
|
||||
html5gum = "0.5.7"
|
||||
regex = { version = "1.11.0", features = ["std", "perf", "unicode-perl"], default-features = false }
|
||||
html5gum = "0.6.1"
|
||||
regex = { version = "1.11.1", features = ["std", "perf", "unicode-perl"], default-features = false }
|
||||
data-url = "0.3.1"
|
||||
bytes = "1.8.0"
|
||||
|
||||
# Cache function results (Used for version check and favicon fetching)
|
||||
cached = { version = "0.53.1", features = ["async"] }
|
||||
cached = { version = "0.54.0", features = ["async"] }
|
||||
|
||||
# Used for custom short lived cookie jar during favicon extraction
|
||||
cookie = "0.18.1"
|
||||
cookie_store = "0.21.0"
|
||||
cookie_store = "0.21.1"
|
||||
|
||||
# Used by U2F, JWT and PostgreSQL
|
||||
openssl = "0.10.68"
|
||||
@@ -155,7 +155,7 @@ semver = "1.0.23"
|
||||
# Allow overriding the default memory allocator
|
||||
# Mainly used for the musl builds, since the default musl malloc is very slow
|
||||
mimalloc = { version = "0.1.43", features = ["secure"], default-features = false, optional = true }
|
||||
which = "6.0.3"
|
||||
which = "7.0.0"
|
||||
|
||||
# Argon2 library with support for the PHC format
|
||||
argon2 = "0.5.3"
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
[workspace.metadata.dylint]
|
||||
libraries = [{ path = "dylints/*" }]
|
||||
@@ -1,7 +0,0 @@
|
||||
# How to run Lints
|
||||
|
||||
```sh
|
||||
cargo install cargo-dylint dylint-link
|
||||
|
||||
RUSTFLAGS="-Aunreachable_patterns" cargo dylint --all -- --features sqlite
|
||||
```
|
||||
@@ -1,2 +0,0 @@
|
||||
[target.'cfg(all())']
|
||||
linker = "dylint-link"
|
||||
@@ -1 +0,0 @@
|
||||
/target
|
||||
-1659
File diff suppressed because it is too large
Load Diff
@@ -1,20 +0,0 @@
|
||||
[package]
|
||||
name = "non_authenticated_routes"
|
||||
version = "0.1.0"
|
||||
authors = ["authors go here"]
|
||||
description = "description goes here"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
clippy_utils = { git = "https://github.com/rust-lang/rust-clippy", rev = "4f0e46b74dbc8441daf084b6f141a7fe414672a2" }
|
||||
dylint_linting = "3.2.1"
|
||||
|
||||
[dev-dependencies]
|
||||
dylint_testing = "3.2.1"
|
||||
|
||||
[package.metadata.rust-analyzer]
|
||||
rustc_private = true
|
||||
@@ -1,3 +0,0 @@
|
||||
[toolchain]
|
||||
channel = "nightly-2024-11-09"
|
||||
components = ["llvm-tools-preview", "rustc-dev"]
|
||||
@@ -1,167 +0,0 @@
|
||||
#![feature(rustc_private)]
|
||||
#![feature(let_chains)]
|
||||
|
||||
extern crate rustc_arena;
|
||||
extern crate rustc_ast;
|
||||
extern crate rustc_ast_pretty;
|
||||
extern crate rustc_attr;
|
||||
extern crate rustc_data_structures;
|
||||
extern crate rustc_errors;
|
||||
extern crate rustc_hir;
|
||||
extern crate rustc_hir_pretty;
|
||||
extern crate rustc_index;
|
||||
extern crate rustc_infer;
|
||||
extern crate rustc_lexer;
|
||||
extern crate rustc_middle;
|
||||
extern crate rustc_mir_dataflow;
|
||||
extern crate rustc_parse;
|
||||
extern crate rustc_span;
|
||||
extern crate rustc_target;
|
||||
extern crate rustc_trait_selection;
|
||||
|
||||
use clippy_utils::diagnostics::span_lint;
|
||||
use rustc_hir::{def_id::DefId, Item, ItemKind, QPath, TyKind};
|
||||
use rustc_lint::{LateContext, LateLintPass};
|
||||
use rustc_span::{symbol::Ident, Span, Symbol};
|
||||
|
||||
dylint_linting::impl_late_lint! {
|
||||
/// ### What it does
|
||||
///
|
||||
/// ### Why is this bad?
|
||||
///
|
||||
/// ### Known problems
|
||||
/// Remove if none.
|
||||
///
|
||||
/// ### Example
|
||||
/// ```rust
|
||||
/// // example code where a warning is issued
|
||||
/// ```
|
||||
/// Use instead:
|
||||
/// ```rust
|
||||
/// // example code that does not raise a warning
|
||||
/// ```
|
||||
pub NON_AUTHENTICATED_ROUTES,
|
||||
Warn,
|
||||
"description goes here",
|
||||
NonAuthenticatedRoutes::default()
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct NonAuthenticatedRoutes {
|
||||
last_function_item: Option<(Ident, Span, bool)>,
|
||||
}
|
||||
|
||||
// Collect all the attribute macros that are applied to the given span
|
||||
fn attr_def_ids(mut span: rustc_span::Span) -> Vec<(DefId, Symbol, Option<DefId>)> {
|
||||
use rustc_span::hygiene::{walk_chain, ExpnKind, MacroKind};
|
||||
use rustc_span::{ExpnData, SyntaxContext};
|
||||
|
||||
let mut def_ids = Vec::new();
|
||||
while span.ctxt() != SyntaxContext::root() {
|
||||
if let ExpnData {
|
||||
kind: ExpnKind::Macro(MacroKind::Attr, macro_symbol),
|
||||
macro_def_id: Some(def_id),
|
||||
parent_module,
|
||||
..
|
||||
} = span.ctxt().outer_expn_data()
|
||||
{
|
||||
def_ids.push((def_id, macro_symbol, parent_module));
|
||||
}
|
||||
span = walk_chain(span, SyntaxContext::root());
|
||||
}
|
||||
def_ids
|
||||
}
|
||||
|
||||
const ROCKET_MACRO_EXCEPTIONS: [(&str, &str); 1] = [("rocket::catch", "catch")];
|
||||
|
||||
const VALID_AUTH_HEADERS: [&str; 6] = [
|
||||
"auth::Headers",
|
||||
"auth::OrgHeaders",
|
||||
"auth::AdminHeaders",
|
||||
"auth::ManagerHeaders",
|
||||
"auth::ManagerHeadersLoose",
|
||||
"auth::OwnerHeaders",
|
||||
];
|
||||
|
||||
impl<'tcx> LateLintPass<'tcx> for NonAuthenticatedRoutes {
|
||||
fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item) {
|
||||
if let ItemKind::Fn(sig, ..) = item.kind {
|
||||
let mut has_auth_headers = false;
|
||||
|
||||
for input in sig.decl.inputs {
|
||||
let TyKind::Path(QPath::Resolved(_, path)) = input.kind else {
|
||||
continue;
|
||||
};
|
||||
|
||||
for seg in path.segments {
|
||||
if let Some(def_id) = seg.res.opt_def_id() {
|
||||
let def = cx.tcx.def_path_str(def_id);
|
||||
if VALID_AUTH_HEADERS.contains(&def.as_str()) {
|
||||
has_auth_headers = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.last_function_item = Some((item.ident, sig.span, has_auth_headers));
|
||||
return;
|
||||
}
|
||||
|
||||
let ItemKind::Struct(_data, _generics) = item.kind else {
|
||||
return;
|
||||
};
|
||||
|
||||
let def_ids = attr_def_ids(item.span);
|
||||
|
||||
let mut is_rocket_route = false;
|
||||
|
||||
for (def_id, sym, parent) in &def_ids {
|
||||
let def_id = cx.tcx.def_path_str(*def_id);
|
||||
let sym = sym.as_str();
|
||||
let parent = parent.map(|parent| cx.tcx.def_path_str(parent));
|
||||
|
||||
if ROCKET_MACRO_EXCEPTIONS.contains(&(&def_id, sym)) {
|
||||
is_rocket_route = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if def_id.starts_with("rocket::") || parent.as_deref() == Some("rocket_codegen") {
|
||||
is_rocket_route = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if !is_rocket_route {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some((func_ident, func_span, has_auth_headers)) = self.last_function_item.take() else {
|
||||
span_lint(cx, NON_AUTHENTICATED_ROUTES, item.span, "No function found before the expanded route");
|
||||
return;
|
||||
};
|
||||
|
||||
if func_ident != item.ident {
|
||||
span_lint(
|
||||
cx,
|
||||
NON_AUTHENTICATED_ROUTES,
|
||||
item.span,
|
||||
"The function before the expanded route does not match the route",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if !has_auth_headers {
|
||||
span_lint(
|
||||
cx,
|
||||
NON_AUTHENTICATED_ROUTES,
|
||||
func_span,
|
||||
"This Rocket route does not have any authentication headers",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ui() {
|
||||
dylint_testing::ui_test(env!("CARGO_PKG_NAME"), "ui");
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
fn main() {}
|
||||
+86
-84
@@ -1,5 +1,5 @@
|
||||
use crate::db::DbPool;
|
||||
use chrono::{SecondsFormat, Utc};
|
||||
use chrono::Utc;
|
||||
use rocket::serde::json::Json;
|
||||
use serde_json::Value;
|
||||
|
||||
@@ -13,7 +13,7 @@ use crate::{
|
||||
crypto,
|
||||
db::{models::*, DbConn},
|
||||
mail,
|
||||
util::NumberOrString,
|
||||
util::{format_date, NumberOrString},
|
||||
CONFIG,
|
||||
};
|
||||
|
||||
@@ -901,14 +901,12 @@ pub async fn _prelogin(data: Json<PreloginData>, mut conn: DbConn) -> Json<Value
|
||||
None => (User::CLIENT_KDF_TYPE_DEFAULT, User::CLIENT_KDF_ITER_DEFAULT, None, None),
|
||||
};
|
||||
|
||||
let result = json!({
|
||||
Json(json!({
|
||||
"kdf": kdf_type,
|
||||
"kdfIterations": kdf_iter,
|
||||
"kdfMemory": kdf_mem,
|
||||
"kdfParallelism": kdf_para,
|
||||
});
|
||||
|
||||
Json(result)
|
||||
}))
|
||||
}
|
||||
|
||||
// https://github.com/bitwarden/server/blob/master/src/Api/Models/Request/Accounts/SecretVerificationRequestModel.cs
|
||||
@@ -1084,14 +1082,15 @@ struct AuthRequestRequest {
|
||||
device_identifier: String,
|
||||
email: String,
|
||||
public_key: String,
|
||||
#[serde(alias = "type")]
|
||||
_type: i32,
|
||||
// Not used for now
|
||||
// #[serde(alias = "type")]
|
||||
// _type: i32,
|
||||
}
|
||||
|
||||
#[post("/auth-requests", data = "<data>")]
|
||||
async fn post_auth_request(
|
||||
data: Json<AuthRequestRequest>,
|
||||
headers: ClientHeaders,
|
||||
client_headers: ClientHeaders,
|
||||
mut conn: DbConn,
|
||||
nt: Notify<'_>,
|
||||
) -> JsonResult {
|
||||
@@ -1099,16 +1098,20 @@ async fn post_auth_request(
|
||||
|
||||
let user = match User::find_by_mail(&data.email, &mut conn).await {
|
||||
Some(user) => user,
|
||||
None => {
|
||||
err!("AuthRequest doesn't exist")
|
||||
}
|
||||
None => err!("AuthRequest doesn't exist", "User not found"),
|
||||
};
|
||||
|
||||
// Validate device uuid and type
|
||||
match Device::find_by_uuid_and_user(&data.device_identifier, &user.uuid, &mut conn).await {
|
||||
Some(device) if device.atype == client_headers.device_type => {}
|
||||
_ => err!("AuthRequest doesn't exist", "Device verification failed"),
|
||||
}
|
||||
|
||||
let mut auth_request = AuthRequest::new(
|
||||
user.uuid.clone(),
|
||||
data.device_identifier.clone(),
|
||||
headers.device_type,
|
||||
headers.ip.ip.to_string(),
|
||||
client_headers.device_type,
|
||||
client_headers.ip.ip.to_string(),
|
||||
data.access_code,
|
||||
data.public_key,
|
||||
);
|
||||
@@ -1123,7 +1126,7 @@ async fn post_auth_request(
|
||||
"requestIpAddress": auth_request.request_ip,
|
||||
"key": null,
|
||||
"masterPasswordHash": null,
|
||||
"creationDate": auth_request.creation_date.and_utc().to_rfc3339_opts(SecondsFormat::Micros, true),
|
||||
"creationDate": format_date(&auth_request.creation_date),
|
||||
"responseDate": null,
|
||||
"requestApproved": false,
|
||||
"origin": CONFIG.domain_origin(),
|
||||
@@ -1132,33 +1135,31 @@ async fn post_auth_request(
|
||||
}
|
||||
|
||||
#[get("/auth-requests/<uuid>")]
|
||||
async fn get_auth_request(uuid: &str, mut conn: DbConn) -> JsonResult {
|
||||
async fn get_auth_request(uuid: &str, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||
if headers.user.uuid != uuid {
|
||||
err!("AuthRequest doesn't exist", "User uuid's do not match")
|
||||
}
|
||||
|
||||
let auth_request = match AuthRequest::find_by_uuid(uuid, &mut conn).await {
|
||||
Some(auth_request) => auth_request,
|
||||
None => {
|
||||
err!("AuthRequest doesn't exist")
|
||||
}
|
||||
None => err!("AuthRequest doesn't exist", "Record not found"),
|
||||
};
|
||||
|
||||
let response_date_utc = auth_request
|
||||
.response_date
|
||||
.map(|response_date| response_date.and_utc().to_rfc3339_opts(SecondsFormat::Micros, true));
|
||||
let response_date_utc = auth_request.response_date.map(|response_date| format_date(&response_date));
|
||||
|
||||
Ok(Json(json!(
|
||||
{
|
||||
"id": uuid,
|
||||
"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": auth_request.creation_date.and_utc().to_rfc3339_opts(SecondsFormat::Micros, true),
|
||||
"responseDate": response_date_utc,
|
||||
"requestApproved": auth_request.approved,
|
||||
"origin": CONFIG.domain_origin(),
|
||||
"object":"auth-request"
|
||||
}
|
||||
)))
|
||||
Ok(Json(json!({
|
||||
"id": uuid,
|
||||
"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)]
|
||||
@@ -1174,6 +1175,7 @@ struct AuthResponseRequest {
|
||||
async fn put_auth_request(
|
||||
uuid: &str,
|
||||
data: Json<AuthResponseRequest>,
|
||||
headers: Headers,
|
||||
mut conn: DbConn,
|
||||
ant: AnonymousNotify<'_>,
|
||||
nt: Notify<'_>,
|
||||
@@ -1181,11 +1183,13 @@ async fn put_auth_request(
|
||||
let data = data.into_inner();
|
||||
let mut auth_request: AuthRequest = match AuthRequest::find_by_uuid(uuid, &mut conn).await {
|
||||
Some(auth_request) => auth_request,
|
||||
None => {
|
||||
err!("AuthRequest doesn't exist")
|
||||
}
|
||||
None => err!("AuthRequest doesn't exist", "Record not found"),
|
||||
};
|
||||
|
||||
if headers.user.uuid != auth_request.user_uuid {
|
||||
err!("AuthRequest doesn't exist", "User uuid's do not match")
|
||||
}
|
||||
|
||||
auth_request.approved = Some(data.request_approved);
|
||||
auth_request.enc_key = Some(data.key);
|
||||
auth_request.master_password_hash = data.master_password_hash;
|
||||
@@ -1197,59 +1201,57 @@ async fn put_auth_request(
|
||||
nt.send_auth_response(&auth_request.user_uuid, &auth_request.uuid, data.device_identifier, &mut conn).await;
|
||||
}
|
||||
|
||||
let response_date_utc = auth_request
|
||||
.response_date
|
||||
.map(|response_date| response_date.and_utc().to_rfc3339_opts(SecondsFormat::Micros, true));
|
||||
let response_date_utc = auth_request.response_date.map(|response_date| format_date(&response_date));
|
||||
|
||||
Ok(Json(json!(
|
||||
{
|
||||
"id": uuid,
|
||||
"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": auth_request.creation_date.and_utc().to_rfc3339_opts(SecondsFormat::Micros, true),
|
||||
"responseDate": response_date_utc,
|
||||
"requestApproved": auth_request.approved,
|
||||
"origin": CONFIG.domain_origin(),
|
||||
"object":"auth-request"
|
||||
}
|
||||
)))
|
||||
Ok(Json(json!({
|
||||
"id": uuid,
|
||||
"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/<uuid>/response?<code>")]
|
||||
async fn get_auth_request_response(uuid: &str, code: &str, mut conn: DbConn) -> JsonResult {
|
||||
async fn get_auth_request_response(
|
||||
uuid: &str,
|
||||
code: &str,
|
||||
client_headers: ClientHeaders,
|
||||
mut conn: DbConn,
|
||||
) -> JsonResult {
|
||||
let auth_request = match AuthRequest::find_by_uuid(uuid, &mut conn).await {
|
||||
Some(auth_request) => auth_request,
|
||||
None => {
|
||||
err!("AuthRequest doesn't exist")
|
||||
}
|
||||
None => err!("AuthRequest doesn't exist", "User not found"),
|
||||
};
|
||||
|
||||
if !auth_request.check_access_code(code) {
|
||||
err!("Access code invalid doesn't exist")
|
||||
if auth_request.device_type != client_headers.device_type
|
||||
&& auth_request.request_ip != client_headers.ip.ip.to_string()
|
||||
&& !auth_request.check_access_code(code)
|
||||
{
|
||||
err!("AuthRequest doesn't exist", "Invalid device, IP or code")
|
||||
}
|
||||
|
||||
let response_date_utc = auth_request
|
||||
.response_date
|
||||
.map(|response_date| response_date.and_utc().to_rfc3339_opts(SecondsFormat::Micros, true));
|
||||
let response_date_utc = auth_request.response_date.map(|response_date| format_date(&response_date));
|
||||
|
||||
Ok(Json(json!(
|
||||
{
|
||||
"id": uuid,
|
||||
"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": auth_request.creation_date.and_utc().to_rfc3339_opts(SecondsFormat::Micros, true),
|
||||
"responseDate": response_date_utc,
|
||||
"requestApproved": auth_request.approved,
|
||||
"origin": CONFIG.domain_origin(),
|
||||
"object":"auth-request"
|
||||
}
|
||||
)))
|
||||
Ok(Json(json!({
|
||||
"id": uuid,
|
||||
"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")]
|
||||
@@ -1261,7 +1263,7 @@ async fn get_auth_requests(headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||
.iter()
|
||||
.filter(|request| request.approved.is_none())
|
||||
.map(|request| {
|
||||
let response_date_utc = request.response_date.map(|response_date| response_date.and_utc().to_rfc3339_opts(SecondsFormat::Micros, true));
|
||||
let response_date_utc = request.response_date.map(|response_date| format_date(&response_date));
|
||||
|
||||
json!({
|
||||
"id": request.uuid,
|
||||
@@ -1270,7 +1272,7 @@ async fn get_auth_requests(headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||
"requestIpAddress": request.request_ip,
|
||||
"key": request.enc_key,
|
||||
"masterPasswordHash": request.master_password_hash,
|
||||
"creationDate": request.creation_date.and_utc().to_rfc3339_opts(SecondsFormat::Micros, true),
|
||||
"creationDate": format_date(&request.creation_date),
|
||||
"responseDate": response_date_utc,
|
||||
"requestApproved": request.approved,
|
||||
"origin": CONFIG.domain_origin(),
|
||||
|
||||
+6
-5
@@ -135,12 +135,13 @@ async fn put_eq_domains(data: Json<EquivDomainData>, headers: Headers, conn: DbC
|
||||
}
|
||||
|
||||
#[get("/hibp/breach?<username>")]
|
||||
async fn hibp_breach(username: &str) -> JsonResult {
|
||||
let url = format!(
|
||||
"https://haveibeenpwned.com/api/v3/breachedaccount/{username}?truncateResponse=false&includeUnverified=false"
|
||||
);
|
||||
|
||||
async fn hibp_breach(username: &str, _headers: Headers) -> JsonResult {
|
||||
if let Some(api_key) = crate::CONFIG.hibp_api_key() {
|
||||
let username: String = url::form_urlencoded::byte_serialize(username.as_bytes()).collect();
|
||||
let url = format!(
|
||||
"https://haveibeenpwned.com/api/v3/breachedaccount/{username}?truncateResponse=false&includeUnverified=false"
|
||||
);
|
||||
|
||||
let res = make_http_request(Method::GET, &url)?.header("hibp-api-key", api_key).send().await?;
|
||||
|
||||
// If we get a 404, return a 404, it means no breached accounts
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::util::LowerCase;
|
||||
use crate::CONFIG;
|
||||
use chrono::{DateTime, NaiveDateTime, TimeDelta, Utc};
|
||||
use chrono::{NaiveDateTime, TimeDelta, Utc};
|
||||
use serde_json::Value;
|
||||
|
||||
use super::{
|
||||
@@ -216,11 +216,13 @@ impl Cipher {
|
||||
Some(p) if p.is_string() => Some(d.data),
|
||||
_ => None,
|
||||
})
|
||||
.map(|d| match d.get("lastUsedDate").and_then(|l| l.as_str()) {
|
||||
Some(l) if DateTime::parse_from_rfc3339(l).is_ok() => d,
|
||||
.map(|mut d| match d.get("lastUsedDate").and_then(|l| l.as_str()) {
|
||||
Some(l) => {
|
||||
d["lastUsedDate"] = json!(crate::util::validate_and_format_date(l));
|
||||
d
|
||||
}
|
||||
_ => {
|
||||
let mut d = d;
|
||||
d["lastUsedDate"] = json!("1970-01-01T00:00:00.000Z");
|
||||
d["lastUsedDate"] = json!("1970-01-01T00:00:00.000000Z");
|
||||
d
|
||||
}
|
||||
})
|
||||
|
||||
+48
-9
@@ -96,7 +96,31 @@ fn smtp_transport() -> AsyncSmtpTransport<Tokio1Executor> {
|
||||
smtp_client.build()
|
||||
}
|
||||
|
||||
// This will sanitize the string values by stripping all the html tags to prevent XSS and HTML Injections
|
||||
fn sanitize_data(data: &mut serde_json::Value) {
|
||||
use regex::Regex;
|
||||
use std::sync::LazyLock;
|
||||
static RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"<[^>]+>").unwrap());
|
||||
|
||||
match data {
|
||||
serde_json::Value::String(s) => *s = RE.replace_all(s, "").to_string(),
|
||||
serde_json::Value::Object(obj) => {
|
||||
for d in obj.values_mut() {
|
||||
sanitize_data(d);
|
||||
}
|
||||
}
|
||||
serde_json::Value::Array(arr) => {
|
||||
for d in arr.iter_mut() {
|
||||
sanitize_data(d);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_text(template_name: &'static str, data: serde_json::Value) -> Result<(String, String, String), Error> {
|
||||
let mut data = data;
|
||||
sanitize_data(&mut data);
|
||||
let (subject_html, body_html) = get_template(&format!("{template_name}.html"), &data)?;
|
||||
let (_subject_text, body_text) = get_template(template_name, &data)?;
|
||||
Ok((subject_html, body_html, body_text))
|
||||
@@ -116,6 +140,10 @@ fn get_template(template_name: &str, data: &serde_json::Value) -> Result<(String
|
||||
None => err!("Template doesn't contain body"),
|
||||
};
|
||||
|
||||
if text_split.next().is_some() {
|
||||
err!("Template contains more than one body");
|
||||
}
|
||||
|
||||
Ok((subject, body))
|
||||
}
|
||||
|
||||
@@ -259,16 +287,15 @@ pub async fn send_invite(
|
||||
}
|
||||
|
||||
let query_string = match query.query() {
|
||||
None => err!(format!("Failed to build invite URL query parameters")),
|
||||
None => err!("Failed to build invite URL query parameters"),
|
||||
Some(query) => query,
|
||||
};
|
||||
|
||||
// `url.Url` would place the anchor `#` after the query parameters
|
||||
let url = format!("{}/#/accept-organization/?{}", CONFIG.domain(), query_string);
|
||||
let (subject, body_html, body_text) = get_text(
|
||||
"email/send_org_invite",
|
||||
json!({
|
||||
"url": url,
|
||||
// `url.Url` would place the anchor `#` after the query parameters
|
||||
"url": format!("{}/#/accept-organization/?{}", CONFIG.domain(), query_string),
|
||||
"img_src": CONFIG._smtp_img_src(),
|
||||
"org_name": org_name,
|
||||
}),
|
||||
@@ -292,17 +319,29 @@ pub async fn send_emergency_access_invite(
|
||||
String::from(grantor_email),
|
||||
);
|
||||
|
||||
let invite_token = encode_jwt(&claims);
|
||||
// Build the query here to ensure proper escaping
|
||||
let mut query = url::Url::parse("https://query.builder").unwrap();
|
||||
{
|
||||
let mut query_params = query.query_pairs_mut();
|
||||
query_params
|
||||
.append_pair("id", emer_id)
|
||||
.append_pair("name", grantor_name)
|
||||
.append_pair("email", address)
|
||||
.append_pair("token", &encode_jwt(&claims));
|
||||
}
|
||||
|
||||
let query_string = match query.query() {
|
||||
None => err!("Failed to build emergency invite URL query parameters"),
|
||||
Some(query) => query,
|
||||
};
|
||||
|
||||
let (subject, body_html, body_text) = get_text(
|
||||
"email/send_emergency_access_invite",
|
||||
json!({
|
||||
"url": CONFIG.domain(),
|
||||
// `url.Url` would place the anchor `#` after the query parameters
|
||||
"url": format!("{}/#/accept-emergency/?{query_string}", CONFIG.domain()),
|
||||
"img_src": CONFIG._smtp_img_src(),
|
||||
"emer_id": emer_id,
|
||||
"email": percent_encode(address.as_bytes(), NON_ALPHANUMERIC).to_string(),
|
||||
"grantor_name": grantor_name,
|
||||
"token": invite_token,
|
||||
}),
|
||||
)?;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ Emergency access for {{{grantor_name}}}
|
||||
<!---------------->
|
||||
You have been invited to become an emergency contact for {{grantor_name}}. To accept this invite, click the following link:
|
||||
|
||||
Click here to join: {{url}}/#/accept-emergency/?id={{emer_id}}&name={{grantor_name}}&email={{email}}&token={{token}}
|
||||
Click here to join: {{{url}}}
|
||||
|
||||
If you do not wish to become an emergency contact for {{grantor_name}}, you can safely ignore this email.
|
||||
{{> email/email_footer_text }}
|
||||
|
||||
@@ -9,7 +9,7 @@ Emergency access for {{{grantor_name}}}
|
||||
</tr>
|
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
|
||||
<a href="{{url}}/#/accept-emergency/?id={{emer_id}}&name={{grantor_name}}&email={{email}}&token={{token}}"
|
||||
<a href="{{{url}}}"
|
||||
clicktracking=off target="_blank" style="color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #3c8dbc; border-color: #3c8dbc; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
Become emergency contact
|
||||
</a>
|
||||
@@ -21,4 +21,4 @@ Emergency access for {{{grantor_name}}}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{{> email/email_footer }}
|
||||
{{> email/email_footer }}
|
||||
|
||||
+11
-5
@@ -438,13 +438,19 @@ pub fn get_env_bool(key: &str) -> Option<bool> {
|
||||
|
||||
use chrono::{DateTime, Local, NaiveDateTime, TimeZone};
|
||||
|
||||
// Format used by Bitwarden API
|
||||
const DATETIME_FORMAT: &str = "%Y-%m-%dT%H:%M:%S%.6fZ";
|
||||
|
||||
/// Formats a UTC-offset `NaiveDateTime` in the format used by Bitwarden API
|
||||
/// responses with "date" fields (`CreationDate`, `RevisionDate`, etc.).
|
||||
pub fn format_date(dt: &NaiveDateTime) -> String {
|
||||
dt.format(DATETIME_FORMAT).to_string()
|
||||
dt.and_utc().to_rfc3339_opts(chrono::SecondsFormat::Micros, true)
|
||||
}
|
||||
|
||||
/// Validates and formats a RFC3339 timestamp
|
||||
/// If parsing fails it will return the start of the unix datetime
|
||||
pub fn validate_and_format_date(dt: &str) -> String {
|
||||
match DateTime::parse_from_rfc3339(dt) {
|
||||
Ok(dt) => dt.to_rfc3339_opts(chrono::SecondsFormat::Micros, true),
|
||||
_ => String::from("1970-01-01T00:00:00.000000Z"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Formats a `DateTime<Local>` using the specified format string.
|
||||
@@ -486,7 +492,7 @@ pub fn format_datetime_http(dt: &DateTime<Local>) -> String {
|
||||
}
|
||||
|
||||
pub fn parse_date(date: &str) -> NaiveDateTime {
|
||||
NaiveDateTime::parse_from_str(date, DATETIME_FORMAT).unwrap()
|
||||
DateTime::parse_from_rfc3339(date).unwrap().naive_utc()
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user