xous-core/apps/vault/src/storage.rs
Jean-Pierre De Jesus DIAZ 5971507410
apps/vault: Implement Display instead of Into
Avoid allocations in some cases, for example, when formatting
to an I/O buffer. Or when formatting an string.

Signed-off-by: Jean-Pierre De Jesus DIAZ <me@jeandudey.tech>
2022-10-12 15:39:01 +02:00

818 lines
25 KiB
Rust

use chrono::{DateTime, NaiveDateTime, Utc};
use std::convert::TryFrom;
use std::{
io::Read,
io::Write,
time::{SystemTime, UNIX_EPOCH},
};
use ctap_crypto::Hash256;
const VAULT_PASSWORD_DICT: &'static str = "vault.passwords";
const VAULT_TOTP_DICT: &'static str = "vault.totp";
const VAULT_TOTP_ALLOC_HINT: usize = 128;
const VAULT_PASSWORD_REC_VERSION: u32 = 1;
// Version history TOTP record:
// - v1 created, basic record for TOTP
// - v2 add HOTP support:
// - `hotp` field added. If 1, then HOTP record. If not existent or not 1, then TOTP
// - If HOTP, then the `timestep` field is re-purposed as the `count` field.
// - v1 records read directly onto v2 records, and `hotp` is always `false` for v1 records
const VAULT_TOTP_REC_VERSION: u32 = 2;
#[derive(Debug)]
pub enum Error {
IoError(std::io::Error),
TotpSerError(TOTPSerializationError),
PasswordSerError(PasswordSerializationError),
KeyExists,
DupesExist(Vec<usize>),
}
impl From<std::io::Error> for Error {
fn from(e: std::io::Error) -> Self {
Self::IoError(e)
}
}
impl From<TOTPSerializationError> for Error {
fn from(e: TOTPSerializationError) -> Self {
Self::TotpSerError(e)
}
}
impl From<PasswordSerializationError> for Error {
fn from(e: PasswordSerializationError) -> Self {
Self::PasswordSerError(e)
}
}
pub struct Manager {
pddb: pddb::Pddb,
}
pub trait StorageContent {
fn settings(&self) -> ContentPDDBSettings;
fn set_ctime(&mut self, value: u64);
fn from_vec(&mut self, data: Vec<u8>) -> Result<(), Error>;
fn to_vec(&self) -> Vec<u8>;
fn hash(&self) -> Vec<u8>;
}
#[derive(Clone)]
pub struct ContentPDDBSettings {
dict: String,
alloc_hint: Option<usize>,
}
pub enum ContentKind {
TOTP,
Password,
}
impl ContentKind {
fn settings(&self) -> ContentPDDBSettings {
match self {
ContentKind::TOTP => TotpRecord::default().settings(),
ContentKind::Password => PasswordRecord::default().settings(),
}
}
}
impl Manager {
pub fn new(_xns: &xous_names::XousNames) -> Manager {
Manager {
pddb: pddb::Pddb::new(),
}
}
fn pddb_exists(&self, dict: &str, key_name: &str, basis: Option<String>) -> bool {
match self.pddb.get(
dict,
&key_name,
basis.as_deref(),
false,
false,
None,
Some(crate::basis_change),
) {
Ok(_) => return true,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => false,
Err(e) => {
log::error!("error while trying to lookup if a key exists, {}", e);
false
}
}
}
fn pddb_store(
&self,
payload: &[u8],
dict: &str,
key_name: &str,
alloc_hint: Option<usize>,
basis: Option<String>,
sync: bool,
overwrite: bool,
) -> Result<(), Error> {
if !overwrite && self.pddb_exists(dict, key_name, basis.clone()) {
return Err(Error::KeyExists);
}
match self.pddb.get(
dict,
&key_name,
basis.as_deref(),
true, // if overwrite, we wanna create both the dict and the key if they don't
// exist
true,
alloc_hint,
Some(crate::basis_change),
) {
Ok(mut data) => match data.write(payload) {
Ok(_) => match sync {
true => Ok(self.pddb.sync().unwrap_or(())),
false => Ok(()),
},
Err(e) => Err(Error::IoError(e)),
},
Err(e) => Err(Error::IoError(e)),
}
}
fn pddb_get(&self, dict: &str, key_name: &str) -> Result<Vec<u8>, Error> {
match self.pddb.get(
dict,
key_name,
None,
false,
false,
None,
Some(crate::basis_change),
) {
Ok(mut record) => {
let mut data = Vec::<u8>::new();
record.read_to_end(&mut data)?;
Ok(data)
}
Err(e) => return Err(Error::IoError(e)),
}
}
fn basis_for_key(&self, dict: &str, key_name: &str) -> Result<String, Error> {
match self.pddb.get(
dict,
key_name,
None,
false,
false,
None,
Some(crate::basis_change),
) {
Ok(record) => Ok(record
.attributes()
.expect("couldn't get key attributes")
.basis),
Err(e) => return Err(Error::IoError(e)),
}
}
pub fn new_record(
&mut self,
record: &mut dyn StorageContent,
basis: Option<String>,
overwrite: bool,
) -> Result<(), Error> {
let record = record;
let settings = record.settings();
record.set_ctime(utc_now().timestamp() as u64);
let serialized_record: Vec<u8> = record.to_vec();
self.pddb_store(
&serialized_record,
&settings.dict,
&hex(record.hash()),
settings.alloc_hint,
basis.clone(),
true,
overwrite,
)
}
pub fn new_records(
&mut self,
records: Vec<Box<dyn StorageContent>>,
basis: Option<String>,
overwrite: bool,
) -> Result<(), Error> {
let mut precords = records.into_iter().peekable();
let mut current_idx = 0; // idk how to use peekable + enumerate
let mut dupes = vec![];
while let Some(record) = precords.next() {
let mut record = record;
let settings = record.settings();
record.set_ctime(utc_now().timestamp() as u64);
let serialized_record: Vec<u8> = record.to_vec();
let should_sync = precords.peek().is_none() || current_idx % 10 == 0;
log::debug!(
"current_idx: {}, should_sync: {}, is_none: {}",
current_idx,
should_sync,
precords.peek().is_none()
);
match self.pddb_store(
&serialized_record,
&settings.dict,
&hex(record.hash()),
settings.alloc_hint,
basis.clone(),
should_sync,
overwrite,
) {
Ok(()) => (),
Err(error) => match error {
Error::KeyExists => {
dupes.push(current_idx);
()
}
_ => return Err(error),
},
}
current_idx += 1;
}
if !dupes.is_empty() {
return Err(Error::DupesExist(dupes));
}
Ok(())
}
pub fn all<T: StorageContent + std::default::Default>(
&self,
kind: ContentKind,
) -> Result<Vec<T>, Error> {
let settings = kind.settings();
let keylist = self.pddb.list_keys(&settings.dict, None)?;
let mut ret = vec![];
for key in keylist {
let mut record = T::default();
record.from_vec(self.pddb_get(&settings.dict, &key)?)?;
ret.push(record);
}
Ok(ret)
}
pub fn get_record<T: StorageContent + std::default::Default>(
&self,
kind: &ContentKind,
key_name: &str,
) -> Result<T, Error> {
let settings = kind.settings();
let mut record = T::default();
record.from_vec(self.pddb_get(&settings.dict, &key_name)?)?;
Ok(record)
}
pub fn update(
&mut self,
kind: &ContentKind,
key_name: &str,
record: &mut dyn StorageContent,
) -> Result<(), Error> {
let settings = kind.settings();
let basis = self.basis_for_key(&settings.dict, key_name)?;
self.pddb
.delete_key(&settings.dict, key_name, Some(&basis))?;
self.new_record(&mut *record, Some(basis), true)
}
pub fn delete(&mut self, kind: ContentKind, key_name: &str) -> Result<(), Error> {
let settings = kind.settings();
let basis = self.basis_for_key(&settings.dict, key_name)?;
self.pddb
.delete_key(&settings.dict, key_name, Some(&basis))
.map_err(|error| Error::IoError(error))
}
}
#[derive(Default)]
pub struct TotpRecord {
pub version: u32,
// as base32, RFC4648 no padding
pub secret: String,
pub name: String,
pub algorithm: crate::totp::TotpAlgorithm,
pub notes: String,
pub digits: u32,
pub timestep: u64,
pub ctime: u64,
pub is_hotp: bool,
}
#[derive(Debug)]
pub enum TOTPSerializationError {
BadVersion,
BadAlgorithm,
BadDigitsAmount,
BadCtime,
BadTimestep,
BadHotp,
MalformedInput,
}
impl StorageContent for TotpRecord {
fn settings(&self) -> ContentPDDBSettings {
ContentPDDBSettings {
dict: VAULT_TOTP_DICT.to_string(),
alloc_hint: Some(VAULT_TOTP_ALLOC_HINT),
}
}
fn set_ctime(&mut self, value: u64) {
self.ctime = value;
}
fn from_vec(&mut self, data: Vec<u8>) -> Result<(), Error> {
let desc_str = String::from_utf8(data).or(Err(TOTPSerializationError::MalformedInput))?;
let mut pr = TotpRecord::default();
let lines = desc_str.split('\n');
for line in lines {
if let Some((tag, data)) = line.split_once(':') {
match tag {
"version" => {
if let Ok(ver) = u32::from_str_radix(data, 10) {
pr.version = ver
} else {
log::warn!("ver error");
return Err(TOTPSerializationError::BadVersion)?;
}
}
"secret" => pr.secret.push_str(data),
"name" => pr.name.push_str(data),
"algorithm" => {
pr.algorithm = match crate::totp::TotpAlgorithm::try_from(data) {
Ok(a) => a,
Err(_) => return Err(TOTPSerializationError::BadAlgorithm)?,
}
}
"notes" => pr.notes.push_str(data),
"digits" => {
if let Ok(digits) = u32::from_str_radix(data, 10) {
pr.digits = digits;
} else {
log::warn!("digits error");
return Err(TOTPSerializationError::BadDigitsAmount)?;
}
}
"ctime" => {
if let Ok(ctime) = u64::from_str_radix(data, 10) {
pr.ctime = ctime;
} else {
log::warn!("ctime error");
return Err(TOTPSerializationError::BadCtime)?;
}
}
"timestep" => {
if let Ok(timestep) = u64::from_str_radix(data, 10) {
pr.timestep = timestep;
} else {
log::warn!("timestep error");
return Err(TOTPSerializationError::BadTimestep)?;
}
}
"hotp" => {
if let Ok(setting) = u8::from_str_radix(data, 10) {
if setting != 0 {
pr.is_hotp = true;
} else {
pr.is_hotp = false;
}
} else {
log::warn!("hotp variant error");
return Err(TOTPSerializationError::BadHotp)?;
}
}
_ => {
log::warn!(
"unexpected tag {} encountered parsing TOTP info, ignoring",
tag
);
}
}
} else {
log::trace!("invalid line skipped: {:?}", line);
}
}
*self = pr;
Ok(())
}
fn to_vec(&self) -> Vec<u8> {
format!(
"{}:{}\n{}:{}\n{}:{}\n{}:{}\n{}:{}\n{}:{}\n{}:{}\n{}:{}\n{}:{}\n",
"version",
self.version,
"secret",
self.secret,
"name",
self.name,
"algorithm",
self.algorithm,
"notes",
self.notes,
"digits",
self.digits,
"timestep",
self.timestep,
"hotp",
if self.is_hotp {1} else {0},
"ctime",
self.ctime,
)
.into_bytes()
}
fn hash(&self) -> Vec<u8> {
let mut h = ctap_crypto::sha256::Sha256::new();
h.update(self.name.as_bytes());
h.finalize().to_vec()
}
}
impl TryFrom<Vec<u8>> for TotpRecord {
type Error = TOTPSerializationError;
fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
let desc_str = String::from_utf8(value).or(Err(TOTPSerializationError::MalformedInput))?;
let mut pr = TotpRecord {
version: VAULT_TOTP_REC_VERSION,
secret: String::new(),
name: String::new(),
algorithm: crate::totp::TotpAlgorithm::HmacSha1,
notes: String::new(),
digits: 0,
ctime: 0,
timestep: 0,
is_hotp: false,
};
let lines = desc_str.split('\n');
for line in lines {
if let Some((tag, data)) = line.split_once(':') {
match tag {
"version" => {
if let Ok(ver) = u32::from_str_radix(data, 10) {
pr.version = ver
} else {
log::warn!("ver error");
return Err(TOTPSerializationError::BadVersion);
}
}
"secret" => pr.secret.push_str(data),
"name" => pr.name.push_str(data),
"algorithm" => {
pr.algorithm = match crate::totp::TotpAlgorithm::try_from(data) {
Ok(a) => a,
Err(_) => return Err(TOTPSerializationError::BadAlgorithm),
}
}
"notes" => pr.notes.push_str(data),
"digits" => {
if let Ok(digits) = u32::from_str_radix(data, 10) {
pr.digits = digits;
} else {
log::warn!("digits error");
return Err(TOTPSerializationError::BadDigitsAmount);
}
}
"ctime" => {
if let Ok(ctime) = u64::from_str_radix(data, 10) {
pr.ctime = ctime;
} else {
log::warn!("ctime error");
return Err(TOTPSerializationError::BadCtime);
}
}
"timestep" => {
if let Ok(timestep) = u64::from_str_radix(data, 10) {
pr.timestep = timestep;
} else {
log::warn!("timestep error");
return Err(TOTPSerializationError::BadTimestep);
}
}
"hotp" => {
if let Ok(setting) = u8::from_str_radix(data, 10) {
if setting != 0 {
pr.is_hotp = true;
} else {
pr.is_hotp = false;
}
} else {
log::warn!("hotp error");
return Err(TOTPSerializationError::BadHotp);
}
}
_ => {
log::warn!(
"unexpected tag {} encountered parsing TOTP info, ignoring",
tag
);
}
}
} else {
log::trace!("invalid line skipped: {:?}", line);
}
}
Ok(pr)
}
}
impl From<TotpRecord> for Vec<u8> {
fn from(tr: TotpRecord) -> Self {
format!(
"{}:{}\n{}:{}\n{}:{}\n{}:{}\n{}:{}\n{}:{}\n{}:{}\n{}:{}\n{}:{}\n",
"version",
tr.version,
"secret",
tr.secret,
"name",
tr.name,
"algorithm",
tr.algorithm,
"notes",
tr.notes,
"digits",
tr.digits,
"timestep",
tr.timestep,
"hotp",
if tr.is_hotp {1} else {0},
"ctime",
tr.ctime,
)
.into_bytes()
}
}
#[derive(Debug)]
pub enum PasswordSerializationError {
MalformedInput,
BadVersion,
BadCount,
BadCtime,
BadAtime,
}
#[derive(Default)]
pub struct PasswordRecord {
pub version: u32,
pub description: String,
pub username: String,
pub password: String,
pub notes: String,
pub ctime: u64,
pub atime: u64,
pub count: u64,
}
impl StorageContent for PasswordRecord {
fn settings(&self) -> ContentPDDBSettings {
ContentPDDBSettings {
dict: VAULT_PASSWORD_DICT.to_string(),
alloc_hint: Some(VAULT_TOTP_ALLOC_HINT),
}
}
fn set_ctime(&mut self, value: u64) {
self.ctime = value;
}
fn from_vec(&mut self, data: Vec<u8>) -> Result<(), Error> {
let desc_str =
String::from_utf8(data).or(Err(PasswordSerializationError::MalformedInput))?;
let mut pr = PasswordRecord::default();
let lines = desc_str.split('\n');
for line in lines {
if let Some((tag, data)) = line.split_once(':') {
match tag {
"version" => {
if let Ok(ver) = u32::from_str_radix(data, 10) {
pr.version = ver
} else {
log::warn!("ver error");
return Err(PasswordSerializationError::BadVersion)?;
}
}
"description" => pr.description.push_str(data),
"username" => pr.username.push_str(data),
"password" => pr.password.push_str(data),
"notes" => pr.notes.push_str(data),
"ctime" => {
if let Ok(ctime) = u64::from_str_radix(data, 10) {
pr.ctime = ctime;
} else {
log::warn!("ctime error");
return Err(PasswordSerializationError::BadCtime)?;
}
}
"atime" => {
if let Ok(atime) = u64::from_str_radix(data, 10) {
pr.atime = atime;
} else {
log::warn!("atime error");
return Err(PasswordSerializationError::BadAtime)?;
}
}
"count" => {
if let Ok(count) = u64::from_str_radix(data, 10) {
pr.count = count;
} else {
log::warn!("count error");
return Err(PasswordSerializationError::BadCount)?;
}
}
_ => {
log::warn!(
"unexpected tag {} encountered parsing password info, ignoring",
tag
);
}
}
} else {
log::trace!("invalid line skipped: {:?}", line);
}
}
*self = pr;
Ok(())
}
fn to_vec(&self) -> Vec<u8> {
format!(
"{}:{}\n{}:{}\n{}:{}\n{}:{}\n{}:{}\n{}:{}\n{}:{}\n{}:{}\n",
"version",
self.version,
"description",
self.description,
"username",
self.username,
"password",
self.password,
"notes",
self.notes,
"ctime",
self.ctime,
"atime",
self.atime,
"count",
self.count,
)
.into_bytes()
}
fn hash(&self) -> Vec<u8> {
let mut h = ctap_crypto::sha256::Sha256::new();
h.update(self.description.as_bytes());
h.update(self.username.as_bytes());
h.finalize().to_vec()
}
}
impl TryFrom<Vec<u8>> for PasswordRecord {
type Error = PasswordSerializationError;
fn try_from(data: Vec<u8>) -> Result<Self, Self::Error> {
let desc_str =
String::from_utf8(data).or(Err(PasswordSerializationError::MalformedInput))?;
let mut pr = PasswordRecord {
version: VAULT_PASSWORD_REC_VERSION,
description: String::new(),
username: String::new(),
password: String::new(),
notes: String::new(),
ctime: 0,
atime: 0,
count: 0,
};
let lines = desc_str.split('\n');
for line in lines {
if let Some((tag, data)) = line.split_once(':') {
match tag {
"version" => {
if let Ok(ver) = u32::from_str_radix(data, 10) {
pr.version = ver
} else {
log::warn!("ver error");
return Err(PasswordSerializationError::BadVersion);
}
}
"description" => pr.description.push_str(data),
"username" => pr.username.push_str(data),
"password" => pr.password.push_str(data),
"notes" => pr.notes.push_str(data),
"ctime" => {
if let Ok(ctime) = u64::from_str_radix(data, 10) {
pr.ctime = ctime;
} else {
log::warn!("ctime error");
return Err(PasswordSerializationError::BadCtime);
}
}
"atime" => {
if let Ok(atime) = u64::from_str_radix(data, 10) {
pr.atime = atime;
} else {
log::warn!("atime error");
return Err(PasswordSerializationError::BadAtime);
}
}
"count" => {
if let Ok(count) = u64::from_str_radix(data, 10) {
pr.count = count;
} else {
log::warn!("count error");
return Err(PasswordSerializationError::BadCount);
}
}
_ => {
log::warn!(
"unexpected tag {} encountered parsing password info, ignoring",
tag
);
}
}
} else {
log::trace!("invalid line skipped: {:?}", line);
}
}
Ok(pr)
}
}
impl From<PasswordRecord> for Vec<u8> {
fn from(pr: PasswordRecord) -> Self {
format!(
"{}:{}\n{}:{}\n{}:{}\n{}:{}\n{}:{}\n{}:{}\n{}:{}\n{}:{}\n",
"version",
pr.version,
"description",
pr.description,
"username",
pr.username,
"password",
pr.password,
"notes",
pr.notes,
"ctime",
pr.ctime,
"atime",
pr.atime,
"count",
pr.count,
)
.into_bytes()
}
}
/// because we don't get Utc::now, as the crate checks your architecture and xous is not recognized as a valid target
fn utc_now() -> DateTime<Utc> {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system time before Unix epoch");
let naive = NaiveDateTime::from_timestamp(now.as_secs() as i64, now.subsec_nanos() as u32);
DateTime::from_utc(naive, Utc)
}
fn hex(data: Vec<u8>) -> String {
use std::fmt::Write;
let mut s = String::with_capacity(2 * data.len());
for byte in data {
write!(s, "{:02X}", byte).unwrap();
}
s
}