Compare commits

..

88 Commits

Author SHA1 Message Date
Daniel García
56b3afa77c Merge pull request #107 from shauder/bug/attachments_for_orgs
Bug/attachments for orgs
2018-07-31 20:08:05 +02:00
Shane A. Faulkner
d335f45e34 Bump version to 0.12.0 2018-07-31 12:07:03 -05:00
Shane A. Faulkner
34d2648509 Merge pull request #3 from shauder/master
Sync working branch with changes in master upstream
2018-07-31 12:05:52 -05:00
Shane A. Faulkner
f39c4fe2f4 Merge pull request #2 from dani-garcia/master
Sync local fork with upstream
2018-07-31 12:03:39 -05:00
Shane A. Faulkner
01875c395b Merge pull request #1 from mprasil/concurrency_fix
WAL journal mode and delete retry added
2018-07-31 11:39:45 -05:00
Miroslav Prasil
2872f40d13 WAL journal mode and delete retry added 2018-07-31 16:43:43 +01:00
mprasil
d7df545078 Merge pull request #104 from jcgruenhage/patch-1
Update matrix.to link in the README
2018-07-26 22:52:12 +01:00
Jan Christian Grünhage
d073f06652 Update matrix.to link in the README
Using the room ID instead of an alias isn't supposed to be working for joining rooms, and doesn't work when joining over federation. It only works when your server is already participating in the room.
2018-07-26 22:42:02 +01:00
Daniel García
3726da9c14 Merge pull request #103 from mprasil/https_doc_fix
Fixed the documentation for https (resolves #101)
2018-07-24 15:28:23 +02:00
Miroslav Prasil
51450a0df9 Fixed the documentation for https (resolves #101) 2018-07-24 12:32:41 +01:00
Shane A. Faulkner
98bae4a0a1 Cleanup and working with 2 or less attachments 2018-07-18 15:35:45 -05:00
Daniel García
48e69cebab Merge pull request #92 from mprasil/not_found
Return 404 in case the path doesn't match instead of 500
2018-07-18 14:07:28 +02:00
Daniel García
798a3b6a43 Merge pull request #91 from mprasil/worker_threads
Change number of workers in image, document the setting (fixes #90)
2018-07-18 14:06:53 +02:00
Miroslav Prasil
2dc1427027 Bump the version 2018-07-18 12:04:48 +01:00
Miroslav Prasil
233d23a527 Return 404 in case the path doesn't match instead of 500 2018-07-18 11:54:33 +01:00
Miroslav Prasil
06f7bd7c97 Change number of workers in image, document the setting (fixes #90) 2018-07-18 10:41:39 +01:00
Daniel García
458a238c38 Merge pull request #89 from mprasil/unconfirmed_guard
Add confirmed check to the OrgHeaders request guard
2018-07-17 11:54:13 +02:00
Miroslav Prasil
de72655bb1 Add confirmed check to the OrgHeaders request guard 2018-07-16 10:23:45 +01:00
Daniel García
4a2350891a Merge pull request #84 from mqus/patch-2
Reflect changes in Archlinux packaging
2018-07-15 12:04:28 +02:00
mqus
4677ae4ac6 Reflect changes in Archlinux packaging
I changed the way bitwarden_rs is packaged (the web interface is now an addon-package instead of bundled) and added a 'stable' package which follows recent releases.
 I assume that following releases instead of the master branch is encouraged so I removed the link to the (still existing) bitwarden_rs-git package which does the latter.
2018-07-15 00:42:17 +02:00
Shane A. Faulkner
31349a47d3 Very dirty addition of missing api's 2018-07-14 01:09:20 -05:00
Daniel García
55b7a3e4d1 Merge pull request #82 from mprasil/not_accepted_user
Do not show organization stuff to not accepted user
2018-07-13 18:42:38 +02:00
Miroslav Prasil
692ed81306 Do not show organization stuff to not accepted user 2018-07-13 17:21:19 +01:00
Daniel García
03172a6cd7 Bump version to 0.10.0 2018-07-13 16:06:01 +02:00
Daniel García
819622e310 Documented U2F, removed debug prints, and documented missing features 2018-07-13 15:58:50 +02:00
Daniel García
970863ffb1 Set facets contentType 2018-07-13 15:05:00 +02:00
Daniel García
e876d3077a Merge remote-tracking branch 'origin/master' into u2f 2018-07-13 13:59:45 +02:00
Daniel García
99d6742fac Merge pull request #81 from mprasil/readme_update
Readme updates (chat, arch, TOC)
2018-07-13 13:58:50 +02:00
Daniel García
75615bb5c8 Ignore U2F challenge if not provided. Also checked that error_code has to be 0 for a successfull registration 2018-07-13 12:37:46 +02:00
Miroslav Prasil
e271b246f3 Readme updates (chat, arch, TOC) 2018-07-13 10:48:49 +01:00
Daniel García
6378d96d1a Add some extra debug prints 2018-07-13 11:07:20 +02:00
Daniel García
c722256cbd Remove debug print 2018-07-13 00:40:59 +02:00
Daniel García
8ff50481e5 Use X-Forwarded-Host if available 2018-07-13 00:33:28 +02:00
Daniel García
be4e6c6f0c Merge branch 'master' into u2f 2018-07-12 23:54:56 +02:00
Daniel García
2f892cb866 Hide org ciphers from unconfirmed users (Showed deciption error) 2018-07-12 23:45:41 +02:00
Daniel García
4f6f510bd4 Improve domain detection, should fix attachment problems. Otherwise, set the DOMAIN env variable to the correct domain 2018-07-12 23:28:16 +02:00
Daniel García
dae92b9018 Implemented U2F, refactored Two Factor authentication, registering U2F device and authentication should work. Works on Chrome on MacOS with a virtual device. 2018-07-12 22:22:10 +02:00
mprasil
dde7c0d99b Merge pull request #74 from mprasil/env_fix
Move the ROCKET_ENV to the runtime image
2018-07-12 13:50:33 +01:00
Miroslav Prasil
79fccccad7 Move the ROCKET_ENV to the runtime image 2018-07-12 13:25:15 +01:00
mprasil
470ad14616 Merge pull request #72 from mqus/patch-1
Add a link to Archlinux AUR packages
2018-07-12 10:53:17 +01:00
Markus Richter
8d13e759fa Merge branch 'master' into patch-1 2018-07-12 11:25:45 +02:00
mqus
3bba02b364 Add aur links to BUILD.md 2018-07-12 11:19:30 +02:00
mqus
251c5c2348 remove aur link in README 2018-07-12 11:09:58 +02:00
mqus
f718827693 Add a link to the packaged AUR version
Hi!
I made two packages for Archlinux, one for the purists (without web interface) and one with the web interface and wanted to link to them here.
I'm not sure if that is the best position to advertise this, let me know if you think the links are better put elsewhere.

Apart from that: If you have any other suggestions/remarks for repackaging, please tell me. 

Currently I'm linking to the latest master commit, but I would really welcome it if you could make a release every now and then.
Thanks for your great work!
2018-07-12 02:23:12 +02:00
Daniel García
869352c361 Merge pull request #70 from dustyrip/dustyrip-patch-1
Enable user to change ROCKET_ENV for container
2018-07-11 21:17:45 +02:00
dustyrip
ca31f117d5 Allow users to specify ROCKET_ENV when starting container 2018-07-11 18:56:28 +00:00
Daniel García
1cb67eee69 Implement leave organization (accessed from the bottom of the user's settings page) 2018-07-11 16:30:03 +02:00
Daniel García
e88d8c856d Change host url to https when it's enabled, should fix some problems downloading attachments 2018-07-11 16:23:39 +02:00
Daniel García
ec37004dfe Merge pull request #68 from mprasil/unprivileged
Document running container with lower privileges (fixes #66)
2018-07-11 16:20:56 +02:00
Miroslav Prasil
03ce42e1cf Document running container with lower privileges (fixes #66) 2018-07-11 11:31:13 +01:00
Daniel García
3f56730b8a Merge pull request #65 from laxmanpradhan/master
updated README to include backup information
2018-07-10 13:41:06 +02:00
laxmanpradhan
57701d5213 typo 2018-07-09 14:51:13 -07:00
laxmanpradhan
f920441b28 updated key files infor 2018-07-09 14:48:51 -07:00
laxmanpradhan
203fb2e3e7 formatting for headings 2018-07-09 14:44:00 -07:00
laxmanpradhan
3c662de4f2 updated to include backup infromation 2018-07-09 14:41:12 -07:00
Daniel García
b1d1926249 Reorganized build instructions 2018-07-09 15:09:30 +02:00
Daniel García
c5dd1a03be Merge pull request #64 from mprasil/master
Update readme for docker hub
2018-07-08 16:19:34 +02:00
Daniel García
df598d7208 Log posible errors when attaching file 2018-07-06 17:23:12 +02:00
Miroslav Prasil
a0ae032ea7 Update readme for docker hub 2018-07-04 17:35:00 +01:00
Daniel García
35b4ad69bd Remove unused warnings 2018-07-04 14:27:47 +02:00
Daniel García
dfb348d630 Install CA certificates to make HTTPS connections work 2018-07-01 17:40:02 +02:00
Daniel García
22786c8c9d Merge pull request #55 from mprasil/debug_prints
Remove some extra debug prints
2018-07-01 16:02:18 +02:00
Daniel García
a1ffa4c28d Allow TOTP generation in organizations (Fixes #50) 2018-07-01 15:49:52 +02:00
Miroslav Prasil
9f8183deb0 Remove some extra debug prints 2018-07-01 14:48:18 +01:00
Daniel García
ea600ab2b8 Don't ignore errors while downloading icons 2018-07-01 15:27:42 +02:00
Daniel García
83da757dfb Merge pull request #54 from mprasil/delete-admin
Implement delete-admin call
2018-07-01 14:45:48 +02:00
Miroslav Prasil
d84d8d756f Implement delete-admin call 2018-07-01 12:43:11 +01:00
Daniel García
4fcdf33621 Merge pull request #47 from mprasil/limits
Add limits configuration to the readme
2018-06-30 17:48:34 +02:00
Miroslav Prasil
400a17a1ce Add limits configuration to the readme 2018-06-30 11:42:35 +01:00
Daniel García
15833e8d95 Added Rocket.toml to the final docker image 2018-06-30 01:51:53 +02:00
Daniel García
7d01947173 Updated dependencies and rust version 2018-06-29 23:18:19 +02:00
Daniel García
6aab2ae6c8 Document configuration a bit and increase JSON size limit to 10MB 2018-06-29 23:11:15 +02:00
Daniel García
64ac81b9ee Added unofficial note and better organized the README file 2018-06-29 14:47:23 +02:00
Daniel García
7c316fc19a Added security headers to web-vault (fixes #44) 2018-06-25 20:35:36 +02:00
Daniel García
1c45c2ec3a Implemented API endpoints to modify profile name and hint, and to change email address, fixes #43 2018-06-17 00:08:05 +02:00
Daniel García
0905355629 Fix wrong case in import struct, invite collections and user Uri back-compat 2018-06-13 14:39:29 +02:00
Daniel García
f24e754ff7 Merge pull request #41 from mprasil/toolchain_build
Use proper toolchain in Dockerfile
2018-06-13 12:08:06 +02:00
Miroslav Prasil
0260667f7a Use proper toolchain in Dockerfile 2018-06-12 23:48:18 +01:00
Daniel García
7983ce4f13 Updated global domains file 2018-06-12 23:24:49 +02:00
Daniel García
5fc0472d88 Removed unneeded cipher code for changing case (fixed by last commit) 2018-06-12 23:15:27 +02:00
Daniel García
410ee9f1f7 Fixed case problems, hopefully this time for real 2018-06-12 23:01:14 +02:00
Daniel García
538dc00234 Improved configuration and documented options. Implemented option to disable web vault and to disable the use of bitwarden's official icon servers 2018-06-12 21:09:42 +02:00
Daniel García
515c84d74d Fixed casing issue 2018-06-12 18:01:11 +02:00
Daniel García
f72efa899e Updated dependencies and created 'rust-toolchain', to mark a working nightly to rustup users, and hopefully avoid some nightly breakage. 2018-06-12 17:30:36 +02:00
Daniel García
483066b9a0 Some style changes, removed useless matches and formats 2018-06-11 15:44:37 +02:00
Daniel García
57850a3379 Fix SSN field in Identity cipher not loading correctly
It needs to be all uppercase otherwise the web vault doesn't load it
2018-06-01 23:16:10 +02:00
Daniel García
3b09750b76 Merge pull request #39 from mprasil/vault_update
Update Vault to 1.27.0
2018-06-01 23:11:10 +02:00
Miroslav Prasil
0da4a8fc8a Update Vault to 1.27.0 2018-06-01 21:24:23 +01:00
33 changed files with 1943 additions and 788 deletions

37
.env
View File

@@ -1,13 +1,40 @@
## Bitwarden_RS Configuration File
## Uncomment any of the following lines to change the defaults
## Main data folder
# DATA_FOLDER=data
## Individual folders, these override %DATA_FOLDER%
# DATABASE_URL=data/db.sqlite3
# PRIVATE_RSA_KEY=data/private_rsa_key.der
# PUBLIC_RSA_KEY=data/public_rsa_key.der
# RSA_KEY_FILENAME=data/rsa_key
# ICON_CACHE_FOLDER=data/icon_cache
# ATTACHMENTS_FOLDER=data/attachments
# true for yes, anything else for no
SIGNUPS_ALLOWED=true
## Web vault settings
# WEB_VAULT_FOLDER=web-vault/
# WEB_VAULT_ENABLED=true
# ROCKET_ENV=production
## Controls if new users can register
# SIGNUPS_ALLOWED=true
## Use a local favicon extractor
## Set to false to use bitwarden's official icon servers
## Set to true to use the local version, which is not as smart,
## but it doesn't send the cipher domains to bitwarden's servers
# LOCAL_ICON_EXTRACTOR=false
## Controls the PBBKDF password iterations to apply on the server
## The change only applies when the password is changed
# PASSWORD_ITERATIONS=100000
## Domain settings
## The domain must match the address from where you access the server
## Unless you are using U2F, or having problems with attachments not downloading, there is no need to change this
## For U2F to work, the server must use HTTPS, you can use Let's Encrypt for free certs
# DOMAIN=https://bw.domain.tld:8443
## Rocket specific settings, check Rocket documentation to learn more
# ROCKET_ENV=staging
# ROCKET_ADDRESS=0.0.0.0 # Enable this to test mobile app
# ROCKET_PORT=8000
# ROCKET_TLS={certs="/path/to/certs.pem",key="/path/to/key.pem"}

View File

@@ -1,25 +1,23 @@
## How to compile bitwarden_rs
Install `rust nightly`, in Windows the recommended way is through `rustup`.
# Build instructions
Install the `openssl` library, in Windows the best option is Microsoft's `vcpkg`,
on other systems use their respective package managers.
## Dependencies
- `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))
- `NodeJS` (required to build the web-vault, (install through your system's package manager or use the [prebuilt binaries](https://nodejs.org/en/download/))
Then run:
## Run/Compile
```sh
# Compile and run
cargo run
# or
cargo build
# or just compile (binary located in target/release/bitwarden_rs)
cargo build --release
```
## How to install the web-vault locally
If you're using docker image, you can just update `VAULT_VERSION` variable in Dockerfile and rebuild the image.
When run, the server is accessible in [http://localhost:80](http://localhost:80).
Install `node.js` and either `yarn` or `npm` (usually included with node)
Clone the web-vault outside the project:
```
git clone https://github.com/bitwarden/web.git web-vault
```
### Install the web-vault
Download the latest official release from the [releases page](https://github.com/bitwarden/web/releases) and extract it.
Modify `web-vault/settings.Production.json` to look like this:
```json
@@ -34,23 +32,23 @@ Modify `web-vault/settings.Production.json` to look like this:
}
```
Then, run the following from the `web-vault` dir:
Then, run the following from the `web-vault` directory:
```sh
# With yarn (recommended)
yarn
yarn gulp dist:selfHosted
# With npm
npm install
npx gulp dist:selfHosted
```
Finally copy the contents of the `web-vault/dist` folder into the `bitwarden_rs/web-vault` folder.
## How to recreate database schemas
# Configuration
The available configuration options are documented in the default `.env` file, and they can be modified by uncommenting the desired options in that file or by setting their respective environment variables. Look at the README file for the main configuration options available.
Note: the environment variables override the values set in the `.env` file.
## How to recreate database schemas (for developers)
Install diesel-cli with cargo:
```sh
cargo install diesel_cli --no-default-features --features sqlite-bundled # Or use only sqlite to use the system version
cargo install diesel_cli --no-default-features --features sqlite-bundled
```
Make sure that the correct path to the database is in the `.env` file.
@@ -63,7 +61,9 @@ diesel migration generate <name>
Modify the *.sql files, making sure that any changes are reverted in the down.sql file.
Apply the migrations and save the generated schemas as follows:
```
```sh
diesel migration redo
diesel print-schema > src/db/schema.rs
```
# This step should be done automatically when using diesel-cli > 1.3.0
# diesel print-schema > src/db/schema.rs
```

651
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,13 @@
[package]
name = "bitwarden_rs"
version = "0.9.0"
version = "0.12.0"
authors = ["Daniel García <dani-garcia@users.noreply.github.com>"]
[dependencies]
# Web framework for nightly with a focus on ease-of-use, expressibility, and speed.
rocket = { version = "0.3.12", features = ["tls"] }
rocket_codegen = "0.3.12"
rocket_contrib = "0.3.12"
rocket = { version = "0.3.14", features = ["tls"] }
rocket_codegen = "0.3.14"
rocket_contrib = "0.3.14"
# HTTP client
reqwest = "0.8.6"
@@ -16,13 +16,13 @@ reqwest = "0.8.6"
multipart = "0.14.2"
# A generic serialization/deserialization framework
serde = "1.0.64"
serde_derive = "1.0.64"
serde_json = "1.0.19"
serde = "1.0.70"
serde_derive = "1.0.70"
serde_json = "1.0.22"
# A safe, extensible ORM and Query builder
diesel = { version = "~1.2.2", features = ["sqlite", "chrono", "r2d2"] }
diesel_migrations = { version = "~1.2.0", features = ["sqlite"] }
diesel = { version = "1.3.2", features = ["sqlite", "chrono", "r2d2"] }
diesel_migrations = { version = "1.3.0", features = ["sqlite"] }
# Bundled SQLite
libsqlite3-sys = { version = "0.9.1", features = ["bundled"] }
@@ -34,7 +34,7 @@ ring = { version = "= 0.11.0", features = ["rsa_signing"] }
uuid = { version = "0.6.5", features = ["v4"] }
# Date and time library for Rust
chrono = "0.4.2"
chrono = "0.4.4"
# TOTP library
oath = "0.10.2"
@@ -45,11 +45,22 @@ data-encoding = "2.1.1"
# JWT library
jsonwebtoken = "= 4.0.1"
# U2F library
u2f = "0.1.2"
# A `dotenv` implementation for Rust
dotenv = { version = "0.13.0", default-features = false }
# Lazy static macro
lazy_static = "1.0.1"
# Numerical libraries
num-traits = "0.2.5"
num-derive = "0.2.2"
[patch.crates-io]
jsonwebtoken = { path = "libs/jsonwebtoken" } # Make jwt use ring 0.11, to match rocket
# Make jwt use ring 0.11, to match rocket
jsonwebtoken = { path = "libs/jsonwebtoken" }
# 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' }

View File

@@ -4,7 +4,7 @@
####################### VAULT BUILD IMAGE #######################
FROM node:9-alpine as vault
ENV VAULT_VERSION "1.26.0"
ENV VAULT_VERSION "1.27.0"
ENV URL "https://github.com/bitwarden/web/archive/v${VAULT_VERSION}.tar.gz"
RUN apk add --update-cache --upgrade \
@@ -31,7 +31,7 @@ RUN git config --global url."https://github.com/".insteadOf ssh://git@github.com
########################## BUILD IMAGE ##########################
# We need to use the Rust build image, because
# we need the Rust compiler and Cargo tooling
FROM rustlang/rust:nightly as build
FROM rust as build
# Using bundled SQLite, no need to install it
# RUN apt-get update && apt-get install -y\
@@ -46,6 +46,7 @@ WORKDIR /app
# Copies over *only* your manifests and vendored dependencies
COPY ./Cargo.* ./
COPY ./libs ./libs
COPY ./rust-toolchain ./rust-toolchain
# Builds your dependencies and removes the
# dummy project, except the target folder
@@ -66,9 +67,13 @@ RUN cargo build --release
# because we already have a binary built
FROM debian:stretch-slim
ENV ROCKET_ENV "staging"
ENV ROCKET_WORKERS=10
# Install needed libraries
RUN apt-get update && apt-get install -y\
openssl\
ca-certificates\
--no-install-recommends\
&& rm -rf /var/lib/apt/lists/*
@@ -79,10 +84,9 @@ EXPOSE 80
# Copies the files from the context (env file and web-vault)
# and the binary from the "build" stage to the current stage
COPY .env .
COPY Rocket.toml .
COPY --from=vault /web-vault ./web-vault
COPY --from=build app/target/release/bitwarden_rs .
# Configures the startup!
# Use production to disable Rocket logging
#CMD ROCKET_ENV=production ./bitwarden_rs
CMD ROCKET_ENV=staging ./bitwarden_rs
CMD ./bitwarden_rs

176
README.md
View File

@@ -4,6 +4,37 @@ Image is based on [Rust implementation of Bitwarden API](https://github.com/dani
_*Note, that this project is not associated with the [Bitwarden](https://bitwarden.com/) project nor 8bit Solutions LLC._
**Table of contents**
- [Features](#features)
- [Missing features](#missing-features)
- [Docker image usage](#docker-image-usage)
- [Starting a container](#starting-a-container)
- [Updating the bitwarden image](#updating-the-bitwarden-image)
- [Configuring bitwarden service](#configuring-bitwarden-service)
- [Disable registration of new users](#disable-registration-of-new-users)
- [Enabling HTTPS](#enabling-https)
- [Enabling U2F authentication](#enabling-u2f-authentication)
- [Changing persistent data location](#changing-persistent-data-location)
- [/data prefix:](#data-prefix)
- [database name and location](#database-name-and-location)
- [attachments location](#attachments-location)
- [icons cache](#icons-cache)
- [Changing the API request size limit](#changing-the-api-request-size-limit)
- [Changing the number of workers](#changing-the-number-of-workers)
- [Other configuration](#other-configuration)
- [Building your own image](#building-your-own-image)
- [Building binary](#building-binary)
- [Available packages](#available-packages)
- [Arch Linux](#arch-linux)
- [Backing up your vault](#backing-up-your-vault)
- [1. the sqlite3 database](#1-the-sqlite3-database)
- [2. the attachments folder](#2-the-attachments-folder)
- [3. the key files](#3-the-key-files)
- [4. Icon Cache](#4-icon-cache)
- [Running the server with non-root user](#running-the-server-with-non-root-user)
- [Get in touch](#get-in-touch)
## Features
Basically full implementation of Bitwarden API is provided including:
@@ -14,6 +45,14 @@ Basically full implementation of Bitwarden API is provided including:
* Vault API support
* Serving the static files for Vault interface
* Website icons API
* Authenticator and U2F support
## Missing features
* Email confirmation
* Other two-factor systems:
* YubiKey OTP (if your key supports U2F, you can use that)
* Duo
* Email codes
## Docker image usage
@@ -44,6 +83,7 @@ docker rm bitwarden
# Start new container with the data mounted
docker run -d --name bitwarden -v /bw-data/:/data/ -p 80:80 mprasil/bitwarden:latest
```
Then visit [http://localhost:80](http://localhost:80)
In case you didn't bind mount the volume for persistent data, you need an intermediate step where you preserve the data with an intermediate container:
@@ -69,6 +109,56 @@ docker rm bitwarden_data
## Configuring bitwarden service
### Disable registration of new users
By default new users can register, if you want to disable that, set the `SIGNUPS_ALLOWED` env variable to `false`:
```sh
docker run -d --name bitwarden \
-e SIGNUPS_ALLOWED=false \
-v /bw-data/:/data/ \
-p 80:80 \
mprasil/bitwarden:latest
```
### Enabling HTTPS
To enable HTTPS, you need to configure the `ROCKET_TLS`.
The values to the option must follow the format:
```
ROCKET_TLS={certs="/path/to/certs.pem",key="/path/to/key.pem"}
```
Where:
- certs: a path to a certificate chain in PEM format
- key: a path to a private key file in PEM format for the certificate in certs
```sh
docker run -d --name bitwarden \
-e ROCKET_TLS={certs='"/ssl/certs.pem",key="/ssl/key.pem"}' \
-v /ssl/keys/:/ssl/ \
-v /bw-data/:/data/ \
-v /icon_cache/ \
-p 443:80 \
mprasil/bitwarden:latest
```
Note that you need to mount ssl files and you need to forward appropriate port.
### Enabling U2F authentication
To enable U2F authentication, you must be serving bitwarden_rs from an HTTPS domain with a valid certificate (Either using the included
HTTPS options or with a reverse proxy). We recommend using a free certificate from Let's Encrypt.
After that, you need to set the `DOMAIN` environment variable to the same address from where bitwarden_rs is being served:
```sh
docker run -d --name bitwarden \
-e DOMAIN=https://bw.domain.tld \
-v /bw-data/:/data/ \
-p 80:80 \
mprasil/bitwarden:latest
```
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`.
### Changing persistent data location
#### /data prefix:
@@ -128,7 +218,35 @@ docker run -d --name bitwarden \
mprasil/bitwarden:latest
```
Note, that in the above example we don't mount the volume locally, which means it won't be persisted during the upgrade unless you use intermediate data container using `--volumes-from`. This will impact performance as bitwarden will have to re-dowload the icons on restart, but might save you from having stale icons in cache as they are not automatically cleaned.
Note, that in the above example we don't mount the volume locally, which means it won't be persisted during the upgrade unless you use intermediate data container using `--volumes-from`. This will impact performance as bitwarden will have to re-download the icons on restart, but might save you from having stale icons in cache as they are not automatically cleaned.
### Changing the API request size limit
By default the API calls are limited to 10MB. This should be sufficient for most cases, however if you want to support large imports, this might be limiting you. On the other hand you might want to limit the request size to something smaller than that to prevent API abuse and possible DOS attack, especially if running with limited resources.
To set the limit, you can use the `ROCKET_LIMITS` variable. Example here shows 10MB limit for posted json in the body (this is the default):
```sh
docker run -d --name bitwarden \
-e ROCKET_LIMITS={json=10485760} \
-v /bw-data/:/data/ \
-p 80:80 \
mprasil/bitwarden:latest
```
### Changing the number of workers
When you run bitwarden_rs, it spawns `2 * <number of cpu cores>` workers to handle requests. On some systems this might lead to low number of workers and hence slow performance, so the default in the docker image is changed to spawn 10 threads. You can override this setting to increase or decrease the number of workers by setting the `ROCKET_WORKERS` variable.
In the example bellow, we're starting with 20 workers:
```sh
docker run -d --name bitwarden \
-e ROCKET_WORKERS=20 \
-v /bw-data/:/data/ \
-p 80:80 \
mprasil/bitwarden:latest
```
### Other configuration
@@ -145,4 +263,58 @@ docker build -t bitwarden_rs .
## Building binary
For building binary outside the Docker environment and running it locally without docker, please see [build instructions](BUILD.md).
For building binary outside the Docker environment and running it locally without docker, please see [build instructions](BUILD.md).
## Available packages
### Arch Linux
Bitwarden_rs is already packaged for Archlinux thanks to @mqus. There is an [AUR package](https://aur.archlinux.org/packages/bitwarden_rs) (optionally with the [vault web interface](https://aur.archlinux.org/packages/bitwarden_rs-vault/) ) available.
## Backing up your vault
### 1. the sqlite3 database
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.
```
sqlite3 /$DATA_FOLDER/db.sqlite3 ".backup '/$DATA_FOLDER/db-backup/backup.sq3'"
```
This command can be run via a CRON job everyday, however note that it will overwrite the same backup.sq3 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.
### 2. the attachments folder
By default, this is located in `$DATA_FOLDER/attachments`
### 3. the key files
This is optional, these are only used to store tokens of users currently logged in, deleting them would simply log each user out forcing them to log in again. By default, these are located in the `$DATA_FOLDER` (by default /data in the docker). There are 3 files: rsa_key.der, rsa_key.pem, rsa_key.pub.der.
### 4. Icon Cache
This is optional, the icon cache can re-download itself however if you have a large cache, it may take a long time. By default it is located in `$DATA_FOLDER/icon_cache`
## Running the server with non-root user
The root user inside the container is already pretty limited in what it can do, so the default setup should be secure enough. However if you wish to go the extra mile to avoid using root even in container, here's how you can do that:
1. Create a data folder that's owned by non-root user, so you can use that user to write persistent data. Get the user `id`. In linux you can run `stat <folder_name>` to get/verify the owner ID.
2. When you run the container, you need to provide the user ID as one of the parameters. Note that this needs to be in the numeric form and not the user name, because docker would try to find such user defined inside the image, which would likely not be there or it would have different ID than your local user and hence wouldn't be able to write the persistent data. This can be done with the `--user` parameter.
3. bitwarden_rs listens on port `80` inside the container by default, this [won't work with non-root user](https://www.w3.org/Daemon/User/Installation/PrivilegedPorts.html), because regular users aren't allowed to open port bellow `1024`. To overcome this, you need to configure server to listen on a different port, you can use `ROCKET_PORT` to do that.
Here's sample docker run, that uses user with id `1000` and with the port redirection configured, so that inside container the service is listening on port `8080` and docker translates that to external (host) port `80`:
```sh
docker run -d --name bitwarden \
--user 1000 \
-e ROCKET_PORT=8080 \
-v /bw-data/:/data/ \
-p 80:8080 \
mprasil/bitwarden:latest
```
## Get in touch
To ask an question, [raising an issue](https://github.com/dani-garcia/bitwarden_rs/issues/new) is fine, also please report any bugs spotted here.
If you prefer to chat, we're usually hanging around at [#bitwarden_rs:matrix.org](https://matrix.to/#/#bitwarden_rs:matrix.org) room on Matrix. Feel free to join us!

2
Rocket.toml Normal file
View File

@@ -0,0 +1,2 @@
[global.limits]
json = 10485760 # 10 MiB

5
diesel.toml Normal file
View File

@@ -0,0 +1,5 @@
# For documentation on how to configure this file,
# see diesel.rs/guides/configuring-diesel-cli
[print_schema]
file = "src/db/schema.rs"

View File

@@ -0,0 +1,8 @@
UPDATE users
SET totp_secret = (
SELECT twofactor.data FROM twofactor
WHERE twofactor.type = 0
AND twofactor.user_uuid = users.uuid
);
DROP TABLE twofactor;

View File

@@ -0,0 +1,15 @@
CREATE TABLE twofactor (
uuid TEXT NOT NULL PRIMARY KEY,
user_uuid TEXT NOT NULL REFERENCES users (uuid),
type INTEGER NOT NULL,
enabled BOOLEAN NOT NULL,
data TEXT NOT NULL,
UNIQUE (user_uuid, type)
);
INSERT INTO twofactor (uuid, user_uuid, type, enabled, data)
SELECT lower(hex(randomblob(16))) , uuid, 0, 1, u.totp_secret FROM users u where u.totp_secret IS NOT NULL;
UPDATE users SET totp_secret = NULL; -- Instead of recreating the table, just leave the columns empty

1
rust-toolchain Normal file
View File

@@ -0,0 +1 @@
nightly-2018-06-26

View File

@@ -3,11 +3,9 @@ use rocket_contrib::Json;
use db::DbConn;
use db::models::*;
use api::{PasswordData, JsonResult, EmptyResult, JsonUpcase};
use api::{PasswordData, JsonResult, EmptyResult, JsonUpcase, NumberOrString};
use auth::Headers;
use util;
use CONFIG;
#[derive(Deserialize, Debug)]
@@ -15,7 +13,6 @@ use CONFIG;
struct RegisterData {
Email: String,
Key: String,
#[serde(deserialize_with = "util::upcase_deserialize")]
Keys: Option<KeysData>,
MasterPasswordHash: String,
MasterPasswordHint: Option<String>,
@@ -34,10 +31,10 @@ fn register(data: JsonUpcase<RegisterData>, conn: DbConn) -> EmptyResult {
let data: RegisterData = data.into_inner().data;
if !CONFIG.signups_allowed {
err!(format!("Signups not allowed"))
err!("Signups not allowed")
}
if let Some(_) = User::find_by_mail(&data.Email, &conn) {
if User::find_by_mail(&data.Email, &conn).is_some() {
err!("Email already exists")
}
@@ -67,6 +64,28 @@ fn profile(headers: Headers, conn: DbConn) -> JsonResult {
Ok(Json(headers.user.to_json(&conn)))
}
#[derive(Deserialize, Debug)]
#[allow(non_snake_case)]
struct ProfileData {
#[serde(rename = "Culture")]
_Culture: String, // Ignored, always use en-US
MasterPasswordHint: Option<String>,
Name: String,
}
#[post("/accounts/profile", data = "<data>")]
fn post_profile(data: JsonUpcase<ProfileData>, headers: Headers, conn: DbConn) -> JsonResult {
let data: ProfileData = data.into_inner().data;
let mut user = headers.user;
user.name = data.Name;
user.password_hint = data.MasterPasswordHint;
user.save(&conn);
Ok(Json(user.to_json(&conn)))
}
#[get("/users/<uuid>/public-key")]
fn get_public_keys(uuid: String, _headers: Headers, conn: DbConn) -> JsonResult {
let user = match User::find_by_uuid(&uuid, &conn) {
@@ -136,13 +155,39 @@ fn post_sstamp(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -
#[derive(Deserialize)]
#[allow(non_snake_case)]
struct ChangeEmailData {
struct EmailTokenData {
MasterPasswordHash: String,
NewEmail: String,
}
#[post("/accounts/email-token", data = "<data>")]
fn post_email_token(data: JsonUpcase<EmailTokenData>, headers: Headers, conn: DbConn) -> EmptyResult {
let data: EmailTokenData = data.into_inner().data;
if !headers.user.check_valid_password(&data.MasterPasswordHash) {
err!("Invalid password")
}
if User::find_by_mail(&data.NewEmail, &conn).is_some() {
err!("Email already in use");
}
Ok(())
}
#[derive(Deserialize)]
#[allow(non_snake_case)]
struct ChangeEmailData {
MasterPasswordHash: String,
NewEmail: String,
Key: String,
NewMasterPasswordHash: String,
#[serde(rename = "Token")]
_Token: NumberOrString,
}
#[post("/accounts/email", data = "<data>")]
fn post_email(data: JsonUpcase<ChangeEmailData>, headers: Headers, conn: DbConn) -> EmptyResult {
let data: ChangeEmailData = data.into_inner().data;
let mut user = headers.user;
@@ -156,6 +201,10 @@ fn post_email(data: JsonUpcase<ChangeEmailData>, headers: Headers, conn: DbConn)
}
user.email = data.NewEmail;
user.set_password(&data.NewMasterPasswordHash);
user.key = data.Key;
user.save(&conn);
Ok(())
@@ -172,17 +221,15 @@ fn delete_account(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn
// Delete ciphers and their attachments
for cipher in Cipher::find_owned_by_user(&user.uuid, &conn) {
match cipher.delete(&conn) {
Ok(()) => (),
Err(_) => err!("Failed deleting cipher")
if cipher.delete(&conn).is_err() {
err!("Failed deleting cipher")
}
}
// Delete folders
for f in Folder::find_by_user(&user.uuid, &conn) {
match f.delete(&conn) {
Ok(()) => (),
Err(_) => err!("Failed deleting folder")
if f.delete(&conn).is_err() {
err!("Failed deleting folder")
}
}

View File

@@ -14,7 +14,6 @@ use data_encoding::HEXLOWER;
use db::DbConn;
use db::models::*;
use util;
use crypto;
use api::{self, PasswordData, JsonResult, EmptyResult, JsonUpcase};
@@ -157,24 +156,6 @@ fn update_cipher_from_data(cipher: &mut Cipher, data: CipherData, headers: &Head
}
}
let uppercase_fields = data.Fields.map(|f| {
let mut value = json!({});
// Copy every field object and change the names to the correct case
copy_values(&f, &mut value);
value
});
// TODO: ******* Backwards compat start **********
// To remove backwards compatibility, just create an empty values object,
// and remove the compat code from cipher::to_json
let mut values = json!({
"Name": data.Name,
"Notes": data.Notes
});
values["Fields"] = uppercase_fields.clone().unwrap_or(Value::Null);
// TODO: ******* Backwards compat end **********
let type_data_opt = match data.Type {
1 => data.Login,
2 => data.SecureNote,
@@ -183,19 +164,24 @@ fn update_cipher_from_data(cipher: &mut Cipher, data: CipherData, headers: &Head
_ => err!("Invalid type")
};
let type_data = match type_data_opt {
let mut type_data = match type_data_opt {
Some(data) => data,
None => err!("Data missing")
};
// Copy the type data and change the names to the correct case
copy_values(&type_data, &mut values);
// TODO: ******* Backwards compat start **********
// To remove backwards compatibility, just delete this code,
// and remove the compat code from cipher::to_json
type_data["Name"] = Value::String(data.Name.clone());
type_data["Notes"] = data.Notes.clone().map(Value::String).unwrap_or(Value::Null);
type_data["Fields"] = data.Fields.clone().unwrap_or(Value::Null);
// TODO: ******* Backwards compat end **********
cipher.favorite = data.Favorite.unwrap_or(false);
cipher.name = data.Name;
cipher.notes = data.Notes;
cipher.fields = uppercase_fields.map(|f| f.to_string());
cipher.data = values.to_string();
cipher.fields = data.Fields.map(|f| f.to_string());
cipher.data = type_data.to_string();
cipher.save(&conn);
@@ -206,23 +192,6 @@ fn update_cipher_from_data(cipher: &mut Cipher, data: CipherData, headers: &Head
Ok(())
}
fn copy_values(from: &Value, to: &mut Value) {
if let Some(map) = from.as_object() {
for (key, val) in map {
copy_values(val, &mut to[util::upcase_first(key)]);
}
} else if let Some(array) = from.as_array() {
// Initialize array with null values
*to = json!(vec![Value::Null; array.len()]);
for (index, val) in array.iter().enumerate() {
copy_values(val, &mut to[index]);
}
} else {
*to = from.clone();
}
}
use super::folders::FolderData;
#[derive(Deserialize)]
@@ -237,9 +206,9 @@ struct ImportData {
#[allow(non_snake_case)]
struct RelationsData {
// Cipher id
key: u32,
Key: usize,
// Folder id
value: u32,
Value: usize,
}
@@ -259,21 +228,18 @@ fn post_ciphers_import(data: JsonUpcase<ImportData>, headers: Headers, conn: DbC
let mut relations_map = HashMap::new();
for relation in data.FolderRelationships {
relations_map.insert(relation.key, relation.value);
relations_map.insert(relation.Key, relation.Value);
}
// Read and create the ciphers
let mut index = 0;
for cipher_data in data.Ciphers {
for (index, cipher_data) in data.Ciphers.into_iter().enumerate() {
let folder_uuid = relations_map.get(&index)
.map(|i| folders[*i as usize].uuid.clone());
.map(|i| folders[*i].uuid.clone());
let mut cipher = Cipher::new(cipher_data.Type, cipher_data.Name.clone());
update_cipher_from_data(&mut cipher, cipher_data, &headers, true, &conn)?;
cipher.move_to_folder(folder_uuid, &headers.user.uuid.clone(), &conn).ok();
index += 1;
}
Ok(())
@@ -358,7 +324,6 @@ fn post_collections_admin(uuid: String, data: JsonUpcase<CollectionsAdminData>,
#[derive(Deserialize)]
#[allow(non_snake_case)]
struct ShareCipherData {
#[serde(deserialize_with = "util::upcase_deserialize")]
Cipher: CipherData,
CollectionIds: Vec<String>,
}
@@ -382,8 +347,8 @@ fn post_cipher_share(uuid: String, data: JsonUpcase<ShareCipherData>, headers: H
None => err!("Organization id not provided"),
Some(_) => {
update_cipher_from_data(&mut cipher, data.Cipher, &headers, true, &conn)?;
for collection in data.CollectionIds.iter() {
match Collection::find_by_uuid(&collection, &conn) {
for uuid in &data.CollectionIds {
match Collection::find_by_uuid(uuid, &conn) {
None => err!("Invalid collection ID provided"),
Some(collection) => {
if collection.is_writable_by_user(&headers.user.uuid, &conn) {
@@ -418,7 +383,8 @@ fn post_attachment(uuid: String, data: Data, content_type: &ContentType, headers
let base_path = Path::new(&CONFIG.attachments_folder).join(&cipher.uuid);
Multipart::with_body(data.open(), boundary).foreach_entry(|mut field| {
let name = field.headers.filename.unwrap(); // This is provided by the client, don't trust it
// This is provided by the client, don't trust it
let name = field.headers.filename.expect("No filename provided");
let file_name = HEXLOWER.encode(&crypto::get_random(vec![0; 10]));
let path = base_path.join(&file_name);
@@ -428,17 +394,43 @@ fn post_attachment(uuid: String, data: Data, content_type: &ContentType, headers
.size_limit(None)
.with_path(path) {
SaveResult::Full(SavedData::File(_, size)) => size as i32,
_ => return
SaveResult::Full(other) => {
println!("Attachment is not a file: {:?}", other);
return;
},
SaveResult::Partial(_, reason) => {
println!("Partial result: {:?}", reason);
return;
},
SaveResult::Error(e) => {
println!("Error: {:?}", e);
return;
}
};
let attachment = Attachment::new(file_name, cipher.uuid.clone(), name, size);
println!("Attachment: {:#?}", attachment);
attachment.save(&conn);
}).expect("Error processing multipart data");
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, &conn)))
}
#[post("/ciphers/<uuid>/attachment-admin", format = "multipart/form-data", data = "<data>")]
fn post_attachment_admin(uuid: String, data: Data, content_type: &ContentType, headers: Headers, conn: DbConn) -> JsonResult {
post_attachment(uuid, data, content_type, headers, conn)
}
#[post("/ciphers/<uuid>/attachment/<attachment_id>/share", format = "multipart/form-data", data = "<data>")]
fn post_attachment_share(uuid: String, attachment_id: String, data: Data, content_type: &ContentType, headers: Headers, conn: DbConn) -> JsonResult {
_delete_cipher_attachment_by_id(&uuid, &attachment_id, &headers, &conn)?;
post_attachment(uuid, data, content_type, headers, conn)
}
#[post("/ciphers/<uuid>/attachment/<attachment_id>/delete-admin")]
fn delete_attachment_post_admin(uuid: String, attachment_id: String, headers: Headers, conn: DbConn) -> EmptyResult {
delete_attachment(uuid, attachment_id, headers, conn)
}
#[post("/ciphers/<uuid>/attachment/<attachment_id>/delete")]
fn delete_attachment_post(uuid: String, attachment_id: String, headers: Headers, conn: DbConn) -> EmptyResult {
delete_attachment(uuid, attachment_id, headers, conn)
@@ -446,29 +438,7 @@ fn delete_attachment_post(uuid: String, attachment_id: String, headers: Headers,
#[delete("/ciphers/<uuid>/attachment/<attachment_id>")]
fn delete_attachment(uuid: String, attachment_id: String, headers: Headers, conn: DbConn) -> EmptyResult {
let attachment = match Attachment::find_by_id(&attachment_id, &conn) {
Some(attachment) => attachment,
None => err!("Attachment doesn't exist")
};
if attachment.cipher_uuid != uuid {
err!("Attachment from other cipher")
}
let cipher = match Cipher::find_by_uuid(&uuid, &conn) {
Some(cipher) => cipher,
None => err!("Cipher doesn't exist")
};
if !cipher.is_write_accessible_to_user(&headers.user.uuid, &conn) {
err!("Cipher cannot be deleted by user")
}
// Delete attachment
match attachment.delete(&conn) {
Ok(()) => Ok(()),
Err(_) => err!("Deleting attachement failed")
}
_delete_cipher_attachment_by_id(&uuid, &attachment_id, &headers, &conn)
}
#[post("/ciphers/<uuid>/delete")]
@@ -476,6 +446,11 @@ fn delete_cipher_post(uuid: String, headers: Headers, conn: DbConn) -> EmptyResu
_delete_cipher_by_uuid(&uuid, &headers, &conn)
}
#[post("/ciphers/<uuid>/delete-admin")]
fn delete_cipher_post_admin(uuid: String, headers: Headers, conn: DbConn) -> EmptyResult {
_delete_cipher_by_uuid(&uuid, &headers, &conn)
}
#[delete("/ciphers/<uuid>")]
fn delete_cipher(uuid: String, headers: Headers, conn: DbConn) -> EmptyResult {
_delete_cipher_by_uuid(&uuid, &headers, &conn)
@@ -567,17 +542,15 @@ fn delete_all(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) ->
// Delete ciphers and their attachments
for cipher in Cipher::find_owned_by_user(&user.uuid, &conn) {
match cipher.delete(&conn) {
Ok(()) => (),
Err(_) => err!("Failed deleting cipher")
if cipher.delete(&conn).is_err() {
err!("Failed deleting cipher")
}
}
// Delete folders
for f in Folder::find_by_user(&user.uuid, &conn) {
match f.delete(&conn) {
Ok(()) => (),
Err(_) => err!("Failed deleting folder")
if f.delete(&conn).is_err() {
err!("Failed deleting folder")
}
}
@@ -599,3 +572,29 @@ fn _delete_cipher_by_uuid(uuid: &str, headers: &Headers, conn: &DbConn) -> Empty
Err(_) => err!("Failed deleting cipher")
}
}
fn _delete_cipher_attachment_by_id(uuid: &str, attachment_id: &str, headers: &Headers, conn: &DbConn) -> EmptyResult {
let attachment = match Attachment::find_by_id(&attachment_id, &conn) {
Some(attachment) => attachment,
None => err!("Attachment doesn't exist")
};
if attachment.cipher_uuid != uuid {
err!("Attachment from other cipher")
}
let cipher = match Cipher::find_by_uuid(&uuid, &conn) {
Some(cipher) => cipher,
None => err!("Cipher doesn't exist")
};
if !cipher.is_write_accessible_to_user(&headers.user.uuid, &conn) {
err!("Cipher cannot be deleted by user")
}
// Delete attachment
match attachment.delete(&conn) {
Ok(()) => Ok(()),
Err(_) => err!("Deleting attachement failed")
}
}

View File

@@ -646,5 +646,121 @@
"wiktionary.org"
],
"Excluded": false
},
{
"Type": 72,
"Domains": [
"airbnb.at",
"airbnb.be",
"airbnb.ca",
"airbnb.ch",
"airbnb.cl",
"airbnb.co.cr",
"airbnb.co.id",
"airbnb.co.in",
"airbnb.co.kr",
"airbnb.co.nz",
"airbnb.co.uk",
"airbnb.co.ve",
"airbnb.com",
"airbnb.com.ar",
"airbnb.com.au",
"airbnb.com.bo",
"airbnb.com.br",
"airbnb.com.bz",
"airbnb.com.co",
"airbnb.com.ec",
"airbnb.com.gt",
"airbnb.com.hk",
"airbnb.com.hn",
"airbnb.com.mt",
"airbnb.com.my",
"airbnb.com.ni",
"airbnb.com.pa",
"airbnb.com.pe",
"airbnb.com.py",
"airbnb.com.sg",
"airbnb.com.sv",
"airbnb.com.tr",
"airbnb.com.tw",
"airbnb.cz",
"airbnb.de",
"airbnb.dk",
"airbnb.es",
"airbnb.fi",
"airbnb.fr",
"airbnb.gr",
"airbnb.gy",
"airbnb.hu",
"airbnb.ie",
"airbnb.is",
"airbnb.it",
"airbnb.jp",
"airbnb.mx",
"airbnb.nl",
"airbnb.no",
"airbnb.pl",
"airbnb.pt",
"airbnb.ru",
"airbnb.se"
],
"Excluded": false
},
{
"Type": 73,
"Domains": [
"eventbrite.at",
"eventbrite.be",
"eventbrite.ca",
"eventbrite.ch",
"eventbrite.cl",
"eventbrite.co.id",
"eventbrite.co.in",
"eventbrite.co.kr",
"eventbrite.co.nz",
"eventbrite.co.uk",
"eventbrite.co.ve",
"eventbrite.com",
"eventbrite.com.au",
"eventbrite.com.bo",
"eventbrite.com.br",
"eventbrite.com.co",
"eventbrite.com.hk",
"eventbrite.com.hn",
"eventbrite.com.pe",
"eventbrite.com.sg",
"eventbrite.com.tr",
"eventbrite.com.tw",
"eventbrite.cz",
"eventbrite.de",
"eventbrite.dk",
"eventbrite.fi",
"eventbrite.fr",
"eventbrite.gy",
"eventbrite.hu",
"eventbrite.ie",
"eventbrite.is",
"eventbrite.it",
"eventbrite.jp",
"eventbrite.mx",
"eventbrite.nl",
"eventbrite.no",
"eventbrite.pl",
"eventbrite.pt",
"eventbrite.ru",
"eventbrite.se"
],
"Excluded": false
},
{
"Type": 74,
"Domains": [
"stackexchange.com",
"superuser.com",
"stackoverflow.com",
"serverfault.com",
"mathoverflow.net"
],
"Excluded": false
}
]

View File

@@ -2,7 +2,7 @@ mod accounts;
mod ciphers;
mod folders;
mod organizations;
mod two_factor;
pub(crate) mod two_factor;
use self::accounts::*;
use self::ciphers::*;
@@ -14,10 +14,12 @@ pub fn routes() -> Vec<Route> {
routes![
register,
profile,
post_profile,
get_public_keys,
post_keys,
post_password,
post_sstamp,
post_email_token,
post_email,
delete_account,
revision_date,
@@ -32,13 +34,17 @@ pub fn routes() -> Vec<Route> {
post_ciphers_admin,
post_ciphers_import,
post_attachment,
post_attachment_admin,
post_attachment_share,
delete_attachment_post,
delete_attachment_post_admin,
delete_attachment,
post_cipher_admin,
post_cipher_share,
post_cipher,
put_cipher,
delete_cipher_post,
delete_cipher_post_admin,
delete_cipher,
delete_cipher_selected,
delete_all,
@@ -55,13 +61,16 @@ pub fn routes() -> Vec<Route> {
get_twofactor,
get_recover,
recover,
disable_twofactor,
generate_authenticator,
activate_authenticator,
disable_authenticator,
generate_u2f,
activate_u2f,
get_organization,
create_organization,
delete_organization,
leave_organization,
get_user_collections,
get_org_collections,
get_org_collection_detail,
@@ -106,8 +115,7 @@ use auth::Headers;
#[put("/devices/identifier/<uuid>/clear-token", data = "<data>")]
fn clear_device_token(uuid: String, data: Json<Value>, headers: Headers, conn: DbConn) -> EmptyResult {
println!("UUID: {:#?}", uuid);
println!("DATA: {:#?}", data);
let _data: Value = data.into_inner();
let device = match Device::find_by_uuid(&uuid, &conn) {
Some(device) => device,
@@ -125,9 +133,8 @@ fn clear_device_token(uuid: String, data: Json<Value>, headers: Headers, conn: D
#[put("/devices/identifier/<uuid>/token", data = "<data>")]
fn put_device_token(uuid: String, data: Json<Value>, headers: Headers, conn: DbConn) -> JsonResult {
println!("UUID: {:#?}", uuid);
println!("DATA: {:#?}", data);
let _data: Value = data.into_inner();
let device = match Device::find_by_uuid(&uuid, &conn) {
Some(device) => device,
None => err!("Device not found")
@@ -150,7 +157,7 @@ struct GlobalDomain {
Excluded: bool,
}
const GLOBAL_DOMAINS: &'static str = include_str!("global_domains.json");
const GLOBAL_DOMAINS: &str = include_str!("global_domains.json");
#[get("/settings/domains")]
fn get_eq_domains(headers: Headers) -> JsonResult {
@@ -185,8 +192,8 @@ struct EquivDomainData {
fn post_eq_domains(data: JsonUpcase<EquivDomainData>, headers: Headers, conn: DbConn) -> EmptyResult {
let data: EquivDomainData = data.into_inner().data;
let excluded_globals = data.ExcludedGlobalEquivalentDomains.unwrap_or(Vec::new());
let equivalent_domains = data.EquivalentDomains.unwrap_or(Vec::new());
let excluded_globals = data.ExcludedGlobalEquivalentDomains.unwrap_or_default();
let equivalent_domains = data.EquivalentDomains.unwrap_or_default();
let mut user = headers.user;
use serde_json::to_string;

View File

@@ -73,6 +73,29 @@ fn delete_organization(org_id: String, data: JsonUpcase<PasswordData>, headers:
}
}
#[post("/organizations/<org_id>/leave")]
fn leave_organization(org_id: String, headers: Headers, conn: DbConn) -> EmptyResult {
match UserOrganization::find_by_user_and_org(&headers.user.uuid, &org_id, &conn) {
None => err!("User not part of organization"),
Some(user_org) => {
if user_org.type_ == UserOrgType::Owner as i32 {
let num_owners = UserOrganization::find_by_org_and_type(
&org_id, UserOrgType::Owner as i32, &conn)
.len();
if num_owners <= 1 {
err!("The last owner can't leave")
}
}
match user_org.delete(&conn) {
Ok(()) => Ok(()),
Err(_) => err!("Failed leaving the organization")
}
}
}
}
#[get("/organizations/<org_id>")]
fn get_organization(org_id: String, _headers: OwnerHeaders, conn: DbConn) -> JsonResult {
match Organization::find_by_uuid(&org_id, &conn) {
@@ -288,8 +311,8 @@ fn get_org_users(org_id: String, headers: AdminHeaders, conn: DbConn) -> JsonRes
#[derive(Deserialize)]
#[allow(non_snake_case)]
struct CollectionData {
id: String,
readOnly: bool,
Id: String,
ReadOnly: bool,
}
#[derive(Deserialize)]
@@ -305,7 +328,7 @@ struct InviteData {
fn send_invite(org_id: String, data: JsonUpcase<InviteData>, headers: AdminHeaders, conn: DbConn) -> EmptyResult {
let data: InviteData = data.into_inner().data;
let new_type = match UserOrgType::from_str(&data.Type.to_string()) {
let new_type = match UserOrgType::from_str(&data.Type.into_string()) {
Some(new_type) => new_type as i32,
None => err!("Invalid type")
};
@@ -319,9 +342,8 @@ fn send_invite(org_id: String, data: JsonUpcase<InviteData>, headers: AdminHeade
match user_opt {
None => err!("User email does not exist"),
Some(user) => {
match UserOrganization::find_by_user_and_org(&user.uuid, &org_id, &conn) {
Some(_) => err!("User already in organization"),
None => ()
if UserOrganization::find_by_user_and_org(&user.uuid, &org_id, &conn).is_some() {
err!("User already in organization")
}
let mut new_user = UserOrganization::new(user.uuid.clone(), org_id.clone());
@@ -331,13 +353,12 @@ fn send_invite(org_id: String, data: JsonUpcase<InviteData>, headers: AdminHeade
// If no accessAll, add the collections received
if !access_all {
for col in data.Collections.iter() {
match Collection::find_by_uuid_and_org(&col.id, &org_id, &conn) {
for col in &data.Collections {
match Collection::find_by_uuid_and_org(&col.Id, &org_id, &conn) {
None => err!("Collection not found in Organization"),
Some(collection) => {
match CollectionUser::save(&user.uuid, &collection.uuid, col.readOnly, &conn) {
Ok(()) => (),
Err(_) => err!("Failed saving collection access for user")
if CollectionUser::save(&user.uuid, &collection.uuid, col.ReadOnly, &conn).is_err() {
err!("Failed saving collection access for user")
}
}
}
@@ -375,7 +396,7 @@ fn confirm_invite(org_id: String, user_id: String, data: JsonUpcase<Value>, head
}
user_to_confirm.status = UserOrgStatus::Confirmed as i32;
user_to_confirm.key = match data["key"].as_str() {
user_to_confirm.key = match data["Key"].as_str() {
Some(key) => key.to_string(),
None => err!("Invalid key provided")
};
@@ -411,7 +432,7 @@ struct EditUserData {
fn edit_user(org_id: String, user_id: String, data: JsonUpcase<EditUserData>, headers: AdminHeaders, conn: DbConn) -> EmptyResult {
let data: EditUserData = data.into_inner().data;
let new_type = match UserOrgType::from_str(&data.Type.to_string()) {
let new_type = match UserOrgType::from_str(&data.Type.into_string()) {
Some(new_type) => new_type as i32,
None => err!("Invalid type")
};
@@ -449,21 +470,19 @@ fn edit_user(org_id: String, user_id: String, data: JsonUpcase<EditUserData>, he
// Delete all the odd collections
for c in CollectionUser::find_by_organization_and_user_uuid(&org_id, &user_to_edit.user_uuid, &conn) {
match c.delete(&conn) {
Ok(()) => (),
Err(_) => err!("Failed deleting old collection assignment")
if c.delete(&conn).is_err() {
err!("Failed deleting old collection assignment")
}
}
// If no accessAll, add the collections received
if !data.AccessAll {
for col in data.Collections.iter() {
match Collection::find_by_uuid_and_org(&col.id, &org_id, &conn) {
for col in &data.Collections {
match Collection::find_by_uuid_and_org(&col.Id, &org_id, &conn) {
None => err!("Collection not found in Organization"),
Some(collection) => {
match CollectionUser::save(&user_to_edit.user_uuid, &collection.uuid, col.readOnly, &conn) {
Ok(()) => (),
Err(_) => err!("Failed saving collection access for user")
if CollectionUser::save(&user_to_edit.user_uuid, &collection.uuid, col.ReadOnly, &conn).is_err() {
err!("Failed saving collection access for user")
}
}
}

View File

@@ -1,28 +1,24 @@
use rocket_contrib::{Json, Value};
use data_encoding::BASE32;
use rocket_contrib::{Json, Value};
use serde_json;
use db::DbConn;
use db::{
models::{TwoFactor, TwoFactorType, User},
DbConn,
};
use crypto;
use api::{PasswordData, JsonResult, NumberOrString, JsonUpcase};
use api::{ApiResult, JsonResult, JsonUpcase, NumberOrString, PasswordData};
use auth::Headers;
#[get("/two-factor")]
fn get_twofactor(headers: Headers) -> JsonResult {
let data = if headers.user.totp_secret.is_none() {
Value::Null
} else {
json!([{
"Enabled": true,
"Type": 0,
"Object": "twoFactorProvider"
}])
};
fn get_twofactor(headers: Headers, conn: DbConn) -> JsonResult {
let twofactors = TwoFactor::find_by_user(&headers.user.uuid, &conn);
let twofactors_json: Vec<Value> = twofactors.iter().map(|c| c.to_json_list()).collect();
Ok(Json(json!({
"Data": data,
"Data": twofactors_json,
"Object": "list"
})))
}
@@ -58,7 +54,7 @@ fn recover(data: JsonUpcase<RecoverTwoFactor>, conn: DbConn) -> JsonResult {
// Get the user
let mut user = match User::find_by_mail(&data.Email, &conn) {
Some(user) => user,
None => err!("Username or password is incorrect. Try again.")
None => err!("Username or password is incorrect. Try again."),
};
// Check password
@@ -71,24 +67,69 @@ fn recover(data: JsonUpcase<RecoverTwoFactor>, conn: DbConn) -> JsonResult {
err!("Recovery code is incorrect. Try again.")
}
user.totp_secret = None;
// Remove all twofactors from the user
for twofactor in TwoFactor::find_by_user(&user.uuid, &conn) {
twofactor.delete(&conn).expect("Error deleting twofactor");
}
// Remove the recovery code, not needed without twofactors
user.totp_recover = None;
user.save(&conn);
Ok(Json(json!({})))
}
#[derive(Deserialize)]
#[allow(non_snake_case)]
struct DisableTwoFactorData {
MasterPasswordHash: String,
Type: NumberOrString,
}
#[post("/two-factor/disable", data = "<data>")]
fn disable_twofactor(
data: JsonUpcase<DisableTwoFactorData>,
headers: Headers,
conn: DbConn,
) -> JsonResult {
let data: DisableTwoFactorData = data.into_inner().data;
let password_hash = data.MasterPasswordHash;
if !headers.user.check_valid_password(&password_hash) {
err!("Invalid password");
}
let type_ = data.Type.into_i32().expect("Invalid type");
if let Some(twofactor) = TwoFactor::find_by_user_and_type(&headers.user.uuid, type_, &conn) {
twofactor.delete(&conn).expect("Error deleting twofactor");
}
Ok(Json(json!({
"Enabled": false,
"Type": type_,
"Object": "twoFactorProvider"
})))
}
#[post("/two-factor/get-authenticator", data = "<data>")]
fn generate_authenticator(data: JsonUpcase<PasswordData>, headers: Headers) -> JsonResult {
fn generate_authenticator(
data: JsonUpcase<PasswordData>,
headers: Headers,
conn: DbConn,
) -> JsonResult {
let data: PasswordData = data.into_inner().data;
if !headers.user.check_valid_password(&data.MasterPasswordHash) {
err!("Invalid password");
}
let (enabled, key) = match headers.user.totp_secret {
Some(secret) => (true, secret),
_ => (false, BASE32.encode(&crypto::get_random(vec![0u8; 20])))
let type_ = TwoFactorType::Authenticator as i32;
let twofactor = TwoFactor::find_by_user_and_type(&headers.user.uuid, type_, &conn);
let (enabled, key) = match twofactor {
Some(tf) => (true, tf.data),
_ => (false, BASE32.encode(&crypto::get_random(vec![0u8; 20]))),
};
Ok(Json(json!({
@@ -100,20 +141,24 @@ fn generate_authenticator(data: JsonUpcase<PasswordData>, headers: Headers) -> J
#[derive(Deserialize, Debug)]
#[allow(non_snake_case)]
struct EnableTwoFactorData {
struct EnableAuthenticatorData {
MasterPasswordHash: String,
Key: String,
Token: NumberOrString,
}
#[post("/two-factor/authenticator", data = "<data>")]
fn activate_authenticator(data: JsonUpcase<EnableTwoFactorData>, headers: Headers, conn: DbConn) -> JsonResult {
let data: EnableTwoFactorData = data.into_inner().data;
fn activate_authenticator(
data: JsonUpcase<EnableAuthenticatorData>,
headers: Headers,
conn: DbConn,
) -> JsonResult {
let data: EnableAuthenticatorData = data.into_inner().data;
let password_hash = data.MasterPasswordHash;
let key = data.Key;
let token = match data.Token.to_i32() {
let token = match data.Token.into_i32() {
Some(n) => n as u64,
None => err!("Malformed token")
None => err!("Malformed token"),
};
if !headers.user.check_valid_password(&password_hash) {
@@ -123,27 +168,24 @@ fn activate_authenticator(data: JsonUpcase<EnableTwoFactorData>, headers: Header
// Validate key as base32 and 20 bytes length
let decoded_key: Vec<u8> = match BASE32.decode(key.as_bytes()) {
Ok(decoded) => decoded,
_ => err!("Invalid totp secret")
_ => err!("Invalid totp secret"),
};
if decoded_key.len() != 20 {
err!("Invalid key length")
}
// Set key in user.totp_secret
let mut user = headers.user;
user.totp_secret = Some(key.to_uppercase());
let type_ = TwoFactorType::Authenticator;
let twofactor = TwoFactor::new(headers.user.uuid.clone(), type_, key.to_uppercase());
// Validate the token provided with the key
if !user.check_totp_code(token) {
if !twofactor.check_totp_code(token) {
err!("Invalid totp code")
}
// Generate totp_recover
let totp_recover = BASE32.encode(&crypto::get_random(vec![0u8; 20]));
user.totp_recover = Some(totp_recover);
user.save(&conn);
let mut user = headers.user;
_generate_recover_code(&mut user, &conn);
twofactor.save(&conn).expect("Error saving twofactor");
Ok(Json(json!({
"Enabled": true,
@@ -152,32 +194,266 @@ fn activate_authenticator(data: JsonUpcase<EnableTwoFactorData>, headers: Header
})))
}
#[derive(Deserialize)]
#[allow(non_snake_case)]
struct DisableTwoFactorData {
MasterPasswordHash: String,
Type: NumberOrString,
fn _generate_recover_code(user: &mut User, conn: &DbConn) {
if user.totp_recover.is_none() {
let totp_recover = BASE32.encode(&crypto::get_random(vec![0u8; 20]));
user.totp_recover = Some(totp_recover);
user.save(conn);
}
}
#[post("/two-factor/disable", data = "<data>")]
fn disable_authenticator(data: JsonUpcase<DisableTwoFactorData>, headers: Headers, conn: DbConn) -> JsonResult {
let data: DisableTwoFactorData = data.into_inner().data;
let password_hash = data.MasterPasswordHash;
let _type = data.Type;
use u2f::messages::{RegisterResponse, SignResponse, U2fSignRequest};
use u2f::protocol::{Challenge, U2f};
use u2f::register::Registration;
if !headers.user.check_valid_password(&password_hash) {
use CONFIG;
const U2F_VERSION: &str = "U2F_V2";
lazy_static! {
static ref APP_ID: String = format!("{}/app-id.json", &CONFIG.domain);
static ref U2F: U2f = U2f::new(APP_ID.clone());
}
#[post("/two-factor/get-u2f", data = "<data>")]
fn generate_u2f(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult {
if !CONFIG.domain_set {
err!("`DOMAIN` environment variable is not set. U2F disabled")
}
let data: PasswordData = data.into_inner().data;
if !headers.user.check_valid_password(&data.MasterPasswordHash) {
err!("Invalid password");
}
let mut user = headers.user;
user.totp_secret = None;
user.totp_recover = None;
let user_uuid = &headers.user.uuid;
user.save(&conn);
let u2f_type = TwoFactorType::U2f as i32;
let register_type = TwoFactorType::U2fRegisterChallenge;
let (enabled, challenge) = match TwoFactor::find_by_user_and_type(user_uuid, u2f_type, &conn) {
Some(_) => (true, String::new()),
None => {
let c = _create_u2f_challenge(user_uuid, register_type, &conn);
(false, c.challenge)
}
};
Ok(Json(json!({
"Enabled": false,
"Type": 0,
"Object": "twoFactorProvider"
"Enabled": enabled,
"Challenge": {
"UserId": headers.user.uuid,
"AppId": APP_ID.to_string(),
"Challenge": challenge,
"Version": U2F_VERSION,
},
"Object": "twoFactorU2f"
})))
}
#[derive(Deserialize, Debug)]
#[allow(non_snake_case)]
struct EnableU2FData {
MasterPasswordHash: String,
DeviceResponse: String,
}
// This struct is copied from the U2F lib
// because challenge is not always sent
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct RegisterResponseCopy {
pub registration_data: String,
pub version: String,
pub challenge: Option<String>,
pub error_code: Option<NumberOrString>,
pub client_data: String,
}
impl RegisterResponseCopy {
fn into_response(self, challenge: String) -> RegisterResponse {
RegisterResponse {
registration_data: self.registration_data,
version: self.version,
challenge: challenge,
client_data: self.client_data,
}
}
}
#[post("/two-factor/u2f", data = "<data>")]
fn activate_u2f(data: JsonUpcase<EnableU2FData>, headers: Headers, conn: DbConn) -> JsonResult {
let data: EnableU2FData = data.into_inner().data;
if !headers.user.check_valid_password(&data.MasterPasswordHash) {
err!("Invalid password");
}
let tf_challenge = TwoFactor::find_by_user_and_type(
&headers.user.uuid,
TwoFactorType::U2fRegisterChallenge as i32,
&conn,
);
if let Some(tf_challenge) = tf_challenge {
let challenge: Challenge = serde_json::from_str(&tf_challenge.data)
.expect("Can't parse U2fRegisterChallenge data");
tf_challenge
.delete(&conn)
.expect("Error deleting U2F register challenge");
let response_copy: RegisterResponseCopy =
serde_json::from_str(&data.DeviceResponse).expect("Can't parse RegisterResponse data");
let error_code = response_copy
.error_code
.clone()
.map_or("0".into(), NumberOrString::into_string);
if error_code != "0" {
err!("Error registering U2F token")
}
let response = response_copy.into_response(challenge.challenge.clone());
match U2F.register_response(challenge.clone(), response) {
Ok(registration) => {
// TODO: Allow more than one U2F device
let mut registrations = Vec::new();
registrations.push(registration);
let tf_registration = TwoFactor::new(
headers.user.uuid.clone(),
TwoFactorType::U2f,
serde_json::to_string(&registrations).unwrap(),
);
tf_registration
.save(&conn)
.expect("Error saving U2F registration");
let mut user = headers.user;
_generate_recover_code(&mut user, &conn);
Ok(Json(json!({
"Enabled": true,
"Challenge": {
"UserId": user.uuid,
"AppId": APP_ID.to_string(),
"Challenge": challenge,
"Version": U2F_VERSION,
},
"Object": "twoFactorU2f"
})))
}
Err(e) => {
println!("Error: {:#?}", e);
err!("Error activating u2f")
}
}
} else {
err!("Can't recover challenge")
}
}
fn _create_u2f_challenge(user_uuid: &str, type_: TwoFactorType, conn: &DbConn) -> Challenge {
let challenge = U2F.generate_challenge().unwrap();
TwoFactor::new(
user_uuid.into(),
type_,
serde_json::to_string(&challenge).unwrap(),
).save(conn)
.expect("Error saving challenge");
challenge
}
// This struct is copied from the U2F lib
// because it doesn't implement Deserialize
#[derive(Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
struct RegistrationCopy {
pub key_handle: Vec<u8>,
pub pub_key: Vec<u8>,
pub attestation_cert: Option<Vec<u8>>,
}
impl Into<Registration> for RegistrationCopy {
fn into(self) -> Registration {
Registration {
key_handle: self.key_handle,
pub_key: self.pub_key,
attestation_cert: self.attestation_cert,
}
}
}
fn _parse_registrations(registations: &str) -> Vec<Registration> {
let registrations_copy: Vec<RegistrationCopy> =
serde_json::from_str(registations).expect("Can't parse RegistrationCopy data");
registrations_copy.into_iter().map(Into::into).collect()
}
pub fn generate_u2f_login(user_uuid: &str, conn: &DbConn) -> ApiResult<U2fSignRequest> {
let challenge = _create_u2f_challenge(user_uuid, TwoFactorType::U2fLoginChallenge, conn);
let type_ = TwoFactorType::U2f as i32;
let twofactor = match TwoFactor::find_by_user_and_type(user_uuid, type_, conn) {
Some(tf) => tf,
None => err!("No U2F devices registered"),
};
let registrations = _parse_registrations(&twofactor.data);
let signed_request: U2fSignRequest = U2F.sign_request(challenge, registrations);
Ok(signed_request)
}
pub fn validate_u2f_login(user_uuid: &str, response: &str, conn: &DbConn) -> ApiResult<()> {
let challenge_type = TwoFactorType::U2fLoginChallenge as i32;
let u2f_type = TwoFactorType::U2f as i32;
let tf_challenge = TwoFactor::find_by_user_and_type(user_uuid, challenge_type, &conn);
let challenge = match tf_challenge {
Some(tf_challenge) => {
let challenge: Challenge = serde_json::from_str(&tf_challenge.data)
.expect("Can't parse U2fLoginChallenge data");
tf_challenge
.delete(&conn)
.expect("Error deleting U2F login challenge");
challenge
}
None => err!("Can't recover login challenge"),
};
let twofactor = match TwoFactor::find_by_user_and_type(user_uuid, u2f_type, conn) {
Some(tf) => tf,
None => err!("No U2F devices registered"),
};
let registrations = _parse_registrations(&twofactor.data);
let response: SignResponse =
serde_json::from_str(response).expect("Can't parse SignResponse data");
let mut _counter: u32 = 0;
for registration in registrations {
let response =
U2F.sign_response(challenge.clone(), registration, response.clone(), _counter);
match response {
Ok(new_counter) => {
_counter = new_counter;
println!("O {:#}", new_counter);
return Ok(());
}
Err(e) => {
println!("E {:#}", e);
break;
}
}
}
err!("error verifying response")
}

View File

@@ -1,4 +1,3 @@
use std::io;
use std::io::prelude::*;
use std::fs::{create_dir_all, File};
@@ -23,24 +22,59 @@ fn icon(domain: String) -> Content<Vec<u8>> {
return Content(icon_type, get_fallback_icon());
}
let url = format!("https://icons.bitwarden.com/{}/icon.png", domain);
// Get the icon, or fallback in case of error
let icon = match get_icon_cached(&domain, &url) {
Ok(icon) => icon,
Err(_) => return Content(icon_type, get_fallback_icon())
};
let icon = get_icon(&domain);
Content(icon_type, icon)
}
fn get_icon(url: &str) -> Result<Vec<u8>, reqwest::Error> {
fn get_icon (domain: &str) -> Vec<u8> {
let path = format!("{}/{}.png", CONFIG.icon_cache_folder, domain);
if let Some(icon) = get_cached_icon(&path) {
return icon;
}
let url = get_icon_url(&domain);
// Get the icon, or fallback in case of error
match download_icon(&url) {
Ok(icon) => {
save_icon(&path, &icon);
icon
},
Err(e) => {
println!("Error downloading icon: {:?}", e);
get_fallback_icon()
}
}
}
fn get_cached_icon(path: &str) -> Option<Vec<u8>> {
// Try to read the cached icon, and return it if it exists
if let Ok(mut f) = File::open(path) {
let mut buffer = Vec::new();
if f.read_to_end(&mut buffer).is_ok() {
return Some(buffer);
}
}
None
}
fn get_icon_url(domain: &str) -> String {
if CONFIG.local_icon_extractor {
format!("http://{}/favicon.ico", domain)
} else {
format!("https://icons.bitwarden.com/{}/icon.png", domain)
}
}
fn download_icon(url: &str) -> Result<Vec<u8>, reqwest::Error> {
println!("Downloading icon for {}...", url);
let mut res = reqwest::get(url)?;
res = match res.error_for_status() {
Err(e) => return Err(e),
Ok(res) => res
};
res = res.error_for_status()?;
let mut buffer: Vec<u8> = vec![];
res.copy_to(&mut buffer)?;
@@ -48,39 +82,31 @@ fn get_icon(url: &str) -> Result<Vec<u8>, reqwest::Error> {
Ok(buffer)
}
fn get_icon_cached(key: &str, url: &str) -> io::Result<Vec<u8>> {
create_dir_all(&CONFIG.icon_cache_folder)?;
let path = &format!("{}/{}.png", CONFIG.icon_cache_folder, key);
fn save_icon(path: &str, icon: &[u8]) {
create_dir_all(&CONFIG.icon_cache_folder).expect("Error creating icon cache");
// Try to read the cached icon, and return it if it exists
match File::open(path) {
Ok(mut f) => {
let mut buffer = Vec::new();
if f.read_to_end(&mut buffer).is_ok() {
return Ok(buffer);
}
/* If error reading file continue */
}
Err(_) => { /* Continue */ }
}
println!("Downloading icon for {}...", key);
let icon = match get_icon(url) {
Ok(icon) => icon,
Err(_) => return Err(io::Error::new(io::ErrorKind::NotFound, ""))
if let Ok(mut f) = File::create(path) {
f.write_all(icon).expect("Error writing icon file");
};
// Save the currently downloaded icon
match File::create(path) {
Ok(mut f) => { f.write_all(&icon).expect("Error writing icon file"); }
Err(_) => { /* Continue */ }
};
Ok(icon)
}
const FALLBACK_ICON_URL: &str = "https://raw.githubusercontent.com/bitwarden/web/master/src/images/fa-globe.png";
fn get_fallback_icon() -> Vec<u8> {
let fallback_icon = "https://raw.githubusercontent.com/bitwarden/web/master/src/images/fa-globe.png";
get_icon_cached("default", fallback_icon).unwrap()
let path = format!("{}/default.png", CONFIG.icon_cache_folder);
if let Some(icon) = get_cached_icon(&path) {
return icon;
}
match download_icon(FALLBACK_ICON_URL) {
Ok(icon) => {
save_icon(&path, &icon);
icon
},
Err(e) => {
println!("Error downloading fallback icon: {:?}", e);
vec![]
}
}
}

View File

@@ -1,40 +1,43 @@
use std::collections::HashMap;
use rocket::{Route, Outcome};
use rocket::request::{self, Request, FromRequest, Form, FormItems, FromForm};
use rocket::request::{self, Form, FormItems, FromForm, FromRequest, Request};
use rocket::{Outcome, Route};
use rocket_contrib::{Json, Value};
use db::DbConn;
use num_traits::FromPrimitive;
use db::models::*;
use db::DbConn;
use util;
use util::{self, JsonMap};
use api::JsonResult;
use api::{ApiResult, JsonResult};
use CONFIG;
pub fn routes() -> Vec<Route> {
routes![ login]
routes![login]
}
#[post("/connect/token", data = "<connect_data>")]
fn login(connect_data: Form<ConnectData>, device_type: DeviceType, conn: DbConn) -> JsonResult {
let data = connect_data.get();
println!("{:#?}", data);
match data.grant_type {
GrantType::RefreshToken =>_refresh_login(data, device_type, conn),
GrantType::Password => _password_login(data, device_type, conn)
GrantType::RefreshToken => _refresh_login(data, device_type, conn),
GrantType::Password => _password_login(data, device_type, conn),
}
}
fn _refresh_login(data: &ConnectData, _device_type: DeviceType, conn: DbConn) -> JsonResult {
// Extract token
let token = data.get("refresh_token").unwrap();
let token = data.get("refresh_token");
// Get device by refresh token
let mut device = match Device::find_by_refresh_token(token, &conn) {
Some(device) => device,
None => err!("Invalid refresh token")
None => err!("Invalid refresh token"),
};
// COMMON
@@ -56,35 +59,34 @@ fn _refresh_login(data: &ConnectData, _device_type: DeviceType, conn: DbConn) ->
fn _password_login(data: &ConnectData, device_type: DeviceType, conn: DbConn) -> JsonResult {
// Validate scope
let scope = data.get("scope").unwrap();
let scope = data.get("scope");
if scope != "api offline_access" {
err!("Scope not supported")
}
// Get the user
let username = data.get("username").unwrap();
let username = data.get("username");
let user = match User::find_by_mail(username, &conn) {
Some(user) => user,
None => err!("Username or password is incorrect. Try again.")
None => err!("Username or password is incorrect. Try again."),
};
// Check password
let password = data.get("password").unwrap();
let password = data.get("password");
if !user.check_valid_password(password) {
err!("Username or password is incorrect. Try again.")
}
// Let's only use the header and ignore the 'devicetype' parameter
let device_type_num = device_type.0;
let (device_id, device_name) = match data.is_device {
false => { (format!("web-{}", user.uuid), String::from("web")) }
true => {
(
data.get("deviceidentifier").unwrap().clone(),
data.get("devicename").unwrap().clone(),
)
}
let (device_id, device_name) = if data.is_device {
(
data.get("deviceidentifier").clone(),
data.get("devicename").clone(),
)
} else {
(format!("web-{}", user.uuid), String::from("web"))
};
// Find device or create new
@@ -104,42 +106,7 @@ fn _password_login(data: &ConnectData, device_type: DeviceType, conn: DbConn) ->
}
};
let twofactor_token = if user.requires_twofactor() {
let twofactor_provider = util::parse_option_string(data.get("twoFactorProvider")).unwrap_or(0);
let twofactor_code = match data.get("twoFactorToken") {
Some(code) => code,
None => err_json!(_json_err_twofactor())
};
match twofactor_provider {
0 /* TOTP */ => {
let totp_code: u64 = match twofactor_code.parse() {
Ok(code) => code,
Err(_) => err!("Invalid Totp code")
};
if !user.check_totp_code(totp_code) {
err_json!(_json_err_twofactor())
}
if util::parse_option_string(data.get("twoFactorRemember")).unwrap_or(0) == 1 {
device.refresh_twofactor_remember();
device.twofactor_remember.clone()
} else {
device.delete_twofactor_remember();
None
}
},
5 /* Remember */ => {
match device.twofactor_remember {
Some(ref remember) if remember == twofactor_code => (),
_ => err_json!(_json_err_twofactor())
};
None // No twofactor token needed here
},
_ => err!("Invalid two factor provider"),
}
} else { None }; // No twofactor token if twofactor is disabled
let twofactor_token = twofactor_auth(&user.uuid, &data, &mut device, &conn)?;
// Common
let user = User::find_by_uuid(&device.user_uuid, &conn).unwrap();
@@ -165,48 +132,127 @@ fn _password_login(data: &ConnectData, device_type: DeviceType, conn: DbConn) ->
Ok(Json(result))
}
fn _json_err_twofactor() -> Value {
json!({
fn twofactor_auth(
user_uuid: &str,
data: &ConnectData,
device: &mut Device,
conn: &DbConn,
) -> ApiResult<Option<String>> {
let twofactors_raw = TwoFactor::find_by_user(user_uuid, conn);
// Remove u2f challenge twofactors (impl detail)
let twofactors: Vec<_> = twofactors_raw.iter().filter(|tf| tf.type_ < 1000).collect();
let providers: Vec<_> = twofactors.iter().map(|tf| tf.type_).collect();
// No twofactor token if twofactor is disabled
if twofactors.len() == 0 {
return Ok(None);
}
let provider = match util::parse_option_string(data.get_opt("twoFactorProvider")) {
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") {
Some(code) => code,
None => err_json!(_json_err_twofactor(&providers, user_uuid, conn)?),
};
let twofactor = twofactors.iter().filter(|tf| tf.type_ == provider).nth(0);
match TwoFactorType::from_i32(provider) {
Some(TwoFactorType::Remember) => {
match &device.twofactor_remember {
Some(remember) if remember == twofactor_code => return Ok(None), // No twofactor token needed here
_ => err_json!(_json_err_twofactor(&providers, user_uuid, conn)?),
}
}
Some(TwoFactorType::Authenticator) => {
let twofactor = match twofactor {
Some(tf) => tf,
None => err!("TOTP not enabled"),
};
let totp_code: u64 = match twofactor_code.parse() {
Ok(code) => code,
_ => err!("Invalid TOTP code"),
};
if !twofactor.check_totp_code(totp_code) {
err_json!(_json_err_twofactor(&providers, user_uuid, conn)?)
}
}
Some(TwoFactorType::U2f) => {
use api::core::two_factor;
two_factor::validate_u2f_login(user_uuid, twofactor_code, conn)?;
}
_ => err!("Invalid two factor provider"),
}
if util::parse_option_string(data.get_opt("twoFactorRemember")).unwrap_or(0) == 1 {
Ok(Some(device.refresh_twofactor_remember()))
} else {
device.delete_twofactor_remember();
Ok(None)
}
}
fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &DbConn) -> ApiResult<Value> {
use api::core::two_factor;
let mut result = json!({
"error" : "invalid_grant",
"error_description" : "Two factor required.",
"TwoFactorProviders" : [ 0 ],
"TwoFactorProviders2" : { "0" : null }
})
}
"TwoFactorProviders" : providers,
"TwoFactorProviders2" : {} // { "0" : null }
});
/*
ConnectData {
grant_type: Password,
is_device: false,
data: {
"scope": "api offline_access",
"client_id": "web",
"grant_type": "password",
"username": "dani@mail",
"password": "8IuV1sJ94tPjyYIK+E+PTjblzjm4W6C4N5wqM0KKsSg="
for provider in providers {
result["TwoFactorProviders2"][provider.to_string()] = Value::Null;
match TwoFactorType::from_i32(*provider) {
Some(TwoFactorType::Authenticator) => { /* Nothing to do for TOTP */ }
Some(TwoFactorType::U2f) if CONFIG.domain_set => {
let request = two_factor::generate_u2f_login(user_uuid, conn)?;
let mut challenge_list = Vec::new();
for key in request.registered_keys {
let mut challenge_map = JsonMap::new();
challenge_map.insert("appId".into(), Value::String(request.app_id.clone()));
challenge_map
.insert("challenge".into(), Value::String(request.challenge.clone()));
challenge_map.insert("version".into(), Value::String(key.version));
challenge_map.insert(
"keyHandle".into(),
Value::String(key.key_handle.unwrap_or_default()),
);
challenge_list.push(Value::Object(challenge_map));
}
let mut map = JsonMap::new();
use serde_json;
let challenge_list_str = serde_json::to_string(&challenge_list).unwrap();
map.insert("Challenges".into(), Value::String(challenge_list_str));
result["TwoFactorProviders2"][provider.to_string()] = Value::Object(map);
}
_ => {}
}
}
Ok(result)
}
RETURNS "TwoFactorToken": "11122233333444555666777888999"
Next login
ConnectData {
grant_type: Password,
is_device: false,
data: {
"scope": "api offline_access",
"username": "dani@mail",
"client_id": "web",
"twofactorprovider": "5",
"twofactortoken": "11122233333444555666777888999",
"grant_type": "password",
"twofactorremember": "0",
"password": "8IuV1sJ94tPjyYIK+E+PTjblzjm4W6C4N5wqM0KKsSg="
}
}
*/
#[derive(Clone, Copy)]
struct DeviceType(i32);
impl<'a, 'r> FromRequest<'a, 'r> for DeviceType {
@@ -221,7 +267,6 @@ impl<'a, 'r> FromRequest<'a, 'r> for DeviceType {
}
}
#[derive(Debug)]
struct ConnectData {
grant_type: GrantType,
@@ -230,10 +275,17 @@ struct ConnectData {
}
#[derive(Debug, Copy, Clone)]
enum GrantType { RefreshToken, Password }
enum GrantType {
RefreshToken,
Password,
}
impl ConnectData {
fn get(&self, key: &str) -> Option<&String> {
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())
}
}
@@ -252,30 +304,33 @@ impl<'f> FromForm<'f> for ConnectData {
for (key, value) in items {
match (key.url_decode(), value.url_decode()) {
(Ok(key), Ok(value)) => data.insert(key.to_lowercase(), value),
_ => return Err(format!("Error decoding key or value")),
_ => return Err("Error decoding key or value".to_string()),
};
}
// Validate needed values
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 (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.get("client_id").unwrap().as_ref() {
"browser" | "mobile" => check_values(&data, &VALUES_DEVICE)?,
_ => false
};
(GrantType::Password, is_device)
}
_ => return Err(format!("Grant type not supported"))
};
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 })
Ok(ConnectData {
grant_type,
is_device,
data,
})
}
}

View File

@@ -1,4 +1,4 @@
mod core;
pub(crate) mod core;
mod icons;
mod identity;
mod web;
@@ -12,8 +12,9 @@ use rocket::response::status::BadRequest;
use rocket_contrib::Json;
// Type aliases for API methods results
type JsonResult = Result<Json, BadRequest<Json>>;
type EmptyResult = Result<(), BadRequest<Json>>;
type ApiResult<T> = Result<T, BadRequest<Json>>;
type JsonResult = ApiResult<Json>;
type EmptyResult = ApiResult<()>;
use util;
type JsonUpcase<T> = Json<util::UpCase<T>>;
@@ -25,7 +26,7 @@ struct PasswordData {
MasterPasswordHash: String
}
#[derive(Deserialize, Debug)]
#[derive(Deserialize, Debug, Clone)]
#[serde(untagged)]
enum NumberOrString {
Number(i32),
@@ -33,14 +34,14 @@ enum NumberOrString {
}
impl NumberOrString {
fn to_string(self) -> String {
fn into_string(self) -> String {
match self {
NumberOrString::Number(n) => n.to_string(),
NumberOrString::String(s) => s
}
}
fn to_i32(self) -> Option<i32> {
fn into_i32(self) -> Option<i32> {
match self {
NumberOrString::Number(n) => Some(n),
NumberOrString::String(s) => s.parse().ok()

View File

@@ -1,39 +1,73 @@
use std::io;
use std::path::{Path, PathBuf};
use rocket::request::Request;
use rocket::response::{self, NamedFile, Responder};
use rocket::response::content::Content;
use rocket::http::{ContentType, Status};
use rocket::Route;
use rocket::response::NamedFile;
use rocket_contrib::Json;
use rocket_contrib::{Json, Value};
use CONFIG;
pub fn routes() -> Vec<Route> {
routes![index, files, attachments, alive]
if CONFIG.web_vault_enabled {
routes![web_index, app_id, web_files, attachments, alive]
} else {
routes![attachments, alive]
}
}
// TODO: Might want to use in memory cache: https://github.com/hgzimmerman/rocket-file-cache
#[get("/")]
fn index() -> io::Result<NamedFile> {
NamedFile::open(
Path::new(&CONFIG.web_vault_folder)
.join("index.html"))
fn web_index() -> WebHeaders<io::Result<NamedFile>> {
web_files("index.html".into())
}
#[get("/app-id.json")]
fn app_id() -> WebHeaders<Content<Json<Value>>> {
let content_type = ContentType::new("application", "fido.trusted-apps+json");
WebHeaders(Content(content_type, Json(json!({
"trustedFacets": [
{
"version": { "major": 1, "minor": 0 },
"ids": [
&CONFIG.domain,
"ios:bundle-id:com.8bit.bitwarden",
"android:apk-key-hash:dUGFzUzf3lmHSLBDBIv+WaFyZMI" ]
}]
}))))
}
#[get("/<p..>", rank = 1)] // Only match this if the other routes don't match
fn files(p: PathBuf) -> io::Result<NamedFile> {
NamedFile::open(
Path::new(&CONFIG.web_vault_folder)
.join(p))
fn web_files(p: PathBuf) -> WebHeaders<io::Result<NamedFile>> {
WebHeaders(NamedFile::open(Path::new(&CONFIG.web_vault_folder).join(p)))
}
struct WebHeaders<R>(R);
impl<'r, R: Responder<'r>> Responder<'r> for WebHeaders<R> {
fn respond_to(self, req: &Request) -> response::Result<'r> {
match self.0.respond_to(req) {
Ok(mut res) => {
res.set_raw_header("Referrer-Policy", "same-origin");
res.set_raw_header("X-Frame-Options", "SAMEORIGIN");
res.set_raw_header("X-Content-Type-Options", "nosniff");
res.set_raw_header("X-XSS-Protection", "1; mode=block");
Ok(res)
},
Err(_) => {
Err(Status::NotFound)
}
}
}
}
#[get("/attachments/<uuid>/<file..>")]
fn attachments(uuid: String, file: PathBuf) -> io::Result<NamedFile> {
NamedFile::open(
Path::new(&CONFIG.attachments_folder)
.join(uuid)
.join(file)
)
NamedFile::open(Path::new(&CONFIG.attachments_folder).join(uuid).join(file))
}

View File

@@ -11,10 +11,11 @@ use serde::ser::Serialize;
use CONFIG;
const JWT_ALGORITHM: jwt::Algorithm = jwt::Algorithm::RS256;
pub const JWT_ISSUER: &'static str = "localhost:8000/identity";
lazy_static! {
pub static ref DEFAULT_VALIDITY: Duration = Duration::hours(2);
pub static ref JWT_ISSUER: String = CONFIG.domain.clone();
static ref JWT_HEADER: jwt::Header = jwt::Header::new(JWT_ALGORITHM);
static ref PRIVATE_RSA_KEY: Vec<u8> = match read_file(&CONFIG.private_rsa_key) {
@@ -30,9 +31,9 @@ lazy_static! {
pub fn encode_jwt<T: Serialize>(claims: &T) -> String {
match jwt::encode(&JWT_HEADER, claims, &PRIVATE_RSA_KEY) {
Ok(token) => return token,
Ok(token) => token,
Err(e) => panic!("Error encoding jwt {}", e)
};
}
}
pub fn decode_jwt(token: &str) -> Result<JWTClaims, String> {
@@ -42,7 +43,7 @@ pub fn decode_jwt(token: &str) -> Result<JWTClaims, String> {
validate_iat: true,
validate_nbf: true,
aud: None,
iss: Some(JWT_ISSUER.into()),
iss: Some(JWT_ISSUER.clone()),
sub: None,
algorithms: vec![JWT_ALGORITHM],
};
@@ -94,7 +95,7 @@ use rocket::Outcome;
use rocket::request::{self, Request, FromRequest};
use db::DbConn;
use db::models::{User, UserOrganization, UserOrgType, Device};
use db::models::{User, UserOrganization, UserOrgType, UserOrgStatus, Device};
pub struct Headers {
pub host: String,
@@ -109,9 +110,31 @@ impl<'a, 'r> FromRequest<'a, 'r> for Headers {
let headers = request.headers();
// Get host
let host = match headers.get_one("Host") {
Some(host) => format!("http://{}", host), // TODO: Check if HTTPS
_ => String::new()
let host = if CONFIG.domain_set {
CONFIG.domain.clone()
} else if let Some(referer) = headers.get_one("Referer") {
referer.to_string()
} else {
// Try to guess from the headers
use std::env;
let protocol = if let Some(proto) = headers.get_one("X-Forwarded-Proto") {
proto
} else if env::var("ROCKET_TLS").is_ok() {
"https"
} else {
"http"
};
let host = if let Some(host) = headers.get_one("X-Forwarded-Host") {
host
} else if let Some(host) = headers.get_one("Host") {
host
} else {
""
};
format!("{}://{}", protocol, host)
};
// Get access_token
@@ -182,7 +205,13 @@ impl<'a, 'r> FromRequest<'a, 'r> for OrgHeaders {
};
let org_user = match UserOrganization::find_by_user_and_org(&headers.user.uuid, &org_id, &conn) {
Some(user) => user,
Some(user) => {
if user.status == UserOrgStatus::Confirmed as i32 {
user
} else {
err_handler!("The current user isn't confirmed member of the organization")
}
}
None => err_handler!("The current user isn't member of the organization")
};

View File

@@ -64,14 +64,33 @@ impl Attachment {
pub fn delete(self, conn: &DbConn) -> QueryResult<()> {
use util;
use std::{thread, time};
let mut retries = 10;
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 = retries - 1;
println!("Had to retry! Retries left: {}", retries);
thread::sleep(time::Duration::from_millis(500));
continue
}
}
}
}
util::delete_file(&self.get_file_path());
diesel::delete(
attachments::table.filter(
attachments::id.eq(self.id)
)
).execute(&**conn).and(Ok(()))
Ok(())
}
pub fn delete_all_by_cipher(cipher_uuid: &str, conn: &DbConn) -> QueryResult<()> {

View File

@@ -3,7 +3,7 @@ use serde_json::Value as JsonValue;
use uuid::Uuid;
use super::{User, Organization, Attachment, FolderCipher, CollectionCipher, UserOrgType};
use super::{User, Organization, Attachment, FolderCipher, CollectionCipher, UserOrgType, UserOrgStatus};
#[derive(Debug, Identifiable, Queryable, Insertable, Associations)]
#[table_name = "ciphers"]
@@ -84,7 +84,7 @@ impl Cipher {
// To remove backwards compatibility, just remove this entire section
// and remove the compat code from ciphers::update_cipher_from_data
if self.type_ == 1 && data_json["Uris"].is_array() {
let uri = data_json["Uris"][0]["uri"].clone();
let uri = data_json["Uris"][0]["Uri"].clone();
data_json["Uri"] = uri;
}
// TODO: ******* Backwards compat end **********
@@ -97,7 +97,7 @@ impl Cipher {
"Favorite": self.favorite,
"OrganizationId": self.organization_uuid,
"Attachments": attachments_json,
"OrganizationUseTotp": false,
"OrganizationUseTotp": true,
"CollectionIds": self.get_collections(user_uuid, &conn),
"Name": self.name,
@@ -266,7 +266,9 @@ impl Cipher {
ciphers::table
.left_join(users_organizations::table.on(
ciphers::organization_uuid.eq(users_organizations::org_uuid.nullable()).and(
users_organizations::user_uuid.eq(user_uuid)
users_organizations::user_uuid.eq(user_uuid).and(
users_organizations::status.eq(UserOrgStatus::Confirmed as i32)
)
)
))
.left_join(ciphers_collections::table)

View File

@@ -2,7 +2,7 @@ use serde_json::Value as JsonValue;
use uuid::Uuid;
use super::{Organization, UserOrganization, UserOrgType};
use super::{Organization, UserOrganization, UserOrgType, UserOrgStatus};
#[derive(Debug, Identifiable, Queryable, Insertable, Associations)]
#[table_name = "collections"]
@@ -78,13 +78,18 @@ impl Collection {
pub fn find_by_user_uuid(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
let mut all_access_collections = users_organizations::table
.filter(users_organizations::user_uuid.eq(user_uuid))
.filter(users_organizations::status.eq(UserOrgStatus::Confirmed as i32))
.filter(users_organizations::access_all.eq(true))
.inner_join(collections::table.on(collections::org_uuid.eq(users_organizations::org_uuid)))
.select(collections::all_columns)
.load::<Self>(&**conn).expect("Error loading collections");
let mut assigned_collections = users_collections::table.inner_join(collections::table)
.left_join(users_organizations::table.on(
users_collections::user_uuid.eq(users_organizations::user_uuid)
))
.filter(users_collections::user_uuid.eq(user_uuid))
.filter(users_organizations::status.eq(UserOrgStatus::Confirmed as i32))
.select(collections::all_columns)
.load::<Self>(&**conn).expect("Error loading collections");

View File

@@ -43,11 +43,14 @@ impl Device {
}
}
pub fn refresh_twofactor_remember(&mut self) {
pub fn refresh_twofactor_remember(&mut self) -> String {
use data_encoding::BASE64;
use crypto;
self.twofactor_remember = Some(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());
twofactor_remember
}
pub fn delete_twofactor_remember(&mut self) {

View File

@@ -6,6 +6,7 @@ mod user;
mod collection;
mod organization;
mod two_factor;
pub use self::attachment::Attachment;
pub use self::cipher::Cipher;
@@ -15,3 +16,4 @@ pub use self::user::User;
pub use self::organization::Organization;
pub use self::organization::{UserOrganization, UserOrgStatus, UserOrgType};
pub use self::collection::{Collection, CollectionUser, CollectionCipher};
pub use self::two_factor::{TwoFactor, TwoFactorType};

View File

@@ -66,11 +66,11 @@ impl Organization {
"Seats": 10,
"MaxCollections": 10,
"Use2fa": false,
"Use2fa": true,
"UseDirectory": false,
"UseEvents": false,
"UseGroups": false,
"UseTotp": false,
"UseTotp": true,
"BusinessName": null,
"BusinessAddress1": null,
@@ -80,8 +80,8 @@ impl Organization {
"BusinessTaxNumber": null,
"BillingEmail": self.billing_email,
"Plan": "Free",
"PlanType": 0, // Free plan
"Plan": "TeamsAnnually",
"PlanType": 5, // TeamsAnnually plan
"Object": "organization",
})
@@ -153,11 +153,11 @@ impl UserOrganization {
"Seats": 10,
"MaxCollections": 10,
"Use2fa": false,
"Use2fa": true,
"UseDirectory": false,
"UseEvents": false,
"UseGroups": false,
"UseTotp": false,
"UseTotp": true,
"MaxStorageGb": 10, // The value doesn't matter, we don't check server-side
@@ -268,6 +268,7 @@ impl UserOrganization {
pub fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
users_organizations::table
.filter(users_organizations::user_uuid.eq(user_uuid))
.filter(users_organizations::status.eq(UserOrgStatus::Confirmed as i32))
.load::<Self>(&**conn).unwrap_or(vec![])
}

112
src/db/models/two_factor.rs Normal file
View File

@@ -0,0 +1,112 @@
use serde_json::Value as JsonValue;
use uuid::Uuid;
use super::User;
#[derive(Debug, Identifiable, Queryable, Insertable, Associations)]
#[table_name = "twofactor"]
#[belongs_to(User, foreign_key = "user_uuid")]
#[primary_key(uuid)]
pub struct TwoFactor {
pub uuid: String,
pub user_uuid: String,
pub type_: i32,
pub enabled: bool,
pub data: String,
}
#[allow(dead_code)]
#[derive(FromPrimitive, ToPrimitive)]
pub enum TwoFactorType {
Authenticator = 0,
Email = 1,
Duo = 2,
YubiKey = 3,
U2f = 4,
Remember = 5,
OrganizationDuo = 6,
// These are implementation details
U2fRegisterChallenge = 1000,
U2fLoginChallenge = 1001,
}
/// Local methods
impl TwoFactor {
pub fn new(user_uuid: String, type_: TwoFactorType, data: String) -> Self {
Self {
uuid: Uuid::new_v4().to_string(),
user_uuid,
type_: type_ as i32,
enabled: true,
data,
}
}
pub fn check_totp_code(&self, totp_code: u64) -> bool {
let totp_secret = self.data.as_bytes();
use data_encoding::BASE32;
use oath::{totp_raw_now, HashType};
let decoded_secret = match BASE32.decode(totp_secret) {
Ok(s) => s,
Err(_) => return false
};
let generated = totp_raw_now(&decoded_secret, 6, 0, 30, &HashType::SHA1);
generated == totp_code
}
pub fn to_json(&self) -> JsonValue {
json!({
"Enabled": self.enabled,
"Key": "", // This key and value vary
"Object": "twoFactorAuthenticator" // This value varies
})
}
pub fn to_json_list(&self) -> JsonValue {
json!({
"Enabled": self.enabled,
"Type": self.type_,
"Object": "twoFactorProvider"
})
}
}
use diesel;
use diesel::prelude::*;
use db::DbConn;
use db::schema::twofactor;
/// Database methods
impl TwoFactor {
pub fn save(&self, conn: &DbConn) -> QueryResult<usize> {
diesel::replace_into(twofactor::table)
.values(self)
.execute(&**conn)
}
pub fn delete(self, conn: &DbConn) -> QueryResult<usize> {
diesel::delete(
twofactor::table.filter(
twofactor::uuid.eq(self.uuid)
)
).execute(&**conn)
}
pub fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
twofactor::table
.filter(twofactor::user_uuid.eq(user_uuid))
.load::<Self>(&**conn).expect("Error loading twofactor")
}
pub fn find_by_user_and_type(user_uuid: &str, type_: i32, conn: &DbConn) -> Option<Self> {
twofactor::table
.filter(twofactor::user_uuid.eq(user_uuid))
.filter(twofactor::type_.eq(type_))
.first::<Self>(&**conn).ok()
}
}

View File

@@ -27,7 +27,8 @@ pub struct User {
pub private_key: Option<String>,
pub public_key: Option<String>,
pub totp_secret: Option<String>,
#[column_name = "totp_secret"]
_totp_secret: Option<String>,
pub totp_recover: Option<String>,
pub security_stamp: String,
@@ -64,7 +65,7 @@ impl User {
private_key: None,
public_key: None,
totp_secret: None,
_totp_secret: None,
totp_recover: None,
equivalent_domains: "[]".to_string(),
@@ -97,28 +98,6 @@ impl User {
pub fn reset_security_stamp(&mut self) {
self.security_stamp = Uuid::new_v4().to_string();
}
pub fn requires_twofactor(&self) -> bool {
self.totp_secret.is_some()
}
pub fn check_totp_code(&self, totp_code: u64) -> bool {
if let Some(ref totp_secret) = self.totp_secret {
// Validate totp
use data_encoding::BASE32;
use oath::{totp_raw_now, HashType};
let decoded_secret = match BASE32.decode(totp_secret.as_bytes()) {
Ok(s) => s,
Err(_) => return false
};
let generated = totp_raw_now(&decoded_secret, 6, 0, 30, &HashType::SHA1);
generated == totp_code
} else {
true
}
}
}
use diesel;
@@ -130,10 +109,13 @@ use db::schema::users;
impl User {
pub fn to_json(&self, conn: &DbConn) -> JsonValue {
use super::UserOrganization;
use super::TwoFactor;
let orgs = UserOrganization::find_by_user(&self.uuid, conn);
let orgs_json: Vec<JsonValue> = orgs.iter().map(|c| c.to_json(&conn)).collect();
let twofactor_enabled = TwoFactor::find_by_user(&self.uuid, conn).len() > 0;
json!({
"Id": self.uuid,
"Name": self.name,
@@ -142,7 +124,7 @@ impl User {
"Premium": true,
"MasterPasswordHint": self.password_hint,
"Culture": "en-US",
"TwoFactorEnabled": self.totp_secret.is_some(),
"TwoFactorEnabled": twofactor_enabled,
"Key": self.key,
"PrivateKey": self.private_key,
"SecurityStamp": self.security_stamp,

View File

@@ -79,6 +79,17 @@ table! {
}
}
table! {
twofactor (uuid) {
uuid -> Text,
user_uuid -> Text,
#[sql_name = "type"]
type_ -> Integer,
enabled -> Bool,
data -> Text,
}
}
table! {
users (uuid) {
uuid -> Text,
@@ -132,6 +143,7 @@ joinable!(devices -> users (user_uuid));
joinable!(folders -> users (user_uuid));
joinable!(folders_ciphers -> ciphers (cipher_uuid));
joinable!(folders_ciphers -> folders (folder_uuid));
joinable!(twofactor -> users (user_uuid));
joinable!(users_collections -> collections (collection_uuid));
joinable!(users_collections -> users (user_uuid));
joinable!(users_organizations -> organizations (org_uuid));
@@ -146,6 +158,7 @@ allow_tables_to_appear_in_same_query!(
folders,
folders_ciphers,
organizations,
twofactor,
users,
users_collections,
users_organizations,

View File

@@ -19,11 +19,15 @@ 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;
use std::{io, env, path::Path, process::{exit, Command}};
use std::{env, path::Path, process::{exit, Command}};
use rocket::Rocket;
#[macro_use]
@@ -46,16 +50,25 @@ fn init_rocket() -> Rocket {
// Embed the migrations from the migrations folder into the application
// This way, the program automatically migrates the database to the latest version
// https://docs.rs/diesel_migrations/*/diesel_migrations/macro.embed_migrations.html
embed_migrations!();
#[allow(unused_imports)]
mod migrations {
embed_migrations!();
pub fn run_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");
use std::io::stdout;
embedded_migrations::run_with_output(&connection, &mut stdout()).expect("Can't run migrations");
}
}
fn main() {
check_db();
check_rsa_keys();
check_web_vault();
check_web_vault();
migrations::run_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");
embedded_migrations::run_with_output(&connection, &mut io::stdout()).expect("Can't run migrations");
init_rocket().launch();
}
@@ -70,6 +83,11 @@ fn check_db() {
exit(1);
}
}
// Turn on WAL in SQLite
use diesel::RunQueryDsl;
let connection = db::get_connection().expect("Can't conect to DB");
diesel::sql_query("PRAGMA journal_mode=wal").execute(&connection).expect("Failed to turn on WAL");
}
fn check_rsa_keys() {
@@ -118,6 +136,10 @@ fn check_rsa_keys() {
}
fn check_web_vault() {
if !CONFIG.web_vault_enabled {
return;
}
let index_path = Path::new(&CONFIG.web_vault_folder).join("index.html");
if !index_path.exists() {
@@ -142,9 +164,13 @@ pub struct Config {
public_rsa_key: String,
web_vault_folder: String,
web_vault_enabled: bool,
local_icon_extractor: bool,
signups_allowed: bool,
password_iterations: i32,
domain: String,
domain_set: bool,
}
impl Config {
@@ -152,21 +178,27 @@ impl Config {
dotenv::dotenv().ok();
let df = env::var("DATA_FOLDER").unwrap_or("data".into());
let key = env::var("RSA_KEY_NAME").unwrap_or("rsa_key".into());
let key = env::var("RSA_KEY_FILENAME").unwrap_or(format!("{}/{}", &df, "rsa_key"));
let domain = env::var("DOMAIN");
Config {
database_url: env::var("DATABASE_URL").unwrap_or(format!("{}/{}", &df, "db.sqlite3")),
icon_cache_folder: env::var("ICON_CACHE_FOLDER").unwrap_or(format!("{}/{}", &df, "icon_cache")),
attachments_folder: env::var("ATTACHMENTS_FOLDER").unwrap_or(format!("{}/{}", &df, "attachments")),
private_rsa_key: format!("{}/{}.der", &df, &key),
private_rsa_key_pem: format!("{}/{}.pem", &df, &key),
public_rsa_key: format!("{}/{}.pub.der", &df, &key),
private_rsa_key: format!("{}.der", &key),
private_rsa_key_pem: format!("{}.pem", &key),
public_rsa_key: format!("{}.pub.der", &key),
web_vault_folder: env::var("WEB_VAULT_FOLDER").unwrap_or("web-vault/".into()),
web_vault_enabled: util::parse_option_string(env::var("WEB_VAULT_ENABLED").ok()).unwrap_or(true),
signups_allowed: util::parse_option_string(env::var("SIGNUPS_ALLOWED").ok()).unwrap_or(false),
local_icon_extractor: util::parse_option_string(env::var("LOCAL_ICON_EXTRACTOR").ok()).unwrap_or(false),
signups_allowed: util::parse_option_string(env::var("SIGNUPS_ALLOWED").ok()).unwrap_or(true),
password_iterations: util::parse_option_string(env::var("PASSWORD_ITERATIONS").ok()).unwrap_or(100_000),
domain_set: domain.is_ok(),
domain: domain.unwrap_or("http://localhost".into()),
}
}
}

View File

@@ -3,7 +3,8 @@
///
#[macro_export]
macro_rules! err {
($err:expr, $err_desc:expr, $msg:expr) => {
($err:expr, $err_desc:expr, $msg:expr) => {{
println!("ERROR: {}", $msg);
err_json!(json!({
"error": $err,
"error_description": $err_desc,
@@ -13,14 +14,13 @@ macro_rules! err {
"Object": "error"
}
}))
};
}};
($msg:expr) => { err!("default_error", "default_error_description", $msg) }
}
#[macro_export]
macro_rules! err_json {
($expr:expr) => {{
println!("ERROR: {}", $expr);
return Err($crate::rocket::response::status::BadRequest(Some($crate::rocket_contrib::Json($expr))));
}}
}
@@ -70,7 +70,7 @@ pub fn delete_file(path: &str) -> bool {
}
const UNITS: [&'static str; 6] = ["bytes", "KB", "MB", "GB", "TB", "PB"];
const UNITS: [&str; 6] = ["bytes", "KB", "MB", "GB", "TB", "PB"];
pub fn get_display_size(size: i32) -> String {
let mut size = size as f64;
@@ -119,7 +119,7 @@ pub fn parse_option_string<S, T>(string: Option<S>) -> Option<T> where S: AsRef<
use chrono::NaiveDateTime;
const DATETIME_FORMAT: &'static str = "%Y-%m-%dT%H:%M:%S%.6fZ";
const DATETIME_FORMAT: &str = "%Y-%m-%dT%H:%M:%S%.6fZ";
pub fn format_date(date: &NaiveDateTime) -> String {
date.format(DATETIME_FORMAT).to_string()
@@ -129,20 +129,12 @@ pub fn format_date(date: &NaiveDateTime) -> String {
/// Deserialization methods
///
use std::collections::BTreeMap as Map;
use std::fmt;
use serde::de::{self, Deserialize, DeserializeOwned, Deserializer};
use serde_json::Value;
use serde::de::{self, DeserializeOwned, Deserializer, MapAccess, SeqAccess, Visitor};
use serde_json::{self, Value};
/// https://github.com/serde-rs/serde/issues/586
pub fn upcase_deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
where T: DeserializeOwned,
D: Deserializer<'de>
{
let map = Map::<String, Value>::deserialize(deserializer)?;
let lower = map.into_iter().map(|(k, v)| (upcase_first(&k), v)).collect();
T::deserialize(Value::Object(lower)).map_err(de::Error::custom)
}
pub type JsonMap = serde_json::Map<String, Value>;
#[derive(PartialEq, Serialize, Deserialize)]
pub struct UpCase<T: DeserializeOwned> {
@@ -150,3 +142,76 @@ pub struct UpCase<T: DeserializeOwned> {
#[serde(flatten)]
pub data: T,
}
/// https://github.com/serde-rs/serde/issues/586
pub fn upcase_deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
where T: DeserializeOwned,
D: Deserializer<'de>
{
let d = deserializer.deserialize_any(UpCaseVisitor)?;
T::deserialize(d).map_err(de::Error::custom)
}
struct UpCaseVisitor;
impl<'de> Visitor<'de> for UpCaseVisitor {
type Value = Value;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("an object or an array")
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where A: MapAccess<'de>
{
let mut result_map = JsonMap::new();
while let Some((key, value)) = map.next_entry()? {
result_map.insert(upcase_first(key), upcase_value(&value));
}
Ok(Value::Object(result_map))
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where A: SeqAccess<'de> {
let mut result_seq = Vec::<Value>::new();
while let Some(value) = seq.next_element()? {
result_seq.push(upcase_value(&value));
}
Ok(Value::Array(result_seq))
}
}
fn upcase_value(value: &Value) -> Value {
if let Some(map) = value.as_object() {
let mut new_value = json!({});
for (key, val) in map {
let processed_key = _process_key(key);
new_value[processed_key] = upcase_value(val);
}
new_value
} else if let Some(array) = value.as_array() {
// Initialize array with null values
let mut new_value = json!(vec![Value::Null; array.len()]);
for (index, val) in array.iter().enumerate() {
new_value[index] = upcase_value(val);
}
new_value
} else {
value.clone()
}
}
fn _process_key(key: &str) -> String {
match key.to_lowercase().as_ref() {
"ssn" => "SSN".into(),
_ => self::upcase_first(key)
}
}