Refactore local store. Create data accessor with shared buffers.
This commit is contained in:
parent
ab056bfe73
commit
53e5bd81af
12 changed files with 1335 additions and 1069 deletions
|
@ -1,6 +1,6 @@
|
|||
use std::{fs::File, io::Read, path::Path};
|
||||
|
||||
use crate::store::HashType;
|
||||
use crate::crypto::HashType;
|
||||
|
||||
pub struct Preferences {
|
||||
_store_type: String,
|
||||
|
@ -11,7 +11,7 @@ pub struct Preferences {
|
|||
|
||||
pub fn try_load_preferences() -> Option<Preferences> {
|
||||
//Alert: develop a better home dir acquisition method
|
||||
let mut file = Path::new("/home/nya/.nyanpass.conf");
|
||||
let mut file = Path::new("~/.nyanpass.conf");
|
||||
if !file.exists() {
|
||||
file = Path::new("/etc/nyanpass.conf");
|
||||
}
|
||||
|
|
274
src/crypto/mod.rs
Normal file
274
src/crypto/mod.rs
Normal file
|
@ -0,0 +1,274 @@
|
|||
use std::{
|
||||
fs::File,
|
||||
io::{BufReader, Read},
|
||||
};
|
||||
|
||||
use aes_gcm::{aead::Aead, Aes256Gcm, KeyInit, Nonce};
|
||||
use argon2::PasswordHasher;
|
||||
use password_hash::SaltString;
|
||||
use rand::{rngs::StdRng, RngCore, SeedableRng};
|
||||
|
||||
use crate::data::DataPage;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, PartialOrd)]
|
||||
pub enum EncryptionType {
|
||||
Invalid = 0,
|
||||
AesGcm = 1,
|
||||
Chacha20Poly1305 = 2,
|
||||
}
|
||||
|
||||
impl From<u8> for EncryptionType {
|
||||
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 From<u8> for HashType {
|
||||
fn from(n: u8) -> Self {
|
||||
match n {
|
||||
0 => HashType::Invalid,
|
||||
1 => HashType::Argon2,
|
||||
2 => HashType::Bcrypt,
|
||||
4 => HashType::Pbkdf2,
|
||||
_ => panic!("Invalid value '{}'.", n),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait EncryptionManager {}
|
||||
|
||||
pub struct EncryptionContext {
|
||||
enc_type: EncryptionType,
|
||||
hash_type: HashType,
|
||||
key: Vec<u8>,
|
||||
salt: SaltString,
|
||||
iv_root: Vec<u8>,
|
||||
}
|
||||
|
||||
impl EncryptionContext {
|
||||
pub fn decrypt_block(&self, ciphertext: &Vec<u8>, offset: usize) -> Result<DataPage, String> {
|
||||
let plaintext = match self.enc_type {
|
||||
EncryptionType::Invalid => unreachable!(),
|
||||
EncryptionType::AesGcm => {
|
||||
let aes_key = aes_gcm::aead::generic_array::GenericArray::from_slice(&self.key);
|
||||
let aes = Aes256Gcm::new(aes_key);
|
||||
|
||||
let nonce = nonce_offset(&self.iv_root, offset);
|
||||
let nonce = Nonce::from_slice(&nonce);
|
||||
|
||||
aes.decrypt(nonce, ciphertext.as_ref())
|
||||
}
|
||||
EncryptionType::Chacha20Poly1305 => todo!(),
|
||||
};
|
||||
|
||||
if plaintext.is_err() {
|
||||
return Err(format!("Decryption error: {}", plaintext.unwrap_err()));
|
||||
}
|
||||
|
||||
Ok(DataPage::load(offset, plaintext.unwrap()))
|
||||
}
|
||||
|
||||
pub fn decrypt_block_from_file(
|
||||
&self,
|
||||
len: usize,
|
||||
offset: usize,
|
||||
file: &mut BufReader<File>,
|
||||
) -> Result<DataPage, String> {
|
||||
let mut buf: Vec<u8> = vec![0; len + 16];
|
||||
match file.read_exact(&mut buf) {
|
||||
Err(err) => Err(format!("Read error: {}", err.to_string())),
|
||||
Ok(_) => self.decrypt_block(&buf, offset),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default(passphrase: &Vec<u8>) -> Self {
|
||||
let mut rng = StdRng::from_entropy();
|
||||
|
||||
let salt = SaltString::generate(&mut rng);
|
||||
|
||||
let (default_hash_type, default_enc_type) = (HashType::Argon2, EncryptionType::AesGcm);
|
||||
|
||||
let key = derive_key(&passphrase, &salt, default_hash_type).unwrap();
|
||||
|
||||
let mut iv: [u8; 12] = [0; 12];
|
||||
rng.fill_bytes(&mut iv);
|
||||
|
||||
EncryptionContext {
|
||||
enc_type: default_enc_type,
|
||||
hash_type: default_hash_type,
|
||||
key,
|
||||
salt,
|
||||
iv_root: iv.to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn encrypt_block(
|
||||
&self,
|
||||
plaintext: &Vec<u8>,
|
||||
offset: usize,
|
||||
) -> Result<DataPage, &'static str> {
|
||||
let ciphertext = match self.enc_type {
|
||||
EncryptionType::Invalid => unreachable!(),
|
||||
EncryptionType::AesGcm => {
|
||||
let aes_key = aes_gcm::aead::generic_array::GenericArray::from_slice(&self.key);
|
||||
let aes = Aes256Gcm::new(aes_key);
|
||||
|
||||
let nonce = nonce_offset(&self.iv_root, offset);
|
||||
let nonce = Nonce::from_slice(&nonce);
|
||||
|
||||
aes.encrypt(nonce, plaintext.as_ref())
|
||||
}
|
||||
EncryptionType::Chacha20Poly1305 => todo!(),
|
||||
};
|
||||
|
||||
if ciphertext.is_err() {
|
||||
return Err("Encryption error");
|
||||
}
|
||||
|
||||
Ok(DataPage::load(offset, ciphertext.unwrap()))
|
||||
}
|
||||
|
||||
pub fn encryption_type(&self) -> EncryptionType {
|
||||
self.enc_type
|
||||
}
|
||||
|
||||
pub fn from_settings(
|
||||
passphrase: &Vec<u8>,
|
||||
enc_type: EncryptionType,
|
||||
hash_type: HashType,
|
||||
salt: SaltString,
|
||||
iv_root: Vec<u8>,
|
||||
) -> Self {
|
||||
let key = derive_key(&passphrase, &salt, hash_type).unwrap();
|
||||
|
||||
EncryptionContext {
|
||||
enc_type,
|
||||
hash_type,
|
||||
key,
|
||||
salt,
|
||||
iv_root,
|
||||
}
|
||||
}
|
||||
|
||||
pub unsafe fn get_key(&self) -> &Vec<u8> {
|
||||
&self.key
|
||||
}
|
||||
|
||||
pub fn hash_type(&self) -> HashType {
|
||||
self.hash_type
|
||||
}
|
||||
|
||||
pub fn offset_iv_by(&self, offset: usize) -> Vec<u8> {
|
||||
nonce_offset(&self.iv_root, offset)
|
||||
}
|
||||
|
||||
pub fn root_iv(&self) -> &Vec<u8> {
|
||||
&self.iv_root
|
||||
}
|
||||
|
||||
pub fn salt(&self) -> &SaltString {
|
||||
&self.salt
|
||||
}
|
||||
}
|
||||
|
||||
pub fn nonce_offset(nonce_root: &Vec<u8>, offset: usize) -> Vec<u8> {
|
||||
let len = nonce_root.len();
|
||||
if len < (usize::BITS / 8) as usize {
|
||||
panic!("Nonce length less than {}", usize::BITS / 8);
|
||||
}
|
||||
let start = len - (usize::BITS / 8) as usize;
|
||||
|
||||
let mut nonce_copy = vec![0; len];
|
||||
nonce_copy.clone_from_slice(&nonce_root);
|
||||
|
||||
let mut num = u64::from_be_bytes(nonce_copy[start..len].try_into().unwrap());
|
||||
num += offset as u64;
|
||||
nonce_copy[start..len].copy_from_slice(&num.to_be_bytes());
|
||||
|
||||
nonce_copy
|
||||
}
|
||||
|
||||
pub fn derive_key(
|
||||
password: &Vec<u8>,
|
||||
salt: &SaltString,
|
||||
hash_type: HashType,
|
||||
) -> Result<Vec<u8>, &'static str> {
|
||||
let hash = match hash_type {
|
||||
HashType::Invalid => unreachable!(),
|
||||
HashType::Argon2 => {
|
||||
let argon = argon2::Argon2::default();
|
||||
argon.hash_password(password, salt)
|
||||
}
|
||||
HashType::Bcrypt => {
|
||||
todo!()
|
||||
}
|
||||
HashType::Pbkdf2 => {
|
||||
todo!()
|
||||
}
|
||||
};
|
||||
|
||||
if hash.is_err() {
|
||||
return Err("Hashing error");
|
||||
}
|
||||
|
||||
let hash = hash.unwrap();
|
||||
|
||||
let hash: Vec<u8> = hash.hash.unwrap().as_bytes().to_owned();
|
||||
Ok(hash)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::nonce_offset;
|
||||
|
||||
#[test]
|
||||
fn nonce_offset_increment() {
|
||||
let nonce: Vec<u8> = 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,
|
||||
"\ninitial = {:x?}\nnew = {:x?}",
|
||||
nonce,
|
||||
nonce_off
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nonce_offset_increment_carry() {
|
||||
let nonce: Vec<u8> = vec![0xfe; 12];
|
||||
|
||||
let nonce_off = nonce_offset(&nonce, 0x2);
|
||||
|
||||
assert!(
|
||||
nonce_off[11] == 0x00
|
||||
&& nonce_off[10] == 0xff
|
||||
&& nonce_off[9] == 0xfe
|
||||
&& nonce_off[8] == 0xfe
|
||||
&& nonce_off[7] == 0xfe
|
||||
&& nonce_off[6] == 0xfe,
|
||||
"\ninitial = {:x?}\nnew = {:x?}",
|
||||
nonce,
|
||||
nonce_off
|
||||
);
|
||||
}
|
||||
}
|
176
src/data/mod.rs
Normal file
176
src/data/mod.rs
Normal file
|
@ -0,0 +1,176 @@
|
|||
use std::io::{Read, Seek, SeekFrom, Write};
|
||||
|
||||
pub trait DataPager {
|
||||
fn get_page(&mut self, offset: usize) -> Result<&mut DataPage, String>;
|
||||
|
||||
fn update_page(&mut self, page: WritableDataPage) -> Result<u64, ()>;
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Hash)]
|
||||
pub struct DataPage {
|
||||
offset: usize,
|
||||
pos: usize,
|
||||
data: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Read for DataPage {
|
||||
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||
let range = self.pos..(self.pos + buf.len()).clamp(0, self.data.len() - buf.len());
|
||||
let size = range.len();
|
||||
buf.copy_from_slice(&self.data[range]);
|
||||
self.pos += size;
|
||||
Ok(size)
|
||||
}
|
||||
}
|
||||
|
||||
impl Seek for DataPage {
|
||||
fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
|
||||
let cur = self.pos;
|
||||
|
||||
let newpos = match pos {
|
||||
SeekFrom::Start(x) => {
|
||||
if x > self.data.len() as u64 {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"Seek position out of buffer bounds.",
|
||||
));
|
||||
}
|
||||
x
|
||||
}
|
||||
SeekFrom::End(x) => {
|
||||
if x > self.data.len() as i64 {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"Seek position out of buffer bounds.",
|
||||
));
|
||||
}
|
||||
|
||||
(self.data.len() as i64 - x) as u64
|
||||
}
|
||||
SeekFrom::Current(x) => {
|
||||
let new = self.pos as i64 + x;
|
||||
|
||||
if new < 0 || new > self.data.len() as i64 {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"Seek position out of buffer bounds.",
|
||||
));
|
||||
}
|
||||
|
||||
new as u64
|
||||
}
|
||||
};
|
||||
|
||||
self.pos = newpos as usize;
|
||||
|
||||
Ok(cur as u64)
|
||||
}
|
||||
}
|
||||
|
||||
impl DataPage {
|
||||
pub(crate) fn load(offset: usize, data: Vec<u8>) -> Self {
|
||||
DataPage {
|
||||
offset,
|
||||
pos: 0,
|
||||
data,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.data.len()
|
||||
}
|
||||
|
||||
pub fn buffer(&self) -> &[u8] {
|
||||
&self.data
|
||||
}
|
||||
|
||||
pub fn offset(&self) -> usize {
|
||||
self.offset
|
||||
}
|
||||
|
||||
pub fn mutate(&mut self) -> WritableDataPage {
|
||||
WritableDataPage { page: self }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WritableDataPage<'a> {
|
||||
page: &'a mut DataPage,
|
||||
}
|
||||
|
||||
impl Write for WritableDataPage<'_> {
|
||||
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||
let pos = self.page.pos;
|
||||
|
||||
let len = buf.len().clamp(0, self.page.data.len() - pos);
|
||||
|
||||
//Is this neccessary?
|
||||
if len == 0 || buf.len() == 0 {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
self.page.data[pos..pos + len].copy_from_slice(&buf);
|
||||
|
||||
Ok(len)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> std::io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl WritableDataPage<'_> {
|
||||
pub fn data(&self) -> &[u8] {
|
||||
self.page.buffer()
|
||||
}
|
||||
|
||||
pub fn offset(&self) -> usize {
|
||||
self.page.offset()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::DataPage;
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
|
||||
fn make_page() -> DataPage {
|
||||
DataPage {
|
||||
offset: 0,
|
||||
data: vec![0; 128],
|
||||
pos: 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn datapage_seek() {
|
||||
let mut stream = make_page();
|
||||
|
||||
{
|
||||
let pos = stream.seek(SeekFrom::Start(64));
|
||||
assert!(pos.is_ok());
|
||||
assert_eq!(pos.unwrap(), 0);
|
||||
}
|
||||
|
||||
{
|
||||
let pos = stream.seek(SeekFrom::Start(128));
|
||||
assert!(pos.is_ok());
|
||||
assert_eq!(pos.unwrap(), 64);
|
||||
}
|
||||
|
||||
{
|
||||
let pos = stream.seek(SeekFrom::Start(256));
|
||||
assert!(pos.is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn datapage_read() {
|
||||
let mut stream = make_page();
|
||||
let mut buf: [u8; 8] = [1; 8];
|
||||
|
||||
assert!(stream.read_exact(&mut buf).is_ok());
|
||||
|
||||
assert_eq!(&buf, &[0; 8]);
|
||||
assert_eq!(stream.stream_position().unwrap(), 8);
|
||||
}
|
||||
}
|
|
@ -2,6 +2,8 @@ use config::try_load_preferences;
|
|||
use store::Store;
|
||||
|
||||
mod config;
|
||||
mod crypto;
|
||||
mod data;
|
||||
mod store;
|
||||
|
||||
fn main() {
|
||||
|
@ -13,7 +15,7 @@ fn main() {
|
|||
|
||||
let _preferences = try_load_preferences();
|
||||
|
||||
let store = match store::local::load(
|
||||
let mut store = match store::local::load(
|
||||
"/tmp/store.db".to_string(),
|
||||
"meowmeow".as_bytes().to_owned(),
|
||||
) {
|
||||
|
|
|
@ -1,905 +0,0 @@
|
|||
use std::{
|
||||
arch::x86_64::_mm_crc32_u32,
|
||||
collections::{hash_map::DefaultHasher, HashMap},
|
||||
fs::File,
|
||||
hash::{Hash, Hasher},
|
||||
io::{BufReader, Read, Seek, SeekFrom, Write},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use aes_gcm::{aead::Aead, Aes256Gcm, KeyInit, Nonce};
|
||||
use argon2::PasswordHasher;
|
||||
use password_hash::SaltString;
|
||||
use rand::{rngs::StdRng, RngCore, SeedableRng};
|
||||
|
||||
use super::{EncryptionStream, EncryptionType, HashType, RecordType, Store, StoreRecord};
|
||||
|
||||
pub struct LocalStore {
|
||||
path: String,
|
||||
header: LocalStoreHeader,
|
||||
meta: LocalStoreMeta,
|
||||
enc_ctx: EncryptionContext,
|
||||
cache: HashMap<String, LocalRecord>,
|
||||
}
|
||||
|
||||
/*
|
||||
Store binary format:
|
||||
Header: 0x00 (first 32 bytes):
|
||||
0x00 - magic number
|
||||
0x04 - last access timestamp
|
||||
0x08 - last write timestamp
|
||||
0x0c - encryption type byte
|
||||
0x0d - hash type byte
|
||||
0x0e - superblock size
|
||||
0x10 - cipher key size
|
||||
0x12 - encrypted block offset
|
||||
0x16 - header CRC-32 checksum
|
||||
0x1a - padding
|
||||
Enc details: 16 bytes
|
||||
0x20 - salt
|
||||
0x30 - master IV
|
||||
0x3c - 4 byte padding with 0
|
||||
---Encrypted section starts here---
|
||||
--Block 0
|
||||
Superblock:
|
||||
Hash:
|
||||
0x40 - master password hash
|
||||
Metadata:
|
||||
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;
|
||||
|
||||
struct LocalStoreHeader {
|
||||
magic_h: u32,
|
||||
last_access: u32,
|
||||
last_write: u32,
|
||||
enc_type: EncryptionType,
|
||||
hash_type: HashType,
|
||||
enc_sup_block_size: u16,
|
||||
enc_key_size: u16,
|
||||
encrypted_block_offset: u32,
|
||||
chksum_crc: u32,
|
||||
}
|
||||
|
||||
impl LocalStoreHeader {
|
||||
fn get_checksum(&self) -> u32 {
|
||||
let mut crc: u32 = 0xffffffff;
|
||||
unsafe {
|
||||
crc = _mm_crc32_u32(crc, self.last_access);
|
||||
crc = _mm_crc32_u32(crc, self.last_write);
|
||||
crc = _mm_crc32_u32(crc, self.enc_type as u32);
|
||||
crc = _mm_crc32_u32(crc, self.hash_type as u32);
|
||||
crc = _mm_crc32_u32(crc, self.enc_sup_block_size as u32);
|
||||
crc = _mm_crc32_u32(crc, self.enc_key_size as u32);
|
||||
crc = _mm_crc32_u32(crc, self.encrypted_block_offset);
|
||||
}
|
||||
crc
|
||||
}
|
||||
|
||||
fn is_checksum_valid(&self) -> bool {
|
||||
let chk = self.get_checksum();
|
||||
chk == self.chksum_crc
|
||||
}
|
||||
}
|
||||
|
||||
struct LocalStoreMeta {
|
||||
record_count: u32,
|
||||
specifier_count: u32,
|
||||
index_node_arity: u16,
|
||||
data_block_size: u32,
|
||||
data_offset: u64,
|
||||
}
|
||||
|
||||
struct EncryptionContext {
|
||||
enc_type: EncryptionType,
|
||||
hash_type: HashType,
|
||||
key: Vec<u8>,
|
||||
salt: SaltString,
|
||||
iv_root: Vec<u8>,
|
||||
}
|
||||
|
||||
fn try_deserialize_header(file: &mut BufReader<File>) -> Result<LocalStoreHeader, &'static str> {
|
||||
let mut header: LocalStoreHeader = LocalStoreHeader {
|
||||
magic_h: 0,
|
||||
last_access: 0,
|
||||
last_write: 0,
|
||||
enc_type: EncryptionType::Invalid,
|
||||
hash_type: HashType::Invalid,
|
||||
enc_sup_block_size: 0,
|
||||
enc_key_size: 0,
|
||||
chksum_crc: 0,
|
||||
encrypted_block_offset: 32,
|
||||
};
|
||||
|
||||
let mut buf: [u8; HEADER_SIZE] = [0; HEADER_SIZE];
|
||||
|
||||
if file.read_exact(&mut buf).is_err() {
|
||||
return Err("Read error.");
|
||||
}
|
||||
|
||||
header.magic_h = u32::from_be_bytes(buf[0..4].try_into().unwrap());
|
||||
if header.magic_h != MAGIC_NUM {
|
||||
return Err("Invalid header magic number.");
|
||||
}
|
||||
|
||||
header.last_access = u32::from_be_bytes(buf[4..8].try_into().unwrap());
|
||||
header.last_write = u32::from_be_bytes(buf[8..12].try_into().unwrap());
|
||||
header.enc_type = EncryptionType::from(buf[12]);
|
||||
header.hash_type = HashType::from(buf[13]);
|
||||
header.enc_sup_block_size = u16::from_be_bytes(buf[14..16].try_into().unwrap());
|
||||
header.enc_key_size = u16::from_be_bytes(buf[16..18].try_into().unwrap());
|
||||
header.encrypted_block_offset = u32::from_be_bytes(buf[18..22].try_into().unwrap());
|
||||
header.chksum_crc = u32::from_be_bytes(buf[22..26].try_into().unwrap());
|
||||
|
||||
if !header.is_checksum_valid() {
|
||||
return Err("Invalid checksum.");
|
||||
}
|
||||
|
||||
Ok(header)
|
||||
}
|
||||
|
||||
fn try_deserialize_superblock(
|
||||
file: &mut BufReader<File>,
|
||||
enc: &EncryptionContext,
|
||||
header: &LocalStoreHeader,
|
||||
) -> Result<LocalStoreMeta, &'static str> {
|
||||
let data = enc
|
||||
.decrypt_block_from_file(header.enc_sup_block_size as usize, 0, file)
|
||||
.unwrap();
|
||||
|
||||
if data.data[0..enc.key.len()].ne(&enc.key) {
|
||||
return Err("Key mismatch.");
|
||||
}
|
||||
|
||||
let meta = deserialize_store_meta(&&data.data[32..]);
|
||||
|
||||
Ok(meta)
|
||||
}
|
||||
|
||||
fn serialize_header(header: &LocalStoreHeader) -> Vec<u8> {
|
||||
let mut buf = Vec::<u8>::new();
|
||||
buf.append(u32::to_be_bytes(MAGIC_NUM).to_vec().as_mut());
|
||||
buf.append(u32::to_be_bytes(header.last_access).to_vec().as_mut());
|
||||
buf.append(u32::to_be_bytes(header.last_write).to_vec().as_mut());
|
||||
buf.push(header.enc_type as u8);
|
||||
buf.push(header.hash_type as u8);
|
||||
buf.append(
|
||||
u16::to_be_bytes(header.enc_sup_block_size)
|
||||
.to_vec()
|
||||
.as_mut(),
|
||||
);
|
||||
buf.append(u16::to_be_bytes(header.enc_key_size).to_vec().as_mut());
|
||||
buf.append(
|
||||
u32::to_be_bytes(header.encrypted_block_offset)
|
||||
.to_vec()
|
||||
.as_mut(),
|
||||
);
|
||||
buf.append(u32::to_be_bytes(header.chksum_crc).to_vec().as_mut());
|
||||
|
||||
let padding_len = 32 - buf.len();
|
||||
let mut pad: Vec<u8> = vec![0; padding_len];
|
||||
buf.append(&mut pad);
|
||||
|
||||
return buf;
|
||||
}
|
||||
|
||||
fn try_create_store(path: &String, passphrase: &Vec<u8>) -> bool {
|
||||
let timestamp: u32 = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs() as u32;
|
||||
|
||||
let mut rng = StdRng::from_entropy();
|
||||
|
||||
let salt = SaltString::generate(&mut rng);
|
||||
|
||||
let (default_hash_type, default_enc_type) = (HashType::Argon2, EncryptionType::AesGcm);
|
||||
|
||||
let key = derive_key(&passphrase, &salt, default_hash_type).unwrap();
|
||||
|
||||
let mut iv: [u8; 12] = [0; 12];
|
||||
rng.fill_bytes(&mut iv);
|
||||
|
||||
let mut salt_buf: [u8; 16] = [0; 16];
|
||||
salt.b64_decode(&mut salt_buf).unwrap();
|
||||
|
||||
let enc = EncryptionContext {
|
||||
enc_type: default_enc_type,
|
||||
hash_type: default_hash_type,
|
||||
key,
|
||||
salt,
|
||||
iv_root: iv.to_vec(),
|
||||
};
|
||||
|
||||
let mut superblock_buf: Vec<u8> = Vec::new();
|
||||
|
||||
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 meta = LocalStoreMeta {
|
||||
record_count: 0,
|
||||
specifier_count: 0,
|
||||
index_node_arity: 16,
|
||||
data_block_size: 8092,
|
||||
data_offset: 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() - 16) as u16,
|
||||
enc_key_size: 256,
|
||||
hash_type: enc.hash_type,
|
||||
encrypted_block_offset: 32,
|
||||
chksum_crc: 0,
|
||||
};
|
||||
|
||||
header.chksum_crc = header.get_checksum();
|
||||
|
||||
let mut index_block: Vec<u8> = vec![0; meta.data_block_size as usize];
|
||||
let root_node_buf = IndexNode {
|
||||
node_type: IndexNodeType::Leaf,
|
||||
children: Vec::new(),
|
||||
}
|
||||
.serialize(&meta);
|
||||
|
||||
index_block[0..root_node_buf.len()].copy_from_slice(&root_node_buf);
|
||||
|
||||
let mut buf = serialize_header(&header);
|
||||
|
||||
buf.extend_from_slice(&salt_buf);
|
||||
buf.extend_from_slice(&iv);
|
||||
buf.append([b'\0'].repeat(16 - iv.len()).as_mut());
|
||||
buf.extend_from_slice(&enc_buf.data);
|
||||
buf.extend_from_slice(&enc.encrypt_block(&index_block, 0x90).unwrap().data);
|
||||
|
||||
match std::fs::File::create(path).unwrap().write(&buf) {
|
||||
Ok(x) => return x == buf.len(),
|
||||
Err(_) => return false,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_enc_salt_iv(file: &mut BufReader<File>) -> Result<(SaltString, Vec<u8>), &'static str> {
|
||||
let mut buf: [u8; 32] = [0; 32];
|
||||
|
||||
file.read_exact(&mut buf).unwrap();
|
||||
|
||||
let salt = match SaltString::b64_encode(&buf[..16]) {
|
||||
Err(_) => return Err("Invalid salt."),
|
||||
Ok(x) => x,
|
||||
};
|
||||
|
||||
let iv = buf[16..28].to_owned();
|
||||
Ok((salt, iv))
|
||||
}
|
||||
|
||||
fn derive_key(
|
||||
password: &Vec<u8>,
|
||||
salt: &SaltString,
|
||||
hash_type: HashType,
|
||||
) -> Result<Vec<u8>, &'static str> {
|
||||
let hash = match hash_type {
|
||||
HashType::Invalid => unreachable!(),
|
||||
HashType::Argon2 => {
|
||||
let argon = argon2::Argon2::default();
|
||||
argon.hash_password(password, salt)
|
||||
}
|
||||
HashType::Bcrypt => {
|
||||
todo!()
|
||||
}
|
||||
HashType::Pbkdf2 => {
|
||||
todo!()
|
||||
}
|
||||
};
|
||||
|
||||
if hash.is_err() {
|
||||
return Err("Hashing error");
|
||||
}
|
||||
|
||||
let hash = hash.unwrap();
|
||||
|
||||
let hash: Vec<u8> = hash.hash.unwrap().as_bytes().to_owned();
|
||||
Ok(hash)
|
||||
}
|
||||
|
||||
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());
|
||||
let index_node_arity = u16::from_be_bytes(data[8..10].try_into().unwrap());
|
||||
let data_block_size = u32::from_be_bytes(data[10..14].try_into().unwrap());
|
||||
let data_offset = u64::from_be_bytes(data[14..22].try_into().unwrap());
|
||||
|
||||
LocalStoreMeta {
|
||||
record_count: records,
|
||||
specifier_count: specifiers,
|
||||
index_node_arity,
|
||||
data_block_size,
|
||||
data_offset,
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize_store_meta(meta: &LocalStoreMeta) -> Vec<u8> {
|
||||
let mut buf: Vec<u8> = 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(u16::to_be_bytes(meta.index_node_arity).to_vec().as_mut());
|
||||
buf.append(u32::to_be_bytes(meta.data_block_size).to_vec().as_mut());
|
||||
buf.append(u64::to_be_bytes(meta.data_offset).to_vec().as_mut());
|
||||
buf.append([b'\0'].repeat(32 - buf.len()).as_mut());
|
||||
|
||||
buf
|
||||
}
|
||||
|
||||
pub fn load(path: String, passphrase: Vec<u8>) -> Result<LocalStore, &'static str> {
|
||||
let path_p = Path::new(&path);
|
||||
|
||||
if !path_p.exists() {
|
||||
if !try_create_store(&path, &passphrase) {
|
||||
return Err("Cannot create store.");
|
||||
}
|
||||
} else if !path_p.is_file() {
|
||||
return Err("Invalid path.");
|
||||
}
|
||||
|
||||
let file = match File::open(&path_p) {
|
||||
Ok(x) => x,
|
||||
Err(_) => return Err("Cannot open store."),
|
||||
};
|
||||
|
||||
let mut reader = BufReader::new(file);
|
||||
|
||||
let header = match try_deserialize_header(&mut reader) {
|
||||
Err(x) => return Err(x),
|
||||
Ok(meta) => meta,
|
||||
};
|
||||
|
||||
let (salt, iv) = match get_enc_salt_iv(&mut reader) {
|
||||
Ok(s) => s,
|
||||
Err(x) => return Err(x),
|
||||
};
|
||||
|
||||
let key = match derive_key(&passphrase, &salt, header.hash_type) {
|
||||
Ok(x) => x,
|
||||
Err(x) => return Err(x),
|
||||
};
|
||||
|
||||
let enc_ctx = EncryptionContext {
|
||||
enc_type: header.enc_type,
|
||||
hash_type: header.hash_type,
|
||||
key,
|
||||
salt,
|
||||
iv_root: iv,
|
||||
};
|
||||
|
||||
let meta = match try_deserialize_superblock(&mut reader, &enc_ctx, &header) {
|
||||
Ok(d) => d,
|
||||
Err(x) => return Err(x),
|
||||
};
|
||||
|
||||
let store = LocalStore {
|
||||
path,
|
||||
header,
|
||||
enc_ctx,
|
||||
meta,
|
||||
cache: HashMap::new(),
|
||||
};
|
||||
|
||||
return Ok(store);
|
||||
}
|
||||
|
||||
struct LocalRecord {
|
||||
record: StoreRecord,
|
||||
position: usize,
|
||||
size: usize,
|
||||
}
|
||||
|
||||
const CRED_FLAG_METADATA: u8 = 0b0000100;
|
||||
|
||||
impl LocalRecord {
|
||||
fn serialize(&self) -> Vec<u8> {
|
||||
let mut buf = Vec::<u8>::new();
|
||||
|
||||
buf.push(self.record.r#type as u8);
|
||||
|
||||
let mut flags: u8 = 0;
|
||||
|
||||
if self.record.meta.is_some() {
|
||||
flags |= CRED_FLAG_METADATA;
|
||||
}
|
||||
buf.push(flags);
|
||||
|
||||
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()
|
||||
};
|
||||
|
||||
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());
|
||||
|
||||
buf.extend(spec_bytes);
|
||||
buf.extend(key_bytes);
|
||||
buf.extend(value_bytes);
|
||||
|
||||
if meta_len > 0 {
|
||||
buf.extend(self.record.meta.as_ref().unwrap().bytes());
|
||||
}
|
||||
|
||||
buf
|
||||
}
|
||||
|
||||
fn from_block(bytes: &[u8]) -> Result<LocalRecord, &'static str> {
|
||||
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;
|
||||
|
||||
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,
|
||||
},
|
||||
position: 0,
|
||||
size: val_offset + val_len + meta_len,
|
||||
};
|
||||
|
||||
Ok(record)
|
||||
}
|
||||
}
|
||||
|
||||
impl Store for LocalStore {
|
||||
fn get_creds(&self, specifier: &String, key: &String) -> Option<StoreRecord> {
|
||||
match self.btree_find_key(&key) {
|
||||
(Some(x), _) => {
|
||||
println!("Found: {}", x);
|
||||
todo!()
|
||||
}
|
||||
(None, _) => return None,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_creds_by_specifier(&self, specifier: &String) -> Vec<StoreRecord> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn store_creds(
|
||||
&self,
|
||||
specifier: &String,
|
||||
key: &String,
|
||||
value: &Vec<u8>,
|
||||
r#type: RecordType,
|
||||
meta: Option<String>,
|
||||
) {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
|
||||
enum IndexNodeType {
|
||||
Internal = 1,
|
||||
Leaf = 2,
|
||||
Invalid = 0,
|
||||
}
|
||||
|
||||
impl From<u8> for IndexNodeType {
|
||||
fn from(value: u8) -> Self {
|
||||
match value {
|
||||
0 => IndexNodeType::Invalid,
|
||||
1 => IndexNodeType::Internal,
|
||||
2 => IndexNodeType::Leaf,
|
||||
_ => panic!("Invalid value {}", value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct IndexNodeEntry {
|
||||
key: u64,
|
||||
pointer: u64,
|
||||
}
|
||||
|
||||
struct IndexNode {
|
||||
node_type: IndexNodeType,
|
||||
children: Vec<IndexNodeEntry>,
|
||||
}
|
||||
|
||||
impl IndexNode {
|
||||
fn children(&self) -> &Vec<IndexNodeEntry> {
|
||||
&self.children
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
self.children.len()
|
||||
}
|
||||
|
||||
fn serialize(&self, meta: &LocalStoreMeta) -> Vec<u8> {
|
||||
let mut buf: Vec<u8> = vec![0; 16 * meta.index_node_arity as usize + 2];
|
||||
|
||||
let mut cnt = 2;
|
||||
for child in self.children() {
|
||||
buf[cnt..(cnt + 8)].copy_from_slice(&child.key.to_be_bytes());
|
||||
buf[(cnt + 8)..(cnt + 16)].copy_from_slice(&child.pointer.to_be_bytes());
|
||||
cnt += 16;
|
||||
}
|
||||
|
||||
buf[0] = self.node_type as u8;
|
||||
buf
|
||||
}
|
||||
}
|
||||
|
||||
impl LocalStore {
|
||||
fn read_node(&self, data: &mut EncryptionStream) -> IndexNode {
|
||||
let node_type = {
|
||||
let mut buf: [u8; 2] = [0; 2];
|
||||
data.read_exact(&mut buf).expect("Cannot read node type.");
|
||||
IndexNodeType::from(buf[0])
|
||||
};
|
||||
|
||||
let mut children = Vec::with_capacity(self.meta.index_node_arity as usize);
|
||||
for _ in 0..self.meta.index_node_arity {
|
||||
let mut buf: [u8; 16] = [0; 16];
|
||||
|
||||
data.read_exact(&mut buf)
|
||||
.expect("Cannot read child node content.");
|
||||
|
||||
let key = u64::from_be_bytes(buf[0..8].try_into().unwrap());
|
||||
|
||||
if key == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
let pointer = u64::from_be_bytes(buf[0..8].try_into().unwrap());
|
||||
|
||||
let entry = IndexNodeEntry { key, pointer };
|
||||
|
||||
children.push(entry);
|
||||
}
|
||||
|
||||
let node = IndexNode {
|
||||
node_type,
|
||||
children,
|
||||
};
|
||||
|
||||
node
|
||||
}
|
||||
|
||||
fn btree_find_key(&self, key: &String) -> (Option<u64>, Vec<u64>) {
|
||||
let mut file: BufReader<File> =
|
||||
BufReader::new(File::open(&self.path).expect("Cannot open file"));
|
||||
|
||||
let hash = {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
key.hash(&mut hasher);
|
||||
hasher.finish()
|
||||
};
|
||||
|
||||
let mut traversal_history = Vec::new();
|
||||
|
||||
let pos = file.seek(SeekFrom::Start(0x90)).unwrap();
|
||||
let mut block = self
|
||||
.enc_ctx
|
||||
.decrypt_block_from_file(self.meta.data_block_size as usize, 0x90, &mut file)
|
||||
.unwrap();
|
||||
|
||||
loop {
|
||||
traversal_history.push(block.stream_position().unwrap());
|
||||
let node = self.read_node(&mut block);
|
||||
|
||||
if node.node_type == IndexNodeType::Invalid {
|
||||
panic!(
|
||||
"Invalid node detected: {}",
|
||||
block.stream_position().unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
if node.node_type == IndexNodeType::Leaf {
|
||||
for child in node.children() {
|
||||
if child.key == hash {
|
||||
return (Some(child.pointer), traversal_history);
|
||||
}
|
||||
}
|
||||
return (None, traversal_history);
|
||||
}
|
||||
|
||||
for child in node.children {
|
||||
if child.key < hash {
|
||||
continue;
|
||||
}
|
||||
|
||||
block.seek(SeekFrom::Start(child.pointer)).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn nonce_offset(nonce_root: &Vec<u8>, offset: usize) -> Vec<u8> {
|
||||
let len = nonce_root.len();
|
||||
if len < (usize::BITS / 8) as usize {
|
||||
panic!("Nonce length less than {}", usize::BITS / 8);
|
||||
}
|
||||
let start = len - (usize::BITS / 8) as usize;
|
||||
|
||||
let mut nonce_copy = vec![0; len];
|
||||
nonce_copy.clone_from_slice(&nonce_root);
|
||||
|
||||
let mut num = u64::from_be_bytes(nonce_copy[start..len].try_into().unwrap());
|
||||
num += offset as u64;
|
||||
nonce_copy[start..len].copy_from_slice(&num.to_be_bytes());
|
||||
|
||||
nonce_copy
|
||||
}
|
||||
|
||||
impl EncryptionContext {
|
||||
fn decrypt_block(
|
||||
&self,
|
||||
ciphertext: &Vec<u8>,
|
||||
offset: usize,
|
||||
) -> Result<EncryptionStream, String> {
|
||||
let plaintext = match self.enc_type {
|
||||
EncryptionType::Invalid => unreachable!(),
|
||||
EncryptionType::AesGcm => {
|
||||
let aes_key = aes_gcm::aead::generic_array::GenericArray::from_slice(&self.key);
|
||||
let aes = Aes256Gcm::new(aes_key);
|
||||
|
||||
let nonce = nonce_offset(&self.iv_root, offset);
|
||||
let nonce = Nonce::from_slice(&nonce);
|
||||
|
||||
aes.decrypt(nonce, ciphertext.as_ref())
|
||||
}
|
||||
EncryptionType::Chacha20Poly1305 => todo!(),
|
||||
};
|
||||
|
||||
if plaintext.is_err() {
|
||||
return Err(format!("Decryption error: {}", plaintext.unwrap_err()));
|
||||
}
|
||||
|
||||
Ok(EncryptionStream {
|
||||
data: plaintext.unwrap(),
|
||||
pos: 0,
|
||||
})
|
||||
}
|
||||
|
||||
fn encrypt_block(
|
||||
&self,
|
||||
plaintext: &Vec<u8>,
|
||||
offset: usize,
|
||||
) -> Result<EncryptionStream, &'static str> {
|
||||
let ciphertext = match self.enc_type {
|
||||
EncryptionType::Invalid => unreachable!(),
|
||||
EncryptionType::AesGcm => {
|
||||
let aes_key = aes_gcm::aead::generic_array::GenericArray::from_slice(&self.key);
|
||||
let aes = Aes256Gcm::new(aes_key);
|
||||
|
||||
let nonce = nonce_offset(&self.iv_root, offset);
|
||||
let nonce = Nonce::from_slice(&nonce);
|
||||
|
||||
aes.encrypt(nonce, plaintext.as_ref())
|
||||
}
|
||||
EncryptionType::Chacha20Poly1305 => todo!(),
|
||||
};
|
||||
|
||||
if ciphertext.is_err() {
|
||||
return Err("Encryption error");
|
||||
}
|
||||
|
||||
Ok(EncryptionStream {
|
||||
data: ciphertext.unwrap(),
|
||||
pos: 0,
|
||||
})
|
||||
}
|
||||
|
||||
fn decrypt_block_from_file(
|
||||
&self,
|
||||
len: usize,
|
||||
offset: usize,
|
||||
file: &mut BufReader<File>,
|
||||
) -> Result<EncryptionStream, String> {
|
||||
let mut buf: Vec<u8> = vec![0; len + 16];
|
||||
match file.read_exact(&mut buf) {
|
||||
Err(err) => Err(format!("Read error: {}", err.to_string())),
|
||||
Ok(_) => self.decrypt_block(&buf, offset),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::mem::size_of;
|
||||
|
||||
use crate::store::{
|
||||
local::{nonce_offset, LocalStoreHeader, HEADER_SIZE},
|
||||
StoreRecord,
|
||||
};
|
||||
|
||||
use super::{IndexNode, IndexNodeEntry, IndexNodeType, LocalRecord};
|
||||
|
||||
#[test]
|
||||
fn header_size_const_greater_than_realsize() {
|
||||
let c = HEADER_SIZE;
|
||||
let r = size_of::<LocalStoreHeader>();
|
||||
|
||||
assert!(c > r, "const = {}, real = {}", c, r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nonce_offset_increment() {
|
||||
{
|
||||
let nonce: Vec<u8> = 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,
|
||||
"\ninitial = {:x?}\nnew = {:x?}",
|
||||
nonce,
|
||||
nonce_off
|
||||
);
|
||||
}
|
||||
|
||||
//Carry test
|
||||
{
|
||||
let nonce: Vec<u8> = vec![0xfe; 12];
|
||||
|
||||
let nonce_off = nonce_offset(&nonce, 0x2);
|
||||
|
||||
assert!(
|
||||
nonce_off[11] == 0x00
|
||||
&& nonce_off[10] == 0xff
|
||||
&& nonce_off[9] == 0xfe
|
||||
&& nonce_off[8] == 0xfe
|
||||
&& nonce_off[7] == 0xfe
|
||||
&& nonce_off[6] == 0xfe,
|
||||
"\ninitial = {:x?}\nnew = {:x?}",
|
||||
nonce,
|
||||
nonce_off
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn localstore_record_serialize() {
|
||||
let record = LocalRecord {
|
||||
position: 0,
|
||||
size: 0,
|
||||
record: StoreRecord {
|
||||
key: "meow".to_owned(),
|
||||
meta: None,
|
||||
specifier: "test".to_owned(),
|
||||
r#type: crate::store::RecordType::LoginPassword,
|
||||
value: vec![1, 2, 3, 4],
|
||||
},
|
||||
};
|
||||
|
||||
let serialized = record.serialize();
|
||||
|
||||
println!("D: {:x?}", serialized);
|
||||
|
||||
let deserialized = LocalRecord::from_block(&serialized).unwrap();
|
||||
|
||||
assert_eq!(deserialized.record, record.record);
|
||||
|
||||
let reserialized = deserialized.serialize();
|
||||
|
||||
assert_eq!(serialized, reserialized);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn index_node_serialize() {
|
||||
let root = IndexNode {
|
||||
node_type: IndexNodeType::Internal,
|
||||
children: vec![IndexNodeEntry {
|
||||
key: 1,
|
||||
pointer: 258,
|
||||
}],
|
||||
};
|
||||
|
||||
let leaf = IndexNode {
|
||||
node_type: IndexNodeType::Leaf,
|
||||
children: vec![
|
||||
IndexNodeEntry {
|
||||
key: 1,
|
||||
pointer: 516,
|
||||
},
|
||||
IndexNodeEntry {
|
||||
key: 4,
|
||||
pointer: 520,
|
||||
},
|
||||
IndexNodeEntry {
|
||||
key: 16,
|
||||
pointer: 524,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
let meta = super::LocalStoreMeta {
|
||||
record_count: 3,
|
||||
specifier_count: 0,
|
||||
index_node_arity: 16,
|
||||
data_block_size: 8192,
|
||||
data_offset: 0,
|
||||
};
|
||||
|
||||
let root_bytes = root.serialize(&meta);
|
||||
let leaf_bytes = leaf.serialize(&meta);
|
||||
|
||||
assert_eq!(IndexNodeType::from(root_bytes[0]), root.node_type);
|
||||
assert_eq!(IndexNodeType::from(leaf_bytes[0]), leaf.node_type);
|
||||
|
||||
assert_eq!(root_bytes[2..10], root.children[0].key.to_be_bytes());
|
||||
assert_eq!(root_bytes[10..18], root.children[0].pointer.to_be_bytes());
|
||||
assert!(root_bytes.iter().skip(18).all(|x| *x == 0u8));
|
||||
|
||||
let mut cnt = 2;
|
||||
for child in leaf.children {
|
||||
assert_eq!(leaf_bytes[cnt..(cnt + 8)], child.key.to_be_bytes());
|
||||
assert_eq!(
|
||||
leaf_bytes[(cnt + 8)..(cnt + 16)],
|
||||
child.pointer.to_be_bytes()
|
||||
);
|
||||
cnt += 16;
|
||||
}
|
||||
assert!(root_bytes.iter().skip(cnt).all(|x| *x == 0u8));
|
||||
}
|
||||
}
|
135
src/store/local/data.rs
Normal file
135
src/store/local/data.rs
Normal file
|
@ -0,0 +1,135 @@
|
|||
use crate::store::{RecordType, StoreRecord};
|
||||
|
||||
pub(super) struct LocalRecord {
|
||||
record: StoreRecord,
|
||||
position: usize,
|
||||
size: usize,
|
||||
}
|
||||
|
||||
const CRED_FLAG_METADATA: u8 = 0b0000100;
|
||||
|
||||
impl LocalRecord {
|
||||
fn serialize(&self) -> Vec<u8> {
|
||||
let mut buf = Vec::<u8>::new();
|
||||
|
||||
buf.push(self.record.r#type as u8);
|
||||
|
||||
let mut flags: u8 = 0;
|
||||
|
||||
if self.record.meta.is_some() {
|
||||
flags |= CRED_FLAG_METADATA;
|
||||
}
|
||||
buf.push(flags);
|
||||
|
||||
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()
|
||||
};
|
||||
|
||||
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());
|
||||
|
||||
buf.extend(spec_bytes);
|
||||
buf.extend(key_bytes);
|
||||
buf.extend(value_bytes);
|
||||
|
||||
if meta_len > 0 {
|
||||
buf.extend(self.record.meta.as_ref().unwrap().bytes());
|
||||
}
|
||||
|
||||
buf
|
||||
}
|
||||
|
||||
fn from_block(bytes: &[u8]) -> Result<LocalRecord, &'static str> {
|
||||
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;
|
||||
|
||||
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,
|
||||
},
|
||||
position: 0,
|
||||
size: val_offset + val_len + meta_len,
|
||||
};
|
||||
|
||||
Ok(record)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::LocalRecord;
|
||||
use crate::store::StoreRecord;
|
||||
|
||||
#[test]
|
||||
fn localstore_record_serialize() {
|
||||
let record = LocalRecord {
|
||||
position: 0,
|
||||
size: 0,
|
||||
record: StoreRecord {
|
||||
key: "meow".to_owned(),
|
||||
meta: None,
|
||||
specifier: "test".to_owned(),
|
||||
r#type: crate::store::RecordType::LoginPassword,
|
||||
value: vec![1, 2, 3, 4],
|
||||
},
|
||||
};
|
||||
|
||||
let serialized = record.serialize();
|
||||
|
||||
println!("D: {:x?}", serialized);
|
||||
|
||||
let deserialized = LocalRecord::from_block(&serialized).unwrap();
|
||||
|
||||
assert_eq!(deserialized.record, record.record);
|
||||
|
||||
let reserialized = deserialized.serialize();
|
||||
|
||||
assert_eq!(serialized, reserialized);
|
||||
}
|
||||
}
|
85
src/store/local/header.rs
Normal file
85
src/store/local/header.rs
Normal file
|
@ -0,0 +1,85 @@
|
|||
use std::{
|
||||
arch::x86_64::_mm_crc32_u32,
|
||||
fs::File,
|
||||
io::{BufReader, Read},
|
||||
};
|
||||
|
||||
use crate::crypto::{EncryptionType, HashType};
|
||||
|
||||
pub const MAGIC_NUM: u32 = 0x6d656f77;
|
||||
pub const HEADER_SIZE: usize = 32;
|
||||
|
||||
pub(super) struct LocalStoreHeader {
|
||||
pub magic_h: u32,
|
||||
pub last_access: u32,
|
||||
pub last_write: u32,
|
||||
pub enc_type: EncryptionType,
|
||||
pub hash_type: HashType,
|
||||
pub enc_sup_block_size: u16,
|
||||
pub enc_key_size: u16,
|
||||
pub encrypted_block_offset: u32,
|
||||
pub chksum_crc: u32,
|
||||
}
|
||||
|
||||
impl LocalStoreHeader {
|
||||
pub fn get_checksum(&self) -> u32 {
|
||||
let mut crc: u32 = 0xffffffff;
|
||||
unsafe {
|
||||
crc = _mm_crc32_u32(crc, self.last_access);
|
||||
crc = _mm_crc32_u32(crc, self.last_write);
|
||||
crc = _mm_crc32_u32(crc, self.enc_type as u32);
|
||||
crc = _mm_crc32_u32(crc, self.hash_type as u32);
|
||||
crc = _mm_crc32_u32(crc, self.enc_sup_block_size as u32);
|
||||
crc = _mm_crc32_u32(crc, self.enc_key_size as u32);
|
||||
crc = _mm_crc32_u32(crc, self.encrypted_block_offset);
|
||||
}
|
||||
crc
|
||||
}
|
||||
|
||||
pub fn is_checksum_valid(&self) -> bool {
|
||||
let chk = self.get_checksum();
|
||||
chk == self.chksum_crc
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn try_deserialize_header(
|
||||
file: &mut BufReader<File>,
|
||||
) -> Result<LocalStoreHeader, &'static str> {
|
||||
let mut header: LocalStoreHeader = LocalStoreHeader {
|
||||
magic_h: 0,
|
||||
last_access: 0,
|
||||
last_write: 0,
|
||||
enc_type: EncryptionType::Invalid,
|
||||
hash_type: HashType::Invalid,
|
||||
enc_sup_block_size: 0,
|
||||
enc_key_size: 0,
|
||||
chksum_crc: 0,
|
||||
encrypted_block_offset: 32,
|
||||
};
|
||||
|
||||
let mut buf: [u8; HEADER_SIZE] = [0; HEADER_SIZE];
|
||||
|
||||
if file.read_exact(&mut buf).is_err() {
|
||||
return Err("Read error.");
|
||||
}
|
||||
|
||||
header.magic_h = u32::from_be_bytes(buf[0..4].try_into().unwrap());
|
||||
if header.magic_h != MAGIC_NUM {
|
||||
return Err("Invalid header magic number.");
|
||||
}
|
||||
|
||||
header.last_access = u32::from_be_bytes(buf[4..8].try_into().unwrap());
|
||||
header.last_write = u32::from_be_bytes(buf[8..12].try_into().unwrap());
|
||||
header.enc_type = EncryptionType::from(buf[12]);
|
||||
header.hash_type = HashType::from(buf[13]);
|
||||
header.enc_sup_block_size = u16::from_be_bytes(buf[14..16].try_into().unwrap());
|
||||
header.enc_key_size = u16::from_be_bytes(buf[16..18].try_into().unwrap());
|
||||
header.encrypted_block_offset = u32::from_be_bytes(buf[18..22].try_into().unwrap());
|
||||
header.chksum_crc = u32::from_be_bytes(buf[22..26].try_into().unwrap());
|
||||
|
||||
if !header.is_checksum_valid() {
|
||||
return Err("Invalid checksum.");
|
||||
}
|
||||
|
||||
Ok(header)
|
||||
}
|
258
src/store/local/index.rs
Normal file
258
src/store/local/index.rs
Normal file
|
@ -0,0 +1,258 @@
|
|||
use std::{
|
||||
collections::hash_map::DefaultHasher,
|
||||
hash::{Hash, Hasher},
|
||||
io::{Read, Seek, SeekFrom},
|
||||
};
|
||||
|
||||
use crate::data::{DataPage, DataPager};
|
||||
|
||||
use super::{LocalStore, LocalStoreMeta};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
|
||||
pub(super) enum IndexNodeType {
|
||||
Internal = 1,
|
||||
Leaf = 2,
|
||||
Invalid = 0,
|
||||
}
|
||||
|
||||
impl From<u8> for IndexNodeType {
|
||||
fn from(value: u8) -> Self {
|
||||
match value {
|
||||
0 => IndexNodeType::Invalid,
|
||||
1 => IndexNodeType::Internal,
|
||||
2 => IndexNodeType::Leaf,
|
||||
_ => panic!("Invalid value {}", value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct IndexNodeEntry {
|
||||
key: u64,
|
||||
pointer: u64,
|
||||
}
|
||||
|
||||
pub(super) struct IndexNode {
|
||||
node_type: IndexNodeType,
|
||||
children: Vec<IndexNodeEntry>,
|
||||
}
|
||||
|
||||
impl IndexNode {
|
||||
pub fn new() -> Self {
|
||||
IndexNode {
|
||||
node_type: IndexNodeType::Leaf,
|
||||
children: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn children(&self) -> &Vec<IndexNodeEntry> {
|
||||
&self.children
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.children.len()
|
||||
}
|
||||
|
||||
pub fn serialize(&self, meta: &LocalStoreMeta) -> Vec<u8> {
|
||||
let mut buf: Vec<u8> = vec![0; 16 * meta.index_node_arity as usize + 2];
|
||||
|
||||
let mut cnt = 2;
|
||||
for child in self.children() {
|
||||
buf[cnt..(cnt + 8)].copy_from_slice(&child.key.to_be_bytes());
|
||||
buf[(cnt + 8)..(cnt + 16)].copy_from_slice(&child.pointer.to_be_bytes());
|
||||
cnt += 16;
|
||||
}
|
||||
|
||||
buf[0] = self.node_type as u8;
|
||||
buf
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hash_key(key: &String) -> u64 {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
key.hash(&mut hasher);
|
||||
hasher.finish()
|
||||
}
|
||||
|
||||
pub(super) fn read_node(meta: &LocalStoreMeta, data: &mut DataPage) -> IndexNode {
|
||||
let node_type = {
|
||||
let mut buf: [u8; 2] = [0; 2];
|
||||
data.read_exact(&mut buf).expect("Cannot read node type.");
|
||||
IndexNodeType::from(buf[0])
|
||||
};
|
||||
|
||||
let mut children = Vec::with_capacity(meta.index_node_arity as usize);
|
||||
for _ in 0..meta.index_node_arity {
|
||||
let mut buf: [u8; 16] = [0; 16];
|
||||
|
||||
data.read_exact(&mut buf)
|
||||
.expect("Cannot read child node content.");
|
||||
|
||||
let key = u64::from_be_bytes(buf[0..8].try_into().unwrap());
|
||||
|
||||
if key == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
let pointer = u64::from_be_bytes(buf[0..8].try_into().unwrap());
|
||||
|
||||
let entry = IndexNodeEntry { key, pointer };
|
||||
|
||||
children.push(entry);
|
||||
}
|
||||
|
||||
let node = IndexNode {
|
||||
node_type,
|
||||
children,
|
||||
};
|
||||
|
||||
node
|
||||
}
|
||||
|
||||
impl LocalStore {
|
||||
pub(super) fn btree_find_key(&mut self, key: &String) -> (Option<u64>, Vec<u64>) {
|
||||
let hash = hash_key(key);
|
||||
|
||||
let mut traversal_history = Vec::new();
|
||||
|
||||
let block = self.io.get_page(0xa0).expect("IO Error.");
|
||||
|
||||
loop {
|
||||
traversal_history.push(block.stream_position().unwrap());
|
||||
let node = read_node(&self.meta, block);
|
||||
|
||||
if node.node_type == IndexNodeType::Invalid {
|
||||
panic!(
|
||||
"Invalid node detected: {}",
|
||||
block.stream_position().unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
if node.node_type == IndexNodeType::Leaf {
|
||||
for child in node.children() {
|
||||
if child.key == hash {
|
||||
return (Some(child.pointer), traversal_history);
|
||||
}
|
||||
}
|
||||
return (None, traversal_history);
|
||||
}
|
||||
|
||||
for child in node.children {
|
||||
if child.key < hash {
|
||||
continue;
|
||||
}
|
||||
|
||||
block.seek(SeekFrom::Start(child.pointer)).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn btree_insert_key(
|
||||
&mut self,
|
||||
key: &String,
|
||||
pointer: u64,
|
||||
) -> Result<Vec<u64>, String> {
|
||||
let (result, traversal_history) = self.btree_find_key(key);
|
||||
|
||||
if result.is_some() {
|
||||
return Err("Value already exists".to_string());
|
||||
}
|
||||
|
||||
let hash = hash_key(key);
|
||||
|
||||
let mut block = &mut self.io.get_page(0xa0).expect("IO Error.");
|
||||
|
||||
if traversal_history.is_empty() {
|
||||
return Err("No nodes found...".to_string());
|
||||
}
|
||||
|
||||
let position = *traversal_history.last().unwrap();
|
||||
|
||||
block.seek(SeekFrom::Start(position)).unwrap();
|
||||
|
||||
let mut node = read_node(&self.meta, &mut block);
|
||||
if node.len() == self.meta.index_node_arity as usize {
|
||||
//TODO Reorganize nodes
|
||||
todo!()
|
||||
}
|
||||
|
||||
let entry = IndexNodeEntry { key: hash, pointer };
|
||||
|
||||
node.children.push(entry);
|
||||
|
||||
block.seek(SeekFrom::Start(position)).unwrap();
|
||||
node.serialize(&self.meta);
|
||||
|
||||
//todo serialize
|
||||
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub(super) fn btree_delete_key(&self, key: &String) {
|
||||
let _hash = hash_key(key);
|
||||
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{IndexNode, IndexNodeEntry, IndexNodeType};
|
||||
|
||||
#[test]
|
||||
fn index_node_serialize() {
|
||||
let root = IndexNode {
|
||||
node_type: IndexNodeType::Internal,
|
||||
children: vec![IndexNodeEntry {
|
||||
key: 1,
|
||||
pointer: 258,
|
||||
}],
|
||||
};
|
||||
|
||||
let leaf = IndexNode {
|
||||
node_type: IndexNodeType::Leaf,
|
||||
children: vec![
|
||||
IndexNodeEntry {
|
||||
key: 1,
|
||||
pointer: 516,
|
||||
},
|
||||
IndexNodeEntry {
|
||||
key: 4,
|
||||
pointer: 520,
|
||||
},
|
||||
IndexNodeEntry {
|
||||
key: 16,
|
||||
pointer: 524,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
let meta = super::LocalStoreMeta {
|
||||
record_count: 3,
|
||||
specifier_count: 0,
|
||||
index_node_arity: 16,
|
||||
data_block_size: 8192,
|
||||
data_offset: 0,
|
||||
};
|
||||
|
||||
let root_bytes = root.serialize(&meta);
|
||||
let leaf_bytes = leaf.serialize(&meta);
|
||||
|
||||
assert_eq!(IndexNodeType::from(root_bytes[0]), root.node_type);
|
||||
assert_eq!(IndexNodeType::from(leaf_bytes[0]), leaf.node_type);
|
||||
|
||||
assert_eq!(root_bytes[2..10], root.children[0].key.to_be_bytes());
|
||||
assert_eq!(root_bytes[10..18], root.children[0].pointer.to_be_bytes());
|
||||
assert!(root_bytes.iter().skip(18).all(|x| *x == 0u8));
|
||||
|
||||
let mut cnt = 2;
|
||||
for child in leaf.children {
|
||||
assert_eq!(leaf_bytes[cnt..(cnt + 8)], child.key.to_be_bytes());
|
||||
assert_eq!(
|
||||
leaf_bytes[(cnt + 8)..(cnt + 16)],
|
||||
child.pointer.to_be_bytes()
|
||||
);
|
||||
cnt += 16;
|
||||
}
|
||||
assert!(root_bytes.iter().skip(cnt).all(|x| *x == 0u8));
|
||||
}
|
||||
}
|
62
src/store/local/io.rs
Normal file
62
src/store/local/io.rs
Normal file
|
@ -0,0 +1,62 @@
|
|||
use std::{
|
||||
collections::HashMap,
|
||||
fs::File,
|
||||
io::{BufReader, Read, Seek, SeekFrom},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
crypto::EncryptionContext,
|
||||
data::{DataPage, DataPager},
|
||||
};
|
||||
|
||||
use super::LocalStoreMeta;
|
||||
|
||||
pub struct DataAccessor {
|
||||
path: String,
|
||||
cache: HashMap<usize, DataPage>,
|
||||
dirty_pages: Vec<DataPage>,
|
||||
enc: EncryptionContext,
|
||||
block_size: usize,
|
||||
}
|
||||
|
||||
impl DataPager for DataAccessor {
|
||||
fn get_page(&mut self, offset: usize) -> Result<&mut DataPage, String> {
|
||||
if !self.cache.contains_key(&offset) {
|
||||
let data = self.read_page_from_file(offset);
|
||||
match data {
|
||||
Ok(x) => self.cache.insert(offset, x),
|
||||
Err(x) => return Err(x),
|
||||
};
|
||||
};
|
||||
|
||||
Ok(self.cache.get_mut(&offset).unwrap())
|
||||
}
|
||||
|
||||
fn update_page(&mut self, page: crate::data::WritableDataPage) -> Result<u64, ()> {
|
||||
self.dirty_pages
|
||||
.push(DataPage::load(page.offset(), page.data().to_vec()));
|
||||
return Ok(1);
|
||||
}
|
||||
}
|
||||
|
||||
impl DataAccessor {
|
||||
fn read_page_from_file(&mut self, offset: usize) -> Result<DataPage, String> {
|
||||
let mut file = BufReader::new(File::open(&self.path).unwrap());
|
||||
file.seek(SeekFrom::Start(offset as u64)).unwrap();
|
||||
|
||||
let mut buf = vec![0; self.block_size + 16];
|
||||
file.read_exact(&mut buf).unwrap();
|
||||
|
||||
self.enc.decrypt_block(&buf, offset)
|
||||
}
|
||||
|
||||
pub(super) fn create(path: &String, enc: EncryptionContext, meta: &LocalStoreMeta) -> Self {
|
||||
DataAccessor {
|
||||
enc,
|
||||
path: path.clone(),
|
||||
cache: HashMap::new(),
|
||||
dirty_pages: Vec::new(),
|
||||
block_size: meta.data_block_size as usize,
|
||||
}
|
||||
}
|
||||
}
|
333
src/store/local/mod.rs
Normal file
333
src/store/local/mod.rs
Normal file
|
@ -0,0 +1,333 @@
|
|||
mod data;
|
||||
mod header;
|
||||
mod index;
|
||||
mod io;
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs::File,
|
||||
io::{BufReader, Read, Write},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use password_hash::SaltString;
|
||||
|
||||
use crate::crypto::EncryptionContext;
|
||||
|
||||
use self::{
|
||||
data::LocalRecord,
|
||||
header::{try_deserialize_header, LocalStoreHeader, MAGIC_NUM},
|
||||
index::IndexNode,
|
||||
io::DataAccessor,
|
||||
};
|
||||
|
||||
use super::{RecordType, Store, StoreRecord};
|
||||
|
||||
pub struct LocalStore {
|
||||
path: String,
|
||||
header: LocalStoreHeader,
|
||||
meta: LocalStoreMeta,
|
||||
cache: HashMap<String, LocalRecord>,
|
||||
io: DataAccessor,
|
||||
}
|
||||
|
||||
/*
|
||||
Store binary format:
|
||||
Header: 0x00 (first 96 bytes):
|
||||
0x00 - magic number
|
||||
0x04 - last access timestamp
|
||||
0x08 - last write timestamp
|
||||
0x0c - encryption type byte
|
||||
0x0d - hash type byte
|
||||
0x0e - superblock size
|
||||
0x10 - cipher key size
|
||||
0x12 - encrypted block offset
|
||||
0x16 - header CRC-32 checksum
|
||||
0x1a - padding (6 bytes)
|
||||
Enc details: 48 bytes
|
||||
0x20 - salt
|
||||
0x40 - master IV
|
||||
0x4c - 3 byte padding with 0
|
||||
0x4f - salt size
|
||||
---Encrypted section starts here---
|
||||
--Block 0
|
||||
Superblock:
|
||||
Hash:
|
||||
0x50 - master password hash
|
||||
Metadata:
|
||||
0x70 - record count
|
||||
0x74 - specifier count
|
||||
0x88 - padding with 0
|
||||
Auth tag: 0x90
|
||||
--Block 1
|
||||
Index:
|
||||
Root node:
|
||||
0xa0 - root node or invalid node if empty db
|
||||
*/
|
||||
|
||||
//Proposal: Initialize IV to 0?
|
||||
|
||||
struct LocalStoreMeta {
|
||||
record_count: u32,
|
||||
specifier_count: u32,
|
||||
index_node_arity: u16,
|
||||
data_block_size: u32,
|
||||
data_offset: u64,
|
||||
}
|
||||
|
||||
fn try_deserialize_superblock(
|
||||
file: &mut BufReader<File>,
|
||||
enc: &EncryptionContext,
|
||||
header: &LocalStoreHeader,
|
||||
) -> Result<LocalStoreMeta, &'static str> {
|
||||
unsafe {
|
||||
let data = match enc.decrypt_block_from_file(header.enc_sup_block_size as usize, 0, file) {
|
||||
Ok(x) => x,
|
||||
Err(_) => return Err("Possibly corrupted store superblock."),
|
||||
};
|
||||
|
||||
if data.buffer()[0..enc.get_key().len()].ne(enc.get_key()) {
|
||||
return Err("Key mismatch.");
|
||||
}
|
||||
|
||||
let meta = deserialize_store_meta(&&data.buffer()[32..]);
|
||||
|
||||
Ok(meta)
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize_header(header: &LocalStoreHeader) -> Vec<u8> {
|
||||
let mut buf = Vec::<u8>::new();
|
||||
buf.append(u32::to_be_bytes(MAGIC_NUM).to_vec().as_mut());
|
||||
buf.append(u32::to_be_bytes(header.last_access).to_vec().as_mut());
|
||||
buf.append(u32::to_be_bytes(header.last_write).to_vec().as_mut());
|
||||
buf.push(header.enc_type as u8);
|
||||
buf.push(header.hash_type as u8);
|
||||
buf.append(
|
||||
u16::to_be_bytes(header.enc_sup_block_size)
|
||||
.to_vec()
|
||||
.as_mut(),
|
||||
);
|
||||
buf.append(u16::to_be_bytes(header.enc_key_size).to_vec().as_mut());
|
||||
buf.append(
|
||||
u32::to_be_bytes(header.encrypted_block_offset)
|
||||
.to_vec()
|
||||
.as_mut(),
|
||||
);
|
||||
buf.append(u32::to_be_bytes(header.chksum_crc).to_vec().as_mut());
|
||||
|
||||
let padding_len = 32 - buf.len();
|
||||
let mut pad: Vec<u8> = vec![0; padding_len];
|
||||
buf.append(&mut pad);
|
||||
|
||||
return buf;
|
||||
}
|
||||
|
||||
fn try_create_store(path: &String, passphrase: &Vec<u8>) -> bool {
|
||||
let timestamp: u32 = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs() as u32;
|
||||
|
||||
let enc = EncryptionContext::default(passphrase);
|
||||
|
||||
let mut superblock_buf: Vec<u8> = Vec::new();
|
||||
|
||||
unsafe {
|
||||
superblock_buf.extend_from_slice(&enc.get_key());
|
||||
}
|
||||
|
||||
if superblock_buf.len() != 32 {
|
||||
superblock_buf.append([b'\0'].repeat(32 - superblock_buf.len()).as_mut());
|
||||
}
|
||||
|
||||
let meta = LocalStoreMeta {
|
||||
record_count: 0,
|
||||
specifier_count: 0,
|
||||
index_node_arity: 16,
|
||||
data_block_size: 8092,
|
||||
data_offset: 0,
|
||||
};
|
||||
|
||||
superblock_buf.append(&mut serialize_store_meta(&meta));
|
||||
superblock_buf.extend_from_slice(&[b'\0'].repeat(64 - superblock_buf.len()));
|
||||
|
||||
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.encryption_type(),
|
||||
enc_sup_block_size: (enc_buf.len() - 16) as u16,
|
||||
enc_key_size: 256,
|
||||
hash_type: enc.hash_type(),
|
||||
encrypted_block_offset: 32,
|
||||
chksum_crc: 0,
|
||||
};
|
||||
|
||||
header.chksum_crc = header.get_checksum();
|
||||
|
||||
let mut index_block: Vec<u8> = vec![0; meta.data_block_size as usize];
|
||||
let root_node_buf = IndexNode::new().serialize(&meta);
|
||||
|
||||
index_block[0..root_node_buf.len()].copy_from_slice(&root_node_buf);
|
||||
|
||||
let mut buf = serialize_header(&header);
|
||||
|
||||
let mut salt_buf: [u8; 32] = [0; 32];
|
||||
enc.salt().b64_decode(&mut salt_buf).unwrap();
|
||||
if salt_buf.len() > 32 {
|
||||
return false;
|
||||
}
|
||||
buf.extend_from_slice(&salt_buf);
|
||||
|
||||
let iv = &enc.root_iv();
|
||||
if iv.len() != 12 {
|
||||
return false;
|
||||
}
|
||||
buf.extend_from_slice(iv);
|
||||
buf.extend_from_slice(&[b'\0'].repeat(4));
|
||||
|
||||
buf.extend_from_slice(&[b'\0'].repeat(0x50 - buf.len()));
|
||||
buf[0x4f] = 16;
|
||||
|
||||
buf.extend_from_slice(&enc_buf.buffer());
|
||||
buf.extend_from_slice(&enc.encrypt_block(&index_block, 0xa0).unwrap().buffer());
|
||||
|
||||
match std::fs::File::create(path).unwrap().write(&buf) {
|
||||
Ok(x) => return x == buf.len(),
|
||||
Err(_) => return false,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_enc_salt_iv(file: &mut BufReader<File>) -> Result<(SaltString, Vec<u8>), &'static str> {
|
||||
let mut buf: [u8; 48] = [0; 48];
|
||||
|
||||
file.read_exact(&mut buf).unwrap();
|
||||
|
||||
let salt_len = buf[47] as usize;
|
||||
|
||||
let salt = match SaltString::b64_encode(&buf[0..salt_len]) {
|
||||
Err(_) => return Err("Invalid salt."),
|
||||
Ok(x) => x,
|
||||
};
|
||||
|
||||
let iv = buf[32..44].to_owned();
|
||||
Ok((salt, iv))
|
||||
}
|
||||
|
||||
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());
|
||||
let index_node_arity = u16::from_be_bytes(data[8..10].try_into().unwrap());
|
||||
let data_block_size = u32::from_be_bytes(data[10..14].try_into().unwrap());
|
||||
let data_offset = u64::from_be_bytes(data[14..22].try_into().unwrap());
|
||||
|
||||
LocalStoreMeta {
|
||||
record_count: records,
|
||||
specifier_count: specifiers,
|
||||
index_node_arity,
|
||||
data_block_size,
|
||||
data_offset,
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize_store_meta(meta: &LocalStoreMeta) -> Vec<u8> {
|
||||
let mut buf: Vec<u8> = 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(u16::to_be_bytes(meta.index_node_arity).to_vec().as_mut());
|
||||
buf.append(u32::to_be_bytes(meta.data_block_size).to_vec().as_mut());
|
||||
buf.append(u64::to_be_bytes(meta.data_offset).to_vec().as_mut());
|
||||
buf.append([b'\0'].repeat(32 - buf.len()).as_mut());
|
||||
|
||||
buf
|
||||
}
|
||||
|
||||
pub fn load(path: String, passphrase: Vec<u8>) -> Result<LocalStore, &'static str> {
|
||||
let path_p = Path::new(&path);
|
||||
|
||||
if !path_p.exists() {
|
||||
if !try_create_store(&path, &passphrase) {
|
||||
return Err("Cannot create store.");
|
||||
}
|
||||
} else if !path_p.is_file() {
|
||||
return Err("Invalid path.");
|
||||
}
|
||||
|
||||
let file = match File::open(&path_p) {
|
||||
Ok(x) => x,
|
||||
Err(_) => return Err("Cannot open store."),
|
||||
};
|
||||
|
||||
let mut reader = BufReader::new(file);
|
||||
|
||||
let header = match try_deserialize_header(&mut reader) {
|
||||
Err(x) => return Err(x),
|
||||
Ok(meta) => meta,
|
||||
};
|
||||
|
||||
let (salt, iv) = match get_enc_salt_iv(&mut reader) {
|
||||
Ok(s) => s,
|
||||
Err(x) => return Err(x),
|
||||
};
|
||||
|
||||
let enc_ctx =
|
||||
EncryptionContext::from_settings(&passphrase, header.enc_type, header.hash_type, salt, iv);
|
||||
|
||||
let meta = match try_deserialize_superblock(&mut reader, &enc_ctx, &header) {
|
||||
Ok(d) => d,
|
||||
Err(x) => return Err(x),
|
||||
};
|
||||
|
||||
let store = LocalStore {
|
||||
io: DataAccessor::create(&path, enc_ctx, &meta),
|
||||
path,
|
||||
header,
|
||||
meta,
|
||||
cache: HashMap::new(),
|
||||
};
|
||||
|
||||
return Ok(store);
|
||||
}
|
||||
|
||||
impl Store for LocalStore {
|
||||
fn get_creds(&mut self, specifier: &String, key: &String) -> Option<StoreRecord> {
|
||||
match self.btree_find_key(&key) {
|
||||
(Some(x), _) => {
|
||||
println!("Found: {}", x);
|
||||
todo!()
|
||||
}
|
||||
(None, _) => return None,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_creds_by_specifier(&mut self, specifier: &String) -> Vec<StoreRecord> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn store_creds(
|
||||
&mut self,
|
||||
specifier: &String,
|
||||
key: &String,
|
||||
value: &Vec<u8>,
|
||||
r#type: RecordType,
|
||||
meta: Option<String>,
|
||||
) {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::mem::size_of;
|
||||
|
||||
use crate::store::local::{header::HEADER_SIZE, LocalStoreHeader};
|
||||
|
||||
#[test]
|
||||
fn header_size_const_greater_than_realsize() {
|
||||
let c = HEADER_SIZE;
|
||||
let r = size_of::<LocalStoreHeader>();
|
||||
|
||||
assert!(c > r, "const = {}, real = {}", c, r);
|
||||
}
|
||||
}
|
160
src/store/mod.rs
160
src/store/mod.rs
|
@ -1,5 +1,3 @@
|
|||
use std::io::{Read, Seek, SeekFrom};
|
||||
|
||||
pub(crate) mod local;
|
||||
pub(crate) mod server;
|
||||
|
||||
|
@ -39,10 +37,10 @@ impl From<u8> for RecordType {
|
|||
}
|
||||
|
||||
pub trait Store {
|
||||
fn get_creds(&self, specifier: &String, key: &String) -> Option<StoreRecord>;
|
||||
fn get_creds_by_specifier(&self, specifier: &String) -> Vec<StoreRecord>;
|
||||
fn get_creds(&mut self, specifier: &String, key: &String) -> Option<StoreRecord>;
|
||||
fn get_creds_by_specifier(&mut self, specifier: &String) -> Vec<StoreRecord>;
|
||||
fn store_creds(
|
||||
&self,
|
||||
&mut self,
|
||||
specifier: &String,
|
||||
key: &String,
|
||||
value: &Vec<u8>,
|
||||
|
@ -50,155 +48,3 @@ pub trait Store {
|
|||
meta: Option<String>,
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, PartialOrd)]
|
||||
pub enum EncryptionType {
|
||||
Invalid = 0,
|
||||
AesGcm = 1,
|
||||
Chacha20Poly1305 = 2,
|
||||
}
|
||||
|
||||
impl From<u8> for EncryptionType {
|
||||
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 From<u8> for HashType {
|
||||
fn from(n: u8) -> Self {
|
||||
match n {
|
||||
0 => HashType::Invalid,
|
||||
1 => HashType::Argon2,
|
||||
2 => HashType::Bcrypt,
|
||||
4 => HashType::Pbkdf2,
|
||||
_ => panic!("Invalid value '{}'.", n),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct EncryptionStream {
|
||||
data: Vec<u8>,
|
||||
pos: usize,
|
||||
}
|
||||
|
||||
impl Read for EncryptionStream {
|
||||
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||
let range = self.pos..(self.pos + buf.len()).clamp(0, self.data.len() - buf.len());
|
||||
let size = range.len();
|
||||
buf.copy_from_slice(&self.data[range]);
|
||||
self.pos += size;
|
||||
Ok(size)
|
||||
}
|
||||
}
|
||||
|
||||
impl Seek for EncryptionStream {
|
||||
fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
|
||||
let cur = self.pos;
|
||||
|
||||
let newpos = match pos {
|
||||
SeekFrom::Start(x) => {
|
||||
if x > self.data.len() as u64 {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"Seek position out of buffer bounds.",
|
||||
));
|
||||
}
|
||||
x
|
||||
}
|
||||
SeekFrom::End(x) => {
|
||||
if x > self.data.len() as i64 {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"Seek position out of buffer bounds.",
|
||||
));
|
||||
}
|
||||
|
||||
(self.data.len() as i64 - x) as u64
|
||||
}
|
||||
SeekFrom::Current(x) => {
|
||||
let new = self.pos as i64 + x;
|
||||
|
||||
if new < 0 || new > self.data.len() as i64 {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"Seek position out of buffer bounds.",
|
||||
));
|
||||
}
|
||||
|
||||
new as u64
|
||||
}
|
||||
};
|
||||
|
||||
self.pos = newpos as usize;
|
||||
|
||||
Ok(cur as u64)
|
||||
}
|
||||
}
|
||||
|
||||
impl EncryptionStream {
|
||||
pub fn len(&self) -> usize {
|
||||
self.data.len()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
|
||||
use super::EncryptionStream;
|
||||
|
||||
fn make_enc_stream() -> EncryptionStream {
|
||||
EncryptionStream {
|
||||
data: vec![0; 128],
|
||||
pos: 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encryption_stream_seek() {
|
||||
let mut stream = make_enc_stream();
|
||||
|
||||
{
|
||||
{
|
||||
let pos = stream.seek(SeekFrom::Start(64));
|
||||
assert!(pos.is_ok());
|
||||
assert_eq!(pos.unwrap(), 0);
|
||||
}
|
||||
|
||||
{
|
||||
let pos = stream.seek(SeekFrom::Start(128));
|
||||
assert!(pos.is_ok());
|
||||
assert_eq!(pos.unwrap(), 64);
|
||||
}
|
||||
|
||||
{
|
||||
let pos = stream.seek(SeekFrom::Start(256));
|
||||
assert!(pos.is_err());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encryption_stream_read() {
|
||||
let mut stream = make_enc_stream();
|
||||
let mut buf: [u8; 8] = [1; 8];
|
||||
|
||||
assert!(stream.read_exact(&mut buf).is_ok());
|
||||
|
||||
assert_eq!(&buf, &[0; 8]);
|
||||
assert_eq!(stream.stream_position().unwrap(), 8);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ pub struct ServerStore {
|
|||
pub fn load(address: String) -> Result<ServerStore, &'static str> {
|
||||
let url = match reqwest::Url::parse(&address) {
|
||||
Ok(x) => x,
|
||||
Err(x) => return Err("Invalid address."),
|
||||
Err(_) => return Err("Invalid address."),
|
||||
};
|
||||
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
|
@ -41,16 +41,16 @@ impl ServerStore {
|
|||
}
|
||||
|
||||
impl Store for ServerStore {
|
||||
fn get_creds(&self, specifier: &String, key: &String) -> Option<StoreRecord> {
|
||||
fn get_creds(&mut self, specifier: &String, key: &String) -> Option<StoreRecord> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn get_creds_by_specifier(&self, specifier: &String) -> Vec<StoreRecord> {
|
||||
fn get_creds_by_specifier(&mut self, specifier: &String) -> Vec<StoreRecord> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn store_creds(
|
||||
&self,
|
||||
&mut self,
|
||||
specifier: &String,
|
||||
key: &String,
|
||||
value: &Vec<u8>,
|
||||
|
|
Loading…
Reference in a new issue