Compare commits

..

2 Commits

Author SHA1 Message Date
Daniel García 38aad4f7be Limit HIBP to authed users 2024-11-10 23:59:06 +01:00
BlackDex 20d9e885bf Update crates and fix several issues
Signed-off-by: BlackDex <black.dex@gmail.com>
2024-11-10 23:56:19 +01:00
19 changed files with 281 additions and 2062 deletions
Generated
+109 -78
View File
File diff suppressed because it is too large Load Diff
+11 -11
View File
@@ -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"
-2
View File
@@ -1,2 +0,0 @@
[workspace.metadata.dylint]
libraries = [{ path = "dylints/*" }]
-7
View File
@@ -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
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"]
-167
View File
@@ -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
View File
@@ -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
View File
@@ -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
+7 -5
View File
@@ -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
View File
@@ -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
View File
@@ -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()
}
//