add routines to populate test entries, and use less synthetic data

This commit is contained in:
bunnie 2022-06-24 00:30:45 +08:00
parent e998e96323
commit ce461a06fe
6 changed files with 494 additions and 31 deletions

23
Cargo.lock generated
View file

@ -154,6 +154,12 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce"
[[package]]
name = "base32"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa"
[[package]]
name = "base64"
version = "0.13.0"
@ -3095,6 +3101,17 @@ dependencies = [
"syn",
]
[[package]]
name = "sha-1"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f"
dependencies = [
"cfg-if 1.0.0",
"cpufeatures",
"digest 0.10.3",
]
[[package]]
name = "sha2"
version = "0.9.8"
@ -3890,15 +3907,19 @@ name = "vault"
version = "0.1.0"
dependencies = [
"arrayref",
"base32",
"byteorder",
"cbor",
"chrono",
"content-plugin-api",
"crypto-common",
"ctap-crypto",
"digest 0.9.0",
"enum-iterator",
"gam",
"graphics-server",
"hex 0.4.3",
"hmac 0.12.1",
"ime-plugin-api",
"locales",
"log",
@ -3909,7 +3930,9 @@ dependencies = [
"passwords",
"pddb",
"rand_core 0.6.3",
"random-pick",
"rkyv",
"sha-1",
"subtle",
"ticktimer-server",
"trng",

View file

@ -39,6 +39,15 @@ chrono = {version = "0.4.19", default-features = false, features = ["std"]}
# password generation
passwords = "3.1.9"
# totp
crypto-common = "0.1.3"
sha-1 = "0.10.0"
hmac = "0.12.1"
digest = "0.9.0"
base32 = "0.4.0"
# testing
random-pick = {version = "1.2.15", optional = true}
[dev-dependencies]
enum-iterator = "0.6.0"
@ -48,4 +57,5 @@ with_ctap1 = ["ctap-crypto/with_ctap1"]
with_ctap2_1 = []
autotest = []
multireset = [] # allows credential reset more than once per boot
default = ["with_ctap1", "with_ctap2_1"]
default = ["with_ctap1", "with_ctap2_1", "testing"]
testing = ["random-pick"]

View file

@ -1,3 +1,4 @@
use core::convert::TryFrom;
use std::thread;
use gam::TextEntryPayload;
use std::sync::{Arc, Mutex, atomic::{AtomicBool, Ordering}};
@ -11,13 +12,19 @@ use chrono::{Utc, DateTime, NaiveDateTime};
use std::time::{SystemTime, UNIX_EPOCH};
use std::cell::RefCell;
use crate::ux::ListItem;
use crate::{ux::{ListItem, deserialize_app_info}, ctap::FIDO_CRED_DICT};
use crate::{VaultMode, SelectedEntry};
use crate::fido::U2F_APP_DICT;
use crate::totp::TotpAlgorithm;
pub(crate) const VAULT_PASSWORD_DICT: &'static str = "vault.passwords";
pub(crate) const VAULT_TOTP_DICT: &'static str = "vault.totp";
/// bytes to reserve for a key entry. Making this slightly larger saves on some churn as stuff gets updated
pub(crate) const VAULT_ALLOC_HINT: usize = 256;
pub(crate) const VAULT_TOTP_ALLOC_HINT: usize = 128;
const VAULT_PASSWORD_REC_VERSION: u32 = 1;
const VAULT_TOTP_REC_VERSION: u32 = 1;
/// time allowed between dialog box swaps for background operations to redraw
const SWAP_DELAY_MS: usize = 300;
@ -32,6 +39,18 @@ pub(crate) struct PasswordRecord {
pub count: u64,
}
pub(crate) struct TotpRecord {
pub version: u32,
// as base32
pub secret: String,
pub name: String,
pub algorithm: TotpAlgorithm,
pub notes: String,
pub digits: u32,
pub timestep: u64,
pub ctime: u64,
}
#[derive(Debug, num_derive::FromPrimitive, num_derive::ToPrimitive)]
pub(crate) enum ActionOp {
/// Menu items
@ -42,6 +61,9 @@ pub(crate) enum ActionOp {
/// Internal ops
UpdateMode,
Quit,
#[cfg(feature="testing")]
/// Testing
GenerateTests,
}
pub(crate) fn start_actions_thread(
@ -96,6 +118,11 @@ pub(crate) fn start_actions_thread(
None => {
log::error!("msg could not be decoded {:?}", msg);
}
#[cfg(feature="testing")]
Some(ActionOp::GenerateTests) => {
manager.populate_tests();
manager.retrieve_db();
}
}
}
xous::destroy_server(sid).ok();
@ -331,7 +358,7 @@ impl ActionManager {
let dict = match entry.mode {
VaultMode::Password => VAULT_PASSWORD_DICT,
VaultMode::Fido => crate::fido::U2F_APP_DICT,
VaultMode::Totp => unimplemented!(),
VaultMode::Totp => VAULT_TOTP_DICT,
};
match self.pddb.borrow().delete_key(dict, entry.key_name.as_str().unwrap_or("UTF8-error"), None) {
Ok(_) => {
@ -647,36 +674,329 @@ impl ActionManager {
}
}
VaultMode::Fido => {
il.push(ListItem { name: "test.com".to_string(), extra: "Used 5 mins ago".to_string(), dirty: true, guid: self.gen_guid() });
il.push(ListItem { name: "google.com".to_string(), extra: "Never used".to_string(), dirty: true, guid: self.gen_guid() });
il.push(ListItem { name: "my app".to_string(), extra: "Used 2 hours ago".to_string(), dirty: true, guid: self.gen_guid() });
il.push(ListItem { name: "💎🙌".to_string(), extra: "Used 2 days ago".to_string(), dirty: true, guid: self.gen_guid() });
il.push(ListItem { name: "百度".to_string(), extra: "Used 1 month ago".to_string(), dirty: true, guid: self.gen_guid() });
il.push(ListItem { name: "duplicate.com".to_string(), extra: "Used 1 week ago".to_string(), dirty: true, guid: self.gen_guid() });
il.push(ListItem { name: "duplicate.com".to_string(), extra: "Used 8 mins ago".to_string(), dirty: true, guid: self.gen_guid() });
il.push(ListItem { name: "amawhat.com".to_string(), extra: "Used 6 days ago".to_string(), dirty: true, guid: self.gen_guid() });
il.push(ListItem { name: "amazon.com".to_string(), extra: "Used 3 days ago".to_string(), dirty: true, guid: self.gen_guid() });
il.push(ListItem { name: "amazingcode.org".to_string(), extra: "Never used".to_string(), dirty: true, guid: self.gen_guid() });
il.push(ListItem { name: "ziggyziggyziggylongdomain.com".to_string(), extra: "Never used".to_string(), dirty: true, guid: self.gen_guid() });
il.push(ListItem { name: "another long domain name.com".to_string(), extra: "Used 2 months ago".to_string(), dirty: true, guid: self.gen_guid() });
il.push(ListItem { name: "bunniestudios.com".to_string(), extra: "Used 30 mins ago".to_string(), dirty: true, guid: self.gen_guid() });
il.push(ListItem { name: "github.com".to_string(), extra: "Used 6 hours ago".to_string(), dirty: true, guid: self.gen_guid() });
// first assemble U2F records
let keylist = match self.pddb.borrow().list_keys(U2F_APP_DICT, None) {
Ok(keylist) => keylist,
Err(e) => {
log::error!("error accessing password database: {:?}", e);
Vec::new()
}
};
for key in keylist {
match self.pddb.borrow().get(
U2F_APP_DICT,
&key,
None,
false, false, None,
Some(crate::basis_change)
) {
Ok(mut record) => {
let mut data = Vec::<u8>::new();
match record.read_to_end(&mut data) {
Ok(_len) => {
if let Some(ai) = deserialize_app_info(data) {
let extra = format!("{}; {}{}",
crate::ux::atime_to_str(ai.atime),
t!("vault.u2f.appinfo.authcount", xous::LANG),
ai.count,
);
let desc = format!("{}", ai.name);
let li = ListItem {
name: desc,
extra,
dirty: true,
guid: key,
};
il.push(li);
} else {
log::error!("couldn't deserialize {}", key);
}
}
Err(e) => log::error!("couldn't access key {}: {:?}", key, e),
}
}
Err(e) => log::error!("couldn't access key {}: {:?}", key, e),
}
}
let keylist = match self.pddb.borrow().list_keys(FIDO_CRED_DICT, None) {
Ok(keylist) => keylist,
Err(e) => {
log::error!("error accessing password database: {:?}", e);
Vec::new()
}
};
// now merge in the FIDO2 records
for key in keylist {
match self.pddb.borrow().get(
FIDO_CRED_DICT,
&key,
None,
false, false, None,
Some(crate::basis_change)
) {
Ok(mut record) => {
let mut data = Vec::<u8>::new();
match record.read_to_end(&mut data) {
Ok(_len) => {
match crate::ctap::storage::deserialize_credential(&data) {
Some(result) => {
let name = if let Some(display_name) = result.user_display_name {
display_name
} else {
String::from_utf8(result.user_handle).unwrap_or("".to_string())
};
let desc = format!("{} / {}", result.rp_id, String::from_utf8(result.credential_id).unwrap_or("---".to_string()));
let extra = format!("FIDO2 {}", name);
let li = ListItem {
name: desc,
extra,
dirty: true,
guid: key,
};
il.push(li);
}
None => {
log::error!("couldn't deserialize {}", key);
}
}
}
Err(e) => log::error!("couldn't access key {}: {:?}", key, e),
}
}
Err(e) => log::error!("couldn't access key {}: {:?}", key, e),
}
}
}
VaultMode::Totp => {
il.push(ListItem { name: "gmail.com".to_string(), extra: "162 321".to_string(), dirty: true, guid: self.gen_guid() });
il.push(ListItem { name: "google.com".to_string(), extra: "445 768".to_string(), dirty: true, guid: self.gen_guid() });
il.push(ListItem { name: "my 图片 app".to_string(), extra: "982 111".to_string(), dirty: true, guid: self.gen_guid() });
il.push(ListItem { name: "🍕🍔🍟🌭".to_string(), extra: "056 182".to_string(), dirty: true, guid: self.gen_guid() });
il.push(ListItem { name: "百度".to_string(), extra: "111 111".to_string(), dirty: true, guid: self.gen_guid() });
il.push(ListItem { name: "duplicate.com".to_string(), extra: "462 124".to_string(), dirty: true, guid: self.gen_guid() });
il.push(ListItem { name: "duplicate.com".to_string(), extra: "462 124".to_string(), dirty: true, guid: self.gen_guid() });
il.push(ListItem { name: "amazon.com".to_string(), extra: "842 012".to_string(), dirty: true, guid: self.gen_guid() });
il.push(ListItem { name: "ziggyziggyziggylongdomain.com".to_string(), extra: "462 212".to_string(), dirty: true, guid: self.gen_guid() });
il.push(ListItem { name: "github.com".to_string(), extra: "Used 6 hours ago".to_string(), dirty: true, guid: self.gen_guid() });
let keylist = match self.pddb.borrow().list_keys(VAULT_TOTP_DICT, None) {
Ok(keylist) => keylist,
Err(e) => {
log::error!("error accessing password database: {:?}", e);
Vec::new()
}
};
for key in keylist {
match self.pddb.borrow().get(
VAULT_TOTP_DICT,
&key,
None,
false, false, None,
Some(crate::basis_change)
) {
Ok(mut record) => {
let mut data = Vec::<u8>::new();
match record.read_to_end(&mut data) {
Ok(_len) => {
if let Some(totp) = deserialize_totp(data) {
let extra = totp.secret;
let desc = format!("{}", totp.name);
let li = ListItem {
name: desc,
extra,
dirty: true,
guid: key,
};
il.push(li);
} else {
log::error!("couldn't deserialize {}", key);
}
}
Err(e) => log::error!("couldn't access key {}: {:?}", key, e),
}
}
Err(e) => log::error!("couldn't access key {}: {:?}", key, e),
}
}
}
}
il.sort();
}
#[cfg(feature="testing")]
pub(crate) fn populate_tests(&mut self) {
use crate::ux::serialize_app_info;
let words = [
"bunnie", "foo", "turtle.net", "Fox.ng", "Bear", "dog food", "Cat.com", "FUzzy", "1off", "www_test_site_com/long_name/stupid/foo.htm",
"._weird~yy%\":'test", "//WHYwhyWHY", "Xyz|zy", "foo:bar", "f🍕🍔🍟🌭d", "💎🙌", "some ノート", "笔录4u", "@u", "sane text", "Käsesoßenrührlöffel"];
let weights = [1; 21];
const TARGET_ENTRIES: usize = 35;
// for each database, populate up to TARGET_ENTRIES
// as this is testing code, it's written a bit more fragile in terms of error handling (fail-panic, rather than fail-dialog)
// --- passwords ---
let pws = self.pddb.borrow().list_keys(VAULT_PASSWORD_DICT, None).unwrap_or(Vec::new());
if pws.len() < TARGET_ENTRIES {
let extra_count = TARGET_ENTRIES - pws.len();
for _ in 0..extra_count {
let desc = random_pick::pick_multiple_from_slice(&words, &weights, 3);
let description = format!("{} {} {}", desc[0], desc[1], desc[2]);
let username = random_pick::pick_from_slice(&words, &weights).unwrap().to_string();
let notes = random_pick::pick_from_slice(&words, &weights).unwrap().to_string();
let pg = PasswordGenerator {
length: 20,
numbers: true,
lowercase_letters: true,
uppercase_letters: true,
symbols: true,
spaces: false,
exclude_similar_characters: true,
strict: true,
};
let password = pg.generate_one().unwrap();
let record = PasswordRecord {
version: VAULT_PASSWORD_REC_VERSION,
description,
username,
password,
notes,
ctime: utc_now().timestamp() as u64,
atime: 0,
count: 0,
};
let ser = serialize_password(&record);
let guid = self.gen_guid();
match self.pddb.borrow().get(
VAULT_PASSWORD_DICT,
&guid,
None, true, true,
Some(VAULT_ALLOC_HINT), Some(crate::basis_change)
) {
Ok(mut data) => {
match data.write(&ser) {
Ok(len) => log::debug!("wrote {} bytes", len),
Err(e) => {
self.modals.show_notification(&format!("{}\n{:?}",
t!("vault.error.internal_error", xous::LANG), e
), None).ok();
}
}
}
Err(e) => {
self.modals.show_notification(&format!("{}\n{:?}",
t!("vault.error.internal_error", xous::LANG), e
), None).ok();
}
}
}
}
// --- U2F + FIDO ---
let fido = self.pddb.borrow().list_keys(FIDO_CRED_DICT, None).unwrap_or(Vec::new());
let u2f = self.pddb.borrow().list_keys(U2F_APP_DICT, None).unwrap_or(Vec::new());
let total = fido.len() + u2f.len();
if total < TARGET_ENTRIES {
let extra_u2f = (TARGET_ENTRIES - total) / 2;
let extra_fido = TARGET_ENTRIES - extra_u2f;
for _ in 0..extra_u2f {
let n = random_pick::pick_multiple_from_slice(&words, &weights, 2);
let name = format!("{} {}", n[0], n[1]);
let notes = random_pick::pick_from_slice(&words, &weights).unwrap().to_string();
let mut id = [0u8; 32];
self.trng.borrow_mut().fill_bytes(&mut id);
let record = crate::AppInfo {
name,
id,
notes,
ctime: utc_now().timestamp() as u64,
atime: 0,
count: 0,
};
let ser = serialize_app_info(&record);
let app_id_str = hex::encode(id);
match self.pddb.borrow().get(
U2F_APP_DICT,
&app_id_str,
None, true, true,
Some(256), Some(crate::basis_change)
) {
Ok(mut app_data) => {
app_data.write(&ser).expect("couldn't create");
}
_ => log::error!("Error creating record"),
}
}
let xns = xous_names::XousNames::new().unwrap();
let mut rng = ctap_crypto::rng256::XousRng256::new(&xns);
for _ in 0..extra_fido {
use crate::ctap::data_formats::*;
let c_id = random_pick::pick_multiple_from_slice(&words, &weights, 2);
let cred_id = format!("{} {}", c_id[0], c_id[1]);
let r_id = random_pick::pick_multiple_from_slice(&words, &weights, 2);
let rp_id = format!("{} {}", r_id[0], r_id[1]);
let handle = random_pick::pick_from_slice(&words, &weights).unwrap().to_string();
let new_credential = PublicKeyCredentialSource {
key_type: PublicKeyCredentialType::PublicKey,
credential_id: cred_id.as_bytes().to_vec(),
private_key: ctap_crypto::ecdsa::SecKey::gensk(&mut rng),
rp_id,
user_handle: handle.as_bytes().to_vec(),
user_display_name: None,
cred_protect_policy: None,
creation_order: 0,
user_name: None,
user_icon: None,
};
let shortid = &cred_id;
match self.pddb.borrow().get(
FIDO_CRED_DICT,
shortid,
None, true, true,
Some(crate::ctap::storage::CRED_INITAL_SIZE), Some(crate::basis_change)
) {
Ok(mut cred) => {
let value = crate::ctap::storage::serialize_credential(new_credential).unwrap();
cred.write(&value).unwrap();
}
_ => log::error!("couldn't create FIDO2 credential")
}
}
}
// TOTP
let totp = self.pddb.borrow().list_keys(VAULT_TOTP_DICT, None).unwrap_or(Vec::new());
if totp.len() < TARGET_ENTRIES {
let extra = TARGET_ENTRIES - totp.len();
for _ in 0..extra {
let names = random_pick::pick_multiple_from_slice(&words, &weights, 3);
let name = format!("{} {} {}", names[0], names[1], names[2]);
let notes = random_pick::pick_from_slice(&words, &weights).unwrap().to_string();
let mut secret_bytes = [0u8; 10];
self.trng.borrow_mut().fill_bytes(&mut secret_bytes);
let record = TotpRecord {
version: VAULT_TOTP_REC_VERSION,
secret: base32::encode(base32::Alphabet::RFC4648 { padding: false }, &secret_bytes),
name,
algorithm: TotpAlgorithm::HmacSha1,
notes,
digits: 6,
timestep: 30,
ctime: utc_now().timestamp() as u64,
};
let ser = serialize_totp(&record);
let guid = self.gen_guid();
match self.pddb.borrow().get(
VAULT_TOTP_DICT,
&guid,
None, true, true,
Some(VAULT_TOTP_ALLOC_HINT), Some(crate::basis_change)
) {
Ok(mut data) => {
match data.write(&ser) {
Ok(len) => log::debug!("wrote {} bytes", len),
Err(e) => {
self.modals.show_notification(&format!("{}\n{:?}",
t!("vault.error.internal_error", xous::LANG), e
), None).ok();
}
}
}
Err(e) => {
self.modals.show_notification(&format!("{}\n{:?}",
t!("vault.error.internal_error", xous::LANG), e
), None).ok();
}
}
}
}
self.pddb.borrow().sync().ok();
}
}
@ -766,12 +1086,94 @@ pub(crate) fn deserialize_password(data: Vec::<u8>) -> Option<PasswordRecord> {
}
}
_ => {
log::warn!("unexpected tag {} encountered parsing app info, aborting", tag);
return None;
log::warn!("unexpected tag {} encountered parsing password info, ignoring", tag);
}
}
} else {
log::debug!("invalid line skipped: {:?}", line);
log::trace!("invalid line skipped: {:?}", line);
}
}
Some(pr)
} else {
None
}
}
pub(crate) fn serialize_totp<'a>(record: &TotpRecord) -> Vec::<u8> {
let ta: String = record.algorithm.into();
format!("{}:{}\n{}:{}\n{}:{}\n{}:{}\n{}:{}\n{}:{}\n{}:{}\n{}:{}\n",
"version", record.version,
"secret", record.secret,
"name", record.name,
"algorithm", ta,
"notes", record.notes,
"digits", record.digits,
"timestep", record.timestep,
"ctime", record.ctime,
).into_bytes()
}
pub(crate) fn deserialize_totp(data: Vec::<u8>) -> Option<TotpRecord> {
if let Ok(desc_str) = String::from_utf8(data) {
let mut pr = TotpRecord {
version: 0,
secret: String::new(),
name: String::new(),
algorithm: TotpAlgorithm::HmacSha1,
notes: String::new(),
digits: 0,
ctime: 0,
timestep: 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 None;
}
}
"secret" => pr.secret.push_str(data),
"name" => pr.name.push_str(data),
"algorithm" => pr.algorithm = match TotpAlgorithm::try_from(data) {
Ok(a) => a,
Err(_) => return None
},
"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 None;
}
}
"ctime" => {
if let Ok(ctime) = u64::from_str_radix(data, 10) {
pr.ctime = ctime;
} else {
log::warn!("ctime error");
return None;
}
}
"timestep" => {
if let Ok(timestep) = u64::from_str_radix(data, 10) {
pr.timestep = timestep;
} else {
log::warn!("timestep error");
return None;
}
}
_ => {
log::warn!("unexpected tag {} encountered parsing TOTP info, ignoring", tag);
}
}
} else {
log::trace!("invalid line skipped: {:?}", line);
}
}
Some(pr)

