mirror of
https://github.com/dani-garcia/vaultwarden.wiki.git
synced 2026-10-25 11:46:48 +02:00
Implement V2 registration support
This commit is contained in:
14 files changed
+614
-93
No files matched your search
@@ -17,6 +17,7 @@ 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};
|
||||
@@ -40,3 +41,4 @@ 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, KeyId, SsoUser, User, UserId, UserKdfType, UserStampException};
|
||||
pub use self::user_signature_key_pair::{SignatureAlgorithm, UserSignatureKeyPair};
|
||||
+68
-15
@@ -20,6 +20,7 @@ use macros::UuidFromParam;
|
||||
|
||||
use super::{
|
||||
Cipher, Device, EmergencyAccess, Favorite, Folder, Membership, MembershipType, TwoFactor, TwoFactorIncomplete,
|
||||
UserSignatureKeyPair,
|
||||
};
|
||||
|
||||
#[derive(Identifiable, Queryable, Insertable, AsChangeset, Selectable)]
|
||||
@@ -71,6 +72,12 @@ pub struct User {
|
||||
pub external_id: Option<String>, // Todo: Needs to be removed in the future, this is not used anymore.
|
||||
|
||||
pub key_id: Option<KeyId>,
|
||||
|
||||
// "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<String>,
|
||||
pub security_state: Option<String>,
|
||||
pub security_version: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Identifiable, Queryable, Insertable)]
|
||||
@@ -158,9 +165,17 @@ impl User {
|
||||
external_id: None, // Todo: Needs to be removed in the future, this is not used anymore.
|
||||
|
||||
key_id: None,
|
||||
|
||||
signed_public_key: None,
|
||||
security_state: None,
|
||||
security_version: 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(),
|
||||
@@ -257,6 +272,57 @@ impl User {
|
||||
|
||||
/// Database methods
|
||||
impl User {
|
||||
async fn v2_signature_key_pair(&self, conn: &DbConn) -> Option<UserSignatureKeyPair> {
|
||||
if !self.is_v2() {
|
||||
return None;
|
||||
}
|
||||
UserSignatureKeyPair::find_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 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 {
|
||||
@@ -277,21 +343,7 @@ impl User {
|
||||
UserStatus::Enabled
|
||||
};
|
||||
|
||||
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
|
||||
};
|
||||
let account_keys = self.account_keys_json(conn).await;
|
||||
|
||||
json!({
|
||||
"_status": status as i32,
|
||||
@@ -367,6 +419,7 @@ 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| {
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
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 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: <https://github.com/bitwarden/server/blob/main/src/Sql/dbo/KeyManagement/Tables/UserSignatureKeyPair.sql>
|
||||
#[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 parse(algorithm: &str) -> Option<Self> {
|
||||
match algorithm {
|
||||
"ed25519" => Some(Self::Ed25519),
|
||||
"mldsa44" => Some(Self::MlDsa44),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 user's key pair. There is at most one, enforced by a unique index on `user_uuid`.
|
||||
pub async fn find_by_user(user_uuid: &UserId, conn: &DbConn) -> Option<Self> {
|
||||
conn.run(move |conn| {
|
||||
user_signature_key_pairs::table
|
||||
.filter(user_signature_key_pairs::user_uuid.eq(user_uuid))
|
||||
.first::<Self>(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, Hash, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct UserSignatureKeyPairId(String);
|
||||
Reference in new issue
Block a user