Skip to main content

bitwarden_vault/cipher/
field.rs

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/// Represents the type of a [FieldView].
20#[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 field
26    Text = 0,
27    /// Hidden text field
28    Hidden = 1,
29    /// Boolean field
30    Boolean = 2,
31    /// Linked field
32    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/// Minimal field view for list/search operations.
77/// Contains only the fields needed for search indexing.
78#[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    /// Only populated if the field has a name.
86    pub name: Option<String>,
87    /// Only populated for [FieldType::Text] fields.
88    pub value: Option<String>,
89    /// The field type.
90    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    /// Compares two sets of FieldView and detects changes in hidden fields, for building password
125    /// history.
126    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}