Browse Source

Merge branch 'master' of https://github.com/dani-garcia/bitwarden_rs

Merge to update
pull/733/head
DA-Michael 6 years ago
parent
commit
a839635413
  1. 7
      .env.template
  2. 1
      .travis.yml
  3. 712
      Cargo.lock
  4. 26
      Cargo.toml
  5. 2
      README.md
  6. 2
      azure-pipelines.yml
  7. 8
      build.rs
  8. 2
      docker/aarch64/mysql/Dockerfile
  9. 3
      docker/aarch64/sqlite/Dockerfile
  10. 2
      docker/amd64/mysql/Dockerfile
  11. 2
      docker/amd64/mysql/Dockerfile.alpine
  12. 104
      docker/amd64/postgresql/Dockerfile
  13. 86
      docker/amd64/postgresql/Dockerfile.alpine
  14. 3
      docker/amd64/sqlite/Dockerfile
  15. 3
      docker/amd64/sqlite/Dockerfile.alpine
  16. 2
      docker/armv6/mysql/Dockerfile
  17. 3
      docker/armv6/sqlite/Dockerfile
  18. 2
      docker/armv7/mysql/Dockerfile
  19. 3
      docker/armv7/sqlite/Dockerfile
  20. 2
      docker/healthcheck.sh
  21. 0
      migrations/mysql/2019-10-10-083032_add_column_to_twofactor/down.sql
  22. 1
      migrations/mysql/2019-10-10-083032_add_column_to_twofactor/up.sql
  23. 13
      migrations/postgresql/2019-09-12-100000_create_tables/down.sql
  24. 121
      migrations/postgresql/2019-09-12-100000_create_tables/up.sql
  25. 26
      migrations/postgresql/2019-09-16-150000_fix_attachments/down.sql
  26. 27
      migrations/postgresql/2019-09-16-150000_fix_attachments/up.sql
  27. 0
      migrations/postgresql/2019-10-10-083032_add_column_to_twofactor/down.sql
  28. 1
      migrations/postgresql/2019-10-10-083032_add_column_to_twofactor/up.sql
  29. 0
      migrations/sqlite/2019-10-10-083032_add_column_to_twofactor/down.sql
  30. 1
      migrations/sqlite/2019-10-10-083032_add_column_to_twofactor/up.sql
  31. 2
      rust-toolchain
  32. 2
      src/api/admin.rs
  33. 21
      src/api/core/mod.rs
  34. 59
      src/api/core/two_factor/authenticator.rs
  35. 10
      src/api/core/two_factor/email.rs
  36. 68
      src/api/icons.rs
  37. 10
      src/api/identity.rs
  38. 14
      src/api/web.rs
  39. 49
      src/config.rs
  40. 11
      src/db/mod.rs
  41. 14
      src/db/models/attachment.rs
  42. 17
      src/db/models/cipher.rs
  43. 49
      src/db/models/collection.rs
  44. 14
      src/db/models/device.rs
  45. 28
      src/db/models/folder.rs
  46. 36
      src/db/models/organization.rs
  47. 16
      src/db/models/two_factor.rs
  48. 35
      src/db/models/user.rs
  49. 1
      src/db/schemas/mysql/schema.rs
  50. 173
      src/db/schemas/postgresql/schema.rs
  51. 1
      src/db/schemas/sqlite/schema.rs
  52. 6
      src/main.rs
  53. BIN
      src/static/images/hibp.png
  54. 25
      src/util.rs

7
.env.template

@ -83,6 +83,10 @@
## Useful to hide other servers in the local network. Check the WIKI for more details
# ICON_BLACKLIST_REGEX=192\.168\.1\.[0-9].*^
## Any IP which is not defined as a global IP will be blacklisted.
## Usefull to secure your internal environment: See https://en.wikipedia.org/wiki/Reserved_IP_addresses for a list of IPs which it will block
# ICON_BLACKLIST_NON_GLOBAL_IPS=true
## 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.
@ -95,6 +99,9 @@
## One option is to use 'openssl rand -base64 48'
## If not set, the admin panel is disabled
# ADMIN_TOKEN=Vy2VyYTTsKPv8W5aEOWUbB/Bt3DEKePbHmI4m9VcemUMS2rEviDowNAFqYi1xjmp
## Enable this to bypass the admin panel security. This option is only
## meant to be used with the use of a separate auth layer in front
# DISABLE_ADMIN_TOKEN=false
## Invitations org admins to invite users, even when signups are disabled

1
.travis.yml

@ -11,6 +11,7 @@ cache: cargo
before_install:
- sudo curl -L https://github.com/hadolint/hadolint/releases/download/v$HADOLINT_VERSION/hadolint-$(uname -s)-$(uname -m) -o /usr/local/bin/hadolint
- sudo chmod +rx /usr/local/bin/hadolint
- rustup set profile minimal
# Nothing to install
install: true

712
Cargo.lock

File diff suppressed because it is too large

26
Cargo.toml

@ -14,6 +14,7 @@ build = "build.rs"
# Empty to keep compatibility, prefer to set USE_SYSLOG=true
enable_syslog = []
mysql = ["diesel/mysql", "diesel_migrations/mysql"]
postgresql = ["diesel/postgres", "diesel_migrations/postgres", "openssl"]
sqlite = ["diesel/sqlite", "diesel_migrations/sqlite", "libsqlite3-sys"]
[target."cfg(not(windows))".dependencies]
@ -25,35 +26,35 @@ rocket = { version = "0.5.0-dev", features = ["tls"], default-features = false }
rocket_contrib = "0.5.0-dev"
# HTTP client
reqwest = "0.9.20"
reqwest = "0.9.22"
# multipart/form-data support
multipart = { version = "0.16.1", features = ["server"], default-features = false }
# WebSockets library
ws = "0.9.0"
ws = "0.9.1"
# MessagePack library
rmpv = "0.4.1"
rmpv = "0.4.2"
# Concurrent hashmap implementation
chashmap = "2.2.2"
# A generic serialization/deserialization framework
serde = "1.0.99"
serde_derive = "1.0.99"
serde_json = "1.0.40"
serde = "1.0.101"
serde_derive = "1.0.101"
serde_json = "1.0.41"
# Logging
log = "0.4.8"
fern = { version = "0.5.8", features = ["syslog-4"] }
# A safe, extensible ORM and Query builder
diesel = { version = "1.4.2", features = [ "chrono", "r2d2"] }
diesel = { version = "1.4.3", features = [ "chrono", "r2d2"] }
diesel_migrations = "1.4.0"
# Bundled SQLite
libsqlite3-sys = { version = "0.12.0", features = ["bundled"], optional = true }
libsqlite3-sys = { version = "0.16.0", features = ["bundled"], optional = true }
# Crypto library
ring = "0.14.6"
@ -77,7 +78,7 @@ jsonwebtoken = "6.0.1"
u2f = "0.1.6"
# Yubico Library
yubico = { version = "0.6.1", features = ["online", "online-tokio"], default-features = false }
yubico = { version = "0.7.1", features = ["online-tokio"], default-features = false }
# A `dotenv` implementation for Rust
dotenv = { version = "0.14.1", default-features = false }
@ -90,7 +91,7 @@ derive_more = "0.15.0"
# Numerical libraries
num-traits = "0.2.8"
num-derive = "0.2.5"
num-derive = "0.3.0"
# Email libraries
lettre = "0.9.2"
@ -105,12 +106,15 @@ handlebars = "2.0.2"
soup = "0.4.1"
regex = "1.3.1"
# Required for SSL support for PostgreSQL
openssl = { version = "0.10.25", optional = true }
# URL encoding library
percent-encoding = "2.1.0"
[patch.crates-io]
# Add support for Timestamp type
rmp = { git = 'https://github.com/dani-garcia/msgpack-rust', branch='ext_types' }
rmp = { git = 'https://github.com/3Hren/msgpack-rust', rev = 'd6c6c672e470341207ed9feb69b56322b5597a11' }
# Use newest ring
rocket = { git = 'https://github.com/SergioBenitez/Rocket', rev = 'dbcb0a75b9556763ac3ab708f40c8f8ed75f1a1e' }

2
README.md

