diff --git a/services/pddb/locales/i18n.json b/services/pddb/locales/i18n.json index 023e8d762..3d32a95f1 100644 --- a/services/pddb/locales/i18n.json +++ b/services/pddb/locales/i18n.json @@ -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": "任意キーを押して、パスワードを再入力してセットアップを確認してください。", diff --git a/services/pddb/src/backend/hw.rs b/services/pddb/src/backend/hw.rs index aad430e1e..7f5d5091a 100644 --- a/services/pddb/src/backend/hw.rs +++ b/services/pddb/src/backend/hw.rs @@ -166,6 +166,8 @@ pub(crate) struct PddbOs { entropy: Rc>, /// 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::, } @@ -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) } } } diff --git a/services/pddb/src/lib.rs b/services/pddb/src/lib.rs index 0dbaba3db..d25acffd1 100644 --- a/services/pddb/src/lib.rs +++ b/services/pddb/src/lib.rs @@ -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} diff --git a/services/pddb/src/main.rs b/services/pddb/src/main.rs index 2a68d59e5..560da6bc2 100644 --- a/services/pddb/src/main.rs +++ b/services/pddb/src/main.rs @@ -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()"), } } } diff --git a/services/status/locales/i18n.json b/services/status/locales/i18n.json index 8a8d21e59..1ee454d8e 100644 --- a/services/status/locales/i18n.json +++ b/services/status/locales/i18n.json @@ -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", diff --git a/services/status/src/main.rs b/services/status/src/main.rs index bab2e6aec..4ea619b45 100644 --- a/services/status/src/main.rs +++ b/services/status/src/main.rs @@ -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(); + } + } + } } });