Skip to content

Commit f75f62e

Browse files
authored
PM-23643: ApiKey -> SecureNote (#362)
This PR maps ApiKey -> SecureNote. The reason it's done in a separate file is to minimize the amount of merge conflicts since we're doing multiple PR's at the same time.
1 parent d3d7976 commit f75f62e

File tree

4 files changed

+240
-4
lines changed

4 files changed

+240
-4
lines changed
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
use credential_exchange_format::ApiKeyCredential;
2+
3+
use crate::{cxf::editable_field::create_field, Field};
4+
5+
/// Convert API key credentials to custom fields
6+
pub fn api_key_to_fields(api_key: &ApiKeyCredential) -> Vec<Field> {
7+
[
8+
api_key.key.as_ref().map(|key| create_field("API Key", key)),
9+
api_key
10+
.username
11+
.as_ref()
12+
.map(|username| create_field("Username", username)),
13+
api_key
14+
.key_type
15+
.as_ref()
16+
.map(|key_type| create_field("Key Type", key_type)),
17+
api_key.url.as_ref().map(|url| create_field("URL", url)),
18+
api_key
19+
.valid_from
20+
.as_ref()
21+
.map(|valid_from| create_field("Valid From", valid_from)),
22+
api_key
23+
.expiry_date
24+
.as_ref()
25+
.map(|expiry_date| create_field("Expiry Date", expiry_date)),
26+
]
27+
.into_iter()
28+
.flatten()
29+
.collect()
30+
}
31+
32+
#[cfg(test)]
33+
mod tests {
34+
use bitwarden_vault::FieldType;
35+
use chrono::NaiveDate;
36+
use credential_exchange_format::{EditableFieldConcealedString, EditableFieldDate};
37+
38+
use super::*;
39+
40+
#[test]
41+
fn test_api_key_to_fields_all_fields() {
42+
let api_key = ApiKeyCredential {
43+
key: Some(
44+
EditableFieldConcealedString("AIzaSyAyRofL-VJHZofHc-qOSkqVOdhvgQoJADk".to_string())
45+
.into(),
46+
),
47+
username: Some("john_doe".to_string().into()),
48+
key_type: Some("Bearer".to_string().into()),
49+
url: Some("https://api.example.com".to_string().into()),
50+
valid_from: Some(
51+
EditableFieldDate(NaiveDate::from_ymd_opt(2025, 1, 1).unwrap()).into(),
52+
),
53+
expiry_date: Some(
54+
EditableFieldDate(NaiveDate::from_ymd_opt(2026, 1, 1).unwrap()).into(),
55+
),
56+
};
57+
58+
let fields = api_key_to_fields(&api_key);
59+
60+
let expected_fields = vec![
61+
Field {
62+
name: Some("API Key".to_string()),
63+
value: Some("AIzaSyAyRofL-VJHZofHc-qOSkqVOdhvgQoJADk".to_string()),
64+
r#type: FieldType::Hidden as u8,
65+
linked_id: None,
66+
},
67+
Field {
68+
name: Some("Username".to_string()),
69+
value: Some("john_doe".to_string()),
70+
r#type: FieldType::Text as u8,
71+
linked_id: None,
72+
},
73+
Field {
74+
name: Some("Key Type".to_string()),
75+
value: Some("Bearer".to_string()),
76+
r#type: FieldType::Text as u8,
77+
linked_id: None,
78+
},
79+
Field {
80+
name: Some("URL".to_string()),
81+
value: Some("https://api.example.com".to_string()),
82+
r#type: FieldType::Text as u8,
83+
linked_id: None,
84+
},
85+
Field {
86+
name: Some("Valid From".to_string()),
87+
value: Some("2025-01-01".to_string()),
88+
r#type: FieldType::Text as u8,
89+
linked_id: None,
90+
},
91+
Field {
92+
name: Some("Expiry Date".to_string()),
93+
value: Some("2026-01-01".to_string()),
94+
r#type: FieldType::Text as u8,
95+
linked_id: None,
96+
},
97+
];
98+
99+
assert_eq!(fields, expected_fields);
100+
}
101+
102+
#[test]
103+
fn test_api_key_to_fields_minimal() {
104+
let api_key = ApiKeyCredential {
105+
key: Some("test-api-key".to_string().into()),
106+
username: None,
107+
key_type: None,
108+
url: None,
109+
valid_from: None,
110+
expiry_date: None,
111+
};
112+
113+
let fields = api_key_to_fields(&api_key);
114+
115+
let expected_fields = vec![Field {
116+
name: Some("API Key".to_string()),
117+
value: Some("test-api-key".to_string()),
118+
r#type: FieldType::Hidden as u8,
119+
linked_id: None,
120+
}];
121+
122+
assert_eq!(fields, expected_fields);
123+
}
124+
125+
#[test]
126+
fn test_api_key_to_fields_empty() {
127+
let api_key = ApiKeyCredential {
128+
key: None,
129+
username: None,
130+
key_type: None,
131+
url: None,
132+
valid_from: None,
133+
expiry_date: None,
134+
};
135+
136+
let fields = api_key_to_fields(&api_key);
137+
138+
assert_eq!(fields, vec![]);
139+
}
140+
141+
#[test]
142+
fn test_api_key_to_fields_partial() {
143+
let api_key = ApiKeyCredential {
144+
key: Some("secret-key".to_string().into()),
145+
username: Some("test_user".to_string().into()),
146+
key_type: Some("API_KEY".to_string().into()),
147+
url: None,
148+
valid_from: None,
149+
expiry_date: None,
150+
};
151+
152+
let fields = api_key_to_fields(&api_key);
153+
154+
let expected_fields = vec![
155+
Field {
156+
name: Some("API Key".to_string()),
157+
value: Some("secret-key".to_string()),
158+
r#type: FieldType::Hidden as u8,
159+
linked_id: None,
160+
},
161+
Field {
162+
name: Some("Username".to_string()),
163+
value: Some("test_user".to_string()),
164+
r#type: FieldType::Text as u8,
165+
linked_id: None,
166+
},
167+
Field {
168+
name: Some("Key Type".to_string()),
169+
value: Some("API_KEY".to_string()),
170+
r#type: FieldType::Text as u8,
171+
linked_id: None,
172+
},
173+
];
174+
175+
assert_eq!(fields, expected_fields);
176+
}
177+
}

crates/bitwarden-exporters/src/cxf/editable_field.rs

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use bitwarden_vault::FieldType;
22
use credential_exchange_format::{
3-
EditableField, EditableFieldBoolean, EditableFieldConcealedString, EditableFieldString,
4-
EditableFieldWifiNetworkSecurityType,
3+
EditableField, EditableFieldBoolean, EditableFieldConcealedString, EditableFieldDate,
4+
EditableFieldString, EditableFieldWifiNetworkSecurityType,
55
};
66

77
use crate::Field;
@@ -58,6 +58,14 @@ impl EditableFieldToField for EditableField<EditableFieldWifiNetworkSecurityType
5858
}
5959
}
6060

