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