Browse Source

Add passkey support to Vaultwarden

Add support for saving and using passkeys, and importing them via .json.

* **src/api/core/ciphers.rs**
  - Add `Passkey` type to `CipherData` struct.
  - Update `update_cipher_from_data` function to handle passkeys.
  - Modify `post_ciphers_import` function to import passkeys.

* **src/db/models/cipher.rs**
  - Add `Passkey` type to `Cipher` struct.
  - Update `type_data_json` handling to include passkeys.
  - Add validation for passkey entries.

* **Database Migrations**
  - Add SQL statements to add passkey fields to MySQL, PostgreSQL, and SQLite schemas.

* **src/api/admin.rs**
  - Add endpoints for managing passkeys: `get_passkeys`, `get_passkey`, `create_passkey`, `update_passkey`, `delete_passkey`.
pull/5490/head
Luca Cassano 2 months ago
parent
commit
2bec0ff2fd
  1. 3
      migrations/mysql/2025-01-09-172300_add_passkey_support/up.sql
  2. 3
      migrations/postgresql/2025-01-09-172300_add_passkey_support/up.sql
  3. 3
      migrations/sqlite/2025-01-09-172300_add_passkey_support/up.sql
  4. 63
      src/api/admin.rs
  5. 5
      src/api/core/ciphers.rs
  6. 19
      src/db/models/cipher.rs

3
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;

3
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;

3
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;

63
src/api/admin.rs

@ -63,6 +63,11 @@ pub fn routes() -> Vec<Route> {
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<Value> {
let passkeys = Passkey::get_all(&mut conn).await;
let passkeys_json: Vec<Value> = passkeys.iter().map(|p| p.to_json()).collect();
Json(json!({
"data": passkeys_json,
"object": "list",
"continuationToken": null,
}))
}
#[get("/passkeys/<passkey_id>")]
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 = "<data>")]
async fn create_passkey(data: Json<PasskeyData>, _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/<passkey_id>", format = "application/json", data = "<data>")]
async fn update_passkey(passkey_id: PasskeyId, data: Json<PasskeyData>, _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/<passkey_id>")]
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
}

5
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<Value>,
identity: Option<Value>,
ssh_key: Option<Value>,
passkey: Option<Value>,
favorite: Option<bool>,
reprompt: Option<i32>,
@ -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"),
};

19
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"),
};

Loading…
Cancel
Save