Commit 52553146 authored by Tony Finn's avatar Tony Finn

Write support now works

parent 7429e526
......@@ -23,21 +23,57 @@
{
"type": "lldb",
"request": "launch",
"name": "Debug executable 'kdbx-rs'",
"name": "Debug executable 'kdbx-dump-header'",
"cargo": {
"args": [
"build",
"--bin=kdbx-rs",
"--bin=kdbx-dump-header",
"--package=kdbx-rs"
],
"filter": {
"name": "kdbx-rs",
"name": "kdbx-dump-header",
"kind": "bin"
}
},
"args": ["res/kdbx4-argon2.kdbx"],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug executable 'kdbx-parse'",
"cargo": {
"args": [
"build",
"--bin=kdbx-parse",
"--package=kdbx-rs"
],
"filter": {
"name": "kdbx-parse",
"kind": "bin"
}
},
"args": ["kdbx_rs.kdbx"],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug executable 'kdbx-generate'",
"cargo": {
"args": [
"build",
"--bin=kdbx-generate",
"--package=kdbx-rs"
],
"filter": {
"name": "kdbx-generate",
"kind": "bin"
}
},
"args": [],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
......
......@@ -10,7 +10,7 @@ Library for reading and writing KDBX libraries from Rust
| License | GPLv3+ | MIT | MIT/Apache | MIT | ISC |
| KDF Support | Argon2/AES| Argon2/AES | AES | Argon2/AES | AES |
| Cipher support | AES | AES/Chacha20 | AES/Salsa20 | AES/Chacha20 | AES |
| .kdbx 4 support | Read only |Read only| No | Read only | No |
| .kdbx 4 support | Yes |Read only| No | Read only | No |
| .kdbx 3 support | No | No | Yes | Read only | No |
| .kdb support | No | No | No | No | Yes |
| Memory protection| No | No | No | Yes | Yes |
......
use kdbx_rs::binary::Unlocked;
use kdbx_rs::types::{Entry, Field, Group, Times};
use kdbx_rs::{CompositeKey, Database, Error, Kdbx};
......@@ -30,7 +29,7 @@ fn main() -> Result<(), Error> {
db.meta.database_name = "BarName".to_string();
db.meta.database_description = "BazDesc".to_string();
let mut group = Group::default();
group.name = "FooGroup".to_string();
group.name = "Root".to_string();
group.uuid = Uuid::from_u128(0x12345678);
group.times = sample_times();
let mut entry = Entry::default();
......@@ -44,7 +43,7 @@ fn main() -> Result<(), Error> {
let output_path = PathBuf::from("kdbx_rs.kdbx");
let mut file = File::create(output_path).expect("Could not open output file");
let mut kdbx = Kdbx::<Unlocked>::from_database(db)?;
let mut kdbx = Kdbx::from_database(db)?;
kdbx.set_key(CompositeKey::from_password("kdbxrs"))?;
kdbx.write(&mut file).expect("Could not write to file");
Ok(())
......
......@@ -50,8 +50,8 @@ pub enum HeaderError {
#[error("Error reading database header - {0}")]
Io(#[from] std::io::Error),
/// A supported field had an unexpected format
#[error("Incompatible database - Malformed field of type {0:?}")]
MalformedField(header::OuterHeaderId),
#[error("Incompatible database - Malformed field of type {0:?}: {1}")]
MalformedField(header::OuterHeaderId, String),
/// A required field is missing in the unencrypted header
#[error("Incompatible database - Missing required field of type {0:?}")]
MissingRequiredField(header::OuterHeaderId),
......
......@@ -214,20 +214,30 @@ impl KdbxHeaderBuilder {
OuterHeaderId::CipherId => {
let cipher = Uuid::from_slice(&header.data)
.map(From::from)
.map_err(|_e| Error::MalformedField(header.ty))?;
.map_err(|_e| {
Error::MalformedField(header.ty, "Cipher UUID not valid".into())
})?;
self.cipher = Some(cipher);
}
OuterHeaderId::KdfParameters => {
self.kdf_params = Some(
variant_dict::parse_variant_dict(&*header.data)
.map_err(|_| Error::MalformedField(OuterHeaderId::KdfParameters))?
.try_into()?,
);
self.kdf_params = match variant_dict::parse_variant_dict(&*header.data) {
Ok(vdict) => Some(vdict.try_into()?),
Err(e) => {
println!("Malformed field: {}", e);
return Err(Error::MalformedField(
OuterHeaderId::KdfParameters,
"Corrupt variant dictionary".into(),
));
}
};
}
OuterHeaderId::CompressionFlags => {
if header.data.len() != 4 {
return Err(Error::MalformedField(OuterHeaderId::CompressionFlags));
return Err(Error::MalformedField(
OuterHeaderId::CompressionFlags,
"Wrong size for compression ID".into(),
));
}
self.compression_type =
Some(wrapper_fields::CompressionType::from(u32::from_le_bytes([
......@@ -302,7 +312,7 @@ impl KdbxHeader {
/// [`getrandom`]: https://docs.rs/getrandom/0.1/getrandom/index.html
pub fn from_os_random() -> std::result::Result<KdbxHeader, getrandom::Error> {
let mut master_seed = vec![0u8; 32];
let mut encryption_iv = vec![0u8; 32];
let mut encryption_iv = vec![0u8; 16];
let mut cipher_salt = vec![0u8; 32];
getrandom(&mut master_seed)?;
getrandom(&mut encryption_iv)?;
......@@ -317,7 +327,7 @@ impl KdbxHeader {
lanes: 2,
},
other_headers: Vec::new(),
compression_type: super::CompressionType::Gzip,
compression_type: super::CompressionType::None,
master_seed,
encryption_iv,
})
......@@ -351,7 +361,6 @@ impl KdbxHeader {
.iter()
.cloned()
.chain(once(self.cipher.into()))
.chain(once(self.kdf_params.clone().into()))
.chain(once(self.compression_type.clone().into()))
.chain(once(HeaderField::new(
OuterHeaderId::MasterSeed,
......@@ -361,6 +370,7 @@ impl KdbxHeader {
OuterHeaderId::EncryptionIv,
self.encryption_iv.clone(),
)))
.chain(once(self.kdf_params.clone().into()))
.chain(once(HeaderField::new(
OuterHeaderId::EndOfHeader,
Vec::new(),
......@@ -470,7 +480,7 @@ impl KdbxInnerHeader {
for header in headers {
writer.write_all(&[header.ty.into()])?;
writer.write_all(&(header.data.len() as u32).to_le_bytes())?;
writer.write_all(&(header.data.len() as i32).to_le_bytes())?;
writer.write_all(&header.data)?;
}
Ok(())
......
use super::{errors, header};
use crate::{crypto, stream};
use crate::{crypto, stream, types};
use std::io::{Read, Write};
use std::ops::{Deref, DerefMut};
pub trait KdbxState: std::fmt::Debug {
fn header(&self) -> &header::KdbxHeader;
......@@ -30,31 +31,6 @@ impl<T: KdbxState> Kdbx<T> {
self.state.write(output)?;
Ok(())
}
/// Generate a new .kdbx from the given database
///
/// Uses OS randomness provided by [`getrandom`] to generate all required seed
/// and IVs.
///
/// Note that you need to set a key with [`Kdbx::set_key`]
/// to be able to write the database
pub fn from_database(
database: crate::Database,
) -> Result<Kdbx<Unlocked>, errors::DatabaseCreationError> {
let header = header::KdbxHeader::from_os_random()?;
let inner_header = header::KdbxInnerHeader::from_os_random()?;
let unlocked = Unlocked {
header,
inner_header,
major_version: 4,
minor_version: 0,
xml_data: None,
composed_key: None,
master_key: None,
database,
};
Ok(Kdbx { state: unlocked })
}
}
#[derive(Debug)]
......@@ -93,7 +69,9 @@ impl Unlocked {
crate::xml::write_xml(&mut encrypted_stream, &self.database)?;
drop(encrypted_stream);
let hmacw = encrypted_stream.finish()?;
let inner = hmacw.finish()?;
inner.flush()?;
Ok(encrypted_buf)
}
}
......@@ -152,6 +130,45 @@ impl Kdbx<Unlocked> {
pub fn raw_xml(&self) -> Option<&[u8]> {
self.state.xml_data.as_deref()
}
/// Generate a new .kdbx from the given database
///
/// Uses OS randomness provided by [`getrandom`] to generate all required seed
/// and IVs.
///
/// Note that you need to set a key with [`Kdbx::set_key`]
/// to be able to write the database
pub fn from_database(
database: crate::Database,
) -> Result<Kdbx<Unlocked>, errors::DatabaseCreationError> {
let header = header::KdbxHeader::from_os_random()?;
let inner_header = header::KdbxInnerHeader::from_os_random()?;
let unlocked = Unlocked {
header,
inner_header,
major_version: 4,
minor_version: 0,
xml_data: None,
composed_key: None,
master_key: None,
database,
};
Ok(Kdbx { state: unlocked })
}
}
impl Deref for Kdbx<Unlocked> {
type Target = types::Database;
fn deref(&self) -> &types::Database {
&self.state.database
}
}
impl DerefMut for Kdbx<Unlocked> {
fn deref_mut(&mut self) -> &mut types::Database {
&mut self.state.database
}
}
#[derive(Debug, PartialEq, Eq)]
......
......@@ -177,11 +177,24 @@ pub enum KdfParams {
impl TryFrom<VariantDict> for KdfParams {
type Error = HeaderError;
fn try_from(mut vdict: VariantDict) -> Result<Self, HeaderError> {
let uuid = vdict
.remove("$UUID")
.and_then(|uuid_val| uuid_val.try_into().ok())
.and_then(|array: Vec<u8>| Uuid::from_slice(&array).ok())
.ok_or_else(|| HeaderError::MalformedField(OuterHeaderId::KdfParameters))?;
let uuid_val = vdict.remove("$UUID").ok_or_else(|| {
HeaderError::MalformedField(
OuterHeaderId::KdfParameters,
"No UUID for kdf parameters".into(),
)
})?;
let uuid_array: Vec<u8> = uuid_val.try_into().map_err(|_| {
HeaderError::MalformedField(
OuterHeaderId::KdfParameters,
"KDF UUID not a byte array".into(),
)
})?;
let uuid = Uuid::from_slice(&uuid_array).map_err(|_| {
HeaderError::MalformedField(
OuterHeaderId::KdfParameters,
"KDF UUID not a valid UUID".into(),
)
})?;
let kdf_algorithm = KdfAlgorithm::from(uuid.clone());
......@@ -229,7 +242,7 @@ impl Into<VariantDict> for KdfParams {
vdict.insert(
"$UUID".into(),
variant_dict::Value::Array(
uuid::Uuid::from(KdfAlgorithm::Aes256_Kdbx4)
uuid::Uuid::from(KdfAlgorithm::Argon2)
.as_bytes()
.iter()
.cloned()
......@@ -237,10 +250,10 @@ impl Into<VariantDict> for KdfParams {
),
);
vdict.insert("M".into(), variant_dict::Value::Uint64(memory_bytes));
vdict.insert("I".into(), variant_dict::Value::Uint64(iterations));
vdict.insert("V".into(), variant_dict::Value::Uint32(version));
vdict.insert("P".into(), variant_dict::Value::Uint32(lanes));
vdict.insert("S".into(), variant_dict::Value::Array(salt));
vdict.insert("I".into(), variant_dict::Value::Uint64(iterations));
vdict.insert("P".into(), variant_dict::Value::Uint32(lanes));
}
KdfParams::Aes { rounds, salt } => {
vdict.insert(
......@@ -328,7 +341,7 @@ impl From<CompressionType> for HeaderField<OuterHeaderId> {
fn from(compression_type: CompressionType) -> HeaderField<OuterHeaderId> {
let compression_type_id: u32 = compression_type.into();
HeaderField::new(
OuterHeaderId::KdfParameters,
OuterHeaderId::CompressionFlags,
Vec::from(compression_type_id.to_le_bytes().as_ref()),
)
}
......
......@@ -11,12 +11,15 @@ use thiserror::Error;
#[derive(Error, Debug)]
/// Wrapper error type for this library
pub enum Error {
/// Failed to open a database
/// Failed to open a KDBX file
#[error("Could not open database: {0}")]
Open(#[from] OpenError),
/// Failed unlocking a database
/// Failed unlocking a KDBX file
#[error("Could not unlock database: {0}")]
Unlock(#[from] UnlockError),
/// Failed to write a KDBX file
#[error("Could not write database: {0}")]
Write(#[from] WriteError),
/// Failed parsing database XML
#[error("Failed to parse database XML: {0}")]
XmlRead(#[from] XmlReadError),
......
This diff is collapsed.
use crate::crypto;
use aes::block_cipher_trait::generic_array::GenericArray;
use aes::block_cipher_trait::BlockCipher;
use block_modes::block_padding::{Padding, Pkcs7};
use block_modes::{BlockMode, Cbc};
use std::io;
use thiserror::Error;
#[derive(Debug, Error)]
pub(crate) enum BlockCipherError {
#[error("Invalid length for IV")]
InvalidIvLength(#[from] block_modes::InvalidKeyIvLength),
}
pub(crate) struct BlockCipherReader<C, R>
where
R: io::Read,
C: BlockCipher,
{
inner: R,
buffer: GenericArray<u8, C::BlockSize>,
buf_idx: usize,
cipher: Cbc<C, Pkcs7>,
first_read: bool,
peek_byte: Option<u8>,
}
impl<C, R> BlockCipherReader<C, R>
where
R: io::Read,
C: BlockCipher,
{
pub(crate) fn wrap(
inner: R,
key: crypto::CipherKey,
iv: &[u8],
) -> Result<BlockCipherReader<C, R>, BlockCipherError> {
Ok(BlockCipherReader {
inner,
cipher: Cbc::new_var(&key.0, &iv)?,
buffer: GenericArray::default(),
buf_idx: 0,
first_read: true,
peek_byte: None,
})
}
fn buffer_next_block(&mut self) -> io::Result<usize> {
self.buf_idx = 0;
let mut buffered_bytes = 0;
if let Some(byte) = self.peek_byte {
self.buffer[0] = byte;
buffered_bytes = 1;
} else if !self.first_read {
return Ok(0);
}
self.first_read = false;
while buffered_bytes < self.buffer.len() {
let count = self.inner.read(&mut self.buffer[buffered_bytes..])?;
if count == 0 && buffered_bytes != 0 {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
format!(
"Data size not a multiple of block size, {} extra bytes",
count
),
));
} else if count == 0 {
return Ok(0);
}
buffered_bytes += count
}
let mut peek_buf = [0u8];
let peek_len = self.inner.read(&mut peek_buf)?;
self.peek_byte = if peek_len > 0 {
Some(peek_buf[0])
} else {
None
};
let mut blocks_to_decrypt = [std::mem::take(&mut self.buffer)];
self.cipher.decrypt_blocks(&mut blocks_to_decrypt);
let [decrypted_block] = blocks_to_decrypt;
self.buffer = decrypted_block;
if self.peek_byte.is_none() {
let unpadded = Pkcs7::unpad(&self.buffer)
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Bad padding"))?;
Ok(unpadded.len())
} else {
Ok(buffered_bytes)
}
}
}
impl<C, R> io::Read for BlockCipherReader<C, R>
where
R: io::Read,
C: BlockCipher,
{
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let mut remaining_in_buffer = self.buffer.len() - self.buf_idx;
if remaining_in_buffer == 0 || self.first_read {
remaining_in_buffer = self.buffer_next_block()?;
}
let copy_len = usize::min(remaining_in_buffer, buf.len());
for i in 0..copy_len {
buf[i] = self.buffer[self.buf_idx + i];
}
self.buf_idx += copy_len;
Ok(copy_len)
}
}
pub trait BlockCipherWriterExt<'a, W>: io::Write
where
W: io::Write + 'a,
{
fn finish(&mut self) -> io::Result<W>;
}
pub(crate) struct BlockCipherWriter<C, W>
where
W: io::Write,
C: BlockCipher,
{
inner: Option<W>,
buffer: GenericArray<u8, C::BlockSize>,
buf_idx: usize,
cipher: Cbc<C, Pkcs7>,
}
impl<C, W> BlockCipherWriter<C, W>
where
W: io::Write,
C: BlockCipher,
{
pub(crate) fn wrap(
inner: W,
key: crypto::CipherKey,
iv: &[u8],
) -> Result<BlockCipherWriter<C, W>, BlockCipherError> {
Ok(BlockCipherWriter {
inner: Some(inner),
cipher: Cbc::new_var(&key.0, &iv)?,
buffer: GenericArray::default(),
buf_idx: 0,
})
}
fn write_buffer(&mut self) -> io::Result<()> {
let inner = self
.inner
.as_mut()
.ok_or_else(|| io::Error::new(io::ErrorKind::BrokenPipe, "Buffer already closed"))?;
let mut blocks_to_encrypt = [std::mem::take(&mut self.buffer)];
self.cipher.encrypt_blocks(&mut blocks_to_encrypt);
inner.write_all(&blocks_to_encrypt[0])?;
Ok(())
}
}
impl<'a, C, W> BlockCipherWriterExt<'a, W> for BlockCipherWriter<C, W>
where
W: io::Write + 'a,
C: BlockCipher,
{
fn finish(&mut self) -> io::Result<W> {
if self.inner.is_some() {
Pkcs7::pad_block(&mut self.buffer, self.buf_idx)
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Could not pad data"))?;
self.write_buffer()?;
Ok(self.inner.take().unwrap())
} else {
Err(io::Error::new(
io::ErrorKind::BrokenPipe,
"Buffer already closed",
))
}
}
}
impl<C, W> io::Write for BlockCipherWriter<C, W>
where
W: io::Write,
C: BlockCipher,
{
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
for byte in buf.iter() {
self.buffer[self.buf_idx] = *byte;
self.buf_idx += 1;
if self.buf_idx == self.buffer.len() {
self.write_buffer()?;
self.buf_idx = 0;
}
}
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
if let Some(inner) = self.inner.as_mut() {
inner.flush()?
}
Ok(())
}
}
pub const HMAC_WRITE_BLOCK_SIZE: usize = 1024 * 1024;
use crate::crypto::HmacKey;
use std::io::{self, Read, Write};
pub(crate) struct HMacReader<R>
where
R: Read,
{
block_idx: u64,
buf_idx: usize,
buffer: Vec<u8>,
inner: R,
hmac_key: HmacKey,
}
impl<R: io::Read> HMacReader<R> {
pub(crate) fn new(inner: R, hmac_key: HmacKey) -> HMacReader<R> {
HMacReader {
block_idx: 0,
buf_idx: 0,
buffer: Vec::new(),
inner,
hmac_key,
}
}
fn buffer_next_block(&mut self) -> io::Result<usize> {
let mut hmac = [0u8; 32];
self.inner.read_exact(&mut hmac)?;
let mut len_buffer = [0u8; 4];
self.inner.read_exact(&mut len_buffer)?;
let len = u32::from_le_bytes(len_buffer) as usize;
self.buffer.resize_with(len, Default::default);
self.inner.read_exact(&mut self.buffer)?;
if !self
.hmac_key
.block_key(self.block_idx)
.verify_data_block(&hmac, &self.buffer)
{
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("HMAC validation failed for block {}", self.block_idx),
));
}
self.buf_idx = 0;
self.block_idx += 1;
Ok(len)
}
}
impl<R: Read> Read for HMacReader<R> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let mut remaining_in_buffer = self.buffer.len() - self.buf_idx;
if remaining_in_buffer == 0 {
remaining_in_buffer = self.buffer_next_block()?;
}
let copy_len = usize::min(remaining_in_buffer, buf.len());
for i in 0..copy_len {
buf[i] = self.buffer[self.buf_idx + i];
}
self.buf_idx += copy_len;
Ok(copy_len)
}
}
pub(crate) struct HmacWriter<'a, W>
where
W: 'a + io::Write,
{
block_idx: u64,
buffer: Vec<u8>,
inner: W,
hmac_key: HmacKey,
_lifetime: std::marker::PhantomData<&'a ()>,
}
impl<'a, W> HmacWriter<'a, W>
where
W: 'a + io::Write,
{
pub(crate) fn new(inner: W, hmac_key: HmacKey) -> HmacWriter<'a, W> {
HmacWriter {
block_idx: 0,
buffer: Vec::with_capacity(HMAC_WRITE_BLOCK_SIZE),
inner,
hmac_key,
_lifetime: std::marker::PhantomData,
}