commit dc7a41e4729fe3ff60e77fb5b0a00b0d574ab3a2 parent 9a7eeed9c528e9b33c01d1ab77d94c4899657a6f Author: Zack Newman <zack@philomathiclife.com> Date: Mon, 25 Dec 2023 22:09:45 -0700 conform to webauthn spec. update webauthn lib Diffstat:
108 files changed, 670 insertions(+), 1028 deletions(-)
diff --git a/.cargo/config.toml b/.cargo/config.toml @@ -0,0 +1,2 @@ +[net] +git-fetch-with-cli = true diff --git a/Cargo.toml b/Cargo.toml @@ -22,12 +22,11 @@ chrono = { version = "0.4.31", default-features = false, features = ["serde"] } dashmap = { version = "5.5.3", default-features = false } data-encoding = { version = "2.5.0", default-features = false } diesel = { version = "2.1.4", default-features = false, features = ["32-column-tables", "chrono", "r2d2", "sqlite"] } -diesel_migrations = { version = "2.1.0", default-features = false } jsonwebtoken = { version = "9.2.0", default-features = false, features = ["use_pem"] } libsqlite3-sys = { version = "0.27.0", default-features = false, features = ["bundled"] } num-derive = { version = "0.4.1", default-features = false } num-traits = { version = "0.2.17", default-features = false } -openssl = { version = "0.10.61", default-features = false } +openssl = { version = "0.10.62", default-features = false } paste = { version = "1.0.14", default-features = false } rand = { version = "0.8.5", default-features = false, features = ["small_rng"] } regex = { version = "1.10.2", default-features = false, features = ["std"] } @@ -38,13 +37,16 @@ rocket_ws = { version = "0.1.0", default-features = false, features = ["tokio-tu semver = { version = "1.0.20", default-features = false } serde = { version = "1.0.193", default-features = false } serde_json = { version = "1.0.108", default-features = false } -tokio = { version = "1.35.0", default-features = false } +tokio = { version = "1.35.1", default-features = false } tokio-tungstenite = { version = "0.20.1", default-features = false } toml = { version = "0.8.8", default-features = false, features = ["parse"] } totp-lite = { version = "2.0.1", default-features = false } url = { version = "2.5.0", default-features = false } uuid = { version = "1.6.1", default-features = false, features = ["v4"] } -webauthn-rs = { version = "0.3.2", default-features = false, features = ["core"] } +webauthn-rs = { version = "0.4.8", default-features = false, features = ["danger-allow-state-serialisation", "danger-user-presence-only-security-keys"] } + +[patch.crates-io] +webauthn-rs-core = { git = "https://git.philomathiclife.com/repos/webauthn-rs-core", tag = "v0.4.9" } [profile.release] lto = true diff --git a/migrations/sqlite/2018-01-14-171611_create_tables/down.sql b/migrations/sqlite/2018-01-14-171611_create_tables/down.sql @@ -1,9 +0,0 @@ -DROP TABLE users; - -DROP TABLE devices; - -DROP TABLE ciphers; - -DROP TABLE attachments; - -DROP TABLE folders; -\ No newline at end of file diff --git a/migrations/sqlite/2018-01-14-171611_create_tables/up.sql b/migrations/sqlite/2018-01-14-171611_create_tables/up.sql @@ -1,62 +0,0 @@ -CREATE TABLE users ( - uuid TEXT NOT NULL PRIMARY KEY, - created_at DATETIME NOT NULL, - updated_at DATETIME NOT NULL, - email TEXT 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 TEXT NOT NULL PRIMARY KEY, - created_at DATETIME NOT NULL, - updated_at DATETIME NOT NULL, - user_uuid TEXT 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 TEXT NOT NULL PRIMARY KEY, - created_at DATETIME NOT NULL, - updated_at DATETIME NOT NULL, - user_uuid TEXT NOT NULL REFERENCES users (uuid), - folder_uuid TEXT REFERENCES folders (uuid), - organization_uuid TEXT, - type INTEGER NOT NULL, - name TEXT NOT NULL, - notes TEXT, - fields TEXT, - data TEXT NOT NULL, - favorite BOOLEAN NOT NULL -); - -CREATE TABLE attachments ( - id TEXT NOT NULL PRIMARY KEY, - cipher_uuid TEXT NOT NULL REFERENCES ciphers (uuid), - file_name TEXT NOT NULL, - file_size INTEGER NOT NULL - -); - -CREATE TABLE folders ( - uuid TEXT NOT NULL PRIMARY KEY, - created_at DATETIME NOT NULL, - updated_at DATETIME NOT NULL, - user_uuid TEXT NOT NULL REFERENCES users (uuid), - name TEXT NOT NULL -); - -\ No newline at end of file diff --git a/migrations/sqlite/2018-02-17-205753_create_collections_and_orgs/down.sql b/migrations/sqlite/2018-02-17-205753_create_collections_and_orgs/down.sql @@ -1,8 +0,0 @@ -DROP TABLE collections; - -DROP TABLE organizations; - - -DROP TABLE users_collections; - -DROP TABLE users_organizations; diff --git a/migrations/sqlite/2018-02-17-205753_create_collections_and_orgs/up.sql b/migrations/sqlite/2018-02-17-205753_create_collections_and_orgs/up.sql @@ -1,31 +0,0 @@ -CREATE TABLE collections ( - uuid TEXT NOT NULL PRIMARY KEY, - org_uuid TEXT NOT NULL REFERENCES organizations (uuid), - name TEXT NOT NULL -); - -CREATE TABLE organizations ( - uuid TEXT NOT NULL PRIMARY KEY, - name TEXT NOT NULL, - billing_email TEXT NOT NULL -); - - -CREATE TABLE users_collections ( - user_uuid TEXT NOT NULL REFERENCES users (uuid), - collection_uuid TEXT NOT NULL REFERENCES collections (uuid), - PRIMARY KEY (user_uuid, collection_uuid) -); - -CREATE TABLE users_organizations ( - uuid TEXT NOT NULL PRIMARY KEY, - user_uuid TEXT NOT NULL REFERENCES users (uuid), - org_uuid TEXT 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) -); diff --git a/migrations/sqlite/2018-04-27-155151_create_users_ciphers/down.sql b/migrations/sqlite/2018-04-27-155151_create_users_ciphers/down.sql diff --git a/migrations/sqlite/2018-04-27-155151_create_users_ciphers/up.sql b/migrations/sqlite/2018-04-27-155151_create_users_ciphers/up.sql @@ -1,34 +0,0 @@ -ALTER TABLE ciphers RENAME TO oldCiphers; - -CREATE TABLE ciphers ( - uuid TEXT NOT NULL PRIMARY KEY, - created_at DATETIME NOT NULL, - updated_at DATETIME NOT NULL, - user_uuid TEXT REFERENCES users (uuid), -- Make this optional - organization_uuid TEXT 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 TEXT NOT NULL REFERENCES ciphers (uuid), - folder_uuid TEXT 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 diff --git a/migrations/sqlite/2018-05-08-161616_create_collection_cipher_map/down.sql b/migrations/sqlite/2018-05-08-161616_create_collection_cipher_map/down.sql @@ -1 +0,0 @@ -DROP TABLE ciphers_collections; -\ No newline at end of file diff --git a/migrations/sqlite/2018-05-08-161616_create_collection_cipher_map/up.sql b/migrations/sqlite/2018-05-08-161616_create_collection_cipher_map/up.sql @@ -1,5 +0,0 @@ -CREATE TABLE ciphers_collections ( - cipher_uuid TEXT NOT NULL REFERENCES ciphers (uuid), - collection_uuid TEXT NOT NULL REFERENCES collections (uuid), - PRIMARY KEY (cipher_uuid, collection_uuid) -); -\ No newline at end of file diff --git a/migrations/sqlite/2018-05-25-232323_update_attachments_reference/down.sql b/migrations/sqlite/2018-05-25-232323_update_attachments_reference/down.sql diff --git a/migrations/sqlite/2018-05-25-232323_update_attachments_reference/up.sql b/migrations/sqlite/2018-05-25-232323_update_attachments_reference/up.sql @@ -1,14 +0,0 @@ -ALTER TABLE attachments RENAME TO oldAttachments; - -CREATE TABLE attachments ( - id TEXT NOT NULL PRIMARY KEY, - cipher_uuid TEXT 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; -\ No newline at end of file diff --git a/migrations/sqlite/2018-06-01-112529_update_devices_twofactor_remember/down.sql b/migrations/sqlite/2018-06-01-112529_update_devices_twofactor_remember/down.sql @@ -1 +0,0 @@ --- This file should undo anything in `up.sql` -\ No newline at end of file diff --git a/migrations/sqlite/2018-06-01-112529_update_devices_twofactor_remember/up.sql b/migrations/sqlite/2018-06-01-112529_update_devices_twofactor_remember/up.sql @@ -1,3 +0,0 @@ -ALTER TABLE devices - ADD COLUMN - twofactor_remember TEXT; -\ No newline at end of file diff --git a/migrations/sqlite/2018-07-11-181453_create_u2f_twofactor/down.sql b/migrations/sqlite/2018-07-11-181453_create_u2f_twofactor/down.sql @@ -1,8 +0,0 @@ -UPDATE users -SET totp_secret = ( - SELECT twofactor.data FROM twofactor - WHERE twofactor.type = 0 - AND twofactor.user_uuid = users.uuid -); - -DROP TABLE twofactor; -\ No newline at end of file diff --git a/migrations/sqlite/2018-07-11-181453_create_u2f_twofactor/up.sql b/migrations/sqlite/2018-07-11-181453_create_u2f_twofactor/up.sql @@ -1,15 +0,0 @@ -CREATE TABLE twofactor ( - uuid TEXT NOT NULL PRIMARY KEY, - user_uuid TEXT NOT NULL REFERENCES users (uuid), - type INTEGER NOT NULL, - enabled BOOLEAN NOT NULL, - data TEXT NOT NULL, - - UNIQUE (user_uuid, type) -); - - -INSERT INTO twofactor (uuid, user_uuid, type, enabled, data) -SELECT lower(hex(randomblob(16))) , uuid, 0, 1, u.totp_secret FROM users u where u.totp_secret IS NOT NULL; - -UPDATE users SET totp_secret = NULL; -- Instead of recreating the table, just leave the columns empty -\ No newline at end of file diff --git a/migrations/sqlite/2018-08-27-172114_update_ciphers/down.sql b/migrations/sqlite/2018-08-27-172114_update_ciphers/down.sql diff --git a/migrations/sqlite/2018-08-27-172114_update_ciphers/up.sql b/migrations/sqlite/2018-08-27-172114_update_ciphers/up.sql @@ -1,3 +0,0 @@ -ALTER TABLE ciphers - ADD COLUMN - password_history TEXT; -\ No newline at end of file diff --git a/migrations/sqlite/2018-09-10-111213_add_invites/down.sql b/migrations/sqlite/2018-09-10-111213_add_invites/down.sql @@ -1 +0,0 @@ -DROP TABLE invitations; -\ No newline at end of file diff --git a/migrations/sqlite/2018-09-10-111213_add_invites/up.sql b/migrations/sqlite/2018-09-10-111213_add_invites/up.sql @@ -1,3 +0,0 @@ -CREATE TABLE invitations ( - email TEXT NOT NULL PRIMARY KEY -); -\ No newline at end of file diff --git a/migrations/sqlite/2018-09-19-144557_add_kdf_columns/down.sql b/migrations/sqlite/2018-09-19-144557_add_kdf_columns/down.sql diff --git a/migrations/sqlite/2018-09-19-144557_add_kdf_columns/up.sql b/migrations/sqlite/2018-09-19-144557_add_kdf_columns/up.sql @@ -1,7 +0,0 @@ -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 100000; diff --git a/migrations/sqlite/2018-11-27-152651_add_att_key_columns/down.sql b/migrations/sqlite/2018-11-27-152651_add_att_key_columns/down.sql diff --git a/migrations/sqlite/2018-11-27-152651_add_att_key_columns/up.sql b/migrations/sqlite/2018-11-27-152651_add_att_key_columns/up.sql @@ -1,3 +0,0 @@ -ALTER TABLE attachments - ADD COLUMN - key TEXT; -\ No newline at end of file diff --git a/migrations/sqlite/2019-05-26-216651_rename_key_and_type_columns/down.sql b/migrations/sqlite/2019-05-26-216651_rename_key_and_type_columns/down.sql @@ -1,7 +0,0 @@ -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; -\ No newline at end of file diff --git a/migrations/sqlite/2019-05-26-216651_rename_key_and_type_columns/up.sql b/migrations/sqlite/2019-05-26-216651_rename_key_and_type_columns/up.sql @@ -1,7 +0,0 @@ -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; -\ No newline at end of file diff --git a/migrations/sqlite/2019-10-10-083032_add_column_to_twofactor/down.sql b/migrations/sqlite/2019-10-10-083032_add_column_to_twofactor/down.sql diff --git a/migrations/sqlite/2019-10-10-083032_add_column_to_twofactor/up.sql b/migrations/sqlite/2019-10-10-083032_add_column_to_twofactor/up.sql @@ -1 +0,0 @@ -ALTER TABLE twofactor ADD COLUMN last_used INTEGER NOT NULL DEFAULT 0; diff --git a/migrations/sqlite/2019-11-17-011009_add_email_verification/down.sql b/migrations/sqlite/2019-11-17-011009_add_email_verification/down.sql @@ -1 +0,0 @@ - diff --git a/migrations/sqlite/2019-11-17-011009_add_email_verification/up.sql b/migrations/sqlite/2019-11-17-011009_add_email_verification/up.sql @@ -1,5 +0,0 @@ -ALTER TABLE users ADD COLUMN verified_at DATETIME DEFAULT NULL; -ALTER TABLE users ADD COLUMN last_verifying_at DATETIME DEFAULT NULL; -ALTER TABLE users ADD COLUMN login_verify_count INTEGER NOT NULL DEFAULT 0; -ALTER TABLE users ADD COLUMN email_new TEXT DEFAULT NULL; -ALTER TABLE users ADD COLUMN email_new_token TEXT DEFAULT NULL; diff --git a/migrations/sqlite/2020-03-13-205045_add_policy_table/down.sql b/migrations/sqlite/2020-03-13-205045_add_policy_table/down.sql @@ -1 +0,0 @@ -DROP TABLE org_policies; diff --git a/migrations/sqlite/2020-03-13-205045_add_policy_table/up.sql b/migrations/sqlite/2020-03-13-205045_add_policy_table/up.sql @@ -1,9 +0,0 @@ -CREATE TABLE org_policies ( - uuid TEXT NOT NULL PRIMARY KEY, - org_uuid TEXT NOT NULL REFERENCES organizations (uuid), - atype INTEGER NOT NULL, - enabled BOOLEAN NOT NULL, - data TEXT NOT NULL, - - UNIQUE (org_uuid, atype) -); diff --git a/migrations/sqlite/2020-04-09-235005_add_cipher_delete_date/down.sql b/migrations/sqlite/2020-04-09-235005_add_cipher_delete_date/down.sql @@ -1 +0,0 @@ - diff --git a/migrations/sqlite/2020-04-09-235005_add_cipher_delete_date/up.sql b/migrations/sqlite/2020-04-09-235005_add_cipher_delete_date/up.sql @@ -1,3 +0,0 @@ -ALTER TABLE ciphers - ADD COLUMN - deleted_at DATETIME; diff --git a/migrations/sqlite/2020-07-01-214531_add_hide_passwords/down.sql b/migrations/sqlite/2020-07-01-214531_add_hide_passwords/down.sql diff --git a/migrations/sqlite/2020-07-01-214531_add_hide_passwords/up.sql b/migrations/sqlite/2020-07-01-214531_add_hide_passwords/up.sql @@ -1,2 +0,0 @@ -ALTER TABLE users_collections -ADD COLUMN hide_passwords BOOLEAN NOT NULL DEFAULT 0; -- FALSE diff --git a/migrations/sqlite/2020-08-02-025025_add_favorites_table/down.sql b/migrations/sqlite/2020-08-02-025025_add_favorites_table/down.sql @@ -1,13 +0,0 @@ -ALTER TABLE ciphers -ADD COLUMN favorite BOOLEAN NOT NULL DEFAULT 0; -- FALSE - --- Transfer favorite status for user-owned ciphers. -UPDATE ciphers -SET favorite = 1 -WHERE EXISTS ( - SELECT * FROM favorites - WHERE favorites.user_uuid = ciphers.user_uuid - AND favorites.cipher_uuid = ciphers.uuid -); - -DROP TABLE favorites; diff --git a/migrations/sqlite/2020-08-02-025025_add_favorites_table/up.sql b/migrations/sqlite/2020-08-02-025025_add_favorites_table/up.sql @@ -1,71 +0,0 @@ -CREATE TABLE favorites ( - user_uuid TEXT NOT NULL REFERENCES users(uuid), - cipher_uuid TEXT NOT NULL REFERENCES ciphers(uuid), - - PRIMARY KEY (user_uuid, cipher_uuid) -); - --- Transfer favorite status for user-owned ciphers. -INSERT INTO favorites(user_uuid, cipher_uuid) -SELECT user_uuid, uuid -FROM ciphers -WHERE favorite = 1 - AND user_uuid IS NOT NULL; - --- Drop the `favorite` column from the `ciphers` table, using the 12-step --- procedure from <https://www.sqlite.org/lang_altertable.html#altertabrename>. --- Note that some steps aren't applicable and are omitted. - --- 1. If foreign key constraints are enabled, disable them using PRAGMA foreign_keys=OFF. --- --- Diesel runs each migration in its own transaction. `PRAGMA foreign_keys` --- is a no-op within a transaction, so this step must be done outside of this --- file, before starting the Diesel migrations. - --- 2. Start a transaction. --- --- Diesel already runs each migration in its own transaction. - --- 4. Use CREATE TABLE to construct a new table "new_X" that is in the --- desired revised format of table X. Make sure that the name "new_X" does --- not collide with any existing table name, of course. - -CREATE TABLE new_ciphers( - uuid TEXT NOT NULL PRIMARY KEY, - created_at DATETIME NOT NULL, - updated_at DATETIME NOT NULL, - user_uuid TEXT REFERENCES users(uuid), - organization_uuid TEXT REFERENCES organizations(uuid), - atype INTEGER NOT NULL, - name TEXT NOT NULL, - notes TEXT, - fields TEXT, - data TEXT NOT NULL, - password_history TEXT, - deleted_at DATETIME -); - --- 5. Transfer content from X into new_X using a statement like: --- INSERT INTO new_X SELECT ... FROM X. - -INSERT INTO new_ciphers(uuid, created_at, updated_at, user_uuid, organization_uuid, atype, - name, notes, fields, data, password_history, deleted_at) -SELECT uuid, created_at, updated_at, user_uuid, organization_uuid, atype, - name, notes, fields, data, password_history, deleted_at -FROM ciphers; - --- 6. Drop the old table X: DROP TABLE X. - -DROP TABLE ciphers; - --- 7. Change the name of new_X to X using: ALTER TABLE new_X RENAME TO X. - -ALTER TABLE new_ciphers RENAME TO ciphers; - --- 11. Commit the transaction started in step 2. - --- 12. If foreign keys constraints were originally enabled, reenable them now. --- --- `PRAGMA foreign_keys` is scoped to a database connection, and Diesel --- migrations are run in a separate database connection that is closed once --- the migrations finish. diff --git a/migrations/sqlite/2020-11-30-224000_add_user_enabled/down.sql b/migrations/sqlite/2020-11-30-224000_add_user_enabled/down.sql diff --git a/migrations/sqlite/2020-11-30-224000_add_user_enabled/up.sql b/migrations/sqlite/2020-11-30-224000_add_user_enabled/up.sql @@ -1 +0,0 @@ -ALTER TABLE users ADD COLUMN enabled BOOLEAN NOT NULL DEFAULT 1; diff --git a/migrations/sqlite/2020-12-09-173101_add_stamp_exception/down.sql b/migrations/sqlite/2020-12-09-173101_add_stamp_exception/down.sql diff --git a/migrations/sqlite/2020-12-09-173101_add_stamp_exception/up.sql b/migrations/sqlite/2020-12-09-173101_add_stamp_exception/up.sql @@ -1 +0,0 @@ -ALTER TABLE users ADD COLUMN stamp_exception TEXT DEFAULT NULL; -\ No newline at end of file diff --git a/migrations/sqlite/2021-03-11-190243_add_sends/down.sql b/migrations/sqlite/2021-03-11-190243_add_sends/down.sql @@ -1 +0,0 @@ -DROP TABLE sends; diff --git a/migrations/sqlite/2021-03-11-190243_add_sends/up.sql b/migrations/sqlite/2021-03-11-190243_add_sends/up.sql @@ -1,25 +0,0 @@ -CREATE TABLE sends ( - uuid TEXT NOT NULL PRIMARY KEY, - user_uuid TEXT REFERENCES users (uuid), - organization_uuid TEXT REFERENCES organizations (uuid), - - name TEXT NOT NULL, - notes TEXT, - - atype INTEGER NOT NULL, - data TEXT NOT NULL, - key TEXT NOT NULL, - password_hash BLOB, - password_salt BLOB, - password_iter INTEGER, - - max_access_count INTEGER, - access_count INTEGER NOT NULL, - - creation_date DATETIME NOT NULL, - revision_date DATETIME NOT NULL, - expiration_date DATETIME, - deletion_date DATETIME NOT NULL, - - disabled BOOLEAN NOT NULL -); -\ No newline at end of file diff --git a/migrations/sqlite/2021-03-15-163412_rename_send_key/down.sql b/migrations/sqlite/2021-03-15-163412_rename_send_key/down.sql diff --git a/migrations/sqlite/2021-03-15-163412_rename_send_key/up.sql b/migrations/sqlite/2021-03-15-163412_rename_send_key/up.sql @@ -1 +0,0 @@ -ALTER TABLE sends RENAME COLUMN key TO akey; diff --git a/migrations/sqlite/2021-04-30-233251_add_reprompt/down.sql b/migrations/sqlite/2021-04-30-233251_add_reprompt/down.sql diff --git a/migrations/sqlite/2021-04-30-233251_add_reprompt/up.sql b/migrations/sqlite/2021-04-30-233251_add_reprompt/up.sql @@ -1,2 +0,0 @@ -ALTER TABLE ciphers -ADD COLUMN reprompt INTEGER; diff --git a/migrations/sqlite/2021-05-11-205202_add_hide_email/down.sql b/migrations/sqlite/2021-05-11-205202_add_hide_email/down.sql diff --git a/migrations/sqlite/2021-05-11-205202_add_hide_email/up.sql b/migrations/sqlite/2021-05-11-205202_add_hide_email/up.sql @@ -1,2 +0,0 @@ -ALTER TABLE sends -ADD COLUMN hide_email BOOLEAN; diff --git a/migrations/sqlite/2021-07-01-203140_add_password_reset_keys/down.sql b/migrations/sqlite/2021-07-01-203140_add_password_reset_keys/down.sql diff --git a/migrations/sqlite/2021-07-01-203140_add_password_reset_keys/up.sql b/migrations/sqlite/2021-07-01-203140_add_password_reset_keys/up.sql @@ -1,5 +0,0 @@ -ALTER TABLE organizations - ADD COLUMN private_key TEXT; - -ALTER TABLE organizations - ADD COLUMN public_key TEXT; diff --git a/migrations/sqlite/2021-08-30-193501_create_emergency_access/down.sql b/migrations/sqlite/2021-08-30-193501_create_emergency_access/down.sql @@ -1 +0,0 @@ -DROP TABLE emergency_access; diff --git a/migrations/sqlite/2021-08-30-193501_create_emergency_access/up.sql b/migrations/sqlite/2021-08-30-193501_create_emergency_access/up.sql @@ -1,14 +0,0 @@ -CREATE TABLE emergency_access ( - uuid TEXT NOT NULL PRIMARY KEY, - grantor_uuid TEXT REFERENCES users (uuid), - grantee_uuid TEXT REFERENCES users (uuid), - email TEXT, - key_encrypted TEXT, - atype INTEGER NOT NULL, - status INTEGER NOT NULL, - wait_time_days INTEGER NOT NULL, - recovery_initiated_at DATETIME, - last_notification_at DATETIME, - updated_at DATETIME NOT NULL, - created_at DATETIME NOT NULL -); diff --git a/migrations/sqlite/2021-10-24-164321_add_2fa_incomplete/down.sql b/migrations/sqlite/2021-10-24-164321_add_2fa_incomplete/down.sql @@ -1 +0,0 @@ -DROP TABLE twofactor_incomplete; diff --git a/migrations/sqlite/2021-10-24-164321_add_2fa_incomplete/up.sql b/migrations/sqlite/2021-10-24-164321_add_2fa_incomplete/up.sql @@ -1,9 +0,0 @@ -CREATE TABLE twofactor_incomplete ( - user_uuid TEXT NOT NULL REFERENCES users(uuid), - device_uuid TEXT NOT NULL, - device_name TEXT NOT NULL, - login_time DATETIME NOT NULL, - ip_address TEXT NOT NULL, - - PRIMARY KEY (user_uuid, device_uuid) -); diff --git a/migrations/sqlite/2022-01-17-234911_add_api_key/down.sql b/migrations/sqlite/2022-01-17-234911_add_api_key/down.sql diff --git a/migrations/sqlite/2022-01-17-234911_add_api_key/up.sql b/migrations/sqlite/2022-01-17-234911_add_api_key/up.sql @@ -1,2 +0,0 @@ -ALTER TABLE users -ADD COLUMN api_key TEXT; diff --git a/migrations/sqlite/2022-03-02-210038_update_devices_primary_key/down.sql b/migrations/sqlite/2022-03-02-210038_update_devices_primary_key/down.sql diff --git a/migrations/sqlite/2022-03-02-210038_update_devices_primary_key/up.sql b/migrations/sqlite/2022-03-02-210038_update_devices_primary_key/up.sql @@ -1,23 +0,0 @@ --- Create new devices table with primary keys on both uuid and user_uuid -CREATE TABLE devices_new ( - uuid TEXT NOT NULL, - created_at DATETIME NOT NULL, - updated_at DATETIME NOT NULL, - user_uuid TEXT NOT NULL, - name TEXT NOT NULL, - atype INTEGER NOT NULL, - push_token TEXT, - refresh_token TEXT NOT NULL, - twofactor_remember TEXT, - PRIMARY KEY(uuid, user_uuid), - FOREIGN KEY(user_uuid) REFERENCES users(uuid) -); - --- Transfer current data to new table -INSERT INTO devices_new SELECT * FROM devices; - --- Drop the old table -DROP TABLE devices; - --- Rename the new table to the original name -ALTER TABLE devices_new RENAME TO devices; diff --git a/migrations/sqlite/2022-07-27-110000_add_group_support/down.sql b/migrations/sqlite/2022-07-27-110000_add_group_support/down.sql @@ -1,3 +0,0 @@ -DROP TABLE groups; -DROP TABLE groups_users; -DROP TABLE collections_groups; -\ No newline at end of file diff --git a/migrations/sqlite/2022-07-27-110000_add_group_support/up.sql b/migrations/sqlite/2022-07-27-110000_add_group_support/up.sql @@ -1,23 +0,0 @@ -CREATE TABLE groups ( - uuid TEXT NOT NULL PRIMARY KEY, - organizations_uuid TEXT NOT NULL REFERENCES organizations (uuid), - name TEXT NOT NULL, - access_all BOOLEAN NOT NULL, - external_id TEXT NULL, - creation_date TIMESTAMP NOT NULL, - revision_date TIMESTAMP NOT NULL -); - -CREATE TABLE groups_users ( - groups_uuid TEXT NOT NULL REFERENCES groups (uuid), - users_organizations_uuid TEXT NOT NULL REFERENCES users_organizations (uuid), - UNIQUE (groups_uuid, users_organizations_uuid) -); - -CREATE TABLE collections_groups ( - collections_uuid TEXT NOT NULL REFERENCES collections (uuid), - groups_uuid TEXT NOT NULL REFERENCES groups (uuid), - read_only BOOLEAN NOT NULL, - hide_passwords BOOLEAN NOT NULL, - UNIQUE (collections_uuid, groups_uuid) -); -\ No newline at end of file diff --git a/migrations/sqlite/2022-10-18-170602_add_events/down.sql b/migrations/sqlite/2022-10-18-170602_add_events/down.sql @@ -1 +0,0 @@ -DROP TABLE event; diff --git a/migrations/sqlite/2022-10-18-170602_add_events/up.sql b/migrations/sqlite/2022-10-18-170602_add_events/up.sql @@ -1,19 +0,0 @@ -CREATE TABLE event ( - uuid TEXT NOT NULL PRIMARY KEY, - event_type INTEGER NOT NULL, - user_uuid TEXT, - org_uuid TEXT, - cipher_uuid TEXT, - collection_uuid TEXT, - group_uuid TEXT, - org_user_uuid TEXT, - act_user_uuid TEXT, - device_type INTEGER, - ip_address TEXT, - event_date DATETIME NOT NULL, - policy_uuid TEXT, - provider_uuid TEXT, - provider_user_uuid TEXT, - provider_org_uuid TEXT, - UNIQUE (uuid) -); diff --git a/migrations/sqlite/2023-01-06-151600_add_reset_password_support/down.sql b/migrations/sqlite/2023-01-06-151600_add_reset_password_support/down.sql diff --git a/migrations/sqlite/2023-01-06-151600_add_reset_password_support/up.sql b/migrations/sqlite/2023-01-06-151600_add_reset_password_support/up.sql @@ -1,2 +0,0 @@ -ALTER TABLE users_organizations -ADD COLUMN reset_password_key TEXT; diff --git a/migrations/sqlite/2023-01-11-205851_add_avatar_color/down.sql b/migrations/sqlite/2023-01-11-205851_add_avatar_color/down.sql diff --git a/migrations/sqlite/2023-01-11-205851_add_avatar_color/up.sql b/migrations/sqlite/2023-01-11-205851_add_avatar_color/up.sql @@ -1,2 +0,0 @@ -ALTER TABLE users -ADD COLUMN avatar_color TEXT; diff --git a/migrations/sqlite/2023-01-31-222222_add_argon2/down.sql b/migrations/sqlite/2023-01-31-222222_add_argon2/down.sql diff --git a/migrations/sqlite/2023-01-31-222222_add_argon2/up.sql b/migrations/sqlite/2023-01-31-222222_add_argon2/up.sql @@ -1,7 +0,0 @@ -ALTER TABLE users - ADD COLUMN - client_kdf_memory INTEGER DEFAULT NULL; - -ALTER TABLE users - ADD COLUMN - client_kdf_parallelism INTEGER DEFAULT NULL; diff --git a/migrations/sqlite/2023-02-18-125735_push_uuid_table/down.sql b/migrations/sqlite/2023-02-18-125735_push_uuid_table/down.sql diff --git a/migrations/sqlite/2023-02-18-125735_push_uuid_table/up.sql b/migrations/sqlite/2023-02-18-125735_push_uuid_table/up.sql @@ -1 +0,0 @@ -ALTER TABLE devices ADD COLUMN push_uuid TEXT; -\ No newline at end of file diff --git a/migrations/sqlite/2023-06-02-200424_create_organization_api_key/down.sql b/migrations/sqlite/2023-06-02-200424_create_organization_api_key/down.sql diff --git a/migrations/sqlite/2023-06-02-200424_create_organization_api_key/up.sql b/migrations/sqlite/2023-06-02-200424_create_organization_api_key/up.sql @@ -1,11 +0,0 @@ -CREATE TABLE organization_api_key ( - uuid TEXT NOT NULL, - org_uuid TEXT NOT NULL, - atype INTEGER NOT NULL, - api_key TEXT NOT NULL, - revision_date DATETIME NOT NULL, - PRIMARY KEY(uuid, org_uuid), - FOREIGN KEY(org_uuid) REFERENCES organizations(uuid) -); - -ALTER TABLE users ADD COLUMN external_id TEXT; diff --git a/migrations/sqlite/2023-06-17-200424_create_auth_requests_table/down.sql b/migrations/sqlite/2023-06-17-200424_create_auth_requests_table/down.sql diff --git a/migrations/sqlite/2023-06-17-200424_create_auth_requests_table/up.sql b/migrations/sqlite/2023-06-17-200424_create_auth_requests_table/up.sql @@ -1,19 +0,0 @@ -CREATE TABLE auth_requests ( - uuid TEXT NOT NULL PRIMARY KEY, - user_uuid TEXT NOT NULL, - organization_uuid TEXT, - request_device_identifier TEXT NOT NULL, - device_type INTEGER NOT NULL, - request_ip TEXT NOT NULL, - response_device_id TEXT, - access_code TEXT NOT NULL, - public_key TEXT NOT NULL, - enc_key TEXT NOT NULL, - master_password_hash TEXT NOT NULL, - approved BOOLEAN, - creation_date DATETIME NOT NULL, - response_date DATETIME, - authentication_date DATETIME, - FOREIGN KEY(user_uuid) REFERENCES users(uuid), - FOREIGN KEY(organization_uuid) REFERENCES organizations(uuid) -); -\ No newline at end of file diff --git a/migrations/sqlite/2023-06-28-133700_add_collection_external_id/down.sql b/migrations/sqlite/2023-06-28-133700_add_collection_external_id/down.sql diff --git a/migrations/sqlite/2023-06-28-133700_add_collection_external_id/up.sql b/migrations/sqlite/2023-06-28-133700_add_collection_external_id/up.sql @@ -1 +0,0 @@ -ALTER TABLE collections ADD COLUMN external_id TEXT; diff --git a/migrations/sqlite/2023-09-01-170620_update_auth_request_table/down.sql b/migrations/sqlite/2023-09-01-170620_update_auth_request_table/down.sql diff --git a/migrations/sqlite/2023-09-01-170620_update_auth_request_table/up.sql b/migrations/sqlite/2023-09-01-170620_update_auth_request_table/up.sql @@ -1,29 +0,0 @@ --- Create new auth_requests table with master_password_hash as nullable column -CREATE TABLE auth_requests_new ( - uuid TEXT NOT NULL PRIMARY KEY, - user_uuid TEXT NOT NULL, - organization_uuid TEXT, - request_device_identifier TEXT NOT NULL, - device_type INTEGER NOT NULL, - request_ip TEXT NOT NULL, - response_device_id TEXT, - access_code TEXT NOT NULL, - public_key TEXT NOT NULL, - enc_key TEXT, - master_password_hash TEXT, - approved BOOLEAN, - creation_date DATETIME NOT NULL, - response_date DATETIME, - authentication_date DATETIME, - FOREIGN KEY (user_uuid) REFERENCES users (uuid), - FOREIGN KEY (organization_uuid) REFERENCES organizations (uuid) -); - --- Transfer current data to new table -INSERT INTO auth_requests_new SELECT * FROM auth_requests; - --- Drop the old table -DROP TABLE auth_requests; - --- Rename the new table to the original name -ALTER TABLE auth_requests_new RENAME TO auth_requests; diff --git a/migrations/sqlite/2023-09-02-212336_move_user_external_id/down.sql b/migrations/sqlite/2023-09-02-212336_move_user_external_id/down.sql diff --git a/migrations/sqlite/2023-09-02-212336_move_user_external_id/up.sql b/migrations/sqlite/2023-09-02-212336_move_user_external_id/up.sql @@ -1,2 +0,0 @@ --- Add the external_id to the users_organizations table -ALTER TABLE "users_organizations" ADD COLUMN "external_id" TEXT; diff --git a/migrations/sqlite/2023-10-21-221242_add_cipher_key/down.sql b/migrations/sqlite/2023-10-21-221242_add_cipher_key/down.sql diff --git a/migrations/sqlite/2023-10-21-221242_add_cipher_key/up.sql b/migrations/sqlite/2023-10-21-221242_add_cipher_key/up.sql @@ -1,2 +0,0 @@ -ALTER TABLE ciphers -ADD COLUMN "key" TEXT; diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs @@ -7,6 +7,9 @@ mod organizations; mod public; mod sends; pub mod two_factor; +use crate::config as config_file; +use crate::error::Error; +use crate::util; pub use ciphers::{CipherData, CipherSyncData, CipherSyncType}; pub fn routes() -> Vec<Route> { let mut eq_domains_routes = routes![get_eq_domains, post_eq_domains, put_eq_domains]; @@ -116,7 +119,7 @@ async fn put_eq_domains( #[allow(unused_variables)] #[get("/hibp/breach?<username>")] fn hibp_breach(username: &str) -> JsonResult { - Err(crate::error::Error::empty().with_code(404)) + Err(Error::empty().with_code(404)) } // We use DbConn here to let the alive healthcheck also verify the database connection. @@ -128,7 +131,7 @@ fn alive(_conn: DbConn) -> Json<String> { #[get("/now")] pub fn now() -> Json<String> { - Json(crate::util::format_date(&chrono::Utc::now().naive_utc())) + Json(util::format_date(&chrono::Utc::now().naive_utc())) } #[get("/version")] @@ -138,7 +141,7 @@ const fn version() -> Json<&'static str> { #[get("/config")] fn config() -> Json<Value> { - let domain = &crate::config::get_config().domain; + let domain = &config_file::get_config().domain; Json(json!({ // Note: The clients use this version to handle backwards compatibility concerns // This means they expect a version that closely matches the Bitwarden server version diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs @@ -4,7 +4,8 @@ use crate::{ EmptyResult, JsonResult, JsonUpcase, JsonUpcaseVec, JsonVec, Notify, NumberOrString, PasswordOrOtpData, UpdateType, }, - auth::{AdminHeaders, Headers, ManagerHeaders, ManagerHeadersLoose, OwnerHeaders}, + auth::{self, AdminHeaders, Headers, ManagerHeaders, ManagerHeadersLoose, OwnerHeaders}, + crypto, db::{ models::{ Cipher, Collection, CollectionCipher, CollectionUser, OrgPolicy, OrgPolicyErr, @@ -14,8 +15,9 @@ use crate::{ DbConn, }, error::Error, - util::convert_json_key_lcase_first, + util, }; +use core::convert; use num_traits::FromPrimitive; use rocket::serde::json::Json; use rocket::Route; @@ -1182,7 +1184,7 @@ async fn list_policies(org_id: &str, _headers: AdminHeaders, conn: DbConn) -> Js #[get("/organizations/<org_id>/policies/token?<token>")] async fn list_policies_token(org_id: &str, token: &str, conn: DbConn) -> JsonResult { - let invite = crate::auth::decode_invite(token)?; + let invite = auth::decode_invite(token)?; let Some(invite_org_id) = invite.org_id else { err!("Invalid token") }; @@ -1211,7 +1213,7 @@ async fn get_policy( }; let policy = (OrgPolicy::find_by_org_and_type(org_id, pol_type_enum, &conn).await).map_or_else( || OrgPolicy::new(String::from(org_id), pol_type_enum, "null".to_owned()), - core::convert::identity, + convert::identity, ); Ok(Json(policy.to_json())) } @@ -1944,12 +1946,12 @@ async fn get_org_export(org_id: &str, headers: AdminHeaders, conn: DbConn) -> Js // Backwards compatible pre v2023.1.0 response Json(json!({ "collections": { - "data": convert_json_key_lcase_first(_get_org_collections(org_id, &conn).await), + "data": util::convert_json_key_lcase_first(_get_org_collections(org_id, &conn).await), "object": "list", "continuationToken": null, }, "ciphers": { - "data": convert_json_key_lcase_first(_get_org_details(org_id, &headers.user.uuid, &conn).await), + "data": util::convert_json_key_lcase_first(_get_org_details(org_id, &headers.user.uuid, &conn).await), "object": "list", "continuationToken": null, } @@ -1957,8 +1959,8 @@ async fn get_org_export(org_id: &str, headers: AdminHeaders, conn: DbConn) -> Js } else { // v2023.1.0 and newer response Json(json!({ - "collections": convert_json_key_lcase_first(_get_org_collections(org_id, &conn).await), - "ciphers": convert_json_key_lcase_first(_get_org_details(org_id, &headers.user.uuid, &conn).await), + "collections": util::convert_json_key_lcase_first(_get_org_collections(org_id, &conn).await), + "ciphers": util::convert_json_key_lcase_first(_get_org_details(org_id, &headers.user.uuid, &conn).await), })) } } @@ -1977,7 +1979,7 @@ async fn _api_key( let org_api_key = if let Some(mut org_api_key) = OrganizationApiKey::find_by_org_uuid(org_id, &conn).await { if rotate { - org_api_key.api_key = crate::crypto::generate_api_key(); + org_api_key.api_key = crypto::generate_api_key(); org_api_key.revision_date = chrono::Utc::now().naive_utc(); org_api_key .save(&conn) @@ -1986,7 +1988,7 @@ async fn _api_key( } org_api_key } else { - let api_key = crate::crypto::generate_api_key(); + let api_key = crypto::generate_api_key(); let new_org_api_key = OrganizationApiKey::new(String::from(org_id), api_key); new_org_api_key .save(&conn) @@ -1996,7 +1998,7 @@ async fn _api_key( }; Ok(Json(json!({ "ApiKey": org_api_key.api_key, - "RevisionDate": crate::util::format_date(&org_api_key.revision_date), + "RevisionDate": util::format_date(&org_api_key.revision_date), "Object": "apiKey", }))) } diff --git a/src/api/core/sends.rs b/src/api/core/sends.rs @@ -2,7 +2,7 @@ use crate::{ api::{EmptyResult, JsonResult, JsonUpcase, Notify, NumberOrString}, auth::{ClientIp, Headers, Host}, db::DbConn, - util::SafeString, + util::{SafeString, UpCase}, }; use chrono::{DateTime, Utc}; use rocket::form::Form; @@ -75,7 +75,7 @@ fn post_send( #[allow(dead_code)] #[derive(FromForm)] struct UploadData<'f> { - model: Json<crate::util::UpCase<SendData>>, + model: Json<UpCase<SendData>>, data: TempFile<'f>, } diff --git a/src/api/core/two_factor/protected_actions.rs b/src/api/core/two_factor/protected_actions.rs @@ -10,6 +10,7 @@ use crate::{ }; use chrono::{Duration, NaiveDateTime, Utc}; use rocket::Route; +use serde_json; pub fn routes() -> Vec<Route> { routes![request_otp, verify_otp] @@ -28,7 +29,7 @@ struct ProtectedActionData { impl ProtectedActionData { fn from_json(string: &str) -> Result<Self, Error> { - let res: Result<Self, crate::serde_json::Error> = serde_json::from_str(string); + let res: Result<Self, serde_json::Error> = serde_json::from_str(string); match res { Ok(x) => Ok(x), Err(_) => err!("Could not decode ProtectedActionData from string"), diff --git a/src/api/core/two_factor/webauthn.rs b/src/api/core/two_factor/webauthn.rs @@ -3,7 +3,7 @@ use crate::{ auth::Headers, config, db::{ - models::{TwoFactor, TwoFactorType}, + models::{TwoFactor, TwoFactorType, WebAuthn, WebauthnRegistration}, DbConn, }, error::Error, @@ -12,14 +12,10 @@ use rocket::serde::json::Json; use rocket::Route; use serde_json::Value; use url::Url; -use webauthn_rs::{ - base64_data::Base64UrlSafeData, - proto::{ - AuthenticationExtensionsClientOutputs, AuthenticatorAssertionResponseRaw, - AuthenticatorAttestationResponseRaw, Credential, PublicKeyCredential, - RegisterPublicKeyCredential, RequestAuthenticationExtensions, - }, - AuthenticationState, RegistrationState, Webauthn, +use webauthn_rs::prelude::{ + AttestationCa, AttestationCaList, AuthenticatorAttachment, PublicKeyCredential, + RegisterPublicKeyCredential, SecurityKeyAuthentication, SecurityKeyRegistration, Uuid, + Webauthn, WebauthnBuilder, WebauthnError, }; pub fn routes() -> Vec<Route> { @@ -31,59 +27,15 @@ pub fn routes() -> Vec<Route> { get_webauthn, ] } - -struct WebauthnConfig { - url: String, - origin: Url, - rpid: String, -} - -impl WebauthnConfig { - fn load() -> Webauthn<Self> { - let domain = &config::get_config().domain; - let domain_origin = config::get_config().domain_origin(); - Webauthn::new(Self { - rpid: domain.domain().expect("a valid domain").to_owned(), - url: domain.to_string(), - origin: Url::parse(&domain_origin).unwrap(), - }) - } -} - -impl webauthn_rs::WebauthnConfig for WebauthnConfig { - fn get_relying_party_name(&self) -> &str { - &self.url - } - fn get_origin(&self) -> &Url { - &self.origin - } - fn get_relying_party_id(&self) -> &str { - &self.rpid - } - /// We have WebAuthn configured to discourage user verification - /// if we leave this enabled, it will cause verification issues when a keys send UV=1. - /// Upstream (the library they use) ignores this when set to discouraged, so we should too. - fn get_require_uv_consistency(&self) -> bool { - false - } -} - -#[derive(Debug, Serialize, Deserialize)] -struct WebauthnRegistration { - id: i32, - name: String, - migrated: bool, - credential: Credential, -} - -impl WebauthnRegistration { - fn to_json(&self) -> Value { - json!({ - "Id": self.id, - "Name": self.name, - "migrated": self.migrated, - }) - } +fn build_webauthn() -> Result<Webauthn, WebauthnError> { + WebauthnBuilder::new( + config::get_config() + .domain + .domain() + .expect("a valid domain"), + &Url::parse(&config::get_config().domain_origin()).expect("a valid URL"), + )? + .build() } #[post("/two-factor/get-webauthn", data = "<data>")] @@ -95,14 +47,18 @@ async fn get_webauthn( let data: PasswordOrOtpData = data.into_inner().data; let user = headers.user; data.validate(&user, false, &conn).await?; - let (enabled, registrations) = get_webauthn_registrations(&user.uuid, &conn).await?; - let registrations_json: Vec<Value> = registrations - .iter() - .map(WebauthnRegistration::to_json) - .collect(); + let (enabled, regs) = get_tf_entry(&user.uuid, i32::from(TwoFactorType::Webauthn), &conn) + .await + .map_or_else( + || Ok((false, Vec::new())), + |tf| { + tf.get_webauthn_registrations() + .map(|regs| (tf.enabled, regs)) + }, + )?; Ok(Json(json!({ "Enabled": enabled, - "Keys": registrations_json, + "Keys": regs.iter().map(WebauthnRegistration::to_json).collect::<Value>(), "Object": "twoFactorWebAuthn" }))) } @@ -116,186 +72,121 @@ async fn generate_webauthn_challenge( let data: PasswordOrOtpData = data.into_inner().data; let user = headers.user; data.validate(&user, false, &conn).await?; - let registrations = get_webauthn_registrations(&user.uuid, &conn) - .await? - .1 - .into_iter() - .map(|r| r.credential.cred_id) // We return the credentialIds to the clients to avoid double registering - .collect(); - let (challenge, state) = WebauthnConfig::load().generate_challenge_register_options( - user.uuid.as_bytes().to_vec(), - user.email, - user.name, - Some(registrations), - None, - None, + // We only allow YubiKeys with firmware 5.2 or 5.4. + let mut attest_list = AttestationCaList::default(); + let mut ca = AttestationCa::yubico_u2f_root_ca_serial_457200631(); + ca.aaguids.clear(); + ca.aaguids + .insert(Uuid::try_parse("ee882879-721c-4913-9775-3dfcce97072a").expect("invaild UUID")); + attest_list.insert(ca)?; + let (challenge, registration) = build_webauthn()?.start_securitykey_registration( + Uuid::try_parse(user.uuid.as_str()).expect("unable to create UUID"), + user.email.as_str(), + user.name.as_str(), + Some(WebAuthn::get_all_credentials_by_user(&user.uuid, &conn).await?), + Some(attest_list), + Some(AuthenticatorAttachment::CrossPlatform), )?; - let type_ = TwoFactorType::WebauthnRegisterChallenge; - TwoFactor::new(user.uuid, type_, serde_json::to_string(&state)?) - .save(&conn) - .await?; + // We replace any existing registration challenges. + TwoFactor::new( + user.uuid, + TwoFactorType::WebauthnRegisterChallenge, + serde_json::to_string(®istration)?, + ) + .replace_challenge(&conn) + .await?; let mut challenge_value = serde_json::to_value(challenge.public_key)?; challenge_value["status"] = "ok".into(); challenge_value["errorMessage"] = "".into(); Ok(Json(challenge_value)) } -#[derive(Debug, Deserialize)] -#[allow(non_snake_case)] +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] struct EnableWebauthnData { - Id: NumberOrString, // 1..5 - Name: String, - DeviceResponse: RegisterPublicKeyCredentialCopy, - MasterPasswordHash: Option<String>, - Otp: Option<String>, -} - -// This is copied from RegisterPublicKeyCredential to change the Response objects casing -#[derive(Debug, Deserialize)] -#[allow(non_snake_case)] -struct RegisterPublicKeyCredentialCopy { - Id: String, - RawId: Base64UrlSafeData, - Response: AuthenticatorAttestationResponseRawCopy, - Type: String, -} - -// This is copied from AuthenticatorAttestationResponseRaw to change clientDataJSON to clientDataJson -#[derive(Debug, Deserialize)] -#[allow(non_snake_case)] -struct AuthenticatorAttestationResponseRawCopy { - AttestationObject: Base64UrlSafeData, - ClientDataJson: Base64UrlSafeData, -} - -impl From<RegisterPublicKeyCredentialCopy> for RegisterPublicKeyCredential { - fn from(r: RegisterPublicKeyCredentialCopy) -> Self { - Self { - id: r.Id, - raw_id: r.RawId, - response: AuthenticatorAttestationResponseRaw { - attestation_object: r.Response.AttestationObject, - client_data_json: r.Response.ClientDataJson, - }, - type_: r.Type, - } - } -} - -// This is copied from PublicKeyCredential to change the Response objects casing -#[derive(Debug, Deserialize)] -#[allow(non_snake_case)] -struct PublicKeyCredentialCopy { - Id: String, - RawId: Base64UrlSafeData, - Response: AuthenticatorAssertionResponseRawCopy, - Extensions: Option<AuthenticationExtensionsClientOutputsCopy>, - Type: String, -} - -// This is copied from AuthenticatorAssertionResponseRaw to change clientDataJSON to clientDataJson -#[derive(Debug, Deserialize)] -#[allow(non_snake_case)] -struct AuthenticatorAssertionResponseRawCopy { - AuthenticatorData: Base64UrlSafeData, - ClientDataJson: Base64UrlSafeData, - Signature: Base64UrlSafeData, - UserHandle: Option<Base64UrlSafeData>, -} - -#[derive(Debug, Deserialize)] -#[allow(non_snake_case)] -struct AuthenticationExtensionsClientOutputsCopy { - #[serde(default)] - Appid: bool, -} - -impl From<PublicKeyCredentialCopy> for PublicKeyCredential { - fn from(r: PublicKeyCredentialCopy) -> Self { - Self { - id: r.Id, - raw_id: r.RawId, - response: AuthenticatorAssertionResponseRaw { - authenticator_data: r.Response.AuthenticatorData, - client_data_json: r.Response.ClientDataJson, - signature: r.Response.Signature, - user_handle: r.Response.UserHandle, - }, - extensions: r - .Extensions - .map(|e| AuthenticationExtensionsClientOutputs { appid: e.Appid }), - type_: r.Type, - } - } + id: u32, + name: String, + device_response: RegisterPublicKeyCredential, + master_password_hash: String, } #[post("/two-factor/webauthn", data = "<data>")] async fn activate_webauthn( - data: JsonUpcase<EnableWebauthnData>, + data: Json<EnableWebauthnData>, headers: Headers, conn: DbConn, ) -> JsonResult { - let data: EnableWebauthnData = data.into_inner().data; + let data = data.into_inner(); let user = headers.user; PasswordOrOtpData { - MasterPasswordHash: data.MasterPasswordHash, - Otp: data.Otp, + MasterPasswordHash: Some(data.master_password_hash), + Otp: None, } .validate(&user, true, &conn) .await?; // Retrieve and delete the saved challenge state - let type_ = i32::from(TwoFactorType::WebauthnRegisterChallenge); - let state = match TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn).await { - Some(tf) => { - let state: RegistrationState = serde_json::from_str(&tf.data)?; - tf.delete(&conn).await?; - state + let tf_challenge = get_tf_entry( + &user.uuid, + i32::from(TwoFactorType::WebauthnRegisterChallenge), + &conn, + ) + .await + .ok_or_else(|| Error::from(String::from("no webauthn challenge")))?; + let registration = serde_json::from_str::<SecurityKeyRegistration>(&tf_challenge.data)?; + tf_challenge.delete_challenge(&conn).await?; + // Verify the credentials with the saved state + let security_key = + build_webauthn()?.finish_securitykey_registration(&data.device_response, ®istration)?; + let cred_id = security_key.cred_id().to_string(); + let regs = match get_tf_entry(&user.uuid, i32::from(TwoFactorType::Webauthn), &conn).await { + None => { + let regs = vec![WebauthnRegistration { + id: data.id, + name: data.name, + security_key, + }]; + let tf = TwoFactor::new( + user.uuid, + TwoFactorType::Webauthn, + serde_json::to_string(®s)?, + ); + tf.insert_insert_webauthn(tf.create_webauthn(cred_id), &conn) + .await?; + regs + } + Some(mut tf) => { + let mut regs = tf.get_webauthn_registrations()?; + regs.push(WebauthnRegistration { + id: data.id, + name: data.name, + security_key, + }); + tf.data = serde_json::to_string(®s)?; + tf.update_insert_webauthn(tf.create_webauthn(cred_id), &conn) + .await?; + regs } - None => err!("Can't recover challenge"), }; - // Verify the credentials with the saved state - let (credential, _data) = - WebauthnConfig::load() - .register_credential(&data.DeviceResponse.into(), &state, |_| Ok(false))?; - let mut registrations: Vec<_> = get_webauthn_registrations(&user.uuid, &conn).await?.1; - // TODO: Check for repeated ID's - registrations.push(WebauthnRegistration { - id: data.Id.into_i32()?, - name: data.Name, - migrated: false, - credential, - }); - // Save the registrations and return them - TwoFactor::new( - user.uuid.clone(), - TwoFactorType::Webauthn, - serde_json::to_string(®istrations)?, - ) - .save(&conn) - .await?; - let keys_json: Vec<Value> = registrations - .iter() - .map(WebauthnRegistration::to_json) - .collect(); Ok(Json(json!({ "Enabled": true, - "Keys": keys_json, + "Keys": regs.iter().map(WebauthnRegistration::to_json).collect::<Value>(), "Object": "twoFactorU2f" }))) } #[put("/two-factor/webauthn", data = "<data>")] async fn activate_webauthn_put( - data: JsonUpcase<EnableWebauthnData>, + data: Json<EnableWebauthnData>, headers: Headers, conn: DbConn, ) -> JsonResult { activate_webauthn(data, headers, conn).await } -#[derive(Deserialize, Debug)] +#[derive(Deserialize)] #[allow(non_snake_case)] struct DeleteU2FData { + Id: NumberOrString, MasterPasswordHash: String, } @@ -311,68 +202,53 @@ async fn delete_webauthn( { err!("Invalid password"); } - let Some(mut tf) = TwoFactor::find_by_user_and_type( + let mut tf = get_tf_entry( &headers.user.uuid, i32::from(TwoFactorType::Webauthn), &conn, ) .await - else { - err!("Webauthn data not found!") + .ok_or_else(|| Error::from(String::from("no twofactor entries")))?; + let mut regs = tf.get_webauthn_registrations()?; + let id = u32::try_from(data.data.Id.into_i32()?).expect("underflow"); + let Some(item_pos) = regs.iter().position(|r| r.id == id) else { + err!("Webauthn entry not found") }; - let web_authn_data: Vec<WebauthnRegistration> = serde_json::from_str(&tf.data)?; - tf.data = serde_json::to_string(&web_authn_data)?; - tf.save(&conn).await?; + let old_cred = regs.remove(item_pos).security_key.cred_id().to_string(); + tf.data = serde_json::to_string(®s)?; + tf.update_delete_webauthn(old_cred, &conn).await?; drop(tf); - let keys_json: Vec<Value> = web_authn_data - .iter() - .map(WebauthnRegistration::to_json) - .collect(); Ok(Json(json!({ "Enabled": true, - "Keys": keys_json, + "Keys": regs.iter().map(WebauthnRegistration::to_json).collect::<Value>(), "Object": "twoFactorU2f" }))) } -async fn get_webauthn_registrations( - user_uuid: &str, - conn: &DbConn, -) -> Result<(bool, Vec<WebauthnRegistration>), Error> { - let type_ = i32::from(TwoFactorType::Webauthn); - match TwoFactor::find_by_user_and_type(user_uuid, type_, conn).await { - Some(tf) => Ok((tf.enabled, serde_json::from_str(&tf.data)?)), - None => Ok((false, Vec::new())), // If no data, return empty list - } +async fn get_tf_entry(user_uuid: &str, type_: i32, conn: &DbConn) -> Option<TwoFactor> { + TwoFactor::find_by_user_and_type(user_uuid, type_, conn).await } pub async fn generate_webauthn_login(user_uuid: &str, conn: &DbConn) -> JsonResult { - // Load saved credentials - let creds: Vec<Credential> = get_webauthn_registrations(user_uuid, conn) - .await? - .1 - .into_iter() - .map(|r| r.credential) - .collect(); - if creds.is_empty() { + let tf = get_tf_entry(user_uuid, i32::from(TwoFactorType::Webauthn), conn) + .await + .ok_or_else(|| Error::from(String::from("no twofactor entries")))?; + let regs = tf.get_webauthn_registrations()?; + if regs.is_empty() { err!("No Webauthn devices registered") } - // Generate a challenge based on the credentials - let ext = RequestAuthenticationExtensions::builder() - .appid(format!("{}/app-id.json", config::get_config().domain)) - .build(); - let (response, state) = - WebauthnConfig::load().generate_challenge_authenticate_options(creds, Some(ext))?; - // Save the challenge state for later validation + let (challenge, auth) = build_webauthn()? + .start_securitykey_authentication(&WebauthnRegistration::to_security_keys(regs))?; + // Save the challenge state for later validation. TwoFactor::new( user_uuid.into(), TwoFactorType::WebauthnLoginChallenge, - serde_json::to_string(&state)?, + serde_json::to_string(&auth)?, ) - .save(conn) + .replace_challenge(conn) .await?; // Return challenge to the clients - Ok(Json(serde_json::to_value(response.public_key)?)) + Ok(Json(serde_json::to_value(challenge.public_key)?)) } pub async fn validate_webauthn_login( @@ -380,31 +256,35 @@ pub async fn validate_webauthn_login( response: &str, conn: &DbConn, ) -> EmptyResult { - let type_ = i32::from(TwoFactorType::WebauthnLoginChallenge); - let state = match TwoFactor::find_by_user_and_type(user_uuid, type_, conn).await { - Some(tf) => { - let state: AuthenticationState = serde_json::from_str(&tf.data)?; - tf.delete(conn).await?; - state - } - None => err!("Can't recover login challenge"), - }; - let rsp: crate::util::UpCase<PublicKeyCredentialCopy> = serde_json::from_str(response)?; - let rsp: PublicKeyCredential = rsp.data.into(); - let mut registrations = get_webauthn_registrations(user_uuid, conn).await?.1; - let (cred_id, auth_data) = WebauthnConfig::load().authenticate_credential(&rsp, &state)?; - for reg in &mut registrations { - if ®.credential.cred_id == cred_id { - reg.credential.counter = auth_data.counter; - TwoFactor::new( - user_uuid.to_owned(), - TwoFactorType::Webauthn, - serde_json::to_string(®istrations)?, - ) - .save(conn) - .await?; - return Ok(()); + let tf_challenge = get_tf_entry( + user_uuid, + i32::from(TwoFactorType::WebauthnLoginChallenge), + conn, + ) + .await + .ok_or_else(|| Error::from(String::from("no webauthn challenge")))?; + let security_key_authentication = + serde_json::from_str::<SecurityKeyAuthentication>(&tf_challenge.data)?; + tf_challenge.delete_challenge(conn).await?; + let resp = serde_json::from_str::<PublicKeyCredential>(response)?; + let mut tf = get_tf_entry(user_uuid, i32::from(TwoFactorType::Webauthn), conn) + .await + .ok_or_else(|| Error::from(String::from("no twofactor entries")))?; + let mut regs = tf.get_webauthn_registrations()?; + let auth = + build_webauthn()?.finish_securitykey_authentication(&resp, &security_key_authentication)?; + if auth.needs_update() { + for reg in &mut regs { + if let Some(update) = reg.security_key.update_credential(&auth) { + if update { + tf.data = serde_json::to_string(®s)?; + tf.update_webauthn(conn).await?; + } + return Ok(()); + } } + Err(Error::from(String::from("Credential not present"))) + } else { + Ok(()) } - err!("Credential not present") } diff --git a/src/api/identity.rs b/src/api/identity.rs @@ -3,7 +3,7 @@ use crate::{ core::accounts::{PreloginData, RegisterData, _prelogin}, ApiResult, EmptyResult, JsonResult, JsonUpcase, }, - auth::{generate_organization_api_key_login_claims, ClientHeaders, ClientIp}, + auth::{self, generate_organization_api_key_login_claims, ClientHeaders, ClientIp}, config, db::{ models::{AuthRequest, Device, OrganizationApiKey, TwoFactor, TwoFactorType, User}, @@ -292,7 +292,7 @@ async fn _organization_api_key_login( ) } let claim = generate_organization_api_key_login_claims(org_api_key.uuid, org_api_key.org_uuid); - let access_token = crate::auth::encode_jwt(&claim); + let access_token = auth::encode_jwt(&claim); Ok(Json(json!({ "access_token": access_token, "expires_in": 3600i32, diff --git a/src/api/mod.rs b/src/api/mod.rs @@ -21,11 +21,12 @@ pub use crate::api::{ web::routes as web_routes, }; use crate::db::{models::User, DbConn}; +use crate::error::Error; use crate::util; use rocket::serde::json::Json; use serde_json::Value; // Type aliases for API methods results -type ApiResult<T> = Result<T, crate::error::Error>; +type ApiResult<T> = Result<T, Error>; type JsonResult = ApiResult<Json<Value>>; pub type EmptyResult = ApiResult<()>; type JsonUpcase<T> = Json<util::UpCase<T>>; @@ -75,13 +76,14 @@ impl NumberOrString { Self::String(s) => s, } } - fn into_i32(self) -> ApiResult<i32> { + #[allow(clippy::wrong_self_convention)] + fn into_i32(&self) -> ApiResult<i32> { use std::num::ParseIntError as PIE; - match self { + match *self { Self::Number(n) => Ok(n), - Self::String(s) => s + Self::String(ref s) => s .parse() - .map_err(|e: PIE| crate::Error::new("Can't convert to number", e.to_string())), + .map_err(|e: PIE| Error::new("Can't convert to number", e.to_string())), } } } diff --git a/src/api/notifications.rs b/src/api/notifications.rs @@ -1,14 +1,16 @@ use crate::{ - auth::{ClientIp, WsAccessTokenHeader}, + auth::{self, ClientIp, WsAccessTokenHeader}, db::models::{Cipher, Folder, User}, Error, }; use chrono::{NaiveDateTime, Utc}; +use core::convert; use rmpv::Value; use rocket::{futures::StreamExt, Route}; use std::sync::OnceLock; use std::{sync::Arc, time::Duration}; -use tokio::sync::mpsc::Sender; +use tokio::sync::mpsc::{channel, Sender}; +use tokio::time; use tokio_tungstenite::tungstenite::Message; static WS_USERS: OnceLock<Arc<WebSocketUsers>> = OnceLock::new(); #[inline] @@ -115,14 +117,14 @@ fn websockets_hub<'r>( err_code!("Invalid claim", 401) }; - let Ok(claims) = crate::auth::decode_login(&token) else { + let Ok(claims) = auth::decode_login(&token) else { err_code!("Invalid token", 401) }; let (mut rx, guard) = { let users = Arc::clone(ws_users()); // Add a channel to send messages to this client to the map let entry_uuid = uuid::Uuid::new_v4(); - let (tx, rx) = tokio::sync::mpsc::channel::<Message>(100); + let (tx, rx) = channel::<Message>(100); users .map .entry(claims.sub.clone()) @@ -136,7 +138,7 @@ fn websockets_hub<'r>( rocket_ws::Stream! { ws => { let mut ws_copy = ws; let _guard = guard; - let mut interval = tokio::time::interval(Duration::from_secs(15)); + let mut interval = time::interval(Duration::from_secs(15)); loop { tokio::select! { res = ws_copy.next() => { @@ -186,7 +188,7 @@ fn anonymous_websockets_hub<'r>( let (mut rx, guard) = { let subscriptions = Arc::clone(ws_anonymous_subscriptions()); // Add a channel to send messages to this client to the map - let (tx, rx) = tokio::sync::mpsc::channel::<Message>(100); + let (tx, rx) = channel::<Message>(100); subscriptions.map.insert(token.clone(), tx); // Once the guard goes out of scope, the connection will have been closed and the entry will be deleted from the map (rx, WSAnonymousEntryMapGuard::new(subscriptions, token)) @@ -195,7 +197,7 @@ fn anonymous_websockets_hub<'r>( rocket_ws::Stream! { ws => { let mut ws_copy = ws; let _guard = guard; - let mut interval = tokio::time::interval(Duration::from_secs(15)); + let mut interval = time::interval(Duration::from_secs(15)); loop { tokio::select! { res = ws_copy.next() => { @@ -269,7 +271,7 @@ fn serialize_date(date: NaiveDateTime) -> Value { } fn convert_option<T: Into<Value>>(option: Option<T>) -> Value { - option.map_or(Value::Nil, core::convert::Into::into) + option.map_or(Value::Nil, convert::Into::into) } const RECORD_SEPARATOR: u8 = 0x1e; @@ -372,7 +374,7 @@ impl WebSocketUsers { Value::Array( col_uuids .into_iter() - .map(core::convert::Into::into) + .map(convert::Into::into) .collect::<Vec<rmpv::Value>>(), ), serialize_date(Utc::now().naive_utc()), @@ -487,7 +489,7 @@ fn create_update( V::Array(vec![V::Map(vec![ ( "ContextId".into(), - acting_device_uuid.map_or(V::Nil, core::convert::Into::into), + acting_device_uuid.map_or(V::Nil, convert::Into::into), ), ("Type".into(), (i32::from(ut)).into()), ("Payload".into(), payload.into()), diff --git a/src/db/mod.rs b/src/db/mod.rs @@ -4,16 +4,19 @@ use crate::{ }; use diesel::{ connection::SimpleConnection, - r2d2::{ConnectionManager, CustomizeConnection, Pool, PooledConnection}, + r2d2::{self, ConnectionManager, CustomizeConnection, Pool, PooledConnection}, + SqliteConnection, }; use rocket::{ http::Status, request::{FromRequest, Outcome}, Request, }; -use std::{sync::Arc, time::Duration}; +use std::{panic, sync::Arc, time::Duration}; use tokio::{ + runtime, sync::{Mutex, OwnedSemaphorePermit, Semaphore}, + task, time::timeout, }; #[path = "schemas/sqlite/schema.rs"] @@ -26,30 +29,30 @@ where F: FnOnce() -> R + Send + 'static, R: Send + 'static, { - match tokio::task::spawn_blocking(job).await { + match task::spawn_blocking(job).await { Ok(ret) => ret, Err(e) => e.try_into_panic().map_or_else( |_| unreachable!("spawn_blocking tasks are never cancelled"), - |panic| std::panic::resume_unwind(panic), + |panic| panic::resume_unwind(panic), ), } } pub struct DbConn { - conn: Arc<Mutex<Option<PooledConnection<ConnectionManager<diesel::SqliteConnection>>>>>, + conn: Arc<Mutex<Option<PooledConnection<ConnectionManager<SqliteConnection>>>>>, permit: Option<OwnedSemaphorePermit>, } #[derive(Debug)] struct DbConnOptions; -impl CustomizeConnection<diesel::SqliteConnection, diesel::r2d2::Error> for DbConnOptions { - fn on_acquire(&self, conn: &mut diesel::SqliteConnection) -> Result<(), diesel::r2d2::Error> { +impl CustomizeConnection<SqliteConnection, r2d2::Error> for DbConnOptions { + fn on_acquire(&self, conn: &mut SqliteConnection) -> Result<(), r2d2::Error> { conn.batch_execute("PRAGMA busy_timeout = 5000; PRAGMA synchronous = NORMAL;") - .map_err(diesel::r2d2::Error::QueryError) + .map_err(r2d2::Error::QueryError) } } #[derive(Clone)] pub struct DbPool { // This is an 'Option' so that we can drop the pool in a 'spawn_blocking'. - pool: Option<Pool<ConnectionManager<diesel::SqliteConnection>>>, + pool: Option<Pool<ConnectionManager<SqliteConnection>>>, semaphore: Arc<Semaphore>, } impl Drop for DbConn { @@ -58,9 +61,9 @@ impl Drop for DbConn { let permit = self.permit.take(); // Since connection can't be on the stack in an async fn during an // await, we have to spawn a new blocking-safe thread... - tokio::task::spawn_blocking(move || { + task::spawn_blocking(move || { // And then re-enter the runtime to wait on the async mutex, but in a blocking fashion. - let mut conn = tokio::runtime::Handle::current().block_on(conn.lock_owned()); + let mut conn = runtime::Handle::current().block_on(conn.lock_owned()); if let Some(conn) = conn.take() { drop(conn); } @@ -72,14 +75,13 @@ impl Drop for DbConn { impl Drop for DbPool { fn drop(&mut self) { let pool = self.pool.take(); - tokio::task::spawn_blocking(move || drop(pool)); + task::spawn_blocking(move || drop(pool)); } } impl DbPool { // For the given database URL, guess its type, run migrations, create pool, and return it pub fn from_config() -> Result<Self, Error> { let url = Config::DATABASE_URL; - paste::paste! {sqlite_migrations::run_migrations()?; } let manager = ConnectionManager::new(url); let pool = Pool::builder() .max_size(u32::from(config::get_config().database_max_conns.get())) @@ -124,6 +126,7 @@ macro_rules! db_run { ( $conn:ident: $body:block ) => { #[allow(unused)] use diesel::prelude::*; + use tokio::task; #[allow(unused)] use $crate::db::FromDb; let mut con = $conn.conn.clone().lock_owned().await; @@ -131,7 +134,7 @@ macro_rules! db_run { #[allow(unused)] use $crate::db::__sqlite_schema::{self as schema, *}; #[allow(unused)] use __sqlite_model::*; } - tokio::task::block_in_place(move || { + task::block_in_place(move || { let $conn = con .as_mut() .expect("internal invariant broken: self.connection is Some"); @@ -151,7 +154,7 @@ impl<T: FromDb> FromDb for Vec<T> { #[allow(clippy::wrong_self_convention)] #[inline] fn from_db(self) -> Self::Output { - self.into_iter().map(crate::db::FromDb::from_db).collect() + self.into_iter().map(FromDb::from_db).collect() } } @@ -160,7 +163,7 @@ impl<T: FromDb> FromDb for Option<T> { #[allow(clippy::wrong_self_convention)] #[inline] fn from_db(self) -> Self::Output { - self.map(crate::db::FromDb::from_db) + self.map(FromDb::from_db) } } @@ -224,29 +227,3 @@ impl<'r> FromRequest<'r> for DbConn { } } } -mod sqlite_migrations { - use diesel_migrations::{EmbeddedMigrations, MigrationHarness}; - const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations/sqlite"); - - pub fn run_migrations() -> Result<(), super::Error> { - use diesel::{Connection, RunQueryDsl}; - let url = crate::Config::DATABASE_URL; - // Establish a connection to the sqlite database (this will create a new one, if it does - // not exist, and exit if there is an error). - let mut connection = diesel::sqlite::SqliteConnection::establish(url)?; - // Run the migrations after successfully establishing a connection - // Disable Foreign Key Checks during migration - // Scoped to a connection. - diesel::sql_query("PRAGMA foreign_keys = OFF") - .execute(&mut connection) - .expect("Failed to disable Foreign Key Checks during migrations"); - - diesel::sql_query("PRAGMA journal_mode=wal") - .execute(&mut connection) - .expect("Failed to turn on WAL"); - connection - .run_pending_migrations(MIGRATIONS) - .expect("Error running migrations"); - Ok(()) - } -} diff --git a/src/db/models/auth_request.rs b/src/db/models/auth_request.rs @@ -1,11 +1,12 @@ use crate::crypto::ct_eq; +use crate::util; use chrono::{NaiveDateTime, Utc}; +use diesel::result::{self, DatabaseErrorKind}; db_object! { - #[derive(Debug, Identifiable, Queryable, Insertable, AsChangeset, Deserialize, Serialize)] + #[derive(AsChangeset, Insertable, Queryable)] #[diesel(table_name = auth_requests)] #[diesel(treat_none_as_null = true)] - #[diesel(primary_key(uuid))] pub struct AuthRequest { pub uuid: String, pub user_uuid: String, @@ -36,7 +37,7 @@ impl AuthRequest { ) -> Self { let now = Utc::now().naive_utc(); Self { - uuid: crate::util::get_uuid(), + uuid: util::get_uuid(), user_uuid, organization_uuid: None, request_device_identifier, @@ -69,7 +70,7 @@ impl AuthRequest { { Ok(_) => Ok(()), // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first. - Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => { + Err(result::Error::DatabaseError(DatabaseErrorKind::ForeignKeyViolation, _)) => { diesel::update(auth_requests::table) .filter(auth_requests::uuid.eq(&self.uuid)) .set(AuthRequestDb::to_db(self)) diff --git a/src/db/models/cipher.rs b/src/db/models/cipher.rs @@ -2,15 +2,17 @@ use super::{ CollectionCipher, Favorite, FolderCipher, User, UserOrgStatus, UserOrgType, UserOrganization, }; use crate::api::core::{CipherData, CipherSyncData, CipherSyncType}; +use crate::util; use chrono::{NaiveDateTime, Utc}; +use diesel::result::{self, DatabaseErrorKind}; use serde_json::Value; use std::borrow::Cow; +use std::string::ToString; db_object! { - #[derive(Identifiable, Queryable, Insertable, AsChangeset)] + #[derive(AsChangeset, Insertable, Queryable)] #[diesel(table_name = ciphers)] #[diesel(treat_none_as_null = true)] - #[diesel(primary_key(uuid))] pub struct Cipher { pub uuid: String, created_at: NaiveDateTime, @@ -45,7 +47,7 @@ impl Cipher { pub fn new(atype: i32, name: String) -> Self { let now = Utc::now().naive_utc(); Self { - uuid: crate::util::get_uuid(), + uuid: util::get_uuid(), created_at: now, updated_at: now, user_uuid: None, @@ -99,7 +101,6 @@ impl Cipher { sync_type: CipherSyncType, conn: &DbConn, ) -> Value { - use crate::util::format_date; let fields_json = self .fields .as_ref() @@ -165,9 +166,9 @@ impl Cipher { "Object": "cipherDetails", "Id": self.uuid, "Type": self.atype, - "CreationDate": format_date(&self.created_at), - "RevisionDate": format_date(&self.updated_at), - "DeletedDate": self.deleted_at.map_or(Value::Null, |d| Value::String(format_date(&d))), + "CreationDate": util::format_date(&self.created_at), + "RevisionDate": util::format_date(&self.updated_at), + "DeletedDate": self.deleted_at.map_or(Value::Null, |d| Value::String(util::format_date(&d))), "Reprompt": self.reprompt.unwrap_or_else(|| i32::from(RepromptType::None)), "OrganizationId": self.organization_uuid, "Key": self.key, @@ -196,7 +197,7 @@ impl Cipher { cipher_sync_data .cipher_folders .get(&self.uuid) - .map(std::string::ToString::to_string) + .map(ToString::to_string) } else { self.get_folder_uuid(user_uuid, conn).await }); @@ -257,7 +258,7 @@ impl Cipher { { Ok(_) => Ok(()), // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first. - Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => { + Err(result::Error::DatabaseError(DatabaseErrorKind::ForeignKeyViolation, _)) => { diesel::update(ciphers::table) .filter(ciphers::uuid.eq(&self.uuid)) .set(CipherDb::to_db(self)) diff --git a/src/db/models/collection.rs b/src/db/models/collection.rs @@ -1,10 +1,12 @@ use super::{User, UserOrgStatus, UserOrgType, UserOrganization}; +use crate::api::core::CipherSyncData; +use crate::util; +use diesel::result::{self, DatabaseErrorKind}; use serde_json::Value; db_object! { - #[derive(Identifiable, Queryable, Insertable, AsChangeset)] + #[derive(AsChangeset, Insertable, Queryable)] #[diesel(table_name = collections)] - #[diesel(primary_key(uuid))] pub struct Collection { pub uuid: String, pub org_uuid: String, @@ -12,9 +14,8 @@ db_object! { pub external_id: Option<String>, } - #[derive(Identifiable, Queryable, Insertable)] + #[derive(Insertable, Queryable)] #[diesel(table_name = users_collections)] - #[diesel(primary_key(user_uuid, collection_uuid))] pub struct CollectionUser { pub user_uuid: String, pub collection_uuid: String, @@ -22,9 +23,8 @@ db_object! { pub hide_passwords: bool, } - #[derive(Identifiable, Queryable, Insertable)] + #[derive(Insertable)] #[diesel(table_name = ciphers_collections)] - #[diesel(primary_key(cipher_uuid, collection_uuid))] pub struct CollectionCipher { cipher_uuid: String, collection_uuid: String, @@ -35,7 +35,7 @@ db_object! { impl Collection { pub fn new(org_uuid: String, name: String, external_id: Option<String>) -> Self { let mut new_model = Self { - uuid: crate::util::get_uuid(), + uuid: util::get_uuid(), org_uuid, name, external_id: None, @@ -72,7 +72,7 @@ impl Collection { pub async fn to_json_details( &self, user_uuid: &str, - cipher_sync_data: Option<&crate::api::core::CipherSyncData>, + cipher_sync_data: Option<&CipherSyncData>, conn: &DbConn, ) -> Value { let (read_only, hide_passwords) = if let Some(cipher_sync_data) = cipher_sync_data { @@ -114,7 +114,7 @@ impl Collection { { Ok(_) => Ok(()), // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first. - Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => { + Err(result::Error::DatabaseError(DatabaseErrorKind::ForeignKeyViolation, _)) => { diesel::update(collections::table) .filter(collections::uuid.eq(&self.uuid)) .set(CollectionDb::to_db(self)) @@ -391,7 +391,7 @@ impl CollectionUser { { Ok(_) => Ok(()), // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first. - Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => { + Err(result::Error::DatabaseError(DatabaseErrorKind::ForeignKeyViolation, _)) => { diesel::update(users_collections::table) .filter(users_collections::user_uuid.eq(user_uuid)) .filter(users_collections::collection_uuid.eq(collection_uuid)) diff --git a/src/db/models/device.rs b/src/db/models/device.rs @@ -1,12 +1,12 @@ use crate::crypto; +use crate::util; use chrono::{NaiveDateTime, Utc}; use core::fmt; db_object! { - #[derive(Identifiable, Queryable, Insertable, AsChangeset)] + #[derive(Insertable, Queryable)] #[diesel(table_name = devices)] #[diesel(treat_none_as_null = true)] - #[diesel(primary_key(uuid, user_uuid))] pub struct Device { pub uuid: String, created_at: NaiveDateTime, @@ -109,7 +109,7 @@ impl Device { self.updated_at = Utc::now().naive_utc(); db_run! { conn: { - crate::util::retry( + util::retry( || diesel::replace_into(devices::table).values(DeviceDb::to_db(self)).execute(conn), 10, ).map_res("Error saving device") diff --git a/src/db/models/favorite.rs b/src/db/models/favorite.rs @@ -3,9 +3,8 @@ use crate::api::EmptyResult; use crate::db::DbConn; use crate::error::MapResult; db_object! { - #[derive(Identifiable, Queryable, Insertable)] + #[derive(Insertable)] #[diesel(table_name = favorites)] - #[diesel(primary_key(user_uuid, cipher_uuid))] pub struct Favorite { user_uuid: String, cipher_uuid: String, diff --git a/src/db/models/folder.rs b/src/db/models/folder.rs @@ -1,11 +1,12 @@ use super::User; +use crate::util; use chrono::{NaiveDateTime, Utc}; +use diesel::result::{self, DatabaseErrorKind}; use serde_json::Value; db_object! { - #[derive(Identifiable, Queryable, Insertable, AsChangeset)] + #[derive(AsChangeset, Insertable, Queryable)] #[diesel(table_name = folders)] - #[diesel(primary_key(uuid))] pub struct Folder { pub uuid: String, created_at: NaiveDateTime, @@ -14,9 +15,8 @@ db_object! { pub name: String, } - #[derive(Identifiable, Queryable, Insertable)] + #[derive(Insertable, Queryable)] #[diesel(table_name = folders_ciphers)] - #[diesel(primary_key(cipher_uuid, folder_uuid))] pub struct FolderCipher { cipher_uuid: String, folder_uuid: String, @@ -28,7 +28,7 @@ impl Folder { pub fn new(user_uuid: String, name: String) -> Self { let now = Utc::now().naive_utc(); Self { - uuid: crate::util::get_uuid(), + uuid: util::get_uuid(), created_at: now, updated_at: now, @@ -37,7 +37,7 @@ impl Folder { } } pub fn to_json(&self) -> Value { - use crate::util::format_date; + use util::format_date; json!({ "Id": self.uuid, "RevisionDate": format_date(&self.updated_at), @@ -73,7 +73,7 @@ impl Folder { { Ok(_) => Ok(()), // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first. - Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => { + Err(result::Error::DatabaseError(DatabaseErrorKind::ForeignKeyViolation, _)) => { diesel::update(folders::table) .filter(folders::uuid.eq(&self.uuid)) .set(FolderDb::to_db(self)) diff --git a/src/db/models/mod.rs b/src/db/models/mod.rs @@ -18,5 +18,5 @@ pub use self::org_policy::{OrgPolicy, OrgPolicyErr, OrgPolicyType}; pub use self::organization::{ Organization, OrganizationApiKey, UserOrgStatus, UserOrgType, UserOrganization, }; -pub use self::two_factor::{TwoFactor, TwoFactorType}; +pub use self::two_factor::{TwoFactor, TwoFactorType, WebAuthn, WebauthnRegistration}; pub use self::user::{User, UserKdfType, UserStampException}; diff --git a/src/db/models/org_policy.rs b/src/db/models/org_policy.rs @@ -2,14 +2,14 @@ use super::{TwoFactor, UserOrgStatus, UserOrgType, UserOrganization}; use crate::api::EmptyResult; use crate::db::DbConn; use crate::error::MapResult; -use crate::util::UpCase; +use crate::util::{self, UpCase}; +use diesel::result::{self, DatabaseErrorKind}; use serde::Deserialize; use serde_json::Value; db_object! { - #[derive(Identifiable, Queryable, Insertable, AsChangeset)] + #[derive(AsChangeset, Insertable, Queryable)] #[diesel(table_name = org_policies)] - #[diesel(primary_key(uuid))] pub struct OrgPolicy { uuid: String, org_uuid: String, @@ -53,7 +53,6 @@ struct ResetPasswordDataModel { AutoEnrollEnabled: bool, } type OrgPolicyResult = Result<(), OrgPolicyErr>; -#[derive(Debug)] pub enum OrgPolicyErr { TwoFactorMissing, SingleOrgEnforced, @@ -63,7 +62,7 @@ pub enum OrgPolicyErr { impl OrgPolicy { pub fn new(org_uuid: String, atype: OrgPolicyType, data: String) -> Self { Self { - uuid: crate::util::get_uuid(), + uuid: util::get_uuid(), org_uuid, atype: i32::from(atype), enabled: false, @@ -95,7 +94,7 @@ impl OrgPolicy { { Ok(_) => Ok(()), // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first. - Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => { + Err(result::Error::DatabaseError(DatabaseErrorKind::ForeignKeyViolation, _)) => { diesel::update(org_policies::table) .filter(org_policies::uuid.eq(&self.uuid)) .set(OrgPolicyDb::to_db(self)) diff --git a/src/db/models/organization.rs b/src/db/models/organization.rs @@ -1,13 +1,15 @@ use super::{CollectionUser, OrgPolicy, OrgPolicyType, TwoFactor, User}; +use crate::crypto; +use crate::util; use chrono::{NaiveDateTime, Utc}; +use diesel::result::{self, DatabaseErrorKind}; use num_traits::FromPrimitive; use serde_json::Value; use std::cmp::Ordering; db_object! { - #[derive(Identifiable, Queryable, Insertable, AsChangeset)] + #[derive(AsChangeset, Insertable, Queryable)] #[diesel(table_name = organizations)] - #[diesel(primary_key(uuid))] pub struct Organization { pub uuid: String, pub name: String, @@ -16,9 +18,8 @@ db_object! { pub public_key: Option<String>, } - #[derive(Identifiable, Queryable, Insertable, AsChangeset)] + #[derive(AsChangeset, Insertable, Queryable)] #[diesel(table_name = users_organizations)] - #[diesel(primary_key(uuid))] pub struct UserOrganization { pub uuid: String, pub user_uuid: String, @@ -31,9 +32,8 @@ db_object! { pub external_id: Option<String>, } - #[derive(Identifiable, Queryable, Insertable, AsChangeset)] + #[derive(AsChangeset, Insertable, Queryable)] #[diesel(table_name = organization_api_key)] - #[diesel(primary_key(uuid, org_uuid))] pub struct OrganizationApiKey { pub uuid: String, pub org_uuid: String, @@ -59,7 +59,7 @@ impl From<UserOrgStatus> for i32 { } } -#[derive(Copy, Clone, PartialEq, Eq, num_derive::FromPrimitive)] +#[derive(Clone, Copy, Eq, PartialEq, num_derive::FromPrimitive)] pub enum UserOrgType { Owner = 0, Admin = 1, @@ -227,7 +227,7 @@ static ACTIVATE_REVOKE_DIFF: i32 = 128i32; impl UserOrganization { pub fn new(user_uuid: String, org_uuid: String) -> Self { Self { - uuid: crate::util::get_uuid(), + uuid: util::get_uuid(), user_uuid, org_uuid, access_all: false, @@ -280,7 +280,7 @@ impl UserOrganization { impl OrganizationApiKey { pub fn new(org_uuid: String, api_key: String) -> Self { Self { - uuid: crate::util::get_uuid(), + uuid: util::get_uuid(), org_uuid, atype: 0, // Type 0 is the default and only type we support currently api_key, @@ -288,7 +288,7 @@ impl OrganizationApiKey { } } pub fn check_valid_api_key(&self, api_key: &str) -> bool { - crate::crypto::ct_eq(&self.api_key, api_key) + crypto::ct_eq(&self.api_key, api_key) } } @@ -310,7 +310,7 @@ impl Organization { { Ok(_) => Ok(()), // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first. - Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => { + Err(result::Error::DatabaseError(DatabaseErrorKind::ForeignKeyViolation, _)) => { diesel::update(organizations::table) .filter(organizations::uuid.eq(&self.uuid)) .set(OrganizationDb::to_db(self)) @@ -450,7 +450,7 @@ impl UserOrganization { { Ok(_) => Ok(()), // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first. - Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => { + Err(result::Error::DatabaseError(DatabaseErrorKind::ForeignKeyViolation, _)) => { diesel::update(users_organizations::table) .filter(users_organizations::uuid.eq(&self.uuid)) .set(UserOrganizationDb::to_db(self)) @@ -716,7 +716,7 @@ impl OrganizationApiKey { { Ok(_) => Ok(()), // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first. - Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => { + Err(result::Error::DatabaseError(DatabaseErrorKind::ForeignKeyViolation, _)) => { diesel::update(organization_api_key::table) .filter(organization_api_key::uuid.eq(&self.uuid)) .set(OrganizationApiKeyDb::to_db(self)) diff --git a/src/db/models/two_factor.rs b/src/db/models/two_factor.rs @@ -1,10 +1,12 @@ -use crate::{api::EmptyResult, db::DbConn, error::MapResult}; +use crate::{api::EmptyResult, db::DbConn, error::Error, util}; +use num_traits::FromPrimitive; use serde_json::Value; +use tokio::task; +use webauthn_rs::prelude::{CredentialID, SecurityKey}; db_object! { - #[derive(Identifiable, Queryable, Insertable, AsChangeset)] + #[derive(AsChangeset, Insertable, Queryable)] #[diesel(table_name = twofactor)] - #[diesel(primary_key(uuid))] pub struct TwoFactor { uuid: String, user_uuid: String, @@ -13,6 +15,37 @@ db_object! { pub data: String, last_used: i64, } + #[derive(Insertable)] + #[diesel(table_name = webauthn)] + pub struct WebAuthn { + pub credential_id: String, + uuid: String, + } +} +#[derive(Deserialize, Serialize)] +pub struct WebauthnRegistration { + pub id: u32, + pub name: String, + pub security_key: SecurityKey, +} + +impl WebauthnRegistration { + pub fn to_json(&self) -> Value { + json!({ + "id": self.id, + "name": self.name, + "migrated": false, + }) + } + pub fn to_security_keys(source: Vec<Self>) -> Vec<SecurityKey> { + let len = source.len(); + source + .into_iter() + .fold(Vec::with_capacity(len), |mut keys, reg| { + keys.push(reg.security_key); + keys + }) + } } impl TwoFactor { pub fn last_used(&self) -> u64 { @@ -21,6 +54,15 @@ impl TwoFactor { pub fn set_last_used(&mut self, last: u64) { self.last_used = i64::try_from(last).expect("overflow"); } + pub fn get_webauthn_registrations(&self) -> Result<Vec<WebauthnRegistration>, Error> { + serde_json::from_str(&self.data).map_err(Error::from) + } + pub fn create_webauthn(&self, credential_id: String) -> WebAuthn { + WebAuthn { + credential_id, + uuid: self.uuid.clone(), + } + } } #[derive(num_derive::FromPrimitive)] @@ -42,12 +84,11 @@ impl From<TwoFactorType> for i32 { } } } - /// Local methods impl TwoFactor { pub fn new(user_uuid: String, atype: TwoFactorType, data: String) -> Self { Self { - uuid: crate::util::get_uuid(), + uuid: util::get_uuid(), user_uuid, atype: i32::from(atype), enabled: true, @@ -67,34 +108,256 @@ impl TwoFactor { /// Database methods impl TwoFactor { + #[allow(clippy::clone_on_ref_ptr, clippy::shadow_unrelated)] pub async fn save(&self, conn: &DbConn) -> EmptyResult { - db_run! { conn: - { - match diesel::replace_into(twofactor::table) - .values(TwoFactorDb::to_db(self)) - .execute(conn) - { - Ok(_) => Ok(()), - // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first. - Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => { - diesel::update(twofactor::table) - .filter(twofactor::uuid.eq(&self.uuid)) - .set(TwoFactorDb::to_db(self)) - .execute(conn) - .map_res("Error saving twofactor") + if matches!(TwoFactorType::from_i32(self.atype), Some(tf) if matches!(tf, TwoFactorType::Webauthn | TwoFactorType::WebauthnLoginChallenge | TwoFactorType::WebauthnRegisterChallenge)) + { + err!("TwoFactor::save must not be called when atype is Webauthn, WebauthnLoginChallenge, or WebauthnRegisterChallenge") + } + use crate::db::__sqlite_schema::twofactor; + use __sqlite_model::TwoFactorDb; + use diesel::prelude::RunQueryDsl; + use diesel::result; + let mut con_res = conn.conn.clone().lock_owned().await; + let con = con_res.as_mut().expect("unable to get a pooled connection"); + task::block_in_place(move || { + diesel::replace_into(twofactor::table) + .values(TwoFactorDb::to_db(self)) + .execute(con) + .map_err(result::Error::into) + .map(|_| ()) + }) + } + #[allow(clippy::clone_on_ref_ptr)] + pub async fn replace_challenge(&self, conn: &DbConn) -> EmptyResult { + if !matches!(TwoFactorType::from_i32(self.atype), Some(tf) if matches!(tf, TwoFactorType::WebauthnLoginChallenge | TwoFactorType::WebauthnRegisterChallenge)) + { + err!("TwoFactor::replace_challenge must only be called when atype is WebauthnLoginChallenge or WebauthnRegisterChallenge") + } + use crate::db::__sqlite_schema::twofactor; + use __sqlite_model::TwoFactorDb; + use diesel::prelude::RunQueryDsl; + use diesel::result; + let mut con_res = conn.conn.clone().lock_owned().await; + let con = con_res.as_mut().expect("unable to get a pooled connection"); + task::block_in_place(move || { + diesel::replace_into(twofactor::table) + .values(TwoFactorDb::to_db(self)) + .execute(con) + .map_err(result::Error::into) + .and_then(|count| { + if count == 1 { + Ok(()) + } else { + Err(Error::from(String::from( + "exactly one webauthn challenge would not have been replaced in twofactor", + ))) } - Err(e) => Err(e.into()), - }.map_res("Error saving twofactor") - } + }) + }) + } + #[allow(clippy::clone_on_ref_ptr)] + pub async fn delete_challenge(self, conn: &DbConn) -> EmptyResult { + if !matches!(TwoFactorType::from_i32(self.atype), Some(tf) if matches!(tf, TwoFactorType::WebauthnLoginChallenge | TwoFactorType::WebauthnRegisterChallenge)) + { + err!("TwoFactor::delete_challenge must only be called when atype is WebauthnLoginChallenge or WebauthnRegisterChallenge") } + use crate::db::__sqlite_schema::twofactor; + use diesel::prelude::{ExpressionMethods, RunQueryDsl}; + use diesel::result; + let mut con_res = conn.conn.clone().lock_owned().await; + let con = con_res.as_mut().expect("unable to get a pooled connection"); + task::block_in_place(move || { + diesel::delete(twofactor::table) + .filter(twofactor::uuid.eq(self.uuid)) + .execute(con) + .map_err(result::Error::into) + .and_then(|count| { + if count == 1 { + Ok(()) + } else { + Err(Error::from(String::from( + "exactly one webauthn challenge would not have been deleted from twofactor", + ))) + } + }) + }) } - + #[allow(clippy::clone_on_ref_ptr)] + pub async fn update_webauthn(&self, conn: &DbConn) -> EmptyResult { + if !matches!(TwoFactorType::from_i32(self.atype), Some(tf) if matches!(tf, TwoFactorType::Webauthn)) + { + err!("TwoFactor::update_webauthn must only be called when atype is Webauthn") + } + use crate::db::__sqlite_schema::twofactor; + use diesel::prelude::{ExpressionMethods, RunQueryDsl}; + use diesel::result; + let mut con_res = conn.conn.clone().lock_owned().await; + let con = con_res.as_mut().expect("unable to get a pooled connection"); + task::block_in_place(move || { + diesel::update(twofactor::table) + .set(twofactor::data.eq(&self.data)) + .filter(twofactor::uuid.eq(&self.uuid)) + .execute(con) + .map_err(result::Error::into) + .and_then(|count| { + if count == 1 { + Ok(()) + } else { + Err(Error::from(String::from( + "exactly one webauthn entry would not have been updated in twofactor", + ))) + } + }) + }) + } + #[allow(clippy::clone_on_ref_ptr, clippy::shadow_unrelated)] pub async fn delete(self, conn: &DbConn) -> EmptyResult { - db_run! { conn: { - diesel::delete(twofactor::table.filter(twofactor::uuid.eq(self.uuid))) - .execute(conn) - .map_res("Error deleting twofactor") - }} + use crate::db::__sqlite_schema::{twofactor, webauthn}; + use diesel::prelude::{Connection, ExpressionMethods, RunQueryDsl}; + use diesel::result; + let mut con_res = conn.conn.clone().lock_owned().await; + let con = con_res.as_mut().expect("unable to get a pooled connection"); + task::block_in_place(move || { + con.transaction(|con| { + diesel::delete(webauthn::table) + .filter(webauthn::uuid.eq(&self.uuid)) + .execute(con) + .and_then(|_| { + diesel::delete(twofactor::table) + .filter(twofactor::uuid.eq(self.uuid)) + .execute(con) + }) + }) + .map_err(result::Error::into) + .map(|_| ()) + }) + } + #[allow(clippy::clone_on_ref_ptr, clippy::shadow_unrelated)] + pub async fn update_delete_webauthn(&self, cred_id: String, conn: &DbConn) -> EmptyResult { + if !matches!(TwoFactorType::from_i32(self.atype), Some(tf) if matches!(tf, TwoFactorType::Webauthn)) + { + err!("TwoFactor::update_delete_webauthn must only be called when atype is Webauthn") + } + use crate::db::__sqlite_schema::{twofactor, webauthn}; + use diesel::prelude::{Connection, ExpressionMethods, RunQueryDsl}; + use diesel::result; + let mut con_res = conn.conn.clone().lock_owned().await; + let con = con_res.as_mut().expect("unable to get a pooled connection"); + task::block_in_place(move || { + con.transaction(|con| { + diesel::delete(webauthn::table) + .filter(webauthn::credential_id.eq(cred_id)) + // We add this filter to ensure that the passed + // Credential ID is associated with the correct UUID. + .filter(webauthn::uuid.eq(&self.uuid)) + .execute(con) + .map_err(result::Error::into) + .and_then(|count| { + if count == 1 { + diesel::update(twofactor::table) + .set(twofactor::data.eq(&self.data)) + .filter(twofactor::uuid.eq(&self.uuid)) + .execute(con) + .map_err(result::Error::into) + .and_then(|count| { + if count == 1 { + Ok(()) + } else { + Err(Error::from(String::from( + "exactly one webauthn entry in twofactor would not have been updated", + ))) + } + }) + } else { + Err(Error::from(String::from( + "exactly one entry would not have been deleted from webauthn", + ))) + } + }) + }) + }) + } + #[allow(clippy::clone_on_ref_ptr, clippy::shadow_unrelated)] + pub async fn insert_insert_webauthn(&self, authn: WebAuthn, conn: &DbConn) -> EmptyResult { + if !matches!(TwoFactorType::from_i32(self.atype), Some(tf) if matches!(tf, TwoFactorType::Webauthn)) + { + err!("TwoFactor::insert_insert_webauthn must only be called when atype is Webauthn") + } + use crate::db::__sqlite_schema::{twofactor, webauthn}; + use __sqlite_model::{TwoFactorDb, WebAuthnDb}; + use diesel::prelude::{Connection, RunQueryDsl}; + use diesel::result; + let mut con_res = conn.conn.clone().lock_owned().await; + let con = con_res.as_mut().expect("unable to get a pooled connection"); + task::block_in_place(move || { + con.transaction(|con| { + diesel::insert_into(twofactor::table) + .values(TwoFactorDb::to_db(self)) + .execute(con) + .map_err(result::Error::into) + .and_then(|count| { + if count == 1 { + diesel::insert_into(webauthn::table) + .values(WebAuthnDb::to_db(&authn)) + .execute(con) + .map_err(result::Error::into) + .and_then(|count| { + if count == 1 { + Ok(()) + } else { + Err(Error::from(String::from( + "exactly one entry would not have been inserted into webauthn", + ))) + } + }) + } else { + Err(Error::from(String::from("exactly one webauthn entry would have not been inserted into twofactor"))) + } + }) + }) + }) + } + #[allow(clippy::clone_on_ref_ptr, clippy::shadow_unrelated)] + pub async fn update_insert_webauthn(&self, authn: WebAuthn, conn: &DbConn) -> EmptyResult { + if !matches!(TwoFactorType::from_i32(self.atype), Some(tf) if matches!(tf, TwoFactorType::Webauthn)) + { + err!("TwoFactor::update_insert_webauthn must only be called when atype is Webauthn") + } + use crate::db::__sqlite_schema::{twofactor, webauthn}; + use __sqlite_model::WebAuthnDb; + use diesel::prelude::{Connection, ExpressionMethods, RunQueryDsl}; + use diesel::result; + let mut con_res = conn.conn.clone().lock_owned().await; + let con = con_res.as_mut().expect("unable to get a pooled connection"); + task::block_in_place(move || { + con.transaction(|con| { + diesel::update(twofactor::table) + .set(twofactor::data.eq(&self.data)) + .filter(twofactor::uuid.eq(&self.uuid)) + .execute(con) + .map_err(result::Error::into) + .and_then(|count| { + if count == 1 { + diesel::insert_into(webauthn::table) + .values(WebAuthnDb::to_db(&authn)) + .execute(con) + .map_err(result::Error::into) + .and_then(|count| { + if count == 1 { + Ok(()) + } else { + Err(Error::from(String::from( + "exactly one entry would not have been inserted into webauthn", + ))) + } + }) + } else { + Err(Error::from(String::from("exactly one webauthn entry would have not been updated in twofactor"))) + } + }) + }) + }) } pub async fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> { @@ -118,12 +381,70 @@ impl TwoFactor { .from_db() }} } - + #[allow(clippy::clone_on_ref_ptr, clippy::shadow_unrelated)] pub async fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> EmptyResult { - db_run! { conn: { - diesel::delete(twofactor::table.filter(twofactor::user_uuid.eq(user_uuid))) - .execute(conn) - .map_res("Error deleting twofactors") - }} + use crate::db::__sqlite_schema::{twofactor, webauthn}; + use diesel::prelude::{Connection, ExpressionMethods, QueryDsl, RunQueryDsl}; + use diesel::result; + let mut con_res = conn.conn.clone().lock_owned().await; + let con = con_res.as_mut().expect("unable to get a pooled connection"); + task::block_in_place(move || { + con.transaction(|con| { + diesel::delete(webauthn::table) + .filter( + webauthn::uuid.eq_any( + twofactor::table + .filter(twofactor::user_uuid.eq(user_uuid)) + .select(twofactor::uuid), + ), + ) + .execute(con) + .and_then(|_| { + diesel::delete(twofactor::table) + .filter(twofactor::user_uuid.eq(user_uuid)) + .execute(con) + }) + }) + .map_err(result::Error::into) + .map(|_| ()) + }) + } +} +impl WebAuthn { + #[allow(clippy::clone_on_ref_ptr, clippy::shadow_unrelated)] + pub async fn get_all_credentials_by_user( + user_uuid: &str, + conn: &DbConn, + ) -> Result<Vec<CredentialID>, Error> { + use crate::db::__sqlite_schema::{twofactor, webauthn}; + use diesel::prelude::{ExpressionMethods, QueryDsl, RunQueryDsl}; + use diesel::result; + let mut con_res = conn.conn.clone().lock_owned().await; + let con = con_res.as_mut().expect("unable to get a pooled connection"); + task::block_in_place(move || { + webauthn::table + .select(webauthn::credential_id) + .filter( + webauthn::uuid.eq_any( + twofactor::table + .filter(twofactor::user_uuid.eq(user_uuid)) + .select(twofactor::uuid), + ), + ) + .load::<String>(con) + .map_err(result::Error::into) + .and_then(|ids| { + let len = ids.len(); + ids.into_iter() + .try_fold(Vec::with_capacity(len), |mut cred_ids, id| { + CredentialID::try_from(id.as_str()) + .map_err(|()| Error::from(String::from("invalid credential ID"))) + .map(|cred_id| { + cred_ids.push(cred_id); + cred_ids + }) + }) + }) + }) } } diff --git a/src/db/models/user.rs b/src/db/models/user.rs @@ -1,6 +1,8 @@ use crate::config; use crate::crypto; +use crate::util; use chrono::{Duration, NaiveDateTime, Utc}; +use diesel::result::{self, DatabaseErrorKind}; use serde_json::Value; db_object! { @@ -90,7 +92,7 @@ impl User { let now = Utc::now().naive_utc(); let email = email.to_lowercase(); Self { - uuid: crate::util::get_uuid(), + uuid: util::get_uuid(), enabled: true, created_at: now, updated_at: now, @@ -106,7 +108,7 @@ impl User { salt: crypto::get_random_bytes::<64>().to_vec(), password_iterations: i32::try_from(config::get_config().password_iterations) .expect("overflow"), - security_stamp: crate::util::get_uuid(), + security_stamp: util::get_uuid(), stamp_exception: None, password_hint: None, private_key: None, @@ -169,12 +171,12 @@ impl User { pub fn check_valid_recovery_code(&self, recovery_code: &str) -> bool { self.totp_recover.as_ref().map_or(false, |totp_recover| { - crate::crypto::ct_eq(recovery_code, totp_recover.to_lowercase()) + crypto::ct_eq(recovery_code, totp_recover.to_lowercase()) }) } pub fn check_valid_api_key(&self, key: &str) -> bool { - matches!(self.api_key, Some(ref api_key) if crate::crypto::ct_eq(api_key, key)) + matches!(self.api_key, Some(ref api_key) if crypto::ct_eq(api_key, key)) } /// Set the password hash generated @@ -213,7 +215,7 @@ impl User { } pub fn reset_security_stamp(&mut self) { - self.security_stamp = crate::util::get_uuid(); + self.security_stamp = util::get_uuid(); } /// Set the stamp_exception to only allow a subsequent request matching a specific route using the current security-stamp. @@ -299,7 +301,7 @@ impl User { { Ok(_) => Ok(()), // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first. - Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => { + Err(result::Error::DatabaseError(DatabaseErrorKind::ForeignKeyViolation, _)) => { diesel::update(users::table) .filter(users::uuid.eq(&self.uuid)) .set(UserDb::to_db(self)) @@ -348,7 +350,7 @@ impl User { pub async fn update_all_revisions(conn: &DbConn) -> EmptyResult { let updated_at = Utc::now().naive_utc(); db_run! {conn: { - crate::util::retry(|| { + util::retry(|| { diesel::update(users::table) .set(users::updated_at.eq(updated_at)) .execute(conn) @@ -364,7 +366,7 @@ impl User { async fn _update_revision(uuid: &str, date: &NaiveDateTime, conn: &DbConn) -> EmptyResult { db_run! {conn: { - crate::util::retry(|| { + util::retry(|| { diesel::update(users::table.filter(users::uuid.eq(uuid))) .set(users::updated_at.eq(date)) .execute(conn) diff --git a/src/db/schemas/sqlite/schema.rs b/src/db/schemas/sqlite/schema.rs @@ -195,8 +195,16 @@ table! { } } +table! { + webauthn (credential_id) { + credential_id -> Text, + uuid -> Text, + } +} + joinable!(folders_ciphers -> ciphers (cipher_uuid)); joinable!(folders_ciphers -> folders (folder_uuid)); +allow_tables_to_appear_in_same_query!(twofactor, webauthn,); allow_tables_to_appear_in_same_query!( ciphers, ciphers_collections, diff --git a/src/error.rs b/src/error.rs @@ -1,12 +1,14 @@ // // Error generator macro // +use core::fmt::{self, Debug, Display, Formatter}; use std::error::Error as StdError; macro_rules! make_error { ( $( $name:ident ( $ty:ty ): $src_fn:expr, $usr_msg_fun:expr ),+ $(,)? ) => { const BAD_REQUEST: u16 = 400; enum ErrorKind { $($name( $ty )),+ } + #[allow(clippy::error_impl_error)] pub struct Error { message: String, error: ErrorKind, error_code: u16 } $(impl From<$ty> for Error { @@ -22,8 +24,8 @@ macro_rules! make_error { match self.error {$( ErrorKind::$name(ref e) => $src_fn(e), )+} } } - impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + impl Display for Error { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self.error {$( ErrorKind::$name(ref e) => f.write_str(&$usr_msg_fun(e, &self.message)), )+} @@ -31,6 +33,7 @@ macro_rules! make_error { } }; } +use core::any::Any; #[cfg(not(all(feature = "priv_sep", target_os = "openbsd")))] use core::convert::Infallible; use diesel::r2d2::PoolError as R2d2Err; @@ -46,7 +49,7 @@ use serde_json::{Error as SerdeErr, Value}; use std::io::Error as IoErr; use std::time::SystemTimeError as TimeErr; use tokio_tungstenite::tungstenite::Error as TungstError; -use webauthn_rs::error::WebauthnError as WebauthnErr; +use webauthn_rs::prelude::WebauthnError as WebauthnErr; #[derive(Serialize)] struct Empty; @@ -117,8 +120,8 @@ impl From<Infallible> for Error { match value {} } } -impl std::fmt::Debug for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Debug for Error { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self.source() { Some(e) => write!(f, "{}.\n[CAUSE] {:#?}", self.message, e), None => match self.error { @@ -131,7 +134,7 @@ impl std::fmt::Debug for Error { } } #[cfg(all(feature = "priv_sep", target_os = "openbsd"))] - ErrorKind::Unveil(ref err) => err.fmt(f), + ErrorKind::Unveil(ref err) => Display::fmt(err, f), ErrorKind::Json(_) => write!(f, "{}", self.message), ErrorKind::Db(_) | ErrorKind::R2d2(_) @@ -203,7 +206,7 @@ fn _serialize(e: &impl serde::Serialize, _msg: &str) -> String { serde_json::to_string(e).unwrap() } -fn _api_error(_: &impl std::any::Any, msg: &str) -> String { +fn _api_error(_: &impl Any, msg: &str) -> String { let json = json!({ "Message": msg, "error": "", diff --git a/src/main.rs b/src/main.rs @@ -14,10 +14,8 @@ clippy::suspicious )] #![allow( - clippy::absolute_paths, clippy::blanket_clippy_restriction_lints, clippy::doc_markdown, - clippy::error_impl_error, clippy::expect_used, clippy::if_then_some_else_none, clippy::implicit_return, @@ -63,8 +61,6 @@ extern crate alloc; #[macro_use] extern crate diesel; #[macro_use] -extern crate diesel_migrations; -#[macro_use] mod error; #[macro_use] extern crate rocket; @@ -85,6 +81,7 @@ pub use error::{Error, MapResult}; use std::env; use std::{fs, path::Path, process}; use tokio::runtime::Builder; +use tokio::signal; pub const VERSION: &str = env!("CARGO_PKG_VERSION"); fn main() -> Result<(), Error> { @@ -196,7 +193,7 @@ async fn launch_rocket(pool: db::DbPool) -> Result<(), Error> { .await?; let shutdown = instance.shutdown(); tokio::spawn(async move { - tokio::signal::ctrl_c() + signal::ctrl_c() .await .expect("Error setting Ctrl-C handler"); shutdown.notify(); diff --git a/src/util.rs b/src/util.rs @@ -1,4 +1,5 @@ use crate::config; +use core::fmt::{self, Display, Formatter}; use rocket::{ fairing::{Fairing, Info, Kind}, http::{ContentType, Header, HeaderMap, Method, Status}, @@ -6,7 +7,7 @@ use rocket::{ response::{self, Responder}, Request, Response, }; -use std::{io::Cursor, ops::Deref}; +use std::{error, io::Cursor, ops::Deref, string::ToString}; use tokio::{ runtime::Handle, time::{sleep, Duration}, @@ -117,7 +118,7 @@ impl Cors { fn get_header(headers: &HeaderMap<'_>, name: &str) -> String { headers .get_one(name) - .map_or_else(String::new, std::string::ToString::to_string) + .map_or_else(String::new, ToString::to_string) } // Check a request's `Origin` header against the list of allowed origins. // If a match exists, return it. Otherwise, return None. @@ -215,8 +216,8 @@ impl<'r, R: 'r + Responder<'r, 'static> + Send> Responder<'r, 'static> for Cache } pub struct SafeString(String); -impl std::fmt::Display for SafeString { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl Display for SafeString { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { self.0.fmt(f) } } @@ -305,7 +306,6 @@ fn format_datetime_http(dt: &DateTime<Local>) -> String { } use serde::de::{self, DeserializeOwned, Deserializer, MapAccess, SeqAccess, Visitor}; use serde_json::{self, Value}; -use std::fmt; type JsonMap = serde_json::Map<String, Value>; #[derive(Serialize, Deserialize)] @@ -329,7 +329,7 @@ struct UpCaseVisitor; impl<'de> Visitor<'de> for UpCaseVisitor { type Value = Value; - fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result { formatter.write_str("an object or an array") } fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error> @@ -409,7 +409,7 @@ where pub async fn retry_db<F, T: Send, E>(mut func: F, max_tries: u32) -> Result<T, E> where F: FnMut() -> Result<T, E> + Send, - E: std::error::Error + Send, + E: error::Error + Send, { let mut tries = 0u32; loop {