Compare commits

...

41 Commits
1.8.0 ... 1.9.1

Author SHA1 Message Date
Daniel García
08a445e2ac Merge pull request #484 from mprasil/hub_repo_change
Point to the new docker hub image location
2019-05-17 15:44:07 +02:00
Daniel García
c0b2877da3 Update deps and swap back to official u2f crate again 2019-05-17 15:39:36 +02:00
Miro Prasil
cf8ca85289 Point to the new docker hub image location 2019-05-16 15:04:51 +01:00
Daniel García
a8a92f6c51 New vault patch release 2019-05-15 18:11:39 +02:00
Daniel García
95f833aacd Update dependencies to use new ring 2019-05-15 18:10:25 +02:00
Daniel García
4f45cc081f Update ring to 0.14, jwt to 6.0, and u2f 2019-05-11 23:18:18 +02:00
Daniel García
2a4cd24c60 Updated web vault to hide org plans again and updated dependencies 2019-05-11 22:27:51 +02:00
Daniel García
4545f271c3 Merge pull request #473 from Starbix/patch-1
Update Runtime Base Image to Alpine v3.9
2019-05-02 22:33:43 +02:00
Cédric Laubacher
2768396a72 Update Runtime Base Image to Alpine v3.9 2019-05-02 21:28:34 +02:00
Daniel García
5521a86693 Change path for served images to avoid collision with vault images 2019-05-01 16:19:22 +02:00
Daniel García
3160780549 Merge pull request #401 from TheMardy/master
Images in Email Templates
2019-04-30 17:52:10 +02:00
TheMardy
f0701657a9 Changed to Bitwarden_RS Logo 2019-04-30 16:08:53 +02:00
Daniel García
21325b7523 Updated .env template 2019-04-27 20:14:37 +02:00
Daniel García
874f5c34bd Formatting 2019-04-26 22:08:26 +02:00
Daniel García
eadab2e9ca Updated dependencies 2019-04-26 22:07:00 +02:00
Daniel García
253faaf023 Use users duo host when required, instead of always using the global one 2019-04-15 13:07:23 +02:00
Daniel García
3d843a6a51 Merge pull request #460 from janost/organization-vault-purge
Fixed purging organization vault
2019-04-14 22:30:51 +02:00
janost
03fdf36bf9 Fixed purging organization vault 2019-04-14 22:12:48 +02:00
Daniel García
fdcc32beda Validate Duo credentials when custom 2019-04-14 22:05:05 +02:00
Daniel García
bf20355c5e Merge branch 'duo' 2019-04-14 22:02:55 +02:00
Daniel García
0136c793b4 Implement better user status API, in the future we'll probably want a way to disable users.
We should migrate from the empty password hash to a separate column then.
2019-04-13 00:01:52 +02:00
Daniel García
2e12114350 Always create the user when inviting from admin panel 2019-04-12 23:44:49 +02:00
Daniel García
f25ab42ebb Merge pull request #455 from ViViDboarder/get_users
Add new endpoint for retrieving all users
2019-04-11 20:42:44 +02:00
ViViDboarder
d3a8a278e6 Add new endpoint for retrieving all users 2019-04-11 11:24:53 -07:00
Daniel García
8d9827c55f Implement selection between global config and user settings for duo keys. 2019-04-11 18:40:03 +02:00
Daniel García
cad63f9761 Auto generate akey 2019-04-11 16:08:26 +02:00
Daniel García
bf446f44f9 Enable DATA_FOLDER to affect default CONFIG_FILE path 2019-04-11 15:41:13 +02:00
Daniel García
621f607297 Update dependencies and fix some warnings 2019-04-11 15:40:19 +02:00
Daniel García
d89bd707a8 Update vault release to show duo button 2019-04-07 18:58:32 +02:00
Daniel García
754087b990 Add global duo config and document options in .env template 2019-04-07 18:58:15 +02:00
Daniel García
cfbeb56371 Implement user duo, initial version
TODO:
- At the moment each user needs to configure a DUO application and input the API keys, we need to check if multiple users can register with the same keys correctly and if so we could implement a global setting.
- Sometimes the Duo frame doesn't load correctly, but canceling, reloading the page and logging in again seems to fix it for me.
2019-04-05 22:09:53 +02:00
Daniel García
3bb46ce496 Make the syslog crate non-optional when available 2019-04-02 22:35:22 +02:00
Daniel García
c5832f2b30 With the latest fern, syslog can be a config option instead of a build flag 2019-03-29 20:27:20 +01:00
Daniel García
d9406b0095 Update to web vault 2.10.0 2019-03-25 23:49:12 +01:00
Daniel García
2475c36a75 Implement log_level config option 2019-03-25 14:23:14 +01:00
Daniel García
c384f9c0ca Set default log level to Info, we don't use debug anyway and it just fills the logs with other crates info. 2019-03-25 14:21:50 +01:00
Daniel García
afbfebf659 Merge pull request #440 from BlackDex/mail-encoding
Fixed long e-mail message extending 1000 lines.
2019-03-25 13:09:51 +01:00
BlackDex
6b686c18f7 Fixed long e-mail message extending 1000 lines.
- Added quoted_printable crate to encode the e-mail messages.
- Change the way the e-mail gets build to use custom part headers.
2019-03-25 09:48:19 +01:00
TheMardy
84fb6aaddb Set correct MIME type 2019-02-17 01:08:24 +01:00
TheMardy
8526055bb7 Added images to email templates 2019-02-16 03:48:23 +01:00
TheMardy
a79334ea4c Added static email image routes 2019-02-16 03:44:30 +01:00
32 changed files with 1213 additions and 633 deletions