View file

@ -19,6 +19,7 @@ mod shims;
use shims::*;
mod submenu;
mod actions;
mod totp;
use locales::t;
@ -80,6 +81,25 @@ UI concept:
- Icontray thread: a simple server that serves as a shim between the IME structure and this to create an icontray function
*/
// TOTP test I65VU7K5ZQL7WB4E https://authenticationtest.com/totpChallenge/
// otpauth-migration://offline?data=Ci8KCke7Wn1dzBf7B4QSG3RvdHBAYXV0aGVudGljYXRpb250ZXN0LmNvbSABKAEwAhABGAEgACjh8Yv%2B%2BP%2F%2F%2F%2F8B
/*
otp_parameters {
secret: "G\273Z}]\314\027\373\007\204"
name: "totp@authenticationtest.com"
algorithm: SHA1
digits: SIX
type: TOTP
}
version: 1
batch_size: 1
batch_index: 0
batch_id: -1883047711
>>> b32decode("I65VU7K5ZQL7WB4E").hex()
'47bb5a7d5dcc17fb0784'
*/
#[derive(Debug, num_derive::FromPrimitive, num_derive::ToPrimitive)]
pub(crate) enum VaultOp {
/// a line of text has arrived

View file

@ -29,6 +29,14 @@ pub fn create_submenu(vault_conn: xous::CID, actions_conn: xous::CID, menu_mgr:
action_payload: MenuPayload::Scalar([0, 0, 0, 0]),
close_on_select: true,
});
#[cfg(feature="testing")]
menu_items.push(MenuItem {
name: xous_ipc::String::from_str("Generate test vectors"),
action_conn: Some(actions_conn),
action_opcode: ActionOp::GenerateTests.to_u32().unwrap(),
action_payload: MenuPayload::Scalar([0, 0, 0, 0]),
close_on_select: true,
});
menu_items.push(MenuItem {
name: xous_ipc::String::from_str(t!("vault.menu_close", xous::LANG)),
action_conn: Some(actions_conn),

View file

@ -554,7 +554,7 @@ pub(crate) fn deserialize_app_info(descriptor: Vec::<u8>) -> Option::<AppInfo> {
}
}
} else {
log::debug!("invalid line skipped: {:?}", line);
log::trace!("invalid line skipped: {:?}", line);
}
}
if appinfo.name.len() > 0