Skip to content

Commit

Permalink
Initial work on exporters
Browse files Browse the repository at this point in the history
  • Loading branch information
Hinton committed Jan 30, 2024
1 parent 3064ac5 commit b61db67
Show file tree
Hide file tree
Showing 10 changed files with 360 additions and 17 deletions.
33 changes: 33 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 24 additions & 0 deletions crates/bitwarden-exporters/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[package]
name = "bitwarden-exporters"
version = "0.1.0"
authors = ["Bitwarden Inc"]
license-file = "LICENSE"
repository = "https://github.com/bitwarden/sdk"
homepage = "https://bitwarden.com"
description = """
Internal crate for the bitwarden crate. Do not use.
"""
keywords = ["bitwarden"]
edition = "2021"
rust-version = "1.57"

[dependencies]
chrono = { version = ">=0.4.26, <0.5", features = [
"clock",
"serde",
"std",
], default-features = false }
csv = "1.3.0"
serde = { version = ">=1.0, <2.0", features = ["derive"] }
serde_json = ">=1.0.96, <2.0"
uuid = { version = ">=1.3.3, <2.0", features = ["serde"] }
6 changes: 6 additions & 0 deletions crates/bitwarden-exporters/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Bitwarden Exporters

This is an internal crate for the Bitwarden SDK do not depend on this directly and use the
[`bitwarden`](https://crates.io/crates/bitwarden) crate instead.

This crate does not follow semantic versioning and the public interface may change at any time.
169 changes: 169 additions & 0 deletions crates/bitwarden-exporters/src/csv.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
use std::collections::HashMap;

use csv::Writer;
use serde::Serializer;
use uuid::Uuid;

use crate::{Cipher, CipherType, Field, Folder};

pub(crate) fn export_csv(folders: Vec<Folder>, ciphers: Vec<Cipher>) -> Result<String, String> {
let folders: HashMap<Uuid, String> = folders.iter().map(|f| (f.id, f.name.clone())).collect();

let rows = ciphers.iter().map(|c| {
let login = if let CipherType::Login(l) = &c.r#type {
Some(l)
} else {
None
};

CsvRow {
folder: c
.folder_id
.and_then(|f| folders.get(&f))
.map(|f| f.to_owned()),
favorite: c.favorite,
r#type: c.r#type.to_string(),
name: c.name.to_owned(),
notes: c.notes.to_owned(),
fields: c.fields.clone(),
reprompt: c.reprompt,
login_uri: login.map(|l| l.login_uris.clone()).unwrap_or_default(),
login_username: login.map(|l| l.username.clone()),
login_password: login.map(|l| l.password.clone()),
login_totp: login.and_then(|l| l.totp.clone()),
}
});

let mut wtr = Writer::from_writer(vec![]);
for row in rows {
wtr.serialize(row).unwrap();
}

String::from_utf8(
wtr.into_inner()
.map_err(|_| "Failed to write CSV".to_string())?,
)
.map_err(|_| "Failed to convert CSV to UTF-8".to_string())
}

#[derive(serde::Serialize)]
struct CsvRow {
folder: Option<String>,
#[serde(serialize_with = "bool_serialize")]
favorite: bool,
r#type: String,
name: String,
notes: Option<String>,
#[serde(serialize_with = "fields_serialize")]
fields: Vec<Field>,
reprompt: u8,
#[serde(serialize_with = "vec_serialize")]
login_uri: Vec<String>,
login_username: Option<String>,
login_password: Option<String>,
login_totp: Option<String>,
}

fn vec_serialize<S>(x: &[String], s: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
s.serialize_str(x.join(",").as_str())
}

fn bool_serialize<S>(x: &bool, s: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
s.serialize_str(if *x { "1" } else { "" })
}

fn fields_serialize<S>(x: &[Field], s: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
s.serialize_str(
x.iter()
.map(|f| {
format!(
"{}: {}",
f.name.to_owned().unwrap_or_default(),
f.value.to_owned().unwrap_or_default()
)
})
.collect::<Vec<String>>()
.join("\n")
.as_str(),
)
}

