Skip to content

Commit 31323f5

Browse files
committed
Initial implementation for converting passwords to BasicAuth
1 parent e956c2c commit 31323f5

File tree

4 files changed

+201
-2
lines changed

4 files changed

+201
-2
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/bitwarden-exporters/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ bitwarden-core = { workspace = true }
2323
bitwarden-crypto = { workspace = true }
2424
bitwarden-vault = { workspace = true }
2525
chrono = { workspace = true, features = ["std"] }
26-
credential-exchange-types = { git = "https://github.com/bitwarden/credential-exchange.git", rev = "224243dd996d7e7cfa22b1bae05cdc7db85b3b3b" }
26+
credential-exchange-types = { git = "https://github.com/bitwarden/credential-exchange.git", rev = "e37495428d0a0486df3f8be95b895f3dc0d9eb42" }
2727
csv = "1.3.0"
2828
schemars = { workspace = true }
2929
serde = { workspace = true }

crates/bitwarden-exporters/src/cxp.rs

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
use bitwarden_crypto::generate_random_bytes;
2+
use credential_exchange_types::{
3+
format::{BasicAuthCredential, Credential, EditableField, FieldType, Item, ItemType},
4+
B64Url,
5+
};
6+
7+
use crate::{Cipher, CipherType, Login, LoginUri};
8+
9+
impl From<Cipher> for Item {
10+
fn from(value: Cipher) -> Self {
11+
let credentials = value.r#type.clone().into();
12+
Self {
13+
id: value.id.as_bytes().as_slice().into(),
14+
creation_at: value.creation_date.timestamp() as u64,
15+
modified_at: value.revision_date.timestamp() as u64,
16+
ty: value.r#type.into(),
17+
title: value.name,
18+
subtitle: None,
19+
credentials,
20+
tags: None,
21+
extensions: None,
22+
}
23+
}
24+
}
25+
26+
impl From<CipherType> for ItemType {
27+
// TODO: We should probably change this to try_from, so we can ignore types
28+
fn from(value: CipherType) -> Self {
29+
match value {
30+
CipherType::Login(_) => ItemType::Login,
31+
CipherType::Card(_) => ItemType::Login,
32+
CipherType::Identity(_) => ItemType::Identity,
33+
CipherType::SecureNote(_) => ItemType::Document,
34+
CipherType::SshKey(_) => todo!(),
35+
}
36+
}
37+
}
38+
39+
impl From<Login> for Vec<Credential> {
40+
fn from(login: Login) -> Self {
41+
vec![Credential::BasicAuth(BasicAuthCredential {
42+
urls: login
43+
.login_uris
44+
.into_iter()
45+
.flat_map(|uri| uri.uri)
46+
.collect(),
47+
username: login.username.map(|value| EditableField {
48+
id: random_id(),
49+
field_type: FieldType::String,
50+
value,
51+
label: None,
52+
}),
53+
password: login.password.map(|value| EditableField {
54+
id: random_id(),
55+
field_type: FieldType::ConcealedString,
56+
value,
57+
label: None,
58+
}),
59+
})]
60+
}
61+
}
62+
63+
impl From<CipherType> for Vec<Credential> {
64+
fn from(value: CipherType) -> Self {
65+
match value {
66+
CipherType::Login(login) => (*login).into(),
67+
CipherType::Card(_) => vec![],
68+
CipherType::Identity(_) => vec![],
69+
CipherType::SecureNote(_) => vec![],
70+
CipherType::SshKey(_) => vec![],
71+
}
72+
}
73+
}
74+
75+
/// Generate a 32 byte random ID
76+
fn random_id() -> B64Url {
77+
generate_random_bytes::<[u8; 32]>().as_slice().into()
78+
}
79+
80+
#[cfg(test)]
81+
mod tests {
82+
use chrono::{DateTime, Utc};
83+
84+
use super::*;
85+
use crate::{CipherType, Field, Login, LoginUri};
86+
87+
#[test]
88+
fn test_login_to_item() {
89+
let cipher = Cipher {
90+
id: "25c8c414-b446-48e9-a1bd-b10700bbd740".parse().unwrap(),
91+
folder_id: Some("942e2984-1b9a-453b-b039-b107012713b9".parse().unwrap()),
92+
93+
name: "Bitwarden".to_string(),
94+
notes: Some("My note".to_string()),
95+
96+
r#type: CipherType::Login(Box::new(Login {
97+
username: Some("[email protected]".to_string()),
98+
password: Some("asdfasdfasdf".to_string()),
99+
login_uris: vec![LoginUri {
100+
uri: Some("https://vault.bitwarden.com".to_string()),
101+
r#match: None,
102+
}],
103+
totp: Some("ABC".to_string()),
104+
})),
105+
106+
favorite: true,
107+
reprompt: 0,
108+
109+
fields: vec![
110+
Field {
111+
name: Some("Text".to_string()),
112+
value: Some("A".to_string()),
113+
r#type: 0,
114+
linked_id: None,
115+
},
116+
Field {
117+
name: Some("Hidden".to_string()),
118+
value: Some("B".to_string()),
119+
r#type: 1,
120+
linked_id: None,
121+
},
122+
Field {
123+
name: Some("Boolean (true)".to_string()),
124+
value: Some("true".to_string()),
125+
r#type: 2,
126+
linked_id: None,
127+
},
128+
Field {
129+
name: Some("Boolean (false)".to_string()),
130+
value: Some("false".to_string()),
131+
r#type: 2,
132+
linked_id: None,
133+
},
134+
Field {
135+
name: Some("Linked".to_string()),
136+
value: None,
137+
r#type: 3,
138+
linked_id: Some(101),
139+
},
140+
],
141+
142+
revision_date: "2024-01-30T14:09:33.753Z".parse().unwrap(),
143+
creation_date: "2024-01-30T11:23:54.416Z".parse().unwrap(),
144+
deleted_date: None,
145+
};
146+
147+
let item: Item = cipher.into();
148+
149+
assert_eq!(
150+
item.creation_at,
151+
"2024-01-30T11:23:54.416Z"
152+
.parse::<DateTime<Utc>>()
153+
.unwrap()
154+
.timestamp() as u64
155+
);
156+
157+
assert_eq!(item.id.to_string(), "JcjEFLRGSOmhvbEHALvXQA");
158+
assert_eq!(item.creation_at, 1706613834);
159+
assert_eq!(item.modified_at, 1706623773);
160+
assert!(matches!(item.ty, ItemType::Login));
161+
assert_eq!(item.title, "Bitwarden");
162+
assert_eq!(item.subtitle, None);
163+
assert_eq!(item.credentials.len(), 1);
164+
assert_eq!(item.tags, None);
165+
assert!(item.extensions.is_none());
166+
167+
let credential = &item.credentials[0];
168+
169+
match credential {
170+
Credential::BasicAuth(basic_auth) => {
171+
let username = basic_auth.username.as_ref().unwrap();
172+
assert!(matches!(username.field_type, FieldType::String));
173+
assert_eq!(username.value, "[email protected]");
174+
assert!(username.label.is_none());
175+
176+
let password = basic_auth.password.as_ref().unwrap();
177+
assert!(matches!(password.field_type, FieldType::ConcealedString));
178+
assert_eq!(password.value, "asdfasdfasdf");
179+
assert!(password.label.is_none());
180+
181+
assert_eq!(
182+
basic_auth.urls,
183+
vec!["https://vault.bitwarden.com".to_string()]
184+
);
185+
}
186+
_ => panic!("Expected Credential::BasicAuth"),
187+
}
188+
}
189+
}

