Header info. Basic AES-GCM. Stored hash in superblock.

This commit is contained in:
nya 2022-12-12 21:07:44 +01:00
parent fab222b19a
commit 821af01c8f
Signed by: femsci
GPG key ID: 08F7911F0E650C67
8 changed files with 2082 additions and 4 deletions

14
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,14 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "lldb",
"request": "launch",
"name": "Cargo launch",
"cargo": {
"args": ["build"]
},
"args": []
}
]
}

1406
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -3,6 +3,16 @@ name = "nyanpass"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
aes-gcm = "0.10.1"
chacha20poly1305 = "0.10.1"
sha2 = "0.10.6"
sha3 = "0.10.6"
argon2 = "0.4.1"
bcrypt = "0.13.0"
pbkdf2 = "0.10"
password-hash = "0.4.2"
rand_core = { version = "0.6", features = ["std"] }
passwords = "3.1.12"
reqwest = "0.11.13"

32
src/config.rs Normal file
View file

@ -0,0 +1,32 @@
use std::{fs::File, io::Read, path::Path};
pub struct Preferences {
_store_path: String,
_pref_hash_algo: String,
_pref_enc_algo: String,
_pref_sign_algo: String,
}
pub fn try_load_preferences() -> Option<Preferences> {
//Alert: develop a better home dir acquisition method
let mut file = Path::new("/home/nya/.nyanpass.conf");
if !file.exists() {
file = Path::new("/etc/nyanpass.conf");
}
let file = File::open(file);
if file.is_err() {
return None;
}
let mut file = file.unwrap();
let mut buf: String = String::new();
if file.read_to_string(&mut buf).is_err() {
return None;
}
println!("{}", buf);
return None;
}

View file

@ -1,4 +1,28 @@
use config::try_load_preferences;
use store::Store;
mod config;
mod store;
fn main() { fn main() {
let args = std::env::args(); let args = std::env::args().collect::<Vec<String>>();
for _arg in args {
//options
}
try_load_preferences();
let store = match store::local::load(
"/tmp/store.db".to_string(),
"meowmeow".as_bytes().to_owned(),
) {
Err(x) => {
println!("Error: {}", x);
std::process::exit(-1);
}
Ok(x) => x,
};
store.get_creds_by_key("meow".to_string());
} }

522
src/store/local.rs Normal file
View file

