...
 
Commits (2)
......@@ -3,6 +3,12 @@ name = "kdbx-rs"
version = "0.1.0"
authors = ["Tony Finn <[email protected]>"]
edition = "2018"
description = "Keepass 2 (KDBX) password database parsing and creation"
readme = "README.md"
repository = "https://gitlab.com/tonyfinn/kdbx-rs"
license = "GPL-3.0+"
keywords = ["kdbx", "keepass", "password", "parser"]
categories = ["encoding", "parser-implementations"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
......
......@@ -2,6 +2,47 @@
Library for reading and writing KDBX libraries from Rust
## Example code
Obtaining the first password in the password database:
```rust
use kdbx_rs::{self, CompositeKey, Error};
fn main() -> Result<(), Error> {
let file_path = "./res/kdbx4-argon2.kdbx";
let kdbx = kdbx_rs::open(file_path)?;
let key = CompositeKey::from_password("kdbxrs");
let unlocked = kdbx.unlock(&key)?;
println!(unlocked.root().entries[0].password())
Ok(())
}
```
Generating a new password database:
```rust
let mut database = Database::default();
database.set_name("My First Database");
database.set_description("Created with kdbx-rs");
let mut entry = Entry::default();
entry.set_password("password1");
entry.set_url("https://example.com");
entry.set_username("User123");
database.add_entry(Entry);
```
Saving a database to a file
```rust
let kdbx = Kdbx::from_database(database)?;
kdbx.set_key(CompositeKey::from_password("foo123"))?;
let mut file = File::create("/tmp/kdbx-rs-example.kdbx")?;
kdbx.write(&mut file)?;
```
## Comparison of Rust Keepass Libraries (as of May 2020)
......
......@@ -12,7 +12,7 @@ fn main() -> Result<(), kdbx_rs::Error> {
}
let kdbx = kdbx_rs::open(&args[1])?;
let key = kdbx_rs::CompositeKey::from_password(args.get(2).unwrap_or(&"kdbxrs".to_string()));
let kdbx = kdbx.unlock(&key).map_err(|(e, _db)| e)?;
let kdbx = kdbx.unlock(&key)?;
let data: Vec<u8> = kdbx.raw_xml().unwrap().iter().cloned().collect();
println!("{}", String::from_utf8(data).unwrap());
Ok(())
......
......@@ -2,7 +2,7 @@
//!
//! Primarily for verifying kdbx-rs changes
use kdbx_rs::types::{Entry, Field, Group, Times};
use kdbx_rs::database::{Entry, Field, Group, Times};
use kdbx_rs::{CompositeKey, Database, Error, Kdbx};
use chrono::NaiveDate;
......
......@@ -12,7 +12,7 @@ fn main() -> Result<(), kdbx_rs::Error> {
}
let kdbx = kdbx_rs::open(&args[1])?;
let key = kdbx_rs::CompositeKey::from_password(args.get(2).unwrap_or(&"kdbxrs".to_string()));
let kdbx = kdbx.unlock(&key).map_err(|(e, _db)| e)?;
let kdbx = kdbx.unlock(&key)?;
let xml = kdbx_rs::xml::parse_xml(kdbx.raw_xml().unwrap());
println!("{:#?}", xml);
Ok(())
......
......@@ -2,14 +2,14 @@
pub(crate) mod errors;
mod header;
mod header_fields;
mod read;
mod states;
mod kdbx;
mod variant_dict;
mod wrapper_fields;
pub use header::{InnerHeaderId, KdbxHeader, KdbxInnerHeader, OuterHeaderId};
pub use read::{from_reader, open};
pub use states::{Kdbx, Locked, Unlocked};
pub use kdbx::{Kdbx, Locked, Unlocked, FailedUnlock};
pub use variant_dict::{Value as VariantDictValue, VariantDict, VariantParseError};
pub use wrapper_fields::{Cipher, CompressionType, KdfAlgorithm, KdfParams};
pub(crate) use wrapper_fields::{KDBX_MAGIC_NUMBER, KEEPASS_MAGIC_NUMBER};
pub use header_fields::{Cipher, CompressionType, KdfAlgorithm, KdfParams};
pub(crate) use header_fields::{KDBX_MAGIC_NUMBER, KEEPASS_MAGIC_NUMBER};
use super::header;
use super::wrapper_fields;
use super::header_fields;
use crate::crypto;
use thiserror::Error;
......@@ -60,7 +60,7 @@ pub enum HeaderError {
MissingRequiredInnerField(header::InnerHeaderId),
/// A parameter for the KDF algorithm is missing
#[error("Incompatible database - Missing paramater {0:?} for KDF {1:?}")]
MissingKdfParam(String, wrapper_fields::KdfAlgorithm),
MissingKdfParam(String, header_fields::KdfAlgorithm),
/// Validating the header against the unencrypted sha256 hash failed
#[error("Corrupt database - Header Checksum failed")]
ChecksumFailed,
......
use super::errors::{self, HeaderError as Error};
use super::variant_dict;
use super::wrapper_fields;
use super::header_fields;
use crate::crypto;
use crate::utils;
use getrandom::getrandom;
......@@ -200,9 +200,9 @@ where
#[derive(Default)]
pub struct KdbxHeaderBuilder {
pub cipher: Option<wrapper_fields::Cipher>,
pub kdf_params: Option<wrapper_fields::KdfParams>,
pub compression_type: Option<wrapper_fields::CompressionType>,
pub cipher: Option<header_fields::Cipher>,
pub kdf_params: Option<header_fields::KdfParams>,
pub compression_type: Option<header_fields::CompressionType>,
pub other_headers: Vec<HeaderField<OuterHeaderId>>,
pub master_seed: Option<Vec<u8>>,
pub encryption_iv: Option<Vec<u8>>,
......@@ -240,7 +240,7 @@ impl KdbxHeaderBuilder {
));
}
self.compression_type =
Some(wrapper_fields::CompressionType::from(u32::from_le_bytes([
Some(header_fields::CompressionType::from(u32::from_le_bytes([
header.data[0],
header.data[1],
header.data[2],
......@@ -285,11 +285,11 @@ impl KdbxHeaderBuilder {
/// from the OS secure RNG
pub struct KdbxHeader {
/// Encryption cipher used for decryption the database
pub cipher: wrapper_fields::Cipher,
pub cipher: header_fields::Cipher,
/// Options for converting credentials to crypto keys
pub kdf_params: wrapper_fields::KdfParams,
pub kdf_params: header_fields::KdfParams,
/// Compression applied prior to encryption
pub compression_type: wrapper_fields::CompressionType,
pub compression_type: header_fields::CompressionType,
/// Custom and unrecognized header types
pub other_headers: Vec<HeaderField<OuterHeaderId>>,
/// Master seed used to make crypto keys DB specific
......@@ -318,8 +318,8 @@ impl KdbxHeader {
getrandom(&mut encryption_iv)?;
getrandom(&mut cipher_salt)?;
Ok(KdbxHeader {
cipher: wrapper_fields::Cipher::Aes256,
kdf_params: wrapper_fields::KdfParams::Argon2 {
cipher: header_fields::Cipher::Aes256,
kdf_params: header_fields::KdfParams::Argon2 {
iterations: 10,
memory_bytes: 0xFFFF * 1024,
salt: cipher_salt,
......@@ -387,7 +387,7 @@ impl KdbxHeader {
#[derive(Default)]
pub struct KdbxInnerHeaderBuilder {
pub inner_stream_cipher: Option<wrapper_fields::InnerStreamCipher>,
pub inner_stream_cipher: Option<header_fields::InnerStreamCipher>,
pub inner_stream_key: Option<Vec<u8>>,
/// Custom and unrecognized header types
pub other_headers: Vec<HeaderField<InnerHeaderId>>,
......@@ -425,7 +425,7 @@ impl KdbxInnerHeaderBuilder {
#[derive(Debug, PartialEq, Eq)]
pub struct KdbxInnerHeader {
/// Cipher identifier for data encrypted in memory
pub inner_stream_cipher: wrapper_fields::InnerStreamCipher,
pub inner_stream_cipher: header_fields::InnerStreamCipher,
/// Cipher key for data encrypted in memory
pub inner_stream_key: Vec<u8>,
/// Headers not handled by this library
......@@ -441,7 +441,7 @@ impl KdbxInnerHeader {
///
/// [`getrandom`]: https://docs.rs/getrandom/0.1/getrandom/index.html
pub fn from_os_random() -> std::result::Result<KdbxInnerHeader, errors::DatabaseCreationError> {
let inner_stream_cipher = wrapper_fields::InnerStreamCipher::ChaCha20;
let inner_stream_cipher = header_fields::InnerStreamCipher::ChaCha20;
let mut inner_stream_key = vec![0u8; 44]; // 32 bit key + 12 bit nonce for chacha20
getrandom::getrandom(&mut inner_stream_key)?;
......
......@@ -33,6 +33,16 @@ impl<T: KdbxState> Kdbx<T> {
}
}
/// Represents a failed attempt at unlocking a database
/// Includes the locked database and the reason the unlockfailed.
pub struct FailedUnlock(pub Kdbx<Locked>, pub errors::UnlockError);
impl From<FailedUnlock> for errors::UnlockError {
fn from(funlock: FailedUnlock) -> errors::UnlockError {
funlock.1
}
}
#[derive(Debug)]
/// An unlocked kdbx file, allowing access to stored credentials
pub struct Unlocked {
......@@ -234,11 +244,11 @@ impl Kdbx<Locked> {
pub fn unlock(
self,
key: &crypto::CompositeKey,
) -> Result<Kdbx<Unlocked>, (errors::UnlockError, Kdbx<Locked>)> {
) -> Result<Kdbx<Unlocked>, FailedUnlock> {
let composed_key = key.composed();
let master_key = match composed_key.master_key(&self.state.header.kdf_params) {
Ok(master_key) => master_key,
Err(e) => return Err((errors::UnlockError::from(e), self)),
Err(e) => return Err(FailedUnlock(self, errors::UnlockError::from(e))),
};
let hmac_key = master_key.hmac_key(&self.state.header.master_seed);
......@@ -265,10 +275,10 @@ impl Kdbx<Locked> {
xml_data: Some(data),
},
}),
Err(e) => Err((e, self)),
Err(e) => Err(FailedUnlock(self, e)),
}
} else {
Err((errors::UnlockError::HmacInvalid, self))
Err(FailedUnlock(self, errors::UnlockError::HmacInvalid))
}
}
}
......@@ -3,6 +3,7 @@
pub use crate::binary::errors::{
DatabaseCreationError, HeaderError, OpenError, UnlockError, WriteError,
};
pub use crate::binary::FailedUnlock;
pub use crate::crypto::KeyGenerationError;
pub use crate::xml::parse::Error as XmlReadError;
pub use crate::xml::serialize::Error as XmlWriteError;
......@@ -33,3 +34,9 @@ pub enum Error {
#[error("Failed to create encryption keys")]
KeyGeneration(#[from] KeyGenerationError),
}
impl From<FailedUnlock> for Error {
fn from(funlock: FailedUnlock) -> Error {
Error::Unlock(funlock.1)
}
}
......@@ -2,6 +2,13 @@
//! Module to read and write KDBX (Keepass 2) database files.
//!
//! The main types in this crate are:
//!
//! * [`Database`] which represents a password database
//! * [`Kdbx`] which represents a database file, including encryption options
//!
//! # Opening a database
//!
//! Databases can be read with the [`kdbx_rs::open`] function. This provides
//! access to heder information. It can then be unlocked by providing a [`CompositeKey`]
//! to the [`Kdbx.unlock`] method to access any encrypted data.
......@@ -13,32 +20,88 @@
//! # let file_path = "./res/kdbx4-argon2.kdbx";
//! let kdbx = kdbx_rs::open(file_path)?;
//! let key = CompositeKey::from_password("kdbxrs");
//! // If unlock fails, the locked database is returned to you
//! // so you can e.g. prompt the user to try another password
//! //
//! // Here we just return if incorrect
//! let unlocked = kdbx.unlock(&key).map_err(|(error, locked)| error)?;
//! let unlocked = kdbx.unlock(&key)?;
//! # Ok(())
//! # }
//! ```
//!
//! # Generating a new password database
//!
//! A database can be created in memory by using the [`Database::default()`]
//! method. This will create an empty database which you can then populate.
//!
//! ```
//! use kdbx_rs::database::{Database, Entry};
//!
//! let mut database = Database::default();
//! database.set_name("My First Database");
//! database.set_description("Created with kdbx-rs");
//!
//! let mut entry = Entry::default();
//! entry.set_password("password1");
//! entry.set_url("https://example.com");
//! entry.set_username("User123");
//!
//! database.add_entry(entry);
//! ```
//!
//! # Saving a database to a file
//!
//! To save a database to a file, you first need to create
//! a [`Kdbx`] instance from that database, for example with
//! [`Kdbx::from_database`]. This will generate encryption options using
//! salts and random values from the OS's secure RNG. These can be customised,
//! or you can save the database as is.
//!
//! Before saving a new database for the first time, you'll need to set the user
//! credentials to save your database. This can be done with [`Kdbx.set_key`].
//! Provide a [`CompositeKey`] instance, which can be created the same way as for
//! unlocking database. This will then be used to generate the remaining keys
//! allowing you to save the database using [`Kdbx.write()`]
//!
//! ```rust
//! use kdbx_rs::{CompositeKey, Kdbx};
//! # use kdbx_rs::Database;
//! # use std::fs::File;
//!
//! # fn main() -> Result<(), kdbx_rs::Error> {
//! # let mut database = Database::default();
//! # let file_path = "/tmp/kdbx-rs-example.kdbx";
//! let mut kdbx = Kdbx::from_database(database)?;
//! kdbx.set_key(CompositeKey::from_password("foo123"))?;
//!
//! let mut file = File::create(file_path).unwrap();
//! kdbx.write(&mut file)?;
//! # Ok(())
//! # }
//! ```
//! Alternatively, [`kdbx_rs::from_reader`] can be used to open a database
//! from a non file source (such as in-memory or a network stream)
//!
//! [`CompositeKey`]: ./struct.CompositeKey.html
//! [`Database`]: ./struct.Database.html
//! [`Database::default()`]: ./struct.Database.html#method.default
//! [`kdbx_rs::from_reader`]: ./fn.from_reader.html
//! [`kdbx_rs::open`]: ./fn.open.html
//! [`Kdbx`]: ./struct.Kdbx.html
//! [`Kdbx.from_database`]: ./struct.Kdbx.html#method.from_database
//! [`Kdbx.set_key`]: ./struct.Kdbx.html#method.set_key
//! [`Kdbx.unlock`]: ./struct.Kdbx.html#method.unlock
//! [`Kdbx.write`]: ./struct.Kdbx.html#method.write
pub mod binary;
mod crypto;
pub mod errors;
mod stream;
pub mod types;
mod types;
mod utils;
pub mod xml;
pub use crate::types::Database;
/// Password database datatypes
pub mod database {
pub use crate::types::*;
}
pub use binary::{from_reader, open, Kdbx};
pub use crypto::CompositeKey;
pub use errors::Error;
......@@ -3,7 +3,7 @@
use chrono::{NaiveDateTime,Timelike};
use uuid::Uuid;
/// A value for a entry's field
/// A value for a `Field` stored in an `Entry`
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Value {
/// A value using in-memory encryption
......@@ -59,34 +59,80 @@ impl Entry {
}
/// Find a field in this entry with a given key
pub fn find_entry(&self, key: &str) -> Option<&str> {
pub fn find(&self, key: &str) -> Option<&Field> {
self.fields.iter()
.find(|i| i.key.as_str() == key)
.and_then(|f| match &f.value {
Value::Empty => None,
Value::Standard(s) => Some(s.as_ref()),
Value::Protected(p) => Some(p.as_ref()),
})
}
/// Find a field in this entry with a given key
pub fn find_mut(&mut self, key: &str) -> Option<&mut Field> {
self.fields.iter_mut()
.find(|i| i.key.as_str() == key)
}
fn find_string_value(&self, key: &str) -> Option<&str> {
self.find(key).and_then(|f| match &f.value {
Value::Empty => None,
Value::Standard(s) => Some(s.as_ref()),
Value::Protected(p) => Some(p.as_ref()),
})
}
/// Return the title of this item
pub fn title(&self) -> Option<&str> {
self.find_entry("Title")
self.find_string_value("Title")
}
/// Set the title of this entry
pub fn set_title<S: ToString>(&mut self, title: S) {
let title = title.to_string();
match self.find_mut("Title") {
Some(f) => f.value = Value::Standard(title),
None => self.fields.push(Field::new("Title", &title))
}
}
/// Return the username of this item
pub fn username(&self) -> Option<&str> {
self.find_entry("UserName")
self.find_string_value("UserName")
}
/// Set the username of this entry
pub fn set_username<S: ToString>(&mut self, username: S) {
let username = username.to_string();
match self.find_mut("UserName") {
Some(f) => f.value = Value::Standard(username),
None => self.fields.push(Field::new("UserName", &username))
}
}
/// Return the URL of this item
pub fn url(&self) -> Option<&str> {
self.find_entry("URL")
self.find_string_value("URL")
}
/// Set the URL of this entry
pub fn set_url<S: ToString>(&mut self, url: S) {
let url = url.to_string();
match self.find_mut("URL") {
Some(f) => f.value = Value::Standard(url),
None => self.fields.push(Field::new("URL", &url))
}
}
/// Return the password of this item
pub fn password(&self) -> Option<&str> {
self.find_entry("Password")
self.find_string_value("Password")
}
/// Set the password of this entry
pub fn set_password<S: ToString>(&mut self, password: S) {
let password = password.to_string();
match self.find_mut("Password") {
Some(f) => f.value = Value::Standard(password),
None => self.fields.push(Field::new("Password", &password))
}
}
}
......@@ -219,6 +265,36 @@ impl Database {
&mut self.meta
}
/// Get the database name
pub fn name(&self) -> &str {
&self.meta.database_name
}
/// Set the database name
pub fn set_name<S: ToString>(&mut self, name: S) {
self.meta.database_name = name.to_string();
}
/// Get the database description
pub fn description(&self) -> &str {
&self.meta.database_description
}
/// Set the database name
pub fn set_description<S: ToString>(&mut self, desc: S) {
self.meta.database_description = desc.to_string();
}
/// Add a entry to the root group
///
/// Creates a root group if none exist
pub fn add_entry(&mut self, entry: Entry) {
if self.groups.len() == 0 {
self.groups.push(Group::default());
}
self.groups[0].entries.push(entry);
}
/// Top level group for database entries
pub fn root(&self) -> Option<&Group> {
self.groups.get(0)
......
use kdbx_rs;
use kdbx_rs::types::{Entry, Field, Group, Times};
use kdbx_rs::database::{Entry, Group, Times};
use chrono::NaiveDate;
use std::fs::read_to_string;
......@@ -29,15 +29,15 @@ fn generate_xml() -> Result<(), kdbx_rs::Error> {
let expected_xml_string = read_to_string(expected_path).unwrap().replace("\r\n", "\n");
let mut db = kdbx_rs::Database::default();
db.meta.database_name = "BarName".to_string();
db.meta.database_description = "BazDesc".to_string();
db.set_name("BarName");
db.set_description("BazDesc");
let mut group = Group::default();
group.name = "FooGroup".to_string();
group.uuid = Uuid::from_u128(0x12345678);
group.times = sample_times();
let mut entry = Entry::default();
entry.add_field(Field::new("Title", "Bar"));
entry.add_field(Field::new("Password", "kdbxrs"));
entry.set_title("Bar");
entry.set_password("kdbxrs");
entry.uuid = Uuid::from_u128(0x654321);
entry.times = sample_times();
group.entries.push(entry);
......
......@@ -13,7 +13,7 @@ fn kdbx4_parsing() -> Result<(), kdbx_rs::Error> {
let db = kdbx_rs::from_reader(file).unwrap();
let key = kdbx_rs::CompositeKey::from_password("kdbxrs");
let db = db.unlock(&key).map_err(|e| e.0)?;
let db = db.unlock(&key)?;
let xml = kdbx_rs::xml::parse_xml(db.raw_xml().unwrap())?;
assert_eq!(1, xml.groups.len());
......@@ -47,7 +47,7 @@ fn kdbx4_parsing_twofish() -> Result<(), kdbx_rs::Error> {
let db = kdbx_rs::from_reader(file).unwrap();
let key = kdbx_rs::CompositeKey::from_password("kdbxrs");
let db = db.unlock(&key).map_err(|e| e.0)?;
let db = db.unlock(&key)?;
let xml = kdbx_rs::xml::parse_xml(db.raw_xml().unwrap())?;
assert_eq!(1, xml.groups.len());
......
......@@ -14,5 +14,5 @@ fn kdbx4_argon2() -> Result<(), kdbx_rs::Error> {
let db = kdbx_rs::from_reader(file).unwrap();
let key = kdbx_rs::CompositeKey::from_password("kdbxrs");
db.unlock(&key).map(|_| ()).map_err(|e| e.0.into())
Ok(db.unlock(&key).map(|_| ())?)
}
use kdbx_rs::{self, Kdbx, CompositeKey};
use kdbx_rs::types::{Entry, Field, Group};
use kdbx_rs::database::{Entry, Field, Group};
const DATABASE_NAME: &str = "BarName";
const DATABASE_DESC: &str = "BazDesc";
......@@ -35,7 +35,7 @@ fn round_trip() -> Result<(), kdbx_rs::Error> {
kdbx.write(&mut output_buf)?;
let reparsed = kdbx_rs::from_reader(&*output_buf)?;
let unlocked = reparsed.unlock(&key()).map_err(|(e, _db)| e)?;
let unlocked = reparsed.unlock(&key())?;
assert_eq!(unlocked.meta().database_name, DATABASE_NAME);
assert_eq!(unlocked.meta().database_description, DATABASE_DESC);
let root = unlocked.root().unwrap();
......