crates/bitwarden-exporters/src/lib.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ uniffi::setup_scaffolding!();
99

1010
mod client_exporter;
1111
mod csv;
12+
mod cxp;
1213
mod encrypted_json;
1314
mod json;
1415
mod models;
@@ -38,6 +39,7 @@ pub struct Folder {
3839
///
3940
/// These are mostly duplicated from the `bitwarden` vault models to facilitate a stable export API
4041
/// that is not tied to the internal vault models. We may revisit this in the future.
42+
#[derive(Clone)]
4143
pub struct Cipher {
4244
pub id: Uuid,
4345
pub folder_id: Option<Uuid>,
@@ -65,6 +67,7 @@ pub struct Field {
6567
pub linked_id: Option<u32>,
6668
}
6769

70+
#[derive(Clone)]
6871
pub enum CipherType {
6972
Login(Box<Login>),
7073
SecureNote(Box<SecureNote>),
@@ -85,18 +88,21 @@ impl fmt::Display for CipherType {
8588
}
8689
}
8790

91+
#[derive(Clone)]
8892
pub struct Login {
8993
pub username: Option<String>,
9094
pub password: Option<String>,
9195
pub login_uris: Vec<LoginUri>,
9296
pub totp: Option<String>,
9397
}
9498

99+
#[derive(Clone)]
95100
pub struct LoginUri {
96101
pub uri: Option<String>,
97102
pub r#match: Option<u8>,
98103
}
99104

105+
#[derive(Clone)]
100106
pub struct Card {
101107
pub cardholder_name: Option<String>,
102108
pub exp_month: Option<String>,
@@ -106,14 +112,17 @@ pub struct Card {
106112
pub number: Option<String>,
107113
}
108114

115+
#[derive(Clone)]
109116
pub struct SecureNote {
110117
pub r#type: SecureNoteType,
111118
}
112119

120+
#[derive(Clone)]
113121
pub enum SecureNoteType {
114122
Generic = 0,
115123
}
116124

125+
#[derive(Clone)]
117126
pub struct Identity {
118127
pub title: Option<String>,
119128
pub first_name: Option<String>,
@@ -135,6 +144,7 @@ pub struct Identity {
135144
pub license_number: Option<String>,
136145
}
137146

147+
#[derive(Clone)]
138148
pub struct SshKey {
139149
/// [OpenSSH private key](https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key), in PEM encoding.
140150
pub private_key: String,

0 commit comments

Comments
 (0)