Compare commits

...

90 Commits
1.4.0 ... 1.5.0

Author SHA1 Message Date
Daniel García
371017b547 Merge pull request #294 from i-ghost/feature/add-systemd-notes
Add systemd setup instructions to README.md
2018-12-16 23:55:56 +01:00
i-ghost
bc20592712 Revise per dani-garcia/bitwarden_rs#294 2018-12-16 21:09:59 +00:00
Daniel García
37a6da3443 Merge pull request #296 from jeslinmx/docsupdate
updated fail2ban setup to match changes in logformat for 1.5.0
2018-12-16 20:06:46 +01:00
Daniel García
53bd169462 Merge pull request #297 from mprasil/vault_2.7.1
Update vault to 2.7.1
2018-12-16 20:06:15 +01:00
Daniel García
5a9aab1a32 Implement fromform, and ignore case and underscores, fixes #298 2018-12-16 20:00:16 +01:00
Miroslav Prasil
23eadf2c9a Update vault t o2.7.1 2018-12-16 16:00:51 +00:00
jes
d5dfda8905 updated fail2ban setup to match changes in logformat for 1.5.0 2018-12-16 22:30:25 +08:00
i-ghost
ce15c7ffba Add systemd setup instructions to README.md 2018-12-15 22:25:17 +00:00
Daniel García
a335bcd682 Merge pull request #291 from mqus/patch-1
Add memory usage note to the vault build documentation
2018-12-14 22:56:44 +01:00
mqus
76c510c5b6 move memory note 2018-12-14 20:32:11 +01:00
mqus
4635d62e2c Add memory usage note to vault build process
As there were multiple people having problems with the build I thought that maybe adding a note would help.
2018-12-14 15:04:46 +01:00
Daniel García
65e36e2931 Merge pull request #289 from mprasil/alpine-fix
Fix failing alpine build
2018-12-14 12:33:17 +01:00
Miroslav Prasil
063dfcf487 FIx failing alpine build 2018-12-13 23:16:36 +00:00
Daniel García
d3e4fb88ee Fix wrong folder name 2018-12-13 18:30:01 +01:00
Daniel García
7ebba736cb Updated dockerfiles to use compiled vaults 2018-12-13 18:19:26 +01:00
Daniel García
a1272c7190 Merge branch 'rocket-0.4' 2018-12-13 17:49:55 +01:00
Daniel García
0a6b4e9961 Update BUILD.md 2018-12-13 17:46:08 +01:00
Daniel García
e66edc1ce9 Merge pull request #285 from itr6/master
Add line for people using docker commands instead of docker-compose
2018-12-13 01:07:17 +01:00
Daniel García
4e827e4f8a Implement better retry and use it while saving device 2018-12-12 22:15:54 +01:00
itr6
295985de7c Update README.md 2018-12-12 14:38:57 -06:00
itr6
a48611aa6d Update README.md 2018-12-12 14:38:23 -06:00
Daniel García
f713e2e092 Merge pull request #284 from Algebro7/log_successful_logins
Close #264. Username and IP address is logged on successful authentication
2018-12-11 22:24:04 +01:00
algebro
e26e2319da Close #264. Usernames and IP addresses are logged on successful authentication 2018-12-11 15:20:06 -05:00
Daniel García
f2ab25085d Updated dependencies, and dockerfiles to use NodeJS 10 LTS 2018-12-11 16:42:52 +01:00
Daniel García
7adc045b80 Updated IP logging to use client_ip, to match old remote behavior.
Improved error logging, now it won't show a generic error message in some situations.
Removed delete device, which is not needed as it will be overwritten later.
Logged more info when an error occurs saving a device.
Added orgmanager to JWT claims.
2018-12-09 17:58:38 +01:00
Daniel García
19754c967f More changes to the push token, and filtered multipart logs 2018-12-07 18:25:18 +01:00
Daniel García
738ad2127b Fixed some clippy linting issues 2018-12-07 15:01:29 +01:00
Daniel García
cb930a0858 Remove some required values during login, now uses default values 2018-12-07 14:32:40 +01:00
Daniel García
94810c106a Migrate to rust 2018 edition 2018-12-07 02:05:45 +01:00
Daniel García
2fde4e6933 Implemented proper logging, with support for file logging, timestamp and syslog (this last one is untested) 2018-12-06 20:35:25 +01:00
Daniel García
259a2f2982 Updated rocket to final release 2018-12-06 20:15:22 +01:00
Daniel García
dc1bb6de20 Update device push token methods to more closely follow the official server response 2018-12-06 16:28:36 +01:00
Daniel García
e34f75c267 Make sure the dockerfile builds the project, instead of assuming the cached dummy is valid 2018-12-04 13:55:37 +01:00
Daniel García
2129946d14 Updated deps and web vault to 2.6.1 2018-12-03 20:28:13 +01:00
Daniel García
bdcdb08fc1 Merge branch 'master' into rocket-0.4 2018-12-01 14:58:39 +01:00
Daniel García
8b5d97790f Updated rocket to rc2 and rest of dependencies 2018-12-01 14:29:19 +01:00
Daniel García
cac7fb145b Merge pull request #274 from itr6/master
Fail2Ban Setup & Walkthrough
2018-11-29 23:20:30 +01:00
itr6
6990525e8a Update README.md 2018-11-29 10:52:47 -06:00
itr6
55579261b7 Update README.md 2018-11-29 10:49:20 -06:00
itr6
70c20f7f52 Update README.md 2018-11-29 10:48:41 -06:00
itr6
b56a905322 Update README.md 2018-11-29 10:42:53 -06:00
Daniel García
e177160ee9 Update web vault to 2.6.0 2018-11-27 17:29:09 +01:00
Daniel García
6364c05789 Fix attachments during key rotation, add individual attachment key 2018-11-27 17:24:12 +01:00
Daniel García
f71f10eac6 Implemented key rotation with the latest vault 2018-11-24 23:00:41 +01:00
Daniel García
0f6ab01f77 Merge pull request #268 from mprasil/master
fix bad path in Alpine Dockerfile
2018-11-22 03:06:26 +01:00
Miroslav Prasil
0935cb90a4 fix bad path in Alpine Dockerfile 2018-11-21 23:35:42 +00:00
Daniel García
e86cac984a Merge pull request #263 from mprasil/master
Update dockerfile for alpine, armv7 and aarch64
2018-11-21 15:57:11 +01:00
Daniel García
f1acc1e05a Merge branch 'master' into rocket-0.4 2018-11-21 15:35:34 +01:00
Daniel García
3d36ac4601 Remove unwrap in connection_lost 2018-11-21 15:07:18 +01:00
Miroslav Prasil
bf1c96695b Update dockerfile for alpine, armv7 and aarch64 2018-11-20 14:55:19 +00:00
Daniel García
9ecb29883c Merge branch 'master' into rocket-0.4
# Conflicts:
#	Cargo.lock
2018-11-19 20:59:41 +01:00
Daniel García
8b3e87cfe0 Update lockfile to fix yubico error 2018-11-19 20:58:59 +01:00
Daniel García
e5af230315 Merge branch 'master' into rocket-0.4
# Conflicts:
#	Cargo.toml
2018-11-19 20:45:42 +01:00
Daniel García
a1da82c868 Add online feature to yubico 2018-11-19 20:45:11 +01:00
Daniel García
981a324027 Merge branch 'master' into rocket-0.4
# Conflicts:
#	src/api/core/mod.rs
2018-11-19 20:30:19 +01:00
Daniel García
00abd4c853 Add create endpoint, fixes #253, fixes #261 2018-11-19 20:27:49 +01:00
Daniel García
a4550e51ea Update dependencies and add /ciphers/create 2018-11-19 20:21:02 +01:00
Daniel García
5edbd0e952 Merge branch 'master' into rocket-0.4
# Conflicts:
#	Cargo.lock
#	Cargo.toml
#	src/api/core/mod.rs
2018-11-19 19:52:43 +01:00
Daniel García
1d4944b88e Merge pull request #258 from Bruceforce/master
Update backup section of README.md
2018-11-19 19:36:10 +01:00
10
660c8f8d7e Use dockerhub instead of gitlab registry for README.md 2018-11-18 14:06:08 +01:00
10
478fc0c9dd Merge remote-tracking branch 'origin/master' 2018-11-18 03:23:16 +01:00
10
a2de4ce40e Improved the backup section of sqlite3 database 2018-11-18 03:22:30 +01:00
Daniel García
17052b665f Merge pull request #257 from Step7750/fix-nfc-mobile
Fixes NFC Response for Mobile Yubikey OTP Login
2018-11-17 15:28:41 +01:00
Stepan Fedorko-Bartos
f344dbaad4 Fixes NFC Response for Mobile Login 2018-11-17 01:25:07 -07:00
Daniel García
c5c9e3fd65 Merge pull request #254 from Step7750/master
Adds Yubikey OTP Support
2018-11-16 20:39:03 +01:00
Stepan Fedorko-Bartos
aba9c28226 Disable Yubikey 2FA if 0 Keys Provided 2018-11-16 12:07:00 -07:00
Stepan Fedorko-Bartos
82e2b8a8c0 Code style changes 2018-11-16 11:52:01 -07:00
Stepan Fedorko-Bartos
5a8d5e426d Switches to Downstream yubico with Optional libusb 2018-11-16 11:28:20 -07:00
Daniel García
f9ecb7201b Merge pull request #256 from mprasil/fix_accepted_sync
Prevent accepted user from seeing ciphers until confirmed (fixes #196)
2018-11-16 16:17:21 +01:00
Miroslav Prasil
044cf19913 Prevent accepted user from seeing ciphers until confirmed (fixes #196) 2018-11-16 14:21:26 +00:00
Stepan Fedorko-Bartos
2c233cda8b Adds Yubikey OTP Support to Readme 2018-11-15 18:59:16 -07:00
Stepan Fedorko-Bartos
6d735806c0 Ensures Yubico Creds are set when opening Yubikey Modal 2018-11-15 18:58:44 -07:00
Stepan Fedorko-Bartos
2433d39df5 Allows Custom Yubico OTP Server 2018-11-15 18:54:53 -07:00
Stepan Fedorko-Bartos
9e0e4b13c5 Adds Yubikey OTP Support 2018-11-15 18:43:09 -07:00
Stepan Fedorko-Bartos
e66436625c Adds Yubico Client ID and Secret Key Env Vars 2018-11-15 18:40:27 -07:00
Stepan Fedorko-Bartos
24a4478b5c Adds yubico-rs library dep 2018-11-15 18:34:17 -07:00
Daniel García
f84cbeaaf8 Merge branch 'master' into rocket-0.4
# Conflicts:
#	src/db/models/organization.rs
2018-11-14 16:14:49 +01:00
Daniel García
3cb911a52f Merge pull request #243 from mprasil/vault_2.5.0
Bump vault version to 2.5.0 [wip]
2018-11-14 16:12:18 +01:00
Miroslav Prasil
dd684753d0 Fix gt() 2018-11-13 21:38:56 +00:00
Miroslav Prasil
f3e6cc6ffd Set PartialOrd to consider invalid i32 UserOrgType lower than anything 2018-11-13 16:34:21 +00:00
Miroslav Prasil
b94f4db52a Fix #242 2018-11-13 15:34:37 +00:00
Miroslav Prasil
66a4c5d48b Implement comparison between i32 and UserOrgType 2018-11-12 17:13:25 +00:00
Miroslav Prasil
b4e222d598 Bump vault version to 2.5.0 2018-11-12 10:41:47 +00:00
Daniel García
4638786507 Merge branch 'master' into rocket-0.4
# Conflicts:
#	src/api/core/mod.rs
2018-11-09 16:06:24 +01:00
Daniel García
6eb1c3d638 Fixed change in organizations header, now Request::get_param() are indexed by segment, not dynamic parameter. 2018-11-06 16:53:34 +01:00
Miroslav Prasil
f408efc927 Also remove libs from other platforms Dockerfiles 2018-11-03 18:56:41 +01:00
Miroslav Prasil
9b20247fc2 Remove libs from Dockerfile 2018-11-03 18:54:36 +01:00
Daniel García
e844f41abc Make the docker images use port 80 (instead of 8000, which is the default in rocket 0.4) 2018-11-01 19:25:10 +01:00
Daniel García
bc532f54d5 Improve login query parsing 2018-11-01 19:25:10 +01:00
Daniel García
c673370103 Updated bw_rs to Rocket version 0.4-rc1 2018-11-01 19:25:09 +01:00
46 changed files with 2366 additions and 2316 deletions

19
.env
View File

@@ -18,6 +18,17 @@
# WEBSOCKET_ADDRESS=0.0.0.0 # WEBSOCKET_ADDRESS=0.0.0.0
# WEBSOCKET_PORT=3012 # WEBSOCKET_PORT=3012
## Enable extended logging
## This shows timestamps and allows logging to file and to syslog
### To enable logging to file, use the LOG_FILE env variable
### To enable syslog, you need to compile with `cargo build --features=enable_syslog'
# EXTENDED_LOGGING=true
## Logging to file
## This requires extended logging
## It's recommended to also set 'ROCKET_CLI_COLORS=off'
# LOG_FILE=/path/to/log
## Controls if new users can register ## Controls if new users can register
# SIGNUPS_ALLOWED=true # SIGNUPS_ALLOWED=true
@@ -40,6 +51,14 @@
## For U2F to work, the server must use HTTPS, you can use Let's Encrypt for free certs ## For U2F to work, the server must use HTTPS, you can use Let's Encrypt for free certs
# DOMAIN=https://bw.domain.tld:8443 # DOMAIN=https://bw.domain.tld:8443
## Yubico (Yubikey) Settings
## Set your Client ID and Secret Key for Yubikey OTP
## You can generate it here: https://upgrade.yubico.com/getapikey/
## You can optionally specify a custom OTP server
# YUBICO_CLIENT_ID=11111
# YUBICO_SECRET_KEY=AAAAAAAAAAAAAAAAAAAAAAAA
# YUBICO_SERVER=http://yourdomain.com/wsapi/2.0/verify
## Rocket specific settings, check Rocket documentation to learn more ## Rocket specific settings, check Rocket documentation to learn more
# ROCKET_ENV=staging # ROCKET_ENV=staging
# ROCKET_ADDRESS=0.0.0.0 # Enable this to test mobile app # ROCKET_ADDRESS=0.0.0.0 # Enable this to test mobile app

View File

@@ -5,3 +5,5 @@ dist: trusty # so we get a VM with higher specs
cache: cargo cache: cargo
rust: rust:
- nightly - nightly
script:
- cargo build --verbose --all-features

View File

@@ -3,13 +3,13 @@
## Dependencies ## Dependencies
- `Rust nightly` (strongly recommended to use [rustup](https://rustup.rs/)) - `Rust nightly` (strongly recommended to use [rustup](https://rustup.rs/))
- `OpenSSL` (should be available in path, install through your system's package manager or use the [prebuilt binaries](https://wiki.openssl.org/index.php/Binaries)) - `OpenSSL` (should be available in path, install through your system's package manager or use the [prebuilt binaries](https://wiki.openssl.org/index.php/Binaries))
- `NodeJS` (required to build the web-vault, (install through your system's package manager or use the [prebuilt binaries](https://nodejs.org/en/download/)) - `NodeJS` (only when compiling the web-vault, install through your system's package manager or use the [prebuilt binaries](https://nodejs.org/en/download/))
## Run/Compile ## Run/Compile
```sh ```sh
# Compile and run # Compile and run
cargo run cargo run --release
# or just compile (binary located in target/release/bitwarden_rs) # or just compile (binary located in target/release/bitwarden_rs)
cargo build --release cargo build --release
``` ```
@@ -17,7 +17,13 @@ cargo build --release
When run, the server is accessible in [http://localhost:80](http://localhost:80). When run, the server is accessible in [http://localhost:80](http://localhost:80).
### Install the web-vault ### Install the web-vault
Clone the git repository at [bitwarden/web](https://github.com/bitwarden/web) and checkout the latest release tag (e.g. v2.1.1): A compiled version of the web vault can be downloaded from [dani-garcia/bw_web_builds](https://github.com/dani-garcia/bw_web_builds/releases).
If you prefer to compile it manually, follow these steps:
*Note: building the Vault needs ~1.5GB of RAM. On systems like a RaspberryPI with 1GB or less, please [enable swapping](https://www.tecmint.com/create-a-linux-swap-file/) or build it on a more powerful machine and copy the directory from there. This much memory is only needed for building it, running bitwarden_rs with vault needs only about 10MB of RAM.*
- Clone the git repository at [bitwarden/web](https://github.com/bitwarden/web) and checkout the latest release tag (e.g. v2.1.1):
```sh ```sh
# clone the repository # clone the repository
git clone https://github.com/bitwarden/web.git web-vault git clone https://github.com/bitwarden/web.git web-vault
@@ -26,13 +32,14 @@ cd web-vault
git checkout "$(git tag | tail -n1)" git checkout "$(git tag | tail -n1)"
``` ```
Apply the patch file from `docker/set-vault-baseurl.patch`: - Apply the patch file from `docker/set-vault-baseurl.patch`:
```sh ```sh
# In the Vault repository directory # In the Vault repository directory
git apply /path/to/bitwarden_rs/docker/set-vault-baseurl.patch git apply /path/to/bitwarden_rs/docker/set-vault-baseurl.patch
``` ```
Then, build the Vault: - Then, build the Vault:
```sh ```sh
npm run sub:init npm run sub:init
npm install npm install

1398
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,21 +2,24 @@
name = "bitwarden_rs" name = "bitwarden_rs"
version = "1.0.0" version = "1.0.0"
authors = ["Daniel García <dani-garcia@users.noreply.github.com>"] authors = ["Daniel García <dani-garcia@users.noreply.github.com>"]
edition = "2018"
[features]
enable_syslog = ["syslog", "fern/syslog-4"]
[dependencies] [dependencies]
# Web framework for nightly with a focus on ease-of-use, expressibility, and speed. # Web framework for nightly with a focus on ease-of-use, expressibility, and speed.
rocket = { version = "0.3.17", features = ["tls"] } rocket = { version = "0.4.0", features = ["tls"], default-features = false }
rocket_codegen = "0.3.17" rocket_contrib = "0.4.0"
rocket_contrib = "0.3.17"
# HTTP client # HTTP client
reqwest = "0.9.2" reqwest = "0.9.5"
# multipart/form-data support # multipart/form-data support
multipart = "0.15.3" multipart = "0.15.4"
# WebSockets library # WebSockets library
ws = "0.7.8" ws = "0.7.9"
# MessagePack library # MessagePack library
rmpv = "0.4.0" rmpv = "0.4.0"
@@ -25,9 +28,14 @@ rmpv = "0.4.0"
chashmap = "2.2.0" chashmap = "2.2.0"
# A generic serialization/deserialization framework # A generic serialization/deserialization framework
serde = "1.0.79" serde = "1.0.82"
serde_derive = "1.0.79" serde_derive = "1.0.82"
serde_json = "1.0.31" serde_json = "1.0.33"
# Logging
log = "0.4.6"
fern = "0.5.7"
syslog = { version = "4.0.1", optional = true }
# A safe, extensible ORM and Query builder # A safe, extensible ORM and Query builder
diesel = { version = "1.3.3", features = ["sqlite", "chrono", "r2d2"] } diesel = { version = "1.3.3", features = ["sqlite", "chrono", "r2d2"] }
@@ -37,7 +45,7 @@ diesel_migrations = { version = "1.3.0", features = ["sqlite"] }
libsqlite3-sys = { version = "0.9.3", features = ["bundled"] } libsqlite3-sys = { version = "0.9.3", features = ["bundled"] }
# Crypto library # Crypto library
ring = { version = "= 0.11.0", features = ["rsa_signing"] } ring = { version = "0.13.5", features = ["rsa_signing"] }
# UUID generation # UUID generation
uuid = { version = "0.7.1", features = ["v4"] } uuid = { version = "0.7.1", features = ["v4"] }
@@ -52,16 +60,19 @@ oath = "0.10.2"
data-encoding = "2.1.1" data-encoding = "2.1.1"
# JWT library # JWT library
jsonwebtoken = "= 4.0.1" jsonwebtoken = "5.0.1"
# U2F library # U2F library
u2f = "0.1.2" u2f = "0.1.2"
# Yubico Library
yubico = { version = "=0.4.0", features = ["online"], default-features = false }
# A `dotenv` implementation for Rust # A `dotenv` implementation for Rust
dotenv = { version = "0.13.0", default-features = false } dotenv = { version = "0.13.0", default-features = false }
# Lazy static macro # Lazy static macro
lazy_static = "1.1.0" lazy_static = { version = "1.2.0", features = ["nightly"] }
# Numerical libraries # Numerical libraries
num-traits = "0.2.6" num-traits = "0.2.6"
@@ -70,17 +81,21 @@ num-derive = "0.2.3"
# Email libraries # Email libraries
lettre = "0.9.0" lettre = "0.9.0"
lettre_email = "0.9.0" lettre_email = "0.9.0"
native-tls = "0.2.1" native-tls = "0.2.2"
# Number encoding library # Number encoding library
byteorder = "1.2.6" byteorder = "1.2.7"
[patch.crates-io] [patch.crates-io]
# Make jwt use ring 0.11, to match rocket # Add support for Timestamp type
jsonwebtoken = { path = "libs/jsonwebtoken" }
rmp = { git = 'https://github.com/dani-garcia/msgpack-rust' } 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 = { git = 'https://github.com/lettre/lettre', rev = 'c988b1760ad81' }
lettre_email = { git = 'https://github.com/lettre/lettre', rev = 'c988b1760ad81' } lettre_email = { git = 'https://github.com/lettre/lettre', rev = 'c988b1760ad81' }
# Version 0.1.2 from crates.io lacks a commit that fixes a certificate error # Version 0.1.2 from crates.io lacks a commit that fixes a certificate error
u2f = { git = 'https://github.com/wisespace-io/u2f-rs', rev = '193de35093a44' } u2f = { git = 'https://github.com/wisespace-io/u2f-rs', rev = '75b9fa5afb4c5' }
# Allows optional libusb support
yubico = { git = 'https://github.com/dani-garcia/yubico-rs' }

View File

@@ -2,27 +2,21 @@
# https://docs.docker.com/develop/develop-images/multistage-build/ # https://docs.docker.com/develop/develop-images/multistage-build/
# https://whitfin.io/speeding-up-rust-docker-builds/ # https://whitfin.io/speeding-up-rust-docker-builds/
####################### VAULT BUILD IMAGE ####################### ####################### VAULT BUILD IMAGE #######################
FROM node:8-alpine as vault FROM alpine as vault
ENV VAULT_VERSION "v2.4.0" ENV VAULT_VERSION "v2.7.1"
ENV URL "https://github.com/bitwarden/web.git" 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 \ RUN apk add --update-cache --upgrade \
curl \ curl \
git \
tar tar
RUN git clone -b $VAULT_VERSION --depth 1 $URL web-build RUN mkdir /web-vault
WORKDIR /web-build WORKDIR /web-vault
COPY /docker/set-vault-baseurl.patch /web-build/ RUN curl -L $URL | tar xz
RUN git apply set-vault-baseurl.patch RUN ls
RUN npm run sub:init && npm install
RUN npm run dist \
&& mv build /web-vault
########################## BUILD IMAGE ########################## ########################## BUILD IMAGE ##########################
# We need to use the Rust build image, because # We need to use the Rust build image, because
@@ -41,7 +35,6 @@ WORKDIR /app
# Copies over *only* your manifests and vendored dependencies # Copies over *only* your manifests and vendored dependencies
COPY ./Cargo.* ./ COPY ./Cargo.* ./
COPY ./libs ./libs
COPY ./rust-toolchain ./rust-toolchain COPY ./rust-toolchain ./rust-toolchain
# Builds your dependencies and removes the # Builds your dependencies and removes the
@@ -54,6 +47,9 @@ RUN find . -not -path "./target*" -delete
# To avoid copying unneeded files, use .dockerignore # To avoid copying unneeded files, use .dockerignore
COPY . . COPY . .
# Make sure that we actually build the project
RUN touch src/main.rs
# Builds again, this time it'll just be # Builds again, this time it'll just be
# your actual source files being built # your actual source files being built
RUN cargo build --release RUN cargo build --release
@@ -64,6 +60,7 @@ RUN cargo build --release
FROM debian:stretch-slim FROM debian:stretch-slim
ENV ROCKET_ENV "staging" ENV ROCKET_ENV "staging"
ENV ROCKET_PORT=80
ENV ROCKET_WORKERS=10 ENV ROCKET_WORKERS=10
# Install needed libraries # Install needed libraries

View File

@@ -2,27 +2,21 @@
# https://docs.docker.com/develop/develop-images/multistage-build/ # https://docs.docker.com/develop/develop-images/multistage-build/
# https://whitfin.io/speeding-up-rust-docker-builds/ # https://whitfin.io/speeding-up-rust-docker-builds/
####################### VAULT BUILD IMAGE ####################### ####################### VAULT BUILD IMAGE #######################
FROM node:8-alpine as vault FROM alpine as vault
ENV VAULT_VERSION "v2.4.0" ENV VAULT_VERSION "v2.7.1"
ENV URL "https://github.com/bitwarden/web.git" 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 \ RUN apk add --update-cache --upgrade \
curl \ curl \
git \
tar tar
RUN git clone -b $VAULT_VERSION --depth 1 $URL web-build RUN mkdir /web-vault
WORKDIR /web-build WORKDIR /web-vault
COPY /docker/set-vault-baseurl.patch /web-build/ RUN curl -L $URL | tar xz
RUN git apply set-vault-baseurl.patch RUN ls
RUN npm run sub:init && npm install
RUN npm run dist \
&& mv build /web-vault
########################## BUILD IMAGE ########################## ########################## BUILD IMAGE ##########################
# We need to use the Rust build image, because # We need to use the Rust build image, because
@@ -39,15 +33,8 @@ RUN apt-get update \
ENV CARGO_HOME "/root/.cargo" ENV CARGO_HOME "/root/.cargo"
ENV USER "root" ENV USER "root"
# Creates a dummy project used to grab dependencies
RUN USER=root cargo new --bin app
WORKDIR /app WORKDIR /app
# Copies over *only* your manifests and vendored dependencies
COPY ./Cargo.* ./
COPY ./libs ./libs
COPY ./rust-toolchain ./rust-toolchain
# Prepare openssl arm64 libs # Prepare openssl arm64 libs
RUN sed 's/^deb/deb-src/' /etc/apt/sources.list > \ RUN sed 's/^deb/deb-src/' /etc/apt/sources.list > \
/etc/apt/sources.list.d/deb-src.list \ /etc/apt/sources.list.d/deb-src.list \
@@ -62,19 +49,12 @@ ENV CROSS_COMPILE="1"
ENV OPENSSL_INCLUDE_DIR="/usr/include/aarch64-linux-gnu" ENV OPENSSL_INCLUDE_DIR="/usr/include/aarch64-linux-gnu"
ENV OPENSSL_LIB_DIR="/usr/lib/aarch64-linux-gnu" ENV OPENSSL_LIB_DIR="/usr/lib/aarch64-linux-gnu"
# Builds your dependencies and removes the
# dummy project, except the target folder
# This folder contains the compiled dependencies
RUN rustup target add aarch64-unknown-linux-gnu
RUN cargo build --release --target=aarch64-unknown-linux-gnu -v
RUN find . -not -path "./target*" -delete
# Copies the complete project # Copies the complete project
# To avoid copying unneeded files, use .dockerignore # To avoid copying unneeded files, use .dockerignore
COPY . . COPY . .
# Builds again, this time it'll just be # Build
# your actual source files being built RUN rustup target add aarch64-unknown-linux-gnu
RUN cargo build --release --target=aarch64-unknown-linux-gnu -v RUN cargo build --release --target=aarch64-unknown-linux-gnu -v
######################## RUNTIME IMAGE ######################## ######################## RUNTIME IMAGE ########################
@@ -83,6 +63,7 @@ RUN cargo build --release --target=aarch64-unknown-linux-gnu -v
FROM balenalib/aarch64-debian:stretch FROM balenalib/aarch64-debian:stretch
ENV ROCKET_ENV "staging" ENV ROCKET_ENV "staging"
ENV ROCKET_PORT=80
ENV ROCKET_WORKERS=10 ENV ROCKET_WORKERS=10
RUN [ "cross-build-start" ] RUN [ "cross-build-start" ]

View File

@@ -2,52 +2,37 @@
# https://docs.docker.com/develop/develop-images/multistage-build/ # https://docs.docker.com/develop/develop-images/multistage-build/
# https://whitfin.io/speeding-up-rust-docker-builds/ # https://whitfin.io/speeding-up-rust-docker-builds/
####################### VAULT BUILD IMAGE ####################### ####################### VAULT BUILD IMAGE #######################
FROM node:8-alpine as vault FROM alpine as vault
ENV VAULT_VERSION "v2.4.0" ENV VAULT_VERSION "v2.7.1"
ENV URL "https://github.com/bitwarden/web.git" 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 \ RUN apk add --update-cache --upgrade \
curl \ curl \
git \
tar tar
RUN git clone -b $VAULT_VERSION --depth 1 $URL web-build RUN mkdir /web-vault
WORKDIR /web-build WORKDIR /web-vault
COPY /docker/set-vault-baseurl.patch /web-build/ RUN curl -L $URL | tar xz
RUN git apply set-vault-baseurl.patch RUN ls
RUN npm run sub:init && npm install
RUN npm run dist \
&& mv build /web-vault
########################## BUILD IMAGE ########################## ########################## BUILD IMAGE ##########################
# Musl build image for statically compiled binary # Musl build image for statically compiled binary
FROM clux/muslrust:nightly-2018-08-24 as build FROM clux/muslrust:nightly-2018-12-01 as build
# Creates a dummy project used to grab dependencies ENV USER "root"
RUN USER=root cargo init --bin
# Copies over *only* your manifests and vendored dependencies WORKDIR /app
COPY ./Cargo.* ./
COPY ./libs ./libs
COPY ./rust-toolchain ./rust-toolchain
# Builds your dependencies and removes the
# dummy project, except the target folder
# This folder contains the compiled dependencies
RUN cargo build --release
RUN find . -not -path "./target*" -delete
# Copies the complete project # Copies the complete project
# To avoid copying unneeded files, use .dockerignore # To avoid copying unneeded files, use .dockerignore
COPY . . COPY . .
# Builds again, this time it'll just be RUN rustup target add x86_64-unknown-linux-musl
# your actual source files being built
# Build
RUN cargo build --release RUN cargo build --release
######################## RUNTIME IMAGE ######################## ######################## RUNTIME IMAGE ########################
@@ -56,6 +41,7 @@ RUN cargo build --release
FROM alpine:3.8 FROM alpine:3.8
ENV ROCKET_ENV "staging" ENV ROCKET_ENV "staging"
ENV ROCKET_PORT=80
ENV ROCKET_WORKERS=10 ENV ROCKET_WORKERS=10
ENV SSL_CERT_DIR=/etc/ssl/certs ENV SSL_CERT_DIR=/etc/ssl/certs
@@ -75,7 +61,7 @@ EXPOSE 3012
COPY .env . COPY .env .
COPY Rocket.toml . COPY Rocket.toml .
COPY --from=vault /web-vault ./web-vault COPY --from=vault /web-vault ./web-vault
COPY --from=build /volume/target/x86_64-unknown-linux-musl/release/bitwarden_rs . COPY --from=build /app/target/x86_64-unknown-linux-musl/release/bitwarden_rs .
# Configures the startup! # Configures the startup!
CMD ./bitwarden_rs CMD ./bitwarden_rs

View File

@@ -2,27 +2,21 @@
# https://docs.docker.com/develop/develop-images/multistage-build/ # https://docs.docker.com/develop/develop-images/multistage-build/
# https://whitfin.io/speeding-up-rust-docker-builds/ # https://whitfin.io/speeding-up-rust-docker-builds/
####################### VAULT BUILD IMAGE ####################### ####################### VAULT BUILD IMAGE #######################
FROM node:8-alpine as vault FROM alpine as vault
ENV VAULT_VERSION "v2.4.0" ENV VAULT_VERSION "v2.7.1"
ENV URL "https://github.com/bitwarden/web.git" 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 \ RUN apk add --update-cache --upgrade \
curl \ curl \
git \
tar tar
RUN git clone -b $VAULT_VERSION --depth 1 $URL web-build RUN mkdir /web-vault
WORKDIR /web-build WORKDIR /web-vault
COPY /docker/set-vault-baseurl.patch /web-build/ RUN curl -L $URL | tar xz
RUN git apply set-vault-baseurl.patch RUN ls
RUN npm run sub:init && npm install
RUN npm run dist \
&& mv build /web-vault
########################## BUILD IMAGE ########################## ########################## BUILD IMAGE ##########################
# We need to use the Rust build image, because # We need to use the Rust build image, because
@@ -39,15 +33,8 @@ RUN apt-get update \
ENV CARGO_HOME "/root/.cargo" ENV CARGO_HOME "/root/.cargo"
ENV USER "root" ENV USER "root"
# Creates a dummy project used to grab dependencies
RUN USER=root cargo new --bin app
WORKDIR /app WORKDIR /app
# Copies over *only* your manifests and vendored dependencies
COPY ./Cargo.* ./
COPY ./libs ./libs
COPY ./rust-toolchain ./rust-toolchain
# Prepare openssl armhf libs # Prepare openssl armhf libs
RUN sed 's/^deb/deb-src/' /etc/apt/sources.list > \ RUN sed 's/^deb/deb-src/' /etc/apt/sources.list > \
/etc/apt/sources.list.d/deb-src.list \ /etc/apt/sources.list.d/deb-src.list \
@@ -62,19 +49,12 @@ ENV CROSS_COMPILE="1"
ENV OPENSSL_INCLUDE_DIR="/usr/include/arm-linux-gnueabihf" ENV OPENSSL_INCLUDE_DIR="/usr/include/arm-linux-gnueabihf"
ENV OPENSSL_LIB_DIR="/usr/lib/arm-linux-gnueabihf" ENV OPENSSL_LIB_DIR="/usr/lib/arm-linux-gnueabihf"
# Builds your dependencies and removes the
# dummy project, except the target folder
# This folder contains the compiled dependencies
RUN rustup target add armv7-unknown-linux-gnueabihf
RUN cargo build --release --target=armv7-unknown-linux-gnueabihf -v
RUN find . -not -path "./target*" -delete
# Copies the complete project # Copies the complete project
# To avoid copying unneeded files, use .dockerignore # To avoid copying unneeded files, use .dockerignore
COPY . . COPY . .
# Builds again, this time it'll just be # Build
# your actual source files being built RUN rustup target add armv7-unknown-linux-gnueabihf
RUN cargo build --release --target=armv7-unknown-linux-gnueabihf -v RUN cargo build --release --target=armv7-unknown-linux-gnueabihf -v
######################## RUNTIME IMAGE ######################## ######################## RUNTIME IMAGE ########################
@@ -83,6 +63,7 @@ RUN cargo build --release --target=armv7-unknown-linux-gnueabihf -v
FROM balenalib/armv7hf-debian:stretch FROM balenalib/armv7hf-debian:stretch
ENV ROCKET_ENV "staging" ENV ROCKET_ENV "staging"
ENV ROCKET_PORT=80
ENV ROCKET_WORKERS=10 ENV ROCKET_WORKERS=10
RUN [ "cross-build-start" ] RUN [ "cross-build-start" ]

197
README.md
View File

@@ -28,6 +28,7 @@ _*Note, that this project is not associated with the [Bitwarden](https://bitward
- [Enabling HTTPS](#enabling-https) - [Enabling HTTPS](#enabling-https)
- [Enabling WebSocket notifications](#enabling-websocket-notifications) - [Enabling WebSocket notifications](#enabling-websocket-notifications)
- [Enabling U2F authentication](#enabling-u2f-authentication) - [Enabling U2F authentication](#enabling-u2f-authentication)
- [Enabling YubiKey OTP authentication](#enabling-yubikey-otp-authentication)
- [Changing persistent data location](#changing-persistent-data-location) - [Changing persistent data location](#changing-persistent-data-location)
- [/data prefix:](#data-prefix) - [/data prefix:](#data-prefix)
- [database name and location](#database-name-and-location) - [database name and location](#database-name-and-location)
@@ -39,6 +40,14 @@ _*Note, that this project is not associated with the [Bitwarden](https://bitward
- [Password hint display](#password-hint-display) - [Password hint display](#password-hint-display)
- [Disabling or overriding the Vault interface hosting](#disabling-or-overriding-the-vault-interface-hosting) - [Disabling or overriding the Vault interface hosting](#disabling-or-overriding-the-vault-interface-hosting)
- [Other configuration](#other-configuration) - [Other configuration](#other-configuration)
- [Fail2Ban Setup](#fail2ban-setup)
- [Logging Failed Login Attempts to Syslog](#logging-failed-login-attempts-to-syslog)
- [Fail2Ban Filter](#fail2ban-filter)
- [Fail2Ban Jail](#fail2ban-jail)
- [Testing Fail2Ban](#testing-fail2ban)
- [Running with systemd-docker](#running-with-systemd-docker)
- [Setting environment variables](#setting-environment-variables)
- [Running the service](#running-the-service)
- [Building your own image](#building-your-own-image) - [Building your own image](#building-your-own-image)
- [Building binary](#building-binary) - [Building binary](#building-binary)
- [Available packages](#available-packages) - [Available packages](#available-packages)
@@ -68,11 +77,11 @@ Basically full implementation of Bitwarden API is provided including:
* Serving the static files for Vault interface * Serving the static files for Vault interface
* Website icons API * Website icons API
* Authenticator and U2F support * Authenticator and U2F support
* YubiKey OTP
## Missing features ## Missing features
* Email confirmation * Email confirmation
* Other two-factor systems: * Other two-factor systems:
* YubiKey OTP (if your key supports U2F, you can use that)
* Duo * Duo
* Email codes * Email codes
@@ -252,6 +261,22 @@ docker run -d --name bitwarden \
Note that the value has to include the `https://` and it may include a port at the end (in the format of `https://bw.domain.tld:port`) when not using `443`. Note that the value has to include the `https://` and it may include a port at the end (in the format of `https://bw.domain.tld:port`) when not using `443`.
### Enabling YubiKey OTP authentication
To enable YubiKey authentication, you must set the `YUBICO_CLIENT_ID` and `YUBICO_SECRET_KEY` env variables.
If `YUBICO_SERVER` is not specified, it will use the default YubiCloud servers. You can generate `YUBICO_CLIENT_ID` and `YUBICO_SECRET_KEY` for the default YubiCloud [here](https://upgrade.yubico.com/getapikey/).
Note: In order to generate API keys or use a YubiKey with an OTP server, it must be registered. After configuring your key in the [YubiKey Personalization Tool](https://www.yubico.com/products/services-software/personalization-tools/use/), you can register it with the default servers [here](https://upload.yubico.com/).
```sh
docker run -d --name bitwarden \
-e YUBICO_CLIENT_ID=12345 \
-e YUBICO_SECRET_KEY=ABCDEABCDEABCDEABCDE= \
-v /bw-data/:/data/ \
-p 80:80 \
mprasil/bitwarden:latest
```
### Changing persistent data location ### Changing persistent data location
#### /data prefix: #### /data prefix:
@@ -399,6 +424,168 @@ Note that you can also change the path where bitwarden_rs looks for static files
Though this is unlikely to be required in small deployment, you can fine-tune some other settings like number of workers using environment variables that are processed by [Rocket](https://rocket.rs), please see details in [documentation](https://rocket.rs/guide/configuration/#environment-variables). Though this is unlikely to be required in small deployment, you can fine-tune some other settings like number of workers using environment variables that are processed by [Rocket](https://rocket.rs), please see details in [documentation](https://rocket.rs/guide/configuration/#environment-variables).
### Fail2Ban Setup
Bitwarden_rs logs failed login attempts to stdout. We need to set this so the host OS can see these. Then we can setup Fail2Ban.
#### Logging Failed Login Attempts to Syslog
We need to set the logging driver to syslog so the host OS and Fail2Ban can see them.
If you are using docker commands, you will need to add: `--log-driver syslog --log-opt tag=$TAG` to your command.
If you are using docker-compose, add this to you yaml file:
```
bitwarden:
logging:
driver: "syslog"
options:
tag: "$TAG"
```
With the above settings in the docker-compose file. Any failed login attempts will look like this in your syslog file:
```
MMM DD hh:mm:ss server-hostname $TAG[773]: [YYYY-MM-DD][hh:mm:ss][bitwarden_rs::api::identity][ERROR] Username or password is incorrect. Try again. IP: XXX.XXX.XXX.XXX. Username: email@domain.com.
```
You can change the '$TAG' to anything you like. Just remember it because it will be in the Fail2Ban filter.
#### Fail2Ban Filter
Create the filter file
```
sudo nano /etc/fail2ban/filter.d/bitwarden.conf
```
And add the following
```
[INCLUDES]
before = common.conf
[Definition]
_daemon = $TAG
failregex = ^%(__prefix_line)s.*Username or password is incorrect\. Try again\. IP: <HOST>\. Username:.*$
ignoreregex =
```
Dont forget to change the '$TAG' to what you set it as from above.
#### Fail2Ban Jail
Now we need the jail, create the jail file
```
sudo nano /etc/fail2ban/jail.d/bitwarden.local
```
and add:
```
[bitwarden]
enabled = true
port = 80,443,8081
filter = bitwarden
action = iptables-allports[name=bitwarden]
logpath = /var/log/syslog
maxretry = 3
bantime = 14400
findtime = 14400
```
Feel free to change the options as you see fit.
#### Testing Fail2Ban
Now just try to login to bitwarden using any email (it doesnt have to be a valid email, just an email format)
If it works correctly and your IP is banned, you can unban the ip by running:
```
sudo fail2ban-client unban XX.XX.XX.XX bitwarden
```
### Running with systemd-docker
These instructions allow you to have systemd manage the lifecycle of the docker container, if you prefer.
First, install the `systemd-docker` package using your system package manager.
This is a wrapper which improves docker integration with systemd.
For full instructions and configuration options, see the [GitHub repository](https://github.com/ibuildthecloud/systemd-docker).
As root, create `/etc/systemd/system/bitwarden.service` using your preferred editor with the following contents:
```ini
[Unit]
Description=Bitwarden
After=docker.service
Requires=docker.service
[Service]
TimeoutStartSec=0
ExecStartPre=/usr/bin/docker pull mprasil/bitwarden:latest
ExecStart=/usr/bin/systemd-docker --cgroups name=systemd --env run \
-p 8080:80 \
-p 8081:3012 \
-v /opt/bw-data:/data/ \
--rm --name %n mprasil/bitwarden:latest
Restart=always
RestartSec=10s
Type=notify
NotifyAccess=all
[Install]
WantedBy=multi-user.target
```
Adjust the above example as necessary. In particular, pay attention to the `-p` and `-v` options,
as these control the port and volume bindings between the container and the host.
Explanation of options which may not be self-explanatory:
- A `TimeoutStartSec` value of 0 stops systemd from considering the service failed
after waiting for the default startup time. This is required as it may take a while for the `docker pull` in `ExecStartPre` to finish.
- `ExecStartPre`: Pull the docker tag before running.
- A `Type` value of `notify` tells systemd to expect a notification from the service that it is ready.
- A `NotifyAccess` value of `all` is required by `systemd-docker`.
#### Setting environment variables
It's possible to directly specify environment variables in the unit file in two ways:
- Using an `Environment` directive in the `[Service]` block.
- Using the `-e` option of `docker`. In this case, you can omit the `--env` option shown in the example above.
To verify that your environment variables are set correctly, check the output of `systemctl show bitwarden.service`
for an `Environment` line.
It's also possible to store environment variables in a separate file using the `EnvironmentFile` directive in the unit file.
Systemd can source a file of the form:
```shell
Key="Value"
```
However, the systemd project does not mandate where this file should be stored. Consult your distribution's documentation for the
best location for this file. For example, RedHat based distributions typically place these files in `/etc/sysconfig/`
If you're unsure, just create a file as root in `/etc/` e.g. `/etc/bitwarden.service.conf`.
In your unit file, add an `EnvironmentFile` directive in the `[Service]` block, the value being the full path to the
file created above. Example:
```ini
[Unit]
Description=Bitwarden
After=docker.service
Requires=docker.service
[Service]
EnvironmentFile=/etc/bitwarden.service.conf
TimeoutStartSec=0
-snip-
```
#### Running the service
After the above installation and configuration is complete, reload systemd using `sudo systemctl daemon-reload`.
Then, start the Bitwarden service using `sudo systemctl start bitwarden`.
To have the service start with the system, use `sudo systemctl enable bitwarden`.
Verify that the container has started using `systemctl status bitwarden`.
## Building your own image ## Building your own image
Clone the repository, then from the root of the repository run: Clone the repository, then from the root of the repository run:
@@ -430,11 +617,19 @@ It will setup a fully functional and secure `bitwarden_rs` application in Kubern
The sqlite3 database should be backed up using the proper sqlite3 backup command. This will ensure the database does not become corrupted if the backup happens during a database write. The sqlite3 database should be backed up using the proper sqlite3 backup command. This will ensure the database does not become corrupted if the backup happens during a database write.
``` ```
mkdir $DATA_FOLDER/db-backup
sqlite3 /$DATA_FOLDER/db.sqlite3 ".backup '/$DATA_FOLDER/db-backup/backup.sqlite3'" sqlite3 /$DATA_FOLDER/db.sqlite3 ".backup '/$DATA_FOLDER/db-backup/backup.sqlite3'"
``` ```
This command can be run via a CRON job everyday, however note that it will overwrite the same `backup.sqlite3` file each time. This backup file should therefore be saved via incremental backup either using a CRON job command that appends a timestamp or from another backup app such as Duplicati. To restore simply overwrite `db.sqlite3` with `backup.sqlite3` (while bitwarden_rs is stopped). This command can be run via a CRON job everyday, however note that it will overwrite the same `backup.sqlite3` file each time. This backup file should therefore be saved via incremental backup either using a CRON job command that appends a timestamp or from another backup app such as Duplicati. To restore simply overwrite `db.sqlite3` with `backup.sqlite3` (while bitwarden_rs is stopped).
Running the above command requires sqlite3 to be installed on the docker host system. You can achieve the same result with a sqlite3 docker container using the following command.
```
docker run --rm --volumes-from=bitwarden bruceforce/bw_backup /backup.sh
```
You can also run a container with integrated cron daemon to automatically backup your database. See https://gitlab.com/1O/bitwarden_rs-backup for examples.
### 2. the attachments folder ### 2. the attachments folder
By default, this is located in `$DATA_FOLDER/attachments` By default, this is located in `$DATA_FOLDER/attachments`

View File

@@ -1,20 +0,0 @@
[package]
name = "jsonwebtoken"
version = "4.0.1"
authors = ["Vincent Prouillet <prouillet.vincent@gmail.com>"]
license = "MIT"
readme = "README.md"
description = "Create and parse JWT in a strongly typed way."
homepage = "https://github.com/Keats/rust-jwt"
repository = "https://github.com/Keats/rust-jwt"
keywords = ["jwt", "web", "api", "token", "json"]
[dependencies]
error-chain = { version = "0.11", default-features = false }
serde_json = "1.0"
serde_derive = "1.0"
serde = "1.0"
ring = { version = "0.11.0", features = ["rsa_signing", "dev_urandom_fallback"] }
base64 = "0.9"
untrusted = "0.5"
chrono = "0.4"

View File

@@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) 2015 Vincent Prouillet
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,120 +0,0 @@
use std::sync::Arc;
use base64;
use ring::{rand, digest, hmac, signature};
use ring::constant_time::verify_slices_are_equal;
use untrusted;
use errors::{Result, ErrorKind};
/// The algorithms supported for signing/verifying
#[derive(Debug, PartialEq, Copy, Clone, Serialize, Deserialize)]
pub enum Algorithm {
/// HMAC using SHA-256
HS256,
/// HMAC using SHA-384
HS384,
/// HMAC using SHA-512
HS512,
/// RSASSA-PKCS1-v1_5 using SHA-256
RS256,
/// RSASSA-PKCS1-v1_5 using SHA-384
RS384,
/// RSASSA-PKCS1-v1_5 using SHA-512
RS512,
}
/// The actual HS signing + encoding
fn sign_hmac(alg: &'static digest::Algorithm, key: &[u8], signing_input: &str) -> Result<String> {
let signing_key = hmac::SigningKey::new(alg, key);
let digest = hmac::sign(&signing_key, signing_input.as_bytes());
Ok(
base64::encode_config::<hmac::Signature>(&digest, base64::URL_SAFE_NO_PAD)
)
}
/// The actual RSA signing + encoding
/// Taken from Ring doc https://briansmith.org/rustdoc/ring/signature/index.html
fn sign_rsa(alg: Algorithm, key: &[u8], signing_input: &str) -> Result<String> {
let ring_alg = match alg {
Algorithm::RS256 => &signature::RSA_PKCS1_SHA256,
Algorithm::RS384 => &signature::RSA_PKCS1_SHA384,
Algorithm::RS512 => &signature::RSA_PKCS1_SHA512,
_ => unreachable!(),
};
let key_pair = Arc::new(
signature::RSAKeyPair::from_der(untrusted::Input::from(key))
.map_err(|_| ErrorKind::InvalidKey)?
);
let mut signing_state = signature::RSASigningState::new(key_pair)
.map_err(|_| ErrorKind::InvalidKey)?;
let mut signature = vec![0; signing_state.key_pair().public_modulus_len()];
let rng = rand::SystemRandom::new();
signing_state.sign(ring_alg, &rng, signing_input.as_bytes(), &mut signature)
.map_err(|_| ErrorKind::InvalidKey)?;
Ok(
base64::encode_config::<[u8]>(&signature, base64::URL_SAFE_NO_PAD)
)
}
/// Take the payload of a JWT, sign it using the algorithm given and return
/// the base64 url safe encoded of the result.
///
/// Only use this function if you want to do something other than JWT.
pub fn sign(signing_input: &str, key: &[u8], algorithm: Algorithm) -> Result<String> {
match algorithm {
Algorithm::HS256 => sign_hmac(&digest::SHA256, key, signing_input),
Algorithm::HS384 => sign_hmac(&digest::SHA384, key, signing_input),
Algorithm::HS512 => sign_hmac(&digest::SHA512, key, signing_input),
Algorithm::RS256 | Algorithm::RS384 | Algorithm::RS512 => sign_rsa(algorithm, key, signing_input),
// TODO: if PKCS1 is made prublic, remove the line above and uncomment below
// Algorithm::RS256 => sign_rsa(&signature::RSA_PKCS1_SHA256, key, signing_input),
// Algorithm::RS384 => sign_rsa(&signature::RSA_PKCS1_SHA384, key, signing_input),
// Algorithm::RS512 => sign_rsa(&signature::RSA_PKCS1_SHA512, key, signing_input),
}
}
/// See Ring RSA docs for more details
fn verify_rsa(alg: &signature::RSAParameters, signature: &str, signing_input: &str, key: &[u8]) -> Result<bool> {
let signature_bytes = base64::decode_config(signature, base64::URL_SAFE_NO_PAD)?;
let public_key_der = untrusted::Input::from(key);
let message = untrusted::Input::from(signing_input.as_bytes());
let expected_signature = untrusted::Input::from(signature_bytes.as_slice());
let res = signature::verify(alg, public_key_der, message, expected_signature);
Ok(res.is_ok())
}
/// Compares the signature given with a re-computed signature for HMAC or using the public key
/// for RSA.
///
/// Only use this function if you want to do something other than JWT.
///
/// `signature` is the signature part of a jwt (text after the second '.')
///
/// `signing_input` is base64(header) + "." + base64(claims)
pub fn verify(signature: &str, signing_input: &str, key: &[u8], algorithm: Algorithm) -> Result<bool> {
match algorithm {
Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512 => {
// we just re-sign the data with the key and compare if they are equal
let signed = sign(signing_input, key, algorithm)?;
Ok(verify_slices_are_equal(signature.as_ref(), signed.as_ref()).is_ok())
},
Algorithm::RS256 => verify_rsa(&signature::RSA_PKCS1_2048_8192_SHA256, signature, signing_input, key),
Algorithm::RS384 => verify_rsa(&signature::RSA_PKCS1_2048_8192_SHA384, signature, signing_input, key),
Algorithm::RS512 => verify_rsa(&signature::RSA_PKCS1_2048_8192_SHA512, signature, signing_input, key),
}
}
impl Default for Algorithm {
fn default() -> Self {
Algorithm::HS256
}
}

View File

@@ -1,68 +0,0 @@
use base64;
use serde_json;
use ring;
error_chain! {
errors {
/// When a token doesn't have a valid JWT shape
InvalidToken {
description("invalid token")
display("Invalid token")
}
/// When the signature doesn't match
InvalidSignature {
description("invalid signature")
display("Invalid signature")
}
/// When the secret given is not a valid RSA key
InvalidKey {
description("invalid key")
display("Invalid Key")
}
// Validation error
/// When a tokens `exp` claim indicates that it has expired
ExpiredSignature {
description("expired signature")
display("Expired Signature")
}
/// When a tokens `iss` claim does not match the expected issuer
InvalidIssuer {
description("invalid issuer")
display("Invalid Issuer")
}
/// When a tokens `aud` claim does not match one of the expected audience values
InvalidAudience {
description("invalid audience")
display("Invalid Audience")
}
/// When a tokens `aud` claim does not match one of the expected audience values
InvalidSubject {
description("invalid subject")
display("Invalid Subject")
}
/// When a tokens `iat` claim is in the future
InvalidIssuedAt {
description("invalid issued at")
display("Invalid Issued At")
}
/// When a tokens nbf claim represents a time in the future
ImmatureSignature {
description("immature signature")
display("Immature Signature")
}
/// When the algorithm in the header doesn't match the one passed to `decode`
InvalidAlgorithm {
description("Invalid algorithm")
display("Invalid Algorithm")
}
}
foreign_links {
Unspecified(ring::error::Unspecified) #[doc = "An error happened while signing/verifying a token with RSA"];
Base64(base64::DecodeError) #[doc = "An error happened while decoding some base64 text"];
Json(serde_json::Error) #[doc = "An error happened while serializing/deserializing JSON"];
Utf8(::std::string::FromUtf8Error) #[doc = "An error happened while trying to convert the result of base64 decoding to a String"];
}
}

View File

@@ -1,64 +0,0 @@
use crypto::Algorithm;
/// A basic JWT header, the alg defaults to HS256 and typ is automatically
/// set to `JWT`. All the other fields are optional.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Header {
/// The type of JWS: it can only be "JWT" here
///
/// Defined in [RFC7515#4.1.9](https://tools.ietf.org/html/rfc7515#section-4.1.9).
#[serde(skip_serializing_if = "Option::is_none")]
pub typ: Option<String>,
/// The algorithm used
///
/// Defined in [RFC7515#4.1.1](https://tools.ietf.org/html/rfc7515#section-4.1.1).
pub alg: Algorithm,
/// Content type
///
/// Defined in [RFC7519#5.2](https://tools.ietf.org/html/rfc7519#section-5.2).
#[serde(skip_serializing_if = "Option::is_none")]
pub cty: Option<String>,
/// JSON Key URL
///
/// Defined in [RFC7515#4.1.2](https://tools.ietf.org/html/rfc7515#section-4.1.2).
#[serde(skip_serializing_if = "Option::is_none")]
pub jku: Option<String>,
/// Key ID
///
/// Defined in [RFC7515#4.1.4](https://tools.ietf.org/html/rfc7515#section-4.1.4).
#[serde(skip_serializing_if = "Option::is_none")]
pub kid: Option<String>,
/// X.509 URL
///
/// Defined in [RFC7515#4.1.5](https://tools.ietf.org/html/rfc7515#section-4.1.5).
#[serde(skip_serializing_if = "Option::is_none")]
pub x5u: Option<String>,
/// X.509 certificate thumbprint
///
/// Defined in [RFC7515#4.1.7](https://tools.ietf.org/html/rfc7515#section-4.1.7).
#[serde(skip_serializing_if = "Option::is_none")]
pub x5t: Option<String>,
}
impl Header {
/// Returns a JWT header with the algorithm given
pub fn new(algorithm: Algorithm) -> Header {
Header {
typ: Some("JWT".to_string()),
alg: algorithm,
cty: None,
jku: None,
kid: None,
x5u: None,
x5t: None,
}
}
}
impl Default for Header {
/// Returns a JWT header using the default Algorithm, HS256
fn default() -> Self {
Header::new(Algorithm::default())
}
}

View File

@@ -1,142 +0,0 @@
//! Create and parses JWT (JSON Web Tokens)
//!
//! Documentation: [stable](https://docs.rs/jsonwebtoken/)
#![recursion_limit = "300"]
#![deny(missing_docs)]
#![allow(unused_doc_comments)]
#![allow(renamed_and_removed_lints)]
#[macro_use]
extern crate error_chain;
#[macro_use]
extern crate serde_derive;
extern crate serde_json;
extern crate serde;
extern crate base64;
extern crate ring;
extern crate untrusted;
extern crate chrono;
/// All the errors, generated using error-chain
pub mod errors;
mod header;
mod crypto;
mod serialization;
mod validation;
pub use header::Header;
pub use crypto::{
Algorithm,
sign,
verify,
};
pub use validation::Validation;
pub use serialization::TokenData;
use serde::de::DeserializeOwned;
use serde::ser::Serialize;
use errors::{Result, ErrorKind};
use serialization::{from_jwt_part, from_jwt_part_claims, to_jwt_part};
use validation::{validate};
/// Encode the header and claims given and sign the payload using the algorithm from the header and the key
///
/// ```rust,ignore
/// #[macro_use]
/// extern crate serde_derive;
/// use jsonwebtoken::{encode, Algorithm, Header};
///
/// /// #[derive(Debug, Serialize, Deserialize)]
/// struct Claims {
/// sub: String,
/// company: String
/// }
///
/// let my_claims = Claims {
/// sub: "b@b.com".to_owned(),
/// company: "ACME".to_owned()
/// };
///
/// // my_claims is a struct that implements Serialize
/// // This will create a JWT using HS256 as algorithm
/// let token = encode(&Header::default(), &my_claims, "secret".as_ref()).unwrap();
/// ```
pub fn encode<T: Serialize>(header: &Header, claims: &T, key: &[u8]) -> Result<String> {
let encoded_header = to_jwt_part(&header)?;
let encoded_claims = to_jwt_part(&claims)?;
let signing_input = [encoded_header.as_ref(), encoded_claims.as_ref()].join(".");
let signature = sign(&*signing_input, key.as_ref(), header.alg)?;
Ok([signing_input, signature].join("."))
}
/// Used in decode: takes the result of a rsplit and ensure we only get 2 parts
/// Errors if we don't
macro_rules! expect_two {
($iter:expr) => {{
let mut i = $iter;
match (i.next(), i.next(), i.next()) {
(Some(first), Some(second), None) => (first, second),
_ => return Err(ErrorKind::InvalidToken.into())
}
}}
}
/// Decode a token into a struct containing 2 fields: `claims` and `header`.
///
/// If the token or its signature is invalid or the claims fail validation, it will return an error.
///
/// ```rust,ignore
/// #[macro_use]
/// extern crate serde_derive;
/// use jsonwebtoken::{decode, Validation, Algorithm};
///
/// #[derive(Debug, Serialize, Deserialize)]
/// struct Claims {
/// sub: String,
/// company: String
/// }
///
/// let token = "a.jwt.token".to_string();
/// // Claims is a struct that implements Deserialize
/// let token_data = decode::<Claims>(&token, "secret", &Validation::new(Algorithm::HS256));
/// ```
pub fn decode<T: DeserializeOwned>(token: &str, key: &[u8], validation: &Validation) -> Result<TokenData<T>> {
let (signature, signing_input) = expect_two!(token.rsplitn(2, '.'));
let (claims, header) = expect_two!(signing_input.rsplitn(2, '.'));
let header: Header = from_jwt_part(header)?;
if !verify(signature, signing_input, key, header.alg)? {
return Err(ErrorKind::InvalidSignature.into());
}
if !validation.algorithms.contains(&header.alg) {
return Err(ErrorKind::InvalidAlgorithm.into());
}
let (decoded_claims, claims_map): (T, _) = from_jwt_part_claims(claims)?;
validate(&claims_map, validation)?;
Ok(TokenData { header: header, claims: decoded_claims })
}
/// Decode a token and return the Header. This is not doing any kind of validation: it is meant to be
/// used when you don't know which `alg` the token is using and want to find out.
///
/// If the token has an invalid format, it will return an error.
///
/// ```rust,ignore
/// use jsonwebtoken::decode_header;
///
/// let token = "a.jwt.token".to_string();
/// let header = decode_header(&token);
/// ```
pub fn decode_header(token: &str) -> Result<Header> {
let (_, signing_input) = expect_two!(token.rsplitn(2, '.'));
let (_, header) = expect_two!(signing_input.rsplitn(2, '.'));
from_jwt_part(header)
}

View File

@@ -1,42 +0,0 @@
use base64;
use serde::de::DeserializeOwned;
use serde::ser::Serialize;
use serde_json::{from_str, to_string, Value};
use serde_json::map::Map;
use errors::{Result};
use header::Header;
/// The return type of a successful call to decode
#[derive(Debug)]
pub struct TokenData<T> {
/// The decoded JWT header
pub header: Header,
/// The decoded JWT claims
pub claims: T
}
/// Serializes to JSON and encodes to base64
pub fn to_jwt_part<T: Serialize>(input: &T) -> Result<String> {
let encoded = to_string(input)?;
Ok(base64::encode_config(encoded.as_bytes(), base64::URL_SAFE_NO_PAD))
}
/// Decodes from base64 and deserializes from JSON to a struct
pub fn from_jwt_part<B: AsRef<str>, T: DeserializeOwned>(encoded: B) -> Result<T> {
let decoded = base64::decode_config(encoded.as_ref(), base64::URL_SAFE_NO_PAD)?;
let s = String::from_utf8(decoded)?;
Ok(from_str(&s)?)
}
/// Decodes from base64 and deserializes from JSON to a struct AND a hashmap
pub fn from_jwt_part_claims<B: AsRef<str>, T: DeserializeOwned>(encoded: B) -> Result<(T, Map<String, Value>)> {
let decoded = base64::decode_config(encoded.as_ref(), base64::URL_SAFE_NO_PAD)?;
let s = String::from_utf8(decoded)?;
let claims: T = from_str(&s)?;
let map: Map<_,_> = from_str(&s)?;
Ok((claims, map))
}

View File

@@ -1,377 +0,0 @@
use chrono::Utc;
use serde::ser::Serialize;
use serde_json::{Value, from_value, to_value};
use serde_json::map::Map;
use errors::{Result, ErrorKind};
use crypto::Algorithm;
/// Contains the various validations that are applied after decoding a token.
///
/// All time validation happen on UTC timestamps.
///
/// ```rust
/// use jsonwebtoken::Validation;
///
/// // Default value
/// let validation = Validation::default();
///
/// // Changing one parameter
/// let mut validation = Validation {leeway: 60, ..Default::default()};
///
/// // Setting audience
/// let mut validation = Validation::default();
/// validation.set_audience(&"Me"); // string
/// validation.set_audience(&["Me", "You"]); // array of strings
/// ```
#[derive(Debug, Clone, PartialEq)]
pub struct Validation {
/// Add some leeway (in seconds) to the `exp`, `iat` and `nbf` validation to
/// account for clock skew.
///
/// Defaults to `0`.
pub leeway: i64,
/// Whether to validate the `exp` field.
///
/// It will return an error if the time in the `exp` field is past.
///
/// Defaults to `true`.
pub validate_exp: bool,
/// Whether to validate the `iat` field.
///
/// It will return an error if the time in the `iat` field is in the future.
///
/// Defaults to `true`.
pub validate_iat: bool,
/// Whether to validate the `nbf` field.
///
/// It will return an error if the current timestamp is before the time in the `nbf` field.
///
/// Defaults to `true`.
pub validate_nbf: bool,
/// If it contains a value, the validation will check that the `aud` field is the same as the
/// one provided and will error otherwise.
/// Since `aud` can be either a String or a Vec<String> in the JWT spec, you will need to use
/// the [set_audience](struct.Validation.html#method.set_audience) method to set it.
///
/// Defaults to `None`.
pub aud: Option<Value>,
/// If it contains a value, the validation will check that the `iss` field is the same as the
/// one provided and will error otherwise.
///
/// Defaults to `None`.
pub iss: Option<String>,
/// If it contains a value, the validation will check that the `sub` field is the same as the
/// one provided and will error otherwise.
///
/// Defaults to `None`.
pub sub: Option<String>,
/// If it contains a value, the validation will check that the `alg` of the header is contained
/// in the ones provided and will error otherwise.
///
/// Defaults to `vec![Algorithm::HS256]`.
pub algorithms: Vec<Algorithm>,
}
impl Validation {
/// Create a default validation setup allowing the given alg
pub fn new(alg: Algorithm) -> Validation {
let mut validation = Validation::default();
validation.algorithms = vec![alg];
validation
}
/// Since `aud` can be either a String or an array of String in the JWT spec, this method will take
/// care of serializing the value.
pub fn set_audience<T: Serialize>(&mut self, audience: &T) {
self.aud = Some(to_value(audience).unwrap());
}
}
impl Default for Validation {
fn default() -> Validation {
Validation {
leeway: 0,
validate_exp: true,
validate_iat: true,
validate_nbf: true,
iss: None,
sub: None,
aud: None,
algorithms: vec![Algorithm::HS256],
}
}
}
pub fn validate(claims: &Map<String, Value>, options: &Validation) -> Result<()> {
let now = Utc::now().timestamp();
if let Some(iat) = claims.get("iat") {
if options.validate_iat && from_value::<i64>(iat.clone())? > now + options.leeway {
return Err(ErrorKind::InvalidIssuedAt.into());
}
}
if let Some(exp) = claims.get("exp") {
if options.validate_exp && from_value::<i64>(exp.clone())? < now - options.leeway {
return Err(ErrorKind::ExpiredSignature.into());
}
}
if let Some(nbf) = claims.get("nbf") {
if options.validate_nbf && from_value::<i64>(nbf.clone())? > now + options.leeway {
return Err(ErrorKind::ImmatureSignature.into());
}
}
if let Some(iss) = claims.get("iss") {
if let Some(ref correct_iss) = options.iss {
if from_value::<String>(iss.clone())? != *correct_iss {
return Err(ErrorKind::InvalidIssuer.into());
}
}
}
if let Some(sub) = claims.get("sub") {
if let Some(ref correct_sub) = options.sub {
if from_value::<String>(sub.clone())? != *correct_sub {
return Err(ErrorKind::InvalidSubject.into());
}
}
}
if let Some(aud) = claims.get("aud") {
if let Some(ref correct_aud) = options.aud {
if aud != correct_aud {
return Err(ErrorKind::InvalidAudience.into());
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use serde_json::{to_value};
use serde_json::map::Map;
use chrono::Utc;
use super::{validate, Validation};
use errors::ErrorKind;
#[test]
fn iat_in_past_ok() {
let mut claims = Map::new();
claims.insert("iat".to_string(), to_value(Utc::now().timestamp() - 10000).unwrap());
let res = validate(&claims, &Validation::default());
assert!(res.is_ok());
}
#[test]
fn iat_in_future_fails() {
let mut claims = Map::new();
claims.insert("iat".to_string(), to_value(Utc::now().timestamp() + 100000).unwrap());
let res = validate(&claims, &Validation::default());
assert!(res.is_err());
match res.unwrap_err().kind() {
&ErrorKind::InvalidIssuedAt => (),
_ => assert!(false),
};
}
#[test]
fn iat_in_future_but_in_leeway_ok() {
let mut claims = Map::new();
claims.insert("iat".to_string(), to_value(Utc::now().timestamp() + 50).unwrap());
let validation = Validation {
leeway: 1000 * 60,
..Default::default()
};
let res = validate(&claims, &validation);
assert!(res.is_ok());
}
#[test]
fn exp_in_future_ok() {
let mut claims = Map::new();
claims.insert("exp".to_string(), to_value(Utc::now().timestamp() + 10000).unwrap());
let res = validate(&claims, &Validation::default());
assert!(res.is_ok());
}
#[test]
fn exp_in_past_fails() {
let mut claims = Map::new();
claims.insert("exp".to_string(), to_value(Utc::now().timestamp() - 100000).unwrap());
let res = validate(&claims, &Validation::default());
assert!(res.is_err());
match res.unwrap_err().kind() {
&ErrorKind::ExpiredSignature => (),
_ => assert!(false),
};
}
#[test]
fn exp_in_past_but_in_leeway_ok() {
let mut claims = Map::new();
claims.insert("exp".to_string(), to_value(Utc::now().timestamp() - 500).unwrap());
let validation = Validation {
leeway: 1000 * 60,
..Default::default()
};
let res = validate(&claims, &validation);
assert!(res.is_ok());
}
#[test]
fn nbf_in_past_ok() {
let mut claims = Map::new();
claims.insert("nbf".to_string(), to_value(Utc::now().timestamp() - 10000).unwrap());
let res = validate(&claims, &Validation::default());
assert!(res.is_ok());
}
#[test]
fn nbf_in_future_fails() {
let mut claims = Map::new();
claims.insert("nbf".to_string(), to_value(Utc::now().timestamp() + 100000).unwrap());
let res = validate(&claims, &Validation::default());
assert!(res.is_err());
match res.unwrap_err().kind() {
&ErrorKind::ImmatureSignature => (),
_ => assert!(false),
};
}
#[test]
fn nbf_in_future_but_in_leeway_ok() {
let mut claims = Map::new();
claims.insert("nbf".to_string(), to_value(Utc::now().timestamp() + 500).unwrap());
let validation = Validation {
leeway: 1000 * 60,
..Default::default()
};
let res = validate(&claims, &validation);
assert!(res.is_ok());
}
#[test]
fn iss_ok() {
let mut claims = Map::new();
claims.insert("iss".to_string(), to_value("Keats").unwrap());
let validation = Validation {
iss: Some("Keats".to_string()),
..Default::default()
};
let res = validate(&claims, &validation);
assert!(res.is_ok());
}
#[test]
fn iss_not_matching_fails() {
let mut claims = Map::new();
claims.insert("iss".to_string(), to_value("Hacked").unwrap());
let validation = Validation {
iss: Some("Keats".to_string()),
..Default::default()
};
let res = validate(&claims, &validation);
assert!(res.is_err());
match res.unwrap_err().kind() {
&ErrorKind::InvalidIssuer => (),
_ => assert!(false),
};
}
#[test]
fn sub_ok() {
let mut claims = Map::new();
claims.insert("sub".to_string(), to_value("Keats").unwrap());
let validation = Validation {
sub: Some("Keats".to_string()),
..Default::default()
};
let res = validate(&claims, &validation);
assert!(res.is_ok());
}
#[test]
fn sub_not_matching_fails() {
let mut claims = Map::new();
claims.insert("sub".to_string(), to_value("Hacked").unwrap());
let validation = Validation {
sub: Some("Keats".to_string()),
..Default::default()
};
let res = validate(&claims, &validation);
assert!(res.is_err());
match res.unwrap_err().kind() {
&ErrorKind::InvalidSubject => (),
_ => assert!(false),
};
}
#[test]
fn aud_string_ok() {
let mut claims = Map::new();
claims.insert("aud".to_string(), to_value("Everyone").unwrap());
let mut validation = Validation::default();
validation.set_audience(&"Everyone");
let res = validate(&claims, &validation);
assert!(res.is_ok());
}
#[test]
fn aud_array_of_string_ok() {
let mut claims = Map::new();
claims.insert("aud".to_string(), to_value(["UserA", "UserB"]).unwrap());
let mut validation = Validation::default();
validation.set_audience(&["UserA", "UserB"]);
let res = validate(&claims, &validation);
assert!(res.is_ok());
}
#[test]
fn aud_type_mismatch_fails() {
let mut claims = Map::new();
claims.insert("aud".to_string(), to_value("Everyone").unwrap());
let mut validation = Validation::default();
validation.set_audience(&["UserA", "UserB"]);
let res = validate(&claims, &validation);
assert!(res.is_err());
match res.unwrap_err().kind() {
&ErrorKind::InvalidAudience => (),
_ => assert!(false),
};
}
#[test]
fn aud_correct_type_not_matching_fails() {
let mut claims = Map::new();
claims.insert("aud".to_string(), to_value("Everyone").unwrap());
let mut validation = Validation::default();
validation.set_audience(&"None");
let res = validate(&claims, &validation);
assert!(res.is_err());
match res.unwrap_err().kind() {
&ErrorKind::InvalidAudience => (),
_ => assert!(false),
};
}
}

View File

@@ -0,0 +1,3 @@
ALTER TABLE attachments
ADD COLUMN
key TEXT;

View File

@@ -1 +1 @@
nightly-2018-10-03 nightly-2018-12-01

View File

@@ -1,13 +1,37 @@
use rocket_contrib::Json; use rocket_contrib::json::Json;
use db::DbConn; use crate::db::models::*;
use db::models::*; use crate::db::DbConn;
use api::{PasswordData, JsonResult, EmptyResult, JsonUpcase, NumberOrString}; use crate::api::{EmptyResult, JsonResult, JsonUpcase, NumberOrString, PasswordData, UpdateType, WebSocketUsers};
use auth::Headers; use crate::auth::Headers;
use mail; use crate::mail;
use CONFIG; use crate::CONFIG;
use rocket::{Route, State};
pub fn routes() -> Vec<Route> {
routes![
register,
profile,
put_profile,
post_profile,
get_public_keys,
post_keys,
post_password,
post_kdf,
post_rotatekey,
post_sstamp,
post_email_token,
post_email,
delete_account,
post_delete_account,
revision_date,
password_hint,
prelogin,
]
}
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
#[allow(non_snake_case)] #[allow(non_snake_case)]
@@ -33,23 +57,22 @@ struct KeysData {
fn register(data: JsonUpcase<RegisterData>, conn: DbConn) -> EmptyResult { fn register(data: JsonUpcase<RegisterData>, conn: DbConn) -> EmptyResult {
let data: RegisterData = data.into_inner().data; let data: RegisterData = data.into_inner().data;
let mut user = match User::find_by_mail(&data.Email, &conn) { let mut user = match User::find_by_mail(&data.Email, &conn) {
Some(mut user) => { Some(user) => {
if Invitation::take(&data.Email, &conn) { if Invitation::take(&data.Email, &conn) {
for mut user_org in UserOrganization::find_invited_by_user(&user.uuid, &conn).iter_mut() { for mut user_org in UserOrganization::find_invited_by_user(&user.uuid, &conn).iter_mut() {
user_org.status = UserOrgStatus::Accepted as i32; user_org.status = UserOrgStatus::Accepted as i32;
if user_org.save(&conn).is_err() { if user_org.save(&conn).is_err() {
err!("Failed to accept user to organization") err!("Failed to accept user to organization")
} }
}; }
user user
} else if CONFIG.signups_allowed { } else if CONFIG.signups_allowed {
err!("Account with this email already exists") err!("Account with this email already exists")
} else { } else {
err!("Registration not allowed") err!("Registration not allowed")
} }
}, }
None => { None => {
if CONFIG.signups_allowed || Invitation::take(&data.Email, &conn) { if CONFIG.signups_allowed || Invitation::take(&data.Email, &conn) {
User::new(data.Email) User::new(data.Email)
@@ -86,7 +109,7 @@ fn register(data: JsonUpcase<RegisterData>, conn: DbConn) -> EmptyResult {
match user.save(&conn) { match user.save(&conn) {
Ok(()) => Ok(()), Ok(()) => Ok(()),
Err(_) => err!("Failed to save user") Err(_) => err!("Failed to save user"),
} }
} }
@@ -99,7 +122,7 @@ fn profile(headers: Headers, conn: DbConn) -> JsonResult {
#[allow(non_snake_case)] #[allow(non_snake_case)]
struct ProfileData { struct ProfileData {
#[serde(rename = "Culture")] #[serde(rename = "Culture")]
_Culture: String, // Ignored, always use en-US _Culture: String, // Ignored, always use en-US
MasterPasswordHint: Option<String>, MasterPasswordHint: Option<String>,
Name: String, Name: String,
} }
@@ -122,7 +145,7 @@ fn post_profile(data: JsonUpcase<ProfileData>, headers: Headers, conn: DbConn) -
}; };
match user.save(&conn) { match user.save(&conn) {
Ok(()) => Ok(Json(user.to_json(&conn))), Ok(()) => Ok(Json(user.to_json(&conn))),
Err(_) => err!("Failed to save user profile") Err(_) => err!("Failed to save user profile"),
} }
} }
@@ -130,7 +153,7 @@ fn post_profile(data: JsonUpcase<ProfileData>, headers: Headers, conn: DbConn) -
fn get_public_keys(uuid: String, _headers: Headers, conn: DbConn) -> JsonResult { fn get_public_keys(uuid: String, _headers: Headers, conn: DbConn) -> JsonResult {
let user = match User::find_by_uuid(&uuid, &conn) { let user = match User::find_by_uuid(&uuid, &conn) {
Some(user) => user, Some(user) => user,
None => err!("User doesn't exist") None => err!("User doesn't exist"),
}; };
Ok(Json(json!({ Ok(Json(json!({
@@ -151,12 +174,10 @@ fn post_keys(data: JsonUpcase<KeysData>, headers: Headers, conn: DbConn) -> Json
match user.save(&conn) { match user.save(&conn) {
Ok(()) => Ok(Json(user.to_json(&conn))), Ok(()) => Ok(Json(user.to_json(&conn))),
Err(_) => err!("Failed to save the user's keys") Err(_) => err!("Failed to save the user's keys"),
} }
} }
#[derive(Deserialize)] #[derive(Deserialize)]
#[allow(non_snake_case)] #[allow(non_snake_case)]
struct ChangePassData { struct ChangePassData {
@@ -178,7 +199,7 @@ fn post_password(data: JsonUpcase<ChangePassData>, headers: Headers, conn: DbCon
user.key = data.Key; user.key = data.Key;
match user.save(&conn) { match user.save(&conn) {
Ok(()) => Ok(()), Ok(()) => Ok(()),
Err(_) => err!("Failed to save password") Err(_) => err!("Failed to save password"),
} }
} }
@@ -208,10 +229,86 @@ fn post_kdf(data: JsonUpcase<ChangeKdfData>, headers: Headers, conn: DbConn) ->
user.key = data.Key; user.key = data.Key;
match user.save(&conn) { match user.save(&conn) {
Ok(()) => Ok(()), Ok(()) => Ok(()),
Err(_) => err!("Failed to save password settings") Err(_) => err!("Failed to save password settings"),
} }
} }
#[derive(Deserialize)]
#[allow(non_snake_case)]
struct UpdateFolderData {
Id: String,
Name: String,
}
use super::ciphers::CipherData;
#[derive(Deserialize)]
#[allow(non_snake_case)]
struct KeyData {
Ciphers: Vec<CipherData>,
Folders: Vec<UpdateFolderData>,
Key: String,
PrivateKey: String,
MasterPasswordHash: String,
}
#[post("/accounts/key", data = "<data>")]
fn post_rotatekey(data: JsonUpcase<KeyData>, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> EmptyResult {
let data: KeyData = data.into_inner().data;
if !headers.user.check_valid_password(&data.MasterPasswordHash) {
err!("Invalid password")
}
let user_uuid = &headers.user.uuid;
// Update folder data
for folder_data in data.Folders {
let mut saved_folder = match Folder::find_by_uuid(&folder_data.Id, &conn) {
Some(folder) => folder,
None => err!("Folder doesn't exist"),
};
if &saved_folder.user_uuid != user_uuid {
err!("The folder is not owned by the user")
}
saved_folder.name = folder_data.Name;
if saved_folder.save(&conn).is_err() {
err!("Failed to save folder")
}
}
// Update cipher data
use super::ciphers::update_cipher_from_data;
for cipher_data in data.Ciphers {
let mut saved_cipher = match Cipher::find_by_uuid(cipher_data.Id.as_ref().unwrap(), &conn) {
Some(cipher) => cipher,
None => err!("Cipher doesn't exist"),
};
if saved_cipher.user_uuid.as_ref().unwrap() != user_uuid {
err!("The cipher is not owned by the user")
}
update_cipher_from_data(&mut saved_cipher, cipher_data, &headers, false, &conn, &ws, UpdateType::SyncCipherUpdate)?
}
// Update user data
let mut user = headers.user;
user.key = data.Key;
user.private_key = Some(data.PrivateKey);
user.reset_security_stamp();
if user.save(&conn).is_err() {
err!("Failed modify user key");
}
Ok(())
}
#[post("/accounts/security-stamp", data = "<data>")] #[post("/accounts/security-stamp", data = "<data>")]
fn post_sstamp(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> EmptyResult { fn post_sstamp(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> EmptyResult {
let data: PasswordData = data.into_inner().data; let data: PasswordData = data.into_inner().data;
@@ -224,7 +321,7 @@ fn post_sstamp(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -
user.reset_security_stamp(); user.reset_security_stamp();
match user.save(&conn) { match user.save(&conn) {
Ok(()) => Ok(()), Ok(()) => Ok(()),
Err(_) => err!("Failed to reset security stamp") Err(_) => err!("Failed to reset security stamp"),
} }
} }
@@ -282,7 +379,7 @@ fn post_email(data: JsonUpcase<ChangeEmailData>, headers: Headers, conn: DbConn)
match user.save(&conn) { match user.save(&conn) {
Ok(()) => Ok(()), Ok(()) => Ok(()),
Err(_) => err!("Failed to save email address") Err(_) => err!("Failed to save email address"),
} }
} }
@@ -302,7 +399,7 @@ fn delete_account(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn
match user.delete(&conn) { match user.delete(&conn) {
Ok(()) => Ok(()), Ok(()) => Ok(()),
Err(_) => err!("Failed deleting user account, are you the only owner of some organization?") Err(_) => err!("Failed deleting user account, are you the only owner of some organization?"),
} }
} }

View File

@@ -1,35 +1,76 @@
use std::collections::{HashSet, HashMap};
use std::path::Path; use std::path::Path;
use std::collections::HashSet;
use rocket::State;
use rocket::Data;
use rocket::http::ContentType; use rocket::http::ContentType;
use rocket::{request::Form, Data, Route, State};
use rocket_contrib::{Json, Value}; use rocket_contrib::json::Json;
use serde_json::Value;
use multipart::server::{Multipart, SaveResult};
use multipart::server::save::SavedData; use multipart::server::save::SavedData;
use multipart::server::{Multipart, SaveResult};
use data_encoding::HEXLOWER; use data_encoding::HEXLOWER;
use db::DbConn; use crate::db::models::*;
use db::models::*; use crate::db::DbConn;
use crypto; use crate::crypto;
use api::{self, PasswordData, JsonResult, EmptyResult, JsonUpcase, WebSocketUsers, UpdateType}; use crate::api::{self, EmptyResult, JsonResult, JsonUpcase, PasswordData, UpdateType, WebSocketUsers};
use auth::Headers; use crate::auth::Headers;
use CONFIG; use crate::CONFIG;
#[derive(FromForm)] pub fn routes() -> Vec<Route> {
#[allow(non_snake_case)] routes![
struct SyncData { sync,
excludeDomains: bool, get_ciphers,
get_cipher,
get_cipher_admin,
get_cipher_details,
post_ciphers,
put_cipher_admin,
post_ciphers_admin,
post_ciphers_create,
post_ciphers_import,
post_attachment,
post_attachment_admin,
post_attachment_share,
delete_attachment_post,
delete_attachment_post_admin,
delete_attachment,
delete_attachment_admin,
post_cipher_admin,
post_cipher_share,
put_cipher_share,
put_cipher_share_seleted,
post_cipher,
put_cipher,
delete_cipher_post,
delete_cipher_post_admin,
delete_cipher,
delete_cipher_admin,
delete_cipher_selected,
delete_cipher_selected_post,
delete_all,
move_cipher_selected,
move_cipher_selected_put,
post_collections_update,
post_collections_admin,
put_collections_admin,
]
} }
#[get("/sync?<data>")] #[derive(FromForm, Default)]
fn sync(data: SyncData, headers: Headers, conn: DbConn) -> JsonResult { struct SyncData {
#[form(field = "excludeDomains")]
exclude_domains: bool, // Default: 'false'
}
#[get("/sync?<data..>")]
fn sync(data: Form<SyncData>, headers: Headers, conn: DbConn) -> JsonResult {
let user_json = headers.user.to_json(&conn); let user_json = headers.user.to_json(&conn);
let folders = Folder::find_by_user(&headers.user.uuid, &conn); let folders = Folder::find_by_user(&headers.user.uuid, &conn);
@@ -41,7 +82,7 @@ fn sync(data: SyncData, headers: Headers, conn: DbConn) -> JsonResult {
let ciphers = Cipher::find_by_user(&headers.user.uuid, &conn); let ciphers = Cipher::find_by_user(&headers.user.uuid, &conn);
let ciphers_json: Vec<Value> = ciphers.iter().map(|c| c.to_json(&headers.host, &headers.user.uuid, &conn)).collect(); let ciphers_json: Vec<Value> = ciphers.iter().map(|c| c.to_json(&headers.host, &headers.user.uuid, &conn)).collect();
let domains_json = if data.excludeDomains { Value::Null } else { api::core::get_eq_domains(headers).unwrap().into_inner() }; let domains_json = if data.exclude_domains { Value::Null } else { api::core::get_eq_domains(headers).unwrap().into_inner() };
Ok(Json(json!({ Ok(Json(json!({
"Profile": user_json, "Profile": user_json,
@@ -53,14 +94,6 @@ fn sync(data: SyncData, headers: Headers, conn: DbConn) -> JsonResult {
}))) })))
} }
#[get("/sync")]
fn sync_no_query(headers: Headers, conn: DbConn) -> JsonResult {
let sync_data = SyncData {
excludeDomains: false,
};
sync(sync_data, headers, conn)
}
#[get("/ciphers")] #[get("/ciphers")]
fn get_ciphers(headers: Headers, conn: DbConn) -> JsonResult { fn get_ciphers(headers: Headers, conn: DbConn) -> JsonResult {
let ciphers = Cipher::find_by_user(&headers.user.uuid, &conn); let ciphers = Cipher::find_by_user(&headers.user.uuid, &conn);
@@ -103,7 +136,7 @@ fn get_cipher_details(uuid: String, headers: Headers, conn: DbConn) -> JsonResul
#[allow(non_snake_case)] #[allow(non_snake_case)]
pub struct CipherData { pub struct CipherData {
// Id is optional as it is included only in bulk share // Id is optional as it is included only in bulk share
Id: Option<String>, pub Id: Option<String>,
// Folder id is not included in import // Folder id is not included in import
FolderId: Option<String>, FolderId: Option<String>,
// TODO: Some of these might appear all the time, no need for Option // TODO: Some of these might appear all the time, no need for Option
@@ -129,12 +162,37 @@ pub struct CipherData {
Favorite: Option<bool>, Favorite: Option<bool>,
PasswordHistory: Option<Value>, PasswordHistory: Option<Value>,
// These are used during key rotation
#[serde(rename = "Attachments")]
_Attachments: Option<Value>, // Unused, contains map of {id: filename}
Attachments2: Option<HashMap<String, Attachments2Data>>
}
#[derive(Deserialize, Debug)]
#[allow(non_snake_case)]
pub struct Attachments2Data {
FileName: String,
Key: String,
} }
#[post("/ciphers/admin", data = "<data>")] #[post("/ciphers/admin", data = "<data>")]
fn post_ciphers_admin(data: JsonUpcase<CipherData>, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> JsonResult { fn post_ciphers_admin(data: JsonUpcase<ShareCipherData>, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> JsonResult {
// TODO: Implement this correctly let data: ShareCipherData = data.into_inner().data;
post_ciphers(data, headers, conn, ws)
let mut cipher = Cipher::new(data.Cipher.Type, data.Cipher.Name.clone());
cipher.user_uuid = Some(headers.user.uuid.clone());
match cipher.save(&conn) {
Ok(()) => (),
Err(_) => err!("Failed saving cipher")
};
share_cipher_by_uuid(&cipher.uuid, data, &headers, &conn, &ws)
}
#[post("/ciphers/create", data = "<data>")]
fn post_ciphers_create(data: JsonUpcase<ShareCipherData>, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> JsonResult {
post_ciphers_admin(data, headers, conn, ws)
} }
#[post("/ciphers", data = "<data>")] #[post("/ciphers", data = "<data>")]
@@ -175,6 +233,28 @@ pub fn update_cipher_from_data(cipher: &mut Cipher, data: CipherData, headers: &
} }
} }
// Modify attachments name and keys when rotating
if let Some(attachments) = data.Attachments2 {
for (id, attachment) in attachments {
let mut saved_att = match Attachment::find_by_id(&id, &conn) {
Some(att) => att,
None => err!("Attachment doesn't exist")
};
if saved_att.cipher_uuid != cipher.uuid {
err!("Attachment is not owned by the cipher")
}
saved_att.key = Some(attachment.Key);
saved_att.file_name = attachment.FileName;
match saved_att.save(&conn) {
Ok(()) => (),
Err(_) => err!("Failed to save attachment")
};
}
}
let type_data_opt = match data.Type { let type_data_opt = match data.Type {
1 => data.Login, 1 => data.Login,
2 => data.SecureNote, 2 => data.SecureNote,
@@ -206,7 +286,7 @@ pub fn update_cipher_from_data(cipher: &mut Cipher, data: CipherData, headers: &
match cipher.save(&conn) { match cipher.save(&conn) {
Ok(()) => (), Ok(()) => (),
Err(_) => println!("Error: Failed to save cipher") Err(_) => err!("Failed to save cipher")
}; };
ws.send_cipher_update(ut, &cipher, &cipher.update_users_revision(&conn)); ws.send_cipher_update(ut, &cipher, &cipher.update_users_revision(&conn));
@@ -253,7 +333,6 @@ fn post_ciphers_import(data: JsonUpcase<ImportData>, headers: Headers, conn: DbC
} }
// Read the relations between folders and ciphers // Read the relations between folders and ciphers
use std::collections::HashMap;
let mut relations_map = HashMap::new(); let mut relations_map = HashMap::new();
for relation in data.FolderRelationships { for relation in data.FolderRelationships {
@@ -496,37 +575,52 @@ fn post_attachment(uuid: String, data: Data, content_type: &ContentType, headers
let base_path = Path::new(&CONFIG.attachments_folder).join(&cipher.uuid); let base_path = Path::new(&CONFIG.attachments_folder).join(&cipher.uuid);
let mut attachment_key = None;
Multipart::with_body(data.open(), boundary).foreach_entry(|mut field| { Multipart::with_body(data.open(), boundary).foreach_entry(|mut field| {
// This is provided by the client, don't trust it match field.headers.name.as_str() {
let name = field.headers.filename.expect("No filename provided"); "key" => {
use std::io::Read;
let file_name = HEXLOWER.encode(&crypto::get_random(vec![0; 10])); let mut key_buffer = String::new();
let path = base_path.join(&file_name); if field.data.read_to_string(&mut key_buffer).is_ok() {
attachment_key = Some(key_buffer);
let size = match field.data.save() }
.memory_threshold(0)
.size_limit(None)
.with_path(path) {
SaveResult::Full(SavedData::File(_, size)) => size as i32,
SaveResult::Full(other) => {
println!("Attachment is not a file: {:?}", other);
return;
}, },
SaveResult::Partial(_, reason) => { "data" => {
println!("Partial result: {:?}", reason); // This is provided by the client, don't trust it
return; let name = field.headers.filename.expect("No filename provided");
},
SaveResult::Error(e) => {
println!("Error: {:?}", e);
return;
}
};
let attachment = Attachment::new(file_name, cipher.uuid.clone(), name, size); let file_name = HEXLOWER.encode(&crypto::get_random(vec![0; 10]));
match attachment.save(&conn) { let path = base_path.join(&file_name);
Ok(()) => (),
Err(_) => println!("Error: failed to save attachment") let size = match field.data.save()
}; .memory_threshold(0)
.size_limit(None)
.with_path(path) {
SaveResult::Full(SavedData::File(_, size)) => size as i32,
SaveResult::Full(other) => {
error!("Attachment is not a file: {:?}", other);
return;
},
SaveResult::Partial(_, reason) => {
error!("Partial result: {:?}", reason);
return;
},
SaveResult::Error(e) => {
error!("Error: {:?}", e);
return;
}
};
let mut attachment = Attachment::new(file_name, cipher.uuid.clone(), name, size);
attachment.key = attachment_key.clone();
match attachment.save(&conn) {
Ok(()) => (),
Err(_) => error!("Failed to save attachment")
};
},
_ => error!("Invalid multipart name")
}
}).expect("Error processing multipart data"); }).expect("Error processing multipart data");
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, &conn))) Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, &conn)))
@@ -657,7 +751,7 @@ fn move_cipher_selected(data: JsonUpcase<Value>, headers: Headers, conn: DbConn,
} }
match cipher.save(&conn) { match cipher.save(&conn) {
Ok(()) => (), Ok(()) => (),
Err(_) => println!("Error: Failed to save cipher") Err(_) => err!("Failed to save cipher")
}; };
ws.send_cipher_update(UpdateType::SyncCipherUpdate, &cipher, &cipher.update_users_revision(&conn)); ws.send_cipher_update(UpdateType::SyncCipherUpdate, &cipher, &cipher.update_users_revision(&conn));
} }
@@ -695,8 +789,7 @@ fn delete_all(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn, ws
for f in Folder::find_by_user(&user.uuid, &conn) { for f in Folder::find_by_user(&user.uuid, &conn) {
if f.delete(&conn).is_err() { if f.delete(&conn).is_err() {
err!("Failed deleting folder") err!("Failed deleting folder")
} } else {
else {
ws.send_folder_update(UpdateType::SyncFolderCreate, &f); ws.send_folder_update(UpdateType::SyncFolderCreate, &f);
} }
} }
@@ -748,6 +841,6 @@ fn _delete_cipher_attachment_by_id(uuid: &str, attachment_id: &str, headers: &He
ws.send_cipher_update(UpdateType::SyncCipherDelete, &cipher, &cipher.update_users_revision(&conn)); ws.send_cipher_update(UpdateType::SyncCipherDelete, &cipher, &cipher.update_users_revision(&conn));
Ok(()) Ok(())
} }
Err(_) => err!("Deleting attachement failed") Err(_) => err!("Deleting attachment failed")
} }
} }

View File

@@ -1,11 +1,26 @@
use rocket::State; use rocket::State;
use rocket_contrib::{Json, Value}; use rocket_contrib::json::Json;
use serde_json::Value;
use db::DbConn; use crate::db::DbConn;
use db::models::*; use crate::db::models::*;
use api::{JsonResult, EmptyResult, JsonUpcase, WebSocketUsers, UpdateType}; use crate::api::{JsonResult, EmptyResult, JsonUpcase, WebSocketUsers, UpdateType};
use auth::Headers; use crate::auth::Headers;
use rocket::Route;
pub fn routes() -> Vec<Route> {
routes![
get_folders,
get_folder,
post_folders,
post_folder,
put_folder,
delete_folder_post,
delete_folder,
]
}
#[get("/folders")] #[get("/folders")]
fn get_folders(headers: Headers, conn: DbConn) -> JsonResult { fn get_folders(headers: Headers, conn: DbConn) -> JsonResult {

View File

@@ -4,178 +4,69 @@ mod folders;
mod organizations; mod organizations;
pub(crate) mod two_factor; pub(crate) mod two_factor;
use self::accounts::*;
use self::ciphers::*;
use self::folders::*;
use self::organizations::*;
use self::two_factor::*;
pub fn routes() -> Vec<Route> { pub fn routes() -> Vec<Route> {
routes![ let mut mod_routes = routes![
register,
profile,
put_profile,
post_profile,
get_public_keys,
post_keys,
post_password,
post_kdf,
post_sstamp,
post_email_token,
post_email,
delete_account,
post_delete_account,
revision_date,
password_hint,
prelogin,
sync,
sync_no_query,
get_ciphers,
get_cipher,
get_cipher_admin,
get_cipher_details,
post_ciphers,
put_cipher_admin,
post_ciphers_admin,
post_ciphers_import,
post_attachment,
post_attachment_admin,
post_attachment_share,
delete_attachment_post,
delete_attachment_post_admin,
delete_attachment,
delete_attachment_admin,
post_cipher_admin,
post_cipher_share,
put_cipher_share,
put_cipher_share_seleted,
post_cipher,
put_cipher,
delete_cipher_post,
delete_cipher_post_admin,
delete_cipher,
delete_cipher_admin,
delete_cipher_selected,
delete_cipher_selected_post,
delete_all,
move_cipher_selected,
move_cipher_selected_put,
get_folders,
get_folder,
post_folders,
post_folder,
put_folder,
delete_folder_post,
delete_folder,
get_twofactor,
get_recover,
recover,
disable_twofactor,
disable_twofactor_put,
generate_authenticator,
activate_authenticator,
activate_authenticator_put,
generate_u2f,
generate_u2f_challenge,
activate_u2f,
activate_u2f_put,
get_organization,
create_organization,
delete_organization,
post_delete_organization,
leave_organization,
get_user_collections,
get_org_collections,
get_org_collection_detail,
get_collection_users,
put_organization,
post_organization,
post_organization_collections,
delete_organization_collection_user,
post_organization_collection_delete_user,
post_organization_collection_update,
put_organization_collection_update,
delete_organization_collection,
post_organization_collection_delete,
post_collections_update,
post_collections_admin,
put_collections_admin,
get_org_details,
get_org_users,
send_invite,
confirm_invite,
get_user,
edit_user,
put_organization_user,
delete_user,
post_delete_user,
post_reinvite_user,
post_org_import,
clear_device_token, clear_device_token,
put_device_token, put_device_token,
get_eq_domains, get_eq_domains,
post_eq_domains, post_eq_domains,
put_eq_domains, put_eq_domains,
];
] let mut routes = Vec::new();
routes.append(&mut accounts::routes());
routes.append(&mut ciphers::routes());
routes.append(&mut folders::routes());
routes.append(&mut organizations::routes());
routes.append(&mut two_factor::routes());
routes.append(&mut mod_routes);
routes
} }
/// ///
/// Move this somewhere else /// Move this somewhere else
/// ///
use rocket::Route; use rocket::Route;
use rocket_contrib::{Json, Value}; use rocket_contrib::json::Json;
use serde_json::Value;
use db::DbConn; use crate::db::DbConn;
use db::models::*;
use api::{JsonResult, EmptyResult, JsonUpcase}; use crate::api::{EmptyResult, JsonResult, JsonUpcase};
use auth::Headers; use crate::auth::Headers;
#[put("/devices/identifier/<uuid>/clear-token", data = "<data>")] #[put("/devices/identifier/<uuid>/clear-token")]
fn clear_device_token(uuid: String, data: Json<Value>, headers: Headers, conn: DbConn) -> EmptyResult { fn clear_device_token(uuid: String) -> EmptyResult {
let _data: Value = data.into_inner(); // This endpoint doesn't have auth header
let device = match Device::find_by_uuid(&uuid, &conn) { let _ = uuid;
Some(device) => device, // uuid is not related to deviceId
None => err!("Device not found")
};
if device.user_uuid != headers.user.uuid { // This only clears push token
err!("Device not owned by user") // https://github.com/bitwarden/core/blob/master/src/Api/Controllers/DevicesController.cs#L109
} // https://github.com/bitwarden/core/blob/master/src/Core/Services/Implementations/DeviceService.cs#L37
Ok(())
match device.delete(&conn) {
Ok(()) => Ok(()),
Err(_) => err!("Failed deleting device")
}
} }
#[put("/devices/identifier/<uuid>/token", data = "<data>")] #[put("/devices/identifier/<uuid>/token", data = "<data>")]
fn put_device_token(uuid: String, data: Json<Value>, headers: Headers, conn: DbConn) -> JsonResult { fn put_device_token(uuid: String, data: JsonUpcase<Value>, headers: Headers) -> JsonResult {
let _data: Value = data.into_inner(); let _data: Value = data.into_inner().data;
// Data has a single string value "PushToken"
let _ = uuid;
// uuid is not related to deviceId
let device = match Device::find_by_uuid(&uuid, &conn) { // TODO: This should save the push token, but we don't have push functionality
Some(device) => device,
None => err!("Device not found")
};
if device.user_uuid != headers.user.uuid { Ok(Json(json!({
err!("Device not owned by user") "Id": headers.device.uuid,
} "Name": headers.device.name,
"Type": headers.device.type_,
// TODO: What does this do? "Identifier": headers.device.uuid,
"CreationDate": crate::util::format_date(&headers.device.created_at),
err!("Not implemented") })))
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
@@ -209,7 +100,6 @@ fn get_eq_domains(headers: Headers) -> JsonResult {
}))) })))
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
#[allow(non_snake_case)] #[allow(non_snake_case)]
struct EquivDomainData { struct EquivDomainData {
@@ -232,9 +122,8 @@ fn post_eq_domains(data: JsonUpcase<EquivDomainData>, headers: Headers, conn: Db
match user.save(&conn) { match user.save(&conn) {
Ok(()) => Ok(Json(json!({}))), Ok(()) => Ok(Json(json!({}))),
Err(_) => err!("Failed to save user") Err(_) => err!("Failed to save user"),
} }
} }
#[put("/settings/domains", data = "<data>")] #[put("/settings/domains", data = "<data>")]

View File

@@ -1,14 +1,53 @@
use rocket::State; use rocket::State;
use rocket_contrib::{Json, Value}; use rocket::request::Form;
use CONFIG; use rocket_contrib::json::Json;
use db::DbConn; use serde_json::Value;
use db::models::*;
use api::{PasswordData, JsonResult, EmptyResult, NumberOrString, JsonUpcase, WebSocketUsers, UpdateType}; use crate::CONFIG;
use auth::{Headers, AdminHeaders, OwnerHeaders}; use crate::db::DbConn;
use crate::db::models::*;
use crate::api::{PasswordData, JsonResult, EmptyResult, NumberOrString, JsonUpcase, WebSocketUsers, UpdateType};
use crate::auth::{Headers, AdminHeaders, OwnerHeaders};
use serde::{Deserialize, Deserializer}; use serde::{Deserialize, Deserializer};
use rocket::Route;
pub fn routes() -> Vec<Route> {
routes![
get_organization,
create_organization,
delete_organization,
post_delete_organization,
leave_organization,
get_user_collections,
get_org_collections,
get_org_collection_detail,
get_collection_users,
put_organization,
post_organization,
post_organization_collections,
delete_organization_collection_user,
post_organization_collection_delete_user,
post_organization_collection_update,
put_organization_collection_update,
delete_organization_collection,
post_organization_collection_delete,
get_org_details,
get_org_users,
send_invite,
confirm_invite,
get_user,
edit_user,
put_organization_user,
delete_user,
post_delete_user,
post_reinvite_user,
post_org_import,
]
}
#[derive(Deserialize)] #[derive(Deserialize)]
#[allow(non_snake_case)] #[allow(non_snake_case)]
@@ -91,7 +130,7 @@ fn leave_organization(org_id: String, headers: Headers, conn: DbConn) -> EmptyRe
match UserOrganization::find_by_user_and_org(&headers.user.uuid, &org_id, &conn) { match UserOrganization::find_by_user_and_org(&headers.user.uuid, &org_id, &conn) {
None => err!("User not part of organization"), None => err!("User not part of organization"),
Some(user_org) => { Some(user_org) => {
if user_org.type_ == UserOrgType::Owner as i32 { if user_org.type_ == UserOrgType::Owner {
let num_owners = UserOrganization::find_by_org_and_type( let num_owners = UserOrganization::find_by_org_and_type(
&org_id, UserOrgType::Owner as i32, &conn) &org_id, UserOrgType::Owner as i32, &conn)
.len(); .len();
@@ -315,14 +354,14 @@ fn get_collection_users(org_id: String, coll_id: String, _headers: AdminHeaders,
} }
#[derive(FromForm)] #[derive(FromForm)]
#[allow(non_snake_case)]
struct OrgIdData { struct OrgIdData {
organizationId: String #[form(field = "organizationId")]
organization_id: String
} }
#[get("/ciphers/organization-details?<data>")] #[get("/ciphers/organization-details?<data..>")]
fn get_org_details(data: OrgIdData, headers: Headers, conn: DbConn) -> JsonResult { fn get_org_details(data: Form<OrgIdData>, headers: Headers, conn: DbConn) -> JsonResult {
let ciphers = Cipher::find_by_org(&data.organizationId, &conn); let ciphers = Cipher::find_by_org(&data.organization_id, &conn);
let ciphers_json: Vec<Value> = ciphers.iter().map(|c| c.to_json(&headers.host, &headers.user.uuid, &conn)).collect(); let ciphers_json: Vec<Value> = ciphers.iter().map(|c| c.to_json(&headers.host, &headers.user.uuid, &conn)).collect();
Ok(Json(json!({ Ok(Json(json!({
@@ -378,9 +417,9 @@ fn send_invite(org_id: String, data: JsonUpcase<InviteData>, headers: AdminHeade
None => err!("Invalid type") None => err!("Invalid type")
}; };
if new_type != UserOrgType::User as i32 && if new_type != UserOrgType::User &&
headers.org_user_type != UserOrgType::Owner as i32 { headers.org_user_type != UserOrgType::Owner {
err!("Only Owners can invite Admins or Owners") err!("Only Owners can invite Managers, Admins or Owners")
} }
for email in data.Emails.iter() { for email in data.Emails.iter() {
@@ -452,9 +491,9 @@ fn confirm_invite(org_id: String, org_user_id: String, data: JsonUpcase<Value>,
None => err!("The specified user isn't a member of the organization") None => err!("The specified user isn't a member of the organization")
}; };
if user_to_confirm.type_ != UserOrgType::User as i32 && if user_to_confirm.type_ != UserOrgType::User &&
headers.org_user_type != UserOrgType::Owner as i32 { headers.org_user_type != UserOrgType::Owner {
err!("Only Owners can confirm Admins or Owners") err!("Only Owners can confirm Managers, Admins or Owners")
} }
if user_to_confirm.status != UserOrgStatus::Accepted as i32 { if user_to_confirm.status != UserOrgStatus::Accepted as i32 {
@@ -502,7 +541,7 @@ fn edit_user(org_id: String, org_user_id: String, data: JsonUpcase<EditUserData>
let data: EditUserData = data.into_inner().data; let data: EditUserData = data.into_inner().data;
let new_type = match UserOrgType::from_str(&data.Type.into_string()) { let new_type = match UserOrgType::from_str(&data.Type.into_string()) {
Some(new_type) => new_type as i32, Some(new_type) => new_type,
None => err!("Invalid type") None => err!("Invalid type")
}; };
@@ -511,21 +550,21 @@ fn edit_user(org_id: String, org_user_id: String, data: JsonUpcase<EditUserData>
None => err!("The specified user isn't member of the organization") None => err!("The specified user isn't member of the organization")
}; };
if new_type != user_to_edit.type_ as i32 && ( if new_type != user_to_edit.type_ && (
user_to_edit.type_ <= UserOrgType::Admin as i32 || user_to_edit.type_ >= UserOrgType::Admin ||
new_type <= UserOrgType::Admin as i32 new_type >= UserOrgType::Admin
) && ) &&
headers.org_user_type != UserOrgType::Owner as i32 { headers.org_user_type != UserOrgType::Owner {
err!("Only Owners can grant and remove Admin or Owner privileges") err!("Only Owners can grant and remove Admin or Owner privileges")
} }
if user_to_edit.type_ == UserOrgType::Owner as i32 && if user_to_edit.type_ == UserOrgType::Owner &&
headers.org_user_type != UserOrgType::Owner as i32 { headers.org_user_type != UserOrgType::Owner {
err!("Only Owners can edit Owner users") err!("Only Owners can edit Owner users")
} }
if user_to_edit.type_ == UserOrgType::Owner as i32 && if user_to_edit.type_ == UserOrgType::Owner &&
new_type != UserOrgType::Owner as i32 { new_type != UserOrgType::Owner {
// Removing owner permmission, check that there are at least another owner // Removing owner permmission, check that there are at least another owner
let num_owners = UserOrganization::find_by_org_and_type( let num_owners = UserOrganization::find_by_org_and_type(
@@ -538,7 +577,7 @@ fn edit_user(org_id: String, org_user_id: String, data: JsonUpcase<EditUserData>
} }
user_to_edit.access_all = data.AccessAll; user_to_edit.access_all = data.AccessAll;
user_to_edit.type_ = new_type; user_to_edit.type_ = new_type as i32;
// Delete all the odd collections // Delete all the odd collections
for c in CollectionUser::find_by_organization_and_user_uuid(&org_id, &user_to_edit.user_uuid, &conn) { for c in CollectionUser::find_by_organization_and_user_uuid(&org_id, &user_to_edit.user_uuid, &conn) {
@@ -591,12 +630,12 @@ fn delete_user(org_id: String, org_user_id: String, headers: AdminHeaders, conn:
None => err!("User to delete isn't member of the organization") None => err!("User to delete isn't member of the organization")
}; };
if user_to_delete.type_ != UserOrgType::User as i32 && if user_to_delete.type_ != UserOrgType::User &&
headers.org_user_type != UserOrgType::Owner as i32 { headers.org_user_type != UserOrgType::Owner {
err!("Only Owners can delete Admins or Owners") err!("Only Owners can delete Admins or Owners")
} }
if user_to_delete.type_ == UserOrgType::Owner as i32 { if user_to_delete.type_ == UserOrgType::Owner {
// Removing owner, check that there are at least another owner // Removing owner, check that there are at least another owner
let num_owners = UserOrganization::find_by_org_and_type( let num_owners = UserOrganization::find_by_org_and_type(
&org_id, UserOrgType::Owner as i32, &conn) &org_id, UserOrgType::Owner as i32, &conn)
@@ -643,17 +682,17 @@ struct RelationsData {
Value: usize, Value: usize,
} }
#[post("/ciphers/import-organization?<query>", data = "<data>")] #[post("/ciphers/import-organization?<query..>", data = "<data>")]
fn post_org_import(query: OrgIdData, data: JsonUpcase<ImportData>, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> EmptyResult { fn post_org_import(query: Form<OrgIdData>, data: JsonUpcase<ImportData>, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> EmptyResult {
let data: ImportData = data.into_inner().data; let data: ImportData = data.into_inner().data;
let org_id = query.organizationId; let org_id = query.into_inner().organization_id;
let org_user = match UserOrganization::find_by_user_and_org(&headers.user.uuid, &org_id, &conn) { let org_user = match UserOrganization::find_by_user_and_org(&headers.user.uuid, &org_id, &conn) {
Some(user) => user, Some(user) => user,
None => err!("User is not part of the organization") None => err!("User is not part of the organization")
}; };
if org_user.type_ > UserOrgType::Admin as i32 { if org_user.type_ < UserOrgType::Admin {
err!("Only admins or owners can import into an organization") err!("Only admins or owners can import into an organization")
} }

View File

@@ -1,16 +1,40 @@
use data_encoding::BASE32; use data_encoding::BASE32;
use rocket_contrib::{Json, Value}; use rocket_contrib::json::Json;
use serde_json; use serde_json;
use serde_json::Value;
use db::{
use crate::db::{
models::{TwoFactor, TwoFactorType, User}, models::{TwoFactor, TwoFactorType, User},
DbConn, DbConn,
}; };
use crypto; use crate::crypto;
use api::{ApiResult, JsonResult, JsonUpcase, NumberOrString, PasswordData}; use crate::api::{ApiResult, JsonResult, JsonUpcase, NumberOrString, PasswordData};
use auth::Headers; use crate::auth::Headers;
use rocket::Route;
pub fn routes() -> Vec<Route> {
routes![
get_twofactor,
get_recover,
recover,
disable_twofactor,
disable_twofactor_put,
generate_authenticator,
activate_authenticator,
activate_authenticator_put,
generate_u2f,
generate_u2f_challenge,
activate_u2f,
activate_u2f_put,
generate_yubikey,
activate_yubikey,
activate_yubikey_put,
]
}
#[get("/two-factor")] #[get("/two-factor")]
fn get_twofactor(headers: Headers, conn: DbConn) -> JsonResult { fn get_twofactor(headers: Headers, conn: DbConn) -> JsonResult {
@@ -50,7 +74,7 @@ struct RecoverTwoFactor {
fn recover(data: JsonUpcase<RecoverTwoFactor>, conn: DbConn) -> JsonResult { fn recover(data: JsonUpcase<RecoverTwoFactor>, conn: DbConn) -> JsonResult {
let data: RecoverTwoFactor = data.into_inner().data; let data: RecoverTwoFactor = data.into_inner().data;
use db::models::User; use crate::db::models::User;
// Get the user // Get the user
let mut user = match User::find_by_mail(&data.Email, &conn) { let mut user = match User::find_by_mail(&data.Email, &conn) {
@@ -219,7 +243,7 @@ fn _generate_recover_code(user: &mut User, conn: &DbConn) {
let totp_recover = BASE32.encode(&crypto::get_random(vec![0u8; 20])); let totp_recover = BASE32.encode(&crypto::get_random(vec![0u8; 20]));
user.totp_recover = Some(totp_recover); user.totp_recover = Some(totp_recover);
if user.save(conn).is_err() { if user.save(conn).is_err() {
println!("Error: Failed to save the user's two factor recovery code") error!("Failed to save the user's two factor recovery code")
} }
} }
} }
@@ -228,7 +252,7 @@ use u2f::messages::{RegisterResponse, SignResponse, U2fSignRequest};
use u2f::protocol::{Challenge, U2f}; use u2f::protocol::{Challenge, U2f};
use u2f::register::Registration; use u2f::register::Registration;
use CONFIG; use crate::CONFIG;
const U2F_VERSION: &str = "U2F_V2"; const U2F_VERSION: &str = "U2F_V2";
@@ -376,7 +400,7 @@ fn activate_u2f(data: JsonUpcase<EnableU2FData>, headers: Headers, conn: DbConn)
}))) })))
} }
Err(e) => { Err(e) => {
println!("Error: {:#?}", e); error!("{:#?}", e);
err!("Error activating u2f") err!("Error activating u2f")
} }
} }
@@ -480,14 +504,228 @@ pub fn validate_u2f_login(user_uuid: &str, response: &str, conn: &DbConn) -> Api
match response { match response {
Ok(new_counter) => { Ok(new_counter) => {
_counter = new_counter; _counter = new_counter;
println!("O {:#}", new_counter); info!("O {:#}", new_counter);
return Ok(()); return Ok(());
} }
Err(e) => { Err(e) => {
println!("E {:#}", e); info!("E {:#}", e);
break; break;
} }
} }
} }
err!("error verifying response") err!("error verifying response")
} }
#[derive(Deserialize, Debug)]
#[allow(non_snake_case)]
struct EnableYubikeyData {
MasterPasswordHash: String,
Key1: Option<String>,
Key2: Option<String>,
Key3: Option<String>,
Key4: Option<String>,
Key5: Option<String>,
Nfc: bool,
}
#[derive(Deserialize, Serialize, Debug)]
#[allow(non_snake_case)]
pub struct YubikeyMetadata {
Keys: Vec<String>,
pub Nfc: bool,
}
use yubico::Yubico;
use yubico::config::Config;
fn parse_yubikeys(data: &EnableYubikeyData) -> Vec<String> {
let mut yubikeys: Vec<String> = Vec::new();
if data.Key1.is_some() {
yubikeys.push(data.Key1.as_ref().unwrap().to_owned());
}
if data.Key2.is_some() {
yubikeys.push(data.Key2.as_ref().unwrap().to_owned());
}
if data.Key3.is_some() {
yubikeys.push(data.Key3.as_ref().unwrap().to_owned());
}
if data.Key4.is_some() {
yubikeys.push(data.Key4.as_ref().unwrap().to_owned());
}
if data.Key5.is_some() {
yubikeys.push(data.Key5.as_ref().unwrap().to_owned());
}
yubikeys
}
fn jsonify_yubikeys(yubikeys: Vec<String>) -> serde_json::Value {
let mut result = json!({});
for (i, key) in yubikeys.into_iter().enumerate() {
result[format!("Key{}", i+1)] = Value::String(key);
}
result
}
fn verify_yubikey_otp(otp: String) -> JsonResult {
if !CONFIG.yubico_cred_set {
err!("`YUBICO_CLIENT_ID` or `YUBICO_SECRET_KEY` environment variable is not set. \
Yubikey OTP Disabled")
}
let yubico = Yubico::new();
let config = Config::default().set_client_id(CONFIG.yubico_client_id.to_owned()).set_key(CONFIG.yubico_secret_key.to_owned());
let result = match CONFIG.yubico_server {
Some(ref server) => yubico.verify(otp, config.set_api_hosts(vec![server.to_owned()])),
None => yubico.verify(otp, config)
};
match result {
Ok(_answer) => Ok(Json(json!({}))),
Err(_e) => err!("Failed to verify OTP"),
}
}
#[post("/two-factor/get-yubikey", data = "<data>")]
fn generate_yubikey(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult {
if !CONFIG.yubico_cred_set {
err!("`YUBICO_CLIENT_ID` or `YUBICO_SECRET_KEY` environment variable is not set. \
Yubikey OTP Disabled")
}
let data: PasswordData = data.into_inner().data;
if !headers.user.check_valid_password(&data.MasterPasswordHash) {
err!("Invalid password");
}
let user_uuid = &headers.user.uuid;
let yubikey_type = TwoFactorType::YubiKey as i32;
let r = TwoFactor::find_by_user_and_type(user_uuid, yubikey_type, &conn);
if let Some(r) = r {
let yubikey_metadata: YubikeyMetadata =
serde_json::from_str(&r.data).expect("Can't parse YubikeyMetadata data");
let mut result = jsonify_yubikeys(yubikey_metadata.Keys);
result["Enabled"] = Value::Bool(true);
result["Nfc"] = Value::Bool(yubikey_metadata.Nfc);
result["Object"] = Value::String("twoFactorU2f".to_owned());
Ok(Json(result))
} else {
Ok(Json(json!({
"Enabled": false,
"Object": "twoFactorU2f",
})))
}
}
#[post("/two-factor/yubikey", data = "<data>")]
fn activate_yubikey(data: JsonUpcase<EnableYubikeyData>, headers: Headers, conn: DbConn) -> JsonResult {
let data: EnableYubikeyData = data.into_inner().data;
if !headers.user.check_valid_password(&data.MasterPasswordHash) {
err!("Invalid password");
}
// Check if we already have some data
let yubikey_data = TwoFactor::find_by_user_and_type(
&headers.user.uuid,
TwoFactorType::YubiKey as i32,
&conn,
);
if let Some(yubikey_data) = yubikey_data {
yubikey_data.delete(&conn).expect("Error deleting current Yubikeys");
}
let yubikeys = parse_yubikeys(&data);
if yubikeys.is_empty() {
return Ok(Json(json!({
"Enabled": false,
"Object": "twoFactorU2f",
})));
}
// Ensure they are valid OTPs
for yubikey in &yubikeys {
if yubikey.len() == 12 {
// YubiKey ID
continue
}
let result = verify_yubikey_otp(yubikey.to_owned());
if let Err(_e) = result {
err!("Invalid Yubikey OTP provided");
}
}
let yubikey_ids: Vec<String> = yubikeys.into_iter().map(|x| (&x[..12]).to_owned()).collect();
let yubikey_metadata = YubikeyMetadata {
Keys: yubikey_ids,
Nfc: data.Nfc,
};
let yubikey_registration = TwoFactor::new(
headers.user.uuid.clone(),
TwoFactorType::YubiKey,
serde_json::to_string(&yubikey_metadata).unwrap(),
);
yubikey_registration
.save(&conn).expect("Failed to save Yubikey info");
let mut result = jsonify_yubikeys(yubikey_metadata.Keys);
result["Enabled"] = Value::Bool(true);
result["Nfc"] = Value::Bool(yubikey_metadata.Nfc);
result["Object"] = Value::String("twoFactorU2f".to_owned());
Ok(Json(result))
}
#[put("/two-factor/yubikey", data = "<data>")]
fn activate_yubikey_put(data: JsonUpcase<EnableYubikeyData>, headers: Headers, conn: DbConn) -> JsonResult {
activate_yubikey(data, headers, conn)
}
pub fn validate_yubikey_login(user_uuid: &str, response: &str, conn: &DbConn) -> ApiResult<()> {
if response.len() != 44 {
err!("Invalid Yubikey OTP length");
}
let yubikey_type = TwoFactorType::YubiKey as i32;
let twofactor = match TwoFactor::find_by_user_and_type(user_uuid, yubikey_type, &conn) {
Some(tf) => tf,
None => err!("No YubiKey devices registered"),
};
let yubikey_metadata: YubikeyMetadata = serde_json::from_str(&twofactor.data).expect("Can't parse Yubikey Metadata");
let response_id = &response[..12];
if !yubikey_metadata.Keys.contains(&response_id.to_owned()) {
err!("Given Yubikey is not registered");
}
let result = verify_yubikey_otp(response.to_owned());
match result {
Ok(_answer) => Ok(()),
Err(_e) => err!("Failed to verify Yubikey against OTP server"),
}
}

View File

@@ -7,7 +7,7 @@ use rocket::http::ContentType;
use reqwest; use reqwest;
use CONFIG; use crate::CONFIG;
pub fn routes() -> Vec<Route> { pub fn routes() -> Vec<Route> {
routes![icon] routes![icon]
@@ -43,7 +43,7 @@ fn get_icon (domain: &str) -> Vec<u8> {
icon icon
}, },
Err(e) => { Err(e) => {
println!("Error downloading icon: {:?}", e); error!("Error downloading icon: {:?}", e);
get_fallback_icon() get_fallback_icon()
} }
} }
@@ -71,7 +71,7 @@ fn get_icon_url(domain: &str) -> String {
} }
fn download_icon(url: &str) -> Result<Vec<u8>, reqwest::Error> { fn download_icon(url: &str) -> Result<Vec<u8>, reqwest::Error> {
println!("Downloading icon for {}...", url); info!("Downloading icon for {}...", url);
let mut res = reqwest::get(url)?; let mut res = reqwest::get(url)?;
res = res.error_for_status()?; res = res.error_for_status()?;
@@ -105,7 +105,7 @@ fn get_fallback_icon() -> Vec<u8> {
icon icon
}, },
Err(e) => { Err(e) => {
println!("Error downloading fallback icon: {:?}", e); error!("Error downloading fallback icon: {:?}", e);
vec![] vec![]
} }
} }

View File

@@ -1,42 +1,57 @@
use std::collections::HashMap; use rocket::request::{Form, FormItems, FromForm};
use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use rocket::Route;
use rocket::request::{self, Form, FormItems, FromForm, FromRequest, Request}; use rocket_contrib::json::Json;
use rocket::{Outcome, Route}; use serde_json::Value;
use rocket_contrib::{Json, Value};
use num_traits::FromPrimitive; use num_traits::FromPrimitive;
use db::models::*; use crate::db::models::*;
use db::DbConn; use crate::db::DbConn;
use util::{self, JsonMap}; use crate::util::{self, JsonMap};
use api::{ApiResult, JsonResult}; use crate::api::{ApiResult, EmptyResult, JsonResult};
use CONFIG; use crate::auth::ClientIp;
use crate::CONFIG;
pub fn routes() -> Vec<Route> { pub fn routes() -> Vec<Route> {
routes![login] routes![login]
} }
#[post("/connect/token", data = "<connect_data>")] #[post("/connect/token", data = "<data>")]
fn login(connect_data: Form<ConnectData>, device_type: DeviceType, conn: DbConn, socket: Option<SocketAddr>) -> JsonResult { fn login(data: Form<ConnectData>, conn: DbConn, ip: ClientIp) -> JsonResult {
let data = connect_data.get(); let data: ConnectData = data.into_inner();
match data.grant_type { match data.grant_type.as_ref() {
GrantType::RefreshToken => _refresh_login(data, device_type, conn), "refresh_token" => {
GrantType::Password => _password_login(data, device_type, conn, socket), _check_is_some(&data.refresh_token, "refresh_token cannot be blank")?;
_refresh_login(data, conn)
}
"password" => {
_check_is_some(&data.client_id, "client_id cannot be blank")?;
_check_is_some(&data.password, "password cannot be blank")?;
_check_is_some(&data.scope, "scope cannot be blank")?;
_check_is_some(&data.username, "username cannot be blank")?;
_check_is_some(&data.device_identifier, "device_identifier cannot be blank")?;
_check_is_some(&data.device_name, "device_name cannot be blank")?;
_check_is_some(&data.device_type, "device_type cannot be blank")?;
_password_login(data, conn, ip)
}
t => err!("Invalid type", t),
} }
} }
fn _refresh_login(data: &ConnectData, _device_type: DeviceType, conn: DbConn) -> JsonResult { fn _refresh_login(data: ConnectData, conn: DbConn) -> JsonResult {
// Extract token // Extract token
let token = data.get("refresh_token"); let token = data.refresh_token.unwrap();
// Get device by refresh token // Get device by refresh token
let mut device = match Device::find_by_refresh_token(token, &conn) { let mut device = match Device::find_by_refresh_token(&token, &conn) {
Some(device) => device, Some(device) => device,
None => err!("Invalid refresh token"), None => err!("Invalid refresh token"),
}; };
@@ -47,90 +62,72 @@ fn _refresh_login(data: &ConnectData, _device_type: DeviceType, conn: DbConn) ->
let (access_token, expires_in) = device.refresh_tokens(&user, orgs); let (access_token, expires_in) = device.refresh_tokens(&user, orgs);
match device.save(&conn) { match device.save(&conn) {
Ok(()) => Ok(Json(json!({ Ok(()) => Ok(Json(json!({
"access_token": access_token, "access_token": access_token,
"expires_in": expires_in, "expires_in": expires_in,
"token_type": "Bearer", "token_type": "Bearer",
"refresh_token": device.refresh_token, "refresh_token": device.refresh_token,
"Key": user.key, "Key": user.key,
"PrivateKey": user.private_key, "PrivateKey": user.private_key,
}))), }))),
Err(_) => err!("Failed to add device to user") Err(e) => err!("Failed to add device to user", e),
} }
} }
fn _password_login(data: &ConnectData, device_type: DeviceType, conn: DbConn, remote: Option<SocketAddr>) -> JsonResult { fn _password_login(data: ConnectData, conn: DbConn, ip: ClientIp) -> JsonResult {
// Get the ip for error reporting
let ip = match remote {
Some(ip) => ip.ip(),
None => IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)),
};
// Validate scope // Validate scope
let scope = data.get("scope"); let scope = data.scope.as_ref().unwrap();
if scope != "api offline_access" { if scope != "api offline_access" {
err!("Scope not supported") err!("Scope not supported")
} }
// Get the user // Get the user
let username = data.get("username"); let username = data.username.as_ref().unwrap();
let user = match User::find_by_mail(username, &conn) { let user = match User::find_by_mail(username, &conn) {
Some(user) => user, Some(user) => user,
None => err!(format!( None => err!(format!(
"Username or password is incorrect. Try again. IP: {}. Username: {}.", "Username or password is incorrect. Try again. IP: {}. Username: {}.",
ip, username ip.ip, username
)), )),
}; };
// Check password // Check password
let password = data.get("password"); let password = data.password.as_ref().unwrap();
if !user.check_valid_password(password) { if !user.check_valid_password(password) {
err!(format!( err!(format!(
"Username or password is incorrect. Try again. IP: {}. Username: {}.", "Username or password is incorrect. Try again. IP: {}. Username: {}.",
ip, username ip.ip, username
)) ))
} }
// Let's only use the header and ignore the 'devicetype' parameter // On iOS, device_type sends "iOS", on others it sends a number
let device_type_num = device_type.0; let device_type = util::try_parse_string(data.device_type.as_ref()).unwrap_or(0);
let device_id = data.device_identifier.clone().expect("No device id provided");
let (device_id, device_name) = if data.is_device { let device_name = data.device_name.clone().expect("No device name provided");
(
data.get("deviceidentifier").clone(),
data.get("devicename").clone(),
)
} else {
(format!("web-{}", user.uuid), String::from("web"))
};
// Find device or create new // Find device or create new
let mut device = match Device::find_by_uuid(&device_id, &conn) { let mut device = match Device::find_by_uuid(&device_id, &conn) {
Some(device) => { Some(device) => {
// Check if valid device // Check if owned device, and recreate if not
if device.user_uuid != user.uuid { if device.user_uuid != user.uuid {
match device.delete(&conn) { info!("Device exists but is owned by another user. The old device will be discarded");
Ok(()) => Device::new(device_id, user.uuid.clone(), device_name, device_type_num), Device::new(device_id, user.uuid.clone(), device_name, device_type)
Err(_) => err!("Tried to delete device not owned by user, but failed")
}
} else { } else {
device device
} }
} }
None => { None => Device::new(device_id, user.uuid.clone(), device_name, device_type),
// Create new device
Device::new(device_id, user.uuid.clone(), device_name, device_type_num)
}
}; };
let twofactor_token = twofactor_auth(&user.uuid, &data, &mut device, &conn)?; let twofactor_token = twofactor_auth(&user.uuid, &data.clone(), &mut device, &conn)?;
// Common // Common
let user = User::find_by_uuid(&device.user_uuid, &conn).unwrap(); let user = User::find_by_uuid(&device.user_uuid, &conn).unwrap();
let orgs = UserOrganization::find_by_user(&user.uuid, &conn); let orgs = UserOrganization::find_by_user(&user.uuid, &conn);
let (access_token, expires_in) = device.refresh_tokens(&user, orgs); let (access_token, expires_in) = device.refresh_tokens(&user, orgs);
if device.save(&conn).is_err() { if let Err(e) = device.save(&conn) {
err!("Failed to add device to user") err!("Failed to add device to user", e)
} }
let mut result = json!({ let mut result = json!({
@@ -147,15 +144,11 @@ fn _password_login(data: &ConnectData, device_type: DeviceType, conn: DbConn, re
result["TwoFactorToken"] = Value::String(token); result["TwoFactorToken"] = Value::String(token);
} }
info!("User {} logged in successfully. IP: {}", username, ip.ip);
Ok(Json(result)) Ok(Json(result))
} }
fn twofactor_auth( fn twofactor_auth(user_uuid: &str, data: &ConnectData, device: &mut Device, conn: &DbConn) -> ApiResult<Option<String>> {
user_uuid: &str,
data: &ConnectData,
device: &mut Device,
conn: &DbConn,
) -> ApiResult<Option<String>> {
let twofactors_raw = TwoFactor::find_by_user(user_uuid, conn); let twofactors_raw = TwoFactor::find_by_user(user_uuid, conn);
// Remove u2f challenge twofactors (impl detail) // Remove u2f challenge twofactors (impl detail)
let twofactors: Vec<_> = twofactors_raw.iter().filter(|tf| tf.type_ < 1000).collect(); let twofactors: Vec<_> = twofactors_raw.iter().filter(|tf| tf.type_ < 1000).collect();
@@ -167,13 +160,10 @@ fn twofactor_auth(
return Ok(None); return Ok(None);
} }
let provider = match util::try_parse_string(data.get_opt("twoFactorProvider")) { let provider = data.two_factor_provider.unwrap_or(providers[0]); // If we aren't given a two factor provider, asume the first one
Some(provider) => provider,
None => providers[0], // If we aren't given a two factor provider, asume the first one
};
let twofactor_code = match data.get_opt("twoFactorToken") { let twofactor_code = match data.two_factor_token {
Some(code) => code, Some(ref code) => code,
None => err_json!(_json_err_twofactor(&providers, user_uuid, conn)?), None => err_json!(_json_err_twofactor(&providers, user_uuid, conn)?),
}; };
@@ -181,8 +171,8 @@ fn twofactor_auth(
match TwoFactorType::from_i32(provider) { match TwoFactorType::from_i32(provider) {
Some(TwoFactorType::Remember) => { Some(TwoFactorType::Remember) => {
match &device.twofactor_remember { match device.twofactor_remember {
Some(remember) if remember == twofactor_code => return Ok(None), // No twofactor token needed here Some(ref remember) if remember == twofactor_code => return Ok(None), // No twofactor token needed here
_ => err_json!(_json_err_twofactor(&providers, user_uuid, conn)?), _ => err_json!(_json_err_twofactor(&providers, user_uuid, conn)?),
} }
} }
@@ -204,15 +194,21 @@ fn twofactor_auth(
} }
Some(TwoFactorType::U2f) => { Some(TwoFactorType::U2f) => {
use api::core::two_factor; use crate::api::core::two_factor;
two_factor::validate_u2f_login(user_uuid, twofactor_code, conn)?; 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"), _ => err!("Invalid two factor provider"),
} }
if util::try_parse_string_or(data.get_opt("twoFactorRemember"), 0) == 1 { if data.two_factor_remember.unwrap_or(0) == 1 {
Ok(Some(device.refresh_twofactor_remember())) Ok(Some(device.refresh_twofactor_remember()))
} else { } else {
device.delete_twofactor_remember(); device.delete_twofactor_remember();
@@ -221,7 +217,7 @@ fn twofactor_auth(
} }
fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &DbConn) -> ApiResult<Value> { fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &DbConn) -> ApiResult<Value> {
use api::core::two_factor; use crate::api::core::two_factor;
let mut result = json!({ let mut result = json!({
"error" : "invalid_grant", "error" : "invalid_grant",
@@ -244,13 +240,9 @@ fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &DbConn) -> Api
let mut challenge_map = JsonMap::new(); let mut challenge_map = JsonMap::new();
challenge_map.insert("appId".into(), Value::String(request.app_id.clone())); challenge_map.insert("appId".into(), Value::String(request.app_id.clone()));
challenge_map challenge_map.insert("challenge".into(), Value::String(request.challenge.clone()));
.insert("challenge".into(), Value::String(request.challenge.clone()));
challenge_map.insert("version".into(), Value::String(key.version)); challenge_map.insert("version".into(), Value::String(key.version));
challenge_map.insert( challenge_map.insert("keyHandle".into(), Value::String(key.key_handle.unwrap_or_default()));
"keyHandle".into(),
Value::String(key.key_handle.unwrap_or_default()),
);
challenge_list.push(Value::Object(challenge_map)); challenge_list.push(Value::Object(challenge_map));
} }
@@ -263,6 +255,19 @@ fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &DbConn) -> Api
result["TwoFactorProviders2"][provider.to_string()] = Value::Object(map); result["TwoFactorProviders2"][provider.to_string()] = Value::Object(map);
} }
Some(TwoFactorType::YubiKey) => {
let twofactor = match TwoFactor::find_by_user_and_type(user_uuid, TwoFactorType::YubiKey as i32, &conn) {
Some(tf) => tf,
None => err!("No YubiKey devices registered"),
};
let yubikey_metadata: two_factor::YubikeyMetadata = serde_json::from_str(&twofactor.data).expect("Can't parse Yubikey Metadata");
let mut map = JsonMap::new();
map.insert("Nfc".into(), Value::Bool(yubikey_metadata.Nfc));
result["TwoFactorProviders2"][provider.to_string()] = Value::Object(map);
}
_ => {} _ => {}
} }
} }
@@ -270,93 +275,64 @@ fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &DbConn) -> Api
Ok(result) Ok(result)
} }
#[derive(Clone, Copy)] #[derive(Debug, Clone, Default)]
struct DeviceType(i32); #[allow(non_snake_case)]
impl<'a, 'r> FromRequest<'a, 'r> for DeviceType {
type Error = &'static str;
fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
let headers = request.headers();
let type_opt = headers.get_one("Device-Type");
let type_num = util::try_parse_string_or(type_opt, 0);
Outcome::Success(DeviceType(type_num))
}
}
#[derive(Debug)]
struct ConnectData { struct ConnectData {
grant_type: GrantType, grant_type: String, // refresh_token, password
is_device: bool,
data: HashMap<String, String>, // Needed for grant_type="refresh_token"
refresh_token: Option<String>,
// Needed for grant_type="password"
client_id: Option<String>, // web, cli, desktop, browser, mobile
password: Option<String>,
scope: Option<String>,
username: Option<String>,
device_identifier: Option<String>,
device_name: Option<String>,
device_type: Option<String>,
// Needed for two-factor auth
two_factor_provider: Option<i32>,
two_factor_token: Option<String>,
two_factor_remember: Option<i32>,
} }
#[derive(Debug, Copy, Clone)]
enum GrantType {
RefreshToken,
Password,
}
impl ConnectData {
fn get(&self, key: &str) -> &String {
&self.data[&key.to_lowercase()]
}
fn get_opt(&self, key: &str) -> Option<&String> {
self.data.get(&key.to_lowercase())
}
}
const VALUES_REFRESH: [&str; 1] = ["refresh_token"];
const VALUES_PASSWORD: [&str; 5] = ["client_id", "grant_type", "password", "scope", "username"];
const VALUES_DEVICE: [&str; 3] = ["deviceidentifier", "devicename", "devicetype"];
impl<'f> FromForm<'f> for ConnectData { impl<'f> FromForm<'f> for ConnectData {
type Error = String; type Error = String;
fn from_form(items: &mut FormItems<'f>, _strict: bool) -> Result<Self, Self::Error> { fn from_form(items: &mut FormItems<'f>, _strict: bool) -> Result<Self, Self::Error> {
let mut data = HashMap::new(); let mut form = Self::default();
for item in items {
let (key, value) = item.key_value_decoded();
let mut normalized_key = key.to_lowercase();
normalized_key.retain(|c| c != '_'); // Remove '_'
// Insert data into map match normalized_key.as_ref() {
for (key, value) in items { "granttype" => form.grant_type = value,
match (key.url_decode(), value.url_decode()) { "refreshtoken" => form.refresh_token = Some(value),
(Ok(key), Ok(value)) => data.insert(key.to_lowercase(), value), "clientid" => form.client_id = Some(value),
_ => return Err("Error decoding key or value".to_string()), "password" => form.password = Some(value),
}; "scope" => form.scope = Some(value),
"username" => form.username = Some(value),
"deviceidentifier" => form.device_identifier = Some(value),
"devicename" => form.device_name = Some(value),
"devicetype" => form.device_type = Some(value),
"twofactorprovider" => form.two_factor_provider = value.parse().ok(),
"twofactortoken" => form.two_factor_token = Some(value),
"twofactorremember" => form.two_factor_remember = value.parse().ok(),
key => warn!("Detected unexpected parameter during login: {}", key),
}
} }
// Validate needed values Ok(form)
let (grant_type, is_device) = match data.get("grant_type").map(String::as_ref) {
Some("refresh_token") => {
check_values(&data, &VALUES_REFRESH)?;
(GrantType::RefreshToken, false) // Device doesn't matter here
}
Some("password") => {
check_values(&data, &VALUES_PASSWORD)?;
let is_device = match data["client_id"].as_ref() {
"browser" | "mobile" => check_values(&data, &VALUES_DEVICE)?,
_ => false,
};
(GrantType::Password, is_device)
}
_ => return Err("Grant type not supported".to_string()),
};
Ok(ConnectData {
grant_type,
is_device,
data,
})
} }
} }
fn check_values(map: &HashMap<String, String>, values: &[&str]) -> Result<bool, String> { fn _check_is_some<T>(value: &Option<T>, msg: &str) -> EmptyResult {
for value in values { if value.is_none() {
if !map.contains_key(*value) { err!(msg)
return Err(format!("{} cannot be blank", value));
}
} }
Ok(true) Ok(())
} }

View File

@@ -12,14 +12,15 @@ pub use self::notifications::routes as notifications_routes;
pub use self::notifications::{start_notification_server, WebSocketUsers, UpdateType}; pub use self::notifications::{start_notification_server, WebSocketUsers, UpdateType};
use rocket::response::status::BadRequest; use rocket::response::status::BadRequest;
use rocket_contrib::Json; use rocket_contrib::json::Json;
use serde_json::Value;
// Type aliases for API methods results // Type aliases for API methods results
type ApiResult<T> = Result<T, BadRequest<Json>>; type ApiResult<T> = Result<T, BadRequest<Json<Value>>>;
type JsonResult = ApiResult<Json>; type JsonResult = ApiResult<Json<Value>>;
type EmptyResult = ApiResult<()>; type EmptyResult = ApiResult<()>;
use util; use crate::util;
type JsonUpcase<T> = Json<util::UpCase<T>>; type JsonUpcase<T> = Json<util::UpCase<T>>;
// Common structs representing JSON data received // Common structs representing JSON data received

View File

@@ -1,12 +1,12 @@
use rocket::Route; use rocket::Route;
use rocket_contrib::Json; use rocket_contrib::json::Json;
use serde_json::Value as JsonValue; use serde_json::Value as JsonValue;
use api::JsonResult; use crate::api::JsonResult;
use auth::Headers; use crate::auth::Headers;
use db::DbConn; use crate::db::DbConn;
use CONFIG; use crate::CONFIG;
pub fn routes() -> Vec<Route> { pub fn routes() -> Vec<Route> {
routes![negotiate, websockets_err] routes![negotiate, websockets_err]
@@ -19,7 +19,7 @@ fn websockets_err() -> JsonResult {
#[post("/hub/negotiate")] #[post("/hub/negotiate")]
fn negotiate(_headers: Headers, _conn: DbConn) -> JsonResult { fn negotiate(_headers: Headers, _conn: DbConn) -> JsonResult {
use crypto; use crate::crypto;
use data_encoding::BASE64URL; use data_encoding::BASE64URL;
let conn_id = BASE64URL.encode(&crypto::get_random(vec![0u8; 16])); let conn_id = BASE64URL.encode(&crypto::get_random(vec![0u8; 16]));
@@ -52,7 +52,7 @@ use chashmap::CHashMap;
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use serde_json::from_str; use serde_json::from_str;
use db::models::{Cipher, Folder, User}; use crate::db::models::{Cipher, Folder, User};
use rmpv::Value; use rmpv::Value;
@@ -139,7 +139,7 @@ impl Handler for WSHandler {
let _id = &query_split[1][3..]; let _id = &query_split[1][3..];
// Validate the user // Validate the user
use auth; use crate::auth;
let claims = match auth::decode_jwt(access_token) { let claims = match auth::decode_jwt(access_token) {
Ok(claims) => claims, Ok(claims) => claims,
Err(_) => { Err(_) => {
@@ -169,7 +169,7 @@ impl Handler for WSHandler {
} }
fn on_message(&mut self, msg: Message) -> ws::Result<()> { fn on_message(&mut self, msg: Message) -> ws::Result<()> {
println!("Server got message '{}'. ", msg); info!("Server got message '{}'. ", msg);
if let Message::Text(text) = msg.clone() { if let Message::Text(text) = msg.clone() {
let json = &text[..text.len() - 1]; // Remove last char let json = &text[..text.len() - 1]; // Remove last char
@@ -219,7 +219,6 @@ impl Factory for WSFactory {
type Handler = WSHandler; type Handler = WSHandler;
fn connection_made(&mut self, out: Sender) -> Self::Handler { fn connection_made(&mut self, out: Sender) -> Self::Handler {
println!("WS: Connection made");
WSHandler { WSHandler {
out, out,
user_uuid: None, user_uuid: None,
@@ -228,12 +227,11 @@ impl Factory for WSFactory {
} }
fn connection_lost(&mut self, handler: Self::Handler) { fn connection_lost(&mut self, handler: Self::Handler) {
println!("WS: Connection lost");
// Remove handler // Remove handler
let user_uuid = &handler.user_uuid.unwrap(); if let Some(user_uuid) = &handler.user_uuid {
if let Some(mut user_conn) = self.users.map.get_mut(user_uuid) { if let Some(mut user_conn) = self.users.map.get_mut(user_uuid) {
user_conn.remove_item(&handler.out); user_conn.remove_item(&handler.out);
}
} }
} }
} }
@@ -244,10 +242,10 @@ pub struct WebSocketUsers {
} }
impl WebSocketUsers { impl WebSocketUsers {
fn send_update(&self, user_uuid: &String, data: Vec<u8>) -> ws::Result<()> { fn send_update(&self, user_uuid: &String, data: &[u8]) -> ws::Result<()> {
if let Some(user) = self.map.get(user_uuid) { if let Some(user) = self.map.get(user_uuid) {
for sender in user.iter() { for sender in user.iter() {
sender.send(data.clone())?; sender.send(data)?;
} }
} }
Ok(()) Ok(())
@@ -264,7 +262,7 @@ impl WebSocketUsers {
ut, ut,
); );
self.send_update(&user.uuid.clone(), data).ok(); self.send_update(&user.uuid.clone(), &data).ok();
} }
pub fn send_folder_update(&self, ut: UpdateType, folder: &Folder) { pub fn send_folder_update(&self, ut: UpdateType, folder: &Folder) {
@@ -277,10 +275,10 @@ impl WebSocketUsers {
ut, ut,
); );
self.send_update(&folder.user_uuid, data).ok(); self.send_update(&folder.user_uuid, &data).ok();
} }
pub fn send_cipher_update(&self, ut: UpdateType, cipher: &Cipher, user_uuids: &Vec<String>) { pub fn send_cipher_update(&self, ut: UpdateType, cipher: &Cipher, user_uuids: &[String]) {
let user_uuid = convert_option(cipher.user_uuid.clone()); let user_uuid = convert_option(cipher.user_uuid.clone());
let org_uuid = convert_option(cipher.organization_uuid.clone()); let org_uuid = convert_option(cipher.organization_uuid.clone());
@@ -296,7 +294,7 @@ impl WebSocketUsers {
); );
for uuid in user_uuids { for uuid in user_uuids {
self.send_update(&uuid, data.clone()).ok(); self.send_update(&uuid, &data).ok();
} }
} }
} }

View File

@@ -6,9 +6,10 @@ use rocket::response::{self, NamedFile, Responder};
use rocket::response::content::Content; use rocket::response::content::Content;
use rocket::http::{ContentType, Status}; use rocket::http::{ContentType, Status};
use rocket::Route; use rocket::Route;
use rocket_contrib::{Json, Value}; use rocket_contrib::json::Json;
use serde_json::Value;
use CONFIG; use crate::CONFIG;
pub fn routes() -> Vec<Route> { pub fn routes() -> Vec<Route> {
if CONFIG.web_vault_enabled { if CONFIG.web_vault_enabled {
@@ -73,7 +74,7 @@ fn attachments(uuid: String, file: PathBuf) -> io::Result<NamedFile> {
#[get("/alive")] #[get("/alive")]
fn alive() -> Json<String> { fn alive() -> Json<String> {
use util::format_date; use crate::util::format_date;
use chrono::Utc; use chrono::Utc;
Json(format_date(&Utc::now().naive_utc())) Json(format_date(&Utc::now().naive_utc()))

View File

@@ -1,22 +1,21 @@
/// ///
/// JWT Handling /// JWT Handling
/// ///
use crate::util::read_file;
use util::read_file;
use chrono::Duration; use chrono::Duration;
use jwt; use jsonwebtoken::{self, Algorithm, Header};
use serde::ser::Serialize; use serde::ser::Serialize;
use CONFIG; use crate::CONFIG;
const JWT_ALGORITHM: jwt::Algorithm = jwt::Algorithm::RS256; const JWT_ALGORITHM: Algorithm = Algorithm::RS256;
lazy_static! { lazy_static! {
pub static ref DEFAULT_VALIDITY: Duration = Duration::hours(2); pub static ref DEFAULT_VALIDITY: Duration = Duration::hours(2);
pub static ref JWT_ISSUER: String = CONFIG.domain.clone(); pub static ref JWT_ISSUER: String = CONFIG.domain.clone();
static ref JWT_HEADER: jwt::Header = jwt::Header::new(JWT_ALGORITHM); static ref JWT_HEADER: Header = Header::new(JWT_ALGORITHM);
static ref PRIVATE_RSA_KEY: Vec<u8> = match read_file(&CONFIG.private_rsa_key) { static ref PRIVATE_RSA_KEY: Vec<u8> = match read_file(&CONFIG.private_rsa_key) {
Ok(key) => key, Ok(key) => key,
@@ -30,17 +29,17 @@ lazy_static! {
} }
pub fn encode_jwt<T: Serialize>(claims: &T) -> String { pub fn encode_jwt<T: Serialize>(claims: &T) -> String {
match jwt::encode(&JWT_HEADER, claims, &PRIVATE_RSA_KEY) { match jsonwebtoken::encode(&JWT_HEADER, claims, &PRIVATE_RSA_KEY) {
Ok(token) => token, Ok(token) => token,
Err(e) => panic!("Error encoding jwt {}", e) Err(e) => panic!("Error encoding jwt {}", e)
} }
} }
pub fn decode_jwt(token: &str) -> Result<JWTClaims, String> { pub fn decode_jwt(token: &str) -> Result<JWTClaims, String> {
let validation = jwt::Validation { let validation = jsonwebtoken::Validation {
leeway: 30, // 30 seconds leeway: 30, // 30 seconds
validate_exp: true, validate_exp: true,
validate_iat: true, validate_iat: false, // IssuedAt is the same as NotBefore
validate_nbf: true, validate_nbf: true,
aud: None, aud: None,
iss: Some(JWT_ISSUER.clone()), iss: Some(JWT_ISSUER.clone()),
@@ -48,10 +47,10 @@ pub fn decode_jwt(token: &str) -> Result<JWTClaims, String> {
algorithms: vec![JWT_ALGORITHM], algorithms: vec![JWT_ALGORITHM],
}; };
match jwt::decode(token, &PUBLIC_RSA_KEY, &validation) { match jsonwebtoken::decode(token, &PUBLIC_RSA_KEY, &validation) {
Ok(decoded) => Ok(decoded.claims), Ok(decoded) => Ok(decoded.claims),
Err(msg) => { Err(msg) => {
println!("Error validating jwt - {:#?}", msg); error!("Error validating jwt - {:#?}", msg);
Err(msg.to_string()) Err(msg.to_string())
} }
} }
@@ -76,6 +75,7 @@ pub struct JWTClaims {
pub orgowner: Vec<String>, pub orgowner: Vec<String>,
pub orgadmin: Vec<String>, pub orgadmin: Vec<String>,
pub orguser: Vec<String>, pub orguser: Vec<String>,
pub orgmanager: Vec<String>,
// user security_stamp // user security_stamp
pub sstamp: String, pub sstamp: String,
@@ -90,12 +90,11 @@ pub struct JWTClaims {
/// ///
/// Bearer token authentication /// Bearer token authentication
/// ///
use rocket::Outcome; use rocket::Outcome;
use rocket::request::{self, Request, FromRequest}; use rocket::request::{self, Request, FromRequest};
use db::DbConn; use crate::db::DbConn;
use db::models::{User, Organization, UserOrganization, UserOrgType, UserOrgStatus, Device}; use crate::db::models::{User, Organization, UserOrganization, UserOrgType, UserOrgStatus, Device};
pub struct Headers { pub struct Headers {
pub host: String, pub host: String,
@@ -139,13 +138,11 @@ impl<'a, 'r> FromRequest<'a, 'r> for Headers {
// Get access_token // Get access_token
let access_token: &str = match request.headers().get_one("Authorization") { let access_token: &str = match request.headers().get_one("Authorization") {
Some(a) => { Some(a) => match a.rsplit("Bearer ").next() {
match a.rsplit("Bearer ").next() { Some(split) => split,
Some(split) => split, None => err_handler!("No access token provided"),
None => err_handler!("No access token provided") },
} None => err_handler!("No access token provided"),
}
None => err_handler!("No access token provided")
}; };
// Check JWT token is valid and get device and user from it // Check JWT token is valid and get device and user from it
@@ -184,7 +181,7 @@ pub struct OrgHeaders {
pub host: String, pub host: String,
pub device: Device, pub device: Device,
pub user: User, pub user: User,
pub org_user_type: i32, pub org_user_type: UserOrgType,
} }
impl<'a, 'r> FromRequest<'a, 'r> for OrgHeaders { impl<'a, 'r> FromRequest<'a, 'r> for OrgHeaders {
@@ -192,13 +189,12 @@ impl<'a, 'r> FromRequest<'a, 'r> for OrgHeaders {
fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> { fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
match request.guard::<Headers>() { match request.guard::<Headers>() {
Outcome::Forward(f) => Outcome::Forward(f), Outcome::Forward(_) => Outcome::Forward(()),
Outcome::Failure(f) => Outcome::Failure(f), Outcome::Failure(f) => Outcome::Failure(f),
Outcome::Success(headers) => { Outcome::Success(headers) => {
// org_id is expected to be the first dynamic param // org_id is expected to be the second param ("/organizations/<org_id>")
match request.get_param::<String>(0) { match request.get_param::<String>(1) {
Err(_) => err_handler!("Error getting the organization id"), Some(Ok(org_id)) => {
Ok(org_id) => {
let conn = match request.guard::<DbConn>() { let conn = match request.guard::<DbConn>() {
Outcome::Success(conn) => conn, Outcome::Success(conn) => conn,
_ => err_handler!("Error getting DB") _ => err_handler!("Error getting DB")
@@ -225,9 +221,16 @@ impl<'a, 'r> FromRequest<'a, 'r> for OrgHeaders {
host: headers.host, host: headers.host,
device: headers.device, device: headers.device,
user: headers.user, user: headers.user,
org_user_type: org_user.type_, org_user_type: {
if let Some(org_usr_type) = UserOrgType::from_i32(org_user.type_) {
org_usr_type
} else { // This should only happen if the DB is corrupted
err_handler!("Unknown user type in the database")
}
},
}) })
} },
_ => err_handler!("Error getting the organization id"),
} }
} }
} }
@@ -238,7 +241,7 @@ pub struct AdminHeaders {
pub host: String, pub host: String,
pub device: Device, pub device: Device,
pub user: User, pub user: User,
pub org_user_type: i32, pub org_user_type: UserOrgType,
} }
impl<'a, 'r> FromRequest<'a, 'r> for AdminHeaders { impl<'a, 'r> FromRequest<'a, 'r> for AdminHeaders {
@@ -246,18 +249,18 @@ impl<'a, 'r> FromRequest<'a, 'r> for AdminHeaders {
fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> { fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
match request.guard::<OrgHeaders>() { match request.guard::<OrgHeaders>() {
Outcome::Forward(f) => Outcome::Forward(f), Outcome::Forward(_) => Outcome::Forward(()),
Outcome::Failure(f) => Outcome::Failure(f), Outcome::Failure(f) => Outcome::Failure(f),
Outcome::Success(headers) => { Outcome::Success(headers) => {
if headers.org_user_type > UserOrgType::Admin as i32 { if headers.org_user_type >= UserOrgType::Admin {
err_handler!("You need to be Admin or Owner to call this endpoint") Outcome::Success(Self {
} else {
Outcome::Success(Self{
host: headers.host, host: headers.host,
device: headers.device, device: headers.device,
user: headers.user, user: headers.user,
org_user_type: headers.org_user_type, org_user_type: headers.org_user_type,
}) })
} else {
err_handler!("You need to be Admin or Owner to call this endpoint")
} }
} }
} }
@@ -275,19 +278,41 @@ impl<'a, 'r> FromRequest<'a, 'r> for OwnerHeaders {
fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> { fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
match request.guard::<OrgHeaders>() { match request.guard::<OrgHeaders>() {
Outcome::Forward(f) => Outcome::Forward(f), Outcome::Forward(_) => Outcome::Forward(()),
Outcome::Failure(f) => Outcome::Failure(f), Outcome::Failure(f) => Outcome::Failure(f),
Outcome::Success(headers) => { Outcome::Success(headers) => {
if headers.org_user_type > UserOrgType::Owner as i32 { if headers.org_user_type == UserOrgType::Owner {
err_handler!("You need to be Owner to call this endpoint") Outcome::Success(Self {
} else {
Outcome::Success(Self{
host: headers.host, host: headers.host,
device: headers.device, device: headers.device,
user: headers.user, user: headers.user,
}) })
} else {
err_handler!("You need to be Owner to call this endpoint")
} }
} }
} }
} }
} }
///
/// Client IP address detection
///
use std::net::IpAddr;
pub struct ClientIp {
pub ip: IpAddr,
}
impl<'a, 'r> FromRequest<'a, 'r> for ClientIp {
type Error = ();
fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
let ip = match request.client_ip() {
Some(addr) => addr,
None => "0.0.0.0".parse().unwrap(),
};
Outcome::Success(ClientIp { ip })
}
}

View File

@@ -9,7 +9,7 @@ use rocket::http::Status;
use rocket::request::{self, FromRequest}; use rocket::request::{self, FromRequest};
use rocket::{Outcome, Request, State}; use rocket::{Outcome, Request, State};
use CONFIG; use crate::CONFIG;
/// An alias to the database connection used /// An alias to the database connection used
type Connection = SqliteConnection; type Connection = SqliteConnection;

View File

@@ -1,7 +1,7 @@
use serde_json::Value as JsonValue; use serde_json::Value;
use super::Cipher; use super::Cipher;
use CONFIG; use crate::CONFIG;
#[derive(Debug, Identifiable, Queryable, Insertable, Associations)] #[derive(Debug, Identifiable, Queryable, Insertable, Associations)]
#[table_name = "attachments"] #[table_name = "attachments"]
@@ -12,6 +12,7 @@ pub struct Attachment {
pub cipher_uuid: String, pub cipher_uuid: String,
pub file_name: String, pub file_name: String,
pub file_size: i32, pub file_size: i32,
pub key: Option<String>
} }
/// Local methods /// Local methods
@@ -22,6 +23,7 @@ impl Attachment {
cipher_uuid, cipher_uuid,
file_name, file_name,
file_size, file_size,
key: None
} }
} }
@@ -29,8 +31,8 @@ impl Attachment {
format!("{}/{}/{}", CONFIG.attachments_folder, self.cipher_uuid, self.id) format!("{}/{}/{}", CONFIG.attachments_folder, self.cipher_uuid, self.id)
} }
pub fn to_json(&self, host: &str) -> JsonValue { pub fn to_json(&self, host: &str) -> Value {
use util::get_display_size; use crate::util::get_display_size;
let web_path = format!("{}/attachments/{}/{}", host, self.cipher_uuid, self.id); let web_path = format!("{}/attachments/{}/{}", host, self.cipher_uuid, self.id);
let display_size = get_display_size(self.file_size); let display_size = get_display_size(self.file_size);
@@ -41,6 +43,7 @@ impl Attachment {
"FileName": self.file_name, "FileName": self.file_name,
"Size": self.file_size.to_string(), "Size": self.file_size.to_string(),
"SizeName": display_size, "SizeName": display_size,
"Key": self.key,
"Object": "attachment" "Object": "attachment"
}) })
} }
@@ -48,8 +51,8 @@ impl Attachment {
use diesel; use diesel;
use diesel::prelude::*; use diesel::prelude::*;
use db::DbConn; use crate::db::DbConn;
use db::schema::attachments; use crate::db::schema::attachments;
/// Database methods /// Database methods
impl Attachment { impl Attachment {
@@ -61,39 +64,21 @@ impl Attachment {
} }
pub fn delete(self, conn: &DbConn) -> QueryResult<()> { pub fn delete(self, conn: &DbConn) -> QueryResult<()> {
use util; crate::util::retry(
use std::{thread, time}; || {
diesel::delete(attachments::table.filter(attachments::id.eq(&self.id)))
.execute(&**conn)
},
10,
)?;
let mut retries = 10; crate::util::delete_file(&self.get_file_path());
loop {
match diesel::delete(
attachments::table.filter(
attachments::id.eq(&self.id)
)
).execute(&**conn) {
Ok(_) => break,
Err(err) => {
if retries < 1 {
println!("ERROR: Failed with 10 retries");
return Err(err)
} else {
retries -= 1;
println!("Had to retry! Retries left: {}", retries);
thread::sleep(time::Duration::from_millis(500));
continue
}
}
}
}
util::delete_file(&self.get_file_path());
Ok(()) Ok(())
} }
pub fn delete_all_by_cipher(cipher_uuid: &str, conn: &DbConn) -> QueryResult<()> { pub fn delete_all_by_cipher(cipher_uuid: &str, conn: &DbConn) -> QueryResult<()> {
for attachement in Attachment::find_by_cipher(&cipher_uuid, &conn) { for attachment in Attachment::find_by_cipher(&cipher_uuid, &conn) {
attachement.delete(&conn)?; attachment.delete(&conn)?;
} }
Ok(()) Ok(())
} }

View File

@@ -1,7 +1,5 @@
use chrono::{NaiveDateTime, Utc}; use chrono::{NaiveDateTime, Utc};
use serde_json::Value as JsonValue; use serde_json::Value;
use uuid::Uuid;
use super::{User, Organization, Attachment, FolderCipher, CollectionCipher, UserOrganization, UserOrgType, UserOrgStatus}; use super::{User, Organization, Attachment, FolderCipher, CollectionCipher, UserOrganization, UserOrgType, UserOrgStatus};
@@ -41,7 +39,7 @@ impl Cipher {
let now = Utc::now().naive_utc(); let now = Utc::now().naive_utc();
Self { Self {
uuid: Uuid::new_v4().to_string(), uuid: crate::util::get_uuid(),
created_at: now, created_at: now,
updated_at: now, updated_at: now,
@@ -63,28 +61,28 @@ impl Cipher {
use diesel; use diesel;
use diesel::prelude::*; use diesel::prelude::*;
use db::DbConn; use crate::db::DbConn;
use db::schema::*; use crate::db::schema::*;
/// Database methods /// Database methods
impl Cipher { impl Cipher {
pub fn to_json(&self, host: &str, user_uuid: &str, conn: &DbConn) -> JsonValue { pub fn to_json(&self, host: &str, user_uuid: &str, conn: &DbConn) -> Value {
use serde_json; use serde_json;
use util::format_date; use crate::util::format_date;
use super::Attachment; use super::Attachment;
let attachments = Attachment::find_by_cipher(&self.uuid, conn); let attachments = Attachment::find_by_cipher(&self.uuid, conn);
let attachments_json: Vec<JsonValue> = attachments.iter().map(|c| c.to_json(host)).collect(); let attachments_json: Vec<Value> = attachments.iter().map(|c| c.to_json(host)).collect();
let fields_json: JsonValue = if let Some(ref fields) = self.fields { let fields_json: Value = if let Some(ref fields) = self.fields {
serde_json::from_str(fields).unwrap() serde_json::from_str(fields).unwrap()
} else { JsonValue::Null }; } else { Value::Null };
let password_history_json: JsonValue = if let Some(ref password_history) = self.password_history { let password_history_json: Value = if let Some(ref password_history) = self.password_history {
serde_json::from_str(password_history).unwrap() serde_json::from_str(password_history).unwrap()
} else { JsonValue::Null }; } else { Value::Null };
let mut data_json: JsonValue = serde_json::from_str(&self.data).unwrap(); let mut data_json: Value = serde_json::from_str(&self.data).unwrap();
// TODO: ******* Backwards compat start ********** // TODO: ******* Backwards compat start **********
// To remove backwards compatibility, just remove this entire section // To remove backwards compatibility, just remove this entire section
@@ -234,7 +232,7 @@ impl Cipher {
} }
pub fn is_write_accessible_to_user(&self, user_uuid: &str, conn: &DbConn) -> bool { pub fn is_write_accessible_to_user(&self, user_uuid: &str, conn: &DbConn) -> bool {
match ciphers::table ciphers::table
.filter(ciphers::uuid.eq(&self.uuid)) .filter(ciphers::uuid.eq(&self.uuid))
.left_join(users_organizations::table.on( .left_join(users_organizations::table.on(
ciphers::organization_uuid.eq(users_organizations::org_uuid.nullable()).and( ciphers::organization_uuid.eq(users_organizations::org_uuid.nullable()).and(
@@ -255,14 +253,11 @@ impl Cipher {
) )
)) ))
.select(ciphers::all_columns) .select(ciphers::all_columns)
.first::<Self>(&**conn).ok() { .first::<Self>(&**conn).ok().is_some()
Some(_) => true,
None => false
}
} }
pub fn is_accessible_to_user(&self, user_uuid: &str, conn: &DbConn) -> bool { pub fn is_accessible_to_user(&self, user_uuid: &str, conn: &DbConn) -> bool {
match ciphers::table ciphers::table
.filter(ciphers::uuid.eq(&self.uuid)) .filter(ciphers::uuid.eq(&self.uuid))
.left_join(users_organizations::table.on( .left_join(users_organizations::table.on(
ciphers::organization_uuid.eq(users_organizations::org_uuid.nullable()).and( ciphers::organization_uuid.eq(users_organizations::org_uuid.nullable()).and(
@@ -281,10 +276,7 @@ impl Cipher {
) )
)) ))
.select(ciphers::all_columns) .select(ciphers::all_columns)
.first::<Self>(&**conn).ok() { .first::<Self>(&**conn).ok().is_some()
Some(_) => true,
None => false
}
} }
pub fn get_folder_uuid(&self, user_uuid: &str, conn: &DbConn) -> Option<String> { pub fn get_folder_uuid(&self, user_uuid: &str, conn: &DbConn) -> Option<String> {
@@ -318,7 +310,9 @@ impl Cipher {
.filter(ciphers::user_uuid.eq(user_uuid).or( // Cipher owner .filter(ciphers::user_uuid.eq(user_uuid).or( // Cipher owner
users_organizations::access_all.eq(true).or( // access_all in Organization users_organizations::access_all.eq(true).or( // access_all in Organization
users_organizations::type_.le(UserOrgType::Admin as i32).or( // Org admin or owner users_organizations::type_.le(UserOrgType::Admin as i32).or( // Org admin or owner
users_collections::user_uuid.eq(user_uuid) // Access to Collection users_collections::user_uuid.eq(user_uuid).and( // Access to Collection
users_organizations::status.eq(UserOrgStatus::Confirmed as i32)
)
) )
) )
)) ))

View File

@@ -1,6 +1,4 @@
use serde_json::Value as JsonValue; use serde_json::Value;
use uuid::Uuid;
use super::{Organization, UserOrganization, UserOrgType, UserOrgStatus}; use super::{Organization, UserOrganization, UserOrgType, UserOrgStatus};
@@ -18,14 +16,14 @@ pub struct Collection {
impl Collection { impl Collection {
pub fn new(org_uuid: String, name: String) -> Self { pub fn new(org_uuid: String, name: String) -> Self {
Self { Self {
uuid: Uuid::new_v4().to_string(), uuid: crate::util::get_uuid(),
org_uuid, org_uuid,
name, name,
} }
} }
pub fn to_json(&self) -> JsonValue { pub fn to_json(&self) -> Value {
json!({ json!({
"Id": self.uuid, "Id": self.uuid,
"OrganizationId": self.org_uuid, "OrganizationId": self.org_uuid,
@@ -37,8 +35,8 @@ impl Collection {
use diesel; use diesel;
use diesel::prelude::*; use diesel::prelude::*;
use db::DbConn; use crate::db::DbConn;
use db::schema::*; use crate::db::schema::*;
/// Database methods /// Database methods
impl Collection { impl Collection {
@@ -150,15 +148,12 @@ impl Collection {
if user_org.access_all { if user_org.access_all {
true true
} else { } else {
match users_collections::table.inner_join(collections::table) users_collections::table.inner_join(collections::table)
.filter(users_collections::collection_uuid.eq(&self.uuid)) .filter(users_collections::collection_uuid.eq(&self.uuid))
.filter(users_collections::user_uuid.eq(&user_uuid)) .filter(users_collections::user_uuid.eq(&user_uuid))
.filter(users_collections::read_only.eq(false)) .filter(users_collections::read_only.eq(false))
.select(collections::all_columns) .select(collections::all_columns)
.first::<Self>(&**conn).ok() { .first::<Self>(&**conn).ok().is_some() // Read only or no access to collection
None => false, // Read only or no access to collection
Some(_) => true,
}
} }
} }
} }

View File

@@ -45,7 +45,7 @@ impl Device {
pub fn refresh_twofactor_remember(&mut self) -> String { pub fn refresh_twofactor_remember(&mut self) -> String {
use data_encoding::BASE64; use data_encoding::BASE64;
use crypto; use crate::crypto;
let twofactor_remember = BASE64.encode(&crypto::get_random(vec![0u8; 180])); let twofactor_remember = BASE64.encode(&crypto::get_random(vec![0u8; 180]));
self.twofactor_remember = Some(twofactor_remember.clone()); self.twofactor_remember = Some(twofactor_remember.clone());
@@ -62,7 +62,7 @@ impl Device {
// If there is no refresh token, we create one // If there is no refresh token, we create one
if self.refresh_token.is_empty() { if self.refresh_token.is_empty() {
use data_encoding::BASE64URL; use data_encoding::BASE64URL;
use crypto; use crate::crypto;
self.refresh_token = BASE64URL.encode(&crypto::get_random_64()); self.refresh_token = BASE64URL.encode(&crypto::get_random_64());
} }
@@ -71,14 +71,14 @@ impl Device {
let time_now = Utc::now().naive_utc(); let time_now = Utc::now().naive_utc();
self.updated_at = time_now; self.updated_at = time_now;
let orgowner: Vec<_> = orgs.iter().filter(|o| o.type_ == 0).map(|o| o.org_uuid.clone()).collect(); let orgowner: Vec<_> = orgs.iter().filter(|o| o.type_ == 0).map(|o| o.org_uuid.clone()).collect();
let orgadmin: Vec<_> = orgs.iter().filter(|o| o.type_ == 1).map(|o| o.org_uuid.clone()).collect(); let orgadmin: Vec<_> = orgs.iter().filter(|o| o.type_ == 1).map(|o| o.org_uuid.clone()).collect();
let orguser: Vec<_> = orgs.iter().filter(|o| o.type_ == 2).map(|o| o.org_uuid.clone()).collect(); let orguser: Vec<_> = orgs.iter().filter(|o| o.type_ == 2).map(|o| o.org_uuid.clone()).collect();
let orgmanager: Vec<_> = orgs.iter().filter(|o| o.type_ == 3).map(|o| o.org_uuid.clone()).collect();
// Create the JWT claims struct, to send to the client // Create the JWT claims struct, to send to the client
use auth::{encode_jwt, JWTClaims, DEFAULT_VALIDITY, JWT_ISSUER}; use crate::auth::{encode_jwt, JWTClaims, DEFAULT_VALIDITY, JWT_ISSUER};
let claims = JWTClaims { let claims = JWTClaims {
nbf: time_now.timestamp(), nbf: time_now.timestamp(),
exp: (time_now + *DEFAULT_VALIDITY).timestamp(), exp: (time_now + *DEFAULT_VALIDITY).timestamp(),
@@ -93,6 +93,7 @@ impl Device {
orgowner, orgowner,
orgadmin, orgadmin,
orguser, orguser,
orgmanager,
sstamp: user.security_stamp.to_string(), sstamp: user.security_stamp.to_string(),
device: self.uuid.to_string(), device: self.uuid.to_string(),
@@ -100,23 +101,29 @@ impl Device {
amr: vec!["Application".into()], amr: vec!["Application".into()],
}; };
(encode_jwt(&claims), DEFAULT_VALIDITY.num_seconds()) (encode_jwt(&claims), DEFAULT_VALIDITY.num_seconds())
} }
} }
use diesel; use diesel;
use diesel::prelude::*; use diesel::prelude::*;
use db::DbConn; use crate::db::DbConn;
use db::schema::devices; use crate::db::schema::devices;
/// Database methods /// Database methods
impl Device { impl Device {
pub fn save(&mut self, conn: &DbConn) -> QueryResult<()> { pub fn save(&mut self, conn: &DbConn) -> QueryResult<()> {
self.updated_at = Utc::now().naive_utc(); self.updated_at = Utc::now().naive_utc();
diesel::replace_into(devices::table) crate::util::retry(
.values(&*self).execute(&**conn).and(Ok(())) || {
diesel::replace_into(devices::table)
.values(&*self)
.execute(&**conn)
},
10,
)
.and(Ok(()))
} }
pub fn delete(self, conn: &DbConn) -> QueryResult<()> { pub fn delete(self, conn: &DbConn) -> QueryResult<()> {

View File

@@ -1,7 +1,5 @@
use chrono::{NaiveDateTime, Utc}; use chrono::{NaiveDateTime, Utc};
use serde_json::Value as JsonValue; use serde_json::Value;
use uuid::Uuid;
use super::{User, Cipher}; use super::{User, Cipher};
@@ -33,7 +31,7 @@ impl Folder {
let now = Utc::now().naive_utc(); let now = Utc::now().naive_utc();
Self { Self {
uuid: Uuid::new_v4().to_string(), uuid: crate::util::get_uuid(),
created_at: now, created_at: now,
updated_at: now, updated_at: now,
@@ -42,8 +40,8 @@ impl Folder {
} }
} }
pub fn to_json(&self) -> JsonValue { pub fn to_json(&self) -> Value {
use util::format_date; use crate::util::format_date;
json!({ json!({
"Id": self.uuid, "Id": self.uuid,
@@ -65,8 +63,8 @@ impl FolderCipher {
use diesel; use diesel;
use diesel::prelude::*; use diesel::prelude::*;
use db::DbConn; use crate::db::DbConn;
use db::schema::{folders, folders_ciphers}; use crate::db::schema::{folders, folders_ciphers};
/// Database methods /// Database methods
impl Folder { impl Folder {

View File

@@ -1,6 +1,6 @@
use serde_json::Value as JsonValue; use std::cmp::Ordering;
use serde_json::Value;
use uuid::Uuid;
use super::{User, CollectionUser, Invitation}; use super::{User, CollectionUser, Invitation};
#[derive(Debug, Identifiable, Queryable, Insertable)] #[derive(Debug, Identifiable, Queryable, Insertable)]
@@ -32,10 +32,101 @@ pub enum UserOrgStatus {
Confirmed = 2, Confirmed = 2,
} }
#[derive(Copy, Clone)]
#[derive(PartialEq)]
#[derive(Eq)]
pub enum UserOrgType { pub enum UserOrgType {
Owner = 0, Owner = 0,
Admin = 1, Admin = 1,
User = 2, User = 2,
Manager = 3,
}
impl Ord for UserOrgType {
fn cmp(&self, other: &UserOrgType) -> Ordering {
if self == other {
Ordering::Equal
} else {
match self {
UserOrgType::Owner => Ordering::Greater,
UserOrgType::Admin => match other {
UserOrgType::Owner => Ordering::Less,
_ => Ordering::Greater
},
UserOrgType::Manager => match other {
UserOrgType::Owner | UserOrgType::Admin => Ordering::Less,
_ => Ordering::Greater
},
UserOrgType::User => Ordering::Less
}
}
}
}
impl PartialOrd for UserOrgType {
fn partial_cmp(&self, other: &UserOrgType) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl PartialEq<i32> for UserOrgType {
fn eq(&self, other: &i32) -> bool {
*other == *self as i32
}
}
impl PartialOrd<i32> for UserOrgType {
fn partial_cmp(&self, other: &i32) -> Option<Ordering> {
if let Some(other) = Self::from_i32(*other) {
return Some(self.cmp(&other))
}
None
}
fn gt(&self, other: &i32) -> bool {
match self.partial_cmp(other) {
Some(Ordering::Less) | Some(Ordering::Equal) => false,
_ => true,
}
}
fn ge(&self, other: &i32) -> bool {
match self.partial_cmp(other) {
Some(Ordering::Less) => false,
_ => true,
}
}
}
impl PartialEq<UserOrgType> for i32 {
fn eq(&self, other: &UserOrgType) -> bool {
*self == *other as i32
}
}
impl PartialOrd<UserOrgType> for i32 {
fn partial_cmp(&self, other: &UserOrgType) -> Option<Ordering> {
if let Some(self_type) = UserOrgType::from_i32(*self) {
return Some(self_type.cmp(other))
}
None
}
fn lt(&self, other: &UserOrgType) -> bool {
match self.partial_cmp(other) {
Some(Ordering::Less) | None => true,
_ => false,
}
}
fn le(&self, other: &UserOrgType) -> bool {
match self.partial_cmp(other) {
Some(Ordering::Less) | Some(Ordering::Equal) | None => true,
_ => false,
}
}
} }
impl UserOrgType { impl UserOrgType {
@@ -44,9 +135,21 @@ impl UserOrgType {
"0" | "Owner" => Some(UserOrgType::Owner), "0" | "Owner" => Some(UserOrgType::Owner),
"1" | "Admin" => Some(UserOrgType::Admin), "1" | "Admin" => Some(UserOrgType::Admin),
"2" | "User" => Some(UserOrgType::User), "2" | "User" => Some(UserOrgType::User),
"3" | "Manager" => Some(UserOrgType::Manager),
_ => None, _ => None,
} }
} }
pub fn from_i32(i: i32) -> Option<Self> {
match i {
0 => Some(UserOrgType::Owner),
1 => Some(UserOrgType::Admin),
2 => Some(UserOrgType::User),
3 => Some(UserOrgType::Manager),
_ => None,
}
}
} }
/// Local methods /// Local methods
@@ -55,7 +158,7 @@ impl Organization {
pub fn new(name: String, billing_email: String) -> Self { pub fn new(name: String, billing_email: String) -> Self {
Self { Self {
uuid: Uuid::new_v4().to_string(), uuid: crate::util::get_uuid(),
name, name,
billing_email, billing_email,
@@ -70,7 +173,7 @@ impl Organization {
} }
} }
pub fn to_json(&self) -> JsonValue { pub fn to_json(&self) -> Value {
json!({ json!({
"Id": self.uuid, "Id": self.uuid,
"Name": self.name, "Name": self.name,
@@ -102,7 +205,7 @@ impl Organization {
impl UserOrganization { impl UserOrganization {
pub fn new(user_uuid: String, org_uuid: String) -> Self { pub fn new(user_uuid: String, org_uuid: String) -> Self {
Self { Self {
uuid: Uuid::new_v4().to_string(), uuid: crate::util::get_uuid(),
user_uuid, user_uuid,
org_uuid, org_uuid,
@@ -132,8 +235,8 @@ impl UserOrganization {
use diesel; use diesel;
use diesel::prelude::*; use diesel::prelude::*;
use db::DbConn; use crate::db::DbConn;
use db::schema::{organizations, users_organizations, users_collections, ciphers_collections}; use crate::db::schema::{organizations, users_organizations, users_collections, ciphers_collections};
/// Database methods /// Database methods
impl Organization { impl Organization {
@@ -181,7 +284,7 @@ impl Organization {
} }
impl UserOrganization { impl UserOrganization {
pub fn to_json(&self, conn: &DbConn) -> JsonValue { pub fn to_json(&self, conn: &DbConn) -> Value {
let org = Organization::find_by_uuid(&self.org_uuid, conn).unwrap(); let org = Organization::find_by_uuid(&self.org_uuid, conn).unwrap();
json!({ json!({
@@ -209,7 +312,7 @@ impl UserOrganization {
}) })
} }
pub fn to_json_user_details(&self, conn: &DbConn) -> JsonValue { pub fn to_json_user_details(&self, conn: &DbConn) -> Value {
let user = User::find_by_uuid(&self.user_uuid, conn).unwrap(); let user = User::find_by_uuid(&self.user_uuid, conn).unwrap();
json!({ json!({
@@ -226,7 +329,7 @@ impl UserOrganization {
}) })
} }
pub fn to_json_collection_user_details(&self, read_only: bool, conn: &DbConn) -> JsonValue { pub fn to_json_collection_user_details(&self, read_only: bool, conn: &DbConn) -> Value {
let user = User::find_by_uuid(&self.user_uuid, conn).unwrap(); let user = User::find_by_uuid(&self.user_uuid, conn).unwrap();
json!({ json!({
@@ -241,7 +344,7 @@ impl UserOrganization {
}) })
} }
pub fn to_json_details(&self, conn: &DbConn) -> JsonValue { pub fn to_json_details(&self, conn: &DbConn) -> Value {
let coll_uuids = if self.access_all { let coll_uuids = if self.access_all {
vec![] // If we have complete access, no need to fill the array vec![] // If we have complete access, no need to fill the array
} else { } else {
@@ -302,7 +405,7 @@ impl UserOrganization {
} }
pub fn has_full_access(self) -> bool { pub fn has_full_access(self) -> bool {
self.access_all || self.type_ < UserOrgType::User as i32 self.access_all || self.type_ >= UserOrgType::Admin
} }
pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> { pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> {

View File

@@ -1,6 +1,4 @@
use serde_json::Value as JsonValue; use serde_json::Value;
use uuid::Uuid;
use super::User; use super::User;
@@ -36,7 +34,7 @@ pub enum TwoFactorType {
impl TwoFactor { impl TwoFactor {
pub fn new(user_uuid: String, type_: TwoFactorType, data: String) -> Self { pub fn new(user_uuid: String, type_: TwoFactorType, data: String) -> Self {
Self { Self {
uuid: Uuid::new_v4().to_string(), uuid: crate::util::get_uuid(),
user_uuid, user_uuid,
type_: type_ as i32, type_: type_ as i32,
enabled: true, enabled: true,
@@ -59,7 +57,7 @@ impl TwoFactor {
generated == totp_code generated == totp_code
} }
pub fn to_json(&self) -> JsonValue { pub fn to_json(&self) -> Value {
json!({ json!({
"Enabled": self.enabled, "Enabled": self.enabled,
"Key": "", // This key and value vary "Key": "", // This key and value vary
@@ -67,7 +65,7 @@ impl TwoFactor {
}) })
} }
pub fn to_json_list(&self) -> JsonValue { pub fn to_json_list(&self) -> Value {
json!({ json!({
"Enabled": self.enabled, "Enabled": self.enabled,
"Type": self.type_, "Type": self.type_,
@@ -78,8 +76,8 @@ impl TwoFactor {
use diesel; use diesel;
use diesel::prelude::*; use diesel::prelude::*;
use db::DbConn; use crate::db::DbConn;
use db::schema::twofactor; use crate::db::schema::twofactor;
/// Database methods /// Database methods
impl TwoFactor { impl TwoFactor {

View File

@@ -1,10 +1,8 @@
use chrono::{NaiveDateTime, Utc}; use chrono::{NaiveDateTime, Utc};
use serde_json::Value as JsonValue; use serde_json::Value;
use uuid::Uuid; use crate::crypto;
use crate::CONFIG;
use crypto;
use CONFIG;
#[derive(Debug, Identifiable, Queryable, Insertable)] #[derive(Debug, Identifiable, Queryable, Insertable)]
@@ -50,7 +48,7 @@ impl User {
let email = mail.to_lowercase(); let email = mail.to_lowercase();
Self { Self {
uuid: Uuid::new_v4().to_string(), uuid: crate::util::get_uuid(),
created_at: now, created_at: now,
updated_at: now, updated_at: now,
name: email.clone(), name: email.clone(),
@@ -61,7 +59,7 @@ impl User {
salt: crypto::get_random_64(), salt: crypto::get_random_64(),
password_iterations: CONFIG.password_iterations, password_iterations: CONFIG.password_iterations,
security_stamp: Uuid::new_v4().to_string(), security_stamp: crate::util::get_uuid(),
password_hint: None, password_hint: None,
private_key: None, private_key: None,
@@ -97,11 +95,10 @@ impl User {
self.password_hash = crypto::hash_password(password.as_bytes(), self.password_hash = crypto::hash_password(password.as_bytes(),
&self.salt, &self.salt,
self.password_iterations as u32); self.password_iterations as u32);
self.reset_security_stamp();
} }
pub fn reset_security_stamp(&mut self) { pub fn reset_security_stamp(&mut self) {
self.security_stamp = Uuid::new_v4().to_string(); self.security_stamp = crate::util::get_uuid();
} }
pub fn is_server_admin(&self) -> bool { pub fn is_server_admin(&self) -> bool {
@@ -114,20 +111,20 @@ impl User {
use diesel; use diesel;
use diesel::prelude::*; use diesel::prelude::*;
use db::DbConn; use crate::db::DbConn;
use db::schema::{users, invitations}; use crate::db::schema::{users, invitations};
use super::{Cipher, Folder, Device, UserOrganization, UserOrgType}; use super::{Cipher, Folder, Device, UserOrganization, UserOrgType};
/// Database methods /// Database methods
impl User { impl User {
pub fn to_json(&self, conn: &DbConn) -> JsonValue { pub fn to_json(&self, conn: &DbConn) -> Value {
use super::{UserOrganization, UserOrgType, UserOrgStatus, TwoFactor}; use super::{UserOrganization, UserOrgType, UserOrgStatus, TwoFactor};
let mut orgs = UserOrganization::find_by_user(&self.uuid, conn); let mut orgs = UserOrganization::find_by_user(&self.uuid, conn);
if self.is_server_admin() { if self.is_server_admin() {
orgs.push(UserOrganization::new_virtual(self.uuid.clone(), UserOrgType::Owner, UserOrgStatus::Confirmed)); orgs.push(UserOrganization::new_virtual(self.uuid.clone(), UserOrgType::Owner, UserOrgStatus::Confirmed));
} }
let orgs_json: Vec<JsonValue> = orgs.iter().map(|c| c.to_json(&conn)).collect(); let orgs_json: Vec<Value> = orgs.iter().map(|c| c.to_json(&conn)).collect();
let twofactor_enabled = !TwoFactor::find_by_user(&self.uuid, conn).is_empty(); let twofactor_enabled = !TwoFactor::find_by_user(&self.uuid, conn).is_empty();
json!({ json!({
@@ -157,7 +154,7 @@ impl User {
pub fn delete(self, conn: &DbConn) -> QueryResult<()> { pub fn delete(self, conn: &DbConn) -> QueryResult<()> {
for user_org in UserOrganization::find_by_user(&self.uuid, &*conn) { for user_org in UserOrganization::find_by_user(&self.uuid, &*conn) {
if user_org.type_ == UserOrgType::Owner as i32 { if user_org.type_ == UserOrgType::Owner {
if UserOrganization::find_by_org_and_type( if UserOrganization::find_by_org_and_type(
&user_org.org_uuid, &user_org.org_uuid,
UserOrgType::Owner as i32, &conn UserOrgType::Owner as i32, &conn
@@ -181,7 +178,7 @@ impl User {
pub fn update_uuid_revision(uuid: &str, conn: &DbConn) { pub fn update_uuid_revision(uuid: &str, conn: &DbConn) {
if let Some(mut user) = User::find_by_uuid(&uuid, conn) { if let Some(mut user) = User::find_by_uuid(&uuid, conn) {
if user.update_revision(conn).is_err(){ if user.update_revision(conn).is_err(){
println!("Warning: Failed to update revision for {}", user.email); warn!("Failed to update revision for {}", user.email);
}; };
}; };
} }

View File

@@ -4,6 +4,7 @@ table! {
cipher_uuid -> Text, cipher_uuid -> Text,
file_name -> Text, file_name -> Text,
file_size -> Integer, file_size -> Integer,
key -> Nullable<Text>,
} }
} }

View File

@@ -4,7 +4,7 @@ use lettre::smtp::ConnectionReuseParameters;
use lettre::smtp::authentication::Credentials; use lettre::smtp::authentication::Credentials;
use lettre_email::EmailBuilder; use lettre_email::EmailBuilder;
use MailConfig; use crate::MailConfig;
fn mailer(config: &MailConfig) -> SmtpTransport { fn mailer(config: &MailConfig) -> SmtpTransport {
let client_security = if config.smtp_ssl { let client_security = if config.smtp_ssl {

View File

@@ -1,40 +1,15 @@
#![feature(plugin, custom_derive, vec_remove_item, try_trait)] #![feature(proc_macro_hygiene, decl_macro, vec_remove_item, try_trait)]
#![plugin(rocket_codegen)] #![recursion_limit = "128"]
#![recursion_limit="128"]
#![allow(proc_macro_derive_resolution_fallback)] // TODO: Remove this when diesel update fixes warnings #![allow(proc_macro_derive_resolution_fallback)] // TODO: Remove this when diesel update fixes warnings
extern crate rocket;
extern crate rocket_contrib; #[macro_use] extern crate rocket;
extern crate reqwest; #[macro_use] extern crate serde_derive;
extern crate multipart; #[macro_use] extern crate serde_json;
extern crate ws; #[macro_use] extern crate log;
extern crate rmpv; #[macro_use] extern crate diesel;
extern crate chashmap; #[macro_use] extern crate diesel_migrations;
extern crate serde; #[macro_use] extern crate lazy_static;
#[macro_use] #[macro_use] extern crate num_derive;
extern crate serde_derive;
#[macro_use]
extern crate serde_json;
#[macro_use]
extern crate diesel;
#[macro_use]
extern crate diesel_migrations;
extern crate ring;
extern crate uuid;
extern crate chrono;
extern crate oath;
extern crate data_encoding;
extern crate jsonwebtoken as jwt;
extern crate u2f;
extern crate dotenv;
#[macro_use]
extern crate lazy_static;
#[macro_use]
extern crate num_derive;
extern crate num_traits;
extern crate lettre;
extern crate lettre_email;
extern crate native_tls;
extern crate byteorder;
use std::{path::Path, process::{exit, Command}}; use std::{path::Path, process::{exit, Command}};
use rocket::Rocket; use rocket::Rocket;
@@ -49,6 +24,9 @@ mod auth;
mod mail; mod mail;
fn init_rocket() -> Rocket { fn init_rocket() -> Rocket {
// TODO: TO HIDE MOUNTING LOG, call ignite, set logging to disabled, call all the mounts, and then enable it again
rocket::ignite() rocket::ignite()
.mount("/", api::web_routes()) .mount("/", api::web_routes())
.mount("/api", api::core_routes()) .mount("/api", api::core_routes())
@@ -68,7 +46,7 @@ mod migrations {
pub fn run_migrations() { pub fn run_migrations() {
// Make sure the database is up to date (create if it doesn't exist, or run the migrations) // Make sure the database is up to date (create if it doesn't exist, or run the migrations)
let connection = ::db::get_connection().expect("Can't conect to DB"); let connection = crate::db::get_connection().expect("Can't conect to DB");
use std::io::stdout; use std::io::stdout;
embedded_migrations::run_with_output(&connection, &mut stdout()).expect("Can't run migrations"); embedded_migrations::run_with_output(&connection, &mut stdout()).expect("Can't run migrations");
@@ -76,6 +54,10 @@ mod migrations {
} }
fn main() { fn main() {
if CONFIG.extended_logging {
init_logging().ok();
}
check_db(); check_db();
check_rsa_keys(); check_rsa_keys();
check_web_vault(); check_web_vault();
@@ -84,13 +66,61 @@ fn main() {
init_rocket().launch(); init_rocket().launch();
} }
fn init_logging() -> Result<(), fern::InitError> {
let mut logger = fern::Dispatch::new()
.format(|out, message, record| {
out.finish(format_args!(
"{}[{}][{}] {}",
chrono::Local::now().format("[%Y-%m-%d][%H:%M:%S]"),
record.target(),
record.level(),
message
))
})
.level(log::LevelFilter::Debug)
.level_for("hyper", log::LevelFilter::Warn)
.level_for("ws", log::LevelFilter::Info)
.level_for("multipart", log::LevelFilter::Info)
.chain(std::io::stdout());
if let Some(log_file) = CONFIG.log_file.as_ref() {
logger = logger.chain(fern::log_file(log_file)?);
}
logger = chain_syslog(logger);
logger.apply()?;
Ok(())
}
#[cfg(not(feature = "enable_syslog"))]
fn chain_syslog(logger: fern::Dispatch) -> fern::Dispatch { logger }
#[cfg(feature = "enable_syslog")]
fn chain_syslog(logger: fern::Dispatch) -> fern::Dispatch {
let syslog_fmt = syslog::Formatter3164 {
facility: syslog::Facility::LOG_USER,
hostname: None,
process: "bitwarden_rs".into(),
pid: 0,
};
match syslog::unix(syslog_fmt) {
Ok(sl) => logger.chain(sl),
Err(e) => {
error!("Unable to connect to syslog: {:?}", e);
logger
}
}
}
fn check_db() { fn check_db() {
let path = Path::new(&CONFIG.database_url); let path = Path::new(&CONFIG.database_url);
if let Some(parent) = path.parent() { if let Some(parent) = path.parent() {
use std::fs; use std::fs;
if fs::create_dir_all(parent).is_err() { if fs::create_dir_all(parent).is_err() {
println!("Error creating database directory"); error!("Error creating database directory");
exit(1); exit(1);
} }
} }
@@ -105,16 +135,16 @@ fn check_rsa_keys() {
// If the RSA keys don't exist, try to create them // If the RSA keys don't exist, try to create them
if !util::file_exists(&CONFIG.private_rsa_key) if !util::file_exists(&CONFIG.private_rsa_key)
|| !util::file_exists(&CONFIG.public_rsa_key) { || !util::file_exists(&CONFIG.public_rsa_key) {
println!("JWT keys don't exist, checking if OpenSSL is available..."); info!("JWT keys don't exist, checking if OpenSSL is available...");
Command::new("openssl") Command::new("openssl")
.arg("version") .arg("version")
.output().unwrap_or_else(|_| { .output().unwrap_or_else(|_| {
println!("Can't create keys because OpenSSL is not available, make sure it's installed and available on the PATH"); info!("Can't create keys because OpenSSL is not available, make sure it's installed and available on the PATH");
exit(1); exit(1);
}); });
println!("OpenSSL detected, creating keys..."); info!("OpenSSL detected, creating keys...");
let mut success = Command::new("openssl").arg("genrsa") let mut success = Command::new("openssl").arg("genrsa")
.arg("-out").arg(&CONFIG.private_rsa_key_pem) .arg("-out").arg(&CONFIG.private_rsa_key_pem)
@@ -138,9 +168,9 @@ fn check_rsa_keys() {
.status.success(); .status.success();
if success { if success {
println!("Keys created correctly."); info!("Keys created correctly.");
} else { } else {
println!("Error creating keys, exiting..."); error!("Error creating keys, exiting...");
exit(1); exit(1);
} }
} }
@@ -154,7 +184,7 @@ fn check_web_vault() {
let index_path = Path::new(&CONFIG.web_vault_folder).join("index.html"); let index_path = Path::new(&CONFIG.web_vault_folder).join("index.html");
if !index_path.exists() { if !index_path.exists() {
println!("Web vault is not found. Please follow the steps in the README to install it"); error!("Web vault is not found. Please follow the steps in the README to install it");
exit(1); exit(1);
} }
} }
@@ -176,7 +206,7 @@ pub struct MailConfig {
impl MailConfig { impl MailConfig {
fn load() -> Option<Self> { fn load() -> Option<Self> {
use util::{get_env, get_env_or}; use crate::util::{get_env, get_env_or};
// When SMTP_HOST is absent, we assume the user does not want to enable it. // When SMTP_HOST is absent, we assume the user does not want to enable it.
let smtp_host = match get_env("SMTP_HOST") { let smtp_host = match get_env("SMTP_HOST") {
@@ -185,7 +215,7 @@ impl MailConfig {
}; };
let smtp_from = get_env("SMTP_FROM").unwrap_or_else(|| { let smtp_from = get_env("SMTP_FROM").unwrap_or_else(|| {
println!("Please specify SMTP_FROM to enable SMTP support."); error!("Please specify SMTP_FROM to enable SMTP support.");
exit(1); exit(1);
}); });
@@ -201,7 +231,7 @@ impl MailConfig {
let smtp_username = get_env("SMTP_USERNAME"); let smtp_username = get_env("SMTP_USERNAME");
let smtp_password = get_env("SMTP_PASSWORD").or_else(|| { let smtp_password = get_env("SMTP_PASSWORD").or_else(|| {
if smtp_username.as_ref().is_some() { if smtp_username.as_ref().is_some() {
println!("SMTP_PASSWORD is mandatory when specifying SMTP_USERNAME."); error!("SMTP_PASSWORD is mandatory when specifying SMTP_USERNAME.");
exit(1); exit(1);
} else { } else {
None None
@@ -235,6 +265,9 @@ pub struct Config {
websocket_enabled: bool, websocket_enabled: bool,
websocket_url: String, websocket_url: String,
extended_logging: bool,
log_file: Option<String>,
local_icon_extractor: bool, local_icon_extractor: bool,
signups_allowed: bool, signups_allowed: bool,
invitations_allowed: bool, invitations_allowed: bool,
@@ -245,12 +278,17 @@ pub struct Config {
domain: String, domain: String,
domain_set: bool, domain_set: bool,
yubico_cred_set: bool,
yubico_client_id: String,
yubico_secret_key: String,
yubico_server: Option<String>,
mail: Option<MailConfig>, mail: Option<MailConfig>,
} }
impl Config { impl Config {
fn load() -> Self { fn load() -> Self {
use util::{get_env, get_env_or}; use crate::util::{get_env, get_env_or};
dotenv::dotenv().ok(); dotenv::dotenv().ok();
let df = get_env_or("DATA_FOLDER", "data".to_string()); let df = get_env_or("DATA_FOLDER", "data".to_string());
@@ -258,6 +296,9 @@ impl Config {
let domain = get_env("DOMAIN"); let domain = get_env("DOMAIN");
let yubico_client_id = get_env("YUBICO_CLIENT_ID");
let yubico_secret_key = get_env("YUBICO_SECRET_KEY");
Config { Config {
database_url: get_env_or("DATABASE_URL", format!("{}/{}", &df, "db.sqlite3")), database_url: get_env_or("DATABASE_URL", format!("{}/{}", &df, "db.sqlite3")),
icon_cache_folder: get_env_or("ICON_CACHE_FOLDER", format!("{}/{}", &df, "icon_cache")), icon_cache_folder: get_env_or("ICON_CACHE_FOLDER", format!("{}/{}", &df, "icon_cache")),
@@ -273,6 +314,9 @@ impl Config {
websocket_enabled: get_env_or("WEBSOCKET_ENABLED", false), websocket_enabled: get_env_or("WEBSOCKET_ENABLED", false),
websocket_url: format!("{}:{}", get_env_or("WEBSOCKET_ADDRESS", "0.0.0.0".to_string()), get_env_or("WEBSOCKET_PORT", 3012)), websocket_url: format!("{}:{}", get_env_or("WEBSOCKET_ADDRESS", "0.0.0.0".to_string()), get_env_or("WEBSOCKET_PORT", 3012)),
extended_logging: get_env_or("EXTENDED_LOGGING", true),
log_file: get_env("LOG_FILE"),
local_icon_extractor: get_env_or("LOCAL_ICON_EXTRACTOR", false), local_icon_extractor: get_env_or("LOCAL_ICON_EXTRACTOR", false),
signups_allowed: get_env_or("SIGNUPS_ALLOWED", true), signups_allowed: get_env_or("SIGNUPS_ALLOWED", true),
server_admin_email: get_env("SERVER_ADMIN_EMAIL"), server_admin_email: get_env("SERVER_ADMIN_EMAIL"),
@@ -283,6 +327,11 @@ impl Config {
domain_set: domain.is_some(), domain_set: domain.is_some(),
domain: domain.unwrap_or("http://localhost".into()), domain: domain.unwrap_or("http://localhost".into()),
yubico_cred_set: yubico_client_id.is_some() && yubico_secret_key.is_some(),
yubico_client_id: yubico_client_id.unwrap_or("00000".into()),
yubico_secret_key: yubico_secret_key.unwrap_or("AAAAAAA".into()),
yubico_server: get_env("YUBICO_SERVER"),
mail: MailConfig::load(), mail: MailConfig::load(),
} }
} }

View File

@@ -2,36 +2,46 @@
/// Macros /// Macros
/// ///
#[macro_export] #[macro_export]
macro_rules! err { macro_rules! _err_object {
($err:expr, $msg:expr) => {{ ($msg:expr) => {{
println!("ERROR: {}", $msg);
err_json!(json!({ err_json!(json!({
"error": $err, "Message": "",
"error_description": $err, "error": "",
"ErrorModel": { "error_description": "",
"Message": $msg, "ValidationErrors": {"": [ $msg ]},
"ValidationErrors": null, "ErrorModel": {
"ExceptionMessage": null, "Message": $msg,
"ExceptionStackTrace": null, "Object": "error"
"InnerExceptionMessage": null, },
"Object": "error" "Object": "error"
}})) }))
}}; }};
($msg:expr) => { err!("unknown_error", $msg) } }
#[macro_export]
macro_rules! err {
($msg:expr) => {{
error!("{}", $msg);
_err_object!($msg)
}};
($usr_msg:expr, $log_value:expr) => {{
error!("{}: {:#?}", $usr_msg, $log_value);
_err_object!($usr_msg)
}}
} }
#[macro_export] #[macro_export]
macro_rules! err_json { macro_rules! err_json {
($expr:expr) => {{ ($expr:expr) => {{
return Err($crate::rocket::response::status::BadRequest(Some($crate::rocket_contrib::Json($expr)))); return Err(rocket::response::status::BadRequest(Some(rocket_contrib::json::Json($expr))));
}} }}
} }
#[macro_export] #[macro_export]
macro_rules! err_handler { macro_rules! err_handler {
($expr:expr) => {{ ($expr:expr) => {{
println!("ERROR: {}", $expr); error!("{}", $expr);
return $crate::rocket::Outcome::Failure(($crate::rocket::http::Status::Unauthorized, $expr)); return rocket::Outcome::Failure((rocket::http::Status::Unauthorized, $expr));
}} }}
} }
@@ -92,6 +102,10 @@ pub fn get_display_size(size: i32) -> String {
format!("{} {}", size, UNITS[unit_counter]) format!("{} {}", size, UNITS[unit_counter])
} }
pub fn get_uuid() -> String {
uuid::Uuid::new_v4().to_string()
}
/// ///
/// String util methods /// String util methods
@@ -238,6 +252,33 @@ fn upcase_value(value: &Value) -> Value {
fn _process_key(key: &str) -> String { fn _process_key(key: &str) -> String {
match key.to_lowercase().as_ref() { match key.to_lowercase().as_ref() {
"ssn" => "SSN".into(), "ssn" => "SSN".into(),
_ => self::upcase_first(key) _ => self::upcase_first(key),
}
}
//
// Retry methods
//
pub fn retry<F, T, E>(func: F, max_tries: i32) -> Result<T, E>
where
F: Fn() -> Result<T, E>,
{
use std::{thread::sleep, time::Duration};
let mut tries = 0;
loop {
match func() {
ok @ Ok(_) => return ok,
err @ Err(_) => {
tries += 1;
if tries >= max_tries {
return err;
}
sleep(Duration::from_millis(500));
}
}
} }
} }