#[cfg(test)]
mod tests {
use super::*;
use crate::CipherLogin;

#[test]
fn test_export_csv() {
let folders = vec![
Folder {
id: "d55d65d7-c161-40a4-94ca-b0d20184d91a".parse().unwrap(),
name: "Test Folder A".to_string(),
},
Folder {
id: "583e7665-0126-4d37-9139-b0d20184dd86".parse().unwrap(),
name: "Test Folder B".to_string(),
},
];
let ciphers = vec![
Cipher {
id: "d55d65d7-c161-40a4-94ca-b0d20184d91a".parse().unwrap(),
folder_id: None,
name: "[email protected]".to_string(),
notes: None,
r#type: CipherType::Login(CipherLogin {
username: "[email protected]".to_string(),
password: "Abc123".to_string(),
login_uris: vec!["https://google.com".to_string()],
totp: None,
}),
favorite: false,
reprompt: 0,
fields: vec![],
},
Cipher {
id: "7dd81bd0-cc72-4f42-96e7-b0fc014e71a3".parse().unwrap(),
folder_id: Some("583e7665-0126-4d37-9139-b0d20184dd86".parse().unwrap()),
name: "Steam Account".to_string(),
notes: None,
r#type: CipherType::Login(CipherLogin {
username: "steam".to_string(),
password: "3Pvb8u7EfbV*nJ".to_string(),
login_uris: vec!["https://steampowered.com".to_string()],
totp: Some("steam://ABCD123".to_string()),
}),
favorite: true,
reprompt: 0,
fields: vec![
Field {
name: Some("Test".to_string()),
value: Some("v".to_string()),
},
Field {
name: Some("Hidden".to_string()),
value: Some("asdfer".to_string()),
},
],
},
];

let csv = export_csv(folders, ciphers).unwrap();
let expected = [
"folder,favorite,type,name,notes,fields,reprompt,login_uri,login_username,login_password,login_totp",
",,login,[email protected],,,0,https://google.com,[email protected],Abc123,",
"Test Folder B,1,login,Steam Account,,\"Test: v\nHidden: asdfer\",0,https://steampowered.com,steam,3Pvb8u7EfbV*nJ,steam://ABCD123",
"",
].join("\n");

assert_eq!(csv, expected);
}
}
11 changes: 11 additions & 0 deletions crates/bitwarden-exporters/src/json.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
use crate::{Cipher, Folder};

pub(crate) fn export_json(folders: Vec<Folder>, ciphers: Vec<Cipher>) -> Result<String, String> {
Ok("".to_owned())
}

struct JsonExport {
encrypted: bool,
folders: Vec<Folder>,
ciphers: Vec<Cipher>,
}
69 changes: 69 additions & 0 deletions crates/bitwarden-exporters/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
use chrono::{DateTime, Utc};
use uuid::Uuid;

mod csv;
use csv::export_csv;
mod json;
use json::export_json;

pub enum Format {
Csv,
Json,
EncryptedJson { password: String },
}

pub struct Folder {
pub id: Uuid,
pub name: String,
}

pub struct Cipher {
pub id: Uuid,
pub folder_id: Option<Uuid>,

pub name: String,
pub notes: Option<String>,

pub r#type: CipherType,

pub favorite: bool,
pub reprompt: u8,

pub fields: Vec<Field>,
}

#[derive(Clone)]
pub struct Field {
name: Option<String>,
value: Option<String>,
}

pub enum CipherType {
Login(CipherLogin),
Identity(),
}

impl ToString for CipherType {
fn to_string(&self) -> String {
match self {
CipherType::Login(_) => "login".to_string(),
CipherType::Identity() => "identity".to_string(),
}
}
}

pub struct CipherLogin {
pub username: String,
pub password: String,
pub login_uris: Vec<String>,
pub totp: Option<String>,
}

pub fn export(folders: Vec<Folder>, ciphers: Vec<Cipher>, format: Format) -> String {
match format {
Format::Csv => export_csv(folders, ciphers).unwrap(),
Format::Json => export_json(folders, ciphers).unwrap(),
// Format::EncryptedJson { password } => export_encrypted_json(folders, ciphers, password),
_ => todo!(),
}
}
1 change: 1 addition & 0 deletions crates/bitwarden/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ base64 = ">=0.21.2, <0.22"
bitwarden-api-api = { path = "../bitwarden-api-api", version = "=0.2.3" }
bitwarden-api-identity = { path = "../bitwarden-api-identity", version = "=0.2.3" }
bitwarden-crypto = { path = "../bitwarden-crypto", version = "=0.1.0" }
bitwarden-exporters = { path = "../bitwarden-exporters", version = "0.1.0" }
bitwarden-generators = { path = "../bitwarden-generators", version = "0.1.0" }
chrono = { version = ">=0.4.26, <0.5", features = [
"clock",
Expand Down
6 changes: 3 additions & 3 deletions crates/bitwarden/src/tool/exporters/client_exporter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use crate::{
};

pub struct ClientExporters<'a> {
pub(crate) _client: &'a crate::Client,
pub(crate) client: &'a crate::Client,
}

impl<'a> ClientExporters<'a> {
Expand All @@ -17,7 +17,7 @@ impl<'a> ClientExporters<'a> {
ciphers: Vec<Cipher>,
format: ExportFormat,
) -> Result<String> {
export_vault(folders, ciphers, format)
export_vault(self.client, folders, ciphers, format)
}

pub async fn export_organization_vault(
Expand All @@ -32,6 +32,6 @@ impl<'a> ClientExporters<'a> {

impl<'a> Client {
pub fn exporters(&'a self) -> ClientExporters<'a> {
ClientExporters { _client: self }
ClientExporters { client: self }
}
}
Loading

0 comments on commit b61db67

Please sign in to comment.