mirror of
https://github.com/saymrwulf/xous-core.git
synced 2026-11-14 08:03:34 +00:00
502 lines
No EOL
21 KiB
Rust
502 lines
No EOL
21 KiB
Rust
#![cfg_attr(target_os = "none", no_std)]
|
|
#![cfg_attr(target_os = "none", no_main)]
|
|
|
|
mod ux;
|
|
use ux::*;
|
|
use num_traits::*;
|
|
use xous_ipc::Buffer;
|
|
use xous::{send_message, Message};
|
|
use usbd_human_interface_device::device::fido::*;
|
|
use std::thread;
|
|
use core::sync::atomic::{AtomicU32, AtomicBool, Ordering};
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
mod ctap;
|
|
use ctap::hid::{ChannelID, CtapHid};
|
|
use ctap::status_code::Ctap2StatusCode;
|
|
use ctap::CtapState;
|
|
mod shims;
|
|
use shims::*;
|
|
mod submenu;
|
|
mod actions;
|
|
mod totp;
|
|
mod prereqs;
|
|
|
|
use locales::t;
|
|
|
|
use framework::ListItem;
|
|
use actions::{ActionOp, start_actions_thread};
|
|
|
|
use crate::prereqs::ntp_updater;
|
|
|
|
// CTAP2 testing notes:
|
|
// run our branch and use this to forward the prompts on to the device:
|
|
// netcat -k -u -l 6502 > /dev/ttyS0
|
|
// use the "autotest" feature to remove some excess prompts that interfere with the test
|
|
|
|
// the OpenSK code is based off of commit f2496a8e6d71a4e838884996a1c9b62121f87df2 from the
|
|
// Google OpenSK repository. The last push was Nov 19 2021, and the initial merge into Xous
|
|
// was finished on June 9 2022. Any patches to this code base will have to be manually
|
|
// applied. Please update the information here to reflect the latest patch status.
|
|
|
|
/*
|
|
UI concept:
|
|
|
|
|-----------------|
|
|
| |
|
|
| List view |
|
|
| area |
|
|
| |
|
|
| |
|
|
| |
|
|
| |
|
|
| |
|
|
|-----------------|
|
|
| List filter |
|
|
|-----------------|
|
|
|F1 | F2 | F3 | F4|
|
|
|-----------------|
|
|
|
|
F1-F4: switch between functions using F-keys. Functions are:
|
|
- FIDO2 (U2F authenicators)
|
|
- TOTP (time based authenticators)
|
|
- Vault (passwords)
|
|
- Prefs (preferences)
|
|
Tap once to switch to the sub-function.
|
|
Once on the sub-function, tap the corresponding F-key again to raise
|
|
the menu for that sub-function.
|
|
|
|
List filter:
|
|
- Any regular keys hit here appear in the search input. It automatically
|
|
filters the content in the list view area to the set of strings that match
|
|
the search input
|
|
|
|
Up/down arrow: picks a list view item
|
|
Left/right arrow: moves up or down the list view in pages
|
|
Enter: picks the selected list view
|
|
Select: *alaways* raises system 'main menu'
|
|
|
|
Organization:
|
|
- Main thread (vaultux object): "responsive" UI operations - must always be able to respond to redraw commands.
|
|
operates on lists of data shared between main & actions thread
|
|
- Actions thread (actions object): "blocking" UI operations - manages multi-sequence dialog queries, database access
|
|
- Fido thread: handles USB interactions. Can always pop up a dialog box, but it cannot override a dialog-in-progress.
|
|
- 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'
|
|
|
|
To clear test entries:
|
|
pddb dictdelete vault.passwords
|
|
pddb dictdelete vault.totp
|
|
pddb dictdelete fido.cred
|
|
pddb dictdelete fido.u2fapps
|
|
|
|
*/
|
|
|
|
#[derive(Debug, num_derive::FromPrimitive, num_derive::ToPrimitive)]
|
|
pub(crate) enum VaultOp {
|
|
/// a line of text has arrived
|
|
Line = 0, // make sure we occupy opcodes with discriminants < 1000, as the rest are used for callbacks
|
|
/// incremental line of text
|
|
IncrementalLine,
|
|
/// redraw our UI
|
|
Redraw,
|
|
/// ignore dirty rectangles and redraw everything
|
|
FullRedraw,
|
|
/// change focus
|
|
ChangeFocus,
|
|
|
|
/// Partial menu
|
|
MenuChangeFont,
|
|
MenuDeleteStage1,
|
|
MenuEditStage1,
|
|
MenuAutotype,
|
|
MenuReadoutMode,
|
|
|
|
/// PDDB basis change
|
|
BasisChange,
|
|
|
|
/// Nop while waiting for prerequisites to be filled
|
|
Nop,
|
|
|
|
/// exit the application
|
|
Quit,
|
|
}
|
|
|
|
#[derive(Copy, Clone, PartialEq, Eq, Debug, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
|
|
enum VaultMode {
|
|
Fido,
|
|
Totp,
|
|
Password,
|
|
}
|
|
|
|
#[derive(Debug, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize, Copy, Clone)]
|
|
struct SelectedEntry {
|
|
key_name: xous_ipc::String::<256>,
|
|
description: xous_ipc::String::<256>,
|
|
mode: VaultMode,
|
|
}
|
|
|
|
static SELF_CONN: AtomicU32 = AtomicU32::new(0);
|
|
const ERR_TIMEOUT_MS: usize = 5000;
|
|
|
|
fn main() -> ! {
|
|
log_server::init_wait().unwrap();
|
|
log::set_max_level(log::LevelFilter::Info);
|
|
log::info!("my PID is {}", xous::process::id());
|
|
|
|
let xns = xous_names::XousNames::new().unwrap();
|
|
let sid = xous::create_server().unwrap();
|
|
// TODO: fix this so there is a uniform public API for the time server
|
|
let time_conn = xous::connect(xous::SID::from_bytes(b"timeserverpublic").unwrap()).unwrap();
|
|
|
|
start_fido_ux_thread();
|
|
|
|
// global shared state between threads.
|
|
let mode = Arc::new(Mutex::new(VaultMode::Fido));
|
|
let item_list = Arc::new(Mutex::new(Vec::<ListItem>::new()));
|
|
let action_active = Arc::new(AtomicBool::new(false));
|
|
|
|
// spawn the actions server. This is responsible for grooming the UX elements. It
|
|
// has to be in its own thread because it uses blocking modal calls that would cause
|
|
// redraws of the background list to block/fail.
|
|
let actions_sid = xous::create_server().unwrap();
|
|
let conn = xous::connect(sid).unwrap();
|
|
SELF_CONN.store(conn, Ordering::SeqCst);
|
|
start_actions_thread(conn, actions_sid, mode.clone(), item_list.clone(), action_active.clone());
|
|
let actions_conn = xous::connect(actions_sid).unwrap();
|
|
|
|
// spawn the FIDO2 USB handler
|
|
let _ = thread::spawn({
|
|
move || {
|
|
let xns = xous_names::XousNames::new().unwrap();
|
|
let tt = ticktimer_server::Ticktimer::new().unwrap();
|
|
let boot_time = ClockValue::new(tt.elapsed_ms() as i64, 1000);
|
|
|
|
let mut rng = ctap_crypto::rng256::XousRng256::new(&xns);
|
|
// this call will block until the PDDB is mounted.
|
|
let usb = usb_device_xous::UsbHid::new();
|
|
// only run the main loop if the SoC is compatible
|
|
if usb.is_soc_compatible() {
|
|
let mut ctap_state = CtapState::new(&mut rng, check_user_presence, boot_time);
|
|
let mut ctap_hid = CtapHid::new();
|
|
loop {
|
|
match usb.u2f_wait_incoming() {
|
|
Ok(msg) => {
|
|
log::trace!("FIDO listener got message: {:?}", msg);
|
|
let now = ClockValue::new(tt.elapsed_ms() as i64, 1000);
|
|
let reply = ctap_hid.process_hid_packet(&msg.packet, now, &mut ctap_state);
|
|
// This block handles sending packets.
|
|
for pkt_reply in reply {
|
|
let mut reply = RawFidoMsg::default();
|
|
reply.packet.copy_from_slice(&pkt_reply);
|
|
let status = usb.u2f_send(reply);
|
|
match status {
|
|
Ok(()) => {
|
|
log::trace!("Sent U2F packet");
|
|
}
|
|
Err(e) => {
|
|
log::error!("Error sending U2F packet: {:?}", e);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Err(e) => {
|
|
match e {
|
|
xous::Error::ProcessTerminated => { // unplug happened, reset the authenticator
|
|
log::info!("CTAP unplug_reset");
|
|
ctap_state.unplug_reset();
|
|
},
|
|
_ => {
|
|
log::warn!("FIDO listener got an error: {:?}", e);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
log::warn!("SoC rev is incompatible with USB HID operations, the U2F/FIDO2 server is not started");
|
|
}
|
|
}
|
|
});
|
|
|
|
// spawn the icontray handler
|
|
let _ = thread::spawn({
|
|
move || {
|
|
icontray_server(conn);
|
|
}
|
|
});
|
|
// spawn the TOTP pumper
|
|
let pump_sid = xous::create_server().unwrap();
|
|
crate::totp::pumper(mode.clone(), pump_sid, conn);
|
|
let pump_conn = xous::connect(pump_sid).unwrap();
|
|
|
|
let menu_sid = xous::create_server().unwrap();
|
|
let menu_mgr = submenu::create_submenu(conn, actions_conn, menu_sid);
|
|
|
|
// this will block all initialization until the prereqs are met
|
|
let (token, mut allow_redraw) = prereqs::prereqs(sid, time_conn);
|
|
let mut vaultux = VaultUx::new(token, &xns, sid, menu_mgr, actions_conn, mode.clone(), item_list, action_active.clone());
|
|
// Trigger the mode update in the actions
|
|
send_message(actions_conn,
|
|
Message::new_blocking_scalar(ActionOp::UpdateMode.to_usize().unwrap(), 0, 0, 0, 0)
|
|
).ok();
|
|
vaultux.update_mode();
|
|
vaultux.get_glyph_style();
|
|
|
|
// starts a thread to keep NTP up-to-date
|
|
ntp_updater(time_conn);
|
|
|
|
let modals = modals::Modals::new(&xns).unwrap();
|
|
let tt = ticktimer_server::Ticktimer::new().unwrap();
|
|
loop {
|
|
let msg = xous::receive_message(sid).unwrap();
|
|
let opcode: Option<VaultOp> = FromPrimitive::from_usize(msg.body.id());
|
|
log::debug!("{:?}", opcode);
|
|
match opcode {
|
|
Some(VaultOp::IncrementalLine) => {
|
|
if action_active.load(Ordering::SeqCst) {
|
|
log::trace!("action active, skipping incremental input");
|
|
send_message(conn,
|
|
Message::new_scalar(VaultOp::Redraw.to_usize().unwrap(), 0, 0, 0, 0)
|
|
).ok();
|
|
continue;
|
|
}
|
|
let buffer = unsafe { Buffer::from_memory_message(msg.body.memory_message().unwrap()) };
|
|
let s = buffer.as_flat::<xous_ipc::String<4000>, _>().unwrap();
|
|
log::debug!("Incremental input: {}", s.as_str());
|
|
vaultux.input(s.as_str()).expect("Vault couldn't accept input string");
|
|
send_message(conn,
|
|
Message::new_scalar(VaultOp::Redraw.to_usize().unwrap(), 0, 0, 0, 0)
|
|
).ok();
|
|
}
|
|
Some(VaultOp::Line) => {
|
|
if action_active.load(Ordering::SeqCst) {
|
|
log::trace!("action active, skipping line input");
|
|
send_message(conn,
|
|
Message::new_scalar(VaultOp::Redraw.to_usize().unwrap(), 0, 0, 0, 0)
|
|
).ok();
|
|
continue;
|
|
}
|
|
let buffer = unsafe { Buffer::from_memory_message(msg.body.memory_message().unwrap()) };
|
|
let s = buffer.as_flat::<xous_ipc::String<4000>, _>().unwrap();
|
|
log::debug!("vaultux got input line: {}", s.as_str());
|
|
match s.as_str() {
|
|
"\u{0011}" => {
|
|
*mode.lock().unwrap() = VaultMode::Fido;
|
|
send_message(actions_conn,
|
|
Message::new_blocking_scalar(ActionOp::UpdateMode.to_usize().unwrap(), 0, 0, 0, 0)
|
|
).ok();
|
|
vaultux.update_mode();
|
|
}
|
|
"\u{0012}" => {
|
|
*mode.lock().unwrap() = VaultMode::Totp;
|
|
send_message(actions_conn,
|
|
Message::new_blocking_scalar(ActionOp::UpdateMode.to_usize().unwrap(), 0, 0, 0, 0)
|
|
).ok();
|
|
vaultux.update_mode();
|
|
// this will start a periodic pump to keep the UX updating
|
|
send_message(pump_conn,
|
|
Message::new_scalar(totp::PumpOp::Pump.to_usize().unwrap(), 0, 0, 0, 0)
|
|
).ok();
|
|
}
|
|
"\u{0013}" => {
|
|
*mode.lock().unwrap() = VaultMode::Password;
|
|
send_message(actions_conn,
|
|
Message::new_blocking_scalar(ActionOp::UpdateMode.to_usize().unwrap(), 0, 0, 0, 0)
|
|
).ok();
|
|
vaultux.update_mode();
|
|
}
|
|
"\u{0014}" => {
|
|
vaultux.raise_menu();
|
|
}
|
|
"↓" => {
|
|
vaultux.nav(NavDir::Down);
|
|
}
|
|
"↑" => {
|
|
vaultux.nav(NavDir::Up);
|
|
}
|
|
"←" => {
|
|
vaultux.nav(NavDir::PageUp);
|
|
}
|
|
"→" => {
|
|
vaultux.nav(NavDir::PageDown);
|
|
}
|
|
_ => {
|
|
// someone hit enter. The string is the whole search query, but what we care is that someone hit enter.
|
|
vaultux.raise_menu();
|
|
}
|
|
}
|
|
send_message(conn,
|
|
Message::new_scalar(VaultOp::Redraw.to_usize().unwrap(), 0, 0, 0, 0)
|
|
).ok();
|
|
}
|
|
Some(VaultOp::Redraw) => {
|
|
if allow_redraw {
|
|
vaultux.redraw().expect("Vault couldn't redraw");
|
|
}
|
|
}
|
|
Some(VaultOp::FullRedraw) => {
|
|
vaultux.update_mode();
|
|
if allow_redraw {
|
|
vaultux.redraw().expect("Vault couldn't redraw");
|
|
}
|
|
}
|
|
Some(VaultOp::BasisChange) => {
|
|
// this set of calls will effectively force a reload of any UX data
|
|
*mode.lock().unwrap() = VaultMode::Fido;
|
|
send_message(actions_conn,
|
|
Message::new_blocking_scalar(ActionOp::UpdateMode.to_usize().unwrap(), 0, 0, 0, 0)
|
|
).ok();
|
|
vaultux.update_mode();
|
|
vaultux.input("").unwrap();
|
|
send_message(conn,
|
|
Message::new_scalar(VaultOp::Redraw.to_usize().unwrap(), 0, 0, 0, 0)
|
|
).ok();
|
|
}
|
|
Some(VaultOp::ChangeFocus) => xous::msg_scalar_unpack!(msg, new_state_code, _, _, _, {
|
|
let new_state = gam::FocusState::convert_focus_change(new_state_code);
|
|
vaultux.change_focus_to(&new_state);
|
|
log::debug!("change focus: {:?}", new_state);
|
|
match new_state {
|
|
gam::FocusState::Background => {
|
|
allow_redraw = false;
|
|
}
|
|
gam::FocusState::Foreground => {
|
|
// HID is always selected if the vault is foregrounded
|
|
vaultux.ensure_hid();
|
|
allow_redraw = true;
|
|
}
|
|
}
|
|
}),
|
|
Some(VaultOp::MenuChangeFont) => {
|
|
for item in FONT_LIST {
|
|
modals
|
|
.add_list_item(item)
|
|
.expect("couldn't build radio item list");
|
|
}
|
|
match modals.get_radiobutton(t!("vault.select_font", xous::LANG)) {
|
|
Ok(style) => {
|
|
vaultux.set_glyph_style(name_to_style(&style).unwrap_or(DEFAULT_FONT));
|
|
},
|
|
_ => log::error!("get_radiobutton failed"),
|
|
}
|
|
vaultux.update_mode();
|
|
}
|
|
Some(VaultOp::MenuAutotype) => {
|
|
modals.dynamic_notification(Some(t!("vault.autotyping", xous::LANG)), None).ok();
|
|
match vaultux.autotype() {
|
|
Err(xous::Error::UseBeforeInit) => { // USB not plugged in
|
|
modals.dynamic_notification_update(Some(t!("vault.error.usb_error", xous::LANG)), None).ok();
|
|
tt.sleep_ms(ERR_TIMEOUT_MS).unwrap();
|
|
},
|
|
Err(xous::Error::InvalidString) => { // deserialzation error
|
|
modals.dynamic_notification_update(Some(t!("vault.error.record_error", xous::LANG)), None).ok();
|
|
tt.sleep_ms(ERR_TIMEOUT_MS).unwrap();
|
|
},
|
|
Err(xous::Error::ProcessNotFound) => { // key or dictionary not found
|
|
modals.dynamic_notification_update(Some(t!("vault.error.not_found", xous::LANG)), None).ok();
|
|
tt.sleep_ms(ERR_TIMEOUT_MS).unwrap();
|
|
},
|
|
Err(xous::Error::InvalidPID) => { // nothing was selected
|
|
modals.dynamic_notification_update(Some(t!("vault.error.nothing_selected", xous::LANG)), None).ok();
|
|
tt.sleep_ms(ERR_TIMEOUT_MS).unwrap();
|
|
},
|
|
Err(xous::Error::OutOfMemory) => { // trouble updating the key
|
|
modals.dynamic_notification_update(Some(t!("vault.error.update_error", xous::LANG)), None).ok();
|
|
tt.sleep_ms(ERR_TIMEOUT_MS).unwrap();
|
|
}
|
|
Ok(_) => {},
|
|
Err(e) => { // unknown error
|
|
modals.dynamic_notification(Some(
|
|
&format!("{}\n{:?}", t!("vault.error.internal_error", xous::LANG), e),
|
|
), None).ok();
|
|
tt.sleep_ms(ERR_TIMEOUT_MS).unwrap();
|
|
}
|
|
}
|
|
modals.dynamic_notification_close().ok();
|
|
}
|
|
Some(VaultOp::MenuDeleteStage1) => {
|
|
// stage 1 happens here because the filtered list and selection entry are in the responsive UX section.
|
|
if let Some(entry) = vaultux.selected_entry() {
|
|
let buf = Buffer::into_buf(entry).expect("IPC error");
|
|
buf.send(actions_conn, ActionOp::MenuDeleteStage2.to_u32().unwrap()).expect("messaging error");
|
|
} else {
|
|
// this will block redraws, but it's just one notification in a sequence so it's OK.
|
|
modals.show_notification(t!("vault.error.nothing_selected", xous::LANG), None).ok();
|
|
}
|
|
}
|
|
Some(VaultOp::MenuEditStage1) => {
|
|
// stage 1 happens here because the filtered list and selection entry are in the responsive UX section.
|
|
if let Some(entry) = vaultux.selected_entry() {
|
|
let buf = Buffer::into_buf(entry).expect("IPC error");
|
|
buf.send(actions_conn, ActionOp::MenuEditStage2.to_u32().unwrap()).expect("messaging error");
|
|
} else {
|
|
// this will block redraws, but it's just one notification in a sequence so it's OK.
|
|
modals.show_notification(t!("vault.error.nothing_selected", xous::LANG), None).ok();
|
|
}
|
|
}
|
|
Some(VaultOp::MenuReadoutMode) => {
|
|
modals.dynamic_notification(Some(t!("vault.readout_switchover", xous::LANG)), None).ok();
|
|
vaultux.readout_mode(true);
|
|
modals.dynamic_notification_close().ok();
|
|
|
|
// TODO: rethink the integration of this menu item. The problem is that if you're in readout
|
|
// mode and the "vault" mode is active, the "pumper" thread keeps sending messages to redraw
|
|
// the TOTP timer bar. This will eventually overflow this server's queue and cause a hang.
|
|
// However, the final integration for this will depend a bit on how the vault upload/download PR
|
|
// turns out.
|
|
modals.show_notification(t!("vault.readout_active", xous::LANG), None).ok();
|
|
// TODO: add some variable here to de-activate HID readout once this is done
|
|
|
|
modals.dynamic_notification(Some(t!("vault.readout_switchover", xous::LANG)), None).ok();
|
|
vaultux.readout_mode(false);
|
|
modals.dynamic_notification_close().ok();
|
|
}
|
|
Some(VaultOp::Quit) => {
|
|
log::error!("got Quit");
|
|
break;
|
|
}
|
|
Some(VaultOp::Nop) => {},
|
|
_ => {
|
|
log::trace!("got unknown message {:?}", msg);
|
|
}
|
|
}
|
|
log::trace!("reached bottom of main loop");
|
|
}
|
|
// clean up our program
|
|
log::error!("main loop exit, destroying servers");
|
|
xns.unregister_server(sid).unwrap();
|
|
xous::destroy_server(sid).unwrap();
|
|
log::trace!("quitting");
|
|
xous::terminate_process(0)
|
|
}
|
|
|
|
fn check_user_presence(_cid: ChannelID) -> Result<(), Ctap2StatusCode> {
|
|
log::warn!("check user presence called, but not implemented!");
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn basis_change() {
|
|
log::info!("got basis change");
|
|
xous::send_message(SELF_CONN.load(Ordering::SeqCst),
|
|
Message::new_scalar(VaultOp::BasisChange.to_usize().unwrap(), 0, 0, 0, 0)
|
|
).unwrap();
|
|
} |