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#[cfg(feature = "wasm")]
16use tsify::Tsify;
17#[cfg(feature = "wasm")]
18use wasm_bindgen::prelude::wasm_bindgen;
19
20use super::{cipher::StrictDecrypt, linked_id::LinkedIdType};
21use crate::{PasswordHistoryView, VaultParseError};
22
23/// Represents the type of a [FieldView].
24#[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 field
30    Text = 0,
31    /// Hidden text field
32    Hidden = 1,
33    /// Boolean field
34    Boolean = 2,
35    /// Linked field
36    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/// Minimal field view for list/search operations.
79/// Contains only the fields needed for search indexing.
80#[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    /// Only populated if the field has a name.
87    pub name: Option<String>,
88    /// Only populated for [FieldType::Text] fields.
89    pub value: Option<String>,
90    /// The field type.
91    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<KeySlotIds, SymmetricKeySlotId, Field> for FieldView {
110    fn encrypt_composite(
111        &self,
112        ctx: &mut KeyStoreContext<KeySlotIds>,
113        key: SymmetricKeySlotId,
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    /// Compares two sets of FieldView and detects changes in hidden fields, for building password
126    /// history.
127    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<KeySlotIds, SymmetricKeySlotId, FieldView> for Field {
163    fn decrypt(
164        &self,
165        ctx: &mut KeyStoreContext<KeySlotIds>,
166        key: SymmetricKeySlotId,
167    ) -> Result<FieldView, CryptoError> {
168        Ok(FieldView {
169            name: self.name.decrypt(ctx, key).ok().flatten(),
170            value: self.value.decrypt(ctx, key).ok().flatten(),
171            r#type: self.r#type,
172            linked_id: self.linked_id,
173        })
174    }
175}
176
177impl Decryptable<KeySlotIds, SymmetricKeySlotId, FieldView> for StrictDecrypt<&Field> {
178    fn decrypt(
179        &self,
180        ctx: &mut KeyStoreContext<KeySlotIds>,
181        key: SymmetricKeySlotId,
182    ) -> Result<FieldView, CryptoError> {
183        Ok(FieldView {
184            name: self.0.name.decrypt(ctx, key)?,
185            value: self.0.value.decrypt(ctx, key)?,
186            r#type: self.0.r#type,
187            linked_id: self.0.linked_id,
188        })
189    }
190}
191
192impl TryFrom<CipherFieldModel> for Field {
193    type Error = VaultParseError;
194
195    fn try_from(model: CipherFieldModel) -> Result<Self, Self::Error> {
196        Ok(Self {
197            name: EncString::try_from_optional(model.name)?,
198            value: EncString::try_from_optional(model.value)?,
199            r#type: require!(model.r#type).try_into()?,
200            linked_id: model
201                .linked_id
202                .map(|id| (id as u32).try_into())
203                .transpose()?,
204        })
205    }
206}
207
208impl TryFrom<bitwarden_api_api::models::FieldType> for FieldType {
209    type Error = MissingFieldError;
210
211    fn try_from(model: bitwarden_api_api::models::FieldType) -> Result<Self, Self::Error> {
212        Ok(match model {
213            bitwarden_api_api::models::FieldType::Text => FieldType::Text,
214            bitwarden_api_api::models::FieldType::Hidden => FieldType::Hidden,
215            bitwarden_api_api::models::FieldType::Boolean => FieldType::Boolean,
216            bitwarden_api_api::models::FieldType::Linked => FieldType::Linked,
217            bitwarden_api_api::models::FieldType::__Unknown(_) => {
218                return Err(MissingFieldError("type"));
219            }
220        })
221    }
222}
223
224impl From<Field> for bitwarden_api_api::models::CipherFieldModel {
225    fn from(field: Field) -> Self {
226        Self {
227            name: field.name.map(|n| n.to_string()),
228            value: field.value.map(|v| v.to_string()),
229            r#type: Some(field.r#type.into()),
230            linked_id: field.linked_id.map(|id| u32::from(id) as i32),
231        }
232    }
233}
234
235impl From<FieldType> for bitwarden_api_api::models::FieldType {
236    fn from(field_type: FieldType) -> Self {
237        match field_type {
238            FieldType::Text => bitwarden_api_api::models::FieldType::Text,
239            FieldType::Hidden => bitwarden_api_api::models::FieldType::Hidden,
240            FieldType::Boolean => bitwarden_api_api::models::FieldType::Boolean,
241            FieldType::Linked => bitwarden_api_api::models::FieldType::Linked,
242        }
243    }
244}
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249
250    #[test]
251    fn test_field_type_try_from_u8_valid() {
252        assert_eq!(FieldType::try_from(0).unwrap(), FieldType::Text);
253        assert_eq!(FieldType::try_from(1).unwrap(), FieldType::Hidden);
254        assert_eq!(FieldType::try_from(2).unwrap(), FieldType::Boolean);
255        assert_eq!(FieldType::try_from(3).unwrap(), FieldType::Linked);
256    }
257
258    #[test]
259    fn test_field_type_try_from_u8_invalid() {
260        assert!(FieldType::try_from(4).is_err());
261        assert!(FieldType::try_from(255).is_err());
262    }
263}