diff --git a/migrations/mysql/2025-01-09-172300_add_passkey_support/up.sql b/migrations/mysql/2025-01-09-172300_add_passkey_support/up.sql new file mode 100644 index 00000000..d3bfa051 --- /dev/null +++ b/migrations/mysql/2025-01-09-172300_add_passkey_support/up.sql @@ -0,0 +1,3 @@ +ALTER TABLE ciphers +ADD COLUMN passkey_id TEXT, +ADD COLUMN passkey_public_key TEXT; diff --git a/migrations/postgresql/2025-01-09-172300_add_passkey_support/up.sql b/migrations/postgresql/2025-01-09-172300_add_passkey_support/up.sql new file mode 100644 index 00000000..d3bfa051 --- /dev/null +++ b/migrations/postgresql/2025-01-09-172300_add_passkey_support/up.sql @@ -0,0 +1,3 @@ +ALTER TABLE ciphers +ADD COLUMN passkey_id TEXT, +ADD COLUMN passkey_public_key TEXT; diff --git a/migrations/sqlite/2025-01-09-172300_add_passkey_support/up.sql b/migrations/sqlite/2025-01-09-172300_add_passkey_support/up.sql new file mode 100644 index 00000000..d3bfa051 --- /dev/null +++ b/migrations/sqlite/2025-01-09-172300_add_passkey_support/up.sql @@ -0,0 +1,3 @@ +ALTER TABLE ciphers +ADD COLUMN passkey_id TEXT, +ADD COLUMN passkey_public_key TEXT; diff --git a/src/api/admin.rs b/src/api/admin.rs index b3e703d9..730272b3 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -63,6 +63,11 @@ pub fn routes() -> Vec { get_diagnostics_config, resend_user_invite, get_diagnostics_http, + get_passkeys, + get_passkey, + create_passkey, + update_passkey, + delete_passkey, ] } @@ -822,3 +827,61 @@ impl<'r> FromRequest<'r> for AdminToken { } } } + +#[get("/passkeys")] +async fn get_passkeys(_token: AdminToken, mut conn: DbConn) -> Json { + let passkeys = Passkey::get_all(&mut conn).await; + let passkeys_json: Vec = passkeys.iter().map(|p| p.to_json()).collect(); + Json(json!({ + "data": passkeys_json, + "object": "list", + "continuationToken": null, + })) +} + +#[get("/passkeys/")] +async fn get_passkey(passkey_id: PasskeyId, _token: AdminToken, mut conn: DbConn) -> JsonResult { + let passkey = Passkey::find_by_uuid(&passkey_id, &mut conn).await.ok_or_else(|| { + err_code!("Passkey doesn't exist", Status::NotFound.code) + })?; + Ok(Json(passkey.to_json())) +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct PasskeyData { + name: String, + credential_id: String, + public_key: String, + user_handle: String, +} + +#[post("/passkeys", format = "application/json", data = "")] +async fn create_passkey(data: Json, _token: AdminToken, mut conn: DbConn) -> JsonResult { + let data: PasskeyData = data.into_inner(); + let passkey = Passkey::new(data.name, data.credential_id, data.public_key, data.user_handle); + passkey.save(&mut conn).await?; + Ok(Json(passkey.to_json())) +} + +#[put("/passkeys/", format = "application/json", data = "")] +async fn update_passkey(passkey_id: PasskeyId, data: Json, _token: AdminToken, mut conn: DbConn) -> JsonResult { + let mut passkey = Passkey::find_by_uuid(&passkey_id, &mut conn).await.ok_or_else(|| { + err_code!("Passkey doesn't exist", Status::NotFound.code) + })?; + let data: PasskeyData = data.into_inner(); + passkey.name = data.name; + passkey.credential_id = data.credential_id; + passkey.public_key = data.public_key; + passkey.user_handle = data.user_handle; + passkey.save(&mut conn).await?; + Ok(Json(passkey.to_json())) +} + +#[delete("/passkeys/")] +async fn delete_passkey(passkey_id: PasskeyId, _token: AdminToken, mut conn: DbConn) -> EmptyResult { + let passkey = Passkey::find_by_uuid(&passkey_id, &mut conn).await.ok_or_else(|| { + err_code!("Passkey doesn't exist", Status::NotFound.code) + })?; + passkey.delete(&mut conn).await +} diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index 6c75d246..2613e724 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -233,7 +233,8 @@ pub struct CipherData { SecureNote = 2, Card = 3, Identity = 4, - SshKey = 5 + SshKey = 5, + Passkey = 6 */ pub r#type: i32, pub name: String, @@ -246,6 +247,7 @@ pub struct CipherData { card: Option, identity: Option, ssh_key: Option, + passkey: Option, favorite: Option, reprompt: Option, @@ -483,6 +485,7 @@ pub async fn update_cipher_from_data( 3 => data.card, 4 => data.identity, 5 => data.ssh_key, + 6 => data.passkey, _ => err!("Invalid type"), }; diff --git a/src/db/models/cipher.rs b/src/db/models/cipher.rs index d9dbd28d..f190bd88 100644 --- a/src/db/models/cipher.rs +++ b/src/db/models/cipher.rs @@ -33,7 +33,8 @@ db_object! { SecureNote = 2, Card = 3, Identity = 4, - SshKey = 5 + SshKey = 5, + Passkey = 6 */ pub atype: i32, pub name: String, @@ -286,12 +287,24 @@ impl Cipher { if self.atype == 5 && (type_data_json["keyFingerprint"].as_str().is_none_or(|v| v.is_empty()) || type_data_json["privateKey"].as_str().is_none_or(|v| v.is_empty()) - || type_data_json["publicKey"].as_str().is_none_or(|v| v.is_empty())) + || type_data_json["publicKey"].as_str().is.none_or(|v| v.is_empty())) { warn!("Error parsing ssh-key, mandatory fields are invalid for {}", self.uuid); type_data_json = Value::Null; } + // Fix invalid Passkey Entries + // This breaks at least the native mobile client if invalid + // The only way to fix this is by setting type_data_json to `null` + // Opening this passkey in the mobile client will probably crash the client, but you can edit, save and afterwards delete it + if self.atype == 6 + && (type_data_json["credentialId"].as_str().is_none_or(|v| v.is_empty()) + || type_data_json["publicKey"].as_str().is.none_or(|v| v.is_empty())) + { + warn!("Error parsing passkey, mandatory fields are invalid for {}", self.uuid); + type_data_json = Value::Null; + } + // Clone the type_data and add some default value. let mut data_json = type_data_json.clone(); @@ -351,6 +364,7 @@ impl Cipher { "card": null, "identity": null, "sshKey": null, + "passkey": null, }); // These values are only needed for user/default syncs @@ -380,6 +394,7 @@ impl Cipher { 3 => "card", 4 => "identity", 5 => "sshKey", + 6 => "passkey", _ => panic!("Wrong type"), };