Skip to content

Commit f52e4eb

Browse files
CipherListView updates (#306)
## 🎟️ Tracking Tangential to [PM-22134](https://bitwarden.atlassian.net/browse/PM-22134) ## 📔 Objective Two updates to ensure parity when migrating from `CipherView` to `CipherListView` on the clients repo - Added `local_data` so the type propagates to the clients repo and can be set there. - Added `CopiableCipherFields` enum + property. - In the browser the copy menu / actions are dynamic based on the populated fields on the cipher. The `CipherListView` doesn't contain these details. This array is filled with the available fields so the extension can still check their availability. When a value is needed the full cipher is decrypted. - Added `has_old_attachments` to identify attachments following old encryption methods. This populates UI signaling to the user that the attachment needs to be re-encrypted. ## 🦮 Reviewer guidelines <!-- Suggested interactions but feel free to use (or not) as you desire! --> - 👍 (`:+1:`) or similar for great changes - 📝 (`:memo:`) or ℹ️ (`:information_source:`) for notes or general info - ❓ (`:question:`) for questions - 🤔 (`:thinking:`) or 💭 (`:thought_balloon:`) for more open inquiry that's not quite a confirmed issue and could potentially benefit from discussion - 🎨 (`:art:`) for suggestions / improvements - ❌ (`:x:`) or ⚠️ (`:warning:`) for more significant problems or concerns needing attention - 🌱 (`:seedling:`) or ♻️ (`:recycle:`) for future improvements or indications of technical debt - ⛏ (`:pick:`) for minor or nitpick changes [PM-22134]: https://bitwarden.atlassian.net/browse/PM-22134?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
1 parent 2ff1ec1 commit f52e4eb

File tree

8 files changed

+407
-8
lines changed

8 files changed

+407
-8
lines changed

crates/bitwarden-vault/src/cipher/card.rs

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize};
66
use tsify_next::Tsify;
77

88
use super::cipher::CipherKind;
9-
use crate::VaultParseError;
9+
use crate::{cipher::cipher::CopyableCipherFields, Cipher, VaultParseError};
1010

1111
#[derive(Serialize, Deserialize, Debug, Clone)]
1212
#[serde(rename_all = "camelCase", deny_unknown_fields)]
@@ -143,6 +143,20 @@ impl CipherKind for Card {
143143

144144
Ok(build_subtitle_card(brand, number))
145145
}
146+
147+
fn get_copyable_fields(&self, _: Option<&Cipher>) -> Vec<CopyableCipherFields> {
148+
[
149+
self.number
150+
.as_ref()
151+
.map(|_| CopyableCipherFields::CardNumber),
152+
self.code
153+
.as_ref()
154+
.map(|_| CopyableCipherFields::CardSecurityCode),
155+
]
156+
.into_iter()
157+
.flatten()
158+
.collect()
159+
}
146160
}
147161

148162
/// Builds the subtitle for a card cipher
@@ -233,4 +247,38 @@ mod tests {
233247
let subtitle = build_subtitle_card(brand, number);
234248
assert_eq!(subtitle, "*4444");
235249
}
250+
#[test]
251+
fn test_get_copyable_fields_code() {
252+
let card = Card {
253+
cardholder_name: None,
254+
exp_month: None,
255+
exp_year: None,
256+
code: Some("2.6TpmzzaQHgYr+mXjdGLQlg==|vT8VhfvMlWSCN9hxGYftZ5rjKRsZ9ofjdlUCx5Gubnk=|uoD3/GEQBWKKx2O+/YhZUCzVkfhm8rFK3sUEVV84mv8=".parse().unwrap()),
257+
brand: None,
258+
number: None,
259+
};
260+
261+
let copyable_fields = card.get_copyable_fields(None);
262+
263+
assert_eq!(
264+
copyable_fields,
265+
vec![CopyableCipherFields::CardSecurityCode]
266+
);
267+
}
268+
269+
#[test]
270+
fn test_get_copyable_fields_number() {
271+
let card = Card {
272+
cardholder_name: None,
273+
exp_month: None,
274+
exp_year: None,
275+
code: None,
276+
brand: None,
277+
number: Some("2.6TpmzzaQHgYr+mXjdGLQlg==|vT8VhfvMlWSCN9hxGYftZ5rjKRsZ9ofjdlUCx5Gubnk=|uoD3/GEQBWKKx2O+/YhZUCzVkfhm8rFK3sUEVV84mv8=".parse().unwrap()),
278+
};
279+
280+
let copyable_fields = card.get_copyable_fields(None);
281+
282+
assert_eq!(copyable_fields, vec![CopyableCipherFields::CardNumber]);
283+
}
236284
}

crates/bitwarden-vault/src/cipher/cipher.rs

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ pub(super) trait CipherKind {
5353
ctx: &mut KeyStoreContext<KeyIds>,
5454
key: SymmetricKeyId,
5555
) -> Result<String, CryptoError>;
56+
57+
/// Returns a list of populated fields for the cipher.
58+
fn get_copyable_fields(&self, cipher: Option<&Cipher>) -> Vec<CopyableCipherFields>;
5659
}
5760