View File

@@ -35,7 +35,7 @@
## Enable extended logging ## Enable extended logging
## This shows timestamps and allows logging to file and to syslog ## This shows timestamps and allows logging to file and to syslog
### To enable logging to file, use the LOG_FILE env variable ### 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 # EXTENDED_LOGGING=true
## Logging to file ## Logging to file
@@ -43,6 +43,17 @@
## It's recommended to also set 'ROCKET_CLI_COLORS=off' ## It's recommended to also set 'ROCKET_CLI_COLORS=off'
# LOG_FILE=/path/to/log # 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 ## Enable WAL for the DB
## Set to false to avoid enabling WAL during startup. ## 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, ## 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_SECRET_KEY=AAAAAAAAAAAAAAAAAAAAAAAA
# YUBICO_SERVER=http://yourdomain.com/wsapi/2.0/verify # 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 specific settings, check Rocket documentation to learn more
# ROCKET_ENV=staging # ROCKET_ENV=staging
# ROCKET_ADDRESS=0.0.0.0 # Enable this to test mobile app # ROCKET_ADDRESS=0.0.0.0 # Enable this to test mobile app

1079
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,21 +11,25 @@ publish = false
build = "build.rs" build = "build.rs"
[features] [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] [dependencies]
# Web framework for nightly with a focus on ease-of-use, expressibility, and speed. # 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 = { version = "0.5.0-dev", features = ["tls"], default-features = false }
rocket_contrib = "0.4.0" rocket_contrib = "0.5.0-dev"
# HTTP client # HTTP client
reqwest = "0.9.12" reqwest = "0.9.17"
# multipart/form-data support # multipart/form-data support
multipart = { version = "0.16.1", features = ["server"], default-features = false } multipart = { version = "0.16.1", features = ["server"], default-features = false }
# WebSockets library # WebSockets library
ws = "0.8.0" ws = "0.8.1"
# MessagePack library # MessagePack library
rmpv = "0.4.0" rmpv = "0.4.0"
@@ -34,14 +38,13 @@ rmpv = "0.4.0"
chashmap = "2.2.2" chashmap = "2.2.2"
# A generic serialization/deserialization framework # A generic serialization/deserialization framework
serde = "1.0.89" serde = "1.0.91"
serde_derive = "1.0.89" serde_derive = "1.0.91"
serde_json = "1.0.39" serde_json = "1.0.39"
# Logging # Logging
log = "0.4.6" log = "0.4.6"
fern = "0.5.7" fern = { version = "0.5.8", features = ["syslog-4"] }
syslog = { version = "4.0.1", optional = true }
# A safe, extensible ORM and Query builder # A safe, extensible ORM and Query builder
diesel = { version = "1.4.2", features = ["sqlite", "chrono", "r2d2"] } 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"] } libsqlite3-sys = { version = "0.12.0", features = ["bundled"] }
# Crypto library # Crypto library
ring = { version = "0.13.5", features = ["rsa_signing"] } ring = "0.14.6"
# UUID generation # UUID generation
uuid = { version = "0.7.2", features = ["v4"] } uuid = { version = "0.7.4", features = ["v4"] }
# Date and time library for Rust # Date and time library for Rust
chrono = "0.4.6" chrono = "0.4.6"
@@ -66,16 +69,16 @@ oath = "0.10.2"
data-encoding = "2.1.2" data-encoding = "2.1.2"
# JWT library # JWT library
jsonwebtoken = "5.0.1" jsonwebtoken = "6.0.1"
# U2F library # U2F library
u2f = "0.1.4" u2f = "0.1.6"
# Yubico Library # Yubico Library
yubico = { version = "0.5.1", features = ["online"], default-features = false } yubico = { version = "0.5.1", features = ["online"], default-features = false }
# A `dotenv` implementation for Rust # 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 macro
lazy_static = "1.3.0" lazy_static = "1.3.0"
@@ -85,20 +88,25 @@ derive_more = "0.14.0"
# Numerical libraries # Numerical libraries
num-traits = "0.2.6" num-traits = "0.2.6"
num-derive = "0.2.4" num-derive = "0.2.5"
# Email libraries # Email libraries
lettre = "0.9.0" lettre = "0.9.1"
lettre_email = "0.9.0" lettre_email = "0.9.1"
native-tls = "0.2.2" native-tls = "0.2.3"
quoted_printable = "0.4.0"
# Template library # Template library
handlebars = "1.1.0" handlebars = "1.1.0"
# For favicon extraction from main website # For favicon extraction from main website
soup = "0.3.0" soup = "0.4.1"
regex = "1.1.2" regex = "1.1.6"
[patch.crates-io] [patch.crates-io]
# Add support for Timestamp type # Add support for Timestamp type
rmp = { git = 'https://github.com/dani-garcia/msgpack-rust' } 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' }

