From 444e15e3caba720a6dc8aef2cdd63eaaccfa43bd Mon Sep 17 00:00:00 2001 From: femsci Date: Wed, 28 Dec 2022 11:03:04 +0100 Subject: [PATCH] Restructure preferences. Use B+ Trees for indices. Decouple common blocks. --- src/config.rs | 6 +- src/main.rs | 4 +- src/store/local.rs | 384 ++++++++++++++++++++++++++++++-------------- src/store/mod.rs | 79 ++++++++- src/store/server.rs | 24 ++- 5 files changed, 358 insertions(+), 139 deletions(-) diff --git a/src/config.rs b/src/config.rs index 74885de..4375694 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,10 +1,12 @@ use std::{fs::File, io::Read, path::Path}; +use crate::store::HashType; + pub struct Preferences { + _store_type: String, _store_path: String, - _pref_hash_algo: String, + _pref_hash_algo: HashType, _pref_enc_algo: String, - _pref_sign_algo: String, } pub fn try_load_preferences() -> Option { diff --git a/src/main.rs b/src/main.rs index cddbf9b..6194287 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,7 +11,7 @@ fn main() { //options } - try_load_preferences(); + let _preferences = try_load_preferences(); let store = match store::local::load( "/tmp/store.db".to_string(), @@ -24,5 +24,5 @@ fn main() { Ok(x) => x, }; - store.get_creds_by_key("meow".to_string()); + store.get_creds("nya".to_string(), "meow".to_string()); } diff --git a/src/store/local.rs b/src/store/local.rs index f45b77b..f5c67b5 100644 --- a/src/store/local.rs +++ b/src/store/local.rs @@ -1,7 +1,9 @@ use std::{ arch::x86_64::_mm_crc32_u32, + collections::{hash_map::DefaultHasher, HashMap}, fs::File, - io::{BufReader, Read, Write}, + hash::{Hash, Hasher}, + io::{BufReader, Read, Seek, SeekFrom, Write}, path::Path, }; @@ -10,12 +12,14 @@ use argon2::PasswordHasher; use password_hash::SaltString; use rand::{rngs::StdRng, RngCore, SeedableRng}; -use super::{Store, StoreRecord}; +use super::{EncryptionType, HashType, RecordType, Store, StoreRecord}; pub struct LocalStore { path: String, header: LocalStoreHeader, + meta: LocalStoreMeta, enc_ctx: EncryptionContext, + cache: HashMap, } /* @@ -39,13 +43,20 @@ pub struct LocalStore { --Block 0 Superblock: Hash: - 0x00 - master password hash + 0x40 - master password hash Metadata: - 0x20 - record count - 0x24 - specifier count - 0x08 - + 0x60 - record count + 0x64 - specifier count + 0x68 - padding with 0 + Auth tag: 0x80 +--Block 1 + Index: + Root node: + 0x90 - root node or invalid node if empty db */ +//Proposal: Initialize IV to 0? + const MAGIC_NUM: u32 = 0x6d656f77; const HEADER_SIZE: usize = 32; @@ -78,14 +89,15 @@ impl LocalStoreHeader { fn is_checksum_valid(&self) -> bool { let chk = self.get_checksum(); - /*println!( - "Checksum: {:x}, Expected: {:x}, Size:: {}", - self.chksum_crc, chk, HEADER_SIZE - );*/ chk == self.chksum_crc } } +struct LocalStoreMeta { + record_count: u32, + specifier_count: u32, +} + struct EncryptionContext { enc_type: EncryptionType, hash_type: HashType, @@ -94,44 +106,6 @@ struct EncryptionContext { iv_root: Vec, } -#[derive(Clone, Copy, PartialEq, PartialOrd)] -enum EncryptionType { - Invalid = 0, - AesGcm = 1, - Chacha20Poly1305 = 2, -} - -impl EncryptionType { - fn from(n: u8) -> EncryptionType { - match n { - 0 => EncryptionType::Invalid, - 1 => EncryptionType::AesGcm, - 2 => EncryptionType::Chacha20Poly1305, - _ => panic!("Invalid value '{}'.", n), - } - } -} - -#[derive(Clone, Copy, PartialEq, PartialOrd)] -enum HashType { - Invalid = 0, - Argon2 = 1, - Bcrypt = 2, - Pbkdf2 = 4, -} - -impl HashType { - fn from(n: u8) -> HashType { - match n { - 0 => HashType::Invalid, - 1 => HashType::Argon2, - 2 => HashType::Bcrypt, - 4 => HashType::Pbkdf2, - _ => panic!("Invalid value '{}'.", n), - } - } -} - fn try_deserialize_header(file: &mut BufReader) -> Result { let mut header: LocalStoreHeader = LocalStoreHeader { magic_h: 0, @@ -176,16 +150,18 @@ fn try_deserialize_superblock( file: &mut BufReader, enc: &EncryptionContext, header: &LocalStoreHeader, -) -> Result { - let mut buf: Vec = vec![0; header.enc_sup_block_size as usize]; +) -> Result { + let data = enc + .decrypt_block_from_file(header.enc_sup_block_size as usize, 0, file) + .unwrap(); - if file.read_exact(&mut buf).is_err() { - return Err("Read error."); + if data[0..enc.key.len()].ne(&enc.key) { + return Err("Key mismatch."); } - let dat = enc.decrypt_block(&buf).unwrap(); - println!("Superblocc: {:?}", &dat); - Ok(String::from_utf8_lossy(&dat).try_into().unwrap()) + let meta = deserialize_store_meta(&data[31..]); + + Ok(meta) } fn serialize_header(header: &LocalStoreHeader) -> Vec { @@ -208,8 +184,8 @@ fn serialize_header(header: &LocalStoreHeader) -> Vec { ); buf.append(u32::to_be_bytes(header.chksum_crc).to_vec().as_mut()); - let padlen = 32 - buf.len(); - let mut pad: Vec = vec![0; padlen]; + let padding_len = 32 - buf.len(); + let mut pad: Vec = vec![0; padding_len]; buf.append(&mut pad); return buf; @@ -240,21 +216,28 @@ fn try_create_store(path: &String, passphrase: &Vec) -> bool { iv_root: iv.to_vec(), }; - let mut enc_buf: Vec = Vec::new(); + let mut superblock_buf: Vec = Vec::new(); - enc_buf.extend_from_slice(&enc.key); - if enc_buf.len() != 32 { - enc_buf.append([b'\0'].repeat(32 - enc_buf.len()).as_mut()); + superblock_buf.extend_from_slice(&enc.key); + if superblock_buf.len() != 32 { + superblock_buf.append([b'\0'].repeat(32 - superblock_buf.len()).as_mut()); } - let enc_buf = enc.encrypt_block(&enc_buf).unwrap(); + let meta = LocalStoreMeta { + record_count: 0, + specifier_count: 0, + }; + + superblock_buf.append(&mut serialize_store_meta(&meta)); + + let enc_buf = enc.encrypt_block(&superblock_buf, 0).unwrap(); let mut header: LocalStoreHeader = LocalStoreHeader { magic_h: MAGIC_NUM, last_access: timestamp, last_write: timestamp, enc_type: enc.enc_type, - enc_sup_block_size: enc_buf.len() as u16, + enc_sup_block_size: (enc_buf.len() - 16) as u16, enc_key_size: 256, hash_type: enc.hash_type, encrypted_block_offset: 32, @@ -319,7 +302,25 @@ fn derive_key( Ok(hash) } -fn get_specifier_map(file: &mut BufReader, header: &LocalStoreHeader) {} +fn deserialize_store_meta(data: &[u8]) -> LocalStoreMeta { + let records = u32::from_be_bytes(data[0..4].try_into().unwrap()); + let specifiers = u32::from_be_bytes(data[4..8].try_into().unwrap()); + + LocalStoreMeta { + record_count: records, + specifier_count: specifiers, + } +} + +fn serialize_store_meta(meta: &LocalStoreMeta) -> Vec { + let mut buf: Vec = Vec::with_capacity(32); + + buf.append(u32::to_be_bytes(meta.record_count).to_vec().as_mut()); + buf.append(u32::to_be_bytes(meta.specifier_count).to_vec().as_mut()); + buf.append([b'\0'].repeat(32 - buf.len()).as_mut()); + + buf +} pub fn load(path: String, passphrase: Vec) -> Result { let path_p = Path::new(&path); @@ -362,112 +363,192 @@ pub fn load(path: String, passphrase: Vec) -> Result d, Err(x) => return Err(x), }; - println!("Data: {}", &sup_data); - - let spec_map = get_specifier_map(&mut reader, &header); - let store = LocalStore { path: path, header, enc_ctx, + meta, + cache: HashMap::new(), }; return Ok(store); } -struct CredentialRecord { - id: u64, - key: String, - value: String, - specifier: Option, - meta: Option, +struct LocalRecord { + record: StoreRecord, + position: usize, + size: usize, } -const CRED_FLAG_SPECIFIER: u8 = 0b0000010; const CRED_FLAG_METADATA: u8 = 0b0000100; -impl CredentialRecord { +impl LocalRecord { fn serialize(&self) -> Vec { - let mut bytes = Vec::::new(); + let mut buf = Vec::::new(); + + buf.push(self.record.r#type as u8); let mut flags: u8 = 0; - if self.specifier.is_some() { - flags |= CRED_FLAG_SPECIFIER; - //put specifier here for better indexing - //use hashtable in superblock - } - - if self.meta.is_some() { + if self.record.meta.is_some() { flags |= CRED_FLAG_METADATA; } - bytes.push(flags); + buf.push(flags); - bytes.append(u64::to_be_bytes(self.id).to_vec().as_mut()); + let spec_bytes = self.record.specifier.bytes(); + let key_bytes = self.record.key.bytes(); + let value_bytes = &self.record.value; + let meta_len = if self.record.meta.is_none() { + 0 + } else { + self.record.meta.as_ref().unwrap().len() + }; - let key_bytes = self.key.bytes(); - let value_bytes = self.value.bytes(); + buf.append(u32::to_be_bytes(spec_bytes.len() as u32).to_vec().as_mut()); + buf.append(u32::to_be_bytes(key_bytes.len() as u32).to_vec().as_mut()); + buf.append(u32::to_be_bytes(value_bytes.len() as u32).to_vec().as_mut()); + buf.append(u32::to_be_bytes(meta_len as u32).to_vec().as_mut()); - bytes.append(u32::to_be_bytes(key_bytes.len() as u32).to_vec().as_mut()); - bytes.append(u32::to_be_bytes(value_bytes.len() as u32).to_vec().as_mut()); + buf.extend(spec_bytes); + buf.extend(key_bytes); + buf.extend(value_bytes); - bytes.extend(key_bytes); - bytes.extend(value_bytes); + if meta_len > 0 { + buf.extend(self.record.meta.as_ref().unwrap().bytes()); + } - bytes + buf } - fn from_block(bytes: &[u8]) { - let flags = bytes[0]; + fn from_block(bytes: &[u8]) -> Result { + let r#type = bytes[0]; + let flags = bytes[1]; + let spec_len = u32::from_be_bytes(bytes[2..6].try_into().unwrap()) as usize; + let key_len = u32::from_be_bytes(bytes[6..10].try_into().unwrap()) as usize; + let val_len = u32::from_be_bytes(bytes[10..14].try_into().unwrap()) as usize; + let meta_len = u32::from_be_bytes(bytes[14..18].try_into().unwrap()) as usize; - let id = u64::from_be_bytes(bytes[1..5].try_into().unwrap()); - todo!() + if key_len < 1 { + return Err("Invalid structure: key length is 0."); + } + + if spec_len < 1 { + return Err("Invalid structure: specifier length is 0."); + } + + if val_len < 1 { + return Err("Invalid structure: value length is 0."); + } + + let key_offset: usize = 18 + spec_len; + let val_offset: usize = key_offset + key_len; + + let specifier = std::str::from_utf8(&bytes[18..(18 + spec_len)]) + .expect("Cannot deserialize the specifier."); + let key = std::str::from_utf8(&bytes[key_offset..key_offset + key_len]) + .expect("Cannot deserialize the key."); + let value = &bytes[val_offset..val_offset + val_len]; + let meta = if meta_len == 0 { + None + } else { + let meta_offset = val_offset + val_len; + Some( + std::str::from_utf8(&bytes[meta_offset..meta_offset + meta_len]) + .expect("Cannot deserialize metadata.") + .to_owned(), + ) + }; + + let record = LocalRecord { + record: StoreRecord { + specifier: specifier.to_owned(), + key: key.to_owned(), + r#type: RecordType::from(r#type), + value: value.to_vec(), + meta: meta, + }, + position: 0, + size: val_offset + val_len + meta_len, + }; + + Ok(record) } - - fn encrypt(&self, typ: EncryptionType) {} } impl Store for LocalStore { - fn get_creds(&self, id: u64) -> Option { + fn get_creds(&self, specifier: String, key: String) -> Option { + self.btree_find_key(&key); todo!() } - fn get_creds_by_key(&self, key: String) -> Vec { + fn get_creds_by_specifier(&self, specifier: String) -> Vec { todo!() } fn store_creds( &self, + specifier: String, key: String, - value: String, - specifier: Option, + value: Vec, + r#type: RecordType, meta: Option, ) { todo!() } } +impl LocalStore { + fn btree_find_key(&self, key: &String) { + let mut file: BufReader = + BufReader::new(File::open(&self.path).expect("Cannot open file")); + + let hash = { + let mut hasher = DefaultHasher::new(); + key.hash(&mut hasher); + hasher.finish() + }; + + let pos = file.seek(SeekFrom::Start(0x90)).unwrap(); + println!("Pos: {:x}", pos); + } +} + +fn nonce_offset(nonce_root: &Vec, offset: usize) -> Vec { + let len = nonce_root.len(); + if len < (usize::BITS / 8) as usize { + panic!("Nonce length less than {}", usize::BITS / 8); + } + + let mut nonce_copy = vec![0; len]; + nonce_copy.clone_from_slice(&nonce_root); + + for i in 0..(usize::BITS / 8) { + println!( + "{}, {:x}", + len - (i as usize + 1), + (offset & (0xff << (8 * i))) >> (8 * i) + ); + nonce_copy[len - (i as usize + 1)] += ((offset & (0xff << (8 * i))) >> (8 * i)) as u8; + } + + nonce_copy +} + impl EncryptionContext { - fn decrypt_block(&self, ciphertext: &Vec) -> Result, &'static str> { + fn decrypt_block(&self, ciphertext: &Vec, offset: usize) -> Result, &'static str> { let plaintext = match self.enc_type { EncryptionType::Invalid => unreachable!(), EncryptionType::AesGcm => { - println!( - "Aes dec: len: {} key: {:?}: iv: {:?}", - ciphertext.len(), - &self.key, - &self.iv_root - ); - let aes_key = aes_gcm::aead::generic_array::GenericArray::from_slice(&self.key); let aes = Aes256Gcm::new(aes_key); - let nonce = Nonce::from_slice(&self.iv_root); + let nonce = nonce_offset(&self.iv_root, offset); + let nonce = Nonce::from_slice(&nonce); aes.decrypt(nonce, ciphertext.as_ref()) } @@ -481,21 +562,15 @@ impl EncryptionContext { Ok(plaintext.unwrap()) } - fn encrypt_block(&self, plaintext: &Vec) -> Result, &'static str> { + fn encrypt_block(&self, plaintext: &Vec, offset: usize) -> Result, &'static str> { let ciphertext = match self.enc_type { EncryptionType::Invalid => unreachable!(), EncryptionType::AesGcm => { - println!( - "Aes enc: len: {} key: {:?}: iv: {:?}", - plaintext.len(), - &self.key, - &self.iv_root - ); - let aes_key = aes_gcm::aead::generic_array::GenericArray::from_slice(&self.key); let aes = Aes256Gcm::new(aes_key); - let nonce = Nonce::from_slice(&self.iv_root); + let nonce = nonce_offset(&self.iv_root, offset); + let nonce = Nonce::from_slice(&nonce); aes.encrypt(nonce, plaintext.as_ref()) } @@ -508,13 +583,32 @@ impl EncryptionContext { Ok(ciphertext.unwrap()) } + + fn decrypt_block_from_file( + &self, + len: usize, + offset: usize, + file: &mut BufReader, + ) -> Result, &'static str> { + let mut buf: Vec = vec![0; len + 16]; + if file.read_exact(&mut buf).is_err() { + return Err("Read error."); + } + + self.decrypt_block(&buf, offset) + } } #[cfg(test)] mod tests { use std::mem::size_of; - use crate::store::local::{LocalStoreHeader, HEADER_SIZE}; + use crate::store::{ + local::{nonce_offset, LocalStoreHeader, HEADER_SIZE}, + StoreRecord, + }; + + use super::LocalRecord; #[test] fn header_size_const_greater_than_realsize() { @@ -523,4 +617,48 @@ mod tests { assert!(c > r, "const = {}, real = {}", c, r); } + + #[test] + fn nonce_offset_increment() { + let nonce: Vec = vec![0; 12]; + + let nonce_off = nonce_offset(&nonce, 0xfeeddeadbeef); + + assert!( + nonce_off[11] == 0xef + && nonce_off[10] == 0xbe + && nonce_off[9] == 0xad + && nonce_off[8] == 0xde + && nonce_off[7] == 0xed + && nonce_off[6] == 0xfe, + "\nn1 = {:x?}\nn2 = {:x?}", + nonce, + nonce_off + ); + } + + #[test] + fn localstore_record_serialize() { + let rec = LocalRecord { + position: 0, + size: 0, + record: StoreRecord { + key: "meow".to_owned(), + meta: None, + specifier: "test".to_owned(), + r#type: crate::store::RecordType::LOGIN_PASSWORD, + value: vec![1, 2, 3, 4], + }, + }; + + let deser = rec.serialize(); + + println!("D: {:x?}", deser); + + let reser = LocalRecord::from_block(&deser).unwrap(); + + let redes = reser.serialize(); + + assert_eq!(deser, redes); + } } diff --git a/src/store/mod.rs b/src/store/mod.rs index 9e2650f..419e8d1 100644 --- a/src/store/mod.rs +++ b/src/store/mod.rs @@ -2,21 +2,86 @@ pub(crate) mod local; pub(crate) mod server; pub struct StoreRecord { - id: u64, + specifier: String, key: String, - value: String, - specifier: Option, + r#type: RecordType, + value: Vec, meta: Option, } +#[derive(Clone, Copy, PartialEq, PartialOrd)] +pub enum RecordType { + LOGIN_PASSWORD = 0, + PASSWORD = 1, + SSH_KEY = 2, + PGP_KEY = 3, + SYM_KEY = 4, + ASYM_KEY_PAIR = 5, + ASYM_PRIV_KEY = 6, +} + +impl RecordType { + pub fn from(n: u8) -> Self { + match n { + 0 => RecordType::LOGIN_PASSWORD, + 1 => RecordType::PASSWORD, + 2 => RecordType::SSH_KEY, + 3 => RecordType::PGP_KEY, + 4 => RecordType::SYM_KEY, + 5 => RecordType::ASYM_KEY_PAIR, + 6 => RecordType::ASYM_PRIV_KEY, + _ => panic!("Invalid value '{}'.", n), + } + } +} + pub trait Store { - fn get_creds(&self, id: u64) -> Option; - fn get_creds_by_key(&self, key: String) -> Vec; + fn get_creds(&self, specifier: String, key: String) -> Option; + fn get_creds_by_specifier(&self, specifier: String) -> Vec; fn store_creds( &self, + specifier: String, key: String, - value: String, - specifier: Option, + value: Vec, + r#type: RecordType, meta: Option, ); } + +#[derive(Clone, Copy, PartialEq, PartialOrd)] +pub enum EncryptionType { + Invalid = 0, + AesGcm = 1, + Chacha20Poly1305 = 2, +} + +impl EncryptionType { + pub fn from(n: u8) -> Self { + match n { + 0 => EncryptionType::Invalid, + 1 => EncryptionType::AesGcm, + 2 => EncryptionType::Chacha20Poly1305, + _ => panic!("Invalid value '{}'.", n), + } + } +} + +#[derive(Clone, Copy, PartialEq, PartialOrd)] +pub enum HashType { + Invalid = 0, + Argon2 = 1, + Bcrypt = 2, + Pbkdf2 = 4, +} + +impl HashType { + pub fn from(n: u8) -> Self { + match n { + 0 => HashType::Invalid, + 1 => HashType::Argon2, + 2 => HashType::Bcrypt, + 4 => HashType::Pbkdf2, + _ => panic!("Invalid value '{}'.", n), + } + } +} diff --git a/src/store/server.rs b/src/store/server.rs index a880ba3..be0f756 100644 --- a/src/store/server.rs +++ b/src/store/server.rs @@ -1,6 +1,6 @@ use reqwest::header::HeaderValue; -use super::{Store, StoreRecord}; +use super::{Store, StoreRecord, RecordType}; pub struct ServerStore { address: String, @@ -27,20 +27,34 @@ pub fn load(address: String) -> Result { return Ok(ServerStore { address: address }); } +impl ServerStore { + fn get_endpoint(&self, path: String) -> String { + let base = self.address.trim(); + let base = if base.ends_with('/') { + &base[..base.len() - 2] + } else { + base + }; + + format!("{}{}", base, path) + } +} + impl Store for ServerStore { - fn get_creds(&self, id: u64) -> Option { + fn get_creds(&self, specifier: String, key: String) -> Option { todo!() } - fn get_creds_by_key(&self, key: String) -> Vec { + fn get_creds_by_specifier(&self, specifier: String) -> Vec { todo!() } fn store_creds( &self, + specifier: String, key: String, - value: String, - specifier: Option, + value: Vec, + r#type: RecordType, meta: Option, ) { todo!()