mirror of
https://github.com/dani-garcia/vaultwarden.git
synced 2025-09-10 18:55:57 +03:00
Compare commits
41 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
08a445e2ac | ||
|
c0b2877da3 | ||
|
cf8ca85289 | ||
|
a8a92f6c51 | ||
|
95f833aacd | ||
|
4f45cc081f | ||
|
2a4cd24c60 | ||
|
4545f271c3 | ||
|
2768396a72 | ||
|
5521a86693 | ||
|
3160780549 | ||
|
f0701657a9 | ||
|
21325b7523 | ||
|
874f5c34bd | ||
|
eadab2e9ca | ||
|
253faaf023 | ||
|
3d843a6a51 | ||
|
03fdf36bf9 | ||
|
fdcc32beda | ||
|
bf20355c5e | ||
|
0136c793b4 | ||
|
2e12114350 | ||
|
f25ab42ebb | ||
|
d3a8a278e6 | ||
|
8d9827c55f | ||
|
cad63f9761 | ||
|
bf446f44f9 | ||
|
621f607297 | ||
|
d89bd707a8 | ||
|
754087b990 | ||
|
cfbeb56371 | ||
|
3bb46ce496 | ||
|
c5832f2b30 | ||
|
d9406b0095 | ||
|
2475c36a75 | ||
|
c384f9c0ca | ||
|
afbfebf659 | ||
|
6b686c18f7 | ||
|
84fb6aaddb | ||
|
8526055bb7 | ||
|
a79334ea4c |
@@ -35,7 +35,7 @@
|
||||
## Enable extended logging
|
||||
## This shows timestamps and allows logging to file and to syslog
|
||||
### To enable logging to file, use the LOG_FILE env variable
|
||||
### To enable syslog, you need to compile with `cargo build --features=enable_syslog'
|
||||
### To enable syslog, use the USE_SYSLOG env variable
|
||||
# EXTENDED_LOGGING=true
|
||||
|
||||
## Logging to file
|
||||
@@ -43,6 +43,17 @@
|
||||
## It's recommended to also set 'ROCKET_CLI_COLORS=off'
|
||||
# LOG_FILE=/path/to/log
|
||||
|
||||
## Logging to Syslog
|
||||
## This requires extended logging
|
||||
## It's recommended to also set 'ROCKET_CLI_COLORS=off'
|
||||
# USE_SYSLOG=false
|
||||
|
||||
## Log level
|
||||
## Change the verbosity of the log output
|
||||
## Valid values are "trace", "debug", "info", "warn", "error" and "off"
|
||||
## This requires extended logging
|
||||
# LOG_LEVEL=Info
|
||||
|
||||
## Enable WAL for the DB
|
||||
## Set to false to avoid enabling WAL during startup.
|
||||
## Note that if the DB already has WAL enabled, you will also need to disable WAL in the DB,
|
||||
@@ -106,6 +117,17 @@
|
||||
# YUBICO_SECRET_KEY=AAAAAAAAAAAAAAAAAAAAAAAA
|
||||
# YUBICO_SERVER=http://yourdomain.com/wsapi/2.0/verify
|
||||
|
||||
## Duo Settings
|
||||
## You need to configure all options to enable global Duo support, otherwise users would need to configure it themselves
|
||||
## Create an account and protect an application as mentioned in this link (only the first step, not the rest):
|
||||
## https://help.bitwarden.com/article/setup-two-step-login-duo/#create-a-duo-security-account
|
||||
## Then set the following options, based on the values obtained from the last step:
|
||||
# DUO_IKEY=<Integration Key>
|
||||
# DUO_SKEY=<Secret Key>
|
||||
# DUO_HOST=<API Hostname>
|
||||
## After that, you should be able to follow the rest of the guide linked above,
|
||||
## ignoring the fields that ask for the values that you already configured beforehand.
|
||||
|
||||
## Rocket specific settings, check Rocket documentation to learn more
|
||||
# ROCKET_ENV=staging
|
||||
# ROCKET_ADDRESS=0.0.0.0 # Enable this to test mobile app
|
||||
|
1079
Cargo.lock
generated
1079
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
48
Cargo.toml
48
Cargo.toml
@@ -11,21 +11,25 @@ publish = false
|
||||
build = "build.rs"
|
||||
|
||||
[features]
|
||||
enable_syslog = ["syslog", "fern/syslog-4"]
|
||||
# Empty to keep compatibility, prefer to set USE_SYSLOG=true
|
||||
enable_syslog = []
|
||||
|
||||
[target."cfg(not(windows))".dependencies]
|
||||
syslog = "4.0.1"
|
||||
|
||||
[dependencies]
|
||||
# Web framework for nightly with a focus on ease-of-use, expressibility, and speed.
|
||||
rocket = { version = "0.4.0", features = ["tls"], default-features = false }
|
||||
rocket_contrib = "0.4.0"
|
||||
rocket = { version = "0.5.0-dev", features = ["tls"], default-features = false }
|
||||
rocket_contrib = "0.5.0-dev"
|
||||
|
||||
# HTTP client
|
||||
reqwest = "0.9.12"
|
||||
reqwest = "0.9.17"
|
||||
|
||||
# multipart/form-data support
|
||||
multipart = { version = "0.16.1", features = ["server"], default-features = false }
|
||||
|
||||
# WebSockets library
|
||||
ws = "0.8.0"
|
||||
ws = "0.8.1"
|
||||
|
||||
# MessagePack library
|
||||
rmpv = "0.4.0"
|
||||
@@ -34,14 +38,13 @@ rmpv = "0.4.0"
|
||||
chashmap = "2.2.2"
|
||||
|
||||
# A generic serialization/deserialization framework
|
||||
serde = "1.0.89"
|
||||
serde_derive = "1.0.89"
|
||||
serde = "1.0.91"
|
||||
serde_derive = "1.0.91"
|
||||
serde_json = "1.0.39"
|
||||
|
||||
# Logging
|
||||
log = "0.4.6"
|
||||
fern = "0.5.7"
|
||||
syslog = { version = "4.0.1", optional = true }
|
||||
fern = { version = "0.5.8", features = ["syslog-4"] }
|
||||
|
||||
# A safe, extensible ORM and Query builder
|
||||
diesel = { version = "1.4.2", features = ["sqlite", "chrono", "r2d2"] }
|
||||
@@ -51,10 +54,10 @@ diesel_migrations = { version = "1.4.0", features = ["sqlite"] }
|
||||
libsqlite3-sys = { version = "0.12.0", features = ["bundled"] }
|
||||
|
||||
# Crypto library
|
||||
ring = { version = "0.13.5", features = ["rsa_signing"] }
|
||||
ring = "0.14.6"
|
||||
|
||||
# UUID generation
|
||||
uuid = { version = "0.7.2", features = ["v4"] }
|
||||
uuid = { version = "0.7.4", features = ["v4"] }
|
||||
|
||||
# Date and time library for Rust
|
||||
chrono = "0.4.6"
|
||||
@@ -66,16 +69,16 @@ oath = "0.10.2"
|
||||
data-encoding = "2.1.2"
|
||||
|
||||
# JWT library
|
||||
jsonwebtoken = "5.0.1"
|
||||
jsonwebtoken = "6.0.1"
|
||||
|
||||
# U2F library
|
||||
u2f = "0.1.4"
|
||||
u2f = "0.1.6"
|
||||
|
||||
# Yubico Library
|
||||
yubico = { version = "0.5.1", features = ["online"], default-features = false }
|
||||
|
||||
# A `dotenv` implementation for Rust
|
||||
dotenv = { version = "0.13.0", default-features = false }
|
||||
dotenv = { version = "0.14.1", default-features = false }
|
||||
|
||||
# Lazy static macro
|
||||
lazy_static = "1.3.0"
|
||||
@@ -85,20 +88,25 @@ derive_more = "0.14.0"
|
||||
|
||||
# Numerical libraries
|
||||
num-traits = "0.2.6"
|
||||
num-derive = "0.2.4"
|
||||
num-derive = "0.2.5"
|
||||
|
||||
# Email libraries
|
||||
lettre = "0.9.0"
|
||||
lettre_email = "0.9.0"
|
||||
native-tls = "0.2.2"
|
||||
lettre = "0.9.1"
|
||||
lettre_email = "0.9.1"
|
||||
native-tls = "0.2.3"
|
||||
quoted_printable = "0.4.0"
|
||||
|
||||
# Template library
|
||||
handlebars = "1.1.0"
|
||||
|
||||
# For favicon extraction from main website
|
||||
soup = "0.3.0"
|
||||
regex = "1.1.2"
|
||||
soup = "0.4.1"
|
||||
regex = "1.1.6"
|
||||
|
||||
[patch.crates-io]
|
||||
# Add support for Timestamp type
|
||||
rmp = { git = 'https://github.com/dani-garcia/msgpack-rust' }
|
||||
|
||||
# Use newest ring
|
||||
rocket = { git = 'https://github.com/SergioBenitez/Rocket', rev = 'dbcb0a75b9556763ac3ab708f40c8f8ed75f1a1e' }
|
||||
rocket_contrib = { git = 'https://github.com/SergioBenitez/Rocket', rev = 'dbcb0a75b9556763ac3ab708f40c8f8ed75f1a1e' }
|
||||
|
@@ -4,7 +4,7 @@
|
||||
####################### VAULT BUILD IMAGE #######################
|
||||
FROM alpine as vault
|
||||
|
||||
ENV VAULT_VERSION "v2.9.0"
|
||||
ENV VAULT_VERSION "v2.10.1"
|
||||
|
||||
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
|
||||
|
||||
|
@@ -4,7 +4,7 @@
|
||||
####################### VAULT BUILD IMAGE #######################
|
||||
FROM alpine as vault
|
||||
|
||||
ENV VAULT_VERSION "v2.9.0"
|
||||
ENV VAULT_VERSION "v2.10.1"
|
||||
|
||||
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
|
||||
|
||||
|
@@ -4,7 +4,7 @@
|
||||
####################### VAULT BUILD IMAGE #######################
|
||||
FROM alpine as vault
|
||||
|
||||
ENV VAULT_VERSION "v2.9.0"
|
||||
ENV VAULT_VERSION "v2.10.1"
|
||||
|
||||
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
|
||||
|
||||
@@ -38,7 +38,7 @@ RUN cargo build --release
|
||||
######################## RUNTIME IMAGE ########################
|
||||
# Create a new stage with a minimal image
|
||||
# because we already have a binary built
|
||||
FROM alpine:3.8
|
||||
FROM alpine:3.9
|
||||
|
||||
ENV ROCKET_ENV "staging"
|
||||
ENV ROCKET_PORT=80
|
||||
|
@@ -4,7 +4,7 @@
|
||||
####################### VAULT BUILD IMAGE #######################
|
||||
FROM alpine as vault
|
||||
|
||||
ENV VAULT_VERSION "v2.9.0"
|
||||
ENV VAULT_VERSION "v2.10.1"
|
||||
|
||||
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
|
||||
|
||||
|
@@ -4,7 +4,7 @@
|
||||
####################### VAULT BUILD IMAGE #######################
|
||||
FROM alpine as vault
|
||||
|
||||
ENV VAULT_VERSION "v2.9.0"
|
||||
ENV VAULT_VERSION "v2.10.1"
|
||||
|
||||
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
|
||||
|
||||
|
@@ -3,7 +3,7 @@
|
||||
---
|
||||
|
||||
[](https://travis-ci.org/dani-garcia/bitwarden_rs)
|
||||
[](https://hub.docker.com/r/mprasil/bitwarden)
|
||||
[](https://hub.docker.com/r/bitwardenrs/server)
|
||||
[](https://deps.rs/repo/github/dani-garcia/bitwarden_rs)
|
||||
[](https://github.com/dani-garcia/bitwarden_rs/releases/latest)
|
||||
[](https://github.com/dani-garcia/bitwarden_rs/blob/master/LICENSE.txt)
|
||||
@@ -34,8 +34,8 @@ Basically full implementation of Bitwarden API is provided including:
|
||||
Pull the docker image and mount a volume from the host for persistent storage:
|
||||
|
||||
```sh
|
||||
docker pull mprasil/bitwarden:latest
|
||||
docker run -d --name bitwarden -v /bw-data/:/data/ -p 80:80 mprasil/bitwarden:latest
|
||||
docker pull bitwardenrs/server:latest
|
||||
docker run -d --name bitwarden -v /bw-data/:/data/ -p 80:80 bitwardenrs/server:latest
|
||||
```
|
||||
This will preserve any persistent data under /bw-data/, you can adapt the path to whatever suits you.
|
||||
|
||||
|
@@ -1 +1 @@
|
||||
nightly-2019-03-14
|
||||
nightly-2019-05-11
|
||||
|
@@ -6,7 +6,7 @@ use rocket::response::{content::Html, Flash, Redirect};
|
||||
use rocket::{Outcome, Route};
|
||||
use rocket_contrib::json::Json;
|
||||
|
||||
use crate::api::{ApiResult, EmptyResult};
|
||||
use crate::api::{ApiResult, EmptyResult, JsonResult};
|
||||
use crate::auth::{decode_admin, encode_jwt, generate_admin_claims, ClientIp};
|
||||
use crate::config::ConfigBuilder;
|
||||
use crate::db::{models::*, DbConn};
|
||||
@@ -21,6 +21,7 @@ pub fn routes() -> Vec<Route> {
|
||||
|
||||
routes![
|
||||
admin_login,
|
||||
get_users,
|
||||
post_admin_login,
|
||||
admin_page,
|
||||
invite_user,
|
||||
@@ -144,9 +145,10 @@ fn invite_user(data: Json<InviteData>, _token: AdminToken, conn: DbConn) -> Empt
|
||||
err!("Invitations are not allowed")
|
||||
}
|
||||
|
||||
let mut user = User::new(email);
|
||||
user.save(&conn)?;
|
||||
|
||||
if CONFIG.mail_enabled() {
|
||||
let mut user = User::new(email);
|
||||
user.save(&conn)?;
|
||||
let org_name = "bitwarden_rs";
|
||||
mail::send_invite(&user.email, &user.uuid, None, None, &org_name, None)
|
||||
} else {
|
||||
@@ -155,6 +157,14 @@ fn invite_user(data: Json<InviteData>, _token: AdminToken, conn: DbConn) -> Empt
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/users")]
|
||||
fn get_users(_token: AdminToken, conn: DbConn) -> JsonResult {
|
||||
let users = User::get_all(&conn);
|
||||
let users_json: Vec<Value> = users.iter().map(|u| u.to_json(&conn)).collect();
|
||||
|
||||
Ok(Json(Value::Array(users_json)))
|
||||
}
|
||||
|
||||
#[post("/users/<uuid>/delete")]
|
||||
fn delete_user(uuid: String, _token: AdminToken, conn: DbConn) -> EmptyResult {
|
||||
let user = match User::find_by_uuid(&uuid, &conn) {
|
||||
|
@@ -870,8 +870,20 @@ fn move_cipher_selected_put(
|
||||
move_cipher_selected(data, headers, conn, nt)
|
||||
}
|
||||
|
||||
#[post("/ciphers/purge", data = "<data>")]
|
||||
fn delete_all(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult {
|
||||
#[derive(FromForm)]
|
||||
struct OrganizationId {
|
||||
#[form(field = "organizationId")]
|
||||
org_id: String,
|
||||
}
|
||||
|
||||
#[post("/ciphers/purge?<organization..>", data = "<data>")]
|
||||
fn delete_all(
|
||||
organization: Option<Form<OrganizationId>>,
|
||||
data: JsonUpcase<PasswordData>,
|
||||
headers: Headers,
|
||||
conn: DbConn,
|
||||
nt: Notify,
|
||||
) -> EmptyResult {
|
||||
let data: PasswordData = data.into_inner().data;
|
||||
let password_hash = data.MasterPasswordHash;
|
||||
|
||||
@@ -881,19 +893,40 @@ fn delete_all(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn, nt
|
||||
err!("Invalid password")
|
||||
}
|
||||
|
||||
// Delete ciphers and their attachments
|
||||
for cipher in Cipher::find_owned_by_user(&user.uuid, &conn) {
|
||||
cipher.delete(&conn)?;
|
||||
}
|
||||
match organization {
|
||||
Some(org_data) => {
|
||||
// Organization ID in query params, purging organization vault
|
||||
match UserOrganization::find_by_user_and_org(&user.uuid, &org_data.org_id, &conn) {
|
||||
None => err!("You don't have permission to purge the organization vault"),
|
||||
Some(user_org) => {
|
||||
if user_org.type_ == UserOrgType::Owner {
|
||||
Cipher::delete_all_by_organization(&org_data.org_id, &conn)?;
|
||||
Collection::delete_all_by_organization(&org_data.org_id, &conn)?;
|
||||
nt.send_user_update(UpdateType::Vault, &user);
|
||||
Ok(())
|
||||
} else {
|
||||
err!("You don't have permission to purge the organization vault");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// No organization ID in query params, purging user vault
|
||||
// Delete ciphers and their attachments
|
||||
for cipher in Cipher::find_owned_by_user(&user.uuid, &conn) {
|
||||
cipher.delete(&conn)?;
|
||||
}
|
||||
|
||||
// Delete folders
|
||||
for f in Folder::find_by_user(&user.uuid, &conn) {
|
||||
f.delete(&conn)?;
|
||||
}
|
||||
// Delete folders
|
||||
for f in Folder::find_by_user(&user.uuid, &conn) {
|
||||
f.delete(&conn)?;
|
||||
}
|
||||
|
||||
user.update_revision(&conn)?;
|
||||
nt.send_user_update(UpdateType::Vault, &user);
|
||||
Ok(())
|
||||
user.update_revision(&conn)?;
|
||||
nt.send_user_update(UpdateType::Vault, &user);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn _delete_cipher_by_uuid(uuid: &str, headers: &Headers, conn: &DbConn, nt: &Notify) -> EmptyResult {
|
||||
|
@@ -1,4 +1,4 @@
|
||||
use data_encoding::BASE32;
|
||||
use data_encoding::{BASE32, BASE64};
|
||||
use rocket_contrib::json::Json;
|
||||
use serde_json;
|
||||
use serde_json::Value;
|
||||
@@ -31,6 +31,9 @@ pub fn routes() -> Vec<Route> {
|
||||
generate_yubikey,
|
||||
activate_yubikey,
|
||||
activate_yubikey_put,
|
||||
get_duo,
|
||||
activate_duo,
|
||||
activate_duo_put,
|
||||
]
|
||||
}
|
||||
|
||||
@@ -231,7 +234,6 @@ pub fn validate_totp_code_str(totp_code: &str, secret: &str) -> EmptyResult {
|
||||
}
|
||||
|
||||
pub fn validate_totp_code(totp_code: u64, secret: &str) -> EmptyResult {
|
||||
use data_encoding::BASE32;
|
||||
use oath::{totp_raw_now, HashType};
|
||||
|
||||
let decoded_secret = match BASE32.decode(secret.as_bytes()) {
|
||||
@@ -714,3 +716,325 @@ pub fn validate_yubikey_login(response: &str, twofactor_data: &str) -> EmptyResu
|
||||
Err(_e) => err!("Failed to verify Yubikey against OTP server"),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct DuoData {
|
||||
host: String,
|
||||
ik: String,
|
||||
sk: String,
|
||||
}
|
||||
|
||||
impl DuoData {
|
||||
fn global() -> Option<Self> {
|
||||
match CONFIG.duo_host() {
|
||||
Some(host) => Some(Self {
|
||||
host,
|
||||
ik: CONFIG.duo_ikey().unwrap(),
|
||||
sk: CONFIG.duo_skey().unwrap(),
|
||||
}),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
fn msg(s: &str) -> Self {
|
||||
Self {
|
||||
host: s.into(),
|
||||
ik: s.into(),
|
||||
sk: s.into(),
|
||||
}
|
||||
}
|
||||
fn secret() -> Self {
|
||||
Self::msg("<global_secret>")
|
||||
}
|
||||
fn obscure(self) -> Self {
|
||||
let mut host = self.host;
|
||||
let mut ik = self.ik;
|
||||
let mut sk = self.sk;
|
||||
|
||||
let digits = 4;
|
||||
let replaced = "************";
|
||||
|
||||
host.replace_range(digits.., replaced);
|
||||
ik.replace_range(digits.., replaced);
|
||||
sk.replace_range(digits.., replaced);
|
||||
|
||||
Self { host, ik, sk }
|
||||
}
|
||||
}
|
||||
|
||||
enum DuoStatus {
|
||||
Global(DuoData), // Using the global duo config
|
||||
User(DuoData), // Using the user's config
|
||||
Disabled(bool), // True if there is a global setting
|
||||
}
|
||||
|
||||
impl DuoStatus {
|
||||
fn data(self) -> Option<DuoData> {
|
||||
match self {
|
||||
DuoStatus::Global(data) => Some(data),
|
||||
DuoStatus::User(data) => Some(data),
|
||||
DuoStatus::Disabled(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
const DISABLED_MESSAGE_DEFAULT: &str = "<To use the global Duo keys, please leave these fields untouched>";
|
||||
|
||||
#[post("/two-factor/get-duo", data = "<data>")]
|
||||
fn get_duo(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||
let data: PasswordData = data.into_inner().data;
|
||||
|
||||
if !headers.user.check_valid_password(&data.MasterPasswordHash) {
|
||||
err!("Invalid password");
|
||||
}
|
||||
|
||||
let data = get_user_duo_data(&headers.user.uuid, &conn);
|
||||
|
||||
let (enabled, data) = match data {
|
||||
DuoStatus::Global(_) => (true, Some(DuoData::secret())),
|
||||
DuoStatus::User(data) => (true, Some(data.obscure())),
|
||||
DuoStatus::Disabled(true) => (false, Some(DuoData::msg(DISABLED_MESSAGE_DEFAULT))),
|
||||
DuoStatus::Disabled(false) => (false, None),
|
||||
};
|
||||
|
||||
let json = if let Some(data) = data {
|
||||
json!({
|
||||
"Enabled": enabled,
|
||||
"Host": data.host,
|
||||
"SecretKey": data.sk,
|
||||
"IntegrationKey": data.ik,
|
||||
"Object": "twoFactorDuo"
|
||||
})
|
||||
} else {
|
||||
json!({
|
||||
"Enabled": enabled,
|
||||
"Object": "twoFactorDuo"
|
||||
})
|
||||
};
|
||||
|
||||
Ok(Json(json))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(non_snake_case, dead_code)]
|
||||
struct EnableDuoData {
|
||||
MasterPasswordHash: String,
|
||||
Host: String,
|
||||
SecretKey: String,
|
||||
IntegrationKey: String,
|
||||
}
|
||||
|
||||
impl From<EnableDuoData> for DuoData {
|
||||
fn from(d: EnableDuoData) -> Self {
|
||||
Self {
|
||||
host: d.Host,
|
||||
ik: d.IntegrationKey,
|
||||
sk: d.SecretKey,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn check_duo_fields_custom(data: &EnableDuoData) -> bool {
|
||||
fn empty_or_default(s: &str) -> bool {
|
||||
let st = s.trim();
|
||||
st.is_empty() || s == DISABLED_MESSAGE_DEFAULT
|
||||
}
|
||||
|
||||
!empty_or_default(&data.Host) && !empty_or_default(&data.SecretKey) && !empty_or_default(&data.IntegrationKey)
|
||||
}
|
||||
|
||||
#[post("/two-factor/duo", data = "<data>")]
|
||||
fn activate_duo(data: JsonUpcase<EnableDuoData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||
let data: EnableDuoData = data.into_inner().data;
|
||||
|
||||
if !headers.user.check_valid_password(&data.MasterPasswordHash) {
|
||||
err!("Invalid password");
|
||||
}
|
||||
|
||||
let (data, data_str) = if check_duo_fields_custom(&data) {
|
||||
let data_req: DuoData = data.into();
|
||||
let data_str = serde_json::to_string(&data_req)?;
|
||||
duo_api_request("GET", "/auth/v2/check", "", &data_req).map_res("Failed to validate Duo credentials")?;
|
||||
(data_req.obscure(), data_str)
|
||||
} else {
|
||||
(DuoData::secret(), String::new())
|
||||
};
|
||||
|
||||
let type_ = TwoFactorType::Duo;
|
||||
let twofactor = TwoFactor::new(headers.user.uuid.clone(), type_, data_str);
|
||||
twofactor.save(&conn)?;
|
||||
|
||||
Ok(Json(json!({
|
||||
"Enabled": true,
|
||||
"Host": data.host,
|
||||
"SecretKey": data.sk,
|
||||
"IntegrationKey": data.ik,
|
||||
"Object": "twoFactorDuo"
|
||||
})))
|
||||
}
|
||||
|
||||
#[put("/two-factor/duo", data = "<data>")]
|
||||
fn activate_duo_put(data: JsonUpcase<EnableDuoData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||
activate_duo(data, headers, conn)
|
||||
}
|
||||
|
||||
fn duo_api_request(method: &str, path: &str, params: &str, data: &DuoData) -> EmptyResult {
|
||||
const AGENT: &str = "bitwarden_rs:Duo/1.0 (Rust)";
|
||||
|
||||
use reqwest::{header::*, Client, Method};
|
||||
use std::str::FromStr;
|
||||
|
||||
let url = format!("https://{}{}", &data.host, path);
|
||||
let date = Utc::now().to_rfc2822();
|
||||
let username = &data.ik;
|
||||
let fields = [&date, method, &data.host, path, params];
|
||||
let password = crypto::hmac_sign(&data.sk, &fields.join("\n"));
|
||||
|
||||
let m = Method::from_str(method).unwrap_or_default();
|
||||
|
||||
Client::new()
|
||||
.request(m, &url)
|
||||
.basic_auth(username, Some(password))
|
||||
.header(USER_AGENT, AGENT)
|
||||
.header(DATE, date)
|
||||
.send()?
|
||||
.error_for_status()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
const DUO_EXPIRE: i64 = 300;
|
||||
const APP_EXPIRE: i64 = 3600;
|
||||
|
||||
const AUTH_PREFIX: &str = "AUTH";
|
||||
const DUO_PREFIX: &str = "TX";
|
||||
const APP_PREFIX: &str = "APP";
|
||||
|
||||
use chrono::Utc;
|
||||
|
||||
fn get_user_duo_data(uuid: &str, conn: &DbConn) -> DuoStatus {
|
||||
let type_ = TwoFactorType::Duo as i32;
|
||||
|
||||
// If the user doesn't have an entry, disabled
|
||||
let twofactor = match TwoFactor::find_by_user_and_type(uuid, type_, &conn) {
|
||||
Some(t) => t,
|
||||
None => return DuoStatus::Disabled(DuoData::global().is_some()),
|
||||
};
|
||||
|
||||
// If the user has the required values, we use those
|
||||
if let Ok(data) = serde_json::from_str(&twofactor.data) {
|
||||
return DuoStatus::User(data);
|
||||
}
|
||||
|
||||
// Otherwise, we try to use the globals
|
||||
if let Some(global) = DuoData::global() {
|
||||
return DuoStatus::Global(global);
|
||||
}
|
||||
|
||||
// If there are no globals configured, just disable it
|
||||
DuoStatus::Disabled(false)
|
||||
}
|
||||
|
||||
// let (ik, sk, ak, host) = get_duo_keys();
|
||||
fn get_duo_keys_email(email: &str, conn: &DbConn) -> ApiResult<(String, String, String, String)> {
|
||||
let data = User::find_by_mail(email, &conn)
|
||||
.and_then(|u| get_user_duo_data(&u.uuid, &conn).data())
|
||||
.or_else(DuoData::global)
|
||||
.map_res("Can't fetch Duo keys")?;
|
||||
|
||||
Ok((data.ik, data.sk, CONFIG.get_duo_akey(), data.host))
|
||||
}
|
||||
|
||||
pub fn generate_duo_signature(email: &str, conn: &DbConn) -> ApiResult<(String, String)> {
|
||||
let now = Utc::now().timestamp();
|
||||
|
||||
let (ik, sk, ak, host) = get_duo_keys_email(email, conn)?;
|
||||
|
||||
let duo_sign = sign_duo_values(&sk, email, &ik, DUO_PREFIX, now + DUO_EXPIRE);
|
||||
let app_sign = sign_duo_values(&ak, email, &ik, APP_PREFIX, now + APP_EXPIRE);
|
||||
|
||||
Ok((format!("{}:{}", duo_sign, app_sign), host))
|
||||
}
|
||||
|
||||
fn sign_duo_values(key: &str, email: &str, ikey: &str, prefix: &str, expire: i64) -> String {
|
||||
let val = format!("{}|{}|{}", email, ikey, expire);
|
||||
let cookie = format!("{}|{}", prefix, BASE64.encode(val.as_bytes()));
|
||||
|
||||
format!("{}|{}", cookie, crypto::hmac_sign(key, &cookie))
|
||||
}
|
||||
|
||||
pub fn validate_duo_login(email: &str, response: &str, conn: &DbConn) -> EmptyResult {
|
||||
let split: Vec<&str> = response.split(':').collect();
|
||||
if split.len() != 2 {
|
||||
err!("Invalid response length");
|
||||
}
|
||||
|
||||
let auth_sig = split[0];
|
||||
let app_sig = split[1];
|
||||
|
||||
let now = Utc::now().timestamp();
|
||||
|
||||
let (ik, sk, ak, _host) = get_duo_keys_email(email, conn)?;
|
||||
|
||||
let auth_user = parse_duo_values(&sk, auth_sig, &ik, AUTH_PREFIX, now)?;
|
||||
let app_user = parse_duo_values(&ak, app_sig, &ik, APP_PREFIX, now)?;
|
||||
|
||||
if !crypto::ct_eq(&auth_user, app_user) || !crypto::ct_eq(&auth_user, email) {
|
||||
err!("Error validating duo authentication")
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_duo_values(key: &str, val: &str, ikey: &str, prefix: &str, time: i64) -> ApiResult<String> {
|
||||
let split: Vec<&str> = val.split('|').collect();
|
||||
if split.len() != 3 {
|
||||
err!("Invalid value length")
|
||||
}
|
||||
|
||||
let u_prefix = split[0];
|
||||
let u_b64 = split[1];
|
||||
let u_sig = split[2];
|
||||
|
||||
let sig = crypto::hmac_sign(key, &format!("{}|{}", u_prefix, u_b64));
|
||||
|
||||
if !crypto::ct_eq(crypto::hmac_sign(key, &sig), crypto::hmac_sign(key, u_sig)) {
|
||||
err!("Duo signatures don't match")
|
||||
}
|
||||
|
||||
if u_prefix != prefix {
|
||||
err!("Prefixes don't match")
|
||||
}
|
||||
|
||||
let cookie_vec = match BASE64.decode(u_b64.as_bytes()) {
|
||||
Ok(c) => c,
|
||||
Err(_) => err!("Invalid Duo cookie encoding"),
|
||||
};
|
||||
|
||||
let cookie = match String::from_utf8(cookie_vec) {
|
||||
Ok(c) => c,
|
||||
Err(_) => err!("Invalid Duo cookie encoding"),
|
||||
};
|
||||
|
||||
let cookie_split: Vec<&str> = cookie.split('|').collect();
|
||||
if cookie_split.len() != 3 {
|
||||
err!("Invalid cookie length")
|
||||
}
|
||||
|
||||
let username = cookie_split[0];
|
||||
let u_ikey = cookie_split[1];
|
||||
let expire = cookie_split[2];
|
||||
|
||||
if !crypto::ct_eq(ikey, u_ikey) {
|
||||
err!("Invalid ikey")
|
||||
}
|
||||
|
||||
let expire = match expire.parse() {
|
||||
Ok(e) => e,
|
||||
Err(_) => err!("Invalid expire time"),
|
||||
};
|
||||
|
||||
if time >= expire {
|
||||
err!("Expired authorization")
|
||||
}
|
||||
|
||||
Ok(username.into())
|
||||
}
|
||||
|
@@ -9,7 +9,7 @@ use num_traits::FromPrimitive;
|
||||
use crate::db::models::*;
|
||||
use crate::db::DbConn;
|
||||
|
||||
use crate::util::{self, JsonMap};
|
||||
use crate::util;
|
||||
|
||||
use crate::api::{ApiResult, EmptyResult, JsonResult};
|
||||
|
||||
@@ -178,6 +178,7 @@ fn twofactor_auth(
|
||||
Some(TwoFactorType::Authenticator) => _tf::validate_totp_code_str(twofactor_code, &selected_data?)?,
|
||||
Some(TwoFactorType::U2f) => _tf::validate_u2f_login(user_uuid, twofactor_code, conn)?,
|
||||
Some(TwoFactorType::YubiKey) => _tf::validate_yubikey_login(twofactor_code, &selected_data?)?,
|
||||
Some(TwoFactorType::Duo) => _tf::validate_duo_login(data.username.as_ref().unwrap(), twofactor_code, conn)?,
|
||||
|
||||
Some(TwoFactorType::Remember) => {
|
||||
match device.twofactor_remember {
|
||||
@@ -226,22 +227,33 @@ fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &DbConn) -> Api
|
||||
let mut challenge_list = Vec::new();
|
||||
|
||||
for key in request.registered_keys {
|
||||
let mut challenge_map = JsonMap::new();
|
||||
|
||||
challenge_map.insert("appId".into(), Value::String(request.app_id.clone()));
|
||||
challenge_map.insert("challenge".into(), Value::String(request.challenge.clone()));
|
||||
challenge_map.insert("version".into(), Value::String(key.version));
|
||||
challenge_map.insert("keyHandle".into(), Value::String(key.key_handle.unwrap_or_default()));
|
||||
|
||||
challenge_list.push(Value::Object(challenge_map));
|
||||
challenge_list.push(json!({
|
||||
"appId": request.app_id,
|
||||
"challenge": request.challenge,
|
||||
"version": key.version,
|
||||
"keyHandle": key.key_handle,
|
||||
}));
|
||||
}
|
||||
|
||||
let mut map = JsonMap::new();
|
||||
use serde_json;
|
||||
let challenge_list_str = serde_json::to_string(&challenge_list).unwrap();
|
||||
|
||||
map.insert("Challenges".into(), Value::String(challenge_list_str));
|
||||
result["TwoFactorProviders2"][provider.to_string()] = Value::Object(map);
|
||||
result["TwoFactorProviders2"][provider.to_string()] = json!({
|
||||
"Challenges": challenge_list_str,
|
||||
});
|
||||
}
|
||||
|
||||
Some(TwoFactorType::Duo) => {
|
||||
let email = match User::find_by_uuid(user_uuid, &conn) {
|
||||
Some(u) => u.email,
|
||||
None => err!("User does not exist"),
|
||||
};
|
||||
|
||||
let (signature, host) = two_factor::generate_duo_signature(&email, conn)?;
|
||||
|
||||
result["TwoFactorProviders2"][provider.to_string()] = json!({
|
||||
"Host": host,
|
||||
"Signature": signature,
|
||||
});
|
||||
}
|
||||
|
||||
Some(tf_type @ TwoFactorType::YubiKey) => {
|
||||
@@ -250,12 +262,11 @@ fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &DbConn) -> Api
|
||||
None => err!("No YubiKey devices registered"),
|
||||
};
|
||||
|
||||
let yubikey_metadata: two_factor::YubikeyMetadata =
|
||||
serde_json::from_str(&twofactor.data).expect("Can't parse Yubikey Metadata");
|
||||
let yubikey_metadata: two_factor::YubikeyMetadata = serde_json::from_str(&twofactor.data)?;
|
||||
|
||||
let mut map = JsonMap::new();
|
||||
map.insert("Nfc".into(), Value::Bool(yubikey_metadata.Nfc));
|
||||
result["TwoFactorProviders2"][provider.to_string()] = Value::Object(map);
|
||||
result["TwoFactorProviders2"][provider.to_string()] = json!({
|
||||
"Nfc": yubikey_metadata.Nfc,
|
||||
})
|
||||
}
|
||||
|
||||
_ => {}
|
||||
|
@@ -9,11 +9,12 @@ use rocket_contrib::json::Json;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::util::Cached;
|
||||
use crate::error::Error;
|
||||
use crate::CONFIG;
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
if CONFIG.web_vault_enabled() {
|
||||
routes![web_index, app_id, web_files, attachments, alive]
|
||||
routes![web_index, app_id, web_files, attachments, alive, images]
|
||||
} else {
|
||||
routes![attachments, alive]
|
||||
}
|
||||
@@ -62,3 +63,13 @@ fn alive() -> Json<String> {
|
||||
|
||||
Json(format_date(&Utc::now().naive_utc()))
|
||||
}
|
||||
|
||||
#[get("/bwrs_images/<filename>")]
|
||||
fn images(filename: String) -> Result<Content<Vec<u8>>, Error> {
|
||||
let image_type = ContentType::new("image", "png");
|
||||
match filename.as_ref() {
|
||||
"mail-github.png" => Ok(Content(image_type , include_bytes!("../static/images/mail-github.png").to_vec())),
|
||||
"logo-gray.png" => Ok(Content(image_type, include_bytes!("../static/images/logo-gray.png").to_vec())),
|
||||
_ => err!("Image not found")
|
||||
}
|
||||
}
|
@@ -40,7 +40,6 @@ fn decode_jwt<T: DeserializeOwned>(token: &str, issuer: String) -> Result<T, Err
|
||||
let validation = jsonwebtoken::Validation {
|
||||
leeway: 30, // 30 seconds
|
||||
validate_exp: true,
|
||||
validate_iat: false, // IssuedAt is the same as NotBefore
|
||||
validate_nbf: true,
|
||||
aud: None,
|
||||
iss: Some(issuer),
|
||||
|
@@ -9,7 +9,10 @@ lazy_static! {
|
||||
println!("Error loading config:\n\t{:?}\n", e);
|
||||
exit(12)
|
||||
});
|
||||
pub static ref CONFIG_FILE: String = get_env("CONFIG_FILE").unwrap_or_else(|| "data/config.json".into());
|
||||
pub static ref CONFIG_FILE: String = {
|
||||
let data_folder = get_env("DATA_FOLDER").unwrap_or_else(|| String::from("data"));
|
||||
get_env("CONFIG_FILE").unwrap_or_else(|| format!("{}/config.json", data_folder))
|
||||
};
|
||||
}
|
||||
|
||||
pub type Pass = String;
|
||||
@@ -61,7 +64,7 @@ macro_rules! make_config {
|
||||
|
||||
/// Merges the values of both builders into a new builder.
|
||||
/// If both have the same element, `other` wins.
|
||||
fn merge(&self, other: &Self) -> Self {
|
||||
fn merge(&self, other: &Self, show_overrides: bool) -> Self {
|
||||
let mut overrides = Vec::new();
|
||||
let mut builder = self.clone();
|
||||
$($(
|
||||
@@ -74,7 +77,7 @@ macro_rules! make_config {
|
||||
}
|
||||
)+)+
|
||||
|
||||
if !overrides.is_empty() {
|
||||
if show_overrides && !overrides.is_empty() {
|
||||
// We can't use warn! here because logging isn't setup yet.
|
||||
println!("[WARNING] The following environment variables are being overriden by the config file,");
|
||||
println!("[WARNING] please use the admin panel to make changes to them:");
|
||||
@@ -275,8 +278,12 @@ make_config! {
|
||||
log_mounts: bool, true, def, false;
|
||||
/// Enable extended logging
|
||||
extended_logging: bool, false, def, true;
|
||||
/// Enable the log to output to Syslog
|
||||
use_syslog: bool, false, def, false;
|
||||
/// Log file path
|
||||
log_file: String, false, option;
|
||||
/// Log level
|
||||
log_level: String, false, def, "Info".to_string();
|
||||
|
||||
/// Enable DB WAL |> Turning this off might lead to worse performance, but might help if using bitwarden_rs on some exotic filesystems,
|
||||
/// that do not support WAL. Please make sure you read project wiki on the topic before changing this setting.
|
||||
@@ -298,6 +305,20 @@ make_config! {
|
||||
yubico_server: String, true, option;
|
||||
},
|
||||
|
||||
/// Global Duo settings (Note that users can override them)
|
||||
duo: _enable_duo {
|
||||
/// Enabled
|
||||
_enable_duo: bool, true, def, false;
|
||||
/// Integration Key
|
||||
duo_ikey: String, true, option;
|
||||
/// Secret Key
|
||||
duo_skey: Pass, true, option;
|
||||
/// Host
|
||||
duo_host: String, true, option;
|
||||
/// Application Key (generated automatically)
|
||||
_duo_akey: Pass, false, option;
|
||||
},
|
||||
|
||||
/// SMTP Email Settings
|
||||
smtp: _enable_smtp {
|
||||
/// Enabled
|
||||
@@ -328,6 +349,12 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
|
||||
}
|
||||
}
|
||||
|
||||
if (cfg.duo_host.is_some() || cfg.duo_ikey.is_some() || cfg.duo_skey.is_some())
|
||||
&& !(cfg.duo_host.is_some() && cfg.duo_ikey.is_some() && cfg.duo_skey.is_some())
|
||||
{
|
||||
err!("All Duo options need to be set for global Duo support")
|
||||
}
|
||||
|
||||
if cfg.yubico_client_id.is_some() != cfg.yubico_secret_key.is_some() {
|
||||
err!("Both `YUBICO_CLIENT_ID` and `YUBICO_SECRET_KEY` need to be set for Yubikey OTP support")
|
||||
}
|
||||
@@ -350,7 +377,7 @@ impl Config {
|
||||
let _usr = ConfigBuilder::from_file(&CONFIG_FILE).unwrap_or_default();
|
||||
|
||||
// Create merged config, config file overwrites env
|
||||
let builder = _env.merge(&_usr);
|
||||
let builder = _env.merge(&_usr, true);
|
||||
|
||||
// Fill any missing with defaults
|
||||
let config = builder.build();
|
||||
@@ -379,7 +406,7 @@ impl Config {
|
||||
// Prepare the combined config
|
||||
let config = {
|
||||
let env = &self.inner.read().unwrap()._env;
|
||||
env.merge(&builder).build()
|
||||
env.merge(&builder, false).build()
|
||||
};
|
||||
validate_config(&config)?;
|
||||
|
||||
@@ -398,6 +425,14 @@ impl Config {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn update_config_partial(&self, other: ConfigBuilder) -> Result<(), Error> {
|
||||
let builder = {
|
||||
let usr = &self.inner.read().unwrap()._usr;
|
||||
usr.merge(&other, false)
|
||||
};
|
||||
self.update_config(builder)
|
||||
}
|
||||
|
||||
pub fn delete_user_config(&self) -> Result<(), Error> {
|
||||
crate::util::delete_file(&CONFIG_FILE)?;
|
||||
|
||||
@@ -433,9 +468,21 @@ impl Config {
|
||||
let inner = &self.inner.read().unwrap().config;
|
||||
inner._enable_smtp && inner.smtp_host.is_some()
|
||||
}
|
||||
pub fn yubico_enabled(&self) -> bool {
|
||||
let inner = &self.inner.read().unwrap().config;
|
||||
inner._enable_yubico && inner.yubico_client_id.is_some() && inner.yubico_secret_key.is_some()
|
||||
|
||||
pub fn get_duo_akey(&self) -> String {
|
||||
if let Some(akey) = self._duo_akey() {
|
||||
akey
|
||||
} else {
|
||||
let akey = crate::crypto::get_random_64();
|
||||
let akey_s = data_encoding::BASE64.encode(&akey);
|
||||
|
||||
// Save the new value
|
||||
let mut builder = ConfigBuilder::default();
|
||||
builder._duo_akey = Some(akey_s.clone());
|
||||
self.update_config_partial(builder).ok();
|
||||
|
||||
akey_s
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_template<T: serde::ser::Serialize>(
|
||||
|
@@ -2,7 +2,8 @@
|
||||
// PBKDF2 derivation
|
||||
//
|
||||
|
||||
use ring::{digest, pbkdf2};
|
||||
use ring::{digest, hmac, pbkdf2};
|
||||
use std::num::NonZeroU32;
|
||||
|
||||
static DIGEST_ALG: &digest::Algorithm = &digest::SHA256;
|
||||
const OUTPUT_LEN: usize = digest::SHA256_OUTPUT_LEN;
|
||||
@@ -10,15 +11,29 @@ const OUTPUT_LEN: usize = digest::SHA256_OUTPUT_LEN;
|
||||
pub fn hash_password(secret: &[u8], salt: &[u8], iterations: u32) -> Vec<u8> {
|
||||
let mut out = vec![0u8; OUTPUT_LEN]; // Initialize array with zeros
|
||||
|
||||
let iterations = NonZeroU32::new(iterations).expect("Iterations can't be zero");
|
||||
pbkdf2::derive(DIGEST_ALG, iterations, salt, secret, &mut out);
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
pub fn verify_password_hash(secret: &[u8], salt: &[u8], previous: &[u8], iterations: u32) -> bool {
|
||||
let iterations = NonZeroU32::new(iterations).expect("Iterations can't be zero");
|
||||
pbkdf2::verify(DIGEST_ALG, iterations, salt, secret, previous).is_ok()
|
||||
}
|
||||
|
||||
//
|
||||
// HMAC
|
||||
//
|
||||
pub fn hmac_sign(key: &str, data: &str) -> String {
|
||||
use data_encoding::HEXLOWER;
|
||||
|
||||
let key = hmac::SigningKey::new(&digest::SHA1, key.as_bytes());
|
||||
let signature = hmac::sign(&key, data.as_bytes());
|
||||
|
||||
HEXLOWER.encode(signature.as_ref())
|
||||
}
|
||||
|
||||
//
|
||||
// Random values
|
||||
//
|
||||
|
@@ -72,9 +72,7 @@ use crate::error::MapResult;
|
||||
/// Database methods
|
||||
impl Cipher {
|
||||
pub fn to_json(&self, host: &str, user_uuid: &str, conn: &DbConn) -> Value {
|
||||
use super::Attachment;
|
||||
use crate::util::format_date;
|
||||
use serde_json;
|
||||
|
||||
let attachments = Attachment::find_by_cipher(&self.uuid, conn);
|
||||
let attachments_json: Vec<Value> = attachments.iter().map(|c| c.to_json(host)).collect();
|
||||
|
@@ -37,6 +37,12 @@ pub struct User {
|
||||
pub client_kdf_iter: i32,
|
||||
}
|
||||
|
||||
enum UserStatus {
|
||||
Enabled = 0,
|
||||
Invited = 1,
|
||||
_Disabled = 2,
|
||||
}
|
||||
|
||||
/// Local methods
|
||||
impl User {
|
||||
pub const CLIENT_KDF_TYPE_DEFAULT: i32 = 0; // PBKDF2: 0
|
||||
@@ -113,14 +119,19 @@ use crate::error::MapResult;
|
||||
/// Database methods
|
||||
impl User {
|
||||
pub fn to_json(&self, conn: &DbConn) -> Value {
|
||||
use super::{TwoFactor, UserOrganization};
|
||||
|
||||
let orgs = UserOrganization::find_by_user(&self.uuid, conn);
|
||||
let orgs_json: Vec<Value> = orgs.iter().map(|c| c.to_json(&conn)).collect();
|
||||
let twofactor_enabled = !TwoFactor::find_by_user(&self.uuid, conn).is_empty();
|
||||
|
||||
// TODO: Might want to save the status field in the DB
|
||||
let status = if self.password_hash.is_empty() {
|
||||
UserStatus::Invited
|
||||
} else {
|
||||
UserStatus::Enabled
|
||||
};
|
||||
|
||||
json!({
|
||||
"_Enabled": !self.password_hash.is_empty(),
|
||||
"_Status": status as i32,
|
||||
"Id": self.uuid,
|
||||
"Name": self.name,
|
||||
"Email": self.email,
|
||||
|
15
src/error.rs
15
src/error.rs
@@ -41,6 +41,8 @@ use regex::Error as RegexErr;
|
||||
use reqwest::Error as ReqErr;
|
||||
use serde_json::{Error as SerdeErr, Value};
|
||||
use std::io::Error as IOErr;
|
||||
|
||||
use std::option::NoneError as NoneErr;
|
||||
use std::time::SystemTimeError as TimeErr;
|
||||
use u2f::u2ferror::U2fError as U2fErr;
|
||||
use yubico::yubicoerror::YubicoError as YubiErr;
|
||||
@@ -73,6 +75,13 @@ make_error! {
|
||||
YubiError(YubiErr): _has_source, _api_error,
|
||||
}
|
||||
|
||||
// This is implemented by hand because NoneError doesn't implement neither Display nor Error
|
||||
impl From<NoneErr> for Error {
|
||||
fn from(_: NoneErr) -> Self {
|
||||
Error::from(("NoneError", String::new()))
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Error {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self.source() {
|
||||
@@ -118,6 +127,12 @@ impl<E: Into<Error>> MapResult<()> for Result<usize, E> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> MapResult<S> for Option<S> {
|
||||
fn map_res(self, msg: &str) -> Result<S, Error> {
|
||||
self.ok_or_else(|| Error::new(msg, ""))
|
||||
}
|
||||
}
|
||||
|
||||
fn _has_source<T>(e: T) -> Option<T> {
|
||||
Some(e)
|
||||
}
|
||||
|
22
src/mail.rs
22
src/mail.rs
@@ -1,8 +1,9 @@
|
||||
use lettre::smtp::authentication::Credentials;
|
||||
use lettre::smtp::ConnectionReuseParameters;
|
||||
use lettre::{ClientSecurity, ClientTlsParameters, SmtpClient, SmtpTransport, Transport};
|
||||
use lettre_email::EmailBuilder;
|
||||
use lettre_email::{EmailBuilder, MimeMultipartType, PartBuilder};
|
||||
use native_tls::{Protocol, TlsConnector};
|
||||
use quoted_printable::encode_to_str;
|
||||
|
||||
use crate::api::EmptyResult;
|
||||
use crate::auth::{encode_jwt, generate_invite_claims};
|
||||
@@ -135,11 +136,28 @@ pub fn send_invite_confirmed(address: &str, org_name: &str) -> EmptyResult {
|
||||
}
|
||||
|
||||
fn send_email(address: &str, subject: &str, body_html: &str, body_text: &str) -> EmptyResult {
|
||||
let html = PartBuilder::new()
|
||||
.body(encode_to_str(body_html))
|
||||
.header(("Content-Type", "text/html; charset=utf-8"))
|
||||
.header(("Content-Transfer-Encoding", "quoted-printable"))
|
||||
.build();
|
||||
|
||||
let text = PartBuilder::new()
|
||||
.body(encode_to_str(body_text))
|
||||
.header(("Content-Type", "text/plain; charset=utf-8"))
|
||||
.header(("Content-Transfer-Encoding", "quoted-printable"))
|
||||
.build();
|
||||
|
||||
let alternative = PartBuilder::new()
|
||||
.message_type(MimeMultipartType::Alternative)
|
||||
.child(text)
|
||||
.child(html);
|
||||
|
||||
let email = EmailBuilder::new()
|
||||
.to(address)
|
||||
.from((CONFIG.smtp_from().as_str(), CONFIG.smtp_from_name().as_str()))
|
||||
.subject(subject)
|
||||
.alternative(body_html, body_text)
|
||||
.child(alternative.build())
|
||||
.build()
|
||||
.map_err(|e| Error::new("Error building email", e.to_string()))?;
|
||||
|
||||
|
28
src/main.rs
28
src/main.rs
@@ -69,6 +69,7 @@ fn launch_info() {
|
||||
}
|
||||
|
||||
fn init_logging() -> Result<(), fern::InitError> {
|
||||
use std::str::FromStr;
|
||||
let mut logger = fern::Dispatch::new()
|
||||
.format(|out, message, record| {
|
||||
out.finish(format_args!(
|
||||
@@ -79,31 +80,30 @@ fn init_logging() -> Result<(), fern::InitError> {
|
||||
message
|
||||
))
|
||||
})
|
||||
.level(log::LevelFilter::Debug)
|
||||
.level_for("hyper", log::LevelFilter::Warn)
|
||||
.level_for("rustls", log::LevelFilter::Warn)
|
||||
.level_for("handlebars", log::LevelFilter::Warn)
|
||||
.level_for("ws", log::LevelFilter::Info)
|
||||
.level_for("multipart", log::LevelFilter::Info)
|
||||
.level_for("html5ever", log::LevelFilter::Info)
|
||||
.level(log::LevelFilter::from_str(&CONFIG.log_level()).expect("Valid log level"))
|
||||
// Hide unknown certificate errors if using self-signed
|
||||
.level_for("rustls::session", log::LevelFilter::Off)
|
||||
// Hide failed to close stream messages
|
||||
.level_for("hyper::server", log::LevelFilter::Warn)
|
||||
.chain(std::io::stdout());
|
||||
|
||||
if let Some(log_file) = CONFIG.log_file() {
|
||||
logger = logger.chain(fern::log_file(log_file)?);
|
||||
}
|
||||
|
||||
logger = chain_syslog(logger);
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
if cfg!(feature = "enable_syslog") || CONFIG.use_syslog() {
|
||||
logger = chain_syslog(logger);
|
||||
}
|
||||
}
|
||||
|
||||
logger.apply()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "enable_syslog"))]
|
||||
fn chain_syslog(logger: fern::Dispatch) -> fern::Dispatch {
|
||||
logger
|
||||
}
|
||||
|
||||
#[cfg(feature = "enable_syslog")]
|
||||
#[cfg(not(windows))]
|
||||
fn chain_syslog(logger: fern::Dispatch) -> fern::Dispatch {
|
||||
let syslog_fmt = syslog::Formatter3164 {
|
||||
facility: syslog::Facility::LOG_USER,
|
||||
|
BIN
src/static/images/logo-gray.png
Normal file
BIN
src/static/images/logo-gray.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.4 KiB |
BIN
src/static/images/mail-github.png
Normal file
BIN
src/static/images/mail-github.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 KiB |
@@ -13,9 +13,9 @@
|
||||
{{#if TwoFactorEnabled}}
|
||||
<span class="badge badge-success ml-2">2FA</span>
|
||||
{{/if}}
|
||||
{{#unless _Enabled}}
|
||||
<span class="badge badge-warning ml-2">Disabled</span>
|
||||
{{/unless}}
|
||||
{{#case _Status 1}}
|
||||
<span class="badge badge-warning ml-2">Invited</span>
|
||||
{{/case}}
|
||||
<span class="d-block">{{Email}}</span>
|
||||
</div>
|
||||
<div class="col">
|
||||
|
@@ -82,7 +82,7 @@ Invitation accepted
|
||||
<table class="body-wrap" cellpadding="0" cellspacing="0" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; width: 100%;" bgcolor="#f6f6f6">
|
||||
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||
<td valign="middle" class="aligncenter middle logo" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; padding: 20px 0 10px;" align="center">
|
||||
<p style="text-align: center"><strong>Bitwarden_rs</strong></p>
|
||||
<img src="{{url}}/bwrs_images/logo-gray.png" alt="" width="250" height="39" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; border: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; max-width: 100%;" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||
@@ -118,11 +118,7 @@ Invitation accepted
|
||||
<td class="aligncenter social-icons" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; padding: 15px 0 0 0;" valign="top">
|
||||
<table cellpadding="0" cellspacing="0" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto;">
|
||||
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||
<td style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; padding: 0 10px;" valign="top">
|
||||
<a href="https://github.com/dani-garcia/bitwarden_rs" target="_blank" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; text-decoration: underline;">
|
||||
<p style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; border: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; max-width: 100%;">GitHub</p>
|
||||
</a>
|
||||
</td>
|
||||
<td style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; padding: 0 10px;" valign="top"><a href="https://github.com/dani-garcia/bitwarden_rs" target="_blank" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; text-decoration: underline;"><img src="{{url}}/bwrs_images/mail-github.png" alt="GitHub" width="30" height="30" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; border: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; max-width: 100%;" /></a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
|
@@ -82,7 +82,7 @@ Invitation to {{org_name}} confirmed
|
||||
<table class="body-wrap" cellpadding="0" cellspacing="0" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; width: 100%;" bgcolor="#f6f6f6">
|
||||
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||
<td valign="middle" class="aligncenter middle logo" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; padding: 20px 0 10px;" align="center">
|
||||
<p style="text-align: center"><strong>Bitwarden_rs</strong></p>
|
||||
<img src="{{url}}/bwrs_images/logo-gray.png" alt="" width="250" height="39" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; border: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; max-width: 100%;" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||
@@ -114,11 +114,7 @@ Invitation to {{org_name}} confirmed
|
||||
<td class="aligncenter social-icons" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; padding: 15px 0 0 0;" valign="top">
|
||||
<table cellpadding="0" cellspacing="0" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto;">
|
||||
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||
<td style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; padding: 0 10px;" valign="top">
|
||||
<a href="https://github.com/dani-garcia/bitwarden_rs" target="_blank" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; text-decoration: underline;">
|
||||
<p style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; border: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; max-width: 100%;">GitHub</p>
|
||||
</a>
|
||||
</td>
|
||||
<td style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; padding: 0 10px;" valign="top"><a href="https://github.com/dani-garcia/bitwarden_rs" target="_blank" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; text-decoration: underline;"><img src="{{url}}/bwrs_images/mail-github.png" alt="GitHub" width="30" height="30" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; border: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; max-width: 100%;" /></a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
|
@@ -82,7 +82,7 @@ Sorry, you have no password hint...
|
||||
<table class="body-wrap" cellpadding="0" cellspacing="0" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; width: 100%;" bgcolor="#f6f6f6">
|
||||
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||
<td valign="middle" class="aligncenter middle logo" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; padding: 20px 0 10px;" align="center">
|
||||
<p style="text-align: center"><strong>Bitwarden_rs</strong></p>
|
||||
<img src="{{url}}/bwrs_images/logo-gray.png" alt="" width="250" height="39" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; border: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; max-width: 100%;" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||
@@ -113,11 +113,7 @@ Sorry, you have no password hint...
|
||||
<td class="aligncenter social-icons" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; padding: 15px 0 0 0;" valign="top">
|
||||
<table cellpadding="0" cellspacing="0" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto;">
|
||||
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||
<td style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; padding: 0 10px;" valign="top">
|
||||
<a href="https://github.com/dani-garcia/bitwarden_rs" target="_blank" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; text-decoration: underline;">
|
||||
<p style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; border: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; max-width: 100%;">GitHub</p>
|
||||
</a>
|
||||
</td>
|
||||
<td style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; padding: 0 10px;" valign="top"><a href="https://github.com/dani-garcia/bitwarden_rs" target="_blank" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; text-decoration: underline;"><img src="{{url}}/bwrs_images/mail-github.png" alt="GitHub" width="30" height="30" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; border: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; max-width: 100%;" /></a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
|
@@ -3,5 +3,6 @@ Your master password hint
|
||||
You (or someone) recently requested your master password hint.
|
||||
|
||||
Your hint is: "{{hint}}"
|
||||
Log in: <a href="{{url}}">Web Vault</a>
|
||||
|
||||
If you did not request your master password hint you can safely ignore this email.
|
||||
|
@@ -82,7 +82,7 @@ Your master password hint
|
||||
<table class="body-wrap" cellpadding="0" cellspacing="0" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; width: 100%;" bgcolor="#f6f6f6">
|
||||
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||
<td valign="middle" class="aligncenter middle logo" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; padding: 20px 0 10px;" align="center">
|
||||
<p style="text-align: center"><strong>Bitwarden_rs</strong></p>
|
||||
<img src="{{url}}/bwrs_images/logo-gray.png" alt="" width="250" height="39" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; border: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; max-width: 100%;" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||
@@ -119,11 +119,7 @@ Your master password hint
|
||||
<td class="aligncenter social-icons" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; padding: 15px 0 0 0;" valign="top">
|
||||
<table cellpadding="0" cellspacing="0" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto;">
|
||||
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||
<td style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; padding: 0 10px;" valign="top">
|
||||
<a href="https://github.com/dani-garcia/bitwarden_rs" target="_blank" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; text-decoration: underline;">
|
||||
<p style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; border: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; max-width: 100%;">GitHub</p>
|
||||
</a>
|
||||
</td>
|
||||
<td style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; padding: 0 10px;" valign="top"><a href="https://github.com/dani-garcia/bitwarden_rs" target="_blank" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; text-decoration: underline;"><img src="{{url}}/bwrs_images/mail-github.png" alt="GitHub" width="30" height="30" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; border: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; max-width: 100%;" /></a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
|
@@ -82,7 +82,7 @@ Join {{org_name}}
|
||||
<table class="body-wrap" cellpadding="0" cellspacing="0" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; width: 100%;" bgcolor="#f6f6f6">
|
||||
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||
<td valign="middle" class="aligncenter middle logo" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; padding: 20px 0 10px;" align="center">
|
||||
<p style="text-align: center"><strong>Bitwarden_rs</strong></p>
|
||||
<img src="{{url}}/bwrs_images/logo-gray.png" alt="" width="250" height="39" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; border: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; max-width: 100%;" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||
@@ -121,11 +121,7 @@ Join {{org_name}}
|
||||
<td class="aligncenter social-icons" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; padding: 15px 0 0 0;" valign="top">
|
||||
<table cellpadding="0" cellspacing="0" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto;">
|
||||
<tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
|
||||
<td style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; padding: 0 10px;" valign="top">
|
||||
<a href="https://github.com/dani-garcia/bitwarden_rs" target="_blank" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; text-decoration: underline;">
|
||||
<p style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; border: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; max-width: 100%;">GitHub</p>
|
||||
</a>
|
||||
</td>
|
||||
<td style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; padding: 0 10px;" valign="top"><a href="https://github.com/dani-garcia/bitwarden_rs" target="_blank" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; text-decoration: underline;"><img src="{{url}}/bwrs_images/mail-github.png" alt="GitHub" width="30" height="30" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; border: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; max-width: 100%;" /></a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
|
Reference in New Issue
Block a user