View File

@@ -4,7 +4,7 @@
####################### VAULT BUILD IMAGE ####################### ####################### VAULT BUILD IMAGE #######################
FROM alpine as vault 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" ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"

View File

@@ -4,7 +4,7 @@
####################### VAULT BUILD IMAGE ####################### ####################### VAULT BUILD IMAGE #######################
FROM alpine as vault 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" ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"

View File

@@ -4,7 +4,7 @@
####################### VAULT BUILD IMAGE ####################### ####################### VAULT BUILD IMAGE #######################
FROM alpine as vault 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" 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 ######################## ######################## RUNTIME IMAGE ########################
# Create a new stage with a minimal image # Create a new stage with a minimal image
# because we already have a binary built # because we already have a binary built
FROM alpine:3.8 FROM alpine:3.9
ENV ROCKET_ENV "staging" ENV ROCKET_ENV "staging"
ENV ROCKET_PORT=80 ENV ROCKET_PORT=80

View File

@@ -4,7 +4,7 @@
####################### VAULT BUILD IMAGE ####################### ####################### VAULT BUILD IMAGE #######################
FROM alpine as vault 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" ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"

View File

@@ -4,7 +4,7 @@
####################### VAULT BUILD IMAGE ####################### ####################### VAULT BUILD IMAGE #######################
FROM alpine as vault 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" ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"

View File

