Skip to content

Commit 8722077

Browse files
committed
Merge branch 'km/cose-content-format' of github.com:bitwarden/sdk-internal into km/cose-content-format
2 parents 916a46e + 9eb4ff8 commit 8722077

File tree

10 files changed

+416
-11
lines changed

10 files changed

+416
-11
lines changed

.github/workflows/lint.yml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ on:
66
branches: ["main"]
77
pull_request:
88

9+
permissions: {}
10+
911
env:
1012
CARGO_TERM_COLOR: always
1113

@@ -15,6 +17,10 @@ jobs:
1517

1618
runs-on: ubuntu-24.04
1719

20+
permissions:
21+
contents: read
22+
security-events: write
23+
1824
steps:
1925
- name: Checkout
2026
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
@@ -54,7 +60,7 @@ jobs:
5460
run: cargo install clippy-sarif sarif-fmt --locked --git https://github.com/psastras/sarif-rs.git --rev 11c33a53f6ffeaed736856b86fb6b7b09fabdfd8
5561

5662
- name: Cargo clippy-sarif
57-
run: cargo clippy --all-features --tests --message-format=json |
63+
run: cargo clippy --all-features --all-targets --message-format=json |
5864
clippy-sarif | tee clippy_result.sarif | sarif-fmt
5965
env:
6066
RUSTFLAGS: "-D warnings"
@@ -70,7 +76,7 @@ jobs:
7076
# status code of the command is caught and reported as failed in GitHub.
7177
# This should be cached from the previous step and should be fast.
7278
- name: Cargo clippy
73-
run: cargo clippy --all-features --tests
79+
run: cargo clippy --all-features --all-targets
7480
env:
7581
RUSTFLAGS: "-D warnings"
7682

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ export RUSTFLAGS="-D warnings"
144144
145145
cargo +nightly fmt --check
146146
cargo +nightly udeps --workspace --all-features
147-
cargo clippy --all-features --tests
147+
cargo clippy --all-features --all-targets
148148
cargo sort --workspace --check
149149
npm run lint
150150
```

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

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

1111
use super::cipher::CipherKind;
12-
use crate::VaultParseError;
12+
use crate::{cipher::cipher::CopyableCipherFields, Cipher, VaultParseError};
1313

1414
#[derive(Serialize, Deserialize, Debug, Clone)]
1515
#[serde(rename_all = "camelCase", deny_unknown_fields)]
@@ -146,6 +146,20 @@ impl CipherKind for Card {
146146

147147
Ok(build_subtitle_card(brand, number))
148148
}
149+
150+
fn get_copyable_fields(&self, _: Option<&Cipher>) -> Vec<CopyableCipherFields> {
151+
[
152+
self.number
153+
.as_ref()
154+
.map(|_| CopyableCipherFields::CardNumber),
155+
self.code
156+
.as_ref()
157+
.map(|_| CopyableCipherFields::CardSecurityCode),
158+
]
159+
.into_iter()
160+
.flatten()
161+
.collect()
162+
}
149163
}
150164

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

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

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

5962
#[allow(missing_docs)]
@@ -187,6 +190,24 @@ pub enum CipherListViewType {
187190
SshKey,
188191
}
189192

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

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

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

225253
impl CipherListView {
@@ -373,7 +401,7 @@ impl Cipher {
373401
CipherType::Card => self.card.as_ref().map(|v| v as _),
374402
CipherType::Identity => self.identity.as_ref().map(|v| v as _),
375403
CipherType::SshKey => self.ssh_key.as_ref().map(|v| v as _),
376-
_ => None,
404+
CipherType::SecureNote => self.secure_note.as_ref().map(|v| v as _),
377405
}
378406
}
379407

@@ -387,6 +415,14 @@ impl Cipher {
387415
.map(|sub| sub.decrypt_subtitle(ctx, key))
388416
.unwrap_or_else(|| Ok(String::new()))
389417
}
418+
419+
/// Returns a list of copyable field names for this cipher,
420+
/// based on the cipher type and populated properties.
421+
fn get_copyable_fields(&self) -> Vec<CopyableCipherFields> {
422+
self.get_kind()
423+
.map(|kind| kind.get_copyable_fields(Some(self)))
424+
.unwrap_or_default()
425+
}
390426
}
391427

392428
impl CipherView {
@@ -596,9 +632,16 @@ impl Decryptable<KeyIds, SymmetricKeyId, CipherListView> for Cipher {
596632
.as_ref()
597633
.map(|a| a.len() as u32)
598634
.unwrap_or(0),
635+
has_old_attachments: self
636+
.attachments
637+
.as_ref()
638+
.map(|a| a.iter().any(|att| att.key.is_none()))
639+
.unwrap_or(false),
599640
creation_date: self.creation_date,
600641
deleted_date: self.deleted_date,
601642
revision_date: self.revision_date,
643+
copyable_fields: self.get_copyable_fields(),
644+
local_data: self.local_data.decrypt(ctx, ciphers_key)?,
602645
})
603646
}
604647
}
@@ -847,9 +890,16 @@ mod tests {
847890
permissions: cipher.permissions,
848891
view_password: cipher.view_password,
849892
attachments: 0,
893+
has_old_attachments: false,
850894
creation_date: cipher.creation_date,
851895
deleted_date: cipher.deleted_date,
852-
revision_date: cipher.revision_date
896+
revision_date: cipher.revision_date,
897+
copyable_fields: vec![
898+
CopyableCipherFields::LoginUsername,
899+
CopyableCipherFields::LoginPassword,
900+
CopyableCipherFields::LoginTotp
901+
],
902+
local_data: None,
853903
}
854904
)
855905
}

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

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

1111
use super::cipher::CipherKind;
12-
use crate::VaultParseError;
12+
use crate::{cipher::cipher::CopyableCipherFields, Cipher, VaultParseError};
1313

1414
#[derive(Serialize, Deserialize, Debug, Clone)]
1515
#[serde(rename_all = "camelCase", deny_unknown_fields)]
@@ -166,6 +166,31 @@ impl CipherKind for Identity {
166166

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

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

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

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)