diff --git a/.typos.toml b/.typos.toml index 7034a849..87c0c4a6 100644 --- a/.typos.toml +++ b/.typos.toml @@ -15,7 +15,6 @@ extend-ignore-re = [ "(?i)helo_name", "Server name sent during.+HELO", # COSE Is short for CBOR Object Signing and Encryption, ignore these specific items - "COSE", "COSEKey", "COSEAlgorithm", # Ignore this specific string as it's valid diff --git a/Cargo.lock b/Cargo.lock index 21defe79..b0a36edf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -934,6 +934,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + [[package]] name = "chacha20" version = "0.10.1" @@ -3228,6 +3234,18 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" +[[package]] +name = "nix" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" +dependencies = [ + "bitflags 2.13.1", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "nom" version = "7.1.3" @@ -5867,6 +5885,7 @@ dependencies = [ "macros", "mimalloc", "moka", + "nix", "num-derive", "num-traits", "opendal", diff --git a/Cargo.toml b/Cargo.toml index 4f2fb283..3e187ff3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,6 +58,7 @@ oidc-accept-string-booleans = ["openidconnect/accept-string-booleans"] unstable = [] [target."cfg(unix)".dependencies] +nix = { version = "0.31.3", features = ["fs"] } # Logging syslog = "7.0.0" @@ -106,7 +107,7 @@ serde = { version = "1.0.229", features = ["derive"] } serde_json = "1.0.151" # A safe, extensible ORM and Query builder -diesel = { version = "2.3.11", features = ["chrono", "r2d2", "numeric", "64-column-tables"] } +diesel = { version = "2.3.11", features = ["chrono", "r2d2", "numeric"] } diesel_migrations = "2.3.2" derive_more = { version = "2.1.1", features = [ diff --git a/migrations/mysql/2026-08-01-120000_user_crypto_v2/down.sql b/migrations/mysql/2026-08-01-120000_user_crypto_v2/down.sql deleted file mode 100644 index 1649ec86..00000000 --- a/migrations/mysql/2026-08-01-120000_user_crypto_v2/down.sql +++ /dev/null @@ -1,6 +0,0 @@ -DROP TABLE IF EXISTS user_signature_key_pairs; - -ALTER TABLE users DROP COLUMN signed_public_key; -ALTER TABLE users DROP COLUMN security_state; -ALTER TABLE users DROP COLUMN security_version; -ALTER TABLE users DROP COLUMN v2_upgrade_token; diff --git a/migrations/mysql/2026-08-01-120000_user_crypto_v2/up.sql b/migrations/mysql/2026-08-01-120000_user_crypto_v2/up.sql deleted file mode 100644 index 8506c6ad..00000000 --- a/migrations/mysql/2026-08-01-120000_user_crypto_v2/up.sql +++ /dev/null @@ -1,17 +0,0 @@ -ALTER TABLE users ADD COLUMN signed_public_key TEXT; -ALTER TABLE users ADD COLUMN security_state TEXT; -ALTER TABLE users ADD COLUMN security_version INTEGER; -ALTER TABLE users ADD COLUMN v2_upgrade_token TEXT; - -DROP TABLE IF EXISTS user_signature_key_pairs; - -CREATE TABLE user_signature_key_pairs ( - uuid CHAR(36) NOT NULL PRIMARY KEY, - user_uuid CHAR(36) NOT NULL UNIQUE, - signature_algorithm INTEGER NOT NULL, -- 0 = ed25519, 1 = mldsa44 - signing_key TEXT NOT NULL, - verifying_key TEXT NOT NULL, - created_at DATETIME NOT NULL, - updated_at DATETIME NOT NULL, - FOREIGN KEY (user_uuid) REFERENCES users (uuid) ON DELETE CASCADE -); diff --git a/migrations/postgresql/2026-08-01-120000_user_crypto_v2/down.sql b/migrations/postgresql/2026-08-01-120000_user_crypto_v2/down.sql deleted file mode 100644 index 1649ec86..00000000 --- a/migrations/postgresql/2026-08-01-120000_user_crypto_v2/down.sql +++ /dev/null @@ -1,6 +0,0 @@ -DROP TABLE IF EXISTS user_signature_key_pairs; - -ALTER TABLE users DROP COLUMN signed_public_key; -ALTER TABLE users DROP COLUMN security_state; -ALTER TABLE users DROP COLUMN security_version; -ALTER TABLE users DROP COLUMN v2_upgrade_token; diff --git a/migrations/postgresql/2026-08-01-120000_user_crypto_v2/up.sql b/migrations/postgresql/2026-08-01-120000_user_crypto_v2/up.sql deleted file mode 100644 index 72fa6dc0..00000000 --- a/migrations/postgresql/2026-08-01-120000_user_crypto_v2/up.sql +++ /dev/null @@ -1,16 +0,0 @@ -ALTER TABLE users ADD COLUMN signed_public_key TEXT; -ALTER TABLE users ADD COLUMN security_state TEXT; -ALTER TABLE users ADD COLUMN security_version INTEGER; -ALTER TABLE users ADD COLUMN v2_upgrade_token TEXT; - -DROP TABLE IF EXISTS user_signature_key_pairs; - -CREATE TABLE user_signature_key_pairs ( - uuid CHAR(36) NOT NULL PRIMARY KEY, - user_uuid CHAR(36) NOT NULL UNIQUE REFERENCES users (uuid) ON DELETE CASCADE, - signature_algorithm INTEGER NOT NULL, -- 0 = ed25519, 1 = mldsa44 - signing_key TEXT NOT NULL, - verifying_key TEXT NOT NULL, - created_at TIMESTAMP NOT NULL, - updated_at TIMESTAMP NOT NULL -); diff --git a/migrations/sqlite/2026-08-01-120000_user_crypto_v2/down.sql b/migrations/sqlite/2026-08-01-120000_user_crypto_v2/down.sql deleted file mode 100644 index 1649ec86..00000000 --- a/migrations/sqlite/2026-08-01-120000_user_crypto_v2/down.sql +++ /dev/null @@ -1,6 +0,0 @@ -DROP TABLE IF EXISTS user_signature_key_pairs; - -ALTER TABLE users DROP COLUMN signed_public_key; -ALTER TABLE users DROP COLUMN security_state; -ALTER TABLE users DROP COLUMN security_version; -ALTER TABLE users DROP COLUMN v2_upgrade_token; diff --git a/migrations/sqlite/2026-08-01-120000_user_crypto_v2/up.sql b/migrations/sqlite/2026-08-01-120000_user_crypto_v2/up.sql deleted file mode 100644 index be3bfcdd..00000000 --- a/migrations/sqlite/2026-08-01-120000_user_crypto_v2/up.sql +++ /dev/null @@ -1,16 +0,0 @@ -ALTER TABLE users ADD COLUMN signed_public_key TEXT; -ALTER TABLE users ADD COLUMN security_state TEXT; -ALTER TABLE users ADD COLUMN security_version INTEGER; -ALTER TABLE users ADD COLUMN v2_upgrade_token TEXT; - -DROP TABLE IF EXISTS user_signature_key_pairs; - -CREATE TABLE user_signature_key_pairs ( - uuid TEXT NOT NULL PRIMARY KEY, - user_uuid TEXT NOT NULL UNIQUE REFERENCES users (uuid) ON DELETE CASCADE, - signature_algorithm INTEGER NOT NULL, -- 0 = ed25519, 1 = mldsa44 - signing_key TEXT NOT NULL, - verifying_key TEXT NOT NULL, - created_at DATETIME NOT NULL, - updated_at DATETIME NOT NULL -); diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index 0f77d2d7..0cb4d3c0 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -22,8 +22,7 @@ use crate::{ models::{ AuthRequest, AuthRequestId, Cipher, CipherId, Device, DeviceId, DeviceType, DeviceWithAuthRequest, EmergencyAccess, EmergencyAccessId, EventType, Folder, FolderId, Invitation, Membership, MembershipId, - OrgPolicy, OrgPolicyType, Organization, OrganizationId, Send, SendId, SignatureAlgorithm, User, UserId, - UserKdfType, UserSignatureKeyPair, + OrgPolicy, OrgPolicyType, Organization, OrganizationId, Send, SendId, User, UserId, UserKdfType, }, }, mail, @@ -42,8 +41,6 @@ pub fn routes() -> Vec { post_profile, put_avatar, get_public_keys, - get_account_public_keys, - get_keys, post_keys, post_password, post_set_password, @@ -105,9 +102,6 @@ pub struct RegisterData { #[serde(alias = "userAsymmetricKeys")] keys: Option, - // Supersedes `keys`, and the only way a v2 account can be registered. - account_keys: Option, - master_password_hint: Option, name: Option, @@ -122,6 +116,36 @@ pub struct RegisterData { org_invite_token: Option, } +impl RegisterData { + fn hash(&self) -> String { + self.compat.fold(|rdc| &rdc.master_password_hash, |rdcu| &rdcu.master_password_authentication.hash).to_owned() + } + + fn kdf(&self) -> &KDFData { + self.compat.fold(|rdc| &rdc.kdf, |rdcu| &rdcu.master_password_authentication.kdf) + } + + fn key(&self) -> String { + self.compat.fold(|rdc| &rdc.key, |rdcu| &rdcu.master_password_unlock.key).to_owned() + } + + // When comparing with salt, email need to be normalized: + // - https://github.com/bitwarden/clients/blob/web-v2026.5.0/libs/common/src/key-management/master-password/services/master-password.service.ts#L171 + fn unprocessable(&self) -> bool { + let mut unprocessable = false; + *self.compat.fold( + |_| &false, + |rdcu| { + let email = self.email.trim().to_lowercase(); + unprocessable = rdcu.master_password_authentication.kdf != rdcu.master_password_unlock.kdf + || rdcu.master_password_authentication.salt != email + || rdcu.master_password_unlock.salt != email; + &unprocessable + }, + ) + } +} + #[derive(Debug, Deserialize)] struct RegisterDataOld { #[serde(flatten)] @@ -159,34 +183,6 @@ impl RegisterDataCompat { RegisterDataCompat::RegisterDataCur(rdcu) => fcu(rdcu), } } - - fn hash(&self) -> String { - self.fold(|rdc| &rdc.master_password_hash, |rdcu| &rdcu.master_password_authentication.hash).to_owned() - } - - fn kdf(&self) -> &KDFData { - self.fold(|rdc| &rdc.kdf, |rdcu| &rdcu.master_password_authentication.kdf) - } - - fn key(&self) -> String { - self.fold(|rdc| &rdc.key, |rdcu| &rdcu.master_password_unlock.key).to_owned() - } - - // When comparing with salt, email need to be normalized: - // - https://github.com/bitwarden/clients/blob/web-v2026.5.0/libs/common/src/key-management/master-password/services/master-password.service.ts#L171 - fn unprocessable(&self, email: &str) -> bool { - let mut unprocessable = false; - *self.fold( - |_| &false, - |rdcu| { - let email = email.trim().to_lowercase(); - unprocessable = rdcu.master_password_authentication.kdf != rdcu.master_password_unlock.kdf - || rdcu.master_password_authentication.salt != email - || rdcu.master_password_unlock.salt != email; - &unprocessable - }, - ) - } } #[derive(Debug, Deserialize)] @@ -196,167 +192,6 @@ struct KeysData { public_key: String, } -/// The `accountKeys` payload, which replaces the flat `keys`/`userAsymmetricKeys` object. -/// -/// It carries either a "v1" state (just the encryption key pair) or a "v2" one, which adds a -/// signature key pair, a signed public key, and a signed security state. The two deprecated -/// top-level fields are still sent by the SDK alongside the nested ones and are only used as a -/// fallback for clients that don't send `publicKeyEncryptionKeyPair` yet. -/// -/// Ref: -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AccountKeysData { - user_key_encrypted_account_private_key: Option, - account_public_key: Option, - - public_key_encryption_key_pair: Option, - signature_key_pair: Option, - security_state: Option, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct PublicKeyEncryptionKeyPairData { - wrapped_private_key: String, - public_key: String, - signed_public_key: Option, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct SignatureKeyPairData { - signature_algorithm: String, - wrapped_signing_key: String, - verifying_key: String, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct SecurityStateData { - security_state: String, - security_version: i32, -} - -pub struct ValidatedAccountKeys { - private_key: String, - public_key: String, - v2: Option, -} - -struct ValidatedV2AccountKeys { - signed_public_key: String, - signing_key: String, - verifying_key: String, - signature_algorithm: SignatureAlgorithm, - security_state: String, - security_version: i32, -} - -impl AccountKeysData { - /// Checks that the payload describes a complete account cryptographic state. - /// - /// The v2 fields have to be all present or all absent: a client that receives a COSE-wrapped - /// private key without the matching signature key pair and security state refuses to unlock the - /// vault, so storing half a state would produce an account nobody can log into. - pub fn validate(self) -> ApiResult { - let (private_key, public_key, signed_public_key) = if let Some(key_pair) = self.public_key_encryption_key_pair { - (key_pair.wrapped_private_key, key_pair.public_key, key_pair.signed_public_key) - // Older clients only send the deprecated top-level fields, which are always v1. - } else if let (Some(private_key), Some(public_key)) = - (self.user_key_encrypted_account_private_key, self.account_public_key) - { - (private_key, public_key, None) - } else { - err!("The account keys are missing an encryption key pair") - }; - - let v2 = match (signed_public_key, self.signature_key_pair, self.security_state) { - (Some(signed_public_key), Some(signature_key_pair), Some(security_state)) => { - let Some(signature_algorithm) = SignatureAlgorithm::from_str(&signature_key_pair.signature_algorithm) - else { - err!(format!("Unsupported signature algorithm: {}", signature_key_pair.signature_algorithm)) - }; - - Some(ValidatedV2AccountKeys { - signed_public_key, - signing_key: signature_key_pair.wrapped_signing_key, - verifying_key: signature_key_pair.verifying_key, - signature_algorithm, - security_state: security_state.security_state, - security_version: security_state.security_version, - }) - } - (None, None, None) => None, - _ => err!( - "Invalid account keys: the signed public key, signature key pair and security state must either all be present or all be absent" - ), - }; - - Ok(ValidatedAccountKeys { - private_key, - public_key, - v2, - }) - } -} - -impl From for ValidatedAccountKeys { - fn from(keys: KeysData) -> Self { - Self { - private_key: keys.encrypted_private_key, - public_key: keys.public_key, - v2: None, - } - } -} - -impl ValidatedAccountKeys { - /// Writes the parts of the state that live on the user itself. The user still needs saving, and - /// [`Self::save_signature_key_pair`] still needs calling once it has been. - /// - /// Rejects downgrading an account from v2 back to v1. - pub fn apply(&self, user: &mut User) -> EmptyResult { - if user.is_v2() && self.v2.is_none() { - err!("Cannot downgrade an account from v2 to v1 encryption") - } - - user.private_key = Some(self.private_key.clone()); - user.public_key = Some(self.public_key.clone()); - - user.signed_public_key = self.v2.as_ref().map(|v2| v2.signed_public_key.clone()); - user.security_state = self.v2.as_ref().map(|v2| v2.security_state.clone()); - user.security_version = self.v2.as_ref().map(|v2| v2.security_version); - - Ok(()) - } - - /// Persists the signature key pair. Separate from [`Self::apply`] because the row has a foreign - /// key to the user, so it can only be written once the user exists. - pub async fn save_signature_key_pair(&self, user_id: &UserId, conn: &DbConn) -> EmptyResult { - // Skip if the account is v1, since v1 accounts don't have a signature key pair. - let Some(v2) = &self.v2 else { - return Ok(()); - }; - - let mut key_pair = match UserSignatureKeyPair::find_active_by_user(user_id, conn).await { - Some(mut key_pair) => { - key_pair.signature_algorithm = v2.signature_algorithm as i32; - key_pair.signing_key.clone_from(&v2.signing_key); - key_pair.verifying_key.clone_from(&v2.verifying_key); - key_pair - } - None => UserSignatureKeyPair::new( - user_id.clone(), - v2.signature_algorithm, - v2.signing_key.clone(), - v2.verifying_key.clone(), - ), - }; - key_pair.save(conn).await - } -} - #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct MasterPasswordAuthentication { @@ -381,12 +216,11 @@ pub struct MasterPasswordUnlock { #[serde(rename_all = "camelCase")] pub struct SetPasswordData { #[serde(flatten)] - compat: RegisterDataCompat, + kdf: KDFData, + key: String, keys: Option, - // Supersedes `keys`, and the only way a v2 account can be initialized here. - account_keys: Option, - + master_password_hash: String, master_password_hint: Option, org_identifier: Option, } @@ -429,7 +263,7 @@ pub async fn register(data: Json, email_verification: bool, conn: let mut pending_emergency_access = None; - if data.compat.unprocessable(&data.email) { + if data.unprocessable() { err_code!("Unexpected RegisterData format", Status::UnprocessableEntity.code); } @@ -552,9 +386,9 @@ pub async fn register(data: Json, email_verification: bool, conn: // Make sure we don't leave a lingering invitation. Invitation::take(&email, &conn).await; - set_kdf_data(&mut user, data.compat.kdf())?; + set_kdf_data(&mut user, data.kdf())?; - user.set_password(&data.compat.hash(), Some(data.compat.key()), true, None, &conn).await?; + user.set_password(&data.hash(), Some(data.key()), true, None, &conn).await?; user.password_hint = password_hint; // Add extra fields if present @@ -562,13 +396,9 @@ pub async fn register(data: Json, email_verification: bool, conn: user.name = name; } - let account_keys = match (data.account_keys, data.keys) { - (Some(account_keys), _) => Some(account_keys.validate()?), - (None, Some(keys)) => Some(keys.into()), - (None, None) => None, - }; - if let Some(ref account_keys) = account_keys { - account_keys.apply(&mut user)?; + if let Some(keys) = data.keys { + user.private_key = Some(keys.encrypted_private_key); + user.public_key = Some(keys.public_key); } if email_verified { @@ -592,10 +422,6 @@ pub async fn register(data: Json, email_verification: bool, conn: user.save(&conn).await?; - if let Some(account_keys) = account_keys { - account_keys.save_signature_key_pair(&user.uuid, &conn).await?; - } - // accept any open emergency access invitations if !CONFIG.mail_enabled() && CONFIG.emergency_access_allowed() { for mut emergency_invite in EmergencyAccess::find_all_invited_by_grantee_email(&user.email, &conn).await { @@ -618,26 +444,16 @@ async fn post_set_password(data: Json, headers: Headers, conn: err!("Account already initialized, cannot set password") } - if data.compat.unprocessable(&user.email) { - err_code!("Unexpected SetPasswordData format", Status::UnprocessableEntity.code); - } - // Check against the password hint setting here so if it fails, // the user can retry without losing their invitation below. let password_hint = clean_password_hint(data.master_password_hint.as_ref()); enforce_password_hint_setting(password_hint.as_ref())?; - let account_keys = match (data.account_keys, data.keys) { - (Some(account_keys), _) => Some(account_keys.validate()?), - (None, Some(keys)) => Some(keys.into()), - (None, None) => None, - }; - - set_kdf_data(&mut user, data.compat.kdf())?; + set_kdf_data(&mut user, &data.kdf)?; user.set_password( - &data.compat.hash(), - Some(data.compat.key()), + &data.master_password_hash, + Some(data.key), false, Some(vec![String::from("revision_date")]), // We need to allow revision-date to use the old security_timestamp &conn, @@ -645,8 +461,9 @@ async fn post_set_password(data: Json, headers: Headers, conn: .await?; user.password_hint = password_hint; - if let Some(ref account_keys) = account_keys { - account_keys.apply(&mut user)?; + if let Some(keys) = data.keys { + user.private_key = Some(keys.encrypted_private_key); + user.public_key = Some(keys.public_key); } if let Some(identifier) = data.org_identifier @@ -675,10 +492,6 @@ async fn post_set_password(data: Json, headers: Headers, conn: user.save(&conn).await?; - if let Some(account_keys) = account_keys { - account_keys.save_signature_key_pair(&user.uuid, &conn).await?; - } - Ok(Json(json!({ "object": "set-password", "captchaBypassToken": "", @@ -760,60 +573,20 @@ async fn get_public_keys(user_id: UserId, _headers: Headers, conn: DbConn) -> Js }))) } -#[get("/users//keys")] -async fn get_account_public_keys(user_id: UserId, _headers: Headers, conn: DbConn) -> JsonResult { - let user = match User::find_by_uuid(&user_id, &conn).await { - Some(user) if user.public_key.is_some() => user, - Some(_) => err_code!("User has no public_key", Status::NotFound.code), - None => err_code!("User doesn't exist", Status::NotFound.code), - }; - - Ok(Json(user.public_keys_json(&conn).await)) -} - -#[get("/accounts/keys")] -async fn get_keys(headers: Headers, conn: DbConn) -> JsonResult { - let user = headers.user; - - Ok(Json(json!({ - "key": user.akey, - "privateKey": user.private_key, - "publicKey": user.public_key, - "accountKeys": user.account_keys_json(&conn).await, - "object": "keys" - }))) -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct PostKeysData { - #[serde(flatten)] - keys: Option, - account_keys: Option, -} - #[post("/accounts/keys", data = "")] -async fn post_keys(data: Json, headers: Headers, conn: DbConn) -> JsonResult { - let data: PostKeysData = data.into_inner(); +async fn post_keys(data: Json, headers: Headers, conn: DbConn) -> JsonResult { + let data: KeysData = data.into_inner(); let mut user = headers.user; - // `accountKeys` supersedes the flat `keys` object when both are sent. - let account_keys = match (data.account_keys, data.keys) { - (Some(account_keys), _) => account_keys.validate()?, - (None, Some(keys)) => keys.into(), - (None, None) => err!("No account keys provided"), - }; + user.private_key = Some(data.encrypted_private_key); + user.public_key = Some(data.public_key); - account_keys.apply(&mut user)?; user.save(&conn).await?; - account_keys.save_signature_key_pair(&user.uuid, &conn).await?; Ok(Json(json!({ - "key": user.akey, "privateKey": user.private_key, "publicKey": user.public_key, - "accountKeys": user.account_keys_json(&conn).await, "object":"keys" }))) } diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index 217ae3dc..2b51fd0c 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -198,7 +198,6 @@ async fn sync(data: SyncData, headers: Headers, client_version: Option Result<(), Error> { } #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - if !metadata.permissions().mode() & 0o111 != 0 { - err!(format!("sendmail command at `{path:?}` isn't executable")); - } + if nix::unistd::access(&path, nix::unistd::AccessFlags::X_OK).is_err() { + err!(format!("sendmail command at `{path:?}` isn't executable")); } } } diff --git a/src/db/models/mod.rs b/src/db/models/mod.rs index ac45f234..0ed8ef91 100644 --- a/src/db/models/mod.rs +++ b/src/db/models/mod.rs @@ -17,7 +17,6 @@ mod two_factor; mod two_factor_duo_context; mod two_factor_incomplete; mod user; -mod user_signature_key_pair; pub use self::archive::Archive; pub use self::attachment::{Attachment, AttachmentId}; @@ -41,4 +40,3 @@ pub use self::two_factor::{TwoFactor, TwoFactorType}; pub use self::two_factor_duo_context::TwoFactorDuoContext; pub use self::two_factor_incomplete::TwoFactorIncomplete; pub use self::user::{Invitation, SsoUser, User, UserId, UserKdfType, UserStampException}; -pub use self::user_signature_key_pair::{SignatureAlgorithm, UserSignatureKeyPair}; diff --git a/src/db/models/org_policy.rs b/src/db/models/org_policy.rs index 88b7872c..d501f8b9 100644 --- a/src/db/models/org_policy.rs +++ b/src/db/models/org_policy.rs @@ -91,6 +91,7 @@ impl OrgPolicy { "type": self.atype, "data": data_json, "enabled": self.enabled, + "revisionDate": null, "object": "policy", }); diff --git a/src/db/models/user.rs b/src/db/models/user.rs index e8786a22..93d750d5 100644 --- a/src/db/models/user.rs +++ b/src/db/models/user.rs @@ -20,7 +20,6 @@ use macros::UuidFromParam; use super::{ Cipher, Device, EmergencyAccess, Favorite, Folder, Membership, MembershipType, TwoFactor, TwoFactorIncomplete, - UserSignatureKeyPair, }; #[derive(Identifiable, Queryable, Insertable, AsChangeset, Selectable)] @@ -70,15 +69,6 @@ pub struct User { pub avatar_color: Option, pub external_id: Option, // Todo: Needs to be removed in the future, this is not used anymore. - - // "v2" account cryptographic state. Either all of these are set (together with a row in - // `user_signature_key_pairs`) or none of them are; see `User::is_v2`. - pub signed_public_key: Option, - pub security_state: Option, - pub security_version: Option, - /// JSON `{"wrappedUserKey1": ..., "wrappedUserKey2": ...}`, letting clients that still hold the - /// v1 user key obtain the v2 one after another client performed the upgrade. Opaque to us. - pub v2_upgrade_token: Option, } #[derive(Identifiable, Queryable, Insertable)] @@ -164,18 +154,9 @@ impl User { avatar_color: None, external_id: None, // Todo: Needs to be removed in the future, this is not used anymore. - - signed_public_key: None, - security_state: None, - security_version: None, - v2_upgrade_token: None, } } - pub fn is_v2(&self) -> bool { - self.signed_public_key.is_some() && self.security_state.is_some() && self.security_version.is_some() - } - pub fn check_valid_password(&self, password: &str) -> bool { crypto::verify_password_hash( password.as_bytes(), @@ -272,61 +253,6 @@ impl User { /// Database methods impl User { - async fn v2_signature_key_pair(&self, conn: &DbConn) -> Option { - if !self.is_v2() { - return None; - } - UserSignatureKeyPair::find_active_by_user(&self.uuid, conn).await - } - - pub async fn account_keys_json(&self, conn: &DbConn) -> Value { - if self.private_key.is_none() { - return Value::Null; - } - - let (signed_public_key, signature_key_pair, security_state) = match self.v2_signature_key_pair(conn).await { - Some(key_pair) => ( - json!(self.signed_public_key), - key_pair.to_json(), - json!({ - "securityState": self.security_state, - "securityVersion": self.security_version, - }), - ), - None => (Value::Null, Value::Null, Value::Null), - }; - - json!({ - "publicKeyEncryptionKeyPair": { - "wrappedPrivateKey": self.private_key, - "publicKey": self.public_key, - "signedPublicKey": signed_public_key, - "object": "publicKeyEncryptionKeyPair", - }, - "signatureKeyPair": signature_key_pair, - "securityState": security_state, - "object": "privateKeys" - }) - } - - pub async fn public_keys_json(&self, conn: &DbConn) -> Value { - let (signed_public_key, verifying_key) = match self.v2_signature_key_pair(conn).await { - Some(key_pair) => (json!(self.signed_public_key), json!(key_pair.verifying_key)), - None => (Value::Null, Value::Null), - }; - - json!({ - "publicKey": self.public_key, - "signedPublicKey": signed_public_key, - "verifyingKey": verifying_key, - "object": "publicKeys" - }) - } - - pub fn v2_upgrade_token_json(&self) -> Value { - self.v2_upgrade_token.as_ref().and_then(|token| serde_json::from_str(token).ok()).unwrap_or(Value::Null) - } - pub async fn to_json(&self, conn: &DbConn) -> Value { let mut orgs_json = Vec::new(); for c in Membership::find_confirmed_by_user(&self.uuid, conn).await { @@ -342,7 +268,21 @@ impl User { UserStatus::Enabled }; - let account_keys = self.account_keys_json(conn).await; + let account_keys = if self.private_key.is_some() { + json!({ + "publicKeyEncryptionKeyPair": { + "wrappedPrivateKey": self.private_key, + "publicKey": self.public_key, + "signedPublicKey": null, + "object": "publicKeyEncryptionKeyPair", + }, + "securityState": null, + "signatureKeyPair": null, + "object": "privateKeys" + }) + } else { + Value::Null + }; json!({ "_status": status as i32, @@ -417,7 +357,6 @@ impl User { Device::delete_all_by_user(&self.uuid, conn).await?; TwoFactor::delete_all_by_user(&self.uuid, conn).await?; TwoFactorIncomplete::delete_all_by_user(&self.uuid, conn).await?; - UserSignatureKeyPair::delete_all_by_user(&self.uuid, conn).await?; Invitation::take(&self.email, conn).await; // Delete invitation if any conn.run(move |conn| { diff --git a/src/db/models/user_signature_key_pair.rs b/src/db/models/user_signature_key_pair.rs deleted file mode 100644 index b7e58f3a..00000000 --- a/src/db/models/user_signature_key_pair.rs +++ /dev/null @@ -1,169 +0,0 @@ -use chrono::{NaiveDateTime, Utc}; -use derive_more::{AsRef, Deref, Display, From}; -use diesel::prelude::*; -use serde_json::Value; - -use crate::{ - api::EmptyResult, - db::{DbConn, schema::user_signature_key_pairs}, - error::MapResult, - util::get_uuid, -}; -use macros::UuidFromParam; - -use super::UserId; - -/// A user's signature key pair, part of the "v2" account cryptographic state. -/// -/// Upstream keeps this in its own table rather than as columns on the user, with a unique index on -/// the user id. The stated intent is to eventually keep superseded key pairs around (an `active` -/// flag was sketched but not shipped), which is why this is modelled as a row with its own identity -/// instead of a set of user attributes. -/// -/// Ref: -#[derive(Identifiable, Queryable, Insertable, AsChangeset, Selectable)] -#[diesel(table_name = user_signature_key_pairs)] -#[diesel(treat_none_as_null = true)] -#[diesel(primary_key(uuid))] -pub struct UserSignatureKeyPair { - pub uuid: UserSignatureKeyPairId, - pub user_uuid: UserId, - - pub signature_algorithm: i32, - /// The signing (private) key, wrapped by the user key. - pub signing_key: String, - /// The COSE-encoded public verifying key. - pub verifying_key: String, - - pub created_at: NaiveDateTime, - pub updated_at: NaiveDateTime, -} - -/// https://github.com/bitwarden/server/blob/main/src/Core/KeyManagement/Enums/SignatureAlgorithm.cs -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum SignatureAlgorithm { - Ed25519 = 0, - MlDsa44 = 1, -} - -impl SignatureAlgorithm { - pub fn from_str(algorithm: &str) -> Option { - match algorithm { - "ed25519" => Some(Self::Ed25519), - "mldsa44" => Some(Self::MlDsa44), - _ => None, - } - } - - pub fn from_i32(algorithm: i32) -> Option { - match algorithm { - 0 => Some(Self::Ed25519), - 1 => Some(Self::MlDsa44), - _ => None, - } - } - - pub fn as_str(self) -> &'static str { - match self { - Self::Ed25519 => "ed25519", - Self::MlDsa44 => "mldsa44", - } - } -} - -/// Local methods -impl UserSignatureKeyPair { - pub fn new( - user_uuid: UserId, - signature_algorithm: SignatureAlgorithm, - signing_key: String, - verifying_key: String, - ) -> Self { - let now = Utc::now().naive_utc(); - - Self { - uuid: UserSignatureKeyPairId(get_uuid()), - user_uuid, - signature_algorithm: signature_algorithm as i32, - signing_key, - verifying_key, - created_at: now, - updated_at: now, - } - } - - pub fn to_json(&self) -> Value { - json!({ - "wrappedSigningKey": self.signing_key, - "verifyingKey": self.verifying_key, - "object": "signatureKeyPair", - }) - } -} - -/// Database methods -impl UserSignatureKeyPair { - pub async fn save(&mut self, conn: &DbConn) -> EmptyResult { - self.updated_at = Utc::now().naive_utc(); - - db_run! { conn: - mysql { - diesel::insert_into(user_signature_key_pairs::table) - .values(&*self) - .on_conflict(diesel::dsl::DuplicatedKeys) - .do_update() - .set(&*self) - .execute(conn) - .map_res("Error saving user signature key pair") - } - postgresql, sqlite { - diesel::insert_into(user_signature_key_pairs::table) - .values(&*self) - .on_conflict(user_signature_key_pairs::user_uuid) - .do_update() - .set(&*self) - .execute(conn) - .map_res("Error saving user signature key pair") - } - } - } - - /// The key pair currently in use by the user. There is at most one today, enforced by a unique - /// index on `user_uuid`. - pub async fn find_active_by_user(user_uuid: &UserId, conn: &DbConn) -> Option { - conn.run(move |conn| { - user_signature_key_pairs::table - .filter(user_signature_key_pairs::user_uuid.eq(user_uuid)) - .first::(conn) - .ok() - }) - .await - } - - pub async fn delete_all_by_user(user_uuid: &UserId, conn: &DbConn) -> EmptyResult { - conn.run(move |conn| { - diesel::delete(user_signature_key_pairs::table.filter(user_signature_key_pairs::user_uuid.eq(user_uuid))) - .execute(conn) - .map_res("Error deleting user signature key pairs") - }) - .await - } -} - -#[derive( - Clone, - Debug, - AsRef, - Deref, - DieselNewType, - Display, - From, - FromForm, - Hash, - PartialEq, - Eq, - Serialize, - Deserialize, - UuidFromParam, -)] -pub struct UserSignatureKeyPairId(String); diff --git a/src/db/schema.rs b/src/db/schema.rs index 8f48a86e..af342186 100644 --- a/src/db/schema.rs +++ b/src/db/schema.rs @@ -217,22 +217,6 @@ table! { api_key -> Nullable, avatar_color -> Nullable, external_id -> Nullable, - signed_public_key -> Nullable, - security_state -> Nullable, - security_version -> Nullable, - v2_upgrade_token -> Nullable, - } -} - -table! { - user_signature_key_pairs (uuid) { - uuid -> Text, - user_uuid -> Text, - signature_algorithm -> Integer, - signing_key -> Text, - verifying_key -> Text, - created_at -> Timestamp, - updated_at -> Timestamp, } } @@ -398,7 +382,6 @@ joinable!(collections_groups -> groups (groups_uuid)); joinable!(event -> users_organizations (uuid)); joinable!(auth_requests -> users (user_uuid)); joinable!(sso_users -> users (user_uuid)); -joinable!(user_signature_key_pairs -> users (user_uuid)); allow_tables_to_appear_in_same_query!( archives, @@ -425,5 +408,4 @@ allow_tables_to_appear_in_same_query!( collections_groups, event, auth_requests, - user_signature_key_pairs, );