@@ -3,7 +3,7 @@
--- ---
[![Travis Build Status](https://travis-ci.org/dani-garcia/bitwarden_rs.svg?branch=master)](https://travis-ci.org/dani-garcia/bitwarden_rs) [![Travis Build Status](https://travis-ci.org/dani-garcia/bitwarden_rs.svg?branch=master)](https://travis-ci.org/dani-garcia/bitwarden_rs)
[![Docker Pulls](https://img.shields.io/docker/pulls/mprasil/bitwarden.svg)](https://hub.docker.com/r/mprasil/bitwarden) [![Docker Pulls](https://img.shields.io/docker/pulls/bitwardenrs/server.svg)](https://hub.docker.com/r/bitwardenrs/server)
[![Dependency Status](https://deps.rs/repo/github/dani-garcia/bitwarden_rs/status.svg)](https://deps.rs/repo/github/dani-garcia/bitwarden_rs) [![Dependency Status](https://deps.rs/repo/github/dani-garcia/bitwarden_rs/status.svg)](https://deps.rs/repo/github/dani-garcia/bitwarden_rs)
[![GitHub Release](https://img.shields.io/github/release/dani-garcia/bitwarden_rs.svg)](https://github.com/dani-garcia/bitwarden_rs/releases/latest) [![GitHub Release](https://img.shields.io/github/release/dani-garcia/bitwarden_rs.svg)](https://github.com/dani-garcia/bitwarden_rs/releases/latest)
[![GPL-3.0 Licensed](https://img.shields.io/github/license/dani-garcia/bitwarden_rs.svg)](https://github.com/dani-garcia/bitwarden_rs/blob/master/LICENSE.txt) [![GPL-3.0 Licensed](https://img.shields.io/github/license/dani-garcia/bitwarden_rs.svg)](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: Pull the docker image and mount a volume from the host for persistent storage:
```sh ```sh
docker pull mprasil/bitwarden:latest docker pull bitwardenrs/server:latest
docker run -d --name bitwarden -v /bw-data/:/data/ -p 80:80 mprasil/bitwarden: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. This will preserve any persistent data under /bw-data/, you can adapt the path to whatever suits you.

View File

@@ -1 +1 @@
nightly-2019-03-14 nightly-2019-05-11

View File

@@ -6,7 +6,7 @@ use rocket::response::{content::Html, Flash, Redirect};
use rocket::{Outcome, Route}; use rocket::{Outcome, Route};
use rocket_contrib::json::Json; 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::auth::{decode_admin, encode_jwt, generate_admin_claims, ClientIp};
use crate::config::ConfigBuilder; use crate::config::ConfigBuilder;
use crate::db::{models::*, DbConn}; use crate::db::{models::*, DbConn};
@@ -21,6 +21,7 @@ pub fn routes() -> Vec<Route> {
routes![ routes![
admin_login, admin_login,
get_users,
post_admin_login, post_admin_login,
admin_page, admin_page,
invite_user, invite_user,
@@ -144,9 +145,10 @@ fn invite_user(data: Json<InviteData>, _token: AdminToken, conn: DbConn) -> Empt
err!("Invitations are not allowed") err!("Invitations are not allowed")
} }
if CONFIG.mail_enabled() {
let mut user = User::new(email); let mut user = User::new(email);
user.save(&conn)?; user.save(&conn)?;
if CONFIG.mail_enabled() {
let org_name = "bitwarden_rs"; let org_name = "bitwarden_rs";
mail::send_invite(&user.email, &user.uuid, None, None, &org_name, None) mail::send_invite(&user.email, &user.uuid, None, None, &org_name, None)
} else { } 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")] #[post("/users/<uuid>/delete")]
fn delete_user(uuid: String, _token: AdminToken, conn: DbConn) -> EmptyResult { fn delete_user(uuid: String, _token: AdminToken, conn: DbConn) -> EmptyResult {
let user = match User::find_by_uuid(&uuid, &conn) { let user = match User::find_by_uuid(&uuid, &conn) {

View File

@@ -870,8 +870,20 @@ fn move_cipher_selected_put(
move_cipher_selected(data, headers, conn, nt) move_cipher_selected(data, headers, conn, nt)
} }
#[post("/ciphers/purge", data = "<data>")] #[derive(FromForm)]
fn delete_all(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult { 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 data: PasswordData = data.into_inner().data;
let password_hash = data.MasterPasswordHash; let password_hash = data.MasterPasswordHash;
@@ -881,6 +893,25 @@ fn delete_all(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn, nt
err!("Invalid password") err!("Invalid password")
} }
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 // Delete ciphers and their attachments
for cipher in Cipher::find_owned_by_user(&user.uuid, &conn) { for cipher in Cipher::find_owned_by_user(&user.uuid, &conn) {
cipher.delete(&conn)?; cipher.delete(&conn)?;
@@ -894,6 +925,8 @@ fn delete_all(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn, nt
user.update_revision(&conn)?; user.update_revision(&conn)?;
nt.send_user_update(UpdateType::Vault, &user); nt.send_user_update(UpdateType::Vault, &user);
Ok(()) Ok(())
}
}
} }
fn _delete_cipher_by_uuid(uuid: &str, headers: &Headers, conn: &DbConn, nt: &Notify) -> EmptyResult { fn _delete_cipher_by_uuid(uuid: &str, headers: &Headers, conn: &DbConn, nt: &Notify) -> EmptyResult {

View File

@@ -1,4 +1,4 @@
use data_encoding::BASE32; use data_encoding::{BASE32, BASE64};
use rocket_contrib::json::Json; use rocket_contrib::json::Json;
use serde_json; use serde_json;
use serde_json::Value; use serde_json::Value;
@@ -31,6 +31,9 @@ pub fn routes() -> Vec<Route> {
generate_yubikey, generate_yubikey,
activate_yubikey, activate_yubikey,
activate_yubikey_put, 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 { pub fn validate_totp_code(totp_code: u64, secret: &str) -> EmptyResult {
use data_encoding::BASE32;
use oath::{totp_raw_now, HashType}; use oath::{totp_raw_now, HashType};
let decoded_secret = match BASE32.decode(secret.as_bytes()) { 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"), 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())
}

View File

@@ -9,7 +9,7 @@ use num_traits::FromPrimitive;
use crate::db::models::*; use crate::db::models::*;
use crate::db::DbConn; use crate::db::DbConn;
use crate::util::{self, JsonMap}; use crate::util;
use crate::api::{ApiResult, EmptyResult, JsonResult}; 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::Authenticator) => _tf::validate_totp_code_str(twofactor_code, &selected_data?)?,
Some(TwoFactorType::U2f) => _tf::validate_u2f_login(user_uuid, twofactor_code, conn)?, 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::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) => { Some(TwoFactorType::Remember) => {
match device.twofactor_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(); let mut challenge_list = Vec::new();
for key in request.registered_keys { for key in request.registered_keys {
let mut challenge_map = JsonMap::new(); challenge_list.push(json!({
"appId": request.app_id,
challenge_map.insert("appId".into(), Value::String(request.app_id.clone())); "challenge": request.challenge,
challenge_map.insert("challenge".into(), Value::String(request.challenge.clone())); "version": key.version,
challenge_map.insert("version".into(), Value::String(key.version)); "keyHandle": key.key_handle,
challenge_map.insert("keyHandle".into(), Value::String(key.key_handle.unwrap_or_default())); }));
challenge_list.push(Value::Object(challenge_map));
} }
let mut map = JsonMap::new();
use serde_json;
let challenge_list_str = serde_json::to_string(&challenge_list).unwrap(); 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()] = json!({
result["TwoFactorProviders2"][provider.to_string()] = Value::Object(map); "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) => { 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"), None => err!("No YubiKey devices registered"),
}; };
let yubikey_metadata: two_factor::YubikeyMetadata = let yubikey_metadata: two_factor::YubikeyMetadata = serde_json::from_str(&twofactor.data)?;
serde_json::from_str(&twofactor.data).expect("Can't parse Yubikey Metadata");
let mut map = JsonMap::new(); result["TwoFactorProviders2"][provider.to_string()] = json!({
map.insert("Nfc".into(), Value::Bool(yubikey_metadata.Nfc)); "Nfc": yubikey_metadata.Nfc,
result["TwoFactorProviders2"][provider.to_string()] = Value::Object(map); })
} }
_ => {} _ => {}

View File

@@ -9,11 +9,12 @@ use rocket_contrib::json::Json;
use serde_json::Value; use serde_json::Value;
use crate::util::Cached; use crate::util::Cached;
use crate::error::Error;
use crate::CONFIG; use crate::CONFIG;
pub fn routes() -> Vec<Route> { pub fn routes() -> Vec<Route> {
if CONFIG.web_vault_enabled() { 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 { } else {
routes![attachments, alive] routes![attachments, alive]
} }
@@ -62,3 +63,13 @@ fn alive() -> Json<String> {
Json(format_date(&Utc::now().naive_utc())) 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")
}
}

View File

@@ -40,7 +40,6 @@ fn decode_jwt<T: DeserializeOwned>(token: &str, issuer: String) -> Result<T, Err
let validation = jsonwebtoken::Validation { let validation = jsonwebtoken::Validation {
leeway: 30, // 30 seconds leeway: 30, // 30 seconds
validate_exp: true, validate_exp: true,
validate_iat: false, // IssuedAt is the same as NotBefore
validate_nbf: true, validate_nbf: true,
aud: None, aud: None,
iss: Some(issuer), iss: Some(issuer),

View File

@@ -9,7 +9,10 @@ lazy_static! {
println!("Error loading config:\n\t{:?}\n", e); println!("Error loading config:\n\t{:?}\n", e);
exit(12) 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; pub type Pass = String;
@@ -61,7 +64,7 @@ macro_rules! make_config {
/// Merges the values of both builders into a new builder. /// Merges the values of both builders into a new builder.
/// If both have the same element, `other` wins. /// 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 overrides = Vec::new();
let mut builder = self.clone(); 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. // 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] The following environment variables are being overriden by the config file,");
println!("[WARNING] please use the admin panel to make changes to them:"); println!("[WARNING] please use the admin panel to make changes to them:");
@@ -275,8 +278,12 @@ make_config! {
log_mounts: bool, true, def, false; log_mounts: bool, true, def, false;
/// Enable extended logging /// Enable extended logging
extended_logging: bool, false, def, true; extended_logging: bool, false, def, true;
/// Enable the log to output to Syslog
use_syslog: bool, false, def, false;
/// Log file path /// Log file path
log_file: String, false, option; 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, /// 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. /// 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; 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 Email Settings
smtp: _enable_smtp { smtp: _enable_smtp {
/// Enabled /// 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() { 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") 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(); let _usr = ConfigBuilder::from_file(&CONFIG_FILE).unwrap_or_default();
// Create merged config, config file overwrites env // Create merged config, config file overwrites env
let builder = _env.merge(&_usr); let builder = _env.merge(&_usr, true);
// Fill any missing with defaults // Fill any missing with defaults
let config = builder.build(); let config = builder.build();
@@ -379,7 +406,7 @@ impl Config {
// Prepare the combined config // Prepare the combined config
let config = { let config = {
let env = &self.inner.read().unwrap()._env; let env = &self.inner.read().unwrap()._env;
env.merge(&builder).build() env.merge(&builder, false).build()
}; };
validate_config(&config)?; validate_config(&config)?;
@@ -398,6 +425,14 @@ impl Config {
Ok(()) 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> { pub fn delete_user_config(&self) -> Result<(), Error> {
crate::util::delete_file(&CONFIG_FILE)?; crate::util::delete_file(&CONFIG_FILE)?;
@@ -433,9 +468,21 @@ impl Config {
let inner = &self.inner.read().unwrap().config; let inner = &self.inner.read().unwrap().config;
inner._enable_smtp && inner.smtp_host.is_some() inner._enable_smtp && inner.smtp_host.is_some()
} }
pub fn yubico_enabled(&self) -> bool {
let inner = &self.inner.read().unwrap().config; pub fn get_duo_akey(&self) -> String {
inner._enable_yubico && inner.yubico_client_id.is_some() && inner.yubico_secret_key.is_some() 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>( pub fn render_template<T: serde::ser::Serialize>(

View File

@@ -2,7 +2,8 @@
// PBKDF2 derivation // PBKDF2 derivation
// //
use ring::{digest, pbkdf2}; use ring::{digest, hmac, pbkdf2};
use std::num::NonZeroU32;
static DIGEST_ALG: &digest::Algorithm = &digest::SHA256; static DIGEST_ALG: &digest::Algorithm = &digest::SHA256;
const OUTPUT_LEN: usize = digest::SHA256_OUTPUT_LEN; 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> { pub fn hash_password(secret: &[u8], salt: &[u8], iterations: u32) -> Vec<u8> {
let mut out = vec![0u8; OUTPUT_LEN]; // Initialize array with zeros 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); pbkdf2::derive(DIGEST_ALG, iterations, salt, secret, &mut out);
out out
} }
pub fn verify_password_hash(secret: &[u8], salt: &[u8], previous: &[u8], iterations: u32) -> bool { 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() 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 // Random values
// //

View File

@@ -72,9 +72,7 @@ use crate::error::MapResult;
/// Database methods /// Database methods
impl Cipher { impl Cipher {
pub fn to_json(&self, host: &str, user_uuid: &str, conn: &DbConn) -> Value { pub fn to_json(&self, host: &str, user_uuid: &str, conn: &DbConn) -> Value {
use super::Attachment;
use crate::util::format_date; use crate::util::format_date;
use serde_json;
let attachments = Attachment::find_by_cipher(&self.uuid, conn); let attachments = Attachment::find_by_cipher(&self.uuid, conn);
let attachments_json: Vec<Value> = attachments.iter().map(|c| c.to_json(host)).collect(); let attachments_json: Vec<Value> = attachments.iter().map(|c| c.to_json(host)).collect();

View File

@@ -37,6 +37,12 @@ pub struct User {
pub client_kdf_iter: i32, pub client_kdf_iter: i32,
} }
enum UserStatus {
Enabled = 0,
Invited = 1,
_Disabled = 2,
}
/// Local methods /// Local methods
impl User { impl User {
pub const CLIENT_KDF_TYPE_DEFAULT: i32 = 0; // PBKDF2: 0 pub const CLIENT_KDF_TYPE_DEFAULT: i32 = 0; // PBKDF2: 0
@@ -113,14 +119,19 @@ use crate::error::MapResult;
/// Database methods /// Database methods
impl User { impl User {
pub fn to_json(&self, conn: &DbConn) -> Value { pub fn to_json(&self, conn: &DbConn) -> Value {
use super::{TwoFactor, UserOrganization};
let orgs = UserOrganization::find_by_user(&self.uuid, conn); let orgs = UserOrganization::find_by_user(&self.uuid, conn);
let orgs_json: Vec<Value> = orgs.iter().map(|c| c.to_json(&conn)).collect(); 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(); 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!({ json!({
"_Enabled": !self.password_hash.is_empty(), "_Status": status as i32,
"Id": self.uuid, "Id": self.uuid,
"Name": self.name, "Name": self.name,
"Email": self.email, "Email": self.email,

View File

@@ -41,6 +41,8 @@ use regex::Error as RegexErr;
use reqwest::Error as ReqErr; use reqwest::Error as ReqErr;
use serde_json::{Error as SerdeErr, Value}; use serde_json::{Error as SerdeErr, Value};
use std::io::Error as IOErr; use std::io::Error as IOErr;
use std::option::NoneError as NoneErr;
use std::time::SystemTimeError as TimeErr; use std::time::SystemTimeError as TimeErr;
use u2f::u2ferror::U2fError as U2fErr; use u2f::u2ferror::U2fError as U2fErr;
use yubico::yubicoerror::YubicoError as YubiErr; use yubico::yubicoerror::YubicoError as YubiErr;
@@ -73,6 +75,13 @@ make_error! {
YubiError(YubiErr): _has_source, _api_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 { impl std::fmt::Debug for Error {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self.source() { 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> { fn _has_source<T>(e: T) -> Option<T> {
Some(e) Some(e)
} }

View File

@@ -1,8 +1,9 @@
use lettre::smtp::authentication::Credentials; use lettre::smtp::authentication::Credentials;
use lettre::smtp::ConnectionReuseParameters; use lettre::smtp::ConnectionReuseParameters;
use lettre::{ClientSecurity, ClientTlsParameters, SmtpClient, SmtpTransport, Transport}; use lettre::{ClientSecurity, ClientTlsParameters, SmtpClient, SmtpTransport, Transport};
use lettre_email::EmailBuilder; use lettre_email::{EmailBuilder, MimeMultipartType, PartBuilder};
use native_tls::{Protocol, TlsConnector}; use native_tls::{Protocol, TlsConnector};
use quoted_printable::encode_to_str;
use crate::api::EmptyResult; use crate::api::EmptyResult;
use crate::auth::{encode_jwt, generate_invite_claims}; 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 { 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() let email = EmailBuilder::new()
.to(address) .to(address)
.from((CONFIG.smtp_from().as_str(), CONFIG.smtp_from_name().as_str())) .from((CONFIG.smtp_from().as_str(), CONFIG.smtp_from_name().as_str()))
.subject(subject) .subject(subject)
.alternative(body_html, body_text) .child(alternative.build())
.build() .build()
.map_err(|e| Error::new("Error building email", e.to_string()))?; .map_err(|e| Error::new("Error building email", e.to_string()))?;

View File

@@ -69,6 +69,7 @@ fn launch_info() {
} }
fn init_logging() -> Result<(), fern::InitError> { fn init_logging() -> Result<(), fern::InitError> {
use std::str::FromStr;
let mut logger = fern::Dispatch::new() let mut logger = fern::Dispatch::new()
.format(|out, message, record| { .format(|out, message, record| {
out.finish(format_args!( out.finish(format_args!(
@@ -79,31 +80,30 @@ fn init_logging() -> Result<(), fern::InitError> {
message message
)) ))
}) })
.level(log::LevelFilter::Debug) .level(log::LevelFilter::from_str(&CONFIG.log_level()).expect("Valid log level"))
.level_for("hyper", log::LevelFilter::Warn) // Hide unknown certificate errors if using self-signed
.level_for("rustls", log::LevelFilter::Warn) .level_for("rustls::session", log::LevelFilter::Off)
.level_for("handlebars", log::LevelFilter::Warn) // Hide failed to close stream messages
.level_for("ws", log::LevelFilter::Info) .level_for("hyper::server", log::LevelFilter::Warn)
.level_for("multipart", log::LevelFilter::Info)
.level_for("html5ever", log::LevelFilter::Info)
.chain(std::io::stdout()); .chain(std::io::stdout());
if let Some(log_file) = CONFIG.log_file() { if let Some(log_file) = CONFIG.log_file() {
logger = logger.chain(fern::log_file(log_file)?); logger = logger.chain(fern::log_file(log_file)?);
} }
#[cfg(not(windows))]
{
if cfg!(feature = "enable_syslog") || CONFIG.use_syslog() {
logger = chain_syslog(logger); logger = chain_syslog(logger);
}
}
logger.apply()?; logger.apply()?;
Ok(()) Ok(())
} }
#[cfg(not(feature = "enable_syslog"))] #[cfg(not(windows))]
fn chain_syslog(logger: fern::Dispatch) -> fern::Dispatch {
logger
}
#[cfg(feature = "enable_syslog")]
fn chain_syslog(logger: fern::Dispatch) -> fern::Dispatch { fn chain_syslog(logger: fern::Dispatch) -> fern::Dispatch {
let syslog_fmt = syslog::Formatter3164 { let syslog_fmt = syslog::Formatter3164 {
facility: syslog::Facility::LOG_USER, facility: syslog::Facility::LOG_USER,

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -13,9 +13,9 @@
{{#if TwoFactorEnabled}} {{#if TwoFactorEnabled}}
<span class="badge badge-success ml-2">2FA</span> <span class="badge badge-success ml-2">2FA</span>
{{/if}} {{/if}}
{{#unless _Enabled}} {{#case _Status 1}}
<span class="badge badge-warning ml-2">Disabled</span> <span class="badge badge-warning ml-2">Invited</span>
{{/unless}} {{/case}}
<span class="d-block">{{Email}}</span> <span class="d-block">{{Email}}</span>
</div> </div>
<div class="col"> <div class="col">

View File

@@ -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"> <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;"> <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"> <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> </td>
</tr> </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;"> <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"> <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;"> <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;"> <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"> <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>
<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>
</tr> </tr>
</table> </table>
</td> </td>

View File

@@ -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"> <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;"> <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"> <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> </td>
</tr> </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;"> <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"> <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;"> <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;"> <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"> <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>
<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>
</tr> </tr>
</table> </table>
</td> </td>

View File

@@ -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"> <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;"> <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"> <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> </td>
</tr> </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;"> <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"> <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;"> <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;"> <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"> <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>
<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>
</tr> </tr>
</table> </table>
</td> </td>

View File

@@ -3,5 +3,6 @@ Your master password hint
You (or someone) recently requested your master password hint. You (or someone) recently requested your master password hint.
Your hint is: "{{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. If you did not request your master password hint you can safely ignore this email.

View File

@@ -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"> <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;"> <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"> <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> </td>
</tr> </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;"> <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"> <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;"> <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;"> <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"> <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>
<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>
</tr> </tr>
</table> </table>
</td> </td>

View File

@@ -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"> <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;"> <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"> <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> </td>
</tr> </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;"> <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"> <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;"> <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;"> <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"> <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>
<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>
</tr> </tr>
</table> </table>
</td> </td>