rework password login to have a count and reboot/suspend after N retries

The implementation is a bit clumsy because the original
premise of the login loop is that it should be infalliable, and
because of the isolation of the password mechanism there isn't
a great way to pass number of failures back and forth across
that firewall (and I don't think there should be).

The application calling the password routine tracks failures
but the mounting routine has to kick that up to the main loop.
However, the PDDB main loop can't get stuck on the mount attempt
if you want to suspend, because it has to respond to the suspend
request.

So the failure message has to get kicked all the way back to the
status thread, which is what kicks off the mount, and then handled
there.
This commit is contained in:
bunnie 2022-10-17 02:40:51 +08:00
parent ec1d1b4444
commit de41e5ecff
6 changed files with 133 additions and 48 deletions

View file

@ -119,6 +119,12 @@
"zh": "密码错误。 再试一次?",
"en-tts": "Incorrect PIN. Try again?"
},
"pddb.failed_attempts": {
"en": "⚠{fails} failed login attempts",
"ja": "⚠{fails}回のログイン試行の失敗",
"zh": "⚠{fails}次失败的登录尝试",
"en-tts": "⚠{fails} failed login attempts"
},
"pddb.checkpass": {
"en": "Press any key, then re-enter your PIN to confirm.",
"ja": "任意キーを押して、パスワードを再入力してセットアップを確認してください。",

View file

@ -166,6 +166,8 @@ pub(crate) struct PddbOs {
entropy: Rc<RefCell<TrngPool>>,
/// connection to the password request manager
pw_cid: xous::CID,
/// Number of consecutive failed login attempts
failed_logins: u64,
#[cfg(all(feature="pddbtest", feature="autobasis"))]
testnames: HashSet::<String>,
}
@ -218,6 +220,7 @@ impl PddbOs {
dna_mode: DnaMode::Normal,
entropy: trngpool,
pw_cid,
failed_logins: 0,
#[cfg(all(feature="pddbtest", feature="autobasis"))]
testnames: HashSet::new(),
};
@ -246,6 +249,7 @@ impl PddbOs {
dna_mode: DnaMode::Normal,
entropy: trngpool,
pw_cid,
failed_logins: 0,
#[cfg(all(feature="pddbtest", feature="autobasis"))]
testnames: HashSet::new(),
}
@ -660,7 +664,8 @@ impl PddbOs {
}
_ => {
log::error!("Couldn't unwrap our system key: {:?}", e);
return PasswordState::Incorrect;
self.failed_logins = self.failed_logins.saturating_add(1);
return PasswordState::Incorrect(self.failed_logins);
}
}
}
@ -675,7 +680,8 @@ impl PddbOs {
}
_ => {
log::error!("Couldn't unwrap our system key: {:?}", e);
return PasswordState::Incorrect;
self.failed_logins = self.failed_logins.saturating_add(1);
return PasswordState::Incorrect(self.failed_logins);
}
}
}
@ -708,8 +714,10 @@ impl PddbOs {
for i in 0..syskey_pt.len() {
unsafe{nuke.add(i).write_volatile(0)};
}
self.failed_logins = 0;
PasswordState::Correct
} else {
self.failed_logins = 0;
PasswordState::Correct
}
}
@ -3098,11 +3106,12 @@ impl PddbOs {
self.dbg_dump(Some("migration".to_string()), Some(&export));
// indicate the migration worked
self.failed_logins = 0;
PasswordState::Correct
}
Err(e) => {
log::error!("Couldn't unwrap our system key: {:?}", e);
PasswordState::Incorrect
PasswordState::Incorrect(self.failed_logins)
}
}
}

View file

