Commit cb3c5940 authored by Tony Finn's avatar Tony Finn

KeepassXC OTP reading, doc improvements

parent 6c29c1f5
......@@ -3,11 +3,9 @@
//! Primarily for investigating the kdbx format. It takes the password
//! on the CLI, which is insecure
use kdbx_rs;
fn main() -> Result<(), kdbx_rs::Error> {
let args: Vec<String> = std::env::args().collect();
if args.len() < 1 {
if args.is_empty() {
println!("Usage: kdbx-decrypt <path to kdbx file> <password>");
}
let kdbx = kdbx_rs::open(&args[1])?;
......
......@@ -2,8 +2,6 @@
//!
//! Primarily for investigating the kdbx format.
use kdbx_rs;
fn print_kdf(params: &kdbx_rs::binary::KdfParams) {
use kdbx_rs::binary::KdfParams as p;
match params {
......@@ -39,7 +37,7 @@ fn print_kdf(params: &kdbx_rs::binary::KdfParams) {
fn main() -> Result<(), kdbx_rs::Error> {
let args: Vec<String> = std::env::args().collect();
if args.len() < 1 {
if args.is_empty() {
println!("Usage: kdbx-dump-header <path to kdbx file>");
}
let kdbx = kdbx_rs::open(&args[1])?;
......
......@@ -12,11 +12,11 @@ use uuid::Uuid;
fn sample_times() -> Times {
Times {
last_access_time: NaiveDate::from_ymd(2020, 05, 01).and_hms(1, 2, 3),
last_modification_time: NaiveDate::from_ymd(2020, 04, 01).and_hms(1, 2, 3),
creation_time: NaiveDate::from_ymd(2020, 04, 01).and_hms(1, 1, 3),
location_changed: NaiveDate::from_ymd(2020, 04, 01).and_hms(1, 1, 3),
expiry_time: NaiveDate::from_ymd(2020, 04, 01).and_hms(1, 1, 3),
last_access_time: NaiveDate::from_ymd(2020, 5, 1).and_hms(1, 2, 3),
last_modification_time: NaiveDate::from_ymd(2020, 4, 1).and_hms(1, 2, 3),
creation_time: NaiveDate::from_ymd(2020, 4, 1).and_hms(1, 1, 3),
location_changed: NaiveDate::from_ymd(2020, 4, 1).and_hms(1, 1, 3),
expiry_time: NaiveDate::from_ymd(2020, 4, 1).and_hms(1, 1, 3),
expires: false,
usage_count: 1,
}
......@@ -34,12 +34,12 @@ fn main() -> Result<(), Error> {
db.meta.database_description = "BazDesc".to_string();
let mut group = Group::default();
group.name = "Root".to_string();
group.uuid = Uuid::from_u128(0x12345678);
group.uuid = Uuid::from_u128(0x1234_5678);
group.times = sample_times();
let mut entry = Entry::default();
entry.add_field(Field::new("Title", "Bar"));
entry.add_field(Field::new("Password", "kdbxrs"));
entry.uuid = Uuid::from_u128(0x654321);
entry.uuid = Uuid::from_u128(0x0065_4321);
entry.times = sample_times();
group.entries.push(entry);
db.groups.push(group);
......
......@@ -3,11 +3,9 @@
//!
//! Primarily for verifying kdbx-rs changes
use kdbx_rs;
fn main() -> Result<(), kdbx_rs::Error> {
let args: Vec<String> = std::env::args().collect();
if args.len() < 1 {
if args.is_empty() {
println!("Usage: kdbx-decrypt <path to kdbx file> <password>");
}
let kdbx = kdbx_rs::open(&args[1])?;
......
......@@ -93,7 +93,7 @@ impl Into<u8> for OuterHeaderId {
impl HeaderId for OuterHeaderId {
fn is_final(&self) -> bool {
return *self == OuterHeaderId::EndOfHeader;
*self == OuterHeaderId::EndOfHeader
}
}
......@@ -138,7 +138,7 @@ impl Into<u8> for InnerHeaderId {
impl HeaderId for InnerHeaderId {
fn is_final(&self) -> bool {
return *self == InnerHeaderId::EndOfHeader;
*self == InnerHeaderId::EndOfHeader
}
}
......
......@@ -5,8 +5,8 @@ use crate::utils;
use std::convert::{TryFrom, TryInto};
use uuid::Uuid;
pub const KEEPASS_MAGIC_NUMBER: u32 = 0x9AA2D903;
pub const KDBX_MAGIC_NUMBER: u32 = 0xB54BFB67;
pub const KEEPASS_MAGIC_NUMBER: u32 = 0x9AA2_D903;
pub const KDBX_MAGIC_NUMBER: u32 = 0xB54B_FB67;
const AES128_UUID: &str = "61ab05a1-9464-41c3-8d74-3a563df8dd35";
const AES256_UUID: &str = "31c1f2e6-bf71-4350-be58-05216afc5aff";
......
......@@ -5,14 +5,22 @@ use std::ops::{Deref, DerefMut};
pub trait KdbxState: std::fmt::Debug {
fn header(&self) -> &header::KdbxHeader;
fn header_mut(&mut self) -> &mut header::KdbxHeader;
fn write<W: Write>(&self, output: W) -> Result<(), errors::WriteError>;
}
#[derive(Debug)]
/// A kdbx file
/// A KeePass 2 archive wrapping a password database
///
/// Most methods are available on a specific state
/// like Kdbx<Locked> or Kdbx<Unlocked>
/// Most methods are available on a specific state like `Kdbx<Locked>`
/// or `Kdbx<Unlocked>`.
///
/// A keepass 2 archive can be obtained in one of two ways. You may read
/// an existing archive using [`kdbx_rs::open`][crate::open] or
/// [`kdbx_rs::from_reader`][crate::from_reader].
///
/// You can also create a password database using [`Database`][crate::Database],
/// then turn it into a KeePass 2 archive using [`Kdbx::from_database`].
pub struct Kdbx<S>
where
S: KdbxState,
......@@ -21,12 +29,17 @@ where
}
impl<T: KdbxState> Kdbx<T> {
/// Unencrypted database configuration and custom data
/// Encryption configuration and unencrypted custom data
pub fn header(&self) -> &header::KdbxHeader {
&self.state.header()
self.state.header()
}
/// Mutable encryption configuration and unencrypted custom data
pub fn header_mut(&mut self) -> &mut header::KdbxHeader {
self.state.header_mut()
}
/// Write this database to the given output stream
/// Write this archive to the given output stream
pub fn write<W: Write>(&self, output: W) -> Result<(), errors::WriteError> {
self.state.write(output)?;
Ok(())
......@@ -34,7 +47,14 @@ impl<T: KdbxState> Kdbx<T> {
}
/// Represents a failed attempt at unlocking a database
/// Includes the locked database and the reason the unlockfailed.
///
/// Includes the locked database and the reason the unlock failed.
/// This allows you to keep the database for interactive user and
/// e.g. promt the user for a new password if the error is key related
///
/// However, for unscripted use, `FailedUnlock` implements
/// `Into<[kdbx_rs::Error]>` and `Into<[kdbx_rs::errors::UnlockError]>`
/// for easy use with the `?` operatior.
pub struct FailedUnlock(pub Kdbx<Locked>, pub errors::UnlockError);
impl From<FailedUnlock> for errors::UnlockError {
......@@ -89,6 +109,10 @@ impl KdbxState for Unlocked {
&self.header
}
fn header_mut(&mut self) -> &mut header::KdbxHeader {
&mut self.header
}
fn write<W: Write>(&self, mut output: W) -> Result<(), errors::WriteError> {
let master_key = self
.master_key
......@@ -120,6 +144,11 @@ impl Kdbx<Unlocked> {
&self.state.inner_header
}
/// Mutable encrypted binaries and database options
pub fn inner_header_mut(&mut self) -> &mut header::KdbxInnerHeader {
&mut self.state.inner_header
}
/// Use the given composite key to encrypt the database
pub fn set_key(
&mut self,
......@@ -144,6 +173,11 @@ impl Kdbx<Unlocked> {
&self.state.database
}
/// Mutable password database stored in this kdbx archive
pub fn database_mut(&mut self) -> &mut crate::Database {
&mut self.state.database
}
/// Generate a new .kdbx from the given database
///
/// Uses OS randomness provided by the `rand` crates's [`OsRng`] to
......@@ -206,6 +240,10 @@ impl KdbxState for Locked {
&self.header
}
fn header_mut(&mut self) -> &mut header::KdbxHeader {
&mut self.header
}
fn write<W: Write>(&self, mut output: W) -> Result<(), errors::WriteError> {
let mut header_buf = Vec::new();
let header_writer = &mut header_buf as &mut dyn Write;
......@@ -270,8 +308,8 @@ impl Kdbx<Locked> {
match parsed {
Ok((inner_header, data, db)) => Ok(Kdbx {
state: Unlocked {
inner_header,
header: self.state.header,
inner_header: inner_header,
major_version: self.state.major_version,
minor_version: self.state.minor_version,
composed_key: Some(composed_key),
......
......@@ -50,7 +50,7 @@ pub fn from_reader<R: Read>(mut input: R) -> Result<Kdbx<Locked>, errors::OpenEr
encrypted_data,
};
Ok(Kdbx { state: state })
Ok(Kdbx { state })
}
/// Read a database from a given path
......
......@@ -10,7 +10,7 @@
//! # 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`]
//! access to header information. It can then be unlocked by providing a [`CompositeKey`]
//! to the [`Kdbx.unlock`] method to access any encrypted data.
//!
//! ```
......
use chacha20::ChaCha20;
use rand::{RngCore, rngs::OsRng};
use rand::{rngs::OsRng, RngCore};
use salsa20::Salsa20;
use sha2::{Digest, Sha256, Sha512};
use stream_cipher::{NewStreamCipher, StreamCipher};
......@@ -7,7 +7,7 @@ use thiserror::Error;
use crate::binary::InnerStreamCipherAlgorithm;
pub const SALSA20_IV: [u8; 8] = [0xE8, 0x30, 0x09, 0x4b, 0x97, 0x20, 0x5d, 0x2a];
pub const SALSA20_IV: [u8; 8] = [0xe8, 0x30, 0x09, 0x4b, 0x97, 0x20, 0x5d, 0x2a];
#[derive(Debug, Error)]
/// Errors creating inner stream used to decrypt protected values
......@@ -19,7 +19,7 @@ pub enum InnerStreamError {
impl InnerStreamCipherAlgorithm {
pub(crate) fn stream_cipher(
&self,
self,
key: &[u8],
) -> Result<Box<dyn StreamCipher>, InnerStreamError> {
match self {
......@@ -28,12 +28,12 @@ impl InnerStreamCipherAlgorithm {
Ok(Box::new(
ChaCha20::new_var(&iv[0..32], &iv[32..44]).unwrap(),
))
},
}
InnerStreamCipherAlgorithm::Salsa20 => {
let iv = Sha256::digest(key);
Ok(Box::new(Salsa20::new_var(&iv[0..32], &SALSA20_IV).unwrap()))
}
_ => Err(InnerStreamError::UnsupportedCipher(*self)),
_ => Err(InnerStreamError::UnsupportedCipher(self)),
}
}
}
......
......@@ -16,10 +16,7 @@ where
R: Read,
{
pub(crate) fn new(inner: R, cipher: C) -> StreamCipherReader<C, R> {
StreamCipherReader {
inner: inner,
cipher,
}
StreamCipherReader { inner, cipher }
}
}
......
//! Keepass data types
use chrono::{NaiveDateTime, Timelike};
use std::borrow::Cow;
use uuid::Uuid;
/// A value for a `Field` stored in an `Entry`
......@@ -118,6 +119,22 @@ impl Entry {
}
}
/// Return the TOTP of this item, as stored by KeepassXC
pub fn otp(&self) -> Option<Otp> {
self.find_string_value("otp")
.map(|url| Otp {
url: Cow::Borrowed(url)
})
}
/// Return the TOTP of this item, as stored by KeepassXC
pub fn set_otp(&mut self, otp: Otp) {
match self.find_mut("otp") {
Some(f) => f.value = Value::Standard(otp.url.to_string()),
None => self.fields.push(Field::new("otp", otp.url.as_ref())),
}
}
/// Return the password of this item
pub fn password(&self) -> Option<&str> {
self.find_string_value("Password")
......@@ -246,7 +263,22 @@ impl Default for Times {
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
/// Decrypted database structure
/// Decrypted password database
///
/// A database is made up of two primary parts, a set of meta
/// information about the database itself, like the name or description
/// and a tree structure of groups and database entries. Groups can also
/// be nested within other groups.
///
/// ## Meta information
///
/// You can access the entire [`Meta`] struct using [`Database::meta()`].
/// For the most common information the following shortcut methods are provided:
///
/// * [`Database::name`] / [`Database::set_name`]
/// * [`Database::description`] / [`Database::set_description`]
///
/// ## Database entries
pub struct Database {
/// Meta information about this database
pub meta: Meta,
......@@ -289,7 +321,7 @@ impl Database {
///
/// Creates a root group if none exist
pub fn add_entry(&mut self, entry: Entry) {
if self.groups.len() == 0 {
if self.groups.is_empty() {
self.groups.push(Group::default());
}
self.groups[0].entries.push(entry);
......@@ -305,3 +337,52 @@ impl Database {
self.groups.get_mut(0)
}
}
/// TOTP one time password secret in KeepassXC format
pub struct Otp<'a> {
url: Cow<'a, str>,
}
impl<'a> Otp<'a> {
/// Create a new OTP password from the given details
pub fn new<S: ToString>(secret: S, period: u32, digits: u32) -> Otp<'static> {
let url = format!("otpauth://totp/kdbxrs:kdbxrs?secret={}&period={}&digits={}", secret.to_string(), period, digits);
Otp {
url: Cow::Owned(url)
}
}
fn find_url_param(&self, key: &str) -> Option<&str> {
let mut parts = self.url.split("?");
let _path = parts.next()?;
let params = parts.next()?;
let params = params.split("&");
for param in params {
let mut param_parts = param.split("=");
let pkey = param_parts.next()?;
if pkey == key {
return param_parts.next()
}
}
None
}
/// Retrieve the secret used to generate one time passwords
pub fn secret(&self) -> Option<&str> {
self.find_url_param("secret")
}
/// Return the period for which passwords are valid
pub fn period(&self) -> Option<u32> {
self.find_url_param("secret")
.and_then(|p| p.parse().ok())
}
/// Return the number of digits in the resulting code
pub fn digits(&self) -> Option<u32> {
self.find_url_param("digits")
.and_then(|p| p.parse().ok())
}
}
\ No newline at end of file
......@@ -73,4 +73,4 @@ pub(crate) fn to_hex_string(data: &[u8]) -> String {
}
output
}
}
\ No newline at end of file
......@@ -118,8 +118,8 @@ fn parse_field<R: Read, S: StreamCipher + ?Sized>(
let to_str = String::from_utf8(decoded)
.map_err(|_| Error::DecryptFailed(key_clone))?;
Value::Protected(to_str)
},
Err(_) => return Err(Error::DecryptFailed(key_clone))
}
Err(_) => return Err(Error::DecryptFailed(key_clone)),
}
} else {
Value::Standard(contents)
......@@ -128,7 +128,7 @@ fn parse_field<R: Read, S: StreamCipher + ?Sized>(
Value::Empty
}
}
XmlEvent::EndElement { name, .. } if &name.local_name == tag_name => break,
XmlEvent::EndElement { name, .. } if name.local_name == tag_name => break,
_ => {}
}
}
......@@ -226,7 +226,7 @@ fn parse_group<R: Read, S: StreamCipher + ?Sized>(
} else if &name.local_name == "UUID" {
group.uuid = parse_uuid(xml_event_reader)?;
} else if &name.local_name == "Name" {
group.name = parse_string(xml_event_reader)?.unwrap_or("".to_string());
group.name = parse_string(xml_event_reader)?.unwrap_or_else(String::new);
} else if &name.local_name == "Times" {
group.times = parse_times(xml_event_reader)?;
}
......@@ -262,12 +262,9 @@ fn parse_custom_data<R: Read, S: StreamCipher + ?Sized>(
let mut fields = Vec::new();
loop {
match xml_event_reader.next()? {
XmlEvent::StartElement { name, .. } => match name.local_name.as_ref() {
"Item" => {
fields.push(parse_field(xml_event_reader, "Item", stream_cipher)?);
}
_ => {}
},
XmlEvent::StartElement { name, .. } if &name.local_name == "Item" => {
fields.push(parse_field(xml_event_reader, "Item", stream_cipher)?);
}
XmlEvent::EndElement { name, .. } if &name.local_name == "CustomData" => break,
_ => {}
}
......@@ -359,7 +356,10 @@ fn parse_file<R: Read, S: StreamCipher + ?Sized>(
}
/// Parse decrypted XML into a database
pub fn parse_xml<R: Read, S: StreamCipher + ?Sized>(xml_data: R, stream_cipher: &mut S) -> Result<Database> {
pub fn parse_xml<R: Read, S: StreamCipher + ?Sized>(
xml_data: R,
stream_cipher: &mut S,
) -> Result<Database> {
let xml_config = xml::ParserConfig::new()
.trim_whitespace(true)
.cdata_to_characters(true);
......
......@@ -17,10 +17,7 @@ type Result<T> = std::result::Result<T, Error>;
fn write_bool_tag<W: Write>(writer: &mut XmlWriter<W>, name: &str, value: bool) -> Result<()> {
writer.write(XmlEvent::start_element(name))?;
writer.write(XmlEvent::characters(match value {
true => "True",
false => "False",
}))?;
writer.write(XmlEvent::characters(if value { "True" } else { "False" }))?;
writer.write(XmlEvent::end_element())?;
Ok(())
}
......@@ -116,7 +113,7 @@ fn write_entry<W: Write>(writer: &mut XmlWriter<W>, entry: &Entry) -> Result<()>
for field in &entry.fields {
write_field(writer, "String", field)?;
}
if entry.history.len() > 0 {
if !entry.history.is_empty() {
writer.write(XmlEvent::start_element("History"))?;
for old_entry in &entry.history {
write_entry(writer, old_entry)?;
......
use kdbx_rs;
use std::fs;
use std::path::PathBuf;
#[test]
fn keepassxc_otp_read_secret() -> Result<(), kdbx_rs::Error> {
let mut file_path = PathBuf::new();
file_path.push(env!("CARGO_MANIFEST_DIR"));
file_path.push("res");
file_path.push("test_input");
file_path.push("keepassxc-otp.kdbx");
let file = fs::File::open(file_path).unwrap();
let db = kdbx_rs::from_reader(file).unwrap();
let key = kdbx_rs::CompositeKey::from_password("kdbxrs");
let db = db.unlock(&key)?;
let xml = db.database();
assert_eq!("ABCDEFGHIJKLMNOP", xml.groups[0].entries[0].otp().unwrap().secret().unwrap());
Ok(())
}
\ No newline at end of file
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment