warden-worker/src/models/cipher.rs

342 lines
12 KiB
Rust

use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
use serde_json::{json, Map, Value};
use crate::models::attachment::AttachmentResponse;
// Cipher types:
// Login = 1,
// SecureNote = 2,
// Card = 3,
// Identity = 4,
// SshKey = 5
/// Common cipher type-specific fields shared across multiple cipher structures.
/// These represent the encrypted content fields that vary based on cipher type.
/// Used with `#[serde(flatten)]` to embed these fields into other structs.
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[serde(rename_all = "camelCase")]
pub struct CipherTypeFields {
// Only one of these should exist, depending on cipher type
#[serde(skip_serializing_if = "Option::is_none")]
pub login: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub card: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub identity: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub secure_note: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ssh_key: Option<Value>,
// Common fields
#[serde(skip_serializing_if = "Option::is_none")]
pub fields: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub password_history: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reprompt: Option<i32>,
}
/// This struct represents the data stored in the `data` column of the `ciphers` table.
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct CipherData {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub notes: Option<String>,
#[serde(flatten)]
pub type_fields: CipherTypeFields,
}
// Custom deserialization function for booleans
fn deserialize_bool_from_int<'de, D>(deserializer: D) -> Result<bool, D::Error>
where
D: Deserializer<'de>,
{
// A visitor is used to handle different data types
struct BoolOrIntVisitor;
impl<'de> de::Visitor<'de> for BoolOrIntVisitor {
type Value = bool;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a boolean or an integer 0 or 1")
}
// Handles boolean values
fn visit_bool<E>(self, value: bool) -> Result<bool, E>
where
E: de::Error,
{
Ok(value)
}
// Handles integer values (0 or 1)
fn visit_u64<E>(self, value: u64) -> Result<bool, E>
where
E: de::Error,
{
match value {
0 => Ok(false),
1 => Ok(true),
_ => Err(de::Error::invalid_value(
de::Unexpected::Unsigned(value),
&"0 or 1",
)),
}
}
}
deserializer.deserialize_any(BoolOrIntVisitor)
}
// The struct that is stored in the database and used in handlers.
// For serialization to JSON for the client, we implement a custom `Serialize`.
#[derive(Debug, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Cipher {
pub id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub user_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub organization_id: Option<String>,
#[serde(rename = "type")]
pub r#type: i32,
pub data: Value,
#[serde(deserialize_with = "deserialize_bool_from_int")]
pub favorite: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub folder_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub deleted_at: Option<String>,
pub created_at: String,
pub updated_at: String,
// Bitwarden specific field for API responses
#[serde(default = "default_object")]
pub object: String,
#[serde(default)]
#[serde(deserialize_with = "deserialize_bool_from_int")]
pub organization_use_totp: bool,
#[serde(default = "default_true")]
#[serde(deserialize_with = "deserialize_bool_from_int")]
pub edit: bool,
#[serde(default = "default_true")]
#[serde(deserialize_with = "deserialize_bool_from_int")]
pub view_password: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub collection_ids: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub attachments: Option<Vec<AttachmentResponse>>,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct CipherDBModel {
pub id: String,
pub user_id: String,
pub organization_id: Option<String>,
pub r#type: i32,
pub data: String,
pub favorite: i32,
pub folder_id: Option<String>,
pub deleted_at: Option<String>,
pub created_at: String,
pub updated_at: String,
}
impl Into<Cipher> for CipherDBModel {
fn into(self) -> Cipher {
Cipher {
id: self.id,
user_id: Some(self.user_id),
organization_id: self.organization_id,
r#type: self.r#type,
data: serde_json::from_str(&self.data).unwrap_or_default(),
favorite: match self.favorite {
0 => false,
_ => true,
},
folder_id: self.folder_id,
deleted_at: self.deleted_at,
created_at: self.created_at,
updated_at: self.updated_at,
object: default_object(),
organization_use_totp: false,
edit: true,
view_password: true,
collection_ids: None,
attachments: None,
}
}
}
impl Serialize for Cipher {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut response_map = Map::new();
response_map.insert("object".to_string(), json!(self.object));
response_map.insert("id".to_string(), json!(self.id));
if self.user_id.is_some() {
response_map.insert("userId".to_string(), json!(self.user_id));
}
response_map.insert("organizationId".to_string(), json!(self.organization_id));
response_map.insert("folderId".to_string(), json!(self.folder_id));
response_map.insert("type".to_string(), json!(self.r#type));
response_map.insert("favorite".to_string(), json!(self.favorite));
response_map.insert("edit".to_string(), json!(self.edit));
response_map.insert("viewPassword".to_string(), json!(self.view_password));
// new key "permissions" used by clients since v2025.6.0
response_map.insert(
"permissions".to_string(),
json! ({
"delete": self.edit, // if edit is true, allow delete
"restore": self.edit, // if edit is true, allow restore
}),
);
response_map.insert(
"organizationUseTotp".to_string(),
json!(self.organization_use_totp),
);
response_map.insert("collectionIds".to_string(), json!(self.collection_ids));
response_map.insert("revisionDate".to_string(), json!(self.updated_at));
response_map.insert("creationDate".to_string(), json!(self.created_at));
response_map.insert("deletedDate".to_string(), json!(self.deleted_at));
response_map.insert("attachments".to_string(), json!(self.attachments));
if let Some(data_obj) = self.data.as_object() {
let data_clone = data_obj.clone();
response_map.insert(
"name".to_string(),
data_clone.get("name").cloned().unwrap_or(Value::Null),
);
response_map.insert(
"notes".to_string(),
data_clone.get("notes").cloned().unwrap_or(Value::Null),
);
response_map.insert(
"fields".to_string(),
data_clone.get("fields").cloned().unwrap_or(Value::Null),
);
response_map.insert(
"passwordHistory".to_string(),
data_clone
.get("passwordHistory")
.cloned()
.unwrap_or(Value::Null),
);
response_map.insert(
"reprompt".to_string(),
data_clone
.get("reprompt")
.cloned()
.unwrap_or(Value::Number(serde_json::Number::from_f64(0.0).unwrap())),
);
let mut login = Value::Null;
let mut secure_note = Value::Null;
let mut card = Value::Null;
let mut identity = Value::Null;
let mut ssh_key = Value::Null;
match self.r#type {
1 => login = data_clone.get("login").cloned().unwrap_or(Value::Null),
2 => secure_note = data_clone.get("secureNote").cloned().unwrap_or(Value::Null),
3 => card = data_clone.get("card").cloned().unwrap_or(Value::Null),
4 => identity = data_clone.get("identity").cloned().unwrap_or(Value::Null),
5 => ssh_key = data_clone.get("sshKey").cloned().unwrap_or(Value::Null),
_ => {}
}
response_map.insert("login".to_string(), login);
response_map.insert("secureNote".to_string(), secure_note);
response_map.insert("card".to_string(), card);
response_map.insert("identity".to_string(), identity);
response_map.insert("sshKey".to_string(), ssh_key);
} else {
response_map.insert("name".to_string(), Value::Null);
response_map.insert("notes".to_string(), Value::Null);
response_map.insert("fields".to_string(), Value::Null);
response_map.insert("passwordHistory".to_string(), Value::Null);
response_map.insert("reprompt".to_string(), Value::Null);
response_map.insert("login".to_string(), Value::Null);
response_map.insert("secureNote".to_string(), Value::Null);
response_map.insert("card".to_string(), Value::Null);
response_map.insert("identity".to_string(), Value::Null);
response_map.insert("sshKey".to_string(), Value::Null);
}
Value::Object(response_map).serialize(serializer)
}
}
fn default_object() -> String {
"cipherDetails".to_string()
}
fn default_true() -> bool {
true
}
/// Represents the "Cipher" object within incoming request payloads.
/// Used for create, update, import, and key rotation scenarios.
/// Aligned with vaultwarden's CipherData structure.
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct CipherRequestData {
// Id is optional as it is included only in bulk share / key rotation
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
// Folder id is not included in import (determined by folder_relationships)
#[serde(skip_serializing_if = "Option::is_none")]
pub folder_id: Option<String>,
#[serde(alias = "organizationID")]
#[serde(skip_serializing_if = "Option::is_none")]
pub organization_id: Option<String>,
#[serde(rename = "type")]
pub r#type: i32,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub notes: Option<String>,
#[serde(default)]
pub favorite: Option<bool>,
#[serde(flatten)]
pub type_fields: CipherTypeFields,
// The revision datetime (in ISO 8601 format) of the client's local copy
// Used to prevent updating a cipher when client doesn't have the latest version
#[serde(skip_serializing_if = "Option::is_none")]
pub last_known_revision_date: Option<String>,
}
/// Represents the full request payload for creating a cipher with collections.
/// Supports both camelCase and PascalCase for compatibility with different clients.
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateCipherRequest {
#[serde(alias = "Cipher")]
pub cipher: CipherRequestData,
#[serde(default)]
#[serde(alias = "CollectionIds")]
pub collection_ids: Vec<String>,
}
/// Response for listing ciphers (GET /api/ciphers)
/// Now we don't use this struct, we use RawJson instead. But we keep it here for reference.
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
#[allow(dead_code)]
pub struct CipherListResponse {
pub data: Vec<Value>,
pub object: String,
pub continuation_token: Option<String>,
}
/// Request body for updating a cipher partially (PUT /api/ciphers/{id}/partial)
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PartialCipherData {
pub folder_id: Option<String>,
pub favorite: bool,
}