Skip to content

Commit

Permalink
more unit tests, update README
Browse files Browse the repository at this point in the history
  • Loading branch information
samuelba committed Apr 6, 2024
1 parent ce8d64b commit 1d3da61
Show file tree
Hide file tree
Showing 5 changed files with 132 additions and 48 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ chrono = { version = ">=0.4.26, <0.5", features = [
clap = { version = "=4.5", features = ["derive"] }
serde = { version = ">=1.0, <2.0", features = ["derive"] }
serde_json = ">=1.0.96, <2.0"
thiserror = "1.0.58"
uuid = { version = ">=1.3.3, <2.0", features = ["serde", "v4"] }
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

This is a simple CLI tool to generate an encrypted Bitwarden JSON export file that can be imported into a Bitwarden vault.

The repo contains a submodule to the [Bitwarden SDK](https://github.com/bitwarden/sdk). Initialize it with `git submodule update --init --recursive`.

## Usage

```bash
Expand Down
58 changes: 10 additions & 48 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
mod types;
use types::{JsonCipher, JsonFolder};

use bitwarden_crypto::Kdf;
use bitwarden_exporters::{export, Cipher, Folder};
mod utils;

use clap::Parser;
use std::num;

#[derive(Debug, Parser)]
struct Args {
Expand All @@ -29,49 +25,15 @@ fn main() {
let json_ciphers_str = args.ciphers;
let password = args.password;

// Parse the JSON strings into the respective types.
let json_folders: Vec<JsonFolder> = match serde_json::from_str(json_folders_str.as_str()) {
Ok(folders) => folders,
Err(e) => {
eprintln!("Error: {}", e);
return;
}
};
let json_ciphers: Vec<JsonCipher> = match serde_json::from_str(json_ciphers_str.as_str()) {
Ok(ciphers) => ciphers,
Err(e) => {
eprintln!("Error: {}", e);
return;
}
};

// Convert the JSON types into the types used by the exporter.
let folders: Vec<Folder> = json_folders
.iter()
.map(|folder| Folder::from(folder.clone()))
.collect();
let ciphers: Vec<Cipher> = json_ciphers
.iter()
.map(|cipher| Cipher::from(cipher.clone()))
.collect();

// Create the export format.
let kdf = Kdf::PBKDF2 {
iterations: num::NonZeroU32::new(600000).unwrap(),
};
let format = bitwarden_exporters::Format::EncryptedJson {
password: password.to_string(),
kdf,
};

// Export the data.
let json_str = match export(folders, ciphers, format) {
Ok(json_str) => json_str,
Err(e) => {
eprintln!("Error: {}", e);
return;
}
};
// Generate the encrypted JSON.
let json_str =
match utils::generate_encrypted_json(&json_folders_str, &json_ciphers_str, &password) {
Ok(json_str) => json_str,
Err(e) => {
eprintln!("Error: {}", e);
return;
}
};

println!("{}", json_str);
}
118 changes: 118 additions & 0 deletions src/utils.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
use crate::types::{JsonCipher, JsonFolder};

use bitwarden_crypto::Kdf;
use bitwarden_exporters::{export, Cipher, Folder};

use std::num;
use thiserror::Error;

#[derive(Error, Debug)]
pub enum UtilsError {
#[error(transparent)]
Serde(#[from] serde_json::Error),

#[error("Export error: {0}")]
Export(#[from] bitwarden_exporters::ExportError),
}

pub fn generate_encrypted_json(
json_folders_str: &str,
json_ciphers_str: &str,
password: &str,
) -> Result<String, UtilsError> {
// Parse the JSON strings into the respective types.
let json_folders: Vec<JsonFolder> = match serde_json::from_str(json_folders_str) {
Ok(folders) => folders,
Err(e) => {
eprintln!("Error: {}", e);
return Err(UtilsError::Serde(e));
}
};
let json_ciphers: Vec<JsonCipher> = match serde_json::from_str(json_ciphers_str) {
Ok(ciphers) => ciphers,
Err(e) => {
eprintln!("Error: {}", e);
return Err(UtilsError::Serde(e));
}
};

// Convert the JSON types into the types used by the exporter.
let folders: Vec<Folder> = json_folders
.iter()
.map(|folder| Folder::from(folder.clone()))
.collect();
let ciphers: Vec<Cipher> = json_ciphers
.iter()
.map(|cipher| Cipher::from(cipher.clone()))
.collect();

// Create the export format.
let kdf = Kdf::PBKDF2 {
iterations: num::NonZeroU32::new(600000).unwrap(),
};
let format = bitwarden_exporters::Format::EncryptedJson {
password: password.to_string(),
kdf,
};

// Export the data.
let json_str = match export(folders, ciphers, format) {
Ok(json_str) => json_str,
Err(e) => {
eprintln!("Error: {}", e);
return Err(UtilsError::Export(e));
}
};

return Ok(json_str);
}

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

#[test]
fn test_generate_encrypted_json() {
let json_folders_str =
r#"[{"id":"00000000-0000-0000-0000-000000000001","name":"My Folder"}]"#;
let json_ciphers_str = r#"[{"folderId":"00000000-0000-0000-0000-000000000001","name":"My Test","notes":"My Notes","username":"my_username","password":"my_password","loginUris":["https://example.com"]}]"#;
let password = "password";

let json_str =
generate_encrypted_json(json_folders_str, json_ciphers_str, password).unwrap();
let json = serde_json::from_str::<serde_json::Value>(&json_str).unwrap();
assert_eq!(json["encrypted"], true);
assert_eq!(json["passwordProtected"], true);
assert_ne!(json["salt"], "");
assert_eq!(json["kdfType"], 0);
assert_eq!(json["kdfIterations"], 600000);
assert_eq!(json["kdfMemory"], serde_json::Value::Null);
assert_eq!(json["kdfParallelism"], serde_json::Value::Null);
assert_ne!(json["encKeyValidation_DO_NOT_EDIT"], "");
assert_eq!(json["encKeyValidation_DO_NOT_EDIT"].as_str().unwrap().starts_with("2."), true);
assert_ne!(json["data"], "");
assert_eq!(json["data"].as_str().unwrap().starts_with("2."), true);
}

#[test]
fn test_generate_encrypted_json_invalid_folders_json() {
let json_folders_str =
r#"[{"id":"00000000-0000-0000-0000-000000000001","name":"My Folder"}"#;
let json_ciphers_str = r#"[{"folderId":"00000000-0000-0000-0000-000000000001","name":"My Test","notes":"My Notes","username":"my_username","password":"my_password","loginUris":["https://example.com"]}]"#;
let password = "password";

let result = generate_encrypted_json(json_folders_str, json_ciphers_str, password);
assert!(result.is_err());
}

#[test]
fn test_generate_encrypted_json_invalid_ciphers_json() {
let json_folders_str =
r#"[{"id":"00000000-0000-0000-0000-000000000001","name":"My Folder"}]"#;
let json_ciphers_str = r#"[{"folderId":"00000000-0000-0000-0000-000000000001","name":"My Test","notes":"My Notes","username":"my_username","password":"my_password","loginUris":["https://example.com"]}"#;
let password = "password";

let result = generate_encrypted_json(json_folders_str, json_ciphers_str, password);
assert!(result.is_err());
}
}

0 comments on commit 1d3da61

Please sign in to comment.