1use std::collections::HashMap;
2
3use bitwarden_api_api::models::CipherFieldModel;
4use bitwarden_core::{
5 MissingFieldError,
6 key_management::{KeyIds, SymmetricKeyId},
7 require,
8};
9use bitwarden_crypto::{
10 CompositeEncryptable, CryptoError, Decryptable, EncString, KeyStoreContext,
11 PrimitiveEncryptable,
12};
13use serde::{Deserialize, Serialize};
14use serde_repr::{Deserialize_repr, Serialize_repr};
15#[cfg(feature = "wasm")]
16use tsify::Tsify;
17#[cfg(feature = "wasm")]
18use wasm_bindgen::prelude::wasm_bindgen;
19
20use super::linked_id::LinkedIdType;
21use crate::{PasswordHistoryView, VaultParseError};
22
23#[derive(Clone, Copy, Serialize_repr, Deserialize_repr, Debug, PartialEq, Eq)]
25#[repr(u8)]
26#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
27#[cfg_attr(feature = "wasm", wasm_bindgen)]
28pub enum FieldType {
29 Text = 0,
31 Hidden = 1,
33 Boolean = 2,
35 Linked = 3,
37}
38
39impl TryFrom<u8> for FieldType {
40 type Error = MissingFieldError;
41
42 fn try_from(value: u8) -> Result<Self, Self::Error> {
43 match value {
44 0 => Ok(FieldType::Text),
45 1 => Ok(FieldType::Hidden),
46 2 => Ok(FieldType::Boolean),
47 3 => Ok(FieldType::Linked),
48 _ => Err(MissingFieldError("FieldType")),
49 }
50 }
51}
52
53#[derive(Serialize, Deserialize, Debug, Clone)]
54#[serde(rename_all = "camelCase", deny_unknown_fields)]
55#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
56#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
57pub struct Field {
58 name: Option<EncString>,
59 value: Option<EncString>,
60 r#type: FieldType,
61
62 linked_id: Option<LinkedIdType>,
63}
64
65#[allow(missing_docs)]
66#[derive(Serialize, Deserialize, Debug, Clone)]
67#[serde(rename_all = "camelCase", deny_unknown_fields)]
68#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
69#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
70pub struct FieldView {
71 pub name: Option<String>,
72 pub value: Option<String>,
73 pub r#type: FieldType,
74
75 pub linked_id: Option<LinkedIdType>,
76}
77
78#[cfg(feature = "wasm")]
81#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
82#[serde(rename_all = "camelCase", deny_unknown_fields)]
83#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
84#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
85pub struct FieldListView {
86 pub name: Option<String>,
88 pub value: Option<String>,
90 pub r#type: FieldType,
92}
93
94#[cfg(feature = "wasm")]
95impl From<FieldView> for FieldListView {
96 fn from(field: FieldView) -> Self {
97 Self {
98 name: field.name,
99 value: if field.r#type == FieldType::Text {
100 field.value
101 } else {
102 None
103 },
104 r#type: field.r#type,
105 }
106 }
107}
108
109impl CompositeEncryptable<KeyIds, SymmetricKeyId, Field> for FieldView {
110 fn encrypt_composite(
111 &self,
112 ctx: &mut KeyStoreContext<KeyIds>,
113 key: SymmetricKeyId,
114 ) -> Result<Field, CryptoError> {
115 Ok(Field {
116 name: self.name.encrypt(ctx, key)?,
117 value: self.value.encrypt(ctx, key)?,
118 r#type: self.r#type,
119 linked_id: self.linked_id,
120 })
121 }
122}
123
124impl FieldView {
125 pub(crate) fn detect_hidden_field_changes(
128 fields: &[FieldView],
129 original: &[FieldView],
130 ) -> Vec<PasswordHistoryView> {
131 let current_fields = Self::extract_hidden_fields(fields);
132 let original_fields = Self::extract_hidden_fields(original);
133
134 original_fields
135 .into_iter()
136 .filter_map(|(field_name, original_value)| {
137 let current_value = current_fields.get(&field_name);
138 if current_value != Some(&original_value) {
139 Some(PasswordHistoryView::new_field(&field_name, &original_value))
140 } else {
141 None
142 }
143 })
144 .collect()
145 }
146
147 fn extract_hidden_fields(fields: &[FieldView]) -> HashMap<String, String> {
148 fields
149 .iter()
150 .filter_map(|f| match (&f.r#type, &f.name, &f.value) {
151 (FieldType::Hidden, Some(name), Some(value))
152 if !name.is_empty() && !value.is_empty() =>
153 {
154 Some((name.clone(), value.clone()))
155 }
156 _ => None,
157 })
158 .collect()
159 }
160}
161
162impl Decryptable<KeyIds, SymmetricKeyId, FieldView> for Field {
163 fn decrypt(
164 &self,
165 ctx: &mut KeyStoreContext<KeyIds>,
166 key: SymmetricKeyId,
167 ) -> Result<FieldView, CryptoError> {
168 Ok(FieldView {
169 name: self.name.decrypt(ctx, key)?,
170 value: self.value.decrypt(ctx, key)?,
171 r#type: self.r#type,
172 linked_id: self.linked_id,
173 })
174 }
175}
176
177impl TryFrom<CipherFieldModel> for Field {
178 type Error = VaultParseError;
179
180 fn try_from(model: CipherFieldModel) -> Result<Self, Self::Error> {
181 Ok(Self {
182 name: EncString::try_from_optional(model.name)?,
183 value: EncString::try_from_optional(model.value)?,
184 r#type: require!(model.r#type).try_into()?,
185 linked_id: model
186 .linked_id
187 .map(|id| (id as u32).try_into())
188 .transpose()?,
189 })
190 }
191}
192
193impl TryFrom<bitwarden_api_api::models::FieldType> for FieldType {
194 type Error = MissingFieldError;
195
196 fn try_from(model: bitwarden_api_api::models::FieldType) -> Result<Self, Self::Error> {
197 Ok(match model {
198 bitwarden_api_api::models::FieldType::Text => FieldType::Text,
199 bitwarden_api_api::models::FieldType::Hidden => FieldType::Hidden,
200 bitwarden_api_api::models::FieldType::Boolean => FieldType::Boolean,
201 bitwarden_api_api::models::FieldType::Linked => FieldType::Linked,
202 bitwarden_api_api::models::FieldType::__Unknown(_) => {
203 return Err(MissingFieldError("type"));
204 }
205 })
206 }
207}
208
209impl From<Field> for bitwarden_api_api::models::CipherFieldModel {
210 fn from(field: Field) -> Self {
211 Self {
212 name: field.name.map(|n| n.to_string()),
213 value: field.value.map(|v| v.to_string()),
214 r#type: Some(field.r#type.into()),
215 linked_id: field.linked_id.map(|id| u32::from(id) as i32),
216 }
217 }
218}
219
220impl From<FieldType> for bitwarden_api_api::models::FieldType {
221 fn from(field_type: FieldType) -> Self {
222 match field_type {
223 FieldType::Text => bitwarden_api_api::models::FieldType::Text,
224 FieldType::Hidden => bitwarden_api_api::models::FieldType::Hidden,
225 FieldType::Boolean => bitwarden_api_api::models::FieldType::Boolean,
226 FieldType::Linked => bitwarden_api_api::models::FieldType::Linked,
227 }
228 }
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234
235 #[test]
236 fn test_field_type_try_from_u8_valid() {
237 assert_eq!(FieldType::try_from(0).unwrap(), FieldType::Text);
238 assert_eq!(FieldType::try_from(1).unwrap(), FieldType::Hidden);
239 assert_eq!(FieldType::try_from(2).unwrap(), FieldType::Boolean);
240 assert_eq!(FieldType::try_from(3).unwrap(), FieldType::Linked);
241 }
242
243 #[test]
244 fn test_field_type_try_from_u8_invalid() {
245 assert!(FieldType::try_from(4).is_err());
246 assert!(FieldType::try_from(255).is_err());
247 }
248}