5861
#[allow(missing_docs)]
@@ -186,6 +189,24 @@ pub enum CipherListViewType {
186189
SshKey,
187190
}
188191

192+
/// Available fields on a cipher and can be copied from a the list view in the UI.
193+
#[derive(Serialize, Deserialize, Debug, PartialEq)]
194+
#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
195+
#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
196+
pub enum CopyableCipherFields {
197+
LoginUsername,
198+
LoginPassword,
199+
LoginTotp,
200+
CardNumber,
201+
CardSecurityCode,
202+
IdentityUsername,
203+
IdentityEmail,
204+
IdentityPhone,
205+
IdentityAddress,
206+
SshKey,
207+
SecureNotes,
208+
}
209+
189210
#[allow(missing_docs)]
190211
#[derive(Serialize, Deserialize, Debug, PartialEq)]
191212
#[serde(rename_all = "camelCase", deny_unknown_fields)]
@@ -215,10 +236,17 @@ pub struct CipherListView {
215236

216237
/// The number of attachments
217238
pub attachments: u32,
239+
/// Indicates if the cipher has old attachments that need to be re-uploaded
240+
pub has_old_attachments: bool,
218241

219242
pub creation_date: DateTime<Utc>,
220243
pub deleted_date: Option<DateTime<Utc>>,
221244
pub revision_date: DateTime<Utc>,
245+
246+
/// Hints for the presentation layer for which fields can be copied.
247+
pub copyable_fields: Vec<CopyableCipherFields>,
248+
249+
pub local_data: Option<LocalDataView>,
222250
}
223251

224252
impl CipherListView {
@@ -366,7 +394,7 @@ impl Cipher {
366394
CipherType::Card => self.card.as_ref().map(|v| v as _),
367395
CipherType::Identity => self.identity.as_ref().map(|v| v as _),
368396
CipherType::SshKey => self.ssh_key.as_ref().map(|v| v as _),
369-
_ => None,
397+
CipherType::SecureNote => self.secure_note.as_ref().map(|v| v as _),
370398
}
371399
}
372400

@@ -380,6 +408,14 @@ impl Cipher {
380408
.map(|sub| sub.decrypt_subtitle(ctx, key))
381409
.unwrap_or_else(|| Ok(String::new()))
382410
}
411+
412+
/// Returns a list of copyable field names for this cipher,
413+
/// based on the cipher type and populated properties.
414+
fn get_copyable_fields(&self) -> Vec<CopyableCipherFields> {
415+
self.get_kind()
416+
.map(|kind| kind.get_copyable_fields(Some(self)))
417+
.unwrap_or_default()
418+
}
383419
}
384420

385421
impl CipherView {
@@ -586,9 +622,16 @@ impl Decryptable<KeyIds, SymmetricKeyId, CipherListView> for Cipher {
586622
.as_ref()
587623
.map(|a| a.len() as u32)
588624
.unwrap_or(0),
625+
has_old_attachments: self
626+
.attachments
627+
.as_ref()
628+
.map(|a| a.iter().any(|att| att.key.is_none()))
629+
.unwrap_or(false),
589630
creation_date: self.creation_date,
590631
deleted_date: self.deleted_date,
591632
revision_date: self.revision_date,
633+
copyable_fields: self.get_copyable_fields(),
634+
local_data: self.local_data.decrypt(ctx, ciphers_key)?,
592635
})
593636
}
594637
}
@@ -837,9 +880,16 @@ mod tests {
837880
permissions: cipher.permissions,
838881
view_password: cipher.view_password,
839882
attachments: 0,
883+
has_old_attachments: false,
840884
creation_date: cipher.creation_date,
841885
deleted_date: cipher.deleted_date,
842-
revision_date: cipher.revision_date
886+
revision_date: cipher.revision_date,
887+
copyable_fields: vec![
888+
CopyableCipherFields::LoginUsername,
889+
CopyableCipherFields::LoginPassword,
890+
CopyableCipherFields::LoginTotp
891+
],
892+
local_data: None,
843893
}
844894
)
845895
}

crates/bitwarden-vault/src/cipher/identity.rs

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize};
66
use tsify_next::Tsify;
77

88
use super::cipher::CipherKind;
9-
use crate::VaultParseError;
9+
use crate::{cipher::cipher::CopyableCipherFields, Cipher, VaultParseError};
1010

1111
#[derive(Serialize, Deserialize, Debug, Clone)]
1212
#[serde(rename_all = "camelCase", deny_unknown_fields)]
@@ -163,6 +163,31 @@ impl CipherKind for Identity {
163163

164164
Ok(build_subtitle_identity(first_name, last_name))
165165
}
166+
167+
fn get_copyable_fields(&self, _: Option<&Cipher>) -> Vec<CopyableCipherFields> {
168+
[
169+
self.username
170+
.as_ref()
171+
.map(|_| CopyableCipherFields::IdentityUsername),
172+
self.email
173+
.as_ref()
174+
.map(|_| CopyableCipherFields::IdentityEmail),
175+
self.phone
176+
.as_ref()
177+
.map(|_| CopyableCipherFields::IdentityPhone),
178+
self.address1
179+
.as_ref()
180+
.or(self.address2.as_ref())
181+
.or(self.address3.as_ref())
182+
.or(self.city.as_ref())
183+
.or(self.state.as_ref())
184+
.or(self.postal_code.as_ref())
185+
.map(|_| CopyableCipherFields::IdentityAddress),
186+
]
187+
.into_iter()
188+
.flatten()
189+
.collect()
190+
}
166191
}
167192

