Compare commits

..

167 Commits

Author SHA1 Message Date
Daniel García
2c2276c5bb Merge pull request #585 from ViViDboarder/mail-auth-over-insecure
Allow explicitly defined smtp auth mechansim
2019-08-27 20:21:23 +02:00
ViViDboarder
672a245548 Remove unecessary clone 2019-08-27 10:40:38 -07:00
ViViDboarder
2d2745195e Allow explicitly defined smtp auth mechansim 2019-08-23 16:22:14 -07:00
Daniel García
026f9da035 Allow removing users two factors 2019-08-21 17:13:06 +02:00
Daniel García
d23d4f2c1d Allow editing HIBP key in the admin panel 2019-08-20 23:53:00 +02:00
Daniel García
515b87755a Update HIBP to v3, requires paid API key, fixes #583 2019-08-20 20:07:12 +02:00
Daniel García
d8ea3d2bfe Merge pull request #582 from vverst/require-device-email-config
Add config option to require new device emails
2019-08-19 22:58:50 +02:00
vpl
ee7837d022 Add option to require new device emails 2019-08-19 22:14:00 +02:00
Daniel García
07743e490b Ignore error sending device email 2019-08-18 19:32:26 +02:00
Daniel García
9101d6e48f Update dependencies 2019-08-18 19:31:54 +02:00
Daniel García
27c23b60b8 Merge pull request #571 from BlackDex/icon-proxy-support
Added reqwest proxy support
2019-08-15 22:14:10 +02:00
BlackDex
e7b6238f43 Added reqwest proxy support 2019-08-12 17:24:32 +02:00
Daniel García
8be2ed6255 Update web vault to 2.11.0 2019-07-30 19:50:35 +02:00
Daniel García
c9c3f07171 Updated dependencies and fixed panic getting icons 2019-07-30 19:42:05 +02:00
Daniel García
8a21c6df10 Merge pull request #541 from vverst/mail-new-device
Add "New Device Logged In From" email
2019-07-30 13:18:42 -04:00
vpl
df71f57d86 Move send device email to end of password login
Send new device email after two factor authentication.
2019-07-25 21:10:27 +02:00
vpl
60e39a9dd1 Move retrieve/new device from connData to separate function 2019-07-22 12:30:26 +02:00
vpl
bc6a53b847 Add new device email when user logs in 2019-07-22 08:26:24 +02:00
Daniel García
05a1137828 Move backend checks to build.rs to fail fast, and updated dependencies 2019-07-09 17:26:34 +02:00
Daniel García
cef38bf40b Merge pull request #525 from fbartels/hadolint
use hadolint for linting Dockerfiles
2019-07-09 17:22:27 +02:00
Felix Bartels
0b13a8c4aa last round of linting fixes
Signed-off-by: Felix Bartels <felix@host-consultants.de>
2019-07-06 08:36:18 +02:00
Felix Bartels
3fbd7919d8 more linting fixes
Signed-off-by: Felix Bartels <felix@host-consultants.de>
2019-07-06 08:16:05 +02:00
Felix Bartels
5f688ff209 no more linting errors for the main Dockerfile
Signed-off-by: Felix Bartels <felix@host-consultants.de>
2019-07-05 22:45:29 +02:00
Felix Bartels
f6cfb5bf21 add hadolint config file
to globally ignore certain rules
2019-07-05 11:06:44 +02:00
Felix Bartels
df8c9f39ac add hadolint to travisfile
Signed-off-by: Felix Bartels <felix@host-consultants.de>
2019-07-04 15:59:50 +02:00
Daniel García
d7ee7caed4 Merge pull request #520 from njfox/fix-email-alias
Fix #468 - Percent-encode the email address in invite link
2019-07-03 22:42:42 +02:00
Nick Fox
2e300da057 Fix #468 - Percent-encode the email address in invite link 2019-07-02 22:55:13 -04:00
Daniel García
3fb63bbe8c Merge pull request #514 from mprasil/dockerfile_cleanup
Dockerfile cleanup
2019-06-26 17:20:10 +02:00
Miro Prasil
9671ed4cca Symlink amd64 Dockerfile to repo root 2019-06-24 09:59:43 +01:00
Miro Prasil
d10ef3fd4b Create Dockerfiles for mysql builds 2019-06-24 09:56:26 +01:00
Miro Prasil
dd0b847912 Move current dockerfiles to their arch folders 2019-06-24 09:52:55 +01:00
Daniel García
8c34ff5d23 Merge pull request #511 from CubityFirst/patch-1
Corrected Spelling
2019-06-18 18:28:00 +02:00
Daniel García
15750256e2 Merge pull request #510 from mprasil/armv6_fix
Making a symlink is no longer necessary
2019-06-18 18:27:47 +02:00
Cubity_First
6989fc7bdb Corrected Spelling
Changed it from Chache to Cache on Line 207
2019-06-18 15:45:19 +01:00
Miro Prasil
4923614730 Making a symlink is no longer necessary 2019-06-17 12:16:26 +01:00
Daniel García
76f38621de Update dependencies and remove unwraps from Cipher::to_json 2019-06-14 22:51:50 +02:00
Daniel García
fff72889f6 Document DB URL in .env file 2019-06-02 13:44:59 +02:00
Daniel García
12af32b9ea Don't print DB URL 2019-06-02 13:39:16 +02:00
Daniel García
9add8e19eb Update dependencies and remove travis unused feature 2019-06-02 00:28:20 +02:00
Daniel García
5710703c50 Make sure the backup option only appears when using sqlite 2019-06-02 00:08:52 +02:00
Daniel García
1322b876e9 Merge pull request #493 from endyman/feature/initial_mysql_support
Initial support for mysql
2019-06-01 23:33:06 +02:00
Daniel García
9ed2ba61c6 Merge pull request #475 from TheMardy/master
Create Backup funcitonality
2019-06-01 23:29:58 +02:00
Nils Domrose
62a461ae15 remove syslog from ci, make features flag more clear 2019-05-30 22:19:58 +02:00
Nils Domrose
6f7220b68e adapt other Dockerfiles 2019-05-28 11:56:49 +02:00
Nils Domrose
4859932d35 fixed typo 2019-05-28 07:48:17 +02:00
Nils Domrose
ee277de707 include libsqlite3-sys optionally, removed non common features 2019-05-27 23:31:56 +02:00
Nils Domrose
c11f47903a revert include libsqlite3-sys optionally 2019-05-27 23:18:45 +02:00
Nils Domrose
6a5f1613e7 include libsqlite3-sys optionally 2019-05-27 23:07:47 +02:00
Nils Domrose
dc36f0cb6c re-added sqlite check_db code, cleanup 2019-05-27 22:58:52 +02:00
Nils Domrose
6c38026ef5 user char(36) for uuid columns 2019-05-27 17:20:20 +02:00
Nils Domrose
4c9cc9890c adapt travis to not enable conflicting features 2019-05-27 00:41:42 +02:00
Nils Domrose
f57b407c60 fix cargo syntax 2019-05-27 00:29:31 +02:00
Nils Domrose
ce0651b79c fix mysql package in ubuntu 2019-05-27 00:23:42 +02:00
Nils Domrose
edc26cb1e1 adapt pipline to no enable conflicting features 2019-05-27 00:19:59 +02:00
Nils Domrose
ff759397f6 initial mysql support 2019-05-26 23:03:05 +02:00
Emil Madsen
badd22ac3d Make docker image build 2019-05-20 22:36:27 +02:00
Emil Madsen
6f78395ef7 Passwordless sudo on azure? 2019-05-20 21:59:18 +02:00
Emil Madsen
5fb6531db8 Attempt to fix azure pipeline 2019-05-20 21:54:01 +02:00
Emil Madsen
eb9d5e1196 Reintroduce .env.template 2019-05-20 21:34:20 +02:00
Emil Madsen
233b48bdad Fix missing joinable in schema 2019-05-20 21:30:31 +02:00
Emil Madsen
e22e290f67 Fix key and type variable names for mysql 2019-05-20 21:24:29 +02:00
Emil Madsen
ab95a69dc8 Rework migrations for MySQL 2019-05-20 21:12:41 +02:00
Emil Madsen
85c8a01f4a Merge branch 'master' of github.com:Skeen/bitwarden_rs 2019-05-20 19:53:18 +02:00
Emil Madsen
42af7c6dab MySQL database 2019-05-20 19:53:14 +02:00
Daniel García
08a445e2ac Merge pull request #484 from mprasil/hub_repo_change
Point to the new docker hub image location
2019-05-17 15:44:07 +02:00
Daniel García
c0b2877da3 Update deps and swap back to official u2f crate again 2019-05-17 15:39:36 +02:00
Miro Prasil
cf8ca85289 Point to the new docker hub image location 2019-05-16 15:04:51 +01:00
Daniel García
a8a92f6c51 New vault patch release 2019-05-15 18:11:39 +02:00
Daniel García
95f833aacd Update dependencies to use new ring 2019-05-15 18:10:25 +02:00
Daniel García
4f45cc081f Update ring to 0.14, jwt to 6.0, and u2f 2019-05-11 23:18:18 +02:00
Daniel García
2a4cd24c60 Updated web vault to hide org plans again and updated dependencies 2019-05-11 22:27:51 +02:00
TheMardy
ef551f4cc6 Create Backup funcitonality
Added create backup functionality to the admin panel
2019-05-03 15:46:29 +02:00
Daniel García
4545f271c3 Merge pull request #473 from Starbix/patch-1
Update Runtime Base Image to Alpine v3.9
2019-05-02 22:33:43 +02:00
Cédric Laubacher
2768396a72 Update Runtime Base Image to Alpine v3.9 2019-05-02 21:28:34 +02:00
Daniel García
5521a86693 Change path for served images to avoid collision with vault images 2019-05-01 16:19:22 +02:00
Daniel García
3160780549 Merge pull request #401 from TheMardy/master
Images in Email Templates
2019-04-30 17:52:10 +02:00
TheMardy
f0701657a9 Changed to Bitwarden_RS Logo 2019-04-30 16:08:53 +02:00
Daniel García
21325b7523 Updated .env template 2019-04-27 20:14:37 +02:00
Daniel García
874f5c34bd Formatting 2019-04-26 22:08:26 +02:00
Daniel García
eadab2e9ca Updated dependencies 2019-04-26 22:07:00 +02:00
Daniel García
253faaf023 Use users duo host when required, instead of always using the global one 2019-04-15 13:07:23 +02:00
Daniel García
3d843a6a51 Merge pull request #460 from janost/organization-vault-purge
Fixed purging organization vault
2019-04-14 22:30:51 +02:00
janost
03fdf36bf9 Fixed purging organization vault 2019-04-14 22:12:48 +02:00
Daniel García
fdcc32beda Validate Duo credentials when custom 2019-04-14 22:05:05 +02:00
Daniel García
bf20355c5e Merge branch 'duo' 2019-04-14 22:02:55 +02:00
Daniel García
0136c793b4 Implement better user status API, in the future we'll probably want a way to disable users.
We should migrate from the empty password hash to a separate column then.
2019-04-13 00:01:52 +02:00
Daniel García
2e12114350 Always create the user when inviting from admin panel 2019-04-12 23:44:49 +02:00
Daniel García
f25ab42ebb Merge pull request #455 from ViViDboarder/get_users
Add new endpoint for retrieving all users
2019-04-11 20:42:44 +02:00
ViViDboarder
d3a8a278e6 Add new endpoint for retrieving all users 2019-04-11 11:24:53 -07:00
Daniel García
8d9827c55f Implement selection between global config and user settings for duo keys. 2019-04-11 18:40:03 +02:00
Daniel García
cad63f9761 Auto generate akey 2019-04-11 16:08:26 +02:00
Daniel García
bf446f44f9 Enable DATA_FOLDER to affect default CONFIG_FILE path 2019-04-11 15:41:13 +02:00
Daniel García
621f607297 Update dependencies and fix some warnings 2019-04-11 15:40:19 +02:00
Daniel García
d89bd707a8 Update vault release to show duo button 2019-04-07 18:58:32 +02:00
Daniel García
754087b990 Add global duo config and document options in .env template 2019-04-07 18:58:15 +02:00
Daniel García
cfbeb56371 Implement user duo, initial version
TODO:
- At the moment each user needs to configure a DUO application and input the API keys, we need to check if multiple users can register with the same keys correctly and if so we could implement a global setting.
- Sometimes the Duo frame doesn't load correctly, but canceling, reloading the page and logging in again seems to fix it for me.
2019-04-05 22:09:53 +02:00
Daniel García
3bb46ce496 Make the syslog crate non-optional when available 2019-04-02 22:35:22 +02:00
Daniel García
c5832f2b30 With the latest fern, syslog can be a config option instead of a build flag 2019-03-29 20:27:20 +01:00
Daniel García
d9406b0095 Update to web vault 2.10.0 2019-03-25 23:49:12 +01:00
Daniel García
2475c36a75 Implement log_level config option 2019-03-25 14:23:14 +01:00
Daniel García
c384f9c0ca Set default log level to Info, we don't use debug anyway and it just fills the logs with other crates info. 2019-03-25 14:21:50 +01:00
Daniel García
afbfebf659 Merge pull request #440 from BlackDex/mail-encoding
Fixed long e-mail message extending 1000 lines.
2019-03-25 13:09:51 +01:00
BlackDex
6b686c18f7 Fixed long e-mail message extending 1000 lines.
- Added quoted_printable crate to encode the e-mail messages.
- Change the way the e-mail gets build to use custom part headers.
2019-03-25 09:48:19 +01:00
Daniel García
349cb33fbd Updated dependencies 2019-03-23 19:48:22 +01:00
Daniel García
d7542b6818 Merge pull request #437 from njfox/fix-smtp-error
Split up long line to stop SMTP from breaking
2019-03-21 14:22:57 +01:00
Nick Fox
7976d39d9d Adjust whitespace 2019-03-20 23:29:29 -04:00
Nick Fox
5ee9676941 Break up long line to stop SMTP from breaking 2019-03-20 23:24:30 -04:00
Daniel García
4b40cda910 Added domain blacklist regex for icons service and improved valid domain check.
Reorganized the icons code a bit.
2019-03-18 22:12:39 +01:00
Daniel García
4689ed7b30 Changed uppercase deserializer to avoid a clone. 2019-03-18 22:02:37 +01:00
Daniel García
084bc2aee3 Use final release of lettre and update dependencies 2019-03-17 14:43:22 +01:00
Daniel García
6d7e15b2fd Use web vault 2.9.0 release 2019-03-14 13:29:03 +01:00
Daniel García
61515160a7 Allow changing error codes and create an empty error.
Return 404 instead of 400 when no accounts breached.
2019-03-14 00:17:36 +01:00
Daniel García
a25bfdd16d Remove unused features from multipart (integration with other servers) 2019-03-13 15:57:00 +01:00
Daniel García
e93538cea9 Add option to use wrapped TLS in email, instead of STARTTLS upgrade 2019-03-10 14:45:42 +01:00
Daniel García
b4244b28b6 Update admin page scripts and fixed broken tooltip 2019-03-09 14:41:34 +01:00
Daniel García
43f9038325 Add option to force resync clients in admin panel 2019-03-07 21:08:33 +01:00
Daniel García
27872f476e Update dependencies 2019-03-07 20:22:08 +01:00
Daniel García
339044f8aa Add warning about config panel values overriding env vars. 2019-03-07 20:22:02 +01:00
Daniel García
0718a090e1 Trim spaces from admin token during authentication and validate that the admin panel token is not empty 2019-03-07 20:21:50 +01:00
Daniel García
9e1f030a80 Explicitly close SMTP connection in case of error. 2019-03-07 20:21:10 +01:00
Daniel García
04922f6aa0 Some formatting and dependency updates 2019-03-03 16:11:55 +01:00
Daniel García
7d2bc9e162 Added option to force 2fa at logins and made some changes to two factor code.
Added newlines to config options to keep them a reasonable length.
2019-03-03 16:09:15 +01:00
Daniel García
c6c00729e3 Update vault to new version. No need to wait for a release when even the official web vault is already using it 2019-02-27 17:28:04 +01:00
Daniel García
10756b0920 Update dependencies and fix some lints 2019-02-27 17:21:04 +01:00
Daniel García
1eb1502a07 Merge pull request #416 from mprasil/armv6
Armv6
2019-02-25 18:26:53 +01:00
Miroslav Prasil
30e72a96a9 Symlink missing ld-linux file 2019-02-25 16:17:34 +00:00
Daniel García
2646db78a4 Merge pull request #414 from FrankPetrilli/patch-1
Minor typo fix conect => connect
2019-02-25 14:21:28 +01:00
Miroslav Prasil
f5358b13f5 Add Dockerfile for armv6 2019-02-25 12:17:22 +00:00
Frank Petrilli
d156170971 Minor typo fix conect => connect 2019-02-24 16:08:38 -08:00
Daniel García
d9bfe847db Merge pull request #410 from gdamjan/remove-uneeded-mutability
remove some unneeded mutability
2019-02-22 22:52:53 +01:00
Дамјан Георгиевски
473f8b8e31 remove some unneeded mutability 2019-02-22 20:25:50 +01:00
Daniel García
aeb4b4c8a5 Remove verbose, otherwise the logs get filled with useless info 2019-02-22 16:16:07 +01:00
Daniel García
980a3e45db Set up CI with Azure Pipelines 2019-02-22 15:51:30 +01:00
Daniel García
5794969f5b Merge pull request #406 from shauder/feature/disable-admin-token
Allow the Admin token to be disabled in the advanced menu
2019-02-20 23:06:52 +01:00
Shane Faulkner
8b5b06c3d1 Allow the Admin token to be disabled in the advanced menu 2019-02-20 14:56:08 -06:00
Daniel García
b50c27b619 Print a warning when an env variable is being overriden by the config file, and reorganize the main file a bit.
Modified the JWT key generation, now it should also show the output of OpenSSL in the logs.
2019-02-20 20:59:37 +01:00
Daniel García
5ee04e31e5 Updated dependencies, removed some unnecessary clones and fixed some lints 2019-02-20 17:54:18 +01:00
Daniel García
bf6ae91a6d Remove margins on small devices 2019-02-18 20:43:34 +01:00
Daniel García
828e3a5795 Add extra padding when the toolbar collapses in small devices 2019-02-18 20:33:32 +01:00
Daniel García
7b5bcd45f8 Show read-only options in the config panel and the env variable names in the tooltips 2019-02-18 19:25:33 +01:00
Daniel García
72de16fb86 Merge pull request #404 from mprasil/disable_wal
Add an option to not enable WAL (should help in #399)
2019-02-18 16:10:16 +01:00
Miroslav Prasil
0b903fc5f4 Extended the template file and refer to wiki 2019-02-18 14:57:21 +00:00
Miroslav Prasil
4df686f49e Add an option to not enable WAL (should help in #399) 2019-02-18 10:48:48 +00:00
Daniel García
d7eeaaf249 Escape user data from admin panel when calling JS 2019-02-17 15:24:14 +01:00
TheMardy
84fb6aaddb Set correct MIME type 2019-02-17 01:08:24 +01:00
Daniel García
a744b9437a Implemented multiple U2f keys, key names, and compromised checks 2019-02-16 23:07:48 +01:00
Daniel García
6027b969f5 Delete old devices when deauthorizing user sessions 2019-02-16 23:06:26 +01:00
Daniel García
93805a5d7b Fix Yubikeys deleted on error 2019-02-16 21:30:55 +01:00
Daniel García
71da961ecd Merge pull request #402 from mprasil/version_in_docker
Include git repo in build so we get version
2019-02-16 12:20:25 +01:00
Miroslav Prasil
dd421809e5 Include git repo in build so we get version 2019-02-16 08:50:16 +00:00
TheMardy
8526055bb7 Added images to email templates 2019-02-16 03:48:23 +01:00
TheMardy
a79334ea4c Added static email image routes 2019-02-16 03:44:30 +01:00
Daniel García
274ea9a4f2 Use the latest fast_chemail crate directly, with the fix 2019-02-15 14:39:30 +01:00
Daniel García
8743d18aca Update travis image and remove now-ignored sudo tag 2019-02-13 18:50:45 +01:00
Daniel García
d3773a433a Removed list of mounted routes at startup by default, with option to add it back. This would get annoying when starting the server frequently, because it printed ~130 lines of mostly useless info 2019-02-13 00:03:16 +01:00
Daniel García
0f0a87becf Add version to initial message 2019-02-12 22:47:00 +01:00
Daniel García
4b57bb8eeb Merge pull request #394 from BlackDex/icon-timeout
Added config option for icon download timeout
2019-02-12 22:00:12 +01:00
BlackDex
3b27dbb0aa Added config option for icon download timeout 2019-02-12 21:56:28 +01:00
Daniel García
ff2fbd322e Update deps and fix email check 2019-02-12 15:01:02 +01:00
Daniel García
9636f33fdb Implement constant time equal check for admin, 2fa recover and 2fa remember tokens 2019-02-11 23:45:55 +01:00
Daniel García
bbe2a1b264 Merge pull request #391 from TheMardy/master
Updated Email Templates
2019-02-10 22:03:20 +01:00
Daniel García
79fdfd6524 Add missing url parameter 2019-02-10 21:40:20 +01:00
Daniel García
d086a99e5b Implemented HTML emails with text alternative 2019-02-10 19:12:34 +01:00
TheMardy
22b0b95209 Added HTML templates (+14 squashed commit)
Squashed commit:

[ece2260] Plaintext send_org_invite

[01d4884] Plaintext pw_hint_some

[6ce5173] Plaintext pw_hint_none

[881af3e] Plaintext invite_confirmed

[ce78621] Plaintext invite_accepted

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

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

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

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

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

[d26d662] Updated send_org_invite template

[71f47af] Updated pw_hint_some template

[c2ca3c2] Updated pw_hint_none template

[50f8bfb] Updated invite_accepted template

[17f96f8] Updated invite_confirmed template
2019-02-10 19:04:18 +01:00
Daniel García
28d1588e73 Show version in admin panel 2019-02-10 16:02:46 +01:00
Daniel García
f3b1a5ff3e Error when admin panel is disabled 2019-02-10 15:26:19 +01:00
Daniel García
330e90a6ac Hide secrets in config panel 2019-02-08 20:49:04 +01:00
112 changed files with 4956 additions and 1820 deletions

View File

@@ -9,10 +9,6 @@ data
.idea .idea
*.iml *.iml
# Git files
.git
.gitignore
# Documentation # Documentation
*.md *.md

View File

@@ -4,8 +4,13 @@
## Main data folder ## Main data folder
# DATA_FOLDER=data # DATA_FOLDER=data
## Individual folders, these override %DATA_FOLDER% ## Database URL
## When using SQLite, this is the path to the DB file, default to %DATA_FOLDER%/db.sqlite3
## When using MySQL, this it is the URL to the DB, including username and password:
## Format: mysql://[user[:password]@]host/database_name
# DATABASE_URL=data/db.sqlite3 # DATABASE_URL=data/db.sqlite3
## Individual folders, these override %DATA_FOLDER%
# RSA_KEY_FILENAME=data/rsa_key # RSA_KEY_FILENAME=data/rsa_key
# ICON_CACHE_FOLDER=data/icon_cache # ICON_CACHE_FOLDER=data/icon_cache
# ATTACHMENTS_FOLDER=data/attachments # ATTACHMENTS_FOLDER=data/attachments
@@ -35,7 +40,7 @@
## Enable extended logging ## Enable extended logging
## This shows timestamps and allows logging to file and to syslog ## This shows timestamps and allows logging to file and to syslog
### To enable logging to file, use the LOG_FILE env variable ### To enable logging to file, use the LOG_FILE env variable
### To enable syslog, you need to compile with `cargo build --features=enable_syslog' ### To enable syslog, use the USE_SYSLOG env variable
# EXTENDED_LOGGING=true # EXTENDED_LOGGING=true
## Logging to file ## Logging to file
@@ -43,12 +48,46 @@
## It's recommended to also set 'ROCKET_CLI_COLORS=off' ## It's recommended to also set 'ROCKET_CLI_COLORS=off'
# LOG_FILE=/path/to/log # LOG_FILE=/path/to/log
## Logging to Syslog
## This requires extended logging
## It's recommended to also set 'ROCKET_CLI_COLORS=off'
# USE_SYSLOG=false
## Log level
## Change the verbosity of the log output
## Valid values are "trace", "debug", "info", "warn", "error" and "off"
## This requires extended logging
# LOG_LEVEL=Info
## Enable WAL for the DB
## Set to false to avoid enabling WAL during startup.
## Note that if the DB already has WAL enabled, you will also need to disable WAL in the DB,
## this setting only prevents bitwarden_rs from automatically enabling it on start.
## Please read project wiki page about this setting first before changing the value as it can
## cause performance degradation or might render the service unable to start.
# ENABLE_DB_WAL=true
## Disable icon downloading ## Disable icon downloading
## Set to true to disable icon downloading, this would still serve icons from $ICON_CACHE_FOLDER, ## Set to true to disable icon downloading, this would still serve icons from $ICON_CACHE_FOLDER,
## but it won't produce any external network request. Needs to set $ICON_CACHE_TTL to 0, ## but it won't produce any external network request. Needs to set $ICON_CACHE_TTL to 0,
## otherwise it will delete them and they won't be downloaded again. ## otherwise it will delete them and they won't be downloaded again.
# DISABLE_ICON_DOWNLOAD=false # DISABLE_ICON_DOWNLOAD=false
## Icon download timeout
## Configure the timeout value when downloading the favicons.
## The default is 10 seconds, but this could be to low on slower network connections
# ICON_DOWNLOAD_TIMEOUT=10
## Icon blacklist Regex
## Any domains or IPs that match this regex won't be fetched by the icon service.
## Useful to hide other servers in the local network. Check the WIKI for more details
# ICON_BLACKLIST_REGEX=192\.168\.1\.[0-9].*^
## Disable 2FA remember
## Enabling this would force the users to use a second factor to login every time.
## Note that the checkbox would still be present, but ignored.
# DISABLE_2FA_REMEMBER=false
## Controls if new users can register ## Controls if new users can register
# SIGNUPS_ALLOWED=true # SIGNUPS_ALLOWED=true
@@ -56,6 +95,7 @@
## One option is to use 'openssl rand -base64 48' ## One option is to use 'openssl rand -base64 48'
## If not set, the admin panel is disabled ## If not set, the admin panel is disabled
# ADMIN_TOKEN=Vy2VyYTTsKPv8W5aEOWUbB/Bt3DEKePbHmI4m9VcemUMS2rEviDowNAFqYi1xjmp # ADMIN_TOKEN=Vy2VyYTTsKPv8W5aEOWUbB/Bt3DEKePbHmI4m9VcemUMS2rEviDowNAFqYi1xjmp
# DISABLE_ADMIN_TOKEN=false
## Invitations org admins to invite users, even when signups are disabled ## Invitations org admins to invite users, even when signups are disabled
# INVITATIONS_ALLOWED=true # INVITATIONS_ALLOWED=true
@@ -82,6 +122,17 @@
# YUBICO_SECRET_KEY=AAAAAAAAAAAAAAAAAAAAAAAA # YUBICO_SECRET_KEY=AAAAAAAAAAAAAAAAAAAAAAAA
# YUBICO_SERVER=http://yourdomain.com/wsapi/2.0/verify # YUBICO_SERVER=http://yourdomain.com/wsapi/2.0/verify
## Duo Settings
## You need to configure all options to enable global Duo support, otherwise users would need to configure it themselves
## Create an account and protect an application as mentioned in this link (only the first step, not the rest):
## https://help.bitwarden.com/article/setup-two-step-login-duo/#create-a-duo-security-account
## Then set the following options, based on the values obtained from the last step:
# DUO_IKEY=<Integration Key>
# DUO_SKEY=<Secret Key>
# DUO_HOST=<API Hostname>
## After that, you should be able to follow the rest of the guide linked above,
## ignoring the fields that ask for the values that you already configured beforehand.
## Rocket specific settings, check Rocket documentation to learn more ## Rocket specific settings, check Rocket documentation to learn more
# ROCKET_ENV=staging # ROCKET_ENV=staging
# ROCKET_ADDRESS=0.0.0.0 # Enable this to test mobile app # ROCKET_ADDRESS=0.0.0.0 # Enable this to test mobile app
@@ -98,3 +149,4 @@
# SMTP_SSL=true # SMTP_SSL=true
# SMTP_USERNAME=username # SMTP_USERNAME=username
# SMTP_PASSWORD=password # SMTP_PASSWORD=password
# SMTP_AUTH_MECHANISM="Plain"

7
.hadolint.yaml Normal file
View File

@@ -0,0 +1,7 @@
ignored:
# disable explicit version for apt install
- DL3008
# disable explicit version for apk install
- DL3018
trustedRegistries:
- docker.io

View File

@@ -1,9 +1,20 @@
# Copied from Rocket's .travis.yml dist: xenial
env:
global:
- HADOLINT_VERSION=1.17.1
language: rust language: rust
sudo: required # so we get a VM with higher specs rust: nightly
dist: trusty # so we get a VM with higher specs
cache: cargo cache: cargo
rust:
- nightly before_install:
- sudo curl -L https://github.com/hadolint/hadolint/releases/download/v$HADOLINT_VERSION/hadolint-$(uname -s)-$(uname -m) -o /usr/local/bin/hadolint
- sudo chmod +rx /usr/local/bin/hadolint
# Nothing to install
install: true
script: script:
- cargo build --verbose --all-features - git ls-files --exclude='Dockerfile*' --ignored | xargs --max-lines=1 hadolint
- cargo build --features "sqlite"
- cargo build --features "mysql"

2220
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,53 +11,58 @@ publish = false
build = "build.rs" build = "build.rs"
[features] [features]
enable_syslog = ["syslog", "fern/syslog-4"] # Empty to keep compatibility, prefer to set USE_SYSLOG=true
enable_syslog = []
mysql = ["diesel/mysql", "diesel_migrations/mysql"]
sqlite = ["diesel/sqlite", "diesel_migrations/sqlite", "libsqlite3-sys"]
[target."cfg(not(windows))".dependencies]
syslog = "4.0.1"
[dependencies] [dependencies]
# Web framework for nightly with a focus on ease-of-use, expressibility, and speed. # Web framework for nightly with a focus on ease-of-use, expressibility, and speed.
rocket = { version = "0.4.0", features = ["tls"], default-features = false } rocket = { version = "0.5.0-dev", features = ["tls"], default-features = false }
rocket_contrib = "0.4.0" rocket_contrib = "0.5.0-dev"
# HTTP client # HTTP client
reqwest = "0.9.9" reqwest = "0.9.19"
# multipart/form-data support # multipart/form-data support
multipart = "0.16.1" multipart = { version = "0.16.1", features = ["server"], default-features = false }
# WebSockets library # WebSockets library
ws = "0.7.9" ws = "0.9.0"
# MessagePack library # MessagePack library
rmpv = "0.4.0" rmpv = "0.4.0"
# Concurrent hashmap implementation # Concurrent hashmap implementation
chashmap = "2.2.0" chashmap = "2.2.2"
# A generic serialization/deserialization framework # A generic serialization/deserialization framework
serde = "1.0.85" serde = "1.0.99"
serde_derive = "1.0.85" serde_derive = "1.0.99"
serde_json = "1.0.37" serde_json = "1.0.40"
# Logging # Logging
log = "0.4.6" log = "0.4.8"
fern = "0.5.7" fern = { version = "0.5.8", features = ["syslog-4"] }
syslog = { version = "4.0.1", optional = true }
# A safe, extensible ORM and Query builder # A safe, extensible ORM and Query builder
diesel = { version = "1.4.1", features = ["sqlite", "chrono", "r2d2"] } diesel = { version = "1.4.2", features = [ "chrono", "r2d2"] }
diesel_migrations = { version = "1.4.0", features = ["sqlite"] } diesel_migrations = "1.4.0"
# Bundled SQLite # Bundled SQLite
libsqlite3-sys = { version = "0.12.0", features = ["bundled"] } libsqlite3-sys = { version = "0.12.0", features = ["bundled"], optional = true }
# Crypto library # Crypto library
ring = { version = "0.13.5", features = ["rsa_signing"] } ring = "0.14.6"
# UUID generation # UUID generation
uuid = { version = "0.7.2", features = ["v4"] } uuid = { version = "0.7.4", features = ["v4"] }
# Date and time library for Rust # Date and time library for Rust
chrono = "0.4.6" chrono = "0.4.7"
# TOTP library # TOTP library
oath = "0.10.2" oath = "0.10.2"
@@ -66,44 +71,47 @@ oath = "0.10.2"
data-encoding = "2.1.2" data-encoding = "2.1.2"
# JWT library # JWT library
jsonwebtoken = "5.0.1" jsonwebtoken = "6.0.1"
# U2F library # U2F library
u2f = "0.1.4" u2f = "0.1.6"
# Yubico Library # Yubico Library
yubico = { version = "0.5.1", features = ["online"], default-features = false } yubico = { version = "0.6.1", features = ["online", "online-tokio"], default-features = false }
# A `dotenv` implementation for Rust # A `dotenv` implementation for Rust
dotenv = { version = "0.13.0", default-features = false } dotenv = { version = "0.14.1", default-features = false }
# Lazy static macro # Lazy static macro
lazy_static = { version = "1.2.0", features = ["nightly"] } lazy_static = "1.3.0"
# More derives # More derives
derive_more = "0.13.0" derive_more = "0.15.0"
# Numerical libraries # Numerical libraries
num-traits = "0.2.6" num-traits = "0.2.8"
num-derive = "0.2.4" num-derive = "0.2.5"
# Email libraries # Email libraries
lettre = "0.9.0" lettre = "0.9.2"
lettre_email = "0.9.0" lettre_email = "0.9.2"
native-tls = "0.2.2" native-tls = "0.2.3"
quoted_printable = "0.4.1"
# Template library # Template library
handlebars = "1.1.0" handlebars = "2.0.1"
# For favicon extraction from main website # For favicon extraction from main website
soup = "0.3.0" soup = "0.4.1"
regex = "1.1.0" regex = "1.2.1"
# URL encoding library
percent-encoding = "2.1.0"
[patch.crates-io] [patch.crates-io]
# Add support for Timestamp type # Add support for Timestamp type
rmp = { git = 'https://github.com/dani-garcia/msgpack-rust' } rmp = { git = 'https://github.com/dani-garcia/msgpack-rust' }
# Use new native_tls version 0.2 # Use newest ring
lettre = { git = 'https://github.com/lettre/lettre', rev = 'c988b1760ad81' } rocket = { git = 'https://github.com/SergioBenitez/Rocket', rev = 'dbcb0a75b9556763ac3ab708f40c8f8ed75f1a1e' }
lettre_email = { git = 'https://github.com/lettre/lettre', rev = 'c988b1760ad81' } rocket_contrib = { git = 'https://github.com/SergioBenitez/Rocket', rev = 'dbcb0a75b9556763ac3ab708f40c8f8ed75f1a1e' }

View File

@@ -1,86 +0,0 @@
# 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.8.0d"
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
# Using bundled SQLite, no need to install it
# RUN apt-get update && apt-get install -y\
# sqlite3\
# --no-install-recommends\
# && rm -rf /var/lib/apt/lists/*
# Creates a dummy project used to grab dependencies
RUN USER=root cargo new --bin app
WORKDIR /app
# Copies over *only* your manifests and build files
COPY ./Cargo.* ./
COPY ./rust-toolchain ./rust-toolchain
COPY ./build.rs ./build.rs
# Builds your dependencies and removes the
# dummy project, except the target folder
# This folder contains the compiled dependencies
RUN cargo build --release
RUN find . -not -path "./target*" -delete
# Copies the complete project
# To avoid copying unneeded files, use .dockerignore
COPY . .
# Make sure that we actually build the project
RUN touch src/main.rs
# Builds again, this time it'll just be
# your actual source files being built
RUN cargo build --release
######################## RUNTIME IMAGE ########################
# Create a new stage with a minimal image
# because we already have a binary built
FROM debian:stretch-slim
ENV ROCKET_ENV "staging"
ENV ROCKET_PORT=80
ENV ROCKET_WORKERS=10
# Install needed libraries
RUN apt-get update && apt-get install -y\
openssl\
ca-certificates\
--no-install-recommends\
&& rm -rf /var/lib/apt/lists/*
RUN mkdir /data
VOLUME /data
EXPOSE 80
EXPOSE 3012
# Copies the files from the context (Rocket.toml file and web-vault)
# and the binary from the "build" stage to the current stage
COPY Rocket.toml .
COPY --from=vault /web-vault ./web-vault
COPY --from=build app/target/release/bitwarden_rs .
# Configures the startup!
CMD ./bitwarden_rs

1
Dockerfile Symbolic link
View File

@@ -0,0 +1 @@
docker/amd64/sqlite/Dockerfile

View File

@@ -3,7 +3,7 @@
--- ---
[![Travis Build Status](https://travis-ci.org/dani-garcia/bitwarden_rs.svg?branch=master)](https://travis-ci.org/dani-garcia/bitwarden_rs) [![Travis Build Status](https://travis-ci.org/dani-garcia/bitwarden_rs.svg?branch=master)](https://travis-ci.org/dani-garcia/bitwarden_rs)
[![Docker Pulls](https://img.shields.io/docker/pulls/mprasil/bitwarden.svg)](https://hub.docker.com/r/mprasil/bitwarden) [![Docker Pulls](https://img.shields.io/docker/pulls/bitwardenrs/server.svg)](https://hub.docker.com/r/bitwardenrs/server)
[![Dependency Status](https://deps.rs/repo/github/dani-garcia/bitwarden_rs/status.svg)](https://deps.rs/repo/github/dani-garcia/bitwarden_rs) [![Dependency Status](https://deps.rs/repo/github/dani-garcia/bitwarden_rs/status.svg)](https://deps.rs/repo/github/dani-garcia/bitwarden_rs)
[![GitHub Release](https://img.shields.io/github/release/dani-garcia/bitwarden_rs.svg)](https://github.com/dani-garcia/bitwarden_rs/releases/latest) [![GitHub Release](https://img.shields.io/github/release/dani-garcia/bitwarden_rs.svg)](https://github.com/dani-garcia/bitwarden_rs/releases/latest)
[![GPL-3.0 Licensed](https://img.shields.io/github/license/dani-garcia/bitwarden_rs.svg)](https://github.com/dani-garcia/bitwarden_rs/blob/master/LICENSE.txt) [![GPL-3.0 Licensed](https://img.shields.io/github/license/dani-garcia/bitwarden_rs.svg)](https://github.com/dani-garcia/bitwarden_rs/blob/master/LICENSE.txt)
@@ -34,8 +34,8 @@ Basically full implementation of Bitwarden API is provided including:
Pull the docker image and mount a volume from the host for persistent storage: Pull the docker image and mount a volume from the host for persistent storage:
```sh ```sh
docker pull mprasil/bitwarden:latest docker pull bitwardenrs/server:latest
docker run -d --name bitwarden -v /bw-data/:/data/ -p 80:80 mprasil/bitwarden:latest docker run -d --name bitwarden -v /bw-data/:/data/ -p 80:80 bitwardenrs/server:latest
``` ```
This will preserve any persistent data under /bw-data/, you can adapt the path to whatever suits you. This will preserve any persistent data under /bw-data/, you can adapt the path to whatever suits you.

25
azure-pipelines.yml Normal file
View File

@@ -0,0 +1,25 @@
pool:
vmImage: 'Ubuntu-16.04'
steps:
- script: |
ls -la
curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain $(cat rust-toolchain)
echo "##vso[task.prependpath]$HOME/.cargo/bin"
displayName: 'Install Rust'
- script: |
sudo apt-get update
sudo apt-get install -y libmysql++-dev
displayName: Install libmysql
- script: |
rustc -Vv
cargo -V
displayName: Query rust and cargo versions
- script : cargo build --features "sqlite"
displayName: 'Build project with sqlite backend'
- script : cargo build --features "mysql"
displayName: 'Build project with mysql backend'

View File

@@ -1,11 +1,21 @@
use std::process::Command; use std::process::Command;
fn main() { fn main() {
#[cfg(all(feature = "sqlite", feature = "mysql"))]
compile_error!("Can't enable both backends");
#[cfg(not(any(feature = "sqlite", feature = "mysql")))]
compile_error!("You need to enable one DB backend. To build with previous defaults do: cargo build --features sqlite");
read_git_info().ok(); read_git_info().ok();
} }
fn run(args: &[&str]) -> Result<String, std::io::Error> { fn run(args: &[&str]) -> Result<String, std::io::Error> {
let out = Command::new(args[0]).args(&args[1..]).output()?; let out = Command::new(args[0]).args(&args[1..]).output()?;
if !out.status.success() {
use std::io::{Error, ErrorKind};
return Err(Error::new(ErrorKind::Other, "Command not successful"));
}
Ok(String::from_utf8(out.stdout).unwrap().trim().to_string()) Ok(String::from_utf8(out.stdout).unwrap().trim().to_string())
} }
@@ -13,8 +23,10 @@ fn run(args: &[&str]) -> Result<String, std::io::Error> {
fn read_git_info() -> Result<(), std::io::Error> { fn read_git_info() -> Result<(), std::io::Error> {
// The exact tag for the current commit, can be empty when // The exact tag for the current commit, can be empty when
// the current commit doesn't have an associated tag // the current commit doesn't have an associated tag
let exact_tag = run(&["git", "describe", "--abbrev=0", "--tags", "--exact-match"])?; let exact_tag = run(&["git", "describe", "--abbrev=0", "--tags", "--exact-match"]).ok();
println!("cargo:rustc-env=GIT_EXACT_TAG={}", exact_tag); if let Some(ref exact) = exact_tag {
println!("cargo:rustc-env=GIT_EXACT_TAG={}", exact);
}
// The last available tag, equal to exact_tag when // The last available tag, equal to exact_tag when
// the current commit is tagged // the current commit is tagged
@@ -27,13 +39,25 @@ fn read_git_info() -> Result<(), std::io::Error> {
// The current git commit hash // The current git commit hash
let rev = run(&["git", "rev-parse", "HEAD"])?; let rev = run(&["git", "rev-parse", "HEAD"])?;
let rev_short = rev.get(..12).unwrap_or_default(); let rev_short = rev.get(..8).unwrap_or_default();
println!("cargo:rustc-env=GIT_REV={}", rev_short); println!("cargo:rustc-env=GIT_REV={}", rev_short);
// Combined version
let version = if let Some(exact) = exact_tag {
exact
} else if &branch != "master" {
format!("{}-{} ({})", last_tag, rev_short, branch)
} else {
format!("{}-{}", last_tag, rev_short)
};
println!("cargo:rustc-env=GIT_VERSION={}", version);
// To access these values, use: // To access these values, use:
// env!("GIT_EXACT_TAG") // env!("GIT_EXACT_TAG")
// env!("GIT_LAST_TAG") // env!("GIT_LAST_TAG")
// env!("GIT_BRANCH") // env!("GIT_BRANCH")
// env!("GIT_REV") // env!("GIT_REV")
// env!("GIT_VERSION")
Ok(()) Ok(())
} }

View File

@@ -2,29 +2,35 @@
# https://docs.docker.com/develop/develop-images/multistage-build/ # https://docs.docker.com/develop/develop-images/multistage-build/
# https://whitfin.io/speeding-up-rust-docker-builds/ # https://whitfin.io/speeding-up-rust-docker-builds/
####################### VAULT BUILD IMAGE ####################### ####################### VAULT BUILD IMAGE #######################
FROM alpine as vault FROM alpine:3.10 as vault
ENV VAULT_VERSION "v2.8.0d" ENV VAULT_VERSION "v2.11.0"
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz" ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
RUN apk add --update-cache --upgrade \ RUN apk add --no-cache --upgrade \
curl \ curl \
tar tar
RUN mkdir /web-vault RUN mkdir /web-vault
WORKDIR /web-vault WORKDIR /web-vault
SHELL ["/bin/ash", "-eo", "pipefail", "-c"]
RUN curl -L $URL | tar xz RUN curl -L $URL | tar xz
RUN ls RUN ls
########################## BUILD IMAGE ########################## ########################## BUILD IMAGE ##########################
# We need to use the Rust build image, because # We need to use the Rust build image, because
# we need the Rust compiler and Cargo tooling # we need the Rust compiler and Cargo tooling
FROM rust as build FROM rust:1.36 as build
# set mysql backend
ARG DB=mysql
RUN apt-get update \ RUN apt-get update \
&& apt-get install -y \ && apt-get install -y \
--no-install-recommends \
gcc-aarch64-linux-gnu \ gcc-aarch64-linux-gnu \
&& mkdir -p ~/.cargo \ && mkdir -p ~/.cargo \
&& echo '[target.aarch64-unknown-linux-gnu]' >> ~/.cargo/config \ && echo '[target.aarch64-unknown-linux-gnu]' >> ~/.cargo/config \
@@ -41,8 +47,10 @@ RUN sed 's/^deb/deb-src/' /etc/apt/sources.list > \
&& dpkg --add-architecture arm64 \ && dpkg --add-architecture arm64 \
&& apt-get update \ && apt-get update \
&& apt-get install -y \ && apt-get install -y \
--no-install-recommends \
libssl-dev:arm64 \ libssl-dev:arm64 \
libc6-dev:arm64 libc6-dev:arm64 \
libmariadb-dev:arm64
ENV CC_aarch64_unknown_linux_gnu="/usr/bin/aarch64-linux-gnu-gcc" ENV CC_aarch64_unknown_linux_gnu="/usr/bin/aarch64-linux-gnu-gcc"
ENV CROSS_COMPILE="1" ENV CROSS_COMPILE="1"
@@ -55,7 +63,7 @@ COPY . .
# Build # Build
RUN rustup target add aarch64-unknown-linux-gnu RUN rustup target add aarch64-unknown-linux-gnu
RUN cargo build --release --target=aarch64-unknown-linux-gnu -v RUN cargo build --features ${DB} --release --target=aarch64-unknown-linux-gnu -v
######################## RUNTIME IMAGE ######################## ######################## RUNTIME IMAGE ########################
# Create a new stage with a minimal image # Create a new stage with a minimal image
@@ -70,9 +78,10 @@ RUN [ "cross-build-start" ]
# Install needed libraries # Install needed libraries
RUN apt-get update && apt-get install -y \ RUN apt-get update && apt-get install -y \
--no-install-recommends \
openssl \ openssl \
ca-certificates \ ca-certificates \
--no-install-recommends\ libmariadbclient-dev \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
RUN mkdir /data RUN mkdir /data
@@ -89,4 +98,4 @@ COPY --from=vault /web-vault ./web-vault
COPY --from=build /app/target/aarch64-unknown-linux-gnu/release/bitwarden_rs . COPY --from=build /app/target/aarch64-unknown-linux-gnu/release/bitwarden_rs .
# Configures the startup! # Configures the startup!
CMD ./bitwarden_rs CMD ["./bitwarden_rs"]

View File

@@ -0,0 +1,101 @@
# Using multistage build:
# https://docs.docker.com/develop/develop-images/multistage-build/
# https://whitfin.io/speeding-up-rust-docker-builds/
####################### VAULT BUILD IMAGE #######################
FROM alpine:3.10 as vault
ENV VAULT_VERSION "v2.11.0"
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
RUN apk add --no-cache --upgrade \
curl \
tar
RUN mkdir /web-vault
WORKDIR /web-vault
SHELL ["/bin/ash", "-eo", "pipefail", "-c"]
RUN curl -L $URL | tar xz
RUN ls
########################## BUILD IMAGE ##########################
# We need to use the Rust build image, because
# we need the Rust compiler and Cargo tooling
FROM rust:1.36 as build
# set sqlite as default for DB ARG for backward comaptibility
ARG DB=sqlite
RUN apt-get update \
&& apt-get install -y \
--no-install-recommends \
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 \
--no-install-recommends \
libssl-dev:arm64 \
libc6-dev:arm64 \
libmariadb-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 --features ${DB} --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 \
--no-install-recommends \
openssl \
ca-certificates \
libmariadbclient-dev \
&& rm -rf /var/lib/apt/lists/*
RUN mkdir /data
RUN [ "cross-build-end" ]
VOLUME /data
EXPOSE 80
# Copies the files from the context (Rocket.toml file and web-vault)
# and the binary from the "build" stage to the current stage
COPY Rocket.toml .
COPY --from=vault /web-vault ./web-vault
COPY --from=build /app/target/aarch64-unknown-linux-gnu/release/bitwarden_rs .
# Configures the startup!
CMD ["./bitwarden_rs"]

View File

@@ -0,0 +1,98 @@
# Using multistage build:
# https://docs.docker.com/develop/develop-images/multistage-build/
# https://whitfin.io/speeding-up-rust-docker-builds/
####################### VAULT BUILD IMAGE #######################
FROM alpine:3.10 as vault
ENV VAULT_VERSION "v2.11.0"
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
RUN apk add --no-cache --upgrade \
curl \
tar
RUN mkdir /web-vault
WORKDIR /web-vault
SHELL ["/bin/ash", "-eo", "pipefail", "-c"]
RUN curl -L $URL | tar xz
RUN ls
########################## BUILD IMAGE ##########################
# We need to use the Rust build image, because
# we need the Rust compiler and Cargo tooling
FROM rust:1.36 as build
# set mysql backend
ARG DB=mysql
# Using bundled SQLite, no need to install it
# RUN apt-get update && apt-get install -y\
# --no-install-recommends \
# sqlite3\
# && rm -rf /var/lib/apt/lists/*
# Install MySQL package
RUN apt-get update && apt-get install -y \
--no-install-recommends \
libmariadb-dev \
&& rm -rf /var/lib/apt/lists/*
# Creates a dummy project used to grab dependencies
RUN USER=root cargo new --bin app
WORKDIR /app
# Copies over *only* your manifests and build files
COPY ./Cargo.* ./
COPY ./rust-toolchain ./rust-toolchain
COPY ./build.rs ./build.rs
# Builds your dependencies and removes the
# dummy project, except the target folder
# This folder contains the compiled dependencies
RUN cargo build --features ${DB} --release
RUN find . -not -path "./target*" -delete
# Copies the complete project
# To avoid copying unneeded files, use .dockerignore
COPY . .
# Make sure that we actually build the project
RUN touch src/main.rs
# Builds again, this time it'll just be
# your actual source files being built
RUN cargo build --features ${DB} --release
######################## RUNTIME IMAGE ########################
# Create a new stage with a minimal image
# because we already have a binary built
FROM debian:stretch-slim
ENV ROCKET_ENV "staging"
ENV ROCKET_PORT=80
ENV ROCKET_WORKERS=10
# Install needed libraries
RUN apt-get update && apt-get install -y \
--no-install-recommends \
openssl \
ca-certificates \
libmariadbclient-dev \
&& rm -rf /var/lib/apt/lists/*
RUN mkdir /data
VOLUME /data
EXPOSE 80
EXPOSE 3012
# Copies the files from the context (Rocket.toml file and web-vault)
# and the binary from the "build" stage to the current stage
COPY Rocket.toml .
COPY --from=vault /web-vault ./web-vault
COPY --from=build app/target/release/bitwarden_rs .
# Configures the startup!
CMD ["./bitwarden_rs"]

View File

@@ -2,28 +2,39 @@
# https://docs.docker.com/develop/develop-images/multistage-build/ # https://docs.docker.com/develop/develop-images/multistage-build/
# https://whitfin.io/speeding-up-rust-docker-builds/ # https://whitfin.io/speeding-up-rust-docker-builds/
####################### VAULT BUILD IMAGE ####################### ####################### VAULT BUILD IMAGE #######################
FROM alpine as vault FROM alpine:3.10 as vault
ENV VAULT_VERSION "v2.8.0d" ENV VAULT_VERSION "v2.11.0"
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz" ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
RUN apk add --update-cache --upgrade \ RUN apk add --no-cache --upgrade \
curl \ curl \
tar tar
RUN mkdir /web-vault RUN mkdir /web-vault
WORKDIR /web-vault WORKDIR /web-vault
SHELL ["/bin/ash", "-eo", "pipefail", "-c"]
RUN curl -L $URL | tar xz RUN curl -L $URL | tar xz
RUN ls RUN ls
########################## BUILD IMAGE ########################## ########################## BUILD IMAGE ##########################
# Musl build image for statically compiled binary # Musl build image for statically compiled binary
FROM clux/muslrust:nightly-2018-12-01 as build FROM clux/muslrust:nightly-2019-07-08 as build
# set mysql backend
ARG DB=mysql
ENV USER "root" ENV USER "root"
# Install needed libraries
RUN apt-get update && apt-get install -y \
--no-install-recommends \
libmysqlclient-dev \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app WORKDIR /app
# Copies the complete project # Copies the complete project
@@ -32,13 +43,16 @@ COPY . .
RUN rustup target add x86_64-unknown-linux-musl RUN rustup target add x86_64-unknown-linux-musl
# Make sure that we actually build the project
RUN touch src/main.rs
# Build # Build
RUN cargo build --release RUN cargo build --features ${DB} --release
######################## RUNTIME IMAGE ######################## ######################## RUNTIME IMAGE ########################
# Create a new stage with a minimal image # Create a new stage with a minimal image
# because we already have a binary built # because we already have a binary built
FROM alpine:3.8 FROM alpine:3.10
ENV ROCKET_ENV "staging" ENV ROCKET_ENV "staging"
ENV ROCKET_PORT=80 ENV ROCKET_PORT=80
@@ -46,10 +60,10 @@ ENV ROCKET_WORKERS=10
ENV SSL_CERT_DIR=/etc/ssl/certs ENV SSL_CERT_DIR=/etc/ssl/certs
# Install needed libraries # Install needed libraries
RUN apk add \ RUN apk add --no-cache \
openssl \ openssl \
ca-certificates \ mariadb-connector-c \
&& rm /var/cache/apk/* ca-certificates
RUN mkdir /data RUN mkdir /data
VOLUME /data VOLUME /data
@@ -63,4 +77,4 @@ COPY --from=vault /web-vault ./web-vault
COPY --from=build /app/target/x86_64-unknown-linux-musl/release/bitwarden_rs . COPY --from=build /app/target/x86_64-unknown-linux-musl/release/bitwarden_rs .
# Configures the startup! # Configures the startup!
CMD ./bitwarden_rs CMD ["./bitwarden_rs"]

View File

@@ -0,0 +1,98 @@
# Using multistage build:
# https://docs.docker.com/develop/develop-images/multistage-build/
# https://whitfin.io/speeding-up-rust-docker-builds/
####################### VAULT BUILD IMAGE #######################
FROM alpine:3.10 as vault
ENV VAULT_VERSION "v2.11.0"
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
RUN apk add --no-cache --upgrade \
curl \
tar
RUN mkdir /web-vault
WORKDIR /web-vault
SHELL ["/bin/ash", "-eo", "pipefail", "-c"]
RUN curl -L $URL | tar xz
RUN ls
########################## BUILD IMAGE ##########################
# We need to use the Rust build image, because
# we need the Rust compiler and Cargo tooling
FROM rust:1.36 as build
# set sqlite as default for DB ARG for backward comaptibility
ARG DB=sqlite
# Using bundled SQLite, no need to install it
# RUN apt-get update && apt-get install -y\
# --no-install-recommends \
# sqlite3 \
# && rm -rf /var/lib/apt/lists/*
# Install MySQL package
RUN apt-get update && apt-get install -y \
--no-install-recommends \
libmariadb-dev \
&& rm -rf /var/lib/apt/lists/*
# Creates a dummy project used to grab dependencies
RUN USER=root cargo new --bin app
WORKDIR /app
# Copies over *only* your manifests and build files
COPY ./Cargo.* ./
COPY ./rust-toolchain ./rust-toolchain
COPY ./build.rs ./build.rs
# Builds your dependencies and removes the
# dummy project, except the target folder
# This folder contains the compiled dependencies
RUN cargo build --features ${DB} --release
RUN find . -not -path "./target*" -delete
# Copies the complete project
# To avoid copying unneeded files, use .dockerignore
COPY . .
# Make sure that we actually build the project
RUN touch src/main.rs
# Builds again, this time it'll just be
# your actual source files being built
RUN cargo build --features ${DB} --release
######################## RUNTIME IMAGE ########################
# Create a new stage with a minimal image
# because we already have a binary built
FROM debian:stretch-slim
ENV ROCKET_ENV "staging"
ENV ROCKET_PORT=80
ENV ROCKET_WORKERS=10
# Install needed libraries
RUN apt-get update && apt-get install -y \
--no-install-recommends \
openssl \
ca-certificates \
libmariadbclient-dev \
&& rm -rf /var/lib/apt/lists/*
RUN mkdir /data
VOLUME /data
EXPOSE 80
EXPOSE 3012
# Copies the files from the context (Rocket.toml file and web-vault)
# and the binary from the "build" stage to the current stage
COPY Rocket.toml .
COPY --from=vault /web-vault ./web-vault
COPY --from=build app/target/release/bitwarden_rs .
# Configures the startup!
CMD ["./bitwarden_rs"]

View File

@@ -0,0 +1,80 @@
# Using multistage build:
# https://docs.docker.com/develop/develop-images/multistage-build/
# https://whitfin.io/speeding-up-rust-docker-builds/
####################### VAULT BUILD IMAGE #######################
FROM alpine:3.10 as vault
ENV VAULT_VERSION "v2.11.0"
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
RUN apk add --no-cache --upgrade \
curl \
tar
RUN mkdir /web-vault
WORKDIR /web-vault
SHELL ["/bin/ash", "-eo", "pipefail", "-c"]
RUN curl -L $URL | tar xz
RUN ls
########################## BUILD IMAGE ##########################
# Musl build image for statically compiled binary
FROM clux/muslrust:nightly-2019-07-08 as build
# set sqlite as default for DB ARG for backward comaptibility
ARG DB=sqlite
ENV USER "root"
# Install needed libraries
RUN apt-get update && apt-get install -y \
--no-install-recommends \
libmysqlclient-dev \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Copies the complete project
# To avoid copying unneeded files, use .dockerignore
COPY . .
RUN rustup target add x86_64-unknown-linux-musl
# Make sure that we actually build the project
RUN touch src/main.rs
# Build
RUN cargo build --features ${DB} --release
######################## RUNTIME IMAGE ########################
# Create a new stage with a minimal image
# because we already have a binary built
FROM alpine:3.10
ENV ROCKET_ENV "staging"
ENV ROCKET_PORT=80
ENV ROCKET_WORKERS=10
ENV SSL_CERT_DIR=/etc/ssl/certs
# Install needed libraries
RUN apk add --no-cache \
openssl \
mariadb-connector-c \
ca-certificates
RUN mkdir /data
VOLUME /data
EXPOSE 80
EXPOSE 3012
# Copies the files from the context (Rocket.toml file and web-vault)
# and the binary from the "build" stage to the current stage
COPY Rocket.toml .
COPY --from=vault /web-vault ./web-vault
COPY --from=build /app/target/x86_64-unknown-linux-musl/release/bitwarden_rs .
# Configures the startup!
CMD ["./bitwarden_rs"]

View File

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

View File

@@ -0,0 +1,101 @@
# Using multistage build:
# https://docs.docker.com/develop/develop-images/multistage-build/
# https://whitfin.io/speeding-up-rust-docker-builds/
####################### VAULT BUILD IMAGE #######################
FROM alpine:3.10 as vault
ENV VAULT_VERSION "v2.11.0"
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
RUN apk add --no-cache --upgrade \
curl \
tar
RUN mkdir /web-vault
WORKDIR /web-vault
SHELL ["/bin/ash", "-eo", "pipefail", "-c"]
RUN curl -L $URL | tar xz
RUN ls
########################## BUILD IMAGE ##########################
# We need to use the Rust build image, because
# we need the Rust compiler and Cargo tooling
FROM rust:1.36 as build
# set sqlite as default for DB ARG for backward comaptibility
ARG DB=sqlite
RUN apt-get update \
&& apt-get install -y \
--no-install-recommends \
gcc-arm-linux-gnueabi \
&& mkdir -p ~/.cargo \
&& echo '[target.arm-unknown-linux-gnueabi]' >> ~/.cargo/config \
&& echo 'linker = "arm-linux-gnueabi-gcc"' >> ~/.cargo/config
ENV CARGO_HOME "/root/.cargo"
ENV USER "root"
WORKDIR /app
# Prepare openssl armel libs
RUN sed 's/^deb/deb-src/' /etc/apt/sources.list > \
/etc/apt/sources.list.d/deb-src.list \
&& dpkg --add-architecture armel \
&& apt-get update \
&& apt-get install -y \
--no-install-recommends \
libssl-dev:armel \
libc6-dev:armel \
libmariadb-dev:armel
ENV CC_arm_unknown_linux_gnueabi="/usr/bin/arm-linux-gnueabi-gcc"
ENV CROSS_COMPILE="1"
ENV OPENSSL_INCLUDE_DIR="/usr/include/arm-linux-gnueabi"
ENV OPENSSL_LIB_DIR="/usr/lib/arm-linux-gnueabi"
# Copies the complete project
# To avoid copying unneeded files, use .dockerignore
COPY . .
# Build
RUN rustup target add arm-unknown-linux-gnueabi
RUN cargo build --features ${DB} --release --target=arm-unknown-linux-gnueabi -v
######################## RUNTIME IMAGE ########################
# Create a new stage with a minimal image
# because we already have a binary built
FROM balenalib/rpi-debian:stretch
ENV ROCKET_ENV "staging"
ENV ROCKET_PORT=80
ENV ROCKET_WORKERS=10
RUN [ "cross-build-start" ]
# Install needed libraries
RUN apt-get update && apt-get install -y \
--no-install-recommends \
openssl \
ca-certificates \
libmariadbclient-dev \
&& rm -rf /var/lib/apt/lists/*
RUN mkdir /data
RUN [ "cross-build-end" ]
VOLUME /data
EXPOSE 80
# Copies the files from the context (Rocket.toml file and web-vault)
# and the binary from the "build" stage to the current stage
COPY Rocket.toml .
COPY --from=vault /web-vault ./web-vault
COPY --from=build /app/target/arm-unknown-linux-gnueabi/release/bitwarden_rs .
# Configures the startup!
CMD ["./bitwarden_rs"]

View File

@@ -2,29 +2,35 @@
# https://docs.docker.com/develop/develop-images/multistage-build/ # https://docs.docker.com/develop/develop-images/multistage-build/
# https://whitfin.io/speeding-up-rust-docker-builds/ # https://whitfin.io/speeding-up-rust-docker-builds/
####################### VAULT BUILD IMAGE ####################### ####################### VAULT BUILD IMAGE #######################
FROM alpine as vault FROM alpine:3.10 as vault
ENV VAULT_VERSION "v2.8.0d" ENV VAULT_VERSION "v2.11.0"
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz" ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
RUN apk add --update-cache --upgrade \ RUN apk add --no-cache --upgrade \
curl \ curl \
tar tar
RUN mkdir /web-vault RUN mkdir /web-vault
WORKDIR /web-vault WORKDIR /web-vault
SHELL ["/bin/ash", "-eo", "pipefail", "-c"]
RUN curl -L $URL | tar xz RUN curl -L $URL | tar xz
RUN ls RUN ls
########################## BUILD IMAGE ########################## ########################## BUILD IMAGE ##########################
# We need to use the Rust build image, because # We need to use the Rust build image, because
# we need the Rust compiler and Cargo tooling # we need the Rust compiler and Cargo tooling
FROM rust as build FROM rust:1.36 as build
# set mysql backend
ARG DB=mysql
RUN apt-get update \ RUN apt-get update \
&& apt-get install -y \ && apt-get install -y \
--no-install-recommends \
gcc-arm-linux-gnueabihf \ gcc-arm-linux-gnueabihf \
&& mkdir -p ~/.cargo \ && mkdir -p ~/.cargo \
&& echo '[target.armv7-unknown-linux-gnueabihf]' >> ~/.cargo/config \ && echo '[target.armv7-unknown-linux-gnueabihf]' >> ~/.cargo/config \
@@ -41,8 +47,11 @@ RUN sed 's/^deb/deb-src/' /etc/apt/sources.list > \
&& dpkg --add-architecture armhf \ && dpkg --add-architecture armhf \
&& apt-get update \ && apt-get update \
&& apt-get install -y \ && apt-get install -y \
--no-install-recommends \
libssl-dev:armhf \ libssl-dev:armhf \
libc6-dev:armhf libc6-dev:armhf \
libmariadb-dev:armhf
ENV CC_armv7_unknown_linux_gnueabihf="/usr/bin/arm-linux-gnueabihf-gcc" ENV CC_armv7_unknown_linux_gnueabihf="/usr/bin/arm-linux-gnueabihf-gcc"
ENV CROSS_COMPILE="1" ENV CROSS_COMPILE="1"
@@ -55,7 +64,7 @@ COPY . .
# Build # Build
RUN rustup target add armv7-unknown-linux-gnueabihf RUN rustup target add armv7-unknown-linux-gnueabihf
RUN cargo build --release --target=armv7-unknown-linux-gnueabihf -v RUN cargo build --features ${DB} --release --target=armv7-unknown-linux-gnueabihf -v
######################## RUNTIME IMAGE ######################## ######################## RUNTIME IMAGE ########################
# Create a new stage with a minimal image # Create a new stage with a minimal image
@@ -70,9 +79,10 @@ RUN [ "cross-build-start" ]
# Install needed libraries # Install needed libraries
RUN apt-get update && apt-get install -y \ RUN apt-get update && apt-get install -y \
--no-install-recommends \
openssl \ openssl \
ca-certificates \ ca-certificates \
--no-install-recommends\ libmariadbclient-dev \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
RUN mkdir /data RUN mkdir /data
@@ -89,4 +99,4 @@ COPY --from=vault /web-vault ./web-vault
COPY --from=build /app/target/armv7-unknown-linux-gnueabihf/release/bitwarden_rs . COPY --from=build /app/target/armv7-unknown-linux-gnueabihf/release/bitwarden_rs .
# Configures the startup! # Configures the startup!
CMD ./bitwarden_rs CMD ["./bitwarden_rs"]

View File

@@ -0,0 +1,101 @@
# Using multistage build:
# https://docs.docker.com/develop/develop-images/multistage-build/
# https://whitfin.io/speeding-up-rust-docker-builds/
####################### VAULT BUILD IMAGE #######################
FROM alpine:3.10 as vault
ENV VAULT_VERSION "v2.11.0"
ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
RUN apk add --no-cache --upgrade \
curl \
tar
RUN mkdir /web-vault
WORKDIR /web-vault
SHELL ["/bin/ash", "-eo", "pipefail", "-c"]
RUN curl -L $URL | tar xz
RUN ls
########################## BUILD IMAGE ##########################
# We need to use the Rust build image, because
# we need the Rust compiler and Cargo tooling
FROM rust:1.36 as build
# set sqlite as default for DB ARG for backward comaptibility
ARG DB=sqlite
RUN apt-get update \
&& apt-get install -y \
--no-install-recommends \
gcc-arm-linux-gnueabihf \
&& mkdir -p ~/.cargo \
&& echo '[target.armv7-unknown-linux-gnueabihf]' >> ~/.cargo/config \
&& echo 'linker = "arm-linux-gnueabihf-gcc"' >> ~/.cargo/config
ENV CARGO_HOME "/root/.cargo"
ENV USER "root"
WORKDIR /app
# Prepare openssl armhf libs
RUN sed 's/^deb/deb-src/' /etc/apt/sources.list > \
/etc/apt/sources.list.d/deb-src.list \
&& dpkg --add-architecture armhf \
&& apt-get update \
&& apt-get install -y \
--no-install-recommends \
libssl-dev:armhf \
libc6-dev:armhf \
libmariadb-dev:armhf
ENV CC_armv7_unknown_linux_gnueabihf="/usr/bin/arm-linux-gnueabihf-gcc"
ENV CROSS_COMPILE="1"
ENV OPENSSL_INCLUDE_DIR="/usr/include/arm-linux-gnueabihf"
ENV OPENSSL_LIB_DIR="/usr/lib/arm-linux-gnueabihf"
# Copies the complete project
# To avoid copying unneeded files, use .dockerignore
COPY . .
# Build
RUN rustup target add armv7-unknown-linux-gnueabihf
RUN cargo build --features ${DB} --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 balenalib/armv7hf-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 \
--no-install-recommends \
openssl \
ca-certificates \
libmariadbclient-dev \
&& rm -rf /var/lib/apt/lists/*
RUN mkdir /data
RUN [ "cross-build-end" ]
VOLUME /data
EXPOSE 80
# Copies the files from the context (Rocket.toml file and web-vault)
# and the binary from the "build" stage to the current stage
COPY Rocket.toml .
COPY --from=vault /web-vault ./web-vault
COPY --from=build /app/target/armv7-unknown-linux-gnueabihf/release/bitwarden_rs .
# Configures the startup!
CMD ["./bitwarden_rs"]

View File

@@ -0,0 +1,62 @@
CREATE TABLE users (
uuid CHAR(36) NOT NULL PRIMARY KEY,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE,
name TEXT NOT NULL,
password_hash BLOB NOT NULL,
salt BLOB NOT NULL,
password_iterations INTEGER NOT NULL,
password_hint TEXT,
`key` TEXT NOT NULL,
private_key TEXT,
public_key TEXT,
totp_secret TEXT,
totp_recover TEXT,
security_stamp TEXT NOT NULL,
equivalent_domains TEXT NOT NULL,
excluded_globals TEXT NOT NULL
);
CREATE TABLE devices (
uuid CHAR(36) NOT NULL PRIMARY KEY,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
user_uuid CHAR(36) NOT NULL REFERENCES users (uuid),
name TEXT NOT NULL,
type INTEGER NOT NULL,
push_token TEXT,
refresh_token TEXT NOT NULL
);
CREATE TABLE ciphers (
uuid CHAR(36) NOT NULL PRIMARY KEY,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
user_uuid CHAR(36) NOT NULL REFERENCES users (uuid),
folder_uuid CHAR(36) REFERENCES folders (uuid),
organization_uuid CHAR(36),
type INTEGER NOT NULL,
name TEXT NOT NULL,
notes TEXT,
fields TEXT,
data TEXT NOT NULL,
favorite BOOLEAN NOT NULL
);
CREATE TABLE attachments (
id CHAR(36) NOT NULL PRIMARY KEY,
cipher_uuid CHAR(36) NOT NULL REFERENCES ciphers (uuid),
file_name TEXT NOT NULL,
file_size INTEGER NOT NULL
);
CREATE TABLE folders (
uuid CHAR(36) NOT NULL PRIMARY KEY,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
user_uuid CHAR(36) NOT NULL REFERENCES users (uuid),
name TEXT NOT NULL
);

View File

@@ -0,0 +1,30 @@
CREATE TABLE collections (
uuid VARCHAR(40) NOT NULL PRIMARY KEY,
org_uuid VARCHAR(40) NOT NULL REFERENCES organizations (uuid),
name TEXT NOT NULL
);
CREATE TABLE organizations (
uuid VARCHAR(40) NOT NULL PRIMARY KEY,
name TEXT NOT NULL,
billing_email TEXT NOT NULL
);
CREATE TABLE users_collections (
user_uuid CHAR(36) NOT NULL REFERENCES users (uuid),
collection_uuid CHAR(36) NOT NULL REFERENCES collections (uuid),
PRIMARY KEY (user_uuid, collection_uuid)
);
CREATE TABLE users_organizations (
uuid CHAR(36) NOT NULL PRIMARY KEY,
user_uuid CHAR(36) NOT NULL REFERENCES users (uuid),
org_uuid CHAR(36) NOT NULL REFERENCES organizations (uuid),
access_all BOOLEAN NOT NULL,
`key` TEXT NOT NULL,
status INTEGER NOT NULL,
type INTEGER NOT NULL,
UNIQUE (user_uuid, org_uuid)
);

View File

@@ -0,0 +1,34 @@
ALTER TABLE ciphers RENAME TO oldCiphers;
CREATE TABLE ciphers (
uuid CHAR(36) NOT NULL PRIMARY KEY,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
user_uuid CHAR(36) REFERENCES users (uuid), -- Make this optional
organization_uuid CHAR(36) REFERENCES organizations (uuid), -- Add reference to orgs table
-- Remove folder_uuid
type INTEGER NOT NULL,
name TEXT NOT NULL,
notes TEXT,
fields TEXT,
data TEXT NOT NULL,
favorite BOOLEAN NOT NULL
);
CREATE TABLE folders_ciphers (
cipher_uuid CHAR(36) NOT NULL REFERENCES ciphers (uuid),
folder_uuid CHAR(36) NOT NULL REFERENCES folders (uuid),
PRIMARY KEY (cipher_uuid, folder_uuid)
);
INSERT INTO ciphers (uuid, created_at, updated_at, user_uuid, organization_uuid, type, name, notes, fields, data, favorite)
SELECT uuid, created_at, updated_at, user_uuid, organization_uuid, type, name, notes, fields, data, favorite FROM oldCiphers;
INSERT INTO folders_ciphers (cipher_uuid, folder_uuid)
SELECT uuid, folder_uuid FROM oldCiphers WHERE folder_uuid IS NOT NULL;
DROP TABLE oldCiphers;
ALTER TABLE users_collections ADD COLUMN read_only BOOLEAN NOT NULL DEFAULT 0; -- False

View File

@@ -0,0 +1,5 @@
CREATE TABLE ciphers_collections (
cipher_uuid CHAR(36) NOT NULL REFERENCES ciphers (uuid),
collection_uuid CHAR(36) NOT NULL REFERENCES collections (uuid),
PRIMARY KEY (cipher_uuid, collection_uuid)
);

View File

@@ -0,0 +1,14 @@
ALTER TABLE attachments RENAME TO oldAttachments;
CREATE TABLE attachments (
id CHAR(36) NOT NULL PRIMARY KEY,
cipher_uuid CHAR(36) NOT NULL REFERENCES ciphers (uuid),
file_name TEXT NOT NULL,
file_size INTEGER NOT NULL
);
INSERT INTO attachments (id, cipher_uuid, file_name, file_size)
SELECT id, cipher_uuid, file_name, file_size FROM oldAttachments;
DROP TABLE oldAttachments;

View File

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

View File

@@ -0,0 +1,3 @@
CREATE TABLE invitations (
email VARCHAR(255) NOT NULL PRIMARY KEY
);

View File

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

View File

@@ -0,0 +1,7 @@
ALTER TABLE attachments CHANGE COLUMN akey `key` TEXT;
ALTER TABLE ciphers CHANGE COLUMN atype type INTEGER NOT NULL;
ALTER TABLE devices CHANGE COLUMN atype type INTEGER NOT NULL;
ALTER TABLE twofactor CHANGE COLUMN atype type INTEGER NOT NULL;
ALTER TABLE users CHANGE COLUMN akey `key` TEXT;
ALTER TABLE users_organizations CHANGE COLUMN akey `key` TEXT;
ALTER TABLE users_organizations CHANGE COLUMN atype type INTEGER NOT NULL;

View File

@@ -0,0 +1,7 @@
ALTER TABLE attachments CHANGE COLUMN `key` akey TEXT;
ALTER TABLE ciphers CHANGE COLUMN type atype INTEGER NOT NULL;
ALTER TABLE devices CHANGE COLUMN type atype INTEGER NOT NULL;
ALTER TABLE twofactor CHANGE COLUMN type atype INTEGER NOT NULL;
ALTER TABLE users CHANGE COLUMN `key` akey TEXT;
ALTER TABLE users_organizations CHANGE COLUMN `key` akey TEXT;
ALTER TABLE users_organizations CHANGE COLUMN type atype INTEGER NOT NULL;

View File

@@ -0,0 +1,9 @@
DROP TABLE users;
DROP TABLE devices;
DROP TABLE ciphers;
DROP TABLE attachments;
DROP TABLE folders;

View File

@@ -0,0 +1,8 @@
DROP TABLE collections;
DROP TABLE organizations;
DROP TABLE users_collections;
DROP TABLE users_organizations;

View File

@@ -0,0 +1 @@
DROP TABLE ciphers_collections;

View File

@@ -0,0 +1 @@
-- This file should undo anything in `up.sql`

View File

@@ -0,0 +1,3 @@
ALTER TABLE devices
ADD COLUMN
twofactor_remember TEXT;

View File

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

View File

@@ -0,0 +1,3 @@
ALTER TABLE ciphers
ADD COLUMN
password_history TEXT;

View File

@@ -0,0 +1 @@
DROP TABLE invitations;

View File

@@ -0,0 +1,7 @@
ALTER TABLE users
ADD COLUMN
client_kdf_type INTEGER NOT NULL DEFAULT 0; -- PBKDF2
ALTER TABLE users
ADD COLUMN
client_kdf_iter INTEGER NOT NULL DEFAULT 5000;

View File

@@ -0,0 +1,7 @@
ALTER TABLE attachments RENAME COLUMN akey TO key;
ALTER TABLE ciphers RENAME COLUMN atype TO type;
ALTER TABLE devices RENAME COLUMN atype TO type;
ALTER TABLE twofactor RENAME COLUMN atype TO type;
ALTER TABLE users RENAME COLUMN akey TO key;
ALTER TABLE users_organizations RENAME COLUMN akey TO key;
ALTER TABLE users_organizations RENAME COLUMN atype TO type;

View File

@@ -0,0 +1,7 @@
ALTER TABLE attachments RENAME COLUMN key TO akey;
ALTER TABLE ciphers RENAME COLUMN type TO atype;
ALTER TABLE devices RENAME COLUMN type TO atype;
ALTER TABLE twofactor RENAME COLUMN type TO atype;
ALTER TABLE users RENAME COLUMN key TO akey;
ALTER TABLE users_organizations RENAME COLUMN key TO akey;
ALTER TABLE users_organizations RENAME COLUMN type TO atype;

View File

@@ -1 +1 @@
nightly-2019-01-26 nightly-2019-08-18

View File

@@ -1,4 +1,5 @@
use serde_json::Value; use serde_json::Value;
use std::process::Command;
use rocket::http::{Cookie, Cookies, SameSite}; use rocket::http::{Cookie, Cookies, SameSite};
use rocket::request::{self, FlashMessage, Form, FromRequest, Request}; use rocket::request::{self, FlashMessage, Form, FromRequest, Request};
@@ -6,41 +7,55 @@ use rocket::response::{content::Html, Flash, Redirect};
use rocket::{Outcome, Route}; use rocket::{Outcome, Route};
use rocket_contrib::json::Json; use rocket_contrib::json::Json;
use crate::api::{ApiResult, EmptyResult}; use crate::api::{ApiResult, EmptyResult, JsonResult};
use crate::auth::{decode_admin, encode_jwt, generate_admin_claims, ClientIp}; use crate::auth::{decode_admin, encode_jwt, generate_admin_claims, ClientIp};
use crate::config::ConfigBuilder; use crate::config::ConfigBuilder;
use crate::db::{models::*, DbConn}; use crate::db::{backup_database, models::*, DbConn};
use crate::error::Error; use crate::error::Error;
use crate::mail; use crate::mail;
use crate::CONFIG; use crate::CONFIG;
pub fn routes() -> Vec<Route> { pub fn routes() -> Vec<Route> {
if CONFIG.admin_token().is_none() { if CONFIG.admin_token().is_none() && !CONFIG.disable_admin_token() {
return Vec::new(); return routes![admin_disabled];
} }
routes![ routes![
admin_login, admin_login,
get_users,
post_admin_login, post_admin_login,
admin_page, admin_page,
invite_user, invite_user,
delete_user, delete_user,
deauth_user, deauth_user,
remove_2fa,
update_revision_users,
post_config, post_config,
delete_config, delete_config,
backup_db,
] ]
} }
lazy_static! {
static ref CAN_BACKUP: bool = cfg!(feature = "sqlite") && Command::new("sqlite").arg("-version").status().is_ok();
}
#[get("/")]
fn admin_disabled() -> &'static str {
"The admin panel is disabled, please configure the 'ADMIN_TOKEN' variable to enable it"
}
const COOKIE_NAME: &str = "BWRS_ADMIN"; const COOKIE_NAME: &str = "BWRS_ADMIN";
const ADMIN_PATH: &str = "/admin"; const ADMIN_PATH: &str = "/admin";
const BASE_TEMPLATE: &str = "admin/base"; const BASE_TEMPLATE: &str = "admin/base";
const VERSION: Option<&str> = option_env!("GIT_VERSION");
#[get("/", rank = 2)] #[get("/", rank = 2)]
fn admin_login(flash: Option<FlashMessage>) -> ApiResult<Html<String>> { fn admin_login(flash: Option<FlashMessage>) -> ApiResult<Html<String>> {
// If there is an error, show it // If there is an error, show it
let msg = flash.map(|msg| format!("{}: {}", msg.name(), msg.msg())); let msg = flash.map(|msg| format!("{}: {}", msg.name(), msg.msg()));
let json = json!({"page_content": "admin/login", "error": msg}); let json = json!({"page_content": "admin/login", "version": VERSION, "error": msg});
// Return the page // Return the page
let text = CONFIG.render_template(BASE_TEMPLATE, &json)?; let text = CONFIG.render_template(BASE_TEMPLATE, &json)?;
@@ -83,23 +98,27 @@ fn post_admin_login(data: Form<LoginForm>, mut cookies: Cookies, ip: ClientIp) -
fn _validate_token(token: &str) -> bool { fn _validate_token(token: &str) -> bool {
match CONFIG.admin_token().as_ref() { match CONFIG.admin_token().as_ref() {
None => false, None => false,
Some(t) => t == token, Some(t) => crate::crypto::ct_eq(t.trim(), token.trim()),
} }
} }
#[derive(Serialize)] #[derive(Serialize)]
struct AdminTemplateData { struct AdminTemplateData {
users: Vec<Value>,
page_content: String, page_content: String,
version: Option<&'static str>,
users: Vec<Value>,
config: Value, config: Value,
can_backup: bool,
} }
impl AdminTemplateData { impl AdminTemplateData {
fn new(users: Vec<Value>) -> Self { fn new(users: Vec<Value>) -> Self {
Self { Self {
users,
page_content: String::from("admin/page"), page_content: String::from("admin/page"),
version: VERSION,
users,
config: CONFIG.prepare_json(), config: CONFIG.prepare_json(),
can_backup: *CAN_BACKUP,
} }
} }
@@ -135,17 +154,26 @@ fn invite_user(data: Json<InviteData>, _token: AdminToken, conn: DbConn) -> Empt
err!("Invitations are not allowed") err!("Invitations are not allowed")
} }
if CONFIG.mail_enabled() {
let mut user = User::new(email); let mut user = User::new(email);
user.save(&conn)?; user.save(&conn)?;
if CONFIG.mail_enabled() {
let org_name = "bitwarden_rs"; let org_name = "bitwarden_rs";
mail::send_invite(&user.email, &user.uuid, None, None, &org_name, None) mail::send_invite(&user.email, &user.uuid, None, None, &org_name, None)
} else { } else {
let mut invitation = Invitation::new(data.email); let invitation = Invitation::new(data.email);
invitation.save(&conn) invitation.save(&conn)
} }
} }
#[get("/users")]
fn get_users(_token: AdminToken, conn: DbConn) -> JsonResult {
let users = User::get_all(&conn);
let users_json: Vec<Value> = users.iter().map(|u| u.to_json(&conn)).collect();
Ok(Json(Value::Array(users_json)))
}
#[post("/users/<uuid>/delete")] #[post("/users/<uuid>/delete")]
fn delete_user(uuid: String, _token: AdminToken, conn: DbConn) -> EmptyResult { fn delete_user(uuid: String, _token: AdminToken, conn: DbConn) -> EmptyResult {
let user = match User::find_by_uuid(&uuid, &conn) { let user = match User::find_by_uuid(&uuid, &conn) {
@@ -163,11 +191,29 @@ fn deauth_user(uuid: String, _token: AdminToken, conn: DbConn) -> EmptyResult {
None => err!("User doesn't exist"), None => err!("User doesn't exist"),
}; };
Device::delete_all_by_user(&user.uuid, &conn)?;
user.reset_security_stamp(); user.reset_security_stamp();
user.save(&conn) user.save(&conn)
} }
#[post("/users/<uuid>/remove-2fa")]
fn remove_2fa(uuid: String, _token: AdminToken, conn: DbConn) -> EmptyResult {
let mut user = match User::find_by_uuid(&uuid, &conn) {
Some(user) => user,
None => err!("User doesn't exist"),
};
TwoFactor::delete_all_by_user(&user.uuid, &conn)?;
user.totp_recover = None;
user.save(&conn)
}
#[post("/users/update_revision")]
fn update_revision_users(_token: AdminToken, conn: DbConn) -> EmptyResult {
User::update_all_revisions(&conn)
}
#[post("/config", data = "<data>")] #[post("/config", data = "<data>")]
fn post_config(data: Json<ConfigBuilder>, _token: AdminToken) -> EmptyResult { fn post_config(data: Json<ConfigBuilder>, _token: AdminToken) -> EmptyResult {
let data: ConfigBuilder = data.into_inner(); let data: ConfigBuilder = data.into_inner();
@@ -179,12 +225,24 @@ fn delete_config(_token: AdminToken) -> EmptyResult {
CONFIG.delete_user_config() CONFIG.delete_user_config()
} }
#[post("/config/backup_db")]
fn backup_db(_token: AdminToken) -> EmptyResult {
if *CAN_BACKUP {
backup_database()
} else {
err!("Can't back up current DB (either it's not SQLite or the 'sqlite' binary is not present)");
}
}
pub struct AdminToken {} pub struct AdminToken {}
impl<'a, 'r> FromRequest<'a, 'r> for AdminToken { impl<'a, 'r> FromRequest<'a, 'r> for AdminToken {
type Error = &'static str; type Error = &'static str;
fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> { fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
if CONFIG.disable_admin_token() {
Outcome::Success(AdminToken {})
} else {
let mut cookies = request.cookies(); let mut cookies = request.cookies();
let access_token = match cookies.get(COOKIE_NAME) { let access_token = match cookies.get(COOKIE_NAME) {
@@ -207,3 +265,4 @@ impl<'a, 'r> FromRequest<'a, 'r> for AdminToken {
Outcome::Success(AdminToken {}) Outcome::Success(AdminToken {})
} }
} }
}

View File

@@ -106,7 +106,7 @@ fn register(data: JsonUpcase<RegisterData>, conn: DbConn) -> EmptyResult {
} }
user.set_password(&data.MasterPasswordHash); user.set_password(&data.MasterPasswordHash);
user.key = data.Key; user.akey = data.Key;
// Add extra fields if present // Add extra fields if present
if let Some(name) = data.Name { if let Some(name) = data.Name {
@@ -204,7 +204,7 @@ fn post_password(data: JsonUpcase<ChangePassData>, headers: Headers, conn: DbCon
} }
user.set_password(&data.NewMasterPasswordHash); user.set_password(&data.NewMasterPasswordHash);
user.key = data.Key; user.akey = data.Key;
user.save(&conn) user.save(&conn)
} }
@@ -231,7 +231,7 @@ fn post_kdf(data: JsonUpcase<ChangeKdfData>, headers: Headers, conn: DbConn) ->
user.client_kdf_iter = data.KdfIterations; user.client_kdf_iter = data.KdfIterations;
user.client_kdf_type = data.Kdf; user.client_kdf_type = data.Kdf;
user.set_password(&data.NewMasterPasswordHash); user.set_password(&data.NewMasterPasswordHash);
user.key = data.Key; user.akey = data.Key;
user.save(&conn) user.save(&conn)
} }
@@ -306,7 +306,7 @@ fn post_rotatekey(data: JsonUpcase<KeyData>, headers: Headers, conn: DbConn, nt:
// Update user data // Update user data
let mut user = headers.user; let mut user = headers.user;
user.key = data.Key; user.akey = data.Key;
user.private_key = Some(data.PrivateKey); user.private_key = Some(data.PrivateKey);
user.reset_security_stamp(); user.reset_security_stamp();
@@ -322,6 +322,7 @@ fn post_sstamp(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -
err!("Invalid password") err!("Invalid password")
} }
Device::delete_all_by_user(&user.uuid, &conn)?;
user.reset_security_stamp(); user.reset_security_stamp();
user.save(&conn) user.save(&conn)
} }
@@ -376,7 +377,7 @@ fn post_email(data: JsonUpcase<ChangeEmailData>, headers: Headers, conn: DbConn)
user.email = data.NewEmail; user.email = data.NewEmail;
user.set_password(&data.NewMasterPasswordHash); user.set_password(&data.NewMasterPasswordHash);
user.key = data.Key; user.akey = data.Key;
user.save(&conn) user.save(&conn)
} }

View File

@@ -74,10 +74,10 @@ fn sync(data: Form<SyncData>, headers: Headers, conn: DbConn) -> JsonResult {
let user_json = headers.user.to_json(&conn); let user_json = headers.user.to_json(&conn);
let folders = Folder::find_by_user(&headers.user.uuid, &conn); let folders = Folder::find_by_user(&headers.user.uuid, &conn);
let folders_json: Vec<Value> = folders.iter().map(|c| c.to_json()).collect(); let folders_json: Vec<Value> = folders.iter().map(Folder::to_json).collect();
let collections = Collection::find_by_user_uuid(&headers.user.uuid, &conn); let collections = Collection::find_by_user_uuid(&headers.user.uuid, &conn);
let collections_json: Vec<Value> = collections.iter().map(|c| c.to_json()).collect(); let collections_json: Vec<Value> = collections.iter().map(Collection::to_json).collect();
let ciphers = Cipher::find_by_user(&headers.user.uuid, &conn); let ciphers = Cipher::find_by_user(&headers.user.uuid, &conn);
let ciphers_json: Vec<Value> = ciphers let ciphers_json: Vec<Value> = ciphers
@@ -267,7 +267,7 @@ pub fn update_cipher_from_data(
err!("Attachment is not owned by the cipher") err!("Attachment is not owned by the cipher")
} }
saved_att.key = Some(attachment.Key); saved_att.akey = Some(attachment.Key);
saved_att.file_name = attachment.FileName; saved_att.file_name = attachment.FileName;
saved_att.save(&conn)?; saved_att.save(&conn)?;
@@ -608,7 +608,7 @@ fn share_cipher_by_uuid(
None => err!("Invalid collection ID provided"), None => err!("Invalid collection ID provided"),
Some(collection) => { Some(collection) => {
if collection.is_writable_by_user(&headers.user.uuid, &conn) { if collection.is_writable_by_user(&headers.user.uuid, &conn) {
CollectionCipher::save(&cipher.uuid.clone(), &collection.uuid, &conn)?; CollectionCipher::save(&cipher.uuid, &collection.uuid, &conn)?;
shared_to_collection = true; shared_to_collection = true;
} else { } else {
err!("No rights to modify the collection") err!("No rights to modify the collection")
@@ -691,7 +691,7 @@ fn post_attachment(
}; };
let mut attachment = Attachment::new(file_name, cipher.uuid.clone(), name, size); let mut attachment = Attachment::new(file_name, cipher.uuid.clone(), name, size);
attachment.key = attachment_key.clone(); attachment.akey = attachment_key.clone();
attachment.save(&conn).expect("Error saving attachment"); attachment.save(&conn).expect("Error saving attachment");
} }
_ => error!("Invalid multipart name"), _ => error!("Invalid multipart name"),
@@ -854,11 +854,7 @@ fn move_cipher_selected(data: JsonUpcase<MoveCipherData>, headers: Headers, conn
// Move cipher // Move cipher
cipher.move_to_folder(data.FolderId.clone(), &user_uuid, &conn)?; cipher.move_to_folder(data.FolderId.clone(), &user_uuid, &conn)?;
nt.send_cipher_update( nt.send_cipher_update(UpdateType::CipherUpdate, &cipher, &[user_uuid.clone()]);
UpdateType::CipherUpdate,
&cipher,
&[user_uuid.clone()]
);
} }
Ok(()) Ok(())
@@ -874,8 +870,20 @@ fn move_cipher_selected_put(
move_cipher_selected(data, headers, conn, nt) move_cipher_selected(data, headers, conn, nt)
} }
#[post("/ciphers/purge", data = "<data>")] #[derive(FromForm)]
fn delete_all(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult { struct OrganizationId {
#[form(field = "organizationId")]
org_id: String,
}
#[post("/ciphers/purge?<organization..>", data = "<data>")]
fn delete_all(
organization: Option<Form<OrganizationId>>,
data: JsonUpcase<PasswordData>,
headers: Headers,
conn: DbConn,
nt: Notify,
) -> EmptyResult {
let data: PasswordData = data.into_inner().data; let data: PasswordData = data.into_inner().data;
let password_hash = data.MasterPasswordHash; let password_hash = data.MasterPasswordHash;
@@ -885,6 +893,25 @@ fn delete_all(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn, nt
err!("Invalid password") err!("Invalid password")
} }
match organization {
Some(org_data) => {
// Organization ID in query params, purging organization vault
match UserOrganization::find_by_user_and_org(&user.uuid, &org_data.org_id, &conn) {
None => err!("You don't have permission to purge the organization vault"),
Some(user_org) => {
if user_org.atype == UserOrgType::Owner {
Cipher::delete_all_by_organization(&org_data.org_id, &conn)?;
Collection::delete_all_by_organization(&org_data.org_id, &conn)?;
nt.send_user_update(UpdateType::Vault, &user);
Ok(())
} else {
err!("You don't have permission to purge the organization vault");
}
}
}
}
None => {
// No organization ID in query params, purging user vault
// Delete ciphers and their attachments // Delete ciphers and their attachments
for cipher in Cipher::find_owned_by_user(&user.uuid, &conn) { for cipher in Cipher::find_owned_by_user(&user.uuid, &conn) {
cipher.delete(&conn)?; cipher.delete(&conn)?;
@@ -899,6 +926,8 @@ fn delete_all(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn, nt
nt.send_user_update(UpdateType::Vault, &user); nt.send_user_update(UpdateType::Vault, &user);
Ok(()) Ok(())
} }
}
}
fn _delete_cipher_by_uuid(uuid: &str, headers: &Headers, conn: &DbConn, nt: &Notify) -> EmptyResult { fn _delete_cipher_by_uuid(uuid: &str, headers: &Headers, conn: &DbConn, nt: &Notify) -> EmptyResult {
let cipher = match Cipher::find_by_uuid(&uuid, &conn) { let cipher = match Cipher::find_by_uuid(&uuid, &conn) {

View File

@@ -25,7 +25,7 @@ pub fn routes() -> Vec<Route> {
fn get_folders(headers: Headers, conn: DbConn) -> JsonResult { fn get_folders(headers: Headers, conn: DbConn) -> JsonResult {
let folders = Folder::find_by_user(&headers.user.uuid, &conn); let folders = Folder::find_by_user(&headers.user.uuid, &conn);
let folders_json: Vec<Value> = folders.iter().map(|c| c.to_json()).collect(); let folders_json: Vec<Value> = folders.iter().map(Folder::to_json).collect();
Ok(Json(json!({ Ok(Json(json!({
"Data": folders_json, "Data": folders_json,

View File

@@ -33,10 +33,10 @@ use rocket::Route;
use rocket_contrib::json::Json; use rocket_contrib::json::Json;
use serde_json::Value; use serde_json::Value;
use crate::db::DbConn;
use crate::api::{EmptyResult, JsonResult, JsonUpcase}; use crate::api::{EmptyResult, JsonResult, JsonUpcase};
use crate::auth::Headers; use crate::auth::Headers;
use crate::db::DbConn;
use crate::error::Error;
#[put("/devices/identifier/<uuid>/clear-token")] #[put("/devices/identifier/<uuid>/clear-token")]
fn clear_device_token(uuid: String) -> EmptyResult { fn clear_device_token(uuid: String) -> EmptyResult {
@@ -63,7 +63,7 @@ fn put_device_token(uuid: String, data: JsonUpcase<Value>, headers: Headers) ->
Ok(Json(json!({ Ok(Json(json!({
"Id": headers.device.uuid, "Id": headers.device.uuid,
"Name": headers.device.name, "Name": headers.device.name,
"Type": headers.device.type_, "Type": headers.device.atype,
"Identifier": headers.device.uuid, "Identifier": headers.device.uuid,
"CreationDate": crate::util::format_date(&headers.device.created_at), "CreationDate": crate::util::format_date(&headers.device.created_at),
}))) })))
@@ -132,17 +132,33 @@ fn put_eq_domains(data: JsonUpcase<EquivDomainData>, headers: Headers, conn: DbC
#[get("/hibp/breach?<username>")] #[get("/hibp/breach?<username>")]
fn hibp_breach(username: String) -> JsonResult { fn hibp_breach(username: String) -> JsonResult {
let url = format!("https://haveibeenpwned.com/api/v2/breachedaccount/{}", username);
let user_agent = "Bitwarden_RS"; let user_agent = "Bitwarden_RS";
let url = format!(
"https://haveibeenpwned.com/api/v3/breachedaccount/{}?truncateResponse=false&includeUnverified=false",
username
);
use reqwest::{header::USER_AGENT, Client}; use reqwest::{header::USER_AGENT, Client};
let value: Value = Client::new() if let Some(api_key) = crate::CONFIG.hibp_api_key() {
let res = Client::new()
.get(&url) .get(&url)
.header(USER_AGENT, user_agent) .header(USER_AGENT, user_agent)
.send()? .header("hibp-api-key", api_key)
.error_for_status()? .send()?;
.json()?;
Ok(Json(value)) // If we get a 404, return a 404, it means no breached accounts
if res.status() == 404 {
return Err(Error::empty().with_code(404));
}
let value: Value = res.error_for_status()?.json()?;
Ok(Json(value))
} else {
Ok(Json(json!([{
"title": "--- Error! ---",
"description": "HaveIBeenPwned API key not set! Go to https://haveibeenpwned.com/API/Key",
"logopath": "/bwrs_images/error-x.svg"
}])))
}
} }

View File

@@ -76,13 +76,13 @@ struct NewCollectionData {
fn create_organization(headers: Headers, data: JsonUpcase<OrgData>, conn: DbConn) -> JsonResult { fn create_organization(headers: Headers, data: JsonUpcase<OrgData>, conn: DbConn) -> JsonResult {
let data: OrgData = data.into_inner().data; let data: OrgData = data.into_inner().data;
let mut org = Organization::new(data.Name, data.BillingEmail); let org = Organization::new(data.Name, data.BillingEmail);
let mut user_org = UserOrganization::new(headers.user.uuid.clone(), org.uuid.clone()); let mut user_org = UserOrganization::new(headers.user.uuid.clone(), org.uuid.clone());
let mut collection = Collection::new(org.uuid.clone(), data.CollectionName); let collection = Collection::new(org.uuid.clone(), data.CollectionName);
user_org.key = data.Key; user_org.akey = data.Key;
user_org.access_all = true; user_org.access_all = true;
user_org.type_ = UserOrgType::Owner as i32; user_org.atype = UserOrgType::Owner as i32;
user_org.status = UserOrgStatus::Confirmed as i32; user_org.status = UserOrgStatus::Confirmed as i32;
org.save(&conn)?; org.save(&conn)?;
@@ -127,7 +127,7 @@ fn leave_organization(org_id: String, headers: Headers, conn: DbConn) -> EmptyRe
match UserOrganization::find_by_user_and_org(&headers.user.uuid, &org_id, &conn) { match UserOrganization::find_by_user_and_org(&headers.user.uuid, &org_id, &conn) {
None => err!("User not part of organization"), None => err!("User not part of organization"),
Some(user_org) => { Some(user_org) => {
if user_org.type_ == UserOrgType::Owner { if user_org.atype == UserOrgType::Owner {
let num_owners = let num_owners =
UserOrganization::find_by_org_and_type(&org_id, UserOrgType::Owner as i32, &conn).len(); UserOrganization::find_by_org_and_type(&org_id, UserOrgType::Owner as i32, &conn).len();
@@ -221,7 +221,7 @@ fn post_organization_collections(
None => err!("Can't find organization details"), None => err!("Can't find organization details"),
}; };
let mut collection = Collection::new(org.uuid.clone(), data.Name); let collection = Collection::new(org.uuid.clone(), data.Name);
collection.save(&conn)?; collection.save(&conn)?;
Ok(Json(collection.to_json())) Ok(Json(collection.to_json()))
@@ -484,7 +484,7 @@ fn send_invite(org_id: String, data: JsonUpcase<InviteData>, headers: AdminHeade
} }
if !CONFIG.mail_enabled() { if !CONFIG.mail_enabled() {
let mut invitation = Invitation::new(email.clone()); let invitation = Invitation::new(email.clone());
invitation.save(&conn)?; invitation.save(&conn)?;
} }
@@ -505,7 +505,7 @@ fn send_invite(org_id: String, data: JsonUpcase<InviteData>, headers: AdminHeade
let mut new_user = UserOrganization::new(user.uuid.clone(), org_id.clone()); let mut new_user = UserOrganization::new(user.uuid.clone(), org_id.clone());
let access_all = data.AccessAll.unwrap_or(false); let access_all = data.AccessAll.unwrap_or(false);
new_user.access_all = access_all; new_user.access_all = access_all;
new_user.type_ = new_type; new_user.atype = new_type;
new_user.status = user_org_status; new_user.status = user_org_status;
// If no accessAll, add the collections received // If no accessAll, add the collections received
@@ -581,7 +581,7 @@ fn reinvite_user(org_id: String, user_org: String, headers: AdminHeaders, conn:
Some(headers.user.email), Some(headers.user.email),
)?; )?;
} else { } else {
let mut invitation = Invitation::new(user.email.clone()); let invitation = Invitation::new(user.email.clone());
invitation.save(&conn)?; invitation.save(&conn)?;
} }
@@ -657,7 +657,7 @@ fn confirm_invite(
None => err!("The specified user isn't a member of the organization"), None => err!("The specified user isn't a member of the organization"),
}; };
if user_to_confirm.type_ != UserOrgType::User && headers.org_user_type != UserOrgType::Owner { if user_to_confirm.atype != UserOrgType::User && headers.org_user_type != UserOrgType::Owner {
err!("Only Owners can confirm Managers, Admins or Owners") err!("Only Owners can confirm Managers, Admins or Owners")
} }
@@ -666,7 +666,7 @@ fn confirm_invite(
} }
user_to_confirm.status = UserOrgStatus::Confirmed as i32; user_to_confirm.status = UserOrgStatus::Confirmed as i32;
user_to_confirm.key = match data["Key"].as_str() { user_to_confirm.akey = match data["Key"].as_str() {
Some(key) => key.to_string(), Some(key) => key.to_string(),
None => err!("Invalid key provided"), None => err!("Invalid key provided"),
}; };
@@ -735,18 +735,18 @@ fn edit_user(
None => err!("The specified user isn't member of the organization"), None => err!("The specified user isn't member of the organization"),
}; };
if new_type != user_to_edit.type_ if new_type != user_to_edit.atype
&& (user_to_edit.type_ >= UserOrgType::Admin || new_type >= UserOrgType::Admin) && (user_to_edit.atype >= UserOrgType::Admin || new_type >= UserOrgType::Admin)
&& headers.org_user_type != UserOrgType::Owner && headers.org_user_type != UserOrgType::Owner
{ {
err!("Only Owners can grant and remove Admin or Owner privileges") err!("Only Owners can grant and remove Admin or Owner privileges")
} }
if user_to_edit.type_ == UserOrgType::Owner && headers.org_user_type != UserOrgType::Owner { if user_to_edit.atype == UserOrgType::Owner && headers.org_user_type != UserOrgType::Owner {
err!("Only Owners can edit Owner users") err!("Only Owners can edit Owner users")
} }
if user_to_edit.type_ == UserOrgType::Owner && new_type != UserOrgType::Owner { if user_to_edit.atype == UserOrgType::Owner && new_type != UserOrgType::Owner {
// Removing owner permmission, check that there are at least another owner // Removing owner permmission, check that there are at least another owner
let num_owners = UserOrganization::find_by_org_and_type(&org_id, UserOrgType::Owner as i32, &conn).len(); let num_owners = UserOrganization::find_by_org_and_type(&org_id, UserOrgType::Owner as i32, &conn).len();
@@ -756,7 +756,7 @@ fn edit_user(
} }
user_to_edit.access_all = data.AccessAll; user_to_edit.access_all = data.AccessAll;
user_to_edit.type_ = new_type as i32; user_to_edit.atype = new_type as i32;
// Delete all the odd collections // Delete all the odd collections
for c in CollectionUser::find_by_organization_and_user_uuid(&org_id, &user_to_edit.user_uuid, &conn) { for c in CollectionUser::find_by_organization_and_user_uuid(&org_id, &user_to_edit.user_uuid, &conn) {
@@ -785,11 +785,11 @@ fn delete_user(org_id: String, org_user_id: String, headers: AdminHeaders, conn:
None => err!("User to delete isn't member of the organization"), None => err!("User to delete isn't member of the organization"),
}; };
if user_to_delete.type_ != UserOrgType::User && headers.org_user_type != UserOrgType::Owner { if user_to_delete.atype != UserOrgType::User && headers.org_user_type != UserOrgType::Owner {
err!("Only Owners can delete Admins or Owners") err!("Only Owners can delete Admins or Owners")
} }
if user_to_delete.type_ == UserOrgType::Owner { if user_to_delete.atype == UserOrgType::Owner {
// Removing owner, check that there are at least another owner // Removing owner, check that there are at least another owner
let num_owners = UserOrganization::find_by_org_and_type(&org_id, UserOrgType::Owner as i32, &conn).len(); let num_owners = UserOrganization::find_by_org_and_type(&org_id, UserOrgType::Owner as i32, &conn).len();
@@ -842,7 +842,7 @@ fn post_org_import(
None => err!("User is not part of the organization"), None => err!("User is not part of the organization"),
}; };
if org_user.type_ < UserOrgType::Admin { if org_user.atype < UserOrgType::Admin {
err!("Only admins or owners can import into an organization") err!("Only admins or owners can import into an organization")
} }
@@ -851,7 +851,7 @@ fn post_org_import(
.Collections .Collections
.into_iter() .into_iter()
.map(|coll| { .map(|coll| {
let mut collection = Collection::new(org_id.clone(), coll.Name); let collection = Collection::new(org_id.clone(), coll.Name);
if collection.save(&conn).is_err() { if collection.save(&conn).is_err() {
err!("Failed to create Collection"); err!("Failed to create Collection");
} }

View File

@@ -1,4 +1,4 @@
use data_encoding::BASE32; use data_encoding::{BASE32, BASE64};
use rocket_contrib::json::Json; use rocket_contrib::json::Json;
use serde_json; use serde_json;
use serde_json::Value; use serde_json::Value;
@@ -31,13 +31,16 @@ pub fn routes() -> Vec<Route> {
generate_yubikey, generate_yubikey,
activate_yubikey, activate_yubikey,
activate_yubikey_put, activate_yubikey_put,
get_duo,
activate_duo,
activate_duo_put,
] ]
} }
#[get("/two-factor")] #[get("/two-factor")]
fn get_twofactor(headers: Headers, conn: DbConn) -> JsonResult { fn get_twofactor(headers: Headers, conn: DbConn) -> JsonResult {
let twofactors = TwoFactor::find_by_user(&headers.user.uuid, &conn); let twofactors = TwoFactor::find_by_user(&headers.user.uuid, &conn);
let twofactors_json: Vec<Value> = twofactors.iter().map(|c| c.to_json_list()).collect(); let twofactors_json: Vec<Value> = twofactors.iter().map(TwoFactor::to_json_list).collect();
Ok(Json(json!({ Ok(Json(json!({
"Data": twofactors_json, "Data": twofactors_json,
@@ -92,9 +95,7 @@ fn recover(data: JsonUpcase<RecoverTwoFactor>, conn: DbConn) -> JsonResult {
} }
// Remove all twofactors from the user // Remove all twofactors from the user
for twofactor in TwoFactor::find_by_user(&user.uuid, &conn) { TwoFactor::delete_all_by_user(&user.uuid, &conn)?;
twofactor.delete(&conn)?;
}
// Remove the recovery code, not needed without twofactors // Remove the recovery code, not needed without twofactors
user.totp_recover = None; user.totp_recover = None;
@@ -102,6 +103,14 @@ fn recover(data: JsonUpcase<RecoverTwoFactor>, conn: DbConn) -> JsonResult {
Ok(Json(json!({}))) Ok(Json(json!({})))
} }
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).ok();
}
}
#[derive(Deserialize)] #[derive(Deserialize)]
#[allow(non_snake_case)] #[allow(non_snake_case)]
struct DisableTwoFactorData { struct DisableTwoFactorData {
@@ -119,7 +128,7 @@ fn disable_twofactor(data: JsonUpcase<DisableTwoFactorData>, headers: Headers, c
err!("Invalid password"); err!("Invalid password");
} }
let type_ = data.Type.into_i32().expect("Invalid type"); let type_ = data.Type.into_i32()?;
if let Some(twofactor) = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn) { if let Some(twofactor) = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn) {
twofactor.delete(&conn)?; twofactor.delete(&conn)?;
@@ -174,10 +183,7 @@ fn activate_authenticator(data: JsonUpcase<EnableAuthenticatorData>, headers: He
let data: EnableAuthenticatorData = data.into_inner().data; let data: EnableAuthenticatorData = data.into_inner().data;
let password_hash = data.MasterPasswordHash; let password_hash = data.MasterPasswordHash;
let key = data.Key; let key = data.Key;
let token = match data.Token.into_i32() { let token = data.Token.into_i32()? as u64;
Some(n) => n as u64,
None => err!("Malformed token"),
};
let mut user = headers.user; let mut user = headers.user;
@@ -199,9 +205,7 @@ fn activate_authenticator(data: JsonUpcase<EnableAuthenticatorData>, headers: He
let twofactor = TwoFactor::new(user.uuid.clone(), type_, key.to_uppercase()); let twofactor = TwoFactor::new(user.uuid.clone(), type_, key.to_uppercase());
// Validate the token provided with the key // Validate the token provided with the key
if !twofactor.check_totp_code(token) { validate_totp_code(token, &twofactor.data)?;
err!("Invalid totp code")
}
_generate_recover_code(&mut user, &conn); _generate_recover_code(&mut user, &conn);
twofactor.save(&conn)?; twofactor.save(&conn)?;
@@ -218,12 +222,29 @@ fn activate_authenticator_put(data: JsonUpcase<EnableAuthenticatorData>, headers
activate_authenticator(data, headers, conn) activate_authenticator(data, headers, conn)
} }
fn _generate_recover_code(user: &mut User, conn: &DbConn) { pub fn validate_totp_code_str(totp_code: &str, secret: &str) -> EmptyResult {
if user.totp_recover.is_none() { let totp_code: u64 = match totp_code.parse() {
let totp_recover = BASE32.encode(&crypto::get_random(vec![0u8; 20])); Ok(code) => code,
user.totp_recover = Some(totp_recover); _ => err!("TOTP code is not a number"),
user.save(conn).ok(); };
validate_totp_code(totp_code, secret)
} }
pub fn validate_totp_code(totp_code: u64, secret: &str) -> EmptyResult {
use oath::{totp_raw_now, HashType};
let decoded_secret = match BASE32.decode(secret.as_bytes()) {
Ok(s) => s,
Err(_) => err!("Invalid TOTP secret"),
};
let generated = totp_raw_now(&decoded_secret, 6, 0, 30, &HashType::SHA1);
if generated != totp_code {
err!("Invalid TOTP code");
}
Ok(())
} }
use u2f::messages::{RegisterResponse, SignResponse, U2fSignRequest}; use u2f::messages::{RegisterResponse, SignResponse, U2fSignRequest};
@@ -244,19 +265,18 @@ fn generate_u2f(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn)
if !CONFIG.domain_set() { if !CONFIG.domain_set() {
err!("`DOMAIN` environment variable is not set. U2F disabled") err!("`DOMAIN` environment variable is not set. U2F disabled")
} }
let data: PasswordData = data.into_inner().data; let data: PasswordData = data.into_inner().data;
let user = headers.user;
if !user.check_valid_password(&data.MasterPasswordHash) { if !headers.user.check_valid_password(&data.MasterPasswordHash) {
err!("Invalid password"); err!("Invalid password");
} }
let u2f_type = TwoFactorType::U2f as i32; let (enabled, keys) = get_u2f_registrations(&headers.user.uuid, &conn)?;
let enabled = TwoFactor::find_by_user_and_type(&user.uuid, u2f_type, &conn).is_some(); let keys_json: Vec<Value> = keys.iter().map(U2FRegistration::to_json).collect();
Ok(Json(json!({ Ok(Json(json!({
"Enabled": enabled, "Enabled": enabled,
"Keys": keys_json,
"Object": "twoFactorU2f" "Object": "twoFactorU2f"
}))) })))
} }
@@ -264,18 +284,16 @@ fn generate_u2f(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn)
#[post("/two-factor/get-u2f-challenge", data = "<data>")] #[post("/two-factor/get-u2f-challenge", data = "<data>")]
fn generate_u2f_challenge(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult { fn generate_u2f_challenge(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult {
let data: PasswordData = data.into_inner().data; let data: PasswordData = data.into_inner().data;
let user = headers.user;
if !user.check_valid_password(&data.MasterPasswordHash) { if !headers.user.check_valid_password(&data.MasterPasswordHash) {
err!("Invalid password"); err!("Invalid password");
} }
let user_uuid = &user.uuid; let _type = TwoFactorType::U2fRegisterChallenge;
let challenge = _create_u2f_challenge(&headers.user.uuid, _type, &conn).challenge;
let challenge = _create_u2f_challenge(user_uuid, TwoFactorType::U2fRegisterChallenge, &conn).challenge;
Ok(Json(json!({ Ok(Json(json!({
"UserId": user.uuid, "UserId": headers.user.uuid,
"AppId": APP_ID.to_string(), "AppId": APP_ID.to_string(),
"Challenge": challenge, "Challenge": challenge,
"Version": U2F_VERSION, "Version": U2F_VERSION,
@@ -291,6 +309,37 @@ struct EnableU2FData {
DeviceResponse: String, DeviceResponse: String,
} }
// This struct is referenced from the U2F lib
// because it doesn't implement Deserialize
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(remote = "Registration")]
struct RegistrationDef {
key_handle: Vec<u8>,
pub_key: Vec<u8>,
attestation_cert: Option<Vec<u8>>,
}
#[derive(Serialize, Deserialize)]
struct U2FRegistration {
id: i32,
name: String,
#[serde(with = "RegistrationDef")]
reg: Registration,
counter: u32,
compromised: bool,
}
impl U2FRegistration {
fn to_json(&self) -> Value {
json!({
"Id": self.id,
"Name": self.name,
"Compromised": self.compromised,
})
}
}
// This struct is copied from the U2F lib // This struct is copied from the U2F lib
// to add an optional error code // to add an optional error code
#[derive(Deserialize)] #[derive(Deserialize)]
@@ -303,8 +352,8 @@ struct RegisterResponseCopy {
pub error_code: Option<NumberOrString>, pub error_code: Option<NumberOrString>,
} }
impl RegisterResponseCopy { impl Into<RegisterResponse> for RegisterResponseCopy {
fn into_response(self) -> RegisterResponse { fn into(self) -> RegisterResponse {
RegisterResponse { RegisterResponse {
registration_data: self.registration_data, registration_data: self.registration_data,
version: self.version, version: self.version,
@@ -331,9 +380,9 @@ fn activate_u2f(data: JsonUpcase<EnableU2FData>, headers: Headers, conn: DbConn)
let challenge: Challenge = serde_json::from_str(&tf_challenge.data)?; let challenge: Challenge = serde_json::from_str(&tf_challenge.data)?;
tf_challenge.delete(&conn)?; tf_challenge.delete(&conn)?;
let response_copy: RegisterResponseCopy = serde_json::from_str(&data.DeviceResponse)?; let response: RegisterResponseCopy = serde_json::from_str(&data.DeviceResponse)?;
let error_code = response_copy let error_code = response
.error_code .error_code
.clone() .clone()
.map_or("0".into(), NumberOrString::into_string); .map_or("0".into(), NumberOrString::into_string);
@@ -342,30 +391,27 @@ fn activate_u2f(data: JsonUpcase<EnableU2FData>, headers: Headers, conn: DbConn)
err!("Error registering U2F token") err!("Error registering U2F token")
} }
let response = response_copy.into_response(); let registration = U2F.register_response(challenge.clone(), response.into())?;
let full_registration = U2FRegistration {
id: data.Id.into_i32()?,
name: data.Name,
reg: registration,
compromised: false,
counter: 0,
};
let registration = U2F.register_response(challenge.clone(), response)?; let mut regs = get_u2f_registrations(&user.uuid, &conn)?.1;
// TODO: Allow more than one U2F device
let mut registrations = Vec::new();
registrations.push(registration);
let tf_registration = TwoFactor::new( // TODO: Check that there is no repeat Id
user.uuid.clone(), regs.push(full_registration);
TwoFactorType::U2f, save_u2f_registrations(&user.uuid, &regs, &conn)?;
serde_json::to_string(&registrations).unwrap(),
);
tf_registration.save(&conn)?;
_generate_recover_code(&mut user, &conn); _generate_recover_code(&mut user, &conn);
let keys_json: Vec<Value> = regs.iter().map(U2FRegistration::to_json).collect();
Ok(Json(json!({ Ok(Json(json!({
"Enabled": true, "Enabled": true,
"Challenge": { "Keys": keys_json,
"UserId": user.uuid,
"AppId": APP_ID.to_string(),
"Challenge": challenge,
"Version": U2F_VERSION,
},
"Object": "twoFactorU2f" "Object": "twoFactorU2f"
}))) })))
} }
@@ -385,52 +431,76 @@ fn _create_u2f_challenge(user_uuid: &str, type_: TwoFactorType, conn: &DbConn) -
challenge challenge
} }
// This struct is copied from the U2F lib fn save_u2f_registrations(user_uuid: &str, regs: &[U2FRegistration], conn: &DbConn) -> EmptyResult {
// because it doesn't implement Deserialize TwoFactor::new(user_uuid.into(), TwoFactorType::U2f, serde_json::to_string(regs)?).save(&conn)
#[derive(Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
struct RegistrationCopy {
pub key_handle: Vec<u8>,
pub pub_key: Vec<u8>,
pub attestation_cert: Option<Vec<u8>>,
} }
impl Into<Registration> for RegistrationCopy { fn get_u2f_registrations(user_uuid: &str, conn: &DbConn) -> Result<(bool, Vec<U2FRegistration>), Error> {
fn into(self) -> Registration { let type_ = TwoFactorType::U2f as i32;
Registration { let (enabled, regs) = match TwoFactor::find_by_user_and_type(user_uuid, type_, conn) {
key_handle: self.key_handle, Some(tf) => (tf.enabled, tf.data),
pub_key: self.pub_key, None => return Ok((false, Vec::new())), // If no data, return empty list
attestation_cert: self.attestation_cert, };
}
} let data = match serde_json::from_str(&regs) {
Ok(d) => d,
Err(_) => {
// If error, try old format
let mut old_regs = _old_parse_registrations(&regs);
if old_regs.len() != 1 {
err!("The old U2F format only allows one device")
} }
fn _parse_registrations(registations: &str) -> Vec<Registration> { // Convert to new format
let registrations_copy: Vec<RegistrationCopy> = let new_regs = vec![U2FRegistration {
serde_json::from_str(registations).expect("Can't parse RegistrationCopy data"); id: 1,
name: "Unnamed U2F key".into(),
reg: old_regs.remove(0),
compromised: false,
counter: 0,
}];
registrations_copy.into_iter().map(Into::into).collect() // Save new format
save_u2f_registrations(user_uuid, &new_regs, &conn)?;
new_regs
}
};
Ok((enabled, data))
}
fn _old_parse_registrations(registations: &str) -> Vec<Registration> {
#[derive(Deserialize)]
struct Helper(#[serde(with = "RegistrationDef")] Registration);
let regs: Vec<Value> = serde_json::from_str(registations).expect("Can't parse Registration data");
regs.into_iter()
.map(|r| serde_json::from_value(r).unwrap())
.map(|Helper(r)| r)
.collect()
} }
pub fn generate_u2f_login(user_uuid: &str, conn: &DbConn) -> ApiResult<U2fSignRequest> { pub fn generate_u2f_login(user_uuid: &str, conn: &DbConn) -> ApiResult<U2fSignRequest> {
let challenge = _create_u2f_challenge(user_uuid, TwoFactorType::U2fLoginChallenge, conn); let challenge = _create_u2f_challenge(user_uuid, TwoFactorType::U2fLoginChallenge, conn);
let type_ = TwoFactorType::U2f as i32; let registrations: Vec<_> = get_u2f_registrations(user_uuid, conn)?
let twofactor = match TwoFactor::find_by_user_and_type(user_uuid, type_, conn) { .1
Some(tf) => tf, .into_iter()
None => err!("No U2F devices registered"), .map(|r| r.reg)
}; .collect();
let registrations = _parse_registrations(&twofactor.data); if registrations.is_empty() {
let signed_request: U2fSignRequest = U2F.sign_request(challenge, registrations); err!("No U2F devices registered")
}
Ok(signed_request) Ok(U2F.sign_request(challenge, registrations))
} }
pub fn validate_u2f_login(user_uuid: &str, response: &str, conn: &DbConn) -> EmptyResult { pub fn validate_u2f_login(user_uuid: &str, response: &str, conn: &DbConn) -> EmptyResult {
let challenge_type = TwoFactorType::U2fLoginChallenge as i32; let challenge_type = TwoFactorType::U2fLoginChallenge as i32;
let u2f_type = TwoFactorType::U2f as i32;
let tf_challenge = TwoFactor::find_by_user_and_type(user_uuid, challenge_type, &conn); let tf_challenge = TwoFactor::find_by_user_and_type(user_uuid, challenge_type, &conn);
let challenge = match tf_challenge { let challenge = match tf_challenge {
@@ -441,27 +511,29 @@ pub fn validate_u2f_login(user_uuid: &str, response: &str, conn: &DbConn) -> Emp
} }
None => err!("Can't recover login challenge"), None => err!("Can't recover login challenge"),
}; };
let twofactor = match TwoFactor::find_by_user_and_type(user_uuid, u2f_type, conn) {
Some(tf) => tf,
None => err!("No U2F devices registered"),
};
let registrations = _parse_registrations(&twofactor.data);
let response: SignResponse = serde_json::from_str(response)?; let response: SignResponse = serde_json::from_str(response)?;
let mut registrations = get_u2f_registrations(user_uuid, conn)?.1;
if registrations.is_empty() {
err!("No U2F devices registered")
}
let mut _counter: u32 = 0; for reg in &mut registrations {
for registration in registrations { let response = U2F.sign_response(challenge.clone(), reg.reg.clone(), response.clone(), reg.counter);
let response = U2F.sign_response(challenge.clone(), registration, response.clone(), _counter);
match response { match response {
Ok(new_counter) => { Ok(new_counter) => {
_counter = new_counter; reg.counter = new_counter;
info!("O {:#}", new_counter); save_u2f_registrations(user_uuid, &registrations, &conn)?;
return Ok(()); return Ok(());
} }
Err(u2f::u2ferror::U2fError::CounterTooLow) => {
reg.compromised = true;
save_u2f_registrations(user_uuid, &registrations, &conn)?;
err!("This device might be compromised!");
}
Err(e) => { Err(e) => {
info!("E {:#}", e); warn!("E {:#}", e);
// break; // break;
} }
} }
@@ -489,7 +561,7 @@ pub struct YubikeyMetadata {
} }
use yubico::config::Config; use yubico::config::Config;
use yubico::Yubico; use yubico::verify;
fn parse_yubikeys(data: &EnableYubikeyData) -> Vec<String> { fn parse_yubikeys(data: &EnableYubikeyData) -> Vec<String> {
let data_keys = [&data.Key1, &data.Key2, &data.Key3, &data.Key4, &data.Key5]; let data_keys = [&data.Key1, &data.Key2, &data.Key3, &data.Key4, &data.Key5];
@@ -517,12 +589,11 @@ fn get_yubico_credentials() -> Result<(String, String), Error> {
fn verify_yubikey_otp(otp: String) -> EmptyResult { fn verify_yubikey_otp(otp: String) -> EmptyResult {
let (yubico_id, yubico_secret) = get_yubico_credentials()?; let (yubico_id, yubico_secret) = get_yubico_credentials()?;
let yubico = Yubico::new();
let config = Config::default().set_client_id(yubico_id).set_key(yubico_secret); let config = Config::default().set_client_id(yubico_id).set_key(yubico_secret);
match CONFIG.yubico_server() { match CONFIG.yubico_server() {
Some(server) => yubico.verify(otp, config.set_api_hosts(vec![server])), Some(server) => verify(otp, config.set_api_hosts(vec![server])),
None => yubico.verify(otp, config), None => verify(otp, config),
} }
.map_res("Failed to verify OTP") .map_res("Failed to verify OTP")
.and(Ok(())) .and(Ok(()))
@@ -573,11 +644,10 @@ fn activate_yubikey(data: JsonUpcase<EnableYubikeyData>, headers: Headers, conn:
} }
// Check if we already have some data // Check if we already have some data
let yubikey_data = TwoFactor::find_by_user_and_type(&user.uuid, TwoFactorType::YubiKey as i32, &conn); let mut yubikey_data = match TwoFactor::find_by_user_and_type(&user.uuid, TwoFactorType::YubiKey as i32, &conn) {
Some(data) => data,
if let Some(yubikey_data) = yubikey_data { None => TwoFactor::new(user.uuid.clone(), TwoFactorType::YubiKey, String::new()),
yubikey_data.delete(&conn)?; };
}
let yubikeys = parse_yubikeys(&data); let yubikeys = parse_yubikeys(&data);
@@ -605,12 +675,8 @@ fn activate_yubikey(data: JsonUpcase<EnableYubikeyData>, headers: Headers, conn:
Nfc: data.Nfc, Nfc: data.Nfc,
}; };
let yubikey_registration = TwoFactor::new( yubikey_data.data = serde_json::to_string(&yubikey_metadata).unwrap();
user.uuid.clone(), yubikey_data.save(&conn)?;
TwoFactorType::YubiKey,
serde_json::to_string(&yubikey_metadata).unwrap(),
);
yubikey_registration.save(&conn)?;
_generate_recover_code(&mut user, &conn); _generate_recover_code(&mut user, &conn);
@@ -628,20 +694,12 @@ fn activate_yubikey_put(data: JsonUpcase<EnableYubikeyData>, headers: Headers, c
activate_yubikey(data, headers, conn) activate_yubikey(data, headers, conn)
} }
pub fn validate_yubikey_login(user_uuid: &str, response: &str, conn: &DbConn) -> EmptyResult { pub fn validate_yubikey_login(response: &str, twofactor_data: &str) -> EmptyResult {
if response.len() != 44 { if response.len() != 44 {
err!("Invalid Yubikey OTP length"); err!("Invalid Yubikey OTP length");
} }
let yubikey_type = TwoFactorType::YubiKey as i32; let yubikey_metadata: YubikeyMetadata = serde_json::from_str(twofactor_data).expect("Can't parse Yubikey Metadata");
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]; let response_id = &response[..12];
if !yubikey_metadata.Keys.contains(&response_id.to_owned()) { if !yubikey_metadata.Keys.contains(&response_id.to_owned()) {
@@ -655,3 +713,325 @@ pub fn validate_yubikey_login(user_uuid: &str, response: &str, conn: &DbConn) ->
Err(_e) => err!("Failed to verify Yubikey against OTP server"), Err(_e) => err!("Failed to verify Yubikey against OTP server"),
} }
} }
#[derive(Serialize, Deserialize)]
struct DuoData {
host: String,
ik: String,
sk: String,
}
impl DuoData {
fn global() -> Option<Self> {
match CONFIG.duo_host() {
Some(host) => Some(Self {
host,
ik: CONFIG.duo_ikey().unwrap(),
sk: CONFIG.duo_skey().unwrap(),
}),
None => None,
}
}
fn msg(s: &str) -> Self {
Self {
host: s.into(),
ik: s.into(),
sk: s.into(),
}
}
fn secret() -> Self {
Self::msg("<global_secret>")
}
fn obscure(self) -> Self {
let mut host = self.host;
let mut ik = self.ik;
let mut sk = self.sk;
let digits = 4;
let replaced = "************";
host.replace_range(digits.., replaced);
ik.replace_range(digits.., replaced);
sk.replace_range(digits.., replaced);
Self { host, ik, sk }
}
}
enum DuoStatus {
Global(DuoData), // Using the global duo config
User(DuoData), // Using the user's config
Disabled(bool), // True if there is a global setting
}
impl DuoStatus {
fn data(self) -> Option<DuoData> {
match self {
DuoStatus::Global(data) => Some(data),
DuoStatus::User(data) => Some(data),
DuoStatus::Disabled(_) => None,
}
}
}
const DISABLED_MESSAGE_DEFAULT: &str = "<To use the global Duo keys, please leave these fields untouched>";
#[post("/two-factor/get-duo", data = "<data>")]
fn get_duo(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 data = get_user_duo_data(&headers.user.uuid, &conn);
let (enabled, data) = match data {
DuoStatus::Global(_) => (true, Some(DuoData::secret())),
DuoStatus::User(data) => (true, Some(data.obscure())),
DuoStatus::Disabled(true) => (false, Some(DuoData::msg(DISABLED_MESSAGE_DEFAULT))),
DuoStatus::Disabled(false) => (false, None),
};
let json = if let Some(data) = data {
json!({
"Enabled": enabled,
"Host": data.host,
"SecretKey": data.sk,
"IntegrationKey": data.ik,
"Object": "twoFactorDuo"
})
} else {
json!({
"Enabled": enabled,
"Object": "twoFactorDuo"
})
};
Ok(Json(json))
}
#[derive(Deserialize)]
#[allow(non_snake_case, dead_code)]
struct EnableDuoData {
MasterPasswordHash: String,
Host: String,
SecretKey: String,
IntegrationKey: String,
}
impl From<EnableDuoData> for DuoData {
fn from(d: EnableDuoData) -> Self {
Self {
host: d.Host,
ik: d.IntegrationKey,
sk: d.SecretKey,
}
}
}
fn check_duo_fields_custom(data: &EnableDuoData) -> bool {
fn empty_or_default(s: &str) -> bool {
let st = s.trim();
st.is_empty() || s == DISABLED_MESSAGE_DEFAULT
}
!empty_or_default(&data.Host) && !empty_or_default(&data.SecretKey) && !empty_or_default(&data.IntegrationKey)
}
#[post("/two-factor/duo", data = "<data>")]
fn activate_duo(data: JsonUpcase<EnableDuoData>, headers: Headers, conn: DbConn) -> JsonResult {
let data: EnableDuoData = data.into_inner().data;
if !headers.user.check_valid_password(&data.MasterPasswordHash) {
err!("Invalid password");
}
let (data, data_str) = if check_duo_fields_custom(&data) {
let data_req: DuoData = data.into();
let data_str = serde_json::to_string(&data_req)?;
duo_api_request("GET", "/auth/v2/check", "", &data_req).map_res("Failed to validate Duo credentials")?;
(data_req.obscure(), data_str)
} else {
(DuoData::secret(), String::new())
};
let type_ = TwoFactorType::Duo;
let twofactor = TwoFactor::new(headers.user.uuid.clone(), type_, data_str);
twofactor.save(&conn)?;
Ok(Json(json!({
"Enabled": true,
"Host": data.host,
"SecretKey": data.sk,
"IntegrationKey": data.ik,
"Object": "twoFactorDuo"
})))
}
#[put("/two-factor/duo", data = "<data>")]
fn activate_duo_put(data: JsonUpcase<EnableDuoData>, headers: Headers, conn: DbConn) -> JsonResult {
activate_duo(data, headers, conn)
}
fn duo_api_request(method: &str, path: &str, params: &str, data: &DuoData) -> EmptyResult {
const AGENT: &str = "bitwarden_rs:Duo/1.0 (Rust)";
use reqwest::{header::*, Client, Method};
use std::str::FromStr;
let url = format!("https://{}{}", &data.host, path);
let date = Utc::now().to_rfc2822();
let username = &data.ik;
let fields = [&date, method, &data.host, path, params];
let password = crypto::hmac_sign(&data.sk, &fields.join("\n"));
let m = Method::from_str(method).unwrap_or_default();
Client::new()
.request(m, &url)
.basic_auth(username, Some(password))
.header(USER_AGENT, AGENT)
.header(DATE, date)
.send()?
.error_for_status()?;
Ok(())
}
const DUO_EXPIRE: i64 = 300;
const APP_EXPIRE: i64 = 3600;
const AUTH_PREFIX: &str = "AUTH";
const DUO_PREFIX: &str = "TX";
const APP_PREFIX: &str = "APP";
use chrono::Utc;
fn get_user_duo_data(uuid: &str, conn: &DbConn) -> DuoStatus {
let type_ = TwoFactorType::Duo as i32;
// If the user doesn't have an entry, disabled
let twofactor = match TwoFactor::find_by_user_and_type(uuid, type_, &conn) {
Some(t) => t,
None => return DuoStatus::Disabled(DuoData::global().is_some()),
};
// If the user has the required values, we use those
if let Ok(data) = serde_json::from_str(&twofactor.data) {
return DuoStatus::User(data);
}
// Otherwise, we try to use the globals
if let Some(global) = DuoData::global() {
return DuoStatus::Global(global);
}
// If there are no globals configured, just disable it
DuoStatus::Disabled(false)
}
// let (ik, sk, ak, host) = get_duo_keys();
fn get_duo_keys_email(email: &str, conn: &DbConn) -> ApiResult<(String, String, String, String)> {
let data = User::find_by_mail(email, &conn)
.and_then(|u| get_user_duo_data(&u.uuid, &conn).data())
.or_else(DuoData::global)
.map_res("Can't fetch Duo keys")?;
Ok((data.ik, data.sk, CONFIG.get_duo_akey(), data.host))
}
pub fn generate_duo_signature(email: &str, conn: &DbConn) -> ApiResult<(String, String)> {
let now = Utc::now().timestamp();
let (ik, sk, ak, host) = get_duo_keys_email(email, conn)?;
let duo_sign = sign_duo_values(&sk, email, &ik, DUO_PREFIX, now + DUO_EXPIRE);
let app_sign = sign_duo_values(&ak, email, &ik, APP_PREFIX, now + APP_EXPIRE);
Ok((format!("{}:{}", duo_sign, app_sign), host))
}
fn sign_duo_values(key: &str, email: &str, ikey: &str, prefix: &str, expire: i64) -> String {
let val = format!("{}|{}|{}", email, ikey, expire);
let cookie = format!("{}|{}", prefix, BASE64.encode(val.as_bytes()));
format!("{}|{}", cookie, crypto::hmac_sign(key, &cookie))
}
pub fn validate_duo_login(email: &str, response: &str, conn: &DbConn) -> EmptyResult {
let split: Vec<&str> = response.split(':').collect();
if split.len() != 2 {
err!("Invalid response length");
}
let auth_sig = split[0];
let app_sig = split[1];
let now = Utc::now().timestamp();
let (ik, sk, ak, _host) = get_duo_keys_email(email, conn)?;
let auth_user = parse_duo_values(&sk, auth_sig, &ik, AUTH_PREFIX, now)?;
let app_user = parse_duo_values(&ak, app_sig, &ik, APP_PREFIX, now)?;
if !crypto::ct_eq(&auth_user, app_user) || !crypto::ct_eq(&auth_user, email) {
err!("Error validating duo authentication")
}
Ok(())
}
fn parse_duo_values(key: &str, val: &str, ikey: &str, prefix: &str, time: i64) -> ApiResult<String> {
let split: Vec<&str> = val.split('|').collect();
if split.len() != 3 {
err!("Invalid value length")
}
let u_prefix = split[0];
let u_b64 = split[1];
let u_sig = split[2];
let sig = crypto::hmac_sign(key, &format!("{}|{}", u_prefix, u_b64));
if !crypto::ct_eq(crypto::hmac_sign(key, &sig), crypto::hmac_sign(key, u_sig)) {
err!("Duo signatures don't match")
}
if u_prefix != prefix {
err!("Prefixes don't match")
}
let cookie_vec = match BASE64.decode(u_b64.as_bytes()) {
Ok(c) => c,
Err(_) => err!("Invalid Duo cookie encoding"),
};
let cookie = match String::from_utf8(cookie_vec) {
Ok(c) => c,
Err(_) => err!("Invalid Duo cookie encoding"),
};
let cookie_split: Vec<&str> = cookie.split('|').collect();
if cookie_split.len() != 3 {
err!("Invalid cookie length")
}
let username = cookie_split[0];
let u_ikey = cookie_split[1];
let expire = cookie_split[2];
if !crypto::ct_eq(ikey, u_ikey) {
err!("Invalid ikey")
}
let expire = match expire.parse() {
Ok(e) => e,
Err(_) => err!("Invalid expire time"),
};
if time >= expire {
err!("Expired authorization")
}
Ok(username.into())
}

View File

@@ -8,7 +8,7 @@ use rocket::Route;
use reqwest::{header::HeaderMap, Client, Response}; use reqwest::{header::HeaderMap, Client, Response};
use rocket::http::{Cookie}; use rocket::http::Cookie;
use regex::Regex; use regex::Regex;
use soup::prelude::*; use soup::prelude::*;
@@ -22,25 +22,55 @@ pub fn routes() -> Vec<Route> {
const FALLBACK_ICON: &[u8; 344] = include_bytes!("../static/fallback-icon.png"); const FALLBACK_ICON: &[u8; 344] = include_bytes!("../static/fallback-icon.png");
const ALLOWED_CHARS: &str = "_-.";
lazy_static! { lazy_static! {
// Reuse the client between requests // Reuse the client between requests
static ref CLIENT: Client = Client::builder() static ref CLIENT: Client = Client::builder()
.use_sys_proxy()
.gzip(true) .gzip(true)
.timeout(Duration::from_secs(5)) .timeout(Duration::from_secs(CONFIG.icon_download_timeout()))
.default_headers(_header_map()) .default_headers(_header_map())
.build() .build()
.unwrap(); .unwrap();
} }
fn is_valid_domain(domain: &str) -> bool {
// Don't allow empty or too big domains or path traversal
if domain.is_empty() || domain.len() > 255 || domain.contains("..") {
return false;
}
// Only alphanumeric or specific characters
for c in domain.chars() {
if !c.is_alphanumeric() && !ALLOWED_CHARS.contains(c) {
return false;
}
}
true
}
#[get("/<domain>/icon.png")] #[get("/<domain>/icon.png")]
fn icon(domain: String) -> Content<Vec<u8>> { fn icon(domain: String) -> Content<Vec<u8>> {
let icon_type = ContentType::new("image", "x-icon"); let icon_type = ContentType::new("image", "x-icon");
// Validate the domain to avoid directory traversal attacks if !is_valid_domain(&domain) {
if domain.contains('/') || domain.contains("..") { warn!("Invalid domain: {:#?}", domain);
return Content(icon_type, FALLBACK_ICON.to_vec()); return Content(icon_type, FALLBACK_ICON.to_vec());
} }
if let Some(blacklist) = CONFIG.icon_blacklist_regex() {
info!("Icon blacklist enabled: {:#?}", blacklist);
let regex = Regex::new(&blacklist).expect("Valid Regex");
if regex.is_match(&domain) {
warn!("Blacklisted domain: {:#?}", domain);
return Content(icon_type, FALLBACK_ICON.to_vec());
}
}
let icon = get_icon(&domain); let icon = get_icon(&domain);
Content(icon_type, icon) Content(icon_type, icon)
@@ -132,11 +162,17 @@ fn icon_is_expired(path: &str) -> bool {
} }
#[derive(Debug)] #[derive(Debug)]
struct IconList { struct Icon {
priority: u8, priority: u8,
href: String, href: String,
} }
impl Icon {
fn new(priority: u8, href: String) -> Self {
Self { href, priority }
}
}
/// Returns a Result/Tuple which holds a Vector IconList and a string which holds the cookies from the last response. /// Returns a Result/Tuple which holds a Vector IconList and a string which holds the cookies from the last response.
/// There will always be a result with a string which will contain https://example.com/favicon.ico and an empty string for the cookies. /// There will always be a result with a string which will contain https://example.com/favicon.ico and an empty string for the cookies.
/// This does not mean that that location does exists, but it is the default location browser use. /// This does not mean that that location does exists, but it is the default location browser use.
@@ -149,13 +185,13 @@ struct IconList {
/// let (mut iconlist, cookie_str) = get_icon_url("github.com")?; /// let (mut iconlist, cookie_str) = get_icon_url("github.com")?;
/// let (mut iconlist, cookie_str) = get_icon_url("gitlab.com")?; /// let (mut iconlist, cookie_str) = get_icon_url("gitlab.com")?;
/// ``` /// ```
fn get_icon_url(domain: &str) -> Result<(Vec<IconList>, String), Error> { fn get_icon_url(domain: &str) -> Result<(Vec<Icon>, String), Error> {
// Default URL with secure and insecure schemes // Default URL with secure and insecure schemes
let ssldomain = format!("https://{}", domain); let ssldomain = format!("https://{}", domain);
let httpdomain = format!("http://{}", domain); let httpdomain = format!("http://{}", domain);
// Create the iconlist // Create the iconlist
let mut iconlist: Vec<IconList> = Vec::new(); let mut iconlist: Vec<Icon> = Vec::new();
// Create the cookie_str to fill it all the cookies from the response // Create the cookie_str to fill it all the cookies from the response
// These cookies can be used to request/download the favicon image. // These cookies can be used to request/download the favicon image.
@@ -167,13 +203,20 @@ fn get_icon_url(domain: &str) -> Result<(Vec<IconList>, String), Error> {
// Extract the URL from the respose in case redirects occured (like @ gitlab.com) // Extract the URL from the respose in case redirects occured (like @ gitlab.com)
let url = content.url().clone(); let url = content.url().clone();
let raw_cookies = content.headers().get_all("set-cookie"); let raw_cookies = content.headers().get_all("set-cookie");
cookie_str = raw_cookies.iter().map(|raw_cookie| { cookie_str = raw_cookies
let cookie = Cookie::parse(raw_cookie.to_str().unwrap_or_default()).unwrap(); .iter()
.filter_map(|raw_cookie| raw_cookie.to_str().ok())
.map(|cookie_str| {
if let Ok(cookie) = Cookie::parse(cookie_str) {
format!("{}={}; ", cookie.name(), cookie.value()) format!("{}={}; ", cookie.name(), cookie.value())
}).collect::<String>(); } else {
String::new()
}
})
.collect::<String>();
// Add the default favicon.ico to the list with the domain the content responded from. // Add the default favicon.ico to the list with the domain the content responded from.
iconlist.push(IconList { priority: 35, href: url.join("/favicon.ico").unwrap().into_string() }); iconlist.push(Icon::new(35, url.join("/favicon.ico").unwrap().into_string()));
let soup = Soup::from_reader(content)?; let soup = Soup::from_reader(content)?;
// Search for and filter // Search for and filter
@@ -185,15 +228,17 @@ fn get_icon_url(domain: &str) -> Result<(Vec<IconList>, String), Error> {
// Loop through all the found icons and determine it's priority // Loop through all the found icons and determine it's priority
for favicon in favicons { for favicon in favicons {
let sizes = favicon.get("sizes").unwrap_or_default(); let sizes = favicon.get("sizes");
let href = url.join(&favicon.get("href").unwrap_or_default()).unwrap().into_string(); let href = favicon.get("href").expect("Missing href");
let priority = get_icon_priority(&href, &sizes); let full_href = url.join(&href).unwrap().into_string();
iconlist.push(IconList { priority, href }) let priority = get_icon_priority(&full_href, sizes);
iconlist.push(Icon::new(priority, full_href))
} }
} else { } else {
// Add the default favicon.ico to the list with just the given domain // Add the default favicon.ico to the list with just the given domain
iconlist.push(IconList { priority: 35, href: format!("{}/favicon.ico", ssldomain) }); iconlist.push(Icon::new(35, format!("{}/favicon.ico", ssldomain)));
} }
// Sort the iconlist by priority // Sort the iconlist by priority
@@ -204,12 +249,16 @@ fn get_icon_url(domain: &str) -> Result<(Vec<IconList>, String), Error> {
} }
fn get_page(url: &str) -> Result<Response, Error> { fn get_page(url: &str) -> Result<Response, Error> {
//CLIENT.get(url).send()?.error_for_status().map_err(Into::into)
get_page_with_cookies(url, "") get_page_with_cookies(url, "")
} }
fn get_page_with_cookies(url: &str, cookie_str: &str) -> Result<Response, Error> { fn get_page_with_cookies(url: &str, cookie_str: &str) -> Result<Response, Error> {
CLIENT.get(url).header("cookie", cookie_str).send()?.error_for_status().map_err(Into::into) CLIENT
.get(url)
.header("cookie", cookie_str)
.send()?
.error_for_status()
.map_err(Into::into)
} }
/// Returns a Integer with the priority of the type of the icon which to prefer. /// Returns a Integer with the priority of the type of the icon which to prefer.
@@ -224,7 +273,7 @@ fn get_page_with_cookies(url: &str, cookie_str: &str) -> Result<Response, Error>
/// priority1 = get_icon_priority("http://example.com/path/to/a/favicon.png", "32x32"); /// priority1 = get_icon_priority("http://example.com/path/to/a/favicon.png", "32x32");
/// priority2 = get_icon_priority("https://example.com/path/to/a/favicon.ico", ""); /// priority2 = get_icon_priority("https://example.com/path/to/a/favicon.ico", "");
/// ``` /// ```
fn get_icon_priority(href: &str, sizes: &str) -> u8 { fn get_icon_priority(href: &str, sizes: Option<String>) -> u8 {
// Check if there is a dimension set // Check if there is a dimension set
let (width, height) = parse_sizes(sizes); let (width, height) = parse_sizes(sizes);
@@ -272,19 +321,19 @@ fn get_icon_priority(href: &str, sizes: &str) -> u8 {
/// let (width, height) = parse_sizes("x128x128"); // (128, 128) /// let (width, height) = parse_sizes("x128x128"); // (128, 128)
/// let (width, height) = parse_sizes("32"); // (0, 0) /// let (width, height) = parse_sizes("32"); // (0, 0)
/// ``` /// ```
fn parse_sizes(sizes: &str) -> (u16, u16) { fn parse_sizes(sizes: Option<String>) -> (u16, u16) {
let mut width: u16 = 0; let mut width: u16 = 0;
let mut height: u16 = 0; let mut height: u16 = 0;
if !sizes.is_empty() { if let Some(sizes) = sizes {
match Regex::new(r"(?x)(\d+)\D*(\d+)").unwrap().captures(sizes.trim()) { match Regex::new(r"(?x)(\d+)\D*(\d+)").unwrap().captures(sizes.trim()) {
None => {}, None => {}
Some(dimensions) => { Some(dimensions) => {
if dimensions.len() >= 3 { if dimensions.len() >= 3 {
width = dimensions[1].parse::<u16>().unwrap_or_default(); width = dimensions[1].parse::<u16>().unwrap_or_default();
height = dimensions[2].parse::<u16>().unwrap_or_default(); height = dimensions[2].parse::<u16>().unwrap_or_default();
} }
}, }
} }
} }
@@ -292,21 +341,18 @@ fn parse_sizes(sizes: &str) -> (u16, u16) {
} }
fn download_icon(domain: &str) -> Result<Vec<u8>, Error> { fn download_icon(domain: &str) -> Result<Vec<u8>, Error> {
let (mut iconlist, cookie_str) = get_icon_url(&domain)?; let (iconlist, cookie_str) = get_icon_url(&domain)?;
let mut buffer = Vec::new(); let mut buffer = Vec::new();
iconlist.truncate(5); for icon in iconlist.iter().take(5) {
for icon in iconlist { match get_page_with_cookies(&icon.href, &cookie_str) {
let url = icon.href;
info!("Downloading icon for {} via {}...", domain, url);
match get_page_with_cookies(&url, &cookie_str) {
Ok(mut res) => { Ok(mut res) => {
info!("Download finished for {}", url); info!("Downloaded icon from {}", icon.href);
res.copy_to(&mut buffer)?; res.copy_to(&mut buffer)?;
break; break;
}, }
Err(_) => info!("Download failed for {}", url), Err(_) => info!("Download failed for {}", icon.href),
}; };
} }

View File

@@ -9,12 +9,14 @@ use num_traits::FromPrimitive;
use crate::db::models::*; use crate::db::models::*;
use crate::db::DbConn; use crate::db::DbConn;
use crate::util::{self, JsonMap}; use crate::util;
use crate::api::{ApiResult, EmptyResult, JsonResult}; use crate::api::{ApiResult, EmptyResult, JsonResult};
use crate::auth::ClientIp; use crate::auth::ClientIp;
use crate::mail;
use crate::CONFIG; use crate::CONFIG;
pub fn routes() -> Vec<Route> { pub fn routes() -> Vec<Route> {
@@ -68,7 +70,7 @@ fn _refresh_login(data: ConnectData, conn: DbConn) -> JsonResult {
"expires_in": expires_in, "expires_in": expires_in,
"token_type": "Bearer", "token_type": "Bearer",
"refresh_token": device.refresh_token, "refresh_token": device.refresh_token,
"Key": user.key, "Key": user.akey,
"PrivateKey": user.private_key, "PrivateKey": user.private_key,
}))) })))
} }
@@ -99,26 +101,19 @@ fn _password_login(data: ConnectData, conn: DbConn, ip: ClientIp) -> JsonResult
) )
} }
// On iOS, device_type sends "iOS", on others it sends a number let (mut device, new_device) = get_device(&data, &conn, &user);
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 twofactor_token = twofactor_auth(&user.uuid, &data, &mut device, &conn)?;
let mut device = match Device::find_by_uuid(&device_id, &conn) {
Some(device) => { if CONFIG.mail_enabled() && new_device {
// Check if owned device, and recreate if not if let Err(e) = mail::send_new_device_logged_in(&user.email, &ip.ip.to_string(), &device.updated_at, &device.name) {
if device.user_uuid != user.uuid { error!("Error sending new device email: {:#?}", e);
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) if CONFIG.require_device_email() {
} else { err!("Could not send login notification email. Please contact your administrator.")
device }
} }
} }
None => Device::new(device_id, user.uuid.clone(), device_name, device_type),
};
let twofactor_token = twofactor_auth(&user.uuid, &data.clone(), &mut device, &conn)?;
// Common // Common
let user = User::find_by_uuid(&device.user_uuid, &conn).unwrap(); let user = User::find_by_uuid(&device.user_uuid, &conn).unwrap();
@@ -132,9 +127,8 @@ fn _password_login(data: ConnectData, conn: DbConn, ip: ClientIp) -> JsonResult
"expires_in": expires_in, "expires_in": expires_in,
"token_type": "Bearer", "token_type": "Bearer",
"refresh_token": device.refresh_token, "refresh_token": device.refresh_token,
"Key": user.key, "Key": user.akey,
"PrivateKey": user.private_key, "PrivateKey": user.private_key,
//"TwoFactorToken": "11122233333444555666777888999"
}); });
if let Some(token) = twofactor_token { if let Some(token) = twofactor_token {
@@ -145,6 +139,35 @@ fn _password_login(data: ConnectData, conn: DbConn, ip: ClientIp) -> JsonResult
Ok(Json(result)) Ok(Json(result))
} }
/// Retrieves an existing device or creates a new device from ConnectData and the User
fn get_device(data: &ConnectData, conn: &DbConn, user: &User) -> (Device, bool) {
// 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");
let mut new_device = false;
// Find device or create new
let device = match Device::find_by_uuid(&device_id, &conn) {
Some(device) => {
// Check if owned device, and recreate if not
if device.user_uuid != user.uuid {
info!("Device exists but is owned by another user. The old device will be discarded");
new_device = true;
Device::new(device_id, user.uuid.clone(), device_name, device_type)
} else {
device
}
}
None => {
new_device = true;
Device::new(device_id, user.uuid.clone(), device_name, device_type)
}
};
(device, new_device)
}
fn twofactor_auth( fn twofactor_auth(
user_uuid: &str, user_uuid: &str,
data: &ConnectData, data: &ConnectData,
@@ -152,62 +175,46 @@ fn twofactor_auth(
conn: &DbConn, conn: &DbConn,
) -> ApiResult<Option<String>> { ) -> ApiResult<Option<String>> {
let twofactors = TwoFactor::find_by_user(user_uuid, conn); let twofactors = TwoFactor::find_by_user(user_uuid, conn);
let providers: Vec<_> = twofactors.iter().map(|tf| tf.type_).collect();
// No twofactor token if twofactor is disabled // No twofactor token if twofactor is disabled
if twofactors.is_empty() { if twofactors.is_empty() {
return Ok(None); return Ok(None);
} }
let provider = data.two_factor_provider.unwrap_or(providers[0]); // If we aren't given a two factor provider, asume the first one let twofactor_ids: Vec<_> = twofactors.iter().map(|tf| tf.atype).collect();
let selected_id = data.two_factor_provider.unwrap_or(twofactor_ids[0]); // If we aren't given a two factor provider, asume the first one
let twofactor_code = match data.two_factor_token { let twofactor_code = match data.two_factor_token {
Some(ref code) => code, Some(ref code) => code,
None => err_json!(_json_err_twofactor(&providers, user_uuid, conn)?), None => err_json!(_json_err_twofactor(&twofactor_ids, user_uuid, conn)?),
}; };
let twofactor = twofactors.iter().filter(|tf| tf.type_ == provider).nth(0); let selected_twofactor = twofactors.into_iter().filter(|tf| tf.atype == selected_id).nth(0);
use crate::api::core::two_factor as _tf;
use crate::crypto::ct_eq;
let selected_data = _selected_data(selected_twofactor);
let mut remember = data.two_factor_remember.unwrap_or(0);
match TwoFactorType::from_i32(selected_id) {
Some(TwoFactorType::Authenticator) => _tf::validate_totp_code_str(twofactor_code, &selected_data?)?,
Some(TwoFactorType::U2f) => _tf::validate_u2f_login(user_uuid, twofactor_code, conn)?,
Some(TwoFactorType::YubiKey) => _tf::validate_yubikey_login(twofactor_code, &selected_data?)?,
Some(TwoFactorType::Duo) => _tf::validate_duo_login(data.username.as_ref().unwrap(), twofactor_code, conn)?,
match TwoFactorType::from_i32(provider) {
Some(TwoFactorType::Remember) => { Some(TwoFactorType::Remember) => {
match device.twofactor_remember { match device.twofactor_remember {
Some(ref remember) if remember == twofactor_code => return Ok(None), // No twofactor token needed here Some(ref code) if !CONFIG.disable_2fa_remember() && ct_eq(code, twofactor_code) => {
_ => err_json!(_json_err_twofactor(&providers, user_uuid, conn)?), remember = 1; // Make sure we also return the token here, otherwise it will only remember the first time
}
_ => err_json!(_json_err_twofactor(&twofactor_ids, user_uuid, conn)?),
} }
} }
Some(TwoFactorType::Authenticator) => {
let twofactor = match twofactor {
Some(tf) => tf,
None => err!("TOTP not enabled"),
};
let totp_code: u64 = match twofactor_code.parse() {
Ok(code) => code,
_ => err!("Invalid TOTP code"),
};
if !twofactor.check_totp_code(totp_code) {
err_json!(_json_err_twofactor(&providers, user_uuid, conn)?)
}
}
Some(TwoFactorType::U2f) => {
use crate::api::core::two_factor;
two_factor::validate_u2f_login(user_uuid, &twofactor_code, conn)?;
}
Some(TwoFactorType::YubiKey) => {
use crate::api::core::two_factor;
two_factor::validate_yubikey_login(user_uuid, twofactor_code, conn)?;
}
_ => err!("Invalid two factor provider"), _ => err!("Invalid two factor provider"),
} }
if data.two_factor_remember.unwrap_or(0) == 1 { if !CONFIG.disable_2fa_remember() && remember == 1 {
Ok(Some(device.refresh_twofactor_remember())) Ok(Some(device.refresh_twofactor_remember()))
} else { } else {
device.delete_twofactor_remember(); device.delete_twofactor_remember();
@@ -215,6 +222,13 @@ fn twofactor_auth(
} }
} }
fn _selected_data(tf: Option<TwoFactor>) -> ApiResult<String> {
match tf {
Some(tf) => Ok(tf.data),
None => err!("Two factor doesn't exist"),
}
}
fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &DbConn) -> ApiResult<Value> { fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &DbConn) -> ApiResult<Value> {
use crate::api::core::two_factor; use crate::api::core::two_factor;
@@ -236,22 +250,33 @@ fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &DbConn) -> Api
let mut challenge_list = Vec::new(); let mut challenge_list = Vec::new();
for key in request.registered_keys { for key in request.registered_keys {
let mut challenge_map = JsonMap::new(); challenge_list.push(json!({
"appId": request.app_id,
challenge_map.insert("appId".into(), Value::String(request.app_id.clone())); "challenge": request.challenge,
challenge_map.insert("challenge".into(), Value::String(request.challenge.clone())); "version": key.version,
challenge_map.insert("version".into(), Value::String(key.version)); "keyHandle": key.key_handle,
challenge_map.insert("keyHandle".into(), Value::String(key.key_handle.unwrap_or_default())); }));
challenge_list.push(Value::Object(challenge_map));
} }
let mut map = JsonMap::new();
use serde_json;
let challenge_list_str = serde_json::to_string(&challenge_list).unwrap(); let challenge_list_str = serde_json::to_string(&challenge_list).unwrap();
map.insert("Challenges".into(), Value::String(challenge_list_str)); result["TwoFactorProviders2"][provider.to_string()] = json!({
result["TwoFactorProviders2"][provider.to_string()] = Value::Object(map); "Challenges": challenge_list_str,
});
}
Some(TwoFactorType::Duo) => {
let email = match User::find_by_uuid(user_uuid, &conn) {
Some(u) => u.email,
None => err!("User does not exist"),
};
let (signature, host) = two_factor::generate_duo_signature(&email, conn)?;
result["TwoFactorProviders2"][provider.to_string()] = json!({
"Host": host,
"Signature": signature,
});
} }
Some(tf_type @ TwoFactorType::YubiKey) => { Some(tf_type @ TwoFactorType::YubiKey) => {
@@ -260,12 +285,11 @@ fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &DbConn) -> Api
None => err!("No YubiKey devices registered"), None => err!("No YubiKey devices registered"),
}; };
let yubikey_metadata: two_factor::YubikeyMetadata = let yubikey_metadata: two_factor::YubikeyMetadata = serde_json::from_str(&twofactor.data)?;
serde_json::from_str(&twofactor.data).expect("Can't parse Yubikey Metadata");
let mut map = JsonMap::new(); result["TwoFactorProviders2"][provider.to_string()] = json!({
map.insert("Nfc".into(), Value::Bool(yubikey_metadata.Nfc)); "Nfc": yubikey_metadata.Nfc,
result["TwoFactorProviders2"][provider.to_string()] = Value::Object(map); })
} }
_ => {} _ => {}

View File

@@ -47,10 +47,13 @@ impl NumberOrString {
} }
} }
fn into_i32(self) -> Option<i32> { fn into_i32(self) -> ApiResult<i32> {
use std::num::ParseIntError as PIE;
match self { match self {
NumberOrString::Number(n) => Some(n), NumberOrString::Number(n) => Ok(n),
NumberOrString::String(s) => s.parse().ok(), NumberOrString::String(s) => s
.parse()
.map_err(|e: PIE| crate::Error::new("Can't convert to number", e.to_string())),
} }
} }
} }

View File

@@ -88,7 +88,7 @@ fn serialize(val: Value) -> Vec<u8> {
fn serialize_date(date: NaiveDateTime) -> Value { fn serialize_date(date: NaiveDateTime) -> Value {
let seconds: i64 = date.timestamp(); let seconds: i64 = date.timestamp();
let nanos: i64 = date.timestamp_subsec_nanos() as i64; let nanos: i64 = date.timestamp_subsec_nanos().into();
let timestamp = nanos << 34 | seconds; let timestamp = nanos << 34 | seconds;
let bs = timestamp.to_be_bytes(); let bs = timestamp.to_be_bytes();
@@ -230,7 +230,7 @@ pub struct WebSocketUsers {
} }
impl WebSocketUsers { impl WebSocketUsers {
fn send_update(&self, user_uuid: &String, data: &[u8]) -> ws::Result<()> { fn send_update(&self, user_uuid: &str, data: &[u8]) -> ws::Result<()> {
if let Some(user) = self.map.get(user_uuid) { if let Some(user) = self.map.get(user_uuid) {
for sender in user.iter() { for sender in user.iter() {
sender.send(data)?; sender.send(data)?;
@@ -249,7 +249,7 @@ impl WebSocketUsers {
ut, ut,
); );
self.send_update(&user.uuid.clone(), &data).ok(); self.send_update(&user.uuid, &data).ok();
} }
pub fn send_folder_update(&self, ut: UpdateType, folder: &Folder) { pub fn send_folder_update(&self, ut: UpdateType, folder: &Folder) {

View File

@@ -9,11 +9,12 @@ use rocket_contrib::json::Json;
use serde_json::Value; use serde_json::Value;
use crate::util::Cached; use crate::util::Cached;
use crate::error::Error;
use crate::CONFIG; use crate::CONFIG;
pub fn routes() -> Vec<Route> { pub fn routes() -> Vec<Route> {
if CONFIG.web_vault_enabled() { if CONFIG.web_vault_enabled() {
routes![web_index, app_id, web_files, attachments, alive] routes![web_index, app_id, web_files, attachments, alive, images]
} else { } else {
routes![attachments, alive] routes![attachments, alive]
} }
@@ -62,3 +63,13 @@ fn alive() -> Json<String> {
Json(format_date(&Utc::now().naive_utc())) Json(format_date(&Utc::now().naive_utc()))
} }
#[get("/bwrs_images/<filename>")]
fn images(filename: String) -> Result<Content<&'static [u8]>, Error> {
match filename.as_ref() {
"mail-github.png" => Ok(Content(ContentType::PNG, include_bytes!("../static/images/mail-github.png"))),
"logo-gray.png" => Ok(Content(ContentType::PNG, include_bytes!("../static/images/logo-gray.png"))),
"error-x.svg" => Ok(Content(ContentType::SVG, include_bytes!("../static/images/error-x.svg"))),
_ => err!("Image not found"),
}
}

View File

@@ -21,17 +21,11 @@ lazy_static! {
pub static ref JWT_ADMIN_ISSUER: String = format!("{}|admin", CONFIG.domain()); pub static ref JWT_ADMIN_ISSUER: String = format!("{}|admin", CONFIG.domain());
static ref PRIVATE_RSA_KEY: Vec<u8> = match read_file(&CONFIG.private_rsa_key()) { static ref PRIVATE_RSA_KEY: Vec<u8> = match read_file(&CONFIG.private_rsa_key()) {
Ok(key) => key, Ok(key) => key,
Err(e) => panic!( Err(e) => panic!("Error loading private RSA Key.\n Error: {}", e),
"Error loading private RSA Key from {}\n Error: {}",
CONFIG.private_rsa_key(), e
),
}; };
static ref PUBLIC_RSA_KEY: Vec<u8> = match read_file(&CONFIG.public_rsa_key()) { static ref PUBLIC_RSA_KEY: Vec<u8> = match read_file(&CONFIG.public_rsa_key()) {
Ok(key) => key, Ok(key) => key,
Err(e) => panic!( Err(e) => panic!("Error loading public RSA Key.\n Error: {}", e),
"Error loading public RSA Key from {}\n Error: {}",
CONFIG.public_rsa_key(), e
),
}; };
} }
@@ -46,7 +40,6 @@ fn decode_jwt<T: DeserializeOwned>(token: &str, issuer: String) -> Result<T, Err
let validation = jsonwebtoken::Validation { let validation = jsonwebtoken::Validation {
leeway: 30, // 30 seconds leeway: 30, // 30 seconds
validate_exp: true, validate_exp: true,
validate_iat: false, // IssuedAt is the same as NotBefore
validate_nbf: true, validate_nbf: true,
aud: None, aud: None,
iss: Some(issuer), iss: Some(issuer),
@@ -293,7 +286,7 @@ impl<'a, 'r> FromRequest<'a, 'r> for OrgHeaders {
device: headers.device, device: headers.device,
user, user,
org_user_type: { org_user_type: {
if let Some(org_usr_type) = UserOrgType::from_i32(org_user.type_) { if let Some(org_usr_type) = UserOrgType::from_i32(org_user.atype) {
org_usr_type org_usr_type
} else { } else {
// This should only happen if the DB is corrupted // This should only happen if the DB is corrupted

View File

@@ -9,9 +9,14 @@ lazy_static! {
println!("Error loading config:\n\t{:?}\n", e); println!("Error loading config:\n\t{:?}\n", e);
exit(12) exit(12)
}); });
pub static ref CONFIG_FILE: String = get_env("CONFIG_FILE").unwrap_or_else(|| "data/config.json".into()); pub static ref CONFIG_FILE: String = {
let data_folder = get_env("DATA_FOLDER").unwrap_or_else(|| String::from("data"));
get_env("CONFIG_FILE").unwrap_or_else(|| format!("{}/config.json", data_folder))
};
} }
pub type Pass = String;
macro_rules! make_config { macro_rules! make_config {
($( ($(
$(#[doc = $groupdoc:literal])? $(#[doc = $groupdoc:literal])?
@@ -59,13 +64,26 @@ macro_rules! make_config {
/// Merges the values of both builders into a new builder. /// Merges the values of both builders into a new builder.
/// If both have the same element, `other` wins. /// If both have the same element, `other` wins.
fn merge(&self, other: &Self) -> Self { fn merge(&self, other: &Self, show_overrides: bool) -> Self {
let mut overrides = Vec::new();
let mut builder = self.clone(); let mut builder = self.clone();
$($( $($(
if let v @Some(_) = &other.$name { if let v @Some(_) = &other.$name {
builder.$name = v.clone(); builder.$name = v.clone();
if self.$name.is_some() {
overrides.push(stringify!($name).to_uppercase());
}
} }
)+)+ )+)+
if show_overrides && !overrides.is_empty() {
// We can't use warn! here because logging isn't setup yet.
println!("[WARNING] The following environment variables are being overriden by the config file,");
println!("[WARNING] please use the admin panel to make changes to them:");
println!("[WARNING] {}\n", overrides.join(", "));
}
builder builder
} }
@@ -114,6 +132,7 @@ macro_rules! make_config {
fn _get_form_type(rust_type: &str) -> &'static str { fn _get_form_type(rust_type: &str) -> &'static str {
match rust_type { match rust_type {
"Pass" => "password",
"String" => "text", "String" => "text",
"bool" => "checkbox", "bool" => "checkbox",
_ => "number" _ => "number"
@@ -161,7 +180,7 @@ macro_rules! make_config {
match $value { match $value {
Some(v) => v, Some(v) => v,
None => { None => {
let f: &Fn(&ConfigItems) -> _ = &$default_fn; let f: &dyn Fn(&ConfigItems) -> _ = &$default_fn;
f($config) f($config)
} }
} }
@@ -183,10 +202,9 @@ make_config! {
folders { folders {
/// Data folder |> Main data folder /// Data folder |> Main data folder
data_folder: String, false, def, "data".to_string(); data_folder: String, false, def, "data".to_string();
/// Database URL /// Database URL
database_url: String, false, auto, |c| format!("{}/{}", c.data_folder, "db.sqlite3"); database_url: String, false, auto, |c| format!("{}/{}", c.data_folder, "db.sqlite3");
/// Icon chache folder /// Icon cache folder
icon_cache_folder: String, false, auto, |c| format!("{}/{}", c.data_folder, "icon_cache"); icon_cache_folder: String, false, auto, |c| format!("{}/{}", c.data_folder, "icon_cache");
/// Attachments folder /// Attachments folder
attachments_folder: String, false, auto, |c| format!("{}/{}", c.data_folder, "attachments"); attachments_folder: String, false, auto, |c| format!("{}/{}", c.data_folder, "attachments");
@@ -208,28 +226,34 @@ make_config! {
/// General settings /// General settings
settings { settings {
/// Domain URL |> This needs to be set to the URL used to access the server, including 'http[s]://' and port, if it's different than the default. Some server functions don't work correctly without this value /// Domain URL |> This needs to be set to the URL used to access the server, including 'http[s]://'
/// and port, if it's different than the default. Some server functions don't work correctly without this value
domain: String, true, def, "http://localhost".to_string(); domain: String, true, def, "http://localhost".to_string();
/// PRIVATE |> Domain set /// Domain Set |> Indicates if the domain is set by the admin. Otherwise the default will be used.
domain_set: bool, false, def, false; domain_set: bool, false, def, false;
/// Enable web vault /// Enable web vault
web_vault_enabled: bool, false, def, true; web_vault_enabled: bool, false, def, true;
/// Disable icon downloads |> Set to true to disable icon downloading, this would still serve icons from $ICON_CACHE_FOLDER, /// HIBP Api Key |> HaveIBeenPwned API Key, request it here: https://haveibeenpwned.com/API/Key
/// but it won't produce any external network request. Needs to set $ICON_CACHE_TTL to 0, hibp_api_key: Pass, true, option;
/// Disable icon downloads |> Set to true to disable icon downloading, this would still serve icons from
/// $ICON_CACHE_FOLDER, but it won't produce any external network request. Needs to set $ICON_CACHE_TTL to 0,
/// otherwise it will delete them and they won't be downloaded again. /// otherwise it will delete them and they won't be downloaded again.
disable_icon_download: bool, true, def, false; disable_icon_download: bool, true, def, false;
/// Allow new signups |> Controls if new users can register. Note that while this is disabled, users could still be invited /// Allow new signups |> Controls if new users can register. Note that while this is disabled, users could still be invited
signups_allowed: bool, true, def, true; signups_allowed: bool, true, def, true;
/// Allow invitations |> Controls whether users can be invited by organization admins, even when signups are disabled /// Allow invitations |> Controls whether users can be invited by organization admins, even when signups are disabled
invitations_allowed: bool, true, def, true; invitations_allowed: bool, true, def, true;
/// Password iterations |> Number of server-side passwords hashing iterations. The changes only apply when a user changes their password. Not recommended to lower the value /// Password iterations |> Number of server-side passwords hashing iterations.
/// The changes only apply when a user changes their password. Not recommended to lower the value
password_iterations: i32, true, def, 100_000; password_iterations: i32, true, def, 100_000;
/// Show password hints |> Controls if the password hint should be shown directly in the web page. Otherwise, if email is disabled, there is no way to see the password hint /// Show password hints |> Controls if the password hint should be shown directly in the web page.
/// Otherwise, if email is disabled, there is no way to see the password hint
show_password_hint: bool, true, def, true; show_password_hint: bool, true, def, true;
/// Admin page token |> The token used to authenticate in this very same page. Changing it here won't deauthorize the current session /// Admin page token |> The token used to authenticate in this very same page. Changing it here won't deauthorize the current session
admin_token: String, true, option; admin_token: Pass, true, option;
}, },
/// Advanced settings /// Advanced settings
@@ -238,14 +262,41 @@ make_config! {
icon_cache_ttl: u64, true, def, 2_592_000; icon_cache_ttl: u64, true, def, 2_592_000;
/// Negative icon cache expiry |> Number of seconds before trying to download an icon that failed again. /// Negative icon cache expiry |> Number of seconds before trying to download an icon that failed again.
icon_cache_negttl: u64, true, def, 259_200; icon_cache_negttl: u64, true, def, 259_200;
/// Icon download timeout |> Number of seconds when to stop attempting to download an icon.
icon_download_timeout: u64, true, def, 10;
/// Icon blacklist Regex |> Any domains or IPs that match this regex won't be fetched by the icon service.
/// Useful to hide other servers in the local network. Check the WIKI for more details
icon_blacklist_regex: String, true, option;
/// Reload templates (Dev) |> When this is set to true, the templates get reloaded with every request. ONLY use this during development, as it can slow down the server /// Disable Two-Factor remember |> Enabling this would force the users to use a second factor to login every time.
/// Note that the checkbox would still be present, but ignored.
disable_2fa_remember: bool, true, def, false;
/// Require new device emails |> When a user logs in an email is required to be sent.
/// If sending the email fails the login attempt will fail.
require_device_email: bool, true, def, false;
/// Reload templates (Dev) |> When this is set to true, the templates get reloaded with every request.
/// ONLY use this during development, as it can slow down the server
reload_templates: bool, true, def, false; reload_templates: bool, true, def, false;
/// Log routes at launch (Dev)
log_mounts: bool, true, def, false;
/// Enable extended logging /// Enable extended logging
extended_logging: bool, false, def, true; extended_logging: bool, false, def, true;
/// Enable the log to output to Syslog
use_syslog: bool, false, def, false;
/// Log file path /// Log file path
log_file: String, false, option; log_file: String, false, option;
/// Log level
log_level: String, false, def, "Info".to_string();
/// Enable DB WAL |> Turning this off might lead to worse performance, but might help if using bitwarden_rs on some exotic filesystems,
/// that do not support WAL. Please make sure you read project wiki on the topic before changing this setting.
enable_db_wal: bool, false, def, true;
/// Disable Admin Token (Know the risks!) |> Disables the Admin Token for the admin page so you may use your own auth in-front
disable_admin_token: bool, true, def, false;
}, },
/// Yubikey settings /// Yubikey settings
@@ -255,11 +306,25 @@ make_config! {
/// Client ID /// Client ID
yubico_client_id: String, true, option; yubico_client_id: String, true, option;
/// Secret Key /// Secret Key
yubico_secret_key: String, true, option; yubico_secret_key: Pass, true, option;
/// Server /// Server
yubico_server: String, true, option; yubico_server: String, true, option;
}, },
/// Global Duo settings (Note that users can override them)
duo: _enable_duo {
/// Enabled
_enable_duo: bool, true, def, false;
/// Integration Key
duo_ikey: String, true, option;
/// Secret Key
duo_skey: Pass, true, option;
/// Host
duo_host: String, true, option;
/// Application Key (generated automatically)
_duo_akey: Pass, false, option;
},
/// SMTP Email Settings /// SMTP Email Settings
smtp: _enable_smtp { smtp: _enable_smtp {
/// Enabled /// Enabled
@@ -268,8 +333,10 @@ make_config! {
smtp_host: String, true, option; smtp_host: String, true, option;
/// Enable SSL /// Enable SSL
smtp_ssl: bool, true, def, true; smtp_ssl: bool, true, def, true;
/// Use explicit TLS |> Enabling this would force the use of an explicit TLS connection, instead of upgrading an insecure one with STARTTLS
smtp_explicit_tls: bool, true, def, false;
/// Port /// Port
smtp_port: u16, true, auto, |c| if c.smtp_ssl {587} else {25}; smtp_port: u16, true, auto, |c| if c.smtp_explicit_tls {465} else if c.smtp_ssl {587} else {25};
/// From Address /// From Address
smtp_from: String, true, def, String::new(); smtp_from: String, true, def, String::new();
/// From Name /// From Name
@@ -277,11 +344,25 @@ make_config! {
/// Username /// Username
smtp_username: String, true, option; smtp_username: String, true, option;
/// Password /// Password
smtp_password: String, true, option; smtp_password: Pass, true, option;
/// Json form auth mechanism |> Defaults for ssl is "Plain" and "Login" and nothing for non-ssl connections. Possible values: ["Plain", "Login", "Xoauth2"]
smtp_auth_mechanism: String, true, option;
}, },
} }
fn validate_config(cfg: &ConfigItems) -> Result<(), Error> { fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
if let Some(ref token) = cfg.admin_token {
if token.trim().is_empty() {
err!("`ADMIN_TOKEN` is enabled but has an empty value. To enable the admin page without token, use `DISABLE_ADMIN_TOKEN`")
}
}
if (cfg.duo_host.is_some() || cfg.duo_ikey.is_some() || cfg.duo_skey.is_some())
&& !(cfg.duo_host.is_some() && cfg.duo_ikey.is_some() && cfg.duo_skey.is_some())
{
err!("All Duo options need to be set for global Duo support")
}
if cfg.yubico_client_id.is_some() != cfg.yubico_secret_key.is_some() { if cfg.yubico_client_id.is_some() != cfg.yubico_secret_key.is_some() {
err!("Both `YUBICO_CLIENT_ID` and `YUBICO_SECRET_KEY` need to be set for Yubikey OTP support") err!("Both `YUBICO_CLIENT_ID` and `YUBICO_SECRET_KEY` need to be set for Yubikey OTP support")
} }
@@ -304,7 +385,7 @@ impl Config {
let _usr = ConfigBuilder::from_file(&CONFIG_FILE).unwrap_or_default(); let _usr = ConfigBuilder::from_file(&CONFIG_FILE).unwrap_or_default();
// Create merged config, config file overwrites env // Create merged config, config file overwrites env
let builder = _env.merge(&_usr); let builder = _env.merge(&_usr, true);
// Fill any missing with defaults // Fill any missing with defaults
let config = builder.build(); let config = builder.build();
@@ -333,7 +414,7 @@ impl Config {
// Prepare the combined config // Prepare the combined config
let config = { let config = {
let env = &self.inner.read().unwrap()._env; let env = &self.inner.read().unwrap()._env;
env.merge(&builder).build() env.merge(&builder, false).build()
}; };
validate_config(&config)?; validate_config(&config)?;
@@ -352,6 +433,14 @@ impl Config {
Ok(()) Ok(())
} }
pub fn update_config_partial(&self, other: ConfigBuilder) -> Result<(), Error> {
let builder = {
let usr = &self.inner.read().unwrap()._usr;
usr.merge(&other, false)
};
self.update_config(builder)
}
pub fn delete_user_config(&self) -> Result<(), Error> { pub fn delete_user_config(&self) -> Result<(), Error> {
crate::util::delete_file(&CONFIG_FILE)?; crate::util::delete_file(&CONFIG_FILE)?;
@@ -387,9 +476,21 @@ impl Config {
let inner = &self.inner.read().unwrap().config; let inner = &self.inner.read().unwrap().config;
inner._enable_smtp && inner.smtp_host.is_some() inner._enable_smtp && inner.smtp_host.is_some()
} }
pub fn yubico_enabled(&self) -> bool {
let inner = &self.inner.read().unwrap().config; pub fn get_duo_akey(&self) -> String {
inner._enable_yubico && inner.yubico_client_id.is_some() && inner.yubico_secret_key.is_some() if let Some(akey) = self._duo_akey() {
akey
} else {
let akey = crate::crypto::get_random_64();
let akey_s = data_encoding::BASE64.encode(&akey);
// Save the new value
let mut builder = ConfigBuilder::default();
builder._duo_akey = Some(akey_s.clone());
self.update_config_partial(builder).ok();
akey_s
}
} }
pub fn render_template<T: serde::ser::Serialize>( pub fn render_template<T: serde::ser::Serialize>(
@@ -416,21 +517,28 @@ fn load_templates(path: &str) -> Handlebars {
let mut hb = Handlebars::new(); let mut hb = Handlebars::new();
// Error on missing params // Error on missing params
hb.set_strict_mode(true); hb.set_strict_mode(true);
// Register helpers
hb.register_helper("case", Box::new(CaseHelper)); hb.register_helper("case", Box::new(CaseHelper));
hb.register_helper("jsesc", Box::new(JsEscapeHelper));
macro_rules! reg { macro_rules! reg {
($name:expr) => {{ ($name:expr) => {{
let template = include_str!(concat!("static/templates/", $name, ".hbs")); let template = include_str!(concat!("static/templates/", $name, ".hbs"));
hb.register_template_string($name, template).unwrap(); hb.register_template_string($name, template).unwrap();
}}; }};
($name:expr, $ext:expr) => {{
reg!($name);
reg!(concat!($name, $ext));
}};
} }
// First register default templates here // First register default templates here
reg!("email/invite_accepted"); reg!("email/invite_accepted", ".html");
reg!("email/invite_confirmed"); reg!("email/invite_confirmed", ".html");
reg!("email/pw_hint_none"); reg!("email/new_device_logged_in", ".html");
reg!("email/pw_hint_some"); reg!("email/pw_hint_none", ".html");
reg!("email/send_org_invite"); reg!("email/pw_hint_some", ".html");
reg!("email/send_org_invite", ".html");
reg!("admin/base"); reg!("admin/base");
reg!("admin/login"); reg!("admin/login");
@@ -444,7 +552,6 @@ fn load_templates(path: &str) -> Handlebars {
hb hb
} }
#[derive(Clone, Copy)]
pub struct CaseHelper; pub struct CaseHelper;
impl HelperDef for CaseHelper { impl HelperDef for CaseHelper {
@@ -454,7 +561,7 @@ impl HelperDef for CaseHelper {
r: &'reg Handlebars, r: &'reg Handlebars,
ctx: &Context, ctx: &Context,
rc: &mut RenderContext<'reg>, rc: &mut RenderContext<'reg>,
out: &mut Output, out: &mut dyn Output,
) -> HelperResult { ) -> HelperResult {
let param = h let param = h
.param(0) .param(0)
@@ -468,3 +575,31 @@ impl HelperDef for CaseHelper {
} }
} }
} }
pub struct JsEscapeHelper;
impl HelperDef for JsEscapeHelper {
fn call<'reg: 'rc, 'rc>(
&self,
h: &Helper<'reg, 'rc>,
_: &'reg Handlebars,
_: &Context,
_: &mut RenderContext<'reg>,
out: &mut dyn Output,
) -> HelperResult {
let param = h
.param(0)
.ok_or_else(|| RenderError::new("Param not found for helper \"js_escape\""))?;
let value = param
.value()
.as_str()
.ok_or_else(|| RenderError::new("Param for helper \"js_escape\" is not a String"))?;
let escaped_value = value.replace('\\', "").replace('\'', "\\x22").replace('\"', "\\x27");
let quoted_value = format!("&quot;{}&quot;", escaped_value);
out.write(&quoted_value)?;
Ok(())
}
}

View File

@@ -2,7 +2,8 @@
// PBKDF2 derivation // PBKDF2 derivation
// //
use ring::{digest, pbkdf2}; use ring::{digest, hmac, pbkdf2};
use std::num::NonZeroU32;
static DIGEST_ALG: &digest::Algorithm = &digest::SHA256; static DIGEST_ALG: &digest::Algorithm = &digest::SHA256;
const OUTPUT_LEN: usize = digest::SHA256_OUTPUT_LEN; const OUTPUT_LEN: usize = digest::SHA256_OUTPUT_LEN;
@@ -10,15 +11,29 @@ const OUTPUT_LEN: usize = digest::SHA256_OUTPUT_LEN;
pub fn hash_password(secret: &[u8], salt: &[u8], iterations: u32) -> Vec<u8> { pub fn hash_password(secret: &[u8], salt: &[u8], iterations: u32) -> Vec<u8> {
let mut out = vec![0u8; OUTPUT_LEN]; // Initialize array with zeros let mut out = vec![0u8; OUTPUT_LEN]; // Initialize array with zeros
let iterations = NonZeroU32::new(iterations).expect("Iterations can't be zero");
pbkdf2::derive(DIGEST_ALG, iterations, salt, secret, &mut out); pbkdf2::derive(DIGEST_ALG, iterations, salt, secret, &mut out);
out out
} }
pub fn verify_password_hash(secret: &[u8], salt: &[u8], previous: &[u8], iterations: u32) -> bool { pub fn verify_password_hash(secret: &[u8], salt: &[u8], previous: &[u8], iterations: u32) -> bool {
let iterations = NonZeroU32::new(iterations).expect("Iterations can't be zero");
pbkdf2::verify(DIGEST_ALG, iterations, salt, secret, previous).is_ok() pbkdf2::verify(DIGEST_ALG, iterations, salt, secret, previous).is_ok()
} }
//
// HMAC
//
pub fn hmac_sign(key: &str, data: &str) -> String {
use data_encoding::HEXLOWER;
let key = hmac::SigningKey::new(&digest::SHA1, key.as_bytes());
let signature = hmac::sign(&key, data.as_bytes());
HEXLOWER.encode(signature.as_ref())
}
// //
// Random values // Random values
// //
@@ -36,3 +51,12 @@ pub fn get_random(mut array: Vec<u8>) -> Vec<u8> {
array array
} }
//
// Constant time compare
//
pub fn ct_eq<T: AsRef<[u8]>, U: AsRef<[u8]>>(a: T, b: U) -> bool {
use ring::constant_time::verify_slices_are_equal;
verify_slices_are_equal(a.as_ref(), b.as_ref()).is_ok()
}

View File

@@ -2,25 +2,36 @@ use std::ops::Deref;
use diesel::r2d2; use diesel::r2d2;
use diesel::r2d2::ConnectionManager; use diesel::r2d2::ConnectionManager;
use diesel::sqlite::SqliteConnection;
use diesel::{Connection as DieselConnection, ConnectionError}; use diesel::{Connection as DieselConnection, ConnectionError};
use rocket::http::Status; use rocket::http::Status;
use rocket::request::{self, FromRequest}; use rocket::request::{self, FromRequest};
use rocket::{Outcome, Request, State}; use rocket::{Outcome, Request, State};
use crate::error::Error;
use chrono::prelude::*;
use std::process::Command;
use crate::CONFIG; use crate::CONFIG;
/// An alias to the database connection used /// An alias to the database connection used
type Connection = SqliteConnection; #[cfg(feature = "sqlite")]
type Connection = diesel::sqlite::SqliteConnection;
#[cfg(feature = "mysql")]
type Connection = diesel::mysql::MysqlConnection;
/// An alias to the type for a pool of Diesel SQLite connections. /// An alias to the type for a pool of Diesel connections.
type Pool = r2d2::Pool<ConnectionManager<Connection>>; type Pool = r2d2::Pool<ConnectionManager<Connection>>;
/// Connection request guard type: a wrapper around an r2d2 pooled connection. /// Connection request guard type: a wrapper around an r2d2 pooled connection.
pub struct DbConn(pub r2d2::PooledConnection<ConnectionManager<Connection>>); pub struct DbConn(pub r2d2::PooledConnection<ConnectionManager<Connection>>);
pub mod models; pub mod models;
#[cfg(feature = "sqlite")]
#[path = "schemas/sqlite/schema.rs"]
pub mod schema;
#[cfg(feature = "mysql")]
#[path = "schemas/mysql/schema.rs"]
pub mod schema; pub mod schema;
/// Initializes a database pool. /// Initializes a database pool.
@@ -34,6 +45,21 @@ pub fn get_connection() -> Result<Connection, ConnectionError> {
Connection::establish(&CONFIG.database_url()) Connection::establish(&CONFIG.database_url())
} }
/// Creates a back-up of the database using sqlite3
pub fn backup_database() -> Result<(), Error> {
let now: DateTime<Utc> = Utc::now();
let file_date = now.format("%Y%m%d").to_string();
let backup_command: String = format!("{}{}{}", ".backup 'db_", file_date, ".sqlite3'");
Command::new("sqlite3")
.current_dir("./data")
.args(&["db.sqlite3", &backup_command])
.output()
.expect("Can't open database, sqlite3 is not available, make sure it's installed and available on the PATH");
Ok(())
}
/// Attempts to retrieve a single connection from the managed database pool. If /// Attempts to retrieve a single connection from the managed database pool. If
/// no pool is currently managed, fails with an `InternalServerError` status. If /// no pool is currently managed, fails with an `InternalServerError` status. If
/// no connections are available, fails with a `ServiceUnavailable` status. /// no connections are available, fails with a `ServiceUnavailable` status.

View File

@@ -12,7 +12,7 @@ pub struct Attachment {
pub cipher_uuid: String, pub cipher_uuid: String,
pub file_name: String, pub file_name: String,
pub file_size: i32, pub file_size: i32,
pub key: Option<String>, pub akey: Option<String>,
} }
/// Local methods /// Local methods
@@ -23,7 +23,7 @@ impl Attachment {
cipher_uuid, cipher_uuid,
file_name, file_name,
file_size, file_size,
key: None, akey: None,
} }
} }
@@ -43,7 +43,7 @@ impl Attachment {
"FileName": self.file_name, "FileName": self.file_name,
"Size": self.file_size.to_string(), "Size": self.file_size.to_string(),
"SizeName": display_size, "SizeName": display_size,
"Key": self.key, "Key": self.akey,
"Object": "attachment" "Object": "attachment"
}) })
} }

View File

@@ -24,7 +24,7 @@ pub struct Cipher {
Card = 3, Card = 3,
Identity = 4 Identity = 4
*/ */
pub type_: i32, pub atype: i32,
pub name: String, pub name: String,
pub notes: Option<String>, pub notes: Option<String>,
pub fields: Option<String>, pub fields: Option<String>,
@@ -37,7 +37,7 @@ pub struct Cipher {
/// Local methods /// Local methods
impl Cipher { impl Cipher {
pub fn new(type_: i32, name: String) -> Self { pub fn new(atype: i32, name: String) -> Self {
let now = Utc::now().naive_utc(); let now = Utc::now().naive_utc();
Self { Self {
@@ -48,7 +48,7 @@ impl Cipher {
user_uuid: None, user_uuid: None,
organization_uuid: None, organization_uuid: None,
type_, atype,
favorite: false, favorite: false,
name, name,
@@ -72,31 +72,20 @@ use crate::error::MapResult;
/// Database methods /// Database methods
impl Cipher { impl Cipher {
pub fn to_json(&self, host: &str, user_uuid: &str, conn: &DbConn) -> Value { pub fn to_json(&self, host: &str, user_uuid: &str, conn: &DbConn) -> Value {
use super::Attachment;
use crate::util::format_date; use crate::util::format_date;
use serde_json;
let attachments = Attachment::find_by_cipher(&self.uuid, conn); let attachments = Attachment::find_by_cipher(&self.uuid, conn);
let attachments_json: Vec<Value> = 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: Value = if let Some(ref fields) = self.fields { let fields_json = self.fields.as_ref().and_then(|s| serde_json::from_str(s).ok()).unwrap_or(Value::Null);
serde_json::from_str(fields).unwrap() let password_history_json = self.password_history.as_ref().and_then(|s| serde_json::from_str(s).ok()).unwrap_or(Value::Null);
} else {
Value::Null
};
let password_history_json: Value = if let Some(ref password_history) = self.password_history { let mut data_json: Value = serde_json::from_str(&self.data).unwrap_or(Value::Null);
serde_json::from_str(password_history).unwrap()
} else {
Value::Null
};
let mut data_json: Value = serde_json::from_str(&self.data).unwrap();
// TODO: ******* Backwards compat start ********** // TODO: ******* Backwards compat start **********
// To remove backwards compatibility, just remove this entire section // To remove backwards compatibility, just remove this entire section
// and remove the compat code from ciphers::update_cipher_from_data // and remove the compat code from ciphers::update_cipher_from_data
if self.type_ == 1 && data_json["Uris"].is_array() { if self.atype == 1 && data_json["Uris"].is_array() {
let uri = data_json["Uris"][0]["Uri"].clone(); let uri = data_json["Uris"][0]["Uri"].clone();
data_json["Uri"] = uri; data_json["Uri"] = uri;
} }
@@ -104,7 +93,7 @@ impl Cipher {
let mut json_object = json!({ let mut json_object = json!({
"Id": self.uuid, "Id": self.uuid,
"Type": self.type_, "Type": self.atype,
"RevisionDate": format_date(&self.updated_at), "RevisionDate": format_date(&self.updated_at),
"FolderId": self.get_folder_uuid(&user_uuid, &conn), "FolderId": self.get_folder_uuid(&user_uuid, &conn),
"Favorite": self.favorite, "Favorite": self.favorite,
@@ -125,7 +114,7 @@ impl Cipher {
"PasswordHistory": password_history_json, "PasswordHistory": password_history_json,
}); });
let key = match self.type_ { let key = match self.atype {
1 => "Login", 1 => "Login",
2 => "SecureNote", 2 => "SecureNote",
3 => "Card", 3 => "Card",
@@ -239,7 +228,7 @@ impl Cipher {
// Cipher owner // Cipher owner
users_organizations::access_all.eq(true).or( users_organizations::access_all.eq(true).or(
// access_all in Organization // access_all in Organization
users_organizations::type_.le(UserOrgType::Admin as i32).or( users_organizations::atype.le(UserOrgType::Admin as i32).or(
// Org admin or owner // Org admin or owner
users_collections::user_uuid.eq(user_uuid).and( users_collections::user_uuid.eq(user_uuid).and(
users_collections::read_only.eq(false), //R/W access to collection users_collections::read_only.eq(false), //R/W access to collection
@@ -270,7 +259,7 @@ impl Cipher {
// Cipher owner // Cipher owner
users_organizations::access_all.eq(true).or( users_organizations::access_all.eq(true).or(
// access_all in Organization // access_all in Organization
users_organizations::type_.le(UserOrgType::Admin as i32).or( users_organizations::atype.le(UserOrgType::Admin as i32).or(
// Org admin or owner // Org admin or owner
users_collections::user_uuid.eq(user_uuid), // Access to Collection users_collections::user_uuid.eq(user_uuid), // Access to Collection
), ),
@@ -317,7 +306,7 @@ impl Cipher {
)) ))
.filter(ciphers::user_uuid.eq(user_uuid).or( // Cipher owner .filter(ciphers::user_uuid.eq(user_uuid).or( // Cipher owner
users_organizations::access_all.eq(true).or( // access_all in Organization users_organizations::access_all.eq(true).or( // access_all in Organization
users_organizations::type_.le(UserOrgType::Admin as i32).or( // Org admin or owner users_organizations::atype.le(UserOrgType::Admin as i32).or( // Org admin or owner
users_collections::user_uuid.eq(user_uuid).and( // Access to Collection users_collections::user_uuid.eq(user_uuid).and( // Access to Collection
users_organizations::status.eq(UserOrgStatus::Confirmed as i32) users_organizations::status.eq(UserOrgStatus::Confirmed as i32)
) )
@@ -367,7 +356,7 @@ impl Cipher {
.filter(ciphers_collections::cipher_uuid.eq(&self.uuid)) .filter(ciphers_collections::cipher_uuid.eq(&self.uuid))
.filter(users_collections::user_uuid.eq(user_id).or( // User has access to collection .filter(users_collections::user_uuid.eq(user_id).or( // User has access to collection
users_organizations::access_all.eq(true).or( // User has access all users_organizations::access_all.eq(true).or( // User has access all
users_organizations::type_.le(UserOrgType::Admin as i32) // User is admin or owner users_organizations::atype.le(UserOrgType::Admin as i32) // User is admin or owner
) )
)) ))
.select(ciphers_collections::collection_uuid) .select(ciphers_collections::collection_uuid)

View File

@@ -43,11 +43,11 @@ use crate::error::MapResult;
/// Database methods /// Database methods
impl Collection { impl Collection {
pub fn save(&mut self, conn: &DbConn) -> EmptyResult { pub fn save(&self, conn: &DbConn) -> EmptyResult {
self.update_users_revision(conn); self.update_users_revision(conn);
diesel::replace_into(collections::table) diesel::replace_into(collections::table)
.values(&*self) .values(self)
.execute(&**conn) .execute(&**conn)
.map_res("Error saving collection") .map_res("Error saving collection")
} }
@@ -146,7 +146,7 @@ impl Collection {
.filter( .filter(
users_collections::collection_uuid.eq(uuid).or( // Directly accessed collection users_collections::collection_uuid.eq(uuid).or( // Directly accessed collection
users_organizations::access_all.eq(true).or( // access_all in Organization users_organizations::access_all.eq(true).or( // access_all in Organization
users_organizations::type_.le(UserOrgType::Admin as i32) // Org admin or owner users_organizations::atype.le(UserOrgType::Admin as i32) // Org admin or owner
) )
) )
).select(collections::all_columns) ).select(collections::all_columns)

View File

@@ -15,7 +15,7 @@ pub struct Device {
pub name: String, pub name: String,
/// https://github.com/bitwarden/core/tree/master/src/Core/Enums /// https://github.com/bitwarden/core/tree/master/src/Core/Enums
pub type_: i32, pub atype: i32,
pub push_token: Option<String>, pub push_token: Option<String>,
pub refresh_token: String, pub refresh_token: String,
@@ -25,7 +25,7 @@ pub struct Device {
/// Local methods /// Local methods
impl Device { impl Device {
pub fn new(uuid: String, user_uuid: String, name: String, type_: i32) -> Self { pub fn new(uuid: String, user_uuid: String, name: String, atype: i32) -> Self {
let now = Utc::now().naive_utc(); let now = Utc::now().naive_utc();
Self { Self {
@@ -35,7 +35,7 @@ impl Device {
user_uuid, user_uuid,
name, name,
type_, atype,
push_token: None, push_token: None,
refresh_token: String::new(), refresh_token: String::new(),
@@ -70,10 +70,10 @@ impl Device {
let time_now = Utc::now().naive_utc(); let time_now = Utc::now().naive_utc();
self.updated_at = time_now; self.updated_at = time_now;
let orgowner: Vec<_> = orgs.iter().filter(|o| o.type_ == 0).map(|o| o.org_uuid.clone()).collect(); let orgowner: Vec<_> = orgs.iter().filter(|o| o.atype == 0).map(|o| o.org_uuid.clone()).collect();
let orgadmin: Vec<_> = orgs.iter().filter(|o| o.type_ == 1).map(|o| o.org_uuid.clone()).collect(); let orgadmin: Vec<_> = orgs.iter().filter(|o| o.atype == 1).map(|o| o.org_uuid.clone()).collect();
let orguser: Vec<_> = orgs.iter().filter(|o| o.type_ == 2).map(|o| o.org_uuid.clone()).collect(); let orguser: Vec<_> = orgs.iter().filter(|o| o.atype == 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(); let orgmanager: Vec<_> = orgs.iter().filter(|o| o.atype == 3).map(|o| o.org_uuid.clone()).collect();
// Create the JWT claims struct, to send to the client // Create the JWT claims struct, to send to the client

View File

@@ -21,9 +21,9 @@ pub struct UserOrganization {
pub org_uuid: String, pub org_uuid: String,
pub access_all: bool, pub access_all: bool,
pub key: String, pub akey: String,
pub status: i32, pub status: i32,
pub type_: i32, pub atype: i32,
} }
pub enum UserOrgStatus { pub enum UserOrgStatus {
@@ -196,9 +196,9 @@ impl UserOrganization {
org_uuid, org_uuid,
access_all: false, access_all: false,
key: String::new(), akey: String::new(),
status: UserOrgStatus::Accepted as i32, status: UserOrgStatus::Accepted as i32,
type_: UserOrgType::User as i32, atype: UserOrgType::User as i32,
} }
} }
} }
@@ -213,7 +213,7 @@ use crate::error::MapResult;
/// Database methods /// Database methods
impl Organization { impl Organization {
pub fn save(&mut self, conn: &DbConn) -> EmptyResult { pub fn save(&self, conn: &DbConn) -> EmptyResult {
UserOrganization::find_by_org(&self.uuid, conn) UserOrganization::find_by_org(&self.uuid, conn)
.iter() .iter()
.for_each(|user_org| { .for_each(|user_org| {
@@ -221,7 +221,7 @@ impl Organization {
}); });
diesel::replace_into(organizations::table) diesel::replace_into(organizations::table)
.values(&*self) .values(self)
.execute(&**conn) .execute(&**conn)
.map_res("Error saving organization") .map_res("Error saving organization")
} }
@@ -266,9 +266,9 @@ impl UserOrganization {
"MaxStorageGb": 10, // The value doesn't matter, we don't check server-side "MaxStorageGb": 10, // The value doesn't matter, we don't check server-side
// These are per user // These are per user
"Key": self.key, "Key": self.akey,
"Status": self.status, "Status": self.status,
"Type": self.type_, "Type": self.atype,
"Enabled": true, "Enabled": true,
"Object": "profileOrganization", "Object": "profileOrganization",
@@ -285,7 +285,7 @@ impl UserOrganization {
"Email": user.email, "Email": user.email,
"Status": self.status, "Status": self.status,
"Type": self.type_, "Type": self.atype,
"AccessAll": self.access_all, "AccessAll": self.access_all,
"Object": "organizationUserUserDetails", "Object": "organizationUserUserDetails",
@@ -315,7 +315,7 @@ impl UserOrganization {
"UserId": self.user_uuid, "UserId": self.user_uuid,
"Status": self.status, "Status": self.status,
"Type": self.type_, "Type": self.atype,
"AccessAll": self.access_all, "AccessAll": self.access_all,
"Collections": coll_uuids, "Collections": coll_uuids,
@@ -323,11 +323,11 @@ impl UserOrganization {
}) })
} }
pub fn save(&mut self, conn: &DbConn) -> EmptyResult { pub fn save(&self, conn: &DbConn) -> EmptyResult {
User::update_uuid_revision(&self.user_uuid, conn); User::update_uuid_revision(&self.user_uuid, conn);
diesel::replace_into(users_organizations::table) diesel::replace_into(users_organizations::table)
.values(&*self) .values(self)
.execute(&**conn) .execute(&**conn)
.map_res("Error adding user to organization") .map_res("Error adding user to organization")
} }
@@ -357,7 +357,7 @@ impl UserOrganization {
} }
pub fn has_full_access(self) -> bool { pub fn has_full_access(self) -> bool {
self.access_all || self.type_ >= UserOrgType::Admin self.access_all || self.atype >= UserOrgType::Admin
} }
pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> { pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> {
@@ -405,10 +405,10 @@ impl UserOrganization {
.expect("Error loading user organizations") .expect("Error loading user organizations")
} }
pub fn find_by_org_and_type(org_uuid: &str, type_: i32, conn: &DbConn) -> Vec<Self> { pub fn find_by_org_and_type(org_uuid: &str, atype: i32, conn: &DbConn) -> Vec<Self> {
users_organizations::table users_organizations::table
.filter(users_organizations::org_uuid.eq(org_uuid)) .filter(users_organizations::org_uuid.eq(org_uuid))
.filter(users_organizations::type_.eq(type_)) .filter(users_organizations::atype.eq(atype))
.load::<Self>(&**conn) .load::<Self>(&**conn)
.expect("Error loading user organizations") .expect("Error loading user organizations")
} }

View File

@@ -9,7 +9,7 @@ use super::User;
pub struct TwoFactor { pub struct TwoFactor {
pub uuid: String, pub uuid: String,
pub user_uuid: String, pub user_uuid: String,
pub type_: i32, pub atype: i32,
pub enabled: bool, pub enabled: bool,
pub data: String, pub data: String,
} }
@@ -32,31 +32,16 @@ pub enum TwoFactorType {
/// Local methods /// Local methods
impl TwoFactor { impl TwoFactor {
pub fn new(user_uuid: String, type_: TwoFactorType, data: String) -> Self { pub fn new(user_uuid: String, atype: TwoFactorType, data: String) -> Self {
Self { Self {
uuid: crate::util::get_uuid(), uuid: crate::util::get_uuid(),
user_uuid, user_uuid,
type_: type_ as i32, atype: atype as i32,
enabled: true, enabled: true,
data, data,
} }
} }
pub fn check_totp_code(&self, totp_code: u64) -> bool {
let totp_secret = self.data.as_bytes();
use data_encoding::BASE32;
use oath::{totp_raw_now, HashType};
let decoded_secret = match BASE32.decode(totp_secret) {
Ok(s) => s,
Err(_) => return false,
};
let generated = totp_raw_now(&decoded_secret, 6, 0, 30, &HashType::SHA1);
generated == totp_code
}
pub fn to_json(&self) -> Value { pub fn to_json(&self) -> Value {
json!({ json!({
"Enabled": self.enabled, "Enabled": self.enabled,
@@ -68,7 +53,7 @@ impl TwoFactor {
pub fn to_json_list(&self) -> Value { pub fn to_json_list(&self) -> Value {
json!({ json!({
"Enabled": self.enabled, "Enabled": self.enabled,
"Type": self.type_, "Type": self.atype,
"Object": "twoFactorProvider" "Object": "twoFactorProvider"
}) })
} }
@@ -100,15 +85,15 @@ impl TwoFactor {
pub fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> { pub fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
twofactor::table twofactor::table
.filter(twofactor::user_uuid.eq(user_uuid)) .filter(twofactor::user_uuid.eq(user_uuid))
.filter(twofactor::type_.lt(1000)) // Filter implementation types .filter(twofactor::atype.lt(1000)) // Filter implementation types
.load::<Self>(&**conn) .load::<Self>(&**conn)
.expect("Error loading twofactor") .expect("Error loading twofactor")
} }
pub fn find_by_user_and_type(user_uuid: &str, type_: i32, conn: &DbConn) -> Option<Self> { pub fn find_by_user_and_type(user_uuid: &str, atype: i32, conn: &DbConn) -> Option<Self> {
twofactor::table twofactor::table
.filter(twofactor::user_uuid.eq(user_uuid)) .filter(twofactor::user_uuid.eq(user_uuid))
.filter(twofactor::type_.eq(type_)) .filter(twofactor::atype.eq(atype))
.first::<Self>(&**conn) .first::<Self>(&**conn)
.ok() .ok()
} }

View File

@@ -20,7 +20,7 @@ pub struct User {
pub password_iterations: i32, pub password_iterations: i32,
pub password_hint: Option<String>, pub password_hint: Option<String>,
pub key: String, pub akey: String,
pub private_key: Option<String>, pub private_key: Option<String>,
pub public_key: Option<String>, pub public_key: Option<String>,
@@ -37,6 +37,12 @@ pub struct User {
pub client_kdf_iter: i32, pub client_kdf_iter: i32,
} }
enum UserStatus {
Enabled = 0,
Invited = 1,
_Disabled = 2,
}
/// Local methods /// Local methods
impl User { impl User {
pub const CLIENT_KDF_TYPE_DEFAULT: i32 = 0; // PBKDF2: 0 pub const CLIENT_KDF_TYPE_DEFAULT: i32 = 0; // PBKDF2: 0
@@ -52,7 +58,7 @@ impl User {
updated_at: now, updated_at: now,
name: email.clone(), name: email.clone(),
email, email,
key: String::new(), akey: String::new(),
password_hash: Vec::new(), password_hash: Vec::new(),
salt: crypto::get_random_64(), salt: crypto::get_random_64(),
@@ -86,7 +92,7 @@ impl User {
pub fn check_valid_recovery_code(&self, recovery_code: &str) -> bool { pub fn check_valid_recovery_code(&self, recovery_code: &str) -> bool {
if let Some(ref totp_recover) = self.totp_recover { if let Some(ref totp_recover) = self.totp_recover {
recovery_code == totp_recover.to_lowercase() crate::crypto::ct_eq(recovery_code, totp_recover.to_lowercase())
} else { } else {
false false
} }
@@ -113,14 +119,19 @@ use crate::error::MapResult;
/// Database methods /// Database methods
impl User { impl User {
pub fn to_json(&self, conn: &DbConn) -> Value { pub fn to_json(&self, conn: &DbConn) -> Value {
use super::{TwoFactor, UserOrganization};
let orgs = UserOrganization::find_by_user(&self.uuid, conn); let orgs = UserOrganization::find_by_user(&self.uuid, conn);
let orgs_json: Vec<Value> = orgs.iter().map(|c| c.to_json(&conn)).collect(); let orgs_json: Vec<Value> = orgs.iter().map(|c| c.to_json(&conn)).collect();
let twofactor_enabled = !TwoFactor::find_by_user(&self.uuid, conn).is_empty(); let twofactor_enabled = !TwoFactor::find_by_user(&self.uuid, conn).is_empty();
// TODO: Might want to save the status field in the DB
let status = if self.password_hash.is_empty() {
UserStatus::Invited
} else {
UserStatus::Enabled
};
json!({ json!({
"_Enabled": !self.password_hash.is_empty(), "_Status": status as i32,
"Id": self.uuid, "Id": self.uuid,
"Name": self.name, "Name": self.name,
"Email": self.email, "Email": self.email,
@@ -129,7 +140,7 @@ impl User {
"MasterPasswordHint": self.password_hint, "MasterPasswordHint": self.password_hint,
"Culture": "en-US", "Culture": "en-US",
"TwoFactorEnabled": twofactor_enabled, "TwoFactorEnabled": twofactor_enabled,
"Key": self.key, "Key": self.akey,
"PrivateKey": self.private_key, "PrivateKey": self.private_key,
"SecurityStamp": self.security_stamp, "SecurityStamp": self.security_stamp,
"Organizations": orgs_json, "Organizations": orgs_json,
@@ -152,7 +163,7 @@ impl User {
pub fn delete(self, conn: &DbConn) -> EmptyResult { pub fn delete(self, conn: &DbConn) -> EmptyResult {
for user_org in UserOrganization::find_by_user(&self.uuid, &*conn) { for user_org in UserOrganization::find_by_user(&self.uuid, &*conn) {
if user_org.type_ == UserOrgType::Owner { if user_org.atype == UserOrgType::Owner {
let owner_type = UserOrgType::Owner as i32; let owner_type = UserOrgType::Owner as i32;
if UserOrganization::find_by_org_and_type(&user_org.org_uuid, owner_type, &conn).len() <= 1 { if UserOrganization::find_by_org_and_type(&user_org.org_uuid, owner_type, &conn).len() <= 1 {
err!("Can't delete last owner") err!("Can't delete last owner")
@@ -178,6 +189,20 @@ impl User {
} }
} }
pub fn update_all_revisions(conn: &DbConn) -> EmptyResult {
let updated_at = Utc::now().naive_utc();
crate::util::retry(
|| {
diesel::update(users::table)
.set(users::updated_at.eq(updated_at))
.execute(&**conn)
},
10,
)
.map_res("Error updating revision date for all users")
}
pub fn update_revision(&mut self, conn: &DbConn) -> EmptyResult { pub fn update_revision(&mut self, conn: &DbConn) -> EmptyResult {
self.updated_at = Utc::now().naive_utc(); self.updated_at = Utc::now().naive_utc();
@@ -225,13 +250,13 @@ impl Invitation {
Self { email } Self { email }
} }
pub fn save(&mut self, conn: &DbConn) -> EmptyResult { pub fn save(&self, conn: &DbConn) -> EmptyResult {
if self.email.trim().is_empty() { if self.email.trim().is_empty() {
err!("Invitation email can't be empty") err!("Invitation email can't be empty")
} }
diesel::replace_into(invitations::table) diesel::replace_into(invitations::table)
.values(&*self) .values(self)
.execute(&**conn) .execute(&**conn)
.map_res("Error saving invitation") .map_res("Error saving invitation")
} }

View File

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

View File

@@ -4,7 +4,7 @@ table! {
cipher_uuid -> Text, cipher_uuid -> Text,
file_name -> Text, file_name -> Text,
file_size -> Integer, file_size -> Integer,
key -> Nullable<Text>, akey -> Nullable<Text>,
} }
} }
@@ -15,8 +15,7 @@ table! {
updated_at -> Timestamp, updated_at -> Timestamp,
user_uuid -> Nullable<Text>, user_uuid -> Nullable<Text>,
organization_uuid -> Nullable<Text>, organization_uuid -> Nullable<Text>,
#[sql_name = "type"] atype -> Integer,
type_ -> Integer,
name -> Text, name -> Text,
notes -> Nullable<Text>, notes -> Nullable<Text>,
fields -> Nullable<Text>, fields -> Nullable<Text>,
@@ -48,8 +47,7 @@ table! {
updated_at -> Timestamp, updated_at -> Timestamp,
user_uuid -> Text, user_uuid -> Text,
name -> Text, name -> Text,
#[sql_name = "type"] atype -> Integer,
type_ -> Integer,
push_token -> Nullable<Text>, push_token -> Nullable<Text>,
refresh_token -> Text, refresh_token -> Text,
twofactor_remember -> Nullable<Text>, twofactor_remember -> Nullable<Text>,
@@ -91,8 +89,7 @@ table! {
twofactor (uuid) { twofactor (uuid) {
uuid -> Text, uuid -> Text,
user_uuid -> Text, user_uuid -> Text,
#[sql_name = "type"] atype -> Integer,
type_ -> Integer,
enabled -> Bool, enabled -> Bool,
data -> Text, data -> Text,
} }
@@ -109,7 +106,7 @@ table! {
salt -> Binary, salt -> Binary,
password_iterations -> Integer, password_iterations -> Integer,
password_hint -> Nullable<Text>, password_hint -> Nullable<Text>,
key -> Text, akey -> Text,
private_key -> Nullable<Text>, private_key -> Nullable<Text>,
public_key -> Nullable<Text>, public_key -> Nullable<Text>,
totp_secret -> Nullable<Text>, totp_secret -> Nullable<Text>,
@@ -136,10 +133,9 @@ table! {
user_uuid -> Text, user_uuid -> Text,
org_uuid -> Text, org_uuid -> Text,
access_all -> Bool, access_all -> Bool,
key -> Text, akey -> Text,
status -> Integer, status -> Integer,
#[sql_name = "type"] atype -> Integer,
type_ -> Integer,
} }
} }

View File

@@ -5,16 +5,18 @@ use std::error::Error as StdError;
macro_rules! make_error { macro_rules! make_error {
( $( $name:ident ( $ty:ty ): $src_fn:expr, $usr_msg_fun:expr ),+ $(,)? ) => { ( $( $name:ident ( $ty:ty ): $src_fn:expr, $usr_msg_fun:expr ),+ $(,)? ) => {
const BAD_REQUEST: u16 = 400;
#[derive(Display)] #[derive(Display)]
pub enum ErrorKind { $($name( $ty )),+ } pub enum ErrorKind { $($name( $ty )),+ }
pub struct Error { message: String, error: ErrorKind } pub struct Error { message: String, error: ErrorKind, error_code: u16 }
$(impl From<$ty> for Error { $(impl From<$ty> for Error {
fn from(err: $ty) -> Self { Error::from((stringify!($name), err)) } fn from(err: $ty) -> Self { Error::from((stringify!($name), err)) }
})+ })+
$(impl<S: Into<String>> From<(S, $ty)> for Error { $(impl<S: Into<String>> From<(S, $ty)> for Error {
fn from(val: (S, $ty)) -> Self { fn from(val: (S, $ty)) -> Self {
Error { message: val.0.into(), error: ErrorKind::$name(val.1) } Error { message: val.0.into(), error: ErrorKind::$name(val.1), error_code: BAD_REQUEST }
} }
})+ })+
impl StdError for Error { impl StdError for Error {
@@ -39,16 +41,23 @@ use regex::Error as RegexErr;
use reqwest::Error as ReqErr; use reqwest::Error as ReqErr;
use serde_json::{Error as SerdeErr, Value}; use serde_json::{Error as SerdeErr, Value};
use std::io::Error as IOErr; use std::io::Error as IOErr;
use std::option::NoneError as NoneErr;
use std::time::SystemTimeError as TimeErr; use std::time::SystemTimeError as TimeErr;
use u2f::u2ferror::U2fError as U2fErr; use u2f::u2ferror::U2fError as U2fErr;
use yubico::yubicoerror::YubicoError as YubiErr; use yubico::yubicoerror::YubicoError as YubiErr;
#[derive(Display, Serialize)]
pub struct Empty {}
// Error struct // Error struct
// Contains a String error message, meant for the user and an enum variant, with an error of different types. // Contains a String error message, meant for the user and an enum variant, with an error of different types.
// //
// After the variant itself, there are two expressions. The first one indicates whether the error contains a source error (that we pretty print). // After the variant itself, there are two expressions. The first one indicates whether the error contains a source error (that we pretty print).
// The second one contains the function used to obtain the response sent to the client // The second one contains the function used to obtain the response sent to the client
make_error! { make_error! {
// Just an empty error
EmptyError(Empty): _no_source, _serialize,
// Used to represent err! calls // Used to represent err! calls
SimpleError(String): _no_source, _api_error, SimpleError(String): _no_source, _api_error,
// Used for special return values, like 2FA errors // Used for special return values, like 2FA errors
@@ -66,6 +75,13 @@ make_error! {
YubiError(YubiErr): _has_source, _api_error, YubiError(YubiErr): _has_source, _api_error,
} }
// This is implemented by hand because NoneError doesn't implement neither Display nor Error
impl From<NoneErr> for Error {
fn from(_: NoneErr) -> Self {
Error::from(("NoneError", String::new()))
}
}
impl std::fmt::Debug for Error { impl std::fmt::Debug for Error {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self.source() { match self.source() {
@@ -80,10 +96,19 @@ impl Error {
(usr_msg, log_msg.into()).into() (usr_msg, log_msg.into()).into()
} }
pub fn empty() -> Self {
Empty {}.into()
}
pub fn with_msg<M: Into<String>>(mut self, msg: M) -> Self { pub fn with_msg<M: Into<String>>(mut self, msg: M) -> Self {
self.message = msg.into(); self.message = msg.into();
self self
} }
pub fn with_code(mut self, code: u16) -> Self {
self.error_code = code;
self
}
} }
pub trait MapResult<S> { pub trait MapResult<S> {
@@ -102,6 +127,12 @@ impl<E: Into<Error>> MapResult<()> for Result<usize, E> {
} }
} }
impl<S> MapResult<S> for Option<S> {
fn map_res(self, msg: &str) -> Result<S, Error> {
self.ok_or_else(|| Error::new(msg, ""))
}
}
fn _has_source<T>(e: T) -> Option<T> { fn _has_source<T>(e: T) -> Option<T> {
Some(e) Some(e)
} }
@@ -142,8 +173,10 @@ impl<'r> Responder<'r> for Error {
let usr_msg = format!("{}", self); let usr_msg = format!("{}", self);
error!("{:#?}", self); error!("{:#?}", self);
let code = Status::from_code(self.error_code).unwrap_or(Status::BadRequest);
Response::build() Response::build()
.status(Status::BadRequest) .status(code)
.header(ContentType::JSON) .header(ContentType::JSON)
.sized_body(Cursor::new(usr_msg)) .sized_body(Cursor::new(usr_msg))
.ok() .ok()

View File

@@ -1,13 +1,17 @@
use lettre::smtp::authentication::Credentials; use lettre::smtp::authentication::Credentials;
use lettre::smtp::authentication::Mechanism as SmtpAuthMechanism;
use lettre::smtp::ConnectionReuseParameters; use lettre::smtp::ConnectionReuseParameters;
use lettre::{ClientSecurity, ClientTlsParameters, SmtpClient, SmtpTransport, Transport}; use lettre::{ClientSecurity, ClientTlsParameters, SmtpClient, SmtpTransport, Transport};
use lettre_email::EmailBuilder; use lettre_email::{EmailBuilder, MimeMultipartType, PartBuilder};
use native_tls::{Protocol, TlsConnector}; use native_tls::{Protocol, TlsConnector};
use percent_encoding::{percent_encode, NON_ALPHANUMERIC};
use quoted_printable::encode_to_str;
use crate::api::EmptyResult; use crate::api::EmptyResult;
use crate::auth::{encode_jwt, generate_invite_claims}; use crate::auth::{encode_jwt, generate_invite_claims};
use crate::error::Error; use crate::error::Error;
use crate::CONFIG; use crate::CONFIG;
use chrono::NaiveDateTime;
fn mailer() -> SmtpTransport { fn mailer() -> SmtpTransport {
let host = CONFIG.smtp_host().unwrap(); let host = CONFIG.smtp_host().unwrap();
@@ -18,7 +22,13 @@ fn mailer() -> SmtpTransport {
.build() .build()
.unwrap(); .unwrap();
ClientSecurity::Required(ClientTlsParameters::new(host.clone(), tls)) let params = ClientTlsParameters::new(host.clone(), tls);
if CONFIG.smtp_explicit_tls() {
ClientSecurity::Wrapper(params)
} else {
ClientSecurity::Required(params)
}
} else { } else {
ClientSecurity::None ClientSecurity::None
}; };
@@ -30,14 +40,31 @@ fn mailer() -> SmtpTransport {
_ => smtp_client, _ => smtp_client,
}; };
let smtp_client = match &CONFIG.smtp_auth_mechanism() {
Some(auth_mechanism_json) => {
let auth_mechanism = serde_json::from_str::<SmtpAuthMechanism>(&auth_mechanism_json);
match auth_mechanism {
Ok(auth_mechanism) => smtp_client.authentication_mechanism(auth_mechanism),
Err(_) => panic!("Failure to parse mechanism. Is it proper Json? Eg. `\"Plain\"` not `Plain`"),
}
},
_ => smtp_client,
};
smtp_client smtp_client
.smtp_utf8(true) .smtp_utf8(true)
.connection_reuse(ConnectionReuseParameters::NoReuse) .connection_reuse(ConnectionReuseParameters::NoReuse)
.transport() .transport()
} }
fn get_text(template_name: &'static str, data: serde_json::Value) -> Result<(String, String), Error> { fn get_text(template_name: &'static str, data: serde_json::Value) -> Result<(String, String, String), Error> {
let text = CONFIG.render_template(template_name, &data)?; let (subject_html, body_html) = get_template(&format!("{}.html", template_name), &data)?;
let (_subject_text, body_text) = get_template(template_name, &data)?;
Ok((subject_html, body_html, body_text))
}
fn get_template(template_name: &str, data: &serde_json::Value) -> Result<(String, String), Error> {
let text = CONFIG.render_template(template_name, data)?;
let mut text_split = text.split("<!---------------->"); let mut text_split = text.split("<!---------------->");
let subject = match text_split.next() { let subject = match text_split.next() {
@@ -60,9 +87,9 @@ pub fn send_password_hint(address: &str, hint: Option<String>) -> EmptyResult {
"email/pw_hint_none" "email/pw_hint_none"
}; };
let (subject, body) = get_text(template_name, json!({ "hint": hint }))?; let (subject, body_html, body_text) = get_text(template_name, json!({ "hint": hint, "url": CONFIG.domain() }))?;
send_email(&address, &subject, &body) send_email(&address, &subject, &body_html, &body_text)
} }
pub fn send_invite( pub fn send_invite(
@@ -82,23 +109,23 @@ pub fn send_invite(
); );
let invite_token = encode_jwt(&claims); let invite_token = encode_jwt(&claims);
let (subject, body) = get_text( let (subject, body_html, body_text) = get_text(
"email/send_org_invite", "email/send_org_invite",
json!({ json!({
"url": CONFIG.domain(), "url": CONFIG.domain(),
"org_id": org_id.unwrap_or_else(|| "_".to_string()), "org_id": org_id.unwrap_or_else(|| "_".to_string()),
"org_user_id": org_user_id.unwrap_or_else(|| "_".to_string()), "org_user_id": org_user_id.unwrap_or_else(|| "_".to_string()),
"email": address, "email": percent_encode(address.as_bytes(), NON_ALPHANUMERIC).to_string(),
"org_name": org_name, "org_name": org_name,
"token": invite_token, "token": invite_token,
}), }),
)?; )?;
send_email(&address, &subject, &body) send_email(&address, &subject, &body_html, &body_text)
} }
pub fn send_invite_accepted(new_user_email: &str, address: &str, org_name: &str) -> EmptyResult { pub fn send_invite_accepted(new_user_email: &str, address: &str, org_name: &str) -> EmptyResult {
let (subject, body) = get_text( let (subject, body_html, body_text) = get_text(
"email/invite_accepted", "email/invite_accepted",
json!({ json!({
"url": CONFIG.domain(), "url": CONFIG.domain(),
@@ -107,11 +134,11 @@ pub fn send_invite_accepted(new_user_email: &str, address: &str, org_name: &str)
}), }),
)?; )?;
send_email(&address, &subject, &body) send_email(&address, &subject, &body_html, &body_text)
} }
pub fn send_invite_confirmed(address: &str, org_name: &str) -> EmptyResult { pub fn send_invite_confirmed(address: &str, org_name: &str) -> EmptyResult {
let (subject, body) = get_text( let (subject, body_html, body_text) = get_text(
"email/invite_confirmed", "email/invite_confirmed",
json!({ json!({
"url": CONFIG.domain(), "url": CONFIG.domain(),
@@ -119,21 +146,62 @@ pub fn send_invite_confirmed(address: &str, org_name: &str) -> EmptyResult {
}), }),
)?; )?;
send_email(&address, &subject, &body) send_email(&address, &subject, &body_html, &body_text)
} }
fn send_email(address: &str, subject: &str, body: &str) -> EmptyResult { pub fn send_new_device_logged_in(address: &str, ip: &str, dt: &NaiveDateTime, device: &str) -> EmptyResult {
use crate::util::upcase_first;
let device = upcase_first(device);
let datetime = dt.format("%A, %B %_d, %Y at %H:%M").to_string();
let (subject, body_html, body_text) = get_text(
"email/new_device_logged_in",
json!({
"url": CONFIG.domain(),
"ip": ip,
"device": device,
"datetime": datetime,
}),
)?;
send_email(&address, &subject, &body_html, &body_text)
}
fn send_email(address: &str, subject: &str, body_html: &str, body_text: &str) -> EmptyResult {
let html = PartBuilder::new()
.body(encode_to_str(body_html))
.header(("Content-Type", "text/html; charset=utf-8"))
.header(("Content-Transfer-Encoding", "quoted-printable"))
.build();
let text = PartBuilder::new()
.body(encode_to_str(body_text))
.header(("Content-Type", "text/plain; charset=utf-8"))
.header(("Content-Transfer-Encoding", "quoted-printable"))
.build();
let alternative = PartBuilder::new()
.message_type(MimeMultipartType::Alternative)
.child(text)
.child(html);
let email = EmailBuilder::new() let email = EmailBuilder::new()
.to(address) .to(address)
.from((CONFIG.smtp_from().as_str(), CONFIG.smtp_from_name().as_str())) .from((CONFIG.smtp_from().as_str(), CONFIG.smtp_from_name().as_str()))
.subject(subject) .subject(subject)
.header(("Content-Type", "text/html")) .child(alternative.build())
.body(body)
.build() .build()
.map_err(|e| Error::new("Error building email", e.to_string()))?; .map_err(|e| Error::new("Error building email", e.to_string()))?;
mailer() let mut transport = mailer();
let result = transport
.send(email.into()) .send(email.into())
.map_err(|e| Error::new("Error sending email", e.to_string())) .map_err(|e| Error::new("Error sending email", e.to_string()))
.and(Ok(())) .and(Ok(()));
// Explicitly close the connection, in case of error
transport.close();
result
} }

View File

@@ -20,8 +20,6 @@ extern crate derive_more;
#[macro_use] #[macro_use]
extern crate num_derive; extern crate num_derive;
use rocket::{fairing::AdHoc, Rocket};
use std::{ use std::{
path::Path, path::Path,
process::{exit, Command}, process::{exit, Command},
@@ -38,38 +36,11 @@ mod mail;
mod util; mod util;
pub use config::CONFIG; pub use config::CONFIG;
pub use error::{Error, MapResult};
fn init_rocket() -> Rocket {
rocket::ignite()
.mount("/", api::web_routes())
.mount("/api", api::core_routes())
.mount("/admin", api::admin_routes())
.mount("/identity", api::identity_routes())
.mount("/icons", api::icons_routes())
.mount("/notifications", api::notifications_routes())
.manage(db::init_pool())
.manage(api::start_notification_server())
.attach(util::AppHeaders())
.attach(unofficial_warning())
}
// Embed the migrations from the migrations folder into the application
// This way, the program automatically migrates the database to the latest version
// https://docs.rs/diesel_migrations/*/diesel_migrations/macro.embed_migrations.html
#[allow(unused_imports)]
mod migrations {
embed_migrations!();
pub fn run_migrations() {
// Make sure the database is up to date (create if it doesn't exist, or run the migrations)
let connection = 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");
}
}
fn main() { fn main() {
launch_info();
if CONFIG.extended_logging() { if CONFIG.extended_logging() {
init_logging().ok(); init_logging().ok();
} }
@@ -79,10 +50,26 @@ fn main() {
check_web_vault(); check_web_vault();
migrations::run_migrations(); migrations::run_migrations();
init_rocket().launch(); launch_rocket();
}
fn launch_info() {
println!("/--------------------------------------------------------------------\\");
println!("| Starting Bitwarden_RS |");
if let Some(version) = option_env!("GIT_VERSION") {
println!("|{:^68}|", format!("Version {}", version));
}
println!("|--------------------------------------------------------------------|");
println!("| This is an *unofficial* Bitwarden implementation, DO NOT use the |");
println!("| official channels to report bugs/features, regardless of client. |");
println!("| Report URL: https://github.com/dani-garcia/bitwarden_rs/issues/new |");
println!("\\--------------------------------------------------------------------/\n");
} }
fn init_logging() -> Result<(), fern::InitError> { fn init_logging() -> Result<(), fern::InitError> {
use std::str::FromStr;
let mut logger = fern::Dispatch::new() let mut logger = fern::Dispatch::new()
.format(|out, message, record| { .format(|out, message, record| {
out.finish(format_args!( out.finish(format_args!(
@@ -93,31 +80,30 @@ fn init_logging() -> Result<(), fern::InitError> {
message message
)) ))
}) })
.level(log::LevelFilter::Debug) .level(log::LevelFilter::from_str(&CONFIG.log_level()).expect("Valid log level"))
.level_for("hyper", log::LevelFilter::Warn) // Hide unknown certificate errors if using self-signed
.level_for("rustls", log::LevelFilter::Warn) .level_for("rustls::session", log::LevelFilter::Off)
.level_for("handlebars", log::LevelFilter::Warn) // Hide failed to close stream messages
.level_for("ws", log::LevelFilter::Info) .level_for("hyper::server", log::LevelFilter::Warn)
.level_for("multipart", log::LevelFilter::Info)
.level_for("html5ever", log::LevelFilter::Info)
.chain(std::io::stdout()); .chain(std::io::stdout());
if let Some(log_file) = CONFIG.log_file() { if let Some(log_file) = CONFIG.log_file() {
logger = logger.chain(fern::log_file(log_file)?); logger = logger.chain(fern::log_file(log_file)?);
} }
#[cfg(not(windows))]
{
if cfg!(feature = "enable_syslog") || CONFIG.use_syslog() {
logger = chain_syslog(logger); logger = chain_syslog(logger);
}
}
logger.apply()?; logger.apply()?;
Ok(()) Ok(())
} }
#[cfg(not(feature = "enable_syslog"))] #[cfg(not(windows))]
fn chain_syslog(logger: fern::Dispatch) -> fern::Dispatch {
logger
}
#[cfg(feature = "enable_syslog")]
fn chain_syslog(logger: fern::Dispatch) -> fern::Dispatch { fn chain_syslog(logger: fern::Dispatch) -> fern::Dispatch {
let syslog_fmt = syslog::Formatter3164 { let syslog_fmt = syslog::Formatter3164 {
facility: syslog::Facility::LOG_USER, facility: syslog::Facility::LOG_USER,
@@ -136,6 +122,7 @@ fn chain_syslog(logger: fern::Dispatch) -> fern::Dispatch {
} }
fn check_db() { fn check_db() {
if cfg!(feature = "sqlite") {
let url = CONFIG.database_url(); let url = CONFIG.database_url();
let path = Path::new(&url); let path = Path::new(&url);
@@ -148,61 +135,52 @@ fn check_db() {
} }
// Turn on WAL in SQLite // Turn on WAL in SQLite
if CONFIG.enable_db_wal() {
use diesel::RunQueryDsl; use diesel::RunQueryDsl;
let connection = db::get_connection().expect("Can't conect to DB"); let connection = db::get_connection().expect("Can't conect to DB");
diesel::sql_query("PRAGMA journal_mode=wal") diesel::sql_query("PRAGMA journal_mode=wal")
.execute(&connection) .execute(&connection)
.expect("Failed to turn on WAL"); .expect("Failed to turn on WAL");
} }
}
db::get_connection().expect("Can't connect to DB");
}
fn check_rsa_keys() { fn check_rsa_keys() {
// If the RSA keys don't exist, try to create them // If the RSA keys don't exist, try to create them
if !util::file_exists(&CONFIG.private_rsa_key()) || !util::file_exists(&CONFIG.public_rsa_key()) { if !util::file_exists(&CONFIG.private_rsa_key()) || !util::file_exists(&CONFIG.public_rsa_key()) {
info!("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(|_| { Command::new("openssl").arg("version").status().unwrap_or_else(|_| {
info!("Can't create keys because OpenSSL is not available, make sure it's installed and available on the PATH"); info!("Can't create keys because OpenSSL is not available, make sure it's installed and available on the PATH");
exit(1); exit(1);
}); });
info!("OpenSSL detected, creating keys..."); info!("OpenSSL detected, creating keys...");
let key = CONFIG.rsa_key_filename();
let pem = format!("{}.pem", key);
let priv_der = format!("{}.der", key);
let pub_der = format!("{}.pub.der", key);
let mut success = Command::new("openssl") let mut success = Command::new("openssl")
.arg("genrsa") .args(&["genrsa", "-out", &pem])
.arg("-out") .status()
.arg(&CONFIG.private_rsa_key_pem())
.output()
.expect("Failed to create private pem file") .expect("Failed to create private pem file")
.status
.success(); .success();
success &= Command::new("openssl") success &= Command::new("openssl")
.arg("rsa") .args(&["rsa", "-in", &pem, "-outform", "DER", "-out", &priv_der])
.arg("-in") .status()
.arg(&CONFIG.private_rsa_key_pem())
.arg("-outform")
.arg("DER")
.arg("-out")
.arg(&CONFIG.private_rsa_key())
.output()
.expect("Failed to create private der file") .expect("Failed to create private der file")
.status
.success(); .success();
success &= Command::new("openssl") success &= Command::new("openssl")
.arg("rsa") .args(&["rsa", "-in", &priv_der, "-inform", "DER"])
.arg("-in") .args(&["-RSAPublicKey_out", "-outform", "DER", "-out", &pub_der])
.arg(&CONFIG.private_rsa_key()) .status()
.arg("-inform")
.arg("DER")
.arg("-RSAPublicKey_out")
.arg("-outform")
.arg("DER")
.arg("-out")
.arg(&CONFIG.public_rsa_key())
.output()
.expect("Failed to create public der file") .expect("Failed to create public der file")
.status
.success(); .success();
if success { if success {
@@ -227,12 +205,54 @@ fn check_web_vault() {
} }
} }
fn unofficial_warning() -> AdHoc { // Embed the migrations from the migrations folder into the application
AdHoc::on_launch("Unofficial Warning", |_| { // This way, the program automatically migrates the database to the latest version
warn!("/--------------------------------------------------------------------\\"); // https://docs.rs/diesel_migrations/*/diesel_migrations/macro.embed_migrations.html
warn!("| This is an *unofficial* Bitwarden implementation, DO NOT use the |"); #[allow(unused_imports)]
warn!("| official channels to report bugs/features, regardless of client. |"); mod migrations {
warn!("| Report URL: https://github.com/dani-garcia/bitwarden_rs/issues/new |");
warn!("\\--------------------------------------------------------------------/"); #[cfg(feature = "sqlite")]
}) embed_migrations!("migrations/sqlite");
#[cfg(feature = "mysql")]
embed_migrations!("migrations/mysql");
pub fn run_migrations() {
// Make sure the database is up to date (create if it doesn't exist, or run the migrations)
let connection = crate::db::get_connection().expect("Can't connect to DB");
use std::io::stdout;
embedded_migrations::run_with_output(&connection, &mut stdout()).expect("Can't run migrations");
}
}
fn launch_rocket() {
// Create Rocket object, this stores current log level and sets it's own
let rocket = rocket::ignite();
// If we aren't logging the mounts, we force the logging level down
if !CONFIG.log_mounts() {
log::set_max_level(log::LevelFilter::Warn);
}
let rocket = rocket
.mount("/", api::web_routes())
.mount("/api", api::core_routes())
.mount("/admin", api::admin_routes())
.mount("/identity", api::identity_routes())
.mount("/icons", api::icons_routes())
.mount("/notifications", api::notifications_routes());
// Force the level up for the fairings, managed state and lauch
if !CONFIG.log_mounts() {
log::set_max_level(log::LevelFilter::max());
}
let rocket = rocket
.manage(db::init_pool())
.manage(api::start_notification_server())
.attach(util::AppHeaders());
// Launch and print error if there is one
// The launch will restore the original logging level
error!("Launch error {:#?}", rocket.launch());
} }

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="450" height="450" version="1">
<circle cx="225" cy="225" r="225" fill="#C33"/>
<g fill="#FFF" stroke="#FFF" stroke-width="70">
<path d="M107 110l236 237M107 347l236-237"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 241 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

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