Compare commits

...

149 Commits
1.2.0 ... 1.5.0

Author SHA1 Message Date
Daniel García
371017b547 Merge pull request #294 from i-ghost/feature/add-systemd-notes
Add systemd setup instructions to README.md
2018-12-16 23:55:56 +01:00
i-ghost
bc20592712 Revise per dani-garcia/bitwarden_rs#294 2018-12-16 21:09:59 +00:00
Daniel García
37a6da3443 Merge pull request #296 from jeslinmx/docsupdate
updated fail2ban setup to match changes in logformat for 1.5.0
2018-12-16 20:06:46 +01:00
Daniel García
53bd169462 Merge pull request #297 from mprasil/vault_2.7.1
Update vault to 2.7.1
2018-12-16 20:06:15 +01:00
Daniel García
5a9aab1a32 Implement fromform, and ignore case and underscores, fixes #298 2018-12-16 20:00:16 +01:00
Miroslav Prasil
23eadf2c9a Update vault t o2.7.1 2018-12-16 16:00:51 +00:00
jes
d5dfda8905 updated fail2ban setup to match changes in logformat for 1.5.0 2018-12-16 22:30:25 +08:00
i-ghost
ce15c7ffba Add systemd setup instructions to README.md 2018-12-15 22:25:17 +00:00
Daniel García
a335bcd682 Merge pull request #291 from mqus/patch-1
Add memory usage note to the vault build documentation
2018-12-14 22:56:44 +01:00
mqus
76c510c5b6 move memory note 2018-12-14 20:32:11 +01:00
mqus
4635d62e2c Add memory usage note to vault build process
As there were multiple people having problems with the build I thought that maybe adding a note would help.
2018-12-14 15:04:46 +01:00
Daniel García
65e36e2931 Merge pull request #289 from mprasil/alpine-fix
Fix failing alpine build
2018-12-14 12:33:17 +01:00
Miroslav Prasil
063dfcf487 FIx failing alpine build 2018-12-13 23:16:36 +00:00
Daniel García
d3e4fb88ee Fix wrong folder name 2018-12-13 18:30:01 +01:00
Daniel García
7ebba736cb Updated dockerfiles to use compiled vaults 2018-12-13 18:19:26 +01:00
Daniel García
a1272c7190 Merge branch 'rocket-0.4' 2018-12-13 17:49:55 +01:00
Daniel García
0a6b4e9961 Update BUILD.md 2018-12-13 17:46:08 +01:00
Daniel García
e66edc1ce9 Merge pull request #285 from itr6/master
Add line for people using docker commands instead of docker-compose
2018-12-13 01:07:17 +01:00
Daniel García
4e827e4f8a Implement better retry and use it while saving device 2018-12-12 22:15:54 +01:00
itr6
295985de7c Update README.md 2018-12-12 14:38:57 -06:00
itr6
a48611aa6d Update README.md 2018-12-12 14:38:23 -06:00
Daniel García
f713e2e092 Merge pull request #284 from Algebro7/log_successful_logins
Close #264. Username and IP address is logged on successful authentication
2018-12-11 22:24:04 +01:00
algebro
e26e2319da Close #264. Usernames and IP addresses are logged on successful authentication 2018-12-11 15:20:06 -05:00
Daniel García
f2ab25085d Updated dependencies, and dockerfiles to use NodeJS 10 LTS 2018-12-11 16:42:52 +01:00
Daniel García
7adc045b80 Updated IP logging to use client_ip, to match old remote behavior.
Improved error logging, now it won't show a generic error message in some situations.
Removed delete device, which is not needed as it will be overwritten later.
Logged more info when an error occurs saving a device.
Added orgmanager to JWT claims.
2018-12-09 17:58:38 +01:00
Daniel García
19754c967f More changes to the push token, and filtered multipart logs 2018-12-07 18:25:18 +01:00
Daniel García
738ad2127b Fixed some clippy linting issues 2018-12-07 15:01:29 +01:00
Daniel García
cb930a0858 Remove some required values during login, now uses default values 2018-12-07 14:32:40 +01:00
Daniel García
94810c106a Migrate to rust 2018 edition 2018-12-07 02:05:45 +01:00
Daniel García
2fde4e6933 Implemented proper logging, with support for file logging, timestamp and syslog (this last one is untested) 2018-12-06 20:35:25 +01:00
Daniel García
259a2f2982 Updated rocket to final release 2018-12-06 20:15:22 +01:00
Daniel García
dc1bb6de20 Update device push token methods to more closely follow the official server response 2018-12-06 16:28:36 +01:00
Daniel García
e34f75c267 Make sure the dockerfile builds the project, instead of assuming the cached dummy is valid 2018-12-04 13:55:37 +01:00
Daniel García
2129946d14 Updated deps and web vault to 2.6.1 2018-12-03 20:28:13 +01:00
Daniel García
bdcdb08fc1 Merge branch 'master' into rocket-0.4 2018-12-01 14:58:39 +01:00
Daniel García
8b5d97790f Updated rocket to rc2 and rest of dependencies 2018-12-01 14:29:19 +01:00
Daniel García
cac7fb145b Merge pull request #274 from itr6/master
Fail2Ban Setup & Walkthrough
2018-11-29 23:20:30 +01:00
itr6
6990525e8a Update README.md 2018-11-29 10:52:47 -06:00
itr6
55579261b7 Update README.md 2018-11-29 10:49:20 -06:00
itr6
70c20f7f52 Update README.md 2018-11-29 10:48:41 -06:00
itr6
b56a905322 Update README.md 2018-11-29 10:42:53 -06:00
Daniel García
e177160ee9 Update web vault to 2.6.0 2018-11-27 17:29:09 +01:00
Daniel García
6364c05789 Fix attachments during key rotation, add individual attachment key 2018-11-27 17:24:12 +01:00
Daniel García
f71f10eac6 Implemented key rotation with the latest vault 2018-11-24 23:00:41 +01:00
Daniel García
0f6ab01f77 Merge pull request #268 from mprasil/master
fix bad path in Alpine Dockerfile
2018-11-22 03:06:26 +01:00
Miroslav Prasil
0935cb90a4 fix bad path in Alpine Dockerfile 2018-11-21 23:35:42 +00:00
Daniel García
e86cac984a Merge pull request #263 from mprasil/master
Update dockerfile for alpine, armv7 and aarch64
2018-11-21 15:57:11 +01:00
Daniel García
f1acc1e05a Merge branch 'master' into rocket-0.4 2018-11-21 15:35:34 +01:00
Daniel García
3d36ac4601 Remove unwrap in connection_lost 2018-11-21 15:07:18 +01:00
Miroslav Prasil
bf1c96695b Update dockerfile for alpine, armv7 and aarch64 2018-11-20 14:55:19 +00:00
Daniel García
9ecb29883c Merge branch 'master' into rocket-0.4
# Conflicts:
#	Cargo.lock
2018-11-19 20:59:41 +01:00
Daniel García
8b3e87cfe0 Update lockfile to fix yubico error 2018-11-19 20:58:59 +01:00
Daniel García
e5af230315 Merge branch 'master' into rocket-0.4
# Conflicts:
#	Cargo.toml
2018-11-19 20:45:42 +01:00
Daniel García
a1da82c868 Add online feature to yubico 2018-11-19 20:45:11 +01:00
Daniel García
981a324027 Merge branch 'master' into rocket-0.4
# Conflicts:
#	src/api/core/mod.rs
2018-11-19 20:30:19 +01:00
Daniel García
00abd4c853 Add create endpoint, fixes #253, fixes #261 2018-11-19 20:27:49 +01:00
Daniel García
a4550e51ea Update dependencies and add /ciphers/create 2018-11-19 20:21:02 +01:00
Daniel García
5edbd0e952 Merge branch 'master' into rocket-0.4
# Conflicts:
#	Cargo.lock
#	Cargo.toml
#	src/api/core/mod.rs
2018-11-19 19:52:43 +01:00
Daniel García
1d4944b88e Merge pull request #258 from Bruceforce/master
Update backup section of README.md
2018-11-19 19:36:10 +01:00
10
660c8f8d7e Use dockerhub instead of gitlab registry for README.md 2018-11-18 14:06:08 +01:00
10
478fc0c9dd Merge remote-tracking branch 'origin/master' 2018-11-18 03:23:16 +01:00
10
a2de4ce40e Improved the backup section of sqlite3 database 2018-11-18 03:22:30 +01:00
Daniel García
17052b665f Merge pull request #257 from Step7750/fix-nfc-mobile
Fixes NFC Response for Mobile Yubikey OTP Login
2018-11-17 15:28:41 +01:00
Stepan Fedorko-Bartos
f344dbaad4 Fixes NFC Response for Mobile Login 2018-11-17 01:25:07 -07:00
Daniel García
c5c9e3fd65 Merge pull request #254 from Step7750/master
Adds Yubikey OTP Support
2018-11-16 20:39:03 +01:00
Stepan Fedorko-Bartos
aba9c28226 Disable Yubikey 2FA if 0 Keys Provided 2018-11-16 12:07:00 -07:00
Stepan Fedorko-Bartos
82e2b8a8c0 Code style changes 2018-11-16 11:52:01 -07:00
Stepan Fedorko-Bartos
5a8d5e426d Switches to Downstream yubico with Optional libusb 2018-11-16 11:28:20 -07:00
Daniel García
f9ecb7201b Merge pull request #256 from mprasil/fix_accepted_sync
Prevent accepted user from seeing ciphers until confirmed (fixes #196)
2018-11-16 16:17:21 +01:00
Miroslav Prasil
044cf19913 Prevent accepted user from seeing ciphers until confirmed (fixes #196) 2018-11-16 14:21:26 +00:00
Stepan Fedorko-Bartos
2c233cda8b Adds Yubikey OTP Support to Readme 2018-11-15 18:59:16 -07:00
Stepan Fedorko-Bartos
6d735806c0 Ensures Yubico Creds are set when opening Yubikey Modal 2018-11-15 18:58:44 -07:00
Stepan Fedorko-Bartos
2433d39df5 Allows Custom Yubico OTP Server 2018-11-15 18:54:53 -07:00
Stepan Fedorko-Bartos
9e0e4b13c5 Adds Yubikey OTP Support 2018-11-15 18:43:09 -07:00
Stepan Fedorko-Bartos
e66436625c Adds Yubico Client ID and Secret Key Env Vars 2018-11-15 18:40:27 -07:00
Stepan Fedorko-Bartos
24a4478b5c Adds yubico-rs library dep 2018-11-15 18:34:17 -07:00
Daniel García
f84cbeaaf8 Merge branch 'master' into rocket-0.4
# Conflicts:
#	src/db/models/organization.rs
2018-11-14 16:14:49 +01:00
Daniel García
3cb911a52f Merge pull request #243 from mprasil/vault_2.5.0
Bump vault version to 2.5.0 [wip]
2018-11-14 16:12:18 +01:00
Miroslav Prasil
dd684753d0 Fix gt() 2018-11-13 21:38:56 +00:00
Miroslav Prasil
f3e6cc6ffd Set PartialOrd to consider invalid i32 UserOrgType lower than anything 2018-11-13 16:34:21 +00:00
Miroslav Prasil
b94f4db52a Fix #242 2018-11-13 15:34:37 +00:00
Miroslav Prasil
66a4c5d48b Implement comparison between i32 and UserOrgType 2018-11-12 17:13:25 +00:00
Miroslav Prasil
b4e222d598 Bump vault version to 2.5.0 2018-11-12 10:41:47 +00:00
Daniel García
dd00591082 Add info about how to fix #176 2018-11-09 16:24:45 +01:00
Daniel García
4638786507 Merge branch 'master' into rocket-0.4
# Conflicts:
#	src/api/core/mod.rs
2018-11-09 16:06:24 +01:00
Daniel García
1e9dd2fd4e Merge pull request #244 from RomanHargrave/multiple-u2f
Make U2F work with vault 2.4.0 changes
2018-11-09 15:47:29 +01:00
Roman Hargrave
62bc58e145 Clean up after u2f endpoint split 2018-11-09 00:27:43 -06:00
Roman Hargrave
760e0ab805 Initial u2f fix 2018-11-09 00:00:31 -06:00
Daniel García
6eb1c3d638 Fixed change in organizations header, now Request::get_param() are indexed by segment, not dynamic parameter. 2018-11-06 16:53:34 +01:00
Miroslav Prasil
f408efc927 Also remove libs from other platforms Dockerfiles 2018-11-03 18:56:41 +01:00
Miroslav Prasil
9b20247fc2 Remove libs from Dockerfile 2018-11-03 18:54:36 +01:00
Daniel García
8f5bfe7938 Merge pull request #240 from mprasil/balenalib
Switch from resin to balenalib
2018-11-03 14:48:38 +01:00
Miroslav Prasil
b359df7045 Switch from resin to balenalib 2018-11-03 10:25:15 +01:00
Daniel García
e844f41abc Make the docker images use port 80 (instead of 8000, which is the default in rocket 0.4) 2018-11-01 19:25:10 +01:00
Daniel García
bc532f54d5 Improve login query parsing 2018-11-01 19:25:10 +01:00
Daniel García
c673370103 Updated bw_rs to Rocket version 0.4-rc1 2018-11-01 19:25:09 +01:00
Daniel García
f1b1000600 Merge pull request #239 from mprasil/admin_warning
Improve the admin account warning.
2018-11-01 19:03:05 +01:00
Miroslav Prasil
c0e248c457 Improve the admin account warning. 2018-11-01 18:54:33 +01:00
mprasil
f510a1b060 Merge pull request #236 from xMateMCPE/patch-1
Grammar fixes to README.md
2018-10-28 23:06:10 +00:00
xMateMCPE
fafc3883c6 Update README.md 2018-10-28 22:32:32 +00:00
Daniel García
1bdb98d139 Merge pull request #234 from icicimov/kubernetes-aws
Add Kubernetes deployment reference
2018-10-28 16:36:42 +01:00
icicimov
2f5ca88fb1 Readme typo fix 2018-10-28 18:16:22 +11:00
icicimov
e7a24159c5 Add Kubernetes deployment reference with proper section and short description 2018-10-27 12:42:56 +11:00
icicimov
e056cc8178 Add Kubernetes deployment reference 2018-10-26 22:37:35 +11:00
Daniel García
8ce4c79612 Merge pull request #233 from Toucan-Sam/patch-2
Update PROXY.md
2018-10-26 00:37:34 +02:00
Toucan-Sam
77d9641323 Update PROXY.md
Removed: `include conf.d/proxy-confs/proxy.conf;` lines because they're specific to user (shauder) and will break nginx if copy-pasted/don't exist.
Changed: Moved listen value and server_name to top as is standard for nginx configs
Changed: Commented out SSL config as it's specific to user (shauder) and will break if copy-pasted/don't exist. But is still useful and a good idea for simplifying nginx config.
Changed: Rearranged location blocks because OCD. First /, then /notifications/hub, then /notifications/hub/negotiate because it looks nicer in a tree where each location grows.
2018-10-25 19:45:22 +13:00
Daniel García
31e4237247 Merge pull request #231 from janost/fix-save-equivalent-domains
Accept PUT and POST on /settings/domains, returns JsonResult, fixes saving Custom Equivalent Domains
2018-10-23 01:30:28 +02:00
janost
c32c65d367 Accept PUT and POST on /settings/domains, returns JsonResult, fixes saving Custom Equivalent Domains 2018-10-23 00:32:43 +02:00
Daniel García
0a4dbaf307 Merge pull request #226 from janost/fix-sync-without-query-string
Fix /sync without query string
2018-10-19 01:34:32 +02:00
janost
daa66b08dc Fix /sync without query string 2018-10-19 00:54:40 +02:00
Daniel García
d613fa1e68 Merge pull request #225 from janost/sync-exclude-domains
Don't send Domains if excludeDomains=true on /sync
2018-10-17 23:49:09 +02:00
janost
55fbd8d468 Don't send Domains if excludeDomains=true on /sync 2018-10-17 23:22:07 +02:00
Daniel García
adf40291e8 Update web vault to 2.4.0 2018-10-16 16:20:56 +02:00
Daniel García
acfc900997 Merge pull request #224 from fbartels/patch-2
refine backup instructions
2018-10-15 22:56:16 +02:00
Felix Bartels
0a08b1afc8 refine backup instructions
as mentioned in https://github.com/dani-garcia/bitwarden_rs/issues/223
2018-10-15 21:47:22 +02:00
Daniel García
eb48a3fac2 Merge pull request #221 from mprasil/ws_disable
Disable WebSockets negotiation by default
2018-10-15 16:20:07 +02:00
Miroslav Prasil
2e7fa6440b Do not spawn WS thread if it's disabled 2018-10-15 15:08:15 +01:00
Miroslav Prasil
9ecc98c3cc Disable WebSockets negotiation by default 2018-10-14 23:25:16 +01:00
Daniel García
02fd68d63b Merge pull request #218 from janost/refactor-folder-save
Folder::save() should return QueryResult instead of bool
2018-10-14 20:07:27 +02:00
Daniel García
235bce1ecb Merge pull request #220 from janost/refactor-user-save
User::save() should return QueryResult instead of bool
2018-10-14 20:07:04 +02:00
janost
e985221b50 User::save() should return QueryResult instead of bool 2018-10-14 19:33:12 +02:00
janost
77cf63c06d Folder::save() should return QueryResult instead of bool 2018-10-14 18:25:04 +02:00
Daniel García
faec050a6d Merge pull request #217 from janost/refactor-device-save
Device::save() should return QueryResult instead of bool
2018-10-14 17:35:59 +02:00
Daniel García
22304f4925 Merge pull request #219 from janost/refactor-organization-save
Organization::save() and UserOrganization::save() should return QueryResult instead of bool
2018-10-14 17:35:30 +02:00
janost
58a78ffa54 Device::save() should return QueryResult instead of bool 2018-10-14 16:17:37 +02:00
janost
64f6c60bfd Organization::save() and UserOrganization::save() should return QueryResult instead of bool 2018-10-14 16:04:23 +02:00
Daniel García
e0614620ef Merge pull request #216 from mprasil/superuser
Implement poor man's admin panel
2018-10-13 17:16:06 +02:00
Miroslav Prasil
a28caa33ef Implement poor man's admin panel 2018-10-12 15:20:10 +01:00
Daniel García
ce4fedf191 Change error response to be closer to upstream 2018-10-10 20:37:04 +02:00
Daniel García
f2078a3849 Merge pull request #213 from janost/refactor-collectioncipher-save-delete
CollectionCipher::save() and delete() should return QueryResult instead of bool
2018-10-07 16:06:47 +02:00
janost
5292d38c73 CollectionCipher::save() and delete() should return QueryResult instead of bool 2018-10-07 11:06:11 +02:00
Daniel García
1049646e27 Merge pull request #210 from janost/cipher-save-refactor
Cipher::save() should return QueryResult instead of bool
2018-10-06 16:08:04 +02:00
janost
380cf06211 Cipher::save() should return QueryResult instead of bool 2018-10-06 14:13:49 +02:00
mprasil
1f35ef2865 Merge pull request #209 from janost/resend-invitation-error-message
Return proper error message for org reinvite
2018-10-05 23:21:08 +01:00
janost
c29bc9309a Return proper error message for org reinvite 2018-10-05 12:29:41 +02:00
Daniel García
7112c86471 Updated dependencies, removed valid mail check (now done by lettre), and updated global domains file 2018-10-04 00:01:04 +02:00
Daniel García
2aabf14372 Merge pull request #206 from mprasil/collection_revision
Collection update updates User revision
2018-10-01 19:31:43 +02:00
Daniel García
77ff9c91c5 Merge pull request #207 from mprasil/continuation_token
Add continuation token when we return object list
2018-10-01 19:31:11 +02:00
Miroslav Prasil
d9457e929c Add continuation token when we return object list 2018-10-01 17:55:48 +01:00
Miroslav Prasil
86b49856a7 Handle return value from Collection::save() 2018-10-01 17:50:31 +01:00
Miroslav Prasil
54f54ee845 Update revision for users on collection save 2018-10-01 17:04:15 +01:00
Daniel García
015bd28cc2 Merge pull request #201 from mprasil/aarch64
Add dockerfile for aarch64 (arm64)
2018-10-01 17:40:20 +02:00
Daniel García
990c83a037 Merge pull request #204 from mprasil/org_improvements
Org improvements
2018-10-01 17:40:14 +02:00
Miroslav Prasil
c3c74506a7 Add missing fields to returned Org json 2018-10-01 16:00:11 +01:00
Miroslav Prasil
fb4e6bab14 Clean up the share_collection handling 2018-10-01 15:59:10 +01:00
Miroslav Prasil
fe38f95f15 Add dockerfile for aarch64 (arm64) 2018-09-28 13:01:47 +01:00
Daniel García
9eaa9c1a17 Add WEBSOCKET_ADDRESS config option (Fixes #199) 2018-09-28 13:50:04 +02:00
Daniel García
8ee681c4a3 Merge pull request #200 from mprasil/copy_fix
Remove unecessary copy from armv7 Dockerfile
2018-09-28 13:31:40 +02:00
Miroslav Prasil
08aee97c1d Remove unecessary copy from armv7 Dockerfile 2018-09-28 09:49:10 +01:00
49 changed files with 3039 additions and 2607 deletions

22
.env
View File

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

View File

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

View File

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

1553
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

93
Dockerfile.aarch64 Normal file
View File

@@ -0,0 +1,93 @@
# Using multistage build:
# https://docs.docker.com/develop/develop-images/multistage-build/
# https://whitfin.io/speeding-up-rust-docker-builds/
####################### VAULT BUILD IMAGE #######################
FROM alpine as vault
ENV VAULT_VERSION "v2.7.1"
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
RUN apk add --update-cache --upgrade \
curl \
tar
RUN mkdir /web-vault
WORKDIR /web-vault
RUN curl -L $URL | tar xz
RUN ls
########################## BUILD IMAGE ##########################
# We need to use the Rust build image, because
# we need the Rust compiler and Cargo tooling
FROM rust as build
RUN apt-get update \
&& apt-get install -y \
gcc-aarch64-linux-gnu \
&& mkdir -p ~/.cargo \
&& echo '[target.aarch64-unknown-linux-gnu]' >> ~/.cargo/config \
&& echo 'linker = "aarch64-linux-gnu-gcc"' >> ~/.cargo/config
ENV CARGO_HOME "/root/.cargo"
ENV USER "root"
WORKDIR /app
# Prepare openssl arm64 libs
RUN sed 's/^deb/deb-src/' /etc/apt/sources.list > \
/etc/apt/sources.list.d/deb-src.list \
&& dpkg --add-architecture arm64 \
&& apt-get update \
&& apt-get install -y \
libssl-dev:arm64 \
libc6-dev:arm64
ENV CC_aarch64_unknown_linux_gnu="/usr/bin/aarch64-linux-gnu-gcc"
ENV CROSS_COMPILE="1"
ENV OPENSSL_INCLUDE_DIR="/usr/include/aarch64-linux-gnu"
ENV OPENSSL_LIB_DIR="/usr/lib/aarch64-linux-gnu"
# Copies the complete project
# To avoid copying unneeded files, use .dockerignore
COPY . .
# Build
RUN rustup target add aarch64-unknown-linux-gnu
RUN cargo build --release --target=aarch64-unknown-linux-gnu -v
######################## RUNTIME IMAGE ########################
# Create a new stage with a minimal image
# because we already have a binary built
FROM balenalib/aarch64-debian:stretch
ENV ROCKET_ENV "staging"
ENV ROCKET_PORT=80
ENV ROCKET_WORKERS=10
RUN [ "cross-build-start" ]
# Install needed libraries
RUN apt-get update && apt-get install -y\
openssl\
ca-certificates\
--no-install-recommends\
&& rm -rf /var/lib/apt/lists/*
RUN mkdir /data
RUN [ "cross-build-end" ]
VOLUME /data
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/aarch64-unknown-linux-gnu/release/bitwarden_rs .
# Configures the startup!
CMD ./bitwarden_rs

View File

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

View File

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

View File

@@ -31,26 +31,25 @@ localhost:443 {
## Nginx (by shauder)
```nginx
server {
include conf.d/ssl/ssl.conf;
listen 443 ssl http2;
server_name vault.*;
location /notifications/hub/negotiate {
include conf.d/proxy-confs/proxy.conf;
proxy_pass http://<SERVER>:80;
}
# Specify SSL config if using a shared one.
#include conf.d/ssl/ssl.conf;
location / {
include conf.d/proxy-confs/proxy.conf;
proxy_pass http://<SERVER>:80;
}
location /notifications/hub {
proxy_pass http://<SERVER>:3012;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
location /notifications/hub/negotiate {
proxy_pass http://<SERVER>:80;
}
}
```

264
README.md
View File

@@ -24,9 +24,11 @@ _*Note, that this project is not associated with the [Bitwarden](https://bitward
- [Configuring bitwarden service](#configuring-bitwarden-service)
- [Disable registration of new users](#disable-registration-of-new-users)
- [Disable invitations](#disable-invitations)
- [Configure server administrator](#configure-server-administrator)
- [Enabling HTTPS](#enabling-https)
- [Enabling WebSocket notifications](#enabling-websocket-notifications)
- [Enabling U2F authentication](#enabling-u2f-authentication)
- [Enabling YubiKey OTP authentication](#enabling-yubikey-otp-authentication)
- [Changing persistent data location](#changing-persistent-data-location)
- [/data prefix:](#data-prefix)
- [database name and location](#database-name-and-location)
@@ -38,10 +40,19 @@ _*Note, that this project is not associated with the [Bitwarden](https://bitward
- [Password hint display](#password-hint-display)
- [Disabling or overriding the Vault interface hosting](#disabling-or-overriding-the-vault-interface-hosting)
- [Other configuration](#other-configuration)
- [Fail2Ban Setup](#fail2ban-setup)
- [Logging Failed Login Attempts to Syslog](#logging-failed-login-attempts-to-syslog)
- [Fail2Ban Filter](#fail2ban-filter)
- [Fail2Ban Jail](#fail2ban-jail)
- [Testing Fail2Ban](#testing-fail2ban)
- [Running with systemd-docker](#running-with-systemd-docker)
- [Setting environment variables](#setting-environment-variables)
- [Running the service](#running-the-service)
- [Building your own image](#building-your-own-image)
- [Building binary](#building-binary)
- [Available packages](#available-packages)
- [Arch Linux](#arch-linux)
- [Kubernetes deployment](#kubernetes-deployment)
- [Backing up your vault](#backing-up-your-vault)
- [1. the sqlite3 database](#1-the-sqlite3-database)
- [2. the attachments folder](#2-the-attachments-folder)
@@ -62,15 +73,15 @@ Basically full implementation of Bitwarden API is provided including:
* Basic single user functionality
* Organizations support
* Attachments
* Vault API support
* Vault API support
* Serving the static files for Vault interface
* Website icons API
* Authenticator and U2F support
* YubiKey OTP
## Missing features
* Email confirmation
* Other two-factor systems:
* YubiKey OTP (if your key supports U2F, you can use that)
* Duo
* Email codes
@@ -140,7 +151,7 @@ docker run -d --name bitwarden \
-p 80:80 \
mprasil/bitwarden:latest
```
Note: While users can't register on their own, they can still be invited by already registered users. Read bellow if you also want to disable that.
Note: While users can't register on their own, they can still be invited by already registered users. Read below if you also want to disable that.
### Disable invitations
@@ -154,6 +165,23 @@ docker run -d --name bitwarden \
-p 80:80 \
mprasil/bitwarden:latest
```
### Configure server administrator
**Warning:** *Never* use your regular account for the admin functionality. This is a bit of a hack using the Vault interface for something it's not intended to do and it breaks any other functionality for the account. Please set up and use separate account just for this functionality.
You can configure one email account to be server administrator via the `SERVER_ADMIN_EMAIL` environment variable:
```sh
docker run -d --name bitwarden \
-e SERVER_ADMIN_EMAIL=admin@example.com \
-v /bw-data/:/data/ \
-p 80:80 \
mprasil/bitwarden:latest
```
This will give the user extra functionality and privileges to manage users on the server. In the Vault, the user will see a special (virtual) organization called `bitwarden_rs`. This organization doesn't actually exist and can't be used for most things. (can't have collections or ciphers) Instead it just contains all the users registered on the server. Deleting users from this organization will actually completely delete the user from the server. Inviting users into this organization will just invite the user so they are able to register, but will not grant any organization membership. (unlike inviting user to regular organization)
You can think of the `bitwarden_rs` organization as sort of Admin interface to manage users on the server. Keep in mind that deleting user this way removes the user permanently without any way to restore the deleted data just as if user deleted their own account.
### Enabling HTTPS
To enable HTTPS, you need to configure the `ROCKET_TLS`.
@@ -176,17 +204,19 @@ docker run -d --name bitwarden \
```
Note that you need to mount ssl files and you need to forward appropriate port.
Due to what is likely a certificate validation bug in Android, you need to make sure that your certificate includes the full chain of trust. In the case of certbot, this means using `fullchain.pem` instead of `cert.pem`.
Softwares used for getting certs are often using symlinks. If that is the case, both locations need to be accessible to the docker container.
Example: [certbot](https://certbot.eff.org/) will create a folder that contains the needed `cert.pem` and `privacy.pem` files in `/etc/letsencrypt/live/mydomain/`
Example: [certbot](https://certbot.eff.org/) will create a folder that contains the needed `fullchain.pem` and `privkey.pem` files in `/etc/letsencrypt/live/mydomain/`
These files are symlinked to `../../archive/mydomain/mykey.pem`
These files are symlinked to `../../archive/mydomain/privkey.pem`
So to use from bitwarden container:
```sh
docker run -d --name bitwarden \
-e ROCKET_TLS='{certs="/ssl/live/mydomain/cert.pem",key="/ssl/live/mydomain/privkey.pem"}' \
-e ROCKET_TLS='{certs="/ssl/live/mydomain/fullchain.pem",key="/ssl/live/mydomain/privkey.pem"}' \
-v /etc/letsencrypt/:/ssl/ \
-v /bw-data/:/data/ \
-p 443:80 \
@@ -196,12 +226,23 @@ docker run -d --name bitwarden \
*Important: This does not apply to the mobile clients, which use push notifications.*
To enable WebSockets notifications, an external reverse proxy is necessary, and it must be configured to do the following:
- Route the `/notifications/hub` endpoint to the WebSocket server, by default at port `3012`, making sure to pass the `Connection` and `Upgrade` headers.
- Route the `/notifications/hub` endpoint to the WebSocket server, by default at port `3012`, making sure to pass the `Connection` and `Upgrade` headers. (Note the port can be changed with `WEBSOCKET_PORT` variable)
- Route everything else, including `/notifications/hub/negotiate`, to the standard Rocket server, by default at port `80`.
- If using Docker, you may need to map both ports with the `-p` flag
Example configurations are included in the [PROXY.md](https://github.com/dani-garcia/bitwarden_rs/blob/master/PROXY.md) file.
Then you need to enable WebSockets negotiation on the bitwarden_rs side by setting the `WEBSOCKET_ENABLED` variable to `true`:
```sh
docker run -d --name bitwarden \
-e WEBSOCKET_ENABLED=true \
-v /bw-data/:/data/ \
-p 80:80 \
-p 3012:3012 \
mprasil/bitwarden:latest
```
Note: The reason for this workaround is the lack of support for WebSockets from Rocket (though [it's a planned feature](https://github.com/SergioBenitez/Rocket/issues/90)), which forces us to launch a secondary server on a separate port.
### Enabling U2F authentication
@@ -220,6 +261,22 @@ docker run -d --name bitwarden \
Note that the value has to include the `https://` and it may include a port at the end (in the format of `https://bw.domain.tld:port`) when not using `443`.
### Enabling YubiKey OTP authentication
To enable YubiKey authentication, you must set the `YUBICO_CLIENT_ID` and `YUBICO_SECRET_KEY` env variables.
If `YUBICO_SERVER` is not specified, it will use the default YubiCloud servers. You can generate `YUBICO_CLIENT_ID` and `YUBICO_SECRET_KEY` for the default YubiCloud [here](https://upgrade.yubico.com/getapikey/).
Note: In order to generate API keys or use a YubiKey with an OTP server, it must be registered. After configuring your key in the [YubiKey Personalization Tool](https://www.yubico.com/products/services-software/personalization-tools/use/), you can register it with the default servers [here](https://upload.yubico.com/).
```sh
docker run -d --name bitwarden \
-e YUBICO_CLIENT_ID=12345 \
-e YUBICO_SECRET_KEY=ABCDEABCDEABCDEABCDE= \
-v /bw-data/:/data/ \
-p 80:80 \
mprasil/bitwarden:latest
```
### Changing persistent data location
#### /data prefix:
@@ -299,7 +356,7 @@ docker run -d --name bitwarden \
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:
In the example below, we're starting with 20 workers:
```sh
docker run -d --name bitwarden \
@@ -325,7 +382,7 @@ docker run -d --name bitwarden \
-p 80:80 \
mprasil/bitwarden:latest
```
When `SMTP_SSL` is set to `true`(this is the default), only TLSv1.1 and TLSv1.2 protocols will be accepted and `SMTP_PORT` will default to `587`. If set to `false`, `SMTP_PORT` will default to `25` and the connection won't be encrypted. This can be very insecure, use this setting only if you know what you're doing.
### Password hint display
@@ -359,7 +416,7 @@ docker run -d --name bitwarden \
-v /bw-data/:/data/ \
-p 80:80 \
mprasil/bitwarden:latest
```
```
Note that you can also change the path where bitwarden_rs looks for static files by providing the `WEB_VAULT_FOLDER` environment variable with the path.
@@ -367,6 +424,168 @@ Note that you can also change the path where bitwarden_rs looks for static files
Though this is unlikely to be required in small deployment, you can fine-tune some other settings like number of workers using environment variables that are processed by [Rocket](https://rocket.rs), please see details in [documentation](https://rocket.rs/guide/configuration/#environment-variables).
### Fail2Ban Setup
Bitwarden_rs logs failed login attempts to stdout. We need to set this so the host OS can see these. Then we can setup Fail2Ban.
#### Logging Failed Login Attempts to Syslog
We need to set the logging driver to syslog so the host OS and Fail2Ban can see them.
If you are using docker commands, you will need to add: `--log-driver syslog --log-opt tag=$TAG` to your command.
If you are using docker-compose, add this to you yaml file:
```
bitwarden:
logging:
driver: "syslog"
options:
tag: "$TAG"
```
With the above settings in the docker-compose file. Any failed login attempts will look like this in your syslog file:
```
MMM DD hh:mm:ss server-hostname $TAG[773]: [YYYY-MM-DD][hh:mm:ss][bitwarden_rs::api::identity][ERROR] Username or password is incorrect. Try again. IP: XXX.XXX.XXX.XXX. Username: email@domain.com.
```
You can change the '$TAG' to anything you like. Just remember it because it will be in the Fail2Ban filter.
#### Fail2Ban Filter
Create the filter file
```
sudo nano /etc/fail2ban/filter.d/bitwarden.conf
```
And add the following
```
[INCLUDES]
before = common.conf
[Definition]
_daemon = $TAG
failregex = ^%(__prefix_line)s.*Username or password is incorrect\. Try again\. IP: <HOST>\. Username:.*$
ignoreregex =
```
Dont forget to change the '$TAG' to what you set it as from above.
#### Fail2Ban Jail
Now we need the jail, create the jail file
```
sudo nano /etc/fail2ban/jail.d/bitwarden.local
```
and add:
```
[bitwarden]
enabled = true
port = 80,443,8081
filter = bitwarden
action = iptables-allports[name=bitwarden]
logpath = /var/log/syslog
maxretry = 3
bantime = 14400
findtime = 14400
```
Feel free to change the options as you see fit.
#### Testing Fail2Ban
Now just try to login to bitwarden using any email (it doesnt have to be a valid email, just an email format)
If it works correctly and your IP is banned, you can unban the ip by running:
```
sudo fail2ban-client unban XX.XX.XX.XX bitwarden
```
### Running with systemd-docker
These instructions allow you to have systemd manage the lifecycle of the docker container, if you prefer.
First, install the `systemd-docker` package using your system package manager.
This is a wrapper which improves docker integration with systemd.
For full instructions and configuration options, see the [GitHub repository](https://github.com/ibuildthecloud/systemd-docker).
As root, create `/etc/systemd/system/bitwarden.service` using your preferred editor with the following contents:
```ini
[Unit]
Description=Bitwarden
After=docker.service
Requires=docker.service
[Service]
TimeoutStartSec=0
ExecStartPre=/usr/bin/docker pull mprasil/bitwarden:latest
ExecStart=/usr/bin/systemd-docker --cgroups name=systemd --env run \
-p 8080:80 \
-p 8081:3012 \
-v /opt/bw-data:/data/ \
--rm --name %n mprasil/bitwarden:latest
Restart=always
RestartSec=10s
Type=notify
NotifyAccess=all
[Install]
WantedBy=multi-user.target
```
Adjust the above example as necessary. In particular, pay attention to the `-p` and `-v` options,
as these control the port and volume bindings between the container and the host.
Explanation of options which may not be self-explanatory:
- A `TimeoutStartSec` value of 0 stops systemd from considering the service failed
after waiting for the default startup time. This is required as it may take a while for the `docker pull` in `ExecStartPre` to finish.
- `ExecStartPre`: Pull the docker tag before running.
- A `Type` value of `notify` tells systemd to expect a notification from the service that it is ready.
- A `NotifyAccess` value of `all` is required by `systemd-docker`.
#### Setting environment variables
It's possible to directly specify environment variables in the unit file in two ways:
- Using an `Environment` directive in the `[Service]` block.
- Using the `-e` option of `docker`. In this case, you can omit the `--env` option shown in the example above.
To verify that your environment variables are set correctly, check the output of `systemctl show bitwarden.service`
for an `Environment` line.
It's also possible to store environment variables in a separate file using the `EnvironmentFile` directive in the unit file.
Systemd can source a file of the form:
```shell
Key="Value"
```
However, the systemd project does not mandate where this file should be stored. Consult your distribution's documentation for the
best location for this file. For example, RedHat based distributions typically place these files in `/etc/sysconfig/`
If you're unsure, just create a file as root in `/etc/` e.g. `/etc/bitwarden.service.conf`.
In your unit file, add an `EnvironmentFile` directive in the `[Service]` block, the value being the full path to the
file created above. Example:
```ini
[Unit]
Description=Bitwarden
After=docker.service
Requires=docker.service
[Service]
EnvironmentFile=/etc/bitwarden.service.conf
TimeoutStartSec=0
-snip-
```
#### Running the service
After the above installation and configuration is complete, reload systemd using `sudo systemctl daemon-reload`.
Then, start the Bitwarden service using `sudo systemctl start bitwarden`.
To have the service start with the system, use `sudo systemctl enable bitwarden`.
Verify that the container has started using `systemctl status bitwarden`.
## Building your own image
Clone the repository, then from the root of the repository run:
@@ -386,6 +605,11 @@ For building binary outside the Docker environment and running it locally withou
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.
## Kubernetes deployment
Please check the [kubernetes-bitwarden_rs](https://github.com/icicimov/kubernetes-bitwarden_rs) repository for example deployment in Kubernetes.
It will setup a fully functional and secure `bitwarden_rs` application in Kubernetes behind [nginx-ingress-controller](https://github.com/kubernetes/ingress-nginx) and AWS [ELBv1](https://aws.amazon.com/elasticloadbalancing/features/#Details_for_Elastic_Load_Balancing_Products). It provides little bit more than just simple deployment but you can use all or just part of the manifests depending on your needs and setup.
## Backing up your vault
### 1. the sqlite3 database
@@ -393,11 +617,19 @@ Bitwarden_rs is already packaged for Archlinux thanks to @mqus. There is an [AUR
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'"
mkdir $DATA_FOLDER/db-backup
sqlite3 /$DATA_FOLDER/db.sqlite3 ".backup '/$DATA_FOLDER/db-backup/backup.sqlite3'"
```
This command can be run via a CRON job everyday, however note that it will overwrite the same backup.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.
This command can be run via a CRON job everyday, however note that it will overwrite the same `backup.sqlite3` file each time. This backup file should therefore be saved via incremental backup either using a CRON job command that appends a timestamp or from another backup app such as Duplicati. To restore simply overwrite `db.sqlite3` with `backup.sqlite3` (while bitwarden_rs is stopped).
Running the above command requires sqlite3 to be installed on the docker host system. You can achieve the same result with a sqlite3 docker container using the following command.
```
docker run --rm --volumes-from=bitwarden bruceforce/bw_backup /backup.sh
```
You can also run a container with integrated cron daemon to automatically backup your database. See https://gitlab.com/1O/bitwarden_rs-backup for examples.
### 2. the attachments folder
By default, this is located in `$DATA_FOLDER/attachments`
@@ -415,8 +647,8 @@ This is optional, the icon cache can re-download itself however if you have a la
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.
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 username, 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 below `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`:

View File

@@ -1,6 +1,6 @@
--- a/src/app/services/services.module.ts
+++ b/src/app/services/services.module.ts
@@ -120,20 +120,17 @@ const notificationsService = new NotificationsService(userService, syncService,
@@ -120,20 +120,16 @@ const notificationsService = new NotificationsService(userService, syncService,
const environmentService = new EnvironmentService(apiService, storageService, notificationsService);
const auditService = new AuditService(cryptoFunctionService, apiService);
@@ -22,7 +22,6 @@
+ const isDev = false;
+ environmentService.baseUrl = window.location.origin;
+ environmentService.notificationsUrl = window.location.origin + '/notifications';
+
await apiService.setUrls({
apiService.setUrls({
base: isDev ? null : window.location.origin,
api: isDev ? 'http://localhost:4000' : null,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1 +1 @@
nightly-2018-09-12
nightly-2018-12-01

View File

@@ -1,14 +1,37 @@
use rocket_contrib::Json;
use rocket_contrib::json::Json;
use db::DbConn;
use db::models::*;
use crate::db::models::*;
use crate::db::DbConn;
use api::{PasswordData, JsonResult, EmptyResult, JsonUpcase, NumberOrString};
use auth::Headers;
use fast_chemail::is_valid_email;
use mail;
use crate::api::{EmptyResult, JsonResult, JsonUpcase, NumberOrString, PasswordData, UpdateType, WebSocketUsers};
use crate::auth::Headers;
use crate::mail;
use CONFIG;
use crate::CONFIG;
use rocket::{Route, State};
pub fn routes() -> Vec<Route> {
routes![
register,
profile,
put_profile,
post_profile,
get_public_keys,
post_keys,
post_password,
post_kdf,
post_rotatekey,
post_sstamp,
post_email_token,
post_email,
delete_account,
post_delete_account,
revision_date,
password_hint,
prelogin,
]
}
#[derive(Deserialize, Debug)]
#[allow(non_snake_case)]
@@ -34,21 +57,22 @@ struct KeysData {
fn register(data: JsonUpcase<RegisterData>, conn: DbConn) -> EmptyResult {
let data: RegisterData = data.into_inner().data;
let mut user = match User::find_by_mail(&data.Email, &conn) {
Some(mut user) => {
Some(user) => {
if Invitation::take(&data.Email, &conn) {
for mut user_org in UserOrganization::find_invited_by_user(&user.uuid, &conn).iter_mut() {
user_org.status = UserOrgStatus::Accepted as i32;
user_org.save(&conn);
};
if user_org.save(&conn).is_err() {
err!("Failed to accept user to organization")
}
}
user
} else if CONFIG.signups_allowed {
err!("Account with this email already exists")
err!("Account with this email already exists")
} else {
err!("Registration not allowed")
err!("Registration not allowed")
}
},
}
None => {
if CONFIG.signups_allowed || Invitation::take(&data.Email, &conn) {
User::new(data.Email)
@@ -83,9 +107,10 @@ fn register(data: JsonUpcase<RegisterData>, conn: DbConn) -> EmptyResult {
user.public_key = Some(keys.PublicKey);
}
user.save(&conn);
Ok(())
match user.save(&conn) {
Ok(()) => Ok(()),
Err(_) => err!("Failed to save user"),
}
}
#[get("/accounts/profile")]
@@ -97,7 +122,7 @@ fn profile(headers: Headers, conn: DbConn) -> JsonResult {
#[allow(non_snake_case)]
struct ProfileData {
#[serde(rename = "Culture")]
_Culture: String, // Ignored, always use en-US
_Culture: String, // Ignored, always use en-US
MasterPasswordHint: Option<String>,
Name: String,
}
@@ -118,16 +143,17 @@ fn post_profile(data: JsonUpcase<ProfileData>, headers: Headers, conn: DbConn) -
Some(ref h) if h.is_empty() => None,
_ => data.MasterPasswordHint,
};
user.save(&conn);
Ok(Json(user.to_json(&conn)))
match user.save(&conn) {
Ok(()) => Ok(Json(user.to_json(&conn))),
Err(_) => err!("Failed to save user profile"),
}
}
#[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) {
Some(user) => user,
None => err!("User doesn't exist")
None => err!("User doesn't exist"),
};
Ok(Json(json!({
@@ -146,9 +172,10 @@ fn post_keys(data: JsonUpcase<KeysData>, headers: Headers, conn: DbConn) -> Json
user.private_key = Some(data.EncryptedPrivateKey);
user.public_key = Some(data.PublicKey);
user.save(&conn);
Ok(Json(user.to_json(&conn)))
match user.save(&conn) {
Ok(()) => Ok(Json(user.to_json(&conn))),
Err(_) => err!("Failed to save the user's keys"),
}
}
#[derive(Deserialize)]
@@ -170,9 +197,10 @@ fn post_password(data: JsonUpcase<ChangePassData>, headers: Headers, conn: DbCon
user.set_password(&data.NewMasterPasswordHash);
user.key = data.Key;
user.save(&conn);
Ok(())
match user.save(&conn) {
Ok(()) => Ok(()),
Err(_) => err!("Failed to save password"),
}
}
#[derive(Deserialize)]
@@ -199,7 +227,84 @@ fn post_kdf(data: JsonUpcase<ChangeKdfData>, headers: Headers, conn: DbConn) ->
user.client_kdf_type = data.Kdf;
user.set_password(&data.NewMasterPasswordHash);
user.key = data.Key;
user.save(&conn);
match user.save(&conn) {
Ok(()) => Ok(()),
Err(_) => err!("Failed to save password settings"),
}
}
#[derive(Deserialize)]
#[allow(non_snake_case)]
struct UpdateFolderData {
Id: String,
Name: String,
}
use super::ciphers::CipherData;
#[derive(Deserialize)]
#[allow(non_snake_case)]
struct KeyData {
Ciphers: Vec<CipherData>,
Folders: Vec<UpdateFolderData>,
Key: String,
PrivateKey: String,
MasterPasswordHash: String,
}
#[post("/accounts/key", data = "<data>")]
fn post_rotatekey(data: JsonUpcase<KeyData>, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> EmptyResult {
let data: KeyData = data.into_inner().data;
if !headers.user.check_valid_password(&data.MasterPasswordHash) {
err!("Invalid password")
}
let user_uuid = &headers.user.uuid;
// Update folder data
for folder_data in data.Folders {
let mut saved_folder = match Folder::find_by_uuid(&folder_data.Id, &conn) {
Some(folder) => folder,
None => err!("Folder doesn't exist"),
};
if &saved_folder.user_uuid != user_uuid {
err!("The folder is not owned by the user")
}
saved_folder.name = folder_data.Name;
if saved_folder.save(&conn).is_err() {
err!("Failed to save folder")
}
}
// Update cipher data
use super::ciphers::update_cipher_from_data;
for cipher_data in data.Ciphers {
let mut saved_cipher = match Cipher::find_by_uuid(cipher_data.Id.as_ref().unwrap(), &conn) {
Some(cipher) => cipher,
None => err!("Cipher doesn't exist"),
};
if saved_cipher.user_uuid.as_ref().unwrap() != user_uuid {
err!("The cipher is not owned by the user")
}
update_cipher_from_data(&mut saved_cipher, cipher_data, &headers, false, &conn, &ws, UpdateType::SyncCipherUpdate)?
}
// Update user data
let mut user = headers.user;
user.key = data.Key;
user.private_key = Some(data.PrivateKey);
user.reset_security_stamp();
if user.save(&conn).is_err() {
err!("Failed modify user key");
}
Ok(())
}
@@ -214,9 +319,10 @@ fn post_sstamp(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -
}
user.reset_security_stamp();
user.save(&conn);
Ok(())
match user.save(&conn) {
Ok(()) => Ok(()),
Err(_) => err!("Failed to reset security stamp"),
}
}
#[derive(Deserialize)]
@@ -246,7 +352,7 @@ fn post_email_token(data: JsonUpcase<EmailTokenData>, headers: Headers, conn: Db
struct ChangeEmailData {
MasterPasswordHash: String,
NewEmail: String,
Key: String,
NewMasterPasswordHash: String,
#[serde(rename = "Token")]
@@ -267,13 +373,14 @@ 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(())
match user.save(&conn) {
Ok(()) => Ok(()),
Err(_) => err!("Failed to save email address"),
}
}
#[post("/accounts/delete", data = "<data>")]
@@ -290,27 +397,10 @@ fn delete_account(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn
err!("Invalid password")
}
// Delete ciphers and their attachments
for cipher in Cipher::find_owned_by_user(&user.uuid, &conn) {
if cipher.delete(&conn).is_err() {
err!("Failed deleting cipher")
}
match user.delete(&conn) {
Ok(()) => Ok(()),
Err(_) => err!("Failed deleting user account, are you the only owner of some organization?"),
}
// Delete folders
for f in Folder::find_by_user(&user.uuid, &conn) {
if f.delete(&conn).is_err() {
err!("Failed deleting folder")
}
}
// Delete devices
for d in Device::find_by_user(&user.uuid, &conn) { d.delete(&conn); }
// Delete user
user.delete(&conn);
Ok(())
}
#[get("/accounts/revision-date")]
@@ -329,22 +419,17 @@ struct PasswordHintData {
fn password_hint(data: JsonUpcase<PasswordHintData>, conn: DbConn) -> EmptyResult {
let data: PasswordHintData = data.into_inner().data;
if !is_valid_email(&data.Email) {
err!("This email address is not valid...");
}
let hint = match User::find_by_mail(&data.Email, &conn) {
Some(user) => user.password_hint,
None => return Ok(()),
};
let user = User::find_by_mail(&data.Email, &conn);
if user.is_none() {
return Ok(());
}
let user = user.unwrap();
if let Some(ref mail_config) = CONFIG.mail {
if let Err(e) = mail::send_password_hint(&user.email, user.password_hint, mail_config) {
if let Err(e) = mail::send_password_hint(&data.Email, hint, mail_config) {
err!(format!("There have been a problem sending the email: {}", e));
}
} else if CONFIG.show_password_hint {
if let Some(hint) = user.password_hint {
if let Some(hint) = hint {
err!(format!("Your password hint is: {}", &hint));
} else {
err!("Sorry, you have no password hint...");

View File

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

View File

@@ -1,11 +1,26 @@
use rocket::State;
use rocket_contrib::{Json, Value};
use rocket_contrib::json::Json;
use serde_json::Value;
use db::DbConn;
use db::models::*;
use crate::db::DbConn;
use crate::db::models::*;
use api::{JsonResult, EmptyResult, JsonUpcase, WebSocketUsers, UpdateType};
use auth::Headers;
use crate::api::{JsonResult, EmptyResult, JsonUpcase, WebSocketUsers, UpdateType};
use crate::auth::Headers;
use rocket::Route;
pub fn routes() -> Vec<Route> {
routes![
get_folders,
get_folder,
post_folders,
post_folder,
put_folder,
delete_folder_post,
delete_folder,
]
}
#[get("/folders")]
fn get_folders(headers: Headers, conn: DbConn) -> JsonResult {
@@ -16,6 +31,7 @@ fn get_folders(headers: Headers, conn: DbConn) -> JsonResult {
Ok(Json(json!({
"Data": folders_json,
"Object": "list",
"ContinuationToken": null,
})))
}
@@ -46,7 +62,9 @@ fn post_folders(data: JsonUpcase<FolderData>, headers: Headers, conn: DbConn, ws
let mut folder = Folder::new(headers.user.uuid.clone(), data.Name);
folder.save(&conn);
if folder.save(&conn).is_err() {
err!("Failed to save folder")
}
ws.send_folder_update(UpdateType::SyncFolderCreate, &folder);
Ok(Json(folder.to_json()))
@@ -72,7 +90,9 @@ fn put_folder(uuid: String, data: JsonUpcase<FolderData>, headers: Headers, conn
folder.name = data.Name;
folder.save(&conn);
if folder.save(&conn).is_err() {
err!("Failed to save folder")
}
ws.send_folder_update(UpdateType::SyncFolderUpdate, &folder);
Ok(Json(folder.to_json()))

View File

@@ -260,7 +260,8 @@
"Type": 26,
"Domains": [
"steampowered.com",
"steamcommunity.com"
"steamcommunity.com",
"steamgames.com"
],
"Excluded": false
},

View File

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

View File

@@ -1,14 +1,53 @@
use rocket::State;
use rocket_contrib::{Json, Value};
use CONFIG;
use db::DbConn;
use db::models::*;
use rocket::request::Form;
use rocket_contrib::json::Json;
use serde_json::Value;
use api::{PasswordData, JsonResult, EmptyResult, NumberOrString, JsonUpcase, WebSocketUsers, UpdateType};
use auth::{Headers, AdminHeaders, OwnerHeaders};
use crate::CONFIG;
use crate::db::DbConn;
use crate::db::models::*;
use crate::api::{PasswordData, JsonResult, EmptyResult, NumberOrString, JsonUpcase, WebSocketUsers, UpdateType};
use crate::auth::{Headers, AdminHeaders, OwnerHeaders};
use serde::{Deserialize, Deserializer};
use rocket::Route;
pub fn routes() -> Vec<Route> {
routes![
get_organization,
create_organization,
delete_organization,
post_delete_organization,
leave_organization,
get_user_collections,
get_org_collections,
get_org_collection_detail,
get_collection_users,
put_organization,
post_organization,
post_organization_collections,
delete_organization_collection_user,
post_organization_collection_delete_user,
post_organization_collection_update,
put_organization_collection_update,
delete_organization_collection,
post_organization_collection_delete,
get_org_details,
get_org_users,
send_invite,
confirm_invite,
get_user,
edit_user,
put_organization_user,
delete_user,
post_delete_user,
post_reinvite_user,
post_org_import,
]
}
#[derive(Deserialize)]
#[allow(non_snake_case)]
@@ -49,9 +88,16 @@ fn create_organization(headers: Headers, data: JsonUpcase<OrgData>, conn: DbConn
user_org.type_ = UserOrgType::Owner as i32;
user_org.status = UserOrgStatus::Confirmed as i32;
org.save(&conn);
user_org.save(&conn);
collection.save(&conn);
if org.save(&conn).is_err() {
err!("Failed creating organization")
}
if user_org.save(&conn).is_err() {
err!("Failed to add user to organization")
}
if collection.save(&conn).is_err() {
err!("Failed creating Collection");
}
Ok(Json(org.to_json()))
}
@@ -84,7 +130,7 @@ fn leave_organization(org_id: String, headers: Headers, conn: DbConn) -> EmptyRe
match UserOrganization::find_by_user_and_org(&headers.user.uuid, &org_id, &conn) {
None => err!("User not part of organization"),
Some(user_org) => {
if user_org.type_ == UserOrgType::Owner as i32 {
if user_org.type_ == UserOrgType::Owner {
let num_owners = UserOrganization::find_by_org_and_type(
&org_id, UserOrgType::Owner as i32, &conn)
.len();
@@ -126,9 +172,11 @@ fn post_organization(org_id: String, _headers: OwnerHeaders, data: JsonUpcase<Or
org.name = data.Name;
org.billing_email = data.BillingEmail;
org.save(&conn);
Ok(Json(org.to_json()))
match org.save(&conn) {
Ok(()) => Ok(Json(org.to_json())),
Err(_) => err!("Failed to modify organization")
}
}
// GET /api/collections?writeOnly=false
@@ -141,7 +189,8 @@ fn get_user_collections(headers: Headers, conn: DbConn) -> JsonResult {
.iter()
.map(Collection::to_json)
.collect::<Value>(),
"Object": "list"
"Object": "list",
"ContinuationToken": null,
})))
}
@@ -153,7 +202,8 @@ fn get_org_collections(org_id: String, _headers: AdminHeaders, conn: DbConn) ->
.iter()
.map(Collection::to_json)
.collect::<Value>(),
"Object": "list"
"Object": "list",
"ContinuationToken": null,
})))
}
@@ -168,7 +218,9 @@ fn post_organization_collections(org_id: String, _headers: AdminHeaders, data: J
let mut collection = Collection::new(org.uuid.clone(), data.Name);
collection.save(&conn);
if collection.save(&conn).is_err() {
err!("Failed saving Collection");
}
Ok(Json(collection.to_json()))
}
@@ -197,7 +249,9 @@ fn post_organization_collection_update(org_id: String, col_id: String, _headers:
}
collection.name = data.Name.clone();
collection.save(&conn);
if collection.save(&conn).is_err() {
err!("Failed updating Collection");
}
Ok(Json(collection.to_json()))
}
@@ -294,40 +348,38 @@ fn get_collection_users(org_id: String, coll_id: String, _headers: AdminHeaders,
Ok(Json(json!({
"Data": user_list,
"Object": "list"
"Object": "list",
"ContinuationToken": null,
})))
}
#[derive(FromForm)]
#[allow(non_snake_case)]
struct OrgIdData {
organizationId: String
#[form(field = "organizationId")]
organization_id: String
}
#[get("/ciphers/organization-details?<data>")]
fn get_org_details(data: OrgIdData, headers: Headers, conn: DbConn) -> JsonResult {
let ciphers = Cipher::find_by_org(&data.organizationId, &conn);
#[get("/ciphers/organization-details?<data..>")]
fn get_org_details(data: Form<OrgIdData>, headers: Headers, conn: DbConn) -> JsonResult {
let ciphers = Cipher::find_by_org(&data.organization_id, &conn);
let ciphers_json: Vec<Value> = ciphers.iter().map(|c| c.to_json(&headers.host, &headers.user.uuid, &conn)).collect();
Ok(Json(json!({
"Data": ciphers_json,
"Object": "list",
"ContinuationToken": null,
})))
}
#[get("/organizations/<org_id>/users")]
fn get_org_users(org_id: String, headers: AdminHeaders, conn: DbConn) -> JsonResult {
match UserOrganization::find_by_user_and_org(&headers.user.uuid, &org_id, &conn) {
Some(_) => (),
None => err!("User isn't member of organization")
}
fn get_org_users(org_id: String, _headers: AdminHeaders, conn: DbConn) -> JsonResult {
let users = UserOrganization::find_by_org(&org_id, &conn);
let users_json: Vec<Value> = users.iter().map(|c| c.to_json_user_details(&conn)).collect();
Ok(Json(json!({
"Data": users_json,
"Object": "list"
"Object": "list",
"ContinuationToken": null,
})))
}
@@ -365,9 +417,9 @@ fn send_invite(org_id: String, data: JsonUpcase<InviteData>, headers: AdminHeade
None => err!("Invalid type")
};
if new_type != UserOrgType::User as i32 &&
headers.org_user_type != UserOrgType::Owner as i32 {
err!("Only Owners can invite Admins or Owners")
if new_type != UserOrgType::User &&
headers.org_user_type != UserOrgType::Owner {
err!("Only Owners can invite Managers, Admins or Owners")
}
for email in data.Emails.iter() {
@@ -378,11 +430,11 @@ fn send_invite(org_id: String, data: JsonUpcase<InviteData>, headers: AdminHeade
match invitation.save(&conn) {
Ok(()) => {
let mut user = User::new(email.clone());
if user.save(&conn) {
if user.save(&conn).is_err() {
err!("Failed to create placeholder for invited user")
} else {
user_org_status = UserOrgStatus::Invited as i32;
user
} else {
err!("Failed to create placeholder for invited user")
}
}
Err(_) => err!(format!("Failed to invite: {}", email))
@@ -399,27 +451,32 @@ fn send_invite(org_id: String, data: JsonUpcase<InviteData>, headers: AdminHeade
};
let mut new_user = UserOrganization::new(user.uuid.clone(), org_id.clone());
let access_all = data.AccessAll.unwrap_or(false);
new_user.access_all = access_all;
new_user.type_ = new_type;
new_user.status = user_org_status;
// Don't create UserOrganization in virtual organization
if org_id != Organization::VIRTUAL_ID {
let mut new_user = UserOrganization::new(user.uuid.clone(), org_id.clone());
let access_all = data.AccessAll.unwrap_or(false);
new_user.access_all = access_all;
new_user.type_ = new_type;
new_user.status = user_org_status;
// If no accessAll, add the collections received
if !access_all {
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) => {
if CollectionUser::save(&user.uuid, &collection.uuid, col.ReadOnly, &conn).is_err() {
err!("Failed saving collection access for user")
// If no accessAll, add the collections received
if !access_all {
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) => {
if CollectionUser::save(&user.uuid, &collection.uuid, col.ReadOnly, &conn).is_err() {
err!("Failed saving collection access for user")
}
}
}
}
}
}
new_user.save(&conn);
if new_user.save(&conn).is_err() {
err!("Failed to add user to organization")
}
}
}
Ok(())
@@ -434,9 +491,9 @@ fn confirm_invite(org_id: String, org_user_id: String, data: JsonUpcase<Value>,
None => err!("The specified user isn't a member of the organization")
};
if user_to_confirm.type_ != UserOrgType::User as i32 &&
headers.org_user_type != UserOrgType::Owner as i32 {
err!("Only Owners can confirm Admins or Owners")
if user_to_confirm.type_ != UserOrgType::User &&
headers.org_user_type != UserOrgType::Owner {
err!("Only Owners can confirm Managers, Admins or Owners")
}
if user_to_confirm.status != UserOrgStatus::Accepted as i32 {
@@ -449,9 +506,10 @@ fn confirm_invite(org_id: String, org_user_id: String, data: JsonUpcase<Value>,
None => err!("Invalid key provided")
};
user_to_confirm.save(&conn);
Ok(())
match user_to_confirm.save(&conn) {
Ok(()) => Ok(()),
Err(_) => err!("Failed to add user to organization")
}
}
#[get("/organizations/<org_id>/users/<org_user_id>")]
@@ -483,7 +541,7 @@ fn edit_user(org_id: String, org_user_id: String, data: JsonUpcase<EditUserData>
let data: EditUserData = data.into_inner().data;
let new_type = match UserOrgType::from_str(&data.Type.into_string()) {
Some(new_type) => new_type as i32,
Some(new_type) => new_type,
None => err!("Invalid type")
};
@@ -492,21 +550,21 @@ fn edit_user(org_id: String, org_user_id: String, data: JsonUpcase<EditUserData>
None => err!("The specified user isn't member of the organization")
};
if new_type != user_to_edit.type_ as i32 && (
user_to_edit.type_ <= UserOrgType::Admin as i32 ||
new_type <= UserOrgType::Admin as i32
if new_type != user_to_edit.type_ && (
user_to_edit.type_ >= UserOrgType::Admin ||
new_type >= UserOrgType::Admin
) &&
headers.org_user_type != UserOrgType::Owner as i32 {
headers.org_user_type != UserOrgType::Owner {
err!("Only Owners can grant and remove Admin or Owner privileges")
}
if user_to_edit.type_ == UserOrgType::Owner as i32 &&
headers.org_user_type != UserOrgType::Owner as i32 {
if user_to_edit.type_ == UserOrgType::Owner &&
headers.org_user_type != UserOrgType::Owner {
err!("Only Owners can edit Owner users")
}
if user_to_edit.type_ == UserOrgType::Owner as i32 &&
new_type != UserOrgType::Owner as i32 {
if user_to_edit.type_ == UserOrgType::Owner &&
new_type != UserOrgType::Owner {
// Removing owner permmission, check that there are at least another owner
let num_owners = UserOrganization::find_by_org_and_type(
@@ -519,7 +577,7 @@ fn edit_user(org_id: String, org_user_id: String, data: JsonUpcase<EditUserData>
}
user_to_edit.access_all = data.AccessAll;
user_to_edit.type_ = new_type;
user_to_edit.type_ = new_type as i32;
// Delete all the odd collections
for c in CollectionUser::find_by_organization_and_user_uuid(&org_id, &user_to_edit.user_uuid, &conn) {
@@ -542,24 +600,42 @@ fn edit_user(org_id: String, org_user_id: String, data: JsonUpcase<EditUserData>
}
}
user_to_edit.save(&conn);
Ok(())
match user_to_edit.save(&conn) {
Ok(()) => Ok(()),
Err(_) => err!("Failed to save user data")
}
}
#[delete("/organizations/<org_id>/users/<org_user_id>")]
fn delete_user(org_id: String, org_user_id: String, headers: AdminHeaders, conn: DbConn) -> EmptyResult {
// We're deleting user in virtual Organization. Delete User, not UserOrganization
if org_id == Organization::VIRTUAL_ID {
match User::find_by_uuid(&org_user_id, &conn) {
Some(user_to_delete) => {
if user_to_delete.uuid == headers.user.uuid {
err!("Delete your account in the account settings")
} else {
match user_to_delete.delete(&conn) {
Ok(()) => return Ok(()),
Err(_) => err!("Failed to delete user - likely because it's the only owner of organization")
}
}
},
None => err!("User not found")
}
}
let user_to_delete = match UserOrganization::find_by_uuid_and_org(&org_user_id, &org_id, &conn) {
Some(user) => user,
None => err!("User to delete isn't member of the organization")
};
if user_to_delete.type_ != UserOrgType::User as i32 &&
headers.org_user_type != UserOrgType::Owner as i32 {
if user_to_delete.type_ != UserOrgType::User &&
headers.org_user_type != UserOrgType::Owner {
err!("Only Owners can delete Admins or Owners")
}
if user_to_delete.type_ == UserOrgType::Owner as i32 {
if user_to_delete.type_ == UserOrgType::Owner {
// Removing owner, check that there are at least another owner
let num_owners = UserOrganization::find_by_org_and_type(
&org_id, UserOrgType::Owner as i32, &conn)
@@ -581,6 +657,11 @@ fn post_delete_user(org_id: String, org_user_id: String, headers: AdminHeaders,
delete_user(org_id, org_user_id, headers, conn)
}
#[post("/organizations/<_org_id>/users/<_org_user_id>/reinvite")]
fn post_reinvite_user(_org_id: String, _org_user_id: String, _headers: AdminHeaders, _conn: DbConn) -> EmptyResult {
err!("This functionality is not implemented. The user needs to manually register before they can be accepted into the organization.")
}
use super::ciphers::CipherData;
use super::ciphers::update_cipher_from_data;
@@ -601,25 +682,28 @@ struct RelationsData {
Value: usize,
}
#[post("/ciphers/import-organization?<query>", data = "<data>")]
fn post_org_import(query: OrgIdData, data: JsonUpcase<ImportData>, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> EmptyResult {
#[post("/ciphers/import-organization?<query..>", data = "<data>")]
fn post_org_import(query: Form<OrgIdData>, data: JsonUpcase<ImportData>, headers: Headers, conn: DbConn, ws: State<WebSocketUsers>) -> EmptyResult {
let data: ImportData = data.into_inner().data;
let org_id = query.organizationId;
let org_id = query.into_inner().organization_id;
let org_user = match UserOrganization::find_by_user_and_org(&headers.user.uuid, &org_id, &conn) {
Some(user) => user,
None => err!("User is not part of the organization")
};
if org_user.type_ > UserOrgType::Admin as i32 {
if org_user.type_ < UserOrgType::Admin {
err!("Only admins or owners can import into an organization")
}
// Read and create the collections
let collections: Vec<_> = data.Collections.into_iter().map(|coll| {
let mut collection = Collection::new(org_id.clone(), coll.Name);
collection.save(&conn);
collection
if collection.save(&conn).is_err() {
err!("Failed to create Collection");
}
Ok(collection)
}).collect();
// Read the relations between collections and ciphers
@@ -638,9 +722,16 @@ fn post_org_import(query: OrgIdData, data: JsonUpcase<ImportData>, headers: Head
// Assign the collections
for (cipher_index, coll_index) in relations {
let cipher_id = &ciphers[cipher_index].uuid;
let coll_id = &collections[coll_index].uuid;
CollectionCipher::save(cipher_id, coll_id, &conn);
let coll = &collections[coll_index];
let coll_id = match coll {
Ok(coll) => coll.uuid.as_str(),
Err(_) => err!("Failed to assign to collection")
};
match CollectionCipher::save(cipher_id, coll_id, &conn) {
Ok(()) => (),
Err(_) => err!("Failed to add cipher to collection")
};
}
let mut user = headers.user;
@@ -648,4 +739,4 @@ fn post_org_import(query: OrgIdData, data: JsonUpcase<ImportData>, headers: Head
Ok(()) => Ok(()),
Err(_) => err!("Failed to update the revision, please log out and log back in to finish import.")
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,11 +1,12 @@
use rocket::Route;
use rocket_contrib::Json;
use rocket_contrib::json::Json;
use serde_json::Value as JsonValue;
use api::JsonResult;
use auth::Headers;
use db::DbConn;
use crate::api::JsonResult;
use crate::auth::Headers;
use crate::db::DbConn;
use CONFIG;
use crate::CONFIG;
pub fn routes() -> Vec<Route> {
routes![negotiate, websockets_err]
@@ -18,21 +19,24 @@ fn websockets_err() -> JsonResult {
#[post("/hub/negotiate")]
fn negotiate(_headers: Headers, _conn: DbConn) -> JsonResult {
use crypto;
use crate::crypto;
use data_encoding::BASE64URL;
let conn_id = BASE64URL.encode(&crypto::get_random(vec![0u8; 16]));
let mut available_transports: Vec<JsonValue> = Vec::new();
if CONFIG.websocket_enabled {
available_transports.push(json!({"transport":"WebSockets", "transferFormats":["Text","Binary"]}));
}
// TODO: Implement transports
// Rocket WS support: https://github.com/SergioBenitez/Rocket/issues/90
// Rocket SSE support: https://github.com/SergioBenitez/Rocket/issues/33
// {"transport":"ServerSentEvents", "transferFormats":["Text"]},
// {"transport":"LongPolling", "transferFormats":["Text","Binary"]}
Ok(Json(json!({
"connectionId": conn_id,
"availableTransports":[
{"transport":"WebSockets", "transferFormats":["Text","Binary"]},
// {"transport":"ServerSentEvents", "transferFormats":["Text"]},
// {"transport":"LongPolling", "transferFormats":["Text","Binary"]}
]
"availableTransports": available_transports
})))
}
@@ -48,7 +52,7 @@ use chashmap::CHashMap;
use chrono::NaiveDateTime;
use serde_json::from_str;
use db::models::{Cipher, Folder, User};
use crate::db::models::{Cipher, Folder, User};
use rmpv::Value;
@@ -135,7 +139,7 @@ impl Handler for WSHandler {
let _id = &query_split[1][3..];
// Validate the user
use auth;
use crate::auth;
let claims = match auth::decode_jwt(access_token) {
Ok(claims) => claims,
Err(_) => {
@@ -165,7 +169,7 @@ impl Handler for WSHandler {
}
fn on_message(&mut self, msg: Message) -> ws::Result<()> {
println!("Server got message '{}'. ", msg);
info!("Server got message '{}'. ", msg);
if let Message::Text(text) = msg.clone() {
let json = &text[..text.len() - 1]; // Remove last char
@@ -215,7 +219,6 @@ impl Factory for WSFactory {
type Handler = WSHandler;
fn connection_made(&mut self, out: Sender) -> Self::Handler {
println!("WS: Connection made");
WSHandler {
out,
user_uuid: None,
@@ -224,12 +227,11 @@ impl Factory for WSFactory {
}
fn connection_lost(&mut self, handler: Self::Handler) {
println!("WS: Connection lost");
// Remove handler
let user_uuid = &handler.user_uuid.unwrap();
if let Some(mut user_conn) = self.users.map.get_mut(user_uuid) {
user_conn.remove_item(&handler.out);
if let Some(user_uuid) = &handler.user_uuid {
if let Some(mut user_conn) = self.users.map.get_mut(user_uuid) {
user_conn.remove_item(&handler.out);
}
}
}
}
@@ -240,16 +242,17 @@ pub struct WebSocketUsers {
}
impl WebSocketUsers {
fn send_update(&self, user_uuid: &String, data: Vec<u8>) -> ws::Result<()> {
fn send_update(&self, user_uuid: &String, data: &[u8]) -> ws::Result<()> {
if let Some(user) = self.map.get(user_uuid) {
for sender in user.iter() {
sender.send(data.clone())?;
sender.send(data)?;
}
}
Ok(())
}
// NOTE: The last modified date needs to be updated before calling these methods
#[allow(dead_code)]
pub fn send_user_update(&self, ut: UpdateType, user: &User) {
let data = create_update(
vec![
@@ -259,7 +262,7 @@ impl WebSocketUsers {
ut,
);
self.send_update(&user.uuid.clone(), data).ok();
self.send_update(&user.uuid.clone(), &data).ok();
}
pub fn send_folder_update(&self, ut: UpdateType, folder: &Folder) {
@@ -272,10 +275,10 @@ impl WebSocketUsers {
ut,
);
self.send_update(&folder.user_uuid, data).ok();
self.send_update(&folder.user_uuid, &data).ok();
}
pub fn send_cipher_update(&self, ut: UpdateType, cipher: &Cipher, user_uuids: &Vec<String>) {
pub fn send_cipher_update(&self, ut: UpdateType, cipher: &Cipher, user_uuids: &[String]) {
let user_uuid = convert_option(cipher.user_uuid.clone());
let org_uuid = convert_option(cipher.organization_uuid.clone());
@@ -291,7 +294,7 @@ impl WebSocketUsers {
);
for uuid in user_uuids {
self.send_update(&uuid, data.clone()).ok();
self.send_update(&uuid, &data).ok();
}
}
}
@@ -355,12 +358,14 @@ pub fn start_notification_server() -> WebSocketUsers {
let factory = WSFactory::init();
let users = factory.users.clone();
thread::spawn(move || {
WebSocket::new(factory)
.unwrap()
.listen(format!("0.0.0.0:{}", CONFIG.websocket_port))
.unwrap();
});
if CONFIG.websocket_enabled {
thread::spawn(move || {
WebSocket::new(factory)
.unwrap()
.listen(&CONFIG.websocket_url)
.unwrap();
});
}
users
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,4 @@
use serde_json::Value as JsonValue;
use uuid::Uuid;
use serde_json::Value;
use super::{Organization, UserOrganization, UserOrgType, UserOrgStatus};
@@ -18,14 +16,14 @@ pub struct Collection {
impl Collection {
pub fn new(org_uuid: String, name: String) -> Self {
Self {
uuid: Uuid::new_v4().to_string(),
uuid: crate::util::get_uuid(),
org_uuid,
name,
}
}
pub fn to_json(&self) -> JsonValue {
pub fn to_json(&self) -> Value {
json!({
"Id": self.uuid,
"OrganizationId": self.org_uuid,
@@ -37,18 +35,23 @@ impl Collection {
use diesel;
use diesel::prelude::*;
use db::DbConn;
use db::schema::*;
use crate::db::DbConn;
use crate::db::schema::*;
/// Database methods
impl Collection {
pub fn save(&mut self, conn: &DbConn) -> bool {
match diesel::replace_into(collections::table)
.values(&*self)
.execute(&**conn) {
Ok(1) => true, // One row inserted
_ => false,
}
pub fn save(&mut self, conn: &DbConn) -> QueryResult<()> {
// Update affected users revision
UserOrganization::find_by_collection_and_org(&self.uuid, &self.org_uuid, conn)
.iter()
.for_each(|user_org| {
User::update_uuid_revision(&user_org.user_uuid, conn);
});
diesel::replace_into(collections::table)
.values(&*self)
.execute(&**conn)
.and(Ok(()))
}
pub fn delete(self, conn: &DbConn) -> QueryResult<()> {
@@ -145,15 +148,12 @@ impl Collection {
if user_org.access_all {
true
} else {
match users_collections::table.inner_join(collections::table)
users_collections::table.inner_join(collections::table)
.filter(users_collections::collection_uuid.eq(&self.uuid))
.filter(users_collections::user_uuid.eq(&user_uuid))
.filter(users_collections::read_only.eq(false))
.select(collections::all_columns)
.first::<Self>(&**conn).ok() {
None => false, // Read only or no access to collection
Some(_) => true,
}
.first::<Self>(&**conn).ok().is_some() // Read only or no access to collection
}
}
}
@@ -254,25 +254,19 @@ pub struct CollectionCipher {
/// Database methods
impl CollectionCipher {
pub fn save(cipher_uuid: &str, collection_uuid: &str, conn: &DbConn) -> bool {
match diesel::replace_into(ciphers_collections::table)
pub fn save(cipher_uuid: &str, collection_uuid: &str, conn: &DbConn) -> QueryResult<()> {
diesel::replace_into(ciphers_collections::table)
.values((
ciphers_collections::cipher_uuid.eq(cipher_uuid),
ciphers_collections::collection_uuid.eq(collection_uuid),
)).execute(&**conn) {
Ok(1) => true, // One row inserted
_ => false,
}
)).execute(&**conn).and(Ok(()))
}
pub fn delete(cipher_uuid: &str, collection_uuid: &str, conn: &DbConn) -> bool {
match diesel::delete(ciphers_collections::table
pub fn delete(cipher_uuid: &str, collection_uuid: &str, conn: &DbConn) -> QueryResult<()> {
diesel::delete(ciphers_collections::table
.filter(ciphers_collections::cipher_uuid.eq(cipher_uuid))
.filter(ciphers_collections::collection_uuid.eq(collection_uuid)))
.execute(&**conn) {
Ok(1) => true, // One row deleted
_ => false,
}
.execute(&**conn).and(Ok(()))
}
pub fn delete_all_by_cipher(cipher_uuid: &str, conn: &DbConn) -> QueryResult<()> {

View File

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

View File

@@ -1,7 +1,5 @@
use chrono::{NaiveDateTime, Utc};
use serde_json::Value as JsonValue;
use uuid::Uuid;
use serde_json::Value;
use super::{User, Cipher};
@@ -33,7 +31,7 @@ impl Folder {
let now = Utc::now().naive_utc();
Self {
uuid: Uuid::new_v4().to_string(),
uuid: crate::util::get_uuid(),
created_at: now,
updated_at: now,
@@ -42,8 +40,8 @@ impl Folder {
}
}
pub fn to_json(&self) -> JsonValue {
use util::format_date;
pub fn to_json(&self) -> Value {
use crate::util::format_date;
json!({
"Id": self.uuid,
@@ -65,21 +63,17 @@ impl FolderCipher {
use diesel;
use diesel::prelude::*;
use db::DbConn;
use db::schema::{folders, folders_ciphers};
use crate::db::DbConn;
use crate::db::schema::{folders, folders_ciphers};
/// Database methods
impl Folder {
pub fn save(&mut self, conn: &DbConn) -> bool {
pub fn save(&mut self, conn: &DbConn) -> QueryResult<()> {
User::update_uuid_revision(&self.user_uuid, conn);
self.updated_at = Utc::now().naive_utc();
match diesel::replace_into(folders::table)
.values(&*self)
.execute(&**conn) {
Ok(1) => true, // One row inserted
_ => false,
}
diesel::replace_into(folders::table)
.values(&*self).execute(&**conn).and(Ok(()))
}
pub fn delete(&self, conn: &DbConn) -> QueryResult<()> {
@@ -93,6 +87,13 @@ impl Folder {
).execute(&**conn).and(Ok(()))
}
pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> QueryResult<()> {
for folder in Self::find_by_user(user_uuid, &conn) {
folder.delete(&conn)?;
}
Ok(())
}
pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> {
folders::table
.filter(folders::uuid.eq(uuid))

View File

@@ -1,7 +1,7 @@
use serde_json::Value as JsonValue;
use std::cmp::Ordering;
use serde_json::Value;
use uuid::Uuid;
use super::{User, CollectionUser};
use super::{User, CollectionUser, Invitation};
#[derive(Debug, Identifiable, Queryable, Insertable)]
#[table_name = "organizations"]
@@ -32,10 +32,101 @@ pub enum UserOrgStatus {
Confirmed = 2,
}
#[derive(Copy, Clone)]
#[derive(PartialEq)]
#[derive(Eq)]
pub enum UserOrgType {
Owner = 0,
Admin = 1,
User = 2,
Manager = 3,
}
impl Ord for UserOrgType {
fn cmp(&self, other: &UserOrgType) -> Ordering {
if self == other {
Ordering::Equal
} else {
match self {
UserOrgType::Owner => Ordering::Greater,
UserOrgType::Admin => match other {
UserOrgType::Owner => Ordering::Less,
_ => Ordering::Greater
},
UserOrgType::Manager => match other {
UserOrgType::Owner | UserOrgType::Admin => Ordering::Less,
_ => Ordering::Greater
},
UserOrgType::User => Ordering::Less
}
}
}
}
impl PartialOrd for UserOrgType {
fn partial_cmp(&self, other: &UserOrgType) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl PartialEq<i32> for UserOrgType {
fn eq(&self, other: &i32) -> bool {
*other == *self as i32
}
}
impl PartialOrd<i32> for UserOrgType {
fn partial_cmp(&self, other: &i32) -> Option<Ordering> {
if let Some(other) = Self::from_i32(*other) {
return Some(self.cmp(&other))
}
None
}
fn gt(&self, other: &i32) -> bool {
match self.partial_cmp(other) {
Some(Ordering::Less) | Some(Ordering::Equal) => false,
_ => true,
}
}
fn ge(&self, other: &i32) -> bool {
match self.partial_cmp(other) {
Some(Ordering::Less) => false,
_ => true,
}
}
}
impl PartialEq<UserOrgType> for i32 {
fn eq(&self, other: &UserOrgType) -> bool {
*self == *other as i32
}
}
impl PartialOrd<UserOrgType> for i32 {
fn partial_cmp(&self, other: &UserOrgType) -> Option<Ordering> {
if let Some(self_type) = UserOrgType::from_i32(*self) {
return Some(self_type.cmp(other))
}
None
}
fn lt(&self, other: &UserOrgType) -> bool {
match self.partial_cmp(other) {
Some(Ordering::Less) | None => true,
_ => false,
}
}
fn le(&self, other: &UserOrgType) -> bool {
match self.partial_cmp(other) {
Some(Ordering::Less) | Some(Ordering::Equal) | None => true,
_ => false,
}
}
}
impl UserOrgType {
@@ -44,29 +135,51 @@ impl UserOrgType {
"0" | "Owner" => Some(UserOrgType::Owner),
"1" | "Admin" => Some(UserOrgType::Admin),
"2" | "User" => Some(UserOrgType::User),
"3" | "Manager" => Some(UserOrgType::Manager),
_ => None,
}
}
pub fn from_i32(i: i32) -> Option<Self> {
match i {
0 => Some(UserOrgType::Owner),
1 => Some(UserOrgType::Admin),
2 => Some(UserOrgType::User),
3 => Some(UserOrgType::Manager),
_ => None,
}
}
}
/// Local methods
impl Organization {
pub const VIRTUAL_ID: &'static str = "00000000-0000-0000-0000-000000000000";
pub fn new(name: String, billing_email: String) -> Self {
Self {
uuid: Uuid::new_v4().to_string(),
uuid: crate::util::get_uuid(),
name,
billing_email,
}
}
pub fn to_json(&self) -> JsonValue {
pub fn new_virtual() -> Self {
Self {
uuid: String::from(Organization::VIRTUAL_ID),
name: String::from("bitwarden_rs"),
billing_email: String::from("none@none.none")
}
}
pub fn to_json(&self) -> Value {
json!({
"Id": self.uuid,
"Name": self.name,
"Seats": 10,
"MaxCollections": 10,
"MaxStorageGb": 10, // The value doesn't matter, we don't check server-side
"Use2fa": true,
"UseDirectory": false,
"UseEvents": false,
@@ -83,7 +196,7 @@ impl Organization {
"BillingEmail": self.billing_email,
"Plan": "TeamsAnnually",
"PlanType": 5, // TeamsAnnually plan
"UsersGetPremium": true,
"Object": "organization",
})
}
@@ -92,7 +205,7 @@ impl Organization {
impl UserOrganization {
pub fn new(user_uuid: String, org_uuid: String) -> Self {
Self {
uuid: Uuid::new_v4().to_string(),
uuid: crate::util::get_uuid(),
user_uuid,
org_uuid,
@@ -103,34 +216,52 @@ impl UserOrganization {
type_: UserOrgType::User as i32,
}
}
pub fn new_virtual(user_uuid: String, type_: UserOrgType, status: UserOrgStatus) -> Self {
Self {
uuid: user_uuid.clone(),
user_uuid,
org_uuid: String::from(Organization::VIRTUAL_ID),
access_all: true,
key: String::new(),
status: status as i32,
type_: type_ as i32,
}
}
}
use diesel;
use diesel::prelude::*;
use db::DbConn;
use db::schema::{organizations, users_organizations, users_collections, ciphers_collections};
use crate::db::DbConn;
use crate::db::schema::{organizations, users_organizations, users_collections, ciphers_collections};
/// Database methods
impl Organization {
pub fn save(&mut self, conn: &DbConn) -> bool {
pub fn save(&mut self, conn: &DbConn) -> QueryResult<()> {
if self.uuid == Organization::VIRTUAL_ID {
return Err(diesel::result::Error::NotFound)
}
UserOrganization::find_by_org(&self.uuid, conn)
.iter()
.for_each(|user_org| {
User::update_uuid_revision(&user_org.user_uuid, conn);
});
match diesel::replace_into(organizations::table)
.values(&*self)
.execute(&**conn) {
Ok(1) => true, // One row inserted
_ => false,
}
diesel::replace_into(organizations::table)
.values(&*self).execute(&**conn).and(Ok(()))
}
pub fn delete(self, conn: &DbConn) -> QueryResult<()> {
use super::{Cipher, Collection};
if self.uuid == Organization::VIRTUAL_ID {
return Err(diesel::result::Error::NotFound)
}
Cipher::delete_all_by_organization(&self.uuid, &conn)?;
Collection::delete_all_by_organization(&self.uuid, &conn)?;
UserOrganization::delete_all_by_organization(&self.uuid, &conn)?;
@@ -143,6 +274,9 @@ impl Organization {
}
pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> {
if uuid == Organization::VIRTUAL_ID {
return Some(Self::new_virtual())
};
organizations::table
.filter(organizations::uuid.eq(uuid))
.first::<Self>(&**conn).ok()
@@ -150,7 +284,7 @@ impl Organization {
}
impl UserOrganization {
pub fn to_json(&self, conn: &DbConn) -> JsonValue {
pub fn to_json(&self, conn: &DbConn) -> Value {
let org = Organization::find_by_uuid(&self.org_uuid, conn).unwrap();
json!({
@@ -158,6 +292,7 @@ impl UserOrganization {
"Name": org.name,
"Seats": 10,
"MaxCollections": 10,
"UsersGetPremium": true,
"Use2fa": true,
"UseDirectory": false,
@@ -177,7 +312,7 @@ impl UserOrganization {
})
}
pub fn to_json_user_details(&self, conn: &DbConn) -> JsonValue {
pub fn to_json_user_details(&self, conn: &DbConn) -> Value {
let user = User::find_by_uuid(&self.user_uuid, conn).unwrap();
json!({
@@ -194,7 +329,7 @@ impl UserOrganization {
})
}
pub fn to_json_collection_user_details(&self, read_only: bool, conn: &DbConn) -> JsonValue {
pub fn to_json_collection_user_details(&self, read_only: bool, conn: &DbConn) -> Value {
let user = User::find_by_uuid(&self.user_uuid, conn).unwrap();
json!({
@@ -209,7 +344,7 @@ impl UserOrganization {
})
}
pub fn to_json_details(&self, conn: &DbConn) -> JsonValue {
pub fn to_json_details(&self, conn: &DbConn) -> Value {
let coll_uuids = if self.access_all {
vec![] // If we have complete access, no need to fill the array
} else {
@@ -230,18 +365,20 @@ impl UserOrganization {
})
}
pub fn save(&mut self, conn: &DbConn) -> bool {
pub fn save(&mut self, conn: &DbConn) -> QueryResult<()> {
if self.org_uuid == Organization::VIRTUAL_ID {
return Err(diesel::result::Error::NotFound)
}
User::update_uuid_revision(&self.user_uuid, conn);
match diesel::replace_into(users_organizations::table)
.values(&*self)
.execute(&**conn) {
Ok(1) => true, // One row inserted
_ => false,
}
diesel::replace_into(users_organizations::table)
.values(&*self).execute(&**conn).and(Ok(()))
}
pub fn delete(self, conn: &DbConn) -> QueryResult<()> {
if self.org_uuid == Organization::VIRTUAL_ID {
return Err(diesel::result::Error::NotFound)
}
User::update_uuid_revision(&self.user_uuid, conn);
CollectionUser::delete_all_by_user(&self.user_uuid, &conn)?;
@@ -260,8 +397,15 @@ impl UserOrganization {
Ok(())
}
pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> QueryResult<()> {
for user_org in Self::find_any_state_by_user(&user_uuid, &conn) {
user_org.delete(&conn)?;
}
Ok(())
}
pub fn has_full_access(self) -> bool {
self.access_all || self.type_ < UserOrgType::User as i32
self.access_all || self.type_ >= UserOrgType::Admin
}
pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> {
@@ -291,10 +435,29 @@ impl UserOrganization {
.load::<Self>(&**conn).unwrap_or_default()
}
pub fn find_by_org(org_uuid: &str, conn: &DbConn) -> Vec<Self> {
pub fn find_any_state_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
users_organizations::table
.filter(users_organizations::org_uuid.eq(org_uuid))
.load::<Self>(&**conn).expect("Error loading user organizations")
.filter(users_organizations::user_uuid.eq(user_uuid))
.load::<Self>(&**conn).unwrap_or_default()
}
pub fn find_by_org(org_uuid: &str, conn: &DbConn) -> Vec<Self> {
if org_uuid == Organization::VIRTUAL_ID {
User::get_all(&*conn).iter().map(|user| {
Self::new_virtual(
user.uuid.clone(),
UserOrgType::User,
if Invitation::find_by_mail(&user.email, &conn).is_some() {
UserOrgStatus::Invited
} else {
UserOrgStatus::Confirmed
})
}).collect()
} else {
users_organizations::table
.filter(users_organizations::org_uuid.eq(org_uuid))
.load::<Self>(&**conn).expect("Error loading user organizations")
}
}
pub fn find_by_org_and_type(org_uuid: &str, type_: i32, conn: &DbConn) -> Vec<Self> {
@@ -330,6 +493,22 @@ impl UserOrganization {
.select(users_organizations::all_columns)
.load::<Self>(&**conn).expect("Error loading user organizations")
}
pub fn find_by_collection_and_org(collection_uuid: &str, org_uuid: &str, conn: &DbConn) -> Vec<Self> {
users_organizations::table
.filter(users_organizations::org_uuid.eq(org_uuid))
.left_join(users_collections::table.on(
users_collections::user_uuid.eq(users_organizations::user_uuid)
))
.filter(
users_organizations::access_all.eq(true).or( // AccessAll..
users_collections::collection_uuid.eq(&collection_uuid) // ..or access to collection with cipher
)
)
.select(users_organizations::all_columns)
.load::<Self>(&**conn).expect("Error loading user organizations")
}
}

View File

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

View File

@@ -1,10 +1,8 @@
use chrono::{NaiveDateTime, Utc};
use serde_json::Value as JsonValue;
use serde_json::Value;
use uuid::Uuid;
use crypto;
use CONFIG;
use crate::crypto;
use crate::CONFIG;
#[derive(Debug, Identifiable, Queryable, Insertable)]
@@ -50,7 +48,7 @@ impl User {
let email = mail.to_lowercase();
Self {
uuid: Uuid::new_v4().to_string(),
uuid: crate::util::get_uuid(),
created_at: now,
updated_at: now,
name: email.clone(),
@@ -61,7 +59,7 @@ impl User {
salt: crypto::get_random_64(),
password_iterations: CONFIG.password_iterations,
security_stamp: Uuid::new_v4().to_string(),
security_stamp: crate::util::get_uuid(),
password_hint: None,
private_key: None,
@@ -97,28 +95,36 @@ impl User {
self.password_hash = crypto::hash_password(password.as_bytes(),
&self.salt,
self.password_iterations as u32);
self.reset_security_stamp();
}
pub fn reset_security_stamp(&mut self) {
self.security_stamp = Uuid::new_v4().to_string();
self.security_stamp = crate::util::get_uuid();
}
pub fn is_server_admin(&self) -> bool {
match CONFIG.server_admin_email {
Some(ref server_admin_email) => &self.email == server_admin_email,
None => false
}
}
}
use diesel;
use diesel::prelude::*;
use db::DbConn;
use db::schema::{users, invitations};
use crate::db::DbConn;
use crate::db::schema::{users, invitations};
use super::{Cipher, Folder, Device, UserOrganization, UserOrgType};
/// Database methods
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();
pub fn to_json(&self, conn: &DbConn) -> Value {
use super::{UserOrganization, UserOrgType, UserOrgStatus, TwoFactor};
let mut orgs = UserOrganization::find_by_user(&self.uuid, conn);
if self.is_server_admin() {
orgs.push(UserOrganization::new_virtual(self.uuid.clone(), UserOrgType::Owner, UserOrgStatus::Confirmed));
}
let orgs_json: Vec<Value> = orgs.iter().map(|c| c.to_json(&conn)).collect();
let twofactor_enabled = !TwoFactor::find_by_user(&self.uuid, conn).is_empty();
json!({
@@ -139,30 +145,40 @@ impl User {
}
pub fn save(&mut self, conn: &DbConn) -> bool {
pub fn save(&mut self, conn: &DbConn) -> QueryResult<()> {
self.updated_at = Utc::now().naive_utc();
match diesel::replace_into(users::table) // Insert or update
.values(&*self)
.execute(&**conn) {
Ok(1) => true, // One row inserted
_ => false,
}
diesel::replace_into(users::table) // Insert or update
.values(&*self).execute(&**conn).and(Ok(()))
}
pub fn delete(self, conn: &DbConn) -> bool {
match diesel::delete(users::table.filter(
users::uuid.eq(self.uuid)))
.execute(&**conn) {
Ok(1) => true, // One row deleted
_ => false,
pub fn delete(self, conn: &DbConn) -> QueryResult<()> {
for user_org in UserOrganization::find_by_user(&self.uuid, &*conn) {
if user_org.type_ == UserOrgType::Owner {
if UserOrganization::find_by_org_and_type(
&user_org.org_uuid,
UserOrgType::Owner as i32, &conn
).len() <= 1 {
return Err(diesel::result::Error::NotFound);
}
}
}
UserOrganization::delete_all_by_user(&self.uuid, &*conn)?;
Cipher::delete_all_by_user(&self.uuid, &*conn)?;
Folder::delete_all_by_user(&self.uuid, &*conn)?;
Device::delete_all_by_user(&self.uuid, &*conn)?;
Invitation::take(&self.email, &*conn); // Delete invitation if any
diesel::delete(users::table.filter(
users::uuid.eq(self.uuid)))
.execute(&**conn).and(Ok(()))
}
pub fn update_uuid_revision(uuid: &str, conn: &DbConn) {
if let Some(mut user) = User::find_by_uuid(&uuid, conn) {
if user.update_revision(conn).is_err(){
println!("Warning: Failed to update revision for {}", user.email);
warn!("Failed to update revision for {}", user.email);
};
};
}
@@ -190,6 +206,11 @@ impl User {
.filter(users::uuid.eq(uuid))
.first::<Self>(&**conn).ok()
}
pub fn get_all(conn: &DbConn) -> Vec<Self> {
users::table
.load::<Self>(&**conn).expect("Error loading users")
}
}
#[derive(Debug, Identifiable, Queryable, Insertable)]

View File

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

View File

@@ -1,33 +1,31 @@
use std::error::Error;
use native_tls::{Protocol, TlsConnector};
use lettre::{Transport, SmtpTransport, SmtpClient, ClientTlsParameters, ClientSecurity};
use lettre::smtp::ConnectionReuseParameters;
use lettre::smtp::authentication::Credentials;
use lettre_email::EmailBuilder;
use MailConfig;
use crate::MailConfig;
fn mailer(config: &MailConfig) -> SmtpTransport {
let client_security = if config.smtp_ssl {
let mut tls_builder = TlsConnector::builder();
tls_builder.min_protocol_version(Some(Protocol::Tlsv11));
ClientSecurity::Required(
ClientTlsParameters::new(config.smtp_host.to_owned(), tls_builder.build().unwrap())
)
let tls = TlsConnector::builder()
.min_protocol_version(Some(Protocol::Tlsv11))
.build()
.unwrap();
ClientSecurity::Required(ClientTlsParameters::new(config.smtp_host.clone(), tls))
} else {
ClientSecurity::None
};
let smtp_client = SmtpClient::new(
(config.smtp_host.to_owned().as_str(), config.smtp_port),
client_security
(config.smtp_host.as_str(), config.smtp_port),
client_security,
).unwrap();
let smtp_client = match (&config.smtp_username, &config.smtp_password) {
(Some(username), Some(password)) => {
smtp_client.credentials(Credentials::new(username.to_owned(), password.to_owned()))
},
(_, _) => smtp_client,
(Some(user), Some(pass)) => smtp_client.credentials(Credentials::new(user.clone(), pass.clone())),
_ => smtp_client,
};
smtp_client
@@ -46,18 +44,19 @@ pub fn send_password_hint(address: &str, hint: Option<String>, config: &MailConf
hint))
} else {
("Sorry, you have no password hint...",
"Sorry, you have not specified any password hint...\n".to_string())
"Sorry, you have not specified any password hint...\n".into())
};
let email = EmailBuilder::new()
.to(address)
.from((config.smtp_from.to_owned(), "Bitwarden-rs"))
.from((config.smtp_from.clone(), "Bitwarden-rs"))
.subject(subject)
.body(body)
.build().unwrap();
.build()
.map_err(|e| e.to_string())?;
match mailer(config).send(email.into()) {
Ok(_) => Ok(()),
Err(e) => Err(e.description().to_string()),
}
mailer(config)
.send(email.into())
.map_err(|e| e.to_string())
.and(Ok(()))
}

View File

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

View File

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