@ -135,38 +135,55 @@ impl Pddb {
self.cb_handle.replace(Some(handle));
}
}
/// This blocks until the PDDB is mounted by the end user. If `None` is specified for the poll_interval_ms,
/// A random interval between 1 and 2 seconds is chosen for the poll wait time. Randomizing the waiting time
/// helps to level out the task scheduler in the case that many threads are waiting on the PDDB simultaneously.
/// This blocks until the PDDB is mounted by the end user. It blocks using the deferred-response pattern,
/// so the blocking does not spin-wait; the caller does not consume CPU cycles.
///
/// This is typically the API call one would use to hold execution of a service until the PDDB is mounted.
pub fn is_mounted_blocking(&self) {
let ret = send_message(self.conn, Message::new_blocking_scalar(
Opcode::IsMounted.to_usize().unwrap(), 0, 0, 0, 0)).expect("couldn't execute IsMounted query");
match ret {
xous::Result::Scalar1(_code) => {
()
},
_ => panic!("Internal error"),
loop {
let ret = send_message(self.conn, Message::new_blocking_scalar(
Opcode::IsMounted.to_usize().unwrap(), 0, 0, 0, 0)).expect("couldn't execute IsMounted query");
match ret {
xous::Result::Scalar2(code, _count) => {
if code == 0 { // mounted successfully
break;
}
},
_ => panic!("Internal error"),
}
}
}
/// Attempts to mount the system basis. Returns `true` on success, `false` on failure.
/// This call may cause a password request box to pop up, in the case that the boot PIN is not currently cached.
pub fn try_mount(&self) -> bool {
///
/// Returns:
/// Ok(true) on successful mount
/// Ok(false) on user-directed abort of mount
/// Ok(usize) on system-forced abort of mount, with `count` failures
pub fn try_mount(&self) -> (bool, usize) {
let ret = send_message(self.conn, Message::new_blocking_scalar(
Opcode::TryMount.to_usize().unwrap(), 0, 0, 0, 0)).expect("couldn't execute IsMounted query");
Opcode::TryMount.to_usize().unwrap(), 0, 0, 0, 0)).expect("couldn't execute TryMounted query");
match ret {
xous::Result::Scalar1(code) => {
if code == 0 {false} else {true}
xous::Result::Scalar2(code, count) => {
if code == 0 {
// mounted successfully
(true, count)
} else if code == 1 {
// user aborted
(true, count)
} else {
// system aborted with `count` retries
(false, count)
}
},
_ => panic!("Internal error"),
_ => panic!("TryMount unexpected return result"),
}
}
/// Unmounts the PDDB. First attempts to unmount any open secret bases, and then finally unmounts
/// the system basis. Returns `true` on success, `false` on failure.
pub fn try_unmount(&self) -> bool {
let ret = send_message(self.conn, Message::new_blocking_scalar(
Opcode::TryUnmount.to_usize().unwrap(), 0, 0, 0, 0)).expect("couldn't execute IsMounted query");
Opcode::TryUnmount.to_usize().unwrap(), 0, 0, 0, 0)).expect("couldn't execute TryUnmount query");
match ret {
xous::Result::Scalar1(code) => {
if code == 0 {false} else {true}

View file

@ -406,7 +406,8 @@ pub(crate) struct BasisRequestPassword {
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum PasswordState {
Correct,
Incorrect,
Incorrect(u64),
ForcedAbort(u64),
Uninit,
}
@ -619,49 +620,52 @@ fn wrapped_main() -> ! {
}),
Opcode::IsMounted => xous::msg_blocking_scalar_unpack!(msg, _, _, _, _, {
if basis_cache.basis_count() > 0 { // if there's anything in the cache, we're mounted.
xous::return_scalar(msg.sender, 1).expect("couldn't return scalar");
xous::return_scalar2(msg.sender, 0, 0).expect("couldn't return scalar");
} else {
mount_notifications.push(msg.sender); // defer response until later
}
}),
Opcode::TryMount => xous::msg_blocking_scalar_unpack!(msg, _, _, _, _, {
if basis_cache.basis_count() > 0 {
xous::return_scalar(msg.sender, 1).expect("couldn't return scalar");
xous::return_scalar2(msg.sender, 0, 0).expect("couldn't return scalar");
} else {
if !pddb_os.rootkeys_initialized() {
// can't mount if we have no root keys
log::info!("{}PDDB.SKIPMOUNT,{}", xous::BOOKEND_START, xous::BOOKEND_END);
xous::return_scalar(msg.sender, 0).expect("could't return scalar");
xous::return_scalar2(msg.sender, 1, 0).expect("could't return scalar");
} else {
match ensure_password(&modals, &mut pddb_os, pw_cid) {
PasswordState::Correct => {
if try_mount_or_format(&modals, &mut pddb_os, &mut basis_cache, PasswordState::Correct, time_resetter) {
is_mounted.store(true, Ordering::SeqCst);
for requester in mount_notifications.drain(..) {
xous::return_scalar(requester, 1).expect("couldn't return scalar");
xous::return_scalar2(requester, 0, 0).expect("couldn't return scalar");
}
assert!(mount_notifications.len() == 0, "apparently I don't understand what drain() does");
log::info!("{}PDDB.MOUNTED,{}", xous::BOOKEND_START, xous::BOOKEND_END);
xous::return_scalar(msg.sender, 1).expect("couldn't return scalar");
xous::return_scalar2(msg.sender, 0, 0).expect("couldn't return scalar");
} else {
xous::return_scalar(msg.sender, 0).expect("couldn't return scalar");
xous::return_scalar2(msg.sender, 1, 0).expect("couldn't return scalar");
}
},
PasswordState::Uninit => {
if try_mount_or_format(&modals, &mut pddb_os, &mut basis_cache, PasswordState::Uninit, time_resetter) {
is_mounted.store(true, Ordering::SeqCst);
for requester in mount_notifications.drain(..) {
xous::return_scalar(requester, 1).expect("couldn't return scalar");
xous::return_scalar2(requester, 0, 0).expect("couldn't return scalar");
}
assert!(mount_notifications.len() == 0, "apparently I don't understand what drain() does");
log::info!("{}PDDB.MOUNTED,{}", xous::BOOKEND_START, xous::BOOKEND_END);
xous::return_scalar(msg.sender, 1).expect("couldn't return scalar");
xous::return_scalar2(msg.sender, 0, 0).expect("couldn't return scalar");
} else {
xous::return_scalar(msg.sender, 0).expect("couldn't return scalar");
xous::return_scalar2(msg.sender, 1, 0).expect("couldn't return scalar");
}
},
PasswordState::ForcedAbort(failcount) => {
xous::return_scalar2(msg.sender, 2, failcount.min(u32::MAX as u64) as usize).expect("couldn't return scalar");
}
// user aborted procedure
_ => xous::return_scalar(msg.sender, 0).expect("couldn't return scalar"),
_ => xous::return_scalar2(msg.sender, 1, 0).expect("couldn't return scalar"),
}
initial_heap = heap_usage();
latest_heap = initial_heap;
@ -1577,23 +1581,39 @@ fn ensure_password(modals: &modals::Modals, pddb_os: &mut PddbOs, _pw_cid: xous:
PasswordState::Correct => {
return PasswordState::Correct
}
PasswordState::Incorrect => {
PasswordState::Incorrect(failcount) => {
pddb_os.clear_password(); // clear the bad password entry
log::info!("{}PDDB.BADPW,{}", xous::BOOKEND_START, xous::BOOKEND_END);
// check if the user wants to re-try or not.
modals.add_list_item(t!("pddb.yes", xous::LANG)).expect("couldn't build radio item list");
modals.add_list_item(t!("pddb.no", xous::LANG)).expect("couldn't build radio item list");
match modals.get_radiobutton(t!("pddb.badpass", xous::LANG)) {
Ok(response) => {
if response.as_str() == t!("pddb.yes", xous::LANG) {
continue;
} else if response.as_str() == t!("pddb.no", xous::LANG) {
return PasswordState::Incorrect;
} else {
panic!("Got unexpected return from radiobutton");
if failcount % 3 == 0 {
// every three failures kick the failure back up the stack
return PasswordState::ForcedAbort(failcount);
} else {
// check if the user wants to re-try or not.
modals.add_list_item(t!("pddb.yes", xous::LANG)).expect("couldn't build radio item list");
modals.add_list_item(t!("pddb.no", xous::LANG)).expect("couldn't build radio item list");
let fail_string = format!(
"{}\n{}",
t!("pddb.badpass", xous::LANG),
t!("pddb.failed_attempts", xous::LANG)
.replace("{fails}", &failcount.to_string())
);
let prompt = if failcount == 0 {
t!("pddb.badpass", xous::LANG)
} else {
&fail_string
};
match modals.get_radiobutton(prompt) {
Ok(response) => {
if response.as_str() == t!("pddb.yes", xous::LANG) {
continue;
} else if response.as_str() == t!("pddb.no", xous::LANG) {
return PasswordState::Incorrect(failcount);
} else {
panic!("Got unexpected return from radiobutton");
}
}
_ => panic!("get_radiobutton failed"),
}
_ => panic!("get_radiobutton failed"),
}
}
PasswordState::Uninit => {
@ -1611,6 +1631,7 @@ fn ensure_password(modals: &modals::Modals, pddb_os: &mut PddbOs, _pw_cid: xous:
}
return PasswordState::Uninit;
}
PasswordState::ForcedAbort(_) => panic!("ForcedAbort is not expecetd from try_login()"),
}
}
}

View file

@ -17,6 +17,12 @@
"zh": "重新生成密钥操作失败,请重新启动并重试。\n原因:",
"en-tts": "Rekey operation failed, please reboot and try again.\nReason: "
},
"login.fail": {
"ja": "⚠ログインに {fails} 回失敗した後、システムが中断されました",
"en": "⚠System was suspended after {fails} failed login attempts.",
"zh": "⚠{fails}次登录尝试失败后系统被挂起",
"en-tts": "⚠System was suspended after {fails} failed login attempts."
},
"socup.ignore": {
"ja": "この更新を無視する",
"en": "Ignore this update",
@ -247,10 +253,10 @@
"en-tts": "Shutdown"
},
"mainmenu.shutdown_confirm": {
"en": "Confirm shutdown:\nDevice will reamin off until a charging cable is plugged in. Proceed?",
"ja": "シャットダウンの確認:\n充電ケーブルが差し込まれるまで、デバイスはオフのままになります。続行しますか?",
"zh": "确认关机:\n设备将保持关闭状态,直到插入充电线。继续?",
"en-tts": "Confirm shutdown:\nDevice will reamin off until a charging cable is plugged in. Proceed?"
"en": "Confirm shutdown:\n🔌Device will reamin off until a charging cable is plugged in. Proceed?",
"ja": "シャットダウンの確認:\n🔌充電ケーブルが差し込まれるまで、デバイスはオフのままになります。続行しますか?",
"zh": "确认关机:\n🔌设备将保持关闭状态,直到插入充电线。继续?",
"en-tts": "Confirm shutdown:\n🔌Device will reamin off until a charging cable is plugged in. Proceed?"
},
"mainmenu.reboot": {
"en": "Reboot",

View file

@ -638,7 +638,33 @@ fn wrapped_main() -> ! {
move || {
let tt = ticktimer_server::Ticktimer::new().unwrap();
tt.sleep_ms(2000).unwrap(); // a brief pause, to allow the other startup bits to finish running
pddb::Pddb::new().try_mount();
loop {
let (no_retry_failure, count) = pddb::Pddb::new().try_mount();
if no_retry_failure {
// this includes both successfully mounted, and user abort of mount attempt
break;
} else {
// this indicates system was guttered due to a retry failure
let xns = xous_names::XousNames::new().unwrap();
let susres = susres::Susres::new_without_hook(&xns).unwrap();
let llio = llio::Llio::new(&xns);
if ((llio.adc_vbus().unwrap() as u32) * 503) < 150_000 {
// try to force suspend if possible, so that users who are just playing around with
// the device don't run the battery down accidentally.
susres.initiate_suspend().ok();
tt.sleep_ms(1000).unwrap();
let modals = modals::Modals::new(&xns).unwrap();
modals.show_notification(
&t!("login.fail", xous::LANG).replace("{fails}", &count.to_string()),
None
).ok();
} else {
// otherwise force a reboot cycle to slow down guessers
susres.reboot(true);
tt.sleep_ms(5000).unwrap();
}
}
}
}
});