61+
impl EditableFieldToField for EditableField<EditableFieldDate> {
62+
const FIELD_TYPE: FieldType = FieldType::Text;
63+
64+
fn field_value(&self) -> String {
65+
self.value.0.to_string()
66+
}
67+
}
68+
6169
/// Convert WiFi security type enum to human-readable string
6270
fn security_type_to_string(security_type: &EditableFieldWifiNetworkSecurityType) -> &str {
6371
use EditableFieldWifiNetworkSecurityType::*;
@@ -216,4 +224,28 @@ mod tests {
216224
custom_security
217225
);
218226
}
227+
228+
#[test]
229+
fn test_create_field_date() {
230+
use chrono::NaiveDate;
231+
232+
let editable_field = EditableField {
233+
id: None,
234+
label: None,
235+
value: EditableFieldDate(NaiveDate::from_ymd_opt(2025, 1, 15).unwrap()),
236+
extensions: None,
237+
};
238+
239+
let field = create_field("Expiry Date".to_string(), &editable_field);
240+
241+
assert_eq!(
242+
field,
243+
Field {
244+
name: Some("Expiry Date".to_string()),
245+
value: Some("2025-01-15".to_string()),
246+
r#type: FieldType::Text as u8,
247+
linked_id: None,
248+
}
249+
);
250+
}
219251
}

crates/bitwarden-exporters/src/cxf/import.rs

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
use chrono::{DateTime, Utc};
22
use credential_exchange_format::{
3-
Account as CxfAccount, BasicAuthCredential, Credential, CreditCardCredential, Item,
4-
PasskeyCredential, WifiCredential,
3+
Account as CxfAccount, ApiKeyCredential, BasicAuthCredential, Credential, CreditCardCredential,
4+
Item, PasskeyCredential, WifiCredential,
55
};
66

77
use crate::{
88
cxf::{
9+
api_key::api_key_to_fields,
910
login::{to_fields, to_login},
1011
wifi::wifi_to_fields,
1112
CxfError,
@@ -80,6 +81,26 @@ fn parse_item(value: Item) -> Vec<ImportingCipher> {
8081
})
8182
}
8283

84+
// API Key credentials -> Secure Note
85+
if let Some(api_key) = grouped.api_key.first() {
86+
let fields = api_key_to_fields(api_key);
87+
88+
output.push(ImportingCipher {
89+
folder_id: None, // TODO: Handle folders
90+
name: value.title.clone(),
91+
notes: None,
92+
r#type: CipherType::SecureNote(Box::new(SecureNote {
93+
r#type: SecureNoteType::Generic,
94+
})),
95+
favorite: false,
96+
reprompt: 0,
97+
fields,
98+
revision_date,
99+
creation_date,
100+
deleted_date: None,
101+
})
102+
}
103+
83104
// WiFi credentials -> Secure Note
84105
if let Some(wifi) = grouped.wifi.first() {
85106
let fields = wifi_to_fields(wifi);
@@ -121,6 +142,10 @@ fn group_credentials_by_type(credentials: Vec<Credential>) -> GroupedCredentials
121142
}
122143

123144
GroupedCredentials {
145+
api_key: filter_credentials(&credentials, |c| match c {
146+
Credential::ApiKey(api_key) => Some(api_key.as_ref()),
147+
_ => None,
148+
}),
124149
basic_auth: filter_credentials(&credentials, |c| match c {
125150
Credential::BasicAuth(basic_auth) => Some(basic_auth.as_ref()),
126151
_ => None,
@@ -141,6 +166,7 @@ fn group_credentials_by_type(credentials: Vec<Credential>) -> GroupedCredentials
141166
}
142167

143168
struct GroupedCredentials {
169+
api_key: Vec<ApiKeyCredential>,
144170
basic_auth: Vec<BasicAuthCredential>,
145171
passkey: Vec<PasskeyCredential>,
146172
credit_card: Vec<CreditCardCredential>,

crates/bitwarden-exporters/src/cxf/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ pub(crate) use export::build_cxf;
1212
pub use export::Account;
1313
mod import;
1414
pub(crate) use import::parse_cxf;
15+
mod api_key;
1516
mod card;
1617
mod editable_field;
1718
mod login;

0 commit comments

Comments
 (0)