@ -50,6 +50,6 @@ See the [bitwarden_rs wiki](https://github.com/dani-garcia/bitwarden_rs/wiki) fo
## Get in touch
To ask an question, [raising an issue](https://github.com/dani-garcia/bitwarden_rs/issues/new) is fine, also please report any bugs spotted here.
To ask a question, [raising an issue](https://github.com/dani-garcia/bitwarden_rs/issues/new) is fine. Please also report any bugs spotted here.
If you prefer to chat, we're usually hanging around at [#bitwarden_rs:matrix.org](https://matrix.to/#/#bitwarden_rs:matrix.org) room on Matrix. Feel free to join us!

2
azure-pipelines.yml

@ -4,7 +4,7 @@ pool:
steps:
- script: |
ls -la
curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain $(cat rust-toolchain)
curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain $(cat rust-toolchain) --profile=minimal
echo "##vso[task.prependpath]$HOME/.cargo/bin"
displayName: 'Install Rust'

8
build.rs

@ -2,9 +2,13 @@ use std::process::Command;
fn main() {
#[cfg(all(feature = "sqlite", feature = "mysql"))]
compile_error!("Can't enable both backends");
compile_error!("Can't enable both sqlite and mysql at the same time");
#[cfg(all(feature = "sqlite", feature = "postgresql"))]
compile_error!("Can't enable both sqlite and postgresql at the same time");
#[cfg(all(feature = "mysql", feature = "postgresql"))]
compile_error!("Can't enable both mysql and postgresql at the same time");
#[cfg(not(any(feature = "sqlite", feature = "mysql")))]
#[cfg(not(any(feature = "sqlite", feature = "mysql", feature = "postgresql")))]
compile_error!("You need to enable one DB backend. To build with previous defaults do: cargo build --features sqlite");
read_git_info().ok();

2
docker/aarch64/mysql/Dockerfile

@ -100,7 +100,7 @@ COPY --from=build /app/target/aarch64-unknown-linux-gnu/release/bitwarden_rs .
COPY docker/healthcheck.sh ./healthcheck.sh
HEALTHCHECK --interval=10s --timeout=1s CMD sh healthcheck.sh || exit 1
HEALTHCHECK --interval=30s --timeout=3s CMD sh healthcheck.sh || exit 1
# Configures the startup!
CMD ["./bitwarden_rs"]

3
docker/aarch64/sqlite/Dockerfile

@ -82,6 +82,7 @@ RUN apt-get update && apt-get install -y \
openssl \
ca-certificates \
curl \
sqlite3 \
&& rm -rf /var/lib/apt/lists/*
RUN mkdir /data
@ -99,7 +100,7 @@ COPY --from=build /app/target/aarch64-unknown-linux-gnu/release/bitwarden_rs .
COPY docker/healthcheck.sh ./healthcheck.sh
HEALTHCHECK --interval=10s --timeout=1s CMD sh healthcheck.sh || exit 1
HEALTHCHECK --interval=30s --timeout=3s CMD sh healthcheck.sh || exit 1
# Configures the startup!
CMD ["./bitwarden_rs"]

2
docker/amd64/mysql/Dockerfile

@ -97,7 +97,7 @@ COPY --from=build app/target/release/bitwarden_rs .
COPY docker/healthcheck.sh ./healthcheck.sh
HEALTHCHECK --interval=10s --timeout=1s CMD sh healthcheck.sh || exit 1
HEALTHCHECK --interval=30s --timeout=3s CMD sh healthcheck.sh || exit 1
# Configures the startup!
CMD ["./bitwarden_rs"]

2
docker/amd64/mysql/Dockerfile.alpine

@ -79,7 +79,7 @@ COPY --from=build /app/target/x86_64-unknown-linux-musl/release/bitwarden_rs .
COPY docker/healthcheck.sh ./healthcheck.sh
HEALTHCHECK --interval=10s --timeout=1s CMD sh healthcheck.sh || exit 1
HEALTHCHECK --interval=30s --timeout=3s CMD sh healthcheck.sh || exit 1
# Configures the startup!
CMD ["./bitwarden_rs"]

104
docker/amd64/postgresql/Dockerfile

@ -0,0 +1,104 @@
# 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:3.10 as vault
ENV VAULT_VERSION "v2.12.0"
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
RUN apk add --no-cache --upgrade \
curl \
tar
RUN mkdir /web-vault
WORKDIR /web-vault
SHELL ["/bin/ash", "-eo", "pipefail", "-c"]
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:1.36 as build
# set mysql backend
ARG DB=postgresql
# Using bundled SQLite, no need to install it
# RUN apt-get update && apt-get install -y\
# --no-install-recommends \
# sqlite3\
# && rm -rf /var/lib/apt/lists/*
# Install MySQL package
RUN apt-get update && apt-get install -y \
--no-install-recommends \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*
# Creates a dummy project used to grab dependencies
RUN USER=root cargo new --bin app
WORKDIR /app
# Copies over *only* your manifests and build files
COPY ./Cargo.* ./
COPY ./rust-toolchain ./rust-toolchain
COPY ./build.rs ./build.rs
# Builds your dependencies and removes the
# dummy project, except the target folder
# This folder contains the compiled dependencies
RUN cargo build --features ${DB} --release
RUN find . -not -path "./target*" -delete
# Copies the complete project
# To avoid copying unneeded files, use .dockerignore
COPY . .
# Make sure that we actually build the project
RUN touch src/main.rs
# Builds again, this time it'll just be
# your actual source files being built
RUN cargo build --features ${DB} --release
######################## RUNTIME IMAGE ########################
# Create a new stage with a minimal image
# because we already have a binary built
FROM debian:stretch-slim
ENV ROCKET_ENV "staging"
ENV ROCKET_PORT=80
ENV ROCKET_WORKERS=10
# Install needed libraries
RUN apt-get update && apt-get install -y \
--no-install-recommends \
openssl \
ca-certificates \
curl \
sqlite3 \
libpq5 \
&& rm -rf /var/lib/apt/lists/*
RUN mkdir /data
VOLUME /data
EXPOSE 80
EXPOSE 3012
# 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/release/bitwarden_rs .
COPY docker/healthcheck.sh ./healthcheck.sh
HEALTHCHECK --interval=30s --timeout=3s CMD sh healthcheck.sh || exit 1
# Configures the startup!
CMD ["./bitwarden_rs"]

86
docker/amd64/postgresql/Dockerfile.alpine

@ -0,0 +1,86 @@
# 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:3.10 as vault
ENV VAULT_VERSION "v2.12.0"
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
RUN apk add --no-cache --upgrade \
curl \
tar
RUN mkdir /web-vault
WORKDIR /web-vault
SHELL ["/bin/ash", "-eo", "pipefail", "-c"]
RUN curl -L $URL | tar xz
RUN ls
########################## BUILD IMAGE ##########################
# Musl build image for statically compiled binary
FROM clux/muslrust:nightly-2019-07-08 as build
# set mysql backend
ARG DB=postgresql
ENV USER "root"
# Install needed libraries
RUN apt-get update && apt-get install -y \
--no-install-recommends \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Copies the complete project
# To avoid copying unneeded files, use .dockerignore
COPY . .
RUN rustup target add x86_64-unknown-linux-musl
# Make sure that we actually build the project
RUN touch src/main.rs
# Build
RUN cargo build --features ${DB} --release
######################## RUNTIME IMAGE ########################
# Create a new stage with a minimal image
# because we already have a binary built
FROM alpine:3.10
ENV ROCKET_ENV "staging"
ENV ROCKET_PORT=80
ENV ROCKET_WORKERS=10
ENV SSL_CERT_DIR=/etc/ssl/certs
# Install needed libraries
RUN apk add --no-cache \
openssl \
postgresql-libs \
curl \
sqlite \
ca-certificates
RUN mkdir /data
VOLUME /data
EXPOSE 80
EXPOSE 3012
# 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/x86_64-unknown-linux-musl/release/bitwarden_rs .
COPY docker/healthcheck.sh ./healthcheck.sh
HEALTHCHECK --interval=30s --timeout=3s CMD sh healthcheck.sh || exit 1
# Configures the startup!
CMD ["./bitwarden_rs"]

3
docker/amd64/sqlite/Dockerfile

@ -81,6 +81,7 @@ RUN apt-get update && apt-get install -y \
openssl \
ca-certificates \
curl \
sqlite3 \
&& rm -rf /var/lib/apt/lists/*
RUN mkdir /data
@ -96,7 +97,7 @@ COPY --from=build app/target/release/bitwarden_rs .
COPY docker/healthcheck.sh ./healthcheck.sh
HEALTHCHECK --interval=10s --timeout=1s CMD sh healthcheck.sh || exit 1
HEALTHCHECK --interval=30s --timeout=3s CMD sh healthcheck.sh || exit 1
# Configures the startup!
CMD ["./bitwarden_rs"]

3
docker/amd64/sqlite/Dockerfile.alpine

@ -63,6 +63,7 @@ ENV SSL_CERT_DIR=/etc/ssl/certs
RUN apk add --no-cache \
openssl \
curl \
sqlite \
ca-certificates
RUN mkdir /data
@ -78,7 +79,7 @@ COPY --from=build /app/target/x86_64-unknown-linux-musl/release/bitwarden_rs .
COPY docker/healthcheck.sh ./healthcheck.sh
HEALTHCHECK --interval=10s --timeout=1s CMD sh healthcheck.sh || exit 1
HEALTHCHECK --interval=30s --timeout=3s CMD sh healthcheck.sh || exit 1
# Configures the startup!

2
docker/armv6/mysql/Dockerfile

@ -100,7 +100,7 @@ COPY --from=build /app/target/arm-unknown-linux-gnueabi/release/bitwarden_rs .
COPY docker/healthcheck.sh ./healthcheck.sh
HEALTHCHECK --interval=10s --timeout=1s CMD sh healthcheck.sh || exit 1
HEALTHCHECK --interval=30s --timeout=3s CMD sh healthcheck.sh || exit 1
# Configures the startup!
CMD ["./bitwarden_rs"]

3
docker/armv6/sqlite/Dockerfile

@ -82,6 +82,7 @@ RUN apt-get update && apt-get install -y \
openssl \
ca-certificates \
curl \
sqlite3 \
&& rm -rf /var/lib/apt/lists/*
RUN mkdir /data
@ -99,7 +100,7 @@ COPY --from=build /app/target/arm-unknown-linux-gnueabi/release/bitwarden_rs .
COPY docker/healthcheck.sh ./healthcheck.sh
HEALTHCHECK --interval=10s --timeout=1s CMD sh healthcheck.sh || exit 1
HEALTHCHECK --interval=30s --timeout=3s CMD sh healthcheck.sh || exit 1
# Configures the startup!
CMD ["./bitwarden_rs"]

2
docker/armv7/mysql/Dockerfile

@ -101,7 +101,7 @@ COPY --from=build /app/target/armv7-unknown-linux-gnueabihf/release/bitwarden_rs
COPY docker/healthcheck.sh ./healthcheck.sh
HEALTHCHECK --interval=10s --timeout=1s CMD sh healthcheck.sh || exit 1
HEALTHCHECK --interval=30s --timeout=3s CMD sh healthcheck.sh || exit 1
# Configures the startup!
CMD ["./bitwarden_rs"]

3
docker/armv7/sqlite/Dockerfile

@ -82,6 +82,7 @@ RUN apt-get update && apt-get install -y \
openssl \
ca-certificates \
curl \
sqlite3 \
&& rm -rf /var/lib/apt/lists/*
RUN mkdir /data
@ -99,7 +100,7 @@ COPY --from=build /app/target/armv7-unknown-linux-gnueabihf/release/bitwarden_rs
COPY docker/healthcheck.sh ./healthcheck.sh
HEALTHCHECK --interval=10s --timeout=1s CMD sh healthcheck.sh || exit 1
HEALTHCHECK --interval=30s --timeout=3s CMD sh healthcheck.sh || exit 1
# Configures the startup!
CMD ["./bitwarden_rs"]

2
docker/healthcheck.sh

@ -4,5 +4,5 @@ if [ -z "$ROCKET_TLS"]
then
curl --fail http://localhost:${ROCKET_PORT:-"80"}/alive || exit 1
else
curl --fail https://localhost:${ROCKET_PORT:-"80"}/alive || exit 1
curl --insecure --fail https://localhost:${ROCKET_PORT:-"80"}/alive || exit 1
fi

0
migrations/mysql/2019-10-10-083032_add_column_to_twofactor/down.sql

1
migrations/mysql/2019-10-10-083032_add_column_to_twofactor/up.sql

@ -0,0 +1 @@
ALTER TABLE twofactor ADD COLUMN last_used INTEGER NOT NULL DEFAULT 0;

13
migrations/postgresql/2019-09-12-100000_create_tables/down.sql

@ -0,0 +1,13 @@
DROP TABLE devices;
DROP TABLE attachments;
DROP TABLE users_collections;
DROP TABLE users_organizations;
DROP TABLE folders_ciphers;
DROP TABLE ciphers_collections;
DROP TABLE twofactor;
DROP TABLE invitations;
DROP TABLE collections;
DROP TABLE folders;
DROP TABLE ciphers;
DROP TABLE users;
DROP TABLE organizations;

121
migrations/postgresql/2019-09-12-100000_create_tables/up.sql

@ -0,0 +1,121 @@
CREATE TABLE users (
uuid CHAR(36) NOT NULL PRIMARY KEY,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE,
name TEXT NOT NULL,
password_hash BYTEA NOT NULL,
salt BYTEA NOT NULL,
password_iterations INTEGER NOT NULL,
password_hint TEXT,
akey TEXT NOT NULL,
private_key TEXT,
public_key TEXT,
totp_secret TEXT,
totp_recover TEXT,
security_stamp TEXT NOT NULL,
equivalent_domains TEXT NOT NULL,
excluded_globals TEXT NOT NULL,
client_kdf_type INTEGER NOT NULL DEFAULT 0,
client_kdf_iter INTEGER NOT NULL DEFAULT 100000
);
CREATE TABLE devices (
uuid CHAR(36) NOT NULL PRIMARY KEY,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
user_uuid CHAR(36) NOT NULL REFERENCES users (uuid),
name TEXT NOT NULL,
atype INTEGER NOT NULL,
push_token TEXT,
refresh_token TEXT NOT NULL,
twofactor_remember TEXT
);
CREATE TABLE organizations (
uuid VARCHAR(40) NOT NULL PRIMARY KEY,
name TEXT NOT NULL,
billing_email TEXT NOT NULL
);
CREATE TABLE ciphers (
uuid CHAR(36) NOT NULL PRIMARY KEY,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
user_uuid CHAR(36) REFERENCES users (uuid),
organization_uuid CHAR(36) REFERENCES organizations (uuid),
atype INTEGER NOT NULL,
name TEXT NOT NULL,
notes TEXT,
fields TEXT,
data TEXT NOT NULL,
favorite BOOLEAN NOT NULL,
password_history TEXT
);
CREATE TABLE attachments (
id CHAR(36) NOT NULL PRIMARY KEY,
cipher_uuid CHAR(36) NOT NULL REFERENCES ciphers (uuid),
file_name TEXT NOT NULL,
file_size INTEGER NOT NULL,
akey TEXT
);
CREATE TABLE folders (
uuid CHAR(36) NOT NULL PRIMARY KEY,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
user_uuid CHAR(36) NOT NULL REFERENCES users (uuid),
name TEXT NOT NULL
);
CREATE TABLE collections (
uuid VARCHAR(40) NOT NULL PRIMARY KEY,
org_uuid VARCHAR(40) NOT NULL REFERENCES organizations (uuid),
name TEXT NOT NULL
);
CREATE TABLE users_collections (
user_uuid CHAR(36) NOT NULL REFERENCES users (uuid),
collection_uuid CHAR(36) NOT NULL REFERENCES collections (uuid),
read_only BOOLEAN NOT NULL DEFAULT false,
PRIMARY KEY (user_uuid, collection_uuid)
);
CREATE TABLE users_organizations (
uuid CHAR(36) NOT NULL PRIMARY KEY,
user_uuid CHAR(36) NOT NULL REFERENCES users (uuid),
org_uuid CHAR(36) NOT NULL REFERENCES organizations (uuid),
access_all BOOLEAN NOT NULL,
akey TEXT NOT NULL,
status INTEGER NOT NULL,
atype INTEGER NOT NULL,
UNIQUE (user_uuid, org_uuid)
);
CREATE TABLE folders_ciphers (
cipher_uuid CHAR(36) NOT NULL REFERENCES ciphers (uuid),
folder_uuid CHAR(36) NOT NULL REFERENCES folders (uuid),
PRIMARY KEY (cipher_uuid, folder_uuid)
);
CREATE TABLE ciphers_collections (
cipher_uuid CHAR(36) NOT NULL REFERENCES ciphers (uuid),
collection_uuid CHAR(36) NOT NULL REFERENCES collections (uuid),
PRIMARY KEY (cipher_uuid, collection_uuid)
);
CREATE TABLE twofactor (
uuid CHAR(36) NOT NULL PRIMARY KEY,
user_uuid CHAR(36) NOT NULL REFERENCES users (uuid),
atype INTEGER NOT NULL,
enabled BOOLEAN NOT NULL,
data TEXT NOT NULL,
UNIQUE (user_uuid, atype)
);
CREATE TABLE invitations (
email VARCHAR(255) NOT NULL PRIMARY KEY
);

26
migrations/postgresql/2019-09-16-150000_fix_attachments/down.sql

@ -0,0 +1,26 @@
ALTER TABLE attachments ALTER COLUMN id TYPE CHAR(36);
ALTER TABLE attachments ALTER COLUMN cipher_uuid TYPE CHAR(36);
ALTER TABLE users ALTER COLUMN uuid TYPE CHAR(36);
ALTER TABLE users ALTER COLUMN email TYPE VARCHAR(255);
ALTER TABLE devices ALTER COLUMN uuid TYPE CHAR(36);
ALTER TABLE devices ALTER COLUMN user_uuid TYPE CHAR(36);
ALTER TABLE organizations ALTER COLUMN uuid TYPE CHAR(40);
ALTER TABLE ciphers ALTER COLUMN uuid TYPE CHAR(36);
ALTER TABLE ciphers ALTER COLUMN user_uuid TYPE CHAR(36);
ALTER TABLE ciphers ALTER COLUMN organization_uuid TYPE CHAR(36);
ALTER TABLE folders ALTER COLUMN uuid TYPE CHAR(36);
ALTER TABLE folders ALTER COLUMN user_uuid TYPE CHAR(36);
ALTER TABLE collections ALTER COLUMN uuid TYPE CHAR(40);
ALTER TABLE collections ALTER COLUMN org_uuid TYPE CHAR(40);
ALTER TABLE users_collections ALTER COLUMN user_uuid TYPE CHAR(36);
ALTER TABLE users_collections ALTER COLUMN collection_uuid TYPE CHAR(36);
ALTER TABLE users_organizations ALTER COLUMN uuid TYPE CHAR(36);
ALTER TABLE users_organizations ALTER COLUMN user_uuid TYPE CHAR(36);
ALTER TABLE users_organizations ALTER COLUMN org_uuid TYPE CHAR(36);
ALTER TABLE folders_ciphers ALTER COLUMN cipher_uuid TYPE CHAR(36);
ALTER TABLE folders_ciphers ALTER COLUMN folder_uuid TYPE CHAR(36);
ALTER TABLE ciphers_collections ALTER COLUMN cipher_uuid TYPE CHAR(36);
ALTER TABLE ciphers_collections ALTER COLUMN collection_uuid TYPE CHAR(36);
ALTER TABLE twofactor ALTER COLUMN uuid TYPE CHAR(36);
ALTER TABLE twofactor ALTER COLUMN user_uuid TYPE CHAR(36);
ALTER TABLE invitations ALTER COLUMN email TYPE VARCHAR(255);

27
migrations/postgresql/2019-09-16-150000_fix_attachments/up.sql

@ -0,0 +1,27 @@
-- Switch from CHAR() types to VARCHAR() types to avoid padding issues.
ALTER TABLE attachments ALTER COLUMN id TYPE TEXT;
ALTER TABLE attachments ALTER COLUMN cipher_uuid TYPE VARCHAR(40);
ALTER TABLE users ALTER COLUMN uuid TYPE VARCHAR(40);
ALTER TABLE users ALTER COLUMN email TYPE TEXT;
ALTER TABLE devices ALTER COLUMN uuid TYPE VARCHAR(40);
ALTER TABLE devices ALTER COLUMN user_uuid TYPE VARCHAR(40);
ALTER TABLE organizations ALTER COLUMN uuid TYPE VARCHAR(40);
ALTER TABLE ciphers ALTER COLUMN uuid TYPE VARCHAR(40);
ALTER TABLE ciphers ALTER COLUMN user_uuid TYPE VARCHAR(40);
ALTER TABLE ciphers ALTER COLUMN organization_uuid TYPE VARCHAR(40);
ALTER TABLE folders ALTER COLUMN uuid TYPE VARCHAR(40);
ALTER TABLE folders ALTER COLUMN user_uuid TYPE VARCHAR(40);
ALTER TABLE collections ALTER COLUMN uuid TYPE VARCHAR(40);
ALTER TABLE collections ALTER COLUMN org_uuid TYPE VARCHAR(40);
ALTER TABLE users_collections ALTER COLUMN user_uuid TYPE VARCHAR(40);
ALTER TABLE users_collections ALTER COLUMN collection_uuid TYPE VARCHAR(40);
ALTER TABLE users_organizations ALTER COLUMN uuid TYPE VARCHAR(40);
ALTER TABLE users_organizations ALTER COLUMN user_uuid TYPE VARCHAR(40);
ALTER TABLE users_organizations ALTER COLUMN org_uuid TYPE VARCHAR(40);
ALTER TABLE folders_ciphers ALTER COLUMN cipher_uuid TYPE VARCHAR(40);
ALTER TABLE folders_ciphers ALTER COLUMN folder_uuid TYPE VARCHAR(40);
ALTER TABLE ciphers_collections ALTER COLUMN cipher_uuid TYPE VARCHAR(40);
ALTER TABLE ciphers_collections ALTER COLUMN collection_uuid TYPE VARCHAR(40);
ALTER TABLE twofactor ALTER COLUMN uuid TYPE VARCHAR(40);
ALTER TABLE twofactor ALTER COLUMN user_uuid TYPE VARCHAR(40);
ALTER TABLE invitations ALTER COLUMN email TYPE TEXT;

0
migrations/postgresql/2019-10-10-083032_add_column_to_twofactor/down.sql

1
migrations/postgresql/2019-10-10-083032_add_column_to_twofactor/up.sql

@ -0,0 +1 @@
ALTER TABLE twofactor ADD COLUMN last_used INTEGER NOT NULL DEFAULT 0;

0
migrations/sqlite/2019-10-10-083032_add_column_to_twofactor/down.sql

1
migrations/sqlite/2019-10-10-083032_add_column_to_twofactor/up.sql

@ -0,0 +1 @@
ALTER TABLE twofactor ADD COLUMN last_used INTEGER NOT NULL DEFAULT 0;

2
rust-toolchain

@ -1 +1 @@
nightly-2019-08-27
nightly-2019-10-14

2
src/api/admin.rs

@ -37,7 +37,7 @@ pub fn routes() -> Vec<Route> {
}
lazy_static! {
static ref CAN_BACKUP: bool = cfg!(feature = "sqlite") && Command::new("sqlite").arg("-version").status().is_ok();
static ref CAN_BACKUP: bool = cfg!(feature = "sqlite") && Command::new("sqlite3").arg("-version").status().is_ok();
}
#[get("/")]

21
src/api/core/mod.rs

@ -141,8 +141,11 @@ fn hibp_breach(username: String) -> JsonResult {
use reqwest::{header::USER_AGENT, Client};
if let Some(api_key) = crate::CONFIG.hibp_api_key() {
let res = Client::new()
.get(&url)
let hibp_client = Client::builder()
.use_sys_proxy()
.build()?;
let res = hibp_client.get(&url)
.header(USER_AGENT, user_agent)
.header("hibp-api-key", api_key)
.send()?;
@ -156,9 +159,17 @@ fn hibp_breach(username: String) -> JsonResult {
Ok(Json(value))
} else {
Ok(Json(json!([{
"title": "--- Error! ---",
"description": "HaveIBeenPwned API key not set! Go to https://haveibeenpwned.com/API/Key",
"logopath": "/bwrs_static/error-x.svg"
"Name": "HaveIBeenPwned",
"Title": "Manual HIBP Check",
"Domain": "haveibeenpwned.com",
"BreachDate": "2019-08-18T00:00:00Z",
"AddedDate": "2019-08-18T00:00:00Z",
"Description": format!("Go to: <a href=\"https://haveibeenpwned.com/account/{account}\" target=\"_blank\" rel=\"noopener\">https://haveibeenpwned.com/account/{account}</a> for a manual check.<br/><br/>HaveIBeenPwned API key not set!<br/>Go to <a href=\"https://haveibeenpwned.com/API/Key\" target=\"_blank\" rel=\"noopener\">https://haveibeenpwned.com/API/Key</a> to purchase an API key from HaveIBeenPwned.<br/><br/>", account=username),
"LogoPath": "/bwrs_static/hibp.png",
"PwnCount": 0,
"DataClasses": [
"Error - No API key set!"
]
}])))
}
}

59
src/api/core/two_factor/authenticator.rs

@ -73,14 +73,10 @@ fn activate_authenticator(data: JsonUpcase<EnableAuthenticatorData>, headers: He
err!("Invalid key length")
}
let type_ = TwoFactorType::Authenticator;
let twofactor = TwoFactor::new(user.uuid.clone(), type_, key.to_uppercase());
// Validate the token provided with the key
validate_totp_code(token, &twofactor.data)?;
// Validate the token provided with the key, and save new twofactor
validate_totp_code(&user.uuid, token, &key.to_uppercase(), &conn)?;
_generate_recover_code(&mut user, &conn);
twofactor.save(&conn)?;
Ok(Json(json!({
"Enabled": true,
@ -94,27 +90,62 @@ fn activate_authenticator_put(data: JsonUpcase<EnableAuthenticatorData>, headers
activate_authenticator(data, headers, conn)
}
pub fn validate_totp_code_str(totp_code: &str, secret: &str) -> EmptyResult {
pub fn validate_totp_code_str(user_uuid: &str, totp_code: &str, secret: &str, conn: &DbConn) -> EmptyResult {
let totp_code: u64 = match totp_code.parse() {
Ok(code) => code,
_ => err!("TOTP code is not a number"),
};
validate_totp_code(totp_code, secret)
validate_totp_code(user_uuid, totp_code, secret, &conn)
}
pub fn validate_totp_code(totp_code: u64, secret: &str) -> EmptyResult {
use oath::{totp_raw_now, HashType};
pub fn validate_totp_code(user_uuid: &str, totp_code: u64, secret: &str, conn: &DbConn) -> EmptyResult {
use oath::{totp_raw_custom_time, HashType};
use std::time::{UNIX_EPOCH, SystemTime};
let decoded_secret = match BASE32.decode(secret.as_bytes()) {
Ok(s) => s,
Err(_) => err!("Invalid TOTP secret"),
};
let generated = totp_raw_now(&decoded_secret, 6, 0, 30, &HashType::SHA1);
if generated != totp_code {
err!("Invalid TOTP code");
let mut twofactor = match TwoFactor::find_by_user_and_type(&user_uuid, TwoFactorType::Authenticator as i32, &conn) {
Some(tf) => tf,
_ => TwoFactor::new(user_uuid.to_string(), TwoFactorType::Authenticator, secret.to_string()),
};
// Get the current system time in UNIX Epoch (UTC)
let current_time: u64 = SystemTime::now().duration_since(UNIX_EPOCH)
.expect("Earlier than 1970-01-01 00:00:00 UTC").as_secs();
// The amount of steps back and forward in time
let steps = 1;
for step in -steps..=steps {
let time_step = (current_time / 30) as i32 + step;
// We need to calculate the time offsite and cast it as an i128.
// Else we can't do math with it on a default u64 variable.
let time_offset: i128 = (step * 30).into();
let generated = totp_raw_custom_time(&decoded_secret, 6, 0, 30, (current_time as i128 + time_offset) as u64, &HashType::SHA1);
// Check the the given code equals the generated and if the time_step is larger then the one last used.
if generated == totp_code && time_step > twofactor.last_used {
// If the step does not equals 0 the time is drifted either server or client side.
if step != 0 {
info!("TOTP Time drift detected. The step offset is {}", step);
}
// Save the last used time step so only totp time steps higher then this one are allowed.
// This will also save a newly created twofactor if the code is correct.
twofactor.last_used = time_step;
twofactor.save(&conn)?;
return Ok(());
} else if generated == totp_code && time_step <= twofactor.last_used {
warn!("This or a TOTP code within {} steps back and forward has already been used!", steps);
err!("Invalid TOTP Code!");
}
}
Ok(())
// Else no valide code received, deny access
err!("Invalid TOTP code!");
}

10
src/api/core/two_factor/email.rs

@ -55,10 +55,18 @@ fn send_email_login(data: JsonUpcase<SendEmailLoginData>, conn: DbConn) -> Empty
err!("Email 2FA is disabled")
}
send_token(&user.uuid, &conn)?;
Ok(())
}
/// Generate the token, save the data for later verification and send email to user
pub fn send_token(user_uuid: &str, conn: &DbConn) -> EmptyResult {
let type_ = TwoFactorType::Email as i32;
let mut twofactor = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn)?;
let mut twofactor = TwoFactor::find_by_user_and_type(user_uuid, type_, &conn)?;
let generated_token = generate_token(CONFIG.email_token_size())?;
let mut twofactor_data = EmailTokenData::from_json(&twofactor.data)?;
twofactor_data.set_token(generated_token);
twofactor.data = twofactor_data.to_json();

68
src/api/icons.rs

@ -1,12 +1,13 @@
use std::fs::{create_dir_all, remove_file, symlink_metadata, File};
use std::io::prelude::*;
use std::net::ToSocketAddrs;
use std::time::{Duration, SystemTime};
use rocket::http::ContentType;
use rocket::response::Content;
use rocket::Route;
use reqwest::{header::HeaderMap, Client, Response};
use reqwest::{header::HeaderMap, Client, Response, Url};
use rocket::http::Cookie;
@ -60,20 +61,38 @@ fn icon(domain: String) -> Content<Vec<u8>> {
return Content(icon_type, FALLBACK_ICON.to_vec());
}
if let Some(blacklist) = CONFIG.icon_blacklist_regex() {
info!("Icon blacklist enabled: {:#?}", blacklist);
Content(icon_type, get_icon(&domain))
}
let regex = Regex::new(&blacklist).expect("Valid Regex");
fn check_icon_domain_is_blacklisted(domain: &str) -> bool {
let mut is_blacklisted = false;
if CONFIG.icon_blacklist_non_global_ips() {
is_blacklisted = (domain, 0)
.to_socket_addrs()
.map(|x| {
for ip_port in x {
if !ip_port.ip().is_global() {
warn!("IP {} for domain '{}' is not a global IP!", ip_port.ip(), domain);
return true;
}
}
false
})
.unwrap_or(false);
}
if regex.is_match(&domain) {
warn!("Blacklisted domain: {:#?}", domain);
return Content(icon_type, FALLBACK_ICON.to_vec());
// Skip the regex check if the previous one is true already
if !is_blacklisted {
if let Some(blacklist) = CONFIG.icon_blacklist_regex() {
let regex = Regex::new(&blacklist).expect("Valid Regex");
if regex.is_match(&domain) {
warn!("Blacklisted domain: {:#?} matched {:#?}", domain, blacklist);
is_blacklisted = true;
}
}
}
let icon = get_icon(&domain);
Content(icon_type, icon)
is_blacklisted
}
fn get_icon(domain: &str) -> Vec<u8> {
@ -202,6 +221,7 @@ fn get_icon_url(domain: &str) -> Result<(Vec<Icon>, String), Error> {
if let Ok(content) = resp {
// 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()
@ -253,12 +273,24 @@ fn get_page(url: &str) -> Result<Response, Error> {
}
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)
if check_icon_domain_is_blacklisted(Url::parse(url).unwrap().host_str().unwrap_or_default()) {
err!("Favicon rel linked to a non blacklisted domain!");
}
if cookie_str.is_empty() {
CLIENT
.get(url)
.send()?
.error_for_status()
.map_err(Into::into)
} else {
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.
@ -341,6 +373,10 @@ fn parse_sizes(sizes: Option<String>) -> (u16, u16) {
}
fn download_icon(domain: &str) -> Result<Vec<u8>, Error> {
if check_icon_domain_is_blacklisted(domain) {
err!("Domain is blacklisted", domain)
}
let (iconlist, cookie_str) = get_icon_url(&domain)?;
let mut buffer = Vec::new();

10
src/api/identity.rs

@ -197,7 +197,7 @@ fn twofactor_auth(
let mut remember = data.two_factor_remember.unwrap_or(0);
match TwoFactorType::from_i32(selected_id) {
Some(TwoFactorType::Authenticator) => _tf::authenticator::validate_totp_code_str(twofactor_code, &selected_data?)?,
Some(TwoFactorType::Authenticator) => _tf::authenticator::validate_totp_code_str(user_uuid, twofactor_code, &selected_data?, conn)?,
Some(TwoFactorType::U2f) => _tf::u2f::validate_u2f_login(user_uuid, twofactor_code, conn)?,
Some(TwoFactorType::YubiKey) => _tf::yubikey::validate_yubikey_login(twofactor_code, &selected_data?)?,
Some(TwoFactorType::Duo) => _tf::duo::validate_duo_login(data.username.as_ref().unwrap(), twofactor_code, conn)?,
@ -293,13 +293,19 @@ fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &DbConn) -> Api
}
Some(tf_type @ TwoFactorType::Email) => {
use crate::api::core::two_factor as _tf;
let twofactor = match TwoFactor::find_by_user_and_type(user_uuid, tf_type as i32, &conn) {
Some(tf) => tf,
None => err!("No twofactor email registered"),
};
let email_data = EmailTokenData::from_json(&twofactor.data)?;
// Send email immediately if email is the only 2FA option
if providers.len() == 1 {
_tf::email::send_token(&user_uuid, &conn)?
}
let email_data = EmailTokenData::from_json(&twofactor.data)?;
result["TwoFactorProviders2"][provider.to_string()] = json!({
"Email": email::obscure_email(&email_data.email),
})

14
src/api/web.rs

@ -1,4 +1,3 @@
use std::io;
use std::path::{Path, PathBuf};
use rocket::http::ContentType;
@ -21,10 +20,10 @@ pub fn routes() -> Vec<Route> {
}
#[get("/")]
fn web_index() -> Cached<io::Result<NamedFile>> {
fn web_index() -> Cached<Option<NamedFile>> {
Cached::short(NamedFile::open(
Path::new(&CONFIG.web_vault_folder()).join("index.html"),
))
).ok())
}
#[get("/app-id.json")]
@ -47,13 +46,13 @@ fn app_id() -> Cached<Content<Json<Value>>> {
}
#[get("/<p..>", rank = 10)] // Only match this if the other routes don't match
fn web_files(p: PathBuf) -> Cached<io::Result<NamedFile>> {
Cached::long(NamedFile::open(Path::new(&CONFIG.web_vault_folder()).join(p)))
fn web_files(p: PathBuf) -> Cached<Option<NamedFile>> {
Cached::long(NamedFile::open(Path::new(&CONFIG.web_vault_folder()).join(p)).ok())
}
#[get("/attachments/<uuid>/<file..>")]
fn attachments(uuid: String, file: PathBuf) -> io::Result<NamedFile> {
NamedFile::open(Path::new(&CONFIG.attachments_folder()).join(uuid).join(file))
fn attachments(uuid: String, file: PathBuf) -> Option<NamedFile> {
NamedFile::open(Path::new(&CONFIG.attachments_folder()).join(uuid).join(file)).ok()
}
#[get("/alive")]
@ -70,6 +69,7 @@ fn static_files(filename: String) -> Result<Content<&'static [u8]>, Error> {
"mail-github.png" => Ok(Content(ContentType::PNG, include_bytes!("../static/images/mail-github.png"))),
"logo-gray.png" => Ok(Content(ContentType::PNG, include_bytes!("../static/images/logo-gray.png"))),
"error-x.svg" => Ok(Content(ContentType::SVG, include_bytes!("../static/images/error-x.svg"))),
"hibp.png" => Ok(Content(ContentType::PNG, include_bytes!("../static/images/hibp.png"))),
"bootstrap.css" => Ok(Content(ContentType::CSS, include_bytes!("../static/scripts/bootstrap.css"))),
"bootstrap-native-v4.js" => Ok(Content(ContentType::JavaScript, include_bytes!("../static/scripts/bootstrap-native-v4.js"))),

49
src/config.rs

@ -267,6 +267,9 @@ make_config! {
/// 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;
/// Icon blacklist non global IPs |> Any IP which is not defined as a global IP will be blacklisted.
/// Usefull to secure your internal environment: See https://en.wikipedia.org/wiki/Reserved_IP_addresses for a list of IPs which it will block
icon_blacklist_non_global_ips: bool, true, def, true;
/// 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.
@ -295,7 +298,7 @@ make_config! {
/// 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
/// Bypass admin page security (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;
},
@ -325,18 +328,6 @@ make_config! {
_duo_akey: Pass, false, option;
},
/// Email 2FA Settings
email_2fa: _enable_email_2fa {
/// Enabled |> Disabling will prevent users from setting up new email 2FA and using existing email 2FA configured
_enable_email_2fa: bool, true, auto, |c| c._enable_smtp && c.smtp_host.is_some();
/// Token number length |> Length of the numbers in an email token. Minimum of 6. Maximum is 19.
email_token_size: u32, true, def, 6;
/// Token expiration time |> Maximum time in seconds a token is valid. The time the user has to open email client and copy token.
email_expiration_time: u64, true, def, 600;
/// Maximum attempts |> Maximum attempts before an email token is reset and a new email will need to be sent
email_attempts_limit: u64, true, def, 3;
},
/// SMTP Email Settings
smtp: _enable_smtp {
/// Enabled
@ -360,9 +351,41 @@ make_config! {
/// Json form auth mechanism |> Defaults for ssl is "Plain" and "Login" and nothing for non-ssl connections. Possible values: ["Plain", "Login", "Xoauth2"]
smtp_auth_mechanism: String, true, option;
},
/// Email 2FA Settings
email_2fa: _enable_email_2fa {
/// Enabled |> Disabling will prevent users from setting up new email 2FA and using existing email 2FA configured
_enable_email_2fa: bool, true, auto, |c| c._enable_smtp && c.smtp_host.is_some();
/// Token number length |> Length of the numbers in an email token. Minimum of 6. Maximum is 19.
email_token_size: u32, true, def, 6;
/// Token expiration time |> Maximum time in seconds a token is valid. The time the user has to open email client and copy token.
email_expiration_time: u64, true, def, 600;
/// Maximum attempts |> Maximum attempts before an email token is reset and a new email will need to be sent
email_attempts_limit: u64, true, def, 3;
},
}
fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
let db_url = cfg.database_url.to_lowercase();
if cfg!(feature = "sqlite") {
if db_url.starts_with("mysql:") || db_url.starts_with("postgresql:") {
err!("`DATABASE_URL` is meant for MySQL or Postgres, while this server is meant for SQLite")
}
}
if cfg!(feature = "mysql") {
if !db_url.starts_with("mysql:") {
err!("`DATABASE_URL` should start with mysql: when using the MySQL server")
}
}
if cfg!(feature = "postgresql") {
if !db_url.starts_with("postgresql:") {
err!("`DATABASE_URL` should start with postgresql: when using the PostgreSQL server")
}
}
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`")

11
src/db/mod.rs

@ -19,6 +19,8 @@ use crate::CONFIG;
type Connection = diesel::sqlite::SqliteConnection;
#[cfg(feature = "mysql")]
type Connection = diesel::mysql::MysqlConnection;
#[cfg(feature = "postgresql")]
type Connection = diesel::pg::PgConnection;
/// An alias to the type for a pool of Diesel connections.
type Pool = r2d2::Pool<ConnectionManager<Connection>>;
@ -33,6 +35,9 @@ pub mod schema;
#[cfg(feature = "mysql")]
#[path = "schemas/mysql/schema.rs"]
pub mod schema;
#[cfg(feature = "postgresql")]
#[path = "schemas/postgresql/schema.rs"]
pub mod schema;
/// Initializes a database pool.
pub fn init_pool() -> Pool {
@ -47,12 +52,16 @@ pub fn get_connection() -> Result<Connection, ConnectionError> {
/// Creates a back-up of the database using sqlite3
pub fn backup_database() -> Result<(), Error> {
use std::path::Path;
let db_url = CONFIG.database_url();
let db_path = Path::new(&db_url).parent().unwrap();
let now: DateTime<Utc> = Utc::now();
let file_date = now.format("%Y%m%d").to_string();
let backup_command: String = format!("{}{}{}", ".backup 'db_", file_date, ".sqlite3'");
Command::new("sqlite3")
.current_dir("./data")
.current_dir(db_path)
.args(&["db.sqlite3", &backup_command])
.output()
.expect("Can't open database, sqlite3 is not available, make sure it's installed and available on the PATH");

14
src/db/models/attachment.rs

@ -3,7 +3,7 @@ use serde_json::Value;
use super::Cipher;
use crate::CONFIG;
#[derive(Debug, Identifiable, Queryable, Insertable, Associations)]
#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)]
#[table_name = "attachments"]
#[belongs_to(Cipher, foreign_key = "cipher_uuid")]
#[primary_key(id)]
@ -59,6 +59,18 @@ use crate::error::MapResult;
/// Database methods
impl Attachment {
#[cfg(feature = "postgresql")]
pub fn save(&self, conn: &DbConn) -> EmptyResult {
diesel::insert_into(attachments::table)
.values(self)
.on_conflict(attachments::id)
.do_update()
.set(self)
.execute(&**conn)
.map_res("Error saving attachment")
}
#[cfg(not(feature = "postgresql"))]
pub fn save(&self, conn: &DbConn) -> EmptyResult {
diesel::replace_into(attachments::table)
.values(self)

17
src/db/models/cipher.rs

@ -5,7 +5,7 @@ use super::{
Attachment, CollectionCipher, FolderCipher, Organization, User, UserOrgStatus, UserOrgType, UserOrganization,
};
#[derive(Debug, Identifiable, Queryable, Insertable, Associations)]
#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)]
#[table_name = "ciphers"]
#[belongs_to(User, foreign_key = "user_uuid")]
#[belongs_to(Organization, foreign_key = "organization_uuid")]
@ -148,6 +148,21 @@ impl Cipher {
user_uuids
}
#[cfg(feature = "postgresql")]
pub fn save(&mut self, conn: &DbConn) -> EmptyResult {
self.update_users_revision(conn);
self.updated_at = Utc::now().naive_utc();
diesel::insert_into(ciphers::table)
.values(&*self)
.on_conflict(ciphers::uuid)
.do_update()
.set(&*self)
.execute(&**conn)
.map_res("Error saving cipher")
}
#[cfg(not(feature = "postgresql"))]
pub fn save(&mut self, conn: &DbConn) -> EmptyResult {
self.update_users_revision(conn);
self.updated_at = Utc::now().naive_utc();

49
src/db/models/collection.rs

@ -2,7 +2,7 @@ use serde_json::Value;
use super::{Organization, UserOrgStatus, UserOrgType, UserOrganization};
#[derive(Debug, Identifiable, Queryable, Insertable, Associations)]
#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)]
#[table_name = "collections"]
#[belongs_to(Organization, foreign_key = "org_uuid")]
#[primary_key(uuid)]
@ -43,6 +43,20 @@ use crate::error::MapResult;
/// Database methods
impl Collection {
#[cfg(feature = "postgresql")]
pub fn save(&self, conn: &DbConn) -> EmptyResult {
self.update_users_revision(conn);
diesel::insert_into(collections::table)
.values(self)
.on_conflict(collections::uuid)
.do_update()
.set(self)
.execute(&**conn)
.map_res("Error saving collection")
}
#[cfg(not(feature = "postgresql"))]
pub fn save(&self, conn: &DbConn) -> EmptyResult {
self.update_users_revision(conn);
@ -200,6 +214,24 @@ impl CollectionUser {
.expect("Error loading users_collections")
}
#[cfg(feature = "postgresql")]
pub fn save(user_uuid: &str, collection_uuid: &str, read_only: bool, conn: &DbConn) -> EmptyResult {
User::update_uuid_revision(&user_uuid, conn);
diesel::insert_into(users_collections::table)
.values((
users_collections::user_uuid.eq(user_uuid),
users_collections::collection_uuid.eq(collection_uuid),
users_collections::read_only.eq(read_only),
))
.on_conflict((users_collections::user_uuid, users_collections::collection_uuid))
.do_update()
.set(users_collections::read_only.eq(read_only))
.execute(&**conn)
.map_res("Error adding user to collection")
}
#[cfg(not(feature = "postgresql"))]
pub fn save(user_uuid: &str, collection_uuid: &str, read_only: bool, conn: &DbConn) -> EmptyResult {
User::update_uuid_revision(&user_uuid, conn);
@ -277,6 +309,21 @@ pub struct CollectionCipher {
/// Database methods
impl CollectionCipher {
#[cfg(feature = "postgresql")]
pub fn save(cipher_uuid: &str, collection_uuid: &str, conn: &DbConn) -> EmptyResult {
Self::update_users_revision(&collection_uuid, conn);
diesel::insert_into(ciphers_collections::table)
.values((
ciphers_collections::cipher_uuid.eq(cipher_uuid),
ciphers_collections::collection_uuid.eq(collection_uuid),
))
.on_conflict((ciphers_collections::cipher_uuid, ciphers_collections::collection_uuid))
.do_nothing()
.execute(&**conn)
.map_res("Error adding cipher to collection")
}
#[cfg(not(feature = "postgresql"))]
pub fn save(cipher_uuid: &str, collection_uuid: &str, conn: &DbConn) -> EmptyResult {
Self::update_users_revision(&collection_uuid, conn);
diesel::replace_into(ciphers_collections::table)

14
src/db/models/device.rs

@ -2,7 +2,7 @@ use chrono::{NaiveDateTime, Utc};
use super::User;
#[derive(Debug, Identifiable, Queryable, Insertable, Associations)]
#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)]
#[table_name = "devices"]
#[belongs_to(User, foreign_key = "user_uuid")]
#[primary_key(uuid)]
@ -114,6 +114,18 @@ use crate::error::MapResult;
/// Database methods
impl Device {
#[cfg(feature = "postgresql")]
pub fn save(&mut self, conn: &DbConn) -> EmptyResult {
self.updated_at = Utc::now().naive_utc();
crate::util::retry(
|| diesel::insert_into(devices::table).values(&*self).on_conflict(devices::uuid).do_update().set(&*self).execute(&**conn),
10,
)
.map_res("Error saving device")
}
#[cfg(not(feature = "postgresql"))]
pub fn save(&mut self, conn: &DbConn) -> EmptyResult {
self.updated_at = Utc::now().naive_utc();

28
src/db/models/folder.rs

@ -3,7 +3,7 @@ use serde_json::Value;
use super::{Cipher, User};
#[derive(Debug, Identifiable, Queryable, Insertable, Associations)]
#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)]
#[table_name = "folders"]
#[belongs_to(User, foreign_key = "user_uuid")]
#[primary_key(uuid)]
@ -71,6 +71,21 @@ use crate::error::MapResult;
/// Database methods
impl Folder {
#[cfg(feature = "postgresql")]
pub fn save(&mut self, conn: &DbConn) -> EmptyResult {
User::update_uuid_revision(&self.user_uuid, conn);
self.updated_at = Utc::now().naive_utc();
diesel::insert_into(folders::table)
.values(&*self)
.on_conflict(folders::uuid)
.do_update()
.set(&*self)
.execute(&**conn)
.map_res("Error saving folder")
}
#[cfg(not(feature = "postgresql"))]
pub fn save(&mut self, conn: &DbConn) -> EmptyResult {
User::update_uuid_revision(&self.user_uuid, conn);
self.updated_at = Utc::now().naive_utc();
@ -113,6 +128,17 @@ impl Folder {
}
impl FolderCipher {
#[cfg(feature = "postgresql")]
pub fn save(&self, conn: &DbConn) -> EmptyResult {
diesel::insert_into(folders_ciphers::table)
.values(&*self)
.on_conflict((folders_ciphers::cipher_uuid, folders_ciphers::folder_uuid))
.do_nothing()
.execute(&**conn)
.map_res("Error adding cipher to folder")
}
#[cfg(not(feature = "postgresql"))]
pub fn save(&self, conn: &DbConn) -> EmptyResult {
diesel::replace_into(folders_ciphers::table)
.values(&*self)

36
src/db/models/organization.rs

@ -3,7 +3,7 @@ use std::cmp::Ordering;
use super::{CollectionUser, User};
#[derive(Debug, Identifiable, Queryable, Insertable)]
#[derive(Debug, Identifiable, Queryable, Insertable, AsChangeset)]
#[table_name = "organizations"]
#[primary_key(uuid)]
pub struct Organization {
@ -12,7 +12,7 @@ pub struct Organization {
pub billing_email: String,
}
#[derive(Debug, Identifiable, Queryable, Insertable)]
#[derive(Debug, Identifiable, Queryable, Insertable, AsChangeset)]
#[table_name = "users_organizations"]
#[primary_key(uuid)]
pub struct UserOrganization {
@ -213,6 +213,24 @@ use crate::error::MapResult;
/// Database methods
impl Organization {
#[cfg(feature = "postgresql")]
pub fn save(&self, conn: &DbConn) -> EmptyResult {
UserOrganization::find_by_org(&self.uuid, conn)
.iter()
.for_each(|user_org| {
User::update_uuid_revision(&user_org.user_uuid, conn);
});
diesel::insert_into(organizations::table)
.values(self)
.on_conflict(organizations::uuid)
.do_update()
.set(self)
.execute(&**conn)
.map_res("Error saving organization")
}
#[cfg(not(feature = "postgresql"))]
pub fn save(&self, conn: &DbConn) -> EmptyResult {
UserOrganization::find_by_org(&self.uuid, conn)
.iter()
@ -323,6 +341,20 @@ impl UserOrganization {
})
}
#[cfg(feature = "postgresql")]
pub fn save(&self, conn: &DbConn) -> EmptyResult {
User::update_uuid_revision(&self.user_uuid, conn);
diesel::insert_into(users_organizations::table)
.values(self)
.on_conflict(users_organizations::uuid)
.do_update()
.set(self)
.execute(&**conn)
.map_res("Error adding user to organization")
}
#[cfg(not(feature = "postgresql"))]
pub fn save(&self, conn: &DbConn) -> EmptyResult {
User::update_uuid_revision(&self.user_uuid, conn);

16
src/db/models/two_factor.rs

@ -9,7 +9,7 @@ use crate::error::MapResult;
use super::User;
#[derive(Debug, Identifiable, Queryable, Insertable, Associations)]
#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)]
#[table_name = "twofactor"]
#[belongs_to(User, foreign_key = "user_uuid")]
#[primary_key(uuid)]
@ -19,6 +19,7 @@ pub struct TwoFactor {
pub atype: i32,
pub enabled: bool,
pub data: String,
pub last_used: i32,
}
#[allow(dead_code)]
@ -47,6 +48,7 @@ impl TwoFactor {
atype: atype as i32,
enabled: true,
data,
last_used: 0,
}
}
@ -69,6 +71,18 @@ impl TwoFactor {
/// Database methods
impl TwoFactor {
#[cfg(feature = "postgresql")]
pub fn save(&self, conn: &DbConn) -> EmptyResult {
diesel::insert_into(twofactor::table)
.values(self)
.on_conflict(twofactor::uuid)
.do_update()
.set(self)
.execute(&**conn)
.map_res("Error saving twofactor")
}
#[cfg(not(feature = "postgresql"))]
pub fn save(&self, conn: &DbConn) -> EmptyResult {
diesel::replace_into(twofactor::table)
.values(self)

35
src/db/models/user.rs

@ -4,7 +4,7 @@ use serde_json::Value;
use crate::crypto;
use crate::CONFIG;
#[derive(Debug, Identifiable, Queryable, Insertable)]
#[derive(Debug, Identifiable, Queryable, Insertable, AsChangeset)]
#[table_name = "users"]
#[primary_key(uuid)]
pub struct User {
@ -148,6 +148,24 @@ impl User {
})
}
#[cfg(feature = "postgresql")]
pub fn save(&mut self, conn: &DbConn) -> EmptyResult {
if self.email.trim().is_empty() {
err!("User email can't be empty")
}
self.updated_at = Utc::now().naive_utc();
diesel::insert_into(users::table) // Insert or update
.values(&*self)
.on_conflict(users::uuid)
.do_update()
.set(&*self)
.execute(&**conn)
.map_res("Error saving user")
}
#[cfg(not(feature = "postgresql"))]
pub fn save(&mut self, conn: &DbConn) -> EmptyResult {
if self.email.trim().is_empty() {
err!("User email can't be empty")
@ -250,6 +268,21 @@ impl Invitation {
Self { email }
}
#[cfg(feature = "postgresql")]
pub fn save(&self, conn: &DbConn) -> EmptyResult {
if self.email.trim().is_empty() {
err!("Invitation email can't be empty")
}
diesel::insert_into(invitations::table)
.values(self)
.on_conflict(invitations::email)
.do_nothing()
.execute(&**conn)
.map_res("Error saving invitation")
}
#[cfg(not(feature = "postgresql"))]
pub fn save(&self, conn: &DbConn) -> EmptyResult {
if self.email.trim().is_empty() {
err!("Invitation email can't be empty")

1
src/db/schemas/mysql/schema.rs

@ -92,6 +92,7 @@ table! {
atype -> Integer,
enabled -> Bool,
data -> Text,
last_used -> Integer,
}
}

173
src/db/schemas/postgresql/schema.rs

@ -0,0 +1,173 @@
table! {
attachments (id) {
id -> Text,
cipher_uuid -> Text,
file_name -> Text,
file_size -> Integer,
akey -> Nullable<Text>,
}
}
table! {
ciphers (uuid) {
uuid -> Text,
created_at -> Timestamp,
updated_at -> Timestamp,
user_uuid -> Nullable<Text>,
organization_uuid -> Nullable<Text>,
atype -> Integer,
name -> Text,
notes -> Nullable<Text>,
fields -> Nullable<Text>,
data -> Text,
favorite -> Bool,
password_history -> Nullable<Text>,
}
}
table! {
ciphers_collections (cipher_uuid, collection_uuid) {
cipher_uuid -> Text,
collection_uuid -> Text,
}
}
table! {
collections (uuid) {
uuid -> Text,
org_uuid -> Text,
name -> Text,
}
}
table! {
devices (uuid) {
uuid -> Text,
created_at -> Timestamp,
updated_at -> Timestamp,
user_uuid -> Text,
name -> Text,
atype -> Integer,
push_token -> Nullable<Text>,
refresh_token -> Text,
twofactor_remember -> Nullable<Text>,
}
}
table! {
folders (uuid) {
uuid -> Text,
created_at -> Timestamp,
updated_at -> Timestamp,
user_uuid -> Text,
name -> Text,
}
}
table! {
folders_ciphers (cipher_uuid, folder_uuid) {
cipher_uuid -> Text,
folder_uuid -> Text,
}
}
table! {
invitations (email) {
email -> Text,
}
}
table! {
organizations (uuid) {
uuid -> Text,
name -> Text,
billing_email -> Text,
}
}
table! {
twofactor (uuid) {
uuid -> Text,
user_uuid -> Text,
atype -> Integer,
enabled -> Bool,
data -> Text,
last_used -> Integer,
}
}
table! {
users (uuid) {
uuid -> Text,
created_at -> Timestamp,
updated_at -> Timestamp,
email -> Text,
name -> Text,
password_hash -> Binary,
salt -> Binary,
password_iterations -> Integer,
password_hint -> Nullable<Text>,
akey -> Text,
private_key -> Nullable<Text>,
public_key -> Nullable<Text>,
totp_secret -> Nullable<Text>,
totp_recover -> Nullable<Text>,
security_stamp -> Text,
equivalent_domains -> Text,
excluded_globals -> Text,
client_kdf_type -> Integer,
client_kdf_iter -> Integer,
}
}
table! {
users_collections (user_uuid, collection_uuid) {
user_uuid -> Text,
collection_uuid -> Text,
read_only -> Bool,
}
}
table! {
users_organizations (uuid) {
uuid -> Text,
user_uuid -> Text,
org_uuid -> Text,
access_all -> Bool,
akey -> Text,
status -> Integer,
atype -> Integer,
}
}
joinable!(attachments -> ciphers (cipher_uuid));
joinable!(ciphers -> organizations (organization_uuid));
joinable!(ciphers -> users (user_uuid));
joinable!(ciphers_collections -> ciphers (cipher_uuid));
joinable!(ciphers_collections -> collections (collection_uuid));
joinable!(collections -> organizations (org_uuid));
joinable!(devices -> users (user_uuid));
joinable!(folders -> users (user_uuid));
joinable!(folders_ciphers -> ciphers (cipher_uuid));
joinable!(folders_ciphers -> folders (folder_uuid));
joinable!(twofactor -> users (user_uuid));
joinable!(users_collections -> collections (collection_uuid));
joinable!(users_collections -> users (user_uuid));
joinable!(users_organizations -> organizations (org_uuid));
joinable!(users_organizations -> users (user_uuid));
allow_tables_to_appear_in_same_query!(
attachments,
ciphers,
ciphers_collections,
collections,
devices,
folders,
folders_ciphers,
invitations,
organizations,
twofactor,
users,
users_collections,
users_organizations,
);

1
src/db/schemas/sqlite/schema.rs

@ -92,6 +92,7 @@ table! {
atype -> Integer,
enabled -> Bool,
data -> Text,
last_used -> Integer,
}
}

6
src/main.rs

@ -1,6 +1,8 @@
#![feature(proc_macro_hygiene, decl_macro, vec_remove_item, try_trait)]
#![feature(proc_macro_hygiene, decl_macro, vec_remove_item, try_trait, ip)]
#![recursion_limit = "256"]
#[cfg(feature = "openssl")]
extern crate openssl;
#[macro_use]
extern crate rocket;
#[macro_use]
@ -215,6 +217,8 @@ mod migrations {
embed_migrations!("migrations/sqlite");
#[cfg(feature = "mysql")]
embed_migrations!("migrations/mysql");
#[cfg(feature = "postgresql")]
embed_migrations!("migrations/postgresql");
pub fn run_migrations() {
// Make sure the database is up to date (create if it doesn't exist, or run the migrations)

BIN
src/static/images/hibp.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

25
src/util.rs

@ -42,12 +42,19 @@ impl CORS {
_ => "".to_string(),
}
}
fn valid_url(url: String) -> String {
match url.as_ref() {
"file://" => "*".to_string(),
_ => url,
}
}
}
impl Fairing for CORS {
fn info(&self) -> Info {
Info {
name: "Add CORS headers to requests",
name: "CORS",
kind: Kind::Response
}
}
@ -56,21 +63,17 @@ impl Fairing for CORS {
let req_headers = request.headers();
// We need to explicitly get the Origin header for Access-Control-Allow-Origin
let req_allow_origin = CORS::get_header(&req_headers, "Origin");
let req_allow_origin = CORS::valid_url(CORS::get_header(&req_headers, "Origin"));
let req_allow_headers = CORS::get_header(&req_headers, "Access-Control-Request-Headers");
response.set_header(Header::new("Access-Control-Allow-Origin", req_allow_origin));
let req_allow_methods =CORS::get_header(&req_headers,"Access-Control-Request-Methods");
if request.method() == Method::Options {
let req_allow_headers = CORS::get_header(&req_headers, "Access-Control-Request-Headers");
let req_allow_method = CORS::get_header(&req_headers,"Access-Control-Request-Method");
if request.method() == Method::Options || response.content_type() == Some(ContentType::JSON) {
// Requests with credentials need explicit values since they do not allow wildcards.
response.set_header(Header::new("Access-Control-Allow-Origin", req_allow_origin));
response.set_header(Header::new("Access-Control-Allow-Methods", req_allow_methods));
response.set_header(Header::new("Access-Control-Allow-Methods", req_allow_method));
response.set_header(Header::new("Access-Control-Allow-Headers", req_allow_headers));
response.set_header(Header::new("Access-Control-Allow-Credentials", "true"));
}
if request.method() == Method::Options {
response.set_status(Status::Ok);
response.set_header(ContentType::Plain);
response.set_sized_body(Cursor::new(""));

Loading…
Cancel
Save