@ -0,0 +1,522 @@
use std::{
arch::x86_64::_mm_crc32_u32,
fs::File,
io::{BufReader, Read, Write},
path::Path,
};
use aes_gcm::{aead::Aead, Aes256Gcm, KeyInit, Nonce};
use argon2::PasswordHasher;
use password_hash::SaltString;
use super::{Store, StoreRecord};
pub struct LocalStore {
path: String,
header: LocalStoreHeader,
enc_ctx: EncryptionContext,
}
/*
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:
0x00 - master password hash
Metadata:
0x20 - record count
0x24 - specifier count
0x08 -
*/
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();
/*println!(
"Checksum: {:x}, Expected: {:x}, Size:: {}",
self.chksum_crc, chk, HEADER_SIZE
);*/
chk == self.chksum_crc
}
}
struct EncryptionContext {
enc_type: EncryptionType,
hash_type: HashType,
key: Vec<u8>,
salt: SaltString,
iv_root: Vec<u8>,
}
#[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<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<String, &'static str> {
let mut buf: Vec<u8> = vec![0; header.enc_sup_block_size as usize];
if file.read_exact(&mut buf).is_err() {
return Err("Read error.");
}
let dat = enc.decrypt_block(&buf).unwrap();
println!("Superblocc: {:?}", &dat);
Ok(String::from_utf8_lossy(&dat).try_into().unwrap())
}
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 padlen = 32 - buf.len();
let mut pad: Vec<u8> = vec![0; padlen];
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 salt = SaltString::generate(&mut rand_core::OsRng);
let (default_hash_type, default_enc_type) = (HashType::Argon2, EncryptionType::AesGcm);
let key = derive_key(&passphrase, &salt, default_hash_type).unwrap();
let iv: &[u8; 12] = b"unique nonce"; //TODO: Nonce generation
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 enc_buf: Vec<u8> = 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());
}
let enc_buf = enc.encrypt_block(&enc_buf).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_key_size: 256,
hash_type: enc.hash_type,
encrypted_block_offset: 32,
chksum_crc: 0,
};
header.chksum_crc = header.get_checksum();
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);
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 get_specifier_map(file: &mut BufReader<File>, header: &LocalStoreHeader) {}
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 sup_data = match try_deserialize_superblock(&mut reader, &enc_ctx, &header) {
Ok(d) => 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,
};
return Ok(store);
}
struct CredentialRecord {
id: u64,
key: String,
value: String,
specifier: Option<String>,
meta: Option<String>,
}
const CRED_FLAG_SPECIFIER: u8 = 0b0000010;
const CRED_FLAG_METADATA: u8 = 0b0000100;
impl CredentialRecord {
fn serialize(&self) -> Vec<u8> {
let mut bytes = Vec::<u8>::new();
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() {
flags |= CRED_FLAG_METADATA;
}
bytes.push(flags);
bytes.append(u64::to_be_bytes(self.id).to_vec().as_mut());
let key_bytes = self.key.bytes();
let value_bytes = self.value.bytes();
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());
bytes.extend(key_bytes);
bytes.extend(value_bytes);
bytes
}
fn from_block(bytes: &[u8]) {
let flags = bytes[0];
let id = u64::from_be_bytes(bytes[1..5].try_into().unwrap());
todo!()
}
fn encrypt(&self, typ: EncryptionType) {}
}
impl Store for LocalStore {
fn get_creds(&self, id: u64) -> Option<StoreRecord> {
todo!()
}
fn get_creds_by_key(&self, key: String) -> Vec<StoreRecord> {
todo!()
}
fn store_creds(
&self,
key: String,
value: String,
specifier: Option<String>,
meta: Option<String>,
) {
todo!()
}
}
impl EncryptionContext {
fn decrypt_block(&self, ciphertext: &Vec<u8>) -> Result<Vec<u8>, &'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);
aes.decrypt(nonce, ciphertext.as_ref())
}
EncryptionType::Chacha20Poly1305 => todo!(),
};
if plaintext.is_err() {
return Err("Decryption error");
}
Ok(plaintext.unwrap())
}
fn encrypt_block(&self, plaintext: &Vec<u8>) -> Result<Vec<u8>, &'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);
aes.encrypt(nonce, plaintext.as_ref())
}
EncryptionType::Chacha20Poly1305 => todo!(),
};
if ciphertext.is_err() {
return Err("Encryption error");
}
Ok(ciphertext.unwrap())
}
}
#[cfg(test)]
mod tests {
use std::mem::size_of;
use crate::store::local::{LocalStoreHeader, HEADER_SIZE};
#[test]
fn header_size_const_greater_than_realsize() {
let c = HEADER_SIZE;
let r = size_of::<LocalStoreHeader>();
assert!(c > r, "const = {}, real = {}", c, r);
}
}

22
src/store/mod.rs Normal file
View file

@ -0,0 +1,22 @@
pub(crate) mod local;
pub(crate) mod server;
pub struct StoreRecord {
id: u64,
key: String,
value: String,
specifier: Option<String>,
meta: Option<String>,
}
pub trait Store {
fn get_creds(&self, id: u64) -> Option<StoreRecord>;
fn get_creds_by_key(&self, key: String) -> Vec<StoreRecord>;
fn store_creds(
&self,
key: String,
value: String,
specifier: Option<String>,
meta: Option<String>,
);
}

48
src/store/server.rs Normal file
View file

@ -0,0 +1,48 @@
use reqwest::header::HeaderValue;
use super::{Store, StoreRecord};
pub struct ServerStore {
address: String,
}
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."),
};
let mut headers = reqwest::header::HeaderMap::new();
headers.append(
"Authorization",
HeaderValue::from_static("$meowmeowplaceholder"),
);
let client = reqwest::Client::builder()
.user_agent(format!("nyanpass v{}", env!("CARGO_PKG_VERSION")))
.default_headers(headers)
.build()
.unwrap();
return Ok(ServerStore { address: address });
}
impl Store for ServerStore {
fn get_creds(&self, id: u64) -> Option<StoreRecord> {
todo!()
}
fn get_creds_by_key(&self, key: String) -> Vec<StoreRecord> {
todo!()
}
fn store_creds(
&self,
key: String,
value: String,
specifier: Option<String>,
meta: Option<String>,
) {
todo!()
}
}