bitwarden_vault/cipher/
field.rs

1use bitwarden_api_api::models::CipherFieldModel;
2use bitwarden_core::{
3    MissingFieldError,
4    key_management::{KeyIds, SymmetricKeyId},
5    require,
6};
7use bitwarden_crypto::{
8    CompositeEncryptable, CryptoError, Decryptable, EncString, KeyStoreContext,
9    PrimitiveEncryptable,
10};
11use serde::{Deserialize, Serialize};
12use serde_repr::{Deserialize_repr, Serialize_repr};
13#[cfg(feature = "wasm")]
14use tsify::Tsify;
15#[cfg(feature = "wasm")]
16use wasm_bindgen::prelude::wasm_bindgen;
17
18use super::linked_id::LinkedIdType;
19use crate::VaultParseError;
20
21/// Represents the type of a [FieldView].
22#[derive(Clone, Copy, Serialize_repr, Deserialize_repr, Debug, PartialEq, Eq)]
23#[repr(u8)]
24#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
25#[cfg_attr(feature = "wasm", wasm_bindgen)]
26pub enum FieldType {
27    /// Text field
28    Text = 0,
29    /// Hidden text field
30    Hidden = 1,
31    /// Boolean field
32    Boolean = 2,
33    /// Linked field
34    Linked = 3,
35}
36
37impl TryFrom<u8> for FieldType {
38    type Error = MissingFieldError;
39
40    fn try_from(value: u8) -> Result<Self, Self::Error> {
41        match value {
42            0 => Ok(FieldType::Text),
43            1 => Ok(FieldType::Hidden),
44            2 => Ok(FieldType::Boolean),
45            3 => Ok(FieldType::Linked),
46            _ => Err(MissingFieldError("FieldType")),
47        }
48    }
49}
50
51#[derive(Serialize, Deserialize, Debug, Clone)]
52#[serde(rename_all = "camelCase", deny_unknown_fields)]
53#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
54#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
55pub struct Field {
56    name: Option<EncString>,
57    value: Option<EncString>,
58    r#type: FieldType,
59
60    linked_id: Option<LinkedIdType>,
61}
62
63#[allow(missing_docs)]
64#[derive(Serialize, Deserialize, Debug, Clone)]
65#[serde(rename_all = "camelCase", deny_unknown_fields)]
66#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
67#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
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
76impl CompositeEncryptable<KeyIds, SymmetricKeyId, Field> for FieldView {
77    fn encrypt_composite(
78        &self,
79        ctx: &mut KeyStoreContext<KeyIds>,
80        key: SymmetricKeyId,
81    ) -> Result<Field, CryptoError> {
82        Ok(Field {
83            name: self.name.encrypt(ctx, key)?,
84            value: self.value.encrypt(ctx, key)?,
85            r#type: self.r#type,
86            linked_id: self.linked_id,
87        })
88    }
89}
90
91impl Decryptable<KeyIds, SymmetricKeyId, FieldView> for Field {
92    fn decrypt(
93        &self,
94        ctx: &mut KeyStoreContext<KeyIds>,
95        key: SymmetricKeyId,
96    ) -> Result<FieldView, CryptoError> {
97        Ok(FieldView {
98            name: self.name.decrypt(ctx, key).ok().flatten(),
99            value: self.value.decrypt(ctx, key).ok().flatten(),
100            r#type: self.r#type,
101            linked_id: self.linked_id,
102        })
103    }
104}
105
106impl TryFrom<CipherFieldModel> for Field {
107    type Error = VaultParseError;
108
109    fn try_from(model: CipherFieldModel) -> Result<Self, Self::Error> {
110        Ok(Self {
111            name: EncString::try_from_optional(model.name)?,
112            value: EncString::try_from_optional(model.value)?,
113            r#type: require!(model.r#type).into(),
114            linked_id: model
115                .linked_id
116                .map(|id| (id as u32).try_into())
117                .transpose()?,
118        })
119    }
120}
121
122impl From<bitwarden_api_api::models::FieldType> for FieldType {
123    fn from(model: bitwarden_api_api::models::FieldType) -> Self {
124        match model {
125            bitwarden_api_api::models::FieldType::Text => FieldType::Text,
126            bitwarden_api_api::models::FieldType::Hidden => FieldType::Hidden,
127            bitwarden_api_api::models::FieldType::Boolean => FieldType::Boolean,
128            bitwarden_api_api::models::FieldType::Linked => FieldType::Linked,
129        }
130    }
131}
132
133impl From<Field> for bitwarden_api_api::models::CipherFieldModel {
134    fn from(field: Field) -> Self {
135        Self {
136            name: field.name.map(|n| n.to_string()),
137            value: field.value.map(|v| v.to_string()),
138            r#type: Some(field.r#type.into()),
139            linked_id: field.linked_id.map(|id| u32::from(id) as i32),
140        }
141    }
142}
143
144impl From<FieldType> for bitwarden_api_api::models::FieldType {
145    fn from(field_type: FieldType) -> Self {
146        match field_type {
147            FieldType::Text => bitwarden_api_api::models::FieldType::Text,
148            FieldType::Hidden => bitwarden_api_api::models::FieldType::Hidden,
149            FieldType::Boolean => bitwarden_api_api::models::FieldType::Boolean,
150            FieldType::Linked => bitwarden_api_api::models::FieldType::Linked,
151        }
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    #[test]
160    fn test_field_type_try_from_u8_valid() {
161        assert_eq!(FieldType::try_from(0).unwrap(), FieldType::Text);
162        assert_eq!(FieldType::try_from(1).unwrap(), FieldType::Hidden);
163        assert_eq!(FieldType::try_from(2).unwrap(), FieldType::Boolean);
164        assert_eq!(FieldType::try_from(3).unwrap(), FieldType::Linked);
165    }
166
167    #[test]
168    fn test_field_type_try_from_u8_invalid() {
169        assert!(FieldType::try_from(4).is_err());
170        assert!(FieldType::try_from(255).is_err());
171    }
172}