Compare commits

..

61 Commits

Author SHA1 Message Date
Daniel García 349cb33fbd Updated dependencies 2019-03-23 19:48:22 +01:00
Daniel García d7542b6818 Merge pull request #437 from njfox/fix-smtp-error
Split up long line to stop SMTP from breaking
2019-03-21 14:22:57 +01:00
Nick Fox 7976d39d9d Adjust whitespace 2019-03-20 23:29:29 -04:00
Nick Fox 5ee9676941 Break up long line to stop SMTP from breaking 2019-03-20 23:24:30 -04:00
Daniel García 4b40cda910 Added domain blacklist regex for icons service and improved valid domain check.
Reorganized the icons code a bit.
2019-03-18 22:12:39 +01:00
Daniel García 4689ed7b30 Changed uppercase deserializer to avoid a clone. 2019-03-18 22:02:37 +01:00
Daniel García 084bc2aee3 Use final release of lettre and update dependencies 2019-03-17 14:43:22 +01:00
Daniel García 6d7e15b2fd Use web vault 2.9.0 release 2019-03-14 13:29:03 +01:00
Daniel García 61515160a7 Allow changing error codes and create an empty error.
Return 404 instead of 400 when no accounts breached.
2019-03-14 00:17:36 +01:00
Daniel García a25bfdd16d Remove unused features from multipart (integration with other servers) 2019-03-13 15:57:00 +01:00
Daniel García e93538cea9 Add option to use wrapped TLS in email, instead of STARTTLS upgrade 2019-03-10 14:45:42 +01:00
Daniel García b4244b28b6 Update admin page scripts and fixed broken tooltip 2019-03-09 14:41:34 +01:00
Daniel García 43f9038325 Add option to force resync clients in admin panel 2019-03-07 21:08:33 +01:00
Daniel García 27872f476e Update dependencies 2019-03-07 20:22:08 +01:00
Daniel García 339044f8aa Add warning about config panel values overriding env vars. 2019-03-07 20:22:02 +01:00
Daniel García 0718a090e1 Trim spaces from admin token during authentication and validate that the admin panel token is not empty 2019-03-07 20:21:50 +01:00
Daniel García 9e1f030a80 Explicitly close SMTP connection in case of error. 2019-03-07 20:21:10 +01:00
Daniel García 04922f6aa0 Some formatting and dependency updates 2019-03-03 16:11:55 +01:00
Daniel García 7d2bc9e162 Added option to force 2fa at logins and made some changes to two factor code.
Added newlines to config options to keep them a reasonable length.
2019-03-03 16:09:15 +01:00
Daniel García c6c00729e3 Update vault to new version. No need to wait for a release when even the official web vault is already using it 2019-02-27 17:28:04 +01:00
Daniel García 10756b0920 Update dependencies and fix some lints 2019-02-27 17:21:04 +01:00
Daniel García 1eb1502a07 Merge pull request #416 from mprasil/armv6
Armv6
2019-02-25 18:26:53 +01:00
Miroslav Prasil 30e72a96a9 Symlink missing ld-linux file 2019-02-25 16:17:34 +00:00
Daniel García 2646db78a4 Merge pull request #414 from FrankPetrilli/patch-1
Minor typo fix conect => connect
2019-02-25 14:21:28 +01:00
Miroslav Prasil f5358b13f5 Add Dockerfile for armv6 2019-02-25 12:17:22 +00:00
Frank Petrilli d156170971 Minor typo fix conect => connect 2019-02-24 16:08:38 -08:00
Daniel García d9bfe847db Merge pull request #410 from gdamjan/remove-uneeded-mutability
remove some unneeded mutability
2019-02-22 22:52:53 +01:00
Дамјан Георгиевски 473f8b8e31 remove some unneeded mutability 2019-02-22 20:25:50 +01:00
Daniel García aeb4b4c8a5 Remove verbose, otherwise the logs get filled with useless info 2019-02-22 16:16:07 +01:00
Daniel García 980a3e45db Set up CI with Azure Pipelines 2019-02-22 15:51:30 +01:00
Daniel García 5794969f5b Merge pull request #406 from shauder/feature/disable-admin-token
Allow the Admin token to be disabled in the advanced menu
2019-02-20 23:06:52 +01:00
Shane Faulkner 8b5b06c3d1 Allow the Admin token to be disabled in the advanced menu 2019-02-20 14:56:08 -06:00
Daniel García b50c27b619 Print a warning when an env variable is being overriden by the config file, and reorganize the main file a bit.
Modified the JWT key generation, now it should also show the output of OpenSSL in the logs.
2019-02-20 20:59:37 +01:00
Daniel García 5ee04e31e5 Updated dependencies, removed some unnecessary clones and fixed some lints 2019-02-20 17:54:18 +01:00
Daniel García bf6ae91a6d Remove margins on small devices 2019-02-18 20:43:34 +01:00
Daniel García 828e3a5795 Add extra padding when the toolbar collapses in small devices 2019-02-18 20:33:32 +01:00
Daniel García 7b5bcd45f8 Show read-only options in the config panel and the env variable names in the tooltips 2019-02-18 19:25:33 +01:00
Daniel García 72de16fb86 Merge pull request #404 from mprasil/disable_wal
Add an option to not enable WAL (should help in #399)
2019-02-18 16:10:16 +01:00
Miroslav Prasil 0b903fc5f4 Extended the template file and refer to wiki 2019-02-18 14:57:21 +00:00
Miroslav Prasil 4df686f49e Add an option to not enable WAL (should help in #399) 2019-02-18 10:48:48 +00:00
Daniel García d7eeaaf249 Escape user data from admin panel when calling JS 2019-02-17 15:24:14 +01:00
Daniel García a744b9437a Implemented multiple U2f keys, key names, and compromised checks 2019-02-16 23:07:48 +01:00
Daniel García 6027b969f5 Delete old devices when deauthorizing user sessions 2019-02-16 23:06:26 +01:00
Daniel García 93805a5d7b Fix Yubikeys deleted on error 2019-02-16 21:30:55 +01:00
Daniel García 71da961ecd Merge pull request #402 from mprasil/version_in_docker
Include git repo in build so we get version
2019-02-16 12:20:25 +01:00
Miroslav Prasil dd421809e5 Include git repo in build so we get version 2019-02-16 08:50:16 +00:00
Daniel García 274ea9a4f2 Use the latest fast_chemail crate directly, with the fix 2019-02-15 14:39:30 +01:00
Daniel García 8743d18aca Update travis image and remove now-ignored sudo tag 2019-02-13 18:50:45 +01:00
Daniel García d3773a433a Removed list of mounted routes at startup by default, with option to add it back. This would get annoying when starting the server frequently, because it printed ~130 lines of mostly useless info 2019-02-13 00:03:16 +01:00
Daniel García 0f0a87becf Add version to initial message 2019-02-12 22:47:00 +01:00
Daniel García 4b57bb8eeb Merge pull request #394 from BlackDex/icon-timeout
Added config option for icon download timeout
2019-02-12 22:00:12 +01:00
BlackDex 3b27dbb0aa Added config option for icon download timeout 2019-02-12 21:56:28 +01:00
Daniel García ff2fbd322e Update deps and fix email check 2019-02-12 15:01:02 +01:00
Daniel García 9636f33fdb Implement constant time equal check for admin, 2fa recover and 2fa remember tokens 2019-02-11 23:45:55 +01:00
Daniel García bbe2a1b264 Merge pull request #391 from TheMardy/master
Updated Email Templates
2019-02-10 22:03:20 +01:00
Daniel García 79fdfd6524 Add missing url parameter 2019-02-10 21:40:20 +01:00
Daniel García d086a99e5b Implemented HTML emails with text alternative 2019-02-10 19:12:34 +01:00
TheMardy 22b0b95209 Added HTML templates (+14 squashed commit)
Squashed commit:

[ece2260] Plaintext send_org_invite

[01d4884] Plaintext pw_hint_some

[6ce5173] Plaintext pw_hint_none

[881af3e] Plaintext invite_confirmed

[ce78621] Plaintext invite_accepted

[13a44a4] Rename send_org_invite.hbs to send_org_invite.html.hbs

[b52bf2f] Rename pw_hint_some.hbs to pw_hint_some.html.hbs

[e0d1aeb] Rename pw_hint_none.hbs to pw_hint_none.html.hbs

[898dbcd] Rename invite_confirmed.hbs to invite_confirmed.html.hbs

[107af31] Rename invite_accepted.hbs to invite_accepted.html.hbs

[d26d662] Updated send_org_invite template

[71f47af] Updated pw_hint_some template

[c2ca3c2] Updated pw_hint_none template

[50f8bfb] Updated invite_accepted template

[17f96f8] Updated invite_confirmed template
2019-02-10 19:04:18 +01:00
Daniel García 28d1588e73 Show version in admin panel 2019-02-10 16:02:46 +01:00
Daniel García f3b1a5ff3e Error when admin panel is disabled 2019-02-10 15:26:19 +01:00
Daniel García 330e90a6ac Hide secrets in config panel 2019-02-08 20:49:04 +01:00
43 changed files with 2001 additions and 953 deletions
-4
View File
@@ -9,10 +9,6 @@ data
.idea
*.iml
# Git files
.git
.gitignore
# Documentation
*.md
+25 -1
View File
@@ -43,12 +43,35 @@
## It's recommended to also set 'ROCKET_CLI_COLORS=off'
# LOG_FILE=/path/to/log
## 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,
## this setting only prevents bitwarden_rs from automatically enabling it on start.
## Please read project wiki page about this setting first before changing the value as it can
## cause performance degradation or might render the service unable to start.
# ENABLE_DB_WAL=true
## Disable icon downloading
## Set to true to disable icon downloading, this would still serve icons from $ICON_CACHE_FOLDER,
## but it won't produce any external network request. Needs to set $ICON_CACHE_TTL to 0,
## otherwise it will delete them and they won't be downloaded again.
# DISABLE_ICON_DOWNLOAD=false
## Icon download timeout
## Configure the timeout value when downloading the favicons.
## The default is 10 seconds, but this could be to low on slower network connections
# ICON_DOWNLOAD_TIMEOUT=10
## Icon blacklist Regex
## Any domains or IPs that match this regex won't be fetched by the icon service.
## Useful to hide other servers in the local network. Check the WIKI for more details
# ICON_BLACKLIST_REGEX=192\.168\.1\.[0-9].*^
## Disable 2FA remember
## Enabling this would force the users to use a second factor to login every time.
## Note that the checkbox would still be present, but ignored.
# DISABLE_2FA_REMEMBER=false
## Controls if new users can register
# SIGNUPS_ALLOWED=true
@@ -56,6 +79,7 @@
## One option is to use 'openssl rand -base64 48'
## If not set, the admin panel is disabled
# ADMIN_TOKEN=Vy2VyYTTsKPv8W5aEOWUbB/Bt3DEKePbHmI4m9VcemUMS2rEviDowNAFqYi1xjmp
# DISABLE_ADMIN_TOKEN=false
## Invitations org admins to invite users, even when signups are disabled
# INVITATIONS_ALLOWED=true
@@ -97,4 +121,4 @@
# SMTP_PORT=587
# SMTP_SSL=true
# SMTP_USERNAME=username
# SMTP_PASSWORD=password
# SMTP_PASSWORD=password
+7 -7
View File
@@ -1,9 +1,9 @@
# Copied from Rocket's .travis.yml
dist: xenial
language: rust
sudo: required # so we get a VM with higher specs
dist: trusty # so we get a VM with higher specs
rust: nightly
cache: cargo
rust:
- nightly
script:
- cargo build --verbose --all-features
# Nothing to install
install: true
script: cargo build --all-features
Generated
+397 -507
View File
File diff suppressed because it is too large Load Diff
+11 -16
View File
@@ -19,24 +19,24 @@ rocket = { version = "0.4.0", features = ["tls"], default-features = false }
rocket_contrib = "0.4.0"
# HTTP client
reqwest = "0.9.9"
reqwest = "0.9.12"
# multipart/form-data support
multipart = "0.16.1"
multipart = { version = "0.16.1", features = ["server"], default-features = false }
# WebSockets library
ws = "0.7.9"
ws = "0.8.0"
# MessagePack library
rmpv = "0.4.0"
# Concurrent hashmap implementation
chashmap = "2.2.0"
chashmap = "2.2.2"
# A generic serialization/deserialization framework
serde = "1.0.85"
serde_derive = "1.0.85"
serde_json = "1.0.37"
serde = "1.0.89"
serde_derive = "1.0.89"
serde_json = "1.0.39"
# Logging
log = "0.4.6"
@@ -44,7 +44,7 @@ fern = "0.5.7"
syslog = { version = "4.0.1", optional = true }
# A safe, extensible ORM and Query builder
diesel = { version = "1.4.1", features = ["sqlite", "chrono", "r2d2"] }
diesel = { version = "1.4.2", features = ["sqlite", "chrono", "r2d2"] }
diesel_migrations = { version = "1.4.0", features = ["sqlite"] }
# Bundled SQLite
@@ -78,10 +78,10 @@ yubico = { version = "0.5.1", features = ["online"], default-features = false }
dotenv = { version = "0.13.0", default-features = false }
# Lazy static macro
lazy_static = { version = "1.2.0", features = ["nightly"] }
lazy_static = "1.3.0"
# More derives
derive_more = "0.13.0"
derive_more = "0.14.0"
# Numerical libraries
num-traits = "0.2.6"
@@ -97,13 +97,8 @@ handlebars = "1.1.0"
# For favicon extraction from main website
soup = "0.3.0"
regex = "1.1.0"
regex = "1.1.2"
[patch.crates-io]
# Add support for Timestamp type
rmp = { git = 'https://github.com/dani-garcia/msgpack-rust' }
# Use new native_tls version 0.2
lettre = { git = 'https://github.com/lettre/lettre', rev = 'c988b1760ad81' }
lettre_email = { git = 'https://github.com/lettre/lettre', rev = 'c988b1760ad81' }
+1 -1
View File
@@ -4,7 +4,7 @@
####################### VAULT BUILD IMAGE #######################
FROM alpine as vault
ENV VAULT_VERSION "v2.8.0d"
ENV VAULT_VERSION "v2.9.0"
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
+1 -1
View File
@@ -4,7 +4,7 @@
####################### VAULT BUILD IMAGE #######################
FROM alpine as vault
ENV VAULT_VERSION "v2.8.0d"
ENV VAULT_VERSION "v2.9.0"
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
+1 -1
View File
@@ -4,7 +4,7 @@
####################### VAULT BUILD IMAGE #######################
FROM alpine as vault
ENV VAULT_VERSION "v2.8.0d"
ENV VAULT_VERSION "v2.9.0"
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
+93
View File
@@ -0,0 +1,93 @@
# Using multistage build:
# https://docs.docker.com/develop/develop-images/multistage-build/
# https://whitfin.io/speeding-up-rust-docker-builds/
####################### VAULT BUILD IMAGE #######################
FROM alpine as vault
ENV VAULT_VERSION "v2.9.0"
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
RUN apk add --update-cache --upgrade \
curl \
tar
RUN mkdir /web-vault
WORKDIR /web-vault
RUN curl -L $URL | tar xz
RUN ls
########################## BUILD IMAGE ##########################
# We need to use the Rust build image, because
# we need the Rust compiler and Cargo tooling
FROM rust as build
RUN apt-get update \
&& apt-get install -y \
gcc-arm-linux-gnueabi \
&& mkdir -p ~/.cargo \
&& echo '[target.arm-unknown-linux-gnueabi]' >> ~/.cargo/config \
&& echo 'linker = "arm-linux-gnueabi-gcc"' >> ~/.cargo/config
ENV CARGO_HOME "/root/.cargo"
ENV USER "root"
WORKDIR /app
# Prepare openssl armel libs
RUN sed 's/^deb/deb-src/' /etc/apt/sources.list > \
/etc/apt/sources.list.d/deb-src.list \
&& dpkg --add-architecture armel \
&& apt-get update \
&& apt-get install -y \
libssl-dev:armel \
libc6-dev:armel
ENV CC_arm_unknown_linux_gnueabi="/usr/bin/arm-linux-gnueabi-gcc"
ENV CROSS_COMPILE="1"
ENV OPENSSL_INCLUDE_DIR="/usr/include/arm-linux-gnueabi"
ENV OPENSSL_LIB_DIR="/usr/lib/arm-linux-gnueabi"
# Copies the complete project
# To avoid copying unneeded files, use .dockerignore
COPY . .
# Build
RUN rustup target add arm-unknown-linux-gnueabi
RUN cargo build --release --target=arm-unknown-linux-gnueabi -v
######################## RUNTIME IMAGE ########################
# Create a new stage with a minimal image
# because we already have a binary built
FROM balenalib/rpi-debian:stretch
ENV ROCKET_ENV "staging"
ENV ROCKET_PORT=80
ENV ROCKET_WORKERS=10
RUN [ "cross-build-start" ]
# Install needed libraries
RUN apt-get update && apt-get install -y\
openssl\
ca-certificates\
--no-install-recommends\
&& ln -s /lib/ld-linux-armhf.so.3 /lib/ld-linux.so.3\
&& rm -rf /var/lib/apt/lists/*
RUN mkdir /data
RUN [ "cross-build-end" ]
VOLUME /data
EXPOSE 80
# Copies the files from the context (Rocket.toml file and web-vault)
# and the binary from the "build" stage to the current stage
COPY Rocket.toml .
COPY --from=vault /web-vault ./web-vault
COPY --from=build /app/target/arm-unknown-linux-gnueabi/release/bitwarden_rs .
# Configures the startup!
CMD ./bitwarden_rs
+1 -1
View File
@@ -4,7 +4,7 @@
####################### VAULT BUILD IMAGE #######################
FROM alpine as vault
ENV VAULT_VERSION "v2.8.0d"
ENV VAULT_VERSION "v2.9.0"
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
+17
View File
@@ -0,0 +1,17 @@
pool:
vmImage: 'Ubuntu-16.04'
steps:
- script: |
ls -la
curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain $(cat rust-toolchain)
echo "##vso[task.prependpath]$HOME/.cargo/bin"
displayName: 'Install Rust'
- script: |
rustc -Vv
cargo -V
displayName: Query rust and cargo versions
- script : cargo build --all-features
displayName: 'Build project'
+21 -3
View File
@@ -6,6 +6,10 @@ fn main() {
fn run(args: &[&str]) -> Result<String, std::io::Error> {
let out = Command::new(args[0]).args(&args[1..]).output()?;
if !out.status.success() {
use std::io::{Error, ErrorKind};
return Err(Error::new(ErrorKind::Other, "Command not successful"));
}
Ok(String::from_utf8(out.stdout).unwrap().trim().to_string())
}
@@ -13,8 +17,10 @@ fn run(args: &[&str]) -> Result<String, std::io::Error> {
fn read_git_info() -> Result<(), std::io::Error> {
// The exact tag for the current commit, can be empty when
// the current commit doesn't have an associated tag
let exact_tag = run(&["git", "describe", "--abbrev=0", "--tags", "--exact-match"])?;
println!("cargo:rustc-env=GIT_EXACT_TAG={}", exact_tag);
let exact_tag = run(&["git", "describe", "--abbrev=0", "--tags", "--exact-match"]).ok();
if let Some(ref exact) = exact_tag {
println!("cargo:rustc-env=GIT_EXACT_TAG={}", exact);
}
// The last available tag, equal to exact_tag when
// the current commit is tagged
@@ -27,13 +33,25 @@ fn read_git_info() -> Result<(), std::io::Error> {
// The current git commit hash
let rev = run(&["git", "rev-parse", "HEAD"])?;
let rev_short = rev.get(..12).unwrap_or_default();
let rev_short = rev.get(..8).unwrap_or_default();
println!("cargo:rustc-env=GIT_REV={}", rev_short);
// Combined version
let version = if let Some(exact) = exact_tag {
exact
} else if &branch != "master" {
format!("{}-{} ({})", last_tag, rev_short, branch)
} else {
format!("{}-{}", last_tag, rev_short)
};
println!("cargo:rustc-env=GIT_VERSION={}", version);
// To access these values, use:
// env!("GIT_EXACT_TAG")
// env!("GIT_LAST_TAG")
// env!("GIT_BRANCH")
// env!("GIT_REV")
// env!("GIT_VERSION")
Ok(())
}
+1 -1
View File
@@ -1 +1 @@
nightly-2019-01-26
nightly-2019-03-14
+42 -23
View File
@@ -15,8 +15,8 @@ use crate::mail;
use crate::CONFIG;
pub fn routes() -> Vec<Route> {
if CONFIG.admin_token().is_none() {
return Vec::new();
if CONFIG.admin_token().is_none() && !CONFIG.disable_admin_token() {
return routes![admin_disabled];
}
routes![
@@ -26,21 +26,28 @@ pub fn routes() -> Vec<Route> {
invite_user,
delete_user,
deauth_user,
update_revision_users,
post_config,
delete_config,
]
}
#[get("/")]
fn admin_disabled() -> &'static str {
"The admin panel is disabled, please configure the 'ADMIN_TOKEN' variable to enable it"
}
const COOKIE_NAME: &str = "BWRS_ADMIN";
const ADMIN_PATH: &str = "/admin";
const BASE_TEMPLATE: &str = "admin/base";
const VERSION: Option<&str> = option_env!("GIT_VERSION");
#[get("/", rank = 2)]
fn admin_login(flash: Option<FlashMessage>) -> ApiResult<Html<String>> {
// If there is an error, show it
let msg = flash.map(|msg| format!("{}: {}", msg.name(), msg.msg()));
let json = json!({"page_content": "admin/login", "error": msg});
let json = json!({"page_content": "admin/login", "version": VERSION, "error": msg});
// Return the page
let text = CONFIG.render_template(BASE_TEMPLATE, &json)?;
@@ -83,22 +90,24 @@ fn post_admin_login(data: Form<LoginForm>, mut cookies: Cookies, ip: ClientIp) -
fn _validate_token(token: &str) -> bool {
match CONFIG.admin_token().as_ref() {
None => false,
Some(t) => t == token,
Some(t) => crate::crypto::ct_eq(t.trim(), token.trim()),
}
}
#[derive(Serialize)]
struct AdminTemplateData {
users: Vec<Value>,
page_content: String,
version: Option<&'static str>,
users: Vec<Value>,
config: Value,
}
impl AdminTemplateData {
fn new(users: Vec<Value>) -> Self {
Self {
users,
page_content: String::from("admin/page"),
version: VERSION,
users,
config: CONFIG.prepare_json(),
}
}
@@ -141,7 +150,7 @@ fn invite_user(data: Json<InviteData>, _token: AdminToken, conn: DbConn) -> Empt
let org_name = "bitwarden_rs";
mail::send_invite(&user.email, &user.uuid, None, None, &org_name, None)
} else {
let mut invitation = Invitation::new(data.email);
let invitation = Invitation::new(data.email);
invitation.save(&conn)
}
}
@@ -163,11 +172,17 @@ fn deauth_user(uuid: String, _token: AdminToken, conn: DbConn) -> EmptyResult {
None => err!("User doesn't exist"),
};
Device::delete_all_by_user(&user.uuid, &conn)?;
user.reset_security_stamp();
user.save(&conn)
}
#[post("/users/update_revision")]
fn update_revision_users(_token: AdminToken, conn: DbConn) -> EmptyResult {
User::update_all_revisions(&conn)
}
#[post("/config", data = "<data>")]
fn post_config(data: Json<ConfigBuilder>, _token: AdminToken) -> EmptyResult {
let data: ConfigBuilder = data.into_inner();
@@ -185,25 +200,29 @@ impl<'a, 'r> FromRequest<'a, 'r> for AdminToken {
type Error = &'static str;
fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
let mut cookies = request.cookies();
if CONFIG.disable_admin_token() {
Outcome::Success(AdminToken {})
} else {
let mut cookies = request.cookies();
let access_token = match cookies.get(COOKIE_NAME) {
Some(cookie) => cookie.value(),
None => return Outcome::Forward(()), // If there is no cookie, redirect to login
};
let access_token = match cookies.get(COOKIE_NAME) {
Some(cookie) => cookie.value(),
None => return Outcome::Forward(()), // If there is no cookie, redirect to login
};
let ip = match request.guard::<ClientIp>() {
Outcome::Success(ip) => ip.ip,
_ => err_handler!("Error getting Client IP"),
};
let ip = match request.guard::<ClientIp>() {
Outcome::Success(ip) => ip.ip,
_ => err_handler!("Error getting Client IP"),
};
if decode_admin(access_token).is_err() {
// Remove admin cookie
cookies.remove(Cookie::named(COOKIE_NAME));
error!("Invalid or expired admin JWT. IP: {}.", ip);
return Outcome::Forward(());
if decode_admin(access_token).is_err() {
// Remove admin cookie
cookies.remove(Cookie::named(COOKIE_NAME));
error!("Invalid or expired admin JWT. IP: {}.", ip);
return Outcome::Forward(());
}
Outcome::Success(AdminToken {})
}
Outcome::Success(AdminToken {})
}
}
+1
View File
@@ -322,6 +322,7 @@ fn post_sstamp(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -
err!("Invalid password")
}
Device::delete_all_by_user(&user.uuid, &conn)?;
user.reset_security_stamp();
user.save(&conn)
}
+4 -8
View File
@@ -74,10 +74,10 @@ fn sync(data: Form<SyncData>, headers: Headers, conn: DbConn) -> JsonResult {
let user_json = headers.user.to_json(&conn);
let folders = Folder::find_by_user(&headers.user.uuid, &conn);
let folders_json: Vec<Value> = folders.iter().map(|c| c.to_json()).collect();
let folders_json: Vec<Value> = folders.iter().map(Folder::to_json).collect();
let collections = Collection::find_by_user_uuid(&headers.user.uuid, &conn);
let collections_json: Vec<Value> = collections.iter().map(|c| c.to_json()).collect();
let collections_json: Vec<Value> = collections.iter().map(Collection::to_json).collect();
let ciphers = Cipher::find_by_user(&headers.user.uuid, &conn);
let ciphers_json: Vec<Value> = ciphers
@@ -608,7 +608,7 @@ fn share_cipher_by_uuid(
None => err!("Invalid collection ID provided"),
Some(collection) => {
if collection.is_writable_by_user(&headers.user.uuid, &conn) {
CollectionCipher::save(&cipher.uuid.clone(), &collection.uuid, &conn)?;
CollectionCipher::save(&cipher.uuid, &collection.uuid, &conn)?;
shared_to_collection = true;
} else {
err!("No rights to modify the collection")
@@ -854,11 +854,7 @@ fn move_cipher_selected(data: JsonUpcase<MoveCipherData>, headers: Headers, conn
// Move cipher
cipher.move_to_folder(data.FolderId.clone(), &user_uuid, &conn)?;
nt.send_cipher_update(
UpdateType::CipherUpdate,
&cipher,
&[user_uuid.clone()]
);
nt.send_cipher_update(UpdateType::CipherUpdate, &cipher, &[user_uuid.clone()]);
}
Ok(())
+1 -1
View File
@@ -25,7 +25,7 @@ pub fn routes() -> Vec<Route> {
fn get_folders(headers: Headers, conn: DbConn) -> JsonResult {
let folders = Folder::find_by_user(&headers.user.uuid, &conn);
let folders_json: Vec<Value> = folders.iter().map(|c| c.to_json()).collect();
let folders_json: Vec<Value> = folders.iter().map(Folder::to_json).collect();
Ok(Json(json!({
"Data": folders_json,
+9 -8
View File
@@ -33,10 +33,10 @@ use rocket::Route;
use rocket_contrib::json::Json;
use serde_json::Value;
use crate::db::DbConn;
use crate::api::{EmptyResult, JsonResult, JsonUpcase};
use crate::auth::Headers;
use crate::db::DbConn;
use crate::error::Error;
#[put("/devices/identifier/<uuid>/clear-token")]
fn clear_device_token(uuid: String) -> EmptyResult {
@@ -137,12 +137,13 @@ fn hibp_breach(username: String) -> JsonResult {
use reqwest::{header::USER_AGENT, Client};
let value: Value = Client::new()
.get(&url)
.header(USER_AGENT, user_agent)
.send()?
.error_for_status()?
.json()?;
let res = Client::new().get(&url).header(USER_AGENT, user_agent).send()?;
// If we get a 404, return a 404, it means no breached accounts
if res.status() == 404 {
return Err(Error::empty().with_code(404));
}
let value: Value = res.error_for_status()?.json()?;
Ok(Json(value))
}
+6 -6
View File
@@ -76,9 +76,9 @@ struct NewCollectionData {
fn create_organization(headers: Headers, data: JsonUpcase<OrgData>, conn: DbConn) -> JsonResult {
let data: OrgData = data.into_inner().data;
let mut org = Organization::new(data.Name, data.BillingEmail);
let org = Organization::new(data.Name, data.BillingEmail);
let mut user_org = UserOrganization::new(headers.user.uuid.clone(), org.uuid.clone());
let mut collection = Collection::new(org.uuid.clone(), data.CollectionName);
let collection = Collection::new(org.uuid.clone(), data.CollectionName);
user_org.key = data.Key;
user_org.access_all = true;
@@ -221,7 +221,7 @@ fn post_organization_collections(
None => err!("Can't find organization details"),
};
let mut collection = Collection::new(org.uuid.clone(), data.Name);
let collection = Collection::new(org.uuid.clone(), data.Name);
collection.save(&conn)?;
Ok(Json(collection.to_json()))
@@ -484,7 +484,7 @@ fn send_invite(org_id: String, data: JsonUpcase<InviteData>, headers: AdminHeade
}
if !CONFIG.mail_enabled() {
let mut invitation = Invitation::new(email.clone());
let invitation = Invitation::new(email.clone());
invitation.save(&conn)?;
}
@@ -581,7 +581,7 @@ fn reinvite_user(org_id: String, user_org: String, headers: AdminHeaders, conn:
Some(headers.user.email),
)?;
} else {
let mut invitation = Invitation::new(user.email.clone());
let invitation = Invitation::new(user.email.clone());
invitation.save(&conn)?;
}
@@ -851,7 +851,7 @@ fn post_org_import(
.Collections
.into_iter()
.map(|coll| {
let mut collection = Collection::new(org_id.clone(), coll.Name);
let collection = Collection::new(org_id.clone(), coll.Name);
if collection.save(&conn).is_err() {
err!("Failed to create Collection");
}
+169 -110
View File
File diff suppressed because it is too large Load Diff
+74 -33
View File
@@ -8,7 +8,7 @@ use rocket::Route;
use reqwest::{header::HeaderMap, Client, Response};
use rocket::http::{Cookie};
use rocket::http::Cookie;
use regex::Regex;
use soup::prelude::*;
@@ -22,25 +22,54 @@ pub fn routes() -> Vec<Route> {
const FALLBACK_ICON: &[u8; 344] = include_bytes!("../static/fallback-icon.png");
const ALLOWED_CHARS: &str = "_-.";
lazy_static! {
// Reuse the client between requests
static ref CLIENT: Client = Client::builder()
.gzip(true)
.timeout(Duration::from_secs(5))
.timeout(Duration::from_secs(CONFIG.icon_download_timeout()))
.default_headers(_header_map())
.build()
.unwrap();
}
fn is_valid_domain(domain: &str) -> bool {
// Don't allow empty or too big domains or path traversal
if domain.is_empty() || domain.len() > 255 || domain.contains("..") {
return false;
}
// Only alphanumeric or specific characters
for c in domain.chars() {
if !c.is_alphanumeric() && !ALLOWED_CHARS.contains(c) {
return false;
}
}
true
}
#[get("/<domain>/icon.png")]
fn icon(domain: String) -> Content<Vec<u8>> {
let icon_type = ContentType::new("image", "x-icon");
// Validate the domain to avoid directory traversal attacks
if domain.contains('/') || domain.contains("..") {
if !is_valid_domain(&domain) {
warn!("Invalid domain: {:#?}", domain);
return Content(icon_type, FALLBACK_ICON.to_vec());
}
if let Some(blacklist) = CONFIG.icon_blacklist_regex() {
info!("Icon blacklist enabled: {:#?}", blacklist);
let regex = Regex::new(&blacklist).expect("Valid Regex");
if regex.is_match(&domain) {
warn!("Blacklisted domain: {:#?}", domain);
return Content(icon_type, FALLBACK_ICON.to_vec());
}
}
let icon = get_icon(&domain);
Content(icon_type, icon)
@@ -132,11 +161,17 @@ fn icon_is_expired(path: &str) -> bool {
}
#[derive(Debug)]
struct IconList {
struct Icon {
priority: u8,
href: String,
}
impl Icon {
fn new(priority: u8, href: String) -> Self {
Self { href, priority }
}
}
/// Returns a Result/Tuple which holds a Vector IconList and a string which holds the cookies from the last response.
/// There will always be a result with a string which will contain https://example.com/favicon.ico and an empty string for the cookies.
/// This does not mean that that location does exists, but it is the default location browser use.
@@ -149,13 +184,13 @@ struct IconList {
/// let (mut iconlist, cookie_str) = get_icon_url("github.com")?;
/// let (mut iconlist, cookie_str) = get_icon_url("gitlab.com")?;
/// ```
fn get_icon_url(domain: &str) -> Result<(Vec<IconList>, String), Error> {
fn get_icon_url(domain: &str) -> Result<(Vec<Icon>, String), Error> {
// Default URL with secure and insecure schemes
let ssldomain = format!("https://{}", domain);
let httpdomain = format!("http://{}", domain);
// Create the iconlist
let mut iconlist: Vec<IconList> = Vec::new();
let mut iconlist: Vec<Icon> = Vec::new();
// Create the cookie_str to fill it all the cookies from the response
// These cookies can be used to request/download the favicon image.
@@ -167,13 +202,16 @@ fn get_icon_url(domain: &str) -> Result<(Vec<IconList>, String), Error> {
// Extract the URL from the respose in case redirects occured (like @ gitlab.com)
let url = content.url().clone();
let raw_cookies = content.headers().get_all("set-cookie");
cookie_str = raw_cookies.iter().map(|raw_cookie| {
let cookie = Cookie::parse(raw_cookie.to_str().unwrap_or_default()).unwrap();
format!("{}={}; ", cookie.name(), cookie.value())
}).collect::<String>();
cookie_str = raw_cookies
.iter()
.map(|raw_cookie| {
let cookie = Cookie::parse(raw_cookie.to_str().unwrap_or_default()).unwrap();
format!("{}={}; ", cookie.name(), cookie.value())
})
.collect::<String>();
// Add the default favicon.ico to the list with the domain the content responded from.
iconlist.push(IconList { priority: 35, href: url.join("/favicon.ico").unwrap().into_string() });
iconlist.push(Icon::new(35, url.join("/favicon.ico").unwrap().into_string()));
let soup = Soup::from_reader(content)?;
// Search for and filter
@@ -185,15 +223,17 @@ fn get_icon_url(domain: &str) -> Result<(Vec<IconList>, String), Error> {
// Loop through all the found icons and determine it's priority
for favicon in favicons {
let sizes = favicon.get("sizes").unwrap_or_default();
let href = url.join(&favicon.get("href").unwrap_or_default()).unwrap().into_string();
let priority = get_icon_priority(&href, &sizes);
let sizes = favicon.get("sizes");
let href = favicon.get("href").expect("Missing href");
let full_href = url.join(&href).unwrap().into_string();
iconlist.push(IconList { priority, href })
let priority = get_icon_priority(&full_href, sizes);
iconlist.push(Icon::new(priority, full_href))
}
} else {
// Add the default favicon.ico to the list with just the given domain
iconlist.push(IconList { priority: 35, href: format!("{}/favicon.ico", ssldomain) });
iconlist.push(Icon::new(35, format!("{}/favicon.ico", ssldomain)));
}
// Sort the iconlist by priority
@@ -204,12 +244,16 @@ fn get_icon_url(domain: &str) -> Result<(Vec<IconList>, String), Error> {
}
fn get_page(url: &str) -> Result<Response, Error> {
//CLIENT.get(url).send()?.error_for_status().map_err(Into::into)
get_page_with_cookies(url, "")
}
fn get_page_with_cookies(url: &str, cookie_str: &str) -> Result<Response, Error> {
CLIENT.get(url).header("cookie", cookie_str).send()?.error_for_status().map_err(Into::into)
CLIENT
.get(url)
.header("cookie", cookie_str)
.send()?
.error_for_status()
.map_err(Into::into)
}
/// Returns a Integer with the priority of the type of the icon which to prefer.
@@ -224,7 +268,7 @@ fn get_page_with_cookies(url: &str, cookie_str: &str) -> Result<Response, Error>
/// priority1 = get_icon_priority("http://example.com/path/to/a/favicon.png", "32x32");
/// priority2 = get_icon_priority("https://example.com/path/to/a/favicon.ico", "");
/// ```
fn get_icon_priority(href: &str, sizes: &str) -> u8 {
fn get_icon_priority(href: &str, sizes: Option<String>) -> u8 {
// Check if there is a dimension set
let (width, height) = parse_sizes(sizes);
@@ -272,19 +316,19 @@ fn get_icon_priority(href: &str, sizes: &str) -> u8 {
/// let (width, height) = parse_sizes("x128x128"); // (128, 128)
/// let (width, height) = parse_sizes("32"); // (0, 0)
/// ```
fn parse_sizes(sizes: &str) -> (u16, u16) {
fn parse_sizes(sizes: Option<String>) -> (u16, u16) {
let mut width: u16 = 0;
let mut height: u16 = 0;
if !sizes.is_empty() {
if let Some(sizes) = sizes {
match Regex::new(r"(?x)(\d+)\D*(\d+)").unwrap().captures(sizes.trim()) {
None => {},
None => {}
Some(dimensions) => {
if dimensions.len() >= 3 {
width = dimensions[1].parse::<u16>().unwrap_or_default();
height = dimensions[2].parse::<u16>().unwrap_or_default();
}
},
}
}
}
@@ -292,21 +336,18 @@ fn parse_sizes(sizes: &str) -> (u16, u16) {
}
fn download_icon(domain: &str) -> Result<Vec<u8>, Error> {
let (mut iconlist, cookie_str) = get_icon_url(&domain)?;
let (iconlist, cookie_str) = get_icon_url(&domain)?;
let mut buffer = Vec::new();
iconlist.truncate(5);
for icon in iconlist {
let url = icon.href;
info!("Downloading icon for {} via {}...", domain, url);
match get_page_with_cookies(&url, &cookie_str) {
for icon in iconlist.iter().take(5) {
match get_page_with_cookies(&icon.href, &cookie_str) {
Ok(mut res) => {
info!("Download finished for {}", url);
info!("Downloaded icon from {}", icon.href);
res.copy_to(&mut buffer)?;
break;
},
Err(_) => info!("Download failed for {}", url),
}
Err(_) => info!("Download failed for {}", icon.href),
};
}
+28 -38
View File
@@ -118,7 +118,7 @@ fn _password_login(data: ConnectData, conn: DbConn, ip: ClientIp) -> JsonResult
None => Device::new(device_id, user.uuid.clone(), device_name, device_type),
};
let twofactor_token = twofactor_auth(&user.uuid, &data.clone(), &mut device, &conn)?;
let twofactor_token = twofactor_auth(&user.uuid, &data, &mut device, &conn)?;
// Common
let user = User::find_by_uuid(&device.user_uuid, &conn).unwrap();
@@ -152,62 +152,45 @@ fn twofactor_auth(
conn: &DbConn,
) -> ApiResult<Option<String>> {
let twofactors = TwoFactor::find_by_user(user_uuid, conn);
let providers: Vec<_> = twofactors.iter().map(|tf| tf.type_).collect();
// No twofactor token if twofactor is disabled
if twofactors.is_empty() {
return Ok(None);
}
let provider = data.two_factor_provider.unwrap_or(providers[0]); // If we aren't given a two factor provider, asume the first one
let twofactor_ids: Vec<_> = twofactors.iter().map(|tf| tf.type_).collect();
let selected_id = data.two_factor_provider.unwrap_or(twofactor_ids[0]); // If we aren't given a two factor provider, asume the first one
let twofactor_code = match data.two_factor_token {
Some(ref code) => code,
None => err_json!(_json_err_twofactor(&providers, user_uuid, conn)?),
None => err_json!(_json_err_twofactor(&twofactor_ids, user_uuid, conn)?),
};
let twofactor = twofactors.iter().filter(|tf| tf.type_ == provider).nth(0);
let selected_twofactor = twofactors.into_iter().filter(|tf| tf.type_ == selected_id).nth(0);
use crate::api::core::two_factor as _tf;
use crate::crypto::ct_eq;
let selected_data = _selected_data(selected_twofactor);
let mut remember = data.two_factor_remember.unwrap_or(0);
match TwoFactorType::from_i32(selected_id) {
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?)?,
match TwoFactorType::from_i32(provider) {
Some(TwoFactorType::Remember) => {
match device.twofactor_remember {
Some(ref remember) if remember == twofactor_code => return Ok(None), // No twofactor token needed here
_ => err_json!(_json_err_twofactor(&providers, user_uuid, conn)?),
Some(ref code) if !CONFIG.disable_2fa_remember() && ct_eq(code, twofactor_code) => {
remember = 1; // Make sure we also return the token here, otherwise it will only remember the first time
}
_ => err_json!(_json_err_twofactor(&twofactor_ids, user_uuid, conn)?),
}
}
Some(TwoFactorType::Authenticator) => {
let twofactor = match twofactor {
Some(tf) => tf,
None => err!("TOTP not enabled"),
};
let totp_code: u64 = match twofactor_code.parse() {
Ok(code) => code,
_ => err!("Invalid TOTP code"),
};
if !twofactor.check_totp_code(totp_code) {
err_json!(_json_err_twofactor(&providers, user_uuid, conn)?)
}
}
Some(TwoFactorType::U2f) => {
use crate::api::core::two_factor;
two_factor::validate_u2f_login(user_uuid, &twofactor_code, conn)?;
}
Some(TwoFactorType::YubiKey) => {
use crate::api::core::two_factor;
two_factor::validate_yubikey_login(user_uuid, twofactor_code, conn)?;
}
_ => err!("Invalid two factor provider"),
}
if data.two_factor_remember.unwrap_or(0) == 1 {
if !CONFIG.disable_2fa_remember() && remember == 1 {
Ok(Some(device.refresh_twofactor_remember()))
} else {
device.delete_twofactor_remember();
@@ -215,6 +198,13 @@ fn twofactor_auth(
}
}
fn _selected_data(tf: Option<TwoFactor>) -> ApiResult<String> {
match tf {
Some(tf) => Ok(tf.data),
None => err!("Two factor doesn't exist"),
}
}
fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &DbConn) -> ApiResult<Value> {
use crate::api::core::two_factor;
+6 -3
View File
@@ -47,10 +47,13 @@ impl NumberOrString {
}
}
fn into_i32(self) -> Option<i32> {
fn into_i32(self) -> ApiResult<i32> {
use std::num::ParseIntError as PIE;
match self {
NumberOrString::Number(n) => Some(n),
NumberOrString::String(s) => s.parse().ok(),
NumberOrString::Number(n) => Ok(n),
NumberOrString::String(s) => s
.parse()
.map_err(|e: PIE| crate::Error::new("Can't convert to number", e.to_string())),
}
}
}
+3 -3
View File
@@ -88,7 +88,7 @@ fn serialize(val: Value) -> Vec<u8> {
fn serialize_date(date: NaiveDateTime) -> Value {
let seconds: i64 = date.timestamp();
let nanos: i64 = date.timestamp_subsec_nanos() as i64;
let nanos: i64 = date.timestamp_subsec_nanos().into();
let timestamp = nanos << 34 | seconds;
let bs = timestamp.to_be_bytes();
@@ -230,7 +230,7 @@ pub struct WebSocketUsers {
}
impl WebSocketUsers {
fn send_update(&self, user_uuid: &String, data: &[u8]) -> ws::Result<()> {
fn send_update(&self, user_uuid: &str, data: &[u8]) -> ws::Result<()> {
if let Some(user) = self.map.get(user_uuid) {
for sender in user.iter() {
sender.send(data)?;
@@ -249,7 +249,7 @@ impl WebSocketUsers {
ut,
);
self.send_update(&user.uuid.clone(), &data).ok();
self.send_update(&user.uuid, &data).ok();
}
pub fn send_folder_update(&self, ut: UpdateType, folder: &Folder) {
+2 -8
View File
@@ -21,17 +21,11 @@ lazy_static! {
pub static ref JWT_ADMIN_ISSUER: String = format!("{}|admin", CONFIG.domain());
static ref PRIVATE_RSA_KEY: Vec<u8> = match read_file(&CONFIG.private_rsa_key()) {
Ok(key) => key,
Err(e) => panic!(
"Error loading private RSA Key from {}\n Error: {}",
CONFIG.private_rsa_key(), e
),
Err(e) => panic!("Error loading private RSA Key.\n Error: {}", e),
};
static ref PUBLIC_RSA_KEY: Vec<u8> = match read_file(&CONFIG.public_rsa_key()) {
Ok(key) => key,
Err(e) => panic!(
"Error loading public RSA Key from {}\n Error: {}",
CONFIG.public_rsa_key(), e
),
Err(e) => panic!("Error loading public RSA Key.\n Error: {}", e),
};
}
+96 -17
View File
@@ -12,6 +12,8 @@ lazy_static! {
pub static ref CONFIG_FILE: String = get_env("CONFIG_FILE").unwrap_or_else(|| "data/config.json".into());
}
pub type Pass = String;
macro_rules! make_config {
($(
$(#[doc = $groupdoc:literal])?
@@ -60,12 +62,25 @@ 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 {
let mut overrides = Vec::new();
let mut builder = self.clone();
$($(
if let v @Some(_) = &other.$name {
builder.$name = v.clone();
if self.$name.is_some() {
overrides.push(stringify!($name).to_uppercase());
}
}
)+)+
if !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:");
println!("[WARNING] {}\n", overrides.join(", "));
}
builder
}
@@ -114,6 +129,7 @@ macro_rules! make_config {
fn _get_form_type(rust_type: &str) -> &'static str {
match rust_type {
"Pass" => "password",
"String" => "text",
"bool" => "checkbox",
_ => "number"
@@ -208,28 +224,31 @@ make_config! {
/// General settings
settings {
/// Domain URL |> This needs to be set to the URL used to access the server, including 'http[s]://' and port, if it's different than the default. Some server functions don't work correctly without this value
/// Domain URL |> This needs to be set to the URL used to access the server, including 'http[s]://'
/// and port, if it's different than the default. Some server functions don't work correctly without this value
domain: String, true, def, "http://localhost".to_string();
/// PRIVATE |> Domain set
/// Domain Set |> Indicates if the domain is set by the admin. Otherwise the default will be used.
domain_set: bool, false, def, false;
/// Enable web vault
web_vault_enabled: bool, false, def, true;
/// Disable icon downloads |> Set to true to disable icon downloading, this would still serve icons from $ICON_CACHE_FOLDER,
/// but it won't produce any external network request. Needs to set $ICON_CACHE_TTL to 0,
/// Disable icon downloads |> Set to true to disable icon downloading, this would still serve icons from
/// $ICON_CACHE_FOLDER, but it won't produce any external network request. Needs to set $ICON_CACHE_TTL to 0,
/// otherwise it will delete them and they won't be downloaded again.
disable_icon_download: bool, true, def, false;
/// Allow new signups |> Controls if new users can register. Note that while this is disabled, users could still be invited
signups_allowed: bool, true, def, true;
/// Allow invitations |> Controls whether users can be invited by organization admins, even when signups are disabled
invitations_allowed: bool, true, def, true;
/// Password iterations |> Number of server-side passwords hashing iterations. The changes only apply when a user changes their password. Not recommended to lower the value
/// Password iterations |> Number of server-side passwords hashing iterations.
/// The changes only apply when a user changes their password. Not recommended to lower the value
password_iterations: i32, true, def, 100_000;
/// Show password hints |> Controls if the password hint should be shown directly in the web page. Otherwise, if email is disabled, there is no way to see the password hint
/// Show password hints |> Controls if the password hint should be shown directly in the web page.
/// Otherwise, if email is disabled, there is no way to see the password hint
show_password_hint: bool, true, def, true;
/// Admin page token |> The token used to authenticate in this very same page. Changing it here won't deauthorize the current session
admin_token: String, true, option;
admin_token: Pass, true, option;
},
/// Advanced settings
@@ -238,14 +257,33 @@ make_config! {
icon_cache_ttl: u64, true, def, 2_592_000;
/// Negative icon cache expiry |> Number of seconds before trying to download an icon that failed again.
icon_cache_negttl: u64, true, def, 259_200;
/// Icon download timeout |> Number of seconds when to stop attempting to download an icon.
icon_download_timeout: u64, true, def, 10;
/// Icon blacklist Regex |> Any domains or IPs that match this regex won't be fetched by the icon service.
/// Useful to hide other servers in the local network. Check the WIKI for more details
icon_blacklist_regex: String, true, option;
/// Reload templates (Dev) |> When this is set to true, the templates get reloaded with every request. ONLY use this during development, as it can slow down the server
/// Disable Two-Factor remember |> Enabling this would force the users to use a second factor to login every time.
/// Note that the checkbox would still be present, but ignored.
disable_2fa_remember: bool, true, def, false;
/// Reload templates (Dev) |> When this is set to true, the templates get reloaded with every request.
/// ONLY use this during development, as it can slow down the server
reload_templates: bool, true, def, false;
/// Log routes at launch (Dev)
log_mounts: bool, true, def, false;
/// Enable extended logging
extended_logging: bool, false, def, true;
/// Log file path
log_file: String, false, option;
/// 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.
enable_db_wal: bool, false, def, true;
/// Disable Admin Token (Know the risks!) |> Disables the Admin Token for the admin page so you may use your own auth in-front
disable_admin_token: bool, true, def, false;
},
/// Yubikey settings
@@ -255,7 +293,7 @@ make_config! {
/// Client ID
yubico_client_id: String, true, option;
/// Secret Key
yubico_secret_key: String, true, option;
yubico_secret_key: Pass, true, option;
/// Server
yubico_server: String, true, option;
},
@@ -268,8 +306,10 @@ make_config! {
smtp_host: String, true, option;
/// Enable SSL
smtp_ssl: bool, true, def, true;
/// Use explicit TLS |> Enabling this would force the use of an explicit TLS connection, instead of upgrading an insecure one with STARTTLS
smtp_explicit_tls: bool, true, def, false;
/// Port
smtp_port: u16, true, auto, |c| if c.smtp_ssl {587} else {25};
smtp_port: u16, true, auto, |c| if c.smtp_explicit_tls {465} else if c.smtp_ssl {587} else {25};
/// From Address
smtp_from: String, true, def, String::new();
/// From Name
@@ -277,11 +317,17 @@ make_config! {
/// Username
smtp_username: String, true, option;
/// Password
smtp_password: String, true, option;
smtp_password: Pass, true, option;
},
}
fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
if let Some(ref token) = cfg.admin_token {
if token.trim().is_empty() {
err!("`ADMIN_TOKEN` is enabled but has an empty value. To enable the admin page without token, use `DISABLE_ADMIN_TOKEN`")
}
}
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")
}
@@ -416,21 +462,27 @@ fn load_templates(path: &str) -> Handlebars {
let mut hb = Handlebars::new();
// Error on missing params
hb.set_strict_mode(true);
// Register helpers
hb.register_helper("case", Box::new(CaseHelper));
hb.register_helper("jsesc", Box::new(JsEscapeHelper));
macro_rules! reg {
($name:expr) => {{
let template = include_str!(concat!("static/templates/", $name, ".hbs"));
hb.register_template_string($name, template).unwrap();
}};
($name:expr, $ext:expr) => {{
reg!($name);
reg!(concat!($name, $ext));
}};
}
// First register default templates here
reg!("email/invite_accepted");
reg!("email/invite_confirmed");
reg!("email/pw_hint_none");
reg!("email/pw_hint_some");
reg!("email/send_org_invite");
reg!("email/invite_accepted", ".html");
reg!("email/invite_confirmed", ".html");
reg!("email/pw_hint_none", ".html");
reg!("email/pw_hint_some", ".html");
reg!("email/send_org_invite", ".html");
reg!("admin/base");
reg!("admin/login");
@@ -444,7 +496,6 @@ fn load_templates(path: &str) -> Handlebars {
hb
}
#[derive(Clone, Copy)]
pub struct CaseHelper;
impl HelperDef for CaseHelper {
@@ -468,3 +519,31 @@ impl HelperDef for CaseHelper {
}
}
}
pub struct JsEscapeHelper;
impl HelperDef for JsEscapeHelper {
fn call<'reg: 'rc, 'rc>(
&self,
h: &Helper<'reg, 'rc>,
_: &'reg Handlebars,
_: &Context,
_: &mut RenderContext<'reg>,
out: &mut Output,
) -> HelperResult {
let param = h
.param(0)
.ok_or_else(|| RenderError::new("Param not found for helper \"js_escape\""))?;
let value = param
.value()
.as_str()
.ok_or_else(|| RenderError::new("Param for helper \"js_escape\" is not a String"))?;
let escaped_value = value.replace('\\', "").replace('\'', "\\x22").replace('\"', "\\x27");
let quoted_value = format!("&quot;{}&quot;", escaped_value);
out.write(&quoted_value)?;
Ok(())
}
}
+9
View File
@@ -36,3 +36,12 @@ pub fn get_random(mut array: Vec<u8>) -> Vec<u8> {
array
}
//
// Constant time compare
//
pub fn ct_eq<T: AsRef<[u8]>, U: AsRef<[u8]>>(a: T, b: U) -> bool {
use ring::constant_time::verify_slices_are_equal;
verify_slices_are_equal(a.as_ref(), b.as_ref()).is_ok()
}
+2 -2
View File
@@ -43,11 +43,11 @@ use crate::error::MapResult;
/// Database methods
impl Collection {
pub fn save(&mut self, conn: &DbConn) -> EmptyResult {
pub fn save(&self, conn: &DbConn) -> EmptyResult {
self.update_users_revision(conn);
diesel::replace_into(collections::table)
.values(&*self)
.values(self)
.execute(&**conn)
.map_res("Error saving collection")
}
+4 -4
View File
@@ -213,7 +213,7 @@ use crate::error::MapResult;
/// Database methods
impl Organization {
pub fn save(&mut self, conn: &DbConn) -> EmptyResult {
pub fn save(&self, conn: &DbConn) -> EmptyResult {
UserOrganization::find_by_org(&self.uuid, conn)
.iter()
.for_each(|user_org| {
@@ -221,7 +221,7 @@ impl Organization {
});
diesel::replace_into(organizations::table)
.values(&*self)
.values(self)
.execute(&**conn)
.map_res("Error saving organization")
}
@@ -323,11 +323,11 @@ impl UserOrganization {
})
}
pub fn save(&mut self, conn: &DbConn) -> EmptyResult {
pub fn save(&self, conn: &DbConn) -> EmptyResult {
User::update_uuid_revision(&self.user_uuid, conn);
diesel::replace_into(users_organizations::table)
.values(&*self)
.values(self)
.execute(&**conn)
.map_res("Error adding user to organization")
}
-15
View File
@@ -42,21 +42,6 @@ impl TwoFactor {
}
}
pub fn check_totp_code(&self, totp_code: u64) -> bool {
let totp_secret = self.data.as_bytes();
use data_encoding::BASE32;
use oath::{totp_raw_now, HashType};
let decoded_secret = match BASE32.decode(totp_secret) {
Ok(s) => s,
Err(_) => return false,
};
let generated = totp_raw_now(&decoded_secret, 6, 0, 30, &HashType::SHA1);
generated == totp_code
}
pub fn to_json(&self) -> Value {
json!({
"Enabled": self.enabled,

Some files were not shown because too many files have changed in this diff Show More