168193
/// Builds the subtitle for a card cipher
@@ -193,6 +218,30 @@ fn build_subtitle_identity(first_name: Option<String>, last_name: Option<String>
193218
#[cfg(test)]
194219
mod tests {
195220
use super::*;
221+
use crate::cipher::cipher::CopyableCipherFields;
222+
223+
fn create_identity() -> Identity {
224+
Identity {
225+
title: None,
226+
first_name: None,
227+
middle_name: None,
228+
last_name: None,
229+
address1: None,
230+
address2: None,
231+
address3: None,
232+
city: None,
233+
state: None,
234+
postal_code: None,
235+
country: None,
236+
company: None,
237+
email: None,
238+
phone: None,
239+
ssn: None,
240+
username: None,
241+
passport_number: None,
242+
license_number: None,
243+
}
244+
}
196245

197246
#[test]
198247
fn test_build_subtitle_identity() {
@@ -229,4 +278,59 @@ mod tests {
229278
let subtitle = build_subtitle_identity(first_name, last_name);
230279
assert_eq!(subtitle, "");
231280
}
281+
282+
#[test]
283+
fn test_get_copyable_fields_identity_empty() {
284+
let identity = create_identity();
285+
286+
let copyable_fields = identity.get_copyable_fields(None);
287+
assert_eq!(copyable_fields, vec![]);
288+
}
289+
290+
#[test]
291+
fn test_get_copyable_fields_identity_has_username() {
292+
let mut identity = create_identity();
293+
identity.username = Some("2.yXXpPbsf6NZhLVkNe/i4Bw==|ol/HTI++aMO1peBBBhSR7Q==|awNmmj31efIXTzaru42/Ay+bQ6V+1MrKxXh1Uo5gca8=".parse().unwrap());
294+
295+
let copyable_fields = identity.get_copyable_fields(None);
296+
assert_eq!(
297+
copyable_fields,
298+
vec![CopyableCipherFields::IdentityUsername]
299+
);
300+
}
301+
302+
#[test]
303+
fn test_get_copyable_fields_identity_has_email() {
304+
let mut identity = create_identity();
305+
identity.email = Some("2.yXXpPbsf6NZhLVkNe/i4Bw==|ol/HTI++aMO1peBBBhSR7Q==|awNmmj31efIXTzaru42/Ay+bQ6V+1MrKxXh1Uo5gca8=".parse().unwrap());
306+
307+
let copyable_fields = identity.get_copyable_fields(None);
308+
assert_eq!(copyable_fields, vec![CopyableCipherFields::IdentityEmail]);
309+
}
310+
311+
#[test]
312+
fn test_get_copyable_fields_identity_has_phone() {
313+
let mut identity = create_identity();
314+
identity.phone = Some("2.yXXpPbsf6NZhLVkNe/i4Bw==|ol/HTI++aMO1peBBBhSR7Q==|awNmmj31efIXTzaru42/Ay+bQ6V+1MrKxXh1Uo5gca8=".parse().unwrap());
315+
316+
let copyable_fields = identity.get_copyable_fields(None);
317+
assert_eq!(copyable_fields, vec![CopyableCipherFields::IdentityPhone]);
318+
}
319+
320+
#[test]
321+
fn test_get_copyable_fields_identity_has_address() {
322+
let mut identity = create_identity();
323+
324+
identity.address1 = Some("2.yXXpPbsf6NZhLVkNe/i4Bw==|ol/HTI++aMO1peBBBhSR7Q==|awNmmj31efIXTzaru42/Ay+bQ6V+1MrKxXh1Uo5gca8=".parse().unwrap());
325+
326+
let mut copyable_fields = identity.get_copyable_fields(None);
327+
328+
assert_eq!(copyable_fields, vec![CopyableCipherFields::IdentityAddress]);
329+
330+
identity.state = Some("2.yXXpPbsf6NZhLVkNe/i4Bw==|ol/HTI++aMO1peBBBhSR7Q==|awNmmj31efIXTzaru42/Ay+bQ6V+1MrKxXh1Uo5gca8=".parse().unwrap());
331+
identity.address1 = None;
332+
333+
copyable_fields = identity.get_copyable_fields(None);
334+
assert_eq!(copyable_fields, vec![CopyableCipherFields::IdentityAddress]);
335+
}
232336
}

crates/bitwarden-vault/src/cipher/local_data.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ pub struct LocalData {
1414
last_launched: Option<DateTime<Utc>>,
1515
}
1616

17-
#[derive(Serialize, Deserialize, Debug, Clone)]
17+
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
1818
#[serde(rename_all = "camelCase", deny_unknown_fields)]
1919
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
2020
#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]

0 commit comments

Comments
 (0)